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,319 @@
1
+ """配置系统 —— 让"后端选择是否使用某插件"能来自预设 / 文件 / 环境变量 / 代码。
2
+
3
+ 为什么需要
4
+ ----------
5
+ v0.2.0 的开关表只能写在 Python 代码里(``BaseApplication(backend_config={...})``),
6
+ 部署时想改一个端口、换一个模型,都得改代码再发版。本模块让同一份应用可以:
7
+
8
+ · 本地开发 —— ``load(preset="chatbot")``
9
+ · 部署上线 —— ``load("server.json", env=True)`` 或直接给环境变量
10
+ · 单元测试 —— ``load(preset="minimal")``(全关,行为退化为 v0.1.0)
11
+
12
+ 合并优先级(后者覆盖前者)
13
+ --------------------------
14
+ preset < file(source) < env < overrides(显式入参)
15
+
16
+ 只依赖标准库;``yaml`` 是可选加速项(装了就能读 ``.yaml/.yml``,没装则提示改用 JSON)。
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import copy
21
+ import json
22
+ import os
23
+ from pathlib import Path
24
+ from typing import Any, Dict, List, Optional, Union
25
+
26
+ from .errors import FrameworkError
27
+ from .plugins.core import BackendConfig
28
+
29
+ PLUGIN_NAMES: List[str] = [
30
+ "safety", "sessions", "knowledge_base", "warmth",
31
+ "observability", "llm_responder", "web_gateway",
32
+ ]
33
+
34
+
35
+ def _all(enabled: bool, **cfg) -> Dict[str, Any]:
36
+ return {"enabled": enabled, "config": dict(cfg)}
37
+
38
+
39
+ #: 场景预设。每个预设都**列全 7 个插件**——未列出的插件会被视为"关闭",
40
+ #: 写全可以避免"我明明没关它,它却没了"的困惑。
41
+ PRESETS: Dict[str, Dict[str, Dict[str, Any]]] = {
42
+ # 全关:行为退回 v0.1.0(能力路由 → 回落 chat)。测试与嵌入式场景用。
43
+ "minimal": {n: _all(False) for n in PLUGIN_NAMES},
44
+
45
+ # 与框架默认一致:基础/安全能力开,LLM 与网关关。
46
+ "default": {
47
+ "safety": _all(True, mode="warn"),
48
+ "sessions": _all(True),
49
+ "knowledge_base": _all(True),
50
+ "warmth": _all(True),
51
+ "observability": _all(True),
52
+ "llm_responder": _all(False),
53
+ "web_gateway": _all(False),
54
+ },
55
+
56
+ # 对外站点智能客服:护栏收紧为 block(面向公众,宁可拦错也别放行)。
57
+ "chatbot": {
58
+ "safety": _all(True, mode="block"),
59
+ "sessions": _all(True, max_history=20),
60
+ "knowledge_base": _all(True),
61
+ "warmth": _all(True),
62
+ "observability": _all(True),
63
+ "llm_responder": _all(False),
64
+ "web_gateway": _all(True, host="0.0.0.0", port=8080),
65
+ },
66
+
67
+ # 游戏 NPC:离线优先(不接 LLM),保留情绪润色与会话内记忆。
68
+ "game_npc": {
69
+ "safety": _all(True, mode="warn"),
70
+ "sessions": _all(True, max_history=12),
71
+ "knowledge_base": _all(True),
72
+ "warmth": _all(True),
73
+ "observability": _all(False),
74
+ "llm_responder": _all(False),
75
+ "web_gateway": _all(False),
76
+ },
77
+
78
+ # 纯 API 服务:回复要"干净"(不润色),护栏收紧,暴露 HTTP。
79
+ "api": {
80
+ "safety": _all(True, mode="block"),
81
+ "sessions": _all(True, max_history=20),
82
+ "knowledge_base": _all(True),
83
+ "warmth": _all(False),
84
+ "observability": _all(True),
85
+ "llm_responder": _all(False),
86
+ "web_gateway": _all(True, host="127.0.0.1", port=8080),
87
+ },
88
+
89
+ # 桌面应用内嵌:只监听本机,避免把服务暴露到局域网。
90
+ "desktop": {
91
+ "safety": _all(True, mode="warn"),
92
+ "sessions": _all(True, max_history=30),
93
+ "knowledge_base": _all(True),
94
+ "warmth": _all(True),
95
+ "observability": _all(False),
96
+ "llm_responder": _all(False),
97
+ "web_gateway": _all(True, host="127.0.0.1", port=8765),
98
+ },
99
+ }
100
+
101
+
102
+ def preset(name: str) -> Dict[str, Dict[str, Any]]:
103
+ """取一份场景预设的**副本**(改它不会污染全局预设)。"""
104
+ if name not in PRESETS:
105
+ raise FrameworkError(
106
+ "未知预设 %r;可用:%s" % (name, ", ".join(sorted(PRESETS)))
107
+ )
108
+ return copy.deepcopy(PRESETS[name])
109
+
110
+
111
+ def _deep_merge(base: Dict[str, Any], over: Dict[str, Any]) -> Dict[str, Any]:
112
+ """两层深合并:``{"a":{"enabled":True,"config":{"k":1}}}`` 级别的覆盖。
113
+
114
+ 只合并到 ``enabled`` / ``config`` 这一层,不再往下钻 ——
115
+ 这样"我只想改 config.port"不会把同插件其它 config 键抹掉。
116
+ """
117
+ out = copy.deepcopy(base)
118
+ for name, spec in (over or {}).items():
119
+ if not isinstance(spec, dict):
120
+ continue
121
+ cur = out.setdefault(name, {"enabled": False, "config": {}})
122
+ if "enabled" in spec:
123
+ cur["enabled"] = bool(spec["enabled"])
124
+ cfg = spec.get("config")
125
+ if isinstance(cfg, dict):
126
+ merged = dict(cur.get("config") or {})
127
+ merged.update(cfg)
128
+ cur["config"] = merged
129
+ return out
130
+
131
+
132
+ # ---------------------------------------------------------------- 文件
133
+
134
+ def from_file(path: Union[str, Path]) -> Dict[str, Dict[str, Any]]:
135
+ """从 JSON / YAML 读取开关表。
136
+
137
+ 期望结构(``plugins`` 键可省略,直接给插件名也行)::
138
+
139
+ {"plugins": {"web_gateway": {"enabled": true, "config": {"port": 9000}}}}
140
+ """
141
+ p = Path(path)
142
+ if not p.exists():
143
+ raise FrameworkError("配置文件不存在:%s" % p)
144
+ text = p.read_text(encoding="utf-8")
145
+ suffix = p.suffix.lower()
146
+ if suffix in (".yaml", ".yml"):
147
+ try:
148
+ import yaml # type: ignore
149
+ except Exception as ex: # noqa: BLE001
150
+ raise FrameworkError(
151
+ "读取 %s 需要 pyyaml(pip install pyyaml),或改用 .json 配置。" % p
152
+ ) from ex
153
+ data = yaml.safe_load(text) or {}
154
+ else:
155
+ try:
156
+ data = json.loads(text or "{}")
157
+ except Exception as ex: # noqa: BLE001
158
+ raise FrameworkError("配置文件不是合法 JSON:%s(%s)" % (p, ex)) from ex
159
+ if not isinstance(data, dict):
160
+ raise FrameworkError("配置文件顶层必须是对象:%s" % p)
161
+ plugins = data.get("plugins")
162
+ return plugins if isinstance(plugins, dict) else data
163
+
164
+
165
+ # ---------------------------------------------------------------- 环境变量
166
+
167
+ def from_env(prefix: str = "PASM_") -> Dict[str, Dict[str, Any]]:
168
+ """把环境变量翻译成开关表覆盖。
169
+
170
+ 支持(值非空才生效)::
171
+
172
+ PASM_PLUGINS=none|default|a,b,c 只启用列出的插件
173
+ PASM_KB_DIR=/data/kb 知识库目录
174
+ PASM_SAFETY_MODE=warn|block
175
+ PASM_LLM_PROVIDER=deepseek|openai|ollama
176
+ PASM_LLM_MODEL=deepseek-chat
177
+ PASM_LLM_BASE_URL=https://... 自建/代理网关
178
+ PASM_LLM_API_KEY=sk-... (也接受 OPENAI_API_KEY 等)
179
+ PASM_HTTP_HOST=0.0.0.0
180
+ PASM_HTTP_PORT=8080
181
+ PASM_HTTP_TOKEN=*** 网关 Bearer 鉴权令牌
182
+ """
183
+ e = os.environ
184
+ out: Dict[str, Dict[str, Any]] = {}
185
+
186
+ def put(plugin: str, key: str, value: Any, *, enable: bool = False) -> None:
187
+ spec = out.setdefault(plugin, {"config": {}})
188
+ if enable:
189
+ spec["enabled"] = True
190
+ spec["config"][key] = value
191
+
192
+ def g(name: str) -> Optional[str]:
193
+ v = e.get(prefix + name)
194
+ return v if v else None
195
+
196
+ raw_plugins = g("PLUGINS")
197
+ if raw_plugins is not None:
198
+ if raw_plugins.strip().lower() in ("none", "off", ""):
199
+ for n in PLUGIN_NAMES:
200
+ out[n] = {"enabled": False}
201
+ else:
202
+ keep = {s.strip() for s in raw_plugins.split(",") if s.strip()}
203
+ for n in PLUGIN_NAMES:
204
+ out[n] = {"enabled": n in keep}
205
+
206
+ if g("KB_DIR"):
207
+ put("knowledge_base", "kb_dir", g("KB_DIR"), enable=True)
208
+ if g("SAFETY_MODE"):
209
+ put("safety", "mode", g("SAFETY_MODE"), enable=True)
210
+
211
+ llm_provider = g("LLM_PROVIDER")
212
+ llm_key = g("LLM_API_KEY")
213
+ if llm_provider or llm_key or g("LLM_MODEL") or g("LLM_BASE_URL"):
214
+ # 只要配了任一项,就认为用户想开 LLM(默认关是为了"没配就别联网")。
215
+ if llm_provider:
216
+ put("llm_responder", "provider", llm_provider)
217
+ if g("LLM_MODEL"):
218
+ put("llm_responder", "model", g("LLM_MODEL"))
219
+ if g("LLM_BASE_URL"):
220
+ put("llm_responder", "base_url", g("LLM_BASE_URL"))
221
+ if llm_key:
222
+ put("llm_responder", "api_key", llm_key)
223
+ out.setdefault("llm_responder", {"config": {}})["enabled"] = True
224
+
225
+ if g("HTTP_HOST"):
226
+ put("web_gateway", "host", g("HTTP_HOST"))
227
+ if g("HTTP_PORT"):
228
+ try:
229
+ put("web_gateway", "port", int(g("HTTP_PORT") or 8080))
230
+ except ValueError:
231
+ raise FrameworkError("PASM_HTTP_PORT 必须是整数:%r" % g("HTTP_PORT"))
232
+ if g("HTTP_TOKEN"):
233
+ put("web_gateway", "token", g("HTTP_TOKEN"))
234
+ if any(k in out.get("web_gateway", {}).get("config", {})
235
+ for k in ("host", "port", "token")):
236
+ out.setdefault("web_gateway", {"config": {}})["enabled"] = True
237
+
238
+ return out
239
+
240
+
241
+ # ---------------------------------------------------------------- 统一入口
242
+
243
+ def load(
244
+ source: Optional[Union[str, Path, Dict[str, Any]]] = None,
245
+ *,
246
+ preset_name: Optional[str] = None,
247
+ env: bool = False,
248
+ env_prefix: str = "PASM_",
249
+ **overrides: Any,
250
+ ) -> BackendConfig:
251
+ """把预设 / 文件 / 环境变量 / 显式覆盖合成一个 :class:`BackendConfig`。
252
+
253
+ 参数
254
+ ----
255
+ source : dict | path | None
256
+ 开关表本体;传路径则按后缀读 JSON/YAML。
257
+ preset_name : str | None
258
+ 场景预设名(``minimal`` / ``default`` / ``chatbot`` / ``game_npc`` /
259
+ ``api`` / ``desktop``)。缺省 = ``default``。
260
+ env : bool
261
+ 是否叠加环境变量。
262
+ **overrides
263
+ 单个插件的覆盖,如 ``load("api", web_gateway={"enabled": True,
264
+ "config": {"port": 9000}})``。
265
+
266
+ 用法::
267
+
268
+ from pasm_framework import load
269
+ cfg = load(preset_name="chatbot", env=True, web_gateway={
270
+ "enabled": True, "config": {"port": 9000}})
271
+ app = MyApp("demo", {"name": "小智"}, backend_config=cfg)
272
+ """
273
+ data = preset(preset_name or "default")
274
+ if source is not None:
275
+ file_data = source if isinstance(source, dict) else from_file(source)
276
+ data = _deep_merge(data, file_data)
277
+ if env:
278
+ data = _deep_merge(data, from_env(env_prefix))
279
+ if overrides:
280
+ data = _deep_merge(data, overrides)
281
+ return BackendConfig(plugins=data)
282
+
283
+
284
+ def to_dict(cfg: BackendConfig) -> Dict[str, Any]:
285
+ """把 ``BackendConfig`` 还原成可序列化的 dict(落盘 / 展示用)。"""
286
+ plugins: Dict[str, Any] = {}
287
+ for name in PLUGIN_NAMES:
288
+ e = cfg.plugins.get(name)
289
+ if e is None:
290
+ continue
291
+ plugins[name] = {"enabled": bool(e.get("enabled", False)),
292
+ "config": dict(e.get("config") or {})}
293
+ return {"plugins": plugins}
294
+
295
+
296
+ def save(cfg: BackendConfig, path: Union[str, Path]) -> Path:
297
+ """写出一份 JSON 配置(可作为部署模板)。"""
298
+ p = Path(path)
299
+ p.parent.mkdir(parents=True, exist_ok=True)
300
+ p.write_text(json.dumps(to_dict(cfg), ensure_ascii=False, indent=2) + "\n",
301
+ encoding="utf-8")
302
+ return p
303
+
304
+
305
+ def describe(cfg: BackendConfig) -> List[str]:
306
+ """生成人类可读的开关清单(``doctor`` / CLI 用)。"""
307
+ lines: List[str] = []
308
+ for name in PLUGIN_NAMES:
309
+ e = cfg.plugins.get(name) or {"enabled": False, "config": {}}
310
+ on = "开" if e.get("enabled") else "关"
311
+ cfgd = e.get("config") or {}
312
+ detail = ""
313
+ if cfgd:
314
+ # 密钥类只显示"已设置",不泄露内容
315
+ safe = {k: ("***" if "key" in k or "token" in k else v)
316
+ for k, v in cfgd.items()}
317
+ detail = " " + json.dumps(safe, ensure_ascii=False)
318
+ lines.append(" [%s] %-16s%s" % (on, name, detail))
319
+ return lines
pasm_framework/demo.py ADDED
@@ -0,0 +1,85 @@
1
+ """开箱即用的演示应用 —— 让 ``pasm-framework serve`` 一条命令就能跑起来。
2
+
3
+ 存在的意义
4
+ ----------
5
+ "即插即用"最直观的证明是:装完包,一条命令,浏览器里就能对话。
6
+ 本模块提供一个**零 LLM 也能回答**的智能客服实例(靠 ``knowledge_base``
7
+ 就资料作答),供 CLI 的 ``serve`` 子命令与教程使用。
8
+
9
+ 它不是生产代码 —— 生产请参考仓库 ``apps/customer_service.py`` 的完整形态。
10
+ """
11
+ from __future__ import annotations
12
+
13
+ from typing import Any, Dict, List, Optional
14
+
15
+ from .config import load
16
+ from .simple import SimpleApplication
17
+
18
+ #: 演示资料:故意覆盖"能答 / 答不上来"两类问题,方便观察护栏与兜底行为。
19
+ DEMO_FAQ: List[Dict[str, Any]] = [
20
+ {"title": "退货政策",
21
+ "content": "商品签收后 7 天内可无理由退货,需保持吊牌完整、不影响二次销售。",
22
+ "source": "faq", "tags": ["退货", "退款", "售后"]},
23
+ {"title": "换货流程",
24
+ "content": "在订单详情页点「申请换货」,选择尺码/颜色后寄回,我们收到当天发出新品。",
25
+ "source": "faq", "tags": ["换货", "售后"]},
26
+ {"title": "配送时效",
27
+ "content": "现货 24 小时内发货;偏远地区 3-5 天送达;支持顺丰加急。",
28
+ "source": "faq", "tags": ["配送", "物流", "发货"]},
29
+ {"title": "运费说明",
30
+ "content": "单笔满 99 元包邮,未满收取 8 元运费;退货运费由我方承担。",
31
+ "source": "faq", "tags": ["运费", "包邮"]},
32
+ {"title": "发票",
33
+ "content": "下单时可勾选电子发票,次日发送至注册邮箱;专票需联系客服登记。",
34
+ "source": "faq", "tags": ["发票", "开票"]},
35
+ {"title": "营业时间",
36
+ "content": "在线客服时间为每天 9:00-21:00,节假日不休。",
37
+ "source": "faq", "tags": ["时间", "客服"]},
38
+ ]
39
+
40
+
41
+ class DemoAgent(SimpleApplication):
42
+ """演示用智能客服(话术面向终端用户)。"""
43
+
44
+ DEFAULT_UNKNOWN = "抱歉,我暂时没有查到相关资料。你可以换个说法再问我,或联系人工客服。"
45
+
46
+
47
+ def make_demo_app(
48
+ *,
49
+ kb_dir: Optional[str] = None,
50
+ llm: Optional[Dict[str, Any]] = None,
51
+ host: str = "127.0.0.1",
52
+ port: int = 8080,
53
+ agent_id: str = "pasm-demo",
54
+ preset_name: str = "chatbot",
55
+ token: Optional[str] = None,
56
+ teach: bool = True,
57
+ ) -> DemoAgent:
58
+ """构造演示应用(已导入 :data:`DEMO_FAQ`)。
59
+
60
+ >>> from pasm_framework.demo import make_demo_app
61
+ >>> app = make_demo_app(port=8099)
62
+ >>> "退货" in app.ask("怎么退货?")
63
+ True
64
+ """
65
+ gw: Dict[str, Any] = {"enabled": True, "config": {"host": host, "port": port}}
66
+ if token:
67
+ gw["config"]["token"] = token
68
+ kb: Dict[str, Any] = {"enabled": True}
69
+ if kb_dir:
70
+ kb["config"] = {"kb_dir": kb_dir}
71
+ cfg = load(
72
+ preset_name=preset_name,
73
+ knowledge_base=kb,
74
+ web_gateway=gw,
75
+ llm_responder={"enabled": bool(llm), "config": dict(llm or {})},
76
+ )
77
+ app = DemoAgent(
78
+ agent_id,
79
+ {"name": "小智", "role": "智能客服", "tone": "温暖、专业、耐心",
80
+ "temper": 0.6, "energy": 0.5, "play": 0.4},
81
+ backend_config=cfg,
82
+ )
83
+ if teach:
84
+ app.teach(DEMO_FAQ)
85
+ return app
@@ -0,0 +1,91 @@
1
+ """CapabilityDiscovery —— 应用"能力"的声明与发现。
2
+
3
+ 为什么单独抽一层
4
+ ----------------
5
+ 产品智能体当前用一堆散落的 ``if chip == "ad"`` / ``_MAP`` / ``ADAPTERS`` 硬编码分支
6
+ 来"识别用户想要什么能力"(广告设计、图文、视频…)。一个能力常有多套入口,
7
+ 改一套 ≠ 改完(见项目记忆:广告设计至少三处入口)。
8
+
9
+ ``CapabilityDiscovery`` 把"能力"变成一等公民:
10
+
11
+ - 每个能力 = ``Capability(name, match, run)``,自带匹配规则与执行体;
12
+ - 应用启动时报到注册,运行时按用户输入做**统一**的能力发现;
13
+ - 匹配规则默认"句首 N 字 + 关键词",但可逐能力覆盖。
14
+
15
+ 这样新增/删除/排查能力都只动注册表,不动散落分支——又是一处稳定的框架面。
16
+ (注:默认"只匹配句首 12 字"是产品侧故意的设计,这里保留为可调参数,
17
+ 不是 bug;见项目记忆。)
18
+ """
19
+ from __future__ import annotations
20
+
21
+ from dataclasses import dataclass
22
+ from typing import Callable, Dict, List, Optional, Tuple
23
+
24
+
25
+ # 默认只匹配句首这么多字符——产品侧有意保留(防止长句里误命中)。
26
+ DEFAULT_HEAD_CHARS = 12
27
+
28
+
29
+ @dataclass
30
+ class Capability:
31
+ """一个可被用户触发的"能力"。
32
+
33
+ ``match`` 决定何时命中;``run(app, text) -> str`` 是执行体,
34
+ 接收应用实例与原始输入,返回回复文本。
35
+ """
36
+
37
+ name: str
38
+ run: Callable[..., str] # 执行体:run(app, text) -> str
39
+ # 匹配规则(任选其一或全部):关键词列表 / 自定义判定 / 句首字数。
40
+ keywords: Tuple[str, ...] = ()
41
+ head_chars: int = DEFAULT_HEAD_CHARS
42
+ predicate: Optional[Callable[[str], bool]] = None
43
+ description: str = ""
44
+
45
+
46
+ class CapabilityDiscovery:
47
+ """能力注册表 + 统一发现。
48
+
49
+ 用法:
50
+ cd = CapabilityDiscovery()
51
+ cd.register(Capability("广告设计", run=my_ad, keywords=("广告", "海报")))
52
+ cap = cd.match("帮我设计一张广告") # -> 该 Capability 或 None
53
+ """
54
+
55
+ def __init__(self, capabilities: Optional[List[Capability]] = None) -> None:
56
+ self._caps: Dict[str, Capability] = {}
57
+ for c in (capabilities or []):
58
+ self.register(c)
59
+
60
+ def register(self, cap: Capability) -> None:
61
+ if not isinstance(cap, Capability):
62
+ raise TypeError("CapabilityDiscovery 只接受 Capability,收到 %r" % type(cap).__name__)
63
+ self._caps[cap.name] = cap
64
+
65
+ def names(self) -> List[str]:
66
+ return list(self._caps.keys())
67
+
68
+ def has(self, name: str) -> bool:
69
+ return name in self._caps
70
+
71
+ def get(self, name: str) -> Optional[Capability]:
72
+ return self._caps.get(name)
73
+
74
+ def match(self, text: str) -> Optional[Capability]:
75
+ """按规则匹配能力。
76
+
77
+ 顺序:先 ``predicate``(精确判定),再 ``keywords``(句首 head_chars 命中)。
78
+ 命中多个时返回**最先注册**的那一个(确定性,避免随机)。
79
+ """
80
+ t = (text or "").strip()
81
+ if not t:
82
+ return None
83
+ for cap in self._caps.values():
84
+ if cap.predicate is not None and cap.predicate(t):
85
+ return cap
86
+ for cap in self._caps.values():
87
+ if cap.keywords:
88
+ span = t[: max(cap.head_chars, 1)]
89
+ if any(k and k in span for k in cap.keywords):
90
+ return cap
91
+ return None
@@ -0,0 +1,26 @@
1
+ """框架异常定义。
2
+
3
+ 框架层只抛这一类错误,方便应用层统一捕获;引擎内部异常(pasm.*)
4
+ 一律在后端里被吞掉、降级,不向上污染框架。
5
+ """
6
+ from __future__ import annotations
7
+
8
+
9
+ class FrameworkError(Exception):
10
+ """框架层统一异常基类。"""
11
+
12
+
13
+ class SurfaceMissing(FrameworkError):
14
+ """某个框架表面(接口)在运行时找不到实现。
15
+
16
+ 典型场景:``BaseApplication`` 期望 ``DomainAdapter`` 提供了某个方法,
17
+ 但实际注入的是 ``NullDomainAdapter``。用于把"静默降级"变成"明确的红灯"。
18
+ """
19
+
20
+
21
+ class BackendContractBroken(FrameworkError):
22
+ """后端没有实现 ``CognitiveBackend`` 协议要求的成员。
23
+
24
+ surface-guard 会专门抓这一类——只要 V1/V2 后端动了契约,
25
+ CI 立刻红,而不是让产品智能体在用户机器上崩。
26
+ """
@@ -0,0 +1,42 @@
1
+ """pasm-framework 插件子系统(即插即用)。
2
+
3
+ 公共 API:
4
+ · ``Message`` / ``PluginContext`` —— 链上消息与上下文
5
+ · ``BasePlugin`` / ``Plugin`` —— 插件基类 / 契约
6
+ · ``PluginManager`` —— Hook 链编排
7
+ · ``BackendConfig`` / ``build_manager``/ ``default_config`` —— 后端开关与装配
8
+ · ``builtin_plugins`` / ``discover_plugins`` —— 内置与第三方插件发现
9
+
10
+ 典型用法见 ``apps/customer_service.py`` 与仓库 README。
11
+ """
12
+ from __future__ import annotations
13
+
14
+ from .core import (
15
+ BackendConfig,
16
+ BasePlugin,
17
+ HOOKS,
18
+ Message,
19
+ Plugin,
20
+ PluginContext,
21
+ PluginManager,
22
+ build_manager,
23
+ default_config,
24
+ )
25
+ from .registry import (
26
+ builtin_plugins,
27
+ discover_plugins,
28
+ )
29
+
30
+ __all__ = [
31
+ "Message",
32
+ "PluginContext",
33
+ "BasePlugin",
34
+ "Plugin",
35
+ "PluginManager",
36
+ "BackendConfig",
37
+ "build_manager",
38
+ "default_config",
39
+ "HOOKS",
40
+ "builtin_plugins",
41
+ "discover_plugins",
42
+ ]
@@ -0,0 +1,30 @@
1
+ """pasm-framework 内置插件库(即插即用)。
2
+
3
+ 7 个 builtin 插件,全部零强制外部依赖,可在后端配置中按名开关:
4
+ · ``sessions`` 会话 / 租户隔离
5
+ · ``knowledge_base`` 站点数据自学资料库(智能客服核心)
6
+ · ``llm_responder`` 可选 LLM 接入(OpenAI / DeepSeek / Ollama)
7
+ · ``warmth`` 回复温度 / 情感润色
8
+ · ``safety`` 护栏(注入拦截 + PII 脱敏)
9
+ · ``observability`` 可观测(指标 + 健康)
10
+ · ``web_gateway`` 零依赖 HTTP 网关(iframe / REST)
11
+ """
12
+ from __future__ import annotations
13
+
14
+ from .knowledge_base import KnowledgeBasePlugin
15
+ from .llm_responder import LLMResponderPlugin
16
+ from .observability import ObservabilityPlugin
17
+ from .safety import SafetyPlugin
18
+ from .sessions import SessionsPlugin
19
+ from .warmth import WarmthPlugin
20
+ from .web_gateway import WebGatewayPlugin
21
+
22
+ __all__ = [
23
+ "SessionsPlugin",
24
+ "KnowledgeBasePlugin",
25
+ "LLMResponderPlugin",
26
+ "WarmthPlugin",
27
+ "SafetyPlugin",
28
+ "ObservabilityPlugin",
29
+ "WebGatewayPlugin",
30
+ ]