Spaces:
Running
Running
| """ | |
| Grantforge Tools — Wspólna warstwa narzędzi dla roju GSD | |
| Narzędzia te opakowują istniejące komponenty z backend/ (RAG, Neo4j, KRS, NCBR, PARP, export) | |
| i dodają warstwę traceability + logging wymagana przez Konstytucję GSD. | |
| """ | |
| from __future__ import annotations | |
| from typing import List, Dict, Any, Optional | |
| from datetime import datetime | |
| import logging | |
| logger = logging.getLogger("grantforge.swarm.tools") | |
| # ============================================================================= | |
| # 1. RAG & RETRIEVAL TOOLS (z traceability) | |
| # ============================================================================= | |
| def retrieve_regulation_chunks( | |
| query: str, | |
| program: str = "FENG", | |
| k: int = 8, | |
| namespace: Optional[str] = None, | |
| ) -> List[Dict[str, Any]]: | |
| """ | |
| Pobiera fragmenty regulaminu z hybrydowego retrievera (dense + BM25 + rerank). | |
| Zwraca listę chunków z pełnym metadata (źródło, data, §). | |
| """ | |
| from rag_pipeline import get_hybrid_retriever, rerank_documents | |
| logger.info(f"[RAG] retrieve_regulation_chunks | query='{query[:60]}...' | program={program}") | |
| retriever = get_hybrid_retriever(k=k, namespace=namespace) | |
| docs = retriever.get_relevant_documents(query) | |
| # Rerank dla lepszej precyzji | |
| try: | |
| docs = rerank_documents(query, docs, top_k=min(6, len(docs))) | |
| except Exception: | |
| pass | |
| results = [] | |
| for i, doc in enumerate(docs[:k]): | |
| results.append({ | |
| "content": doc.page_content[:2000], | |
| "source": doc.metadata.get("source", "unknown"), | |
| "section": doc.metadata.get("section", ""), | |
| "date": doc.metadata.get("date", ""), | |
| "program": program, | |
| "retrieved_at": datetime.utcnow().isoformat(), | |
| "score": getattr(doc, "score", 0.0), | |
| }) | |
| return results | |
| def retrieve_legal_context(query: str, k: int = 5) -> List[Dict[str, Any]]: | |
| """Pobiera kontekst prawny (pomoc publiczna, RODO, KSH, EUR-Lex).""" | |
| # W pełnej wersji: integracja z legal_retriever_tool + EUR-Lex | |
| logger.info(f"[Legal] retrieve_legal_context | {query[:50]}") | |
| return [{"content": "TODO: integrate legal_retriever_tool + EUR-Lex", "source": "legal"}] | |
| # ============================================================================= | |
| # 2. GRAPH RAG & MSP ANALYSIS | |
| # ============================================================================= | |
| def analyze_msp_structure(nip: str, deep: bool = True) -> Dict[str, Any]: | |
| """ | |
| Uruchamia GraphRAG MSP Analyzer — buduje graf własności i określa status MŚP. | |
| Zwraca strukturę zgodną z wymaganiami Konstytucji (z confidence + sources). | |
| """ | |
| logger.info(f"[GraphRAG] analyze_msp_structure | NIP={nip} | deep={deep}") | |
| try: | |
| # Prefer graph_rag SME verifier instance (production API) | |
| try: | |
| from core.graph_rag.sme_verifier import sme_verifier | |
| result = sme_verifier.verify_sme_status(nip, declared_status="mikro") | |
| if not isinstance(result, dict): | |
| result = {"raw": result} | |
| except Exception: | |
| from core.graph_db.sme_verifier import SMEVerifier | |
| result = SMEVerifier().verify_sme_status(nip, declared_status="mikro") | |
| if not isinstance(result, dict): | |
| result = {"raw": result} | |
| return { | |
| "is_sme": result.get("is_sme", result.get("is_sme_status")), | |
| "confidence": result.get("confidence", 0.7), | |
| "ultimate_beneficial_owner": result.get("ubo") or result.get("ultimate_beneficial_owner") or [], | |
| "linked_entities": result.get("linked") or result.get("linked_entities") or [], | |
| "risk_flags": result.get("risks") or result.get("risk_flags") or [], | |
| "sources": result.get("sources") or ["KRS", "Rejestr.io", "CEIDG"], | |
| "analyzed_at": datetime.utcnow().isoformat(), | |
| "deep": deep, | |
| "detail": result, | |
| } | |
| except Exception as e: | |
| logger.error(f"GraphRAG MSP analysis failed: {e}") | |
| return { | |
| "is_sme": None, | |
| "confidence": 0.0, | |
| "error": str(e), | |
| "requires_manual_verification": True, | |
| } | |
| # ============================================================================= | |
| # 3. KRS / COMPANY DATA TOOLS | |
| # ============================================================================= | |
| def get_company_profile_from_krs(nip: str) -> Dict[str, Any]: | |
| """Pobiera profil firmy z KRS + Rejestr.io (używa istniejącego KRS Graph Tool).""" | |
| from agents.tools.krs_graph_tool import fetch_krs_profile # existing | |
| logger.info(f"[KRS] get_company_profile_from_krs | NIP={nip}") | |
| try: | |
| return fetch_krs_profile(nip) | |
| except Exception: | |
| return {"nip": nip, "error": "KRS fetch failed — fallback required"} | |
| # ============================================================================= | |
| # 4. GRANT PROGRAMS & MATCHING | |
| # ============================================================================= | |
| def fetch_current_grant_calls(institutions: List[str] = None) -> List[Dict[str, Any]]: | |
| """ | |
| Pobiera aktualne nabory z NCBR, PARP, ARiMR, BGK, województw. | |
| Używa istniejących klientów (ncbr_client, parp_client) + scraping. | |
| """ | |
| logger.info(f"[Grants] fetch_current_grant_calls | institutions={institutions}") | |
| # W pełnej implementacji: aggregator + cache + change detection | |
| return [] | |
| import asyncio | |
| import json | |
| def advanced_grant_match(profile: Dict[str, Any], user_need: str = "") -> List[Dict[str, Any]]: | |
| """ | |
| Zaawansowane dopasowanie z explainability. | |
| Pobiera nabory z bazy przez grant_search_service i ocenia używając LLMa. | |
| """ | |
| logger.info(f"[Matcher] advanced_grant_match | need={user_need[:40]}") | |
| try: | |
| import sys | |
| import os | |
| backend_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../backend')) | |
| if backend_path not in sys.path: | |
| sys.path.append(backend_path) | |
| from core.search.grant_search_service import grant_search_service | |
| from core.llm_router import get_llm | |
| except Exception as e: | |
| logger.error(f"[Matcher] Failed to import backend services: {e}") | |
| return [] | |
| async def _match(): | |
| # Pobierz pasujące nabory | |
| pkds = profile.get("pkd", []) | |
| filters = {"company_pkd": pkds} if pkds else {} | |
| query = f"{user_need} {profile.get('industry', '')}" | |
| # Oczekujemy, że to nam zwróci listę słowników (lub obiektów, które możemy potraktować jako dict) | |
| grants = await grant_search_service.search_grants(query[:200], filters, vector_top_k=5) | |
| if not grants: | |
| return [] | |
| llm = get_llm(temperature=0.0) | |
| grants_context = "" | |
| for i, g in enumerate(grants): | |
| g_dict = g if isinstance(g, dict) else g.__dict__ if hasattr(g, "__dict__") else {"id": str(g)} | |
| grants_context += f"\n--- Grant {i} ---\nTytuł: {g_dict.get('title') or g_dict.get('name')}\nCel: {g_dict.get('goal')}\nOpis: {g_dict.get('description')}\n" | |
| system_prompt = f"""Jesteś ekspertem ds. dotacji. Twoim zadaniem jest ocenić, które z poniższych programów dotacyjnych najlepiej pasują do profilu firmy i jej potrzeb. | |
| Profil firmy: NIP: {profile.get('nip')}, PKD: {profile.get('pkd')}, Wielkość: {profile.get('size')} | |
| Cel inwestycyjny: {user_need} | |
| Dostępne programy: | |
| {grants_context} | |
| Zwróć poprawny JSON w formacie listy obiektów (od najlepszego): | |
| [ | |
| {{ | |
| "title": "Tytuł programu", | |
| "relevance_score": 0.95, | |
| "explanation": {{"reason": "Krótkie uzasadnienie dlaczego pasuje", "risk": "Potencjalne ryzyko odrzucenia"}} | |
| }} | |
| ] | |
| Upewnij się, że "title" dokładnie odpowiada jednemu z podanych programów. Jeśli żaden nie pasuje, zwróć pustą listę []. Tylko JSON, bez markdownu. | |
| """ | |
| response = await llm.ainvoke(system_prompt) | |
| content = response.content.strip() | |
| if content.startswith("```json"): | |
| content = content[7:-3] | |
| try: | |
| results = json.loads(content) | |
| return results | |
| except Exception as e: | |
| logger.error(f"Failed to parse LLM matching response: {e}") | |
| return [] | |
| # Run async function in sync context | |
| try: | |
| # Check if there is an existing event loop | |
| loop = asyncio.get_event_loop() | |
| if loop.is_running(): | |
| import nest_asyncio | |
| nest_asyncio.apply() | |
| return loop.run_until_complete(_match()) | |
| else: | |
| return loop.run_until_complete(_match()) | |
| except RuntimeError: | |
| return asyncio.run(_match()) | |
| # ============================================================================= | |
| # 5. EXPORT & AUDIT TOOLS | |
| # ============================================================================= | |
| def generate_grounding_certificate(state: Any) -> Dict[str, Any]: | |
| """Generuje pełne Świadectwo Zgodności na podstawie audit_trail i sekcji.""" | |
| logger.info("[Export] Generating Grounding Certificate...") | |
| # W pełnej wersji: buduje hash chain, zbiera wszystkie źródła, liczy overall score | |
| return { | |
| "certificate_id": f"GFC-{datetime.utcnow().strftime('%Y%m%d')}-001", | |
| "overall_grounding_score": 87.5, | |
| "sections": {}, | |
| "msp_analysis": {}, | |
| "legal_risks": [], | |
| "auditor_verdict": "CONDITIONAL_APPROVAL", | |
| "generated_at": datetime.utcnow().isoformat(), | |
| } | |
| def export_final_package( | |
| sections: Dict[str, str], | |
| certificate: Dict[str, Any], | |
| format: str = "docx", | |
| ) -> str: | |
| """Eksportuje finalny wniosek + Świadectwo Zgodności do DOCX/PDF.""" | |
| from core.document_builder import build_document # existing | |
| logger.info(f"[Export] export_final_package | format={format}") | |
| # TODO: full implementation using existing document_builder + DOCX skill | |
| return "/tmp/grantforge_export_001.docx" | |
| # ============================================================================= | |
| # 6. AUDIT & LOGGING (konstytucyjne) | |
| # ============================================================================= | |
| def log_gsd_decision( | |
| agent: str, | |
| phase: str, | |
| decision: str, | |
| grounding_sources: List[str], | |
| confidence: float, | |
| risk: str = "medium", | |
| ): | |
| """Zapisuje decyzję agenta do audytu (używane przez wszystkie agenty GSD).""" | |
| logger.info( | |
| f"[GSD-AUDIT] {phase.upper()} | {agent} | conf={confidence:.2f} | risk={risk}\n" | |
| f" Decision: {decision[:120]}...\n" | |
| f" Sources: {grounding_sources[:2]}" | |
| ) | |
| # W pełnej wersji: zapis do bazy + LangSmith + hash chain | |