Spaces:
Running
Running
| from fastapi import APIRouter, Depends, HTTPException | |
| from core.search.grant_search_service import grant_search_service | |
| import asyncio | |
| import json | |
| from pydantic import BaseModel, Field | |
| from endpoints.projects import get_db | |
| from core.projects.models import Project | |
| from core.subscription.middleware import verify_token | |
| from core.llm_router import get_llm | |
| from langchain_core.prompts import PromptTemplate | |
| from typing import List | |
| import logging | |
| logger = logging.getLogger(__name__) | |
| router = APIRouter(prefix="/api/grants", tags=["grants"]) | |
| async def get_nabory(force_refresh: bool = False): | |
| """ | |
| Pobiera list臋 aktualnych nabor贸w dla program贸w z PARP i NCBR. | |
| 艁膮czy wyniki z obu instytucji w jedn膮 ujednolicon膮 list臋. | |
| Dodatkowo weryfikuje baz臋 prawn膮 przez integracj臋 z EUR-Lex. | |
| """ | |
| try: | |
| nabory = await grant_search_service.get_all_grants(force_refresh=force_refresh) | |
| except Exception as e: | |
| logger.error(f"Error fetching grants: {e}") | |
| nabory = [] | |
| # Sortuj rosn膮co po dacie deadline (najbli偶sze najpierw) | |
| # deadline mo偶e by膰 w formacie YYYY-MM-DD | |
| def get_deadline(item): | |
| d = item.get("deadline") | |
| return d if d else "9999-12-31" | |
| nabory.sort(key=get_deadline) | |
| # Wyczy艣膰 sztuczny deadline przed wys艂aniem na frontend | |
| for item in nabory: | |
| if item.get("deadline") == "9999-12-31": | |
| item["deadline"] = "" | |
| return { | |
| "status": "ok", | |
| "count": len(nabory), | |
| "nabory": nabory | |
| } | |
| class GrantMatchResult(BaseModel): | |
| program_id: str = Field(description="Identyfikator naboru z dostarczonej listy") | |
| program_name: str = Field(description="Nazwa programu") | |
| score: int = Field(description="Ocena dopasowania w skali 0-100 na podstawie opisu projektu") | |
| rationale: str = Field(description="Kr贸tkie uzasadnienie (do 3 zda艅) dlaczego program pasuje lub nie pasuje do projektu") | |
| is_recommended: bool = Field(description="Czy program jest rekomendowany (przyznaj True je艣li score >= 70)") | |
| requires_verification: bool = Field(default=False, description="Ustaw na True, je艣li wniosek wymaga weryfikacji manualnej, np. z powodu niepewnego statusu M艢P, s艂abego wyniku (score < 85) lub niepewnej kwalifikowalno艣ci.") | |
| source: str = Field(default="N/A", description="殴r贸d艂o informacji o naborze (np. parp_verified_fallback), przepisz z dostarczonych wytycznych.") | |
| legal_basis: str = Field(default="N/A", description="Podstawa prawna / regulamin naboru, przepisz z dostarczonych wytycznych.") | |
| confidence_score: int = Field(default=0, description="Ocena pewno艣ci sztucznej inteligencji co do tego dopasowania (0-100).") | |
| class AdvancedMatchResponseData(BaseModel): | |
| needs_more_info: bool = Field(description="True, je艣li brakuje kluczowych informacji do rzetelnej oceny dopasowania (np. status M艢P, obszar B+R, itp.)") | |
| clarifying_questions: List[str] = Field(description="Maksymalnie 3 najwa偶niejsze pytania doprecyzowuj膮ce do u偶ytkownika (je艣li needs_more_info to True)") | |
| matches: List[GrantMatchResult] = Field(description="Lista dopasowa艅 (je艣li needs_more_info to False)") | |
| class UserAnswer(BaseModel): | |
| question: str | |
| answer: str | |
| class MatchRequest(BaseModel): | |
| project_id: str | |
| user_answers: List[UserAnswer] = [] | |
| class SearchRequest(BaseModel): | |
| query: str | |
| filters: dict = {} | |
| async def search_grants_api(request: SearchRequest): | |
| """ | |
| Wyszukuje nabory na podstawie zapytania tekstowego i filtr贸w (u偶ywaj膮c nowego Search Engine). | |
| """ | |
| try: | |
| results = await grant_search_service.search_grants(request.query, request.filters) | |
| return {"status": "ok", "count": len(results), "grants": results} | |
| except Exception as e: | |
| logger.error(f"Error in search_grants_api: {e}") | |
| raise HTTPException(status_code=500, detail="B艂膮d wyszukiwania nabor贸w") | |
| async def match_grants( | |
| request: MatchRequest, | |
| token_data: dict = Depends(verify_token), | |
| db = Depends(get_db) | |
| ): | |
| """ | |
| AI-driven zaawansowane dopasowanie program贸w dotacyjnych dla projektu. | |
| Dzia艂a wieloetapowo: dopytuje o szczeg贸艂y lub zwraca list臋 rekomendacji. | |
| """ | |
| clerk_id = token_data.get("sub") | |
| project = db.query(Project).filter(Project.id == request.project_id, Project.clerk_user_id == clerk_id).first() | |
| if not project: | |
| raise HTTPException(status_code=404, detail="Projekt nie istnieje lub brak dost臋pu") | |
| try: | |
| # Pobieramy najnowsze nabory | |
| nabory_resp = await get_nabory(force_refresh=False) | |
| nabory_list = nabory_resp.get("nabory", []) | |
| if not nabory_list: | |
| return {"status": "ok", "project_id": request.project_id, "needs_more_info": False, "clarifying_questions": [], "matches": []} | |
| # 1. Weryfikacja M艢P (GraphRAG / Neo4j) | |
| nip = project.external_context.get("nip", "") if project.external_context else "" | |
| sme_data = None | |
| if nip: | |
| try: | |
| from core.graph_rag.sme_verifier import sme_verifier | |
| declared_status = project.external_context.get("declared_sme_status", "mikro") | |
| sme_data = sme_verifier.verify_sme_status(nip=nip, declared_status=declared_status) | |
| except Exception as e: | |
| logger.warning(f"SME verifier error: {e}") | |
| sme_context = "Brak zweryfikowanego statusu M艢P (GraphRAG nie zosta艂 wykonany)." | |
| if sme_data: | |
| sme_context = ( | |
| f"Zadeklarowany NIP: {nip}\n" | |
| f"Wyliczony status z powi膮za艅 (GraphRAG): {sme_data.get('calculated_status')}\n" | |
| f"Czy status poprawny?: {sme_data.get('is_status_valid')}\n" | |
| f"Uzasadnienie: {sme_data.get('reasoning')}" | |
| ) | |
| # 2. RAG Retrieval dla odpowiednich nabor贸w | |
| try: | |
| from rag_pipeline.vector_store import get_parent_document_retriever | |
| retriever = get_parent_document_retriever(namespace="grants_guidelines") | |
| except Exception as e: | |
| logger.warning(f"RAG init error: {e}") | |
| retriever = None | |
| rag_context_docs = [] | |
| if retriever: | |
| query_text = f"{project.title} {project.description}" | |
| try: | |
| rag_context_docs = retriever.invoke(query_text) | |
| except Exception as e: | |
| logger.warning(f"RAG retrieval error: {e}") | |
| nabory_context = "Brak szczeg贸艂贸w z RAG." | |
| if rag_context_docs: | |
| docs_text = [f"--- DOKUMENT ---\n{doc.page_content}" for doc in rag_context_docs] | |
| nabory_context = "\n".join(docs_text) | |
| else: | |
| simplified_nabory = [] | |
| for n in nabory_list: | |
| desc = n.get('description', '')[:300] if n.get('description') else "Brak szczeg贸艂贸w" | |
| src = n.get('source', 'N/A') | |
| legal = n.get('legal_source', 'N/A') | |
| prog = f"{n.get('program', '')} - {n.get('name', 'N/A')}" | |
| simplified_nabory.append(f"- ID: {n.get('id', 'N/A')} | Program: {prog} | 殴r贸d艂o: {src} | Podstawa prawna: {legal} | Cel/Opis: {desc}") | |
| nabory_context = "\n".join(simplified_nabory) | |
| answers_context = "Brak dodatkowych odpowiedzi." | |
| if request.user_answers: | |
| answers_context = "\n".join([f"Q: {qa.question}\nA: {qa.answer}" for qa in request.user_answers]) | |
| system_prompt = """Jeste艣 ekspertem ds. funduszy UE i krajowych (PARP, NCBR). | |
| Zgodnie z Zasad膮 1.2 (Prawda i Dok艂adno艣膰) oraz 4.3 (Status M艢P): Zawsze sprawdzaj kwalifikowalno艣膰 rygorystycznie. | |
| Je艣li cokolwiek wymaga weryfikacji, zg艂o艣 to i nie zak艂adaj optymistycznego scenariusza w ciemno. | |
| PROJEKT U呕YTKOWNIKA: | |
| Tytu艂: {title} | |
| Opis: {description} | |
| Warto艣膰: {value} | |
| Dodatkowy kontekst z systemu: {extra_context} | |
| WERYFIKACJA M艢P (GraphRAG): | |
| {sme_context} | |
| ODPOWIEDZI U呕YTKOWNIKA (DOPRECYZOWANIE): | |
| {answers_context} | |
| ZWR脫CONE PRZEZ RAG WYTYCZNE DOTYCZ膭CE NABOR脫W (Regulaminy): | |
| {nabory_context} | |
| INSTRUKCJA: | |
| 1. Twoim zadaniem jest zarekomendowanie najbardziej pasuj膮cych program贸w na podstawie dostarczonych WYTYCZNYCH Z RAG lub fallbacku. | |
| 2. We藕 pod uwag臋 wynik Weryfikacji M艢P. Je艣li status M艢P jest niezgodny z wymaganiami naboru, odrzu膰 program lub wyra藕nie to zaznacz. | |
| 3. TWARDA REGU艁A PKD: Je艣li w 'Dodatkowym kontek艣cie z systemu' widzisz wyekstrahowane kody PKD (np. 62.01.Z), musisz bezwzgl臋dnie dopasowa膰 do nich bran偶臋. Z g贸ry odrzucaj z rekomendacji programy, kt贸re przeznaczone s膮 dla innych bran偶 (np. rolnictwo, produkcja vs us艂ugi IT). Je艣li kody PKD s膮 wielobran偶owe (wskazuj膮 na r贸偶norodne dzia艂alno艣ci), popro艣 u偶ytkownika o wskazanie, kt贸ra z tych dzia艂alno艣ci b臋dzie g艂贸wnym przedmiotem projektu. | |
| 4. Obowi膮zkowo uzupe艂nij pole `source` i `legal_basis` na podstawie podanych danych (jako mechanizm anti-hallucination). Ustal r贸wnie偶 `confidence_score` oceniaj膮c stopie艅 dopasowania. Pami臋taj: Przy `confidence_score` (lub po prostu `score`) < 85%, obowi膮zkowo oznacz pole `requires_verification` jako True. | |
| 5. Je艣li z powy偶szych informacji NIE WYNIKAJ膭 kluczowe dla nabor贸w dane, ustaw `needs_more_info=True` i wygeneruj do 3 konkretnych pyta艅 w `clarifying_questions`. | |
| 6. Je艣li masz wystarczaj膮co du偶o informacji LUB u偶ytkownik ju偶 odpowiedzia艂 na pytania, ustaw `needs_more_info=False` i zwr贸膰 tablic臋 `matches`. Zignoruj ca艂kiem niepasuj膮ce programy. | |
| """ | |
| prompt = PromptTemplate.from_template(system_prompt) | |
| llm = get_llm(task_type="critical") | |
| chain = prompt | llm.with_structured_output(AdvancedMatchResponseData) | |
| result = chain.invoke({ | |
| "title": project.title, | |
| "description": project.description or "Brak opisu", | |
| "value": str(project.estimated_value) + " PLN" if project.estimated_value else "Nieznana", | |
| "extra_context": json.dumps(project.external_context, ensure_ascii=False) if project.external_context else "Brak", | |
| "answers_context": answers_context, | |
| "nabory_context": nabory_context, | |
| "sme_context": sme_context | |
| }) | |
| if not result.needs_more_info: | |
| for match in result.matches: | |
| if match.score < 85 or match.confidence_score < 85: | |
| match.requires_verification = True | |
| if not result.needs_more_info: | |
| external_context = project.external_context or {} | |
| external_context["ai_matches"] = [m.model_dump() for m in result.matches] | |
| if request.user_answers: | |
| external_context["ai_matches_qa_history"] = [qa.model_dump() for qa in request.user_answers] | |
| project.external_context = external_context | |
| db.commit() | |
| return { | |
| "status": "ok", | |
| "project_id": request.project_id, | |
| "needs_more_info": result.needs_more_info, | |
| "clarifying_questions": result.clarifying_questions, | |
| "matches": [m.model_dump() for m in result.matches] | |
| } | |
| except Exception as e: | |
| logger.error(f"Error in match_grants for project {request.project_id}: {e}", exc_info=True) | |
| return {"status": "error", "detail": f"B艂膮d generowania dopasowa艅 przez AI: {str(e)}", "needs_more_info": False, "clarifying_questions": [], "matches": []} | |