aia 0.9.24 → 0.10.2
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 +84 -3
- data/README.md +179 -59
- data/bin/aia +6 -0
- data/docs/cli-reference.md +145 -72
- data/docs/configuration.md +156 -19
- data/docs/examples/tools/index.md +2 -2
- data/docs/faq.md +11 -11
- data/docs/guides/available-models.md +11 -11
- data/docs/guides/basic-usage.md +18 -17
- data/docs/guides/chat.md +57 -11
- data/docs/guides/executable-prompts.md +15 -15
- data/docs/guides/first-prompt.md +2 -2
- data/docs/guides/getting-started.md +6 -6
- data/docs/guides/image-generation.md +24 -24
- data/docs/guides/local-models.md +2 -2
- data/docs/guides/models.md +96 -18
- data/docs/guides/tools.md +4 -4
- data/docs/installation.md +2 -2
- data/docs/prompt_management.md +11 -11
- data/docs/security.md +3 -3
- data/docs/workflows-and-pipelines.md +1 -1
- data/examples/README.md +6 -6
- data/examples/headlines +3 -3
- data/lib/aia/aia_completion.bash +2 -2
- data/lib/aia/aia_completion.fish +4 -4
- data/lib/aia/aia_completion.zsh +2 -2
- data/lib/aia/chat_processor_service.rb +31 -21
- data/lib/aia/config/cli_parser.rb +403 -403
- data/lib/aia/config/config_section.rb +87 -0
- data/lib/aia/config/defaults.yml +219 -0
- data/lib/aia/config/defaults_loader.rb +147 -0
- data/lib/aia/config/mcp_parser.rb +151 -0
- data/lib/aia/config/model_spec.rb +67 -0
- data/lib/aia/config/validator.rb +185 -136
- data/lib/aia/config.rb +336 -17
- data/lib/aia/directive_processor.rb +14 -6
- data/lib/aia/directives/configuration.rb +24 -10
- data/lib/aia/directives/models.rb +3 -4
- data/lib/aia/directives/utility.rb +3 -2
- data/lib/aia/directives/web_and_file.rb +50 -47
- data/lib/aia/logger.rb +328 -0
- data/lib/aia/prompt_handler.rb +18 -22
- data/lib/aia/ruby_llm_adapter.rb +572 -69
- data/lib/aia/session.rb +9 -8
- data/lib/aia/ui_presenter.rb +20 -16
- data/lib/aia/utility.rb +50 -18
- data/lib/aia.rb +91 -66
- data/lib/extensions/ruby_llm/modalities.rb +2 -0
- data/mcp_servers/apple-mcp.json +8 -0
- data/mcp_servers/mcp_server_chart.json +11 -0
- data/mcp_servers/playwright_one.json +8 -0
- data/mcp_servers/playwright_two.json +8 -0
- data/mcp_servers/tavily_mcp_server.json +8 -0
- metadata +83 -25
- data/lib/aia/config/base.rb +0 -308
- data/lib/aia/config/defaults.rb +0 -91
- data/lib/aia/config/file_loader.rb +0 -163
- data/mcp_servers/imcp.json +0 -7
- data/mcp_servers/launcher.json +0 -11
- data/mcp_servers/timeserver.json +0 -8
data/lib/aia/config.rb
CHANGED
|
@@ -1,30 +1,349 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
# lib/aia/config.rb
|
|
2
4
|
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
5
|
+
# AIA Configuration using Anyway Config
|
|
6
|
+
#
|
|
7
|
+
# Schema is defined in lib/aia/config/defaults.yml (single source of truth)
|
|
8
|
+
# Configuration uses nested sections for better organization:
|
|
9
|
+
# - AIA.config.llm.temperature
|
|
10
|
+
# - AIA.config.prompts.dir
|
|
11
|
+
# - AIA.config.models.first.name
|
|
12
|
+
#
|
|
13
|
+
# Configuration sources (lowest to highest priority):
|
|
14
|
+
# 1. Bundled defaults: lib/aia/config/defaults.yml (ships with gem)
|
|
15
|
+
# 2. User config: ~/.aia/config.yml
|
|
16
|
+
# 3. Environment variables (AIA_*)
|
|
17
|
+
# 4. CLI arguments (applied via overrides)
|
|
18
|
+
# 5. Embedded directives (//config)
|
|
19
|
+
|
|
20
|
+
require 'anyway_config'
|
|
21
|
+
require 'yaml'
|
|
22
|
+
require 'date'
|
|
7
23
|
|
|
8
|
-
require_relative 'config/
|
|
24
|
+
require_relative 'config/config_section'
|
|
25
|
+
require_relative 'config/model_spec'
|
|
26
|
+
require_relative 'config/defaults_loader'
|
|
27
|
+
require_relative 'config/mcp_parser'
|
|
9
28
|
|
|
10
29
|
module AIA
|
|
11
|
-
class Config
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
30
|
+
class Config < Anyway::Config
|
|
31
|
+
config_name :aia
|
|
32
|
+
env_prefix :aia
|
|
33
|
+
|
|
34
|
+
# ==========================================================================
|
|
35
|
+
# Schema Definition (loaded from defaults.yml - single source of truth)
|
|
36
|
+
# ==========================================================================
|
|
37
|
+
|
|
38
|
+
DEFAULTS_PATH = File.expand_path('config/defaults.yml', __dir__).freeze
|
|
39
|
+
SCHEMA = Loaders::DefaultsLoader.schema
|
|
40
|
+
|
|
41
|
+
# Nested section attributes (defined as hashes, converted to ConfigSection)
|
|
42
|
+
attr_config :service, :llm, :prompts, :output, :audio, :image, :embedding,
|
|
43
|
+
:tools, :flags, :registry, :paths, :logger
|
|
44
|
+
|
|
45
|
+
# Array/collection attributes
|
|
46
|
+
attr_config :models, :pipeline, :require_libs, :mcp_servers, :context_files
|
|
47
|
+
|
|
48
|
+
# Runtime attributes (not loaded from config files)
|
|
49
|
+
attr_accessor :prompt_id, :stdin_content, :remaining_args, :dump_file,
|
|
50
|
+
:completion, :executable_prompt,
|
|
51
|
+
:executable_prompt_file, :tool_names, :loaded_tools, :next_prompt,
|
|
52
|
+
:log_level_override, :log_file_override,
|
|
53
|
+
:connected_mcp_servers, # Array of successfully connected MCP server names
|
|
54
|
+
:failed_mcp_servers # Array of {name:, error:} hashes for failed MCP servers
|
|
55
|
+
|
|
56
|
+
# Alias for next prompt (for backward compatibility with directives)
|
|
57
|
+
def next
|
|
58
|
+
@next_prompt
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def next=(value)
|
|
62
|
+
@next_prompt = value
|
|
63
|
+
# Also prepend to pipeline
|
|
64
|
+
pipeline.unshift(value) if value && !value.empty?
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# ==========================================================================
|
|
68
|
+
# Type Coercion
|
|
69
|
+
# ==========================================================================
|
|
70
|
+
|
|
71
|
+
# Create a coercion that merges incoming value with SCHEMA defaults for a section
|
|
72
|
+
def self.config_section_with_defaults(section_key)
|
|
73
|
+
defaults = SCHEMA[section_key] || {}
|
|
74
|
+
->(v) {
|
|
75
|
+
return v if v.is_a?(ConfigSection)
|
|
76
|
+
incoming = v || {}
|
|
77
|
+
merged = deep_merge_hashes(defaults.dup, incoming)
|
|
78
|
+
ConfigSection.new(merged)
|
|
79
|
+
}
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Deep merge helper for coercion
|
|
83
|
+
def self.deep_merge_hashes(base, overlay)
|
|
84
|
+
base.merge(overlay || {}) do |_key, old_val, new_val|
|
|
85
|
+
if old_val.is_a?(Hash) && new_val.is_a?(Hash)
|
|
86
|
+
deep_merge_hashes(old_val, new_val)
|
|
87
|
+
else
|
|
88
|
+
new_val.nil? ? old_val : new_val
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Convert array of hashes to array of ModelSpec objects
|
|
94
|
+
TO_MODEL_SPECS = ->(v) {
|
|
95
|
+
return [] if v.nil?
|
|
96
|
+
return v if v.is_a?(Array) && v.first.is_a?(ModelSpec)
|
|
97
|
+
|
|
98
|
+
model_counts = Hash.new(0)
|
|
99
|
+
|
|
100
|
+
Array(v).map do |spec|
|
|
101
|
+
# Handle string format from CLI
|
|
102
|
+
if spec.is_a?(String)
|
|
103
|
+
if spec.include?('=')
|
|
104
|
+
name, role = spec.split('=', 2)
|
|
105
|
+
spec = { name: name.strip, role: role.strip }
|
|
106
|
+
else
|
|
107
|
+
spec = { name: spec.strip }
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
spec = spec.transform_keys(&:to_sym) if spec.respond_to?(:transform_keys)
|
|
112
|
+
name = spec[:name]
|
|
113
|
+
|
|
114
|
+
model_counts[name] += 1
|
|
115
|
+
instance = model_counts[name]
|
|
116
|
+
|
|
117
|
+
ModelSpec.new(
|
|
118
|
+
name: name,
|
|
119
|
+
role: spec[:role],
|
|
120
|
+
instance: instance,
|
|
121
|
+
internal_id: instance > 1 ? "#{name}##{instance}" : name
|
|
122
|
+
)
|
|
123
|
+
end
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
coerce_types(
|
|
127
|
+
# Nested sections -> ConfigSection objects (with SCHEMA defaults merged)
|
|
128
|
+
service: config_section_with_defaults(:service),
|
|
129
|
+
llm: config_section_with_defaults(:llm),
|
|
130
|
+
prompts: config_section_with_defaults(:prompts),
|
|
131
|
+
output: config_section_with_defaults(:output),
|
|
132
|
+
audio: config_section_with_defaults(:audio),
|
|
133
|
+
image: config_section_with_defaults(:image),
|
|
134
|
+
embedding: config_section_with_defaults(:embedding),
|
|
135
|
+
tools: config_section_with_defaults(:tools),
|
|
136
|
+
flags: config_section_with_defaults(:flags),
|
|
137
|
+
registry: config_section_with_defaults(:registry),
|
|
138
|
+
paths: config_section_with_defaults(:paths),
|
|
139
|
+
|
|
140
|
+
# Arrays
|
|
141
|
+
models: TO_MODEL_SPECS,
|
|
142
|
+
pipeline: { type: :string, array: true },
|
|
143
|
+
require_libs: { type: :string, array: true },
|
|
144
|
+
context_files: { type: :string, array: true }
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# ==========================================================================
|
|
148
|
+
# Callbacks
|
|
149
|
+
# ==========================================================================
|
|
150
|
+
|
|
151
|
+
on_load :expand_paths, :ensure_arrays
|
|
152
|
+
|
|
153
|
+
# ==========================================================================
|
|
154
|
+
# Class Methods
|
|
155
|
+
# ==========================================================================
|
|
156
|
+
|
|
157
|
+
class << self
|
|
158
|
+
# Setup configuration with CLI overrides
|
|
159
|
+
#
|
|
160
|
+
# @param cli_overrides [Hash] overrides from CLI parsing
|
|
161
|
+
# @return [Config] configured instance
|
|
162
|
+
def setup(cli_overrides = {})
|
|
163
|
+
new(overrides: cli_overrides)
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# ==========================================================================
|
|
168
|
+
# Instance Methods
|
|
169
|
+
# ==========================================================================
|
|
170
|
+
|
|
171
|
+
# Mapping of flat CLI keys to their nested config locations
|
|
172
|
+
CLI_TO_NESTED_MAP = {
|
|
173
|
+
# flags section
|
|
174
|
+
chat: [:flags, :chat],
|
|
175
|
+
cost: [:flags, :cost],
|
|
176
|
+
fuzzy: [:flags, :fuzzy],
|
|
177
|
+
tokens: [:flags, :tokens],
|
|
178
|
+
no_mcp: [:flags, :no_mcp],
|
|
179
|
+
terse: [:flags, :terse],
|
|
180
|
+
debug: [:flags, :debug],
|
|
181
|
+
verbose: [:flags, :verbose],
|
|
182
|
+
consensus: [:flags, :consensus],
|
|
183
|
+
# llm section
|
|
184
|
+
adapter: [:llm, :adapter],
|
|
185
|
+
temperature: [:llm, :temperature],
|
|
186
|
+
max_tokens: [:llm, :max_tokens],
|
|
187
|
+
top_p: [:llm, :top_p],
|
|
188
|
+
frequency_penalty: [:llm, :frequency_penalty],
|
|
189
|
+
presence_penalty: [:llm, :presence_penalty],
|
|
190
|
+
# prompts section
|
|
191
|
+
prompts_dir: [:prompts, :dir],
|
|
192
|
+
roles_prefix: [:prompts, :roles_prefix],
|
|
193
|
+
role: [:prompts, :role],
|
|
194
|
+
parameter_regex: [:prompts, :parameter_regex],
|
|
195
|
+
system_prompt: [:prompts, :system_prompt],
|
|
196
|
+
# output section
|
|
197
|
+
output: [:output, :file],
|
|
198
|
+
history_file: [:output, :history_file],
|
|
199
|
+
append: [:output, :append],
|
|
200
|
+
markdown: [:output, :markdown],
|
|
201
|
+
# audio section
|
|
202
|
+
speak: [:audio, :speak],
|
|
203
|
+
voice: [:audio, :voice],
|
|
204
|
+
speech_model: [:audio, :speech_model],
|
|
205
|
+
transcription_model: [:audio, :transcription_model],
|
|
206
|
+
# image section
|
|
207
|
+
image_size: [:image, :size],
|
|
208
|
+
image_quality: [:image, :quality],
|
|
209
|
+
image_style: [:image, :style],
|
|
210
|
+
# tools section
|
|
211
|
+
tool_paths: [:tools, :paths],
|
|
212
|
+
allowed_tools: [:tools, :allowed],
|
|
213
|
+
rejected_tools: [:tools, :rejected],
|
|
214
|
+
# registry section
|
|
215
|
+
refresh: [:registry, :refresh],
|
|
216
|
+
# paths section
|
|
217
|
+
extra_config_file: [:paths, :extra_config_file]
|
|
218
|
+
}.freeze
|
|
219
|
+
|
|
220
|
+
def initialize(overrides: {})
|
|
221
|
+
super()
|
|
222
|
+
apply_overrides(overrides) if overrides && !overrides.empty?
|
|
223
|
+
process_mcp_files(overrides[:mcp_files]) if overrides[:mcp_files]
|
|
15
224
|
end
|
|
16
225
|
|
|
17
|
-
#
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
226
|
+
# Apply CLI or runtime overrides to configuration
|
|
227
|
+
#
|
|
228
|
+
# @param overrides [Hash] key-value pairs to override
|
|
229
|
+
def apply_overrides(overrides)
|
|
230
|
+
overrides.each do |key, value|
|
|
231
|
+
next if value.nil?
|
|
232
|
+
|
|
233
|
+
key_sym = key.to_sym
|
|
234
|
+
|
|
235
|
+
# Check if this is a flat CLI key that maps to a nested location
|
|
236
|
+
if CLI_TO_NESTED_MAP.key?(key_sym)
|
|
237
|
+
section, nested_key = CLI_TO_NESTED_MAP[key_sym]
|
|
238
|
+
section_obj = send(section)
|
|
239
|
+
section_obj.send("#{nested_key}=", value) if section_obj.respond_to?("#{nested_key}=")
|
|
240
|
+
elsif respond_to?("#{key}=")
|
|
241
|
+
send("#{key}=", value)
|
|
242
|
+
elsif key.to_s.include?('__')
|
|
243
|
+
# Handle nested keys like 'llm__temperature'
|
|
244
|
+
parts = key.to_s.split('__')
|
|
245
|
+
apply_nested_override(parts, value)
|
|
246
|
+
end
|
|
23
247
|
end
|
|
24
248
|
end
|
|
25
249
|
|
|
26
|
-
|
|
27
|
-
|
|
250
|
+
# Convert config to hash (for dump, etc.)
|
|
251
|
+
def to_h
|
|
252
|
+
{
|
|
253
|
+
service: service.to_h,
|
|
254
|
+
llm: llm.to_h,
|
|
255
|
+
models: models.map(&:to_h),
|
|
256
|
+
prompts: prompts.to_h,
|
|
257
|
+
output: output.to_h,
|
|
258
|
+
audio: audio.to_h,
|
|
259
|
+
image: image.to_h,
|
|
260
|
+
embedding: embedding.to_h,
|
|
261
|
+
tools: tools.to_h,
|
|
262
|
+
flags: flags.to_h,
|
|
263
|
+
registry: registry.to_h,
|
|
264
|
+
paths: paths.to_h,
|
|
265
|
+
pipeline: pipeline,
|
|
266
|
+
require_libs: require_libs,
|
|
267
|
+
mcp_servers: mcp_servers,
|
|
268
|
+
context_files: context_files
|
|
269
|
+
}
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
private
|
|
273
|
+
|
|
274
|
+
def expand_paths
|
|
275
|
+
# Expand ~ in paths
|
|
276
|
+
if paths.aia_dir
|
|
277
|
+
paths.aia_dir = File.expand_path(paths.aia_dir)
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
if paths.config_file
|
|
281
|
+
paths.config_file = File.expand_path(paths.config_file)
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
if prompts.dir
|
|
285
|
+
prompts.dir = File.expand_path(prompts.dir)
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
if prompts.roles_dir
|
|
289
|
+
prompts.roles_dir = File.expand_path(prompts.roles_dir)
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
if output.history_file
|
|
293
|
+
output.history_file = File.expand_path(output.history_file)
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def ensure_arrays
|
|
298
|
+
# Ensure array fields are actually arrays
|
|
299
|
+
self.pipeline = [] if pipeline.nil?
|
|
300
|
+
self.require_libs = [] if require_libs.nil?
|
|
301
|
+
self.context_files = [] if context_files.nil?
|
|
302
|
+
self.mcp_servers = [] if mcp_servers.nil?
|
|
303
|
+
|
|
304
|
+
# Ensure tools.paths is an array
|
|
305
|
+
tools.paths = [] if tools.paths.nil?
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# Process MCP JSON files and merge servers into mcp_servers
|
|
309
|
+
#
|
|
310
|
+
# @param mcp_files [Array<String>] paths to MCP JSON configuration files
|
|
311
|
+
def process_mcp_files(mcp_files)
|
|
312
|
+
return if mcp_files.nil? || mcp_files.empty?
|
|
313
|
+
|
|
314
|
+
servers_from_files = McpParser.parse_files(mcp_files)
|
|
315
|
+
return if servers_from_files.empty?
|
|
316
|
+
|
|
317
|
+
# Merge with existing mcp_servers (CLI files take precedence)
|
|
318
|
+
self.mcp_servers = (mcp_servers || []) + servers_from_files
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def apply_nested_override(parts, value)
|
|
322
|
+
section = parts[0].to_sym
|
|
323
|
+
key = parts[1].to_sym
|
|
324
|
+
|
|
325
|
+
case section
|
|
326
|
+
when :llm
|
|
327
|
+
llm.send("#{key}=", value) if llm.respond_to?("#{key}=")
|
|
328
|
+
when :prompts
|
|
329
|
+
prompts.send("#{key}=", value) if prompts.respond_to?("#{key}=")
|
|
330
|
+
when :output
|
|
331
|
+
output.send("#{key}=", value) if output.respond_to?("#{key}=")
|
|
332
|
+
when :audio
|
|
333
|
+
audio.send("#{key}=", value) if audio.respond_to?("#{key}=")
|
|
334
|
+
when :image
|
|
335
|
+
image.send("#{key}=", value) if image.respond_to?("#{key}=")
|
|
336
|
+
when :embedding
|
|
337
|
+
embedding.send("#{key}=", value) if embedding.respond_to?("#{key}=")
|
|
338
|
+
when :tools
|
|
339
|
+
tools.send("#{key}=", value) if tools.respond_to?("#{key}=")
|
|
340
|
+
when :flags
|
|
341
|
+
flags.send("#{key}=", value) if flags.respond_to?("#{key}=")
|
|
342
|
+
when :registry
|
|
343
|
+
registry.send("#{key}=", value) if registry.respond_to?("#{key}=")
|
|
344
|
+
when :paths
|
|
345
|
+
paths.send("#{key}=", value) if paths.respond_to?("#{key}=")
|
|
346
|
+
end
|
|
28
347
|
end
|
|
29
348
|
end
|
|
30
349
|
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# lib/aia/directive_processor.rb
|
|
2
2
|
|
|
3
|
-
require 'active_support/all'
|
|
3
|
+
# require 'active_support/all'
|
|
4
4
|
require 'faraday'
|
|
5
5
|
require 'word_wrapper'
|
|
6
6
|
require_relative 'directives/registry'
|
|
@@ -9,7 +9,7 @@ module AIA
|
|
|
9
9
|
class DirectiveProcessor
|
|
10
10
|
using Refinements
|
|
11
11
|
|
|
12
|
-
EXCLUDED_METHODS = %w[
|
|
12
|
+
EXCLUDED_METHODS = %w[run initialize private?]
|
|
13
13
|
|
|
14
14
|
def initialize
|
|
15
15
|
@prefix_size = PromptManager::Prompt::DIRECTIVE_SIGNAL.size
|
|
@@ -17,18 +17,24 @@ module AIA
|
|
|
17
17
|
Directives::WebAndFile.included_files = @included_files
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
+
|
|
20
21
|
def directive?(string)
|
|
21
22
|
Directives::Registry.directive?(string)
|
|
22
23
|
end
|
|
23
24
|
|
|
25
|
+
|
|
24
26
|
def process(string, context_manager)
|
|
25
27
|
return string unless directive?(string)
|
|
26
28
|
|
|
27
29
|
content = if string.is_a?(RubyLLM::Message)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
begin
|
|
31
|
+
string.content
|
|
32
|
+
rescue StandardError
|
|
33
|
+
string.to_s
|
|
34
|
+
end
|
|
35
|
+
else
|
|
36
|
+
string.to_s
|
|
37
|
+
end
|
|
32
38
|
|
|
33
39
|
key = content.strip
|
|
34
40
|
sans_prefix = key[@prefix_size..]
|
|
@@ -38,6 +44,7 @@ module AIA
|
|
|
38
44
|
Directives::Registry.process(method_name, args, context_manager)
|
|
39
45
|
end
|
|
40
46
|
|
|
47
|
+
|
|
41
48
|
def run(directives)
|
|
42
49
|
return {} if directives.nil? || directives.empty?
|
|
43
50
|
|
|
@@ -76,6 +83,7 @@ module AIA
|
|
|
76
83
|
end
|
|
77
84
|
end
|
|
78
85
|
|
|
86
|
+
|
|
79
87
|
def respond_to_missing?(method_name, include_private = false)
|
|
80
88
|
Directives::Registry.respond_to?(method_name, include_private) || super
|
|
81
89
|
end
|
|
@@ -7,12 +7,17 @@ module AIA
|
|
|
7
7
|
args = Array(args)
|
|
8
8
|
|
|
9
9
|
if args.empty?
|
|
10
|
-
ap AIA.config
|
|
10
|
+
ap AIA.config.to_h
|
|
11
11
|
""
|
|
12
12
|
elsif args.length == 1
|
|
13
13
|
config_item = args.first
|
|
14
|
-
local_cfg =
|
|
15
|
-
|
|
14
|
+
local_cfg = {}
|
|
15
|
+
# Use method-based access for Anyway::Config
|
|
16
|
+
if AIA.config.respond_to?(config_item)
|
|
17
|
+
local_cfg[config_item] = AIA.config.send(config_item)
|
|
18
|
+
else
|
|
19
|
+
local_cfg[config_item] = nil
|
|
20
|
+
end
|
|
16
21
|
ap local_cfg
|
|
17
22
|
""
|
|
18
23
|
else
|
|
@@ -24,7 +29,13 @@ module AIA
|
|
|
24
29
|
new_value = %w[true t yes y on 1 yea yeah yep yup].include?(new_value.downcase)
|
|
25
30
|
end
|
|
26
31
|
|
|
27
|
-
|
|
32
|
+
# Use method-based setter for Anyway::Config
|
|
33
|
+
setter = "#{config_item}="
|
|
34
|
+
if AIA.config.respond_to?(setter)
|
|
35
|
+
AIA.config.send(setter, new_value)
|
|
36
|
+
else
|
|
37
|
+
puts "Warning: Unknown config option '#{config_item}'"
|
|
38
|
+
end
|
|
28
39
|
""
|
|
29
40
|
end
|
|
30
41
|
end
|
|
@@ -33,29 +44,32 @@ module AIA
|
|
|
33
44
|
if args.empty?
|
|
34
45
|
# Display details for all configured models
|
|
35
46
|
puts
|
|
36
|
-
models =
|
|
47
|
+
models = AIA.config.models
|
|
37
48
|
|
|
38
49
|
if models.size == 1
|
|
39
50
|
puts "Current Model:"
|
|
40
51
|
puts "=============="
|
|
41
|
-
puts AIA.
|
|
52
|
+
puts AIA.client.model.to_h.pretty_inspect
|
|
42
53
|
else
|
|
43
54
|
puts "Multi-Model Configuration:"
|
|
44
55
|
puts "=========================="
|
|
45
56
|
puts "Model count: #{models.size}"
|
|
46
|
-
|
|
47
|
-
puts "
|
|
57
|
+
first_model = models.first.respond_to?(:name) ? models.first.name : models.first.to_s
|
|
58
|
+
puts "Primary model: #{first_model} (used for consensus when --consensus flag is enabled)"
|
|
59
|
+
consensus = AIA.config.flags.consensus
|
|
60
|
+
puts "Consensus mode: #{consensus.nil? ? 'auto-detect (disabled by default)' : consensus}"
|
|
48
61
|
puts
|
|
49
62
|
puts "Model Details:"
|
|
50
63
|
puts "-" * 50
|
|
51
64
|
|
|
52
|
-
models.each_with_index do |
|
|
65
|
+
models.each_with_index do |model_spec, index|
|
|
66
|
+
model_name = model_spec.respond_to?(:name) ? model_spec.name : model_spec.to_s
|
|
53
67
|
puts "#{index + 1}. #{model_name}#{index == 0 ? ' (primary)' : ''}"
|
|
54
68
|
|
|
55
69
|
# Try to get model details if available
|
|
56
70
|
begin
|
|
57
71
|
# Access the model details from RubyLLM's model registry
|
|
58
|
-
model_info = RubyLLM::Models.find(
|
|
72
|
+
model_info = RubyLLM::Models.find(model_name)
|
|
59
73
|
if model_info
|
|
60
74
|
puts " Provider: #{model_info.provider || 'Unknown'}"
|
|
61
75
|
puts " Context window: #{model_info.context_window || 'Unknown'}"
|
|
@@ -31,12 +31,11 @@ module AIA
|
|
|
31
31
|
|
|
32
32
|
def self.available_models(args = nil, context_manager = nil)
|
|
33
33
|
# Check if we're using a local provider
|
|
34
|
-
current_models = AIA.config.
|
|
35
|
-
current_models = [current_models] if current_models.is_a?(String)
|
|
34
|
+
current_models = AIA.config.models
|
|
36
35
|
|
|
37
|
-
# Extract model names (handles
|
|
36
|
+
# Extract model names (handles ModelSpec objects)
|
|
38
37
|
model_names = current_models.map do |m|
|
|
39
|
-
m.
|
|
38
|
+
m.respond_to?(:name) ? m.name : m.to_s
|
|
40
39
|
end
|
|
41
40
|
|
|
42
41
|
using_local_provider = model_names.any? { |m| m.start_with?('ollama/', 'lms/') }
|
|
@@ -14,10 +14,11 @@ module AIA
|
|
|
14
14
|
width = TTY::Screen.width - indent - 2
|
|
15
15
|
filter = args.first&.downcase
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
loaded_tools = AIA.config.loaded_tools || []
|
|
18
|
+
if loaded_tools.empty?
|
|
18
19
|
puts "No tools are available"
|
|
19
20
|
else
|
|
20
|
-
tools_to_display =
|
|
21
|
+
tools_to_display = loaded_tools
|
|
21
22
|
|
|
22
23
|
if filter
|
|
23
24
|
tools_to_display = tools_to_display.select do |tool|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# lib/aia/directives/web_and_file.rb
|
|
2
2
|
|
|
3
3
|
require 'faraday'
|
|
4
|
-
require 'active_support/all'
|
|
4
|
+
# require 'active_support/all'
|
|
5
5
|
require 'clipboard'
|
|
6
6
|
|
|
7
7
|
module AIA
|
|
@@ -9,73 +9,76 @@ module AIA
|
|
|
9
9
|
module WebAndFile
|
|
10
10
|
PUREMD_API_KEY = ENV.fetch('PUREMD_API_KEY', nil)
|
|
11
11
|
|
|
12
|
-
def self.webpage(args,
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
def self.webpage(args, _context_manager = nil)
|
|
13
|
+
if PUREMD_API_KEY.nil?
|
|
14
|
+
'ERROR: PUREMD_API_KEY is required in order to include a webpage'
|
|
15
|
+
else
|
|
16
|
+
url = `echo #{args.shift}`.strip
|
|
17
|
+
puremd_url = "https://pure.md/#{url}"
|
|
18
|
+
|
|
19
|
+
response = Faraday.get(puremd_url) do |req|
|
|
20
|
+
req.headers['x-puremd-api-token'] = PUREMD_API_KEY
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
if 200 == response.status
|
|
24
|
+
response.body
|
|
15
25
|
else
|
|
16
|
-
|
|
17
|
-
puremd_url = "https://pure.md/#{url}"
|
|
18
|
-
|
|
19
|
-
response = Faraday.get(puremd_url) do |req|
|
|
20
|
-
req.headers['x-puremd-api-token'] = PUREMD_API_KEY
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
if 200 == response.status
|
|
24
|
-
response.body
|
|
25
|
-
else
|
|
26
|
-
"Error: Status was #{response.status}\n#{ap response}"
|
|
27
|
-
end
|
|
26
|
+
"Error: Status was #{response.status}\n#{ap response}"
|
|
28
27
|
end
|
|
29
28
|
end
|
|
29
|
+
end
|
|
30
30
|
|
|
31
|
-
def self.include(args, context_manager = nil)
|
|
32
|
-
# echo takes care of envars and tilde expansion
|
|
33
|
-
file_path = `echo #{args.shift}`.strip
|
|
34
31
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
32
|
+
def self.include(args, _context_manager = nil)
|
|
33
|
+
# echo takes care of envars and tilde expansion
|
|
34
|
+
file_path = `echo #{args.shift}`.strip
|
|
35
|
+
|
|
36
|
+
if file_path.start_with?(%r{http?://})
|
|
37
|
+
webpage(args)
|
|
38
|
+
else
|
|
39
|
+
include_file(file_path)
|
|
40
40
|
end
|
|
41
|
+
end
|
|
41
42
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
end
|
|
53
|
-
end
|
|
43
|
+
|
|
44
|
+
def self.include_file(file_path)
|
|
45
|
+
@included_files ||= []
|
|
46
|
+
if @included_files.include?(file_path)
|
|
47
|
+
''
|
|
48
|
+
elsif File.exist?(file_path) && File.readable?(file_path)
|
|
49
|
+
@included_files << file_path
|
|
50
|
+
File.read(file_path)
|
|
51
|
+
else
|
|
52
|
+
"Error: File '#{file_path}' is not accessible"
|
|
54
53
|
end
|
|
54
|
+
end
|
|
55
|
+
|
|
55
56
|
|
|
56
57
|
def self.included_files
|
|
57
58
|
@included_files ||= []
|
|
58
59
|
end
|
|
59
60
|
|
|
61
|
+
|
|
60
62
|
def self.included_files=(files)
|
|
61
63
|
@included_files = files
|
|
62
64
|
end
|
|
63
65
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
66
|
+
|
|
67
|
+
def self.paste(_args = [], _context_manager = nil)
|
|
68
|
+
|
|
69
|
+
content = Clipboard.paste
|
|
70
|
+
content.to_s
|
|
71
|
+
rescue StandardError => e
|
|
72
|
+
"Error: Unable to paste from clipboard - #{e.message}"
|
|
73
|
+
|
|
71
74
|
end
|
|
72
75
|
|
|
73
76
|
# Set up aliases - these work on the module's singleton class
|
|
74
77
|
class << self
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
78
|
+
alias website webpage
|
|
79
|
+
alias web webpage
|
|
80
|
+
alias import include
|
|
81
|
+
alias clipboard paste
|
|
79
82
|
end
|
|
80
83
|
end
|
|
81
84
|
end
|