1
0
Fork 0

Merge branch 'main' into prompts

This commit is contained in:
Bram Schoenmakers 2024-04-10 12:08:55 +02:00
commit ee601f4b2e
Signed by: bram
GPG key ID: 0CCD19DFDC63258F
4 changed files with 243 additions and 176 deletions

View file

@ -12,7 +12,7 @@
This Emacs package provides the following functionalities from the [[https://www.kagi.com][Kagi search engine]]:
- FastGPT :: Kagi's open source LLM offering, as a shell inspired by [[https://github.com/xenodium/chatgpt-shell][xenodium's chatgpt-shell]].
- FastGPT :: Kagi's LLM offering, as a shell inspired by [[https://github.com/xenodium/chatgpt-shell][xenodium's chatgpt-shell]].
- Universal Summarizer :: Summarizes texts, webpages, videos and more.
Both functions are accessed through Kagi's [[https://help.kagi.com/kagi/api/overview.html][APIs]]. Before a call can be made, some setup should be done on the Kagi website (see below).
@ -41,13 +41,15 @@ kagi.el has some functions that use FastGPT to perform certain operations on tex
The summarize commands accept a single universal prefix, which allows you to:
- insert the summary at point;
- choose a (different) target language;
- choose which summary engine to use.
- choose which summary engine to use;
- choose which summary format to use (prose or bullet list);
- opt-out from caching at Kagi for confidential content.
Note that texts submitted to Kagi are subject to their [[https://kagi.com/privacy#Summarizer][Privacy Policy]].
* Installation and configuration
kagi.el is available on [[https://melpa.org/#/kagi][MELPA]].
kagi.el is available on [[https://melpa.org/#/kagi][MELPA]] and [[https://stable.melpa.org/#/kagi][MELPA Stable]].
To install from Git, clone with:
@ -65,7 +67,7 @@ You may want to load and configure the package with ~use-package~, for example p
;; or use a function, e.g. with the password-store package:
(kagi-api-token (lambda () (password-store-get "Kagi/API")))
;; Univernal Summarizer settings
;; Universal Summarizer settings
(kagi-summarizer-engine "cecil")
(kagi-summarizer-default-language "EN")
(kagi-summarizer-cache t)
@ -78,12 +80,12 @@ You may want to load and configure the package with ~use-package~, for example p
(kagi-bold ((t (:inherit modus-themes-bold)))))
#+end_src
The token can be supplied directly as a string, but you could write a lambda to retrieve the token from a more secure location (e.g. with the combination of [[https://passwordstore.org/][pass(1)]] and the password-store package that comes with it).
The token can be supplied directly as a string, but you could write a lambda to retrieve the token from a more secure location (e.g. with the combination of [[https://passwordstore.org/][pass(1)]] and the /password-store/ package that comes with it).
** Kagi API setup
1. Create a Kagi account if you haven't done so already. An account is free, and comes with 100 trial searches.
2. In [[https://kagi.com/settings?p=billing_api][your account settings]], put a balance for the API part (note that this is a separate balance than the subscription). The recommendation is to start with a one-time charge of $5. A single query ranges from 1 to 5 cents typically, depending on the amount of tokens processed.
2. In [[https://kagi.com/settings?p=billing_api][your account settings]], put a balance for the API part (note that this is a separate balance than the subscription). The recommendation is to start with a one-time charge of $5. Check the pricing for the [[https://help.kagi.com/kagi/api/fastgpt.html#pricing][FastGPT API]] and the [[https://help.kagi.com/kagi/api/summarizer.html#pricing][Summarizer API]] for for the actual costs.
3. In [[https://kagi.com/settings?p=api][the API portal]], create an API token. Put the result in ~kagi-api-token~ (or write a function to access it securely).
** Configuration settings
@ -113,7 +115,7 @@ The token can be supplied directly as a string, but you could write a lambda to
|----------------------------------------+---------------------------------------------------------|
| kagi-api-token | The Kagi API token. |
| kagi-fastgpt-api-url | The Kagi FastGPT API entry point. |
| kagi-stubbed-responses | Whether the package should return a stubbed response. |
| kagi-fastgpt-prompts | Prompts to choose for a buffer, text or region. |
| kagi-summarizer-api-url | The Kagi Summarizer API entry point. |
| kagi-summarizer-cache | Determines whether the Summarizer should cache results. |
| kagi-summarizer-default-language | Default target language of the summary. |
@ -124,42 +126,6 @@ The token can be supplied directly as a string, but you could write a lambda to
The code to generate the table of configuration items was inspired by an idea of [[https://xenodium.com/generating-elisp-org-docs/][Álvaro Ramírez]] (a.k.a. xenodium).
** Examples of custom functions
The =kagi-summarize= function allows you to summarize texts or URLs from Emacs Lisp.
By overriding a variable with a =let= construct you can (temporarily) deviate from the default / configured value. A few examples are shown below:
*** Language override
To obtain a Dutch summary of a video you may want to define the following function:
#+begin_src elisp
(defun my/kagi/dutch-summary (text-or-url)
"Obtain a Dutch summary for the given TEXT-OR-URL."
(let ((kagi-summarize-default-language "NL"))
(kagi-summarize text-or-url)))
#+end_src
Note that, when you invoke the summarizer functionality interactively, you can also temporarily choose a different target language with the universal prefix (=C-u=) on one of the =kagi-summarize-*= commands.
*** Caching override
The [[https://help.kagi.com/kagi/api/summarizer.html][Summarizer API]] comes with the following note:
#+begin_quote
For handling sensitive information and documents, we recommend setting the 'cache' API parameter to False. In this way, the document will "flow through" our infrastructure and will not be retained anywhere after processing.
#+end_quote
In a similar fashion as above, you could define a function that disables caching temporarily (while having it enabled by default).
#+begin_src elisp
(defun my/kagi/sensitive-summary (text)
"Summarize the current TEXT with caching disabled.")
(let ((kagi-summarizer-cache nil))
(kagi-summarize text))
#+end_src
** Embark integration
The kagi.el package can be integrated with [[https://github.com/oantolin/embark][Embark]]. Use it to easily summarize, translate or proofread a buffer, region or a URL. In order to be consistent with all keymaps, and to avoid clashes, the functionality is behind the /K/ prefix key. For example, press /K s/ to invoke the summarize functionality.
@ -216,8 +182,29 @@ kagi.el comes with some unit tests, written with [[https://github.com/jorgenscha
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.
Needless to say, the tests won't make actual API calls. Otherwise it wouldn't be unit tests.
* Changelog
** 0.5pre
*** Breaking changes
- Removed function =kagi-fastgpt= as announced in the 0.4 changelog.
*** New
- =kagi-summarize= has a =no-cache= parameter to opt out from caching at Kagi's side.
- =kagi-summarize-buffer= and =kagi-summarize-region= also have a =no-cache= parameter which can be toggled interactively when called with the universal prefix.
- Add support for traditional Chinese as possible summary language.
*** Fixes
- Fixed language code for Czech summaries.
- Handle error responses when calling the FastGPT API.
** 0.4
*** Breaking changes
@ -240,14 +227,14 @@ There's also a [[https://github.com/casey/just][justfile]] which allows you to e
- Added autoload markers where they were missing.
- Language selection menu for summaries was not formatted properly.
** 0.3.1
** 0.3.1 :noexport:
*** Fixes
- Fix for displaying a summary in a new buffer.
- Fix for =kagi-summarize-region= that doesn't need to ask for insert at point.
** 0.3
** 0.3 :noexport:
*** New
@ -265,7 +252,7 @@ There's also a [[https://github.com/casey/just][justfile]] which allows you to e
- **text** is converted to the =kagi-bold= face.
- $text$ is converted to the new =kagi-italic= face.
** 0.2
** 0.2 :noexport:
*** Breaking changes
@ -287,7 +274,7 @@ There's also a [[https://github.com/casey/just][justfile]] which allows you to e
- Add boolean variable =kagi-stubbed-responses= to enable stubbed responses, to replace actual API calls. Since calls are metered, it's more economic to use stubbed responses for development / debugging purposes.
** 0.1
** 0.1 :noexport:
Initial release.

View file

@ -1,14 +1,19 @@
set positional-arguments
# for convenience, run cask through bash
# Run all unit tests
default: test
# For convenience, run cask through bash
cask *args:
cask $@
# Compile the Emacs Lisp file(s)
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
# Run unit tests matching a pattern (matches all tests by default)
test pattern="." flags="": compile
cask exec buttercup -L . --pattern {{pattern}} --no-skip {{flags}}
buttercup:
emacs -batch -f package-initialize -L . -f buttercup-run-discover
# Run unit tests matching a pattern with verbose debug info on failure
testv pattern=".": (test pattern "--traceback full")

View file

@ -52,6 +52,13 @@ TEXT is the output text, optionally with a list of REFERENCES."
(when references
(list (cons "references" references))))))))
(defun kagi-test--error-output ()
"Construct a fictitious erroneous result from the Kagi API."
(json-encode
'((data . ((output . nil)))
(error . (((code . 42)
(msg . "Too bad")))))))
(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."
@ -75,6 +82,9 @@ The EXPECT-ARGS correspond to the arguments passed to the `expect' macro."
:var ((dummy-output "text"))
(before-each
(spy-on #'kagi--call-api :and-return-value (kagi-test--dummy-output dummy-output)))
(it "throws an error for invalid tokens"
(setq kagi-api-token 42)
(expect (kagi--curl-flags "foo") :to-throw))
(describe "FastGPT"
(describe "kagi-fastgpt-prompt"
:var ((kagi--fastgpt-prompts))
@ -191,96 +201,103 @@ https://www.example.com"
(spy-on #'completing-read :and-return-value "function")
(call-interactively #'kagi-fastgpt-prompt)
(kagi-test--expect-arg #'kagi--fastgpt 0 :to-equal "result")))
(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 non-interactively"
(kagi-proofread "foo")
(expect #'kagi-fastgpt-prompt :to-have-been-called)
(kagi-test--expect-arg #'kagi-fastgpt-prompt 0 :to-match "foo")
(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)
(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))))
(it "handles empty output and returned errors from the API gracefully"
(spy-on #'kagi--call-api :and-return-value (kagi-test--error-output))
(spy-on #'kagi--fastgpt :and-call-through)
(expect (kagi-fastgpt-prompt "foo") :to-throw)
(expect (spy-context-thrown-signal
(spy-calls-most-recent #'kagi--fastgpt))
:to-equal '(error "Too bad (42)"))))
(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 non-interactively"
(kagi-proofread "foo")
(expect #'kagi-fastgpt-prompt :to-have-been-called)
(kagi-test--expect-arg #'kagi-fastgpt-prompt 0 :to-match "foo")
(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)
(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)
@ -364,7 +381,32 @@ https://www.example.com"
(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)))
(kagi-test--expect-object #'kagi--call-summarizer "summary_type" :to-equal 'summary))
(it "lets Kagi cache by default"
(kagi-summarize just-enough-text-input)
(kagi-test--expect-object #'kagi--call-summarizer "cache" :to-equal t))
(it "does not let Kagi cache when no-cache is set"
(kagi-summarize just-enough-text-input nil nil nil t)
(kagi-test--expect-object #'kagi--call-summarizer "cache" :to-equal nil))
(it "lets the no-cache argument override the configured value"
(setq kagi-summarizer-cache t)
(kagi-summarize just-enough-text-input nil nil nil t)
(kagi-test--expect-object #'kagi--call-summarizer "cache" :to-equal nil))
(it "does not let Kagi cache if configured"
(setq kagi-summarizer-cache nil)
(kagi-summarize just-enough-text-input)
(kagi-test--expect-object #'kagi--call-summarizer "cache" :to-equal nil))
(it "caches by default for an invalid configuration value"
(setq kagi-summarizer-cache 'invalid)
(kagi-summarize just-enough-text-input)
(kagi-test--expect-object #'kagi--call-summarizer "cache" :to-equal t))
(it "handles empty output and returned errors from the API gracefully"
(spy-on #'kagi--call-api :and-return-value (kagi-test--error-output))
(spy-on #'kagi-summarize :and-call-through)
(expect (kagi-summarize just-enough-text-input) :to-throw)
(expect (spy-context-thrown-signal
(spy-calls-most-recent #'kagi-summarize))
:to-equal '(error "Too bad (42)"))))
(describe "kagi-summarize-buffer"
(before-each
(spy-on #'read-buffer)
@ -394,19 +436,20 @@ https://www.example.com"
(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))
(spy-on #'kagi--get-summarizer-parameters :and-return-value '(t lang bram random maybe))
(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)))
(kagi-test--expect-arg #'kagi-summarize 3 :to-equal 'random)
(kagi-test--expect-arg #'kagi-summarize 4 :to-equal 'maybe)))
(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--get-summarizer-parameters :and-return-value '(lang bram random maybe))
(spy-on #'kagi-summarize :and-return-value dummy-output)
(spy-on #'buffer-name :and-return-value "buffer-name")
(spy-on #'buffer-substring-no-properties))
@ -416,7 +459,8 @@ https://www.example.com"
(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))
(kagi-test--expect-arg #'kagi-summarize 3 :to-equal 'random)
(kagi-test--expect-arg #'kagi-summarize 4 :to-equal 'maybe))
(it "opens a buffer with the summary"
(call-interactively #'kagi-summarize-region)
(expect #'kagi--display-summary :to-have-been-called)
@ -431,7 +475,8 @@ https://www.example.com"
(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))
(kagi-test--expect-arg #'kagi-summarize 3 :to-equal 'random)
(kagi-test--expect-arg #'kagi-summarize 4 :to-equal nil))
(it "opens a buffer with the summary"
(call-interactively #'kagi-summarize-url)
(expect #'kagi--display-summary :to-have-been-called)

86
kagi.el
View file

@ -124,7 +124,7 @@ https://help.kagi.com/kagi/api/summarizer.html."
:group 'kagi)
(defvar kagi--languages '(("Bulgarian" . "BG")
("Czech" . "CZ")
("Czech" . "CS")
("Danish" . "DA")
("German" . "DE")
("Greek" . "EL")
@ -151,7 +151,8 @@ https://help.kagi.com/kagi/api/summarizer.html."
("Swedish" . "SV")
("Turkish" . "TR")
("Ukrainian" . "UK")
("Chinese (simplified)" . "ZH"))
("Chinese (simplified)" . "ZH")
("Chinese (traditional)" . "ZH-HANT"))
"Supported languages by the Kagi LLM.")
(defvar kagi--summarizer-languages (append
@ -177,10 +178,10 @@ The value should be a string of two characters representing the
(defcustom kagi-summarizer-cache t
"Determines whether the Summarizer should cache results.
Repetitive queries won't be charged if caching is enabled (the
default). For sensitive texts, you may opt for disabling the
cache by setting this item to nil (but subsequent queries on the
same text will be charged.)"
Repetitive requests on the same text won't be charged if caching
is enabled (the default). For sensitive texts, you may opt for
disabling the cache by setting this item to nil (but subsequent
queries on the same text will be charged.)"
:type 'boolean
:group 'kagi)
@ -322,7 +323,7 @@ list of conses."
(append items
`(("engine" . ,kagi-summarizer-engine)
("summary_type" . ,kagi-summarizer-default-summary-format)
("cache" . ,kagi-summarizer-cache))
("cache" . ,(if kagi-summarizer-cache t nil)))
;; prevent a nil in the result list, causing (json-encode)
;; to generate a wrong request object.
@ -375,12 +376,15 @@ retrieving a result from Lisp code."
(parsed-response (json-parse-string response))
(output (kagi--gethash parsed-response "data" "output"))
(references (kagi--gethash parsed-response "data" "references")))
(string-trim
(format "%s\n\n%s"
(kagi--format-output output)
(kagi--format-references references)))))
(define-obsolete-function-alias 'kagi-fastgpt 'kagi-fastgpt-prompt "0.4")
(if output
(string-trim (format "%s\n\n%s"
(kagi--format-output output)
(kagi--format-references references)))
(if-let ((firsterror (aref (kagi--gethash parsed-response "error") 0)))
(error (format "%s (%s)"
(gethash "msg" firsterror)
(gethash "code" firsterror)))
(error "An error occurred while querying FastGPT")))))
(defun kagi--fastgpt-display-result (result)
"Display the RESULT of a FastGPT prompt in a new buffer."
@ -617,7 +621,7 @@ to `kagi-summarizer-default-language'."
kagi-summarizer-default-summary-format)
(t 'summary))))
(defun kagi-summarize (text-or-url &optional language engine format)
(defun kagi-summarize (text-or-url &optional language engine format no-cache)
"Return the summary of the given TEXT-OR-URL.
LANGUAGE is a supported two letter abbreviation of the language,
@ -628,13 +632,19 @@ ENGINE is the name of a supported summarizer engine, as
defined in `kagi--summarizer-engines'.
FORMAT is the summary format, where `summary' returns a paragraph
of text and `takeaway' returns a bullet list."
of text and `takeaway' returns a bullet list.
When NO-CACHE is t, inputs are not retained inside Kagi's
infrastructure. When nil, the default value for
`kagi-summarizer-cache' is used. Set to t for confidential
content."
(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)))
(kagi--summarizer-format format))
(kagi-summarizer-cache (if no-cache nil kagi-summarizer-cache)))
(if-let* ((response (if (kagi--url-p text-or-url)
(kagi--call-url-summarizer text-or-url)
(kagi--call-text-summarizer text-or-url)))
@ -647,13 +657,17 @@ of text and `takeaway' returns a bullet list."
(gethash "code" firsterror)))
(error "An error occurred while requesting a summary")))))
(defun kagi--get-summarizer-parameters (&optional prompt-insert-p)
(defun kagi--get-summarizer-parameters (&optional prompts)
"Return a list of interactively obtained summarizer parameters.
Not all commands need to insert a summary, so only prompt for
this when PROMPT-INSERT-P is non-nil."
Some parameters need to be called interactively, however, for
some clients that doesn't make sense. E.g. we don't want to ask
to insert when the region is highlighted. Therefore, PROMPTS is a
list of items that can be prompted interactively. It is
a (possibly empty) list with possible elements \\='prompt-for-insert
or \\='prompt-for-no-cache."
(append
(when prompt-insert-p
(when (seq-contains-p prompts 'prompt-for-insert)
(list
(and (equal current-prefix-arg '(4))
(not buffer-read-only)
@ -686,10 +700,14 @@ this when PROMPT-INSERT-P is non-nil."
summary-formats
kagi-summarizer-default-summary-format
nil
#'string=))))))
#'string=))))
(list
(and (seq-contains-p prompts 'prompt-for-no-cache)
(equal current-prefix-arg '(4))
(y-or-n-p "Cache the result?")))))
;;;###autoload
(defun kagi-summarize-buffer (buffer &optional insert language engine format interactive-p)
(defun kagi-summarize-buffer (buffer &optional insert language engine format no-cache interactive-p)
"Summarize the BUFFER's content and show it in a new window.
By default, the summary is shown in a new buffer.
@ -708,6 +726,11 @@ defined in `kagi--summarizer-engines'.
FORMAT is the summary format, where `summary' returns a paragraph
of text and `takeaway' returns a bullet list.
When NO-CACHE is t, inputs are not retained inside Kagi's
infrastructure. When nil, the default value for
`kagi-summarizer-cache' is used. Set to t for confidential
content.
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
@ -716,10 +739,11 @@ 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)
(kagi--get-summarizer-parameters '(prompt-for-insert
prompt-for-no-cache))
(list t)))
(let ((summary (with-current-buffer buffer
(kagi-summarize (buffer-string) language engine format)))
(kagi-summarize (buffer-string) language engine format no-cache)))
(summary-buffer-name (with-current-buffer buffer
(kagi--summary-buffer-name (buffer-name)))))
(cond ((and insert (not buffer-read-only)) (kagi--insert-summary summary))
@ -727,7 +751,7 @@ INTERACTIVE-P is t when called interactively."
(t summary))))
;;;###autoload
(defun kagi-summarize-region (begin end &optional language engine format)
(defun kagi-summarize-region (begin end &optional language engine format no-cache)
"Summarize the region's content marked by BEGIN and END positions.
The summary is always shown in a new buffer.
@ -742,17 +766,23 @@ defined in `kagi--summarizer-engines'.
FORMAT is the summary format, where `summary' returns a paragraph
of text and `takeaway' returns a bullet list.
When NO-CACHE is t, inputs are not retained inside Kagi's
infrastructure. When nil, the default value for
`kagi-summarizer-cache' is used. Set to t for confidential
content.
With a single universal prefix argument (`C-u'), the user is
prompted for which target LANGUAGE to use, which summarizer
ENGINE to use and which summary FORMAT to use."
(interactive (append
(list (region-beginning) (region-end))
(kagi--get-summarizer-parameters)))
(kagi--get-summarizer-parameters '(prompt-for-no-cache))))
(kagi--display-summary
(kagi-summarize (buffer-substring-no-properties begin end)
language
engine
format)
format
no-cache)
(kagi--summary-buffer-name (buffer-name))))
;;;###autoload
@ -793,7 +823,7 @@ types are supported:
(interactive
(cons
(read-string (format-prompt "URL" ""))
(kagi--get-summarizer-parameters t)))
(kagi--get-summarizer-parameters '(prompt-for-insert))))
(let ((summary (kagi-summarize url language engine format)))
(if (and insert (not buffer-read-only))
(kagi--insert-summary summary)