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
|
@@ -1,15 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
# lib/aia/directives/execution_directives.rb
|
|
2
4
|
|
|
3
5
|
require 'shellwords'
|
|
4
6
|
|
|
5
7
|
module AIA
|
|
6
8
|
class ExecutionDirectives < Directive
|
|
7
|
-
|
|
9
|
+
state_setting! :concurrent, :conc, :verify, :decompose, :debate,
|
|
10
|
+
:delegate, :del, :spawn, :orchestrate, :orch,
|
|
11
|
+
:add_recruit, :add, :drop_recruit, :drop, :reskill
|
|
12
|
+
|
|
13
|
+
desc "Execute Ruby code (requires allow_ruby_eval: true in config)"
|
|
8
14
|
def ruby(args, context_manager = nil)
|
|
15
|
+
unless AIA.config.flags.allow_ruby_eval
|
|
16
|
+
puts "ERROR: /ruby is disabled. Set `allow_ruby_eval: true` in " \
|
|
17
|
+
"~/.aia/config.yml or pass --allow-ruby-eval to enable it. " \
|
|
18
|
+
"Note: this executes arbitrary Ruby with full process privileges."
|
|
19
|
+
return ""
|
|
20
|
+
end
|
|
21
|
+
|
|
9
22
|
ruby_code = args.join(' ')
|
|
23
|
+
$stderr.puts "WARNING: /ruby executing: #{ruby_code}"
|
|
10
24
|
|
|
11
25
|
begin
|
|
12
|
-
String(eval(ruby_code))
|
|
26
|
+
String(eval(ruby_code)) # rubocop:disable Security/Eval
|
|
13
27
|
rescue StandardError => e
|
|
14
28
|
<<~ERROR
|
|
15
29
|
This ruby code failed: #{ruby_code}
|
|
@@ -17,12 +31,135 @@ module AIA
|
|
|
17
31
|
ERROR
|
|
18
32
|
end
|
|
19
33
|
end
|
|
20
|
-
|
|
34
|
+
alias rb ruby
|
|
21
35
|
|
|
22
36
|
desc "Use text-to-speech to speak the text"
|
|
23
37
|
def say(args, context_manager = nil)
|
|
24
|
-
|
|
38
|
+
audio = AIA.config.audio
|
|
39
|
+
voice = audio.voice
|
|
40
|
+
env = {}
|
|
41
|
+
env['SPEECH_MODEL'] = audio.speech_model if audio.speech_model
|
|
42
|
+
if voice && !voice.strip.empty?
|
|
43
|
+
system(env, 'say', '-v', voice, *args)
|
|
44
|
+
else
|
|
45
|
+
system(env, 'say', *args)
|
|
46
|
+
end
|
|
25
47
|
""
|
|
26
48
|
end
|
|
49
|
+
|
|
50
|
+
desc "Execute next prompt with concurrent MCP server access"
|
|
51
|
+
def concurrent(args, context_manager = nil)
|
|
52
|
+
AIA.turn_state.force_concurrent_mcp = true
|
|
53
|
+
"Concurrent MCP mode enabled for next prompt."
|
|
54
|
+
end
|
|
55
|
+
alias conc concurrent
|
|
56
|
+
|
|
57
|
+
desc "Run next prompt through verification (two independent answers + reconciliation)"
|
|
58
|
+
def verify(args, context_manager = nil)
|
|
59
|
+
AIA.turn_state.force_verify = true
|
|
60
|
+
"Verification mode enabled for next prompt."
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
desc "Decompose next prompt into parallel sub-tasks"
|
|
64
|
+
def decompose(args, context_manager = nil)
|
|
65
|
+
AIA.turn_state.force_decompose = true
|
|
66
|
+
"Decomposition mode enabled for next prompt."
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
desc "Multi-round debate between robots in the network"
|
|
70
|
+
def debate(args, context_manager = nil)
|
|
71
|
+
AIA.turn_state.force_debate = true
|
|
72
|
+
"Debate mode enabled for next prompt."
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
desc "Delegate subtasks to specific robots via TrakFlow plan"
|
|
76
|
+
def delegate(args, context_manager = nil)
|
|
77
|
+
AIA.turn_state.force_delegate = true
|
|
78
|
+
"Delegation mode enabled for next prompt."
|
|
79
|
+
end
|
|
80
|
+
alias del delegate
|
|
81
|
+
|
|
82
|
+
desc "Spawn a specialist robot for the next prompt. " \
|
|
83
|
+
"Usage: /spawn | /spawn <type> | /spawn <name> <provider/model> <system prompt>"
|
|
84
|
+
def spawn(args, context_manager = nil)
|
|
85
|
+
state = AIA.turn_state
|
|
86
|
+
state.force_spawn = true
|
|
87
|
+
|
|
88
|
+
if args.size >= 2
|
|
89
|
+
spec = AIA::SpawnSpecParser.parse(args)
|
|
90
|
+
state.spawn_spec = spec
|
|
91
|
+
state.spawn_type = nil
|
|
92
|
+
"Spawn mode enabled: '#{spec[:name]}' (#{spawn_model_summary(spec)}) for next prompt."
|
|
93
|
+
else
|
|
94
|
+
type = args.first
|
|
95
|
+
state.spawn_spec = nil
|
|
96
|
+
state.spawn_type = type
|
|
97
|
+
type_msg = type ? " (#{type})" : " (auto-detect)"
|
|
98
|
+
"Spawn mode enabled#{type_msg} for next prompt."
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
desc "Recruit a robot into the crew (persists, @mention-able). Usage: " \
|
|
103
|
+
"/add_recruit <name> [provider/model] [skill:<id>...] [system prompt] (alias: /add)"
|
|
104
|
+
def add_recruit(args, context_manager = nil)
|
|
105
|
+
return "Usage: /add_recruit <name> [provider/model] [skill:<id> ...] [system prompt]" if args.empty?
|
|
106
|
+
|
|
107
|
+
spec = args.size >= 2 ? AIA::SpawnSpecParser.parse(args) : { name: args[0] }
|
|
108
|
+
robot = AIA::Crew.recruit(spec)
|
|
109
|
+
"Recruited '#{robot.name}' into the crew (#{recruit_summary(spec)})."
|
|
110
|
+
rescue AIA::CrewError => e
|
|
111
|
+
"Recruit failed: #{e.message}"
|
|
112
|
+
end
|
|
113
|
+
alias add add_recruit
|
|
114
|
+
|
|
115
|
+
desc "Drop a robot from the crew. Usage: /drop_recruit <name> (alias: /drop)"
|
|
116
|
+
def drop_recruit(args, context_manager = nil)
|
|
117
|
+
return "Usage: /drop_recruit <name>" if args.empty?
|
|
118
|
+
|
|
119
|
+
AIA::Crew.drop(args.first)
|
|
120
|
+
"Dropped '#{args.first}' from the crew."
|
|
121
|
+
rescue AIA::CrewError => e
|
|
122
|
+
"Drop failed: #{e.message}"
|
|
123
|
+
end
|
|
124
|
+
alias drop drop_recruit
|
|
125
|
+
|
|
126
|
+
desc "Reset a crew member to a clean slate and re-skill it. " \
|
|
127
|
+
"Usage: /reskill <name> [skill:<id>...] [system prompt]"
|
|
128
|
+
def reskill(args, context_manager = nil)
|
|
129
|
+
return "Usage: /reskill <name> [skill:<id> ...] [system prompt]" if args.empty?
|
|
130
|
+
|
|
131
|
+
skills, rest = AIA::SpawnSpecParser.extract_skills(args[1..].to_a)
|
|
132
|
+
prompt = rest.join(' ').strip
|
|
133
|
+
robot = AIA::Crew.reskill(args.first, skills: skills, system_prompt: (prompt.empty? ? nil : prompt))
|
|
134
|
+
suffix = skills.empty? ? '' : " with #{skills.join(', ')}"
|
|
135
|
+
"Reskilled '#{robot.name}'#{suffix} (clean slate)."
|
|
136
|
+
rescue AIA::CrewError => e
|
|
137
|
+
"Reskill failed: #{e.message}"
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
desc "3-tier layered orchestration: orchestrator → lead agents → specialists"
|
|
141
|
+
def orchestrate(args, context_manager = nil)
|
|
142
|
+
AIA.turn_state.force_orchestrate = true
|
|
143
|
+
"Orchestration mode enabled. Your next prompt is the application requirements."
|
|
144
|
+
end
|
|
145
|
+
alias orch orchestrate
|
|
146
|
+
|
|
147
|
+
private
|
|
148
|
+
|
|
149
|
+
# Human-readable model description for the /spawn confirmation message.
|
|
150
|
+
def spawn_model_summary(spec)
|
|
151
|
+
model = spec[:model]
|
|
152
|
+
return 'inherited model' unless model
|
|
153
|
+
|
|
154
|
+
spec[:provider] ? "#{spec[:provider]}/#{model}" : model
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Model plus any assigned skills, for the /add_recruit confirmation message.
|
|
158
|
+
def recruit_summary(spec)
|
|
159
|
+
parts = [spawn_model_summary(spec)]
|
|
160
|
+
skills = Array(spec[:skills])
|
|
161
|
+
parts << "skills: #{skills.join(', ')}" unless skills.empty?
|
|
162
|
+
parts.join('; ')
|
|
163
|
+
end
|
|
27
164
|
end
|
|
28
165
|
end
|
|
@@ -4,14 +4,14 @@ module AIA
|
|
|
4
4
|
class ModelDirectives < Directive
|
|
5
5
|
desc "List all available AI models"
|
|
6
6
|
def available_models(args = nil, context_manager = nil)
|
|
7
|
-
positive_terms, negative_terms = parse_search_terms(Array(args))
|
|
8
|
-
|
|
9
7
|
current_models = AIA.config.models
|
|
10
8
|
|
|
11
9
|
model_names = current_models.map do |m|
|
|
12
10
|
m.respond_to?(:name) ? m.name : m.to_s
|
|
13
11
|
end
|
|
14
12
|
|
|
13
|
+
positive_terms, negative_terms = parse_search_terms(Array(args))
|
|
14
|
+
|
|
15
15
|
using_local_provider = model_names.any? { |m| m.start_with?('ollama/', 'lms/') }
|
|
16
16
|
|
|
17
17
|
if using_local_provider
|
|
@@ -22,16 +22,36 @@ module AIA
|
|
|
22
22
|
|
|
23
23
|
""
|
|
24
24
|
end
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
alias am available_models
|
|
26
|
+
alias available available_models
|
|
27
|
+
alias models available_models
|
|
28
|
+
alias all_models available_models
|
|
29
|
+
alias llms available_models
|
|
30
30
|
|
|
31
31
|
desc "Compare responses from multiple models"
|
|
32
32
|
def compare(args, context_manager = nil)
|
|
33
|
-
|
|
33
|
+
prompt, models = parse_compare_args(args)
|
|
34
|
+
|
|
35
|
+
return report_error('Error: No prompt provided for comparison') unless prompt
|
|
36
|
+
return report_error('Error: No models specified. Use --models model1,model2,model3') if models.empty?
|
|
37
|
+
|
|
38
|
+
puts "\nComparing responses for: #{prompt}\n"
|
|
39
|
+
puts '=' * 80
|
|
40
|
+
|
|
41
|
+
models.each { |model_name| compare_with_model(model_name.strip, prompt) }
|
|
34
42
|
|
|
43
|
+
puts "\n" + ('=' * 80)
|
|
44
|
+
puts "\nComparison complete!"
|
|
45
|
+
|
|
46
|
+
''
|
|
47
|
+
end
|
|
48
|
+
alias cmp compare
|
|
49
|
+
|
|
50
|
+
# --- helpers (no desc → not registered) ---
|
|
51
|
+
|
|
52
|
+
# Split a //compare arg list into [prompt, models]; the first non-flag
|
|
53
|
+
# token is the prompt, `--models a,b,c` supplies the model list.
|
|
54
|
+
def parse_compare_args(args)
|
|
35
55
|
prompt = nil
|
|
36
56
|
models = []
|
|
37
57
|
|
|
@@ -46,46 +66,33 @@ module AIA
|
|
|
46
66
|
end
|
|
47
67
|
end
|
|
48
68
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
puts "\nComparing responses for: #{prompt}\n"
|
|
53
|
-
puts '=' * 80
|
|
54
|
-
|
|
55
|
-
results = {}
|
|
56
|
-
|
|
57
|
-
models.each do |model_name|
|
|
58
|
-
model_name.strip!
|
|
59
|
-
puts "\n🤖 **#{model_name}:**"
|
|
60
|
-
puts '-' * 40
|
|
61
|
-
|
|
62
|
-
begin
|
|
63
|
-
chat = RubyLLM.chat(model: model_name)
|
|
64
|
-
response = chat.ask(prompt)
|
|
65
|
-
content = response.content
|
|
69
|
+
[prompt, models]
|
|
70
|
+
end
|
|
66
71
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
error_msg = "Error with #{model_name}: #{e.message}"
|
|
71
|
-
puts error_msg
|
|
72
|
-
results[model_name] = error_msg
|
|
73
|
-
end
|
|
74
|
-
end
|
|
72
|
+
def compare_with_model(model_name, prompt)
|
|
73
|
+
puts "\n🤖 **#{model_name}:**"
|
|
74
|
+
puts '-' * 40
|
|
75
75
|
|
|
76
|
-
|
|
77
|
-
puts
|
|
78
|
-
|
|
79
|
-
|
|
76
|
+
response = RubyLLM.chat(model: model_name).ask(prompt)
|
|
77
|
+
puts response.content
|
|
78
|
+
rescue StandardError => e
|
|
79
|
+
puts "Error with #{model_name}: #{e.message}"
|
|
80
80
|
end
|
|
81
|
-
alias_method :cmp, :compare
|
|
82
81
|
|
|
83
|
-
#
|
|
82
|
+
# Log and print a directive error, then return nil so the chat loop
|
|
83
|
+
# skips forwarding it to the robot (an error is not conversational output).
|
|
84
|
+
def report_error(msg)
|
|
85
|
+
AIA::LoggerManager.aia_logger.error(msg)
|
|
86
|
+
puts msg
|
|
87
|
+
nil
|
|
88
|
+
end
|
|
84
89
|
|
|
85
|
-
def show_local_models(current_models, positive_terms, negative_terms)
|
|
90
|
+
def show_local_models(current_models, positive_terms = nil, negative_terms = nil)
|
|
86
91
|
require 'net/http'
|
|
87
92
|
require 'json'
|
|
88
93
|
|
|
94
|
+
positive_terms, negative_terms = normalized_model_search_terms(positive_terms, negative_terms)
|
|
95
|
+
|
|
89
96
|
puts "\nLocal LLM Models:"
|
|
90
97
|
puts
|
|
91
98
|
|
|
@@ -101,144 +108,159 @@ module AIA
|
|
|
101
108
|
end
|
|
102
109
|
end
|
|
103
110
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
response = Net::HTTP.get_response(uri)
|
|
108
|
-
|
|
109
|
-
unless response.is_a?(Net::HTTPSuccess)
|
|
110
|
-
puts "❌ Cannot connect to Ollama at #{api_base}"
|
|
111
|
-
return
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
data = JSON.parse(response.body)
|
|
115
|
-
models = data['models'] || []
|
|
111
|
+
# :reek:TooManyStatements -- sequential terminal report: fetch, connection guards, filtered listing
|
|
112
|
+
def show_ollama_models(api_base, positive_terms = nil, negative_terms = nil)
|
|
113
|
+
positive_terms, negative_terms = normalized_model_search_terms(positive_terms, negative_terms)
|
|
116
114
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
115
|
+
data = fetch_json("#{api_base}/api/tags")
|
|
116
|
+
if data.nil?
|
|
117
|
+
puts "❌ Cannot connect to Ollama at #{api_base}"
|
|
118
|
+
return
|
|
119
|
+
end
|
|
121
120
|
|
|
122
|
-
|
|
123
|
-
|
|
121
|
+
models = data['models'] || []
|
|
122
|
+
if models.empty?
|
|
123
|
+
puts "No Ollama models found"
|
|
124
|
+
return
|
|
125
|
+
end
|
|
124
126
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
name = model['name']
|
|
128
|
-
size = model['size'] ? format_bytes(model['size']) : 'unknown'
|
|
129
|
-
modified = model['modified_at'] ? Time.parse(model['modified_at']).strftime('%Y-%m-%d') : 'unknown'
|
|
127
|
+
puts "Ollama Models (#{api_base}):"
|
|
128
|
+
puts "-" * 60
|
|
130
129
|
|
|
131
|
-
|
|
132
|
-
|
|
130
|
+
entries = models.map { |model| format_ollama_entry(model) }
|
|
131
|
+
print_filtered_entries(entries, positive_terms, negative_terms, "Ollama model(s) available")
|
|
132
|
+
rescue StandardError => e
|
|
133
|
+
puts "❌ Error fetching Ollama models: #{e.message}"
|
|
134
|
+
end
|
|
133
135
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
+
def format_ollama_entry(model)
|
|
137
|
+
size = model['size'] ? format_bytes(model['size']) : 'unknown'
|
|
138
|
+
modified = model['modified_at'] ? Time.parse(model['modified_at']).strftime('%Y-%m-%d') : 'unknown'
|
|
139
|
+
"- ollama/#{model['name']} (size: #{size}, modified: #{modified})"
|
|
140
|
+
end
|
|
136
141
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
+
# Fetch and parse JSON from a local model server; nil unless HTTP success.
|
|
143
|
+
def fetch_json(uri_string)
|
|
144
|
+
uri = URI(uri_string)
|
|
145
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
146
|
+
http.open_timeout = 5
|
|
147
|
+
http.read_timeout = 5
|
|
148
|
+
response = http.request(Net::HTTP::Get.new(uri))
|
|
149
|
+
return nil unless response.is_a?(Net::HTTPSuccess)
|
|
142
150
|
|
|
143
|
-
|
|
144
|
-
puts "#{counter} Ollama model(s) available"
|
|
145
|
-
puts
|
|
146
|
-
rescue StandardError => e
|
|
147
|
-
puts "❌ Error fetching Ollama models: #{e.message}"
|
|
148
|
-
end
|
|
151
|
+
JSON.parse(response.body)
|
|
149
152
|
end
|
|
150
153
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
154
|
+
# The entry is a String; Array#intersect? would raise TypeError
|
|
155
|
+
def entry_matches?(entry_lc, positive_terms, negative_terms)
|
|
156
|
+
(positive_terms.empty? || positive_terms.any? { |q| entry_lc.include?(q) }) &&
|
|
157
|
+
negative_terms.none? { |q| entry_lc.include?(q) }
|
|
158
|
+
end
|
|
155
159
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
+
def print_filtered_entries(entries, positive_terms, negative_terms, summary_label)
|
|
161
|
+
counter = 0
|
|
162
|
+
entries.each do |entry|
|
|
163
|
+
next unless entry_matches?(entry.downcase, positive_terms, negative_terms)
|
|
160
164
|
|
|
161
|
-
|
|
162
|
-
|
|
165
|
+
puts entry
|
|
166
|
+
counter += 1
|
|
167
|
+
end
|
|
163
168
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
169
|
+
puts
|
|
170
|
+
puts "#{counter} #{summary_label}"
|
|
171
|
+
puts
|
|
172
|
+
end
|
|
168
173
|
|
|
169
|
-
|
|
170
|
-
|
|
174
|
+
# :reek:TooManyStatements -- sequential terminal report: fetch, connection guards, filtered listing
|
|
175
|
+
def show_lms_models(api_base, positive_terms = nil, negative_terms = nil)
|
|
176
|
+
positive_terms, negative_terms = normalized_model_search_terms(positive_terms, negative_terms)
|
|
171
177
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
178
|
+
data = fetch_json("#{api_base.gsub(%r{/v1/?$}, '')}/v1/models")
|
|
179
|
+
if data.nil?
|
|
180
|
+
puts "❌ Cannot connect to LM Studio at #{api_base}"
|
|
181
|
+
return
|
|
182
|
+
end
|
|
177
183
|
|
|
178
|
-
|
|
179
|
-
|
|
184
|
+
models = data['data'] || []
|
|
185
|
+
if models.empty?
|
|
186
|
+
puts "No LM Studio models found"
|
|
187
|
+
return
|
|
188
|
+
end
|
|
180
189
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
counter += 1
|
|
184
|
-
end
|
|
185
|
-
end
|
|
190
|
+
puts "LM Studio Models (#{api_base}):"
|
|
191
|
+
puts "-" * 60
|
|
186
192
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
puts "❌ Error fetching LM Studio models: #{e.message}"
|
|
192
|
-
end
|
|
193
|
+
entries = models.map { |model| "- lms/#{model['id']}" }
|
|
194
|
+
print_filtered_entries(entries, positive_terms, negative_terms, "LM Studio model(s) available")
|
|
195
|
+
rescue StandardError => e
|
|
196
|
+
puts "❌ Error fetching LM Studio models: #{e.message}"
|
|
193
197
|
end
|
|
194
198
|
|
|
195
199
|
def format_bytes(bytes)
|
|
196
|
-
units = [
|
|
200
|
+
units = %w[B KB MB GB TB]
|
|
197
201
|
return "0 B" if bytes.zero?
|
|
198
202
|
|
|
199
203
|
exp = (Math.log(bytes) / Math.log(1024)).to_i
|
|
200
204
|
exp = [exp, units.length - 1].min
|
|
201
205
|
|
|
202
|
-
"%.1f %s" % [bytes.to_f / (1024
|
|
206
|
+
"%.1f %s" % [bytes.to_f / (1024**exp), units[exp]]
|
|
203
207
|
end
|
|
204
208
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
209
|
+
# :reek:TooManyStatements -- sequential terminal report: header, filtered model listing, summary
|
|
210
|
+
def show_rubyllm_models(positive_terms = nil, negative_terms = nil)
|
|
211
|
+
positive_terms, negative_terms = normalized_model_search_terms(positive_terms, negative_terms)
|
|
208
212
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
header += " excluding: #{negative_terms.join(', ')}" unless negative_terms.empty?
|
|
213
|
+
# expand comma-separated terms passed as a single token
|
|
214
|
+
positive_terms = positive_terms.first.split(',') if positive_terms.size == 1
|
|
212
215
|
|
|
213
|
-
puts
|
|
216
|
+
puts rubyllm_header(positive_terms, negative_terms)
|
|
214
217
|
puts
|
|
215
218
|
|
|
216
|
-
|
|
219
|
+
# modality terms (e.g. "text_to_text") trigger capability checks; the rest
|
|
220
|
+
# are plain substring filters applied to the formatted entry string
|
|
221
|
+
modality_terms, substring_terms = positive_terms.partition { |q| q.include?('_to_') }
|
|
217
222
|
|
|
223
|
+
counter = 0
|
|
218
224
|
RubyLLM.models.all.each do |llm|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
in_1m = llm.pricing.text_tokens.standard.to_h[:input_per_million]
|
|
225
|
-
entry = "- #{llm.id} (#{llm.provider}) in: $#{in_1m} cw: #{cw} mode: #{mode} caps: #{caps}"
|
|
226
|
-
entry_down = entry.downcase
|
|
227
|
-
|
|
228
|
-
show_it = true
|
|
229
|
-
modality_terms.each { |q| show_it &&= llm.modalities.send("#{q}?") }
|
|
230
|
-
text_terms.each { |q| show_it &&= entry_down.include?(q) }
|
|
231
|
-
negative_terms.each { |q| show_it &&= !entry_down.include?(q) }
|
|
232
|
-
|
|
233
|
-
if show_it
|
|
234
|
-
counter += 1
|
|
235
|
-
puts entry
|
|
236
|
-
end
|
|
225
|
+
entry = format_rubyllm_entry(llm)
|
|
226
|
+
next unless rubyllm_entry_visible?(llm, entry, modality_terms, substring_terms, negative_terms)
|
|
227
|
+
|
|
228
|
+
counter += 1
|
|
229
|
+
puts entry
|
|
237
230
|
end
|
|
238
231
|
|
|
239
|
-
puts if counter
|
|
232
|
+
puts if counter.positive?
|
|
240
233
|
puts "#{counter} LLMs matching your query"
|
|
241
234
|
puts
|
|
242
235
|
end
|
|
236
|
+
|
|
237
|
+
def rubyllm_header(positive_terms, negative_terms)
|
|
238
|
+
header = "\nAvailable LLMs"
|
|
239
|
+
header += " for #{positive_terms.join(' and ')}" if positive_terms.any?
|
|
240
|
+
header += " (excluding: #{negative_terms.join(', ')})" if negative_terms.any?
|
|
241
|
+
header + ':'
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def format_rubyllm_entry(llm)
|
|
245
|
+
modalities = llm.modalities
|
|
246
|
+
mode = "#{modalities.input.join(',')} to #{modalities.output.join(',')}"
|
|
247
|
+
in_1m = llm.pricing.text_tokens.standard.to_h[:input_per_million]
|
|
248
|
+
"- #{llm.id} (#{llm.provider}) in: $#{in_1m} cw: #{llm.context_window} mode: #{mode} caps: #{llm.capabilities.join(',')}"
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def rubyllm_entry_visible?(llm, entry, modality_terms, substring_terms, negative_terms)
|
|
252
|
+
modality_terms.all? { |q| llm.modalities.send("#{q}?") } &&
|
|
253
|
+
substring_terms.all? { |q| entry.include?(q) } &&
|
|
254
|
+
negative_terms.none? { |q| entry.downcase.include?(q) }
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def normalized_model_search_terms(positive_terms, negative_terms = nil)
|
|
258
|
+
return parse_search_terms(Array(positive_terms)) if negative_terms.nil?
|
|
259
|
+
|
|
260
|
+
[
|
|
261
|
+
Array(positive_terms).compact.map { |term| term.to_s.downcase },
|
|
262
|
+
Array(negative_terms).compact.map { |term| term.to_s.downcase }
|
|
263
|
+
]
|
|
264
|
+
end
|
|
243
265
|
end
|
|
244
266
|
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# lib/aia/directives/trakflow_directives.rb
|
|
4
|
+
#
|
|
5
|
+
# Chat directives for interacting with TrakFlow task tracking.
|
|
6
|
+
|
|
7
|
+
module AIA
|
|
8
|
+
class TrakFlowDirectives < Directive
|
|
9
|
+
desc "Show TrakFlow ready tasks and project summary"
|
|
10
|
+
def tasks(args, context_manager = nil)
|
|
11
|
+
bridge = TrakFlowBridge.new
|
|
12
|
+
return report_error("TrakFlow not available. Run 'tf init' to initialize a project.") unless bridge.available?
|
|
13
|
+
|
|
14
|
+
if args.first == "summary"
|
|
15
|
+
bridge.project_summary || report_status("No summary available.")
|
|
16
|
+
else
|
|
17
|
+
bridge.check_ready_tasks || report_status("No ready tasks found.")
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
alias tf tasks
|
|
21
|
+
|
|
22
|
+
desc "Create a TrakFlow plan from a description"
|
|
23
|
+
def plan(args, context_manager = nil)
|
|
24
|
+
bridge = TrakFlowBridge.new
|
|
25
|
+
return report_error("TrakFlow not available. Run 'tf init' to initialize a project.") unless bridge.available?
|
|
26
|
+
|
|
27
|
+
description = args.join(' ')
|
|
28
|
+
return report_error("Usage: /plan <description>") if description.empty?
|
|
29
|
+
|
|
30
|
+
bridge.create_task(description) || report_error("Failed to create plan.")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
desc "Create a TrakFlow task"
|
|
34
|
+
def task(args, context_manager = nil)
|
|
35
|
+
bridge = TrakFlowBridge.new
|
|
36
|
+
return report_error("TrakFlow not available. Run 'tf init' to initialize a project.") unless bridge.available?
|
|
37
|
+
|
|
38
|
+
title = args.join(' ')
|
|
39
|
+
return report_error("Usage: /task <title>") if title.empty?
|
|
40
|
+
|
|
41
|
+
bridge.create_task(title) || report_error("Failed to create task.")
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
# Log and print a directive error, then return nil so the chat loop
|
|
47
|
+
# skips forwarding it to the robot (an error is not conversational output).
|
|
48
|
+
def report_error(msg)
|
|
49
|
+
AIA::LoggerManager.aia_logger.error(msg)
|
|
50
|
+
puts msg
|
|
51
|
+
nil
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Same as report_error, but for a non-error empty-result status
|
|
55
|
+
# (e.g. "no ready tasks found") rather than a failure.
|
|
56
|
+
def report_status(msg)
|
|
57
|
+
AIA::LoggerManager.aia_logger.info(msg)
|
|
58
|
+
puts msg
|
|
59
|
+
nil
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|