dotfiles/emacs/.emacs.d/config/init-ai.el

742 lines
33 KiB
EmacsLisp

;; -*- lexical-binding: t; -*-
(defvar ai-map (make-sparse-keymap))
(define-key leader-map "a" (cons "ai" ai-map))
(use-package gptel
:straight (:type git :host github :repo "karthink/gptel" :branch "feature-tool-use")
:commands (gptel-request)
:autoload (gptel--strip-mode-suffix)
:init
;; Annoyingly, gptel fails to require gptel-context, so we have to do it manually
(autoload 'gptel-context-add "gptel-context")
(autoload 'gptel-context-add-file "gptel-context")
(setq gptel-directives
`((default . ,(lambda ()
(let ((msg "You are a large language model living in Emacs and a helpful assistant. Respond concisely."))
(if (and gptel-use-tools gptel-tools)
(concat msg " Only use tools when strictly necessary.")
msg))))
(shell-command . "You are a command line helper. Generate shell commands that do what is requested, without any additional description or explanation. Reply in plain text with no Markdown or other syntax. Reply with the command only.")
(code-review . "You are a code reviewer. Provide feedback on the code snippet below. Highlight any issues, suggest improvements, and provide explanations for your suggestions. Respond in plain text with no Markdown or other syntax.")))
:config
;; Default to creating a new buffer when invoking gptel
(defun gptel-before-advice (name &optional _ initial interactivep)
(interactive
(let* ((backend (default-value 'gptel-backend))
(backend-name
(generate-new-buffer-name (format "*%s*" (gptel-backend-name backend)))))
(list (read-buffer "Create or choose gptel buffer: "
backend-name nil ; DEFAULT and REQUIRE-MATCH
(lambda (b) ; PREDICATE
(buffer-local-value 'gptel-mode
(get-buffer (or (car-safe b) b)))))
(condition-case nil
(gptel--get-api-key
(gptel-backend-key backend))
((error user-error)
(setq gptel-api-key
(read-passwd
(format "%s API key: " backend-name)))))
(and (use-region-p)
(buffer-substring (region-beginning)
(region-end)))
t))))
(advice-add 'gptel :before #'gptel-before-advice)
(defvar gptel-backend-openai (gptel-make-openai "ChatGPT"
:key #'gptel-api-key-from-auth-source
:stream t
:models '(gpt-4o
gpt-4o-mini
o1
o1-mini)))
(defvar gptel-backend-gemini (gptel-make-gemini "Gemini"
:stream t
:key (password-store-get "gemini-api-key")))
(defun ollama-models ()
(if (executable-find "ollama")
(->> (shell-command-to-string "ollama list")
(s-lines)
(cdr)
(mapcar (lambda (line)
(let ((parts (s-split-up-to " " line 1 t)))
(car parts))))
(-filter #'s-present?))
'("llama3.1:latest"
"mistral-nemo:latest"
"gemma2:2b"
"tinyllama:latest")))
(defvar gptel-backend-ollama (gptel-make-ollama "Ollama"
:stream t
:models (ollama-models)))
(defvar gptel-backend-anthropic (gptel-make-anthropic "Claude"
:stream t
:key (password-store-get "anthropic-api-key")))
;; Azure setup
(when (file-exists-p (expand-file-name "~/.azure/msal_token_cache.json"))
(defcustom gptel-azure-host nil
"The host for gptel to connect to Azure-hosted OpenAI models."
:type 'string)
(defcustom gptel-azure-deployment "gpt-4o"
"The Azure OpenAI deployment name to use for gptel."
:type 'string)
(defcustom gptel-azure-openai-version "2024-10-01-preview"
"The OpenAI API version in Azure to use for gptel.")
(defun get-azure-bearer-token ()
"Extract the access token for cognitiveservices.azure.com from the MSAL token cache JSON. Run `az login' to populate the cache."
(let* ((json-object-type 'hash-table)
(json-array-type 'list)
(json-key-type 'string)
(data (json-read-file (expand-file-name "~/.azure/msal_token_cache.json")))
(access-tokens (gethash "AccessToken" data)))
(cl-loop for key being the hash-keys of access-tokens
for token-data = (gethash key access-tokens)
if (and (string-match-p (rx "https://cognitiveservices.azure.com/.default")
(gethash "target" token-data))
(gethash "secret" token-data))
return (gethash "secret" token-data))))
(defun make-gptel-backend-azure ()
(gptel-make-azure "Azure OpenAI"
:protocol "https"
:stream t
:host gptel-azure-host
:endpoint (format "/openai/deployments/%s/chat/completions?api-version=%s"
gptel-azure-deployment gptel-azure-openai-version)
:key ""
:header `(("Authorization" . ,(format "Bearer %s" (get-azure-bearer-token))))
:models '("gpt-4o")))
(defvar gptel-backend-azure (make-gptel-backend-azure))
(defun gptel-reauth-azure ()
(interactive)
(setq gptel-backend-azure (make-gptel-backend-azure))))
(setq gptel-backend gptel-backend-anthropic
gptel-model 'claude-3-5-sonnet-20241022)
(defun gptel-select-backend (backend)
(interactive (list (cl-loop
for (name . backend) in gptel--known-backends
nconc (cl-loop for model in (gptel-backend-models backend)
collect (list (concat name ":" (format "%s" model)) backend model))
into models-alist finally return
(cdr (assoc (completing-read "Backend: " models-alist nil t)
models-alist)))))
(setq gptel-backend (car backend)
gptel-model (cadr backend)))
(defvar tavily-search-api-key (password-store-get "tavily-api-key")
"API key for accessing the Tavily Search API.")
(defun tavily-search-query (query &optional topic)
"Perform a web search using the Tavily Search API with the given QUERY."
(let* ((url-request-method "POST")
(url "https://api.tavily.com/search")
(url-request-extra-headers '(("Content-Type" . "application/json")))
(url-request-data (json-encode `(("query" . ,query)
("api_key" . ,tavily-search-api-key)
,@(when topic `("topic" . ,topic))))))
(with-current-buffer (url-retrieve-synchronously url)
(goto-char (point-min))
(when (re-search-forward "^$" nil 'move)
(buffer-substring-no-properties (point) (point-max))))))
(defun gptel-preview-code-execution (code mode &optional prompt)
"Preview CODE in a buffer using MODE before execution.
PROMPT is an optional custom confirmation message."
(let ((preview-buffer (get-buffer-create "*Code Preview*")))
(with-current-buffer preview-buffer
(erase-buffer)
(funcall mode)
(insert code)
(display-buffer preview-buffer)
(if (yes-or-no-p (or prompt "Execute this code? "))
(progn
(with-selected-window (get-buffer-window preview-buffer)
(quit-window))
t)
(progn
(with-selected-window (get-buffer-window preview-buffer)
(quit-window))
nil)))))
(defvar execute-shell-command-tool
(gptel-make-tool
:name "execute_shell_command"
:function (lambda (callback command)
(make-process
:name "gptel-shell"
:buffer (generate-new-buffer " *gptel-shell*")
:command (list shell-file-name shell-command-switch command)
:sentinel (lambda (proc _event)
(let ((result (with-current-buffer (process-buffer proc)
(buffer-string))))
(kill-buffer (process-buffer proc))
(funcall callback result)))))
:description "Execute a shell command and return the result"
:args (list '(:name "command"
:type "string"
:description "The shell command to execute"))
:confirm t
:async t
:category "shell"))
(defvar read-url-tool
(gptel-make-tool
:name "read_url"
:function (lambda (callback url)
(url-retrieve url
(lambda (_status)
(goto-char (point-min))
(forward-paragraph)
(let ((dom (libxml-parse-html-region (point) (point-max))))
(kill-buffer)
(with-temp-buffer
(shr-insert-document dom)
(funcall callback (buffer-substring-no-properties
(point-min)
(point-max))))))))
:description "Fetch and read the contents of a URL"
:args (list '(:name "url"
:type "string"
:description "The URL to read"))
:async t
:category "web"))
(defvar open-url-for-user-tool
(gptel-make-tool
:name "open_url_for_user"
:function (lambda (url)
(browse-url url))
:description "Open a URL for the user to view. Note: this will open the URL for the user. If you want to read the contents of the URL, use the 'read_url' tool."
:args (list '(:name "url"
:type "string"
:description "The URL to open"))
:category "web"))
(defvar web-search-tool
(gptel-make-tool
:name "web_search"
:function (lambda (callback query)
(let* ((url-request-method "POST")
(url "https://api.tavily.com/search")
(url-request-extra-headers '(("Content-Type" . "application/json")))
(url-request-data (json-encode `(("query" . ,query)
("api_key" . ,tavily-search-api-key)))))
(url-retrieve url
(lambda (_status)
(goto-char (point-min))
(re-search-forward "^$" nil 'move)
(funcall callback
(buffer-substring-no-properties (point) (point-max)))))))
:description "Perform a web search"
:args (list '(:name "query"
:type "string"
:description "The search query string"))
:async t
:category "web"))
(defvar news-search-tool
(gptel-make-tool
:name "news_search"
:function (lambda (callback query)
(let* ((url-request-method "POST")
(url "https://api.tavily.com/search")
(url-request-extra-headers '(("Content-Type" . "application/json")))
(url-request-data (json-encode `(("query" . ,query)
("api_key" . ,tavily-search-api-key)
("topic" . "news")))))
(url-retrieve url
(lambda (_status)
(goto-char (point-min))
(re-search-forward "^$" nil 'move)
(funcall callback
(buffer-substring-no-properties (point) (point-max)))))))
:description "Search for current events/news"
:args (list '(:name "query"
:type "string"
:description "The search query string"))
:async t
:category "web"))
(defvar emacs-symbol-help-tool
(gptel-make-tool
:name "emacs_symbol_help"
:function (lambda (symbol)
(let ((symbol (if (stringp symbol)
(intern symbol)
symbol)))
(if (fboundp symbol)
(documentation symbol)
(documentation-property symbol 'variable-documentation))))
:description "Retrieve the help documentation for an Emacs Lisp symbol running in the current Emacs process"
:args (list '(:name "symbol"
:type "string"
:description "The symbol to retrieve the help documentation for"))
:category "emacs"))
(defvar read-buffer-tool
(gptel-make-tool
:name "read_buffer"
:function (lambda (buffer)
(unless (buffer-live-p (get-buffer buffer))
(error "Error: buffer %s is not live" buffer))
(with-current-buffer buffer
(buffer-substring-no-properties (point-min) (point-max))))
:description "Return the contents of an Emacs buffer"
:args (list '(:name "buffer"
:type "string"
:description "The name of the buffer whose contents are to be retrieved"))
:category "emacs"))
(defvar evaluate-elisp-tool
(gptel-make-tool
:name "evaluate_elisp"
:function (lambda (elisp-code)
(condition-case err
(if (gptel-preview-code-execution elisp-code
'emacs-lisp-mode
"Execute this Emacs Lisp code? ")
(eval (read elisp-code))
(format "%s\n\nResult: code execution cancelled" elisp-code))
(error (format "Error: %s" (error-message-string err)))))
:description "Evaluate arbitrary Emacs Lisp code in the current Emacs process"
:args (list '(:name "elisp_code"
:type "string"
:description "The Emacs Lisp code to evaluate"))
:category "emacs"))
(defvar evaluate-python-tool
(gptel-make-tool
:name "evaluate_python"
:function (lambda (callback code)
(let ((temp-file (make-temp-file "gptel-python-" nil ".py")))
(if (gptel-preview-code-execution code 'python-mode)
(progn
(with-temp-file temp-file
(insert code))
(make-process
:name "gptel-python"
:buffer (generate-new-buffer " *gptel-python*")
:command (list "docker" "run" "--rm"
"-v" (concat temp-file ":/code.py")
"jupyter/scipy-notebook"
"python" "/code.py")
:sentinel (lambda (proc _event)
(let ((result (with-current-buffer (process-buffer proc)
(buffer-string))))
(delete-file temp-file)
(kill-buffer (process-buffer proc))
(funcall callback result)))))
(progn
(when (file-exists-p temp-file)
(delete-file temp-file))
(funcall callback
(format "%s\n\nResult: code execution cancelled" code))))))
:description "Evaluate Python code in a sandboxed Docker container with scientific computing libraries"
:args (list '(:name "code"
:type "string"
:description "The Python code to evaluate"))
:include t
:async t
:category "programming"))
(defvar append-to-buffer-tool
(gptel-make-tool
:name "append_to_buffer"
:function (lambda (buffer text)
(with-current-buffer (get-buffer-create buffer)
(save-excursion
(goto-char (point-max))
(insert text)))
(format "Appended text to buffer %s" buffer))
:description "Append text to the an Emacs buffer. If the buffer does not exist, it will be created."
:args (list '(:name "buffer"
:type "string"
:description "The name of the buffer to append text to.")
'(:name "text"
:type "string"
:description "The text to append to the buffer."))
:category "emacs"))
(defvar echo-message-tool
(gptel-make-tool
:name "echo_message"
:function (lambda (text)
(message "%s" text)
(format "Message sent: %s" text))
:description "Send a message to the *Messages* buffer"
:args (list '(:name "text"
:type "string"
:description "The text to send to the messages buffer"))
:category "emacs"))
(defvar open-file-in-emacs-tool
(gptel-make-tool
:name "open_file_in_emacs"
:function (lambda (file)
(find-file (expand-file-name file))
(format "%s opened in Emacs" file))
:description "Open a file in the current Emacs process"
:args (list '(:name "file"
:type "string"
:description "The file to open"))
:category "emacs"))
(defvar list-directory-tool
(gptel-make-tool
:name "list_directory"
:function (lambda (directory)
(mapconcat #'identity
(directory-files directory)
"\n"))
:description "List the contents of a given directory"
:args (list '(:name "directory"
:type "string"
:description "The path to the directory to list"))
:category "filesystem"))
(defvar read-file-tool
(gptel-make-tool
:name "read_file"
:function (lambda (filepath)
(with-temp-buffer
(insert-file-contents (expand-file-name filepath))
(buffer-string)))
:description "Read and display the contents of a file"
:args (list '(:name "filepath"
:type "string"
:description "Path to the file to read. Supports relative paths and ~."))
:category "filesystem"))
(defvar make-directory-tool
(gptel-make-tool
:name "make_directory"
:function (lambda (parent name)
(condition-case nil
(progn
(make-directory (expand-file-name name parent) t)
(format "Directory %s created/verified in %s" name parent))
(error (format "Error creating directory %s in %s" name parent))))
:description "Create a new directory with the given name in the specified parent directory"
:args (list '(:name "parent"
:type "string"
:description "The parent directory where the new directory should be created, e.g. /tmp")
'(:name "name"
:type "string"
:description "The name of the new directory to create, e.g. testdir"))
:category "filesystem"))
(defvar create-file-tool
(gptel-make-tool
:name "create_file"
:function (lambda (path filename content)
(let ((full-path (expand-file-name filename path)))
(with-temp-buffer
(insert content)
(write-file full-path))
(format "Created file %s in %s" filename path)))
:description "Create a new file with the specified content"
:args (list '(:name "path"
:type "string"
:description "The directory where to create the file")
'(:name "filename"
:type "string"
:description "The name of the file to create")
'(:name "content"
:type "string"
:description "The content to write to the file"))
:category "filesystem"))
(defvar domainr-rapidapi-key (password-store-get "rapidapi-api-key")
"API key for accessing the Domainr API through RapidAPI (mashape-key).")
(defvar domainr-registrar "namecheap.com"
"The domain registrar to use for domain registration.")
(defvar search-domain-tool
(gptel-make-tool
:name "search_domain"
:function (lambda (callback query)
(unless domainr-rapidapi-key
(error "domainr-rapidapi-key is not set"))
(let* ((url (format "https://domainr.p.rapidapi.com/v2/search?mashape-key=%s&query=%s"
domainr-rapidapi-key
(url-encode-url query)))
(url-request-extra-headers
'(("Content-Type" . "application/json"))))
(url-retrieve url
(lambda (_status)
(goto-char (point-min))
(re-search-forward "^$" nil 'move)
(funcall callback
(buffer-substring-no-properties (point) (point-max)))))))
:description "Search for available domain names based on a query"
:args (list '(:name "query"
:type "string"
:description "The search query for domain names"))
:async t
:category "web"))
(defvar check-domain-status-tool
(gptel-make-tool
:name "check_domain_status"
:function (lambda (callback domain)
(unless domainr-rapidapi-key
(error "domainr-rapidapi-key is not set"))
(let* ((url (format "https://domainr.p.rapidapi.com/v2/status?mashape-key=%s&domain=%s"
domainr-rapidapi-key
(url-encode-url domain)))
(url-request-extra-headers
'(("Content-Type" . "application/json"))))
(url-retrieve url
(lambda (_status)
(goto-char (point-min))
(re-search-forward "^$" nil 'move)
(funcall callback
(buffer-substring-no-properties (point) (point-max)))))))
:description "Check the availability status of a specific domain name"
:args (list '(:name "domain"
:type "string"
:description "The domain name to check"))
:async t
:category "web"))
(defvar register-domain-tool
(gptel-make-tool
:name "register_domain"
:function (lambda (callback domain)
(unless domainr-rapidapi-key
(error "domainr-rapidapi-key is not set"))
(let* ((url (format "https://domainr.p.rapidapi.com/v2/register?mashape-key=%s&domain=%s&registrar=%s"
domainr-rapidapi-key
(url-encode-url domain)
domainr-registrar))
(url-automatic-caching nil))
(url-retrieve url
(lambda (status)
(goto-char (point-min))
(when-let ((redirect (plist-get status :redirect)))
(browse-url-default-browser (string-trim redirect))
(funcall callback
(format "Opened registration page for %s" domain)))))))
:description "Open the registration page for a domain name in the default browser"
:args (list '(:name "domain"
:type "string"
:description "The domain name to register"))
:async t
:category "web"))
(defvar gptel-toolsets
(list :default (list read-url-tool
open-url-for-user-tool
web-search-tool
emacs-symbol-help-tool
evaluate-python-tool)
:empty nil
:news (list read-url-tool
open-url-for-user-tool
web-search-tool
news-search-tool)
:business (list read-url-tool
open-url-for-user-tool
web-search-tool
news-search-tool
search-domain-tool
check-domain-status-tool
register-domain-tool)
:coding-assistant (list execute-shell-command-tool
read-url-tool
open-url-for-user-tool
web-search-tool
emacs-symbol-help-tool
read-buffer-tool
evaluate-elisp-tool
evaluate-python-tool
append-to-buffer-tool
open-file-in-emacs-tool
list-directory-tool
read-file-tool
make-directory-tool
create-file-tool))
"Plist of names to sets of gptel tools")
(defun gptel-toolset (name)
(plist-get gptel-toolsets name))
(defun gptel-select-toolset-name ()
(let* ((toolsets (->> gptel-toolsets
(-partition 2)
(-map #'car)
(-map (lambda (name-kw)
(cons (s-chop-prefix ":"
(symbol-name name-kw))
name-kw)))))
(selected (completing-read "Toolset: " toolsets)))
(alist-get selected toolsets nil nil #'equal)))
(defun gptel-select-toolset (name)
(interactive (list (gptel-select-toolset-name)))
(setq gptel-tools (gptel-toolset name)))
(defmacro gptel-with-toolset (name &rest body)
(declare (indent 1))
`(let ((gptel-tools (gptel-toolset ,name)))
,@body))
(gptel-select-toolset :default)
:general
("C-c RET" #'gptel-send
"C-c C-<return>" #'gptel-menu)
(ai-map
"g" #'gptel
"o" #'gptel-org
"s" #'gptel-send
"m" #'gptel-menu
"b" #'gptel-chat-with-buffer
"f" #'gptel-format
"B" #'gptel-select-backend
"a" #'gptel-context-add
"F" #'gptel-context-add-file
"k" #'gptel-abort
"t" #'gptel-select-toolset))
(use-package gptel-quick
:straight (:type git :host github :repo "karthink/gptel-quick")
:commands gptel-quick
:config
(defun gptel-quick-around-advice (orig-fun &rest args)
(gptel-with-toolset :empty
(apply orig-fun args)))
(advice-add 'gptel-quick :around #'gptel-quick-around-advice)
:general
(embark-general-map
"?" #'gptel-quick)
(ai-map
"?" #'gptel-quick))
(use-package aimenu
:straight `(:local-repo ,(expand-file-name "packages/aimenu" user-emacs-directory) :type nil)
:defer t
:config
(setq aimenu-gptel-backend gptel-backend-openai
aimenu-gptel-model 'gpt-4o-mini)
:general
(ai-map "i" #'aimenu))
(defun gptel-commit-message ()
"Generate a commit message via gptel."
(interactive)
(unless git-commit-mode
(user-error "Not in a git commit buffer!"))
(let* ((diff-buf (magit-get-mode-buffer 'magit-diff-mode))
(diff (with-current-buffer diff-buf
(buffer-substring-no-properties
(point-min)
;; Skip the last line, which is just the [back] button
(save-excursion
(goto-char (point-max))
(forward-line -1)
(point)))))
(prompt (format "%s\n\nWrite a clear, concise commit message for this diff. The first line should succinctly summarize the changes made and should be no more than 50 characters. If additional context is needed, include it in an additional paragraph separate by a blank line from the first line. Do not use the word 'enhance' or talk about the user experience. Be specific. Reply in plain text with no Markdown or other syntax. Reply with the commit message only." diff)))
(message "Generating commit message...")
(gptel-with-toolset :empty
(gptel-request prompt
:stream t
:system "You are a professional software engineer."))))
(defun gptel-pr-review (pr)
"Review a pull request via gptel."
(interactive (list (forge-get-pullreq
(forge-read-pullreq "Review PR: "))))
(let ((diff (forge-pullreq-diff pr))
(name (generate-new-buffer-name (format "*PR review: %s*" (oref pr title))))
(prompt "Review the changes in this pull request. Highlight any issues, suggest improvements, and provide explanations for your suggestions. Keep your feedback specific and concise."))
(gptel name nil diff t)
(with-current-buffer name
(goto-char (point-max))
(newline)
(insert (gptel-prompt-prefix-string))
(insert (format " %s" prompt)))))
(with-eval-after-load 'git-commit
(keymap-set git-commit-mode-map "C-c RET" #'gptel-commit-message))
(defvar comfy-ui-path (expand-file-name "~/ComfyUI")
"Path to ComfyUI source repository.")
(defvar comfy-ui-command (list "pipenv" "run" "python" "main.py")
"Command to run ComfyUI server.")
(defvar-local comfy-ui--url nil
"URL for this buffer's ComfyUI process.")
(defun comfy-ui-process-filter (proc string)
(when-let ((match (s-match "To see the GUI go to: \\(.*\\)" string)))
(with-current-buffer (process-buffer proc)
(setq comfy-ui--url (nth 1 match))
(let ((browse-url-browser-function #'browse-url-default-browser))
(browse-url comfy-ui--url))))
(when (buffer-live-p (process-buffer proc))
(with-current-buffer (process-buffer proc)
(let ((moving (= (point) (process-mark proc))))
(save-excursion
;; Insert the text, advancing the process marker.
(goto-char (process-mark proc))
(insert string)
(set-marker (process-mark proc) (point)))
(if moving (goto-char (process-mark proc)))))))
(defun comfy-ui ()
"Launch Comfy UI in a subprocess and opens the web UI."
(interactive)
(unless (file-exists-p (expand-file-name (f-join comfy-ui-path "main.py")))
(user-error "Could not find ComfyUI installation!"))
(if-let ((proc (get-process "comfy-ui")))
(with-current-buffer (process-buffer proc)
(browse-url comfy-ui--url))
(with-temp-buffer
(cd comfy-ui-path)
(make-process :name "comfy-ui"
:buffer "*ComfyUI*"
:command comfy-ui-command
:filter #'comfy-ui-process-filter))))
(defvar ollama-copilot-proxy-port 11435
"Port for the Ollama Copilot proxy server.")
(defvar ollama-copilot-model "codellama:code"
"Model for the Ollama Copilot proxy server.")
(defun ollama-copilot-ensure ()
"Start the Ollama Copilot proxy server if it's not already running."
(let ((proc-name "ollama-copilot"))
(unless (get-process proc-name)
(unless (executable-find "ollama-copilot")
(user-error "Could not find ollama-copilot executable!"))
(make-process :name proc-name
:buffer (format "*%s*" proc-name)
:command `("ollama-copilot"
"-proxy-port" ,(format ":%s" ollama-copilot-proxy-port)
"-model" ,ollama-copilot-model)))))
(defvar ollama-copilot--proxy-cache nil
"Internal variable to cache the old proxy value.")
(define-minor-mode ollama-copilot-mode
"Minor mode to use ollama-copilot as a local Copilot proxy."
:global t
(require 'copilot)
(if ollama-copilot-mode
(progn
(ollama-copilot-ensure)
(setq ollama-copilot--proxy-cache copilot-network-proxy)
(setq copilot-network-proxy `(:host "127.0.0.1"
:port ,ollama-copilot-proxy-port
:rejectUnauthorized :json-false))
(copilot-diagnose))
(setq copilot-network-proxy ollama-copilot--proxy-cache)
(copilot-diagnose)))
(provide 'init-ai)