feat(backend): implement Tri-Domain Knowledge Architecture
This commit is contained in:
parent
a71f4051d2
commit
2a1c33539f
@ -15,7 +15,8 @@ async def process_uploaded_material_oss(
|
||||
original_filename: str,
|
||||
context_id: str,
|
||||
push_event_fn,
|
||||
user_id: str
|
||||
user_id: str,
|
||||
org_id: str = "org_001"
|
||||
):
|
||||
"""
|
||||
1. Downloads native PDF from OSS
|
||||
@ -53,7 +54,11 @@ async def process_uploaded_material_oss(
|
||||
storageDir = os.getenv("DEEPVIEW_STORAGE_DIR", os.path.expanduser("~/Downloads/Coding/医生助理智能体/backend/storage"))
|
||||
storage_root = Path(storageDir)
|
||||
user_id_safe = user_id if user_id else "unknown"
|
||||
org_id_safe = org_id if org_id else "org_001"
|
||||
|
||||
user_dir = storage_root / "users" / user_id_safe
|
||||
org_dir = storage_root / "orgs" / org_id_safe
|
||||
platform_dir = storage_root / "platform"
|
||||
|
||||
ext = os.path.splitext(original_filename)[1].lower()
|
||||
|
||||
@ -86,7 +91,13 @@ async def process_uploaded_material_oss(
|
||||
raw_path = base_dir / f"{file_hash}_raw{ext}"
|
||||
md_path = base_dir / f"{file_hash}.md"
|
||||
elif context_id.startswith("wiki:") or context_id == "deepview":
|
||||
base_dir = storage_root / "wiki"
|
||||
# 存入本机构域的 wiki 库
|
||||
base_dir = org_dir / "wiki"
|
||||
raw_path = base_dir / f"{file_hash}_raw{ext}"
|
||||
md_path = base_dir / f"{file_hash}.md"
|
||||
elif context_id.startswith("platform:"):
|
||||
# 预留平台运维口
|
||||
base_dir = platform_dir / "wiki"
|
||||
raw_path = base_dir / f"{file_hash}_raw{ext}"
|
||||
md_path = base_dir / f"{file_hash}.md"
|
||||
elif context_id.startswith("doctor:"):
|
||||
|
||||
@ -180,6 +180,13 @@ class DeepviewSSEServer:
|
||||
os.makedirs(userDir, exist_ok=True)
|
||||
return userDir
|
||||
|
||||
def _getOrgStorageDir(self, orgId: str) -> str:
|
||||
"""返回机构的存储沙箱根路径,不存在时自动创建。"""
|
||||
storageDir = os.getenv("DEEPVIEW_STORAGE_DIR", os.path.expanduser("~/Downloads/Coding/医生助理智能体/backend/storage"))
|
||||
orgDir = os.path.join(storageDir, "orgs", orgId)
|
||||
os.makedirs(orgDir, exist_ok=True)
|
||||
return orgDir
|
||||
|
||||
# ──────────────────────────────────────────────
|
||||
# ──────────────────────────────────────────────
|
||||
|
||||
@ -525,9 +532,14 @@ class DeepviewSSEServer:
|
||||
userObj = await self._extractUser(request)
|
||||
userId = userObj.get("sub", "unknown") if userObj else "unknown"
|
||||
|
||||
# 从环境变量获取存储根目录,并获取用户专属沙箱
|
||||
# 从环境变量获取存储根目录,并获取用户专属沙箱与机构知识域
|
||||
storageDir = os.getenv("DEEPVIEW_STORAGE_DIR", os.path.expanduser("~/Downloads/Coding/医生助理智能体/backend/storage"))
|
||||
userDir = self._getUserStorageDir(userId)
|
||||
|
||||
# 解析 orgId
|
||||
orgId = userObj.get("org", "org_001") if userObj else "org_001"
|
||||
orgDir = self._getOrgStorageDir(orgId)
|
||||
platformDir = os.path.join(storageDir, "platform")
|
||||
|
||||
# 1. 加载唯一的 SKILL.md (上下文无关版)
|
||||
skillPath = os.path.join(
|
||||
@ -557,7 +569,8 @@ class DeepviewSSEServer:
|
||||
## 🎙️ 当前上下文模式:单次面诊录音复盘
|
||||
- 录音 ASR 文件:{asrPath}
|
||||
- 医生风格档案:{userDir}/doctor_profile.md
|
||||
- 企业知识库目录:{storageDir}/wiki/
|
||||
- 企业知识库目录:{orgDir}/wiki/
|
||||
- 平台规则参考:{platformDir}/wiki/
|
||||
|
||||
### 你的工作重心
|
||||
基于这一次面诊录音,分析信任断点、沟通体征、改进建议。
|
||||
@ -572,14 +585,15 @@ class DeepviewSSEServer:
|
||||
- 核心档案:{userDir}/clients/{clientId}/profile.md
|
||||
- 历史录音目录:{userDir}/clients/{clientId}/history/
|
||||
- 医生风格档案:{userDir}/doctor_profile.md
|
||||
- 企业知识库目录:{storageDir}/wiki/
|
||||
- 企业知识库目录:{orgDir}/wiki/
|
||||
- 平台规则参考:{platformDir}/wiki/
|
||||
|
||||
### 你的工作重心
|
||||
基于该客户的全生命周期数据,提供诊前策略、社交杠杆分析、跨品类破冰方案。
|
||||
优先读取 profile.md 获取全景概览,必要时深入 history/ 追溯原始录音细节。
|
||||
"""
|
||||
else:
|
||||
systemPrompt += f"\n\n## 通用模式\n企业知识库目录:{storageDir}/wiki/\n"
|
||||
systemPrompt += f"\n\n## 通用模式\n企业知识库目录:{orgDir}/wiki/\n平台规则参考:{platformDir}/wiki/\n"
|
||||
|
||||
# 3. 加载上下文履历,实现真正的 Stateful Context
|
||||
history = db.get_messages_as_conversation(chatId)
|
||||
@ -649,7 +663,13 @@ class DeepviewSSEServer:
|
||||
userObj = await self._extractUser(request)
|
||||
userId = userObj.get("sub", "unknown") if userObj else "unknown"
|
||||
userDir = self._getUserStorageDir(userId)
|
||||
|
||||
# 解析 orgId
|
||||
orgId = userObj.get("org", "org_001") if userObj else "org_001"
|
||||
orgDir = self._getOrgStorageDir(orgId)
|
||||
|
||||
storageDir = os.getenv("DEEPVIEW_STORAGE_DIR", os.path.expanduser("~/Downloads/Coding/医生助理智能体/backend/storage"))
|
||||
platformDir = os.path.join(storageDir, "platform")
|
||||
|
||||
# 构建基础 Prompt
|
||||
systemPrompt = "你是深维面诊智能军师。\n"
|
||||
@ -662,6 +682,10 @@ class DeepviewSSEServer:
|
||||
systemPrompt = f.read()
|
||||
|
||||
systemPrompt = systemPrompt.replace("{{STORAGE_DIR}}", storageDir)
|
||||
# 兼容一下如果 prompt 里有企业知识库占位
|
||||
# (因为 V3 里移除了,如果有人加回来,这里替换)
|
||||
systemPrompt = systemPrompt.replace(f"{storageDir}/wiki/", f"{orgDir}/wiki/")
|
||||
systemPrompt += f"\n\n## 规则遵循\n企业知识库:{orgDir}/wiki/\n平台规则:{platformDir}/wiki/\n"
|
||||
|
||||
if contextId.startswith("recording:"):
|
||||
recordingId = contextId.split(":", 1)[1]
|
||||
@ -1103,7 +1127,8 @@ xray.module5: track1(数组,每项含node/action/strategy/purpose), track2(数
|
||||
original_filename=filename,
|
||||
context_id=context_id,
|
||||
push_event_fn=self._pushEvent,
|
||||
user_id=user["userId"]
|
||||
user_id=user["userId"],
|
||||
org_id=user.get("org", "org_001")
|
||||
)
|
||||
# 2. X-ray stage (Auto orchestrate)
|
||||
if context_id.startswith("recording:"):
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
|
||||
## ⚙️ 先决上下文 (Required Context)
|
||||
- `<CLIENT_PROFILE>`: `storage/users/{userId}/clients/{client_id}/profile.md`
|
||||
- `<WIKI>`: `storage/wiki/`
|
||||
- `<WIKI>`: 机构知识库 `storage/orgs/{orgId}/wiki/`,以及平台规则 `storage/platform/wiki/`
|
||||
- `<NEW_ASR_INPUT>`: `storage/users/{userId}/clients/{client_id}/history/{date}.md`或者 `inbox` 裸推演
|
||||
|
||||
## 🧠 运行指令 (Execution Instructions)
|
||||
|
||||
193
docs/SPEC_tri_domain_knowledge_architecture.md
Normal file
193
docs/SPEC_tri_domain_knowledge_architecture.md
Normal file
@ -0,0 +1,193 @@
|
||||
# SPEC: 三元知识域架构 (Tri-Domain Knowledge Architecture)
|
||||
|
||||
> **版本**: v1.0
|
||||
> **前置依赖**: [SPEC_deepview_metadata_scoping_v3.md](./SPEC_deepview_metadata_scoping_v3.md) (Prosumer-First 沙箱隔离)
|
||||
> **设计哲学**: [DESIGN_PHILOSOPHY_AI_NATIVE.md](./DESIGN_PHILOSOPHY_AI_NATIVE.md) (AI-Native 产品心法)
|
||||
> **状态**: 草案 (Proposal)
|
||||
|
||||
---
|
||||
|
||||
## 1. 问题陈述
|
||||
|
||||
V3 SPEC 成功地将用户数据(客户档案、录音、个人风格)锁死在 `storage/users/{userId}/` 的物理沙箱内。但当前的 `storage/wiki/` 仍然是一个**无主的全局公域**——任何经过认证的用户上传的知识库文件,都会落入这个公有池,并被所有用户的 Agent 无差别读取。
|
||||
|
||||
在单机构运营阶段,这不构成安全问题。但本产品作为一款轻量级 SaaS 工具,**将很快遇到多家医疗机构同时使用的场景**。届时:
|
||||
|
||||
| 风险 | 场景 |
|
||||
|------|------|
|
||||
| **知识污染** | A 机构的"报价策略.md"被 B 机构医生的 Agent 读取,导致 AI 输出了竞争对手的定价策略 |
|
||||
| **方法论冲突** | A 机构上传的"保守型话术规范"与 B 机构的"激进型破冰话术"同在一个池中,Agent 无法判断听谁的 |
|
||||
| **逆向泄密** | 医生在对话中追问"知识库里有什么内容",Agent 如实汇报,变相暴露了其他机构的核心商业资产 |
|
||||
|
||||
**结论**:`wiki/` 需要从"一个桶"裂变为"三元域",否则沙箱隔离只做了一半。
|
||||
|
||||
---
|
||||
|
||||
## 2. 三元域模型 (Tri-Domain Model)
|
||||
|
||||
### 2.1 架构总览
|
||||
|
||||
```
|
||||
storage/
|
||||
├── platform/ ← 🌐 Domain 1: 平台域 (Platform)
|
||||
│ └── wiki/
|
||||
│ ├── system_instructions.md # 系统级行为约束
|
||||
│ └── medical_ethics_baseline.md # 医疗合规底线
|
||||
│
|
||||
├── orgs/ ← 🏢 Domain 2: 机构域 (Organization)
|
||||
│ ├── {orgId_A}/
|
||||
│ │ └── wiki/
|
||||
│ │ ├── rfm_rules.md # A机构的 RFM 客群规则
|
||||
│ │ ├── pricing_2026.md # A机构的报价表
|
||||
│ │ └── talk_track_v3.md # A机构的话术规范
|
||||
│ │
|
||||
│ └── {orgId_B}/
|
||||
│ └── wiki/
|
||||
│ └── ... # B机构的独立知识库
|
||||
│
|
||||
└── users/ ← 👤 Domain 3: 个人域 (User)
|
||||
└── {userId}/
|
||||
├── clients/ # 私有客户档案 (V3 已实现)
|
||||
├── inbox/ # 未归档录音 (V3 已实现)
|
||||
└── doctor_profile.md # 个人风格画像 (V3 已实现)
|
||||
```
|
||||
|
||||
### 2.2 三元域的职责定义
|
||||
|
||||
| 域 | 物理路径 | 写入权限 | 读取权限 | 内容特征 |
|
||||
|----|---------|---------|---------|---------|
|
||||
| **平台域** `platform/wiki/` | 仅运维/开发者 | 所有用户(只读) | 通用底线:医疗合规、AI 行为红线、系统级兜底 Prompt |
|
||||
| **机构域** `orgs/{orgId}/wiki/` | 机构管理员 | 该机构下所有用户 | 企业差异化资产:RFM 规则、定价表、话术方法论、培训心法 |
|
||||
| **个人域** `users/{userId}/` | 用户本人 | 仅用户本人 | 私有工作数据:客户、录音、个人风格、Inbox |
|
||||
|
||||
### 2.3 上下文注入优先级
|
||||
|
||||
当 Agent 构建 System Prompt 时,三元域按**由近及远**的优先级叠加注入:
|
||||
|
||||
```
|
||||
[优先级 1] 个人域 → doctor_profile.md, 当前客户 profile.md
|
||||
[优先级 2] 机构域 → orgs/{orgId}/wiki/*
|
||||
[优先级 3] 平台域 → platform/wiki/*
|
||||
```
|
||||
|
||||
**冲突解决原则**:近域覆盖远域。如果个人域的 `doctor_profile.md` 声明了"我不使用激进话术",即使机构域的 `talk_track_v3.md` 中含有激进话术范例,Agent 应优先遵从个人偏好。
|
||||
|
||||
---
|
||||
|
||||
## 3. 用户与机构的关系模型
|
||||
|
||||
### 3.1 Prosumer 生命周期
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ Phase 1: 自由人 (Freelancer) │
|
||||
│ ─ 注册即可用,无需任何机构归属 │
|
||||
│ ─ Agent 读取: platform/wiki/* + users/{userId}/* │
|
||||
│ ─ orgs 层级对该用户透明(不存在) │
|
||||
├──────────────────────────────────────────────────────────────┤
|
||||
│ Phase 2: 受邀加入机构 (Org Member) │
|
||||
│ ─ 机构管理员发出邀请码,用户确认加入后绑定 orgId │
|
||||
│ ─ Agent 读取: platform/* + orgs/{orgId}/* + users/{userId}/* │
|
||||
│ ─ 个人域数据对机构管理员不可见(除非显式授权移交) │
|
||||
├──────────────────────────────────────────────────────────────┤
|
||||
│ Phase 3: 授权联邦 (Federation) [远期预留] │
|
||||
│ ─ 医生主动授权特定客户档案向机构汇报系统开放 │
|
||||
│ ─ 机构管理层获得聚合视图(只读),不触碰个人原始录音 │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3.2 数据流向的不可逆栅栏
|
||||
|
||||
- **上行不可逆**:个人域数据只有经用户显式确认后,才可拷贝(非移动)至机构域的聚合报告。机构管理员永远不可直接遍历 `users/{userId}/` 目录。
|
||||
- **下行只读注入**:机构域和平台域对个人域的影响仅限于"知识注入"(Agent Prompt 中追加上下文)。任何域都不能向个人域写入文件。
|
||||
- **跨机构绝对隔离**:`orgs/{orgId_A}/` 与 `orgs/{orgId_B}/` 之间零交集。一个用户同时隶属两家机构时,Agent 只加载当前活跃会话所选的机构上下文。
|
||||
|
||||
---
|
||||
|
||||
## 4. 现有 `wiki/` 的迁移路径
|
||||
|
||||
当前 `storage/wiki/gemini_rfm_rules.md` 实质上是某一家机构的商业方法论,不应继续驻留在全局公域。
|
||||
|
||||
### 4.1 迁移策略(渐进式)
|
||||
|
||||
| 阶段 | 动作 | 影响 |
|
||||
|------|------|------|
|
||||
| **Phase 0 (当前)** | `storage/wiki/` 保持原址不动 | 继续服务单机构运营,无破坏性 |
|
||||
| **Phase 1 (首次多机构)** | 引入 `storage/platform/wiki/` 存放通用底线;将 `storage/wiki/` 整体 rename 为 `storage/orgs/{首个orgId}/wiki/` | 一次性 rename,后端路径切换 |
|
||||
| **Phase 2 (稳态)** | 新机构入驻时,由运维在 `storage/orgs/{newOrgId}/wiki/` 下初始化空目录 | 零干扰,增量部署 |
|
||||
|
||||
### 4.2 后端改动影响面预估
|
||||
|
||||
需改动的位置与 V3 沙箱重构完全同构,核心变量仅从 `storageDir + "/wiki"` 变为 `storageDir + "/orgs/" + orgId + "/wiki"`:
|
||||
|
||||
| 文件 | 函数 | 变更内容 |
|
||||
|------|------|---------|
|
||||
| `deepview_sse.py` | `_handleChat` | Prompt 中 wiki 路径追加 orgId |
|
||||
| `deepview_sse.py` | `_handleReportGenerate` | 同上 |
|
||||
| `deepview_materials.py` | `process_uploaded_material_oss` | `wiki:` 上下文路由至 `orgs/{orgId}/wiki/` |
|
||||
| `deepview_sse.py` | `_handleMaterialsList` | 列表查询时过滤至当前用户 orgId |
|
||||
|
||||
### 4.3 前端改动
|
||||
|
||||
- 零改动(与 V3 一致)。orgId 通过 JWT Token 中的 claim(如 `org` 字段)透传,前端感知不到域的存在。
|
||||
|
||||
---
|
||||
|
||||
## 5. 安全边界与防御策略
|
||||
|
||||
### 5.1 物理隔离防线
|
||||
|
||||
```python
|
||||
# 防路径穿越攻击:所有用户可控的 path 片段必须经过白名单校验
|
||||
def _sanitizePath(segment: str) -> str:
|
||||
"""只允许字母数字下划线连字符,严禁 .. / \\ 等"""
|
||||
import re
|
||||
if not re.match(r'^[a-zA-Z0-9_\-]+$', segment):
|
||||
raise ValueError(f"Invalid path segment: {segment}")
|
||||
return segment
|
||||
```
|
||||
|
||||
### 5.2 Agent 工具权限矩阵
|
||||
|
||||
| Agent 工具 | 平台域 | 机构域 (本org) | 机构域 (他org) | 个人域 (本人) | 个人域 (他人) |
|
||||
|-----------|--------|--------------|--------------|-------------|-------------|
|
||||
| `file_read` | ✅ | ✅ | ❌ | ✅ | ❌ |
|
||||
| `file_write` | ❌ | ❌ | ❌ | ✅ | ❌ |
|
||||
| `file_list` | ✅ | ✅ | ❌ | ✅ | ❌ |
|
||||
|
||||
### 5.3 审计日志(远期)
|
||||
|
||||
对 `orgs/{orgId}/wiki/` 目录的任何写入操作,应记录 `(timestamp, userId, action, filePath)` 至审计表,支持机构管理员追溯"谁在什么时候传了什么"。
|
||||
|
||||
---
|
||||
|
||||
## 6. 与 V3 SPEC 的关系
|
||||
|
||||
本 SPEC 是 V3 的**正统延伸**,不是替代。二者的关系为:
|
||||
|
||||
- **V3 SPEC** 解决了"个人域"的物理隔离问题(`users/{userId}/`)
|
||||
- **本 SPEC** 在 V3 之上,向外延伸了"机构域"(`orgs/{orgId}/`)和"平台域"(`platform/`),形成完整的三元安全边界
|
||||
|
||||
三者合并后的**完整隔离模型**:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ Platform Domain (只读注入, 运维独占写) │
|
||||
│ ┌─────────────────────────────────────────┐ │
|
||||
│ │ Org Domain (机构管理员写, 成员只读) │ │
|
||||
│ │ ┌─────────────────────────────────────┐ │ │
|
||||
│ │ │ User Domain (用户独占读写) │ │ │
|
||||
│ │ │ ┌──────────────────────┐ │ │ │
|
||||
│ │ │ │ Client Data (归档后) │ │ │ │
|
||||
│ │ │ └──────────────────────┘ │ │ │
|
||||
│ │ │ ┌──────────────────────┐ │ │ │
|
||||
│ │ │ │ Inbox (裸推演态) │ │ │ │
|
||||
│ │ │ └──────────────────────┘ │ │ │
|
||||
│ │ └─────────────────────────────────────┘ │ │
|
||||
│ └─────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
> *本规范为深维医生助理三元知识域架构草案,经审阅确认后,将指导后续的机构域隔离实现。*
|
||||
Loading…
Reference in New Issue
Block a user