branch: externals/minuet commit 4dbbceaad4f9a72144c0a15658fa41fbcd8d261c Author: Milan Glacier <d...@milanglacier.com> Commit: Milan Glacier <d...@milanglacier.com>
initial commit. --- .gitignore | 2 + README.md | 400 ++++++++++++++ assets/minuet-completion-in-region.jpg | Bin 0 -> 101413 bytes assets/minuet-overlay.jpg | Bin 0 -> 41102 bytes minuet.el | 953 +++++++++++++++++++++++++++++++++ prompt.md | 157 ++++++ 6 files changed, 1512 insertions(+) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..d166713bc6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.tags +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000000..506847b621 --- /dev/null +++ b/README.md @@ -0,0 +1,400 @@ +- [Minuet AI](#minuet-ai) +- [Features](#features) +- [Requirements](#requirements) +- [Installation](#installation) +- [API Keys](#api-keys) +- [Selecting a Provider or Model](#selecting-a-provider-or-model) +- [System Prompt](#system-prompt) +- [Configuration](#configuration) + - [minuet-provider](#minuet-provider) + - [minuet-context-window](#minuet-context-window) + - [minuet-context-ratio](#minuet-context-ratio) + - [minuet-request-timeout](#minuet-request-timeout) + - [minuet-add-single-line-entry](#minuet-add-single-line-entry) + - [minuet-n-completions](#minuet-n-completions) +- [Provider Options](#provider-options) + - [OpenAI](#openai) + - [Claude](#claude) + - [Codestral](#codestral) + - [Gemini](#gemini) + - [OpenAI-compatible](#openai-compatible) + - [OpenAI-FIM-Compatible](#openai-fim-compatible) + +# Minuet AI + +Minuet AI: Dance with Intelligence in Your Code 💃. + +`Minuet-ai` brings the grace and harmony of a minuet to your coding process. +Just as dancers move during a minuet. + +# Features + +- AI-powered code completion +- Support for multiple AI providers (OpenAI, Claude, Gemini, Codestral, + Huggingface, and OpenAI-compatible services) +- Customizable configuration options +- Streaming support to enable completion delivery even with slower LLMs + +**With completion-in-region**: + + + +**With overlay frontend**: + + + +# Requirements + +- emacs 29+ +- plz 0.9+ +- dash +- An API key for at least one of the supported AI providers + +# Installation + +Currently you need to install from github via `package-vc` or +`straight`, or manually install this package. + +```elisp + +;; install with straight +(straight-use-package '(minuet :host github :repo "milanglacier/minuet-ai.el")) + +(use-package minuet + :init + (general-define-key + ;; use completion-in-region for completion + "M-y" #'minuet-completion-region + ;; use overlay for completion + "M-p" #'minuet-previous-suggestion ;; invoke completion or cycle to next completion + "M-n" #'minuet-next-suggestion ;; invoke completion or cycle to previous completion + "M-A" #'minuet-accept-suggestion ;; accept whole completion + "M-a" #'minuet-accept-suggestion-line ;; accept current line completion + "M-e" #'minuet-dismiss-suggestion) + + ;; if you want to enable auto suggestion. + ;; Note that you can manually invoke completions without enable minuet-auto-suggestion-mode + (add-hook 'prog-mode-hook #'minuet-auto-suggestion-mode) + + :config + (setq minuet-provider 'openai-fim-compatible) + ) +``` + +Example for Ollama + +<details> + +```elisp +(use-package minuet + :init + (general-define-key + ;; use completion-in-region for completion + "M-y" #'minuet-completion-region + ;; use overlay for completion + "M-p" #'minuet-previous-suggestion ;; invoke completion or cycle to next completion + "M-n" #'minuet-next-suggestion ;; invoke completion or cycle to previous completion + "M-A" #'minuet-accept-suggestion ;; accept whole completion + "M-a" #'minuet-accept-suggestion-line ;; accept current line completion + "M-e" #'minuet-dismiss-suggestion) + + ;; if you want to enable auto suggestion. + ;; Note that you can manually invoke completions without enable minuet-auto-suggestion-mode + (add-hook 'prog-mode-hook #'minuet-auto-suggestion-mode) + + :config + (setq minuet-provider 'openai-fim-compatible) + (plist-put minuet-openai-fim-compatible-options :end-point "http://localhost:11434/v1/completions") + ;; an arbitrary non-null environment variable as placeholder + (plist-put minuet-openai-fim-compatible-options :name "Ollama") + (plist-put minuet-openai-fim-compatible-options :api-key "TERM") + (plist-put minuet-openai-fim-compatible-options :model "qwen2.5-coder:3b") + ) +``` + +</details> + +# API Keys + +Minuet AI requires API keys to function. Set the following environment variables: + +- `OPENAI_API_KEY` for OpenAI +- `GEMINI_API_KEY` for Gemini +- `ANTHROPIC_API_KEY` for Claude +- `CODESTRAL_API_KEY` for Codestral +- `HF_API_KEY` for Huggingface +- Custom environment variable for OpenAI-compatible services (as specified in your configuration) + +**Note:** Provide the name of the environment variable to Minuet +inside the provider options, not the actual value. For instance, pass +`OPENAI_API_KEY` to Minuet, not the value itself (e.g., `sk-xxxx`). + +If using Ollama, you need to assign an arbitrary, non-null environment +variable as a placeholder for it to function. + +# Selecting a Provider or Model + +For optimal performance, consider using the `deepseek-chat` model, +which is compatible with both `openai-fim-compatible` and +`openai-compatible` providers. Alternatively, the `gemini-flash` model +offers a free and fast experience. For local LLM inference, you can +deploy either `qwen-coder` or `deepseek-coder` through ollama using +the `openai-fim-compatible` provider. + +# System Prompt + +See [prompt](./prompt.md) for the default system prompt used by `minuet` and +instructions on customization. + +Please note that the System Prompt only applies to chat-based LLMs (OpenAI, +OpenAI-Compatible, Claude, and Gemini). It does not apply to Codestral and +OpenAI-FIM-compatible models. + +# Configuration + +Below are commonly used configuration options. To view the complete +list of available settings, search for `minuet` through the +`customize` interface. + +## minuet-provider + +Set the provider you want to use for completion with minuet, available +options: `openai`, `openai-compatible`, `claude`, `gemini`, +`openai-fim-compatible`, and `codestral`. + +The default is `openai-fim-compatible` (with deepseek). + +You can use `ollama` with either `openai-compatible` or +`openai-fim-compatible` provider, depending on your model is a chat +model or code completion (FIM) model. + +## minuet-context-window + +The maximum total characters of the context before and after cursor. +This limits how much surrounding code is sent to the LLM for context. + +The default is 12800, which roughly equates to 4000 tokens after +tokenization. + +## minuet-context-ratio + +Ratio of context before cursor vs after cursor. When the total +characters exceed the context window, this ratio determines how much +context to keep before vs after the cursor. A larger ratio means more +context before the cursor will be used. The ratio should between 0 and +`1`, and default is `0.75`. + +## minuet-request-timeout + +Maximum timeout in seconds for sending completion requests. In case of +the timeout, the incomplete completion items will be delivered. The +default is `3`. + +## minuet-add-single-line-entry + +For `minuet-completion-in-region` function, Whether to create +additional single-line completion items. When non-nil and a +completion item has multiple lines, create another completion item +containing only its first line. This option has no impact for +overlay-based suggesion. + +## minuet-n-completions + +Number of completion items to request from the language model. This +number is encoded as part of the prompt for the chat LLM. Note that +when `minuet-add-single-line-entry` is true, the actual number of +returned items may exceed this value. Additionally, the LLM cannot +guarantee the exact number of completion items specified, as this +parameter serves only as a prompt guideline. The default is `3`. + +# Provider Options + +You can customize the provider options using `plist-put`, for example: + +```lisp +(with-eval-after-load 'minuet + ;; change openai model to gpt-4o + (plist-put minuet-openai-options :model "gpt-4o") + + ;; change openai-compatible provider to use fireworks + (plist-put minuet-openai-compatible-options :end-point "https://api.fireworks.ai/inference/v1/chat/completions") + (plist-put minuet-openai-compatible-options :api-key "FIREWORKS_API_KEY") + (plist-put minuet-openai-compatible-options :model "accounts/fireworks/models/llama-v3p3-70b-instruct") +) +``` + +To pass optional paramters (like `max_tokens` and `top_p`) to send to +the REST request, you can use function +`minuet-set-optional-options`: + +```lisp +(minuet-set-optional-options minuet-openai-options :max_tokens 256) +(minuet-set-optional-options minuet-openai-options :top_p 0.9) +``` + +## OpenAI + +<details> + +Below is the default value: + +```lisp +(defvar minuet-openai-options + `(:model "gpt-4o-mini" + :system + (:template minuet-default-system-template + :prompt minuet-default-prompt + :guidelines minuet-default-guidelines + :n-completions-template minuet-default-n-completion-template) + :fewshots minuet-default-fewshots + :optional nil) + "config options for Minuet OpenAI provider") + +``` + +</details> + +## Claude + +<details> + +Below is the default value: + +```lisp +(defvar minuet-claude-options + `(:model "claude-3-5-sonnet-20241022" + :max_tokens 512 + :system + (:template minuet-default-system-template + :prompt minuet-default-prompt + :guidelines minuet-default-guidelines + :n-completions-template minuet-default-n-completion-template) + :fewshots minuet-default-fewshots + :optional nil) + "config options for Minuet Claude provider") +``` + +</details> + +## Codestral + +<details> + +Codestral is a text completion model, not a chat model, so the system prompt +and few shot examples does not apply. Note that you should use the +`CODESTRAL_API_KEY`, not the `MISTRAL_API_KEY`, as they are using different +endpoint. To use the Mistral endpoint, simply modify the `end_point` and +`api_key` parameters in the configuration. + +Below is the default value: + +```lisp +(defvar minuet-codestral-options + '(:model "codestral-latest" + :end-point "https://codestral.mistral.ai/v1/fim/completions" + :api-key "CODESTRAL_API_KEY" + :optional nil) + "config options for Minuet Codestral provider") +``` + +The following configuration is not the default, but recommended to prevent +request timeout from outputing too many tokens. + +```lisp +(minuet-set-optional-options minuet-codestral-options :stop ["\n\n"]) +(minuet-set-optional-options minuet-codestral-options :max_tokens 256) +``` + +</details> + +## Gemini + +You should use the end point from Google AI Studio instead of Google +Cloud. You can get an API key via their [Google API +page](https://makersuite.google.com/app/apikey). + +<details> + +The following config is the default. + +```lisp +(defvar minuet-gemini-options + `(:model "gemini-1.5-flash-latest" + :system + (:template minuet-default-system-template + :prompt minuet-default-prompt + :guidelines minuet-default-guidelines + :n-completions-template minuet-default-n-completion-template) + :fewshots minuet-default-fewshots + :optional nil) + "config options for Minuet Gemini provider") +``` + +The following configuration is not the default, but recommended to prevent +request timeout from outputing too many tokens. You can also adjust the safety +settings following the example: + +```lisp +(minuet-set-optional-options minuet-gemini-options + :generationConfig + '(:maxOutputTokens 256 + :topP 0.9)) +(minuet-set-optional-options minuet-gemini-options + :safetySettings + [(:category "HARM_CATEGORY_DANGEROUS_CONTENT" + :threshold "BLOCK_NONE") + (:category "HARM_CATEGORY_HATE_SPEECH" + :threshold "BLOCK_NONE") + (:category "HARM_CATEGORY_HARASSMENT" + :threshold "BLOCK_NONE") + (:category "HARM_CATEGORY_SEXUALLY_EXPLICIT" + :threshold "BLOCK_NONE")]) +``` + +</details> + +## OpenAI-compatible + +Use any providers compatible with OpenAI's chat completion API. + +For example, you can set the `end_point` to +`http://localhost:11434/v1/chat/completions` to use `ollama`. + +<details> + +The following config is the default. + +```lisp +(defvar minuet-openai-fim-compatible-options + '(:model "deepseek-chat" + :end-point "https://api.deepseek.com/beta/completions" + :api-key "DEEPSEEK_API_KEY" + :name "Deepseek" + :optional nil) + "config options for Minuet OpenAI FIM compatible provider") +``` + +</details> + +## OpenAI-FIM-Compatible + +Use any provider compatible with OpenAI's completion API. This request uses the +text completion API, not chat completion, so system prompts and few-shot +examples are not applicable. + +For example, you can set the `end_point` to +`http://localhost:11434/v1/completions` to use `ollama`. + +<details> + +```lisp +(defvar minuet-openai-fim-compatible-options + '(:model "deepseek-chat" + :end-point "https://api.deepseek.com/beta/completions" + :api-key "DEEPSEEK_API_KEY" + :name "Deepseek" + :optional nil) + "config options for Minuet OpenAI FIM compatible provider") +``` + +</details> diff --git a/assets/minuet-completion-in-region.jpg b/assets/minuet-completion-in-region.jpg new file mode 100644 index 0000000000..7aff29656d Binary files /dev/null and b/assets/minuet-completion-in-region.jpg differ diff --git a/assets/minuet-overlay.jpg b/assets/minuet-overlay.jpg new file mode 100644 index 0000000000..0d9bf02cd8 Binary files /dev/null and b/assets/minuet-overlay.jpg differ diff --git a/minuet.el b/minuet.el new file mode 100644 index 0000000000..d0c7dd4b9e --- /dev/null +++ b/minuet.el @@ -0,0 +1,953 @@ +;;; minuet.el --- Code completion using LLM. -*- lexical-binding: t; -*- + +;; Author: Milan Glacier <d...@milanglacier.com> +;; Maintainer: Milan Glacier <d...@milanglacier.com> +;; Version: 0.1 +;; Package-Requires: ((emacs "29") (plz "0.9") (dash "2.19.1")) + +;;; Commentary: +;; This package implements an AI-powered code completion tool for Emacs. It +;; supports to use a variety of LLMs to generate code completions. + +;;; Code: + +(require 'plz) +(require 'dash) + +(defgroup minuet nil + "Minuet group." + :group 'applications) + + +(defcustom minuet-auto-suggestion-debounce-delay 0.2 + "Debounce delay in seconds for auto-suggestions." + :type 'number + :group 'minuet) + +(defcustom minuet-auto-suggestion-block-functions '(minuet-evil-not-insert-state-p) + "List of functions that determine whether auto-suggestions should be blocked. +Each function should return non-nil if auto-suggestions should be blocked. +If any function in this list returns non-nil, auto-suggestions will not be shown." + :type '(repeat function) + :group 'minuet) + +(defcustom minuet-auto-suggestion-throttle-delay 1.0 + "Minimum time in seconds between auto-suggestions." + :type 'number + :group 'minuet) + +(defface minuet-suggestion-face + '((t :inherit shadow)) + "Face used for displaying inline suggestions.") + + +(defvar-local minuet--current-overlay nil + "Overlay used for displaying the current suggestion.") + + +(defvar-local minuet--last-point nil + "Last known cursor position for suggestion overlay.") + +(defvar-local minuet--auto-last-point nil + "Last known cursor position for auto-suggestion.") + + +(defvar-local minuet--current-suggestions nil + "List of current completion suggestions.") + +(defvar-local minuet--current-suggestion-index 0 + "Index of currently displayed suggestion.") + +(defvar-local minuet--current-requests nil + "List of current active request processes for this buffer.") + + +(defvar-local minuet--last-auto-suggestion-time nil + "Timestamp of last auto-suggestion.") + +(defvar-local minuet--debounce-timer nil + "Timer for debouncing auto-suggestions.") + +(defvar minuet-buffer-name "*minuet*" "The basename for minuet buffers") +(defcustom minuet-provider 'openai-fim-compatible + "The provider to use for code completion. +Must be one of the supported providers: codestral, openai, claude, etc." + :type '(choice (const :tag "Codestral" codestral) + (const :tag "OpenAI" openai) + (const :tag "Claude" claude) + (const :tag "OpenAI Compatible" openai-compatible) + (const :tag "OpenAI FIM Compatible" openai-fim-compatible) + (const :tag "Gemini" gemini)) + :group 'minuet) + +(defcustom minuet-context-window 12800 + "The maximum total characters of the context before and after cursor. +This limits how much surrounding code is sent to the LLM for context." + :type 'integer + :group 'minuet) + +(defcustom minuet-context-ratio 0.75 + "Ratio of context before cursor vs after cursor. +When the total characters exceed the context window, this ratio determines +how much context to keep before vs after the cursor. A larger ratio means +more context before the cursor will be used." + :type 'float + :group 'minuet) + +(defcustom minuet-request-timeout 3 + "Maximum timeout in seconds for sending completion requests." + :type 'integer + :group 'minuet) + +(defcustom minuet-add-single-line-entry t + "Whether to create additional single-line completion items. +When non-nil and a completion item has multiple lines, create another +completion item containing only its first line." + :type 'boolean + :group 'minuet) + +(defcustom minuet-after-cursor-filter-length 15 + "Length of context after cursor used to filter completion text. +Defines the length of non-whitespace context after the cursor used to +filter completion text. Set to 0 to disable filtering. + +Example: With after_cursor_filter_length = 3 and context: +'def fib(n):\\n|\\n\\nfib(5)' (where | represents cursor position), +if the completion text contains 'fib', then 'fib' and subsequent text +will be removed. This setting filters repeated text generated by the +LLM. A large value (e.g., 15) is recommended to avoid false positives." + :type 'integer + :group 'minuet) + +(defcustom minuet-n-completions 3 + "Number of completion items to request from the language model. +This number is encoded as part of the prompt for the chat LLM. Note that +when `minuet-add-single-line-entry' is true, the actual number of +returned items may exceed this value. Additionally, the LLM cannot +guarantee the exact number of completion items specified, as this +parameter serves only as a prompt guideline." + :type 'integer + :group 'minuet) + +(defvar minuet-default-prompt + "You are the backend of an AI-powered code completion engine. Your task is to +provide code suggestions based on the user's input. The user's code will be +enclosed in markers: + +- `<contextAfterCursor>`: Code context after the cursor +- `<cursorPosition>`: Current cursor location +- `<contextBeforeCursor>`: Code context before the cursor + +Note that the user's code will be prompted in reverse order: first the code +after the cursor, then the code before the cursor. +" + "The default prompt for minuet completion") + +(defvar minuet-default-guidelines + "Guidelines: +1. Offer completions after the `<cursorPosition>` marker. +2. Make sure you have maintained the user's existing whitespace and indentation. + This is REALLY IMPORTANT! +3. Provide multiple completion options when possible. +4. Return completions separated by the marker <endCompletion>. +5. The returned message will be further parsed and processed. DO NOT include + additional comments or markdown code block fences. Return the result directly. +6. Keep each completion option concise, limiting it to a single line or a few lines. +7. Create entirely new code completion that DO NOT REPEAT OR COPY any user's existing code around <cursorPosition>." + "The default guidelines for minuet completion") + +(defvar minuet-default-n-completion-template + "8. Provide at most %d completion items." + "The default prompt for minuet for number of completions request.") + +(defvar minuet-default-system-template + "{{{:prompt}}}\n{{{:guidelines}}}\n{{{:n-completions-template}}}" + "The default template for minuet system template") + +(defvar minuet-default-fewshots + `((:role "user" + :content "# language: python +<contextAfterCursor> + +fib(5) +<contextBeforeCursor> +def fibonacci(n): + <cursorPosition>") + (:role "assistant" + :content " ''' + Recursive Fibonacci implementation + ''' + if n < 2: + return n + return fib(n - 1) + fib(n - 2) +<endCompletion> + ''' + Iterative Fibonacci implementation + ''' + a, b = 0, 1 + for _ in range(n): + a, b = b, a + b + return a +<endCompletion> +"))) + +(defvar minuet-claude-options + `(:model "claude-3-5-sonnet-20241022" + :max_tokens 512 + :system + (:template minuet-default-system-template + :prompt minuet-default-prompt + :guidelines minuet-default-guidelines + :n-completions-template minuet-default-n-completion-template) + :fewshots minuet-default-fewshots + :optional nil) + "config options for Minuet Claude provider") + +(defvar minuet-openai-options + `(:model "gpt-4o-mini" + :system + (:template minuet-default-system-template + :prompt minuet-default-prompt + :guidelines minuet-default-guidelines + :n-completions-template minuet-default-n-completion-template) + :fewshots minuet-default-fewshots + :optional nil) + "config options for Minuet OpenAI provider") + +(defvar minuet-codestral-options + '(:model "codestral-latest" + :end-point "https://codestral.mistral.ai/v1/fim/completions" + :api-key "CODESTRAL_API_KEY" + :optional nil) + "config options for Minuet Codestral provider") + +(defvar minuet-openai-compatible-options + `(:end-point "https://api.groq.com/openai/v1/chat/completions" + :api-key "GROQ_API_KEY" + :model "llama-3.3-70b-versatile" + :system + (:template minuet-default-system-template + :prompt minuet-default-prompt + :guidelines minuet-default-guidelines + :n-completions-template minuet-default-n-completion-template) + :fewshots minuet-default-fewshots + :optional nil) + "config options for Minuet OpenAI compatible provider") + +(defvar minuet-openai-fim-compatible-options + '(:model "deepseek-chat" + :end-point "https://api.deepseek.com/beta/completions" + :api-key "DEEPSEEK_API_KEY" + :name "Deepseek" + :optional nil) + "config options for Minuet OpenAI FIM compatible provider") + +(defvar minuet-gemini-options + `(:model "gemini-1.5-flash-latest" + :system + (:template minuet-default-system-template + :prompt minuet-default-prompt + :guidelines minuet-default-guidelines + :n-completions-template minuet-default-n-completion-template) + :fewshots minuet-default-fewshots + :optional nil) + ;; (:generationConfig + ;; (:stopSequences nil + ;; :maxOutputTokens 256 + ;; :topP 0.8)) + "config options for Minuet Gemini provider") + + +(defun minuet-evil-not-insert-state-p () + "Return non-nil if evil is loaded and not in insert or emacs state." + (and (bound-and-true-p evil-local-mode) + (not (or (evil-insert-state-p) + (evil-emacs-state-p))))) + +(defun minuet-set-optional-options (options key val &optional field) + "Set the value of KEY in the FIELD of OPTIONS to VAL. If FIELD is +not provided, it defaults to :optional. If VAL is nil, then +remove KEY from OPTIONS. This helper function simplifies +setting values in a two-level nested plist structure." + (let ((field (or field :optional))) + (if val + (setf (plist-get options field) + (plist-put (plist-get options field) key val)) + (setf (plist-get options field) + (map-delete (plist-get options field) key))))) + +(defun minuet--eval-value (value) + "If value is a function (either lambda or a callable symbol), eval +the function (with no argument) and return the result. Else if value +is a symbol, return its value. Else return itself." + (cond ((functionp value) (funcall value)) + ((boundp value) (symbol-value value)) + (t value))) + +(defun minuet--cancel-requests () + "Cancel all current minuet requests for this buffer." + (when minuet--current-requests + (dolist (proc minuet--current-requests) + (when (process-live-p proc) + (minuet--log (format "%s process killed" (prin1-to-string proc))) + (delete-process proc))) + (setq minuet--current-requests nil))) + +(defun minuet--cleanup-suggestion (&optional no-cancel) + "Remove the current suggestion overlay and cancel any pending requests unless NO-CANCEL is t." + (unless no-cancel + (minuet--cancel-requests)) + (when minuet--current-overlay + (delete-overlay minuet--current-overlay) + (setq minuet--current-overlay nil)) + (remove-hook 'post-command-hook #'minuet--on-cursor-moved t) + (setq minuet--last-point nil)) + +(defun minuet--cursor-moved-p () + "Check if cursor moved from last suggestion position." + (and minuet--last-point + (not (eq minuet--last-point (point))))) + +(defun minuet--on-cursor-moved () + (when (minuet--cursor-moved-p) + (minuet--cleanup-suggestion))) + +(defun minuet--display-suggestion (suggestions &optional index) + "Display suggestion from SUGGESTIONS at INDEX using an overlay at point." + ;; we only cancel requests when cursor is moved. Because the + ;; completion items may be accumulated during multiple concurrent + ;; curl requests. + (minuet--cleanup-suggestion t) + (add-hook 'post-command-hook #'minuet--on-cursor-moved nil t) + (when-let* ((suggestions suggestions) + (cursor-not-moved (not (minuet--cursor-moved-p))) + (index (or index 0)) + (total (length suggestions)) + (suggestion (nth index suggestions)) + (ov (make-overlay (point) (point)))) + (setq minuet--current-suggestions suggestions + minuet--current-suggestion-index index + minuet--last-point (point)) + (overlay-put ov 'after-string + (propertize (format "%s (%d/%d)" + suggestion + (1+ index) + total) + 'face 'minuet-suggestion-face)) + (overlay-put ov 'minuet t) + (setq minuet--current-overlay ov))) + +;;;###autoload +(defun minuet-next-suggestion () + "Cycle to next suggestion." + (interactive) + (if (and minuet--current-suggestions + minuet--current-overlay) + (let ((next-index (mod (1+ minuet--current-suggestion-index) + (length minuet--current-suggestions)))) + (minuet--display-suggestion minuet--current-suggestions next-index)) + (minuet-show-suggestion))) + +;;;###autoload +(defun minuet-previous-suggestion () + "Cycle to previous suggestion." + (interactive) + (if (and minuet--current-suggestions + minuet--current-overlay) + (let ((prev-index (mod (1- minuet--current-suggestion-index) + (length minuet--current-suggestions)))) + (minuet--display-suggestion minuet--current-suggestions prev-index)) + (funcall minuet-show-suggestion))) + +;;;###autoload +(defun minuet-show-suggestion () + "Show code suggestion using overlay at point." + (interactive) + (minuet--cleanup-suggestion) + (setq minuet--last-point (point)) + (let ((current-buffer (current-buffer)) + (available-p-fn (intern (format "minuet--%s-available-p" minuet-provider))) + (complete-fn (intern (format "minuet--%s-complete" minuet-provider))) + (context (minuet--get-context))) + (unless (funcall available-p-fn) + (minuet--log (format "Minuet provider %s is not available" minuet-provider)) + (error "Minuet provider %s is not available" minuet-provider)) + (funcall complete-fn + context + (lambda (items) + (setq items (-distinct items)) + (with-current-buffer current-buffer + (when (and items (not (minuet--cursor-moved-p))) + (minuet--display-suggestion items 0))))))) + +(defun minuet--log (message &optional message-p) + "Log minuet messages into `minuet-buffer-name'. Also print the message when MESSAGE-P is t." + (with-current-buffer (get-buffer-create minuet-buffer-name) + (goto-char (point-max)) + (insert (format "%s %s\n" message (format-time-string "%Y-%02m-%02d %02H:%02M:%02S"))) + (when message-p + (message "%s" message)))) + +(defun minuet--add-tab-comment () + (if-let* ((language-p (derived-mode-p 'prog-mode 'text-mode 'conf-mode)) + (commentstring (format "%s %%s%s" + (or (replace-regexp-in-string "^%" "%%" comment-start) "#") + (or comment-end "")))) + (if indent-tabs-mode + (format commentstring "indentation: use \t for a tab") + (format commentstring (format "indentation: use %d spaces for a tab" tab-width))) + "")) + +(defun minuet--add-language-comment () + (if-let* ((language-p (derived-mode-p 'prog-mode 'text-mode 'conf-mode)) + (mode (symbol-name major-mode)) + (mode (replace-regexp-in-string "-ts-mode" "" mode)) + (mode (replace-regexp-in-string "-mode" "" mode)) + (commentstring (format "%s %%s%s" + (or (replace-regexp-in-string "^%" "%%" comment-start) "#") + (or comment-end "")))) + (format commentstring (concat "language: " mode)) + "")) + +(defun minuet--add-single-line-entry (data) + (cl-loop + for item in data + when (stringp item) + append (list (car (split-string item "\n")) + item))) + +(defun minuet--remove-spaces (items) + "Remove trailing and leading spaces in each item in items" + ;; emacs use \\` and \\' to match the beginning/end of the string, + ;; ^ and $ are used to match bol or eol + (setq items (mapcar (lambda (x) + (if (or (equal x "") + (string-match "\\`[\s\t\n]+\\'" x)) + nil + (setq x (replace-regexp-in-string "[\s\t\n]+\\'" "" x) + x (replace-regexp-in-string "\\`[\s\t\n]+" "" x)))) + items) + items (seq-filter #'identity items)) + items) + +(defun minuet--get-context () + (let* ((point (point)) + (n-chars-before point) + (point-max (point-max)) + (n-chars-after (- point-max point)) + (context-before-cursor (buffer-substring-no-properties (point-min) point)) + (context-after-cursor (buffer-substring-no-properties point point-max))) + ;; use some heuristic to decide the context length of before cursor and after cursor + (when (>= (+ n-chars-before n-chars-after) minuet-context-window) + (cond ((< n-chars-before (* minuet-context-ratio minuet-context-window)) + ;; If the context length before cursor does not exceed the maximum + ;; size, we include the full content before the cursor. + (setq context-after-cursor + (substring context-after-cursor 0 (- minuet-context-window n-chars-before)))) + ((< n-chars-after (* (- 1 minuet-context-ratio) minuet-context-window)) + ;; if the context length after cursor does not exceed the maximum + ;; size, we include the full content after the cursor. + (setq context-before-cursor + (substring context-before-cursor (- (+ n-chars-before n-chars-after) + minuet-context-window)))) + (t + ;; at the middle of the file, use the context_ratio to determine the allocation + (setq context-after-cursor + (substring context-after-cursor 0 + (floor + (* minuet-context-window (- 1 minuet-context-ratio)))) + context-before-cursor + (substring context-before-cursor + (max 0 (- n-chars-before (floor (* minuet-context-window minuet-context-ratio))))))))) + `(:before-cursor ,context-before-cursor + :after-cursor ,context-after-cursor + :additional ,(format "%s\n%s" (minuet--add-language-comment) (minuet--add-tab-comment))))) + +(defun minuet--make-chat-llm-shot (context) + (concat + (plist-get context :additional) + "\n<contextAfterCursor>\n" + (plist-get context :after-cursor) + "\n<contextBeforeCursor>\n" + (plist-get context :before-cursor) + "<cursorPosition>")) + +(defun minuet--make-context-filter-sequence (context len) + (if-let* ((is-string (stringp context)) + (is-positive (> len 0)) + (context (replace-regexp-in-string "\\`[\s\t\n]+" "" context)) + (should-filter (>= (length context) len)) + (context (substring context 0 len)) + (context (replace-regexp-in-string "[\s\t\n]+\\'" "" context))) + context + "")) + +(defun minuet--filter-text (text sequence) + "Remove the SEQUENCE and the rest part from TEXT." + (cond + ((or (null sequence) (null text)) text) + ((equal sequence "") text) + (t + (let ((start (string-match-p (regexp-quote sequence) text))) + (if start + (substring text 0 start) + text))))) + +(defun minuet--filter-sequence-in-items (items sequence) + "For each item in ITEMS, apply `minuet--filter-text' with SEQUENCE." + (mapcar (lambda (x) (minuet--filter-text x sequence)) + items)) + +(defun minuet--filter-context-sequence-in-items (items context) + "Obtain the filter sequence from CONTEXT, and apply the filter sequence in each item in ITEMS." + (minuet--filter-sequence-in-items + items (minuet--make-context-filter-sequence + (plist-get context :after-cursor) + minuet-after-cursor-filter-length))) + +(defun minuet--stream-decode (response get-text-fn) + (setq response (split-string response "[\r]?\n")) + (let (result) + (dolist (line response) + (if-let* ((json (condition-case err + (json-parse-string + (replace-regexp-in-string "^data: " "" line) + :object-type 'plist :array-type 'list) + (error nil))) + (text (condition-case err + (funcall get-text-fn json) + (error nil)))) + (when (and (stringp text) + (not (equal text ""))) + (setq result `(,@result ,text))))) + (setq result (apply #'concat result)) + (if (equal result "") + (progn (minuet--log (format "Minuet returns no text for streaming: %s" response)) + nil) + result))) + +(defmacro minuet--make-process-stream-filter (response) + "store the data into responses which is a plain list" + `(lambda (proc text) + (funcall #'internal-default-process-filter proc text) + ;; (setq ,response (append ,response (list text))) + (push text ,response))) + +(defun minuet--stream-decode-raw (response get-text-fn) + "Decode the raw stream stored in the temp variable create by `minuet--make-process-stream-filter'" + (when-let* ((response (nreverse response)) + (response (apply #'concat response))) + (minuet--stream-decode response get-text-fn))) + +(defun minuet--handle-chat-completion-timeout (context err response get-text-fn name callback) + "Handle the timeout error for chat completion. This function will +decode and send the partial complete response to the callback,and log +the error" + (if (equal (car (plz-error-curl-error err)) 28) + (progn + (minuet--log (format "%s Request timeout" name)) + (when-let* ((result (minuet--stream-decode-raw response get-text-fn)) + (completion-items (minuet--parse-completion-itmes-default result)) + (completion-items (minuet--filter-context-sequence-in-items + completion-items + context)) + (completion-items (minuet--remove-spaces completion-items))) + (funcall callback completion-items))) + (minuet--log (format "An error occured when sending request to %s" name)) + (minuet--log err))) + +(defmacro minuet--with-temp-response (&rest body) + "Execute BODY with a temporary response collection. +This macro creates a local variable `--response--' that can be used +to collect process output within the BODY. It's designed to work in +conjunction with `minuet--make-process-stream-filter'. +The `--response--' variable is initialized as an empty list and can +be used to accumulate text output from a process. After execution, +`--response--' will contain the collected responses in reverse order." + `(let (--response--) ,@body)) + +;;;###autoload +(defun minuet-accept-suggestion () + "Accept the current overlay suggestion." + (interactive) + (when (and minuet--current-suggestions + minuet--current-overlay) + (let ((suggestion (nth minuet--current-suggestion-index + minuet--current-suggestions))) + (minuet--cleanup-suggestion) + (insert suggestion)))) + +;;;###autoload +(defun minuet-dismiss-suggestion () + "Dismiss the current overlay suggestion." + (interactive) + (minuet--cleanup-suggestion)) + +;;;###autoload +(defun minuet-accept-suggestion-line () + "Accept only the first line of the current overlay suggestion." + (interactive) + (when (and minuet--current-suggestions + minuet--current-overlay) + (let* ((suggestion (nth minuet--current-suggestion-index + minuet--current-suggestions)) + (first-line (car (split-string suggestion "\n")))) + (minuet--cleanup-suggestion) + (insert first-line)))) + +;;;###autoload +(defun minuet-completion-in-region () + "Complete code in region with LLM." + (interactive) + (let ((current-buffer (current-buffer)) + (available-p-fn (intern (format "minuet--%s-available-p" minuet-provider))) + (complete-fn (intern (format "minuet--%s-complete" minuet-provider))) + (context (minuet--get-context))) + (unless (funcall available-p-fn) + (minuet--log (format "Minuet provider %s is not available" minuet-provider)) + (error "Minuet provider %s is not available" minuet-provider)) + (funcall complete-fn + context + (lambda (items) + (with-current-buffer current-buffer + (setq items (if minuet-add-single-line-entry + (minuet--add-single-line-entry items) + items) + items (-distinct items)) + ;; When there is only one completion item, + ;; the completion-in-region function + ;; automatically inserts the text into the + ;; buffer. We want to prevent this automatic + ;; behavior to ensure users can dismiss the + ;; completion item if desired. + (when (length= items 1) + (push "" items)) + ;; close current minibuffer session, if any + (when (active-minibuffer-window) + (abort-recursive-edit)) + (completion-in-region (point) (point) items)))))) + +(defun minuet--check-env-var (env-var) + (when-let ((var (getenv env-var))) + (not (equal var "")))) + +(defun minuet--codestral-available-p () + (minuet--check-env-var (plist-get minuet-codestral-options :api-key))) + +(defun minuet--openai-available-p () + (minuet--check-env-var "OPENAI_API_KEY")) + +(defun minuet--claude-available-p () + (minuet--check-env-var "ANTHROPIC_API_KEY")) + +(defun minuet--openai-compatible-available-p () + (when-let* ((options minuet-openai-compatible-options) + (env-var (plist-get options :api-key)) + (end-point (plist-get options :end-point)) + (model (plist-get options :model))) + (minuet--check-env-var env-var))) + +(defun minuet--openai-fim-compatible-available-p () + (when-let* ((options minuet-openai-fim-compatible-options) + (env-var (plist-get options :api-key)) + (name (plist-get options :name)) + (end-point (plist-get options :end-point)) + (model (plist-get options :model))) + (minuet--check-env-var env-var))) + +(defun minuet--gemini-available-p () + (minuet--check-env-var "GEMINI_API_KEY")) + +(defun minuet--parse-completion-itmes-default (items) + (split-string items "<endCompletion>")) + +(defun minuet--make-system-prompt (template &optional n-completions) + (let* ((tmpl (plist-get template :template)) + (tmpl (minuet--eval-value tmpl)) + (n-completions (or n-completions minuet-n-completions 1)) + (n-completions-template (plist-get template :n-completions-template)) + (n-completions-template (minuet--eval-value n-completions-template)) + (n-completions-template (if (stringp n-completions-template) + (format n-completions-template (or minuet-n-completions 1)) + ""))) + (setq tmpl (replace-regexp-in-string "{{{:n-completions-template}}}" + n-completions-template + tmpl)) + (map-do + (lambda (k v) + (setq v (minuet--eval-value v)) + (when (and (not (equal k :template)) + (not (equal k :n-completions-template)) + (stringp v)) + (setq tmpl + (replace-regexp-in-string + (concat "{{{" (symbol-name k) "}}}") + v + tmpl)))) + template) + ;; replace placeholders that are not replaced + (setq tmpl (replace-regexp-in-string "{{{.*}}}" + "" + tmpl)) + tmpl + )) + +(defun minuet--openai-fim-complete-base (options get-text-fn context callback) + (let ((total-try (or minuet-n-completions 1)) + (name (plist-get options :name)) + completion-items) + (dotimes (_ total-try) + (minuet--with-temp-response + (push + (plz 'post (plist-get options :end-point) + :headers `(("Content-Type" . "application/json") + ("Accept" . "application/json") + ("Authorization" . ,(concat "Bearer " (getenv (plist-get options :api-key))))) + :timeout minuet-request-timeout + :body (json-serialize `(,@(plist-get options :optional) + :stream t + :model ,(plist-get options :model) + :prompt ,(format "%s\n%s" + (plist-get context :additional) + (plist-get context :before-cursor)) + :suffix ,(plist-get context :after-cursor))) + :as 'string + :filter (minuet--make-process-stream-filter --response--) + :then + (lambda (json) + (when-let ((result (minuet--stream-decode json get-text-fn))) + ;; insert the current result into the completion items list + (push result completion-items)) + (setq completion-items (minuet--filter-context-sequence-in-items + completion-items + context)) + (setq completion-items (minuet--remove-spaces completion-items)) + (funcall callback completion-items)) + :else + (lambda (err) + (if (equal (car (plz-error-curl-error err)) 28) + (progn + (minuet--log (format "%s Request timeout" name)) + (when-let ((result (minuet--stream-decode-raw --response-- get-text-fn))) + (push result completion-items))) + (minuet--log (format "An error occured when sending request to %s" name)) + (minuet--log err)) + (setq completion-items + (minuet--filter-context-sequence-in-items + completion-items + context)) + (setq completion-items (minuet--remove-spaces completion-items)) + (funcall callback completion-items))) + minuet--current-requests))))) + +(defun minuet--codestral-complete (context callback) + (minuet--openai-fim-complete-base + (plist-put (copy-tree minuet-codestral-options) :name "Codestral") + #'minuet--openai-get-text-fn + context + callback)) + +(defun minuet--openai-fim-compatible-complete (context callback) + (minuet--openai-fim-complete-base + (copy-tree minuet-openai-fim-compatible-options) + #'minuet--openai-fim-get-text-fn + context + callback)) + +(defun minuet--openai-fim-get-text-fn (json) + (--> json + (plist-get it :choices) + car + (plist-get it :text))) + +(defun minuet--openai-get-text-fn (json) + (--> json + (plist-get it :choices) + car + (plist-get it :delta) + (plist-get it :content))) + +(defun minuet--openai-complete-base (options context callback) + (minuet--with-temp-response + (push + (plz 'post (plist-get options :end-point) + :headers `(("Content-Type" . "application/json") + ("Accept" . "application/json") + ("Authorization" . ,(concat "Bearer " (getenv (plist-get options :api-key))))) + :timeout minuet-request-timeout + :body (json-serialize `(,@(plist-get options :optional) + :stream t + :model ,(plist-get options :model) + :messages ,(vconcat + `((:role "system" + :content ,(minuet--make-system-prompt (plist-get options :system))) + ,@(minuet--eval-value (plist-get options :fewshots)) + (:role "user" + :content ,(minuet--make-chat-llm-shot context)))))) + :as 'string + :filter (minuet--make-process-stream-filter --response--) + :then + (lambda (json) + (when-let* ((result (minuet--stream-decode json #'minuet--openai-get-text-fn)) + (completion-items (minuet--parse-completion-itmes-default result)) + (completion-items (minuet--filter-context-sequence-in-items + completion-items + context)) + (completion-items (minuet--remove-spaces completion-items))) + ;; insert the current result into the completion items list + (funcall callback completion-items))) + :else + (lambda (err) + (minuet--handle-chat-completion-timeout + context err --response-- #'minuet--openai-get-text-fn "OpenAI" callback))) + minuet--current-requests))) + +(defun minuet--openai-complete (context callback) + (minuet--openai-complete-base + (--> (copy-tree minuet-openai-options) + (plist-put it :end-point "https://api.openai.com/v1/chat/completions") + (plist-put it :api-key "OPENAI_API_KEY")) + context callback)) + +(defun minuet--openai-compatible-complete (context callback) + (minuet--openai-complete-base + (copy-tree minuet-openai-compatible-options) context callback)) + +(defun minuet--claude-get-text-fn (json) + (--> json + (plist-get it :delta) + (plist-get it :text))) + +(defun minuet--claude-complete (context callback) + (minuet--with-temp-response + (push + (plz 'post "https://api.anthropic.com/v1/messages" + :headers `(("Content-Type" . "application/json") + ("Accept" . "application/json") + ("x-api-key" . ,(getenv "ANTHROPIC_API_KEY")) + ("anthropic-version" . "2023-06-01")) + :timeout minuet-request-timeout + :body (json-serialize (let ((options (copy-tree minuet-claude-options))) + `(,@(plist-get options :optional) + :stream t + :model ,(plist-get options :model) + :system ,(minuet--make-system-prompt (plist-get options :system)) + :max_tokens ,(plist-get options :max_tokens) + :messages ,(vconcat + `(,@(minuet--eval-value (plist-get options :fewshots)) + (:role "user" + :content ,(minuet--make-chat-llm-shot context))))))) + :as 'string + :filter (minuet--make-process-stream-filter --response--) + :then + (lambda (json) + (when-let* ((result (minuet--stream-decode json #'minuet--claude-get-text-fn)) + (completion-items (minuet--parse-completion-itmes-default result)) + (completion-items (minuet--filter-context-sequence-in-items + completion-items + context)) + (completion-items (minuet--remove-spaces completion-items))) + ;; insert the current result into the completion items list + (funcall callback completion-items))) + :else + (lambda (err) + (minuet--handle-chat-completion-timeout + context err --response-- #'minuet--claude-get-text-fn "Claude" callback))) + minuet--current-requests))) + +(defun minuet--gemini-get-text-fn (json) + (--> json + (plist-get it :candidates) + car + (plist-get it :content) + (plist-get it :parts) + car + (plist-get it :text))) + +(defun minuet--gemini-complete (context callback) + (minuet--with-temp-response + (push + (plz 'post (format "https://generativelanguage.googleapis.com/v1beta/models/%s:streamGenerateContent?alt=sse&key=%s" + (plist-get minuet-gemini-options :model) + (getenv "GEMINI_API_KEY")) + :headers `(("Content-Type" . "application/json") + ("Accept" . "application/json")) + :timeout minuet-request-timeout + :body (json-serialize + (let* ((options (copy-tree minuet-gemini-options)) + (fewshots (minuet--eval-value (plist-get options :fewshots))) + (fewshots (mapcar + (lambda (shot) + `(:role + ,(if (equal (plist-get shot :role) "user") "user" "model") + :parts + [(:text ,(plist-get shot :content))])) + fewshots))) + `(,@(plist-get options :optional) + :system_instruction (:parts (:text ,(minuet--make-system-prompt (plist-get options :system)))) + :contents ,(vconcat + `(,@fewshots + (:role "user" + :parts [(:text ,(minuet--make-chat-llm-shot context))])))))) + :as 'string + :filter (minuet--make-process-stream-filter --response--) + :then + (lambda (json) + (when-let* ((result (minuet--stream-decode json #'minuet--gemini-get-text-fn)) + (completion-items (minuet--parse-completion-itmes-default result)) + (completion-items (minuet--filter-context-sequence-in-items + completion-items + context)) + (completion-items (minuet--remove-spaces completion-items))) + (funcall callback completion-items))) + :else + (lambda (err) + (minuet--handle-chat-completion-timeout + context err --response-- #'minuet--gemini-get-text-fn "Gemini" callback))) + minuet--current-requests))) + + +(defun minuet--setup-auto-suggestion () + "Setup auto-suggestion with post-command-hook." + (add-hook 'post-command-hook #'minuet--maybe-show-suggestion nil t)) + +(defun minuet--maybe-show-suggestion () + "Show suggestion with debouncing and throttling." + (when (or (null minuet--last-auto-suggestion-time) + (> (float-time (time-since minuet--last-auto-suggestion-time)) + minuet-auto-suggestion-throttle-delay)) + (when minuet--debounce-timer + (cancel-timer minuet--debounce-timer)) + (setq minuet--debounce-timer + (run-with-timer + minuet-auto-suggestion-debounce-delay nil + (lambda () + (when (and (eq (window-buffer (selected-window)) (current-buffer)) + (or (null minuet--auto-last-point) + (not (eq minuet--auto-last-point (point)))) + (not (run-hook-with-args-until-success 'minuet-auto-suggestion-block-functions))) + (setq minuet--last-auto-suggestion-time (current-time) + minuet--auto-last-point (point)) + (minuet-show-suggestion))))))) + +(defun minuet--cleanup-auto-suggestion () + "Clean up auto-suggestion timers and hooks." + (remove-hook 'post-command-hook #'minuet--maybe-show-suggestion t) + (when minuet--debounce-timer + (cancel-timer minuet--debounce-timer) + (setq minuet--debounce-timer nil)) + (setq minuet--auto-last-point nil)) + +;;;###autoload +(define-minor-mode minuet-auto-suggestion-mode + "Toggle automatic code suggestions. +When enabled, Minuet will automatically show suggestions while you type." + :init-value nil + :lighter " Minuet" + :group 'minuet + (if minuet-auto-suggestion-mode + (minuet--setup-auto-suggestion) + (minuet--cleanup-auto-suggestion))) + +(provide 'minuet) +;;; minuet.el ends here diff --git a/prompt.md b/prompt.md new file mode 100644 index 0000000000..cf88c044b2 --- /dev/null +++ b/prompt.md @@ -0,0 +1,157 @@ +# Default Template + +`{{{:prompt}}}\n{{{:guidelines}}}\n{{{:n_completion_template}}}` + +# Default Prompt + +You are the backend of an AI-powered code completion engine. Your task is to +provide code suggestions based on the user's input. The user's code will be +enclosed in markers: + +- `<contextAfterCursor>`: Code context after the cursor +- `<cursorPosition>`: Current cursor location +- `<contextBeforeCursor>`: Code context before the cursor + +Note that the user's code will be prompted in reverse order: first the code +after the cursor, then the code before the cursor. + +# Default Guidelines + +Guidelines: + +1. Offer completions after the `<cursorPosition>` marker. +2. Make sure you have maintained the user's existing whitespace and indentation. + This is REALLY IMPORTANT! +3. Provide multiple completion options when possible. +4. Return completions separated by the marker `<endCompletion>`. +5. The returned message will be further parsed and processed. DO NOT include + additional comments or markdown code block fences. Return the result directly. +6. Keep each completion option concise, limiting it to a single line or a few lines. +7. Create entirely new code completion that DO NOT REPEAT OR COPY any user's + existing code around `<cursorPosition>`. + +# Default `:n_completions` template + +8. Provide at most %d completion items. + +# Default Few Shots Examples + +```lisp +`((:role "user" + :content "# language: python +<contextAfterCursor> + +fib(5) +<contextBeforeCursor> +def fibonacci(n): + <cursorPosition>") + (:role "assistant" + :content " ''' + Recursive Fibonacci implementation + ''' + if n < 2: + return n + return fib(n - 1) + fib(n - 2) +<endCompletion> + ''' + Iterative Fibonacci implementation + ''' + a, b = 0, 1 + for _ in range(n): + a, b = b, a + b + return a +<endCompletion> +")) +``` + +# Customization + +You can customize the `:template` by encoding placeholders within +triple braces. These placeholders will be interpolated using the +corresponding key-value pairs from the table. The value can be a +function that takes no argument and returns a string, or a symbol +whose value is a string. + +Here's a simplified example for illustrative purposes (not intended for actual +configuration): + +```lisp +(setq my-minuet-simple-template "{{{:assistant}}}\n{{{:role}}}") +(setq my-minuet-simple-role "you are also a computer scientist") +(defun my-simple-assistant-prompt () "" "you are a helpful assistant.") + +(plist-put + minuet-openai-options + :system + '(:template my-minuet-simple-template ; note: you do not need the comma , for interpolation + :assistant my-simple-assistant-prompt + :role my-minuet-simple-role)) +``` + +Note that `:n_completion_template` is a special placeholder as it contains one +`%d` which will be encoded with `minuet-n-completions`, if you want to +customize this template, make sure your prompt also contains only one `%d`. + +Similarly, `:fewshots` can be a plist in the following form or a function that +takes no argument and returns a plist in the following form: + +```lisp +`((:role "user" + :content "# language: python +<contextAfterCursor> + +fib(5) +<contextBeforeCursor> +def fibonacci(n): + <cursorPosition>") + (:role "assistant" + :content " ''' + Recursive Fibonacci implementation + ''' + if n < 2: + return n + return fib(n - 1) + fib(n - 2) +<endCompletion> + ''' + Iterative Fibonacci implementation + ''' + a, b = 0, 1 + for _ in range(n): + a, b = b, a + b + return a +<endCompletion> +")) +``` + +Below is an example to configure the prompt based on major mode: + +```lisp +(defun my-minuet-few-shots () + (if (derived-mode-p 'js-mode) + (list '(:role "user" + :content "// language: javascript +<contextAfterCursor> + +fib(5) +<contextBeforeCursor> +function fibonacci(n) { + <cursorPosition>") + '(:role "assistant" + :content " // Recursive Fibonacci implementation + if (n < 2) { + return n; + } + return fibonacci(n - 1) + fibonacci(n - 2); +<endCompletion> + // Iterative Fibonacci implementation + let a = 0, b = 1; + for (let i = 0; i < n; i++) { + [a, b] = [b, a + b]; + } + return a; +<endCompletion> +")) + minuet-default-fewshots)) + +(plist-put minuet-openai-options :fewshots #'my-minuet-few-shots) +```