Adding a DSPy Template
Guide for adding a new shipped template to the Python sidecar’s template registry.
Overview
Templates are DSPy module wrappers that ship with the sidecar binary. Each template has a template_id, a default
signature, and a description. The Rust agent harness references templates by ID — it never sends Python source code.
Prerequisites
- Python sidecar dev environment set up (
cd apps/python-sidecar && uv sync --group dev) - Familiarity with DSPy modules (
dspy.Predict,dspy.ChainOfThought, etc.) - Understanding of the template-only execution model (see the Skill System documentation)
Steps
1. Create the template module
Create a new file in apps/python-sidecar/src/inklings/dspy/templates/:
"""My Template -- brief description of what it does."""
from __future__ import annotations
import dspyfrom inklings.dspy.templates.base import BaseTemplate
class MyTemplate(BaseTemplate): template_id: str = "my_template" description: str = "Brief description of what this template does"
def __init__(self, signature: str = "input -> output") -> None: super().__init__(signature) self.module: dspy.Predict = dspy.Predict(signature)Key requirements:
- Extend
BaseTemplate— providesforward(),save_state(), andload_state()for free - Set
template_id— must match the registry key (snake_case) - Set
description— shown in the manifest response - Set
self.module— must be adspy.Moduleinstance - Accept a
signatureparameter — allows the harness to customize input/output fields
2. Override forward() if needed
The default forward() in BaseTemplate calls self.module(**kwargs) and converts the result to a dict. Override it
only if your template needs custom pre/post-processing:
def forward(self, **kwargs: Any) -> dict[str, Any]: # Custom logic before the DSPy call enriched = {"input": kwargs["input"], "context": "additional context"} result = self.module(**enriched) return self._result_to_dict(result)3. Register in the template registry
Open apps/python-sidecar/src/inklings/dspy/templates/registry.py and add an entry to the TEMPLATES dict:
"my_template": { "class": "MyTemplate", "module_path": "inklings.dspy.templates.my_template", "signature": "input -> output", "description": "Brief description of what this template does", "parameters": {},},Fields:
| Field | Purpose |
|---|---|
class | Python class name to import |
module_path | Dotted import path to the module file |
signature | Default DSPy signature (can be overridden at execution time) |
description | Human-readable description (shown in manifest) |
parameters | Optional template-specific parameters with type, default, and description |
If your template accepts extra parameters (like num_chains or max_iterations), declare them:
"parameters": { "max_iterations": { "type": "int", "default": 3, "description": "Maximum refinement iterations", },},4. Write tests
Create apps/python-sidecar/tests/test_my_template.py:
from unittest.mock import MagicMock, patch
from inklings.dspy.templates.my_template import MyTemplate
def test_my_template_initializes(): t = MyTemplate(signature="question -> answer") assert t.template_id == "my_template" assert t.module is not None
def test_my_template_forward(mock_prediction): t = MyTemplate() with patch.object(t.module, "__call__", return_value=mock_prediction): result = t.forward(input="test") assert "output" in result
def test_my_template_state_roundtrip(fake_save_factory): t = MyTemplate() t.module.save = fake_save_factory({"test": "state"}) state = t.save_state() assert state == {"test": "state"}Use the shared fixtures from conftest.py (mock_prediction, fake_save_factory) for mocking DSPy internals.
Also add your template to test_all_templates.py — it parametrizes across all registered templates to verify basic
instantiation and manifest inclusion.
5. Verify the manifest
Run the manifest test to confirm your template appears:
uv run pytest tests/test_manifest.py -vThe manifest handler reads from the TEMPLATES registry and returns metadata for all templates. Your new template
should appear in the response.
6. Run the full suite
uv run pytestuv run ruff check .uv run mypy src/All tests should pass and coverage should remain above 80%.
Existing Templates
| Template ID | DSPy Module | Description |
|---|---|---|
predict | dspy.Predict | Basic single LLM call with signature |
chain_of_thought | dspy.ChainOfThought | Step-by-step reasoning before output |
react | dspy.ReAct | Interleaved reasoning and action steps |
multi_chain_comparison | dspy.MultiChainComparison | Generate and compare multiple reasoning chains |
best_of_n | Custom | Run N completions and select the best |
refine | Custom | Iteratively improve output with self-critique |
parallel | Custom | Run multiple sub-modules and aggregate results |
retrieve | dspy.Retrieve + dspy.Predict | Retrieval-augmented generation with context injection |
program_of_thought | dspy.ProgramOfThought | Generate and execute code (requires sandbox) |
code_act | dspy.CodeAct | Agent with code execution (requires sandbox) |
Architecture Notes
- Template-only execution: The sidecar only runs shipped templates.
dspy_moduleskill artifacts store atemplate_id+state_blob(serialized DSPy JSON state), never source code. - State serialization:
save_state()/load_state()use DSPy’s native JSON serialization via a temp file. The state blob is opaque to the Rust side. - LLM routing: Templates never make direct API calls. The
InklingsLMadapter intercepts DSPy’s LLM calls and routes them back to Rust via JSON-RPC IPC, where theProviderRegistryhandles actual API requests.
See Also
- Skill System — how skill artifacts reference templates
- Python Sidecar Security — security model and import allowlist
- Python Sidecar IPC Reference — JSON-RPC protocol specification
Was this page helpful?
Thanks for your feedback!