summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorVineet Kumar <git@vineetk.net>2026-05-24 16:33:52 -0400
committerVineet Kumar <git@vineetk.net>2026-05-24 17:07:06 -0400
commitac814c732425644ec0b4368f643af8d800782941 (patch)
tree7c1b3672fdc142076c9115210fad0173cbc8e326
parentecf082bd0a47da1436e4f8b482f6b92f10f0b924 (diff)
import web_search and web_fetch wholesale from gptel-agent
-rw-r--r--llm-tools-hl.el2
-rw-r--r--llm-tools-web.el118
-rw-r--r--llm-tools.el42
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
5Call TOOL-CB if there is an error or a timeout. TOOL-CB and ARGS are
6passed to URL-CB. FAILED-MSG is a fragment used for messaging. Handles
7cleanup."
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
54Call TOOL-CB with the results as a string. QUERY is the search string.
55COUNT 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
215This tool uses the Emacs web browser (eww) with its default search engine to perform searches. No API key is required.
216
217If 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)