aia 2.0.0.0.pre.alpha → 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 +5 -1
- data/.loki +3 -223
- data/.reek.yml +160 -0
- data/.rubocop_strict.yml +15 -0
- data/.version +1 -1
- data/CHANGELOG.md +10 -5
- data/README.md +1 -1
- data/Rakefile +0 -113
- data/_typos.toml +10 -0
- data/aia.gemspec +92 -0
- data/config/aia.yml +13 -0
- data/lib/aia/chat_loop.rb +8 -52
- data/lib/aia/config/cli_parser.rb +66 -57
- data/lib/aia/config/mcp_parser.rb +27 -19
- data/lib/aia/config/validator.rb +83 -94
- data/lib/aia/config.rb +3 -2
- data/lib/aia/content_extractor.rb +2 -0
- data/lib/aia/cost_calculator.rb +1 -0
- data/lib/aia/debate_handler.rb +28 -20
- data/lib/aia/delegate_handler.rb +7 -3
- data/lib/aia/directive.rb +10 -8
- data/lib/aia/directives/configuration_directives.rb +115 -101
- data/lib/aia/directives/context_directives.rb +31 -28
- data/lib/aia/directives/execution_directives.rb +18 -13
- data/lib/aia/directives/model_directives.rb +134 -154
- data/lib/aia/directives/trakflow_directives.rb +27 -9
- data/lib/aia/directives/utility_directives.rb +71 -71
- data/lib/aia/directives/web_and_file_directives.rb +63 -50
- data/lib/aia/layered_orchestrator.rb +66 -43
- data/lib/aia/logger.rb +21 -21
- data/lib/aia/mcp_connection_manager.rb +50 -40
- data/lib/aia/mcp_server_config.rb +30 -0
- data/lib/aia/mcp_utility.rb +7 -4
- data/lib/aia/mention_router.rb +7 -50
- data/lib/aia/network_builder.rb +10 -5
- data/lib/aia/network_memory_manager.rb +6 -6
- data/lib/aia/pipeline_orchestrator.rb +20 -10
- data/lib/aia/plugin_monitor.rb +9 -6
- data/lib/aia/prompt_decomposer.rb +5 -3
- data/lib/aia/prompt_handler.rb +43 -49
- data/lib/aia/robot_factory.rb +12 -8
- data/lib/aia/robot_namer.rb +2 -8
- data/lib/aia/session.rb +7 -3
- data/lib/aia/session_tracker.rb +39 -41
- data/lib/aia/similarity_scorer.rb +5 -3
- data/lib/aia/special_mode_handler.rb +52 -32
- data/lib/aia/speech.rb +67 -0
- data/lib/aia/startup_coordinator.rb +1 -0
- data/lib/aia/streaming_runner.rb +6 -3
- data/lib/aia/system_prompt_assembler.rb +11 -7
- data/lib/aia/task_coordinator.rb +10 -5
- data/lib/aia/timing.rb +15 -0
- data/lib/aia/tool_filter.rb +1 -0
- data/lib/aia/tool_filter_strategy.rb +28 -25
- data/lib/aia/tool_introspection.rb +17 -0
- data/lib/aia/tool_loader.rb +12 -6
- data/lib/aia/tools/task_board_tool.rb +1 -0
- data/lib/aia/trakflow_bridge.rb +5 -3
- data/lib/aia/turn_state.rb +1 -0
- data/lib/aia/ui_presenter.rb +67 -59
- data/lib/aia/utility.rb +5 -3
- data/lib/aia/variable_input_collector.rb +1 -0
- data/lib/aia/verification_network.rb +1 -2
- data/lib/aia.rb +16 -0
- metadata +15 -9
- data/.quality/flay_baseline.txt +0 -1
- data/.quality/flog_baseline.txt +0 -29
- data/.quality/reek_baseline.txt +0 -80
data/lib/aia/mention_router.rb
CHANGED
|
@@ -10,6 +10,7 @@ module AIA
|
|
|
10
10
|
class MentionRouter
|
|
11
11
|
include ContentExtractor
|
|
12
12
|
include HandlerProtocol
|
|
13
|
+
include Speech
|
|
13
14
|
|
|
14
15
|
# Reserved @mention that broadcasts the message to every crew member.
|
|
15
16
|
BROADCAST_TOKEN = 'crew'
|
|
@@ -25,6 +26,7 @@ module AIA
|
|
|
25
26
|
#
|
|
26
27
|
# @param context [HandlerContext] — reads context.robot and context.prompt
|
|
27
28
|
# @return [Boolean]
|
|
29
|
+
# :reek:TooManyStatements -- mention routing: scan, resolve, report unknowns, dispatch; one decision chain
|
|
28
30
|
def handle(context) # rubocop:disable Naming/PredicateMethod
|
|
29
31
|
robot = context.robot
|
|
30
32
|
prompt = context.prompt
|
|
@@ -120,10 +122,11 @@ module AIA
|
|
|
120
122
|
|
|
121
123
|
# A single addressed robot streams its reply token-by-token.
|
|
122
124
|
def run_streamed(bot, prompt)
|
|
125
|
+
name = bot.name
|
|
123
126
|
result, streamed_content, elapsed = @streaming_runner.run(
|
|
124
127
|
bot, prompt,
|
|
125
|
-
header: "\nAI (#{
|
|
126
|
-
spinner_message: "#{
|
|
128
|
+
header: "\nAI (#{name}):\n ",
|
|
129
|
+
spinner_message: "#{name} processing..."
|
|
127
130
|
)
|
|
128
131
|
content = streamed_content || extract_content(result)
|
|
129
132
|
|
|
@@ -137,7 +140,7 @@ module AIA
|
|
|
137
140
|
finalize_reply(bot, prompt, result, content, elapsed)
|
|
138
141
|
content
|
|
139
142
|
rescue StandardError => e
|
|
140
|
-
@ui_presenter.display_info("Error from #{
|
|
143
|
+
@ui_presenter.display_info("Error from #{name}: #{e.class}: #{e.message}")
|
|
141
144
|
nil
|
|
142
145
|
end
|
|
143
146
|
|
|
@@ -156,6 +159,7 @@ module AIA
|
|
|
156
159
|
|
|
157
160
|
# Run one crew member to completion (off the streaming path), in a worker
|
|
158
161
|
# thread. Returns [bot, result, elapsed, error] — no shared state touched.
|
|
162
|
+
# :reek:DuplicateMethodCall -- each clock_gettime must read the clock at that moment (start, success end, failure end)
|
|
159
163
|
def run_member(bot, prompt)
|
|
160
164
|
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
161
165
|
result = bot.run(prompt, mcp: :inherit, tools: :inherit)
|
|
@@ -209,52 +213,5 @@ module AIA
|
|
|
209
213
|
return message.model if message.respond_to?(:model) && message.model
|
|
210
214
|
nil
|
|
211
215
|
end
|
|
212
|
-
|
|
213
|
-
def speak(content)
|
|
214
|
-
return unless AIA.speak?
|
|
215
|
-
|
|
216
|
-
audio = AIA.config.audio
|
|
217
|
-
command = audio.speak_command || 'say'
|
|
218
|
-
env = {}
|
|
219
|
-
env['SPEECH_MODEL'] = audio.speech_model if audio.speech_model
|
|
220
|
-
|
|
221
|
-
if command == 'say'
|
|
222
|
-
run_with_spinner("Speaking...") do
|
|
223
|
-
if audio.voice && !audio.voice.to_s.strip.empty?
|
|
224
|
-
system(env, command, '-v', audio.voice, content.to_s)
|
|
225
|
-
else
|
|
226
|
-
system(env, command, content.to_s)
|
|
227
|
-
end
|
|
228
|
-
end
|
|
229
|
-
else
|
|
230
|
-
require 'tempfile'
|
|
231
|
-
tmpfile = Tempfile.new(['aia-tts-', '.mp3'])
|
|
232
|
-
tmpfile.close
|
|
233
|
-
begin
|
|
234
|
-
run_with_spinner("Converting to audio...") do
|
|
235
|
-
system(env, command, content.to_s, tmpfile.path)
|
|
236
|
-
end
|
|
237
|
-
if File.size?(tmpfile.path)
|
|
238
|
-
run_with_spinner("Speaking...") do
|
|
239
|
-
system('afplay', tmpfile.path)
|
|
240
|
-
end
|
|
241
|
-
end
|
|
242
|
-
ensure
|
|
243
|
-
tmpfile.unlink
|
|
244
|
-
end
|
|
245
|
-
end
|
|
246
|
-
rescue StandardError => e
|
|
247
|
-
$stderr.puts "Warning: Speech failed: #{e.message}"
|
|
248
|
-
end
|
|
249
|
-
|
|
250
|
-
def run_with_spinner(message)
|
|
251
|
-
spinner = TTY::Spinner.new("[:spinner] #{message}", format: :bouncing_ball, output: $stderr)
|
|
252
|
-
spinner.auto_spin
|
|
253
|
-
begin
|
|
254
|
-
yield
|
|
255
|
-
ensure
|
|
256
|
-
spinner.stop
|
|
257
|
-
end
|
|
258
|
-
end
|
|
259
216
|
end
|
|
260
217
|
end
|
data/lib/aia/network_builder.rb
CHANGED
|
@@ -15,6 +15,7 @@ module AIA
|
|
|
15
15
|
# @param config [AIA::Config]
|
|
16
16
|
# @param namer [RobotNamer]
|
|
17
17
|
# @return [RobotLab::Network]
|
|
18
|
+
# :reek:TooManyStatements -- gathers shared robot inputs then declares one network task per pipeline step
|
|
18
19
|
def build_pipeline_network(config, namer)
|
|
19
20
|
prompts = config.pipeline
|
|
20
21
|
run_config = RobotFactory.build_run_config(config)
|
|
@@ -66,13 +67,15 @@ module AIA
|
|
|
66
67
|
# @param config [AIA::Config]
|
|
67
68
|
# @param namer [RobotNamer]
|
|
68
69
|
# @return [RobotLab::Network]
|
|
70
|
+
# :reek:TooManyStatements -- gathers shared robot inputs, declares one task per roster member plus the synthesizer
|
|
69
71
|
def build_consensus_network(config, namer)
|
|
70
72
|
run_config = RobotFactory.build_run_config(config)
|
|
71
|
-
tools
|
|
72
|
-
mcp
|
|
73
|
-
|
|
73
|
+
tools = ToolLoader.filtered_tools(config)
|
|
74
|
+
mcp = RobotFactory.mcp_server_configs(config)
|
|
75
|
+
models = config.models
|
|
76
|
+
primary = models.first
|
|
74
77
|
aia_config = config
|
|
75
|
-
roster =
|
|
78
|
+
roster = models.map { |spec| { name: namer.name_for(spec.name), spec: spec } }
|
|
76
79
|
build_robot = ->(entry) { build_roster_robot(entry, roster, tools, mcp, run_config, aia_config) }
|
|
77
80
|
|
|
78
81
|
RobotLab.create_network(name: "aia-consensus") do
|
|
@@ -89,11 +92,12 @@ module AIA
|
|
|
89
92
|
config: run_config
|
|
90
93
|
)
|
|
91
94
|
task :consensus, synthesizer,
|
|
92
|
-
depends_on:
|
|
95
|
+
depends_on: models.map { |s| s.internal_id.to_sym }
|
|
93
96
|
end
|
|
94
97
|
end
|
|
95
98
|
|
|
96
99
|
# Shared helper: builds a single roster robot (used by parallel and consensus networks).
|
|
100
|
+
# :reek:LongParameterList { max_params: 6 } -- pass-through of the shared per-network build inputs
|
|
97
101
|
def build_roster_robot(entry, roster, tools, mcp, run_config, aia_config)
|
|
98
102
|
spec = entry[:spec]
|
|
99
103
|
identity = SystemPromptAssembler.build_identity_prompt(entry[:name], spec, roster)
|
|
@@ -116,6 +120,7 @@ module AIA
|
|
|
116
120
|
# @param namer [RobotNamer]
|
|
117
121
|
# @param server_groups [Array<Array<Hash>>]
|
|
118
122
|
# @return [RobotLab::Network]
|
|
123
|
+
# :reek:TooManyStatements -- gathers shared robot inputs, declares one task per server group plus the synthesizer
|
|
119
124
|
def build_concurrent_mcp_network(config, namer, server_groups)
|
|
120
125
|
run_config = RobotFactory.build_run_config(config)
|
|
121
126
|
tools = ToolLoader.filtered_tools(config)
|
|
@@ -16,12 +16,12 @@ module AIA
|
|
|
16
16
|
def initialize_memory(network, config)
|
|
17
17
|
return network unless network.respond_to?(:memory)
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
data = network.memory.data
|
|
20
|
+
data.session_id = SecureRandom.hex(8)
|
|
21
|
+
data.model_count = config.models.size
|
|
22
|
+
data.model_names = config.models.map(&:name)
|
|
23
|
+
data.mode = config.flags.consensus ? :consensus : :parallel
|
|
24
|
+
data.turn_count = 0
|
|
25
25
|
|
|
26
26
|
network
|
|
27
27
|
end
|
|
@@ -25,6 +25,8 @@ module AIA
|
|
|
25
25
|
# Process all prompts in the pipeline.
|
|
26
26
|
#
|
|
27
27
|
# @param config [AIA::Config]
|
|
28
|
+
# :reek:TooManyStatements -- shift-based loop so front-matter pipeline edits take effect; tracking interleaved
|
|
29
|
+
# :reek:DuplicateMethodCall -- config.pipeline is deliberately re-read each iteration because prompt front matter mutates it mid-loop
|
|
28
30
|
def process(config)
|
|
29
31
|
bridge = TrakFlowBridge.new
|
|
30
32
|
tracking = bridge.available? && config.flags.track_pipeline
|
|
@@ -82,11 +84,14 @@ module AIA
|
|
|
82
84
|
end
|
|
83
85
|
end
|
|
84
86
|
|
|
87
|
+
# :reek:TooManyStatements -- image request assembly, spinner-wrapped generation, then save/display of the result
|
|
85
88
|
def generate_image(prompt_text, config)
|
|
86
89
|
img_cfg = config.image
|
|
90
|
+
quality = img_cfg.quality
|
|
91
|
+
style = img_cfg.style
|
|
87
92
|
params = {}
|
|
88
|
-
params[:quality] =
|
|
89
|
-
params[:style] =
|
|
93
|
+
params[:quality] = quality if quality && !quality.to_s.strip.empty?
|
|
94
|
+
params[:style] = style if style && !style.to_s.strip.empty?
|
|
90
95
|
|
|
91
96
|
image = @ui.with_spinner("Generating image") do
|
|
92
97
|
RubyLLM.paint(
|
|
@@ -124,14 +129,16 @@ module AIA
|
|
|
124
129
|
end
|
|
125
130
|
|
|
126
131
|
# Check if concurrent MCP mode should be used
|
|
132
|
+
# :reek:TooManyStatements -- ordered eligibility gates (server count, auto/forced, discovery, grouping threshold) before the build
|
|
127
133
|
def maybe_use_concurrent_mcp(prompt_text, config)
|
|
128
134
|
return nil unless (config.mcp_servers || []).size > 1
|
|
129
135
|
|
|
136
|
+
turn_state = AIA.turn_state
|
|
130
137
|
concurrency = config.respond_to?(:concurrency) ? config.concurrency : nil
|
|
131
|
-
return nil unless concurrency&.auto ||
|
|
138
|
+
return nil unless concurrency&.auto || turn_state.force_concurrent_mcp
|
|
132
139
|
|
|
133
|
-
if
|
|
134
|
-
|
|
140
|
+
if turn_state.force_concurrent_mcp
|
|
141
|
+
turn_state.force_concurrent_mcp = false
|
|
135
142
|
end
|
|
136
143
|
|
|
137
144
|
discovery = MCPDiscovery.new
|
|
@@ -151,13 +158,15 @@ module AIA
|
|
|
151
158
|
end
|
|
152
159
|
|
|
153
160
|
# Build prompt text from a prompt_id
|
|
161
|
+
# :reek:TooManyStatements -- layered prompt assembly: parameters, role prefix, skills, stdin, context files, in a fixed order
|
|
154
162
|
def build_prompt_text(prompt_id, config)
|
|
155
163
|
parsed = @prompt_handler.fetch_prompt(prompt_id)
|
|
156
164
|
return nil unless parsed
|
|
157
165
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
values
|
|
166
|
+
parameters = parsed.respond_to?(:parameters) ? parsed.parameters : nil
|
|
167
|
+
if parameters && !parameters.empty?
|
|
168
|
+
values = @input_collector.collect(parameters)
|
|
169
|
+
values.each { |k, v| parameters[k] = v }
|
|
161
170
|
end
|
|
162
171
|
|
|
163
172
|
prompt_text = parsed.to_s
|
|
@@ -180,8 +189,9 @@ module AIA
|
|
|
180
189
|
prompt_text = "#{prompt_text}\n\n#{skill_content}" if skill_content
|
|
181
190
|
end
|
|
182
191
|
|
|
183
|
-
|
|
184
|
-
|
|
192
|
+
stdin_content = config.stdin_content
|
|
193
|
+
if stdin_content && !stdin_content.strip.empty?
|
|
194
|
+
prompt_text = "#{prompt_text}\n\n#{stdin_content}"
|
|
185
195
|
config.stdin_content = nil
|
|
186
196
|
end
|
|
187
197
|
|
data/lib/aia/plugin_monitor.rb
CHANGED
|
@@ -13,6 +13,7 @@ module AIA
|
|
|
13
13
|
# CPU). If `listen` cannot be loaded, it falls back to a background thread that
|
|
14
14
|
# polls file signatures ([mtime, size]). Either way the same #react logic drives
|
|
15
15
|
# {PluginLoader}, so behavior is identical — only latency differs.
|
|
16
|
+
# :reek:TooManyInstanceVariables -- holds both backends' state (listener vs polling thread) plus watch config and callbacks
|
|
16
17
|
class PluginMonitor
|
|
17
18
|
DEFAULT_INTERVAL = 1.0 # seconds (polling fallback only)
|
|
18
19
|
|
|
@@ -83,9 +84,11 @@ module AIA
|
|
|
83
84
|
#
|
|
84
85
|
# @return [Array<Array(Symbol, String)>] e.g. [[:added, "/p/foo.rb"], ...]
|
|
85
86
|
def detect_changes(old_snapshot, new_snapshot)
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
old_keys = old_snapshot.keys
|
|
88
|
+
new_keys = new_snapshot.keys
|
|
89
|
+
added = (new_keys - old_keys).map { |path| [:added, path] }
|
|
90
|
+
removed = (old_keys - new_keys).map { |path| [:removed, path] }
|
|
91
|
+
modified = (old_keys & new_keys)
|
|
89
92
|
.reject { |path| new_snapshot[path] == old_snapshot[path] }
|
|
90
93
|
.map { |path| [:modified, path] }
|
|
91
94
|
added + removed + modified
|
|
@@ -115,9 +118,9 @@ module AIA
|
|
|
115
118
|
# Map a listen callback into per-file reactions, ignoring anything that is
|
|
116
119
|
# not a top-level file of the plugins directory (listen watches recursively).
|
|
117
120
|
def handle_listen_events(modified, added, removed)
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
+
{ added: added, modified: modified, removed: removed }.each do |event, paths|
|
|
122
|
+
Array(paths).each { |path| react(event, path) if top_level?(path) }
|
|
123
|
+
end
|
|
121
124
|
end
|
|
122
125
|
|
|
123
126
|
# listen reports resolved (symlink-free) paths, and on macOS the plugins
|
|
@@ -75,7 +75,9 @@ module AIA
|
|
|
75
75
|
#
|
|
76
76
|
# @param prompt [String] the user's prompt
|
|
77
77
|
# @return [Array<String>] sub-task descriptions (empty if not decomposable)
|
|
78
|
+
# :reek:TooManyStatements -- schema vs JSON-fallback probe setup, run, extraction, and rescue reporting in one flow
|
|
78
79
|
def decompose(prompt)
|
|
80
|
+
logger = AIA.logger
|
|
79
81
|
probe = build_probe_robot
|
|
80
82
|
use_schema = structured_output?
|
|
81
83
|
|
|
@@ -83,17 +85,17 @@ module AIA
|
|
|
83
85
|
probe.with_schema(SUBTASKS_SCHEMA)
|
|
84
86
|
full_prompt = DECOMPOSITION_PROMPT % { prompt: prompt }
|
|
85
87
|
else
|
|
86
|
-
|
|
88
|
+
logger.warn("PromptDecomposer: configured model does not support structured output — using JSON prompt fallback")
|
|
87
89
|
full_prompt = (DECOMPOSITION_PROMPT + FALLBACK_JSON_INSTRUCTION) % { prompt: prompt }
|
|
88
90
|
end
|
|
89
91
|
|
|
90
92
|
result = probe.run(full_prompt, mcp: :none, tools: :none)
|
|
91
93
|
content = extract_content(result)
|
|
92
|
-
|
|
94
|
+
logger.debug("PromptDecomposer#decompose content class=#{content.class}")
|
|
93
95
|
subtasks = extract_subtasks(content)
|
|
94
96
|
subtasks.is_a?(Array) ? subtasks.select { |t| t.is_a?(String) && !t.empty? } : []
|
|
95
97
|
rescue StandardError => e
|
|
96
|
-
|
|
98
|
+
logger.warn("PromptDecomposer#decompose failed: #{e.class}: #{e.message}")
|
|
97
99
|
[]
|
|
98
100
|
end
|
|
99
101
|
|
data/lib/aia/prompt_handler.rb
CHANGED
|
@@ -4,6 +4,7 @@ require 'pm'
|
|
|
4
4
|
require 'erb'
|
|
5
5
|
|
|
6
6
|
module AIA
|
|
7
|
+
# :reek:TooManyMethods -- prompt/role fetching, front-matter merging, and fzf search form one lookup pipeline of small named steps
|
|
7
8
|
class PromptHandler
|
|
8
9
|
include AIA::SkillUtils
|
|
9
10
|
|
|
@@ -65,11 +66,12 @@ module AIA
|
|
|
65
66
|
return handle_missing_role("roles/") if role_id.nil?
|
|
66
67
|
return fetch_role_from_path(role_id) if path_based_id?(role_id)
|
|
67
68
|
|
|
68
|
-
|
|
69
|
-
|
|
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}"
|
|
70
72
|
end
|
|
71
73
|
|
|
72
|
-
role_file_path = File.join(@prompts_dir, "#{role_id}#{
|
|
74
|
+
role_file_path = File.join(@prompts_dir, "#{role_id}#{prompts_cfg.extname}")
|
|
73
75
|
|
|
74
76
|
parsed = if File.exist?(role_file_path)
|
|
75
77
|
# PM.parse(role_id) cannot resolve subdirectory IDs like "roles/jersey_mike"
|
|
@@ -171,56 +173,47 @@ module AIA
|
|
|
171
173
|
end
|
|
172
174
|
|
|
173
175
|
# Apply root-level shorthand keys to AIA.config
|
|
174
|
-
#
|
|
176
|
+
# :reek:TooManyStatements -- one guarded assignment per supported front-matter shorthand key
|
|
175
177
|
def apply_root_shorthands(meta_hash)
|
|
176
|
-
|
|
177
|
-
model_val = meta_hash['model'] || meta_hash[:model]
|
|
178
|
-
if model_val
|
|
179
|
-
AIA.config.models = [model_val]
|
|
180
|
-
end
|
|
178
|
+
cfg = AIA.config
|
|
181
179
|
|
|
182
|
-
#
|
|
183
|
-
|
|
184
|
-
if
|
|
185
|
-
AIA.config.llm.temperature = temp_val
|
|
186
|
-
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
|
|
187
183
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
if top_p_val
|
|
191
|
-
AIA.config.llm.top_p = top_p_val
|
|
192
|
-
end
|
|
184
|
+
temp_val = shorthand(meta_hash, :temperature)
|
|
185
|
+
cfg.llm.temperature = temp_val if temp_val
|
|
193
186
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
if next_val
|
|
197
|
-
if AIA.config.pipeline.any?
|
|
198
|
-
logger.info "Prompt metadata 'next: #{next_val}' overrides remaining pipeline #{AIA.config.pipeline.inspect}"
|
|
199
|
-
end
|
|
200
|
-
AIA.config.pipeline = [next_val]
|
|
201
|
-
end
|
|
187
|
+
top_p_val = shorthand(meta_hash, :top_p)
|
|
188
|
+
cfg.llm.top_p = top_p_val if top_p_val
|
|
202
189
|
|
|
203
|
-
# pipeline →
|
|
204
|
-
|
|
205
|
-
if
|
|
206
|
-
if AIA.config.pipeline.any?
|
|
207
|
-
logger.info "Prompt metadata 'pipeline' overrides remaining pipeline #{AIA.config.pipeline.inspect}"
|
|
208
|
-
end
|
|
209
|
-
AIA.config.pipeline = Array(pipeline_val)
|
|
210
|
-
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
|
|
211
193
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
194
|
+
pipeline_val = shorthand(meta_hash, :pipeline)
|
|
195
|
+
apply_pipeline_shorthand(cfg, Array(pipeline_val), "pipeline") if pipeline_val
|
|
196
|
+
|
|
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?
|
|
203
|
+
end
|
|
217
204
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
|
222
216
|
end
|
|
223
|
-
# rubocop:enable Metrics/AbcSize
|
|
224
217
|
|
|
225
218
|
def logger
|
|
226
219
|
@logger ||= LoggerManager.aia_logger
|
|
@@ -353,8 +346,9 @@ module AIA
|
|
|
353
346
|
end
|
|
354
347
|
|
|
355
348
|
def search_role_id_with_fzf(initial_query)
|
|
356
|
-
|
|
357
|
-
|
|
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) }
|
|
358
352
|
fzf = AIA::Fzf.new(
|
|
359
353
|
list: role_files,
|
|
360
354
|
directory: @prompts_dir,
|
|
@@ -369,8 +363,8 @@ module AIA
|
|
|
369
363
|
raise "No role ID selected"
|
|
370
364
|
end
|
|
371
365
|
|
|
372
|
-
unless role.start_with?(
|
|
373
|
-
role =
|
|
366
|
+
unless role.start_with?(prompts_cfg.roles_prefix)
|
|
367
|
+
role = prompts_cfg.roles_prefix + '/' + role
|
|
374
368
|
end
|
|
375
369
|
|
|
376
370
|
role
|
data/lib/aia/robot_factory.rb
CHANGED
|
@@ -14,6 +14,7 @@ require_relative 'network_builder'
|
|
|
14
14
|
require_relative 'history_transfer'
|
|
15
15
|
|
|
16
16
|
module AIA
|
|
17
|
+
# :reek:TooManyMethods -- factory facade: every robot/network build variant plus its run-config helpers lives behind one entry point
|
|
17
18
|
class RobotFactory
|
|
18
19
|
# The max_tokens default from defaults.yml — optimised for cloud APIs that treat
|
|
19
20
|
# it as an output-only cap. Local providers (Ollama, LM Studio) bound input+output
|
|
@@ -114,9 +115,10 @@ module AIA
|
|
|
114
115
|
|
|
115
116
|
# Reuse the build-time tool cache when present, else load tools fresh.
|
|
116
117
|
def apply_tool_cache(config)
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
config.
|
|
118
|
+
cached = ToolLoader.cached_tools
|
|
119
|
+
if cached
|
|
120
|
+
config.loaded_tools = cached
|
|
121
|
+
config.tool_names = cached.map { |t| t.respond_to?(:name) ? t.name : t.class.name }.join(', ')
|
|
120
122
|
else
|
|
121
123
|
ToolLoader.load_tools(config)
|
|
122
124
|
end
|
|
@@ -141,14 +143,16 @@ module AIA
|
|
|
141
143
|
# - max_tokens remains a RobotLab::RunConfig field; AIA normalizes it at
|
|
142
144
|
# the RubyLLM/OpenAI provider boundary for models that need
|
|
143
145
|
# max_completion_tokens
|
|
146
|
+
# :reek:TooManyStatements -- one guarded param per LLM tuning knob, with provider-specific max_tokens handling
|
|
144
147
|
def build_run_config(config)
|
|
145
148
|
model_spec = config.models.first
|
|
149
|
+
llm = config.llm
|
|
146
150
|
params = {}
|
|
147
151
|
|
|
148
|
-
temp =
|
|
152
|
+
temp = llm.temperature
|
|
149
153
|
params[:temperature] = temp if temp && model_supports_temperature?(model_spec)
|
|
150
154
|
|
|
151
|
-
max =
|
|
155
|
+
max = llm.max_tokens
|
|
152
156
|
# Local providers (Ollama, LM Studio) have a small total context window
|
|
153
157
|
# (input + output combined, typically 32768 tokens for qwen3-class models).
|
|
154
158
|
# Sending the cloud default of 32767 as num_predict leaves almost no room
|
|
@@ -158,13 +162,13 @@ module AIA
|
|
|
158
162
|
local = model_spec&.local_provider?
|
|
159
163
|
params[:max_tokens] = max if max && !(local && max == DEFAULT_LOCAL_MAX_TOKENS)
|
|
160
164
|
|
|
161
|
-
tp =
|
|
165
|
+
tp = llm.top_p
|
|
162
166
|
params[:top_p] = tp if tp
|
|
163
167
|
|
|
164
|
-
fp =
|
|
168
|
+
fp = llm.frequency_penalty
|
|
165
169
|
params[:frequency_penalty] = fp if fp && fp != 0.0
|
|
166
170
|
|
|
167
|
-
pp =
|
|
171
|
+
pp = llm.presence_penalty
|
|
168
172
|
params[:presence_penalty] = pp if pp && pp != 0.0
|
|
169
173
|
|
|
170
174
|
RobotLab::RunConfig.new(**params)
|
data/lib/aia/robot_namer.rb
CHANGED
|
@@ -103,14 +103,8 @@ module AIA
|
|
|
103
103
|
end
|
|
104
104
|
|
|
105
105
|
def unique_name(base)
|
|
106
|
-
@used_names[base]
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if @used_names[base] == 1
|
|
110
|
-
base
|
|
111
|
-
else
|
|
112
|
-
"#{base}#{@used_names[base]}"
|
|
113
|
-
end
|
|
106
|
+
count = @used_names[base] = (@used_names[base] || 0) + 1
|
|
107
|
+
count == 1 ? base : "#{base}#{count}"
|
|
114
108
|
end
|
|
115
109
|
end
|
|
116
110
|
end
|
data/lib/aia/session.rb
CHANGED
|
@@ -23,6 +23,7 @@ require_relative "tool_filter_registry"
|
|
|
23
23
|
require_relative "chat_loop"
|
|
24
24
|
|
|
25
25
|
module AIA
|
|
26
|
+
# :reek:TooManyInstanceVariables -- session hub wires presenter, trackers, handlers, and robot; each collaborator is a named field
|
|
26
27
|
class Session
|
|
27
28
|
include ContentExtractor
|
|
28
29
|
|
|
@@ -35,14 +36,17 @@ module AIA
|
|
|
35
36
|
|
|
36
37
|
# Starts the session, processing all prompts in the pipeline and then
|
|
37
38
|
# optionally starting an interactive chat session.
|
|
39
|
+
# :reek:TooManyStatements -- session boot script: build robot, startup coordination, optional immediate chat, pipeline run, chat handoff
|
|
38
40
|
def start
|
|
41
|
+
cfg = AIA.config
|
|
42
|
+
|
|
39
43
|
# Build robot or network
|
|
40
|
-
@robot = RobotFactory.build(
|
|
44
|
+
@robot = RobotFactory.build(cfg)
|
|
41
45
|
AIA.client = @robot
|
|
42
46
|
|
|
43
47
|
# Run all startup coordination: MCP, tools, filters, task board, bus
|
|
44
48
|
coordinator = StartupCoordinator.new(robot: @robot, ui_presenter: @ui_presenter)
|
|
45
|
-
coordinator.run(
|
|
49
|
+
coordinator.run(cfg)
|
|
46
50
|
@filters = coordinator.filters
|
|
47
51
|
@mcp_manager = coordinator.mcp_manager
|
|
48
52
|
|
|
@@ -63,7 +67,7 @@ module AIA
|
|
|
63
67
|
input_collector: @input_collector,
|
|
64
68
|
ui_presenter: @ui_presenter,
|
|
65
69
|
session_tracker: @session_tracker
|
|
66
|
-
).process(
|
|
70
|
+
).process(cfg)
|
|
67
71
|
|
|
68
72
|
# Start chat mode after all prompts are processed
|
|
69
73
|
return unless AIA.chat?
|
data/lib/aia/session_tracker.rb
CHANGED
|
@@ -97,61 +97,26 @@ module AIA
|
|
|
97
97
|
|
|
98
98
|
# Expand a network SimpleFlow::Result into one turn entry per robot.
|
|
99
99
|
# Computes TF-IDF similarity of each response against the first.
|
|
100
|
-
#
|
|
100
|
+
# :reek:TooManyStatements -- collect entries and texts in one pass, then score and accumulate; splitting hides the pairing
|
|
101
101
|
def record_network_turn(input:, flow_result:, decisions: nil, elapsed: nil)
|
|
102
102
|
@turn_count += 1
|
|
103
103
|
now = Time.now
|
|
104
104
|
|
|
105
105
|
# Collect robot data in order for similarity scoring
|
|
106
|
-
robot_entries
|
|
106
|
+
robot_entries = []
|
|
107
107
|
response_texts = []
|
|
108
108
|
|
|
109
|
-
# rubocop:disable Metrics/BlockLength
|
|
110
109
|
flow_result.context.each do |task_name, robot_result|
|
|
111
110
|
next if task_name == :run_params
|
|
112
111
|
next unless robot_result.respond_to?(:raw)
|
|
113
112
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
tokens = input_tokens + output_tokens
|
|
118
|
-
|
|
119
|
-
model_id = extract_model_id_from_raw(raw)
|
|
120
|
-
model_id ||= robot_result.respond_to?(:robot_name) ? robot_result.robot_name : task_name.to_s
|
|
121
|
-
|
|
122
|
-
cost = tokens.positive? ? compute_cost_for_model(model_id, input_tokens, output_tokens) : 0.0
|
|
123
|
-
robot_elapsed = robot_result.respond_to?(:duration) ? (robot_result.duration || 0) : 0
|
|
124
|
-
|
|
125
|
-
text = if robot_result.respond_to?(:reply)
|
|
126
|
-
robot_result.reply.to_s
|
|
127
|
-
elsif robot_result.respond_to?(:content)
|
|
128
|
-
robot_result.content.to_s
|
|
129
|
-
else
|
|
130
|
-
""
|
|
131
|
-
end
|
|
132
|
-
# rubocop:enable Metrics/BlockLength
|
|
133
|
-
response_texts << text
|
|
134
|
-
|
|
135
|
-
robot_entries << {
|
|
136
|
-
model: model_id,
|
|
137
|
-
input_length: input.to_s.length,
|
|
138
|
-
input_tokens: input_tokens,
|
|
139
|
-
output_tokens: output_tokens,
|
|
140
|
-
tokens: tokens,
|
|
141
|
-
cost: cost,
|
|
142
|
-
elapsed: robot_elapsed,
|
|
143
|
-
decisions: decisions&.to_h,
|
|
144
|
-
timestamp: now
|
|
145
|
-
}
|
|
113
|
+
response_texts << extract_response_text(robot_result)
|
|
114
|
+
robot_entries << network_robot_entry(task_name, robot_result,
|
|
115
|
+
input: input, decisions: decisions, now: now)
|
|
146
116
|
end
|
|
147
|
-
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
148
117
|
|
|
149
118
|
# Compute similarity scores (first model is reference)
|
|
150
|
-
scores =
|
|
151
|
-
SimilarityScorer.score(response_texts)
|
|
152
|
-
else
|
|
153
|
-
Array.new(robot_entries.size)
|
|
154
|
-
end
|
|
119
|
+
scores = robot_entries.size > 1 ? SimilarityScorer.score(response_texts) : Array.new(robot_entries.size)
|
|
155
120
|
|
|
156
121
|
robot_entries.each_with_index do |entry, i|
|
|
157
122
|
entry[:similarity] = scores[i]
|
|
@@ -161,6 +126,39 @@ module AIA
|
|
|
161
126
|
end
|
|
162
127
|
end
|
|
163
128
|
|
|
129
|
+
# One turn entry for a single robot's result within a network flow.
|
|
130
|
+
def network_robot_entry(task_name, robot_result, input:, decisions:, now:)
|
|
131
|
+
raw = robot_result.raw
|
|
132
|
+
input_tokens = (raw.respond_to?(:input_tokens) && raw.input_tokens) || 0
|
|
133
|
+
output_tokens = (raw.respond_to?(:output_tokens) && raw.output_tokens) || 0
|
|
134
|
+
tokens = input_tokens + output_tokens
|
|
135
|
+
|
|
136
|
+
model_id = extract_model_id_from_raw(raw)
|
|
137
|
+
model_id ||= robot_result.respond_to?(:robot_name) ? robot_result.robot_name : task_name.to_s
|
|
138
|
+
|
|
139
|
+
{
|
|
140
|
+
model: model_id,
|
|
141
|
+
input_length: input.to_s.length,
|
|
142
|
+
input_tokens: input_tokens,
|
|
143
|
+
output_tokens: output_tokens,
|
|
144
|
+
tokens: tokens,
|
|
145
|
+
cost: tokens.positive? ? compute_cost_for_model(model_id, input_tokens, output_tokens) : 0.0,
|
|
146
|
+
elapsed: robot_result.respond_to?(:duration) ? (robot_result.duration || 0) : 0,
|
|
147
|
+
decisions: decisions&.to_h,
|
|
148
|
+
timestamp: now
|
|
149
|
+
}
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def extract_response_text(robot_result)
|
|
153
|
+
if robot_result.respond_to?(:reply)
|
|
154
|
+
robot_result.reply.to_s
|
|
155
|
+
elsif robot_result.respond_to?(:content)
|
|
156
|
+
robot_result.content.to_s
|
|
157
|
+
else
|
|
158
|
+
""
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
164
162
|
def extract_model_id_from_raw(raw)
|
|
165
163
|
return nil unless raw
|
|
166
164
|
return raw.model_id if raw.respond_to?(:model_id) && raw.model_id
|