1
0
Fork 0
kagi.el/kagi.el
2023-12-25 12:34:42 +01:00

310 lines
11 KiB
EmacsLisp

;;; kagi.el --- Kagi API integration -*- lexical-binding: t; -*-
;; Copyright (C) 2023 Bram Schoenmakers
;; Author: Bram Schoenmakers <me@bramschoenmakers.nl>
;; Maintainer: Bram Schoenmakers <me@bramschoenmakers.nl>
;; Created: 16 Dec 2023
;; Package-Version: 0.1
;; Package-Requires: ((emacs "28.2") (shell-maker "0.44.1"))
;; Keywords: convenience
;; URL: https://codeberg.org/bram85/kagi.el
;; This file is not part of GNU Emacs.
;; MIT License
;; Copyright (c) 2023 Bram Schoenmakers
;; Permission is hereby granted, free of charge, to any person obtaining a copy
;; of this software and associated documentation files (the "Software"), to deal
;; in the Software without restriction, including without limitation the rights
;; to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
;; copies of the Software, and to permit persons to whom the Software is
;; furnished to do so, subject to the following conditions:
;; The above copyright notice and this permission notice shall be included in all
;; copies or substantial portions of the Software.
;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
;; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
;; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
;; AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
;; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
;; OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
;; SOFTWARE.
;;; Commentary:
;; This package provides a shell to submit prompts to FastGPT, inspired by
;; [xenodium's chatgpt-shell].
;;
;; Kagi is a relatively new ad-free search engine, offering additional
;; services such as the [Universal Summarizer] or more notably [FastGPT],
;; their open source LLM offering. Some functionality is provided through
;; an API.
;;
;; [xenodium's chatgpt-shell] <https://github.com/xenodium/chatgpt-shell>
;;
;; [Universal Summarizer] <https://kagi.com/summarizer>
;;
;; [FastGPT] <https://kagi.com/fastgpt>
;;
;; API documentation: https://help.kagi.com/kagi/api/fastgpt.html
;;; Code:
(require 'shell-maker)
(defcustom kagi-api-token nil
"The Kagi API token.
The token can be generated inside your account at
https://kagi.com/settings?p=api"
:type '(choice string function)
:group 'kagi)
(defcustom kagi-api-fastgpt-url "https://kagi.com/api/v0/fastgpt"
"The Kagi FastGPT API entry point."
:type '(choice string function)
:group 'kagi)
(defcustom kagi-api-summarizer-url "https://kagi.com/api/v0/summarize"
"The Kagi Summarizer API entry point."
:type '(choice string function)
:group 'kagi)
(defcustom kagi-api-summarizer-engine "cecil"
"Which summary engine to use."
:type '(choice
(const "agnes")
(const "cecil")
(const "daphne")
(const "muriel"))
:group 'kagi)
(defcustom kagi-api-summarize-default-language nil
"Default target language of the summary."
:type '(choice
(const :tag "Document language" nil)
(const :tag "Bulgarian" "BG")
(const :tag "Czech" "CZ")
(const :tag "Danish" "DA")
(const :tag "German" "DE")
(const :tag "Greek" "EL")
(const :tag "English" "EN")
(const :tag "Spanish" "ES")
(const :tag "Estonian" "ET")
(const :tag "Finnish" "FI")
(const :tag "French" "FR")
(const :tag "Hungarian" "HU")
(const :tag "Indonesian" "ID")
(const :tag "Italian" "IT")
(const :tag "Japanese" "JA")
(const :tag "Korean" "KO")
(const :tag "Lithuanian" "LT")
(const :tag "Latvian" "LV")
(const :tag "Norwegian" "NB")
(const :tag "Dutch" "NL")
(const :tag "Polish" "PL")
(const :tag "Portuguese" "PT")
(const :tag "Romanian" "RO")
(const :tag "Russian" "RU")
(const :tag "Slovak" "SK")
(const :tag "Slovenian" "SL")
(const :tag "Swedish" "SV")
(const :tag "Turkish" "TR")
(const :tag "Ukrainian" "UK")
(const :tag "Chinese (simplified)" "ZH"))
:group 'kagi)
(defun kagi--curl-flags ()
"Collect flags for a `curl' command to call the Kagi API."
(let ((token (cond ((functionp kagi-api-token) (funcall kagi-api-token))
((stringp kagi-api-token) kagi-api-token)
(t (error "No API token configured in variable kagi-api-token")))))
`("--silent"
"--header" ,(format "Authorization: Bot %s" token)
"--header" "Content-Type: application/json"
"--data" "@-")))
(defun kagi--html-bold-to-face (string)
"Convert HTML tags inside STRING to faces.
Fun fact: initial version of this function was generated by
FastGPT with the following prompt:
write an Emacs Lisp function that accepts a string with html
bold tags, and returns a string with bold face text properties
applied for the tags content atd the tags removed"
(with-temp-buffer
(insert string)
(goto-char (point-min))
(let ((bold-match (rx (seq "<b>" (group (* (not "<"))) "</b>"))))
(while (re-search-forward bold-match nil t)
(replace-match (propertize (match-string 1) 'font-lock-face 'bold) t nil)))
(buffer-string)))
(defun kagi--format-output (output)
"Format the OUTPUT by replacing markup elements to proper faces."
(kagi--html-bold-to-face output))
(defun kagi--format-reference-index (i)
"Format the index of reference number I."
(propertize (format "[%d]" i) 'font-lock-face 'bold))
(defun kagi--format-references (references)
"Format the REFRENCES as a string.
The REFERENCES is a part of the JSON response, see
https://help.kagi.com/kagi/api/fastgpt.html for more information."
(string-join
(seq-map-indexed (lambda (ref i)
(let ((title (gethash "title" ref))
(snippet (gethash "snippet" ref))
(url (gethash "url" ref)))
(format "%s %s\n%s\n%s"
(kagi--format-reference-index (1+ i))
title
(kagi--html-bold-to-face snippet) url)))
references)
"\n\n"))
(defun kagi--call-fastgpt (prompt)
"Submit the given PROMPT to the FastGPT API.
Returns the JSON response as a string. See
https://help.kagi.com/kagi/api/fastgpt.html for the
interpretation."
(with-temp-buffer
(insert (json-encode `((query . ,prompt))))
(let* ((call-process-flags '(nil nil "curl" t t nil))
(curl-flags (kagi--curl-flags))
(all-flags (append call-process-flags
curl-flags
(list kagi-api-fastgpt-url)))
(return (apply #'call-process-region all-flags)))
(if (eql return 0)
(buffer-string)
(error "Call to FastGPT API returned with status %s" return)))))
(defun kagi--call-summarizer (obj)
"Submit the given TEXT to the Summarizer API.
Returns the JSON response as a string. See
https://help.kagi.com/kagi/api/summarizer.html for the
interpretation."
(with-temp-buffer
(insert (json-encode obj))
(let* ((call-process-flags '(nil nil "curl" t t nil))
(curl-flags (kagi--curl-flags))
(all-flags (append call-process-flags
curl-flags
(list kagi-api-summarizer-url)))
(return (apply #'call-process-region all-flags)))
(if (eql return 0)
(buffer-string)
(error "Call to Summarizer API returned with status %s" return)))))
(defun kagi--build-summarizer-request-object (items)
"Build a request object for a summary.
Common request elements are returned based on the package's
configuration. The given ITEMS are appended to it, which is a
list of conses."
(append items
`(("engine" . ,kagi-api-summarizer-engine)
("summary_type" . "summary"))
;; prevent a nil in the result list, causing (json-encode)
;; to generate a wrong request object.
(when kagi-api-summarize-default-language
`(("target_language" . kagi-api-summarize-default-language)))))
(defun kagi--call-text-summarizer (text)
"Return a response object from the Summarizer with the TEXT summary."
(let ((request-obj (kagi--build-summarizer-request-object
`(("text" . ,text)))))
(kagi--call-summarizer request-obj )))
(defun kagi--call-url-summarizer (url)
"Return a response object from the Summarizer with the URL summary."
(let ((request-obj (kagi--build-summarizer-request-object
`(("url" . ,url)))))
(kagi--call-summarizer request-obj)))
(defun kagi--get-summary (f)
(let* ((response (funcall f))
(parsed-response (json-parse-string response))
(data (gethash "data" parsed-response))
(output (gethash "output" data)))
(kagi--format-output output)))
(defun kagi--display-summary (f buffer-name)
(let ((summary (kagi--get-summary f)))
(with-current-buffer (get-buffer-create buffer-name)
(insert summary)
(goto-char 0)
(text-mode)
(display-buffer buffer-name))))
(defun kagi--process-prompt (prompt)
"Submit a PROMPT to FastGPT and process the API response.
Returns a formatted string to be displayed by the shell."
(let* ((response (kagi--call-fastgpt prompt))
(parsed-response (json-parse-string response))
(data (gethash "data" parsed-response))
(output (gethash "output" data))
(references (gethash "references" data)))
(format "%s\n\n%s" (kagi--format-output output) (kagi--format-references references))))
(defvar kagi-fastgpt--config
(make-shell-maker-config
:name "FastGPT"
:prompt "fastgpt > "
:execute-command (lambda (command _history callback error-callback)
(funcall callback (kagi--process-prompt command) nil)))
"The FastGPT shell configuration for shell-maker.")
;;; FastGPT shell
;;;###autoload
(defun kagi-fastgpt-shell ()
"Start an FastGPT shell."
(interactive)
(shell-maker-start kagi-fastgpt--config))
;;; Summarizer
(defun kagi--summary-buffer-name ()
"Generate an alternative name for the summary based on the given BUFFER-NAME."
(format "%s (summary)" (buffer-name)))
;;;###autoload
(defun kagi-summarize-buffer (buffer)
"Summarize the BUFFER's content."
(interactive "b")
(with-current-buffer buffer
(kagi--display-summary
(lambda () (kagi--call-text-summarizer (buffer-string)))
(kagi--summary-buffer-name))))
;;;###autoload
(defun kagi-summarize-region (begin end)
(interactive "r")
(kagi--display-summary
(lambda () (kagi--call-text-summarizer (buffer-substring begin end)))
(kagi--summary-buffer-name)))
;;;###autoload
(defun kagi-summarize-url (url)
(interactive "sURL: ")
(kagi--display-summary
(lambda () (kagi--call-url-summarizer url))
"*URL summary*"))
(provide 'kagi)
;;; kagi.el ends here