diff options
| -rw-r--r-- | llm-tools-hl.el | 2 | ||||
| -rw-r--r-- | llm-tools-web.el | 118 | ||||
| -rw-r--r-- | llm-tools.el | 42 |
3 files changed, 160 insertions, 2 deletions
diff --git a/llm-tools-hl.el b/llm-tools-hl.el index c682334..4bf3a6e 100644 --- a/llm-tools-hl.el +++ b/llm-tools-hl.el | |||
| @@ -105,3 +105,5 @@ If INCLUDE-CONTENT is non-nil, append `|LINE' after each hash." | |||
| 105 | ;; maybe if it just shifted a little bit (since the anchor is line number and hash of the line's content), there can be some kind of autohealing? | 105 | ;; maybe if it just shifted a little bit (since the anchor is line number and hash of the line's content), there can be some kind of autohealing? |
| 106 | ;; rather, the llm would not format it like the reads. it's just going to provide text according to what the anchors it gives say | 106 | ;; rather, the llm would not format it like the reads. it's just going to provide text according to what the anchors it gives say |
| 107 | ;; so I just need to verify if the anchors didn't change | 107 | ;; so I just need to verify if the anchors didn't change |
| 108 | |||
| 109 | (provide 'llm-tools-hl) | ||
diff --git a/llm-tools-web.el b/llm-tools-web.el new file mode 100644 index 0000000..7002713 --- /dev/null +++ b/llm-tools-web.el | |||
| @@ -0,0 +1,118 @@ | |||
| 1 | ;;; Web tools, taken from gptel-agent | ||
| 2 | (defun gptel-agent--fetch-with-timeout (url url-cb tool-cb failed-msg &rest args) | ||
| 3 | "Fetch URL and call URL-CB in the result buffer. | ||
| 4 | |||
| 5 | Call TOOL-CB if there is an error or a timeout. TOOL-CB and ARGS are | ||
| 6 | passed to URL-CB. FAILED-MSG is a fragment used for messaging. Handles | ||
| 7 | cleanup." | ||
| 8 | (let* ((timeout 30) timer done | ||
| 9 | (inherit-process-coding-system t) | ||
| 10 | (proc-buffer | ||
| 11 | (url-retrieve | ||
| 12 | url (lambda (status) | ||
| 13 | (setq done t) | ||
| 14 | (when timer (cancel-timer timer)) | ||
| 15 | (if-let* ((err (plist-get status :error))) | ||
| 16 | (funcall tool-cb | ||
| 17 | (format "Error: %s failed with error: %S" failed-msg err)) | ||
| 18 | (apply url-cb tool-cb args)) | ||
| 19 | (kill-buffer (current-buffer))) | ||
| 20 | args 'silent))) | ||
| 21 | (setq timer | ||
| 22 | (run-at-time | ||
| 23 | timeout nil | ||
| 24 | (lambda (buf cb) | ||
| 25 | (unless done | ||
| 26 | (setq done t) | ||
| 27 | (let ((kill-buffer-query-functions)) (kill-buffer buf)) | ||
| 28 | (funcall | ||
| 29 | cb (format "Error: %s timed out after %d seconds." | ||
| 30 | failed-msg timeout)))) | ||
| 31 | proc-buffer tool-cb)) | ||
| 32 | proc-buffer)) | ||
| 33 | |||
| 34 | ;;;; Web Searching, taken from gptel-agent | ||
| 35 | (defun llm-tools--shr-next-link () | ||
| 36 | "Jump to the next SHR link in the buffer. Return jump position." | ||
| 37 | (let ((current-prop (get-char-property (point) 'shr-url)) | ||
| 38 | (next-pos (point))) | ||
| 39 | (while (and (not (eobp)) | ||
| 40 | (setq next-pos | ||
| 41 | (or (next-single-property-change (point) 'shr-url) | ||
| 42 | (point-max))) | ||
| 43 | (let ((next-prop (get-char-property next-pos 'shr-url))) | ||
| 44 | (or (equal next-prop current-prop) | ||
| 45 | (equal next-prop nil)))) | ||
| 46 | (goto-char next-pos)) | ||
| 47 | (goto-char next-pos))) | ||
| 48 | |||
| 49 | (defvar llm-tools--web-search-active nil) | ||
| 50 | |||
| 51 | (defun llm-tools--web-search-eww (tool-cb query &optional count) | ||
| 52 | "Search the web using eww's default search engine (usually DuckDuckGo). | ||
| 53 | |||
| 54 | Call TOOL-CB with the results as a string. QUERY is the search string. | ||
| 55 | COUNT is the number of results to return (default 5)." | ||
| 56 | ;; No more than two active searches at one time | ||
| 57 | (setq llm-tools--web-search-active | ||
| 58 | (cl-delete-if-not | ||
| 59 | (lambda (buf) (and (buffer-live-p buf) | ||
| 60 | (process-live-p (get-buffer-process buf)))) | ||
| 61 | llm-tools--web-search-active)) | ||
| 62 | (if (>= (length llm-tools--web-search-active) 2) | ||
| 63 | (progn (message "Web search: waiting for turn") | ||
| 64 | (run-at-time 5 nil #'llm-tools--web-search-eww | ||
| 65 | tool-cb query count)) | ||
| 66 | (push (llm-tools--fetch-with-timeout | ||
| 67 | (concat eww-search-prefix (url-hexify-string query)) | ||
| 68 | #'llm-tools--web-search-eww-callback | ||
| 69 | tool-cb (format "Web search for \"%s\"" query)) | ||
| 70 | llm-tools--web-search-active))) | ||
| 71 | |||
| 72 | (defun llm-tools--web-search-eww-callback (cb) | ||
| 73 | "Extract website text and run callback CB with it." | ||
| 74 | (let* ((count 5) (results)) | ||
| 75 | (goto-char (point-min)) | ||
| 76 | (goto-char url-http-end-of-headers) | ||
| 77 | (let* ((dom (libxml-parse-html-region (point) (point-max))) | ||
| 78 | (result-count 0)) | ||
| 79 | (eww-score-readability dom) | ||
| 80 | (with-temp-buffer | ||
| 81 | (shr-insert-document (eww-highest-readability dom)) | ||
| 82 | (goto-char (point-min)) | ||
| 83 | (while (and (not (eobp)) (< result-count count)) | ||
| 84 | (let ((pos (point)) | ||
| 85 | (url (get-char-property (point) 'shr-url)) | ||
| 86 | (next-pos (llm-tools--shr-next-link))) | ||
| 87 | (when-let* (((stringp url)) | ||
| 88 | (idx (string-search "http" url)) | ||
| 89 | (url-fmt (url-unhex-string (substring url idx)))) | ||
| 90 | (cl-incf result-count) | ||
| 91 | (push (concat url-fmt "\n\n" | ||
| 92 | (string-trim | ||
| 93 | (buffer-substring-no-properties pos next-pos)) | ||
| 94 | "\n\n----\n") | ||
| 95 | results)))))) | ||
| 96 | (funcall cb (apply #'concat (nreverse results))))) | ||
| 97 | |||
| 98 | ;;;; Read URLs, taken from gptel-agent | ||
| 99 | (defun llm-tools--read-url (tool-cb url) | ||
| 100 | "Fetch URL text and call TOOL-CB with it." | ||
| 101 | (llm-tools--fetch-with-timeout | ||
| 102 | url | ||
| 103 | (lambda (cb) | ||
| 104 | (goto-char (point-min)) (forward-paragraph) | ||
| 105 | (condition-case errdata | ||
| 106 | (let ((dom (libxml-parse-html-region (point) (point-max)))) | ||
| 107 | (with-temp-buffer | ||
| 108 | (eww-score-readability dom) | ||
| 109 | (shr-insert-document (eww-highest-readability dom)) | ||
| 110 | (decode-coding-region (point-min) (point-max) 'utf-8) | ||
| 111 | (funcall | ||
| 112 | cb (buffer-substring-no-properties | ||
| 113 | (point-min) (point-max))))) | ||
| 114 | (error (funcall cb (format "Error: Request failed with error data:\n%S" | ||
| 115 | errdata))))) | ||
| 116 | tool-cb (format "Fetch for \"%s\"" url))) | ||
| 117 | |||
| 118 | (provide 'llm-tools-web) | ||
diff --git a/llm-tools.el b/llm-tools.el index 41f6c56..855f12f 100644 --- a/llm-tools.el +++ b/llm-tools.el | |||
| @@ -1,3 +1,6 @@ | |||
| 1 | (require 'llm-tools-hl) | ||
| 2 | (require 'llm-tools-web) | ||
| 3 | |||
| 1 | ;; read_file (hl-file-read) | 4 | ;; read_file (hl-file-read) |
| 2 | (gptel-make-tool | 5 | (gptel-make-tool |
| 3 | :name "read_file" | 6 | :name "read_file" |
| @@ -203,5 +206,40 @@ working directory. Defaults to current directory if omitted." | |||
| 203 | :confirm nil | 206 | :confirm nil |
| 204 | :include t) | 207 | :include t) |
| 205 | 208 | ||
| 206 | ;; TODO fetch_url (use gptel-agent's one as a base, but needs mime handling like PDFs and images) | 209 | ;; web_search (taken wholesale from gptel-agent) |
| 207 | ;; TODO web_search (use gptel-agent's eww/shr method as a base) | 210 | (gptel-make-tool |
| 211 | :name "web_search" | ||
| 212 | :function #'llm-tools--web-search-eww | ||
| 213 | :description "Search the web for the first five results to a query. The query can be an arbitrary string. Returns the top five results from the search engine as a list of plists. Each object has the keys `:url` and `:excerpt` for the corresponding search result. | ||
| 214 | |||
| 215 | This tool uses the Emacs web browser (eww) with its default search engine to perform searches. No API key is required. | ||
| 216 | |||
| 217 | If required, consider using the url as the input to the `Read` tool to get the contents of the url. Note that this might not work as the `Read` tool does not handle javascript-enabled pages." | ||
| 218 | :args '((:name "query" | ||
| 219 | :type string | ||
| 220 | :description "The natural language search query, can be multiple words.") | ||
| 221 | (:name "count" | ||
| 222 | :type integer | ||
| 223 | :description "Number of results to return (default 5)" | ||
| 224 | :optional t)) | ||
| 225 | :include t | ||
| 226 | :confirm nil | ||
| 227 | :async t) | ||
| 228 | |||
| 229 | ;; fetch_url (use gptel-agent's one as a base, but adds mime handling like PDFs and images) | ||
| 230 | ; TODO add mime handling for documents and images | ||
| 231 | (gptel-make-tool | ||
| 232 | :function #'llm-tools--read-url | ||
| 233 | :name "web_fetch" | ||
| 234 | :description "Fetch and read the contents of a URL. | ||
| 235 | |||
| 236 | - Returns the text of the URL (not HTML) formatted for reading. | ||
| 237 | - Request times out after 30 seconds." | ||
| 238 | :args '(( :name "url" | ||
| 239 | :type "string" | ||
| 240 | :description "The URL to read")) | ||
| 241 | :async t | ||
| 242 | :confirm nil | ||
| 243 | :include t) | ||
| 244 | |||
| 245 | (provide 'llm-tools) | ||
