Metadata-Version: 2.4
Name: oiax
Version: 0.1.3
Summary: oiax — semantic policy routing for agent fleets. Delivers the right governance context to the right agent at the right turn, by meaning.
Author: Brian McMahon
License: AGPL-3.0
Project-URL: Homepage, https://github.com/nousergon/oiax
Project-URL: Repository, https://github.com/nousergon/oiax
Project-URL: Issues, https://github.com/nousergon/oiax/issues
Keywords: agent,policy-routing,governance,semantic-search,normative-text
Classifier: License :: OSI Approved :: GNU Affero General Public License v3
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Operating System :: OS Independent
Classifier: Typing :: Typed
Classifier: Intended Audience :: Developers
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: fastembed>=0.8.0
Requires-Dist: scikit-learn>=1.3
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: pytest-cov>=4.0; extra == "dev"
Requires-Dist: mypy>=1.0; extra == "dev"
Requires-Dist: ruff>=0.4; extra == "dev"
Dynamic: license-file

# oiax

> **oiax** — semantic policy routing for agent fleets. The tiller that delivers the right governance context to the right agent at the right turn, by meaning.

[![CI](https://github.com/nousergon/oiax/actions/workflows/ci.yml/badge.svg)](https://github.com/nousergon/oiax/actions/workflows/ci.yml)

oiax routes a free-text prompt against a governance corpus and delivers the policies that bear on that turn — by meaning, before the agent decides, at ~6ms with no network call.

**The retrieval design is for normative text** (policies, coding standards, ADRs, compliance rules), not general knowledge. Four decisions make it correct:

1. **Whole-document delivery, never chunks.** A rule and its carve-out are semantically distant but logically inseparable.
2. **Precision over recall, asymmetric errors.** A miss degrades to the status quo; a false positive actively degrades the layer.
3. **Surface names, never rules.** Matched terms make a bad match dismissible at a glance.
4. **Runtime-agnostic core, harness-specific adapters.** The router returns structured hits; each harness gets its own thin delivery layer.

## Installation

```bash
pip install oiax
```

Requires Python ≥ 3.11. On first use, a ~90MB ONNX embedding model downloads and caches locally. Subsequent routes are ~6ms.

## Quick start

### Route a prompt

```python
from oiax import build_index, route
from oiax.corpus import PolicyDirCorpus

# Load from a directory of markdown files, each carrying an **Agent-trigger:**
# line (see "Corpus format" below)
corpus = PolicyDirCorpus("./policies/")
index = build_index(corpus)

# Route a prompt — at most two hits, ranked by reciprocal-rank fusion
hits = route("How do I deploy to production?", index)
for hit in hits:
    print(f"{hit.name} ({hit.score:.2f}): {', '.join(hit.why)}")
```

### Route with query expansions

```python
import json

expansions = json.load(open("./routing-expansions.json"))
index = build_index(corpus, expansions=expansions)
hits = route("help me merge my PR", index)
```

### Use a custom corpus

```python
from oiax.corpus import Document

class MyCorpus:
    def documents(self):
        yield Document(
            name="deploy-policy",
            trigger_line="deploying to production",
            body="Always run the test suite before deploying...",
        )

hits = route("deploy to prod", build_index(MyCorpus()))
```

## Corpus format

Policy files are markdown with an `**Agent-trigger:**` header — a one-line statement of what the document governs. This is used for both lexical matching (TF-IDF) and semantic matching (embeddings).

```markdown
# My deploy policy

**Agent-trigger:** deploying the application to production, CI/CD configuration

Always run the test suite before deploying. Never deploy on Friday.
```

The `PolicyDirCorpus` loader reads all `*.md` files in a directory. The filename (without `.md`) becomes the document `name` returned in route hits.

## Claude Code integration

`oiax.adapters.claude_code` is a `UserPromptSubmit` hook adapter. Register it in `~/.claude/settings.json`:

```json
{
  "hooks": {
    "UserPromptSubmit": [
      {
        "matcher": "",
        "hooks": [{
          "type": "command",
          "command": "python3 -m oiax.adapters.claude_code /path/to/policies/ --expansions /path/to/expansions.json",
          "timeout": 8
        }]
      }
    ]
  }
}
```

On every prompt, the adapter routes the prompt text against the policy corpus and injects a context paragraph naming the policies that may apply — with the matched terms, so a bad match is dismissible at a glance. Never blocks: any error exits 0 silently.

## How selection works

Both scorers rank every document. Their rankings are combined by **reciprocal-rank
fusion** — each scorer contributes `1 / (60 + rank)` — and the top two documents are
returned. A document ranked moderately by *both* scorers therefore beats one ranked
first by only one, which is the whole reason to run a hybrid.

`lex_threshold` and `sem_threshold` are **admission floors** ("is this document a
candidate at all"), not the selection rule. They are what makes abstention possible:
a prompt neither scorer admits routes to nothing.

Absolute score cutoffs are deliberately not the selection rule. TF-IDF cosine and
embedding cosine are not on a common scale, and the right cutoff for either moves
with the corpus. Through 0.1.2 oiax selected on absolute cutoffs with a semantic
threshold of 0.55; on the reference corpus, correct semantic matches score 0.40–0.48,
so the semantic half never fired and recall sat at 0.185. Rank fusion is scale-free.

Defaults are calibrated, not chosen: `src/oiax/eval/corpora/README.md` records the
sweep, the operating point, and what it was picked over.

## Evaluation harness

Measure routing quality against labelled ground truth:

```bash
python -m oiax.eval.route_eval score ./policies/ < labelled.jsonl   # shipped config
python -m oiax.eval.route_eval sweep ./policies/ < labelled.jsonl   # the full grid
```

The labelled file is JSONL — one JSON object per line:

```json
{"prompt": "How do I deploy to production?", "expected": ["deploy-policy"]}
{"prompt": "What's for lunch?", "expected": []}
```

Reported: `recall@2`, `precision`, `F1`, `top-1 accuracy`, and the false-alarm rate over
negative prompts (`"expected": []`). Read precision against the two-hit cap — with one
expected label it cannot exceed 0.5 for that prompt.

Two corpora ship at `oiax/eval/corpora/`: a 15-document **reference** corpus with 52
labelled prompts (the calibration set — recall@2 0.648, top-1 0.673, zero false alarms),
and a 5-document synthetic smoke corpus that is structurally useful and **cannot**
calibrate anything. Judge labels are evidence, not proof — hand-check a slice before
treating any rate as authoritative.

## API

### `oiax.router`

| Callable | Signature | Returns |
|---|---|---|
| `route` | `route(prompt: str, index: Index | None = None) -> list[RouteHit]` | Scored hits |
| `build_index` | `build_index(corpus, *, expansions, lex_threshold, sem_threshold, rrf_k, top_k) -> Index` | Built index |
| `semantic_ready` | `semantic_ready() -> bool` | `False` when the embedding model failed to load and routing is lexical-only — surface it, do not swallow it |

### `RouteHit`

```python
@dataclass(frozen=True)
class RouteHit:
    name: str       # document name (surface name only, never body text)
    score: float    # best RAW scorer score, [0, 1] — hits are ORDERED by fusion, not by this
    why: list[str]  # matched terms, and/or "semantic match"
```

### `oiax.corpus`

| Class | Purpose |
|---|---|
| `Document(name, trigger_line, body)` | One document in the routing corpus |
| `Corpus` (Protocol) | Any object with `.documents() -> Iterator[Document]` |
| `PolicyDirCorpus(path)` | Reads `*.md` files with `**Agent-trigger:**` headers |

### `oiax.adapters`

| Module | Purpose |
|---|---|
| `claude_code.py` | UserPromptSubmit hook adapter |
| `stdout.py` | Debug adapter — prints hits as text |

## When you need oiax

You need oiax when your rule corpus is too large to inject into every context (context-window pressure, attention dilution) and too important to leave to the agent's judgment (silent policy violations).

You do **not** need oiax when your corpus fits in a single `CLAUDE.md` — static injection is free and optimal for that case.

## Development

```bash
git clone https://github.com/nousergon/oiax.git
cd oiax
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"

pytest                          # test suite
ruff check src/ tests/          # lint
mypy src/oiax                   # type check
```

All three run in CI on Python 3.11, 3.12 and 3.13 and are required to merge. See
[CONTRIBUTING.md](CONTRIBUTING.md).

## License

AGPL-3.0 — see [LICENSE](LICENSE).
