Spaces:
Running
Running
| """ | |
| Agencja Krytyka — Multi-Perspektywowy Audytor Wniosków. | |
| FAZA 4: Pydantic structured output z confidence_score + human_review_required. | |
| FAZA 5: Trzy role audytorów (Prawnik, Finansista, Innowator) → scalony wynik. | |
| Zgodność: AI Act Art. 13 (transparency), Art. 14 (human oversight). | |
| """ | |
| import logging | |
| from typing import List, Dict, Literal | |
| from pydantic import BaseModel, Field | |
| from core.llm_router import get_llm | |
| from core.audit_logger import audit_log | |
| from tenacity import retry, stop_after_attempt, wait_exponential | |
| logger = logging.getLogger(__name__) | |
| # ────────────────────────────────────────────────────────────────────────────── | |
| # Modele Pydantic (FAZA 4 — strukturyzowane wyjście) | |
| # ────────────────────────────────────────────────────────────────────────────── | |
| class AuditIssue(BaseModel): | |
| category: str = Field( | |
| description="Kategoria błędu, np. 'Budżet', 'Wykluczenia', 'DNSH', 'Spójność logiki'." | |
| ) | |
| severity: Literal["critical", "high", "medium", "low"] = Field( | |
| description="Powaga błędu." | |
| ) | |
| message: str = Field( | |
| description="Opis wskazanego błędu wraz ze zidentyfikowaną niespójnością." | |
| ) | |
| rule_citation: str = Field( | |
| default="", | |
| description="Cytat lub nazwa przywołanej reguły / paragrafu regulaminu.", | |
| ) | |
| recommendation: str = Field( | |
| default="", description="Rekomendacja: co i jak poprawić." | |
| ) | |
| affected_section: str = Field( | |
| default="", description="Tytuł sekcji wniosku, w której znaleziono błąd." | |
| ) | |
| problem_quote: str = Field( | |
| default="", description="Krótki cytat problematycznego zdania z wniosku." | |
| ) | |
| perspective: str = Field( | |
| default="generalny", | |
| description="Rola audytora, który znalazł błąd (prawnik/finansista/innowator/generalny).", | |
| ) | |
| class GlobalAuditOutput(BaseModel): | |
| """ | |
| Ustrukturyzowany wynik audytu całego wniosku dotacyjnego. | |
| FAZA 4: confidence_score + human_review_required. | |
| """ | |
| is_approved: bool = Field( | |
| description="Czy wniosek nadaje się do wysłania bez krytycznych błędów." | |
| ) | |
| export_status: Literal["blocked", "warning", "ok"] = Field( | |
| description="Stan eksportu: blocked (błąd krytyczny), warning (błędy wysokie), ok (brak poważnych)." | |
| ) | |
| overall_score: int = Field(description="Ogólna ocena poprawności w skali 0–100.") | |
| confidence_score: float = Field( | |
| default=0.85, | |
| description="Pewność modelu co do wyników audytu (0.0–1.0). Wartość < 0.7 → wymaga weryfikacji człowieka.", | |
| ) | |
| human_review_required: bool = Field( | |
| default=False, | |
| description="True gdy score < 60 lub istnieją błędy critical → wymaga weryfikacji eksperta.", | |
| ) | |
| issues: List[AuditIssue] = Field( | |
| description="Wykryte błędy, rozbieżności i nieprawidłowości formalne." | |
| ) | |
| perspectives_summary: Dict[str, str] = Field( | |
| default_factory=dict, | |
| description="Skrótowe opinie poszczególnych ról audytorów (prawnik/finansista/innowator).", | |
| ) | |
| ai_disclaimer: str = Field( | |
| default="Wynik audytu wygenerowany przez AI na podstawie regulaminów programu. " | |
| "Zalecana weryfikacja przez doradcę dotacyjnego lub radcę prawnego przed złożeniem wniosku.", | |
| description="Obowiązkowy disclaimer AI Act Art. 13.", | |
| ) | |
| # ────────────────────────────────────────────────────────────────────────────── | |
| # Pomocnicze prompty per rola (FAZA 5 — Multi-Perspective Audit) | |
| # ────────────────────────────────────────────────────────────────────────────── | |
| _ROLE_PROMPTS = { | |
| "prawnik": """ | |
| Jesteś PRAWNIKIEM DOTACYJNYM specjalizującym się w polskim prawie i regulacjach UE. | |
| Analizujesz WYŁĄCZNIE aspekty prawno-formalne: | |
| - Kwalifikowalność kosztów (zakaz podwójnego finansowania, de minimis) | |
| - Wykluczenia prawne (zakaz działalności z aneksów rozporządzeń) | |
| - DNSH (Do No Significant Harm) — zgodność z taksonomią UE | |
| - Warunki formalne dokumentacji (daty, podpisy, pełnomocnictwa) | |
| - Zgodność z Rozporządzeniem UE 2021/1060 i krajowymi wytycznymi MFiPR | |
| Zwróć TYLKO błędy prawne i formalne. Ignoruj aspekty innowacyjności czy ROI. | |
| """, | |
| "finansista": """ | |
| Jesteś ANALITYKIEM FINANSOWYM specjalizującym się w budżetach projektów dotacyjnych. | |
| Analizujesz WYŁĄCZNIE aspekty finansowe: | |
| - Budżet vs Harmonogram rzeczowo-finansowy (spójność kwot i terminów) | |
| - Racjonalność kosztów (rynkowość cen, uzasadnienie wydatków) | |
| - Limity intensywności pomocy dla danej kategorii firmy | |
| - Koszty pośrednie (ryczałt / metoda rzeczywista — poprawność zastosowania) | |
| - Ryzyko finansowe projektu i zabezpieczenia | |
| Zwróć TYLKO błędy finansowe i rachunkowe. Ignoruj kwestie prawne i innowacyjność. | |
| """, | |
| "innowator": """ | |
| Jesteś EKSPERTEM OD INNOWACJI oceniającym potencjał i spójność merytoryczną projektu. | |
| Analizujesz WYŁĄCZNIE aspekty merytoryczno-innowacyjne: | |
| - Poziom innowacyjności (czy projekt jest wystarczająco innowacyjny dla danego programu?) | |
| - Spójność logiczna: cele → działania → rezultaty → wskaźniki (logframe) | |
| - Opis prac B+R (czy istnieje element badawczy i jest właściwie uzasadniony?) | |
| - Potencjał komercjalizacji i skalowalność | |
| - Opis ryzyk projektu i plany mitigacji | |
| Zwróć TYLKO błędy merytoryczne i innowacyjne. Ignoruj kwestie prawne i finansowe. | |
| """, | |
| } | |
| _SHARED_INSTRUCTIONS = """ | |
| Pamiętaj: | |
| - Absolony zakaz halucynacji. Jeśli nie masz pewności — napisz "Brak wystarczających informacji." | |
| - Zawsze odpowiadaj po polsku, używając precyzyjnego, urzędowego języka. | |
| - Podaj CYTAT i REKOMENDACJĘ dla każdego defektu. | |
| - Wskaż affected_section (tytuł sekcji) i problem_quote (krótki cytat). | |
| - UWAGA: Jako `affected_section` MUSISZ użyć jednej z poniższych dokładnych nazw (nie wymyślaj własnych!): | |
| "Streszczenie Projektu", "Opis przedsiębiorstwa i potencjał", "Opis innowacji / B+R", | |
| "Analiza rynku i konkurencji", "Agenda badawcza / cele", "Poziom gotowości technologii (TRL)", | |
| "Budżet i kwalifikowalność kosztów", "Harmonogram rzeczowo-finansowy", "Zespół projektowy", | |
| "Zarządzanie ryzykiem", "Wpływ społeczny i środowiskowy (DNSH)", "Prawa własności intelektualnej", | |
| "Wskaźniki sukcesu i ewaluacja", "Ogólne". | |
| - Jeśli wniosek nie ma błędów i jest idealny, zwróć pustą listę `issues` i ustaw `partial_score` na 100. Wynik 0 oznacza krytyczny brak zgodności. | |
| """ | |
| # ────────────────────────────────────────────────────────────────────────────── | |
| # Główna funkcja audytu (sync wrapper nad async) | |
| # ────────────────────────────────────────────────────────────────────────────── | |
| class _PerspectiveResult(BaseModel): | |
| """Wynik cząstkowy jednej roli audytora.""" | |
| issues: List[AuditIssue] = Field(default_factory=list) | |
| summary: str = Field(default="") | |
| partial_score: int = Field(default=100) | |
| async def _run_perspective_audit( | |
| role: str, | |
| role_prompt: str, | |
| program_name: str, | |
| content: str, | |
| ) -> _PerspectiveResult: | |
| """Wywołanie LLM dla jednej roli audytora.""" | |
| llm = get_llm(task_type="legal_audit", structured_output_schema=_PerspectiveResult) | |
| prompt = f"""{role_prompt} | |
| {_SHARED_INSTRUCTIONS} | |
| Nazwa/Typ programu: {program_name} | |
| TREŚĆ WNIOSKU: | |
| --------------------- | |
| {content[:150000]} | |
| --------------------- | |
| Oceń wniosek ze swojej perspektywy ({role}) i zwróć: issues, summary (2-3 zdania), partial_score (0-100). | |
| """ | |
| def _invoke_llm(): | |
| return llm.invoke(prompt) | |
| try: | |
| result: _PerspectiveResult = _invoke_llm() | |
| return result | |
| except Exception as e: | |
| logger.warning(f"[MultiAudit][{role}] Błąd perspektywy: {e}") | |
| return _PerspectiveResult( | |
| summary=f"Perspektywa {role} — błąd LLM: {str(e)[:100]}", partial_score=50 | |
| ) | |
| def _compute_final_score(scores: List[int], has_critical: bool) -> int: | |
| """Średnia ważona wyników perspektyw. Kara za critical.""" | |
| if not scores: | |
| return 0 | |
| base = int(sum(scores) / len(scores)) | |
| return max(0, base - 20) if has_critical else base | |
| def audit_final_document( | |
| project_id: str, | |
| program_name: str, | |
| content: str, | |
| enable_multi_perspective: bool = True, | |
| is_external_audit: bool = False, | |
| ) -> GlobalAuditOutput: | |
| """ | |
| Agencja Krytyka — główny punkt wejścia. | |
| Parametry: | |
| project_id: ID projektu (do logowania) | |
| program_name: Nazwa programu (FENG, KPO, etc.) | |
| content: Pełna treść wygenerowanego wniosku | |
| enable_multi_perspective: Włącz 3 role audytorów (domyślnie True) | |
| Zwraca: | |
| GlobalAuditOutput z issues, score, confidence, human_review_required | |
| """ | |
| if not content or len(content.strip()) < 50: | |
| from core.telemetry import telemetry | |
| telemetry.log( | |
| "WARN", | |
| "Auditor", | |
| "Dokument zbyt krótki do audytu", | |
| {"project_id": project_id}, | |
| ) | |
| return GlobalAuditOutput( | |
| is_approved=False, | |
| export_status="blocked", | |
| overall_score=0, | |
| confidence_score=1.0, | |
| human_review_required=True, | |
| issues=[ | |
| AuditIssue( | |
| category="Formalności", | |
| severity="critical", | |
| message="Dokument jest pusty lub zbyt krótki do przeprowadzenia audytu.", | |
| rule_citation="Minimum objętościowe wniosku", | |
| recommendation="Wygeneruj zawartość wniosku przed uruchomieniem audytu.", | |
| ) | |
| ], | |
| ) | |
| all_issues: List[AuditIssue] = [] | |
| perspectives_summary: Dict[str, str] = {} | |
| perspective_scores: List[int] = [] | |
| # ── Blok Multi-Perspective (FAZA 5) ─────────────────────────────────────── | |
| if enable_multi_perspective: | |
| logger.info( | |
| f"[Audytor] Uruchamianie audytu multi-perspektywowego(LangGraph) dla projektu {project_id}" | |
| ) | |
| from core.telemetry import telemetry | |
| telemetry.log( | |
| "INFO", | |
| "Auditor", | |
| "Uruchamianie audytu multi-perspektywowego", | |
| {"project_id": project_id}, | |
| ) | |
| try: | |
| from agents.auditor_panel_graph import auditor_panel_app | |
| initial_state = { | |
| "project_id": project_id, | |
| "program_name": program_name, | |
| "content": content, | |
| "is_external_audit": is_external_audit, | |
| "issues": [], | |
| "perspectives_summary": {}, | |
| "perspective_scores": [], | |
| "legal_attempts": 0, | |
| "legal_queries": [], | |
| "messages": [], | |
| "prawnik_done": False, | |
| "finansista_attempts": 0, | |
| "finansista_queries": [], | |
| "finansista_messages": [], | |
| "finansista_done": False, | |
| "innowator_attempts": 0, | |
| "innowator_queries": [], | |
| "innowator_messages": [], | |
| "innowator_done": False, | |
| } | |
| # Synchronous execution of the state graph with increased recursion limit | |
| result_state = auditor_panel_app.invoke( | |
| initial_state, config={"recursion_limit": 150} | |
| ) | |
| # Extrakcja finalnego wyniku z węzła zarządzającego | |
| if "final_output" in result_state and result_state["final_output"]: | |
| logger.info( | |
| f"[Audytor] Pomyślnie zakończono graf LangGraph. Status: {result_state['final_output'].export_status}" | |
| ) | |
| return result_state["final_output"] | |
| else: | |
| logger.warning( | |
| "[Audytor] Graf zakończył pracę, ale nie zwrócił final_output. Fallback." | |
| ) | |
| enable_multi_perspective = False | |
| except Exception as e: | |
| logger.error( | |
| f"[Audytor] Błąd multi-perspektywowego grafu LangGraph: {e}. Fallback na audyt ogólny." | |
| ) | |
| enable_multi_perspective = False | |
| # ── Fallback: audyt ogólny (jeśli multi-perspective wyłączony lub failed) ─ | |
| if not enable_multi_perspective or not all_issues: | |
| logger.info(f"[Audytor] Audyt generalny dla projektu {project_id}") | |
| from core.telemetry import telemetry | |
| telemetry.log( | |
| "INFO", | |
| "Auditor", | |
| "Uruchamianie audytu generalnego (Fallback)", | |
| {"project_id": project_id}, | |
| ) | |
| try: | |
| llm_general = get_llm( | |
| task_type="legal_audit", structured_output_schema=GlobalAuditOutput | |
| ) | |
| general_prompt = f""" | |
| Jesteś surowym, precyzyjnym audytorem dotacyjnym specjalizującym się w polskim prawie funduszy europejskich. | |
| {"Pamiętaj, że weryfikujesz wniosek z firmy doradczej (zewnętrzny), musisz surowo wyłapać ich błędy." if is_external_audit else ""} | |
| Zakaz halucynacji. Jeśli nie masz pewności — napisz: "Brak wystarczających informacji." | |
| Odpowiadaj po polsku, precyzyjnym urzędowym językiem. | |
| Nazwa/Typ programu: {program_name} | |
| Wykonaj weryfikację krzyżową (Cross-Check): | |
| 1. Zgodność z celami programu | |
| 2. Budżet vs Harmonogram (spójność kwot i terminów) | |
| 3. Koszty kwalifikowalne i wykluczenia | |
| 4. Zasada DNSH (Do No Significant Harm) — zgodność klimatyczna | |
| 5. Warunki formalne i zakaz podwójnego finansowania | |
| 6. Rozbieżności merytoryczne między sekcjami | |
| Podaj CYTAT i REKOMENDACJĘ dla każdego defektu. | |
| Jako affected_section użyj TYLKO jednej z nazw: "Streszczenie Projektu", "Opis przedsiębiorstwa i potencjał", "Opis innowacji / B+R", "Analiza rynku i konkurencji", "Agenda badawcza / cele", "Poziom gotowości technologii (TRL)", "Budżet i kwalifikowalność kosztów", "Harmonogram rzeczowo-finansowy", "Zespół projektowy", "Zarządzanie ryzykiem", "Wpływ społeczny i środowiskowy (DNSH)", "Prawa własności intelektualnej", "Wskaźniki sukcesu i ewaluacja", "Ogólne". | |
| Wskaż problem_quote. | |
| Ustaw confidence_score (0.0–1.0) oraz human_review_required (True gdy score<60 lub błąd critical). | |
| TREŚĆ WNIOSKU: | |
| --------------------- | |
| {content[:10000]} | |
| --------------------- | |
| """ | |
| def _invoke_general_llm(): | |
| return llm_general.invoke(general_prompt) | |
| result: GlobalAuditOutput = _invoke_general_llm() | |
| # Zapewnij human_review logikę | |
| result.human_review_required = result.overall_score < 60 or any( | |
| i.severity == "critical" for i in result.issues | |
| ) | |
| result.export_status = _determine_export_status(result.issues) | |
| _log_audit(project_id, result) | |
| return result | |
| except Exception as e: | |
| import traceback | |
| traceback.print_exc() | |
| return _error_output(e) | |
| # ── Deduplikacja issues (podobne wiadomości z różnych perspektyw) ────────── | |
| seen_messages = set() | |
| deduplicated: List[AuditIssue] = [] | |
| for issue in all_issues: | |
| key = (issue.category, issue.message[:60]) | |
| if key not in seen_messages: | |
| seen_messages.add(key) | |
| deduplicated.append(issue) | |
| has_critical = any(i.severity == "critical" for i in deduplicated) | |
| any(i.severity == "high" for i in deduplicated) | |
| overall = _compute_final_score(perspective_scores, has_critical) | |
| output = GlobalAuditOutput( | |
| is_approved=not has_critical, | |
| export_status=_determine_export_status(deduplicated), | |
| overall_score=overall, | |
| confidence_score=round(min(1.0, len(perspective_scores) / 3 * 0.9 + 0.1), 2), | |
| human_review_required=(overall < 60 or has_critical), | |
| issues=deduplicated, | |
| perspectives_summary=perspectives_summary, | |
| ) | |
| _log_audit(project_id, output) | |
| return output | |
| def _determine_export_status( | |
| issues: List[AuditIssue], | |
| ) -> Literal["blocked", "warning", "ok"]: | |
| """Określa status eksportu na podstawie najpoważniejszego błędu.""" | |
| severities = {i.severity for i in issues} | |
| if "critical" in severities: | |
| return "blocked" | |
| if "high" in severities: | |
| return "warning" | |
| return "ok" | |
| def _log_audit(project_id: str, result: GlobalAuditOutput) -> None: | |
| try: | |
| audit_log( | |
| "AUDYTOR_MULTI", | |
| f"Projekt: {project_id} | Score: {result.overall_score} | " | |
| f"Issues: {len(result.issues)} | HumanReview: {result.human_review_required} | " | |
| f"Confidence: {result.confidence_score:.2f}", | |
| ) | |
| except Exception: | |
| pass | |
| def _error_output(e: Exception) -> GlobalAuditOutput: | |
| return GlobalAuditOutput( | |
| is_approved=False, | |
| export_status="blocked", | |
| overall_score=0, | |
| confidence_score=0.0, | |
| human_review_required=True, | |
| issues=[ | |
| AuditIssue( | |
| category="Błąd Systemowy", | |
| severity="critical", | |
| message=f"Awaria mechanizmu audytu LLM: {str(e)[:200]}", | |
| recommendation="Sprawdź logi serwera i spróbuj ponownie.", | |
| ) | |
| ], | |
| ) | |