@@ -47,6 +47,71 @@ def initialize(user, assistant, conversation = nil, message = nil)
4747
4848 private
4949
50+ def anthropic_format_tools ( openai_tools )
51+ return [ ] if openai_tools . blank?
52+
53+ openai_tools . map do |tool |
54+ function = tool [ :function ]
55+ {
56+ name : function [ :name ] ,
57+ description : function [ :description ] ,
58+ input_schema : {
59+ type : function . dig ( :parameters , :type ) || "object" ,
60+ properties : function . dig ( :parameters , :properties ) || { } ,
61+ required : function . dig ( :parameters , :required ) || [ ]
62+ }
63+ }
64+ end
65+ rescue => e
66+ Rails . logger . info "Error formatting tools for Anthropic: #{ e . message } "
67+ [ ]
68+ end
69+
70+ def handle_tool_use_streaming ( intermediate_response )
71+ event_type = intermediate_response [ "type" ]
72+
73+ case event_type
74+ when "content_block_start"
75+ content_block = intermediate_response [ "content_block" ]
76+ if content_block &.dig ( "type" ) == "tool_use"
77+ index = intermediate_response [ "index" ] || 0
78+ Rails . logger . info "#### Starting tool_use block at index #{ index } "
79+ @stream_response_tool_calls [ index ] = {
80+ "id" => content_block [ "id" ] ,
81+ "name" => content_block [ "name" ] ,
82+ "input" => { }
83+ }
84+ end
85+ when "content_block_delta"
86+ delta = intermediate_response [ "delta" ]
87+ index = intermediate_response [ "index" ] || 0
88+
89+ if delta &.dig ( "type" ) == "input_json_delta"
90+ if @stream_response_tool_calls [ index ]
91+ partial_json = delta [ "partial_json" ]
92+ @stream_response_tool_calls [ index ] [ "_partial_json" ] ||= ""
93+ @stream_response_tool_calls [ index ] [ "_partial_json" ] += partial_json
94+
95+ begin
96+ @stream_response_tool_calls [ index ] [ "input" ] = JSON . parse ( @stream_response_tool_calls [ index ] [ "_partial_json" ] )
97+ rescue JSON ::ParserError
98+ Rails . logger . info "#### JSON still incomplete, continuing to accumulate"
99+ end
100+ else
101+ Rails . logger . error "#### Received input_json_delta for index #{ index } but no tool call initialized"
102+ end
103+ end
104+ when "content_block_stop"
105+ index = intermediate_response [ "index" ] || 0
106+ if @stream_response_tool_calls [ index ]
107+ @stream_response_tool_calls [ index ] . delete ( "_partial_json" )
108+ end
109+ end
110+
111+ rescue => e
112+ Rails . logger . error "Error handling Anthropic tool use streaming: #{ e . message } "
113+ end
114+
50115 def client_method_name
51116 :messages
52117 end
@@ -62,12 +127,14 @@ def set_client_config(config)
62127 model : @assistant . language_model . api_name ,
63128 system : config [ :instructions ] ,
64129 messages : config [ :messages ] ,
130+ tools : @assistant . language_model . supports_tools? && anthropic_format_tools ( Toolbox . tools ) || nil ,
65131 parameters : {
66132 model : @assistant . language_model . api_name ,
67133 system : config [ :instructions ] ,
68134 messages : config [ :messages ] ,
69135 max_tokens : 2000 , # we should really set this dynamically, based on the model, to the max
70136 stream : config [ :streaming ] && @response_handler || nil ,
137+ tools : @assistant . language_model . supports_tools? && anthropic_format_tools ( Toolbox . tools ) || nil ,
71138 } . compact . merge ( config [ :params ] &.except ( :response_format ) || { } )
72139 } . compact
73140 end
@@ -76,6 +143,8 @@ def stream_handler(&chunk_handler)
76143 proc do |intermediate_response , bytesize |
77144 chunk = intermediate_response . dig ( "delta" , "text" )
78145
146+ handle_tool_use_streaming ( intermediate_response )
147+
79148 if ( input_tokens = intermediate_response . dig ( "message" , "usage" , "input_tokens" ) )
80149 # https://docs.anthropic.com/en/api/messages-streaming
81150 @message . input_token_count = input_tokens
@@ -95,14 +164,24 @@ def stream_handler(&chunk_handler)
95164 raise ::Anthropic ::ConfigurationError
96165 rescue => e
97166 Rails . logger . info "\n Unhandled error in AIBackend::Anthropic response handler: #{ e . message } "
98- Rails . logger . info e . backtrace
99167 end
100168 end
101169
102170 def preceding_conversation_messages
103171 @conversation . messages . for_conversation_version ( @message . version ) . where ( "messages.index < ?" , @message . index ) . collect do |message |
104- if @assistant . supports_images? && message . documents . present?
105-
172+ # Anthropic doesn't support "tool" role - convert tool messages to user messages with tool_result content
173+ if message . tool?
174+ {
175+ role : "user" ,
176+ content : [
177+ {
178+ type : "tool_result" ,
179+ tool_use_id : message . tool_call_id ,
180+ content : message . content_text || ""
181+ }
182+ ]
183+ }
184+ elsif @assistant . supports_images? && message . documents . present?
106185 content = [ { type : "text" , text : message . content_text } ]
107186 content += message . documents . collect do |document |
108187 { type : "image" ,
@@ -114,6 +193,32 @@ def preceding_conversation_messages
114193 }
115194 end
116195
196+ {
197+ role : message . role ,
198+ content : content
199+ }
200+ elsif message . assistant? && message . content_tool_calls . present?
201+ Rails . logger . info "#### Converting assistant message with tool calls"
202+ Rails . logger . info "#### Tool calls: #{ message . content_tool_calls . inspect } "
203+
204+ content = [ ]
205+
206+ if message . content_text . present?
207+ content << { type : "text" , text : message . content_text }
208+ end
209+
210+ message . content_tool_calls . each do |tool_call |
211+ arguments = tool_call . dig ( "function" , "arguments" ) || tool_call . dig ( :function , :arguments ) || "{}"
212+ input = arguments . is_a? ( String ) ? JSON . parse ( arguments ) : arguments
213+
214+ content << {
215+ type : "tool_use" ,
216+ id : tool_call [ "id" ] || tool_call [ :id ] ,
217+ name : tool_call . dig ( "function" , "name" ) || tool_call . dig ( :function , :name ) ,
218+ input : input
219+ }
220+ end
221+
117222 {
118223 role : message . role ,
119224 content : content
0 commit comments