aia 1.1.1 → 2.0.0.0.pre.beta2
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 +9 -1
- data/.loki +11 -0
- data/.reek.yml +160 -0
- data/.rubocop.yml +116 -0
- data/.rubocop_strict.yml +15 -0
- data/.version +1 -1
- data/CHANGELOG.md +269 -55
- data/IMPLEMENTATION_PLAN.md +506 -0
- data/README.md +267 -239
- data/Rakefile +5 -5
- data/_typos.toml +10 -0
- data/aia.gemspec +92 -0
- data/architecture_review.md +314 -0
- data/bin/aia +16 -0
- data/config/aia.yml +13 -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 +263 -167
- data/lib/aia/config/cli_parser.rb +217 -145
- data/lib/aia/config/defaults.yml +62 -33
- data/lib/aia/config/mcp_parser.rb +52 -51
- data/lib/aia/config/model_spec.rb +34 -2
- data/lib/aia/config/validator.rb +171 -216
- data/lib/aia/config.rb +111 -145
- data/lib/aia/content_extractor.rb +155 -0
- data/lib/aia/cost_calculator.rb +39 -0
- data/lib/aia/crew.rb +164 -0
- data/lib/aia/debate_handler.rb +174 -0
- data/lib/aia/delegate_handler.rb +116 -0
- data/lib/aia/directive.rb +43 -26
- data/lib/aia/directive_processor.rb +16 -7
- data/lib/aia/directives/configuration_directives.rb +214 -60
- data/lib/aia/directives/context_directives.rb +67 -52
- data/lib/aia/directives/execution_directives.rb +141 -4
- data/lib/aia/directives/model_directives.rb +163 -141
- data/lib/aia/directives/trakflow_directives.rb +62 -0
- data/lib/aia/directives/utility_directives.rb +227 -30
- data/lib/aia/directives/web_and_file_directives.rb +126 -77
- 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 +471 -0
- data/lib/aia/logger.rb +45 -25
- data/lib/aia/mcp_config_normalizer.rb +35 -0
- data/lib/aia/mcp_connection_manager.rb +315 -0
- data/lib/aia/mcp_discovery.rb +44 -0
- data/lib/aia/mcp_grouper.rb +33 -0
- data/lib/aia/mcp_server_config.rb +30 -0
- data/lib/aia/mcp_utility.rb +60 -0
- data/lib/aia/mention_router.rb +217 -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 +160 -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 +272 -0
- data/lib/aia/plugin_loader.rb +170 -0
- data/lib/aia/plugin_monitor.rb +211 -0
- data/lib/aia/prompt_decomposer.rb +159 -0
- data/lib/aia/prompt_handler.rb +56 -82
- data/lib/aia/robot_builder.rb +51 -0
- data/lib/aia/robot_factory.rb +338 -0
- data/lib/aia/robot_namer.rb +110 -0
- data/lib/aia/session.rb +87 -17
- data/lib/aia/session_tracker.rb +207 -0
- data/lib/aia/similarity_scorer.rb +41 -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 +322 -0
- data/lib/aia/speech.rb +67 -0
- data/lib/aia/startup_coordinator.rb +151 -0
- data/lib/aia/streaming_runner.rb +172 -0
- data/lib/aia/system_prompt_assembler.rb +92 -0
- data/lib/aia/task_coordinator.rb +207 -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/timing.rb +15 -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 +83 -0
- data/lib/aia/tool_filter_registry.rb +30 -0
- data/lib/aia/tool_filter_strategy.rb +146 -0
- data/lib/aia/tool_introspection.rb +17 -0
- data/lib/aia/tool_loader.rb +216 -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 +115 -0
- data/lib/aia/trakflow_bridge.rb +175 -0
- data/lib/aia/turn_state.rb +95 -0
- data/lib/aia/ui_presenter.rb +182 -206
- data/lib/aia/utility.rb +136 -87
- data/lib/aia/{history_manager.rb → variable_input_collector.rb} +9 -9
- data/lib/aia/verification_network.rb +57 -0
- data/lib/aia.rb +124 -63
- data/mkdocs.yml +1 -0
- metadata +187 -58
- data/justfile +0 -215
- data/lib/aia/adapter/chat_execution.rb +0 -242
- data/lib/aia/adapter/error_handler.rb +0 -68
- data/lib/aia/adapter/gem_activator.rb +0 -57
- data/lib/aia/adapter/mcp_connector.rb +0 -274
- data/lib/aia/adapter/modality_handlers.rb +0 -167
- data/lib/aia/adapter/model_registry.rb +0 -81
- data/lib/aia/adapter/multi_model_chat.rb +0 -218
- data/lib/aia/adapter/provider_configurator.rb +0 -59
- data/lib/aia/adapter/tool_filter.rb +0 -85
- data/lib/aia/adapter/tool_loader.rb +0 -90
- data/lib/aia/chat_processor_service.rb +0 -178
- data/lib/aia/prompt_pipeline.rb +0 -183
- data/lib/aia/ruby_llm_adapter.rb +0 -95
- data/lib/extensions/openstruct_merge.rb +0 -48
- data/lib/extensions/ruby_llm/.irbrc +0 -56
- data/lib/extensions/ruby_llm/modalities.rb +0 -36
- data/lib/extensions/ruby_llm/provider_fix.rb +0 -79
- data/lib/refinements/string.rb +0 -16
- data/main.just +0 -76
data/lib/aia/prompt_handler.rb
CHANGED
|
@@ -3,15 +3,15 @@
|
|
|
3
3
|
require 'pm'
|
|
4
4
|
require 'erb'
|
|
5
5
|
|
|
6
|
-
|
|
7
6
|
module AIA
|
|
7
|
+
# :reek:TooManyMethods -- prompt/role fetching, front-matter merging, and fzf search form one lookup pipeline of small named steps
|
|
8
8
|
class PromptHandler
|
|
9
9
|
include AIA::SkillUtils
|
|
10
10
|
|
|
11
11
|
# Struct for path-based role content (bypasses PM parsing)
|
|
12
12
|
RoleContent = Struct.new(:content) do
|
|
13
|
-
def to_s
|
|
14
|
-
def metadata
|
|
13
|
+
def to_s = content
|
|
14
|
+
def metadata = nil
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
# Root-level YAML keys that are shorthands for deeper config paths
|
|
@@ -25,7 +25,7 @@ module AIA
|
|
|
25
25
|
'next' => [%w[config next], %w[config pipeline]],
|
|
26
26
|
'pipeline' => [%w[config pipeline], %w[config next]],
|
|
27
27
|
'shell' => [%w[config shell], %w[config flags shell]],
|
|
28
|
-
'erb' => [%w[config erb], %w[config flags erb]]
|
|
28
|
+
'erb' => [%w[config erb], %w[config flags erb]]
|
|
29
29
|
}.freeze
|
|
30
30
|
|
|
31
31
|
def initialize
|
|
@@ -39,7 +39,6 @@ module AIA
|
|
|
39
39
|
register_pm_directives
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
-
|
|
43
42
|
def fetch_prompt(prompt_id)
|
|
44
43
|
if prompt_id == '__FUZZY_SEARCH__'
|
|
45
44
|
return fuzzy_search_prompt('')
|
|
@@ -54,7 +53,7 @@ module AIA
|
|
|
54
53
|
parsed = if File.exist?(prompt_file_path)
|
|
55
54
|
PM.parse(prompt_id)
|
|
56
55
|
else
|
|
57
|
-
|
|
56
|
+
$stderr.puts "Warning: Invalid prompt ID or file not found: #{prompt_id}"
|
|
58
57
|
logger.warn("Invalid prompt ID or file not found: #{prompt_id}")
|
|
59
58
|
handle_missing_prompt(prompt_id)
|
|
60
59
|
end
|
|
@@ -63,23 +62,23 @@ module AIA
|
|
|
63
62
|
parsed
|
|
64
63
|
end
|
|
65
64
|
|
|
66
|
-
|
|
67
65
|
def fetch_role(role_id)
|
|
68
66
|
return handle_missing_role("roles/") if role_id.nil?
|
|
69
67
|
return fetch_role_from_path(role_id) if path_based_id?(role_id)
|
|
70
68
|
|
|
71
|
-
|
|
72
|
-
|
|
69
|
+
prompts_cfg = AIA.config.prompts
|
|
70
|
+
unless role_id.start_with?(prompts_cfg.roles_prefix)
|
|
71
|
+
role_id = "#{prompts_cfg.roles_prefix}/#{role_id}"
|
|
73
72
|
end
|
|
74
73
|
|
|
75
|
-
role_file_path = File.join(@prompts_dir, "#{role_id}#{
|
|
74
|
+
role_file_path = File.join(@prompts_dir, "#{role_id}#{prompts_cfg.extname}")
|
|
76
75
|
|
|
77
76
|
parsed = if File.exist?(role_file_path)
|
|
78
77
|
# PM.parse(role_id) cannot resolve subdirectory IDs like "roles/jersey_mike"
|
|
79
78
|
# and returns the ID string as content. Parse from raw file content instead.
|
|
80
79
|
PM.parse_string(File.read(role_file_path))
|
|
81
80
|
else
|
|
82
|
-
|
|
81
|
+
$stderr.puts "Warning: Invalid role ID or file not found: #{role_id}"
|
|
83
82
|
logger.warn("Invalid role ID or file not found: #{role_id}")
|
|
84
83
|
handle_missing_role(role_id)
|
|
85
84
|
end
|
|
@@ -88,7 +87,6 @@ module AIA
|
|
|
88
87
|
parsed
|
|
89
88
|
end
|
|
90
89
|
|
|
91
|
-
|
|
92
90
|
# Load role for a specific model (ADR-005)
|
|
93
91
|
# Takes a model spec hash and default role, returns rendered role text
|
|
94
92
|
def load_role_for_model(model_spec, default_role = nil)
|
|
@@ -103,12 +101,11 @@ module AIA
|
|
|
103
101
|
role_parsed = fetch_role(role_id)
|
|
104
102
|
role_parsed.to_s
|
|
105
103
|
rescue => e
|
|
106
|
-
|
|
104
|
+
$stderr.puts "Warning: Could not load role '#{role_id}' for model: #{e.message}"
|
|
107
105
|
logger.warn("Could not load role '#{role_id}' for model: #{e.message}")
|
|
108
106
|
nil
|
|
109
107
|
end
|
|
110
108
|
|
|
111
|
-
|
|
112
109
|
# Applies YAML front matter metadata to AIA.config.
|
|
113
110
|
# Processes root-level shorthand keys, detects conflicts with config: section,
|
|
114
111
|
# and deep merges the config: section into AIA.config.
|
|
@@ -132,10 +129,8 @@ module AIA
|
|
|
132
129
|
deep_merge_config(config_section) if config_section
|
|
133
130
|
end
|
|
134
131
|
|
|
135
|
-
|
|
136
132
|
private
|
|
137
133
|
|
|
138
|
-
|
|
139
134
|
def fetch_executable_prompt
|
|
140
135
|
content = AIA.config.executable_prompt_content
|
|
141
136
|
parsed = PM.parse(content)
|
|
@@ -143,18 +138,16 @@ module AIA
|
|
|
143
138
|
parsed
|
|
144
139
|
end
|
|
145
140
|
|
|
146
|
-
|
|
147
141
|
def register_pm_directives
|
|
148
142
|
AIA::Directive.register_all
|
|
149
143
|
end
|
|
150
144
|
|
|
151
|
-
|
|
152
145
|
# Detect conflicts between root shorthand keys and the config: section.
|
|
153
146
|
# Raises AIA::ConfigurationError when the same setting is specified in both places.
|
|
154
147
|
def detect_shorthand_conflicts(meta_hash, config_section)
|
|
155
148
|
# Check for mutually exclusive root keys: next and pipeline
|
|
156
|
-
has_next = meta_hash.key?('next')
|
|
157
|
-
has_pipeline = meta_hash.key?('pipeline')
|
|
149
|
+
has_next = meta_hash.key?('next') || meta_hash.key?(:next)
|
|
150
|
+
has_pipeline = meta_hash.key?('pipeline') || meta_hash.key?(:pipeline)
|
|
158
151
|
|
|
159
152
|
if has_next && has_pipeline
|
|
160
153
|
raise ConfigurationError, "Both 'next' and 'pipeline' specified at root level — they are mutually exclusive"
|
|
@@ -173,64 +166,54 @@ module AIA
|
|
|
173
166
|
|
|
174
167
|
unless value.nil?
|
|
175
168
|
raise ConfigurationError,
|
|
176
|
-
|
|
169
|
+
"Conflict: '#{root_key}' at root level and '#{path.join('.')}' in config section"
|
|
177
170
|
end
|
|
178
171
|
end
|
|
179
172
|
end
|
|
180
173
|
end
|
|
181
174
|
|
|
182
|
-
|
|
183
175
|
# Apply root-level shorthand keys to AIA.config
|
|
176
|
+
# :reek:TooManyStatements -- one guarded assignment per supported front-matter shorthand key
|
|
184
177
|
def apply_root_shorthands(meta_hash)
|
|
185
|
-
|
|
186
|
-
model_val = meta_hash['model'] || meta_hash[:model]
|
|
187
|
-
if model_val
|
|
188
|
-
AIA.config.models = [model_val]
|
|
189
|
-
end
|
|
178
|
+
cfg = AIA.config
|
|
190
179
|
|
|
191
|
-
#
|
|
192
|
-
|
|
193
|
-
if
|
|
194
|
-
AIA.config.llm.temperature = temp_val
|
|
195
|
-
end
|
|
180
|
+
# model → cfg.models (replace with single-model array)
|
|
181
|
+
model_val = shorthand(meta_hash, :model)
|
|
182
|
+
cfg.models = [model_val] if model_val
|
|
196
183
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
if top_p_val
|
|
200
|
-
AIA.config.llm.top_p = top_p_val
|
|
201
|
-
end
|
|
184
|
+
temp_val = shorthand(meta_hash, :temperature)
|
|
185
|
+
cfg.llm.temperature = temp_val if temp_val
|
|
202
186
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
if next_val
|
|
206
|
-
if AIA.config.pipeline.any?
|
|
207
|
-
logger.info "Prompt metadata 'next: #{next_val}' overrides remaining pipeline #{AIA.config.pipeline.inspect}"
|
|
208
|
-
end
|
|
209
|
-
AIA.config.pipeline = [next_val]
|
|
210
|
-
end
|
|
187
|
+
top_p_val = shorthand(meta_hash, :top_p)
|
|
188
|
+
cfg.llm.top_p = top_p_val if top_p_val
|
|
211
189
|
|
|
212
|
-
# pipeline →
|
|
213
|
-
|
|
214
|
-
if
|
|
215
|
-
if AIA.config.pipeline.any?
|
|
216
|
-
logger.info "Prompt metadata 'pipeline' overrides remaining pipeline #{AIA.config.pipeline.inspect}"
|
|
217
|
-
end
|
|
218
|
-
AIA.config.pipeline = Array(pipeline_val)
|
|
219
|
-
end
|
|
190
|
+
# next / pipeline → cfg.pipeline (replace)
|
|
191
|
+
next_val = shorthand(meta_hash, :next)
|
|
192
|
+
apply_pipeline_shorthand(cfg, [next_val], "next: #{next_val}") if next_val
|
|
220
193
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
unless shell_val.nil?
|
|
224
|
-
AIA.config.flags.shell = shell_val
|
|
225
|
-
end
|
|
194
|
+
pipeline_val = shorthand(meta_hash, :pipeline)
|
|
195
|
+
apply_pipeline_shorthand(cfg, Array(pipeline_val), "pipeline") if pipeline_val
|
|
226
196
|
|
|
227
|
-
# erb →
|
|
228
|
-
|
|
229
|
-
unless
|
|
230
|
-
|
|
231
|
-
|
|
197
|
+
# shell / erb → cfg.flags (and PM's shell/erb via metadata)
|
|
198
|
+
shell_val = shorthand(meta_hash, :shell)
|
|
199
|
+
cfg.flags.shell = shell_val unless shell_val.nil?
|
|
200
|
+
|
|
201
|
+
erb_val = shorthand(meta_hash, :erb)
|
|
202
|
+
cfg.flags.erb = erb_val unless erb_val.nil?
|
|
232
203
|
end
|
|
233
204
|
|
|
205
|
+
# Fetch a front-matter shorthand value by string or symbol key.
|
|
206
|
+
def shorthand(meta_hash, key)
|
|
207
|
+
meta_hash[key.to_s] || meta_hash[key]
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Replace cfg.pipeline, logging when a remaining pipeline is overridden.
|
|
211
|
+
def apply_pipeline_shorthand(cfg, new_pipeline, label)
|
|
212
|
+
if cfg.pipeline.any?
|
|
213
|
+
logger.info "Prompt metadata '#{label}' overrides remaining pipeline #{cfg.pipeline.inspect}"
|
|
214
|
+
end
|
|
215
|
+
cfg.pipeline = new_pipeline
|
|
216
|
+
end
|
|
234
217
|
|
|
235
218
|
def logger
|
|
236
219
|
@logger ||= LoggerManager.aia_logger
|
|
@@ -253,7 +236,6 @@ module AIA
|
|
|
253
236
|
end
|
|
254
237
|
end
|
|
255
238
|
|
|
256
|
-
|
|
257
239
|
# Recursively merge a hash into a config object
|
|
258
240
|
def deep_merge_into_config(config_obj, hash)
|
|
259
241
|
hash.each do |key, value|
|
|
@@ -266,7 +248,6 @@ module AIA
|
|
|
266
248
|
end
|
|
267
249
|
end
|
|
268
250
|
|
|
269
|
-
|
|
270
251
|
# Dig into a hash with an array of keys, returning nil if any key is missing
|
|
271
252
|
def dig_hash(hash, keys)
|
|
272
253
|
keys.reduce(hash) do |h, key|
|
|
@@ -275,7 +256,6 @@ module AIA
|
|
|
275
256
|
end
|
|
276
257
|
end
|
|
277
258
|
|
|
278
|
-
|
|
279
259
|
# Recursively symbolize all keys in a hash
|
|
280
260
|
def symbolize_keys_deep(obj)
|
|
281
261
|
case obj
|
|
@@ -290,23 +270,21 @@ module AIA
|
|
|
290
270
|
end
|
|
291
271
|
end
|
|
292
272
|
|
|
293
|
-
|
|
294
273
|
def handle_missing_prompt(prompt_id)
|
|
295
274
|
prompt_id = prompt_id.to_s.strip
|
|
296
275
|
if prompt_id.empty?
|
|
297
|
-
|
|
276
|
+
$stderr.puts "Error: Prompt ID cannot be empty"
|
|
298
277
|
exit 1
|
|
299
278
|
end
|
|
300
279
|
|
|
301
280
|
if AIA.config.flags.fuzzy
|
|
302
281
|
fuzzy_search_prompt(prompt_id)
|
|
303
282
|
else
|
|
304
|
-
|
|
283
|
+
$stderr.puts "Error: Could not find prompt with ID: #{prompt_id}"
|
|
305
284
|
exit 1
|
|
306
285
|
end
|
|
307
286
|
end
|
|
308
287
|
|
|
309
|
-
|
|
310
288
|
def fetch_role_from_path(role_id)
|
|
311
289
|
expanded = File.expand_path(role_id)
|
|
312
290
|
expanded += '.md' if File.extname(expanded).empty?
|
|
@@ -320,7 +298,6 @@ module AIA
|
|
|
320
298
|
RoleContent.new(File.read(expanded))
|
|
321
299
|
end
|
|
322
300
|
|
|
323
|
-
|
|
324
301
|
def fuzzy_search_prompt(prompt_id)
|
|
325
302
|
new_prompt_id = search_prompt_id_with_fzf(prompt_id)
|
|
326
303
|
|
|
@@ -331,23 +308,21 @@ module AIA
|
|
|
331
308
|
PM.parse(new_prompt_id)
|
|
332
309
|
end
|
|
333
310
|
|
|
334
|
-
|
|
335
311
|
def handle_missing_role(role_id)
|
|
336
312
|
role_id = role_id.to_s.strip
|
|
337
313
|
if role_id.empty? || role_id == "roles/"
|
|
338
|
-
|
|
314
|
+
$stderr.puts "Error: Role ID cannot be empty"
|
|
339
315
|
exit 1
|
|
340
316
|
end
|
|
341
317
|
|
|
342
318
|
if AIA.config.flags.fuzzy
|
|
343
319
|
fuzzy_search_role(role_id)
|
|
344
320
|
else
|
|
345
|
-
|
|
321
|
+
$stderr.puts "Error: Could not find role with ID: #{role_id}"
|
|
346
322
|
exit 1
|
|
347
323
|
end
|
|
348
324
|
end
|
|
349
325
|
|
|
350
|
-
|
|
351
326
|
def fuzzy_search_role(role_id)
|
|
352
327
|
new_role_id = search_role_id_with_fzf(role_id)
|
|
353
328
|
if new_role_id.nil? || new_role_id.empty?
|
|
@@ -357,10 +332,9 @@ module AIA
|
|
|
357
332
|
PM.parse(new_role_id)
|
|
358
333
|
end
|
|
359
334
|
|
|
360
|
-
|
|
361
335
|
def search_prompt_id_with_fzf(initial_query)
|
|
362
336
|
prompt_files = Dir.glob(File.join(@prompts_dir, "*#{AIA.config.prompts.extname}"))
|
|
363
|
-
|
|
337
|
+
.map { |file| File.basename(file, AIA.config.prompts.extname) }
|
|
364
338
|
fzf = AIA::Fzf.new(
|
|
365
339
|
list: prompt_files,
|
|
366
340
|
directory: @prompts_dir,
|
|
@@ -371,10 +345,10 @@ module AIA
|
|
|
371
345
|
fzf.run || (raise "No prompt ID selected")
|
|
372
346
|
end
|
|
373
347
|
|
|
374
|
-
|
|
375
348
|
def search_role_id_with_fzf(initial_query)
|
|
376
|
-
|
|
377
|
-
|
|
349
|
+
prompts_cfg = AIA.config.prompts
|
|
350
|
+
role_files = Dir.glob(File.join(@roles_dir, "*#{prompts_cfg.extname}"))
|
|
351
|
+
.map { |file| File.basename(file, prompts_cfg.extname) }
|
|
378
352
|
fzf = AIA::Fzf.new(
|
|
379
353
|
list: role_files,
|
|
380
354
|
directory: @prompts_dir,
|
|
@@ -389,8 +363,8 @@ module AIA
|
|
|
389
363
|
raise "No role ID selected"
|
|
390
364
|
end
|
|
391
365
|
|
|
392
|
-
unless role.start_with?(
|
|
393
|
-
role =
|
|
366
|
+
unless role.start_with?(prompts_cfg.roles_prefix)
|
|
367
|
+
role = prompts_cfg.roles_prefix + '/' + role
|
|
394
368
|
end
|
|
395
369
|
|
|
396
370
|
role
|
|
@@ -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
|