DSel is a declarative, modular, and optimizable framework for working with Large Language Models (LLMs) in Emacs Lisp. Inspired by Python’s DSPy, it provides a structured approach to building LLM-powered applications with type checking, schema validation, and optimization capabilities.
DSel leverages the existing llm.el
library for backend LLM interactions while adding powerful abstractions for creating robust LLM applications.
DSel is built around several key principles:
- Declarative Signatures: Focus on what you want from the LLM, not how to get it
- Modular Programs: Compose complex behaviors from simple, reusable components
- Native Optimization: Improve LLM behavior with data-driven refinements
- Emacs Integration: Seamless integration with the Emacs ecosystem
- Emacs 28.1+
llm.el
(and an LLM provider likellm-openai
)
- Clone the repository:
git clone https://github.com/cosmicz/dsel.git
- Add to your load path:
(add-to-list 'load-path "~/path/to/dsel")
(require 'dsel)
- Configure with your preferred LLM provider:
(require 'llm-ollama) ;; or another provider
(dsel-configure :lm (make-llm-ollama :chat-model "gemma3:12b"))
Cloud can be faster (than my M2 ):
(require 'llm-openai)
(setq llm-warn-on-nonfree nil)
(dsel-configure
:log-level 'info
:lm (make-llm-openai-compatible
:url "https://openrouter.ai/api/v1"
:key (my/get-openrouter-api-key)
:chat-model "google/gemini-2.5-flash-preview-05-20"))
;; 1. Define a signature (the schema for your LLM task)
(dsel-defsignature quick-start-signature
"Answer the question."
:input-fields '((:name question :type string :desc "The question to answer"))
:output-fields '((:name answer :type string :desc "The answer to the question")))
;; 2. Create a predictor (a module that uses the signature)
(dsel-defpredict quick-start-predictor quick-start-signature)
;; 3. Run the predictor synchronously (blocks until complete)
(let ((prediction (dsel-forward quick-start-predictor :question "What is the capital of France?")))
(message "Answer: %s" (dsel-get-field prediction 'answer)))
;; Output: Answer: The capital of France is Paris.
For interactive applications, use the async API with dsel-aio-then
:
;; 1. Same signature and predictor as above
;; 2. Run the predictor asynchronously (non-blocking)
(dsel-aio-then
(dsel-aforward quick-start-predictor :question "What is machine learning?")
;; Success callback
(lambda (prediction)
(message "Answer: %s" (dsel-get-field prediction 'answer)))
;; Error callback
(lambda (err)
(message "Error: %s" err)))
Create reusable modules for complex applications:
;; 1. Define a question-answering module
(dsel-defmodule qa-module
"A question-answering module with configurable strategies."
:submodules
((predictor quick-start-predictor))
(response-style 'concise :type symbol))
;; 2. Define its async behavior
(dsel-defaforward qa-module (&key question style)
"Answer a question with the specified style."
(let ((predictor (cdr (assq 'predictor submodules))))
(dsel-aio-await (dsel-aforward predictor :question question))))
;; 3. Use the module
(let ((qa (make-qa-module :response-style 'detailed)))
(dsel-aio-then
(dsel-aforward qa :question "Explain photosynthesis")
(lambda (result) (message "QA Result: %s" (dsel-get-field result 'answer)))
(lambda (err) (message "QA Error: %s" err))))
A schema that defines the inputs and outputs for an LLM task, along with instructions.
A signature declares the inputs and outputs for an LLM task, including types and validation:
(dsel-defsignature sentiment-signature
"Classify the sentiment of the given text."
:input-fields '((:name text
:type string
:desc "The text to classify"
:prefix "TEXT:"))
:output-fields '((:name sentiment
:type string
:desc "The sentiment of the text"
:prefix "SENTIMENT:"
:enum ["positive" "negative" "neutral"])
(:name confidence
:type number
:desc "Confidence score from 0.0 to 1.0"
:prefix "CONFIDENCE:"
:optional t)))
Data instances used for few-shot learning or evaluation. Examples help improve performance through few-shot learning:
;; Create individual examples
(setq example1 (dsel-make-example :text "I love this product!" :sentiment "positive" :confidence 0.95))
(setq example2 (dsel-make-example :text "I hate this product." :sentiment "negative" :confidence 0.9))
;; Define a collection of examples
(dsel-defexamples sentiment-examples '(text)
(:text "I love this!" :sentiment "positive" :confidence 0.95)
(:text "I hate this." :sentiment "negative" :confidence 0.9)
(:text "This is okay." :sentiment "neutral" :confidence 0.8))
;; Create a predictor with examples
(dsel-defpredict fewshot-predictor sentiment-signature
:demos sentiment-examples)
Basic module for making LLM calls using a signature.
Predictors are modules that execute LLM calls with a specific signature:
;; Simple predictor
(dsel-defpredict sentiment-predictor sentiment-signature)
;; Predictor with configuration options
(dsel-defpredict configured-predictor sentiment-signature
:config '(:temperature 0.2
:max-tokens 100))
Generate predictions by calling `dsel-forward`:
(let ((prediction (dsel-forward sentiment-predictor :text "I love this product!")))
(message "Sentiment: %s" (dsel-get-field prediction 'sentiment))
(message "Confidence: %s" (dsel-get-field prediction 'confidence)))
Module that prompts the LLM for step-by-step reasoning before answering. For complex reasoning tasks, use chain-of-thought predictors:
;; Create a chain-of-thought predictor
(dsel-defchain-of-thought sentiment-cot-predictor sentiment-signature
:rationale-field-name 'reasoning
:rationale-field-desc "Explain why you classified the text with this sentiment"
:rationale-field-prefix "REASONING:")
;; Use the chain-of-thought predictor
(let ((result (dsel-forward sentiment-cot-predictor
:text "The product works as expected, but it's expensive.")))
(message "Reasoning: %s" (dsel-get-field result 'reasoning))
(message "Sentiment: %s" (dsel-get-field result 'sentiment)))
dsel-module
is the base class for all runnable components in DSel.
DSel provides powerful macros for creating modular, composable LLM applications with async-first design:
Use dsel-defmodule
to create custom modules that combine multiple predictors:
;; Define a sentiment analysis module that combines basic and chain-of-thought predictors
(dsel-defmodule sentiment-analysis-module
"A module that performs sentiment analysis with different strategies."
:submodules
((basic-predictor sentiment-predictor)
(cot-predictor sentiment-cot-predictor))
(strategy 'basic :type symbol)
(use-reasoning nil :type boolean))
Use dsel-defaforward
to define async forward methods for your modules:
;; Define the async forward method
(dsel-defaforward sentiment-analysis-module (&key text strategy use-reasoning)
"Analyze sentiment of TEXT using the specified strategy."
(let* ((predictor-name (cond
((or use-reasoning (eq strategy 'cot)) 'cot-predictor)
((eq strategy 'basic) 'basic-predictor)
(t 'basic-predictor)))
(predictor (cdr (assq predictor-name submodules))))
;; Use dsel-aio-await to wait for async results
(dsel-aio-await (dsel-aforward predictor :text text))))
Use dsel-aio-then
for non-blocking async execution with callbacks:
;; Create module instance
(setq my-module (make-sentiment-analysis-module :strategy 'cot))
;; Async execution with callbacks
(dsel-aio-then
(dsel-aforward my-module :text "I love this framework!" :use-reasoning t)
;; Success callback
(lambda (prediction)
(message "Sentiment: %s" (dsel-get-field prediction 'sentiment))
(message "Reasoning: %s" (dsel-get-field prediction 'reasoning)))
;; Error callback
(lambda (err)
(message "Error: %s" err)))
Use dsel-forward
for blocking synchronous execution when needed:
;; Synchronous execution (blocks until complete)
(let ((result (dsel-forward my-module :text "This is okay." :strategy 'basic)))
(message "Result: %s" (dsel-get-field result 'sentiment)))
Create sophisticated pipelines by chaining multiple async operations:
;; RAG module example
(dsel-defmodule rag-module
"A Retrieval-Augmented Generation module."
:submodules
((retriever retriever-predictor)
(generator generator-predictor))
(max-results 5 :type number))
(dsel-defaforward rag-module (&key query)
"Execute RAG pipeline: retrieve documents, then generate answer."
(let* ((retriever (cdr (assq 'retriever submodules)))
;; First, retrieve relevant documents
(retrieval-results (d
8000
sel-aio-await
(dsel-aforward retriever
:query query
:max_results (rag-module-max-results this))))
;; Extract and join documents
(documents (dsel-get-field retrieval-results 'results))
(context (mapconcat #'identity documents "\n\n"))
;; Then generate answer using retrieved context
(generation-results (dsel-aio-await
(dsel-aforward (cdr (assq 'generator submodules))
:query query
:context context))))
;; Return combined results
(dsel-make-prediction
:query query
:context context
:answer (dsel-get-field generation-results 'answer)
:confidence (dsel-get-field generation-results 'confidence))))
Compiles modules to improve their performance, such as selecting the best few-shot examples.
Improve performance by selecting the best few-shot examples:
;; Define an optimizer that selects the most relevant examples
(dsel-defoptimizer sentiment-optimizer labeled-fewshot
:metric (lambda (gold pred)
(string= (dsel-get-field gold 'sentiment)
(dsel-get-field pred 'sentiment)))
:k 3) ;; Number of examples to select
;; Create an optimized predictor
(setq optimized-predictor
(dsel-compile sentiment-optimizer sentiment-predictor :trainset sentiment-examples))
;; Use the optimized predictor
(dsel-forward optimized-predictor :text "This product is just okay.")
Handles LLM prompt formatting and output parsing.
DSel provides a rich type system for input and output fields:
string
: Text valuesinteger
: Whole numbersnumber
: Floating-point numbersboolean
: True/false valuesarray
: Lists of valuesobject
: Key-value structuresenum
: Values from a predefined set
Each type is validated and properly coerced from the LLM’s text response.
DSel supports complex nested data structures with full type validation:
(dsel-defsignature product-signature
"Analyze product data"
:input-fields '((:name query :type string :desc "Search query"))
:output-fields '((:name product
:type object
:desc "Product details"
:properties ((:name name :type string :desc "Product name")
(:name price :type number :desc "Product price")
(:name available :type boolean :desc "Is available")
(:name categories
:type array
:desc "Product categories"
:items (:type string))
(:name metadata
:type object
:desc "Additional metadata"
:properties ((:name created-at :type string :desc "Creation date")
(:name updated-at :type string :desc "Last update date")
(:name rating :type number :desc "Product rating"))
:required (created-at)))
:required (name price))))
Accessing nested fields is straightforward:
(let ((product (dsel-get-field prediction 'product)))
;; Access basic fields
(alist-get 'name product) ;; => "Premium Coffee Maker"
(alist-get 'price product) ;; => 129.99
(alist-get 'available product) ;; => t
;; Access array elements
(let ((categories (alist-get 'categories product)))
(aref categories 0)) ;; => "Kitchen"
;; Access nested objects
(let ((metadata (alist-get 'metadata product)))
(alist-get 'rating metadata))) ;; => 4.8
Required fields must be present and non-empty (except for boolean fields where nil
is valid).
Optional fields can be omitted or have empty/nil values.
DSel provides robust error handling for parsing failures. Instead of stopping execution when the LLM produces invalid output, errors are collected in the prediction’s errors
slot:
(let ((prediction (dsel-forward predictor :text "Some input")))
;; Check if prediction has errors
(if (dsel-prediction-ok-p prediction)
(message "Success: %s" (dsel-get-field prediction 'result))
;; Handle errors gracefully
(message "Errors occurred:")
(dsel-prediction-report-errors prediction)))
Error types include:
:missing-required
: Required field not found in LLM response:required-field-empty
: Required field is present but empty:coercion
: Field value cannot be converted to the expected type
You can also check for specific field errors:
;; Check if a specific field had an error
(when-let ((error (dsel-prediction-field-error prediction 'field-name)))
(message "Field error: %s" (plist-get error :message)))
For advanced use cases, you can customize how prompts are formatted and responses are parsed by implementing your own adapter:
(cl-defstruct (my-custom-adapter (:include dsel-adapter))
"My custom adapter implementation.")
(cl-defmethod dsel-adapter-format-prompt ((adapter my-custom-adapter) ...)
...)
(cl-defmethod dsel-adapter-parse-output ((adapter my-custom-adapter) ...)
...)
A simple sentiment analysis system: sentiment-classifier.el
A practical tool that generates conventional commit messages from git diffs: git-commit-generator.el
DSel includes a flexible logging system to help with debugging:
;; Configure logging
(dsel-configure
:log-buffer "*DSel Log*" ;; Buffer to write logs (nil to disable)
:log-level 'debug ;; Level: debug, info, warning, or error
:log-to-messages t) ;; Also log to *Messages* buffer
Run tests with:
make test
Run specific tests:
make test SELECTOR="adapter" # Run all adapter tests
DSel is currently in active develoment, its APIs may change.
- [X] Declarative Signatures: Define LLM tasks using structured “signatures” with comprehensive type validation
- [ ] Type Support: Rich type system including strings, numbers, booleans, arrays, objects, and enums
- [ ] Date / Time types
- [X] Nested Structures: Support for complex nested objects and arrays with validation
- [X] Modular Programs: Compose simple modules into complex LLM-powered applications
- [X] Optimizers: improve LLM performance through few-shot example selection
- [X] Chain-of-Thought: Built-in support for chain-of-thought reasoning
- [X] Async: asynchronous processing by default
- [ ] Tool Integration: Integrate with external tools and APIs for enhanced functionality
- [ ] ReAct: module for reasoning and action generation
- [ ] Retrievers: modules for interacting with external data sources
- [ ] Refine: Refine LLM outputs with additional context or examples
- [ ] Caching and Persistency
- [ ] Assertions and Suggestions: Validate and suggest improvements for LLM outputs
- [ ] Program of Thought: running programs to solve a problem
- [ ] Complex Optimization: advanced optimization techniques beyond few-shot learning
Contributions are welcome! Please see CONTRIBUTING.org for details.
This project is licensed under the MIT License - see the LICENSE file for details.