branch: externals/ellama
commit 025c56b0543cc70dfcfe38e646a743de3964e6c7
Merge: dfda86230d d72767e9b8
Author: Sergey Kostyaev <[email protected]>
Commit: GitHub <[email protected]>

    Merge pull request #358 from s-kostyaev/add-tools
    
    Add tools support
---
 NEWS.org            |  14 ++
 README.org          |  13 +-
 ellama-tools.el     | 556 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 ellama-transient.el |  25 ++-
 ellama.el           | 164 +++++++---------
 ellama.info         |  66 ++++---
 6 files changed, 720 insertions(+), 118 deletions(-)

diff --git a/NEWS.org b/NEWS.org
index 71e3c9d2ce..9a6298c2fc 100644
--- a/NEWS.org
+++ b/NEWS.org
@@ -1,3 +1,17 @@
+* Version 1.10.0
+- Added comprehensive tool support, enabling filesystem operations, shell
+  commands, and utilities through LLM tools, including tool registration, file
+  operations, directory tree exploration, and date/time utilities.
+- Integrated tool system into ~ellama.el~, adding dependencies, modifying
+  session creation, and refactoring error and response handlers.
+- Implemented a new transient menu for managing tools with enable/disable
+  functionalities, accessible via the main transient interface.
+- Added user confirmation system for potentially dangerous operations like file
+  and shell command executions.
+- Refactored tool confirmation logic and response handling in ~ellama-tools.el~
+  and ~ellama.el~ for improved structure and JSON encoding.
+- Fixed directory tree function errors, ensuring proper checks and formatted
+  output.
 * Version 1.9.1
 - Restricted template variable expansion to self-contained variables that don't
   span multiple lines to avoid conflicts with code snippets.
diff --git a/README.org b/README.org
index be991be778..79cfa69fda 100644
--- a/README.org
+++ b/README.org
@@ -222,7 +222,18 @@ More sofisticated configuration example:
     Options include streaming for real-time output, async for asynchronous
     processing, or skipping every N messages to reduce resource usage.
 - ~ellama-blueprint-variable-regexp~: Regular expression to match blueprint
-  variables like ~{var_name}~.
+    variables like ~{var_name}~.
+- ~ellama-tools-enable-by-name~: Enable a specific tool by its name. Use
+    this command to activate individual tools. Requires the tool name as input.
+- ~ellama-tools-enable-all~: Enable all available tools at once. Use this
+    command to activate every tool in the system for comprehensive 
functionality
+    without manual selection.
+- ~ellama-tools-disable-by-name~: Disable a specific tool by its name. Use
+    this command to deactivate individual tools when their functionality is no
+    longer needed.
+- ~ellama-tools-disable-all~: Disable all enabled tools simultaneously. Use
+    this command to reset the system to a minimal state, ensuring no tools are
+    active.
 
 * Keymap
 
diff --git a/ellama-tools.el b/ellama-tools.el
new file mode 100644
index 0000000000..cf7fbf7518
--- /dev/null
+++ b/ellama-tools.el
@@ -0,0 +1,556 @@
+;;; ellama-tools.el --- Working with tools -*- lexical-binding: t; 
package-lint-main-file: "ellama.el"; -*-
+
+;; Copyright (C) 2023-2025  Free Software Foundation, Inc.
+
+;; Author: Sergey Kostyaev <[email protected]>
+;; SPDX-License-Identifier: GPL-3.0-or-later
+
+;; This file is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation; either version 3, or (at your option)
+;; any later version.
+
+;; This file is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;
+;; Ellama is a tool for interacting with large language models from Emacs.
+;; It allows you to ask questions and receive responses from the
+;; LLMs.  Ellama can perform various tasks such as translation, code
+;; review, summarization, enhancing grammar/spelling or wording and
+;; more through the Emacs interface.  Ellama natively supports streaming
+;; output, making it effortless to use with your preferred text editor.
+;;
+
+;;; Code:
+(require 'project)
+
+(defvar ellama-tools-available nil
+  "Alist containing all registered tools.")
+
+(defvar ellama-tools-enabled nil
+  "List of tools that have been enabled.")
+
+(defvar-local ellama-tools-confirm-allowed (make-hash-table)
+  "Contains hash table of allowed functions.
+Key is a function name symbol.  Value is a boolean t.")
+
+(defun ellama-tools-confirm (prompt function &optional args)
+  "Ask user for confirmation before calling FUNCTION with ARGS.
+PROMPT is the message to display to the user.  FUNCTION is the function
+to call if confirmed.  ARGS are the arguments to pass to FUNCTION.  User
+can approve once (y), approve for all future calls (a), or forbid (n).
+Returns the result of FUNCTION if approved, \"Forbidden by the user\"
+otherwise."
+  (let ((confirmation (gethash function ellama-tools-confirm-allowed nil)))
+    (cond
+     ;; If user has approved all calls, just execute the function
+     ((when confirmation
+        (if args
+            (apply function args)
+          (funcall function))))
+     ;; Otherwise, ask for confirmation
+     (t
+      (let* ((answer (read-char-choice
+                      (format "%s (y)es, (a)lways, (n)o: " prompt)
+                      '(?y ?a ?n)))
+             (result (cond
+                      ;; Yes - execute function once
+                      ((eq answer ?y)
+                       (if args
+                           (apply function args)
+                         (funcall function)))
+                      ;; Always - remember approval and execute function
+                      ((eq answer ?a)
+                       (puthash function t ellama-tools-confirm-allowed)
+                       (if args
+                           (apply function args)
+                         (funcall function)))
+                      ;; No - return nil
+                      ((eq answer ?n)
+                       "Forbidden by the user"))))
+        (if (stringp result)
+            result
+          (json-encode result)))))))
+
+(defun ellama-tools--enable-by-name (name)
+  "Add to `ellama-tools-enabled' each tool that matches NAME."
+  (let* ((tool-name name)
+         (tool (seq-find (lambda (tool) (string= tool-name (llm-tool-name 
tool)))
+                         ellama-tools-available)))
+    (add-to-list 'ellama-tools-enabled tool)
+    nil))
+
+(defun ellama-tools-enable-by-name (&optional name)
+  "Add to `ellama-tools-enabled' each tool that matches NAME."
+  (interactive)
+  (let ((tool-name (or name
+                       (completing-read
+                        "Tool to enable: "
+                        (cl-remove-if
+                         (lambda (tname)
+                           (cl-find-if
+                            (lambda (tool)
+                              (string= tname (llm-tool-name tool)))
+                            ellama-tools-enabled))
+                         (mapcar (lambda (tool) (llm-tool-name tool)) 
ellama-tools-available))))))
+    (ellama-tools--enable-by-name tool-name)))
+
+(defun ellama-tools-enable-by-name-tool (name)
+  "Add to `ellama-tools-enabled' each tool that matches NAME."
+  (ellama-tools-confirm
+   (format "Allow enabling tool %s?" name)
+   'ellama-tools--enable-by-name
+   (list name)))
+
+(add-to-list
+ 'ellama-tools-available
+ (llm-make-tool :function
+                'ellama-tools-enable-by-name-tool
+                :name
+                "enable_tool"
+                :args
+                (list '(:name
+                        "name"
+                        :type
+                        string
+                        :description
+                        "Name of the tool to enable."))
+                :description
+                "Enable each tool that matches NAME. You need to reply to the 
user before using newly enabled tool."))
+
+(defun ellama-tools--disable-by-name (name)
+  "Remove from `ellama-tools-enabled' each tool that matches NAME."
+  (let* ((tool (seq-find (lambda (tool) (string= name (llm-tool-name tool)))
+                         ellama-tools-enabled)))
+    (setq ellama-tools-enabled (seq-remove (lambda (enabled-tool) (eq 
enabled-tool tool))
+                                           ellama-tools-enabled))))
+
+(defun ellama-tools-disable-by-name (&optional name)
+  "Remove from `ellama-tools-enabled' each tool that matches NAME."
+  (interactive)
+  (let* ((tool-name (or name
+                        (completing-read
+                         "Tool to disable: "
+                         (mapcar (lambda (tool) (llm-tool-name tool)) 
ellama-tools-enabled)))))
+    (ellama-tools--disable-by-name tool-name)))
+
+(defun ellama-tools-disable-by-name-tool (name)
+  "Remove from `ellama-tools-enabled' each tool that matches NAME."
+  (ellama-tools-confirm
+   (format "Allow disabling tool %s?" name)
+   'ellama-tools--disable-by-name
+   (list name)))
+
+(add-to-list
+ 'ellama-tools-available
+ (llm-make-tool :function
+                'ellama-tools-disable-by-name-tool
+                :name
+                "disable_tool"
+                :args
+                (list '(:name
+                        "name"
+                        :type
+                        string
+                        :description
+                        "Name of the tool to disable."))
+                :description
+                "Disable each tool that matches NAME."))
+
+(defun ellama-tools-enable-all ()
+  "Enable all available tools."
+  (interactive)
+  (setq ellama-tools-enabled ellama-tools-available))
+
+(defun ellama-tools-disable-all ()
+  "Disable all enabled tools."
+  (interactive)
+  (setq ellama-tools-enabled nil))
+
+(defun ellama-tools--read-file (path)
+  "Read the file located at the specified PATH."
+  (if (not (file-exists-p path))
+      (format "File %s doesn't exists." path)
+    (with-temp-buffer
+      (insert-file-contents-literally path)
+      (buffer-string))))
+
+(defun ellama-tools-read-file (path)
+  "Read the file located at the specified PATH."
+  (ellama-tools-confirm
+   (format "Allow reading file %s?" path)
+   'ellama-tools--read-file
+   (list path)))
+
+(add-to-list
+ 'ellama-tools-available
+ (llm-make-tool :function
+                'ellama-tools-read-file
+                :name
+                "read_file"
+                :args
+                (list '(:name
+                        "path"
+                        :type
+                        string
+                        :description
+                        "Path to the file."))
+                :description
+                "Read the file located at the specified PATH."))
+
+(defun ellama-tools--write-file (path content)
+  "Write CONTENT to the file located at the specified PATH."
+  (with-temp-buffer
+    (insert content)
+    (setq buffer-file-name path)
+    (save-buffer)))
+
+(defun ellama-tools-write-file (path content)
+  "Write CONTENT to the file located at the specified PATH."
+  (ellama-tools-confirm
+   (format "Allow writing file %s?" path)
+   'ellama-tools--write-file
+   (list path content)))
+
+(add-to-list
+ 'ellama-tools-available
+ (llm-make-tool :function
+                'ellama-tools-write-file
+                :name
+                "write_file"
+                :args
+                (list '(:name
+                        "path"
+                        :type
+                        string
+                        :description
+                        "Path to the file.")
+                      '(:name
+                        "content"
+                        :type
+                        string
+                        :description
+                        "Content to write to the file."))
+                :description
+                "Write CONTENT to the file located at the specified PATH."))
+
+(defun ellama-tools--directory-tree (dir &optional depth)
+  "Return a string representing the directory tree under DIR.
+DEPTH is the current recursion depth, used internally."
+  (if (not (file-exists-p dir))
+      (format "Directory %s doesn't exists" dir)
+    (let ((indent (make-string (* (or depth 0) 2) ? ))
+          (tree ""))
+      (dolist (f (sort (cl-remove-if
+                        (lambda (f)
+                          (string-prefix-p "." f))
+                        (directory-files dir))
+                       #'string-lessp))
+        (let* ((full   (expand-file-name f dir))
+               (name   (file-name-nondirectory f))
+               (type   (if (file-directory-p full) "|-" "`-"))
+               (line   (concat indent type name "\n")))
+          (setq tree (concat tree line))
+          (when (file-directory-p full)
+            (setq tree (concat tree
+                               (ellama-tools--directory-tree full (+ (or depth 
0) 1)))))))
+      tree)))
+
+(defun ellama-tools-directory-tree (dir)
+  "Return a string representing the directory tree under DIR."
+  (ellama-tools-confirm
+   (format "Allow LLM to see %s directory tree?" dir)
+   'ellama-tools--directory-tree
+   (list dir nil)))
+
+(add-to-list
+ 'ellama-tools-available
+ (llm-make-tool :function
+                'ellama-tools-directory-tree
+                :name
+                "directory_tree"
+                :args
+                (list '(:name
+                        "dir"
+                        :type
+                        string
+                        :description
+                        "Directory path to generate tree for."))
+                :description
+                "Return a string representing the directory tree under DIR."))
+
+(defun ellama-tools--move-file (path newpath)
+  "Move the file from the specified PATH to the NEWPATH."
+  (if (and (file-exists-p path)
+           (not (file-exists-p newpath)))
+      (progn
+        (rename-file path newpath))
+    (error "Cannot move file: source file does not exist or destination 
already exists")))
+
+(defun ellama-tools-move-file (path newpath)
+  "Move the file from the specified PATH to the NEWPATH."
+  (ellama-tools-confirm
+   (format "Allow moving file %s to %s?" path newpath)
+   'ellama-tools--move-file
+   (list path newpath)))
+
+(add-to-list
+ 'ellama-tools-available
+ (llm-make-tool :function
+                'ellama-tools-move-file
+                :name
+                "move_file"
+                :args
+                (list '(:name
+                        "path"
+                        :type
+                        string
+                        :description
+                        "Current path of the file.")
+                      '(:name
+                        "newpath"
+                        :type
+                        string
+                        :description
+                        "New path for the file."))
+                :description
+                "Move the file from the specified PATH to the NEWPATH."))
+
+(defun ellama-tools--edit-file (path oldcontent newcontent)
+  "Edit file located at PATH.
+Replace OLDCONTENT with NEWCONTENT."
+  (let ((content (with-temp-buffer
+                   (insert-file-contents-literally path)
+                   (buffer-string))))
+    (when (string-match oldcontent content)
+      (with-temp-buffer
+        (insert content)
+        (goto-char (match-beginning 0))
+        (delete-region (match-beginning 0) (match-end 0))
+        (insert newcontent)
+        (write-region (point-min) (point-max) path)))))
+
+(defun ellama-tools-edit-file (path oldcontent newcontent)
+  "Edit file located at PATH.
+Replace OLDCONTENT with NEWCONTENT."
+  (ellama-tools-confirm
+   (format "Allow editing file %s?" path)
+   'ellama-tools--edit-file
+   (list path oldcontent newcontent)))
+
+(add-to-list
+ 'ellama-tools-available
+ (llm-make-tool :function
+                'ellama-tools-edit-file
+                :name
+                "edit_file"
+                :args
+                (list '(:name
+                        "path"
+                        :type
+                        string
+                        :description
+                        "Path to the file.")
+                      '(:name
+                        "oldcontent"
+                        :type
+                        string
+                        :description
+                        "Old content to be replaced.")
+                      '(:name
+                        "newcontent"
+                        :type
+                        string
+                        :description
+                        "New content to replace with."))
+                :description
+                "Edit file located at PATH. Replace OLDCONTENT with 
NEWCONTENT."))
+
+(defun ellama-tools--shell-command (cmd)
+  "Execute shell command CMD."
+  (shell-command-to-string cmd))
+
+(defun ellama-tools-shell-command (cmd)
+  "Execute shell command CMD."
+  (ellama-tools-confirm
+   (format "Allow executing shell command %s?" cmd)
+   'ellama-tools--shell-command
+   (list cmd)))
+
+(add-to-list
+ 'ellama-tools-available
+ (llm-make-tool :function
+                'ellama-tools-shell-command
+                :name
+                "shell_command"
+                :args
+                (list '(:name
+                        "cmd"
+                        :type
+                        string
+                        :description
+                        "Shell command to execute."))
+                :description
+                "Execute shell command CMD."))
+
+(defun ellama-tools--grep (search-string)
+  "Grep SEARCH-STRING in directory files."
+  (shell-command-to-string (format "find . -type f -exec grep --color=never 
-nh -e %s \\{\\} +" search-string)))
+
+(defun ellama-tools-grep (search-string)
+  "Grep SEARCH-STRING in directory files."
+  (ellama-tools-confirm
+   (format "Allow grepping for %s in directory files?" search-string)
+   'ellama-tools--grep
+   (list search-string)))
+
+(add-to-list
+ 'ellama-tools-available
+ (llm-make-tool :function
+                'ellama-tools-grep
+                :name
+                "grep"
+                :args
+                (list '(:name
+                        "search-string"
+                        :type
+                        string
+                        :description
+                        "String to search for."))
+                :description
+                "Grep SEARCH-STRING in directory files."))
+
+(defun ellama-tools--list ()
+  "List all available tools."
+  (json-encode (mapcar
+                (lambda (tool)
+                  `(("name" . ,(llm-tool-name tool))
+                    ("description" . ,(llm-tool-description tool))))
+                ellama-tools-available)))
+
+(defun ellama-tools-list ()
+  "List all available tools."
+  (ellama-tools-confirm
+   "Allow LLM to see available tools?"
+   'ellama-tools--list))
+
+(add-to-list
+ 'ellama-tools-available
+ (llm-make-tool :function
+                'ellama-tools-list
+                :name
+                "list_tools"
+                :args
+                nil
+                :description
+                "List all available tools."))
+
+(defun ellama-tools--search (search-string)
+  "Search available tools that matches SEARCH-STRING."
+  (json-encode
+   (cl-remove-if-not
+    (lambda (item)
+      (or (string-match-p search-string (alist-get "name" item nil nil 
'string=))
+          (string-match-p search-string (alist-get "description" item nil nil 
'string=))))
+    (mapcar
+     (lambda (tool)
+       `(("name" . ,(llm-tool-name tool))
+         ("description" . ,(llm-tool-description tool))))
+     ellama-tools-available))))
+
+(defun ellama-tools-search (search-string)
+  "Search available tools that matches SEARCH-STRING."
+  (ellama-tools-confirm
+   (format "Allow searching tools with pattern %s?" search-string)
+   'ellama-tools--search
+   (list search-string)))
+
+(add-to-list
+ 'ellama-tools-available
+ (llm-make-tool :function
+                'ellama-tools-search
+                :name
+                "search_tools"
+                :args
+                (list '(:name
+                        "search-string"
+                        :type
+                        string
+                        :description
+                        "String to search for in tool names or descriptions."))
+                :description
+                "Search available tools that matches SEARCH-STRING."))
+
+(defun ellama-tools--today ()
+  "Return current date."
+  (format-time-string "%Y-%m-%d"))
+
+(defun ellama-tools-today ()
+  "Return current date."
+  (ellama-tools-confirm
+   "Allow reading current date?"
+   'ellama-tools--today))
+
+(add-to-list
+ 'ellama-tools-available
+ (llm-make-tool :function
+                'ellama-tools-today
+                :name
+                "today"
+                :args
+                nil
+                :description
+                "Return current date."))
+
+(defun ellama-tools--now ()
+  "Return current date, time and timezone."
+  (format-time-string "%Y-%m-%d %H:%M:%S %Z"))
+
+(defun ellama-tools-now ()
+  "Return current date, time and timezone."
+  (ellama-tools-confirm
+   "Allow reading current date, time and timezone?"
+   'ellama-tools--now))
+
+(add-to-list
+ 'ellama-tools-available
+ (llm-make-tool :function
+                'ellama-tools-now
+                :name
+                "now"
+                :args
+                nil
+                :description
+                "Return current date, time and timezone."))
+
+(defun ellama-tools--project-root ()
+  "Return current project root directory."
+  (when (project-current)
+    (project-root (project-current))))
+
+(defun ellama-tools-project-root ()
+  "Return current project root directory."
+  (ellama-tools-confirm
+   "Allow LLM to know the project root directory?"
+   'ellama-tools--project-root))
+
+(add-to-list
+ 'ellama-tools-available
+ (llm-make-tool :function
+                'ellama-tools-project-root
+                :name
+                "project_root"
+                :args
+                nil
+                :description
+                "Return current project root directory."))
+
+(provide 'ellama-tools)
+;;; ellama-tools.el ends here
diff --git a/ellama-transient.el b/ellama-transient.el
index 81c9a56feb..77a9d6a445 100644
--- a/ellama-transient.el
+++ b/ellama-transient.el
@@ -430,6 +430,28 @@ ARGS used for transient arguments."
     ("k" "Kill" ellama-kill-current-buffer)
     ("q" "Quit" transient-quit-one)]])
 
+;;;###autoload (autoload 'ellama-transient-tools-menu "ellama-transient" nil t)
+(transient-define-prefix ellama-transient-tools-menu ()
+  ["Tools Commands"
+   :description (lambda ()
+                  (format "Enabled tools:\n%s"
+                          (string-join (mapcar (lambda (tool)
+                                                 (llm-tool-name tool))
+                                               ellama-tools-enabled)
+                                       " ")))
+   ["Tools"
+    ("e" "Enable tool" ellama-tools-enable-by-name
+     :transient t)
+    ("E" "Enable all tools" ellama-tools-enable-all
+     :transient t)
+    ("d" "Disable tool" ellama-tools-disable-by-name
+     :transient t)
+    ("D" "Disable all tools" ellama-tools-disable-all
+     :transient t)]
+   ["Quit"
+    ("k" "Kill" ellama-kill-current-buffer)
+    ("q" "Quit" transient-quit-one)]])
+
 (transient-define-suffix ellama-transient-chat (&optional args)
   "Chat with Ellama.  ARGS used for transient arguments."
   (interactive (list (transient-args transient-current-command)))
@@ -450,7 +472,8 @@ ARGS used for transient arguments."
   ["Main"
    [("c" "Chat" ellama-transient-chat)
     ("b" "Chat with blueprint" ellama-blueprint-select)
-    ("B" "Blueprint Commands" ellama-transient-blueprint-menu)]
+    ("B" "Blueprint Commands" ellama-transient-blueprint-menu)
+    ("T" "Tools Commands" ellama-transient-tools-menu)]
    [("a" "Ask Commands" ellama-transient-ask-menu)
     ("C" "Code Commands" ellama-transient-code-menu)]]
   ["Text"
diff --git a/ellama.el b/ellama.el
index e60167da14..ecc5d6c53f 100644
--- a/ellama.el
+++ b/ellama.el
@@ -6,7 +6,7 @@
 ;; URL: http://github.com/s-kostyaev/ellama
 ;; Keywords: help local tools
 ;; Package-Requires: ((emacs "28.1") (llm "0.24.0") (plz "0.8") (transient 
"0.7") (compat "29.1"))
-;; Version: 1.9.1
+;; Version: 1.10.0
 ;; SPDX-License-Identifier: GPL-3.0-or-later
 ;; Created: 8th Oct 2023
 
@@ -40,6 +40,7 @@
 (require 'llm-provider-utils)
 (require 'compat)
 (eval-when-compile (require 'rx))
+(require 'ellama-tools)
 
 (defgroup ellama nil
   "Tool for interacting with LLMs."
@@ -859,7 +860,8 @@ Defaults to md, but supports org.  Depends on 
`ellama-major-mode'."
   "Create new ellama session with unique id.
 Provided PROVIDER and PROMPT will be used in new session.
 If EPHEMERAL non nil new session will not be associated with any file."
-  (let* ((name (ellama-generate-name provider 'ellama prompt))
+  (let* ((dir default-directory)
+        (name (ellama-generate-name provider 'ellama prompt))
         (count 1)
         (name-with-suffix (format "%s %d" name count))
         (id (if (and (not (ellama-get-session-buffer name))
@@ -889,7 +891,7 @@ If EPHEMERAL non nil new session will not be associated 
with any file."
     (setq ellama--current-session-id id)
     (puthash id buffer ellama--active-sessions)
     (with-current-buffer buffer
-      (setq default-directory ellama-sessions-directory)
+      (setq default-directory dir)
       (funcall ellama-major-mode)
       (setq ellama--current-session session)
       (ellama-session-mode +1))
@@ -1361,6 +1363,66 @@ REASONING-BUFFER is a buffer for reasoning."
              (concat "</think>\n" (string-trim text))
            (string-trim text))))))))
 
+(defun ellama--error-handler (buffer errcb)
+  "Error handler function.
+BUFFER is the current ellama buffer.
+ERRCB is an error callback."
+  (lambda (_ msg)
+    (with-current-buffer buffer
+      (cancel-change-group ellama--change-group)
+      (when ellama-spinner-enabled
+       (spinner-stop))
+      (funcall errcb msg)
+      (setq ellama--current-request nil)
+      (ellama-request-mode -1))))
+
+(defun ellama--response-handler (handler reasoning-buffer buffer donecb errcb 
provider llm-prompt async)
+  "Response handler function.
+HANDLER handles text insertion.
+REASONING-BUFFER used for reasoning output.
+BUFFER is the current ellama buffer.
+DONECB is a done callback.
+PROVIDER is an llm provider.
+ERRCB is an error callback.
+LLM-PROMPT is current llm prompt.
+ASYNC flag is for asyncronous requests."
+  (lambda (response)
+    (let ((text (plist-get response :text))
+         (reasoning (plist-get response :reasoning))
+         (tool-result (plist-get response :tool-results)))
+      (if tool-result
+         (if async
+             (llm-chat-async
+              provider
+              llm-prompt
+              (ellama--response-handler handler reasoning-buffer buffer donecb 
errcb provider llm-prompt async)
+              (ellama--error-handler buffer errcb)
+              t)
+           (llm-chat-streaming
+            provider
+            llm-prompt
+            handler
+            (ellama--response-handler handler reasoning-buffer buffer donecb 
errcb provider llm-prompt async)
+            (ellama--error-handler buffer errcb)
+            t))
+       (funcall handler response)
+       (when (or ellama--current-session
+                 (not reasoning))
+         (kill-buffer reasoning-buffer))
+       (with-current-buffer buffer
+         (accept-change-group ellama--change-group)
+         (when ellama-spinner-enabled
+           (spinner-stop))
+         (if (and (listp donecb)
+                  (functionp (car donecb)))
+             (mapc (lambda (fn) (funcall fn text))
+                   donecb)
+           (funcall donecb text))
+         (when ellama-session-hide-org-quotes
+           (ellama-collapse-org-quotes))
+         (setq ellama--current-request nil)
+         (ellama-request-mode -1))))))
+
 (defun ellama-stream (prompt &rest args)
   "Query ellama for PROMPT.
 ARGS contains keys for fine control.
@@ -1430,8 +1492,10 @@ failure (with BUFFER current).
                                  system 'system))
                               (ellama-session-prompt session))
                           (setf (ellama-session-prompt session)
-                                (llm-make-chat-prompt prompt-with-ctx :context 
system)))
-                      (llm-make-chat-prompt prompt-with-ctx :context system))))
+                                (llm-make-chat-prompt prompt-with-ctx :context 
system
+                                                      :tools 
ellama-tools-enabled)))
+                      (llm-make-chat-prompt prompt-with-ctx :context system
+                                            :tools ellama-tools-enabled))))
     (with-current-buffer reasoning-buffer
       (org-mode))
     (with-current-buffer buffer
@@ -1450,67 +1514,15 @@ failure (with BUFFER current).
                          ('async (llm-chat-async
                                   provider
                                   llm-prompt
-                                  (lambda (response)
-                                    (let ((text (plist-get response :text))
-                                          (reasoning (plist-get response 
:reasoning)))
-                                      (funcall handler response)
-                                      (when (or ellama--current-session
-                                                (not reasoning))
-                                        (kill-buffer reasoning-buffer))
-                                      (with-current-buffer buffer
-                                        (accept-change-group 
ellama--change-group)
-                                        (when ellama-spinner-enabled
-                                          (spinner-stop))
-                                        (if (and (listp donecb)
-                                                 (functionp (car donecb)))
-                                            (mapc (lambda (fn) (funcall fn 
text))
-                                                  donecb)
-                                          (funcall donecb text))
-                                        (when ellama-session-hide-org-quotes
-                                          (ellama-collapse-org-quotes))
-                                        (setq ellama--current-request nil)
-                                        (ellama-request-mode -1))))
-                                  (lambda (_ msg)
-                                    (with-current-buffer buffer
-                                      (cancel-change-group 
ellama--change-group)
-                                      (when ellama-spinner-enabled
-                                        (spinner-stop))
-                                      (funcall errcb msg)
-                                      (setq ellama--current-request nil)
-                                      (ellama-request-mode -1)))
+                                  (ellama--response-handler handler 
reasoning-buffer buffer donecb errcb provider llm-prompt t)
+                                  (ellama--error-handler buffer errcb)
                                   t))
                          ('streaming (llm-chat-streaming
                                       provider
                                       llm-prompt
                                       handler
-                                      (lambda (response)
-                                        (let ((text (plist-get response :text))
-                                              (reasoning (plist-get response 
:reasoning)))
-                                          (funcall handler response)
-                                          (when (or ellama--current-session
-                                                    (not reasoning))
-                                            (kill-buffer reasoning-buffer))
-                                          (with-current-buffer buffer
-                                            (accept-change-group 
ellama--change-group)
-                                            (when ellama-spinner-enabled
-                                              (spinner-stop))
-                                            (if (and (listp donecb)
-                                                     (functionp (car donecb)))
-                                                (mapc (lambda (fn) (funcall fn 
text))
-                                                      donecb)
-                                              (funcall donecb text))
-                                            (when 
ellama-session-hide-org-quotes
-                                              (ellama-collapse-org-quotes))
-                                            (setq ellama--current-request nil)
-                                            (ellama-request-mode -1))))
-                                      (lambda (_ msg)
-                                        (with-current-buffer buffer
-                                          (cancel-change-group 
ellama--change-group)
-                                          (when ellama-spinner-enabled
-                                            (spinner-stop))
-                                          (funcall errcb msg)
-                                          (setq ellama--current-request nil)
-                                          (ellama-request-mode -1)))
+                                      (ellama--response-handler handler 
reasoning-buffer buffer donecb errcb provider llm-prompt nil)
+                                      (ellama--error-handler buffer errcb)
                                       t))
                          ((pred integerp)
                           (let* ((cnt 0)
@@ -1525,34 +1537,8 @@ failure (with BUFFER current).
                              provider
                              llm-prompt
                              skip-handler
-                             (lambda (response)
-                               (let ((text (plist-get response :text))
-                                     (reasoning (plist-get response 
:reasoning)))
-                                 (funcall handler response)
-                                 (when (or ellama--current-session
-                                           (not reasoning))
-                                   (kill-buffer reasoning-buffer))
-                                 (with-current-buffer buffer
-                                   (accept-change-group ellama--change-group)
-                                   (when ellama-spinner-enabled
-                                     (spinner-stop))
-                                   (if (and (listp donecb)
-                                            (functionp (car donecb)))
-                                       (mapc (lambda (fn) (funcall fn text))
-                                             donecb)
-                                     (funcall donecb text))
-                                   (when ellama-session-hide-org-quotes
-                                     (ellama-collapse-org-quotes))
-                                   (setq ellama--current-request nil)
-                                   (ellama-request-mode -1))))
-                             (lambda (_ msg)
-                               (with-current-buffer buffer
-                                 (cancel-change-group ellama--change-group)
-                                 (when ellama-spinner-enabled
-                                   (spinner-stop))
-                                 (funcall errcb msg)
-                                 (setq ellama--current-request nil)
-                                 (ellama-request-mode -1)))
+                             (ellama--response-handler handler 
reasoning-buffer buffer donecb errcb provider llm-prompt t)
+                             (ellama--error-handler buffer errcb)
                              t))))))
          (with-current-buffer buffer
            (setq ellama--current-request request)))))))
diff --git a/ellama.info b/ellama.info
index 75001aa9fa..08b080ed55 100644
--- a/ellama.info
+++ b/ellama.info
@@ -326,6 +326,18 @@ File: ellama.info,  Node: Commands,  Next: Keymap,  Prev: 
Installation,  Up: Top
      resource usage.
    • ‘ellama-blueprint-variable-regexp’: Regular expression to match
      blueprint variables like ‘{var_name}’.
+   • ‘ellama-tools-enable-by-name’: Enable a specific tool by its name.
+     Use this command to activate individual tools.  Requires the tool
+     name as input.
+   • ‘ellama-tools-enable-all’: Enable all available tools at once.  Use
+     this command to activate every tool in the system for comprehensive
+     functionality without manual selection.
+   • ‘ellama-tools-disable-by-name’: Disable a specific tool by its
+     name.  Use this command to deactivate individual tools when their
+     functionality is no longer needed.
+   • ‘ellama-tools-disable-all’: Disable all enabled tools
+     simultaneously.  Use this command to reset the system to a minimal
+     state, ensuring no tools are active.
 
 
 File: ellama.info,  Node: Keymap,  Next: Configuration,  Prev: Commands,  Up: 
Top
@@ -1424,33 +1436,33 @@ Tag Table:
 Node: Top1379
 Node: Installation3613
 Node: Commands8621
-Node: Keymap15330
-Node: Configuration18163
-Node: Context Management23762
-Node: Transient Menus for Context Management24670
-Node: Managing the Context26284
-Node: Considerations27059
-Node: Minor modes27652
-Node: ellama-context-header-line-mode29640
-Node: ellama-context-header-line-global-mode30465
-Node: ellama-context-mode-line-mode31185
-Node: ellama-context-mode-line-global-mode32033
-Node: Ellama Session Header Line Mode32737
-Node: Enabling and Disabling33306
-Node: Customization33753
-Node: Ellama Session Mode Line Mode34041
-Node: Enabling and Disabling (1)34626
-Node: Customization (1)35073
-Node: Using Blueprints35367
-Node: Key Components of Ellama Blueprints35986
-Node: Creating and Managing Blueprints36593
-Node: Variable Management37574
-Node: Keymap and Mode38043
-Node: Transient Menus38979
-Node: Running Blueprints programmatically39525
-Node: Acknowledgments40112
-Node: Contributions40825
-Node: GNU Free Documentation License41209
+Node: Keymap16060
+Node: Configuration18893
+Node: Context Management24492
+Node: Transient Menus for Context Management25400
+Node: Managing the Context27014
+Node: Considerations27789
+Node: Minor modes28382
+Node: ellama-context-header-line-mode30370
+Node: ellama-context-header-line-global-mode31195
+Node: ellama-context-mode-line-mode31915
+Node: ellama-context-mode-line-global-mode32763
+Node: Ellama Session Header Line Mode33467
+Node: Enabling and Disabling34036
+Node: Customization34483
+Node: Ellama Session Mode Line Mode34771
+Node: Enabling and Disabling (1)35356
+Node: Customization (1)35803
+Node: Using Blueprints36097
+Node: Key Components of Ellama Blueprints36716
+Node: Creating and Managing Blueprints37323
+Node: Variable Management38304
+Node: Keymap and Mode38773
+Node: Transient Menus39709
+Node: Running Blueprints programmatically40255
+Node: Acknowledgments40842
+Node: Contributions41555
+Node: GNU Free Documentation License41939
 
 End Tag Table
 


Reply via email to