diff options
Diffstat (limited to 'llm-tools-web.el')
| -rw-r--r-- | llm-tools-web.el | 118 |
1 files changed, 118 insertions, 0 deletions
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) | ||
