branch: elpa/gptel commit 7b00e85cbb025777f4761322fbf4ea9109de917c Author: Karthik Chikmagalur <karthikchikmaga...@gmail.com> Commit: Karthik Chikmagalur <karthikchikmaga...@gmail.com>
gptel: Allow all JSON schema keys in tool arg specs (#788) Change how gptel tool argument specs are parsed into the JSON schema for tool args understood by LLMs. Instead of managing each argument keyword (like :type, :description etc) manually, remove keywords used only by gptel (:name and :optional) and attach the provided arg spec as is into the tool and (eventually) its JSON specification. This makes correctness the responsibility of the tool author, but gptel now supports all schema keywords, including `allOf', `anyOf', `default' and so on. * gptel.el (gptel--parse-tools): Make the above change. This covers tool specs for OpenAI and Ollama. * gptel-gemini.el (gptel--parse-tools): Make the above change. * gptel-anthropic.el (gptel--parse-tools): Make the above change. * test: Add unit tests for `gptel--parse-tools' (all backends). --- gptel-anthropic.el | 32 +++++++++++++++----------------- gptel-gemini.el | 24 ++++++++++++------------ gptel.el | 35 +++++++++++++++++------------------ test | 2 +- 4 files changed, 45 insertions(+), 48 deletions(-) diff --git a/gptel-anthropic.el b/gptel-anthropic.el index bd5c58bb74..0584c4afc4 100644 --- a/gptel-anthropic.el +++ b/gptel-anthropic.el @@ -87,7 +87,7 @@ information if the stream contains it. Not my best work, I know." (if-let* ((signature (plist-get delta :signature))) (plist-put info :signature (concat (plist-get info :signature) signature)))))))) - + ((looking-at "content_block_start") ;Is the following block text or tool-use? (forward-line 1) (forward-char 5) (when-let* ((cblock (plist-get (gptel--json-read) :content_block))) @@ -99,7 +99,7 @@ information if the stream contains it. Not my best work, I know." (plist-get info :tool-use)))) ("thinking" (plist-put info :reasoning (plist-get cblock :thinking)) (plist-put info :reasoning-block 'in))))) - + ((looking-at "content_block_stop") (cond ((plist-get info :partial_json) ;End of tool block @@ -117,7 +117,7 @@ information if the stream contains it. Not my best work, I know." ((eq (plist-get info :reasoning-block) 'in) ;End of reasoning block (plist-put info :reasoning-block t)))) ;Signal end of reasoning stream to filter - + ((looking-at "message_delta") ;; collect stop_reason, usage_tokens and prepare tools (forward-line 1) (forward-char 5) @@ -250,25 +250,23 @@ TOOLS is a list of `gptel-tool' structs, which see." :description (gptel-tool-description tool) :input_schema ;NOTE: Anthropic wants "{}" if the function takes no args, not null (list :type "object" + ;; See the generic implementation for an explanation of this + ;; transformation. :properties (cl-loop for arg in (gptel-tool-args tool) - for name = (plist-get arg :name) - for type = (plist-get arg :type) + for argspec = (copy-sequence arg) + for name = (plist-get arg :name) ;handled differently + for type = (plist-get arg :type) ;to add additional keys to objects for newname = (or (and (keywordp name) name) (make-symbol (concat ":" name))) - for enum = (plist-get arg :enum) - append (list newname - `(:type ,(plist-get arg :type) - :description ,(plist-get arg :description) - ,@(if enum (list :enum (vconcat enum))) - ,@(cond - ((equal type "object") - (list - :properties (plist-get arg :properties) - :required (or (plist-get arg :required) []))) - ((equal type "array") - (list :items (plist-get arg :items))))))) + do ;ARGSPEC is ARG without unrecognized keys + (cl-remf argspec :name) + (cl-remf argspec :optional) + if (equal (plist-get arg :type) "object") + do (unless (plist-member argspec :required) + (plist-put argspec :required [])) + append (list newname argspec)) :required (vconcat (delq nil (mapcar diff --git a/gptel-gemini.el b/gptel-gemini.el index 471a5124b3..5a8a2af3ea 100644 --- a/gptel-gemini.el +++ b/gptel-gemini.el @@ -157,23 +157,23 @@ TOOLS is a list of `gptel-tool' structs, which see." (if (not (gptel-tool-args tool)) :null ;NOTE: Gemini wants :null if the function takes no args (list :type "object" + ;; See the generic implementation for an explanation of this + ;; transformation. :properties (cl-loop for arg in (gptel-tool-args tool) - for name = (plist-get arg :name) - for type = (plist-get arg :type) + for argspec = (copy-sequence arg) + for name = (plist-get arg :name) ;handled differently + for type = (plist-get arg :type) ;to add additional keys to objects for newname = (or (and (keywordp name) name) (make-symbol (concat ":" name))) - for enum = (plist-get arg :enum) - append (list newname - `(:type ,(plist-get arg :type) - :description ,(plist-get arg :description) - ,@(if enum (list :enum (vconcat enum))) - ,@(cond - ((equal type "object") - (list :parameters (plist-get arg :parameters))) - ((equal type "array") - (list :items (plist-get arg :items))))))) + do ;ARGSPEC is ARG without unrecognized keys + (cl-remf argspec :name) + (cl-remf argspec :optional) + if (equal (plist-get arg :type) "object") + do (unless (plist-member argspec :required) + (plist-put argspec :required [])) + append (list newname argspec)) :required (vconcat (delq nil (mapcar diff --git a/gptel.el b/gptel.el index 62528d64bc..f32f00312b 100644 --- a/gptel.el +++ b/gptel.el @@ -1719,29 +1719,28 @@ implementation, used by OpenAI-compatible APIs and Ollama." (list :parameters (list :type "object" + ;; gptel's tool args spec is close to the JSON schema, except + ;; that we use (:name "argname" ...) + ;; instead of (:argname (...)), and + ;; (:optional t) for each arg instead of (:required [...]) + ;; for all args at once. Handle this difference by + ;; modifying a copy of the gptel tool arg spec. :properties (cl-loop for arg in (gptel-tool-args tool) - for name = (plist-get arg :name) - for type = (plist-get arg :type) + for argspec = (copy-sequence arg) + for name = (plist-get arg :name) ;handled differently + for type = (plist-get arg :type) ;to add additional keys to objects for newname = (or (and (keywordp name) name) (make-symbol (concat ":" name))) - for enum = (plist-get arg :enum) - append - (list newname - `(:type ,type - :description ,(plist-get arg :description) - ,@(if enum (list :enum (vconcat enum))) - ,@(cond - ((equal type "object") - (list :properties (plist-get arg :properties) - :required (or (plist-get arg :required) - (vector)) - :additionalProperties :json-false)) - ((equal type "array") - ;; TODO(tool) If the item type is an object, - ;; add :additionalProperties to it - (list :items (plist-get arg :items))))))) + do ;ARGSPEC is ARG without unrecognized keys + (cl-remf argspec :name) + (cl-remf argspec :optional) + if (equal (plist-get arg :type) "object") + do (unless (plist-member argspec :required) + (plist-put argspec :required [])) + (plist-put argspec :additionalProperties :json-false) + append (list newname argspec)) :required (vconcat (delq nil (mapcar diff --git a/test b/test index 9556080bcc..c0c937b6c5 160000 --- a/test +++ b/test @@ -1 +1 @@ -Subproject commit 9556080bcc8dee4bfec86cf78190892dd2493c46 +Subproject commit c0c937b6c516bc9f7fb6b5b28ae19ac8dc399cae