Files
B.Tech-Project-III/negot8/backend/agents/matching_agent.py
2026-04-05 00:43:23 +05:30

112 lines
3.5 KiB
Python

"""
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": <integer 0-100>,
"match_reasoning": "<one concise sentence explaining the score>",
"key_alignments": ["<thing 1 that matches well>", "<thing 2>"],
"key_gaps": ["<gap 1>", "<gap 2>"]
}}
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
)