zhuye 0.1.1__tar.gz
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.
- zhuye-0.1.1/PKG-INFO +99 -0
- zhuye-0.1.1/README.md +85 -0
- zhuye-0.1.1/pyproject.toml +32 -0
- zhuye-0.1.1/setup.cfg +4 -0
- zhuye-0.1.1/tests/test_protocol.py +305 -0
- zhuye-0.1.1/zhuye/__init__.py +53 -0
- zhuye-0.1.1/zhuye/agent.py +523 -0
- zhuye-0.1.1/zhuye/protocol.py +675 -0
- zhuye-0.1.1/zhuye/py.typed +0 -0
- zhuye-0.1.1/zhuye.egg-info/PKG-INFO +99 -0
- zhuye-0.1.1/zhuye.egg-info/SOURCES.txt +12 -0
- zhuye-0.1.1/zhuye.egg-info/dependency_links.txt +1 -0
- zhuye-0.1.1/zhuye.egg-info/requires.txt +3 -0
- zhuye-0.1.1/zhuye.egg-info/top_level.txt +1 -0
zhuye-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: zhuye
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: ZhuYe Agent SDK for Python: spawn `zhuye serve` and drive it over JSONL stdio.
|
|
5
|
+
Author: ZhuYe Contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Asketisch/ZhuYe
|
|
8
|
+
Project-URL: Repository, https://github.com/Asketisch/ZhuYe
|
|
9
|
+
Project-URL: Issues, https://github.com/Asketisch/ZhuYe/issues
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
14
|
+
|
|
15
|
+
# zhuye (Python SDK)
|
|
16
|
+
|
|
17
|
+
让 Python 项目直接驱动 Zhuye Agent 的轻量 SDK。底层通过 spawn `zhuye serve` 子进程、走 JSONL stdio 协议,**零运行时依赖**(仅 stdlib)。
|
|
18
|
+
|
|
19
|
+
## 安装
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install zhuye
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
需要本机有 `zhuye` 二进制在 PATH(或通过 `ZHUYE_BIN` 指向)。从 [GitHub Releases](https://github.com/Asketisch/ZhuYe/releases) 下载,或从本仓库 `make build` 产出 `target/release/zhuye`。
|
|
26
|
+
|
|
27
|
+
## 用法
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from zhuye import ZhuyeAgent, ToolOutput, ContentBlock
|
|
31
|
+
|
|
32
|
+
agent = ZhuyeAgent.spawn()
|
|
33
|
+
|
|
34
|
+
# 注册客户端自定义工具(LLM 调用时由本地函数返回结果)
|
|
35
|
+
def get_weather(args):
|
|
36
|
+
return ToolOutput(
|
|
37
|
+
content=[ContentBlock(type="text", text=f"{args['city']} 今天晴 26℃")],
|
|
38
|
+
is_error=False,
|
|
39
|
+
metadata={},
|
|
40
|
+
elapsed_ms=0,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
agent.register_tool(
|
|
44
|
+
name="get_weather",
|
|
45
|
+
description="查询城市天气",
|
|
46
|
+
parameters={
|
|
47
|
+
"type": "object",
|
|
48
|
+
"properties": {"city": {"type": "string"}},
|
|
49
|
+
"required": ["city"],
|
|
50
|
+
},
|
|
51
|
+
handler=get_weather,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# 高层:聚合 delta,返回最终文本
|
|
55
|
+
print(agent.prompt("北京天气如何?"))
|
|
56
|
+
|
|
57
|
+
# 低层:遍历 turn 事件
|
|
58
|
+
for ev in agent.submit("第二轮"):
|
|
59
|
+
msg = ev["msg"]
|
|
60
|
+
if msg["type"] == "agent_message_delta":
|
|
61
|
+
print(msg["delta"], end="", flush=True)
|
|
62
|
+
if msg["type"] == "turn_complete":
|
|
63
|
+
break
|
|
64
|
+
|
|
65
|
+
agent.close()
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## 全局事件
|
|
69
|
+
|
|
70
|
+
`next_global_event(timeout_secs=5)` 读取 `id=""` 的会话事件,与 `submit()` 的
|
|
71
|
+
按 submission 路由的事件迭代器独立。超时抛 `queue.Empty`,进程 stdout
|
|
72
|
+
关闭后返回 `None`;未知变体原样返回。
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
handshake = agent.next_global_event(timeout_secs=5)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## 环境变量
|
|
79
|
+
|
|
80
|
+
| 变量 | 作用 |
|
|
81
|
+
|------|------|
|
|
82
|
+
| `ZHUYE_BIN` | `zhuye` 二进制路径(默认搜索 PATH)。 |
|
|
83
|
+
| `ZHUYE_MODEL` | 模型 spec(`mock/mock-1` 用于离线 SDK 集成测试)。 |
|
|
84
|
+
| `ZHUYE_REMOTE_TOOL_TIMEOUT_SECS` | 远程工具等待客户端回执的上限,默认 120s。 |
|
|
85
|
+
|
|
86
|
+
## 测试
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
pip install -e .[dev]
|
|
90
|
+
pytest -ra
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
mock serve 进程跑在 Node 上(Node ≥ 18),不依赖真 `zhuye` 二进制 —— 测试完全离线,也不需要网络/API key。
|
|
94
|
+
|
|
95
|
+
端到端测试(对真 `zhuye serve` 子进程的集成)在仓库 CI 里跑;在仓库根配置好 `ZHUYE_BIN` 后执行 `./tests/run_all.sh sdk-python`。
|
|
96
|
+
|
|
97
|
+
## 协议
|
|
98
|
+
|
|
99
|
+
SDK 与 `zhuye serve` 之间的 wire 协议见 `../PROTOCOL.md`(同仓库 `sdks/PROTOCOL.md`)。Rust 核心新增协议变体时,SDK 自动降为 `UnknownEventMsg`,不会中断下游。
|
zhuye-0.1.1/README.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# zhuye (Python SDK)
|
|
2
|
+
|
|
3
|
+
让 Python 项目直接驱动 Zhuye Agent 的轻量 SDK。底层通过 spawn `zhuye serve` 子进程、走 JSONL stdio 协议,**零运行时依赖**(仅 stdlib)。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install zhuye
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
需要本机有 `zhuye` 二进制在 PATH(或通过 `ZHUYE_BIN` 指向)。从 [GitHub Releases](https://github.com/Asketisch/ZhuYe/releases) 下载,或从本仓库 `make build` 产出 `target/release/zhuye`。
|
|
12
|
+
|
|
13
|
+
## 用法
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from zhuye import ZhuyeAgent, ToolOutput, ContentBlock
|
|
17
|
+
|
|
18
|
+
agent = ZhuyeAgent.spawn()
|
|
19
|
+
|
|
20
|
+
# 注册客户端自定义工具(LLM 调用时由本地函数返回结果)
|
|
21
|
+
def get_weather(args):
|
|
22
|
+
return ToolOutput(
|
|
23
|
+
content=[ContentBlock(type="text", text=f"{args['city']} 今天晴 26℃")],
|
|
24
|
+
is_error=False,
|
|
25
|
+
metadata={},
|
|
26
|
+
elapsed_ms=0,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
agent.register_tool(
|
|
30
|
+
name="get_weather",
|
|
31
|
+
description="查询城市天气",
|
|
32
|
+
parameters={
|
|
33
|
+
"type": "object",
|
|
34
|
+
"properties": {"city": {"type": "string"}},
|
|
35
|
+
"required": ["city"],
|
|
36
|
+
},
|
|
37
|
+
handler=get_weather,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# 高层:聚合 delta,返回最终文本
|
|
41
|
+
print(agent.prompt("北京天气如何?"))
|
|
42
|
+
|
|
43
|
+
# 低层:遍历 turn 事件
|
|
44
|
+
for ev in agent.submit("第二轮"):
|
|
45
|
+
msg = ev["msg"]
|
|
46
|
+
if msg["type"] == "agent_message_delta":
|
|
47
|
+
print(msg["delta"], end="", flush=True)
|
|
48
|
+
if msg["type"] == "turn_complete":
|
|
49
|
+
break
|
|
50
|
+
|
|
51
|
+
agent.close()
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## 全局事件
|
|
55
|
+
|
|
56
|
+
`next_global_event(timeout_secs=5)` 读取 `id=""` 的会话事件,与 `submit()` 的
|
|
57
|
+
按 submission 路由的事件迭代器独立。超时抛 `queue.Empty`,进程 stdout
|
|
58
|
+
关闭后返回 `None`;未知变体原样返回。
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
handshake = agent.next_global_event(timeout_secs=5)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## 环境变量
|
|
65
|
+
|
|
66
|
+
| 变量 | 作用 |
|
|
67
|
+
|------|------|
|
|
68
|
+
| `ZHUYE_BIN` | `zhuye` 二进制路径(默认搜索 PATH)。 |
|
|
69
|
+
| `ZHUYE_MODEL` | 模型 spec(`mock/mock-1` 用于离线 SDK 集成测试)。 |
|
|
70
|
+
| `ZHUYE_REMOTE_TOOL_TIMEOUT_SECS` | 远程工具等待客户端回执的上限,默认 120s。 |
|
|
71
|
+
|
|
72
|
+
## 测试
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
pip install -e .[dev]
|
|
76
|
+
pytest -ra
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
mock serve 进程跑在 Node 上(Node ≥ 18),不依赖真 `zhuye` 二进制 —— 测试完全离线,也不需要网络/API key。
|
|
80
|
+
|
|
81
|
+
端到端测试(对真 `zhuye serve` 子进程的集成)在仓库 CI 里跑;在仓库根配置好 `ZHUYE_BIN` 后执行 `./tests/run_all.sh sdk-python`。
|
|
82
|
+
|
|
83
|
+
## 协议
|
|
84
|
+
|
|
85
|
+
SDK 与 `zhuye serve` 之间的 wire 协议见 `../PROTOCOL.md`(同仓库 `sdks/PROTOCOL.md`)。Rust 核心新增协议变体时,SDK 自动降为 `UnknownEventMsg`,不会中断下游。
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "zhuye"
|
|
3
|
+
version = "0.1.1"
|
|
4
|
+
description = "ZhuYe Agent SDK for Python: spawn `zhuye serve` and drive it over JSONL stdio."
|
|
5
|
+
requires-python = ">=3.10"
|
|
6
|
+
license = { text = "MIT" }
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
authors = [{ name = "ZhuYe Contributors" }]
|
|
9
|
+
# 无运行时依赖:仅 stdlib(os / subprocess / threading / json / dataclasses)。
|
|
10
|
+
dependencies = []
|
|
11
|
+
|
|
12
|
+
[project.urls]
|
|
13
|
+
Homepage = "https://github.com/Asketisch/ZhuYe"
|
|
14
|
+
Repository = "https://github.com/Asketisch/ZhuYe"
|
|
15
|
+
Issues = "https://github.com/Asketisch/ZhuYe/issues"
|
|
16
|
+
|
|
17
|
+
[project.optional-dependencies]
|
|
18
|
+
dev = ["pytest>=7.0"]
|
|
19
|
+
|
|
20
|
+
[build-system]
|
|
21
|
+
requires = ["setuptools>=68"]
|
|
22
|
+
build-backend = "setuptools.build_meta"
|
|
23
|
+
|
|
24
|
+
[tool.setuptools.packages.find]
|
|
25
|
+
include = ["zhuye*"]
|
|
26
|
+
|
|
27
|
+
[tool.setuptools.package-data]
|
|
28
|
+
zhuye = ["py.typed"]
|
|
29
|
+
|
|
30
|
+
[tool.pytest.ini_options]
|
|
31
|
+
testpaths = ["tests"]
|
|
32
|
+
addopts = "-ra"
|
zhuye-0.1.1/setup.cfg
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"""zhuye Python SDK 测试:用本地 Node "mock serve" 驱动协议。
|
|
2
|
+
|
|
3
|
+
不依赖真 `zhuye` 二进制 —— mock 子进程用 Node 简单回显 JSONL,
|
|
4
|
+
覆盖 SDK 的协议回路(handshake / submit / prompt / register_tool /
|
|
5
|
+
shutdown)。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import queue
|
|
12
|
+
import shutil
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
import time
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
import pytest
|
|
19
|
+
|
|
20
|
+
from zhuye import ZhuyeAgent, ToolOutput, ContentBlock
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
MOCK_SERVE_JS = r"""
|
|
24
|
+
// 必须用 console.log (sync flush); process.stdout.write 是 async flush,
|
|
25
|
+
// 在 Node 上会让 Python readline() 在 pipe EOF 之前拿不到任何数据。
|
|
26
|
+
console.log(JSON.stringify({
|
|
27
|
+
id: '',
|
|
28
|
+
msg: {
|
|
29
|
+
type: 'session_configured',
|
|
30
|
+
session_id: 'mock-session',
|
|
31
|
+
model: 'mock/mock-1',
|
|
32
|
+
provider: 'mock',
|
|
33
|
+
approval_policy: 'auto',
|
|
34
|
+
sandbox_policy: 'workspace_only',
|
|
35
|
+
},
|
|
36
|
+
}));
|
|
37
|
+
const readline = require('node:readline');
|
|
38
|
+
const rl = readline.createInterface({ input: process.stdin });
|
|
39
|
+
// 未收到 shutdown 就关闭 stdin 属于异常退出,用于验证客户端关闭流程。
|
|
40
|
+
rl.on('close', () => process.exit(23));
|
|
41
|
+
rl.on('line', (line) => {
|
|
42
|
+
let sub;
|
|
43
|
+
try { sub = JSON.parse(line); } catch { return; }
|
|
44
|
+
const opType = sub.op && sub.op.type;
|
|
45
|
+
if (opType === 'shutdown') {
|
|
46
|
+
console.log(JSON.stringify({ id: sub.id, msg: { type: 'shutdown_complete' } }));
|
|
47
|
+
process.exit(0);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (opType === 'register_tools') {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
// 测试注入:让 mock 以指定 submission id emit 一条事件(模拟
|
|
54
|
+
// core 下发的 tool_execution_request 等),用于验证 SDK 侧路由与回执。
|
|
55
|
+
if (opType === 'inject_event') {
|
|
56
|
+
console.log(JSON.stringify({ id: sub.id, msg: sub.op.event }));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
// 测试回执:把客户端写回的 ToolOutput 以全局事件(id='')回显,
|
|
60
|
+
// 供测试断言 SDK 确实调用了本地 handler 并写入了正确回执。
|
|
61
|
+
// 注:回执 submission 的 id 是 SDK 新生成的 uuid(无监听者),
|
|
62
|
+
// 故 echo 走全局事件通道。
|
|
63
|
+
if (opType === 'tool_execution_response') {
|
|
64
|
+
console.log(JSON.stringify({
|
|
65
|
+
id: '',
|
|
66
|
+
msg: {
|
|
67
|
+
type: 'tool_echo',
|
|
68
|
+
call_id: sub.op.call_id,
|
|
69
|
+
output: sub.op.output,
|
|
70
|
+
},
|
|
71
|
+
}));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (opType === 'user_input' || opType === 'interrupt') {
|
|
75
|
+
console.log(JSON.stringify({
|
|
76
|
+
id: sub.id, msg: { type: 'turn_started', turn_id: 't', user_message_id: 'm' },
|
|
77
|
+
}));
|
|
78
|
+
console.log(JSON.stringify({
|
|
79
|
+
id: sub.id, msg: { type: 'agent_message_delta', delta: 'mock-reply' },
|
|
80
|
+
}));
|
|
81
|
+
// v1.5 additive 变体:工具参数增量(SDK 应原样透传给客户端)。
|
|
82
|
+
console.log(JSON.stringify({
|
|
83
|
+
id: sub.id, msg: {
|
|
84
|
+
type: 'tool_call_args_delta', call_id: 'c1', delta: '{"text":"pi',
|
|
85
|
+
},
|
|
86
|
+
}));
|
|
87
|
+
console.log(JSON.stringify({
|
|
88
|
+
id: sub.id, msg: { type: 'tool_call_output_delta', call_id: 'c1', delta: 'preview', is_stderr: true },
|
|
89
|
+
}));
|
|
90
|
+
// 未知变体:SDK 不得因不认识而抛错(按未知 type 降级)。
|
|
91
|
+
console.log(JSON.stringify({
|
|
92
|
+
id: sub.id, msg: { type: 'some_future_event', payload: { x: 1 } },
|
|
93
|
+
}));
|
|
94
|
+
console.log(JSON.stringify({
|
|
95
|
+
id: sub.id, msg: {
|
|
96
|
+
type: 'turn_complete', turn_id: 't', status: 'success',
|
|
97
|
+
usage: { input_tokens: 0, output_tokens: 1 },
|
|
98
|
+
},
|
|
99
|
+
}));
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _spawn_mock() -> subprocess.Popen:
|
|
106
|
+
node = shutil.which("node")
|
|
107
|
+
if not node:
|
|
108
|
+
pytest.skip("node not available for mock serve")
|
|
109
|
+
return subprocess.Popen(
|
|
110
|
+
[node, "-e", MOCK_SERVE_JS],
|
|
111
|
+
stdin=subprocess.PIPE,
|
|
112
|
+
stdout=subprocess.PIPE,
|
|
113
|
+
stderr=subprocess.PIPE,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@pytest.fixture
|
|
118
|
+
def agent() -> ZhuyeAgent:
|
|
119
|
+
proc = _spawn_mock()
|
|
120
|
+
try:
|
|
121
|
+
ag = ZhuyeAgent.from_popen(proc, handshake_timeout_secs=30.0)
|
|
122
|
+
except Exception:
|
|
123
|
+
proc.kill()
|
|
124
|
+
raise
|
|
125
|
+
yield ag
|
|
126
|
+
if not ag._closed:
|
|
127
|
+
ag.close()
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_prompt_aggregates_deltas(agent: ZhuyeAgent) -> None:
|
|
131
|
+
assert agent.prompt("hi") == "mock-reply"
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_register_tool_then_submit_iterates_turn_events(agent: ZhuyeAgent) -> None:
|
|
135
|
+
calls = []
|
|
136
|
+
|
|
137
|
+
def handler(args):
|
|
138
|
+
calls.append(args)
|
|
139
|
+
return ToolOutput(
|
|
140
|
+
content=[ContentBlock(type="text", text="x")],
|
|
141
|
+
is_error=False,
|
|
142
|
+
metadata={},
|
|
143
|
+
elapsed_ms=0,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
agent.register_tool("foo", "foo tool", {"type": "object"}, handler)
|
|
147
|
+
events = list(agent.submit("test"))
|
|
148
|
+
types = [ev["msg"]["type"] for ev in events]
|
|
149
|
+
assert "turn_started" in types
|
|
150
|
+
assert "agent_message_delta" in types
|
|
151
|
+
assert types[-1] == "turn_complete"
|
|
152
|
+
# register_tools 被静默接受(无回执事件)。
|
|
153
|
+
assert calls == []
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def test_close_sends_shutdown(agent: ZhuyeAgent) -> None:
|
|
157
|
+
# mock 在收到 shutdown 时退出 0,只收到 EOF 时退出 23。
|
|
158
|
+
agent.close()
|
|
159
|
+
agent.close()
|
|
160
|
+
assert agent._closed is True
|
|
161
|
+
assert agent._proc.returncode == 0
|
|
162
|
+
with pytest.raises(RuntimeError, match="agent is closed"):
|
|
163
|
+
agent.submit("closed")
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def test_tool_execution_response_handled(agent: ZhuyeAgent) -> None:
|
|
167
|
+
"""喂一个全局(id="")的 tool_execution_request:SDK 应在 reader 线程外
|
|
168
|
+
调本地 handler 并写回正确回执(与真实 serve 的 wire 行为一致,
|
|
169
|
+
见 PROTOCOL.md §5)。mock 把回执以全局 `tool_echo` 事件回显供断言。
|
|
170
|
+
"""
|
|
171
|
+
received = {}
|
|
172
|
+
|
|
173
|
+
def handler(args):
|
|
174
|
+
received.update(args)
|
|
175
|
+
return ToolOutput(
|
|
176
|
+
content=[ContentBlock(type="text", text=f"echo:{args.get('q')}")],
|
|
177
|
+
is_error=False,
|
|
178
|
+
metadata={},
|
|
179
|
+
elapsed_ms=0,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
agent.register_tool("echo", "echo back", {"type": "object"}, handler)
|
|
183
|
+
# 注入 id="" 的全局事件(真实 serve 的远程工具请求即此形态)。
|
|
184
|
+
agent.submit_op(
|
|
185
|
+
{
|
|
186
|
+
"type": "inject_event",
|
|
187
|
+
"event": {
|
|
188
|
+
"type": "tool_execution_request",
|
|
189
|
+
"call_id": "c1",
|
|
190
|
+
"tool": "echo",
|
|
191
|
+
"args": {"q": "hi"},
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
submission_id="sub-inject",
|
|
195
|
+
)
|
|
196
|
+
# 等 mock 回显的 tool_echo 入全局队列。
|
|
197
|
+
deadline = time.monotonic() + 5.0
|
|
198
|
+
echo: dict[str, Any] | None = None
|
|
199
|
+
while time.monotonic() < deadline:
|
|
200
|
+
try:
|
|
201
|
+
ev = agent.next_global_event(timeout_secs=0.1)
|
|
202
|
+
except queue.Empty:
|
|
203
|
+
continue
|
|
204
|
+
if ev is not None and ev.get("msg", {}).get("type") == "tool_echo":
|
|
205
|
+
echo = ev
|
|
206
|
+
break
|
|
207
|
+
assert echo is not None, "5s 内未收到 mock 的 tool_echo 回显"
|
|
208
|
+
assert echo["msg"]["call_id"] == "c1"
|
|
209
|
+
assert echo["msg"]["output"]["content"][0]["text"] == "echo:hi"
|
|
210
|
+
assert echo["msg"]["output"]["is_error"] is False
|
|
211
|
+
assert received == {"q": "hi"}
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def test_tool_call_args_delta_透传到事件流(agent: ZhuyeAgent) -> None:
|
|
215
|
+
"""v1.5:additive 变体 `tool_call_args_delta` 应原样出现在 turn 事件流里。
|
|
216
|
+
|
|
217
|
+
协议变更是 additive 也必须同步 SDK —— 客户端要能拿到工具参数增量做
|
|
218
|
+
渐进渲染;字段名保持 snake_case,与 Rust serde 一致。
|
|
219
|
+
"""
|
|
220
|
+
events = list(agent.submit("hi"))
|
|
221
|
+
types = [ev["msg"]["type"] for ev in events]
|
|
222
|
+
assert "tool_call_args_delta" in types, f"未见 tool_call_args_delta:{types}"
|
|
223
|
+
d = next(ev["msg"] for ev in events if ev["msg"]["type"] == "tool_call_args_delta")
|
|
224
|
+
assert d["call_id"] == "c1", f"call_id 不对:{d}"
|
|
225
|
+
assert d["delta"] == '{"text":"pi', f"delta 不对:{d}"
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def test_未知事件变体降级不崩(agent: ZhuyeAgent) -> None:
|
|
229
|
+
"""协议新增变体时,SDK 不得因不认识而抛错(按未知 type 降级)。"""
|
|
230
|
+
events = list(agent.submit("hi"))
|
|
231
|
+
types = [ev["msg"]["type"] for ev in events]
|
|
232
|
+
assert "some_future_event" in types, f"未知变体应被原样透传:{types}"
|
|
233
|
+
assert types[-1] == "turn_complete", f"未知变体不应打断收尾:{types}"
|
|
234
|
+
# agent 仍可用。
|
|
235
|
+
assert agent.prompt("hi") == "mock-reply"
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def test_wire_field_names_stable() -> None:
|
|
239
|
+
"""核心 wire 字段名 / 判别符保持 snake_case,与 Rust serde 一致。"""
|
|
240
|
+
sub = {"id": "x", "op": {"type": "user_input", "items": [{"type": "text", "text": "hi"}]}}
|
|
241
|
+
enc = json.dumps(sub)
|
|
242
|
+
assert "user_input" in enc
|
|
243
|
+
assert "UserInput" not in enc
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def test_tool_output_wire_format() -> None:
|
|
247
|
+
out = ToolOutput(
|
|
248
|
+
content=[ContentBlock(type="text", text="hi")],
|
|
249
|
+
is_error=False,
|
|
250
|
+
metadata={},
|
|
251
|
+
elapsed_ms=0,
|
|
252
|
+
)
|
|
253
|
+
enc = json.dumps(out)
|
|
254
|
+
assert "is_error" in enc
|
|
255
|
+
assert "elapsed_ms" in enc
|
|
256
|
+
assert "isError" not in enc
|
|
257
|
+
assert "elapsedMs" not in enc
|
|
258
|
+
|
|
259
|
+
def test_optional_fields_preserve_required_keys() -> None:
|
|
260
|
+
"""零依赖类型声明保留必填与可选字段,不依赖 Python 3.11 的 NotRequired。"""
|
|
261
|
+
from zhuye import protocol
|
|
262
|
+
|
|
263
|
+
assert protocol.UserInputOp.__required_keys__ == {"type", "items"}
|
|
264
|
+
assert protocol.UserInputOp.__optional_keys__ == {"thread_settings"}
|
|
265
|
+
assert protocol.Submission.__required_keys__ == {"id", "op"}
|
|
266
|
+
assert protocol.Submission.__optional_keys__ == {"client_user_message_id"}
|
|
267
|
+
assert protocol.ThreadSettings.__required_keys__ == set()
|
|
268
|
+
assert protocol.ThreadSettings.__optional_keys__ == {"effort", "permission_mode", "model"}
|
|
269
|
+
assert protocol.ErrorEvent.__required_keys__ == {"type", "code", "message"}
|
|
270
|
+
assert protocol.ErrorEvent.__optional_keys__ == {"details"}
|
|
271
|
+
# v1.3 收尾标记:类型层与 TS SDK 的 SubmissionClosed 对齐(此前仅在
|
|
272
|
+
# agent._is_terminal 做字符串判定,联合类型漏了该变体)。
|
|
273
|
+
assert protocol.SubmissionClosed.__required_keys__ == {"type"}
|
|
274
|
+
assert protocol.SubmissionClosed.__optional_keys__ == set()
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def test_global_events_and_eof(agent: ZhuyeAgent) -> None:
|
|
278
|
+
"""全局事件有公开读取口,EOF 后重复读取也不会挂起。"""
|
|
279
|
+
event = agent.next_global_event(timeout_secs=1)
|
|
280
|
+
assert event["msg"]["type"] == "session_configured"
|
|
281
|
+
with pytest.raises(queue.Empty):
|
|
282
|
+
agent.next_global_event(timeout_secs=0.01)
|
|
283
|
+
agent.close()
|
|
284
|
+
assert agent.next_global_event(timeout_secs=1) is None
|
|
285
|
+
assert agent.next_global_event(timeout_secs=0.01) is None
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def test_tool_output_delta_forwarded(agent: ZhuyeAgent) -> None:
|
|
289
|
+
"""输出增量经过 SDK 路由后保持字段与顺序。"""
|
|
290
|
+
messages = [e["msg"] for e in agent.submit("hi")]
|
|
291
|
+
delta = next(m for m in messages if m["type"] == "tool_call_output_delta")
|
|
292
|
+
assert delta == {"type": "tool_call_output_delta", "call_id": "c1", "delta": "preview", "is_stderr": True}
|
|
293
|
+
assert messages[-1]["type"] == "turn_complete"
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
@pytest.mark.skipif(sys.version_info < (3, 10), reason="联合类型求值需要受支持的 Python 版本")
|
|
297
|
+
def test_wire_type_annotations() -> None:
|
|
298
|
+
"""公开注解必须描述 Rust 实际发送与接收的 wire 值。"""
|
|
299
|
+
from typing import get_args, get_type_hints
|
|
300
|
+
from zhuye import protocol
|
|
301
|
+
|
|
302
|
+
assert set(get_args(get_type_hints(protocol.TurnComplete)["status"])) == {"success", "max_iterations", "stopped", "token_budget_exceeded"}
|
|
303
|
+
assert set(get_args(get_type_hints(protocol.PlanApprovalOp)["choice"])) == {"auto_mode", "manual_approve", "revise"}
|
|
304
|
+
assert get_type_hints(protocol.TurnAborted)["reason"] == protocol.AbortReason
|
|
305
|
+
assert protocol.ReviewDecisionDeny.__required_keys__ == {"deny"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""zhuye Python SDK —— 让脚本项目能直接驱动 ZhuYe Agent。
|
|
2
|
+
|
|
3
|
+
底层通过 spawn `zhuye serve` 子进程、走 JSONL stdio 协议,**零原生
|
|
4
|
+
依赖、零 Python 运行时第三方包**(仅 stdlib)。需要本机 `zhuye` 二
|
|
5
|
+
进制在 PATH(或通过 `ZHUYE_BIN` 指向)。
|
|
6
|
+
|
|
7
|
+
最小示例::
|
|
8
|
+
|
|
9
|
+
from zhuye import ZhuyeAgent
|
|
10
|
+
|
|
11
|
+
agent = ZhuyeAgent.spawn()
|
|
12
|
+
text = agent.prompt("hello")
|
|
13
|
+
print(text)
|
|
14
|
+
agent.close()
|
|
15
|
+
|
|
16
|
+
更复杂的场景(自定义工具、多轮、错误处理)见 README.md。
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from .agent import (
|
|
22
|
+
ContentBlock,
|
|
23
|
+
Event,
|
|
24
|
+
ZhuyeAgent,
|
|
25
|
+
ZhuyeAgentOptions,
|
|
26
|
+
ToolHandler,
|
|
27
|
+
ToolOutput,
|
|
28
|
+
)
|
|
29
|
+
from .protocol import (
|
|
30
|
+
EVENT_ID_NONE,
|
|
31
|
+
EventMsg,
|
|
32
|
+
Op,
|
|
33
|
+
RemoteToolSpec,
|
|
34
|
+
Submission,
|
|
35
|
+
UnknownEventMsg,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
"ZhuyeAgent",
|
|
40
|
+
"ZhuyeAgentOptions",
|
|
41
|
+
"ToolHandler",
|
|
42
|
+
"ToolOutput",
|
|
43
|
+
"ContentBlock",
|
|
44
|
+
"Op",
|
|
45
|
+
"Submission",
|
|
46
|
+
"Event",
|
|
47
|
+
"EventMsg",
|
|
48
|
+
"UnknownEventMsg",
|
|
49
|
+
"RemoteToolSpec",
|
|
50
|
+
"EVENT_ID_NONE",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
__version__ = "0.1.1"
|