pasm-framework 0.3.0__tar.gz → 0.4.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.
- {pasm_framework-0.3.0 → pasm_framework-0.4.0}/PKG-INFO +48 -5
- {pasm_framework-0.3.0 → pasm_framework-0.4.0}/README.md +46 -2
- {pasm_framework-0.3.0 → pasm_framework-0.4.0}/pasm_framework/__init__.py +40 -1
- {pasm_framework-0.3.0 → pasm_framework-0.4.0}/pasm_framework/__main__.py +86 -1
- {pasm_framework-0.3.0 → pasm_framework-0.4.0}/pasm_framework/application.py +37 -5
- {pasm_framework-0.3.0 → pasm_framework-0.4.0}/pasm_framework/demo.py +6 -0
- pasm_framework-0.4.0/pasm_framework/plugins/builtins/_web_assets.py +509 -0
- {pasm_framework-0.3.0 → pasm_framework-0.4.0}/pasm_framework/plugins/builtins/knowledge_base.py +51 -1
- {pasm_framework-0.3.0 → pasm_framework-0.4.0}/pasm_framework/plugins/builtins/sessions.py +22 -0
- {pasm_framework-0.3.0 → pasm_framework-0.4.0}/pasm_framework/plugins/builtins/web_gateway.py +292 -18
- {pasm_framework-0.3.0 → pasm_framework-0.4.0}/pasm_framework.egg-info/PKG-INFO +48 -5
- {pasm_framework-0.3.0 → pasm_framework-0.4.0}/pasm_framework.egg-info/SOURCES.txt +1 -0
- {pasm_framework-0.3.0 → pasm_framework-0.4.0}/pyproject.toml +5 -4
- {pasm_framework-0.3.0 → pasm_framework-0.4.0}/LICENSE +0 -0
- {pasm_framework-0.3.0 → pasm_framework-0.4.0}/pasm_framework/adapter.py +0 -0
- {pasm_framework-0.3.0 → pasm_framework-0.4.0}/pasm_framework/config.py +0 -0
- {pasm_framework-0.3.0 → pasm_framework-0.4.0}/pasm_framework/discovery.py +0 -0
- {pasm_framework-0.3.0 → pasm_framework-0.4.0}/pasm_framework/errors.py +0 -0
- {pasm_framework-0.3.0 → pasm_framework-0.4.0}/pasm_framework/plugins/__init__.py +0 -0
- {pasm_framework-0.3.0 → pasm_framework-0.4.0}/pasm_framework/plugins/builtins/__init__.py +0 -0
- {pasm_framework-0.3.0 → pasm_framework-0.4.0}/pasm_framework/plugins/builtins/llm_responder.py +0 -0
- {pasm_framework-0.3.0 → pasm_framework-0.4.0}/pasm_framework/plugins/builtins/observability.py +0 -0
- {pasm_framework-0.3.0 → pasm_framework-0.4.0}/pasm_framework/plugins/builtins/safety.py +0 -0
- {pasm_framework-0.3.0 → pasm_framework-0.4.0}/pasm_framework/plugins/builtins/warmth.py +0 -0
- {pasm_framework-0.3.0 → pasm_framework-0.4.0}/pasm_framework/plugins/core.py +0 -0
- {pasm_framework-0.3.0 → pasm_framework-0.4.0}/pasm_framework/plugins/registry.py +0 -0
- {pasm_framework-0.3.0 → pasm_framework-0.4.0}/pasm_framework/py.typed +0 -0
- {pasm_framework-0.3.0 → pasm_framework-0.4.0}/pasm_framework/scaffold.py +0 -0
- {pasm_framework-0.3.0 → pasm_framework-0.4.0}/pasm_framework/service.py +0 -0
- {pasm_framework-0.3.0 → pasm_framework-0.4.0}/pasm_framework/simple.py +0 -0
- {pasm_framework-0.3.0 → pasm_framework-0.4.0}/pasm_framework/skill.py +0 -0
- {pasm_framework-0.3.0 → pasm_framework-0.4.0}/pasm_framework.egg-info/dependency_links.txt +0 -0
- {pasm_framework-0.3.0 → pasm_framework-0.4.0}/pasm_framework.egg-info/entry_points.txt +0 -0
- {pasm_framework-0.3.0 → pasm_framework-0.4.0}/pasm_framework.egg-info/requires.txt +0 -0
- {pasm_framework-0.3.0 → pasm_framework-0.4.0}/pasm_framework.egg-info/top_level.txt +0 -0
- {pasm_framework-0.3.0 → pasm_framework-0.4.0}/setup.cfg +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pasm-framework
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: PASM 应用开发框架 —— 产品智能体/应用与认知引擎之间的防腐层(V1↔V2 唯一变动点)
|
|
5
5
|
Author: arronzheng
|
|
6
|
-
License: MIT
|
|
6
|
+
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://gitee.com/arronzheng/pasm-framework
|
|
8
8
|
Project-URL: Repository, https://github.com/arronJack/pasm-framework
|
|
9
9
|
Project-URL: Source, https://gitee.com/arronzheng/pasm-framework
|
|
@@ -11,7 +11,6 @@ Project-URL: Issues, https://gitee.com/arronzheng/pasm-framework/issues
|
|
|
11
11
|
Keywords: pasm,agent,application-framework,anti-corruption-layer,cognitive,capability,skill,domain-adapter
|
|
12
12
|
Classifier: Development Status :: 3 - Alpha
|
|
13
13
|
Classifier: Intended Audience :: Developers
|
|
14
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
15
14
|
Classifier: Operating System :: OS Independent
|
|
16
15
|
Classifier: Programming Language :: Python :: 3
|
|
17
16
|
Classifier: Programming Language :: Python :: 3.10
|
|
@@ -54,7 +53,7 @@ PASM V1 → V2 升级时,**不重写 4 个产品智能体 + 3 个技能**。
|
|
|
54
53
|
|
|
55
54
|
`CognitiveBackend` 协议的**单一真相源仍在基座** `pasm_skills.sdk.backend`,本包只做重导出。
|
|
56
55
|
|
|
57
|
-
## 流式与工具调用(v0.3.0
|
|
56
|
+
## 流式与工具调用(v0.3.0)
|
|
58
57
|
|
|
59
58
|
**真流式(SSE)** —— `stream()` 与 `handle()` **共用同一条管线**,不会行为漂移:
|
|
60
59
|
|
|
@@ -163,6 +162,48 @@ sla = "my_pkg.my_module:SlaPlugin"
|
|
|
163
162
|
⚠️ 插件名**拼错不会静默通过**:`app.plugins.unknown()`、`app.app_summary()["plugin_config_unknown"]`
|
|
164
163
|
以及 `pasm-framework doctor` 都会报告。
|
|
165
164
|
|
|
165
|
+
## 一键起在线客服(v0.4.0)
|
|
166
|
+
|
|
167
|
+
**一条命令**拿到:访客对话页 + 站长管理台 + 一行嵌入代码 + REST 示例(双令牌自动生成)。
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
pasm-framework cs --port 8080 --name "小智客服"
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
输出会直接给你三段:
|
|
174
|
+
|
|
175
|
+
```
|
|
176
|
+
① 访客对话页 http://127.0.0.1:8080/
|
|
177
|
+
② 站长管理台 http://127.0.0.1:8080/console?token=<管理令牌>
|
|
178
|
+
③ 嵌入任意语言站点 <script src=".../embed.js" data-pasm-token="<公开令牌>" ...></script>
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
**站点接入只要一行**(PHP / Java / Node / 静态页都行):
|
|
182
|
+
|
|
183
|
+
```html
|
|
184
|
+
<script src="http://你的服务器:8080/embed.js"
|
|
185
|
+
data-pasm-token="公开令牌"
|
|
186
|
+
data-title="在线客服"
|
|
187
|
+
data-greeting="你好!我是智能客服,有什么可以帮你?"
|
|
188
|
+
data-color="#2563eb" defer></script>
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
> ⚠️ 属性名必须是 **`data-pasm-token`**(写成 `data-token` 挂件拿不到令牌 → 401)。
|
|
192
|
+
|
|
193
|
+
**双令牌作用域**(安全红线,selftest 已固化):
|
|
194
|
+
|
|
195
|
+
| 令牌 | 能做什么 |
|
|
196
|
+
| --- | --- |
|
|
197
|
+
| `token`(管理) | 全部接口:对话 + 导入资料 + 看会话 + 管理台 |
|
|
198
|
+
| `public_token`(访客) | **仅** `/api/chat`、`/api/chat/stream`;配了它时挂件页对访客开放 |
|
|
199
|
+
|
|
200
|
+
`serve_token` **只返回 `public_token`,绝不回落管理令牌** —— 挂件页是给访客看的,
|
|
201
|
+
一旦回落,源码里就能读到后台令牌。未配 `public_token` 时挂件页本身也不公开。
|
|
202
|
+
`/embed.js` 与 `/healthz` 是唯一无条件公开的路径(本身不含密钥)。
|
|
203
|
+
|
|
204
|
+
管理台里可以**粘贴整篇帮助文档**(`POST /api/ingest/text`)自动切块、识别「问:/答:」存成问答对,
|
|
205
|
+
访客立刻就能问到 —— 这就是"资料库自学"的入口。
|
|
206
|
+
|
|
166
207
|
## 参考实现:站点智能客服
|
|
167
208
|
|
|
168
209
|
`apps/customer_service.py` 用插件组合出一个可上线的客服 Agent(零 LLM 也能回答)。
|
|
@@ -258,7 +299,7 @@ print(app.handle("你好")) # → "reply:你好"(回落 chat)
|
|
|
258
299
|
## 快速自检
|
|
259
300
|
|
|
260
301
|
```bash
|
|
261
|
-
python -m pasm_framework selftest #
|
|
302
|
+
python -m pasm_framework selftest # 55 项
|
|
262
303
|
python -m pasm_framework doctor # 真装配一遍插件并报拼错的名字
|
|
263
304
|
python -m pasm_framework version
|
|
264
305
|
```
|
|
@@ -273,6 +314,8 @@ python -m pasm_framework version
|
|
|
273
314
|
| [`docs/cross-platform-strategy.md`](docs/cross-platform-strategy.md) | 桌面端跨 Linux/macOS、手机端换架构方案 |
|
|
274
315
|
| [`docs/customer-service-plugin.md`](docs/customer-service-plugin.md) | 智能客服可行性与部署形态研究 |
|
|
275
316
|
| [`docs/audit-2026-09-20.md`](docs/audit-2026-09-20.md) | v0.2.0 体检报告 |
|
|
317
|
+
| [`docs/plugin-ownership.md`](docs/plugin-ownership.md) | 插件放子目录还是独立仓(决策 + 理由) |
|
|
318
|
+
| [`docs/audit-7repos-2026-09-20.md`](docs/audit-7repos-2026-09-20.md) | **七仓全盘审计**(含跨平台实测) |
|
|
276
319
|
| [`docs/openapi.yaml`](docs/openapi.yaml) | HTTP 契约(单一真相源) |
|
|
277
320
|
| [`sdks/`](sdks/README.md) | C# / Java / PHP / Node / Go / Python 客户端 |
|
|
278
321
|
|
|
@@ -26,7 +26,7 @@ PASM V1 → V2 升级时,**不重写 4 个产品智能体 + 3 个技能**。
|
|
|
26
26
|
|
|
27
27
|
`CognitiveBackend` 协议的**单一真相源仍在基座** `pasm_skills.sdk.backend`,本包只做重导出。
|
|
28
28
|
|
|
29
|
-
## 流式与工具调用(v0.3.0
|
|
29
|
+
## 流式与工具调用(v0.3.0)
|
|
30
30
|
|
|
31
31
|
**真流式(SSE)** —— `stream()` 与 `handle()` **共用同一条管线**,不会行为漂移:
|
|
32
32
|
|
|
@@ -135,6 +135,48 @@ sla = "my_pkg.my_module:SlaPlugin"
|
|
|
135
135
|
⚠️ 插件名**拼错不会静默通过**:`app.plugins.unknown()`、`app.app_summary()["plugin_config_unknown"]`
|
|
136
136
|
以及 `pasm-framework doctor` 都会报告。
|
|
137
137
|
|
|
138
|
+
## 一键起在线客服(v0.4.0)
|
|
139
|
+
|
|
140
|
+
**一条命令**拿到:访客对话页 + 站长管理台 + 一行嵌入代码 + REST 示例(双令牌自动生成)。
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
pasm-framework cs --port 8080 --name "小智客服"
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
输出会直接给你三段:
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
① 访客对话页 http://127.0.0.1:8080/
|
|
150
|
+
② 站长管理台 http://127.0.0.1:8080/console?token=<管理令牌>
|
|
151
|
+
③ 嵌入任意语言站点 <script src=".../embed.js" data-pasm-token="<公开令牌>" ...></script>
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
**站点接入只要一行**(PHP / Java / Node / 静态页都行):
|
|
155
|
+
|
|
156
|
+
```html
|
|
157
|
+
<script src="http://你的服务器:8080/embed.js"
|
|
158
|
+
data-pasm-token="公开令牌"
|
|
159
|
+
data-title="在线客服"
|
|
160
|
+
data-greeting="你好!我是智能客服,有什么可以帮你?"
|
|
161
|
+
data-color="#2563eb" defer></script>
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
> ⚠️ 属性名必须是 **`data-pasm-token`**(写成 `data-token` 挂件拿不到令牌 → 401)。
|
|
165
|
+
|
|
166
|
+
**双令牌作用域**(安全红线,selftest 已固化):
|
|
167
|
+
|
|
168
|
+
| 令牌 | 能做什么 |
|
|
169
|
+
| --- | --- |
|
|
170
|
+
| `token`(管理) | 全部接口:对话 + 导入资料 + 看会话 + 管理台 |
|
|
171
|
+
| `public_token`(访客) | **仅** `/api/chat`、`/api/chat/stream`;配了它时挂件页对访客开放 |
|
|
172
|
+
|
|
173
|
+
`serve_token` **只返回 `public_token`,绝不回落管理令牌** —— 挂件页是给访客看的,
|
|
174
|
+
一旦回落,源码里就能读到后台令牌。未配 `public_token` 时挂件页本身也不公开。
|
|
175
|
+
`/embed.js` 与 `/healthz` 是唯一无条件公开的路径(本身不含密钥)。
|
|
176
|
+
|
|
177
|
+
管理台里可以**粘贴整篇帮助文档**(`POST /api/ingest/text`)自动切块、识别「问:/答:」存成问答对,
|
|
178
|
+
访客立刻就能问到 —— 这就是"资料库自学"的入口。
|
|
179
|
+
|
|
138
180
|
## 参考实现:站点智能客服
|
|
139
181
|
|
|
140
182
|
`apps/customer_service.py` 用插件组合出一个可上线的客服 Agent(零 LLM 也能回答)。
|
|
@@ -230,7 +272,7 @@ print(app.handle("你好")) # → "reply:你好"(回落 chat)
|
|
|
230
272
|
## 快速自检
|
|
231
273
|
|
|
232
274
|
```bash
|
|
233
|
-
python -m pasm_framework selftest #
|
|
275
|
+
python -m pasm_framework selftest # 55 项
|
|
234
276
|
python -m pasm_framework doctor # 真装配一遍插件并报拼错的名字
|
|
235
277
|
python -m pasm_framework version
|
|
236
278
|
```
|
|
@@ -245,6 +287,8 @@ python -m pasm_framework version
|
|
|
245
287
|
| [`docs/cross-platform-strategy.md`](docs/cross-platform-strategy.md) | 桌面端跨 Linux/macOS、手机端换架构方案 |
|
|
246
288
|
| [`docs/customer-service-plugin.md`](docs/customer-service-plugin.md) | 智能客服可行性与部署形态研究 |
|
|
247
289
|
| [`docs/audit-2026-09-20.md`](docs/audit-2026-09-20.md) | v0.2.0 体检报告 |
|
|
290
|
+
| [`docs/plugin-ownership.md`](docs/plugin-ownership.md) | 插件放子目录还是独立仓(决策 + 理由) |
|
|
291
|
+
| [`docs/audit-7repos-2026-09-20.md`](docs/audit-7repos-2026-09-20.md) | **七仓全盘审计**(含跨平台实测) |
|
|
248
292
|
| [`docs/openapi.yaml`](docs/openapi.yaml) | HTTP 契约(单一真相源) |
|
|
249
293
|
| [`sdks/`](sdks/README.md) | C# / Java / PHP / Node / Go / Python 客户端 |
|
|
250
294
|
|
|
@@ -39,7 +39,7 @@ PASM V1→V2 升级时,**不重写 4 个产品智能体 + 3 个技能**。手
|
|
|
39
39
|
"""
|
|
40
40
|
from __future__ import annotations
|
|
41
41
|
|
|
42
|
-
__version__ = "0.
|
|
42
|
+
__version__ = "0.4.0"
|
|
43
43
|
|
|
44
44
|
from .adapter import ( # noqa: F401
|
|
45
45
|
DomainAdapter,
|
|
@@ -292,6 +292,45 @@ def selftest() -> bool:
|
|
|
292
292
|
gw = pm.get("web_gateway")
|
|
293
293
|
check(gw is not None, "web_gateway 插件可构造(不启动服务器)")
|
|
294
294
|
|
|
295
|
+
# ---- v0.3.1 站点插件安全不变式(不需要起服务器)----
|
|
296
|
+
# `_Handler._gw` 读的是 `self.server.gateway`,所以给个假 server 就能
|
|
297
|
+
# 直接调鉴权逻辑,无需真的监听端口。
|
|
298
|
+
from .plugins.builtins.web_gateway import _Handler as _GWHandler
|
|
299
|
+
|
|
300
|
+
class _ProbeHandler(_GWHandler):
|
|
301
|
+
def __init__(self, gateway, path, headers=None):
|
|
302
|
+
self.server = type("_S", (), {"gateway": gateway})()
|
|
303
|
+
self.path = path
|
|
304
|
+
self.headers = headers or {}
|
|
305
|
+
|
|
306
|
+
def _probe(gateway, path, tok=None):
|
|
307
|
+
h = _ProbeHandler(gateway, path,
|
|
308
|
+
{"Authorization": "Bearer " + tok} if tok else {})
|
|
309
|
+
return h._authorized(path)
|
|
310
|
+
|
|
311
|
+
_g = type(gw)({"token": "ADMIN", "public_token": "PUB"})
|
|
312
|
+
check(_probe(_g, "/console", "ADMIN") and not _probe(_g, "/console", "PUB"),
|
|
313
|
+
"网关:管理台只认管理令牌(公开令牌被拒)")
|
|
314
|
+
check(not _probe(_g, "/console?token=PUB", "PUB"),
|
|
315
|
+
"网关:?token= 携带公开令牌也进不了管理台")
|
|
316
|
+
_h = _ProbeHandler(_g, "/api/chat", {"Authorization": "Bearer PUB"})
|
|
317
|
+
check(_h._authorized("/api/chat", admin=False)
|
|
318
|
+
and not _h._authorized("/api/chat", admin=True),
|
|
319
|
+
"网关:公开令牌仅限对话作用域")
|
|
320
|
+
check(_probe(_g, "/embed.js") and _probe(_g, "/healthz"),
|
|
321
|
+
"网关:/embed.js 与 /healthz 无条件可达")
|
|
322
|
+
_g2 = type(gw)({"token": "ADMIN"}) # 没配 public_token
|
|
323
|
+
check(_g2.serve_token == "",
|
|
324
|
+
"★ 未配 public_token 时 serve_token 为空(绝不回落管理令牌)")
|
|
325
|
+
check(_ProbeHandler(_g2, "/")._authorized("/") is False,
|
|
326
|
+
"★ 未配 public_token 时挂件页不公开(不泄漏管理令牌)")
|
|
327
|
+
check(_ProbeHandler(_g2, "/",
|
|
328
|
+
{"Authorization": "Bearer ADMIN"})._authorized("/"),
|
|
329
|
+
"网关:站长用管理令牌仍可打开挂件页")
|
|
330
|
+
_g3 = type(gw)({}) # 无令牌 = 开发开放模式
|
|
331
|
+
check(_ProbeHandler(_g3, "/console")._authorized("/console"),
|
|
332
|
+
"网关:无令牌配置时保持开放(兼容本机开发)")
|
|
333
|
+
|
|
295
334
|
# ---- v0.2.1 收尾阶段不变式 ----
|
|
296
335
|
# 1) on_reply_final 对**每条**回复路径恰好跑一次(生成/能力/模板)。
|
|
297
336
|
class _Probe(BasePlugin):
|
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
pasm-framework plugins 列出内置插件与默认开关
|
|
7
7
|
pasm-framework config [--preset X | --file f.json] [--env] 查看解析后的开关表
|
|
8
8
|
pasm-framework doctor 环境体检(能不能跑、缺什么)
|
|
9
|
-
pasm-framework new <dir> [--kind app|chatbot|game_npc] [--force] 生成项目
|
|
9
|
+
pasm-framework new <dir> [--kind app|chatbot|game_npc|customer_service] [--force] 生成项目
|
|
10
10
|
pasm-framework serve [--port 8080] [--host 0.0.0.0] [--kb ./kb] 起一个演示客服
|
|
11
|
+
pasm-framework cs [--port 8080] [--name X] 一键起在线客服(双令牌+嵌入码+管理台)
|
|
11
12
|
|
|
12
13
|
设计约束:只用标准库 ``argparse``,不引入 CLI 框架 —— 保证"装完就能跑"。
|
|
13
14
|
"""
|
|
@@ -210,6 +211,74 @@ def _cmd_serve(args) -> int:
|
|
|
210
211
|
return 0
|
|
211
212
|
|
|
212
213
|
|
|
214
|
+
def _cmd_cs(args) -> int:
|
|
215
|
+
"""一键起在线客服:自动生成双令牌,打印可分享链接与嵌入代码。
|
|
216
|
+
|
|
217
|
+
面向"不懂代码也能跑":一条命令拿到
|
|
218
|
+
· 访客对话页(可直接分享 / iframe 嵌入)
|
|
219
|
+
· 站长管理台(粘贴资料 → 立即生效)
|
|
220
|
+
· 一行 <script> 嵌入代码(任意语言开发的站点都能挂)
|
|
221
|
+
"""
|
|
222
|
+
import time
|
|
223
|
+
import secrets
|
|
224
|
+
|
|
225
|
+
from .demo import make_demo_app
|
|
226
|
+
|
|
227
|
+
serve_token = args.serve_token or secrets.token_hex(8)
|
|
228
|
+
public_token = args.public_token or secrets.token_hex(8)
|
|
229
|
+
|
|
230
|
+
llm = None
|
|
231
|
+
if args.llm_provider:
|
|
232
|
+
llm = {"provider": args.llm_provider}
|
|
233
|
+
if args.llm_model:
|
|
234
|
+
llm["model"] = args.llm_model
|
|
235
|
+
if args.llm_key:
|
|
236
|
+
llm["api_key"] = args.llm_key
|
|
237
|
+
|
|
238
|
+
app = make_demo_app(kb_dir=args.kb, llm=llm, host=args.host, port=args.port,
|
|
239
|
+
token=serve_token, public_token=public_token,
|
|
240
|
+
preset_name="chatbot")
|
|
241
|
+
|
|
242
|
+
shown_host = args.host if args.host != "0.0.0.0" else "127.0.0.1"
|
|
243
|
+
base = "http://%s:%d" % (shown_host, args.port)
|
|
244
|
+
lan = "http://<你的局域网IP>:%d" % args.port if args.host == "0.0.0.0" else base
|
|
245
|
+
# 属性名必须与 embed.js 读取的一致(data-pasm-token),否则挂件拿不到令牌。
|
|
246
|
+
embed = ("<script src=\"%s/embed.js\"\n"
|
|
247
|
+
" data-pasm-token=\"%s\"\n"
|
|
248
|
+
" data-title=\"%s\"\n"
|
|
249
|
+
" data-greeting=\"你好!我是智能客服,有什么可以帮你?\"\n"
|
|
250
|
+
" data-color=\"#2563eb\" defer></script>"
|
|
251
|
+
% (lan, public_token, args.name))
|
|
252
|
+
|
|
253
|
+
print("=" * 62)
|
|
254
|
+
print("在线客服已启动(%s)" % args.name)
|
|
255
|
+
print("=" * 62)
|
|
256
|
+
print("① 访客对话页(分享给客户 / 直接打开测试):")
|
|
257
|
+
print(" %s/" % base)
|
|
258
|
+
print("② 站长管理台(粘贴资料 → 访客立即能答):")
|
|
259
|
+
print(" %s/console?token=%s" % (base, serve_token))
|
|
260
|
+
print(" (链接已带管理令牌;打开后令牌会从地址栏消失,存进本次会话)")
|
|
261
|
+
print("③ 嵌入你自己的站点(任意语言:PHP/Java/Node/静态页均可):")
|
|
262
|
+
print(" 在 </body> 前加这几行:")
|
|
263
|
+
print(" %s" % embed.replace("\n", "\n "))
|
|
264
|
+
print("④ REST 接口(程序对接):")
|
|
265
|
+
print(" POST %s/api/chat {\"text\": \"...\"}" % base)
|
|
266
|
+
print(" Header: Authorization: Bearer %s" % public_token)
|
|
267
|
+
print("-" * 62)
|
|
268
|
+
print("令牌说明:公开令牌只能对话;管理令牌才能导入资料/看会话。")
|
|
269
|
+
print("资料来源:启动时自动导入演示 FAQ;往 %s 放 .txt/.md 或在管理台粘贴即可。" % (args.kb or "~/.pasm_framework/kb"))
|
|
270
|
+
print("Ctrl+C 退出")
|
|
271
|
+
print("=" * 62)
|
|
272
|
+
try:
|
|
273
|
+
app.serve()
|
|
274
|
+
while True:
|
|
275
|
+
time.sleep(1)
|
|
276
|
+
except KeyboardInterrupt:
|
|
277
|
+
print("\n已停止。")
|
|
278
|
+
app.close()
|
|
279
|
+
return 0
|
|
280
|
+
|
|
281
|
+
|
|
213
282
|
def main(argv=None) -> int:
|
|
214
283
|
argv = list(sys.argv[1:] if argv is None else argv)
|
|
215
284
|
cmd = argv[0] if argv else "selftest"
|
|
@@ -260,6 +329,21 @@ def main(argv=None) -> int:
|
|
|
260
329
|
p_srv.add_argument("--llm-model", default=None)
|
|
261
330
|
p_srv.add_argument("--llm-key", default=None)
|
|
262
331
|
|
|
332
|
+
p_cs = sub.add_parser("cs", help="一键起在线客服(双令牌 + 可分享链接 + 嵌入代码)")
|
|
333
|
+
p_cs.add_argument("--name", default="在线客服", help="客服名(打印在横幅里)")
|
|
334
|
+
p_cs.add_argument("--host", default="127.0.0.1",
|
|
335
|
+
help="0.0.0.0 表示允许局域网访问")
|
|
336
|
+
p_cs.add_argument("--port", type=int, default=8080)
|
|
337
|
+
p_cs.add_argument("--kb", default=None, help="知识库目录(默认 ~/.pasm_framework/kb)")
|
|
338
|
+
p_cs.add_argument("--serve-token", dest="serve_token", default=None,
|
|
339
|
+
help="管理令牌(缺省自动生成)")
|
|
340
|
+
p_cs.add_argument("--public-token", dest="public_token", default=None,
|
|
341
|
+
help="公开令牌(缺省自动生成)")
|
|
342
|
+
p_cs.add_argument("--llm-provider", default=None,
|
|
343
|
+
help="openai / deepseek / ollama(不填则离线模式)")
|
|
344
|
+
p_cs.add_argument("--llm-model", default=None)
|
|
345
|
+
p_cs.add_argument("--llm-key", default=None)
|
|
346
|
+
|
|
263
347
|
try:
|
|
264
348
|
ns = parser.parse_args(argv)
|
|
265
349
|
except SystemExit as ex: # argparse 的报错也是退出
|
|
@@ -271,6 +355,7 @@ def main(argv=None) -> int:
|
|
|
271
355
|
"doctor": _cmd_doctor,
|
|
272
356
|
"new": _cmd_new,
|
|
273
357
|
"serve": _cmd_serve,
|
|
358
|
+
"cs": _cmd_cs,
|
|
274
359
|
}.get(ns.sub)
|
|
275
360
|
if handler is None:
|
|
276
361
|
print(USAGE)
|
|
@@ -106,8 +106,11 @@ class BaseApplication(BaseAgent):
|
|
|
106
106
|
去重按 ``title``;凡"有依据的知识"(领域 / 知识库)都带 ``source``,
|
|
107
107
|
纯对话记忆(episodic)不带 —— 应用可据此区分"资料"与"闲聊"。
|
|
108
108
|
"""
|
|
109
|
-
|
|
110
|
-
|
|
109
|
+
# 对话记忆(episodic)—— 不带 source。
|
|
110
|
+
memories = super().recall(query, k=k)
|
|
111
|
+
seen = {h.get("title") for h in memories}
|
|
112
|
+
# 「有依据的知识」单独收集:领域 + 知识库。
|
|
113
|
+
sourced: List[Dict[str, Any]] = []
|
|
111
114
|
|
|
112
115
|
if hasattr(self.domain, "knowledge_for"):
|
|
113
116
|
try:
|
|
@@ -118,7 +121,7 @@ class BaseApplication(BaseAgent):
|
|
|
118
121
|
it = dict(item)
|
|
119
122
|
it.setdefault("source",
|
|
120
123
|
"domain:%s" % type(self.domain).__name__)
|
|
121
|
-
|
|
124
|
+
sourced.append(it)
|
|
122
125
|
seen.add(it.get("title"))
|
|
123
126
|
except Exception:
|
|
124
127
|
pass
|
|
@@ -132,12 +135,41 @@ class BaseApplication(BaseAgent):
|
|
|
132
135
|
for item in kb.recall(query, k=k):
|
|
133
136
|
if item.get("title") in seen:
|
|
134
137
|
continue
|
|
135
|
-
|
|
138
|
+
sourced.append(item)
|
|
136
139
|
seen.add(item.get("title"))
|
|
137
140
|
except Exception:
|
|
138
141
|
pass
|
|
139
142
|
|
|
140
|
-
|
|
143
|
+
# 排序:**有依据的知识优先于对话记忆**。
|
|
144
|
+
# 否则"上次聊过一句提到过某个词"的闲聊记忆会排到 FAQ 前面,
|
|
145
|
+
# 客服就变成"答非所问"(实测过:闲聊记忆抢占,资料库排到了后面)。
|
|
146
|
+
merged = sourced + memories
|
|
147
|
+
return merged[:k] if k else merged
|
|
148
|
+
|
|
149
|
+
# ---- 区分「资料」与「闲聊」---------------------------------
|
|
150
|
+
def knowledge_facts(
|
|
151
|
+
self, facts: Optional[List[Dict[str, Any]]]
|
|
152
|
+
) -> List[Dict[str, Any]]:
|
|
153
|
+
"""从检索结果里挑出**有依据的知识**(带 ``source`` 的那些)。
|
|
154
|
+
|
|
155
|
+
为什么必须有这个方法:``recall()`` 返回的是**混合**结果 ——
|
|
156
|
+
引擎的对话记忆(episodic,不带 ``source``)+ 领域知识 + 知识库资料
|
|
157
|
+
(都带 ``source``)。写 ``_render_reply`` 时如果直接取 ``facts[0]``
|
|
158
|
+
就会把"上一轮用户自己说过的话"当成资料答回去。
|
|
159
|
+
|
|
160
|
+
实测过的翻车现场:用户问"你们老板叫什么",
|
|
161
|
+
系统把上一轮的"积分怎么算"当成资料答了出来 ——
|
|
162
|
+
因为那条对话记忆被检索命中,而它**不是知识**。
|
|
163
|
+
|
|
164
|
+
所以任何"就资料作答"的回复策略都应该先过这个过滤器::
|
|
165
|
+
|
|
166
|
+
def _render_reply(self, text, facts, mood):
|
|
167
|
+
k = self.knowledge_facts(facts)
|
|
168
|
+
return k[0]["brief"] if k else "抱歉,我暂时没有相关资料。"
|
|
169
|
+
|
|
170
|
+
保持原顺序(资料在前、且各自内部已按相关度排序),只做过滤不做重排。
|
|
171
|
+
"""
|
|
172
|
+
return [f for f in (facts or []) if f.get("source")]
|
|
141
173
|
|
|
142
174
|
# ---- 对话(覆盖基类,支持注入已检索事实)-----------------
|
|
143
175
|
def chat(self, text: str, facts: Optional[List[Dict[str, Any]]] = None) -> str:
|
|
@@ -53,10 +53,14 @@ def make_demo_app(
|
|
|
53
53
|
agent_id: str = "pasm-demo",
|
|
54
54
|
preset_name: str = "chatbot",
|
|
55
55
|
token: Optional[str] = None,
|
|
56
|
+
public_token: Optional[str] = None,
|
|
56
57
|
teach: bool = True,
|
|
57
58
|
) -> DemoAgent:
|
|
58
59
|
"""构造演示应用(已导入 :data:`DEMO_FAQ`)。
|
|
59
60
|
|
|
61
|
+
``token`` 是**管理令牌**(全部接口);``public_token`` 是**访客令牌**
|
|
62
|
+
(仅对话接口,可安全嵌进第三方站点)。只给 ``token`` 时行为同旧版。
|
|
63
|
+
|
|
60
64
|
>>> from pasm_framework.demo import make_demo_app
|
|
61
65
|
>>> app = make_demo_app(port=8099)
|
|
62
66
|
>>> "退货" in app.ask("怎么退货?")
|
|
@@ -65,6 +69,8 @@ def make_demo_app(
|
|
|
65
69
|
gw: Dict[str, Any] = {"enabled": True, "config": {"host": host, "port": port}}
|
|
66
70
|
if token:
|
|
67
71
|
gw["config"]["token"] = token
|
|
72
|
+
if public_token:
|
|
73
|
+
gw["config"]["public_token"] = public_token
|
|
68
74
|
kb: Dict[str, Any] = {"enabled": True}
|
|
69
75
|
if kb_dir:
|
|
70
76
|
kb["config"] = {"kb_dir": kb_dir}
|