aia 0.9.12 → 0.9.14
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 +2 -0
- data/.version +1 -1
- data/CHANGELOG.md +44 -0
- data/README.md +62 -2
- data/docs/directives-reference.md +75 -3
- data/docs/guides/chat.md +59 -7
- data/lib/aia/chat_processor_service.rb +19 -4
- data/lib/aia/config/cli_parser.rb +9 -0
- data/lib/aia/context_manager.rb +61 -1
- data/lib/aia/directives/configuration.rb +77 -4
- data/lib/aia/directives/models.rb +150 -17
- data/lib/aia/ruby_llm_adapter.rb +66 -8
- data/lib/aia/session.rb +56 -6
- data/lib/aia/ui_presenter.rb +206 -0
- metadata +4 -19
- data/_notes.txt +0 -231
@@ -85,30 +85,163 @@ module AIA
|
|
85
85
|
puts "===================="
|
86
86
|
puts
|
87
87
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
88
|
+
# Manual descriptions for all directives
|
89
|
+
directive_descriptions = {
|
90
|
+
# Configuration directives
|
91
|
+
'config' => 'View or set configuration values',
|
92
|
+
'model' => 'View or change the AI model',
|
93
|
+
'temperature' => 'Set the temperature parameter for AI responses',
|
94
|
+
'top_p' => 'Set the top_p parameter for AI responses',
|
95
|
+
'clear' => 'Clear the conversation context',
|
96
|
+
'review' => 'Display the current conversation context with checkpoint markers',
|
97
|
+
'checkpoint' => 'Create a named checkpoint of the current context',
|
98
|
+
'restore' => 'Restore context to a previous checkpoint',
|
99
|
+
|
100
|
+
# Utility directives
|
101
|
+
'tools' => 'List available tools',
|
102
|
+
'next' => 'Set the next prompt in the sequence',
|
103
|
+
'pipeline' => 'Set or view the prompt workflow sequence',
|
104
|
+
'terse' => 'Add instruction for concise responses',
|
105
|
+
'robot' => 'Display ASCII robot art',
|
106
|
+
|
107
|
+
# Execution directives
|
108
|
+
'ruby' => 'Execute Ruby code',
|
109
|
+
'shell' => 'Execute shell commands',
|
110
|
+
'say' => 'Use text-to-speech to speak the text',
|
111
|
+
|
112
|
+
# Web and File directives
|
113
|
+
'webpage' => 'Fetch and include content from a webpage',
|
114
|
+
'include' => 'Include content from a file',
|
115
|
+
'include_file' => 'Include file content (internal use)',
|
116
|
+
'included_files' => 'List files that have been included',
|
117
|
+
'included_files=' => 'Set the list of included files',
|
118
|
+
|
119
|
+
# Model directives
|
120
|
+
'available_models' => 'List all available AI models',
|
121
|
+
'compare' => 'Compare responses from multiple models',
|
122
|
+
'help' => 'Show this help message',
|
123
|
+
|
124
|
+
# Aliases (these get their descriptions from main directive)
|
125
|
+
'cfg' => nil, # alias for config
|
126
|
+
'temp' => nil, # alias for temperature
|
127
|
+
'topp' => nil, # alias for top_p
|
128
|
+
'context' => nil, # alias for review
|
129
|
+
'cp' => nil, # alias for checkpoint
|
130
|
+
'workflow' => nil, # alias for pipeline
|
131
|
+
'rb' => nil, # alias for ruby
|
132
|
+
'sh' => nil, # alias for shell
|
133
|
+
'web' => nil, # alias for webpage
|
134
|
+
'website' => nil, # alias for webpage
|
135
|
+
'import' => nil, # alias for include
|
136
|
+
'models' => nil, # alias for available_models
|
137
|
+
'all_models' => nil, # alias for available_models
|
138
|
+
'am' => nil, # alias for available_models
|
139
|
+
'llms' => nil, # alias for available_models
|
140
|
+
'available' => nil, # alias for available_models
|
141
|
+
'cmp' => nil, # alias for compare
|
142
|
+
}
|
143
|
+
|
144
|
+
# Get all registered directive modules from the Registry
|
145
|
+
all_modules = [
|
146
|
+
AIA::Directives::WebAndFile,
|
147
|
+
AIA::Directives::Utility,
|
148
|
+
AIA::Directives::Configuration,
|
149
|
+
AIA::Directives::Execution,
|
150
|
+
AIA::Directives::Models
|
151
|
+
]
|
152
|
+
|
153
|
+
all_directives = {}
|
154
|
+
excluded_methods = ['run', 'initialize', 'private?', 'descriptions', 'aliases', 'build_aliases',
|
155
|
+
'desc', 'method_added', 'register_directive_module', 'process',
|
156
|
+
'directive?', 'prefix_size']
|
157
|
+
|
158
|
+
# Collect directives from all modules
|
159
|
+
all_modules.each do |mod|
|
160
|
+
methods = mod.methods(false).map(&:to_s).reject { |m| excluded_methods.include?(m) }
|
161
|
+
|
162
|
+
methods.each do |method|
|
163
|
+
# Skip if this is an alias (has nil description)
|
164
|
+
next if directive_descriptions.key?(method) && directive_descriptions[method].nil?
|
165
|
+
|
166
|
+
description = directive_descriptions[method] || method.gsub('_', ' ').capitalize
|
167
|
+
|
168
|
+
all_directives[method] = {
|
169
|
+
module: mod.name.split('::').last,
|
170
|
+
description: description,
|
171
|
+
aliases: []
|
172
|
+
}
|
173
|
+
end
|
174
|
+
end
|
93
175
|
|
94
|
-
|
95
|
-
|
176
|
+
# Manually map known aliases
|
177
|
+
alias_mappings = {
|
178
|
+
'config' => ['cfg'],
|
179
|
+
'temperature' => ['temp'],
|
180
|
+
'top_p' => ['topp'],
|
181
|
+
'review' => ['context'],
|
182
|
+
'checkpoint' => ['cp'],
|
183
|
+
'pipeline' => ['workflow'],
|
184
|
+
'ruby' => ['rb'],
|
185
|
+
'shell' => ['sh'],
|
186
|
+
'webpage' => ['web', 'website'],
|
187
|
+
'include' => ['import'],
|
188
|
+
'available_models' => ['models', 'all_models', 'am', 'llms', 'available'],
|
189
|
+
'compare' => ['cmp']
|
190
|
+
}
|
191
|
+
|
192
|
+
# Apply alias mappings
|
193
|
+
alias_mappings.each do |directive, aliases|
|
194
|
+
if all_directives[directive]
|
195
|
+
all_directives[directive][:aliases] = aliases
|
196
|
+
end
|
197
|
+
end
|
96
198
|
|
97
|
-
|
199
|
+
# Sort and display directives by category
|
200
|
+
categories = {
|
201
|
+
'Configuration' => ['config', 'model', 'temperature', 'top_p', 'clear', 'review', 'checkpoint', 'restore'],
|
202
|
+
'Utility' => ['tools', 'next', 'pipeline', 'terse', 'robot', 'help'],
|
203
|
+
'Execution' => ['ruby', 'shell', 'say'],
|
204
|
+
'Web & Files' => ['webpage', 'include'],
|
205
|
+
'Models' => ['available_models', 'compare']
|
206
|
+
}
|
207
|
+
|
208
|
+
categories.each do |category, directives|
|
209
|
+
puts "#{category}:"
|
210
|
+
puts "-" * category.length
|
211
|
+
|
212
|
+
directives.each do |directive|
|
213
|
+
info = all_directives[directive]
|
214
|
+
next unless info
|
215
|
+
|
216
|
+
if info[:aliases] && !info[:aliases].empty?
|
217
|
+
with_prefix = info[:aliases].map { |m| PromptManager::Prompt::DIRECTIVE_SIGNAL + m }
|
218
|
+
alias_text = " (aliases: #{with_prefix.join(', ')})"
|
219
|
+
else
|
220
|
+
alias_text = ""
|
221
|
+
end
|
98
222
|
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
with_prefix = others.map { |m| PromptManager::Prompt::DIRECTIVE_SIGNAL + m }
|
103
|
-
others_line = "\tAliases:#{with_prefix.join(' ')}\n"
|
223
|
+
puts " //#{directive}#{alias_text}"
|
224
|
+
puts " #{info[:description]}"
|
225
|
+
puts
|
104
226
|
end
|
227
|
+
end
|
105
228
|
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
229
|
+
# Show any uncategorized directives
|
230
|
+
categorized = categories.values.flatten
|
231
|
+
uncategorized = all_directives.keys - categorized - ['include_file', 'included_files', 'included_files=']
|
232
|
+
|
233
|
+
if uncategorized.any?
|
234
|
+
puts "Other:"
|
235
|
+
puts "------"
|
236
|
+
uncategorized.sort.each do |directive|
|
237
|
+
info = all_directives[directive]
|
238
|
+
puts " //#{directive}"
|
239
|
+
puts " #{info[:description]}"
|
240
|
+
puts
|
241
|
+
end
|
110
242
|
end
|
111
243
|
|
244
|
+
puts "\nTotal: #{all_directives.size} directives available"
|
112
245
|
""
|
113
246
|
end
|
114
247
|
|
data/lib/aia/ruby_llm_adapter.rb
CHANGED
@@ -310,9 +310,15 @@ module AIA
|
|
310
310
|
prompt_parts << ""
|
311
311
|
|
312
312
|
results.each do |model_name, result|
|
313
|
-
|
313
|
+
# Extract content from RubyLLM::Message if needed
|
314
|
+
content = if result.respond_to?(:content)
|
315
|
+
result.content
|
316
|
+
else
|
317
|
+
result.to_s
|
318
|
+
end
|
319
|
+
next if content.start_with?("Error with")
|
314
320
|
prompt_parts << "#{model_name}:"
|
315
|
-
prompt_parts <<
|
321
|
+
prompt_parts << content
|
316
322
|
prompt_parts << ""
|
317
323
|
end
|
318
324
|
|
@@ -321,13 +327,64 @@ module AIA
|
|
321
327
|
end
|
322
328
|
|
323
329
|
def format_individual_responses(results)
|
324
|
-
|
330
|
+
# For metrics support, return a special structure if all results have token info
|
331
|
+
has_metrics = results.values.all? { |r| r.respond_to?(:input_tokens) && r.respond_to?(:output_tokens) }
|
332
|
+
|
333
|
+
if has_metrics && AIA.config.show_metrics
|
334
|
+
# Return structured data that preserves metrics for multi-model
|
335
|
+
format_multi_model_with_metrics(results)
|
336
|
+
else
|
337
|
+
# Original string formatting for non-metrics mode
|
338
|
+
output = []
|
339
|
+
results.each do |model_name, result|
|
340
|
+
output << "from: #{model_name}"
|
341
|
+
# Extract content from RubyLLM::Message if needed
|
342
|
+
content = if result.respond_to?(:content)
|
343
|
+
result.content
|
344
|
+
else
|
345
|
+
result.to_s
|
346
|
+
end
|
347
|
+
output << content
|
348
|
+
output << "" # Add blank line between results
|
349
|
+
end
|
350
|
+
output.join("\n")
|
351
|
+
end
|
352
|
+
end
|
353
|
+
|
354
|
+
def format_multi_model_with_metrics(results)
|
355
|
+
# Create a composite response that includes all model responses and metrics
|
356
|
+
formatted_content = []
|
357
|
+
metrics_data = []
|
358
|
+
|
325
359
|
results.each do |model_name, result|
|
326
|
-
|
327
|
-
|
328
|
-
|
360
|
+
formatted_content << "from: #{model_name}"
|
361
|
+
formatted_content << result.content
|
362
|
+
formatted_content << ""
|
363
|
+
|
364
|
+
# Collect metrics for each model
|
365
|
+
metrics_data << {
|
366
|
+
model_id: model_name,
|
367
|
+
input_tokens: result.input_tokens,
|
368
|
+
output_tokens: result.output_tokens
|
369
|
+
}
|
370
|
+
end
|
371
|
+
|
372
|
+
# Return a special MultiModelResponse that ChatProcessorService can handle
|
373
|
+
MultiModelResponse.new(formatted_content.join("\n"), metrics_data)
|
374
|
+
end
|
375
|
+
|
376
|
+
# Helper class to carry multi-model response with metrics
|
377
|
+
class MultiModelResponse
|
378
|
+
attr_reader :content, :metrics_list
|
379
|
+
|
380
|
+
def initialize(content, metrics_list)
|
381
|
+
@content = content
|
382
|
+
@metrics_list = metrics_list
|
383
|
+
end
|
384
|
+
|
385
|
+
def multi_model?
|
386
|
+
true
|
329
387
|
end
|
330
|
-
output.join("\n")
|
331
388
|
end
|
332
389
|
|
333
390
|
|
@@ -480,7 +537,8 @@ module AIA
|
|
480
537
|
chat_instance.ask(text_prompt, with: AIA.config.context_files)
|
481
538
|
end
|
482
539
|
|
483
|
-
response
|
540
|
+
# Return the full response object to preserve token information
|
541
|
+
response
|
484
542
|
rescue StandardError => e
|
485
543
|
e.message
|
486
544
|
end
|
data/lib/aia/session.rb
CHANGED
@@ -372,11 +372,34 @@ module AIA
|
|
372
372
|
conversation = @context_manager.get_context
|
373
373
|
|
374
374
|
@ui_presenter.display_thinking_animation
|
375
|
-
|
375
|
+
response_data = @chat_processor.process_prompt(conversation)
|
376
|
+
|
377
|
+
# Handle new response format with metrics
|
378
|
+
if response_data.is_a?(Hash)
|
379
|
+
content = response_data[:content]
|
380
|
+
metrics = response_data[:metrics]
|
381
|
+
multi_metrics = response_data[:multi_metrics]
|
382
|
+
else
|
383
|
+
content = response_data
|
384
|
+
metrics = nil
|
385
|
+
multi_metrics = nil
|
386
|
+
end
|
376
387
|
|
377
|
-
@ui_presenter.display_ai_response(
|
378
|
-
|
379
|
-
|
388
|
+
@ui_presenter.display_ai_response(content)
|
389
|
+
|
390
|
+
# Display metrics if enabled and available (chat mode only)
|
391
|
+
if AIA.config.show_metrics
|
392
|
+
if multi_metrics
|
393
|
+
# Display metrics for each model in multi-model mode
|
394
|
+
@ui_presenter.display_multi_model_metrics(multi_metrics)
|
395
|
+
elsif metrics
|
396
|
+
# Display metrics for single model
|
397
|
+
@ui_presenter.display_token_metrics(metrics)
|
398
|
+
end
|
399
|
+
end
|
400
|
+
|
401
|
+
@context_manager.add_to_context(role: "assistant", content: content)
|
402
|
+
@chat_processor.speak(content)
|
380
403
|
|
381
404
|
@ui_presenter.display_separator
|
382
405
|
end
|
@@ -384,10 +407,12 @@ module AIA
|
|
384
407
|
|
385
408
|
def process_chat_directive(follow_up_prompt)
|
386
409
|
directive_output = @directive_processor.process(follow_up_prompt, @context_manager)
|
387
|
-
|
410
|
+
|
388
411
|
return handle_clear_directive if follow_up_prompt.strip.start_with?("//clear")
|
412
|
+
return handle_checkpoint_directive(directive_output) if follow_up_prompt.strip.start_with?("//checkpoint")
|
413
|
+
return handle_restore_directive(directive_output) if follow_up_prompt.strip.start_with?("//restore")
|
389
414
|
return handle_empty_directive_output if directive_output.nil? || directive_output.strip.empty?
|
390
|
-
|
415
|
+
|
391
416
|
handle_successful_directive(follow_up_prompt, directive_output)
|
392
417
|
end
|
393
418
|
|
@@ -415,6 +440,31 @@ module AIA
|
|
415
440
|
nil
|
416
441
|
end
|
417
442
|
|
443
|
+
def handle_checkpoint_directive(directive_output)
|
444
|
+
@ui_presenter.display_info(directive_output)
|
445
|
+
nil
|
446
|
+
end
|
447
|
+
|
448
|
+
def handle_restore_directive(directive_output)
|
449
|
+
# If the restore was successful, we also need to refresh the client's context
|
450
|
+
if directive_output.start_with?("Context restored")
|
451
|
+
# Try to clear and rebuild the client's context
|
452
|
+
if AIA.config.client && AIA.config.client.respond_to?(:clear_context)
|
453
|
+
AIA.config.client.clear_context
|
454
|
+
end
|
455
|
+
|
456
|
+
# Optionally reinitialize the client for a clean state
|
457
|
+
begin
|
458
|
+
AIA.config.client = AIA::RubyLLMAdapter.new
|
459
|
+
rescue => e
|
460
|
+
STDERR.puts "Error reinitializing client after restore: #{e.message}"
|
461
|
+
end
|
462
|
+
end
|
463
|
+
|
464
|
+
@ui_presenter.display_info(directive_output)
|
465
|
+
nil
|
466
|
+
end
|
467
|
+
|
418
468
|
def handle_empty_directive_output
|
419
469
|
nil
|
420
470
|
end
|
data/lib/aia/ui_presenter.rb
CHANGED
@@ -117,5 +117,211 @@ module AIA
|
|
117
117
|
yield
|
118
118
|
end
|
119
119
|
end
|
120
|
+
|
121
|
+
def display_token_metrics(metrics)
|
122
|
+
return unless metrics
|
123
|
+
|
124
|
+
output_lines = []
|
125
|
+
output_lines << "═" * 55
|
126
|
+
output_lines << "Model: #{metrics[:model_id]}"
|
127
|
+
|
128
|
+
if AIA.config.show_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
|
+
if AIA.config.out_file && !AIA.config.out_file.nil?
|
141
|
+
File.open(AIA.config.out_file, 'a') do |file|
|
142
|
+
output_lines.each { |line| file.puts line }
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def display_multi_model_metrics(metrics_list)
|
148
|
+
return unless metrics_list && !metrics_list.empty?
|
149
|
+
|
150
|
+
output_lines = []
|
151
|
+
|
152
|
+
# Determine table width based on whether costs are shown
|
153
|
+
if AIA.config.show_cost
|
154
|
+
table_width = 80
|
155
|
+
else
|
156
|
+
table_width = 60
|
157
|
+
end
|
158
|
+
|
159
|
+
output_lines << "═" * table_width
|
160
|
+
output_lines << "Multi-Model Token Usage"
|
161
|
+
output_lines << "─" * table_width
|
162
|
+
|
163
|
+
# Build header row
|
164
|
+
if AIA.config.show_cost
|
165
|
+
output_lines << sprintf("%-20s %10s %10s %10s %12s %10s",
|
166
|
+
"Model", "Input", "Output", "Total", "Cost", "x1000")
|
167
|
+
output_lines << "─" * table_width
|
168
|
+
else
|
169
|
+
output_lines << sprintf("%-20s %10s %10s %10s",
|
170
|
+
"Model", "Input", "Output", "Total")
|
171
|
+
output_lines << "─" * table_width
|
172
|
+
end
|
173
|
+
|
174
|
+
# Process each model
|
175
|
+
total_input = 0
|
176
|
+
total_output = 0
|
177
|
+
total_cost = 0.0
|
178
|
+
|
179
|
+
metrics_list.each do |metrics|
|
180
|
+
model_name = metrics[:model_id]
|
181
|
+
# Truncate model name if too long
|
182
|
+
model_name = model_name[0..17] + ".." if model_name.length > 19
|
183
|
+
|
184
|
+
input_tokens = metrics[:input_tokens] || 0
|
185
|
+
output_tokens = metrics[:output_tokens] || 0
|
186
|
+
total_tokens = input_tokens + output_tokens
|
187
|
+
|
188
|
+
if AIA.config.show_cost
|
189
|
+
cost_data = calculate_cost(metrics)
|
190
|
+
if cost_data[:available]
|
191
|
+
cost_str = "$#{'%.5f' % cost_data[:total_cost]}"
|
192
|
+
x1000_str = "$#{'%.2f' % (cost_data[:total_cost] * 1000)}"
|
193
|
+
total_cost += cost_data[:total_cost]
|
194
|
+
else
|
195
|
+
cost_str = "N/A"
|
196
|
+
x1000_str = "N/A"
|
197
|
+
end
|
198
|
+
|
199
|
+
output_lines << sprintf("%-20s %10d %10d %10d %12s %10s",
|
200
|
+
model_name, input_tokens, output_tokens, total_tokens, cost_str, x1000_str)
|
201
|
+
else
|
202
|
+
output_lines << sprintf("%-20s %10d %10d %10d",
|
203
|
+
model_name, input_tokens, output_tokens, total_tokens)
|
204
|
+
end
|
205
|
+
|
206
|
+
total_input += input_tokens
|
207
|
+
total_output += output_tokens
|
208
|
+
end
|
209
|
+
|
210
|
+
# Display totals row
|
211
|
+
output_lines << "─" * table_width
|
212
|
+
total_tokens = total_input + total_output
|
213
|
+
|
214
|
+
if AIA.config.show_cost && total_cost > 0
|
215
|
+
cost_str = "$#{'%.5f' % total_cost}"
|
216
|
+
x1000_str = "$#{'%.2f' % (total_cost * 1000)}"
|
217
|
+
output_lines << sprintf("%-20s %10d %10d %10d %12s %10s",
|
218
|
+
"TOTAL", total_input, total_output, total_tokens, cost_str, x1000_str)
|
219
|
+
else
|
220
|
+
output_lines << sprintf("%-20s %10d %10d %10d",
|
221
|
+
"TOTAL", total_input, total_output, total_tokens)
|
222
|
+
end
|
223
|
+
|
224
|
+
output_lines << "═" * table_width
|
225
|
+
|
226
|
+
# Output to STDOUT
|
227
|
+
output_lines.each { |line| puts line }
|
228
|
+
|
229
|
+
# Also write to file if configured
|
230
|
+
if AIA.config.out_file && !AIA.config.out_file.nil?
|
231
|
+
File.open(AIA.config.out_file, 'a') do |file|
|
232
|
+
output_lines.each { |line| file.puts line }
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
private
|
238
|
+
|
239
|
+
def display_metrics_basic(metrics)
|
240
|
+
puts "Input tokens: #{metrics[:input_tokens] || 'N/A'}"
|
241
|
+
puts "Output tokens: #{metrics[:output_tokens] || 'N/A'}"
|
242
|
+
|
243
|
+
if metrics[:input_tokens] && metrics[:output_tokens]
|
244
|
+
total = metrics[:input_tokens] + metrics[:output_tokens]
|
245
|
+
puts "Total tokens: #{total}"
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
def format_metrics_basic(metrics)
|
250
|
+
lines = []
|
251
|
+
lines << "Input tokens: #{metrics[:input_tokens] || 'N/A'}"
|
252
|
+
lines << "Output tokens: #{metrics[:output_tokens] || 'N/A'}"
|
253
|
+
|
254
|
+
if metrics[:input_tokens] && metrics[:output_tokens]
|
255
|
+
total = metrics[:input_tokens] + metrics[:output_tokens]
|
256
|
+
lines << "Total tokens: #{total}"
|
257
|
+
end
|
258
|
+
|
259
|
+
lines
|
260
|
+
end
|
261
|
+
|
262
|
+
def display_metrics_with_cost(metrics)
|
263
|
+
cost_data = calculate_cost(metrics)
|
264
|
+
|
265
|
+
if cost_data[:available]
|
266
|
+
puts "Input tokens: #{metrics[:input_tokens]} ($#{'%.5f' % cost_data[:input_cost]})"
|
267
|
+
puts "Output tokens: #{metrics[:output_tokens]} ($#{'%.5f' % cost_data[:output_cost]})"
|
268
|
+
puts "Total cost: $#{'%.5f' % cost_data[:total_cost]}"
|
269
|
+
puts "Cost x1000: $#{'%.2f' % (cost_data[:total_cost] * 1000)}"
|
270
|
+
else
|
271
|
+
puts "Input tokens: #{metrics[:input_tokens] || 'N/A'}"
|
272
|
+
puts "Output tokens: #{metrics[:output_tokens] || 'N/A'}"
|
273
|
+
total = (metrics[:input_tokens] || 0) + (metrics[:output_tokens] || 0)
|
274
|
+
puts "Total tokens: #{total}"
|
275
|
+
puts "Cost: N/A (pricing not available)"
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
def format_metrics_with_cost(metrics)
|
280
|
+
lines = []
|
281
|
+
cost_data = calculate_cost(metrics)
|
282
|
+
|
283
|
+
if cost_data[:available]
|
284
|
+
lines << "Input tokens: #{metrics[:input_tokens]} ($#{'%.5f' % cost_data[:input_cost]})"
|
285
|
+
lines << "Output tokens: #{metrics[:output_tokens]} ($#{'%.5f' % cost_data[:output_cost]})"
|
286
|
+
lines << "Total cost: $#{'%.5f' % cost_data[:total_cost]}"
|
287
|
+
lines << "Cost x1000: $#{'%.2f' % (cost_data[:total_cost] * 1000)}"
|
288
|
+
else
|
289
|
+
lines << "Input tokens: #{metrics[:input_tokens] || 'N/A'}"
|
290
|
+
lines << "Output tokens: #{metrics[:output_tokens] || 'N/A'}"
|
291
|
+
total = (metrics[:input_tokens] || 0) + (metrics[:output_tokens] || 0)
|
292
|
+
lines << "Total tokens: #{total}"
|
293
|
+
lines << "Cost: N/A (pricing not available)"
|
294
|
+
end
|
295
|
+
|
296
|
+
lines
|
297
|
+
end
|
298
|
+
|
299
|
+
def calculate_cost(metrics)
|
300
|
+
return { available: false } unless metrics[:model_id] && metrics[:input_tokens] && metrics[:output_tokens]
|
301
|
+
|
302
|
+
# Look up model info from RubyLLM
|
303
|
+
begin
|
304
|
+
model_info = RubyLLM::Models.find(metrics[:model_id])
|
305
|
+
return { available: false } unless model_info
|
306
|
+
|
307
|
+
input_price = model_info.input_price_per_million
|
308
|
+
output_price = model_info.output_price_per_million
|
309
|
+
|
310
|
+
return { available: false } unless input_price && output_price
|
311
|
+
|
312
|
+
input_cost = metrics[:input_tokens] * input_price / 1_000_000.0
|
313
|
+
output_cost = metrics[:output_tokens] * output_price / 1_000_000.0
|
314
|
+
total_cost = input_cost + output_cost
|
315
|
+
|
316
|
+
{
|
317
|
+
available: true,
|
318
|
+
input_cost: input_cost,
|
319
|
+
output_cost: output_cost,
|
320
|
+
total_cost: total_cost
|
321
|
+
}
|
322
|
+
rescue StandardError => e
|
323
|
+
{ available: false, error: e.message }
|
324
|
+
end
|
325
|
+
end
|
120
326
|
end
|
121
327
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: aia
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.9.
|
4
|
+
version: 0.9.14
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dewayne VanHoozer
|
@@ -71,14 +71,14 @@ dependencies:
|
|
71
71
|
requirements:
|
72
72
|
- - ">="
|
73
73
|
- !ruby/object:Gem::Version
|
74
|
-
version: 0.5.
|
74
|
+
version: 0.5.8
|
75
75
|
type: :runtime
|
76
76
|
prerelease: false
|
77
77
|
version_requirements: !ruby/object:Gem::Requirement
|
78
78
|
requirements:
|
79
79
|
- - ">="
|
80
80
|
- !ruby/object:Gem::Version
|
81
|
-
version: 0.5.
|
81
|
+
version: 0.5.8
|
82
82
|
- !ruby/object:Gem::Dependency
|
83
83
|
name: ruby_llm
|
84
84
|
requirement: !ruby/object:Gem::Requirement
|
@@ -177,20 +177,6 @@ dependencies:
|
|
177
177
|
- - ">="
|
178
178
|
- !ruby/object:Gem::Version
|
179
179
|
version: '0'
|
180
|
-
- !ruby/object:Gem::Dependency
|
181
|
-
name: versionaire
|
182
|
-
requirement: !ruby/object:Gem::Requirement
|
183
|
-
requirements:
|
184
|
-
- - ">="
|
185
|
-
- !ruby/object:Gem::Version
|
186
|
-
version: '0'
|
187
|
-
type: :runtime
|
188
|
-
prerelease: false
|
189
|
-
version_requirements: !ruby/object:Gem::Requirement
|
190
|
-
requirements:
|
191
|
-
- - ">="
|
192
|
-
- !ruby/object:Gem::Version
|
193
|
-
version: '0'
|
194
180
|
- !ruby/object:Gem::Dependency
|
195
181
|
name: word_wrapper
|
196
182
|
requirement: !ruby/object:Gem::Requirement
|
@@ -329,7 +315,6 @@ files:
|
|
329
315
|
- LICENSE
|
330
316
|
- README.md
|
331
317
|
- Rakefile
|
332
|
-
- _notes.txt
|
333
318
|
- bin/aia
|
334
319
|
- docs/advanced-prompting.md
|
335
320
|
- docs/assets/images/aia.png
|
@@ -448,7 +433,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
448
433
|
- !ruby/object:Gem::Version
|
449
434
|
version: '0'
|
450
435
|
requirements: []
|
451
|
-
rubygems_version: 3.
|
436
|
+
rubygems_version: 3.6.9
|
452
437
|
specification_version: 4
|
453
438
|
summary: Multi-model AI CLI with dynamic prompts, consensus responses, shell & Ruby
|
454
439
|
integration, and seamless chat workflows.
|