Thank you for contributing a Sinapsis Template!
This repository provides the recommended structure and development tooling for building reusable Templates that integrate with the Sinapsis ecosystem.
Please follow the guidelines below to ensure consistency, maintainability, and compatibility across community-contributed packages.
🛠️ Development Setup • ⚡ Asynchronous Agents • 📁 Repository Structure • 🧩 Adding a Template • 🔒 Data Models and Type Safety • 📐 Template Design • 🔀 Agent Composition • ⚙️ Configuration • 🧪 Testing • ✅ Code Quality • 📙 Documentation • 🐳 Docker • 🚀 Pull Requests • 🔄 CI • 📦 Versioning • 💬 Questions
- Python 3.10+
uv- Git
Clone the repository and enter the project directory:
git clone <repository-url>
cd <repository>Install the project dependencies and development dependencies using uv:
uv sync --devThis installs the project into the local virtual environment together with the development dependency group, including:
pytestfor testing.pytest-asynciofor asynchronous Template tests.rufffor linting and formatting.pre-commitfor automated code quality checks.tyfor static type checking.
You can optionally activate the virtual environment:
source .venv/bin/activateActivation is not required. Commands can also be run directly through uv:
uv run pytestOptional: This section is only required when developing or running asynchronous Agents that depend on
sinapsis-async. It is not required for developing synchronous Templates or running the standard Template contract tests.
Asynchronous Agents use the sinapsis-async package and the
sinapsis-async CLI.
sinapsis-async is distributed through the private Sinapsis package index and
requires a Sinapsis account.
Users must have access to:
https://platform.sinapsis.tech
and valid credentials for the private Sinapsis package index.
Install the package using uv and the private Sinapsis package index:
uv pip install sinapsis-async \
--extra-index-url https://private.pypi.sinapsis.techIf using ~/.netrc, create a credentials file containing your Sinapsis
private package index credentials:
cat <<'EOF' >> ~/.netrc
machine private.pypi.sinapsis.tech
login <your-username>
password <your-password-or-token>
EOF
chmod 600 ~/.netrcReplace <your-username> and <your-password-or-token> with your Sinapsis
account credentials or package index token.
The chmod 600 command ensures that the .netrc file is readable and
writable only by your user account.
You can then install sinapsis-async without including credentials in the
command:
uv pip install sinapsis-async \
--extra-index-url https://private.pypi.sinapsis.techUsing ~/.netrc is recommended for local development because it avoids
exposing credentials in shell history.
Alternatively, credentials can be supplied directly to the package index URL:
uv pip install sinapsis-async \
--extra-index-url https://<username>:<password-or-token>@private.pypi.sinapsis.techNever commit credentials to source control or include them in
pyproject.toml, uv.lock, Dockerfiles, or other project files.
For CI environments, use the CI platform's secret management functionality rather than storing credentials in the repository.
Asynchronous Agent configurations should be run using:
sinapsis-async run <config>Synchronous Agents use the standard Sinapsis CLI:
sinapsis run <config>The recommended repository structure is:
.
├── src/
│ └── <package_name>/
│ ├── __init__.py
│ ├── templates/
│ │ ├── __init__.py
│ │ └── my_template.py
│ │
│ └── configs/
│ └── my_template.yaml
│
├── tests/
│ ├── __init__.py
│ ├── README.md
│ ├── conftest.py
│ ├── fixtures.py
│ ├── test_contract.py
│ ├── test_async_contract.py
│ └── test_discovery.py
│
├── .github/
│ └── workflows/
│
├── docker/
│ ├── Dockerfile
│ └── docker-compose.yml
│
├── README.md
├── CONTRIBUTING.md
├── pyproject.toml
└── uv.lock
All Template implementations should live under:
src/<package_name>/templates/
Example Agent configurations should live under:
src/<package_name>/configs/
Create the Template implementation under the package's templates/ directory.
For example:
src/<package_name>/templates/my_template.py
Templates are exposed through templates/__init__.py using the package's
Template lookup.
The lookup maps public Template names to the modules where they are implemented:
import importlib
from collections.abc import Callable
from typing import Any
_root_lib_path = "your_package.templates"
_template_lookup = {
"MyTemplate": f"{_root_lib_path}.my_template",
}
def __getattr__(name: str) -> Callable[..., Any]:
if name in _template_lookup:
module = importlib.import_module(_template_lookup[name])
return getattr(module, name)
raise AttributeError(
f"template `{name}` not found in {_root_lib_path}"
)
__all__ = list(_template_lookup.keys())When adding a new Template, add it to _template_lookup:
_template_lookup = {
"MyTemplate": f"{_root_lib_path}.my_template",
"AnotherTemplate": f"{_root_lib_path}.another_template",
}The Template class name must match the key used in _template_lookup.
For example:
_template_lookup = {
"MyTemplate": f"{_root_lib_path}.my_template",
}expects the module to contain:
class MyTemplate(Template):
...The __all__ attribute is generated automatically from _template_lookup:
__all__ = list(_template_lookup.keys())The __all__ list is used by the baseline contract tests to determine which
Templates belong to the package and should be tested.
It also provides the public interface for importing Templates directly from the package:
from <package_name>.templates import MyTemplateSinapsis also discovers registered Templates through:
from sinapsis.templates import MyTemplateThe repository's discovery tests verify that package Templates are available through the global Sinapsis Template registry.
Each Template module should be independently importable.
Avoid placing unrelated Template implementations in a single module unless there is a clear reason to do so.
For example:
templates/
├── __init__.py
├── my_template.py
└── another_template.py
This allows the lazy lookup mechanism to import only the Template that is requested.
The lookup should then be:
_template_lookup = {
"MyTemplate": f"{_root_lib_path}.my_template",
"AnotherTemplate": f"{_root_lib_path}.another_template",
}Every Template should:
- Inherit from the appropriate Sinapsis
Templatebase class. - Accept a
DataContaineras input. - Return a
DataContaineras output. - Implement
execute()for synchronous execution and/orasync_execute()for asynchronous execution. - Avoid mutating the input
DataContainerdirectly. - Raise meaningful exceptions for invalid inputs.
- Use
TemplateAttributesfor configurable Template parameters. - Validate required configuration through the attributes model.
- Include clear type hints.
- Include docstrings for public classes and methods.
A basic synchronous Template looks like:
from sinapsis import DataContainer
from sinapsis.templates import Template
class MyTemplate(Template):
"""Example Sinapsis Template."""
def execute(
self,
data_container: DataContainer,
) -> DataContainer:
"""Execute the Template."""
...An asynchronous Template looks like:
from sinapsis import DataContainer
from sinapsis.templates import Template
class MyAsyncTemplate(Template):
"""Example asynchronous Sinapsis Template."""
async def async_execute(
self,
data_container: DataContainer,
) -> DataContainer:
"""Execute the Template asynchronously."""
...If a Template supports both synchronous and asynchronous execution, both methods should be tested.
Sinapsis uses strongly typed data models to define the structure of Template configuration and the data flowing between Templates.
The main building blocks are:
- Pydantic
BaseModelfor validated configuration and metadata models. - Pydantic dataclasses for structured runtime data such as
DataContainerand packet types. - Type hints throughout the Template and Agent APIs.
Templates should use these models rather than passing unstructured dictionaries or framework-specific state between components.
The primary runtime data structure passed between Templates is the
DataContainer.
A DataContainer can contain one or more supported packet types, including:
PacketImagePacketAudioPacketTextPacketFilePacketTimeSeriesPacketBinaryPacketDataFramePacketLLMConversationPacket
Templates should use the appropriate packet type for the data they produce.
For example, a Template producing text should create a TextPacket rather
than placing an arbitrary string directly into generic_data.
Configuration should be represented using Pydantic models derived from the appropriate Sinapsis configuration base classes.
This provides:
- Runtime validation.
- Consistent serialization.
- Clear type definitions.
- Automatic validation of required attributes.
- Better error messages for invalid configuration.
- Compatibility with the Sinapsis Agent configuration system.
Avoid bypassing model validation by passing unvalidated dictionaries or arbitrary objects when an existing Sinapsis model is available.
Templates should aim to be:
A Template should perform one logical task.
Prefer several composable Templates over one Template that implements a large number of unrelated operations.
Templates should communicate through DataContainer and supported packet
types rather than relying on framework-specific global state.
Avoid storing execution state between calls.
A Template should generally be safe to reuse across multiple executions.
Identical inputs should produce identical outputs unless randomness, external services, or other non-deterministic behaviour are intrinsic to the Template.
Use type hints throughout the public API and validate inputs early.
Errors should clearly explain what went wrong and, where possible, how the caller can correct the problem.
Avoid modifying the input DataContainer directly.
Prefer returning a new container or combining containers using the supported
DataContainer operations.
For example:
return data_container + DataContainer(
texts=[result_packet],
)rather than directly modifying:
data_container.texts.append(result_packet)Sinapsis Agents are declarative.
An Agent configuration describes what Templates should execute and how they are connected. The Agent runtime is responsible for executing the workflow.
Template authors should avoid implementing Agent orchestration logic inside individual Templates.
For example, if a workflow needs to branch into multiple execution paths, the preferred approach is to use the appropriate Sinapsis data-flow Templates rather than manually implementing branching logic inside another Template.
For example:
┌──────────────┐
│ Agent │
└──────┬───────┘
│
▼
┌──────────────┐
│ Template │
└──────┬───────┘
│
▼
┌───────────────────┐
│ SplitDataFlow │
└───────┬───────────┘
│
┌────────┴────────┐
▼ ▼
┌──────────────┐ ┌──────────────┐
│ Branch A │ │ Branch B │
│ Template │ │ Template │
└──────┬───────┘ └──────┬───────┘
│ │
└────────┬────────┘
▼
┌───────────────────┐
│ MergeDataFlow │
└────────┬──────────┘
│
▼
┌──────────────┐
│ Template │
└──────────────┘
SplitDataFlow and MergeDataFlow allow the workflow configuration to
explicitly define how data containers are branched and recombined.
This is particularly important when handling container ownership and copying semantics.
For example, the workflow can explicitly define whether data should be:
- Reused by reference.
- Shallow-copied.
- Deep-copied.
- Combined when branches are merged.
Template authors should therefore avoid manually implementing branching, merging, or container-copying semantics inside application-specific Templates.
If a workflow requires branching or merging, prefer composing the appropriate Sinapsis data-flow Templates in the Agent configuration.
This keeps:
- Template logic focused on one task.
- Agent orchestration declarative.
- Data ownership explicit.
- Copying semantics visible in the Agent configuration.
- Workflows easier to understand and modify.
When creating a new Template, ask whether the functionality belongs inside the Template itself or whether it should instead be expressed through Agent composition.
Templates that expose configurable behaviour should define an
AttributesBaseModel based on TemplateAttributes.
For example:
from pydantic import Field
from sinapsis.templates import TemplateAttributes
class MyTemplateAttributes(TemplateAttributes):
"""Configuration for MyTemplate."""
keywords: list[str] = Field(
min_length=1,
default_factory=lambda: ["sinapsis"],
)Required configuration should be validated by the attributes model.
If an attribute has no sensible default, make it required rather than silently providing an arbitrary value.
Example Agent configurations should be provided under:
src/<package_name>/configs/
For example:
agent:
name: my_template_agent
templates:
- template_name: MyTemplate
class_name: MyTemplate
attributes:
keywords:
- sinapsis
- agentFor asynchronous Agents, use the asynchronous Sinapsis CLI:
sinapsis-async run <config>Synchronous Agents use:
sinapsis run <config>The repository includes baseline contract tests that are automatically applied
to every Template listed in the package's templates.__all__.
The package's __all__ is derived from _template_lookup in
templates/__init__.py.
This means that adding a Template to _template_lookup automatically includes
it in the baseline contract tests.
The tests verify the general Sinapsis Template contract, including:
- Template discovery.
- Template instantiation.
- Template attribute validation.
- Synchronous execution.
- Asynchronous execution where supported.
DataContainerinput and output behaviour.- Input container immutability.
- Availability through
sinapsis.templates.
Run the complete test suite:
uv run pytestRun with verbose output:
uv run pytest -vThe baseline tests verify that a Template behaves correctly as a Sinapsis component. They cannot determine whether the Template performs its intended business logic.
Every Template should therefore have additional tests covering its specific behaviour.
Tests should verify, where applicable:
- Successful execution.
- Expected output packets.
- Expected packet contents.
- Expected packet metadata.
- Invalid inputs.
- Required configuration.
- Edge cases.
- Error handling.
- External service failures.
- Synchronous execution.
- Asynchronous execution.
For example, if a Template produces an ImagePacket, do not only check that
execution returned a DataContainer.
Also check that:
- An
ImagePacketwas created. - The packet contains the expected image.
- The image has the expected shape.
- Relevant metadata is correct.
Similarly, if a Template produces a TextPacket, check the actual packet
content.
See tests/README.md for additional guidance on the
baseline tests and template-specific testing.
This project uses:
- Ruff for linting and formatting.
- Pytest for testing.
pytest-asynciofor asynchronous tests.tyfor static type checking.- pre-commit for automated checks.
Run all configured pre-commit hooks:
uv run pre-commit run --all-filesRun Ruff directly:
uv run ruff check .Run the test suite:
uv run pytestRun static type checking:
uv run ty checkAll configured checks should pass before opening a pull request.
Documentation is built as part of the continuous integration workflow.
Before opening a pull request, build the documentation locally to ensure that all documentation pages, references, and generated API documentation are valid.
Install the project and development dependencies:
uv sync --devBuild the documentation from the documentation source directory:
uv run sphinx-build -b html docs docs/_build/htmlTo build the documentation with warnings treated as errors:
uv run sphinx-build -W -b html docs docs/_build/htmlThe -W flag treats Sphinx warnings as errors, helping to catch broken
references, invalid directives, missing documentation, and other issues
before the changes reach CI.
To preview the generated documentation locally, start a simple HTTP server:
uv run python -m http.server --directory docs/_build/htmlThe documentation will be available at the local address printed by the server.
When adding or modifying a Template, update the documentation where appropriate. Documentation should explain:
- What the Template does.
- Which
DataPackettypes it consumes. - Which
DataPackettypes it produces. - Required and optional Template attributes.
- Expected Agent configuration.
- Synchronous and asynchronous execution support.
- External dependencies or services.
- Example Agent configurations where appropriate.
If a Template introduces new public functionality, ensure that its public classes and methods have appropriate docstrings and that relevant examples are included in the documentation.
Docker support is optional.
If Docker support is included, use the appropriate Sinapsis base image.
For CPU-based Templates:
FROM sinapsis:baseFor GPU-based Templates:
FROM sinapsis-nvidia:baseThe Docker configuration should be placed under:
docker/
If the required Sinapsis base image is not available locally, refer to the main Sinapsis repository for instructions on building the base images.
The Docker environment should be tested locally before submitting a pull request if Docker-related files have been changed.
Before opening a pull request, ensure that:
- New Templates are located under
src/<package_name>/templates/. - New Templates are added to
_template_lookup. - New Templates are automatically exposed through
__all__. - Template attributes are properly validated using the appropriate Pydantic model.
- Data is exchanged through
DataContainerand supported packet types. - Template-specific tests cover the expected behaviour.
- Async execution is tested when implemented.
- Example Agent configurations are added or updated where appropriate.
- Agent branching and merging use the appropriate data-flow Templates rather than custom orchestration logic.
- All tests pass.
- All pre-commit checks pass.
- Static type checking passes.
- Documentation is updated if behaviour or usage has changed.
- Docker configuration is tested if Docker files were modified.
- Commits are focused and descriptive.
Run the complete test suite:
uv run pytestRun all quality checks:
uv run pre-commit run --all-filesRun static type checking:
uv run ty checkPull requests targeting main are validated by GitHub Actions.
Depending on the workflow configuration, CI may run:
- Linting and formatting checks.
- Baseline contract tests.
- Template-specific tests.
- Static type checking.
- Package builds using
uv build. - Documentation builds.
Please ensure that all required CI checks pass before requesting a review.
Published packages should follow Semantic Versioning where applicable.
- Patch releases fix backwards-compatible bugs.
- Minor releases add backwards-compatible functionality.
- Major releases introduce breaking changes.
Changes to public Template behaviour, configuration, or interfaces should be considered when determining the appropriate version.
If you are unsure whether a proposed change belongs in a community Template package or in the core Sinapsis framework, please open an issue or start a discussion before beginning implementation.
As a general guideline:
- Functionality specific to one use case or integration belongs in a community Template package.
- Generic functionality shared across many Templates may be better suited to the Sinapsis framework itself.
- Workflow orchestration should generally be expressed declaratively through Agent configuration and existing data-flow Templates.
When in doubt, start a discussion with the maintainers before investing in a large implementation.