"""Agent graph node logic. Pure module: no FastAPI imports and no HTTPException. LLM client/config lives in llm_setup — nodes call llm_call only. """ from __future__ import annotations import logging from typing import Literal from langgraph.graph import END from agent.decorators import normalize_job_posts, parse_match_response from agent.models import AgentState from agent.prompt import prompt, user_prompt from llm_setup import llm_call logger = logging.getLogger("agent") async def prepare_context(state: AgentState) -> dict: """Validate inputs and decide whether matching should run.""" subject = (state.get("subject") or "").strip() resume_text = (state.get("resume_text") or "").strip() job_posts = normalize_job_posts(state.get("job_posts")) if not resume_text: return { "status": "skipped", "error": "resume_text is empty", "suggested_job_post_ids": [], "summary": "", "reasoning": "", } if not job_posts: return { "status": "skipped", "error": "no active job posts to match against", "suggested_job_post_ids": [], "summary": "", "reasoning": "", } return { "subject": subject, "resume_text": resume_text, "job_posts": job_posts, "status": "ready", "error": "", } def route_after_prepare(state: AgentState) -> Literal["match_jobs", "__end__"]: if state.get("status") == "ready": return "match_jobs" return END async def match_jobs(state: AgentState) -> dict: """Ask the LLM (via llm_setup.llm_call) to map the candidate to job posts.""" try: data = await llm_call(prompt(), user_prompt(state), json_mode=True) allowed_ids = {item["id"] for item in state.get("job_posts") or []} suggested, summary, reasoning = parse_match_response(data, allowed_ids) return { "status": "matched", "suggested_job_post_ids": suggested, "summary": summary, "reasoning": reasoning, } except Exception as exc: logger.exception("agent match_jobs failed") return { "status": "failed", "error": str(exc), "suggested_job_post_ids": [], "summary": "", "reasoning": "", } async def finalize(state: AgentState) -> dict: """Normalize terminal state for callers.""" return { "suggested_job_post_ids": state.get("suggested_job_post_ids") or [], "summary": state.get("summary") or "", "reasoning": state.get("reasoning") or "", "status": state.get("status") or "failed", "error": state.get("error") or "", }