● Zion Boggan
repos/Darwin/cortex/state.py
zionboggan.com ↗
373 lines · python
History for this file →
1
"""Darwin Cortex - LIVE-STATE awareness.
2
 
3
Per Darwin's source-of-truth hierarchy, LIVE INFRA is the HIGHEST authority
4
(live infra > docs). This module builds a compact, structured snapshot of the
5
organism's REAL current state - pulled live, not from RAG documents - so that
6
status/state questions ("is white-pc down?", "what's stale?", "what are my
7
agents doing?", "how many live sessions?") get answered from ground truth.
8
 
9
Sources (all reachable from the Cortex host):
10
  1. Cortex /health (SELF)        - the 10 nodes: 5 engines + 5 data-sources
11
                                     with per-source freshness. Called in-process
12
                                     (build_health_snapshot) - no self-HTTP.
13
  2. Deck   :9893/health + /tree  - active agents, by host/model, what they are.
14
  3. darwin-term :9894/sessions   - live tmux sessions across ct215/241/247.
15
 
16
Every source is wrapped in try/except + a short timeout, so a dead source
17
degrades the snapshot gracefully and NEVER breaks /ask. The whole snapshot is
18
cached for a few seconds so repeated /ask calls don't re-probe every time.
19
 
20
The snapshot is exposed two ways:
21
  - as_context_block(): a compact top-authority text block fed to the synthesizer
22
  - state-intent detection: is_state_query() routes status questions to lead
23
    with this live snapshot instead of RAG.
24
"""
25
from __future__ import annotations
26
 
27
import os
28
import re
29
import time
30
from datetime import datetime, timezone
31
from typing import Any, Optional
32
 
33
import httpx
34
 
35
DECK_URL = os.environ.get("DECK_URL", "http://REDACTED-IP:9893")
36
DARWIN_TERM_URL = os.environ.get("DARWIN_TERM_URL", "http://REDACTED-IP:9894")
37
DARWIN_TERM_TOKEN = os.environ.get("DARWIN_TERM_TOKEN", "")
38
RAG_BRIDGE_URL = os.environ.get("RAG_BRIDGE_URL", "http://REDACTED-IP:9875")
39
 
40
CACHE_TTL_S = float(os.environ.get("DARWIN_STATE_CACHE_TTL", "8"))
41
_PROBE_TIMEOUT = 3.0
42
 
43
STALE_THRESHOLD_H = float(os.environ.get("SOURCE_STALE_THRESHOLD_H", "12"))
44
 
45
_cache: dict[str, Any] = {"snapshot": None, "ts": 0.0}
46
 
47
 
48
_STATE_PATTERNS = [
49
    re.compile(r"\b(is|are|was|were)\b.*\b(up|down|live|stale|running|active|asleep|alive|dead|online|offline|reachable)\b", re.I),
50
    re.compile(r"\bwhat('?s| is| are)\b.*\b(stale|status|happening|going on|running|active|live|down|up)\b", re.I),
51
    re.compile(r"\bwhat\b.*\b(agents?|sessions?|engines?|sources?|nodes?|containers?|cts?)\b.*\b(doing|running|active|up|down|live|stale)\b", re.I),
52
    re.compile(r"\bhow many\b.*\b(sessions?|agents?|engines?|sources?|nodes?|containers?|cts?)\b", re.I),
53
    re.compile(r"\b(status|health|state)\b.*\b(infra|infrastructure|ingestion|ingest|organism|system|darwin|engines?|sources?|agents?|sessions?)\b", re.I),
54
    re.compile(r"\bwhat are my\b.*\b(agents?|sessions?|engines?|sources?)\b", re.I),
55
    re.compile(r"\b(anything|what'?s?)\s+(stale|down|broken|offline|asleep)\b", re.I),
56
]
57
 
58
 
59
def is_state_query(query: str) -> bool:
60
    """True when the query is asking about live state/status, not knowledge."""
61
    if not query:
62
        return False
63
    q = query.strip()
64
    return any(p.search(q) for p in _STATE_PATTERNS)
65
 
66
 
67
async def _build_health_snapshot() -> dict | None:
68
    """Call the Cortex /health builder in-process (no self-HTTP) so the live
69
    organism state (engines + data-source freshness, incl. white-pc DOWN and
70
    ct243-media stale) is part of the snapshot."""
71
    try:
72
        from main import health as _health
73
        resp = await _health()
74
        engines = []
75
        for s in resp.engines:
76
            engines.append({
77
                "name": s.engine,
78
                "status": s.status,
79
                "staleness_h": s.staleness_h,
80
                "last_ingest": s.last_ingest.isoformat() if s.last_ingest else None,
81
                "ms": s.ms,
82
            })
83
        return {"ok": resp.ok, "engines": engines}
84
    except Exception:
85
        return None
86
 
87
 
88
async def _probe_deck() -> dict | None:
89
    try:
90
        async with httpx.AsyncClient(timeout=_PROBE_TIMEOUT) as client:
91
            h = await client.get(f"{DECK_URL}/health")
92
            h.raise_for_status()
93
            health = h.json()
94
            agents: list[dict] = []
95
            try:
96
                t = await client.get(f"{DECK_URL}/tree")
97
                t.raise_for_status()
98
                nodes = t.json().get("nodes", [])
99
                for n in nodes:
100
                    if not isinstance(n, dict):
101
                        continue
102
                    if n.get("status") != "running":
103
                        continue
104
                    model = n.get("model", "")
105
                    if model in ("system", "orchestrator"):
106
                        continue
107
                    agents.append({
108
                        "host": n.get("host"),
109
                        "model": model,
110
                        "label": (n.get("label") or "")[:90],
111
                        "tool": n.get("current_tool"),
112
                    })
113
            except Exception:
114
                pass
115
            return {
116
                "active_agents": int(health.get("active_agents", 0)),
117
                "total_agents": int(health.get("total_agents", 0)),
118
                "error_agents": int(health.get("error_agents", 0)),
119
                "running": agents,
120
            }
121
    except Exception:
122
        return None
123
 
124
 
125
async def _probe_ingestion() -> dict | None:
126
    """Read ALL homelab-rag ingestion sources directly from the bridge /sources.
127
 
128
    The Cortex /health organism only surfaces 5 mapped nodes, so a stale source
129
    that isn't a mapped organism node (e.g. ct243-media, days stale) would never
130
    appear. This probe captures the FULL ingestion picture so 'what's stale'
131
    names every stale/down source by its real source_id.
132
    """
133
    try:
134
        async with httpx.AsyncClient(timeout=_PROBE_TIMEOUT) as client:
135
            r = await client.get(f"{RAG_BRIDGE_URL}/sources")
136
            r.raise_for_status()
137
            payload = r.json()
138
        sources = payload.get("sources", []) if isinstance(payload, dict) else []
139
        total = 0
140
        stale: list[dict] = []
141
        freshest = None
142
        for s in sources:
143
            if not isinstance(s, dict):
144
                continue
145
            total += 1
146
            sh = s.get("staleness_h")
147
            if sh is not None:
148
                if freshest is None or sh < freshest:
149
                    freshest = sh
150
                if sh > STALE_THRESHOLD_H:
151
                    stale.append({"id": s.get("source_id"), "staleness_h": sh})
152
        stale.sort(key=lambda x: -(x["staleness_h"] or 0))
153
        return {"total": total, "stale": stale, "freshest_h": freshest}
154
    except Exception:
155
        return None
156
 
157
 
158
async def _probe_term() -> dict | None:
159
    if not DARWIN_TERM_TOKEN:
160
        return None
161
    try:
162
        async with httpx.AsyncClient(timeout=_PROBE_TIMEOUT) as client:
163
            r = await client.get(
164
                f"{DARWIN_TERM_URL}/sessions",
165
                params={"token": DARWIN_TERM_TOKEN},
166
            )
167
            r.raise_for_status()
168
            sessions = r.json().get("sessions", [])
169
            by_host: dict[str, int] = {}
170
            by_agent: dict[str, int] = {}
171
            for s in sessions:
172
                if not isinstance(s, dict):
173
                    continue
174
                by_host[s.get("target", "?")] = by_host.get(s.get("target", "?"), 0) + 1
175
                by_agent[s.get("agent", "?")] = by_agent.get(s.get("agent", "?"), 0) + 1
176
            return {
177
                "count": len(sessions),
178
                "by_host": by_host,
179
                "by_agent": by_agent,
180
            }
181
    except Exception:
182
        return None
183
 
184
 
185
async def get_snapshot() -> dict:
186
    """Return the live-state snapshot, cached for CACHE_TTL_S seconds."""
187
    now = time.monotonic()
188
    snap = _cache.get("snapshot")
189
    if snap is not None and (now - _cache["ts"]) < CACHE_TTL_S:
190
        return snap
191
 
192
    import asyncio
193
    health, deck, term, ingestion = await asyncio.gather(
194
        _build_health_snapshot(), _probe_deck(), _probe_term(), _probe_ingestion(),
195
        return_exceptions=True,
196
    )
197
    snap = {
198
        "ts": datetime.now(timezone.utc).isoformat(),
199
        "health": health if isinstance(health, dict) else None,
200
        "deck": deck if isinstance(deck, dict) else None,
201
        "term": term if isinstance(term, dict) else None,
202
        "ingestion": ingestion if isinstance(ingestion, dict) else None,
203
    }
204
    _cache["snapshot"] = snap
205
    _cache["ts"] = now
206
    return snap
207
 
208
 
209
def _render_health(health: dict | None) -> list[str]:
210
    if not health:
211
        return ["Organism /health: UNAVAILABLE (could not read live health)."]
212
    lines: list[str] = []
213
    engines = health.get("engines", [])
214
    up = [e for e in engines if e.get("status") == "up"]
215
    down = [e for e in engines if e.get("status") != "up"]
216
    stale = [
217
        e for e in engines
218
        if e.get("staleness_h") is not None and e["staleness_h"] > STALE_THRESHOLD_H
219
    ]
220
    lines.append(
221
        f"Organism: {len(up)}/{len(engines)} nodes up. "
222
        f"Down: {', '.join(e['name'] for e in down) if down else 'none'}."
223
    )
224
    if stale:
225
        stale_desc = ", ".join(
226
            f"{e['name']} (~{e['staleness_h']:.0f}h stale)" for e in stale
227
        )
228
        lines.append(f"Stale data-sources (>{STALE_THRESHOLD_H:.0f}h): {stale_desc}.")
229
    else:
230
        lines.append("No data-source is stale beyond threshold.")
231
    detail = []
232
    for e in engines:
233
        bit = e["name"] + "=" + e["status"]
234
        if e.get("staleness_h") is not None:
235
            bit += f"({e['staleness_h']:.1f}h)"
236
        detail.append(bit)
237
    lines.append("Nodes: " + ", ".join(detail) + ".")
238
    return lines
239
 
240
 
241
def _render_ingestion(ing: dict | None) -> list[str]:
242
    if not ing:
243
        return ["Ingestion sources: UNAVAILABLE."]
244
    stale = ing.get("stale") or []
245
    total = ing.get("total", 0)
246
    if not stale:
247
        return [f"Ingestion: all {total} sources fresh (freshest ~{ing.get('freshest_h')}h)."]
248
    desc = ", ".join(
249
        f"{s['id']} (~{s['staleness_h']:.0f}h stale)" for s in stale
250
    )
251
    return [f"Ingestion: {len(stale)}/{total} sources STALE (>{STALE_THRESHOLD_H:.0f}h): {desc}."]
252
 
253
 
254
def _render_deck(deck: dict | None) -> list[str]:
255
    if not deck:
256
        return ["Agents (Deck): UNAVAILABLE."]
257
    lines = [
258
        f"Active agents right now: {deck.get('active_agents', 0)} "
259
        f"(total ever: {deck.get('total_agents', 0)}, errored: {deck.get('error_agents', 0)})."
260
    ]
261
    running = deck.get("running") or []
262
    if running:
263
        for a in running[:6]:
264
            label = a.get("label") or "(no label)"
265
            tool = f", using {a['tool']}" if a.get("tool") else ""
266
            lines.append(
267
                f"  - {a.get('host', '?')}/{a.get('model', '?')}: {label}{tool}"
268
            )
269
    return lines
270
 
271
 
272
def _render_term(term: dict | None) -> list[str]:
273
    if not term:
274
        return ["Live tmux sessions (darwin-term): UNAVAILABLE."]
275
    by_host = ", ".join(f"{h}:{n}" for h, n in (term.get("by_host") or {}).items())
276
    by_agent = ", ".join(f"{a}:{n}" for a, n in (term.get("by_agent") or {}).items())
277
    return [
278
        f"Live tmux sessions: {term.get('count', 0)} total"
279
        + (f" (by host: {by_host})" if by_host else "")
280
        + (f" (by type: {by_agent})" if by_agent else "")
281
        + "."
282
    ]
283
 
284
 
285
def render_block(snap: dict) -> str:
286
    """Compact human/LLM-readable live-state block (top authority)."""
287
    lines = ["LIVE STATE (authoritative - live infra outranks docs):"]
288
    lines += _render_health(snap.get("health"))
289
    lines += _render_ingestion(snap.get("ingestion"))
290
    lines += _render_deck(snap.get("deck"))
291
    lines += _render_term(snap.get("term"))
292
    return "\n".join(lines)
293
 
294
 
295
async def as_context_block() -> str:
296
    """Convenience: snapshot -> rendered block."""
297
    return render_block(await get_snapshot())
298
 
299
 
300
def deterministic_state_answer(query: str, snap: dict) -> str:
301
    """A direct, LLM-free answer for state questions, built from the snapshot.
302
 
303
    Used (a) as the deterministic fallback when the LLM is cold, and (b) to LEAD
304
    the answer for state-intent queries. Tries to answer the specific question;
305
    always appends the compact state block as evidence.
306
    """
307
    q = query.lower()
308
    health = snap.get("health") or {}
309
    engines = {e["name"]: e for e in (health.get("engines") or [])}
310
    lead: Optional[str] = None
311
 
312
    alias = {
313
        "white pc": "white-pc", "white-pc": "white-pc", "whitepc": "white-pc",
314
        "3060": "3060", "rtx": "3060", "gpu": "3060",
315
        "proxmox": "proxmox", "pve": "proxmox",
316
        "browser": "browser-history", "browser history": "browser-history",
317
    }
318
    target = None
319
    for k, v in alias.items():
320
        if k in q:
321
            target = v
322
            break
323
    if target and target in engines:
324
        e = engines[target]
325
        if e["status"] != "up":
326
            lead = f"Yes - {target} is DOWN (not ingesting / no live data)."
327
        else:
328
            sh = e.get("staleness_h")
329
            fresh = f"last ingest ~{sh:.1f}h ago" if sh is not None else "live"
330
            lead = f"No - {target} is UP ({fresh})."
331
 
332
    if lead is None and ("stale" in q or "out of date" in q or "outdated" in q):
333
        ing = snap.get("ingestion") or {}
334
        stale_src = ing.get("stale") or []
335
        down = [e for e in (health.get("engines") or []) if e.get("status") != "up"]
336
        parts = []
337
        if stale_src:
338
            parts.append(
339
                "stale ingestion: " + ", ".join(
340
                    f"{s['id']} (~{s['staleness_h']:.0f}h)" for s in stale_src
341
                )
342
            )
343
        if down:
344
            parts.append("down/not-ingesting: " + ", ".join(e["name"] for e in down))
345
        lead = ("What's stale right now - " + "; ".join(parts) + ".") if parts \
346
            else "Nothing is stale right now; all data-sources are fresh."
347
 
348
    if lead is None and "session" in q:
349
        term = snap.get("term")
350
        if term:
351
            by_host = ", ".join(f"{h}:{n}" for h, n in (term.get("by_host") or {}).items())
352
            lead = (
353
                f"You have {term.get('count', 0)} live tmux sessions"
354
                + (f" ({by_host})" if by_host else "") + "."
355
            )
356
 
357
    if lead is None and "agent" in q:
358
        deck = snap.get("deck")
359
        if deck:
360
            running = deck.get("running") or []
361
            if running:
362
                summ = "; ".join(
363
                    f"{a.get('host', '?')}/{a.get('model', '?')} - {(a.get('label') or '')[:60]}"
364
                    for a in running[:4]
365
                )
366
                lead = f"{deck.get('active_agents', 0)} agents active right now: {summ}."
367
            else:
368
                lead = f"{deck.get('active_agents', 0)} agents active right now."
369
 
370
    block = render_block(snap)
371
    if lead:
372
        return f"{lead}\n\n{block}"
373
    return block