Ctrl K
ComunidadGuías…

butler/eval_integration/manager.py

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

Archivo original ↗Preguntar a KiBot
"""Eval integration manager — multi-sink orchestration."""

from __future__ import annotations

import json
from pathlib import Path
from typing import Any, cast

from butler.contracts.eval_ports import ScoreSinkPort, SuiteRunResult
from butler.eval_integration.report_schema import build_unified_report
from butler.eval_integration.suite_registry import get_suite, list_suite_ids
from butler.eval_integration.sinks.audit_sink import AuditSink
from butler.eval_integration.sinks.deepeval_sink import DeepEvalSink
from butler.eval_integration.sinks.junit_sink import JUnitSink
from butler.eval_integration.sinks.langfuse_sink import LangFuseSink
from butler.eval_integration.sinks.ragas_sink import RagasSink
from butler.eval_integration.sync_policy import evaluate_sync

ROOT = Path(__file__).resolve().parents[2]
DEFAULT_REPORT = ROOT / ".butler" / "reports" / "eval-unified.json"


class EvalIntegrationManager:
    def __init__(self, sinks: list[ScoreSinkPort] | None = None) -> None:
        self._sinks: list[ScoreSinkPort] = sinks or [
            AuditSink(),
            JUnitSink(),
            LangFuseSink(),
            DeepEvalSink(),
            RagasSink(),
        ]

    def list_suites(self) -> list[str]:
        return cast(list[str], list_suite_ids())

    def run_suites(
        self,
        suite_ids: list[str],
        *,
        warn_only: bool = False,
        sync_dataset: bool = False,
        push_langfuse: bool | None = None,
    ) -> list[SuiteRunResult]:
        results: list[SuiteRunResult] = []
        for sid in suite_ids:
            suite = get_suite(sid)
            result = suite.run(
                warn_only=warn_only,
                sync_dataset=sync_dataset,
                push_langfuse=push_langfuse,
            )
            payload = {"suite_id": sid, "metrics": result.metrics, "ok": result.ok}
            for sink in self._sinks:
                if sink.backend_id == "langfuse" and sid not in (
                    "agent_weekly",
                    "regression",
                    "wechat_corpus",
                    "tcr",
                    "memory_mb",
                    "b9_oracle",
                ):
                    continue
                if sink.backend_id == "deepeval" and sid != "deepeval_agent":
                    continue
                if sink.backend_id == "ragas" and sid != "ragas_memory":
                    continue
                from butler.eval_integration.manager_ops import write_suite_result_safe

                ref = write_suite_result_safe(sink, result, payload)
                if ref:
                    result.sink_refs[sink.backend_id] = ref
            results.append(result)
        return results

    def build_report(
        self,
        results: list[SuiteRunResult],
    ) -> dict[str, Any]:
        sink_status = {s.backend_id: {"registered": True} for s in self._sinks}
        return cast(
            dict[str, Any],
            build_unified_report(results, sink_status=sink_status),
        )

    def run_and_write(
        self,
        suite_ids: list[str],
        *,
        out: Path = DEFAULT_REPORT,
        warn_only: bool = False,
        sync_dataset: bool = False,
        push_langfuse: bool | None = None,
    ) -> tuple[dict[str, Any], list[SuiteRunResult]]:
        results = self.run_suites(
            suite_ids,
            warn_only=warn_only,
            sync_dataset=sync_dataset,
            push_langfuse=push_langfuse,
        )
        for r in results:
            if r.suite_id == "tcr":
                rate = r.metrics.get("trajectory_compliance_rate")
                if rate is not None:
                    from butler.eval_integration.manager_ops import analyse_transform_signals_safe

                    analyse_transform_signals_safe(tcr_rate=float(rate))
        report = self.build_report(results)
        out.parent.mkdir(parents=True, exist_ok=True)
        out.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
        return report, results

    def sync_check(self, suite_id: str) -> dict[str, Any]:
        """Weak consistency check across sinks."""
        rows: dict[str, dict[str, Any] | None] = {}
        for sink in self._sinks:
            rows[sink.backend_id] = sink.read_latest(suite_id)
        return cast(dict[str, Any], evaluate_sync(suite_id, rows))