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
data/lib/aia/config.rb
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
# 5. Embedded directives (/config)
|
|
19
19
|
#
|
|
20
20
|
# When -c / --config-file is used, it REPLACES sources 2 and 3:
|
|
21
|
-
# defaults
|
|
21
|
+
# defaults -> config file -> CLI arguments -> embedded directives
|
|
22
22
|
|
|
23
23
|
require 'myway_config'
|
|
24
24
|
require 'yaml'
|
|
@@ -28,7 +28,7 @@ require_relative 'config/model_spec'
|
|
|
28
28
|
require_relative 'config/mcp_parser'
|
|
29
29
|
|
|
30
30
|
module AIA
|
|
31
|
-
# Backward compatibility alias
|
|
31
|
+
# Backward compatibility alias
|
|
32
32
|
ConfigSection = MywayConfig::ConfigSection
|
|
33
33
|
|
|
34
34
|
class Config < MywayConfig::Base
|
|
@@ -36,7 +36,7 @@ module AIA
|
|
|
36
36
|
env_prefix :aia
|
|
37
37
|
defaults_path File.expand_path('config/defaults.yml', __dir__)
|
|
38
38
|
|
|
39
|
-
# AIA is a CLI tool, not a Rails app
|
|
39
|
+
# AIA is a CLI tool, not a Rails app -- no environment sections needed.
|
|
40
40
|
class << self
|
|
41
41
|
def validate_environment!
|
|
42
42
|
# no-op: AIA has no environment sections in defaults.yml
|
|
@@ -49,7 +49,8 @@ module AIA
|
|
|
49
49
|
|
|
50
50
|
# Nested section attributes (defined as hashes, converted to ConfigSection)
|
|
51
51
|
attr_config :service, :llm, :prompts, :roles, :skills, :output, :audio, :image, :embedding,
|
|
52
|
-
:tools, :flags, :registry, :paths, :logger
|
|
52
|
+
:tools, :flags, :registry, :paths, :logger, :rules, :concurrency,
|
|
53
|
+
:tool_filter
|
|
53
54
|
|
|
54
55
|
# Array/collection attributes
|
|
55
56
|
attr_config :models, :pipeline, :require_libs, :mcp_servers, :mcp_use, :mcp_skip, :context_files
|
|
@@ -58,17 +59,18 @@ module AIA
|
|
|
58
59
|
attr_accessor :prompt_id, :stdin_content, :remaining_args, :dump_file,
|
|
59
60
|
:completion, :mcp_list, :list_tools, :list_skills,
|
|
60
61
|
:executable_prompt_content,
|
|
61
|
-
:tool_names, :loaded_tools,
|
|
62
|
+
:tool_names, :loaded_tools, :loaded_plugins,
|
|
62
63
|
:log_level_override, :log_file_override,
|
|
63
|
-
:connected_mcp_servers,
|
|
64
|
-
:
|
|
64
|
+
:connected_mcp_servers, # Array of successfully connected MCP server names
|
|
65
|
+
:mcp_server_tool_counts, # Hash of name => tool_count for connected MCP servers
|
|
66
|
+
:failed_mcp_servers # Array of {name:, error:} hashes for failed MCP servers
|
|
65
67
|
|
|
66
68
|
# ==========================================================================
|
|
67
69
|
# Type Coercion
|
|
68
70
|
# ==========================================================================
|
|
69
71
|
|
|
70
72
|
# Convert array of hashes to array of ModelSpec objects
|
|
71
|
-
TO_MODEL_SPECS =
|
|
73
|
+
TO_MODEL_SPECS = lambda { |v|
|
|
72
74
|
return [] if v.nil?
|
|
73
75
|
return v if v.is_a?(Array) && v.first.is_a?(ModelSpec)
|
|
74
76
|
|
|
@@ -113,8 +115,12 @@ module AIA
|
|
|
113
115
|
embedding: config_section_coercion(:embedding),
|
|
114
116
|
tools: config_section_coercion(:tools),
|
|
115
117
|
flags: config_section_coercion(:flags),
|
|
118
|
+
logger: config_section_coercion(:logger),
|
|
116
119
|
registry: config_section_coercion(:registry),
|
|
117
120
|
paths: config_section_coercion(:paths),
|
|
121
|
+
rules: config_section_coercion(:rules),
|
|
122
|
+
concurrency: config_section_coercion(:concurrency),
|
|
123
|
+
tool_filter: config_section_coercion(:tool_filter),
|
|
118
124
|
|
|
119
125
|
# Arrays
|
|
120
126
|
models: TO_MODEL_SPECS,
|
|
@@ -152,50 +158,65 @@ module AIA
|
|
|
152
158
|
# Mapping of flat CLI keys to their nested config locations
|
|
153
159
|
CLI_TO_NESTED_MAP = {
|
|
154
160
|
# flags section
|
|
155
|
-
chat: [
|
|
156
|
-
cost: [
|
|
157
|
-
fuzzy: [
|
|
158
|
-
tokens: [
|
|
159
|
-
no_mcp: [
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
161
|
+
chat: %i[flags chat],
|
|
162
|
+
cost: %i[flags cost],
|
|
163
|
+
fuzzy: %i[flags fuzzy],
|
|
164
|
+
tokens: %i[flags tokens],
|
|
165
|
+
no_mcp: %i[flags no_mcp],
|
|
166
|
+
debug: %i[flags debug],
|
|
167
|
+
verbose: %i[flags verbose],
|
|
168
|
+
consensus: %i[flags consensus],
|
|
169
|
+
track_pipeline: %i[flags track_pipeline],
|
|
170
|
+
expert_routing: %i[flags expert_routing],
|
|
171
|
+
auto_tool_filter: %i[flags auto_tool_filter],
|
|
172
|
+
tool_filter_load: %i[flags tool_filter_load],
|
|
173
|
+
tool_filter_save: %i[flags tool_filter_save],
|
|
174
|
+
allow_ruby_eval: %i[flags allow_ruby_eval],
|
|
175
|
+
thinking: %i[flags thinking],
|
|
176
|
+
tool_filter_timeout_s: %i[tool_filter timeout_s],
|
|
177
|
+
concurrent_auto: %i[concurrency auto],
|
|
164
178
|
# llm section
|
|
165
|
-
temperature: [
|
|
166
|
-
max_tokens: [
|
|
167
|
-
top_p: [
|
|
168
|
-
frequency_penalty: [
|
|
169
|
-
presence_penalty: [
|
|
179
|
+
temperature: %i[llm temperature],
|
|
180
|
+
max_tokens: %i[llm max_tokens],
|
|
181
|
+
top_p: %i[llm top_p],
|
|
182
|
+
frequency_penalty: %i[llm frequency_penalty],
|
|
183
|
+
presence_penalty: %i[llm presence_penalty],
|
|
170
184
|
# prompts section
|
|
171
|
-
prompts_dir: [
|
|
172
|
-
roles_prefix: [
|
|
173
|
-
role: [
|
|
174
|
-
skills_dir: [
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
system_prompt: [
|
|
185
|
+
prompts_dir: %i[prompts dir],
|
|
186
|
+
roles_prefix: %i[prompts roles_prefix],
|
|
187
|
+
role: %i[prompts role],
|
|
188
|
+
skills_dir: %i[skills dir],
|
|
189
|
+
skills: %i[prompts skills],
|
|
190
|
+
tools_prefix: %i[prompts tools_prefix],
|
|
191
|
+
tool: %i[prompts tool],
|
|
192
|
+
system_prompt: %i[prompts system_prompt],
|
|
179
193
|
# output section
|
|
180
|
-
output: [
|
|
181
|
-
history_file: [
|
|
182
|
-
append: [
|
|
183
|
-
markdown: [
|
|
184
|
-
# audio section
|
|
185
|
-
speak: [
|
|
186
|
-
voice: [
|
|
187
|
-
|
|
188
|
-
|
|
194
|
+
output: %i[output file],
|
|
195
|
+
history_file: %i[output history_file],
|
|
196
|
+
append: %i[output append],
|
|
197
|
+
markdown: %i[output markdown],
|
|
198
|
+
# audio section (speak is a flag, not audio config)
|
|
199
|
+
speak: %i[flags speak],
|
|
200
|
+
voice: %i[audio voice],
|
|
201
|
+
speak_command: %i[audio speak_command],
|
|
202
|
+
speech_model: %i[audio speech_model],
|
|
203
|
+
transcription_model: %i[audio transcription_model],
|
|
189
204
|
# image section
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
205
|
+
paint: %i[flags paint],
|
|
206
|
+
image_model: %i[image model],
|
|
207
|
+
image_size: %i[image size],
|
|
208
|
+
image_quality: %i[image quality],
|
|
209
|
+
image_style: %i[image style],
|
|
193
210
|
# tools section
|
|
194
|
-
tool_paths: [
|
|
195
|
-
allowed_tools: [
|
|
196
|
-
rejected_tools: [
|
|
211
|
+
tool_paths: %i[tools paths],
|
|
212
|
+
allowed_tools: %i[tools allowed],
|
|
213
|
+
rejected_tools: %i[tools rejected],
|
|
197
214
|
# registry section
|
|
198
|
-
refresh: [
|
|
215
|
+
refresh: %i[registry refresh],
|
|
216
|
+
# paths section
|
|
217
|
+
plugins_dir: %i[paths plugins_dir],
|
|
218
|
+
# rules section
|
|
219
|
+
rules_enabled: %i[rules enabled]
|
|
199
220
|
}.freeze
|
|
200
221
|
|
|
201
222
|
def initialize(overrides: {})
|
|
@@ -207,7 +228,9 @@ module AIA
|
|
|
207
228
|
load_extra_config(extra_config_path) if extra_config_path
|
|
208
229
|
|
|
209
230
|
apply_models_env_var unless overrides[:models]
|
|
231
|
+
apply_plugins_dir_env_var unless overrides[:plugins_dir]
|
|
210
232
|
apply_overrides(overrides) if overrides && !overrides.empty?
|
|
233
|
+
expand_paths
|
|
211
234
|
process_mcp_files(overrides[:mcp_files]) if overrides[:mcp_files]
|
|
212
235
|
end
|
|
213
236
|
|
|
@@ -223,6 +246,8 @@ module AIA
|
|
|
223
246
|
section, nested_key = CLI_TO_NESTED_MAP[key_sym]
|
|
224
247
|
section_obj = send(section)
|
|
225
248
|
section_obj.send("#{nested_key}=", value) if section_obj.respond_to?("#{nested_key}=")
|
|
249
|
+
elsif key_sym == :models
|
|
250
|
+
self.models = TO_MODEL_SPECS.call(Array(value))
|
|
226
251
|
elsif respond_to?("#{key}=")
|
|
227
252
|
send("#{key}=", value)
|
|
228
253
|
elsif key.to_s.include?('__')
|
|
@@ -234,6 +259,7 @@ module AIA
|
|
|
234
259
|
end
|
|
235
260
|
|
|
236
261
|
# Convert config to hash (for dump, etc.)
|
|
262
|
+
# rubocop:disable Metrics/AbcSize
|
|
237
263
|
def to_h
|
|
238
264
|
{
|
|
239
265
|
service: service.to_h,
|
|
@@ -248,8 +274,11 @@ module AIA
|
|
|
248
274
|
embedding: embedding.to_h,
|
|
249
275
|
tools: tools.to_h,
|
|
250
276
|
flags: flags.to_h,
|
|
277
|
+
logger: logger.to_h,
|
|
251
278
|
registry: registry.to_h,
|
|
252
279
|
paths: paths.to_h,
|
|
280
|
+
rules: rules.to_h,
|
|
281
|
+
concurrency: concurrency.to_h,
|
|
253
282
|
pipeline: pipeline,
|
|
254
283
|
require_libs: require_libs,
|
|
255
284
|
mcp_servers: mcp_servers,
|
|
@@ -258,41 +287,28 @@ module AIA
|
|
|
258
287
|
context_files: context_files
|
|
259
288
|
}
|
|
260
289
|
end
|
|
290
|
+
# rubocop:enable Metrics/AbcSize
|
|
261
291
|
|
|
262
292
|
private
|
|
263
293
|
|
|
264
294
|
# Load a config file that REPLACES the user's personal config.
|
|
265
|
-
# Resets all values to bundled defaults first, then applies the
|
|
266
|
-
# file's values on top. This means -c gives you a clean slate
|
|
267
|
-
# based on defaults + the specified file only.
|
|
268
|
-
#
|
|
269
|
-
# Uses the same YAML structure as defaults.yml (nested sections).
|
|
270
|
-
# Supports both flat format and wrapped (defaults:) format.
|
|
271
|
-
#
|
|
272
|
-
# @param path [String] path to the YAML config file
|
|
273
295
|
def load_extra_config(path)
|
|
274
296
|
path = File.expand_path(path)
|
|
275
297
|
|
|
276
298
|
unless File.exist?(path)
|
|
277
|
-
|
|
299
|
+
$stderr.puts "ERROR: Config file not found: #{path}"
|
|
278
300
|
exit 1
|
|
279
|
-
return #
|
|
301
|
+
return # rubocop:disable Lint/UnreachableCode -- exit is mocked in tests
|
|
280
302
|
end
|
|
281
303
|
|
|
282
|
-
# Reset everything to bundled defaults so the user's personal
|
|
283
|
-
# config (~/.config/aia/aia.yml) does not bleed through.
|
|
284
304
|
reset_to_defaults
|
|
285
305
|
|
|
286
|
-
raw = YAML.
|
|
287
|
-
File.read(path),
|
|
288
|
-
permitted_classes: [Symbol],
|
|
306
|
+
raw = YAML.safe_load_file(path, permitted_classes: [Symbol],
|
|
289
307
|
symbolize_names: true,
|
|
290
|
-
aliases: true
|
|
291
|
-
) || {}
|
|
308
|
+
aliases: true) || {}
|
|
292
309
|
|
|
293
310
|
config_hash = raw.key?(:defaults) ? (raw[:defaults] || {}) : raw
|
|
294
311
|
|
|
295
|
-
# Store the path for reference (visible in --dump output)
|
|
296
312
|
paths[:extra_config_file] = path
|
|
297
313
|
|
|
298
314
|
config_hash.each do |key, value|
|
|
@@ -304,7 +320,7 @@ module AIA
|
|
|
304
320
|
when :mcp_servers
|
|
305
321
|
self.mcp_servers = Array(value)
|
|
306
322
|
when :service, :llm, :prompts, :roles, :skills, :output, :audio, :image, :embedding,
|
|
307
|
-
:tools, :flags, :registry, :paths, :logger
|
|
323
|
+
:tools, :flags, :registry, :paths, :logger, :rules, :concurrency
|
|
308
324
|
section = send(key)
|
|
309
325
|
if section.is_a?(MywayConfig::ConfigSection) && value.is_a?(Hash)
|
|
310
326
|
merge_into_section(section, value)
|
|
@@ -315,9 +331,6 @@ module AIA
|
|
|
315
331
|
expand_paths
|
|
316
332
|
end
|
|
317
333
|
|
|
318
|
-
# Reset all configuration values to the bundled defaults from
|
|
319
|
-
# defaults.yml, discarding anything set by the user's personal
|
|
320
|
-
# config file or environment variables.
|
|
321
334
|
def reset_to_defaults
|
|
322
335
|
defaults = self.class.schema
|
|
323
336
|
|
|
@@ -334,10 +347,6 @@ module AIA
|
|
|
334
347
|
end
|
|
335
348
|
end
|
|
336
349
|
|
|
337
|
-
# Recursively reset a ConfigSection to match the given defaults hash.
|
|
338
|
-
#
|
|
339
|
-
# @param section [MywayConfig::ConfigSection] target section
|
|
340
|
-
# @param defaults_hash [Hash] default values to restore
|
|
341
350
|
def reset_section(section, defaults_hash)
|
|
342
351
|
defaults_hash.each do |key, value|
|
|
343
352
|
existing = section[key.to_sym]
|
|
@@ -349,11 +358,6 @@ module AIA
|
|
|
349
358
|
end
|
|
350
359
|
end
|
|
351
360
|
|
|
352
|
-
# Recursively merge a hash into a ConfigSection, preserving
|
|
353
|
-
# existing values for keys not present in the hash.
|
|
354
|
-
#
|
|
355
|
-
# @param section [MywayConfig::ConfigSection] target section
|
|
356
|
-
# @param hash [Hash] values to merge in
|
|
357
361
|
def merge_into_section(section, hash)
|
|
358
362
|
hash.each do |key, value|
|
|
359
363
|
existing = section[key.to_sym]
|
|
@@ -365,76 +369,59 @@ module AIA
|
|
|
365
369
|
end
|
|
366
370
|
end
|
|
367
371
|
|
|
368
|
-
# Apply AIA_MODEL env var if set (supports comma-separated models with optional roles)
|
|
369
|
-
# Format: MODEL[=ROLE][,MODEL[=ROLE]]...
|
|
370
372
|
def apply_models_env_var
|
|
371
|
-
models_env = ENV
|
|
373
|
+
models_env = ENV.fetch('AIA_MODEL', nil)
|
|
372
374
|
return if models_env.nil? || models_env.empty?
|
|
373
375
|
|
|
374
376
|
self.models = TO_MODEL_SPECS.call(models_env.split(',').map(&:strip))
|
|
375
377
|
end
|
|
376
378
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
if paths.config_file
|
|
384
|
-
paths.config_file = File.expand_path(paths.config_file)
|
|
385
|
-
end
|
|
386
|
-
|
|
387
|
-
if prompts.dir
|
|
388
|
-
prompts.dir = File.expand_path(prompts.dir)
|
|
389
|
-
end
|
|
390
|
-
|
|
391
|
-
if prompts.roles_dir
|
|
392
|
-
prompts.roles_dir = File.expand_path(prompts.roles_dir)
|
|
393
|
-
end
|
|
379
|
+
# Backward-compatible top-level env var for plugin directory.
|
|
380
|
+
# Unlike most nested config values, this uses AIA_PLUGINS_DIR per requirements.
|
|
381
|
+
def apply_plugins_dir_env_var
|
|
382
|
+
plugins_dir_env = ENV.fetch('AIA_PLUGINS_DIR', nil)
|
|
383
|
+
return if plugins_dir_env.nil? || plugins_dir_env.empty?
|
|
394
384
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
end
|
|
398
|
-
|
|
399
|
-
if roles.dir
|
|
400
|
-
roles.dir = File.expand_path(roles.dir)
|
|
401
|
-
end
|
|
385
|
+
paths.plugins_dir = plugins_dir_env
|
|
386
|
+
end
|
|
402
387
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
388
|
+
def expand_paths
|
|
389
|
+
expand_path_on(paths, :aia_dir)
|
|
390
|
+
expand_path_on(paths, :config_file)
|
|
391
|
+
expand_path_on(paths, :plugins_dir)
|
|
392
|
+
expand_path_on(prompts, :dir)
|
|
393
|
+
expand_path_on(prompts, :roles_dir)
|
|
394
|
+
expand_path_on(roles, :dir)
|
|
395
|
+
expand_path_on(skills, :dir)
|
|
396
|
+
expand_path_on(tools, :dir)
|
|
397
|
+
expand_path_on(output, :history_file)
|
|
398
|
+
expand_path_on(rules, :dir)
|
|
399
|
+
end
|
|
406
400
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
401
|
+
def expand_path_on(obj, method_name)
|
|
402
|
+
return unless obj.respond_to?(method_name)
|
|
403
|
+
val = obj.send(method_name)
|
|
404
|
+
return unless val
|
|
405
|
+
obj.send(:"#{method_name}=", File.expand_path(val))
|
|
410
406
|
end
|
|
411
407
|
|
|
412
408
|
def ensure_arrays
|
|
413
|
-
# Ensure array fields are actually arrays
|
|
414
409
|
self.pipeline = [] if pipeline.nil?
|
|
415
410
|
self.require_libs = [] if require_libs.nil?
|
|
416
411
|
self.context_files = [] if context_files.nil?
|
|
417
412
|
self.mcp_servers = [] if mcp_servers.nil?
|
|
418
413
|
self.mcp_use = [] if mcp_use.nil?
|
|
419
414
|
self.mcp_skip = [] if mcp_skip.nil?
|
|
420
|
-
|
|
421
|
-
# Ensure tools.paths is an array
|
|
422
415
|
tools.paths = [] if tools.paths.nil?
|
|
423
|
-
|
|
424
|
-
# Ensure prompts.skills is an array
|
|
425
416
|
prompts.skills = [] if prompts.respond_to?(:skills) && prompts.skills.nil?
|
|
426
417
|
end
|
|
427
418
|
|
|
428
|
-
# Process MCP JSON files and merge servers into mcp_servers
|
|
429
|
-
#
|
|
430
|
-
# @param mcp_files [Array<String>] paths to MCP JSON configuration files
|
|
431
419
|
def process_mcp_files(mcp_files)
|
|
432
420
|
return if mcp_files.nil? || mcp_files.empty?
|
|
433
421
|
|
|
434
422
|
servers_from_files = McpParser.parse_files(mcp_files)
|
|
435
423
|
return if servers_from_files.empty?
|
|
436
424
|
|
|
437
|
-
# Merge with existing mcp_servers (CLI files take precedence)
|
|
438
425
|
self.mcp_servers = (mcp_servers || []) + servers_from_files
|
|
439
426
|
end
|
|
440
427
|
|
|
@@ -442,32 +429,10 @@ module AIA
|
|
|
442
429
|
section = parts[0].to_sym
|
|
443
430
|
key = parts[1].to_sym
|
|
444
431
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
prompts.send("#{key}=", value) if prompts.respond_to?("#{key}=")
|
|
450
|
-
when :output
|
|
451
|
-
output.send("#{key}=", value) if output.respond_to?("#{key}=")
|
|
452
|
-
when :audio
|
|
453
|
-
audio.send("#{key}=", value) if audio.respond_to?("#{key}=")
|
|
454
|
-
when :image
|
|
455
|
-
image.send("#{key}=", value) if image.respond_to?("#{key}=")
|
|
456
|
-
when :embedding
|
|
457
|
-
embedding.send("#{key}=", value) if embedding.respond_to?("#{key}=")
|
|
458
|
-
when :tools
|
|
459
|
-
tools.send("#{key}=", value) if tools.respond_to?("#{key}=")
|
|
460
|
-
when :flags
|
|
461
|
-
flags.send("#{key}=", value) if flags.respond_to?("#{key}=")
|
|
462
|
-
when :registry
|
|
463
|
-
registry.send("#{key}=", value) if registry.respond_to?("#{key}=")
|
|
464
|
-
when :paths
|
|
465
|
-
paths.send("#{key}=", value) if paths.respond_to?("#{key}=")
|
|
466
|
-
when :roles
|
|
467
|
-
roles.send("#{key}=", value) if roles.respond_to?("#{key}=")
|
|
468
|
-
when :skills
|
|
469
|
-
skills.send("#{key}=", value) if skills.respond_to?("#{key}=")
|
|
470
|
-
end
|
|
432
|
+
target = respond_to?(section) ? send(section) : nil
|
|
433
|
+
return unless target.respond_to?("#{key}=")
|
|
434
|
+
|
|
435
|
+
target.send("#{key}=", value)
|
|
471
436
|
end
|
|
472
437
|
end
|
|
473
438
|
end
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# lib/aia/content_extractor.rb
|
|
4
|
+
#
|
|
5
|
+
# Shared module for extracting text content from various response types.
|
|
6
|
+
# Handles RobotLab::RobotResult, SimpleFlow::Result, and plain strings.
|
|
7
|
+
# Include this module instead of defining extract_content locally.
|
|
8
|
+
|
|
9
|
+
module AIA
|
|
10
|
+
module ContentExtractor
|
|
11
|
+
# Extract text content from a response object.
|
|
12
|
+
#
|
|
13
|
+
# @param result the response (RobotResult, SimpleFlow::Result, String, etc.)
|
|
14
|
+
# @return [String] the extracted text
|
|
15
|
+
def extract_content(result)
|
|
16
|
+
if defined?(SimpleFlow::Result) && result.is_a?(SimpleFlow::Result)
|
|
17
|
+
return extract_network_content(result)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
if result.respond_to?(:reply)
|
|
21
|
+
result.reply
|
|
22
|
+
elsif result.respond_to?(:last_text_content)
|
|
23
|
+
result.last_text_content
|
|
24
|
+
elsif result.respond_to?(:content)
|
|
25
|
+
result.content
|
|
26
|
+
else
|
|
27
|
+
result.to_s
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Extract content from a Network's SimpleFlow::Result.
|
|
32
|
+
# Each robot's result is stored in context under its task name.
|
|
33
|
+
# Includes per-robot timing when available.
|
|
34
|
+
#
|
|
35
|
+
# @param flow_result [SimpleFlow::Result]
|
|
36
|
+
# @return [String]
|
|
37
|
+
def extract_network_content(flow_result)
|
|
38
|
+
parts = []
|
|
39
|
+
flow_result.context.each do |task_name, robot_result|
|
|
40
|
+
next if task_name == :run_params
|
|
41
|
+
|
|
42
|
+
content = if robot_result.respond_to?(:reply)
|
|
43
|
+
robot_result.reply
|
|
44
|
+
elsif robot_result.respond_to?(:content)
|
|
45
|
+
robot_result.content
|
|
46
|
+
else
|
|
47
|
+
robot_result.to_s
|
|
48
|
+
end
|
|
49
|
+
next unless content && !content.empty?
|
|
50
|
+
|
|
51
|
+
duration = robot_result.respond_to?(:duration) ? robot_result.duration : nil
|
|
52
|
+
robot_name = robot_result.respond_to?(:robot_name) ? robot_result.robot_name : nil
|
|
53
|
+
label = robot_name && robot_name != task_name.to_s ? "#{robot_name} [#{task_name}]" : task_name.to_s
|
|
54
|
+
header = if duration
|
|
55
|
+
"**#{label}** (#{format_duration(duration)}):"
|
|
56
|
+
else
|
|
57
|
+
"**#{label}:**"
|
|
58
|
+
end
|
|
59
|
+
parts << "#{header}\n#{content}"
|
|
60
|
+
end
|
|
61
|
+
parts.join("\n\n")
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Store structured robot results into a network's shared memory.
|
|
65
|
+
# Each robot's output is written as a hash under result_<task_name>.
|
|
66
|
+
#
|
|
67
|
+
# @param flow_result [SimpleFlow::Result]
|
|
68
|
+
# @param network [RobotLab::Network]
|
|
69
|
+
def store_results_in_memory(flow_result, network)
|
|
70
|
+
return unless network.respond_to?(:memory)
|
|
71
|
+
|
|
72
|
+
memory = network.memory
|
|
73
|
+
flow_result.context.each do |task_name, robot_result|
|
|
74
|
+
next if task_name == :run_params
|
|
75
|
+
next unless robot_result.respond_to?(:reply)
|
|
76
|
+
|
|
77
|
+
memory.current_writer = task_name.to_s
|
|
78
|
+
memory.set(:"result_#{task_name}", {
|
|
79
|
+
content: robot_result.reply,
|
|
80
|
+
model: robot_result.respond_to?(:robot_name) ? robot_result.robot_name : nil,
|
|
81
|
+
duration: robot_result.respond_to?(:duration) ? robot_result.duration : nil
|
|
82
|
+
})
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Present an AI result: extract content, record the turn, display,
|
|
87
|
+
# write to output file, show metrics, speak, and print separator.
|
|
88
|
+
# Callers pass only the keyword args they need; everything optional
|
|
89
|
+
# defaults to nil / false so this works for both full ChatLoop turns
|
|
90
|
+
# and simpler SpecialModeHandler dispatches.
|
|
91
|
+
#
|
|
92
|
+
# @param result the response object
|
|
93
|
+
# @param streamed_content [String, nil] pre-streamed content (skips display_ai_response)
|
|
94
|
+
# @param prompt [String, nil] user prompt for tracker
|
|
95
|
+
# @param elapsed [Float, nil] elapsed seconds
|
|
96
|
+
# @param ui_presenter [UIPresenter] for display calls
|
|
97
|
+
# @param tracker [SessionTracker, nil] to record the turn
|
|
98
|
+
# @param decisions [Hash, nil] routing decisions for tracker
|
|
99
|
+
# @return [String] the extracted content
|
|
100
|
+
def present_result(result, ui_presenter:, streamed_content: nil, prompt: nil, elapsed: nil, tracker: nil, decisions: nil)
|
|
101
|
+
content = streamed_content || extract_content(result)
|
|
102
|
+
|
|
103
|
+
if tracker && prompt
|
|
104
|
+
tracker.record_turn(
|
|
105
|
+
model: AIA.config.models.first.name,
|
|
106
|
+
input: prompt,
|
|
107
|
+
result: result,
|
|
108
|
+
decisions: decisions,
|
|
109
|
+
elapsed: elapsed
|
|
110
|
+
)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
if streamed_content
|
|
114
|
+
puts
|
|
115
|
+
else
|
|
116
|
+
ui_presenter.display_ai_response(content)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
output_to_file(content)
|
|
120
|
+
display_metrics(result, elapsed: elapsed) if respond_to?(:display_metrics, true)
|
|
121
|
+
speak(content) if respond_to?(:speak, true)
|
|
122
|
+
ui_presenter.display_separator
|
|
123
|
+
|
|
124
|
+
content
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Write AI response content to the configured output file.
|
|
128
|
+
#
|
|
129
|
+
# @param content [String] the AI response text
|
|
130
|
+
def output_to_file(content)
|
|
131
|
+
out_file = AIA.config.output.file
|
|
132
|
+
return unless out_file
|
|
133
|
+
|
|
134
|
+
File.open(out_file, 'a') { |f| f.puts "\nAI: #{content}" }
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Format a duration in seconds to a human-readable string.
|
|
138
|
+
#
|
|
139
|
+
# @param seconds [Float, nil]
|
|
140
|
+
# @return [String]
|
|
141
|
+
def format_duration(seconds)
|
|
142
|
+
return "0.0s" unless seconds
|
|
143
|
+
|
|
144
|
+
if seconds < 60
|
|
145
|
+
"%.1fs" % seconds
|
|
146
|
+
else
|
|
147
|
+
minutes = (seconds / 60).to_i
|
|
148
|
+
secs = seconds % 60
|
|
149
|
+
"#{minutes}m %04.1fs" % secs
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# lib/aia/cost_calculator.rb
|
|
4
|
+
#
|
|
5
|
+
# Shared cost calculation logic used by SessionTracker and UIPresenter.
|
|
6
|
+
# Looks up per-million token pricing from RubyLLM::Models and returns
|
|
7
|
+
# a breakdown hash.
|
|
8
|
+
|
|
9
|
+
module AIA
|
|
10
|
+
module CostCalculator
|
|
11
|
+
# @param model_id [String, nil]
|
|
12
|
+
# @param input_tokens [Integer]
|
|
13
|
+
# @param output_tokens [Integer]
|
|
14
|
+
# @return [Hash] { available: true, total_cost:, input_cost:, output_cost: }
|
|
15
|
+
# or { available: false } when pricing is unavailable
|
|
16
|
+
def self.calculate(model_id:, input_tokens:, output_tokens:)
|
|
17
|
+
return { available: false } unless model_id && defined?(RubyLLM::Models)
|
|
18
|
+
|
|
19
|
+
# RubyLLM::Models.find raises ModelNotFoundError on an unknown id (it never
|
|
20
|
+
# returns nil), so the unknown-model case is handled by the rescue below.
|
|
21
|
+
model_info = RubyLLM::Models.find(model_id)
|
|
22
|
+
|
|
23
|
+
input_price = model_info.respond_to?(:input_price_per_million) ? model_info.input_price_per_million : nil
|
|
24
|
+
output_price = model_info.respond_to?(:output_price_per_million) ? model_info.output_price_per_million : nil
|
|
25
|
+
return { available: false } unless input_price && output_price
|
|
26
|
+
|
|
27
|
+
input_cost = input_tokens * input_price / 1_000_000.0
|
|
28
|
+
output_cost = output_tokens * output_price / 1_000_000.0
|
|
29
|
+
|
|
30
|
+
{ available: true, total_cost: input_cost + output_cost, input_cost: input_cost, output_cost: output_cost }
|
|
31
|
+
rescue RubyLLM::ModelNotFoundError
|
|
32
|
+
# Expected when pricing for the model isn't in the registry — not an error.
|
|
33
|
+
{ available: false }
|
|
34
|
+
rescue StandardError => e
|
|
35
|
+
{ available: false, error: e.message }
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|