Spaces:
Runtime error
Runtime error
| from typing import List, Optional, Dict, Any | |
| from app.schemas.evaluation_schema import ( | |
| RetrievalTestCase, | |
| RetrievalEvaluationRunRequest, | |
| RetrievalSingleResult, | |
| RetrievalEvaluationSummary, | |
| RetrievalEvaluationReport | |
| ) | |
| from app.evaluation.retrieval_eval_storage import load_retrieval_test_cases | |
| from app.retrieval.hybrid_search_service import retrieve_chunks | |
| def run_retrieval_evaluation( | |
| request: RetrievalEvaluationRunRequest | |
| ) -> RetrievalEvaluationReport: | |
| all_test_cases = load_retrieval_test_cases() | |
| if request.test_case_ids: | |
| selected_ids = set(request.test_case_ids) | |
| test_cases = [ | |
| test_case for test_case in all_test_cases | |
| if test_case.test_case_id in selected_ids | |
| ] | |
| else: | |
| test_cases = all_test_cases | |
| results = [] | |
| for test_case in test_cases: | |
| result = evaluate_single_test_case( | |
| test_case=test_case, | |
| top_k_override=request.top_k_override, | |
| retrieval_mode_override=request.retrieval_mode_override | |
| ) | |
| results.append(result) | |
| summary = build_evaluation_summary(results) | |
| return RetrievalEvaluationReport( | |
| summary=summary, | |
| results=results | |
| ) | |
| def evaluate_single_test_case( | |
| test_case: RetrievalTestCase, | |
| top_k_override: Optional[int] = None, | |
| retrieval_mode_override: Optional[str] = None | |
| ) -> RetrievalSingleResult: | |
| top_k = top_k_override or test_case.top_k | |
| retrieval_mode = retrieval_mode_override or test_case.retrieval_mode | |
| retrieval_output = retrieve_chunks( | |
| query=test_case.question, | |
| document_id=test_case.search_document_id, | |
| top_k=top_k, | |
| retrieval_mode=retrieval_mode | |
| ) | |
| retrieved_results = retrieval_output.get("results", []) | |
| expected_document_hit = evaluate_expected_document_hit( | |
| retrieved_results, | |
| test_case.expected_document_id | |
| ) | |
| expected_source_file_hit = evaluate_expected_source_file_hit( | |
| retrieved_results, | |
| test_case.expected_source_file_name | |
| ) | |
| expected_page_hit = evaluate_expected_page_hit( | |
| retrieved_results, | |
| test_case.expected_page_numbers | |
| ) | |
| expected_chunk_hit = evaluate_expected_chunk_hit( | |
| retrieved_results, | |
| test_case.expected_chunk_ids | |
| ) | |
| best_match_rank = find_best_match_rank( | |
| retrieved_results=retrieved_results, | |
| test_case=test_case | |
| ) | |
| reciprocal_rank = 0.0 | |
| if best_match_rank is not None and best_match_rank > 0: | |
| reciprocal_rank = 1.0 / best_match_rank | |
| failure_reasons = build_failure_reasons( | |
| expected_document_hit=expected_document_hit, | |
| expected_source_file_hit=expected_source_file_hit, | |
| expected_page_hit=expected_page_hit, | |
| expected_chunk_hit=expected_chunk_hit | |
| ) | |
| passed = len(failure_reasons) == 0 | |
| top_result = None | |
| if retrieved_results: | |
| top_result = simplify_result(retrieved_results[0], rank=1) | |
| retrieved_results_preview = [ | |
| simplify_result(result, rank=index + 1) | |
| for index, result in enumerate(retrieved_results[:10]) | |
| ] | |
| return RetrievalSingleResult( | |
| test_case_id=test_case.test_case_id, | |
| question=test_case.question, | |
| passed=passed, | |
| failure_reasons=failure_reasons, | |
| expected_document_id=test_case.expected_document_id, | |
| expected_source_file_name=test_case.expected_source_file_name, | |
| expected_page_numbers=test_case.expected_page_numbers, | |
| expected_chunk_ids=test_case.expected_chunk_ids, | |
| top_k=top_k, | |
| retrieval_mode=retrieval_mode, | |
| retrieved_count=len(retrieved_results), | |
| expected_document_hit=expected_document_hit, | |
| expected_source_file_hit=expected_source_file_hit, | |
| expected_page_hit=expected_page_hit, | |
| expected_chunk_hit=expected_chunk_hit, | |
| best_match_rank=best_match_rank, | |
| reciprocal_rank=reciprocal_rank, | |
| top_result=top_result, | |
| retrieved_results_preview=retrieved_results_preview | |
| ) | |
| def evaluate_expected_document_hit( | |
| results: List[Dict[str, Any]], | |
| expected_document_id: Optional[str] | |
| ) -> Optional[bool]: | |
| if not expected_document_id: | |
| return None | |
| return any( | |
| result.get("document_id") == expected_document_id | |
| for result in results | |
| ) | |
| def evaluate_expected_source_file_hit( | |
| results: List[Dict[str, Any]], | |
| expected_source_file_name: Optional[str] | |
| ) -> Optional[bool]: | |
| if not expected_source_file_name: | |
| return None | |
| return any( | |
| result.get("source_file_name") == expected_source_file_name | |
| for result in results | |
| ) | |
| def evaluate_expected_page_hit( | |
| results: List[Dict[str, Any]], | |
| expected_page_numbers: List[int] | |
| ) -> Optional[bool]: | |
| if not expected_page_numbers: | |
| return None | |
| expected_pages = set(expected_page_numbers) | |
| return any( | |
| result.get("page_number") in expected_pages | |
| for result in results | |
| ) | |
| def evaluate_expected_chunk_hit( | |
| results: List[Dict[str, Any]], | |
| expected_chunk_ids: List[str] | |
| ) -> Optional[bool]: | |
| if not expected_chunk_ids: | |
| return None | |
| expected_chunks = set(expected_chunk_ids) | |
| return any( | |
| result.get("chunk_id") in expected_chunks | |
| for result in results | |
| ) | |
| def find_best_match_rank( | |
| retrieved_results: List[Dict[str, Any]], | |
| test_case: RetrievalTestCase | |
| ) -> Optional[int]: | |
| for index, result in enumerate(retrieved_results, start=1): | |
| if result_matches_any_expectation(result, test_case): | |
| return index | |
| return None | |
| def result_matches_any_expectation( | |
| result: Dict[str, Any], | |
| test_case: RetrievalTestCase | |
| ) -> bool: | |
| if ( | |
| test_case.expected_chunk_ids | |
| and result.get("chunk_id") in set(test_case.expected_chunk_ids) | |
| ): | |
| return True | |
| if ( | |
| test_case.expected_page_numbers | |
| and result.get("page_number") in set(test_case.expected_page_numbers) | |
| ): | |
| return True | |
| if ( | |
| test_case.expected_document_id | |
| and result.get("document_id") == test_case.expected_document_id | |
| ): | |
| return True | |
| if ( | |
| test_case.expected_source_file_name | |
| and result.get("source_file_name") == test_case.expected_source_file_name | |
| ): | |
| return True | |
| return False | |
| def build_failure_reasons( | |
| expected_document_hit: Optional[bool], | |
| expected_source_file_hit: Optional[bool], | |
| expected_page_hit: Optional[bool], | |
| expected_chunk_hit: Optional[bool] | |
| ) -> List[str]: | |
| failure_reasons = [] | |
| if expected_document_hit is False: | |
| failure_reasons.append("Expected document was not retrieved.") | |
| if expected_source_file_hit is False: | |
| failure_reasons.append("Expected source file was not retrieved.") | |
| if expected_page_hit is False: | |
| failure_reasons.append("Expected page was not retrieved.") | |
| if expected_chunk_hit is False: | |
| failure_reasons.append("Expected chunk was not retrieved.") | |
| return failure_reasons | |
| def simplify_result(result: Dict[str, Any], rank: int) -> Dict[str, Any]: | |
| content = result.get("content", "") | |
| return { | |
| "rank": rank, | |
| "score": result.get("score"), | |
| "chunk_id": result.get("chunk_id"), | |
| "document_id": result.get("document_id"), | |
| "source_file_name": result.get("source_file_name"), | |
| "page_number": result.get("page_number"), | |
| "content_type": result.get("content_type"), | |
| "content_preview": content[:300] | |
| } | |
| def build_evaluation_summary( | |
| results: List[RetrievalSingleResult] | |
| ) -> RetrievalEvaluationSummary: | |
| total_cases = len(results) | |
| if total_cases == 0: | |
| return RetrievalEvaluationSummary( | |
| total_cases=0, | |
| passed_cases=0, | |
| failed_cases=0, | |
| pass_rate=0.0, | |
| mean_reciprocal_rank=0.0 | |
| ) | |
| passed_cases = sum(1 for result in results if result.passed) | |
| failed_cases = total_cases - passed_cases | |
| pass_rate = round(passed_cases / total_cases, 4) | |
| mean_reciprocal_rank = round( | |
| sum(result.reciprocal_rank for result in results) / total_cases, | |
| 4 | |
| ) | |
| document_hit_rate = compute_optional_rate( | |
| [result.expected_document_hit for result in results] | |
| ) | |
| source_file_hit_rate = compute_optional_rate( | |
| [result.expected_source_file_hit for result in results] | |
| ) | |
| page_hit_rate = compute_optional_rate( | |
| [result.expected_page_hit for result in results] | |
| ) | |
| chunk_hit_rate = compute_optional_rate( | |
| [result.expected_chunk_hit for result in results] | |
| ) | |
| return RetrievalEvaluationSummary( | |
| total_cases=total_cases, | |
| passed_cases=passed_cases, | |
| failed_cases=failed_cases, | |
| pass_rate=pass_rate, | |
| mean_reciprocal_rank=mean_reciprocal_rank, | |
| document_hit_rate=document_hit_rate, | |
| source_file_hit_rate=source_file_hit_rate, | |
| page_hit_rate=page_hit_rate, | |
| chunk_hit_rate=chunk_hit_rate | |
| ) | |
| def compute_optional_rate(values: List[Optional[bool]]) -> Optional[float]: | |
| actual_values = [ | |
| value for value in values | |
| if value is not None | |
| ] | |
| if not actual_values: | |
| return None | |
| true_count = sum(1 for value in actual_values if value is True) | |
| return round(true_count / len(actual_values), 4) | |