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:
parent
b212c24c4a
commit
8fca5bc762
2 changed files with 94 additions and 12 deletions
15
README.org
15
README.org
|
@ -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]].
|
||||
- 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.
|
||||
- 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).
|
||||
|
||||
*** 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 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.
|
||||
|
||||
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?
|
||||
|
||||
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?
|
||||
|
||||
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.
|
||||
|
|
91
gptel.el
91
gptel.el
|
@ -85,6 +85,20 @@ When set to nil, it is inserted all at once.
|
|||
:group 'gptel
|
||||
: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-mode (if (featurep 'markdown-mode)
|
||||
'markdown-mode
|
||||
|
@ -130,6 +144,8 @@ When set to nil, it is inserted all at once.
|
|||
(status-str (plist-get response :status)))
|
||||
(if content-str
|
||||
(with-current-buffer gptel-buffer
|
||||
(setq content-str (gptel--transform-response
|
||||
content-str gptel-buffer))
|
||||
(save-excursion
|
||||
(put-text-property 0 (length content-str) 'gptel 'response content-str)
|
||||
(message "Querying ChatGPT... done.")
|
||||
|
@ -185,13 +201,7 @@ instead."
|
|||
prompts)
|
||||
(and max-entries (cl-decf max-entries)))
|
||||
(cons (list :role "system"
|
||||
:content
|
||||
(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))
|
||||
:content gptel--system-message)
|
||||
prompts)))))
|
||||
|
||||
(defun gptel--request-data (prompts)
|
||||
|
@ -205,7 +215,30 @@ instead."
|
|||
(plist-put prompts-plist :max_tokens (gptel--numberize gptel--max-tokens)))
|
||||
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)
|
||||
"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!"
|
||||
(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)
|
||||
"Playback CONTENT-STR in BUF.
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue