aia 0.9.23 → 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.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/.version +1 -1
  3. data/CHANGELOG.md +95 -3
  4. data/README.md +187 -60
  5. data/bin/aia +6 -0
  6. data/docs/cli-reference.md +145 -72
  7. data/docs/configuration.md +156 -19
  8. data/docs/directives-reference.md +28 -8
  9. data/docs/examples/tools/index.md +2 -2
  10. data/docs/faq.md +11 -11
  11. data/docs/guides/available-models.md +11 -11
  12. data/docs/guides/basic-usage.md +18 -17
  13. data/docs/guides/chat.md +57 -11
  14. data/docs/guides/executable-prompts.md +15 -15
  15. data/docs/guides/first-prompt.md +2 -2
  16. data/docs/guides/getting-started.md +6 -6
  17. data/docs/guides/image-generation.md +24 -24
  18. data/docs/guides/local-models.md +2 -2
  19. data/docs/guides/models.md +96 -18
  20. data/docs/guides/tools.md +4 -4
  21. data/docs/index.md +2 -2
  22. data/docs/installation.md +2 -2
  23. data/docs/prompt_management.md +11 -11
  24. data/docs/security.md +3 -3
  25. data/docs/workflows-and-pipelines.md +1 -1
  26. data/examples/README.md +6 -6
  27. data/examples/headlines +3 -3
  28. data/lib/aia/aia_completion.bash +2 -2
  29. data/lib/aia/aia_completion.fish +4 -4
  30. data/lib/aia/aia_completion.zsh +2 -2
  31. data/lib/aia/chat_processor_service.rb +31 -21
  32. data/lib/aia/config/cli_parser.rb +403 -403
  33. data/lib/aia/config/config_section.rb +87 -0
  34. data/lib/aia/config/defaults.yml +219 -0
  35. data/lib/aia/config/defaults_loader.rb +147 -0
  36. data/lib/aia/config/mcp_parser.rb +151 -0
  37. data/lib/aia/config/model_spec.rb +67 -0
  38. data/lib/aia/config/validator.rb +185 -136
  39. data/lib/aia/config.rb +336 -17
  40. data/lib/aia/directive_processor.rb +14 -6
  41. data/lib/aia/directives/checkpoint.rb +283 -0
  42. data/lib/aia/directives/configuration.rb +27 -98
  43. data/lib/aia/directives/models.rb +15 -9
  44. data/lib/aia/directives/registry.rb +2 -0
  45. data/lib/aia/directives/utility.rb +25 -9
  46. data/lib/aia/directives/web_and_file.rb +50 -47
  47. data/lib/aia/logger.rb +328 -0
  48. data/lib/aia/prompt_handler.rb +18 -22
  49. data/lib/aia/ruby_llm_adapter.rb +584 -65
  50. data/lib/aia/session.rb +49 -156
  51. data/lib/aia/topic_context.rb +125 -0
  52. data/lib/aia/ui_presenter.rb +20 -16
  53. data/lib/aia/utility.rb +50 -18
  54. data/lib/aia.rb +91 -66
  55. data/lib/extensions/ruby_llm/modalities.rb +2 -0
  56. data/mcp_servers/apple-mcp.json +8 -0
  57. data/mcp_servers/mcp_server_chart.json +11 -0
  58. data/mcp_servers/playwright_one.json +8 -0
  59. data/mcp_servers/playwright_two.json +8 -0
  60. data/mcp_servers/tavily_mcp_server.json +8 -0
  61. metadata +85 -26
  62. data/lib/aia/config/base.rb +0 -288
  63. data/lib/aia/config/defaults.rb +0 -91
  64. data/lib/aia/config/file_loader.rb +0 -163
  65. data/lib/aia/context_manager.rb +0 -134
  66. data/mcp_servers/imcp.json +0 -7
  67. data/mcp_servers/launcher.json +0 -11
  68. 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
- # This file contains the configuration settings for the AIA application.
4
- # The Config class is responsible for managing configuration settings
5
- # for the AIA application. It provides methods to parse command-line
6
- # arguments, environment variables, and configuration files.
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/base'
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
- # Delegate all functionality to the modular config system
13
- def self.setup
14
- ConfigModules::Base.setup
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
- # Maintain backward compatibility by delegating to Base module
18
- def self.method_missing(method_name, *args, &block)
19
- if ConfigModules::Base.respond_to?(method_name)
20
- ConfigModules::Base.send(method_name, *args, &block)
21
- else
22
- super
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
- def self.respond_to_missing?(method_name, include_private = false)
27
- ConfigModules::Base.respond_to?(method_name, include_private) || super
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[ run initialize private? ]
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
- string.content rescue string.to_s
29
- else
30
- string.to_s
31
- end
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