chumak 0.1.0__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.
- chumak-0.1.0/.github/dependabot.yml +18 -0
- chumak-0.1.0/.github/workflows/ci.yaml +81 -0
- chumak-0.1.0/.gitignore +32 -0
- chumak-0.1.0/.python-version +1 -0
- chumak-0.1.0/CHANGELOG.md +13 -0
- chumak-0.1.0/CONTRIBUTING.md +78 -0
- chumak-0.1.0/PKG-INFO +155 -0
- chumak-0.1.0/README.md +140 -0
- chumak-0.1.0/pyproject.toml +53 -0
- chumak-0.1.0/src/chumak/__init__.py +48 -0
- chumak-0.1.0/src/chumak/handlers/__init__.py +27 -0
- chumak-0.1.0/src/chumak/handlers/base.py +49 -0
- chumak-0.1.0/src/chumak/handlers/langchain.py +47 -0
- chumak-0.1.0/src/chumak/handlers/subprocess.py +135 -0
- chumak-0.1.0/src/chumak/handlers/types.py +20 -0
- chumak-0.1.0/src/chumak/loader.py +259 -0
- chumak-0.1.0/src/chumak/meta.py +80 -0
- chumak-0.1.0/src/chumak/profile.py +100 -0
- chumak-0.1.0/src/chumak/response.py +125 -0
- chumak-0.1.0/src/chumak/surface.py +44 -0
- chumak-0.1.0/tests/__init__.py +0 -0
- chumak-0.1.0/tests/conftest.py +135 -0
- chumak-0.1.0/tests/test_langchain_live.py +93 -0
- chumak-0.1.0/tests/test_loader.py +220 -0
- chumak-0.1.0/tests/test_meta.py +73 -0
- chumak-0.1.0/tests/test_profile.py +67 -0
- chumak-0.1.0/tests/test_subprocess_handler.py +129 -0
- chumak-0.1.0/tests/test_surface.py +57 -0
- chumak-0.1.0/uv.lock +1319 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
version: 2
|
|
2
|
+
updates:
|
|
3
|
+
- package-ecosystem: "uv"
|
|
4
|
+
directory: "/"
|
|
5
|
+
schedule:
|
|
6
|
+
interval: "weekly"
|
|
7
|
+
commit-message:
|
|
8
|
+
prefix: "fix"
|
|
9
|
+
prefix-development: "chore"
|
|
10
|
+
include: "scope"
|
|
11
|
+
|
|
12
|
+
- package-ecosystem: "github-actions"
|
|
13
|
+
directory: "/"
|
|
14
|
+
schedule:
|
|
15
|
+
interval: "weekly"
|
|
16
|
+
commit-message:
|
|
17
|
+
prefix: "ci"
|
|
18
|
+
include: "scope"
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
name: ๐ฆ๐ CI
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
on:
|
|
5
|
+
push:
|
|
6
|
+
branches:
|
|
7
|
+
- '**'
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
permissions:
|
|
11
|
+
contents: write
|
|
12
|
+
pull-requests: write
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
env:
|
|
16
|
+
DEFAULT_PYTHON_VERSION: '3.13'
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
jobs:
|
|
20
|
+
validation:
|
|
21
|
+
name: ๐งช Validation
|
|
22
|
+
if: ${{ !contains(github.event.head_commit.message, 'chore(main)') }} # don't run on chore(main) commits (e.g. release - it already ran)
|
|
23
|
+
strategy:
|
|
24
|
+
fail-fast: false
|
|
25
|
+
matrix:
|
|
26
|
+
python-version: ${{ github.ref == 'refs/heads/main' && fromJson('["3.13"]') || fromJson('["default"]') }}
|
|
27
|
+
os: ${{ github.ref == 'refs/heads/main' && fromJson('["ubuntu-latest", "macos-latest", "windows-latest"]') || fromJson('["ubuntu-latest"]') }}
|
|
28
|
+
|
|
29
|
+
runs-on: ${{ matrix.os }}
|
|
30
|
+
permissions:
|
|
31
|
+
contents: read
|
|
32
|
+
issues: none
|
|
33
|
+
pull-requests: none
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
steps:
|
|
37
|
+
- uses: actions/checkout@v6
|
|
38
|
+
|
|
39
|
+
- uses: corriander/gha/uv/test@main
|
|
40
|
+
env:
|
|
41
|
+
UV_PYTHON: ${{ matrix.python-version == 'default' && env.DEFAULT_PYTHON_VERSION || matrix.python-version }}
|
|
42
|
+
|
|
43
|
+
release:
|
|
44
|
+
name: ๐ Release
|
|
45
|
+
runs-on: ubuntu-latest
|
|
46
|
+
needs: validation
|
|
47
|
+
permissions:
|
|
48
|
+
contents: write
|
|
49
|
+
pull-requests: write
|
|
50
|
+
id-token: write # for PyPI trusted publishing
|
|
51
|
+
if: | # always run on default branch as long as no failures, even if validation is skipped
|
|
52
|
+
always()
|
|
53
|
+
&& !contains(needs.*.result, 'failure')
|
|
54
|
+
&& github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
|
|
55
|
+
steps:
|
|
56
|
+
- uses: googleapis/release-please-action@v5
|
|
57
|
+
id: release
|
|
58
|
+
with:
|
|
59
|
+
release-type: python
|
|
60
|
+
|
|
61
|
+
- uses: actions/checkout@v6
|
|
62
|
+
|
|
63
|
+
- uses: astral-sh/setup-uv@v7
|
|
64
|
+
if: steps.release.outputs.release_created
|
|
65
|
+
|
|
66
|
+
- name: Build
|
|
67
|
+
if: steps.release.outputs.release_created
|
|
68
|
+
run: uv build
|
|
69
|
+
|
|
70
|
+
- uses: svenstaro/upload-release-action@v2
|
|
71
|
+
if: steps.release.outputs.release_created
|
|
72
|
+
with:
|
|
73
|
+
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
|
74
|
+
file: dist/*
|
|
75
|
+
tag: ${{ steps.release.outputs.tag_name }}
|
|
76
|
+
file_glob: true
|
|
77
|
+
make_latest: false
|
|
78
|
+
|
|
79
|
+
- name: Publish to PyPI
|
|
80
|
+
if: steps.release.outputs.release_created
|
|
81
|
+
run: uv publish --trusted-publishing always
|
chumak-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.py[cod]
|
|
3
|
+
*$py.class
|
|
4
|
+
|
|
5
|
+
.venv/
|
|
6
|
+
venv/
|
|
7
|
+
.python-version-local
|
|
8
|
+
|
|
9
|
+
build/
|
|
10
|
+
dist/
|
|
11
|
+
*.egg-info/
|
|
12
|
+
.eggs/
|
|
13
|
+
|
|
14
|
+
.pytest_cache/
|
|
15
|
+
.ruff_cache/
|
|
16
|
+
.ty_cache/
|
|
17
|
+
.coverage
|
|
18
|
+
.coverage.*
|
|
19
|
+
htmlcov/
|
|
20
|
+
.tox/
|
|
21
|
+
|
|
22
|
+
.env
|
|
23
|
+
.env.local
|
|
24
|
+
.env.*.local
|
|
25
|
+
|
|
26
|
+
.vscode/
|
|
27
|
+
.idea/
|
|
28
|
+
*.swp
|
|
29
|
+
*.swo
|
|
30
|
+
.DS_Store
|
|
31
|
+
|
|
32
|
+
uv.lock.local
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.13
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 (2026-05-23)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* scaffold chumak inference substrate ([37187d8](https://github.com/corriander/chumak/commit/37187d84c775f43e1cfe3922769031e265dd169b))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **deps:** bump langchain-openai from 1.2.1 to 1.2.2 ([fc7bb6a](https://github.com/corriander/chumak/commit/fc7bb6a486a049a79a81806c12703ce528443444))
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
## Principles
|
|
4
|
+
|
|
5
|
+
- **Keep the substrate thin.** chumak coordinates handlers, profiles, and meta. It does not own prompts, domain concepts, or secret management โ those are consumer concerns.
|
|
6
|
+
- **Handlers are pluggable.** Adding a transport (LangChain, subprocess, โฆ) is dropping a module under `src/chumak/handlers/`, extending `HandlerType`, and registering it in `HANDLER_REGISTRY`. No edits in `surface.py` or `profile.py`.
|
|
7
|
+
- **The library never reads env vars unprompted.** The profile env-overlay is the one exception, gated on the prefix the consumer passes to `ProfileLoader`.
|
|
8
|
+
- **Prefer fixtures over hand-rolled setup.** See `tests/conftest.py` (`write_profile`, `make_loader`, `stub_handler`).
|
|
9
|
+
|
|
10
|
+
## Dev setup
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
git clone <repo>
|
|
14
|
+
cd chumak
|
|
15
|
+
uv sync # core deps + dev tools (ruff, ty, pytest, pytest-mock, langchain-anthropic)
|
|
16
|
+
uv sync --extra openai # add when running the LangChain live integration test
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Python 3.12+ (no 3.13-only syntax is used โ keep it that way to stay broadly compatible with consumer projects).
|
|
20
|
+
|
|
21
|
+
## Tests
|
|
22
|
+
|
|
23
|
+
### Unit tests (default โ fast, hermetic, no network)
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
uv run pytest
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
These cover every code path. The `stub_handler` fixture swaps the LangChain handler in `HANDLER_REGISTRY` for a deterministic fake so surface/meta tests never touch a real LLM. Use it when writing new tests that need a known `HandlerResult`.
|
|
30
|
+
|
|
31
|
+
Profile-loader tests use `write_profile` to drop TOML files into a per-test tmp dir and `make_loader` to build a `ProfileLoader` pointed at it. Pass `env=` to inject a synthetic environment without mutating `os.environ`.
|
|
32
|
+
|
|
33
|
+
### Integration test โ LangChain handler against an OpenAI-compat backend
|
|
34
|
+
|
|
35
|
+
Marker-gated, off by default. Exercises chumak end-to-end through a real LangChain `init_chat_model` call against any OpenAI-API-compatible server: llama.cpp, vLLM, LiteLLM, real OpenAI, etc. This is the test that proves the `openai:<model>` + `base_url` wiring still works after handler / langchain version bumps.
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
uv sync --extra openai # install langchain-openai
|
|
39
|
+
uv run pytest --integration tests/test_langchain_live.py -v
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Defaults: `http://localhost:8080/v1`, model `gpt-3.5-turbo`, dummy API key. Override per-run via env vars:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
CHUMAK_TEST_OPENAI_URL=http://localhost:8000/v1 \
|
|
46
|
+
CHUMAK_TEST_OPENAI_MODEL=qwen2.5-7b-instruct \
|
|
47
|
+
uv run pytest --integration tests/test_langchain_live.py -v
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The test uses a tiny `ColourTag { colour: str, is_warm: bool }` schema โ small enough that any reasonable 7B-class instruct model handles it. If you add coverage for a new handler or option, add a sibling test under the same marker and document the env vars it needs here.
|
|
51
|
+
|
|
52
|
+
#### Why no subprocess live test?
|
|
53
|
+
|
|
54
|
+
The subprocess handler is harder to gate (each CLI has its own auth / install requirements). Cover it with unit tests against a temporary script (`tests/test_subprocess_handler.py` does this), and rely on downstream consumers for true end-to-end exercise.
|
|
55
|
+
|
|
56
|
+
## Quality checks
|
|
57
|
+
|
|
58
|
+
Before opening a PR:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
uv run ruff check .
|
|
62
|
+
uv run ruff format .
|
|
63
|
+
uv run ty check src tests
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
All three must pass clean. CI will run the same.
|
|
67
|
+
|
|
68
|
+
## Adding a handler
|
|
69
|
+
|
|
70
|
+
1. Module under `src/chumak/handlers/<name>.py` exporting a class with `execute(prompt, output_schema, profile) -> HandlerResult`.
|
|
71
|
+
2. Add the discriminator to `HandlerType` in `src/chumak/handlers/types.py`.
|
|
72
|
+
3. Register the class in `HANDLER_REGISTRY` (`src/chumak/handlers/__init__.py`).
|
|
73
|
+
4. If the handler needs new profile fields, add them to `Profile` with the validator enforcing mutually-exclusive field sets per handler.
|
|
74
|
+
5. Add unit tests via `stub_handler` for the surface side and direct handler tests for transport-specific quirks.
|
|
75
|
+
|
|
76
|
+
## Adding a new profile field
|
|
77
|
+
|
|
78
|
+
Profile fields live in `src/chumak/profile.py`. The validator enforces which handler types may use which fields โ extend it when you add fields that only make sense for a specific handler. The env-overlay walker in `loader.py` picks them up automatically as long as they're declared on the `Profile` model.
|
chumak-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: chumak
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Thin inference substrate: user-authored profiles, LangChain as a handler, optional provenance.
|
|
5
|
+
Author-email: Alex Corrie <alex.j.corrie@gmail.com>
|
|
6
|
+
Requires-Python: >=3.12
|
|
7
|
+
Requires-Dist: langchain-core>=0.3
|
|
8
|
+
Requires-Dist: langchain>=0.3
|
|
9
|
+
Requires-Dist: pydantic>=2.10
|
|
10
|
+
Provides-Extra: anthropic
|
|
11
|
+
Requires-Dist: langchain-anthropic>=0.3; extra == 'anthropic'
|
|
12
|
+
Provides-Extra: openai
|
|
13
|
+
Requires-Dist: langchain-openai>=1.2.2; extra == 'openai'
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# chumak
|
|
17
|
+
|
|
18
|
+
A thin **inference substrate** for Python projects: user-authored profiles, LangChain
|
|
19
|
+
as a handler, optional provenance/meta on every response.
|
|
20
|
+
|
|
21
|
+
> *Chumaks* (ะงัะผะฐะบะธ) were wandering Ukrainian salt-traders who traversed the steppe
|
|
22
|
+
> between distant places. They named the Milky Way after themselves โ
|
|
23
|
+
> *ะงัะผะฐััะบะธะน ะจะปัั
*, the Chumaks' Way โ because they navigated by it.
|
|
24
|
+
|
|
25
|
+
## What it is
|
|
26
|
+
|
|
27
|
+
A small library that abstracts away which LLM you're calling and how.
|
|
28
|
+
|
|
29
|
+
1. Author **profiles** (TOML files) under the app's XDG config dir.
|
|
30
|
+
2. Load a profile via `ProfileLoader` (with inheritance + env-var overrides)
|
|
31
|
+
3. Call `infer(prompt=..., output_schema=..., profile=...)` and get back a validated
|
|
32
|
+
pydantic payload, normalised citations, and (optionally) a provenance `Meta` stamp.
|
|
33
|
+
|
|
34
|
+
Two built-in handlers:
|
|
35
|
+
|
|
36
|
+
- **`langchain`** โ uses `langchain.chat_models.init_chat_model(profile.model)` so a
|
|
37
|
+
single identifier (`anthropic:claude-opus-4-7`, `openai:gpt-5`, โฆ) routes to the
|
|
38
|
+
right provider. Structured output, citations, and token usage all handled.
|
|
39
|
+
- **`subprocess`** โ shells out to a CLI (`claude --print`, `codex exec`, etc.).
|
|
40
|
+
Useful for prompt iteration via an existing, authorised tool.
|
|
41
|
+
Schema is injected into the prompt as JSON Schema; stdout is parsed and validated.
|
|
42
|
+
|
|
43
|
+
## Profiles
|
|
44
|
+
|
|
45
|
+
Profiles are user-authored. chumak ships at most one generic example
|
|
46
|
+
(`anthropic-claude-opus-4-7` via the LangChain handler). Everything else is yours.
|
|
47
|
+
|
|
48
|
+
Profiles live in consumer app directory, e.g. `~/.config/<your-app>/chumak/profiles/`.
|
|
49
|
+
chumak does not impose a config dir; the app passes `search_paths` to `ProfileLoader`.
|
|
50
|
+
|
|
51
|
+
### File shape
|
|
52
|
+
|
|
53
|
+
```toml
|
|
54
|
+
# ~/.config/galops-vision/chumak/profiles/claude.toml
|
|
55
|
+
handler = "langchain"
|
|
56
|
+
model = "anthropic:claude-opus-4-7"
|
|
57
|
+
temperature = 0.0
|
|
58
|
+
|
|
59
|
+
[model_kwargs]
|
|
60
|
+
max_tokens = 4096
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Inheritance
|
|
64
|
+
|
|
65
|
+
```toml
|
|
66
|
+
# claude-account-b.toml
|
|
67
|
+
extends = "claude"
|
|
68
|
+
|
|
69
|
+
[model_kwargs]
|
|
70
|
+
# api_key sourced from env โ see below
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Env-var overlay
|
|
74
|
+
|
|
75
|
+
Every field on a profile is overridable from the environment. chumak does not
|
|
76
|
+
provision special fields; the convention is uniform:
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
{APP_PREFIX}_PROFILE_{PROFILE_NAME}_{FIELD_PATH}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
with `__` as the nested-field delimiter (single `_` stays inside field names):
|
|
83
|
+
|
|
84
|
+
```sh
|
|
85
|
+
# top-level field
|
|
86
|
+
export MYAPP_VISION_PROFILE_CLAUDE_MODEL=anthropic:claude-haiku-4-5
|
|
87
|
+
|
|
88
|
+
# nested into model_kwargs
|
|
89
|
+
export MYAPP_VISION_PROFILE_CLAUDE_ACCOUNT_B_MODEL_KWARGS__API_KEY=sk-ant-...
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
This means a profile file can be effectively empty on disk (just declaring the
|
|
93
|
+
profile's existence and maybe an `extends`), with all values supplied by the
|
|
94
|
+
environment. You decide which fields are sensitive and never touch disk.
|
|
95
|
+
|
|
96
|
+
## Usage
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from pathlib import Path
|
|
100
|
+
from chumak import ProfileLoader, infer
|
|
101
|
+
|
|
102
|
+
loader = ProfileLoader(
|
|
103
|
+
search_paths=[Path.home() / ".config/my-app/chumak/profiles"],
|
|
104
|
+
env_prefix="MYAPP",
|
|
105
|
+
)
|
|
106
|
+
loader.names() # -> ["claude", "claude-creative", ...]
|
|
107
|
+
profile = loader.load("claude")
|
|
108
|
+
|
|
109
|
+
from pydantic import BaseModel
|
|
110
|
+
|
|
111
|
+
class AnneSchema(BaseModel):
|
|
112
|
+
title: str
|
|
113
|
+
value: int
|
|
114
|
+
|
|
115
|
+
result = infer(
|
|
116
|
+
prompt="Extract title and value from this text: ...",
|
|
117
|
+
output_schema=AnneSchema,
|
|
118
|
+
profile=profile,
|
|
119
|
+
)
|
|
120
|
+
result.payload # -> MissionTitle(title=..., bounty=...)
|
|
121
|
+
result.citations # -> [Citation, ...] (if the model supplied any)
|
|
122
|
+
result.meta # -> Meta with cost, generated_at, model identity
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### With provenance
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
from chumak import Provenance
|
|
129
|
+
|
|
130
|
+
result = infer(
|
|
131
|
+
prompt="...",
|
|
132
|
+
output_schema=AnneSchema,
|
|
133
|
+
profile=profile,
|
|
134
|
+
provenance=Provenance(
|
|
135
|
+
artefact_type="model@v1",
|
|
136
|
+
artefact_id="artifact-type:2026-05-20T12:34:56Z",
|
|
137
|
+
),
|
|
138
|
+
)
|
|
139
|
+
result.meta.artefact_type # "mission_title@v1"
|
|
140
|
+
result.meta.derived_from # [...]
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Design notes
|
|
144
|
+
|
|
145
|
+
- **No domain knowledge**: chumak carries no built-in prompts, no role concepts
|
|
146
|
+
(tactical/narrator etc. โ that's an app concern; just name your profile).
|
|
147
|
+
- **LangChain is a handler, not the spine**: subprocess CLIs are first-class.
|
|
148
|
+
- **Provenance is opt-in**: omit `provenance=` and `meta.artefact_type` is `None`.
|
|
149
|
+
- **The lib never reads env directly** for its own settings. The env overlay
|
|
150
|
+
for profiles is a deliberate, scoped exception, gated on the prefix the
|
|
151
|
+
consumer passes in.
|
|
152
|
+
|
|
153
|
+
## Tooling
|
|
154
|
+
|
|
155
|
+
uv, Python 3.12+, ruff, ty, pytest. See [CONTRIBUTING.md](CONTRIBUTING.md) for dev setup, the integration test, and quality-check commands.
|
chumak-0.1.0/README.md
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# chumak
|
|
2
|
+
|
|
3
|
+
A thin **inference substrate** for Python projects: user-authored profiles, LangChain
|
|
4
|
+
as a handler, optional provenance/meta on every response.
|
|
5
|
+
|
|
6
|
+
> *Chumaks* (ะงัะผะฐะบะธ) were wandering Ukrainian salt-traders who traversed the steppe
|
|
7
|
+
> between distant places. They named the Milky Way after themselves โ
|
|
8
|
+
> *ะงัะผะฐััะบะธะน ะจะปัั
*, the Chumaks' Way โ because they navigated by it.
|
|
9
|
+
|
|
10
|
+
## What it is
|
|
11
|
+
|
|
12
|
+
A small library that abstracts away which LLM you're calling and how.
|
|
13
|
+
|
|
14
|
+
1. Author **profiles** (TOML files) under the app's XDG config dir.
|
|
15
|
+
2. Load a profile via `ProfileLoader` (with inheritance + env-var overrides)
|
|
16
|
+
3. Call `infer(prompt=..., output_schema=..., profile=...)` and get back a validated
|
|
17
|
+
pydantic payload, normalised citations, and (optionally) a provenance `Meta` stamp.
|
|
18
|
+
|
|
19
|
+
Two built-in handlers:
|
|
20
|
+
|
|
21
|
+
- **`langchain`** โ uses `langchain.chat_models.init_chat_model(profile.model)` so a
|
|
22
|
+
single identifier (`anthropic:claude-opus-4-7`, `openai:gpt-5`, โฆ) routes to the
|
|
23
|
+
right provider. Structured output, citations, and token usage all handled.
|
|
24
|
+
- **`subprocess`** โ shells out to a CLI (`claude --print`, `codex exec`, etc.).
|
|
25
|
+
Useful for prompt iteration via an existing, authorised tool.
|
|
26
|
+
Schema is injected into the prompt as JSON Schema; stdout is parsed and validated.
|
|
27
|
+
|
|
28
|
+
## Profiles
|
|
29
|
+
|
|
30
|
+
Profiles are user-authored. chumak ships at most one generic example
|
|
31
|
+
(`anthropic-claude-opus-4-7` via the LangChain handler). Everything else is yours.
|
|
32
|
+
|
|
33
|
+
Profiles live in consumer app directory, e.g. `~/.config/<your-app>/chumak/profiles/`.
|
|
34
|
+
chumak does not impose a config dir; the app passes `search_paths` to `ProfileLoader`.
|
|
35
|
+
|
|
36
|
+
### File shape
|
|
37
|
+
|
|
38
|
+
```toml
|
|
39
|
+
# ~/.config/galops-vision/chumak/profiles/claude.toml
|
|
40
|
+
handler = "langchain"
|
|
41
|
+
model = "anthropic:claude-opus-4-7"
|
|
42
|
+
temperature = 0.0
|
|
43
|
+
|
|
44
|
+
[model_kwargs]
|
|
45
|
+
max_tokens = 4096
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Inheritance
|
|
49
|
+
|
|
50
|
+
```toml
|
|
51
|
+
# claude-account-b.toml
|
|
52
|
+
extends = "claude"
|
|
53
|
+
|
|
54
|
+
[model_kwargs]
|
|
55
|
+
# api_key sourced from env โ see below
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Env-var overlay
|
|
59
|
+
|
|
60
|
+
Every field on a profile is overridable from the environment. chumak does not
|
|
61
|
+
provision special fields; the convention is uniform:
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
{APP_PREFIX}_PROFILE_{PROFILE_NAME}_{FIELD_PATH}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
with `__` as the nested-field delimiter (single `_` stays inside field names):
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
# top-level field
|
|
71
|
+
export MYAPP_VISION_PROFILE_CLAUDE_MODEL=anthropic:claude-haiku-4-5
|
|
72
|
+
|
|
73
|
+
# nested into model_kwargs
|
|
74
|
+
export MYAPP_VISION_PROFILE_CLAUDE_ACCOUNT_B_MODEL_KWARGS__API_KEY=sk-ant-...
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
This means a profile file can be effectively empty on disk (just declaring the
|
|
78
|
+
profile's existence and maybe an `extends`), with all values supplied by the
|
|
79
|
+
environment. You decide which fields are sensitive and never touch disk.
|
|
80
|
+
|
|
81
|
+
## Usage
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from pathlib import Path
|
|
85
|
+
from chumak import ProfileLoader, infer
|
|
86
|
+
|
|
87
|
+
loader = ProfileLoader(
|
|
88
|
+
search_paths=[Path.home() / ".config/my-app/chumak/profiles"],
|
|
89
|
+
env_prefix="MYAPP",
|
|
90
|
+
)
|
|
91
|
+
loader.names() # -> ["claude", "claude-creative", ...]
|
|
92
|
+
profile = loader.load("claude")
|
|
93
|
+
|
|
94
|
+
from pydantic import BaseModel
|
|
95
|
+
|
|
96
|
+
class AnneSchema(BaseModel):
|
|
97
|
+
title: str
|
|
98
|
+
value: int
|
|
99
|
+
|
|
100
|
+
result = infer(
|
|
101
|
+
prompt="Extract title and value from this text: ...",
|
|
102
|
+
output_schema=AnneSchema,
|
|
103
|
+
profile=profile,
|
|
104
|
+
)
|
|
105
|
+
result.payload # -> MissionTitle(title=..., bounty=...)
|
|
106
|
+
result.citations # -> [Citation, ...] (if the model supplied any)
|
|
107
|
+
result.meta # -> Meta with cost, generated_at, model identity
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### With provenance
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
from chumak import Provenance
|
|
114
|
+
|
|
115
|
+
result = infer(
|
|
116
|
+
prompt="...",
|
|
117
|
+
output_schema=AnneSchema,
|
|
118
|
+
profile=profile,
|
|
119
|
+
provenance=Provenance(
|
|
120
|
+
artefact_type="model@v1",
|
|
121
|
+
artefact_id="artifact-type:2026-05-20T12:34:56Z",
|
|
122
|
+
),
|
|
123
|
+
)
|
|
124
|
+
result.meta.artefact_type # "mission_title@v1"
|
|
125
|
+
result.meta.derived_from # [...]
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Design notes
|
|
129
|
+
|
|
130
|
+
- **No domain knowledge**: chumak carries no built-in prompts, no role concepts
|
|
131
|
+
(tactical/narrator etc. โ that's an app concern; just name your profile).
|
|
132
|
+
- **LangChain is a handler, not the spine**: subprocess CLIs are first-class.
|
|
133
|
+
- **Provenance is opt-in**: omit `provenance=` and `meta.artefact_type` is `None`.
|
|
134
|
+
- **The lib never reads env directly** for its own settings. The env overlay
|
|
135
|
+
for profiles is a deliberate, scoped exception, gated on the prefix the
|
|
136
|
+
consumer passes in.
|
|
137
|
+
|
|
138
|
+
## Tooling
|
|
139
|
+
|
|
140
|
+
uv, Python 3.12+, ruff, ty, pytest. See [CONTRIBUTING.md](CONTRIBUTING.md) for dev setup, the integration test, and quality-check commands.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "chumak"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Thin inference substrate: user-authored profiles, LangChain as a handler, optional provenance."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
authors = [
|
|
8
|
+
{ name = "Alex Corrie", email = "alex.j.corrie@gmail.com" },
|
|
9
|
+
]
|
|
10
|
+
dependencies = [
|
|
11
|
+
"pydantic>=2.10",
|
|
12
|
+
"langchain>=0.3",
|
|
13
|
+
"langchain-core>=0.3",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.optional-dependencies]
|
|
17
|
+
anthropic = ["langchain-anthropic>=0.3"]
|
|
18
|
+
openai = ["langchain-openai>=1.2.2"]
|
|
19
|
+
|
|
20
|
+
[dependency-groups]
|
|
21
|
+
dev = [
|
|
22
|
+
"pytest>=8",
|
|
23
|
+
"pytest-mock>=3.14",
|
|
24
|
+
"ruff>=0.15.14",
|
|
25
|
+
"ty>=0.0.21",
|
|
26
|
+
"langchain-anthropic>=0.3",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[build-system]
|
|
30
|
+
requires = ["hatchling"]
|
|
31
|
+
build-backend = "hatchling.build"
|
|
32
|
+
|
|
33
|
+
[tool.hatch.build]
|
|
34
|
+
dev-mode-dirs = ["src"]
|
|
35
|
+
|
|
36
|
+
[tool.hatch.build.targets.wheel]
|
|
37
|
+
packages = ["src/chumak"]
|
|
38
|
+
|
|
39
|
+
[tool.ruff]
|
|
40
|
+
src = ["src"]
|
|
41
|
+
line-length = 99
|
|
42
|
+
|
|
43
|
+
[tool.ruff.lint]
|
|
44
|
+
select = ["E", "F", "W", "I", "UP", "B", "SIM", "RUF"]
|
|
45
|
+
|
|
46
|
+
[tool.ty.environment]
|
|
47
|
+
extra-paths = ["src"]
|
|
48
|
+
|
|
49
|
+
[tool.pytest.ini_options]
|
|
50
|
+
testpaths = ["tests"]
|
|
51
|
+
markers = [
|
|
52
|
+
"integration: requires a live inference backend (use --integration to enable)",
|
|
53
|
+
]
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""chumak โ thin inference substrate.
|
|
2
|
+
|
|
3
|
+
Public surface in `surface.infer`. Profile-loading in `loader.ProfileLoader`.
|
|
4
|
+
Response types in `response`. Handlers are pluggable via `handlers.HANDLER_REGISTRY`.
|
|
5
|
+
|
|
6
|
+
The library is subject-agnostic and carries no domain knowledge: no
|
|
7
|
+
built-in prompts, no role names, no per-domain artefact types. Consumers
|
|
8
|
+
build those on top.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from chumak.handlers import HANDLER_REGISTRY, Handler, HandlerType, PromptDelivery
|
|
12
|
+
from chumak.loader import (
|
|
13
|
+
ProfileCycleError,
|
|
14
|
+
ProfileLoader,
|
|
15
|
+
ProfileLoaderError,
|
|
16
|
+
ProfileNotFoundError,
|
|
17
|
+
)
|
|
18
|
+
from chumak.profile import Profile
|
|
19
|
+
from chumak.response import (
|
|
20
|
+
ArtefactRef,
|
|
21
|
+
Citation,
|
|
22
|
+
Cost,
|
|
23
|
+
InferResult,
|
|
24
|
+
Meta,
|
|
25
|
+
ProducedBy,
|
|
26
|
+
Provenance,
|
|
27
|
+
)
|
|
28
|
+
from chumak.surface import infer
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"HANDLER_REGISTRY",
|
|
32
|
+
"ArtefactRef",
|
|
33
|
+
"Citation",
|
|
34
|
+
"Cost",
|
|
35
|
+
"Handler",
|
|
36
|
+
"HandlerType",
|
|
37
|
+
"InferResult",
|
|
38
|
+
"Meta",
|
|
39
|
+
"ProducedBy",
|
|
40
|
+
"Profile",
|
|
41
|
+
"ProfileCycleError",
|
|
42
|
+
"ProfileLoader",
|
|
43
|
+
"ProfileLoaderError",
|
|
44
|
+
"ProfileNotFoundError",
|
|
45
|
+
"PromptDelivery",
|
|
46
|
+
"Provenance",
|
|
47
|
+
"infer",
|
|
48
|
+
]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Handlers package.
|
|
2
|
+
|
|
3
|
+
Owns the `HandlerType` discriminator and the `HANDLER_REGISTRY` mapping each
|
|
4
|
+
`HandlerType` to its concrete handler class. Adding a new handler type means
|
|
5
|
+
dropping a module here, extending `HandlerType` in `types.py`, and adding
|
|
6
|
+
the class to the registry below โ no edits in `surface.py` or `profile.py`.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from chumak.handlers.base import Handler, HandlerResult
|
|
10
|
+
from chumak.handlers.langchain import LangChainHandler
|
|
11
|
+
from chumak.handlers.subprocess import SubprocessHandler
|
|
12
|
+
from chumak.handlers.types import HandlerType, PromptDelivery
|
|
13
|
+
|
|
14
|
+
HANDLER_REGISTRY: dict[HandlerType, type[Handler]] = {
|
|
15
|
+
HandlerType.LANGCHAIN: LangChainHandler,
|
|
16
|
+
HandlerType.SUBPROCESS: SubprocessHandler,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"HANDLER_REGISTRY",
|
|
21
|
+
"Handler",
|
|
22
|
+
"HandlerResult",
|
|
23
|
+
"HandlerType",
|
|
24
|
+
"LangChainHandler",
|
|
25
|
+
"PromptDelivery",
|
|
26
|
+
"SubprocessHandler",
|
|
27
|
+
]
|