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
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# lib/aia/prompt_decomposer.rb
|
|
4
|
+
#
|
|
5
|
+
# Decomposes complex prompts into independent sub-tasks.
|
|
6
|
+
# A coordinator robot breaks them down, specialist robots execute,
|
|
7
|
+
# and the coordinator reassembles.
|
|
8
|
+
|
|
9
|
+
require 'json'
|
|
10
|
+
|
|
11
|
+
module AIA
|
|
12
|
+
class PromptDecomposer
|
|
13
|
+
include ContentExtractor
|
|
14
|
+
|
|
15
|
+
DECOMPOSITION_PROMPT = <<~PROMPT
|
|
16
|
+
Analyze this user request and determine if it can be broken into independent sub-tasks.
|
|
17
|
+
|
|
18
|
+
Rules:
|
|
19
|
+
- Only decompose if sub-tasks are truly independent (can run in parallel)
|
|
20
|
+
- Each sub-task should be self-contained
|
|
21
|
+
- 2-5 sub-tasks maximum
|
|
22
|
+
- Keep sub-task descriptions clear and specific
|
|
23
|
+
- Use an empty subtasks array if the request cannot be meaningfully decomposed
|
|
24
|
+
|
|
25
|
+
User request: %{prompt}
|
|
26
|
+
PROMPT
|
|
27
|
+
|
|
28
|
+
# Appended to DECOMPOSITION_PROMPT when the model does not support
|
|
29
|
+
# structured output and with_schema cannot be used.
|
|
30
|
+
FALLBACK_JSON_INSTRUCTION = <<~INSTRUCTION
|
|
31
|
+
|
|
32
|
+
Respond with ONLY a JSON object — no explanation, no markdown, no prose:
|
|
33
|
+
{"subtasks": ["sub-task description 1", "sub-task description 2"]}
|
|
34
|
+
INSTRUCTION
|
|
35
|
+
|
|
36
|
+
# JSON schema for structured-output-capable models.
|
|
37
|
+
# Uses a wrapper object because Claude requires a top-level object.
|
|
38
|
+
SUBTASKS_SCHEMA = {
|
|
39
|
+
name: 'subtask_decomposition',
|
|
40
|
+
schema: {
|
|
41
|
+
type: 'object',
|
|
42
|
+
properties: {
|
|
43
|
+
subtasks: {
|
|
44
|
+
type: 'array',
|
|
45
|
+
items: { type: 'string' },
|
|
46
|
+
description: 'Independent sub-task descriptions. Empty array if not decomposable.'
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
required: ['subtasks'],
|
|
50
|
+
additionalProperties: false
|
|
51
|
+
},
|
|
52
|
+
strict: true
|
|
53
|
+
}.freeze
|
|
54
|
+
|
|
55
|
+
SYNTHESIS_TEMPLATE = <<~PROMPT
|
|
56
|
+
Original request: %{prompt}
|
|
57
|
+
|
|
58
|
+
Sub-task results:
|
|
59
|
+
%{results}
|
|
60
|
+
|
|
61
|
+
Synthesize these results into a coherent final response that fully addresses the original request.
|
|
62
|
+
PROMPT
|
|
63
|
+
|
|
64
|
+
def initialize(robot)
|
|
65
|
+
@robot = robot
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Attempt to decompose a prompt into sub-tasks.
|
|
69
|
+
#
|
|
70
|
+
# Uses a temporary probe robot so the decomposition prompt and response do
|
|
71
|
+
# not pollute the main conversation history. When the configured model
|
|
72
|
+
# supports structured output (structured_output? == true) with_schema is
|
|
73
|
+
# used to get a guaranteed Hash back. Otherwise a JSON instruction is
|
|
74
|
+
# appended to the prompt and the text response is parsed manually.
|
|
75
|
+
#
|
|
76
|
+
# @param prompt [String] the user's prompt
|
|
77
|
+
# @return [Array<String>] sub-task descriptions (empty if not decomposable)
|
|
78
|
+
def decompose(prompt)
|
|
79
|
+
probe = build_probe_robot
|
|
80
|
+
use_schema = structured_output?
|
|
81
|
+
|
|
82
|
+
if use_schema
|
|
83
|
+
probe.with_schema(SUBTASKS_SCHEMA)
|
|
84
|
+
full_prompt = DECOMPOSITION_PROMPT % { prompt: prompt }
|
|
85
|
+
else
|
|
86
|
+
AIA.logger.warn("PromptDecomposer: configured model does not support structured output — using JSON prompt fallback")
|
|
87
|
+
full_prompt = (DECOMPOSITION_PROMPT + FALLBACK_JSON_INSTRUCTION) % { prompt: prompt }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
result = probe.run(full_prompt, mcp: :none, tools: :none)
|
|
91
|
+
content = extract_content(result)
|
|
92
|
+
AIA.logger.debug("PromptDecomposer#decompose content class=#{content.class}")
|
|
93
|
+
subtasks = extract_subtasks(content)
|
|
94
|
+
subtasks.is_a?(Array) ? subtasks.select { |t| t.is_a?(String) && !t.empty? } : []
|
|
95
|
+
rescue StandardError => e
|
|
96
|
+
AIA.logger.warn("PromptDecomposer#decompose failed: #{e.class}: #{e.message}")
|
|
97
|
+
[]
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Synthesize sub-task results into a final response.
|
|
101
|
+
#
|
|
102
|
+
# @param prompt [String] the original user prompt
|
|
103
|
+
# @param results [Array<String>] results from each sub-task
|
|
104
|
+
# @return the synthesized response
|
|
105
|
+
def synthesize(prompt, results)
|
|
106
|
+
formatted_results = results.each_with_index.map do |r, i|
|
|
107
|
+
"Sub-task #{i + 1}:\n#{r}"
|
|
108
|
+
end.join("\n\n")
|
|
109
|
+
|
|
110
|
+
@robot.run(
|
|
111
|
+
SYNTHESIS_TEMPLATE % { prompt: prompt, results: formatted_results },
|
|
112
|
+
mcp: :none, tools: :none
|
|
113
|
+
)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
private
|
|
117
|
+
|
|
118
|
+
def structured_output?
|
|
119
|
+
RubyLLM.models.find(AIA.config.models.first.internal_id).structured_output?
|
|
120
|
+
rescue StandardError
|
|
121
|
+
false
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Build a fresh temporary robot for the decomposition probe.
|
|
125
|
+
# This robot has no conversation history and is discarded after use,
|
|
126
|
+
# keeping @robot's history clean for the normal or synthesis path.
|
|
127
|
+
def build_probe_robot
|
|
128
|
+
config = AIA.config
|
|
129
|
+
run_config = RobotFactory.build_run_config(config)
|
|
130
|
+
RobotLab.build(
|
|
131
|
+
name: "decompose-probe",
|
|
132
|
+
model: config.models.first.internal_id,
|
|
133
|
+
system_prompt: nil,
|
|
134
|
+
config: run_config
|
|
135
|
+
)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Extract the subtasks array from the probe response.
|
|
139
|
+
# Hash path: with_schema caused ruby_llm to auto-parse the JSON response.
|
|
140
|
+
# String path: fallback text parsing for models without structured output.
|
|
141
|
+
def extract_subtasks(content)
|
|
142
|
+
case content
|
|
143
|
+
when Hash
|
|
144
|
+
content['subtasks'] || content[:subtasks] || []
|
|
145
|
+
when Array
|
|
146
|
+
content
|
|
147
|
+
when String
|
|
148
|
+
parsed = JSON.parse(content.gsub(/```(?:json)?\s*/i, '').gsub('```', '').strip)
|
|
149
|
+
parsed.is_a?(Hash) ? parsed['subtasks'] || parsed[:subtasks] || [] : parsed
|
|
150
|
+
else
|
|
151
|
+
[]
|
|
152
|
+
end
|
|
153
|
+
rescue JSON::ParserError
|
|
154
|
+
[]
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
data/lib/aia/prompt_handler.rb
CHANGED
|
@@ -3,15 +3,14 @@
|
|
|
3
3
|
require 'pm'
|
|
4
4
|
require 'erb'
|
|
5
5
|
|
|
6
|
-
|
|
7
6
|
module AIA
|
|
8
7
|
class PromptHandler
|
|
9
8
|
include AIA::SkillUtils
|
|
10
9
|
|
|
11
10
|
# Struct for path-based role content (bypasses PM parsing)
|
|
12
11
|
RoleContent = Struct.new(:content) do
|
|
13
|
-
def to_s
|
|
14
|
-
def metadata
|
|
12
|
+
def to_s = content
|
|
13
|
+
def metadata = nil
|
|
15
14
|
end
|
|
16
15
|
|
|
17
16
|
# Root-level YAML keys that are shorthands for deeper config paths
|
|
@@ -25,7 +24,7 @@ module AIA
|
|
|
25
24
|
'next' => [%w[config next], %w[config pipeline]],
|
|
26
25
|
'pipeline' => [%w[config pipeline], %w[config next]],
|
|
27
26
|
'shell' => [%w[config shell], %w[config flags shell]],
|
|
28
|
-
'erb' => [%w[config erb], %w[config flags erb]]
|
|
27
|
+
'erb' => [%w[config erb], %w[config flags erb]]
|
|
29
28
|
}.freeze
|
|
30
29
|
|
|
31
30
|
def initialize
|
|
@@ -39,7 +38,6 @@ module AIA
|
|
|
39
38
|
register_pm_directives
|
|
40
39
|
end
|
|
41
40
|
|
|
42
|
-
|
|
43
41
|
def fetch_prompt(prompt_id)
|
|
44
42
|
if prompt_id == '__FUZZY_SEARCH__'
|
|
45
43
|
return fuzzy_search_prompt('')
|
|
@@ -54,7 +52,7 @@ module AIA
|
|
|
54
52
|
parsed = if File.exist?(prompt_file_path)
|
|
55
53
|
PM.parse(prompt_id)
|
|
56
54
|
else
|
|
57
|
-
|
|
55
|
+
$stderr.puts "Warning: Invalid prompt ID or file not found: #{prompt_id}"
|
|
58
56
|
logger.warn("Invalid prompt ID or file not found: #{prompt_id}")
|
|
59
57
|
handle_missing_prompt(prompt_id)
|
|
60
58
|
end
|
|
@@ -63,7 +61,6 @@ module AIA
|
|
|
63
61
|
parsed
|
|
64
62
|
end
|
|
65
63
|
|
|
66
|
-
|
|
67
64
|
def fetch_role(role_id)
|
|
68
65
|
return handle_missing_role("roles/") if role_id.nil?
|
|
69
66
|
return fetch_role_from_path(role_id) if path_based_id?(role_id)
|
|
@@ -79,7 +76,7 @@ module AIA
|
|
|
79
76
|
# and returns the ID string as content. Parse from raw file content instead.
|
|
80
77
|
PM.parse_string(File.read(role_file_path))
|
|
81
78
|
else
|
|
82
|
-
|
|
79
|
+
$stderr.puts "Warning: Invalid role ID or file not found: #{role_id}"
|
|
83
80
|
logger.warn("Invalid role ID or file not found: #{role_id}")
|
|
84
81
|
handle_missing_role(role_id)
|
|
85
82
|
end
|
|
@@ -88,7 +85,6 @@ module AIA
|
|
|
88
85
|
parsed
|
|
89
86
|
end
|
|
90
87
|
|
|
91
|
-
|
|
92
88
|
# Load role for a specific model (ADR-005)
|
|
93
89
|
# Takes a model spec hash and default role, returns rendered role text
|
|
94
90
|
def load_role_for_model(model_spec, default_role = nil)
|
|
@@ -103,12 +99,11 @@ module AIA
|
|
|
103
99
|
role_parsed = fetch_role(role_id)
|
|
104
100
|
role_parsed.to_s
|
|
105
101
|
rescue => e
|
|
106
|
-
|
|
102
|
+
$stderr.puts "Warning: Could not load role '#{role_id}' for model: #{e.message}"
|
|
107
103
|
logger.warn("Could not load role '#{role_id}' for model: #{e.message}")
|
|
108
104
|
nil
|
|
109
105
|
end
|
|
110
106
|
|
|
111
|
-
|
|
112
107
|
# Applies YAML front matter metadata to AIA.config.
|
|
113
108
|
# Processes root-level shorthand keys, detects conflicts with config: section,
|
|
114
109
|
# and deep merges the config: section into AIA.config.
|
|
@@ -132,10 +127,8 @@ module AIA
|
|
|
132
127
|
deep_merge_config(config_section) if config_section
|
|
133
128
|
end
|
|
134
129
|
|
|
135
|
-
|
|
136
130
|
private
|
|
137
131
|
|
|
138
|
-
|
|
139
132
|
def fetch_executable_prompt
|
|
140
133
|
content = AIA.config.executable_prompt_content
|
|
141
134
|
parsed = PM.parse(content)
|
|
@@ -143,18 +136,16 @@ module AIA
|
|
|
143
136
|
parsed
|
|
144
137
|
end
|
|
145
138
|
|
|
146
|
-
|
|
147
139
|
def register_pm_directives
|
|
148
140
|
AIA::Directive.register_all
|
|
149
141
|
end
|
|
150
142
|
|
|
151
|
-
|
|
152
143
|
# Detect conflicts between root shorthand keys and the config: section.
|
|
153
144
|
# Raises AIA::ConfigurationError when the same setting is specified in both places.
|
|
154
145
|
def detect_shorthand_conflicts(meta_hash, config_section)
|
|
155
146
|
# Check for mutually exclusive root keys: next and pipeline
|
|
156
|
-
has_next = meta_hash.key?('next')
|
|
157
|
-
has_pipeline = meta_hash.key?('pipeline')
|
|
147
|
+
has_next = meta_hash.key?('next') || meta_hash.key?(:next)
|
|
148
|
+
has_pipeline = meta_hash.key?('pipeline') || meta_hash.key?(:pipeline)
|
|
158
149
|
|
|
159
150
|
if has_next && has_pipeline
|
|
160
151
|
raise ConfigurationError, "Both 'next' and 'pipeline' specified at root level — they are mutually exclusive"
|
|
@@ -173,14 +164,14 @@ module AIA
|
|
|
173
164
|
|
|
174
165
|
unless value.nil?
|
|
175
166
|
raise ConfigurationError,
|
|
176
|
-
|
|
167
|
+
"Conflict: '#{root_key}' at root level and '#{path.join('.')}' in config section"
|
|
177
168
|
end
|
|
178
169
|
end
|
|
179
170
|
end
|
|
180
171
|
end
|
|
181
172
|
|
|
182
|
-
|
|
183
173
|
# Apply root-level shorthand keys to AIA.config
|
|
174
|
+
# rubocop:disable Metrics/AbcSize
|
|
184
175
|
def apply_root_shorthands(meta_hash)
|
|
185
176
|
# model → AIA.config.models (replace with single-model array)
|
|
186
177
|
model_val = meta_hash['model'] || meta_hash[:model]
|
|
@@ -226,11 +217,10 @@ module AIA
|
|
|
226
217
|
|
|
227
218
|
# erb → AIA.config.flags.erb (and PM's erb via metadata)
|
|
228
219
|
erb_val = meta_hash['erb'] || meta_hash[:erb]
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
end
|
|
220
|
+
return if erb_val.nil?
|
|
221
|
+
AIA.config.flags.erb = erb_val
|
|
232
222
|
end
|
|
233
|
-
|
|
223
|
+
# rubocop:enable Metrics/AbcSize
|
|
234
224
|
|
|
235
225
|
def logger
|
|
236
226
|
@logger ||= LoggerManager.aia_logger
|
|
@@ -253,7 +243,6 @@ module AIA
|
|
|
253
243
|
end
|
|
254
244
|
end
|
|
255
245
|
|
|
256
|
-
|
|
257
246
|
# Recursively merge a hash into a config object
|
|
258
247
|
def deep_merge_into_config(config_obj, hash)
|
|
259
248
|
hash.each do |key, value|
|
|
@@ -266,7 +255,6 @@ module AIA
|
|
|
266
255
|
end
|
|
267
256
|
end
|
|
268
257
|
|
|
269
|
-
|
|
270
258
|
# Dig into a hash with an array of keys, returning nil if any key is missing
|
|
271
259
|
def dig_hash(hash, keys)
|
|
272
260
|
keys.reduce(hash) do |h, key|
|
|
@@ -275,7 +263,6 @@ module AIA
|
|
|
275
263
|
end
|
|
276
264
|
end
|
|
277
265
|
|
|
278
|
-
|
|
279
266
|
# Recursively symbolize all keys in a hash
|
|
280
267
|
def symbolize_keys_deep(obj)
|
|
281
268
|
case obj
|
|
@@ -290,23 +277,21 @@ module AIA
|
|
|
290
277
|
end
|
|
291
278
|
end
|
|
292
279
|
|
|
293
|
-
|
|
294
280
|
def handle_missing_prompt(prompt_id)
|
|
295
281
|
prompt_id = prompt_id.to_s.strip
|
|
296
282
|
if prompt_id.empty?
|
|
297
|
-
|
|
283
|
+
$stderr.puts "Error: Prompt ID cannot be empty"
|
|
298
284
|
exit 1
|
|
299
285
|
end
|
|
300
286
|
|
|
301
287
|
if AIA.config.flags.fuzzy
|
|
302
288
|
fuzzy_search_prompt(prompt_id)
|
|
303
289
|
else
|
|
304
|
-
|
|
290
|
+
$stderr.puts "Error: Could not find prompt with ID: #{prompt_id}"
|
|
305
291
|
exit 1
|
|
306
292
|
end
|
|
307
293
|
end
|
|
308
294
|
|
|
309
|
-
|
|
310
295
|
def fetch_role_from_path(role_id)
|
|
311
296
|
expanded = File.expand_path(role_id)
|
|
312
297
|
expanded += '.md' if File.extname(expanded).empty?
|
|
@@ -320,7 +305,6 @@ module AIA
|
|
|
320
305
|
RoleContent.new(File.read(expanded))
|
|
321
306
|
end
|
|
322
307
|
|
|
323
|
-
|
|
324
308
|
def fuzzy_search_prompt(prompt_id)
|
|
325
309
|
new_prompt_id = search_prompt_id_with_fzf(prompt_id)
|
|
326
310
|
|
|
@@ -331,23 +315,21 @@ module AIA
|
|
|
331
315
|
PM.parse(new_prompt_id)
|
|
332
316
|
end
|
|
333
317
|
|
|
334
|
-
|
|
335
318
|
def handle_missing_role(role_id)
|
|
336
319
|
role_id = role_id.to_s.strip
|
|
337
320
|
if role_id.empty? || role_id == "roles/"
|
|
338
|
-
|
|
321
|
+
$stderr.puts "Error: Role ID cannot be empty"
|
|
339
322
|
exit 1
|
|
340
323
|
end
|
|
341
324
|
|
|
342
325
|
if AIA.config.flags.fuzzy
|
|
343
326
|
fuzzy_search_role(role_id)
|
|
344
327
|
else
|
|
345
|
-
|
|
328
|
+
$stderr.puts "Error: Could not find role with ID: #{role_id}"
|
|
346
329
|
exit 1
|
|
347
330
|
end
|
|
348
331
|
end
|
|
349
332
|
|
|
350
|
-
|
|
351
333
|
def fuzzy_search_role(role_id)
|
|
352
334
|
new_role_id = search_role_id_with_fzf(role_id)
|
|
353
335
|
if new_role_id.nil? || new_role_id.empty?
|
|
@@ -357,10 +339,9 @@ module AIA
|
|
|
357
339
|
PM.parse(new_role_id)
|
|
358
340
|
end
|
|
359
341
|
|
|
360
|
-
|
|
361
342
|
def search_prompt_id_with_fzf(initial_query)
|
|
362
343
|
prompt_files = Dir.glob(File.join(@prompts_dir, "*#{AIA.config.prompts.extname}"))
|
|
363
|
-
|
|
344
|
+
.map { |file| File.basename(file, AIA.config.prompts.extname) }
|
|
364
345
|
fzf = AIA::Fzf.new(
|
|
365
346
|
list: prompt_files,
|
|
366
347
|
directory: @prompts_dir,
|
|
@@ -371,10 +352,9 @@ module AIA
|
|
|
371
352
|
fzf.run || (raise "No prompt ID selected")
|
|
372
353
|
end
|
|
373
354
|
|
|
374
|
-
|
|
375
355
|
def search_role_id_with_fzf(initial_query)
|
|
376
356
|
role_files = Dir.glob(File.join(@roles_dir, "*#{AIA.config.prompts.extname}"))
|
|
377
|
-
|
|
357
|
+
.map { |file| File.basename(file, AIA.config.prompts.extname) }
|
|
378
358
|
fzf = AIA::Fzf.new(
|
|
379
359
|
list: role_files,
|
|
380
360
|
directory: @prompts_dir,
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# lib/aia/robot_builder.rb
|
|
4
|
+
#
|
|
5
|
+
# Builds a single RobotLab::Robot from AIA configuration.
|
|
6
|
+
# Extracted from RobotFactory to isolate single-robot construction.
|
|
7
|
+
|
|
8
|
+
module AIA
|
|
9
|
+
class RobotBuilder
|
|
10
|
+
class << self
|
|
11
|
+
# Build a single robot for one model spec.
|
|
12
|
+
#
|
|
13
|
+
# @param config [AIA::Config]
|
|
14
|
+
# @param namer [RobotNamer]
|
|
15
|
+
# @return [RobotLab::Robot]
|
|
16
|
+
def build(config, namer:)
|
|
17
|
+
model_spec = config.models.first
|
|
18
|
+
robot_name = namer.name_for(model_spec.name)
|
|
19
|
+
roster = [{ name: robot_name, spec: model_spec }]
|
|
20
|
+
identity = SystemPromptAssembler.build_identity_prompt(robot_name, model_spec, roster)
|
|
21
|
+
base_prompt = SystemPromptAssembler.resolve_system_prompt(config, model_spec)
|
|
22
|
+
thinking_note = thinking_mode_instruction(model_spec)
|
|
23
|
+
system_prompt = [identity, base_prompt, thinking_note].compact.join("\n\n")
|
|
24
|
+
|
|
25
|
+
RobotFactory.build_robot(
|
|
26
|
+
model_spec,
|
|
27
|
+
name: robot_name,
|
|
28
|
+
system_prompt: system_prompt,
|
|
29
|
+
local_tools: ToolLoader.filtered_tools(config),
|
|
30
|
+
mcp_servers: RobotFactory.mcp_server_configs(config),
|
|
31
|
+
on_content: nil,
|
|
32
|
+
config: RobotFactory.build_run_config(config)
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
# Local thinking-mode models (e.g. qwen3 on Ollama) sometimes generate
|
|
39
|
+
# reasoning content but leave the response section empty, causing the user
|
|
40
|
+
# to receive only raw thinking text. This instruction nudges the model to
|
|
41
|
+
# always emit a plain-text answer after its reasoning.
|
|
42
|
+
def thinking_mode_instruction(model_spec)
|
|
43
|
+
return nil unless model_spec&.local_provider?
|
|
44
|
+
|
|
45
|
+
"After completing your reasoning, always write a clear, complete response " \
|
|
46
|
+
"in plain text. Never leave your response empty after thinking — your " \
|
|
47
|
+
"answer must appear outside the thinking block."
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|