Compare commits

...

23 commits

Author SHA1 Message Date
Alain M. Lafon
3f31258e48
gptel: Add gptel-add-context function 2024-05-13 12:20:17 +02:00
Karthik Chikmagalur
8ccdc31b12 README: Mention llm and Ellama
* gptel.el (header): Add email

* README.org (Alternatives): Mention llm and Ellama
2024-05-03 08:41:10 -07:00
Karthik Chikmagalur
69fb2f09f3 Merge branch 'elpa/gptel' of https://git.sv.gnu.org/git/emacs/nongnu 2024-05-03 08:25:35 -07:00
Karthik Chikmagalur
533724042e README: Mention Org features
* README.org: Mention gptel's Org features, consult-web and use
consistent (lower-)casing for gptel.  Add a MELPA stable and a
NonGNU ELPA badge.
2024-05-02 17:11:10 -07:00
Karthik Chikmagalur
f663f3a9db README: Mention Org features
* README.org: Mention gptel's Org features, consult-web and use
consistent (lower-)casing for gptel.
2024-05-01 16:10:59 -07:00
Karthik Chikmagalur
cdb07d0d2b gptel: Update description and bump version
gptel.el (header): Update description and bump version.
2024-05-01 13:11:44 -07:00
Karthik Chikmagalur
97ab6cbd1e gptel: Add .elpaignore
* .elpaignore: Ignore the test directory
2024-04-29 13:05:47 -07:00
Karthik Chikmagalur
c319966997 gptel-org: Further improve stream converter
* test/gptel-org-test.el (gptel-org--test-compare-org): Add a
helper function to view the markdown input and org output
interactively.

* gptel-org.el (gptel--stream-convert-markdown->org): Handle
single asterisks at the end of messages.  Addresses #296.
2024-04-29 09:16:12 -07:00
Karthik Chikmagalur
4273f067e8 gptel-org: Improve stream converter
* test/gptel-org-test.el (gptel-org--test-stream-conversion): Add
a test harness to make comparing markdown to org easy.

* gptel-org.el (gptel--stream-convert-markdown->org): Handle
headings and strong chars in the converter.  Addresses #296.
2024-04-28 23:05:08 -07:00
Karthik Chikmagalur
b2985392f4 gptel: Linting for NonGNU ELPA
* gptel.el (gptel--next-variant, gptel--mark-response,
gptel--sanitize-model, gptel--url-parse-response,
gptel--at-word-end, gptel--get-api-key, gptel-log-level,
gptel-backend, gptel-temperature, gptel-model, gptel-max-tokens,
gptel-directives, gptel-crowdsourced-prompts-file,
gptel-display-buffer-action, gptel-use-header-line,
gptel-response-prefix-alist, gptel-prompt-prefix-alist,
gptel-post-stream-hook, gptel-post-response-functions,
gptel-pre-response-hook, gptel-response-filter-functions,
gptel-curl-file-size-threshold, gptel-stream, gptel-api-key,
gptel-proxy): Remove customization group "gptel" from all user
options, as this is inferred from the "gptel-" prefix.  Adjust
customization types for some options.  Minor linting and
formatting changes in multiple functions.
2024-04-28 09:16:22 -07:00
Karthik Chikmagalur
306fe3bd8c gptel-ollama: Fix parsing error (#179)
* gptel-ollama.el (gptel-curl--parse-stream): Don't throw an error
when regex-searching.
2024-04-27 20:14:17 -07:00
Karthik Chikmagalur
a2b16c43b1 gptel-org: Include org-element-lineage-map with gptel (#294)
* gptel-org.el (gptel-org--element-lineage-map): Include
`org-element-lineage-map` for compatibility with older versions of Org (pre
9.7).
2024-04-25 13:58:33 -07:00
Karthik Chikmagalur
44feb1637f gptel-transient: Update header-line in gptel--suffix-send
* gptel-transient.el (gptel--suffix-send): Update the header line
when using starting a request in `gptel--suffix-send`.  Fix #293.
2024-04-24 16:06:31 -07:00
Karthik Chikmagalur
66a63e6c82 gptel-ollama: switch to chat API
* gptel-ollama.el (gptel-curl--parse-stream,
gptel--parse-response, gptel--request-data, gptel--parse-buffer,
gptel--ollama-context, gptel--ollama-token-count,
gptel-make-ollama): Switch to Ollama's chat API from
the completions API.  This makes interacting with Ollama fully
stateless, like all the other APIs, and should help significantly
with issues like #249 and #279.  Support non-streaming responses
from Ollama in the process.

Remove `gptel--ollama-context` as it is no longer needed.

Add a `gptel--ollama-token-count` for tracking token costs. A UI
affordance for this is not implemented yet, but is planned.
2024-04-22 12:39:21 -07:00
Karthik Chikmagalur
9b094b8b1e gptel: Fix url-retrieve response parser bug
* gptel.el (gptel--url-parse-response): Fix quoted variable in
response parser.
2024-04-22 12:37:20 -07:00
Norio Suzuki
2b938114cf
gptel: Add GPT 4 Turbo (#286)
* gptel.el (gptel--openai, gptel-model): Add gpt-4-turbo model.
2024-04-20 10:35:26 -07:00
Cash Prokop-Weaver
70889ad95c
gptel-gemini: Add Gemini 1.5 (#284)
gptel-gemini.el (gptel-make-gemini): Add support for the Gemini
1.5 pro model. Closes #212.
2024-04-10 09:50:07 -07:00
Bryan Larsen
e994a443d3
README: add OpenRouter instructions (#282)
README: Add instructions for using OpenRouter.
2024-04-06 15:08:15 -07:00
Tiou Lims
b4088e3f7b
README: New pacakge based on gptel, magit-gptcommit (#281)
README: Mention magit-gptcommit.
2024-04-06 15:06:33 -07:00
Karthik Chikmagalur
7b6e3c5900 gptel: Release v0.8.5
* gptel.el: Bump version.
2024-04-04 01:13:46 -07:00
Karthik Chikmagalur
4d4b61af94 gptel-transient: More robust dry-run commands
* gptel.el (gptel--inspect-query): `gptel--inspect-query` now
takes data to display as an argument.  Reduce its function to
displaying a buffer with the data.

* gptel-transient.el (gptel-menu, gptel--suffix-send): Fold
dry-run the option into `gptel--suffix-send` and call it with a
dry-run flag instead of using an alternate pathway for dry-runs.
The "Inspect query" suffixes of `gptel-menu` now perform actual
dry-runs, avoiding issues like #276.
2024-04-04 01:13:46 -07:00
Karthik Chikmagalur
567af4d2ee gptel-org: Read config from Org properties (#141)
* gptel.el: Load gptel-org after Org is loaded.

* gptel-org.el (gptel-org--send-with-props): Advise `gptel-send`
and `gptel--suffix-send` to use gptel's local config (stored under
the current Org heading) when applicable.  Advice is a hacky way
to do it, but this is the simplest option without explicit
indirection via `derived-mode-p`-based dispatch code in many more
places in gptel.
2024-04-04 01:13:46 -07:00
Karthik Chikmagalur
f2fd2b13b0 gptel-org: Move response transform code for Org
* gptel.el (gptel--convert-markdown->org,
gptel--stream-convert-markdown->org, gptel-set-topic): Move code
for transforming responses and setting the GPTEL_TOPIC property to
gptel-org.  Add declarations for the byte-compiler.

* gptel-org.el (gptel-org-branching-context, gptel-org-set-topic,
gptel-org--restore-state, gptel--convert-markdown->org,
gptel--stream-convert-markdown->org): Add `gptel-org-set-topic` to
set the topic per heading in Org buffers.  Fix typo in
gptel-org--restore-state.  Add declarations for
the byte-compiler and page markers for the reader.
2024-04-04 01:13:46 -07:00
9 changed files with 681 additions and 440 deletions

1
.elpaignore Normal file
View file

@ -0,0 +1 @@
test

View file

@ -1,8 +1,8 @@
#+title: GPTel: A simple LLM client for Emacs
#+title: gptel: A simple LLM client for Emacs
[[https://melpa.org/#/gptel][file:https://melpa.org/packages/gptel-badge.svg]]
[[https://elpa.nongnu.org/nongnu/gptel.svg][file:https://elpa.nongnu.org/nongnu/gptel.svg]] [[https://stable.melpa.org/packages/gptel-badge.svg][file:https://stable.melpa.org/packages/gptel-badge.svg]] [[https://melpa.org/#/gptel][file:https://melpa.org/packages/gptel-badge.svg]]
GPTel is a simple Large Language Model chat client for Emacs, with support for multiple models and backends.
gptel is a simple Large Language Model chat client for Emacs, with support for multiple models and backends.
| LLM Backend | Supports | Requires |
|--------------------+----------+---------------------------|
@ -20,6 +20,7 @@ GPTel is a simple Large Language Model chat client for Emacs, with support for m
| Perplexity | ✓ | [[https://docs.perplexity.ai/docs/getting-started][API key]] |
| Anthropic (Claude) | ✓ | [[https://www.anthropic.com/api][API key]] |
| Groq | ✓ | [[https://console.groq.com/keys][API key]] |
| OpenRouter | ✓ | [[https://openrouter.ai/keys][API key]] |
*General usage*: ([[https://www.youtube.com/watch?v=bsRnh_brggM][YouTube Demo]])
@ -39,7 +40,7 @@ https://github-production-user-asset-6210df.s3.amazonaws.com/8607532/278854024-a
- You can go back and edit your previous prompts or LLM responses when continuing a conversation. These will be fed back to the model.
- Don't like gptel's workflow? Use it to create your own for any supported model/backend with a [[https://github.com/karthink/gptel/wiki#defining-custom-gptel-commands][simple API]].
GPTel uses Curl if available, but falls back to url-retrieve to work without external dependencies.
gptel uses Curl if available, but falls back to url-retrieve to work without external dependencies.
** Contents :toc:
- [[#installation][Installation]]
@ -61,10 +62,12 @@ GPTel uses Curl if available, but falls back to url-retrieve to work without ext
- [[#perplexity][Perplexity]]
- [[#anthropic-claude][Anthropic (Claude)]]
- [[#groq][Groq]]
- [[#openrouter][OpenRouter]]
- [[#usage][Usage]]
- [[#in-any-buffer][In any buffer:]]
- [[#in-a-dedicated-chat-buffer][In a dedicated chat buffer:]]
- [[#save-and-restore-your-chat-sessions][Save and restore your chat sessions]]
- [[#extra-org-mode-conveniences][Extra Org mode conveniences]]
- [[#faq][FAQ]]
- [[#i-want-the-window-to-scroll-automatically-as-the-response-is-inserted][I want the window to scroll automatically as the response is inserted]]
- [[#i-want-the-cursor-to-move-to-the-next-prompt-after-the-response-is-inserted][I want the cursor to move to the next prompt after the response is inserted]]
@ -76,13 +79,13 @@ GPTel uses Curl if available, but falls back to url-retrieve to work without ext
- [[#why-another-llm-client][Why another LLM client?]]
- [[#additional-configuration][Additional Configuration]]
- [[#alternatives][Alternatives]]
- [[#extensions-using-gptel][Extensions using GPTel]]
- [[#extensions-using-gptel][Extensions using gptel]]
- [[#breaking-changes][Breaking Changes]]
- [[#acknowledgments][Acknowledgments]]
** Installation
GPTel is on MELPA. Ensure that MELPA is in your list of sources, then install gptel with =M-x package-install⏎= =gptel=.
gptel is on MELPA. Ensure that MELPA is in your list of sources, then install it with =M-x package-install⏎= =gptel=.
(Optional: Install =markdown-mode=.)
@ -506,20 +509,71 @@ The above code makes the backend available to select. If you want it to be the
#+html: </details>
#+html: <details><summary>
**** OpenRouter
#+html: </summary>
Register a backend with
#+begin_src emacs-lisp
;; OpenRouter offers an OpenAI compatible API
(gptel-make-openai "OpenRouter" ;Any name you want
:host "openrouter.ai"
:endpoint "/api/v1/chat/completions"
:stream t
:key "your-api-key" ;can be a function that returns the key
:models '("openai/gpt-3.5-turbo"
"mistralai/mixtral-8x7b-instruct"
"meta-llama/codellama-34b-instruct"
"codellama/codellama-70b-instruct"
"google/palm-2-codechat-bison-32k"
"google/gemini-pro"))
#+end_src
You can pick this backend from the menu when using gptel (see [[#usage][Usage]]).
***** (Optional) Set as the default gptel backend
The above code makes the backend available to select. If you want it to be the default backend for gptel, you can set this as the value of =gptel-backend=. Use this instead of the above.
#+begin_src emacs-lisp
;; OPTIONAL configuration
(setq gptel-model "mixtral-8x7b-32768"
gptel-backend
(gptel-make-openai "OpenRouter" ;Any name you want
:host "openrouter.ai"
:endpoint "/api/v1/chat/completions"
:stream t
:key "your-api-key" ;can be a function that returns the key
:models '("openai/gpt-3.5-turbo"
"mistralai/mixtral-8x7b-instruct"
"meta-llama/codellama-34b-instruct"
"codellama/codellama-70b-instruct"
"google/palm-2-codechat-bison-32k"
"google/gemini-pro")))
#+end_src
#+html: </details>
** Usage
(This is also a [[https://www.youtube.com/watch?v=bsRnh_brggM][video demo]] showing various uses of gptel.)
|-------------------+-------------------------------------------------------------------------|
| *Command* | Description |
|-------------------+-------------------------------------------------------------------------|
| =gptel-send= | Send conversation up to =(point)=, or selection if region is active. Works anywhere in Emacs. |
| =gptel= | Create a new dedicated chat buffer. Not required to use gptel. |
| =C-u= =gptel-send= | Transient menu for preferences, input/output redirection etc. |
| =gptel-menu= | /(Same)/ |
|-------------------+-------------------------------------------------------------------------|
| =gptel-set-topic= | /(Org-mode only)/ Limit conversation context to an Org heading |
|-------------------+-------------------------------------------------------------------------|
|-----------------------------+------------------------------------------------------------------------------------------------|
| *Command* | Description |
|-----------------------------+------------------------------------------------------------------------------------------------|
| =gptel-send= | Send conversation up to =(point)=, or selection if region is active. Works anywhere in Emacs. |
| =gptel= | Create a new dedicated chat buffer. Not required to use gptel. |
| =C-u= =gptel-send= | Transient menu for preferences, input/output redirection etc. |
| =gptel-menu= | /(Same)/ |
|-----------------------------+------------------------------------------------------------------------------------------------|
| *Command* /(Org mode only)/ | |
|-----------------------------+------------------------------------------------------------------------------------------------|
| =gptel-org-set-topic= | Limit conversation context to an Org heading |
| =gptel-org-set-properties= | Write gptel configuration as Org properties (for self-contained chat logs) |
|-----------------------------+------------------------------------------------------------------------------------------------|
*** In any buffer:
@ -562,12 +616,51 @@ The default mode is =markdown-mode= if available, else =text-mode=. You can set
Saving the file will save the state of the conversation as well. To resume the chat, open the file and turn on =gptel-mode= before editing the buffer.
*** Extra Org mode conveniences
gptel offers a few extra conveniences in Org mode.
- You can limit the conversation context to an Org heading with the command =gptel-org-set-topic=.
- You can have branching conversations in Org mode, where each hierarchical outline path through the document is a separate conversation branch. This is also useful for limiting the context size of each query. See the variable =gptel-org-branching-context=.
- You can declare the gptel model, backend, temperature, system message and other parameters as Org properties with the command =gptel-org-set-properties=. gptel queries under the corresponding heading will always use these settings, allowing you to create mostly reproducible LLM chat notebooks, and to have simultaneous chats with different models, model settings and directives under different Org headings.
*** Optional: Add more context to your query
You can add contextual information from any Emacs buffer by utilizing
=gptel-add-context=. Each call will add another snippet including
metadata like buffer-name, LOC and major mode.
1. Select the text you want to add as context. If no text is selected,
the entire content of the current buffer will be used.
2. =gptel-add-context= adds the selected text or the whole buffer
content to the "*gptel-context*" buffer.
3. Proceed with LLM interactions using =gptel= as usual. The added
context will influence the LLM's responses, making them more
relevant and contextualized.
4. At any point, you can manually edit the "*gptel-context*" buffer to
remove stale information.
**** Practical Applications
- Enhancing code development sessions with relevant documentation or
code snippets as a reference.
- Accumulating research notes or sources while writing papers or
articles to ensure consistency in the narrative or arguments.
- Providing detailed error logs or system information during debugging
sessions to assist in generating more accurate solutions or
suggestions from the LLM.
** FAQ
#+html: <details><summary>
**** I want the window to scroll automatically as the response is inserted
#+html: </summary>
To be minimally annoying, GPTel does not move the cursor by default. Add the following to your configuration to enable auto-scrolling.
To be minimally annoying, gptel does not move the cursor by default. Add the following to your configuration to enable auto-scrolling.
#+begin_src emacs-lisp
(add-hook 'gptel-post-stream-hook 'gptel-auto-scroll)
@ -578,7 +671,7 @@ To be minimally annoying, GPTel does not move the cursor by default. Add the fo
**** I want the cursor to move to the next prompt after the response is inserted
#+html: </summary>
To be minimally annoying, GPTel does not move the cursor by default. Add the following to your configuration to move the cursor:
To be minimally annoying, gptel does not move the cursor by default. Add the following to your configuration to move the cursor:
#+begin_src emacs-lisp
(add-hook 'gptel-post-response-functions 'gptel-end-of-response)
@ -625,7 +718,7 @@ Or see this [[https://github.com/karthink/gptel/wiki#save-transient-flags][wiki
**** I want to use gptel in a way that's not supported by =gptel-send= or the options menu
#+html: </summary>
GPTel's default usage pattern is simple, and will stay this way: Read input in any buffer and insert the response below it. Some custom behavior is possible with the transient menu (=C-u M-x gptel-send=).
gptel's default usage pattern is simple, and will stay this way: Read input in any buffer and insert the response below it. Some custom behavior is possible with the transient menu (=C-u M-x gptel-send=).
For more programmable usage, gptel provides a general =gptel-request= function that accepts a custom prompt and a callback to act on the response. You can use this to build custom workflows not supported by =gptel-send=. See the documentation of =gptel-request=, and the [[https://github.com/karthink/gptel/wiki][wiki]] for examples.
@ -732,17 +825,21 @@ Features being considered or in the pipeline:
Other Emacs clients for LLMs include
- [[https://github.com/ahyatt/llm][llm]]: llm provides a uniform API across language model providers for building LLM clients in Emacs, and is intended as a library for use by package authors. For similar scripting purposes, gptel provides the command =gptel-request=, which see.
- [[https://github.com/s-kostyaev/ellama][Ellama]]: A full-fledged LLM client built on llm, that supports many LLM providers (Ollama, Open AI, Vertex, GPT4All and more). Its usage differs from gptel in that it provides separate commands for dozens of common tasks, like general chat, summarizing code/text, refactoring code, improving grammar, translation and so on.
- [[https://github.com/xenodium/chatgpt-shell][chatgpt-shell]]: comint-shell based interaction with ChatGPT. Also supports DALL-E, executable code blocks in the responses, and more.
- [[https://github.com/rksm/org-ai][org-ai]]: Interaction through special =#+begin_ai ... #+end_ai= Org-mode blocks. Also supports DALL-E, querying ChatGPT with the contents of project files, and more.
There are several more: [[https://github.com/CarlQLange/chatgpt-arcana.el][chatgpt-arcana]], [[https://github.com/MichaelBurge/leafy-mode][leafy-mode]], [[https://github.com/iwahbe/chat.el][chat.el]]
*** Extensions using GPTel
*** Extensions using gptel
These are packages that depend on GPTel to provide additional functionality
These are packages that use gptel to provide additional functionality
- [[https://github.com/kamushadenes/gptel-extensions.el][gptel-extensions]]: Extra utility functions for GPTel.
- [[https://github.com/kamushadenes/gptel-extensions.el][gptel-extensions]]: Extra utility functions for gptel.
- [[https://github.com/kamushadenes/ai-blog.el][ai-blog.el]]: Streamline generation of blog posts in Hugo.
- [[https://github.com/douo/magit-gptcommit][magit-gptcommit]]: Generate Commit Messages within magit-status Buffer using gptel.
- [[https://github.com/armindarvish/consult-web][consult-web]]: Provides gptel as a source when querying multiple local and online sources.
** Breaking Changes

View file

@ -36,6 +36,8 @@
(declare-function json-read "json" ())
(defvar json-object-type)
(declare-function gptel--stream-convert-markdown->org "gptel-org")
(defconst gptel-curl--common-args
(if (memq system-type '(windows-nt ms-dos))
'("--disable" "--location" "--silent" "-XPOST"

View file

@ -114,7 +114,8 @@
(name &key curl-args header key (stream nil)
(host "generativelanguage.googleapis.com")
(protocol "https")
(models '("gemini-pro"))
(models '("gemini-pro"
"gemini-1.5-pro-latest"))
(endpoint "/v1beta/models"))
"Register a Gemini backend for gptel with NAME.

View file

@ -34,79 +34,94 @@
(:copier nil)
(:include gptel-backend)))
(defvar-local gptel--ollama-context nil
"Context for ollama conversations.
(defvar-local gptel--ollama-token-count 0
"Token count for ollama conversations.
This variable holds the context array for conversations with
Ollama models.")
This variable holds the total token count for conversations with
Ollama models.
Intended for internal use only.")
(cl-defmethod gptel-curl--parse-stream ((_backend gptel-ollama) info)
";TODO: "
(when (bobp)
(re-search-forward "^{")
"Parse response stream for the Ollama API."
(when (and (bobp) (re-search-forward "^{" nil t))
(forward-line 0))
(let* ((content-strs) (content) (pt (point)))
(condition-case nil
(while (setq content (gptel--json-read))
(setq pt (point))
(let ((done (map-elt content :done))
(response (map-elt content :response)))
(response (map-nested-elt content '(:message :content))))
(push response content-strs)
(unless (eq done :json-false)
(with-current-buffer (plist-get info :buffer)
(setq gptel--ollama-context (map-elt content :context)))
(cl-incf gptel--ollama-token-count
(+ (or (map-elt content :prompt_eval_count) 0)
(or (map-elt content :eval_count) 0))))
(goto-char (point-max)))))
(error (goto-char pt)))
(apply #'concat (nreverse content-strs))))
(cl-defmethod gptel--parse-response ((_backend gptel-ollama) response info)
(when-let ((context (map-elt response :context)))
"Parse a one-shot RESPONSE from the Ollama API."
(when-let ((context
(+ (or (map-elt response :prompt_eval_count) 0)
(or (map-elt response :eval_count) 0))))
(with-current-buffer (plist-get info :buffer)
(setq gptel--ollama-context context)))
(map-elt response :response))
(cl-incf gptel--ollama-token-count context)))
(map-nested-elt response '(:message :content)))
(cl-defmethod gptel--request-data ((_backend gptel-ollama) prompts)
"JSON encode PROMPTS for Ollama."
"JSON encode PROMPTS for sending to ChatGPT."
(let ((prompts-plist
`(:model ,gptel-model
,@prompts
:messages [,@prompts]
:stream ,(or (and gptel-stream gptel-use-curl
(gptel-backend-stream gptel-backend))
:json-false))))
(when gptel--ollama-context
(plist-put prompts-plist :context gptel--ollama-context))
(gptel-backend-stream gptel-backend))
:json-false)))
options-plist)
(when gptel-temperature
(setq options-plist
(plist-put options-plist :temperature
gptel-temperature)))
(when gptel-max-tokens
(setq options-plist
(plist-put options-plist :num_predict
gptel-max-tokens)))
(when options-plist
(plist-put prompts-plist :options options-plist))
prompts-plist))
(cl-defmethod gptel--parse-buffer ((_backend gptel-ollama) &optional _max-entries)
(let ((prompts)
(prop (text-property-search-backward
'gptel 'response
(when (get-char-property (max (point-min) (1- (point)))
'gptel)
t))))
(if (and (prop-match-p prop)
(prop-match-value prop))
(user-error "No user prompt found!")
(setq prompts (list
:system gptel--system-message
:prompt
(if (prop-match-p prop)
(string-trim
(buffer-substring-no-properties (prop-match-beginning prop)
(prop-match-end prop))
(format "[\t\r\n ]*\\(?:%s\\)?[\t\r\n ]*"
(regexp-quote (gptel-prompt-prefix-string)))
(format "[\t\r\n ]*\\(?:%s\\)?[\t\r\n ]*"
(regexp-quote (gptel-response-prefix-string))))
"")))
prompts)))
(cl-defmethod gptel--parse-buffer ((_backend gptel-ollama) &optional max-entries)
(let ((prompts) (prop))
(while (and
(or (not max-entries) (>= max-entries 0))
(setq prop (text-property-search-backward
'gptel 'response
(when (get-char-property (max (point-min) (1- (point)))
'gptel)
t))))
(push (list :role (if (prop-match-value prop) "assistant" "user")
:content
(string-trim
(buffer-substring-no-properties (prop-match-beginning prop)
(prop-match-end prop))
(format "[\t\r\n ]*\\(?:%s\\)?[\t\r\n ]*"
(regexp-quote (gptel-prompt-prefix-string)))
(format "[\t\r\n ]*\\(?:%s\\)?[\t\r\n ]*"
(regexp-quote (gptel-response-prefix-string)))))
prompts)
(and max-entries (cl-decf max-entries)))
(cons (list :role "system"
:content gptel--system-message)
prompts)))
;;;###autoload
(cl-defun gptel-make-ollama
(name &key curl-args header key models stream
(host "localhost:11434")
(protocol "http")
(endpoint "/api/generate"))
(endpoint "/api/chat"))
"Register an Ollama backend for gptel with NAME.
Keyword arguments:

View file

@ -3,7 +3,7 @@
;; Copyright (C) 2024 Karthik Chikmagalur
;; Author: Karthik Chikmagalur <karthikchikmagalur@gmail.com>
;; Keywords:
;; Keywords:
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
@ -20,14 +20,53 @@
;;; Commentary:
;;
;;
;;; Code:
(eval-when-compile (require 'cl-lib))
(require 'org-element)
(require 'outline)
(declare-function org-element-begin "org-element")
;; Functions used for saving/restoring gptel state in Org buffers
(defvar org-entry-property-inherited-from)
(declare-function org-entry-get "org")
(declare-function org-entry-put "org")
(declare-function org-with-wide-buffer "org-macs")
(declare-function org-set-property "org")
(declare-function org-property-values "org")
(declare-function org-open-line "org")
(declare-function org-at-heading-p "org")
(declare-function org-get-heading "org")
(declare-function org-at-heading-p "org")
;; Bundle `org-element-lineage-map' if it's not available (for Org 9.67 or older)
(eval-when-compile
(if (fboundp 'org-element-lineage-map)
(progn (declare-function org-element-lineage-map "org-element-ast")
(defalias 'gptel-org--element-lineage-map 'org-element-lineage-map))
(defun gptel-org--element-lineage-map (datum fun &optional types with-self first-match)
"Map FUN across ancestors of DATUM, from closest to furthest.
DATUM is an object or element. For TYPES, WITH-SELF and
FIRST-MATCH see `org-element-lineage-map'.
This function is provided for compatibility with older versions
of Org."
(declare (indent 2))
(setq fun (if (functionp fun) fun `(lambda (node) ,fun)))
(let ((up (if with-self datum (org-element-parent datum)))
acc rtn)
(catch :--first-match
(while up
(when (or (not types) (org-element-type-p up types))
(setq rtn (funcall fun up))
(if (and first-match rtn)
(throw :--first-match rtn)
(when rtn (push rtn acc))))
(setq up (org-element-parent up)))
(nreverse acc))))))
;;; User options
@ -118,7 +157,7 @@ value of `gptel-org-branching-context', which see."
(unless prompt-end (setq prompt-end (point)))
(let ((max-entries (and gptel--num-messages-to-send
(* 2 gptel--num-messages-to-send)))
(topic-start (gptel--get-topic-start)))
(topic-start (gptel-org--get-topic-start)))
(when topic-start
;; narrow to GPTEL_TOPIC property scope
(narrow-to-region topic-start prompt-end))
@ -126,7 +165,7 @@ value of `gptel-org-branching-context', which see."
;; Create prompt from direct ancestors of point
(save-excursion
(let* ((org-buf (current-buffer))
(start-bounds (org-element-lineage-map
(start-bounds (gptel-org--element-lineage-map
(org-element-at-point) #'org-element-begin
'(headline org-data) 'with-self))
(end-bounds
@ -161,6 +200,31 @@ value of `gptel-org-branching-context', which see."
;; Create prompt the usual way
(gptel--parse-buffer gptel-backend max-entries))))
(defun gptel-org--send-with-props (send-fun &rest args)
"Conditionally modify SEND-FUN's calling environment.
If in an Org buffer under a heading containing a stored gptel
configuration, use that for requests instead. This includes the
system message, model and provider (backend), among other
parameters."
(if (derived-mode-p 'org-mode)
(pcase-let ((`(,gptel--system-message ,gptel-backend ,gptel-model
,gptel-temperature ,gptel-max-tokens)
(seq-mapn (lambda (a b) (or a b))
(gptel-org--entry-properties)
(list gptel--system-message gptel-backend gptel-model
gptel-temperature gptel-max-tokens))))
(apply send-fun args))
(apply send-fun args)))
(advice-add 'gptel-send :around #'gptel-org--send-with-props)
(advice-add 'gptel--suffix-send :around #'gptel-org--send-with-props)
;; ;; NOTE: Basic uses in org-mode are covered by advising gptel-send and
;; ;; gptel--suffix-send. For custom commands it might be necessary to advise
;; ;; gptel-request instead.
;; (advice-add 'gptel-request :around #'gptel-org--send-with-props)
;;; Saving and restoring state
(defun gptel-org--entry-properties (&optional pt)
@ -181,14 +245,6 @@ value of `gptel-org-branching-context', which see."
(when tokens (setq tokens (gptel--numberize tokens)))
(list system backend model temperature tokens)))
;; (pcase-let ((`(,gptel--system-message ,gptel-backend
;; ,gptel-model ,gptel-temperature)
;; (if (derived-mode-p 'org-mode)
;; (progn (require 'gptel-org)
;; (gptel-org--entry-properties))
;; `(,gptel--system-message ,gptel-backend
;; ,gptel-model ,gptel-temperature)))))
(defun gptel-org--restore-state ()
"Restore gptel state for Org buffers when turning on `gptel-mode'."
(save-restriction
@ -231,10 +287,8 @@ non-nil (default), display a message afterwards."
(unless (equal (default-value 'gptel-temperature) gptel-temperature)
(org-entry-put pt "GPTEL_TEMPERATURE"
(number-to-string gptel-temperature)))
(unless (string= (default-value 'gptel--system-message)
gptel--system-message)
(org-entry-put pt "GPTEL_SYSTEM"
(string-replace "\n" "\\n" gptel--system-message)))
(org-entry-put pt "GPTEL_SYSTEM"
(string-replace "\n" "\\n" gptel--system-message))
(when gptel-max-tokens
(org-entry-put
pt "GPTEL_MAX_TOKENS" (number-to-string gptel-max-tokens)))
@ -260,7 +314,171 @@ non-nil (default), display a message afterwards."
(> attempts 0))
(funcall write-bounds (1- attempts)))))))
(funcall write-bounds 6))))
;;; Transforming responses
(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 (save-excursion
(beginning-of-line)
(skip-chars-forward " \t")
(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)))
(delete-char -1))))
("*"
(cond
((save-match-data
(and (looking-back "\\(?:[[:space:]]\\|\s\\)\\(?:_\\|\\*\\)"
(max (- (point) 2) (point-min)))
(not (looking-at "[[:space:]]\\|\s"))))
;; Possible beginning of emphasis
(and
(save-excursion
(when (and (re-search-forward (regexp-quote (match-string 0))
(line-end-position) t)
(looking-at "[[:space]]\\|\s")
(not (looking-back "\\(?:[[:space]]\\|\s\\)\\(?:_\\|\\*\\)"
(max (- (point) 2) (point-min)))))
(delete-char -1) (insert "/") t))
(progn (delete-char -1) (insert "/"))))
((save-excursion
(ignore-errors (backward-char 2))
(looking-at "\\(?:$\\|\\`\\)\n\\*[[:space:]]"))
;; Bullet point, replace with hyphen
(delete-char -1) (insert "-"))))))
(buffer-string)))
(defun gptel--replace-source-marker (num-ticks &optional end)
"Replace markdown style backticks with Org equivalents.
NUM-TICKS is the number of backticks being replaced. If END is
true these are \"ending\" backticks.
This is intended for use in the markdown to org stream converter."
(let ((from (match-beginning 0)))
(delete-region from (point))
(if (and (= num-ticks 3)
(save-excursion (beginning-of-line)
(skip-chars-forward " \t")
(eq (point) from)))
(insert (if end "#+end_src" "#+begin_src "))
(insert "="))))
(defun gptel--stream-convert-markdown->org ()
"Return a Markdown to Org converter.
This function parses a stream of Markdown text to Org
continuously when it is called with successive chunks of the
text stream."
(letrec ((in-src-block nil) ;explicit nil to address BUG #183
(temp-buf (generate-new-buffer-name "*gptel-temp*"))
(start-pt (make-marker))
(ticks-total 0)
(cleanup-fn
(lambda (&rest _)
(when (buffer-live-p (get-buffer temp-buf))
(set-marker start-pt nil)
(kill-buffer temp-buf))
(remove-hook 'gptel-post-response-functions cleanup-fn))))
(add-hook 'gptel-post-response-functions cleanup-fn)
(lambda (str)
(let ((noop-p) (ticks 0))
(with-current-buffer (get-buffer-create temp-buf)
(save-excursion (goto-char (point-max)) (insert str))
(when (marker-position start-pt) (goto-char start-pt))
(when in-src-block (setq ticks ticks-total))
(save-excursion
(while (re-search-forward "`\\|\\*\\{1,2\\}\\|_\\|^#+" nil t)
(pcase (match-string 0)
("`"
;; Count number of consecutive backticks
(backward-char)
(while (and (char-after) (eq (char-after) ?`))
(forward-char)
(if in-src-block (cl-decf ticks) (cl-incf ticks)))
;; Set the verbatim state of the parser
(if (and (eobp)
;; Special case heuristic: If the response ends with
;; ^``` we don't wait for more input.
;; FIXME: This can have false positives.
(not (save-excursion (beginning-of-line)
(looking-at "^```$"))))
;; End of input => there could be more backticks coming,
;; so we wait for more input
(progn (setq noop-p t) (set-marker start-pt (match-beginning 0)))
;; We reached a character other than a backtick
(cond
;; Ticks balanced, end src block
((= ticks 0)
(progn (setq in-src-block nil)
(gptel--replace-source-marker ticks-total 'end)))
;; Positive number of ticks, start an src block
((and (> ticks 0) (not in-src-block))
(setq ticks-total ticks
in-src-block t)
(gptel--replace-source-marker ticks-total))
;; Negative number of ticks or in a src block already,
;; reset ticks
(t (setq ticks ticks-total)))))
;; Handle other chars: heading, emphasis, bold and bullet items
((and (guard (and (not in-src-block) (eq (char-before) ?#))) heading)
(if (eobp)
;; Not enough information about the heading yet
(progn (setq noop-p t) (set-marker start-pt (match-beginning 0)))
;; Convert markdown heading to Org heading
(when (looking-at "[[:space:]]")
(delete-region (line-beginning-position) (point))
(insert (make-string (length heading) ?*)))))
((and "**" (guard (not in-src-block)))
(cond
;; TODO Not sure why this branch was needed
;; ((looking-at "\\*\\(?:[[:word:]]\\|\s\\)") (delete-char 1))
;; Looking back at "w**" or " **"
((looking-back "\\(?:[[:word:][:punct:]\n]\\|\s\\)\\*\\{2\\}"
(max (- (point) 3) (point-min)))
(delete-char -1))))
((and "*" (guard (not in-src-block)))
(if (eobp)
;; Not enough information about the "*" yet
(progn (setq noop-p t) (set-marker start-pt (match-beginning 0)))
;; "*" is either emphasis or a bullet point
(save-match-data
(save-excursion
(ignore-errors (backward-char 2))
(cond
((or (looking-at
"[^[:space:][:punct:]\n]\\(?:_\\|\\*\\)\\(?:[[:space:][:punct:]]\\|$\\)")
(looking-at
"\\(?:[[:space:][:punct:]]\\)\\(?:_\\|\\*\\)\\([^[:space:][:punct:]]\\|$\\)"))
;; Emphasis, replace with slashes
(forward-char 2) (delete-char -1) (insert "/"))
((looking-at "\\(?:$\\|\\`\\)\n\\*[[:space:]]")
;; Bullet point, replace with hyphen
(forward-char 2) (delete-char -1) (insert "-"))))))))))
(if noop-p
(buffer-substring (point) start-pt)
(prog1 (buffer-substring (point) (point-max))
(set-marker start-pt (point-max)))))))))
(provide 'gptel-org)
;;; gptel-org.el ends here

View file

@ -336,24 +336,19 @@ Also format its value in the Transient menu."
(lambda ()
"Inspect the query that will be sent as a lisp object."
(interactive)
(let* ((extra (gptel--get-directive
(transient-args
transient-current-command)))
(gptel--system-message
(concat gptel--system-message extra)))
(gptel--sanitize-model)
(gptel--inspect-query))))
(gptel--sanitize-model)
(gptel--inspect-query
(gptel--suffix-send
(cons "I" (transient-args transient-current-command))))))
("J" "Inspect query (JSON)"
(lambda ()
"Inspect the query that will be sent as a JSON object."
(interactive)
(let* ((extra (gptel--get-directive
(transient-args
transient-current-command)))
(gptel--system-message
(concat gptel--system-message extra)))
(gptel--sanitize-model)
(gptel--inspect-query 'json))))]]
(gptel--sanitize-model)
(gptel--inspect-query
(gptel--suffix-send
(cons "I" (transient-args transient-current-command)))
'json)))]]
(interactive)
(gptel--sanitize-model)
(transient-setup 'gptel-menu))
@ -594,6 +589,7 @@ Or in an extended conversation:
(buffer) (position)
(callback) (gptel-buffer-name)
(system-extra (gptel--get-directive args))
(dry-run (and (member "I" args) t))
;; Input redirection: grab prompt from elsewhere?
(prompt
(cond
@ -689,44 +685,49 @@ Or in an extended conversation:
(setq buffer (get-buffer-create gptel-buffer-name))
(with-current-buffer buffer (setq position (point)))))
(gptel-request
prompt
:buffer (or buffer (current-buffer))
:position position
:in-place (and in-place (not output-to-other-buffer-p))
:stream stream
:system (concat gptel--system-message system-extra)
:callback callback)
(prog1 (gptel-request prompt
:buffer (or buffer (current-buffer))
:position position
:in-place (and in-place (not output-to-other-buffer-p))
:stream stream
:system (concat gptel--system-message system-extra)
:callback callback
:dry-run dry-run)
;; NOTE: Possible future race condition here if Emacs ever drops the GIL.
;; The HTTP request callback might modify the buffer before the in-place
;; text is killed below.
(when in-place
;; Kill the latest prompt
(let ((beg
(if (use-region-p)
(region-beginning)
(save-excursion
(text-property-search-backward
'gptel 'response
(when (get-char-property (max (point-min) (1- (point)))
'gptel)
t))
(point))))
(end (if (use-region-p) (region-end) (point))))
(unless output-to-other-buffer-p
;; store the killed text in gptel-history
(gptel--attach-response-history
(list (buffer-substring-no-properties beg end))))
(kill-region beg end)))
(gptel--update-status " Waiting..." 'warning)
(when output-to-other-buffer-p
(message (concat "Prompt sent to buffer: "
(propertize gptel-buffer-name 'face 'help-key-binding)))
(display-buffer
buffer '((display-buffer-reuse-window
display-buffer-pop-up-window)
(reusable-frames . visible))))))
;; NOTE: Possible future race condition here if Emacs ever drops the GIL.
;; The HTTP request callback might modify the buffer before the in-place
;; text is killed below.
(when in-place
;; Kill the latest prompt
(let ((beg
(if (use-region-p)
(region-beginning)
(save-excursion
(text-property-search-backward
'gptel 'response
(when (get-char-property (max (point-min) (1- (point)))
'gptel)
t))
(point))))
(end (if (use-region-p) (region-end) (point))))
(unless output-to-other-buffer-p
;; store the killed text in gptel-history
(gptel--attach-response-history
(list (buffer-substring-no-properties beg end))))
(kill-region beg end)))
(when output-to-other-buffer-p
(message (concat "Prompt sent to buffer: "
(propertize gptel-buffer-name 'face 'help-key-binding)))
(display-buffer
buffer '((display-buffer-reuse-window
display-buffer-pop-up-window)
(reusable-frames . visible)))))))
;; Allow calling from elisp
(put 'gptel--suffix-send 'interactive-only nil)
;; ** Suffix to regenerate response

415
gptel.el
View file

@ -2,8 +2,8 @@
;; Copyright (C) 2023 Karthik Chikmagalur
;; Author: Karthik Chikmagalur
;; Version: 0.7.0
;; Author: Karthik Chikmagalur <karthik.chikmagalur@gmail.com>
;; Version: 0.8.6
;; Package-Requires: ((emacs "27.1") (transient "0.4.0") (compat "29.1.4.1"))
;; Keywords: convenience
;; URL: https://github.com/karthink/gptel
@ -32,7 +32,7 @@
;; gptel supports
;;
;; - The services ChatGPT, Azure, Gemini, Anthropic AI, Anyscale, Together.ai,
;; Perplexity, and Kagi (FastGPT & Summarizer)
;; Perplexity, Anyscale, OpenRouter, Groq and Kagi (FastGPT & Summarizer)
;; - Local models via Ollama, Llama.cpp, Llamafiles or GPT4All
;;
;; Additionally, any LLM service (local or remote) that provides an
@ -54,10 +54,14 @@
;; key or to a function of no arguments that returns the key. (It tries to
;; use `auth-source' by default)
;;
;; ChatGPT is configured out of the box. For the other sources:
;;
;; - For Azure: define a gptel-backend with `gptel-make-azure', which see.
;; - For Gemini: define a gptel-backend with `gptel-make-gemini', which see.
;; - For Anthropic (Claude): define a gptel-backend with `gptel-make-anthropic',
;; which see
;; - For Together.ai, Anyscale, Perplexity, Groq and OpenRouter: define a
;; gptel-backend with `gptel-make-openai', which see.
;; - For Kagi: define a gptel-backend with `gptel-make-kagi', which see.
;;
;; For local models using Ollama, Llama.cpp or GPT4All:
@ -65,6 +69,7 @@
;; - The model has to be running on an accessible address (or localhost)
;; - Define a gptel-backend with `gptel-make-ollama' or `gptel-make-gpt4all',
;; which see.
;; - Llama.cpp or Llamafiles: Define a gptel-backend with `gptel-make-openai',
;;
;; Consult the package README for examples and more help with configuring
;; backends.
@ -81,14 +86,14 @@
;; - Call `gptel-send' to send the text up to the cursor. Select a region to
;; send only the region.
;;
;; - You can select previous prompts and responses to
;; continue the conversation.
;; - You can select previous prompts and responses to continue the conversation.
;;
;; - Call `gptel-send' with a prefix argument to access a menu where you can set
;; your backend, model and other parameters, or to redirect the
;; prompt/response.
;;
;; To use this in a dedicated buffer:
;;
;; - M-x gptel: Start a chat session
;; - C-u M-x gptel: Start another session or multiple independent chat sessions
;;
@ -98,11 +103,28 @@
;; model, or choose to redirect the input or output elsewhere (such as to the
;; kill ring).
;;
;; - You can save this buffer to a file. When opening this file, turning on
;; `gptel-mode' will allow resuming the conversation.
;; - You can save this buffer to a file. When opening this file, turn on
;; `gptel-mode' before editing it to restore the conversation state and
;; continue chatting.
;;
;; gptel in Org mode:
;;
;; gptel offers a few extra conveniences in Org mode.
;; - You can limit the conversation context to an Org heading with
;; `gptel-org-set-topic'.
;;
;; - You can have branching conversations in Org mode, where each hierarchical
;; outline path through the document is a separate conversation branch.
;; See the variable `gptel-org-branching-context'.
;;
;; - You can declare the gptel model, backend, temperature, system message and
;; other parameters as Org properties with the command
;; `gptel-org-set-properties'. gptel queries under the corresponding heading
;; will always use these settings, allowing you to create mostly reproducible
;; LLM chat notebooks.
;;
;; Finally, gptel offers a general purpose API for writing LLM ineractions
;; that suit how you work, see `gptel-request'.
;; that suit your workflow, see `gptel-request'.
;;; Code:
(declare-function markdown-mode "markdown-mode")
@ -111,19 +133,18 @@
(declare-function gptel-system-prompt "gptel-transient")
(declare-function pulse-momentary-highlight-region "pulse")
;; Functions used for saving/restoring gptel state in Org buffers
(defvar org-entry-property-inherited-from)
(declare-function org-entry-get "org")
(declare-function org-entry-put "org")
(declare-function org-with-wide-buffer "org-macs")
(declare-function org-set-property "org")
(declare-function org-property-values "org")
(declare-function org-open-line "org")
(declare-function org-at-heading-p "org")
(declare-function org-get-heading "org")
(declare-function ediff-make-cloned-buffer "ediff-util")
(declare-function ediff-regions-internal "ediff")
(declare-function gptel-org--create-prompt "gptel-org")
(declare-function gptel-org-set-topic "gptel-org")
(declare-function gptel-org--save-state "gptel-org")
(declare-function gptel-org--restore-state "gptel-org")
(declare-function gptel--stream-convert-markdown->org "gptel-org")
(declare-function gptel--convert-markdown->org "gptel-org")
(define-obsolete-function-alias
'gptel-set-topic 'gptel-org-set-topic "0.7.5")
(eval-when-compile
(require 'subr-x)
(require 'cl-lib))
@ -134,6 +155,9 @@
(require 'cl-generic)
(require 'gptel-openai)
(with-eval-after-load 'org
(require 'gptel-org))
;; User options
@ -150,11 +174,19 @@
"Use `gptel-make-openai' instead."
"0.5.0")
(defcustom gptel-context-prompt-preamble
"Here, you find more context for the following user prompts. Key aspects are:
- User inputs are encapsulated within Emacs Org mode src blocks.
- Naming Convention: Each src block is identified using a structured name format '{{name-of-original-buffer}}:{{beginning-line-number}}-{{ending-line-number}}'. This scheme offers insight into the origin and scope of the code or text snippet.
- Mode Indication: The mode of the original file is included within each src block. This detail informs you about the programming language or markup format of the snippet, aiding in accurate interpretation and response."
"Instruct the llm about how to treat the additional context from *gptel-context*."
:group 'gptel
:type 'string)
(defcustom gptel-proxy ""
"Path to a proxy to use for gptel interactions.
Passed to curl via --proxy arg, for example \"proxy.yourorg.com:80\"
Leave it empty if you don't use a proxy."
:group 'gptel
:type 'string)
(defcustom gptel-api-key #'gptel-api-key-from-auth-source
@ -164,7 +196,6 @@ OpenAI by default.
Can also be a function of no arguments that returns an API
key (more secure) for the active backend."
:group 'gptel
:type '(choice
(string :tag "API key")
(function :tag "Function that returns the API key")))
@ -180,13 +211,11 @@ When set to nil, Emacs waits for the full response and inserts it
all at once. This wait is asynchronous.
\='tis a bit silly."
:group 'gptel
:type 'boolean)
(make-obsolete-variable 'gptel-playback 'gptel-stream "0.3.0")
(defcustom gptel-use-curl (and (executable-find "curl") t)
"Whether gptel should prefer Curl when available."
:group 'gptel
:type 'boolean)
(defcustom gptel-curl-file-size-threshold 130000
@ -205,11 +234,10 @@ and the typical size of the data being sent in GPTel queries.
A larger value may improve performance by avoiding the overhead of creating
temporary files for small data payloads, while a smaller value may be needed
if the command-line argument size is limited by the operating system."
:group 'gptel
:type 'integer)
:type 'natnum)
(defcustom gptel-response-filter-functions
'(gptel--convert-org)
(list #'gptel--convert-org)
"Abnormal hook for transforming the response from an LLM.
This is used to format the response in some way, such as filling
@ -223,7 +251,6 @@ should return the transformed string.
NOTE: This is only used for non-streaming responses. To
transform streaming responses, use `gptel-post-stream-hook' and
`gptel-post-response-functions'."
:group 'gptel
:type 'hook)
(defcustom gptel-pre-response-hook nil
@ -233,7 +260,6 @@ This hook is called in the buffer where the LLM response will be
inserted.
Note: this hook only runs if the request succeeds."
:group 'gptel
:type 'hook)
(define-obsolete-variable-alias
@ -253,7 +279,6 @@ end positions.
Note: this hook runs even if the request fails. In this case the
response beginning and end positions are both the cursor position
at the time of the request."
:group 'gptel
:type 'hook)
;; (defcustom gptel-pre-stream-insert-hook nil
@ -269,18 +294,16 @@ at the time of the request."
This hook is called in the buffer from which the prompt was sent
to the LLM, and after a text insertion."
:group 'gptel
:type 'hook)
(defcustom gptel-default-mode (if (fboundp 'markdown-mode)
'markdown-mode
'text-mode)
'markdown-mode
'text-mode)
"The default major mode for dedicated chat buffers.
If `markdown-mode' is available, it is used. Otherwise gptel
defaults to `text-mode'."
:group 'gptel
:type 'symbol)
:type 'function)
;; TODO: Handle `prog-mode' using the `comment-start' variable
(defcustom gptel-prompt-prefix-alist
@ -294,7 +317,6 @@ responses, and is removed from the query before it is sent.
This is an alist mapping major modes to the prefix strings. This
is only inserted in dedicated gptel buffers."
:group 'gptel
:type '(alist :key-type symbol :value-type string))
(defcustom gptel-response-prefix-alist
@ -308,7 +330,6 @@ responses.
This is an alist mapping major modes to the reply prefix strings. This
is only inserted in dedicated gptel buffers before the AI's response."
:group 'gptel
:type '(alist :key-type symbol :value-type string))
(defcustom gptel-use-header-line t
@ -316,8 +337,7 @@ is only inserted in dedicated gptel buffers before the AI's response."
When set to nil, use the mode line for (minimal) status
information and the echo area for messages."
:type 'boolean
:group 'gptel)
:type 'boolean)
(defcustom gptel-display-buffer-action '(pop-to-buffer)
"The action used to display gptel chat buffers.
@ -331,17 +351,12 @@ where FUNCTION is a function or a list of functions. Each such
function should accept two arguments: a buffer to display and an
alist of the same form as ALIST. See info node `(elisp)Choosing
Window' for details."
:group 'gptel
:type '(choice
(const :tag "Use display-buffer defaults" nil)
(const :tag "Display in selected window" (pop-to-buffer-same-window))
(cons :tag "Specify display-buffer action"
(choice function (repeat :tag "Functions" function))
alist)))
:type display-buffer--action-custom-type)
(defcustom gptel-crowdsourced-prompts-file
(let ((cache-dir (or (getenv "XDG_CACHE_HOME")
(getenv "XDG_DATA_HOME")
(let ((cache-dir (or (eval-when-compile
(require 'xdg)
(xdg-cache-home))
user-emacs-directory)))
(expand-file-name "gptel-crowdsourced-prompts.csv" cache-dir))
"File used to store crowdsourced system prompts.
@ -349,7 +364,6 @@ Window' for details."
These are prompts cached from an online source (see
`gptel--crowdsourced-prompts-url'), and can be set from the
transient menu interface provided by `gptel-menu'."
:group 'gptel
:type 'file)
;; Model and interaction parameters
@ -366,7 +380,6 @@ request to the LLM.
Each entry in this alist maps a symbol naming the directive to
the string that is sent. To set the directive for a chat session
interactively call `gptel-send' with a prefix argument."
:group 'gptel
:safe #'always
:type '(alist :key-type symbol :value-type string))
@ -384,8 +397,7 @@ responses.
To set the target token count for a chat session interactively
call `gptel-send' with a prefix argument."
:safe #'always
:group 'gptel
:type '(choice (integer :tag "Specify Token count")
:type '(choice (natnum :tag "Specify Token count")
(const :tag "Default" nil)))
(defcustom gptel-model "gpt-3.5-turbo"
@ -397,18 +409,21 @@ by the LLM provider's API.
The current options for ChatGPT are
- \"gpt-3.5-turbo\"
- \"gpt-3.5-turbo-16k\"
- \"gpt-4\" (experimental)
- \"gpt-4-1106-preview\" (experimental)
- \"gpt-4\"
- \"gpt-4-turbo\"
- \"gpt-4-turbo-preview\"
- \"gpt-4-32k\"
- \"gpt-4-1106-preview\"
To set the model for a chat session interactively call
`gptel-send' with a prefix argument."
:safe #'always
:group 'gptel
:type '(choice
(string :tag "Specify model name")
(const :tag "GPT 3.5 turbo" "gpt-3.5-turbo")
(const :tag "GPT 3.5 turbo 16k" "gpt-3.5-turbo-16k")
(const :tag "GPT 4" "gpt-4")
(const :tag "GPT 4 turbo" "gpt-4-turbo")
(const :tag "GPT 4 turbo (preview)" "gpt-4-turbo-preview")
(const :tag "GPT 4 32k" "gpt-4-32k")
(const :tag "GPT 4 1106 (preview)" "gpt-4-1106-preview")))
@ -422,7 +437,6 @@ of the response, with 2.0 being the most random.
To set the temperature for a chat session interactively call
`gptel-send' with a prefix argument."
:safe #'always
:group 'gptel
:type 'number)
(defvar gptel--known-backends nil
@ -439,7 +453,7 @@ with differing settings.")
"ChatGPT"
:key 'gptel-api-key
:stream t
:models '("gpt-3.5-turbo" "gpt-3.5-turbo-16k" "gpt-4"
:models '("gpt-3.5-turbo" "gpt-3.5-turbo-16k" "gpt-4" "gpt-4-turbo"
"gpt-4-turbo-preview" "gpt-4-32k" "gpt-4-1106-preview"
"gpt-4-0125-preview")))
@ -461,7 +475,6 @@ one of the available backend creation functions:
See their documentation for more information and the package
README for examples."
:safe #'always
:group 'gptel
:type `(choice
(const :tag "ChatGPT" ,gptel--openai)
(restricted-sexp :match-alternatives (gptel-backend-p 'nil)
@ -490,7 +503,6 @@ debug: Log request/response bodies, headers and all other
When non-nil, information is logged to `gptel--log-buffer-name',
which see."
:group 'gptel
:type '(choice
(const :tag "No logging" nil)
(const :tag "Limited" info)
@ -522,7 +534,7 @@ and \"apikey\" as USER."
;; FIXME Should we utf-8 encode the api-key here?
(defun gptel--get-api-key (&optional key)
"Get api key from KEY, or from `gptel-api-key'."
(when-let* ((key-sym (or key (gptel-backend-key gptel-backend))))
(when-let ((key-sym (or key (gptel-backend-key gptel-backend))))
(cl-typecase key-sym
(function (funcall key-sym))
(string key-sym)
@ -534,15 +546,18 @@ and \"apikey\" as USER."
(defsubst gptel--numberize (val)
"Ensure VAL is a number."
(if (stringp val) (string-to-number val) val))
(cond
((numberp val) val)
((stringp val) (string-to-number val))
((error "%S cannot be converted to a number" val))))
(defun gptel-auto-scroll ()
"Scroll window if LLM response continues below viewport.
Note: This will move the cursor."
(when-let* ((win (get-buffer-window (current-buffer) 'visible))
((not (pos-visible-in-window-p (point) win)))
(scroll-error-top-bottom t))
(when-let ((win (get-buffer-window (current-buffer) 'visible))
((not (pos-visible-in-window-p (point) win)))
(scroll-error-top-bottom t))
(condition-case nil
(with-selected-window win
(scroll-up-command))
@ -580,7 +595,7 @@ Note: This will move the cursor."
"Execute BODY at end of the current word or punctuation."
`(save-excursion
(skip-syntax-forward "w.")
,@body))
,(macroexp-progn body)))
(defun gptel-prompt-prefix-string ()
(or (alist-get major-mode gptel-prompt-prefix-alist) ""))
@ -652,9 +667,6 @@ Valid JSON unless NO-JSON is t."
;; Saving and restoring state
(declare-function gptel-org--restore-state "gptel-org")
(declare-function gptel-org--save-state "gptel-org")
(defun gptel--restore-state ()
"Restore gptel state when turning on `gptel-mode'."
(when (buffer-file-name)
@ -884,7 +896,7 @@ Model parameters can be let-bound around calls to this function."
((markerp position) position)
((integerp position)
(set-marker (make-marker) position buffer))))
(full-prompt
(full-prompt-draft
(cond
((null prompt) (gptel--create-prompt start-marker))
((stringp prompt)
@ -895,6 +907,17 @@ Model parameters can be let-bound around calls to this function."
(insert prompt)
(gptel--create-prompt))))
((consp prompt) prompt)))
(context-prompt
(when (get-buffer "*gptel-context*")
(list :role "user"
:content (format "%s\n\n%s"
gptel-context-prompt-preamble
(with-current-buffer "*gptel-context*"
(save-excursion
(buffer-substring-no-properties (point-min) (point-max))))))))
(full-prompt (if context-prompt
(append (list context-prompt) full-prompt-draft)
full-prompt-draft))
(request-data (gptel--request-data gptel-backend full-prompt))
(info (list :data request-data
:buffer buffer
@ -932,29 +955,27 @@ waiting for the response."
(gptel--update-status " Waiting..." 'warning)))
(declare-function json-pretty-print-buffer "json")
(defun gptel--inspect-query (&optional arg)
"Show the full LLM query to be sent in a new buffer.
(defun gptel--inspect-query (request-data &optional arg)
"Show REQUEST-DATA, the full LLM query to be sent, in a buffer.
This functions as a dry run of `gptel-send'. If prefix ARG is
This functions as a dry run of `gptel-send'. If ARG is
the symbol json, show the encoded JSON query instead of the lisp
structure gptel uses."
(let* ((request-data
(gptel-request nil :stream gptel-stream :dry-run t)))
(with-current-buffer (get-buffer-create "*gptel-query*")
(let ((standard-output (current-buffer))
(inhibit-read-only t))
(buffer-disable-undo)
(erase-buffer)
(if (eq arg 'json)
(progn (fundamental-mode)
(insert (gptel--json-encode request-data))
(json-pretty-print-buffer))
(lisp-data-mode)
(prin1 request-data)
(pp-buffer))
(goto-char (point-min))
(view-mode 1)
(display-buffer (current-buffer) gptel-display-buffer-action)))))
(with-current-buffer (get-buffer-create "*gptel-query*")
(let ((standard-output (current-buffer))
(inhibit-read-only t))
(buffer-disable-undo)
(erase-buffer)
(if (eq arg 'json)
(progn (fundamental-mode)
(insert (gptel--json-encode request-data))
(json-pretty-print-buffer))
(lisp-data-mode)
(prin1 request-data)
(pp-buffer))
(goto-char (point-min))
(view-mode 1)
(display-buffer (current-buffer) gptel-display-buffer-action))))
(defun gptel--insert-response (response info)
"Insert the LLM RESPONSE into the gptel buffer.
@ -1012,37 +1033,6 @@ See `gptel--url-get-response' for details."
(with-current-buffer gptel-buffer
(run-hook-with-args 'gptel-post-response-functions response-beg response-end)))))
(defun gptel-set-topic ()
"Set a topic and limit this conversation to the current heading.
This limits the context sent to the LLM to the text between the
current heading and the cursor position."
(interactive)
(pcase major-mode
('org-mode
(org-set-property
"GPTEL_TOPIC"
(completing-read "Set topic as: "
(org-property-values "GPTEL_TOPIC")
nil nil (downcase
(truncate-string-to-width
(substring-no-properties
(replace-regexp-in-string
"\\s-+" "-"
(org-get-heading)))
50)))))
('markdown-mode
(message
"Support for multiple topics per buffer is not implemented for `markdown-mode'."))))
(defun gptel--get-topic-start ()
"If a conversation topic is set, return it."
(pcase major-mode
('org-mode
(when (org-entry-get (point) "GPTEL_TOPIC" 'inherit)
(marker-position org-entry-property-inherited-from)))
('markdown-mode nil)))
(defun gptel--create-prompt (&optional prompt-end)
"Return a full conversation prompt from the contents of this buffer.
@ -1136,6 +1126,7 @@ the response is inserted into the current buffer after point."
(encode-coding-string
(gptel--json-encode (plist-get info :data))
'utf-8)))
;; why do these checks not occur inside of `gptel--log'?
(when gptel-log-level ;logging
(when (eq gptel-log-level 'debug)
(gptel--log (gptel--json-encode
@ -1191,7 +1182,7 @@ See `gptel-curl--get-response' for its contents.")
((or (memq url-http-response-status '(200 100))
(string-match-p "\\(?:1\\|2\\)00 OK" http-msg))
(list (string-trim (gptel--parse-response backend response
'(:buffer response-buffer)))
`(:buffer ,response-buffer)))
http-msg))
((plist-get response :error)
(let* ((error-data (plist-get response :error))
@ -1199,11 +1190,13 @@ See `gptel-curl--get-response' for its contents.")
(error-type (plist-get error-data :type))
(backend-name (gptel-backend-name backend)))
(if (stringp error-data)
(progn (message "%s error: (%s) %s" backend-name http-msg error-data)
(setq error-msg (string-trim error-data)))
(progn
(message "%s error: (%s) %s" backend-name http-msg error-data)
(setq error-msg (string-trim error-data)))
(when (stringp error-msg)
(message "%s error: (%s) %s" backend-name http-msg (string-trim error-msg)))
(when error-type (setq http-msg (concat "(" http-msg ") " (string-trim error-type)))))
(when error-type
(setq http-msg (concat "(" http-msg ") " (string-trim error-type)))))
(list nil (concat "(" http-msg ") " (or error-msg "")))))
((eq response 'json-read-error)
(list nil (concat "(" http-msg ") Malformed JSON in response.") "json-read-error"))
@ -1218,7 +1211,7 @@ See `gptel-curl--get-response' for its contents.")
"Check if MODEL is available in BACKEND, adjust accordingly.
If SHOOSH is true, don't issue a warning."
(let* ((available (gptel-backend-models backend)))
(let ((available (gptel-backend-models backend)))
(unless (member model available)
(let ((fallback (car available)))
(unless shoosh
@ -1281,152 +1274,6 @@ INTERACTIVEP is t when gptel is called interactively."
(substitute-command-keys "\\[gptel-send]")))
(current-buffer)))
(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)))
(delete-char -1))))
("*"
(cond
((save-match-data
(and (looking-back "\\(?:[[:space:]]\\|\s\\)\\(?:_\\|\\*\\)"
(max (- (point) 2) (point-min)))
(not (looking-at "[[:space:]]\\|\s"))))
;; Possible beginning of emphasis
(and
(save-excursion
(when (and (re-search-forward (regexp-quote (match-string 0))
(line-end-position) t)
(looking-at "[[:space]]\\|\s")
(not (looking-back "\\(?:[[:space]]\\|\s\\)\\(?:_\\|\\*\\)"
(max (- (point) 2) (point-min)))))
(delete-char -1) (insert "/") t))
(progn (delete-char -1) (insert "/"))))
((save-excursion
(ignore-errors (backward-char 2))
(looking-at "\\(?:$\\|\\`\\)\n\\*[[:space:]]"))
;; Bullet point, replace with hyphen
(delete-char -1) (insert "-"))))))
(buffer-string)))
(defun gptel--replace-source-marker (num-ticks &optional end)
"Replace markdown style backticks with Org equivalents.
NUM-TICKS is the number of backticks being replaced. If END is
true these are \"ending\" backticks.
This is intended for use in the markdown to org stream converter."
(let ((from (match-beginning 0)))
(delete-region from (point))
(if (and (= num-ticks 3)
(save-excursion (beginning-of-line)
(skip-chars-forward " \t")
(eq (point) from)))
(insert (if end "#+end_src" "#+begin_src "))
(insert "="))))
(defun gptel--stream-convert-markdown->org ()
"Return a Markdown to Org converter.
This function parses a stream of Markdown text to Org
continuously when it is called with successive chunks of the
text stream."
(letrec ((in-src-block nil) ;explicit nil to address BUG #183
(temp-buf (generate-new-buffer-name "*gptel-temp*"))
(start-pt (make-marker))
(ticks-total 0)
(cleanup-fn
(lambda (&rest _)
(when (buffer-live-p (get-buffer temp-buf))
(set-marker start-pt nil)
(kill-buffer temp-buf))
(remove-hook 'gptel-post-response-functions cleanup-fn))))
(add-hook 'gptel-post-response-functions cleanup-fn)
(lambda (str)
(let ((noop-p) (ticks 0))
(with-current-buffer (get-buffer-create temp-buf)
(save-excursion (goto-char (point-max)) (insert str))
(when (marker-position start-pt) (goto-char start-pt))
(when in-src-block (setq ticks ticks-total))
(save-excursion
(while (re-search-forward "`\\|\\*\\{1,2\\}\\|_" nil t)
(pcase (match-string 0)
("`"
;; Count number of consecutive backticks
(backward-char)
(while (and (char-after) (eq (char-after) ?`))
(forward-char)
(if in-src-block (cl-decf ticks) (cl-incf ticks)))
;; Set the verbatim state of the parser
(if (and (eobp)
;; Special case heuristic: If the response ends with
;; ^``` we don't wait for more input.
;; FIXME: This can have false positives.
(not (save-excursion (beginning-of-line)
(looking-at "^```$"))))
;; End of input => there could be more backticks coming,
;; so we wait for more input
(progn (setq noop-p t) (set-marker start-pt (match-beginning 0)))
;; We reached a character other than a backtick
(cond
;; Ticks balanced, end src block
((= ticks 0)
(progn (setq in-src-block nil)
(gptel--replace-source-marker ticks-total 'end)))
;; Positive number of ticks, start an src block
((and (> ticks 0) (not in-src-block))
(setq ticks-total ticks
in-src-block t)
(gptel--replace-source-marker ticks-total))
;; Negative number of ticks or in a src block already,
;; reset ticks
(t (setq ticks ticks-total)))))
;; Handle other chars: emphasis, bold and bullet items
((and "**" (guard (not in-src-block)))
(cond
((looking-at "\\*\\(?:[[:word:]]\\|\s\\)")
(delete-char 1))
((looking-back "\\(?:[[:word:]]\\|\s\\)\\*\\{2\\}"
(max (- (point) 3) (point-min)))
(delete-char -1))))
((and "*" (guard (not in-src-block)))
(save-match-data
(save-excursion
(ignore-errors (backward-char 2))
(cond
((or (looking-at
"[^[:space:][:punct:]\n]\\(?:_\\|\\*\\)\\(?:[[:space:][:punct:]]\\|$\\)")
(looking-at
"\\(?:[[:space:][:punct:]]\\)\\(?:_\\|\\*\\)\\([^[:space:][:punct:]]\\|$\\)"))
;; Emphasis, replace with slashes
(forward-char 2) (delete-char -1) (insert "/"))
((looking-at "\\(?:$\\|\\`\\)\n\\*[[:space:]]")
;; Bullet point, replace with hyphen
(forward-char 2) (delete-char -1) (insert "-")))))))))
(if noop-p
(buffer-substring (point) start-pt)
(prog1 (buffer-substring (point) (point-max))
(set-marker start-pt (point-max)))))))))
;; Response tweaking commands
@ -1505,7 +1352,7 @@ context for the ediff session."
"Mark gptel response at point, if any."
(interactive)
(unless (gptel--in-response-p) (user-error "No gptel response at point"))
(pcase-let* ((`(,beg . ,end) (gptel--get-bounds)))
(pcase-let ((`(,beg . ,end) (gptel--get-bounds)))
(goto-char beg) (push-mark) (goto-char end) (activate-mark)))
(defun gptel--previous-variant (&optional arg)
@ -1534,6 +1381,28 @@ context for the ediff session."
(goto-char (+ beg offset))
(pulse-momentary-highlight-region beg (+ beg (length alt-response)))))
;;;###autoload
(defun gptel-add-context ()
"Add selected region (or whole buffer) to *gptel-context*."
(interactive)
(let* ((context (if (use-region-p)
(buffer-substring-no-properties (region-beginning) (region-end))
(buffer-substring-no-properties (point-min) (point-max))))
(src-name (buffer-name))
(start (line-number-at-pos (region-beginning)))
(end (line-number-at-pos (region-end)))
(region-major-mode (symbol-name major-mode))
(loc (format "%s:%d-%d" src-name start end)))
(with-current-buffer (get-buffer-create "*gptel-context*")
(org-mode)
(goto-char (point-max))
(unless (bolp) (insert "\n"))
(insert (format "#+NAME: %s\n" loc))
(insert (format "#+BEGIN_SRC %s\n" region-major-mode))
(insert (format "%s\n" context))
(insert "#+END_SRC\n\n"))
(message "Context has been added to *gptel-context*.")))
(defun gptel--next-variant (&optional arg)
"Switch to next gptel-response at this point, if it exists."
(interactive "p")
@ -1541,3 +1410,7 @@ context for the ediff session."
(provide 'gptel)
;;; gptel.el ends here
;; Local Variables:
;; bug-reference-url-format: "https://github.com/karthink/gptel/issues/%s"
;; End:

View file

@ -19,17 +19,40 @@
;; (forward-char)
(condition-case-unless-debug err
(thread-first
(gptel--json-read :object-type 'plist)
;; (json-read)
(gptel--json-read)
(map-nested-elt '(:choices 0 :delta :content))
(push strs))
(error strs)
(:success strs)))
(setq strs (delq nil (nreverse strs))))))
;;; Basic tests for markdown to org conversion
(let ((string-sequences
'(("" "```" "cpp" "
(defun gptel-org--test-compare-org (md-list)
"Given a list of markdown-formatted strings MD-LIST, covert it to
Org markup incrementally and display both in buffers."
(let ((func (gptel--stream-convert-markdown->org)))
(with-current-buffer (get-buffer-create "gptel-org-test-md-input.md")
(erase-buffer)
(apply #'insert md-list)
(display-buffer (current-buffer)))
(with-current-buffer (get-buffer-create "gptel-org-test-org-output.org")
(erase-buffer)
(apply #'insert (mapcar func md-list))
(display-buffer (current-buffer)))))
(defmacro gptel-org--test-stream-conversion (md-list org-str)
`(ert-deftest
,(intern (concat "gptel-org--test-stream-conversion-"
(substring (sha1 (prin1-to-string md-list)) 0 10)))
()
(let ((func (gptel--stream-convert-markdown->org)))
(prog1
(should
(string= (apply #'concat (mapcar func ,md-list))
,org-str))
(setq gptel-post-response-functions nil)))))
(gptel-org--test-stream-conversion
'("" "```" "cpp" "
" "#include" " <" "cstdio" ">
" "int" " main" "()" " {
@ -37,7 +60,18 @@
" " " " return" " " "0" ";
" "}
" "```")
("" "Here" " is" " a" " simple" " C" "++" " program" " that" " uses" " the" " `" "printf" "`" " function" " to" " print" " the" " string" " containing" " " "5" " back" "ticks" ":
"#+begin_src cpp
#include <cstdio>
int main() {
printf(\"`````\n\");
return 0;
}
#+end_src")
(gptel-org--test-stream-conversion
;; markdown
'("" "Here" " is" " a" " simple" " C" "++" " program" " that" " uses" " the" " `" "printf" "`" " function" " to" " print" " the" " string" " containing" " " "5" " back" "ticks" ":
" "```" "cpp" "
" "#include" " <" "iostream" ">
@ -51,17 +85,8 @@
" "``" "`
" "In" " this" " code" " snippet" "," " `" "printf" "(\"" "``" "``" "`" "\n" "\");" "`" " is" " used" " to" " print" " the" " string" " \"" "``" "```" "\"" " followed" " by" " a" " newline" " character" ".")
("" "In" " the" " definition" " of" " the" " `" "struct" " json" "_parser" "`," " the" " line" " `" "L" "isp" "_Object" " *" "object" "_workspace" ";" "`" " declares" " a" " pointer" " named" " `" "object" "_workspace" "`" " of" " type" " `" "L" "isp" "_Object" "`.\n\n" "The" " aster" "isk" " (*)" " in" " `" "L" "isp" "_Object" " *" "object" "_workspace" ";" "`" " is" " the" " pointer" " ind" "irection" " operator" " in" " C" "." " It" " indicates" " that" " `" "object" "_workspace" "`" " is" " a" " pointer" " to" " an" " object" " of" " type" " `" "L" "isp" "_Object" "`." " This" " means" " that" " `" "object" "_workspace" "`" " will" " store" " the" " memory" " address" " (" "location" ")" " of" " a" " `" "L" "isp" "_Object" "`" " variable" " rather" " than" " storing" " the" " actual" " `" "L" "isp" "_Object" "`" " value" ".\n\n" "Therefore" "," " `" "object" "_workspace" "`" " will" " be" " used" " to" " point" " to" " or" " reference" " locations" " in" " memory" " where" " `" "L" "isp" "_Object" "`" " instances" " are" " stored" "." " This" " allows" " the" " `" "struct" " json" "_parser" "`" " to" " store" " and" " work" " with" " `" "L" "isp" "_Object" "`" " instances" " indirectly" " through" " pointers" ".")))
(converted-sequences
'("#+begin_src cpp
#include <cstdio>
int main() {
printf(\"`````\n\");
return 0;
}
#+end_src"
"Here is a simple C++ program that uses the =printf= function to print the string containing 5 backticks:
;; Org
"Here is a simple C++ program that uses the =printf= function to print the string containing 5 backticks:
#+begin_src cpp
#include <iostream>
@ -74,27 +99,35 @@ int main() {
}
#+end_src
In this code snippet, =printf(\"`````\n\");= is used to print the string \"=\" followed by a newline character."
"In the definition of the =struct json_parser=, the line =Lisp_Object *object_workspace;= declares a pointer named =object_workspace= of type =Lisp_Object=.
In this code snippet, =printf(\"`````\n\");= is used to print the string \"=\" followed by a newline character.")
(gptel-org--test-stream-conversion
;; markdown
'("" "In" " the" " definition" " of" " the" " `" "struct" " json" "_parser" "`," " the" " line" " `" "L" "isp" "_Object" " *" "object" "_workspace" ";" "`" " declares" " a" " pointer" " named" " `" "object" "_workspace" "`" " of" " type" " `" "L" "isp" "_Object" "`.\n\n" "The" " aster" "isk" " (*)" " in" " `" "L" "isp" "_Object" " *" "object" "_workspace" ";" "`" " is" " the" " pointer" " ind" "irection" " operator" " in" " C" "." " It" " indicates" " that" " `" "object" "_workspace" "`" " is" " a" " pointer" " to" " an" " object" " of" " type" " `" "L" "isp" "_Object" "`." " This" " means" " that" " `" "object" "_workspace" "`" " will" " store" " the" " memory" " address" " (" "location" ")" " of" " a" " `" "L" "isp" "_Object" "`" " variable" " rather" " than" " storing" " the" " actual" " `" "L" "isp" "_Object" "`" " value" ".\n\n" "Therefore" "," " `" "object" "_workspace" "`" " will" " be" " used" " to" " point" " to" " or" " reference" " locations" " in" " memory" " where" " `" "L" "isp" "_Object" "`" " instances" " are" " stored" "." " This" " allows" " the" " `" "struct" " json" "_parser" "`" " to" " store" " and" " work" " with" " `" "L" "isp" "_Object" "`" " instances" " indirectly" " through" " pointers" ".")
;; org
"In the definition of the =struct json_parser=, the line =Lisp_Object *object_workspace;= declares a pointer named =object_workspace= of type =Lisp_Object=.
The asterisk (*) in =Lisp_Object *object_workspace;= is the pointer indirection operator in C. It indicates that =object_workspace= is a pointer to an object of type =Lisp_Object=. This means that =object_workspace= will store the memory address (location) of a =Lisp_Object= variable rather than storing the actual =Lisp_Object= value.
Therefore, =object_workspace= will be used to point to or reference locations in memory where =Lisp_Object= instances are stored. This allows the =struct json_parser= to store and work with =Lisp_Object= instances indirectly through pointers.")))
(ert-deftest test--gptel--convert-markdown->org ()
(cl-loop
for input in string-sequences
for output in converted-sequences
do
(should (string= (gptel--convert-markdown->org (apply #'concat input))
output))))
(ert-deftest test--gptel--stream-convert-markdown->org ()
(cl-loop
for input in string-sequences
for output in converted-sequences
for func = (gptel--stream-convert-markdown->org)
do
(should
(string= (apply #'concat (mapcar func input))
output)))))
Therefore, =object_workspace= will be used to point to or reference locations in memory where =Lisp_Object= instances are stored. This allows the =struct json_parser= to store and work with =Lisp_Object= instances indirectly through pointers.")
(gptel-org--test-stream-conversion
;; markdown
'("#" "# Advantages of"
" Org-Mode: A Detailed Overview\n\nOrg-mode is a powerful and versatile Emacs extension that offers a wide range of features"
" for organizing information, planning projects, and writing documents. Here's a detailed overview of its advantages:\n\n**Note Taking and Idea Management:**\n\n"
"* **Unified platform:** Org-mode provides a single platform for capturing notes, ideas, and tasks, eliminating the need for multiple tools and ensuring everything is in one place.\n* **Flexibility:** Notes can be structured hierarchically"
" using headlines and subheadings, allowing for easy organization and navigation.\n* **Linking and tags:** Link notes together for easy reference and connect them with tags for categorized browsing.\n* **Metadata:** Capture additional information like author, deadline, and"
" priority for better organization and search.\n\n**Project Management and Planning:**\n\n* **Task management:** Create to-do lists with deadlines, priority levels, and tags for efficient task management.\n* **Gantt charts and time estimates:** Visualize project timelines and estimate time commitments for better planning and organization.\n* **Calendar integration:** Link tasks to your calendar for better integration and time management.\n* **"
"Progress tracking:** Track the progress of tasks and projects with checkboxes and progress bars.\n\n**Writing and Document Creation:**\n\n* **Rich text formatting:** Org-mode supports a variety of text formatting options, including bold, italics, headings, and lists, for creating professional-looking documents.\n* **Exporting to various formats:** Easily export your notes and documents to various formats like PDF, HTML, LaTeX, and"
" plain text.\n* **Beamer presentations:** Create Beamer presentations directly within Org-mode for academic or professional presentations.\n* **Source code blocks:** Include and highlight code blocks for easy reference and documentation.\n\n**Additional Features:**\n\n* **Org-Capture:** Quickly capture ideas, notes, and tasks from anywhere using keyboard shortcuts.\n* **Org-Agenda:** View and manage your tasks and appointments in a calendar-like format.\n* **Emacs Lisp scripting:** Add custom"
" functionality and automate tasks with Emacs Lisp scripting.\n* *"
"*Active development and community support:** Benefit from the active development and supportive community of Org-mode users.\n\n**Overall Advantages:**\n\n* **Increased productivity:** Organize your thoughts, tasks, and projects efficiently, leading to increased productivity.\n*"
" **Improved focus:** Eliminate distractions and stay focused on the task at hand.\n* **Enhanced creativity:** Capture ideas quickly and easily, fostering creativity and innovation.\n* **Knowledge management:** Build a comprehensive knowledge base"
" of notes, ideas, and projects for future reference.\n*"
" **Personalized workflow:** Tailor Org-mode to your specific needs and preferences for a truly personalized workflow.\n\n**Limitations:**\n\n* **Learning curve:** While powerful, Org-mode has a steeper learning curve compared to simpler note-taking apps.\n* **Emacs dependency:** Org-mode requires Emacs, which may not be suitable for all users.\n* **Limited mobile support:** Mobile support for Org-mode is available but not as feature-rich as"
" the desktop version.\n\n**Who should use Org-mode?**\n\n"
"* Students\n* Researchers\n* Writers\n* Project managers\n* Anyone who wants to improve their personal organization and productivity\n\n**Conclusion:**\n\nOrg-mode is a powerful and versatile tool that can significantly increase your productivity and organization. Its flexibility, rich features, and active community make it an excellent choice for managing notes, projects, and documents. If you're looking for a way to streamline your workflow and unleash your creativity, Org-mode is definitely worth exploring.")
;; org
"** Advantages of Org-Mode: A Detailed Overview\n\nOrg-mode is a powerful and versatile Emacs extension that offers a wide range of features for organizing information, planning projects, and writing documents. Here's a detailed overview of its advantages:\n\n*Note Taking and Idea Management:*\n\n- *Unified platform:* Org-mode provides a single platform for capturing notes, ideas, and tasks, eliminating the need for multiple tools and ensuring everything is in one place.\n- *Flexibility:* Notes can be structured hierarchically using headlines and subheadings, allowing for easy organization and navigation.\n- *Linking and tags:* Link notes together for easy reference and connect them with tags for categorized browsing.\n- *Metadata:* Capture additional information like author, deadline, and priority for better organization and search.\n\n*Project Management and Planning:*\n\n- *Task management:* Create to-do lists with deadlines, priority levels, and tags for efficient task management.\n- *Gantt charts and time estimates:* Visualize project timelines and estimate time commitments for better planning and organization.\n- *Calendar integration:* Link tasks to your calendar for better integration and time management.\n- *Progress tracking:* Track the progress of tasks and projects with checkboxes and progress bars.\n\n*Writing and Document Creation:*\n\n- *Rich text formatting:* Org-mode supports a variety of text formatting options, including bold, italics, headings, and lists, for creating professional-looking documents.\n- *Exporting to various formats:* Easily export your notes and documents to various formats like PDF, HTML, LaTeX, and plain text.\n- *Beamer presentations:* Create Beamer presentations directly within Org-mode for academic or professional presentations.\n- *Source code blocks:* Include and highlight code blocks for easy reference and documentation.\n\n*Additional Features:*\n\n- *Org-Capture:* Quickly capture ideas, notes, and tasks from anywhere using keyboard shortcuts.\n- *Org-Agenda:* View and manage your tasks and appointments in a calendar-like format.\n- *Emacs Lisp scripting:* Add custom functionality and automate tasks with Emacs Lisp scripting.\n- *Active development and community support:* Benefit from the active development and supportive community of Org-mode users.\n\n*Overall Advantages:*\n\n- *Increased productivity:* Organize your thoughts, tasks, and projects efficiently, leading to increased productivity.\n- *Improved focus:* Eliminate distractions and stay focused on the task at hand.\n- *Enhanced creativity:* Capture ideas quickly and easily, fostering creativity and innovation.\n- *Knowledge management:* Build a comprehensive knowledge base of notes, ideas, and projects for future reference.\n- *Personalized workflow:* Tailor Org-mode to your specific needs and preferences for a truly personalized workflow.\n\n*Limitations:*\n\n- *Learning curve:* While powerful, Org-mode has a steeper learning curve compared to simpler note-taking apps.\n- *Emacs dependency:* Org-mode requires Emacs, which may not be suitable for all users.\n- *Limited mobile support:* Mobile support for Org-mode is available but not as feature-rich as the desktop version.\n\n*Who should use Org-mode?*\n\n- Students\n- Researchers\n- Writers\n- Project managers\n- Anyone who wants to improve their personal organization and productivity\n\n*Conclusion:*\n\nOrg-mode is a powerful and versatile tool that can significantly increase your productivity and organization. Its flexibility, rich features, and active community make it an excellent choice for managing notes, projects, and documents. If you're looking for a way to streamline your workflow and unleash your creativity, Org-mode is definitely worth exploring.")