gptel icon indicating copy to clipboard operation
gptel copied to clipboard

gptel agents or configuration presets

Open karthink opened this issue 11 months ago • 6 comments

A suggestion when you do get to this, I find the system message one prefers often depends on the model that is selected. I find when having a dialogue, in which I switch models mid-chat, I also sometimes need to switch the system prompt to better match the new model.

This appears to be a need that many users have. There are many discussions threads where people have asked how to implement a "presets" feature. I think it might make sense to add a presets feature to the package which is a bundle of configuration you can switch to all at once.

(gptel-make-preset
 "coding-preset"
 :system "system message for coding here..."    ; can be a function, see gptel-directives
 :backend gptel--anthropic-backend              ; or name of backend, like "Claude"
 :model   'claude-3-sonnet-20240229
 :context 'gptel-context-lsp
 :tools  (list gptel-tool-1 gptel-tool-2 ...)   ; list of gptel tools to supply
 :callback nil)

You can then select a preset from the transient menu, which sets all of these options at once. Via the "scope" switch in the menu, this preset can be set globally, in one buffer or just for the next request.

Some of these options/sources (like gptel-context-lsp) don't exist yet. :callback is basically a custom action you can specify instead of inserting the response, with nil being the default callback gptel uses.

In other LLM clients this is called an "agent", but really they're just a bundle of prompts+configuration.

Originally posted by @karthink in https://github.com/karthink/gptel/issues/416#issuecomment-2567232641

karthink avatar Jan 02 '25 02:01 karthink

Gptel presets would be very useful!

I created a per-project side panel chat workflow inspired by Cursor, and changing the context for each project is not ideal. I would much appreciate a list of presets I could set (maybe with .dir-locals) for each project.

References:

Functionality

(use-package ai-project-agent
  :after (gptel flycheck)
  :bind (("C-c c a" . ai-project-agent-toggle-panel)
         ("C-c c d" . ai-project-agent-clear-panel)
         ("C-c c c" . gptel-add)
         ("C-c c l" . ai-project-agent-send-lint-feedback)
         ("C-c c RET" . ai-project-agent-send)))

ovistoica avatar Jan 18 '25 05:01 ovistoica

I agree that this would be great to have! I envision being able to use it to easily create an agent-based workflow, so it would be nice if a preset could simply be passed to gptel-request:

(gptel-request
 "prompt"
 :preset "some-preset-name")

Since a lot of gptel settings can be set via dynamic variables, a possible stop-gap implementation right now could be to define a preset as an alist of variable bindings (e.g. gptel--system-message, gptel-backend, gptel-model, etc.) and then bind them during the call to gptel-request (e.g. with cl-progv). Not sure if it would work in practice, just an idea!

skissue avatar Jan 18 '25 23:01 skissue

This is based on our discussion: :fsm could be another parameter for an agent.

ahmed-shariff avatar Jan 26 '25 05:01 ahmed-shariff

Maybe a bit offtopic, but is there a way to enable tools provided by API itself? Gemini provides grounding - https://ai.google.dev/gemini-api/docs/grounding as I understand it, this doesn't require any extra support on the client side in terms of processing responses. I looked around the source code but couldn't quite figure out where to put tool configuration for Gemini.

keelah-mt avatar Mar 03 '25 10:03 keelah-mt

You can then select a preset from the transient menu, which sets all of these options at once. Via the "scope" switch in the menu, this preset can be set globally, in one buffer or just for the next request.

I am throwing ideas out there. Would it make more sense to do it the other way around? i.e., all variables are read from a current-preset. Changing a value in the transient menu changes the value of the given variable in the current-preset (globally or locally based on the scope). We can have additional options for the presets where the user can reset the variables to the default values of the preset, and also options to save/create new presets from the transient-menu. There can also be a with-gptel-preset wrapper for gptel-equest with options to override some of the variables.

ahmed-shariff avatar Mar 07 '25 00:03 ahmed-shariff

One other thing I would suggest for this feature is to add inheritance: A :parent property that adds in all of the settings from another "agent", so that I can derive specific agents from more general ones.

jwiegley avatar Apr 13 '25 06:04 jwiegley

I've added initial support for presets on the feature-presets branch. You can select a preset from the transient menu after defining it.

Image

You can use the = Scope option to apply the preset globally, buffer-locally or for the next request only.

To define a preset, use gptel-make-preset, which see. Here are a couple of examples:

(gptel-make-preset 'gpt4coding
  :description "A preset optimized for coding tasks"
  :backend "ChatGPT"                    ;backend or backend name
  :model 'gpt-4.1
  :system "You are an expert coding assistant. Your role is to provide high-quality code solutions, refactorings, and explanations."
  :tools '("read_buffer" "modify_buffer")) ;gptel tools or tool names
(gptel-make-preset "proofreading"         ;name is a symbol or string, symbol preferred
  :description "Preset for proofreading tasks"
  :backend "Claude"
  :model 'claude-3-7-sonnet-20250219
  :tools '("read_buffer" "spell_check" "grammar_check")
  :temperature 0.7
  :use-context 'system)

All keyword arguments to gptel-make-preset are optional.

There are a couple of special keys used by this function: :description (for your reference only) and :parents (for inheriting settings from other presets). Apart from these, there is no predefined set of recognized keys. Any key of the form :foo corresponds to setting gptel-foo (preferred) or gptel--foo. So you can set any gptel option in a preset, including internal ones. Unrecognized keys are ignored with a warning.

Switching to a preset simply applies the settings specified, and does not touch or reset other settings. So if

  1. applying preset1 sets gptel-foo to t
  2. and preset2 doesn't include a setting for gptel-foo,
  3. gptel-foo will stay as t when you switch to preset2. This is a consequence of the open-ended specification format.

Please let me know if you have suggestions. (feature-presets branch)

karthink avatar May 07 '25 07:05 karthink

I agree that this would be great to have! I envision being able to use it to easily create an agent-based workflow, so it would be nice if a preset could simply be passed to gptel-request:

(gptel-request "prompt" :preset "some-preset-name")

Since a lot of gptel settings can be set via dynamic variables, a possible stop-gap implementation right now could be to define a preset as an alist of variable bindings (e.g. gptel--system-message, gptel-backend, gptel-model, etc.) and then bind them during the call to gptel-request (e.g. with cl-progv). Not sure if it would work in practice, just an idea!

@skissue Yeah, we need a simple way of invoking gptel-request with a preset. I like @ahmed-shariff's idea of a with-gptel-preset macro better than supplying a :preset arg, since gptel-request's code is already pretty involved (and getting more complicated every month). cl-progv would be the way to do it, yeah.

You can then select a preset from the transient menu, which sets all of these options at once. Via the "scope" switch in the menu, this preset can be set globally, in one buffer or just for the next request.

I am throwing ideas out there. Would it make more sense to do it the other way around? i.e., all variables are read from a current-preset. Changing a value in the transient menu changes the value of the given variable in the current-preset (globally or locally based on the scope).

I don't think it makes sense to have a preset at the "bottom".

EDIT: If you want to simulate it, you could place a comprehensive (gptel-make-preset 'default ...) in your gptel configuration and apply it. Then applying the default preset will reset gptel (but only sort of, see below).

We can have additional options for the presets where the user can reset the variables to the default values of the preset

Under the current implementation, the user can simply apply the preset again to "reset" the defaults. However if a preset does not specify a value for :foo/gptel-foo, then it will remain at its current, possibly changed value.

, and also options to save/create new presets from the transient-menu.

I thought about this, but save them to where? The only place I can write it to from the transient menu is to the user's custom-variables block/file, and Emacs users generally don't like that. We do a a limited version of this when saving a chat buffer to disk right now, where we save some options to the buffer itself.

There can also be a with-gptel-preset wrapper for gptel-equest with options to override some of the variables.

This is a neat idea, and simple to implement too -- however because gptel-request is asynchronous, it won't work for settings that come into play in the response part of the request cycle, such as gptel-include-tool-results. It's also not apparent to the user which settings will be respected and which won't. The ones that specify the LLM choice and LLM behavior (like the backend, model, system message etc) will be respected, while the ones that specify how gptel handles the response, like gptel-include-reasoning, may not be.

One other thing I would suggest for this feature is to add inheritance: A :parent property that adds in all of the settings from another "agent", so that I can derive specific agents from more general ones.

@jwiegley :parent is respected by gptel-make-preset. But perhaps it should be :parents with more than one parent allowed?

karthink avatar May 07 '25 07:05 karthink

Maybe a bit offtopic, but is there a way to enable tools provided by API itself? Gemini provides grounding - https://ai.google.dev/gemini-api/docs/grounding as I understand it, this doesn't require any extra support on the client side in terms of processing responses. I looked around the source code but couldn't quite figure out where to put tool configuration for Gemini.

This is off-topic for this thread. See #810, #750.

karthink avatar May 07 '25 08:05 karthink

@jwiegley :parent is respected by gptel-make-preset. But perhaps it should be :parents with more than one parent allowed?

:parents makes a lot of sense to me, I suppose with the later member of that list taking priority over settings from the earlier members?

jwiegley avatar May 07 '25 19:05 jwiegley

:parents` makes a lot of sense to me, I suppose with the later member of that list taking priority over settings from the earlier members?

Changed :parent to :parents, with this priority.

karthink avatar May 07 '25 20:05 karthink

This is pretty cool. I'll post any issues I come across with this here. The first thing I noted was the "@" missing on the "Request Parameters" section:

Image

But, "@" triggers the infix. Seems, the value is hidden in transient-format-value when gptel--known-presets is not set. Perhaps this should be moved to an :if predicate in the gptel-menu prefix?

but save them to where?

Instead of saving, perhaps generate a snippet in a temporary buffer or add it to the kill-ring?

ahmed-shariff avatar May 07 '25 22:05 ahmed-shariff

There were a few other issues that cropped up:

  • The :set-value doesn't work correctly - the value passed is a string, but the keys on gptel--known-presets are all symbols. I wrapped the name in intern-soft to have that work:
@@ -900,7 +900,7 @@ can be applied globally, buffer-locally or for the next request only."
   :set-value #'(lambda (name)
                  (when name
                   (gptel--apply-preset
-                   (assoc name gptel--known-presets)
+                   (assoc (intern-soft name) gptel--known-presets 'equal)
                    (lambda (sym val) (gptel--set-with-scope
                                  sym val gptel--set-buffer-locally)))
                   (message "Applied gptel preset %s"
  • While the above patch sets the variables (e.g., model and backend), it does not update the header of the group:

https://github.com/user-attachments/assets/ae17daba-ff4a-4d18-abe1-8f66e80d04c0

I tried to trace what's going on, for some reason, the transient-format-value gets a different object than the transient-infix-set of gptel--preset. Not entirely sure whats causing that.

  • Also, how could this be extended to set the fsm of a preset? This is coming from our discussion in https://github.com/karthink/gptel/discussions/539#discussioncomment-12194564. Could be another key in gptel-make-preset, but then it needs to be communicated to gptel-reqest. The following could be done now?

Oh, I think I found a way. Are you okay with using a buffer-local variable to store the fsm/transition table? I think I can pass it through to gptel--suffix-send. - (https://github.com/karthink/gptel/discussions/539#discussioncomment-12194206)

  • Should there be a way to go back to default values after setting the preset? i.e., revert applying the preset?

I am throwing ideas out there. Would it make more sense to do it the other way around? i.e., all variables are read from a current-preset. Changing a value in the transient menu changes the value of the given variable in the current-preset (globally or locally based on the scope).

I don't think it makes sense to have a preset at the "bottom".

This was my naive solution to this 😅 Applying a preset can perhaps generate a default preset that the user can use to reset/undo applying a preconfigured preset?

ahmed-shariff avatar May 07 '25 23:05 ahmed-shariff

Perhaps this should be moved to an :if predicate in the gptel-menu prefix?

That was my initial idea, but unfortunately it doesn't work. Since the header (Request Parameters) is actually part of this infix, this infix must always be displayed. Alternatively, you can add a second text header (Request Parameters) that is displayed only when this infix isn't, but I can't get that header to align properly.

but save them to where?

Instead of saving, perhaps generate a snippet in a temporary buffer or add it to the kill-ring?

Adding it to the kill-ring is simple but seems invasive, unless it is clearly signaled beforehand.

But the other main issue with saving a preset is deciding which parameters to include. I'll probably have to map across all gptel user options and check if the current value differs from the default. This is tricky by itself, but what if the user expects the default value (of gptel-use-context, say) to be explicitly saved as well? Saving every gptel option will generate a very large list.

Example of a preset with all gptel options
(cl-loop for sym being the symbols
         if (and (string-prefix-p "gptel-" (symbol-name sym))
                 (get sym 'standard-value))
         append (list (intern (concat ":" (substring (symbol-name sym) 6)))
                      (eval sym)))
(:pre-response-hook (my/gptel-easy-page)
 :crowdsourced-prompts-file "/home/karthik/.cache/gptel-crowdsourced-prompts.csv"
 :curl-extra-args nil
 :model gpt-4.1-mini
 :use-context nil
 :include-reasoning t
 :use-header-line t
 :curl-file-size-threshold 130000
 :org-convert-response t
 :include-tool-results t
 :track-response t
 :cache nil
 :proxy ""
 :log-level nil
 :augment-post-modify-hook nil
 :gh-github-token-file
 "/home/karthik/.emacs.d/.cache/copilot-chat/github-token"
 :tools ("modify_buffer" "read_buffer" "open_file_or_dir" "append_to_buffer" "read_url")
 :use-tools t
 :default-mode org-mode
 :rewrite-directives-hook (gptel-rewrite-commit-message)
 :post-rewrite-functions nil
 :prompt-prefix-alist ((markdown-mode . "#### ") (org-mode . "*Prompt*: ") (text-mode . "### "))
 :mode-hook nil
 :org-ignore-elements (property-drawer)
 :save-state-hook nil
 :post-request-hook nil
 :augment-handler-functions (my/apply-gptel-preset)
 :post-stream-hook nil
 :track-media nil
 :rewrite-default-action nil
 :stream t
 :gh-token-file "/home/karthik/.emacs.d/.cache/copilot-chat/token"
 :api-key gptel-api-key-from-auth-source
 :ask-display-buffer-action ((display-buffer-reuse-window display-buffer-in-side-window) (side . right)
                             (slot . 10) (window-width . 0.25)
                             (window-parameters (no-delete-other-windows . t)) (bump-use-time . t))
 :confirm-tool-calls auto
 :max-tokens nil
 :prompt-filter-hook nil
 :augment-pre-modify-hook nil
 :org-branching-context t
 :response-prefix-alist ((markdown-mode . "") (org-mode . "*Response*:\n") (text-mode . ""))
 :use-curl t
 :backend "ChatGPT"
 :temperature 1.0
 :display-buffer-action (pop-to-buffer-same-window)
 :context-wrap-function gptel-context--wrap-default
 :post-response-functions nil           ; OMITTED SOME GARBAGE HERE
 :response-separator "\n\n"
 :directives nil)                       ; OMITTED A LONG ALIST HERE 

karthink avatar May 08 '25 00:05 karthink

There were a few other issues that cropped up:

  • The :set-value doesn't work correctly - the value passed is a string, but the keys on gptel--known-presets are all symbols. I wrapped the name in intern-soft to have that work:
@@ -900,7 +900,7 @@ can be applied globally, buffer-locally or for the next request only."
   :set-value #'(lambda (name)
                  (when name
                   (gptel--apply-preset
-                   (assoc name gptel--known-presets)
+                   (assoc (intern-soft name) gptel--known-presets 'equal)
                    (lambda (sym val) (gptel--set-with-scope
                                  sym val gptel--set-buffer-locally)))
                   (message "Applied gptel preset %s"

Thanks, I'll fix this.

  • While the above patch sets the variables (e.g., model and backend), it does not update the header of the group:

I tried to trace what's going on, for some reason, the transient-format-value gets a different object than the transient-infix-set of gptel--preset. Not entirely sure whats causing that.

This is because I'm calling (transient-setup) in the transient-infix-set of gptel--preset. But before I can address this, there's a more fundamental, conceptual problem with presets that needs to be tackled.

  1. Suppose you apply a preset foo, and the header in the menu displays @foo.
  2. Then you change one setting, like the model, manually.
  3. Is the preset foo still active? Should it still show up in the menu header as @foo?

If it says @foo in the header, you might be led to believe that all the settings from foo are currently in effect, which is not true. This extends to settings that don't even have an entry in the transient menu, like gptel-stream.

On the other hand, if 1/10 preset settings (like the model) is different but the other 9 are the same, does it imply that foo is not active any more and shouldn't be displayed?

If I never display a prefix in the menu at all, the prefix is only meaningful when it is applied -- as a process, not state, and there is no chance of confusion. That was my thinking.

  • Also, how could this be extended to set the fsm of a preset? This is coming from our discussion in https://github.com/karthink/gptel/discussions/539#discussioncomment-12194564. Could be another key in gptel-make-preset, but then it needs to be communicated to gptel-reqest. The following could be done now?

Oh, I think I found a way. Are you okay with using a buffer-local variable to store the fsm/transition table? I think I can pass it through to gptel--suffix-send. - (https://github.com/karthink/gptel/discussions/539#discussioncomment-12194206)

Hmm, unfortunately I still don't see a clean way to do this. Presets only influence the environment, and not arguments of functions. Manipulating the environment is actually one advantage of the let-binding model, and why I didn't make :model and :backend arguments for gptel-request.

So the only way would be to make the fsm part of the environment:

  1. (defvar gptel--handlers nil "Default handlers for gptel-request fsm"), with another one for transitions.
  2. Set gptel--fsm using a preset's :fsm value.
  3. When running gptel-request, try using (in order) (i) :fsm provided as argument, (ii) fsm created from gptel--handlers etc and (iii) fsm created from gptel-request--handlers etc.

This still doesn't work for gptel-send, because gptel-send uses gptel-send--handlers, which are different from the ones in step 3. I haven't thought about our discussion in a while, I need to take another look with fresh eyes now.

I don't think it makes sense to have a preset at the "bottom".

This was my naive solution to this 😅 Applying a preset can perhaps generate a default preset that the user can use to reset/undo applying a preconfigured preset?

This could work if I could use gptel's default settings, where (for instance) ChatGPT/gpt-4.1-mini is the default backend/model. But users would want to reset it to their default settings, not gptel's.

karthink avatar May 08 '25 01:05 karthink

But the other main issue with saving a preset is deciding which parameters to include. I'll probably have to map across all gptel user options and check if the current value differs from the default. This is tricky by itself, but what if the user expects the default value (of gptel-use-context, say) to be explicitly saved as well? Saving every gptel option will generate a very large list.

Example of a preset with all gptel options

That's a lot of options :D While that is a long list, wouldn't it still be worth exposing that information? My thought process comes from the following: "I want to be able to reuse the current configuration I have, let me put this into a preset". Given that, going in and removing the options I am not interested in would be easier than trying to figure out which variables are influencing the output that I want to add to a preset. For now, it can be a simple temp buffer that spits out the same output you have. If users find it useful, it can be further refined like gptel--inspect-fsm?

This is because I'm calling (transient-setup) in the transient-infix-set of gptel--preset. But before I can address this, there's a more fundamental, conceptual problem with presets that needs to be tackled.

Suppose you apply a preset foo, and the header in the menu displays @foo. Then you change one setting, like the model, manually. Is the preset foo still active? Should it still show up in the menu header as @foo? If it says @foo in the header, you might be led to believe that all the settings from foo are currently in effect, which is not true. This > extends to settings that don't even have an entry in the transient menu, like gptel-stream.

On the other hand, if 1/10 preset settings (like the model) are different but the other 9 are the same, does it imply that foo is not active anymore and shouldn't be displayed?

I see. A simpler solution is to have this as an infix that just sets the value instead of showing which preset is active - i.e., "apply the values from this preset". I think this would be a simpler mental model as well. This also would resolve the "default" option, or eliminate the need for it from gptel's point of view. If there is a default set of values the users want to always fall back to, they just define a default preset that they can apply.

Manipulating the environment is actually one advantage of the let-binding model

It took a while to understand this, and I've come to appreciate this a lot :)

So the only way would be to make the fsm part of the environment:

  1. (defvar gptel--handlers nil "Default handlers for gptel-request fsm"), with another one for transitions.
  2. Set gptel--fsm using a preset's :fsm value.
  3. When running gptel-request, try using (in order) (i) :fsm provided as argument, (ii) fsm created from gptel--handlers etc and (iii) fsm created from gptel-request--handlers etc.

This still doesn't work for gptel-send, because gptel-send uses gptel-send--handlers, which are different from the ones in step 3. I haven't thought about our discussion in a while, I need to take another look with fresh eyes now.

While this is probably not an ideal way to think of this, gptel-*--handlers and gptel-request--transition are internal representations? If that is a comfortable assumption to make, they can be made constants that are used in the event that a gptel-fsm is nil. This can maybe be displayed as an infix option/toggle in the menu, where it's "default" if the value is nil and "custom" if not.

ahmed-shariff avatar May 08 '25 02:05 ahmed-shariff

That's a lot of options :D While that is a long list, wouldn't it still be worth exposing that information?

My thought process comes from the following: "I want to be able to reuse the current configuration I have, let me put this into a preset". Given that, going in and removing the options I am not interested in would be easier than trying to figure out which variables are influencing the output that I want to add to a preset. For now, it can be a simple temp buffer that spits out the same output you have. If users find it useful, it can be further refined like gptel--inspect-fsm?

Good point. I can filter out a few of these variables that it doesn't make sense to set in a preset (like gptel-directives). As well as the hooks, since they can have byte-compiled functions that can't be serialized reliably.

On the other hand, if 1/10 preset settings (like the model) are different but the other 9 are the same, does it imply that foo is not active anymore and shouldn't be displayed?

I see. A simpler solution is to have this as an infix that just sets the value instead of showing which preset is active - i.e., "apply the values from this preset". I think this would be a simpler mental model as well. This also would resolve the "default" option, or eliminate the need for it from gptel's point of view. If there is a default set of values the users want to always fall back to, they just define a default preset that they can apply.

Isn't that exactly how it currently works? Perhaps only the infix display needs changing, since it currently looks like it should be reporting the "active" preset at all times.

So the only way would be to make the fsm part of the environment:

  1. (defvar gptel--handlers nil "Default handlers for gptel-request fsm"), with another one for transitions. 2. Set gptel--fsm using a preset's :fsm value. 3. When running gptel-request, try using (in order) (i) :fsm provided as argument, (ii) fsm created from gptel--handlers etc and (iii) fsm created from gptel-request--handlers etc.

This still doesn't work for gptel-send, because gptel-send uses gptel-send--handlers, which are different from the ones in step 3. I haven't thought about our discussion in a while, I need to take another look with fresh eyes now.

While this is probably not an ideal way to think of this, gptel-*--handlers and gptel-request--transition are internal representations? If that is a comfortable assumption to make, they can be made constants that are used in the event that a gptel-fsm is nil. This can maybe be displayed as an infix option/toggle in the menu, where it's "default" if the value is nil and "custom" if not.

I don't follow -- but with some more thought I think the problem is not really about getting the fsm from the environment into gptel-request. I think it's changing the fsm when the backend changes, a problem that's independent of how the fsm options are displayed:

Currently you can add :request--transitions and :request--handlers (or :send--handlers) to a preset to set gptel-request--transitions etc buffer-locally. But from discussion #539, setting them buffer-locally creates an issue when you change backends in the same buffer:

Assuming the openai assistant session is tied to a buffer, an easy way to achieve this for the assistant is to just set gptel-request--transitions and gptel-request--handlers buffer-locally. Then both gptel-send and gptel--suffix-send should work seamlessly with your custom handlers without having to worry about breaking gptel-send in non-assistant buffers.

gptel already sets the backend, model and request parameters buffer-locally anyway.

True. But that also means, that if I switch to a different backend in the same session, those variables would also need to change?

This problem still persists with presets. A preset can set the handlers and transitions required for the assistants API (buffer-locally) -- so far so good.

But if you

  1. change the backend to a regular (chat-completions) API the handlers/transitions are now wrong.
  2. change to a different preset the handlers/transitions are still wrong since most presets won't explicitly set the handlers/transitions.

Re: #539, I still think the cleanest way to do it is to provide a separate gptel-assistant-send command, and find some way to integrate it with gptel-menu. It's not clear yet how to do that.

karthink avatar May 08 '25 03:05 karthink

Isn't that exactly how it currently works?

That is correct, though the implementation itself seemed like it was expected to display the active preset.

This problem still persists with presets. A preset can set the handlers and transitions required for the assistants API (buffer-locally) -- so far so good.

But if you

  1. change the backend to a regular (chat-completions) API the handlers/transitions are now wrong.
  2. change to a different preset the handlers/transitions are still wrong since most presets won't explicitly set the handlers/transitions.

That's a good point. This means backends and fsms should be treated as coupled?

Here's another maybe hairbrained solution: have a gptel-compatible (backend fsm) method. If this returns nil, then the request fails with an appropriate error message?

ahmed-shariff avatar May 08 '25 04:05 ahmed-shariff

Here's another maybe hairbrained solution: have a gptel-compatible (backend fsm) method.

ah well.... the more I think about it, the more ways it can get complicated 😅

Perhaps, this should be documented as an "advanced feature" and not try to account for every possible outcome. i..e, if the default values of gptel-*--handlers and gptel-request--transition (or a gptel-fasm variable) are changed, perhaps show a warning which can be turned off with a customizable variable?

ahmed-shariff avatar May 08 '25 05:05 ahmed-shariff

While the above patch sets the variables (e.g., model and backend), it does not update the header of the group:

I've fixed this (please update). This feature now also includes a surprise.

karthink avatar May 08 '25 06:05 karthink

This feature now also includes a surprise.

nice :D

The user-error in preset transient's :reader closes the menu, is that by design?

ahmed-shariff avatar May 08 '25 20:05 ahmed-shariff

The user-error in preset transient's :reader closes the menu, is that by design?

It wasn't. Fixed.

karthink avatar May 09 '25 08:05 karthink

It also would be worth adding hooks to run before and after changing presets.

ahmed-shariff avatar May 09 '25 22:05 ahmed-shariff

There can also be a with-gptel-preset wrapper for gptel-equest with options to override some of the variables.

This is a neat idea, and simple to implement too -- however because gptel-request is asynchronous, it won't work for settings that come into play in the response part of the request cycle, such as gptel-include-tool-results. It's also not apparent to the user which settings will be respected and which won't. The ones that specify the LLM choice and LLM behavior (like the backend, model, system message etc) will be respected, while the ones that specify how gptel handles the response, like gptel-include-reasoning, may not be.

I added gptel-with-preset -- you can now run gptel-request with a preset applied:

(gptel-with-preset coder                ; coder is the name of a preset
  (gptel-request "prompt" :callback ...))

I made it so quoting the name of the preset works too:

(gptel-with-preset 'coder ...)

Since forgetting to not quote arguments to macros is a common annoyance.

karthink avatar May 10 '25 01:05 karthink

Added a new menu for presets instead of using an infix + completing-read. Pressing @ or clicking on the preset here

Image

opens up this small menu where you can pick a preset, or save the current gptel configuration as a new preset:

Image

There is also a new interactive command gptel--save-preset to do the same, but it's marked as internal for the time being.

Saving the preset will register it (for this Emacs session), but also copy the lisp code to the kill-ring for you to place in your config. I'm not too happy about clobbering the kill-ring but popping up a buffer is proving very annoying, as anything to do with windows and Transient tends to be.

karthink avatar May 11 '25 01:05 karthink

This is nice!

There seems to be an issue with gptel--preset-mismatch-p. With a preset test, doing (gptel--preset-mismatch-p 'test) results in the following:

Debugger entered--Lisp error: (wrong-number-of-arguments sort 5)
  (sort val :lessp #'string-lessp :in-place t)
  (equal (sort val :lessp #'string-lessp :in-place t) (sort (mapcar #'gptel-tool-name gptel-tools) :lessp #'string-lessp :in-place t))
  (or (equal (sort val :lessp #'string-lessp :in-place t) (sort (mapcar #'gptel-tool-name gptel-tools) :lessp #'string-lessp :in-place t)) (throw 'mismatch t))
  (cond ((memq key '(:description :parents)) 'nil) ((eq key :system) (or (equal gptel--system-message val) (and (symbolp val) (assq val gptel-directives)) (throw 'mismatch t))) ((eq key :backend) (or (if (stringp val) (equal (progn (or (progn ...) (signal ... ...)) (aref gptel-backend 1)) val) (eq gptel-backend val)) (throw 'mismatch t))) ((eq key :tools) (or (equal (sort val :lessp #'string-lessp :in-place t) (sort (mapcar #'gptel-tool-name gptel-tools) :lessp #'string-lessp :in-place t)) (throw 'mismatch t))) (t (let* ((suffix (substring (symbol-name key) 1)) (sym (or (intern-soft (concat "gptel-" suffix)) (intern-soft (concat "gptel--" suffix))))) (or (and sym (boundp sym) (equal (eval sym) val)) (throw 'mismatch t)))))
  (while elm (progn (setq key (car-safe (prog1 elm (setq elm (cdr elm))))) (setq val (car-safe (prog1 elm (setq elm (cdr elm)))))) (cond ((memq key '(:description :parents)) 'nil) ((eq key :system) (or (equal gptel--system-message val) (and (symbolp val) (assq val gptel-directives)) (throw 'mismatch t))) ((eq key :backend) (or (if (stringp val) (equal (progn (or ... ...) (aref gptel-backend 1)) val) (eq gptel-backend val)) (throw 'mismatch t))) ((eq key :tools) (or (equal (sort val :lessp #'string-lessp :in-place t) (sort (mapcar #'gptel-tool-name gptel-tools) :lessp #'string-lessp :in-place t)) (throw 'mismatch t))) (t (let* ((suffix (substring (symbol-name key) 1)) (sym (or (intern-soft ...) (intern-soft ...)))) (or (and sym (boundp sym) (equal (eval sym) val)) (throw 'mismatch t))))))
  (catch 'mismatch (while elm (progn (setq key (car-safe (prog1 elm (setq elm (cdr elm))))) (setq val (car-safe (prog1 elm (setq elm (cdr elm)))))) (cond ((memq key '(:description :parents)) 'nil) ((eq key :system) (or (equal gptel--system-message val) (and (symbolp val) (assq val gptel-directives)) (throw 'mismatch t))) ((eq key :backend) (or (if (stringp val) (equal (progn ... ...) val) (eq gptel-backend val)) (throw 'mismatch t))) ((eq key :tools) (or (equal (sort val :lessp #'string-lessp :in-place t) (sort (mapcar ... gptel-tools) :lessp #'string-lessp :in-place t)) (throw 'mismatch t))) (t (let* ((suffix (substring ... 1)) (sym (or ... ...))) (or (and sym (boundp sym) (equal ... val)) (throw 'mismatch t)))))))
  (let ((elm (or (gptel-get-preset name) (gptel-get-preset (intern-soft name)))) key val) (catch 'mismatch (while elm (progn (setq key (car-safe (prog1 elm (setq elm ...)))) (setq val (car-safe (prog1 elm (setq elm ...))))) (cond ((memq key '(:description :parents)) 'nil) ((eq key :system) (or (equal gptel--system-message val) (and (symbolp val) (assq val gptel-directives)) (throw 'mismatch t))) ((eq key :backend) (or (if (stringp val) (equal ... val) (eq gptel-backend val)) (throw 'mismatch t))) ((eq key :tools) (or (equal (sort val :lessp ... :in-place t) (sort ... :lessp ... :in-place t)) (throw 'mismatch t))) (t (let* ((suffix ...) (sym ...)) (or (and sym ... ...) (throw ... t))))))))
  gptel--preset-mismatch-p(test)

Saving the preset will register it (for this Emacs session), but also copy the lisp code to the kill-ring for you to place in your config. I'm not too happy about clobbering the kill-ring but popping up a buffer is proving very annoying, as anything to do with windows and Transient tends to be.

Why would display-buffer not work in this case?

For selecting a preset, would it not be preferable to use completing-read as opposed to using a dynamic menu? If using a menu like this, perhaps there should be a key arg (probably a mandatory arg than key) in the preset the user can configure?

ahmed-shariff avatar May 11 '25 03:05 ahmed-shariff

Debugger entered--Lisp error: (wrong-number-of-arguments sort 5)
  (sort val :lessp #'string-lessp :in-place t)

That's odd, I don't get it. What version of Emacs are you on?

Saving the preset will register it (for this Emacs session), but also copy the lisp code to the kill-ring for you to place in your config. I'm not too happy about clobbering the kill-ring but popping up a buffer is proving very annoying, as anything to do with windows and Transient tends to be.

Why would display-buffer not work in this case?

display-bufpfer from a transient infix is a big mess. From a suffix that exits the transient it's not much of a problem.

For selecting a preset, would it not be preferable to use completing-read as opposed to using a dynamic menu?

I think completing-read is better by a small margin:

  • Dynamic menu is slightly faster: you can press @d to select the default preset instead of @dRET.
  • completing-read scales better with increasing numbers of presets.
  • In terms of code complexity they're about equal, because the completing-read approach also requires a new EIEIO class and methods.

The main reason I had to switch was for the preset saving interface: There's no room to put it anywhere in gptel-menu, which is already quite cluttered. So I had to put it in a "presets" sub-menu.

Once there's a sub-menu dedicated to presets, it doesn't make much sense to open the sub-menu, then run completing-read -- that's too much work. So I switched to a dynamic menu.

I too would prefer completing-read! If you have ideas about how to present the option to save presets in a discoverable way (so no hidden keybinding like C-u @ etc) without cluttering up gptel-menu further, let me know.

If using a menu like this, perhaps there should be a key arg (probably a mandatory arg than key) in the preset the user can configure?

I would like to avoid mixing interface customization with gptel behavior customization.

karthink avatar May 11 '25 04:05 karthink

That's odd, I don't get it. What version of Emacs are you on?

I am on 29.4. I don't have my PC with the 30.1 on me, I'll test it on that when I can. Is that the built-in sort function being used? By default, mine uses sort


The main reason I had to switch was for the preset saving interface: There's no room to put it anywhere in gptel-menu, which is already quite cluttered. So I had to put it in a "presets" sub-menu.

I would agree with this, when trying to support more features with presets, a dedicated prefix makes sense. But within the sub-menu, instead of a list of dynamically generated keys, I think it would be better to have a completing-read interface to select and set the preset. Right now I am playing around with setting up different presets particularly to reduce the time on setting up tools. I can see that list getting very long.

I also like how you have set up the save-presets; they allow me to configure temporary presets I might use only for a given session.

Once there's a sub-menu dedicated to presets, it doesn't make much sense to open the sub-menu, then run completing-read -- that's too much work. So I switched to a dynamic menu.

Correct me if I am wrong, the way the keys are generated for the "Apply preset" list can change if I create such a temporary preset, right? Which was why I was asking about adding a key option to the prefix. But I agree with you that it is not a good solution. Which also means I would not always be able to rely on muscle memory for this. Which makes completing-read preferable in terms of scaling, but also can potentially be quicker with proper history sorting and incremental filtering. If a user wants a single key access to a given preset, they can always append the gptel--preset transient.


For the "clear preset" could we consider a different key than "DEL". I use a few 60% keyboards :D


From a suffix that exits the transient it's not much of a problem.

Save can be suffix?

Also, is there a reason to limit the saved values to the subset you have in gptel--save-preset?

Another use-case the "temporary presets" got me excited about is managing task-specific contexts. This might be a workaround for having local context (https://github.com/karthink/gptel/issues/475)?

Of course, this requires a different way to encode/decode the context from overlays. A naive solution: the gptel-context--alist can also have entries that are (<buffer name> <start> <end>)?

ahmed-shariff avatar May 11 '25 05:05 ahmed-shariff

I am on 29.4. I don't have my PC with the 30.1 on me, I'll test it on that when I can. Is that the built-in sort function being used? By default, mine uses sort

Can you run (sort '("c" "a" "b") :lessp #'string< :in-place t) and let me know if it throws an error in Emacs 29?


The main reason I had to switch was for the preset saving interface: There's no room to put it anywhere in gptel-menu, which is already quite cluttered. So I had to put it in a "presets" sub-menu.

I would agree with this, when trying to support more features with presets, a dedicated prefix makes sense. But within the sub-menu, instead of a list of dynamically generated keys, I think it would be better to have a completing-read interface to select and set the preset. Right now I am playing around with setting up different presets particularly to reduce the time on setting up tools. I can see that list getting very long.

Let's say the key to select a preset in the presets sub-menu is s. Then you're looking at @sd<RET> to select a preset starting with d. If you include bringing up gptel-menu (charitably C-c g, say), that's C-c g @ s d <RET>. Or from gptel-send, that's C-u C-c <RET> @ s d <RET>. That's too many keys.

I've been using presets for the past week and it's a killer feature, but no one will use it if it takes this many keys to switch presets.

I also like how you have set up the save-presets; they allow me to configure temporary presets I might use only for a given session.

👍

Which also means I would not always be able to rely on muscle memory for this. Which makes completing-read preferable in terms of scaling, but also can potentially be quicker with proper history sorting and incremental filtering.

You're right about the dynamic menu not scaling well, and also about the fact that you can't rely on muscle memory with generated keys. It's just that I would really like choosing a preset to be fast, two key presses would be ideal.

If a user wants a single key access to a given preset, they can always append the gptel--preset transient.

Here I disagree, extending a transient is not for the faint of heart. As gptel has gained more users I've come to realize that most folks aren't using even a tenth of what it provides, either because features are not discoverable or because it's too cumbersome. There's little point in spending dozens of hours designing features that almost no one will find.

If activating a preset takes more than a couple of keys, it simply won't be used much. By the same token, if making it faster requires extending a transient, almost no one will do it.


For the "clear preset" could we consider a different key than "DEL". I use a few 60% keyboards :D

DEL is backspace, should be present on every keyboard. The delete key is <delete>.


From a suffix that exits the transient it's not much of a problem.

Save can be suffix?

It could, but infix felt more natural when I tried it. You can change it to a suffix and see how it feels.

Also, what if the user wants to save the prefix for the session but has no intention of adding it to their config? "Temporary presets", as you put it below. Then the pop-up buffer is a nuisance, doubly so if it's a suffix and the transient menu goes away. If it's in the kill-ring it can be ignored without hassle.

Also, is there a reason to limit the saved values to the subset you have in gptel--save-preset?

No, I just thought it covered the important things. What else do you think should be part of it?

Another use-case the "temporary presets" got me excited about is managing task-specific contexts. This might be a workaround for having local context (https://github.com/karthink/gptel/issues/475)?

Of course, this requires a different way to encode/decode the context from overlays. A naive solution: the gptel-context--alist can also have entries that are (<buffer name> <start> <end>)?

That solution starts to fail the moment you add buffer editing by LLMs (via tool-use) into the equation.

Local context is currently blocked by our use of overlays, a poor decision in hindsight. In most other editors, users send files, not buffers or regions to LLMs -- this is trivial to do locally. So either I have to introduce a second way to add context, with a different UI, which is confusing. Or I need to throw away the overlay method in favor of something else entirely.

I'm currently working on supporting a RAG pipeline in gptel, which requires some deep changes to gptel-request. Adding files as context is a small part of a RAG pipeline, so I think I'll have a better idea of how to do it (in an Emacs-appropriate way) once I'm near the end of the RAG implementation. See also #815.

Making context-specification save-able with presets would definitely be great, it's just going to be a bit in the future.

(To discuss buffer-local context further please use #475 or #481.)

karthink avatar May 11 '25 06:05 karthink

I am on 29.4. I don't have my PC with the 30.1 on me, I'll test it on that when I can. Is that the built-in sort function being used? By default, mine uses sort

Switched to the (sort SEQ PRED) calling convention, this error should be gone now.

karthink avatar May 11 '25 07:05 karthink