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/session.rb CHANGED
@@ -53,8 +53,9 @@ module AIA
53
53
  end
54
54
 
55
55
  def setup_output_file
56
- if AIA.config.out_file && !AIA.config.out_file.nil? && !AIA.append? && File.exist?(AIA.config.out_file)
57
- File.open(AIA.config.out_file, "w") { } # Truncate the file
56
+ out_file = AIA.config.output.file
57
+ if out_file && !out_file.nil? && !AIA.append? && File.exist?(out_file)
58
+ File.open(out_file, "w") { } # Truncate the file
58
59
  end
59
60
  end
60
61
 
@@ -117,7 +118,7 @@ module AIA
117
118
  end
118
119
 
119
120
  def setup_prompt_processing(prompt_id)
120
- role_id = AIA.config.role
121
+ role_id = AIA.config.prompts.role
121
122
 
122
123
  begin
123
124
  prompt = @prompt_handler.get_prompt(prompt_id, role_id)
@@ -351,8 +352,8 @@ module AIA
351
352
 
352
353
  break if follow_up_prompt.nil? || follow_up_prompt.strip.downcase == "exit" || follow_up_prompt.strip.empty?
353
354
 
354
- if AIA.config.out_file
355
- File.open(AIA.config.out_file, "a") do |file|
355
+ if AIA.config.output.file
356
+ File.open(AIA.config.output.file, "a") do |file|
356
357
  file.puts "\nYou: #{follow_up_prompt}"
357
358
  end
358
359
  end
@@ -383,8 +384,8 @@ module AIA
383
384
 
384
385
  @ui_presenter.display_ai_response(content)
385
386
 
386
- # Display metrics if enabled and available (chat mode only)
387
- if AIA.config.show_metrics
387
+ # Display token usage if enabled and available (chat mode only)
388
+ if AIA.config.flags.tokens
388
389
  if multi_metrics
389
390
  # Display metrics for each model in multi-model mode
390
391
  @ui_presenter.display_multi_model_metrics(multi_metrics)
@@ -471,7 +472,7 @@ module AIA
471
472
 
472
473
  def cleanup_chat_prompt
473
474
  if @chat_prompt_id
474
- puts "[DEBUG] Cleaning up chat prompt: #{@chat_prompt_id}" if AIA.debug?
475
+ logger.debug("Cleaning up chat prompt", chat_prompt_id: @chat_prompt_id)
475
476
  begin
476
477
  @chat_prompt.delete
477
478
  @chat_prompt_id = nil # Prevent repeated attempts if error occurs elsewhere
@@ -26,8 +26,9 @@ module AIA
26
26
  puts "\nAI: "
27
27
  format_chat_response(response)
28
28
 
29
- if AIA.config.out_file && !AIA.config.out_file.nil?
30
- File.open(AIA.config.out_file, 'a') do |file|
29
+ out_file = AIA.config.output.file
30
+ if out_file && !out_file.nil?
31
+ File.open(out_file, 'a') do |file|
31
32
  file.puts "\nAI: "
32
33
  format_chat_response(response, file)
33
34
  end
@@ -125,7 +126,7 @@ module AIA
125
126
  output_lines << "═" * 55
126
127
  output_lines << "Model: #{metrics[:model_id]}"
127
128
 
128
- if AIA.config.show_cost
129
+ if AIA.config.flags.cost
129
130
  output_lines.concat(format_metrics_with_cost(metrics))
130
131
  else
131
132
  output_lines.concat(format_metrics_basic(metrics))
@@ -137,20 +138,21 @@ module AIA
137
138
  output_lines.each { |line| puts line }
138
139
 
139
140
  # Also write to file if configured
140
- if AIA.config.out_file && !AIA.config.out_file.nil?
141
- File.open(AIA.config.out_file, 'a') do |file|
141
+ out_file = AIA.config.output.file
142
+ if out_file && !out_file.nil?
143
+ File.open(out_file, 'a') do |file|
142
144
  output_lines.each { |line| file.puts line }
143
145
  end
144
146
  end
145
147
  end
146
-
148
+
147
149
  def display_multi_model_metrics(metrics_list)
148
150
  return unless metrics_list && !metrics_list.empty?
149
151
 
150
152
  output_lines = []
151
153
 
152
154
  # Determine table width based on whether costs are shown
153
- if AIA.config.show_cost
155
+ if AIA.config.flags.cost
154
156
  table_width = 80
155
157
  else
156
158
  table_width = 60
@@ -161,7 +163,7 @@ module AIA
161
163
  output_lines << "─" * table_width
162
164
 
163
165
  # Build header row
164
- if AIA.config.show_cost
166
+ if AIA.config.flags.cost
165
167
  output_lines << sprintf("%-20s %10s %10s %10s %12s %10s",
166
168
  "Model", "Input", "Output", "Total", "Cost", "x1000")
167
169
  output_lines << "─" * table_width
@@ -177,15 +179,16 @@ module AIA
177
179
  total_cost = 0.0
178
180
 
179
181
  metrics_list.each do |metrics|
180
- model_name = metrics[:model_id]
182
+ # Use display_name if available (includes role), otherwise fall back to model_id
183
+ model_name = metrics[:display_name] || metrics[:model_id]
181
184
  # Truncate model name if too long
182
- model_name = model_name[0..17] + ".." if model_name.length > 19
183
-
185
+ model_name = model_name[0..17] + ".." if model_name.to_s.length > 19
186
+
184
187
  input_tokens = metrics[:input_tokens] || 0
185
188
  output_tokens = metrics[:output_tokens] || 0
186
189
  total_tokens = input_tokens + output_tokens
187
190
 
188
- if AIA.config.show_cost
191
+ if AIA.config.flags.cost
189
192
  cost_data = calculate_cost(metrics)
190
193
  if cost_data[:available]
191
194
  cost_str = "$#{'%.5f' % cost_data[:total_cost]}"
@@ -211,7 +214,7 @@ module AIA
211
214
  output_lines << "─" * table_width
212
215
  total_tokens = total_input + total_output
213
216
 
214
- if AIA.config.show_cost && total_cost > 0
217
+ if AIA.config.flags.cost && total_cost > 0
215
218
  cost_str = "$#{'%.5f' % total_cost}"
216
219
  x1000_str = "$#{'%.2f' % (total_cost * 1000)}"
217
220
  output_lines << sprintf("%-20s %10d %10d %10d %12s %10s",
@@ -227,13 +230,14 @@ module AIA
227
230
  output_lines.each { |line| puts line }
228
231
 
229
232
  # Also write to file if configured
230
- if AIA.config.out_file && !AIA.config.out_file.nil?
231
- File.open(AIA.config.out_file, 'a') do |file|
233
+ out_file = AIA.config.output.file
234
+ if out_file && !out_file.nil?
235
+ File.open(out_file, 'a') do |file|
232
236
  output_lines.each { |line| file.puts line }
233
237
  end
234
238
  end
235
239
  end
236
-
240
+
237
241
  private
238
242
 
239
243
  def display_metrics_basic(metrics)
data/lib/aia/utility.rb CHANGED
@@ -10,22 +10,49 @@ module AIA
10
10
  end
11
11
 
12
12
  def user_tools?
13
- AIA.config&.tool_paths && !AIA.config.tool_paths.empty?
13
+ AIA.config&.tools&.paths && !AIA.config.tools.paths.empty?
14
14
  end
15
15
 
16
16
  def mcp_servers?
17
17
  AIA.config&.mcp_servers && !AIA.config.mcp_servers.empty?
18
18
  end
19
19
 
20
+ # Returns only successfully connected MCP server names
20
21
  def mcp_server_names
22
+ # Use connected_mcp_servers if available (populated during MCP setup)
23
+ connected = AIA.config&.connected_mcp_servers
24
+ return connected if connected && !connected.empty?
25
+
26
+ # Fallback to configured servers if connection status not yet known
21
27
  return [] unless mcp_servers?
22
28
  AIA.config.mcp_servers.map { |s| s[:name] || s["name"] }.compact
23
29
  end
24
30
 
31
+ # Returns true if there are any connected MCP servers
32
+ def connected_mcp_servers?
33
+ connected = AIA.config&.connected_mcp_servers
34
+ connected && !connected.empty?
35
+ end
36
+
37
+ # Returns list of failed MCP servers with their errors
38
+ def failed_mcp_servers
39
+ AIA.config&.failed_mcp_servers || []
40
+ end
41
+
25
42
  def supports_tools?
26
- AIA.config&.client&.model&.supports_functions? || false
43
+ AIA.client&.model&.supports_functions? || false
27
44
  end
28
45
 
46
+ # Returns the last refresh date from models.json modification time
47
+ def models_last_refresh
48
+ aia_dir = AIA.config&.paths&.aia_dir
49
+ return nil if aia_dir.nil?
50
+
51
+ models_file = File.join(File.expand_path(aia_dir), 'models.json')
52
+ return nil unless File.exist?(models_file)
53
+
54
+ File.mtime(models_file).strftime('%Y-%m-%d %H:%M')
55
+ end
29
56
 
30
57
  # Displays the AIA robot ASCII art
31
58
  # Yes, its slightly frivolous but it does contain some
@@ -37,40 +64,45 @@ module AIA
37
64
 
38
65
  mcp_version = defined?(RubyLLM::MCP::VERSION) ? " MCP v" + RubyLLM::MCP::VERSION : ''
39
66
 
40
- # Extract model names from config (handles hash format from ADR-005)
41
- model_display = if AIA.config&.model
42
- models = AIA.config.model
43
- if models.is_a?(String)
44
- models
45
- elsif models.is_a?(Array)
46
- if models.first.is_a?(Hash)
47
- models.map { |spec| spec[:model] }.join(', ')
67
+ # Extract model names from config (handles ModelSpec objects or Hashes)
68
+ model_display = if AIA.config&.models && !AIA.config.models.empty?
69
+ models = AIA.config.models
70
+ models.map do |spec|
71
+ if spec.is_a?(AIA::ModelSpec)
72
+ spec.name
73
+ elsif spec.is_a?(Hash)
74
+ spec[:name] || spec['name'] || spec.to_s
48
75
  else
49
- models.join(', ')
76
+ spec.to_s
50
77
  end
51
- else
52
- models.to_s
53
- end
78
+ end.join(', ')
54
79
  else
55
80
  'unknown-model'
56
81
  end
57
82
 
58
- mcp_line = mcp_servers? ? "MCP: #{mcp_server_names.join(', ')}" : ''
83
+ # Build MCP line based on connection status
84
+ mcp_line = if !mcp_servers?
85
+ '' # No MCP servers configured
86
+ elsif connected_mcp_servers?
87
+ "MCP: #{mcp_server_names.join(', ')}"
88
+ else
89
+ "MCP: (none connected)"
90
+ end
59
91
 
60
92
  puts <<-ROBOT
61
93
 
62
94
  , ,
63
95
  (\\____/) AI Assistant (v#{AIA::VERSION}) is Online
64
96
  (_oo_) #{model_display}#{supports_tools? ? ' (supports tools)' : ''}
65
- (O) using #{AIA.config&.adapter || 'unknown-adapter'} (v#{RubyLLM::VERSION}#{mcp_version})
97
+ (O) using #{AIA.config&.llm&.adapter || 'unknown-adapter'} (v#{RubyLLM::VERSION}#{mcp_version})
66
98
  __||__ \\) model db was last refreshed on
67
- [/______\\] / #{AIA.config&.last_refresh || 'unknown'}
99
+ [/______\\] / #{models_last_refresh || 'unknown'}
68
100
  / \\__AI__/ \\/ #{user_tools? ? 'I will also use your tools' : (tools? ? 'You can share my tools' : 'I did not bring any tools')}
69
101
  / /__\\ #{mcp_line}
70
102
  (\\ /____\\ #{user_tools? && tools? ? 'My Toolbox contains:' : ''}
71
103
  ROBOT
72
104
  if user_tools? && tools?
73
- tool_names = AIA.config.respond_to?(:tool_names) ? AIA.config.tool_names : AIA.config.tools
105
+ tool_names = AIA.config.tool_names
74
106
  if tool_names && !tool_names.to_s.empty?
75
107
  puts WordWrapper::MinimumRaggedness.new(
76
108
  width,
data/lib/aia.rb CHANGED
@@ -21,14 +21,20 @@ DebugMeDefaultOptions[:skip1] = true
21
21
  require_relative 'extensions/openstruct_merge' # adds self.merge self.get_value
22
22
  require_relative 'extensions/ruby_llm/modalities' # adds model.modalities.text_to_text? etc.
23
23
 
24
- require_relative 'refinements/string.rb' # adds #include_any? #include_all?
25
-
26
-
27
-
24
+ require_relative 'refinements/string' # adds #include_any? #include_all?
28
25
 
29
26
  require_relative 'aia/utility'
30
27
  require_relative 'aia/version'
31
28
  require_relative 'aia/config'
29
+ require_relative 'aia/logger'
30
+
31
+ # Top-level logger method available anywhere in the application
32
+ def logger
33
+ AIA::LoggerManager.aia_logger
34
+ end
35
+
36
+ require_relative 'aia/config/cli_parser'
37
+ require_relative 'aia/config/validator'
32
38
  require_relative 'aia/prompt_handler'
33
39
  require_relative 'aia/ruby_llm_adapter'
34
40
  require_relative 'aia/directive_processor'
@@ -41,86 +47,105 @@ require_relative 'aia/session'
41
47
  # provides an interface for interacting with AI models and managing prompts.
42
48
  module AIA
43
49
  at_exit do
44
- STDERR.puts "Exiting AIA application..."
45
- # Clean up temporary STDIN file if it exists
46
- if @config&.stdin_temp_file && File.exist?(@config.stdin_temp_file)
47
- File.unlink(@config.stdin_temp_file)
48
- end
50
+ warn 'Exiting AIA application...'
49
51
  end
50
52
 
51
53
  @config = nil
54
+ @client = nil
52
55
 
53
- def self.config
54
- @config
55
- end
56
+ class << self
57
+ attr_accessor :config, :client
56
58
 
57
- def self.client
58
- @config.client
59
- end
59
+ def good_file?(filename)
60
+ File.exist?(filename) &&
61
+ File.readable?(filename) &&
62
+ !File.directory?(filename)
63
+ end
60
64
 
61
- def self.client=(client)
62
- @config.client = client
63
- end
64
65
 
65
- def self.good_file?(filename)
66
- File.exist?(filename) &&
67
- File.readable?(filename) &&
68
- !File.directory?(filename)
69
- end
66
+ def bad_file?(filename)
67
+ !good_file?(filename)
68
+ end
70
69
 
71
- def self.bad_file?(filename)
72
- !good_file?(filename)
73
- end
74
70
 
75
- def self.build_flags
76
- @config.each_pair do |key, value|
77
- if [TrueClass, FalseClass].include?(value.class)
78
- define_singleton_method("#{key}?") do
79
- @config[key]
80
- end
81
- end
71
+ # Convenience flag accessors (delegate to config.flags section)
72
+ def chat?
73
+ @config&.flags&.chat == true
82
74
  end
83
- end
84
75
 
85
- def self.run
86
- @config = Config.setup
87
76
 
88
- build_flags
77
+ def debug?
78
+ @config&.flags&.debug == true
79
+ end
89
80
 
90
- # Load Fzf if fuzzy search is enabled and fzf is installed
91
- if @config.fuzzy
92
- begin
93
- # Cache fzf availability check for better performance
94
- if system('which fzf >/dev/null 2>&1')
95
- require_relative 'aia/fzf'
96
- else
97
- warn "Warning: Fuzzy search enabled but fzf not found. Install fzf for enhanced search capabilities."
98
- end
99
- rescue StandardError => e
100
- warn "Warning: Failed to load fzf: #{e.message}"
101
- end
81
+
82
+ def verbose?
83
+ @config&.flags&.verbose == true
84
+ end
85
+
86
+
87
+ def fuzzy?
88
+ @config&.flags&.fuzzy == true
89
+ end
90
+
91
+
92
+ def terse?
93
+ @config&.flags&.terse == true
94
+ end
95
+
96
+
97
+ def speak?
98
+ @config&.flags&.speak == true
99
+ end
100
+
101
+
102
+ def append?
103
+ @config&.output&.append == true
102
104
  end
103
105
 
104
- prompt_handler = PromptHandler.new
105
106
 
106
- # Initialize the appropriate client adapter based on configuration
107
- @config.client = if 'ruby_llm' == @config.adapter
108
- RubyLLMAdapter.new
109
- else
110
- # TODO: ?? some other LLM API wrapper
111
- STDERR.puts "ERROR: There is no adapter for #{@config.adapter}"
112
- exit 1
113
- end
107
+ def run
108
+ # Parse CLI arguments
109
+ cli_overrides = CLIParser.parse
114
110
 
115
- # There are two kinds of sessions: batch and chat
116
- # A chat session is started when the --chat CLI option is used
117
- # BUT its also possible to start a chat session with an initial prompt AND
118
- # within that initial prompt there can be a workflow (aka pipeline)
119
- # defined. If that is the case, then the chat session will not start
120
- # until the initial prompt has completed its workflow.
111
+ # Create config with CLI overrides
112
+ @config = Config.setup(cli_overrides)
121
113
 
122
- session = Session.new(prompt_handler)
114
+ # Validate and tailor configuration (handles --dump early exit)
115
+ ConfigValidator.tailor(@config)
123
116
 
124
- session.start
117
+ # Load Fzf if fuzzy search is enabled and fzf is installed
118
+ if @config.flags.fuzzy
119
+ begin
120
+ if system('which fzf >/dev/null 2>&1')
121
+ require_relative 'aia/fzf'
122
+ else
123
+ warn 'Warning: Fuzzy search enabled but fzf not found. Install fzf for enhanced search capabilities.'
124
+ end
125
+ rescue StandardError => e
126
+ warn "Warning: Failed to load fzf: #{e.message}"
127
+ end
128
+ end
129
+
130
+ prompt_handler = PromptHandler.new
131
+
132
+ # Initialize the appropriate client adapter based on configuration
133
+ @client = if 'ruby_llm' == @config.llm.adapter
134
+ RubyLLMAdapter.new
135
+ else
136
+ warn "ERROR: There is no adapter for #{@config.llm.adapter}"
137
+ exit 1
138
+ end
139
+
140
+ # There are two kinds of sessions: batch and chat
141
+ # A chat session is started when the --chat CLI option is used
142
+ # BUT its also possible to start a chat session with an initial prompt AND
143
+ # within that initial prompt there can be a workflow (aka pipeline)
144
+ # defined. If that is the case, then the chat session will not start
145
+ # until the initial prompt has completed its workflow.
146
+
147
+ session = Session.new(prompt_handler)
148
+ session.start
149
+ end
125
150
  end
126
151
  end
@@ -1,5 +1,7 @@
1
1
  # lib/extensions/ruby_llm/modalities.rb
2
2
 
3
+ require 'ruby_llm'
4
+
3
5
  class RubyLLM::Model::Modalities
4
6
  #
5
7
  def text_to_text? = input.include?('text') && output.include?('text')
@@ -0,0 +1,8 @@
1
+ {
2
+ "mcpServers": {
3
+ "apple-mcp": {
4
+ "command": "npx",
5
+ "args": ["-y", "apple-mcp"]
6
+ }
7
+ }
8
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "mcpServers": {
3
+ "mcp-server-chart": {
4
+ "command": "npx",
5
+ "args": [
6
+ "-y",
7
+ "@antv/mcp-server-chart"
8
+ ]
9
+ }
10
+ }
11
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "mcpServers": {
3
+ "playwright": {
4
+ "command": "npx",
5
+ "args": ["@playwright/mcp"]
6
+ }
7
+ }
8
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "mcpServers": {
3
+ "playwright": {
4
+ "command": "npx",
5
+ "args": ["@executeautomation/playwright-mcp-server"]
6
+ }
7
+ }
8
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "mcpServers": {
3
+ "tavily-remote-mcp": {
4
+ "command": "npx -y mcp-remote https://mcp.tavily.com/mcp/?tavilyApiKey=$TAVILY_API_KEY",
5
+ "env": {}
6
+ }
7
+ }
8
+ }