aia 1.1.1 → 2.0.0.0.pre.beta2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. checksums.yaml +4 -4
  2. data/.envrc +9 -1
  3. data/.loki +11 -0
  4. data/.reek.yml +160 -0
  5. data/.rubocop.yml +116 -0
  6. data/.rubocop_strict.yml +15 -0
  7. data/.version +1 -1
  8. data/CHANGELOG.md +269 -55
  9. data/IMPLEMENTATION_PLAN.md +506 -0
  10. data/README.md +267 -239
  11. data/Rakefile +5 -5
  12. data/_typos.toml +10 -0
  13. data/aia.gemspec +92 -0
  14. data/architecture_review.md +314 -0
  15. data/bin/aia +16 -0
  16. data/config/aia.yml +13 -0
  17. data/docs/AGENTS.md +40 -0
  18. data/docs/advanced-prompting.md +67 -3
  19. data/docs/cli-reference.md +312 -56
  20. data/docs/configuration.md +130 -19
  21. data/docs/contributing.md +56 -2
  22. data/docs/directives-reference.md +593 -78
  23. data/docs/faq.md +85 -3
  24. data/docs/guides/available-models.md +1 -1
  25. data/docs/guides/basic-usage.md +6 -6
  26. data/docs/guides/chat.md +40 -16
  27. data/docs/guides/crew.md +239 -0
  28. data/docs/guides/executable-prompts.md +1 -1
  29. data/docs/guides/index.md +1 -0
  30. data/docs/guides/models.md +15 -0
  31. data/docs/index.md +29 -2
  32. data/docs/installation.md +44 -17
  33. data/docs/mcp-integration.md +40 -0
  34. data/docs/prompt_management.md +85 -86
  35. data/docs/security.md +47 -0
  36. data/docs/special_projects_guide.md +386 -0
  37. data/docs/tools-and-mcp-examples.md +23 -0
  38. data/docs/workflows-and-pipelines.md +84 -7
  39. data/examples/.gitignore +1 -0
  40. data/examples/00_setup_aia.sh +27 -44
  41. data/examples/11_multi_model.sh +4 -14
  42. data/examples/12_token_usage.sh +3 -12
  43. data/examples/18_tools.sh +10 -2
  44. data/examples/22_chat_mode.sh +0 -10
  45. data/examples/23_verify.sh +139 -0
  46. data/examples/24_decompose.sh +139 -0
  47. data/examples/25_spawn.sh +139 -0
  48. data/examples/26_debate.sh +97 -0
  49. data/examples/27_mention_routing.sh +157 -0
  50. data/examples/28_model_switching.sh +106 -0
  51. data/examples/29_agent_harness.sh +177 -0
  52. data/examples/README.md +65 -0
  53. data/examples/advanced_multi_robot_capabilities_without_examples.md +106 -0
  54. data/examples/aia_config.yml +1 -1
  55. data/examples/aia_config_orchestrator.yml +45 -0
  56. data/examples/common.sh +18 -6
  57. data/examples/context/tech_stack.md +2 -2
  58. data/examples/prompts_dir/roles/orchestrator.md +21 -0
  59. data/examples/requirements/sinatra_taskflow_app.md +139 -0
  60. data/examples/rules/01_classify_ruby.rb +16 -0
  61. data/examples/rules/02_prefer_claude_for_code.rb +19 -0
  62. data/examples/rules/03_gate_prompt_length.rb +19 -0
  63. data/examples/rules/04_tool_selection.rb +41 -0
  64. data/examples/rules/README.md +30 -0
  65. data/examples/run_all.sh +48 -15
  66. data/examples/tools/word_count_tool.rb +1 -1
  67. data/lib/AGENTS.md +57 -0
  68. data/lib/aia/chat_loop.rb +263 -167
  69. data/lib/aia/config/cli_parser.rb +217 -145
  70. data/lib/aia/config/defaults.yml +62 -33
  71. data/lib/aia/config/mcp_parser.rb +52 -51
  72. data/lib/aia/config/model_spec.rb +34 -2
  73. data/lib/aia/config/validator.rb +171 -216
  74. data/lib/aia/config.rb +111 -145
  75. data/lib/aia/content_extractor.rb +155 -0
  76. data/lib/aia/cost_calculator.rb +39 -0
  77. data/lib/aia/crew.rb +164 -0
  78. data/lib/aia/debate_handler.rb +174 -0
  79. data/lib/aia/delegate_handler.rb +116 -0
  80. data/lib/aia/directive.rb +43 -26
  81. data/lib/aia/directive_processor.rb +16 -7
  82. data/lib/aia/directives/configuration_directives.rb +214 -60
  83. data/lib/aia/directives/context_directives.rb +67 -52
  84. data/lib/aia/directives/execution_directives.rb +141 -4
  85. data/lib/aia/directives/model_directives.rb +163 -141
  86. data/lib/aia/directives/trakflow_directives.rb +62 -0
  87. data/lib/aia/directives/utility_directives.rb +227 -30
  88. data/lib/aia/directives/web_and_file_directives.rb +126 -77
  89. data/lib/aia/errors.rb +15 -0
  90. data/lib/aia/fact_asserter.rb +27 -0
  91. data/lib/aia/fzf.rb +9 -31
  92. data/lib/aia/handler_context.rb +17 -0
  93. data/lib/aia/handler_protocol.rb +19 -0
  94. data/lib/aia/history_transfer.rb +55 -0
  95. data/lib/aia/input_collector.rb +3 -3
  96. data/lib/aia/layered_orchestrator.rb +471 -0
  97. data/lib/aia/logger.rb +45 -25
  98. data/lib/aia/mcp_config_normalizer.rb +35 -0
  99. data/lib/aia/mcp_connection_manager.rb +315 -0
  100. data/lib/aia/mcp_discovery.rb +44 -0
  101. data/lib/aia/mcp_grouper.rb +33 -0
  102. data/lib/aia/mcp_server_config.rb +30 -0
  103. data/lib/aia/mcp_utility.rb +60 -0
  104. data/lib/aia/mention_router.rb +217 -0
  105. data/lib/aia/model_alias_registry.rb +97 -0
  106. data/lib/aia/model_switch_handler.rb +100 -0
  107. data/lib/aia/network_builder.rb +160 -0
  108. data/lib/aia/network_memory_manager.rb +55 -0
  109. data/lib/aia/patches/ruby_llm_streaming_error.rb +43 -0
  110. data/lib/aia/patches/ruby_llm_tool_error.rb +96 -0
  111. data/lib/aia/pipeline_orchestrator.rb +272 -0
  112. data/lib/aia/plugin_loader.rb +170 -0
  113. data/lib/aia/plugin_monitor.rb +211 -0
  114. data/lib/aia/prompt_decomposer.rb +159 -0
  115. data/lib/aia/prompt_handler.rb +56 -82
  116. data/lib/aia/robot_builder.rb +51 -0
  117. data/lib/aia/robot_factory.rb +338 -0
  118. data/lib/aia/robot_namer.rb +110 -0
  119. data/lib/aia/session.rb +87 -17
  120. data/lib/aia/session_tracker.rb +207 -0
  121. data/lib/aia/similarity_scorer.rb +41 -0
  122. data/lib/aia/skill_utils.rb +105 -1
  123. data/lib/aia/spawn_handler.rb +129 -0
  124. data/lib/aia/spawn_spec_parser.rb +65 -0
  125. data/lib/aia/special_mode_handler.rb +322 -0
  126. data/lib/aia/speech.rb +67 -0
  127. data/lib/aia/startup_coordinator.rb +151 -0
  128. data/lib/aia/streaming_runner.rb +172 -0
  129. data/lib/aia/system_prompt_assembler.rb +92 -0
  130. data/lib/aia/task_coordinator.rb +207 -0
  131. data/lib/aia/task_decomposer.rb +57 -0
  132. data/lib/aia/task_executor.rb +51 -0
  133. data/lib/aia/tfidf_math.rb +27 -0
  134. data/lib/aia/timing.rb +15 -0
  135. data/lib/aia/tool_filter/tfidf.rb +116 -0
  136. data/lib/aia/tool_filter/wordnet_expander.rb +127 -0
  137. data/lib/aia/tool_filter.rb +83 -0
  138. data/lib/aia/tool_filter_registry.rb +30 -0
  139. data/lib/aia/tool_filter_strategy.rb +146 -0
  140. data/lib/aia/tool_introspection.rb +17 -0
  141. data/lib/aia/tool_loader.rb +216 -0
  142. data/lib/aia/tool_utility.rb +30 -0
  143. data/lib/aia/tools/delegate_to_foreman_tool.rb +70 -0
  144. data/lib/aia/tools/recruit_robot_tool.rb +60 -0
  145. data/lib/aia/tools/reskill_robot_tool.rb +44 -0
  146. data/lib/aia/tools/task_board_tool.rb +115 -0
  147. data/lib/aia/trakflow_bridge.rb +175 -0
  148. data/lib/aia/turn_state.rb +95 -0
  149. data/lib/aia/ui_presenter.rb +182 -206
  150. data/lib/aia/utility.rb +136 -87
  151. data/lib/aia/{history_manager.rb → variable_input_collector.rb} +9 -9
  152. data/lib/aia/verification_network.rb +57 -0
  153. data/lib/aia.rb +124 -63
  154. data/mkdocs.yml +1 -0
  155. metadata +187 -58
  156. data/justfile +0 -215
  157. data/lib/aia/adapter/chat_execution.rb +0 -242
  158. data/lib/aia/adapter/error_handler.rb +0 -68
  159. data/lib/aia/adapter/gem_activator.rb +0 -57
  160. data/lib/aia/adapter/mcp_connector.rb +0 -274
  161. data/lib/aia/adapter/modality_handlers.rb +0 -167
  162. data/lib/aia/adapter/model_registry.rb +0 -81
  163. data/lib/aia/adapter/multi_model_chat.rb +0 -218
  164. data/lib/aia/adapter/provider_configurator.rb +0 -59
  165. data/lib/aia/adapter/tool_filter.rb +0 -85
  166. data/lib/aia/adapter/tool_loader.rb +0 -90
  167. data/lib/aia/chat_processor_service.rb +0 -178
  168. data/lib/aia/prompt_pipeline.rb +0 -183
  169. data/lib/aia/ruby_llm_adapter.rb +0 -95
  170. data/lib/extensions/openstruct_merge.rb +0 -48
  171. data/lib/extensions/ruby_llm/.irbrc +0 -56
  172. data/lib/extensions/ruby_llm/modalities.rb +0 -36
  173. data/lib/extensions/ruby_llm/provider_fix.rb +0 -79
  174. data/lib/refinements/string.rb +0 -16
  175. data/main.just +0 -76
@@ -1,12 +1,17 @@
1
1
  # lib/aia/ui_presenter.rb
2
2
 
3
3
  require 'tty-screen'
4
+ require 'tty-spinner'
5
+ require 'tty-table'
4
6
  require 'reline'
7
+ require 'fileutils'
5
8
 
6
9
  module AIA
10
+ # :reek:TooManyMethods -- one small builder per table/report section keeps each display testable in isolation
7
11
  class UIPresenter
8
- USER_PROMPT = "Follow up (cntl-D or 'exit' to end) #=> "
9
-
12
+ USER_PROMPT = "Follow up (cntl-D or 'exit' to end) #=> ".freeze
13
+ HISTORY_FILE = File.join(Dir.home, '.config', 'aia', 'chat_history')
14
+ MAX_HISTORY = 50
10
15
 
11
16
  def initialize
12
17
  @terminal_width = TTY::Screen.width
@@ -16,27 +21,20 @@ module AIA
16
21
  puts "#{'═' * @terminal_width}\n"
17
22
  end
18
23
 
19
-
20
24
  def display_ai_response(response)
21
25
  puts "\nAI: "
22
26
  format_chat_response(response)
23
-
24
- out_file = AIA.config.output.file
25
- if out_file && !out_file.nil?
26
- File.open(out_file, 'a') do |file|
27
- file.puts "\nAI: "
28
- format_chat_response(response, file)
29
- end
30
- end
31
27
  end
32
28
 
33
-
29
+ # :reek:TooManyStatements -- line-by-line rendering with a code-fence state machine; the states share locals
34
30
  def format_chat_response(response, output = $stdout)
35
31
  indent = ' '
36
32
 
37
33
  # Convert RubyLLM::Message to string if necessary
38
- response_text = if response.is_a?(RubyLLM::Message)
34
+ response_text = if defined?(RubyLLM::Message) && response.is_a?(RubyLLM::Message)
39
35
  response.content.to_s
36
+ elsif response.respond_to?(:reply)
37
+ response.reply.to_s
40
38
  elsif response.respond_to?(:to_s)
41
39
  response.to_s
42
40
  else
@@ -67,17 +65,14 @@ module AIA
67
65
  end
68
66
  end
69
67
 
70
-
71
68
  def display_separator
72
69
  puts "\n#{'─' * @terminal_width}"
73
70
  end
74
71
 
75
-
76
72
  def display_chat_end
77
73
  puts "\nChat session ended."
78
74
  end
79
75
 
80
-
81
76
  # This is the follow up question in a chat session
82
77
  def ask_question
83
78
  puts USER_PROMPT
@@ -85,24 +80,34 @@ module AIA
85
80
  begin
86
81
  input = Reline.readline('', true)
87
82
  return nil if input.nil? # Handle Ctrl+D
88
- Reline::HISTORY << input unless input.strip.empty?
83
+ save_chat_history unless input.strip.empty?
89
84
  input
90
85
  rescue Interrupt
91
86
  puts "\nChat session interrupted."
92
- return 'exit'
87
+ 'exit'
93
88
  end
94
89
  end
95
90
 
91
+ # Load persistent chat history into Reline
92
+ def load_chat_history
93
+ Reline::HISTORY.clear
94
+ history_file = chat_history_file
95
+ return unless history_file && File.exist?(history_file)
96
+
97
+ lines = File.readlines(history_file, chomp: true).last(MAX_HISTORY)
98
+ lines.each { |line| Reline::HISTORY << line }
99
+ end
100
+
96
101
  def display_info(message)
97
- puts "\n#{message}"
102
+ $stderr.puts "\n#{message}"
98
103
  end
99
104
 
100
105
  def display_error(message)
101
- warn "ERROR: #{message}"
106
+ $stderr.puts "\n❌ ERROR: #{message}\n"
102
107
  end
103
108
 
104
109
  def display_warning(message)
105
- warn "WARNING: #{message}"
110
+ $stderr.puts "\n⚠ WARNING: #{message}\n"
106
111
  end
107
112
 
108
113
  def with_spinner(message = "Processing", operation_type = nil)
@@ -118,213 +123,184 @@ module AIA
118
123
  result
119
124
  end
120
125
 
126
+ # :reek:TooManyStatements -- one metrics table with header/row/alignment variants for cost vs no-cost display
121
127
  def display_token_metrics(metrics)
122
128
  return unless metrics
123
-
124
- output_lines = []
125
- output_lines << "═" * 55
126
- output_lines << "Model: #{metrics[:model_id]}"
127
-
129
+
130
+ model_id = metrics[:model_id] || 'unknown'
131
+ input_tokens = metrics[:input_tokens] || 0
132
+ output_tokens = metrics[:output_tokens] || 0
133
+ total_tokens = input_tokens + output_tokens
134
+ time_str = format_elapsed(metrics[:elapsed])
135
+
128
136
  if AIA.config.flags.cost
129
- output_lines.concat(format_metrics_with_cost(metrics))
130
- else
131
- output_lines.concat(format_metrics_basic(metrics))
132
- end
133
-
134
- output_lines << "═" * 55
135
-
136
- # Output to STDOUT
137
- output_lines.each { |line| puts line }
138
-
139
- # Also write to file if configured
140
- out_file = AIA.config.output.file
141
- if out_file && !out_file.nil?
142
- File.open(out_file, 'a') do |file|
143
- output_lines.each { |line| file.puts line }
137
+ cost_data = calculate_cost(metrics)
138
+ if cost_data[:available]
139
+ header = %w[Model Input Output Total Cost x1000 Time]
140
+ row = [
141
+ model_id,
142
+ input_tokens, output_tokens, total_tokens,
143
+ "$#{'%.5f' % cost_data[:total_cost]}",
144
+ "$#{'%.2f' % (cost_data[:total_cost] * 1000)}",
145
+ time_str
146
+ ]
147
+ alignments = %i[left right right right right right right]
148
+ else
149
+ header = %w[Model Input Output Total Cost Time]
150
+ row = [model_id, input_tokens, output_tokens, total_tokens, "N/A", time_str]
151
+ alignments = %i[left right right right right right]
144
152
  end
153
+ else
154
+ header = %w[Model Input Output Total Time]
155
+ row = [model_id, input_tokens, output_tokens, total_tokens, time_str]
156
+ alignments = %i[left right right right right]
145
157
  end
158
+
159
+ table = TTY::Table.new(header, [row])
160
+ rendered = table.render(:unicode, resize: true, alignments: alignments, padding: [0, 1])
161
+
162
+ puts rendered
163
+ write_to_output_file(rendered)
146
164
  end
147
165
 
166
+ # :reek:TooManyStatements -- table assembly: rows, totals, render, output; each piece delegated to a builder
148
167
  def display_multi_model_metrics(metrics_list)
149
168
  return unless metrics_list && !metrics_list.empty?
150
-
151
- output_lines = []
152
-
153
- # Determine table width based on whether costs are shown
154
- if AIA.config.flags.cost
155
- table_width = 80
156
- else
157
- table_width = 60
158
- end
159
-
160
- output_lines << "═" * table_width
161
- output_lines << "Multi-Model Token Usage"
162
- output_lines << "─" * table_width
163
-
164
- # Build header row
165
- if AIA.config.flags.cost
166
- output_lines << sprintf("%-20s %10s %10s %10s %12s %10s",
167
- "Model", "Input", "Output", "Total", "Cost", "x1000")
168
- output_lines << "─" * table_width
169
- else
170
- output_lines << sprintf("%-20s %10s %10s %10s",
171
- "Model", "Input", "Output", "Total")
172
- output_lines << "─" * table_width
173
- end
174
-
175
- # Process each model
176
- total_input = 0
177
- total_output = 0
178
- total_cost = 0.0
179
-
180
- metrics_list.each do |metrics|
181
- # Use display_name if available (includes role), otherwise fall back to model_id
182
- model_name = metrics[:display_name] || metrics[:model_id]
183
- # Truncate model name if too long
184
- model_name = model_name[0..17] + ".." if model_name.to_s.length > 19
185
-
186
- input_tokens = metrics[:input_tokens] || 0
187
- output_tokens = metrics[:output_tokens] || 0
188
- total_tokens = input_tokens + output_tokens
189
-
190
- if AIA.config.flags.cost
191
- cost_data = calculate_cost(metrics)
192
- if cost_data[:available]
193
- cost_str = "$#{'%.5f' % cost_data[:total_cost]}"
194
- x1000_str = "$#{'%.2f' % (cost_data[:total_cost] * 1000)}"
195
- total_cost += cost_data[:total_cost]
196
- else
197
- cost_str = "N/A"
198
- x1000_str = "N/A"
199
- end
200
-
201
- output_lines << sprintf("%-20s %10d %10d %10d %12s %10s",
202
- model_name, input_tokens, output_tokens, total_tokens, cost_str, x1000_str)
203
- else
204
- output_lines << sprintf("%-20s %10d %10d %10d",
205
- model_name, input_tokens, output_tokens, total_tokens)
206
- end
207
-
208
- total_input += input_tokens
209
- total_output += output_tokens
210
- end
211
-
212
- # Display totals row
213
- output_lines << "─" * table_width
214
- total_tokens = total_input + total_output
215
-
216
- if AIA.config.flags.cost && total_cost > 0
217
- cost_str = "$#{'%.5f' % total_cost}"
218
- x1000_str = "$#{'%.2f' % (total_cost * 1000)}"
219
- output_lines << sprintf("%-20s %10d %10d %10d %12s %10s",
220
- "TOTAL", total_input, total_output, total_tokens, cost_str, x1000_str)
221
- else
222
- output_lines << sprintf("%-20s %10d %10d %10d",
223
- "TOTAL", total_input, total_output, total_tokens)
224
- end
225
-
226
- output_lines << "═" * table_width
227
-
228
- # Output to STDOUT
229
- output_lines.each { |line| puts line }
230
-
231
- # Also write to file if configured
232
- out_file = AIA.config.output.file
233
- if out_file && !out_file.nil?
234
- File.open(out_file, 'a') do |file|
235
- output_lines.each { |line| file.puts line }
236
- end
237
- end
169
+
170
+ show_cost = AIA.config.flags.cost
171
+ show_similarity = metrics_list.any? { |m| m.key?(:similarity) }
172
+ totals = MultiModelTotals.new(0, 0, 0.0, 0.0)
173
+
174
+ rows = metrics_list.map { |metrics| multi_model_row(metrics, totals, show_cost, show_similarity) }
175
+ rows << :separator
176
+ rows << multi_model_totals_row(totals, show_cost, show_similarity)
177
+
178
+ table = TTY::Table.new(multi_model_header(show_cost, show_similarity), rows)
179
+ rendered = table.render(:unicode, resize: true,
180
+ alignments: multi_model_alignments(show_cost, show_similarity),
181
+ padding: [0, 1])
182
+
183
+ puts "\nMulti-Model Token Usage"
184
+ puts rendered
185
+ write_to_output_file("Multi-Model Token Usage\n#{rendered}")
238
186
  end
239
187
 
240
188
  private
241
-
242
- def display_metrics_basic(metrics)
243
- puts "Input tokens: #{metrics[:input_tokens] || 'N/A'}"
244
- puts "Output tokens: #{metrics[:output_tokens] || 'N/A'}"
245
-
246
- if metrics[:input_tokens] && metrics[:output_tokens]
247
- total = metrics[:input_tokens] + metrics[:output_tokens]
248
- puts "Total tokens: #{total}"
249
- end
189
+
190
+ # Running totals accumulated while building multi-model table rows.
191
+ MultiModelTotals = Struct.new(:input, :output, :cost, :max_elapsed)
192
+
193
+ def multi_model_header(show_cost, show_similarity)
194
+ header = %w[Model Input Output Total]
195
+ header += %w[Cost x1000] if show_cost
196
+ header << "Time"
197
+ header << "Sim" if show_similarity
198
+ header
250
199
  end
251
-
252
- def format_metrics_basic(metrics)
253
- lines = []
254
- lines << "Input tokens: #{metrics[:input_tokens] || 'N/A'}"
255
- lines << "Output tokens: #{metrics[:output_tokens] || 'N/A'}"
256
-
257
- if metrics[:input_tokens] && metrics[:output_tokens]
258
- total = metrics[:input_tokens] + metrics[:output_tokens]
259
- lines << "Total tokens: #{total}"
260
- end
261
-
262
- lines
200
+
201
+ def multi_model_row(metrics, totals, show_cost, show_similarity)
202
+ input_tokens = metrics[:input_tokens] || 0
203
+ output_tokens = metrics[:output_tokens] || 0
204
+ elapsed = metrics[:elapsed]
205
+
206
+ totals.input += input_tokens
207
+ totals.output += output_tokens
208
+ totals.max_elapsed = [totals.max_elapsed, elapsed || 0].max
209
+
210
+ row = [(metrics[:model_id] || metrics[:display_name]).to_s,
211
+ input_tokens, output_tokens, input_tokens + output_tokens]
212
+ row += multi_model_cost_cells(metrics, totals) if show_cost
213
+ row << format_elapsed(elapsed)
214
+ row << format_similarity(metrics[:similarity]) if show_similarity
215
+ row
263
216
  end
264
-
265
- def display_metrics_with_cost(metrics)
217
+
218
+ # Cost cells for one row; adds to totals only when pricing is available.
219
+ def multi_model_cost_cells(metrics, totals)
266
220
  cost_data = calculate_cost(metrics)
267
-
268
- if cost_data[:available]
269
- puts "Input tokens: #{metrics[:input_tokens]} ($#{'%.5f' % cost_data[:input_cost]})"
270
- puts "Output tokens: #{metrics[:output_tokens]} ($#{'%.5f' % cost_data[:output_cost]})"
271
- puts "Total cost: $#{'%.5f' % cost_data[:total_cost]}"
272
- puts "Cost x1000: $#{'%.2f' % (cost_data[:total_cost] * 1000)}"
273
- else
274
- puts "Input tokens: #{metrics[:input_tokens] || 'N/A'}"
275
- puts "Output tokens: #{metrics[:output_tokens] || 'N/A'}"
276
- total = (metrics[:input_tokens] || 0) + (metrics[:output_tokens] || 0)
277
- puts "Total tokens: #{total}"
278
- puts "Cost: N/A (pricing not available)"
221
+ return %w[N/A N/A] unless cost_data[:available]
222
+
223
+ cost = cost_data[:total_cost]
224
+ totals.cost += cost
225
+ format_cost_cells(cost)
226
+ end
227
+
228
+ def format_cost_cells(cost)
229
+ ["$#{'%.5f' % cost}", "$#{'%.2f' % (cost * 1000)}"]
230
+ end
231
+
232
+ def multi_model_totals_row(totals, show_cost, show_similarity)
233
+ row = ["TOTAL", totals.input, totals.output, totals.input + totals.output]
234
+ row += totals.cost.positive? ? format_cost_cells(totals.cost) : ["", ""] if show_cost
235
+ row << format_elapsed(totals.max_elapsed)
236
+ row << "" if show_similarity
237
+ row
238
+ end
239
+
240
+ def multi_model_alignments(show_cost, show_similarity)
241
+ alignments = %i[left right right right]
242
+ alignments += %i[right right] if show_cost
243
+ alignments << :right
244
+ alignments << :right if show_similarity
245
+ alignments
246
+ end
247
+
248
+ def save_chat_history
249
+ history_file = chat_history_file
250
+ return unless history_file
251
+ dir = File.dirname(history_file)
252
+ FileUtils.mkdir_p(dir)
253
+
254
+ entries = Reline::HISTORY.to_a.last(MAX_HISTORY)
255
+ File.write(history_file, entries.join("\n") + "\n")
256
+ end
257
+
258
+ def chat_history_file
259
+ config = AIA.config
260
+ if config.respond_to?(:output) && config.output.respond_to?(:history_file)
261
+ hf = config.output.history_file
262
+ return nil if hf == false # --no-history-file disables chat history
263
+ return File.expand_path(hf) if hf
264
+ end
265
+ paths = config.respond_to?(:paths) ? config.paths : nil
266
+ if paths.respond_to?(:aia_dir) && paths.aia_dir
267
+ return File.join(File.expand_path(paths.aia_dir), 'chat_history')
279
268
  end
269
+ HISTORY_FILE
280
270
  end
281
-
282
- def format_metrics_with_cost(metrics)
283
- lines = []
284
- cost_data = calculate_cost(metrics)
285
-
286
- if cost_data[:available]
287
- lines << "Input tokens: #{metrics[:input_tokens]} ($#{'%.5f' % cost_data[:input_cost]})"
288
- lines << "Output tokens: #{metrics[:output_tokens]} ($#{'%.5f' % cost_data[:output_cost]})"
289
- lines << "Total cost: $#{'%.5f' % cost_data[:total_cost]}"
290
- lines << "Cost x1000: $#{'%.2f' % (cost_data[:total_cost] * 1000)}"
271
+
272
+ def format_similarity(score)
273
+ return "ref" if score.nil?
274
+ "%.1f%%" % (score * 100)
275
+ end
276
+
277
+ def format_elapsed(seconds)
278
+ return "" unless seconds
279
+
280
+ if seconds < 60
281
+ "%.1fs" % seconds
291
282
  else
292
- lines << "Input tokens: #{metrics[:input_tokens] || 'N/A'}"
293
- lines << "Output tokens: #{metrics[:output_tokens] || 'N/A'}"
294
- total = (metrics[:input_tokens] || 0) + (metrics[:output_tokens] || 0)
295
- lines << "Total tokens: #{total}"
296
- lines << "Cost: N/A (pricing not available)"
283
+ minutes = (seconds / 60).to_i
284
+ secs = seconds % 60
285
+ "#{minutes}m %04.1fs" % secs
297
286
  end
298
-
299
- lines
300
287
  end
301
-
288
+
289
+ def write_to_output_file(text)
290
+ out_file = AIA.config.output.file
291
+ return unless out_file
292
+
293
+ File.open(out_file, 'a') { |f| f.puts text }
294
+ end
295
+
302
296
  def calculate_cost(metrics)
303
297
  return { available: false } unless metrics[:model_id] && metrics[:input_tokens] && metrics[:output_tokens]
304
-
305
- # Look up model info from RubyLLM
306
- begin
307
- model_info = RubyLLM::Models.find(metrics[:model_id])
308
- return { available: false } unless model_info
309
-
310
- input_price = model_info.input_price_per_million
311
- output_price = model_info.output_price_per_million
312
-
313
- return { available: false } unless input_price && output_price
314
-
315
- input_cost = metrics[:input_tokens] * input_price / 1_000_000.0
316
- output_cost = metrics[:output_tokens] * output_price / 1_000_000.0
317
- total_cost = input_cost + output_cost
318
-
319
- {
320
- available: true,
321
- input_cost: input_cost,
322
- output_cost: output_cost,
323
- total_cost: total_cost
324
- }
325
- rescue StandardError => e
326
- { available: false, error: e.message }
327
- end
298
+
299
+ CostCalculator.calculate(
300
+ model_id: metrics[:model_id],
301
+ input_tokens: metrics[:input_tokens],
302
+ output_tokens: metrics[:output_tokens]
303
+ )
328
304
  end
329
305
  end
330
306
  end