aia 0.9.18 → 0.9.20
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 +220 -78
- data/README.md +128 -3
- data/docs/cli-reference.md +71 -4
- data/docs/guides/models.md +196 -1
- data/lib/aia/chat_processor_service.rb +14 -5
- data/lib/aia/config/base.rb +6 -1
- data/lib/aia/config/cli_parser.rb +116 -2
- data/lib/aia/config/file_loader.rb +33 -1
- data/lib/aia/prompt_handler.rb +22 -1
- data/lib/aia/ruby_llm_adapter.rb +224 -134
- data/lib/aia/session.rb +120 -28
- data/lib/aia/utility.rb +19 -1
- metadata +1 -1
@@ -79,10 +79,42 @@ module AIA
|
|
79
79
|
|
80
80
|
def apply_file_config_to_struct(config, file_config)
|
81
81
|
file_config.each do |key, value|
|
82
|
-
|
82
|
+
# Special handling for model array with roles (ADR-005 v2)
|
83
|
+
if (key == :model || key == 'model') && value.is_a?(Array) && value.first.is_a?(Hash)
|
84
|
+
config[:model] = process_model_array_with_roles(value)
|
85
|
+
else
|
86
|
+
config[key] = value
|
87
|
+
end
|
83
88
|
end
|
84
89
|
end
|
85
90
|
|
91
|
+
# Process model array with roles from config file (ADR-005 v2)
|
92
|
+
# Format: [{model: "gpt-4o", role: "architect"}, ...]
|
93
|
+
# Also supports models without roles: [{model: "gpt-4o"}, ...]
|
94
|
+
def process_model_array_with_roles(models_array)
|
95
|
+
return [] if models_array.nil? || models_array.empty?
|
96
|
+
|
97
|
+
model_specs = []
|
98
|
+
model_counts = Hash.new(0)
|
99
|
+
|
100
|
+
models_array.each do |spec|
|
101
|
+
model_name = spec[:model] || spec['model']
|
102
|
+
role_name = spec[:role] || spec['role']
|
103
|
+
|
104
|
+
model_counts[model_name] += 1
|
105
|
+
instance = model_counts[model_name]
|
106
|
+
|
107
|
+
model_specs << {
|
108
|
+
model: model_name,
|
109
|
+
role: role_name,
|
110
|
+
instance: instance,
|
111
|
+
internal_id: instance > 1 ? "#{model_name}##{instance}" : model_name
|
112
|
+
}
|
113
|
+
end
|
114
|
+
|
115
|
+
model_specs
|
116
|
+
end
|
117
|
+
|
86
118
|
def normalize_last_refresh_date(config)
|
87
119
|
return unless config.last_refresh&.is_a?(String)
|
88
120
|
|
data/lib/aia/prompt_handler.rb
CHANGED
@@ -101,7 +101,7 @@ module AIA
|
|
101
101
|
def fetch_role(role_id)
|
102
102
|
# Handle nil role_id
|
103
103
|
return handle_missing_role("roles/") if role_id.nil?
|
104
|
-
|
104
|
+
|
105
105
|
# Prepend roles_prefix if not already present
|
106
106
|
unless role_id.start_with?(AIA.config.roles_prefix)
|
107
107
|
role_id = "#{AIA.config.roles_prefix}/#{role_id}"
|
@@ -126,6 +126,27 @@ module AIA
|
|
126
126
|
handle_missing_role(role_id)
|
127
127
|
end
|
128
128
|
|
129
|
+
# Load role for a specific model (ADR-005)
|
130
|
+
# Takes a model spec hash and default role, returns role text
|
131
|
+
def load_role_for_model(model_spec, default_role = nil)
|
132
|
+
# Determine which role to use
|
133
|
+
role_id = if model_spec.is_a?(Hash)
|
134
|
+
model_spec[:role] || default_role
|
135
|
+
else
|
136
|
+
# Backward compatibility: if model_spec is a string, use default role
|
137
|
+
default_role
|
138
|
+
end
|
139
|
+
|
140
|
+
return nil if role_id.nil? || role_id.empty?
|
141
|
+
|
142
|
+
# Load the role using existing fetch_role method
|
143
|
+
role_prompt = fetch_role(role_id)
|
144
|
+
role_prompt.text
|
145
|
+
rescue => e
|
146
|
+
puts "Warning: Could not load role '#{role_id}' for model: #{e.message}"
|
147
|
+
nil
|
148
|
+
end
|
149
|
+
|
129
150
|
def handle_missing_role(role_id)
|
130
151
|
# Handle empty/nil role_id
|
131
152
|
role_id = role_id.to_s.strip
|
data/lib/aia/ruby_llm_adapter.rb
CHANGED
@@ -5,11 +5,13 @@ require_relative '../extensions/ruby_llm/provider_fix'
|
|
5
5
|
|
6
6
|
module AIA
|
7
7
|
class RubyLLMAdapter
|
8
|
-
attr_reader :tools
|
8
|
+
attr_reader :tools, :model_specs
|
9
9
|
|
10
10
|
def initialize
|
11
|
-
@
|
11
|
+
@model_specs = extract_models_config # Full specs with role info
|
12
|
+
@models = extract_model_names(@model_specs) # Just model names for backward compat
|
12
13
|
@chats = {}
|
14
|
+
@contexts = {} # Store isolated contexts for each model
|
13
15
|
|
14
16
|
configure_rubyllm
|
15
17
|
refresh_local_model_registry
|
@@ -80,44 +82,72 @@ module AIA
|
|
80
82
|
end
|
81
83
|
|
82
84
|
|
85
|
+
# Create an isolated RubyLLM::Context for a model to prevent cross-talk (ADR-002)
|
86
|
+
# Each model gets its own context with provider-specific configuration
|
87
|
+
def create_isolated_context_for_model(model_name)
|
88
|
+
config = RubyLLM.config.dup
|
89
|
+
|
90
|
+
# Apply provider-specific configuration
|
91
|
+
if model_name.start_with?('lms/')
|
92
|
+
config.openai_api_base = ENV.fetch('LMS_API_BASE', 'http://localhost:1234/v1')
|
93
|
+
config.openai_api_key = 'dummy' # Local servers don't need a real API key
|
94
|
+
elsif model_name.start_with?('osaurus/')
|
95
|
+
config.openai_api_base = ENV.fetch('OSAURUS_API_BASE', 'http://localhost:11434/v1')
|
96
|
+
config.openai_api_key = 'dummy' # Local servers don't need a real API key
|
97
|
+
end
|
98
|
+
|
99
|
+
RubyLLM::Context.new(config)
|
100
|
+
end
|
101
|
+
|
102
|
+
|
103
|
+
# Extract the actual model name and provider from the prefixed model_name
|
104
|
+
# Returns: [actual_model, provider] where provider may be nil for auto-detection
|
105
|
+
def extract_model_and_provider(model_name)
|
106
|
+
if model_name.start_with?('ollama/')
|
107
|
+
[model_name.sub('ollama/', ''), 'ollama']
|
108
|
+
elsif model_name.start_with?('lms/') || model_name.start_with?('osaurus/')
|
109
|
+
[model_name.sub(%r{^(lms|osaurus)/}, ''), 'openai']
|
110
|
+
else
|
111
|
+
[model_name, nil] # Let RubyLLM auto-detect provider
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
|
83
116
|
def setup_chats_with_tools
|
84
117
|
valid_chats = {}
|
118
|
+
valid_contexts = {}
|
119
|
+
valid_specs = []
|
85
120
|
failed_models = []
|
86
121
|
|
87
|
-
@
|
122
|
+
@model_specs.each do |spec|
|
123
|
+
model_name = spec[:model] # Actual model name (e.g., "gpt-4o")
|
124
|
+
internal_id = spec[:internal_id] # Key for storage (e.g., "gpt-4o#1", "gpt-4o#2")
|
125
|
+
|
88
126
|
begin
|
89
|
-
#
|
90
|
-
|
91
|
-
# For Ollama models, extract the actual model name and use assume_model_exists
|
92
|
-
actual_model = model_name.sub('ollama/', '')
|
93
|
-
chat = RubyLLM.chat(model: actual_model, provider: 'ollama', assume_model_exists: true)
|
94
|
-
elsif model_name.start_with?('osaurus/')
|
95
|
-
# For Osaurus models (OpenAI-compatible), create a custom context with the right API base
|
96
|
-
actual_model = model_name.sub('osaurus/', '')
|
97
|
-
custom_config = RubyLLM.config.dup
|
98
|
-
custom_config.openai_api_base = ENV.fetch('OSAURUS_API_BASE', 'http://localhost:11434/v1')
|
99
|
-
custom_config.openai_api_key = 'dummy' # Local servers don't need a real API key
|
100
|
-
context = RubyLLM::Context.new(custom_config)
|
101
|
-
chat = context.chat(model: actual_model, provider: 'openai', assume_model_exists: true)
|
102
|
-
elsif model_name.start_with?('lms/')
|
103
|
-
# For LM Studio models (OpenAI-compatible), create a custom context with the right API base
|
104
|
-
actual_model = model_name.sub('lms/', '')
|
105
|
-
lms_api_base = ENV.fetch('LMS_API_BASE', 'http://localhost:1234/v1')
|
127
|
+
# Create isolated context for this model to prevent cross-talk (ADR-002)
|
128
|
+
context = create_isolated_context_for_model(model_name)
|
106
129
|
|
107
|
-
|
108
|
-
|
130
|
+
# Determine provider and actual model name
|
131
|
+
actual_model, provider = extract_model_and_provider(model_name)
|
109
132
|
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
chat = context.chat(model: actual_model, provider: 'openai', assume_model_exists: true)
|
115
|
-
else
|
116
|
-
chat = RubyLLM.chat(model: model_name)
|
133
|
+
# Validate LM Studio models
|
134
|
+
if model_name.start_with?('lms/')
|
135
|
+
lms_api_base = ENV.fetch('LMS_API_BASE', 'http://localhost:1234/v1')
|
136
|
+
validate_lms_model!(actual_model, lms_api_base)
|
117
137
|
end
|
118
|
-
|
138
|
+
|
139
|
+
# Create chat using isolated context
|
140
|
+
chat = if provider
|
141
|
+
context.chat(model: actual_model, provider: provider, assume_model_exists: true)
|
142
|
+
else
|
143
|
+
context.chat(model: actual_model)
|
144
|
+
end
|
145
|
+
|
146
|
+
valid_chats[internal_id] = chat
|
147
|
+
valid_contexts[internal_id] = context
|
148
|
+
valid_specs << spec
|
119
149
|
rescue StandardError => e
|
120
|
-
failed_models << "#{
|
150
|
+
failed_models << "#{internal_id}: #{e.message}"
|
121
151
|
end
|
122
152
|
end
|
123
153
|
|
@@ -135,10 +165,12 @@ module AIA
|
|
135
165
|
end
|
136
166
|
|
137
167
|
@chats = valid_chats
|
168
|
+
@contexts = valid_contexts
|
169
|
+
@model_specs = valid_specs
|
138
170
|
@models = valid_chats.keys
|
139
171
|
|
140
|
-
# Update the config to reflect only the valid models
|
141
|
-
AIA.config.model = @
|
172
|
+
# Update the config to reflect only the valid models (keep as specs)
|
173
|
+
AIA.config.model = @model_specs
|
142
174
|
|
143
175
|
# Report successful models
|
144
176
|
if failed_models.any?
|
@@ -254,40 +286,96 @@ module AIA
|
|
254
286
|
result
|
255
287
|
end
|
256
288
|
|
257
|
-
def single_model_chat(prompt,
|
258
|
-
chat_instance = @chats[
|
289
|
+
def single_model_chat(prompt, internal_id)
|
290
|
+
chat_instance = @chats[internal_id]
|
259
291
|
modes = chat_instance.model.modalities
|
260
292
|
|
261
293
|
# TODO: Need to consider how to handle multi-mode models
|
262
294
|
result = if modes.text_to_text?
|
263
|
-
text_to_text_single(prompt,
|
295
|
+
text_to_text_single(prompt, internal_id)
|
264
296
|
elsif modes.image_to_text?
|
265
|
-
image_to_text_single(prompt,
|
297
|
+
image_to_text_single(prompt, internal_id)
|
266
298
|
elsif modes.text_to_image?
|
267
|
-
text_to_image_single(prompt,
|
299
|
+
text_to_image_single(prompt, internal_id)
|
268
300
|
elsif modes.text_to_audio?
|
269
|
-
text_to_audio_single(prompt,
|
301
|
+
text_to_audio_single(prompt, internal_id)
|
270
302
|
elsif modes.audio_to_text?
|
271
|
-
audio_to_text_single(prompt,
|
303
|
+
audio_to_text_single(prompt, internal_id)
|
272
304
|
else
|
273
305
|
# TODO: what else can be done?
|
274
|
-
"Error: No matching modality for model #{
|
306
|
+
"Error: No matching modality for model #{internal_id}"
|
275
307
|
end
|
276
308
|
|
277
309
|
result
|
278
310
|
end
|
279
311
|
|
280
|
-
|
312
|
+
# Prepend role content to prompt for a specific model (ADR-005)
|
313
|
+
def prepend_model_role(prompt, internal_id)
|
314
|
+
# Get model spec to find role
|
315
|
+
spec = get_model_spec(internal_id)
|
316
|
+
return prompt unless spec && spec[:role]
|
317
|
+
|
318
|
+
# Get role content using PromptHandler
|
319
|
+
# Need to create PromptHandler instance if not already available
|
320
|
+
prompt_handler = AIA::PromptHandler.new
|
321
|
+
role_content = prompt_handler.load_role_for_model(spec, AIA.config.role)
|
322
|
+
|
323
|
+
return prompt unless role_content
|
324
|
+
|
325
|
+
# Prepend role to prompt based on prompt type
|
326
|
+
if prompt.is_a?(String)
|
327
|
+
# Simple string prompt
|
328
|
+
"#{role_content}\n\n#{prompt}"
|
329
|
+
elsif prompt.is_a?(Array)
|
330
|
+
# Conversation array - prepend to first user message
|
331
|
+
prepend_role_to_conversation(prompt, role_content)
|
332
|
+
else
|
333
|
+
prompt
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
337
|
+
def prepend_role_to_conversation(conversation, role_content)
|
338
|
+
# Find the first user message and prepend role
|
339
|
+
modified = conversation.dup
|
340
|
+
first_user_index = modified.find_index { |msg| msg[:role] == "user" || msg["role"] == "user" }
|
341
|
+
|
342
|
+
if first_user_index
|
343
|
+
msg = modified[first_user_index].dup
|
344
|
+
role_key = msg.key?(:role) ? :role : "role"
|
345
|
+
content_key = msg.key?(:content) ? :content : "content"
|
346
|
+
|
347
|
+
msg[content_key] = "#{role_content}\n\n#{msg[content_key]}"
|
348
|
+
modified[first_user_index] = msg
|
349
|
+
end
|
350
|
+
|
351
|
+
modified
|
352
|
+
end
|
353
|
+
|
354
|
+
def multi_model_chat(prompt_or_contexts)
|
281
355
|
results = {}
|
282
356
|
|
357
|
+
# Check if we're receiving per-model contexts (Hash) or shared prompt (String/Array) - ADR-002 revised
|
358
|
+
per_model_contexts = prompt_or_contexts.is_a?(Hash) &&
|
359
|
+
prompt_or_contexts.keys.all? { |k| @models.include?(k) }
|
360
|
+
|
283
361
|
Async do |task|
|
284
|
-
@models.each do |
|
362
|
+
@models.each do |internal_id|
|
285
363
|
task.async do
|
286
364
|
begin
|
287
|
-
|
288
|
-
|
365
|
+
# Use model-specific context if available, otherwise shared prompt
|
366
|
+
prompt = if per_model_contexts
|
367
|
+
prompt_or_contexts[internal_id]
|
368
|
+
else
|
369
|
+
prompt_or_contexts
|
370
|
+
end
|
371
|
+
|
372
|
+
# Add per-model role if specified (ADR-005)
|
373
|
+
prompt = prepend_model_role(prompt, internal_id)
|
374
|
+
|
375
|
+
result = single_model_chat(prompt, internal_id)
|
376
|
+
results[internal_id] = result
|
289
377
|
rescue StandardError => e
|
290
|
-
results[
|
378
|
+
results[internal_id] = "Error with #{internal_id}: #{e.message}"
|
291
379
|
end
|
292
380
|
end
|
293
381
|
end
|
@@ -319,14 +407,17 @@ module AIA
|
|
319
407
|
primary_chat = @chats[primary_model]
|
320
408
|
|
321
409
|
# Build the consensus prompt with all model responses
|
410
|
+
# Note: This prompt does NOT include the model's role (ADR-005)
|
411
|
+
# The primary model synthesizes neutrally without role bias
|
322
412
|
consensus_prompt = build_consensus_prompt(results)
|
323
413
|
|
324
414
|
begin
|
325
415
|
# Have the primary model generate the consensus
|
416
|
+
# The consensus prompt is already role-neutral
|
326
417
|
consensus_result = primary_chat.ask(consensus_prompt).content
|
327
418
|
|
328
|
-
# Format the consensus response
|
329
|
-
"from: #{primary_model}
|
419
|
+
# Format the consensus response - no role label for consensus
|
420
|
+
"from: #{primary_model}\n#{consensus_result}"
|
330
421
|
rescue StandardError => e
|
331
422
|
# If consensus fails, fall back to individual responses
|
332
423
|
"Error generating consensus: #{e.message}\n\n" + format_individual_responses(results)
|
@@ -370,10 +461,14 @@ module AIA
|
|
370
461
|
# Return structured data that preserves metrics for multi-model
|
371
462
|
format_multi_model_with_metrics(results)
|
372
463
|
else
|
373
|
-
# Original string formatting for non-metrics mode
|
464
|
+
# Original string formatting for non-metrics mode with role labels (ADR-005)
|
374
465
|
output = []
|
375
|
-
results.each do |
|
376
|
-
|
466
|
+
results.each do |internal_id, result|
|
467
|
+
# Get model spec to include role in output
|
468
|
+
spec = get_model_spec(internal_id)
|
469
|
+
display_name = format_model_display_name(spec)
|
470
|
+
|
471
|
+
output << "from: #{display_name}"
|
377
472
|
# Extract content from RubyLLM::Message if needed
|
378
473
|
content = if result.respond_to?(:content)
|
379
474
|
result.content
|
@@ -387,6 +482,27 @@ module AIA
|
|
387
482
|
end
|
388
483
|
end
|
389
484
|
|
485
|
+
# Format display name with instance number and role (ADR-005)
|
486
|
+
def format_model_display_name(spec)
|
487
|
+
return spec unless spec.is_a?(Hash)
|
488
|
+
|
489
|
+
model_name = spec[:model]
|
490
|
+
instance = spec[:instance]
|
491
|
+
role = spec[:role]
|
492
|
+
|
493
|
+
# Add instance number if > 1
|
494
|
+
display = if instance > 1
|
495
|
+
"#{model_name} ##{instance}"
|
496
|
+
else
|
497
|
+
model_name
|
498
|
+
end
|
499
|
+
|
500
|
+
# Add role label if present
|
501
|
+
display += " (#{role})" if role
|
502
|
+
|
503
|
+
display
|
504
|
+
end
|
505
|
+
|
390
506
|
def format_multi_model_with_metrics(results)
|
391
507
|
# Create a composite response that includes all model responses and metrics
|
392
508
|
formatted_content = []
|
@@ -452,96 +568,46 @@ module AIA
|
|
452
568
|
|
453
569
|
# Clear the chat context/history
|
454
570
|
# Needed for the //clear and //restore directives
|
571
|
+
# Simplified with ADR-002: Each model has isolated context, no global state to manage
|
455
572
|
def clear_context
|
456
|
-
@chats.
|
457
|
-
|
458
|
-
if chat.instance_variable_defined?(:@messages)
|
459
|
-
chat.instance_variable_get(:@messages)
|
460
|
-
# Force a completely empty array, not just attempting to clear it
|
461
|
-
chat.instance_variable_set(:@messages, [])
|
462
|
-
end
|
463
|
-
end
|
464
|
-
|
465
|
-
# Option 2: Force RubyLLM to create a new chat instance at the global level
|
466
|
-
# This ensures any shared state is reset
|
467
|
-
RubyLLM.instance_variable_set(:@chat, nil) if RubyLLM.instance_variable_defined?(:@chat)
|
573
|
+
old_chats = @chats.dup
|
574
|
+
new_chats = {}
|
468
575
|
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
576
|
+
@models.each do |model_name|
|
577
|
+
begin
|
578
|
+
# Get the isolated context for this model
|
579
|
+
context = @contexts[model_name]
|
580
|
+
actual_model, provider = extract_model_and_provider(model_name)
|
473
581
|
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
actual_model = model_name.sub('ollama/', '')
|
481
|
-
@chats[model_name] = RubyLLM.chat(model: actual_model, provider: 'ollama', assume_model_exists: true)
|
482
|
-
elsif model_name.start_with?('osaurus/')
|
483
|
-
actual_model = model_name.sub('osaurus/', '')
|
484
|
-
custom_config = RubyLLM.config.dup
|
485
|
-
custom_config.openai_api_base = ENV.fetch('OSAURUS_API_BASE', 'http://localhost:11434/v1')
|
486
|
-
custom_config.openai_api_key = 'dummy'
|
487
|
-
context = RubyLLM::Context.new(custom_config)
|
488
|
-
@chats[model_name] = context.chat(model: actual_model, provider: 'openai', assume_model_exists: true)
|
489
|
-
elsif model_name.start_with?('lms/')
|
490
|
-
actual_model = model_name.sub('lms/', '')
|
491
|
-
lms_api_base = ENV.fetch('LMS_API_BASE', 'http://localhost:1234/v1')
|
492
|
-
|
493
|
-
# Validate model exists in LM Studio
|
494
|
-
validate_lms_model!(actual_model, lms_api_base)
|
495
|
-
|
496
|
-
custom_config = RubyLLM.config.dup
|
497
|
-
custom_config.openai_api_base = lms_api_base
|
498
|
-
custom_config.openai_api_key = 'dummy'
|
499
|
-
context = RubyLLM::Context.new(custom_config)
|
500
|
-
@chats[model_name] = context.chat(model: actual_model, provider: 'openai', assume_model_exists: true)
|
501
|
-
else
|
502
|
-
@chats[model_name] = RubyLLM.chat(model: model_name)
|
503
|
-
end
|
582
|
+
# Create a fresh chat instance from the same isolated context
|
583
|
+
chat = if provider
|
584
|
+
context.chat(model: actual_model, provider: provider, assume_model_exists: true)
|
585
|
+
else
|
586
|
+
context.chat(model: actual_model)
|
587
|
+
end
|
504
588
|
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
end
|
509
|
-
rescue StandardError => e
|
510
|
-
# If we can't create a new chat, keep the old one but clear its context
|
511
|
-
warn "Warning: Could not recreate chat for #{model_name}: #{e.message}. Keeping existing instance."
|
512
|
-
@chats[model_name] = old_chats[model_name]
|
513
|
-
# Clear the old chat's messages if possible
|
514
|
-
if @chats[model_name] && @chats[model_name].instance_variable_defined?(:@messages)
|
515
|
-
@chats[model_name].instance_variable_set(:@messages, [])
|
516
|
-
end
|
589
|
+
# Re-add tools if they were previously loaded
|
590
|
+
if @tools && !@tools.empty? && chat.model&.supports_functions?
|
591
|
+
chat.with_tools(*@tools)
|
517
592
|
end
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
if chat
|
593
|
+
|
594
|
+
new_chats[model_name] = chat
|
595
|
+
rescue StandardError => e
|
596
|
+
# If recreation fails, keep the old chat but clear its messages
|
597
|
+
warn "Warning: Could not recreate chat for #{model_name}: #{e.message}. Clearing existing chat."
|
598
|
+
chat = old_chats[model_name]
|
599
|
+
if chat&.instance_variable_defined?(:@messages)
|
525
600
|
chat.instance_variable_set(:@messages, [])
|
526
601
|
end
|
602
|
+
chat.clear_history if chat&.respond_to?(:clear_history)
|
603
|
+
new_chats[model_name] = chat
|
527
604
|
end
|
528
605
|
end
|
529
606
|
|
530
|
-
|
531
|
-
|
532
|
-
chat.clear_history if chat.respond_to?(:clear_history)
|
533
|
-
end
|
534
|
-
|
535
|
-
# Final verification
|
536
|
-
@chats.each_value do |chat|
|
537
|
-
if chat.instance_variable_defined?(:@messages) && !chat.instance_variable_get(:@messages).empty?
|
538
|
-
chat.instance_variable_set(:@messages, [])
|
539
|
-
end
|
540
|
-
end
|
541
|
-
|
542
|
-
return 'Chat context successfully cleared.'
|
607
|
+
@chats = new_chats
|
608
|
+
'Chat context successfully cleared.'
|
543
609
|
rescue StandardError => e
|
544
|
-
|
610
|
+
"Error clearing chat context: #{e.message}"
|
545
611
|
end
|
546
612
|
|
547
613
|
|
@@ -624,16 +690,40 @@ module AIA
|
|
624
690
|
def extract_models_config
|
625
691
|
models_config = AIA.config.model
|
626
692
|
|
627
|
-
# Handle backward compatibility
|
693
|
+
# Handle backward compatibility
|
628
694
|
if models_config.is_a?(String)
|
629
|
-
|
695
|
+
# Old format: single string
|
696
|
+
[{model: models_config, role: nil, instance: 1, internal_id: models_config}]
|
630
697
|
elsif models_config.is_a?(Array)
|
631
|
-
models_config
|
698
|
+
if models_config.empty?
|
699
|
+
# Empty array - use default
|
700
|
+
[{model: 'gpt-4o-mini', role: nil, instance: 1, internal_id: 'gpt-4o-mini'}]
|
701
|
+
elsif models_config.first.is_a?(Hash)
|
702
|
+
# New format: array of hashes with model specs
|
703
|
+
models_config
|
704
|
+
else
|
705
|
+
# Old format: array of strings
|
706
|
+
models_config.map { |m| {model: m, role: nil, instance: 1, internal_id: m} }
|
707
|
+
end
|
632
708
|
else
|
633
|
-
|
709
|
+
# Fallback to default
|
710
|
+
[{model: 'gpt-4o-mini', role: nil, instance: 1, internal_id: 'gpt-4o-mini'}]
|
634
711
|
end
|
635
712
|
end
|
636
713
|
|
714
|
+
def extract_model_names(model_specs)
|
715
|
+
# Extract just the model names from the specs
|
716
|
+
# For models with instance > 1, use internal_id (e.g., "gpt-4o#2")
|
717
|
+
model_specs.map do |spec|
|
718
|
+
spec[:internal_id]
|
719
|
+
end
|
720
|
+
end
|
721
|
+
|
722
|
+
def get_model_spec(internal_id)
|
723
|
+
# Find the spec for a given internal_id
|
724
|
+
@model_specs.find { |spec| spec[:internal_id] == internal_id }
|
725
|
+
end
|
726
|
+
|
637
727
|
|
638
728
|
def extract_text_prompt(prompt)
|
639
729
|
if prompt.is_a?(String)
|