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/aia.gemspec
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'lib/aia/version'
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = 'aia'
|
|
7
|
+
spec.version = AIA::VERSION
|
|
8
|
+
spec.authors = ['Dewayne VanHoozer']
|
|
9
|
+
spec.email = ['dvanhoozer@gmail.com']
|
|
10
|
+
|
|
11
|
+
spec.summary = 'Multi-model AI CLI with dynamic prompts, consensus responses, shell & Ruby integration, and seamless chat workflows.'
|
|
12
|
+
spec.description = <<~DESC
|
|
13
|
+
AIA is a powerful CLI console application that brings multi-model AI capabilities to your command line, supporting 20+ providers including OpenAI, Anthropic, and Google. Built on robot_lab for robust robot orchestration, AIA v2 provides a thin CLI shell over a rich execution engine. Run multiple AI models simultaneously for comparison, get consensus responses from collaborative AI teams, or compare individual outputs side-by-side. With dynamic prompt management, embedded directives, shell and Ruby integration, interactive chats, and comprehensive history tracking, AIA transforms how you interact with AI.
|
|
14
|
+
DESC
|
|
15
|
+
|
|
16
|
+
spec.homepage = 'https://github.com/MadBomber/aia'
|
|
17
|
+
spec.license = 'MIT'
|
|
18
|
+
|
|
19
|
+
spec.required_ruby_version = '>= 4.0.0'
|
|
20
|
+
|
|
21
|
+
spec.metadata['allowed_push_host'] = 'https://rubygems.org'
|
|
22
|
+
|
|
23
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
|
24
|
+
spec.metadata['source_code_uri'] = spec.homepage
|
|
25
|
+
spec.metadata['changelog_uri'] = spec.homepage
|
|
26
|
+
spec.metadata['rubygems_mfa_required'] = 'true'
|
|
27
|
+
|
|
28
|
+
# Specify which files should be added to the gem when it is released.
|
|
29
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
|
30
|
+
spec.files = Dir.chdir(__dir__) do
|
|
31
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
|
32
|
+
(File.expand_path(f) == __FILE__) ||
|
|
33
|
+
f.start_with?(*%w[bin/ test/ spec/ features/ .git Gemfile])
|
|
34
|
+
end + ['.version']
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
spec.bindir = 'bin'
|
|
38
|
+
spec.executables = %w[aia]
|
|
39
|
+
spec.require_paths = %w[lib]
|
|
40
|
+
|
|
41
|
+
# Pin bigdecimal to 4.x — Ruby 4 ships it as a default gem, but some transitive
|
|
42
|
+
# deps declare >= 3.0 which can cause RubyGems to activate 3.x first.
|
|
43
|
+
spec.add_dependency 'bigdecimal', '>= 4.0'
|
|
44
|
+
|
|
45
|
+
# Core dependencies
|
|
46
|
+
spec.add_dependency 'robot_lab', '~> 0.2' # Execution engine: robots, networks, tools, MCP, memory
|
|
47
|
+
spec.add_dependency 'myway_config' # AIA-specific config (CLI settings, prompts, UI)
|
|
48
|
+
spec.add_dependency 'prompt_manager', '~> 1.0.2' # Prompt parsing/directive DSL
|
|
49
|
+
spec.add_dependency 'lumberjack', '~> 2.1' # Structured logging, 3 loggers (aia, llm, mcp); 2.x API (LogFile shift_age/autoflush)
|
|
50
|
+
spec.add_dependency 'activesupport' # Required by robot_lab (missing from its gemspec)
|
|
51
|
+
spec.add_dependency 'simple_flow'
|
|
52
|
+
spec.add_dependency 'trak_flow'
|
|
53
|
+
|
|
54
|
+
# CLI & UI
|
|
55
|
+
spec.add_dependency 'reline' # Interactive chat input with history
|
|
56
|
+
spec.add_dependency 'tty-screen' # Terminal width detection
|
|
57
|
+
spec.add_dependency 'tty-spinner' # Loading animation and concurrent MCP connection spinners
|
|
58
|
+
spec.add_dependency 'tty-table', '~> 0.12' # Adaptive terminal-width table rendering for metrics
|
|
59
|
+
spec.add_dependency 'classifier', '~> 2.3' # TF-IDF similarity (Option A tool filtering)
|
|
60
|
+
spec.add_dependency 'word_wrapper' # Terminal text wrapping for tool listings
|
|
61
|
+
spec.add_dependency 'amazing_print' # Config dump formatting (--dump, /config)
|
|
62
|
+
spec.add_dependency 'clipboard' # System clipboard access (/paste directive)
|
|
63
|
+
|
|
64
|
+
# Utilities
|
|
65
|
+
spec.add_dependency 'faraday' # HTTP client for /webpage directive
|
|
66
|
+
spec.add_dependency 'listen' # File-watch for live plugin reload (polling fallback if absent)
|
|
67
|
+
spec.add_dependency 'shellwords' # Shell escaping
|
|
68
|
+
|
|
69
|
+
spec.add_development_dependency 'debug_me'
|
|
70
|
+
spec.add_development_dependency 'minitest'
|
|
71
|
+
spec.add_development_dependency 'minitest-reporters'
|
|
72
|
+
spec.add_development_dependency 'mocha'
|
|
73
|
+
spec.add_development_dependency 'rake'
|
|
74
|
+
spec.add_development_dependency 'simplecov'
|
|
75
|
+
spec.add_development_dependency 'simplecov_lcov_formatter'
|
|
76
|
+
spec.add_development_dependency 'tocer'
|
|
77
|
+
spec.add_development_dependency 'webmock'
|
|
78
|
+
|
|
79
|
+
spec.post_install_message = <<~MSG
|
|
80
|
+
|
|
81
|
+
╔══════════════════════════════════════════════════════════════╗
|
|
82
|
+
║ AIA — AI Assistant v2.0 ║
|
|
83
|
+
║ ║
|
|
84
|
+
║ v2 is powered by robot_lab for robust orchestration ║
|
|
85
|
+
║ Full CLI backward compatibility with v1 ║
|
|
86
|
+
╚══════════════════════════════════════════════════════════════╝
|
|
87
|
+
|
|
88
|
+
Get started: aia --help
|
|
89
|
+
Full docs: https://madbomber.github.io/aia
|
|
90
|
+
|
|
91
|
+
MSG
|
|
92
|
+
end
|
data/config/aia.yml
ADDED
data/lib/aia/chat_loop.rb
CHANGED
|
@@ -11,8 +11,11 @@ require "reline"
|
|
|
11
11
|
require "pm"
|
|
12
12
|
|
|
13
13
|
module AIA
|
|
14
|
+
# :reek:TooManyInstanceVariables -- interactive-loop hub wires robot, presenter, tracker, routers, and handlers together by design
|
|
15
|
+
# :reek:TooManyMethods -- one private helper per loop concern (context, metrics, speech, history)
|
|
14
16
|
class ChatLoop
|
|
15
17
|
include ContentExtractor
|
|
18
|
+
include Speech
|
|
16
19
|
|
|
17
20
|
def initialize(robot, ui_presenter, directive_processor,
|
|
18
21
|
session_tracker: nil, alias_registry: nil, filters: {})
|
|
@@ -42,6 +45,7 @@ module AIA
|
|
|
42
45
|
end
|
|
43
46
|
|
|
44
47
|
# Start the interactive chat session
|
|
48
|
+
# :reek:BooleanParameter -- lets Session skip re-sending context files after a pipeline; two entry methods would duplicate rescue/ensure
|
|
45
49
|
def start(skip_context_files: false)
|
|
46
50
|
setup_session
|
|
47
51
|
process_initial_context(skip_context_files)
|
|
@@ -62,9 +66,10 @@ module AIA
|
|
|
62
66
|
end
|
|
63
67
|
|
|
64
68
|
def process_initial_context(skip_context_files)
|
|
65
|
-
|
|
69
|
+
files = AIA.config.context_files
|
|
70
|
+
return if skip_context_files || !files || files.empty?
|
|
66
71
|
|
|
67
|
-
context =
|
|
72
|
+
context = files.map do |file|
|
|
68
73
|
File.read(file) rescue "Error reading file: #{file}"
|
|
69
74
|
end.join("\n\n")
|
|
70
75
|
|
|
@@ -262,6 +267,7 @@ module AIA
|
|
|
262
267
|
# Each robot_result.duration holds the elapsed seconds.
|
|
263
268
|
# Similarity scores compare each model's response text against the
|
|
264
269
|
# first model using TF-IDF cosine similarity.
|
|
270
|
+
# :reek:TooManyStatements -- one pass builds paired metric and similarity arrays; splitting hides the pairing
|
|
265
271
|
def display_network_metrics(flow_result)
|
|
266
272
|
metrics_list = []
|
|
267
273
|
response_texts = []
|
|
@@ -337,55 +343,5 @@ module AIA
|
|
|
337
343
|
|
|
338
344
|
File.open(out_file, "a") { |f| f.puts "\nYou: #{input}" }
|
|
339
345
|
end
|
|
340
|
-
|
|
341
|
-
def speak(content)
|
|
342
|
-
return unless AIA.speak?
|
|
343
|
-
|
|
344
|
-
audio = AIA.config.audio
|
|
345
|
-
command = audio.speak_command || 'say'
|
|
346
|
-
env = {}
|
|
347
|
-
env['SPEECH_MODEL'] = audio.speech_model if audio.speech_model
|
|
348
|
-
|
|
349
|
-
if command == 'say'
|
|
350
|
-
# Local TTS: say converts and plays in one step
|
|
351
|
-
run_with_spinner("Speaking...") do
|
|
352
|
-
if audio.voice && !audio.voice.to_s.strip.empty?
|
|
353
|
-
system(env, command, '-v', audio.voice, content.to_s)
|
|
354
|
-
else
|
|
355
|
-
system(env, command, content.to_s)
|
|
356
|
-
end
|
|
357
|
-
end
|
|
358
|
-
else
|
|
359
|
-
# Custom TTS script: stage 2 = convert text → audio file,
|
|
360
|
-
# stage 3 = play the file. AIA passes the output path as $2.
|
|
361
|
-
require 'tempfile'
|
|
362
|
-
tmpfile = Tempfile.new(['aia-tts-', '.mp3'])
|
|
363
|
-
tmpfile.close
|
|
364
|
-
begin
|
|
365
|
-
run_with_spinner("Converting to audio...") do
|
|
366
|
-
system(env, command, content.to_s, tmpfile.path)
|
|
367
|
-
end
|
|
368
|
-
if File.size?(tmpfile.path)
|
|
369
|
-
run_with_spinner("Speaking...") do
|
|
370
|
-
system('afplay', tmpfile.path)
|
|
371
|
-
end
|
|
372
|
-
end
|
|
373
|
-
ensure
|
|
374
|
-
tmpfile.unlink
|
|
375
|
-
end
|
|
376
|
-
end
|
|
377
|
-
rescue StandardError => e
|
|
378
|
-
$stderr.puts "Warning: Speech failed: #{e.message}"
|
|
379
|
-
end
|
|
380
|
-
|
|
381
|
-
def run_with_spinner(message)
|
|
382
|
-
spinner = TTY::Spinner.new("[:spinner] #{message}", format: :bouncing_ball, output: $stderr)
|
|
383
|
-
spinner.auto_spin
|
|
384
|
-
begin
|
|
385
|
-
yield
|
|
386
|
-
ensure
|
|
387
|
-
spinner.stop
|
|
388
|
-
end
|
|
389
|
-
end
|
|
390
346
|
end
|
|
391
347
|
end
|
|
@@ -56,6 +56,7 @@ module AIA
|
|
|
56
56
|
"aia [options] --chat [PROMPT_ID] [CONTEXT_FILE]*"
|
|
57
57
|
end
|
|
58
58
|
|
|
59
|
+
# :reek:TooManyStatements -- one opts.on registration per CLI flag; splitting the group would scatter related flags
|
|
59
60
|
def setup_mode_options(opts, options)
|
|
60
61
|
opts.separator "\nMode Options:"
|
|
61
62
|
|
|
@@ -96,6 +97,7 @@ module AIA
|
|
|
96
97
|
end
|
|
97
98
|
end
|
|
98
99
|
|
|
100
|
+
# :reek:TooManyStatements -- one opts.on registration per CLI flag; splitting the group would scatter related flags
|
|
99
101
|
def setup_model_options(opts, options)
|
|
100
102
|
opts.separator "\nModel Options:"
|
|
101
103
|
|
|
@@ -127,6 +129,7 @@ module AIA
|
|
|
127
129
|
end
|
|
128
130
|
end
|
|
129
131
|
|
|
132
|
+
# :reek:TooManyStatements -- one opts.on registration per CLI flag; splitting the group would scatter related flags
|
|
130
133
|
def setup_file_options(opts, options)
|
|
131
134
|
opts.separator "\nFile & Output Options:"
|
|
132
135
|
|
|
@@ -157,6 +160,8 @@ module AIA
|
|
|
157
160
|
end
|
|
158
161
|
end
|
|
159
162
|
|
|
163
|
+
# :reek:TooManyStatements -- one opts.on registration per CLI flag; splitting the group would scatter related flags
|
|
164
|
+
# :reek:DuplicateMethodCall -- options[:pipeline] is appended inside two separate OptionParser closures
|
|
160
165
|
def setup_prompt_options(opts, options)
|
|
161
166
|
opts.separator "\nPrompt Options:"
|
|
162
167
|
|
|
@@ -228,6 +233,7 @@ module AIA
|
|
|
228
233
|
end
|
|
229
234
|
end
|
|
230
235
|
|
|
236
|
+
# :reek:TooManyStatements -- one opts.on registration per CLI flag; splitting the group would scatter related flags
|
|
231
237
|
def setup_audio_image_options(opts, options)
|
|
232
238
|
opts.separator "\nAudio & Image Options:"
|
|
233
239
|
|
|
@@ -264,6 +270,7 @@ module AIA
|
|
|
264
270
|
end
|
|
265
271
|
end
|
|
266
272
|
|
|
273
|
+
# :reek:TooManyStatements -- one opts.on registration per CLI flag; splitting the group would scatter related flags
|
|
267
274
|
def setup_tool_options(opts, options)
|
|
268
275
|
opts.separator "\nTool & Extension Options:"
|
|
269
276
|
|
|
@@ -303,6 +310,7 @@ module AIA
|
|
|
303
310
|
setup_meta_options(opts, options)
|
|
304
311
|
end
|
|
305
312
|
|
|
313
|
+
# :reek:TooManyStatements -- one opts.on registration per CLI flag; splitting the group would scatter related flags
|
|
306
314
|
def setup_logging_options(opts, options)
|
|
307
315
|
opts.on("--log-level LEVEL", "Set log level (debug|info|warn|error|fatal)") do |level|
|
|
308
316
|
level = level.downcase
|
|
@@ -337,6 +345,7 @@ module AIA
|
|
|
337
345
|
end
|
|
338
346
|
end
|
|
339
347
|
|
|
348
|
+
# :reek:TooManyStatements -- one opts.on registration per CLI flag; splitting the group would scatter related flags
|
|
340
349
|
def setup_output_options(opts, options)
|
|
341
350
|
opts.on("--[no-]thinking", "Show raw thinking/reasoning blocks in output (default: off)") do |v|
|
|
342
351
|
options[:thinking] = v
|
|
@@ -364,6 +373,7 @@ module AIA
|
|
|
364
373
|
end
|
|
365
374
|
end
|
|
366
375
|
|
|
376
|
+
# :reek:TooManyStatements -- one opts.on registration per CLI flag; splitting the group would scatter related flags
|
|
367
377
|
def setup_mcp_options(opts, options)
|
|
368
378
|
opts.on("--mcp FILE", "Load MCP server(s) from JSON file (repeatable)") do |file|
|
|
369
379
|
options[:mcp_files] ||= []
|
|
@@ -440,11 +450,12 @@ module AIA
|
|
|
440
450
|
#
|
|
441
451
|
# @param model_string [String] comma-separated models with optional roles
|
|
442
452
|
# @return [Array<Hash>] array of model specs
|
|
453
|
+
# :reek:TooManyStatements -- one-pass parser over MODEL[=ROLE] specs sharing the instance counter
|
|
443
454
|
def parse_models_with_roles(model_string)
|
|
444
455
|
models = []
|
|
445
456
|
model_counts = Hash.new(0)
|
|
446
457
|
|
|
447
|
-
# rubocop:disable Metrics/BlockLength
|
|
458
|
+
# rubocop:disable-next Metrics/BlockLength
|
|
448
459
|
model_string.split(',').each do |spec|
|
|
449
460
|
spec.strip!
|
|
450
461
|
|
|
@@ -480,46 +491,45 @@ module AIA
|
|
|
480
491
|
}
|
|
481
492
|
end
|
|
482
493
|
end
|
|
483
|
-
# rubocop:enable Metrics/BlockLength
|
|
484
494
|
|
|
485
495
|
models
|
|
486
496
|
end
|
|
487
497
|
|
|
488
498
|
def validate_role_exists(role_id)
|
|
489
|
-
if AIA::SkillUtils.path_based_id?(role_id)
|
|
490
|
-
expanded = File.expand_path(role_id)
|
|
491
|
-
expanded += '.md' if File.extname(expanded).empty?
|
|
492
|
-
raise ArgumentError, "Role file not found: #{expanded}" unless File.exist?(expanded)
|
|
499
|
+
return validate_role_path!(role_id) if AIA::SkillUtils.path_based_id?(role_id)
|
|
493
500
|
|
|
494
|
-
|
|
495
|
-
end
|
|
496
|
-
|
|
497
|
-
prompts_dir = ENV.fetch('AIA_PROMPTS__DIR', File.join(Dir.home, '.prompts'))
|
|
501
|
+
prompts_dir = ENV.fetch('AIA_PROMPTS__DIR', File.join(Dir.home, '.prompts'))
|
|
498
502
|
roles_prefix = ENV.fetch('AIA_PROMPTS__ROLES_PREFIX', 'roles')
|
|
499
|
-
|
|
500
|
-
unless role_id.start_with?(roles_prefix)
|
|
501
|
-
role_id = "#{roles_prefix}/#{role_id}"
|
|
502
|
-
end
|
|
503
|
+
role_id = "#{roles_prefix}/#{role_id}" unless role_id.start_with?(roles_prefix)
|
|
503
504
|
|
|
504
505
|
role_file_path = File.join(prompts_dir, "#{role_id}.md")
|
|
505
|
-
|
|
506
506
|
return if File.exist?(role_file_path)
|
|
507
|
-
available_roles = list_available_role_names(prompts_dir, roles_prefix)
|
|
508
507
|
|
|
509
|
-
|
|
508
|
+
raise ArgumentError, role_not_found_message(role_file_path, prompts_dir, roles_prefix)
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
# A path-based role id must resolve to an existing file.
|
|
512
|
+
def validate_role_path!(role_id)
|
|
513
|
+
expanded = File.expand_path(role_id)
|
|
514
|
+
expanded += '.md' if File.extname(expanded).empty?
|
|
515
|
+
raise ArgumentError, "Role file not found: #{expanded}" unless File.exist?(expanded)
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
def role_not_found_message(role_file_path, prompts_dir, roles_prefix)
|
|
519
|
+
available_roles = list_available_role_names(prompts_dir, roles_prefix)
|
|
520
|
+
message = "Role file not found: #{role_file_path}\n\n"
|
|
510
521
|
|
|
511
522
|
if available_roles.empty?
|
|
512
|
-
|
|
513
|
-
|
|
523
|
+
message + "No roles directory found at #{File.join(prompts_dir, roles_prefix)}\n" \
|
|
524
|
+
"Create the directory and add role files to use this feature."
|
|
514
525
|
else
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
526
|
+
message + "Available roles:\n" +
|
|
527
|
+
available_roles.map { |r| " - #{r}" }.join("\n") +
|
|
528
|
+
"\n\nCreate the role file or use an existing role."
|
|
518
529
|
end
|
|
519
|
-
|
|
520
|
-
raise ArgumentError, error_msg
|
|
521
530
|
end
|
|
522
531
|
|
|
532
|
+
# :reek:TooManyStatements -- sequential terminal report: guards, then a markdown table per role; exits after printing
|
|
523
533
|
def list_available_roles
|
|
524
534
|
prompts_dir = ENV.fetch('AIA_PROMPTS__DIR', File.join(Dir.home, '.prompts'))
|
|
525
535
|
roles_prefix = ENV.fetch('AIA_PROMPTS__ROLES_PREFIX', 'roles')
|
|
@@ -565,48 +575,19 @@ module AIA
|
|
|
565
575
|
.sort
|
|
566
576
|
end
|
|
567
577
|
|
|
568
|
-
#
|
|
578
|
+
# :reek:TooManyStatements -- sequential --available-models report: parse query, print matches, summary, exit
|
|
569
579
|
def list_available_models(query)
|
|
570
580
|
require 'ruby_llm'
|
|
571
581
|
|
|
572
|
-
query =
|
|
573
|
-
|
|
574
|
-
else
|
|
575
|
-
query.split(',')
|
|
576
|
-
end
|
|
577
|
-
# rubocop:enable Metrics/AbcSize
|
|
582
|
+
query = query.nil? ? [] : query.split(',')
|
|
583
|
+
modality_terms, substring_terms = parse_model_query(query)
|
|
578
584
|
|
|
579
585
|
header = "\nAvailable LLMs"
|
|
580
586
|
header += " for #{query.join(' and ')}" if query.any?
|
|
581
|
-
|
|
582
587
|
puts header + ':'
|
|
583
588
|
puts
|
|
584
589
|
|
|
585
|
-
|
|
586
|
-
q2 = query.reject { |q| q.include?('_to_') }
|
|
587
|
-
|
|
588
|
-
counter = 0
|
|
589
|
-
|
|
590
|
-
RubyLLM.models.all.each do |llm|
|
|
591
|
-
inputs = llm.modalities.input.join(',')
|
|
592
|
-
outputs = llm.modalities.output.join(',')
|
|
593
|
-
entry = "- #{llm.id} (#{llm.provider}) #{inputs} to #{outputs}"
|
|
594
|
-
|
|
595
|
-
if query.nil? || query.empty?
|
|
596
|
-
counter += 1
|
|
597
|
-
puts entry
|
|
598
|
-
next
|
|
599
|
-
end
|
|
600
|
-
|
|
601
|
-
show_it = true
|
|
602
|
-
q1.each { |q| show_it &&= llm.modalities.send("#{q}?") }
|
|
603
|
-
q2.each { |q| show_it &&= entry.include?(q) }
|
|
604
|
-
|
|
605
|
-
if show_it
|
|
606
|
-
counter += 1
|
|
607
|
-
puts entry
|
|
608
|
-
end
|
|
609
|
-
end
|
|
590
|
+
counter = print_matching_models(modality_terms, substring_terms)
|
|
610
591
|
|
|
611
592
|
puts if counter.positive?
|
|
612
593
|
puts "#{counter} LLMs matching your query"
|
|
@@ -614,8 +595,36 @@ module AIA
|
|
|
614
595
|
|
|
615
596
|
exit
|
|
616
597
|
end
|
|
598
|
+
|
|
599
|
+
# Print each matching model entry; returns the number printed.
|
|
600
|
+
def print_matching_models(modality_terms, substring_terms)
|
|
601
|
+
RubyLLM.models.all.count do |llm|
|
|
602
|
+
entry = format_model_entry(llm)
|
|
603
|
+
visible = model_entry_visible?(llm, entry, modality_terms, substring_terms)
|
|
604
|
+
puts entry if visible
|
|
605
|
+
visible
|
|
606
|
+
end
|
|
607
|
+
end
|
|
608
|
+
|
|
609
|
+
# Split a query into modality terms (e.g. "text_to_text", leading ':'
|
|
610
|
+
# stripped) and plain substring terms.
|
|
611
|
+
def parse_model_query(query)
|
|
612
|
+
modality, substrings = query.partition { |q| q.include?('_to_') }
|
|
613
|
+
[modality.map { |q| q.delete_prefix(':') }, substrings]
|
|
614
|
+
end
|
|
615
|
+
|
|
616
|
+
def format_model_entry(llm)
|
|
617
|
+
modalities = llm.modalities
|
|
618
|
+
"- #{llm.id} (#{llm.provider}) #{modalities.input.join(',')} to #{modalities.output.join(',')}"
|
|
619
|
+
end
|
|
620
|
+
|
|
621
|
+
def model_entry_visible?(llm, entry, modality_terms, substring_terms)
|
|
622
|
+
modality_terms.all? { |q| llm.modalities.send("#{q}?") } &&
|
|
623
|
+
substring_terms.all? { |q| entry.include?(q) }
|
|
624
|
+
end
|
|
617
625
|
# rubocop:enable Metrics/ModuleLength
|
|
618
626
|
|
|
627
|
+
# :reek:DuplicateMethodCall -- each `exit 1` terminates a distinct validation failure; there is no value to hoist
|
|
619
628
|
def process_tools_paths(path_list)
|
|
620
629
|
paths = []
|
|
621
630
|
|
|
@@ -45,6 +45,7 @@ module AIA
|
|
|
45
45
|
#
|
|
46
46
|
# @param file_paths [Array<String>] paths to JSON configuration files
|
|
47
47
|
# @return [Array<Hash>] array of server configurations with nested transport
|
|
48
|
+
# :reek:TooManyStatements -- per-file loop with warn-and-continue handling for missing files, bad JSON, and read errors
|
|
48
49
|
def parse_files(file_paths)
|
|
49
50
|
return [] if file_paths.nil? || file_paths.empty?
|
|
50
51
|
|
|
@@ -93,23 +94,29 @@ module AIA
|
|
|
93
94
|
# @return [Array<Hash>] array of server configurations
|
|
94
95
|
def convert_mcp_servers_format(mcp_servers)
|
|
95
96
|
mcp_servers.map do |name, config|
|
|
96
|
-
|
|
97
|
-
transport[:command] = config['command'] if config['command']
|
|
98
|
-
transport[:args] = Array(config['args']) if config['args']
|
|
99
|
-
transport[:env] = config['env'] if config['env']
|
|
100
|
-
transport[:url] = config['url'] if config['url']
|
|
101
|
-
transport[:headers] = config['headers'] if config['headers']
|
|
102
|
-
|
|
103
|
-
server = { name: name, transport: transport }
|
|
97
|
+
server = { name: name, transport: build_transport(config) }
|
|
104
98
|
server[:timeout] = config['timeout'].to_i if config['timeout']
|
|
99
|
+
server.merge!(routing_metadata(config))
|
|
100
|
+
end
|
|
101
|
+
end
|
|
105
102
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
103
|
+
def build_transport(config)
|
|
104
|
+
transport = { type: config['type'] || 'stdio' }
|
|
105
|
+
transport[:command] = config['command'] if config['command']
|
|
106
|
+
transport[:args] = Array(config['args']) if config['args']
|
|
107
|
+
transport[:env] = config['env'] if config['env']
|
|
108
|
+
transport[:url] = config['url'] if config['url']
|
|
109
|
+
transport[:headers] = config['headers'] if config['headers']
|
|
110
|
+
transport
|
|
111
|
+
end
|
|
110
112
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
+
# Routing metadata preserved for KBS/AIA
|
|
114
|
+
def routing_metadata(config)
|
|
115
|
+
meta = {}
|
|
116
|
+
meta[:topics] = Array(config['topics']) if config['topics']
|
|
117
|
+
meta[:independent] = config['independent'] unless config['independent'].nil?
|
|
118
|
+
meta[:group] = config['group'] if config['group']
|
|
119
|
+
meta
|
|
113
120
|
end
|
|
114
121
|
|
|
115
122
|
# Convert simple format to robot_lab nested transport format
|
|
@@ -123,12 +130,13 @@ module AIA
|
|
|
123
130
|
|
|
124
131
|
transport = { type: parsed['type'] || 'stdio' }
|
|
125
132
|
|
|
126
|
-
|
|
133
|
+
command = parsed['command']
|
|
134
|
+
if command.is_a?(Array)
|
|
127
135
|
# Command is an array: first element is command, rest are args
|
|
128
|
-
transport[:command] =
|
|
129
|
-
transport[:args] =
|
|
130
|
-
elsif
|
|
131
|
-
transport[:command] =
|
|
136
|
+
transport[:command] = command.first
|
|
137
|
+
transport[:args] = command[1..] || []
|
|
138
|
+
elsif command
|
|
139
|
+
transport[:command] = command
|
|
132
140
|
transport[:args] = parsed['args'] || []
|
|
133
141
|
end
|
|
134
142
|
|