pbl_kdb_ext/README.md
2026-09-18 19:02:17 +08:00

199 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# pbl_kdb_ext —— PBL 知识库KDB只读桩扩展模块M7
## 1. 模块用途
`pbl_kdb_ext` 是 PBLProject/Problem-Based LearningAgent OS 的**知识库只读扩展层**,为
Designer / Critic Agent 与教师端提供「查得到、改不了」的知识条目检索与研究数据聚合能力。
设计定位(对应两个开放问题的当期落地口径):
| 开放问题 | 当期口径 | 本模块实现 |
|---------|---------|-----------|
| **Q-OPEN-5** KDB 是否落地向量库 | 暂不引入向量库/Embedding | **不落地向量桩**:检索走结构化字段等值 + `LIKE` 关键词匹配,返回体带 `stub` 标记,后续可平滑替换为向量检索而不改契约 |
| **Q-OPEN-6** research 数据是否可写回 | 只读、匿名 | **只读匿名聚合**:分组统计评估分数,最小分组 `k_min=5`,样本不足的分组直接抑制(`suppressed_groups` 计数),返回 `anonymous=true / written=false` |
### G6 范围纪律(硬约束,可 grep 核对)
本模块**零写入业务对象**,明确**不实现**以下任何能力:
-`kdb.write` / `kdb.add_candidate` / `kdb.propose_pattern`(知识条目写入、候选入库、模式提炼)
-`research.write`(研究数据写回)
-`sim.*`(仿真/模拟相关全部接口)
`pbl_kdb_item.is_write_locked` 恒为 `1`,模块内**不存在**任何 `UPDATE` / `INSERT` / `DELETE`
`pbl_kdb_item` 的代码路径;唯一的写操作是**查询留痕日志** `pbl_kdb_query_log`(审计用途,
append-only不属于业务对象写入
## 2. 数据表2 张,定义在 `models/`
### 2.1 `pbl_kdb_item` —— 知识条目表(只读,写锁定)
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | str(32) | 主键 |
| `tenant_id` | str(32) | 租户隔离,**所有查询强制打头** |
| `code` | str(64) | 条目业务编码(唯一,配合 tenant_id |
| `item_type` | str(32) | 条目类型pattern / case / rubric_ref 等,取自 appcodes |
| `title` | str(255) | 标题(检索字段) |
| `summary_txt` | text | 摘要正文(`LIKE` 关键词匹配字段) |
| `payload_json` | text | 结构化载荷JSON读取时反序列化为 dict |
| `source` | str(64) | 来源(可作过滤维度) |
| `version_no` | int | 版本号 |
| `status` | str(16) | 状态,检索仅命中 `published` |
| `is_write_locked` | short | **恒为 1**:写锁定标记,本模块只读约束的数据层体现 |
| `created_at` / `updated_at` | timestamp | 审计时间戳 |
> 表数据由上游(蓝图编译 / 治理域)产生并置 `is_write_locked=1`;本模块只消费,不生产。
### 2.2 `pbl_kdb_query_log` —— 检索留痕日志表append-only
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | str(32) | 主键 |
| `tenant_id` | str(32) | 租户隔离 |
| `query_uid` | str(64) | 查询唯一标识(`Q` + sha256(参数+时间) 前 20 位) |
| `caller_type` | str(16) | 调用方类型agent / teacher缺省 `agent` |
| `caller_id` | str(32) | 调用方 ID取自 `actor_id()` |
| `query_json` | text | 查询参数快照(剔除 `caller_id`,避免重复存储) |
| `hit_count` | int | 命中条数 |
| `latency_ms` | int | 检索耗时(毫秒) |
| `ok` | short | 是否成功(恒 1异常路径直接抛错不落日志 |
| `created_at` | timestamp | 记录时间 |
用途:检索行为可观测(命中率 / 延迟)与审计追溯。**只 INSERT不 UPDATE / DELETE。**
### 2.3 跨模块只读依赖
`pbl_research_aggregate` 只读聚合 **`pbl_assessment_record`**(属 `pbl_assessment` 模块,
M6 产出)的 `total_score` / `band` / `assessor_type` / `blueprint_id` 字段——**跨模块只读,
不写不建表**,符合交互/扩展层模块的数据所有权边界。
## 3. 契约接口3 个,全部只读)
三个契约均为 `wwwroot/api/*.dspy` 薄封装 → 调用 `pbl_kdb_ext/api.py` 中的同名 async 函数
(函数经 `load_pbl_kdb_ext()` 注册到 ServerEnvdspy 内直接以全局名调用,无需 import
| # | 契约名 | HTTP 路径(自动路由) | 入参 | 返回要点 |
|---|--------|---------------------|------|---------|
| 1 | `pbl_kdb_search` | `/pbl_kdb_ext/api/pbl_kdb_search.dspy` | `item_type``source``keyword``limit`(≤50默认 10)、`caller_type` | `{ok, data[], hit_count, latency_ms, writable:false, stub}`;固定过滤 `tenant_id` + `is_write_locked=1` + `status='published'`;写一条 `pbl_kdb_query_log` |
| 2 | `pbl_kdb_get` | `/pbl_kdb_ext/api/pbl_kdb_get.dspy` | `id``code` | `{ok, data{...payload_json 已反序列化}, is_write_locked:true, stub}`;未命中抛 `PBL_KDB_NOT_FOUND` |
| 3 | `pbl_research_aggregate` | `/pbl_kdb_ext/api/pbl_research_aggregate.dspy` | `group_by` ∈ {`band`, `assessor_type`, `blueprint_id`}(默认 `band` | `{ok, dimension, groups[{group,n,avg_score,min_score,max_score}], suppressed_groups, k_min:5, anonymous:true, written:false, note}`;维度非法抛 `PBL_AGG_DIM_INVALID` |
错误码沿用 `pbl_common` 统一错误体系(`PblError``PBL_KDB_NOT_FOUND``PBL_AGG_DIM_INVALID`
本模块**不另设** `errors.py`——错误类型与错误码常量由公共内核 `pbl_common.api` 统一提供,
模块内只做抛出,避免与公共内核重复定义(交付清单与磁盘文件一一对应)。
## 4. 目录结构
```
pbl_kdb_ext/
├── pbl_kdb_ext/ # Python 包(包目录 = 模块名)
│ ├── __init__.py # ② 导出三个契约函数(三处同步之②)
│ ├── init.py # ③ load_pbl_kdb_ext()env.<契约> = <契约>
│ └── api.py # ① 契约实现(只读检索 / 详情 / 匿名聚合)
├── wwwroot/
│ ├── index.ui # 模块入口页(三张卡片 → urlwidget 到内容区)
│ └── api/
│ ├── pbl_kdb_search.dspy
│ ├── pbl_kdb_get.dspy
│ └── pbl_research_aggregate.dspy
├── models/
│ ├── pbl_kdb_item.json # 表定义(四段式 summary/fields/indexes/codes
│ └── pbl_kdb_query_log.json
├── json/ # CRUD 定义(只读浏览用,无 editable 写入 URL
│ ├── kdb_ext_pbl_kdb_item.json
│ └── kdb_ext_pbl_kdb_query_log.json
├── scripts/load_path.py # RBAC 路径注册(显式枚举,无通配符)
├── sql/pbl_kdb_ext.sql # DDL建表脚本
├── skill/SKILL.md # Agent 必读模块技能文档
├── pyproject.toml
└── README.md # 本文件
```
## 5. 挂载方式(宿主应用集成)
模块**不是独立部署单元**——无自己的 `app.py` / 端口 / Dockerfile / 服务脚本,只能由宿主应用
`apps/pbls`)挂载:
```python
# apps/pbls/app/pbls.py 的 init() 中ServerEnv() 与基础模块加载之后:
from pbl_kdb_ext.init import load_pbl_kdb_ext
load_pbl_kdb_ext() # 返回 'pbl_kdb_ext'
```
`load_pbl_kdb_ext()` 是**唯一集成点**,做且只做一件事:把三个契约函数注册到 `ServerEnv`
`env.pbl_kdb_search` / `env.pbl_kdb_get` / `env.pbl_research_aggregate`),使 `.dspy` / `.ui`
能以全局名直接调用。宿主决定何时、以何顺序挂载。
### 5.1 三处同步铁律
新增/删除一个契约函数必须同步改三处,缺一即运行期报错:
1. `pbl_kdb_ext/api.py` —— 函数实现;
2. `pbl_kdb_ext/__init__.py` —— `from .api import <fn>` 导出(缺 → ImportError / AttributeError
3. `pbl_kdb_ext/init.py` —— `env.<fn> = <fn>`(缺 → dspy 内 `NameError: name '<fn>' is not defined`)。
核对命令:`grep -rn 'pbl_kdb_search' pbl_kdb_ext/ --include='*.py'` 应恰好命中这三处。
### 5.2 RBAC 注册(`scripts/load_path.py`
`apps/pbls/build.sh` 调用 `register()`,逐条注册(**显式枚举,禁止 `%` / `*` 通配符**
| 路径 | 角色 |
|------|------|
| `/pbl_kdb_ext` | `logined` |
| `/pbl_kdb_ext/index.ui` | `logined` |
| `/pbl_kdb_ext/api/pbl_kdb_search.dspy` | `logined` |
| `/pbl_kdb_ext/api/pbl_kdb_get.dspy` | `logined` |
| `/pbl_kdb_ext/api/pbl_research_aggregate.dspy` | `logined` |
入口页的**目录路径与文件路径都要注册**,否则入口页上线即 403。`set_role_perm.py` 不在位时
脚本打印 `PENDING <role> <path>` 清单并以退出码 1 返回(不静默跳过、不抛异常),由部署角色
手工补注册。本模块全部接口为登录可读,无 `any`(匿名)路径,也无 teacher/admin 专属写路径
(因为没有写接口)。
### 5.3 数据库名
模块内**不硬编码库名**,统一通过 `pbl_common` 的 DB 适配层(`sql_rows` / `sql_exec`
`'pbl'` 逻辑库标识)由宿主应用的 `get_module_dbname()` 解析到真实库。
## 6. 只读 / 匿名聚合约束说明(验收要点)
1. **零写入**:模块内无 `kdb.write` / `research.write` / `sim.*` 任何实现;
`grep -rn "def .*write\|def .*sim_" pbl_kdb_ext/` 结果为空。
2. **写锁定**`pbl_kdb_search` 的 WHERE 固定包含 `is_write_locked = 1``pbl_kdb_get`
返回体固定 `is_write_locked: true`;无任何路径可修改该字段。
3. **租户隔离**:三个契约的 SQL 全部以 `tenant_id = ${t}$` 打头(`tenant_id()` 取自
`pbl_common` 请求上下文),缺失租户上下文即报错,不降级为全租户查询。
4. **匿名聚合(隐私线)**`k_min = 5`;分组样本数 `< 5` 的分组**不出现在 `groups` 中**
仅计入 `suppressed_groups`;返回体不含任何个体标识(无 student_id / user_id / 明细行)。
5. **聚合维度白名单**`group_by` 仅允许 `band` / `assessor_type` / `blueprint_id`
其它值抛 `PBL_AGG_DIM_INVALID`(同时杜绝 SQL 注入——维度名不来自自由文本拼接)。
6. **参数化 SQL**:全部使用 sqlor `${name}$` 占位符,无字符串拼接用户输入。
7. **桩标记透明**:检索/详情返回体带 `stub: 'kdb_stub: no vector store (Q-OPEN-5)'`
调用方可据此判断结果来自结构化匹配而非语义向量检索。
8. **可观测**:每次检索落一条 `pbl_kdb_query_log`hit_count / latency_ms / query_json
用于命中率与延迟分析;日志表 append-only。
## 7. 依赖
- **基础/公共**`pbl_common`租户上下文、DB 适配、`PblError` 错误码、`actor_id` /
`now_str` / `json_dump` 工具)、`ahserver`ServerEnv`sqlor`(数据访问)。
- **数据依赖(只读)**`pbl_assessment``pbl_assessment_record`(聚合数据源)。
- **被依赖**`pbl_agent_runtime`Designer/Critic Agent 检索知识条目)、
`pbl_scense_ext` / 教师端页面(研究数据聚合展示)。
## 8. 本地校验
```bash
cd modules/pbl_kdb_ext
python3 -m py_compile pbl_kdb_ext/*.py scripts/load_path.py # .py 编译
python3 -c "import json;[json.load(open('models/'+f)) for f in ('pbl_kdb_item.json','pbl_kdb_query_log.json')]"
python3 -c "import json;json.load(open('wwwroot/index.ui'));print('index.ui JSON OK')"
grep -rn "^import\|^from" wwwroot/ --include='*.dspy' # dspy 审计:应为空
grep -rn "entire_url(" wwwroot/index.ui | grep -v "/pbl_kdb_ext/" # 绝对路径核对:应为空
```
> `.dspy` 不能用 `py_compile` 校验(框架运行期注入 async 函数体,顶层 `return`/`await` 合法),
> 只做 grep 审计。