feat: TenantSource 抽象——File源(测试/档案)与DB源(生产),registry 数据源可切换+reload upsert

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-10-13 08:50:16 +08:00
parent 386b5eb90a
commit c7fd533320
9 changed files with 720 additions and 85 deletions
+4 -1
View File
@@ -81,7 +81,10 @@ async def avatar(request: Request):
# 任意文件。显式拒绝分隔符与目录点,命中即回退静态兜底。
if name in {".", ".."} or "/" in name or "\\" in name:
name = ""
candidate = runtime.seed_dir / "assets" / name if name else None
# seed_dir=None = DB 源租户(Task 4;资产目录尚无 DB 侧存储)——直接
# 落静态兜底,不让 None 进路径拼接炸 500
candidate = (runtime.seed_dir / "assets" / name
if name and runtime.seed_dir is not None else None)
if candidate is not None and candidate.is_file():
return FileResponse(
candidate, headers={"Access-Control-Allow-Origin": "*"})
+30 -9
View File
@@ -47,6 +47,11 @@ def _format_deflection_pool(deflections) -> str:
)
# 四件套键名(单一事实源): PromptProvider 构造校验 / FileTenantSource 读盘
# / DbTenantSource 快照组形态, 三处消费同一清单
PROMPTS_KEYS = ("system", "company_facts", "extractor", "evaluator")
@dataclass(frozen=True)
class PromptProvider:
"""租户提示词四件套持有者(Task 6, R1 话术单一事实源)。
@@ -54,9 +59,12 @@ class PromptProvider:
frozen(Task 9 收口, Task 6 审查遗留): 实例跨租户运行时/模块级
DEFAULT_PROVIDER 共享, 不可变防共享单例被毒化。
- from_dir(dir): 读 system/company_facts/extractor/evaluator 四个 md,
存实例字段——提示词属于租户(seed_profiles/<slug>/prompts/),不再有
代码级 app/engine/prompts 副本;
- from_dir(dir): 读 system/company_facts/extractor/evaluator 四个 md
委托 from_config——提示词属于租户(seed_profiles/<slug>/prompts/),
不再有代码级 app/engine/prompts 副本;
- from_config(md: dict)(Phase 2 Task 4): 快照 prompts 组(dict 四键)
直接构造——registry 对文件/DB 两源的统一入口(文件源读盘入快照,
DB 源嵌 config_json);缺键/值非字符串 → 显性 ValueError 带键名;
- build_system(wall_deflections): system.md 的 $DEFLECTION_POOL 占位符
替换为按语言枚举的话术清单 + "\\n\\n" + company_facts(行为 prompt
在前、事实卡居中、当轮提醒最后拼接——拼接位在 AIService 内);
@@ -68,15 +76,28 @@ class PromptProvider:
extractor: str
evaluator: str
@classmethod
def from_config(cls, md: dict) -> "PromptProvider":
"""快照 prompts 组 → PromptProvider。缺键/非字符串显性炸带键名
(DB 快照坏数据不留到 build_system 才炸)。"""
bad = [k for k in PROMPTS_KEYS if not isinstance((md or {}).get(k), str)]
if bad:
raise ValueError(f"prompts 配置缺少(或非字符串)键: {', '.join(bad)}")
return cls(
system=md["system"],
company_facts=md["company_facts"],
extractor=md["extractor"],
evaluator=md["evaluator"])
@classmethod
def from_dir(cls, prompts_dir) -> "PromptProvider":
d = Path(prompts_dir)
return cls(
system=(d / "system.md").read_text(encoding="utf-8"),
company_facts=(d / "company_facts.md").read_text(encoding="utf-8"),
extractor=(d / "extractor.md").read_text(encoding="utf-8"),
evaluator=(d / "evaluator.md").read_text(encoding="utf-8"),
)
return cls.from_config({
"system": (d / "system.md").read_text(encoding="utf-8"),
"company_facts": (d / "company_facts.md").read_text(encoding="utf-8"),
"extractor": (d / "extractor.md").read_text(encoding="utf-8"),
"evaluator": (d / "evaluator.md").read_text(encoding="utf-8"),
})
def build_system(self, wall_deflections=None) -> str:
system = self.system