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,88 @@
|
|
1
|
+
# lib/aia/config/defaults.rb
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
require 'toml-rb'
|
5
|
+
require 'date'
|
6
|
+
require 'prompt_manager'
|
7
|
+
|
8
|
+
module AIA
|
9
|
+
module ConfigModules
|
10
|
+
module Defaults
|
11
|
+
DEFAULT_CONFIG = OpenStruct.new({
|
12
|
+
adapter: 'ruby_llm', # 'ruby_llm' or ???
|
13
|
+
#
|
14
|
+
aia_dir: File.join(ENV['HOME'], '.aia'),
|
15
|
+
config_file: File.join(ENV['HOME'], '.aia', 'config.yml'),
|
16
|
+
out_file: 'temp.md',
|
17
|
+
log_file: File.join(ENV['HOME'], '.prompts', '_prompts.log'),
|
18
|
+
context_files: [],
|
19
|
+
#
|
20
|
+
prompts_dir: File.join(ENV['HOME'], '.prompts'),
|
21
|
+
prompt_extname: PromptManager::Storage::FileSystemAdapter::PROMPT_EXTENSION,
|
22
|
+
#
|
23
|
+
roles_prefix: 'roles',
|
24
|
+
roles_dir: File.join(ENV['HOME'], '.prompts', 'roles'),
|
25
|
+
role: '',
|
26
|
+
|
27
|
+
#
|
28
|
+
system_prompt: '',
|
29
|
+
|
30
|
+
# Tools
|
31
|
+
tools: '', # Comma-separated string of loaded tool names (set by adapter)
|
32
|
+
allowed_tools: nil, # nil means all tools are allowed; otherwise an Array of Strings which are the tool names
|
33
|
+
rejected_tools: nil, # nil means no tools are rejected
|
34
|
+
tool_paths: [], # Strings - absolute and relative to tools
|
35
|
+
|
36
|
+
# Flags
|
37
|
+
markdown: true,
|
38
|
+
shell: true,
|
39
|
+
erb: true,
|
40
|
+
chat: false,
|
41
|
+
clear: false,
|
42
|
+
terse: false,
|
43
|
+
verbose: false,
|
44
|
+
debug: $DEBUG_ME,
|
45
|
+
fuzzy: false,
|
46
|
+
speak: false,
|
47
|
+
append: false, # Default to not append to existing out_file
|
48
|
+
|
49
|
+
# workflow
|
50
|
+
pipeline: [],
|
51
|
+
|
52
|
+
# PromptManager::Prompt Tailoring
|
53
|
+
parameter_regex: PromptManager::Prompt.parameter_regex.to_s,
|
54
|
+
|
55
|
+
# LLM tuning parameters
|
56
|
+
temperature: 0.7,
|
57
|
+
max_tokens: 2048,
|
58
|
+
top_p: 1.0,
|
59
|
+
frequency_penalty: 0.0,
|
60
|
+
presence_penalty: 0.0,
|
61
|
+
|
62
|
+
# Audio Parameters
|
63
|
+
voice: 'alloy',
|
64
|
+
speak_command: 'afplay', # 'afplay' for audio files on MacOS
|
65
|
+
|
66
|
+
# Image Parameters
|
67
|
+
image_size: '1024x1024',
|
68
|
+
image_quality: 'standard',
|
69
|
+
image_style: 'vivid',
|
70
|
+
|
71
|
+
# Models
|
72
|
+
model: ['gpt-4o-mini'],
|
73
|
+
consensus: nil, # nil/false = individual responses; true = consensus response
|
74
|
+
speech_model: 'tts-1',
|
75
|
+
transcription_model: 'whisper-1',
|
76
|
+
embedding_model: 'text-embedding-ada-002',
|
77
|
+
image_model: 'dall-e-3',
|
78
|
+
|
79
|
+
# Model Regristery
|
80
|
+
refresh: 7, # days between refreshes of model info; 0 means every startup
|
81
|
+
last_refresh: Date.today - 1,
|
82
|
+
|
83
|
+
# Ruby libraries to require for Ruby binding
|
84
|
+
require_libs: [],
|
85
|
+
}).freeze
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
# lib/aia/config/file_loader.rb
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
require 'toml-rb'
|
5
|
+
require 'erb'
|
6
|
+
require 'date'
|
7
|
+
|
8
|
+
module AIA
|
9
|
+
module ConfigModules
|
10
|
+
module FileLoader
|
11
|
+
class << self
|
12
|
+
def load_config_file(file, config)
|
13
|
+
if File.exist?(file)
|
14
|
+
ext = File.extname(file).downcase
|
15
|
+
content = File.read(file)
|
16
|
+
|
17
|
+
# Process ERB if filename ends with .erb
|
18
|
+
if file.end_with?('.erb')
|
19
|
+
content = ERB.new(content).result
|
20
|
+
file = file.chomp('.erb')
|
21
|
+
File.write(file, content)
|
22
|
+
end
|
23
|
+
|
24
|
+
file_config = case ext
|
25
|
+
when '.yml', '.yaml'
|
26
|
+
YAML.safe_load(content, permitted_classes: [Symbol], symbolize_names: true)
|
27
|
+
when '.toml'
|
28
|
+
TomlRB.parse(content)
|
29
|
+
else
|
30
|
+
raise "Unsupported config file format: #{ext}"
|
31
|
+
end
|
32
|
+
|
33
|
+
file_config.each do |key, value|
|
34
|
+
config[key.to_sym] = value
|
35
|
+
end
|
36
|
+
else
|
37
|
+
raise "Config file not found: #{file}"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def cf_options(file)
|
42
|
+
config = OpenStruct.new
|
43
|
+
|
44
|
+
if File.exist?(file)
|
45
|
+
content = read_and_process_config_file(file)
|
46
|
+
file_config = parse_config_content(content, File.extname(file).downcase)
|
47
|
+
apply_file_config_to_struct(config, file_config)
|
48
|
+
else
|
49
|
+
STDERR.puts "WARNING:Config file not found: #{file}"
|
50
|
+
end
|
51
|
+
|
52
|
+
normalize_last_refresh_date(config)
|
53
|
+
config
|
54
|
+
end
|
55
|
+
|
56
|
+
def read_and_process_config_file(file)
|
57
|
+
content = File.read(file)
|
58
|
+
|
59
|
+
# Process ERB if filename ends with .erb
|
60
|
+
if file.end_with?('.erb')
|
61
|
+
content = ERB.new(content).result
|
62
|
+
processed_file = file.chomp('.erb')
|
63
|
+
File.write(processed_file, content)
|
64
|
+
end
|
65
|
+
|
66
|
+
content
|
67
|
+
end
|
68
|
+
|
69
|
+
def parse_config_content(content, ext)
|
70
|
+
case ext
|
71
|
+
when '.yml', '.yaml'
|
72
|
+
YAML.safe_load(content, permitted_classes: [Symbol], symbolize_names: true)
|
73
|
+
when '.toml'
|
74
|
+
TomlRB.parse(content)
|
75
|
+
else
|
76
|
+
raise "Unsupported config file format: #{ext}"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def apply_file_config_to_struct(config, file_config)
|
81
|
+
file_config.each do |key, value|
|
82
|
+
config[key] = value
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def normalize_last_refresh_date(config)
|
87
|
+
return unless config.last_refresh&.is_a?(String)
|
88
|
+
|
89
|
+
config.last_refresh = Date.strptime(config.last_refresh, '%Y-%m-%d')
|
90
|
+
end
|
91
|
+
|
92
|
+
def dump_config(config, file)
|
93
|
+
# Implementation for config dump
|
94
|
+
ext = File.extname(file).downcase
|
95
|
+
|
96
|
+
config.last_refresh = config.last_refresh.to_s if config.last_refresh.is_a? Date
|
97
|
+
|
98
|
+
config_hash = config.to_h
|
99
|
+
|
100
|
+
# Remove prompt_id to prevent automatic initial pompting in --chat mode
|
101
|
+
config_hash.delete(:prompt_id)
|
102
|
+
|
103
|
+
# Remove dump_file key to prevent automatic exit on next load
|
104
|
+
config_hash.delete(:dump_file)
|
105
|
+
|
106
|
+
content = case ext
|
107
|
+
when '.yml', '.yaml'
|
108
|
+
YAML.dump(config_hash)
|
109
|
+
when '.toml'
|
110
|
+
TomlRB.dump(config_hash)
|
111
|
+
else
|
112
|
+
raise "Unsupported config file format: #{ext}"
|
113
|
+
end
|
114
|
+
|
115
|
+
File.write(file, content)
|
116
|
+
puts "Config successfully dumped to #{file}"
|
117
|
+
end
|
118
|
+
|
119
|
+
def generate_completion_script(shell)
|
120
|
+
script_path = File.join(File.dirname(__FILE__), "../../aia_completion.#{shell}")
|
121
|
+
|
122
|
+
if File.exist?(script_path)
|
123
|
+
puts File.read(script_path)
|
124
|
+
else
|
125
|
+
STDERR.puts "ERROR: The shell '#{shell}' is not supported or the completion script is missing."
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,184 @@
|
|
1
|
+
# lib/aia/config/validator.rb
|
2
|
+
|
3
|
+
require 'ostruct'
|
4
|
+
|
5
|
+
module AIA
|
6
|
+
module ConfigModules
|
7
|
+
module Validator
|
8
|
+
class << self
|
9
|
+
def tailor_the_config(config)
|
10
|
+
remaining_args = config.remaining_args.dup
|
11
|
+
config.remaining_args = nil
|
12
|
+
|
13
|
+
stdin_content = process_stdin_content
|
14
|
+
config.stdin_content = stdin_content if stdin_content && !stdin_content.strip.empty?
|
15
|
+
|
16
|
+
process_prompt_id_from_args(config, remaining_args)
|
17
|
+
validate_and_set_context_files(config, remaining_args)
|
18
|
+
handle_executable_prompt(config)
|
19
|
+
validate_required_prompt_id(config)
|
20
|
+
process_role_configuration(config)
|
21
|
+
handle_fuzzy_search_prompt_id(config)
|
22
|
+
normalize_boolean_flags(config)
|
23
|
+
handle_completion_script(config)
|
24
|
+
validate_final_prompt_requirements(config)
|
25
|
+
configure_prompt_manager(config)
|
26
|
+
prepare_pipeline(config)
|
27
|
+
validate_pipeline_prompts(config)
|
28
|
+
|
29
|
+
config
|
30
|
+
end
|
31
|
+
|
32
|
+
def process_stdin_content
|
33
|
+
stdin_content = ''
|
34
|
+
|
35
|
+
if !STDIN.tty? && !STDIN.closed?
|
36
|
+
begin
|
37
|
+
stdin_content << "\n" + STDIN.read
|
38
|
+
STDIN.reopen('/dev/tty') # Reopen STDIN for interactive use
|
39
|
+
rescue => _
|
40
|
+
# If we can't reopen, continue without error
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
stdin_content
|
45
|
+
end
|
46
|
+
|
47
|
+
def process_prompt_id_from_args(config, remaining_args)
|
48
|
+
return if remaining_args.empty?
|
49
|
+
|
50
|
+
maybe_id = remaining_args.first
|
51
|
+
maybe_id_plus = File.join(config.prompts_dir, maybe_id + config.prompt_extname)
|
52
|
+
|
53
|
+
if AIA.bad_file?(maybe_id) && AIA.good_file?(maybe_id_plus)
|
54
|
+
config.prompt_id = remaining_args.shift
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def validate_and_set_context_files(config, remaining_args)
|
59
|
+
return if remaining_args.empty?
|
60
|
+
|
61
|
+
bad_files = remaining_args.reject { |filename| AIA.good_file?(filename) }
|
62
|
+
if bad_files.any?
|
63
|
+
STDERR.puts "Error: The following files do not exist: #{bad_files.join(', ')}"
|
64
|
+
exit 1
|
65
|
+
end
|
66
|
+
|
67
|
+
config.context_files ||= []
|
68
|
+
config.context_files += remaining_args
|
69
|
+
end
|
70
|
+
|
71
|
+
def handle_executable_prompt(config)
|
72
|
+
return unless config.executable_prompt && config.context_files && !config.context_files.empty?
|
73
|
+
|
74
|
+
config.executable_prompt_file = config.context_files.pop
|
75
|
+
end
|
76
|
+
|
77
|
+
def validate_required_prompt_id(config)
|
78
|
+
return unless config.prompt_id.nil? && !config.chat && !config.fuzzy
|
79
|
+
|
80
|
+
STDERR.puts "Error: A prompt ID is required unless using --chat, --fuzzy, or providing context files. Use -h or --help for help."
|
81
|
+
exit 1
|
82
|
+
end
|
83
|
+
|
84
|
+
def process_role_configuration(config)
|
85
|
+
return if config.role.empty?
|
86
|
+
|
87
|
+
unless config.roles_prefix.empty?
|
88
|
+
unless config.role.start_with?(config.roles_prefix)
|
89
|
+
config.role.prepend "#{config.roles_prefix}/"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
config.roles_dir ||= File.join(config.prompts_dir, config.roles_prefix)
|
94
|
+
|
95
|
+
if config.prompt_id.nil? || config.prompt_id.empty?
|
96
|
+
if !config.role.nil? && !config.role.empty?
|
97
|
+
config.prompt_id = config.role
|
98
|
+
config.pipeline.prepend config.prompt_id
|
99
|
+
config.role = ''
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def handle_fuzzy_search_prompt_id(config)
|
105
|
+
return unless config.fuzzy && config.prompt_id.empty?
|
106
|
+
|
107
|
+
# When fuzzy search is enabled but no prompt ID is provided,
|
108
|
+
# set a special value to trigger fuzzy search without an initial query
|
109
|
+
# SMELL: This feels like a cludge
|
110
|
+
config.prompt_id = '__FUZZY_SEARCH__'
|
111
|
+
end
|
112
|
+
|
113
|
+
def normalize_boolean_flags(config)
|
114
|
+
normalize_boolean_flag(config, :chat)
|
115
|
+
normalize_boolean_flag(config, :fuzzy)
|
116
|
+
normalize_boolean_flag(config, :consensus)
|
117
|
+
end
|
118
|
+
|
119
|
+
def normalize_boolean_flag(config, flag)
|
120
|
+
return if [TrueClass, FalseClass].include?(config[flag].class)
|
121
|
+
|
122
|
+
config[flag] = case config[flag]
|
123
|
+
when nil, '', 'false', false
|
124
|
+
false
|
125
|
+
when 'true', true
|
126
|
+
true
|
127
|
+
else
|
128
|
+
# For any other non-empty string value, treat as true
|
129
|
+
true
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def handle_completion_script(config)
|
134
|
+
return unless config.completion
|
135
|
+
|
136
|
+
FileLoader.generate_completion_script(config.completion)
|
137
|
+
exit
|
138
|
+
end
|
139
|
+
|
140
|
+
def validate_final_prompt_requirements(config)
|
141
|
+
# Only require a prompt_id if we're not in chat mode, not using fuzzy search, and no context files
|
142
|
+
if !config.chat && !config.fuzzy && (config.prompt_id.nil? || config.prompt_id.empty?) && (!config.context_files || config.context_files.empty?)
|
143
|
+
STDERR.puts "Error: A prompt ID is required unless using --chat, --fuzzy, or providing context files. Use -h or --help for help."
|
144
|
+
exit 1
|
145
|
+
end
|
146
|
+
|
147
|
+
# If we're in chat mode with context files but no prompt_id, that's valid
|
148
|
+
# This is handled implicitly - no action needed
|
149
|
+
end
|
150
|
+
|
151
|
+
def configure_prompt_manager(config)
|
152
|
+
return unless config.parameter_regex
|
153
|
+
|
154
|
+
PromptManager::Prompt.parameter_regex = Regexp.new(config.parameter_regex)
|
155
|
+
end
|
156
|
+
|
157
|
+
def prepare_pipeline(config)
|
158
|
+
return if config.prompt_id.nil? || config.prompt_id.empty? || config.prompt_id == config.pipeline.first
|
159
|
+
|
160
|
+
config.pipeline.prepend config.prompt_id
|
161
|
+
end
|
162
|
+
|
163
|
+
def validate_pipeline_prompts(config)
|
164
|
+
return if config.pipeline.empty?
|
165
|
+
|
166
|
+
and_exit = false
|
167
|
+
|
168
|
+
config.pipeline.each do |prompt_id|
|
169
|
+
# Skip empty prompt IDs (can happen in chat-only mode)
|
170
|
+
next if prompt_id.nil? || prompt_id.empty?
|
171
|
+
|
172
|
+
prompt_file_path = File.join(config.prompts_dir, "#{prompt_id}.txt")
|
173
|
+
unless File.exist?(prompt_file_path)
|
174
|
+
STDERR.puts "Error: Prompt ID '#{prompt_id}' does not exist at #{prompt_file_path}"
|
175
|
+
and_exit = true
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
exit(1) if and_exit
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|