PMFlow/backend/evals/rag_project_understanding_e...

152 lines
5.2 KiB
Python

"""Project understanding regression eval for AdaptiveRAGService.
Usage:
python backend/evals/rag_project_understanding_eval.py --tenant-id 1
"""
from __future__ import annotations
import argparse
import asyncio
import json
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Any
def _bootstrap_path() -> None:
this_file = Path(__file__).resolve()
backend_root = this_file.parents[1]
if str(backend_root) not in sys.path:
sys.path.insert(0, str(backend_root))
_bootstrap_path()
from app.database import AsyncSessionLocal # noqa: E402
from app.services.adaptive_rag_service import AdaptiveRAGService # noqa: E402
@dataclass(slots=True)
class EvalCase:
question: str
expected_keywords: list[str]
CASES: list[EvalCase] = [
EvalCase(
question="智能配置功能是用来干嘛的?",
expected_keywords=["智能配置", "配置", "/ai-config", "知识库"],
),
EvalCase(
question="能力管理页面主要做什么?",
expected_keywords=["能力", "技能", "/skills", "推荐"],
),
EvalCase(
question="AI 管家页面有哪些用途?",
expected_keywords=["AI 管家", "/ai-steward", "冲突", "问答"],
),
EvalCase(
question="项目列表页面入口在哪,负责什么?",
expected_keywords=["项目", "/projects", "状态"],
),
EvalCase(
question="团队负载功能的目标是什么?",
expected_keywords=["负载", "/workloads", "调配"],
),
]
def _keyword_hit_ratio(text: str, keywords: list[str]) -> float:
haystack = str(text or "").lower()
if not keywords:
return 1.0
hit = 0
for kw in keywords:
if str(kw).strip().lower() in haystack:
hit += 1
return round(hit / max(len(keywords), 1), 4)
async def _run_eval(tenant_id: int) -> dict[str, Any]:
async with AsyncSessionLocal() as db:
service = AdaptiveRAGService(db, tenant_id=tenant_id)
system_snapshot = await service.context_cache.get_system_snapshot(db=db, tenant_id=tenant_id)
rows: list[dict[str, Any]] = []
evidence_hits: list[float] = []
for case in CASES:
feature_matches = service._match_feature_catalog_entries(
question=case.question,
system_snapshot=system_snapshot,
limit=2,
)
direct_answer = service._build_snapshot_direct_answer(
case.question,
system_snapshot=system_snapshot,
user_snapshot={},
) or ""
retrieval = await service.multi_step_search(
question=case.question,
project_id=None,
source_chat_id=None,
session_state=None,
)
retrieval_text = "\n".join(
str(item.get("label") or "") + " " + str(item.get("metadata") or "")
for item in retrieval.get("items") or []
if isinstance(item, dict)
)
merged_text = f"{direct_answer}\n{retrieval_text}"
hit_ratio = _keyword_hit_ratio(merged_text, case.expected_keywords)
evidence_ratio = 0.0
if feature_matches:
top = feature_matches[0]
expected_evidence: list[str] = []
route = str(top.get("route") or "").strip()
page_file = str(top.get("page_file") or "").strip()
apis = top.get("apis") if isinstance(top.get("apis"), list) else []
if route:
expected_evidence.append(route)
if page_file:
expected_evidence.append(page_file)
for api in apis[:2]:
if str(api).strip():
expected_evidence.append(str(api).strip())
evidence_ratio = _keyword_hit_ratio(direct_answer, expected_evidence[:3]) if expected_evidence else 0.0
evidence_hits.append(evidence_ratio)
rows.append(
{
"question": case.question,
"expected_keywords": case.expected_keywords,
"keyword_hit_ratio": hit_ratio,
"feature_evidence_ratio": evidence_ratio,
"direct_answer_preview": direct_answer[:240],
"retrieval_item_count": len(retrieval.get("items") or []),
}
)
avg_score = round(sum(float(x["keyword_hit_ratio"]) for x in rows) / max(len(rows), 1), 4)
avg_feature_evidence_ratio = round(sum(evidence_hits) / max(len(evidence_hits), 1), 4)
return {
"tenant_id": tenant_id,
"case_count": len(rows),
"avg_keyword_hit_ratio": avg_score,
"avg_feature_evidence_ratio": avg_feature_evidence_ratio,
"cases": rows,
}
def main() -> None:
parser = argparse.ArgumentParser(description="Run project understanding eval.")
parser.add_argument("--tenant-id", type=int, default=1, help="Tenant ID")
args = parser.parse_args()
result = asyncio.run(_run_eval(args.tenant_id))
print(json.dumps(result, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()