gptel: Add org-mode support and update README

gptel.el (gptel-response-filter-functions, gptel-send,
gptel--create-prompt, gptel--transform-response, gptel--convert-org,
gptel--convert-markdown->org): Add support for org-mode by transforming
the response manually.  (Note: Asking ChatGPT to format its results in
org-mode markup produces inconsistent results.)

Additionally, the abnormal hook `gptel-resposne-filter-functions' is
added for arbitrary transformations of the response.  Its implementation
seems needlessly complex, and in the future we should change it to
use `run-hook-wrapped' with a local accumulator.
This commit is contained in:
Karthik Chikmagalur 2023-03-10 04:12:32 -08:00
parent b212c24c4a
commit 8fca5bc762
2 changed files with 94 additions and 12 deletions

View file

@ -6,7 +6,7 @@ GPTel is a simple, no-frills ChatGPT client for Emacs.
- Requires an [[https://platform.openai.com/account/api-keys][OpenAI API key]]. - Requires an [[https://platform.openai.com/account/api-keys][OpenAI API key]].
- No external dependencies, only Emacs. Also, it's async. - No external dependencies, only Emacs. Also, it's async.
- Interaction is in a Markdown (or text) buffer. - Interaction is in a Markdown, Org or text buffer.
- Supports conversations (not just one-off queries) and multiple independent sessions. - Supports conversations (not just one-off queries) and multiple independent sessions.
- You can go back and edit your previous prompts, or even ChatGPT's previous responses when continuing a conversation. These will be fed back to ChatGPT. - You can go back and edit your previous prompts, or even ChatGPT's previous responses when continuing a conversation. These will be fed back to ChatGPT.
@ -39,6 +39,8 @@ Procure an [[https://platform.openai.com/account/api-keys][OpenAI API key]].
Optional: Set =gptel-api-key= to the key or to a function that returns the key (more secure). Optional: Set =gptel-api-key= to the key or to a function that returns the key (more secure).
*** In a dedicated buffer:
Run =M-x gptel= to start or switch to the ChatGPT buffer. It will ask you for the key if you skipped the previous step. Run =M-x gptel= to start or switch to the ChatGPT buffer. It will ask you for the key if you skipped the previous step.
Run it with a prefix-arg (=C-u M-x gptel=) to start a new session. Run it with a prefix-arg (=C-u M-x gptel=) to start a new session.
@ -47,6 +49,14 @@ In the gptel buffer, send your prompt with =M-x gptel-send=, bound to =C-c RET=.
That's it. You can go back and edit previous prompts and responses if you want. That's it. You can go back and edit previous prompts and responses if you want.
The default mode is =markdown-mode= if available, else =text-mode=. You can set =gptel-default-mode= to =org-mode= if desired.
*** In any buffer:
Select a region of text, call =M-x gptel-send=.
The response will be inserted below your region. You can select both the original prompt and the resposne and call =M-x gptel-send= again to continue the conversation.
** Why another ChatGPT client? ** Why another ChatGPT client?
Existing Emacs clients don't /reliably/ let me use it the simple way I can in the browser. They will get better, but I wanted something for now. Existing Emacs clients don't /reliably/ let me use it the simple way I can in the browser. They will get better, but I wanted something for now.
@ -65,6 +75,3 @@ Maybe all of these, I don't know yet. As a start, I wanted to replicate the web
** Will you add feature X? ** Will you add feature X?
Maybe, I'd like to experiment a bit first. Maybe, I'd like to experiment a bit first.
- Support for Org Mode instead of Markdown, including source blocks etc, is planned.
- I'm experimenting with using it in code buffers.

View file

@ -85,6 +85,20 @@ When set to nil, it is inserted all at once.
:group 'gptel :group 'gptel
:type 'boolean) :type 'boolean)
(defcustom gptel-response-filter-functions
'(gptel--convert-org)
"Abnormal hook for transforming the response from ChatGPT.
This is useful if you want to format the response in some way,
such as filling paragraphs, adding annotations or recording
information in the response like links.
Each function in this hook receives two arguments, the response
string to transform and the ChatGPT interaction buffer. It should
return the transformed string."
:group 'gptel
:type 'hook)
(defvar gptel-default-session "*ChatGPT*") (defvar gptel-default-session "*ChatGPT*")
(defvar gptel-default-mode (if (featurep 'markdown-mode) (defvar gptel-default-mode (if (featurep 'markdown-mode)
'markdown-mode 'markdown-mode
@ -130,6 +144,8 @@ When set to nil, it is inserted all at once.
(status-str (plist-get response :status))) (status-str (plist-get response :status)))
(if content-str (if content-str
(with-current-buffer gptel-buffer (with-current-buffer gptel-buffer
(setq content-str (gptel--transform-response
content-str gptel-buffer))
(save-excursion (save-excursion
(put-text-property 0 (length content-str) 'gptel 'response content-str) (put-text-property 0 (length content-str) 'gptel 'response content-str)
(message "Querying ChatGPT... done.") (message "Querying ChatGPT... done.")
@ -185,13 +201,7 @@ instead."
prompts) prompts)
(and max-entries (cl-decf max-entries))) (and max-entries (cl-decf max-entries)))
(cons (list :role "system" (cons (list :role "system"
:content :content gptel--system-message)
(concat
(when (eq major-mode 'org-mode)
(concat
"In this conversation, format your responses as in an org-mode buffer in Emacs."
" Do NOT use Markdown. I repeat, use org-mode markup and not markdown.\n"))
gptel--system-message))
prompts))))) prompts)))))
(defun gptel--request-data (prompts) (defun gptel--request-data (prompts)
@ -205,7 +215,30 @@ instead."
(plist-put prompts-plist :max_tokens (gptel--numberize gptel--max-tokens))) (plist-put prompts-plist :max_tokens (gptel--numberize gptel--max-tokens)))
prompts-plist)) prompts-plist))
(aio-defun gptel--get-response (prompts) ;; TODO: Use `run-hook-wrapped' with an accumulator instead to handle
;; buffer-local hooks, etc.
(defun gptel--transform-response (content-str buffer)
(let ((filtered-str content-str))
(dolist (filter-func gptel-response-filter-functions filtered-str)
(condition-case nil
(when (functionp filter-func)
(setq filtered-str
(funcall filter-func filtered-str buffer)))
(error
(display-warning '(gptel filter-functions)
(format "Function %S returned an error"
filter-func)))))))
(defun gptel--convert-org (content buffer)
"Transform CONTENT according to required major-mode.
Currently only org-mode is handled.
BUFFER is the interaction buffer for ChatGPT."
(pcase (buffer-local-value 'major-mode buffer)
('org-mode (gptel--convert-markdown->org content))
(_ content)))
(aio-defun gptel--url-get-response (prompts) (aio-defun gptel--url-get-response (prompts)
"Fetch response for PROMPTS from ChatGPT. "Fetch response for PROMPTS from ChatGPT.
@ -287,6 +320,48 @@ Ask for API-KEY if `gptel-api-key' is unset."
(message "Send your query with %s!" (message "Send your query with %s!"
(substitute-command-keys "\\[gptel-send]")))) (substitute-command-keys "\\[gptel-send]"))))
(defun gptel--convert-markdown->org (str)
"Convert string STR from markdown to org markup.
This is a very basic converter that handles only a few markup
elements."
(interactive)
(with-temp-buffer
(insert str)
(goto-char (point-min))
(while (re-search-forward "`\\|\\*\\{1,2\\}\\|_" nil t)
(pcase (match-string 0)
("`" (if (looking-at "``")
(progn (backward-char)
(delete-char 3)
(insert "#+begin_src ")
(when (re-search-forward "^```" nil t)
(replace-match "#+end_src")))
(replace-match "=")))
("**" (cond
((looking-at "\\*\\(?:[[:word:]]\\|\s\\)")
(delete-char 1))
((looking-back "\\(?:[[:word:]]\\|\s\\)\\*\\{2\\}"
(max (- (point) 3) (point-min)))
(backward-delete-char 1))))
((or "_" "*")
(if (save-match-data
(and (looking-back "\\(?:[[:space:]]\\|\s\\)\\(?:_\\|\\*\\)"
(max (- (point) 2) (point-min)))
(not (looking-at "[[:space:]]\\|\s"))))
;; Possible beginning of italics
(and
(save-excursion
(when (and (re-search-forward (regexp-quote (match-string 0)) nil t)
(looking-at "[[:space]]\\|\s")
(not (looking-back "\\(?:[[:space]]\\|\s\\)\\(?:_\\|\\*\\)"
(max (- (point) 2) (point-min)))))
(backward-delete-char 1)
(insert "/") t))
(progn (backward-delete-char 1)
(insert "/")))))))
(buffer-string)))
(defun gptel--playback (buf content-str start-pt) (defun gptel--playback (buf content-str start-pt)
"Playback CONTENT-STR in BUF. "Playback CONTENT-STR in BUF.