branch: externals/llm
commit ee26766b72f34057d798d0ecad012a7b91772570
Author: Andrew Hyatt <[email protected]>
Commit: GitHub <[email protected]>

    When we detect an error calling tools, use the error callback instead (#240)
    
    Before, we signaled the error, which isn't appropriate for async
    functions.
    
    Fixes https://github.com/ahyatt/llm/issues/237
---
 llm-integration-test.el    |  33 +++++++-----
 llm-provider-utils-test.el |  90 ++++++++++++++++---------------
 llm-provider-utils.el      | 128 +++++++++++++++++++++++++++------------------
 3 files changed, 145 insertions(+), 106 deletions(-)

diff --git a/llm-integration-test.el b/llm-integration-test.el
index bd54e2008d..8253ef4bd1 100644
--- a/llm-integration-test.el
+++ b/llm-integration-test.el
@@ -333,18 +333,27 @@ else.  We really just want to see if it's in the right 
ballpark."
 
 (llm-def-integration-test llm-boolean-tool-use (provider)
   (when (member 'tool-use (llm-capabilities provider))
-    (llm-chat provider (llm-make-chat-prompt
-                        "Is Lyon the capital of France?"
-                        :tools
-                        (list (llm-make-tool
-                               :function (lambda (result)
-                                           (should-not result))
-                               :name "verifier"
-                               :description "Test the LLM's decision on the 
veracity of the statement."
-                               :args '((:name "llm-decision"
-                                              :description "The decision on 
the statement by the LLM."
-                                              :type boolean))
-                               :async nil))))))
+    ;; We test if it is a list to verify that the tool was used.  We can't test
+    ;; the exact value due to the statement, value, which is necessary for 
worse
+    ;; model to pass, since they just feel the need to always pass a statement.
+    (should (listp
+             (llm-chat provider (llm-make-chat-prompt
+                                 "Is Lyon the capital of France?  You must 
answer using the verifier tool."
+                                 :tools
+                                 (list (llm-make-tool
+                                        :function (lambda (result &optional _)
+                                                    (should-not result))
+                                        :name "verifier"
+                                        :description "Record the LLM's 
decision on the veracity of the statement."
+                                        :args '((:name "decision"
+                                                       :description "The 
true/false judgement on the veracity of the statement"
+                                                       :type boolean
+                                                       :required t)
+                                                (:name "statement"
+                                                       :description "Statement 
explaining the decision"
+                                                       :type string))
+                                        :async nil))
+                                 :tool-options (make-llm-tool-options 
:tool-choice 'any)))))))
 
 (llm-def-integration-test llm-tool-use-multi-output (provider)
   (when (member 'tool-use (llm-capabilities provider))
diff --git a/llm-provider-utils-test.el b/llm-provider-utils-test.el
index 7b679cb58e..4c6c0f3ca3 100644
--- a/llm-provider-utils-test.el
+++ b/llm-provider-utils-test.el
@@ -167,49 +167,51 @@
                                                prompt tool-uses))
 
 (ert-deftest llm-provider-utils-execute-tool-uses--missing-tool ()
-  (should-error
-   (llm-provider-utils-execute-tool-uses
-    (make-llm-testing-provider)
-    (llm-make-chat-prompt
-     ""
-     :tools (list
-             (llm-make-tool
-              :name "tool-a"
-              :description "Tool A"
-              :function (lambda (&rest args) "Result A")
-              :args '())))
-    (list
-     (make-llm-provider-utils-tool-use
-      :id "1"
-      :name "tool-b"
-      :args '()))
-    nil
-    nil
-    #'identity)
-   :type '(llm-tool-unknown-tool)))
+  (llm-provider-utils-execute-tool-uses
+   (make-llm-testing-provider)
+   (llm-make-chat-prompt
+    ""
+    :tools (list
+            (llm-make-tool
+             :name "tool-a"
+             :description "Tool A"
+             :function (lambda (&rest args) "Result A")
+             :args '())))
+   (list
+    (make-llm-provider-utils-tool-use
+     :id "1"
+     :name "tool-b"
+     :args '()))
+   nil
+   nil
+   (lambda (results) (ert-fail "Should not succeed."))
+   (lambda (type msg)
+     (should (equal type 'llm-tool-unknown-tool))
+     (should (stringp msg)))))
 
 (ert-deftest llm-provider-utils-execute-tool-uses--unknown-arg ()
-  (should-error
-   (llm-provider-utils-execute-tool-uses
-    (make-llm-testing-provider)
-    (llm-make-chat-prompt
-     ""
-     :tools (list
-             (llm-make-tool
-              :name "tool-a"
-              :description "Tool A"
-              :function (lambda (&rest args) "Result A")
-              :args '((:name "arg1" :type string :description "Argument 1")))))
-    (list
-     (make-llm-provider-utils-tool-use
-      :id "1"
-      :name "tool-a"
-      :args '((arg1 . "value1")
-              (arg2 . "value2"))))
-    nil
-    nil
-    #'identity)
-   :type '(llm-tool-unknown-argument)))
+  (llm-provider-utils-execute-tool-uses
+   (make-llm-testing-provider)
+   (llm-make-chat-prompt
+    ""
+    :tools (list
+            (llm-make-tool
+             :name "tool-a"
+             :description "Tool A"
+             :function (lambda (&rest args) "Result A")
+             :args '((:name "arg1" :type string :description "Argument 1")))))
+   (list
+    (make-llm-provider-utils-tool-use
+     :id "1"
+     :name "tool-a"
+     :args '((arg1 . "value1")
+             (arg2 . "value2"))))
+   nil
+   nil
+   (lambda (results) (ert-fail "Should not succeed."))
+   (lambda (type msg)
+     (should (equal type 'llm-tool-unknown-argument))
+     (should (stringp msg)))))
 
 (ert-deftest llm-provider-utils-execute-tool-uses--missing-arg ()
   (should-error
@@ -230,8 +232,10 @@
       :args '()))
     nil
     nil
-    #'identity)
-   :type '(llm-tool-missing-argument)))
+    (lambda (results) (ert-fail "Should not succeed."))
+    (lambda (type msg)
+      (should (equal type 'llm-tool-missing-argument))
+      (should (stringp msg))))))
 
 (provide 'llm-provider-utils-test)
 ;;; llm-provider-utils-test.el ends here
diff --git a/llm-provider-utils.el b/llm-provider-utils.el
index 55afba5532..c776e79bc4 100644
--- a/llm-provider-utils.el
+++ b/llm-provider-utils.el
@@ -343,7 +343,9 @@ return a list of `llm-chat-prompt-tool-use' structs.")
                                           provider response)
                                          multi-output
                                          (lambda (result)
-                                           (setq final-result result))))
+                                           (setq final-result result))
+                                         (lambda (type msg)
+                                           (signal type msg))))
     ;; In most cases, final-result will be available immediately.  However, 
when
     ;; executing tools, we need to wait for their callbacks, and only after
     ;; those are called with this be ready.
@@ -373,7 +375,10 @@ return a list of `llm-chat-prompt-tool-use' structs.")
                         multi-output
                         (lambda (result)
                           (llm-provider-utils-callback-in-buffer
-                           buf success-callback result))))))
+                           buf success-callback result))
+                        (lambda (type msg)
+                          (llm-provider-utils-callback-in-buffer
+                           buf error-callback type msg))))))
      :on-error (lambda (_ data)
                  (llm-provider-utils-callback-in-buffer
                   buf error-callback 'error
@@ -462,7 +467,9 @@ Any strings will be concatenated, integers will be added, 
etc."
                             provider tool-uses-raw))))
           multi-output
           (lambda (result)
-            (llm-provider-utils-callback-in-buffer buf response-callback 
result)))))
+            (llm-provider-utils-callback-in-buffer buf response-callback 
result))
+          (lambda (type msg)
+            (llm-provider-utils-callback-in-buffer buf error-callback type 
msg)))))
      :on-error (lambda (_ data)
                  (llm-provider-utils-callback-in-buffer
                   buf error-callback 'error
@@ -738,7 +745,8 @@ ROLE will be `assistant' by default, but can be passed in 
for other roles."
                                   (format "%s" output))
                        :tool-results tool-results)))))
 
-(defun llm-provider-utils-process-result (provider prompt partial-result 
multi-output success-callback)
+(defun llm-provider-utils-process-result (provider prompt partial-result 
multi-output success-callback
+                                                   error-callback)
   "Process the RESPONSE from the provider for PROMPT.
 This execute function calls if there are any, does any result
 appending to the prompt, and returns an appropriate response for
@@ -754,7 +762,9 @@ MULTI-OUTPUT is true if multiple outputs are expected to be 
passed to
 SUCCESS-CALLBACK.
 
 SUCCESS-CALLBACK is the callback that will be run when all functions
-complete."
+complete.
+
+ERROR-CALLBACK is the callback that will be run on error."
   (when (and (plist-get partial-result :text)
              (> (length (plist-get partial-result :text)) 0))
     (llm-provider-append-to-prompt provider prompt (plist-get partial-result 
:text)))
@@ -764,7 +774,7 @@ complete."
       ;; will be done inside `llm-provider-utils-execute-tool-uses'.
       (llm-provider-utils-execute-tool-uses
        provider prompt tool-uses multi-output
-       partial-result success-callback)
+       partial-result success-callback error-callback)
     (funcall success-callback
              (if multi-output partial-result
                (plist-get partial-result :text)))))
@@ -824,7 +834,8 @@ This will convert all :json-false and :false values to nil."
    (t args)))
 
 (defun llm-provider-utils-execute-tool-uses (provider prompt tool-uses 
multi-output
-                                                      partial-result 
success-callback)
+                                                      partial-result 
success-callback
+                                                      error-callback)
   "Execute TOOL-USES, a list of `llm-provider-utils-tool-use'.
 
 A response suitable for returning to the client will be returned.
@@ -853,50 +864,65 @@ have returned results."
                    (seq-find
                     (lambda (f) (equal name (llm-tool-name f)))
                     (llm-chat-prompt-tools prompt))
-                   (signal 'llm-tool-unknown-tool `(:tool ,name))))
-            (call-args (cl-loop for arg in (llm-tool-args tool)
-                                collect (cdr (or
-                                              (seq-find (lambda (a)
-                                                          (eq (intern 
(plist-get arg :name))
-                                                              (car a)))
-                                                        arguments)
-                                              ;; Arg wasn't found, if it wasn't
-                                              ;; optional, signal an error.
-                                              (unless (plist-get arg :optional)
-                                                (signal 
'llm-tool-missing-argument
-                                                        `((:tool ,name :arg 
,arg))))))))
-            (end-func (lambda (result)
-                        (llm--log
-                         'api-funcall
-                         :provider provider
-                         :msg (format "%s --> %s"
-                                      (format "%S" (cons name call-args))
-                                      (format "%s" result)))
-                        (push (cons name result) tool-use-and-results)
-                        (push (cons tool-use result) results)
-                        (when (= (length results) (length tool-uses))
-                          (llm-provider-utils-populate-tool-uses
-                           provider prompt results)
-                          (funcall success-callback
-                                   (if multi-output
-                                       
(llm-provider-utils-final-multi-output-result
-                                        (append partial-result
-                                                `(:tool-results 
,tool-use-and-results)))
-                                     tool-use-and-results))))))
-       ;; Check to see that there were no unknown args.
-       (dolist (arg-key (map-keys arguments))
-         (unless (seq-find
-                  (lambda (a) (eq (intern (plist-get a :name))
-                                  arg-key))
-                  (llm-tool-args tool))
-           (signal 'llm-tool-unknown-argument
-                   `((:tool ,name :arg ,arg-key)))))
-
-       (if (llm-tool-async tool)
-           (apply (llm-tool-function tool)
-                  (append (list end-func) call-args))
-         (funcall end-func (apply (llm-tool-function tool)
-                                  (llm-provider-utils--normalize-args 
call-args))))))))
+                   (progn
+                     (funcall error-callback 'llm-tool-unknown-tool
+                              (format "Unknown tool '%s' called" name))
+                     nil)))
+            (call-args (when tool
+                         (cl-loop for arg in (llm-tool-args tool)
+                                  collect (cdr (or
+                                                (seq-find (lambda (a)
+                                                            (eq (intern 
(plist-get arg :name))
+                                                                (car a)))
+                                                          arguments)
+                                                ;; Arg wasn't found, if it 
wasn't
+                                                ;; optional, signal an error.
+                                                (progn
+                                                  (unless (plist-get arg 
:optional)
+                                                    (funcall error-callback 
'llm-tool-missing-argument
+                                                             (format "Missing 
required argument '%s' for tool '%s'"
+                                                                     
(plist-get arg :name)
+                                                                     name)))
+                                                  nil))))))
+            (end-func (when call-args
+                        (lambda (result)
+                          (llm--log
+                           'api-funcall
+                           :provider provider
+                           :msg (format "%s --> %s"
+                                        (format "%S" (cons name call-args))
+                                        (format "%s" result)))
+                          (push (cons name result) tool-use-and-results)
+                          (push (cons tool-use result) results)
+                          (when (= (length results) (length tool-uses))
+                            (llm-provider-utils-populate-tool-uses
+                             provider prompt results)
+                            (funcall success-callback
+                                     (if multi-output
+                                         
(llm-provider-utils-final-multi-output-result
+                                          (append partial-result
+                                                  `(:tool-results 
,tool-use-and-results)))
+                                       tool-use-and-results))))))
+            (failed nil))
+       (when end-func
+         ;; Check to see that there were no unknown args.
+         (dolist (arg-key (map-keys arguments))
+           (unless (seq-find
+                    (lambda (a) (eq (intern (plist-get a :name))
+                                    arg-key))
+                    (llm-tool-args tool))
+             (funcall error-callback 'llm-tool-unknown-argument
+                      (format "Unknown argument '%s' for tool '%s'"
+                              (symbol-name arg-key)
+                              name))
+             (setf failed t)))
+
+         (unless failed
+           (if (llm-tool-async tool)
+               (apply (llm-tool-function tool)
+                      (append (list end-func) call-args))
+             (funcall end-func (apply (llm-tool-function tool)
+                                      (llm-provider-utils--normalize-args 
call-args))))))))))
 
 
 ;; This is a useful method for getting out of the request buffer when it's time

Reply via email to