hemisphere-harness 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.
- hemisphere_harness-0.1.0/.env.example +87 -0
- hemisphere_harness-0.1.0/.github/workflows/ci.yml +45 -0
- hemisphere_harness-0.1.0/.github/workflows/publish.yml +62 -0
- hemisphere_harness-0.1.0/.gitignore +10 -0
- hemisphere_harness-0.1.0/CONTRIBUTING.md +71 -0
- hemisphere_harness-0.1.0/LICENSE +21 -0
- hemisphere_harness-0.1.0/PKG-INFO +542 -0
- hemisphere_harness-0.1.0/README.md +502 -0
- hemisphere_harness-0.1.0/docs/architecture.md +289 -0
- hemisphere_harness-0.1.0/docs/images/web-run.png +0 -0
- hemisphere_harness-0.1.0/docs/providers.md +383 -0
- hemisphere_harness-0.1.0/pyproject.toml +66 -0
- hemisphere_harness-0.1.0/src/hemisphere_harness/__init__.py +140 -0
- hemisphere_harness-0.1.0/src/hemisphere_harness/__main__.py +7 -0
- hemisphere_harness-0.1.0/src/hemisphere_harness/_http.py +88 -0
- hemisphere_harness-0.1.0/src/hemisphere_harness/cli.py +228 -0
- hemisphere_harness-0.1.0/src/hemisphere_harness/config.py +133 -0
- hemisphere_harness-0.1.0/src/hemisphere_harness/conversation.py +64 -0
- hemisphere_harness-0.1.0/src/hemisphere_harness/errors.py +26 -0
- hemisphere_harness-0.1.0/src/hemisphere_harness/examples/__init__.py +0 -0
- hemisphere_harness-0.1.0/src/hemisphere_harness/examples/simple_loop.py +319 -0
- hemisphere_harness-0.1.0/src/hemisphere_harness/harness.py +609 -0
- hemisphere_harness-0.1.0/src/hemisphere_harness/left/__init__.py +16 -0
- hemisphere_harness-0.1.0/src/hemisphere_harness/left/base.py +68 -0
- hemisphere_harness-0.1.0/src/hemisphere_harness/left/experimental.py +257 -0
- hemisphere_harness-0.1.0/src/hemisphere_harness/left/jev_client.py +153 -0
- hemisphere_harness-0.1.0/src/hemisphere_harness/left/mock.py +75 -0
- hemisphere_harness-0.1.0/src/hemisphere_harness/providers.py +340 -0
- hemisphere_harness-0.1.0/src/hemisphere_harness/py.typed +0 -0
- hemisphere_harness-0.1.0/src/hemisphere_harness/right/__init__.py +14 -0
- hemisphere_harness-0.1.0/src/hemisphere_harness/right/base.py +80 -0
- hemisphere_harness-0.1.0/src/hemisphere_harness/right/llm.py +113 -0
- hemisphere_harness-0.1.0/src/hemisphere_harness/right/mock.py +89 -0
- hemisphere_harness-0.1.0/src/hemisphere_harness/right/openai_compatible.py +79 -0
- hemisphere_harness-0.1.0/src/hemisphere_harness/right/prompts.py +303 -0
- hemisphere_harness-0.1.0/src/hemisphere_harness/schemas.py +386 -0
- hemisphere_harness-0.1.0/src/hemisphere_harness/trace.py +96 -0
- hemisphere_harness-0.1.0/src/hemisphere_harness/web/__init__.py +5 -0
- hemisphere_harness-0.1.0/src/hemisphere_harness/web/app.py +487 -0
- hemisphere_harness-0.1.0/src/hemisphere_harness/web/envfile.py +171 -0
- hemisphere_harness-0.1.0/src/hemisphere_harness/web/runs.py +428 -0
- hemisphere_harness-0.1.0/src/hemisphere_harness/web/static/app.css +530 -0
- hemisphere_harness-0.1.0/src/hemisphere_harness/web/static/app.js +1792 -0
- hemisphere_harness-0.1.0/src/hemisphere_harness/web/static/favicon.svg +1 -0
- hemisphere_harness-0.1.0/src/hemisphere_harness/web/static/index.html +40 -0
- hemisphere_harness-0.1.0/src/hemisphere_harness/web/static/locked.html +19 -0
- hemisphere_harness-0.1.0/tests/conftest.py +24 -0
- hemisphere_harness-0.1.0/tests/test_conversation.py +231 -0
- hemisphere_harness-0.1.0/tests/test_experimental_left.py +253 -0
- hemisphere_harness-0.1.0/tests/test_harness.py +540 -0
- hemisphere_harness-0.1.0/tests/test_jev_client.py +233 -0
- hemisphere_harness-0.1.0/tests/test_mock_and_config.py +126 -0
- hemisphere_harness-0.1.0/tests/test_providers.py +443 -0
- hemisphere_harness-0.1.0/tests/test_right_openai_compatible.py +137 -0
- hemisphere_harness-0.1.0/tests/test_web.py +545 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Copy to .env. Everything is optional: with nothing set, the example runs offline.
|
|
2
|
+
# `python -m hemisphere_harness` lists the providers you can name below.
|
|
3
|
+
|
|
4
|
+
# --- Right hemisphere (the Master) ------------------------------------------------------
|
|
5
|
+
# Setting RIGHT_MODEL is what switches the right hemisphere from the offline script to a
|
|
6
|
+
# live LLM. Pick a strong, long-context model: this is the part that holds the whole picture.
|
|
7
|
+
#
|
|
8
|
+
# RIGHT_PROVIDER endpoint key variable
|
|
9
|
+
# openai (default) https://api.openai.com/v1 OPENAI_API_KEY
|
|
10
|
+
# nvidia https://integrate.api.nvidia.com/v1 NVIDIA_API_KEY
|
|
11
|
+
# xai, grok https://api.x.ai/v1 XAI_API_KEY
|
|
12
|
+
# openrouter https://openrouter.ai/api/v1 OPENROUTER_API_KEY
|
|
13
|
+
# ollama http://localhost:11434/v1 none
|
|
14
|
+
# lmstudio http://localhost:1234/v1 none
|
|
15
|
+
# vllm http://localhost:8000/v1 none
|
|
16
|
+
# llamacpp http://localhost:8080/v1 none
|
|
17
|
+
# openai-compatible set RIGHT_BASE_URL RIGHT_API_KEY, if needed
|
|
18
|
+
RIGHT_PROVIDER=openai
|
|
19
|
+
RIGHT_MODEL=
|
|
20
|
+
|
|
21
|
+
# The key for the provider you chose. A provider's key is sent only to its own endpoint.
|
|
22
|
+
OPENAI_API_KEY=
|
|
23
|
+
# NVIDIA_API_KEY=
|
|
24
|
+
# XAI_API_KEY=
|
|
25
|
+
# OPENROUTER_API_KEY=
|
|
26
|
+
|
|
27
|
+
# Override the provider's endpoint, for a proxy or a self-hosted server:
|
|
28
|
+
# RIGHT_BASE_URL=http://localhost:8000/v1
|
|
29
|
+
# Override the provider's key variable. This one is sent to whichever endpoint is in use:
|
|
30
|
+
# RIGHT_API_KEY=
|
|
31
|
+
|
|
32
|
+
# RIGHT_TIMEOUT_S=120
|
|
33
|
+
# Unset by default, because some models reject the parameter:
|
|
34
|
+
# RIGHT_TEMPERATURE=0.2
|
|
35
|
+
# true also sends response_format={"type": "json_object"}. Helps small local models:
|
|
36
|
+
# RIGHT_JSON_MODE=false
|
|
37
|
+
|
|
38
|
+
# --- Left hemisphere (the Emissary) -----------------------------------------------------
|
|
39
|
+
# LEFT_PROVIDER what it is needs
|
|
40
|
+
# auto (default) jev if JEV_API_KEY is set, otherwise mock
|
|
41
|
+
# jev TypeSafe AI's hosted Jev JEV_API_KEY
|
|
42
|
+
# ollama a decision model served by Ollama, locally LEFT_MODEL
|
|
43
|
+
# systemone any other server implementing /v1/systemone LEFT_BASE_URL, LEFT_MODEL
|
|
44
|
+
# llm-experimental EXPERIMENTAL: a chat model, not calibrated LEFT_MODEL
|
|
45
|
+
# mock offline stand-in for development
|
|
46
|
+
# LEFT_PROVIDER=auto
|
|
47
|
+
|
|
48
|
+
# TypeSafe's hosted Jev. Keys: https://console.typesafe.ai/keys
|
|
49
|
+
JEV_API_KEY=
|
|
50
|
+
# The client appends /v1/systemone to the base URL:
|
|
51
|
+
# JEV_BASE_URL=https://api.typesafe.ai
|
|
52
|
+
# JEV_MODEL=jev-latest
|
|
53
|
+
# JEV_TIMEOUT_S=10
|
|
54
|
+
# TypeSafe's own names are accepted too: TYPESAFE_API_KEY, TYPESAFE_BASE_URL,
|
|
55
|
+
# TYPESAFE_DEFAULT_MODEL.
|
|
56
|
+
|
|
57
|
+
# A local decision model through Ollama. `ollama show <model>` must list "decision".
|
|
58
|
+
# LEFT_PROVIDER=ollama
|
|
59
|
+
# LEFT_MODEL=tev1
|
|
60
|
+
|
|
61
|
+
# For ollama, systemone and llm-experimental:
|
|
62
|
+
# LEFT_BASE_URL=http://localhost:11434
|
|
63
|
+
# LEFT_API_KEY=
|
|
64
|
+
# The first call may have to load the model, so the default is generous:
|
|
65
|
+
# LEFT_TIMEOUT_S=120
|
|
66
|
+
# llm-experimental only. "none" stops a reasoning model thinking before it answers;
|
|
67
|
+
# "omit" leaves the parameter out for servers that reject it:
|
|
68
|
+
# LEFT_REASONING_EFFORT=none
|
|
69
|
+
|
|
70
|
+
# --- The loop ---------------------------------------------------------------------------
|
|
71
|
+
# Minimum probability of the left's pick for the left to commit on its own:
|
|
72
|
+
# HARNESS_COMMIT_THRESHOLD=0.8
|
|
73
|
+
# Policy gate: at or above this, an option is allowed without sign-off:
|
|
74
|
+
# HARNESS_PERMIT_YES_THRESHOLD=0.7
|
|
75
|
+
# Policy gate: at or below this, only a human can approve an option:
|
|
76
|
+
# HARNESS_PERMIT_NO_THRESHOLD=0.3
|
|
77
|
+
# HARNESS_MAX_STEPS=6
|
|
78
|
+
# Operator policy in plain language. Leave unset to disable the gate:
|
|
79
|
+
# HARNESS_POLICY=Refunds above $500 need human sign-off.
|
|
80
|
+
# Before the answer is shown, the left checks its factual claims and the right revises
|
|
81
|
+
# any that do not hold. Skipped when the left is the mock:
|
|
82
|
+
# HARNESS_CHECK_ANSWERS=true
|
|
83
|
+
|
|
84
|
+
# --- Web UI (hemisphere-harness web) ----------------------------------------------------
|
|
85
|
+
# The UI reads and writes this file. Without this, a new sign-in token is made at every
|
|
86
|
+
# start. Read from the shell only, not from this file:
|
|
87
|
+
# HARNESS_WEB_TOKEN=
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
concurrency:
|
|
13
|
+
group: ci-${{ github.ref }}
|
|
14
|
+
cancel-in-progress: true
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
test:
|
|
18
|
+
name: Python ${{ matrix.python-version }}
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
timeout-minutes: 10
|
|
21
|
+
strategy:
|
|
22
|
+
fail-fast: false
|
|
23
|
+
matrix:
|
|
24
|
+
python-version: ["3.11", "3.12", "3.13", "3.14"]
|
|
25
|
+
steps:
|
|
26
|
+
- uses: actions/checkout@v5
|
|
27
|
+
|
|
28
|
+
- uses: actions/setup-python@v6
|
|
29
|
+
with:
|
|
30
|
+
python-version: ${{ matrix.python-version }}
|
|
31
|
+
cache: pip
|
|
32
|
+
cache-dependency-path: pyproject.toml
|
|
33
|
+
|
|
34
|
+
- name: Install
|
|
35
|
+
run: python -m pip install -e ".[dev]"
|
|
36
|
+
|
|
37
|
+
- name: Tests
|
|
38
|
+
run: python -m pytest -q
|
|
39
|
+
|
|
40
|
+
- name: Types
|
|
41
|
+
run: python -m mypy --strict src
|
|
42
|
+
|
|
43
|
+
# With nothing configured the example runs offline, so this needs no secrets.
|
|
44
|
+
- name: Offline example
|
|
45
|
+
run: python -m hemisphere_harness.examples.simple_loop
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
# Runs when a release is published on GitHub. Uploads use PyPI's trusted publishing, so no
|
|
4
|
+
# PyPI token is stored anywhere. The upload job runs in the "pypi" environment, which can
|
|
5
|
+
# require an approval before anything is published.
|
|
6
|
+
|
|
7
|
+
on:
|
|
8
|
+
release:
|
|
9
|
+
types: [published]
|
|
10
|
+
workflow_dispatch:
|
|
11
|
+
|
|
12
|
+
permissions: {}
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
build:
|
|
16
|
+
name: Build and check
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
permissions:
|
|
19
|
+
contents: read
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v5
|
|
22
|
+
|
|
23
|
+
- uses: actions/setup-python@v6
|
|
24
|
+
with:
|
|
25
|
+
python-version: "3.13"
|
|
26
|
+
|
|
27
|
+
- name: The release tag matches the package version
|
|
28
|
+
if: github.event_name == 'release'
|
|
29
|
+
run: |
|
|
30
|
+
version=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])")
|
|
31
|
+
if [ "${GITHUB_REF_NAME}" != "v${version}" ]; then
|
|
32
|
+
echo "Release tag ${GITHUB_REF_NAME} does not match version ${version} in pyproject.toml" >&2
|
|
33
|
+
exit 1
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
- name: Build
|
|
37
|
+
run: |
|
|
38
|
+
python -m pip install build twine
|
|
39
|
+
python -m build
|
|
40
|
+
python -m twine check --strict dist/*
|
|
41
|
+
|
|
42
|
+
- uses: actions/upload-artifact@v4
|
|
43
|
+
with:
|
|
44
|
+
name: dist
|
|
45
|
+
path: dist/
|
|
46
|
+
|
|
47
|
+
publish:
|
|
48
|
+
name: Upload to PyPI
|
|
49
|
+
needs: build
|
|
50
|
+
runs-on: ubuntu-latest
|
|
51
|
+
environment:
|
|
52
|
+
name: pypi
|
|
53
|
+
url: https://pypi.org/project/hemisphere-harness/
|
|
54
|
+
permissions:
|
|
55
|
+
id-token: write # trusted publishing: PyPI checks this workflow's identity, no token needed
|
|
56
|
+
steps:
|
|
57
|
+
- uses: actions/download-artifact@v4
|
|
58
|
+
with:
|
|
59
|
+
name: dist
|
|
60
|
+
path: dist/
|
|
61
|
+
|
|
62
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
## Setup
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
python -m venv .venv && source .venv/bin/activate # or: uv venv && source .venv/bin/activate
|
|
7
|
+
pip install -e ".[dev]" # or: uv pip install -e ".[dev]"
|
|
8
|
+
|
|
9
|
+
pytest
|
|
10
|
+
python -m hemisphere_harness.examples.simple_loop # the offline demo
|
|
11
|
+
python -m hemisphere_harness # lists providers
|
|
12
|
+
hemisphere-harness web # the web UI
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The web UI's page is plain HTML, CSS and JavaScript in `src/hemisphere_harness/web/static/`,
|
|
16
|
+
with no build step and no dependencies. Edit and reload. Insert anything a model or a
|
|
17
|
+
user wrote with `textContent`, never `innerHTML`, and keep everything loaded from the
|
|
18
|
+
server itself, because the Content-Security-Policy allows nothing else.
|
|
19
|
+
|
|
20
|
+
The test suite makes no network calls and needs no credentials. Please keep it that way:
|
|
21
|
+
test HTTP clients against `httpx.MockTransport`, as `tests/test_jev_client.py` and
|
|
22
|
+
`tests/test_right_openai_compatible.py` do.
|
|
23
|
+
|
|
24
|
+
Types are checked with `mypy --strict src`. CI runs the tests, the type check and the
|
|
25
|
+
offline example on Python 3.11 to 3.14 for every pull request, so run them locally first.
|
|
26
|
+
|
|
27
|
+
## Adding a provider
|
|
28
|
+
|
|
29
|
+
This is the most useful kind of contribution, and usually the smallest.
|
|
30
|
+
[docs/providers.md](docs/providers.md) has the full guide. In short:
|
|
31
|
+
|
|
32
|
+
- **An OpenAI-compatible service** is one line in `src/hemisphere_harness/providers.py`,
|
|
33
|
+
one table row, and one test parameter.
|
|
34
|
+
- **A service with its own API** is a subclass of `LLMRightHemisphere` with one method,
|
|
35
|
+
`complete`.
|
|
36
|
+
- **A provider that pulls in a heavy SDK** is better as its own package, published with
|
|
37
|
+
an entry point, so that this package keeps its short list of small dependencies.
|
|
38
|
+
|
|
39
|
+
## What the design protects
|
|
40
|
+
|
|
41
|
+
The project is small on purpose, and its point is a specific relationship between two
|
|
42
|
+
parts. Changes are easy to accept when they keep these true, and need a good argument
|
|
43
|
+
when they do not. [docs/architecture.md](docs/architecture.md) says where each one is
|
|
44
|
+
enforced.
|
|
45
|
+
|
|
46
|
+
1. Only the right hemisphere generates. The left returns typed answers and nothing else.
|
|
47
|
+
2. The left commits only a routine, low-stakes decision that it is sure of.
|
|
48
|
+
3. Everything else returns to the right before anything is done.
|
|
49
|
+
4. An operator's policy binds both hemispheres.
|
|
50
|
+
5. Every step ends with the right reintegrating it.
|
|
51
|
+
6. A failing left hemisphere is survivable. A failing right hemisphere is not.
|
|
52
|
+
|
|
53
|
+
Two practical consequences:
|
|
54
|
+
|
|
55
|
+
- A feature that lets the left act on more than it does today needs a test showing what
|
|
56
|
+
still stops it.
|
|
57
|
+
- The harness reads the left's probabilities as calibrated. A left-hemisphere provider
|
|
58
|
+
whose numbers are not must say so.
|
|
59
|
+
|
|
60
|
+
## Licence
|
|
61
|
+
|
|
62
|
+
The project is MIT licensed. By contributing you agree that your contribution is
|
|
63
|
+
released under the same licence.
|
|
64
|
+
|
|
65
|
+
## Pull requests
|
|
66
|
+
|
|
67
|
+
- Add or update tests for what you change. For a rule in the list above, check that the
|
|
68
|
+
test fails when the rule is broken.
|
|
69
|
+
- Keep provider-specific knowledge in the provider's own module. `harness.py` and
|
|
70
|
+
`schemas.py` should not learn any provider's name.
|
|
71
|
+
- Update `README.md`, `docs/` and `.env.example` when behaviour or configuration changes.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Zaenus
|
|
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.
|