aaas 0.2.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.
aaas/__init__.py ADDED
@@ -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
+ ]
aaas/__main__.py ADDED
@@ -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()))
aaas/_jsonschema.py ADDED
@@ -0,0 +1,208 @@
1
+ """一个**故意很小**的 JSON Schema 校验器 —— 只为落实 §7 第 4 条。
2
+
3
+ 规范要求:``invokeFunction.arguments`` 的源头是模型输出,MUST 被调用方当作
4
+ **不可信输入**处理,执行前按 ``FunctionDeclaration.parameters`` 校验(§7 第 4 条)。
5
+ 这条 MUST 需要一个校验器,但它不需要一个完整实现。
6
+
7
+ 三条设计取舍,都是为了让「宁可放过、绝不误拒」成立:
8
+
9
+ 1. **不认识的 keyword 一律忽略。** 声明里的 ``format`` / ``$schema`` / 自定义注解
10
+ 不是拒绝的理由 —— 误拒合法参数比漏放一次非法参数更坏:前者会让一个健康的调用
11
+ 永久失败,而模型无从知道为什么。
12
+ 2. **没有第三方依赖。** 引 ``jsonschema`` 会让这个包为了 150 行逻辑背上整个依赖树,
13
+ 而 SDK 的定位是「装上就能用」。
14
+ 3. **错误带 JSON Pointer 路径。** 模型看到 ``/page_size: 期望 integer`` 才知道该改哪一格;
15
+ 看到一句「参数不合法」它只会换个参数再撞一次。
16
+
17
+ 支持的关键字:``type`` ``enum`` ``const`` ``properties`` ``required``
18
+ ``additionalProperties`` ``items`` ``minimum`` ``maximum`` ``exclusiveMinimum``
19
+ ``exclusiveMaximum`` ``minLength`` ``maxLength`` ``pattern`` ``minItems`` ``maxItems``
20
+ ``anyOf`` ``oneOf`` ``allOf`` ``$ref``(仅本文件内 ``#/$defs/…``)。
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import re
26
+ from typing import Any
27
+
28
+ __all__ = ["validate", "SchemaSubset"]
29
+
30
+ _TYPES = {
31
+ "object": dict,
32
+ "array": list,
33
+ "string": str,
34
+ "boolean": bool,
35
+ "null": type(None),
36
+ }
37
+
38
+
39
+ class SchemaSubset:
40
+ """校验一个实例。``validate`` 返回错误清单,空清单即通过。"""
41
+
42
+ def __init__(self, root: dict[str, Any] | None = None) -> None:
43
+ self.root = root or {}
44
+
45
+ def validate(self, instance: Any, schema: dict[str, Any] | None = None) -> list[str]:
46
+ """→ ``["/page_size: 期望 integer,收到 string"]`` 形式的错误清单。"""
47
+ return self._walk(instance, schema if schema is not None else self.root, "")
48
+
49
+ # ------------------------------------------------------------------ 内部
50
+
51
+ def _resolve(self, schema: dict[str, Any]) -> dict[str, Any]:
52
+ """只解析 ``#/$defs/…`` —— 支持外部 ``$ref`` 会把这件事变成一次网络与文件系统请求。"""
53
+ ref = schema.get("$ref")
54
+ if not isinstance(ref, str):
55
+ return schema
56
+ if not ref.startswith("#/"):
57
+ return {} # 外部引用:不认识 → 按「无约束」处理(宁可放过)
58
+ node: Any = {"$defs": self.root.get("$defs", {})}
59
+ for part in ref[2:].split("/"):
60
+ part = part.replace("~1", "/").replace("~0", "~")
61
+ if not isinstance(node, dict) or part not in node:
62
+ return {}
63
+ node = node[part]
64
+ return node if isinstance(node, dict) else {}
65
+
66
+ def _walk(self, inst: Any, schema: dict[str, Any], path: str) -> list[str]:
67
+ schema = self._resolve(schema)
68
+ errs: list[str] = []
69
+ where = path or "/"
70
+
71
+ # const / enum —— 最便宜的两个先判
72
+ if "const" in schema and inst != schema["const"]:
73
+ errs.append(f"{where}: 期望 {schema['const']!r},收到 {inst!r}")
74
+ if "enum" in schema and inst not in schema["enum"]:
75
+ errs.append(f"{where}: 必须是 {schema['enum']} 之一,收到 {inst!r}")
76
+
77
+ # type —— 注意 bool 是 int 的子类,顺序要小心
78
+ want = schema.get("type")
79
+ if isinstance(want, str):
80
+ if not _is_type(inst, want):
81
+ errs.append(f"{where}: 期望 {want},收到 {_type_name(inst)}")
82
+ return errs
83
+ elif isinstance(want, list):
84
+ if not any(_is_type(inst, w) for w in want):
85
+ errs.append(f"{where}: 期望 {'/'.join(want)} 之一,收到 {_type_name(inst)}")
86
+ return errs
87
+
88
+ if isinstance(inst, str):
89
+ errs += self._strings(inst, schema, where)
90
+ if isinstance(inst, (int, float)) and not isinstance(inst, bool):
91
+ errs += self._numbers(inst, schema, where)
92
+ if isinstance(inst, dict):
93
+ errs += self._objects(inst, schema, path)
94
+ if isinstance(inst, list):
95
+ errs += self._arrays(inst, schema, path)
96
+
97
+ for key in ("allOf",):
98
+ for sub in schema.get(key, ()) if isinstance(schema.get(key), list) else ():
99
+ errs += self._walk(inst, sub, path)
100
+ if isinstance(schema.get("anyOf"), list):
101
+ if not any(not self._walk(inst, s, path) for s in schema["anyOf"]):
102
+ errs.append(f"{where}: 不满足 anyOf 中任何一支")
103
+ if isinstance(schema.get("oneOf"), list):
104
+ hits = sum(1 for s in schema["oneOf"] if not self._walk(inst, s, path))
105
+ if hits != 1:
106
+ errs.append(f"{where}: oneOf 要求恰好命中一支,实际命中 {hits} 支")
107
+ return errs
108
+
109
+ def _strings(self, inst: str, schema: dict[str, Any], where: str) -> list[str]:
110
+ errs = []
111
+ if "minLength" in schema and len(inst) < schema["minLength"]:
112
+ errs.append(f"{where}: 长度不得小于 {schema['minLength']}")
113
+ if "maxLength" in schema and len(inst) > schema["maxLength"]:
114
+ errs.append(f"{where}: 长度不得大于 {schema['maxLength']}")
115
+ pattern = schema.get("pattern")
116
+ if isinstance(pattern, str):
117
+ try:
118
+ if not re.search(pattern, inst):
119
+ errs.append(f"{where}: 不匹配 {pattern}")
120
+ except re.error:
121
+ pass # 声明里的正则本身写坏了 → 不能因此拒掉数据
122
+ return errs
123
+
124
+ def _numbers(self, inst: float, schema: dict[str, Any], where: str) -> list[str]:
125
+ errs = []
126
+ if "minimum" in schema and inst < schema["minimum"]:
127
+ errs.append(f"{where}: 不得小于 {schema['minimum']}")
128
+ if "maximum" in schema and inst > schema["maximum"]:
129
+ errs.append(f"{where}: 不得大于 {schema['maximum']}")
130
+ if "exclusiveMinimum" in schema and inst <= schema["exclusiveMinimum"]:
131
+ errs.append(f"{where}: 必须大于 {schema['exclusiveMinimum']}")
132
+ if "exclusiveMaximum" in schema and inst >= schema["exclusiveMaximum"]:
133
+ errs.append(f"{where}: 必须小于 {schema['exclusiveMaximum']}")
134
+ return errs
135
+
136
+ def _objects(self, inst: dict[str, Any], schema: dict[str, Any], path: str) -> list[str]:
137
+ errs = []
138
+ for name in schema.get("required", ()) if isinstance(schema.get("required"), list) else ():
139
+ if name not in inst:
140
+ errs.append(f"{path}/{name}: 必填字段缺失")
141
+ props = schema.get("properties")
142
+ props = props if isinstance(props, dict) else {}
143
+ for name, value in inst.items():
144
+ child = f"{path}/{name}"
145
+ if name in props:
146
+ errs += self._walk(value, props[name], child)
147
+ continue
148
+ extra = schema.get("additionalProperties")
149
+ if extra is False:
150
+ errs.append(f"{child}: 不允许的字段(additionalProperties: false)")
151
+ elif isinstance(extra, dict):
152
+ errs += self._walk(value, extra, child)
153
+ return errs
154
+
155
+ def _arrays(self, inst: list[Any], schema: dict[str, Any], path: str) -> list[str]:
156
+ errs = []
157
+ if "minItems" in schema and len(inst) < schema["minItems"]:
158
+ errs.append(f"{path or '/'}: 至少 {schema['minItems']} 项")
159
+ if "maxItems" in schema and len(inst) > schema["maxItems"]:
160
+ errs.append(f"{path or '/'}: 至多 {schema['maxItems']} 项")
161
+ items = schema.get("items")
162
+ if isinstance(items, dict):
163
+ for i, value in enumerate(inst):
164
+ errs += self._walk(value, items, f"{path}/{i}")
165
+ return errs
166
+
167
+
168
+ def _type_name(value: Any) -> str:
169
+ if value is None:
170
+ return "null"
171
+ if isinstance(value, bool):
172
+ return "boolean"
173
+ if isinstance(value, int):
174
+ return "integer"
175
+ if isinstance(value, float):
176
+ return "number"
177
+ if isinstance(value, str):
178
+ return "string"
179
+ if isinstance(value, list):
180
+ return "array"
181
+ if isinstance(value, dict):
182
+ return "object"
183
+ return type(value).__name__
184
+
185
+
186
+ def _is_type(value: Any, want: str) -> bool:
187
+ if want == "integer":
188
+ # 3.0 是 integer:JSON 里没有 3 与 3.0 的区别,而模型常把 10 写成 10.0。
189
+ return isinstance(value, int) and not isinstance(value, bool) or (
190
+ isinstance(value, float) and value.is_integer()
191
+ )
192
+ if want == "number":
193
+ return isinstance(value, (int, float)) and not isinstance(value, bool)
194
+ if want == "boolean":
195
+ return isinstance(value, bool)
196
+ expected = _TYPES.get(want)
197
+ if expected is None:
198
+ return True # 不认识的类型名 → 无约束
199
+ if want == "object":
200
+ return isinstance(value, dict)
201
+ if want == "array":
202
+ return isinstance(value, list)
203
+ return isinstance(value, expected)
204
+
205
+
206
+ def validate(instance: Any, schema: dict[str, Any] | None = None) -> list[str]:
207
+ """便捷入口:``validate(args, decl.parameters)`` → 错误清单。"""
208
+ return SchemaSubset(schema).validate(instance, schema)