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
|
@@ -1,242 +0,0 @@
|
|
|
1
|
-
# lib/aia/adapter/chat_execution.rb
|
|
2
|
-
# frozen_string_literal: true
|
|
3
|
-
|
|
4
|
-
module AIA
|
|
5
|
-
module Adapter
|
|
6
|
-
module ChatExecution
|
|
7
|
-
def chat(prompt)
|
|
8
|
-
if @models.size == 1
|
|
9
|
-
single_model_chat(prompt, @models.first)
|
|
10
|
-
else
|
|
11
|
-
multi_model_chat(prompt)
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def single_model_chat(prompt, internal_id)
|
|
16
|
-
chat_instance = @chats[internal_id]
|
|
17
|
-
modes = chat_instance.model.modalities
|
|
18
|
-
|
|
19
|
-
# TODO: Need to consider how to handle multi-mode models
|
|
20
|
-
if modes.text_to_text?
|
|
21
|
-
text_to_text_single(prompt, internal_id)
|
|
22
|
-
elsif modes.image_to_text?
|
|
23
|
-
image_to_text_single(prompt, internal_id)
|
|
24
|
-
elsif modes.text_to_image?
|
|
25
|
-
text_to_image_single(prompt, internal_id)
|
|
26
|
-
elsif modes.text_to_audio?
|
|
27
|
-
text_to_audio_single(prompt, internal_id)
|
|
28
|
-
elsif modes.audio_to_text?
|
|
29
|
-
audio_to_text_single(prompt, internal_id)
|
|
30
|
-
else
|
|
31
|
-
# TODO: what else can be done?
|
|
32
|
-
"Error: No matching modality for model #{internal_id}"
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
# Clear the chat context/history
|
|
37
|
-
# Needed for the /clear and /restore directives
|
|
38
|
-
# Simplified with ADR-002: Each model has isolated context, no global state to manage
|
|
39
|
-
def clear_context
|
|
40
|
-
old_chats = @chats.dup
|
|
41
|
-
new_chats = {}
|
|
42
|
-
|
|
43
|
-
@models.each do |model_name|
|
|
44
|
-
begin
|
|
45
|
-
# Get the isolated context for this model
|
|
46
|
-
context = @contexts[model_name]
|
|
47
|
-
actual_model, provider = extract_model_and_provider(model_name)
|
|
48
|
-
|
|
49
|
-
# Create a fresh chat instance from the same isolated context
|
|
50
|
-
chat = if provider
|
|
51
|
-
context.chat(model: actual_model, provider: provider, assume_model_exists: true)
|
|
52
|
-
else
|
|
53
|
-
context.chat(model: actual_model)
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
# Re-add tools if they were previously loaded
|
|
57
|
-
if @tools && !@tools.empty? && chat.model&.supports_functions?
|
|
58
|
-
chat.with_tools(*@tools)
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
new_chats[model_name] = chat
|
|
62
|
-
rescue StandardError => e
|
|
63
|
-
# If recreation fails, keep the old chat but clear its messages
|
|
64
|
-
warn "Warning: Could not recreate chat for #{model_name}: #{e.message}. Clearing existing chat."
|
|
65
|
-
chat = old_chats[model_name]
|
|
66
|
-
if chat&.instance_variable_defined?(:@messages)
|
|
67
|
-
chat.instance_variable_set(:@messages, [])
|
|
68
|
-
end
|
|
69
|
-
chat.clear_history if chat&.respond_to?(:clear_history)
|
|
70
|
-
new_chats[model_name] = chat
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
@chats = new_chats
|
|
75
|
-
'Chat context successfully cleared.'
|
|
76
|
-
rescue StandardError => e
|
|
77
|
-
"Error clearing chat context: #{e.message}"
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
def setup_chats_with_tools
|
|
81
|
-
valid_chats = {}
|
|
82
|
-
valid_contexts = {}
|
|
83
|
-
valid_specs = []
|
|
84
|
-
failed_models = []
|
|
85
|
-
|
|
86
|
-
@model_specs.each do |spec|
|
|
87
|
-
model_name = spec[:model] # Actual model name (e.g., "gpt-4o")
|
|
88
|
-
internal_id = spec[:internal_id] # Key for storage (e.g., "gpt-4o#1", "gpt-4o#2")
|
|
89
|
-
|
|
90
|
-
begin
|
|
91
|
-
# Create isolated context for this model to prevent cross-talk (ADR-002)
|
|
92
|
-
context = create_isolated_context_for_model(model_name)
|
|
93
|
-
|
|
94
|
-
# Determine provider and actual model name
|
|
95
|
-
actual_model, provider = extract_model_and_provider(model_name)
|
|
96
|
-
|
|
97
|
-
# Validate LM Studio models
|
|
98
|
-
if model_name.start_with?('lms/')
|
|
99
|
-
lms_api_base = ENV.fetch('LMS_API_BASE', 'http://localhost:1234/v1')
|
|
100
|
-
validate_lms_model!(actual_model, lms_api_base)
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
# Create chat using isolated context
|
|
104
|
-
chat = if provider
|
|
105
|
-
context.chat(model: actual_model, provider: provider, assume_model_exists: true)
|
|
106
|
-
else
|
|
107
|
-
context.chat(model: actual_model)
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
valid_chats[internal_id] = chat
|
|
111
|
-
valid_contexts[internal_id] = context
|
|
112
|
-
valid_specs << spec
|
|
113
|
-
rescue StandardError => e
|
|
114
|
-
failed_models << "#{internal_id}: #{e.message}"
|
|
115
|
-
end
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
# Report failed models but continue with valid ones
|
|
119
|
-
unless failed_models.empty?
|
|
120
|
-
warn "\nFailed to initialize the following models:"
|
|
121
|
-
failed_models.each { |failure| warn " - #{failure}" }
|
|
122
|
-
logger = LoggerManager.aia_logger
|
|
123
|
-
failed_models.each { |failure| logger.error("Model initialization failed: #{failure}") }
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
# If no models initialized successfully, exit
|
|
127
|
-
if valid_chats.empty?
|
|
128
|
-
warn "\nNo valid models could be initialized. Exiting."
|
|
129
|
-
warn "\nAvailable models can be listed with: bin/aia --help models"
|
|
130
|
-
exit 1
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
@chats = valid_chats
|
|
134
|
-
@contexts = valid_contexts
|
|
135
|
-
@model_specs = valid_specs
|
|
136
|
-
@models = valid_chats.keys
|
|
137
|
-
|
|
138
|
-
# Report successful models
|
|
139
|
-
if failed_models.any?
|
|
140
|
-
warn "\nSuccessfully initialized: #{@models.join(', ')}"
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
# Use the first chat to determine tool support (assuming all models have similar tool support)
|
|
144
|
-
first_chat = @chats.values.first
|
|
145
|
-
return unless first_chat&.model&.supports_functions?
|
|
146
|
-
|
|
147
|
-
load_tools_lazy_mcp_support_only_when_needed
|
|
148
|
-
|
|
149
|
-
@chats.each_value do |chat|
|
|
150
|
-
chat.with_tools(*tools) unless tools.empty?
|
|
151
|
-
end
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
# Create an isolated RubyLLM::Context for a model to prevent cross-talk (ADR-002)
|
|
155
|
-
# Each model gets its own context with provider-specific configuration
|
|
156
|
-
def create_isolated_context_for_model(model_name)
|
|
157
|
-
config = RubyLLM.config.dup
|
|
158
|
-
|
|
159
|
-
# Apply provider-specific configuration
|
|
160
|
-
if model_name.start_with?('lms/')
|
|
161
|
-
config.openai_api_base = ENV.fetch('LMS_API_BASE', 'http://localhost:1234/v1')
|
|
162
|
-
config.openai_api_key = 'dummy' # Local servers don't need a real API key
|
|
163
|
-
elsif model_name.start_with?('osaurus/')
|
|
164
|
-
config.openai_api_base = ENV.fetch('OSAURUS_API_BASE', 'http://localhost:11434/v1')
|
|
165
|
-
config.openai_api_key = 'dummy' # Local servers don't need a real API key
|
|
166
|
-
end
|
|
167
|
-
|
|
168
|
-
RubyLLM::Context.new(config)
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
# Extract the actual model name and provider from the prefixed model_name
|
|
172
|
-
# Returns: [actual_model, provider] where provider may be nil for auto-detection
|
|
173
|
-
def extract_model_and_provider(model_name)
|
|
174
|
-
if model_name.start_with?('ollama/')
|
|
175
|
-
[model_name.sub('ollama/', ''), 'ollama']
|
|
176
|
-
elsif model_name.start_with?('lms/') || model_name.start_with?('osaurus/')
|
|
177
|
-
[model_name.sub(%r{^(lms|osaurus)/}, ''), 'openai']
|
|
178
|
-
else
|
|
179
|
-
[model_name, nil] # Let RubyLLM auto-detect provider
|
|
180
|
-
end
|
|
181
|
-
end
|
|
182
|
-
|
|
183
|
-
def validate_lms_model!(model_name, api_base)
|
|
184
|
-
require 'net/http'
|
|
185
|
-
require 'json'
|
|
186
|
-
|
|
187
|
-
# Build the /v1/models endpoint URL
|
|
188
|
-
uri = URI("#{api_base.gsub(%r{/v1/?$}, '')}/v1/models")
|
|
189
|
-
|
|
190
|
-
begin
|
|
191
|
-
response = Net::HTTP.get_response(uri)
|
|
192
|
-
|
|
193
|
-
unless response.is_a?(Net::HTTPSuccess)
|
|
194
|
-
raise "Cannot connect to LM Studio at #{api_base}. Is LM Studio running?"
|
|
195
|
-
end
|
|
196
|
-
|
|
197
|
-
data = JSON.parse(response.body)
|
|
198
|
-
available_models = data['data']&.map { |m| m['id'] } || []
|
|
199
|
-
|
|
200
|
-
unless available_models.include?(model_name)
|
|
201
|
-
error_msg = "'#{model_name}' is not a valid LM Studio model.\n\n"
|
|
202
|
-
if available_models.empty?
|
|
203
|
-
error_msg += "No models are currently loaded in LM Studio.\n"
|
|
204
|
-
error_msg += "Please load a model in LM Studio first."
|
|
205
|
-
else
|
|
206
|
-
error_msg += "Available LM Studio models:\n"
|
|
207
|
-
available_models.each { |m| error_msg += " - lms/#{m}\n" }
|
|
208
|
-
end
|
|
209
|
-
raise error_msg
|
|
210
|
-
end
|
|
211
|
-
rescue JSON::ParserError => e
|
|
212
|
-
raise "Invalid response from LM Studio at #{api_base}: #{e.message}"
|
|
213
|
-
rescue StandardError => e
|
|
214
|
-
# Re-raise our custom error messages, wrap others
|
|
215
|
-
raise if e.message.include?("not a valid LM Studio") || e.message.include?("Cannot connect")
|
|
216
|
-
raise "Error connecting to LM Studio: #{e.message}"
|
|
217
|
-
end
|
|
218
|
-
end
|
|
219
|
-
|
|
220
|
-
private
|
|
221
|
-
|
|
222
|
-
def load_tools_lazy_mcp_support_only_when_needed
|
|
223
|
-
mcp_connector = McpConnector.new
|
|
224
|
-
tool_loader = ToolLoader.new(mcp_connector)
|
|
225
|
-
|
|
226
|
-
@tools = tool_loader.load_tools
|
|
227
|
-
ToolFilter.detect_conflicts
|
|
228
|
-
@tools = ToolFilter.filter_allowed(@tools)
|
|
229
|
-
@tools = ToolFilter.filter_rejected(@tools)
|
|
230
|
-
@tools = ToolFilter.drop_duplicates(@tools)
|
|
231
|
-
|
|
232
|
-
if tools.empty?
|
|
233
|
-
AIA.config.tool_names = ''
|
|
234
|
-
AIA.config.loaded_tools = []
|
|
235
|
-
else
|
|
236
|
-
AIA.config.tool_names = @tools.map(&:name).join(', ')
|
|
237
|
-
AIA.config.loaded_tools = @tools
|
|
238
|
-
end
|
|
239
|
-
end
|
|
240
|
-
end
|
|
241
|
-
end
|
|
242
|
-
end
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
# lib/aia/adapter/error_handler.rb
|
|
2
|
-
# frozen_string_literal: true
|
|
3
|
-
|
|
4
|
-
module AIA
|
|
5
|
-
module Adapter
|
|
6
|
-
module ErrorHandler
|
|
7
|
-
# Handles tool execution crashes gracefully
|
|
8
|
-
# Logs error with short traceback, repairs conversation, and returns error message
|
|
9
|
-
def handle_tool_crash(chat_instance, exception)
|
|
10
|
-
error_msg = "Tool error: #{exception.class} - #{exception.message}"
|
|
11
|
-
|
|
12
|
-
# Log error with short traceback (first 5 lines)
|
|
13
|
-
warn "\n #{error_msg}"
|
|
14
|
-
if exception.backtrace
|
|
15
|
-
short_trace = exception.backtrace.first(5).map { |line| " #{line}" }.join("\n")
|
|
16
|
-
warn short_trace
|
|
17
|
-
end
|
|
18
|
-
warn "" # blank line for readability
|
|
19
|
-
|
|
20
|
-
# Repair incomplete tool calls to maintain conversation integrity
|
|
21
|
-
repair_incomplete_tool_calls(chat_instance, error_msg)
|
|
22
|
-
|
|
23
|
-
# Return error message so conversation can continue
|
|
24
|
-
error_msg
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
# Repairs conversation history when a tool call fails (timeout, error, etc.)
|
|
28
|
-
# When an MCP tool times out, the conversation gets into an invalid state:
|
|
29
|
-
# - Assistant message with tool_calls was added to history
|
|
30
|
-
# - But no tool result message was added (because the tool failed)
|
|
31
|
-
# - The API requires tool results for each tool_call_id
|
|
32
|
-
# This method adds synthetic error tool results to fix the conversation.
|
|
33
|
-
def repair_incomplete_tool_calls(chat_instance, error_message)
|
|
34
|
-
return unless chat_instance.respond_to?(:messages)
|
|
35
|
-
|
|
36
|
-
messages = chat_instance.messages
|
|
37
|
-
return if messages.empty?
|
|
38
|
-
|
|
39
|
-
# Find the last assistant message that has tool_calls
|
|
40
|
-
last_assistant_with_tools = messages.reverse.find do |msg|
|
|
41
|
-
msg.role == :assistant && msg.respond_to?(:tool_calls) && msg.tool_calls&.any?
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
return unless last_assistant_with_tools
|
|
45
|
-
|
|
46
|
-
# Get the tool_call_ids that need results
|
|
47
|
-
tool_call_ids = last_assistant_with_tools.tool_calls.keys
|
|
48
|
-
|
|
49
|
-
# Check which tool_call_ids already have results
|
|
50
|
-
existing_tool_results = messages.select { |m| m.role == :tool }.map(&:tool_call_id).compact
|
|
51
|
-
|
|
52
|
-
# Add synthetic error results for any missing tool_call_ids
|
|
53
|
-
tool_call_ids.each do |tool_call_id|
|
|
54
|
-
next if existing_tool_results.include?(tool_call_id.to_s) || existing_tool_results.include?(tool_call_id)
|
|
55
|
-
|
|
56
|
-
# Add a synthetic tool result with the error message
|
|
57
|
-
chat_instance.add_message(
|
|
58
|
-
role: :tool,
|
|
59
|
-
content: "Error: #{error_message}",
|
|
60
|
-
tool_call_id: tool_call_id
|
|
61
|
-
)
|
|
62
|
-
end
|
|
63
|
-
rescue StandardError
|
|
64
|
-
# Don't let repair failures cascade
|
|
65
|
-
end
|
|
66
|
-
end
|
|
67
|
-
end
|
|
68
|
-
end
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
# lib/aia/adapter/gem_activator.rb
|
|
2
|
-
# frozen_string_literal: true
|
|
3
|
-
|
|
4
|
-
module AIA
|
|
5
|
-
module Adapter
|
|
6
|
-
class GemActivator
|
|
7
|
-
# Activate a gem and add its lib path to $LOAD_PATH
|
|
8
|
-
# This bypasses Bundler's restrictions on loading non-bundled gems
|
|
9
|
-
def self.activate_gem_for_require(lib)
|
|
10
|
-
return if Gem.try_activate(lib)
|
|
11
|
-
|
|
12
|
-
gem_path = find_gem_path(lib)
|
|
13
|
-
if gem_path
|
|
14
|
-
lib_path = File.join(gem_path, 'lib')
|
|
15
|
-
$LOAD_PATH.unshift(lib_path) unless $LOAD_PATH.include?(lib_path)
|
|
16
|
-
end
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
# Find gem path by searching gem directories directly
|
|
20
|
-
# This bypasses Bundler's restrictions
|
|
21
|
-
def self.find_gem_path(gem_name)
|
|
22
|
-
gem_dirs = Gem.path.flat_map do |base|
|
|
23
|
-
gems_dir = File.join(base, 'gems')
|
|
24
|
-
next [] unless File.directory?(gems_dir)
|
|
25
|
-
|
|
26
|
-
Dir.glob(File.join(gems_dir, "#{gem_name}-*")).select do |path|
|
|
27
|
-
File.directory?(path) && File.basename(path).match?(/^#{Regexp.escape(gem_name)}-[\d.]+/)
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
# Return the most recent version
|
|
32
|
-
gem_dirs.sort.last
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
# Some tool libraries use lazy loading (e.g., Zeitwerk) and need explicit
|
|
36
|
-
# triggering to load tool classes into ObjectSpace
|
|
37
|
-
def self.trigger_tool_loading(lib)
|
|
38
|
-
# Convert lib name to constant (e.g., 'shared_tools' -> SharedTools)
|
|
39
|
-
const_name = lib.split(/[_-]/).map(&:capitalize).join
|
|
40
|
-
|
|
41
|
-
begin
|
|
42
|
-
mod = Object.const_get(const_name)
|
|
43
|
-
|
|
44
|
-
# Try common methods that libraries use to load tools
|
|
45
|
-
if mod.respond_to?(:load_all_tools)
|
|
46
|
-
mod.load_all_tools
|
|
47
|
-
elsif mod.respond_to?(:tools)
|
|
48
|
-
# Calling .tools often triggers lazy loading
|
|
49
|
-
mod.tools
|
|
50
|
-
end
|
|
51
|
-
rescue NameError
|
|
52
|
-
# Constant doesn't exist, library might use different naming
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
end
|
|
@@ -1,274 +0,0 @@
|
|
|
1
|
-
# lib/aia/adapter/mcp_connector.rb
|
|
2
|
-
# frozen_string_literal: true
|
|
3
|
-
|
|
4
|
-
require 'simple_flow'
|
|
5
|
-
|
|
6
|
-
module AIA
|
|
7
|
-
module Adapter
|
|
8
|
-
class McpConnector
|
|
9
|
-
# Default timeout for MCP client initialization (in milliseconds)
|
|
10
|
-
# RubyLLM::MCP expects timeout in milliseconds (e.g., 8000 = 8 seconds)
|
|
11
|
-
# Using a short timeout to prevent slow servers from blocking startup
|
|
12
|
-
MCP_DEFAULT_TIMEOUT = 8_000 # 8 seconds (same as RubyLLM::MCP default)
|
|
13
|
-
|
|
14
|
-
def support_mcp(tools)
|
|
15
|
-
if AIA.config.flags.no_mcp
|
|
16
|
-
logger.debug("MCP processing bypassed via --no-mcp flag")
|
|
17
|
-
return
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
if AIA.config.mcp_servers.nil? || AIA.config.mcp_servers.empty?
|
|
21
|
-
logger.debug("No MCP servers configured, skipping MCP setup")
|
|
22
|
-
return
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
# Initialize tracking (kept for compatibility with Utility.robot)
|
|
26
|
-
AIA.config.connected_mcp_servers = []
|
|
27
|
-
AIA.config.failed_mcp_servers = []
|
|
28
|
-
|
|
29
|
-
servers = filter_mcp_servers(AIA.config.mcp_servers)
|
|
30
|
-
server_names = servers.map { |s| s[:name] || s['name'] }.compact
|
|
31
|
-
|
|
32
|
-
logger.info("Starting parallel MCP connection", server_count: servers.size, servers: server_names)
|
|
33
|
-
$stderr.puts "MCP: Connecting to #{server_names.join(', ')}..."
|
|
34
|
-
$stderr.flush
|
|
35
|
-
|
|
36
|
-
LoggerManager.configure_mcp_logger
|
|
37
|
-
|
|
38
|
-
# Build steps array first (outside the block to preserve self reference)
|
|
39
|
-
# Each step is a [name, callable] pair for parallel execution
|
|
40
|
-
connector = self
|
|
41
|
-
steps = servers.map do |server|
|
|
42
|
-
name = (server[:name] || server['name']).to_sym
|
|
43
|
-
logger.debug("Building connection step", server: name)
|
|
44
|
-
[name, connector.send(:build_mcp_connection_step, server)]
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
# Build parallel pipeline - each server is independent (depends_on: :none)
|
|
48
|
-
# All servers will connect concurrently using fiber-based async
|
|
49
|
-
logger.debug("Creating SimpleFlow pipeline", step_count: steps.size)
|
|
50
|
-
pipeline = SimpleFlow::Pipeline.new(concurrency: :async) do
|
|
51
|
-
steps.each do |name, callable|
|
|
52
|
-
step name, callable, depends_on: :none
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
# Execute all connections in parallel
|
|
57
|
-
start_time = Time.now
|
|
58
|
-
initial_result = SimpleFlow::Result.new({ tools: [] })
|
|
59
|
-
final_result = pipeline.call_parallel(initial_result)
|
|
60
|
-
elapsed = Time.now - start_time
|
|
61
|
-
|
|
62
|
-
logger.info("Parallel MCP connection completed", elapsed_seconds: elapsed.round(2))
|
|
63
|
-
|
|
64
|
-
# Extract results and populate config arrays for compatibility
|
|
65
|
-
extract_mcp_results(final_result, tools)
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def filter_mcp_servers(servers)
|
|
69
|
-
use_list = Array(AIA.config.mcp_use)
|
|
70
|
-
skip_list = Array(AIA.config.mcp_skip)
|
|
71
|
-
|
|
72
|
-
if !use_list.empty?
|
|
73
|
-
servers = servers.select do |server|
|
|
74
|
-
name = server[:name] || server['name']
|
|
75
|
-
use_list.include?(name)
|
|
76
|
-
end
|
|
77
|
-
logger.info("MCP servers filtered by --mcp-use", use: use_list, remaining: servers.size)
|
|
78
|
-
elsif !skip_list.empty?
|
|
79
|
-
servers = servers.reject do |server|
|
|
80
|
-
name = server[:name] || server['name']
|
|
81
|
-
skip_list.include?(name)
|
|
82
|
-
end
|
|
83
|
-
logger.info("MCP servers filtered by --mcp-skip", skip: skip_list, remaining: servers.size)
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
servers
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
private
|
|
90
|
-
|
|
91
|
-
def logger
|
|
92
|
-
@logger ||= LoggerManager.aia_logger
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
def build_mcp_connection_step(server)
|
|
96
|
-
# Capture logger in closure for use within the lambda
|
|
97
|
-
log = logger
|
|
98
|
-
|
|
99
|
-
->(result) {
|
|
100
|
-
name = server[:name] || server['name']
|
|
101
|
-
start_time = Time.now
|
|
102
|
-
|
|
103
|
-
begin
|
|
104
|
-
log.debug("Registering MCP client", server: name)
|
|
105
|
-
|
|
106
|
-
# Register client with RubyLLM::MCP
|
|
107
|
-
client = register_single_mcp_client(server)
|
|
108
|
-
|
|
109
|
-
log.debug("Starting client connection", server: name)
|
|
110
|
-
|
|
111
|
-
# Start and verify connection
|
|
112
|
-
client.start
|
|
113
|
-
caps = client.capabilities
|
|
114
|
-
has_capabilities = caps && (caps.is_a?(Hash) ? !caps.empty? : caps)
|
|
115
|
-
|
|
116
|
-
elapsed = Time.now - start_time
|
|
117
|
-
|
|
118
|
-
if client.alive? && has_capabilities
|
|
119
|
-
# Success - get tools and record in context
|
|
120
|
-
tools = begin
|
|
121
|
-
client.tools
|
|
122
|
-
rescue StandardError => tool_err
|
|
123
|
-
log.warn("Failed to retrieve tools", server: name, error: tool_err.message)
|
|
124
|
-
[]
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
tool_names = tools.map { |t| t.respond_to?(:name) ? t.name : t.to_s }
|
|
128
|
-
log.info("Connected successfully", server: name, elapsed_seconds: elapsed.round(2), tool_count: tools.size)
|
|
129
|
-
log.debug("Available tools", server: name, tools: tool_names)
|
|
130
|
-
|
|
131
|
-
result
|
|
132
|
-
.with_context(name.to_sym, { status: :connected, tools: tools })
|
|
133
|
-
.continue(result.value)
|
|
134
|
-
else
|
|
135
|
-
# Connection issue - determine specific error
|
|
136
|
-
error = determine_mcp_connection_error(client, caps)
|
|
137
|
-
log.warn("Connection failed", server: name, elapsed_seconds: elapsed.round(2), error: error)
|
|
138
|
-
log.debug("Connection details", server: name, alive: client.alive?, capabilities: caps.inspect)
|
|
139
|
-
|
|
140
|
-
result
|
|
141
|
-
.with_error(name.to_sym, error)
|
|
142
|
-
.with_context(name.to_sym, { status: :failed })
|
|
143
|
-
.continue(result.value) # Continue to allow other servers
|
|
144
|
-
end
|
|
145
|
-
rescue StandardError => e
|
|
146
|
-
elapsed = Time.now - start_time
|
|
147
|
-
error_msg = e.message.downcase.include?('timeout') ?
|
|
148
|
-
"Connection timed out" : e.message
|
|
149
|
-
|
|
150
|
-
log.error("Connection exception", server: name, elapsed_seconds: elapsed.round(2), error_class: e.class.name, error: error_msg)
|
|
151
|
-
log.debug("Exception backtrace", server: name, backtrace: e.backtrace&.first(3))
|
|
152
|
-
|
|
153
|
-
result
|
|
154
|
-
.with_error(name.to_sym, error_msg)
|
|
155
|
-
.with_context(name.to_sym, { status: :failed })
|
|
156
|
-
.continue(result.value)
|
|
157
|
-
end
|
|
158
|
-
}
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
def register_single_mcp_client(server)
|
|
162
|
-
name = server[:name] || server['name']
|
|
163
|
-
command = server[:command] || server['command']
|
|
164
|
-
args = server[:args] || server['args'] || []
|
|
165
|
-
env = server[:env] || server['env'] || {}
|
|
166
|
-
|
|
167
|
-
raw_timeout = server[:timeout] || server['timeout'] ||
|
|
168
|
-
server[:request_timeout] || server['request_timeout'] ||
|
|
169
|
-
MCP_DEFAULT_TIMEOUT
|
|
170
|
-
request_timeout = raw_timeout.to_i < 1000 ? (raw_timeout.to_i * 1000) : raw_timeout.to_i
|
|
171
|
-
request_timeout = [request_timeout, 30_000].min
|
|
172
|
-
|
|
173
|
-
logger.debug("Configuring client", server: name, command: command, args: args, timeout_ms: request_timeout)
|
|
174
|
-
logger.debug("Environment variables", server: name, env_keys: env.keys) unless env.empty?
|
|
175
|
-
|
|
176
|
-
mcp_config = { command: command, args: Array(args) }
|
|
177
|
-
mcp_config[:env] = env unless env.empty?
|
|
178
|
-
|
|
179
|
-
begin
|
|
180
|
-
logger.debug("Adding client to RubyLLM::MCP with request_timeout", server: name)
|
|
181
|
-
RubyLLM::MCP.add_client(
|
|
182
|
-
name: name,
|
|
183
|
-
transport_type: :stdio,
|
|
184
|
-
config: mcp_config,
|
|
185
|
-
request_timeout: request_timeout,
|
|
186
|
-
start: false
|
|
187
|
-
)
|
|
188
|
-
rescue ArgumentError => e
|
|
189
|
-
# If request_timeout isn't supported in this version, try without it
|
|
190
|
-
if e.message.include?('timeout')
|
|
191
|
-
logger.debug("Retrying without request_timeout (unsupported in this RubyLLM::MCP version)", server: name)
|
|
192
|
-
RubyLLM::MCP.add_client(
|
|
193
|
-
name: name,
|
|
194
|
-
transport_type: :stdio,
|
|
195
|
-
config: mcp_config,
|
|
196
|
-
start: false
|
|
197
|
-
)
|
|
198
|
-
else
|
|
199
|
-
logger.error("Failed to add client", server: name, error: e.message)
|
|
200
|
-
raise
|
|
201
|
-
end
|
|
202
|
-
end
|
|
203
|
-
|
|
204
|
-
logger.debug("Client registered successfully", server: name)
|
|
205
|
-
RubyLLM::MCP.clients[name]
|
|
206
|
-
end
|
|
207
|
-
|
|
208
|
-
def determine_mcp_connection_error(client, caps)
|
|
209
|
-
if !client.alive?
|
|
210
|
-
"Connection failed"
|
|
211
|
-
elsif caps.nil?
|
|
212
|
-
"Connection timed out (no response)"
|
|
213
|
-
elsif caps.is_a?(Hash) && caps.empty?
|
|
214
|
-
"Connection timed out (empty capabilities)"
|
|
215
|
-
else
|
|
216
|
-
"Connection timed out (no capabilities received)"
|
|
217
|
-
end
|
|
218
|
-
end
|
|
219
|
-
|
|
220
|
-
def extract_mcp_results(result, tools)
|
|
221
|
-
logger.debug("Extracting MCP connection results from SimpleFlow pipeline")
|
|
222
|
-
all_tools = []
|
|
223
|
-
|
|
224
|
-
result.context.each do |server_name, info|
|
|
225
|
-
name = server_name.to_s
|
|
226
|
-
if info[:status] == :connected
|
|
227
|
-
tool_count = (info[:tools] || []).size
|
|
228
|
-
logger.debug("Extracting tools from connected server", server: name, tool_count: tool_count)
|
|
229
|
-
AIA.config.connected_mcp_servers << name
|
|
230
|
-
all_tools.concat(info[:tools] || [])
|
|
231
|
-
end
|
|
232
|
-
end
|
|
233
|
-
|
|
234
|
-
result.errors.each do |server_name, messages|
|
|
235
|
-
logger.debug("Recording failure", server: server_name, error: messages.first)
|
|
236
|
-
AIA.config.failed_mcp_servers << {
|
|
237
|
-
name: server_name.to_s,
|
|
238
|
-
error: messages.first
|
|
239
|
-
}
|
|
240
|
-
end
|
|
241
|
-
|
|
242
|
-
tools.concat(all_tools)
|
|
243
|
-
|
|
244
|
-
logger.info("MCP results",
|
|
245
|
-
connected_count: AIA.config.connected_mcp_servers.size,
|
|
246
|
-
failed_count: AIA.config.failed_mcp_servers.size,
|
|
247
|
-
total_tools: all_tools.size
|
|
248
|
-
)
|
|
249
|
-
|
|
250
|
-
# Report results
|
|
251
|
-
report_mcp_connection_results(all_tools.size)
|
|
252
|
-
end
|
|
253
|
-
|
|
254
|
-
def report_mcp_connection_results(tool_count)
|
|
255
|
-
if AIA.config.connected_mcp_servers.any?
|
|
256
|
-
logger.info("Successfully connected", servers: AIA.config.connected_mcp_servers)
|
|
257
|
-
$stderr.puts "MCP: Connected to #{AIA.config.connected_mcp_servers.join(', ')} (#{tool_count} tools)"
|
|
258
|
-
end
|
|
259
|
-
|
|
260
|
-
AIA.config.failed_mcp_servers.each do |failure|
|
|
261
|
-
logger.warn("Server failed", server: failure[:name], error: failure[:error])
|
|
262
|
-
$stderr.puts " MCP: '#{failure[:name]}' failed - #{failure[:error]}"
|
|
263
|
-
end
|
|
264
|
-
|
|
265
|
-
if AIA.config.connected_mcp_servers.empty? && AIA.config.failed_mcp_servers.any?
|
|
266
|
-
logger.error("No MCP servers connected successfully")
|
|
267
|
-
$stderr.puts "MCP: No servers connected successfully"
|
|
268
|
-
end
|
|
269
|
-
|
|
270
|
-
$stderr.flush
|
|
271
|
-
end
|
|
272
|
-
end
|
|
273
|
-
end
|
|
274
|
-
end
|