● Zion Boggan
repos/Darwin/cortex/main.py
zionboggan.com ↗
156 lines · python
History for this file →
1
"""Darwin Cortex Gateway - FastAPI app.
2
 
3
Port :9890 (HTTP API). MCP SSE shim on :9891 via mcp_server.py + mcp-proxy.
4
 
5
Start:
6
    uvicorn main:app --host 0.0.0.0 --port 9890
7
"""
8
from __future__ import annotations
9
 
10
import asyncio
11
import sys
12
import os
13
import time
14
from datetime import datetime, timezone
15
from typing import Optional
16
 
17
from fastapi import FastAPI
18
from fastapi.middleware.cors import CORSMiddleware
19
 
20
sys.path.insert(0, os.path.dirname(__file__))
21
 
22
from models import AskRequest, AskResponse, EngineStatus, HealthResponse, Hit, Receipt
23
from ranking import rank_hits, detect_conflicts
24
from synthesis import synthesize, warmup as synth_warmup
25
import state as live_state
26
from adapters.rag import RagAdapter
27
from adapters.cbm import CbmAdapter
28
from adapters.serena import SerenaAdapter
29
from adapters.memgraph import MemgraphAdapter
30
from adapters.inbox import InboxAdapter
31
from adapters.sources import source_statuses
32
 
33
app = FastAPI(title="Darwin Cortex", version="1.0.0")
34
 
35
app.add_middleware(
36
    CORSMiddleware,
37
    allow_origins=["*"],
38
    allow_methods=["*"],
39
    allow_headers=["*"],
40
)
41
 
42
ALL_ADAPTERS = {
43
    "rag": RagAdapter(),
44
    "cbm": CbmAdapter(),
45
    "serena": SerenaAdapter(),
46
    "memgraph": MemgraphAdapter(),
47
    "inbox": InboxAdapter(),
48
}
49
 
50
 
51
@app.on_event("startup")
52
async def _startup() -> None:
53
    """Best-effort: warm the LLM synthesizer onto the GPU so the first real
54
    /ask hits a resident model instead of a cold load."""
55
    asyncio.create_task(synth_warmup())
56
 
57
 
58
@app.get("/health", response_model=HealthResponse)
59
async def health() -> HealthResponse:
60
    """Whole-organism liveness: the 5 query engines PLUS the real ingestion
61
    data-sources (proxmox/white-pc/3060/browser-history/agents) with live
62
    per-source freshness from the homelab-rag manifest. Same EngineStatus
63
    schema for every entry; data-source entries are additive and never replace
64
    the engine entries, so the response stays backward-compatible."""
65
    async def probe(key: str, adapter) -> EngineStatus:
66
        _, status = await adapter.query("ping", k=1)
67
        return status
68
 
69
    engine_task = asyncio.gather(*(probe(k, v) for k, v in ALL_ADAPTERS.items()))
70
    sources_task = source_statuses()
71
    engine_statuses, data_sources = await asyncio.gather(
72
        engine_task, sources_task, return_exceptions=True,
73
    )
74
 
75
    if isinstance(engine_statuses, Exception):
76
        engine_statuses = []
77
    if isinstance(data_sources, Exception):
78
        data_sources = []
79
 
80
    all_statuses = list(engine_statuses) + list(data_sources)
81
    ok = any(s.status == "up" for s in all_statuses)
82
    return HealthResponse(engines=all_statuses, ok=ok)
83
 
84
 
85
@app.post("/ask", response_model=AskResponse)
86
async def ask(req: AskRequest) -> AskResponse:
87
    """Fan out to all engines, reconcile, synthesize."""
88
    t0 = time.monotonic()
89
 
90
    scope = req.scope or list(ALL_ADAPTERS.keys())
91
    adapters = {k: v for k, v in ALL_ADAPTERS.items() if k in scope}
92
 
93
    state_first = live_state.is_state_query(req.query)
94
    snapshot_task = asyncio.create_task(live_state.get_snapshot())
95
 
96
    async def run_adapter(key: str, adapter):
97
        return await adapter.query(req.query, k=req.k)
98
 
99
    tasks = {k: asyncio.create_task(run_adapter(k, v)) for k, v in adapters.items()}
100
    results = await asyncio.gather(*tasks.values(), return_exceptions=True)
101
 
102
    try:
103
        snapshot = await snapshot_task
104
        state_block = live_state.render_block(snapshot)
105
    except Exception:
106
        snapshot, state_block = None, None
107
 
108
    all_hits: list[Hit] = []
109
    engine_statuses: list[EngineStatus] = []
110
 
111
    for (key, _), result in zip(tasks.items(), results):
112
        if isinstance(result, Exception):
113
            engine_statuses.append(EngineStatus(engine=key, status="down"))
114
            continue
115
        hits, status = result
116
        all_hits.extend(hits)
117
        engine_statuses.append(status)
118
 
119
    ranked = rank_hits(all_hits, k=req.k * len(adapters))
120
    conflicts = detect_conflicts(ranked)
121
 
122
    answer: Optional[str] = None
123
    receipts = []
124
    if req.mode in ("chat", "both"):
125
        answer, receipts = await synthesize(
126
            req.query,
127
            ranked[:req.k],
128
            req.synthesize,
129
            live_state=state_block,
130
            state_first=state_first and state_block is not None,
131
        )
132
        if state_first and state_block:
133
            receipts = [Receipt(
134
                id="r0",
135
                source="live-state",
136
                path="cortex:/health + deck + darwin-term",
137
                last_seen=datetime.now(timezone.utc),
138
                confidence=1.0,
139
                snippet=state_block[:200],
140
            )] + list(receipts)
141
 
142
    took_ms = int((time.monotonic() - t0) * 1000)
143
 
144
    return AskResponse(
145
        answer=answer,
146
        receipts=receipts,
147
        hits=ranked[:req.k],
148
        conflicts=conflicts,
149
        engines=engine_statuses,
150
        took_ms=took_ms,
151
    )
152
 
153
 
154
@app.get("/")
155
async def root():
156
    return {"service": "darwin-cortex", "version": "1.0.0", "docs": "/docs"}