diff --git a/README.org b/README.org index 5ad336e..a41ed10 100644 --- a/README.org +++ b/README.org @@ -59,7 +59,7 @@ You can create a Sourcegraph access token at https://sourcegraph.com/users/yourn (setopt cody-workspace-root "/your/path/to/some/project/dir") ; optional :config (defalias 'cody-start 'cody-login)) -#+end_src +#+end_src * Available commands @@ -68,3 +68,94 @@ Autocompletion is the main use case for now. Chat is just a stub. - `M-x cody-login` will start Cody up and apply it to applicable buffers. - `M-x cody-logout` shuts Cody down. - `M-x cody-mode` toggles Cody on and off in a given buffer. + +* PREVIEW gptel support + +We have an experimental backend for [[https://github.com/karthink/gptel][gptel]]. + +#+begin_src emacs-lisp + (require 'gptel-cody) + + ;; Set Cody as the default backend + (setq gptel-backend + (gptel-make-cody "Cody" + :key "sgp_ADD_DOTCOM_TOKEN_HERE")) + + ;; Or use against enterprise instance + (setq gptel-backend + (gptel-make-cody "Cody" + :host "sourcegraph.sourcegraph.com" + :key "sgp_TOKEN_HERE")) + + ;; Specify models to avoid request against server to fetch list of models + (setq gptel-backend (gptel-make-cody "Cody" + :host "sourcegraph.sourcegraph.com" + :key "sgp_TOKEN_HERE" + :models '("anthropic::2023-06-01::claude-3.5-sonnet"))) +#+end_src + +You can have multiple cody accounts by giving the backends different names. IE +replace the word "Cody" with the name you want to refer to it by. + +** DONE ensure model selection works +Right now I think I am just hardcoding it at request time. + +As follow-up need to handle errors querying the backend to find out which +models are allowed by the logged in user. + +** DONE cody system prompt +** DONE make it possible to change instance (test against s2) +** DONE model list from backend? +I think we have this sort of functionality, but I do see a lot of hardcoding +in the cody client. + +This should be possible, since I just tried with a random model on my free +account and got this response from the backend +: the requested chat model is not available ("openai/gpt-4o", onProTier=false, hasFeatureFlags=false) +** TODO remote prompts + +#+begin_src graphql + query ViewerPrompts($query: String!) { + prompts(query: $query, first: 100, viewerIsAffiliated: true, orderBy: PROMPT_NAME_WITH_OWNER) { + nodes { + id + name + nameWithOwner + owner { + namespaceName + } + description + draft + definition { + text + } + url + } + totalCount + pageInfo { + hasNextPage + endCursor + } + } + } +#+end_src + +** TODO handle empty text + +I don't know if this is a bug in gptel or strictness on Cody's side. But I set +a directive without having inline prose, and we ended up having a final bit of +the transcript being + +#+begin_example + { + "speaker": "human", + "text": "" + } +#+end_example + +This lead to a failure from the server. +** TODO see if we can plug into same auth source +Maybe also parse vscode state? Right now it is all in a sqlite file. +** TODO enhanced context for enterprise +[[file:context-discussion.md][context-discussions.md]] +** TODO handle errors from backend diff --git a/context-discussion.md b/context-discussion.md new file mode 100644 index 0000000..6ad44b7 --- /dev/null +++ b/context-discussion.md @@ -0,0 +1,155 @@ +### does gptel make it possible to extend its context sources. Right now in its transient menu it offers adding a file or a buffer. I want to do a remote API call to retrieve context. Is that possible to do? + +Yes, gptel does make it possible to extend its context sources, and you can add a remote API call to retrieve context. While this functionality isn't directly exposed in the transient menu, you can extend gptel's context capabilities by: + +1. Creating a new function to fetch context from your remote API. +2. Adding this context to gptel's context system. +3. Optionally, adding a new option to the transient menu. + +Here's how you could approach this: + +1. Create a function to fetch and add context from your API: + +```elisp +(defun my-gptel-add-api-context () + "Fetch context from a remote API and add it to gptel's context." + (interactive) + (let ((api-response (my-fetch-from-api))) ; Replace with your API call + (with-temp-buffer + (insert api-response) + (gptel-context-add)))) +``` + +2. The `gptel-context-add` function (aliased as `gptel-add`) is flexible and can work with different types of input. By calling it in a temporary buffer with the API response, you're effectively adding the API response as context. + +3. If you want to add this to the transient menu, you can advise the `gptel-menu` function to add your new option: + +```elisp +(defun my-gptel-menu-add-api-option (orig-fun &rest args) + "Advice to add API context option to gptel-menu." + (let ((transient-current-prefix 'gptel-menu)) + (transient-append-suffix 'gptel-menu "Context" + '("a" "Add API context" my-gptel-add-api-context)))) + +(advice-add 'gptel-menu :around #'my-gptel-menu-add-api-option) +``` + +This will add a new option "a" to add API context in the "Context" section of the gptel menu. + +4. If you want more control over how the API context is formatted or stored, you can modify the `gptel-context--alist` directly: + +```elisp +(defun my-gptel-add-api-context () + "Fetch context from a remote API and add it to gptel's context." + (interactive) + (let ((api-response (my-fetch-from-api))) + (push (cons "API Context" api-response) gptel-context--alist))) +``` + +5. You might also want to modify the `gptel-context--wrap-default` function or set a custom `gptel-context-wrap-function` to handle your new context type appropriately. + +By implementing these extensions, you can effectively add a remote API as a context source for gptel, making it available through both programmatic calls and the transient menu interface. + +### I want to process the prompt by the user and inspect things like the current repository the user is in to decide what context to include when "enhanced" context is on. Adjust your response to account for that. + +Certainly! Your requirement to process the user's prompt and inspect the current environment (like the Git repository) to decide on context is a great use case for enhancing gptel's functionality. Here's an adjusted approach that takes this into account: + +1. First, let's create a function to gather enhanced context: + +```elisp +(defun my-gptel-gather-enhanced-context (prompt) + "Gather enhanced context based on PROMPT and current environment." + (let ((context "")) + ;; Check if we're in a Git repository + (when (vc-git-root default-directory) + (setq context (concat context "Current Git repository: " + (vc-git-root default-directory) "\n"))) + + ;; Analyze the prompt for keywords + (cond + ((string-match-p "\\bfile\\b" prompt) + (setq context (concat context "Current file: " (buffer-file-name) "\n"))) + ((string-match-p "\\bproject\\b" prompt) + (setq context (concat context "Project root: " + (project-root (project-current)) "\n"))) + ((string-match-p "\\bfunction\\b" prompt) + (setq context (concat context "Current function: " + (which-function) "\n")))) + + ;; You could add more conditions here based on the prompt or environment + + context)) +``` + +2. Now, let's modify gptel's context wrapping function to include this enhanced context: + +```elisp +(defun my-gptel-context-wrap-function (message contexts) + "Wrap MESSAGE with CONTEXTS and enhanced context." + (let* ((enhanced-context (my-gptel-gather-enhanced-context message)) + (original-context (gptel-context--string contexts)) + (combined-context (concat enhanced-context original-context))) + (if (> (length combined-context) 0) + (pcase gptel-use-context + ('system (concat message "\n\n" combined-context)) + ('user (concat combined-context "\n\n" message)) + ('nil message)) + message))) + +(setq gptel-context-wrap-function #'my-gptel-context-wrap-function) +``` + +3. To make this "enhanced" context toggleable, we can add a new option to gptel's menu: + +```elisp +(defvar my-gptel-use-enhanced-context nil + "Whether to use enhanced context in gptel queries.") + +(defun my-gptel-toggle-enhanced-context () + "Toggle enhanced context for gptel." + (interactive) + (setq my-gptel-use-enhanced-context (not my-gptel-use-enhanced-context)) + (message "Enhanced context %s" + (if my-gptel-use-enhanced-context "enabled" "disabled"))) + +(defun my-gptel-menu-add-enhanced-context-option (orig-fun &rest args) + "Advice to add enhanced context toggle to gptel-menu." + (let ((transient-current-prefix 'gptel-menu)) + (transient-append-suffix 'gptel-menu "Context" + '("e" "Toggle enhanced context" my-gptel-toggle-enhanced-context)))) + +(advice-add 'gptel-menu :around #'my-gptel-menu-add-enhanced-context-option) +``` + +4. Finally, update the context wrap function to respect this toggle: + +```elisp +(defun my-gptel-context-wrap-function (message contexts) + "Wrap MESSAGE with CONTEXTS and enhanced context if enabled." + (let* ((enhanced-context (if my-gptel-use-enhanced-context + (my-gptel-gather-enhanced-context message) + "")) + (original-context (gptel-context--string contexts)) + (combined-context (concat enhanced-context original-context))) + (if (> (length combined-context) 0) + (pcase gptel-use-context + ('system (concat message "\n\n" combined-context)) + ('user (concat combined-context "\n\n" message)) + ('nil message)) + message))) +``` + +This approach allows you to: + +1. Gather enhanced context based on the user's prompt and current environment. +2. Integrate this enhanced context with gptel's existing context system. +3. Toggle the use of enhanced context via the gptel menu. +4. Process each prompt to decide what context to include when enhanced context is enabled. + +You can further expand the `my-gptel-gather-enhanced-context` function to include more sophisticated analysis of the prompt or to make API calls if necessary. This setup provides a flexible framework for adding intelligent, context-aware enhancements to gptel queries. + + + + + + diff --git a/gptel-cody.el b/gptel-cody.el new file mode 100644 index 0000000..5de9547 --- /dev/null +++ b/gptel-cody.el @@ -0,0 +1,188 @@ +;;; gptel-cody.el --- Cody support for gptel -*- lexical-binding: t; -*- + +;; Copyright (C) 2024 Sourcegraph + +;; Author: Keegan Carruthers-Smith +;; Version: 0.0.1-alpha +;; Package-Requires: ((emacs "27.1") (gptel "0.9.0")) +;; Keywords: convenience +;; URL: https://github.com/sourcegraph/gptel-cody + +;; SPDX-License-Identifier: Apache-2.0 + +;;; Commentary: +;; This file adds support for the Cody API to gptel + +;;; Code: +(require 'gptel) + +(defconst gptel-cody--client-name "Cody-Emacs-gptel" + "The client name for Cody API requests.") + +(defconst gptel-cody--version + (package-get-version) + "The version of gptel-cody package.") + +(defconst gptel-cody--user-agent + (format "%s/%s" gptel-cody--client-name gptel-cody--version) + "The User-Agent string for Cody API requests.") + +(cl-defstruct (gptel-cody (:constructor gptel--make-cody) + (:copier nil) + (:include gptel-backend))) + +(cl-defmethod gptel-cody-fetch-models-async ((backend gptel-cody)) + "Asynchronously fetch models for the Cody backend and update the models slot." + (let* ((host (gptel-backend-host backend)) + (url (format "https://%s/.api/modelconfig/supported-models.json" host)) + (url-request-method "GET") + (url-request-extra-headers + (funcall (gptel-backend-header backend)))) + (url-retrieve + url + (lambda (status) + (if-let ((err (plist-get status :error))) + (message "Error fetching Cody models: %s" err) + (goto-char (point-min)) + (re-search-forward "\n\n") + (let* ((json-object-type 'alist) + (json-array-type 'list) + (json-key-type 'symbol) + (response (json-read)) + (default-chat-model (cdr (assoc 'chat (cdr (assoc 'defaultModels response))))) + (all-models (cdr (assoc 'models response))) + (chat-models (seq-filter + (lambda (model) + (and (member "chat" (cdr (assoc 'capabilities model))) + (not (string-match-p "fast-edit" (cdr (assoc 'modelRef model)))))) + all-models)) + (chat-models-names (mapcar (lambda (model) (cdr (assoc 'modelRef model))) chat-models)) + (chat-models-names (cons default-chat-model (remove default-chat-model chat-models-names)))) + (setf (gptel-cody-models backend) chat-models-names) + (message "Updated %s models: %s" (gptel-backend-name backend) chat-models-names)))) + nil t t))) + +(cl-defmethod gptel-curl--parse-stream ((_backend gptel-cody) _info) + "Parse Cody's streaming response." + (let ((content-strs)) + (condition-case nil + (while (re-search-forward "^data:" nil t) + (when-let* ((response (gptel--json-read)) + (delta (plist-get response :deltaText))) + (push delta content-strs))) + (error + (goto-char (match-beginning 0)))) + (apply #'concat (nreverse content-strs)))) + +(cl-defmethod gptel--parse-response ((_backend gptel-cody) response _info) + "Parse Cody's RESPONSE." + (plist-get response :deltaText)) + +(cl-defmethod gptel--request-data ((_backend gptel-cody) prompts) + "Prepare REQUEST-DATA for Cody API." + `(:model ,gptel-model + :messages ,(vconcat prompts) + :maxTokensToSample ,(or gptel-max-tokens 4000) + :temperature ,(or gptel-temperature 0) + :topK -1 + :topP -1 + :stopSequences [""] + :stream ,(or (and gptel-stream gptel-use-curl + (gptel-backend-stream gptel-backend)) + :json-false))) + +(cl-defmethod gptel--parse-buffer ((_backend gptel-cody) &optional max-entries) + "Parse current buffer backwards from point and return a list of prompts. + +MAX-ENTRIES is the number of queries/responses to include for context." + (let ((prompts) (prop)) + (while (and + (or (not max-entries) (>= max-entries 0)) + (setq prop (text-property-search-backward + 'gptel 'response + (when (get-char-property (max (point-min) (1- (point))) + 'gptel) + t)))) + (push (list :speaker (if (prop-match-value prop) "assistant" "human") + :text + (string-trim + (buffer-substring-no-properties (prop-match-beginning prop) + (prop-match-end prop)) + (format "[\t\r\n ]*\\(?:%s\\)?[\t\r\n ]*" + (regexp-quote (gptel-prompt-prefix-string))) + (format "[\t\r\n ]*\\(?:%s\\)?[\t\r\n ]*" + (regexp-quote (gptel-response-prefix-string))))) + prompts) + (and max-entries (cl-decf max-entries))) + (cons (list :speaker "system" + :text gptel--system-message) + prompts))) + +(cl-defun gptel-cody-add-directive () + "Add the Cody directive to the end of `gptel-directives` if missing." + (unless (assq 'cody gptel-directives) + (customize-set-variable 'gptel-directives + (append gptel-directives + '((cody . "You are Cody, an AI coding assistant from Sourcegraph. If your answer contains fenced code blocks in Markdown, include the relevant full file path in the code block tag using this structure: ```$LANGUAGE:$FILEPATH```")))))) + +;;;###autoload +(cl-defun gptel-make-cody + (name &key + (host "sourcegraph.com") + (protocol "https") + (endpoint "/.api/completions/stream") + (stream t) + (models nil) + (key 'gptel-api-key) + (header (lambda () + (when-let (key-resolved (gptel--get-api-key key)) + `(("Authorization" . ,(concat "token " key-resolved)) + ("User-Agent" . ,gptel-cody--user-agent))))) + curl-args) + "Create a Cody API backend for gptel. + +NAME is a string to identify this backend. + +Keyword arguments: + +CURL-ARGS (optional) is a list of additional curl arguments. + +HOST (optional) is the API host, defaults to \"sourcegraph.com\". + +MODELS is a list of available model names. + +STREAM is a boolean to toggle streaming responses, defaults to t. + +PROTOCOL (optional) specifies the protocol, https by default. + +ENDPOINT (optional) is the API endpoint for completions. + +HEADER (optional) is for additional headers to send with each +request. It should be an alist or a function that returns an +alist. + +KEY (optional) is a variable whose value is the API key, or +function that returns the key." + (gptel-cody-add-directive) + (declare (indent 1)) + (let ((backend (gptel--make-cody + :curl-args curl-args + :name name + :host host + :header header + :key key + :models (or models (if (string= host "sourcegraph.com") + '("anthropic/claude-3-5-sonnet-20240620") + '("anthropic::2023-06-01::claude-3.5-sonnet"))) + :protocol protocol + :endpoint endpoint + :stream stream + :url (concat protocol "://" host endpoint "?api-version=2&client-name=" gptel-cody--client-name "&client-version=" gptel-cody--version)))) + ;; using default models, so fetch from remote. + (unless models (gptel-cody-fetch-models-async backend)) + (setf (alist-get name gptel--known-backends + nil nil #'equal) + backend))) + +(provide 'gptel-cody) +;;; gptel-cody.el ends here