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
@@ -0,0 +1,178 @@
1
+ # lib/aia/directives/models.rb
2
+
3
+ module AIA
4
+ module Directives
5
+ module Models
6
+ class << self
7
+ def descriptions
8
+ @descriptions ||= {}
9
+ end
10
+
11
+ def aliases
12
+ @aliases ||= {}
13
+ end
14
+
15
+ def build_aliases(methods)
16
+ methods.each do |method_name|
17
+ method = self.method(method_name)
18
+ aliases[method_name] = []
19
+
20
+ methods.each do |other_method_name|
21
+ next if method_name == other_method_name
22
+ other_method = self.method(other_method_name)
23
+
24
+ if method == other_method
25
+ aliases[method_name] << other_method_name
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+ def self.available_models(args = nil, context_manager = nil)
33
+ query = args
34
+
35
+ if 1 == query.size
36
+ query = query.first.split(',')
37
+ end
38
+
39
+ header = "\nAvailable LLMs"
40
+ header += " for #{query.join(' and ')}" if query
41
+
42
+ puts header + ':'
43
+ puts
44
+
45
+ q1 = query.select { |q| q.include?('_to_') } # SMELL: ??
46
+ q2 = query.reject { |q| q.include?('_to_') }
47
+
48
+ counter = 0
49
+
50
+ RubyLLM.models.all.each do |llm|
51
+ cw = llm.context_window
52
+ caps = llm.capabilities.join(',')
53
+ inputs = llm.modalities.input.join(',')
54
+ outputs = llm.modalities.output.join(',')
55
+ mode = "#{inputs} to #{outputs}"
56
+ in_1m = llm.pricing.text_tokens.standard.to_h[:input_per_million]
57
+ entry = "- #{llm.id} (#{llm.provider}) in: $#{in_1m} cw: #{cw} mode: #{mode} caps: #{caps}"
58
+
59
+ if query.nil? || query.empty?
60
+ counter += 1
61
+ puts entry
62
+ next
63
+ end
64
+
65
+ show_it = true
66
+ q1.each { |q| show_it &&= llm.modalities.send("#{q}?") }
67
+ q2.each { |q| show_it &&= entry.include?(q) }
68
+
69
+ if show_it
70
+ counter += 1
71
+ puts entry
72
+ end
73
+ end
74
+
75
+ puts if counter > 0
76
+ puts "#{counter} LLMs matching your query"
77
+ puts
78
+
79
+ ""
80
+ end
81
+
82
+ def self.help(args = nil, context_manager = nil)
83
+ puts
84
+ puts "Available Directives"
85
+ puts "===================="
86
+ puts
87
+
88
+ directives = self.methods(false).map(&:to_s).reject do |m|
89
+ ['run', 'initialize', 'private?', 'descriptions', 'aliases', 'build_aliases'].include?(m)
90
+ end.sort
91
+
92
+ build_aliases(directives)
93
+
94
+ directives.each do |directive|
95
+ next unless descriptions[directive]
96
+
97
+ others = aliases[directive]
98
+
99
+ if others.empty?
100
+ others_line = ""
101
+ else
102
+ with_prefix = others.map { |m| PromptManager::Prompt::DIRECTIVE_SIGNAL + m }
103
+ others_line = "\tAliases:#{with_prefix.join(' ')}\n"
104
+ end
105
+
106
+ puts <<~TEXT
107
+ //#{directive} #{descriptions[directive]}
108
+ #{others_line}
109
+ TEXT
110
+ end
111
+
112
+ ""
113
+ end
114
+
115
+ def self.compare(args, context_manager = nil)
116
+ return 'Error: No prompt provided for comparison' if args.empty?
117
+
118
+ # Parse arguments - first arg is the prompt, --models flag specifies models
119
+ prompt = nil
120
+ models = []
121
+
122
+ i = 0
123
+ while i < args.length
124
+ if args[i] == '--models' && i + 1 < args.length
125
+ models = args[i + 1].split(',')
126
+ i += 2
127
+ else
128
+ prompt ||= args[i]
129
+ i += 1
130
+ end
131
+ end
132
+
133
+ return 'Error: No prompt provided for comparison' unless prompt
134
+ return 'Error: No models specified. Use --models model1,model2,model3' if models.empty?
135
+
136
+ puts "\nComparing responses for: #{prompt}\n"
137
+ puts '=' * 80
138
+
139
+ results = {}
140
+
141
+ models.each do |model_name|
142
+ model_name.strip!
143
+ puts "\n🤖 **#{model_name}:**"
144
+ puts '-' * 40
145
+
146
+ begin
147
+ # Create a temporary chat instance for this model
148
+ chat = RubyLLM.chat(model: model_name)
149
+ response = chat.ask(prompt)
150
+ content = response.content
151
+
152
+ puts content
153
+ results[model_name] = content
154
+ rescue StandardError => e
155
+ error_msg = "Error with #{model_name}: #{e.message}"
156
+ puts error_msg
157
+ results[model_name] = error_msg
158
+ end
159
+ end
160
+
161
+ puts '\n' + '=' * 80
162
+ puts "\nComparison complete!"
163
+
164
+ ''
165
+ end
166
+
167
+ # Set up aliases - these work on the module's singleton class
168
+ class << self
169
+ alias_method :am, :available_models
170
+ alias_method :available, :available_models
171
+ alias_method :models, :available_models
172
+ alias_method :all_models, :available_models
173
+ alias_method :llms, :available_models
174
+ alias_method :cmp, :compare
175
+ end
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,120 @@
1
+ # lib/aia/directives/registry.rb
2
+
3
+ require_relative 'web_and_file'
4
+ require_relative 'utility'
5
+ require_relative 'configuration'
6
+ require_relative 'execution'
7
+ require_relative 'models'
8
+
9
+ module AIA
10
+ module Directives
11
+ module Registry
12
+ EXCLUDED_METHODS = %w[ run initialize private? ]
13
+
14
+ class << self
15
+ def descriptions
16
+ @descriptions ||= {}
17
+ end
18
+
19
+ def aliases
20
+ @aliases ||= {}
21
+ end
22
+
23
+ def desc(description, method_name = nil)
24
+ @last_description = description
25
+ descriptions[method_name.to_s] = description if method_name
26
+ nil
27
+ end
28
+
29
+ def method_added(method_name)
30
+ if @last_description
31
+ descriptions[method_name.to_s] = @last_description
32
+ @last_description = nil
33
+ end
34
+ super if defined?(super)
35
+ end
36
+
37
+ def build_aliases(private_methods)
38
+ private_methods.each do |method_name|
39
+ method = instance_method(method_name)
40
+
41
+ aliases[method_name] = []
42
+
43
+ private_methods.each do |other_method_name|
44
+ next if method_name == other_method_name
45
+
46
+ other_method = instance_method(other_method_name)
47
+
48
+ if method == other_method
49
+ aliases[method_name] << other_method_name
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ def register_directive_module(mod)
56
+ @directive_modules ||= []
57
+ @directive_modules << mod
58
+ end
59
+
60
+ def process(directive_name, args, context_manager)
61
+ if EXCLUDED_METHODS.include?(directive_name)
62
+ return "Error: #{directive_name} is not a valid directive"
63
+ end
64
+
65
+ # Check all registered directive modules
66
+ @directive_modules ||= []
67
+ @directive_modules.each do |mod|
68
+ if mod.respond_to?(directive_name)
69
+ return mod.send(directive_name, args, context_manager)
70
+ end
71
+ end
72
+
73
+ return "Error: Unknown directive '#{directive_name}'"
74
+ end
75
+
76
+ def run(directives)
77
+ return {} if directives.nil? || directives.empty?
78
+
79
+ directives.each do |key, _|
80
+ sans_prefix = key[prefix_size..]
81
+ args = sans_prefix.split(' ')
82
+ method_name = args.shift.downcase
83
+
84
+ if EXCLUDED_METHODS.include?(method_name)
85
+ directives[key] = "Error: #{method_name} is not a valid directive: #{key}"
86
+ next
87
+ elsif respond_to?(method_name, true)
88
+ directives[key] = send(method_name, args)
89
+ else
90
+ directives[key] = "Error: Unknown directive '#{key}'"
91
+ end
92
+ end
93
+
94
+ directives
95
+ end
96
+
97
+ def prefix_size
98
+ PromptManager::Prompt::DIRECTIVE_SIGNAL.size
99
+ end
100
+
101
+ def directive?(string)
102
+ content = if string.is_a?(RubyLLM::Message)
103
+ string.content rescue string.to_s
104
+ else
105
+ string.to_s
106
+ end
107
+
108
+ content.strip.start_with?(PromptManager::Prompt::DIRECTIVE_SIGNAL)
109
+ end
110
+ end
111
+
112
+ # Register all directive modules
113
+ register_directive_module(WebAndFile)
114
+ register_directive_module(Utility)
115
+ register_directive_module(Configuration)
116
+ register_directive_module(Execution)
117
+ register_directive_module(Models)
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,70 @@
1
+ # lib/aia/directives/utility.rb
2
+
3
+ require 'tty-screen'
4
+ require 'word_wrapper'
5
+
6
+ module AIA
7
+ module Directives
8
+ module Utility
9
+ TERSE_PROMPT = "\nKeep your response short and to the point.\n"
10
+
11
+ def self.tools(args = [], context_manager = nil)
12
+ indent = 4
13
+ spaces = " " * indent
14
+ width = TTY::Screen.width - indent - 2
15
+
16
+ if AIA.config.tools.empty?
17
+ puts "No tools are available"
18
+ else
19
+ puts
20
+ puts "Available Tools"
21
+ puts "==============="
22
+
23
+ AIA.config.tools.each do |tool|
24
+ name = tool.respond_to?(:name) ? tool.name : tool.class.name
25
+ puts "\n#{name}"
26
+ puts "-" * name.size
27
+ puts WordWrapper::MinimumRaggedness.new(width, tool.description).wrap.split("\n").map { |s| spaces + s + "\n" }.join
28
+ end
29
+ end
30
+ puts
31
+
32
+ ''
33
+ end
34
+
35
+ def self.next(args = [], context_manager = nil)
36
+ if args.empty?
37
+ ap AIA.config.next
38
+ else
39
+ AIA.config.next = args.shift
40
+ end
41
+ ''
42
+ end
43
+
44
+ def self.pipeline(args = [], context_manager = nil)
45
+ if args.empty?
46
+ ap AIA.config.pipeline
47
+ elsif 1 == args.size
48
+ AIA.config.pipeline += args.first.split(',').map(&:strip).reject { |id| id.empty? }
49
+ else
50
+ AIA.config.pipeline += args.map { |id| id.gsub(',', '').strip }.reject { |id| id.empty? }
51
+ end
52
+ ''
53
+ end
54
+
55
+ def self.terse(args, context_manager = nil)
56
+ TERSE_PROMPT
57
+ end
58
+
59
+ def self.robot(args, context_manager = nil)
60
+ AIA::Utility.robot
61
+ ""
62
+ end
63
+
64
+ # Set up aliases - these work on the module's singleton class
65
+ class << self
66
+ alias_method :workflow, :pipeline
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,71 @@
1
+ # lib/aia/directives/web_and_file.rb
2
+
3
+ require 'faraday'
4
+ require 'active_support/all'
5
+
6
+ module AIA
7
+ module Directives
8
+ module WebAndFile
9
+ PUREMD_API_KEY = ENV.fetch('PUREMD_API_KEY', nil)
10
+
11
+ def self.webpage(args, context_manager = nil)
12
+ if PUREMD_API_KEY.nil?
13
+ "ERROR: PUREMD_API_KEY is required in order to include a webpage"
14
+ else
15
+ url = `echo #{args.shift}`.strip
16
+ puremd_url = "https://pure.md/#{url}"
17
+
18
+ response = Faraday.get(puremd_url) do |req|
19
+ req.headers['x-puremd-api-token'] = PUREMD_API_KEY
20
+ end
21
+
22
+ if 200 == response.status
23
+ response.body
24
+ else
25
+ "Error: Status was #{response.status}\n#{ap response}"
26
+ end
27
+ end
28
+ end
29
+
30
+ def self.include(args, context_manager = nil)
31
+ # echo takes care of envars and tilde expansion
32
+ file_path = `echo #{args.shift}`.strip
33
+
34
+ if file_path.start_with?(/http?:\/\//)
35
+ webpage(args)
36
+ else
37
+ include_file(file_path)
38
+ end
39
+ end
40
+
41
+ def self.include_file(file_path)
42
+ @included_files ||= []
43
+ if @included_files.include?(file_path)
44
+ ""
45
+ else
46
+ if File.exist?(file_path) && File.readable?(file_path)
47
+ @included_files << file_path
48
+ File.read(file_path)
49
+ else
50
+ "Error: File '#{file_path}' is not accessible"
51
+ end
52
+ end
53
+ end
54
+
55
+ def self.included_files
56
+ @included_files ||= []
57
+ end
58
+
59
+ def self.included_files=(files)
60
+ @included_files = files
61
+ end
62
+
63
+ # Set up aliases - these work on the module's singleton class
64
+ class << self
65
+ alias_method :website, :webpage
66
+ alias_method :web, :webpage
67
+ alias_method :import, :include
68
+ end
69
+ end
70
+ end
71
+ end
@@ -59,13 +59,22 @@ module AIA
59
59
  end
60
60
 
61
61
  def handle_missing_prompt(prompt_id)
62
+ # Handle empty/nil prompt_id
63
+ prompt_id = prompt_id.to_s.strip
64
+ if prompt_id.empty?
65
+ STDERR.puts "Error: Prompt ID cannot be empty"
66
+ exit 1
67
+ end
68
+
62
69
  if AIA.config.fuzzy
63
70
  return fuzzy_search_prompt(prompt_id)
64
71
  elsif AIA.config.fuzzy
65
72
  puts "Warning: Fuzzy search is enabled but Fzf tool is not available."
66
- raise "Error: Could not find prompt with ID: #{prompt_id}"
73
+ STDERR.puts "Error: Could not find prompt with ID: #{prompt_id}"
74
+ exit 1
67
75
  else
68
- raise "Error: Could not find prompt with ID: #{prompt_id}"
76
+ STDERR.puts "Error: Could not find prompt with ID: #{prompt_id}"
77
+ exit 1
69
78
  end
70
79
  end
71
80
 
@@ -90,6 +99,9 @@ module AIA
90
99
  end
91
100
 
92
101
  def fetch_role(role_id)
102
+ # Handle nil role_id
103
+ return handle_missing_role("roles/") if role_id.nil?
104
+
93
105
  # Prepend roles_prefix if not already present
94
106
  unless role_id.start_with?(AIA.config.roles_prefix)
95
107
  role_id = "#{AIA.config.roles_prefix}/#{role_id}"
@@ -115,10 +127,18 @@ module AIA
115
127
  end
116
128
 
117
129
  def handle_missing_role(role_id)
130
+ # Handle empty/nil role_id
131
+ role_id = role_id.to_s.strip
132
+ if role_id.empty? || role_id == "roles/"
133
+ STDERR.puts "Error: Role ID cannot be empty"
134
+ exit 1
135
+ end
136
+
118
137
  if AIA.config.fuzzy
119
138
  return fuzzy_search_role(role_id)
120
139
  else
121
- raise "Error: Could not find role with ID: #{role_id}"
140
+ STDERR.puts "Error: Could not find role with ID: #{role_id}"
141
+ exit 1
122
142
  end
123
143
  end
124
144