Metadata-Version: 2.1
Name: zhanla-sdk-py
Version: 0.1.2.12
Summary: Zhanla SDK for instrumenting AI components
Project-URL: Homepage, https://github.com/zhanla-ai
Project-URL: Repository, https://github.com/zhanla-ai
Author-email: Zhanla <chouweimin@berkeley.edu>
License: Proprietary
Requires-Python: >=3.10
Provides-Extra: anthropic
Requires-Dist: anthropic; extra == 'anthropic'
Provides-Extra: dev
Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
Requires-Dist: pytest>=7.0; extra == 'dev'
Provides-Extra: example-providers
Requires-Dist: anthropic; extra == 'example-providers'
Requires-Dist: google-genai; extra == 'example-providers'
Requires-Dist: openai; extra == 'example-providers'
Provides-Extra: google
Requires-Dist: google-genai; extra == 'google'
Provides-Extra: openai
Requires-Dist: openai; extra == 'openai'
Description-Content-Type: text/markdown

# zhanla-sdk-py

Python SDK for defining Zhanla components in code.

Use it to declare tools, skills, agents, orchestrations, and evals as module-level Python objects, then run or upload them with the `zhanla` CLI.

## Installation

```bash
pip install zhanla-sdk-py zhanla
```

Requires Python `>=3.10`.

Install a provider SDK only when your component makes LLM calls:

```bash
pip install anthropic
pip install openai
pip install google-genai
```

## Quick start

Create `components.py`:

```python
import zhanla


def classify_priority(message: str, customer_tier: str = "standard", **_) -> dict:
    if "urgent" in message.lower() or customer_tier == "enterprise":
        return {"priority": "high"}
    return {"priority": "normal"}


priority_tool = zhanla.Tool(
    name="priority_tool",
    description="Classify support ticket priority.",
    key="priority-tool",
    fn=classify_priority,
    input_schema={"message": str, "customer_tier": str},
    output_schema={"priority": str},
)


def priority_eval(model_response, expected_output=None, **_) -> dict:
    response = zhanla.parse_json_response(model_response)
    expected = zhanla.parse_json_response(expected_output or "{}")
    return {
        "score": 1.0
        if response.get("priority") == expected.get("priority")
        else 0.0
    }


priority_eval_component = zhanla.CodeEval(
    name="priority_eval",
    description="Check predicted priority.",
    key="priority-eval",
    fn=priority_eval,
)
```

Create `tickets.json`:

```json
[
  { "_schema": { "message": "string", "expected_output": "object" } },
  { "message": "urgent outage", "expected_output": { "priority": "high" } }
]
```

Run locally:

```bash
zhanla validate components.py
zhanla run components.py:priority-tool --dataset tickets.json --eval components.py:priority-eval --dry-run --yes
```

After `zhanla login`, drop `--dry-run` to sync definitions, dataset rows, and results to the web app.

## Authoring rules

- Import with `import zhanla`.
- Define components as module-level objects so the CLI can discover them.
- Give every component an explicit stable `key`.
- Keys must contain only lowercase letters, digits, and hyphens.
- CLI target suffixes use `key`, not Python variable name or display `name`.
- Prompt-backed components need `model` plus exactly one of `client` or `runner` for local execution.
- `Skill` is prompt-only configuration and cannot be run directly.

## Provider-backed components

Use `client=` for normal provider usage:

```python
import anthropic
import zhanla

support_agent = zhanla.Agent(
    name="support_agent",
    description="Respond to support requests.",
    key="support-agent",
    instructions='Answer clearly. Return JSON: {"answer": "..."}',
    model="claude-sonnet-4-6",
    client=anthropic.Anthropic(),
    output_schema={"answer": str},
    json_repair=True,
)
```

`client=` creates a `zhanla.Runner` internally. Use `runner=` when you need custom runner behavior or want to share one runner:

```python
runner = zhanla.Runner(client=anthropic.Anthropic())
```

OpenAI:

```python
import openai

agent = zhanla.Agent(
    name="openai_agent",
    description="Use OpenAI.",
    key="openai-agent",
    instructions="Return JSON.",
    model="gpt-4.1",
    client=openai.OpenAI(),
)
```

Google Gemini:

```python
import google.genai

agent = zhanla.Agent(
    name="gemini_agent",
    description="Use Gemini.",
    key="gemini-agent",
    instructions="Return JSON.",
    model="gemini-2.5-flash",
    client=google.genai.Client(),
)
```

OpenRouter through the OpenAI-compatible client:

```python
import os
import openai

runner = zhanla.Runner(
    client=openai.OpenAI(
        base_url="https://openrouter.ai/api/v1",
        api_key=os.environ["OPENROUTER_API_KEY"],
    )
)
```

## Components

### `Tool`

Use a `Tool` for deterministic Python logic.

```python
lookup_customer = zhanla.Tool(
    name="lookup_customer",
    description="Fetch a customer record.",
    key="lookup-customer",
    fn=get_customer,
    input_schema={"customer_id": str},
    output_schema={"id": str, "email": str},
)
```

`fn` can be sync or async. Non-dict returns are wrapped as `{"result": value}`.

Python schemas can be shorthand dicts, JSON-Schema-shaped dicts, or Pydantic model classes. The CLI validates the first local output against `output_schema`.

### `Skill`

Use a `Skill` for reusable prompt instructions.

```python
summarize_ticket = zhanla.Skill(
    name="summarize_ticket",
    description="Summarize support tickets.",
    key="summarize-ticket",
    instructions="Summarize the ticket in one short paragraph.",
)
```

Skills can be attached to agents or orchestrations, but they are not top-level local runtime targets.

### `Agent`

Use an `Agent` for LLM-backed work with optional tools, skills, and nested agents.

```python
support_agent = zhanla.Agent(
    name="support_agent",
    description="Respond to support requests.",
    key="support-agent",
    instructions="Answer clearly and use available tools when needed.",
    model="claude-sonnet-4-6",
    client=anthropic.Anthropic(),
    tools=[lookup_customer],
    skills=[summarize_ticket],
    output_schema={"answer": str},
)
```

### `LLMProcessor`

Use an `LLMProcessor` for one prompt-defined transformation.

```python
intent_classifier = zhanla.LLMProcessor(
    name="intent_classifier",
    description="Classify intent.",
    key="intent-classifier",
    instructions='Return JSON: {"intent": "billing|technical|other"}',
    model="claude-sonnet-4-6",
    client=anthropic.Anthropic(),
    output_schema={"intent": str},
)
```

### `Orchestration`

Use `Step` to compose components into a DAG.

```python
support_pipeline = zhanla.Orchestration(
    name="support_pipeline",
    description="Classify priority, then reply.",
    key="support-pipeline",
    steps=[
        zhanla.Step(component=priority_tool, name="classify", next=["reply"]),
        zhanla.Step(component=support_agent, name="reply"),
    ],
)
```

Use `Conditional` for routing:

```python
zhanla.Step(
    name="route",
    component=zhanla.Conditional(
        condition=lambda state: state["classify"]["priority"] == "high",
        if_true="urgent_reply",
        if_false="normal_reply",
    ),
)
```

## Evals

### `CodeEval`

`CodeEval` functions receive canonical text kwargs:

- `model_response`
- `expected_output`
- `model_input`

`model_response` must be a required parameter. The other two can be optional.

```python
def score(model_response, expected_output=None, model_input=None, **_):
    response = zhanla.parse_json_response(model_response)
    expected = zhanla.parse_json_response(expected_output or "{}")
    return {"score": 1.0 if response == expected else 0.0}
```

Non-dict returns are wrapped as `{"score": value}`.

`model_response_format` defaults to `"JSON"` and can be set to `"TEXT"` or `"YAML"` for synced eval metadata. Current local and CLI-managed eval-only execution still passes strings to the function, so parse structured values yourself.

### `LLMEval`

Use either `instructions` or `questions`, not both.

```python
tone_eval = zhanla.LLMEval(
    name="tone_eval",
    description="Evaluate tone.",
    key="tone-eval",
    instructions='Return JSON: {"score": 0.0, "reason": "..."}',
    model="claude-sonnet-4-6",
    client=anthropic.Anthropic(),
    output_schema={"score": float, "reason": str},
)
```

### `Checklist` and `EvalTree`

```python
answer_quality = zhanla.Checklist(
    name="answer_quality",
    description="Combine evals.",
    key="answer-quality",
    evals=[priority_eval_component, tone_eval],
    weights=[0.8, 0.2],
)
```

```python
adaptive_eval = zhanla.EvalTree(
    name="adaptive_eval",
    description="Route evals by threshold.",
    key="adaptive-eval",
    root=zhanla.Branch(
        eval=priority_eval_component,
        threshold=0.8,
        if_pass=[zhanla.Edge(weight=1.0, node=zhanla.Leaf(eval=priority_eval_component))],
        if_fail=[zhanla.Edge(weight=1.0, node=zhanla.Leaf(eval=tone_eval))],
    ),
)
```

## CLI usage

```bash
zhanla validate workflow.py
zhanla upload workflow.py:support-pipeline
zhanla run workflow.py:support-pipeline --dataset tickets.json --eval evals.py:answer-quality --dry-run --yes
zhanla run workflow.py:support-pipeline --dataset tickets.json --web-eval answer-quality --yes
```

Local JSON datasets must start with a `_schema` metadata row. CSV datasets use their header row as fields.

## Programmatic execution

Run a component in your app with trajectory tracking:

```python
import zhanla

output = await zhanla.run_component(support_agent, {"message": "Need help"})
```

Set `ZHANLA_API_KEY="bm_kid_....bm_sec_..."` to export production LLM trajectories. The exporter is fail-silent and batches in the background.

Use `zhanla.execute_component(component, input)` for bare dispatch without trajectory export. In serverless environments, call `zhanla.flush()` before shutdown.

## Observability helpers

Use `zhanla.wrap(client)` when calling a provider client directly inside a tool or helper function:

```python
client = zhanla.wrap(anthropic.Anthropic())
```

Runner-backed components already wrap their client internally.

Use `zhanla.parse_json_response(text)` to parse bare JSON or fenced JSON from model output.

## Common gotchas

1. Components must be module-level for CLI discovery.
2. Every component needs an explicit valid `key`.
3. CLI target suffixes use `key`, not variable name or display `name`.
4. `Skill` cannot run directly.
5. Prompt-backed local execution requires `model` plus `client` or `runner`, not both.
6. `Tool.input_schema` must normalize to an object JSON Schema.
7. Local eval kwargs are text strings; parse JSON/YAML inside the eval body.
8. Python `CodeEval.fn` must require `model_response`.
9. `LLMEval` must provide exactly one of `instructions` or `questions`.
10. Local JSON datasets must start with `_schema`; `{"schema": ..., "rows": ...}` is not accepted by the CLI.
