Metadata-Version: 2.4
Name: hai-agent-api
Version: 0.1.3
Summary: Shared FastAPI surface for the H Agent Platform agents API
Requires-Python: >=3.12
Requires-Dist: boto3>=1.35.0
Requires-Dist: fastapi>=0.136.3
Requires-Dist: jsonschema>=4.23.0
Requires-Dist: pillow>=11.0.0
Requires-Dist: pydantic>=2.9.2
Requires-Dist: requests>=2.32.4
Requires-Dist: typing-extensions>=4.12.2
Description-Content-Type: text/markdown

# hai-agent-api

Shared FastAPI surface for the **agents API**. This package defines the HTTP routes, request/response models, and authentication dependencies. Your application supplies the backing logic by implementing the service protocols and passing them to `create_router`.

## Install

```bash
uv pip install hai-agent-api
```

You also need an ASGI server to run locally, for example:

```bash
uv pip install "uvicorn[standard]"
```

## Quick start

`hai-agent-api` does not ship a runnable server. Wire the router into your own FastAPI app and provide implementations for all five services.

```python
# app.py
from agent_api import ApiConfig, AuthConfig, Services, create_router
from agent_api.user import ApiUser
from agent_interface.specs.skill import Skill
from agp_types import Page, PageRequest
from fastapi import FastAPI, HTTPException

# ---------------------------------------------------------------------------
# Example service implementations
# ---------------------------------------------------------------------------

class InMemorySkillService:
    """Minimal skill catalog backed by an in-memory dict."""

    def __init__(self) -> None:
        self._skills: dict[str, Skill] = {}

    async def create_skill(self, user: ApiUser, create: Skill) -> Skill:
        if create.name in self._skills:
            raise HTTPException(status_code=409, detail=f"Skill {create.name!r} already exists.")
        self._skills[create.name] = create
        return create

    async def get_page(
        self,
        user: ApiUser,
        page_request: PageRequest,
        *,
        name: str | None = None,
        search: str | None = None,
    ) -> Page[Skill]:
        items = list(self._skills.values())
        if name:
            items = [s for s in items if name.lower() in s.name.lower()]
        if search:
            q = search.lower()
            items = [s for s in items if q in s.name.lower() or q in (s.description or "").lower()]
        return Page[Skill](items=items, page=page_request.page, size=page_request.size, total=len(items))

    async def get_skill(self, user: ApiUser, name: str) -> Skill:
        skill = self._skills.get(name)
        if skill is None:
            raise HTTPException(status_code=404, detail=f"Skill {name!r} not found.")
        return skill

    async def update_skill(self, user: ApiUser, name: str, update: Skill) -> Skill:
        if update.name != name:
            raise HTTPException(status_code=422, detail="update.name must match the path name.")
        if name not in self._skills:
            raise HTTPException(status_code=404, detail=f"Skill {name!r} not found.")
        self._skills[name] = update
        return update

    async def delete_skill(self, user: ApiUser, name: str) -> None:
        if name not in self._skills:
            raise HTTPException(status_code=404, detail=f"Skill {name!r} not found.")
        del self._skills[name]


class StubService:
    """Placeholder for services you have not implemented yet."""

    def __getattr__(self, name: str):
        async def _not_implemented(*_args, **_kwargs):
            raise HTTPException(status_code=501, detail=f"{name} is not implemented.")

        return _not_implemented


# ---------------------------------------------------------------------------
# FastAPI app
# ---------------------------------------------------------------------------

app = FastAPI(title="agents-api", version="0.0.0")

services = Services(
    agents=StubService(),
    environments=StubService(),
    skills=InMemorySkillService(),
    sessions=StubService(),
    memories=StubService(),
)

app.include_router(
    create_router(
        services,
        config=ApiConfig(auth=AuthConfig(mode="local")),
    ),
    prefix="/api",
)
```

Run the server:

```bash
uvicorn app:app --reload --port 8000
```

Open the interactive docs at [http://localhost:8000/docs](http://localhost:8000/docs). Skill endpoints are available under `/api/v2/skills`; unimplemented services return `501 Not Implemented`.

### Try a request

```bash
curl -s -X POST http://localhost:8000/api/v2/skills \
  -H 'Content-Type: application/json' \
  -d '{"name": "my-skill", "description": "Example", "body": "Do the thing."}' | jq
```

## Service protocols

Implement these protocols (structural typing—no base class required) and pass them in a `Services` container:

| Protocol | Router prefix | Responsibility |
|----------|---------------|----------------|
| `AgentServiceProtocol` | `/v2/agents` | Agent catalog CRUD and spec resolution |
| `EnvironmentServiceProtocol` | `/v2/environments` | Environment catalog CRUD |
| `SkillServiceProtocol` | `/v2/skills` | Skill catalog CRUD |
| `SessionServiceProtocol` | `/v2/sessions` | Agentic session lifecycle, events, quota |
| `MemoryServiceProtocol` | `/v2/memories` | Per-org key/value memory (hidden from OpenAPI) |

Full method signatures live in `agent_api.services.protocols`. Raise `fastapi.HTTPException` for expected client errors (404, 409, 422, etc.). Session creation may raise `agent_api.exceptions.IdempotencyKeyConflict`; the sessions router maps that to HTTP 422.

## Configuration

`ApiConfig` controls router behaviour:

| Field | Default | Purpose |
|-------|---------|---------|
| `long_poll_max_wait_for_seconds` | `60` | Upper bound for long-poll on `/sessions/{id}/changes` |
| `session_share_url_template` | `/share/api/v1/trajectories/{session_id}` | Template for public share links |
| `auth` | `AuthConfig(mode="platform")` | How requests are authenticated (see below) |

### Authentication

Pass `AuthConfig` via `ApiConfig.auth` when calling `create_router`:

| Mode | Use case | Behaviour |
|------|----------|-----------|
| `platform` | Agent Platform (default) | Requires `X-User-Sub` and `X-User-Org` headers injected by the gateway; returns 401 when missing |
| `local` | Local development / standalone apps | Protected routes accept optional identity headers and fall back to default UUIDs when omitted |

Local mode defaults:

| Field | Default |
|-------|---------|
| `default_user_id` | `00000000-0000-0000-0000-000000000001` |
| `default_org_id` | `00000000-0000-0000-0000-000000000002` |
| `default_email` | `local-dev@example.com` |

Override headers when you need a specific identity:

```bash
curl -s http://localhost:8000/api/v2/skills \
  -H 'X-User-Sub: 11111111-1111-1111-1111-111111111111' \
  -H 'X-User-Org: 22222222-2222-2222-2222-222222222222'
```

Agent Platform keeps the default `platform` mode—no change required in production wiring.

## API layout

`create_router` returns a router with this structure:

```
/v2/sessions/...
/v2/memories/...
/v2/skills/...
/v2/environments/...
/v2/agents/...
```

Mount it under your chosen prefix (commonly `/api`, yielding `/api/v2/...`).
