branch: elpa/gptel
commit 8fca5bc762afb3c1b0f48f5e7273cfa0b48677de
Author: Karthik Chikmagalur <[email protected]>
Commit: Karthik Chikmagalur <[email protected]>

    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.
---
 README.org | 15 ++++++++---
 gptel.el   | 91 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
 2 files changed, 94 insertions(+), 12 deletions(-)

diff --git a/README.org b/README.org
index f148354686..b895e51a9a 100644
--- a/README.org
+++ b/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.
diff --git a/gptel.el b/gptel.el
index ed1f5d4300..b5d73103c5 100644
--- a/gptel.el
+++ b/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.
 

Reply via email to