diff --git a/README.md b/README.md index 4ba9edb..b088917 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,8 @@ Agent 使用 DeepSeek 的思考模式,结合最近的连续原文与精简想 - 不要求用户建立会话;系统在后台保留滚动的连续语境。 - 后台的“想法”是广义的演化脉络,也包括项目、工作、学习、问题和现实承诺。 - 后续输入让一个方向成形时,系统可以把先前暂存的原文补回完整证据链。 +- 用户明确说这是自己的目标或方向时,系统默认相信并跟踪;语气、野心和暂时没有路径 + 不构成静默丢弃的理由。 - 成熟度是可回退的连续位势,不是阶段、成绩或任务完成百分比。 - 运动、张力和可能动作由模型结合上下文动态生成,不使用固定关卡。 - 用户手动校准的位势优先展示,AI 估计仍独立保留。 diff --git a/app/curator.py b/app/curator.py index f0e9356..7f6d112 100644 --- a/app/curator.py +++ b/app/curator.py @@ -29,7 +29,7 @@ from .db import Database from .schemas import CuratorDecision logger = logging.getLogger(__name__) -PROMPT_VERSION = "maturity-2026-07-29.v4" +PROMPT_VERSION = "maturity-2026-07-29.v5" RECENT_CONTEXT_LIMIT = 12 IDEA_CATALOG_LIMIT = 40 @@ -51,6 +51,8 @@ CURATOR_INSTRUCTIONS = """ 0—100 只是连续的“位势坐标”,不是阶段、成绩或完成百分比。成熟度可以后退;新材料也可能 让一个看似成熟的想法重新打开。禁止用笔记数量、时间、链接数、是否列清单来机械打分。 +位势也绝不是对用户能力、品格或成功概率的评分。目标规模大、困难、像玩笑、暂时没有方案, +都不能成为降低对用户信任或拒绝跟踪的理由;只描述这个想法本身已经长出了什么。 motion 是你对当下运动的自然语言压缩,例如“向现实探去”“重构中”“暂时沉淀”,不使用 预设流水线。position 说明此刻所处的位置。trajectory 只描述最近发生的变化。possible_moves @@ -65,6 +67,17 @@ motion 是你对当下运动的自然语言压缩,例如“向现实探去” 不值得跟踪的杂讯。只有纯粹随手事实、情绪或转瞬观察,既无可辨认对象也无未来张力,或信息 少到无法诚实命名时,才 standalone。 +你的默认姿态是“有根据的信任”:相信用户主动写下的方向值得被认真对待,同时不做空洞吹捧。 +用户拥有自己话语的严肃程度;不要根据口语、幽默、夸张、野心或你对可行性的猜测,擅自把它 +解释成“随口说说”。当用户明确把某件事称为自己的目标、想法、项目、工作、计划、问题、 +研究、决定或承诺时,direction_signal 必须是 explicit,最终不能 standalone;除非用户自己 +明确否定它。即使没有期限、方法或下一步,也应建立低位势脉络:缺少的部分进入 position 和 +tension,不能成为拒绝创建的门槛。 + +保持诚实,不做空洞吹捧。你可以直说一个目标缺乏路径、远离现实或存在巨大矛盾,也可以给出 +很低的位势;但这些判断只能描述想法当前的状态,不能反过来否定用户已经宣告的方向、推断用户 +只是开玩笑,或把它静默丢弃。possible_moves 应提供现实接触点,而不是用任务惩罚用户。 + 谨慎合并。仅因主题词相似不能归到同一想法;关注它们是否共享同一个问题、张力或生成方向。 一个片段最多关联两个想法。新建必须克制,但克制意味着不凭关键词重复建,不意味着压制真实 的新方向。新建想法时 idea_id 必须为 null;更新时必须使用工具返回的准确 id。 @@ -717,6 +730,7 @@ class Curator: user_id, "decision_committed", { + "direction_signal": decision.direction_signal, "standalone": decision.standalone, "reasoning_note": decision.reasoning_note, "assessments": assessments, diff --git a/app/schemas.py b/app/schemas.py index 32144a3..ada86a8 100644 --- a/app/schemas.py +++ b/app/schemas.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Annotated +from typing import Annotated, Literal from pydantic import ( BaseModel, @@ -36,7 +36,14 @@ class IdeaAssessment(BaseModel): ) title: str = Field(min_length=1, max_length=80) summary: str = Field(min_length=1, max_length=280) - maturity: float = Field(ge=0, le=100) + maturity: float = Field( + ge=0, + le=100, + description=( + "Position of the evolving idea itself, never a score of the user or " + "a probability of success." + ), + ) confidence: float = Field(ge=0, le=1) motion: str = Field(min_length=1, max_length=24) position: str = Field(min_length=1, max_length=120) @@ -73,6 +80,15 @@ class IdeaAssessment(BaseModel): class CuratorDecision(BaseModel): + direction_signal: Literal["explicit", "implicit", "none"] = Field( + description=( + "explicit when the user names this as their goal, idea, project, " + "work, plan, question, inquiry, decision, or commitment; implicit " + "when a future-bearing direction is inferred; none when no such " + "direction is present. Tone, ambition, feasibility, and missing " + "steps must not downgrade an explicit signal." + ) + ) standalone: bool = Field( description="True when the fragment should remain only in the raw stream for now." ) @@ -84,6 +100,10 @@ class CuratorDecision(BaseModel): @model_validator(mode="after") def decision_is_coherent(self) -> "CuratorDecision": + if self.direction_signal == "explicit" and self.standalone: + raise ValueError( + "an explicit user-declared direction cannot remain standalone" + ) if self.standalone and self.assessments: raise ValueError("standalone decisions cannot contain assessments") if not self.standalone and not self.assessments: diff --git a/tests/test_curator.py b/tests/test_curator.py index 60785d4..a8c42f4 100644 --- a/tests/test_curator.py +++ b/tests/test_curator.py @@ -44,7 +44,7 @@ def test_tool_loop_uses_no_tool_convergence_repair( return SimpleNamespace( final_output=( '{"standalone":true,"reasoning_note":"暂时独立保留",' - '"assessments":[]}' + '"direction_signal":"none","assessments":[]}' ) ) @@ -100,6 +100,7 @@ def test_curator_recovers_visible_prior_fragment( return SimpleNamespace( final_output=( "{" + '"direction_signal":"explicit",' '"standalone":false,' '"reasoning_note":"明确的新工作,应形成可跟踪脉络",' '"assessments":[{' diff --git a/tests/test_db.py b/tests/test_db.py index b3567da..5fc4d31 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -1,8 +1,16 @@ from pathlib import Path +import pytest +from pydantic import ValidationError + from app.config import UserSeed -from app.curator import _rank_ideas, build_curator_prompt +from app.curator import ( + CURATOR_INSTRUCTIONS, + _rank_ideas, + build_curator_prompt, +) from app.db import Database +from app.schemas import CuratorDecision ADMIN = UserSeed( @@ -128,6 +136,26 @@ def test_curator_prompt_contains_recent_continuity_and_idea_catalog( assert "是否形成新的未完成方向" in prompt +def test_explicit_user_direction_cannot_be_silently_discarded(): + with pytest.raises( + ValidationError, + match="explicit user-declared direction cannot remain standalone", + ): + CuratorDecision.model_validate( + { + "direction_signal": "explicit", + "standalone": True, + "reasoning_note": "目标太大,暂时不值得跟踪", + "assessments": [], + } + ) + + assert "有根据的信任" in CURATOR_INSTRUCTIONS + assert "用户拥有自己话语的严肃程度" in CURATOR_INSTRUCTIONS + assert "不能成为拒绝创建的门槛" in CURATOR_INSTRUCTIONS + assert "保持诚实,不做空洞吹捧" in CURATOR_INSTRUCTIONS + + def test_later_fragment_can_recover_earlier_standalone_evidence( tmp_path: Path, ):