Metadata-Version: 2.4
Name: overton-sdk
Version: 0.1.0
Summary: Python SDK for the Overton platform.
Project-URL: Repository, https://github.com/Overton-Bio/overton-sdk
Requires-Python: >=3.11
Description-Content-Type: text/markdown
Requires-Dist: httpx>=0.27
Provides-Extra: local
Requires-Dist: overton-core>=0.1.0; extra == "local"
Provides-Extra: dev
Requires-Dist: pytest; extra == "dev"
Requires-Dist: overton-core>=0.1.0; extra == "dev"
Provides-Extra: integration
Requires-Dist: overton-core[all-providers]>=0.1.0; extra == "integration"

# Overton SDK

Overton is a probabilistic knowledge graph that resolves entities and relations from
unstructured text. You write raw evidence in natural language; Overton handles LLM-based
extraction, entity deduplication, and uncertainty quantification.

## Prerequisites

You need two things from Overton:

- An **API token** (`ovtn_...`)
- Your **hostname** (e.g. `api.overton.bio`)

Overton is a hosted platform — there's nothing to run or operate on your side.
[Contact us](mailto:hello@overton.bio) to get a token.

## Installation

```bash
pip install overton-sdk
```

Requires Python 3.11+. The only dependency is `httpx`.

## Authorization and client initialization

Overton authenticates with the bearer token (`ovtn_...`) Overton issued you.
Construct an auth object and pass it to the client, along with your hostname:

```python
import os
import overton_sdk

client = overton_sdk.OvertonClient(
    auth=overton_sdk.UserTokenAuth(os.environ["OVERTON_TOKEN"]),
    hostname="overton.example.com",   # bare host; scheme defaults to https
)
```

### Environment variables

If `OVERTON_TOKEN` and `OVERTON_HOSTNAME` are set, the client picks them up with no
arguments:

```bash
export OVERTON_TOKEN=ovtn_...
export OVERTON_HOSTNAME=overton.example.com
```

```python
client = overton_sdk.OvertonClient()
```

### Configuration

Tune transport behavior with a `Config` object:

```python
from overton_sdk import Config

client = overton_sdk.OvertonClient(
    auth=overton_sdk.UserTokenAuth(token),
    hostname="overton.example.com",
    config=Config(timeout=60.0, max_retries=3, verify=True),
)
```

Use the client as a context manager to ensure connections are closed:

```python
with overton_sdk.OvertonClient() as client:
    m = client.manifold("my-manifold")
    ...
```

> [!TIP]
> For local development you can point at a server over plain HTTP by passing a full
> `hostname="http://localhost:8000"` — an explicit scheme is respected.

## Core concepts

| Concept | What it is |
|---|---|
| **Manifold** | A named knowledge graph. Create as many as you need — one per domain, project, or dataset. |
| **Evidence** | A piece of text you write in. Overton extracts entities and relations from it using an LLM. |
| **Contributor ID** | What `write()` returns — the ID of the evidence you just wrote. Use it for raw reads and curation. |
| **Entity ID** | The ID of a resolved real-world entity. Different from a contributor ID. Get entity IDs from `query()`. |
| **Covariate** | Structured metadata on evidence (`{"source": "reuters", "year": "2024"}`). Overton uses it to build conditional distributions. |

## Quickstart

```python
import os
import overton_sdk

client = overton_sdk.OvertonClient(
    auth=overton_sdk.UserTokenAuth(os.environ["OVERTON_TOKEN"]),
    hostname="overton.example.com",
)
m = client.manifold("earnings")

# Write evidence
ids = m.write("Apple reported record profits of $94B in Q1 2024, driven by iPhone sales")

# Query resolved entities
results = m.query("tech company earnings", top_k=5)
for entity in results:
    print(entity.entity_id, entity.point_estimate(), entity.type_distribution)

# Manifold statistics
stats = m.measures()
print(f"{stats.entity_count} entities, entropy={stats.aggregate_entropy:.3f}")
```

## Writing evidence

```python
# Single string
m.write("Google acquired DeepMind for £400M in 2014")

# Multiple items in one call
m.write(
    "Apple reported record Q1 profits",
    "Microsoft Azure revenue grew 29% year over year",
)

# With source and covariates
m.write({
    "text": "Apple reported record Q1 profits",
    "source": "reuters",
    "covariates": {"quarter": "Q1-2024", "sentiment": "positive"},
})

# Mix of strings and dicts
m.write(
    "Apple reported record profits",
    {"text": "Google missed analyst estimates", "covariates": {"sentiment": "negative"}},
)
```

`write()` returns a list of contributor IDs — one per entity or relation mention
extracted from your text by the LLM.

## Reading

```python
# Raw read — get back the original text using a contributor ID
text = m.read(contributor_id, raw=True)

# Resolved read — get the entity distribution using an entity ID
# Note: entity IDs come from query(), not from write()
entity = m.read(entity_id)
print(entity.type_distribution)    # {"ORG": 0.91, "PERSON": 0.09}
print(entity.point_estimate())     # "ORG"
print(entity.contributor_weights)  # which evidence pieces contributed and how much

# Read a relation
relation = m.read_relation(relation_id)
print(relation.type_distribution)
print(relation.point_estimate({"cell_line": "HeLa"}))   # conditional on a covariate
```

## Querying

```python
results = m.query("pharmaceutical company", top_k=10)

for entity in results:
    print(entity.entity_id)
    print(entity.point_estimate())      # most likely type
    print(entity.type_distribution)     # full probability distribution
    print(entity.alias_ids)             # other IDs this entity was known by
```

Results are `EntityDistribution` objects ranked by semantic similarity.

## Measures

```python
# Manifold-level
mm = m.measures()
mm.entity_count          # number of resolved entities
mm.relation_count        # number of resolved relations
mm.aggregate_entropy     # average type uncertainty across all entities (nats)
mm.component_count       # number of connected subgraphs
mm.uncertainty_map       # {entity_id: entropy} for every entity

# Entity-level
em = m.entity_measures(entity_id)
em.type_entropy            # uncertainty over entity type (0 = certain, ln(N) = maximally uncertain)
em.evidence_concentration  # 1.0 = one source dominates, 0.0 = evidence spread evenly
em.effective_sample_size   # Kish ESS — effective number of independent evidence pieces

# Relation-level
rm = m.relation_measures(relation_id)
rm.marginal_entropy        # uncertainty over relation type
rm.marginal_perplexity     # effective number of competing relation types
rm.is_contradictory        # True if evidence strongly conflicts
rm.mutual_information      # list[CovariateMI] — which covariates are informative

# Divergence between two entities or relations
div = m.divergence(entity_id_a, entity_id_b)               # Jensen-Shannon (default)
div = m.divergence(entity_id_a, entity_id_b, metric="kl")  # KL divergence
print(div.value)    # 0.0 = identical distributions, 1.0 = maximally different
```

## Curation

```python
# Retract — soft delete, reversible. Evidence is ignored in future resolutions.
event = m.retract(contributor_id)
print(event.reversible)   # True

# Hard delete — permanent. Evidence is removed entirely.
event = m.hard_delete(contributor_id)
print(event.reversible)   # False

# Merge — declare that two entity IDs refer to the same real-world entity
new_entity_id, events = m.merge(["entity-abc", "entity-xyz"])

# Split — declare that one entity ID actually contains multiple distinct entities.
# partition is a list of contributor ID groups, one group per new entity.
new_entity_ids, events = m.split(entity_id, partition=[
    ["contributor-1", "contributor-2"],   # -> entity A
    ["contributor-3"],                     # -> entity B
])

# Full audit log
log = m.event_log()
for event in log:
    print(event.kind, event.timestamp, event.actor, event.reversible)
```

## Error handling

All SDK exceptions subclass `OvertonError`, which exposes `.status_code`. Concrete
subclasses are keyed by HTTP status code:

```python
from overton_sdk import NotFoundError, UnauthorizedError, OvertonError

try:
    entity = m.read("entity-123")
except NotFoundError:
    print("entity not found")
except UnauthorizedError:
    print("invalid or expired API token")
except OvertonError as e:
    print(f"API error {e.status_code}: {e}")
```

| Exception | HTTP status | When |
|---|---|---|
| `BadRequestError` | 400 | Malformed request |
| `UnauthorizedError` | 401 | Missing or invalid API token |
| `PermissionDeniedError` | 403 | Token lacks permission for this action |
| `NotFoundError` | 404 | Entity, relation, or contributor not found |
| `UnprocessableEntityError` | 422 | Request failed validation |
| `InternalServerError` | 5xx | Server-side failure |
| `OvertonError` | any | Base class — exposes `.status_code` |

> [!NOTE]
> `AuthError` and `ValidationError` remain available as aliases of `UnauthorizedError`
> and `UnprocessableEntityError` for backwards compatibility.

## Static type analysis

The SDK ships a `py.typed` marker (PEP 561), so type checkers like mypy and pyright
resolve its annotations out of the box.

```python
from overton_sdk import (
    OvertonClient,
    ManifoldHandle,
    EntityDistribution,
    RelationDistribution,
    ManifoldMeasures,
    EntityMeasures,
    RelationMeasures,
    DivergenceResult,
    CurationEvent,
)

def analyse(m: ManifoldHandle) -> list[EntityDistribution]:
    return m.query("some query", top_k=20)
```

## Full example — biomedical literature

```python
import os
import overton_sdk

client = overton_sdk.OvertonClient(
    auth=overton_sdk.UserTokenAuth(os.environ["OVERTON_TOKEN"]),
    hostname="overton.example.com",
)
m = client.manifold("biomedical")

# Ingest papers with provenance
m.write(
    {
        "text": "BRCA1 strongly activates PARP1 in the presence of DNA damage",
        "source": "pmid:12345678",
        "covariates": {"cell_line": "HeLa", "year": "2021"},
    },
    {
        "text": "BRCA1 was shown to inhibit PARP1 under normoxic conditions",
        "source": "pmid:99887766",
        "covariates": {"cell_line": "MCF7", "year": "2023"},
    },
)

# Find the BRCA1-PARP1 relation
results = m.query("BRCA1 PARP1 interaction", top_k=5)
if results:
    entity = results[0]
    print(f"Entity: {entity.point_estimate()} (confidence: {max(entity.type_distribution.values()):.2f})")

# Check manifold health
mm = m.measures()
print(f"Manifold: {mm.entity_count} entities, {mm.relation_count} relations")
print(f"Mean uncertainty: {mm.aggregate_entropy:.3f} nats")
```

## Version

```python
import overton_sdk
print(overton_sdk.__version__)   # "0.1.0"
```
