gptel-kagi.el (7923B)
1 ;;; gptel-kagi.el --- Kagi support for gptel -*- lexical-binding: t; -*- 2 3 ;; Copyright (C) 2023 Karthik Chikmagalur 4 5 ;; Author: Karthik Chikmagalur <karthikchikmagalur@gmail.com> 6 ;; Keywords: hypermedia 7 8 ;; This program is free software; you can redistribute it and/or modify 9 ;; it under the terms of the GNU General Public License as published by 10 ;; the Free Software Foundation, either version 3 of the License, or 11 ;; (at your option) any later version. 12 13 ;; This program is distributed in the hope that it will be useful, 14 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 15 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 ;; GNU General Public License for more details. 17 18 ;; You should have received a copy of the GNU General Public License 19 ;; along with this program. If not, see <https://www.gnu.org/licenses/>. 20 21 ;;; Commentary: 22 23 ;; This file adds support for the Kagi FastGPT LLM API to gptel 24 25 ;;; Code: 26 (require 'gptel) 27 (require 'cl-generic) 28 (eval-when-compile 29 (require 'cl-lib)) 30 31 (declare-function gptel-context--wrap "gptel-context") 32 33 ;;; Kagi 34 (cl-defstruct (gptel-kagi (:constructor gptel--make-kagi) 35 (:copier nil) 36 (:include gptel-backend))) 37 38 (cl-defmethod gptel--parse-response ((_backend gptel-kagi) response info) 39 (let* ((data (plist-get response :data)) 40 (output (plist-get data :output)) 41 (references (plist-get data :references))) 42 (when references 43 (setq references 44 (cl-loop with linker = 45 (pcase (buffer-local-value 'major-mode 46 (plist-get info :buffer)) 47 ('org-mode 48 (lambda (text url) 49 (format "[[%s][%s]]" url text))) 50 ('markdown-mode 51 (lambda (text url) 52 (format "[%s](%s)" text url))) 53 (_ (lambda (text url) 54 (buttonize 55 text (lambda (data) (browse-url data)) 56 url)))) 57 for ref across references 58 for title = (plist-get ref :title) 59 for snippet = (plist-get ref :snippet) 60 for url = (plist-get ref :url) 61 for n upfrom 1 62 collect 63 (concat (format "[%d] " n) 64 (funcall linker title url) ": " 65 (replace-regexp-in-string 66 "</?b>" "*" snippet)) 67 into ref-strings 68 finally return 69 (concat "\n\n" (mapconcat #'identity ref-strings "\n"))))) 70 (concat output references))) 71 72 ;; TODO: Add model and backend-specific request-params support 73 (cl-defmethod gptel--request-data ((_backend gptel-kagi) prompts) 74 "JSON encode PROMPTS for Kagi." 75 (pcase-exhaustive (gptel--model-name gptel-model) 76 ("fastgpt" 77 `(,@prompts :web_search t :cache t)) 78 ((and model (guard (string-prefix-p "summarize" model))) 79 `(,@prompts :engine ,(substring model 10))))) 80 81 (cl-defmethod gptel--parse-buffer ((_backend gptel-kagi) &optional _max-entries) 82 (let ((url (or (thing-at-point 'url) 83 (get-text-property (point) 'shr-url) 84 (get-text-property (point) 'image-url))) 85 ;; (filename (thing-at-point 'existing-filename)) ;no file upload support yet 86 (prop (text-property-search-backward 87 'gptel 'response 88 (when (get-char-property (max (point-min) (1- (point))) 89 'gptel) 90 t)))) 91 (if (and url (string-prefix-p "summarize" (gptel--model-name gptel-model))) 92 (list :url url) 93 (if (and (or gptel-mode gptel-track-response) 94 (prop-match-p prop) 95 (prop-match-value prop)) 96 (user-error "No user prompt found!") 97 (let ((prompts 98 (if (or gptel-mode gptel-track-response) 99 (string-trim 100 (buffer-substring-no-properties (prop-match-beginning prop) 101 (prop-match-end prop)) 102 (format "[\t\r\n ]*\\(?:%s\\)?[\t\r\n ]*" 103 (regexp-quote (gptel-prompt-prefix-string))) 104 (format "[\t\r\n ]*\\(?:%s\\)?[\t\r\n ]*" 105 (regexp-quote (gptel-response-prefix-string)))) 106 (string-trim (buffer-substring-no-properties (point-min) (point-max)))))) 107 (pcase-exhaustive (gptel--model-name gptel-model) 108 ("fastgpt" (setq prompts (list :query (if (prop-match-p prop) prompts "")))) 109 ((and model (guard (string-prefix-p "summarize" model))) 110 ;; If the entire contents of the prompt looks like a url, send the url 111 ;; Else send the text of the region 112 (setq prompts 113 (if-let (((prop-match-p prop)) 114 (engine (substring model 10))) 115 ;; It's a region of text 116 (list :text prompts) 117 "")))) 118 prompts))))) 119 120 (cl-defmethod gptel--wrap-user-prompt ((_backend gptel-kagi) prompts) 121 (cond 122 ((plist-get prompts :url) 123 (message "Ignoring gptel context for URL summary request.")) 124 ((plist-get prompts :query) 125 (cl-callf gptel-context--wrap (plist-get prompts :query))) 126 ((plist-get prompts :text) 127 (cl-callf gptel-context--wrap (plist-get prompts :text))))) 128 129 ;;;###autoload 130 (cl-defun gptel-make-kagi 131 (name &key curl-args stream key 132 (host "kagi.com") 133 (header (lambda () `(("Authorization" . ,(concat "Bot " (gptel--get-api-key)))))) 134 (models '((fastgpt :capabilities (nosystem)) 135 (summarize:cecil :capabilities (nosystem)) 136 (summarize:agnes :capabilities (nosystem)) 137 (summarize:daphne :capabilities (nosystem)) 138 (summarize:muriel :capabilities (nosystem)))) 139 (protocol "https") 140 (endpoint "/api/v0/")) 141 "Register a Kagi FastGPT backend for gptel with NAME. 142 143 Keyword arguments: 144 145 CURL-ARGS (optional) is a list of additional Curl arguments. 146 147 HOST is the Kagi host (with port), defaults to \"kagi.com\". 148 149 MODELS is a list of available Kagi models: only fastgpt is supported. 150 151 STREAM is a boolean to toggle streaming responses, defaults to 152 false. Kagi does not support a streaming API yet. 153 154 PROTOCOL (optional) specifies the protocol, https by default. 155 156 ENDPOINT (optional) is the API endpoint for completions, defaults to 157 \"/api/v0/fastgpt\". 158 159 HEADER (optional) is for additional headers to send with each 160 request. It should be an alist or a function that retuns an 161 alist, like: 162 ((\"Content-Type\" . \"application/json\")) 163 164 KEY (optional) is a variable whose value is the API key, or 165 function that returns the key. 166 167 Example: 168 ------- 169 170 (gptel-make-kagi \"Kagi\" :key my-kagi-key)" 171 (declare (indent 1)) 172 stream ;Silence byte-compiler 173 (let ((backend (gptel--make-kagi 174 :curl-args curl-args 175 :name name 176 :host host 177 :header header 178 :key key 179 :models (gptel--process-models models) 180 :protocol protocol 181 :endpoint endpoint 182 :url 183 (lambda () 184 (concat protocol "://" host endpoint 185 (if (equal gptel-model 'fastgpt) 186 "fastgpt" "summarize")))))) 187 (prog1 backend 188 (setf (alist-get name gptel--known-backends 189 nil nil #'equal) 190 backend)))) 191 192 (provide 'gptel-kagi) 193 ;;; gptel-kagi.el ends here