# Ezop Python SDK

Ezop tracks the lifecycle of your AI agents — registrations, versions, and runs — so you have full observability across every deployment.

[Documentation](docs/README.md)

## Installation

```bash
pip install ezop
```

## Configuration

```bash
export EZOP_API_KEY=your-ezop-api-key-here
export EZOP_API_URL=https://api.ezop.ai
```

---

## Usage

### Initialize the agent

```python
from ezop import Agent

agent = Agent.init(
    name="customer-support-bot",
    owner="growth-team",
    version="v0.3",
    runtime="langchain",
    description="Handles tier-1 customer support tickets",
    default_permissions=["read:tickets"],
    permissions=["read:tickets", "write:replies"],
    changelog="Switched to new retrieval pipeline",
)
```

Call `Agent.init()` once at startup. It is safe to call on every deployment:

- If the agent does not exist, it will be created on the platform.
- If the agent already exists, registration returns the existing agent.
- If a new `version` is provided, the platform registers it as a new version under the same agent. If the version already exists, version registration is also a no-op.

### Track runs

`Agent.init()` starts a run automatically. Call `agent.close()` when the invocation is done:

```python
agent = Agent.init(name="my-bot", owner="my-team", version="v1.0", runtime="langchain")

agent.close(
    status="success",
    metadata={"user_id": user_id},
)
```

### Track steps with spans and events

Use `span` for steps with duration and `emit` for single points in time:

```python
# span: a record with start_time and end_time
with agent.span("retrieval", metadata={"query": user_input}):
    docs = retriever.search(user_input)

with agent.span("llm.call", metadata={"model": "claude"}):
    result = llm.generate(user_input)

# emit: a single point-in-time event
agent.emit(name="action.selected", category="reasoning")

agent.close(status="success")
```

Spans can be nested — child spans automatically record the parent's `span_id` as `parent_id`, enabling tree reconstruction:

```python
with agent.span("model.prompt") as s1:
    plan = llm.plan(user_input)

    with agent.span("tool.call", metadata={"tool": "stripe.refund"}) as s2:
        refund = stripe.refund(plan.charge_id)
# produces: model.prompt → tool.call (parent_id links them)
```

Events emitted inside a span are automatically linked to that span via `span_id`:

```python
with agent.span("llm.call") as s:
    agent.emit(name="token.count", category="llm", metadata={"tokens": 42})
# event.span_id == s.span_id
```

- **Runs** — the final outcome of a single agent invocation
- **Spans** — structured execution steps within a run (e.g. LLM call, retrieval, tool use)
- **Events** — raw logs inside a span

---

## API Reference

### `agent.init()`

Registers the agent and its version with the Ezop platform, and returns an `Agent` instance.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `name` | `str` | Yes | Agent name. Together with `owner`, uniquely identifies the agent on the platform. |
| `owner` | `str` | Yes | Team or user that owns the agent. |
| `version` | `str` | Yes | Version string (e.g. `"v1.2.0"`). A new version is registered if it does not exist yet. |
| `runtime` | `str` | Yes | Runtime or framework used (e.g. `"langchain"`, `"crew"`, `"custom"`). |
| `description` | `str` | No | Human-readable description of the agent. |
| `default_permissions` | `list[str]` | No | Permissions granted to all versions of this agent by default. |
| `permissions` | `list[str]` | No | Permissions granted to this specific version. |
| `changelog` | `str` | No | Description of what changed in this version. |

---

### `agent.close()`

Closes the current run and records its outcome.

```python
agent.close(
    status="success",
    message=None,
    metadata={"user_id": "u-123"},
)
```

| Parameter | Type | Required | Description |
|---|---|---|---|
| `status` | `str` | Yes | Final status of the run. One of `"success"`, `"failed"`, `"partial"`, `"canceled"`, `"running"`. |
| `message` | `str` | No | Human-readable message describing the outcome, e.g. a failure reason. |
| `metadata` | `dict` | No | Any arbitrary JSON-serialisable data you want to attach to the run (e.g. user context, request identifiers, feature flags). |

---

### `agent.emit()`

Emits an event on the current run. Events capture discrete steps within a run such as LLM calls, tool invocations, or retrieval operations.

```python
agent.emit(
    name="llm.call",
    category="llm",
    span_id="span-123",        # optional
    input={"prompt": "hello"},
    output={"text": "hi"},
    metadata={"model": "claude"},
    error=None,
)
```

| Parameter | Type | Required | Description |
|---|---|---|---|
| `name` | `str` | Yes | Event name (e.g. `"llm.call"`, `"tool.invoke"`). |
| `category` | `str` | Yes | Event category (e.g. `"llm"`, `"tool"`, `"retrieval"`). |
| `span_id` | `str` | No | Links this event to a span. Auto-set when emitting inside a `span` context manager. |
| `input` | `any` | No | Input passed to this step. |
| `output` | `any` | No | Output produced by this step. |
| `metadata` | `dict` | No | Any arbitrary JSON-serialisable data to attach to the event. |
| `error` | `str` | No | Error message if the event failed. |

---

### `agent.span()`

Returns a context manager that tracks a scoped duration as a span record. On enter it creates the span with a `start_time`; on exit it updates the span with an `end_time`.

```python
with agent.span("llm.call", metadata={"model": "claude"}) as s:
    result = llm.generate(prompt)
```

| Parameter | Type | Required | Description |
|---|---|---|---|
| `name` | `str` | Yes | Span name (e.g. `"llm.call"`, `"tool.invoke"`). |
| `metadata` | `dict` | No | Any arbitrary JSON-serialisable data to attach to the span. |

Nested spans automatically propagate context — each child span records the enclosing span's `id` as its `parent_id`, enabling tree reconstruction:

```
model.prompt  (id: A, parent_id: None)
  └── tool.call  (id: B, parent_id: A)
        └── memory.read  (id: C, parent_id: B)
```

Events emitted inside a span are automatically linked to that span via `span_id`.

---

## Logging

The SDK uses Python's standard `logging` module under the `ezop` namespace. To enable logs in your application:

```python
import logging
logging.getLogger("ezop").setLevel(logging.DEBUG)
```
