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.
- checksums.yaml +4 -4
- data/.envrc +9 -1
- data/.loki +11 -0
- data/.reek.yml +160 -0
- data/.rubocop.yml +116 -0
- data/.rubocop_strict.yml +15 -0
- data/.version +1 -1
- data/CHANGELOG.md +269 -55
- data/IMPLEMENTATION_PLAN.md +506 -0
- data/README.md +267 -239
- data/Rakefile +5 -5
- data/_typos.toml +10 -0
- data/aia.gemspec +92 -0
- data/architecture_review.md +314 -0
- data/bin/aia +16 -0
- data/config/aia.yml +13 -0
- data/docs/AGENTS.md +40 -0
- data/docs/advanced-prompting.md +67 -3
- data/docs/cli-reference.md +312 -56
- data/docs/configuration.md +130 -19
- data/docs/contributing.md +56 -2
- data/docs/directives-reference.md +593 -78
- data/docs/faq.md +85 -3
- data/docs/guides/available-models.md +1 -1
- data/docs/guides/basic-usage.md +6 -6
- data/docs/guides/chat.md +40 -16
- data/docs/guides/crew.md +239 -0
- data/docs/guides/executable-prompts.md +1 -1
- data/docs/guides/index.md +1 -0
- data/docs/guides/models.md +15 -0
- data/docs/index.md +29 -2
- data/docs/installation.md +44 -17
- data/docs/mcp-integration.md +40 -0
- data/docs/prompt_management.md +85 -86
- data/docs/security.md +47 -0
- data/docs/special_projects_guide.md +386 -0
- data/docs/tools-and-mcp-examples.md +23 -0
- data/docs/workflows-and-pipelines.md +84 -7
- data/examples/.gitignore +1 -0
- data/examples/00_setup_aia.sh +27 -44
- data/examples/11_multi_model.sh +4 -14
- data/examples/12_token_usage.sh +3 -12
- data/examples/18_tools.sh +10 -2
- data/examples/22_chat_mode.sh +0 -10
- data/examples/23_verify.sh +139 -0
- data/examples/24_decompose.sh +139 -0
- data/examples/25_spawn.sh +139 -0
- data/examples/26_debate.sh +97 -0
- data/examples/27_mention_routing.sh +157 -0
- data/examples/28_model_switching.sh +106 -0
- data/examples/29_agent_harness.sh +177 -0
- data/examples/README.md +65 -0
- data/examples/advanced_multi_robot_capabilities_without_examples.md +106 -0
- data/examples/aia_config.yml +1 -1
- data/examples/aia_config_orchestrator.yml +45 -0
- data/examples/common.sh +18 -6
- data/examples/context/tech_stack.md +2 -2
- data/examples/prompts_dir/roles/orchestrator.md +21 -0
- data/examples/requirements/sinatra_taskflow_app.md +139 -0
- data/examples/rules/01_classify_ruby.rb +16 -0
- data/examples/rules/02_prefer_claude_for_code.rb +19 -0
- data/examples/rules/03_gate_prompt_length.rb +19 -0
- data/examples/rules/04_tool_selection.rb +41 -0
- data/examples/rules/README.md +30 -0
- data/examples/run_all.sh +48 -15
- data/examples/tools/word_count_tool.rb +1 -1
- data/lib/AGENTS.md +57 -0
- data/lib/aia/chat_loop.rb +263 -167
- data/lib/aia/config/cli_parser.rb +217 -145
- data/lib/aia/config/defaults.yml +62 -33
- data/lib/aia/config/mcp_parser.rb +52 -51
- data/lib/aia/config/model_spec.rb +34 -2
- data/lib/aia/config/validator.rb +171 -216
- data/lib/aia/config.rb +111 -145
- data/lib/aia/content_extractor.rb +155 -0
- data/lib/aia/cost_calculator.rb +39 -0
- data/lib/aia/crew.rb +164 -0
- data/lib/aia/debate_handler.rb +174 -0
- data/lib/aia/delegate_handler.rb +116 -0
- data/lib/aia/directive.rb +43 -26
- data/lib/aia/directive_processor.rb +16 -7
- data/lib/aia/directives/configuration_directives.rb +214 -60
- data/lib/aia/directives/context_directives.rb +67 -52
- data/lib/aia/directives/execution_directives.rb +141 -4
- data/lib/aia/directives/model_directives.rb +163 -141
- data/lib/aia/directives/trakflow_directives.rb +62 -0
- data/lib/aia/directives/utility_directives.rb +227 -30
- data/lib/aia/directives/web_and_file_directives.rb +126 -77
- data/lib/aia/errors.rb +15 -0
- data/lib/aia/fact_asserter.rb +27 -0
- data/lib/aia/fzf.rb +9 -31
- data/lib/aia/handler_context.rb +17 -0
- data/lib/aia/handler_protocol.rb +19 -0
- data/lib/aia/history_transfer.rb +55 -0
- data/lib/aia/input_collector.rb +3 -3
- data/lib/aia/layered_orchestrator.rb +471 -0
- data/lib/aia/logger.rb +45 -25
- data/lib/aia/mcp_config_normalizer.rb +35 -0
- data/lib/aia/mcp_connection_manager.rb +315 -0
- data/lib/aia/mcp_discovery.rb +44 -0
- data/lib/aia/mcp_grouper.rb +33 -0
- data/lib/aia/mcp_server_config.rb +30 -0
- data/lib/aia/mcp_utility.rb +60 -0
- data/lib/aia/mention_router.rb +217 -0
- data/lib/aia/model_alias_registry.rb +97 -0
- data/lib/aia/model_switch_handler.rb +100 -0
- data/lib/aia/network_builder.rb +160 -0
- data/lib/aia/network_memory_manager.rb +55 -0
- data/lib/aia/patches/ruby_llm_streaming_error.rb +43 -0
- data/lib/aia/patches/ruby_llm_tool_error.rb +96 -0
- data/lib/aia/pipeline_orchestrator.rb +272 -0
- data/lib/aia/plugin_loader.rb +170 -0
- data/lib/aia/plugin_monitor.rb +211 -0
- data/lib/aia/prompt_decomposer.rb +159 -0
- data/lib/aia/prompt_handler.rb +56 -82
- data/lib/aia/robot_builder.rb +51 -0
- data/lib/aia/robot_factory.rb +338 -0
- data/lib/aia/robot_namer.rb +110 -0
- data/lib/aia/session.rb +87 -17
- data/lib/aia/session_tracker.rb +207 -0
- data/lib/aia/similarity_scorer.rb +41 -0
- data/lib/aia/skill_utils.rb +105 -1
- data/lib/aia/spawn_handler.rb +129 -0
- data/lib/aia/spawn_spec_parser.rb +65 -0
- data/lib/aia/special_mode_handler.rb +322 -0
- data/lib/aia/speech.rb +67 -0
- data/lib/aia/startup_coordinator.rb +151 -0
- data/lib/aia/streaming_runner.rb +172 -0
- data/lib/aia/system_prompt_assembler.rb +92 -0
- data/lib/aia/task_coordinator.rb +207 -0
- data/lib/aia/task_decomposer.rb +57 -0
- data/lib/aia/task_executor.rb +51 -0
- data/lib/aia/tfidf_math.rb +27 -0
- data/lib/aia/timing.rb +15 -0
- data/lib/aia/tool_filter/tfidf.rb +116 -0
- data/lib/aia/tool_filter/wordnet_expander.rb +127 -0
- data/lib/aia/tool_filter.rb +83 -0
- data/lib/aia/tool_filter_registry.rb +30 -0
- data/lib/aia/tool_filter_strategy.rb +146 -0
- data/lib/aia/tool_introspection.rb +17 -0
- data/lib/aia/tool_loader.rb +216 -0
- data/lib/aia/tool_utility.rb +30 -0
- data/lib/aia/tools/delegate_to_foreman_tool.rb +70 -0
- data/lib/aia/tools/recruit_robot_tool.rb +60 -0
- data/lib/aia/tools/reskill_robot_tool.rb +44 -0
- data/lib/aia/tools/task_board_tool.rb +115 -0
- data/lib/aia/trakflow_bridge.rb +175 -0
- data/lib/aia/turn_state.rb +95 -0
- data/lib/aia/ui_presenter.rb +182 -206
- data/lib/aia/utility.rb +136 -87
- data/lib/aia/{history_manager.rb → variable_input_collector.rb} +9 -9
- data/lib/aia/verification_network.rb +57 -0
- data/lib/aia.rb +124 -63
- data/mkdocs.yml +1 -0
- metadata +187 -58
- data/justfile +0 -215
- data/lib/aia/adapter/chat_execution.rb +0 -242
- data/lib/aia/adapter/error_handler.rb +0 -68
- data/lib/aia/adapter/gem_activator.rb +0 -57
- data/lib/aia/adapter/mcp_connector.rb +0 -274
- data/lib/aia/adapter/modality_handlers.rb +0 -167
- data/lib/aia/adapter/model_registry.rb +0 -81
- data/lib/aia/adapter/multi_model_chat.rb +0 -218
- data/lib/aia/adapter/provider_configurator.rb +0 -59
- data/lib/aia/adapter/tool_filter.rb +0 -85
- data/lib/aia/adapter/tool_loader.rb +0 -90
- data/lib/aia/chat_processor_service.rb +0 -178
- data/lib/aia/prompt_pipeline.rb +0 -183
- data/lib/aia/ruby_llm_adapter.rb +0 -95
- data/lib/extensions/openstruct_merge.rb +0 -48
- data/lib/extensions/ruby_llm/.irbrc +0 -56
- data/lib/extensions/ruby_llm/modalities.rb +0 -36
- data/lib/extensions/ruby_llm/provider_fix.rb +0 -79
- data/lib/refinements/string.rb +0 -16
- data/main.just +0 -76
data/lib/aia/ui_presenter.rb
CHANGED
|
@@ -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
|
-
|
|
83
|
+
save_chat_history unless input.strip.empty?
|
|
89
84
|
input
|
|
90
85
|
rescue Interrupt
|
|
91
86
|
puts "\nChat session interrupted."
|
|
92
|
-
|
|
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
|
-
|
|
106
|
+
$stderr.puts "\n❌ ERROR: #{message}\n"
|
|
102
107
|
end
|
|
103
108
|
|
|
104
109
|
def display_warning(message)
|
|
105
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
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
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|