Compare commits

...

78 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
Karthik Chikmagalur
8dbcbbb908 gptel-org: Move session save/restore code for Org
* gptel.el (gptel--restore-backend, gptel--save-state,
gptel--restore-state): Move the Org-specific code for saving and
restoring state to gptel-org.

* gptel-org.el (gptel-org--entry-properties,
gptel-org--save-state, gptel-org--restore-state): Org-specific
code for saving and restoring state using Org properties, moved
from gptel-org.
2024-04-04 00:25:09 -07:00
Karthik Chikmagalur
2982ede17d gptel-org: Add gptel-org
* gptel.el (gptel--create-prompt): Split the Org mode logic in
`gptel--create-prompt` into gptel-org.el.

* gptel-org.el (gptel-org-branching-context,
gptel-org--create-prompt): Handle prompt creation for Org buffers
in `gptel-org--create-prompt`.  The option
`gptel-org-branching-context` limits the context in Org buffers to
the direct ancestors of the active heading.  This is useful for
conversations with branches.  gptel-org.el is intended to
eventually contain all the Org mode specific gptel code, and will
not be loaded unless gptel is called in an Org buffer.
2024-04-04 00:25:08 -07:00
Karthik Chikmagalur
5d74ec4de0 gptel: Set system message correctly in gptel-request
* gptel.el (gptel-request): let-bind `gptel--system-message`
correctly in `gptel-request`.  The Anthropic API requires the
system message to be attached to the query differently from the
others, causing the let-bindings in `gptel-request` to not work as
expected. (#276)
2024-04-03 09:34:07 -07:00
Karthik Chikmagalur
53a905dafc gptel: Show chosen system message in header-line
* gptel.el (gptel): Show chosen system message in
header-line (#274).  Remove the context indicator `[Send: ...]`,
as this needs to be reworked for the upcoming context-inclusion
features anyway.

* gptel-transient.el (gptel-system-prompt): Autoload
`gptel-system-prompt`, this is required for the gptel header-line.
2024-04-02 19:33:39 -07:00
Karthik Chikmagalur
53ee34653e gptel-openai: Typo in gptel--json-read
* gptel-openai.el (gptel--json-read): Fix typo in
`gptel--json-read` that caused macroexpansion intended to use
json.el fail.  This error was reported by @vkz.  Fix #260.
2024-04-02 11:50:30 -07:00
Karthik Chikmagalur
f24ec164cd gptel: Adjust doc-string
* gptel.el (gptel-post-response-functions): Adjust docstring for
this hook to mention that the hook runs in the response buffer,
not the prompt buffer. (#269)
2024-03-30 09:42:30 -07:00
Karthik Chikmagalur
81bb467104 gptel: Set window when running post-response hook
* gptel.el (gptel--insert-response): Moving point in a buffer does
not move window-point in a window displaying that buffer if the
window is not selected (#269).  So select the gptel response
window explicitly (if possible) when running
`gptel-post-response-functions`.

NOTE: Instead of selecting the window before running the hook, We
could run `set-window-point` in the window after running it.
Since the hook can have any kind of behavior (smooth-scrolling for
instance) we want it to play out interactively.

* gptel-curl.el (gptel--curl-stream-cleanup): Ditto.
2024-03-29 15:47:32 -07:00
Karthik Chikmagalur
26326c302e gptel-anthropic: Parsing fix
gptel-anthropic.el (gptel-curl--parse-stream): When parsing
responses from Anthropic, wait for more input when the
corresponding data chunk for the event hasn't arrived yet. (#261)
2024-03-29 14:08:32 -07:00
Karthik Chikmagalur
9a5a4a60d5 README: Mention wiki entry on saving transient flags
* README.org (FAQ): Mention wiki entry on saving transient flags.
2024-03-21 11:30:30 -07:00
Karthik Chikmagalur
12e00cbd09 gptel-transient: No pre-fill when reading from minibuffer
* gptel-transient.el (gptel--suffix-send): Don't pre-fill the
minibuffer prompt with the current line when reading the query
from the minibuffer.  (If the region is active, it is used as the
initial contents -- this behavior is unchanged.)
2024-03-21 10:40:31 -07:00
Karthik Chikmagalur
34a52aa047 gptel-anthropic: Remove debug code
* gptel-anthropic.el (gptel--parse-response): Remove left over
debugging code.
2024-03-21 10:23:16 -07:00
Karthik Chikmagalur
9eea4be5ed gptel-transient: Fix gptel-menu definition bug (#265)
gptel-transient.el (gptel-menu): Fix error in definition syntax.
Fix provided by @wrn.
2024-03-20 19:46:41 -07:00
Fredrik Bergroth
9bc54bed9c
gptel-transient: Remove "-ts" suffix from major mode (#259)
gptel-transient (gptel--rewrite-message): Remove "-ts" suffix from
major mode.
2024-03-19 18:06:48 -07:00
Karthik Chikmagalur
5d069cfca8 gptel-anthropic: Simplify stream parser
* gptel-anthropic.el (gptel-curl--parse-stream): Remove extraneous
statements and simplify the stream parser.  Addresses #261, but
the main problem is still elusive.
2024-03-19 18:01:55 -07:00
Karthik Chikmagalur
22f7043c32 gptel: Fix gptel-end-of-response
* gptel.el (gptel-end-of-response): Handle non-interactive case
where one of the function arguments can be nil.  Fix #262.
2024-03-19 17:39:13 -07:00
Karthik Chikmagalur
5dcbf40066 gptel: Make model parameters global
* gptel.el (gptel-backend, gptel-model, gptel-temperature,
gptel-max-tokens, gptel--num-messages-to-send,
gptel--system-message): Make all model/request paramters global
variables, i.e. not buffer-local by default.  This is following
the discussion in #249.

* gptel-transient.el (gptel-menu, gptel-system-prompt--setup,
gptel-system-prompt, gptel--suffix-system-message,
gptel--infix-provider, gptel--infix-temperature, gptel--switches,
gptel--set-buffer-locally, gptel--set-with-scope): and associated
transient methods: add a toggle `gptel--set-buffer-locally` to
allow model parameters to be set buffer-locally.  The function
`gptel--set-with-scope` can be used to reset a variable or set it
buffer-locally.

Reorder gptel-transient so all the custom classes, methods and
utility functions are at the top.

* README.org (all backend sections): Replace `setq-default` with
setq in the recommended configuration.
2024-03-16 20:59:48 -07:00
Karthik Chikmagalur
e3b3591d73 README: Add support for Groq (#257)
* README.org: Mention details for configuring Groq.
2024-03-16 20:27:54 -07:00
Karthik Chikmagalur
94b13e78ec gptel-transient: enable additional directive by default
* gptel-transient.el (gptel-menu, gptel--infix-add-directive,
gptel--suffix-system-message):  Enable the additional directive
option in `gptel-menu` by default, and change the wording of
documentation strings for transient options.
2024-03-16 20:17:52 -07:00
Karthik Chikmagalur
b31c9be5e0 gptel-ollama: Adjust Ollama stream parser for libjansson
* gptel-ollama.el (gptel-curl--parse-stream): libjansson and
json.el behave differently w.r.t moving point when there is a
parsing error.  Fix by explicitly handling point when there is an
error. (#255)
2024-03-16 20:17:52 -07:00
Karthik Chikmagalur
73a0cc25ba gptel-transient: Simplify model selection
* gptel-transient.el (gptel--infix-provider, gptel--infix-model):
Simplify model and backend selection into a single step.  Remove
unused and obsolete option `gptel--infix-provider`.
2024-03-16 20:17:52 -07:00
Karthik Chikmagalur
6d3e4a99f5 gptel-transient: Rename additional-directive functions
* gptel-transient.el (gptel--get-directive,
gptel--infix-add-directive, gptel-menu): Rename the
directive-related functions to be shorter/more consistent with the
rest of the code.

gptel--additional-directive     -> gptel--infix-add-directive
gptel--additional-directive-get -> gptel--get-directive
2024-03-16 20:17:52 -07:00
Karthik Chikmagalur
161c77ad7f gptel-transient: Adjust several menu options
* gptel-transient.el (gptel-menu, gptel-system-prompt--setup,
gptel-system-prompt, gptel--infix-temperature,
transient-format-value, gptel--additional-directive,
gptel--suffix-system-message): Tweak to the "additional directive"
overlay display.  `gptel-menu` changes based on feedback
from #249 (thanks to @jwr):

- Keys to set the system message are remapped from "h" to
"s" (mnemonic)
- `gptel--infix-temperature` is now hidden by default and requires
enabling `gptel-expert-commands`
- Keys to prompt from minibuf and respond in echo area are changed
to "m" and "e" respectively.
2024-03-16 20:17:51 -07:00
Karthik Chikmagalur
376fb4b423 gptel-transient: Additional directives option (#249)
* gptel.el (gptel-end-of-response, gptel-beginning-of-response,
gptel-expert-commands): Add `gptel-expert-commands` to selectively
enable experimental options in `gptel-menu`.  This should keep the
interface from overwhelming new users.  Add a command to move to
the beginning of a response.

* gptel-transient.el (gptel-menu, gptel-system-prompt,
gptel--instructions-make-overlay, gptel-option-overlaid,
transient-format-value, gptel--additional-directive,
gptel--additional-directive-get): Add a transient option to
include a (short) additional instruction/directive along with the
system message.  This makes it convenient to have an extensive
system message and specify additional, per-response tasks (such as
refactoring) on top.  Ensure that the dry run options handle this
correctly.  This option is made available when
`gptel-expert-commands` is turned on.

NOTE: WIP design.  The nomenclature for `gptel-expert-commands`
and "additional directive" is subject to change.
2024-03-16 20:17:35 -07:00
Karthik Chikmagalur
7e6b106516 gptel-transient: Change menu display
* gptel-transient.el (gptel-menu, gptel-lisp-variable,
gptel--infix-num-messages-to-send, gptel--infix-max-tokens,
gptel--infix-provider): Improve the wording and the display of the
default settings.  Define a new class `gptel-lisp-variable` to
faciliate this.
2024-03-14 20:28:21 -07:00
Karthik Chikmagalur
f529457bbe gptel: Use visual-line-mode when ediff-ing
* gptel.el (gptel--ediff): Use `visual-line-mode` when ediff-ing.
2024-03-14 20:28:21 -07:00
Karthik Chikmagalur
dade9ec8e1 gptel: Add introspection commands
* gptel.el (gptel--inspect-query): Add a function to inspect the
query that will be sent.  This is useful to ensure that (only and
all of) what you expect to be sent is being sent.

* gptel-transient.el (gptel-menu): Allow the query in progress to
be inspected from `gptel-menu` as an elisp or JSON object.
2024-03-14 20:28:21 -07:00
Karthik Chikmagalur
260be9d8d4 gptel: Consolidate HTTP request process
* gptel.el (gptel-request, gptel-send, gptel--url-get-response):
Consolidate the HTTP query construction into `gptel-request`,
and use it as the single point for sending gptel queries.  This
simplifies the code, makes it easier to debug and (later) advise
or otherwise modify.  To this end, `gptel-send` reuses
`gptel-request` and no longer does its own thing.  Adjust other
HTTP request-related functions accordingly.

* gptel-curl.el (gptel-curl--get-args, gptel-curl--get-response):
Receive the full request data instead of constructing it partly in
`gptel-curl--get-args`.
2024-03-14 20:28:21 -07:00
Karthik Chikmagalur
e5f54d1d09 gptel-anthropic: Modify order of request items
* gptel-anthropic.el (gptel--request-data): Move the messages to
the end of the request data object for easier inspection.
2024-03-14 20:28:21 -07:00
Karthik Chikmagalur
12340eda46 gptel-transient: Truncate system prompt when messaging
* gptel-transient.el (gptel-system-prompt--setup): Truncate the
chosen system prompt when displaying it as a message. (#249)
2024-03-14 20:28:21 -07:00
Karthik Chikmagalur
07992f79cc gptel-anthropic: Support for the Claude haiku model
* gptel-anthropic.el (gptel-make-anthropic): Add Claude-3-haiku to
the list of supported Anthropic models.
2024-03-14 20:28:21 -07:00
Karthik Chikmagalur
e18ceb1f84 gptel: Improve logging
* gptel.el (gptel--url-get-response): Log request headers as JSON objects.

* gptel-curl.el (gptel-curl--get-args, gptel-curl--get-response):
Log request headers as JSON objects, Curl command as a shell
command you can copy and paste into the terminal.
2024-03-14 20:28:21 -07:00
Karthik Chikmagalur
f58ad9435c gptel: Use libjansson support if available
Using the libjansson JSON parser gives us a modest boost in speed.
It's not as significant a speedup as it is for LSP clients since
our jSON payloads are smaller and less frequent -- but we might as
well use it.

* gptel.el (gptel--json-read, gptel--json-encode,
gptel--url-get-response, gptel--parse-response): Define macros to
use the libjansson-supported `json-parse-buffer` and
`json-serialize`.  Replace use of `json-encode` and `json-read`
appropriately.

* gptel-openai.el: (gptel-curl--parse-stream) : Use
`gptel--json-read` instead of `json-read`.

* gptel-ollama.el (gptel-curl--parse-stream): Use
`gptel--json-read` instead of `json-read`.

* gptel-gemini.el (gptel-curl--parse-stream): Use
`gptel--json-read` instead of `json-read`.

* gptel-curl.el (gptel-curl--get-args, gptel-curl--get-response,
gptel-curl--log-response, gptel-curl--stream-cleanup,
gptel-curl--parse-response): Use `gptel--json-read` and
`gptel--json-encode` in place of the json.el versions.

* gptel-anthropic.el (gptel-curl--parse-stream): Use
`gptel--json-read` instead of `json-read`.

* test/gptel-org-test.el: Use `gptel--json-read`.
2024-03-14 20:28:21 -07:00
Karthik Chikmagalur
fbb0ee29c4 gptel-org-test: Add mores tests for org conversion
* test/gptel-org-test.el: Rename file from test-gptel-org.el, and
add more tests.
2024-03-13 00:52:56 -07:00
Karthik Chikmagalur
9925dc91b4 gptel: Improve markdown converter
* gptel.el (gptel--stream-convert-markdown->org,
gptel--replace-source-marker): Handle backquote conversion much
better during stream-based conversion.  One-shot conversions still
require fixing.
2024-03-13 00:48:22 -07:00
Karthik Chikmagalur
d502ad8ecb test-gptel-org: Add markdown conversion tests
* test/test-gptel-org.el (test--gptel-convert-markdown->org,
test--gptel--stream-convert-markdown->org): Add tests

Also remove out-of-date demo image.
2024-03-11 00:49:05 -07:00
Cash Prokop-Weaver
3935a6dcf8
♻️: Untangle Gemini model and endpoint #212 (#213)
gptel-gemini.el (gptel-make-gemini): Decouple the Gemini model
from the API endpoint.  This is to support additional model
options in the future.
2024-03-10 21:02:42 -07:00
Karthik Chikmagalur
3d6147830e gptel: Fix model/backend mismatch (#242)
* gptel.el (gptel--sanitize-model, gptel): Check for mismatches
between the default values of gptel-backend and gptel-model when
starting a new gptel chat.  Previously the default value of
gptel-backend was compared against the buffer-local value of
gptel-model.
2024-03-09 14:26:45 -08:00
Karthik Chikmagalur
5e9e36d854 gptel: rear-nonsticky text-property by default
* gptel.el (gptel--insert-response, gptel--restore-state): Don't
set the rear-nonsticky property on the gptel response text.
Instead, the gptel text-property is globally declared to be
nonsticky via `text-property-default-nonsticky`.

This change is since markdown-mode makes rear-nonsticky a
font-lock-managed property and we can't set it manually.  Setting
this property explicitly also makes all text properties in the
range nonsticky, which can have unintended side effects.

* gptel-curl.el (gptel-curl--stream-insert-response): Ditto.
2024-03-08 19:09:48 -08:00
Karthik Chikmagalur
b634f05fe5 gptel: Tweak markdown to org conversion
gptel.el (gptel--convert-markdown->org,
gptel--stream-convert-markdown->org): Handle the case of Markdown
bullet lists that begin with a "*", which the Gemini LLMs produce
often.  Address #238.
2024-03-07 18:24:31 -08:00
Karthik Chikmagalur
2487ada4d6 gptel-anthropic: Handle missing response chunks (trial)
gptel-anthropic.el (gptel-curl--parse-stream): Reset point
explicitly when parsing streaming responses returned by the
Anthropic API.  Try to address #233.
2024-03-07 17:03:21 -08:00
Karthik Chikmagalur
199595b0c8 gptel: Handle status HTTP 100
gptel.el (gptel--url-parse-response): Handle HTTP 100 followed by
200.  Note: this fix is brittle, it will break if 100 is followed
by an error code.

gptel-curl.el (gptel-curl--stream-filter,
gptel-curl--parse-stream): Ditto.  Address #194.
2024-03-07 10:46:30 -08:00
Karthik Chikmagalur
a32f4effe5 gptel-curl: Handle empty responses correctly
gptel-curl.el (gptel-curl--stream-cleanup): The LLM response can
be empty with HTTP status 200, for example when the API responds
with an error description instead.  Handle this case gracefully.
When `gptel-mode` is enabled, also inform the user that the
response was empty.  Fix #234.

gptel.el (gptel-post-response-functions): Documentation.  Explain
the arguments passed to each hook function in this hook when the
response fails or is empty.
2024-03-07 10:23:59 -08:00
Karthik Chikmagalur
0d6264f268 gptel-curl: Adjust response beginning position
gptel-curl.el (gptel-curl--stream-cleanup,
gptel-curl--stream-insert-response): Don't consider
`gptel-response-prefix-string` part of the response for the
purpose of running `gptel-post-response-functions`.
2024-03-05 22:51:14 -08:00
Karthik Chikmagalur
eb088f2f21 gptel-anthropic: support Anthropic AI's Claude 3 (#229)
* gptel.el: Mention Anthropic in the package description.

* gptel-anthropic.el (gptel-anthropic, gptel-make-anthropic,
gptel--parse-response, gptel--request-data, gptel--parse-buffer,
gptel-curl--parse-stream): Add support for Anthropic AI's Claude 3
models.

* README.org: Add instructions for using Anthropic AI's Claude 3
models.
2024-03-04 11:28:57 -08:00
Karthik Chikmagalur
87c190076e README: Clarify example configuration code
README.org (all custom backends): Clarify how to set a backend as
the gptel default.  This has been confusing users.
2024-02-28 18:28:36 -08:00
Marten Lienen
149261ee79
gptel-transient: Avoid clashes with the custom directive key (#219)
gptel-transient (gptel--system-prompt-setup): When assigning keys
to directives, avoid clashes with the system-message edit key
(`h`), and use an arbitrary unused key if no mnemonic key
assignments are possible.
2024-02-27 22:36:06 -08:00
Karthik Chikmagalur
8ba07d042c gptel: Bump version
* gptel.el: Bump version
2024-02-21 00:11:03 -08:00
r0man
43f625ecb9
gptel-openai: curl-args slot in gptel-backend (#221)
gptel-openai.el (gptel-backend, gptel-make-openai,
gptel-make-azure): Add a curl-args slot to the backend struct for
additional Curl arguments.

Usage example: This can be used to set the `--cert` and `--key`
options in a custom backend that uses mutal TLS to communicate
with an OpenAI proxy/gateway.

gptel-curl.el (gptel-curl--get-args): Add backend-specific
curl-args when creating HTTP requests.

gptel-gemini.el (gptel-make-gemini): Add a curl-args slot to the
constructor.
gptel-kagi.el (gptel-make-kagi): Ditto.
gptel-ollama.el (gptel-make-ollama): Ditto.
2024-02-20 15:21:46 -08:00
Karthik Chikmagalur
226f8f0d90 gptel: Add customizable display-action (#216)
* gptel.el (gptel-display-buffer-action, gptel): Add
`gptel-display-buffer-action` to customize the display of the
gptel buffer.

* README: Mention new option.
2024-02-10 12:04:03 -08:00
Karthik Chikmagalur
5465271541 gptel: Add gpt-4-0125-preview to model list (Fix #215)
* gptel.el (gptel--openai): Add "gpt-4-0125-preview" to the list
of ChatGPT models.  Also address byte-compiler warnings.
2024-02-09 12:15:03 -08:00
Karthik Chikmagalur
ef8b9093d2 gptel-gemini: Use permissive API safety settings
* gptel-gemini.el (gptel-make-gemini, gptel--request-data): The
Gemini API misclassifies harmless questions (like "What's 2+2",
see #208) as harmful.  Use the most permissive safety settings the
API offers.

Also respect the value of `:stream` used when defining Gemini
backends.
2024-02-07 19:03:45 -08:00
Karthik Chikmagalur
e2eccd8b08 gptel: Remove references to gptel--debug (fix #205)
* gptel.el (gptel--url-parse-response): Remove reference to
`gptel--debug`.

* gptel-curl.el (gptel-curl--sentinel): Remove reference to
`gptel--debug`.
2024-02-04 22:23:45 -08:00
Karthik Chikmagalur
bf994c0765 gptel: Add response regeneration, history and ediff
* gptel.el (gptel--attach-response-history, gptel--ediff,
gptel--next-variant, gptel--previous-variant,
gptel--mark-response):

Add `gptel--attach-response-history` -- this can be used to add
text properties to the next gptel response in the buffer.  This
is (currently) useful for tracking changes when the response
overwrites existing text.

The next three commands -- `gptel--ediff`,
`gptel--previous-variant`, `gptel--next-variant` -- provide
facilities for manipulating a gptel response at point when there
is history.  `gptel--mark-response` marks the response at point.
These are considered internal functions for now and can be
accessed from the transient menu, where they work together with
`gptel--regenerate`.

The input arguments to these commands are expected to change to
support copilot-style functionality in the near future.

* gptel-transient.el (gptel-menu, gptel--suffix-send,
gptel--regenerate):

Change the transient menu layout to be more compact (with a newly
added column.)  When overwriting the prompt with a response, save
the prompt to the gptel response's history.  Add
`gptel--regenerate` to regenerate a response.  This is accessible
from the transient menu when the point is inside response text.
2024-02-04 22:17:14 -08:00
Karthik Chikmagalur
49cfc78378 gptel: Add page boundaries, restructure files
* gptel.el: Add page boundaries and reorder file.

* gptel-transient.el: Add page boundaries.
2024-02-04 21:06:14 -08:00
Dave Berton
d8c604b53b
README: Update with instructions for perplexity.ai (#204)
README.org: perplexity.ai provides an OpenAI-style API.  Also remove 
some extra whitespace.
2024-02-03 16:36:24 -08:00
13 changed files with 2174 additions and 719 deletions

1
.elpaignore Normal file
View file

@ -0,0 +1 @@
test

View file

@ -1,22 +1,26 @@
#+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 |
|-----------------+----------+---------------------------|
| ChatGPT | ✓ | [[https://platform.openai.com/account/api-keys][API key]] |
| Azure | ✓ | Deployment and API key |
| Ollama | ✓ | [[https://ollama.ai/][Ollama running locally]] |
| GPT4All | ✓ | [[https://gpt4all.io/index.html][GPT4All running locally]] |
| Gemini | ✓ | [[https://makersuite.google.com/app/apikey][API key]] |
| Llama.cpp | ✓ | [[https://github.com/ggerganov/llama.cpp/tree/master/examples/server#quick-start][Llama.cpp running locally]] |
| Llamafile | ✓ | [[https://github.com/Mozilla-Ocho/llamafile#quickstart][Local Llamafile server]] |
| Kagi FastGPT | ✓ | [[https://kagi.com/settings?p=api][API key]] |
| Kagi Summarizer | ✓ | [[https://kagi.com/settings?p=api][API key]] |
| together.ai | ✓ | [[https://api.together.xyz/settings/api-keys][API key]] |
| Anyscale | ✓ | [[https://docs.endpoints.anyscale.com/][API key]] |
| LLM Backend | Supports | Requires |
|--------------------+----------+---------------------------|
| ChatGPT | ✓ | [[https://platform.openai.com/account/api-keys][API key]] |
| Azure | ✓ | Deployment and API key |
| Ollama | ✓ | [[https://ollama.ai/][Ollama running locally]] |
| GPT4All | ✓ | [[https://gpt4all.io/index.html][GPT4All running locally]] |
| Gemini | ✓ | [[https://makersuite.google.com/app/apikey][API key]] |
| Llama.cpp | ✓ | [[https://github.com/ggerganov/llama.cpp/tree/master/examples/server#quick-start][Llama.cpp running locally]] |
| Llamafile | ✓ | [[https://github.com/Mozilla-Ocho/llamafile#quickstart][Local Llamafile server]] |
| Kagi FastGPT | ✓ | [[https://kagi.com/settings?p=api][API key]] |
| Kagi Summarizer | ✓ | [[https://kagi.com/settings?p=api][API key]] |
| together.ai | ✓ | [[https://api.together.xyz/settings/api-keys][API key]] |
| Anyscale | ✓ | [[https://docs.endpoints.anyscale.com/][API key]] |
| 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]])
@ -36,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]]
@ -55,10 +59,15 @@ GPTel uses Curl if available, but falls back to url-retrieve to work without ext
- [[#kagi-fastgpt--summarizer][Kagi (FastGPT & Summarizer)]]
- [[#togetherai][together.ai]]
- [[#anyscale][Anyscale]]
- [[#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]]
@ -70,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=.)
@ -150,12 +159,22 @@ Register a backend with
#+end_src
Refer to the documentation of =gptel-make-azure= to set more parameters.
You can pick this backend from the menu when using gptel. (see [[#usage][Usage]])
You can pick this backend from the menu when using gptel. (see [[#usage][Usage]]).
If you want it to be the default, set it as the default value of =gptel-backend=:
***** (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
(setq-default gptel-backend (gptel-make-azure "Azure-1" ...)
gptel-model "gpt-3.5-turbo")
;; OPTIONAL configuration
(setq
gptel-model "gpt-3.5-turbo"
gptel-backend (gptel-make-azure "Azure-1"
:protocol "https"
:host "YOUR_RESOURCE_NAME.openai.azure.com"
:endpoint "/openai/deployments/YOUR_DEPLOYMENT_NAME/chat/completions?api-version=2023-05-15"
:stream t
:key #'gptel-api-key
:models '("gpt-3.5-turbo" "gpt-4")))
#+end_src
#+html: </details>
@ -166,19 +185,26 @@ If you want it to be the default, set it as the default value of =gptel-backend=
Register a backend with
#+begin_src emacs-lisp
(gptel-make-gpt4all "GPT4All" ;Name of your choosing
:protocol "http"
:protocol "http"
:host "localhost:4891" ;Where it's running
:models '("mistral-7b-openorca.Q4_0.gguf")) ;Available models
#+end_src
These are the required parameters, refer to the documentation of =gptel-make-gpt4all= for more.
You can pick this backend from the menu when using gptel (see [[#usage][Usage]]), or set this as the default value of =gptel-backend=. Additionally you may want to increase the response token size since GPT4All uses very short (often truncated) responses by default:
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. Additionally you may want to increase the response token size since GPT4All uses very short (often truncated) responses by default.
#+begin_src emacs-lisp
;; OPTIONAL configuration
(setq-default gptel-model "mistral-7b-openorca.Q4_0.gguf" ;Pick your default model
gptel-backend (gptel-make-gpt4all "GPT4All" :protocol ...))
(setq-default gptel-max-tokens 500)
(setq
gptel-max-tokens 500
gptel-model "mistral-7b-openorca.Q4_0.gguf"
gptel-backend (gptel-make-gpt4all "GPT4All"
:protocol "http"
:host "localhost:4891"
:models '("mistral-7b-openorca.Q4_0.gguf")))
#+end_src
#+html: </details>
@ -196,12 +222,19 @@ Register a backend with
#+end_src
These are the required parameters, refer to the documentation of =gptel-make-ollama= for more.
You can pick this backend from the menu when using gptel (see [[#usage][Usage]]), or set this as the default value of =gptel-backend=:
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-default gptel-model "mistral:latest" ;Pick your default model
gptel-backend (gptel-make-ollama "Ollama" :host ...))
(setq
gptel-model "mistral:latest"
gptel-backend (gptel-make-ollama "Ollama"
:host "localhost:11434"
:stream t
:models '("mistral:latest")))
#+end_src
#+html: </details>
@ -213,18 +246,22 @@ You can pick this backend from the menu when using gptel (see [[#usage][Usage]])
Register a backend with
#+begin_src emacs-lisp
;; :key can be a function that returns the API key.
(gptel-make-gemini "Gemini"
:key "YOUR_GEMINI_API_KEY"
:stream t)
(gptel-make-gemini "Gemini" :key "YOUR_GEMINI_API_KEY" :stream t)
#+end_src
These are the required parameters, refer to the documentation of =gptel-make-gemini= for more.
You can pick this backend from the menu when using gptel (see [[#usage][Usage]]), or set this as the default value of =gptel-backend=:
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-default gptel-model "gemini-pro" ;Pick your default model
gptel-backend (gptel-make-gemini "Gemini" :host ...))
(setq
gptel-model "gemini-pro"
gptel-backend (gptel-make-gemini "Gemini"
:key "YOUR_GEMINI_API_KEY"
:stream t))
#+end_src
#+html: </details>
@ -247,11 +284,20 @@ Register a backend with
#+end_src
These are the required parameters, refer to the documentation of =gptel-make-openai= for more.
You can pick this backend from the menu when using gptel (see [[#usage][Usage]]), or set this as the default value of =gptel-backend=:
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-default gptel-backend (gptel-make-openai "llama-cpp" ...)
gptel-model "test")
(setq
gptel-model "test"
gptel-backend (gptel-make-openai "llama-cpp"
:stream t
:protocol "http"
:host "localhost:8000"
:models '("test")))
#+end_src
#+html: </details>
@ -272,12 +318,17 @@ Register a backend with
#+end_src
These are the required parameters, refer to the documentation of =gptel-make-kagi= for more.
You can pick this backend and the model (fastgpt/summarizer) from the transient menu when using gptel. Alternatively you can set this as the default value of =gptel-backend=:
You can pick this backend and the model (fastgpt/summarizer) from the transient menu when using gptel.
***** (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-default gptel-model "fastgpt"
gptel-backend (gptel-make-kagi "Kagi" :key ...))
(setq
gptel-model "fastgpt"
gptel-backend (gptel-make-kagi "Kagi"
:key "YOUR_KAGI_API_KEY"))
#+end_src
The alternatives to =fastgpt= include =summarize:cecil=, =summarize:agnes=, =summarize:daphne= and =summarize:muriel=. The difference between the summarizer engines is [[https://help.kagi.com/kagi/api/summarizer.html#summarization-engines][documented here]].
@ -300,11 +351,24 @@ Register a backend with
"codellama/CodeLlama-34b-Instruct-hf"))
#+end_src
You can pick this backend from the menu when using gptel (see [[#usage][Usage]]), or set this as the default value of =gptel-backend=:
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-default gptel-backend (gptel-make-openai "TogetherAI" ...)
gptel-model "mistralai/Mixtral-8x7B-Instruct-v0.1")
(setq
gptel-model "mistralai/Mixtral-8x7B-Instruct-v0.1"
gptel-backend
(gptel-make-openai "TogetherAI"
:host "api.together.xyz"
:key "your-api-key"
:stream t
:models '(;; has many more, check together.ai
"mistralai/Mixtral-8x7B-Instruct-v0.1"
"codellama/CodeLlama-13b-Instruct-hf"
"codellama/CodeLlama-34b-Instruct-hf")))
#+end_src
#+html: </details>
@ -322,29 +386,194 @@ Register a backend with
"mistralai/Mixtral-8x7B-Instruct-v0.1"))
#+end_src
You can pick this backend from the menu when using gptel (see [[#usage][Usage]]), or set this as the default value of =gptel-backend=:
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-default gptel-backend (gptel-make-openai "Anyscale" ...)
gptel-model "mistralai/Mixtral-8x7B-Instruct-v0.1")
(setq
gptel-model "mistralai/Mixtral-8x7B-Instruct-v0.1"
gptel-backend
(gptel-make-openai "Anyscale"
:host "api.endpoints.anyscale.com"
:key "your-api-key"
:models '(;; has many more, check anyscale
"mistralai/Mixtral-8x7B-Instruct-v0.1")))
#+end_src
#+html: </details>
#+html: <details><summary>
**** Perplexity
#+html: </summary>
Register a backend with
#+begin_src emacs-lisp
;; Perplexity offers an OpenAI compatible API
(gptel-make-openai "Perplexity" ;Any name you want
:host "api.perplexity.ai"
:key "your-api-key" ;can be a function that returns the key
:endpoint "/chat/completions"
:stream t
:models '(;; has many more, check perplexity.ai
"pplx-7b-chat"
"pplx-70b-chat"
"pplx-7b-online"
"pplx-70b-online"))
#+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 "pplx-7b-chat"
gptel-backend
(gptel-make-openai "Perplexity"
:host "api.perplexity.ai"
:key "your-api-key"
:endpoint "/chat/completions"
:stream t
:models '(;; has many more, check perplexity.ai
"pplx-7b-chat"
"pplx-70b-chat"
"pplx-7b-online"
"pplx-70b-online")))
#+end_src
#+html: </details>
#+html: <details><summary>
**** Anthropic (Claude)
#+html: </summary>
Register a backend with
#+begin_src emacs-lisp
(gptel-make-anthropic "Claude" ;Any name you want
:stream t ;Streaming responses
:key "your-api-key")
#+end_src
The =:key= can be a function that returns the key (more secure).
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 "claude-3-sonnet-20240229" ; "claude-3-opus-20240229" also available
gptel-backend (gptel-make-anthropic "Claude"
:stream t :key "your-api-key"))
#+end_src
#+html: </details>
#+html: <details><summary>
**** Groq
#+html: </summary>
Register a backend with
#+begin_src emacs-lisp
;; Groq offers an OpenAI compatible API
(gptel-make-openai "Groq" ;Any name you want
:host "api.groq.com"
:endpoint "/openai/v1/chat/completions"
:stream t
:key "your-api-key" ;can be a function that returns the key
:models '("mixtral-8x7b-32768"
"gemma-7b-it"
"llama2-70b-4096"))
#+end_src
You can pick this backend from the menu when using gptel (see [[#usage][Usage]]). Note that Groq is fast enough that you could easily set =:stream nil= and still get near-instant responses.
***** (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 "Groq"
:host "api.groq.com"
:endpoint "/openai/v1/chat/completions"
:stream t
:key "your-api-key"
:models '("mixtral-8x7b-32768"
"gemma-7b-it"
"llama2-70b-4096")))
#+end_src
#+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:
@ -385,14 +614,53 @@ The default mode is =markdown-mode= if available, else =text-mode=. You can set
**** Save and restore your chat sessions
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.
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)
@ -403,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)
@ -428,35 +696,35 @@ Anywhere in Emacs: Use =gptel-pre-response-hook= and =gptel-post-response-functi
Any model options you set are saved for the current buffer. But the redirection options in the menu are set for the next query only:
https://github.com/karthink/gptel/assets/8607532/2ecc6be9-aa52-4287-a739-ba06e1369ec2
#+html: <img src="https://github.com/karthink/gptel/assets/8607532/2ecc6be9-aa52-4287-a739-ba06e1369ec2" alt="https://github.com/karthink/gptel/assets/8607532/2ecc6be9-aa52-4287-a739-ba06e1369ec2">
You can make them persistent across this Emacs session by pressing ~C-x C-s~:
https://github.com/karthink/gptel/assets/8607532/b8bcb6ad-c974-41e1-9336-fdba0098a2fe
#+html: <img src="https://github.com/karthink/gptel/assets/8607532/b8bcb6ad-c974-41e1-9336-fdba0098a2fe" alt="https://github.com/karthink/gptel/assets/8607532/b8bcb6ad-c974-41e1-9336-fdba0098a2fe">
(You can also cycle through presets you've saved with ~C-x p~ and ~C-x n~.)
Now these will be enabled whenever you send a query from the transient menu. If you want to use these options without invoking the transient menu, you can use a keyboard macro:
Now these will be enabled whenever you send a query from the transient menu. If you want to use these saved options without invoking the transient menu, you can use a keyboard macro:
#+begin_src emacs-lisp
;; Replace with your key to invoke the transient menu:
(keymap-global-set "<f6>" "C-u C-c <return> <return>")
#+end_src
See [[https://github.com/karthink/gptel/issues/94#issuecomment-1657093458][this comment by Tianshu Wang]] for an Elisp solution.
Or see this [[https://github.com/karthink/gptel/wiki#save-transient-flags][wiki entry]].
#+html: </details>
#+html: <details><summary>
**** 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.
#+html: </details>
#+html: <details><summary>
**** (Doom Emacs) Sending a query from the gptel menu fails because of a key conflict with Org mode
**** (Doom Emacs) Sending a query from the gptel menu fails because of a key conflict with Org mode
#+html: </summary>
Doom binds ~RET~ in Org mode to =+org/dwim-at-point=, which appears to conflict with gptel's transient menu bindings for some reason.
@ -524,14 +792,15 @@ Other Emacs clients for LLMs prescribe the format of the interaction (a comint s
| =gptel-temperature= | Randomness in response text, 0 to 2. |
|-------------------+---------------------------------------------------------|
|-----------------------------+----------------------------------------|
| *Chat UI options* | |
|-----------------------------+----------------------------------------|
| =gptel-default-mode= | Major mode for dedicated chat buffers. |
| =gptel-prompt-prefix-alist= | Text inserted before queries. |
| =gptel-response-prefix-alist= | Text inserted before responses. |
| =gptel-use-header-line= | Display status messages in header-line (default) or minibuffer |
|-----------------------------+----------------------------------------|
|-----------------------------+----------------------------------------------------------------|
| *Chat UI options* | |
|-----------------------------+----------------------------------------------------------------|
| =gptel-default-mode= | Major mode for dedicated chat buffers. |
| =gptel-prompt-prefix-alist= | Text inserted before queries. |
| =gptel-response-prefix-alist= | Text inserted before responses. |
| =gptel-use-header-line= | Display status messages in header-line (default) or minibuffer |
| =gptel-display-buffer-action= | Placement of the gptel chat buffer. |
|-----------------------------+----------------------------------------------------------------|
** COMMENT Will you add feature X?
@ -556,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

156
gptel-anthropic.el Normal file
View file

@ -0,0 +1,156 @@
;;; gptel-anthropic.el --- Anthropic AI suppport for gptel -*- lexical-binding: t; -*-
;; Copyright (C) 2023 Karthik Chikmagalur
;; Author: Karthik Chikmagalur <karthikchikmagalur@gmail.com>
;; 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
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;; This file adds support for Anthropic's Messages API to gptel
;;; Code:
(require 'cl-generic)
(eval-when-compile
(require 'cl-lib))
(require 'map)
(require 'gptel)
(defvar json-object-type)
(declare-function prop-match-value "text-property-search")
(declare-function text-property-search-backward "text-property-search")
(declare-function json-read "json" ())
;;; Anthropic (Messages API)
(cl-defstruct (gptel-anthropic (:constructor gptel--make-anthropic)
(:copier nil)
(:include gptel-backend)))
(cl-defmethod gptel-curl--parse-stream ((_backend gptel-anthropic) _info)
(let* ((content-strs)
(pt (point)))
(condition-case nil
(while (re-search-forward "^event: " nil t)
(setq pt (match-beginning 0))
(if (equal (line-end-position) (point-max))
(error "Data block incomplete"))
(when (looking-at "content_block_\\(?:start\\|delta\\|stop\\)")
(forward-line 1) (forward-char 5)
(when-let* ((response (gptel--json-read))
(content (map-nested-elt
response '(:delta :text))))
(push content content-strs))))
(error (goto-char pt)))
(apply #'concat (nreverse content-strs))))
(cl-defmethod gptel--parse-response ((_backend gptel-anthropic) response _info)
(map-nested-elt response '(:content 0 :text)))
(cl-defmethod gptel--request-data ((_backend gptel-anthropic) prompts)
"JSON encode PROMPTS for sending to ChatGPT."
(let ((prompts-plist
`(:model ,gptel-model
:system ,gptel--system-message
:stream ,(or (and gptel-stream gptel-use-curl
(gptel-backend-stream gptel-backend))
:json-false)
:max_tokens ,(or gptel-max-tokens 1024)
:messages [,@prompts])))
(when gptel-temperature
(plist-put prompts-plist :temperature gptel-temperature))
prompts-plist))
(cl-defmethod gptel--parse-buffer ((_backend gptel-anthropic) &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)))
prompts))
;;;###autoload
(cl-defun gptel-make-anthropic
(name &key curl-args stream key
(header
(lambda () (when-let (key (gptel--get-api-key))
`(("x-api-key" . ,key)
("anthropic-version" . "2023-06-01")))))
(models '("claude-3-sonnet-20240229"
"claude-3-haiku-20240307"
"claude-3-opus-20240229"))
(host "api.anthropic.com")
(protocol "https")
(endpoint "/v1/messages"))
"Register an Anthropic API-compatible backend for gptel with NAME.
Keyword arguments:
CURL-ARGS (optional) is a list of additional Curl arguments.
HOST (optional) is the API host, \"api.anthropic.com\" by default.
MODELS is a list of available model names.
STREAM is a boolean to toggle streaming responses, defaults to
false.
PROTOCOL (optional) specifies the protocol, https by default.
ENDPOINT (optional) is the API endpoint for completions, defaults to
\"/v1/messages\".
HEADER (optional) is for additional headers to send with each
request. It should be an alist or a function that retuns an
alist, like:
((\"Content-Type\" . \"application/json\"))
KEY is a variable whose value is the API key, or function that
returns the key."
(declare (indent 1))
(let ((backend (gptel--make-anthropic
:curl-args curl-args
:name name
:host host
:header header
:key key
:models models
:protocol protocol
:endpoint endpoint
:stream stream
:url (if protocol
(concat protocol "://" host endpoint)
(concat host endpoint)))))
(prog1 backend
(setf (alist-get name gptel--known-backends
nil nil #'equal)
backend))))
(provide 'gptel-anthropic)
;;; gptel-backends.el ends here

View file

@ -32,7 +32,11 @@
(require 'cl-lib)
(require 'subr-x))
(require 'map)
(require 'json)
(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))
@ -45,16 +49,14 @@
(defvar gptel-curl--process-alist nil
"Alist of active GPTel curl requests.")
(defun gptel-curl--get-args (prompts token)
(defun gptel-curl--get-args (data token)
"Produce list of arguments for calling Curl.
PROMPTS is the data to send, TOKEN is a unique identifier."
REQUEST-DATA is the data to send, TOKEN is a unique identifier."
(let* ((url (let ((backend-url (gptel-backend-url gptel-backend)))
(if (functionp backend-url)
(funcall backend-url) backend-url)))
(data (encode-coding-string
(json-encode (gptel--request-data gptel-backend prompts))
'utf-8))
(data-json (encode-coding-string (gptel--json-encode data) 'utf-8))
(headers
(append '(("Content-Type" . "application/json"))
(when-let ((header (gptel-backend-header gptel-backend)))
@ -62,15 +64,19 @@ PROMPTS is the data to send, TOKEN is a unique identifier."
(funcall header) header)))))
(when gptel-log-level
(when (eq gptel-log-level 'debug)
(gptel--log (json-encode headers) "request headers"))
(gptel--log data "request body"))
(gptel--log (gptel--json-encode
(mapcar (lambda (pair) (cons (intern (car pair)) (cdr pair)))
headers))
"request headers"))
(gptel--log data-json "request body"))
(append
gptel-curl--common-args
(gptel-backend-curl-args gptel-backend)
(list (format "-w(%s . %%{size_header})" token))
(if (length< data gptel-curl-file-size-threshold)
(list (format "-d%s" data))
(if (length< data-json gptel-curl-file-size-threshold)
(list (format "-d%s" data-json))
(letrec
((temp-filename (make-temp-file "gptel-curl-data" nil ".json" data))
((temp-filename (make-temp-file "gptel-curl-data" nil ".json" data-json))
(cleanup-fn (lambda (&rest _)
(when (file-exists-p temp-filename)
(delete-file temp-filename)
@ -93,7 +99,7 @@ PROMPTS is the data to send, TOKEN is a unique identifier."
"Retrieve response to prompt in INFO.
INFO is a plist with the following keys:
- :prompt (the prompt being sent)
- :data (the data being sent)
- :buffer (the gptel buffer)
- :position (marker at which to insert the response).
@ -102,13 +108,13 @@ the response is inserted into the current buffer after point."
(let* ((token (md5 (format "%s%s%s%s"
(random) (emacs-pid) (user-full-name)
(recent-keys))))
(args (gptel-curl--get-args (plist-get info :prompt) token))
(args (gptel-curl--get-args (plist-get info :data) token))
(stream (and gptel-stream (gptel-backend-stream gptel-backend)))
(process (apply #'start-process "gptel-curl"
(generate-new-buffer "*gptel-curl*") "curl" args)))
(when (eq gptel-log-level 'debug)
(gptel--log (json-encode (cons "curl" args))
"request Curl command"))
(gptel--log (mapconcat #'shell-quote-argument (cons "curl" args) " \\\n")
"request Curl command" 'no-json))
(with-current-buffer (process-buffer process)
(set-process-query-on-exit-flag process nil)
(setf (alist-get process gptel-curl--process-alist)
@ -153,7 +159,7 @@ PROC-INFO is the plist containing process metadata."
(goto-char (point-min))
(when (re-search-forward " ?\n ?\n" nil t)
(when (eq gptel-log-level 'debug)
(gptel--log (json-encode-string
(gptel--log (gptel--json-encode
(buffer-substring-no-properties
(point-min) (1- (point))))
"response headers"))
@ -198,30 +204,26 @@ PROCESS and _STATUS are process parameters."
(tracking-marker (plist-get info :tracking-marker))
(start-marker (plist-get info :position))
(http-status (plist-get info :http-status))
(http-msg (plist-get info :status))
response-beg response-end)
(http-msg (plist-get info :status)))
(when gptel-log-level (gptel-curl--log-response proc-buf info)) ;logging
(if (equal http-status "200")
(progn
;; Finish handling response
(with-current-buffer (marker-buffer start-marker)
(setq response-beg (+ start-marker 2)
response-end (marker-position tracking-marker))
(pulse-momentary-highlight-region response-beg tracking-marker)
(when gptel-mode (save-excursion (goto-char tracking-marker)
(insert "\n\n" (gptel-prompt-prefix-string)))))
(with-current-buffer gptel-buffer
(when gptel-mode (gptel--update-status " Ready" 'success))))
(if (member http-status '("200" "100")) ;Finish handling response
(with-current-buffer gptel-buffer
(if (not tracking-marker) ;Empty response
(when gptel-mode (gptel--update-status " Empty response" 'success))
(pulse-momentary-highlight-region start-marker tracking-marker)
(when gptel-mode
(save-excursion (goto-char tracking-marker)
(insert "\n\n" (gptel-prompt-prefix-string)))
(gptel--update-status " Ready" 'success))))
;; Or Capture error message
(with-current-buffer proc-buf
(goto-char (point-max))
(search-backward (plist-get info :token))
(backward-char)
(pcase-let* ((`(,_ . ,header-size) (read (current-buffer)))
(json-object-type 'plist)
(response (progn (goto-char header-size)
(condition-case nil (json-read)
(json-readtable-error 'json-read-error))))
(condition-case nil (gptel--json-read)
(error 'json-read-error))))
(error-data (plist-get response :error)))
(cond
(error-data
@ -238,8 +240,16 @@ PROCESS and _STATUS are process parameters."
(when gptel-mode
(gptel--update-status
(format " Response Error: %s" http-msg) 'error))))
(with-current-buffer gptel-buffer
(run-hook-with-args 'gptel-post-response-functions response-beg response-end)))
;; Run hook in visible window to set window-point, BUG #269
(if-let ((gptel-window (get-buffer-window gptel-buffer 'visible)))
(with-selected-window gptel-window
(run-hook-with-args 'gptel-post-response-functions
(marker-position start-marker)
(marker-position (or tracking-marker start-marker))))
(with-current-buffer gptel-buffer
(run-hook-with-args 'gptel-post-response-functions
(marker-position start-marker)
(marker-position (or tracking-marker start-marker))))))
(setf (alist-get process gptel-curl--process-alist nil 'remove) nil)
(kill-buffer proc-buf)))
@ -261,7 +271,8 @@ See `gptel--url-get-response' for details."
(insert "\n\n")
(when gptel-mode
;; Put prefix before AI response.
(insert (gptel-response-prefix-string))))
(insert (gptel-response-prefix-string)))
(move-marker start-marker (point)))
(setq tracking-marker (set-marker (make-marker) (point)))
(set-marker-insertion-type tracking-marker t)
(plist-put info :tracking-marker tracking-marker))
@ -269,9 +280,8 @@ See `gptel--url-get-response' for details."
(when transformer
(setq response (funcall transformer response)))
(add-text-properties
0 (length response) '(gptel response rear-nonsticky t)
response)
(put-text-property
0 (length response) 'gptel 'response response)
(goto-char tracking-marker)
;; (run-hooks 'gptel-pre-stream-hook)
(insert response)
@ -314,7 +324,7 @@ See `gptel--url-get-response' for details."
display-buffer-pop-up-window)
(reusable-frames . visible))))
;; Run pre-response hook
(when (and (equal (plist-get proc-info :http-status) "200")
(when (and (member (plist-get proc-info :http-status) '("200" "100"))
gptel-pre-response-hook)
(with-current-buffer (marker-buffer (plist-get proc-info :position))
(run-hooks 'gptel-pre-response-hook))))
@ -322,7 +332,8 @@ See `gptel--url-get-response' for details."
(when-let ((http-msg (plist-get proc-info :status))
(http-status (plist-get proc-info :http-status)))
;; Find data chunk(s) and run callback
(when-let (((equal http-status "200"))
;; FIXME Handle the case where HTTP 100 is followed by HTTP (not 200) BUG #194
(when-let (((member http-status '("200" "100")))
(response (funcall (plist-get proc-info :parser) nil proc-info))
((not (equal response ""))))
(funcall (or (plist-get proc-info :callback)
@ -346,9 +357,6 @@ See `gptel-curl--get-response' for its contents.")
PROCESS and _STATUS are process parameters."
(let ((proc-buf (process-buffer process)))
(when gptel--debug
(with-current-buffer proc-buf
(clone-buffer "*gptel-error*" 'show)))
(when-let* (((eq (process-status process) 'exit))
(proc-info (alist-get process gptel-curl--process-alist))
(proc-callback (plist-get proc-info :callback)))
@ -381,13 +389,13 @@ PROC-INFO is a plist with contextual information."
(save-match-data
(and (string-match "HTTP/[.0-9]+ +\\([0-9]+\\)" http-msg)
(match-string 1 http-msg))))
(json-object-type 'plist)
(response (progn (goto-char header-size)
(condition-case nil
(json-read)
(json-readtable-error 'json-read-error)))))
(gptel--json-read)
(error 'json-read-error)))))
(cond
((equal http-status "200")
;; FIXME Handle the case where HTTP 100 is followed by HTTP (not 200) BUG #194
((member http-status '("200" "100"))
(list (string-trim
(funcall parser nil response proc-info))
http-msg))

View file

@ -30,6 +30,7 @@
(declare-function prop-match-value "text-property-search")
(declare-function text-property-search-backward "text-property-search")
(declare-function json-read "json")
(defvar json-object-type)
;;; Gemini
(cl-defstruct
@ -38,15 +39,14 @@
(:include gptel-backend)))
(cl-defmethod gptel-curl--parse-stream ((_backend gptel-gemini) _info)
(let* ((json-object-type 'plist)
(content-strs))
(let* ((content-strs))
(condition-case nil
;; while-let is Emacs 29.1+ only
(while (prog1 (search-forward "{" nil t)
(backward-char 1))
(save-match-data
(when-let*
((response (json-read))
((response (gptel--json-read))
(text (map-nested-elt
response '(:candidates 0 :content :parts 0 :text))))
(push text content-strs))))
@ -60,7 +60,15 @@
(cl-defmethod gptel--request-data ((_backend gptel-gemini) prompts)
"JSON encode PROMPTS for sending to Gemini."
(let ((prompts-plist
`(:contents [,@prompts]))
`(:contents [,@prompts]
:safetySettings [(:category "HARM_CATEGORY_HARASSMENT"
:threshold "BLOCK_NONE")
(:category "HARM_CATEGORY_SEXUALLY_EXPLICIT"
:threshold "BLOCK_NONE")
(:category "HARM_CATEGORY_DANGEROUS_CONTENT"
:threshold "BLOCK_NONE")
(:category "HARM_CATEGORY_HATE_SPEECH"
:threshold "BLOCK_NONE")]))
params)
(when gptel-temperature
(setq params
@ -103,21 +111,23 @@
;;;###autoload
(cl-defun gptel-make-gemini
(name &key header key stream
(name &key curl-args header key (stream nil)
(host "generativelanguage.googleapis.com")
(protocol "https")
(models '("gemini-pro"))
(endpoint "/v1beta/models/gemini-pro:"))
(models '("gemini-pro"
"gemini-1.5-pro-latest"))
(endpoint "/v1beta/models"))
"Register a Gemini backend for gptel with NAME.
Keyword arguments:
CURL-ARGS (optional) is a list of additional Curl arguments.
HOST (optional) is the API host, defaults to
\"generativelanguage.googleapis.com\".
MODELS is a list of available model names. Currently only
\"gemini-pro\" is available.
MODELS is a list of available model names.
STREAM is a boolean to enable streaming responses, defaults to
false.
@ -125,8 +135,7 @@ false.
PROTOCOL (optional) specifies the protocol, \"https\" by default.
ENDPOINT (optional) is the API endpoint for completions, defaults to
\"/v1beta/models/gemini-pro:streamGenerateContent\" if STREAM is true and
\"/v1beta/models/gemini-pro:generateContent\" otherwise.
\"/v1beta/models\".
HEADER (optional) is for additional headers to send with each
request. It should be an alist or a function that retuns an
@ -137,6 +146,7 @@ KEY (optional) is a variable whose value is the API key, or
function that returns the key."
(declare (indent 1))
(let ((backend (gptel--make-gemini
:curl-args curl-args
:name name
:host host
:header header
@ -145,13 +155,18 @@ function that returns the key."
:endpoint endpoint
:stream stream
:key key
:url
(lambda ()
(concat protocol "://" host endpoint
(if gptel-stream
"streamGenerateContent"
"generateContent")
"?key=" (gptel--get-api-key))))))
:url (lambda ()
(let ((method (if (and stream
gptel-stream)
"streamGenerateContent"
"generateContent")))
(format "%s://%s%s/%s:%s?key=%s"
protocol
host
endpoint
gptel-model
method
(gptel--get-api-key)))))))
(prog1 backend
(setf (alist-get name gptel--known-backends
nil nil #'equal)

View file

@ -120,7 +120,7 @@
;;;###autoload
(cl-defun gptel-make-kagi
(name &key stream key
(name &key curl-args stream key
(host "kagi.com")
(header (lambda () `(("Authorization" . ,(concat "Bot " (gptel--get-api-key))))))
(models '("fastgpt"
@ -132,6 +132,8 @@
Keyword arguments:
CURL-ARGS (optional) is a list of additional Curl arguments.
HOST is the Kagi host (with port), defaults to \"kagi.com\".
MODELS is a list of available Kagi models: only fastgpt is supported.
@ -159,6 +161,7 @@ Example:
(declare (indent 1))
stream ;Silence byte-compiler
(let ((backend (gptel--make-kagi
:curl-args curl-args
:name name
:host host
:header header

View file

@ -26,89 +26,108 @@
(require 'gptel)
(require 'cl-generic)
(declare-function json-read "json" ())
(defvar json-object-type)
;;; Ollama
(cl-defstruct (gptel-ollama (:constructor gptel--make-ollama)
(: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* ((json-object-type 'plist)
(content-strs)
(content))
(let* ((content-strs) (content) (pt (point)))
(condition-case nil
(while (setq content (json-read))
(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)
(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 (forward-line 0)))
(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 header key models stream
(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:
CURL-ARGS (optional) is a list of additional Curl arguments.
HOST is where Ollama runs (with port), defaults to localhost:11434
MODELS is a list of available model names.
@ -140,6 +159,7 @@ Example:
:stream t)"
(declare (indent 1))
(let ((backend (gptel--make-ollama
:curl-args curl-args
:name name
:host host
:header header

View file

@ -44,12 +44,35 @@
(declare-function gptel-prompt-prefix-string "gptel")
(declare-function gptel-response-prefix-string "gptel")
(defmacro gptel--json-read ()
(if (fboundp 'json-parse-buffer)
`(json-parse-buffer
:object-type 'plist
:null-object nil
:false-object :json-false)
(require 'json)
(defvar json-object-type)
(declare-function json-read "json" ())
`(let ((json-object-type 'plist))
(json-read))))
(defmacro gptel--json-encode (object)
(if (fboundp 'json-serialize)
`(json-serialize ,object
:null-object nil
:false-object :json-false)
(require 'json)
(defvar json-false)
(declare-function json-encode "json" (object))
`(let ((json-false :json-false))
(json-encode ,object))))
;;; Common backend struct for LLM support
(cl-defstruct
(gptel-backend (:constructor gptel--make-backend)
(:copier gptel--copy-backend))
name host header protocol stream
endpoint key models url)
endpoint key models url curl-args)
;;; OpenAI (ChatGPT)
(cl-defstruct (gptel-openai (:constructor gptel--make-openai)
@ -57,13 +80,12 @@
(:include gptel-backend)))
(cl-defmethod gptel-curl--parse-stream ((_backend gptel-openai) _info)
(let* ((json-object-type 'plist)
(content-strs))
(let* ((content-strs))
(condition-case nil
(while (re-search-forward "^data:" nil t)
(save-match-data
(unless (looking-at " *\\[DONE\\]")
(when-let* ((response (json-read))
(when-let* ((response (gptel--json-read))
(delta (map-nested-elt
response '(:choices 0 :delta)))
(content (plist-get delta :content)))
@ -115,7 +137,7 @@
;;;###autoload
(cl-defun gptel-make-openai
(name &key models stream key
(name &key curl-args models stream key
(header
(lambda () (when-let (key (gptel--get-api-key))
`(("Authorization" . ,(concat "Bearer " key))))))
@ -126,6 +148,8 @@
Keyword arguments:
CURL-ARGS (optional) is a list of additional Curl arguments.
HOST (optional) is the API host, typically \"api.openai.com\".
MODELS is a list of available model names.
@ -147,6 +171,7 @@ KEY (optional) is a variable whose value is the API key, or
function that returns the key."
(declare (indent 1))
(let ((backend (gptel--make-openai
:curl-args curl-args
:name name
:host host
:header header
@ -166,7 +191,7 @@ function that returns the key."
;;; Azure
;;;###autoload
(cl-defun gptel-make-azure
(name &key host
(name &key curl-args host
(protocol "https")
(header (lambda () `(("api-key" . ,(gptel--get-api-key)))))
(key 'gptel-api-key)
@ -175,6 +200,8 @@ function that returns the key."
Keyword arguments:
CURL-ARGS (optional) is a list of additional Curl arguments.
HOST is the API host.
MODELS is a list of available model names.
@ -207,6 +234,7 @@ Example:
:models \\='(\"gpt-3.5-turbo\" \"gpt-4\"))"
(declare (indent 1))
(let ((backend (gptel--make-openai
:curl-args curl-args
:name name
:host host
:header header
@ -230,6 +258,8 @@ Example:
Keyword arguments:
CURL-ARGS (optional) is a list of additional Curl arguments.
HOST is where GPT4All runs (with port), typically localhost:8491
MODELS is a list of available model names.

484
gptel-org.el Normal file
View file

@ -0,0 +1,484 @@
;;; gptel-org.el --- Org functions for gptel -*- lexical-binding: t; -*-
;; Copyright (C) 2024 Karthik Chikmagalur
;; Author: Karthik Chikmagalur <karthikchikmagalur@gmail.com>
;; 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
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
;;; 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
(defcustom gptel-org-branching-context nil
"Use the lineage of the current heading as the context for gptel in Org buffers.
This makes each same level heading a separate conversation
branch.
By default, gptel uses a linear context: all the text up to the
cursor is sent to the LLM. Enabling this option makes the
context the hierarchical lineage of the current Org heading. In
this example:
-----
Top level text
* Heading 1
heading 1 text
* Heading 2
heading 2 text
** Heading 2.1
heading 2.1 text
** Heading 2.2
heading 2.2 text
-----
With the cursor at the end of the buffer, the text sent to the
LLM will be limited to
-----
Top level text
* Heading 2
heading 2 text
** Heading 2.2
heading 2.2 text
-----
This makes it feasible to have multiple conversation branches."
:local t
:type 'boolean
:group 'gptel)
;;; Setting context and creating queries
(defun gptel-org--get-topic-start ()
"If a conversation topic is set, return it."
(when (org-entry-get (point) "GPTEL_TOPIC" 'inherit)
(marker-position org-entry-property-inherited-from)))
(defun gptel-org-set-topic (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
(list
(progn
(or (derived-mode-p 'org-mode)
(user-error "Support for multiple topics per buffer is only implemented for `org-mode'."))
(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))))))
(when (stringp topic) (org-set-property "GPTEL_TOPIC" topic)))
;; NOTE: This can be converted to a cl-defmethod for `gptel--parse-buffer'
;; (conceptually cleaner), but will cause load-order issues in gptel.el and
;; might be harder to debug.
(defun gptel-org--create-prompt (&optional prompt-end)
"Return a full conversation prompt from the contents of this Org buffer.
If `gptel--num-messages-to-send' is set, limit to that many
recent exchanges.
The prompt is constructed from the contents of the buffer up to
point, or PROMPT-END if provided. Its contents depend on the
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-org--get-topic-start)))
(when topic-start
;; narrow to GPTEL_TOPIC property scope
(narrow-to-region topic-start prompt-end))
(if gptel-org-branching-context
;; Create prompt from direct ancestors of point
(save-excursion
(let* ((org-buf (current-buffer))
(start-bounds (gptel-org--element-lineage-map
(org-element-at-point) #'org-element-begin
'(headline org-data) 'with-self))
(end-bounds
(cl-loop
for pos in (cdr start-bounds)
while
(and (>= pos (point-min)) ;respect narrowing
(goto-char pos)
;; org-element-lineage always returns an extra
;; (org-data) element at point 1. If there is also a
;; heading here, it is either a false positive or we
;; would be double counting it. So we reject this node
;; when also at a heading.
(not (and (eq pos 1) (org-at-heading-p))))
do (outline-next-heading)
collect (point) into ends
finally return (cons prompt-end ends))))
(with-temp-buffer
(setq-local gptel-backend
(buffer-local-value 'gptel-backend org-buf)
gptel--system-message
(buffer-local-value 'gptel--system-message org-buf)
gptel-model
(buffer-local-value 'gptel-model org-buf))
(cl-loop for start in start-bounds
for end in end-bounds
do (insert-buffer-substring org-buf start end)
(goto-char (point-min)))
(goto-char (point-max))
(let ((major-mode 'org-mode))
(gptel--parse-buffer gptel-backend max-entries)))))
;; 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)
"Find gptel configuration properties stored in the current heading."
(pcase-let
((`(,system ,backend ,model ,temperature ,tokens)
(mapcar
(lambda (prop) (org-entry-get (or pt (point)) prop 'selective))
'("GPTEL_SYSTEM" "GPTEL_BACKEND" "GPTEL_MODEL"
"GPTEL_TEMPERATURE" "GPTEL_MAX_TOKENS"))))
(when system
(setq system (string-replace "\\n" "\n" system)))
(when backend
(setq backend (alist-get backend gptel--known-backends
nil nil #'equal)))
(when temperature
(setq temperature (gptel--numberize temperature)))
(when tokens (setq tokens (gptel--numberize tokens)))
(list system backend model temperature tokens)))
(defun gptel-org--restore-state ()
"Restore gptel state for Org buffers when turning on `gptel-mode'."
(save-restriction
(widen)
(condition-case status
(progn
(when-let ((bounds (org-entry-get (point-min) "GPTEL_BOUNDS")))
(mapc (pcase-lambda (`(,beg . ,end))
(put-text-property beg end 'gptel 'response))
(read bounds)))
(pcase-let ((`(,system ,backend ,model ,temperature ,tokens)
(gptel-org--entry-properties (point-min))))
(when system (setq-local gptel--system-message system))
(if backend (setq-local gptel-backend backend)
(message
(substitute-command-keys
(concat
"Could not activate gptel backend \"%s\"! "
"Switch backends with \\[universal-argument] \\[gptel-send]"
" before using gptel."))
backend))
(when model (setq-local gptel-model model))
(when temperature (setq-local gptel-temperature temperature))
(when tokens (setq-local gptel-max-tokens tokens))))
(:success (message "gptel chat restored."))
(error (message "Could not restore gptel state, sorry! Error: %s" status)))))
(defun gptel-org-set-properties (pt &optional msg)
"Store the active gptel configuration under the current heading.
The active gptel configuration includes the current system
message, language model and provider (backend), and additional
settings when applicable.
PT is the cursor position by default. If MSG is
non-nil (default), display a message afterwards."
(interactive (list (point) t))
(org-entry-put pt "GPTEL_MODEL" gptel-model)
(org-entry-put pt "GPTEL_BACKEND" (gptel-backend-name gptel-backend))
(unless (equal (default-value 'gptel-temperature) gptel-temperature)
(org-entry-put pt "GPTEL_TEMPERATURE"
(number-to-string gptel-temperature)))
(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)))
(when msg
(message "Added gptel configuration to current headline.")))
(defun gptel-org--save-state ()
"Write the gptel state to the Org buffer as Org properties."
(org-with-wide-buffer
(goto-char (point-min))
(when (org-at-heading-p)
(org-open-line 1))
(gptel-org-set-properties (point-min))
;; Save response boundaries
(letrec ((write-bounds
(lambda (attempts)
(let* ((bounds (gptel--get-buffer-bounds))
(offset (caar bounds))
(offset-marker (set-marker (make-marker) offset)))
(org-entry-put (point-min) "GPTEL_BOUNDS"
(prin1-to-string (gptel--get-buffer-bounds)))
(when (and (not (= (marker-position offset-marker) offset))
(> 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

@ -32,7 +32,63 @@
(declare-function ediff-regions-internal "ediff")
(declare-function ediff-make-cloned-buffer "ediff-utils")
;; * Helper functions
;; * Helper functions and vars
(defvar gptel--set-buffer-locally nil
"Set model parameters from `gptel-menu' buffer-locally.
Affects the system message too.")
(defun gptel--set-with-scope (sym value &optional scope)
"Set SYMBOL's symbol-value to VALUE with SCOPE.
If SCOPE is non-nil, set it buffer-locally, else clear any
buffer-local value and set its default global value."
(if scope
(set (make-local-variable sym) value)
(kill-local-variable sym)
(set sym value)))
(defun gptel--get-directive (args)
"Find the additional directive in the transient ARGS.
Meant to be called when `gptel-menu' is active."
(cl-some (lambda (s) (and (stringp s) (string-prefix-p ":" s)
(concat "\n\n" (substring s 1))))
args))
(defun gptel--instructions-make-overlay (text &optional ov)
"TODO"
(save-excursion
(cond
((use-region-p) (goto-char (region-beginning)))
((gptel--in-response-p) (gptel-beginning-of-response))
(t (text-property-search-backward 'gptel 'response)))
(skip-chars-forward "\n \t")
(if (and ov (overlayp ov))
(move-overlay ov (point) (point) (current-buffer))
(setq ov (make-overlay (point) (point) nil t)))
(overlay-put ov 'before-string nil)
;; (unless (or (bobp) (eq (char-before) "\n"))
;; (overlay-put ov 'before-string (propertize "\n" 'font-lock-face 'shadow)))
(overlay-put ov 'category 'gptel)
(overlay-put
ov 'after-string
(concat
(propertize (concat "GPTEL: " text)
'font-lock-face '(:inherit shadow :box t))
"\n"))
ov))
(defun gptel--transient-read-variable (prompt initial-input history)
"Read value from minibuffer and interpret the result as a Lisp object.
PROMPT, INITIAL-INPUT and HISTORY are as in the Transient reader
documention."
(ignore-errors
(read-from-minibuffer prompt initial-input read-expression-map t history)))
(defun gptel--refactor-or-rewrite ()
"Rewrite should be refactored into refactor.
@ -45,7 +101,9 @@ Or is it the other way around?"
"Set a generic refactor/rewrite message for the buffer."
(if (derived-mode-p 'prog-mode)
(format "You are a %s programmer. Refactor the following code. Generate only code, no explanation."
(substring (symbol-name major-mode) nil -5))
(thread-last (symbol-name major-mode)
(string-remove-suffix "-mode")
(string-remove-suffix "-ts")))
(format "You are a prose editor. Rewrite the following text to be more professional.")))
(defvar gptel--crowdsourced-prompts-url
@ -103,6 +161,101 @@ which see."
(forward-line 1)))))
gptel--crowdsourced-prompts))
;; * Transient classes and methods for gptel
(defclass gptel--switches (transient-lisp-variable)
((display-if-true :initarg :display-if-true :initform "for this buffer")
(display-if-false :initarg :display-if-false :initform "globally"))
"Boolean lisp variable class for gptel-transient.")
(cl-defmethod transient-infix-read ((obj gptel--switches))
"Cycle through the mutually exclusive switches."
(not (oref obj value)))
(cl-defmethod transient-format-value ((obj gptel--switches))
(with-slots (value display-if-true display-if-false) obj
(format
(propertize "(%s)" 'face 'transient-delimiter)
(concat
(propertize display-if-false
'face (if value 'transient-inactive-value 'transient-value))
(propertize "|" 'face 'transient-delimiter)
(propertize display-if-true
'face (if value 'transient-value 'transient-inactive-value))))))
(defclass gptel-lisp-variable (transient-lisp-variable)
((display-nil :initarg :display-nil))
"Lisp variables that show :display-nil instead of nil.")
(cl-defmethod transient-format-value
((obj gptel-lisp-variable))
(propertize (prin1-to-string (or (oref obj value)
(oref obj display-nil)))
'face 'transient-value))
(cl-defmethod transient-infix-set ((obj gptel-lisp-variable) value)
(funcall (oref obj set-value)
(oref obj variable)
(oset obj value value)
gptel--set-buffer-locally))
(defclass gptel-provider-variable (transient-lisp-variable)
((model :initarg :model)
(model-value :initarg :model-value)
(always-read :initform t)
(set-value :initarg :set-value :initform #'set))
"Class used for gptel-backends.")
(cl-defmethod transient-format-value ((obj gptel-provider-variable))
(propertize (concat (gptel-backend-name (oref obj value)) ":"
(buffer-local-value (oref obj model) transient--original-buffer))
'face 'transient-value))
(cl-defmethod transient-infix-set ((obj gptel-provider-variable) value)
(pcase-let ((`(,backend-value ,model-value) value))
(funcall (oref obj set-value)
(oref obj variable)
(oset obj value backend-value)
gptel--set-buffer-locally)
(funcall (oref obj set-value)
(oref obj model)
(oset obj model-value model-value)
gptel--set-buffer-locally)))
(defclass gptel-option-overlaid (transient-option)
((display-nil :initarg :display-nil)
(overlay :initarg :overlay))
"Transient options for overlays displayed in the working buffer.")
(cl-defmethod transient-format-value ((obj gptel-option-overlaid))
"set up the in-buffer overlay for additional directive, a string.
Also format its value in the Transient menu."
(let ((value (oref obj value))
(ov (oref obj overlay))
(argument (oref obj argument)))
;; Making an overlay
(if (or (not value) (string-empty-p value))
(when ov (delete-overlay ov))
(with-current-buffer transient--original-buffer
(oset obj overlay (gptel--instructions-make-overlay value ov)))
(letrec ((ov-clear-hook
(lambda () (when-let* ((ov (oref obj overlay))
((overlayp ov)))
(remove-hook 'transient-exit-hook
ov-clear-hook)
(delete-overlay ov)))))
(add-hook 'transient-exit-hook ov-clear-hook)))
;; Updating transient menu display
(if value
(propertize (concat argument (truncate-string-to-width value 25 nil nil "..."))
'face 'transient-value)
(propertize
(concat "(" (symbol-name (oref obj display-nil)) ")")
'face 'transient-inactive-value))))
;; * Transient Prefixes
(define-obsolete-function-alias 'gptel-send-menu 'gptel-menu "0.3.2")
@ -113,22 +266,29 @@ which see."
"Change parameters of prompt to send to the LLM."
;; :incompatible '(("-m" "-n" "-k" "-e"))
[:description
(lambda () (format "Directive: %s"
(truncate-string-to-width
gptel--system-message (max (- (window-width) 14) 20) nil nil t)))
("h" "Set directives for chat" gptel-system-prompt :transient t)]
[["Session Parameters"
(lambda ()
(string-replace
"\n" ""
(truncate-string-to-width
gptel--system-message (max (- (window-width) 12) 14) nil nil t)))
[""
"Instructions"
("s" "Set system message" gptel-system-prompt :transient t)
(gptel--infix-add-directive)]]
[["Model Parameters"
:pad-keys t
(gptel--infix-variable-scope)
(gptel--infix-provider)
;; (gptel--infix-model)
(gptel--infix-max-tokens)
(gptel--infix-num-messages-to-send)
(gptel--infix-temperature)]
["Prompt:"
("p" "From minibuffer instead" "p")
("y" "From kill-ring instead" "y")
("i" "Replace/Delete prompt" "i")
"Response to:"
(gptel--infix-temperature :if (lambda () gptel-expert-commands))]
["Prompt from"
("m" "Minibuffer instead" "m")
("y" "Kill-ring instead" "y")
""
("i" "Respond in place" "i")]
["Response to"
("e" "Echo area instead" "e")
("g" "gptel session" "g"
:class transient-option
:prompt "Existing or new gptel session: "
@ -148,7 +308,10 @@ which see."
:reader
(lambda (prompt _ _history)
(read-buffer prompt (buffer-name (other-buffer)) nil)))
("k" "Kill-ring" "k")]
("k" "Kill-ring" "k")]]
[["Send"
(gptel--suffix-send)
("M-RET" "Regenerate" gptel--regenerate :if gptel--in-response-p)]
[:description gptel--refactor-or-rewrite
:if use-region-p
("r"
@ -158,7 +321,34 @@ which see."
(lambda () (if (derived-mode-p 'prog-mode)
"Refactor" "Rewrite"))
gptel-rewrite-menu)]
["Send" (gptel--suffix-send)]]
["Tweak Response" :if gptel--in-response-p :pad-keys t
("SPC" "Mark" gptel--mark-response)
("P" "Previous variant" gptel--previous-variant
:if gptel--at-response-history-p
:transient t)
("N" "Next variant" gptel--previous-variant
:if gptel--at-response-history-p
:transient t)
("E" "Ediff previous" gptel--ediff
:if gptel--at-response-history-p)]
["Dry Run" :if (lambda () (or gptel-log-level gptel-expert-commands))
("I" "Inspect query (Lisp)"
(lambda ()
"Inspect the query that will be sent as a lisp object."
(interactive)
(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)
(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))
@ -169,22 +359,18 @@ which see."
(transient-parse-suffixes
'gptel-system-prompt
(cl-loop for (type . prompt) in gptel-directives
with taken
;; Avoid clashes with the custom directive key
with unused-keys = (delete ?s (number-sequence ?a ?z))
with width = (window-width)
for name = (symbol-name type)
for key =
(let ((idx 0) pos)
(while (or (not pos) (member pos taken))
(setq pos (substring name idx (1+ idx)))
(cl-incf idx))
(push pos taken)
pos)
for key = (seq-find (lambda (k) (member k unused-keys)) name (seq-first unused-keys))
do (setq unused-keys (delete key unused-keys))
;; The explicit declaration ":transient transient--do-return" here
;; appears to be required for Transient v0.5 and up. Without it, these
;; are treated as suffixes when invoking `gptel-system-prompt' directly,
;; and infixes when going through `gptel-menu'.
;; TODO: Raise an issue with Transient.
collect (list (key-description key)
collect (list (key-description (list key))
(concat (capitalize name) " "
(propertize " " 'display '(space :align-to 20))
(propertize
@ -196,13 +382,15 @@ which see."
")")
'face 'shadow))
`(lambda () (interactive)
(message "Directive: %s" ,prompt)
(setq gptel--system-message ,prompt))
(message "Directive: %s"
,(string-replace "\n" ""
(truncate-string-to-width prompt 100 nil nil t)))
(gptel--set-with-scope 'gptel--system-message ,prompt
gptel--set-buffer-locally))
:transient 'transient--do-return)
into prompt-suffixes
finally return
(nconc
(list (list 'gptel--suffix-system-message))
prompt-suffixes
(list (list "SPC" "Pick crowdsourced prompt"
'gptel--read-crowdsourced-prompt
@ -211,24 +399,31 @@ which see."
;; instead of returning to the system prompt menu.
:transient 'transient--do-exit))))))
;;;###autoload (autoload 'gptel-system-prompt "gptel-transient" nil t)
(transient-define-prefix gptel-system-prompt ()
"Change the LLM system prompt.
"Set the LLM system message for LLM interactions in this buffer.
The \"system\" prompt establishes directives for the chat
session. Some examples of system prompts are:
The \"system message\" establishes directives for the chat
session and modifies the behavior of the LLM. Some examples of
system prompts are:
You are a helpful assistant. Answer as concisely as possible.
Reply only with shell commands and no prose.
You are a poet. Reply only in verse.
More extensive system messages can be useful for specific tasks.
Customize `gptel-directives' for task-specific prompts."
[:description
(lambda () (format "Current directive: %s"
(truncate-string-to-width
gptel--system-message 100 nil nil t)))
:class transient-column
:setup-children gptel-system-prompt--setup
:pad-keys t])
(lambda () (string-replace
"\n" ""
(truncate-string-to-width
gptel--system-message (max (- (window-width) 12) 14) nil nil t)))
[(gptel--suffix-system-message)]
[(gptel--infix-variable-scope)]]
[:class transient-column
:setup-children gptel-system-prompt--setup
:pad-keys t])
;; ** Prefix for rewriting/refactoring
@ -254,17 +449,19 @@ Customize `gptel-directives' for task-specific prompts."
(setq gptel--rewrite-message (gptel--rewrite-message)))
(transient-setup 'gptel-rewrite-menu))
;; * Transient Infixes
;; ** Infixes for model parameters
(defun gptel--transient-read-variable (prompt initial-input history)
"Read value from minibuffer and interpret the result as a Lisp object.
PROMPT, INITIAL-INPUT and HISTORY are as in the Transient reader
documention."
(ignore-errors
(read-from-minibuffer prompt initial-input read-expression-map t history)))
(transient-define-infix gptel--infix-variable-scope ()
"Set gptel's model parameters and system message in this buffer or globally."
:argument "scope"
:variable 'gptel--set-buffer-locally
:class 'gptel--switches
:format " %k %d %v"
:key "="
:description (propertize "Set" 'face 'transient-inactive-argument))
(transient-define-infix gptel--infix-num-messages-to-send ()
"Number of recent messages to send with each exchange.
@ -273,9 +470,12 @@ By default, the full conversation history is sent with every new
prompt. This retains the full context of the conversation, but
can be expensive in token size. Set how many recent messages to
include."
:description "Number of past messages to send"
:class 'transient-lisp-variable
:description "previous responses"
:class 'gptel-lisp-variable
:variable 'gptel--num-messages-to-send
:set-value #'gptel--set-with-scope
:display-nil 'all
:format " %k %v %d"
:key "-n"
:prompt "Number of past messages to include for context (leave empty for all): "
:reader 'gptel--transient-read-variable)
@ -287,80 +487,74 @@ This is roughly the number of words in the response. 100-300 is a
reasonable range for short answers, 400 or more for longer
responses."
:description "Response length (tokens)"
:class 'transient-lisp-variable
:class 'gptel-lisp-variable
:variable 'gptel-max-tokens
:set-value #'gptel--set-with-scope
:display-nil 'auto
:key "-c"
:prompt "Response length in tokens (leave empty: default, 80-200: short, 200-500: long): "
:reader 'gptel--transient-read-variable)
(defclass gptel-provider-variable (transient-lisp-variable)
((model :initarg :model)
(model-value :initarg :model-value)
(always-read :initform t)
(set-value :initarg :set-value :initform #'set))
"Class used for gptel-backends.")
(cl-defmethod transient-format-value ((obj gptel-provider-variable))
(propertize (concat (gptel-backend-name (oref obj value)) ":"
(buffer-local-value (oref obj model) transient--original-buffer))
'face 'transient-value))
(cl-defmethod transient-infix-set ((obj gptel-provider-variable) value)
(pcase-let ((`(,backend-value ,model-value) value))
(funcall (oref obj set-value)
(oref obj variable)
(oset obj value backend-value))
(funcall (oref obj set-value)
(oref obj model)
(oset obj model-value model-value))))
(transient-define-infix gptel--infix-provider ()
"AI Provider for Chat."
:description "GPT Model: "
:description "GPT Model"
:class 'gptel-provider-variable
:prompt "Model provider: "
:prompt "Model: "
:variable 'gptel-backend
:set-value #'gptel--set-with-scope
:model 'gptel-model
:key "-m"
:reader (lambda (prompt &rest _)
(let* ((backend-name
(if (<= (length gptel--known-backends) 1)
(caar gptel--known-backends)
(completing-read
prompt
(mapcar #'car gptel--known-backends))))
(backend (alist-get backend-name gptel--known-backends
nil nil #'equal))
(backend-models (gptel-backend-models backend))
(model-name (if (= (length backend-models) 1)
(car backend-models)
(completing-read
"Model: " backend-models))))
(list backend model-name))))
(transient-define-infix gptel--infix-model ()
"AI Model for Chat."
:description "GPT Model: "
:class 'transient-lisp-variable
:variable 'gptel-model
:key "-m"
:choices '("gpt-3.5-turbo" "gpt-3.5-turbo-16k" "gpt-4" "gpt-4-1106-preview")
:reader (lambda (prompt &rest _)
(completing-read
prompt
'("gpt-3.5-turbo" "gpt-3.5-turbo-16k" "gpt-4" "gpt-4-1106-preview"))))
(cl-loop
for (name . backend) in gptel--known-backends
nconc (cl-loop for model in (gptel-backend-models backend)
collect (list (concat name ":" model) backend model))
into models-alist finally return
(cdr (assoc (completing-read prompt models-alist nil t)
models-alist)))))
(transient-define-infix gptel--infix-temperature ()
"Temperature of request."
:description "Randomness (0 - 2.0)"
:description "Temperature (0 - 2.0)"
:class 'transient-lisp-variable
:variable 'gptel-temperature
:set-value #'gptel--set-with-scope
:key "-t"
:prompt "Set temperature (0.0-2.0, leave empty for default): "
:prompt "Temperature controls the response randomness (0.0-2.0, leave empty for default): "
:reader 'gptel--transient-read-variable)
;; ** Infix for the refactor/rewrite system message
(transient-define-infix gptel--infix-add-directive ()
"Additional directive intended for the next query only.
This is useful to define a quick task on top of a more extensive
or detailed system message.
For example, with code/text selected:
- Rewrite this function to do X while avoiding Y.
- Change the tone of the following paragraph to be more direct.
Or in an extended conversation:
- Phrase you next response in ten words or less.
- Pretend for now that you're an anthropologist."
:class 'gptel-option-overlaid
;; :variable 'gptel--instructions
:display-nil 'none
:overlay nil
:argument ":"
:prompt "Instructions for next response only: "
:reader (lambda (prompt initial history)
(let* ((extra (read-string prompt initial history)))
(unless (string-empty-p extra) extra)))
:format " %k %d %v"
:key "d"
:argument ":"
:description "Add directive"
:transient t)
(transient-define-infix gptel--infix-rewrite-prompt ()
"Chat directive (system message) to use for rewriting or refactoring."
:description (lambda () (if (derived-mode-p 'prog-mode)
@ -375,6 +569,7 @@ responses."
(read-string
prompt (gptel--rewrite-message) history)))
;; * Transient Suffixes
;; ** Suffix to send prompt
@ -383,7 +578,8 @@ responses."
"Send ARGS."
:key "RET"
:description "Send prompt"
(interactive (list (transient-args transient-current-command)))
(interactive (list (transient-args
(or transient-current-command 'gptel-menu))))
(let ((stream gptel-stream)
(in-place (and (member "i" args) t))
(output-to-other-buffer-p)
@ -392,16 +588,17 @@ responses."
(backend-name (gptel-backend-name gptel-backend))
(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
((member "p" args)
((member "m" args)
(read-string
(format "Ask %s: " (gptel-backend-name gptel-backend))
(apply #'buffer-substring-no-properties
(if (use-region-p)
(list (region-beginning) (region-end))
(list (line-beginning-position) (line-end-position))))))
(and (use-region-p)
(buffer-substring-no-properties
(region-beginning) (region-end)))))
((member "y" args)
(unless (car-safe kill-ring)
(user-error "`kill-ring' is empty! Nothing to send"))
@ -411,7 +608,7 @@ responses."
;; Output redirection: Send response elsewhere?
(cond
((member "m" args)
((member "e" args)
(setq stream nil)
(setq callback
(lambda (resp info)
@ -429,7 +626,7 @@ responses."
backend-name
(truncate-string-to-width resp 30))))))
((setq gptel-buffer-name
(cl-some (lambda (s) (and (string-prefix-p "g" s)
(cl-some (lambda (s) (and (stringp s) (string-prefix-p "g" s)
(substring s 1)))
args))
(setq output-to-other-buffer-p t)
@ -481,46 +678,80 @@ responses."
(gptel--update-status " Waiting..." 'warning)
(setq position (point)))))))
((setq gptel-buffer-name
(cl-some (lambda (s) (and (string-prefix-p "b" s)
(cl-some (lambda (s) (and (stringp s) (string-prefix-p "b" s)
(substring s 1)))
args))
(setq output-to-other-buffer-p t)
(setq buffer (get-buffer-create gptel-buffer-name))
(with-current-buffer buffer (setq position (point)))))
;; Create prompt, unless doing input-redirection above
(unless prompt
(setq prompt (gptel--create-prompt (gptel--at-word-end (point)))))
(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)
(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))))
(kill-region beg end)))
(gptel--update-status " Waiting..." 'warning)
(gptel-request
prompt
:buffer (or buffer (current-buffer))
:position position
:in-place (and in-place (not output-to-other-buffer-p))
:stream stream
:callback callback)
(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
(defun gptel--regenerate ()
"Regenerate gptel response at point."
(interactive)
(when (gptel--in-response-p)
(pcase-let* ((`(,beg . ,end) (gptel--get-bounds))
(history (get-char-property (point) 'gptel-history))
(prev-responses (cons (buffer-substring-no-properties beg end)
history)))
(when gptel-mode ;Remove prefix/suffix
(save-excursion
(goto-char beg)
(when (looking-back (concat "\n+" (regexp-quote (gptel-response-prefix-string)))
(point-min) 'greedy)
(setq beg (match-beginning 0)))
(goto-char end)
(when (looking-at
(concat "\n+" (regexp-quote (gptel-prompt-prefix-string))))
(setq end (match-end 0)))))
(delete-region beg end)
(gptel--attach-response-history prev-responses)
(call-interactively #'gptel--suffix-send))))
;; ** Set system message
(defun gptel--read-crowdsourced-prompt ()
@ -553,10 +784,13 @@ This uses the prompts in the variable
(message "No prompts available.")))
(transient-define-suffix gptel--suffix-system-message ()
"Edit LLM directives."
"Edit LLM system message.
When LOCAL is non-nil, set the system message only in the current buffer."
:transient 'transient--do-exit
:description "Set custom directives"
:key "h"
:description "Set or edit system message"
:format " %k %d"
:key "s"
(interactive)
(let ((orig-buf (current-buffer))
(msg-start (make-marker)))
@ -607,7 +841,8 @@ This uses the prompts in the variable
(let ((system-message
(buffer-substring msg-start (point-max))))
(with-current-buffer orig-buf
(setq gptel--system-message system-message)))
(gptel--set-with-scope 'gptel--system-message system-message
gptel--set-buffer-locally)))
(funcall quit-to-menu)))
(local-set-key (kbd "C-c C-k") quit-to-menu)))))

851
gptel.el

File diff suppressed because it is too large Load diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 230 KiB

133
test/gptel-org-test.el Normal file
View file

@ -0,0 +1,133 @@
;; -*- lexical-binding: t; -*-
(require 'ert)
(require 'gptel)
(require 'cl-generic)
(declare-function json-read "json" ())
(defvar json-object-type)
;;; Methods for collecting data from HTTP logs
(cl-defgeneric gptel-test--read-response (backend &optional from to))
(cl-defmethod gptel-test--read-response ((_backend gptel-openai) &optional from to)
(setq from (or from (point-min))
to (or to (point-max)))
(save-restriction
(narrow-to-region from to)
(goto-char from)
(let ((strs))
(while (re-search-forward "^data: *" nil t)
;; (forward-char)
(condition-case-unless-debug err
(thread-first
(gptel--json-read)
(map-nested-elt '(:choices 0 :delta :content))
(push strs))
(error strs)
(:success strs)))
(setq strs (delq nil (nreverse strs))))))
(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" "()" " {
" " " " printf" "(\"" "``" "``" "`" "\n" "\");
" " " " return" " " "0" ";
" "}
" "```")
"#+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" ">
" "int" " main" "()" " {
" " " " //" " Using" " printf" " to" " print" " " "5" " back" "ticks" "
" " " " printf" "(\"" "``" "``" "`" "\n" "\");
" " " " return" " " "0" ";
" "}
" "``" "`
" "In" " this" " code" " snippet" "," " `" "printf" "(\"" "``" "``" "`" "\n" "\");" "`" " is" " used" " to" " print" " the" " string" " \"" "``" "```" "\"" " followed" " by" " a" " newline" " character" ".")
;; Org
"Here is a simple C++ program that uses the =printf= function to print the string containing 5 backticks:
#+begin_src cpp
#include <iostream>
int main() {
// Using printf to print 5 backticks
printf(\"`````\n\");
return 0;
}
#+end_src
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.")
(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.")