import argparse import time from pathlib import Path from typing import Any from src.common.console import configure_utf8_stdio from src.common.env_loader import load_project_env from .io_utils import save_json from .answer_pipeline import DEFAULT_CONFIG_PATH, AnswerPipeline TEST_QUERIES = [ "Học vụ liên hệ ai?", "CNTT ở đâu?", "Giấy tờ sinh viên làm ở đâu?", "Học bổng hỏi ai?", "Phòng CNTT ở đâu?", "Khoa CNTT ở đâu?", "Có thể học vượt để ra trường sớm không?", "Có giới hạn số lần học lại một môn không?", "Muốn tạm nghỉ học cần mẫu đơn nào?", "Điểm rèn luyện 85 là loại gì?", "Tính điểm học bổng nếu GPA 3.2 và rèn luyện 90", "Email phòng CTCT-HSSV là gì?", "Điều kiện xét học bổng khuyến khích học tập là gì?", "Đơn xin vào KTX cần thông tin gì?", "Form xin xác nhận sinh viên nằm ở trang nào?", "GPA 3.4 là loại học lực gì?", "Điểm B+ quy đổi sang thang điểm 4 là bao nhiêu?", "Khoa Công nghệ thông tin có ngành nào?", "Khoa Tiếng Anh có những ngành nào?", "Quy trình vào ký túc xá như thế nào?", "Thủ tục xét học bổng gồm những bước nào?", "Học vụ", "Giấy tờ", "Liên hệ ai?", ] EXPECTED_RESULTS = { "Học vụ liên hệ ai?": { "status": "needs_clarification", "llm_called": False, "clarification_needed": True, }, "CNTT ở đâu?": { "status": "needs_clarification", "llm_called": False, "clarification_needed": True, }, "Giấy tờ sinh viên làm ở đâu?": { "status": "needs_clarification", "llm_called": False, "clarification_needed": True, }, "Học bổng hỏi ai?": { "status": "needs_clarification", "llm_called": False, "clarification_needed": True, }, "Phòng CNTT ở đâu?": { "intent": "office_query", "clarification_needed": False, }, "Khoa CNTT ở đâu?": { "intent": "faculty_query", "clarification_needed": False, }, "Có thể học vượt để ra trường sớm không?": { "clarification_needed": False, }, "Có giới hạn số lần học lại một môn không?": { "clarification_needed": False, }, "Muốn tạm nghỉ học cần mẫu đơn nào?": { "clarification_needed": False, }, "Điểm rèn luyện 85 là loại gì?": { "clarification_needed": False, }, } def main() -> None: configure_utf8_stdio() load_project_env() parser = argparse.ArgumentParser( description="Run robust answer-generation batch evaluation." ) parser.add_argument( "--config", default=str(DEFAULT_CONFIG_PATH), help="Config YAML path." ) parser.add_argument( "--limit", type=int, default=10, help="Max queries to run by default." ) parser.add_argument( "--all", action="store_true", help="Run all bundled test queries." ) args = parser.parse_args() pipeline = AnswerPipeline(config_path=args.config) queries = TEST_QUERIES if args.all else TEST_QUERIES[: max(0, args.limit)] output_path = Path(pipeline.config["output"]["test_report"]) results: list[dict[str, Any]] = [] validation_errors: list[str] = [] for query in queries: print("=" * 80) print("Query:", query) try: result = pipeline.answer(query) except Exception as exc: result = _build_unhandled_error_result(query, exc) validation_errors.extend(_validate_expected_result(query, result)) results.append(result) save_json(_build_report(results, validation_errors), output_path) print( "Status:", result.get("status"), "| llm_called:", result.get("llm_called"), "| used_cache:", result.get("used_cache"), ) print(result.get("answer", "")) for error in _validate_expected_result(query, result): print("Validation:", error) if result.get("llm_called"): time.sleep( float(pipeline.config.get("llm", {}).get("request_sleep_seconds", 2)) ) save_json(_build_report(results, validation_errors), output_path) print( f"\nAnswer-generation batch evaluation completed. Report saved: {output_path}" ) if validation_errors: print("\nValidation errors:") for error in validation_errors: print("-", error) raise SystemExit(1) def _build_report( results: list[dict[str, Any]], validation_errors: list[str] | None = None, ) -> dict[str, Any]: validation_errors = validation_errors or [] summary = { "total_queries": len(results), "answered_count": sum( 1 for result in results if result.get("status") == "answered" ), "needs_clarification_count": sum( 1 for result in results if result.get("status") == "needs_clarification" ), "fallback_count": sum( 1 for result in results if result.get("status") in {"fallback", "api_error", "retrieval_error", "low_confidence"} ), "api_error_count": sum( 1 for result in results if result.get("status") == "api_error" ), "cache_hit_count": sum( 1 for result in results if result.get("used_cache") is True ), "llm_call_count": sum( 1 for result in results if result.get("llm_called") is True ), "low_confidence_count": sum( 1 for result in results if result.get("status") == "low_confidence" ), "retrieval_error_count": sum( 1 for result in results if result.get("status") == "retrieval_error" ), "validation_error_count": len(validation_errors), } return { "pipeline_stage": "answer_generation_batch_eval", "summary": summary, "total_queries": summary["total_queries"], "answered_count": summary["answered_count"], "fallback_count": summary["fallback_count"], "api_error_count": summary["api_error_count"], "cache_hit_count": summary["cache_hit_count"], "llm_call_count": summary["llm_call_count"], "validation_errors": validation_errors, "results": results, "status": "failed" if validation_errors else "completed", } def _validate_expected_result(query: str, result: dict[str, Any]) -> list[str]: expected = EXPECTED_RESULTS.get(query) if not expected: return [] errors: list[str] = [] for field, expected_value in expected.items(): actual_value = result.get(field) if actual_value != expected_value: errors.append( f"{query}: expected {field}={expected_value!r}, got {actual_value!r}" ) return errors def _build_unhandled_error_result(query: str, exc: Exception) -> dict[str, Any]: return { "query": query, "answer": "Mình gặp lỗi ngoài dự kiến khi xử lý câu hỏi này, nhưng batch test vẫn tiếp tục chạy.", "status": "fallback", "error_type": "unknown", "error_message": str(exc), "intent": None, "strategy": None, "retrieval_query": None, "citations": [], "citations_used": [], "structured_result": None, "tool_result": None, "llm_called": False, "used_cache": False, "clarification_needed": False, "context_used": "", } if __name__ == "__main__": main()