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.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/.version +1 -1
  3. data/CHANGELOG.md +84 -3
  4. data/README.md +179 -59
  5. data/bin/aia +6 -0
  6. data/docs/cli-reference.md +145 -72
  7. data/docs/configuration.md +156 -19
  8. data/docs/examples/tools/index.md +2 -2
  9. data/docs/faq.md +11 -11
  10. data/docs/guides/available-models.md +11 -11
  11. data/docs/guides/basic-usage.md +18 -17
  12. data/docs/guides/chat.md +57 -11
  13. data/docs/guides/executable-prompts.md +15 -15
  14. data/docs/guides/first-prompt.md +2 -2
  15. data/docs/guides/getting-started.md +6 -6
  16. data/docs/guides/image-generation.md +24 -24
  17. data/docs/guides/local-models.md +2 -2
  18. data/docs/guides/models.md +96 -18
  19. data/docs/guides/tools.md +4 -4
  20. data/docs/installation.md +2 -2
  21. data/docs/prompt_management.md +11 -11
  22. data/docs/security.md +3 -3
  23. data/docs/workflows-and-pipelines.md +1 -1
  24. data/examples/README.md +6 -6
  25. data/examples/headlines +3 -3
  26. data/lib/aia/aia_completion.bash +2 -2
  27. data/lib/aia/aia_completion.fish +4 -4
  28. data/lib/aia/aia_completion.zsh +2 -2
  29. data/lib/aia/chat_processor_service.rb +31 -21
  30. data/lib/aia/config/cli_parser.rb +403 -403
  31. data/lib/aia/config/config_section.rb +87 -0
  32. data/lib/aia/config/defaults.yml +219 -0
  33. data/lib/aia/config/defaults_loader.rb +147 -0
  34. data/lib/aia/config/mcp_parser.rb +151 -0
  35. data/lib/aia/config/model_spec.rb +67 -0
  36. data/lib/aia/config/validator.rb +185 -136
  37. data/lib/aia/config.rb +336 -17
  38. data/lib/aia/directive_processor.rb +14 -6
  39. data/lib/aia/directives/configuration.rb +24 -10
  40. data/lib/aia/directives/models.rb +3 -4
  41. data/lib/aia/directives/utility.rb +3 -2
  42. data/lib/aia/directives/web_and_file.rb +50 -47
  43. data/lib/aia/logger.rb +328 -0
  44. data/lib/aia/prompt_handler.rb +18 -22
  45. data/lib/aia/ruby_llm_adapter.rb +572 -69
  46. data/lib/aia/session.rb +9 -8
  47. data/lib/aia/ui_presenter.rb +20 -16
  48. data/lib/aia/utility.rb +50 -18
  49. data/lib/aia.rb +91 -66
  50. data/lib/extensions/ruby_llm/modalities.rb +2 -0
  51. data/mcp_servers/apple-mcp.json +8 -0
  52. data/mcp_servers/mcp_server_chart.json +11 -0
  53. data/mcp_servers/playwright_one.json +8 -0
  54. data/mcp_servers/playwright_two.json +8 -0
  55. data/mcp_servers/tavily_mcp_server.json +8 -0
  56. metadata +83 -25
  57. data/lib/aia/config/base.rb +0 -308
  58. data/lib/aia/config/defaults.rb +0 -91
  59. data/lib/aia/config/file_loader.rb +0 -163
  60. data/mcp_servers/imcp.json +0 -7
  61. data/mcp_servers/launcher.json +0 -11
  62. data/mcp_servers/timeserver.json +0 -8
data/lib/aia/logger.rb ADDED
@@ -0,0 +1,328 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/aia/logger.rb
4
+ #
5
+ # Centralized logger management for AIA using Lumberjack.
6
+ # Provides loggers for three systems:
7
+ # - aia: Used within the AIA codebase for application-level logging
8
+ # - llm: Passed to RubyLLM gem's configuration (RubyLLM.logger)
9
+ # - mcp: Passed to RubyLLM::MCP process (RubyLLM::MCP.logger)
10
+ #
11
+ # Configuration is read from AIA.config.logger section:
12
+ # logger:
13
+ # aia:
14
+ # file: STDOUT
15
+ # level: warn
16
+ # llm:
17
+ # file: STDOUT
18
+ # level: warn
19
+ # mcp:
20
+ # file: STDOUT
21
+ # level: warn
22
+ #
23
+ # Lumberjack provides structured logging, context isolation,
24
+ # automatic log file rolling, and multi-process safe file writes.
25
+ #
26
+ # For testing, use test_mode! to switch to Lumberjack's :test device:
27
+ # AIA::LoggerManager.test_mode!
28
+ # # ... run tests ...
29
+ # AIA::LoggerManager.clear_test_logs! # between tests
30
+ # entries = AIA::LoggerManager.aia_logger.device.entries # inspect logs
31
+
32
+ require 'lumberjack'
33
+
34
+ module AIA
35
+ module LoggerManager
36
+ # Log level mapping from config strings to Lumberjack severity constants
37
+ LOG_LEVELS = {
38
+ 'debug' => Lumberjack::Severity::DEBUG,
39
+ 'info' => Lumberjack::Severity::INFO,
40
+ 'warn' => Lumberjack::Severity::WARN,
41
+ 'error' => Lumberjack::Severity::ERROR,
42
+ 'fatal' => Lumberjack::Severity::FATAL
43
+ }.freeze
44
+
45
+ class << self
46
+ # Track whether we're in test mode
47
+ attr_accessor :test_mode
48
+
49
+ # Get or create the AIA application logger
50
+ #
51
+ # @return [Lumberjack::Logger] The AIA logger instance
52
+ def aia_logger
53
+ @aia_logger ||= create_logger(:aia)
54
+ end
55
+
56
+ # Get or create the RubyLLM logger
57
+ #
58
+ # @return [Lumberjack::Logger] The LLM logger instance
59
+ def llm_logger
60
+ @llm_logger ||= create_logger(:llm)
61
+ end
62
+
63
+ # Get or create the RubyLLM::MCP logger
64
+ #
65
+ # @return [Lumberjack::Logger] The MCP logger instance
66
+ def mcp_logger
67
+ @mcp_logger ||= create_logger(:mcp)
68
+ end
69
+
70
+ # Configure RubyLLM's logger
71
+ # RubyLLM.logger is a memoized method that uses config.logger or creates a new one.
72
+ # We need to:
73
+ # 1. Set config.logger to our logger
74
+ # 2. Set config.log_file in case the logger gets recreated
75
+ # 3. Reset the memoized @logger so next call uses our config
76
+ def configure_llm_logger
77
+ return unless defined?(RubyLLM)
78
+
79
+ logger = llm_logger
80
+
81
+ # Set our logger on the RubyLLM config object
82
+ if RubyLLM.config.respond_to?(:logger=)
83
+ RubyLLM.config.logger = logger
84
+ end
85
+
86
+ # Also set log_file and log_level in case the logger gets recreated
87
+ if RubyLLM.config.respond_to?(:log_file=)
88
+ file = effective_log_file(logger_config_for(:llm))
89
+ RubyLLM.config.log_file = resolve_log_file_io(file)
90
+ end
91
+
92
+ if RubyLLM.config.respond_to?(:log_level=)
93
+ level = effective_log_level(logger_config_for(:llm))
94
+ RubyLLM.config.log_level = LOG_LEVELS.fetch(level, Lumberjack::Severity::WARN)
95
+ end
96
+
97
+ # Reset the memoized @logger on RubyLLM module so next call uses our config
98
+ # RubyLLM.logger does: @logger ||= config.logger || Logger.new(...)
99
+ RubyLLM.instance_variable_set(:@logger, nil) if RubyLLM.instance_variable_defined?(:@logger)
100
+ end
101
+
102
+ # Configure RubyLLM::MCP's logger
103
+ # RubyLLM::MCP.config has attr_writer :logger and a memoized getter.
104
+ # We need to set both the config.logger and reset any memoization.
105
+ def configure_mcp_logger
106
+ return unless defined?(RubyLLM::MCP)
107
+
108
+ logger = mcp_logger
109
+
110
+ # First reset the memoized @logger so our settings take effect
111
+ # RubyLLM::MCP.config.logger does: @logger ||= Logger.new(...)
112
+ config = RubyLLM::MCP.config
113
+ config.instance_variable_set(:@logger, nil) if config.instance_variable_defined?(:@logger)
114
+
115
+ # Set our logger on the MCP config object
116
+ # RubyLLM::MCP.config has attr_writer :logger (which sets @logger directly)
117
+ if config.respond_to?(:logger=)
118
+ config.logger = logger
119
+ end
120
+
121
+ # Also set log_file and log_level in case the logger gets recreated
122
+ if config.respond_to?(:log_file=)
123
+ file = effective_log_file(logger_config_for(:mcp))
124
+ config.log_file = resolve_log_file_io(file)
125
+ end
126
+
127
+ if config.respond_to?(:log_level=)
128
+ level = effective_log_level(logger_config_for(:mcp))
129
+ config.log_level = LOG_LEVELS.fetch(level, Lumberjack::Severity::WARN)
130
+ end
131
+ end
132
+
133
+ # Convert log file specification to IO object or file path
134
+ # RubyLLM::MCP expects an IO object or file path for log_file config
135
+ def resolve_log_file_io(file)
136
+ case file.to_s.upcase
137
+ when 'STDOUT'
138
+ $stdout
139
+ when 'STDERR'
140
+ $stderr
141
+ else
142
+ File.expand_path(file)
143
+ end
144
+ end
145
+
146
+ # Get the log level symbol for RubyLLM configuration
147
+ #
148
+ # @return [Symbol] The log level as a symbol (e.g., :warn, :debug)
149
+ def llm_log_level_symbol
150
+ config = logger_config_for(:llm)
151
+ level = effective_log_level(config)
152
+ level.to_sym
153
+ end
154
+
155
+ # Reset all cached loggers (useful for testing or reconfiguration)
156
+ def reset!
157
+ @aia_logger = nil
158
+ @llm_logger = nil
159
+ @mcp_logger = nil
160
+ @test_mode = false
161
+ end
162
+
163
+ # =======================================================================
164
+ # Test Mode Support
165
+ # =======================================================================
166
+ # Use Lumberjack's :test device to capture log entries in memory
167
+ # for assertions in tests.
168
+
169
+ # Enable test mode - all loggers will use Lumberjack's :test device
170
+ # which captures entries in memory for inspection and assertions.
171
+ #
172
+ # @param level [Symbol, String] Log level for test loggers (default: :debug)
173
+ def test_mode!(level: :debug)
174
+ reset!
175
+ @test_mode = true
176
+ @test_level = LOG_LEVELS.fetch(level.to_s, Lumberjack::Severity::DEBUG)
177
+
178
+ # Pre-create loggers with test devices
179
+ @aia_logger = create_test_logger(:aia)
180
+ @llm_logger = create_test_logger(:llm)
181
+ @mcp_logger = create_test_logger(:mcp)
182
+
183
+ # Surface logging errors in tests instead of swallowing them
184
+ Lumberjack.raise_logger_errors = true
185
+ end
186
+
187
+ # Check if test mode is enabled
188
+ #
189
+ # @return [Boolean] true if in test mode
190
+ def test_mode?
191
+ @test_mode == true
192
+ end
193
+
194
+ # Clear all test log entries (call between tests)
195
+ def clear_test_logs!
196
+ return unless test_mode?
197
+
198
+ [@aia_logger, @llm_logger, @mcp_logger].each do |logger|
199
+ logger&.device&.clear if logger&.device.respond_to?(:clear)
200
+ end
201
+ end
202
+
203
+ # Get all entries from a specific test logger
204
+ #
205
+ # @param system [Symbol] The logger to get entries from (:aia, :llm, :mcp)
206
+ # @return [Array<Lumberjack::LogEntry>] Array of log entries
207
+ def test_entries(system = :aia)
208
+ logger = case system
209
+ when :aia then aia_logger
210
+ when :llm then llm_logger
211
+ when :mcp then mcp_logger
212
+ else raise ArgumentError, "Unknown logger: #{system}"
213
+ end
214
+
215
+ return [] unless logger&.device.respond_to?(:entries)
216
+
217
+ logger.device.entries
218
+ end
219
+
220
+ # Get the last entry from a specific test logger
221
+ #
222
+ # @param system [Symbol] The logger to get entry from (:aia, :llm, :mcp)
223
+ # @return [Lumberjack::LogEntry, nil] The last log entry or nil
224
+ def last_test_entry(system = :aia)
225
+ test_entries(system).last
226
+ end
227
+
228
+ private
229
+
230
+ # Create a test logger with Lumberjack's :test device
231
+ #
232
+ # @param system [Symbol] The system name for progname
233
+ # @return [Lumberjack::Logger] Logger with test device
234
+ def create_test_logger(system)
235
+ Lumberjack::Logger.new(
236
+ :test,
237
+ level: @test_level || Lumberjack::Severity::DEBUG,
238
+ progname: system.to_s.upcase
239
+ )
240
+ end
241
+
242
+ # Create a logger instance from configuration
243
+ #
244
+ # @param system [Symbol] The system to create a logger for (:aia, :llm, :mcp)
245
+ # @return [Lumberjack::Logger] Configured logger instance
246
+ def create_logger(system)
247
+ config = logger_config_for(system)
248
+
249
+ file = effective_log_file(config)
250
+ level = effective_log_level(config, system)
251
+ flush = config&.flush != false # default to true
252
+
253
+ device = create_device(file, flush: flush)
254
+ Lumberjack::Logger.new(
255
+ device,
256
+ level: LOG_LEVELS.fetch(level, Lumberjack::Severity::WARN),
257
+ progname: system.to_s.upcase
258
+ )
259
+ end
260
+
261
+ # Get the effective log file, considering any override
262
+ #
263
+ # @param config [ConfigSection, nil] The logger config for a specific system
264
+ # @return [String] The log file path or STDOUT/STDERR
265
+ def effective_log_file(config)
266
+ # CLI override (--log-to) takes precedence over per-system config
267
+ override = AIA.config&.log_file_override
268
+ return override if override && !override.to_s.empty?
269
+
270
+ config&.file || 'STDOUT'
271
+ end
272
+
273
+ # Get the effective log level, considering any override
274
+ #
275
+ # @param config [ConfigSection, nil] The logger config for a specific system
276
+ # @return [String] The log level to use
277
+ def effective_log_level(config, _system = nil)
278
+ # CLI override takes precedence over per-system config
279
+ override = AIA.config&.log_level_override
280
+ return override.to_s.downcase if override && !override.to_s.empty?
281
+
282
+ config&.level&.to_s&.downcase || 'warn'
283
+ end
284
+
285
+ # Create appropriate Lumberjack device based on file config
286
+ #
287
+ # @param file [String] The file config value
288
+ # @param flush [Boolean] If true, flush immediately (no buffering)
289
+ # @return [Lumberjack::Device] The device instance
290
+ def create_device(file, flush: true)
291
+ # buffer_size: 0 means immediate flush (no buffering)
292
+ buffer_size = flush ? 0 : 8192
293
+
294
+ case file.to_s.upcase
295
+ when 'STDOUT'
296
+ Lumberjack::Device::Writer.new($stdout, buffer_size: buffer_size)
297
+ when 'STDERR'
298
+ Lumberjack::Device::Writer.new($stderr, buffer_size: buffer_size)
299
+ else
300
+ path = File.expand_path(file)
301
+ # Use date rolling for file-based logs
302
+ # Multiple loggers can safely write to the same file
303
+ Lumberjack::Device::DateRollingLogFile.new(
304
+ path,
305
+ roll: :daily,
306
+ buffer_size: buffer_size
307
+ )
308
+ end
309
+ end
310
+
311
+ # Get the logger configuration for a specific system
312
+ #
313
+ # @param system [Symbol] The system (:aia, :llm, :mcp)
314
+ # @return [ConfigSection, nil] The configuration section
315
+ def logger_config_for(system)
316
+ return nil unless AIA.config&.logger
317
+
318
+ case system
319
+ when :aia then AIA.config.logger.aia
320
+ when :llm then AIA.config.logger.llm
321
+ when :mcp then AIA.config.logger.mcp
322
+ end
323
+ rescue NoMethodError
324
+ nil
325
+ end
326
+ end
327
+ end
328
+ end
@@ -8,14 +8,14 @@ require 'erb'
8
8
  module AIA
9
9
  class PromptHandler
10
10
  def initialize
11
- @prompts_dir = AIA.config.prompts_dir
12
- @roles_dir = AIA.config.roles_dir # A sub-directory of @prompts_dir
11
+ @prompts_dir = AIA.config.prompts.dir
12
+ @roles_dir = AIA.config.prompts.roles_dir # A sub-directory of @prompts_dir
13
13
  @directive_processor = AIA::DirectiveProcessor.new
14
14
 
15
15
  PromptManager::Prompt.storage_adapter =
16
16
  PromptManager::Storage::FileSystemAdapter.config do |c|
17
17
  c.prompts_dir = @prompts_dir
18
- c.prompt_extension = '.txt' # default
18
+ c.prompt_extension = AIA.config.prompts.extname
19
19
  c.params_extension = '.json' # default
20
20
  end.new
21
21
  end
@@ -24,7 +24,7 @@ module AIA
24
24
  def get_prompt(prompt_id, role_id = '')
25
25
  prompt = fetch_prompt(prompt_id)
26
26
 
27
- unless role_id.empty?
27
+ unless role_id.nil? || role_id.empty?
28
28
  role_prompt = fetch_role(role_id)
29
29
  prompt.text.prepend(role_prompt.text)
30
30
  end
@@ -45,8 +45,8 @@ module AIA
45
45
  id: prompt_id,
46
46
  directives_processor: @directive_processor,
47
47
  external_binding: binding,
48
- erb_flag: AIA.config.erb,
49
- envar_flag: AIA.config.shell
48
+ erb_flag: AIA.config.flags.erb,
49
+ envar_flag: AIA.config.flags.shell
50
50
  )
51
51
 
52
52
  # Parameters should be extracted during initialization or to_s
@@ -66,12 +66,8 @@ module AIA
66
66
  exit 1
67
67
  end
68
68
 
69
- if AIA.config.fuzzy
69
+ if AIA.config.flags.fuzzy
70
70
  return fuzzy_search_prompt(prompt_id)
71
- elsif AIA.config.fuzzy
72
- puts "Warning: Fuzzy search is enabled but Fzf tool is not available."
73
- STDERR.puts "Error: Could not find prompt with ID: #{prompt_id}"
74
- exit 1
75
71
  else
76
72
  STDERR.puts "Error: Could not find prompt with ID: #{prompt_id}"
77
73
  exit 1
@@ -89,8 +85,8 @@ module AIA
89
85
  id: new_prompt_id,
90
86
  directives_processor: @directive_processor,
91
87
  external_binding: binding,
92
- erb_flag: AIA.config.erb,
93
- envar_flag: AIA.config.shell
88
+ erb_flag: AIA.config.flags.erb,
89
+ envar_flag: AIA.config.flags.shell
94
90
  )
95
91
 
96
92
  raise "Error: Could not find prompt with ID: #{prompt_id} even with fuzzy search" if prompt.nil?
@@ -103,8 +99,8 @@ module AIA
103
99
  return handle_missing_role("roles/") if role_id.nil?
104
100
 
105
101
  # Prepend roles_prefix if not already present
106
- unless role_id.start_with?(AIA.config.roles_prefix)
107
- role_id = "#{AIA.config.roles_prefix}/#{role_id}"
102
+ unless role_id.start_with?(AIA.config.prompts.roles_prefix)
103
+ role_id = "#{AIA.config.prompts.roles_prefix}/#{role_id}"
108
104
  end
109
105
 
110
106
  # NOTE: roles_prefix is a sub-directory of the prompts directory
@@ -115,8 +111,8 @@ module AIA
115
111
  id: role_id,
116
112
  directives_processor: @directive_processor,
117
113
  external_binding: binding,
118
- erb_flag: AIA.config.erb,
119
- envar_flag: AIA.config.shell
114
+ erb_flag: AIA.config.flags.erb,
115
+ envar_flag: AIA.config.flags.shell
120
116
  )
121
117
  return role_prompt if role_prompt
122
118
  else
@@ -155,7 +151,7 @@ module AIA
155
151
  exit 1
156
152
  end
157
153
 
158
- if AIA.config.fuzzy
154
+ if AIA.config.flags.fuzzy
159
155
  return fuzzy_search_role(role_id)
160
156
  else
161
157
  STDERR.puts "Error: Could not find role with ID: #{role_id}"
@@ -173,8 +169,8 @@ module AIA
173
169
  id: new_role_id,
174
170
  directives_processor: @directive_processor,
175
171
  external_binding: binding,
176
- erb_flag: AIA.config.erb,
177
- envar_flag: AIA.config.shell
172
+ erb_flag: AIA.config.flags.erb,
173
+ envar_flag: AIA.config.flags.shell
178
174
  )
179
175
 
180
176
  raise "Error: Could not find role with ID: #{role_id} even with fuzzy search" if role_prompt.nil?
@@ -214,8 +210,8 @@ module AIA
214
210
  raise "No role ID selected"
215
211
  end
216
212
 
217
- unless role.start_with?(AIA.config.role_prefix)
218
- role = AIA.config.role_prefix + '/' + role
213
+ unless role.start_with?(AIA.config.prompts.roles_prefix)
214
+ role = AIA.config.prompts.roles_prefix + '/' + role
219
215
  end
220
216
 
221
217
  role