Metadata-Version: 2.4
Name: custodia-sdk
Version: 0.1.1
Summary: Custodia tracing SDK
Author: ssabrut
Author-email: ssabrut <michael.gunawan2002@gmail.com>
License-Expression: LicenseRef-Proprietary
License-File: LICENSE
Classifier: Programming Language :: Python :: 3
Classifier: Operating System :: OS Independent
Requires-Dist: httpx>=0.28.1
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.44.0
Requires-Dist: opentelemetry-sdk>=1.44.0
Requires-Python: >=3.11
Description-Content-Type: text/markdown

# custodia-sdk

Python tracing SDK for Custodia. Instruments function calls as OpenTelemetry
spans and exports them via OTLP/HTTP.

> **License:** proprietary. Use requires a paid commercial license from
> Neurona. See [LICENSE](./LICENSE). Contact michael.gunawan2002@gmail.com
> to obtain one.

## Install

```bash
pip install custodia-sdk
# or
uv add custodia-sdk
```

## Configuration

Set via environment variables:

| Variable | Description | Default |
|---|---|---|
| `CUSTODIA_INGEST_URL` | OTLP/HTTP traces endpoint | `http://localhost:4318/v1/traces` |
| `CUSTODIA_API_KEY` | Bearer token sent with exported spans | *(none)* |
| `CUSTODIA_SERVICE_NAME` | `service.name` resource attribute | `unknown-service` |

## Usage

```python
from custodia import trace, trace_async, trace_span, atrace_span


@trace(name="fetch_user", metadata={"component": "db"})
def get_user(user_id: str):
    return db.query(user_id)


@trace_async(name="call_llm")
async def generate(prompt: str) -> str:
    return await llm_client.complete(prompt)


with trace_span("parse_response") as span:
    data = json.loads(raw)
    span.set_attribute("record_count", len(data))

async with atrace_span("call_downstream") as span:
    resp = await http_client.get(url)
    span.set_attribute("http.status_code", resp.status_code)
```

- `trace` / `trace_async`: decorators that auto-capture args/return value as
  `gen_ai.prompt` / `gen_ai.completion` span attributes.
- `trace_span` / `atrace_span`: context managers for manual span control,
  no automatic I/O capture.

### Tool calls

For agentic code that dispatches tool/function calls, use the tool-call
variants — they record the OpenTelemetry GenAI semantic convention
attributes (`gen_ai.tool.name`, `gen_ai.tool.description`,
`gen_ai.tool.call.id`, `gen_ai.tool.call.arguments`, `gen_ai.tool.call.result`)
instead of the generic prompt/completion attributes:

```python
from custodia import (
    trace_tool_call,
    trace_tool_call_async,
    tool_call_span,
    atool_call_span,
    set_tool_call_result,
)


@trace_tool_call(name="get_weather", description="Look up current weather")
def get_weather(location: str) -> str:
    return weather_api.fetch(location)


@trace_tool_call_async(name="get_weather")
async def get_weather_async(location: str) -> str:
    return await weather_api.fetch(location)


# call with the model-issued id so the span can be correlated back to it
get_weather("SF", tool_call_id="call_123")

# for data-driven dispatch (e.g. a dict of tool name -> callable)
with tool_call_span(
    "get_weather", arguments={"location": "SF"}, tool_call_id="call_123"
) as span:
    result = weather_api.fetch("SF")
    set_tool_call_result(span, result)

async with atool_call_span("get_weather", arguments={"location": "SF"}) as span:
    result = await weather_api.fetch("SF")
    set_tool_call_result(span, result)
```

- `trace_tool_call` / `trace_tool_call_async`: decorators for a tool
  handler function. Pass `tool_call_id` as a keyword argument to the
  wrapped call to attribute the span to a specific model-issued tool call;
  a random id is generated if omitted.
- `tool_call_span` / `atool_call_span`: context managers for tool dispatch
  with no single dedicated function to decorate. Call
  `set_tool_call_result(span, result)` before the block ends to record
  the tool's return value.
