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.
- checksums.yaml +4 -4
- data/.version +1 -1
- data/CHANGELOG.md +66 -2
- data/README.md +133 -4
- data/docs/advanced-prompting.md +721 -0
- data/docs/cli-reference.md +582 -0
- data/docs/configuration.md +347 -0
- data/docs/contributing.md +332 -0
- data/docs/directives-reference.md +490 -0
- data/docs/examples/index.md +277 -0
- data/docs/examples/mcp/index.md +479 -0
- data/docs/examples/prompts/analysis/index.md +78 -0
- data/docs/examples/prompts/automation/index.md +108 -0
- data/docs/examples/prompts/development/index.md +125 -0
- data/docs/examples/prompts/index.md +333 -0
- data/docs/examples/prompts/learning/index.md +127 -0
- data/docs/examples/prompts/writing/index.md +62 -0
- data/docs/examples/tools/index.md +292 -0
- data/docs/faq.md +414 -0
- data/docs/guides/available-models.md +366 -0
- data/docs/guides/basic-usage.md +477 -0
- data/docs/guides/chat.md +474 -0
- data/docs/guides/executable-prompts.md +417 -0
- data/docs/guides/first-prompt.md +454 -0
- data/docs/guides/getting-started.md +455 -0
- data/docs/guides/image-generation.md +507 -0
- data/docs/guides/index.md +46 -0
- data/docs/guides/models.md +507 -0
- data/docs/guides/tools.md +856 -0
- data/docs/index.md +173 -0
- data/docs/installation.md +238 -0
- data/docs/mcp-integration.md +612 -0
- data/docs/prompt_management.md +579 -0
- data/docs/security.md +629 -0
- data/docs/tools-and-mcp-examples.md +1186 -0
- data/docs/workflows-and-pipelines.md +563 -0
- data/examples/tools/mcp/github_mcp_server.json +11 -0
- data/examples/tools/mcp/imcp.json +7 -0
- data/lib/aia/chat_processor_service.rb +19 -3
- data/lib/aia/config/base.rb +224 -0
- data/lib/aia/config/cli_parser.rb +409 -0
- data/lib/aia/config/defaults.rb +88 -0
- data/lib/aia/config/file_loader.rb +131 -0
- data/lib/aia/config/validator.rb +184 -0
- data/lib/aia/config.rb +10 -860
- data/lib/aia/directive_processor.rb +27 -372
- data/lib/aia/directives/configuration.rb +114 -0
- data/lib/aia/directives/execution.rb +37 -0
- data/lib/aia/directives/models.rb +178 -0
- data/lib/aia/directives/registry.rb +120 -0
- data/lib/aia/directives/utility.rb +70 -0
- data/lib/aia/directives/web_and_file.rb +71 -0
- data/lib/aia/prompt_handler.rb +23 -3
- data/lib/aia/ruby_llm_adapter.rb +307 -128
- data/lib/aia/session.rb +27 -14
- data/lib/aia/utility.rb +12 -8
- data/lib/aia.rb +11 -2
- data/lib/extensions/ruby_llm/.irbrc +56 -0
- data/mkdocs.yml +165 -0
- metadata +77 -20
- /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
|
data/lib/aia/prompt_handler.rb
CHANGED
@@ -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
|
-
|
73
|
+
STDERR.puts "Error: Could not find prompt with ID: #{prompt_id}"
|
74
|
+
exit 1
|
67
75
|
else
|
68
|
-
|
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
|
-
|
140
|
+
STDERR.puts "Error: Could not find role with ID: #{role_id}"
|
141
|
+
exit 1
|
122
142
|
end
|
123
143
|
end
|
124
144
|
|