Compare commits
54 commits
82e903223e
...
aafd0bf27c
Author | SHA1 | Date | |
---|---|---|---|
Bram Schoenmakers | aafd0bf27c | ||
Bram Schoenmakers | bad0c2a50e | ||
Bram Schoenmakers | eb846635a0 | ||
Bram Schoenmakers | fa402763fa | ||
Bram Schoenmakers | 602b7dffb4 | ||
Bram Schoenmakers | 02be283f3a | ||
Bram Schoenmakers | 199cc478df | ||
Bram Schoenmakers | b3191b750d | ||
Bram Schoenmakers | c8a7658bd3 | ||
Bram Schoenmakers | fbb4ead05c | ||
Bram Schoenmakers | 1702fadaeb | ||
Bram Schoenmakers | e2b81ef432 | ||
Bram Schoenmakers | 28793401df | ||
Bram Schoenmakers | f086fd433a | ||
Bram Schoenmakers | 3324bfe1d7 | ||
Bram Schoenmakers | 146d128cd2 | ||
Bram Schoenmakers | e3e18dfc25 | ||
Bram Schoenmakers | f5cc1475bb | ||
Bram Schoenmakers | 01a761a2ff | ||
Bram Schoenmakers | 7d6f6f1cf1 | ||
Bram Schoenmakers | 016e453238 | ||
Bram Schoenmakers | 6d1b4cbc0f | ||
Bram Schoenmakers | d2187e309a | ||
Bram Schoenmakers | 21c9fcda38 | ||
Bram Schoenmakers | f68276593e | ||
Bram Schoenmakers | f78c2688a4 | ||
Bram Schoenmakers | de0ab986ae | ||
Bram Schoenmakers | da103bf236 | ||
Bram Schoenmakers | 76ae8b858a | ||
Bram Schoenmakers | 316df52e09 | ||
Bram Schoenmakers | 8e68af9216 | ||
Bram Schoenmakers | 1bdbb69000 | ||
Bram Schoenmakers | f8b01e4225 | ||
Bram Schoenmakers | a83540fc0b | ||
Bram Schoenmakers | ed6df4ba19 | ||
Bram Schoenmakers | 5415473f67 | ||
Bram Schoenmakers | 0c82995ed9 | ||
Bram Schoenmakers | 019391a618 | ||
Bram Schoenmakers | fbdeec45d9 | ||
Bram Schoenmakers | 2dcf2b7c6a | ||
Bram Schoenmakers | a3d9eb9a30 | ||
Bram Schoenmakers | 4bb7a1d63e | ||
Bram Schoenmakers | ec63275484 | ||
Bram Schoenmakers | 62fe326f2c | ||
Bram Schoenmakers | 4e3163f980 | ||
Bram Schoenmakers | b6c7d4b6ed | ||
Bram Schoenmakers | 4dd622c865 | ||
Bram Schoenmakers | e6da7af9ac | ||
Bram Schoenmakers | d59232e9bb | ||
Bram Schoenmakers | 239b8cd17d | ||
Bram Schoenmakers | c01425f31c | ||
Bram Schoenmakers | 166730181a | ||
Bram Schoenmakers | 2a89340c1e | ||
Bram Schoenmakers | 09f24ccaa3 |
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1 +1,2 @@
|
|||
/.cask
|
||||
*.elc
|
||||
|
|
10
Cask
Normal file
10
Cask
Normal file
|
@ -0,0 +1,10 @@
|
|||
;;; -*- lisp-data -*-
|
||||
|
||||
(source gnu)
|
||||
(source melpa-stable)
|
||||
(source melpa)
|
||||
|
||||
(depends-on "shell-maker")
|
||||
|
||||
(development
|
||||
(depends-on "buttercup"))
|
15
README.org
15
README.org
|
@ -200,6 +200,17 @@ If you recognize this confusion, you may want to add the following line to your
|
|||
|
||||
Because the =fastgpt-shell-mode-map= only becomes available when =kagi-fastgpt-shell= has been invoked, the keybinding is done in a mode hook.
|
||||
|
||||
* Development
|
||||
|
||||
kagi.el comes with some unit tests, written with [[https://github.com/jorgenschaefer/emacs-buttercup/][buttercup]] and can be executed in a controlled [[https://github.com/cask/cask/][Cask]] environment:
|
||||
|
||||
1. =git clone https://github.com/cask/cask/=
|
||||
2. =make -C cask install=
|
||||
3. Run =cask= in the kagi.el directory to setup the environment.
|
||||
4. Run the tests with =cask exec buttercup -L .=
|
||||
|
||||
There's also a [[https://github.com/casey/just][justfile]] which allows you to execute =just test= to compile the Emacs Lisp source and run the unit tests afterwards in one go.
|
||||
|
||||
* Changelog
|
||||
|
||||
** 0.4pre
|
||||
|
@ -214,6 +225,10 @@ Because the =fastgpt-shell-mode-map= only becomes available when =kagi-fastgpt-s
|
|||
|
||||
- =kagi-proofread= asks FastGPT to proofread the region, a buffer or a text input.
|
||||
|
||||
- =kagi-summarize-buffer= returns the summary when called non-interactively.
|
||||
|
||||
- Unit tests added.
|
||||
|
||||
*** Fixes
|
||||
|
||||
- Change the prompt for =kagi-translate= to return translations only, without preamble.
|
||||
|
|
14
justfile
Normal file
14
justfile
Normal file
|
@ -0,0 +1,14 @@
|
|||
set positional-arguments
|
||||
|
||||
# for convenience, run cask through bash
|
||||
cask *args:
|
||||
cask $@
|
||||
|
||||
compile:
|
||||
cask emacs -batch -L . -L test --eval "(setq byte-compile-error-on-warn t)" -f batch-byte-compile $(cask files); (ret=$? ; cask clean-elc && exit $ret)
|
||||
|
||||
test pattern=".": compile
|
||||
cask exec buttercup -L . --pattern {{pattern}} --no-skip
|
||||
|
||||
buttercup:
|
||||
emacs -batch -f package-initialize -L . -f buttercup-run-discover
|
430
kagi-test.el
Normal file
430
kagi-test.el
Normal file
|
@ -0,0 +1,430 @@
|
|||
;;; kagi-test.el --- Kagi API tests -*- lexical-binding: t; -*-
|
||||
|
||||
;; Copyright (C) 2023 - 2024 Bram Schoenmakers
|
||||
|
||||
;; Author: Bram Schoenmakers <me@bramschoenmakers.nl>
|
||||
;; Maintainer: Bram Schoenmakers <me@bramschoenmakers.nl>
|
||||
;; Created: 11 Feb 2024
|
||||
;; 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 - 2024 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:
|
||||
|
||||
;; API documentation: https://help.kagi.com/kagi/api/fastgpt.html
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'buttercup)
|
||||
(require 'kagi)
|
||||
|
||||
(defun kagi-test--dummy-output (text &optional references)
|
||||
"Construct a fictitious result from the Kagi FastGPT API.
|
||||
|
||||
TEXT is the output text, optionally with a list of REFERENCES."
|
||||
(json-encode (list (cons "data" (append
|
||||
(list (cons "output" text))
|
||||
(when references
|
||||
(list (cons "references" references))))))))
|
||||
|
||||
(buttercup-define-matcher-for-binary-function
|
||||
:to-be-equal-including-properties equal-including-properties
|
||||
:expect-match-phrase "Expected `%A' to be equal (incl. properties) to %b, but `%A' was %a."
|
||||
:expect-mismatch-phrase "Expected `%A' not to be equal (incl. properties) to %b, but `%A' was %a.")
|
||||
|
||||
(defmacro kagi-test--expect-arg (function-symbol num &rest expect-args)
|
||||
"Check the argument NUM of the first call of FUNCTION-SYMBOL.
|
||||
|
||||
The EXPECT-ARGS correspond to the arguments passed to the `expect' macro."
|
||||
`(let ((args (spy-calls-args-for ,function-symbol 0)))
|
||||
(expect (nth ,num args) ,@expect-args)))
|
||||
|
||||
(defmacro kagi-test--expect-object (function-symbol key &rest expect-args)
|
||||
"Check the argument NUM of the first call of FUNCTION-SYMBOL.
|
||||
|
||||
The EXPECT-ARGS correspond to the arguments passed to the `expect' macro."
|
||||
`(let ((args (car (spy-calls-args-for ,function-symbol 0))))
|
||||
(expect (map-elt args ,key) ,@expect-args)))
|
||||
|
||||
(describe "kagi.el"
|
||||
:var ((dummy-output "text"))
|
||||
(before-each
|
||||
(spy-on #'kagi--call-api :and-return-value (kagi-test--dummy-output dummy-output)))
|
||||
(describe "FastGPT"
|
||||
(describe "kagi-fastgpt-prompt"
|
||||
(before-each
|
||||
(spy-on #'message)
|
||||
(spy-on #'kagi--fastgpt-display-result))
|
||||
(it "converts *bold* markup to a bold face"
|
||||
(spy-on #'kagi--call-api :and-return-value (kagi-test--dummy-output "**bold**"))
|
||||
(expect (kagi-fastgpt-prompt "foo")
|
||||
:to-be-equal-including-properties
|
||||
(propertize "bold" 'font-lock-face 'kagi-bold)))
|
||||
(it "converts <b>bold</b> markup to a bold face"
|
||||
(spy-on #'kagi--call-api :and-return-value (kagi-test--dummy-output "<b>bold</b>"))
|
||||
(expect (kagi-fastgpt-prompt "foo")
|
||||
:to-be-equal-including-properties
|
||||
(propertize "bold" 'font-lock-face 'kagi-bold)))
|
||||
(it "converts $italic$ markup to an italic face"
|
||||
(spy-on #'kagi--call-api :and-return-value (kagi-test--dummy-output "$italic$"))
|
||||
(expect (kagi-fastgpt-prompt "foo")
|
||||
:to-be-equal-including-properties
|
||||
(propertize "italic" 'font-lock-face 'kagi-italic)))
|
||||
(it "converts ```code``` markup to a code face"
|
||||
(spy-on #'kagi--call-api :and-return-value (kagi-test--dummy-output "```echo $*```"))
|
||||
(expect (kagi-fastgpt-prompt "foo")
|
||||
:to-be-equal-including-properties
|
||||
(propertize "echo $*" 'font-lock-face 'kagi-code)))
|
||||
(it "formats references properly"
|
||||
(spy-on #'kagi--call-api
|
||||
:and-return-value
|
||||
(kagi-test--dummy-output
|
||||
"Main text"
|
||||
'(((title . "First title")
|
||||
(snippet . "**Snippet 1**")
|
||||
(url . "https://www.example.org"))
|
||||
((title . "Second title")
|
||||
(snippet . "Snippet $2$")
|
||||
(url . "https://www.example.com")))))
|
||||
(expect (kagi-fastgpt-prompt "foo")
|
||||
:to-be-equal-including-properties
|
||||
(format "Main text
|
||||
|
||||
%s First title
|
||||
%s
|
||||
https://www.example.org
|
||||
|
||||
%s Second title
|
||||
Snippet %s
|
||||
https://www.example.com"
|
||||
(propertize "[1]" 'font-lock-face 'kagi-bold)
|
||||
(propertize "Snippet 1" 'font-lock-face 'kagi-bold)
|
||||
(propertize "[2]" 'font-lock-face 'kagi-bold)
|
||||
(propertize "2" 'font-lock-face 'kagi-italic))))
|
||||
(it "inserts the output when requested"
|
||||
(spy-on #'insert)
|
||||
(kagi-fastgpt-prompt "foo" t)
|
||||
;; one additional insert call is to fill the temporary buffer
|
||||
;; for POST data
|
||||
(expect #'insert :to-have-been-called-times 2)
|
||||
(expect #'insert :to-have-been-called-with dummy-output))
|
||||
(it "does not insert the output by default"
|
||||
(spy-on #'insert)
|
||||
(kagi-fastgpt-prompt "foo")
|
||||
;; one insert call is to fill the temporary buffer for POST
|
||||
;; data
|
||||
(expect #'insert :to-have-been-called-times 1))
|
||||
(it "shows short output in the echo area when called interactively"
|
||||
(spy-on #'kagi--call-api :and-return-value (kagi-test--dummy-output dummy-output))
|
||||
(kagi-fastgpt-prompt "foo" nil t)
|
||||
(expect #'message :to-have-been-called-with dummy-output)
|
||||
(expect #'kagi--fastgpt-display-result :not :to-have-been-called))
|
||||
(it "shows longer output in a separate buffer when called interactively"
|
||||
(spy-on #'kagi--call-api :and-return-value (kagi-test--dummy-output (format "%s\n%s" dummy-output dummy-output)))
|
||||
(kagi-fastgpt-prompt "foo" nil t)
|
||||
(expect #'message :not :to-have-been-called)
|
||||
(expect #'kagi--fastgpt-display-result :to-have-been-called))
|
||||
(it "makes exactly one API call"
|
||||
(kagi-fastgpt-prompt "foo")
|
||||
(expect #'kagi--call-api :to-have-been-called-times 1)))
|
||||
(describe "kagi-translate"
|
||||
(before-each
|
||||
(spy-on #'kagi-fastgpt-prompt))
|
||||
(it "calls kagi-fastgpt-prompt non-interactively with target language in prompt"
|
||||
(kagi-translate "hello" "toki pona")
|
||||
(expect #'kagi-fastgpt-prompt :to-have-been-called-times 1)
|
||||
;; not going to test the exact phrasing of the prompt, but
|
||||
;; at least 'toki pona' has to appear.
|
||||
(kagi-test--expect-arg #'kagi-fastgpt-prompt 0 :to-match "toki pona")
|
||||
;; called non-interactively
|
||||
(kagi-test--expect-arg #'kagi-fastgpt-prompt 2 :to-equal nil))
|
||||
(it "calls kagi-fastgpt-prompt non-interactively with source and target language in prompt"
|
||||
(kagi-translate "bonjour" "toki pona" "French")
|
||||
;; has 'toki pona' in the prompt
|
||||
(kagi-test--expect-arg #'kagi-fastgpt-prompt 0 :to-match "toki pona")
|
||||
;; and has French in the prompt
|
||||
(kagi-test--expect-arg #'kagi-fastgpt-prompt 0 :to-match "French")
|
||||
;; called non-interactively
|
||||
(kagi-test--expect-arg #'kagi-fastgpt-prompt 2 :to-equal nil))
|
||||
(it "passes the region text to kagi-fastgpt-prompt, if active"
|
||||
(spy-on #'use-region-p :and-return-value t)
|
||||
(spy-on #'buffer-substring-no-properties :and-return-value "region text")
|
||||
(spy-on #'region-beginning)
|
||||
(spy-on #'region-end)
|
||||
(spy-on #'kagi--read-language :and-return-value "toki pona")
|
||||
(call-interactively #'kagi-translate)
|
||||
(kagi-test--expect-arg #'kagi-fastgpt-prompt 0 :to-match "region text")
|
||||
(kagi-test--expect-arg #'kagi-fastgpt-prompt 0 :to-match "toki pona")
|
||||
(kagi-test--expect-arg #'kagi-fastgpt-prompt 2 :to-equal t))
|
||||
(it "passes the user input if the region is inactive"
|
||||
(spy-on #'use-region-p)
|
||||
(spy-on #'kagi--read-language :and-return-value "toki pona")
|
||||
(spy-on #'read-buffer :and-return-value "user text")
|
||||
(spy-on #'get-buffer)
|
||||
(call-interactively #'kagi-translate)
|
||||
(kagi-test--expect-arg #'kagi-fastgpt-prompt 0 :to-match "user text")
|
||||
(kagi-test--expect-arg #'kagi-fastgpt-prompt 2 :to-equal t))
|
||||
(it "passes the buffer text if buffer is selected"
|
||||
(spy-on #'use-region-p)
|
||||
(spy-on #'kagi--read-language :and-return-value "toki pona")
|
||||
(spy-on #'read-buffer)
|
||||
(spy-on #'get-buffer :and-return-value t)
|
||||
(spy-on #'set-buffer)
|
||||
(spy-on #'buffer-string :and-return-value "buffer text")
|
||||
(call-interactively #'kagi-translate)
|
||||
(kagi-test--expect-arg #'kagi-fastgpt-prompt 0 :to-match "buffer text")
|
||||
(kagi-test--expect-arg #'kagi-fastgpt-prompt 2 :to-equal t))
|
||||
(it "raises an error when no text is given"
|
||||
(spy-on #'use-region-p)
|
||||
(spy-on #'kagi--read-language :and-return-value "toki pona")
|
||||
(spy-on #'read-buffer :and-return-value "")
|
||||
(spy-on #'get-buffer)
|
||||
(expect (call-interactively #'kagi-translate) :to-throw)))
|
||||
(describe "kagi-proofread"
|
||||
(before-each
|
||||
(spy-on #'kagi-fastgpt-prompt))
|
||||
(it "calls kagi-fastgpt-prompt"
|
||||
(kagi-proofread "foo")
|
||||
(expect #'kagi-fastgpt-prompt :to-have-been-called)
|
||||
(kagi-test--expect-arg #'kagi-fastgpt-prompt 0 :to-match "foo"))
|
||||
(it "passes the region text to kagi-fastgpt-prompt, if active"
|
||||
(spy-on #'use-region-p :and-return-value t)
|
||||
(spy-on #'buffer-substring-no-properties :and-return-value "region text")
|
||||
(spy-on #'region-beginning)
|
||||
(spy-on #'region-end)
|
||||
(call-interactively #'kagi-proofread)
|
||||
(kagi-test--expect-arg #'kagi-fastgpt-prompt 0 :to-match "region text")
|
||||
(kagi-test--expect-arg #'kagi-fastgpt-prompt 2 :to-equal t))
|
||||
(it "passes the user input if the region is inactive"
|
||||
(spy-on #'use-region-p)
|
||||
(spy-on #'read-buffer :and-return-value "user text")
|
||||
(spy-on #'get-buffer)
|
||||
(call-interactively #'kagi-proofread)
|
||||
(kagi-test--expect-arg #'kagi-fastgpt-prompt 0 :to-match "user text")
|
||||
(kagi-test--expect-arg #'kagi-fastgpt-prompt 2 :to-equal t))
|
||||
(it "passes the buffer text if buffer is selected"
|
||||
(spy-on #'use-region-p)
|
||||
(spy-on #'read-buffer)
|
||||
(spy-on #'get-buffer :and-return-value t)
|
||||
(spy-on #'set-buffer)
|
||||
(spy-on #'buffer-string :and-return-value "buffer text")
|
||||
(call-interactively #'kagi-proofread)
|
||||
(kagi-test--expect-arg #'kagi-fastgpt-prompt 0 :to-match "buffer text")
|
||||
(kagi-test--expect-arg #'kagi-fastgpt-prompt 2 :to-equal t))
|
||||
(it "raises an error when no text is given"
|
||||
(spy-on #'use-region-p)
|
||||
(spy-on #'read-buffer :and-return-value "")
|
||||
(spy-on #'get-buffer)
|
||||
(expect (call-interactively #'kagi-proofread) :to-throw))))
|
||||
(describe "Summarizer"
|
||||
:var ((just-enough-text-input nil)
|
||||
(just-too-little-text-input nil)
|
||||
(dummy-https-url "https://www.example.com")
|
||||
(dummy-http-url "http://www.example.com")
|
||||
(dummy-ftp-url "ftp://example.com")
|
||||
;; make pattern matching case sensitive
|
||||
(case-fold-search nil))
|
||||
(before-all
|
||||
(dotimes (_ 50) (push "a" just-enough-text-input))
|
||||
(setq just-too-little-text-input (string-join (cdr just-enough-text-input) " "))
|
||||
(setq just-enough-text-input (string-join just-enough-text-input " ")))
|
||||
(before-each
|
||||
(spy-on #'kagi--call-summarizer :and-call-through)
|
||||
(spy-on #'kagi--display-summary))
|
||||
(describe "kagi-summarize"
|
||||
:var ((kagi-summarizer-default-language))
|
||||
(before-each
|
||||
(setq kagi-summarizer-default-language "NL"))
|
||||
(it "returns a summary on minimal text input"
|
||||
(expect (kagi-summarize just-enough-text-input) :to-equal dummy-output))
|
||||
(it "makes exactly one API call"
|
||||
(kagi-summarize just-enough-text-input)
|
||||
(expect #'kagi--call-api :to-have-been-called-times 1))
|
||||
(it "throws on just too little text output"
|
||||
(expect (kagi-summarize just-too-little-text-input) :to-throw))
|
||||
(it "throws an error on too little text input"
|
||||
(expect (kagi-summarize "foo") :to-throw))
|
||||
(it "throws an error on empty input"
|
||||
(expect (kagi-summarize "") :to-throw))
|
||||
(it "throws an error on missing input"
|
||||
(expect (kagi-summarize nil) :to-throw))
|
||||
(it "returns a summary for a valid language code"
|
||||
(expect (kagi-summarize just-enough-text-input "NL" :to-equal dummy-output))
|
||||
(kagi-test--expect-object #'kagi--call-summarizer "target_language" :to-equal "NL"))
|
||||
(it "returns a summary for a valid language code with wrong capitalization"
|
||||
(expect (kagi-summarize just-enough-text-input "nL" :to-equal dummy-output))
|
||||
(kagi-test--expect-object #'kagi--call-summarizer "target_language" :to-equal "NL"))
|
||||
(it "returns a summary for a valid language name"
|
||||
(expect (kagi-summarize just-enough-text-input "Dutch" :to-equal dummy-output))
|
||||
(kagi-test--expect-object #'kagi--call-summarizer "target_language" :to-equal "NL"))
|
||||
(it "returns a summary for a valid language name with different capitalization"
|
||||
(expect (kagi-summarize just-enough-text-input "dUtch" :to-equal dummy-output))
|
||||
(kagi-test--expect-object #'kagi--call-summarizer "target_language" :to-equal "NL"))
|
||||
(it "falls back to the default language for invalid language codes"
|
||||
(expect (kagi-summarize just-enough-text-input "VL") :to-equal dummy-output)
|
||||
(kagi-test--expect-object #'kagi--call-summarizer "target_language" :to-equal "NL"))
|
||||
(it "falls back to the default language for invalid language names"
|
||||
(expect (kagi-summarize just-enough-text-input "Valyrian") :to-equal dummy-output)
|
||||
(kagi-test--expect-object #'kagi--call-summarizer "target_language" :to-equal "NL"))
|
||||
(it "falls back to English if the default language is invalid"
|
||||
(setq kagi-summarizer-default-language "XY")
|
||||
(expect (kagi-summarize just-enough-text-input "Valyrian") :to-equal dummy-output)
|
||||
(kagi-test--expect-object #'kagi--call-summarizer "target_language" :to-equal "EN"))
|
||||
(it "returns a summary for an HTTPS URL"
|
||||
(expect (kagi-summarize dummy-https-url) :to-equal dummy-output))
|
||||
(it "returns a summary for an uppercase HTTPS URL"
|
||||
(expect (kagi-summarize (upcase dummy-https-url)) :to-equal dummy-output))
|
||||
(it "returns a summary for an HTTP URL"
|
||||
(expect (kagi-summarize dummy-http-url) :to-equal dummy-output))
|
||||
(it "throws for an unsupported URL scheme"
|
||||
(expect (kagi-summarize dummy-ftp-url) :to-throw))
|
||||
(it "returns a summary for a valid engine with different capitalization"
|
||||
(expect (kagi-summarize dummy-https-url nil "Daphne") :to-equal dummy-output)
|
||||
(kagi-test--expect-object #'kagi--call-summarizer "engine" :to-equal "daphne"))
|
||||
(it "uses kagi-summarizer-engine variable for invalid engine values"
|
||||
(setq kagi-summarizer-engine "Daphne")
|
||||
(expect (kagi-summarize dummy-https-url nil "bram") :to-equal dummy-output)
|
||||
(kagi-test--expect-object #'kagi--call-summarizer "engine" :to-equal "daphne"))
|
||||
(it "uses the cecil engine when an invalid engine is configured"
|
||||
(setq kagi-summarizer-engine "steve")
|
||||
(expect (kagi-summarize dummy-https-url) :to-equal dummy-output)
|
||||
(kagi-test--expect-object #'kagi--call-summarizer "engine" :to-equal "cecil"))
|
||||
(it "returns a summary when the summary style is requested"
|
||||
(expect (kagi-summarize just-enough-text-input nil nil 'summary) :to-equal dummy-output))
|
||||
(it "returns a summary when the take-away style is requested"
|
||||
(expect (kagi-summarize just-enough-text-input nil nil 'takeaway) :to-equal dummy-output))
|
||||
(it "uses the summary style when an invalid format is given"
|
||||
(kagi-summarize just-enough-text-input nil nil 'invalid)
|
||||
(kagi-test--expect-object #'kagi--call-summarizer "summary_type" :to-equal 'summary))
|
||||
(it "uses the summary style when an invalid format is configured"
|
||||
(setq kagi-summarizer-default-summary-format 'invalid)
|
||||
(kagi-summarize just-enough-text-input)
|
||||
(kagi-test--expect-object #'kagi--call-summarizer "summary_type" :to-equal 'summary)))
|
||||
(describe "kagi-summarize-buffer"
|
||||
(before-each
|
||||
(spy-on #'read-buffer)
|
||||
(spy-on #'set-buffer)
|
||||
(spy-on #'buffer-name :and-return-value "dummy-buffer")
|
||||
(spy-on #'kagi-summarize :and-return-value dummy-output)
|
||||
(spy-on #'kagi--insert-summary))
|
||||
(it "returns the summary when called non-interactively"
|
||||
(expect (kagi-summarize-buffer "dummy") :to-be dummy-output)
|
||||
(expect #'kagi-summarize :to-have-been-called)
|
||||
(expect #'kagi--display-summary :not :to-have-been-called)
|
||||
(expect #'kagi--insert-summary :not :to-have-been-called))
|
||||
(it "inserts the summary when requested, non-interactively"
|
||||
(kagi-summarize-buffer "dummy" t)
|
||||
(expect #'kagi-summarize :to-have-been-called)
|
||||
(expect #'kagi--display-summary :not :to-have-been-called)
|
||||
(expect #'kagi--insert-summary :to-have-been-called))
|
||||
(it "displays the summary when called interactively"
|
||||
(call-interactively #'kagi-summarize-buffer)
|
||||
(expect #'kagi-summarize :to-have-been-called)
|
||||
(expect #'kagi--display-summary :to-have-been-called)
|
||||
(expect #'kagi--insert-summary :not :to-have-been-called))
|
||||
(it "inserts the summary when requested, interactively"
|
||||
(spy-on #'kagi--get-summarizer-parameters :and-return-value '(t nil nil))
|
||||
(call-interactively #'kagi-summarize-buffer)
|
||||
(expect #'kagi-summarize :to-have-been-called)
|
||||
(expect #'kagi--display-summary :not :to-have-been-called)
|
||||
(expect #'kagi--insert-summary :to-have-been-called))
|
||||
(it "passes arguments to kagi-summary"
|
||||
(spy-on #'kagi--get-summarizer-parameters :and-return-value '(t lang bram random))
|
||||
(call-interactively #'kagi-summarize-buffer)
|
||||
(expect #'kagi-summarize :to-have-been-called)
|
||||
(expect #'kagi--display-summary :not :to-have-been-called)
|
||||
(expect #'kagi--insert-summary :to-have-been-called)
|
||||
(kagi-test--expect-arg #'kagi-summarize 1 :to-equal 'lang)
|
||||
(kagi-test--expect-arg #'kagi-summarize 2 :to-equal 'bram)
|
||||
(kagi-test--expect-arg #'kagi-summarize 3 :to-equal 'random)))
|
||||
(describe "kagi-summarize-region"
|
||||
(before-each
|
||||
(spy-on #'region-beginning)
|
||||
(spy-on #'region-end)
|
||||
(spy-on #'kagi--get-summarizer-parameters :and-return-value '(lang bram random))
|
||||
(spy-on #'kagi-summarize :and-return-value dummy-output)
|
||||
(spy-on #'buffer-name :and-return-value "buffer-name")
|
||||
(spy-on #'buffer-substring-no-properties))
|
||||
(it "passes arguments to kagi-summary"
|
||||
(call-interactively #'kagi-summarize-region)
|
||||
(kagi-test--expect-arg #'kagi--display-summary 1 :to-equal "buffer-name (summary)")
|
||||
(expect #'kagi-summarize :to-have-been-called)
|
||||
(kagi-test--expect-arg #'kagi-summarize 1 :to-equal 'lang)
|
||||
(kagi-test--expect-arg #'kagi-summarize 2 :to-equal 'bram)
|
||||
(kagi-test--expect-arg #'kagi-summarize 3 :to-equal 'random))
|
||||
(it "opens a buffer with the summary"
|
||||
(call-interactively #'kagi-summarize-region)
|
||||
(expect #'kagi--display-summary :to-have-been-called)
|
||||
(kagi-test--expect-arg #'kagi--display-summary 0 :to-equal dummy-output)))
|
||||
(describe "kagi-summarize-url"
|
||||
(before-each
|
||||
(spy-on #'kagi-summarize :and-return-value dummy-output)
|
||||
(spy-on #'read-string :and-return-value "https://www.example.com")
|
||||
(spy-on #'kagi--get-summarizer-parameters :and-return-value '(nil lang bram random))
|
||||
(spy-on #'kagi--insert-summary))
|
||||
(it "passes arguments to kagi-summary"
|
||||
(call-interactively #'kagi-summarize-url)
|
||||
(kagi-test--expect-arg #'kagi-summarize 1 :to-equal 'lang)
|
||||
(kagi-test--expect-arg #'kagi-summarize 2 :to-equal 'bram)
|
||||
(kagi-test--expect-arg #'kagi-summarize 3 :to-equal 'random))
|
||||
(it "opens a buffer with the summary"
|
||||
(call-interactively #'kagi-summarize-url)
|
||||
(expect #'kagi--display-summary :to-have-been-called)
|
||||
(expect #'kagi--insert-summary :not :to-have-been-called)
|
||||
(kagi-test--expect-arg #'kagi--display-summary 0 :to-equal dummy-output))
|
||||
(it "opens a buffer with the domain name for an HTTPS URL"
|
||||
(call-interactively #'kagi-summarize-url)
|
||||
(expect #'kagi--display-summary :to-have-been-called)
|
||||
(kagi-test--expect-arg #'kagi--display-summary 1 :to-equal "example.com (summary)"))
|
||||
(it "opens a buffer with the domain name for an HTTP URL"
|
||||
(spy-on #'read-string :and-return-value "http://www.example.com")
|
||||
(call-interactively #'kagi-summarize-url)
|
||||
(expect #'kagi--display-summary :to-have-been-called)
|
||||
(kagi-test--expect-arg #'kagi--display-summary 1 :to-equal "example.com (summary)"))
|
||||
(it "opens a buffer with the domain name for an HTTPS URL ending with /"
|
||||
(spy-on #'read-string :and-return-value "https://www.example.com/")
|
||||
(call-interactively #'kagi-summarize-url)
|
||||
(expect #'kagi--display-summary :to-have-been-called)
|
||||
(kagi-test--expect-arg #'kagi--display-summary 1 :to-equal "example.com (summary)"))
|
||||
(it "opens a buffer with the domain name for an HTTPS URL ending with path"
|
||||
(spy-on #'read-string :and-return-value "https://www.example.com/aaa/")
|
||||
(call-interactively #'kagi-summarize-url)
|
||||
(expect #'kagi--display-summary :to-have-been-called)
|
||||
(kagi-test--expect-arg #'kagi--display-summary 1 :to-equal "example.com (summary)"))
|
||||
(it "opens a buffer with the domain name for an HTTPS URL with subdomain"
|
||||
(spy-on #'read-string :and-return-value "https://abc.example.com/")
|
||||
(call-interactively #'kagi-summarize-url)
|
||||
(expect #'kagi--display-summary :to-have-been-called)
|
||||
(kagi-test--expect-arg #'kagi--display-summary 1 :to-equal "abc.example.com (summary)"))
|
||||
(it "inserts the summary when requested non-interactively"
|
||||
(kagi-summarize-url "https://www.example.com" t)
|
||||
(expect #'kagi--display-summary :not :to-have-been-called)
|
||||
(expect #'kagi--insert-summary :to-have-been-called)
|
||||
(kagi-test--expect-arg #'kagi--insert-summary 0 :to-equal dummy-output)))))
|
||||
|
||||
;;; kagi-test.el ends here
|
154
kagi.el
154
kagi.el
|
@ -63,14 +63,6 @@ https://kagi.com/settings?p=api"
|
|||
:type '(choice string function)
|
||||
:group 'kagi)
|
||||
|
||||
(defcustom kagi-stubbed-responses nil
|
||||
"Whether the package should return a stubbed response.
|
||||
|
||||
To be used for testing purposes, such that no credits are spent
|
||||
on dummy data."
|
||||
:type 'boolean
|
||||
:group 'kagi)
|
||||
|
||||
(defcustom kagi-fastgpt-api-url "https://kagi.com/api/v0/fastgpt"
|
||||
"The Kagi FastGPT API entry point."
|
||||
:type '(choice string function)
|
||||
|
@ -257,34 +249,21 @@ https://help.kagi.com/kagi/api/fastgpt.html for more information."
|
|||
"--header" "Content-Type: application/json"
|
||||
"--data" "@-")))
|
||||
|
||||
(defvar kagi--fastgpt-stubbed-response
|
||||
"{\"data\":{\"output\":\"<b>Test</b> response in **bold** and $italic$.\"}}"
|
||||
"Stubbed response for the Kagi FastGPT endpoint.")
|
||||
(defun kagi--call-api (object url)
|
||||
"Submit the OBJECT to the API end-point at URL.
|
||||
|
||||
(defvar kagi--summarizer-stubbed-response
|
||||
"{\"data\":{\"output\":\"```Test``` response.\"}}"
|
||||
"Stubbed response for the Kagi Summarizer endpoint.")
|
||||
|
||||
(defun kagi--call-process-region (&rest args)
|
||||
"`call-process-region' wrapper.
|
||||
|
||||
Calls `call-process-region' with ARGS, unless
|
||||
`kagi-stubbed-responses' is non-nil.
|
||||
|
||||
In that case, this function will not do an actual API call but
|
||||
return some dummy data."
|
||||
(if (not kagi-stubbed-responses)
|
||||
(apply #'call-process-region args)
|
||||
(let* ((url (car (last args)))
|
||||
(response (cond
|
||||
((string= url kagi-fastgpt-api-url)
|
||||
kagi--fastgpt-stubbed-response)
|
||||
((string= url kagi-summarizer-api-url)
|
||||
kagi--summarizer-stubbed-response)
|
||||
(t ""))))
|
||||
(erase-buffer)
|
||||
(insert response)
|
||||
0)))
|
||||
The OBJECT will be JSON encoded and sent as HTTP POST data."
|
||||
(with-temp-buffer
|
||||
(insert (json-encode object))
|
||||
(let* ((call-process-flags '(nil nil "curl" t t nil))
|
||||
(curl-flags (kagi--curl-flags))
|
||||
(all-flags (append call-process-flags
|
||||
curl-flags
|
||||
(list url)))
|
||||
(return (apply #'call-process-region all-flags)))
|
||||
(if (zerop return)
|
||||
(buffer-string)
|
||||
(error "Call to Kagi API returned with status %s" return)))))
|
||||
|
||||
(defun kagi--call-fastgpt (prompt)
|
||||
"Submit the given PROMPT to the FastGPT API.
|
||||
|
@ -292,17 +271,7 @@ return some dummy data."
|
|||
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-fastgpt-api-url)))
|
||||
(return (apply #'kagi--call-process-region all-flags)))
|
||||
(if (zerop return)
|
||||
(buffer-string)
|
||||
(error "Call to FastGPT API returned with status %s" return)))))
|
||||
(kagi--call-api (list (cons 'query prompt)) kagi-fastgpt-api-url))
|
||||
|
||||
(defun kagi--call-summarizer (obj)
|
||||
"Submit a request to the Summarizer API.
|
||||
|
@ -312,17 +281,7 @@ The given OBJ is encoded to JSON and used as the request's POST data.
|
|||
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-summarizer-api-url)))
|
||||
(return (apply #'kagi--call-process-region all-flags)))
|
||||
(if (zerop return)
|
||||
(buffer-string)
|
||||
(error "Call to Summarizer API returned with status %s" return)))))
|
||||
(kagi--call-api obj kagi-summarizer-api-url))
|
||||
|
||||
(defun kagi--build-summarizer-request-object (items)
|
||||
"Build a request object for a summary.
|
||||
|
@ -557,6 +516,53 @@ no issues."
|
|||
"Non-nil if string S is a URL."
|
||||
(string-match-p (rx (seq bos "http" (? "s") "://" (+ (not space)) eos)) s))
|
||||
|
||||
(defun kagi--valid-language-name-p (language)
|
||||
"Return non-nil if LANGUAGE is a valid language name."
|
||||
(and (stringp language)
|
||||
(map-elt kagi--summarizer-languages (capitalize language))))
|
||||
|
||||
(defun kagi--valid-language-code-p (language)
|
||||
"Return t if LANGUAGE is a valid two letter language code for the summarizer."
|
||||
(and (stringp language)
|
||||
(seq-contains-p
|
||||
(map-values kagi--summarizer-languages)
|
||||
(upcase language))))
|
||||
|
||||
(defun kagi--summarizer-determine-language (hint)
|
||||
"Determine the language for the summary given a language HINT.
|
||||
|
||||
The HINT may be a language code (e.g. `DE') or a language
|
||||
name (e.g. `German'). If as invalid hint is given, it falls back
|
||||
to `kagi-summarizer-default-language'."
|
||||
(cond
|
||||
((kagi--valid-language-code-p hint) (upcase hint))
|
||||
((kagi--valid-language-name-p hint)
|
||||
(map-elt kagi--summarizer-languages (capitalize hint)))
|
||||
((kagi--valid-language-code-p kagi-summarizer-default-language)
|
||||
kagi-summarizer-default-language)
|
||||
(t "EN")))
|
||||
|
||||
(defun kagi--valid-engine-p (engine)
|
||||
"Return non-nil when the given ENGINE is valid."
|
||||
(and (stringp engine)
|
||||
(map-elt kagi--summarizer-engines (downcase engine))))
|
||||
|
||||
(defun kagi--summarizer-engine (hint)
|
||||
"Return a valid engine name based on the name given in HINT."
|
||||
(cond ((kagi--valid-engine-p hint) (downcase hint))
|
||||
((kagi--valid-engine-p kagi-summarizer-engine)
|
||||
(downcase kagi-summarizer-engine))
|
||||
(t "cecil")))
|
||||
|
||||
(defun kagi--summarizer-format (hint)
|
||||
"Return a valid summary type based on the type given in HINT."
|
||||
(let* ((custom-type (cdr (get 'kagi-summarizer-default-summary-format 'custom-type)))
|
||||
(choices (mapcar (lambda (e) (car (last e))) custom-type)))
|
||||
(cond ((seq-contains-p choices hint) hint)
|
||||
((seq-contains-p choices kagi-summarizer-default-summary-format)
|
||||
kagi-summarizer-default-summary-format)
|
||||
(t 'summary))))
|
||||
|
||||
(defun kagi-summarize (text-or-url &optional language engine format)
|
||||
"Return the summary of the given TEXT-OR-URL.
|
||||
|
||||
|
@ -569,15 +575,12 @@ defined in `kagi--summarizer-engines'.
|
|||
|
||||
FORMAT is the summary format, where `summary' returns a paragraph
|
||||
of text and `takeaway' returns a bullet list."
|
||||
(let* ((kagi-summarizer-default-language
|
||||
(if (stringp language)
|
||||
(upcase language)
|
||||
kagi-summarizer-default-language))
|
||||
(kagi-summarizer-engine
|
||||
(if (stringp engine)
|
||||
(downcase engine)
|
||||
kagi-summarizer-engine))
|
||||
(kagi-summarizer-default-summary-format format))
|
||||
(let ((kagi-summarizer-default-language
|
||||
(kagi--summarizer-determine-language language))
|
||||
(kagi-summarizer-engine
|
||||
(kagi--summarizer-engine engine))
|
||||
(kagi-summarizer-default-summary-format
|
||||
(kagi--summarizer-format format)))
|
||||
(if-let* ((response (if (kagi--url-p text-or-url)
|
||||
(kagi--call-url-summarizer text-or-url)
|
||||
(kagi--call-text-summarizer text-or-url)))
|
||||
|
@ -632,7 +635,7 @@ this when PROMPT-INSERT-P is non-nil."
|
|||
#'string=))))))
|
||||
|
||||
;;;###autoload
|
||||
(defun kagi-summarize-buffer (buffer &optional insert language engine format)
|
||||
(defun kagi-summarize-buffer (buffer &optional insert language engine format interactive-p)
|
||||
"Summarize the BUFFER's content and show it in a new window.
|
||||
|
||||
By default, the summary is shown in a new buffer.
|
||||
|
@ -654,17 +657,20 @@ of text and `takeaway' returns a bullet list.
|
|||
With a single universal prefix argument (`C-u'), the user is
|
||||
prompted whether the summary has to be inserted at point, which
|
||||
target LANGUAGE to use, which summarizer ENGINE to use and which
|
||||
summary FORMAT to use."
|
||||
(interactive (cons
|
||||
(read-buffer (format-prompt "Buffer" "") nil t)
|
||||
(kagi--get-summarizer-parameters t)))
|
||||
summary FORMAT to use.
|
||||
|
||||
INTERACTIVE-P is t when called interactively."
|
||||
(interactive (append
|
||||
(list (read-buffer (format-prompt "Buffer" "") nil t))
|
||||
(kagi--get-summarizer-parameters t)
|
||||
(list t)))
|
||||
(let ((summary (with-current-buffer buffer
|
||||
(kagi-summarize (buffer-string) language engine format)))
|
||||
(summary-buffer-name (with-current-buffer buffer
|
||||
(kagi--summary-buffer-name (buffer-name)))))
|
||||
(if (and insert (not buffer-read-only))
|
||||
(kagi--insert-summary summary)
|
||||
(kagi--display-summary summary summary-buffer-name))))
|
||||
(cond ((and insert (not buffer-read-only)) (kagi--insert-summary summary))
|
||||
(interactive-p (kagi--display-summary summary summary-buffer-name))
|
||||
(t summary))))
|
||||
|
||||
;;;###autoload
|
||||
(defun kagi-summarize-region (begin end &optional language engine format)
|
||||
|
|
Loading…
Reference in a new issue