branch: elpa/gptel
commit 1a3b393a4b676c89e7d675c7d1b395c73d238755
Author: Karthik Chikmagalur <karthikchikmaga...@gmail.com>
Commit: Karthik Chikmagalur <karthikchikmaga...@gmail.com>

    gptel-anthropic: Parse tool results from buffer
    
    * gptel-anthropic.el (gptel--anthropic-format-tool-id,
    gptel--anthropic-unformat-tool-id, gptel-curl--parse-stream,
    gptel--parse-response, gptel--parse-tool-results,
    gptel--parse-buffer):
    - Unformat and format tool ids for storage, store only the UUID
      part of the ID, such as abcd in toolu_abcd.
    - Match the gptel text property values for (tool . id)
    - Create tool call and result messages from each recorded call
    
    Parse tool messages in the buffer.  This commit covers the
    Anthropic backend.  The strategy is to pcase on the gptel
    property.  Since tool insertions have one distinct gptel text
    property, the call and response message are recovered at the same
    time.
    
    Introduce support for the `ignore' property in the Anthropic
    prompt creator.  Users can make use of it to have non-conversation
    content intermingled with chat.
---
 gptel-anthropic.el | 45 +++++++++++++++++++++++++++++++++++++++++----
 1 file changed, 41 insertions(+), 4 deletions(-)

diff --git a/gptel-anthropic.el b/gptel-anthropic.el
index c97cbf5bc2..7965d4ae81 100644
--- a/gptel-anthropic.el
+++ b/gptel-anthropic.el
@@ -126,7 +126,9 @@ information if the stream contains it.  Not my best work, I 
know."
                 ;; Then shape the tool-use block by adding args so we can call 
the functions
                 (mapc (lambda (tool-call)
                         (plist-put tool-call :args (plist-get tool-call 
:input))
-                        (plist-put tool-call :input nil))
+                        (plist-put tool-call :input nil)
+                        (plist-put tool-call :id 
(gptel--anthropic-unformat-tool-id
+                                                  (plist-get tool-call :id))))
                       tool-use)))
             (when-let* ((response (gptel--json-read)))
               (plist-put info :output-tokens
@@ -170,6 +172,8 @@ Mutate state INFO with response metadata."
       for call = (copy-sequence call-raw) do
       (plist-put call :args (plist-get call :input))
       (plist-put call :input nil)
+      (plist-put call :id (gptel--anthropic-unformat-tool-id
+                           (plist-get call :id)))
       collect call into calls
       finally do (plist-put info :tool-use calls)))
    finally return
@@ -250,7 +254,8 @@ TOOL-USE is a list of plists containing tool names, 
arguments and call results."
        (let* ((result (plist-get tool-call :result))
               (formatted
                (list :type "tool_result"
-                     :tool_use_id (plist-get tool-call :id)
+                     :tool_use_id (gptel--anthropic-format-tool-id
+                                   (plist-get tool-call :id))
                      :content (if (stringp result) result
                                 (prin1-to-string result)))))
          (prog1 formatted
@@ -261,6 +266,18 @@ TOOL-USE is a list of plists containing tool names, 
arguments and call results."
 ;; NOTE: No `gptel--inject-prompt' method required for gptel-anthropic, since
 ;; this is handled by its defgeneric implementation
 
+(defun gptel--anthropic-format-tool-id (tool-id)
+  (if (string-prefix-p "toolu_" tool-id)
+      tool-id
+    (format "toolu_%s" tool-id)))
+
+(defun gptel--anthropic-unformat-tool-id (tool-id)
+  (or (and (string-match "toolu_\\(.+\\)" tool-id)
+           (match-string 1 tool-id))
+      (progn
+        (message "Unexpected tool_call_id format: %s" tool-id)
+        tool-id)))
+
 (cl-defmethod gptel--parse-list ((_backend gptel-anthropic) prompt-list)
   (cl-loop for text in prompt-list
            for role = t then (not role)
@@ -268,7 +285,7 @@ TOOL-USE is a list of plists containing tool names, 
arguments and call results."
            (list :role (if role "user" "assistant")
                  :content `[(:type "text" :text ,text)])))
 
-(cl-defmethod gptel--parse-buffer ((_backend gptel-anthropic) &optional 
max-entries)
+(cl-defmethod gptel--parse-buffer ((backend gptel-anthropic) &optional 
max-entries)
   (let ((prompts) (prev-pt (point))
         (include-media (and gptel-track-media (or (gptel--model-capable-p 
'media)
                                                   (gptel--model-capable-p 
'url)))))
@@ -283,7 +300,6 @@ TOOL-USE is a list of plists containing tool names, 
arguments and call results."
           ;; We check for blank prompts by skipping whitespace and comparing
           ;; point against the previous.
           (unless (save-excursion (skip-syntax-forward " ") (>= (point) 
prev-pt))
-            ;; XXX update for tools
             (pcase (get-char-property (point) 'gptel)
               ('response
                (when-let* ((content
@@ -291,6 +307,27 @@ TOOL-USE is a list of plists containing tool names, 
arguments and call results."
                              (buffer-substring-no-properties (point) 
prev-pt))))
                  (when (not (string-blank-p content))
                    (push (list :role "assistant" :content content) prompts))))
+              (`(tool . ,id)
+               (save-excursion
+                 (condition-case nil
+                     (let* ((tool-call (read (current-buffer)))
+                            (id (gptel--anthropic-format-tool-id id))
+                            (name (plist-get tool-call :name))
+                            (arguments (plist-get tool-call :args)))
+                       (plist-put tool-call :id id)
+                       (plist-put tool-call :result
+                                  (string-trim (buffer-substring-no-properties
+                                                (point) prev-pt)))
+                       (push (gptel--parse-tool-results backend (list 
tool-call))
+                             prompts)
+                       (push (list :role "assistant"
+                                   :content `[( :type "tool_use" :id ,id :name 
,name
+                                                :input ,arguments)])
+                             prompts))
+                   ((end-of-file invalid-read-syntax)
+                    (message (format "Could not parse tool-call %s on line %s"
+                                     id (line-number-at-pos (point))))))))
+              ('ignore)
               ('nil                     ; user role: possibly with media
                (if include-media
                    (when-let* ((content (gptel--anthropic-parse-multipart

Reply via email to