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.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +10 -10
  3. data/README.cn.md +307 -64
  4. data/README.md +311 -64
  5. data/Rakefile +10 -1
  6. data/config/anthropic_config.yml +151 -0
  7. data/config/image_generation_config.yml +22 -0
  8. data/config/multimodal_config.yml +85 -0
  9. data/config/sensenova_config.yml +63 -0
  10. data/config/zhipu_config.yml +73 -0
  11. data/examples/anthropic_basic_chat.rb +143 -0
  12. data/examples/anthropic_example.rb +232 -0
  13. data/examples/anthropic_multimodal.rb +212 -0
  14. data/examples/anthropic_streaming.rb +312 -0
  15. data/examples/anthropic_tool_calling.rb +393 -0
  16. data/examples/automatic_cleanup_example.rb +109 -0
  17. data/examples/history_management_examples.rb +522 -0
  18. data/examples/image_generation_example.rb +130 -0
  19. data/examples/monitoring_example.rb +121 -0
  20. data/examples/multimodal_example.rb +63 -0
  21. data/examples/relevance_based_strategy_example.rb +87 -0
  22. data/examples/sensenova_example.rb +129 -0
  23. data/examples/stt_example.rb +287 -0
  24. data/examples/tts_example.rb +244 -0
  25. data/examples/video_generation_example.rb +189 -0
  26. data/examples/zhipu_example.rb +151 -0
  27. data/lib/smart_prompt/anthropic_adapter.rb +363 -281
  28. data/lib/smart_prompt/compression_engine.rb +201 -0
  29. data/lib/smart_prompt/context_strategy.rb +22 -0
  30. data/lib/smart_prompt/conversation.rb +81 -191
  31. data/lib/smart_prompt/engine.rb +36 -19
  32. data/lib/smart_prompt/history_manager.rb +596 -0
  33. data/lib/smart_prompt/hybrid_strategy.rb +222 -0
  34. data/lib/smart_prompt/image_generation_adapter.rb +297 -0
  35. data/lib/smart_prompt/lru_cache.rb +133 -0
  36. data/lib/smart_prompt/message.rb +57 -0
  37. data/lib/smart_prompt/multimodal_adapter.rb +277 -0
  38. data/lib/smart_prompt/openai_adapter.rb +1 -25
  39. data/lib/smart_prompt/persistence_layer.rb +197 -0
  40. data/lib/smart_prompt/relevance_based_strategy.rb +221 -0
  41. data/lib/smart_prompt/sensenova_adapter.rb +410 -0
  42. data/lib/smart_prompt/session.rb +140 -0
  43. data/lib/smart_prompt/sliding_window_strategy.rb +100 -0
  44. data/lib/smart_prompt/stt_adapter.rb +381 -0
  45. data/lib/smart_prompt/summary_based_strategy.rb +152 -0
  46. data/lib/smart_prompt/token_counter.rb +74 -0
  47. data/lib/smart_prompt/tts_adapter.rb +403 -0
  48. data/lib/smart_prompt/version.rb +1 -1
  49. data/lib/smart_prompt/video_generation_adapter.rb +330 -0
  50. data/lib/smart_prompt/worker.rb +25 -3
  51. data/lib/smart_prompt/zhipu_adapter.rb +616 -0
  52. data/lib/smart_prompt.rb +22 -2
  53. data/workers/history_management_examples.rb +407 -0
  54. data/workers/image_generation_workers.rb +119 -0
  55. data/workers/multimodal_workers.rb +110 -0
  56. data/workers/sensenova_workers.rb +62 -0
  57. data/workers/stt_workers.rb +195 -0
  58. data/workers/tts_workers.rb +388 -0
  59. data/workers/video_generation_workers.rb +264 -0
  60. data/workers/zhipu_workers.rb +113 -0
  61. metadata +84 -8
@@ -1,298 +1,380 @@
1
- require "net/http"
2
- require "json"
1
+ require "anthropic"
2
+ require "base64"
3
3
  require "uri"
4
-
5
- module SmartPrompt
6
- class AnthropicAdapter < LLMAdapter
7
- DEFAULT_URL = "https://api.anthropic.com"
8
- DEFAULT_VERSION = "2023-06-01"
9
- DEFAULT_MAX_TOKENS = 4096
10
-
11
- def initialize(config)
12
- super
13
- @api_key = resolve_api_key(@config["api_key"]) || ENV["ANTHROPIC_API_KEY"]
14
- @url = (@config["url"] || DEFAULT_URL).chomp("/")
15
- @anthropic_version = @config["anthropic_version"] || DEFAULT_VERSION
16
- @request_timeout = @config["request_timeout"] || 240
17
-
18
- raise LLMAPIError, "Invalid Anthropic configuration: missing api_key" if @api_key.nil? || @api_key.empty?
19
-
20
- @messages_uri = URI("#{@url}/v1/messages")
21
- SmartPrompt.logger.info "Successful creation an Anthropic client."
22
- rescue URI::InvalidURIError => e
23
- SmartPrompt.logger.error "Failed to initialize Anthropic client: #{e.message}"
24
- raise LLMAPIError, "Invalid Anthropic configuration: #{e.message}"
25
- rescue LLMAPIError
26
- raise
27
- rescue => e
28
- SmartPrompt.logger.error "Failed to initialize Anthropic client: #{e.message}"
29
- raise Error, "Unexpected error initializing Anthropic client: #{e.message}"
30
- end
31
-
32
- def send_request(messages, model = nil, temperature = 0.7, tools = nil, proc = nil)
33
- SmartPrompt.logger.info "AnthropicAdapter: Sending request to Anthropic"
34
- temperature = 0.7 if temperature.nil?
35
- model_name = model || @config["model"]
36
- SmartPrompt.logger.info "AnthropicAdapter: Using model #{model_name}"
37
-
38
- parameters = build_parameters(messages, model_name, temperature, tools, !proc.nil?)
39
- SmartPrompt.logger.info "Send parameters is: #{parameters}"
40
-
41
- response = post_messages(parameters, proc)
42
- SmartPrompt.logger.info "AnthropicAdapter: Received response from Anthropic"
43
-
44
- return if proc
45
-
46
- @last_response = response
47
- extract_content(response)
48
- rescue JSON::ParserError
49
- SmartPrompt.logger.error "Failed to parse Anthropic API response"
50
- raise LLMAPIError, "Failed to parse Anthropic API response"
51
- rescue LLMAPIError
52
- raise
53
- rescue => e
54
- SmartPrompt.logger.error "Unexpected error during Anthropic request: #{e.message}"
55
- raise Error, "Unexpected error during Anthropic request: #{e.message}"
56
- ensure
57
- SmartPrompt.logger.info "Successful send a message"
58
- end
59
-
60
- private
61
-
62
- def resolve_api_key(api_key)
63
- return api_key unless api_key.is_a?(String)
64
-
65
- match = api_key.match(/\AENV\[(["']?)([A-Za-z_][A-Za-z0-9_]*)\1\]\z/)
66
- return ENV[match[2]] if match
67
-
68
- api_key
69
- end
70
-
71
- def build_parameters(messages, model_name, temperature, tools, stream)
72
- anthropic_messages, system = normalize_messages(messages)
73
- parameters = {
74
- model: model_name,
75
- messages: anthropic_messages,
76
- max_tokens: @config["max_tokens"] || @config["max_completion_tokens"] || DEFAULT_MAX_TOKENS,
77
- temperature: @config["temperature"] || temperature,
78
- }
79
- parameters[:system] = system unless system.empty?
80
- parameters[:tools] = normalize_tools(tools) if tools
81
- parameters[:stream] = true if stream
82
- parameters
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 normalize_messages(messages)
86
- system_messages = []
87
- anthropic_messages = []
88
-
89
- messages.each do |message|
90
- role = message["role"] || message[:role]
91
- content = message["content"] || message[:content]
92
-
93
- case role.to_s
94
- when "system"
95
- system_messages << content.to_s
96
- when "user", "assistant"
97
- anthropic_messages << {
98
- role: role.to_s,
99
- content: normalize_content(content),
100
- }
101
- when "tool"
102
- anthropic_messages << {
103
- role: "user",
104
- content: normalize_tool_result(message),
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
- anthropic_messages << {
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
- content.to_s
121
- end
122
-
123
- def normalize_tool_result(message)
124
- tool_use_id = message["tool_call_id"] || message[:tool_call_id]
125
- content = message["content"] || message[:content]
126
-
127
- [{
128
- type: "tool_result",
129
- tool_use_id: tool_use_id.to_s,
130
- content: content.to_s,
131
- }]
132
- end
133
-
134
- def normalize_tools(tools)
135
- tools.map do |tool|
136
- function = tool["function"] || tool[:function] || tool
137
- {
138
- name: function["name"] || function[:name],
139
- description: function["description"] || function[:description],
140
- input_schema: function["parameters"] || function[:parameters] || {},
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
- def post_messages(parameters, stream_proc)
146
- http = Net::HTTP.new(@messages_uri.host, @messages_uri.port)
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
- accumulated_response
205
- end
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
- def openai_stream_chunk(event)
208
- case event["type"]
209
- when "message_start"
210
- message = event["message"] || {}
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
- def extract_content(response)
242
- text_parts = []
243
- tool_calls = []
244
-
245
- response.fetch("content", []).each do |block|
246
- case block["type"]
247
- when "text"
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
- end
298
- end
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