AgentEvalTool/backend/agenteval/intelligent_eval/decision_logs.py
sinohqb 71543f042a refactor(intelligent-eval): 可见性接缝收敛(Phase 1)
将「已删即 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 不再可见)。
2026-08-24 05:47:00 +08:00

267 lines
8.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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