""" backend/agents/matching_agent.py Scores how well an applicant's preferences match an open contract's requirements using a single Gemini call. Returns a 0-100 score and a short explanation — used to rank applicants before the poster picks one. """ import json from agents.base_agent import BaseAgent _MATCHING_PROMPT = """You are a matching agent. Your job is to score how well an applicant's preferences align with an open contract's requirements. Contract type: {contract_type} CONTRACT REQUIREMENTS (what the poster needs): {requirements} APPLICANT PREFERENCES (what the applicant brings / wants): {preferences} Evaluate on these axes: 1. Core match — does the applicant fundamentally fit what the contract needs? 2. Budget/rate alignment — are financial expectations compatible? 3. Timeline/availability — do schedules overlap? 4. Skills/criteria — do they have what is required? 5. Flexibility potential — could a negotiation bridge remaining gaps? RESPOND ONLY with valid JSON in this exact format: {{ "match_score": , "match_reasoning": "", "key_alignments": ["", ""], "key_gaps": ["", ""] }} Scoring guide: 90-100 → Nearly perfect fit, minimal negotiation needed 70-89 → Good fit, small gaps bridgeable in negotiation 50-69 → Moderate fit, notable differences but workable 30-49 → Weak fit, significant gaps — negotiation will be hard 0-29 → Poor fit, fundamental incompatibility """ class MatchingAgent(BaseAgent): def __init__(self): super().__init__(system_prompt="You are a concise JSON-only matching agent.") async def score_applicant( self, contract_requirements: dict, applicant_preferences: dict, contract_type: str, ) -> dict: """ Score how well an applicant matches a contract. Returns: { "match_score": int (0-100), "match_reasoning": str, "key_alignments": list[str], "key_gaps": list[str], } On failure returns a safe default with score=0. """ prompt = _MATCHING_PROMPT.format( contract_type=contract_type, requirements=json.dumps(contract_requirements, indent=2), preferences=json.dumps(applicant_preferences, indent=2), ) try: # self.call() returns a dict — BaseAgent already handles JSON parsing result = await self.call(prompt) if "error" in result: raise ValueError(result["error"]) # Clamp score to 0-100 result["match_score"] = max(0, min(100, int(result.get("match_score", 0)))) return result except Exception as e: print(f"[MatchingAgent] score_applicant failed: {e}") return { "match_score": 0, "match_reasoning": "Could not compute score.", "key_alignments": [], "key_gaps": [], } # Module-level singleton _agent = None def get_matching_agent() -> MatchingAgent: global _agent if _agent is None: _agent = MatchingAgent() return _agent async def score_applicant( contract_requirements: dict, applicant_preferences: dict, contract_type: str, ) -> dict: """Convenience wrapper — use the module-level singleton.""" return await get_matching_agent().score_applicant( contract_requirements, applicant_preferences, contract_type )