feat: recover evolving threads from freeform notes
This commit is contained in:
+49
-10
@@ -29,7 +29,7 @@ from .db import Database
|
||||
from .schemas import CuratorDecision
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
PROMPT_VERSION = "maturity-2026-07-28.v3"
|
||||
PROMPT_VERSION = "maturity-2026-07-29.v4"
|
||||
RECENT_CONTEXT_LIMIT = 12
|
||||
IDEA_CATALOG_LIMIT = 40
|
||||
|
||||
@@ -38,7 +38,9 @@ CURATOR_INSTRUCTIONS = """
|
||||
你是一个私人思想笔记本内部的“策展者”。用户界面必须安静,所以你的工作发生在幕后。
|
||||
|
||||
你的任务不是给句子贴标签,也不是强迫想法进入固定工作流,而是辨认:新片段是否属于一个
|
||||
持续演化的想法;如果属于,它此刻大致在哪里、正在怎样运动、内在张力是什么。
|
||||
持续演化的脉络;如果属于,它此刻大致在哪里、正在怎样运动、内在张力是什么。这里的“想法”
|
||||
是面向用户的安静称呼,后台必须把它理解得更广:观点、问题、项目、工作、学习、创作、决策和
|
||||
现实承诺,只要会随时间变化、值得未来回来,都可能是一个脉络。
|
||||
|
||||
判断“成熟度”时使用融合性的人类经验,而不是计数规则。综合考察:
|
||||
1. 想法是否已经有自己的身份、边界与可复述的核心;
|
||||
@@ -54,9 +56,23 @@ motion 是你对当下运动的自然语言压缩,例如“向现实探去”
|
||||
预设流水线。position 说明此刻所处的位置。trajectory 只描述最近发生的变化。possible_moves
|
||||
是 1—4 个根据当前张力即时生成的可能动作,不是任务清单,不承诺固定的下一关。
|
||||
|
||||
在决定 standalone 或新建前,必须把两个问题分开:
|
||||
1. 它是否延续某个已有脉络;
|
||||
2. 即使不延续,它是否带着值得未来回来的未完成方向。
|
||||
“不属于已有想法”绝不等于“应当 standalone”。如果片段已经显露出可命名的对象,并带有
|
||||
未解问题、现实承诺、明确意图或下一步动作之一,即使只有第一条,也通常应建立一个低位势的
|
||||
新脉络。尤其是用户明确说“新工作”“需要做”“我想研究”“我要先……”时,不要把它当作
|
||||
不值得跟踪的杂讯。只有纯粹随手事实、情绪或转瞬观察,既无可辨认对象也无未来张力,或信息
|
||||
少到无法诚实命名时,才 standalone。
|
||||
|
||||
谨慎合并。仅因主题词相似不能归到同一想法;关注它们是否共享同一个问题、张力或生成方向。
|
||||
一个片段最多关联两个想法。若现在还不值得形成或归入想法,让它 standalone,原文仍会保留。
|
||||
新建想法时 idea_id 必须为 null;更新时必须使用工具返回的准确 id。
|
||||
一个片段最多关联两个想法。新建必须克制,但克制意味着不凭关键词重复建,不意味着压制真实
|
||||
的新方向。新建想法时 idea_id 必须为 null;更新时必须使用工具返回的准确 id。
|
||||
|
||||
如果 recent_context 中较早的原文与当前判断共同构成同一脉络,但它当时可能只被暂存,使用
|
||||
supporting_fragment_ids 把它补回证据链。只可填写 recent_context 中出现的准确 id,最多六条;
|
||||
当前 new_fragment 会被系统自动关联,不要填写它。不要为主题词相似而回收,也不要填写
|
||||
later_context 中的记录。
|
||||
|
||||
输入会直接提供最近的连续原文和精简想法目录。把“这个、上面、第二点、继续、刚才”等指代
|
||||
放回连续语境中理解;不要因为新片段换了关键词就丢掉思想上的延续。目录中的 id 只供系统内部
|
||||
@@ -190,12 +206,10 @@ class CuratorRunHooks(RunHooks[CuratorContext]):
|
||||
state.llm_started_at = None
|
||||
|
||||
|
||||
def build_curator_prompt(
|
||||
fragment: dict[str, Any],
|
||||
ideas: list[dict[str, Any]],
|
||||
recent_fragments: list[dict[str, Any]],
|
||||
) -> str:
|
||||
prior_context = [
|
||||
def _prior_context(
|
||||
fragment: dict[str, Any], recent_fragments: list[dict[str, Any]]
|
||||
) -> list[dict[str, Any]]:
|
||||
return [
|
||||
{
|
||||
"id": item["id"],
|
||||
"created_at": item["created_at"],
|
||||
@@ -205,6 +219,14 @@ def build_curator_prompt(
|
||||
if item["id"] != fragment["id"]
|
||||
and item["created_at"] <= fragment["created_at"]
|
||||
][-RECENT_CONTEXT_LIMIT:]
|
||||
|
||||
|
||||
def build_curator_prompt(
|
||||
fragment: dict[str, Any],
|
||||
ideas: list[dict[str, Any]],
|
||||
recent_fragments: list[dict[str, Any]],
|
||||
) -> str:
|
||||
prior_context = _prior_context(fragment, recent_fragments)
|
||||
later_context = [
|
||||
{
|
||||
"created_at": item["created_at"],
|
||||
@@ -256,6 +278,9 @@ def build_curator_prompt(
|
||||
f"当前共有 {len(ideas)} 个已有想法。{catalog_note}\n"
|
||||
"如果新片段既验证了一个既有想法、又形成了一个新的独立问题,可以给出两个 assessment;"
|
||||
"不要因为表面主题变化而遗漏它对上一段思想过程的反馈。\n\n"
|
||||
"先分别判断“是否延续已有脉络”与“是否形成新的未完成方向”。若较早的 recent_context "
|
||||
"与当前片段共同构成同一脉络,在对应 assessment 的 supporting_fragment_ids 中补回;"
|
||||
"没有确切证据时保持为空。\n\n"
|
||||
"最终只输出符合以下 JSON Schema 的 JSON 对象:\n"
|
||||
f"{json.dumps(output_schema, ensure_ascii=False)}"
|
||||
)
|
||||
@@ -635,6 +660,20 @@ class Curator:
|
||||
raise ValueError(
|
||||
"curator referenced an unknown existing idea"
|
||||
)
|
||||
eligible_fragment_ids = {
|
||||
item["id"]
|
||||
for item in _prior_context(fragment, recent_fragments)
|
||||
}
|
||||
unknown_fragment_ids = [
|
||||
supporting_id
|
||||
for item in decision.assessments
|
||||
for supporting_id in item.supporting_fragment_ids
|
||||
if supporting_id not in eligible_fragment_ids
|
||||
]
|
||||
if unknown_fragment_ids:
|
||||
raise ValueError(
|
||||
"curator referenced a fragment outside recent_context"
|
||||
)
|
||||
break
|
||||
except (ValidationError, ValueError, TypeError) as exc:
|
||||
decision = None
|
||||
|
||||
@@ -604,19 +604,38 @@ class Database:
|
||||
continue
|
||||
seen.add(idea_id)
|
||||
affected.append(idea_id)
|
||||
connection.execute(
|
||||
"""
|
||||
INSERT OR IGNORE INTO idea_fragments (
|
||||
idea_id, fragment_id, relevance, created_at
|
||||
) VALUES (?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
idea_id,
|
||||
fragment_id,
|
||||
assessment.get("relevance", 1),
|
||||
now,
|
||||
),
|
||||
)
|
||||
evidence_fragment_ids: list[str] = []
|
||||
for evidence_fragment_id in [
|
||||
fragment_id,
|
||||
*assessment.get("supporting_fragment_ids", []),
|
||||
]:
|
||||
if evidence_fragment_id in evidence_fragment_ids:
|
||||
continue
|
||||
evidence_exists = connection.execute(
|
||||
"""
|
||||
SELECT 1 FROM fragments
|
||||
WHERE id = ? AND user_id = ?
|
||||
""",
|
||||
(evidence_fragment_id, user_id),
|
||||
).fetchone()
|
||||
if not evidence_exists:
|
||||
raise ValueError(
|
||||
"supporting fragment not found for user"
|
||||
)
|
||||
evidence_fragment_ids.append(evidence_fragment_id)
|
||||
connection.execute(
|
||||
"""
|
||||
INSERT OR IGNORE INTO idea_fragments (
|
||||
idea_id, fragment_id, relevance, created_at
|
||||
) VALUES (?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
idea_id,
|
||||
evidence_fragment_id,
|
||||
assessment.get("relevance", 1),
|
||||
now,
|
||||
),
|
||||
)
|
||||
version_state = connection.execute(
|
||||
"""
|
||||
SELECT maturity_override FROM ideas
|
||||
|
||||
@@ -44,12 +44,33 @@ class IdeaAssessment(BaseModel):
|
||||
trajectory: str = Field(min_length=1, max_length=280)
|
||||
possible_moves: list[str] = Field(min_length=1, max_length=4)
|
||||
relevance: float = Field(default=1, ge=0, le=1)
|
||||
supporting_fragment_ids: list[str] = Field(
|
||||
default_factory=list,
|
||||
max_length=6,
|
||||
description=(
|
||||
"Ids of earlier fragments from recent_context that are substantive "
|
||||
"evidence for this same evolving thread. The new fragment is linked "
|
||||
"automatically and must not be included."
|
||||
),
|
||||
)
|
||||
|
||||
@field_validator("possible_moves")
|
||||
@classmethod
|
||||
def moves_are_brief(cls, moves: list[str]) -> list[str]:
|
||||
return [move.strip()[:100] for move in moves if move.strip()][:4]
|
||||
|
||||
@field_validator("supporting_fragment_ids")
|
||||
@classmethod
|
||||
def supporting_fragments_are_unique(
|
||||
cls, fragment_ids: list[str]
|
||||
) -> list[str]:
|
||||
unique: list[str] = []
|
||||
for fragment_id in fragment_ids:
|
||||
value = fragment_id.strip()
|
||||
if value and value not in unique:
|
||||
unique.append(value)
|
||||
return unique[:6]
|
||||
|
||||
|
||||
class CuratorDecision(BaseModel):
|
||||
standalone: bool = Field(
|
||||
|
||||
Reference in New Issue
Block a user