Spaces:
Sleeping
Sleeping
| from __future__ import annotations | |
| import asyncio | |
| import json | |
| import logging | |
| from typing import Any | |
| from src.agents.prompts import REFLECTOR_SYSTEM_PROMPT | |
| from src.core.config import get_llm_client, get_settings | |
| from src.core.models import MatchResult, ParsedQuery | |
| logger = logging.getLogger(__name__) | |
| class ReflectorAgent: | |
| def __init__(self) -> None: | |
| self._client = None | |
| settings = get_settings() | |
| self.model = settings.openai_model | |
| def client(self): | |
| if self._client is None: | |
| try: | |
| self._client = get_llm_client() | |
| except Exception: | |
| logger.warning("LLM client unavailable for reflector") | |
| self._client = None | |
| return self._client | |
| async def reflect(self, query: ParsedQuery, results: list[MatchResult]) -> dict[str, Any]: | |
| try: | |
| if self.client is None: | |
| raise RuntimeError("LLM client unavailable") | |
| from langchain_core.messages import HumanMessage, SystemMessage | |
| candidates_text = json.dumps( | |
| [ | |
| { | |
| "profile_id": r.profile_id, | |
| "name": r.name, | |
| "title": r.current_title, | |
| "company": r.current_company, | |
| "overall_score": r.scores.overall, | |
| "confidence": r.scores.confidence, | |
| "matched_skills": r.matched_skills[:10], | |
| "missing_skills": r.missing_skills[:10], | |
| } | |
| for r in results[:20] | |
| ], | |
| indent=2, | |
| ) | |
| messages = [ | |
| SystemMessage(content=REFLECTOR_SYSTEM_PROMPT), | |
| HumanMessage(content=f"Candidates:\n{candidates_text}"), | |
| ] | |
| response = await asyncio.wait_for( | |
| self.client.ainvoke(messages), | |
| timeout=30.0, | |
| ) | |
| content = response.content if hasattr(response, "content") else str(response) | |
| if not content or not content.strip(): | |
| logger.warning("Reflector LLM returned empty content, using fallback") | |
| evaluations = self._fallback_evaluate(results) | |
| else: | |
| evaluations = json.loads(content) | |
| except TimeoutError: | |
| logger.warning("Reflector LLM timed out, using fallback") | |
| evaluations = self._fallback_evaluate(results) | |
| except Exception as e: | |
| logger.warning(f"Reflector LLM failed: {type(e).__name__}: {e}, using fallback") | |
| evaluations = self._fallback_evaluate(results) | |
| should_replan = self._should_replan(evaluations) | |
| good_matches = {"strong_match", "good_match"} | |
| good_count = sum( | |
| 1 for ev in (evaluations if isinstance(evaluations, list) else []) | |
| if isinstance(ev, dict) and ev.get("overall_assessment") in good_matches | |
| ) | |
| return { | |
| "evaluations": evaluations if isinstance(evaluations, list) else [], | |
| "good_match_count": good_count, | |
| "should_replan": should_replan, | |
| "feedback": self._generate_feedback(evaluations, results), | |
| } | |
| def _fallback_evaluate(self, results: list[MatchResult]) -> list[dict]: | |
| evaluations: list[dict] = [] | |
| for r in results[:20]: | |
| if r.scores.overall >= 0.8: | |
| assessment = "strong_match" | |
| elif r.scores.overall >= 0.6: | |
| assessment = "good_match" | |
| elif r.scores.overall >= 0.4: | |
| assessment = "potential_match" | |
| else: | |
| assessment = "weak_match" | |
| evaluations.append({ | |
| "profile_id": r.profile_id, | |
| "overall_assessment": assessment, | |
| "key_strengths": r.matched_skills[:5], | |
| "key_gaps": r.missing_skills[:5], | |
| "concerns": [], | |
| "should_keep": assessment in ("strong_match", "good_match", "potential_match"), | |
| }) | |
| return evaluations | |
| def _should_replan( # noqa: E501 | |
| self, evaluations: list[dict[str, Any]] | dict[str, Any], threshold: int = 3 | |
| ) -> bool: | |
| if isinstance(evaluations, list): | |
| good_matches = {"strong_match", "good_match"} | |
| good = sum( | |
| 1 for ev in evaluations | |
| if isinstance(ev, dict) and ev.get("overall_assessment") in good_matches | |
| ) | |
| return good < threshold | |
| return False | |
| def _generate_feedback( # noqa: E501 | |
| self, evaluations: list[dict[str, Any]] | dict[str, Any], results: list[MatchResult] | |
| ) -> str: | |
| if isinstance(evaluations, list): | |
| weak = [ | |
| ev for ev in evaluations | |
| if isinstance(ev, dict) and ev.get("overall_assessment") == "weak_match" | |
| ] | |
| if weak: | |
| return f"{len(weak)} weak matches found. Consider broadening criteria." | |
| if not results: | |
| return "No results found. Query may be too restrictive." | |
| return "Results look reasonable." | |