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.
Files changed (55) hide show
  1. hemisphere_harness-0.1.0/.env.example +87 -0
  2. hemisphere_harness-0.1.0/.github/workflows/ci.yml +45 -0
  3. hemisphere_harness-0.1.0/.github/workflows/publish.yml +62 -0
  4. hemisphere_harness-0.1.0/.gitignore +10 -0
  5. hemisphere_harness-0.1.0/CONTRIBUTING.md +71 -0
  6. hemisphere_harness-0.1.0/LICENSE +21 -0
  7. hemisphere_harness-0.1.0/PKG-INFO +542 -0
  8. hemisphere_harness-0.1.0/README.md +502 -0
  9. hemisphere_harness-0.1.0/docs/architecture.md +289 -0
  10. hemisphere_harness-0.1.0/docs/images/web-run.png +0 -0
  11. hemisphere_harness-0.1.0/docs/providers.md +383 -0
  12. hemisphere_harness-0.1.0/pyproject.toml +66 -0
  13. hemisphere_harness-0.1.0/src/hemisphere_harness/__init__.py +140 -0
  14. hemisphere_harness-0.1.0/src/hemisphere_harness/__main__.py +7 -0
  15. hemisphere_harness-0.1.0/src/hemisphere_harness/_http.py +88 -0
  16. hemisphere_harness-0.1.0/src/hemisphere_harness/cli.py +228 -0
  17. hemisphere_harness-0.1.0/src/hemisphere_harness/config.py +133 -0
  18. hemisphere_harness-0.1.0/src/hemisphere_harness/conversation.py +64 -0
  19. hemisphere_harness-0.1.0/src/hemisphere_harness/errors.py +26 -0
  20. hemisphere_harness-0.1.0/src/hemisphere_harness/examples/__init__.py +0 -0
  21. hemisphere_harness-0.1.0/src/hemisphere_harness/examples/simple_loop.py +319 -0
  22. hemisphere_harness-0.1.0/src/hemisphere_harness/harness.py +609 -0
  23. hemisphere_harness-0.1.0/src/hemisphere_harness/left/__init__.py +16 -0
  24. hemisphere_harness-0.1.0/src/hemisphere_harness/left/base.py +68 -0
  25. hemisphere_harness-0.1.0/src/hemisphere_harness/left/experimental.py +257 -0
  26. hemisphere_harness-0.1.0/src/hemisphere_harness/left/jev_client.py +153 -0
  27. hemisphere_harness-0.1.0/src/hemisphere_harness/left/mock.py +75 -0
  28. hemisphere_harness-0.1.0/src/hemisphere_harness/providers.py +340 -0
  29. hemisphere_harness-0.1.0/src/hemisphere_harness/py.typed +0 -0
  30. hemisphere_harness-0.1.0/src/hemisphere_harness/right/__init__.py +14 -0
  31. hemisphere_harness-0.1.0/src/hemisphere_harness/right/base.py +80 -0
  32. hemisphere_harness-0.1.0/src/hemisphere_harness/right/llm.py +113 -0
  33. hemisphere_harness-0.1.0/src/hemisphere_harness/right/mock.py +89 -0
  34. hemisphere_harness-0.1.0/src/hemisphere_harness/right/openai_compatible.py +79 -0
  35. hemisphere_harness-0.1.0/src/hemisphere_harness/right/prompts.py +303 -0
  36. hemisphere_harness-0.1.0/src/hemisphere_harness/schemas.py +386 -0
  37. hemisphere_harness-0.1.0/src/hemisphere_harness/trace.py +96 -0
  38. hemisphere_harness-0.1.0/src/hemisphere_harness/web/__init__.py +5 -0
  39. hemisphere_harness-0.1.0/src/hemisphere_harness/web/app.py +487 -0
  40. hemisphere_harness-0.1.0/src/hemisphere_harness/web/envfile.py +171 -0
  41. hemisphere_harness-0.1.0/src/hemisphere_harness/web/runs.py +428 -0
  42. hemisphere_harness-0.1.0/src/hemisphere_harness/web/static/app.css +530 -0
  43. hemisphere_harness-0.1.0/src/hemisphere_harness/web/static/app.js +1792 -0
  44. hemisphere_harness-0.1.0/src/hemisphere_harness/web/static/favicon.svg +1 -0
  45. hemisphere_harness-0.1.0/src/hemisphere_harness/web/static/index.html +40 -0
  46. hemisphere_harness-0.1.0/src/hemisphere_harness/web/static/locked.html +19 -0
  47. hemisphere_harness-0.1.0/tests/conftest.py +24 -0
  48. hemisphere_harness-0.1.0/tests/test_conversation.py +231 -0
  49. hemisphere_harness-0.1.0/tests/test_experimental_left.py +253 -0
  50. hemisphere_harness-0.1.0/tests/test_harness.py +540 -0
  51. hemisphere_harness-0.1.0/tests/test_jev_client.py +233 -0
  52. hemisphere_harness-0.1.0/tests/test_mock_and_config.py +126 -0
  53. hemisphere_harness-0.1.0/tests/test_providers.py +443 -0
  54. hemisphere_harness-0.1.0/tests/test_right_openai_compatible.py +137 -0
  55. 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,10 @@
1
+ .env
2
+ .hemisphere/
3
+ .venv/
4
+ __pycache__/
5
+ *.py[cod]
6
+ *.egg-info/
7
+ .pytest_cache/
8
+ dist/
9
+ build/
10
+ .mypy_cache/
@@ -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.