将「已删即 404」语义收进 IntelligentEvalRepository 单一接缝,消除三处独立裁决; 任务监控开始隐藏已删评估的任务(本 Phase 唯一刻意行为变化)。 - repository.py 新增 visible() 谓词与 require_live_eval() 服务接缝; 六处裸谓词统一走它,get()/get_including_deleted() 语义不变。 - decision_logs.py 删除本地 _require_eval,三处调用迁至 repository 接缝; count_decisions 由 len(.all()) 改为 func.count。 - task_queue.py list_tasks 与 stats 过滤已删评估的任务(行为变化)。 - web/routers/intelligent_evals.py: _require_eval_exists → _require_live_eval, 把 LookupError 翻译为 404;expired 会话 Markdown 标注下沉至 read_model.report_markdown_by_eval;配置快照 11 字段序列化收至 config_snapshot.snapshot_to_dict 单一出口。 - AGENTS.md 登记可见性纪律(已知陷阱 #6)。 - 补 characterization 测试锁定四处契约;更新 task_queue 测试以使用 真实 eval_id(可见性过滤后字面 eval_id 不再可见)。
267 lines
8.9 KiB
Python
267 lines
8.9 KiB
Python
"""Decision-log service (P3 deepening, S2).
|
||
|
||
Pulled out of ``web/routers/intelligent_evals.py`` so the router only handles
|
||
HTTP validation and error translation. The ORM writes and reads now live here.
|
||
"""
|
||
|
||
from typing import Any
|
||
|
||
from sqlalchemy import func
|
||
from sqlmodel import Session, select
|
||
|
||
from agenteval.intelligent_eval.models import IntelligentEvalStatus
|
||
from agenteval.intelligent_eval.repository import IntelligentEvalRepository
|
||
from agenteval.storage.db import (
|
||
IntelligentEvalDB,
|
||
IntelligentEvalDecisionLogDB,
|
||
IntelligentEvalSessionDB,
|
||
)
|
||
|
||
|
||
def _log_to_dict(log: IntelligentEvalDecisionLogDB) -> dict:
|
||
return {
|
||
"id": log.id,
|
||
"eval_id": log.eval_id,
|
||
"decision_type": log.decision_type,
|
||
"reason": log.reason,
|
||
"context": log.get_context(),
|
||
"cron_id": log.cron_id,
|
||
"created_at": log.created_at.isoformat() if log.created_at else None,
|
||
}
|
||
|
||
|
||
def _append_row(
|
||
eval_id: str,
|
||
decision_type: str,
|
||
reason: str,
|
||
cron_id: str,
|
||
context: dict[str, Any],
|
||
session: Session,
|
||
) -> dict:
|
||
log = IntelligentEvalDecisionLogDB(
|
||
eval_id=eval_id,
|
||
decision_type=decision_type,
|
||
reason=reason,
|
||
cron_id=cron_id,
|
||
)
|
||
log.set_context(context)
|
||
session.add(log)
|
||
session.commit()
|
||
session.refresh(log)
|
||
return _log_to_dict(log)
|
||
|
||
|
||
def create_decision_log(
|
||
eval_id: str,
|
||
decision_type: str,
|
||
reason: str,
|
||
cron_id: str,
|
||
context: dict[str, Any],
|
||
session: Session,
|
||
) -> dict:
|
||
"""Agent-reporting entry: create a decision log, or return the existing
|
||
one if the (eval_id, decision_type, context) tuple is already recorded.
|
||
|
||
P3 真问题修复 (T8 / Gitea #6): agent 会在同一分钟内重复上报相同决策,
|
||
按 context JSON 去重保表干净。**去重只服务 agent 上报路径**——平台落账
|
||
每次都是新事实,用 ``append_decision_log``,调用方无需知道去重存在。
|
||
|
||
Raises ``LookupError`` if eval not found.
|
||
"""
|
||
IntelligentEvalRepository(session).require_live_eval(eval_id)
|
||
|
||
# Dedupe: same (eval, decision_type, context) → return existing.
|
||
# Limit to 100 records to avoid loading too many into memory; in practice,
|
||
# an eval rarely has more than a few dozen logs of the same type.
|
||
for existing in session.exec(
|
||
select(IntelligentEvalDecisionLogDB)
|
||
.where(
|
||
IntelligentEvalDecisionLogDB.eval_id == eval_id,
|
||
IntelligentEvalDecisionLogDB.decision_type == decision_type,
|
||
)
|
||
.limit(100)
|
||
).all():
|
||
if existing.get_context() == context:
|
||
return _log_to_dict(existing)
|
||
|
||
return _append_row(eval_id, decision_type, reason, cron_id, context, session)
|
||
|
||
|
||
def append_decision_log(
|
||
eval_id: str,
|
||
decision_type: str,
|
||
reason: str,
|
||
cron_id: str,
|
||
context: dict[str, Any],
|
||
session: Session,
|
||
) -> dict:
|
||
"""Platform-bookkeeping entry: 纯追加,不去重。
|
||
|
||
平台每次落账都是新事实(attempt 递增、task_id 不同、闸门判定),
|
||
相同 context 也总是追加一行。
|
||
|
||
Raises ``LookupError`` if eval not found.
|
||
"""
|
||
IntelligentEvalRepository(session).require_live_eval(eval_id)
|
||
return _append_row(eval_id, decision_type, reason, cron_id, context, session)
|
||
|
||
|
||
def count_decisions(eval_id: str, decision_type: str, session: Session) -> int:
|
||
"""该评估某类型决策日志的条数(平台闸门计数与 attempt 落账的计数原语)。"""
|
||
return session.exec(
|
||
select(func.count())
|
||
.select_from(IntelligentEvalDecisionLogDB)
|
||
.where(
|
||
IntelligentEvalDecisionLogDB.eval_id == eval_id,
|
||
IntelligentEvalDecisionLogDB.decision_type == decision_type,
|
||
)
|
||
).one()
|
||
|
||
|
||
def list_decision_logs(eval_id: str, session: Session) -> list[dict]:
|
||
"""List decision logs for an eval. Raises ``LookupError`` if eval not found."""
|
||
IntelligentEvalRepository(session).require_live_eval(eval_id)
|
||
|
||
logs = session.exec(
|
||
select(IntelligentEvalDecisionLogDB)
|
||
.where(IntelligentEvalDecisionLogDB.eval_id == eval_id)
|
||
.order_by(IntelligentEvalDecisionLogDB.created_at.desc())
|
||
).all()
|
||
|
||
return [_log_to_dict(log) for log in logs]
|
||
|
||
|
||
def _supplement_executing(
|
||
ev: IntelligentEvalDB,
|
||
sessions: list,
|
||
completed: int,
|
||
estimated: int,
|
||
types: set[str],
|
||
session: Session,
|
||
) -> int:
|
||
"""Supplement decision logs for EXECUTING evals."""
|
||
added = 0
|
||
if "execute_session" not in types and completed < estimated:
|
||
_append_row(
|
||
ev.id,
|
||
"execute_session",
|
||
"平台兜底:时段欠账需执行会话",
|
||
"platform",
|
||
{"platform_supplemented": True, "completed": completed, "estimated": estimated},
|
||
session,
|
||
)
|
||
added += 1
|
||
elif "start_analysis" not in types and sessions and completed >= estimated:
|
||
_append_row(
|
||
ev.id,
|
||
"start_analysis",
|
||
"平台兜底:所有会话已完成开始分析",
|
||
"platform",
|
||
{"platform_supplemented": True, "completed": completed, "estimated": estimated},
|
||
session,
|
||
)
|
||
added += 1
|
||
return added
|
||
|
||
|
||
def _supplement_completed(
|
||
ev: IntelligentEvalDB,
|
||
sessions: list,
|
||
completed: int,
|
||
estimated: int,
|
||
plan: dict,
|
||
types: set[str],
|
||
session: Session,
|
||
) -> int:
|
||
"""Supplement decision logs for COMPLETED evals (historical backfill)."""
|
||
added = 0
|
||
if "execute_session" not in types:
|
||
slots = plan.get("time_distribution") or []
|
||
if slots:
|
||
for slot in slots:
|
||
_append_row(
|
||
ev.id,
|
||
"execute_session",
|
||
f"平台兜底:时段{slot.get('time_slot', '')}执行会话(历史回填)",
|
||
"platform",
|
||
{
|
||
"platform_supplemented": True,
|
||
"time_slot": slot.get("time_slot"),
|
||
"sessions": slot.get("sessions"),
|
||
"completed": completed,
|
||
"estimated": estimated,
|
||
},
|
||
session,
|
||
)
|
||
added += 1
|
||
else:
|
||
_append_row(
|
||
ev.id,
|
||
"execute_session",
|
||
"平台兜底:执行会话(历史回填)",
|
||
"platform",
|
||
{"platform_supplemented": True, "completed": completed, "estimated": estimated},
|
||
session,
|
||
)
|
||
added += 1
|
||
if "start_analysis" not in types and sessions:
|
||
_append_row(
|
||
ev.id,
|
||
"start_analysis",
|
||
"平台兜底:所有会话已完成开始分析(历史回填)",
|
||
"platform",
|
||
{"platform_supplemented": True, "completed": completed, "estimated": estimated},
|
||
session,
|
||
)
|
||
added += 1
|
||
return added
|
||
|
||
|
||
def supplement_decision_logs(session: Session) -> int:
|
||
"""Platform audit backfill for decision logs.
|
||
|
||
方案③的决策日志由 OpenClaw agent 上报(LLM 自主,尽力而为)——异常路径
|
||
(如卡死恢复后重试)agent 可能跳过上报,导致决策过程页面为空。这里按评估
|
||
状态推导决策并补录:
|
||
|
||
- EXECUTING:欠账(completed < estimated)补 execute_session,所有会话
|
||
完成后补 start_analysis。
|
||
- COMPLETED:历史评估/异常路径可能完全没有决策日志,回填 execute_session
|
||
(按 plan 时段逐条)+ start_analysis,让旧报告也有决策过程可看。
|
||
|
||
只补"该类型缺失"的,不重复;且只记录状态,不改变 agent 的实际执行。
|
||
|
||
Returns:
|
||
补录的决策日志条数。
|
||
"""
|
||
evals = session.exec(
|
||
select(IntelligentEvalDB).where(
|
||
IntelligentEvalDB.status.in_(
|
||
[
|
||
IntelligentEvalStatus.EXECUTING.value,
|
||
IntelligentEvalStatus.COMPLETED.value,
|
||
]
|
||
)
|
||
)
|
||
).all()
|
||
|
||
added = 0
|
||
for ev in evals:
|
||
plan = ev.get_plan() if ev.plan else {}
|
||
estimated = plan.get("estimated_sessions", 0)
|
||
sessions = session.exec(select(IntelligentEvalSessionDB).where(IntelligentEvalSessionDB.eval_id == ev.id)).all()
|
||
completed = sum(1 for s in sessions if s.status == "completed")
|
||
types = {
|
||
x.decision_type
|
||
for x in session.exec(
|
||
select(IntelligentEvalDecisionLogDB).where(IntelligentEvalDecisionLogDB.eval_id == ev.id)
|
||
).all()
|
||
}
|
||
|
||
if ev.status == IntelligentEvalStatus.EXECUTING.value:
|
||
added += _supplement_executing(ev, sessions, completed, estimated, types, session)
|
||
elif ev.status == IntelligentEvalStatus.COMPLETED.value:
|
||
added += _supplement_completed(ev, sessions, completed, estimated, plan, types, session)
|
||
|
||
return added
|