|
| 1 | +# Qoder Cloud Agents Python SDK |
| 2 | + |
| 3 | +Python 3.10+,同步与原生异步客户端,支持类型化响应、自动分页、SSE 和文件传输。 |
| 4 | + |
| 5 | +完整 API 参考:[Forward API](docs/forward-api.md) · [Managed API](docs/managed-api.md),包含初始化、同步/异步调用、分页、SSE、文件传输,以及全部资源方法的参数、返回类型与 HTTP 路由。 |
| 6 | + |
| 7 | +## 安装与配置 |
| 8 | + |
| 9 | +```bash |
| 10 | +# 在本仓库中安装 |
| 11 | +python -m pip install . |
| 12 | +# 开发环境 |
| 13 | +python -m pip install -e '.[dev]' |
| 14 | +``` |
| 15 | + |
| 16 | +```python |
| 17 | +from qca import Forward, Managed |
| 18 | + |
| 19 | +with Forward() as client: |
| 20 | + for model in client.models.list().data: |
| 21 | + if model.is_enabled: |
| 22 | + print(model.id) |
| 23 | + |
| 24 | +with Managed() as client: |
| 25 | + for agent in client.agents.list(limit=20): |
| 26 | + print(agent.id, agent.name) |
| 27 | +``` |
| 28 | + |
| 29 | +也可使用 `from qca.forward import Client` 或 `from qca.managed import Client`。两种模式独立实例化,使用各自的资源与类型。 |
| 30 | + |
| 31 | +| 配置 | Forward | Managed | |
| 32 | +|---|---|---| |
| 33 | +| 令牌 | `QODER_ACCESS_TOKEN` | `QODER_ACCESS_TOKEN` | |
| 34 | +| API 根地址 | `QODER_FORWARD_BASE_URL` | `QODER_BASE_URL` | |
| 35 | +| 默认地址 | `https://api.qoder.com/api/v1/forward/` | `https://api.qoder.com/api/v1/cloud/` | |
| 36 | + |
| 37 | +显式参数优先于环境变量。客户端不读取 `.env`;只有 examples 读取 `.env.live`。CN 环境需要显式配置对应根地址: |
| 38 | + |
| 39 | +```python |
| 40 | +client = Forward( |
| 41 | + access_token="your-access-token", |
| 42 | + base_url="https://api.qoder.com.cn/api/v1/forward", |
| 43 | + timeout=30.0, |
| 44 | + max_retries=2, |
| 45 | +) |
| 46 | +``` |
| 47 | + |
| 48 | +## 会话 |
| 49 | + |
| 50 | +Forward 通过 Identity 和 Template 创建 Session,Managed 通过 Agent 和 Environment 创建 Session: |
| 51 | + |
| 52 | +```python |
| 53 | +from qca import Forward |
| 54 | + |
| 55 | +with Forward() as client: |
| 56 | + environment = client.environments.create(name="demo", config={"type": "cloud"}) |
| 57 | + identity = client.identities.create(external_id="example-user", name="示例用户") |
| 58 | + template = client.templates.create( |
| 59 | + name="assistant", environment_id=environment.id, |
| 60 | + model="ultimate", # 使用当前账号已启用的模型 |
| 61 | + system="根据可读取的资料回答问题。", |
| 62 | + tools=[{"type": "agent_toolset_20260401"}], |
| 63 | + ) |
| 64 | + session = client.sessions.create(identity_id=identity.id, template_id=template.id) |
| 65 | + print(session.id) |
| 66 | +``` |
| 67 | + |
| 68 | +```python |
| 69 | +from qca import Managed |
| 70 | + |
| 71 | +with Managed() as client: |
| 72 | + environment = client.environments.create(name="demo", config={"type": "cloud"}) |
| 73 | + agent = client.agents.create( |
| 74 | + name="assistant", model={"id": "ultimate"}, |
| 75 | + system="根据可读取的资料回答问题。", |
| 76 | + tools=[{"type": "agent_toolset_20260401"}], |
| 77 | + ) |
| 78 | + session = client.sessions.create(environment_id=environment.id, agent=agent.id) |
| 79 | + print(session.id) |
| 80 | +``` |
| 81 | + |
| 82 | +片段会创建资源。包含执行断言和清理的完整用例见 [examples](examples/README.md)。Forward 还提供 Schedule、Batch、Channel;Managed 提供 Deployment、Dream、自托管环境 Work API。 |
| 83 | + |
| 84 | +## 消息与 SSE |
| 85 | + |
| 86 | +下列代码适用于两种客户端。在同一段对话中复用 `session_id`,同一条逻辑消息的 HTTP 重试复用幂等键。 |
| 87 | + |
| 88 | +```python |
| 89 | +from uuid import uuid4 |
| 90 | + |
| 91 | +sent = client.sessions.events.send( |
| 92 | + session_id, |
| 93 | + events=[{"type": "user.message", "content": [{"type": "text", "text": "你好"}]}], |
| 94 | + extra_headers={"Idempotency-Key": uuid4().hex}, |
| 95 | +) |
| 96 | + |
| 97 | +with client.sessions.events.stream( |
| 98 | + session_id, |
| 99 | + extra_headers={"Last-Event-ID": sent.data[0].id}, |
| 100 | + event_deltas=["agent.message"], |
| 101 | +) as stream: |
| 102 | + for event in stream: |
| 103 | + if event.type == "agent.message": |
| 104 | + print(event.to_json()) |
| 105 | + elif event.type == "session.status_idle": |
| 106 | + print(event.stop_reason) |
| 107 | + break |
| 108 | + elif event.type in ("session.error", "session.status_terminated"): |
| 109 | + raise RuntimeError(f"Session stopped: {event.type}") |
| 110 | +``` |
| 111 | + |
| 112 | +SDK 不自动重连 SSE。保存 `stream.last_event_id`,重连时通过 `Last-Event-ID` 恢复,不要重发已被接收的消息。`event_start`、`event_delta` 是预览,最终事件会再次包含完整内容;同一个 ID 的增量事件不会被去重。idle 可能表示等待确认或达到预算,业务成功还需检查 `stop_reason` 和最终回复。 |
| 113 | + |
| 114 | +## 异步 |
| 115 | + |
| 116 | +异步客户端使用 `httpx.AsyncClient`,请求、重试等待、SSE 读取均为原生异步 I/O。 |
| 117 | + |
| 118 | +```python |
| 119 | +import asyncio |
| 120 | +from qca import AsyncManaged |
| 121 | + |
| 122 | +async def main(): |
| 123 | + async with AsyncManaged() as client: |
| 124 | + async for agent in client.agents.list(limit=20): |
| 125 | + print(agent.id) |
| 126 | + first_page = await client.sessions.list(limit=10) |
| 127 | + print(first_page.data) |
| 128 | + |
| 129 | +asyncio.run(main()) |
| 130 | +``` |
| 131 | + |
| 132 | +异步流使用 `async with await client.sessions.events.stream(...)`。完整片段见 [Forward 异步示例](examples/forward/async_session.py)和 [Managed 异步示例](examples/managed/async_session.py)。本地文件读取通过线程执行,网络请求直接使用异步 HTTP 客户端。 |
| 133 | + |
| 134 | +## 参数与响应 |
| 135 | + |
| 136 | +方法使用 snake_case、关键字参数和类型注解。嵌套资源的目标 ID 可以作为位置参数,祖先 ID 必须具名: |
| 137 | + |
| 138 | +```python |
| 139 | +credential = client.vaults.credentials.retrieve("credential-id", vault_id="vault-id") |
| 140 | +memory = client.memory_stores.memories.retrieve("memory-id", memory_store_id="store-id") |
| 141 | +``` |
| 142 | + |
| 143 | +各 mode 的 `types/*_params.py` 使用 `TypedDict` 定义请求。嵌套参数直接传普通字典;联合类型直接传对应的字符串、字典或列表。响应是 Pydantic 模型,可以直接访问字段,未知字段也会保留。 |
| 144 | + |
| 145 | +```python |
| 146 | +from qca import NOT_GIVEN |
| 147 | + |
| 148 | +client.identities.update("identity-id", name=NOT_GIVEN) # 不发送 name |
| 149 | +client.identities.update("identity-id", name=None) # 发送 null |
| 150 | +client.identities.update("identity-id", enabled=False) # 保留 false |
| 151 | + |
| 152 | +identity = client.identities.retrieve("identity-id") |
| 153 | +print(identity.to_dict()) |
| 154 | +print(identity.to_json()) |
| 155 | +print(identity._request_id) |
| 156 | +print("name" in identity.model_fields_set) # 区分缺失与 null |
| 157 | +``` |
| 158 | + |
| 159 | +能否清空字段由服务端决定。方法均支持 `extra_headers`、`extra_query`、`extra_body`、`timeout`;extra 值优先于方法参数。空数组、空对象、0、false 均保留。 |
| 160 | + |
| 161 | +## 分页 |
| 162 | + |
| 163 | +```python |
| 164 | +page = client.sessions.list(limit=20) |
| 165 | +print(page.data) # 当前页 |
| 166 | +for session in page: # 自动获取后续页 |
| 167 | + print(session.id) |
| 168 | +for page in client.sessions.list().iter_pages(): |
| 169 | + print(len(page.data)) |
| 170 | +``` |
| 171 | + |
| 172 | +SDK 按 Go API 区分 `after_id` / `before_id` 与 `next_page` 分页,后续请求保留过滤条件。游标不前进或循环时抛出异常。非分页列表响应(例如 Models)通过 `.data` 访问。 |
| 173 | + |
| 174 | +## 错误、超时与重试 |
| 175 | + |
| 176 | +```python |
| 177 | +from qca import APIConnectionError, APIStatusError, APITimeoutError |
| 178 | + |
| 179 | +try: |
| 180 | + session = client.sessions.retrieve("sess-id", timeout=10) |
| 181 | +except APITimeoutError: |
| 182 | + print("请求超时") |
| 183 | +except APIConnectionError: |
| 184 | + print("网络连接失败") |
| 185 | +except APIStatusError as exc: |
| 186 | + print(exc.status_code, exc.code, exc.type, exc.request_id) |
| 187 | +``` |
| 188 | + |
| 189 | +HTTP 状态分别对应 `BadRequestError`、`AuthenticationError`、`PermissionDeniedError`、`NotFoundError`、`ConflictError`、`UnprocessableEntityError`、`RateLimitError`、`InternalServerError`。非 JSON 错误正文保留在 `.body`。响应无法解码为声明类型时抛出 `APIResponseValidationError`。 |
| 190 | + |
| 191 | +默认连接超时 10 秒,其余 HTTP 阶段 60 秒。可传浮点秒数、`httpx.Timeout`、`None`;超时按 HTTP 阶段和单次尝试计算。端到端任务期限由调用方管理,异步代码可用 `asyncio.wait_for`。 |
| 192 | + |
| 193 | +默认最多重试 2 次:GET/HEAD 或携带 `Idempotency-Key` 的请求可对连接错误、408、429、5xx 重试;无幂等键的其他请求仅对 429 重试;409 不自动重试。在上述约束内遵循 `x-should-retry` 和有效的 `Retry-After-Ms` / `Retry-After`,否则指数退避。已建立的 SSE 不重试。 |
| 194 | + |
| 195 | +`client.with_options(max_retries=0, timeout=20)` 返回独立配置的客户端,复用同一个 HTTP 连接池;关闭任一客户端会关闭这个池。 |
| 196 | + |
| 197 | +## 文件和自定义 HTTP |
| 198 | + |
| 199 | +```python |
| 200 | +from pathlib import Path |
| 201 | + |
| 202 | +file = client.files.upload(file=Path("report.txt")) |
| 203 | +skill = client.skills.create(files=[("example/SKILL.md", b"---\nname: example\n---\nExample skill")]) |
| 204 | +with client.files.download(file.id) as content: |
| 205 | + content.write_to_file("downloaded.txt") |
| 206 | +``` |
| 207 | + |
| 208 | +上传支持 bytes、二进制文件对象、Path、`(文件名, 内容[, MIME 类型])`。调用方传入的文件对象由调用方关闭;上传内容会缓存以便重试。metadata 使用 JSON 编码,Skill 相对路径保留在 multipart 文件名中。 |
| 209 | + |
| 210 | +Files 下载先获取临时链接,再流式读取存储地址;API 认证、默认请求头、Cookie 不会发送到存储主机。Skill Version 下载直接返回 API 的二进制响应。异步下载使用 `await client.files.download(...)`、`await response.write_to_file(...)`。 |
| 211 | + |
| 212 | +```python |
| 213 | +import httpx |
| 214 | +from qca import Forward |
| 215 | + |
| 216 | +with Forward(http_client=httpx.Client(proxy="http://localhost:8080")) as client: |
| 217 | + raw = client.models.with_raw_response.list() |
| 218 | + print(raw.status_code, raw.headers) |
| 219 | + models = raw.parse() |
| 220 | +``` |
| 221 | + |
| 222 | +异步客户端可传 `httpx.AsyncClient`,异步 raw response 使用 `await raw.parse()`。动态令牌提供者传到 `credential=`,每次 HTTP 尝试调用 `get_token()`;异步客户端也接受异步 `get_token()`。静态 access_token 优先于提供者,显式 Authorization 请求头优先于两者。 |
| 223 | + |
| 224 | +需要先查看响应头再读取正文时使用 `with_streaming_response`。退出上下文时关闭连接: |
| 225 | + |
| 226 | +```python |
| 227 | +with client.models.with_streaming_response.list() as response: |
| 228 | + print(response.headers) |
| 229 | + models = response.parse() |
| 230 | +``` |
| 231 | + |
| 232 | +异步版本使用 `async with client.models.with_streaming_response.list()`,通过 `await response.parse()` 解析正文;也可以按块迭代 `iter_bytes()` / `iter_lines()`。 |
| 233 | + |
| 234 | +## 目录 |
| 235 | + |
| 236 | +```text |
| 237 | +src/qca/ |
| 238 | + __init__.py |
| 239 | + common/ # HTTP、鉴权、错误、分页、上传/下载、SSE |
| 240 | + forward/ |
| 241 | + _client.py |
| 242 | + resources/ # identities/configs、sessions/events 等 |
| 243 | + types/ # 请求 TypedDict、响应模型 |
| 244 | + managed/ |
| 245 | + _client.py |
| 246 | + resources/ # agents、deployments、environments/work 等 |
| 247 | + types/ |
| 248 | +tests/ # 资源面、文档契约、公共层与模拟执行场景 |
| 249 | +examples/ # 每个场景一个文件;各 mode 含 6 个同步场景、异步片段及 live 测试 |
| 250 | +docs/ # Forward / Managed 完整 API 参考 |
| 251 | +``` |
0 commit comments