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/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-next 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,
|
|
@@ -262,37 +291,25 @@ module AIA
|
|
|
262
291
|
private
|
|
263
292
|
|
|
264
293
|
# Load a config file that REPLACES the user's personal config.
|
|
265
|
-
#
|
|
266
|
-
#
|
|
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
|
|
294
|
+
# :reek:TooManyStatements -- single case dispatch mapping YAML keys to typed config setters; one branch per key family
|
|
295
|
+
# :reek:DuplicateMethodCall -- Array(value) sits in mutually exclusive case branches; hoisting it would coerce hash-section values too
|
|
273
296
|
def load_extra_config(path)
|
|
274
297
|
path = File.expand_path(path)
|
|
275
298
|
|
|
276
299
|
unless File.exist?(path)
|
|
277
|
-
|
|
300
|
+
$stderr.puts "ERROR: Config file not found: #{path}"
|
|
278
301
|
exit 1
|
|
279
|
-
return #
|
|
302
|
+
return # rubocop:disable Lint/UnreachableCode -- exit is mocked in tests
|
|
280
303
|
end
|
|
281
304
|
|
|
282
|
-
# Reset everything to bundled defaults so the user's personal
|
|
283
|
-
# config (~/.config/aia/aia.yml) does not bleed through.
|
|
284
305
|
reset_to_defaults
|
|
285
306
|
|
|
286
|
-
raw = YAML.
|
|
287
|
-
File.read(path),
|
|
288
|
-
permitted_classes: [Symbol],
|
|
307
|
+
raw = YAML.safe_load_file(path, permitted_classes: [Symbol],
|
|
289
308
|
symbolize_names: true,
|
|
290
|
-
aliases: true
|
|
291
|
-
) || {}
|
|
309
|
+
aliases: true) || {}
|
|
292
310
|
|
|
293
311
|
config_hash = raw.key?(:defaults) ? (raw[:defaults] || {}) : raw
|
|
294
312
|
|
|
295
|
-
# Store the path for reference (visible in --dump output)
|
|
296
313
|
paths[:extra_config_file] = path
|
|
297
314
|
|
|
298
315
|
config_hash.each do |key, value|
|
|
@@ -304,7 +321,7 @@ module AIA
|
|
|
304
321
|
when :mcp_servers
|
|
305
322
|
self.mcp_servers = Array(value)
|
|
306
323
|
when :service, :llm, :prompts, :roles, :skills, :output, :audio, :image, :embedding,
|
|
307
|
-
:tools, :flags, :registry, :paths, :logger
|
|
324
|
+
:tools, :flags, :registry, :paths, :logger, :rules, :concurrency
|
|
308
325
|
section = send(key)
|
|
309
326
|
if section.is_a?(MywayConfig::ConfigSection) && value.is_a?(Hash)
|
|
310
327
|
merge_into_section(section, value)
|
|
@@ -315,9 +332,6 @@ module AIA
|
|
|
315
332
|
expand_paths
|
|
316
333
|
end
|
|
317
334
|
|
|
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
335
|
def reset_to_defaults
|
|
322
336
|
defaults = self.class.schema
|
|
323
337
|
|
|
@@ -334,10 +348,6 @@ module AIA
|
|
|
334
348
|
end
|
|
335
349
|
end
|
|
336
350
|
|
|
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
351
|
def reset_section(section, defaults_hash)
|
|
342
352
|
defaults_hash.each do |key, value|
|
|
343
353
|
existing = section[key.to_sym]
|
|
@@ -349,11 +359,6 @@ module AIA
|
|
|
349
359
|
end
|
|
350
360
|
end
|
|
351
361
|
|
|
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
362
|
def merge_into_section(section, hash)
|
|
358
363
|
hash.each do |key, value|
|
|
359
364
|
existing = section[key.to_sym]
|
|
@@ -365,76 +370,59 @@ module AIA
|
|
|
365
370
|
end
|
|
366
371
|
end
|
|
367
372
|
|
|
368
|
-
# Apply AIA_MODEL env var if set (supports comma-separated models with optional roles)
|
|
369
|
-
# Format: MODEL[=ROLE][,MODEL[=ROLE]]...
|
|
370
373
|
def apply_models_env_var
|
|
371
|
-
models_env = ENV
|
|
374
|
+
models_env = ENV.fetch('AIA_MODEL', nil)
|
|
372
375
|
return if models_env.nil? || models_env.empty?
|
|
373
376
|
|
|
374
377
|
self.models = TO_MODEL_SPECS.call(models_env.split(',').map(&:strip))
|
|
375
378
|
end
|
|
376
379
|
|
|
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
|
|
380
|
+
# Backward-compatible top-level env var for plugin directory.
|
|
381
|
+
# Unlike most nested config values, this uses AIA_PLUGINS_DIR per requirements.
|
|
382
|
+
def apply_plugins_dir_env_var
|
|
383
|
+
plugins_dir_env = ENV.fetch('AIA_PLUGINS_DIR', nil)
|
|
384
|
+
return if plugins_dir_env.nil? || plugins_dir_env.empty?
|
|
394
385
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
end
|
|
398
|
-
|
|
399
|
-
if roles.dir
|
|
400
|
-
roles.dir = File.expand_path(roles.dir)
|
|
401
|
-
end
|
|
386
|
+
paths.plugins_dir = plugins_dir_env
|
|
387
|
+
end
|
|
402
388
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
389
|
+
def expand_paths
|
|
390
|
+
expand_path_on(paths, :aia_dir)
|
|
391
|
+
expand_path_on(paths, :config_file)
|
|
392
|
+
expand_path_on(paths, :plugins_dir)
|
|
393
|
+
expand_path_on(prompts, :dir)
|
|
394
|
+
expand_path_on(prompts, :roles_dir)
|
|
395
|
+
expand_path_on(roles, :dir)
|
|
396
|
+
expand_path_on(skills, :dir)
|
|
397
|
+
expand_path_on(tools, :dir)
|
|
398
|
+
expand_path_on(output, :history_file)
|
|
399
|
+
expand_path_on(rules, :dir)
|
|
400
|
+
end
|
|
406
401
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
402
|
+
def expand_path_on(obj, method_name)
|
|
403
|
+
return unless obj.respond_to?(method_name)
|
|
404
|
+
val = obj.send(method_name)
|
|
405
|
+
return unless val
|
|
406
|
+
obj.send(:"#{method_name}=", File.expand_path(val))
|
|
410
407
|
end
|
|
411
408
|
|
|
412
409
|
def ensure_arrays
|
|
413
|
-
# Ensure array fields are actually arrays
|
|
414
410
|
self.pipeline = [] if pipeline.nil?
|
|
415
411
|
self.require_libs = [] if require_libs.nil?
|
|
416
412
|
self.context_files = [] if context_files.nil?
|
|
417
413
|
self.mcp_servers = [] if mcp_servers.nil?
|
|
418
414
|
self.mcp_use = [] if mcp_use.nil?
|
|
419
415
|
self.mcp_skip = [] if mcp_skip.nil?
|
|
420
|
-
|
|
421
|
-
# Ensure tools.paths is an array
|
|
422
416
|
tools.paths = [] if tools.paths.nil?
|
|
423
|
-
|
|
424
|
-
# Ensure prompts.skills is an array
|
|
425
417
|
prompts.skills = [] if prompts.respond_to?(:skills) && prompts.skills.nil?
|
|
426
418
|
end
|
|
427
419
|
|
|
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
420
|
def process_mcp_files(mcp_files)
|
|
432
421
|
return if mcp_files.nil? || mcp_files.empty?
|
|
433
422
|
|
|
434
423
|
servers_from_files = McpParser.parse_files(mcp_files)
|
|
435
424
|
return if servers_from_files.empty?
|
|
436
425
|
|
|
437
|
-
# Merge with existing mcp_servers (CLI files take precedence)
|
|
438
426
|
self.mcp_servers = (mcp_servers || []) + servers_from_files
|
|
439
427
|
end
|
|
440
428
|
|
|
@@ -442,32 +430,10 @@ module AIA
|
|
|
442
430
|
section = parts[0].to_sym
|
|
443
431
|
key = parts[1].to_sym
|
|
444
432
|
|
|
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
|
|
433
|
+
target = respond_to?(section) ? send(section) : nil
|
|
434
|
+
return unless target.respond_to?("#{key}=")
|
|
435
|
+
|
|
436
|
+
target.send("#{key}=", value)
|
|
471
437
|
end
|
|
472
438
|
end
|
|
473
439
|
end
|
|
@@ -0,0 +1,155 @@
|
|
|
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
|
+
# :reek:TooManyStatements -- one pass duck-types reply/content/duration into labeled sections
|
|
38
|
+
def extract_network_content(flow_result)
|
|
39
|
+
parts = []
|
|
40
|
+
flow_result.context.each do |task_name, robot_result|
|
|
41
|
+
next if task_name == :run_params
|
|
42
|
+
|
|
43
|
+
content = if robot_result.respond_to?(:reply)
|
|
44
|
+
robot_result.reply
|
|
45
|
+
elsif robot_result.respond_to?(:content)
|
|
46
|
+
robot_result.content
|
|
47
|
+
else
|
|
48
|
+
robot_result.to_s
|
|
49
|
+
end
|
|
50
|
+
next unless content && !content.empty?
|
|
51
|
+
|
|
52
|
+
duration = robot_result.respond_to?(:duration) ? robot_result.duration : nil
|
|
53
|
+
robot_name = robot_result.respond_to?(:robot_name) ? robot_result.robot_name : nil
|
|
54
|
+
label = robot_name && robot_name != task_name.to_s ? "#{robot_name} [#{task_name}]" : task_name.to_s
|
|
55
|
+
header = if duration
|
|
56
|
+
"**#{label}** (#{format_duration(duration)}):"
|
|
57
|
+
else
|
|
58
|
+
"**#{label}:**"
|
|
59
|
+
end
|
|
60
|
+
parts << "#{header}\n#{content}"
|
|
61
|
+
end
|
|
62
|
+
parts.join("\n\n")
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Store structured robot results into a network's shared memory.
|
|
66
|
+
# Each robot's output is written as a hash under result_<task_name>.
|
|
67
|
+
#
|
|
68
|
+
# @param flow_result [SimpleFlow::Result]
|
|
69
|
+
# @param network [RobotLab::Network]
|
|
70
|
+
def store_results_in_memory(flow_result, network)
|
|
71
|
+
return unless network.respond_to?(:memory)
|
|
72
|
+
|
|
73
|
+
memory = network.memory
|
|
74
|
+
flow_result.context.each do |task_name, robot_result|
|
|
75
|
+
next if task_name == :run_params
|
|
76
|
+
next unless robot_result.respond_to?(:reply)
|
|
77
|
+
|
|
78
|
+
memory.current_writer = task_name.to_s
|
|
79
|
+
memory.set(:"result_#{task_name}", {
|
|
80
|
+
content: robot_result.reply,
|
|
81
|
+
model: robot_result.respond_to?(:robot_name) ? robot_result.robot_name : nil,
|
|
82
|
+
duration: robot_result.respond_to?(:duration) ? robot_result.duration : nil
|
|
83
|
+
})
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Present an AI result: extract content, record the turn, display,
|
|
88
|
+
# write to output file, show metrics, speak, and print separator.
|
|
89
|
+
# Callers pass only the keyword args they need; everything optional
|
|
90
|
+
# defaults to nil / false so this works for both full ChatLoop turns
|
|
91
|
+
# and simpler SpecialModeHandler dispatches.
|
|
92
|
+
#
|
|
93
|
+
# @param result the response object
|
|
94
|
+
# @param streamed_content [String, nil] pre-streamed content (skips display_ai_response)
|
|
95
|
+
# @param prompt [String, nil] user prompt for tracker
|
|
96
|
+
# @param elapsed [Float, nil] elapsed seconds
|
|
97
|
+
# @param ui_presenter [UIPresenter] for display calls
|
|
98
|
+
# @param tracker [SessionTracker, nil] to record the turn
|
|
99
|
+
# @param decisions [Hash, nil] routing decisions for tracker
|
|
100
|
+
# @return [String] the extracted content
|
|
101
|
+
# :reek:LongParameterList { max_params: 7 } -- presentation seam: each keyword is an independent optional aspect
|
|
102
|
+
def present_result(result, ui_presenter:, streamed_content: nil, prompt: nil, elapsed: nil, tracker: nil, decisions: nil)
|
|
103
|
+
content = streamed_content || extract_content(result)
|
|
104
|
+
|
|
105
|
+
if tracker && prompt
|
|
106
|
+
tracker.record_turn(
|
|
107
|
+
model: AIA.config.models.first.name,
|
|
108
|
+
input: prompt,
|
|
109
|
+
result: result,
|
|
110
|
+
decisions: decisions,
|
|
111
|
+
elapsed: elapsed
|
|
112
|
+
)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
if streamed_content
|
|
116
|
+
puts
|
|
117
|
+
else
|
|
118
|
+
ui_presenter.display_ai_response(content)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
output_to_file(content)
|
|
122
|
+
display_metrics(result, elapsed: elapsed) if respond_to?(:display_metrics, true)
|
|
123
|
+
speak(content) if respond_to?(:speak, true)
|
|
124
|
+
ui_presenter.display_separator
|
|
125
|
+
|
|
126
|
+
content
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Write AI response content to the configured output file.
|
|
130
|
+
#
|
|
131
|
+
# @param content [String] the AI response text
|
|
132
|
+
def output_to_file(content)
|
|
133
|
+
out_file = AIA.config.output.file
|
|
134
|
+
return unless out_file
|
|
135
|
+
|
|
136
|
+
File.open(out_file, 'a') { |f| f.puts "\nAI: #{content}" }
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Format a duration in seconds to a human-readable string.
|
|
140
|
+
#
|
|
141
|
+
# @param seconds [Float, nil]
|
|
142
|
+
# @return [String]
|
|
143
|
+
def format_duration(seconds)
|
|
144
|
+
return "0.0s" unless seconds
|
|
145
|
+
|
|
146
|
+
if seconds < 60
|
|
147
|
+
"%.1fs" % seconds
|
|
148
|
+
else
|
|
149
|
+
minutes = (seconds / 60).to_i
|
|
150
|
+
secs = seconds % 60
|
|
151
|
+
"#{minutes}m %04.1fs" % secs
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
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
|
+
# :reek:TooManyStatements -- linear price lookup: guard, registry fetch, per-direction cost math, result hash
|
|
17
|
+
def self.calculate(model_id:, input_tokens:, output_tokens:)
|
|
18
|
+
return { available: false } unless model_id && defined?(RubyLLM::Models)
|
|
19
|
+
|
|
20
|
+
# RubyLLM::Models.find raises ModelNotFoundError on an unknown id (it never
|
|
21
|
+
# returns nil), so the unknown-model case is handled by the rescue below.
|
|
22
|
+
model_info = RubyLLM::Models.find(model_id)
|
|
23
|
+
|
|
24
|
+
input_price = model_info.respond_to?(:input_price_per_million) ? model_info.input_price_per_million : nil
|
|
25
|
+
output_price = model_info.respond_to?(:output_price_per_million) ? model_info.output_price_per_million : nil
|
|
26
|
+
return { available: false } unless input_price && output_price
|
|
27
|
+
|
|
28
|
+
input_cost = input_tokens * input_price / 1_000_000.0
|
|
29
|
+
output_cost = output_tokens * output_price / 1_000_000.0
|
|
30
|
+
|
|
31
|
+
{ available: true, total_cost: input_cost + output_cost, input_cost: input_cost, output_cost: output_cost }
|
|
32
|
+
rescue RubyLLM::ModelNotFoundError
|
|
33
|
+
# Expected when pricing for the model isn't in the registry — not an error.
|
|
34
|
+
{ available: false }
|
|
35
|
+
rescue StandardError => e
|
|
36
|
+
{ available: false, error: e.message }
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|