Metadata-Version: 2.4
Name: tai-daf-sdk
Version: 2.0.0a3
Summary: Python SDK for DAF (Declarative Agentic Framework) — thin MCP client
Author-email: DAF Team <dev@example.com>
License: MIT
Project-URL: Homepage, https://github.com/your-org/tai-daf-sdk
Project-URL: Repository, https://github.com/your-org/tai-daf-sdk
Keywords: ai,agents,llm,framework,sdk
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.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: httpx>=0.24.0
Requires-Dist: pydantic>=2.0.0
Requires-Dist: python-dotenv>=1.0.0
Requires-Dist: mcp<2,>=1.10.0
Provides-Extra: test
Requires-Dist: pytest>=7.0.0; extra == "test"
Requires-Dist: pytest-asyncio>=0.21.0; extra == "test"
Requires-Dist: pytest-cov>=4.0.0; extra == "test"
Provides-Extra: lint
Requires-Dist: black==26.3.1; extra == "lint"
Requires-Dist: ruff>=0.1.0; extra == "lint"
Requires-Dist: mypy>=1.0.0; extra == "lint"
Requires-Dist: types-requests>=2.32.0; extra == "lint"
Requires-Dist: bandit[toml]>=1.8.0; extra == "lint"
Requires-Dist: pip-audit>=2.7.0; extra == "lint"
Requires-Dist: pre-commit>=3.7.0; extra == "lint"
Provides-Extra: dev
Requires-Dist: tai-daf-sdk[lint,test]; extra == "dev"

# tai-daf-sdk

Python SDK for **DAF — Declarative Agentic Framework**. Thin, MCP-native client. Every call goes to the same MCP tool the backend exposes at `/mcp` — no separate REST surface to keep in sync, no per-resource wrappers to hand-write.

**PyPI:** <https://pypi.org/project/tai-daf-sdk/>

## Install

Alpha channel while the surface stabilises:

```bash
pip install --pre tai-daf-sdk
```

Python ≥ 3.10.

## 30-second quickstart

```python
from tai_daf_sdk import DAF

with DAF(
    base_url="https://daf-sdk-backend-dev.azurewebsites.net",
    api_key="daf_...",
) as client:
    # List LLM endpoints on your account
    endpoints = client.llm_endpoints.list()
    ep_id = endpoints["endpoints"][0]["id"]

    # Create an agent bound to that endpoint
    agent = client.agents.create(
        name="my-assistant",
        system_instructions="You answer politely.",
        llm_endpoint_id=ep_id,
        confirm=True,
    )
    print("Created:", agent["created"]["id"])
```

## Async version

Same API, `async` prefix, always inside `async with`:

```python
import asyncio
from tai_daf_sdk import AsyncDAF

async def main():
    async with AsyncDAF(base_url="...", api_key="daf_...") as client:
        agents = await client.agents.list()
        for a in agents["agents"]:
            print(a["id"], a["name"])

asyncio.run(main())
```

## How the dispatch works

The SDK is a **thin dispatch layer over MCP tools**. `client.<resource>.<method>(**kwargs)` maps to `daf_<method>_<resource>`:

| Python call | MCP tool |
|---|---|
| `client.agents.list()` | `daf_list_agents` |
| `client.agents.create(...)` | `daf_create_agent` (singular fallback) |
| `client.agents.describe(name_or_id=...)` | `daf_describe_agent` |
| `client.agents.update(agent_id=..., patch=...)` | `daf_update_agent` |
| `client.agents.delete(agent_id=..., confirm=True)` | `daf_delete_agent` |
| `client.swarms.list()` | `daf_list_swarms` |
| `client.llm_endpoints.list()` | `daf_list_llm_endpoints` |

**Singular fallback.** Every collection is plural on the SDK (`client.agents`, `client.swarms`) so it reads naturally. When you call `.create()` the SDK first tries `daf_create_agents` (plural), and if that's not registered, falls back to `daf_create_agent` (singular). This matches the backend's real naming — list ops are plural, per-item ops are singular.

**Escape hatch for irregular tool names.** A handful of tools don't fit `daf_<method>_<resource>` — like `daf_attach_guardrail_to_agent`, `daf_run_tool`, `daf_mcp_health_check`, `daf_export_agent`, `daf_import_agent`, `daf_list_node_types`. Use `client.call(tool_name, args_dict)`:

```python
result = client.call(
    "daf_attach_guardrail_to_agent",
    {"agent_id": agent_id, "guardrail_id": gid, "confirm": True},
)
```

Async client has the same escape hatch:

```python
result = await async_client.call("daf_attach_guardrail_to_agent", {...})
```

## Errors

The SDK maps the backend's structured error codes to typed exceptions:

| Backend code | Exception |
|---|---|
| `VALIDATION_ERROR` | `BadRequestError` |
| `ENTITY_NOT_FOUND` | `NotFoundError` |
| `FORBIDDEN` | `PermissionError` |
| `NAME_TAKEN` / `DUPLICATE_URL` | `ConflictError` |
| `QUOTA_EXCEEDED` | `RateLimitError` |
| Others | `APIError` (base class) |

```python
from tai_daf_sdk.exceptions import BadRequestError

try:
    client.agents.create(name="broken", model_provider="azure", confirm=True)
except BadRequestError as exc:
    print(f"Invalid config: {exc}")
```

`ConnectionError` is raised for transport failures (backend unreachable, DNS, TLS).

## Configuration

Constructor accepts:

- `base_url` (str) — DAF backend URL. Local dev = `http://localhost:8012`, dev cloud = `https://daf-sdk-backend-dev.azurewebsites.net`.
- `api_key` (str) — DAF API key (starts with `daf_`). Get one from Settings → API keys in the webapp.
- `token` (str) — Bearer JWT alternative to API key. Only one of `api_key` / `token` at a time.
- `timeout` (float, default 60.0) — HTTP timeout in seconds.

Alternatively, set `DAF_MCP_STDIO_COMMAND` to run the SDK against a local stdio MCP server instead of HTTP.

## Streaming

For tools whose backend implementation streams SSE events (`daf_start_workflow`, `daf_subscribe_workflow`), use `client.stream(tool_name, args)`:

```python
for event in client.stream("daf_subscribe_workflow", {"run_id": run_id}):
    if event.get("event") == "output_node":
        print(event["data"]["content"], end="", flush=True)
```

Async equivalent iterates via `async for`:

```python
async for event in async_client.stream("daf_subscribe_workflow", {"run_id": run_id}):
    ...
```

## Full guide + more examples

- **Guide:** [docs/GUIDE.md](docs/GUIDE.md) — every capability with working code.
- **Examples:** [`examples/`](examples/) — runnable scripts:
  - `01_hello_agent.py` — create + describe + delete an agent
  - `02_swarm_workflow.py` — compose a two-agent swarm via workflow drafts
  - `03_triggers.py` — webhook + cron + event triggers
  - `04_memory.py` — per-agent memory + shared memory
  - `05_guardrails.py` — attach a guardrail to an agent
  - `06_streaming.py` — subscribe to workflow SSE events

Each example is self-contained. Pass `--url` and `--api-key` (or set `DAF_MCP_URL` / `DAF_API_KEY` env vars).

## Regenerating from your backend's catalog

When the backend adds tools, the SDK's tool registry catches up automatically:

```bash
python -m tai_daf_sdk.codegen \
    --mcp-url https://daf-sdk-backend-dev.azurewebsites.net \
    --api-key daf_... \
    --out tai_daf_sdk/
```

Regenerates:

- `_tool_registry.py` — MCP tool → resource+method map
- `types.py` — TypedDict per tool input
- `client.pyi` — IDE stubs for autocomplete
- `models.py` — Pydantic wrappers where declared

The CI workflow `.github/workflows/mcp-catalog-sync.yml` runs this on schedule and fails when the shipped registry drifts from the live backend catalog.

## End-to-end test scenarios

`scripts/sdk_tests/` — 45 scripts (24 customer sandbox stories + 21 read-only sweeps) verify the whole surface against a live backend. Same story IDs as the MCP-side suite in `daf-sdk-backend/scripts/mcp_tests` so you can cross-check SDK ↔ MCP agree on the customer contract.

```bash
python -m scripts.sdk_tests.run_all \
    --url https://daf-sdk-backend-dev.azurewebsites.net \
    --api-key daf_...
```

Filter with `--set 1|2` or `--filter us_01` for one scenario at a time.

## License

MIT.
