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
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
# lib/aia/chat_processor_service.rb
|
|
2
|
-
|
|
3
|
-
module AIA
|
|
4
|
-
class ChatProcessorService
|
|
5
|
-
def initialize(ui_presenter, directive_processor = nil)
|
|
6
|
-
@ui_presenter = ui_presenter
|
|
7
|
-
@speaker = AIA.speak? ? AiClient.new(AIA.config.audio.speech_model) : nil
|
|
8
|
-
@directive_processor = directive_processor
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def speak(text)
|
|
13
|
-
return unless AIA.speak?
|
|
14
|
-
|
|
15
|
-
@speaker ||= AiClient.new(AIA.config.audio.speech_model) if AIA.config.audio.speech_model
|
|
16
|
-
|
|
17
|
-
if @speaker
|
|
18
|
-
system(AIA.config.audio.speak_command, @speaker.speak(text).path)
|
|
19
|
-
else
|
|
20
|
-
warn "Warning: Unable to speak. Speech model not configured properly."
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def process_prompt(prompt)
|
|
26
|
-
result = nil
|
|
27
|
-
@ui_presenter.with_spinner("Processing", determine_operation_type) do
|
|
28
|
-
result = send_to_client(prompt)
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
# Debug output to understand what we're receiving
|
|
32
|
-
logger.debug("Result received", result_class: result.class.name)
|
|
33
|
-
logger.debug("Result details", inspect: result.inspect[0..500])
|
|
34
|
-
|
|
35
|
-
# Preserve token information if available for metrics
|
|
36
|
-
if result.is_a?(String)
|
|
37
|
-
logger.debug("Processing result", type: "String")
|
|
38
|
-
{ content: result, metrics: nil }
|
|
39
|
-
elsif result.respond_to?(:multi_model?) && result.multi_model?
|
|
40
|
-
logger.debug("Processing result", type: "multi-model response")
|
|
41
|
-
# Handle multi-model response with metrics
|
|
42
|
-
{
|
|
43
|
-
content: result.content,
|
|
44
|
-
metrics: nil, # Individual model metrics handled separately
|
|
45
|
-
multi_metrics: result.metrics_list
|
|
46
|
-
}
|
|
47
|
-
elsif result.respond_to?(:content)
|
|
48
|
-
logger.debug("Processing result", type: "standard response with content method")
|
|
49
|
-
# Standard response object with content method
|
|
50
|
-
{
|
|
51
|
-
content: result.content,
|
|
52
|
-
metrics: {
|
|
53
|
-
input_tokens: result.respond_to?(:input_tokens) ? result.input_tokens : nil,
|
|
54
|
-
output_tokens: result.respond_to?(:output_tokens) ? result.output_tokens : nil,
|
|
55
|
-
model_id: result.respond_to?(:model_id) ? result.model_id : nil
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
else
|
|
59
|
-
logger.debug("Processing result", type: "fallback (unexpected type)")
|
|
60
|
-
# Fallback for unexpected response types
|
|
61
|
-
{ content: result.to_s, metrics: nil }
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
# conversation is an Array of Hashes (single model) or Hash of Arrays (multi-model per-model contexts)
|
|
67
|
-
# Each entry is an interchange with the LLM.
|
|
68
|
-
def send_to_client(conversation_or_conversations)
|
|
69
|
-
maybe_change_model
|
|
70
|
-
|
|
71
|
-
# Handle per-model conversations (Hash) or single conversation (Array) - ADR-002 revised
|
|
72
|
-
if conversation_or_conversations.is_a?(Hash)
|
|
73
|
-
# Multi-model with per-model contexts: pass Hash directly to adapter
|
|
74
|
-
logger.debug("Sending per-model conversations to client")
|
|
75
|
-
result = AIA.client.chat(conversation_or_conversations)
|
|
76
|
-
else
|
|
77
|
-
# Single conversation for single model
|
|
78
|
-
logger.debug("Sending conversation to client", conversation: conversation_or_conversations.inspect[0..500])
|
|
79
|
-
result = AIA.client.chat(conversation_or_conversations)
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
logger.debug("Client returned", result_class: result.class.name, result: result.inspect[0..500])
|
|
83
|
-
result
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
def maybe_change_model
|
|
88
|
-
# With multiple models, we don't need to change the model in the same way
|
|
89
|
-
# The RubyLLMAdapter now handles multiple models internally
|
|
90
|
-
# This method is kept for backward compatibility but may not be needed
|
|
91
|
-
models = AIA.config.models
|
|
92
|
-
return if models.is_a?(Array) && models.size > 1
|
|
93
|
-
|
|
94
|
-
return unless AIA.client.respond_to?(:model) && AIA.client.model.respond_to?(:id)
|
|
95
|
-
client_model = AIA.client.model.id
|
|
96
|
-
|
|
97
|
-
# Get the first model name for comparison
|
|
98
|
-
first_model = models.first
|
|
99
|
-
model_name = first_model.respond_to?(:name) ? first_model.name : first_model.to_s
|
|
100
|
-
|
|
101
|
-
# client_model is the full resolved ID (e.g. "claude-sonnet-4-20250514"),
|
|
102
|
-
# model_name is the configured alias (e.g. "claude-sonnet-4").
|
|
103
|
-
# The alias is always a prefix/substring of the resolved ID, so check that way.
|
|
104
|
-
unless client_model.downcase.include?(model_name.downcase)
|
|
105
|
-
AIA.client = AIA.client.class.new
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
def output_response(response)
|
|
111
|
-
speak(response)
|
|
112
|
-
|
|
113
|
-
out_file = AIA.config.output.file
|
|
114
|
-
|
|
115
|
-
# Output to STDOUT or file based on out_file configuration
|
|
116
|
-
if out_file.nil? || 'STDOUT' == out_file.upcase
|
|
117
|
-
print "\nAI:\n "
|
|
118
|
-
puts response
|
|
119
|
-
else
|
|
120
|
-
mode = AIA.append? ? 'a' : 'w'
|
|
121
|
-
File.open(out_file, mode) do |file|
|
|
122
|
-
file.puts "\nAI: "
|
|
123
|
-
# Handle multi-line responses by adding proper indentation
|
|
124
|
-
response_lines = response.to_s.split("\n")
|
|
125
|
-
response_lines.each do |line|
|
|
126
|
-
file.puts " #{line}"
|
|
127
|
-
end
|
|
128
|
-
end
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
history_file = AIA.config.output.history_file
|
|
132
|
-
if history_file
|
|
133
|
-
File.open(history_file, 'a') do |f|
|
|
134
|
-
f.puts "=== #{Time.now} ==="
|
|
135
|
-
f.puts "Prompt: #{AIA.config.prompt_id}"
|
|
136
|
-
f.puts "Response: #{response}"
|
|
137
|
-
f.puts "==="
|
|
138
|
-
end
|
|
139
|
-
end
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
def process_next_prompts(response, prompt_handler)
|
|
144
|
-
if @directive_processor.directive?(response)
|
|
145
|
-
directive_result = @directive_processor.process(response, @history_manager.history)
|
|
146
|
-
response = directive_result[:result]
|
|
147
|
-
@history_manager.history = directive_result[:modified_history] if directive_result[:modified_history]
|
|
148
|
-
end
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
def determine_operation_type
|
|
153
|
-
# With multiple models, determine operation type from the first model
|
|
154
|
-
# or provide a generic description
|
|
155
|
-
models = AIA.config.models
|
|
156
|
-
if models.is_a?(Array) && models.size > 1
|
|
157
|
-
"MULTI-MODEL PROCESSING"
|
|
158
|
-
else
|
|
159
|
-
mode = AIA.client.model.modalities
|
|
160
|
-
mode.input.join(',') + " TO " + mode.output.join(',')
|
|
161
|
-
end
|
|
162
|
-
end
|
|
163
|
-
end
|
|
164
|
-
end
|
data/lib/aia/prompt_pipeline.rb
DELETED
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
# lib/aia/prompt_pipeline.rb
|
|
2
|
-
# frozen_string_literal: true
|
|
3
|
-
|
|
4
|
-
require "pm"
|
|
5
|
-
|
|
6
|
-
module AIA
|
|
7
|
-
class PromptPipeline
|
|
8
|
-
include AIA::SkillUtils
|
|
9
|
-
|
|
10
|
-
def initialize(prompt_handler, chat_processor, ui_presenter, input_collector)
|
|
11
|
-
@prompt_handler = prompt_handler
|
|
12
|
-
@chat_processor = chat_processor
|
|
13
|
-
@ui_presenter = ui_presenter
|
|
14
|
-
@input_collector = input_collector
|
|
15
|
-
@include_context_flag = true
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
# Process all prompts in the pipeline sequentially
|
|
19
|
-
def process_all
|
|
20
|
-
prompt_count = 0
|
|
21
|
-
total_prompts = AIA.config.pipeline.size
|
|
22
|
-
|
|
23
|
-
until AIA.config.pipeline.empty?
|
|
24
|
-
prompt_count += 1
|
|
25
|
-
prompt_id = AIA.config.pipeline.shift
|
|
26
|
-
|
|
27
|
-
puts "\n--- Processing prompt #{prompt_count}/#{total_prompts}: #{prompt_id} ---" if AIA.verbose? && total_prompts > 1
|
|
28
|
-
|
|
29
|
-
process_single(prompt_id)
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
# Process a single prompt with all its requirements
|
|
34
|
-
def process_single(prompt_id)
|
|
35
|
-
return if prompt_id.nil? || prompt_id.empty?
|
|
36
|
-
|
|
37
|
-
prompt_text = build_prompt_text(prompt_id)
|
|
38
|
-
return unless prompt_text
|
|
39
|
-
|
|
40
|
-
send_and_get_response(prompt_text)
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def build_prompt_text(prompt_id)
|
|
44
|
-
role_id = AIA.config.prompts.role
|
|
45
|
-
|
|
46
|
-
begin
|
|
47
|
-
prompt_parsed = @prompt_handler.fetch_prompt(prompt_id)
|
|
48
|
-
rescue StandardError => e
|
|
49
|
-
warn "Error processing prompt '#{prompt_id}': #{e.message}"
|
|
50
|
-
AIA::LoggerManager.aia_logger.error("Error processing prompt '#{prompt_id}': #{e.message}")
|
|
51
|
-
return nil
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
return nil unless prompt_parsed
|
|
55
|
-
|
|
56
|
-
role_parsed = nil
|
|
57
|
-
unless role_id.nil? || role_id.empty?
|
|
58
|
-
begin
|
|
59
|
-
role_parsed = @prompt_handler.fetch_role(role_id)
|
|
60
|
-
rescue StandardError => e
|
|
61
|
-
warn "Warning: Could not load role '#{role_id}': #{e.message}"
|
|
62
|
-
AIA::LoggerManager.aia_logger.warn("Could not load role '#{role_id}': #{e.message}")
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
# Merge parameters from role and prompt
|
|
67
|
-
all_params = {}
|
|
68
|
-
all_params.merge!(role_parsed.metadata.parameters) if role_parsed&.metadata&.parameters
|
|
69
|
-
all_params.merge!(prompt_parsed.metadata.parameters) if prompt_parsed.metadata&.parameters
|
|
70
|
-
|
|
71
|
-
# Collect parameter values from user
|
|
72
|
-
values = @input_collector.collect(all_params)
|
|
73
|
-
|
|
74
|
-
# Render role, skills, and prompt.
|
|
75
|
-
# Order: role (personality) → skills (task instructions) → prompt (user request)
|
|
76
|
-
parts = []
|
|
77
|
-
parts << role_parsed.to_s(values) if role_parsed
|
|
78
|
-
load_skills(AIA.config.prompts.skills).each { |body| parts << body }
|
|
79
|
-
parts << prompt_parsed.to_s(values)
|
|
80
|
-
|
|
81
|
-
if @include_context_flag
|
|
82
|
-
# Process stdin content
|
|
83
|
-
if AIA.config.stdin_content && !AIA.config.stdin_content.strip.empty?
|
|
84
|
-
parts << PM.parse(AIA.config.stdin_content).to_s
|
|
85
|
-
end
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
prompt_text = parts.join("\n\n")
|
|
89
|
-
|
|
90
|
-
if @include_context_flag
|
|
91
|
-
prompt_text = add_context_files(prompt_text)
|
|
92
|
-
@include_context_flag = false
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
prompt_text
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
# Load skill bodies for the given skill IDs in order.
|
|
99
|
-
# Each skill lives at skills_dir/<name>/SKILL.md; supports prefix matching.
|
|
100
|
-
# Path-based IDs (starting with /, ~/, ./, ../) are resolved as direct paths.
|
|
101
|
-
# Returns only the body content (front matter stripped).
|
|
102
|
-
def load_skills(skill_ids)
|
|
103
|
-
return [] if skill_ids.nil? || skill_ids.empty?
|
|
104
|
-
|
|
105
|
-
skills_dir = AIA.config.skills.dir
|
|
106
|
-
|
|
107
|
-
Array(skill_ids).filter_map do |skill_name|
|
|
108
|
-
skill_name = skill_name.to_s.strip
|
|
109
|
-
next if skill_name.empty?
|
|
110
|
-
|
|
111
|
-
unless path_based_id?(skill_name) || Dir.exist?(skills_dir)
|
|
112
|
-
warn "Warning: No skill matching '#{skill_name}' found in #{skills_dir}"
|
|
113
|
-
next
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
skill_dir = find_skill_dir(skill_name, skills_dir)
|
|
117
|
-
unless skill_dir
|
|
118
|
-
if path_based_id?(skill_name)
|
|
119
|
-
warn "Warning: No skill directory found at '#{File.expand_path(skill_name)}'"
|
|
120
|
-
else
|
|
121
|
-
warn "Warning: No skill matching '#{skill_name}' found in #{skills_dir}"
|
|
122
|
-
end
|
|
123
|
-
next
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
next skill_body(File.read(skill_dir)) if File.file?(skill_dir)
|
|
127
|
-
|
|
128
|
-
skill_path = File.join(skill_dir, 'SKILL.md')
|
|
129
|
-
unless File.exist?(skill_path)
|
|
130
|
-
warn "Warning: Skill '#{skill_name}' has no SKILL.md in #{skill_dir}"
|
|
131
|
-
next
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
skill_body(File.read(skill_path))
|
|
135
|
-
end
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
# Add context files to prompt text
|
|
139
|
-
def add_context_files(prompt_text)
|
|
140
|
-
return prompt_text unless AIA.config.context_files && !AIA.config.context_files.empty?
|
|
141
|
-
|
|
142
|
-
context = AIA.config.context_files.map do |file|
|
|
143
|
-
File.read(file) rescue "Error reading file: #{file}"
|
|
144
|
-
end.join("\n\n")
|
|
145
|
-
|
|
146
|
-
"#{prompt_text}\n\nContext:\n#{context}"
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
private
|
|
150
|
-
|
|
151
|
-
# Send prompt to AI and handle the response
|
|
152
|
-
def send_and_get_response(prompt_text)
|
|
153
|
-
response_data = @chat_processor.process_prompt(prompt_text)
|
|
154
|
-
|
|
155
|
-
if response_data.is_a?(Hash)
|
|
156
|
-
content = response_data[:content]
|
|
157
|
-
metrics = response_data[:metrics]
|
|
158
|
-
multi_metrics = response_data[:multi_metrics]
|
|
159
|
-
else
|
|
160
|
-
content = response_data
|
|
161
|
-
metrics = nil
|
|
162
|
-
multi_metrics = nil
|
|
163
|
-
end
|
|
164
|
-
|
|
165
|
-
@chat_processor.output_response(content)
|
|
166
|
-
|
|
167
|
-
if AIA.config.flags.tokens
|
|
168
|
-
if multi_metrics
|
|
169
|
-
@ui_presenter.display_multi_model_metrics(multi_metrics)
|
|
170
|
-
elsif metrics
|
|
171
|
-
@ui_presenter.display_token_metrics(metrics)
|
|
172
|
-
end
|
|
173
|
-
end
|
|
174
|
-
|
|
175
|
-
# Process any directives in the response
|
|
176
|
-
directive_processor = DirectiveProcessor.new
|
|
177
|
-
if directive_processor.directive?(content)
|
|
178
|
-
directive_result = directive_processor.process(content, nil)
|
|
179
|
-
puts "\nDirective output: #{directive_result}" if directive_result && !directive_result.strip.empty?
|
|
180
|
-
end
|
|
181
|
-
end
|
|
182
|
-
end
|
|
183
|
-
end
|
data/lib/aia/ruby_llm_adapter.rb
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
# lib/aia/ruby_llm_adapter.rb
|
|
2
|
-
# frozen_string_literal: true
|
|
3
|
-
|
|
4
|
-
require 'fileutils'
|
|
5
|
-
require 'json'
|
|
6
|
-
require_relative '../extensions/ruby_llm/provider_fix'
|
|
7
|
-
require_relative 'adapter/gem_activator'
|
|
8
|
-
require_relative 'adapter/provider_configurator'
|
|
9
|
-
require_relative 'adapter/model_registry'
|
|
10
|
-
require_relative 'adapter/tool_loader'
|
|
11
|
-
require_relative 'adapter/mcp_connector'
|
|
12
|
-
require_relative 'adapter/tool_filter'
|
|
13
|
-
require_relative 'adapter/chat_execution'
|
|
14
|
-
require_relative 'adapter/multi_model_chat'
|
|
15
|
-
require_relative 'adapter/modality_handlers'
|
|
16
|
-
require_relative 'adapter/error_handler'
|
|
17
|
-
|
|
18
|
-
module AIA
|
|
19
|
-
class RubyLLMAdapter
|
|
20
|
-
include Adapter::ChatExecution
|
|
21
|
-
include Adapter::MultiModelChat
|
|
22
|
-
include Adapter::ModalityHandlers
|
|
23
|
-
include Adapter::ErrorHandler
|
|
24
|
-
|
|
25
|
-
# Re-export for backward compatibility (tests reference AIA::RubyLLMAdapter::MultiModelResponse)
|
|
26
|
-
MultiModelResponse = Adapter::MultiModelChat::MultiModelResponse
|
|
27
|
-
|
|
28
|
-
attr_reader :tools, :model_specs, :chats
|
|
29
|
-
|
|
30
|
-
# Delegates to the first chat instance's model object.
|
|
31
|
-
# Used by single-model callers to inspect the active model.
|
|
32
|
-
def model
|
|
33
|
-
@chats.values.first&.model
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def initialize
|
|
37
|
-
@model_specs = extract_models_config # Full specs with role info
|
|
38
|
-
@models = extract_model_names(@model_specs) # Just model names for backward compat
|
|
39
|
-
@chats = {}
|
|
40
|
-
@contexts = {} # Store isolated contexts for each model
|
|
41
|
-
|
|
42
|
-
Adapter::ProviderConfigurator.configure
|
|
43
|
-
Adapter::ModelRegistry.new.refresh
|
|
44
|
-
setup_chats_with_tools
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
private
|
|
48
|
-
|
|
49
|
-
# Helper to access the AIA logger for application-level logging
|
|
50
|
-
def logger
|
|
51
|
-
@logger ||= LoggerManager.aia_logger
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def extract_models_config
|
|
55
|
-
# Use config.models which returns array of ModelSpec objects
|
|
56
|
-
models_config = AIA.config.models
|
|
57
|
-
|
|
58
|
-
if models_config.nil? || models_config.empty?
|
|
59
|
-
# Fallback to default
|
|
60
|
-
[{model: 'gpt-4o-mini', role: nil, instance: 1, internal_id: 'gpt-4o-mini'}]
|
|
61
|
-
else
|
|
62
|
-
# Convert ModelSpec objects to hash format expected by adapter
|
|
63
|
-
models_config.map do |spec|
|
|
64
|
-
if spec.respond_to?(:name)
|
|
65
|
-
# ModelSpec object
|
|
66
|
-
{model: spec.name, role: spec.role, instance: spec.instance, internal_id: spec.internal_id}
|
|
67
|
-
elsif spec.is_a?(Hash)
|
|
68
|
-
# Hash format (legacy or from config.model accessor)
|
|
69
|
-
model_name = spec[:model] || spec[:name]
|
|
70
|
-
{model: model_name, role: spec[:role], instance: spec[:instance] || 1, internal_id: spec[:internal_id] || model_name}
|
|
71
|
-
elsif spec.is_a?(String)
|
|
72
|
-
# String format (legacy)
|
|
73
|
-
{model: spec, role: nil, instance: 1, internal_id: spec}
|
|
74
|
-
else
|
|
75
|
-
# Unknown format, skip
|
|
76
|
-
nil
|
|
77
|
-
end
|
|
78
|
-
end.compact
|
|
79
|
-
end
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
def extract_model_names(model_specs)
|
|
83
|
-
# Extract just the model names from the specs
|
|
84
|
-
# For models with instance > 1, use internal_id (e.g., "gpt-4o#2")
|
|
85
|
-
model_specs.map do |spec|
|
|
86
|
-
spec[:internal_id]
|
|
87
|
-
end
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
def get_model_spec(internal_id)
|
|
91
|
-
# Find the spec for a given internal_id
|
|
92
|
-
@model_specs.find { |spec| spec[:internal_id] == internal_id }
|
|
93
|
-
end
|
|
94
|
-
end
|
|
95
|
-
end
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
# lib/extensions/openstruct_merge.rb
|
|
2
|
-
#
|
|
3
|
-
|
|
4
|
-
require 'ostruct'
|
|
5
|
-
|
|
6
|
-
class OpenStruct
|
|
7
|
-
def self.merge(*args)
|
|
8
|
-
result = OpenStruct.new
|
|
9
|
-
|
|
10
|
-
args.each do |arg|
|
|
11
|
-
unless [Hash, OpenStruct].include?(arg.class)
|
|
12
|
-
raise ArgumentError, "Only OpenStruct or Hash objects are allowed. bad: #{arg.class}"
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
arg.each_pair do |key, value|
|
|
16
|
-
set_value(result, key, value)
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
result
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
# Sets value in result OpenStruct, handling nested OpenStruct and Hash objects
|
|
24
|
-
# Skip nil values to avoid overwriting existing values with nil
|
|
25
|
-
def self.set_value(result, key, value)
|
|
26
|
-
# Skip nil values - don't overwrite existing values with nil
|
|
27
|
-
return if value.nil?
|
|
28
|
-
|
|
29
|
-
if value.is_a?(OpenStruct) || value.is_a?(Hash)
|
|
30
|
-
current_value = result[key]
|
|
31
|
-
current_value = {} if current_value.nil?
|
|
32
|
-
merged_value = merge(current_value, value.to_h)
|
|
33
|
-
result[key] = merged_value
|
|
34
|
-
else
|
|
35
|
-
result[key] = value
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
__END__
|
|
41
|
-
|
|
42
|
-
# Usage example
|
|
43
|
-
os1 = OpenStruct.new(a: 1, b: 2, e: OpenStruct.new(x: 9))
|
|
44
|
-
os2 = OpenStruct.new(b: 3, c: 4)
|
|
45
|
-
os3 = {d: 5, e: {y: 10}}
|
|
46
|
-
|
|
47
|
-
merged_os = OpenStruct.merge(os1, os2, os3)
|
|
48
|
-
puts merged_os.inspect
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
require 'ruby_llm'
|
|
2
|
-
require_relative 'modalities'
|
|
3
|
-
|
|
4
|
-
RubyLLM.configure do |config|
|
|
5
|
-
config.openai_api_key = ENV['OPENAI_API_KEY']
|
|
6
|
-
end
|
|
7
|
-
|
|
8
|
-
C = RubyLLM::Chat.new
|
|
9
|
-
M = C.model.modalities
|
|
10
|
-
|
|
11
|
-
__END__
|
|
12
|
-
|
|
13
|
-
# I = ["text", "image", "pdf", "audio", "file"]
|
|
14
|
-
# O = ["text", "embeddings", "audio", "image", "moderation"]
|
|
15
|
-
|
|
16
|
-
# I.each do |i|
|
|
17
|
-
# puts '#'
|
|
18
|
-
# O.each do |o|
|
|
19
|
-
# puts <<~HEREDOC
|
|
20
|
-
# def #{i}_to_#{o}? = input.include?('#{i}') && output.include?('#{o}')
|
|
21
|
-
# HEREDOC
|
|
22
|
-
# end
|
|
23
|
-
# end
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
#
|
|
28
|
-
def text_to_text? = input.include?('text') && output.include?('text')
|
|
29
|
-
def text_to_embeddings? = input.include?('text') && output.include?('embeddings')
|
|
30
|
-
def text_to_audio? = input.include?('text') && output.include?('audio')
|
|
31
|
-
def text_to_image? = input.include?('text') && output.include?('image')
|
|
32
|
-
def text_to_moderation? = input.include?('text') && output.include?('moderation')
|
|
33
|
-
#
|
|
34
|
-
def image_to_text? = input.include?('image') && output.include?('text')
|
|
35
|
-
def image_to_embeddings? = input.include?('image') && output.include?('embeddings')
|
|
36
|
-
def image_to_audio? = input.include?('image') && output.include?('audio')
|
|
37
|
-
def image_to_image? = input.include?('image') && output.include?('image')
|
|
38
|
-
def image_to_moderation? = input.include?('image') && output.include?('moderation')
|
|
39
|
-
#
|
|
40
|
-
def pdf_to_text? = input.include?('pdf') && output.include?('text')
|
|
41
|
-
def pdf_to_embeddings? = input.include?('pdf') && output.include?('embeddings')
|
|
42
|
-
def pdf_to_audio? = input.include?('pdf') && output.include?('audio')
|
|
43
|
-
def pdf_to_image? = input.include?('pdf') && output.include?('image')
|
|
44
|
-
def pdf_to_moderation? = input.include?('pdf') && output.include?('moderation')
|
|
45
|
-
#
|
|
46
|
-
def audio_to_text? = input.include?('audio') && output.include?('text')
|
|
47
|
-
def audio_to_embeddings? = input.include?('audio') && output.include?('embeddings')
|
|
48
|
-
def audio_to_audio? = input.include?('audio') && output.include?('audio')
|
|
49
|
-
def audio_to_image? = input.include?('audio') && output.include?('image')
|
|
50
|
-
def audio_to_moderation? = input.include?('audio') && output.include?('moderation')
|
|
51
|
-
#
|
|
52
|
-
def file_to_text? = input.include?('file') && output.include?('text')
|
|
53
|
-
def file_to_embeddings? = input.include?('file') && output.include?('embeddings')
|
|
54
|
-
def file_to_audio? = input.include?('file') && output.include?('audio')
|
|
55
|
-
def file_to_image? = input.include?('file') && output.include?('image')
|
|
56
|
-
def file_to_moderation? = input.include?('file') && output.include?('moderation')
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
# lib/extensions/ruby_llm/modalities.rb
|
|
2
|
-
|
|
3
|
-
require 'ruby_llm'
|
|
4
|
-
|
|
5
|
-
class RubyLLM::Model::Modalities
|
|
6
|
-
#
|
|
7
|
-
def text_to_text? = input.include?('text') && output.include?('text')
|
|
8
|
-
def text_to_embeddings? = input.include?('text') && output.include?('embeddings')
|
|
9
|
-
def text_to_audio? = input.include?('text') && output.include?('audio')
|
|
10
|
-
def text_to_image? = input.include?('text') && output.include?('image')
|
|
11
|
-
def text_to_moderation? = input.include?('text') && output.include?('moderation')
|
|
12
|
-
#
|
|
13
|
-
def image_to_text? = input.include?('image') && output.include?('text')
|
|
14
|
-
def image_to_embeddings? = input.include?('image') && output.include?('embeddings')
|
|
15
|
-
def image_to_audio? = input.include?('image') && output.include?('audio')
|
|
16
|
-
def image_to_image? = input.include?('image') && output.include?('image')
|
|
17
|
-
def image_to_moderation? = input.include?('image') && output.include?('moderation')
|
|
18
|
-
#
|
|
19
|
-
def pdf_to_text? = input.include?('pdf') && output.include?('text')
|
|
20
|
-
def pdf_to_embeddings? = input.include?('pdf') && output.include?('embeddings')
|
|
21
|
-
def pdf_to_audio? = input.include?('pdf') && output.include?('audio')
|
|
22
|
-
def pdf_to_image? = input.include?('pdf') && output.include?('image')
|
|
23
|
-
def pdf_to_moderation? = input.include?('pdf') && output.include?('moderation')
|
|
24
|
-
#
|
|
25
|
-
def audio_to_text? = input.include?('audio') && output.include?('text')
|
|
26
|
-
def audio_to_embeddings? = input.include?('audio') && output.include?('embeddings')
|
|
27
|
-
def audio_to_audio? = input.include?('audio') && output.include?('audio')
|
|
28
|
-
def audio_to_image? = input.include?('audio') && output.include?('image')
|
|
29
|
-
def audio_to_moderation? = input.include?('audio') && output.include?('moderation')
|
|
30
|
-
#
|
|
31
|
-
def file_to_text? = input.include?('file') && output.include?('text')
|
|
32
|
-
def file_to_embeddings? = input.include?('file') && output.include?('embeddings')
|
|
33
|
-
def file_to_audio? = input.include?('file') && output.include?('audio')
|
|
34
|
-
def file_to_image? = input.include?('file') && output.include?('image')
|
|
35
|
-
def file_to_moderation? = input.include?('file') && output.include?('moderation')
|
|
36
|
-
end
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
# lib/extensions/ruby_llm/provider_fix.rb
|
|
2
|
-
#
|
|
3
|
-
# Monkey patch to fix LM Studio compatibility with RubyLLM
|
|
4
|
-
# LM Studio sometimes returns response.body as a String that fails JSON parsing
|
|
5
|
-
# This causes "String does not have #dig method" errors in parse_error
|
|
6
|
-
|
|
7
|
-
# Load RubyLLM first to ensure Provider class exists
|
|
8
|
-
require 'ruby_llm'
|
|
9
|
-
|
|
10
|
-
module RubyLLM
|
|
11
|
-
module ProviderErrorFix
|
|
12
|
-
# Override the parse_error method to handle String responses from LM Studio
|
|
13
|
-
# Parses error response from provider API.
|
|
14
|
-
#
|
|
15
|
-
# Supports two error formats:
|
|
16
|
-
# 1. OpenAI standard: {"error": {"message": "...", "type": "...", "code": "..."}}
|
|
17
|
-
# 2. Simple format: {"error": "error message"}
|
|
18
|
-
#
|
|
19
|
-
# @param response [Faraday::Response] The HTTP response
|
|
20
|
-
# @return [String, nil] The error message or nil if parsing fails
|
|
21
|
-
#
|
|
22
|
-
# @example OpenAI format
|
|
23
|
-
# response = double(body: '{"error": {"message": "Rate limit exceeded"}}')
|
|
24
|
-
# parse_error(response) #=> "Rate limit exceeded"
|
|
25
|
-
#
|
|
26
|
-
# @example Simple format (LM Studio, some local providers)
|
|
27
|
-
# response = double(body: '{"error": "Token limit exceeded"}')
|
|
28
|
-
# parse_error(response) #=> "Token limit exceeded"
|
|
29
|
-
def parse_error(response)
|
|
30
|
-
return if response.body.empty?
|
|
31
|
-
|
|
32
|
-
body = try_parse_json(response.body)
|
|
33
|
-
case body
|
|
34
|
-
when Hash
|
|
35
|
-
# Handle both formats:
|
|
36
|
-
# - {"error": "message"} (LM Studio, some providers)
|
|
37
|
-
# - {"error": {"message": "..."}} (OpenAI standard)
|
|
38
|
-
error_value = body['error']
|
|
39
|
-
return nil unless error_value
|
|
40
|
-
|
|
41
|
-
case error_value
|
|
42
|
-
when Hash
|
|
43
|
-
error_value['message']
|
|
44
|
-
when String
|
|
45
|
-
error_value
|
|
46
|
-
else
|
|
47
|
-
error_value.to_s if error_value
|
|
48
|
-
end
|
|
49
|
-
when Array
|
|
50
|
-
body.filter_map do |part|
|
|
51
|
-
next unless part.is_a?(Hash)
|
|
52
|
-
|
|
53
|
-
error_value = part['error']
|
|
54
|
-
next unless error_value
|
|
55
|
-
|
|
56
|
-
case error_value
|
|
57
|
-
when Hash then error_value['message']
|
|
58
|
-
when String then error_value
|
|
59
|
-
else error_value.to_s if error_value
|
|
60
|
-
end
|
|
61
|
-
end.join('. ')
|
|
62
|
-
else
|
|
63
|
-
body.to_s
|
|
64
|
-
end
|
|
65
|
-
rescue StandardError => e
|
|
66
|
-
RubyLLM.logger.debug "Error parsing response: #{e.message}"
|
|
67
|
-
nil
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
# Apply the prepend to all Provider subclasses
|
|
73
|
-
# LM Studio uses the OpenAI provider, so we need to prepend to all provider classes
|
|
74
|
-
RubyLLM::Provider.prepend(RubyLLM::ProviderErrorFix)
|
|
75
|
-
|
|
76
|
-
# Also prepend to all registered provider classes
|
|
77
|
-
RubyLLM::Provider.providers.each do |slug, provider_class|
|
|
78
|
-
provider_class.prepend(RubyLLM::ProviderErrorFix)
|
|
79
|
-
end
|
data/lib/refinements/string.rb
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
# lib/aia_refinements/string.rb
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
module Refinements
|
|
5
|
-
refine String do
|
|
6
|
-
def include_all?(substrings)
|
|
7
|
-
Array(substrings).all? { |substring| self.include?(substring) }
|
|
8
|
-
end
|
|
9
|
-
alias :all? :include_all?
|
|
10
|
-
|
|
11
|
-
def include_any?(substrings)
|
|
12
|
-
Array(substrings).any? { |substring| self.include?(substring) }
|
|
13
|
-
end
|
|
14
|
-
alias :any? :include_any?
|
|
15
|
-
end
|
|
16
|
-
end
|