Class: DecisionAgent::Testing::BatchTestImporter

Inherits:
Object
  • Object
show all
Defined in:
lib/decision_agent/testing/batch_test_importer.rb

Overview

Imports test scenarios from CSV or Excel files

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeBatchTestImporter

Returns a new instance of BatchTestImporter.



12
13
14
15
# File 'lib/decision_agent/testing/batch_test_importer.rb', line 12

def initialize
  @errors = []
  @warnings = []
end

Instance Attribute Details

#errorsObject (readonly)

Returns the value of attribute errors.



10
11
12
# File 'lib/decision_agent/testing/batch_test_importer.rb', line 10

def errors
  @errors
end

#warningsObject (readonly)

Returns the value of attribute warnings.



10
11
12
# File 'lib/decision_agent/testing/batch_test_importer.rb', line 10

def warnings
  @warnings
end

Instance Method Details

#import_csv(file_path, options = {}) ⇒ Array<TestScenario>

Import test scenarios from a CSV file

Parameters:

  • file_path (String)

    Path to CSV file

  • options (Hash) (defaults to: {})

    Import options

    • :context_columns [Array] Column names to use as context (default: all except id, expected_decision, expected_confidence)
    • :id_column [String] Column name for test ID (default: 'id')
    • :expected_decision_column [String] Column name for expected decision (default: 'expected_decision')
    • :expected_confidence_column [String] Column name for expected confidence (default: 'expected_confidence')
    • :skip_header [Boolean] Skip first row (default: true)
    • :progress_callback [Proc] Callback for progress updates (called with { processed: N, total: M, percentage: X })

Returns:

Raises:



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/decision_agent/testing/batch_test_importer.rb', line 27

def import_csv(file_path, options = {})
  @errors = []
  @warnings = []

  options = {
    context_columns: nil,
    id_column: "id",
    expected_decision_column: "expected_decision",
    expected_confidence_column: "expected_confidence",
    skip_header: true,
    progress_callback: nil
  }.merge(options)

  scenarios = []
  row_number = 0

  # Count total rows for progress tracking (if callback provided)
  total_rows = nil
  if options[:progress_callback]
    begin
      total_rows = count_csv_rows(file_path, options[:skip_header])
    rescue StandardError => e
      # If counting fails, continue without progress tracking
      warn "[DecisionAgent] Failed to count CSV rows: #{e.message}"
      total_rows = nil
    end
  end

  if options[:skip_header]
    CSV.foreach(file_path, headers: true) do |row|
      row_number += 1
      begin
        scenario = parse_csv_row(row, row_number, options)
        scenarios << scenario if scenario
      rescue StandardError => e
        @errors << "Row #{row_number}: #{e.message}"
      end

      # Call progress callback if provided
      if options[:progress_callback] && total_rows
        options[:progress_callback].call(
          processed: row_number,
          total: total_rows,
          percentage: (row_number.to_f / total_rows * 100).round(2)
        )
      end
    end
  else
    # Without headers, we need to use numeric indices
    # This is a simplified case - in practice, users should provide headers
    CSV.foreach(file_path, headers: false) do |row|
      row_number += 1
      begin
        # Convert array to hash with numeric keys
        row_hash = row.each_with_index.to_h { |val, idx| [idx.to_s, val] }
        scenario = parse_hash_row(row_hash, row_number, options.merge(id_column: "0"))
        scenarios << scenario if scenario
      rescue StandardError => e
        @errors << "Row #{row_number}: #{e.message}"
      end

      # Call progress callback if provided
      if options[:progress_callback] && total_rows
        options[:progress_callback].call(
          processed: row_number,
          total: total_rows,
          percentage: (row_number.to_f / total_rows * 100).round(2)
        )
      end
    end
  end

  raise ImportError, "Failed to import: #{@errors.join('; ')}" if @errors.any? && scenarios.empty?

  scenarios
end

#import_excel(file_path, options = {}) ⇒ Array<TestScenario>

Import test scenarios from an Excel file (.xlsx, .xls)

Parameters:

  • file_path (String)

    Path to Excel file

  • options (Hash) (defaults to: {})

    Import options (same as import_csv)

    • :sheet [String|Integer] Sheet name or index (default: first sheet)
    • :progress_callback [Proc] Callback for progress updates

Returns:



110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'lib/decision_agent/testing/batch_test_importer.rb', line 110

def import_excel(file_path, options = {})
  @errors = []
  @warnings = []

  options = {
    context_columns: nil,
    id_column: "id",
    expected_decision_column: "expected_decision",
    expected_confidence_column: "expected_confidence",
    skip_header: true,
    sheet: 0,
    progress_callback: nil
  }.merge(options)

  begin
    spreadsheet = Roo::Spreadsheet.open(file_path)

    # Select sheet by name or index
    spreadsheet.default_sheet = if options[:sheet].is_a?(Integer)
                                  spreadsheet.sheets[options[:sheet]] || spreadsheet.sheets.first
                                elsif options[:sheet].is_a?(String)
                                  options[:sheet]
                                else
                                  spreadsheet.sheets.first
                                end

    scenarios = []
    row_number = 0

    # Get total rows for progress tracking
    first_row = spreadsheet.first_row
    last_row = spreadsheet.last_row
    return [] unless first_row && last_row && first_row <= last_row

    total_rows = last_row - first_row + 1
    total_rows -= 1 if options[:skip_header] && total_rows.positive?

    # Read header row if skip_header is true
    header_row = nil
    if options[:skip_header] && first_row
      header_data = spreadsheet.row(first_row)
      # Handle different return types from Roo (including Proc/lambda)
      header_row = if header_data.is_a?(Array)
                     header_data
                   elsif header_data.is_a?(Proc)
                     header_data.call
                   elsif header_data.respond_to?(:to_a)
                     header_data.to_a
                   elsif header_data.respond_to?(:to_ary)
                     header_data.to_ary
                   else
                     # Fallback: try to convert to array
                     [header_data].flatten
                   end
      row_number = 1 # Start from row 2 (after header)
    end

    # Process data rows
    start_row = options[:skip_header] ? (first_row + 1) : first_row
    return [] unless start_row && last_row && start_row <= last_row

    (start_row..last_row).each do |row_index|
      row_number += 1
      row_data_raw = spreadsheet.row(row_index)
      # Handle different return types from Roo (including Proc/lambda)
      row_data = if row_data_raw.is_a?(Array)
                   row_data_raw
                 elsif row_data_raw.is_a?(Proc)
                   row_data_raw.call
                 elsif row_data_raw.respond_to?(:to_a)
                   row_data_raw.to_a
                 elsif row_data_raw.respond_to?(:to_ary)
                   row_data_raw.to_ary
                 else
                   # Fallback: try to convert to array
                   [row_data_raw].flatten
                 end

      begin
        # Convert row data to hash using headers
        row_hash = if header_row
                     header_row.each_with_index.to_h { |header, idx| [header.to_s, row_data[idx]] }
                   else
                     # Use numeric indices if no headers
                     row_data.each_with_index.to_h { |val, idx| [idx.to_s, val] }
                   end

        scenario = parse_hash_row(row_hash, row_number, options)
        scenarios << scenario if scenario
      rescue StandardError => e
        @errors << "Row #{row_number}: #{e.message}"
      end

      # Call progress callback if provided
      next unless options[:progress_callback] && total_rows.positive?

      processed = row_number - (options[:skip_header] ? 1 : 0)
      options[:progress_callback].call(
        processed: processed,
        total: total_rows,
        percentage: (processed.to_f / total_rows * 100).round(2)
      )
    end

    raise ImportError, "Failed to import: #{@errors.join('; ')}" if @errors.any? && scenarios.empty?

    scenarios
  rescue Roo::HeaderRowNotFoundError => e
    raise ImportError, "Excel file has no header row: #{e.message}"
  rescue StandardError => e
    raise ImportError, "Failed to read Excel file: #{e.message}"
  end
end

#import_from_array(data, options = {}) ⇒ Array<TestScenario>

Import test scenarios from an array of hashes (for programmatic use)

Parameters:

  • data (Array<Hash>)

    Array of hashes with test data

  • options (Hash) (defaults to: {})

    Same as import_csv

Returns:

Raises:



228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
# File 'lib/decision_agent/testing/batch_test_importer.rb', line 228

def import_from_array(data, options = {})
  @errors = []
  @warnings = []

  options = {
    id_column: "id",
    expected_decision_column: "expected_decision",
    expected_confidence_column: "expected_confidence"
  }.merge(options)

  scenarios = []
  row_number = 0

  data.each do |row|
    row_number += 1
    begin
      scenario = parse_hash_row(row, row_number, options)
      scenarios << scenario if scenario
    rescue StandardError => e
      @errors << "Row #{row_number}: #{e.message}"
    end
  end

  raise ImportError, "Failed to import: #{@errors.join('; ')}" if @errors.any? && scenarios.empty?

  scenarios
end