Skip to content

Custom Python package

About this page

This documents a stack generated from soliplex-template. A generated project ships its own copy of this page without this note.

Every entry under src/ is a self-contained project directory: a pyproject.toml, an src/ package inside it, and its own tests. This project's own is src/myproject/:

src/
  myproject/
    pyproject.toml
    src/myproject/
    tests/unit/
  <other>/

src/myproject/ is the project directory; src/myproject/src/myproject/ is the importable package inside it. <other>/ is whatever else you clone — same shape, no special handling.

Work in it the way you would work in any checkout:

cd src/myproject
uv sync                 # create/refresh the dev environment (installs pytest)
uv run pytest           # run the project's tests
uv pip install -e .     # or a plain editable install into another environment

The stack root has its own pyproject.toml, but it is a tooling environment (soliplex-cli, the docs site) rather than a library — the importable code all lives under src/.

How it reaches the backend

The Soliplex backend bind-mounts ./src (read-only) into its container as /app/src and puts this project's package directory on PYTHONPATH as /app/src/myproject/src (see docker-compose.yml), so anything you define here is importable by dotted name from the Soliplex config under backend/environment/ — no image rebuild needed to edit your code. The backend watches it too: its serve command carries --reload-dirs=/app/src/myproject/src, so saving a change to your code restarts the server.

Because only the package directories named in PYTHONPATH are import roots, /app/src itself is not one: a repo you clone next to this project is visible to the container but contributes nothing to the import namespace until you add its own src/ to PYTHONPATH.

What ships wired up

Two examples are referenced from the config so you can see the pattern:

  • a tool, myproject.tools.greeting, referenced from backend/environment/rooms/custom/room_config.yaml;
  • a FastAPI router, myproject.views.router, registered via app_router_operations in backend/environment/installation.yaml.

Dotted names into this package can equally be used in the installation.yaml meta: section (tool/agent/skill config classes, MCP wrappers, secret sources) — see the commented myproject.* examples there.

Making it your own

Add modules under src/myproject/src/myproject/, reference them by dotted name from the config, and add tests under src/myproject/tests/unit/. Delete the demonstration custom room once you have your own.

Adding a second project

src/ is where you clone any other repo you want to work on alongside this one — the soliplex backend itself, a library you depend on, a tool you are patching.

1. Clone it

The soliplex-template skill does the clone and the wiring below in one step, which is the recommended route:

uv run <skill>/scripts/src_projects.py clone <url>
uv run <skill>/scripts/src_projects.py list     # what is there, what is on the path

By hand it is just:

git clone <url> src/<other>

Nothing to do for git either way: .gitignore ignores everything under src/ except this project's own directory, so the clone stays part of its own repo and never shows up in this stack's git status. That is a rule, not a list — a third and fourth clone need no further edits.

2. Work on it

Each project directory carries its own environment, so treat it as the standalone checkout it is:

cd src/<other>
uv sync
uv run pytest

Its .venv is its own; nothing is shared with this project's, or with the stack root's.

3. Make it importable by the backend (optional)

Only needed if the backend should run that checkout's code. The ./src bind mount already carries it into the container at /app/src/<other>; what it lacks is a place on the import path.

src_projects.py clone does this automatically, and src_projects.py add <other> does it for a checkout you cloned by hand. Either way it also checks the checkout's declared dependencies against the backend image and tells you whether a rebuild is needed — see What PYTHONPATH does and does not do below for why that matters.

By hand, append its package directory to the backend's PYTHONPATH in docker-compose.yml:

      PYTHONPATH: /app/src/myproject/src:/app/src/<other>/src

and, so edits to it restart the server, a matching --reload-dirs line to the backend's serve command, after this project's own:

      --reload=config
      --reload-dirs=/app/src/myproject/src
      --reload-dirs=/app/src/<other>/src

The trailing /src is right for a checkout that uses a src/ layout (most modern projects, including this one). For a flat-layout repo — packages sitting at the repo root — use /app/src/<other> instead. Point at the directory that contains the importable package, never at the package itself, and never at /app/src: making the mount point an import root is what this layout exists to avoid.

Then restart the backend. This is a compose change, so the --reload=config flag does not pick it up:

docker compose up -d backend

What PYTHONPATH does and does not do

  • It shadows installed distributions. PYTHONPATH is searched before site-packages, so a soliplex checkout listed there wins over the soliplex installed in the image. That is what makes a backend dev-mode checkout work — and what makes an accidental name collision confusing.
  • Every project on it is watched. The backend's serve command carries one --reload-dirs per PYTHONPATH entry, which src_projects.py keeps in step, so editing any of them restarts the server: a soliplex checkout is no special case. src_projects.py list shows which are watched.
  • It does not install anything. The checkout's own dependencies are not resolved: only its source becomes importable. If it imports a third-party package the backend image does not already have, the import fails at runtime — and docker compose up -d backend will not fix it, because the dependency has to be baked into the image. Add it to backend/requirements.txt, then docker compose build backend; see Backend image & dependencies. src_projects.py checks the project's direct requirements for you, reporting each as satisfied, MISSING, or CONFLICT: installed, at a version the project rejects. requirements.txt cannot fix a conflict, since the image's own versions are frozen as constraints; adapt the project to the image's versions, pick a soliplex image tag that has versions it accepts, or customize backend/Dockerfile. For what lies deeper than the direct requirements, see Checking it in the backend's environment.
  • The mount is read-only, and the container runs as the PUID:PGID from .env. Edit the checkout on the host; the backend only reads it.

Checking it in the backend's environment

uv run pytest in a project directory runs against that project's own uv.lock, which is not what the backend has: the backend imports the project's code alongside the soliplex image's own versions. When src_projects.py adds a project, it checks the direct requirements in its pyproject.toml against the image, but not the requirements of those requirements.

To check the code itself against the image, import it in the dev-mode soliplex-dev service, which runs the backend's image with the very same PYTHONPATH (--no-deps: the check needs no database):

docker compose run --rm --no-deps soliplex-dev /app/.venv/bin/python -c "import myproject.tools, myproject.views"

For another project on the path, import its own package instead. A silent exit means everything its modules import is there; an ImportError, or a version error raised on import, names what the image lacks or has at the wrong version, however deep in the tree. It does not run the project's tests, so code that imports cleanly but needs a newer API still fails only when it runs — which is what the tests are for.

Running its tests there

soliplex-dev is built with every src/ project's dev dependency group (pytest and the like) installed on top of the backend's image, with the image's own versions frozen, so a project's tests run there against the very versions the backend imports it with:

docker compose run --rm --no-deps -w /app/src/myproject soliplex-dev /app/.venv/bin/python -m pytest

For another project, pass its own directory to -w. The dev groups are listed in soliplex-dev/requirements-dev.txt, which src_projects.py regenerates whenever it adds or clones a project. Every project directory under src/ contributes, whether or not it is on PYTHONPATH, so taking one off the path (src_projects.py remove) leaves the file as it is. After changing a project's dev group yourself, or deleting a project's directory from src/, regenerate the file and rebuild:

uv run <skill>/scripts/src_projects.py dev-requirements
docker compose build soliplex-dev

Commit the regenerated file: a change to it is a change to what the tests run against. A dev requirement that needs a different version of something the backend image has fails that build, as one in backend/requirements.txt does; adapt the project's dev group to the image's versions.