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**:
+
+![example-completion-in-region](./assets/minuet-completion-in-region.jpg)
+
+**With overlay frontend**:
+
+![example-overlay](./assets/minuet-overlay.jpg)
+
+# 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)
+```

Reply via email to