# ruff: noqa: E501 """Full pipeline end-to-end test with real Ollama LLM. Tests every agent node in the LangGraph search pipeline.""" from __future__ import annotations import asyncio import logging import sys from pathlib import Path # Ensure project root is on path sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s") logger = logging.getLogger("e2e-test") errors: list[str] = [] successes: list[str] = [] def check(step: str, ok: bool, detail: str = ""): if ok: successes.append(step) logger.info(f" PASS [{step}] {detail}") else: errors.append(step) logger.error(f" FAIL [{step}] {detail}") import traceback # noqa: E402 async def run_tests(): from src.core.models import MatchResult, MatchScores def _make_match_result(pid: str, name: str, title: str) -> MatchResult: """Build a minimal MatchResult for testing.""" return MatchResult( query_id="e2e-test", rank=0, profile_id=pid, name=name, current_title=title, current_company="", matched_skills=[], missing_skills=[], scores=MatchScores( semantic_similarity=0.5, keyword_match=0.5, skill_match=0.5, experience_match=0.5, location_match=0.5, overall=0.5, confidence=0.5, ), ) # ========================================================== # STEP 1: Config & LLM connectivity # ========================================================== logger.info("=" * 60) logger.info("STEP 1: Config & LLM Connectivity") logger.info("=" * 60) from src.core.config import get_llm_client, get_settings settings = get_settings() check("config-loads", settings.llm_provider == "ollama", f"provider={settings.llm_provider}") client = get_llm_client() try: resp = await client.ainvoke("Say exactly: HELLO OLLAMA") text = resp.content if hasattr(resp, 'content') else str(resp) check("ollama-connect", "HELLO OLLAMA" in text.upper(), f"response: {text[:100]}") except Exception as e: check("ollama-connect", False, str(e)) # ========================================================== # STEP 2: Profile Store & Index Loading # ========================================================== logger.info("=" * 60) logger.info("STEP 2: Profile Store & Index Loading") logger.info("=" * 60) from src.core.config import DATA_DIR from src.core.profile_store import ProfileStore from src.matching.scorer import DEFAULT_SLIDER_WEIGHTS, CandidateScorer store = ProfileStore() check("profile-store-loaded", len(store) > 0, f"profiles={len(store)}") # Fetch a sample profile using get_all_sample samples = store.get_all_sample() check("sample-profiles-loaded", len(samples) > 0, f"count={len(samples)}") sample = next(iter(samples.values())) if samples else None check("profile-fetch", sample is not None, f"first profile id={sample.profile_id if sample else 'N/A'}") # Load indexes from src.language.multilingual import MultilingualEmbedder from src.search.bm25_search import BM25Search from src.search.hybrid import HybridSearch from src.search.reranker import CrossEncoderReranker from src.search.vector_search import VectorSearch index_dir = DATA_DIR / "indexes" faiss_path = index_dir / "faiss_index.bin" bm25_path = index_dir / "bm25_index.pkl" id_map_path = index_dir / "faiss_id_map.json" check("faiss-exists", faiss_path.exists(), str(faiss_path)) check("bm25-exists", bm25_path.exists(), str(bm25_path)) vector_search = VectorSearch(dimension=384) try: vector_search.load(faiss_path, id_map_path) check("faiss-load", vector_search.size > 0, f"vectors={vector_search.size}") except Exception as e: check("faiss-load", False, str(e)) bm25 = BM25Search() try: bm25.load(bm25_path) check("bm25-load", bm25.size > 0, f"docs={bm25.size}") except Exception as e: check("bm25-load", False, str(e)) logger.info("Loading embedding model (may take ~30s on first run)...") embedder = MultilingualEmbedder() hybrid = HybridSearch(vector_search, bm25, embedder) reranker = CrossEncoderReranker(timeout_ms=5000) # Test search try: results = hybrid.search("python developer", top_k=5) check("hybrid-search", len(results) > 0, f"results={len(results)}") except Exception as e: check("hybrid-search", False, str(e)) # Test empty search try: empty_results = hybrid.search("zzzzzzzzzznonexistent", top_k=5) check("hybrid-empty-search", len(empty_results) > 0, "empty query still returns results as fallback") except Exception as e: check("hybrid-empty-search", False, str(e)) # ========================================================== # STEP 3: Create executor with dependencies # ========================================================== logger.info("=" * 60) logger.info("STEP 3: Create agent dependencies") logger.info("=" * 60) scorer = CandidateScorer() from src.agents.executor import ExecutorAgent executor = ExecutorAgent(hybrid, reranker, scorer, store) from src.agents.planner import PlannerAgent planner = PlannerAgent() from src.agents.reflector import ReflectorAgent reflector = ReflectorAgent() # ========================================================== # STEP 4: Planner Agent (async) # ========================================================== logger.info("=" * 60) logger.info("STEP 4: Planner Agent") logger.info("=" * 60) try: query = "senior DevOps engineer with 5+ years AWS experience in Bangalore" plan = await planner.plan(query) check("planner-plan", plan is not None, f"plan type={type(plan).__name__ if plan else 'None'}") if plan: req_skills = plan.required_skills check("planner-has-required-skills", len(req_skills) > 0, f"required_skills={[str(s) for s in req_skills[:5]]}") check("planner-has-experience", plan.experience is not None, f"exp={plan.experience}") except Exception as e: check("planner-plan", False, f"Exception: {e}") traceback.print_exc() # ========================================================== # STEP 5: Executor Agent (async) # ========================================================== logger.info("=" * 60) logger.info("STEP 5: Executor Agent") logger.info("=" * 60) try: exec_results = await executor.execute(plan, top_k=5) check("executor-execute", exec_results is not None, f"type={type(exec_results).__name__}") if exec_results and isinstance(exec_results, list): check("executor-has-results", len(exec_results) > 0, f"count={len(exec_results)}") if exec_results: r = exec_results[0] check("executor-result-has-score", hasattr(r, 'scores') and r.scores is not None, f"scores={r.scores}") except Exception as e: check("executor-execute", False, f"Exception: {e}") traceback.print_exc() # ========================================================== # STEP 6: Scorer with slider weights # ========================================================== logger.info("=" * 60) logger.info("STEP 6: Scorer with Slider Weights") logger.info("=" * 60) slider_weights = { "skill_match": 80, "experience_match": 70, "education_match": 50, "assessment_score": 40, "behavioral_signals": 30, "cultural_fit": 20, } try: scores = scorer.compute_overall( { "semantic_similarity": 0.85, "keyword_match": 0.75, "skill_match": 0.90, "experience_match": 0.80, "location_match": 0.60, "education_match": 0.70, "cross_encoder_score": 0.50, "behavioral_signals": 0.65, "cultural_fit": 0.55, }, slider_weights=slider_weights, ) check("scorer-overall", scores.overall > 0, f"overall={scores.overall:.3f}") check("scorer-confidence", scores.confidence > 0, f"confidence={scores.confidence:.3f}") except Exception as e: check("scorer-overall", False, f"Exception: {e}") traceback.print_exc() # ========================================================== # STEP 7: Reflector Agent # ========================================================== logger.info("=" * 60) logger.info("STEP 7: Reflector Agent") logger.info("=" * 60) # Build a MatchResult list for the reflector match_results = [] if exec_results and isinstance(exec_results, list): for mr in exec_results: match_results.append(mr) try: eval_results = await reflector.reflect(plan, match_results) check("reflector-evaluate", eval_results is not None, f"type={type(eval_results).__name__ if eval_results else 'None'}") if eval_results and isinstance(eval_results, dict): check("reflector-has-feedback", "feedback" in eval_results or "evaluations" in eval_results, f"keys={list(eval_results.keys())}") except Exception as e: check("reflector-evaluate", False, f"Exception: {e}") traceback.print_exc() # ========================================================== # STEP 8: Rationale Generator # ========================================================== logger.info("=" * 60) logger.info("STEP 8: Rationale Generator") logger.info("=" * 60) from src.rationale.generator import RationaleGenerator gen = RationaleGenerator() try: # Use template rationale with a sample match result if exec_results and isinstance(exec_results, list) and len(exec_results) > 0: mr = exec_results[0] profile = store.get(mr.profile_id) if profile: rationale = gen._template_rationale(mr, profile) check("rationale-template-fallback", rationale is not None and len(rationale.summary) > 0, f"summary_len={len(rationale.summary) if rationale else 0}") else: check("rationale-template-fallback", True, "skipped (no profile)") else: check("rationale-template-fallback", True, "skipped (no results)") except Exception as e: check("rationale-template-fallback", False, str(e)) traceback.print_exc() # ========================================================== # STEP 9: Full Orchestrator # ========================================================== logger.info("=" * 60) logger.info("STEP 9: Full Orchestrator (LangGraph)") logger.info("=" * 60) from src.agents.orchestrator import Orchestrator orchestrator = Orchestrator(planner, executor, reflector) try: result = await orchestrator.run( raw_query="senior software engineer Python AWS", slider_weights=DEFAULT_SLIDER_WEIGHTS, ) check("orchestrator-run", result is not None, f"type={type(result).__name__ if result else 'None'}") if result: from src.core.models import SearchResponse if isinstance(result, SearchResponse): results_arr = result.results check("orchestrator-has-results", len(results_arr) > 0, f"count={len(results_arr)}") search_metadata = result.search_metadata check("orchestrator-has-metadata", search_metadata is not None, f"metadata={search_metadata}") if search_metadata: check("orchestrator-processing-time", search_metadata.total_time_ms > 0, f"time={search_metadata.total_time_ms}ms") check("orchestrator-listwise", search_metadata.listwise_ranked is not None, f"listwise_ranked={search_metadata.listwise_ranked}") except Exception as e: check("orchestrator-run", False, f"Exception: {e}") traceback.print_exc() # ========================================================== # STEP 10: Fairness Components # ========================================================== logger.info("=" * 60) logger.info("STEP 10: Fairness Components") logger.info("=" * 60) from src.fairness.anonymizer import anonymize_text_for_bias, style_anonymize from src.fairness.bias_detector import BiasDetector try: pii_text = "John Doe worked at Google in Bangalore. Email: johndoe@email.com" anon_text = anonymize_text_for_bias(pii_text) check("pii-anonymize-text", "[NAME]" in anon_text, f"result: {anon_text[:80]}") except Exception as e: check("pii-anonymize-text", False, str(e)) try: styled = style_anonymize("I spearheaded the devops transformation and leveraged cutting-edge tech") check("style-anonymize", "spearheaded" not in styled and "leveraged" not in styled, f"result: {styled[:80]}") except Exception as e: check("style-anonymize", False, str(e)) detector = BiasDetector() try: if samples: sample = next(iter(samples.values())) mr = _make_match_result( pid=sample.profile_id, name=sample.personal.name if sample.personal else sample.profile_id, title=sample.professional.current_title if sample.professional else "", ) profiles_dict = {sample.profile_id: sample} bias_result = detector.detect_bias( matches=[mr], profiles=profiles_dict, bias_type="name", ) check("bias-detector", isinstance(bias_result, dict), f"result={bias_result}") except Exception as e: check("bias-detector", False, str(e)) # ========================================================== # STEP 11: Listwise Ranking # ========================================================== logger.info("=" * 60) logger.info("STEP 11: Plackett-Luce Listwise Ranking") logger.info("=" * 60) from src.ranking.listwise_ranker import PlackettLuceRanker ranker = PlackettLuceRanker() try: # Build MatchResult objects from sample profiles results_for_rank: list[MatchResult] = [] if samples: for pid, prof in list(samples.items())[:10]: mr = _make_match_result( pid=pid, name=prof.personal.name if prof.personal else pid, title=prof.professional.current_title if prof.professional else "", ) results_for_rank.append(mr) if results_for_rank: ranked = await ranker.arank(results_for_rank) check("listwise-rank", ranked is not None and len(ranked) > 0, f"count={len(ranked) if ranked else 0}") except Exception as e: check("listwise-rank", False, f"Exception: {e}") traceback.print_exc() # ========================================================== # STEP 12: Evaluation Metrics # ========================================================== logger.info("=" * 60) logger.info("STEP 12: Evaluation Metrics") logger.info("=" * 60) from src.evaluation.metrics import mean_reciprocal_rank, ndcg_at_k, precision_at_k, recall_at_k try: retrieved = ["a", "b", "c", "d", "e"] relevant = {"a", "c", "f"} p_at_3 = precision_at_k(retrieved, relevant, 3) r_at_5 = recall_at_k(retrieved, relevant, 5) mrr = mean_reciprocal_rank(retrieved, relevant) ndcg = ndcg_at_k(retrieved, relevant, 5) check("eval-precision", 0.0 <= p_at_3 <= 1.0, f"p@3={p_at_3:.3f}") check("eval-recall", 0.0 <= r_at_5 <= 1.0, f"r@5={r_at_5:.3f}") check("eval-mrr", 0.0 <= mrr <= 1.0, f"mrr={mrr:.3f}") check("eval-ndcg", 0.0 <= ndcg <= 1.0, f"ndcg={ndcg:.3f}") except Exception as e: check("eval-metrics", False, str(e)) # ========================================================== # STEP 13: Hinglish query # ========================================================== logger.info("=" * 60) logger.info("STEP 13: Hinglish Query") logger.info("=" * 60) from src.language.code_mixed import CodeMixedProcessor cm_processor = CodeMixedProcessor() try: hinglish_query = "Mujhe ek senior Python developer chahiye with 5 years experience" lang_result = cm_processor.detect_code_mixed(hinglish_query) check("hinglish-detect", lang_result is not None, f"result={lang_result}") except Exception as e: check("hinglish-detect", False, str(e)) try: hinglish_plan = await planner.plan(hinglish_query) check("hinglish-plan", hinglish_plan is not None, f"plan type={type(hinglish_plan).__name__ if hinglish_plan else 'None'}") except Exception as e: check("hinglish-plan", False, str(e)) # ========================================================== # SUMMARY # ========================================================== logger.info("=" * 60) logger.info("SUMMARY") logger.info("=" * 60) logger.info(f"Total steps: {len(successes) + len(errors)}") logger.info(f"Passed: {len(successes)}") logger.info(f"Failed: {len(errors)}") if errors: logger.error("FAILED STEPS:") for e in errors: logger.error(f" - {e}") return 1 else: logger.info("ALL STEPS PASSED!") return 0 if __name__ == "__main__": exit_code = asyncio.run(run_tests()) sys.exit(exit_code)