pasm-framework 0.3.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.
@@ -0,0 +1,292 @@
1
+ """项目脚手架 —— ``pasm-framework new`` 的实现。
2
+
3
+ 目标:从"读文档 20 分钟"变成"一条命令拿到能跑的项目"。
4
+ 生成的内容刻意保持**零第三方依赖**(只依赖 pasm-framework 本身),
5
+ 所以 ``pip install pasm-framework`` 之后立刻就能 ``python main.py``。
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from pathlib import Path
10
+ from typing import Dict, List, Tuple
11
+
12
+ from .errors import FrameworkError
13
+
14
+ KINDS = ("app", "chatbot", "game_npc")
15
+
16
+ _MAIN_APP = '''"""最小 AI 应用 —— pasm-framework 脚手架生成。
17
+
18
+ 跑起来:python main.py (离线,用内置知识库+模板回复)
19
+ 接 LLM:PASM_LLM_API_KEY=sk-xxx PASM_LLM_PROVIDER=deepseek python main.py
20
+ """
21
+ from pasm_framework import SimpleApplication, capability, load
22
+
23
+
24
+ class MyApp(SimpleApplication):
25
+ """把每个能力写成一个方法,用 @capability 声明触发词。"""
26
+
27
+ @capability(keywords=("帮助", "help"), description="显示帮助")
28
+ def help(self, text):
29
+ return "我能回答资料库里的问题。试着问我:营业时间 / 怎么退货"
30
+
31
+ @capability(keywords=("时间", "几点"), description="营业时间")
32
+ def hours(self, text):
33
+ return "我们的营业时间是每天 9:00 - 21:00。"
34
+
35
+
36
+ def main():
37
+ # preset 决定"开哪些插件":minimal / default / chatbot / api / desktop
38
+ cfg = load(preset_name="default", knowledge_base={
39
+ "enabled": True, "config": {"kb_dir": "./kb"}})
40
+
41
+ app = MyApp("my-app", {"name": "小智", "role": "智能助手"},
42
+ backend_config=cfg)
43
+
44
+ # 导入你的资料(一次性;之后落盘复用)
45
+ app.teach([
46
+ {"title": "退货政策", "content": "签收后 7 天内可无理由退货。",
47
+ "source": "faq", "tags": ["退货"]},
48
+ ])
49
+
50
+ for q in ["怎么退货?", "你们几点下班?"]:
51
+ print("我:%s" % q)
52
+ print("AI:%s" % app.ask(q))
53
+ print("-" * 40)
54
+
55
+
56
+ if __name__ == "__main__":
57
+ main()
58
+ '''
59
+
60
+ _MAIN_CHATBOT = '''"""站点智能客服 —— pasm-framework 脚手架生成。
61
+
62
+ 跑起来(控制台):python main.py
63
+ 跑起来(对外服务):python main.py --serve → 浏览器打开 http://127.0.0.1:8080/
64
+ 嵌入站点:<iframe src="http://你的主机:8080/" width="420" height="620"></iframe>
65
+
66
+ 接 LLM(可选):PASM_LLM_API_KEY=sk-xxx PASM_LLM_PROVIDER=deepseek python main.py
67
+ """
68
+ import sys
69
+
70
+ from pasm_framework import SimpleApplication, load
71
+
72
+
73
+ class CustomerService(SimpleApplication):
74
+ """零 LLM 也能"就资料作答、查不到如实说不知道"。"""
75
+
76
+ # 检索不到资料时的话术(面向用户,别写开发提示)
77
+ DEFAULT_UNKNOWN = "抱歉,我暂时没有查到相关资料。你可以换个说法,或联系人工客服 400-000-0000。"
78
+
79
+
80
+ def build():
81
+ cfg = load(
82
+ preset_name="chatbot", # 护栏收紧 + 开 HTTP 网关
83
+ knowledge_base={"enabled": True, "config": {"kb_dir": "./kb"}},
84
+ web_gateway={"enabled": True, "config": {"host": "0.0.0.0", "port": 8080}},
85
+ )
86
+ app = CustomerService(
87
+ "shop-cs", {"name": "小智", "role": "智能客服", "tone": "温暖、专业、耐心"},
88
+ backend_config=cfg,
89
+ )
90
+
91
+ # ====== 把站点资料喂进来(FAQ / 帮助中心 / 商品说明)======
92
+ # 真实项目里可以从 CMS/数据库/接口拉,然后调 app.teach(...)
93
+ app.teach([
94
+ {"title": "退货政策", "content": "商品签收后 7 天内可无理由退货,需保持吊牌完整。",
95
+ "source": "faq", "tags": ["退货", "售后"]},
96
+ {"title": "配送时效", "content": "现货 24 小时内发货,偏远地区 3-5 天送达。",
97
+ "source": "faq", "tags": ["配送", "物流", "发货"]},
98
+ {"title": "发票", "content": "下单时可勾选电子发票,次日发送至注册邮箱。",
99
+ "source": "faq", "tags": ["发票"]},
100
+ {"title": "营业时间", "content": "客服在线时间 9:00-21:00,节假日不休。",
101
+ "source": "faq", "tags": ["时间"]},
102
+ ])
103
+ return app
104
+
105
+
106
+ def main(argv=None):
107
+ argv = list(sys.argv[1:] if argv is None else argv)
108
+ app = build()
109
+
110
+ if "--serve" in argv:
111
+ print("网关已启动:http://0.0.0.0:8080/ (Ctrl+C 退出)")
112
+ print("嵌入站点:<iframe src='http://你的主机:8080/' width='420' height='620'></iframe>")
113
+ try:
114
+ app.serve()
115
+ import time
116
+ while True:
117
+ time.sleep(1)
118
+ except KeyboardInterrupt:
119
+ app.close()
120
+ return 0
121
+
122
+ for q in ["怎么退货?", "多久能发货?", "你们几点上班?",
123
+ "能开增值税专用发票吗?", "你们老板是谁?"]:
124
+ print("客户:%s" % q)
125
+ print("客服:%s" % app.ask(q, session_id="demo-1"))
126
+ print("-" * 50)
127
+ print("资料库统计:%s" % app.plugins.get("knowledge_base").stats())
128
+ return 0
129
+
130
+
131
+ if __name__ == "__main__":
132
+ raise SystemExit(main())
133
+ '''
134
+
135
+ _MAIN_NPC = '''"""游戏 NPC —— pasm-framework 脚手架生成(离线、零 LLM)。
136
+
137
+ PASM 的差异点在"身份":NPC 有性格 / 情绪 / 记忆 / 会被玩家反馈塑形。
138
+ 跑起来:python main.py
139
+
140
+ 要点
141
+ ----
142
+ · ``observe`` 写记忆,``feel`` 改情绪,``feedback`` 让行为被玩家塑形;
143
+ · 情绪会影响 NPC 的说话温度(warmth 插件);
144
+ · 状态落盘在 persist_dir,重启后性格/关系还在。
145
+ """
146
+ from pasm_framework import SimpleApplication, capability, load
147
+
148
+
149
+ class Innkeeper(SimpleApplication):
150
+ """酒馆老板:记得熟客,被夸奖更热情,被欺负会冷淡。"""
151
+
152
+ @capability(keywords=("买", "酒", "来一"), description="买酒")
153
+ def buy(self, text):
154
+ self.observe(title="玩家买酒", brief=text, tags=["交易"], salience=3)
155
+ self.feel(0.2)
156
+ return "给你满上!%s" % ("今天算你便宜点。" if self.mood > 0.3 else "")
157
+
158
+ @capability(keywords=("聊", "说说", "故事"), description="闲聊")
159
+ def chat_about(self, text):
160
+ hits = self.recall(text, k=2)
161
+ if hits:
162
+ return "这个啊……我记得:%s" % hits[0].get("brief", "")
163
+ return "唉,最近不太平,没什么好说的。"
164
+
165
+ @capability(keywords=("打", "揍", "滚"), description="敌对行为")
166
+ def hostile(self, text):
167
+ self.feel(-0.5)
168
+ self.observe(title="玩家挑衅", brief=text, tags=["敌意"], salience=4)
169
+ return "……你再这样我就叫卫兵了。" if self.mood < -0.3 else "别闹。"
170
+
171
+
172
+ def main():
173
+ cfg = load(preset_name="game_npc") # 离线优先,不接 LLM
174
+ npc = Innkeeper("innkeeper-01", {
175
+ "name": "老橡", "role": "酒馆老板",
176
+ "tone": "粗嗓门但热心", "temper": 0.7, "energy": 0.4, "play": 0.6,
177
+ }, backend_config=cfg)
178
+
179
+ script = ["打二两酒", "说说镇上的事", "揍你一顿", "打二两酒"]
180
+ for line in script:
181
+ reply = npc.ask(line, session_id="player-1")
182
+ print("玩家:%s" % line)
183
+ print("老橡:%s (情绪 %.2f)" % (reply, npc.mood))
184
+ print("-" * 50)
185
+
186
+
187
+ if __name__ == "__main__":
188
+ main()
189
+ '''
190
+
191
+ _CONFIG_JSON = '''{
192
+ "plugins": {
193
+ "safety": {"enabled": true, "config": {"mode": "warn"}},
194
+ "sessions": {"enabled": true, "config": {"max_history": 20}},
195
+ "knowledge_base": {"enabled": true, "config": {"kb_dir": "./kb"}},
196
+ "warmth": {"enabled": true, "config": {}},
197
+ "observability": {"enabled": true, "config": {}},
198
+ "llm_responder": {"enabled": false, "config": {}},
199
+ "web_gateway": {"enabled": false, "config": {"host": "127.0.0.1", "port": 8080}}
200
+ }
201
+ }
202
+ '''
203
+
204
+ _README = """# {name}
205
+
206
+ 由 `pasm-framework new` 生成(模板:**{kind}**)。
207
+
208
+ ## 跑起来
209
+
210
+ ```bash
211
+ pip install pasm-framework
212
+ python main.py{serve_hint}
213
+ ```
214
+
215
+ ## 换个配置
216
+
217
+ 开关表在 `config.json`(也可用环境变量,见下)。代码里这样加载:
218
+
219
+ ```python
220
+ from pasm_framework import load
221
+ cfg = load("config.json", env=True)
222
+ app = MyApp("my-app", {{"name": "小智"}}, backend_config=cfg)
223
+ ```
224
+
225
+ 环境变量(优先级高于 `config.json`):
226
+
227
+ | 变量 | 作用 |
228
+ | --- | --- |
229
+ | `PASM_PLUGINS` | `none` / `default` / `safety,sessions,...` |
230
+ | `PASM_KB_DIR` | 知识库目录 |
231
+ | `PASM_SAFETY_MODE` | `warn` 或 `block` |
232
+ | `PASM_LLM_PROVIDER` / `PASM_LLM_MODEL` / `PASM_LLM_API_KEY` | 接入 LLM |
233
+ | `PASM_HTTP_HOST` / `PASM_HTTP_PORT` / `PASM_HTTP_TOKEN` | HTTP 网关与鉴权 |
234
+
235
+ ## 下一步
236
+
237
+ - 教程:`docs/tutorials/`(从快速上手到多语言接入)
238
+ - 自检:`pasm-framework doctor`
239
+ - 看插件:`pasm-framework plugins`
240
+ """
241
+
242
+ _REQ = """pasm-framework>=0.2.1
243
+ # 可选:接 LLM 时不需要额外依赖(内置用标准库 urllib)
244
+ # 可选:读 .yaml 配置时需要
245
+ # pyyaml>=6
246
+ """
247
+
248
+ _TEMPLATES: Dict[str, Tuple[str, str]] = {
249
+ "app": (_MAIN_APP, ""),
250
+ "chatbot": (_MAIN_CHATBOT, " --serve"),
251
+ "game_npc": (_MAIN_NPC, ""),
252
+ }
253
+
254
+
255
+ def render(kind: str, name: str) -> Dict[str, str]:
256
+ """返回 ``{相对路径: 内容}``(纯函数,便于测试与预览)。"""
257
+ if kind not in _TEMPLATES:
258
+ raise FrameworkError(
259
+ "未知模板 %r;可用:%s" % (kind, ", ".join(KINDS)))
260
+ main_py, serve_hint = _TEMPLATES[kind]
261
+ return {
262
+ "main.py": main_py,
263
+ "config.json": _CONFIG_JSON,
264
+ "requirements.txt": _REQ,
265
+ "README.md": _README.format(name=name, kind=kind, serve_hint=serve_hint),
266
+ }
267
+
268
+
269
+ def create(target_dir: str, *, kind: str = "app", name: str = "",
270
+ force: bool = False) -> List[str]:
271
+ """在 ``target_dir`` 生成项目;返回写出的相对路径列表。
272
+
273
+ ``force=False`` 时,若目录已存在同名文件 → 报错,不覆盖用户内容。
274
+ """
275
+ root = Path(target_dir)
276
+ files = render(kind, name or root.name or "my-app")
277
+ if root.exists() and not root.is_dir():
278
+ raise FrameworkError("目标已存在且不是目录:%s" % root)
279
+ if not force:
280
+ clash = [rel for rel in files if (root / rel).exists()]
281
+ if clash:
282
+ raise FrameworkError(
283
+ "目标目录已存在同名文件:%s(加 --force 覆盖)" % ", ".join(clash))
284
+ root.mkdir(parents=True, exist_ok=True)
285
+ (root / "kb").mkdir(exist_ok=True)
286
+ written: List[str] = []
287
+ for rel, content in files.items():
288
+ p = root / rel
289
+ p.parent.mkdir(parents=True, exist_ok=True)
290
+ p.write_text(content, encoding="utf-8")
291
+ written.append(rel)
292
+ return written
@@ -0,0 +1,154 @@
1
+ """CognitiveAssembler + CognitiveService —— 框架与引擎之间的「唯一变动点」。
2
+
3
+ 为什么这层必须存在(V1→V2 不重写智能体/技能的核心机制)
4
+ ------------------------------------------------------
5
+ `pasm_skills.sdk.BaseAgent` 已经通过 ``CognitiveBackend`` 协议与引擎解耦(见
6
+ ``pasm_skills/sdk/backend.py``):它只认识协议,不认识 ``pasm.*``。
7
+
8
+ 本模块在协议之上再架一层**装配层**,把"选哪个后端、怎么把它适配成应用可用的认知服务"
9
+ 这件事彻底收口:
10
+
11
+ · ``CognitiveBackend`` (来自基座 sdk) —— 引擎侧稳定契约(episode_push/recall/feedback…)。
12
+ · ``CognitiveService`` —— 应用侧稳定契约:在后端外面套一层,
13
+ 附带 domain 适配器钩子,但**对外仍是
14
+ CognitiveBackend**(这样 BaseAgent 无需改动)。
15
+ · ``CognitiveAssembler`` —— 装配器:V1 调 :meth:`v1` 拿到 V1 后端;
16
+ V2 只需新增 :meth:`v2` 返回 ``PasmV2Backend``,
17
+ **上层(4 智能体 / 3 技能)一行不改**。
18
+
19
+ 所以"唯一变动点"就是这里:引擎大改时,只有 ``CognitiveAssembler.v2``(以及
20
+ ``PasmV2Backend``)需要写,应用层完全不动。
21
+ """
22
+ from __future__ import annotations
23
+
24
+ from typing import Any, Dict, List, Optional
25
+
26
+ from pasm_skills.sdk.backend import (
27
+ CognitiveBackend,
28
+ create_v1_backend,
29
+ )
30
+ from .adapter import DomainAdapter, NullDomainAdapter
31
+
32
+
33
+ # ============================================================ 认知服务(应用侧契约)
34
+
35
+ class CognitiveService:
36
+ """包在 ``CognitiveBackend`` 外面的应用侧认知服务。
37
+
38
+ 它**实现** ``CognitiveBackend`` 协议(属性 + 方法齐全),因此可以直接作为
39
+ ``BaseAgent(backend=...)`` 的入参;同时额外携带 ``domain`` 钩子,
40
+ 供 ``BaseApplication`` 在检索/回复时注入领域知识。
41
+
42
+ V1→V2:本类不变。变的只是它内部委托的那个 ``_backend`` 是 V1 还是 V2。
43
+ """
44
+
45
+ def __init__(
46
+ self,
47
+ backend: CognitiveBackend,
48
+ domain: DomainAdapter = NullDomainAdapter(),
49
+ ) -> None:
50
+ # 不变量:入参必须真满足协议,否则就是"变动点"被改坏了——
51
+ # surface-guard 会抓这个(见框架 __init__ 的契约断言)。
52
+ if not isinstance(backend, CognitiveBackend):
53
+ raise TypeError(
54
+ "CognitiveService 需要 CognitiveBackend 协议实现,收到 %r"
55
+ % type(backend).__name__
56
+ )
57
+ self._backend = backend
58
+ self.domain = domain
59
+
60
+ # ---- CognitiveBackend 协议:全部委托给后端 ----
61
+
62
+ @property
63
+ def is_core(self) -> bool:
64
+ return bool(getattr(self._backend, "is_core", False))
65
+
66
+ @property
67
+ def emotion_system(self) -> Optional[Any]:
68
+ return getattr(self._backend, "emotion_system", None)
69
+
70
+ def episode_push(self, *, title, brief, tags, category, salience) -> None:
71
+ self._backend.episode_push(
72
+ title=title, brief=brief, tags=tags,
73
+ category=category, salience=salience,
74
+ )
75
+
76
+ def recall(self, query: str, k: int = 5) -> List[Dict[str, Any]]:
77
+ # 基础检索走后端;领域知识注入留给 BaseApplication(避免污染协议签名)。
78
+ return self._backend.recall(query, k=k)
79
+
80
+ def feedback(self, kind: str, action: Optional[str] = None) -> Dict[str, float]:
81
+ fn = getattr(self._backend, "feedback", None)
82
+ if fn is not None:
83
+ return fn(kind, action=action)
84
+ return {}
85
+
86
+ def learn_pick(self, candidates: List[str], base=None, **kw) -> str:
87
+ fn = getattr(self._backend, "learn_pick", None)
88
+ if fn is not None:
89
+ try:
90
+ return fn(candidates, base=base, **kw)
91
+ except TypeError:
92
+ try:
93
+ return fn(candidates)
94
+ except Exception:
95
+ pass
96
+ except Exception:
97
+ pass
98
+ import random
99
+ pool = [c for c in candidates if c]
100
+ return random.choice(pool) if pool else ""
101
+
102
+ def counts(self) -> Dict[str, int]:
103
+ fn = getattr(self._backend, "counts", None)
104
+ if fn is not None:
105
+ try:
106
+ return fn()
107
+ except Exception:
108
+ return {}
109
+ return {}
110
+
111
+
112
+ # ============================================================ 装配器(唯一变动点)
113
+
114
+ class CognitiveAssembler:
115
+ """把后端装配成应用可用的 ``CognitiveService``。
116
+
117
+ 这是 V1↔V2 的唯一变动点:
118
+
119
+ - ``CognitiveAssembler.v1(...)`` → 返回 V1 后端装配出的服务(当前默认)。
120
+ - ``CognitiveAssembler.v2(...)`` → (V2.0 落地时新增)返回 V2 后端装配出的服务。
121
+ 上层 ``BaseApplication`` / 4 智能体 / 3 技能只调用装配器,**不直接碰后端类型**,
122
+ 因此换引擎 = 改这一处工厂方法。
123
+ """
124
+
125
+ @classmethod
126
+ def wrap(
127
+ cls,
128
+ backend: CognitiveBackend,
129
+ domain: Optional[DomainAdapter] = None,
130
+ ) -> CognitiveService:
131
+ """把任意 CognitiveBackend 包成 CognitiveService。"""
132
+ return CognitiveService(backend, domain or NullDomainAdapter())
133
+
134
+ @classmethod
135
+ def v1(
136
+ cls,
137
+ persist_dir,
138
+ persona: Optional[dict] = None,
139
+ *,
140
+ use_core: bool = True,
141
+ stage: int = 0,
142
+ domain: Optional[DomainAdapter] = None,
143
+ ) -> CognitiveService:
144
+ """默认路径:V1 后端(core 优先,降级 light)装配成服务。"""
145
+ backend = create_v1_backend(
146
+ persist_dir, persona, use_core=use_core, stage=stage
147
+ )
148
+ return cls.wrap(backend, domain)
149
+
150
+ # —— V2.0 落地时在此新增(示例,先留接口不实现,避免提前引入 pasm_v2 依赖):
151
+ # @classmethod
152
+ # def v2(cls, persist_dir, persona=None, *, domain=None) -> CognitiveService:
153
+ # from pasm_skills.contrib.v2 import create_v2_backend
154
+ # return cls.wrap(create_v2_backend(persist_dir, persona), domain)
@@ -0,0 +1,174 @@
1
+ """SimpleApplication —— 让"最小可用应用"从 30 行降到 3 行。
2
+
3
+ ``BaseApplication`` 继承自 ``BaseAgent``,而 ``BaseAgent`` 是抽象基类,
4
+ 要求子类实现 ``action_pool()`` 与 ``_render_reply()``。对"我就想快速做个
5
+ 客服/问答/NPC"的人来说这是不必要的门槛 —— 本模块把这两个都给了默认实现,
6
+ 并额外提供 ``@capability`` 装饰器,让"加一个能力"= 加一个方法。
7
+
8
+ 三种用法,由简到繁
9
+ ------------------
10
+ 1. 最简(0 个方法)::
11
+
12
+ from pasm_framework import SimpleApplication
13
+ app = SimpleApplication("demo", {"name": "小智"})
14
+ print(app.ask("你好"))
15
+
16
+ 2. 自定义回复(传函数)::
17
+
18
+ app = SimpleApplication("demo", {"name": "小智"},
19
+ reply=lambda text, facts, mood: "你说的是:" + text)
20
+
21
+ 3. 加能力(装饰器)::
22
+
23
+ class Shop(SimpleApplication):
24
+ @capability(keywords=("退货", "退款"))
25
+ def refund(self, text):
26
+ return "退货请点这里:/refund"
27
+
28
+ Shop("shop", {"name": "客服"}).handle("我要退货") # → 能力命中
29
+ """
30
+ from __future__ import annotations
31
+
32
+ from typing import Any, Callable, Dict, List, Optional
33
+
34
+ from .application import BaseApplication
35
+ from .config import load
36
+ from .discovery import Capability
37
+
38
+ #: ``reply`` 回调签名:``(text, facts, mood) -> str``
39
+ ReplyFn = Callable[[str, List[Dict[str, Any]], float], str]
40
+
41
+
42
+ def capability(
43
+ name: Optional[str] = None,
44
+ keywords: Any = (),
45
+ *,
46
+ predicate: Optional[Callable[[str], bool]] = None,
47
+ description: str = "",
48
+ head_chars: int = 12,
49
+ ) -> Callable[[Callable[..., str]], Callable[..., str]]:
50
+ """把实例方法标记为一个能力(动作)。
51
+
52
+ 被标记的方法签名应为 ``def m(self, text: str) -> str``。
53
+ 命中后 ``text`` 会作为第一个位置参数传入(即它等价于 ``Capability.run``)。
54
+
55
+ ``keywords`` 只在**句首 head_chars 个字**内匹配(框架既有语义,防止
56
+ "我在想退货这事到底……"这种长句被误触发)。
57
+ """
58
+
59
+ def deco(fn: Callable[..., str]) -> Callable[..., str]:
60
+ fn.__pasm_capability__ = { # type: ignore[attr-defined]
61
+ "name": name or fn.__name__,
62
+ "keywords": tuple(keywords) if not isinstance(keywords, str)
63
+ else (keywords,),
64
+ "predicate": predicate,
65
+ "description": description,
66
+ "head_chars": head_chars,
67
+ }
68
+ return fn
69
+
70
+ return deco
71
+
72
+
73
+ class SimpleApplication(BaseApplication):
74
+ """带默认回复实现的通用应用底座。
75
+
76
+ 参数
77
+ ----
78
+ reply : ReplyFn | None
79
+ 自定义回复生成器;缺省用"就资料作答、查不到就如实说"的内置策略。
80
+ unknown : str | None
81
+ 检索不到资料时的兜底话术(**面向最终用户**,别写开发提示)。
82
+ backend_config : dict | BackendConfig | None
83
+ 插件开关表;也可用 ``preset=`` 直接指定场景预设。
84
+ preset : str | None
85
+ 场景预设名(见 :mod:`pasm_framework.config`)。给了 preset 才生效,
86
+ 与 ``backend_config`` 同时给时 ``backend_config`` 优先。
87
+ """
88
+
89
+ #: 检索不到资料时的默认话术(面向用户,不暴露"插件/配置"这类内部概念)。
90
+ DEFAULT_UNKNOWN = "抱歉,我暂时没有查到相关资料。你可以换个说法再问我,或联系人工客服。"
91
+
92
+ #: 子类由 ``__init_subclass__`` 自动填充:``[(method_name, spec), ...]``
93
+ _PASM_AUTO_CAPS: List[tuple] = []
94
+
95
+ def __init_subclass__(cls, **kw: Any) -> None:
96
+ super().__init_subclass__(**kw)
97
+ auto: List[tuple] = []
98
+ for attr, obj in vars(cls).items():
99
+ spec = getattr(obj, "__pasm_capability__", None)
100
+ if isinstance(spec, dict):
101
+ auto.append((attr, spec))
102
+ if auto:
103
+ cls._PASM_AUTO_CAPS = auto
104
+
105
+ def __init__(
106
+ self,
107
+ agent_id: str,
108
+ persona: Optional[Dict[str, Any]] = None,
109
+ *,
110
+ reply: Optional[ReplyFn] = None,
111
+ capabilities: Optional[List[Capability]] = None,
112
+ preset: Optional[str] = None,
113
+ unknown: Optional[str] = None,
114
+ backend_config: Optional[Any] = None,
115
+ **kwargs: Any,
116
+ ) -> None:
117
+ self._reply_fn = reply
118
+ self._unknown = unknown or self.DEFAULT_UNKNOWN
119
+ caps = list(capabilities or [])
120
+ # 把 @capability 标记过的方法转成 Capability 对象。
121
+ for method_name, spec in type(self)._PASM_AUTO_CAPS:
122
+ caps.append(Capability(
123
+ spec["name"],
124
+ run=self._make_runner(method_name),
125
+ keywords=spec["keywords"],
126
+ predicate=spec["predicate"],
127
+ description=spec["description"],
128
+ head_chars=spec["head_chars"],
129
+ ))
130
+ if backend_config is None and preset is not None:
131
+ backend_config = load(preset_name=preset)
132
+ super().__init__(
133
+ agent_id, persona or {"name": "助手"},
134
+ capabilities=caps or None,
135
+ backend_config=backend_config,
136
+ **kwargs,
137
+ )
138
+
139
+ @staticmethod
140
+ def _make_runner(method_name: str) -> Callable[..., str]:
141
+ def run(app: Any, text: str) -> str:
142
+ return getattr(app, method_name)(text)
143
+ return run
144
+
145
+ # ---- BaseAgent 抽象方法的默认实现 ----
146
+ def action_pool(self) -> List[str]: # noqa: D102
147
+ return ["reply"]
148
+
149
+ def _render_reply( # noqa: D102
150
+ self, text: str, facts: List[Dict[str, Any]], mood: float
151
+ ) -> str:
152
+ if self._reply_fn is not None:
153
+ return self._reply_fn(text, facts, mood)
154
+ # 默认策略:只依据"有来源的知识"作答,不编造。
155
+ knowledge = [f for f in (facts or []) if f.get("source")]
156
+ if knowledge:
157
+ top = knowledge[0]
158
+ body = (top.get("brief") or top.get("title") or "").strip()
159
+ return body or self._unknown
160
+ return self._unknown
161
+
162
+ # ---- 便捷方法 ----
163
+ def ask(self, text: str, session_id: str = "default",
164
+ user_id: Optional[str] = None) -> str:
165
+ """语义化别名:``handle`` 是框架内部叫法,``ask`` 是给人看的。"""
166
+ return self.handle(text, session_id=session_id, user_id=user_id)
167
+
168
+ def teach(self, items: List[Dict[str, Any]]) -> int:
169
+ """``ingest`` 的语义化别名(喂资料给知识库,返回新增条数)。
170
+
171
+ 两个名字等价,统一由 ``BaseApplication.ingest`` 实现;未启用
172
+ ``knowledge_base`` 时会抛 ``FrameworkError`` 指引开启方式。
173
+ """
174
+ return self.ingest(items)