aia 1.1.1 → 2.0.0.0.pre.alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +259 -50
- 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 +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 +304 -164
- 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 +108 -142
- 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 -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/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,223 +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?
|
|
64
|
+
def process_initial_context(skip_context_files)
|
|
65
|
+
return if skip_context_files || !AIA.config.context_files || AIA.config.context_files.empty?
|
|
49
66
|
|
|
50
|
-
|
|
67
|
+
context = AIA.config.context_files.map do |file|
|
|
68
|
+
File.read(file) rescue "Error reading file: #{file}"
|
|
69
|
+
end.join("\n\n")
|
|
51
70
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
71
|
+
return if context.empty?
|
|
72
|
+
|
|
73
|
+
result, streamed_content, _elapsed = @streaming_runner.run(@robot, context)
|
|
74
|
+
present_result(result,
|
|
75
|
+
streamed_content: streamed_content,
|
|
76
|
+
ui_presenter: @ui_presenter)
|
|
77
|
+
end
|
|
58
78
|
|
|
59
|
-
|
|
79
|
+
def run_loop
|
|
80
|
+
loop do
|
|
81
|
+
follow_up_prompt = @ui_presenter.ask_question
|
|
82
|
+
break if should_exit_chat?(follow_up_prompt)
|
|
60
83
|
|
|
61
|
-
|
|
62
|
-
return if role_content.nil? || role_content.strip.empty?
|
|
84
|
+
log_user_input(follow_up_prompt)
|
|
63
85
|
|
|
64
|
-
|
|
86
|
+
processed_prompt = resolve_input(follow_up_prompt)
|
|
87
|
+
next if processed_prompt.nil?
|
|
65
88
|
|
|
66
|
-
|
|
89
|
+
next if model_switch_handled?
|
|
90
|
+
next if special_routing_handled?(processed_prompt, @robot)
|
|
67
91
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
chat.add_message(system_msg)
|
|
92
|
+
execute_and_present(processed_prompt, @robot)
|
|
93
|
+
clear_turn_mcp_filter
|
|
71
94
|
end
|
|
72
95
|
end
|
|
73
96
|
|
|
74
|
-
def
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
skills_dir = AIA.config.skills.dir
|
|
79
|
-
bodies = Array(skills).filter_map do |skill_name|
|
|
80
|
-
skill_name = skill_name.to_s.strip
|
|
81
|
-
next if skill_name.empty?
|
|
82
|
-
|
|
83
|
-
skill_path = find_skill_dir(skill_name, skills_dir)
|
|
84
|
-
next unless skill_path
|
|
97
|
+
def should_exit_chat?(input)
|
|
98
|
+
input.nil? || input.strip.downcase == "exit" || input.strip.empty?
|
|
99
|
+
end
|
|
85
100
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
end
|
|
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)
|
|
92
106
|
end
|
|
107
|
+
end
|
|
93
108
|
|
|
94
|
-
|
|
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
|
|
95
122
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
@
|
|
100
|
-
|
|
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
|
|
101
128
|
end
|
|
102
129
|
|
|
103
|
-
def
|
|
104
|
-
return
|
|
130
|
+
def model_switch_handled?
|
|
131
|
+
return false unless @model_switch_handler.handle(HandlerContext.new(config: AIA.config))
|
|
105
132
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
133
|
+
update_robot
|
|
134
|
+
true
|
|
135
|
+
end
|
|
109
136
|
|
|
110
|
-
|
|
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
|
|
111
142
|
|
|
112
|
-
|
|
113
|
-
|
|
143
|
+
if @mention_router.handle(HandlerContext.new(robot: active_robot, prompt: processed_prompt))
|
|
144
|
+
clear_turn_mcp_filter
|
|
145
|
+
return true
|
|
146
|
+
end
|
|
114
147
|
|
|
115
|
-
|
|
116
|
-
@chat_processor.speak(content)
|
|
117
|
-
@ui_presenter.display_separator
|
|
148
|
+
false
|
|
118
149
|
end
|
|
119
150
|
|
|
120
|
-
def
|
|
121
|
-
|
|
122
|
-
return unless File.exist?("/dev/tty") && File.readable?("/dev/tty") && File.writable?("/dev/tty")
|
|
151
|
+
def execute_and_present(processed_prompt, active_robot)
|
|
152
|
+
log_robot_tools(active_robot)
|
|
123
153
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
return if piped_input.empty?
|
|
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
|
|
130
158
|
|
|
131
|
-
|
|
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
|
|
132
172
|
|
|
133
|
-
|
|
134
|
-
|
|
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
|
|
135
180
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
@ui_presenter.display_separator
|
|
181
|
+
def increment_turn_counter
|
|
182
|
+
return unless @robot.respond_to?(:memory) && @robot.memory.respond_to?(:data)
|
|
139
183
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
return
|
|
144
|
-
rescue StandardError => e
|
|
145
|
-
warn "Warning: Error handling piped input: #{e.message}"
|
|
146
|
-
return
|
|
147
|
-
end
|
|
184
|
+
data = @robot.memory.data
|
|
185
|
+
count = data.respond_to?(:turn_count) ? (data.turn_count || 0) : 0
|
|
186
|
+
data.turn_count = count + 1
|
|
148
187
|
end
|
|
149
188
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
|
153
193
|
|
|
154
|
-
|
|
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
|
|
155
199
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
|
161
205
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
|
211
|
+
end
|
|
166
212
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
rescue StandardError => e
|
|
170
|
-
@ui_presenter.display_info("Error: #{e.class}: #{e.message}")
|
|
171
|
-
next
|
|
172
|
-
end
|
|
213
|
+
def process_directive(follow_up_prompt)
|
|
214
|
+
directive_output = @directive_processor.process(follow_up_prompt, nil)
|
|
173
215
|
|
|
174
|
-
|
|
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)
|
|
221
|
+
@ui_presenter.display_info(directive_output) unless directive_output.nil? || directive_output.strip.empty?
|
|
222
|
+
return nil
|
|
223
|
+
end
|
|
175
224
|
|
|
176
|
-
|
|
177
|
-
content = response_data[:content]
|
|
178
|
-
metrics = response_data[:metrics]
|
|
179
|
-
multi_metrics = response_data[:multi_metrics]
|
|
180
|
-
else
|
|
181
|
-
content = response_data
|
|
182
|
-
metrics = nil
|
|
183
|
-
multi_metrics = nil
|
|
184
|
-
end
|
|
225
|
+
return nil if directive_output.nil? || directive_output.strip.empty?
|
|
185
226
|
|
|
186
|
-
|
|
227
|
+
puts "\n#{directive_output}\n"
|
|
228
|
+
"I executed this directive: #{follow_up_prompt}\nHere's the output: #{directive_output}\nLet's continue our conversation."
|
|
229
|
+
end
|
|
187
230
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
|
195
238
|
|
|
196
|
-
|
|
197
|
-
|
|
239
|
+
if defined?(SimpleFlow::Result) && result.is_a?(SimpleFlow::Result)
|
|
240
|
+
display_network_metrics(result)
|
|
241
|
+
return
|
|
198
242
|
end
|
|
199
|
-
end
|
|
200
243
|
|
|
201
|
-
|
|
202
|
-
|
|
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
|
|
203
256
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
+
}
|
|
207
297
|
end
|
|
208
298
|
|
|
209
|
-
return
|
|
299
|
+
return if metrics_list.empty?
|
|
210
300
|
|
|
211
|
-
|
|
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] }
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
@ui_presenter.display_multi_model_metrics(metrics_list)
|
|
212
308
|
end
|
|
213
309
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
|
217
316
|
end
|
|
218
317
|
|
|
219
|
-
#
|
|
220
|
-
|
|
221
|
-
|
|
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?
|
|
222
322
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
current_content = []
|
|
323
|
+
# active_robot is always a Network; resolve to the chief for tool inspection.
|
|
324
|
+
target = robot.respond_to?(:chief) ? robot.chief : robot
|
|
226
325
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
|
232
337
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
internal_id = internal_id.sub(/\s+#/, '#')
|
|
338
|
+
File.open(out_file, "a") { |f| f.puts "\nYou: #{input}" }
|
|
339
|
+
end
|
|
236
340
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
|
241
375
|
end
|
|
242
376
|
end
|
|
377
|
+
rescue StandardError => e
|
|
378
|
+
$stderr.puts "Warning: Speech failed: #{e.message}"
|
|
379
|
+
end
|
|
243
380
|
|
|
244
|
-
|
|
245
|
-
|
|
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
|
|
246
388
|
end
|
|
247
|
-
|
|
248
|
-
responses
|
|
249
389
|
end
|
|
250
390
|
end
|
|
251
391
|
end
|