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
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/aia/config/config_section.rb
4
+ #
5
+ # ConfigSection provides method access to nested configuration hashes.
6
+ # This allows dot-notation access like: config.llm.temperature
7
+ # instead of: config[:llm][:temperature]
8
+
9
+ module AIA
10
+ class ConfigSection
11
+ def initialize(hash = {})
12
+ @data = {}
13
+ (hash || {}).each do |key, value|
14
+ @data[key.to_sym] = value.is_a?(Hash) ? ConfigSection.new(value) : value
15
+ end
16
+ end
17
+
18
+ def method_missing(method, *args, &block)
19
+ key = method.to_s
20
+ if key.end_with?('=')
21
+ @data[key.chomp('=').to_sym] = args.first
22
+ elsif @data.key?(method)
23
+ @data[method]
24
+ else
25
+ nil
26
+ end
27
+ end
28
+
29
+ def respond_to_missing?(method, include_private = false)
30
+ key = method.to_s.chomp('=').to_sym
31
+ @data.key?(key) || super
32
+ end
33
+
34
+ def to_h
35
+ @data.transform_values do |v|
36
+ v.is_a?(ConfigSection) ? v.to_h : v
37
+ end
38
+ end
39
+
40
+ def [](key)
41
+ @data[key.to_sym]
42
+ end
43
+
44
+ def []=(key, value)
45
+ @data[key.to_sym] = value
46
+ end
47
+
48
+ def merge(other)
49
+ other_hash = other.is_a?(ConfigSection) ? other.to_h : other
50
+ ConfigSection.new(deep_merge(to_h, other_hash || {}))
51
+ end
52
+
53
+ def keys
54
+ @data.keys
55
+ end
56
+
57
+ def values
58
+ @data.values
59
+ end
60
+
61
+ def each(&block)
62
+ @data.each(&block)
63
+ end
64
+
65
+ def empty?
66
+ @data.empty?
67
+ end
68
+
69
+ def key?(key)
70
+ @data.key?(key.to_sym)
71
+ end
72
+
73
+ alias has_key? key?
74
+
75
+ private
76
+
77
+ def deep_merge(base, overlay)
78
+ base.merge(overlay) do |_key, old_val, new_val|
79
+ if old_val.is_a?(Hash) && new_val.is_a?(Hash)
80
+ deep_merge(old_val, new_val)
81
+ else
82
+ new_val
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,219 @@
1
+ # AIA Configuration Schema and Defaults
2
+ #
3
+ # This file is the SINGLE SOURCE OF TRUTH for AIA configuration.
4
+ # All attributes must be declared here to be recognized.
5
+ # It is bundled with the gem and loaded automatically at lowest priority.
6
+ #
7
+ # Loading priority (lowest to highest):
8
+ # 1. This file (bundled defaults)
9
+ # 2. User config (~/.aia/config.yml)
10
+ # 3. Environment variables (AIA_*)
11
+ # 4. CLI arguments
12
+ # 5. Embedded directives (//config)
13
+ #
14
+ # Environment Variable Naming:
15
+ # Use double underscore for nested keys:
16
+ # AIA_LLM__TEMPERATURE=0.5
17
+ # AIA_PROMPTS__DIR=~/my-prompts
18
+ # AIA_FLAGS__DEBUG=true
19
+
20
+ service:
21
+ name: aia
22
+
23
+ # -----------------------------------------------------------------------------
24
+ # LLM Configuration
25
+ # Access: AIA.config.llm.adapter, AIA.config.llm.temperature, etc.
26
+ # Env: AIA_LLM__ADAPTER, AIA_LLM__TEMPERATURE, etc.
27
+ # -----------------------------------------------------------------------------
28
+ llm:
29
+ adapter: ruby_llm
30
+ temperature: 0.7
31
+ max_tokens: 2048
32
+ top_p: 1.0
33
+ frequency_penalty: 0.0
34
+ presence_penalty: 0.0
35
+
36
+ # -----------------------------------------------------------------------------
37
+ # Models Configuration
38
+ # Access: AIA.config.models (array of ModelSpec objects)
39
+ # Each model has: name, role, instance, internal_id
40
+ #
41
+ # Example YAML:
42
+ # models:
43
+ # - name: gpt-4o
44
+ # role: architect
45
+ # - name: claude-3-opus
46
+ # role: reviewer
47
+ #
48
+ # Example CLI: --model "gpt-4o=architect,claude-3-opus=reviewer"
49
+ # -----------------------------------------------------------------------------
50
+ models:
51
+ - name: gpt-4o-mini
52
+ role: ~
53
+
54
+ # -----------------------------------------------------------------------------
55
+ # Prompts Configuration
56
+ # Access: AIA.config.prompts.dir, AIA.config.prompts.roles_prefix, etc.
57
+ # Env: AIA_PROMPTS__DIR, AIA_PROMPTS__ROLES_PREFIX, etc.
58
+ # -----------------------------------------------------------------------------
59
+ prompts:
60
+ dir: ~/.prompts
61
+ extname: .txt
62
+ roles_prefix: roles
63
+ roles_dir: ~/.prompts/roles
64
+ role: ~
65
+ system_prompt: ~
66
+ parameter_regex: ~
67
+
68
+ # -----------------------------------------------------------------------------
69
+ # Output Configuration
70
+ # Access: AIA.config.output.file, AIA.config.output.append, etc.
71
+ # Env: AIA_OUTPUT__FILE, AIA_OUTPUT__APPEND, etc.
72
+ # -----------------------------------------------------------------------------
73
+ output:
74
+ file: temp.md
75
+ append: false
76
+ markdown: true
77
+ history_file: ~/.prompts/_prompts.log
78
+
79
+ # -----------------------------------------------------------------------------
80
+ # Audio Configuration
81
+ # Access: AIA.config.audio.voice, AIA.config.audio.speak_command, etc.
82
+ # Env: AIA_AUDIO__VOICE, AIA_AUDIO__SPEAK_COMMAND, etc.
83
+ # -----------------------------------------------------------------------------
84
+ audio:
85
+ voice: alloy
86
+ speak_command: afplay
87
+ speech_model: tts-1
88
+ transcription_model: whisper-1
89
+
90
+ # -----------------------------------------------------------------------------
91
+ # Image Configuration
92
+ # Access: AIA.config.image.model, AIA.config.image.size, etc.
93
+ # Env: AIA_IMAGE__MODEL, AIA_IMAGE__SIZE, etc.
94
+ # -----------------------------------------------------------------------------
95
+ image:
96
+ model: dall-e-3
97
+ size: 1024x1024
98
+ quality: standard
99
+ style: vivid
100
+
101
+ # -----------------------------------------------------------------------------
102
+ # Embedding Configuration
103
+ # Access: AIA.config.embedding.model
104
+ # Env: AIA_EMBEDDING__MODEL
105
+ # -----------------------------------------------------------------------------
106
+ embedding:
107
+ model: text-embedding-ada-002
108
+
109
+ # -----------------------------------------------------------------------------
110
+ # Tools Configuration
111
+ # Access: AIA.config.tools.paths, AIA.config.tools.allowed, etc.
112
+ # Env: AIA_TOOLS__PATHS, AIA_TOOLS__ALLOWED, etc.
113
+ # -----------------------------------------------------------------------------
114
+ tools:
115
+ paths: []
116
+ allowed: ~
117
+ rejected: ~
118
+
119
+ # -----------------------------------------------------------------------------
120
+ # Flags (Boolean Options)
121
+ # Access: AIA.config.flags.chat, AIA.config.flags.debug, etc.
122
+ # Env: AIA_FLAGS__CHAT=true, AIA_FLAGS__DEBUG=true, etc.
123
+ # -----------------------------------------------------------------------------
124
+ flags:
125
+ chat: false
126
+ cost: false
127
+ debug: false
128
+ verbose: false
129
+ fuzzy: false
130
+ tokens: false
131
+ no_mcp: false
132
+ speak: false
133
+ terse: false
134
+ shell: true
135
+ erb: true
136
+ clear: false
137
+ consensus: false
138
+
139
+ # -----------------------------------------------------------------------------
140
+ # Logger Configuration
141
+ # Access: AIA.config.logger.aia.file, AIA.config.logger.llm.level, etc.
142
+ # Env: AIA_LOGGER__AIA__FILE, AIA_LOGGER__LLM__LEVEL, etc.
143
+ #
144
+ # Configures logging for three systems:
145
+ # aia: AIA application logging
146
+ # llm: RubyLLM gem logging
147
+ # mcp: RubyLLM::MCP gem logging
148
+ #
149
+ # file: STDOUT, STDERR, or a file path (e.g., ~/.aia/aia.log)
150
+ # All three loggers can write to the same file safely.
151
+ # level: debug, info, warn, error, fatal
152
+ # flush: true = immediate write (no buffering), false = buffered writes
153
+ # -----------------------------------------------------------------------------
154
+ logger:
155
+ aia:
156
+ file: STDOUT
157
+ level: warn
158
+ flush: true
159
+ llm:
160
+ file: STDOUT
161
+ level: warn
162
+ flush: true
163
+ mcp:
164
+ file: STDOUT
165
+ level: warn
166
+ flush: true
167
+
168
+ # -----------------------------------------------------------------------------
169
+ # Pipeline/Workflow Configuration
170
+ # Access: AIA.config.pipeline (array of prompt IDs)
171
+ # -----------------------------------------------------------------------------
172
+ pipeline: []
173
+
174
+ # -----------------------------------------------------------------------------
175
+ # Model Registry Configuration
176
+ # Access: AIA.config.registry.refresh
177
+ # Env: AIA_REGISTRY__REFRESH
178
+ # Note: last_refresh is derived from models.json file modification time
179
+ # -----------------------------------------------------------------------------
180
+ registry:
181
+ refresh: 7 # days between refreshes (0 = disable periodic refresh)
182
+
183
+ # -----------------------------------------------------------------------------
184
+ # Required Ruby Libraries
185
+ # Access: AIA.config.require_libs (array)
186
+ # -----------------------------------------------------------------------------
187
+ require_libs: []
188
+
189
+ # -----------------------------------------------------------------------------
190
+ # MCP Servers Configuration
191
+ # Access: AIA.config.mcp_servers (array of server configs)
192
+ #
193
+ # Example:
194
+ # mcp_servers:
195
+ # - name: my-server
196
+ # command: /path/to/server
197
+ # args: []
198
+ # env: {}
199
+ # timeout: 8000
200
+ # -----------------------------------------------------------------------------
201
+ mcp_servers: []
202
+
203
+ # -----------------------------------------------------------------------------
204
+ # Paths Configuration
205
+ # Access: AIA.config.paths.aia_dir, AIA.config.paths.config_file
206
+ # Env: AIA_PATHS__AIA_DIR, AIA_PATHS__CONFIG_FILE
207
+ #
208
+ # Note: anyway_config uses XDG Base Directory Specification by default.
209
+ # User config is loaded from ~/.config/aia/aia.yml
210
+ # -----------------------------------------------------------------------------
211
+ paths:
212
+ aia_dir: ~/.config/aia
213
+ config_file: ~/.config/aia/aia.yml
214
+
215
+ # -----------------------------------------------------------------------------
216
+ # Context Files (set at runtime)
217
+ # Access: AIA.config.context_files (array of file paths)
218
+ # -----------------------------------------------------------------------------
219
+ context_files: []
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/aia/config/defaults_loader.rb
4
+ #
5
+ # Configuration Loaders for Anyway Config
6
+ #
7
+ # Provides two custom loaders:
8
+ # 1. DefaultsLoader - Loads bundled defaults from defaults.yml
9
+ # 2. UserConfigLoader - Loads user config from ~/.config/aia/aia.yml
10
+ #
11
+ # Loading priority (lowest to highest):
12
+ # 1. Bundled defaults (DefaultsLoader)
13
+ # 2. User config (UserConfigLoader)
14
+ # 3. Environment variables (AIA_*)
15
+ # 4. CLI arguments (applied separately)
16
+
17
+ require 'anyway_config'
18
+ require 'yaml'
19
+
20
+ module AIA
21
+ module Loaders
22
+ # Loads bundled default configuration values from defaults.yml
23
+ class DefaultsLoader < Anyway::Loaders::Base
24
+ DEFAULTS_PATH = File.expand_path('../defaults.yml', __FILE__).freeze
25
+
26
+ class << self
27
+ # Returns the path to the bundled defaults file
28
+ #
29
+ # @return [String] path to defaults.yml
30
+ def defaults_path
31
+ DEFAULTS_PATH
32
+ end
33
+
34
+ # Check if defaults file exists
35
+ #
36
+ # @return [Boolean]
37
+ def defaults_exist?
38
+ File.exist?(DEFAULTS_PATH)
39
+ end
40
+
41
+ # Load and parse the raw YAML content
42
+ #
43
+ # @return [Hash] parsed YAML with symbolized keys
44
+ def load_raw_yaml
45
+ return {} unless defaults_exist?
46
+
47
+ content = File.read(defaults_path)
48
+ YAML.safe_load(
49
+ content,
50
+ permitted_classes: [Symbol, Date],
51
+ symbolize_names: true,
52
+ aliases: true
53
+ ) || {}
54
+ rescue Psych::SyntaxError => e
55
+ warn "AIA: Failed to parse bundled defaults #{defaults_path}: #{e.message}"
56
+ {}
57
+ end
58
+
59
+ # Returns the schema (all configuration keys and their defaults)
60
+ #
61
+ # @return [Hash] the complete defaults
62
+ def schema
63
+ load_raw_yaml
64
+ end
65
+
66
+ # Get a list of all top-level configuration sections
67
+ #
68
+ # @return [Array<Symbol>] list of section names
69
+ def sections
70
+ schema.keys
71
+ end
72
+ end
73
+
74
+ # Called by Anyway Config to load configuration
75
+ #
76
+ # @param name [Symbol] the config name (unused, always :aia)
77
+ # @return [Hash] configuration hash
78
+ def call(name:, **_options)
79
+ return {} unless self.class.defaults_exist?
80
+
81
+ trace!(:bundled_defaults, path: self.class.defaults_path) do
82
+ self.class.load_raw_yaml
83
+ end
84
+ end
85
+ end
86
+
87
+ # Loads user configuration from XDG config directory
88
+ # Follows XDG Base Directory Specification: ~/.config/aia/aia.yml
89
+ class UserConfigLoader < Anyway::Loaders::Base
90
+ class << self
91
+ # Returns the path to the user config file
92
+ # Uses XDG_CONFIG_HOME if set, otherwise defaults to ~/.config
93
+ #
94
+ # @return [String] path to user config file
95
+ def user_config_path
96
+ xdg_config_home = ENV.fetch('XDG_CONFIG_HOME', File.expand_path('~/.config'))
97
+ File.join(xdg_config_home, 'aia', 'aia.yml')
98
+ end
99
+
100
+ # Check if user config file exists
101
+ #
102
+ # @return [Boolean]
103
+ def user_config_exist?
104
+ File.exist?(user_config_path)
105
+ end
106
+
107
+ # Load and parse the user YAML content
108
+ #
109
+ # @return [Hash] parsed YAML with symbolized keys
110
+ def load_user_yaml
111
+ return {} unless user_config_exist?
112
+
113
+ content = File.read(user_config_path)
114
+ YAML.safe_load(
115
+ content,
116
+ permitted_classes: [Symbol, Date],
117
+ symbolize_names: true,
118
+ aliases: true
119
+ ) || {}
120
+ rescue Psych::SyntaxError => e
121
+ warn "AIA: Failed to parse user config #{user_config_path}: #{e.message}"
122
+ {}
123
+ end
124
+ end
125
+
126
+ # Called by Anyway Config to load configuration
127
+ #
128
+ # @param name [Symbol] the config name (unused, always :aia)
129
+ # @return [Hash] configuration hash
130
+ def call(name:, **_options)
131
+ return {} unless self.class.user_config_exist?
132
+
133
+ trace!(:user_config, path: self.class.user_config_path) do
134
+ self.class.load_user_yaml
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
140
+
141
+ # Register loaders in priority order (lowest to highest)
142
+ # 1. bundled_defaults - gem's default values
143
+ # 2. user_config - user's ~/.config/aia/aia.yml
144
+ # 3. yml - standard anyway_config yml loader (for config/aia.yml in app directory)
145
+ # 4. env - environment variables
146
+ Anyway.loaders.insert_before :yml, :bundled_defaults, AIA::Loaders::DefaultsLoader
147
+ Anyway.loaders.insert_after :bundled_defaults, :user_config, AIA::Loaders::UserConfigLoader
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/aia/config/mcp_parser.rb
4
+ #
5
+ # Parses MCP server JSON configuration files and converts them
6
+ # to the format expected by AIA's config system.
7
+ #
8
+ # Supports two JSON formats:
9
+ #
10
+ # 1. Simple format (single server):
11
+ # {
12
+ # "type": "stdio",
13
+ # "command": ["npx", "-y", "@server/name", "/path"]
14
+ # }
15
+ #
16
+ # 2. mcpServers format (one or more servers):
17
+ # {
18
+ # "mcpServers": {
19
+ # "server_name": {
20
+ # "command": "python",
21
+ # "args": ["-m", "module_name"],
22
+ # "env": {},
23
+ # "timeout": 8000
24
+ # }
25
+ # }
26
+ # }
27
+
28
+ require 'json'
29
+
30
+ module AIA
31
+ module McpParser
32
+ class << self
33
+ # Parse MCP server configuration files and return array of server configs
34
+ #
35
+ # @param file_paths [Array<String>] paths to JSON configuration files
36
+ # @return [Array<Hash>] array of server configurations
37
+ def parse_files(file_paths)
38
+ return [] if file_paths.nil? || file_paths.empty?
39
+
40
+ servers = []
41
+
42
+ file_paths.each do |file_path|
43
+ expanded_path = File.expand_path(file_path)
44
+
45
+ unless File.exist?(expanded_path)
46
+ warn "Warning: MCP config file not found: #{file_path}"
47
+ next
48
+ end
49
+
50
+ begin
51
+ json_content = File.read(expanded_path)
52
+ parsed = JSON.parse(json_content)
53
+ servers.concat(convert_to_config_format(parsed, file_path))
54
+ rescue JSON::ParserError => e
55
+ warn "Warning: Invalid JSON in MCP config file '#{file_path}': #{e.message}"
56
+ rescue StandardError => e
57
+ warn "Warning: Error reading MCP config file '#{file_path}': #{e.message}"
58
+ end
59
+ end
60
+
61
+ servers
62
+ end
63
+
64
+ private
65
+
66
+ # Convert parsed JSON to the config format
67
+ #
68
+ # @param parsed [Hash] parsed JSON content
69
+ # @param file_path [String] original file path (for deriving server name)
70
+ # @return [Array<Hash>] array of server configurations
71
+ def convert_to_config_format(parsed, file_path)
72
+ if parsed.key?('mcpServers')
73
+ convert_mcp_servers_format(parsed['mcpServers'])
74
+ else
75
+ convert_simple_format(parsed, file_path)
76
+ end
77
+ end
78
+
79
+ # Convert mcpServers format to config format
80
+ #
81
+ # @param mcp_servers [Hash] the mcpServers hash from JSON
82
+ # @return [Array<Hash>] array of server configurations
83
+ def convert_mcp_servers_format(mcp_servers)
84
+ mcp_servers.map do |name, config|
85
+ server = { name: name }
86
+
87
+ if config['command']
88
+ server[:command] = config['command']
89
+ end
90
+
91
+ if config['args']
92
+ server[:args] = Array(config['args'])
93
+ end
94
+
95
+ if config['env']
96
+ server[:env] = config['env']
97
+ end
98
+
99
+ if config['timeout']
100
+ server[:timeout] = config['timeout'].to_i
101
+ end
102
+
103
+ if config['url']
104
+ server[:url] = config['url']
105
+ end
106
+
107
+ if config['headers']
108
+ server[:headers] = config['headers']
109
+ end
110
+
111
+ server
112
+ end
113
+ end
114
+
115
+ # Convert simple format to config format
116
+ #
117
+ # @param parsed [Hash] parsed JSON with type and command
118
+ # @param file_path [String] file path for deriving server name
119
+ # @return [Array<Hash>] array with single server configuration
120
+ def convert_simple_format(parsed, file_path)
121
+ # Derive name from filename (e.g., "filesystem.json" -> "filesystem")
122
+ name = File.basename(file_path, '.*')
123
+
124
+ server = { name: name }
125
+
126
+ if parsed['command'].is_a?(Array)
127
+ # Command is an array: first element is command, rest are args
128
+ server[:command] = parsed['command'].first
129
+ server[:args] = parsed['command'][1..] || []
130
+ elsif parsed['command']
131
+ server[:command] = parsed['command']
132
+ server[:args] = parsed['args'] || []
133
+ end
134
+
135
+ if parsed['env']
136
+ server[:env] = parsed['env']
137
+ end
138
+
139
+ if parsed['timeout']
140
+ server[:timeout] = parsed['timeout'].to_i
141
+ end
142
+
143
+ if parsed['type']
144
+ server[:type] = parsed['type']
145
+ end
146
+
147
+ [server]
148
+ end
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/aia/config/model_spec.rb
4
+ #
5
+ # ModelSpec represents a single model configuration with optional role.
6
+ # This provides typed access to model configuration instead of raw hashes.
7
+ #
8
+ # Example:
9
+ # spec = ModelSpec.new(name: 'gpt-4o', role: 'architect')
10
+ # spec.name # => 'gpt-4o'
11
+ # spec.role # => 'architect'
12
+ # spec.internal_id # => 'gpt-4o'
13
+
14
+ module AIA
15
+ class ModelSpec
16
+ attr_accessor :name, :role, :instance, :internal_id
17
+
18
+ def initialize(hash = {})
19
+ hash = hash.transform_keys(&:to_sym) if hash.respond_to?(:transform_keys)
20
+
21
+ @name = hash[:name]
22
+ @role = hash[:role]
23
+ @instance = hash[:instance] || 1
24
+ @internal_id = hash[:internal_id] || @name
25
+ end
26
+
27
+ def to_h
28
+ {
29
+ name: @name,
30
+ role: @role,
31
+ instance: @instance,
32
+ internal_id: @internal_id
33
+ }
34
+ end
35
+
36
+ def to_s
37
+ if @role
38
+ "#{@name}=#{@role}"
39
+ else
40
+ @name.to_s
41
+ end
42
+ end
43
+
44
+ def ==(other)
45
+ return false unless other.is_a?(ModelSpec)
46
+ name == other.name && role == other.role && instance == other.instance
47
+ end
48
+
49
+ def eql?(other)
50
+ self == other
51
+ end
52
+
53
+ def hash
54
+ [name, role, instance].hash
55
+ end
56
+
57
+ # Check if this model has a role assigned
58
+ def role?
59
+ !@role.nil? && !@role.empty?
60
+ end
61
+
62
+ # Check if this is a duplicate instance of the same model
63
+ def duplicate?
64
+ @instance > 1
65
+ end
66
+ end
67
+ end