1
0
Fork 0

Compare commits

...

30 commits

Author SHA1 Message Date
Bram Schoenmakers ee601f4b2e
Merge branch 'main' into prompts 2024-04-10 12:08:55 +02:00
Bram Schoenmakers c8f1461ada
Add changelog entry 2024-04-10 07:30:24 +02:00
Bram Schoenmakers 3d05ca00da
Merge branch 'improved-error-handling' 2024-04-10 07:27:51 +02:00
Bram Schoenmakers 42061aae3a
Simplify error response function, no need for customization 2024-04-10 07:25:44 +02:00
Bram Schoenmakers 380c63a0c3
Add test for proper error handling by kagi-summarize 2024-04-10 07:22:36 +02:00
Bram Schoenmakers 21a7a2e175
Add testv recipe for more verbose logging 2024-04-10 07:20:20 +02:00
Bram Schoenmakers 2c2e88eb04
Add test for invalid API tokens 2024-04-09 20:32:54 +02:00
Bram Schoenmakers c25c0ead95
Fix fictitious error output 2024-04-09 19:48:41 +02:00
Bram Schoenmakers 4dabc7ecf7
Mention that the tests won't make API calls 2024-04-05 23:15:48 +02:00
Bram Schoenmakers e4200270c0
First attempt to improve error handling whenever FastGPT returns error output 2024-04-05 22:54:09 +02:00
Bram Schoenmakers af03f7d1f8
Remove TODO 2024-03-31 11:47:47 +02:00
Bram Schoenmakers ef799275ea
Remove details 2024-03-28 06:25:05 +01:00
Bram Schoenmakers 5753556163
Fix language code for Czech 2024-03-27 22:28:21 +01:00
Bram Schoenmakers ccfc34b1c0
Remove the open source claim 2024-03-27 22:13:45 +01:00
Bram Schoenmakers 407781fe4e
Adjust docstring 2024-03-23 10:05:57 +01:00
Bram Schoenmakers 19e779ad37
Minor docstring adjustments 2024-03-23 09:45:31 +01:00
Bram Schoenmakers 658662bfd4
Remove the obsoleted function kagi-fastgpt 2024-03-19 21:42:06 +01:00
Bram Schoenmakers 47ef4d218f
Support traditional Chinese as possible summary language 2024-03-19 21:36:26 +01:00
Bram Schoenmakers d473b1b6fa
Remove suggestion for defining your own functions 2024-03-07 19:46:24 +01:00
Bram Schoenmakers 7e302e0357
Do not export old changelog entries 2024-03-06 23:06:41 +01:00
Bram Schoenmakers 581b8b3bc5
Update README 2024-03-06 21:26:22 +01:00
Bram Schoenmakers 3671f78c6a
Refactored the internal kagi--get-summarizer-parameters function
Take a list of promptable items, to be passed on from the caller.
2024-03-06 21:20:15 +01:00
Bram Schoenmakers 72119bd9b7
Add no-cache parameter to kagi-summarize-{buffer,region} 2024-03-06 21:06:43 +01:00
Bram Schoenmakers 9773229d6b
Normalize to t when kagi-summarizer-cache is non-nil 2024-03-06 20:35:59 +01:00
Bram Schoenmakers 5732de8bcd
Add no-cache parameter to kagi-summarize 2024-03-06 20:28:23 +01:00
Bram Schoenmakers e1f2f6fcc8
Various fixes in the README 2024-03-05 22:23:30 +01:00
Bram Schoenmakers 64e2779335
Extend list of options when the universal prefix is passed 2024-03-05 21:49:45 +01:00
Bram Schoenmakers 7d061a8ef8
Add comments that appear with just --list 2024-03-02 21:58:07 +01:00
Bram Schoenmakers afac09ab32
Remove unused recipe 2024-03-02 21:57:47 +01:00
Bram Schoenmakers 3080cea644
Make test the default recipe 2024-03-02 21:55:00 +01:00
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)