aia 0.9.11 → 0.9.12

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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/.version +1 -1
  3. data/CHANGELOG.md +66 -2
  4. data/README.md +133 -4
  5. data/docs/advanced-prompting.md +721 -0
  6. data/docs/cli-reference.md +582 -0
  7. data/docs/configuration.md +347 -0
  8. data/docs/contributing.md +332 -0
  9. data/docs/directives-reference.md +490 -0
  10. data/docs/examples/index.md +277 -0
  11. data/docs/examples/mcp/index.md +479 -0
  12. data/docs/examples/prompts/analysis/index.md +78 -0
  13. data/docs/examples/prompts/automation/index.md +108 -0
  14. data/docs/examples/prompts/development/index.md +125 -0
  15. data/docs/examples/prompts/index.md +333 -0
  16. data/docs/examples/prompts/learning/index.md +127 -0
  17. data/docs/examples/prompts/writing/index.md +62 -0
  18. data/docs/examples/tools/index.md +292 -0
  19. data/docs/faq.md +414 -0
  20. data/docs/guides/available-models.md +366 -0
  21. data/docs/guides/basic-usage.md +477 -0
  22. data/docs/guides/chat.md +474 -0
  23. data/docs/guides/executable-prompts.md +417 -0
  24. data/docs/guides/first-prompt.md +454 -0
  25. data/docs/guides/getting-started.md +455 -0
  26. data/docs/guides/image-generation.md +507 -0
  27. data/docs/guides/index.md +46 -0
  28. data/docs/guides/models.md +507 -0
  29. data/docs/guides/tools.md +856 -0
  30. data/docs/index.md +173 -0
  31. data/docs/installation.md +238 -0
  32. data/docs/mcp-integration.md +612 -0
  33. data/docs/prompt_management.md +579 -0
  34. data/docs/security.md +629 -0
  35. data/docs/tools-and-mcp-examples.md +1186 -0
  36. data/docs/workflows-and-pipelines.md +563 -0
  37. data/examples/tools/mcp/github_mcp_server.json +11 -0
  38. data/examples/tools/mcp/imcp.json +7 -0
  39. data/lib/aia/chat_processor_service.rb +19 -3
  40. data/lib/aia/config/base.rb +224 -0
  41. data/lib/aia/config/cli_parser.rb +409 -0
  42. data/lib/aia/config/defaults.rb +88 -0
  43. data/lib/aia/config/file_loader.rb +131 -0
  44. data/lib/aia/config/validator.rb +184 -0
  45. data/lib/aia/config.rb +10 -860
  46. data/lib/aia/directive_processor.rb +27 -372
  47. data/lib/aia/directives/configuration.rb +114 -0
  48. data/lib/aia/directives/execution.rb +37 -0
  49. data/lib/aia/directives/models.rb +178 -0
  50. data/lib/aia/directives/registry.rb +120 -0
  51. data/lib/aia/directives/utility.rb +70 -0
  52. data/lib/aia/directives/web_and_file.rb +71 -0
  53. data/lib/aia/prompt_handler.rb +23 -3
  54. data/lib/aia/ruby_llm_adapter.rb +307 -128
  55. data/lib/aia/session.rb +27 -14
  56. data/lib/aia/utility.rb +12 -8
  57. data/lib/aia.rb +11 -2
  58. data/lib/extensions/ruby_llm/.irbrc +56 -0
  59. data/mkdocs.yml +165 -0
  60. metadata +77 -20
  61. /data/{images → docs/assets/images}/aia.png +0 -0
@@ -2,119 +2,58 @@
2
2
 
3
3
  require 'active_support/all'
4
4
  require 'faraday'
5
- require 'word_wrapper' # Pure ruby word wrapping
5
+ require 'word_wrapper'
6
+ require_relative 'directives/registry'
6
7
 
7
8
  module AIA
8
9
  class DirectiveProcessor
9
10
  using Refinements
10
11
 
11
- PUREMD_API_KEY = ENV.fetch('PUREMD_API_KEY', nil)
12
12
  EXCLUDED_METHODS = %w[ run initialize private? ]
13
- @descriptions = {}
14
- @aliases = {}
15
-
16
- class << self
17
- attr_reader :descriptions, :aliases
18
-
19
- def desc(description, method_name = nil)
20
- @last_description = description
21
- @descriptions[method_name.to_s] = description if method_name
22
- nil
23
- end
24
-
25
- def method_added(method_name)
26
- if @last_description
27
- @descriptions[method_name.to_s] = @last_description
28
- @last_description = nil
29
- end
30
- super if defined?(super)
31
- end
32
-
33
- def build_aliases(private_methods)
34
- private_methods.each do |method_name|
35
- method = instance_method(method_name)
36
-
37
- @aliases[method_name] = []
38
-
39
- private_methods.each do |other_method_name|
40
- next if method_name == other_method_name
41
-
42
- other_method = instance_method(other_method_name)
43
-
44
- if method == other_method
45
- @aliases[method_name] << other_method_name
46
- end
47
- end
48
- end
49
- end
50
- end
51
13
 
52
14
  def initialize
53
- @prefix_size = PromptManager::Prompt::DIRECTIVE_SIGNAL.size
15
+ @prefix_size = PromptManager::Prompt::DIRECTIVE_SIGNAL.size
54
16
  @included_files = []
17
+ Directives::WebAndFile.included_files = @included_files
55
18
  end
56
19
 
57
- def directive?(a_string)
58
- # Handle RubyLLM::Message objects by extracting their content first
59
- content = if a_string.is_a?(RubyLLM::Message)
60
- a_string.content rescue a_string.to_s
61
- else
62
- a_string.to_s
63
- end
64
-
65
- content.strip.start_with?(PromptManager::Prompt::DIRECTIVE_SIGNAL)
20
+ def directive?(string)
21
+ Directives::Registry.directive?(string)
66
22
  end
67
23
 
68
- # Used with the chat loop to allow user to enter a single directive
69
- def process(a_string, context_manager)
70
- return a_string unless directive?(a_string)
24
+ def process(string, context_manager)
25
+ return string unless directive?(string)
71
26
 
72
- # Handle RubyLLM::Message objects by extracting their content first
73
- content = if a_string.is_a?(RubyLLM::Message)
74
- a_string.content rescue a_string.to_s
27
+ content = if string.is_a?(RubyLLM::Message)
28
+ string.content rescue string.to_s
75
29
  else
76
- a_string.to_s
30
+ string.to_s
77
31
  end
78
32
 
79
33
  key = content.strip
80
34
  sans_prefix = key[@prefix_size..]
81
- args = sans_prefix.split(' ')
35
+ args = sans_prefix.split(' ')
82
36
  method_name = args.shift.downcase
83
37
 
84
- if EXCLUDED_METHODS.include?(method_name)
85
- return "Error: #{method_name} is not a valid directive: #{key}"
86
- elsif respond_to?(method_name, true)
87
- return send(method_name, args, context_manager)
88
- else
89
- return "Error: Unknown directive '#{key}'"
90
- end
38
+ Directives::Registry.process(method_name, args, context_manager)
91
39
  end
92
40
 
93
41
  def run(directives)
94
42
  return {} if directives.nil? || directives.empty?
43
+
95
44
  directives.each do |key, _|
96
45
  sans_prefix = key[@prefix_size..]
97
- args = sans_prefix.split(' ')
46
+ args = sans_prefix.split(' ')
98
47
  method_name = args.shift.downcase
99
48
 
100
- if EXCLUDED_METHODS.include?(method_name)
101
- directives[key] = "Error: #{method_name} is not a valid directive: #{key}"
102
- next
103
- elsif respond_to?(method_name, true)
104
- directives[key] = send(method_name, args)
105
- else
106
- directives[key] = "Error: Unknown directive '#{key}'"
107
- end
49
+ # Use the new module-based directive system
50
+ # Pass nil as context_manager since it's not available at the prompt processing level
51
+ directives[key] = Directives::Registry.process(method_name, args, nil)
108
52
  end
109
53
 
110
54
  directives
111
55
  end
112
56
 
113
-
114
- #####################################################
115
- ## Directives are implemented as private methods
116
- ## All directives return a String. It can be empty.
117
- #
118
57
  private
119
58
 
120
59
  def private?(method_name)
@@ -125,304 +64,20 @@ module AIA
125
64
  ## Directives ##
126
65
  ################
127
66
 
67
+ # All directive implementations are now in separate modules
68
+ # and are accessed through the Registry
128
69
 
129
- desc "webpage inserted as markdown to context using pure.md"
130
- def webpage(args, context_manager=nil)
131
- if PUREMD_API_KEY.nil?
132
- "ERROR: PUREMD_API_KEY is required in order to include a webpage"
133
- else
134
- url = `echo #{args.shift}`.strip
135
- puremd_url = "https://pure.md/#{url}"
136
-
137
- response = Faraday.get(puremd_url) do |req|
138
- req.headers['x-puremd-api-token'] = PUREMD_API_KEY
139
- end
140
-
141
- if 200 == response.status
142
- response.body
143
- else
144
- "Error: wtatus was #{r.status}\n#{ap response}"
145
- end
146
- end
147
- end
148
-
149
- desc "Specify the next prompt ID to process after this one"
150
- def next(args = [], context_manager=nil)
151
- if args.empty?
152
- ap AIA.config.next
153
- else
154
- AIA.config.next = args.shift
155
- end
156
- ''
157
- end
158
-
159
- desc "Show a list of tools and their description"
160
- def tools(args = [], context_manager=nil)
161
- indent = 4
162
- spaces = " "*indent
163
- width = TTY::Screen.width - indent - 2
164
-
165
- if AIA.config.tools.empty?
166
- puts "No tools are available"
167
- else
168
- puts
169
- puts "Available Tools"
170
- puts "==============="
171
-
172
- AIA.config.tools.each do |tool|
173
- name = tool.respond_to?(:name) ? tool.name : tool.class.name
174
- puts "\n#{name}"
175
- puts "-"*name.size
176
- puts WordWrapper::MinimumRaggedness.new(width, tool.description).wrap.split("\n").map{|s| spaces+s+"\n"}.join
177
- end
178
- end
179
- puts
180
-
181
- ''
182
- end
183
-
184
- desc "Specify a sequence pf prompt IDs to process after this one"
185
- def pipeline(args = [], context_manager=nil)
186
- if args.empty?
187
- ap AIA.config.pipeline
188
- elsif 1 == args.size
189
- AIA.config.pipeline += args.first.split(',').map(&:strip).reject{|id| id.empty?}
190
- else
191
- AIA.config.pipeline += args.map{|id| id.gsub(',', '').strip}.reject{|id| id.empty?}
192
- end
193
- ''
194
- end
195
- alias_method :workflow, :pipeline
196
-
197
- desc "Inserts the contents of a file Example: //include path/to/file"
198
- def include(args, context_manager=nil)
199
- # echo takes care of envars and tilde expansion
200
- file_path = `echo #{args.shift}`.strip
201
-
202
- if file_path.start_with?(/http?:\/\//)
203
- return webpage(args)
204
- end
205
-
206
- if @included_files.include?(file_path)
207
- ""
70
+ # Keep backward compatibility by delegating to Registry
71
+ def method_missing(method_name, *args, &block)
72
+ if Directives::Registry.respond_to?(method_name, true)
73
+ Directives::Registry.send(method_name, *args, &block)
208
74
  else
209
- if File.exist?(file_path) && File.readable?(file_path)
210
- @included_files << file_path
211
- File.read(file_path)
212
- else
213
- "Error: File '#{file_path}' is not accessible"
214
- end
75
+ super
215
76
  end
216
77
  end
217
- alias_method :include_file, :include
218
- alias_method :import, :include
219
-
220
- desc "Without arguments it will print a list of all config items and their values _or_ //config item (for one item's value) _or_ //config item = value (to set a value of an item)"
221
- def config(args = [], context_manager=nil)
222
- args = Array(args)
223
-
224
- if args.empty?
225
- ap AIA.config
226
- ""
227
- elsif args.length == 1
228
- config_item = args.first
229
- local_cfg = Hash.new
230
- local_cfg[config_item] = AIA.config[config_item]
231
- ap local_cfg
232
- ""
233
- else
234
- config_item = args.shift
235
- boolean = AIA.respond_to?("#{config_item}?")
236
- new_value = args.join(' ').gsub('=', '').strip
237
-
238
- if boolean
239
- new_value = %w[true t yes y on 1 yea yeah yep yup].include?(new_value.downcase)
240
- end
241
-
242
- AIA.config[config_item] = new_value
243
- ""
244
- end
245
- end
246
- alias_method :cfg, :config
247
-
248
- desc "Shortcut for //config top_p _and_ //config top_p = value"
249
- def top_p(args, context_manager=nil)
250
- send(:config, args.prepend('top_p'), context_manager)
251
- end
252
- alias_method :topp, :top_p
253
-
254
- desc "Review the current context"
255
- def review(args, context_manager=nil)
256
- ap context_manager.get_context
257
- ''
258
- end
259
- alias_method :context, :review
260
-
261
- desc "Shortcut for //config model _and_ //config model = value"
262
- def model(args, context_manager=nil)
263
- if args.empty?
264
- puts
265
- puts AIA.config.client.model.to_h.pretty_inspect
266
- puts
267
- else
268
- send(:config, args.prepend('model'), context_manager)
269
- end
270
-
271
- return ''
272
- end
273
-
274
- desc "Shortcut for //config temperature _and_ //config temperature = value"
275
- def temperature(args, context_manager=nil)
276
- send(:config, args.prepend('temperature'), context_manager)
277
- end
278
- alias_method :temp, :temperature
279
-
280
- desc "Clears the conversation history (aka context) same as //config clear = true"
281
- def clear(args, context_manager=nil)
282
- # TODO: review the robot's code in the Session class for when the
283
- # //clear directive is used in a follow up prompt. That processing
284
- # should be moved here so that it is also available in batch
285
- # sessions.
286
- if context_manager.nil?
287
- return "Error: Context manager not available for //clear directive."
288
- end
289
-
290
- context_manager.clear_context
291
-
292
- ''
293
- end
294
-
295
- desc "Shortcut for a one line of ruby code; result is added to the context"
296
- def ruby(args, context_manager=nil)
297
- ruby_code = args.join(' ')
298
-
299
- begin
300
- String(eval(ruby_code))
301
- rescue Exception => e
302
- <<~ERROR
303
- This ruby code failed: #{ruby_code}
304
- #{e.message}
305
- ERROR
306
- end
307
- end
308
- alias_method :rb, :ruby
309
-
310
-
311
- desc "Executes one line of shell code; result is added to the context"
312
- def shell(args, context_manager=nil)
313
- shell_code = args.join(' ')
314
-
315
- `#{shell_code}`
316
- end
317
- alias_method :sh, :shell
318
-
319
- desc "Use the system's say command to speak text //say some text"
320
- def say(args, context_manager=nil)
321
- `say #{args.join(' ')}`
322
- ""
323
- end
324
-
325
- desc "Inserts an instruction to keep responses short and to the point."
326
- def terse(args, context_manager=nil)
327
- AIA::Session::TERSE_PROMPT
328
- end
329
-
330
- desc "Display the ASCII art AIA robot."
331
- def robot(args, context_manager=nil)
332
- AIA::Utility.robot
333
- ""
334
- end
335
-
336
- desc "All Available models or query on [partial LLM or provider name] Examples: //llms ; //llms openai ; //llms claude"
337
- def available_models( args=nil, context_manager=nil)
338
- query = args
339
-
340
- if 1 == query.size
341
- query = query.first.split(',')
342
- end
343
-
344
- header = "\nAvailable LLMs"
345
- header += " for #{query.join(' and ')}" if query
346
-
347
- puts header + ':'
348
- puts
349
-
350
- q1 = query.select{|q| q.include?('_to_')} # SMELL: ?? .map{|q| ':'==q[0] ? q[1...] : q}
351
- q2 = query.reject{|q| q.include?('_to_')}
352
-
353
- counter = 0
354
-
355
- RubyLLM.models.all.each do |llm|
356
- cw = llm.context_window
357
- caps = llm.capabilities.join(',')
358
- inputs = llm.modalities.input.join(',')
359
- outputs = llm.modalities.output.join(',')
360
- mode = "#{inputs} to #{outputs}"
361
- in_1m = llm.pricing.text_tokens.standard.to_h[:input_per_million]
362
- entry = "- #{llm.id} (#{llm.provider}) in: $#{in_1m} cw: #{cw} mode: #{mode} caps: #{caps}"
363
-
364
- if query.nil? || query.empty?
365
- counter += 1
366
- puts entry
367
- next
368
- end
369
-
370
- show_it = true
371
- q1.each{|q| show_it &&= llm.modalities.send("#{q}?")}
372
- q2.each{|q| show_it &&= entry.include?(q)}
373
-
374
- if show_it
375
- counter += 1
376
- puts entry
377
- end
378
- end
379
-
380
- puts if counter > 0
381
- puts "#{counter} LLMs matching your query"
382
- puts
383
-
384
- ""
385
- end
386
- alias_method :am, :available_models
387
- alias_method :available, :available_models
388
- alias_method :models, :available_models
389
- alias_method :all_models, :available_models
390
- alias_method :llms, :available_models
391
-
392
- desc "Generates this help content"
393
- def help(args=nil, context_manager=nil)
394
- puts
395
- puts "Available Directives"
396
- puts "===================="
397
- puts
398
-
399
- directives = self.class
400
- .private_instance_methods(false)
401
- .map(&:to_s)
402
- .reject { |m| EXCLUDED_METHODS.include?(m) }
403
- .sort
404
-
405
- self.class.build_aliases(directives)
406
-
407
- directives.each do |directive|
408
- next unless self.class.descriptions[directive]
409
-
410
- others = self.class.aliases[directive]
411
-
412
- if others.empty?
413
- others_line = ""
414
- else
415
- with_prefix = others.map{|m| PromptManager::Prompt::DIRECTIVE_SIGNAL + m}
416
- others_line = "\tAliases:#{with_prefix.join(' ')}\n"
417
- end
418
-
419
- puts <<~TEXT
420
- //#{directive} #{self.class.descriptions[directive]}
421
- #{others_line}
422
- TEXT
423
- end
424
78
 
425
- ""
79
+ def respond_to_missing?(method_name, include_private = false)
80
+ Directives::Registry.respond_to?(method_name, include_private) || super
426
81
  end
427
82
  end
428
83
  end
@@ -0,0 +1,114 @@
1
+ # lib/aia/directives/configuration.rb
2
+
3
+ module AIA
4
+ module Directives
5
+ module Configuration
6
+ def self.config(args = [], context_manager = nil)
7
+ args = Array(args)
8
+
9
+ if args.empty?
10
+ ap AIA.config
11
+ ""
12
+ elsif args.length == 1
13
+ config_item = args.first
14
+ local_cfg = Hash.new
15
+ local_cfg[config_item] = AIA.config[config_item]
16
+ ap local_cfg
17
+ ""
18
+ else
19
+ config_item = args.shift
20
+ boolean = AIA.respond_to?("#{config_item}?")
21
+ new_value = args.join(' ').gsub('=', '').strip
22
+
23
+ if boolean
24
+ new_value = %w[true t yes y on 1 yea yeah yep yup].include?(new_value.downcase)
25
+ end
26
+
27
+ AIA.config[config_item] = new_value
28
+ ""
29
+ end
30
+ end
31
+
32
+ def self.model(args, context_manager = nil)
33
+ if args.empty?
34
+ # Display details for all configured models
35
+ puts
36
+ models = Array(AIA.config.model)
37
+
38
+ if models.size == 1
39
+ puts "Current Model:"
40
+ puts "=============="
41
+ puts AIA.config.client.model.to_h.pretty_inspect
42
+ else
43
+ puts "Multi-Model Configuration:"
44
+ puts "=========================="
45
+ puts "Model count: #{models.size}"
46
+ puts "Primary model: #{models.first} (used for consensus when --consensus flag is enabled)"
47
+ puts "Consensus mode: #{AIA.config.consensus.nil? ? 'auto-detect (disabled by default)' : AIA.config.consensus}"
48
+ puts
49
+ puts "Model Details:"
50
+ puts "-" * 50
51
+
52
+ models.each_with_index do |model_name, index|
53
+ puts "#{index + 1}. #{model_name}#{index == 0 ? ' (primary)' : ''}"
54
+
55
+ # Try to get model details if available
56
+ begin
57
+ # Access the model details from RubyLLM's model registry
58
+ model_info = RubyLLM::Models.find(name: model_name)
59
+ if model_info
60
+ puts " Provider: #{model_info.provider || 'Unknown'}"
61
+ puts " Context window: #{model_info.context_window || 'Unknown'}"
62
+ puts " Input cost: $#{model_info.input_cost || 'Unknown'}"
63
+ puts " Output cost: $#{model_info.output_cost || 'Unknown'}"
64
+ puts " Mode: #{model_info.modalities || 'Unknown'}"
65
+ puts " Capabilities: #{(model_info.capabilities || []).join(', ')}" if model_info.capabilities&.any?
66
+ else
67
+ puts " Details: Model not found in registry"
68
+ end
69
+ rescue StandardError => e
70
+ puts " Details: Unable to fetch (#{e.class.name}: #{e.message})"
71
+ end
72
+ puts
73
+ end
74
+ end
75
+ puts
76
+ else
77
+ send(:config, args.prepend('model'), context_manager)
78
+ end
79
+
80
+ return ''
81
+ end
82
+
83
+ def self.temperature(args, context_manager = nil)
84
+ send(:config, args.prepend('temperature'), context_manager)
85
+ end
86
+
87
+ def self.top_p(args, context_manager = nil)
88
+ send(:config, args.prepend('top_p'), context_manager)
89
+ end
90
+
91
+ def self.clear(args, context_manager = nil)
92
+ if context_manager.nil?
93
+ return "Error: Context manager not available for //clear directive."
94
+ end
95
+
96
+ context_manager.clear_context
97
+ ''
98
+ end
99
+
100
+ def self.review(args, context_manager = nil)
101
+ ap context_manager.get_context
102
+ ''
103
+ end
104
+
105
+ # Set up aliases - these work on the module's singleton class
106
+ class << self
107
+ alias_method :cfg, :config
108
+ alias_method :temp, :temperature
109
+ alias_method :topp, :top_p
110
+ alias_method :context, :review
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,37 @@
1
+ # lib/aia/directives/execution.rb
2
+
3
+ module AIA
4
+ module Directives
5
+ module Execution
6
+ def self.ruby(args, context_manager = nil)
7
+ ruby_code = args.join(' ')
8
+
9
+ begin
10
+ String(eval(ruby_code))
11
+ rescue Exception => e
12
+ <<~ERROR
13
+ This ruby code failed: #{ruby_code}
14
+ #{e.message}
15
+ ERROR
16
+ end
17
+ end
18
+
19
+ def self.shell(args, context_manager = nil)
20
+ shell_code = args.join(' ')
21
+
22
+ `#{shell_code}`
23
+ end
24
+
25
+ def self.say(args, context_manager = nil)
26
+ `say #{args.join(' ')}`
27
+ ""
28
+ end
29
+
30
+ # Set up aliases - these work on the module's singleton class
31
+ class << self
32
+ alias_method :rb, :ruby
33
+ alias_method :sh, :shell
34
+ end
35
+ end
36
+ end
37
+ end