aia 1.1.1 → 2.0.0.0.pre.alpha

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 (169) hide show
  1. checksums.yaml +4 -4
  2. data/.envrc +5 -1
  3. data/.loki +231 -0
  4. data/.quality/flay_baseline.txt +1 -0
  5. data/.quality/flog_baseline.txt +29 -0
  6. data/.quality/reek_baseline.txt +80 -0
  7. data/.rubocop.yml +116 -0
  8. data/.version +1 -1
  9. data/CHANGELOG.md +259 -50
  10. data/IMPLEMENTATION_PLAN.md +506 -0
  11. data/README.md +266 -238
  12. data/Rakefile +118 -5
  13. data/architecture_review.md +314 -0
  14. data/bin/aia +16 -0
  15. data/docs/AGENTS.md +40 -0
  16. data/docs/advanced-prompting.md +67 -3
  17. data/docs/cli-reference.md +312 -56
  18. data/docs/configuration.md +130 -19
  19. data/docs/contributing.md +56 -2
  20. data/docs/directives-reference.md +593 -78
  21. data/docs/faq.md +85 -3
  22. data/docs/guides/available-models.md +1 -1
  23. data/docs/guides/basic-usage.md +6 -6
  24. data/docs/guides/chat.md +40 -16
  25. data/docs/guides/crew.md +239 -0
  26. data/docs/guides/executable-prompts.md +1 -1
  27. data/docs/guides/index.md +1 -0
  28. data/docs/guides/models.md +15 -0
  29. data/docs/index.md +29 -2
  30. data/docs/installation.md +44 -17
  31. data/docs/mcp-integration.md +40 -0
  32. data/docs/prompt_management.md +85 -86
  33. data/docs/security.md +47 -0
  34. data/docs/special_projects_guide.md +386 -0
  35. data/docs/tools-and-mcp-examples.md +23 -0
  36. data/docs/workflows-and-pipelines.md +84 -7
  37. data/examples/.gitignore +1 -0
  38. data/examples/00_setup_aia.sh +27 -44
  39. data/examples/11_multi_model.sh +4 -14
  40. data/examples/12_token_usage.sh +3 -12
  41. data/examples/18_tools.sh +10 -2
  42. data/examples/22_chat_mode.sh +0 -10
  43. data/examples/23_verify.sh +139 -0
  44. data/examples/24_decompose.sh +139 -0
  45. data/examples/25_spawn.sh +139 -0
  46. data/examples/26_debate.sh +97 -0
  47. data/examples/27_mention_routing.sh +157 -0
  48. data/examples/28_model_switching.sh +106 -0
  49. data/examples/29_agent_harness.sh +177 -0
  50. data/examples/README.md +65 -0
  51. data/examples/advanced_multi_robot_capabilities_without_examples.md +106 -0
  52. data/examples/aia_config.yml +1 -1
  53. data/examples/aia_config_orchestrator.yml +45 -0
  54. data/examples/common.sh +18 -6
  55. data/examples/context/tech_stack.md +2 -2
  56. data/examples/prompts_dir/roles/orchestrator.md +21 -0
  57. data/examples/requirements/sinatra_taskflow_app.md +139 -0
  58. data/examples/rules/01_classify_ruby.rb +16 -0
  59. data/examples/rules/02_prefer_claude_for_code.rb +19 -0
  60. data/examples/rules/03_gate_prompt_length.rb +19 -0
  61. data/examples/rules/04_tool_selection.rb +41 -0
  62. data/examples/rules/README.md +30 -0
  63. data/examples/run_all.sh +48 -15
  64. data/examples/tools/word_count_tool.rb +1 -1
  65. data/lib/AGENTS.md +57 -0
  66. data/lib/aia/chat_loop.rb +304 -164
  67. data/lib/aia/config/cli_parser.rb +174 -111
  68. data/lib/aia/config/defaults.yml +62 -33
  69. data/lib/aia/config/mcp_parser.rb +39 -46
  70. data/lib/aia/config/model_spec.rb +34 -2
  71. data/lib/aia/config/validator.rb +108 -142
  72. data/lib/aia/config.rb +110 -145
  73. data/lib/aia/content_extractor.rb +153 -0
  74. data/lib/aia/cost_calculator.rb +38 -0
  75. data/lib/aia/crew.rb +164 -0
  76. data/lib/aia/debate_handler.rb +166 -0
  77. data/lib/aia/delegate_handler.rb +112 -0
  78. data/lib/aia/directive.rb +33 -18
  79. data/lib/aia/directive_processor.rb +16 -7
  80. data/lib/aia/directives/configuration_directives.rb +160 -20
  81. data/lib/aia/directives/context_directives.rb +38 -26
  82. data/lib/aia/directives/execution_directives.rb +136 -4
  83. data/lib/aia/directives/model_directives.rb +76 -34
  84. data/lib/aia/directives/trakflow_directives.rb +44 -0
  85. data/lib/aia/directives/utility_directives.rb +203 -6
  86. data/lib/aia/directives/web_and_file_directives.rb +96 -60
  87. data/lib/aia/errors.rb +15 -0
  88. data/lib/aia/fact_asserter.rb +27 -0
  89. data/lib/aia/fzf.rb +9 -31
  90. data/lib/aia/handler_context.rb +17 -0
  91. data/lib/aia/handler_protocol.rb +19 -0
  92. data/lib/aia/history_transfer.rb +55 -0
  93. data/lib/aia/input_collector.rb +3 -3
  94. data/lib/aia/layered_orchestrator.rb +448 -0
  95. data/lib/aia/logger.rb +24 -4
  96. data/lib/aia/mcp_config_normalizer.rb +35 -0
  97. data/lib/aia/mcp_connection_manager.rb +305 -0
  98. data/lib/aia/mcp_discovery.rb +44 -0
  99. data/lib/aia/mcp_grouper.rb +33 -0
  100. data/lib/aia/mcp_utility.rb +57 -0
  101. data/lib/aia/mention_router.rb +260 -0
  102. data/lib/aia/model_alias_registry.rb +97 -0
  103. data/lib/aia/model_switch_handler.rb +100 -0
  104. data/lib/aia/network_builder.rb +155 -0
  105. data/lib/aia/network_memory_manager.rb +55 -0
  106. data/lib/aia/patches/ruby_llm_streaming_error.rb +43 -0
  107. data/lib/aia/patches/ruby_llm_tool_error.rb +96 -0
  108. data/lib/aia/pipeline_orchestrator.rb +262 -0
  109. data/lib/aia/plugin_loader.rb +170 -0
  110. data/lib/aia/plugin_monitor.rb +208 -0
  111. data/lib/aia/prompt_decomposer.rb +157 -0
  112. data/lib/aia/prompt_handler.rb +19 -39
  113. data/lib/aia/robot_builder.rb +51 -0
  114. data/lib/aia/robot_factory.rb +334 -0
  115. data/lib/aia/robot_namer.rb +116 -0
  116. data/lib/aia/session.rb +83 -17
  117. data/lib/aia/session_tracker.rb +209 -0
  118. data/lib/aia/similarity_scorer.rb +39 -0
  119. data/lib/aia/skill_utils.rb +105 -1
  120. data/lib/aia/spawn_handler.rb +129 -0
  121. data/lib/aia/spawn_spec_parser.rb +65 -0
  122. data/lib/aia/special_mode_handler.rb +302 -0
  123. data/lib/aia/startup_coordinator.rb +150 -0
  124. data/lib/aia/streaming_runner.rb +169 -0
  125. data/lib/aia/system_prompt_assembler.rb +88 -0
  126. data/lib/aia/task_coordinator.rb +202 -0
  127. data/lib/aia/task_decomposer.rb +57 -0
  128. data/lib/aia/task_executor.rb +51 -0
  129. data/lib/aia/tfidf_math.rb +27 -0
  130. data/lib/aia/tool_filter/tfidf.rb +116 -0
  131. data/lib/aia/tool_filter/wordnet_expander.rb +127 -0
  132. data/lib/aia/tool_filter.rb +82 -0
  133. data/lib/aia/tool_filter_registry.rb +30 -0
  134. data/lib/aia/tool_filter_strategy.rb +143 -0
  135. data/lib/aia/tool_loader.rb +210 -0
  136. data/lib/aia/tool_utility.rb +30 -0
  137. data/lib/aia/tools/delegate_to_foreman_tool.rb +70 -0
  138. data/lib/aia/tools/recruit_robot_tool.rb +60 -0
  139. data/lib/aia/tools/reskill_robot_tool.rb +44 -0
  140. data/lib/aia/tools/task_board_tool.rb +114 -0
  141. data/lib/aia/trakflow_bridge.rb +173 -0
  142. data/lib/aia/turn_state.rb +94 -0
  143. data/lib/aia/ui_presenter.rb +166 -198
  144. data/lib/aia/utility.rb +134 -87
  145. data/lib/aia/{history_manager.rb → variable_input_collector.rb} +8 -9
  146. data/lib/aia/verification_network.rb +58 -0
  147. data/lib/aia.rb +108 -63
  148. data/mkdocs.yml +1 -0
  149. metadata +179 -56
  150. data/justfile +0 -215
  151. data/lib/aia/adapter/chat_execution.rb +0 -242
  152. data/lib/aia/adapter/error_handler.rb +0 -68
  153. data/lib/aia/adapter/gem_activator.rb +0 -57
  154. data/lib/aia/adapter/mcp_connector.rb +0 -274
  155. data/lib/aia/adapter/modality_handlers.rb +0 -167
  156. data/lib/aia/adapter/model_registry.rb +0 -81
  157. data/lib/aia/adapter/multi_model_chat.rb +0 -218
  158. data/lib/aia/adapter/provider_configurator.rb +0 -59
  159. data/lib/aia/adapter/tool_filter.rb +0 -85
  160. data/lib/aia/adapter/tool_loader.rb +0 -90
  161. data/lib/aia/chat_processor_service.rb +0 -178
  162. data/lib/aia/prompt_pipeline.rb +0 -183
  163. data/lib/aia/ruby_llm_adapter.rb +0 -95
  164. data/lib/extensions/openstruct_merge.rb +0 -48
  165. data/lib/extensions/ruby_llm/.irbrc +0 -56
  166. data/lib/extensions/ruby_llm/modalities.rb +0 -36
  167. data/lib/extensions/ruby_llm/provider_fix.rb +0 -79
  168. data/lib/refinements/string.rb +0 -16
  169. data/main.just +0 -76
@@ -1,12 +1,16 @@
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
7
10
  class UIPresenter
8
- USER_PROMPT = "Follow up (cntl-D or 'exit' to end) #=> "
9
-
11
+ USER_PROMPT = "Follow up (cntl-D or 'exit' to end) #=> ".freeze
12
+ HISTORY_FILE = File.join(Dir.home, '.config', 'aia', 'chat_history')
13
+ MAX_HISTORY = 50
10
14
 
11
15
  def initialize
12
16
  @terminal_width = TTY::Screen.width
@@ -16,27 +20,19 @@ module AIA
16
20
  puts "#{'═' * @terminal_width}\n"
17
21
  end
18
22
 
19
-
20
23
  def display_ai_response(response)
21
24
  puts "\nAI: "
22
25
  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
26
  end
32
27
 
33
-
34
28
  def format_chat_response(response, output = $stdout)
35
29
  indent = ' '
36
30
 
37
31
  # Convert RubyLLM::Message to string if necessary
38
- response_text = if response.is_a?(RubyLLM::Message)
32
+ response_text = if defined?(RubyLLM::Message) && response.is_a?(RubyLLM::Message)
39
33
  response.content.to_s
34
+ elsif response.respond_to?(:reply)
35
+ response.reply.to_s
40
36
  elsif response.respond_to?(:to_s)
41
37
  response.to_s
42
38
  else
@@ -67,17 +63,14 @@ module AIA
67
63
  end
68
64
  end
69
65
 
70
-
71
66
  def display_separator
72
67
  puts "\n#{'─' * @terminal_width}"
73
68
  end
74
69
 
75
-
76
70
  def display_chat_end
77
71
  puts "\nChat session ended."
78
72
  end
79
73
 
80
-
81
74
  # This is the follow up question in a chat session
82
75
  def ask_question
83
76
  puts USER_PROMPT
@@ -85,24 +78,34 @@ module AIA
85
78
  begin
86
79
  input = Reline.readline('', true)
87
80
  return nil if input.nil? # Handle Ctrl+D
88
- Reline::HISTORY << input unless input.strip.empty?
81
+ save_chat_history unless input.strip.empty?
89
82
  input
90
83
  rescue Interrupt
91
84
  puts "\nChat session interrupted."
92
- return 'exit'
85
+ 'exit'
93
86
  end
94
87
  end
95
88
 
89
+ # Load persistent chat history into Reline
90
+ def load_chat_history
91
+ Reline::HISTORY.clear
92
+ history_file = chat_history_file
93
+ return unless history_file && File.exist?(history_file)
94
+
95
+ lines = File.readlines(history_file, chomp: true).last(MAX_HISTORY)
96
+ lines.each { |line| Reline::HISTORY << line }
97
+ end
98
+
96
99
  def display_info(message)
97
- puts "\n#{message}"
100
+ $stderr.puts "\n#{message}"
98
101
  end
99
102
 
100
103
  def display_error(message)
101
- warn "ERROR: #{message}"
104
+ $stderr.puts "\n❌ ERROR: #{message}\n"
102
105
  end
103
106
 
104
107
  def display_warning(message)
105
- warn "WARNING: #{message}"
108
+ $stderr.puts "\n⚠ WARNING: #{message}\n"
106
109
  end
107
110
 
108
111
  def with_spinner(message = "Processing", operation_type = nil)
@@ -120,211 +123,176 @@ module AIA
120
123
 
121
124
  def display_token_metrics(metrics)
122
125
  return unless metrics
123
-
124
- output_lines = []
125
- output_lines << "═" * 55
126
- output_lines << "Model: #{metrics[:model_id]}"
127
-
126
+
127
+ model_id = metrics[:model_id] || 'unknown'
128
+ input_tokens = metrics[:input_tokens] || 0
129
+ output_tokens = metrics[:output_tokens] || 0
130
+ total_tokens = input_tokens + output_tokens
131
+ time_str = format_elapsed(metrics[:elapsed])
132
+
128
133
  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 }
134
+ cost_data = calculate_cost(metrics)
135
+ if cost_data[:available]
136
+ header = %w[Model Input Output Total Cost x1000 Time]
137
+ row = [
138
+ model_id,
139
+ input_tokens, output_tokens, total_tokens,
140
+ "$#{'%.5f' % cost_data[:total_cost]}",
141
+ "$#{'%.2f' % (cost_data[:total_cost] * 1000)}",
142
+ time_str
143
+ ]
144
+ alignments = %i[left right right right right right right]
145
+ else
146
+ header = %w[Model Input Output Total Cost Time]
147
+ row = [model_id, input_tokens, output_tokens, total_tokens, "N/A", time_str]
148
+ alignments = %i[left right right right right right]
144
149
  end
150
+ else
151
+ header = %w[Model Input Output Total Time]
152
+ row = [model_id, input_tokens, output_tokens, total_tokens, time_str]
153
+ alignments = %i[left right right right right]
145
154
  end
155
+
156
+ table = TTY::Table.new(header, [row])
157
+ rendered = table.render(:unicode, resize: true, alignments: alignments, padding: [0, 1])
158
+
159
+ puts rendered
160
+ write_to_output_file(rendered)
146
161
  end
147
162
 
163
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity
148
164
  def display_multi_model_metrics(metrics_list)
149
165
  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
166
+
167
+ show_cost = AIA.config.flags.cost
168
+ show_similarity = metrics_list.any? { |m| m.key?(:similarity) }
169
+ total_input = 0
170
+ total_output = 0
171
+ total_cost = 0.0
172
+ max_elapsed = 0.0
173
+
174
+ header = %w[Model Input Output Total]
175
+ header += %w[Cost x1000] if show_cost
176
+ header << "Time"
177
+ header << "Sim" if show_similarity
178
+
179
+ rows = metrics_list.map do |metrics|
180
+ model_name = (metrics[:model_id] || metrics[:display_name]).to_s
181
+ input_tokens = metrics[:input_tokens] || 0
187
182
  output_tokens = metrics[:output_tokens] || 0
188
- total_tokens = input_tokens + output_tokens
189
-
190
- if AIA.config.flags.cost
183
+ total_tokens = input_tokens + output_tokens
184
+ elapsed = metrics[:elapsed]
185
+ time_str = format_elapsed(elapsed)
186
+
187
+ total_input += input_tokens
188
+ total_output += output_tokens
189
+ max_elapsed = [max_elapsed, elapsed || 0].max
190
+
191
+ row = [model_name, input_tokens, output_tokens, total_tokens]
192
+
193
+ if show_cost
191
194
  cost_data = calculate_cost(metrics)
192
195
  if cost_data[:available]
193
- cost_str = "$#{'%.5f' % cost_data[:total_cost]}"
194
- x1000_str = "$#{'%.2f' % (cost_data[:total_cost] * 1000)}"
196
+ row << "$#{'%.5f' % cost_data[:total_cost]}"
197
+ row << "$#{'%.2f' % (cost_data[:total_cost] * 1000)}"
195
198
  total_cost += cost_data[:total_cost]
196
199
  else
197
- cost_str = "N/A"
198
- x1000_str = "N/A"
200
+ row += ["N/A", "N/A"]
199
201
  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
202
  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)
203
+
204
+ row << time_str
205
+ row << format_similarity(metrics[:similarity]) if show_similarity
206
+ row
224
207
  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
208
+
209
+ # Totals row
210
+ all_tokens = total_input + total_output
211
+ total_time = format_elapsed(max_elapsed)
212
+ rows << :separator
213
+
214
+ totals = ["TOTAL", total_input, total_output, all_tokens]
215
+ if show_cost && total_cost.positive?
216
+ totals << "$#{'%.5f' % total_cost}"
217
+ totals << "$#{'%.2f' % (total_cost * 1000)}"
218
+ elsif show_cost
219
+ totals += ["", ""]
237
220
  end
221
+ totals << total_time
222
+ totals << "" if show_similarity
223
+ rows << totals
224
+
225
+ alignments = %i[left right right right]
226
+ alignments += %i[right right] if show_cost
227
+ alignments << :right
228
+ alignments << :right if show_similarity
229
+
230
+ table = TTY::Table.new(header, rows)
231
+ rendered = table.render(:unicode, resize: true, alignments: alignments, padding: [0, 1])
232
+
233
+ puts "\nMulti-Model Token Usage"
234
+ puts rendered
235
+ write_to_output_file("Multi-Model Token Usage\n#{rendered}")
238
236
  end
237
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity
239
238
 
240
239
  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
240
+
241
+ def save_chat_history
242
+ history_file = chat_history_file
243
+ return unless history_file
244
+ dir = File.dirname(history_file)
245
+ FileUtils.mkdir_p(dir)
246
+
247
+ entries = Reline::HISTORY.to_a.last(MAX_HISTORY)
248
+ File.write(history_file, entries.join("\n") + "\n")
250
249
  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}"
250
+
251
+ def chat_history_file
252
+ config = AIA.config
253
+ if config.respond_to?(:output) && config.output.respond_to?(:history_file)
254
+ hf = config.output.history_file
255
+ return nil if hf == false # --no-history-file disables chat history
256
+ return File.expand_path(hf) if hf
260
257
  end
261
-
262
- lines
263
- end
264
-
265
- def display_metrics_with_cost(metrics)
266
- 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)"
258
+ if config.respond_to?(:paths) && config.paths.respond_to?(:aia_dir) && config.paths.aia_dir
259
+ return File.join(File.expand_path(config.paths.aia_dir), 'chat_history')
279
260
  end
261
+ HISTORY_FILE
280
262
  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)}"
263
+
264
+ def format_similarity(score)
265
+ return "ref" if score.nil?
266
+ "%.1f%%" % (score * 100)
267
+ end
268
+
269
+ def format_elapsed(seconds)
270
+ return "" unless seconds
271
+
272
+ if seconds < 60
273
+ "%.1fs" % seconds
291
274
  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)"
275
+ minutes = (seconds / 60).to_i
276
+ secs = seconds % 60
277
+ "#{minutes}m %04.1fs" % secs
297
278
  end
298
-
299
- lines
300
279
  end
301
-
280
+
281
+ def write_to_output_file(text)
282
+ out_file = AIA.config.output.file
283
+ return unless out_file
284
+
285
+ File.open(out_file, 'a') { |f| f.puts text }
286
+ end
287
+
302
288
  def calculate_cost(metrics)
303
289
  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
290
+
291
+ CostCalculator.calculate(
292
+ model_id: metrics[:model_id],
293
+ input_tokens: metrics[:input_tokens],
294
+ output_tokens: metrics[:output_tokens]
295
+ )
328
296
  end
329
297
  end
330
298
  end