Metadata-Version: 2.5
Name: aice-recommendation
Version: 0.0.2
Summary: Python SDK for the AICE Recommendation Engine
Author-email: Arnold Opiyo <arnoldopiyo@adanianlabs.io>, Alex Waweru <alexwaweru@aiceafrica.com>, Adanian Labs <engineering@adanianlabs.io>
License: MIT
License-File: LICENSE
Keywords: aice,api,machine-learning,recommendation,recommendation-engine,recommender
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: httpx>=0.27
Provides-Extra: dev
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
Requires-Dist: pytest>=8.3; extra == 'dev'
Requires-Dist: respx>=0.21; extra == 'dev'
Description-Content-Type: text/markdown

# aice-recommendation

Python SDK for the AICE Recommendation Engine: catalog management, event
tracking, recommendations, training jobs and config. A thin `httpx` client;
all scoring happens server-side.

**This is the 0.0.x line, for the engine currently running at
`https://reco.aiceafrica.com`** (unprefixed routes such as `/users`,
`/ingest`). 0.0.3 will target the vertical-routed engine and adds
`vertical=` to the client; the rest of the API stays the same.

## Installation

```bash
pip install aice-recommendation==0.0.2
```

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

## Quick start

```python
from aice_recommendation import RecommendationClient

client = RecommendationClient(
    base_url="https://reco.aiceafrica.com",
    api_key="sk-proj_...",   # the project's key; it decides the project and vertical
)

# Users and items must exist before events can reference them.
client.create_user("user-1", metadata={"region": "KE"})
client.create_item("item-42", metadata={"category": "electronics"})
client.track("user-1", "item-42", "purchase")

job = client.train(epochs=20)
print(client.job_status(job.job_id).status)   # pending → running → complete / failed

recs = client.recommend("user-1", n=10)
for r in recs.recommendations:
    print(r.rank, r.item_id, r.score, r.explanation)

client.close()
```

The client is also a context manager (`with RecommendationClient(...) as client:`).

Async variant, same methods:

```python
from aice_recommendation import AsyncRecommendationClient

async with AsyncRecommendationClient(base_url="https://reco.aiceafrica.com", api_key=KEY) as client:
    recs = await client.recommend("user-1", n=10)
```

## Several events at once

```python
from aice_recommendation import Event

result = client.track_batch([
    Event("user-1", "item-42", "view"),
    Event("user-2", "item-7", "purchase", metadata={"price": 12.5}),
])
print(result.ingested_count, result.failed_count, result.errors)
```

On the current server the batch is all or nothing: if any event is invalid
(unknown user or item, more than 50 metadata keys, `event_type` over 100
characters), the whole call raises `ValidationError` and nothing is ingested.

## API surface

| Endpoint | Method |
|---|---|
| `POST /users` | `create_user(user_id, metadata=None)` |
| `GET  /users/{id}` | `get_user(user_id)` |
| `PUT  /users/{id}` | `update_user(user_id, metadata=…)` |
| `POST /items` | `create_item(item_id, metadata=None)` |
| `GET  /items/{id}` | `get_item(item_id)` |
| `PUT  /items/{id}` | `update_item(item_id, metadata=…)` |
| `POST /ingest` | `track(user_id, item_id, event_type, timestamp=None, metadata=None)` |
| `POST /ingest/batch` | `track_batch(events)` |
| `GET  /recommend/{user_id}` | `recommend(user_id, n=10, persist=False)` |
| `POST /train` | `train(epochs=20)` |
| `GET  /train/{job_id}` | `job_status(job_id)` |
| `GET  /config` | `get_config()` |
| `PUT  /config` | `update_config(overrides)` |
| `GET  /verticals` | `list_verticals()` |
| `GET  /verticals/{name}` | `get_vertical(name)` |

The `/admin` routes (creating organizations and projects, rotating keys) are
not covered; keys are issued by the AICE console.

Ids are sent as a single escaped path segment, so an id containing `/`, `?`
or `#` can't reach a different route. The current server still can't look up
an id containing `/` (it answers 404).

## Errors

Every HTTP error raises a subclass of `RecommendationError`, which carries
`status_code` and the server's raw `detail`:

| Status | Exception |
|---|---|
| 400, 422 | `ValidationError` |
| 401 | `AuthenticationError` |
| 403 | `ForbiddenError` |
| 404 | `NotFoundError` |
| 409 | `ConflictError` |
| 5xx | `ServerError` |
| other 4xx | `RecommendationError` |

```python
from aice_recommendation import NotFoundError

try:
    client.get_user("missing")
except NotFoundError as err:
    print(err.status_code, err)   # 404 User not found.
```

Requests are not retried, because the server doesn't deduplicate retried
writes and a retried `track` could count an event twice. Timeouts and
connection failures surface as `httpx` exceptions (`httpx.TimeoutException`,
`httpx.ConnectError`, …).

## Migrating from `reco` (`RecoClient`)

| Before | After |
|---|---|
| `from reco import RecoClient` | `from aice_recommendation import RecommendationClient` |
| `RecoError` | `RecommendationError` |
| `AuthError` | `AuthenticationError` |
| `Item.organization_id` | `Item.project_id` |

Method names and their arguments are unchanged.

## Development

```bash
pip install -e ".[dev]"
pytest                                       # offline tests; the live test is skipped
RECO_API_KEY=sk-proj_... pytest tests/test_live.py
```

The live test runs the full flow against `RECO_BASE_URL` (default
`https://reco.aiceafrica.com`). Each run leaves `sdktest-*` users, items and
events, plus one 1-epoch training job, on that project. It never changes the
project config.
