Spaces:
Running
Running
| import os | |
| import httpx | |
| import time | |
| from fastapi import APIRouter, Depends, Request, Query, HTTPException | |
| from fastapi.responses import StreamingResponse | |
| from core.telemetry import telemetry | |
| from integrations.eurlex_client import EURLexClient | |
| from core.search.regulation_snapshot import regulation_snapshot_store | |
| from endpoints.admin import verify_admin | |
| from core.subscription.middleware import verify_bearer_token | |
| import secrets | |
| router = APIRouter() | |
| _DIAG_STREAM_TICKETS: dict[str, dict] = {} | |
| _DIAG_TICKET_TTL = 60 | |
| def _mint_diag_ticket(user_id: str) -> str: | |
| now = time.time() | |
| for k, v in list(_DIAG_STREAM_TICKETS.items()): | |
| if v.get("exp", 0) < now or v.get("used"): | |
| _DIAG_STREAM_TICKETS.pop(k, None) | |
| ticket = secrets.token_urlsafe(32) | |
| _DIAG_STREAM_TICKETS[ticket] = {"user_id": user_id, "exp": now + _DIAG_TICKET_TTL, "used": False} | |
| return ticket | |
| def _consume_diag_ticket(ticket: str) -> str: | |
| entry = _DIAG_STREAM_TICKETS.pop(ticket, None) | |
| if not entry or entry.get("used") or entry.get("exp", 0) < time.time(): | |
| raise HTTPException(status_code=401, detail="Invalid or expired stream ticket") | |
| return entry["user_id"] | |
| async def ping_service(url: str, env_var: str): | |
| start = time.perf_counter() | |
| api_key = os.getenv(env_var) | |
| if not api_key: | |
| return {"status": "error", "message": f"{env_var} not set", "latency_ms": 0} | |
| try: | |
| async with httpx.AsyncClient(timeout=3.0) as client: | |
| await client.get(url) | |
| latency = int((time.perf_counter() - start) * 1000) | |
| return { | |
| "status": "ok", | |
| "message": "API Key configured and service reachable", | |
| "latency_ms": latency, | |
| } | |
| except Exception as e: | |
| latency = int((time.perf_counter() - start) * 1000) | |
| return {"status": "error", "message": str(e), "latency_ms": latency} | |
| async def check_gemini(): | |
| return await ping_service( | |
| "https://generativelanguage.googleapis.com/v1beta/models", "GOOGLE_API_KEY" | |
| ) | |
| async def check_grok(): | |
| # Akceptuj XAI_API_KEY lub GROK_API_KEY (HF Secret zwykle: XAI_API_KEY) | |
| env_var = ( | |
| "XAI_API_KEY" | |
| if os.getenv("XAI_API_KEY") | |
| else "GROK_API_KEY" | |
| ) | |
| return await ping_service("https://api.x.ai/v1/models", env_var) | |
| async def check_pinecone(): | |
| return await ping_service("https://api.pinecone.io", "PINECONE_API_KEY") | |
| async def check_clerk(): | |
| return await ping_service( | |
| "https://api.clerk.com/v1/users?limit=1", "CLERK_SECRET_KEY" | |
| ) | |
| async def check_langsmith(): | |
| return await ping_service( | |
| "https://api.smith.langchain.com/api/v1/workspaces", "LANGCHAIN_API_KEY" | |
| ) | |
| async def check_crawl4ai(): | |
| from core.crawl4ai_client import check_crawl4ai_health | |
| return await check_crawl4ai_health() | |
| async def check_neo4j(): | |
| start = time.perf_counter() | |
| neo4j_uri = os.getenv("NEO4J_URI") | |
| if not neo4j_uri or "..." in neo4j_uri: | |
| return { | |
| "status": "disabled", | |
| "message": "Neo4j nie jest skonfigurowane (NEO4J_URI brak). GraphRAG działa w trybie ograniczonym — opcjonalne dla MSP.", | |
| "latency_ms": 0, | |
| } | |
| try: | |
| from neo4j import AsyncGraphDatabase | |
| driver = AsyncGraphDatabase.driver( | |
| neo4j_uri, | |
| auth=(os.getenv("NEO4J_USERNAME", os.getenv("NEO4J_USER", "neo4j")), os.getenv("NEO4J_PASSWORD", "")), | |
| ) | |
| await driver.verify_connectivity() | |
| await driver.close() | |
| latency = int((time.perf_counter() - start) * 1000) | |
| return { | |
| "status": "ok", | |
| "message": "Neo4j connection successful", | |
| "latency_ms": latency, | |
| } | |
| except ImportError: | |
| latency = int((time.perf_counter() - start) * 1000) | |
| return { | |
| "status": "ok", | |
| "message": "NEO4J_URI configured", | |
| "latency_ms": latency, | |
| } | |
| except Exception as e: | |
| latency = int((time.perf_counter() - start) * 1000) | |
| return {"status": "error", "message": str(e), "latency_ms": latency} | |
| async def check_database(): | |
| start = time.perf_counter() | |
| try: | |
| from core.subscription.db import SessionLocal | |
| from sqlalchemy import text | |
| db = SessionLocal() | |
| db.execute(text("SELECT 1")) | |
| db.close() | |
| latency = int((time.perf_counter() - start) * 1000) | |
| return { | |
| "status": "ok", | |
| "message": "Database connection successful", | |
| "latency_ms": latency, | |
| } | |
| except Exception as e: | |
| latency = int((time.perf_counter() - start) * 1000) | |
| return {"status": "error", "message": str(e), "latency_ms": latency} | |
| async def check_eurlex(): | |
| start = time.perf_counter() | |
| try: | |
| client = EURLexClient() | |
| result = client.check_status() | |
| latency = int((time.perf_counter() - start) * 1000) | |
| result["latency_ms"] = latency | |
| return result | |
| except Exception as e: | |
| latency = int((time.perf_counter() - start) * 1000) | |
| return {"status": "error", "message": str(e), "latency_ms": latency} | |
| async def check_parp(): | |
| start = time.perf_counter() | |
| try: | |
| async with httpx.AsyncClient(timeout=3.0) as client: | |
| await client.get("https://www.parp.gov.pl/") | |
| latency = int((time.perf_counter() - start) * 1000) | |
| return {"status": "ok", "message": "PARP website reachable", "latency_ms": latency} | |
| except Exception as e: | |
| latency = int((time.perf_counter() - start) * 1000) | |
| return {"status": "error", "message": str(e), "latency_ms": latency} | |
| async def check_ncbr(): | |
| start = time.perf_counter() | |
| try: | |
| async with httpx.AsyncClient(timeout=3.0) as client: | |
| await client.get("https://www.gov.pl/web/ncbr") | |
| latency = int((time.perf_counter() - start) * 1000) | |
| return {"status": "ok", "message": "NCBR website reachable", "latency_ms": latency} | |
| except Exception as e: | |
| latency = int((time.perf_counter() - start) * 1000) | |
| return {"status": "error", "message": str(e), "latency_ms": latency} | |
| async def check_bgk(): | |
| start = time.perf_counter() | |
| try: | |
| async with httpx.AsyncClient(timeout=3.0) as client: | |
| await client.get("https://www.bgk.pl/") | |
| latency = int((time.perf_counter() - start) * 1000) | |
| return {"status": "ok", "message": "BGK website reachable", "latency_ms": latency} | |
| except Exception as e: | |
| latency = int((time.perf_counter() - start) * 1000) | |
| return {"status": "error", "message": str(e), "latency_ms": latency} | |
| async def get_system_health(_admin: dict = Depends(verify_admin)): | |
| """Returns the health status of various external dependencies.""" | |
| gemini_status = await check_gemini() | |
| grok_status = await check_grok() | |
| pinecone_status = await check_pinecone() | |
| clerk_status = await check_clerk() | |
| langsmith_status = await check_langsmith() | |
| neo4j_status = await check_neo4j() | |
| db_status = await check_database() | |
| eurlex_status = await check_eurlex() | |
| crawl4ai_status = await check_crawl4ai() | |
| parp_status = await check_parp() | |
| ncbr_status = await check_ncbr() | |
| bgk_status = await check_bgk() | |
| return { | |
| "gemini": gemini_status, | |
| "grok": grok_status, | |
| "pinecone": pinecone_status, | |
| "clerk": clerk_status, | |
| "langsmith": langsmith_status, | |
| "neo4j": neo4j_status, | |
| "database": db_status, | |
| "eurlex": eurlex_status, | |
| "crawl4ai": crawl4ai_status, | |
| "parp_gov": parp_status, | |
| "ncbr_gov": ncbr_status, | |
| "bgk": bgk_status, | |
| } | |
| async def get_acquisition_sources_health(_admin: dict = Depends(verify_admin)): | |
| """ | |
| Endpoint for Acquisition Health Dashboard. | |
| Returns detailed health and scraping statistics for all data sources. | |
| """ | |
| try: | |
| from core.search.grant_search_service import grant_search_service | |
| health = await grant_search_service.get_sources_health() | |
| return health | |
| except Exception as e: | |
| return {"status": "error", "error": str(e)} | |
| async def get_watchdog_stats(_admin: dict = Depends(verify_admin)): | |
| """Parses watchdog logs and returns statistics of auto-recovery interventions.""" | |
| stats = { | |
| "total_interventions": 0, | |
| "retries_429": 0, | |
| "retries_500": 0, | |
| "retries_refusal": 0, | |
| "aborts": 0, | |
| "recent_events": [], | |
| } | |
| try: | |
| log_path = "logs/watchdog.log" | |
| if not os.path.exists(log_path): | |
| return stats | |
| with open(log_path, "r", encoding="utf-8") as f: | |
| lines = f.readlines() | |
| for line in lines: | |
| line = line.strip() | |
| if not line: | |
| continue | |
| stats["total_interventions"] += 1 | |
| if "[RETRY 429]" in line: | |
| stats["retries_429"] += 1 | |
| elif "[RETRY 500]" in line: | |
| stats["retries_500"] += 1 | |
| elif "[RETRY REFUSAL]" in line: | |
| stats["retries_refusal"] += 1 | |
| elif "[ABORT]" in line: | |
| stats["aborts"] += 1 | |
| stats["recent_events"] = [line.strip() for line in lines[-50:]] | |
| stats["recent_events"].reverse() | |
| except Exception as e: | |
| stats["error"] = str(e) | |
| return stats | |
| async def instrument_pipeline_health( | |
| limit: int = Query(2000, ge=10, le=10000), | |
| _admin: dict = Depends(verify_admin), | |
| ): | |
| """ | |
| Instrument-first pipeline health for ops: | |
| schema coverage, readiness mix, gold families, dossier flags. | |
| """ | |
| from core.subscription.db import SessionLocal | |
| from core.projects.instrument_ops import ( | |
| build_plan_12m_metrics_snapshot, | |
| compute_instrument_project_metrics, | |
| gold_family_coverage, | |
| ) | |
| from core.grants.dossier_ops import compute_readiness_distribution | |
| session = SessionLocal() | |
| try: | |
| projects = compute_instrument_project_metrics(session, limit=limit) | |
| dossier = compute_readiness_distribution(session, limit=min(limit, 5000)) | |
| return { | |
| "status": "ok", | |
| "gold": gold_family_coverage(), | |
| "projects": projects, | |
| "catalog_dossier": dossier, | |
| "plan_12m": build_plan_12m_metrics_snapshot( | |
| project_metrics=projects, | |
| catalog_stats={"dossier_readiness": dossier}, | |
| ), | |
| } | |
| finally: | |
| session.close() | |
| async def credibility_pipeline_health(_admin: dict = Depends(verify_admin)): | |
| """ | |
| Najważniejszy endpoint dla wizji "najwyższej wiarygodności". | |
| Raportuje stan całego łańcucha ugruntowania: GUS, MSP/GraphRAG, Regulation Snapshots, | |
| EUR-Lex live, mock warnings, pokrycie reguł. | |
| Umożliwia weryfikację, czy system jest godny zaufania na poziomie 'best source of grants on the internet'. | |
| """ | |
| report = { | |
| "timestamp": __import__("datetime").datetime.utcnow().isoformat(), | |
| "overall_credibility": "unknown", | |
| "components": {} | |
| } | |
| try: | |
| snaps = regulation_snapshot_store.list_all() if regulation_snapshot_store else [] | |
| total_rules = sum(len(s.key_rules or []) for s in snaps) | |
| total_excl = sum(len(s.exclusions or []) for s in snaps) | |
| programs = sorted(set(s.program for s in snaps)) | |
| report["components"]["regulation_engine"] = { | |
| "snapshots": len(snaps), | |
| "programs_covered": programs, | |
| "total_structured_rules": total_rules, | |
| "total_exclusions": total_excl, | |
| "status": "strong" if len(snaps) >= 3 and total_rules > 15 else "limited", | |
| "primary_storage": "Postgres" | |
| } | |
| except Exception as e: | |
| report["components"]["regulation_engine"] = {"status": "error", "detail": str(e)} | |
| try: | |
| el = EURLexClient() | |
| el_status = el.check_status() | |
| sample = el.search_legal_acts("Fundusze Europejskie dla Nowoczesnej Gospodarki", limit=1) | |
| report["components"]["eurlex_live"] = { | |
| "status": el_status.get("status"), | |
| "message": el_status.get("message"), | |
| "sample_hits": len(sample), | |
| "note": "Live source of truth for EU law — higher authority than national program rules" | |
| } | |
| except Exception as e: | |
| report["components"]["eurlex_live"] = {"status": "error", "detail": str(e)} | |
| report["components"]["gus_company_data"] = { | |
| "real_gus_integration": "present (BIR1.1 + RegonAPI)", | |
| "mock_only_for_specific_test_nips": True, | |
| "recommendation": "Zawsze sprawdzaj flagę 'data_source' / 'using_real_data' w msp_analysis i company_data", | |
| "risk": "Geoblocking na niektórych serwerach — fallbacki obniżają wiarygodność" | |
| } | |
| report["components"]["msp_graph_rag"] = { | |
| "early_enforcement": "create_project + match + audit", | |
| "mock_warning": "Zawsze emitowane w msp_risk_note + using_real_data", | |
| "status": "transparent" | |
| } | |
| report["components"]["grounding_certificates"] = { | |
| "emission": "Forced in generate_section (helpers.py) + RegulationSnapshot + Engine checks", | |
| "content": "version_hash, effective_date, key_rules count, engine eligibility decisions", | |
| "status": "active (Faza 3)" | |
| } | |
| try: | |
| from core.search.grant_search_service import grant_search_service | |
| from core.trust.trust_scorer import compute_platform_trust_score | |
| aq_health = await grant_search_service.get_sources_health() | |
| link_info = { | |
| "high_quality_links_pct": aq_health.get("precise_regulation_links_percentage", 0), | |
| "breakdown": aq_health.get("regulation_link_quality", {}), | |
| "status": "good" if aq_health.get("precise_regulation_links_percentage", 0) >= 50 else "needs_improvement" | |
| } | |
| report["components"]["acquisition_regulation_link_quality"] = link_info | |
| trust = compute_platform_trust_score(report, { | |
| "snapshots_count": len(report.get("components", {}).get("regulation_engine", {}).get("snapshots", [])), | |
| "total_structured_rules": report.get("components", {}).get("regulation_engine", {}).get("total_structured_rules", 0) | |
| }) | |
| report["platform_trust_score"] = trust | |
| except Exception as e: | |
| report["components"]["acquisition_regulation_link_quality"] = {"status": "error", "detail": str(e)} | |
| strong = sum(1 for c in report["components"].values() if isinstance(c, dict) and c.get("status") in ("strong", "active", "transparent", "present")) | |
| report["overall_credibility"] = "high" if strong >= 4 else "building" if strong >= 2 else "needs_improvement" | |
| report["recommendation_for_users"] = "Używaj /trigger-snapshot + /test-eligibility regularnie. Wgraj zewnętrzne wnioski do Reverse Audit dla maksymalnej weryfikacji." | |
| try: | |
| from core.trust.trust_scorer import compute_platform_trust_score | |
| platform_score = compute_platform_trust_score(report, { | |
| "snapshots_count": len(report.get("components", {}).get("regulation_engine", {}).get("snapshots", [])), | |
| "total_structured_rules": report.get("components", {}).get("regulation_engine", {}).get("total_structured_rules", 0) | |
| }) | |
| report["platform_credibility"] = { | |
| "score": platform_score["overall_score"], | |
| "level": platform_score["level"], | |
| "breakdown": platform_score["breakdown"] | |
| } | |
| except Exception as e: | |
| report["platform_credibility"] = {"error": str(e)} | |
| return report | |
| async def mint_diagnostics_stream_ticket(_admin: dict = Depends(verify_admin)): | |
| """Short-lived ticket for EventSource (avoid putting JWT in query string).""" | |
| user_id = _admin.get("sub") or "admin" | |
| ticket = _mint_diag_ticket(user_id) | |
| return {"ticket": ticket, "expires_in": _DIAG_TICKET_TTL} | |
| async def diagnostics_stream( | |
| request: Request, | |
| ticket: str = Query(None), | |
| token: str = Query(None), | |
| ): | |
| """SSE endpoint for streaming real-time telemetry logs.""" | |
| if ticket: | |
| _consume_diag_ticket(ticket) | |
| elif token: | |
| from core.subscription.auth_utils import is_dev_test_token | |
| if is_dev_test_token(token): | |
| verify_bearer_token(token) | |
| else: | |
| raise HTTPException( | |
| status_code=401, | |
| detail="JWT in query string is disabled; mint a stream ticket first", | |
| ) | |
| else: | |
| # Router-level verify_admin already ran for non-SSE; EventSource has no headers. | |
| raise HTTPException(status_code=401, detail="Missing stream ticket") | |
| return StreamingResponse(telemetry.subscribe(), media_type="text/event-stream") | |