Ctrl K
ComunidadGuías…

butler/gateway/wechat_scenario_sim/evaluation.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
from __future__ import annotations

from typing import Any

from .schema import ScenarioCase
from .utils import _any_tool_in_list, _tool_in_list, resolve_handler_session_key


def evaluation_reply_text(
    handler: Any,
    *,
    owner_id: str,
    session_key: str,
    reply: str,
    tools: list[str],
    platform: str = "wechat",
) -> str:
    # Lazy imports to break circular dependency:
    # butler.tools.registry → butler.core → butler.core.agent_loop
    # → butler.ops → butler.tools.registry
    from butler.core.session_epoch import load_epoch_transcript_rows
    from butler.gateway.outbound_files import expand_reply_with_wechat_attachments
    from butler.gateway.wechat_scenario_sim_ops import delegate_enrichment_imports_ready
    from butler.report import get_last_report

    delegated = _any_tool_in_list(("delegate_task",), tools) or any(
        mark in reply for mark in ("代理已完成", "代理未能", "委派", "task_")
    )

    if not delegated:
        return str(expand_reply_with_wechat_attachments(reply))
    if not delegate_enrichment_imports_ready():
        return reply
    canonical = resolve_handler_session_key(
        handler,
        owner_id=owner_id,
        session_key=session_key,
        platform=platform,
    )
    report = get_last_report(canonical)
    if report is None:
        return reply
    chunks: list[str] = [reply]
    summary = (report.summary or "").strip()
    if summary and summary not in reply:
        chunks.append(summary)
    child_sk = (report.child_session_key or "").strip()
    if child_sk:
        for row in reversed(load_epoch_transcript_rows(child_sk, max_lines=80)):
            if str(row.get("type") or "") != "assistant":
                continue
            text = str(row.get("content_preview") or row.get("content") or "").strip()
            if text and text not in reply and text not in (summary or ""):
                chunks.append(text)
            break
    merged = "\n\n".join(chunks) if len(chunks) > 1 else reply
    return str(expand_reply_with_wechat_attachments(merged))


def _delegate_evidence_in_reply(reply: str) -> bool:
    return any(
        mark in reply
        for mark in ("委派", "代理已完成", "代理未能", "task_", "📎 委派")
    )


def evaluate_scenario_case(
    tools: list[str],
    reply: str,
    case: ScenarioCase,
    *,
    strict: bool = False,
) -> tuple[list[str], list[str]]:
    errors: list[str] = []
    warnings: list[str] = []
    for bad in case.reject_reply_any:
        if bad in reply:
            errors.append(f"reply must not contain {bad!r}")
    for tool in case.forbid_tools:
        if _tool_in_list(tool, tools):
            errors.append(f"forbidden tool {tool}")
    if case.expect_reply_any:
        if not any(needle in reply for needle in case.expect_reply_any):
            msg = f"reply missing any of {case.expect_reply_any}"
            if case.soft:
                warnings.append(msg)
            else:
                errors.append(msg)
    if case.expect_tools_any and not _any_tool_in_list(case.expect_tools_any, tools):
        msg = f"expected tools any of {case.expect_tools_any}, got {tools}"
        delegate_ok = (
            _any_tool_in_list(("delegate_task",), case.expect_tools_any)
            and _delegate_evidence_in_reply(reply)
        )
        if (strict or case.require_tools) and not case.soft and not delegate_ok:
            errors.append(msg)
        elif not delegate_ok:
            warnings.append(msg)
    if case.prefer_tools_any and not _any_tool_in_list(case.prefer_tools_any, tools):
        direct_ok = bool(case.expect_reply_any) and any(x in reply for x in case.expect_reply_any)
        msg = f"preferred tools {case.prefer_tools_any}, got {tools}"
        if strict and not direct_ok and not case.soft:
            errors.append(msg)
        else:
            warnings.append(msg)
    return errors, warnings


def evaluate_outbound_capture(
    capture_bodies: list[str],
    case: ScenarioCase,
) -> list[str]:
    errors: list[str] = []
    count = len(capture_bodies)
    if case.min_outbound_messages and count < case.min_outbound_messages:
        errors.append(
            f"expected >= {case.min_outbound_messages} outbound messages, got {count}"
        )
    if case.expect_outbound_any:
        joined = "\n".join(capture_bodies)
        if not any(needle in joined for needle in case.expect_outbound_any):
            errors.append(f"outbound missing any of {case.expect_outbound_any}")
    return errors