aia 0.8.5 → 0.9.0
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 +9 -1
- data/COMMITS.md +23 -0
- data/README.md +131 -163
- data/lib/aia/chat_processor_service.rb +14 -1
- data/lib/aia/config.rb +157 -10
- data/lib/aia/prompt_handler.rb +7 -2
- data/lib/aia/ruby_llm_adapter.rb +225 -0
- data/lib/aia/session.rb +127 -96
- data/lib/aia/ui_presenter.rb +10 -1
- data/lib/aia/utility.rb +1 -1
- data/lib/aia.rb +14 -3
- data/lib/extensions/ruby_llm/chat.rb +197 -0
- data/mcp_servers/README.md +90 -0
- data/mcp_servers/filesystem.json +9 -0
- data/mcp_servers/imcp.json +7 -0
- data/mcp_servers/launcher.json +11 -0
- data/mcp_servers/playwright_server_definition.json +9 -0
- data/mcp_servers/timeserver.json +8 -0
- metadata +33 -11
- data/lib/aia/ai_client_adapter.rb +0 -210
data/lib/aia/config.rb
CHANGED
@@ -9,6 +9,9 @@ require 'yaml'
|
|
9
9
|
require 'toml-rb'
|
10
10
|
require 'erb'
|
11
11
|
require 'optparse'
|
12
|
+
require 'json'
|
13
|
+
require 'tempfile'
|
14
|
+
require 'fileutils'
|
12
15
|
|
13
16
|
module AIA
|
14
17
|
class Config
|
@@ -26,10 +29,14 @@ module AIA
|
|
26
29
|
role: '',
|
27
30
|
system_prompt: '',
|
28
31
|
|
32
|
+
# MCP configuration
|
33
|
+
mcp_servers: [],
|
34
|
+
allowed_tools: nil, # nil means all tools are allowed; otherwise an Array of Strings which are the tool names
|
35
|
+
|
29
36
|
# Flags
|
30
37
|
markdown: true,
|
31
|
-
shell:
|
32
|
-
erb:
|
38
|
+
shell: true,
|
39
|
+
erb: true,
|
33
40
|
chat: false,
|
34
41
|
clear: false,
|
35
42
|
terse: false,
|
@@ -60,6 +67,7 @@ module AIA
|
|
60
67
|
speech_model: 'tts-1',
|
61
68
|
transcription_model: 'whisper-1',
|
62
69
|
voice: 'alloy',
|
70
|
+
adapter: 'ruby_llm', # 'ruby_llm' or ???
|
63
71
|
|
64
72
|
# Embedding parameters
|
65
73
|
embedding_model: 'text-embedding-ada-002',
|
@@ -192,6 +200,13 @@ module AIA
|
|
192
200
|
PromptManager::Prompt.parameter_regex = Regexp.new(config.parameter_regex)
|
193
201
|
end
|
194
202
|
|
203
|
+
debug_me{[ 'config.mcp_servers' ]}
|
204
|
+
|
205
|
+
unless config.mcp_servers.empty?
|
206
|
+
# create a single JSON file contain all of the MCP server definitions specified my the --mcp option
|
207
|
+
config.mcp_servers = combine_mcp_server_json_files config.mcp_servers
|
208
|
+
end
|
209
|
+
|
195
210
|
config
|
196
211
|
end
|
197
212
|
|
@@ -236,19 +251,23 @@ module AIA
|
|
236
251
|
puts "Debug: Setting chat mode to true" if config.debug
|
237
252
|
end
|
238
253
|
|
239
|
-
opts.on("
|
240
|
-
|
254
|
+
opts.on("--adapter ADAPTER", "Interface that adapts AIA to the LLM") do |adapter|
|
255
|
+
adapter.downcase!
|
256
|
+
valid_adapters = %w[ ruby_llm ] # NOTE: Add additional adapters here when needed
|
257
|
+
if valid_adapters.include? adapter
|
258
|
+
config.adapter = adapter
|
259
|
+
else
|
260
|
+
STDERR.puts "ERROR: Invalid adapter #{adapter} must be one of these: #{valid_adapters.join(', ')}"
|
261
|
+
exit 1
|
262
|
+
end
|
241
263
|
end
|
242
264
|
|
243
|
-
opts.on("--shell", "Enables `aia` to access your terminal's shell environment from inside the prompt text, allowing for dynamic content insertion using system environment variables and shell commands. Includes safety features to confirm or block dangerous commands.") do
|
244
|
-
config.shell = true
|
245
|
-
end
|
246
265
|
|
247
|
-
opts.on("
|
248
|
-
config.
|
266
|
+
opts.on("-m MODEL", "--model MODEL", "Name of the LLM model to use") do |model|
|
267
|
+
config.model = model
|
249
268
|
end
|
250
269
|
|
251
|
-
opts.on("--terse", "
|
270
|
+
opts.on("--terse", "Adds a special instruction to the prompt asking the AI to keep responses short and to the point") do
|
252
271
|
config.terse = true
|
253
272
|
end
|
254
273
|
|
@@ -415,6 +434,44 @@ module AIA
|
|
415
434
|
opts.on("--rq LIBS", "Ruby libraries to require for Ruby directive") do |libs|
|
416
435
|
config.require_libs = libs.split(',')
|
417
436
|
end
|
437
|
+
|
438
|
+
opts.on("--mcp FILE", "Add MCP server configuration from JSON file. Can be specified multiple times.") do |file|
|
439
|
+
# debug_me FIXME ruby-mcp-client is looking for a single JSON file that
|
440
|
+
# could contain multiple server definitions that looks like this:
|
441
|
+
# {
|
442
|
+
# "mcpServers": {
|
443
|
+
# "server one": { ... },
|
444
|
+
# "server two": { ... }, ....
|
445
|
+
# }
|
446
|
+
# }
|
447
|
+
# FIXME: need to rurn multiple JSON files into one.
|
448
|
+
if AIA.good_file?(file)
|
449
|
+
config.mcp_servers ||= []
|
450
|
+
config.mcp_servers << file
|
451
|
+
begin
|
452
|
+
server_config = JSON.parse(File.read(file))
|
453
|
+
config.mcp_servers_config ||= []
|
454
|
+
config.mcp_servers_config << server_config
|
455
|
+
rescue JSON::ParserError => e
|
456
|
+
STDERR.puts "Error parsing MCP server config file #{file}: #{e.message}"
|
457
|
+
exit 1
|
458
|
+
end
|
459
|
+
else
|
460
|
+
STDERR.puts "MCP server config file not found: #{file}"
|
461
|
+
exit 1
|
462
|
+
end
|
463
|
+
end
|
464
|
+
|
465
|
+
opts.on("--at", "--allowed_tools TOOLS_LIST", "Allow only these tools to be used") do |tools_list|
|
466
|
+
config.allowed_tools ||= []
|
467
|
+
if tools_list.empty?
|
468
|
+
STDERR.puts "No list of tool names provided for --allowed_tools option"
|
469
|
+
exit 1
|
470
|
+
else
|
471
|
+
config.allowed_tools += tools_list.split(',').map(&:strip)
|
472
|
+
config.allowed_tools.uniq!
|
473
|
+
end
|
474
|
+
end
|
418
475
|
end
|
419
476
|
|
420
477
|
args = ARGV.dup
|
@@ -500,5 +557,95 @@ module AIA
|
|
500
557
|
File.write(file, content)
|
501
558
|
puts "Config successfully dumped to #{file}"
|
502
559
|
end
|
560
|
+
|
561
|
+
|
562
|
+
# Combine multiple MCP server JSON files into a single file
|
563
|
+
def self.combine_mcp_server_json_files(file_paths)
|
564
|
+
raise ArgumentError, "No JSON files provided" if file_paths.nil? || file_paths.empty?
|
565
|
+
|
566
|
+
# The output will have only one top-level key: "mcpServers"
|
567
|
+
mcp_servers = {} # This will store all collected server_name => server_config pairs
|
568
|
+
|
569
|
+
file_paths.each do |file_path|
|
570
|
+
file_content = JSON.parse(File.read(file_path))
|
571
|
+
# Clean basename, e.g., "filesystem.json" -> "filesystem", "foo.json.erb" -> "foo"
|
572
|
+
cleaned_basename = File.basename(file_path).sub(/\.json\.erb$/, '').sub(/\.json$/, '')
|
573
|
+
|
574
|
+
if file_content.is_a?(Hash)
|
575
|
+
if file_content.key?("mcpServers") && file_content["mcpServers"].is_a?(Hash)
|
576
|
+
# Case A: {"mcpServers": {"name1": {...}, "name2": {...}}}
|
577
|
+
file_content["mcpServers"].each do |server_name, server_data|
|
578
|
+
if mcp_servers.key?(server_name)
|
579
|
+
STDERR.puts "Warning: Duplicate MCP server name '#{server_name}' found. Overwriting with definition from #{file_path}."
|
580
|
+
end
|
581
|
+
mcp_servers[server_name] = server_data
|
582
|
+
end
|
583
|
+
# Check if the root hash itself is a single server definition
|
584
|
+
elsif is_single_server_definition?(file_content)
|
585
|
+
# Case B: {"type": "stdio", ...} or {"url": "...", ...}
|
586
|
+
# Use "name" property from JSON if present, otherwise use cleaned_basename
|
587
|
+
server_name = file_content["name"] || cleaned_basename
|
588
|
+
if mcp_servers.key?(server_name)
|
589
|
+
STDERR.puts "Warning: Duplicate MCP server name '#{server_name}' (from file #{file_path}). Overwriting."
|
590
|
+
end
|
591
|
+
mcp_servers[server_name] = file_content
|
592
|
+
else
|
593
|
+
# Case D: Fallback for {"custom_name1": {server_config1}, "custom_name2": {server_config2}}
|
594
|
+
# This assumes top-level keys are server names and values are server configs.
|
595
|
+
file_content.each do |server_name, server_data|
|
596
|
+
if server_data.is_a?(Hash) && is_single_server_definition?(server_data)
|
597
|
+
if mcp_servers.key?(server_name)
|
598
|
+
STDERR.puts "Warning: Duplicate MCP server name '#{server_name}' found in #{file_path}. Overwriting."
|
599
|
+
end
|
600
|
+
mcp_servers[server_name] = server_data
|
601
|
+
else
|
602
|
+
STDERR.puts "Warning: Unrecognized structure for key '#{server_name}' in #{file_path}. Value is not a valid server definition. Skipping."
|
603
|
+
end
|
604
|
+
end
|
605
|
+
end
|
606
|
+
elsif file_content.is_a?(Array)
|
607
|
+
# Case C: [ {server_config1}, {server_config2_with_name} ]
|
608
|
+
file_content.each_with_index do |server_data, index|
|
609
|
+
if server_data.is_a?(Hash) && is_single_server_definition?(server_data)
|
610
|
+
# Use "name" property from JSON if present, otherwise generate one
|
611
|
+
server_name = server_data["name"] || "#{cleaned_basename}_#{index}"
|
612
|
+
if mcp_servers.key?(server_name)
|
613
|
+
STDERR.puts "Warning: Duplicate MCP server name '#{server_name}' (from array in #{file_path}). Overwriting."
|
614
|
+
end
|
615
|
+
mcp_servers[server_name] = server_data
|
616
|
+
else
|
617
|
+
STDERR.puts "Warning: Unrecognized item in array in #{file_path} at index #{index}. Skipping."
|
618
|
+
end
|
619
|
+
end
|
620
|
+
else
|
621
|
+
STDERR.puts "Warning: Unrecognized JSON structure in #{file_path}. Skipping."
|
622
|
+
end
|
623
|
+
end
|
624
|
+
|
625
|
+
# Create the final output structure
|
626
|
+
output = {"mcpServers" => mcp_servers}
|
627
|
+
temp_file = Tempfile.new(['combined', '.json'])
|
628
|
+
temp_file.write(JSON.pretty_generate(output))
|
629
|
+
temp_file.close
|
630
|
+
|
631
|
+
temp_file.path
|
632
|
+
end
|
633
|
+
|
634
|
+
# Helper method to determine if a hash represents a valid MCP server definition
|
635
|
+
def self.is_single_server_definition?(config)
|
636
|
+
return false unless config.is_a?(Hash)
|
637
|
+
type = config['type']
|
638
|
+
if type
|
639
|
+
return true if type == 'stdio' && config.key?('command')
|
640
|
+
return true if type == 'sse' && config.key?('url')
|
641
|
+
# Potentially other explicit types if they exist in MCP
|
642
|
+
return false # Known type but missing required fields for it, or unknown type
|
643
|
+
else
|
644
|
+
# Infer type
|
645
|
+
return true if config.key?('command') || config.key?('args') || config.key?('env') # stdio
|
646
|
+
return true if config.key?('url') # sse
|
647
|
+
end
|
648
|
+
false
|
649
|
+
end
|
503
650
|
end
|
504
651
|
end
|
data/lib/aia/prompt_handler.rb
CHANGED
@@ -49,6 +49,7 @@ module AIA
|
|
49
49
|
envar_flag: AIA.config.shell
|
50
50
|
)
|
51
51
|
|
52
|
+
# Parameters should be extracted during initialization or to_s
|
52
53
|
return prompt if prompt
|
53
54
|
else
|
54
55
|
puts "Warning: Invalid prompt ID or file not found: #{prompt_id}"
|
@@ -140,8 +141,11 @@ module AIA
|
|
140
141
|
end
|
141
142
|
|
142
143
|
|
144
|
+
# FIXME: original implementation used a search_proc to look into the content of the prompt
|
145
|
+
# files. The use of the select statement does not work.
|
143
146
|
def search_prompt_id_with_fzf(initial_query)
|
144
|
-
prompt_files = Dir.glob(File.join(@prompts_dir, "*.txt"))
|
147
|
+
prompt_files = Dir.glob(File.join(@prompts_dir, "*.txt"))
|
148
|
+
.map { |file| File.basename(file, ".txt") }
|
145
149
|
fzf = AIA::Fzf.new(
|
146
150
|
list: prompt_files,
|
147
151
|
directory: @prompts_dir,
|
@@ -153,7 +157,8 @@ module AIA
|
|
153
157
|
end
|
154
158
|
|
155
159
|
def search_role_id_with_fzf(initial_query)
|
156
|
-
role_files = Dir.glob(File.join(@roles_dir, "*.txt"))
|
160
|
+
role_files = Dir.glob(File.join(@roles_dir, "*.txt"))
|
161
|
+
.map { |file| File.basename(file, ".txt") }
|
157
162
|
fzf = AIA::Fzf.new(
|
158
163
|
list: role_files,
|
159
164
|
directory: @prompts_dir,
|
@@ -0,0 +1,225 @@
|
|
1
|
+
# lib/aia/ruby_llm_adapter.rb
|
2
|
+
|
3
|
+
require 'ruby_llm'
|
4
|
+
require 'mcp_client'
|
5
|
+
|
6
|
+
module AIA
|
7
|
+
class RubyLLMAdapter
|
8
|
+
def initialize
|
9
|
+
|
10
|
+
debug_me('=== RubyLLMAdapter ===')
|
11
|
+
|
12
|
+
@model = AIA.config.model
|
13
|
+
model_info = extract_model_parts(@model)
|
14
|
+
|
15
|
+
# Configure RubyLLM with available API keys
|
16
|
+
RubyLLM.configure do |config|
|
17
|
+
config.openai_api_key = ENV.fetch('OPENAI_API_KEY', nil)
|
18
|
+
config.anthropic_api_key = ENV.fetch('ANTHROPIC_API_KEY', nil)
|
19
|
+
config.gemini_api_key = ENV.fetch('GEMINI_API_KEY', nil)
|
20
|
+
config.deepseek_api_key = ENV.fetch('DEEPSEEK_API_KEY', nil)
|
21
|
+
|
22
|
+
# Bedrock configuration
|
23
|
+
config.bedrock_api_key = ENV.fetch('AWS_ACCESS_KEY_ID', nil)
|
24
|
+
config.bedrock_secret_key = ENV.fetch('AWS_SECRET_ACCESS_KEY', nil)
|
25
|
+
config.bedrock_region = ENV.fetch('AWS_REGION', nil)
|
26
|
+
config.bedrock_session_token = ENV.fetch('AWS_SESSION_TOKEN', nil)
|
27
|
+
end
|
28
|
+
|
29
|
+
debug_me{[ :model_info ]}
|
30
|
+
|
31
|
+
mcp_client, mcp_tools = generate_mcp_tools(model_info[:provider])
|
32
|
+
|
33
|
+
debug_me{[ :mcp_tools ]}
|
34
|
+
|
35
|
+
if mcp_tools && !mcp_tools.empty?
|
36
|
+
RubyLLM::Chat.with_mcp(client: mcp_client, call_tool_method: :call_tool, tools: mcp_tools)
|
37
|
+
end
|
38
|
+
|
39
|
+
@chat = RubyLLM.chat(model: model_info[:model])
|
40
|
+
end
|
41
|
+
|
42
|
+
def chat(prompt)
|
43
|
+
if @model.downcase.include?('dall-e') || @model.downcase.include?('image-generation')
|
44
|
+
text_to_image(prompt)
|
45
|
+
elsif @model.downcase.include?('vision') || @model.downcase.include?('image')
|
46
|
+
image_to_text(prompt)
|
47
|
+
elsif @model.downcase.include?('tts') || @model.downcase.include?('speech')
|
48
|
+
text_to_audio(prompt)
|
49
|
+
elsif @model.downcase.include?('whisper') || @model.downcase.include?('transcription')
|
50
|
+
audio_to_text(prompt)
|
51
|
+
else
|
52
|
+
text_to_text(prompt)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def transcribe(audio_file)
|
57
|
+
@chat.ask("Transcribe this audio", with: { audio: audio_file })
|
58
|
+
end
|
59
|
+
|
60
|
+
def speak(text)
|
61
|
+
output_file = "#{Time.now.to_i}.mp3"
|
62
|
+
|
63
|
+
# Note: RubyLLM doesn't have a direct text-to-speech feature
|
64
|
+
# This is a placeholder for a custom implementation or external service
|
65
|
+
begin
|
66
|
+
# Try using a TTS API if available
|
67
|
+
# For now, we'll use a mock implementation
|
68
|
+
File.write(output_file, "Mock TTS audio content")
|
69
|
+
system("#{AIA.config.speak_command} #{output_file}") if File.exist?(output_file) && system("which #{AIA.config.speak_command} > /dev/null 2>&1")
|
70
|
+
"Audio generated and saved to: #{output_file}"
|
71
|
+
rescue => e
|
72
|
+
"Error generating audio: #{e.message}"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def method_missing(method, *args, &block)
|
77
|
+
debug_me(tag: '== missing ==', levels: 25){[ :method, :args ]}
|
78
|
+
if @chat.respond_to?(method)
|
79
|
+
@chat.public_send(method, *args, &block)
|
80
|
+
else
|
81
|
+
super
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def respond_to_missing?(method, include_private = false)
|
86
|
+
@chat.respond_to?(method) || super
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
# Generate an array of MCP tools, filtered and formatted for the correct provider.
|
92
|
+
# @param config [OpenStruct] the config object containing mcp_servers, allowed_tools, and model
|
93
|
+
# @return [Array<Hash>, nil] the filtered and formatted MCP tools or nil if no tools
|
94
|
+
def generate_mcp_tools(provider)
|
95
|
+
return [nil, nil] unless AIA.config.mcp_servers && !AIA.config.mcp_servers.empty?
|
96
|
+
|
97
|
+
debug_me('=== generate_mcp_tools ===')
|
98
|
+
|
99
|
+
# AIA.config.mcp_servers is now a path to the combined JSON file
|
100
|
+
mcp_client = MCPClient.create_client(server_definition_file: AIA.config.mcp_servers)
|
101
|
+
debug_me
|
102
|
+
all_tools = mcp_client.list_tools(cache: false).map(&:name)
|
103
|
+
debug_me
|
104
|
+
allowed = AIA.config.allowed_tools
|
105
|
+
debug_me
|
106
|
+
filtered_tools = allowed.nil? ? all_tools : all_tools & allowed
|
107
|
+
debug_me{[ :filtered_tools ]}
|
108
|
+
|
109
|
+
debug_me{[ :provider ]}
|
110
|
+
|
111
|
+
mcp_tools = if :anthropic == provider.to_sym
|
112
|
+
debug_me
|
113
|
+
mcp_client.to_anthropic_tools(tool_names: filtered_tools)
|
114
|
+
else
|
115
|
+
debug_me
|
116
|
+
mcp_client.to_openai_tools(tool_names: filtered_tools)
|
117
|
+
end
|
118
|
+
[mcp_client, mcp_tools]
|
119
|
+
rescue => e
|
120
|
+
STDERR.puts "ERROR: Failed to generate MCP tools: #{e.message}"
|
121
|
+
nil
|
122
|
+
end
|
123
|
+
|
124
|
+
def extract_model_parts(model_string)
|
125
|
+
parts = model_string.split('/')
|
126
|
+
parts.map!(&:strip)
|
127
|
+
|
128
|
+
if parts.length > 1
|
129
|
+
provider = parts[0]
|
130
|
+
model = parts[1]
|
131
|
+
else
|
132
|
+
provider = nil # RubyLLM will figure it out from the model name
|
133
|
+
model = parts[0]
|
134
|
+
end
|
135
|
+
|
136
|
+
{ provider: provider, model: model }
|
137
|
+
end
|
138
|
+
|
139
|
+
def extract_text_prompt(prompt)
|
140
|
+
if prompt.is_a?(String)
|
141
|
+
prompt
|
142
|
+
elsif prompt.is_a?(Hash) && prompt[:text]
|
143
|
+
prompt[:text]
|
144
|
+
elsif prompt.is_a?(Hash) && prompt[:content]
|
145
|
+
prompt[:content]
|
146
|
+
else
|
147
|
+
prompt.to_s
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def text_to_text(prompt)
|
152
|
+
text_prompt = extract_text_prompt(prompt)
|
153
|
+
@chat.ask(text_prompt)
|
154
|
+
end
|
155
|
+
|
156
|
+
def text_to_image(prompt)
|
157
|
+
text_prompt = extract_text_prompt(prompt)
|
158
|
+
output_file = "#{Time.now.to_i}.png"
|
159
|
+
|
160
|
+
begin
|
161
|
+
RubyLLM.paint(text_prompt, output_path: output_file,
|
162
|
+
size: AIA.config.image_size,
|
163
|
+
quality: AIA.config.image_quality,
|
164
|
+
style: AIA.config.image_style)
|
165
|
+
"Image generated and saved to: #{output_file}"
|
166
|
+
rescue => e
|
167
|
+
"Error generating image: #{e.message}"
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
def image_to_text(prompt)
|
172
|
+
image_path = extract_image_path(prompt)
|
173
|
+
text_prompt = extract_text_prompt(prompt)
|
174
|
+
|
175
|
+
if image_path && File.exist?(image_path)
|
176
|
+
begin
|
177
|
+
@chat.ask(text_prompt, with: { image: image_path })
|
178
|
+
rescue => e
|
179
|
+
"Error analyzing image: #{e.message}"
|
180
|
+
end
|
181
|
+
else
|
182
|
+
text_to_text(prompt)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
def text_to_audio(prompt)
|
187
|
+
text_prompt = extract_text_prompt(prompt)
|
188
|
+
output_file = "#{Time.now.to_i}.mp3"
|
189
|
+
|
190
|
+
begin
|
191
|
+
# Note: RubyLLM doesn't have a direct TTS feature
|
192
|
+
# This is a placeholder for a custom implementation
|
193
|
+
File.write(output_file, text_prompt)
|
194
|
+
system("#{AIA.config.speak_command} #{output_file}") if File.exist?(output_file) && system("which #{AIA.config.speak_command} > /dev/null 2>&1")
|
195
|
+
"Audio generated and saved to: #{output_file}"
|
196
|
+
rescue => e
|
197
|
+
"Error generating audio: #{e.message}"
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
def audio_to_text(prompt)
|
202
|
+
if prompt.is_a?(String) && File.exist?(prompt) &&
|
203
|
+
prompt.downcase.end_with?('.mp3', '.wav', '.m4a', '.flac')
|
204
|
+
begin
|
205
|
+
@chat.ask("Transcribe this audio", with: { audio: prompt })
|
206
|
+
rescue => e
|
207
|
+
"Error transcribing audio: #{e.message}"
|
208
|
+
end
|
209
|
+
else
|
210
|
+
# Fall back to regular chat if no valid audio file is found
|
211
|
+
text_to_text(prompt)
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
def extract_image_path(prompt)
|
216
|
+
if prompt.is_a?(String)
|
217
|
+
prompt.scan(/\b[\w\/\.\-]+\.(jpg|jpeg|png|gif|webp)\b/i).first&.first
|
218
|
+
elsif prompt.is_a?(Hash)
|
219
|
+
prompt[:image] || prompt[:image_path]
|
220
|
+
else
|
221
|
+
nil
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|