grantforge-api / backend /gsd /tools /gsd_tools.py
GrantForge Bot
Deploy sha-565ad85979610064f6d1c18ab3b6404357d61073 — source build (no GHCR)
ce8f04a
Raw
History Blame Contribute Delete
10.7 kB
"""
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