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.
Files changed (170) hide show
  1. checksums.yaml +4 -4
  2. data/.envrc +5 -1
  3. data/.loki +231 -0
  4. data/.quality/flay_baseline.txt +1 -0
  5. data/.quality/flog_baseline.txt +29 -0
  6. data/.quality/reek_baseline.txt +80 -0
  7. data/.rubocop.yml +116 -0
  8. data/.version +1 -1
  9. data/CHANGELOG.md +266 -42
  10. data/IMPLEMENTATION_PLAN.md +506 -0
  11. data/README.md +266 -238
  12. data/Rakefile +118 -5
  13. data/architecture_review.md +314 -0
  14. data/bin/aia +16 -0
  15. data/docs/AGENTS.md +40 -0
  16. data/docs/advanced-prompting.md +67 -3
  17. data/docs/cli-reference.md +312 -56
  18. data/docs/configuration.md +130 -19
  19. data/docs/contributing.md +56 -2
  20. data/docs/directives-reference.md +593 -78
  21. data/docs/faq.md +85 -3
  22. data/docs/guides/available-models.md +1 -1
  23. data/docs/guides/basic-usage.md +6 -6
  24. data/docs/guides/chat.md +40 -16
  25. data/docs/guides/crew.md +239 -0
  26. data/docs/guides/executable-prompts.md +1 -1
  27. data/docs/guides/index.md +1 -0
  28. data/docs/guides/models.md +15 -0
  29. data/docs/index.md +29 -2
  30. data/docs/installation.md +44 -17
  31. data/docs/mcp-integration.md +40 -0
  32. data/docs/prompt_management.md +85 -86
  33. data/docs/security.md +47 -0
  34. data/docs/special_projects_guide.md +386 -0
  35. data/docs/tools-and-mcp-examples.md +23 -0
  36. data/docs/workflows-and-pipelines.md +84 -7
  37. data/examples/.gitignore +1 -0
  38. data/examples/00_setup_aia.sh +27 -44
  39. data/examples/11_multi_model.sh +4 -14
  40. data/examples/12_token_usage.sh +3 -12
  41. data/examples/18_tools.sh +10 -2
  42. data/examples/22_chat_mode.sh +0 -10
  43. data/examples/23_verify.sh +139 -0
  44. data/examples/24_decompose.sh +139 -0
  45. data/examples/25_spawn.sh +139 -0
  46. data/examples/26_debate.sh +97 -0
  47. data/examples/27_mention_routing.sh +157 -0
  48. data/examples/28_model_switching.sh +106 -0
  49. data/examples/29_agent_harness.sh +177 -0
  50. data/examples/README.md +65 -0
  51. data/examples/advanced_multi_robot_capabilities_without_examples.md +106 -0
  52. data/examples/aia_config.yml +1 -1
  53. data/examples/aia_config_orchestrator.yml +45 -0
  54. data/examples/common.sh +19 -0
  55. data/examples/context/tech_stack.md +2 -2
  56. data/examples/prompts_dir/project_summary +2 -2
  57. data/examples/prompts_dir/roles/orchestrator.md +21 -0
  58. data/examples/requirements/sinatra_taskflow_app.md +139 -0
  59. data/examples/rules/01_classify_ruby.rb +16 -0
  60. data/examples/rules/02_prefer_claude_for_code.rb +19 -0
  61. data/examples/rules/03_gate_prompt_length.rb +19 -0
  62. data/examples/rules/04_tool_selection.rb +41 -0
  63. data/examples/rules/README.md +30 -0
  64. data/examples/run_all.sh +48 -15
  65. data/examples/tools/word_count_tool.rb +1 -1
  66. data/lib/AGENTS.md +57 -0
  67. data/lib/aia/chat_loop.rb +306 -159
  68. data/lib/aia/config/cli_parser.rb +174 -111
  69. data/lib/aia/config/defaults.yml +62 -33
  70. data/lib/aia/config/mcp_parser.rb +39 -46
  71. data/lib/aia/config/model_spec.rb +34 -2
  72. data/lib/aia/config/validator.rb +121 -138
  73. data/lib/aia/config.rb +110 -145
  74. data/lib/aia/content_extractor.rb +153 -0
  75. data/lib/aia/cost_calculator.rb +38 -0
  76. data/lib/aia/crew.rb +164 -0
  77. data/lib/aia/debate_handler.rb +166 -0
  78. data/lib/aia/delegate_handler.rb +112 -0
  79. data/lib/aia/directive.rb +33 -18
  80. data/lib/aia/directive_processor.rb +16 -7
  81. data/lib/aia/directives/configuration_directives.rb +160 -20
  82. data/lib/aia/directives/context_directives.rb +38 -26
  83. data/lib/aia/directives/execution_directives.rb +136 -4
  84. data/lib/aia/directives/model_directives.rb +76 -34
  85. data/lib/aia/directives/trakflow_directives.rb +44 -0
  86. data/lib/aia/directives/utility_directives.rb +203 -6
  87. data/lib/aia/directives/web_and_file_directives.rb +96 -60
  88. data/lib/aia/errors.rb +15 -0
  89. data/lib/aia/fact_asserter.rb +27 -0
  90. data/lib/aia/fzf.rb +9 -31
  91. data/lib/aia/handler_context.rb +17 -0
  92. data/lib/aia/handler_protocol.rb +19 -0
  93. data/lib/aia/history_transfer.rb +55 -0
  94. data/lib/aia/input_collector.rb +3 -3
  95. data/lib/aia/layered_orchestrator.rb +448 -0
  96. data/lib/aia/logger.rb +24 -4
  97. data/lib/aia/mcp_config_normalizer.rb +35 -0
  98. data/lib/aia/mcp_connection_manager.rb +305 -0
  99. data/lib/aia/mcp_discovery.rb +44 -0
  100. data/lib/aia/mcp_grouper.rb +33 -0
  101. data/lib/aia/mcp_utility.rb +57 -0
  102. data/lib/aia/mention_router.rb +260 -0
  103. data/lib/aia/model_alias_registry.rb +97 -0
  104. data/lib/aia/model_switch_handler.rb +100 -0
  105. data/lib/aia/network_builder.rb +155 -0
  106. data/lib/aia/network_memory_manager.rb +55 -0
  107. data/lib/aia/patches/ruby_llm_streaming_error.rb +43 -0
  108. data/lib/aia/patches/ruby_llm_tool_error.rb +96 -0
  109. data/lib/aia/pipeline_orchestrator.rb +262 -0
  110. data/lib/aia/plugin_loader.rb +170 -0
  111. data/lib/aia/plugin_monitor.rb +208 -0
  112. data/lib/aia/prompt_decomposer.rb +157 -0
  113. data/lib/aia/prompt_handler.rb +19 -39
  114. data/lib/aia/robot_builder.rb +51 -0
  115. data/lib/aia/robot_factory.rb +334 -0
  116. data/lib/aia/robot_namer.rb +116 -0
  117. data/lib/aia/session.rb +83 -17
  118. data/lib/aia/session_tracker.rb +209 -0
  119. data/lib/aia/similarity_scorer.rb +39 -0
  120. data/lib/aia/skill_utils.rb +105 -1
  121. data/lib/aia/spawn_handler.rb +129 -0
  122. data/lib/aia/spawn_spec_parser.rb +65 -0
  123. data/lib/aia/special_mode_handler.rb +302 -0
  124. data/lib/aia/startup_coordinator.rb +150 -0
  125. data/lib/aia/streaming_runner.rb +169 -0
  126. data/lib/aia/system_prompt_assembler.rb +88 -0
  127. data/lib/aia/task_coordinator.rb +202 -0
  128. data/lib/aia/task_decomposer.rb +57 -0
  129. data/lib/aia/task_executor.rb +51 -0
  130. data/lib/aia/tfidf_math.rb +27 -0
  131. data/lib/aia/tool_filter/tfidf.rb +116 -0
  132. data/lib/aia/tool_filter/wordnet_expander.rb +127 -0
  133. data/lib/aia/tool_filter.rb +82 -0
  134. data/lib/aia/tool_filter_registry.rb +30 -0
  135. data/lib/aia/tool_filter_strategy.rb +143 -0
  136. data/lib/aia/tool_loader.rb +210 -0
  137. data/lib/aia/tool_utility.rb +30 -0
  138. data/lib/aia/tools/delegate_to_foreman_tool.rb +70 -0
  139. data/lib/aia/tools/recruit_robot_tool.rb +60 -0
  140. data/lib/aia/tools/reskill_robot_tool.rb +44 -0
  141. data/lib/aia/tools/task_board_tool.rb +114 -0
  142. data/lib/aia/trakflow_bridge.rb +173 -0
  143. data/lib/aia/turn_state.rb +94 -0
  144. data/lib/aia/ui_presenter.rb +166 -198
  145. data/lib/aia/utility.rb +134 -87
  146. data/lib/aia/{history_manager.rb → variable_input_collector.rb} +8 -9
  147. data/lib/aia/verification_network.rb +58 -0
  148. data/lib/aia.rb +108 -63
  149. data/mkdocs.yml +1 -0
  150. metadata +179 -56
  151. data/justfile +0 -215
  152. data/lib/aia/adapter/chat_execution.rb +0 -242
  153. data/lib/aia/adapter/error_handler.rb +0 -68
  154. data/lib/aia/adapter/gem_activator.rb +0 -57
  155. data/lib/aia/adapter/mcp_connector.rb +0 -274
  156. data/lib/aia/adapter/modality_handlers.rb +0 -167
  157. data/lib/aia/adapter/model_registry.rb +0 -81
  158. data/lib/aia/adapter/multi_model_chat.rb +0 -218
  159. data/lib/aia/adapter/provider_configurator.rb +0 -59
  160. data/lib/aia/adapter/tool_filter.rb +0 -85
  161. data/lib/aia/adapter/tool_loader.rb +0 -90
  162. data/lib/aia/chat_processor_service.rb +0 -164
  163. data/lib/aia/prompt_pipeline.rb +0 -183
  164. data/lib/aia/ruby_llm_adapter.rb +0 -95
  165. data/lib/extensions/openstruct_merge.rb +0 -48
  166. data/lib/extensions/ruby_llm/.irbrc +0 -56
  167. data/lib/extensions/ruby_llm/modalities.rb +0 -36
  168. data/lib/extensions/ruby_llm/provider_fix.rb +0 -79
  169. data/lib/refinements/string.rb +0 -16
  170. data/main.just +0 -76
@@ -1,167 +0,0 @@
1
- # lib/aia/adapter/modality_handlers.rb
2
- # frozen_string_literal: true
3
-
4
- require 'tempfile'
5
-
6
- module AIA
7
- module Adapter
8
- module ModalityHandlers
9
- def transcribe(audio_file)
10
- # Use the first model for transcription
11
- first_model = @models.first
12
- @chats[first_model].ask('Transcribe this audio', with: audio_file).content
13
- end
14
-
15
- def speak(_text)
16
- # NOTE: RubyLLM doesn't have a direct text-to-speech feature
17
- # This is a placeholder for a custom implementation or external service
18
- tmpfile = Tempfile.new(['aia-tts-', '.mp3'])
19
- begin
20
- tmpfile.write('Mock TTS audio content')
21
- tmpfile.close
22
- speak_cmd = AIA.config.audio.speak_command
23
- if system('which', speak_cmd, out: File::NULL, err: File::NULL)
24
- system(speak_cmd, tmpfile.path)
25
- end
26
- "Audio generated successfully"
27
- rescue StandardError => e
28
- "Error generating audio: #{e.message}"
29
- ensure
30
- tmpfile.unlink
31
- end
32
- end
33
-
34
- private
35
-
36
- def extract_text_prompt(prompt)
37
- if prompt.is_a?(String)
38
- prompt
39
- elsif prompt.is_a?(Hash) && prompt[:text]
40
- prompt[:text]
41
- elsif prompt.is_a?(Hash) && prompt[:content]
42
- prompt[:content]
43
- else
44
- prompt.to_s
45
- end
46
- end
47
-
48
- #########################################
49
- ## text
50
-
51
- def text_to_text_single(prompt, model_name)
52
- chat_instance = @chats[model_name]
53
- text_prompt = extract_text_prompt(prompt)
54
-
55
- response = if AIA.config.context_files.empty?
56
- chat_instance.ask(text_prompt)
57
- else
58
- chat_instance.ask(text_prompt, with: AIA.config.context_files)
59
- end
60
-
61
- # Return the full response object to preserve token information
62
- response
63
- rescue StandardError => e
64
- # Tool crashes should not crash AIA - log and continue gracefully
65
- handle_tool_crash(chat_instance, e)
66
- end
67
-
68
- #########################################
69
- ## Image
70
-
71
- def extract_image_path(prompt)
72
- if prompt.is_a?(String)
73
- match = prompt.match(%r{\b[\w/.\-_]+?\.(jpg|jpeg|png|gif|webp)\b}i)
74
- match ? match[0] : nil
75
- elsif prompt.is_a?(Hash)
76
- prompt[:image] || prompt[:image_path]
77
- end
78
- end
79
-
80
- def text_to_image_single(prompt, model_name)
81
- text_prompt = extract_text_prompt(prompt)
82
- image_name = extract_image_path(text_prompt)
83
-
84
- begin
85
- image = RubyLLM.paint(text_prompt, size: AIA.config.image.size)
86
- if image_name
87
- image_path = image.save(image_name)
88
- "Image generated and saved to: #{image_path}"
89
- else
90
- "Image generated and available at: #{image.url}"
91
- end
92
- rescue StandardError => e
93
- "Error generating image: #{e.message}"
94
- end
95
- end
96
-
97
- def image_to_text_single(prompt, model_name)
98
- image_path = extract_image_path(prompt)
99
- text_prompt = extract_text_prompt(prompt)
100
-
101
- if image_path && File.exist?(image_path)
102
- begin
103
- @chats[model_name].ask(text_prompt, with: image_path).content
104
- rescue StandardError => e
105
- "Error analyzing image: #{e.message}"
106
- end
107
- else
108
- text_to_text_single(prompt, model_name)
109
- end
110
- end
111
-
112
- #########################################
113
- ## audio
114
-
115
- def audio_file?(filepath)
116
- filepath.to_s.downcase.end_with?('.mp3', '.wav', '.m4a', '.flac')
117
- end
118
-
119
- def text_to_audio_single(prompt, model_name)
120
- text_prompt = extract_text_prompt(prompt)
121
-
122
- # NOTE: RubyLLM doesn't have a direct TTS feature
123
- # TODO: This is a placeholder for a custom implementation
124
- tmpfile = Tempfile.new(['aia-tts-', '.mp3'])
125
- begin
126
- tmpfile.write(text_prompt)
127
- tmpfile.close
128
- speak_cmd = AIA.config.audio.speak_command
129
- if system('which', speak_cmd, out: File::NULL, err: File::NULL)
130
- system(speak_cmd, tmpfile.path)
131
- end
132
- "Audio generated successfully"
133
- rescue StandardError => e
134
- "Error generating audio: #{e.message}"
135
- ensure
136
- tmpfile.unlink
137
- end
138
- end
139
-
140
- def audio_to_text_single(prompt, model_name)
141
- text_prompt = extract_text_prompt(prompt)
142
- text_prompt = 'Transcribe this audio' if text_prompt.nil? || text_prompt.empty?
143
-
144
- # TODO: I don't think that "prompt" would ever be an audio filepath.
145
- # Check prompt to see if it is a PromptManager object that has context_files
146
-
147
- if prompt.is_a?(String) &&
148
- File.exist?(prompt) &&
149
- audio_file?(prompt)
150
- begin
151
- response = if AIA.config.context_files.empty?
152
- @chats[model_name].ask(text_prompt)
153
- else
154
- @chats[model_name].ask(text_prompt, with: AIA.config.context_files)
155
- end
156
- response.content
157
- rescue StandardError => e
158
- "Error transcribing audio: #{e.message}"
159
- end
160
- else
161
- # Fall back to regular chat if no valid audio file is found
162
- text_to_text_single(prompt, model_name)
163
- end
164
- end
165
- end
166
- end
167
- end
@@ -1,81 +0,0 @@
1
- # lib/aia/adapter/model_registry.rb
2
- # frozen_string_literal: true
3
-
4
- require 'fileutils'
5
- require 'json'
6
-
7
- module AIA
8
- module Adapter
9
- class ModelRegistry
10
- def refresh
11
- return if models_json_path.nil? # Skip if no aia_dir configured
12
-
13
- # On first use, copy the bundled models.json from RubyLLM (no API calls)
14
- copy_bundled_models_to_local unless File.exist?(models_json_path)
15
-
16
- # Point RubyLLM at our local registry file for all model lookups
17
- RubyLLM.config.model_registry_file = models_json_path
18
-
19
- # Coerce refresh_days to integer (env vars come as strings)
20
- refresh_days = AIA.config.registry.refresh
21
- refresh_days = refresh_days.to_i if refresh_days.respond_to?(:to_i)
22
- refresh_days ||= 7 # Default to 7 days if nil
23
-
24
- # If refresh is disabled (0), just use the local file as-is
25
- return if refresh_days.zero?
26
-
27
- # Only refresh from provider APIs when enough days have elapsed
28
- last_refresh = models_last_refresh
29
- return unless last_refresh.nil? || Date.today > (last_refresh + refresh_days)
30
-
31
- # Refresh models from provider APIs and models.dev
32
- RubyLLM.models.refresh!
33
-
34
- # Save refreshed models to our local JSON file
35
- save_models_to_json
36
- end
37
-
38
- def models_json_path
39
- aia_dir = AIA.config.paths&.aia_dir
40
- return nil if aia_dir.nil?
41
-
42
- File.join(File.expand_path(aia_dir), 'models.json')
43
- end
44
-
45
- # Returns the last refresh date based on models.json modification time
46
- def models_last_refresh
47
- path = models_json_path
48
- return nil if path.nil? || !File.exist?(path)
49
-
50
- File.mtime(path).to_date
51
- end
52
-
53
- def copy_bundled_models_to_local
54
- aia_dir = File.expand_path(AIA.config.paths.aia_dir)
55
- FileUtils.mkdir_p(aia_dir)
56
-
57
- # RubyLLM.config.model_registry_file points to the gem's bundled models.json
58
- # before we redirect it to our local copy
59
- bundled_path = RubyLLM.config.model_registry_file
60
-
61
- if bundled_path && File.exist?(bundled_path)
62
- FileUtils.cp(bundled_path, models_json_path)
63
- else
64
- # Fallback: save whatever RubyLLM has loaded from its bundled data
65
- save_models_to_json
66
- end
67
- end
68
-
69
- def save_models_to_json
70
- return if models_json_path.nil?
71
-
72
- aia_dir = File.expand_path(AIA.config.paths.aia_dir)
73
- FileUtils.mkdir_p(aia_dir)
74
-
75
- models_data = RubyLLM.models.all.map(&:to_h)
76
-
77
- File.write(models_json_path, JSON.pretty_generate(models_data))
78
- end
79
- end
80
- end
81
- end
@@ -1,218 +0,0 @@
1
- # lib/aia/adapter/multi_model_chat.rb
2
- # frozen_string_literal: true
3
-
4
- require 'async'
5
-
6
- module AIA
7
- module Adapter
8
- module MultiModelChat
9
- # Helper class to carry multi-model response with metrics
10
- class MultiModelResponse
11
- attr_reader :content, :metrics_list
12
-
13
- def initialize(content, metrics_list)
14
- @content = content
15
- @metrics_list = metrics_list
16
- end
17
-
18
- def multi_model?
19
- true
20
- end
21
- end
22
-
23
- # Prepend role content to prompt for a specific model (ADR-005)
24
- def prepend_model_role(prompt, internal_id)
25
- # Get model spec to find role
26
- spec = get_model_spec(internal_id)
27
- return prompt unless spec && spec[:role]
28
-
29
- # Get role content using PromptHandler
30
- prompt_handler = AIA::PromptHandler.new
31
- role_content = prompt_handler.load_role_for_model(spec, AIA.config.prompts.role)
32
-
33
- return prompt unless role_content
34
-
35
- # Prepend role to prompt based on prompt type
36
- if prompt.is_a?(String)
37
- "#{role_content}\n\n#{prompt}"
38
- elsif prompt.is_a?(Array)
39
- prepend_role_to_conversation(prompt, role_content)
40
- else
41
- prompt
42
- end
43
- end
44
-
45
- def prepend_role_to_conversation(conversation, role_content)
46
- # Find the first user message and prepend role
47
- modified = conversation.dup
48
- first_user_index = modified.find_index { |msg| msg[:role] == "user" || msg["role"] == "user" }
49
-
50
- if first_user_index
51
- msg = modified[first_user_index].dup
52
- content_key = msg.key?(:content) ? :content : "content"
53
-
54
- msg[content_key] = "#{role_content}\n\n#{msg[content_key]}"
55
- modified[first_user_index] = msg
56
- end
57
-
58
- modified
59
- end
60
-
61
- def multi_model_chat(prompt_or_contexts)
62
- results = {}
63
-
64
- # Check if we're receiving per-model contexts (Hash) or shared prompt (String/Array) - ADR-002 revised
65
- per_model_contexts = prompt_or_contexts.is_a?(Hash) &&
66
- prompt_or_contexts.keys.all? { |k| @models.include?(k) }
67
-
68
- Async do |task|
69
- @models.each do |internal_id|
70
- task.async do
71
- begin
72
- # Use model-specific context if available, otherwise shared prompt
73
- prompt = if per_model_contexts
74
- prompt_or_contexts[internal_id]
75
- else
76
- prompt_or_contexts
77
- end
78
-
79
- # Add per-model role if specified (ADR-005)
80
- prompt = prepend_model_role(prompt, internal_id)
81
-
82
- result = single_model_chat(prompt, internal_id)
83
- results[internal_id] = result
84
- rescue StandardError => e
85
- results[internal_id] = "Error with #{internal_id}: #{e.message}"
86
- end
87
- end
88
- end
89
- end
90
-
91
- # Format and return results from all models
92
- format_multi_model_results(results)
93
- end
94
-
95
- def format_multi_model_results(results)
96
- if should_use_consensus_mode?
97
- generate_consensus_response(results)
98
- else
99
- format_individual_responses(results)
100
- end
101
- end
102
-
103
- def should_use_consensus_mode?
104
- AIA.config.flags.consensus == true
105
- end
106
-
107
- def generate_consensus_response(results)
108
- primary_model = @models.first
109
- primary_chat = @chats[primary_model]
110
-
111
- consensus_prompt = build_consensus_prompt(results)
112
-
113
- begin
114
- consensus_result = primary_chat.ask(consensus_prompt).content
115
- "from: #{primary_model}\n#{consensus_result}"
116
- rescue StandardError => e
117
- "Error generating consensus: #{e.message}\n\n" + format_individual_responses(results)
118
- end
119
- end
120
-
121
- def build_consensus_prompt(results)
122
- prompt_parts = []
123
- prompt_parts << "You are tasked with creating a consensus response based on multiple AI model responses to the same query."
124
- prompt_parts << "Please analyze the following responses and provide a unified, comprehensive answer that:"
125
- prompt_parts << "- Incorporates the best insights from all models"
126
- prompt_parts << "- Resolves any contradictions with clear reasoning"
127
- prompt_parts << "- Provides additional context or clarification when helpful"
128
- prompt_parts << "- Maintains accuracy and avoids speculation"
129
- prompt_parts << ""
130
- prompt_parts << "Model responses:"
131
- prompt_parts << ""
132
-
133
- results.each do |model_name, result|
134
- content = if result.respond_to?(:content)
135
- result.content
136
- else
137
- result.to_s
138
- end
139
- next if content.start_with?("Error with")
140
- prompt_parts << "#{model_name}:"
141
- prompt_parts << content
142
- prompt_parts << ""
143
- end
144
-
145
- prompt_parts << "Please provide your consensus response:"
146
- prompt_parts.join("\n")
147
- end
148
-
149
- def format_individual_responses(results)
150
- has_metrics = results.values.any? { |r| r.respond_to?(:input_tokens) && r.respond_to?(:output_tokens) }
151
-
152
- if has_metrics
153
- format_multi_model_with_metrics(results)
154
- else
155
- output = []
156
- results.each do |internal_id, result|
157
- spec = get_model_spec(internal_id)
158
- display_name = format_model_display_name(spec)
159
-
160
- output << "from: #{display_name}"
161
- content = if result.respond_to?(:content)
162
- result.content
163
- else
164
- result.to_s
165
- end
166
- output << content
167
- output << "" # Add blank line between results
168
- end
169
- output.join("\n")
170
- end
171
- end
172
-
173
- # Format display name with instance number and role (ADR-005)
174
- def format_model_display_name(spec)
175
- return spec unless spec.is_a?(Hash)
176
-
177
- model_name = spec[:model]
178
- instance = spec[:instance]
179
- role = spec[:role]
180
-
181
- display = if instance > 1
182
- "#{model_name} ##{instance}"
183
- else
184
- model_name
185
- end
186
-
187
- display += " (#{role})" if role
188
-
189
- display
190
- end
191
-
192
- def format_multi_model_with_metrics(results)
193
- formatted_content = []
194
- metrics_data = []
195
-
196
- results.each do |internal_id, result|
197
- spec = get_model_spec(internal_id)
198
- display_name = format_model_display_name(spec)
199
-
200
- formatted_content << "from: #{display_name}"
201
- content = result.respond_to?(:content) ? result.content : result.to_s
202
- formatted_content << content
203
- formatted_content << ""
204
-
205
- actual_model = spec ? spec[:model] : internal_id
206
- metrics_data << {
207
- model_id: actual_model,
208
- display_name: display_name,
209
- input_tokens: result.respond_to?(:input_tokens) ? result.input_tokens : nil,
210
- output_tokens: result.respond_to?(:output_tokens) ? result.output_tokens : nil
211
- }
212
- end
213
-
214
- MultiModelResponse.new(formatted_content.join("\n"), metrics_data)
215
- end
216
- end
217
- end
218
- end
@@ -1,59 +0,0 @@
1
- # lib/aia/adapter/provider_configurator.rb
2
- # frozen_string_literal: true
3
-
4
- module AIA
5
- module Adapter
6
- class ProviderConfigurator
7
- def self.configure
8
- # TODO: Add some of these configuration items to AIA.config
9
- # Note: RubyLLM supports specific providers. Use provider prefix (e.g., "xai/grok-beta")
10
- # for providers not directly configured here.
11
- RubyLLM.configure do |config|
12
- config.anthropic_api_key = ENV.fetch('ANTHROPIC_API_KEY', nil)
13
- config.deepseek_api_key = ENV.fetch('DEEPSEEK_API_KEY', nil)
14
- config.gemini_api_key = ENV.fetch('GEMINI_API_KEY', nil)
15
- config.gpustack_api_key = ENV.fetch('GPUSTACK_API_KEY', nil)
16
- config.mistral_api_key = ENV.fetch('MISTRAL_API_KEY', nil)
17
- config.openrouter_api_key = ENV.fetch('OPEN_ROUTER_API_KEY', nil)
18
- config.perplexity_api_key = ENV.fetch('PERPLEXITY_API_KEY', nil)
19
-
20
- # These providers require a little something extra
21
- config.openai_api_key = ENV.fetch('OPENAI_API_KEY', nil)
22
- config.openai_organization_id = ENV.fetch('OPENAI_ORGANIZATION_ID', nil)
23
- config.openai_project_id = ENV.fetch('OPENAI_PROJECT_ID', nil)
24
-
25
- config.bedrock_api_key = ENV.fetch('BEDROCK_ACCESS_KEY_ID', nil)
26
- config.bedrock_secret_key = ENV.fetch('BEDROCK_SECRET_ACCESS_KEY', nil)
27
- config.bedrock_region = ENV.fetch('BEDROCK_REGION', nil)
28
- config.bedrock_session_token = ENV.fetch('BEDROCK_SESSION_TOKEN', nil)
29
-
30
- # Ollama is based upon the OpenAI API so it needs to over-ride a few things
31
- config.ollama_api_base = ENV.fetch('OLLAMA_API_BASE', nil)
32
-
33
- # --- Custom OpenAI Endpoint ---
34
- # Use this for Azure OpenAI, proxies, or self-hosted models via OpenAI-compatible APIs.
35
- # For osaurus: Use model name prefix "osaurus/" and set OSAURUS_API_BASE env var
36
- # For LM Studio: Use model name prefix "lms/" and set LMS_API_BASE env var
37
- config.openai_api_base = ENV.fetch('OPENAI_API_BASE', nil) # e.g., "https://your-azure.openai.azure.com"
38
-
39
- # --- Connection Settings ---
40
- config.request_timeout = 120 # Request timeout in seconds (default: 120)
41
- config.max_retries = 3 # Max retries on transient network errors (default: 3)
42
- config.retry_interval = 0.1 # Initial delay in seconds (default: 0.1)
43
- config.retry_backoff_factor = 2 # Multiplier for subsequent retries (default: 2)
44
- config.retry_interval_randomness = 0.5 # Jitter factor (default: 0.5)
45
-
46
- # Configure RubyLLM logger from centralized LoggerManager
47
- config.log_level = LoggerManager.llm_log_level_symbol
48
- end
49
-
50
- # Configure RubyLLM's logger output destination
51
- LoggerManager.configure_llm_logger
52
-
53
- # Configure RubyLLM::MCP's logger early, before any MCP operations
54
- # This ensures all MCP debug/info logs go to the configured log file
55
- LoggerManager.configure_mcp_logger
56
- end
57
- end
58
- end
59
- end
@@ -1,85 +0,0 @@
1
- # lib/aia/adapter/tool_filter.rb
2
- # frozen_string_literal: true
3
-
4
- require 'set'
5
-
6
- module AIA
7
- module Adapter
8
- class ToolFilter
9
- def self.filter_allowed(tools)
10
- allowed = AIA.config.tools.allowed
11
- return tools if allowed.nil? || allowed.empty?
12
-
13
- allowed_list = Array(allowed).map(&:strip)
14
-
15
- tools.select do |tool|
16
- tool_name = tool.respond_to?(:name) ? tool.name : tool.class.name
17
- allowed_list.any? { |allowed_pattern| tool_name.include?(allowed_pattern) }
18
- end
19
- end
20
-
21
- def self.filter_rejected(tools)
22
- rejected = AIA.config.tools.rejected
23
- return tools if rejected.nil? || rejected.empty?
24
-
25
- rejected_list = Array(rejected).map(&:strip)
26
-
27
- tools.reject do |tool|
28
- tool_name = tool.respond_to?(:name) ? tool.name : tool.class.name
29
- rejected_list.any? { |rejected_pattern| tool_name.include?(rejected_pattern) }
30
- end
31
- end
32
-
33
- def self.detect_conflicts
34
- allowed = AIA.config.tools.allowed
35
- rejected = AIA.config.tools.rejected
36
- return [] if allowed.nil? || allowed.empty? || rejected.nil? || rejected.empty?
37
-
38
- allowed_list = Array(allowed).map(&:strip)
39
- rejected_list = Array(rejected).map(&:strip)
40
-
41
- conflicts = allowed_list & rejected_list
42
- return [] if conflicts.empty?
43
-
44
- logger = LoggerManager.aia_logger
45
- conflicts.each do |pattern|
46
- msg = "Tool pattern '#{pattern}' appears in both --allowed-tools and --rejected-tools. Rejected takes precedence."
47
- logger.warn(msg)
48
- warn "WARNING: #{msg}"
49
- end
50
-
51
- conflicts
52
- end
53
-
54
- def self.drop_duplicates(tools)
55
- seen_names = Set.new
56
- original_size = tools.size
57
-
58
- logger = LoggerManager.aia_logger
59
- logger.debug("Checking tools for duplicates", tool_count: original_size)
60
-
61
- result = tools.select do |tool|
62
- tool_name = tool.name
63
- if seen_names.include?(tool_name)
64
- logger.warn("Duplicate tool detected - keeping first occurrence only", tool: tool_name)
65
- warn "WARNING: Duplicate tool name detected: '#{tool_name}'. Only the first occurrence will be used."
66
- false
67
- else
68
- seen_names.add(tool_name)
69
- true
70
- end
71
- end
72
-
73
- removed_count = original_size - result.size
74
- if removed_count > 0
75
- logger.info("Removed duplicate tools", removed_count: removed_count, remaining_count: result.size)
76
- warn "Removed #{removed_count} duplicate tools"
77
- else
78
- logger.debug("No duplicate tools found")
79
- end
80
-
81
- result
82
- end
83
- end
84
- end
85
- end
@@ -1,90 +0,0 @@
1
- # lib/aia/adapter/tool_loader.rb
2
- # frozen_string_literal: true
3
-
4
- module AIA
5
- module Adapter
6
- class ToolLoader
7
- def initialize(mcp_connector)
8
- @mcp_connector = mcp_connector
9
- end
10
-
11
- def load_tools
12
- tools = []
13
-
14
- tools += scan_local_tools
15
- @mcp_connector.support_mcp(tools)
16
- tools
17
- end
18
-
19
- def scan_local_tools
20
- tools = []
21
-
22
- # First, load any required libraries specified in config
23
- load_require_libs
24
-
25
- # Then, load tool files from tools.paths
26
- load_tool_files
27
-
28
- # Now scan ObjectSpace for RubyLLM::Tool subclasses
29
- tool_classes = ObjectSpace.each_object(Class).select do |klass|
30
- next false unless klass < RubyLLM::Tool
31
-
32
- # Filter out tools that can't be instantiated without arguments
33
- # RubyLLM calls tool.new without args, so we must verify each tool works
34
- begin
35
- klass.new
36
- true
37
- rescue ArgumentError, LoadError, StandardError
38
- # Skip tools that require arguments or have missing dependencies
39
- false
40
- end
41
- end
42
-
43
- tools + tool_classes
44
- end
45
-
46
- private
47
-
48
- def load_require_libs
49
- require_libs = AIA.config.require_libs
50
- return if require_libs.nil? || require_libs.empty?
51
-
52
- require_libs.each do |lib|
53
- begin
54
- # Activate gem and add to load path (bypasses Bundler's restrictions)
55
- GemActivator.activate_gem_for_require(lib)
56
-
57
- require lib
58
-
59
- # After requiring, trigger tool loading if the library supports it
60
- # This handles gems like shared_tools that use Zeitwerk lazy loading
61
- GemActivator.trigger_tool_loading(lib)
62
- rescue LoadError => e
63
- warn "Warning: Failed to require library '#{lib}': #{e.message}"
64
- warn "Hint: Make sure the gem is installed: gem install #{lib}"
65
- rescue StandardError => e
66
- warn "Warning: Error in library '#{lib}': #{e.class} - #{e.message}"
67
- end
68
- end
69
- end
70
-
71
- def load_tool_files
72
- paths = AIA.config.tools&.paths
73
- return if paths.nil? || paths.empty?
74
-
75
- paths.each do |path|
76
- expanded_path = File.expand_path(path)
77
- if File.exist?(expanded_path)
78
- begin
79
- require expanded_path
80
- rescue LoadError, StandardError => e
81
- warn "Warning: Failed to load tool file '#{path}': #{e.message}"
82
- end
83
- else
84
- warn "Warning: Tool file not found: #{path}"
85
- end
86
- end
87
- end
88
- end
89
- end
90
- end