grantforge-api / backend /agents /matcher.py
GrantForge Bot
Deploy to Hugging Face
3b7f713
# 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"}