Metadata-Version: 2.4
Name: cilow
Version: 0.3.0
Summary: Python client SDK for Cilow — the verifiable context layer for AI agents.
License: Apache-2.0
Project-URL: Homepage, https://github.com/Generalized-Labs/cilow
Project-URL: Repository, https://github.com/Generalized-Labs/cilow
Keywords: cilow,ai,agents,memory,context,bitemporal,abstention
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: OS Independent
Classifier: Intended Audience :: Developers
Classifier: Topic :: Software Development :: Libraries
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: httpx
Requires-Dist: httpx>=0.27; extra == "httpx"
Dynamic: license-file

# cilow — Python

The Python client for **Cilow**, the verifiable context layer for AI agents. Point it at a
running [`cilow-serve`](../../docs/user_guide/mcp_server.md) and call **four core verbs** —
`ingest`, `context_pack`, `forget`, `explain` — the same four the MCP core profile advertises.
The headline feature is **calibrated deferral**: every read tells you when the engine
*doesn't know* instead of fabricating an answer.

## Install

```sh
cd sdk/python && pip install -e .
```

Python 3.9+. **Zero required dependencies** — the client uses only the standard library
(`urllib` + `json`). `httpx` is an optional extra (`pip install 'cilow[httpx]'`) for users
who want pooling/async; it is not needed for the default client.

## Point it at a server

```sh
cargo build --profile fast --features serve --bin cilow-serve
CILOW_DB=/tmp/cilow.log CILOW_TOKENS="demo:1:1" ./target/fast/cilow-serve
# listening on http://0.0.0.0:8080  ·  bearer token "demo"
```

The token maps to a tenant scope that the server **stamps** onto every request — a client can
never read another tenant's memory, and a client-supplied `scope` is ignored over HTTP.

## Five lines

```python
from cilow import CilowClient

cilow = CilowClient(base_url="http://localhost:8080", token="demo")
cilow.ingest(facts=[{"subject": "Maya", "predicate": "employer", "object": "Acme"}])
r = cilow.context_pack("where does Maya work", anchor="Maya", attribute="employer")
print(r.reason if r.abstained else r.pack)      # the injectible pack, citations on r.citations
print(cilow.explain(r.trace_id).report.grounded)  # why it surfaced, vs the tenant threshold
```

## The four core verbs

| Verb | Signature | Returns | What it does |
|------|-----------|---------|--------------|
| `ingest` | `ingest(*, facts= \| text= \| url= \| content_base64=, mime=, subject_hint=, metadata=, source=, raw_context=, branch=, scope=, source_id=, observed_at=)` | `IngestResponse` | The **one write door**. Never rejects: known kinds extract, unknown kinds are retained and recallable. A new value for an exclusive attribute *supersedes* the old one (history stays queryable). |
| `context_pack` | `context_pack(query, *, scope=, now=, as_of=, anchor=, attribute=)` | `ContextPackResponse` | The **product read**: a ready-to-inject `pack`, structured `citations` / `claims` with `is_current`, and the faithfulness fields `abstained` / `reason_code` / `grounding` / `pack_sufficient` / `as_of` / `trace_id`. No free-form synthesis. |
| `forget` | `forget(entity, *, attribute=, value=, erase=, scope=, at=)` | `ForgetResponse` | **Withdraw**, not erase history: recall stops returning it; as-of-T reads before `at` still see it. `erase=True` is the destructive right-to-be-forgotten path. |
| `explain` | `explain(trace_id, *, scope=)` | `ExplainResponse` | **Why** a pack/recall surfaced what it surfaced: the cited claims (labels resolved, inclusion probability each), `best_nonconformity` vs the tenant threshold `q_hat`, and `report.grounded`. `found=False` + `error` is in-band. |

Plus `health()` (liveness probe, no auth). All responses are frozen `dataclass`es.

`ingest` takes **exactly one** content form. `source_id` (the idempotency key the wire requires)
defaults to a **content hash** — `ingest_source_id(...)` — so re-sending the same payload is a
no-op; the key actually sent is echoed on `.source_id`. `.kind` tells you which server path ran
(`"facts"` → the `remember` receipt on `.remember`; `"content"` → the raw-durable receipt on
`.content` with `blob_id` / `detected` / `reader_error`).

`anchor` (an entity surface form like `"Maya"`) + `attribute` (a predicate like `"employer"`)
take the exact-key **fast path**. Omit them for free-text semantic reads — same response shape.

### Full profile / legacy aliases

The engine's full tool profile (`CILOW_MCP_PROFILE=full`) also advertises the older verbs, and
the client keeps them as aliases with their original receipts:

| Alias | Signature | Returns |
|-------|-----------|---------|
| `remember` | `remember(facts, *, scope=, source=, source_id=, observed_at=, raw_context=, branch=)` | `RememberResponse` |
| `recall` | `recall(query, *, scope=, as_of=, now=, anchor=, attribute=, branch=)` | `RecallResponse` |
| `answer` | `answer(query, *, scope=, now=, anchor=, attribute=, branch=)` | `AnswerResponse` |
| `timeline` | `timeline(entity, *, scope=, at=, t_from=, t_to=)` | `list[TimelineEntry]` |
| `track_entity` | `track_entity(name, *, scope=, at=)` | `TrackEntityResponse` |

Also available: `remember_text`, `ingest_image` / `ingest_audio` / `ingest_pdf` /
`ingest_structured`, `save` / `ingest_any`, `feedback`, `consolidate`, `fork` / `branch_list`,
`blob_get` / `blob_meta` / `blob_list`, and the `client.memories.*` id-based REST surface.

### Time is in microseconds

Every time field (`observed_at`, `as_of`, `now`, `at`, `t_from`, `t_to`, and the `valid_from`
/ `valid_until` on timeline entries) is **epoch microseconds (µs)**, matching the engine. They
default to "now". Use the exported `now_micros()` helper:

```python
from cilow import now_micros
t = now_micros()
cilow.remember(facts, observed_at=t)
past = cilow.recall("...", as_of=t - 86_400_000_000, now=t)  # a day ago
```

## The deferral contract

`context_pack` (and the `recall` / `answer` aliases) **never raise** for a deferral — they return
a normal response with a boolean flag. When `abstained` is `True`, the pack / claims / citations
are empty and `reason_code` says why: `below_threshold` / `no_candidates` are a *calibrated*
abstain (memory genuinely lacks grounded context — do not fabricate past it), while
`provider_unavailable` is an *infrastructure* abstain (retry when the provider recovers). Branch
with `r.is_calibrated_abstain` / `r.is_infrastructure_abstain` rather than matching strings.

On `answer` / `context_pack` an abstain also names **which layer stopped it** — `r.terminal_layer`
(`structural` | `gate` | `provider` | `reader` | `verifier`; `r.stopped_at` is the same thing gated on
`abstained`). Only `provider` is retryable. Both surfaces also carry `r.evidence`: the claim, span,
and verbatim lanes collapsed into one list ordered by fused cross-lane rank (`Evidence.origins`
says which lanes agreed; `Evidence.structure` is present when a typed claim backs the unit). It is
present on abstains too — the raw lanes have no standing at the gate, but you may still read them.

```python
r = cilow.context_pack("what is Maya's salary", anchor="Maya", attribute="salary")
if r.abstained:
    # r.pack is empty; r.reason explains the configured gate decision.
    return f"I don't have that in memory ({r.reason})."
return r.pack
```

Every read carries a `trace_id`. Hand it to `explain` for the audit trail — the cited claims,
the tenant's conformal threshold `q_hat`, the best candidate's `best_nonconformity`, and the
`margin` (`q̂ − n`, ≥0 grounded, <0 a gap) — or to `feedback` so memory learns from the outcome.
This is the difference between "I don't know" and a confident wrong answer.

## Errors

`CilowError` is raised for genuine failures — auth (`401` / JSON-RPC `-32001`), bad params,
transport errors, malformed responses. A deferral is not an error.

```python
from cilow import CilowError
try:
    cilow.recall("...")
except CilowError as e:
    if e.is_unauthorized:  # bad/expired token
        ...
```

## Develop

```sh
pip install -e .
python3 -m pytest -q tests             # mocks the HTTP layer (no server needed)
python examples/quickstart.py          # runs against a live server
```
