Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rename llm-tool-functions to llm-tool, fixes to elisp-to-tool #141

Merged
merged 38 commits into from
Jan 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
3434a59
Rename function calling to tool use and change the interface
ahyatt Dec 28, 2024
fe691ca
Fix issues in tool use in conversations, make clearer struct names
ahyatt Dec 29, 2024
80fb034
Get Gemini working with tool use
ahyatt Dec 31, 2024
b741a5d
Port ollama to tool use, simplify plz requests
ahyatt Dec 31, 2024
b4ccd86
Stop double pushing and use plist-put
ahyatt Dec 31, 2024
ad69d2c
Use tool calling in Claude, also add image support
ahyatt Dec 31, 2024
2d3dbd3
Add async and array items
ahyatt Jan 1, 2025
8e9b026
Fix lint issues, bump version to 0.21
ahyatt Jan 1, 2025
4c38e66
Test for array
ahyatt Jan 1, 2025
42439d0
Fix all tests
ahyatt Jan 1, 2025
1f73946
Update README.org to talk about tool use
ahyatt Jan 1, 2025
2946987
Update copyright notices
ahyatt Jan 1, 2025
bda5cf5
Rename and update the elisp converstion tool
ahyatt Jan 1, 2025
a831f03
Fix byte compilation
ahyatt Jan 1, 2025
a10d053
Fix extra quote in tester
ahyatt Jan 1, 2025
0f64b1a
Add to NEWS.org
ahyatt Jan 1, 2025
5df681c
Switch to json-serialize, which changes json schema
ahyatt Jan 4, 2025
b6efdd1
Fix issues causing integration tests to fail
ahyatt Jan 4, 2025
d9665a5
Use json-parse-string instead of json-read-from-string
ahyatt Jan 4, 2025
81f8a8a
Merge branch 'main' into tool-use
ahyatt Jan 4, 2025
ecb92e6
Remove plistp, which is too new to use
ahyatt Jan 5, 2025
54ff404
Update elisp tool utility with latest changes
ahyatt Jan 6, 2025
e048acc
Make enums specifications in tools vectors
ahyatt Jan 6, 2025
11f7f61
Use :optional instead of :required
ahyatt Jan 11, 2025
f52f093
Fix use of required in integration test
ahyatt Jan 14, 2025
692f683
Fix issue in Open AI tool use, and fix tester's use of required
ahyatt Jan 14, 2025
b27cda0
Fix to previous fix of Open AI's streaming tool use
ahyatt Jan 16, 2025
3bcd2b7
Fix request tests
ahyatt Jan 16, 2025
7b2d8b7
Switch back to types as symbols, support properties correctly
ahyatt Jan 19, 2025
a08525d
Merge branch 'main' into tool-use
ahyatt Jan 20, 2025
ce2def8
Fix string type in tester
ahyatt Jan 20, 2025
cb8de0b
Implement streaming tool use in Claude
ahyatt Jan 20, 2025
c4c78a1
Move tool use struct earlier in the file
ahyatt Jan 20, 2025
0454da3
Fix docstring
ahyatt Jan 20, 2025
1eb7ca2
Fix docstring part 2
ahyatt Jan 20, 2025
44c047c
Change tool-use-function to be tool-use, fixes to elisp-to-tool
ahyatt Jan 21, 2025
178afd6
Merge branch 'main' into tool-use
ahyatt Jan 21, 2025
71ed5ac
Fix leftover call in llm-tester
ahyatt Jan 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions NEWS.org
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
* Version 0.22.0
- Change ~llm-tool-function~ to ~llm-tool~, change ~make-llm-tool-function~ to take any arguments.
* Version 0.21.0
- Incompatible change to function calling, which is now tool use, affecting arguments and methods.
- Support image understanding in Claude
Expand Down
4 changes: 2 additions & 2 deletions README.org
Original file line number Diff line number Diff line change
Expand Up @@ -205,13 +205,13 @@ Tool use is a way to give the LLM a list of functions it can call, and have it c

This basic structure is useful because it can guarantee a well-structured output (if the LLM does decide to use the tool). *Not every LLM can handle tool use, and those that do not will ignore the tools entirely*. The function =llm-capabilities= will return a list with =tool-use= in it if the LLM supports tool use. Right now only Gemini, Vertex, Claude, and Open AI support tool use. However, even for LLMs that handle tool use, there is sometimes a difference in the capabilities. Right now, it is possible to write tools that succeed in Open AI but cause errors in Gemini, because Gemini does not appear to handle tools that have types that contain other types. So client programs are advised for right now to keep function to simple types.

The way to call functions is to attach a list of functions to the =tools= slot in the prompt. This is a list of =llm-tool-function= structs, which is a tool that is an elisp function, with a name, a description, and a list of arguments. The docstrings give an explanation of the format. An example is:
The way to call functions is to attach a list of functions to the =tools= slot in the prompt. This is a list of =llm-tool= structs, which is a tool that is an elisp function, with a name, a description, and a list of arguments. The docstrings give an explanation of the format. An example is:

#+begin_src emacs-lisp
(llm-chat-async my-llm-provider (llm-make-chat-prompt
"What is the capital of France?"
:tools
(list (llm-make-tool-function
(list (llm-make-tool
:function (lambda (callback result)
;; In this example function the assumption is that the
;; callback will be called after processing the result is
Expand Down
6 changes: 3 additions & 3 deletions llm-claude.el
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@

(defun llm-claude--tool-call (tool)
"A Claude version of a function spec for TOOL."
`(:name ,(llm-tool-function-name tool)
:description ,(llm-tool-function-description tool)
`(:name ,(llm-tool-name tool)
:description ,(llm-tool-description tool)
:input_schema ,(llm-provider-utils-openai-arguments
(llm-tool-function-args tool))))
(llm-tool-args tool))))

(cl-defmethod llm-provider-chat-request ((provider llm-claude) prompt stream)
(let ((request
Expand Down
278 changes: 139 additions & 139 deletions llm-integration-test.el

Large diffs are not rendered by default.

18 changes: 9 additions & 9 deletions llm-provider-utils.el
Original file line number Diff line number Diff line change
Expand Up @@ -593,18 +593,18 @@ Each plist has the structure:

;; The Open AI tool spec follows the JSON schema spec. See
;; https://json-schema.org/understanding-json-schema.
(cl-defmethod llm-provider-utils-openai-tool-spec ((tool llm-tool-function))
(cl-defmethod llm-provider-utils-openai-tool-spec ((tool llm-tool))
"Convert TOOL to an Open AI function spec.
Open AI's function spec is a standard way to do this, and will be
applicable to many endpoints.

This returns a JSON object (a list that can be converted to JSON)."
`(:type "function"
:function
(:name ,(llm-tool-function-name tool)
:description ,(llm-tool-function-description tool)
(:name ,(llm-tool-name tool)
:description ,(llm-tool-description tool)
:parameters ,(llm-provider-utils-openai-arguments
(llm-tool-function-args tool)))))
(llm-tool-args tool)))))

(defun llm-provider-utils-openai-collect-streaming-tool-uses (data)
"Read Open AI compatible streaming output DATA to collect tool-uses."
Expand Down Expand Up @@ -721,9 +721,9 @@ have returned results."
(let* ((name (llm-provider-utils-tool-use-name tool-use))
(arguments (llm-provider-utils-tool-use-args tool-use))
(tool (seq-find
(lambda (f) (equal name (llm-tool-function-name f)))
(lambda (f) (equal name (llm-tool-name f)))
(llm-chat-prompt-tools prompt)))
(call-args (cl-loop for arg in (llm-tool-function-args tool)
(call-args (cl-loop for arg in (llm-tool-args tool)
collect (cdr (seq-find (lambda (a)
(eq (intern (plist-get arg :name))
(car a)))
Expand All @@ -741,10 +741,10 @@ have returned results."
(llm-provider-utils-populate-tool-uses
provider prompt results)
(funcall success-callback tool-use-and-results)))))
(if (llm-tool-function-async tool)
(apply (llm-tool-function-function tool)
(if (llm-tool-async tool)
(apply (llm-tool-function tool)
(append (list end-func) call-args))
(funcall end-func (apply (llm-tool-function-function tool) call-args)))))))
(funcall end-func (apply (llm-tool-function tool) call-args)))))))


;; This is a useful method for getting out of the request buffer when it's time
Expand Down
2 changes: 1 addition & 1 deletion llm-test.el
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@
(:name "Request with tools"
:prompt (lambda () (llm-make-chat-prompt
"Hello world"
:tools (list (llm-make-tool-function
:tools (list (llm-make-tool
:name "func"
:description "desc"
:args '((:name "arg1" :description "desc1" :type string)
Expand Down
2 changes: 1 addition & 1 deletion llm-tester.el
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ for, and you need to provide the most likely function you know
of by calling the `describe_function' function."
:temperature 0.1
:tools
(list (llm-make-tool-function
(list (llm-make-tool
:function (lambda (f) f)
:name "describe_function"
:description "Takes an elisp function name and shows the user the functions and their descriptions."
Expand Down
6 changes: 3 additions & 3 deletions llm-vertex.el
Original file line number Diff line number Diff line change
Expand Up @@ -226,10 +226,10 @@ the key must be regenerated every hour."
[(:function_declarations
,(vconcat (mapcar
(lambda (tool)
`(:name ,(llm-tool-function-name tool)
:description ,(llm-tool-function-description tool)
`(:name ,(llm-tool-name tool)
:description ,(llm-tool-description tool)
:parameters ,(llm-provider-utils-openai-arguments
(llm-tool-function-args tool))))
(llm-tool-args tool))))
(llm-chat-prompt-tools prompt))))]))
(llm-vertex--chat-parameters prompt)))

Expand Down
17 changes: 12 additions & 5 deletions llm.el
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
;; Author: Andrew Hyatt <[email protected]>
;; Homepage: https://github.com/ahyatt/llm
;; Package-Requires: ((emacs "28.1") (plz "0.8") (plz-event-source "0.1.1") (plz-media-type "0.2.1"))
;; Package-Version: 0.21.0
;; Package-Version: 0.22.0
;; SPDX-License-Identifier: GPL-3.0-or-later
;;
;; This program is free software; you can redistribute it and/or
Expand Down Expand Up @@ -96,9 +96,8 @@ TOOL-NAME is the name of the tool. This is required.
RESULT is the result of the tool use. This is required."
call-id tool-name result)

(cl-defstruct (llm-tool-function (:constructor llm-make-tool-function))
"This is a struct to represent a single function tool available to the
LLM.
(cl-defstruct llm-tool
"This is a struct for a single tool available to the LLM.

All fields are required.

Expand Down Expand Up @@ -223,6 +222,14 @@ This is deprecated, and you should use `llm-make-chat-prompt'
instead."
(llm-make-chat-prompt text))

(cl-defun llm-make-tool (&key function name description args async &allow-other-keys)
"Create a `llm-tool' struct with FUNCTION, NAME, DESCRIPTION, ARGS, and ASYNC."
(make-llm-tool :function function
:name name
:description description
:args args
:async async))

(cl-defun llm-make-chat-prompt (content &key context examples tools
temperature max-tokens response-format
non-standard-params)
Expand Down Expand Up @@ -257,7 +264,7 @@ to the chat as a whole. This is optional.
EXAMPLES is a list of conses, where the car is an example
inputs, and cdr is the corresponding example outputs. This is optional.

TOOLS is a list of `llm-tool-use' structs. These may be
TOOLS is a list of `llm-tool' structs. These may be
called IF the LLM supports them. If the LLM does not support
them, a `not-implemented' signal will be thrown. This is
optional. When this is given, the LLM will either call the
Expand Down
23 changes: 12 additions & 11 deletions utilities/elisp-to-tool.el
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,12 @@
:name
"switch_to_buffer"
:args
'((:name "buffer_or_name" :type "string" :description "A buffer, a string (buffer name), or nil. If nil, switch to the buffer returned by 'other_buffer'." :required t) (:name "norecord" :type "boolean" :description "If non-nil, do not put the buffer at the front of the buffer list, and do not make the window displaying it the most recently selected one." :required t) (:name "force_same_window" :type "boolean" :description "If non-nil, the buffer must be displayed in the selected window when called non-interactively; if impossible, signal an error rather than calling 'pop_to_buffer'." :required t))
'((:name "buffer_or_name" :type string :description "A buffer, a string (buffer name), or nil. If a string that doesn't identify an existing buffer, create a buffer with that name. If nil, switch to the buffer returned by 'other_buffer'." :required t) (:name "norecord" :type boolean :description "If non-nil, do not put the buffer at the front of the buffer list, and do not make the window displaying it the most recently selected one." :required t) (:name "force_same_window" :type boolean :description "If non-nil, the buffer must be displayed in the selected window when called non-interactively; if impossible, signal an error rather than calling 'pop_to_buffer'." :required t))
:description
"Display buffer BUFFER_OR_NAME in the selected window. If the selected window cannot display the specified buffer because it is a minibuffer window or strongly dedicated to another buffer, call 'pop_to_buffer' to select the buffer in another window. Returns the buffer switched to."
"Display buffer BUFFER_OR_NAME in the selected window. WARNING: This is NOT the way to work on another buffer temporarily within a Lisp program! Use 'set_buffer' instead. That avoids messing with the 'window_buffer' correspondences. If the selected window cannot display the specified buffer because it is a minibuffer window or strongly dedicated to another buffer, call 'pop_to_buffer' to select the buffer in another window. In interactive use, if the selected window is strongly dedicated to its buffer, the value of the option 'switch_to_buffer_in_dedicated_window' specifies how to proceed. Return the buffer switched to."
:async
nil))
nil)
)

;; A demonstration of the resulting function call in action.
(defun elisp-to-tool-llm-switch-buffer (instructions)
Expand Down Expand Up @@ -115,7 +116,7 @@ Documentation strings should start with uppercase and end with a period."
(list
:name (downcase (elisp-to-tool-el-to-js-name
(assoc-default 'name arg)))
:type (assoc-default 'type arg)
:type (intern (assoc-default 'type arg))
:description (assoc-default 'description arg))
(if (assoc-default 'required arg)
(list :required t))))
Expand All @@ -125,23 +126,23 @@ Documentation strings should start with uppercase and end with a period."
:name "elisp-to-tool-info"
:description "The function to create a OpenAI-compatible tool use spec, given the arguments and their documentation. Some of the aspects of the tool can be automatically retrieved, so this function is supplying the parts that cannot be automatically retrieved."
:args '((:name "args"
:type "array"
:items (:type "object"
:type array
:items (:type object
:properties (:name
(:type "string"
(:type string
:description "The name of the argument")
:type
(:type "string"
(:type string
:enum ["string""number" "integer" "boolean"]
:description "The type of the argument. It could be 'string', 'number', 'integer', 'boolean', or the more special forms.")
:description
(:type "string"
(:type string
:description "The description of the argument")
:required
(:type "boolean"
(:type boolean
:description "Whether the argument is required or not"))))
(:name "description"
:type "string"
:type string
:description "The documentation of the function to transform.")))))
(lambda (result) (message "Result: %S" result))
(lambda (_ msg) (error msg)))))
Expand Down
Loading