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,282 @@
1
+ """命令行入口。
2
+
3
+ pasm-framework 自检 + 用法(等价 selftest)
4
+ pasm-framework selftest 框架自检
5
+ pasm-framework version 版本
6
+ pasm-framework plugins 列出内置插件与默认开关
7
+ pasm-framework config [--preset X | --file f.json] [--env] 查看解析后的开关表
8
+ pasm-framework doctor 环境体检(能不能跑、缺什么)
9
+ pasm-framework new <dir> [--kind app|chatbot|game_npc] [--force] 生成项目
10
+ pasm-framework serve [--port 8080] [--host 0.0.0.0] [--kb ./kb] 起一个演示客服
11
+
12
+ 设计约束:只用标准库 ``argparse``,不引入 CLI 框架 —— 保证"装完就能跑"。
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import argparse
17
+ import sys
18
+
19
+ USAGE = """pasm-framework —— PASM 应用开发框架
20
+
21
+ 用法:
22
+ pasm-framework selftest 框架自检
23
+ pasm-framework version 版本
24
+ pasm-framework plugins 列出内置插件与默认开关
25
+ pasm-framework config [--preset X] [--file f] 查看解析后的开关表
26
+ pasm-framework doctor 环境体检
27
+ pasm-framework new <目录> [--kind ...] 生成项目脚手架
28
+ pasm-framework serve [--port 8080] 跑一个演示智能客服
29
+
30
+ 新手建议顺序: new → 改 main.py → doctor → serve
31
+ 文档: docs/tutorials/(快速上手 / 插件 / 客服 / LLM / 部署 / 游戏 NPC / 多语言)"""
32
+
33
+
34
+ def _cmd_plugins(_args) -> int:
35
+ from .plugins.registry import builtin_plugins, default_config
36
+ cfg = default_config()
37
+ print("内置插件(共 %d 个):" % len(builtin_plugins()))
38
+ for name, cls in builtin_plugins().items():
39
+ on = "开" if cfg.entry(name).get("enabled") else "关"
40
+ doc = (cls.__doc__ or "").strip().splitlines()
41
+ brief = doc[0] if doc else ""
42
+ print(" [%s] %-16s %s" % (on, name, brief))
43
+ print("\n开关方式: pasm_framework.load(preset_name=..., **overrides)"
44
+ " 或环境变量 PASM_PLUGINS / PASM_KB_DIR / PASM_HTTP_PORT ...")
45
+ return 0
46
+
47
+
48
+ def _cmd_config(args) -> int:
49
+ from .config import describe, load, to_dict
50
+ cfg = load(args.file, preset_name=args.preset, env=args.env)
51
+ print("解析后的开关表(预设=%s 文件=%s 环境变量=%s):"
52
+ % (args.preset, args.file or "-", "是" if args.env else "否"))
53
+ print("\n".join(describe(cfg)))
54
+ if args.json:
55
+ import json
56
+ print(json.dumps(to_dict(cfg), ensure_ascii=False, indent=2))
57
+ return 0
58
+
59
+
60
+ def _cmd_doctor(_args) -> int:
61
+ """环境体检:把"跑不起来"的原因说清楚,而不是丢一个 traceback。"""
62
+ import platform
63
+ import tempfile
64
+ from pathlib import Path
65
+
66
+ ok = True
67
+
68
+ def line(good: bool, msg: str, hint: str = "") -> None:
69
+ nonlocal ok
70
+ if not good:
71
+ ok = False
72
+ print(" %s %s%s" % ("v" if good else "x", msg,
73
+ (" → %s" % hint) if (hint and not good) else ""))
74
+
75
+ print("pasm-framework doctor")
76
+ print("[1] 运行环境")
77
+ v = sys.version_info
78
+ line(v >= (3, 10), "Python %d.%d.%d(要求 >= 3.10)" % (v.major, v.minor, v.micro),
79
+ "升级 Python 到 3.10+")
80
+ line(True, "%s %s" % (platform.system(), platform.release()))
81
+
82
+ print("[2] 依赖")
83
+ try:
84
+ import pasm_skills # noqa: F401
85
+ from pasm_skills import __version__ as skv # type: ignore
86
+ line(True, "pasm-skills %s 可导入" % skv)
87
+ except Exception as ex: # noqa: BLE001
88
+ line(False, "pasm-skills 不可导入(%s)" % ex,
89
+ "pip install pasm-skills>=0.5.1")
90
+ from . import __version__
91
+ line(True, "pasm-framework %s" % __version__)
92
+
93
+ try:
94
+ from .plugins.registry import builtin_plugins
95
+ n = len(builtin_plugins())
96
+ line(n >= 7, "内置插件 %d 个可加载" % n, "重装 pasm-framework")
97
+ except Exception as ex: # noqa: BLE001
98
+ line(False, "插件库加载失败(%s)" % ex, "检查安装是否完整")
99
+
100
+ print("[3] 可写性")
101
+ try:
102
+ with tempfile.TemporaryDirectory() as td:
103
+ p = Path(td) / "probe.txt"
104
+ p.write_text("ok", encoding="utf-8")
105
+ line(True, "临时目录可写")
106
+ except Exception as ex: # noqa: BLE001
107
+ line(False, "临时目录不可写(%s)" % ex, "检查磁盘与权限")
108
+ try:
109
+ from .plugins.builtins.knowledge_base import KnowledgeBasePlugin
110
+ kd = Path(tempfile.mkdtemp()) / "kb"
111
+ kb = KnowledgeBasePlugin({"kb_dir": str(kd)})
112
+ kb.ingest([{"title": "probe", "content": "probe", "source": "doctor"}])
113
+ line(kb.stats()["total"] == 1, "知识库可读写(%s)" % kd)
114
+ except Exception as ex: # noqa: BLE001
115
+ line(False, "知识库不可用(%s)" % ex, "检查 PASM_KB_DIR 权限")
116
+
117
+ print("[4] 可选能力")
118
+ import os
119
+ provider = os.environ.get("PASM_LLM_PROVIDER")
120
+ key = (os.environ.get("PASM_LLM_API_KEY") or os.environ.get("OPENAI_API_KEY")
121
+ or os.environ.get("DEEPSEEK_API_KEY"))
122
+ if provider or key:
123
+ line(True, "LLM 已配置(provider=%s, key=%s)"
124
+ % (provider or "openai", "已设置" if key else "未设置"))
125
+ else:
126
+ line(True, "LLM 未配置 —— 不影响运行(走模板/资料库回复)")
127
+ try:
128
+ import yaml # noqa: F401
129
+ line(True, "pyyaml 可用(可读 .yaml 配置)")
130
+ except Exception: # noqa: BLE001
131
+ line(True, "pyyaml 未安装 —— 用 .json 配置即可")
132
+
133
+ print("[5] 插件装配")
134
+ try:
135
+ from .plugins.core import BasePlugin, build_manager
136
+ from .plugins.registry import discover_plugins
137
+
138
+ eps = discover_plugins()
139
+ line(True, "entry-point 第三方插件 %d 个(group=pasm_framework.plugins)" % len(eps))
140
+
141
+ class _Probe(BasePlugin):
142
+ name = "_doctor_probe"
143
+
144
+ def on_reply_final(self, ctx): # pragma: no cover - 仅探针
145
+ pass
146
+
147
+ pm = build_manager({"_doctor_probe": {"enabled": True, "class": _Probe}})
148
+ line("_doctor_probe" in pm.names(),
149
+ "内联自定义插件可注册(backend_config 里写 class=)",
150
+ "检查 BackendConfig 用法")
151
+ cfg_typo = build_manager({"knowlege_base": {"enabled": True}})
152
+ line(bool(cfg_typo.unknown()),
153
+ "拼错的插件名会被记录(示例:%s)" % (cfg_typo.unknown() or "-"),
154
+ "这行应为 v;若不是,unknown() 机制失效")
155
+ except Exception as ex: # noqa: BLE001
156
+ line(False, "插件装配探测失败(%s)" % ex, "检查 plugins 目录是否完整")
157
+
158
+ print("\n体检结论: %s" % ("全部通过,可以开始开发" if ok else "存在问题,见上面 x 项"))
159
+ if ok:
160
+ print("下一步: pasm-framework new myapp --kind chatbot")
161
+ return 0 if ok else 1
162
+
163
+
164
+ def _cmd_new(args) -> int:
165
+ from .scaffold import KINDS, create
166
+ try:
167
+ written = create(args.dir, kind=args.kind, name=args.name or "", force=args.force)
168
+ except Exception as ex: # noqa: BLE001
169
+ print("生成失败:%s" % ex)
170
+ return 1
171
+ print("已在 %s 生成项目(模板 %s):" % (args.dir, args.kind))
172
+ for rel in written:
173
+ print(" + %s" % rel)
174
+ print(" + kb/ (知识库目录)")
175
+ print("\n下一步:")
176
+ print(" cd %s" % args.dir)
177
+ print(" python main.py" + (" --serve" if args.kind == "chatbot" else ""))
178
+ print("\n其他模板: %s" % ", ".join(KINDS))
179
+ return 0
180
+
181
+
182
+ def _cmd_serve(args) -> int:
183
+ import time
184
+
185
+ from .demo import make_demo_app
186
+ llm = None
187
+ if args.llm_provider:
188
+ llm = {"provider": args.llm_provider}
189
+ if args.llm_model:
190
+ llm["model"] = args.llm_model
191
+ if args.llm_key:
192
+ llm["api_key"] = args.llm_key
193
+ app = make_demo_app(kb_dir=args.kb, llm=llm, host=args.host, port=args.port,
194
+ token=args.token)
195
+ print("演示智能客服已启动")
196
+ print(" 对话界面: http://%s:%d/" % (args.host if args.host != "0.0.0.0" else "127.0.0.1", args.port))
197
+ print(" REST 接口: POST /api/chat {\"text\": \"怎么退货?\"}")
198
+ print(" 健康检查: GET /healthz | 插件: GET /api/plugins")
199
+ if args.token:
200
+ print(" 已开启鉴权: 请求需带 Authorization: Bearer <token>")
201
+ print(" 嵌入站点: <iframe src='http://你的主机:%d/' width='420' height='620'></iframe>" % args.port)
202
+ print(" Ctrl+C 退出")
203
+ try:
204
+ app.serve()
205
+ while True:
206
+ time.sleep(1)
207
+ except KeyboardInterrupt:
208
+ print("\n已停止。")
209
+ app.close()
210
+ return 0
211
+
212
+
213
+ def main(argv=None) -> int:
214
+ argv = list(sys.argv[1:] if argv is None else argv)
215
+ cmd = argv[0] if argv else "selftest"
216
+
217
+ if cmd in ("selftest", "test", "self-test"):
218
+ from . import selftest
219
+ ok = selftest()
220
+ if not argv:
221
+ print()
222
+ print(USAGE)
223
+ return 0 if ok else 1
224
+ if cmd in ("version", "v", "--version"):
225
+ from . import __version__
226
+ print("pasm-framework %s" % __version__)
227
+ return 0
228
+ if cmd in ("help", "-h", "--help"):
229
+ print(USAGE)
230
+ return 0
231
+
232
+ parser = argparse.ArgumentParser(prog="pasm-framework", add_help=True,
233
+ description="PASM 应用开发框架 CLI")
234
+ sub = parser.add_subparsers(dest="sub")
235
+
236
+ sub.add_parser("plugins", help="列出内置插件与默认开关")
237
+ sub.add_parser("doctor", help="环境体检")
238
+
239
+ p_cfg = sub.add_parser("config", help="查看解析后的开关表")
240
+ p_cfg.add_argument("--preset", default=None,
241
+ help="场景预设: minimal/default/chatbot/game_npc/api/desktop")
242
+ p_cfg.add_argument("--file", default=None, help="配置文件 (.json/.yaml)")
243
+ p_cfg.add_argument("--env", action="store_true", help="叠加 PASM_* 环境变量")
244
+ p_cfg.add_argument("--json", action="store_true", help="同时打印 JSON")
245
+
246
+ p_new = sub.add_parser("new", help="生成项目脚手架")
247
+ p_new.add_argument("dir", help="目标目录")
248
+ p_new.add_argument("--kind", default="app",
249
+ help="模板: app / chatbot / game_npc(默认 app)")
250
+ p_new.add_argument("--name", default="", help="项目名(写进 README)")
251
+ p_new.add_argument("--force", action="store_true", help="覆盖已存在的同名文件")
252
+
253
+ p_srv = sub.add_parser("serve", help="跑一个演示智能客服")
254
+ p_srv.add_argument("--host", default="127.0.0.1")
255
+ p_srv.add_argument("--port", type=int, default=8080)
256
+ p_srv.add_argument("--kb", default=None, help="知识库目录(默认 ~/.pasm_framework/kb)")
257
+ p_srv.add_argument("--token", default=None, help="HTTP 鉴权令牌")
258
+ p_srv.add_argument("--llm-provider", default=None,
259
+ help="openai / deepseek / ollama(不填则离线模式)")
260
+ p_srv.add_argument("--llm-model", default=None)
261
+ p_srv.add_argument("--llm-key", default=None)
262
+
263
+ try:
264
+ ns = parser.parse_args(argv)
265
+ except SystemExit as ex: # argparse 的报错也是退出
266
+ return int(ex.code or 2)
267
+
268
+ handler = {
269
+ "plugins": _cmd_plugins,
270
+ "config": _cmd_config,
271
+ "doctor": _cmd_doctor,
272
+ "new": _cmd_new,
273
+ "serve": _cmd_serve,
274
+ }.get(ns.sub)
275
+ if handler is None:
276
+ print(USAGE)
277
+ return 2
278
+ return handler(ns)
279
+
280
+
281
+ if __name__ == "__main__":
282
+ raise SystemExit(main())
@@ -0,0 +1,80 @@
1
+ """DomainAdapter —— 领域知识 / 规则注入的契约。
2
+
3
+ 为什么单独抽一层
4
+ ----------------
5
+ NPC / 老人陪伴 / 学习陪伴 三个产品智能体现在把"领域逻辑"写死在各自的
6
+ ``BaseAgent`` 子类里(动作池语义、回复风格、约束规则全混在一起)。
7
+ 一旦要换引擎或加第四个领域,这些逻辑只能靠"读懂旧子类再抄"来维持。
8
+
9
+ ``DomainAdapter`` 把"这个领域知道什么、约束什么、怎么润色回复"抽成显式契约:
10
+ 每个产品提供一个 ``DomainAdapter`` 实现,``BaseApplication`` 只认契约、不认具体领域。
11
+ 这样领域逻辑可独立测试、可独立替换——又是一处 V1↔V2 不该动的稳定面。
12
+ """
13
+ from __future__ import annotations
14
+
15
+ from typing import Any, Dict, List, Protocol, runtime_checkable
16
+
17
+
18
+ @runtime_checkable
19
+ class DomainAdapter(Protocol):
20
+ """领域适配契约(应用侧只读、可选实现)。
21
+
22
+ 所有方法都是可选的:框架用 ``getattr``/``hasattr`` 探测,缺失就走兜底。
23
+ 这样一个"最小领域适配器"只需要实现 ``knowledge_for`` 即可。
24
+ """
25
+
26
+ def knowledge_for(self, query: str) -> List[Dict[str, Any]]:
27
+ """给定用户问句,返回该领域相关的结构化知识片段。
28
+
29
+ 返回结构与 ``CognitiveBackend.recall`` 对齐:
30
+ ``[{title, brief, tags, sal, ...}, ...]``,方便合并进检索结果。
31
+ """
32
+ ...
33
+
34
+ def constraints(self) -> Dict[str, Any]:
35
+ """该领域的硬性约束(如"不允许承诺医疗诊断"、"动作池白名单")。"""
36
+ ...
37
+
38
+ def augment_reply(self, text: str, reply: str, facts: List[Dict[str, Any]]) -> str:
39
+ """在 ``_render_reply`` 产出后,再做一次领域级润色/兜底。"""
40
+ ...
41
+
42
+
43
+ class NullDomainAdapter:
44
+ """空领域适配器:什么都不注入。
45
+
46
+ 框架的默认实现——保证"没有领域知识也能跑"(与 BaseAgent 的离线优先一致)。
47
+ 它**不**实现 ``knowledge_for`` 等方法,因此框架探测到缺失时走兜底,
48
+ 而不是假装有知识。
49
+ """
50
+
51
+ is_core = False
52
+
53
+
54
+ class StaticDomainAdapter:
55
+ """用一份静态知识表 + 简单关键词命中实现的最简 DomainAdapter。
56
+
57
+ 适合脚手架期快速验证"领域注入"链路,真实产品应替换为带检索的实现。
58
+ """
59
+
60
+ def __init__(self, knowledge: List[Dict[str, Any]], constraints: Dict[str, Any] | None = None):
61
+ self._knowledge = list(knowledge or [])
62
+ self._constraints = dict(constraints or {})
63
+
64
+ def knowledge_for(self, query: str) -> List[Dict[str, Any]]:
65
+ q = (query or "").strip()
66
+ if not q:
67
+ return []
68
+ hit = []
69
+ for item in self._knowledge:
70
+ blob = " ".join(str(item.get(k, "")) for k in ("title", "brief", "tags"))
71
+ if any(tok and tok in blob for tok in q):
72
+ hit.append(dict(item))
73
+ return hit
74
+
75
+ def constraints(self) -> Dict[str, Any]:
76
+ return dict(self._constraints)
77
+
78
+ def augment_reply(self, text: str, reply: str, facts: List[Dict[str, Any]]) -> str:
79
+ # 默认不润色;子类可覆盖。
80
+ return reply