pbl_evidence/README.md
2026-09-21 13:14:12 +08:00

12 KiB
Raw Blame History

pbl_evidence

PBL项目式学习产出物与学习证据模块。它做两件事:维护学生产出物(pbl_artifact 以及把运行时产生的学习事件幂等地采集成学习证据(pbl_evidence)。

  • 模块类型业务模块Python 包),不是独立部署单元 —— 没有 app.py、没有自己的端口、 没有 Dockerfile由宿主应用apps/pbls)调用 load_pbl_evidence() 挂载后运行。
  • 交付里程碑M5a事件 → 证据 幂等采集 + 产出物 CRUD、M5b回放 / 增量游标 / 统计)。
  • 下游M6 评估Rubric 加权打分)以本模块的证据表为唯一事实源。

Features能力清单

能力 契约函数 DSPY 路径 说明
产出物新增 pbl_artifact_create /pbl_evidence/api/pbl_artifact_create.dspy 自动补 tenant_id / artifact_uid / created_at
产出物读取 pbl_artifact_read /pbl_evidence/api/pbl_artifact_read.dspy 按 id 读,强制租户过滤
产出物更新 pbl_artifact_update /pbl_evidence/api/pbl_artifact_update.dspy 白名单列更新,禁改 tenant_id
产出物删除 pbl_artifact_delete /pbl_evidence/api/pbl_artifact_delete.dspy 带归属校验
产出物列表 pbl_artifact_list /pbl_evidence/api/pbl_artifact_list.dspy 分页 + 会话/蓝图/作者过滤
单事件落证据 pbl_evidence_collect /pbl_evidence/api/pbl_evidence_collect.dspy 一条事件 → 一条证据,重复调用返回 deduped=True
批量采集M5a 主能力) pbl_evidence_collect_from_events /pbl_evidence/api/pbl_evidence_collect_from_events.dspy pbl_runtime_event → 映射 → 幂等落库,返回 scanned/created/skipped/updated/ignored/failed + watermark
证据列表 pbl_evidence_list /pbl_evidence/api/pbl_evidence_list.dspy 学习者/产出物/会话/蓝图/类型/时间窗过滤 + 分页
证据统计 pbl_evidence_stats /pbl_evidence/api/pbl_evidence_stats.dspy 证据类型分布,供 M6 与概览页
增量游标 pbl_evidence_watermark (无独立 dspy供 M5b 回放 / cron 与内部调用) 已采集最大事件时间 + 总量3s 轮询兜底的起点

模块入口页:/pbl_evidence/index.uibricks 卡片导航,聚合上述 9 个 .dspy

幂等语义(本模块的核心承诺)

重复采集绝不产生第二条证据,也绝不报错(学生侧无感):

  1. 数据库层:唯一索引 uk_ev_dedup(tenant_id, source_event_id, evidence_type) —— 同一租户下「同一来源事件 + 同一证据类型」最多一行。这是唯一可信的防重屏障。
  2. 应用层双保险:写入前先按 (tenant_id, source_event_id, evidence_type) 预查, 命中即 skipped(或 update_existing=True 时刷新 payload未命中才 INSERT ... ON DUPLICATE KEY UPDATE,因此并发/重放同时命中也不会抛 IntegrityError。
  3. 去重键dedup_key = md5(tenant_id|source_event_id|evidence_type)32 位定长, 与 VARCHAR(32) 严格对齐),便于外部对账与排障。
  4. 事件 → 证据映射是配置化常量表evidence_map.EVENT_TO_EVIDENCE37 条同义词映射 + 7 条忽略黑名单),未知事件类型降级为 observation 而不丢事件;不靠 SQL 硬编码、不靠猜。

硬约束 C-3上线顺序必须遵守

本模块批量采集读取的 pbl_runtime_event 表由 scense_runtime 的 M11b 建表并写入。

  • M5 不得早于 M11b 上线,且 M5 与 M11 不得并行开发联调。
  • 事件表缺失时 collector.resolve_event_table() 返回 None → 抛 CollectError → 契约层转成明确错误码 PBL_E_DB_UNAVAILABLE不会静默返回空成功fail-closed便于运维定位
  • 兼容层:事件表名按候选顺序发现 pbl_runtime_eventruntime_eventpbl_event 列名一律经 information_schema 发现,不猜列名(分区表/改名不会导致整链 500

Data tables数据表

表定义在 models/{table}.json(四段式 summary/fields/indexes/codesDDL 由 json2ddl 生成, 建表 SQL 快照在 sql/pbl_evidence.sql

pbl_artifact —— 学生产出物

类型 说明
id str(32) PK 主键
tenant_id str(32) NOT NULL 租户(强制打头,缺失 fail-closed
artifact_uid str(64) 业务唯一码,与 tenant 组成唯一键
session_id / blueprint_id / team_id str(32) 会话 / 蓝图 / 团队归属
creator_id str(32) 作者(学习者)
title str(255) 标题
artifact_type str(64) 类型(编码 pbl_artifact_type
content_json text 产出物内容(序列化 JSON
version_no str(32) 版本号(内联,版本快照由蓝图侧维护)
status str(32) 状态(编码 pbl_artifact_status
submitted_at / created_at / updated_at datetime 时间与审计列(应用层写入sqlor 不自动填)

索引:uk_art_uid(tenant_id, artifact_uid) 唯一;idx_art_session / idx_art_blueprint / idx_art_creator(均以 tenant_id 打头)。

pbl_evidence —— 学习证据

类型 说明
id str(32) PK 主键
tenant_id str(32) NOT NULL 租户
artifact_id str(32) 归属产出物('0' = 无强归属的观测类证据)
evidence_type str(64) 证据类型(编码 pbl_evidence_type8 类)
source_event_id str(32) NOT NULL 来源 runtime_event 事件 ID幂等锚点
session_id / learner_id / blueprint_id str(32) 查询维度
payload_json text 证据体(事件摘要序列化 JSON
occurred_at datetime 事件发生时间
dedup_key str(32) md5(tenant|event|type)
collect_batch_id str(32) 采集批次(排障用)
created_at / updated_at datetime 审计列(应用层写入)

索引:

  • uk_ev_dedup UNIQUE (tenant_id, source_event_id, evidence_type) —— 幂等的唯一事实源。 含义:同一租户内,一条来源事件的一种证据类型只允许一行;重放、双通道(广播 + 3s 轮询兜底) 命中同一事件时第二次写入被唯一键吸收,不产生重复证据。
  • idx_ev_learner(tenant_id, learner_id, occurred_at)idx_ev_artifact(tenant_id, artifact_id)idx_ev_type_time(tenant_id, evidence_type, occurred_at)idx_ev_batch(tenant_id, collect_batch_id)

CRUD 定义在 json/evidence_pbl_artifact.json / json/evidence_pbl_evidence.json tblname + paramseditable 的 new/update/delete_data_url 指向 wwwroot/api/ 自定义 dspy

Installation

作为模块,它随宿主应用一起安装(不单独起服务):

# 1) 安装 Python 包(在宿主 pkgs/ 或机构 modules/ 下)
cd pbl_evidence && pip install -e .

# 2) 生成建表 DDL 并执行models/ 为四段式 JSON用 json2ddl
cd models && json2ddl mysql . > mysql.ddl.sql
mysql -h <db_host> -u<user> -p<pwd> <dbname> < mysql.ddl.sql
#    (手工建表务必带 COLLATE utf8mb4_unicode_ci否则与 xls2ddl 产物 collation 混用报 1267

# 3) 生成 CRUD 前端(可选;已有手写 api/ dspy 时按 build.sh 流程执行)
cd json && xls2ui -m ../models -o ../wwwroot pbl_evidence *.json

# 4) 登记 RBAC 权限(双层:模块层 + 中央宿主层,见下)
python3 scripts/load_path.py --check          # 清单与 wwwroot 磁盘一致性核对(不连库)
RBAC_SET_PERM=/path/to/set_role_perm.py python3 scripts/load_path.py   # 实际写权限

scripts/load_path.py 逐条显式登记(禁通配符/pbl_evidence/index.uiwwwroot/api/ 下全部 9 个 .dspy,角色均为 logined。中央宿主 apps/pbls/scripts/load_path.py 另有一份等价的硬编码兜底清单(不同代码路径,模块脚本 import 失败时仍能登记)—— 双层 RBAC 回退,两层都必须在册。

自检(无需连库,可放 CI

python3 scripts/selfcheck_m5a.py    # 三处同步 / RBAC 清单 vs 磁盘 / 模型四段式 / 幂等键语义

Integration宿主挂载方式

模块只依赖基础包sqlor、ahserver ServerEnv、appPublic 工具)、pbl_common(租户上下文/ 错误码/时间序列化)、以及自己的两张表。不依赖任何宿主入口文件或宿主的 wwwroot 路径。

# apps/pbls/app/pbls.py
from ahserver.serverenv import ServerEnv
from appbase.init import load_appbase
from rbac.init import load_rbac
from bricks_for_python.init import load_pybricks

def get_module_dbname(m):
    # 库名由宿主决定;本模块绝不硬编码 DBNAME
    return MODULE_DBNAME_MAP.get(m, DEFAULT_DBNAME)

def init():
    env = ServerEnv()                       # 必须在 load_rbac() 之前
    env.get_module_dbname = get_module_dbname   # ← 宿主必须先注册,否则本模块 fail-closed
    load_appbase(); load_rbac(); load_pybricks()

    from pbl_evidence.init import load_pbl_evidence
    load_pbl_evidence()                     # 唯一集成点

前置条件:宿主必须在 ServerEnv 上注册 get_module_dbname。取不到库名时本模块直接抛 PblErrorfail-closed不会退化成查错库。租户上下文由 pbl_common.api.tenant_id() 提供, 所有读写强制带 tenant_id,缺失即报错。

函数注册三处同步(改一处必须改三处): ① pbl_evidence/api.py 定义 → ② pbl_evidence/__init__.py 导出 → ③ pbl_evidence/init.py env.<name> = <name>。漏 ② → import 期 ImportError漏 ③ → .dspy 调用 NameError。 新增对外路径还要同步 ④ scripts/load_path.py+ 中央 apps/pbls/scripts/load_path.py)。

目录结构

pbl_evidence/
├── pbl_evidence/            # Python 包(= 模块名)
│   ├── __init__.py          # 导出(三处同步之 ②)
│   ├── init.py              # load_pbl_evidence()(三处同步之 ③)
│   ├── api.py               # 10 个契约函数(三处同步之 ①)
│   ├── collector.py         # 事件扫描 → 映射 → 幂等落库list/stats/watermark
│   ├── evidence_map.py      # 纯函数层事件→证据类型映射、dedup_key、时间规范化
│   └── db.py                # 数据访问层get_module_dbname、列名发现、q_all/q_one/q_exec
├── models/                  # pbl_artifact.json / pbl_evidence.json四段式表定义
├── json/                    # CRUD 定义tblname + params + editable
├── wwwroot/                 # index.ui + api/ 9 个 .dspy自动路由 /pbl_evidence/...
├── sql/pbl_evidence.sql     # 建表 SQL 快照
├── scripts/load_path.py     # RBAC 登记(模块层,禁通配符)+ --check 一致性核对
├── scripts/selfcheck_m5a.py # 离线自检(三处同步 / RBAC / 模型 / 幂等键)
├── skill/SKILL.md           # 面向 agent 的模块技能文档
└── pyproject.toml

已知边界(如实说明)

  • 批量采集(pbl_evidence_collect_from_events)的真实回放未在本地环境跑通:该环境 information_schema.tables 中不存在 pbl_runtime_eventM11b 未上线),采集会按设计抛 CollectError → PBL_E_DB_UNAVAILABLE。已在 M11b 落库的联调环境验证前,此项标注为「待联调」。
  • pbl_evidence_watermark 只作为契约函数注册(供 M5b/cron 内部调用),未单独暴露 .dspy 如需外部轮询调用,新增 .dspy 后必须同步登记 RBAC。