grantforge-api / backend /endpoints /grants_intelligence.py
GrantForge Bot
Deploy sha-9a5957fcdef15b7e2623f8b147cda6026475aee0 — source build (no GHCR)
3a3734f
Raw
History Blame Contribute Delete
6.61 kB
"""Hybrid Grant Intelligence System — admin dashboard, rekomendacje, research."""
from __future__ import annotations
import logging
import os
from typing import Any, Dict, List, Optional
from fastapi import APIRouter, Depends, Query
from pydantic import BaseModel, Field
from core.subscription.db import SessionLocal
logger = logging.getLogger(__name__)
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
router = APIRouter(prefix="/intelligence", tags=["grant-intelligence"])
class CompanyProfileRequest(BaseModel):
name: Optional[str] = ""
size: str = "mikro"
region: Optional[str] = None
voivodeship: Optional[str] = None
pkd: List[str] = Field(default_factory=list)
pkd_codes: List[str] = Field(default_factory=list)
entity_type: str = "przedsiębiorca"
industry: Optional[str] = None
class HybridSearchRequest(BaseModel):
query: str = ""
filters: Dict[str, Any] = Field(default_factory=dict)
limit: int = 50
trusted_only: bool = False
class GrantResearchRequest(BaseModel):
grant_id: str
company_profile: Optional[CompanyProfileRequest] = None
changes: List[Dict[str, Any]] = Field(default_factory=list)
class ResolveVerificationRequest(BaseModel):
resolution_note: str = ""
status: str = "approved"
@router.get("/dashboard")
async def intelligence_dashboard(db=Depends(get_db)):
"""Dashboard administracyjny: research, kompletność, kolejka weryfikacji."""
from core.grants.catalog_service import get_catalog_stats
from core.grants.human_verification import hvq_stats, list_pending_verifications
from core.grants.live_research import research_source_state
from core.grants.versioning import get_recent_versions
return {
"status": "ok",
"layers": {
"curated_kb": get_catalog_stats(db),
"live_research": research_source_state(db),
"human_verification": hvq_stats(db),
"recent_changes": get_recent_versions(db, limit=15),
"pending_verifications": list_pending_verifications(db, limit=20),
},
"feature_flags": {
"ENABLE_LIVE_RESEARCH": os.environ.get("ENABLE_LIVE_RESEARCH", "true"),
"ENABLE_GRANT_RESEARCH_AGENT": os.environ.get("ENABLE_GRANT_RESEARCH_AGENT", "true"),
"ENABLE_HYBRID_SEARCH": os.environ.get("ENABLE_HYBRID_SEARCH", "true"),
"ENABLE_PINECONE_SEMANTIC": os.environ.get("ENABLE_PINECONE_SEMANTIC", "true"),
"HUMAN_VERIF_QUEUE": os.environ.get("HUMAN_VERIF_QUEUE", "true"),
},
"hybrid_search_status": __import__(
"core.grants.hybrid_search_status", fromlist=["get_hybrid_search_status"]
).get_hybrid_search_status(),
}
@router.post("/recommendations")
async def company_recommendations(request: CompanyProfileRequest, db=Depends(get_db)):
"""Rekomendacje dla firmy z relevance score i wyjaśnieniem."""
from core.grants.recommendations import recommend_grants_for_company
company = request.model_dump()
if company.get("voivodeship") and not company.get("region"):
company["region"] = company["voivodeship"]
if company.get("pkd_codes") and not company.get("pkd"):
company["pkd"] = company["pkd_codes"]
return recommend_grants_for_company(db, company, limit=15)
@router.post("/hybrid-search")
async def hybrid_search_endpoint(request: HybridSearchRequest, db=Depends(get_db)):
"""Hybrydowe wyszukiwanie PostgreSQL FTS + Pinecone semantic."""
from core.grants.hybrid_search import hybrid_search_catalog
results = hybrid_search_catalog(
db,
query=request.query,
filters=request.filters,
limit=request.limit,
trusted_only=request.trusted_only,
)
return {"status": "ok", "count": len(results), "grants": results}
@router.post("/research/run-cycle")
async def run_research_cycle(db=Depends(get_db)):
"""Uruchamia cykl Live Research (detection + job queue)."""
from core.grants.live_research import run_live_research_cycle
return {"status": "ok", **run_live_research_cycle(db)}
@router.get("/research/jobs")
async def list_jobs(
status: Optional[str] = Query(None),
limit: int = Query(50, ge=1, le=200),
db=Depends(get_db),
):
from core.grants.live_research import list_research_jobs
return {"status": "ok", "jobs": list_research_jobs(db, status=status, limit=limit)}
@router.post("/agent/run")
async def run_research_agent(request: GrantResearchRequest, db=Depends(get_db)):
"""Ręczne uruchomienie grant_research_agent LangGraph."""
from agents.grant_research_agent import run_grant_research_agent
from core.grants.completeness import grant_dict_from_row
from core.grants.models import Grant
row = db.query(Grant).filter(Grant.source_id == request.grant_id).first()
if not row:
return {"status": "error", "detail": "grant_not_found"}
grant_data = grant_dict_from_row(row)
company = request.company_profile.model_dump() if request.company_profile else {}
result = run_grant_research_agent(
grant_data,
changes=request.changes,
company_profile=company,
)
return {"status": "ok", "result": result}
@router.get("/verification/queue")
async def verification_queue(
status: str = Query("pending"),
limit: int = Query(50, ge=1, le=200),
db=Depends(get_db),
):
from core.grants.human_verification import list_pending_verifications
return {
"status": "ok",
"items": list_pending_verifications(db, status=status, limit=limit),
}
@router.post("/verification/{item_id}/resolve")
async def resolve_verification_item(
item_id: str,
request: ResolveVerificationRequest,
db=Depends(get_db),
):
from core.grants.human_verification import resolve_verification
return resolve_verification(
db,
item_id,
resolution_note=request.resolution_note,
new_status=request.status,
)
@router.get("/versions")
async def grant_versions(
grant_id: Optional[str] = Query(None),
limit: int = Query(50, ge=1, le=200),
db=Depends(get_db),
):
from core.grants.versioning import get_recent_versions
return {
"status": "ok",
"versions": get_recent_versions(db, grant_source_id=grant_id, limit=limit),
}
def register_intelligence_routes(app) -> None:
"""Montuje router na aplikacji FastAPI."""
app.include_router(router)
logger.info("[GrantIntelligence] endpoints registered at /api/grants/intelligence")