M3b:世谱激活接入组装管线(关键词命中/常驻/预算/符文展开)与前端记忆治理页(候选箱/重忆确认/正影/删除/实录溯源)

This commit is contained in:
chenxin committed 2026-08-27 09:22:20 +08:00
1 parent eb0027757d
commit e914aec9c4
8 files changed
+569 -1

No files matched your search

@@ -426,6 +426,15 @@ async def build_generation_prompt(
if recalled:
memory_lines = [f"- {m.content}" for m in recalled]
parts.append(f"【获准记忆(来源可查)】\n" + "\n".join(memory_lines))
# 世谱激活(河图):关键词命中扫描近期文本,常驻条目恒注入;预算内按 insertion_order
tree_character_id = await _tree_character_id(question.tree_id)
activated = await _activate_lorebook(
tree_character_id,
scan_text=f"{transcript[-600:]}\n{question.content}",
expand=(lambda t: render(t, ctx, registry=registry)),
)
if activated:
parts.append("【世界认知(世谱命中)】\n" + "\n".join(f"- {line}" for line in activated))
if transcript:
parts.append(f"【共同经历(祖先路径)】\n{transcript}")
parts.append(f"【当前问题】\n{expand(question.content)}")
@@ -899,3 +908,56 @@ async def _tree_character_id(tree_id: str) -> str:
async with AsyncSessionLocal() as session:
tree = await session.get(MirrorTree, tree_id)
return tree.character_id if tree else ""
async def _activate_lorebook(
character_id: str,
*,
scan_text: str,
expand=None, # noqa: ANN001 — 符文展开函数(char/user 名),无上下文时不展开
char_budget: int = 600,
) -> list[str]:
"""世谱激活管线(mirror-tree PRD 借 ST 范式):关键词命中 + 常驻,预算内按插入序。
扫描范围为近期文本(祖先链尾部 + 当前问题);条目内容经符文展开后注入。
次级键逻辑、递归扫描与时序效果(sticky/cooldown)随 M5 循环轮次状态接入。
"""
from sqlalchemy import select
from app.models.lorebook import Lorebook, LorebookEntry
text = "".join(scan_text.lower().split())
async with AsyncSessionLocal() as session:
books = (
await session.execute(
select(Lorebook.id).where(
Lorebook.character_id == character_id, Lorebook.status == "active"
)
)
).scalars().all()
if not books:
return []
entries = (
(
await session.execute(
select(LorebookEntry)
.where(LorebookEntry.lorebook_id.in_(tuple(books)), LorebookEntry.enabled.is_(True))
.order_by(LorebookEntry.insertion_order)
)
)
.scalars()
.all()
)
picked: list[str] = []
used = 0
for entry in entries:
keys = [k.lower() for k in json.loads(entry.keys_json or "[]") if k]
hit = entry.constant or any("".join(k.split()) in text for k in keys)
if not hit:
continue
content = entry.content if expand is None else expand(entry.content)
if used + len(content) > char_budget:
continue
picked.append(content)
used += len(content)
return picked