feat(backend): implement prosumer storage isolation pattern
This commit is contained in:
parent
e6cf28cb55
commit
a71f4051d2
@ -8,9 +8,9 @@
|
|||||||
1. **服从企业知识库 (Wiki Obedience)**
|
1. **服从企业知识库 (Wiki Obedience)**
|
||||||
- 在任何战略推荐中,必须严格贯彻 `storage/wiki/` 目录下的方法论(如 RFM 客群召回策略)。如果客户的画像符合“A4重要挽留”,必须使用对应的专属激活打法。
|
- 在任何战略推荐中,必须严格贯彻 `storage/wiki/` 目录下的方法论(如 RFM 客群召回策略)。如果客户的画像符合“A4重要挽留”,必须使用对应的专属激活打法。
|
||||||
2. **千人千面适配 (Persona Adoption)**
|
2. **千人千面适配 (Persona Adoption)**
|
||||||
- 每个医生/顾问的沟通口吻截然不同。你必须在每次输出前读取对应 `storage/doctors/{doc_id}.md` 的偏好设定,将你给出的“话术范例”转换成该医生的真实风格(禁止使用他反感的术语)。
|
- 每个医生/顾问的沟通口吻截然不同。你必须在每次输出前读取对应 `storage/users/{userId}/doctor_profile.md` 的偏好设定,将你给出的“话术范例”转换成该医生的真实风格(禁止使用他反感的术语)。
|
||||||
3. **基于真实档案 (Profile Grounding)**
|
3. **基于真实档案 (Profile Grounding)**
|
||||||
- 所有诊断、切入点,必须100%基于 `storage/clients/{client_id}/profile.md` 中已经记录的痛点、引荐关系网、真实效果回馈。绝不允许凭空捏造痛点。
|
- 所有诊断、切入点,必须100%基于 `storage/users/{userId}/clients/{client_id}/profile.md` 中已经记录的痛点、引荐关系网、真实效果回馈。绝不允许凭空捏造痛点。
|
||||||
|
|
||||||
## 🔍 Execution Cycle (运行生命周期)
|
## 🔍 Execution Cycle (运行生命周期)
|
||||||
- 当你收到 `Chat` 或者 `Strategy Pull` 唤醒时:
|
- 当你收到 `Chat` 或者 `Strategy Pull` 唤醒时:
|
||||||
|
|||||||
@ -52,6 +52,8 @@ async def process_uploaded_material_oss(
|
|||||||
# 3. 解析 Context 路由(深维专属双上下文 FS-as-Database 管线)
|
# 3. 解析 Context 路由(深维专属双上下文 FS-as-Database 管线)
|
||||||
storageDir = os.getenv("DEEPVIEW_STORAGE_DIR", os.path.expanduser("~/Downloads/Coding/医生助理智能体/backend/storage"))
|
storageDir = os.getenv("DEEPVIEW_STORAGE_DIR", os.path.expanduser("~/Downloads/Coding/医生助理智能体/backend/storage"))
|
||||||
storage_root = Path(storageDir)
|
storage_root = Path(storageDir)
|
||||||
|
user_id_safe = user_id if user_id else "unknown"
|
||||||
|
user_dir = storage_root / "users" / user_id_safe
|
||||||
|
|
||||||
ext = os.path.splitext(original_filename)[1].lower()
|
ext = os.path.splitext(original_filename)[1].lower()
|
||||||
|
|
||||||
@ -68,18 +70,18 @@ async def process_uploaded_material_oss(
|
|||||||
# 旧格式(已归档):recording:clientId/asrId
|
# 旧格式(已归档):recording:clientId/asrId
|
||||||
clientId = parts[0]
|
clientId = parts[0]
|
||||||
asrId = parts[-1]
|
asrId = parts[-1]
|
||||||
base_dir = storage_root / "clients" / clientId / "history"
|
base_dir = user_dir / "clients" / clientId / "history"
|
||||||
raw_path = base_dir / f"{asrId}_raw{ext}"
|
raw_path = base_dir / f"{asrId}_raw{ext}"
|
||||||
md_path = base_dir / f"{asrId}.md"
|
md_path = base_dir / f"{asrId}.md"
|
||||||
else:
|
else:
|
||||||
# 新格式(Inbox):recording:asrId
|
# 新格式(Inbox):recording:asrId
|
||||||
asrId = parts[0]
|
asrId = parts[0]
|
||||||
base_dir = storage_root / "inbox" / asrId
|
base_dir = user_dir / "inbox" / asrId
|
||||||
raw_path = base_dir / f"asr_raw{ext}"
|
raw_path = base_dir / f"asr_raw{ext}"
|
||||||
md_path = base_dir / "asr.md"
|
md_path = base_dir / "asr.md"
|
||||||
elif context_id.startswith("client:"):
|
elif context_id.startswith("client:"):
|
||||||
clientId = context_id.split(":", 1)[1]
|
clientId = context_id.split(":", 1)[1]
|
||||||
base_dir = storage_root / "clients" / clientId
|
base_dir = user_dir / "clients" / clientId
|
||||||
# 客户全景上下文:碎片化资料存档(可能是合同、病历单据等)
|
# 客户全景上下文:碎片化资料存档(可能是合同、病历单据等)
|
||||||
raw_path = base_dir / f"{file_hash}_raw{ext}"
|
raw_path = base_dir / f"{file_hash}_raw{ext}"
|
||||||
md_path = base_dir / f"{file_hash}.md"
|
md_path = base_dir / f"{file_hash}.md"
|
||||||
@ -88,13 +90,12 @@ async def process_uploaded_material_oss(
|
|||||||
raw_path = base_dir / f"{file_hash}_raw{ext}"
|
raw_path = base_dir / f"{file_hash}_raw{ext}"
|
||||||
md_path = base_dir / f"{file_hash}.md"
|
md_path = base_dir / f"{file_hash}.md"
|
||||||
elif context_id.startswith("doctor:"):
|
elif context_id.startswith("doctor:"):
|
||||||
doctorId = context_id.split(":", 1)[1]
|
base_dir = user_dir
|
||||||
base_dir = storage_root / "doctors"
|
raw_path = base_dir / f"doctor_profile_raw{ext}"
|
||||||
raw_path = base_dir / f"{doctorId}_raw{ext}"
|
md_path = base_dir / "doctor_profile.md"
|
||||||
md_path = base_dir / f"{doctorId}.md"
|
|
||||||
else:
|
else:
|
||||||
# Fallback 容错隔离区
|
# Fallback 容错隔离区
|
||||||
base_dir = storage_root / "misc" / context_id.replace(":", "_")
|
base_dir = user_dir / "misc" / context_id.replace(":", "_")
|
||||||
raw_path = base_dir / f"{file_hash}_raw{ext}"
|
raw_path = base_dir / f"{file_hash}_raw{ext}"
|
||||||
md_path = base_dir / f"{file_hash}.md"
|
md_path = base_dir / f"{file_hash}.md"
|
||||||
|
|
||||||
|
|||||||
@ -170,7 +170,17 @@ class DeepviewSSEServer:
|
|||||||
self._initProfilesDb()
|
self._initProfilesDb()
|
||||||
|
|
||||||
# ──────────────────────────────────────────────
|
# ──────────────────────────────────────────────
|
||||||
# SSE 推送基础设施
|
# 存储辅助方法
|
||||||
|
# ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _getUserStorageDir(self, userId: str) -> str:
|
||||||
|
"""返回当前用户的专属存储沙箱根路径,不存在时自动创建。"""
|
||||||
|
storageDir = os.getenv("DEEPVIEW_STORAGE_DIR", os.path.expanduser("~/Downloads/Coding/医生助理智能体/backend/storage"))
|
||||||
|
userDir = os.path.join(storageDir, "users", userId)
|
||||||
|
os.makedirs(userDir, exist_ok=True)
|
||||||
|
return userDir
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────
|
||||||
# ──────────────────────────────────────────────
|
# ──────────────────────────────────────────────
|
||||||
|
|
||||||
def _pushEvent(self, userId: str, eventName: str, data: dict) -> None:
|
def _pushEvent(self, userId: str, eventName: str, data: dict) -> None:
|
||||||
@ -511,8 +521,13 @@ class DeepviewSSEServer:
|
|||||||
db = SessionDB()
|
db = SessionDB()
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
|
|
||||||
# 从环境变量获取存储根目录
|
# 此处获取 user 的 userId
|
||||||
|
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"))
|
storageDir = os.getenv("DEEPVIEW_STORAGE_DIR", os.path.expanduser("~/Downloads/Coding/医生助理智能体/backend/storage"))
|
||||||
|
userDir = self._getUserStorageDir(userId)
|
||||||
|
|
||||||
# 1. 加载唯一的 SKILL.md (上下文无关版)
|
# 1. 加载唯一的 SKILL.md (上下文无关版)
|
||||||
skillPath = os.path.join(
|
skillPath = os.path.join(
|
||||||
@ -526,7 +541,6 @@ class DeepviewSSEServer:
|
|||||||
systemPrompt = f.read()
|
systemPrompt = f.read()
|
||||||
|
|
||||||
systemPrompt = systemPrompt.replace("{{STORAGE_DIR}}", storageDir)
|
systemPrompt = systemPrompt.replace("{{STORAGE_DIR}}", storageDir)
|
||||||
systemPrompt = systemPrompt.replace("{{DOCTOR_ID}}", doctorId)
|
|
||||||
|
|
||||||
# 2. 核心架构逻辑:根据 contextId 前缀路由上下文包
|
# 2. 核心架构逻辑:根据 contextId 前缀路由上下文包
|
||||||
if contextId.startswith("recording:"):
|
if contextId.startswith("recording:"):
|
||||||
@ -536,13 +550,13 @@ class DeepviewSSEServer:
|
|||||||
parts = recordingId.split("/")
|
parts = recordingId.split("/")
|
||||||
clientId = parts[0] if len(parts) > 1 else "unknown_client"
|
clientId = parts[0] if len(parts) > 1 else "unknown_client"
|
||||||
asrId = parts[-1]
|
asrId = parts[-1]
|
||||||
asrPath = f"{storageDir}/clients/{clientId}/history/{asrId}.md"
|
asrPath = f"{userDir}/clients/{clientId}/history/{asrId}.md"
|
||||||
|
|
||||||
systemPrompt += f"""
|
systemPrompt += f"""
|
||||||
|
|
||||||
## 🎙️ 当前上下文模式:单次面诊录音复盘
|
## 🎙️ 当前上下文模式:单次面诊录音复盘
|
||||||
- 录音 ASR 文件:{asrPath}
|
- 录音 ASR 文件:{asrPath}
|
||||||
- 医生风格档案:{storageDir}/doctors/{doctorId}.md
|
- 医生风格档案:{userDir}/doctor_profile.md
|
||||||
- 企业知识库目录:{storageDir}/wiki/
|
- 企业知识库目录:{storageDir}/wiki/
|
||||||
|
|
||||||
### 你的工作重心
|
### 你的工作重心
|
||||||
@ -554,10 +568,10 @@ class DeepviewSSEServer:
|
|||||||
systemPrompt += f"""
|
systemPrompt += f"""
|
||||||
|
|
||||||
## 👤 当前上下文模式:客户全景档案
|
## 👤 当前上下文模式:客户全景档案
|
||||||
- 客户档案目录:{storageDir}/clients/{clientId}/
|
- 客户档案目录:{userDir}/clients/{clientId}/
|
||||||
- 核心档案:{storageDir}/clients/{clientId}/profile.md
|
- 核心档案:{userDir}/clients/{clientId}/profile.md
|
||||||
- 历史录音目录:{storageDir}/clients/{clientId}/history/
|
- 历史录音目录:{userDir}/clients/{clientId}/history/
|
||||||
- 医生风格档案:{storageDir}/doctors/{doctorId}.md
|
- 医生风格档案:{userDir}/doctor_profile.md
|
||||||
- 企业知识库目录:{storageDir}/wiki/
|
- 企业知识库目录:{storageDir}/wiki/
|
||||||
|
|
||||||
### 你的工作重心
|
### 你的工作重心
|
||||||
@ -632,6 +646,9 @@ class DeepviewSSEServer:
|
|||||||
if not contextId:
|
if not contextId:
|
||||||
return web.json_response({"error": "Missing contextId"}, status=400, headers=_CORS_HEADERS)
|
return web.json_response({"error": "Missing contextId"}, status=400, headers=_CORS_HEADERS)
|
||||||
|
|
||||||
|
userObj = await self._extractUser(request)
|
||||||
|
userId = userObj.get("sub", "unknown") if userObj else "unknown"
|
||||||
|
userDir = self._getUserStorageDir(userId)
|
||||||
storageDir = os.getenv("DEEPVIEW_STORAGE_DIR", os.path.expanduser("~/Downloads/Coding/医生助理智能体/backend/storage"))
|
storageDir = os.getenv("DEEPVIEW_STORAGE_DIR", os.path.expanduser("~/Downloads/Coding/医生助理智能体/backend/storage"))
|
||||||
|
|
||||||
# 构建基础 Prompt
|
# 构建基础 Prompt
|
||||||
@ -645,7 +662,6 @@ class DeepviewSSEServer:
|
|||||||
systemPrompt = f.read()
|
systemPrompt = f.read()
|
||||||
|
|
||||||
systemPrompt = systemPrompt.replace("{{STORAGE_DIR}}", storageDir)
|
systemPrompt = systemPrompt.replace("{{STORAGE_DIR}}", storageDir)
|
||||||
systemPrompt = systemPrompt.replace("{{DOCTOR_ID}}", doctorId)
|
|
||||||
|
|
||||||
if contextId.startswith("recording:"):
|
if contextId.startswith("recording:"):
|
||||||
recordingId = contextId.split(":", 1)[1]
|
recordingId = contextId.split(":", 1)[1]
|
||||||
@ -656,14 +672,14 @@ class DeepviewSSEServer:
|
|||||||
# 旧格式(已归档):recording:clientId/asrId
|
# 旧格式(已归档):recording:clientId/asrId
|
||||||
clientId = parts[0]
|
clientId = parts[0]
|
||||||
asrId = parts[-1]
|
asrId = parts[-1]
|
||||||
asrPath = f"{storageDir}/clients/{clientId}/history/{asrId}.md"
|
asrPath = f"{userDir}/clients/{clientId}/history/{asrId}.md"
|
||||||
clientProfilePath = f"{storageDir}/clients/{clientId}/profile.md"
|
clientProfilePath = f"{userDir}/clients/{clientId}/profile.md"
|
||||||
else:
|
else:
|
||||||
# 新格式(Inbox):recording:asrId
|
# 新格式(Inbox):recording:asrId
|
||||||
asrId = parts[0]
|
asrId = parts[0]
|
||||||
asrPath = f"{storageDir}/inbox/{asrId}/asr.md"
|
asrPath = f"{userDir}/inbox/{asrId}/asr.md"
|
||||||
clientProfilePath = None
|
clientProfilePath = None
|
||||||
systemPrompt += f"\n## 🎙️ 第1模式:单条面诊录音复盘\n你的唯一任务是提取基于这通录音的战术复盘。\n录音路径:{asrPath}\n医生档案参考:{storageDir}/doctors/{doctorId}.md"
|
systemPrompt += f"\n## 🎙️ 第1模式:单条面诊录音复盘\n你的唯一任务是提取基于这通录音的战术复盘。\n录音路径:{asrPath}\n医生档案参考:{userDir}/doctor_profile.md"
|
||||||
if clientProfilePath and os.path.exists(clientProfilePath):
|
if clientProfilePath and os.path.exists(clientProfilePath):
|
||||||
systemPrompt += f"\n客户档案参考:{clientProfilePath}"
|
systemPrompt += f"\n客户档案参考:{clientProfilePath}"
|
||||||
systemPrompt += f"\n\n🚨 【系统硬约束:Speaker 天然推断指令】"
|
systemPrompt += f"\n\n🚨 【系统硬约束:Speaker 天然推断指令】"
|
||||||
@ -671,7 +687,7 @@ class DeepviewSSEServer:
|
|||||||
systemPrompt += f"\n在输出所有报告正文时,请直接使用真实姓名,彻底抹除 Speaker_X 痕迹。"
|
systemPrompt += f"\n在输出所有报告正文时,请直接使用真实姓名,彻底抹除 Speaker_X 痕迹。"
|
||||||
else:
|
else:
|
||||||
clientId = contextId.split(":", 1)[1] if ":" in contextId else contextId
|
clientId = contextId.split(":", 1)[1] if ":" in contextId else contextId
|
||||||
systemPrompt += f"\n## 👤 第2模式:客户全景档案战略\n你的唯一任务是生成该客户的全景战略洞察报告。\n档案路径:{storageDir}/clients/{clientId}/profile.md\n历史录音目录:{storageDir}/clients/{clientId}/history/\n医生风格:{storageDir}/doctors/{doctorId}.md"
|
systemPrompt += f"\n## 👤 第2模式:客户全景档案战略\n你的唯一任务是生成该客户的全景战略洞察报告。\n档案路径:{userDir}/clients/{clientId}/profile.md\n历史录音目录:{userDir}/clients/{clientId}/history/\n医生风格:{userDir}/doctor_profile.md"
|
||||||
# ── Stage 1 内容引擎: Hermes AIAgent (md2md) ──
|
# ── Stage 1 内容引擎: Hermes AIAgent (md2md) ──
|
||||||
# 只负责生成富文本分析报告,不管格式
|
# 只负责生成富文本分析报告,不管格式
|
||||||
contentChecklist = """
|
contentChecklist = """
|
||||||
@ -1477,8 +1493,8 @@ xray.module5: track1(数组,每项含node/action/strategy/purpose), track2(数
|
|||||||
if not user:
|
if not user:
|
||||||
return web.json_response({"error": "Unauthorized"}, status=401, headers=_CORS_HEADERS)
|
return web.json_response({"error": "Unauthorized"}, status=401, headers=_CORS_HEADERS)
|
||||||
|
|
||||||
storageDir = os.getenv("DEEPVIEW_STORAGE_DIR", os.path.expanduser("~/Downloads/Coding/医生助理智能体/backend/storage"))
|
userDir = self._getUserStorageDir(user.get("sub", "unknown"))
|
||||||
clientsDir = os.path.join(storageDir, "clients")
|
clientsDir = os.path.join(userDir, "clients")
|
||||||
|
|
||||||
clients = []
|
clients = []
|
||||||
if os.path.exists(clientsDir):
|
if os.path.exists(clientsDir):
|
||||||
@ -1518,8 +1534,8 @@ xray.module5: track1(数组,每项含node/action/strategy/purpose), track2(数
|
|||||||
import time
|
import time
|
||||||
clientId = f"p_{str(uuid.uuid4())[:8]}"
|
clientId = f"p_{str(uuid.uuid4())[:8]}"
|
||||||
|
|
||||||
storageDir = os.getenv("DEEPVIEW_STORAGE_DIR", os.path.expanduser("~/Downloads/Coding/医生助理智能体/backend/storage"))
|
userDir = self._getUserStorageDir(user.get("sub", "unknown"))
|
||||||
clientDir = os.path.join(storageDir, "clients", clientId)
|
clientDir = os.path.join(userDir, "clients", clientId)
|
||||||
os.makedirs(clientDir, exist_ok=True)
|
os.makedirs(clientDir, exist_ok=True)
|
||||||
|
|
||||||
profileData = f"# 客户基本档案\n姓名:{name}\n手机号/尾号:{phone}\n建档时间:{time.strftime('%Y-%m-%d')}\n"
|
profileData = f"# 客户基本档案\n姓名:{name}\n手机号/尾号:{phone}\n建档时间:{time.strftime('%Y-%m-%d')}\n"
|
||||||
@ -1545,8 +1561,8 @@ xray.module5: track1(数组,每项含node/action/strategy/purpose), track2(数
|
|||||||
if not clientId:
|
if not clientId:
|
||||||
return web.json_response({"error": "Missing client ID"}, status=400, headers=_CORS_HEADERS)
|
return web.json_response({"error": "Missing client ID"}, status=400, headers=_CORS_HEADERS)
|
||||||
|
|
||||||
storageDir = os.getenv("DEEPVIEW_STORAGE_DIR", os.path.expanduser("~/Downloads/Coding/医生助理智能体/backend/storage"))
|
userDir = self._getUserStorageDir(user.get("sub", "unknown"))
|
||||||
profilePath = os.path.join(storageDir, "clients", clientId, "profile.md")
|
profilePath = os.path.join(userDir, "clients", clientId, "profile.md")
|
||||||
|
|
||||||
content = ""
|
content = ""
|
||||||
if os.path.exists(profilePath):
|
if os.path.exists(profilePath):
|
||||||
@ -1584,12 +1600,12 @@ xray.module5: track1(数组,每项含node/action/strategy/purpose), track2(数
|
|||||||
return web.json_response({"error": "Missing reportId or clientId"}, status=400, headers=_CORS_HEADERS)
|
return web.json_response({"error": "Missing reportId or clientId"}, status=400, headers=_CORS_HEADERS)
|
||||||
|
|
||||||
userId = user["userId"]
|
userId = user["userId"]
|
||||||
storageDir = os.getenv("DEEPVIEW_STORAGE_DIR", os.path.expanduser("~/Downloads/Coding/医生助理智能体/backend/storage"))
|
userDir = self._getUserStorageDir(user.get("sub", "unknown"))
|
||||||
|
|
||||||
# 1. 物理文件迁移:inbox/{reportId}/ → clients/{clientId}/history/{reportId}/
|
# 1. 物理文件迁移:users/{userId}/inbox/{reportId}/ → users/{userId}/clients/{clientId}/history/{reportId}/
|
||||||
import shutil
|
import shutil
|
||||||
inboxDir = os.path.join(storageDir, "inbox", reportId)
|
inboxDir = os.path.join(userDir, "inbox", reportId)
|
||||||
targetDir = os.path.join(storageDir, "clients", clientId, "history", reportId)
|
targetDir = os.path.join(userDir, "clients", clientId, "history", reportId)
|
||||||
|
|
||||||
if os.path.exists(inboxDir):
|
if os.path.exists(inboxDir):
|
||||||
os.makedirs(os.path.dirname(targetDir), exist_ok=True)
|
os.makedirs(os.path.dirname(targetDir), exist_ok=True)
|
||||||
|
|||||||
@ -4,9 +4,9 @@
|
|||||||
给定一段全新长文本(通常为一段面诊录音 ASR 转写),Agent 的任务是从中提取出增量的面诊体征数据(信任断点、真实拒因、个人喜好偏好),并将其**无缝追加**到客户的既有 `profile.md` 中。这是碎片化数据生长的引擎。
|
给定一段全新长文本(通常为一段面诊录音 ASR 转写),Agent 的任务是从中提取出增量的面诊体征数据(信任断点、真实拒因、个人喜好偏好),并将其**无缝追加**到客户的既有 `profile.md` 中。这是碎片化数据生长的引擎。
|
||||||
|
|
||||||
## ⚙️ 先决上下文 (Required Context)
|
## ⚙️ 先决上下文 (Required Context)
|
||||||
- `<CLIENT_PROFILE>`: `storage/clients/{client_id}/profile.md`
|
- `<CLIENT_PROFILE>`: `storage/users/{userId}/clients/{client_id}/profile.md`
|
||||||
- `<RFM_WIKI>`: `storage/wiki/gemini_rfm_rules.md`
|
- `<WIKI>`: `storage/wiki/`
|
||||||
- `<NEW_ASR_INPUT>`: `storage/clients/{client_id}/history/{date}.md`
|
- `<NEW_ASR_INPUT>`: `storage/users/{userId}/clients/{client_id}/history/{date}.md`或者 `inbox` 裸推演
|
||||||
|
|
||||||
## 🧠 运行指令 (Execution Instructions)
|
## 🧠 运行指令 (Execution Instructions)
|
||||||
1. **录音精读**:通读 `<NEW_ASR_INPUT>`。寻找客户提到的“核心痛点”、“预算卡点”、“对于过往医疗服务的不满”、“主动提起的熟人/朋友名字”等。
|
1. **录音精读**:通读 `<NEW_ASR_INPUT>`。寻找客户提到的“核心痛点”、“预算卡点”、“对于过往医疗服务的不满”、“主动提起的熟人/朋友名字”等。
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user