Spaces:
Sleeping
Sleeping
| """ | |
| Worker Agent 5 — Evaluation Agent | |
| Task: Score the complete RAG pipeline end-to-end. | |
| Measures faithfulness, relevance, and pipeline integrity. | |
| """ | |
| from __future__ import annotations | |
| import json | |
| import math | |
| from pathlib import Path | |
| from typing import Optional | |
| from workers.base_worker import BaseWorker | |
| from workers.embedding_env import mock_embed | |
| EVALUATION_TASK_CONFIGS = { | |
| "easy_evaluation": {"task_id": "easy_evaluation", "difficulty": "easy", "step_budget": 8}, | |
| "medium_evaluation": {"task_id": "medium_evaluation", "difficulty": "medium", "step_budget": 10}, | |
| "hard_evaluation": {"task_id": "hard_evaluation", "difficulty": "hard", "step_budget": 12}, | |
| } | |
| def cosine_sim(a: list[float], b: list[float]) -> float: | |
| dot = sum(x * y for x, y in zip(a, b)) | |
| na = math.sqrt(sum(x * x for x in a)) or 1.0 | |
| nb = math.sqrt(sum(x * x for x in b)) or 1.0 | |
| return dot / (na * nb) | |
| class EvaluationEnv(BaseWorker): | |
| """ | |
| Worker Agent 5: Evaluation Agent. | |
| Actions: | |
| - run_faithfulness_check: Check if retrieved chunks contain expected answers | |
| - run_relevance_check: Check semantic overlap of retrieved chunks with queries | |
| - check_pipeline_integrity: Verify all upstream outputs exist | |
| - compute_composite_score: Compute weighted final score | |
| - generate_eval_report: Generate full evaluation report | |
| - submit: Submit evaluation results | |
| """ | |
| VALID_ACTIONS = [ | |
| "run_faithfulness_check", "run_relevance_check", | |
| "check_pipeline_integrity", "compute_composite_score", | |
| "generate_eval_report", "submit" | |
| ] | |
| def __init__(self) -> None: | |
| super().__init__(worker_id="worker_5", worker_name="Evaluation Agent") | |
| self.faithfulness_score: float = 0.0 | |
| self.relevance_score: float = 0.0 | |
| self.pipeline_integrity_score: float = 0.0 | |
| self.composite_score: float = 0.0 | |
| self.faithfulness_done: bool = False | |
| self.relevance_done: bool = False | |
| self.integrity_done: bool = False | |
| self.composite_done: bool = False | |
| self.eval_report: dict = {} | |
| self.task_config: dict = {} | |
| self.index: dict = {} | |
| self.retrieval_results: list[dict] = [] | |
| self.ground_truth: list[dict] = [] | |
| self.chunk_data: dict[str, str] = {} # chunk_id -> text | |
| def reset( | |
| self, | |
| task_id: str, | |
| index: Optional[dict] = None, | |
| retrieval_results: Optional[list] = None, | |
| chunk_data: Optional[dict] = None, | |
| ) -> dict: | |
| self._reset_episode_tracking() | |
| self.task_id = task_id | |
| self.task_config = EVALUATION_TASK_CONFIGS.get(task_id, EVALUATION_TASK_CONFIGS["easy_evaluation"]) | |
| self.step_budget = self.task_config["step_budget"] | |
| self.step_budget_remaining = self.step_budget | |
| self.faithfulness_score = 0.0 | |
| self.relevance_score = 0.0 | |
| self.pipeline_integrity_score = 0.0 | |
| self.composite_score = 0.0 | |
| self.faithfulness_done = False | |
| self.relevance_done = False | |
| self.integrity_done = False | |
| self.composite_done = False | |
| self.eval_report = {} | |
| self.index = index or {} | |
| self.retrieval_results = retrieval_results or [] | |
| self.chunk_data = chunk_data or self._load_chunk_data() | |
| self.ground_truth = self._load_ground_truth() | |
| return self.state() | |
| def step(self, action_dict: dict) -> tuple[dict, float, bool, dict]: | |
| operation = action_dict.get("operation", "") | |
| parameters = action_dict.get("parameters", {}) | |
| if self.is_done: | |
| return self.state(), 0.0, True, {"error": "episode_already_done", "action": operation} | |
| if operation not in self.VALID_ACTIONS: | |
| self._record_governance_event("invalid_action", "medium", f"Unknown: {operation}") | |
| reward = self._clip_reward(0.0) | |
| self.step_count += 1 | |
| self.step_budget_remaining -= 1 | |
| self.total_reward += reward | |
| if self._is_budget_exhausted(): | |
| self.is_done = True | |
| info = {"error": f"invalid_action:{operation}", "action": operation} | |
| self._record_action(operation, reward, info) | |
| return self.state(), reward, self.is_done, info | |
| reward, info = self._dispatch(operation, parameters) | |
| reward = self._clip_reward(reward) | |
| self.step_count += 1 | |
| self.step_budget_remaining -= 1 | |
| self.total_reward += reward | |
| done = self.is_done or self._is_budget_exhausted() | |
| if done: | |
| self.is_done = True | |
| self._record_action(operation, reward, info) | |
| return self.state(), reward, done, info | |
| def state(self) -> dict: | |
| base = self._get_base_state() | |
| base.update({ | |
| "faithfulness_score": round(self.faithfulness_score, 4), | |
| "relevance_score": round(self.relevance_score, 4), | |
| "pipeline_integrity_score": round(self.pipeline_integrity_score, 4), | |
| "composite_score": round(self.composite_score, 4), | |
| "faithfulness_done": self.faithfulness_done, | |
| "relevance_done": self.relevance_done, | |
| "integrity_done": self.integrity_done, | |
| "composite_done": self.composite_done, | |
| }) | |
| return base | |
| def generate_run_report(self) -> dict: | |
| report = self._get_base_report() | |
| report.update({ | |
| "faithfulness_score": self.faithfulness_score, | |
| "relevance_score": self.relevance_score, | |
| "pipeline_integrity_score": self.pipeline_integrity_score, | |
| "composite_score": self.composite_score, | |
| "eval_report": self.eval_report, | |
| "final_score": self._compute_final_score(), | |
| }) | |
| return report | |
| def evaluate_run(self) -> dict: | |
| epsilon = 1e-6 | |
| score = min(max(self._compute_final_score(), epsilon), 1.0 - epsilon) | |
| gates = { | |
| "faithfulness_checked": self.faithfulness_done, | |
| "relevance_checked": self.relevance_done, | |
| "integrity_checked": self.integrity_done, | |
| "composite_computed": self.composite_done, | |
| "submitted": self.submitted, | |
| } | |
| return {"approved": all(gates.values()) and score >= 0.55, "gates": gates, "composite_score": score} | |
| def _dispatch(self, operation: str, parameters: dict) -> tuple[float, dict]: | |
| if operation == "run_faithfulness_check": | |
| return self._run_faithfulness_check() | |
| elif operation == "run_relevance_check": | |
| return self._run_relevance_check() | |
| elif operation == "check_pipeline_integrity": | |
| return self._check_pipeline_integrity() | |
| elif operation == "compute_composite_score": | |
| return self._compute_composite_score() | |
| elif operation == "generate_eval_report": | |
| return self._generate_eval_report() | |
| elif operation == "submit": | |
| return self._submit() | |
| return 0.0, {"error": "unknown_operation"} | |
| def _run_faithfulness_check(self) -> tuple[float, dict]: | |
| hits = 0 | |
| for qa in self.ground_truth: | |
| chunk_text = self.chunk_data.get(qa["chunk_id"], "") | |
| answer = qa["answer"].lower() | |
| if answer in chunk_text.lower(): | |
| hits += 1 | |
| self.faithfulness_score = hits / max(len(self.ground_truth), 1) | |
| self.faithfulness_done = True | |
| reward = 0.4 * self.faithfulness_score + 0.1 | |
| return reward, {"error": None, "action": "run_faithfulness_check", "faithfulness_score": round(self.faithfulness_score, 4)} | |
| def _run_relevance_check(self) -> tuple[float, dict]: | |
| if not self.retrieval_results: | |
| self.relevance_score = 0.5 | |
| self.relevance_done = True | |
| return 0.2, {"error": None, "action": "run_relevance_check", "relevance_score": 0.5} | |
| scores = [] | |
| for result in self.retrieval_results[:10]: | |
| q_vec = mock_embed(result["question"]) | |
| for cid in result.get("retrieved_chunk_ids", [])[:3]: | |
| chunk_text = self.chunk_data.get(cid, result["question"]) | |
| c_vec = mock_embed(chunk_text) | |
| scores.append(cosine_sim(q_vec, c_vec)) | |
| self.relevance_score = sum(scores) / max(len(scores), 1) | |
| self.relevance_done = True | |
| reward = 0.3 * self.relevance_score + 0.1 | |
| return reward, {"error": None, "action": "run_relevance_check", "relevance_score": round(self.relevance_score, 4)} | |
| def _check_pipeline_integrity(self) -> tuple[float, dict]: | |
| checks = { | |
| "index_exists": len(self.index) > 0, | |
| "chunks_exist": len(self.chunk_data) > 0, | |
| "ground_truth_exists": len(self.ground_truth) > 0, | |
| "retrieval_results_exist": len(self.retrieval_results) > 0, | |
| } | |
| passed = sum(checks.values()) | |
| self.pipeline_integrity_score = passed / len(checks) | |
| self.integrity_done = True | |
| reward = 0.2 * self.pipeline_integrity_score + 0.1 | |
| return reward, {"error": None, "action": "check_pipeline_integrity", "checks": checks, "integrity_score": round(self.pipeline_integrity_score, 4)} | |
| def _compute_composite_score(self) -> tuple[float, dict]: | |
| epsilon = 1e-6 | |
| raw = ( | |
| 0.4 * self.faithfulness_score + | |
| 0.3 * self.relevance_score + | |
| 0.2 * self.pipeline_integrity_score + | |
| 0.1 * (self.step_budget_remaining / max(self.step_budget, 1)) | |
| ) | |
| self.composite_score = min(max(raw, epsilon), 1.0 - epsilon) | |
| self.composite_done = True | |
| return 0.3, {"error": None, "action": "compute_composite_score", "composite_score": round(self.composite_score, 4)} | |
| def _generate_eval_report(self) -> tuple[float, dict]: | |
| self.eval_report = { | |
| "faithfulness_score": round(self.faithfulness_score, 4), | |
| "relevance_score": round(self.relevance_score, 4), | |
| "pipeline_integrity_score": round(self.pipeline_integrity_score, 4), | |
| "composite_score": round(self.composite_score, 4), | |
| "checks_completed": { | |
| "faithfulness": self.faithfulness_done, | |
| "relevance": self.relevance_done, | |
| "integrity": self.integrity_done, | |
| "composite": self.composite_done, | |
| }, | |
| "recommendation": "APPROVE" if self.composite_score >= 0.6 else "REJECT", | |
| } | |
| return 0.25, {"error": None, "action": "generate_eval_report", "report": self.eval_report} | |
| def _submit(self) -> tuple[float, dict]: | |
| if not (self.faithfulness_done and self.relevance_done and self.integrity_done): | |
| self._record_governance_event("incomplete_eval", "high", "submit before all checks done") | |
| return 0.05, {"error": "evaluation_incomplete", "action": "submit"} | |
| self.submitted = True | |
| self.is_done = True | |
| final_score = self._compute_final_score() | |
| return min(final_score + 0.15, 0.99), {"error": None, "action": "submit", "final_score": final_score} | |
| def _compute_final_score(self) -> float: | |
| epsilon = 1e-6 | |
| raw = ( | |
| 0.4 * self.faithfulness_score + | |
| 0.3 * self.relevance_score + | |
| 0.2 * self.pipeline_integrity_score + | |
| 0.1 * (self.step_budget_remaining / max(self.step_budget, 1)) | |
| ) | |
| return float(min(max(raw, epsilon), 1.0 - epsilon)) | |
| def _load_chunk_data(self) -> dict[str, str]: | |
| path = Path("data/nexacrm_corpus.json") | |
| if path.exists(): | |
| with open(path) as f: | |
| corpus = json.load(f) | |
| return {c["chunk_id"]: c["text"] for c in corpus} | |
| from data.setup_dataset import GROUND_TRUTH_QA | |
| return {qa["chunk_id"]: f"{qa['question']} {qa['answer']}" for qa in GROUND_TRUTH_QA} | |
| def _load_ground_truth(self) -> list[dict]: | |
| path = Path("data/ground_truth_qa.json") | |
| if path.exists(): | |
| with open(path) as f: | |
| return json.load(f)[:20] | |
| from data.setup_dataset import GROUND_TRUTH_QA | |
| return GROUND_TRUTH_QA[:20] | |