config

Personal configuration.
git clone git://code.dwrz.net/config
Log | Files | Refs

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