Metadata-Version: 2.4
Name: uno-sdk
Version: 1.1.0
Summary: Python SDK for Uno — the ClawdChat agent tool gateway (2000+ tools). Sync & async clients, OpenAI/Anthropic adapters.
Project-URL: Homepage, https://clawdtools.uno
Project-URL: Documentation, https://clawdtools.uno/docs/sdk/python
Project-URL: Repository, https://github.com/xray918/uno-sdk
Project-URL: Issues, https://github.com/xray918/uno-sdk/issues
Author-email: ClawdChat <dev@clawdchat.cn>
License: MIT
License-File: LICENSE
Keywords: agent,anthropic,clawdchat,llm,mcp,openai,sdk,tool-gateway,uno
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Requires-Dist: httpx>=0.27
Provides-Extra: all
Requires-Dist: anthropic>=0.30; extra == 'all'
Requires-Dist: openai>=1.0; extra == 'all'
Provides-Extra: anthropic
Requires-Dist: anthropic>=0.30; extra == 'anthropic'
Provides-Extra: dev
Requires-Dist: pytest-asyncio>=0.24.0; extra == 'dev'
Requires-Dist: pytest>=8.0.0; extra == 'dev'
Requires-Dist: respx>=0.21.0; extra == 'dev'
Provides-Extra: openai
Requires-Dist: openai>=1.0; extra == 'openai'
Description-Content-Type: text/markdown

# Uno SDK for Python

> Search and call **2000+ real-world tools** from Python in two lines. Powered by [ClawdChat](https://clawdtools.uno).

[![PyPI](https://img.shields.io/pypi/v/uno-sdk.svg)](https://pypi.org/project/uno-sdk/)
[![Python](https://img.shields.io/pypi/pyversions/uno-sdk.svg)](https://pypi.org/project/uno-sdk/)
[![License](https://img.shields.io/pypi/l/uno-sdk.svg)](https://github.com/xray918/uno-sdk/blob/main/LICENSE)

## Install

```bash
pip install uno-sdk                    # core
pip install uno-sdk[openai]            # + OpenAI adapter
pip install uno-sdk[anthropic]         # + Anthropic adapter
pip install uno-sdk[all]               # everything
```

> Note on naming: the PyPI distribution is **`uno-sdk`** and the import name is **`uno_sdk`**. The bare `uno` PyPI slot is held by an unrelated Python 2-era package (2014, no longer installable) — `uno_sdk` keeps our namespace clean.

## Quick Start

```python
from uno_sdk import Uno

uno = Uno(api_key="uk-xxx")

# Search tools
tools = uno.search("send email")
print(tools[0].name, tools[0].description)

# Call a tool
result = uno.call("email.send_email", {
    "to": "alice@example.com",
    "subject": "Hello",
    "body": "Hi from Uno!"
})
print(result.data)
```

## OpenAI Integration

```python
from uno_sdk import Uno
from uno_sdk.adapters import OpenAIAdapter
from openai import OpenAI

uno = Uno(api_key="uk-xxx")
openai_client = OpenAI()

# Get tools in OpenAI format
tools = uno.search("weather", adapter=OpenAIAdapter())

# Use with chat completions
response = openai_client.chat.completions.create(
    model="gpt-4o",
    messages=[{"role": "user", "content": "What's the weather in Beijing?"}],
    tools=tools,
)

# Execute the tool call
if response.choices[0].message.tool_calls:
    tc = response.choices[0].message.tool_calls[0]
    import json
    slug = OpenAIAdapter.slug_from_function_name(tc.function.name)
    result = uno.call(slug, json.loads(tc.function.arguments))
    print(result.data)
```

## Anthropic Integration

```python
from uno_sdk import Uno
from uno_sdk.adapters import AnthropicAdapter
import anthropic

uno = Uno(api_key="uk-xxx")
client = anthropic.Anthropic()

tools = uno.search("search", adapter=AnthropicAdapter())
response = client.messages.create(
    model="claude-sonnet-4-20250514",
    messages=[{"role": "user", "content": "Search for AI news"}],
    tools=tools,
    max_tokens=1024,
)
```

## Async

```python
from uno_sdk import AsyncUno

async with AsyncUno(api_key="uk-xxx") as uno:
    tools = await uno.search("translate")
    result = await uno.call("translate.text", {"text": "hello", "to": "zh"})
```

## Resilience: retry, timeout, validation

`call()` gains optional keyword args for transient-error resilience and
client-side argument validation. All opts are off by default — existing
callers see no behaviour change.

```python
from uno_sdk import Uno, RateLimitError, UpstreamTimeoutError

uno = Uno(api_key="uk-xxx")

# Retry up to 3 times on rate limit / upstream timeout / 5xx, with exp backoff.
result = uno.call(
    "tikhub-douyin.fetch_user_post_videos",
    {"sec_user_id": "MS4w...", "count": 35},
    retry=3,
    retry_delay=0.5,            # 0.5s, 1s, 2s
    timeout=30,                 # per-request timeout override
)

# Validate args against the tool's JSON Schema before sending.
# Does one extra search() to fetch the schema lazily; for hot paths
# prefer call_tool() with a pre-fetched Tool instance.
uno.call("weather.get_current", {"city": "Beijing"}, validate=True)

# call_tool() — skips the search, validates against a known Tool for free.
tool = uno.search("weather")[0]
uno.call_tool(tool, {"city": "Beijing"})  # validates by default
```

## Batch concurrent calls (async only)

`AsyncUno.call_batch` fans out N tool calls concurrently with a semaphore
to stay under the gateway rate limit. Results are returned in input order.

```python
from uno_sdk import AsyncUno

async with AsyncUno(api_key="uk-xxx") as uno:
    # Fetch 30 video stat records in parallel, max 10 in flight at once.
    calls = [("tikhub-douyin.fetch_video_stats", {"aweme_id": aid})
             for aid in aweme_ids[:30]]
    results = await uno.call_batch(calls, max_concurrency=10, return_exceptions=True)
    for aid, r in zip(aweme_ids, results):
        if isinstance(r, Exception):
            print(f"{aid}: {r}")
        else:
            print(f"{aid}: {r.data}")
```

## Long-running jobs: submit → poll (async only)

Tools like transcription / video generation take 30s+ and use a
submit-then-poll pattern. `AsyncUno.call_async` wraps the whole flow —
you don't need to know which tools are submit/result shaped.

```python
from uno_sdk import AsyncUno

async with AsyncUno(api_key="uk-xxx") as uno:
    # One-shot: submit, then poll until done or 180s timeout.
    res = await uno.call_async(
        submit_tool="qingdou-video-text.qingdou_submit",
        submit_args={"urls": "https://v.douyin.com/xxx/"},
        result_tool="qingdou-video-text.qingdou_result",
        max_wait=180,              # total deadline
        wait_seconds_per_call=50,  # server-side wait per poll (under 60s MCP timeout)
        poll_interval=3,           # client-side gap when server returns pending
    )
    print(res.raw["items"][0]["content"])  # full transcript
```

The defaults (`batch_id` / `status` / `done`/`pending`) match qingdou and
the gateway's submit/result convention. For tools with different field
names, pass `id_field` / `status_field` / `done_value` / `pending_values`.

## MCP (Claude Desktop / Cursor)

No SDK needed — connect directly:

```json
{
  "mcpServers": {
    "uno": {
      "url": "https://clawdtools.uno/mcp"
    }
  }
}
```

OAuth login opens automatically in your browser.

## Error Handling

```python
from uno_sdk.exceptions import (
    AuthError, AuthRequiredError, QuotaError, RateLimitError,
    ToolNotFoundError, ToolDisabledError, InvalidArgumentsError,
    UpstreamTimeoutError, UpstreamCancelledError, ServerError,
)

try:
    result = uno.call("github.list_repos", {})
except AuthRequiredError as e:
    print(f"Please authorize: {e.auth_url}")
except RateLimitError as e:
    print(f"Rate limited, retry after {e.retry_after}s")
except QuotaError:
    print("Out of credits — visit https://clawdtools.uno/pricing")
except ToolNotFoundError:
    print("Tool not found — search first")
except InvalidArgumentsError as e:
    print(f"Bad arguments: {e}")
except UpstreamTimeoutError:
    print("Tool timed out at upstream — retry with `retry=3`")
except ServerError as e:
    print(f"Server error: {e}")
```

The `RETRYABLE_ERRORS` tuple lists the exceptions worth retrying:

```python
from uno_sdk import RETRYABLE_ERRORS
# (RateLimitError, UpstreamTimeoutError, UpstreamCancelledError, ServerError)
```

## API

### `Uno(api_key, base_url="https://clawdtools.uno", timeout=180)`

| Method | Returns | Description |
|---|---|---|
| `search(query, *, limit=10, adapter=None)` | `list[Tool]` or `list[dict]` | Search tools |
| `call(tool, arguments={}, *, retry=0, retry_on=RETRYABLE_ERRORS, retry_delay=0.5, timeout=None, validate=False)` | `CallResult` | Call a tool, with optional retry + validation |
| `call_tool(tool, arguments={}, *, validate=True, retry=0, …)` | `CallResult` | Call using a pre-fetched `Tool` — skips search, validates for free |
| `me()` | `dict` | Current user info |

`AsyncUno` has the same methods, all `async`, plus:

| Method | Returns | Description |
|---|---|---|
| `await call_batch(calls, *, max_concurrency=10, return_exceptions=False, …)` | `list[CallResult \| UnoError]` | Concurrent batch with order preservation |
| `await call_async(submit_tool, submit_args, result_tool, *, max_wait=180, …)` | `CallResult` | Submit + poll long-running jobs to completion |

### `Tool`

| Field | Type | Description |
|---|---|---|
| `slug` | `str` | Tool identifier (e.g. `weather.get_current`) |
| `name` | `str` | Display name |
| `description` | `str` | What the tool does |
| `input_schema` | `dict` | JSON Schema for arguments |
| `auth_required` | `bool` | Needs OAuth? |
| `pricing_mode` | `str` | `free` / `per_call` / `per_token` |
| `credit_cost` | `float` | Credits per call |
| `stats` | `dict` | Calls / rating / etc. |

| Method | Returns | Description |
|---|---|---|
| `validate_args(arguments)` | `list[str]` | Lightweight JSON-Schema validation. Empty list = valid. |

### `CallResult`

| Field | Type | Description |
|---|---|---|
| `data` | `Any` | Tool response data |
| `error` | `str \| None` | Error message if failed |
| `meta` | `dict` | Latency, credits used |
| `raw` | `dict` | Full response envelope (for extracting `auth_url` / `recharge_url` / etc.) |
| `ok` | `bool` | `True` if no error (property) |

## Companion: Uno CLI

Prefer a command-line flow? `pip install uno-cli` ships the `uno` command (search, call, multi-account OAuth, scope enforcement) — same credentials file, same gateway. See [`uno-cli`](https://pypi.org/project/uno-cli/).

## Get an API Key

1. Visit [clawdtools.uno/login](https://clawdtools.uno/login)
2. Log in with ClawdChat / Google / Phone
3. Copy your API key from the Dashboard

## License

MIT © ClawdChat.
