Metadata-Version: 2.4
Name: areev
Version: 0.2.0
Summary: Python SDK for the Areev knowledge database — HTTP, gRPC, and MCP transports
Project-URL: Repository, https://github.com/AreevAI/areev-sdk-python
Project-URL: Documentation, https://areev.ai/docs
License-Expression: BUSL-1.1
License-File: LICENSE
Requires-Python: >=3.10
Requires-Dist: httpx>=0.27
Requires-Dist: pydantic>=2.0
Provides-Extra: dev
Requires-Dist: datamodel-code-generator>=0.26; extra == 'dev'
Requires-Dist: grpcio-tools>=1.60; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: unasync>=0.6; extra == 'dev'
Provides-Extra: grpc
Requires-Dist: grpcio>=1.60; extra == 'grpc'
Requires-Dist: protobuf>=5.0; extra == 'grpc'
Description-Content-Type: text/markdown

# Areev Python SDK

Python client library for the [Areev](https://areev.ai) knowledge database.

A **grain** is one typed memory fact — the atomic unit Areev stores (e.g. the
Fact "John likes coffee"). Everything in this SDK ultimately reads or writes
grains.

## Installation

```bash
pip install areev
```

For gRPC transport:
```bash
pip install areev[grpc]
```

## Quick Start

```python
from areev import Areev

areev = Areev()  # reads AREEV_API_KEY, AREEV_URL from env
areev.remember("John likes coffee")

results = areev.recall("what does John like?")
for hit in results.results:
    print(f"  {hit.grain_type}: {hit.fields}")
```

The synchronous `Areev` exposes the **same full resource surface** as
`AsyncAreev` (`client.grains`, `client.compliance`, `client.scope`, …) with
**identical guardrails** — automatic retry with idempotency keys, secret/PII
redaction in logs, `confirm=True` guards on irreversible operations, and
server-side tier/credit/authz verdicts surfaced as typed exceptions:

```python
areev = Areev()

# Data-subject rights (GDPR/CCPA) — never tier- or credit-gated, PII kept out of logs
audit   = areev.compliance.audit()
export  = areev.compliance.export_user("user-123")
verify  = areev.compliance.verify_latest()

# Irreversible erasure requires explicit confirm=True
areev.grains.forget_user("user-123", confirm=True)   # GDPR Art. 17
areev.scope.erase("/org/team-a", confirm=True)
areev.consent.grant(user_id="user-123", purpose="marketing")
```

### Configuration

The client reads from environment variables by default:

| Variable | Default | Description |
|----------|---------|-------------|
| `AREEV_API_KEY` | — | API key (sent as `Authorization: Bearer <key>`) |
| `AREEV_URL` | `https://app.areev.ai` | Server endpoint |
| `AREEV_MEMORY_ID` | `default` | Memory database ID |

Or pass them explicitly:

```python
areev = Areev(api_key="ar_...", url="https://dub.areev.ai", memory_id="my-memory")
```

### Async

```python
from areev import AsyncAreev

async with AsyncAreev() as areev:
    await areev.remember("John likes coffee")
    results = await areev.recall("what does John like?")
```

## API

The top-level client exposes two headline methods (`remember`, `recall`) plus
22 resource namespaces. They split into two groups.

**Core** — what most integrations use day to day:

| Namespace | What it's for |
|-----------|---------------|
| `remember(text)` / `recall(query)` | Store NL memory (LLM extracts structure) / search it |
| `grains` | Typed grain CRUD (`add`, `get`, `recall`, `supersede`, `forget`, …) |
| `harness` | Conversational harness (Areev's LLM-plus-tools runtime) |
| `tools` | v1 tools surface — bind/list/invoke tools on a harness |
| `connectors` | Third-party connector catalog + OAuth (Axtion-backed) |
| `memories` | Memory-database lifecycle + stats |
| `system` | Health, server models, capabilities |

**Advanced / Compliance** — governance, identity, and ingestion surfaces you
reach for as you scale: `compliance` (GDPR/CCPA audit, export, verify),
`consent`, `scope`, `policy`, `authz`, `agent_identities`, `sessions`,
`namespaces`, `provenance`, `preferences`, `hooks`, `goals`, `chat`,
`connections`, `knowledge_sources`, `imports`.

Common Core methods:

| Surface | Description |
|---------|-------------|
| `remember(text)` | Store natural-language memory (LLM extracts structure) |
| `recall(query)` | Search memories → typed `RecallResponse` |
| `grains.add(grain_type, **fields)` | Add a typed grain → content hash |
| `grains.get(hash)` | Get a grain by hash |
| `grains.recall(query)` | Search → typed `RecallResponse` (same as top-level `recall`) |
| `grains.supersede(old_hash, **fields)` | Update a grain → new hash |
| `grains.forget(hash)` / `grains.forget_many(hashes)` | Delete grains |
| `harness.chat(slug=…, message=…)` | Single harness turn (`completed` / `requires_action`) |
| `harness.chat_resume(slug=…, session_id=…, tool_outputs=…)` | Resume a paused session |
| `harness.chat_cancel(slug=…, session_id=…)` | Cancel a paused session |
| `harness.chat_interactive(slug=…, message=…, executors=…)` | Harness chat with auto pause/resume for `client://` tools |
| `system.health()` | Health check |
| `memories.stats(memory_id)` | Database statistics |

### Harness Chat

Use `harness.chat_interactive` to drive a harness (Areev's LLM-plus-tools
runtime) with client-side tool executors. The helper runs the pause/resume
loop for you when the model calls a `client://` tool:

```python
from areev import Areev

areev = Areev()

def get_weather(_name, args):
    city = args.get("city", "unknown")
    return {"city": city, "temp_c": 22, "conditions": "sunny"}

response = areev.harness.chat_interactive(
    slug="weather-harness",
    message="What's the weather in Paris?",
    executors={"get_weather": get_weather},
    conversation_id="conv-1",
)
print(response["text"])
```

`AsyncAreev` exposes the same method as a coroutine. `harness.chat`,
`harness.chat_resume`, and `harness.chat_cancel` are the low-level primitives
if you want to run the loop yourself.

## End-to-End Quickstart (async)

A full flow: connect → remember/recall → create a harness and chat → register a
client-side tool → connect a third-party connector and bind one of its actions
as a tool — without hand-writing a single JSON Schema.

```python
import asyncio
from areev import AsyncAreev


async def main():
    async with AsyncAreev(
        api_key="ap_local_...",            # ap_*/ar_* key
        url="http://localhost:4210",       # your cell
        memory_id="my-first-memory",
    ) as areev:
        # 1. Remember + recall
        await areev.remember("Ada prefers email over Slack")
        hits = await areev.recall("how should I contact Ada?")
        for h in hits.results:
            print(h.grain_type, h.fields)

        # 2. Create a harness wired to a provider, then chat
        await areev.harness.create(
            name="Sales Bot",
            slug="sales-bot",
            description="Outbound sales assistant",
            llm_config={"provider_id": "openai", "model": "gpt-4o-mini"},
            # provider_type defaults to provider_id ("openai") automatically.
        )
        reply = await areev.harness.chat(slug="sales-bot", message="Say hi")
        print(reply)

        # 3. Add a client-side tool (executed in your process)
        def get_weather(_name, args):
            return {"city": args.get("city"), "temp_c": 22}

        await areev.harness.chat_interactive(
            slug="sales-bot",
            message="What's the weather in Paris?",
            executors={"get_weather": get_weather},
        )

        # 4. Connect a third-party connector (OAuth), then poll for completion
        flow = await areev.connectors.authorize(
            "gmail", redirect_uri="https://yourapp.example/oauth/cb"
        )
        print("Open this URL to authorize:", flow["authorize_url"])
        # ... user consents in the browser, provider redirects back ...
        status = await areev.connectors.poll_oauth("gmail", flow["state"])
        # poll until status["status"] == "success"

        # 5. Bind a connector action as a tool — schema fetched for you
        await areev.tools.bind_axtion(
            slug="sales-bot",
            connector="gmail",
            action="send-email",          # the action key from connectors.actions()
            description="Send an email via Gmail",
        )


asyncio.run(main())
```

### Connectors

> **`connectors` vs `connections` vs `harness` vs `chat`** — `connectors` is the
> third-party **catalog + OAuth** surface (discover providers, run the handshake);
> `connections` is the **stored credential records** for connectors you've already
> authorized; `harness` is the **chatbot-with-tools** runtime (LLM + bound tools);
> `chat` is **memory-engine chat** (converse over your stored grains).

`client.connectors` is the catalog + OAuth surface (backed by Axtion). All
methods return clean, parsed data:

| Method | Returns |
|--------|---------|
| `connectors.list()` | `[{"name", "display_name", "category", "version"}, ...]` |
| `connectors.get(name)` | Raw connector detail (metadata + action descriptors) |
| `connectors.actions(name)` | `[{"name", "display_name", "description", "param_schema", "required"}, ...]` |
| `connectors.authorize(name, redirect_uri=…)` | `{"authorize_url", "state", ...}` |
| `connectors.poll_oauth(name, state)` | `{"status": "pending"\|"success"\|"error"\|"expired", ...}` |
| `connectors.store_credentials(body)` | Store API-key credentials (non-OAuth connectors) |

#### Canonical connector flow (OAuth → bind a tool)

```python
# 0. Discover what's available
catalog = await areev.connectors.list()
# → [{"name": "gmail", "display_name": "Google Gmail",
#     "category": "Communication", "version": "1.0.0"}, ...]

# 1. Start the OAuth handshake for a connector key from the catalog
flow = await areev.connectors.authorize(
    "gmail", redirect_uri="https://yourapp.example/oauth/cb"
)

# 2. Send the user to the authorize URL to consent
print("Open this URL to authorize:", flow["authorize_url"])

# 3. After the provider redirects back, poll until the handshake completes
#    (back off between polls — don't hammer the endpoint in a tight loop)
import asyncio

status = await areev.connectors.poll_oauth("gmail", flow["state"])
while status["status"] == "pending":
    await asyncio.sleep(2)
    status = await areev.connectors.poll_oauth("gmail", flow["state"])
assert status["status"] == "success"

# 4. Bind one of the connector's actions as a harness tool — schema fetched
#    for you (no hand-written JSON Schema)
await areev.tools.bind_axtion(
    slug="sales-bot",
    connector="gmail",
    action="send-email",          # an action key from connectors.actions("gmail")
    description="Send an email via Gmail",
)
```

> **Prerequisite — redirect-uri allowlist.** The `redirect_uri` you pass to
> `connectors.authorize(...)` must be **pre-registered on your org** by an
> admin. An unregistered URI is rejected before any provider redirect — the
> call raises `ValidationError` (HTTP 400). Register your callback URL(s) in
> org settings before going live.

`tools.bind_axtion(slug=…, connector=…, action=…)` looks up the action's
parameter schema from `connectors.actions()` and binds it for you — so the
agent gets a fully-typed tool without you writing any JSON Schema.

The sync client (`Areev`) exposes the identical surface without `await`.

## Transports

| Transport | Extra | Status |
|-----------|-------|--------|
| HTTP/REST | _(default)_ | Available |
| gRPC | `areev[grpc]` | Available |
| MCP | — | Planned |

## Code Generation

Generate gRPC stubs, Pydantic models, the async raw-HTTP resource clients,
and the synchronous SDK surface from the Areev spec:

```bash
pip install areev[dev]
./scripts/generate.sh
```

The **async** modules under `src/areev/resources/` and `src/areev/http.py` are
the single source of truth. The **sync** twin (`src/areev/_sync/`) is derived
mechanically from them by `scripts/generate_sync.py` (via
[`unasync`](https://pypi.org/project/unasync/)) — so the two surfaces cannot
drift in their guardrails. Run `python scripts/generate_sync.py --check` in CI
to fail the build if the committed sync tree is stale.

## License

BUSL-1.1
