smart_prompt 0.4.4 → 0.5.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/CHANGELOG.md +10 -10
- data/README.cn.md +307 -64
- data/README.md +311 -64
- data/Rakefile +10 -1
- data/config/anthropic_config.yml +151 -0
- data/config/image_generation_config.yml +22 -0
- data/config/multimodal_config.yml +85 -0
- data/config/sensenova_config.yml +63 -0
- data/config/zhipu_config.yml +73 -0
- data/examples/anthropic_basic_chat.rb +143 -0
- data/examples/anthropic_example.rb +232 -0
- data/examples/anthropic_multimodal.rb +212 -0
- data/examples/anthropic_streaming.rb +312 -0
- data/examples/anthropic_tool_calling.rb +393 -0
- data/examples/automatic_cleanup_example.rb +109 -0
- data/examples/history_management_examples.rb +522 -0
- data/examples/image_generation_example.rb +130 -0
- data/examples/monitoring_example.rb +121 -0
- data/examples/multimodal_example.rb +63 -0
- data/examples/relevance_based_strategy_example.rb +87 -0
- data/examples/sensenova_example.rb +129 -0
- data/examples/stt_example.rb +287 -0
- data/examples/tts_example.rb +244 -0
- data/examples/video_generation_example.rb +189 -0
- data/examples/zhipu_example.rb +151 -0
- data/lib/smart_prompt/anthropic_adapter.rb +363 -281
- data/lib/smart_prompt/compression_engine.rb +201 -0
- data/lib/smart_prompt/context_strategy.rb +22 -0
- data/lib/smart_prompt/conversation.rb +81 -191
- data/lib/smart_prompt/engine.rb +36 -19
- data/lib/smart_prompt/history_manager.rb +596 -0
- data/lib/smart_prompt/hybrid_strategy.rb +222 -0
- data/lib/smart_prompt/image_generation_adapter.rb +297 -0
- data/lib/smart_prompt/lru_cache.rb +133 -0
- data/lib/smart_prompt/message.rb +57 -0
- data/lib/smart_prompt/multimodal_adapter.rb +277 -0
- data/lib/smart_prompt/openai_adapter.rb +1 -25
- data/lib/smart_prompt/persistence_layer.rb +197 -0
- data/lib/smart_prompt/relevance_based_strategy.rb +221 -0
- data/lib/smart_prompt/sensenova_adapter.rb +410 -0
- data/lib/smart_prompt/session.rb +140 -0
- data/lib/smart_prompt/sliding_window_strategy.rb +100 -0
- data/lib/smart_prompt/stt_adapter.rb +381 -0
- data/lib/smart_prompt/summary_based_strategy.rb +152 -0
- data/lib/smart_prompt/token_counter.rb +74 -0
- data/lib/smart_prompt/tts_adapter.rb +403 -0
- data/lib/smart_prompt/version.rb +1 -1
- data/lib/smart_prompt/video_generation_adapter.rb +330 -0
- data/lib/smart_prompt/worker.rb +25 -3
- data/lib/smart_prompt/zhipu_adapter.rb +616 -0
- data/lib/smart_prompt.rb +22 -2
- data/workers/history_management_examples.rb +407 -0
- data/workers/image_generation_workers.rb +119 -0
- data/workers/multimodal_workers.rb +110 -0
- data/workers/sensenova_workers.rb +62 -0
- data/workers/stt_workers.rb +195 -0
- data/workers/tts_workers.rb +388 -0
- data/workers/video_generation_workers.rb +264 -0
- data/workers/zhipu_workers.rb +113 -0
- metadata +84 -8
|
@@ -1,298 +1,380 @@
|
|
|
1
|
-
require "
|
|
2
|
-
require "
|
|
1
|
+
require "anthropic"
|
|
2
|
+
require "base64"
|
|
3
3
|
require "uri"
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module SmartPrompt
|
|
7
|
+
class AnthropicAdapter < LLMAdapter
|
|
8
|
+
def initialize(config)
|
|
9
|
+
super
|
|
10
|
+
SmartPrompt.logger.info "Start create the SmartPrompt AnthropicAdapter."
|
|
11
|
+
|
|
12
|
+
# Parse API key (support environment variable reference)
|
|
13
|
+
api_key = @config["api_key"]
|
|
14
|
+
if api_key.is_a?(String) && api_key.start_with?("ENV[") && api_key.end_with?("]")
|
|
15
|
+
api_key = eval(api_key)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Determine base_url with priority: config['url'] > ENV['ANTHROPIC_BASE_URL'] > default
|
|
19
|
+
base_url = @config["url"] || ENV["ANTHROPIC_BASE_URL"]
|
|
20
|
+
|
|
21
|
+
begin
|
|
22
|
+
# Create Anthropic::Client instance
|
|
23
|
+
client_options = { api_key: api_key }
|
|
24
|
+
client_options[:base_url] = base_url if base_url
|
|
25
|
+
|
|
26
|
+
@client = Anthropic::Client.new(**client_options)
|
|
27
|
+
SmartPrompt.logger.info "Successful creation an Anthropic client."
|
|
28
|
+
rescue => e
|
|
29
|
+
SmartPrompt.logger.error "Failed to initialize Anthropic client: #{e.message}"
|
|
30
|
+
raise LLMAPIError, "Invalid Anthropic configuration: #{e.message}"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
# Extract system message from messages array
|
|
37
|
+
# @param messages [Array] Array of message hashes
|
|
38
|
+
# @return [String, nil] System message content or nil if not found
|
|
39
|
+
def extract_system_message(messages)
|
|
40
|
+
system_msg = messages.find { |msg| msg[:role] == "system" || msg["role"] == "system" }
|
|
41
|
+
system_msg ? (system_msg[:content] || system_msg["content"]) : nil
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Convert SmartPrompt message format to Anthropic format
|
|
45
|
+
# @param messages [Array] Array of message hashes in SmartPrompt format
|
|
46
|
+
# @return [Array] Array of message hashes in Anthropic format
|
|
47
|
+
def convert_messages_to_anthropic_format(messages)
|
|
48
|
+
messages.reject { |msg|
|
|
49
|
+
role = msg[:role] || msg["role"]
|
|
50
|
+
role == "system"
|
|
51
|
+
}.map do |msg|
|
|
52
|
+
role = msg[:role] || msg["role"]
|
|
53
|
+
content = msg[:content] || msg["content"]
|
|
54
|
+
|
|
55
|
+
# Convert content based on its type
|
|
56
|
+
converted_content = if content.is_a?(String)
|
|
57
|
+
# String content: convert to hash format
|
|
58
|
+
{ type: "text", text: content }
|
|
59
|
+
elsif content.is_a?(Array)
|
|
60
|
+
# Array content: process each item
|
|
61
|
+
content.map do |item|
|
|
62
|
+
item_type = item[:type] || item["type"]
|
|
63
|
+
|
|
64
|
+
if item_type == "text"
|
|
65
|
+
# Keep text items as-is
|
|
66
|
+
item
|
|
67
|
+
elsif item_type == "image_url"
|
|
68
|
+
# Convert image_url to Anthropic format
|
|
69
|
+
image_url = item[:image_url] || item["image_url"]
|
|
70
|
+
url = image_url.is_a?(Hash) ? (image_url[:url] || image_url["url"]) : image_url
|
|
71
|
+
prepare_image_content(url)
|
|
72
|
+
else
|
|
73
|
+
# Keep other types as-is
|
|
74
|
+
item
|
|
75
|
+
end
|
|
76
|
+
end.compact # Remove nil values from failed image conversions
|
|
77
|
+
else
|
|
78
|
+
content
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
{ role: role, content: [converted_content] }
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Prepare image content for Anthropic API
|
|
86
|
+
# @param image_url [String] Image URL (HTTP/HTTPS or data URL)
|
|
87
|
+
# @return [Hash, nil] Anthropic format image content or nil if invalid
|
|
88
|
+
def prepare_image_content(image_url)
|
|
89
|
+
return nil unless image_url.is_a?(String)
|
|
90
|
+
|
|
91
|
+
if image_url.start_with?("http://", "https://")
|
|
92
|
+
# HTTP/HTTPS URL format
|
|
93
|
+
{
|
|
94
|
+
type: "image",
|
|
95
|
+
source: {
|
|
96
|
+
type: "url",
|
|
97
|
+
url: image_url,
|
|
98
|
+
},
|
|
99
|
+
}
|
|
100
|
+
elsif image_url.start_with?("data:")
|
|
101
|
+
# Data URL format: data:image/jpeg;base64,<base64_data>
|
|
102
|
+
match = image_url.match(/^data:(image\/[^;]+);base64,(.+)$/)
|
|
103
|
+
|
|
104
|
+
if match
|
|
105
|
+
media_type = match[1]
|
|
106
|
+
base64_data = match[2]
|
|
107
|
+
|
|
108
|
+
{
|
|
109
|
+
type: "image",
|
|
110
|
+
source: {
|
|
111
|
+
type: "base64",
|
|
112
|
+
media_type: media_type,
|
|
113
|
+
data: base64_data,
|
|
114
|
+
},
|
|
115
|
+
}
|
|
116
|
+
else
|
|
117
|
+
SmartPrompt.logger.warn "Invalid image URL format: #{image_url}"
|
|
118
|
+
nil
|
|
119
|
+
end
|
|
120
|
+
else
|
|
121
|
+
SmartPrompt.logger.warn "Invalid image URL format: #{image_url}"
|
|
122
|
+
nil
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Convert OpenAI format tools to Anthropic format
|
|
127
|
+
# @param tools [Array, nil] Array of tool definitions in OpenAI format
|
|
128
|
+
# @return [Array, nil] Array of tool definitions in Anthropic format or nil
|
|
129
|
+
def convert_tools_to_anthropic_format(tools)
|
|
130
|
+
# Handle nil or empty array
|
|
131
|
+
return nil if tools.nil? || tools.empty?
|
|
132
|
+
|
|
133
|
+
# Convert each tool definition
|
|
134
|
+
tools.map do |tool|
|
|
135
|
+
# Extract function field
|
|
136
|
+
function = tool[:function] || tool["function"]
|
|
137
|
+
next nil unless function
|
|
138
|
+
|
|
139
|
+
# Extract name, description, and parameters
|
|
140
|
+
name = function[:name] || function["name"]
|
|
141
|
+
description = function[:description] || function["description"]
|
|
142
|
+
parameters = function[:parameters] || function["parameters"]
|
|
143
|
+
|
|
144
|
+
# Build Anthropic format tool definition
|
|
145
|
+
{
|
|
146
|
+
name: name,
|
|
147
|
+
description: description,
|
|
148
|
+
input_schema: parameters,
|
|
149
|
+
}
|
|
150
|
+
end.compact # Remove nil values from failed conversions
|
|
83
151
|
end
|
|
84
152
|
|
|
85
|
-
def
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
153
|
+
def convert_response_to_openai_format(response)
|
|
154
|
+
begin
|
|
155
|
+
# Normalize response to a Hash with symbol keys
|
|
156
|
+
raw_response = if response.respond_to?(:to_h)
|
|
157
|
+
response.to_h
|
|
158
|
+
elsif response.is_a?(Hash)
|
|
159
|
+
response
|
|
160
|
+
else
|
|
161
|
+
JSON.parse(response.to_json)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
response_hash = deep_symbolize(raw_response)
|
|
165
|
+
|
|
166
|
+
# Handle content blocks (text, tool_use, etc.)
|
|
167
|
+
content_blocks = response_hash[:content] || []
|
|
168
|
+
text_content = ""
|
|
169
|
+
tool_calls = []
|
|
170
|
+
|
|
171
|
+
case content_blocks
|
|
172
|
+
when String
|
|
173
|
+
text_content = content_blocks
|
|
174
|
+
when Array
|
|
175
|
+
content_blocks.each do |block|
|
|
176
|
+
block_hash = block.respond_to?(:to_h) ? block.to_h : block
|
|
177
|
+
block_hash = deep_symbolize(block_hash)
|
|
178
|
+
next unless block_hash.is_a?(Hash)
|
|
179
|
+
|
|
180
|
+
case block_hash[:type]
|
|
181
|
+
when "text"
|
|
182
|
+
text_content << block_hash[:text].to_s
|
|
183
|
+
when "tool_use"
|
|
184
|
+
tool_calls << {
|
|
185
|
+
"index" => tool_calls.size,
|
|
186
|
+
"id" => block_hash[:id] || "tool_call_#{tool_calls.size}",
|
|
187
|
+
"type" => "function",
|
|
188
|
+
"function" => {
|
|
189
|
+
"name" => block_hash[:name],
|
|
190
|
+
"arguments" => JSON.generate(block_hash[:input] || {}),
|
|
191
|
+
},
|
|
192
|
+
}
|
|
193
|
+
end
|
|
194
|
+
end
|
|
106
195
|
else
|
|
107
|
-
|
|
108
|
-
role: "user",
|
|
109
|
-
content: normalize_content(content),
|
|
110
|
-
}
|
|
196
|
+
text_content = content_blocks.to_s
|
|
111
197
|
end
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
[anthropic_messages, system_messages.join("\n\n")]
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
def normalize_content(content)
|
|
118
|
-
return content if content.is_a?(Array)
|
|
119
198
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
199
|
+
# Map stop reason to OpenAI finish_reason semantics
|
|
200
|
+
stop_reason = response_hash[:stop_reason] || response_hash[:finish_reason]
|
|
201
|
+
finish_reason = case stop_reason
|
|
202
|
+
when "tool_use"
|
|
203
|
+
"tool_calls"
|
|
204
|
+
when "end_turn", nil
|
|
205
|
+
"stop"
|
|
206
|
+
else
|
|
207
|
+
stop_reason
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Map usage information
|
|
211
|
+
usage = response_hash[:usage] || {}
|
|
212
|
+
prompt_tokens = usage[:input_tokens]
|
|
213
|
+
completion_tokens = usage[:output_tokens]
|
|
214
|
+
cache_read_tokens = usage[:cache_read_input_tokens]
|
|
215
|
+
cache_creation_tokens = usage[:cache_creation_input_tokens]
|
|
216
|
+
total_tokens = if prompt_tokens || completion_tokens
|
|
217
|
+
[prompt_tokens, completion_tokens].compact.sum
|
|
218
|
+
end
|
|
219
|
+
prompt_cache_hit_tokens = cache_read_tokens
|
|
220
|
+
prompt_cache_miss_tokens = if prompt_tokens && cache_read_tokens
|
|
221
|
+
prompt_tokens - cache_read_tokens
|
|
222
|
+
end
|
|
223
|
+
prompt_tokens_details = {}
|
|
224
|
+
prompt_tokens_details["cached_tokens"] = cache_read_tokens if cache_read_tokens
|
|
225
|
+
|
|
226
|
+
usage_hash = {}
|
|
227
|
+
usage_hash["prompt_tokens"] = prompt_tokens if prompt_tokens
|
|
228
|
+
usage_hash["completion_tokens"] = completion_tokens if completion_tokens
|
|
229
|
+
usage_hash["total_tokens"] = total_tokens if total_tokens
|
|
230
|
+
usage_hash["prompt_tokens_details"] = prompt_tokens_details unless prompt_tokens_details.empty?
|
|
231
|
+
usage_hash["prompt_cache_hit_tokens"] = prompt_cache_hit_tokens if prompt_cache_hit_tokens
|
|
232
|
+
usage_hash["prompt_cache_miss_tokens"] = prompt_cache_miss_tokens if prompt_cache_miss_tokens
|
|
233
|
+
|
|
234
|
+
created_ts = response_hash[:created_at] || response_hash[:created] || Time.now.to_i
|
|
235
|
+
|
|
236
|
+
message_role = response_hash[:role] || "assistant"
|
|
237
|
+
|
|
238
|
+
openai_response = {
|
|
239
|
+
"id" => response_hash[:id],
|
|
240
|
+
"object" => "chat.completion",
|
|
241
|
+
"created" => created_ts,
|
|
242
|
+
"model" => response_hash[:model],
|
|
243
|
+
"choices" => [
|
|
244
|
+
{
|
|
245
|
+
"index" => 0,
|
|
246
|
+
"message" => {
|
|
247
|
+
"role" => message_role,
|
|
248
|
+
"content" => text_content.empty? ? nil : text_content,
|
|
249
|
+
},
|
|
250
|
+
"finish_reason" => finish_reason,
|
|
251
|
+
},
|
|
252
|
+
],
|
|
141
253
|
}
|
|
142
|
-
end
|
|
143
|
-
end
|
|
144
254
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
http.use_ssl = @messages_uri.scheme == "https"
|
|
148
|
-
http.read_timeout = @request_timeout
|
|
149
|
-
http.open_timeout = @request_timeout
|
|
150
|
-
|
|
151
|
-
request = Net::HTTP::Post.new(@messages_uri)
|
|
152
|
-
request["Content-Type"] = "application/json"
|
|
153
|
-
request["x-api-key"] = @api_key
|
|
154
|
-
request["anthropic-version"] = @anthropic_version
|
|
155
|
-
request.body = JSON.generate(parameters)
|
|
156
|
-
|
|
157
|
-
if stream_proc
|
|
158
|
-
handle_streaming_response(http, request, stream_proc)
|
|
159
|
-
else
|
|
160
|
-
handle_response(http.request(request))
|
|
161
|
-
end
|
|
162
|
-
rescue SocketError => e
|
|
163
|
-
SmartPrompt.logger.error "Failed to connect to Anthropic API: #{e.message}"
|
|
164
|
-
raise LLMAPIError, "Network error: Unable to connect to Anthropic API"
|
|
165
|
-
rescue Net::OpenTimeout, Net::ReadTimeout
|
|
166
|
-
SmartPrompt.logger.error "Request to Anthropic API timed out"
|
|
167
|
-
raise LLMAPIError, "Request to Anthropic API timed out"
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
def handle_response(response)
|
|
171
|
-
body = JSON.parse(response.body)
|
|
172
|
-
return body if response.is_a?(Net::HTTPSuccess)
|
|
173
|
-
|
|
174
|
-
message = body.dig("error", "message") || response.message
|
|
175
|
-
SmartPrompt.logger.error "Anthropic API error: #{message}"
|
|
176
|
-
raise LLMAPIError, "Anthropic API error: #{message}"
|
|
177
|
-
end
|
|
178
|
-
|
|
179
|
-
def handle_streaming_response(http, request, stream_proc)
|
|
180
|
-
accumulated_response = nil
|
|
181
|
-
|
|
182
|
-
http.request(request) do |response|
|
|
183
|
-
unless response.is_a?(Net::HTTPSuccess)
|
|
184
|
-
body = response.body.to_s.empty? ? {} : JSON.parse(response.body)
|
|
185
|
-
message = body.dig("error", "message") || response.message
|
|
186
|
-
SmartPrompt.logger.error "Anthropic API error: #{message}"
|
|
187
|
-
raise LLMAPIError, "Anthropic API error: #{message}"
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
response.read_body do |chunk|
|
|
191
|
-
chunk.each_line do |line|
|
|
192
|
-
next unless line.start_with?("data:")
|
|
193
|
-
|
|
194
|
-
data = line.delete_prefix("data:").strip
|
|
195
|
-
next if data.empty?
|
|
196
|
-
|
|
197
|
-
event = JSON.parse(data)
|
|
198
|
-
accumulated_response = event if event["type"] == "message_start"
|
|
199
|
-
stream_proc.call(openai_stream_chunk(event), chunk.bytesize)
|
|
200
|
-
end
|
|
255
|
+
unless tool_calls.empty?
|
|
256
|
+
openai_response["choices"][0]["message"]["tool_calls"] = tool_calls
|
|
201
257
|
end
|
|
202
|
-
end
|
|
203
258
|
|
|
204
|
-
|
|
205
|
-
|
|
259
|
+
openai_response["usage"] = usage_hash unless usage_hash.empty?
|
|
260
|
+
openai_response["system_fingerprint"] = response_hash[:system_fingerprint] if response_hash[:system_fingerprint]
|
|
206
261
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
{
|
|
212
|
-
"id" => message["id"],
|
|
213
|
-
"object" => "chat.completion.chunk",
|
|
214
|
-
"created" => Time.now.to_i,
|
|
215
|
-
"model" => message["model"],
|
|
216
|
-
"choices" => [{
|
|
217
|
-
"index" => 0,
|
|
218
|
-
"delta" => {},
|
|
219
|
-
}],
|
|
220
|
-
"usage" => message["usage"],
|
|
221
|
-
}
|
|
222
|
-
when "content_block_delta"
|
|
223
|
-
{
|
|
224
|
-
"choices" => [{
|
|
225
|
-
"index" => 0,
|
|
226
|
-
"delta" => {
|
|
227
|
-
"content" => event.dig("delta", "text").to_s,
|
|
228
|
-
},
|
|
229
|
-
}],
|
|
230
|
-
}
|
|
231
|
-
else
|
|
232
|
-
{
|
|
233
|
-
"choices" => [{
|
|
234
|
-
"index" => 0,
|
|
235
|
-
"delta" => {},
|
|
236
|
-
}],
|
|
237
|
-
}
|
|
262
|
+
@last_response = openai_response
|
|
263
|
+
openai_response
|
|
264
|
+
rescue => e
|
|
265
|
+
SmartPrompt.logger.error "Failed to convert Anthropic response: #{e.message}"
|
|
266
|
+
raise LLMAPIError, "Failed to convert Anthropic response: #{e.message}"
|
|
238
267
|
end
|
|
239
268
|
end
|
|
240
269
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
text_parts << block["text"].to_s
|
|
249
|
-
when "tool_use"
|
|
250
|
-
tool_calls << openai_tool_call(block)
|
|
251
|
-
else
|
|
252
|
-
text_parts << block.to_s
|
|
270
|
+
# Deeply symbolize hash keys for consistent access
|
|
271
|
+
def deep_symbolize(obj)
|
|
272
|
+
case obj
|
|
273
|
+
when Hash
|
|
274
|
+
obj.each_with_object({}) do |(k, v), memo|
|
|
275
|
+
key = k.is_a?(String) || k.is_a?(Symbol) ? k.to_sym : k
|
|
276
|
+
memo[key] = deep_symbolize(v)
|
|
253
277
|
end
|
|
278
|
+
when Array
|
|
279
|
+
obj.map { |item| deep_symbolize(item) }
|
|
280
|
+
else
|
|
281
|
+
obj
|
|
254
282
|
end
|
|
255
|
-
|
|
256
|
-
content = text_parts.join
|
|
257
|
-
return content if tool_calls.empty?
|
|
258
|
-
|
|
259
|
-
openai_response(response, content, tool_calls)
|
|
260
|
-
end
|
|
261
|
-
|
|
262
|
-
def openai_response(response, content, tool_calls)
|
|
263
|
-
{
|
|
264
|
-
"id" => response["id"],
|
|
265
|
-
"object" => "chat.completion",
|
|
266
|
-
"created" => Time.now.to_i,
|
|
267
|
-
"model" => response["model"],
|
|
268
|
-
"choices" => [{
|
|
269
|
-
"index" => 0,
|
|
270
|
-
"message" => {
|
|
271
|
-
"role" => "assistant",
|
|
272
|
-
"content" => content,
|
|
273
|
-
"tool_calls" => tool_calls,
|
|
274
|
-
},
|
|
275
|
-
"finish_reason" => openai_finish_reason(response["stop_reason"]),
|
|
276
|
-
}],
|
|
277
|
-
"usage" => response["usage"],
|
|
278
|
-
}
|
|
279
|
-
end
|
|
280
|
-
|
|
281
|
-
def openai_tool_call(block)
|
|
282
|
-
{
|
|
283
|
-
"id" => block["id"],
|
|
284
|
-
"type" => "function",
|
|
285
|
-
"function" => {
|
|
286
|
-
"name" => block["name"],
|
|
287
|
-
"arguments" => JSON.generate(block["input"] || {}),
|
|
288
|
-
},
|
|
289
|
-
}
|
|
290
|
-
end
|
|
291
|
-
|
|
292
|
-
def openai_finish_reason(stop_reason)
|
|
293
|
-
return "tool_calls" if stop_reason == "tool_use"
|
|
294
|
-
|
|
295
|
-
stop_reason
|
|
296
283
|
end
|
|
297
|
-
|
|
298
|
-
|
|
284
|
+
|
|
285
|
+
public
|
|
286
|
+
|
|
287
|
+
# Send request to Anthropic API
|
|
288
|
+
# @param messages [Array] Array of message hashes
|
|
289
|
+
# @param model [String, nil] Model name (optional, uses config default if nil)
|
|
290
|
+
# @param temperature [Float, nil] Temperature value (optional, uses config or 0.7 if nil)
|
|
291
|
+
# @param tools [Array, nil] Array of tool definitions (optional)
|
|
292
|
+
# @param proc [Proc, nil] Callback for streaming responses (optional)
|
|
293
|
+
# @return [Hash, nil] OpenAI-formatted response (nil for streaming mode)
|
|
294
|
+
def send_request(messages, model = nil, temperature = nil, tools = nil, proc = nil)
|
|
295
|
+
begin
|
|
296
|
+
# Determine model name (parameter > config)
|
|
297
|
+
model_name = model || @config["model"]
|
|
298
|
+
|
|
299
|
+
# Determine temperature (config > parameter > default 0.7)
|
|
300
|
+
temp_value = @config["temperature"] || temperature || 0.7
|
|
301
|
+
|
|
302
|
+
# Determine max_tokens (config > default 1024)
|
|
303
|
+
max_tokens_value = @config["max_tokens"] || 1024
|
|
304
|
+
|
|
305
|
+
SmartPrompt.logger.info "AnthropicAdapter: Sending request to Anthropic"
|
|
306
|
+
SmartPrompt.logger.info "AnthropicAdapter: Using model #{model_name}"
|
|
307
|
+
|
|
308
|
+
# Extract system message
|
|
309
|
+
system_message = extract_system_message(messages)
|
|
310
|
+
|
|
311
|
+
# Convert messages to Anthropic format
|
|
312
|
+
converted_messages = convert_messages_to_anthropic_format(messages)
|
|
313
|
+
|
|
314
|
+
# Build request parameters
|
|
315
|
+
parameters = {
|
|
316
|
+
model: model_name,
|
|
317
|
+
messages: converted_messages,
|
|
318
|
+
max_tokens: max_tokens_value,
|
|
319
|
+
temperature: temp_value,
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
# Add system message if present
|
|
323
|
+
parameters[:system] = system_message if system_message
|
|
324
|
+
|
|
325
|
+
# Convert and add tools if provided
|
|
326
|
+
if tools
|
|
327
|
+
anthropic_tools = convert_tools_to_anthropic_format(tools)
|
|
328
|
+
parameters[:tools] = anthropic_tools if anthropic_tools
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
SmartPrompt.logger.info "Send parameters is: #{parameters}"
|
|
332
|
+
|
|
333
|
+
# Send request to Anthropic API
|
|
334
|
+
if proc
|
|
335
|
+
# Streaming mode: use stream method
|
|
336
|
+
stream = @client.messages.stream(**parameters)
|
|
337
|
+
|
|
338
|
+
# Iterate through the stream and call proc for each event
|
|
339
|
+
stream.each do |event|
|
|
340
|
+
# Convert event to hash format for compatibility
|
|
341
|
+
event_hash = {
|
|
342
|
+
"type" => event.type.to_s,
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
# Add delta information for content_block_delta events
|
|
346
|
+
if event.type == :content_block_delta && event.delta.type == :text_delta
|
|
347
|
+
event_hash["delta"] = { "text" => event.delta.text }
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
proc.call(event_hash, 0)
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
SmartPrompt.logger.info "Successful send a message (streaming)"
|
|
354
|
+
nil
|
|
355
|
+
else
|
|
356
|
+
# Non-streaming mode: use create method
|
|
357
|
+
response = @client.messages.create(**parameters)
|
|
358
|
+
SmartPrompt.logger.info "Successful send a message"
|
|
359
|
+
SmartPrompt.logger.info "AnthropicAdapter: Received response from Anthropic"
|
|
360
|
+
|
|
361
|
+
# Convert response to openai format
|
|
362
|
+
convert_response_to_openai_format(response)
|
|
363
|
+
end
|
|
364
|
+
rescue => e
|
|
365
|
+
SmartPrompt.logger.error "Anthropic API error: #{e.message}"
|
|
366
|
+
SmartPrompt.logger.error "Error class: #{e.class}"
|
|
367
|
+
SmartPrompt.logger.error "Backtrace: #{e.backtrace.first(5).join("\n")}"
|
|
368
|
+
raise LLMAPIError, "Failed to send request to Anthropic: #{e.message}"
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
# Embeddings method (not supported by Anthropic API)
|
|
373
|
+
# @param text [String] Text to generate embeddings for
|
|
374
|
+
# @param model [String] Model name
|
|
375
|
+
# @raise [NotImplementedError] Always raises as Anthropic doesn't support embeddings
|
|
376
|
+
def embeddings(text, model)
|
|
377
|
+
raise NotImplementedError, "Anthropic API does not support embeddings. Please use OpenAI or other providers for embedding generation."
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
end
|