aia 1.1.0 → 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.
- checksums.yaml +4 -4
- data/.envrc +5 -1
- data/.loki +231 -0
- data/.quality/flay_baseline.txt +1 -0
- data/.quality/flog_baseline.txt +29 -0
- data/.quality/reek_baseline.txt +80 -0
- data/.rubocop.yml +116 -0
- data/.version +1 -1
- data/CHANGELOG.md +266 -42
- data/IMPLEMENTATION_PLAN.md +506 -0
- data/README.md +266 -238
- data/Rakefile +118 -5
- data/architecture_review.md +314 -0
- data/bin/aia +16 -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 +19 -0
- data/examples/context/tech_stack.md +2 -2
- data/examples/prompts_dir/project_summary +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 +306 -159
- data/lib/aia/config/cli_parser.rb +174 -111
- data/lib/aia/config/defaults.yml +62 -33
- data/lib/aia/config/mcp_parser.rb +39 -46
- data/lib/aia/config/model_spec.rb +34 -2
- data/lib/aia/config/validator.rb +121 -138
- data/lib/aia/config.rb +110 -145
- data/lib/aia/content_extractor.rb +153 -0
- data/lib/aia/cost_calculator.rb +38 -0
- data/lib/aia/crew.rb +164 -0
- data/lib/aia/debate_handler.rb +166 -0
- data/lib/aia/delegate_handler.rb +112 -0
- data/lib/aia/directive.rb +33 -18
- data/lib/aia/directive_processor.rb +16 -7
- data/lib/aia/directives/configuration_directives.rb +160 -20
- data/lib/aia/directives/context_directives.rb +38 -26
- data/lib/aia/directives/execution_directives.rb +136 -4
- data/lib/aia/directives/model_directives.rb +76 -34
- data/lib/aia/directives/trakflow_directives.rb +44 -0
- data/lib/aia/directives/utility_directives.rb +203 -6
- data/lib/aia/directives/web_and_file_directives.rb +96 -60
- 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 +448 -0
- data/lib/aia/logger.rb +24 -4
- data/lib/aia/mcp_config_normalizer.rb +35 -0
- data/lib/aia/mcp_connection_manager.rb +305 -0
- data/lib/aia/mcp_discovery.rb +44 -0
- data/lib/aia/mcp_grouper.rb +33 -0
- data/lib/aia/mcp_utility.rb +57 -0
- data/lib/aia/mention_router.rb +260 -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 +155 -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 +262 -0
- data/lib/aia/plugin_loader.rb +170 -0
- data/lib/aia/plugin_monitor.rb +208 -0
- data/lib/aia/prompt_decomposer.rb +157 -0
- data/lib/aia/prompt_handler.rb +19 -39
- data/lib/aia/robot_builder.rb +51 -0
- data/lib/aia/robot_factory.rb +334 -0
- data/lib/aia/robot_namer.rb +116 -0
- data/lib/aia/session.rb +83 -17
- data/lib/aia/session_tracker.rb +209 -0
- data/lib/aia/similarity_scorer.rb +39 -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 +302 -0
- data/lib/aia/startup_coordinator.rb +150 -0
- data/lib/aia/streaming_runner.rb +169 -0
- data/lib/aia/system_prompt_assembler.rb +88 -0
- data/lib/aia/task_coordinator.rb +202 -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/tool_filter/tfidf.rb +116 -0
- data/lib/aia/tool_filter/wordnet_expander.rb +127 -0
- data/lib/aia/tool_filter.rb +82 -0
- data/lib/aia/tool_filter_registry.rb +30 -0
- data/lib/aia/tool_filter_strategy.rb +143 -0
- data/lib/aia/tool_loader.rb +210 -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 +114 -0
- data/lib/aia/trakflow_bridge.rb +173 -0
- data/lib/aia/turn_state.rb +94 -0
- data/lib/aia/ui_presenter.rb +166 -198
- data/lib/aia/utility.rb +134 -87
- data/lib/aia/{history_manager.rb → variable_input_collector.rb} +8 -9
- data/lib/aia/verification_network.rb +58 -0
- data/lib/aia.rb +108 -63
- data/mkdocs.yml +1 -0
- metadata +179 -56
- 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 -164
- 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/chat_loop.rb
CHANGED
|
@@ -1,27 +1,53 @@
|
|
|
1
|
-
# lib/aia/chat_loop.rb
|
|
2
1
|
# frozen_string_literal: true
|
|
3
2
|
|
|
3
|
+
# lib/aia/chat_loop.rb
|
|
4
|
+
#
|
|
5
|
+
# Thin interactive chat shell for AIA v2.
|
|
6
|
+
# Delegates to StreamingRunner, MentionRouter, and SpecialModeHandler
|
|
7
|
+
# for execution concerns. Owns the REPL loop, directive processing,
|
|
8
|
+
# expert routing, and output coordination.
|
|
9
|
+
|
|
4
10
|
require "reline"
|
|
5
11
|
require "pm"
|
|
6
12
|
|
|
7
13
|
module AIA
|
|
8
14
|
class ChatLoop
|
|
9
|
-
include
|
|
15
|
+
include ContentExtractor
|
|
10
16
|
|
|
11
|
-
def initialize(
|
|
12
|
-
|
|
13
|
-
@
|
|
17
|
+
def initialize(robot, ui_presenter, directive_processor,
|
|
18
|
+
session_tracker: nil, alias_registry: nil, filters: {})
|
|
19
|
+
@robot = robot
|
|
20
|
+
@ui_presenter = ui_presenter
|
|
14
21
|
@directive_processor = directive_processor
|
|
22
|
+
@tracker = session_tracker || SessionTracker.new
|
|
23
|
+
@alias_registry = alias_registry || ModelAliasRegistry.new
|
|
24
|
+
@model_switch_handler = ModelSwitchHandler.new(@alias_registry, @ui_presenter)
|
|
25
|
+
@filters = filters
|
|
26
|
+
|
|
27
|
+
@streaming_runner = StreamingRunner.new
|
|
28
|
+
@mention_router = MentionRouter.new(
|
|
29
|
+
ui_presenter: @ui_presenter,
|
|
30
|
+
tracker: @tracker,
|
|
31
|
+
streaming_runner: @streaming_runner
|
|
32
|
+
)
|
|
33
|
+
@special_mode_handler = SpecialModeHandler.new(
|
|
34
|
+
robot: @robot,
|
|
35
|
+
ui_presenter: @ui_presenter,
|
|
36
|
+
tracker: @tracker
|
|
37
|
+
)
|
|
38
|
+
@tool_filter_strategy = ToolFilterStrategy.new(
|
|
39
|
+
filters: filters,
|
|
40
|
+
ui_presenter: @ui_presenter
|
|
41
|
+
)
|
|
15
42
|
end
|
|
16
43
|
|
|
17
44
|
# Start the interactive chat session
|
|
18
45
|
def start(skip_context_files: false)
|
|
19
46
|
setup_session
|
|
20
|
-
process_role_context
|
|
21
|
-
process_skill_context
|
|
22
47
|
process_initial_context(skip_context_files)
|
|
23
|
-
handle_piped_input
|
|
24
48
|
run_loop
|
|
49
|
+
rescue StandardError => e
|
|
50
|
+
AIA.debug_warn("ChatLoop error: #{e.class}: #{e.message}", exc: e)
|
|
25
51
|
ensure
|
|
26
52
|
@ui_presenter.display_chat_end
|
|
27
53
|
end
|
|
@@ -29,216 +55,337 @@ module AIA
|
|
|
29
55
|
private
|
|
30
56
|
|
|
31
57
|
def setup_session
|
|
32
|
-
initialize_ui
|
|
33
|
-
setup_signals
|
|
34
|
-
Reline::HISTORY.clear
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def initialize_ui
|
|
38
58
|
puts "\nEntering interactive chat mode..."
|
|
39
59
|
@ui_presenter.display_chat_header
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def setup_signals
|
|
43
60
|
Signal.trap("INT") { exit }
|
|
61
|
+
@ui_presenter.load_chat_history
|
|
44
62
|
end
|
|
45
63
|
|
|
46
|
-
def
|
|
47
|
-
|
|
48
|
-
return if role.nil? || role.empty?
|
|
49
|
-
|
|
50
|
-
prompt_handler = AIA::PromptHandler.new
|
|
51
|
-
role_parsed = prompt_handler.fetch_role(role)
|
|
52
|
-
return if role_parsed.nil?
|
|
53
|
-
|
|
54
|
-
role_content = role_parsed.to_s
|
|
55
|
-
return if role_content.nil? || role_content.strip.empty?
|
|
64
|
+
def process_initial_context(skip_context_files)
|
|
65
|
+
return if skip_context_files || !AIA.config.context_files || AIA.config.context_files.empty?
|
|
56
66
|
|
|
57
|
-
|
|
67
|
+
context = AIA.config.context_files.map do |file|
|
|
68
|
+
File.read(file) rescue "Error reading file: #{file}"
|
|
69
|
+
end.join("\n\n")
|
|
58
70
|
|
|
59
|
-
|
|
71
|
+
return if context.empty?
|
|
60
72
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
73
|
+
result, streamed_content, _elapsed = @streaming_runner.run(@robot, context)
|
|
74
|
+
present_result(result,
|
|
75
|
+
streamed_content: streamed_content,
|
|
76
|
+
ui_presenter: @ui_presenter)
|
|
65
77
|
end
|
|
66
78
|
|
|
67
|
-
def
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
skills_dir = AIA.config.skills.dir
|
|
72
|
-
bodies = Array(skills).filter_map do |skill_name|
|
|
73
|
-
skill_name = skill_name.to_s.strip
|
|
74
|
-
next if skill_name.empty?
|
|
79
|
+
def run_loop
|
|
80
|
+
loop do
|
|
81
|
+
follow_up_prompt = @ui_presenter.ask_question
|
|
82
|
+
break if should_exit_chat?(follow_up_prompt)
|
|
75
83
|
|
|
76
|
-
|
|
77
|
-
next unless skill_path
|
|
84
|
+
log_user_input(follow_up_prompt)
|
|
78
85
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
else
|
|
82
|
-
md = File.join(skill_path, 'SKILL.md')
|
|
83
|
-
skill_body(File.read(md)) if File.exist?(md)
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
+
processed_prompt = resolve_input(follow_up_prompt)
|
|
87
|
+
next if processed_prompt.nil?
|
|
86
88
|
|
|
87
|
-
|
|
89
|
+
next if model_switch_handled?
|
|
90
|
+
next if special_routing_handled?(processed_prompt, @robot)
|
|
88
91
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
@chat_processor.output_response(content)
|
|
93
|
-
@ui_presenter.display_separator
|
|
92
|
+
execute_and_present(processed_prompt, @robot)
|
|
93
|
+
clear_turn_mcp_filter
|
|
94
|
+
end
|
|
94
95
|
end
|
|
95
96
|
|
|
96
|
-
def
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
context = AIA.config.context_files.map do |file|
|
|
100
|
-
File.read(file) rescue "Error reading file: #{file}"
|
|
101
|
-
end.join("\n\n")
|
|
102
|
-
|
|
103
|
-
return if context.empty?
|
|
104
|
-
|
|
105
|
-
response_data = @chat_processor.process_prompt(context)
|
|
106
|
-
content = response_data.is_a?(Hash) ? response_data[:content] : response_data
|
|
107
|
-
|
|
108
|
-
@chat_processor.output_response(content)
|
|
109
|
-
@chat_processor.speak(content)
|
|
110
|
-
@ui_presenter.display_separator
|
|
97
|
+
def should_exit_chat?(input)
|
|
98
|
+
input.nil? || input.strip.downcase == "exit" || input.strip.empty?
|
|
111
99
|
end
|
|
112
100
|
|
|
113
|
-
def
|
|
114
|
-
|
|
115
|
-
|
|
101
|
+
def resolve_input(follow_up_prompt)
|
|
102
|
+
if follow_up_prompt.strip.start_with?('/')
|
|
103
|
+
resolve_directive(follow_up_prompt)
|
|
104
|
+
else
|
|
105
|
+
parse_prompt_string(follow_up_prompt)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
116
108
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
109
|
+
def resolve_directive(follow_up_prompt)
|
|
110
|
+
if @directive_processor.directive?(follow_up_prompt)
|
|
111
|
+
result = process_directive(follow_up_prompt)
|
|
112
|
+
# A directive (e.g. /model, /config) may have rebuilt AIA.client;
|
|
113
|
+
# re-bind so the next prompt uses the new model.
|
|
114
|
+
rebind_robot_if_rebuilt
|
|
115
|
+
result
|
|
116
|
+
else
|
|
117
|
+
name = follow_up_prompt.strip.split.first
|
|
118
|
+
@ui_presenter.display_info("Unknown directive: #{name} (use /help to see available directives)")
|
|
119
|
+
nil
|
|
120
|
+
end
|
|
121
|
+
end
|
|
121
122
|
|
|
122
|
-
|
|
123
|
+
def parse_prompt_string(raw)
|
|
124
|
+
PM.parse_string(raw).to_s
|
|
125
|
+
rescue StandardError => e
|
|
126
|
+
@ui_presenter.display_info("Error: #{e.class}: #{e.message}")
|
|
127
|
+
nil
|
|
128
|
+
end
|
|
123
129
|
|
|
124
|
-
|
|
130
|
+
def model_switch_handled?
|
|
131
|
+
return false unless @model_switch_handler.handle(HandlerContext.new(config: AIA.config))
|
|
125
132
|
|
|
126
|
-
|
|
127
|
-
|
|
133
|
+
update_robot
|
|
134
|
+
true
|
|
135
|
+
end
|
|
128
136
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
137
|
+
def special_routing_handled?(processed_prompt, active_robot)
|
|
138
|
+
if @special_mode_handler.handle(processed_prompt)
|
|
139
|
+
clear_turn_mcp_filter
|
|
140
|
+
return true
|
|
141
|
+
end
|
|
132
142
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
return
|
|
137
|
-
rescue StandardError => e
|
|
138
|
-
warn "Warning: Error handling piped input: #{e.message}"
|
|
139
|
-
return
|
|
143
|
+
if @mention_router.handle(HandlerContext.new(robot: active_robot, prompt: processed_prompt))
|
|
144
|
+
clear_turn_mcp_filter
|
|
145
|
+
return true
|
|
140
146
|
end
|
|
147
|
+
|
|
148
|
+
false
|
|
141
149
|
end
|
|
142
150
|
|
|
143
|
-
def
|
|
144
|
-
|
|
145
|
-
follow_up_prompt = @ui_presenter.ask_question
|
|
151
|
+
def execute_and_present(processed_prompt, active_robot)
|
|
152
|
+
log_robot_tools(active_robot)
|
|
146
153
|
|
|
147
|
-
|
|
154
|
+
resolved_tools = @tool_filter_strategy.resolve(processed_prompt)
|
|
155
|
+
if (AIA.debug? || AIA.verbose?) && resolved_tools
|
|
156
|
+
puts "\nFiltered tools (#{resolved_tools.size}): #{resolved_tools.join(', ')}"
|
|
157
|
+
end
|
|
148
158
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
159
|
+
# Plain turn runs the chief directly; aggregation modes run the whole network.
|
|
160
|
+
turn_target = aggregation_mode? ? active_robot : active_robot.chief
|
|
161
|
+
result, streamed_content, elapsed = run_streaming_turn(turn_target, processed_prompt, resolved_tools)
|
|
162
|
+
return unless result
|
|
163
|
+
|
|
164
|
+
increment_turn_counter
|
|
165
|
+
present_result(result,
|
|
166
|
+
streamed_content: streamed_content,
|
|
167
|
+
prompt: processed_prompt,
|
|
168
|
+
elapsed: elapsed,
|
|
169
|
+
ui_presenter: @ui_presenter,
|
|
170
|
+
tracker: @tracker)
|
|
171
|
+
end
|
|
154
172
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
173
|
+
def run_streaming_turn(turn_target, processed_prompt, resolved_tools)
|
|
174
|
+
@streaming_runner.run(turn_target, processed_prompt, tools: resolved_tools)
|
|
175
|
+
rescue StandardError => e
|
|
176
|
+
@ui_presenter.display_info("Error communicating with AI: #{e.class}: #{e.message}")
|
|
177
|
+
clear_turn_mcp_filter
|
|
178
|
+
nil
|
|
179
|
+
end
|
|
159
180
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
rescue StandardError => e
|
|
163
|
-
@ui_presenter.display_info("Error: #{e.class}: #{e.message}")
|
|
164
|
-
next
|
|
165
|
-
end
|
|
181
|
+
def increment_turn_counter
|
|
182
|
+
return unless @robot.respond_to?(:memory) && @robot.memory.respond_to?(:data)
|
|
166
183
|
|
|
167
|
-
|
|
184
|
+
data = @robot.memory.data
|
|
185
|
+
count = data.respond_to?(:turn_count) ? (data.turn_count || 0) : 0
|
|
186
|
+
data.turn_count = count + 1
|
|
187
|
+
end
|
|
168
188
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
else
|
|
174
|
-
content = response_data
|
|
175
|
-
metrics = nil
|
|
176
|
-
multi_metrics = nil
|
|
177
|
-
end
|
|
189
|
+
# Clear per-turn MCP server filter so next turn sees all
|
|
190
|
+
def clear_turn_mcp_filter
|
|
191
|
+
AIA.turn_state.active_mcp_servers = nil
|
|
192
|
+
end
|
|
178
193
|
|
|
179
|
-
|
|
194
|
+
# Update robot reference after a model switch and propagate to sub-components
|
|
195
|
+
def update_robot
|
|
196
|
+
@robot = AIA.client
|
|
197
|
+
@special_mode_handler.robot = @robot
|
|
198
|
+
end
|
|
180
199
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
end
|
|
187
|
-
end
|
|
200
|
+
# Re-bind @robot when a directive rebuilt AIA.client into a different robot
|
|
201
|
+
# (e.g. /model). No-op when the client is unchanged or unset.
|
|
202
|
+
def rebind_robot_if_rebuilt
|
|
203
|
+
update_robot if AIA.client && !AIA.client.equal?(@robot)
|
|
204
|
+
end
|
|
188
205
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
206
|
+
# True when the crew should run as an aggregation network (consensus /
|
|
207
|
+
# parallel / pipeline) rather than routing a plain turn to the chief.
|
|
208
|
+
def aggregation_mode?
|
|
209
|
+
cfg = AIA.config
|
|
210
|
+
cfg.flags.consensus || Array(cfg.pipeline).length > 1 || Array(cfg.models).length > 1
|
|
192
211
|
end
|
|
193
212
|
|
|
194
213
|
def process_directive(follow_up_prompt)
|
|
195
214
|
directive_output = @directive_processor.process(follow_up_prompt, nil)
|
|
196
215
|
|
|
197
|
-
|
|
216
|
+
# These directives either mutate state or set a mode flag for the NEXT
|
|
217
|
+
# prompt. Display their confirmation and return nil so the loop skips
|
|
218
|
+
# forwarding them to the robot (which would fire the handler prematurely
|
|
219
|
+
# on wrapper text and consume the flag before the real prompt arrives).
|
|
220
|
+
if @directive_processor.state_setting?(follow_up_prompt)
|
|
198
221
|
@ui_presenter.display_info(directive_output) unless directive_output.nil? || directive_output.strip.empty?
|
|
199
222
|
return nil
|
|
200
223
|
end
|
|
201
224
|
|
|
202
225
|
return nil if directive_output.nil? || directive_output.strip.empty?
|
|
203
226
|
|
|
204
|
-
handle_successful_directive(follow_up_prompt, directive_output)
|
|
205
|
-
end
|
|
206
|
-
|
|
207
|
-
def handle_successful_directive(follow_up_prompt, directive_output)
|
|
208
227
|
puts "\n#{directive_output}\n"
|
|
209
228
|
"I executed this directive: #{follow_up_prompt}\nHere's the output: #{directive_output}\nLet's continue our conversation."
|
|
210
229
|
end
|
|
211
230
|
|
|
212
|
-
#
|
|
213
|
-
|
|
214
|
-
|
|
231
|
+
# Display token metrics if enabled.
|
|
232
|
+
# Handles both single-robot results and multi-model network results.
|
|
233
|
+
#
|
|
234
|
+
# Token data lives on the raw RubyLLM::Message stored in
|
|
235
|
+
# RobotResult#raw (RobotLab::Message objects do not carry usage info).
|
|
236
|
+
def display_metrics(result, elapsed: nil)
|
|
237
|
+
return unless AIA.config.flags.tokens
|
|
215
238
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
239
|
+
if defined?(SimpleFlow::Result) && result.is_a?(SimpleFlow::Result)
|
|
240
|
+
display_network_metrics(result)
|
|
241
|
+
return
|
|
242
|
+
end
|
|
219
243
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
244
|
+
raw = result.respond_to?(:raw) ? result.raw : nil
|
|
245
|
+
return unless raw && raw.respond_to?(:input_tokens) && raw.input_tokens
|
|
246
|
+
|
|
247
|
+
model_id = extract_model_id(raw) || AIA.config.models.first.name
|
|
248
|
+
metrics = {
|
|
249
|
+
model_id: model_id,
|
|
250
|
+
input_tokens: raw.input_tokens,
|
|
251
|
+
output_tokens: raw.output_tokens,
|
|
252
|
+
elapsed: elapsed
|
|
253
|
+
}
|
|
254
|
+
@ui_presenter.display_token_metrics(metrics)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Extract per-robot metrics from a network SimpleFlow::Result
|
|
258
|
+
# and display the multi-model cost table.
|
|
259
|
+
#
|
|
260
|
+
# Each robot_result.raw holds the original RubyLLM::Message with
|
|
261
|
+
# input_tokens, output_tokens, and model_id.
|
|
262
|
+
# Each robot_result.duration holds the elapsed seconds.
|
|
263
|
+
# Similarity scores compare each model's response text against the
|
|
264
|
+
# first model using TF-IDF cosine similarity.
|
|
265
|
+
def display_network_metrics(flow_result)
|
|
266
|
+
metrics_list = []
|
|
267
|
+
response_texts = []
|
|
268
|
+
|
|
269
|
+
flow_result.context.each do |task_name, robot_result|
|
|
270
|
+
next if task_name == :run_params
|
|
271
|
+
next unless robot_result.respond_to?(:raw)
|
|
272
|
+
|
|
273
|
+
raw = robot_result.raw
|
|
274
|
+
next unless raw && raw.respond_to?(:input_tokens) && raw.input_tokens
|
|
275
|
+
|
|
276
|
+
model_id = extract_model_id(raw)
|
|
277
|
+
display_name = robot_result.respond_to?(:robot_name) ? robot_result.robot_name : task_name.to_s
|
|
278
|
+
elapsed = robot_result.respond_to?(:duration) ? robot_result.duration : nil
|
|
279
|
+
|
|
280
|
+
# Collect response text for similarity scoring
|
|
281
|
+
text = if robot_result.respond_to?(:reply)
|
|
282
|
+
robot_result.reply.to_s
|
|
283
|
+
elsif robot_result.respond_to?(:content)
|
|
284
|
+
robot_result.content.to_s
|
|
285
|
+
else
|
|
286
|
+
""
|
|
287
|
+
end
|
|
288
|
+
response_texts << text
|
|
289
|
+
|
|
290
|
+
metrics_list << {
|
|
291
|
+
model_id: model_id || display_name,
|
|
292
|
+
display_name: display_name,
|
|
293
|
+
input_tokens: raw.input_tokens || 0,
|
|
294
|
+
output_tokens: raw.output_tokens || 0,
|
|
295
|
+
elapsed: elapsed
|
|
296
|
+
}
|
|
297
|
+
end
|
|
225
298
|
|
|
226
|
-
|
|
227
|
-
internal_id = display_name.sub(/\s+\([^)]+\)\s*$/, '')
|
|
228
|
-
internal_id = internal_id.sub(/\s+#/, '#')
|
|
299
|
+
return if metrics_list.empty?
|
|
229
300
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
end
|
|
301
|
+
# Compute TF-IDF similarity against the first model's response
|
|
302
|
+
if metrics_list.size > 1
|
|
303
|
+
scores = SimilarityScorer.score(response_texts)
|
|
304
|
+
metrics_list.each_with_index { |m, i| m[:similarity] = scores[i] }
|
|
235
305
|
end
|
|
236
306
|
|
|
237
|
-
|
|
238
|
-
|
|
307
|
+
@ui_presenter.display_multi_model_metrics(metrics_list)
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# Pull the actual model identifier from a RubyLLM response message
|
|
311
|
+
# so cost calculation can look up pricing.
|
|
312
|
+
def extract_model_id(message)
|
|
313
|
+
return message.model_id if message.respond_to?(:model_id) && message.model_id
|
|
314
|
+
return message.model if message.respond_to?(:model) && message.model
|
|
315
|
+
nil
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# Show the actual tools available to the robot.
|
|
319
|
+
# Only prints when --debug is enabled.
|
|
320
|
+
def log_robot_tools(robot)
|
|
321
|
+
return unless AIA.debug?
|
|
322
|
+
|
|
323
|
+
# active_robot is always a Network; resolve to the chief for tool inspection.
|
|
324
|
+
target = robot.respond_to?(:chief) ? robot.chief : robot
|
|
325
|
+
|
|
326
|
+
local = Array(target.local_tools).map { |t| t.respond_to?(:name) ? t.name : t.class.name }
|
|
327
|
+
mcp = Array(target.mcp_tools).map { |t| t.respond_to?(:name) ? t.name : t.class.name }
|
|
328
|
+
|
|
329
|
+
$stderr.puts "[DEBUG] Tool filter strategy: #{@tool_filter_strategy.active_strategy_label}"
|
|
330
|
+
$stderr.puts "[DEBUG] Robot local_tools (#{local.size}): #{local.join(', ')}"
|
|
331
|
+
$stderr.puts "[DEBUG] Robot mcp_tools (#{mcp.size}): #{mcp.join(', ')}"
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def log_user_input(input)
|
|
335
|
+
out_file = AIA.config.output.file
|
|
336
|
+
return unless out_file
|
|
337
|
+
|
|
338
|
+
File.open(out_file, "a") { |f| f.puts "\nYou: #{input}" }
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def speak(content)
|
|
342
|
+
return unless AIA.speak?
|
|
343
|
+
|
|
344
|
+
audio = AIA.config.audio
|
|
345
|
+
command = audio.speak_command || 'say'
|
|
346
|
+
env = {}
|
|
347
|
+
env['SPEECH_MODEL'] = audio.speech_model if audio.speech_model
|
|
348
|
+
|
|
349
|
+
if command == 'say'
|
|
350
|
+
# Local TTS: say converts and plays in one step
|
|
351
|
+
run_with_spinner("Speaking...") do
|
|
352
|
+
if audio.voice && !audio.voice.to_s.strip.empty?
|
|
353
|
+
system(env, command, '-v', audio.voice, content.to_s)
|
|
354
|
+
else
|
|
355
|
+
system(env, command, content.to_s)
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
else
|
|
359
|
+
# Custom TTS script: stage 2 = convert text → audio file,
|
|
360
|
+
# stage 3 = play the file. AIA passes the output path as $2.
|
|
361
|
+
require 'tempfile'
|
|
362
|
+
tmpfile = Tempfile.new(['aia-tts-', '.mp3'])
|
|
363
|
+
tmpfile.close
|
|
364
|
+
begin
|
|
365
|
+
run_with_spinner("Converting to audio...") do
|
|
366
|
+
system(env, command, content.to_s, tmpfile.path)
|
|
367
|
+
end
|
|
368
|
+
if File.size?(tmpfile.path)
|
|
369
|
+
run_with_spinner("Speaking...") do
|
|
370
|
+
system('afplay', tmpfile.path)
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
ensure
|
|
374
|
+
tmpfile.unlink
|
|
375
|
+
end
|
|
239
376
|
end
|
|
377
|
+
rescue StandardError => e
|
|
378
|
+
$stderr.puts "Warning: Speech failed: #{e.message}"
|
|
379
|
+
end
|
|
240
380
|
|
|
241
|
-
|
|
381
|
+
def run_with_spinner(message)
|
|
382
|
+
spinner = TTY::Spinner.new("[:spinner] #{message}", format: :bouncing_ball, output: $stderr)
|
|
383
|
+
spinner.auto_spin
|
|
384
|
+
begin
|
|
385
|
+
yield
|
|
386
|
+
ensure
|
|
387
|
+
spinner.stop
|
|
388
|
+
end
|
|
242
389
|
end
|
|
243
390
|
end
|
|
244
391
|
end
|