grantforge-api / backend /agents /evaluator.py
GrantForge Bot
Deploy to Hugging Face
afd56bc
from pydantic import BaseModel, Field
from typing import Literal
from core.llm_router import get_llm
from langchain_core.prompts import PromptTemplate
from rag_pipeline import get_hybrid_retriever, rerank_documents
import logging
from tenacity import retry, stop_after_attempt, wait_exponential
logger = logging.getLogger(__name__)
class ExpenseEvaluationResponse(BaseModel):
czy_wydatek_kwalifikowalny: bool = Field(
description="Zwr贸膰 True je艣li wydatek jest w 100% zgodny z regulaminem i wytycznymi programu (kwalifikowalny)."
)
uzasadnienie_prawne: str = Field(
description="Cytat lub konkretne odwo艂anie do regulaminu uzasadniaj膮ce kwalifikowalno艣膰 lub jej brak."
)
kategoria_badan: Literal[
"badania przemys艂owe",
"prace rozwojowe",
"prace przedwdro偶eniowe",
"brak/nie dotyczy",
] = Field(
description="Wybierz do jakiej kategorii zgodnie z polskim/unijnym prawem nale偶y ten wydatek. Wybierz 'brak/nie dotyczy' tylko je艣li wydatek jest ca艂kowicie poza B+R."
)
intensywnosc_pomocy: float = Field(
description="Zwr贸膰 w formie warto艣ci zmiennoprzecinkowej np. 0.50 (co oznacza 50%), 0.80 (co oznacza 80%) bazuj膮c na wielko艣ci firmy i rodzaju bada艅. 0.0 oznacza wydatek niekwalifikowalny."
)
def evaluate_project_expense(
expense_description: str,
expense_amount: float,
project_title: str,
program_name: str,
company_size: str,
tenant_id: str = None,
) -> ExpenseEvaluationResponse:
"""
Agent ds. Oceny Kwalifikowalno艣ci (FAZA 4).
Wymusza twarde, ustrukturyzowane ramy JSON za pomoc膮 Pydantic.
Opiera si臋 na wiedzy RAG dotycz膮cej wybranego programu.
"""
# Pr贸ba za艂adowania kontekstu z RAG - Hard Filtering na aktualn膮 perspektyw臋
# Domy艣lnie wyszukujemy tylko w najnowszej perspektywie (FAZA 3, zapobieganie aplikacji starych przepis贸w)
hard_filter = {"rok_perspektywy": {"$eq": "2021-2027"}}
if program_name:
# Operator $and dla Pinecone Vector Store
hard_filter = {
"$and": [
{"program_name": {"$eq": program_name}},
{"rok_perspektywy": {"$eq": "2021-2027"}},
]
}
context_text = "Brak specyficznego regulaminu programu w bazie."
try:
retriever = get_hybrid_retriever(
k=10, metadata_filter=hard_filter, namespace=tenant_id
)
if retriever:
query_for_rag = f"kwalifikowalno艣膰 wydatku badania kategoria intensywno艣膰 dotacji pomoc publiczna: {expense_description}"
docs = retriever.invoke(query_for_rag)
reranked_docs = rerank_documents(query_for_rag, docs, top_n=4)
context_text = "\n\n".join(
[
f"[殴R脫D艁O: {d.metadata.get('source', 'Brak')}]: {d.page_content}"
for d in reranked_docs
]
)
except Exception as e:
logger.error(f"[ExpenseEvaluator] Error fetching RAG context: {str(e)}")
template = """
Jeste艣 G艂贸wnym Prawnikiem i Audytorem Dotacyjnym oceniaj膮cym kwalifikowalno艣膰 wydatk贸w.
Oceniasz pojedynczy wydatek dla projektu w ramach programu: {program_name}.
Wielko艣膰 przedsi臋biorstwa wnioskodawcy: {company_size}.
Opis wydatku do weryfikacji:
"{expense_description}" (Kwota: {expense_amount} PLN)
Kontekst z regulamin贸w z bazy wiedzy:
--------------------------------------------------
{context}
--------------------------------------------------
Zasady:
1. Przeanalizuj czy podany wydatek kwalifikuje si臋 do obj臋cia wsparciem zgodnie z baz膮 wiedzy.
2. Okre艣l kategori臋 bada艅 dla wydatku, zgodnie z definicjami (badania przemys艂owe, prace rozwojowe, przedwdro偶eniowe).
3. Je艣li wydatek jest kwalifikowalny, przypisz prawid艂ow膮 intensywno艣膰 pomocy (zazwyczaj mniejszy procent dla prac rozwojowych/du偶ych firm, wi臋kszy dla bada艅 przemys艂owych/M艢P).
4. Podaj bardzo precyzyjne uzasadnienie prawne odnosz膮ce si臋 do regulaminu.
"""
prompt = PromptTemplate.from_template(template)
# LLM z typowaniem - GPT-4o jest du偶o lepszy do takich zada艅 analitycznych
structured_llm = get_llm(
task_type="legal_audit", structured_output_schema=ExpenseEvaluationResponse
)
chain = prompt | structured_llm
@retry(
stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10)
)
def _invoke_chain():
return chain.invoke(
{
"program_name": program_name or "Og贸lne zasady dotacji B+R",
"company_size": company_size or "M艢P (nieokre艣lona wielko艣膰)",
"expense_description": expense_description,
"expense_amount": expense_amount,
"context": context_text,
}
)
result = _invoke_chain()
return result