aia 2.0.0.0.pre.alpha → 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 +5 -1
- data/.loki +3 -223
- data/.reek.yml +160 -0
- data/.rubocop_strict.yml +15 -0
- data/.version +1 -1
- data/CHANGELOG.md +10 -5
- data/README.md +1 -1
- data/Rakefile +0 -113
- data/_typos.toml +10 -0
- data/aia.gemspec +92 -0
- data/config/aia.yml +13 -0
- data/lib/aia/chat_loop.rb +8 -52
- data/lib/aia/config/cli_parser.rb +66 -57
- data/lib/aia/config/mcp_parser.rb +27 -19
- data/lib/aia/config/validator.rb +83 -94
- data/lib/aia/config.rb +3 -2
- data/lib/aia/content_extractor.rb +2 -0
- data/lib/aia/cost_calculator.rb +1 -0
- data/lib/aia/debate_handler.rb +28 -20
- data/lib/aia/delegate_handler.rb +7 -3
- data/lib/aia/directive.rb +10 -8
- data/lib/aia/directives/configuration_directives.rb +115 -101
- data/lib/aia/directives/context_directives.rb +31 -28
- data/lib/aia/directives/execution_directives.rb +18 -13
- data/lib/aia/directives/model_directives.rb +134 -154
- data/lib/aia/directives/trakflow_directives.rb +27 -9
- data/lib/aia/directives/utility_directives.rb +71 -71
- data/lib/aia/directives/web_and_file_directives.rb +63 -50
- data/lib/aia/layered_orchestrator.rb +66 -43
- data/lib/aia/logger.rb +21 -21
- data/lib/aia/mcp_connection_manager.rb +50 -40
- data/lib/aia/mcp_server_config.rb +30 -0
- data/lib/aia/mcp_utility.rb +7 -4
- data/lib/aia/mention_router.rb +7 -50
- data/lib/aia/network_builder.rb +10 -5
- data/lib/aia/network_memory_manager.rb +6 -6
- data/lib/aia/pipeline_orchestrator.rb +20 -10
- data/lib/aia/plugin_monitor.rb +9 -6
- data/lib/aia/prompt_decomposer.rb +5 -3
- data/lib/aia/prompt_handler.rb +43 -49
- data/lib/aia/robot_factory.rb +12 -8
- data/lib/aia/robot_namer.rb +2 -8
- data/lib/aia/session.rb +7 -3
- data/lib/aia/session_tracker.rb +39 -41
- data/lib/aia/similarity_scorer.rb +5 -3
- data/lib/aia/special_mode_handler.rb +52 -32
- data/lib/aia/speech.rb +67 -0
- data/lib/aia/startup_coordinator.rb +1 -0
- data/lib/aia/streaming_runner.rb +6 -3
- data/lib/aia/system_prompt_assembler.rb +11 -7
- data/lib/aia/task_coordinator.rb +10 -5
- data/lib/aia/timing.rb +15 -0
- data/lib/aia/tool_filter.rb +1 -0
- data/lib/aia/tool_filter_strategy.rb +28 -25
- data/lib/aia/tool_introspection.rb +17 -0
- data/lib/aia/tool_loader.rb +12 -6
- data/lib/aia/tools/task_board_tool.rb +1 -0
- data/lib/aia/trakflow_bridge.rb +5 -3
- data/lib/aia/turn_state.rb +1 -0
- data/lib/aia/ui_presenter.rb +67 -59
- data/lib/aia/utility.rb +5 -3
- data/lib/aia/variable_input_collector.rb +1 -0
- data/lib/aia/verification_network.rb +1 -2
- data/lib/aia.rb +16 -0
- metadata +15 -9
- data/.quality/flay_baseline.txt +0 -1
- data/.quality/flog_baseline.txt +0 -29
- data/.quality/reek_baseline.txt +0 -80
|
@@ -29,10 +29,29 @@ module AIA
|
|
|
29
29
|
alias llms available_models
|
|
30
30
|
|
|
31
31
|
desc "Compare responses from multiple models"
|
|
32
|
-
# rubocop:disable Metrics/MethodLength
|
|
33
32
|
def compare(args, context_manager = nil)
|
|
34
|
-
|
|
33
|
+
prompt, models = parse_compare_args(args)
|
|
35
34
|
|
|
35
|
+
return report_error('Error: No prompt provided for comparison') unless prompt
|
|
36
|
+
return report_error('Error: No models specified. Use --models model1,model2,model3') if models.empty?
|
|
37
|
+
|
|
38
|
+
puts "\nComparing responses for: #{prompt}\n"
|
|
39
|
+
puts '=' * 80
|
|
40
|
+
|
|
41
|
+
models.each { |model_name| compare_with_model(model_name.strip, prompt) }
|
|
42
|
+
|
|
43
|
+
puts "\n" + ('=' * 80)
|
|
44
|
+
puts "\nComparison complete!"
|
|
45
|
+
|
|
46
|
+
''
|
|
47
|
+
end
|
|
48
|
+
alias cmp compare
|
|
49
|
+
|
|
50
|
+
# --- helpers (no desc → not registered) ---
|
|
51
|
+
|
|
52
|
+
# Split a //compare arg list into [prompt, models]; the first non-flag
|
|
53
|
+
# token is the prompt, `--models a,b,c` supplies the model list.
|
|
54
|
+
def parse_compare_args(args)
|
|
36
55
|
prompt = nil
|
|
37
56
|
models = []
|
|
38
57
|
|
|
@@ -47,42 +66,26 @@ module AIA
|
|
|
47
66
|
end
|
|
48
67
|
end
|
|
49
68
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
puts "\nComparing responses for: #{prompt}\n"
|
|
54
|
-
puts '=' * 80
|
|
55
|
-
|
|
56
|
-
results = {}
|
|
57
|
-
|
|
58
|
-
models.each do |model_name|
|
|
59
|
-
model_name.strip!
|
|
60
|
-
puts "\n🤖 **#{model_name}:**"
|
|
61
|
-
puts '-' * 40
|
|
69
|
+
[prompt, models]
|
|
70
|
+
end
|
|
62
71
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
content = response.content
|
|
72
|
+
def compare_with_model(model_name, prompt)
|
|
73
|
+
puts "\n🤖 **#{model_name}:**"
|
|
74
|
+
puts '-' * 40
|
|
67
75
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
puts error_msg
|
|
73
|
-
results[model_name] = error_msg
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
puts "\n" + ('=' * 80)
|
|
78
|
-
puts "\nComparison complete!"
|
|
79
|
-
|
|
80
|
-
''
|
|
76
|
+
response = RubyLLM.chat(model: model_name).ask(prompt)
|
|
77
|
+
puts response.content
|
|
78
|
+
rescue StandardError => e
|
|
79
|
+
puts "Error with #{model_name}: #{e.message}"
|
|
81
80
|
end
|
|
82
|
-
# rubocop:enable Metrics/MethodLength
|
|
83
|
-
alias cmp compare
|
|
84
81
|
|
|
85
|
-
#
|
|
82
|
+
# Log and print a directive error, then return nil so the chat loop
|
|
83
|
+
# skips forwarding it to the robot (an error is not conversational output).
|
|
84
|
+
def report_error(msg)
|
|
85
|
+
AIA::LoggerManager.aia_logger.error(msg)
|
|
86
|
+
puts msg
|
|
87
|
+
nil
|
|
88
|
+
end
|
|
86
89
|
|
|
87
90
|
def show_local_models(current_models, positive_terms = nil, negative_terms = nil)
|
|
88
91
|
require 'net/http'
|
|
@@ -105,110 +108,93 @@ module AIA
|
|
|
105
108
|
end
|
|
106
109
|
end
|
|
107
110
|
|
|
108
|
-
#
|
|
111
|
+
# :reek:TooManyStatements -- sequential terminal report: fetch, connection guards, filtered listing
|
|
109
112
|
def show_ollama_models(api_base, positive_terms = nil, negative_terms = nil)
|
|
110
113
|
positive_terms, negative_terms = normalized_model_search_terms(positive_terms, negative_terms)
|
|
111
114
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
response = http.request(Net::HTTP::Get.new(uri))
|
|
115
|
+
data = fetch_json("#{api_base}/api/tags")
|
|
116
|
+
if data.nil?
|
|
117
|
+
puts "❌ Cannot connect to Ollama at #{api_base}"
|
|
118
|
+
return
|
|
119
|
+
end
|
|
118
120
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
121
|
+
models = data['models'] || []
|
|
122
|
+
if models.empty?
|
|
123
|
+
puts "No Ollama models found"
|
|
124
|
+
return
|
|
125
|
+
end
|
|
123
126
|
|
|
124
|
-
|
|
125
|
-
|
|
127
|
+
puts "Ollama Models (#{api_base}):"
|
|
128
|
+
puts "-" * 60
|
|
126
129
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
130
|
+
entries = models.map { |model| format_ollama_entry(model) }
|
|
131
|
+
print_filtered_entries(entries, positive_terms, negative_terms, "Ollama model(s) available")
|
|
132
|
+
rescue StandardError => e
|
|
133
|
+
puts "❌ Error fetching Ollama models: #{e.message}"
|
|
134
|
+
end
|
|
131
135
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
name = model['name']
|
|
138
|
-
size = model['size'] ? format_bytes(model['size']) : 'unknown'
|
|
139
|
-
modified = model['modified_at'] ? Time.parse(model['modified_at']).strftime('%Y-%m-%d') : 'unknown'
|
|
140
|
-
|
|
141
|
-
entry = "- ollama/#{name} (size: #{size}, modified: #{modified})"
|
|
142
|
-
entry_lc = entry.downcase
|
|
143
|
-
|
|
144
|
-
# entry_lc is a String; Array#intersect? would raise TypeError
|
|
145
|
-
show_it = positive_terms.empty? || positive_terms.any? { |q| entry_lc.include?(q) }
|
|
146
|
-
show_it &&= negative_terms.none? { |q| entry_lc.include?(q) }
|
|
147
|
-
if show_it
|
|
148
|
-
puts entry
|
|
149
|
-
counter += 1
|
|
150
|
-
end
|
|
151
|
-
end
|
|
136
|
+
def format_ollama_entry(model)
|
|
137
|
+
size = model['size'] ? format_bytes(model['size']) : 'unknown'
|
|
138
|
+
modified = model['modified_at'] ? Time.parse(model['modified_at']).strftime('%Y-%m-%d') : 'unknown'
|
|
139
|
+
"- ollama/#{model['name']} (size: #{size}, modified: #{modified})"
|
|
140
|
+
end
|
|
152
141
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
142
|
+
# Fetch and parse JSON from a local model server; nil unless HTTP success.
|
|
143
|
+
def fetch_json(uri_string)
|
|
144
|
+
uri = URI(uri_string)
|
|
145
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
146
|
+
http.open_timeout = 5
|
|
147
|
+
http.read_timeout = 5
|
|
148
|
+
response = http.request(Net::HTTP::Get.new(uri))
|
|
149
|
+
return nil unless response.is_a?(Net::HTTPSuccess)
|
|
150
|
+
|
|
151
|
+
JSON.parse(response.body)
|
|
159
152
|
end
|
|
160
|
-
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
161
153
|
|
|
162
|
-
#
|
|
163
|
-
def
|
|
164
|
-
positive_terms
|
|
154
|
+
# The entry is a String; Array#intersect? would raise TypeError
|
|
155
|
+
def entry_matches?(entry_lc, positive_terms, negative_terms)
|
|
156
|
+
(positive_terms.empty? || positive_terms.any? { |q| entry_lc.include?(q) }) &&
|
|
157
|
+
negative_terms.none? { |q| entry_lc.include?(q) }
|
|
158
|
+
end
|
|
165
159
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
http.read_timeout = 5
|
|
171
|
-
response = http.request(Net::HTTP::Get.new(uri))
|
|
160
|
+
def print_filtered_entries(entries, positive_terms, negative_terms, summary_label)
|
|
161
|
+
counter = 0
|
|
162
|
+
entries.each do |entry|
|
|
163
|
+
next unless entry_matches?(entry.downcase, positive_terms, negative_terms)
|
|
172
164
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
end
|
|
165
|
+
puts entry
|
|
166
|
+
counter += 1
|
|
167
|
+
end
|
|
177
168
|
|
|
178
|
-
|
|
179
|
-
|
|
169
|
+
puts
|
|
170
|
+
puts "#{counter} #{summary_label}"
|
|
171
|
+
puts
|
|
172
|
+
end
|
|
180
173
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
end
|
|
174
|
+
# :reek:TooManyStatements -- sequential terminal report: fetch, connection guards, filtered listing
|
|
175
|
+
def show_lms_models(api_base, positive_terms = nil, negative_terms = nil)
|
|
176
|
+
positive_terms, negative_terms = normalized_model_search_terms(positive_terms, negative_terms)
|
|
185
177
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
name = model['id']
|
|
192
|
-
entry = "- lms/#{name}"
|
|
193
|
-
entry_lc = entry.downcase
|
|
194
|
-
|
|
195
|
-
# entry_lc is a String; Array#intersect? would raise TypeError
|
|
196
|
-
show_it = positive_terms.empty? || positive_terms.any? { |q| entry_lc.include?(q) }
|
|
197
|
-
show_it &&= negative_terms.none? { |q| entry_lc.include?(q) }
|
|
198
|
-
if show_it
|
|
199
|
-
puts entry
|
|
200
|
-
counter += 1
|
|
201
|
-
end
|
|
202
|
-
end
|
|
178
|
+
data = fetch_json("#{api_base.gsub(%r{/v1/?$}, '')}/v1/models")
|
|
179
|
+
if data.nil?
|
|
180
|
+
puts "❌ Cannot connect to LM Studio at #{api_base}"
|
|
181
|
+
return
|
|
182
|
+
end
|
|
203
183
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
puts
|
|
207
|
-
|
|
208
|
-
puts "❌ Error fetching LM Studio models: #{e.message}"
|
|
184
|
+
models = data['data'] || []
|
|
185
|
+
if models.empty?
|
|
186
|
+
puts "No LM Studio models found"
|
|
187
|
+
return
|
|
209
188
|
end
|
|
189
|
+
|
|
190
|
+
puts "LM Studio Models (#{api_base}):"
|
|
191
|
+
puts "-" * 60
|
|
192
|
+
|
|
193
|
+
entries = models.map { |model| "- lms/#{model['id']}" }
|
|
194
|
+
print_filtered_entries(entries, positive_terms, negative_terms, "LM Studio model(s) available")
|
|
195
|
+
rescue StandardError => e
|
|
196
|
+
puts "❌ Error fetching LM Studio models: #{e.message}"
|
|
210
197
|
end
|
|
211
|
-
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
212
198
|
|
|
213
199
|
def format_bytes(bytes)
|
|
214
200
|
units = %w[B KB MB GB TB]
|
|
@@ -220,59 +206,53 @@ module AIA
|
|
|
220
206
|
"%.1f %s" % [bytes.to_f / (1024**exp), units[exp]]
|
|
221
207
|
end
|
|
222
208
|
|
|
223
|
-
#
|
|
209
|
+
# :reek:TooManyStatements -- sequential terminal report: header, filtered model listing, summary
|
|
224
210
|
def show_rubyllm_models(positive_terms = nil, negative_terms = nil)
|
|
225
211
|
positive_terms, negative_terms = normalized_model_search_terms(positive_terms, negative_terms)
|
|
226
212
|
|
|
227
213
|
# expand comma-separated terms passed as a single token
|
|
228
|
-
if positive_terms.size == 1
|
|
229
|
-
positive_terms = positive_terms.first.split(',')
|
|
230
|
-
end
|
|
231
|
-
|
|
232
|
-
header = "\nAvailable LLMs"
|
|
233
|
-
header += " for #{positive_terms.join(' and ')}" if positive_terms.any?
|
|
234
|
-
header += " (excluding: #{negative_terms.join(', ')})" if negative_terms.any?
|
|
214
|
+
positive_terms = positive_terms.first.split(',') if positive_terms.size == 1
|
|
235
215
|
|
|
236
|
-
puts
|
|
216
|
+
puts rubyllm_header(positive_terms, negative_terms)
|
|
237
217
|
puts
|
|
238
218
|
|
|
239
219
|
# modality terms (e.g. "text_to_text") trigger capability checks; the rest
|
|
240
220
|
# are plain substring filters applied to the formatted entry string
|
|
241
|
-
|
|
221
|
+
modality_terms, substring_terms = positive_terms.partition { |q| q.include?('_to_') }
|
|
242
222
|
|
|
243
223
|
counter = 0
|
|
244
|
-
|
|
245
224
|
RubyLLM.models.all.each do |llm|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
inputs = llm.modalities.input.join(',')
|
|
249
|
-
outputs = llm.modalities.output.join(',')
|
|
250
|
-
mode = "#{inputs} to #{outputs}"
|
|
251
|
-
in_1m = llm.pricing.text_tokens.standard.to_h[:input_per_million]
|
|
252
|
-
entry = "- #{llm.id} (#{llm.provider}) in: $#{in_1m} cw: #{cw} mode: #{mode} caps: #{caps}"
|
|
253
|
-
|
|
254
|
-
if positive_terms.empty? && negative_terms.empty?
|
|
255
|
-
counter += 1
|
|
256
|
-
puts entry
|
|
257
|
-
next
|
|
258
|
-
end
|
|
225
|
+
entry = format_rubyllm_entry(llm)
|
|
226
|
+
next unless rubyllm_entry_visible?(llm, entry, modality_terms, substring_terms, negative_terms)
|
|
259
227
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
q2.each { |q| show_it &&= entry.include?(q) }
|
|
263
|
-
negative_terms.each { |q| show_it &&= !entry.downcase.include?(q) }
|
|
264
|
-
|
|
265
|
-
if show_it
|
|
266
|
-
counter += 1
|
|
267
|
-
puts entry
|
|
268
|
-
end
|
|
228
|
+
counter += 1
|
|
229
|
+
puts entry
|
|
269
230
|
end
|
|
270
231
|
|
|
271
232
|
puts if counter.positive?
|
|
272
233
|
puts "#{counter} LLMs matching your query"
|
|
273
234
|
puts
|
|
274
235
|
end
|
|
275
|
-
|
|
236
|
+
|
|
237
|
+
def rubyllm_header(positive_terms, negative_terms)
|
|
238
|
+
header = "\nAvailable LLMs"
|
|
239
|
+
header += " for #{positive_terms.join(' and ')}" if positive_terms.any?
|
|
240
|
+
header += " (excluding: #{negative_terms.join(', ')})" if negative_terms.any?
|
|
241
|
+
header + ':'
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def format_rubyllm_entry(llm)
|
|
245
|
+
modalities = llm.modalities
|
|
246
|
+
mode = "#{modalities.input.join(',')} to #{modalities.output.join(',')}"
|
|
247
|
+
in_1m = llm.pricing.text_tokens.standard.to_h[:input_per_million]
|
|
248
|
+
"- #{llm.id} (#{llm.provider}) in: $#{in_1m} cw: #{llm.context_window} mode: #{mode} caps: #{llm.capabilities.join(',')}"
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def rubyllm_entry_visible?(llm, entry, modality_terms, substring_terms, negative_terms)
|
|
252
|
+
modality_terms.all? { |q| llm.modalities.send("#{q}?") } &&
|
|
253
|
+
substring_terms.all? { |q| entry.include?(q) } &&
|
|
254
|
+
negative_terms.none? { |q| entry.downcase.include?(q) }
|
|
255
|
+
end
|
|
276
256
|
|
|
277
257
|
def normalized_model_search_terms(positive_terms, negative_terms = nil)
|
|
278
258
|
return parse_search_terms(Array(positive_terms)) if negative_terms.nil?
|
|
@@ -9,12 +9,12 @@ module AIA
|
|
|
9
9
|
desc "Show TrakFlow ready tasks and project summary"
|
|
10
10
|
def tasks(args, context_manager = nil)
|
|
11
11
|
bridge = TrakFlowBridge.new
|
|
12
|
-
return "TrakFlow not available. Run 'tf init' to initialize a project." unless bridge.available?
|
|
12
|
+
return report_error("TrakFlow not available. Run 'tf init' to initialize a project.") unless bridge.available?
|
|
13
13
|
|
|
14
14
|
if args.first == "summary"
|
|
15
|
-
bridge.project_summary || "No summary available."
|
|
15
|
+
bridge.project_summary || report_status("No summary available.")
|
|
16
16
|
else
|
|
17
|
-
bridge.check_ready_tasks || "No ready tasks found."
|
|
17
|
+
bridge.check_ready_tasks || report_status("No ready tasks found.")
|
|
18
18
|
end
|
|
19
19
|
end
|
|
20
20
|
alias tf tasks
|
|
@@ -22,23 +22,41 @@ module AIA
|
|
|
22
22
|
desc "Create a TrakFlow plan from a description"
|
|
23
23
|
def plan(args, context_manager = nil)
|
|
24
24
|
bridge = TrakFlowBridge.new
|
|
25
|
-
return "TrakFlow not available. Run 'tf init' to initialize a project." unless bridge.available?
|
|
25
|
+
return report_error("TrakFlow not available. Run 'tf init' to initialize a project.") unless bridge.available?
|
|
26
26
|
|
|
27
27
|
description = args.join(' ')
|
|
28
|
-
return "Usage: /plan <description>" if description.empty?
|
|
28
|
+
return report_error("Usage: /plan <description>") if description.empty?
|
|
29
29
|
|
|
30
|
-
bridge.create_task(description) || "Failed to create plan."
|
|
30
|
+
bridge.create_task(description) || report_error("Failed to create plan.")
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
desc "Create a TrakFlow task"
|
|
34
34
|
def task(args, context_manager = nil)
|
|
35
35
|
bridge = TrakFlowBridge.new
|
|
36
|
-
return "TrakFlow not available. Run 'tf init' to initialize a project." unless bridge.available?
|
|
36
|
+
return report_error("TrakFlow not available. Run 'tf init' to initialize a project.") unless bridge.available?
|
|
37
37
|
|
|
38
38
|
title = args.join(' ')
|
|
39
|
-
return "Usage: /task <title>" if title.empty?
|
|
39
|
+
return report_error("Usage: /task <title>") if title.empty?
|
|
40
40
|
|
|
41
|
-
bridge.create_task(title) || "Failed to create task."
|
|
41
|
+
bridge.create_task(title) || report_error("Failed to create task.")
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
# Log and print a directive error, then return nil so the chat loop
|
|
47
|
+
# skips forwarding it to the robot (an error is not conversational output).
|
|
48
|
+
def report_error(msg)
|
|
49
|
+
AIA::LoggerManager.aia_logger.error(msg)
|
|
50
|
+
puts msg
|
|
51
|
+
nil
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Same as report_error, but for a non-error empty-result status
|
|
55
|
+
# (e.g. "no ready tasks found") rather than a failure.
|
|
56
|
+
def report_status(msg)
|
|
57
|
+
AIA::LoggerManager.aia_logger.info(msg)
|
|
58
|
+
puts msg
|
|
59
|
+
nil
|
|
42
60
|
end
|
|
43
61
|
end
|
|
44
62
|
end
|
|
@@ -6,49 +6,24 @@ require 'word_wrapper'
|
|
|
6
6
|
module AIA
|
|
7
7
|
class UtilityDirectives < Directive
|
|
8
8
|
desc "List available tools (optional filter by name or description substring)"
|
|
9
|
-
# rubocop:disable Metrics/AbcSize
|
|
10
9
|
def tools(args = [], context_manager = nil)
|
|
11
|
-
|
|
12
|
-
spaces = " " * indent
|
|
13
|
-
width = TTY::Screen.width - indent - 2
|
|
14
|
-
filter = args.first&.downcase
|
|
15
|
-
|
|
10
|
+
raw_filter = args.first
|
|
16
11
|
loaded_tools = Array(AIA.config.loaded_tools) + all_mcp_tools
|
|
17
12
|
|
|
18
13
|
if loaded_tools.empty?
|
|
19
14
|
puts "No tools are available"
|
|
20
15
|
else
|
|
21
|
-
tools_to_display = loaded_tools
|
|
22
|
-
|
|
23
|
-
if filter
|
|
24
|
-
tools_to_display = tools_to_display.select do |tool|
|
|
25
|
-
name = tool.respond_to?(:name) ? tool.name : tool.class.name
|
|
26
|
-
desc = tool.respond_to?(:description) ? tool.description.to_s : ""
|
|
27
|
-
"#{name} #{desc}".downcase.include?(filter)
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
|
|
16
|
+
tools_to_display = filter_tools(loaded_tools, raw_filter&.downcase)
|
|
31
17
|
if tools_to_display.empty?
|
|
32
|
-
puts "No tools match the filter: #{
|
|
18
|
+
puts "No tools match the filter: #{raw_filter}"
|
|
33
19
|
else
|
|
34
|
-
|
|
35
|
-
header = filter ? "Available Tools (filtered by '#{args.first}')" : "Available Tools"
|
|
36
|
-
puts header
|
|
37
|
-
puts "=" * header.length
|
|
38
|
-
|
|
39
|
-
tools_to_display.each do |tool|
|
|
40
|
-
name = tool.respond_to?(:name) ? tool.name : tool.class.name
|
|
41
|
-
puts "\n#{name}"
|
|
42
|
-
puts "-" * name.size
|
|
43
|
-
puts WordWrapper::MinimumRaggedness.new(width, tool.description).wrap.split("\n").map { |s| spaces + s + "\n" }.join
|
|
44
|
-
end
|
|
20
|
+
print_tools_report(tools_to_display, raw_filter)
|
|
45
21
|
end
|
|
46
22
|
end
|
|
47
23
|
puts
|
|
48
24
|
|
|
49
25
|
''
|
|
50
26
|
end
|
|
51
|
-
# rubocop:enable Metrics/AbcSize
|
|
52
27
|
|
|
53
28
|
desc "List loaded plugin basenames"
|
|
54
29
|
def plugins(args = [], context_manager = nil)
|
|
@@ -68,57 +43,22 @@ module AIA
|
|
|
68
43
|
end
|
|
69
44
|
|
|
70
45
|
desc "Show MCP server connection status and available tools"
|
|
71
|
-
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
72
46
|
def mcp(args = [], context_manager = nil)
|
|
73
47
|
connected = AIA.config&.connected_mcp_servers || []
|
|
74
48
|
failed = AIA.config&.failed_mcp_servers || []
|
|
75
|
-
defined_count = AIA::Utility.effective_mcp_server_names.size
|
|
76
49
|
|
|
77
50
|
puts
|
|
78
51
|
puts "MCP Server Status"
|
|
79
52
|
puts "================="
|
|
80
|
-
puts "Defined: #{
|
|
53
|
+
puts "Defined: #{AIA::Utility.effective_mcp_server_names.size} " \
|
|
54
|
+
"Connected: #{connected.size} Failed: #{failed.size}"
|
|
81
55
|
puts
|
|
82
56
|
|
|
83
|
-
if connected.any?
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
# Group tools by their MCP server name
|
|
87
|
-
tools_by_server = {}
|
|
88
|
-
connected.each { |name| tools_by_server[name] = [] }
|
|
89
|
-
|
|
90
|
-
mcp_tools.each do |tool|
|
|
91
|
-
server_name = tool.respond_to?(:mcp) ? tool.mcp : nil
|
|
92
|
-
if server_name && tools_by_server.key?(server_name)
|
|
93
|
-
tools_by_server[server_name] << tool
|
|
94
|
-
end
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
puts "Connected Servers:"
|
|
98
|
-
connected.each do |name|
|
|
99
|
-
tools = tools_by_server[name] || []
|
|
100
|
-
puts " #{name} (#{tools.size} tools)"
|
|
101
|
-
tools.each do |tool|
|
|
102
|
-
tool_name = tool.respond_to?(:name) ? tool.name : tool.class.name
|
|
103
|
-
puts " - #{tool_name}"
|
|
104
|
-
end
|
|
105
|
-
end
|
|
106
|
-
puts
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
if failed.any?
|
|
110
|
-
puts "Failed Servers:"
|
|
111
|
-
failed.each do |f|
|
|
112
|
-
name = f[:name] || f['name']
|
|
113
|
-
error = f[:error] || f['error']
|
|
114
|
-
puts " #{name}: #{error}"
|
|
115
|
-
end
|
|
116
|
-
puts
|
|
117
|
-
end
|
|
57
|
+
print_connected_servers(connected) if connected.any?
|
|
58
|
+
print_failed_servers(failed) if failed.any?
|
|
118
59
|
|
|
119
60
|
''
|
|
120
61
|
end
|
|
121
|
-
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
122
62
|
|
|
123
63
|
desc "Show active robot configuration"
|
|
124
64
|
def robots(args = [], context_manager = nil)
|
|
@@ -152,13 +92,72 @@ module AIA
|
|
|
152
92
|
|
|
153
93
|
private
|
|
154
94
|
|
|
95
|
+
def filter_tools(tools, filter)
|
|
96
|
+
return tools unless filter
|
|
97
|
+
|
|
98
|
+
tools.select do |tool|
|
|
99
|
+
"#{ToolIntrospection.tool_name(tool)} #{ToolIntrospection.tool_description(tool)}"
|
|
100
|
+
.downcase.include?(filter)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def print_tools_report(tools_to_display, raw_filter)
|
|
105
|
+
puts
|
|
106
|
+
header = raw_filter ? "Available Tools (filtered by '#{raw_filter}')" : "Available Tools"
|
|
107
|
+
puts header
|
|
108
|
+
puts "=" * header.length
|
|
109
|
+
|
|
110
|
+
indent = 4
|
|
111
|
+
width = TTY::Screen.width - indent - 2
|
|
112
|
+
tools_to_display.each { |tool| print_tool_entry(tool, width, " " * indent) }
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def print_tool_entry(tool, width, spaces)
|
|
116
|
+
name = ToolIntrospection.tool_name(tool)
|
|
117
|
+
puts "\n#{name}"
|
|
118
|
+
puts "-" * name.size
|
|
119
|
+
puts WordWrapper::MinimumRaggedness.new(width, tool.description).wrap.split("\n").map { |s| spaces + s + "\n" }.join
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def print_connected_servers(connected)
|
|
123
|
+
grouped = tools_by_server(connected, all_mcp_tools)
|
|
124
|
+
|
|
125
|
+
puts "Connected Servers:"
|
|
126
|
+
connected.each do |name|
|
|
127
|
+
tools = grouped[name] || []
|
|
128
|
+
puts " #{name} (#{tools.size} tools)"
|
|
129
|
+
tools.each { |tool| puts " - #{ToolIntrospection.tool_name(tool)}" }
|
|
130
|
+
end
|
|
131
|
+
puts
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Group MCP tools under the connected server each one came from.
|
|
135
|
+
def tools_by_server(connected, mcp_tools)
|
|
136
|
+
grouped = connected.to_h { |name| [name, []] }
|
|
137
|
+
mcp_tools.each do |tool|
|
|
138
|
+
server_name = tool.respond_to?(:mcp) ? tool.mcp : nil
|
|
139
|
+
grouped[server_name] << tool if server_name && grouped.key?(server_name)
|
|
140
|
+
end
|
|
141
|
+
grouped
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def print_failed_servers(failed)
|
|
145
|
+
puts "Failed Servers:"
|
|
146
|
+
failed.each do |f|
|
|
147
|
+
puts " #{f[:name] || f['name']}: #{f[:error] || f['error']}"
|
|
148
|
+
end
|
|
149
|
+
puts
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# :reek:TooManyStatements -- mode classification plus sequential report of every crew member
|
|
155
153
|
def show_network(network)
|
|
154
|
+
cfg = AIA.config
|
|
156
155
|
robot_count = network.robot_count
|
|
157
|
-
mode = if
|
|
156
|
+
mode = if cfg.flags.consensus
|
|
158
157
|
"Consensus"
|
|
159
|
-
elsif
|
|
158
|
+
elsif cfg.pipeline.length > 1
|
|
160
159
|
"Pipeline"
|
|
161
|
-
elsif
|
|
160
|
+
elsif cfg.models.length > 1
|
|
162
161
|
"Parallel"
|
|
163
162
|
else
|
|
164
163
|
"Crew" # single-model session wrapped as a one-member crew
|
|
@@ -185,6 +184,7 @@ module AIA
|
|
|
185
184
|
show_robot_detail(bot)
|
|
186
185
|
end
|
|
187
186
|
|
|
187
|
+
# :reek:TooManyStatements -- one labeled line per robot attribute (model, wage, tool counts, role)
|
|
188
188
|
def show_robot_detail(bot)
|
|
189
189
|
puts " #{bot.name}"
|
|
190
190
|
|