gptel.el (73991B)
1 ;;; gptel.el --- Interact with ChatGPT or other LLMs -*- lexical-binding: t; -*- 2 3 ;; Copyright (C) 2023 Karthik Chikmagalur 4 5 ;; Author: Karthik Chikmagalur <karthik.chikmagalur@gmail.com> 6 ;; Package-Version: 20241112.624 7 ;; Package-Revision: 4aa6b7ca79b1 8 ;; Package-Requires: ((emacs "27.1") (transient "0.4.0") (compat "29.1.4.1")) 9 ;; Keywords: convenience 10 ;; URL: https://github.com/karthink/gptel 11 12 ;; SPDX-License-Identifier: GPL-3.0-or-later 13 14 ;; This program is free software; you can redistribute it and/or modify 15 ;; it under the terms of the GNU General Public License as published by 16 ;; the Free Software Foundation, either version 3 of the License, or 17 ;; (at your option) any later version. 18 19 ;; This program is distributed in the hope that it will be useful, 20 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 21 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 22 ;; GNU General Public License for more details. 23 24 ;; You should have received a copy of the GNU General Public License 25 ;; along with this program. If not, see <https://www.gnu.org/licenses/>. 26 27 ;; This file is NOT part of GNU Emacs. 28 29 ;;; Commentary: 30 31 ;; gptel is a simple Large Language Model chat client, with support for multiple 32 ;; models and backends. 33 ;; 34 ;; It works in the spirit of Emacs, available at any time and in any buffer. 35 ;; 36 ;; gptel supports 37 ;; 38 ;; - The services ChatGPT, Azure, Gemini, Anthropic AI, Anyscale, Together.ai, 39 ;; Perplexity, Anyscale, OpenRouter, Groq, PrivateGPT, DeepSeek, Cerebras, 40 ;; Github Models and Kagi (FastGPT & Summarizer) 41 ;; - Local models via Ollama, Llama.cpp, Llamafiles or GPT4All 42 ;; 43 ;; Additionally, any LLM service (local or remote) that provides an 44 ;; OpenAI-compatible API is supported. 45 ;; 46 ;; Features: 47 ;; - It’s async and fast, streams responses. 48 ;; - Interact with LLMs from anywhere in Emacs (any buffer, shell, minibuffer, 49 ;; wherever) 50 ;; - LLM responses are in Markdown or Org markup. 51 ;; - Supports conversations and multiple independent sessions. 52 ;; - Supports multi-modal models (send images, documents). 53 ;; - Save chats as regular Markdown/Org/Text files and resume them later. 54 ;; - You can go back and edit your previous prompts or LLM responses when 55 ;; continuing a conversation. These will be fed back to the model. 56 ;; - Redirect prompts and responses easily 57 ;; - Rewrite, refactor or fill in regions in buffers 58 ;; - Write your own commands for custom tasks with a simple API. 59 ;; 60 ;; Requirements for ChatGPT, Azure, Gemini or Kagi: 61 ;; 62 ;; - You need an appropriate API key. Set the variable `gptel-api-key' to the 63 ;; key or to a function of no arguments that returns the key. (It tries to 64 ;; use `auth-source' by default) 65 ;; 66 ;; ChatGPT is configured out of the box. For the other sources: 67 ;; 68 ;; - For Azure: define a gptel-backend with `gptel-make-azure', which see. 69 ;; - For Gemini: define a gptel-backend with `gptel-make-gemini', which see. 70 ;; - For Anthropic (Claude): define a gptel-backend with `gptel-make-anthropic', 71 ;; which see 72 ;; - For Together.ai, Anyscale, Perplexity, Groq, OpenRouter, DeepSeek, Cerebras or 73 ;; Github Models: define a gptel-backend with `gptel-make-openai', which see. 74 ;; - For PrivateGPT: define a backend with `gptel-make-privategpt', which see. 75 ;; - For Kagi: define a gptel-backend with `gptel-make-kagi', which see. 76 ;; 77 ;; For local models using Ollama, Llama.cpp or GPT4All: 78 ;; 79 ;; - The model has to be running on an accessible address (or localhost) 80 ;; - Define a gptel-backend with `gptel-make-ollama' or `gptel-make-gpt4all', 81 ;; which see. 82 ;; - Llama.cpp or Llamafiles: Define a gptel-backend with `gptel-make-openai', 83 ;; 84 ;; Consult the package README for examples and more help with configuring 85 ;; backends. 86 ;; 87 ;; Usage: 88 ;; 89 ;; gptel can be used in any buffer or in a dedicated chat buffer. The 90 ;; interaction model is simple: Type in a query and the response will be 91 ;; inserted below. You can continue the conversation by typing below the 92 ;; response. 93 ;; 94 ;; To use this in any buffer: 95 ;; 96 ;; - Call `gptel-send' to send the buffer's text up to the cursor. Select a 97 ;; region to send only the region. 98 ;; 99 ;; - You can select previous prompts and responses to continue the conversation. 100 ;; 101 ;; - Call `gptel-send' with a prefix argument to access a menu where you can set 102 ;; your backend, model and other parameters, or to redirect the 103 ;; prompt/response. 104 ;; 105 ;; To use this in a dedicated buffer: 106 ;; 107 ;; - M-x gptel: Start a chat session 108 ;; 109 ;; - In the chat session: Press `C-c RET' (`gptel-send') to send your prompt. 110 ;; Use a prefix argument (`C-u C-c RET') to access a menu. In this menu you 111 ;; can set chat parameters like the system directives, active backend or 112 ;; model, or choose to redirect the input or output elsewhere (such as to the 113 ;; kill ring). 114 ;; 115 ;; - You can save this buffer to a file. When opening this file, turn on 116 ;; `gptel-mode' before editing it to restore the conversation state and 117 ;; continue chatting. 118 ;; 119 ;; - To include media files with your request, you can add them to the context 120 ;; (described next), or include them as links in Org or Markdown mode chat 121 ;; buffers. Sending media is disabled by default, you can turn it on globally 122 ;; via `gptel-track-media', or locally in a chat buffer via the header line. 123 ;; 124 ;; Include more context with requests: 125 ;; 126 ;; If you want to provide the LLM with more context, you can add arbitrary 127 ;; regions, buffers or files to the query with `gptel-add'. To add text or 128 ;; media files, call `gptel-add' in Dired or use the dedicated `gptel-add-file'. 129 ;; 130 ;; You can also add context from gptel's menu instead (gptel-send with a prefix 131 ;; arg), as well as examine or modify context. 132 ;; 133 ;; When context is available, gptel will include it with each LLM query. 134 ;; 135 ;; Rewrite/refactor interface 136 ;; 137 ;; In any buffer: with a region selected, you can rewrite prose, refactor code 138 ;; or fill in the region. Use gptel's menu (C-u M-x `gptel-send') to access 139 ;; this feature. 140 ;; 141 ;; gptel in Org mode: 142 ;; 143 ;; gptel offers a few extra conveniences in Org mode. 144 ;; - You can limit the conversation context to an Org heading with 145 ;; `gptel-org-set-topic'. 146 ;; 147 ;; - You can have branching conversations in Org mode, where each hierarchical 148 ;; outline path through the document is a separate conversation branch. 149 ;; See the variable `gptel-org-branching-context'. 150 ;; 151 ;; - You can declare the gptel model, backend, temperature, system message and 152 ;; other parameters as Org properties with the command 153 ;; `gptel-org-set-properties'. gptel queries under the corresponding heading 154 ;; will always use these settings, allowing you to create mostly reproducible 155 ;; LLM chat notebooks. 156 ;; 157 ;; Finally, gptel offers a general purpose API for writing LLM ineractions 158 ;; that suit your workflow, see `gptel-request'. 159 160 ;;; Code: 161 (declare-function markdown-mode "markdown-mode") 162 (declare-function gptel-curl-get-response "gptel-curl") 163 (declare-function gptel-menu "gptel-transient") 164 (declare-function gptel-system-prompt "gptel-transient") 165 (declare-function pulse-momentary-highlight-region "pulse") 166 167 (declare-function ediff-make-cloned-buffer "ediff-util") 168 (declare-function ediff-regions-internal "ediff") 169 170 (declare-function gptel-org--create-prompt "gptel-org") 171 (declare-function gptel-org-set-topic "gptel-org") 172 (declare-function gptel-org--save-state "gptel-org") 173 (declare-function gptel-org--restore-state "gptel-org") 174 (declare-function gptel--stream-convert-markdown->org "gptel-org") 175 (declare-function gptel--convert-markdown->org "gptel-org") 176 (define-obsolete-function-alias 177 'gptel-set-topic 'gptel-org-set-topic "0.7.5") 178 179 (eval-when-compile 180 (require 'subr-x) 181 (require 'cl-lib)) 182 (require 'compat nil t) 183 (require 'url) 184 (require 'map) 185 (require 'text-property-search) 186 (require 'cl-generic) 187 (require 'gptel-openai) 188 189 (with-eval-after-load 'org 190 (require 'gptel-org)) 191 192 193 ;;; User options 194 195 (defgroup gptel nil 196 "Interact with LLMs from anywhere in Emacs." 197 :group 'hypermedia) 198 199 ;; (defcustom gptel-host "api.openai.com" 200 ;; "The API host queried by gptel." 201 ;; :group 'gptel 202 ;; :type 'string) 203 (make-obsolete-variable 204 'gptel-host 205 "Use `gptel-make-openai' instead." 206 "0.5.0") 207 208 (defcustom gptel-proxy "" 209 "Path to a proxy to use for gptel interactions. 210 Passed to curl via --proxy arg, for example \"proxy.yourorg.com:80\" 211 Leave it empty if you don't use a proxy." 212 :type 'string) 213 214 (defcustom gptel-api-key #'gptel-api-key-from-auth-source 215 "An API key (string) for the default LLM backend. 216 217 OpenAI by default. 218 219 Can also be a function of no arguments that returns an API 220 key (more secure) for the active backend." 221 :type '(choice 222 (string :tag "API key") 223 (function :tag "Function that returns the API key"))) 224 225 (defcustom gptel-stream t 226 "Stream responses from the LLM as they are received. 227 228 This option is ignored unless 229 - the LLM backend supports streaming, and 230 - Curl is in use (see `gptel-use-curl') 231 232 When set to nil, Emacs waits for the full response and inserts it 233 all at once. This wait is asynchronous. 234 235 \='tis a bit silly." 236 :type 'boolean) 237 (make-obsolete-variable 'gptel-playback 'gptel-stream "0.3.0") 238 239 (defcustom gptel-use-curl (and (executable-find "curl") t) 240 "Whether gptel should prefer Curl when available." 241 :type 'boolean) 242 243 (defcustom gptel-curl-file-size-threshold 130000 244 "Size threshold for using file input with Curl. 245 246 Specifies the size threshold for when to use a temporary file to pass data to 247 Curl in GPTel queries. If the size of the data to be sent exceeds this 248 threshold, the data is written to a temporary file and passed to Curl using the 249 `--data-binary' option with a file reference. Otherwise, the data is passed 250 directly as a command-line argument. 251 252 The value is an integer representing the number of bytes. 253 254 Adjusting this value may be necessary depending on the environment 255 and the typical size of the data being sent in GPTel queries. 256 A larger value may improve performance by avoiding the overhead of creating 257 temporary files for small data payloads, while a smaller value may be needed 258 if the command-line argument size is limited by the operating system." 259 :type 'natnum) 260 261 (defcustom gptel-response-filter-functions 262 (list #'gptel--convert-org) 263 "Abnormal hook for transforming the response from an LLM. 264 265 This is used to format the response in some way, such as filling 266 paragraphs, adding annotations or recording information in the 267 response like links. 268 269 Each function in this hook receives two arguments, the response 270 string to transform and the LLM interaction buffer. It 271 should return the transformed string. 272 273 NOTE: This is only used for non-streaming responses. To 274 transform streaming responses, use `gptel-post-stream-hook' and 275 `gptel-post-response-functions'." 276 :type 'hook) 277 278 (defcustom gptel-pre-response-hook nil 279 "Hook run before inserting the LLM response into the current buffer. 280 281 This hook is called in the buffer where the LLM response will be 282 inserted. 283 284 Note: this hook only runs if the request succeeds." 285 :type 'hook) 286 287 (define-obsolete-variable-alias 288 'gptel-post-response-hook 'gptel-post-response-functions 289 "0.6.0" 290 "Post-response functions are now called with two arguments: the 291 start and end buffer positions of the response.") 292 293 (defcustom gptel-post-response-functions nil 294 "Abnormal hook run after inserting the LLM response into the current buffer. 295 296 This hook is called in the buffer to which the LLM response is 297 sent, and after the full response has been inserted. Each 298 function is called with two arguments: the response beginning and 299 end positions. 300 301 Note: this hook runs even if the request fails. In this case the 302 response beginning and end positions are both the cursor position 303 at the time of the request." 304 :type 'hook) 305 306 ;; (defcustom gptel-pre-stream-insert-hook nil 307 ;; "Hook run before each insertion of the LLM's streaming response. 308 309 ;; This hook is called in the buffer from which the prompt was sent 310 ;; to the LLM, immediately before text insertion." 311 ;; :group 'gptel 312 ;; :type 'hook) 313 314 (defcustom gptel-post-stream-hook nil 315 "Hook run after each insertion of the LLM's streaming response. 316 317 This hook is called in the buffer from which the prompt was sent 318 to the LLM, and after a text insertion." 319 :type 'hook) 320 321 (defcustom gptel-save-state-hook nil 322 "Hook run before gptel saves model parameters to a file. 323 324 You can use this hook to store additional conversation state or 325 model parameters to the chat buffer, or to modify the buffer in 326 some other way." 327 :type 'hook) 328 329 (defcustom gptel-default-mode (if (fboundp 'markdown-mode) 330 'markdown-mode 331 'text-mode) 332 "The default major mode for dedicated chat buffers. 333 334 If `markdown-mode' is available, it is used. Otherwise gptel 335 defaults to `text-mode'." 336 :type 'function) 337 338 ;; TODO: Handle `prog-mode' using the `comment-start' variable 339 (defcustom gptel-prompt-prefix-alist 340 '((markdown-mode . "### ") 341 (org-mode . "*** ") 342 (text-mode . "### ")) 343 "String used as a prefix to the query being sent to the LLM. 344 345 This is meant for the user to distinguish between queries and 346 responses, and is removed from the query before it is sent. 347 348 This is an alist mapping major modes to the prefix strings. This 349 is only inserted in dedicated gptel buffers." 350 :type '(alist :key-type symbol :value-type string)) 351 352 (defcustom gptel-response-prefix-alist 353 '((markdown-mode . "") 354 (org-mode . "") 355 (text-mode . "")) 356 "String inserted before the response from the LLM. 357 358 This is meant for the user to distinguish between queries and 359 responses. 360 361 This is an alist mapping major modes to the reply prefix strings. This 362 is only inserted in dedicated gptel buffers before the AI's response." 363 :type '(alist :key-type symbol :value-type string)) 364 365 (defcustom gptel-use-header-line t 366 "Whether `gptel-mode' should use header-line for status information. 367 368 When set to nil, use the mode line for (minimal) status 369 information and the echo area for messages." 370 :type 'boolean) 371 372 (defcustom gptel-display-buffer-action '(pop-to-buffer) 373 "The action used to display gptel chat buffers. 374 375 The gptel buffer is displayed in a window using 376 377 (display-buffer BUFFER gptel-display-buffer-action) 378 379 The value of this option has the form (FUNCTION . ALIST), 380 where FUNCTION is a function or a list of functions. Each such 381 function should accept two arguments: a buffer to display and an 382 alist of the same form as ALIST. See info node `(elisp)Choosing 383 Window' for details." 384 :type display-buffer--action-custom-type) 385 386 (defcustom gptel-crowdsourced-prompts-file 387 (let ((cache-dir (or (eval-when-compile 388 (require 'xdg) 389 (xdg-cache-home)) 390 user-emacs-directory))) 391 (expand-file-name "gptel-crowdsourced-prompts.csv" cache-dir)) 392 "File used to store crowdsourced system prompts. 393 394 These are prompts cached from an online source (see 395 `gptel--crowdsourced-prompts-url'), and can be set from the 396 transient menu interface provided by `gptel-menu'." 397 :type 'file) 398 399 ;; Model and interaction parameters 400 (defcustom gptel-directives 401 '((default . "You are a large language model living in Emacs and a helpful assistant. Respond concisely.") 402 (programming . "You are a large language model and a careful programmer. Provide code and only code as output without any additional text, prompt or note.") 403 (writing . "You are a large language model and a writing assistant. Respond concisely.") 404 (chat . "You are a large language model and a conversation partner. Respond concisely.")) 405 "System prompts (directives) for the LLM. 406 407 These are system instructions sent at the beginning of each 408 request to the LLM. 409 410 Each entry in this alist maps a symbol naming the directive to 411 the string that is sent. To set the directive for a chat session 412 interactively call `gptel-send' with a prefix argument." 413 :safe #'always 414 :type '(alist :key-type symbol :value-type string)) 415 416 (defvar gptel--system-message (alist-get 'default gptel-directives) 417 "The system message used by gptel.") 418 (put 'gptel--system-message 'safe-local-variable #'always) 419 420 (defcustom gptel-max-tokens nil 421 "Max tokens per response. 422 423 This is roughly the number of words in the response. 100-300 is a 424 reasonable range for short answers, 400 or more for longer 425 responses. 426 427 To set the target token count for a chat session interactively 428 call `gptel-send' with a prefix argument." 429 :safe #'always 430 :type '(choice (natnum :tag "Specify Token count") 431 (const :tag "Default" nil))) 432 433 (defcustom gptel-model 'gpt-4o-mini 434 "GPT Model for chat. 435 436 The name of the model, as a symbol. This is the name as expected 437 by the LLM provider's API. 438 439 The current options for ChatGPT are 440 - `gpt-3.5-turbo' 441 - `gpt-3.5-turbo-16k' 442 - `gpt-4o-mini' 443 - `gpt-4' 444 - `gpt-4o' 445 - `gpt-4-turbo' 446 - `gpt-4-turbo-preview' 447 - `gpt-4-32k' 448 - `gpt-4-1106-preview' 449 450 To set the model for a chat session interactively call 451 `gptel-send' with a prefix argument." 452 :safe #'always 453 :type '(choice 454 (symbol :tag "Specify model name") 455 (const :tag "GPT 4 omni mini" gpt-4o-mini) 456 (const :tag "GPT 3.5 turbo" gpt-3.5-turbo) 457 (const :tag "GPT 3.5 turbo 16k" gpt-3.5-turbo-16k) 458 (const :tag "GPT 4" gpt-4) 459 (const :tag "GPT 4 omni" gpt-4o) 460 (const :tag "GPT 4 turbo" gpt-4-turbo) 461 (const :tag "GPT 4 turbo (preview)" gpt-4-turbo-preview) 462 (const :tag "GPT 4 32k" gpt-4-32k) 463 (const :tag "GPT 4 1106 (preview)" gpt-4-1106-preview))) 464 465 (defcustom gptel-temperature 1.0 466 "\"Temperature\" of the LLM response. 467 468 This is a number between 0.0 and 2.0 that controls the randomness 469 of the response, with 2.0 being the most random. 470 471 To set the temperature for a chat session interactively call 472 `gptel-send' with a prefix argument." 473 :safe #'always 474 :type 'number) 475 476 (defvar gptel--known-backends) 477 478 (defconst gptel--openai-models 479 '((gpt-4o 480 :description "Advanced model for complex tasks; cheaper & faster than GPT-Turbo" 481 :capabilities (media tool json url) 482 :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp") 483 :context-window 128 484 :input-cost 2.50 485 :output-cost 10 486 :cutoff-date "2023-10") 487 (gpt-4o-mini 488 :description "Cheap model for fast tasks; cheaper & more capable than GPT-3.5 Turbo" 489 :capabilities (media tool json url) 490 :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp") 491 :context-window 128 492 :input-cost 0.15 493 :output-cost 0.60 494 :cutoff-date "2023-10") 495 (gpt-4-turbo 496 :description "Previous high-intelligence model" 497 :capabilities (media tool url) 498 :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp") 499 :context-window 128 500 :input-cost 10 501 :output-cost 30 502 :cutoff-date "2023-12") 503 ;; points to gpt-4-0613 504 (gpt-4 505 :description "GPT-4 snapshot from June 2023 with improved function calling support" 506 :context-window 8.192 507 :input-cost 30 508 :output-cost 60 509 :cutoff-date "2023-09") 510 (gpt-4-turbo-preview 511 :description "Points to gpt-4-0125-preview" 512 :context-window 128 513 :input-cost 10 514 :output-cost 30 515 :cutoff-date "2023-12") 516 (gpt-4-0125-preview 517 :description "GPT-4 Turbo preview model intended to reduce cases of “laziness”" 518 :context-window 128 519 :input-cost 10 520 :output-cost 30 521 :cutoff-date "2023-12") 522 (o1-preview 523 :description "Reasoning model designed to solve hard problems across domains" 524 :context-window 128 525 :input-cost 15 526 :output-cost 60 527 :cutoff-date "2023-10" 528 :capabilities (nosystem) 529 :request-params (:stream :json-false)) 530 (o1-mini 531 :description "Faster and cheaper reasoning model good at coding, math, and science" 532 :context-window 128 533 :input-cost 3 534 :output-cost 12 535 :cutoff-date "2023-10" 536 :capabilities (nosystem) 537 :request-params (:stream :json-false)) 538 ;; limited information available 539 (gpt-4-32k 540 :input-cost 60 541 :output-cost 120) 542 (gpt-4-1106-preview 543 :description "Preview model with improved function calling support" 544 :context-window 128 545 :input-cost 10 546 :output-cost 30 547 :cutoff-date "2023-04") 548 (gpt-3.5-turbo 549 :description "More expensive & less capable than GPT-4o-mini; use that instead" 550 :capabilities (tool) 551 :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp") 552 :context-window 16.358 553 :input-cost 0.50 554 :output-cost 1.50 555 :cutoff-date "2021-09") 556 (gpt-3.5-turbo-16k 557 :description "More expensive & less capable than GPT-4o-mini; use that instead" 558 :mime-types ("image/jpeg" "image/png" "image/gif" "image/webp") 559 :context-window 16.385 560 :input-cost 3 561 :output-cost 4 562 :cutoff-date "2021-09")) 563 "List of available OpenAI models and associated properties. 564 Keys: 565 566 - `:description': a brief description of the model. 567 568 - `:capabilities': a list of capabilities supported by the model. 569 570 - `:mime-types': a list of supported MIME types for media files. 571 572 - `:context-window': the context window size, in thousands of tokens. 573 574 - `:input-cost': the input cost, in US dollars per million tokens. 575 576 - `:output-cost': the output cost, in US dollars per million tokens. 577 578 - `:cutoff-date': the knowledge cutoff date. 579 580 - `:request-params': a plist of additional request parameters to 581 include when using this model. 582 583 Information about the OpenAI models was obtained from the following 584 sources: 585 586 - <https://openai.com/pricing> 587 - <https://platform.openai.com/docs/models>") 588 589 (defvar gptel--openai 590 (gptel-make-openai 591 "ChatGPT" 592 :key 'gptel-api-key 593 :stream t 594 :models gptel--openai-models)) 595 596 (defcustom gptel-backend gptel--openai 597 "LLM backend to use. 598 599 This is the default \"backend\", an object of type 600 `gptel-backend' containing connection, authentication and model 601 information. 602 603 A backend for ChatGPT is pre-defined by gptel. Backends for 604 other LLM providers (local or remote) may be constructed using 605 one of the available backend creation functions: 606 - `gptel-make-openai' 607 - `gptel-make-azure' 608 - `gptel-make-ollama' 609 - `gptel-make-gpt4all' 610 - `gptel-make-gemini' 611 See their documentation for more information and the package 612 README for examples." 613 :safe #'always 614 :type `(choice 615 (const :tag "ChatGPT" ,gptel--openai) 616 (restricted-sexp :match-alternatives (gptel-backend-p 'nil) 617 :tag "Other backend"))) 618 619 (defvar gptel-expert-commands nil 620 "Whether experimental gptel options should be enabled. 621 622 This opens up advanced options in `gptel-menu'.") 623 624 (defvar-local gptel--bounds nil) 625 (put 'gptel--bounds 'safe-local-variable #'always) 626 627 (defvar gptel--num-messages-to-send nil) 628 (put 'gptel--num-messages-to-send 'safe-local-variable #'always) 629 630 (defcustom gptel-log-level nil 631 "Logging level for gptel. 632 633 This is one of nil or the symbols info and debug: 634 635 nil: Don't log responses 636 info: Log request and response bodies 637 debug: Log request/response bodies, headers and all other 638 connection settings. 639 640 When non-nil, information is logged to `gptel--log-buffer-name', 641 which see." 642 :type '(choice 643 (const :tag "No logging" nil) 644 (const :tag "Limited" info) 645 (const :tag "Full" debug))) 646 (make-obsolete-variable 647 'gptel--debug 'gptel-log-level "0.6.5") 648 649 (defcustom gptel-track-response t 650 "Distinguish between user messages and LLM responses. 651 652 When creating a prompt to send to the LLM, gptel distinguishes 653 between text entered by the user and past LLM responses. This 654 distinction is necessary for back-and-forth conversation with an 655 LLM. 656 657 In regular Emacs buffers you can turn this behavior off by 658 setting `gptel-track-response' to nil. All text, including 659 past LLM responses, is then treated as user input when sending 660 queries. 661 662 This variable has no effect in dedicated chat buffers (buffers 663 with `gptel-mode' enabled), where user prompts and responses are 664 always handled separately." 665 :type 'boolean) 666 667 (defcustom gptel-track-media nil 668 "Whether supported media in chat buffers should be sent. 669 670 When the active `gptel-model' supports it, gptel can send images 671 or other media from links in chat buffers to the LLM. To use 672 this, the following steps are required. 673 674 1. `gptel-track-media' (this variable) should be non-nil 675 676 2. The LLM should provide vision or document support. Currently, 677 only the OpenAI, Anthropic and Ollama APIs are supported. See 678 the documentation of `gptel-make-openai', `gptel-make-anthropic' 679 and `gptel-make-ollama' resp. for details on how to specify media 680 support for models. 681 682 3. Only \"standalone\" links in chat buffers are considered. 683 These are links on their own line with no surrounding text. 684 Further: 685 686 - In Org mode, only files or URLs of the form 687 [[/path/to/media][bracket links]] and <angle/link/path> 688 are sent. 689 690 - In Markdown mode, only files or URLS of the form 691 [bracket link](/path/to/media) and <angle/link/path> 692 are sent. 693 694 This option has no effect in non-chat buffers. To include 695 media (including images) more generally, use `gptel-add'." 696 :type 'boolean) 697 698 (defcustom gptel-use-context 'system 699 "Where in the request to inject gptel's additional context. 700 701 gptel always includes the active region or the buffer up to the 702 cursor in the request to the LLM. Additionally, you can add 703 other buffers or their regions to the context with 704 `gptel-add-context', or from gptel's menu. This data will be 705 sent with every request. 706 707 This option controls whether and where this additional context is 708 included in the request. 709 710 Currently supported options are: 711 712 nil - Do not use the context. 713 system - Include the context with the system message. 714 user - Include the context with the user prompt." 715 :group 'gptel 716 :type '(choice 717 (const :tag "Don't include context" nil) 718 (const :tag "With system message" system) 719 (const :tag "With user prompt" user))) 720 721 (defvar-local gptel--old-header-line nil) 722 723 (defvar gptel-context--alist nil 724 "List of gptel's context sources. 725 726 Each entry is of the form 727 (buffer . (overlay1 overlay2 ...)) 728 or 729 (\"path/to/file\").") 730 731 732 ;;; Utility functions 733 734 (defun gptel-api-key-from-auth-source (&optional host user) 735 "Lookup api key in the auth source. 736 By default, the LLM host for the active backend is used as HOST, 737 and \"apikey\" as USER." 738 (if-let ((secret 739 (plist-get 740 (car (auth-source-search 741 :host (or host (gptel-backend-host gptel-backend)) 742 :user (or user "apikey") 743 :require '(:secret))) 744 :secret))) 745 (if (functionp secret) 746 (encode-coding-string (funcall secret) 'utf-8) 747 secret) 748 (user-error "No `gptel-api-key' found in the auth source"))) 749 750 ;; FIXME Should we utf-8 encode the api-key here? 751 (defun gptel--get-api-key (&optional key) 752 "Get api key from KEY, or from `gptel-api-key'." 753 (when-let ((key-sym (or key (gptel-backend-key gptel-backend)))) 754 (cl-typecase key-sym 755 (function (funcall key-sym)) 756 (string key-sym) 757 (symbol (if-let ((val (symbol-value key-sym))) 758 (gptel--get-api-key 759 (symbol-value key-sym)) 760 (error "`gptel-api-key' is not valid"))) 761 (t (error "`gptel-api-key' is not valid"))))) 762 763 (defsubst gptel--to-number (val) 764 "Ensure VAL is a number." 765 (cond 766 ((numberp val) val) 767 ((stringp val) (string-to-number val)) 768 ((error "%S cannot be converted to a number" val)))) 769 770 (defsubst gptel--to-string (s) 771 "Convert S to a string, if possible." 772 (cl-etypecase s 773 (symbol (symbol-name s)) 774 (string s) 775 (number (number-to-string s)))) 776 777 (defsubst gptel--intern (s) 778 "Intern S, if possible." 779 (cl-etypecase s 780 (symbol s) 781 (string (intern s)))) 782 783 (defun gptel--merge-plists (&rest plists) 784 "Merge PLISTS, altering the first one. 785 786 Later plists in the sequence take precedence over earlier ones." 787 (let (;; (rtn (copy-sequence (pop plists))) 788 (rtn (pop plists)) 789 p v ls) 790 (while plists 791 (setq ls (pop plists)) 792 (while ls 793 (setq p (pop ls) v (pop ls)) 794 (setq rtn (plist-put rtn p v)))) 795 rtn)) 796 (defun gptel-auto-scroll () 797 "Scroll window if LLM response continues below viewport. 798 799 Note: This will move the cursor." 800 (when-let ((win (get-buffer-window (current-buffer) 'visible)) 801 ((not (pos-visible-in-window-p (point) win))) 802 (scroll-error-top-bottom t)) 803 (condition-case nil 804 (with-selected-window win 805 (scroll-up-command)) 806 (error nil)))) 807 808 (defun gptel-beginning-of-response (&optional _ _ arg) 809 "Move point to the beginning of the LLM response ARG times." 810 (interactive "p") 811 ;; FIXME: Only works for arg == 1 812 (gptel-end-of-response nil nil (- (or arg 1)))) 813 814 (defun gptel-end-of-response (&optional _ _ arg) 815 "Move point to the end of the LLM response ARG times." 816 (interactive (list nil nil 817 (prefix-numeric-value current-prefix-arg))) 818 (unless arg (setq arg 1)) 819 (let ((search (if (> arg 0) 820 #'text-property-search-forward 821 #'text-property-search-backward))) 822 (dotimes (_ (abs arg)) 823 (funcall search 'gptel 'response t) 824 (if (> arg 0) 825 (when (looking-at (concat "\n\\{1,2\\}" 826 (regexp-quote 827 (gptel-prompt-prefix-string)) 828 "?")) 829 (goto-char (match-end 0))) 830 (when (looking-back (concat (regexp-quote 831 (gptel-response-prefix-string)) 832 "?") 833 (point-min)) 834 (goto-char (match-beginning 0))))))) 835 836 (defmacro gptel--at-word-end (&rest body) 837 "Execute BODY at end of the current word or punctuation." 838 `(save-excursion 839 (skip-syntax-forward "w.") 840 ,(macroexp-progn body))) 841 842 (defun gptel-prompt-prefix-string () 843 "Prefix before user prompts in `gptel-mode'." 844 (or (alist-get major-mode gptel-prompt-prefix-alist) "")) 845 846 (defun gptel-response-prefix-string () 847 "Prefix before LLM responses in `gptel-mode'." 848 (or (alist-get major-mode gptel-response-prefix-alist) "")) 849 850 (defsubst gptel--trim-prefixes (s) 851 "Remove prompt/response prefixes from string S." 852 (string-trim s 853 (format "[\t\r\n ]*\\(?:%s\\)?[\t\r\n ]*" 854 (regexp-quote (gptel-prompt-prefix-string))) 855 (format "[\t\r\n ]*\\(?:%s\\)?[\t\r\n ]*" 856 (regexp-quote (gptel-response-prefix-string))))) 857 858 (defsubst gptel--link-standalone-p (beg end) 859 "Return non-nil if positions BEG and END are isolated. 860 861 This means the extent from BEG to END is the only non-whitespace 862 content on this line." 863 (save-excursion 864 (and (= beg (progn (goto-char beg) (beginning-of-line) 865 (skip-chars-forward "\t ") 866 (point))) 867 (= end (progn (goto-char end) (end-of-line) 868 (skip-chars-backward "\t ") 869 (point)))))) 870 871 (defvar-local gptel--backend-name nil 872 "Store to persist backend name across Emacs sessions. 873 874 Note: Changing this variable does not affect gptel\\='s behavior 875 in any way.") 876 (put 'gptel--backend-name 'safe-local-variable #'always) 877 878 ;;;; Model interface 879 ;; NOTE: This interface would be simpler to implement as a defstruct. But then 880 ;; users cannot set `gptel-model' to a symbol/string directly, or we'd need 881 ;; another map from these symbols to the actual model structs. 882 883 (defsubst gptel--model-name (model) 884 "Get name of gptel MODEL." 885 (gptel--to-string model)) 886 887 (defsubst gptel--model-capabilities (model) 888 "Get MODEL capabilities." 889 (get model :capabilities)) 890 891 (defsubst gptel--model-mimes (model) 892 "Get supported mime-types for MODEL." 893 (get model :mime-types)) 894 895 (defsubst gptel--model-capable-p (cap &optional model) 896 "Return non-nil if MODEL supports capability CAP." 897 (memq cap (gptel--model-capabilities 898 (or model gptel-model)))) 899 900 ;; TODO Handle model mime specifications like "image/*" 901 (defsubst gptel--model-mime-capable-p (mime &optional model) 902 "Return non nil if MODEL can understand MIME type." 903 (car-safe (member mime (gptel--model-mimes 904 (or model gptel-model))))) 905 906 (defsubst gptel--model-request-params (model) 907 "Get model-specific request parameters for MODEL." 908 (get model :request-params)) 909 910 ;;;; File handling 911 (defun gptel--base64-encode (file) 912 "Encode FILE as a base64 string. 913 914 FILE is assumed to exist and be a regular file." 915 (with-temp-buffer 916 (insert-file-contents-literally file) 917 (base64-encode-region (point-min) (point-max) 918 :no-line-break) 919 (buffer-string))) 920 921 ;;;; Response text recognition 922 923 (defun gptel--get-buffer-bounds () 924 "Return the gptel response boundaries in the buffer as an alist." 925 (save-excursion 926 (save-restriction 927 (widen) 928 (goto-char (point-max)) 929 (let ((prop) (bounds)) 930 (while (setq prop (text-property-search-backward 931 'gptel 'response t)) 932 (push (cons (prop-match-beginning prop) 933 (prop-match-end prop)) 934 bounds)) 935 bounds)))) 936 937 (defun gptel--get-bounds () 938 "Return the gptel response boundaries around point." 939 (let (prop) 940 (save-excursion 941 (when (text-property-search-backward 942 'gptel 'response t) 943 (when (setq prop (text-property-search-forward 944 'gptel 'response t)) 945 (cons (prop-match-beginning prop) 946 (prop-match-end prop))))))) 947 948 (defun gptel--in-response-p (&optional pt) 949 "Check if position PT is inside a gptel response." 950 (get-char-property (or pt (point)) 'gptel)) 951 952 (defun gptel--at-response-history-p (&optional pt) 953 "Check if gptel response at position PT has variants." 954 (get-char-property (or pt (point)) 'gptel-history)) 955 956 (defvar gptel--mode-description-alist 957 '((js2-mode . "Javascript") 958 (sh-mode . "Shell") 959 (enh-ruby-mode . "Ruby") 960 (yaml-mode . "Yaml") 961 (yaml-ts-mode . "Yaml") 962 (rustic-mode . "Rust")) 963 "Mapping from unconventionally named major modes to languages. 964 965 This is used when generating system prompts for rewriting and 966 when including context from these major modes.") 967 968 (defun gptel--strip-mode-suffix (mode-sym) 969 "Remove the -mode suffix from MODE-SYM. 970 971 MODE-SYM is typically a major-mode symbol." 972 (or (alist-get mode-sym gptel--mode-description-alist) 973 (let ((mode-name (thread-last 974 (symbol-name mode-sym) 975 (string-remove-suffix "-mode") 976 (string-remove-suffix "-ts")))) 977 (if (provided-mode-derived-p 978 mode-sym 'prog-mode 'text-mode 'tex-mode) 979 mode-name "")))) 980 981 982 ;;; Logging 983 984 (defconst gptel--log-buffer-name "*gptel-log*" 985 "Log buffer for gptel.") 986 987 (declare-function json-pretty-print "json") 988 989 (defun gptel--log (data &optional type no-json) 990 "Log DATA to `gptel--log-buffer-name'. 991 992 TYPE is a label for data being logged. DATA is assumed to be 993 Valid JSON unless NO-JSON is t." 994 (with-current-buffer (get-buffer-create gptel--log-buffer-name) 995 (let ((p (goto-char (point-max)))) 996 (unless (bobp) (insert "\n")) 997 (insert (format "{\"gptel\": \"%s\", " (or type "none")) 998 (format-time-string "\"timestamp\": \"%Y-%m-%d %H:%M:%S\"}\n") 999 data) 1000 (unless no-json (ignore-errors (json-pretty-print p (point))))))) 1001 1002 1003 ;;; Saving and restoring state 1004 1005 (defun gptel--restore-state () 1006 "Restore gptel state when turning on `gptel-mode'." 1007 (when (buffer-file-name) 1008 (if (derived-mode-p 'org-mode) 1009 (progn 1010 (require 'gptel-org) 1011 (gptel-org--restore-state)) 1012 (when gptel--bounds 1013 (mapc (pcase-lambda (`(,beg . ,end)) 1014 (put-text-property beg end 'gptel 'response)) 1015 gptel--bounds) 1016 (message "gptel chat restored.")) 1017 (when gptel--backend-name 1018 (if-let ((backend (alist-get 1019 gptel--backend-name gptel--known-backends 1020 nil nil #'equal))) 1021 (setq-local gptel-backend backend) 1022 (message 1023 (substitute-command-keys 1024 (concat 1025 "Could not activate gptel backend \"%s\"! " 1026 "Switch backends with \\[universal-argument] \\[gptel-send]" 1027 " before using gptel.")) 1028 gptel--backend-name)))))) 1029 1030 (defun gptel--save-state () 1031 "Write the gptel state to the buffer. 1032 1033 This saves chat metadata when writing the buffer to disk. To 1034 restore a chat session, turn on `gptel-mode' after opening the 1035 file." 1036 (run-hooks 'gptel-save-state-hook) 1037 (if (derived-mode-p 'org-mode) 1038 (progn 1039 (require 'gptel-org) 1040 (gptel-org--save-state)) 1041 (let ((print-escape-newlines t)) 1042 (save-excursion 1043 (save-restriction 1044 (add-file-local-variable 'gptel-model gptel-model) 1045 (add-file-local-variable 'gptel--backend-name 1046 (gptel-backend-name gptel-backend)) 1047 (unless (equal (default-value 'gptel-temperature) gptel-temperature) 1048 (add-file-local-variable 'gptel-temperature gptel-temperature)) 1049 (unless (string= (default-value 'gptel--system-message) 1050 gptel--system-message) 1051 (add-file-local-variable 'gptel--system-message gptel--system-message)) 1052 (when gptel-max-tokens 1053 (add-file-local-variable 'gptel-max-tokens gptel-max-tokens)) 1054 (when (natnump gptel--num-messages-to-send) 1055 (add-file-local-variable 'gptel--num-messages-to-send 1056 gptel--num-messages-to-send)) 1057 (add-file-local-variable 'gptel--bounds (gptel--get-buffer-bounds))))))) 1058 1059 1060 ;;; Minor mode and UI 1061 1062 ;; NOTE: It's not clear that this is the best strategy: 1063 (add-to-list 'text-property-default-nonsticky '(gptel . t)) 1064 1065 ;;;###autoload 1066 (define-minor-mode gptel-mode 1067 "Minor mode for interacting with LLMs." 1068 :lighter " GPT" 1069 :keymap 1070 (let ((map (make-sparse-keymap))) 1071 (define-key map (kbd "C-c RET") #'gptel-send) 1072 map) 1073 (if gptel-mode 1074 (progn 1075 (unless (or (derived-mode-p 'org-mode 'markdown-mode) 1076 (eq major-mode 'text-mode)) 1077 (gptel-mode -1) 1078 (user-error (format "`gptel-mode' is not supported in `%s'." major-mode))) 1079 (add-hook 'before-save-hook #'gptel--save-state nil t) 1080 (gptel--restore-state) 1081 (if gptel-use-header-line 1082 (setq gptel--old-header-line header-line-format 1083 header-line-format 1084 (list '(:eval (concat (propertize " " 'display '(space :align-to 0)) 1085 (format "%s" (gptel-backend-name gptel-backend)))) 1086 (propertize " Ready" 'face 'success) 1087 '(:eval 1088 (let* ((model (gptel--model-name gptel-model)) 1089 (system 1090 (propertize 1091 (buttonize 1092 (format "[Prompt: %s]" 1093 (or (car-safe (rassoc gptel--system-message gptel-directives)) 1094 (truncate-string-to-width gptel--system-message 15 nil nil t))) 1095 (lambda (&rest _) (gptel-system-prompt))) 1096 'mouse-face 'highlight 1097 'help-echo "System message for session")) 1098 (context 1099 (and gptel-context--alist 1100 (cl-loop for entry in gptel-context--alist 1101 if (bufferp (car entry)) count it into bufs 1102 else count (stringp (car entry)) into files 1103 finally return 1104 (propertize 1105 (buttonize 1106 (concat "[Context: " 1107 (and (> bufs 0) (format "%d buf" bufs)) 1108 (and (> bufs 1) "s") 1109 (and (> bufs 0) (> files 0) ", ") 1110 (and (> files 0) (format "%d file" files)) 1111 (and (> files 1) "s") 1112 "]") 1113 (lambda (&rest _) 1114 (require 'gptel-context) 1115 (gptel-context--buffer-setup))) 1116 'mouse-face 'highlight 1117 'help-echo "Active gptel context")))) 1118 (toggle-track-media 1119 (lambda (&rest _) 1120 (setq-local gptel-track-media 1121 (not gptel-track-media)) 1122 (if gptel-track-media 1123 (message 1124 (concat 1125 "Sending media from included links. To include media, create " 1126 "a \"standalone\" link in a paragraph by itself, separated from surrounding text.")) 1127 (message "Ignoring image links. Only link text will be sent.")) 1128 (run-at-time 0 nil #'force-mode-line-update))) 1129 (track-media 1130 (and (gptel--model-capable-p 'media) 1131 (if gptel-track-media 1132 (propertize 1133 (buttonize "[Sending media]" toggle-track-media) 1134 'mouse-face 'highlight 1135 'help-echo 1136 "Sending media from standalone links/urls when supported.\nClick to toggle") 1137 (propertize 1138 (buttonize "[Ignoring media]" toggle-track-media) 1139 'mouse-face 'highlight 1140 'help-echo 1141 "Ignoring images from standalone links/urls.\nClick to toggle"))))) 1142 (concat 1143 (propertize 1144 " " 'display 1145 `(space :align-to (- right ,(+ 5 (length model) (length system) 1146 (length track-media) (length context))))) 1147 track-media (and context " ") context " " system " " 1148 (propertize 1149 (buttonize (concat "[" model "]") 1150 (lambda (&rest _) (gptel-menu))) 1151 'mouse-face 'highlight 1152 'help-echo "GPT model in use")))))) 1153 (setq mode-line-process 1154 '(:eval (concat " " 1155 (buttonize (gptel--model-name gptel-model) 1156 (lambda (&rest _) (gptel-menu)))))))) 1157 (remove-hook 'before-save-hook #'gptel--save-state t) 1158 (if gptel-use-header-line 1159 (setq header-line-format gptel--old-header-line 1160 gptel--old-header-line nil) 1161 (setq mode-line-process nil)))) 1162 1163 (defun gptel--update-status (&optional msg face) 1164 "Update status MSG in FACE." 1165 (when gptel-mode 1166 (if gptel-use-header-line 1167 (and (consp header-line-format) 1168 (setf (nth 1 header-line-format) 1169 (propertize msg 'face face))) 1170 (if (member msg '(" Typing..." " Waiting...")) 1171 (setq mode-line-process (propertize msg 'face face)) 1172 (setq mode-line-process 1173 '(:eval (concat " " 1174 (buttonize (gptel--model-name gptel-model) 1175 (lambda (&rest _) (gptel-menu)))))) 1176 (message (propertize msg 'face face)))) 1177 (force-mode-line-update))) 1178 1179 (declare-function gptel-context--wrap "gptel-context") 1180 1181 1182 ;;; Send queries, handle responses 1183 (cl-defun gptel-request 1184 (&optional prompt &key callback 1185 (buffer (current-buffer)) 1186 position context dry-run 1187 (stream nil) (in-place nil) 1188 (system gptel--system-message)) 1189 "Request a response from the `gptel-backend' for PROMPT. 1190 1191 The request is asynchronous, the function immediately returns 1192 with the data that was sent. 1193 1194 Note: This function is not fully self-contained. Consider 1195 let-binding the parameters `gptel-backend' and `gptel-model' 1196 around calls to it as required. 1197 1198 If PROMPT is 1199 - a string, it is used to create a full prompt suitable for 1200 sending to the LLM. 1201 - nil but region is active, the region contents are used. 1202 - nil, the current buffer's contents up to (point) are used. 1203 Previous responses from the LLM are identified as responses. 1204 - A list of plists, it is used as is. 1205 1206 Keyword arguments: 1207 1208 CALLBACK, if supplied, is a function of two arguments, called 1209 with the RESPONSE (a string) and INFO (a plist): 1210 1211 (callback RESPONSE INFO) 1212 1213 RESPONSE is nil if there was no response or an error. 1214 1215 The INFO plist has (at least) the following keys: 1216 :data - The request data included with the query 1217 :position - marker at the point the request was sent, unless 1218 POSITION is specified. 1219 :buffer - The buffer current when the request was sent, 1220 unless BUFFER is specified. 1221 :status - Short string describing the result of the request 1222 1223 Example of a callback that messages the user with the response 1224 and info: 1225 1226 (lambda (response info) 1227 (if response 1228 (let ((posn (marker-position (plist-get info :position))) 1229 (buf (buffer-name (plist-get info :buffer)))) 1230 (message \"Response for request from %S at %d: %s\" 1231 buf posn response)) 1232 (message \"gptel-request failed with message: %s\" 1233 (plist-get info :status)))) 1234 1235 Or, for just the response: 1236 1237 (lambda (response _) 1238 ;; Do something with response 1239 (message (rot13-string response))) 1240 1241 If CALLBACK is omitted, the response is inserted at the point the 1242 request was sent. 1243 1244 BUFFER and POSITION are the buffer and position (integer or 1245 marker) at which the response is inserted. If a CALLBACK is 1246 specified, no response is inserted and these arguments are 1247 ignored, but they are still available in the INFO plist passed 1248 to CALLBACK for you to use. 1249 1250 BUFFER defaults to the current buffer, and POSITION to the value 1251 of (point) or (region-end), depending on whether the region is 1252 active. 1253 1254 CONTEXT is any additional data needed for the callback to run. It 1255 is included in the INFO argument to the callback. 1256 1257 SYSTEM is the system message (chat directive) sent to the LLM. If 1258 omitted, the value of `gptel--system-message' for the current 1259 buffer is used. 1260 1261 The following keywords are mainly for internal use: 1262 1263 IN-PLACE is a boolean used by the default callback when inserting 1264 the response to determine if delimiters are needed between the 1265 prompt and the response. 1266 1267 STREAM is a boolean that determines if the response should be 1268 streamed, as in `gptel-stream'. Do not set this if you are 1269 specifying a custom CALLBACK! 1270 1271 If DRY-RUN is non-nil, construct and return the full 1272 query data as usual, but do not send the request. 1273 1274 Model parameters can be let-bound around calls to this function." 1275 (declare (indent 1)) 1276 ;; TODO Remove this check in version 1.0 1277 (gptel--sanitize-model) 1278 (let* ((gptel--system-message 1279 ;Add context chunks to system message if required 1280 (if (and gptel-context--alist 1281 (eq gptel-use-context 'system) 1282 (not (gptel--model-capable-p 'nosystem))) 1283 (gptel-context--wrap system) 1284 system)) 1285 (gptel-stream stream) 1286 (start-marker 1287 (cond 1288 ((null position) 1289 (if (use-region-p) 1290 (set-marker (make-marker) (region-end)) 1291 (gptel--at-word-end (point-marker)))) 1292 ((markerp position) position) 1293 ((integerp position) 1294 (set-marker (make-marker) position buffer)))) 1295 (full-prompt 1296 (cond 1297 ((null prompt) 1298 (gptel--create-prompt start-marker)) 1299 ((stringp prompt) 1300 ;; FIXME Dear reader, welcome to Jank City: 1301 (with-temp-buffer 1302 (let ((gptel-model (buffer-local-value 'gptel-model buffer)) 1303 (gptel-backend (buffer-local-value 'gptel-backend buffer))) 1304 (insert prompt) 1305 (gptel--create-prompt)))) 1306 ((consp prompt) prompt))) 1307 (request-data (gptel--request-data gptel-backend full-prompt)) 1308 (info (list :data request-data 1309 :buffer buffer 1310 :position start-marker))) 1311 ;; This context should not be confused with the context aggregation context! 1312 (when context (plist-put info :context context)) 1313 (when in-place (plist-put info :in-place in-place)) 1314 (unless dry-run 1315 (funcall (if gptel-use-curl 1316 #'gptel-curl-get-response #'gptel--url-get-response) 1317 info callback)) 1318 request-data)) 1319 1320 ;; TODO: Handle multiple requests(#15). (Only one request from one buffer at a time?) 1321 ;;;###autoload 1322 (defun gptel-send (&optional arg) 1323 "Submit this prompt to the current LLM backend. 1324 1325 By default, the contents of the buffer up to the cursor position 1326 are sent. If the region is active, its contents are sent 1327 instead. 1328 1329 The response from the LLM is inserted below the cursor position 1330 at the time of sending. To change this behavior or model 1331 parameters, use prefix arg ARG activate a transient menu with 1332 more options instead. 1333 1334 This command is asynchronous, you can continue to use Emacs while 1335 waiting for the response." 1336 (interactive "P") 1337 (if (and arg (require 'gptel-transient nil t)) 1338 (call-interactively #'gptel-menu) 1339 (message "Querying %s..." (gptel-backend-name gptel-backend)) 1340 (gptel--sanitize-model) 1341 (gptel-request nil :stream gptel-stream) 1342 (gptel--update-status " Waiting..." 'warning))) 1343 1344 (declare-function json-pretty-print-buffer "json") 1345 (defun gptel--inspect-query (request-data &optional arg) 1346 "Show REQUEST-DATA, the full LLM query to be sent, in a buffer. 1347 1348 This functions as a dry run of `gptel-send'. If ARG is 1349 the symbol json, show the encoded JSON query instead of the Lisp 1350 structure gptel uses." 1351 (with-current-buffer (get-buffer-create "*gptel-query*") 1352 (let ((standard-output (current-buffer)) 1353 (inhibit-read-only t)) 1354 (buffer-disable-undo) 1355 (erase-buffer) 1356 (if (eq arg 'json) 1357 (progn (fundamental-mode) 1358 (insert (gptel--json-encode request-data)) 1359 (json-pretty-print-buffer)) 1360 (lisp-data-mode) 1361 (prin1 request-data) 1362 (pp-buffer)) 1363 (goto-char (point-min)) 1364 (view-mode 1) 1365 (display-buffer (current-buffer) gptel-display-buffer-action)))) 1366 1367 (defun gptel--insert-response (response info) 1368 "Insert the LLM RESPONSE into the gptel buffer. 1369 1370 INFO is a plist containing information relevant to this buffer. 1371 See `gptel--url-get-response' for details." 1372 (let* ((status-str (plist-get info :status)) 1373 (gptel-buffer (plist-get info :buffer)) 1374 (start-marker (plist-get info :position)) 1375 response-beg response-end) 1376 ;; Handle read-only buffers 1377 (when (with-current-buffer gptel-buffer 1378 (or buffer-read-only 1379 (get-char-property start-marker 'read-only))) 1380 (message "Buffer is read only, displaying reply in buffer \"*LLM response*\"") 1381 (display-buffer 1382 (with-current-buffer (get-buffer-create "*LLM response*") 1383 (visual-line-mode 1) 1384 (goto-char (point-max)) 1385 (move-marker start-marker (point) (current-buffer)) 1386 (current-buffer)) 1387 '((display-buffer-reuse-window 1388 display-buffer-pop-up-window) 1389 (reusable-frames . visible)))) 1390 ;; Insert response and status message/error message 1391 (with-current-buffer gptel-buffer 1392 (if response 1393 (progn 1394 (setq response (gptel--transform-response 1395 response gptel-buffer)) 1396 (save-excursion 1397 (put-text-property 1398 0 (length response) 'gptel 'response response) 1399 (with-current-buffer (marker-buffer start-marker) 1400 (goto-char start-marker) 1401 (run-hooks 'gptel-pre-response-hook) 1402 (unless (or (bobp) (plist-get info :in-place)) 1403 (insert "\n\n") 1404 (when gptel-mode 1405 (insert (gptel-response-prefix-string)))) 1406 (setq response-beg (point)) ;Save response start position 1407 (insert response) 1408 (setq response-end (point)) 1409 (pulse-momentary-highlight-region response-beg response-end) 1410 (when gptel-mode (insert "\n\n" (gptel-prompt-prefix-string)))) ;Save response end position 1411 (when gptel-mode (gptel--update-status " Ready" 'success)))) 1412 (gptel--update-status 1413 (format " Response Error: %s" status-str) 'error) 1414 (message "gptel response error: (%s) %s" 1415 status-str (plist-get info :error)))) 1416 ;; Run hook in visible window to set window-point, BUG #269 1417 (if-let ((gptel-window (get-buffer-window gptel-buffer 'visible))) 1418 (with-selected-window gptel-window 1419 (run-hook-with-args 'gptel-post-response-functions response-beg response-end)) 1420 (with-current-buffer gptel-buffer 1421 (run-hook-with-args 'gptel-post-response-functions response-beg response-end))))) 1422 1423 (defun gptel--create-prompt (&optional prompt-end) 1424 "Return a full conversation prompt from the contents of this buffer. 1425 1426 If `gptel--num-messages-to-send' is set, limit to that many 1427 recent exchanges. 1428 1429 If the region is active limit the prompt to the region contents 1430 instead. 1431 1432 If `gptel-context--alist' is non-nil and the additional 1433 context needs to be included with the user prompt, add it. 1434 1435 If PROMPT-END (a marker) is provided, end the prompt contents 1436 there." 1437 (save-excursion 1438 (save-restriction 1439 (let* ((max-entries (and gptel--num-messages-to-send 1440 (* 2 gptel--num-messages-to-send))) 1441 (prompt-end (or prompt-end (point-max))) 1442 (prompts 1443 (cond 1444 ((use-region-p) 1445 ;; Narrow to region 1446 (narrow-to-region (region-beginning) (region-end)) 1447 (goto-char (point-max)) 1448 (gptel--parse-buffer gptel-backend max-entries)) 1449 ((derived-mode-p 'org-mode) 1450 (require 'gptel-org) 1451 (goto-char prompt-end) 1452 (gptel-org--create-prompt prompt-end)) 1453 (t (goto-char prompt-end) 1454 (gptel--parse-buffer gptel-backend max-entries))))) 1455 ;; NOTE: prompts is modified in place here 1456 (when gptel-context--alist 1457 ;; Inject context chunks into the last user prompt if required. 1458 ;; This is also the fallback for when `gptel-use-context' is set to 1459 ;; 'system but the model does not support system messages. 1460 (when (and gptel-use-context 1461 (or (eq gptel-use-context 'user) 1462 (gptel--model-capable-p 'nosystem)) 1463 (> (length prompts) 0)) ;FIXME context should be injected 1464 ;even when there are no prompts 1465 (gptel--wrap-user-prompt gptel-backend prompts)) 1466 ;; Inject media chunks into the first user prompt if required. Media 1467 ;; chunks are always included with the first user message, 1468 ;; irrespective of the preference in `gptel-use-context'. This is 1469 ;; because media cannot be included (in general) with system messages. 1470 (when (and gptel-use-context gptel-track-media 1471 (gptel--model-capable-p 'media)) 1472 (gptel--wrap-user-prompt gptel-backend prompts :media))) 1473 prompts)))) 1474 1475 (cl-defgeneric gptel--parse-buffer (backend max-entries) 1476 "Parse current buffer backwards from point and return a list of prompts. 1477 1478 BACKEND is the LLM backend in use. 1479 1480 MAX-ENTRIES is the number of queries/responses to include for 1481 contexbt.") 1482 1483 (cl-defgeneric gptel--parse-media-links (mode beg end) 1484 "Find media links between BEG and END. 1485 1486 MODE is the major-mode of the buffer. 1487 1488 Returns a plist where each entry is of the form 1489 (:text \"some text\") 1490 or 1491 (:media \"media uri or file path\")." 1492 (ignore mode) ;byte-compiler 1493 (list `(:text ,(buffer-substring beg end)))) 1494 1495 (defvar markdown-regex-link-inline) 1496 (defvar markdown-regex-angle-uri) 1497 (declare-function markdown-link-at-pos "markdown-mode") 1498 (declare-function mailcap-file-name-to-mime-type "mailcap") 1499 1500 (cl-defmethod gptel--parse-media-links ((_mode (eql 'markdown-mode)) beg end) 1501 "Parse text and actionable links between BEG and END. 1502 1503 Return a list of the form 1504 ((:text \"some text\") 1505 (:media \"/path/to/media.png\" :mime \"image/png\") 1506 (:text \"More text\")) 1507 for inclusion into the user prompt for the gptel request." 1508 (require 'mailcap) ;FIXME Avoid this somehow 1509 (let ((parts) (from-pt)) 1510 (save-excursion 1511 (setq from-pt (goto-char beg)) 1512 (while (re-search-forward 1513 (concat "\\(?:" markdown-regex-link-inline "\\|" 1514 markdown-regex-angle-uri "\\)") 1515 end t) 1516 (when-let* ((link-at-pt (markdown-link-at-pos (point))) 1517 ((gptel--link-standalone-p 1518 (car link-at-pt) (cadr link-at-pt))) 1519 (path (nth 3 link-at-pt)) 1520 (path (string-remove-prefix "file://" path)) 1521 (mime (mailcap-file-name-to-mime-type path)) 1522 ((gptel--model-mime-capable-p mime))) 1523 (cond 1524 ((seq-some (lambda (p) (string-prefix-p p path)) 1525 '("https:" "http:" "ftp:")) 1526 ;; Collect text up to this image, and collect this image url 1527 (when (gptel--model-capable-p 'url) ; FIXME This is not a good place 1528 ; to check for url capability! 1529 (push (list :text (buffer-substring-no-properties from-pt (car link-at-pt))) 1530 parts) 1531 (push (list :url path :mime mime) parts) 1532 (setq from-pt (cadr link-at-pt)))) 1533 ((file-readable-p path) 1534 ;; Collect text up to this image, and collect this image 1535 (push (list :text (buffer-substring-no-properties from-pt (car link-at-pt))) 1536 parts) 1537 (push (list :media path :mime mime) parts) 1538 (setq from-pt (cadr link-at-pt))))))) 1539 (unless (= from-pt end) 1540 (push (list :text (buffer-substring-no-properties from-pt end)) parts)) 1541 (nreverse parts))) 1542 1543 (cl-defgeneric gptel--wrap-user-prompt (backend _prompts) 1544 "Wrap the last prompt in PROMPTS with gptel's context. 1545 1546 PROMPTS is a structure as returned by `gptel--parse-buffer'. 1547 Typically this is a list of plists. 1548 1549 BACKEND is the gptel backend in use." 1550 (display-warning 1551 '(gptel context) 1552 (format "Context support not implemented for backend %s, ignoring context" 1553 (gptel-backend-name backend)))) 1554 1555 (cl-defgeneric gptel--request-data (backend prompts) 1556 "Generate a plist of all data for an LLM query. 1557 1558 BACKEND is the LLM backend in use. 1559 1560 PROMPTS is the plist of previous user queries and LLM responses.") 1561 1562 ;; TODO: Use `run-hook-wrapped' with an accumulator instead to handle 1563 ;; buffer-local hooks, etc. 1564 (defun gptel--transform-response (content-str buffer) 1565 "Filter CONTENT-STR through `gptel-response-filter-functions`. 1566 1567 BUFFER is passed along with CONTENT-STR to each function in this 1568 hook." 1569 (let ((filtered-str content-str)) 1570 (dolist (filter-func gptel-response-filter-functions filtered-str) 1571 (condition-case nil 1572 (when (functionp filter-func) 1573 (setq filtered-str 1574 (funcall filter-func filtered-str buffer))) 1575 (error 1576 (display-warning '(gptel filter-functions) 1577 (format "Function %S returned an error" 1578 filter-func))))))) 1579 1580 (defun gptel--convert-org (content buffer) 1581 "Transform CONTENT according to required major-mode. 1582 1583 Currently only `org-mode' is handled. 1584 1585 BUFFER is the LLM interaction buffer." 1586 (if (with-current-buffer buffer (derived-mode-p 'org-mode)) 1587 (gptel--convert-markdown->org content) 1588 content)) 1589 1590 (defun gptel--url-get-response (info &optional callback) 1591 "Fetch response to prompt in INFO from the LLM. 1592 1593 INFO is a plist with the following keys: 1594 - :data (the data being sent) 1595 - :buffer (the gptel buffer) 1596 - :position (marker at which to insert the response). 1597 1598 Call CALLBACK with the response and INFO afterwards. If omitted 1599 the response is inserted into the current buffer after point." 1600 (let* ((inhibit-message t) 1601 (message-log-max nil) 1602 (backend gptel-backend) 1603 (url-request-method "POST") 1604 (url-request-extra-headers 1605 (append '(("Content-Type" . "application/json")) 1606 (when-let ((header (gptel-backend-header gptel-backend))) 1607 (if (functionp header) 1608 (funcall header) header)))) 1609 (url-request-data 1610 (encode-coding-string 1611 (gptel--json-encode (plist-get info :data)) 1612 'utf-8))) 1613 ;; why do these checks not occur inside of `gptel--log'? 1614 (when gptel-log-level ;logging 1615 (when (eq gptel-log-level 'debug) 1616 (gptel--log (gptel--json-encode 1617 (mapcar (lambda (pair) (cons (intern (car pair)) (cdr pair))) 1618 url-request-extra-headers)) 1619 "request headers")) 1620 (gptel--log url-request-data "request body")) 1621 (url-retrieve (let ((backend-url (gptel-backend-url gptel-backend))) 1622 (if (functionp backend-url) 1623 (funcall backend-url) backend-url)) 1624 (lambda (_) 1625 (pcase-let ((`(,response ,http-msg ,error) 1626 (gptel--url-parse-response backend (current-buffer)))) 1627 (plist-put info :status http-msg) 1628 (when error (plist-put info :error error)) 1629 (funcall (or callback #'gptel--insert-response) 1630 response info) 1631 (kill-buffer))) 1632 nil t nil))) 1633 1634 (cl-defgeneric gptel--parse-response (backend response proc-info) 1635 "Response extractor for LLM requests. 1636 1637 BACKEND is the LLM backend in use. 1638 1639 RESPONSE is the parsed JSON of the response, as a plist. 1640 1641 PROC-INFO is a plist with process information and other context. 1642 See `gptel-curl--get-response' for its contents.") 1643 1644 (defvar url-http-end-of-headers) 1645 (defvar url-http-response-status) 1646 (defun gptel--url-parse-response (backend response-buffer) 1647 "Parse response from BACKEND in RESPONSE-BUFFER." 1648 (when (buffer-live-p response-buffer) 1649 (with-current-buffer response-buffer 1650 (when gptel-log-level ;logging 1651 (save-excursion 1652 (goto-char url-http-end-of-headers) 1653 (when (eq gptel-log-level 'debug) 1654 (gptel--log (gptel--json-encode (buffer-substring-no-properties (point-min) (point))) 1655 "response headers")) 1656 (gptel--log (buffer-substring-no-properties (point) (point-max)) 1657 "response body"))) 1658 (if-let* ((http-msg (string-trim (buffer-substring (line-beginning-position) 1659 (line-end-position)))) 1660 (response (progn (goto-char url-http-end-of-headers) 1661 (condition-case nil 1662 (gptel--json-read) 1663 (error 'json-read-error))))) 1664 (cond 1665 ;; FIXME Handle the case where HTTP 100 is followed by HTTP (not 200) BUG #194 1666 ((or (memq url-http-response-status '(200 100)) 1667 (string-match-p "\\(?:1\\|2\\)00 OK" http-msg)) 1668 (list (string-trim (gptel--parse-response backend response 1669 `(:buffer ,response-buffer 1670 :backend ,backend))) 1671 http-msg)) 1672 ((plist-get response :error) 1673 (let* ((error-data (plist-get response :error)) 1674 (error-msg (plist-get error-data :message)) 1675 (error-type (plist-get error-data :type)) 1676 (backend-name (gptel-backend-name backend))) 1677 (if (stringp error-data) 1678 (progn 1679 (message "%s error: (%s) %s" backend-name http-msg error-data) 1680 (setq error-msg (string-trim error-data))) 1681 (when (stringp error-msg) 1682 (message "%s error: (%s) %s" backend-name http-msg (string-trim error-msg))) 1683 (when error-type 1684 (setq http-msg (concat "(" http-msg ") " (string-trim error-type))))) 1685 (list nil (concat "(" http-msg ") " (or error-msg ""))))) 1686 ((eq response 'json-read-error) 1687 (list nil (concat "(" http-msg ") Malformed JSON in response.") "json-read-error")) 1688 (t (list nil (concat "(" http-msg ") Could not parse HTTP response.") 1689 "Could not parse HTTP response."))) 1690 (list nil (concat "(" http-msg ") Could not parse HTTP response.") 1691 "Could not parse HTTP response."))))) 1692 1693 (cl-defun gptel--sanitize-model (&key (backend gptel-backend) 1694 (model gptel-model) 1695 (shoosh t)) 1696 "Check if MODEL is available in BACKEND, adjust accordingly. 1697 1698 If SHOOSH is true, don't issue a warning." 1699 (let ((available (gptel-backend-models backend))) 1700 (when (stringp model) 1701 (unless shoosh 1702 (display-warning 1703 'gptel 1704 (format "`gptel-model' expects a symbol, found string \"%s\" 1705 Resetting `gptel-model' to %s" 1706 model model))) 1707 (setq gptel-model (gptel--intern model) 1708 model gptel-model)) 1709 (unless (member model available) 1710 (let ((fallback (car available))) 1711 (unless shoosh 1712 (display-warning 1713 'gptel 1714 (format (concat "Preferred `gptel-model' \"%s\" not" 1715 "supported in \"%s\", using \"%s\" instead") 1716 model (gptel-backend-name backend) fallback))) 1717 (setq-local gptel-model fallback))))) 1718 1719 ;;;###autoload 1720 (defun gptel (name &optional _ initial interactivep) 1721 "Switch to or start a chat session with NAME. 1722 1723 Ask for API-KEY if `gptel-api-key' is unset. 1724 1725 If region is active, use it as the INITIAL prompt. Returns the 1726 buffer created or switched to. 1727 1728 INTERACTIVEP is t when gptel is called interactively." 1729 (interactive 1730 (let* ((backend (default-value 'gptel-backend)) 1731 (backend-name 1732 (format "*%s*" (gptel-backend-name backend)))) 1733 (list (read-buffer 1734 "Create or choose gptel buffer: " 1735 backend-name nil ; DEFAULT and REQUIRE-MATCH 1736 (lambda (b) ; PREDICATE 1737 ;; NOTE: buffer check is required (#450) 1738 (and-let* ((buf (get-buffer (or (car-safe b) b)))) 1739 (buffer-local-value 'gptel-mode buf)))) 1740 (condition-case nil 1741 (gptel--get-api-key 1742 (gptel-backend-key backend)) 1743 ((error user-error) 1744 (setq gptel-api-key 1745 (read-passwd 1746 (format "%s API key: " backend-name))))) 1747 (and (use-region-p) 1748 (buffer-substring (region-beginning) 1749 (region-end))) 1750 t))) 1751 (with-current-buffer (get-buffer-create name) 1752 (cond ;Set major mode 1753 ((eq major-mode gptel-default-mode)) 1754 ((eq gptel-default-mode 'text-mode) 1755 (text-mode) 1756 (visual-line-mode 1)) 1757 (t (funcall gptel-default-mode))) 1758 (gptel--sanitize-model :backend (default-value 'gptel-backend) 1759 :model (default-value 'gptel-model) 1760 :shoosh nil) 1761 (unless gptel-mode (gptel-mode 1)) 1762 (goto-char (point-max)) 1763 (skip-chars-backward "\t\r\n") 1764 (if (bobp) (insert (or initial (gptel-prompt-prefix-string)))) 1765 (when interactivep 1766 (display-buffer (current-buffer) gptel-display-buffer-action) 1767 (message "Send your query with %s!" 1768 (substitute-command-keys "\\[gptel-send]"))) 1769 (current-buffer))) 1770 1771 1772 ;;; Response tweaking commands 1773 1774 (defun gptel--attach-response-history (history &optional buf) 1775 "Attach HISTORY to the next gptel response in buffer BUF. 1776 1777 HISTORY is a list of strings typically containing text replaced 1778 by gptel. BUF is the current buffer if not specified. 1779 1780 This is used to maintain variants of prompts or responses to diff 1781 against if required." 1782 (with-current-buffer (or buf (current-buffer)) 1783 (letrec ((gptel--attach-after 1784 (lambda (b e) 1785 (put-text-property b e 'gptel-history 1786 (append (ensure-list history) 1787 (get-char-property (1- e) 'gptel-history))) 1788 (remove-hook 'gptel-post-response-functions 1789 gptel--attach-after 'local)))) 1790 (add-hook 'gptel-post-response-functions gptel--attach-after 1791 nil 'local)))) 1792 1793 (defun gptel--ediff (&optional arg bounds-func) 1794 "Ediff response at point against previous gptel responses. 1795 1796 If prefix ARG is non-nil, select the previous response to ediff 1797 against interactively. 1798 1799 If specified, use BOUNDS-FUNC to compute the bounds of the 1800 response at point. This can be used to include additional 1801 context for the ediff session." 1802 (interactive "P") 1803 (when (gptel--at-response-history-p) 1804 (pcase-let* ((`(,beg . ,end) (funcall (or bounds-func #'gptel--get-bounds))) 1805 (prev-response 1806 (if arg 1807 (completing-read "Choose response variant to diff against: " 1808 (get-char-property (point) 'gptel-history) 1809 nil t) 1810 (car-safe (get-char-property (point) 'gptel-history)))) 1811 (buffer-mode major-mode) 1812 (bufname (buffer-name)) 1813 (`(,new-buf ,new-beg ,new-end) 1814 (with-current-buffer 1815 (get-buffer-create (concat bufname "-PREVIOUS-*")) 1816 (let ((inhibit-read-only t)) 1817 (erase-buffer) 1818 (delay-mode-hooks (funcall buffer-mode)) 1819 (visual-line-mode) 1820 (insert prev-response) 1821 (goto-char (point-min)) 1822 (list (current-buffer) (point-min) (point-max)))))) 1823 (unless prev-response (user-error "gptel response is additive: no changes to ediff")) 1824 (require 'ediff) 1825 (letrec ((cwc (current-window-configuration)) 1826 (gptel--ediff-restore 1827 (lambda () 1828 (when (window-configuration-p cwc) 1829 (set-window-configuration cwc)) 1830 (kill-buffer (get-buffer (concat bufname "-PREVIOUS-*"))) 1831 (kill-buffer (get-buffer (concat bufname "-CURRENT-*"))) 1832 (remove-hook 'ediff-quit-hook gptel--ediff-restore)))) 1833 (add-hook 'ediff-quit-hook gptel--ediff-restore) 1834 (apply 1835 #'ediff-regions-internal 1836 (get-buffer (ediff-make-cloned-buffer (current-buffer) "-CURRENT-*")) 1837 beg end new-buf new-beg new-end 1838 nil 1839 (list 'ediff-regions-wordwise 'word-wise nil) 1840 ;; (if (transient-arg-value "-w" args) 1841 ;; (list 'ediff-regions-wordwise 'word-wise nil) 1842 ;; (list 'ediff-regions-linewise nil nil)) 1843 ))))) 1844 1845 (defun gptel--mark-response () 1846 "Mark gptel response at point, if any." 1847 (interactive) 1848 (unless (gptel--in-response-p) (user-error "No gptel response at point")) 1849 (pcase-let ((`(,beg . ,end) (gptel--get-bounds))) 1850 (goto-char beg) (push-mark) (goto-char end) (activate-mark))) 1851 1852 (defun gptel--previous-variant (&optional arg) 1853 "Switch to previous gptel-response at this point, if it exists." 1854 (interactive "p") 1855 (pcase-let* ((`(,beg . ,end) (gptel--get-bounds)) 1856 (history (get-char-property (point) 'gptel-history)) 1857 (alt-response (car-safe history)) 1858 (offset)) 1859 (unless (and history alt-response) 1860 (user-error "No variant responses available")) 1861 (if (> arg 0) 1862 (setq history (append (cdr history) 1863 (list (buffer-substring-no-properties beg end)))) 1864 (setq 1865 alt-response (car (last history)) 1866 history (cons (buffer-substring-no-properties beg end) 1867 (nbutlast history)))) 1868 (add-text-properties 1869 0 (length alt-response) 1870 `(gptel response gptel-history ,history) 1871 alt-response) 1872 (setq offset (min (- (point) beg) (1- (length alt-response)))) 1873 (delete-region beg end) 1874 (insert alt-response) 1875 (goto-char (+ beg offset)) 1876 (pulse-momentary-highlight-region beg (+ beg (length alt-response))))) 1877 1878 (defun gptel--next-variant (&optional arg) 1879 "Switch to next gptel-response at this point, if it exists." 1880 (interactive "p") 1881 (gptel--previous-variant (- arg))) 1882 1883 (provide 'gptel) 1884 ;;; gptel.el ends here 1885 1886 ;; Local Variables: 1887 ;; bug-reference-url-format: "https://github.com/karthink/gptel/issues/%s" 1888 ;; End: