初始化提交
This commit is contained in:
commit
887c1f449e
963 files changed
+166869
No files matched your search
@@ -0,0 +1,5 @@
|
||||
"""资料导入领域逻辑(纯函数,无 IO)。"""
|
||||
|
||||
from app.modules.imports.parsing import ParsedItemDraft, parse_import
|
||||
|
||||
__all__ = ["ParsedItemDraft", "parse_import"]
|
||||
@@ -0,0 +1,120 @@
|
||||
"""酒馆角色卡解析器。
|
||||
|
||||
从 PNG(``tEXt`` chunk 的 ``chara``/``ccv3``)/ JSON / CHARX(zip 含 ``card.json``)
|
||||
解析角色卡,归一化为统一的 ``CharacterCard``(V2/V3 的 ``data`` 字段拍平)。
|
||||
|
||||
纯字节 / 文本解析,无文件 IO——调用方(service 层)负责读文件后传入字节。
|
||||
V1 扁平结构自动归一化;``character_book`` 原样保留,由映射层转世谱条目。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import io
|
||||
import json
|
||||
import zipfile
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
_PNG_SIGNATURE = b"\x89PNG\r\n\x1a\n"
|
||||
_ZIP_SIGNATURE = b"PK\x03\x04"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CharacterCard:
|
||||
"""归一化后的角色卡(V1/V2/V3 统一为拍平字段)。"""
|
||||
|
||||
spec_version: str # "1" / "2.0" / "3.0"
|
||||
name: str = ""
|
||||
description: str = ""
|
||||
personality: str = ""
|
||||
scenario: str = ""
|
||||
first_mes: str = ""
|
||||
mes_example: str = ""
|
||||
creator_notes: str = ""
|
||||
system_prompt: str = ""
|
||||
post_history_instructions: str = ""
|
||||
alternate_greetings: list[str] = field(default_factory=list)
|
||||
tags: list[str] = field(default_factory=list)
|
||||
creator: str = ""
|
||||
character_version: str = ""
|
||||
character_book: dict | None = None # 原始 lorebook,由映射层转 LorebookBook
|
||||
nickname: str | None = None # V3
|
||||
extensions: dict = field(default_factory=dict) # 保留未知键(酒馆规范:不得删除)
|
||||
|
||||
|
||||
def parse_character_card(raw: bytes) -> CharacterCard:
|
||||
"""从原始字节解析角色卡(自动识别 PNG / JSON / CHARX)。"""
|
||||
if raw[:8] == _PNG_SIGNATURE:
|
||||
data = _parse_png(raw)
|
||||
elif raw[:4] == _ZIP_SIGNATURE:
|
||||
data = _parse_charx(raw)
|
||||
else:
|
||||
data = json.loads(raw.decode("utf-8-sig"))
|
||||
return _normalize(data)
|
||||
|
||||
|
||||
def _parse_png(raw: bytes) -> dict:
|
||||
"""从 PNG tEXt chunk 提取 chara / ccv3(base64 → JSON)。优先 ccv3。"""
|
||||
chunks: list[tuple[str, str]] = []
|
||||
offset = len(_PNG_SIGNATURE)
|
||||
while offset + 8 <= len(raw):
|
||||
length = int.from_bytes(raw[offset : offset + 4], "big")
|
||||
chunk_type = raw[offset + 4 : offset + 8]
|
||||
data_start = offset + 8
|
||||
data_end = data_start + length
|
||||
if data_end > len(raw):
|
||||
break
|
||||
if chunk_type == b"tEXt":
|
||||
null = raw[data_start:data_end].find(b"\x00")
|
||||
if null >= 0:
|
||||
keyword = raw[data_start : data_start + null].decode("latin-1")
|
||||
text = raw[data_start + null + 1 : data_end].decode("latin-1")
|
||||
chunks.append((keyword, text))
|
||||
offset = data_end + 4 # 跳过 CRC
|
||||
|
||||
for want in ("ccv3", "chara"):
|
||||
for keyword, text in chunks:
|
||||
if keyword == want:
|
||||
decoded = base64.b64decode(text)
|
||||
return json.loads(decoded.decode("utf-8"))
|
||||
raise ValueError("PNG 中未找到角色卡元数据(chara / ccv3 chunk)")
|
||||
|
||||
|
||||
def _parse_charx(raw: bytes) -> dict:
|
||||
"""从 CHARX(zip)提取 card.json。"""
|
||||
with zipfile.ZipFile(io.BytesIO(raw)) as zf:
|
||||
card_name = next((n for n in zf.namelist() if n.endswith("card.json")), None)
|
||||
if card_name is None:
|
||||
raise ValueError("CHARX 中未找到 card.json")
|
||||
with zf.open(card_name) as f:
|
||||
return json.loads(f.read().decode("utf-8"))
|
||||
|
||||
|
||||
def _normalize(data: dict) -> CharacterCard:
|
||||
"""V1 扁平结构归一化为 V2/V3(data 包裹)。"""
|
||||
if isinstance(data.get("data"), dict):
|
||||
inner = data["data"]
|
||||
spec_version = str(data.get("spec_version", "2.0"))
|
||||
else:
|
||||
inner = data
|
||||
spec_version = "1"
|
||||
|
||||
return CharacterCard(
|
||||
spec_version=spec_version,
|
||||
name=inner.get("name", ""),
|
||||
description=inner.get("description", ""),
|
||||
personality=inner.get("personality", ""),
|
||||
scenario=inner.get("scenario", ""),
|
||||
first_mes=inner.get("first_mes", ""),
|
||||
mes_example=inner.get("mes_example", ""),
|
||||
creator_notes=inner.get("creator_notes", ""),
|
||||
system_prompt=inner.get("system_prompt", ""),
|
||||
post_history_instructions=inner.get("post_history_instructions", ""),
|
||||
alternate_greetings=list(inner.get("alternate_greetings", []) or []),
|
||||
tags=list(inner.get("tags", []) or []),
|
||||
creator=inner.get("creator", ""),
|
||||
character_version=inner.get("character_version", ""),
|
||||
character_book=inner.get("character_book"),
|
||||
nickname=inner.get("nickname"),
|
||||
extensions=dict(inner.get("extensions", {}) or {}),
|
||||
)
|
||||
@@ -0,0 +1,71 @@
|
||||
"""角色卡导出映射:魂镜 Character + 世谱 → ST V3 卡静态快照。
|
||||
|
||||
反向于 import mapping。导出为静态快照——丢弃演化维度(人格版本 / 记忆 / 情绪),
|
||||
仅保留当前设定 + 世谱。``mission`` / ``safety_guidelines`` 道体无 ST 对应字段,不导出。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
from app.models.character import Character
|
||||
from app.models.lorebook import Lorebook, LorebookEntry
|
||||
|
||||
|
||||
def character_to_card(
|
||||
character: Character,
|
||||
lorebook: Lorebook | None = None,
|
||||
entries: list[LorebookEntry] | None = None,
|
||||
) -> dict:
|
||||
"""Character + 世谱 → ST V3 卡 dict。"""
|
||||
profile = json.loads(character.profile_json or "{}")
|
||||
source_meta = profile.get("source_meta") or {}
|
||||
data: dict = {
|
||||
"name": character.name,
|
||||
"description": profile.get("background", ""),
|
||||
"personality": character.personality_summary or "",
|
||||
"scenario": profile.get("scenario", ""),
|
||||
"first_mes": profile.get("first_mes", ""),
|
||||
"mes_example": profile.get("mes_example", ""),
|
||||
"alternate_greetings": profile.get("alternate_greetings", []),
|
||||
"tags": profile.get("tags", []),
|
||||
"creator": source_meta.get("creator", ""),
|
||||
"character_version": source_meta.get("character_version", ""),
|
||||
"creator_notes": source_meta.get("creator_notes", ""),
|
||||
"system_prompt": profile.get("system_prompt", ""),
|
||||
"post_history_instructions": profile.get("post_history_instructions", ""),
|
||||
"extensions": profile.get("st_extensions", {}),
|
||||
}
|
||||
if character.display_name and character.display_name != character.name:
|
||||
data["nickname"] = character.display_name
|
||||
if lorebook is not None:
|
||||
data["character_book"] = lorebook_to_st_book(lorebook, entries or [])
|
||||
return {"spec": "chara_card_v3", "spec_version": "3.0", "data": data}
|
||||
|
||||
|
||||
def lorebook_to_st_book(lorebook: Lorebook, entries: list[LorebookEntry]) -> dict:
|
||||
return {
|
||||
"name": lorebook.name,
|
||||
"description": lorebook.description,
|
||||
"scan_depth": lorebook.scan_depth,
|
||||
"token_budget": lorebook.token_budget,
|
||||
"recursive_scanning": lorebook.recursive_scanning,
|
||||
"entries": [entry_to_st(e) for e in entries],
|
||||
}
|
||||
|
||||
|
||||
def entry_to_st(entry: LorebookEntry) -> dict:
|
||||
return {
|
||||
"keys": json.loads(entry.keys_json or "[]"),
|
||||
"content": entry.content,
|
||||
"secondary_keys": json.loads(entry.secondary_keys_json or "[]"),
|
||||
"constant": entry.constant,
|
||||
"selective": entry.selective,
|
||||
"enabled": entry.enabled,
|
||||
"case_sensitive": entry.case_sensitive,
|
||||
"use_regex": entry.use_regex,
|
||||
"position": entry.position,
|
||||
"insertion_order": entry.insertion_order,
|
||||
"priority": entry.priority,
|
||||
"comment": entry.comment,
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
"""解析模型返回为候选草稿(纯函数,无 IO)。
|
||||
|
||||
模型可能返回纯 JSON、markdown 代码块或夹杂解释文本;本模块容错提取首个 JSON 数组,
|
||||
并归一化为候选草稿。evidence 序号由 application 映射为 ``import_items.id``。
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProfileCandidateDraft:
|
||||
field_name: str
|
||||
value: str
|
||||
confidence: float
|
||||
reason: str
|
||||
evidence: list[int] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
class MemoryCandidateDraft:
|
||||
type: str
|
||||
title: str
|
||||
content: str
|
||||
importance: float
|
||||
sensitivity: str
|
||||
reason: str
|
||||
evidence: list[int] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
class PersonalityProposalDraft:
|
||||
field_name: str
|
||||
change: str
|
||||
reason: str
|
||||
evidence: list[int] = field(default_factory=list)
|
||||
|
||||
|
||||
def parse_profile_candidates(raw: str) -> list[ProfileCandidateDraft]:
|
||||
drafts: list[ProfileCandidateDraft] = []
|
||||
for item in _extract_json_array(raw):
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
field_name = str(item.get("field", "")).strip()
|
||||
value = str(item.get("value", "")).strip()
|
||||
if not field_name or not value:
|
||||
continue
|
||||
drafts.append(
|
||||
ProfileCandidateDraft(
|
||||
field_name=field_name,
|
||||
value=value,
|
||||
confidence=_clamp_float(item.get("confidence"), 0.5),
|
||||
reason=str(item.get("reason", "")).strip(),
|
||||
evidence=_int_list(item.get("evidence")),
|
||||
)
|
||||
)
|
||||
return drafts
|
||||
|
||||
|
||||
def parse_memory_candidates(raw: str) -> list[MemoryCandidateDraft]:
|
||||
drafts: list[MemoryCandidateDraft] = []
|
||||
for item in _extract_json_array(raw):
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
title = str(item.get("title", "")).strip()
|
||||
content = str(item.get("content", "")).strip()
|
||||
if not title or not content:
|
||||
continue
|
||||
drafts.append(
|
||||
MemoryCandidateDraft(
|
||||
type=str(item.get("type", "other")).strip() or "other",
|
||||
title=title,
|
||||
content=content,
|
||||
importance=_clamp_float(item.get("importance"), 0.5),
|
||||
sensitivity=_sensitivity(item.get("sensitivity")),
|
||||
reason=str(item.get("reason", "")).strip(),
|
||||
evidence=_int_list(item.get("evidence")),
|
||||
)
|
||||
)
|
||||
return drafts
|
||||
|
||||
|
||||
def parse_personality_proposals(raw: str) -> list[PersonalityProposalDraft]:
|
||||
drafts: list[PersonalityProposalDraft] = []
|
||||
for item in _extract_json_array(raw):
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
field_name = str(item.get("field", "")).strip()
|
||||
change = str(item.get("change", "")).strip()
|
||||
if not field_name or not change:
|
||||
continue
|
||||
drafts.append(
|
||||
PersonalityProposalDraft(
|
||||
field_name=field_name,
|
||||
change=change,
|
||||
reason=str(item.get("reason", "")).strip(),
|
||||
evidence=_int_list(item.get("evidence")),
|
||||
)
|
||||
)
|
||||
return drafts
|
||||
|
||||
|
||||
def _extract_json_array(raw: str) -> list[Any]:
|
||||
text = raw.strip()
|
||||
matched = re.search(r"```(?:json)?\s*(\[.*?\])\s*```", text, re.DOTALL)
|
||||
if matched:
|
||||
text = matched.group(1)
|
||||
else:
|
||||
start, end = text.find("["), text.rfind("]")
|
||||
if start != -1 and end > start:
|
||||
text = text[start : end + 1]
|
||||
try:
|
||||
data = json.loads(text)
|
||||
except json.JSONDecodeError:
|
||||
return []
|
||||
return data if isinstance(data, list) else []
|
||||
|
||||
|
||||
def _clamp_float(value: Any, default: float) -> float:
|
||||
try:
|
||||
num = float(value)
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
return max(0.0, min(1.0, num))
|
||||
|
||||
|
||||
def _int_list(value: Any) -> list[int]:
|
||||
if not isinstance(value, list):
|
||||
return []
|
||||
result: list[int] = []
|
||||
for item in value:
|
||||
try:
|
||||
result.append(int(item))
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
return result
|
||||
|
||||
|
||||
def _sensitivity(value: Any) -> str:
|
||||
text = str(value or "").strip().lower()
|
||||
return text if text in {"low", "medium", "high"} else "low"
|
||||
@@ -0,0 +1,66 @@
|
||||
"""角色卡字段映射:ST CharacterCard → 魂镜 Character + 世谱条目。
|
||||
|
||||
纯逻辑,无 IO。``system_prompt`` / ``post_history_instructions`` 导入时存入 profile,
|
||||
运行时作道体增补层注入(绝不覆盖恒定层,见融合设计 §6)。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from app.modules.imports.card_parser import CharacterCard
|
||||
|
||||
|
||||
def card_to_profile(card: CharacterCard) -> dict:
|
||||
"""ST 卡 → Character profile JSON(自由字段,保留酒馆全部信息)。"""
|
||||
return {
|
||||
"background": card.description,
|
||||
"scenario": card.scenario,
|
||||
"first_mes": card.first_mes,
|
||||
"mes_example": card.mes_example,
|
||||
"alternate_greetings": card.alternate_greetings,
|
||||
"tags": card.tags,
|
||||
# 运行时作道体增补层注入(§6 安全边界),导入先保存
|
||||
"system_prompt": card.system_prompt,
|
||||
"post_history_instructions": card.post_history_instructions,
|
||||
"source_meta": {
|
||||
"creator": card.creator,
|
||||
"character_version": card.character_version,
|
||||
"creator_notes": card.creator_notes,
|
||||
"spec_version": card.spec_version,
|
||||
},
|
||||
"st_extensions": card.extensions, # 保留未知键(酒馆规范:不得删除)
|
||||
}
|
||||
|
||||
|
||||
def card_to_daoti(card: CharacterCard) -> dict:
|
||||
"""ST 卡 → 道体字段。
|
||||
|
||||
``personality`` 作 ``personality_summary`` 初始值(已拍板);``mission`` /
|
||||
``safety_guidelines`` 酒馆卡没有,导入不设,留用户 / 演化填。
|
||||
"""
|
||||
return {"personality_summary": card.personality or None}
|
||||
|
||||
|
||||
def character_book_to_entries(book: dict | None) -> list[dict]:
|
||||
"""ST character_book entries → 世谱条目构造参数。"""
|
||||
if not book:
|
||||
return []
|
||||
result = []
|
||||
for e in book.get("entries") or []:
|
||||
enabled_raw = e.get("enabled", True)
|
||||
result.append(
|
||||
{
|
||||
"keys": list(e.get("keys") or []),
|
||||
"content": e.get("content") or "",
|
||||
"secondary_keys": list(e.get("secondary_keys") or []),
|
||||
"constant": bool(e.get("constant", False)),
|
||||
"selective": bool(e.get("selective", False)),
|
||||
"enabled": True if enabled_raw is None else bool(enabled_raw),
|
||||
"case_sensitive": bool(e.get("case_sensitive", False)),
|
||||
"use_regex": bool(e.get("use_regex", False)),
|
||||
"position": e.get("position") or "after_char",
|
||||
"insertion_order": int(e.get("insertion_order") or 100),
|
||||
"priority": int(e.get("priority") or 10),
|
||||
"comment": e.get("comment") or e.get("name") or "",
|
||||
}
|
||||
)
|
||||
return result
|
||||
@@ -0,0 +1,113 @@
|
||||
"""导入资料结构解析(纯函数,无 IO)。
|
||||
|
||||
按 ``material_type`` / ``format`` 把原始资料切片为条目草稿。只做结构解析
|
||||
(消息数组、段落、字段),语义抽取(档案 / 记忆 / 人格候选)留给后续
|
||||
模型驱动阶段。领域层禁止 IO,本模块仅产出数据结构。
|
||||
"""
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
# 5a 不切片的媒体资料(仅存元数据与授权记录,spec §18 仅预留)。
|
||||
_NON_TEXT_MATERIAL_TYPES = {"voice_sample"}
|
||||
|
||||
|
||||
@dataclass
|
||||
class ParsedItemDraft:
|
||||
"""解析产出的条目草稿;落库前由 service 补 sequence / id。"""
|
||||
|
||||
item_type: str
|
||||
role: str
|
||||
content: str
|
||||
metadata: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
def parse_import(
|
||||
material_type: str, fmt: str, *, content: str | None = None
|
||||
) -> list[ParsedItemDraft]:
|
||||
"""按格式解析文本资料;媒体资料返回空。"""
|
||||
if material_type in _NON_TEXT_MATERIAL_TYPES:
|
||||
return []
|
||||
|
||||
if fmt == "json" and content:
|
||||
return _parse_json(content)
|
||||
|
||||
if content:
|
||||
return _parse_paragraphs(content)
|
||||
|
||||
return []
|
||||
|
||||
|
||||
def _parse_json(content: str) -> list[ParsedItemDraft]:
|
||||
try:
|
||||
data = json.loads(content)
|
||||
except json.JSONDecodeError:
|
||||
return _parse_paragraphs(content)
|
||||
|
||||
if (
|
||||
isinstance(data, list)
|
||||
and data
|
||||
and isinstance(data[0], dict)
|
||||
and ("role" in data[0] or "content" in data[0])
|
||||
):
|
||||
return _parse_messages(data)
|
||||
if isinstance(data, dict):
|
||||
return _parse_fields(data)
|
||||
return _parse_paragraphs(content)
|
||||
|
||||
|
||||
def _parse_messages(messages: list[dict[str, Any]]) -> list[ParsedItemDraft]:
|
||||
drafts: list[ParsedItemDraft] = []
|
||||
for msg in messages:
|
||||
if not isinstance(msg, dict):
|
||||
continue
|
||||
text = str(msg.get("content", "")).strip()
|
||||
if not text:
|
||||
continue
|
||||
meta = {k: v for k, v in msg.items() if k not in ("role", "content")}
|
||||
drafts.append(
|
||||
ParsedItemDraft(
|
||||
item_type="message",
|
||||
role=_normalize_role(str(msg.get("role", "other"))),
|
||||
content=text,
|
||||
metadata=meta,
|
||||
)
|
||||
)
|
||||
return drafts
|
||||
|
||||
|
||||
def _parse_fields(data: dict[str, Any]) -> list[ParsedItemDraft]:
|
||||
drafts: list[ParsedItemDraft] = []
|
||||
for key, value in data.items():
|
||||
text = str(value).strip()
|
||||
if text:
|
||||
drafts.append(
|
||||
ParsedItemDraft(
|
||||
item_type="profile_field",
|
||||
role="other",
|
||||
content=text,
|
||||
metadata={"field": key},
|
||||
)
|
||||
)
|
||||
return drafts
|
||||
|
||||
|
||||
def _parse_paragraphs(text: str) -> list[ParsedItemDraft]:
|
||||
drafts: list[ParsedItemDraft] = []
|
||||
for chunk in text.split("\n\n"):
|
||||
clean = chunk.strip()
|
||||
if clean:
|
||||
drafts.append(ParsedItemDraft(item_type="paragraph", role="other", content=clean))
|
||||
return drafts
|
||||
|
||||
|
||||
def _normalize_role(role: str) -> str:
|
||||
lowered = role.lower()
|
||||
if lowered in ("user", "human"):
|
||||
return "user"
|
||||
if lowered in ("assistant", "ai", "bot"):
|
||||
return "assistant"
|
||||
if lowered == "system":
|
||||
return "system"
|
||||
return "other"
|
||||
@@ -0,0 +1,85 @@
|
||||
"""候选抽取 prompt 构建(纯函数,无 IO)。
|
||||
|
||||
接收 ``(role, content)`` 元组列表(由 application 从 ORM 转换),产出
|
||||
``(system, user)`` 消息对,喂给模型网关。模型返回的 JSON 由 ``extract`` 模块解析。
|
||||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
PROFILE_FIELDS = (
|
||||
"display_name",
|
||||
"birthday",
|
||||
"height",
|
||||
"weight",
|
||||
"background",
|
||||
"speaking_style",
|
||||
)
|
||||
MEMORY_TYPES = (
|
||||
"user",
|
||||
"preference",
|
||||
"event",
|
||||
"relationship",
|
||||
"skill",
|
||||
"goal",
|
||||
"other",
|
||||
)
|
||||
PERSONALITY_FIELDS = (
|
||||
"identity_profile",
|
||||
"role_destiny",
|
||||
"personality_seed",
|
||||
"expression_style",
|
||||
"relationship_pattern",
|
||||
"traits",
|
||||
"values",
|
||||
"emotion_baseline",
|
||||
)
|
||||
|
||||
|
||||
def _format_items(items: Sequence[tuple[str, str]]) -> str:
|
||||
lines = [f"[{idx}] ({role}) {content}" for idx, (role, content) in enumerate(items)]
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def build_profile_extraction_messages(
|
||||
items: Sequence[tuple[str, str]],
|
||||
) -> tuple[str, str]:
|
||||
system = (
|
||||
"你是资料分析助手。从用户提供的资料中提取人物档案字段候选,只提取明确出现的信息。"
|
||||
f"field 取自:{', '.join(PROFILE_FIELDS)}。"
|
||||
'以 JSON 数组返回,每个元素:{"field": str, "value": str, "confidence": 0~1, '
|
||||
'"reason": str, "evidence": [条目序号]}。'
|
||||
"evidence 为资料条目序号([0]/[1]…)列表。若资料无档案信息,返回 []。"
|
||||
"只返回 JSON 数组,不要解释或 markdown。"
|
||||
)
|
||||
return system, f"资料:\n{_format_items(items)}"
|
||||
|
||||
|
||||
def build_memory_extraction_messages(
|
||||
items: Sequence[tuple[str, str]],
|
||||
) -> tuple[str, str]:
|
||||
system = (
|
||||
"你是记忆分析助手。从用户提供的资料中提取值得长期记住的记忆候选。"
|
||||
f"type 取自:{', '.join(MEMORY_TYPES)}。"
|
||||
'以 JSON 数组返回:{"type": str, "title": str, "content": str, '
|
||||
'"importance": 0~1, "sensitivity": "low|medium|high", "reason": str, '
|
||||
'"evidence": [条目序号]}。'
|
||||
"只提取有长期价值的非琐碎信息。若无可提取返回 []。只返回 JSON 数组。"
|
||||
)
|
||||
return system, f"资料:\n{_format_items(items)}"
|
||||
|
||||
|
||||
def build_personality_extraction_messages(
|
||||
items: Sequence[tuple[str, str]], personality_summary: str
|
||||
) -> tuple[str, str]:
|
||||
system = (
|
||||
"你是人格分析助手。基于用户提供的资料和当前人格,提出人格完善建议。"
|
||||
f"field 取自:{', '.join(PERSONALITY_FIELDS)}。"
|
||||
'以 JSON 数组返回:{"field": str, "change": str, "reason": str, '
|
||||
'"evidence": [条目序号]}。'
|
||||
"change 为建议写入该字段的新内容。只在资料充分支撑时提议。若无需完善返回 []。"
|
||||
"只返回 JSON 数组。"
|
||||
)
|
||||
user = (
|
||||
f"当前人格摘要:\n{personality_summary or '(尚未形成)'}\n\n资料:\n{_format_items(items)}"
|
||||
)
|
||||
return system, user
|
||||
Reference in new issue
Block a user