aia 1.0.0.pre.beta → 1.1.0
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/.version +1 -1
- data/CHANGELOG.md +89 -0
- data/COMMITS.md +192 -11
- data/README.md +327 -110
- data/docs/cli-reference.md +93 -10
- data/docs/configuration.md +29 -36
- data/docs/contributing.md +2 -2
- data/docs/directives-reference.md +49 -27
- data/docs/examples/index.md +2 -2
- data/docs/examples/mcp/index.md +93 -97
- data/docs/examples/prompts/automation/index.md +3 -2
- data/docs/examples/tools/index.md +17 -27
- data/docs/faq.md +9 -12
- data/docs/guides/basic-usage.md +4 -4
- data/docs/guides/chat.md +39 -34
- data/docs/guides/tools.md +4 -4
- data/docs/index.md +36 -62
- data/docs/installation.md +1 -1
- data/docs/mcp-integration.md +75 -139
- data/docs/prompt_management.md +88 -1
- data/docs/security.md +79 -81
- data/docs/tools-and-mcp-examples.md +8 -6
- data/docs/workflows-and-pipelines.md +2 -6
- data/examples/.gitignore +1 -0
- data/examples/README.md +41 -0
- data/examples/run_all.sh +261 -0
- data/lib/aia/adapter/chat_execution.rb +9 -7
- data/lib/aia/adapter/mcp_connector.rb +0 -29
- data/lib/aia/adapter/modality_handlers.rb +23 -15
- data/lib/aia/adapter/tool_filter.rb +21 -0
- data/lib/aia/adapter/tool_loader.rb +1 -9
- data/lib/aia/chat_loop.rb +244 -0
- data/lib/aia/chat_processor_service.rb +6 -3
- data/lib/aia/config/cli_parser.rb +56 -18
- data/lib/aia/config/defaults.yml +17 -2
- data/lib/aia/config/validator.rb +52 -11
- data/lib/aia/config.rb +29 -3
- data/lib/aia/directive.rb +29 -0
- data/lib/aia/directives/configuration_directives.rb +2 -1
- data/lib/aia/directives/execution_directives.rb +1 -1
- data/lib/aia/directives/model_directives.rb +28 -27
- data/lib/aia/directives/web_and_file_directives.rb +78 -40
- data/lib/aia/errors.rb +20 -1
- data/lib/aia/fzf.rb +8 -7
- data/lib/aia/input_collector.rb +24 -0
- data/lib/aia/prompt_handler.rb +36 -8
- data/lib/aia/prompt_pipeline.rb +183 -0
- data/lib/aia/session.rb +22 -372
- data/lib/aia/skill_utils.rb +61 -0
- data/lib/aia/ui_presenter.rb +8 -0
- data/lib/aia.rb +4 -0
- metadata +19 -45
data/lib/aia/directive.rb
CHANGED
|
@@ -84,5 +84,34 @@ module AIA
|
|
|
84
84
|
""
|
|
85
85
|
end
|
|
86
86
|
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
# Split args into positive and negative search terms.
|
|
91
|
+
# Tokens prefixed with -, ~, or ! are negative (exclusion) terms.
|
|
92
|
+
# All other tokens (bare or +-prefixed) are positive (inclusion) terms.
|
|
93
|
+
# All terms are downcased.
|
|
94
|
+
#
|
|
95
|
+
# @param args [Array<String>] raw argument tokens
|
|
96
|
+
# @return [Array<Array<String>>] [positive_terms, negative_terms]
|
|
97
|
+
def parse_search_terms(args)
|
|
98
|
+
positive = []
|
|
99
|
+
negative = []
|
|
100
|
+
|
|
101
|
+
Array(args).each do |arg|
|
|
102
|
+
arg.split.each do |token|
|
|
103
|
+
downcased = token.downcase
|
|
104
|
+
if downcased =~ /\A[-~!]/
|
|
105
|
+
negative << downcased[1..]
|
|
106
|
+
elsif downcased.start_with?('+')
|
|
107
|
+
positive << downcased[1..]
|
|
108
|
+
else
|
|
109
|
+
positive << downcased
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
[positive, negative]
|
|
115
|
+
end
|
|
87
116
|
end
|
|
88
117
|
end
|
|
@@ -32,7 +32,8 @@ module AIA
|
|
|
32
32
|
if AIA.config.respond_to?(setter)
|
|
33
33
|
AIA.config.send(setter, new_value)
|
|
34
34
|
else
|
|
35
|
-
|
|
35
|
+
warn "Warning: Unknown config option '#{config_item}'"
|
|
36
|
+
AIA::LoggerManager.aia_logger.warn("Unknown config option '#{config_item}'")
|
|
36
37
|
end
|
|
37
38
|
""
|
|
38
39
|
end
|
|
@@ -4,6 +4,8 @@ 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
|
+
|
|
7
9
|
current_models = AIA.config.models
|
|
8
10
|
|
|
9
11
|
model_names = current_models.map do |m|
|
|
@@ -13,9 +15,9 @@ module AIA
|
|
|
13
15
|
using_local_provider = model_names.any? { |m| m.start_with?('ollama/', 'lms/') }
|
|
14
16
|
|
|
15
17
|
if using_local_provider
|
|
16
|
-
show_local_models(model_names,
|
|
18
|
+
show_local_models(model_names, positive_terms, negative_terms)
|
|
17
19
|
else
|
|
18
|
-
show_rubyllm_models(
|
|
20
|
+
show_rubyllm_models(positive_terms, negative_terms)
|
|
19
21
|
end
|
|
20
22
|
|
|
21
23
|
""
|
|
@@ -80,7 +82,7 @@ module AIA
|
|
|
80
82
|
|
|
81
83
|
# --- helpers (no desc → not registered) ---
|
|
82
84
|
|
|
83
|
-
def show_local_models(current_models,
|
|
85
|
+
def show_local_models(current_models, positive_terms, negative_terms)
|
|
84
86
|
require 'net/http'
|
|
85
87
|
require 'json'
|
|
86
88
|
|
|
@@ -91,15 +93,15 @@ module AIA
|
|
|
91
93
|
if model_spec.start_with?('ollama/')
|
|
92
94
|
api_base = ENV.fetch('OLLAMA_API_BASE', 'http://localhost:11434')
|
|
93
95
|
api_base = api_base.gsub(%r{/v1/?$}, '')
|
|
94
|
-
show_ollama_models(api_base,
|
|
96
|
+
show_ollama_models(api_base, positive_terms, negative_terms)
|
|
95
97
|
elsif model_spec.start_with?('lms/')
|
|
96
98
|
api_base = ENV.fetch('LMS_API_BASE', 'http://localhost:1234')
|
|
97
|
-
show_lms_models(api_base,
|
|
99
|
+
show_lms_models(api_base, positive_terms, negative_terms)
|
|
98
100
|
end
|
|
99
101
|
end
|
|
100
102
|
end
|
|
101
103
|
|
|
102
|
-
def show_ollama_models(api_base,
|
|
104
|
+
def show_ollama_models(api_base, positive_terms, negative_terms)
|
|
103
105
|
begin
|
|
104
106
|
uri = URI("#{api_base}/api/tags")
|
|
105
107
|
response = Net::HTTP.get_response(uri)
|
|
@@ -127,8 +129,12 @@ module AIA
|
|
|
127
129
|
modified = model['modified_at'] ? Time.parse(model['modified_at']).strftime('%Y-%m-%d') : 'unknown'
|
|
128
130
|
|
|
129
131
|
entry = "- ollama/#{name} (size: #{size}, modified: #{modified})"
|
|
132
|
+
entry_down = entry.downcase
|
|
133
|
+
|
|
134
|
+
show_it = positive_terms.empty? || positive_terms.any? { |q| entry_down.include?(q) }
|
|
135
|
+
show_it &&= negative_terms.none? { |q| entry_down.include?(q) }
|
|
130
136
|
|
|
131
|
-
if
|
|
137
|
+
if show_it
|
|
132
138
|
puts entry
|
|
133
139
|
counter += 1
|
|
134
140
|
end
|
|
@@ -142,7 +148,7 @@ module AIA
|
|
|
142
148
|
end
|
|
143
149
|
end
|
|
144
150
|
|
|
145
|
-
def show_lms_models(api_base,
|
|
151
|
+
def show_lms_models(api_base, positive_terms, negative_terms)
|
|
146
152
|
begin
|
|
147
153
|
uri = URI("#{api_base.gsub(%r{/v1/?$}, '')}/v1/models")
|
|
148
154
|
response = Net::HTTP.get_response(uri)
|
|
@@ -167,8 +173,12 @@ module AIA
|
|
|
167
173
|
models.each do |model|
|
|
168
174
|
name = model['id']
|
|
169
175
|
entry = "- lms/#{name}"
|
|
176
|
+
entry_down = entry.downcase
|
|
177
|
+
|
|
178
|
+
show_it = positive_terms.empty? || positive_terms.any? { |q| entry_down.include?(q) }
|
|
179
|
+
show_it &&= negative_terms.none? { |q| entry_down.include?(q) }
|
|
170
180
|
|
|
171
|
-
if
|
|
181
|
+
if show_it
|
|
172
182
|
puts entry
|
|
173
183
|
counter += 1
|
|
174
184
|
end
|
|
@@ -192,22 +202,17 @@ module AIA
|
|
|
192
202
|
"%.1f %s" % [bytes.to_f / (1024 ** exp), units[exp]]
|
|
193
203
|
end
|
|
194
204
|
|
|
195
|
-
def show_rubyllm_models(
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
if query && 1 == query.size
|
|
199
|
-
query = query.first.split(',')
|
|
200
|
-
end
|
|
205
|
+
def show_rubyllm_models(positive_terms, negative_terms)
|
|
206
|
+
modality_terms = positive_terms.select { |q| q.include?('_to_') }
|
|
207
|
+
text_terms = positive_terms.reject { |q| q.include?('_to_') }
|
|
201
208
|
|
|
202
209
|
header = "\nAvailable LLMs"
|
|
203
|
-
header += " for #{
|
|
210
|
+
header += " for #{positive_terms.join(' and ')}" unless positive_terms.empty?
|
|
211
|
+
header += " excluding: #{negative_terms.join(', ')}" unless negative_terms.empty?
|
|
204
212
|
|
|
205
213
|
puts header + ':'
|
|
206
214
|
puts
|
|
207
215
|
|
|
208
|
-
q1 = query ? query.select { |q| q.include?('_to_') } : []
|
|
209
|
-
q2 = query ? query.reject { |q| q.include?('_to_') } : []
|
|
210
|
-
|
|
211
216
|
counter = 0
|
|
212
217
|
|
|
213
218
|
RubyLLM.models.all.each do |llm|
|
|
@@ -218,16 +223,12 @@ module AIA
|
|
|
218
223
|
mode = "#{inputs} to #{outputs}"
|
|
219
224
|
in_1m = llm.pricing.text_tokens.standard.to_h[:input_per_million]
|
|
220
225
|
entry = "- #{llm.id} (#{llm.provider}) in: $#{in_1m} cw: #{cw} mode: #{mode} caps: #{caps}"
|
|
221
|
-
|
|
222
|
-
if query.nil? || query.empty?
|
|
223
|
-
counter += 1
|
|
224
|
-
puts entry
|
|
225
|
-
next
|
|
226
|
-
end
|
|
226
|
+
entry_down = entry.downcase
|
|
227
227
|
|
|
228
228
|
show_it = true
|
|
229
|
-
|
|
230
|
-
|
|
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) }
|
|
231
232
|
|
|
232
233
|
if show_it
|
|
233
234
|
counter += 1
|
|
@@ -2,11 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
require 'faraday'
|
|
4
4
|
require 'clipboard'
|
|
5
|
+
require 'yaml'
|
|
6
|
+
require 'io/console'
|
|
7
|
+
require 'word_wrapper'
|
|
5
8
|
|
|
6
9
|
module AIA
|
|
7
10
|
class WebAndFileDirectives < Directive
|
|
11
|
+
include AIA::SkillUtils
|
|
8
12
|
PUREMD_API_KEY = ENV.fetch('PUREMD_API_KEY', nil)
|
|
9
|
-
SKILLS_DIR = File.expand_path('~/.claude/skills')
|
|
10
13
|
|
|
11
14
|
desc "Fetch and include content from a webpage"
|
|
12
15
|
def webpage(args, _context_manager = nil)
|
|
@@ -30,46 +33,94 @@ module AIA
|
|
|
30
33
|
alias_method :website, :webpage
|
|
31
34
|
alias_method :web, :webpage
|
|
32
35
|
|
|
33
|
-
desc "List available
|
|
34
|
-
def skills(
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
desc "List available AIA skills"
|
|
37
|
+
def skills(args = [], _context_manager = nil)
|
|
38
|
+
dir = aia_skills_dir
|
|
39
|
+
|
|
40
|
+
unless Dir.exist?(dir)
|
|
41
|
+
puts "No skills directory found at #{dir}"
|
|
37
42
|
return nil
|
|
38
43
|
end
|
|
39
44
|
|
|
40
|
-
|
|
41
|
-
.select { |e| Dir.exist?(File.join(SKILLS_DIR, e)) }
|
|
42
|
-
.sort
|
|
45
|
+
positive_terms, negative_terms = parse_search_terms(Array(args))
|
|
43
46
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
skill_dirs = Dir.children(dir)
|
|
48
|
+
.select { |e| Dir.exist?(File.join(dir, e)) }
|
|
49
|
+
.sort
|
|
50
|
+
|
|
51
|
+
if skill_dirs.empty?
|
|
52
|
+
puts "No skills found in #{dir}"
|
|
53
|
+
return nil
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
skill_data = skill_dirs.filter_map do |name|
|
|
57
|
+
fm = parse_front_matter(File.join(dir, name, 'SKILL.md'))
|
|
58
|
+
text = "#{name} #{fm.values.join(' ').downcase}"
|
|
59
|
+
pos_ok = positive_terms.empty? || positive_terms.all? { |t| text.include?(t) }
|
|
60
|
+
neg_ok = negative_terms.none? { |t| text.include?(t) }
|
|
61
|
+
[name, fm] if pos_ok && neg_ok
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
if skill_data.empty?
|
|
65
|
+
puts "No skills matching your query"
|
|
66
|
+
return nil
|
|
51
67
|
end
|
|
52
68
|
|
|
69
|
+
width = terminal_width
|
|
70
|
+
puts "\nAvailable Skills (#{dir}):"
|
|
71
|
+
puts "=" * [width, 60].min
|
|
72
|
+
|
|
73
|
+
skill_data.each do |name, fm|
|
|
74
|
+
display_name = fm['name'] || name
|
|
75
|
+
desc_text = fm['description'].to_s.strip
|
|
76
|
+
puts "\n #{display_name}"
|
|
77
|
+
unless desc_text.empty?
|
|
78
|
+
puts WordWrapper::MinimumRaggedness.new(width - 4, desc_text).wrap
|
|
79
|
+
.split("\n").map { |l| " #{l}" }.join("\n")
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
puts "\nTotal: #{skill_data.size} skill#{'s' if skill_data.size != 1}"
|
|
53
84
|
nil
|
|
54
85
|
end
|
|
55
86
|
|
|
56
|
-
desc "Include
|
|
87
|
+
desc "Include an AIA skill from the skills directory"
|
|
57
88
|
def skill(args = [], _context_manager = nil)
|
|
89
|
+
args = Array(args)
|
|
58
90
|
skill_name = args.first&.strip
|
|
91
|
+
|
|
59
92
|
if skill_name.nil? || skill_name.empty?
|
|
60
|
-
|
|
93
|
+
warn "Error: /skill requires a skill name. Use /skills to list available skills."
|
|
94
|
+
AIA::LoggerManager.aia_logger.error("/skill requires a skill name")
|
|
61
95
|
return nil
|
|
62
96
|
end
|
|
63
97
|
|
|
64
|
-
|
|
98
|
+
dir = aia_skills_dir
|
|
99
|
+
|
|
100
|
+
unless Dir.exist?(dir)
|
|
101
|
+
warn "Error: Skills directory not found at #{dir}"
|
|
102
|
+
AIA::LoggerManager.aia_logger.error("Skills directory not found at #{dir}")
|
|
103
|
+
return nil
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
skill_dir = find_skill_dir(skill_name, dir)
|
|
65
107
|
unless skill_dir
|
|
66
|
-
|
|
108
|
+
if path_based_id?(skill_name)
|
|
109
|
+
warn "Error: No skill directory found at '#{File.expand_path(skill_name)}'. Use /skills to list available skills."
|
|
110
|
+
AIA::LoggerManager.aia_logger.error("No skill directory found at '#{File.expand_path(skill_name)}'")
|
|
111
|
+
else
|
|
112
|
+
warn "Error: No skill matching '#{skill_name}' found in #{dir}. Use /skills to list available skills."
|
|
113
|
+
AIA::LoggerManager.aia_logger.error("No skill matching '#{skill_name}' found in #{dir}")
|
|
114
|
+
end
|
|
67
115
|
return nil
|
|
68
116
|
end
|
|
69
117
|
|
|
118
|
+
return File.read(skill_dir) if File.file?(skill_dir)
|
|
119
|
+
|
|
70
120
|
skill_path = File.join(skill_dir, 'SKILL.md')
|
|
71
121
|
unless File.exist?(skill_path)
|
|
72
|
-
|
|
122
|
+
warn "Error: Skill directory '#{File.basename(skill_dir)}' has no SKILL.md. Use /skills to list available skills."
|
|
123
|
+
AIA::LoggerManager.aia_logger.error("Skill directory '#{File.basename(skill_dir)}' has no SKILL.md")
|
|
73
124
|
return nil
|
|
74
125
|
end
|
|
75
126
|
|
|
@@ -87,28 +138,15 @@ module AIA
|
|
|
87
138
|
|
|
88
139
|
private
|
|
89
140
|
|
|
90
|
-
def
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
exact = File.join(SKILLS_DIR, skill_name)
|
|
94
|
-
return safe_skill_path(exact) if Dir.exist?(exact)
|
|
95
|
-
|
|
96
|
-
Dir.children(SKILLS_DIR)
|
|
97
|
-
.sort
|
|
98
|
-
.each do |entry|
|
|
99
|
-
next unless entry.start_with?(skill_name)
|
|
100
|
-
candidate = File.join(SKILLS_DIR, entry)
|
|
101
|
-
return safe_skill_path(candidate) if Dir.exist?(candidate)
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
nil
|
|
141
|
+
def aia_skills_dir
|
|
142
|
+
AIA.config.skills.dir
|
|
105
143
|
end
|
|
106
144
|
|
|
107
|
-
def
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
nil
|
|
145
|
+
def terminal_width
|
|
146
|
+
IO.console&.winsize&.last || 80
|
|
147
|
+
rescue StandardError
|
|
148
|
+
80
|
|
112
149
|
end
|
|
150
|
+
|
|
113
151
|
end
|
|
114
152
|
end
|
data/lib/aia/errors.rb
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# lib/aia/errors.rb
|
|
2
2
|
|
|
3
3
|
module AIA
|
|
4
|
-
class
|
|
4
|
+
# Base error class for all AIA errors
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
|
|
7
|
+
# Raised for configuration validation failures
|
|
8
|
+
class ConfigurationError < Error; end
|
|
9
|
+
|
|
10
|
+
# Raised for prompt loading, parsing, or processing failures
|
|
11
|
+
class PromptError < Error; end
|
|
12
|
+
|
|
13
|
+
# Raised for RubyLLM::Tool loading or execution failures
|
|
14
|
+
class ToolError < Error; end
|
|
15
|
+
|
|
16
|
+
# Raised for MCP server connection or communication failures
|
|
17
|
+
class MCPError < Error; end
|
|
18
|
+
|
|
19
|
+
# Raised for LLM adapter failures (model setup, chat execution)
|
|
20
|
+
class AdapterError < Error; end
|
|
21
|
+
|
|
22
|
+
# Raised for directive parsing or execution failures
|
|
23
|
+
class DirectiveError < Error; end
|
|
5
24
|
end
|
data/lib/aia/fzf.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
# fzf is a general-purpose command-line fuzzy finder
|
|
3
3
|
|
|
4
4
|
require 'shellwords'
|
|
5
|
+
require 'open3'
|
|
5
6
|
require 'tempfile'
|
|
6
7
|
|
|
7
8
|
class AIA::Fzf
|
|
@@ -37,19 +38,19 @@ class AIA::Fzf
|
|
|
37
38
|
|
|
38
39
|
def build_command
|
|
39
40
|
fzf_options = DEFAULT_PARAMETERS.dup
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
escaped_dir = Shellwords.escape(directory)
|
|
42
|
+
escaped_ext = Shellwords.escape(extension)
|
|
43
|
+
fzf_options << "--header=#{Shellwords.escape("#{subject} which contain: #{query}\nPress ESC to cancel.")}"
|
|
44
|
+
fzf_options << "--preview=#{Shellwords.escape("cat #{escaped_dir}/{1}#{escaped_ext}")}"
|
|
42
45
|
fzf_options << "--prompt=#{Shellwords.escape(prompt)}"
|
|
43
46
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
@command = "cat #{tempfile_path} | #{fzf_command}"
|
|
47
|
+
@fzf_args = fzf_options
|
|
47
48
|
end
|
|
48
49
|
|
|
49
50
|
|
|
50
51
|
def run
|
|
51
|
-
|
|
52
|
-
selected =
|
|
52
|
+
input = list.join("\n")
|
|
53
|
+
selected, status = Open3.capture2('fzf', *@fzf_args, stdin_data: input)
|
|
53
54
|
|
|
54
55
|
selected.strip.empty? ? nil : selected.strip
|
|
55
56
|
ensure
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# lib/aia/input_collector.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module AIA
|
|
5
|
+
class InputCollector
|
|
6
|
+
# Collect variable values from user input via HistoryManager
|
|
7
|
+
def collect(parameters)
|
|
8
|
+
return {} if parameters.nil? || parameters.empty?
|
|
9
|
+
|
|
10
|
+
values = {}
|
|
11
|
+
input_manager = AIA::HistoryManager.new
|
|
12
|
+
|
|
13
|
+
parameters.each do |name, default|
|
|
14
|
+
value = input_manager.request_variable_value(
|
|
15
|
+
variable_name: name,
|
|
16
|
+
default_value: default,
|
|
17
|
+
)
|
|
18
|
+
values[name] = value
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
values
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
data/lib/aia/prompt_handler.rb
CHANGED
|
@@ -6,6 +6,14 @@ require 'erb'
|
|
|
6
6
|
|
|
7
7
|
module AIA
|
|
8
8
|
class PromptHandler
|
|
9
|
+
include AIA::SkillUtils
|
|
10
|
+
|
|
11
|
+
# Struct for path-based role content (bypasses PM parsing)
|
|
12
|
+
RoleContent = Struct.new(:content) do
|
|
13
|
+
def to_s; content; end
|
|
14
|
+
def metadata; nil; end
|
|
15
|
+
end
|
|
16
|
+
|
|
9
17
|
# Root-level YAML keys that are shorthands for deeper config paths
|
|
10
18
|
SHORTHAND_KEYS = %w[model temperature top_p next pipeline shell erb].freeze
|
|
11
19
|
|
|
@@ -46,7 +54,8 @@ module AIA
|
|
|
46
54
|
parsed = if File.exist?(prompt_file_path)
|
|
47
55
|
PM.parse(prompt_id)
|
|
48
56
|
else
|
|
49
|
-
|
|
57
|
+
warn "Warning: Invalid prompt ID or file not found: #{prompt_id}"
|
|
58
|
+
logger.warn("Invalid prompt ID or file not found: #{prompt_id}")
|
|
50
59
|
handle_missing_prompt(prompt_id)
|
|
51
60
|
end
|
|
52
61
|
|
|
@@ -57,6 +66,7 @@ module AIA
|
|
|
57
66
|
|
|
58
67
|
def fetch_role(role_id)
|
|
59
68
|
return handle_missing_role("roles/") if role_id.nil?
|
|
69
|
+
return fetch_role_from_path(role_id) if path_based_id?(role_id)
|
|
60
70
|
|
|
61
71
|
unless role_id.start_with?(AIA.config.prompts.roles_prefix)
|
|
62
72
|
role_id = "#{AIA.config.prompts.roles_prefix}/#{role_id}"
|
|
@@ -65,9 +75,12 @@ module AIA
|
|
|
65
75
|
role_file_path = File.join(@prompts_dir, "#{role_id}#{AIA.config.prompts.extname}")
|
|
66
76
|
|
|
67
77
|
parsed = if File.exist?(role_file_path)
|
|
68
|
-
PM.parse(role_id)
|
|
78
|
+
# PM.parse(role_id) cannot resolve subdirectory IDs like "roles/jersey_mike"
|
|
79
|
+
# and returns the ID string as content. Parse from raw file content instead.
|
|
80
|
+
PM.parse_string(File.read(role_file_path))
|
|
69
81
|
else
|
|
70
|
-
|
|
82
|
+
warn "Warning: Invalid role ID or file not found: #{role_id}"
|
|
83
|
+
logger.warn("Invalid role ID or file not found: #{role_id}")
|
|
71
84
|
handle_missing_role(role_id)
|
|
72
85
|
end
|
|
73
86
|
|
|
@@ -90,7 +103,8 @@ module AIA
|
|
|
90
103
|
role_parsed = fetch_role(role_id)
|
|
91
104
|
role_parsed.to_s
|
|
92
105
|
rescue => e
|
|
93
|
-
|
|
106
|
+
warn "Warning: Could not load role '#{role_id}' for model: #{e.message}"
|
|
107
|
+
logger.warn("Could not load role '#{role_id}' for model: #{e.message}")
|
|
94
108
|
nil
|
|
95
109
|
end
|
|
96
110
|
|
|
@@ -280,19 +294,33 @@ module AIA
|
|
|
280
294
|
def handle_missing_prompt(prompt_id)
|
|
281
295
|
prompt_id = prompt_id.to_s.strip
|
|
282
296
|
if prompt_id.empty?
|
|
283
|
-
|
|
297
|
+
warn "Error: Prompt ID cannot be empty"
|
|
284
298
|
exit 1
|
|
285
299
|
end
|
|
286
300
|
|
|
287
301
|
if AIA.config.flags.fuzzy
|
|
288
302
|
fuzzy_search_prompt(prompt_id)
|
|
289
303
|
else
|
|
290
|
-
|
|
304
|
+
warn "Error: Could not find prompt with ID: #{prompt_id}"
|
|
291
305
|
exit 1
|
|
292
306
|
end
|
|
293
307
|
end
|
|
294
308
|
|
|
295
309
|
|
|
310
|
+
def fetch_role_from_path(role_id)
|
|
311
|
+
expanded = File.expand_path(role_id)
|
|
312
|
+
expanded += '.md' if File.extname(expanded).empty?
|
|
313
|
+
|
|
314
|
+
unless File.exist?(expanded)
|
|
315
|
+
$stderr.puts "Warning: Role file not found at path: #{expanded}"
|
|
316
|
+
logger.warn("Role file not found at path: #{expanded}")
|
|
317
|
+
return handle_missing_role(role_id)
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
RoleContent.new(File.read(expanded))
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
|
|
296
324
|
def fuzzy_search_prompt(prompt_id)
|
|
297
325
|
new_prompt_id = search_prompt_id_with_fzf(prompt_id)
|
|
298
326
|
|
|
@@ -307,14 +335,14 @@ module AIA
|
|
|
307
335
|
def handle_missing_role(role_id)
|
|
308
336
|
role_id = role_id.to_s.strip
|
|
309
337
|
if role_id.empty? || role_id == "roles/"
|
|
310
|
-
|
|
338
|
+
warn "Error: Role ID cannot be empty"
|
|
311
339
|
exit 1
|
|
312
340
|
end
|
|
313
341
|
|
|
314
342
|
if AIA.config.flags.fuzzy
|
|
315
343
|
fuzzy_search_role(role_id)
|
|
316
344
|
else
|
|
317
|
-
|
|
345
|
+
warn "Error: Could not find role with ID: #{role_id}"
|
|
318
346
|
exit 1
|
|
319
347
|
end
|
|
320
348
|
end
|