1
0
Fork 0

Compare commits

...

25 commits

Author SHA1 Message Date
Bram Schoenmakers ddbc23ce63
Merge branch 'prompts' 2024-04-10 19:24:24 +02:00
Bram Schoenmakers 3bb896cfa8
Explain a prompt text generated by a function 2024-04-10 18:16:48 +02:00
Bram Schoenmakers 2252dc0451
Revise docstrings 2024-04-10 18:00:52 +02:00
Bram Schoenmakers 989f3816e9
Remove the kagi-fastgpt-prompt-synonym command
It's rather specific, it's up to the user to add it.

There is one example, kagi-fastgpt-prompt-definition, which should be sufficient.
2024-04-10 17:59:39 +02:00
Bram Schoenmakers 9dc6eaf7ca
Minor changes 2024-04-10 17:50:55 +02:00
Bram Schoenmakers 423f449fe7
Add suggestion that a command can be bound to a key 2024-04-10 17:46:49 +02:00
Bram Schoenmakers 94a383345f
Remove TODO 2024-04-10 17:44:28 +02:00
Bram Schoenmakers 454ed2e513
Update changelog 2024-04-10 16:35:04 +02:00
Bram Schoenmakers d828c52da0
Explain custom prompts in README 2024-04-10 16:21:24 +02:00
Bram Schoenmakers ee601f4b2e
Merge branch 'main' into prompts 2024-04-10 12:08:55 +02:00
Bram Schoenmakers ba593c2fc3
Fix docstring 2024-04-05 21:13:09 +02:00
Bram Schoenmakers ba26b5ec1e
Make the user-facing name an optional argument 2024-04-01 13:13:30 +02:00
Bram Schoenmakers 3fe442758c
Improve readability 2024-03-31 19:49:45 +02:00
Bram Schoenmakers 3f60094815
Add check that kagi-fastgpt-prompt is called non-interactively 2024-03-31 17:49:38 +02:00
Bram Schoenmakers b0e33aa8e6
Prompts may be functions returning text
This can be used to make an interactive variant of a prompt and a
non-interactive one.
2024-03-31 17:43:20 +02:00
Bram Schoenmakers 48d90f7208
Adjust define-kagi-fastgpt-prompt to match kagi-proofread functionality 2024-03-31 10:51:32 +02:00
Bram Schoenmakers 55487fab5b
Docstring and variable name changes 2024-03-28 22:33:11 +01:00
Bram Schoenmakers 3a445b3c90
Fix tests for the changed variable name 2024-03-28 22:25:27 +01:00
Bram Schoenmakers 3843afb785
Push the symbol name as a string in the car 2024-03-28 22:24:46 +01:00
Bram Schoenmakers 26bcbcea77
Add macro to define your own prompts 2024-03-28 22:14:23 +01:00
Bram Schoenmakers 9b58724986
Call function when a predefined prompt has one
Also test if the variable with predefined prompts is used properly by kagi-fastgpt-prompt.
2024-03-23 09:37:39 +01:00
Bram Schoenmakers 554a19285b
Isolate the prompt construction in a separate function 2024-03-22 23:45:09 +01:00
Bram Schoenmakers 2b9ab2fa0a
Improve the placeholder handling in prompts 2024-03-22 23:07:11 +01:00
Bram Schoenmakers 086b6b30f6
Let kagi-fastgpt-prompt use predefined prompts and fill in placeholders 2024-03-19 21:30:38 +01:00
Bram Schoenmakers 6f9fd09471
Define a variable for storing default and user-defined FastGPT prompts 2024-03-01 22:46:14 +01:00
3 changed files with 298 additions and 156 deletions

View file

@ -30,6 +30,7 @@ kagi.el has some functions that use FastGPT to perform certain operations on tex
- =kagi-translate= :: Translates strings or complete buffers to another language (including programming languages).
- =kagi-proofread= :: Proofread a string or buffer.
- =kagi-fastgpt-prompt-definition= :: Returns the definition of a word.
** Universal Summarizer
@ -126,9 +127,45 @@ 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).
** Defining your own prompts
kagi.el comes with a macro to define your own prompts easily: =define-kagi-fastgpt-prompt=. When the prompt contains the placeholder =%s=, it will be replaced with the region or an interactively used word.
An example usage of this macro comes by default with this package:
#+begin_src elisp
(define-kagi-fastgpt-prompt kagi-fastgpt-prompt-definition
"Define the following word: %s"
"Definition")
#+end_src
The first argument is the name of the command that will be defined. The second argument the prompt that will be sent. The third argument is optional and gives your prompt a user visible name. It will be shown when calling =kagi-fastgpt-prompt= interactively.
The defined prompt becomes a typical Emacs command that takes one argument to fill the placeholder. You could bind the prompt command to a key, use it to integrate with Embark (see below) or to list all your prompts with a Hydra.
The prompt string may also be a function that returns the prompt
string. The function may take one argument: whether the command was
called interactively or not. This can be used to alter the prompt
based on how the command was called. E.g. a non-interactive version
could contain an instruction to say either /Yes/ or /No/. See
=kagi-proofread= for an example:
#+begin_src elisp
(define-kagi-fastgpt-prompt kagi-proofread
(lambda (interactive-p)
(format "Proofread the following text. %s
%%s" (if interactive-p "" "Say OK if there are no issues.")))
"Proofread")
#+end_src
Note the =%%s= notation, =format= turns it into =%s= which becomes the prompt placeholder.
** 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.
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. It can also be used to call your custom prompts with =define-kagi-fastgpt-prompt=.
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.
#+begin_src elisp
(defmacro embark-kagi-map (name &rest keys)
@ -145,6 +182,7 @@ The kagi.el package can be integrated with [[https://github.com/oantolin/embark]
(keymap-set embark-buffer-map "K" embark-kagi-buffer-map)
(embark-kagi-map embark-kagi-region-map
"d" #'kagi-fastgpt-prompt-definition
"p" #'kagi-proofread
"s" #'kagi-summarize-region
"t" #'kagi-translate)
@ -155,11 +193,11 @@ The kagi.el package can be integrated with [[https://github.com/oantolin/embark]
(keymap-set embark-url-map "K" embark-kagi-url-map)
#+end_src
** Key bindings
** FastGPT shell key bindings
Since the FastGPT shell inherits from =comint-mode= indirectly, many key bindings are also inherited. Enter the =help= command in the shell to get more info, or run =describe-keymap= on =fastgpt-shell-mode-map=.
One of those bindings is =C-c C-o=, which flushes the last output. However, this binding is used in =org-mode= to open a URL an point. Typical FastGPT results include URLs so one may be tempted to type =C-c C-o= to browse the URL, only to have the output erased.
One of those bindings is =C-c C-o=, which flushes the last output. However, this binding is used in =org-mode= to open a URL an point. Typical FastGPT results include URLs so one may be tempted to type =C-c C-o= to browse the URL, only to have the output erased (which you can undo, actually).
If you recognize this confusion, you may want to add the following line to your configuration file to shadow the =comint-mode= binding with something more appropriate:
@ -194,6 +232,12 @@ Needless to say, the tests won't make actual API calls. Otherwise it wouldn't be
*** New
- The =define-kagi-fastgpt-prompt= macro allows you to define your own prompts, that may contain a placeholder to fill in relevant text.
- The macro is used to (re)define commands:
- =kagi-fastgpt-prompt-definition=: Define the a word.
- =kagi-proofread= is now defined with the macro.
- =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.

View file

@ -87,6 +87,7 @@ The EXPECT-ARGS correspond to the arguments passed to the `expect' macro."
(expect (kagi--curl-flags "foo") :to-throw))
(describe "FastGPT"
(describe "kagi-fastgpt-prompt"
:var ((kagi--fastgpt-prompts))
(before-each
(spy-on #'message)
(spy-on #'kagi--fastgpt-display-result))
@ -162,102 +163,141 @@ https://www.example.com"
(it "makes exactly one API call"
(kagi-fastgpt-prompt "foo")
(expect #'kagi--call-api :to-have-been-called-times 1))
(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"
(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))))
(it "replaces all occurrences of %s with the user input or region when it appears in the prompt"
(spy-on #'kagi--fastgpt)
(spy-on #'completing-read :and-return-value "foo %s%s bar")
(spy-on #'kagi--get-text-for-prompt :and-return-value "region")
(call-interactively #'kagi-fastgpt-prompt)
(kagi-test--expect-arg #'kagi--fastgpt 0 :to-equal "foo regionregion bar"))
(it "retrieves the user input once when %s appears multiple times in the prompt"
(spy-on #'kagi--fastgpt)
(spy-on #'completing-read :and-return-value "%s%s")
(spy-on #'kagi--get-text-for-prompt :and-return-value "")
(call-interactively #'kagi-fastgpt-prompt)
(expect #'kagi--get-text-for-prompt :to-have-been-called-times 1))
(it "replaces all occurrences of %% with % when it appears in the prompt"
(spy-on #'kagi--fastgpt)
(spy-on #'completing-read :and-return-value "foo%%s")
(spy-on #'kagi--get-text-for-prompt)
(call-interactively #'kagi-fastgpt-prompt)
(kagi-test--expect-arg #'kagi--fastgpt 0 :to-equal "foo%s")
(expect #'kagi--get-text-for-prompt :not :to-have-been-called))
(it "does not replace an invalid placeholder %b"
(spy-on #'kagi--fastgpt)
(spy-on #'completing-read :and-return-value "foo%bar")
(spy-on #'kagi--get-text-for-prompt)
(call-interactively #'kagi-fastgpt-prompt)
(kagi-test--expect-arg #'kagi--fastgpt 0 :to-equal "foo%bar")
(expect #'kagi--get-text-for-prompt :not :to-have-been-called))
(it "uses the cdr when the user enters a predefined prompt name"
(setq kagi--fastgpt-prompts '(("foo" . "bar")))
(spy-on #'kagi--fastgpt)
(spy-on #'completing-read :and-return-value "foo")
(call-interactively #'kagi-fastgpt-prompt)
(kagi-test--expect-arg #'kagi--fastgpt 0 :to-equal "bar"))
(it "calls the function when a predefined prompt has one"
(setq kagi--fastgpt-prompts '(("function" . (lambda () "result"))))
(spy-on #'kagi--fastgpt)
(spy-on #'completing-read :and-return-value "function")
(call-interactively #'kagi-fastgpt-prompt)
(kagi-test--expect-arg #'kagi--fastgpt 0 :to-equal "result")))
(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)

172
kagi.el
View file

@ -73,6 +73,41 @@ https://kagi.com/settings?p=api"
:type '(choice string function)
:group 'kagi)
(defvar kagi--fastgpt-prompts '()
"List of prompts that were defined with `define-kagi-fastgpt-prompt'.")
(defmacro define-kagi-fastgpt-prompt (symbol-name prompt &optional name)
"Define a command SYMBOL-NAME that executes the given PROMPT.
PROMPT can be a string or a function returning a string. The
function may take one argument: whether the command was called
interactively or not. This can be used to alter the prompt based
on how the command was called. E.g. a non-interactive version
could contain an instruction to say either Yes or No. See
`kagi-proofread' for an example.
When PROMPT contains %s, it will be replaced with the region (if
active), the (narrowed) buffer content of the selected buffer or
a manually entered prompt.
The NAME is also shown as an option when `kagi-fastgpt-prompt' is
called interactively, to select the corresponding prompt. When no
NAME is given, the SYMBOL-NAME is shown instead."
`(progn
(push (cons (or ,name (symbol-name ',symbol-name)) ,prompt) kagi--fastgpt-prompts)
(defun ,symbol-name (text &optional interactive-p)
(interactive (list (kagi--get-text-for-prompt) t))
(let* ((prompt-template (if (functionp ,prompt)
(funcall ,prompt interactive-p)
,prompt))
(expanded-prompt (kagi--fastgpt-expand-prompt-placeholders
prompt-template
(lambda () text))))
(kagi-fastgpt-prompt
expanded-prompt
nil
interactive-p)))))
(defvar kagi--summarizer-engines
'(("agnes" . "Friendly, descriptive, fast summary.")
("cecil" . "Formal, technical, analytical summary.")
@ -386,45 +421,6 @@ retrieving a result from Lisp code."
(interactive)
(shell-maker-start kagi-fastgpt--config))
;;;###autoload
(defun kagi-fastgpt-prompt (prompt &optional insert interactive-p)
"Feed the given PROMPT to FastGPT.
If INSERT is non-nil, the response is inserted at point (if the
buffer is writable).
If INTERACTIVE-P is non-nil, the result is presented either in
the minibuffer for single line outputs, or shown in a separate
buffer.
If INTERACTIVE-P is nil, the result is returned as a
string (suitable for invocations from Emacs Lisp)."
(interactive (list (read-string "fastgpt> ")
current-prefix-arg
t))
(let* ((result (kagi--fastgpt prompt))
(result-lines (length (string-lines result))))
(cond ((and insert (not buffer-read-only))
(save-excursion
(insert result)))
((and interactive-p (eql result-lines 1))
(message result))
((and interactive-p (> result-lines 1))
(kagi--fastgpt-display-result result))
((not interactive-p) result))))
(defun kagi--read-language (prompt)
"Read a language from the minibuffer interactively.
PROMPT is passed to the corresponding parameters of
`completing-read', refer to its documentation for more info."
(completing-read prompt kagi--languages
nil
nil
nil
kagi--language-history
"English"))
(defun kagi--get-text-for-prompt ()
"Return the text to insert in a prompt.
@ -445,6 +441,81 @@ the text manually."
((< 0 (length buffer-or-text)) buffer-or-text)
(t (error "No buffer or text entered"))))))
(defun kagi--fastgpt-expand-prompt-placeholders (prompt text-function)
"Expand all occurrences of %s in PROMPT with the result of TEXT-FUNCTION.
It gets replaced with the region text, (narrowed) buffer text or
user input."
(let ((user-text))
(replace-regexp-in-string (rx (seq "%" anychar))
(lambda (match)
(pcase match
("%%" "%")
("%s" (or user-text (setq user-text (funcall text-function))))
(_ match)))
prompt t t)))
(defun kagi--fastgpt-construct-prompt ()
"Construct a prompt, either a predefined one or entered by the user.
When the selected prompt contains %s, then the value is
interactively obtained from the user (the region, buffer content
or text input)."
(let* ((prompt-name (completing-read "fastgpt> " kagi--fastgpt-prompts))
(prompt-cdr (alist-get prompt-name kagi--fastgpt-prompts prompt-name nil #'string=))
(prompt-template (if (functionp prompt-cdr) (funcall prompt-cdr) prompt-cdr)))
(kagi--fastgpt-expand-prompt-placeholders prompt-template (lambda () (kagi--get-text-for-prompt)))))
;;;###autoload
(defun kagi-fastgpt-prompt (prompt &optional insert interactive-p)
"Feed the given PROMPT to FastGPT.
When PROMPT contains %s, it will be replaced with the region (if
active), the (narrowed) buffer content of the selected buffer or
a manually entered prompt. %s remains unprocessed when
`kagi-fastgpt-prompt' is called non-interactively (when
INTERACTIVE-P is nil). %% becomes % and any other placeholder is
left as-is.
If INSERT is non-nil, the response is inserted at point (if the
buffer is writable).
If INTERACTIVE-P is non-nil, the result is presented either in
the minibuffer for single line outputs, or shown in a separate
buffer.
If INTERACTIVE-P is nil, the result is returned as a
string (suitable for invocations from Emacs Lisp)."
(interactive (list (kagi--fastgpt-construct-prompt)
current-prefix-arg
t))
(let* ((result (kagi--fastgpt prompt))
(result-lines (length (string-lines result))))
(cond ((and insert (not buffer-read-only))
(save-excursion
(insert result)))
((and interactive-p (eql result-lines 1))
(message result))
((and interactive-p (> result-lines 1))
(kagi--fastgpt-display-result result))
((not interactive-p) result))))
(define-kagi-fastgpt-prompt kagi-fastgpt-prompt-definition
"Define the following word: %s"
"Definition")
(defun kagi--read-language (prompt)
"Read a language from the minibuffer interactively.
PROMPT is passed to the corresponding parameters of
`completing-read', refer to its documentation for more info."
(completing-read prompt kagi--languages
nil
nil
nil
kagi--language-history
"English"))
;;;###autoload
(defun kagi-translate (text target-language &optional source-language interactive-p)
"Translate the TEXT to TARGET-LANGUAGE using FastGPT.
@ -475,26 +546,13 @@ result is short, otherwise it is displayed in a new buffer."
text)))
(kagi-fastgpt-prompt prompt nil interactive-p)))
;;;###autoload
(defun kagi-proofread (text &optional interactive-p)
"Proofread the given TEXT using FastGPT.
(define-kagi-fastgpt-prompt kagi-proofread
(lambda (interactive-p)
(format "Proofread the following text. %s
The TEXT can be either from the region, a (narrowed) buffer or
entered manually.
When `kagi-proofread' is called non-interactively (INTERACTIVE-P is
nil), the function should return the string 'OK' when there are
no issues."
(interactive
(list (kagi--get-text-for-prompt) t))
(let ((prompt (format "Proofread the following text. %s
%s"
(if interactive-p
""
"Say OK if there are no issues.")
text)))
(kagi-fastgpt-prompt prompt nil interactive-p)))
%%s" (if interactive-p "" "Say OK if there are no issues.")))
"Proofread")
;;; Summarizer