Skip to content

Anoncheg1/emacs-cui

Repository files navigation

https://github.com/Anoncheg1/emacs-cui/workflows/melpazoid/badge.svg https://melpa.org/packages/cui-badge.svg https://github.com/Anoncheg1/emacs-cui/workflows/melpazoid-release/badge.svg https://stable.melpa.org/packages/cui-badge.svg

CUI - chat user interface to LLMs and agents in Org blocks.

Version: 0.3.2 Emacs package, provide block #+begin_cui in Org mode for communication with LLMs APIs. Inspired by Robert Krahn’s org-ai package

Protocol: “OpenAI like” HTTP REST API. (url.el)

Philosophy: stateless, one-direction for text, extensibility and comfort.

Features:

  • Tags and links expansion of @ tags, Org [] links
  • Noweb and tangling support as if it is src block of Org mode
  • Parallel requests in the same or different buffer without blocking
  • Highlighting of markdown big blocks, tags and links, headers, bold text, Org tables, LaTeX
  • Request with image or audio like @/a.jpg or [[/a.wav]] or [[file:/a.jpg]]
  • Xref jumping For Elisp Markdown blocks
  • Transparency for inspecting raw data of request, powerful debugger
  • Customization and autofilling, hooks
  • Prompt engineering tools, there is :chain of calls out-of-the-box
  • No external dependencies

Screenshot

  • At left - optional debugging window
  • At center - main window with agent answer.
  • At right - optional help window with expanded link.

https://raw.githubusercontent.com/Anoncheg1/public-share/main/oai.png

Prerequisites

Emacs 29.1+, 30.2

Token API for remote LLMs.

Optional: org-links, available at MELA and at https://github.com/Anoncheg1/emacs-org-links

Files

FilePurpose
cui.elMinor mode and C-c C-c activation of #+begin_cui
cui-restapi.elConnection to REST API (OpenAI-compatible)
cui-block.elHandling cui block
cui-block-msgs.elfor chat messages
cui-block-tags.elExpand tags and links in block
cui-timers.elManaging requests, buffers and notifications
cui-prompt.el(optional) Working with prompt-engineering, :chain parameter
cui-async1.el(optional) Solve “callback hell” for url.el, used by cui-prompt.el
cui-optional.el(optional) Hooks functions for postprocessing

structure

cui.el
 ├─ cui-block.el
 ├─ cui-block-msgs.el (→ cui-block.el)
 ├─ cui-block-tags.el (→ cui-block.el, cui-block-msgs.el)
 ├─ cui-restapi.el (→ cui-block.el, cui-block-msgs.el, cui-async1.el, cui-timers.el)
 └─ cui-prompt.el (→ cui-block.el, cui-block-msgs.el, cui-block-tags.el, cui-restapi.el, cui-async1.el, cui-timers.el)

Supported services *

  • remote: any server that provides an OpenAI-compliant endpoint to LLM model or agent.
  • local inference engines like llama.cpp, vLLM, huggingface, etc.

cui-restapi-con-service ./cui-restapi.el

[*] In practice all LLMs are not free, remote are considered as SaaSS (Service as a Software Substitute). These systems perform remote computation, not provide access to data, model weights, code, or licensing rights needed to reproduce the model.

Installation

Installation - from MELPA

  1. add to .emacs
(require 'package)
(add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/") t)
(add-to-list 'package-archives '("melpa-stable" . "https://stable.melpa.org/packages/") t)
(package-initialize)
  1. Install via M-x package-install RET cui RET or M-x package-list-packages

Configuration

Connection

cui-restapi-con-token - “token-string” or token per service: '(:together "token-string1" :local "token-string2") or nil for reading from “~/.authinfo” or “~/.authinfo.gpg”

cui-restapi-con-service - default service symbol if not specified in header of cui block like this: #+begin_cui :service local

(add-to-list 'load-path "path/to/cui")
(require 'cui)
(add-hook 'org-mode-hook #'cui-mode) ; cui.el
(setq cui-restapi-con-token "xxx") ; cui-restapi.el

How to connect to not supported service?

(plist-put cui-restapi-con-endpoints :local "http://localhost:8000/v1/chat/completions")
;; Optionally, set it as default
(setq cui-restapi-con-service 'local)

Redefine:

  • cui-restapi--get-headers - If you need some special HTTP headers.
  • cui-restapi--get-single-response-text and cui-restapi--normalize-response - if HTTP responses is not standard for "stream":false and "stream":true respectively.

Fontification configuration

By default fintified only major markdown elements: “#headers”, quotes, LaTeX, links&tags.

To enable all Org fintifications in cui blocks:

(setq org-protecting-blocks (delete "cui" org-protecting-blocks))
(setq org-protecting-blocks (delete "ai" org-protecting-blocks))
(org-restart-font-lock)

To disable all fontifications:

(setopt cui-fontification-flag nil)

or

(advice-add 'cui--add-cui-font-lock-to-org-keywords :override (lambda () t))

Auto-fill

To set use custom function or disable auto-fill by setting it to nil:

M-x customize-variable RET cui-restapi-fill-function

Key-Token security approaches to keep it safe

  1. Simplies way to set in .emacs (same with “M-x customize”):
(setq cui-restapi-con-token "xxx")
  1. Next level is to keep token in separate Elisp file:

~/.emacs.d/cui-tokens.el

(setq cui-restapi-con-token "xxx")

.emacs

(add-to-list 'load-path "~/.emacs.d/cui-tokens.el")
(require 'cui-tokens)
  1. Keep token in separate ”auth-source” file in plain text

To get more infomration use Shell commnd:

$ info auth-source

Secret should be stored in the format:

machine local password <your token>

or

machine local--0 password <your token>
machine local--1 password <your token>
  1. Keep token in encrypted “auth-source” file. Full decription will be provided later…

Optional hooks

This hooks add space “ “ before lines that in cui block that looks like Org header, that fix confustion for Org logic.

(add-hook 'cui-block-after-chat-insertion-hook #'cui-optional-remove-distant-empty-lines-hook-function)

This hook remove empty lines if there is too much of them in response.

(add-hook 'cui-block-after-chat-insertion-hook #'cui-optional-remove-headers-hook-function)

Optional Markdown folding

We provide cui-optional-markdown-cycle function same as org-cycle. It allow to fold and unfold markdown headers in cui block.

To set for Org mode, add those lines to your ~/.emacs file:

(require 'cui-optional)
(add-hook 'org-mode-hook #'cui-optional-markdown-folding-activation)
(add-hook 'org-tab-first-hook #'cui-optional-markdown-cycle) ; For TAB key on "# headers"

If you use https://github.com/Anoncheg1/emacs-indent and indent-for-tab-command for TAB key instead of org-cycle:

(require 'cui-optional)
(add-hook 'org-mode-hook #'cui-optional-markdown-folding-activation)
(defun cui-org-mode-hook ()
  (setq-local indent-for-tab-steps
              (list
               #'indent-for-tab-step-1-region-to-column
               #'indent-for-tab-step-2-region-fill-prefix
               #'indent-for-tab-step-3-region-indent-lines
               #'cui-optional-markdown-cycle ; toggle Markdown header in cui block
               #'indent-for-tab-step-4-insert-tab
               #'indent-for-tab-step-5-indent-line
               #'indent-for-tab-step-6-completion)))

(add-hook 'org-mode-hook 'cui-org-mode-hook)

To cycle block use command M-x cui-optional-cycle-block or add this advice to make it with default “Shift-TAB” key:

(advice-add 'org-shifttab :around #'cui-optional-markdown-folding-shifttab-advice)

Supported “[ROLE]:” chat prefixes

  • SYS-EVERYWHERE
  • SYS - role: system
  • AI - role: assistant
  • AI_REASON - excluded
  • ME - role: user

See cui-block-roles-prefixes in .cui-block.el for more info.

Supported parameteres for #+begin_cui

  • :model - string, ex: :model “meta-llama/Llama-3.3-70B-Instruct-Turbo-Free”
  • :service - label/string, ex: :service deepseek–1
  • :max-tokens - int, ex: :max-tokens 300
  • :stream - bool, ex: :stream nil
  • :top-p - float, ex: :top-p 12.45
  • :temperature - float
  • :frequency-penalty - float
  • :presence-penalty - float
  • :sys-everywhere - string (persistant-sys-prompts) ./cui-block.el
  • :chat :completion - bool
  • :chain - bool ./cui-prompt.el

Debugging

To get full request body and more information set:

(setopt cui-debug-buffer "*debug-cui*")
(setq debug-on-error t) ; optional
(ert-enabled t) ; debug output to stdout for ert tests

Built-in url.el have ability to output request and resonse headers, for that you need to set

(setq url-debug '(http))

Extension guide

Hooks for post-processing of LLM resonse

cui-block-msgs-after-prepare-messages-hook for multimodal switching

Here we check that :content of the last user message is not just a string but a list, this happen when image link exist in body of ia block.

(defun my/cui-hook-multimodal-image-swith (plist)
  (let* ((messages (plist-get plist :content ))
         (idx (cui-block-msgs--find-last-user-index messages))
         (last-user-mes (when messages (aref messages idx)))
         (content (when last-user-mes (plist-get last-user-mes :content ))))
         (when (and (stringp content)
                    (string-match cui-restapi--multimodal-internal-link-re content))
         (plist-put plist :service "localmultimodal")
          (plist-put plist :model nil)))
  plist)

(add-hook 'cui-block-msgs-after-prepare-messages-hook #'my/cui-hook-multimodal-image-swith)

cui-block-after-chat-insertion-hook

Hook accpet two arguments (par1 par2)

type
simbol \=’role, \=’text or’end.
role-text
text or role name.
pos
position before text insertion.
stream
stream mode or a single insertion.

There is two implementations for hook in cui-optional.el:

  • cui-optional-remove-distant-empty-lines-hook-function
  • cui-optional-remove-headers-hook-function

Example to remove empty lines after LLM answer:

(require 'cui-optional) ; for `cui-optional-remove-distant-empty-lines'

(defun my/cui-postprocess (type _content _pos _stream)
    (when (equal type 'end)
      (save-excursion
        (let* ((context (cui-block-p))
               (con-beg (org-element-property :contents-begin context))
               (con-end (org-element-property :contents-end context)))
          (cui-optional-remove-distant-empty-lines con-beg con-end)))))

(add-hook 'cui-block-after-chat-insertion-hook #'my/cui-postprocess)

Hooks for for pre-processing of requests to LLM or text of blocks

There are two hooks for that:

  • before any - raw text level: as `cui-block-parse-part-hook’ in cui-block.el
  • after all - vector level: as `cui-restapi-after-prepare-messages-hook’ in cui-restapi.el

More info: doc.org in Main path of JSON decoding section.

Custom roles

There are cui-block-roles-prefixes variable that allow to customize roles at step of parsing cui block before sending.

For incoming RestAPI conversion to prefixes cui-block-roles-restapi variable used.

Elisp: Get content of block

(cui-block-tags-get-content)

Or

(cui-block-tags--clear-properties
 (cui-block-tags-replace (cui-block-get-content (cui-block-p) nil :export nil)
                         (list (cui-block-get-header-marker (cui-block-p)))))

Elisp: query LLM directly.

Here is example that print result in asynchronous way. Service name and model string define all connection information.

(setq debug-on-message "^#<buffer.*")
(let ((cui-restapi-show-error-function #'cui-restapi--show-error))
  (cui-restapi--url-request 'github "openai/gpt-4.1"
                       (lambda (result) (print (cui-restapi--get-single-response-text result)))
                       :messages '((:role user :content "test"))  ; chat
                       :stream nil))

Org functions for block

  • org-in-src-block-p = cui-block-p
  • cui-block-insert-result = cui-block-insert-result
  • org-babel-where-is-src-block-result = cui-block-where-is-result
  • org-fill-paragraph = cui-block-fill-paragraph
  • org-babel-mark-block = cui-block-mark-at-point
  • org-babel-find-named-block = cui-find-named-block
  • org-src–contents-area = cui-block–region, cui-block–contents-region
  • org-babel–normalize-body = cui-block–region, cui-block–contents-region
  • org-babel-insert-header-arg - TODO!!

Tangling and noweb notes

We dont activate noweb in subsequent block at expansion of links and tags.

“cui-noweb” (Org property for the entry at point-or-marker) is used to control noweb (org-entry-get) if :noweb block parameter is not set.

Example:

#+NAME: ina
#+begin_cui
test
#+end_cui

#+begin_cui
<<ina()>>
#+end_cui

Chain of noweb references is possible, referenced cui blocks evaluated with :tangle context.

“.cui” “.ai” files as links expanded as new chat messages, other files inserted in markdown block and with masked chat prefixes by adding “_” before chat role.

How this works?

We add “cui” as a type of special block to variables org-protecting-blocks and org-structure-template-alist.

Org-mode intrernally use org-element-special-block-parser to process our cui block as special.

For highlighting we use hack to inject our font-lock staff to font-lock-defaults variable initialized by org-set-font-lock-defaults function. Source blocks only fontified with org-src-font-lock-fontify-block and not exposed as Org element for now, that is why it is not possible to use org-babel-do-in-edit-buffer to call functions inside of it with its major-mode of language.

For parallel requests we use cui-timers--element-marker-variable-dict variable that keeps pairs: url-buffer (key) -> Header-marker (variable). This allow for same block have many url-buffers.

For RESTAPI requesting LLMs we provide two functions:

  • cui-restapi-request-prepare - support streaming
  • cui-restapi-request-llm-retries - retry request if it fails.

For make chain of calls we have cui-async1.el static framework that simplify making parallel and sequencial requests.

For keys remapping we bind keys to Org commands or key if pointer is not in cui block, we call original key or command with “hack” functions.

How images sent?

We do it in two steps, first we replace image link or tag with @image_jpeg:/path.jpg in cui-block-tags-replace and in cui-block-tags--compose-block-for-path-full as any other link, then cui-restapi--payload we encode image to base64 and split text around link and replace string in :context to a vector.

Other packages

Donate, sponsor the author

You can sponsor author crypto money directly with crypto currencies:

  • BTC (Bitcoin) address: 1CcDWSQ2vgqv5LxZuWaHGW52B9fkT5io25

https://raw.githubusercontent.com/Anoncheg1/public-share/refs/heads/main/BTC-1CcDWSQ2vgqv5LxZuWaHGW52B9fkT5io25.png

  • USDT (Tether TRX-TRON) address: TVoXfYMkVYLnQZV3mGZ6GvmumuBfGsZzsN

https://raw.githubusercontent.com/Anoncheg1/public-share/refs/heads/main/USDT-TVoXfYMkVYLnQZV3mGZ6GvmumuBfGsZzsN.png

  • TON (Telegram) address: UQC8rjJFCHQkfdp7KmCkTZCb5dGzLFYe2TzsiZpfsnyTFt9D

About

Call LLMs and agents from Org-mode blocks.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors