uno-sdk 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- uno_sdk/__init__.py +19 -0
- uno_sdk/_version.py +1 -0
- uno_sdk/adapters/__init__.py +11 -0
- uno_sdk/adapters/anthropic.py +60 -0
- uno_sdk/adapters/openai.py +66 -0
- uno_sdk/client.py +136 -0
- uno_sdk/exceptions.py +31 -0
- uno_sdk/models.py +52 -0
- uno_sdk-1.0.0.dist-info/METADATA +211 -0
- uno_sdk-1.0.0.dist-info/RECORD +12 -0
- uno_sdk-1.0.0.dist-info/WHEEL +4 -0
- uno_sdk-1.0.0.dist-info/licenses/LICENSE +21 -0
uno_sdk/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Uno SDK — search and call 2000+ tools in two lines of Python.
|
|
2
|
+
|
|
3
|
+
Install::
|
|
4
|
+
|
|
5
|
+
pip install uno-sdk
|
|
6
|
+
|
|
7
|
+
Quick start::
|
|
8
|
+
|
|
9
|
+
from uno_sdk import Uno
|
|
10
|
+
uno = Uno(api_key="uk-...")
|
|
11
|
+
tools = uno.search("weather")
|
|
12
|
+
result = uno.call("weather-free.get_current_weather", {"location": "Beijing"})
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from uno_sdk._version import __version__
|
|
16
|
+
from uno_sdk.client import AsyncUno, Uno
|
|
17
|
+
from uno_sdk.models import CallResult, Tool
|
|
18
|
+
|
|
19
|
+
__all__ = ["AsyncUno", "CallResult", "Tool", "Uno", "__version__"]
|
uno_sdk/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.0.0"
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Framework adapters — convert UNO tools to framework-native format."""
|
|
2
|
+
|
|
3
|
+
from uno_sdk.adapters.openai import OpenAIAdapter
|
|
4
|
+
|
|
5
|
+
__all__ = ["OpenAIAdapter"]
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
from uno_sdk.adapters.anthropic import AnthropicAdapter
|
|
9
|
+
__all__.append("AnthropicAdapter")
|
|
10
|
+
except ImportError:
|
|
11
|
+
pass
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Anthropic tool_use adapter.
|
|
2
|
+
|
|
3
|
+
Converts UNO Tool objects to the format expected by
|
|
4
|
+
anthropic.messages.create(tools=[...]).
|
|
5
|
+
|
|
6
|
+
Usage::
|
|
7
|
+
|
|
8
|
+
from uno_sdk import Uno
|
|
9
|
+
from uno_sdk.adapters import AnthropicAdapter
|
|
10
|
+
|
|
11
|
+
uno = Uno(api_key="uk-xxx")
|
|
12
|
+
tools = uno.search("weather", adapter=AnthropicAdapter())
|
|
13
|
+
|
|
14
|
+
import anthropic
|
|
15
|
+
response = anthropic.messages.create(
|
|
16
|
+
model="claude-sonnet-4-20250514",
|
|
17
|
+
messages=[{"role": "user", "content": "What's the weather?"}],
|
|
18
|
+
tools=tools,
|
|
19
|
+
)
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
from uno_sdk.models import Tool
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class AnthropicAdapter:
|
|
30
|
+
"""Convert UNO tools to Anthropic tool_use format."""
|
|
31
|
+
|
|
32
|
+
def wrap_tool(self, tool: Tool) -> dict[str, Any]:
|
|
33
|
+
"""Convert a single tool to Anthropic tool schema.
|
|
34
|
+
|
|
35
|
+
Anthropic tool names must match ^[a-zA-Z0-9_-]{1,64}$ so we
|
|
36
|
+
replace dots with double underscores (same as OpenAI adapter).
|
|
37
|
+
"""
|
|
38
|
+
func_name = tool.slug.replace(".", "__")
|
|
39
|
+
|
|
40
|
+
schema = dict(tool.input_schema) if tool.input_schema else {}
|
|
41
|
+
if "type" not in schema:
|
|
42
|
+
schema["type"] = "object"
|
|
43
|
+
|
|
44
|
+
desc = tool.description or tool.name
|
|
45
|
+
if tool.auth_required:
|
|
46
|
+
desc = f"[OAuth required] {desc}"
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
"name": func_name,
|
|
50
|
+
"description": desc[:1024],
|
|
51
|
+
"input_schema": schema,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
def wrap_tools(self, tools: list[Tool]) -> list[dict[str, Any]]:
|
|
55
|
+
return [self.wrap_tool(t) for t in tools]
|
|
56
|
+
|
|
57
|
+
@staticmethod
|
|
58
|
+
def slug_from_tool_name(tool_name: str) -> str:
|
|
59
|
+
"""Reverse: weather__get_current → weather.get_current."""
|
|
60
|
+
return tool_name.replace("__", ".", 1)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""OpenAI function calling adapter.
|
|
2
|
+
|
|
3
|
+
Converts UNO Tool objects to the format expected by
|
|
4
|
+
openai.chat.completions.create(tools=[...]).
|
|
5
|
+
|
|
6
|
+
Usage::
|
|
7
|
+
|
|
8
|
+
from uno_sdk import Uno
|
|
9
|
+
from uno_sdk.adapters import OpenAIAdapter
|
|
10
|
+
|
|
11
|
+
uno = Uno(api_key="uk-xxx")
|
|
12
|
+
tools = uno.search("email", adapter=OpenAIAdapter())
|
|
13
|
+
|
|
14
|
+
import openai
|
|
15
|
+
response = openai.chat.completions.create(
|
|
16
|
+
model="gpt-4o",
|
|
17
|
+
messages=[{"role": "user", "content": "Send an email to ..."}],
|
|
18
|
+
tools=tools,
|
|
19
|
+
)
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
from uno_sdk.models import Tool
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class OpenAIAdapter:
|
|
30
|
+
"""Convert UNO tools to OpenAI function calling format."""
|
|
31
|
+
|
|
32
|
+
def wrap_tool(self, tool: Tool) -> dict[str, Any]:
|
|
33
|
+
"""Convert a single tool to OpenAI function schema.
|
|
34
|
+
|
|
35
|
+
OpenAI function names must match ^[a-zA-Z0-9_-]+$ so we replace
|
|
36
|
+
dots in UNO slugs with double underscores.
|
|
37
|
+
"""
|
|
38
|
+
# OpenAI function name: replace dots with __ (reversible)
|
|
39
|
+
func_name = tool.slug.replace(".", "__")
|
|
40
|
+
|
|
41
|
+
schema = dict(tool.input_schema) if tool.input_schema else {}
|
|
42
|
+
# Ensure schema has type: object (OpenAI requirement)
|
|
43
|
+
if "type" not in schema:
|
|
44
|
+
schema["type"] = "object"
|
|
45
|
+
|
|
46
|
+
desc = tool.description or tool.name
|
|
47
|
+
if tool.auth_required:
|
|
48
|
+
desc = f"[OAuth required] {desc}"
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
"type": "function",
|
|
52
|
+
"function": {
|
|
53
|
+
"name": func_name,
|
|
54
|
+
"description": desc[:1024],
|
|
55
|
+
"parameters": schema,
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
def wrap_tools(self, tools: list[Tool]) -> list[dict[str, Any]]:
|
|
60
|
+
"""Convert a list of tools."""
|
|
61
|
+
return [self.wrap_tool(t) for t in tools]
|
|
62
|
+
|
|
63
|
+
@staticmethod
|
|
64
|
+
def slug_from_function_name(func_name: str) -> str:
|
|
65
|
+
"""Reverse the name encoding: weather__get_current → weather.get_current."""
|
|
66
|
+
return func_name.replace("__", ".", 1)
|
uno_sdk/client.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Uno SDK client — synchronous and asynchronous."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from uno_sdk.exceptions import AuthError, AuthRequiredError, QuotaError, ToolNotFoundError, UnoError
|
|
10
|
+
from uno_sdk.models import CallResult, Tool
|
|
11
|
+
|
|
12
|
+
_DEFAULT_BASE = "https://clawdtools.uno"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _handle_error(resp_data: dict) -> None:
|
|
16
|
+
"""Raise the appropriate exception based on error code."""
|
|
17
|
+
error = resp_data.get("error")
|
|
18
|
+
if not error:
|
|
19
|
+
return
|
|
20
|
+
msg = resp_data.get("message", str(error))
|
|
21
|
+
code = error if isinstance(error, str) else None
|
|
22
|
+
|
|
23
|
+
if code == "auth_required":
|
|
24
|
+
raise AuthRequiredError(msg, auth_url=resp_data.get("auth_url"), code=code)
|
|
25
|
+
if code == "insufficient_credits":
|
|
26
|
+
raise QuotaError(msg, code=code)
|
|
27
|
+
if code == "tool_not_found":
|
|
28
|
+
raise ToolNotFoundError(msg, code=code)
|
|
29
|
+
if code in ("invalid_api_key", "not_authenticated"):
|
|
30
|
+
raise AuthError(msg, code=code)
|
|
31
|
+
raise UnoError(msg, code=code)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Uno:
|
|
35
|
+
"""Synchronous Uno client.
|
|
36
|
+
|
|
37
|
+
Usage::
|
|
38
|
+
|
|
39
|
+
from uno_sdk import Uno
|
|
40
|
+
uno = Uno(api_key="uk-xxx")
|
|
41
|
+
tools = uno.search("send email")
|
|
42
|
+
result = uno.call("email.send_email", {"to": "a@b.com", "subject": "Hi"})
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(self, api_key: str, base_url: str = _DEFAULT_BASE, timeout: float = 180):
|
|
46
|
+
self._base = base_url.rstrip("/")
|
|
47
|
+
self._client = httpx.Client(
|
|
48
|
+
base_url=self._base,
|
|
49
|
+
headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
|
|
50
|
+
timeout=timeout,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def search(self, query: str = "", *, limit: int = 10, adapter: Any = None) -> list[Tool] | list[dict]:
|
|
54
|
+
"""Search tools. If adapter is provided, return framework-native format."""
|
|
55
|
+
resp = self._client.get("/v1/tools", params={"q": query, "limit": limit})
|
|
56
|
+
resp.raise_for_status()
|
|
57
|
+
data = resp.json()
|
|
58
|
+
tools = [Tool.from_api(t) for t in data.get("tools", [])]
|
|
59
|
+
if adapter:
|
|
60
|
+
return adapter.wrap_tools(tools)
|
|
61
|
+
return tools
|
|
62
|
+
|
|
63
|
+
def call(self, tool: str, arguments: dict[str, Any] | None = None) -> CallResult:
|
|
64
|
+
"""Call a tool and return the result."""
|
|
65
|
+
resp = self._client.post("/v1/call", json={"tool": tool, "arguments": arguments or {}})
|
|
66
|
+
if resp.status_code == 401:
|
|
67
|
+
raise AuthError("Invalid API key", code="invalid_api_key", status=401)
|
|
68
|
+
data = resp.json()
|
|
69
|
+
_handle_error(data)
|
|
70
|
+
return CallResult.from_api(data)
|
|
71
|
+
|
|
72
|
+
def me(self) -> dict:
|
|
73
|
+
"""Get current user info (credits, plan, etc.)."""
|
|
74
|
+
resp = self._client.get("/v1/auth/me")
|
|
75
|
+
resp.raise_for_status()
|
|
76
|
+
return resp.json()
|
|
77
|
+
|
|
78
|
+
def close(self):
|
|
79
|
+
self._client.close()
|
|
80
|
+
|
|
81
|
+
def __enter__(self):
|
|
82
|
+
return self
|
|
83
|
+
|
|
84
|
+
def __exit__(self, *args):
|
|
85
|
+
self.close()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class AsyncUno:
|
|
89
|
+
"""Asynchronous Uno client.
|
|
90
|
+
|
|
91
|
+
Usage::
|
|
92
|
+
|
|
93
|
+
from uno_sdk import AsyncUno
|
|
94
|
+
async with AsyncUno(api_key="uk-xxx") as uno:
|
|
95
|
+
tools = await uno.search("weather")
|
|
96
|
+
result = await uno.call("weather.get_current", {"city": "Beijing"})
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
def __init__(self, api_key: str, base_url: str = _DEFAULT_BASE, timeout: float = 180):
|
|
100
|
+
self._base = base_url.rstrip("/")
|
|
101
|
+
self._client = httpx.AsyncClient(
|
|
102
|
+
base_url=self._base,
|
|
103
|
+
headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
|
|
104
|
+
timeout=timeout,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
async def search(self, query: str = "", *, limit: int = 10, adapter: Any = None) -> list[Tool] | list[dict]:
|
|
108
|
+
resp = await self._client.get("/v1/tools", params={"q": query, "limit": limit})
|
|
109
|
+
resp.raise_for_status()
|
|
110
|
+
data = resp.json()
|
|
111
|
+
tools = [Tool.from_api(t) for t in data.get("tools", [])]
|
|
112
|
+
if adapter:
|
|
113
|
+
return adapter.wrap_tools(tools)
|
|
114
|
+
return tools
|
|
115
|
+
|
|
116
|
+
async def call(self, tool: str, arguments: dict[str, Any] | None = None) -> CallResult:
|
|
117
|
+
resp = await self._client.post("/v1/call", json={"tool": tool, "arguments": arguments or {}})
|
|
118
|
+
if resp.status_code == 401:
|
|
119
|
+
raise AuthError("Invalid API key", code="invalid_api_key", status=401)
|
|
120
|
+
data = resp.json()
|
|
121
|
+
_handle_error(data)
|
|
122
|
+
return CallResult.from_api(data)
|
|
123
|
+
|
|
124
|
+
async def me(self) -> dict:
|
|
125
|
+
resp = await self._client.get("/v1/auth/me")
|
|
126
|
+
resp.raise_for_status()
|
|
127
|
+
return resp.json()
|
|
128
|
+
|
|
129
|
+
async def close(self):
|
|
130
|
+
await self._client.aclose()
|
|
131
|
+
|
|
132
|
+
async def __aenter__(self):
|
|
133
|
+
return self
|
|
134
|
+
|
|
135
|
+
async def __aexit__(self, *args):
|
|
136
|
+
await self.close()
|
uno_sdk/exceptions.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Uno SDK exceptions."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class UnoError(Exception):
|
|
5
|
+
"""Base exception for Uno SDK."""
|
|
6
|
+
def __init__(self, message: str, code: str | None = None, status: int | None = None):
|
|
7
|
+
super().__init__(message)
|
|
8
|
+
self.code = code
|
|
9
|
+
self.status = status
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AuthError(UnoError):
|
|
13
|
+
"""Authentication failed (invalid or missing API key)."""
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class QuotaError(UnoError):
|
|
18
|
+
"""Insufficient credits."""
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ToolNotFoundError(UnoError):
|
|
23
|
+
"""The requested tool does not exist."""
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AuthRequiredError(UnoError):
|
|
28
|
+
"""The tool requires OAuth authorization."""
|
|
29
|
+
def __init__(self, message: str, auth_url: str | None = None, **kw):
|
|
30
|
+
super().__init__(message, **kw)
|
|
31
|
+
self.auth_url = auth_url
|
uno_sdk/models.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Data models for the Uno SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class Tool:
|
|
11
|
+
"""A tool returned by search."""
|
|
12
|
+
slug: str
|
|
13
|
+
name: str
|
|
14
|
+
description: str
|
|
15
|
+
input_schema: dict[str, Any] = field(default_factory=dict)
|
|
16
|
+
auth_required: bool = False
|
|
17
|
+
pricing_mode: str = "free"
|
|
18
|
+
credit_cost: float = 0
|
|
19
|
+
stats: dict[str, Any] = field(default_factory=dict)
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def from_api(cls, data: dict) -> Tool:
|
|
23
|
+
return cls(
|
|
24
|
+
slug=data.get("tool", ""),
|
|
25
|
+
name=data.get("name", ""),
|
|
26
|
+
description=data.get("desc") or data.get("description", ""),
|
|
27
|
+
input_schema=data.get("input_schema", {}),
|
|
28
|
+
auth_required=data.get("auth_required", False),
|
|
29
|
+
pricing_mode=data.get("pricing", {}).get("mode", "free"),
|
|
30
|
+
credit_cost=data.get("pricing", {}).get("cost", 0),
|
|
31
|
+
stats=data.get("stats", {}),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class CallResult:
|
|
37
|
+
"""Result of calling a tool."""
|
|
38
|
+
data: Any = None
|
|
39
|
+
error: str | None = None
|
|
40
|
+
meta: dict[str, Any] = field(default_factory=dict)
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def ok(self) -> bool:
|
|
44
|
+
return self.error is None
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def from_api(cls, data: dict) -> CallResult:
|
|
48
|
+
return cls(
|
|
49
|
+
data=data.get("data"),
|
|
50
|
+
error=data.get("error"),
|
|
51
|
+
meta=data.get("meta", {}),
|
|
52
|
+
)
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: uno-sdk
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Python SDK for Uno — the ClawdChat agent tool gateway (2000+ tools). Sync & async clients, OpenAI/Anthropic adapters.
|
|
5
|
+
Project-URL: Homepage, https://clawdtools.uno
|
|
6
|
+
Project-URL: Documentation, https://clawdtools.uno/docs/sdk/python
|
|
7
|
+
Project-URL: Repository, https://github.com/xray918/uno-sdk
|
|
8
|
+
Project-URL: Issues, https://github.com/xray918/uno-sdk/issues
|
|
9
|
+
Author-email: ClawdChat <dev@clawdchat.cn>
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: agent,anthropic,clawdchat,llm,mcp,openai,sdk,tool-gateway,uno
|
|
13
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Requires-Dist: httpx>=0.27
|
|
25
|
+
Provides-Extra: all
|
|
26
|
+
Requires-Dist: anthropic>=0.30; extra == 'all'
|
|
27
|
+
Requires-Dist: openai>=1.0; extra == 'all'
|
|
28
|
+
Provides-Extra: anthropic
|
|
29
|
+
Requires-Dist: anthropic>=0.30; extra == 'anthropic'
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: pytest-asyncio>=0.24.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
33
|
+
Requires-Dist: respx>=0.21.0; extra == 'dev'
|
|
34
|
+
Provides-Extra: openai
|
|
35
|
+
Requires-Dist: openai>=1.0; extra == 'openai'
|
|
36
|
+
Description-Content-Type: text/markdown
|
|
37
|
+
|
|
38
|
+
# Uno SDK for Python
|
|
39
|
+
|
|
40
|
+
> Search and call **2000+ real-world tools** from Python in two lines. Powered by [ClawdChat](https://clawdtools.uno).
|
|
41
|
+
|
|
42
|
+
[](https://pypi.org/project/uno-sdk/)
|
|
43
|
+
[](https://pypi.org/project/uno-sdk/)
|
|
44
|
+
[](https://github.com/xray918/uno-sdk/blob/main/LICENSE)
|
|
45
|
+
|
|
46
|
+
## Install
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install uno-sdk # core
|
|
50
|
+
pip install uno-sdk[openai] # + OpenAI adapter
|
|
51
|
+
pip install uno-sdk[anthropic] # + Anthropic adapter
|
|
52
|
+
pip install uno-sdk[all] # everything
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
> 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.
|
|
56
|
+
|
|
57
|
+
## Quick Start
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from uno_sdk import Uno
|
|
61
|
+
|
|
62
|
+
uno = Uno(api_key="uk-xxx")
|
|
63
|
+
|
|
64
|
+
# Search tools
|
|
65
|
+
tools = uno.search("send email")
|
|
66
|
+
print(tools[0].name, tools[0].description)
|
|
67
|
+
|
|
68
|
+
# Call a tool
|
|
69
|
+
result = uno.call("email.send_email", {
|
|
70
|
+
"to": "alice@example.com",
|
|
71
|
+
"subject": "Hello",
|
|
72
|
+
"body": "Hi from Uno!"
|
|
73
|
+
})
|
|
74
|
+
print(result.data)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## OpenAI Integration
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
from uno_sdk import Uno
|
|
81
|
+
from uno_sdk.adapters import OpenAIAdapter
|
|
82
|
+
from openai import OpenAI
|
|
83
|
+
|
|
84
|
+
uno = Uno(api_key="uk-xxx")
|
|
85
|
+
openai_client = OpenAI()
|
|
86
|
+
|
|
87
|
+
# Get tools in OpenAI format
|
|
88
|
+
tools = uno.search("weather", adapter=OpenAIAdapter())
|
|
89
|
+
|
|
90
|
+
# Use with chat completions
|
|
91
|
+
response = openai_client.chat.completions.create(
|
|
92
|
+
model="gpt-4o",
|
|
93
|
+
messages=[{"role": "user", "content": "What's the weather in Beijing?"}],
|
|
94
|
+
tools=tools,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Execute the tool call
|
|
98
|
+
if response.choices[0].message.tool_calls:
|
|
99
|
+
tc = response.choices[0].message.tool_calls[0]
|
|
100
|
+
import json
|
|
101
|
+
slug = OpenAIAdapter.slug_from_function_name(tc.function.name)
|
|
102
|
+
result = uno.call(slug, json.loads(tc.function.arguments))
|
|
103
|
+
print(result.data)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Anthropic Integration
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from uno_sdk import Uno
|
|
110
|
+
from uno_sdk.adapters import AnthropicAdapter
|
|
111
|
+
import anthropic
|
|
112
|
+
|
|
113
|
+
uno = Uno(api_key="uk-xxx")
|
|
114
|
+
client = anthropic.Anthropic()
|
|
115
|
+
|
|
116
|
+
tools = uno.search("search", adapter=AnthropicAdapter())
|
|
117
|
+
response = client.messages.create(
|
|
118
|
+
model="claude-sonnet-4-20250514",
|
|
119
|
+
messages=[{"role": "user", "content": "Search for AI news"}],
|
|
120
|
+
tools=tools,
|
|
121
|
+
max_tokens=1024,
|
|
122
|
+
)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Async
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
from uno_sdk import AsyncUno
|
|
129
|
+
|
|
130
|
+
async with AsyncUno(api_key="uk-xxx") as uno:
|
|
131
|
+
tools = await uno.search("translate")
|
|
132
|
+
result = await uno.call("translate.text", {"text": "hello", "to": "zh"})
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## MCP (Claude Desktop / Cursor)
|
|
136
|
+
|
|
137
|
+
No SDK needed — connect directly:
|
|
138
|
+
|
|
139
|
+
```json
|
|
140
|
+
{
|
|
141
|
+
"mcpServers": {
|
|
142
|
+
"uno": {
|
|
143
|
+
"url": "https://clawdtools.uno/mcp"
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
OAuth login opens automatically in your browser.
|
|
150
|
+
|
|
151
|
+
## Error Handling
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
from uno_sdk.exceptions import AuthRequiredError, QuotaError, ToolNotFoundError
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
result = uno.call("github.list_repos", {})
|
|
158
|
+
except AuthRequiredError as e:
|
|
159
|
+
print(f"Please authorize: {e.auth_url}")
|
|
160
|
+
except QuotaError:
|
|
161
|
+
print("Out of credits — visit https://clawdtools.uno/pricing")
|
|
162
|
+
except ToolNotFoundError:
|
|
163
|
+
print("Tool not found — search first")
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## API
|
|
167
|
+
|
|
168
|
+
### `Uno(api_key, base_url="https://clawdtools.uno", timeout=180)`
|
|
169
|
+
|
|
170
|
+
| Method | Returns | Description |
|
|
171
|
+
|---|---|---|
|
|
172
|
+
| `search(query, limit=10, adapter=None)` | `list[Tool]` or `list[dict]` | Search tools |
|
|
173
|
+
| `call(tool, arguments={})` | `CallResult` | Call a tool |
|
|
174
|
+
| `me()` | `dict` | Current user info |
|
|
175
|
+
|
|
176
|
+
`AsyncUno` has the same methods, all `async`.
|
|
177
|
+
|
|
178
|
+
### `Tool`
|
|
179
|
+
|
|
180
|
+
| Field | Type | Description |
|
|
181
|
+
|---|---|---|
|
|
182
|
+
| `slug` | `str` | Tool identifier (e.g. `weather.get_current`) |
|
|
183
|
+
| `name` | `str` | Display name |
|
|
184
|
+
| `description` | `str` | What the tool does |
|
|
185
|
+
| `input_schema` | `dict` | JSON Schema for arguments |
|
|
186
|
+
| `auth_required` | `bool` | Needs OAuth? |
|
|
187
|
+
| `pricing_mode` | `str` | `free` / `per_call` / `per_token` |
|
|
188
|
+
| `credit_cost` | `float` | Credits per call |
|
|
189
|
+
|
|
190
|
+
### `CallResult`
|
|
191
|
+
|
|
192
|
+
| Field | Type | Description |
|
|
193
|
+
|---|---|---|
|
|
194
|
+
| `data` | `Any` | Tool response data |
|
|
195
|
+
| `error` | `str \| None` | Error message if failed |
|
|
196
|
+
| `meta` | `dict` | Latency, credits used |
|
|
197
|
+
| `ok` | `bool` | `True` if no error |
|
|
198
|
+
|
|
199
|
+
## Companion: Uno CLI
|
|
200
|
+
|
|
201
|
+
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/).
|
|
202
|
+
|
|
203
|
+
## Get an API Key
|
|
204
|
+
|
|
205
|
+
1. Visit [clawdtools.uno/login](https://clawdtools.uno/login)
|
|
206
|
+
2. Log in with ClawdChat / Google / Phone
|
|
207
|
+
3. Copy your API key from the Dashboard
|
|
208
|
+
|
|
209
|
+
## License
|
|
210
|
+
|
|
211
|
+
MIT © ClawdChat.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
uno_sdk/__init__.py,sha256=lx7FXE4kyZKoS10xTdF029lRcrPkvIGOL6skHO0MVn0,495
|
|
2
|
+
uno_sdk/_version.py,sha256=J-j-u0itpEFT6irdmWmixQqYMadNl1X91TxUmoiLHMI,22
|
|
3
|
+
uno_sdk/client.py,sha256=Q_VHcBSILr-UghbQBngyugV_fe0578d3l3MZu3kRpsw,4660
|
|
4
|
+
uno_sdk/exceptions.py,sha256=mBZdzpJdI1zmt5LhWozw1GGvY6uIvL5K6weFV1olpVI,765
|
|
5
|
+
uno_sdk/models.py,sha256=fMo8GreS_mhpDYhIhPjTXorjt5CXdduS5Z5aKp5BtSY,1443
|
|
6
|
+
uno_sdk/adapters/__init__.py,sha256=8_BL79-OQJWGgrwtixsl4ip6m38Ix8Sj0Jv0z0sM9F4,289
|
|
7
|
+
uno_sdk/adapters/anthropic.py,sha256=XPTjW8q2bzqaXtLCS1WmOQlZfKhGRRJYxrePrwzixtQ,1715
|
|
8
|
+
uno_sdk/adapters/openai.py,sha256=bA2A5uuzBG6tyeN6mDJekQp_DjZcwwFltuGXWoIDoGw,1961
|
|
9
|
+
uno_sdk-1.0.0.dist-info/METADATA,sha256=IF5lR09DZeHrsNBlgejERyAO1ZHXzsbm0WFEQI46aMc,6338
|
|
10
|
+
uno_sdk-1.0.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
11
|
+
uno_sdk-1.0.0.dist-info/licenses/LICENSE,sha256=P4MQssWPW9flLESkfwI7OzqoIg0jvtSpk4Q7ltDC2y4,1066
|
|
12
|
+
uno_sdk-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ClawdChat
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|