soliplex-template 0.11__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Enfold Systems, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,7 @@
1
+ Metadata-Version: 2.4
2
+ Name: soliplex-template
3
+ Version: 0.11
4
+ Summary: Docker Compose template for a Soliplex stack, plus the soliplex-template Agent Skill and its build tooling.
5
+ Requires-Python: >=3.12
6
+ License-File: LICENSE
7
+ Dynamic: license-file
@@ -0,0 +1,38 @@
1
+ # Soliplex Docker Compose Template
2
+
3
+ A starting point for running Soliplex and related services under
4
+ Docker Compose.
5
+
6
+ ## Quickstart
7
+
8
+ ```bash
9
+ git clone https://github.com/soliplex/soliplex-template.git
10
+ cd soliplex-template
11
+ ./scripts/generate-secrets.sh # populates .secrets/*.gen (gitignored)
12
+ echo 'OLLAMA_BASE_URL=http://your-ollama-host:11434' > .env
13
+ docker compose up
14
+ ```
15
+
16
+ Then open <http://localhost:9000>. The terminal client (TUI) is bundled in the
17
+ backend image — run it against the stack with
18
+ `docker compose exec backend soliplex-tui --url http://localhost:8000`. This
19
+ template also serves the TUI as a web app (the optional `tui` service) at
20
+ <https://localhost:9443/tui/>.
21
+
22
+ `OLLAMA_BASE_URL` must point at an Ollama server that serves the models
23
+ referenced in `backend/environment/installation.yaml`. The first `up` builds
24
+ images and initializes Postgres, so it takes a few minutes.
25
+
26
+ ## Documentation
27
+
28
+ Full documentation — prerequisites, exposed ports, architecture, secrets, the
29
+ RAG pipeline, configuration, and generating a customized project — lives at
30
+ <https://soliplex.github.io/soliplex-template/> (sources under `docs/`, built
31
+ with [Zensical](https://zensical.org)).
32
+
33
+ Build the docs locally with:
34
+
35
+ ```bash
36
+ uv run zensical serve # preview at http://localhost:8000
37
+ uv run zensical build # static site under site/
38
+ ```
@@ -0,0 +1,131 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "soliplex-template"
7
+ version = "0.11"
8
+ description = "Docker Compose template for a Soliplex stack, plus the soliplex-template Agent Skill and its build tooling."
9
+ requires-python = ">=3.12"
10
+ # The 'soliplex_template' package (src/) is stdlib-only; the bundled skill's
11
+ # add_room.py imports 'soliplex_template.rooms' from it.
12
+ dependencies = []
13
+
14
+ # Tooling for the soliplex-template skill. The scripts also carry PEP 723
15
+ # inline metadata so they can run standalone via `uv run <script>`, but the
16
+ # CI workflow installs this group with `uv sync --frozen --group dev`:
17
+ # - mako: scripts/refresh_skill_template.py and
18
+ # skill/scripts/generate_soliplex_project.py
19
+ # render the .mako template tree
20
+ # - skills-ref: scripts/build_skill.py validates the assembled skill against
21
+ # the Agent Skills spec (provides the `agentskills` CLI)
22
+ # - pyyaml: skill/scripts/soliplex_config.py parses `soliplex-cli config`
23
+ # output (resolved installation YAML)
24
+ # - pytest / pytest-cov / coverage: run the tests/unit/scripts/ suite
25
+ # (`uv run --group dev pytest`)
26
+ # - zensical: builds the documentation site under docs/ into site/
27
+ # (`uv run zensical build`); also drives the docs CI workflow
28
+ [dependency-groups]
29
+ dev = [
30
+ "coverage",
31
+ "mako",
32
+ "pyyaml",
33
+ "pytest",
34
+ "pytest-cov",
35
+ "ruff",
36
+ # Direct runtime dep of generate_soliplex_project.py (read_properties);
37
+ # pinned to match that script's PEP 723 header.
38
+ "skills-ref==0.1.1",
39
+ "soliplex-skills >= 0.4",
40
+ "zensical",
41
+ ]
42
+
43
+ # The repo is configuration + Dockerfiles + skill source, but it also ships the
44
+ # importable 'soliplex_template' package (src/), so it builds as a wheel. PEP 420
45
+ # namespace package (no __init__.py); clients import the module directly, e.g.
46
+ # 'from soliplex_template.rooms import ...'.
47
+ [tool.setuptools.packages.find]
48
+ where = ["src"]
49
+ namespaces = true
50
+
51
+ # Hermetic unit tests for the repo's Python scripts. The suite holds 100% branch
52
+ # coverage; --cov=scripts measures the repo-level build tooling, --cov=skill/scripts
53
+ # the bundled generator/version helpers, and --cov=tests/unit keeps the test
54
+ # files themselves dead-code-free.
55
+ [tool.pytest.ini_options]
56
+ python_files = "test_*.py"
57
+ # Functional tests live under tests/functional/ and are opt-in (they generate a
58
+ # real project and shell out to git/openssl/docker). Run them explicitly with
59
+ # uv run --group dev pytest tests/functional --no-cov
60
+ # (--no-cov because the generator runs in a child process, so it isn't measured
61
+ # and would otherwise trip --cov-fail-under). They are not collected by default.
62
+ testpaths = ["tests/unit"]
63
+ addopts = "--cov=scripts --cov=skills/soliplex-template/scripts --cov=src/soliplex_template --cov=tests/unit --cov-branch --cov-fail-under=100"
64
+ markers = [
65
+ "needs_docker: functional test requiring the docker CLI + daemon",
66
+ ]
67
+
68
+ [tool.coverage.report]
69
+ show_missing = true
70
+
71
+ # Lint/format config mirrors soliplex/soliplex (same rule selection, line length,
72
+ # single-line imports, and pytest-style tweaks). ruff-format is enforced by the
73
+ # pre-commit hook; `ruff check` uses these rules on demand.
74
+ [tool.ruff]
75
+ line-length = 79
76
+ exclude = [
77
+ ]
78
+ target-version = "py313"
79
+
80
+ [tool.ruff.lint]
81
+ select = [
82
+ # flake8
83
+ "F",
84
+ # pycodestyle
85
+ "E",
86
+ # flake8-bugbear
87
+ "B",
88
+ # pyupgrade
89
+ "U",
90
+ # isort
91
+ "I",
92
+ # pandas vet
93
+ "PD",
94
+ # tryceratops
95
+ "TRY",
96
+ # flake8-pytest-style
97
+ "PT",
98
+ ]
99
+ ignore = [
100
+ "PT019", # incorrecly flags '@mock.patch' args as fixtures
101
+ ]
102
+
103
+ [tool.ruff.lint.isort]
104
+ force-single-line = true
105
+
106
+ [tool.ruff.lint.flake8-pytest-style]
107
+ parametrize-names-type = "csv"
108
+
109
+ # pymarkdown config mirrors soliplex/soliplex (front-matter on; the noisy
110
+ # stylistic rules disabled).
111
+ [tool.pymarkdown]
112
+ extensions.front-matter.enabled = true
113
+ plugins.line-length.enabled = false # md013
114
+ plugins.no-duplicate-heading.enabled = false # md024
115
+ plugins.no-inline-html.enabled = false # md033
116
+ plugins.no-emphasis-as-heading.enabled = false # md036
117
+ plugins.first-line-heading.enabled = false # md041
118
+ plugins.code-block-style.enabled = false # md046
119
+
120
+
121
+ # Lets the `soliplex-skills` console script (and CI) manage the published
122
+ # `soliplex-template` skill without repeating these constants on the command
123
+ # line. The bundled scripts/skill_versions.py hardcodes the same values, since
124
+ # it cannot read this file once the skill is installed elsewhere.
125
+ [[tool.soliplex-skills.skill]]
126
+ name = "soliplex-template"
127
+ owner = "soliplex"
128
+ repo = "soliplex-template"
129
+ asset_tarball = "soliplex-template-skill.tar.gz"
130
+ pointer_tag = "template-skill-latest"
131
+ rolling_prefix = "template-skill"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,218 @@
1
+ """Generic, template-agnostic logic for adding a room to a Soliplex stack.
2
+
3
+ This is the reusable core behind the ``soliplex-template`` skill's bundled
4
+ ``add_room.py`` -- a PEP 723 shim that owns the ``.mako`` templates and the
5
+ CLI and delegates the stack-level work here. It ships in the published
6
+ ``soliplex-template`` distribution so the skill (and any other consumer) can
7
+ ``from soliplex_template.rooms import ...``.
8
+
9
+ It works on a *rendered* room config (text the caller produced however it likes
10
+ -- a template, an existing room's config, or built by hand) plus the stack's
11
+ ``installation.yaml``, which it edits line-based (comment-preserving):
12
+
13
+ - ``resolve_project`` / ``resolve_package_name`` -- locate + introspect it.
14
+ - ``validate_room_id`` -- the room-id / path-segment rule.
15
+ - ``add_room_path`` -- ensure ``room_paths`` loads the room (``added`` /
16
+ ``COVERED`` by a ``./rooms`` parent entry / ``unchanged`` when already
17
+ listed), preserving comments and layout.
18
+ - ``install_room`` -- write the room dir + config (+ optional prompt file) and
19
+ apply the ``room_paths`` edit; honors dry-run and force.
20
+
21
+ Pure filesystem work -- no Docker, no running backend, stdlib only.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import dataclasses
27
+ import pathlib
28
+ import re
29
+
30
+ # A room id usable as a path segment and a YAML id: no '/', no '..', no
31
+ # leading dot (mirrors rag_db.py's DB_NAME_RE).
32
+ ROOM_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$")
33
+
34
+ # Placeholder when the stack's own package can't be inferred (no 'src/<pkg>/');
35
+ # the '<pkg>.tools.greeting' demo tool in the skill templates references it.
36
+ DEFAULT_PACKAGE_NAME = "your_package"
37
+ # Written into the room dir when a prompt file is supplied; the config then
38
+ # points its system_prompt at this file (the 'search' demo room uses the form).
39
+ PROMPT_FILE_NAME = "prompt.txt"
40
+
41
+ # Stack markers: the files that mark a directory as a generated stack.
42
+ COMPOSE_FILE = "docker-compose.yml"
43
+ ENVIRONMENT_DIR = pathlib.PurePosixPath("backend", "environment")
44
+ INSTALLATION_FILE = ENVIRONMENT_DIR / "installation.yaml"
45
+ ROOMS_DIR = ENVIRONMENT_DIR / "rooms"
46
+
47
+ # room_paths splice: the anchor line and the entry-already-present probe.
48
+ _ROOM_PATHS_RE = re.compile(r"^room_paths:\s*$")
49
+
50
+ ADDED = "added"
51
+ UNCHANGED = "unchanged"
52
+ # room_paths may point at the rooms parent directory to auto-discover every
53
+ # room beneath it; when it does, a new room needs no room_paths entry.
54
+ ROOMS_PARENT_ENTRY = "./rooms"
55
+ COVERED = f'covered by "{ROOMS_PARENT_ENTRY}"'
56
+
57
+
58
+ class AddRoomError(Exception):
59
+ """A user-facing error (printed without a traceback).
60
+
61
+ Message construction lives in these classmethod factories so call sites
62
+ read ``raise AddRoomError.<reason>(...)`` with no inline message string.
63
+ """
64
+
65
+ @classmethod
66
+ def compose_not_found(cls, path):
67
+ return cls(
68
+ f"no {COMPOSE_FILE} at {path} "
69
+ "(run with --project-dir pointing at the stack directory)"
70
+ )
71
+
72
+ @classmethod
73
+ def not_a_stack(cls, path):
74
+ return cls(
75
+ f"{path} is not a generated Soliplex stack: missing "
76
+ f"'{INSTALLATION_FILE}'"
77
+ )
78
+
79
+ @classmethod
80
+ def bad_room_id(cls, room_id):
81
+ return cls(
82
+ f"room id {room_id!r} must match {ROOM_ID_RE.pattern} "
83
+ "(letters, digits, '.', '_', '-'; no leading dot)"
84
+ )
85
+
86
+ @classmethod
87
+ def unknown_template(cls, name, available):
88
+ avail = ", ".join(sorted(available)) or "(none found)"
89
+ return cls(f"unknown template {name!r}; available: {avail}")
90
+
91
+ @classmethod
92
+ def prompt_file_missing(cls, path):
93
+ return cls(f"prompt file {path} does not exist")
94
+
95
+ @classmethod
96
+ def room_exists(cls, path):
97
+ return cls(f"{path} already exists (use force to overwrite it)")
98
+
99
+ @classmethod
100
+ def no_room_paths(cls, path):
101
+ return cls(
102
+ f"no 'room_paths:' block in {path} to extend "
103
+ "(unexpected installation.yaml shape)"
104
+ )
105
+
106
+
107
+ def validate_room_id(room_id: str) -> None:
108
+ if not ROOM_ID_RE.match(room_id):
109
+ raise AddRoomError.bad_room_id(room_id)
110
+
111
+
112
+ def resolve_project(project_dir: str) -> pathlib.Path:
113
+ """Return the resolved stack root, or raise if it is not a stack."""
114
+ project = pathlib.Path(project_dir).resolve()
115
+ if not (project / COMPOSE_FILE).is_file():
116
+ raise AddRoomError.compose_not_found(project / COMPOSE_FILE)
117
+ if not (project / INSTALLATION_FILE).is_file():
118
+ raise AddRoomError.not_a_stack(project)
119
+ return project
120
+
121
+
122
+ def resolve_package_name(project: pathlib.Path, override: str | None) -> str:
123
+ """The stack's own package (for ``<pkg>.tools.greeting``), or placeholder.
124
+
125
+ Prefer an explicit ``override``; otherwise infer the single package under
126
+ ``src/`` (the generator scaffolds ``src/<package_name>/tools.py``); failing
127
+ that, return ``DEFAULT_PACKAGE_NAME`` for the operator to edit.
128
+ """
129
+ if override is not None:
130
+ return override
131
+ src = project / "src"
132
+ if src.is_dir():
133
+ packages = [
134
+ child.name
135
+ for child in sorted(src.iterdir())
136
+ if child.is_dir() and (child / "tools.py").is_file()
137
+ ]
138
+ if len(packages) == 1:
139
+ return packages[0]
140
+ return DEFAULT_PACKAGE_NAME
141
+
142
+
143
+ def add_room_path(text: str, room_id: str) -> tuple[str, str]:
144
+ """Ensure ``room_paths`` loads ``rooms/<room_id>``; return (text, action).
145
+
146
+ Action is ``"unchanged"`` when the explicit entry is already listed,
147
+ ``COVERED`` when a ``./rooms`` entry already auto-discovers every room
148
+ beneath it (so no entry is needed), or ``"added"`` when the
149
+ ``- "./rooms/<id>"`` entry is spliced in. The edit is line-based, so
150
+ comments and unrelated layout are preserved. Raises ``AddRoomError`` when
151
+ the file has no top-level ``room_paths:`` block.
152
+ """
153
+ entry = f"{ROOMS_PARENT_ENTRY}/{room_id}"
154
+ probe = re.compile(r'-\s*["\']?' + re.escape(entry) + r'["\']?\s*$')
155
+ parent_probe = re.compile(
156
+ r'-\s*["\']?' + re.escape(ROOMS_PARENT_ENTRY) + r'/?["\']?\s*$'
157
+ )
158
+ lines = text.splitlines(keepends=True)
159
+ if any(probe.search(line) for line in lines):
160
+ return text, UNCHANGED
161
+ if any(parent_probe.search(line) for line in lines):
162
+ return text, COVERED
163
+ idx = next(
164
+ (i for i, line in enumerate(lines) if _ROOM_PATHS_RE.match(line)),
165
+ None,
166
+ )
167
+ if idx is None:
168
+ raise AddRoomError.no_room_paths(INSTALLATION_FILE)
169
+ lines.insert(idx + 1, f' - "{entry}"\n')
170
+ return "".join(lines), ADDED
171
+
172
+
173
+ @dataclasses.dataclass(frozen=True)
174
+ class RoomInstall:
175
+ """The outcome of ``install_room``: where the config went + the room_paths
176
+ action (``added`` / ``COVERED`` / ``unchanged``)."""
177
+
178
+ config_path: pathlib.Path
179
+ path_action: str
180
+
181
+
182
+ def install_room(
183
+ project: pathlib.Path,
184
+ room_id: str,
185
+ *,
186
+ config_text: str,
187
+ prompt_text: str | None = None,
188
+ force: bool = False,
189
+ dry_run: bool = False,
190
+ ) -> RoomInstall:
191
+ """Install a rendered room into ``project``; return a ``RoomInstall``.
192
+
193
+ Writes ``rooms/<room_id>/room_config.yaml`` (and ``prompt.txt`` when
194
+ ``prompt_text`` is given), and ensures ``room_paths`` loads it. With
195
+ ``dry_run`` it computes the outcome but writes nothing. Raises
196
+ ``AddRoomError`` when the room directory already exists and ``force`` is
197
+ false. ``config_text`` is template-agnostic -- any caller-produced room
198
+ config.
199
+ """
200
+ room_dir = project / ROOMS_DIR / room_id
201
+ config_path = room_dir / "room_config.yaml"
202
+ if room_dir.exists() and not force:
203
+ raise AddRoomError.room_exists(room_dir)
204
+
205
+ installation = project / INSTALLATION_FILE
206
+ new_installation, path_action = add_room_path(
207
+ installation.read_text(), room_id
208
+ )
209
+
210
+ if not dry_run:
211
+ room_dir.mkdir(parents=True, exist_ok=True)
212
+ config_path.write_text(config_text)
213
+ if prompt_text is not None:
214
+ (room_dir / PROMPT_FILE_NAME).write_text(prompt_text)
215
+ if path_action == ADDED:
216
+ installation.write_text(new_installation)
217
+
218
+ return RoomInstall(config_path=config_path, path_action=path_action)
@@ -0,0 +1,7 @@
1
+ Metadata-Version: 2.4
2
+ Name: soliplex-template
3
+ Version: 0.11
4
+ Summary: Docker Compose template for a Soliplex stack, plus the soliplex-template Agent Skill and its build tooling.
5
+ Requires-Python: >=3.12
6
+ License-File: LICENSE
7
+ Dynamic: license-file
@@ -0,0 +1,8 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/soliplex_template/rooms.py
5
+ src/soliplex_template.egg-info/PKG-INFO
6
+ src/soliplex_template.egg-info/SOURCES.txt
7
+ src/soliplex_template.egg-info/dependency_links.txt
8
+ src/soliplex_template.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ soliplex_template