Skip to content

Latest commit

 

History

History
944 lines (674 loc) · 23.7 KB

File metadata and controls

944 lines (674 loc) · 23.7 KB

Contributing

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


Development Setup

Prerequisites

  • 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 --dev

This installs the project into the local virtual environment together with the development dependency group, including:

  • pytest for testing.
  • pytest-asyncio for asynchronous Template tests.
  • ruff for linting and formatting.
  • pre-commit for automated code quality checks.
  • ty for static type checking.

You can optionally activate the virtual environment:

source .venv/bin/activate

Activation is not required. Commands can also be run directly through uv:

uv run pytest

Asynchronous Agents

Optional: 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.

Installing sinapsis-async

Install the package using uv and the private Sinapsis package index:

uv pip install sinapsis-async \
  --extra-index-url https://private.pypi.sinapsis.tech

Private Package Index Authentication

If 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 ~/.netrc

Replace <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.tech

Using ~/.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.tech

Never 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.

Running Asynchronous Agents

Asynchronous Agent configurations should be run using:

sinapsis-async run <config>

Synchronous Agents use the standard Sinapsis CLI:

sinapsis run <config>

Repository Structure

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/

Adding a Template

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 MyTemplate

Sinapsis also discovers registered Templates through:

from sinapsis.templates import MyTemplate

The repository's discovery tests verify that package Templates are available through the global Sinapsis Template registry.

Keep Template Modules Independent

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",
}

Writing Templates

Every Template should:

  • Inherit from the appropriate Sinapsis Template base class.
  • Accept a DataContainer as input.
  • Return a DataContainer as output.
  • Implement execute() for synchronous execution and/or async_execute() for asynchronous execution.
  • Avoid mutating the input DataContainer directly.
  • Raise meaningful exceptions for invalid inputs.
  • Use TemplateAttributes for 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.


Data Models and Type Safety

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 BaseModel for validated configuration and metadata models.
  • Pydantic dataclasses for structured runtime data such as DataContainer and 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:

  • Packet
  • ImagePacket
  • AudioPacket
  • TextPacket
  • FilePacket
  • TimeSeriesPacket
  • BinaryPacket
  • DataFramePacket
  • LLMConversationPacket

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.


Template Design Guidelines

Templates should aim to be:

Single-purpose

A Template should perform one logical task.

Prefer several composable Templates over one Template that implements a large number of unrelated operations.

Composable

Templates should communicate through DataContainer and supported packet types rather than relying on framework-specific global state.

Stateless

Avoid storing execution state between calls.

A Template should generally be safe to reuse across multiple executions.

Deterministic where possible

Identical inputs should produce identical outputs unless randomness, external services, or other non-deterministic behaviour are intrinsic to the Template.

Well-typed

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.

Non-mutating

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)

Declarative Agent Composition

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.


Template Configuration

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
        - agent

For asynchronous Agents, use the asynchronous Sinapsis CLI:

sinapsis-async run <config>

Synchronous Agents use:

sinapsis run <config>

Testing

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.
  • DataContainer input and output behaviour.
  • Input container immutability.
  • Availability through sinapsis.templates.

Run the complete test suite:

uv run pytest

Run with verbose output:

uv run pytest -v

Template-specific tests

The 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 ImagePacket was 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.


Code Quality

This project uses:

  • Ruff for linting and formatting.
  • Pytest for testing.
  • pytest-asyncio for asynchronous tests.
  • ty for static type checking.
  • pre-commit for automated checks.

Run all configured pre-commit hooks:

uv run pre-commit run --all-files

Run Ruff directly:

uv run ruff check .

Run the test suite:

uv run pytest

Run static type checking:

uv run ty check

All configured checks should pass before opening a pull request.


Documentation

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 --dev

Build the documentation from the documentation source directory:

uv run sphinx-build -b html docs docs/_build/html

To build the documentation with warnings treated as errors:

uv run sphinx-build -W -b html docs docs/_build/html

The -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/html

The 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 DataPacket types it consumes.
  • Which DataPacket types 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.

Documentation builds are also validated by GitHub Actions for pull requests targeting main.

Docker

Docker support is optional.

If Docker support is included, use the appropriate Sinapsis base image.

For CPU-based Templates:

FROM sinapsis:base

For GPU-based Templates:

FROM sinapsis-nvidia:base

The 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.


Pull Requests

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 DataContainer and 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 pytest

Run all quality checks:

uv run pre-commit run --all-files

Run static type checking:

uv run ty check

Continuous Integration

Pull 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.


Versioning

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.


Questions and Design Discussions

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.