# ruff: noqa: E402 import logging from typing import Dict, Any from langchain_core.messages import SystemMessage, HumanMessage from schemas import AgentState logger = logging.getLogger(__name__) from schemas import MatchOutput from core.llm_router import get_llm def matcher_node(state: Any) -> Dict[str, Any]: """ Węzeł sprawdzający dopasowanie każdego z wyszukanych naborów do profilu firmy. Używa lokalnego modelu LLM do wyliczenia "relevance_score" oraz poetyckiego podsumowania "poetic_match". """ try: if isinstance(state, dict): state = AgentState(**state) except Exception as e: logger.error(f"Błąd inicjalizacji stanu w matcher_node: {e}") return {"current_agent": "supervisor"} profile = state.profile grants = state.eligible_grants if not profile or not grants: return { "current_agent": "supervisor" } # Jeśli brak danych, przekaż do supervisora # Budujemy prosty opis firmy jako ciąg tekstowy company_desc = f"Firma {profile.nip}, branża: {', '.join(profile.pkd_codes)}, wielkość: {profile.size}. " if profile.innovation_focus: company_desc += ( f"Skupienie na innowacjach: {', '.join(profile.innovation_focus)}. " ) if profile.investment_plans: plans = ", ".join([p.description for p in profile.investment_plans]) company_desc += f"Plany inwestycyjne: {plans}." logger.info(f"Matcher uruchomiony dla {len(grants)} znalezisk dla {profile.nip}") from agents.helpers import ANTI_HALLUCINATION_PROMPT system_prompt = ( ANTI_HALLUCINATION_PROMPT + "\n\n" "Jesteś analitykiem ds. dotacji o zacięciu wirtualnego poety funduszowego. " "Twoim zadaniem jest ocena dopasowania dotacji do profilu firmy. " "Dla każdej podanej dotacji i firmy oceń procentowe dopasowanie (relevance_score od 0 do 1), " "napisz jedno zwięzłe zdanie (poetic_match) po polsku opisujące to dopasowanie oraz podaj wyjaśnienie (explanation), " "które logicznie uzasadnia tę ocenę: krótki powód (reason), kluczowe wspierające kryteria (criteria) " "oraz ewentualne ryzyka/twarde warunki (risks). Odpowiadaj ZAWSZE I WYŁĄCZNIE w języku polskim." ) updated_grants = [] # Przesiewamy granty, by oceniać tylko te brakujące grants_to_eval = [] for grant in grants: if grant.poetic_match and grant.relevance_score > 0 and grant.explanation: updated_grants.append(grant) else: grants_to_eval.append(grant) if not grants_to_eval: return {"eligible_grants": updated_grants, "current_agent": "supervisor"} system_msgs = [SystemMessage(content=system_prompt) for _ in grants_to_eval] human_msgs = [ HumanMessage( content=( f"Profil Firmy: {company_desc}\n" f"Nabór: {grant.title}\n" f"Instytucja: {grant.institution}\n" f"Opis Naboru: {grant.description}\n\n" "Zwróć wynik dopasowania uwzględniając strukturę MatchOutput." ) ) for grant in grants_to_eval ] messages_batch = [[s, h] for s, h in zip(system_msgs, human_msgs)] try: llm = get_llm(task_type="standard", structured_output_schema=MatchOutput) # Równoległe wywołanie wszystkich ewaluacji naraz results = llm.batch(messages_batch, config={"max_concurrency": 3}) for grant, parsed in zip(grants_to_eval, results): if isinstance(parsed, dict): grant.relevance_score = parsed.get("relevance_score", 0.0) grant.poetic_match = parsed.get("poetic_match", "") grant.explanation = parsed.get("explanation") else: grant.relevance_score = getattr(parsed, "relevance_score", 0.0) grant.poetic_match = getattr(parsed, "poetic_match", "") if getattr(parsed, "explanation", None): grant.explanation = parsed.explanation.dict() if hasattr(parsed.explanation, "dict") else parsed.explanation updated_grants.append(grant) except Exception as e: logger.error(f"Błąd LLM w operacji matcher_node (wieloprocesowej): {e}. Próba wykonania sekwencyjnego...") # Awaryjnie wykonaj zapytania sekwencyjnie for i, grant in enumerate(grants_to_eval): try: parsed = llm.invoke(messages_batch[i]) if isinstance(parsed, dict): grant.relevance_score = parsed.get("relevance_score", 0.0) grant.poetic_match = parsed.get("poetic_match", "") grant.explanation = parsed.get("explanation") else: grant.relevance_score = getattr(parsed, "relevance_score", 0.0) grant.poetic_match = getattr(parsed, "poetic_match", "") if getattr(parsed, "explanation", None): grant.explanation = parsed.explanation.dict() if hasattr(parsed.explanation, "dict") else parsed.explanation updated_grants.append(grant) except Exception as seq_err: logger.error(f"Błąd sekwencyjny dla grantu {grant.title}: {seq_err}") grant.relevance_score = 0.0 grant.poetic_match = "Błąd dopasowania." grant.explanation = { "reason": "Błąd LLM (fallback sekwencyjny)", "criteria": [], "risks": "Nie udało się zweryfikować", } updated_grants.append(grant) return {"eligible_grants": updated_grants, "current_agent": "supervisor"}