aaas 0.2.0__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.
aaas-0.2.0/PKG-INFO ADDED
@@ -0,0 +1,171 @@
1
+ Metadata-Version: 2.4
2
+ Name: aaas
3
+ Version: 0.2.0
4
+ Summary: Python SDK for the Agent as a Service protocol — declare an Agent on one connection, keep every function body in your own process.
5
+ Project-URL: Homepage, https://github.com/YY-Yifei/aaas
6
+ Project-URL: Repository, https://github.com/YY-Yifei/aaas
7
+ Project-URL: Specification, https://github.com/YY-Yifei/aaas/blob/main/PROTOCOL.md
8
+ Keywords: aaas,agent,function-calling,json-rpc,websocket,llm,protocol
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Classifier: Typing :: Typed
18
+ Requires-Python: >=3.10
19
+ Description-Content-Type: text/markdown
20
+ Requires-Dist: websockets>=12.0
21
+ Provides-Extra: test
22
+ Requires-Dist: pytest>=7.0; extra == "test"
23
+ Requires-Dist: pytest-asyncio>=0.23; extra == "test"
24
+
25
+ # AaaS — Agent as a Service
26
+
27
+ **调用方在一条连接之内声明一个 Agent —— 它的职责,和它的能力(每个能力带完整参数 schema)——
28
+ 服务端用模型驱动这个 Agent,而每个能力的执行体始终留在调用方进程内。**
29
+
30
+ 「服务」这个词是字面的:调用方不部署任何东西、不注册任何回调地址、不持有任何后端,
31
+ 只要有一条能连出去的 WebSocket,就能得到一个**此刻才存在、由它自己定义的** Agent。
32
+
33
+ 这个仓库有两样东西,可以分开用:
34
+
35
+ | | 是什么 | 给谁 |
36
+ |---|---|---|
37
+ | **[`PROTOCOL.md`](https://github.com/YY-Yifei/aaas/blob/main/PROTOCOL.md)** | 协议规范:7 个方法、9 种事件、17 个错误码、15 条一致性断言 | 要实现这个协议的人 |
38
+ | **[`src/aaas/`](https://github.com/YY-Yifei/aaas/tree/main/src/aaas)** | Python SDK(`pip install aaas`):调用方与服务端两侧的参考实现 | 要**用**这个协议的人 |
39
+
40
+ 规范是源头,SDK 是它的实现。两者冲突时以规范为准,并视为缺陷。
41
+
42
+ ---
43
+
44
+ ## 安装
45
+
46
+ ```bash
47
+ pip install aaas
48
+ ```
49
+
50
+ 唯一硬依赖是 `websockets`。Python ≥ 3.10。
51
+
52
+ ## 10 分钟:一轮含反向 function calling
53
+
54
+ 调用方侧 —— 你声明 Agent,并在**自己的进程里**执行能力:
55
+
56
+ ```python
57
+ import asyncio, aaas
58
+
59
+ @aaas.function # 参数 schema 从签名推导,不用手写
60
+ async def search_local_index(query: str, page_size: int = 10) -> str:
61
+ """在调用方的本地持仓索引里检索
62
+
63
+ Args:
64
+ query: 检索词
65
+ page_size: 每页条数
66
+ """
67
+ return f"找到 3 条匹配「{query}」的记录"
68
+
69
+ async def main():
70
+ async with aaas.Caller("wss://api.example.com/aaas", token="...") as caller:
71
+ await caller.declare(aaas.AgentDefinition(
72
+ revision="v1",
73
+ instructions="你正在协助用户操作债券计算器。金额单位是万元。",
74
+ functions=(search_local_index.declaration,),
75
+ ), tools=[search_local_index])
76
+
77
+ turn = await caller.send("帮我查一下持仓")
78
+ async for event in turn: # 一轮不是一次请求-响应,是可迭代的事件流
79
+ if event.kind == "message.delta":
80
+ print(event.text, end="")
81
+
82
+ final = await turn.end()
83
+ assert final.status == "completed"
84
+
85
+ asyncio.run(main())
86
+ ```
87
+
88
+ 服务端侧 —— 你只需要写「用模型驱动这一轮」,协议层由 SDK 负责:
89
+
90
+ ```python
91
+ import aaas
92
+
93
+ async def my_agent(ctx: aaas.TurnContext) -> None:
94
+ ctx.reasoning("用户问本地索引,先调工具") # 未协商 reasoning 时静默不发
95
+ result = await ctx.call("search_local_index", {"query": ctx.text})
96
+ ctx.reply(*(p.text for p in (result.parts or ()) if p.text))
97
+ ctx.usage_of(output_tokens=42)
98
+
99
+ await aaas.Server(my_agent).serve("127.0.0.1", 8765)
100
+ ```
101
+
102
+ `seq` 怎么排、调用怎么超时、取消时 `cancelCall` 与终态谁先、断连时怎么收尾 ——
103
+ 都由 SDK 负责。它替你守的是 §5.3 那四条和 §5.8 那七条,那几处正是这类协议最容易写错的地方。
104
+
105
+ 看不装也能跑的效果:
106
+
107
+ ```bash
108
+ python -m aaas # 起一个本地服务端 + 一个调用方,跑完一轮含反向 function calling
109
+ ```
110
+
111
+ ## SDK 覆盖了什么
112
+
113
+ | 模块 | 管什么 |
114
+ |---|---|
115
+ | `aaas.Caller` | 连接、声明、开轮、**接住服务端的反向调用**(执行体在本地) |
116
+ | `aaas.Server` + `TurnContext` | 7 个方法、事件序号、取消次序、断连收尾 |
117
+ | `aaas.function` | 从 Python 签名与类型注解推 `parameters`(消灭手写 schema) |
118
+ | `aaas.wire` | JSON-RPC 2.0 子集、`id` 分区(`c-*` / `s-*`)、batch 拒绝 |
119
+ | `aaas.errors` | 17 个错误码 + 5 个关闭码,两级形状(`code` 分类 / `reason` 具体) |
120
+ | `aaas.types` | 线上对象的 Python 投影,与 `schema/` 逐字段对齐 |
121
+
122
+ 两侧都带**诊断**:调用方会在 `caller.diagnostics` 里记下它观测到的对端违规
123
+ (`id` 前缀不符、`seq` 缺口、方向不符、轮终态后的事件……)。规范里有大量 MUST 只能由
124
+ 对端观测到,SDK 把这些变成结构化清单,而不是散落的 `print`。
125
+
126
+ ## 仓库结构
127
+
128
+ ```
129
+ PROTOCOL.md 协议规范(唯一载体,1350+ 行)
130
+ schema/
131
+ aaas.data.schema.json 9 个核心对象的 $defs,全封闭
132
+ c2s.json / s2c.json 按**流动方向**切分的两个封闭世界
133
+ src/aaas/ Python SDK
134
+ tests/ 37 个测试(单元 + 端到端)
135
+ tools/ 自检工具
136
+ ```
137
+
138
+ `c2s` 与 `s2c` 按方向切分,不按方法:`invokeFunction.params` 只存在于 `s2c.json`,
139
+ 它的 `result` 只存在于 `c2s.json`。于是「从错误方向发来一个形状合法的消息」在**结构上**
140
+ 无处安放——它不是被某条规则禁止的,而是没有被定义过。
141
+
142
+ ## 自检
143
+
144
+ ```bash
145
+ python tools/validate_examples.py PROTOCOL.md # 正文示例 vs schema:14 checked, 0 mismatched
146
+ python tools/check_refs.py PROTOCOL.md # §X.Y / S1-S7 / C1-C15 悬空引用:0
147
+ python tools/check_leaks.py # 正文不得引用仓库外的材料
148
+ python tools/check_projection.py # schema ↔ SDK dataclass 逐字段对齐
149
+ python -m pytest tests -q # 37 passed
150
+ ```
151
+
152
+ `check_projection.py` 值得单独说一句:SDK 的 dataclass 是 schema 的第三份投影,
153
+ 两份东西一定漂移。开发过程中它抓到过 `Event` 漏 `reason` 与 `elapsedMs` 两个字段——
154
+ 读代码完全正常,只有真发出一条 `functionResult` 才撞上 `TypeError`。
155
+
156
+ ## 协议现状
157
+
158
+ Draft `0.2.0`,**不承诺向后兼容**。15 条一致性断言见 §8;单层套件,每条都能判 PASS/FAIL,
159
+ SKIP 必须显式记录——静默跳过是最常见的「假一致性」。
160
+
161
+ 还没写的:`LICENSE`。在那之前默认「保留所有权利」。
162
+
163
+ ## 与 A2A / MCP 的关系
164
+
165
+ 不是子集,不是超集,是**正交**。
166
+
167
+ - **A2A** 的方向是「我来之前 Agent 已经在那儿了」(服务端自述 `AgentCard`);AaaS 是「我声明之后 Agent 才存在」。
168
+ A2A 的 `AgentSkill` 有 `name`/`description`/`tags`/`examples`,**没有参数 schema**,所以 skill 无法直接变成一次 function calling。
169
+ - **MCP** 的工具提供方是**可被连入的服务器**;AaaS 的工具提供方是**发起连接的那一方**,方向相反。
170
+ - **OpenAI function calling** 是模型 API 的一个参数形状。AaaS 在其中 `name`/`description`/`parameters` 三格上同源,
171
+ 在此之上补了「谁来执行、怎么派发、失败怎么回」的整条链路。
aaas-0.2.0/README.md ADDED
@@ -0,0 +1,147 @@
1
+ # AaaS — Agent as a Service
2
+
3
+ **调用方在一条连接之内声明一个 Agent —— 它的职责,和它的能力(每个能力带完整参数 schema)——
4
+ 服务端用模型驱动这个 Agent,而每个能力的执行体始终留在调用方进程内。**
5
+
6
+ 「服务」这个词是字面的:调用方不部署任何东西、不注册任何回调地址、不持有任何后端,
7
+ 只要有一条能连出去的 WebSocket,就能得到一个**此刻才存在、由它自己定义的** Agent。
8
+
9
+ 这个仓库有两样东西,可以分开用:
10
+
11
+ | | 是什么 | 给谁 |
12
+ |---|---|---|
13
+ | **[`PROTOCOL.md`](https://github.com/YY-Yifei/aaas/blob/main/PROTOCOL.md)** | 协议规范:7 个方法、9 种事件、17 个错误码、15 条一致性断言 | 要实现这个协议的人 |
14
+ | **[`src/aaas/`](https://github.com/YY-Yifei/aaas/tree/main/src/aaas)** | Python SDK(`pip install aaas`):调用方与服务端两侧的参考实现 | 要**用**这个协议的人 |
15
+
16
+ 规范是源头,SDK 是它的实现。两者冲突时以规范为准,并视为缺陷。
17
+
18
+ ---
19
+
20
+ ## 安装
21
+
22
+ ```bash
23
+ pip install aaas
24
+ ```
25
+
26
+ 唯一硬依赖是 `websockets`。Python ≥ 3.10。
27
+
28
+ ## 10 分钟:一轮含反向 function calling
29
+
30
+ 调用方侧 —— 你声明 Agent,并在**自己的进程里**执行能力:
31
+
32
+ ```python
33
+ import asyncio, aaas
34
+
35
+ @aaas.function # 参数 schema 从签名推导,不用手写
36
+ async def search_local_index(query: str, page_size: int = 10) -> str:
37
+ """在调用方的本地持仓索引里检索
38
+
39
+ Args:
40
+ query: 检索词
41
+ page_size: 每页条数
42
+ """
43
+ return f"找到 3 条匹配「{query}」的记录"
44
+
45
+ async def main():
46
+ async with aaas.Caller("wss://api.example.com/aaas", token="...") as caller:
47
+ await caller.declare(aaas.AgentDefinition(
48
+ revision="v1",
49
+ instructions="你正在协助用户操作债券计算器。金额单位是万元。",
50
+ functions=(search_local_index.declaration,),
51
+ ), tools=[search_local_index])
52
+
53
+ turn = await caller.send("帮我查一下持仓")
54
+ async for event in turn: # 一轮不是一次请求-响应,是可迭代的事件流
55
+ if event.kind == "message.delta":
56
+ print(event.text, end="")
57
+
58
+ final = await turn.end()
59
+ assert final.status == "completed"
60
+
61
+ asyncio.run(main())
62
+ ```
63
+
64
+ 服务端侧 —— 你只需要写「用模型驱动这一轮」,协议层由 SDK 负责:
65
+
66
+ ```python
67
+ import aaas
68
+
69
+ async def my_agent(ctx: aaas.TurnContext) -> None:
70
+ ctx.reasoning("用户问本地索引,先调工具") # 未协商 reasoning 时静默不发
71
+ result = await ctx.call("search_local_index", {"query": ctx.text})
72
+ ctx.reply(*(p.text for p in (result.parts or ()) if p.text))
73
+ ctx.usage_of(output_tokens=42)
74
+
75
+ await aaas.Server(my_agent).serve("127.0.0.1", 8765)
76
+ ```
77
+
78
+ `seq` 怎么排、调用怎么超时、取消时 `cancelCall` 与终态谁先、断连时怎么收尾 ——
79
+ 都由 SDK 负责。它替你守的是 §5.3 那四条和 §5.8 那七条,那几处正是这类协议最容易写错的地方。
80
+
81
+ 看不装也能跑的效果:
82
+
83
+ ```bash
84
+ python -m aaas # 起一个本地服务端 + 一个调用方,跑完一轮含反向 function calling
85
+ ```
86
+
87
+ ## SDK 覆盖了什么
88
+
89
+ | 模块 | 管什么 |
90
+ |---|---|
91
+ | `aaas.Caller` | 连接、声明、开轮、**接住服务端的反向调用**(执行体在本地) |
92
+ | `aaas.Server` + `TurnContext` | 7 个方法、事件序号、取消次序、断连收尾 |
93
+ | `aaas.function` | 从 Python 签名与类型注解推 `parameters`(消灭手写 schema) |
94
+ | `aaas.wire` | JSON-RPC 2.0 子集、`id` 分区(`c-*` / `s-*`)、batch 拒绝 |
95
+ | `aaas.errors` | 17 个错误码 + 5 个关闭码,两级形状(`code` 分类 / `reason` 具体) |
96
+ | `aaas.types` | 线上对象的 Python 投影,与 `schema/` 逐字段对齐 |
97
+
98
+ 两侧都带**诊断**:调用方会在 `caller.diagnostics` 里记下它观测到的对端违规
99
+ (`id` 前缀不符、`seq` 缺口、方向不符、轮终态后的事件……)。规范里有大量 MUST 只能由
100
+ 对端观测到,SDK 把这些变成结构化清单,而不是散落的 `print`。
101
+
102
+ ## 仓库结构
103
+
104
+ ```
105
+ PROTOCOL.md 协议规范(唯一载体,1350+ 行)
106
+ schema/
107
+ aaas.data.schema.json 9 个核心对象的 $defs,全封闭
108
+ c2s.json / s2c.json 按**流动方向**切分的两个封闭世界
109
+ src/aaas/ Python SDK
110
+ tests/ 37 个测试(单元 + 端到端)
111
+ tools/ 自检工具
112
+ ```
113
+
114
+ `c2s` 与 `s2c` 按方向切分,不按方法:`invokeFunction.params` 只存在于 `s2c.json`,
115
+ 它的 `result` 只存在于 `c2s.json`。于是「从错误方向发来一个形状合法的消息」在**结构上**
116
+ 无处安放——它不是被某条规则禁止的,而是没有被定义过。
117
+
118
+ ## 自检
119
+
120
+ ```bash
121
+ python tools/validate_examples.py PROTOCOL.md # 正文示例 vs schema:14 checked, 0 mismatched
122
+ python tools/check_refs.py PROTOCOL.md # §X.Y / S1-S7 / C1-C15 悬空引用:0
123
+ python tools/check_leaks.py # 正文不得引用仓库外的材料
124
+ python tools/check_projection.py # schema ↔ SDK dataclass 逐字段对齐
125
+ python -m pytest tests -q # 37 passed
126
+ ```
127
+
128
+ `check_projection.py` 值得单独说一句:SDK 的 dataclass 是 schema 的第三份投影,
129
+ 两份东西一定漂移。开发过程中它抓到过 `Event` 漏 `reason` 与 `elapsedMs` 两个字段——
130
+ 读代码完全正常,只有真发出一条 `functionResult` 才撞上 `TypeError`。
131
+
132
+ ## 协议现状
133
+
134
+ Draft `0.2.0`,**不承诺向后兼容**。15 条一致性断言见 §8;单层套件,每条都能判 PASS/FAIL,
135
+ SKIP 必须显式记录——静默跳过是最常见的「假一致性」。
136
+
137
+ 还没写的:`LICENSE`。在那之前默认「保留所有权利」。
138
+
139
+ ## 与 A2A / MCP 的关系
140
+
141
+ 不是子集,不是超集,是**正交**。
142
+
143
+ - **A2A** 的方向是「我来之前 Agent 已经在那儿了」(服务端自述 `AgentCard`);AaaS 是「我声明之后 Agent 才存在」。
144
+ A2A 的 `AgentSkill` 有 `name`/`description`/`tags`/`examples`,**没有参数 schema**,所以 skill 无法直接变成一次 function calling。
145
+ - **MCP** 的工具提供方是**可被连入的服务器**;AaaS 的工具提供方是**发起连接的那一方**,方向相反。
146
+ - **OpenAI function calling** 是模型 API 的一个参数形状。AaaS 在其中 `name`/`description`/`parameters` 三格上同源,
147
+ 在此之上补了「谁来执行、怎么派发、失败怎么回」的整条链路。
@@ -0,0 +1,59 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "aaas"
7
+ version = "0.2.0"
8
+ description = "Python SDK for the Agent as a Service protocol — declare an Agent on one connection, keep every function body in your own process."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ keywords = [
12
+ "aaas",
13
+ "agent",
14
+ "function-calling",
15
+ "json-rpc",
16
+ "websocket",
17
+ "llm",
18
+ "protocol",
19
+ ]
20
+ classifiers = [
21
+ "Development Status :: 4 - Beta",
22
+ "Intended Audience :: Developers",
23
+ "Programming Language :: Python :: 3",
24
+ "Programming Language :: Python :: 3.10",
25
+ "Programming Language :: Python :: 3.11",
26
+ "Programming Language :: Python :: 3.12",
27
+ "Programming Language :: Python :: 3.13",
28
+ "Topic :: Software Development :: Libraries :: Python Modules",
29
+ "Typing :: Typed",
30
+ ]
31
+ dependencies = ["websockets>=12.0"]
32
+
33
+ [project.optional-dependencies]
34
+ test = ["pytest>=7.0", "pytest-asyncio>=0.23"]
35
+
36
+ [project.urls]
37
+ Homepage = "https://github.com/YY-Yifei/aaas"
38
+ Repository = "https://github.com/YY-Yifei/aaas"
39
+ Specification = "https://github.com/YY-Yifei/aaas/blob/main/PROTOCOL.md"
40
+
41
+ [project.scripts]
42
+ aaas-demo = "aaas.__main__:main"
43
+
44
+ [tool.setuptools]
45
+ package-dir = { "" = "src" }
46
+ zip-safe = false
47
+
48
+ [tool.setuptools.packages.find]
49
+ where = ["src"]
50
+
51
+ [tool.setuptools.package-data]
52
+ aaas = ["py.typed"]
53
+
54
+ [tool.pytest.ini_options]
55
+ testpaths = ["tests"]
56
+ # 让 `pytest` 不需要先 pip install -e .
57
+ pythonpath = ["src"]
58
+ asyncio_mode = "auto"
59
+ filterwarnings = ["error::DeprecationWarning:aaas.*"]
aaas-0.2.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,146 @@
1
+ """`aaas` —— Agent as a Service 协议的 Python SDK。
2
+
3
+ **一句话**:调用方在一条连接之内声明一个 Agent(职责 + 每个能力带完整参数 schema),
4
+ 服务端用模型驱动它,而**每个能力的执行体始终留在调用方进程内**。
5
+
6
+ 两种角色,各一个入口:
7
+
8
+ ``aaas.Caller``
9
+ 你是调用方。你声明 Agent、开一轮、并在自己进程里执行 function。
10
+ 服务端会反向发 ``aaas/invokeFunction``,:class:`~aaas.caller.Caller` 替你接住它。
11
+
12
+ ``aaas.Server``
13
+ 你是服务端。你实现 :class:`~aaas.server.TurnContext` 那一段业务(接模型),
14
+ 协议层(信封、id 分区、seq 连续、取消次序、断连收尾)由它负责。
15
+
16
+ 最小可跑的一段::
17
+
18
+ import asyncio, aaas
19
+
20
+ @aaas.function
21
+ async def search_local_index(query: str) -> str:
22
+ \"\"\"在调用方的本地持仓索引里检索
23
+
24
+ Args:
25
+ query: 检索词
26
+ \"\"\"
27
+ return f"找到 3 条匹配 {query} 的记录"
28
+
29
+ async def agent(ctx: aaas.TurnContext) -> None:
30
+ result = await ctx.call("search_local_index", {"query": ctx.text})
31
+ ctx.reply(*(p.text for p in (result.parts or ()) if p.text))
32
+
33
+ async def main() -> None:
34
+ server = await aaas.Server(agent).start(port=8765)
35
+ async with aaas.Caller("ws://127.0.0.1:8765", tools=[search_local_index]) as caller:
36
+ await caller.declare(aaas.AgentDefinition(
37
+ revision="v1",
38
+ instructions="你正在协助用户操作债券计算器。",
39
+ functions=(search_local_index.declaration,),
40
+ ))
41
+ turn = await caller.send("查一下持仓")
42
+ async for event in turn:
43
+ if event.kind == "message.completed":
44
+ print([p.text for p in event.parts])
45
+ await server.close()
46
+
47
+ asyncio.run(main())
48
+
49
+ 想直接看效果:``python -m aaas``。
50
+ """
51
+
52
+ from __future__ import annotations
53
+
54
+ from .caller import OPTIONAL_FEATURES, REQUIRED_FEATURES, Caller, ConnectResult, Turn
55
+ from .errors import (
56
+ AAASError,
57
+ AgentDefinitionInvalid,
58
+ AgentNotDeclared,
59
+ FunctionUnknown,
60
+ LimitExceeded,
61
+ Overloaded,
62
+ RateLimited,
63
+ SessionNotFound,
64
+ TurnLimitExceeded,
65
+ TurnNotFound,
66
+ Unauthenticated,
67
+ UnsupportedFeature,
68
+ UnsupportedVersion,
69
+ connection_lost,
70
+ )
71
+ from .function import FunctionRegistry, Tool, function
72
+ from .server import Connection, Server, Session, TurnContext
73
+ from .types import (
74
+ EVENT_KINDS,
75
+ AgentDefinition,
76
+ ErrorInfo,
77
+ Event,
78
+ FunctionCall,
79
+ FunctionDeclaration,
80
+ FunctionResult,
81
+ Message,
82
+ Part,
83
+ Usage,
84
+ )
85
+ from .wire import IdAllocator, Parsed, ParseFailure
86
+
87
+ __version__ = "0.2.0"
88
+
89
+ #: 协议主版本 —— 线上出现在 ``connect.params.protocolVersion``(``PROTOCOL.md`` §5.1.1)。
90
+ PROTOCOL_VERSION = "1"
91
+
92
+ #: WS 子协议名(§5.1)。它与 :data:`PROTOCOL_VERSION` 是两个不同的契约。
93
+ SUBPROTOCOL = "aaas.v1"
94
+
95
+ __all__ = [
96
+ # 两个入口
97
+ "Caller",
98
+ "Server",
99
+ # 调用方侧
100
+ "Turn",
101
+ "ConnectResult",
102
+ # 服务端侧
103
+ "TurnContext",
104
+ "Connection",
105
+ "Session",
106
+ # 能力
107
+ "function",
108
+ "Tool",
109
+ "FunctionRegistry",
110
+ # 数据模型
111
+ "AgentDefinition",
112
+ "FunctionDeclaration",
113
+ "Message",
114
+ "Part",
115
+ "Event",
116
+ "FunctionCall",
117
+ "FunctionResult",
118
+ "Usage",
119
+ "ErrorInfo",
120
+ "EVENT_KINDS",
121
+ # 错误
122
+ "AAASError",
123
+ "AgentDefinitionInvalid",
124
+ "AgentNotDeclared",
125
+ "FunctionUnknown",
126
+ "LimitExceeded",
127
+ "Overloaded",
128
+ "RateLimited",
129
+ "SessionNotFound",
130
+ "TurnLimitExceeded",
131
+ "TurnNotFound",
132
+ "Unauthenticated",
133
+ "UnsupportedFeature",
134
+ "UnsupportedVersion",
135
+ "connection_lost",
136
+ # 信封
137
+ "IdAllocator",
138
+ "Parsed",
139
+ "ParseFailure",
140
+ # 常量
141
+ "__version__",
142
+ "PROTOCOL_VERSION",
143
+ "SUBPROTOCOL",
144
+ "REQUIRED_FEATURES",
145
+ "OPTIONAL_FEATURES",
146
+ ]
@@ -0,0 +1,124 @@
1
+ """``python -m aaas`` —— 一条连接上跑完「声明 → 一轮 → 反向 function calling」。
2
+
3
+ 这是那个「10 分钟」判据的可执行版本。它同时是自检:四段都跑通,说明
4
+ ``connect`` / ``declareAgent`` / ``sendInput`` / ``invokeFunction`` / ``event`` 五个方法
5
+ 在两侧都接上了。
6
+
7
+ $ python -m aaas
8
+ [server] listening on ws://127.0.0.1:8765
9
+ [caller] connected session=sess_… features=[…] history=stateless
10
+ [caller] declared revision=bond-calc-v1 functions=['search_local_index']
11
+ [server] turn turn_… started
12
+ [tool ] search_local_index({'query': '查一下持仓'}) → 找到 3 条匹配「查一下持仓」的记录
13
+ [caller] message.completed 找到 3 条匹配「查一下持仓」的记录
14
+ [caller] turn.completed status=completed finishReason=stop calls=1
15
+ [caller] diagnostics 0 条
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import asyncio
21
+ import sys
22
+ from typing import Any
23
+
24
+ import aaas
25
+
26
+ #: 一个「本地」能力 —— 它的执行体只在这一个进程里(§1.2 G2)。
27
+ HOLDINGS = {
28
+ "持仓": ["国债 24 附息 03", "地方债 23 粤 12", "政金债 22 国开 07"],
29
+ }
30
+
31
+
32
+ @aaas.function
33
+ async def search_local_index(query: str, page_size: int = 10) -> str:
34
+ """在调用方的本地持仓索引里检索
35
+
36
+ Args:
37
+ query: 检索词
38
+ page_size: 每页条数
39
+ """
40
+ hits = [name for name in HOLDINGS["持仓"] if query in name or "持仓" in query]
41
+ hits = hits[:page_size]
42
+ return f"找到 {len(hits)} 条匹配「{query}」的记录:" + "、".join(hits)
43
+
44
+
45
+ async def tiny_agent(ctx: aaas.TurnContext) -> None:
46
+ """一个不接模型的「模型」:它只证明链路是通的。
47
+
48
+ 真实实现把这一段换成你的 LLM 调用。要发什么事件、``seq`` 怎么排、
49
+ 调用怎么超时、取消怎么收尾 —— 都由 :class:`aaas.TurnContext` 负责。
50
+ """
51
+ print(f"[server] turn {ctx.turn_id} started text={ctx.text!r}")
52
+
53
+ # 推理事件只在协商了 reasoning 时才会真的发出去。
54
+ ctx.reasoning("用户问的是本地索引,先调一次工具。")
55
+
56
+ result = await ctx.call("search_local_index", {"query": ctx.text})
57
+ if result.parts:
58
+ for part in result.parts:
59
+ if part.text:
60
+ print(f"[tool ] search_local_index → {part.text}")
61
+ ctx.delta(part.text)
62
+ ctx.reply(part.text)
63
+ ctx.usage_of(output_tokens=42)
64
+
65
+
66
+ async def main() -> int:
67
+ port = 8765
68
+ server = await aaas.Server(tiny_agent).start("127.0.0.1", port)
69
+ print(f"[server] listening on ws://127.0.0.1:{port}")
70
+
71
+ try:
72
+ async with aaas.Caller(
73
+ f"ws://127.0.0.1:{port}", tools=[search_local_index]
74
+ ) as caller:
75
+ info = caller.connect_result
76
+ assert info is not None
77
+ print(
78
+ f"[caller] connected session={info.session_id} "
79
+ f"features={list(info.features)} history={info.history_mode}"
80
+ )
81
+
82
+ revision, names = await caller.declare(
83
+ aaas.AgentDefinition(
84
+ revision="bond-calc-v1",
85
+ instructions="你正在协助用户操作债券计算器。金额单位是万元。",
86
+ functions=(search_local_index.declaration,),
87
+ )
88
+ )
89
+ print(f"[caller] declared revision={revision} functions={list(names)}")
90
+
91
+ turn = await caller.send("查一下持仓")
92
+ async for event in turn:
93
+ if event.kind == "message.completed":
94
+ text = "".join(p.text or "" for p in (event.parts or ()))
95
+ print(f"[caller] message.completed {text}")
96
+ elif event.kind == "turn.completed":
97
+ calls = event.usage.calls if event.usage else None
98
+ print(
99
+ f"[caller] turn.completed status={event.status} "
100
+ f"finishReason={event.finish_reason} calls={calls}"
101
+ )
102
+
103
+ final = await turn.end()
104
+ print(f"[caller] text={turn.text!r} ok={turn.ok}")
105
+
106
+ if caller.diagnostics:
107
+ print(f"[caller] diagnostics={len(caller.diagnostics)}")
108
+ for entry in caller.diagnostics:
109
+ print(f" {entry['code']}: {entry['message']}")
110
+ else:
111
+ print("[caller] diagnostics 0 条(没有观测到对端违规)")
112
+
113
+ if final.status != "completed":
114
+ print(f"[fail ] 本轮没有正常完成:{final.status}", file=sys.stderr)
115
+ return 1
116
+ finally:
117
+ await server.close()
118
+
119
+ print("[ok] 一轮含反向 function calling 已跑完")
120
+ return 0
121
+
122
+
123
+ if __name__ == "__main__":
124
+ raise SystemExit(asyncio.run(main()))