File size: 5,409 Bytes
ed33951
 
159f5a5
16b7df8
 
159f5a5
9dfccd9
159f5a5
 
16b7df8
6a4e71a
159f5a5
dc7da82
7eb57e9
dc7da82
 
9dfccd9
 
 
451d52a
9dfccd9
e8b3591
 
9dfccd9
 
 
159f5a5
16b7df8
 
 
 
 
 
 
 
159f5a5
 
 
16b7df8
159f5a5
 
9dfccd9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
451d52a
9dfccd9
e8b3591
 
9dfccd9
 
159f5a5
 
16b7df8
6a4e71a
dc7da82
 
 
7eb57e9
9dfccd9
 
 
68af3c5
9dfccd9
 
 
 
 
 
 
 
e646563
9dfccd9
e646563
 
9dfccd9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68af3c5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
from dotenv import load_dotenv
load_dotenv()

from contextlib import asynccontextmanager

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

from agent.api import router as agent_router
from graph_store.api import router as graph_router
from graph_store.stream import router as graph_stream_router
from ingestion.api import router as ingestion_router
from src.confluence_agent.router import router as confluence_router
from toolsforgitnotionslack.router import router as tools_router
from src.file_agent.router import router as file_router
from src.jira_agent.router import router as jira_router
from src.utils.middleware import RequestLoggingMiddleware
from src.auth.router import router as auth_router
from src.analytics.router import router as analytics_router
from src.anomaly.router import router as anomaly_router
from src.admin.router import router as admin_router
from src.admin.users_api import router as admin_users_router
from src.admin.users_api import audit_router as admin_audit_router
from src.workspace.router import router as workspace_router
from src.ws.router import router as ws_router
# src.utils.logger configures the root JSON handler on import


@asynccontextmanager
async def lifespan(app: FastAPI):
    yield
    from graph_store.writer import close_driver
    await close_driver()


app = FastAPI(
    title="Enterprise Knowledge Copilot",
    version="0.1.0",
    lifespan=lifespan,
)

# ---------------------------------------------------------------------------
# CORS β€” allow the Vite dev server and any configured origins
# ---------------------------------------------------------------------------
from src.config import settings as _settings

_cors_origins = [o.strip() for o in _settings.cors_origins.split(",") if o.strip()]

app.add_middleware(
    CORSMiddleware,
    allow_origins=_cors_origins,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)
app.add_middleware(RequestLoggingMiddleware)

# ---------------------------------------------------------------------------
# Routers
# ---------------------------------------------------------------------------
app.include_router(auth_router)
app.include_router(analytics_router)
app.include_router(anomaly_router)
app.include_router(admin_router)
app.include_router(admin_users_router)
app.include_router(admin_audit_router)
app.include_router(workspace_router)
app.include_router(ws_router)
app.include_router(agent_router)
app.include_router(ingestion_router)
app.include_router(graph_router)
app.include_router(graph_stream_router)
app.include_router(jira_router)
app.include_router(confluence_router)
app.include_router(file_router)
app.include_router(tools_router)


# ---------------------------------------------------------------------------
# Health endpoint β€” must be registered BEFORE the SPA catch-all
# ---------------------------------------------------------------------------

@app.get("/health", tags=["infra"])
async def health() -> dict:
    import asyncio

    results: dict = {"status": "ok", "neo4j": "unknown", "redis": "unknown", "qdrant": "unknown"}

    # Neo4j β€” reuse module-level singleton; do not close it here
    try:
        from graph_store.writer import get_driver
        driver = get_driver()
        await asyncio.wait_for(driver.verify_connectivity(), timeout=3)
        results["neo4j"] = "ok"
    except Exception as exc:
        results["neo4j"] = f"error: {exc}"
        results["status"] = "degraded"

    # Redis
    try:
        import redis.asyncio as aioredis
        r = aioredis.from_url(_settings.redis_url, socket_connect_timeout=3)
        await r.ping()
        await r.aclose()
        results["redis"] = "ok"
    except Exception as exc:
        results["redis"] = f"error: {exc}"
        results["status"] = "degraded"

    # Qdrant β€” supports local (host+port) and hosted (url+api_key)
    try:
        from qdrant_client import AsyncQdrantClient
        if _settings.qdrant_url:
            qc = AsyncQdrantClient(
                url=_settings.qdrant_url,
                api_key=_settings.qdrant_api_key or None,
                timeout=3,
            )
        else:
            qc = AsyncQdrantClient(
                host=_settings.qdrant_host,
                port=_settings.qdrant_port,
                timeout=3,
            )
        await qc.get_collections()
        await qc.close()
        results["qdrant"] = "ok"
    except Exception as exc:
        results["qdrant"] = f"error: {exc}"
        results["status"] = "degraded"

    return results


# ---------------------------------------------------------------------------
# Serve React build β€” SPA catch-all MUST be last (catches everything else)
# ---------------------------------------------------------------------------
import os as _os
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse as _FileResponse

_dist = _os.path.join(_os.path.dirname(__file__), "frontend", "dist")
if _os.path.exists(_dist):
    app.mount("/assets", StaticFiles(directory=_os.path.join(_dist, "assets")), name="assets")

    @app.get("/{full_path:path}", include_in_schema=False)
    async def serve_spa(full_path: str):
        file = _os.path.join(_dist, full_path)
        if _os.path.isfile(file):
            return _FileResponse(file)
        return _FileResponse(_os.path.join(_dist, "index.html"))