Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
| 主体 | `subjects`、`subject_aliases`、`platform_identities`、`subject_links`、`subject_alias_blocks` |
| 记忆 | `memories`、`memory_subjects`、`memory_tags`、`sources`、`memory_revisions` |
| 当前状态 | `current_state`、`state_reports` |
| 自我与目标 | `persona_versions`、`persona_attempts`、`goals`、`goal_sources`、`goal_people`、`goal_memories`、`goal_duplicates`、`goal_dedup_jobs`、`goal_reminder_plans`、`notifications`、`runtime_settings` |
| 自我与目标 | `persona_versions`、`persona_attempts`、`goals`、`goal_sources`、`goal_people`、`goal_memories`、`goal_duplicates`、`goal_dedup_jobs`、`goal_auto_reviews`、`goal_duplicate_judgments`、`goal_reminder_plans`、`notifications`、`runtime_settings` |
| 模型 | `model_calls` |
| 召回 | `memory_fts_jieba`、`memory_fts_trigram`、`vector_dirty`、`recalls`、`recall_items` |

Expand Down Expand Up @@ -213,6 +213,8 @@ auto_v1 的初判使用 `consolidation_pair_auto_v1.md`(report_only 仍用 pai

目标阶段每次读取最多 100 个未合并的开放目标及修订;逐目标在自己的短事务调用 `review_goal_basis(conn, current, goal_id=..., expected_revision=...)`,将依据观察、目标历史、报告项和维护游标原子提交。采用目标线的单项事务接口,是为了使报告/游标与目标变化一同回滚;独立的 review_goal_basis_items 仍供其他分页调用者使用。冲突跳过并汇总,下一次从 0 扫描;新目标不越过本次 goal_through 上界。依据恢复/再变化由目标模块结束旧标注,不自动删除或关闭目标,过期标识沿用实时投影。

目标阶段的确定性项目处理完后,按运行时固定的两个开关(`settings_json.goal_auto`,且当前设置仍开启)依次执行 G2 和 G1(全自动阶段 G 线,默认关闭)。两者都是“只读快照取材料 → 事务外调用模型 → 单个短写事务核对修订号并写结果和报告项”:G1 为 `auto_review_material`/`judge_goal_review`/`apply_goal_review`,G2 为 `duplicate_auto_material`/`judge_duplicate_pair`/`apply_duplicate_judgment`。G2 与创建时复核共用 `dedup_review_material`、`dedup_review_decision`、`dedup_review_stale`,因此现有去重探测测到的就是维护路径的判断。指纹表 `goal_auto_reviews`、`goal_duplicate_judgments`(迁移 039)记录已完成的判断,续跑和下一次维护跳过未变化的目标;失败不记指纹。运行统计连同游标写在 `progress_json.goal_auto`,与每项结果同事务提交,续跑从游标继续并累加计数;逐项结果写 `maintenance_items`(phase=goals)。`goals.auto_review_after_message_id` 是管理员重开时的消息水位,之后的候选只取更新的消息,`_merge` 取双方较大值。G1 按最久未复核优先(忽略本次运行自己的判断,续跑顺序不变),跳过本次运行中 G2 刚合并进来的存续目标;“我”的候选消息只取目标所在入口和目标创建之后涉及的人发过言的入口,查询走迁移 039 的 `messages(sender_subject_id,entry_id,id)` 索引。G1 关闭时逐条通过核对的合格依据写入 `goal_auto_review_evidence`,`memory_ops.message_references` 据此保护它们不被消息清理删除,重开时删除这次关闭的保护。G2 按较晚一方的 `host_scope_json` 组材料和合并;较晚一方本身或已被合并进它的任一宿主目标要并进其范围外的目标时只记 kept、保留标记;自动合并在 `goal_duplicate_judgments.merge_record_json` 记下撤销所需的 ID 和字段值(不含正文),`undo_auto_merge` 在核对存续目标修订号后原样退回。

`PATCH /admin/api/settings/consolidation` 暴露 max_calls、resolution(默认 auto_v1,可退回 report_only_v1)及 merge_enabled、conflict_enabled、dependency_enabled、persona_enabled、goal_review_enabled(全开),时间复用 lifecycle.maintenance_time;更新与审计同事务。模型项目开关进入候选指纹和待办筛选,关闭期间不消耗该类调用,重新打开可发现延期项目;合并和矛盾共用一轮分类,关闭矛盾建议时仍防止矛盾记忆合并。报告通过原维护接口汇总变化、失败、模型建议、persona 结果、目标复核和全部模型用量;无变化/未到期只汇总,记忆建议始终仅管理员可见。

## M3 persona
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -653,6 +653,20 @@ GD2:`pending`/运行中的目标在补判结束前不进入 prepare 和 sear

探测脚本支持仓库外 `--corpus` 和 `--out`;`--fake` 使用本地传输,不读取模型配置、不发送网络请求,结果明确标为模拟并被选型函数拒绝。使用方法与本轮验证汇总见 [探测说明](evals/goal_dedup_probe/README.md)。

## 全自动阶段 G:目标自动复核

两项都在每日维护的目标阶段、确定性项目之后执行。开关 `goals.goal_auto_review_enabled` 与 `goals.goal_duplicate_auto_judge_enabled` 默认关闭,新库和旧库都不开;设置页“目标与提醒”提供两个复选框,只有显式保存过才写入运行设置。开关在接受维护请求时固定到该次运行,运行中被关闭时停止。模型调用都在事务外,写回前核对目标修订号,不一致跳过,下次再处理;不新增模型用途。两项都受用途暂停和每日 token 上限约束:G1 调用前按整理的做法检查 `chat`/learning 准入,G2 的 `goal_dedup_judge` 用途本身受上限约束;不可用时本轮停止,不记指纹,下次再试。

**G1 内部目标按依据完成或放弃**:只处理 `origin=internal`、`kind=normal`、`state=open`、未合并且没有吸收宿主或管理员目标的目标;询问沿用 7 天过期,不进入 G1。材料是目标正文、涉及的人、截止时间、创建时间、来源消息(目标的出处)和候选消息。候选消息须晚于目标创建(管理员重开过则只取重开之后的新消息),不是目标自己的来源,所在入口的可见范围不窄于目标;发送者是涉及的人(为空时取来源消息中不是角色的发送者),或者是“我”而且发在这个目标的对话里(目标所在入口,或目标创建之后涉及的人发过言的入口)。迁移 039 为此在 messages 上加了 `(sender_subject_id, entry_id, id)` 索引。别处无关入口里我的发言既不进候选,也不改变指纹。最多 30 条:先固定保留最新的 10 条(不论入口和关键词),其余按目标入口且含关键词、目标入口或含关键词、最近的顺序补足。没有候选消息不调用模型。目标按最久未复核优先处理(从未复核的最先,其余按本次运行之前最后一次判断的时间升序),因调用上限没轮到的目标下次排在前面;本次维护中刚被 G2 合并进来的存续目标留到下一次维护再复核,以便管理员仍可撤销那次合并。提示词 `goal_review_auto_v1.md`,走 `chat` 用途(purpose `goal_auto_review`),只输出 `{"decision","evidence","reason"}`。代码核对:判断不是 open、依据非空且都在材料内;目标自己的来源(包括合并时继承的)一律不算依据,合格依据只能来自候选消息,其中至少一条晚于目标创建、发送者是涉及的人或“我”、类型是 message/self_output/action_result;目标仍为内部、未结束、修订未变。通过才置为完成或放弃,`closed_by=maintenance`,取消未取走的提醒,记目标快照(动作 `auto_review`,原因 `auto_completed`/`auto_abandoned`)和操作记录。每次判断写一行 `goal_auto_reviews`(迁移 039),指纹为“目标修订号+发送者合格的最新消息 ID”,指纹不变不再调用模型,有新的合格消息就重判。超时、限流、暂停失败不记指纹,下次再试;输出无效或核对失败保持未结束并记固定原因。模型的文字理由不保存。自动关闭时,逐条通过核对的合格依据消息记在 `goal_auto_review_evidence`,30 天消息清理不会删除它们;一同被引用的不合格消息不受保护,管理员重开(否决这次关闭)后解除这次关闭的保护。管理员可在目标详情、目标页“每日维护自动处理的目标”和维护报告中查看依据消息并重开(`POST /admin/api/goals/{id}/reopen`,带 `expected_revision`);重开后只有更晚的新消息才能作为依据,合并时存续目标取双方较大的重开水位,原依据不会再次关闭它。

**G2 可能重复补判**:对仍为 `possible` 的目标对,以较晚的一条为待判目标,调用与创建时去重相同的材料组装 `dedup_review_material`(`goal_dedup_probe` 驱动的创建路径用的也是它)和现行 D6 提示词,走 `goal_dedup_judge` 用途,每对预算 60 秒。较晚的一条按它自己的令牌范围组材料和合并,与创建时复核一致。只有宿主目标作为被并入的一方(较晚的一条本身,或任何已被合并进它的宿主目标)、要并进它令牌范围外的目标时才拦:不调用模型、不合并,保留可能重复标记,记 `kept`/`host_scope_not_merged`,按指纹不再重复处理;内部目标并进宿主目标照创建时规则判断。只有创建时规则会合并到这一对中较早的一方(唯一 same、无其他歧义、未超限、无序列冲突)才合并,走现有 `_merge`;含管理员创建的目标时不合并,撤销标记并记录。判为不重复撤销标记;不确定或仍有歧义保留标记。完成的判断记 `goal_duplicate_judgments`,按指纹(全部观察到的目标修订和最新来源消息)不再重判;超时或失败保留标记,下次再试;目标去重判断关闭时不运行。已知限制:指纹包含全部观察到的候选目标修订号,其中任一变化都会让 uncertain 的一对再判一次,重判可能偏多。写回只核对修订号,维护开始后才到的消息本次看不到,靠管理员重开兜底。

自动合并可以撤销:`POST /admin/api/goal-duplicate-judgments/{id}/undo`(管理员会话、CSRF,带存续目标的 `expected_revision`)只对每日维护的自动合并有效。合并时记下存续目标的截止时间、提前量、重开水位、来源、涉及人、承诺记忆、依据标注,以及重定向、可能重复关系和待取提醒的 ID(不含正文);撤销时存续目标在合并后被修改过(包括管理员编辑等任何修订)返回 409;否则恢复被并入目标,存续目标退回合并前的字段和关系。提醒按一条简单规则处理(宁可多发一次,不丢):已有提醒记录一律不改挂、不改正文、不恢复,已取走的不动;取消两边当前所有待取的提醒,两边都按还原后的字段重排计划,各自按当前所处阶段补发至多一条新提醒(新 ID):临近时刻已过且未到截止发临近提醒,已过截止时过期提醒开启则发过期提醒并排次日计划,关闭则发一条“目标已到期”提醒、不排次日计划,临近时刻未到则交给新计划。宿主可能因此对同一阶段多收到一条。这一对记为驳回,以后不再自动合并。目标页、目标详情和维护报告在自动合并项上提供撤销。

每次维护 G1 最多调用 100 次、G2 最多 50 次,超出的记“预算用尽”,留到下次。维护报告的 `goal_auto_review`/`goal_duplicate_review` 汇总调用、关闭、保持、无候选、未变化、预算用尽和停止原因(中断续跑时累加),并逐项列出关闭、合并、撤销标记及依据;`GET /admin/api/goal-auto-actions` 分页列出自动处理记录。宿主接口的目标字段不变。

评测:`uv run python evals/goal_review_eval/run.py --corpus <JSON> --out <仓库外目录>`,默认语料是冻结的公开 dev 集 `evals/goal_review_v1.json`。每例隔离数据库、固定时钟,原样写入入口、人物、消息和目标后跑真实的目标阶段(只开 G1);`expected`、`reason`、`category` 在写入前剔除,全部案例跑完后才计分。确定性计分包括误关闭(应 open 却被关闭,或完成与放弃方向相反)、宿主/管理员目标被关闭数、识别率(应关闭案例中状态正确的比例)和按类别明细;`call_counts` 统计超时、429、截断、无效调用和执行错误,`valid` 要求完整、无超时/429/截断/执行错误且没有失败的复核。出现超时、429 或复核失败即停止,整次无效,换新目录重跑。`--fake completed|abandoned|open|invalid|timeout` 只检查管线,不读模型配置、不访问网络。G2 用现有 `evals/goal_dedup_probe/run.py --method D6 --budget-seconds 60` 测量。

## M2 人物与身份联系(后端)

人物管理接口沿用管理员会话、CSRF、JSON Content-Type 和本机 Host 校验;人物页提供列表、详情、否认、确认合并和别名管理,有待确认联系时导航和试用页会提示。
Expand Down
72 changes: 72 additions & 0 deletions evals/goal_review_eval/goal_review_scoring.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""Deterministic G1 scoring; reads labels only after every case has run."""
from __future__ import annotations

from collections import Counter

CLOSED = ('completed', 'abandoned')
# Completed model refusals / malformed output are quality observations; the goal
# simply stays open. Anything else in the call ledger is an execution failure.
QUALITY_CALL_RESULTS = {'success', 'invalid_output', 'item_error', 'content_rejection'}


def ratio(numerator, denominator):
return {'numerator': numerator, 'denominator': denominator,
'rate': numerator / denominator if denominator else None}


def score_case(case, result):
"""result: {'state', 'evidence_keys', 'origin', ...} from the product database."""
expected = case['expected']['state']
actual = result['state']
false_close = (expected == 'open' and actual != 'open') or (
expected in CLOSED and actual in CLOSED and actual != expected)
external = case['goal']['origin'] in ('host', 'admin')
row = {'case_id': case['id'], 'category': case['category'], 'origin': case['goal']['origin'],
'expected': expected, 'actual': actual, 'correct': actual == expected,
'false_close': false_close, 'external_closed': external and actual != 'open',
'actual_evidence': result.get('evidence_keys', []),
'expected_evidence': case['expected'].get('evidence', []),
'review_reason': result.get('review_reason'), 'model_calls': result.get('model_calls', 0)}
if actual in CLOSED and row['expected_evidence']:
row['evidence_overlap'] = sorted(set(row['actual_evidence']) & set(row['expected_evidence']))
return row


def call_counts(calls):
"""Same validity vocabulary as the other evaluations: timeouts, 429, truncation, invalid calls."""
return {'calls': len(calls),
'timeouts': sum(bool(c.get('timed_out')) for c in calls),
'rate_limited': sum(c.get('status_code') == 429 for c in calls),
'truncated': sum(c.get('finish_reason') not in (None, 'stop') for c in calls),
'invalid_calls': sum(c.get('result_category') in ('invalid_output', 'item_error') for c in calls),
'execution_errors': sum(c.get('result_category') not in QUALITY_CALL_RESULTS for c in calls)}


def report(rows, calls, *, expected_cases, failures=()):
"""Gates: false closures 0, host/admin closures 0, recognition >= 80%."""
should_close = [r for r in rows if r['expected'] in CLOSED]
categories = {}
for name in sorted({r['category'] for r in rows}):
members = [r for r in rows if r['category'] == name]
categories[name] = {'cases': len(members), 'correct': sum(r['correct'] for r in members),
'false_closures': sum(r['false_close'] for r in members),
'actual': dict(Counter(r['actual'] for r in members))}
counts = call_counts(calls)
complete = len(rows) == expected_cases
valid = (complete and not failures and counts['timeouts'] == 0 and counts['rate_limited'] == 0
and counts['truncated'] == 0 and counts['execution_errors'] == 0)
result = {'cases': len(rows), 'complete': complete, 'valid': valid, 'call_counts': counts,
'review_failures': list(failures),
'false_closures': {'count': sum(r['false_close'] for r in rows),
'cases': [r['case_id'] for r in rows if r['false_close']]},
'external_closed': {'count': sum(r['external_closed'] for r in rows),
'cases': [r['case_id'] for r in rows if r['external_closed']]},
'recognition': ratio(sum(r['correct'] for r in should_close), len(should_close)),
'accuracy': ratio(sum(r['correct'] for r in rows), len(rows)),
'categories': categories, 'results': rows}
result['gates'] = {
'false_closures_zero': result['false_closures']['count'] == 0,
'external_closed_zero': result['external_closed']['count'] == 0,
'recognition_at_least_80_percent': (result['recognition']['rate'] is not None
and result['recognition']['rate'] >= .8)}
return result
Loading