[claude] feat: Nexus — persistent conversational awareness space with live memory (#1208) (#1211)

This commit is contained in:
2026-03-23 22:34:48 +00:00
parent 25157a71a8
commit 3217c32356
9 changed files with 568 additions and 0 deletions

View File

@@ -42,6 +42,7 @@ from dashboard.routes.hermes import router as hermes_router
from dashboard.routes.loop_qa import router as loop_qa_router
from dashboard.routes.memory import router as memory_router
from dashboard.routes.mobile import router as mobile_router
from dashboard.routes.nexus import router as nexus_router
from dashboard.routes.models import api_router as models_api_router
from dashboard.routes.models import router as models_router
from dashboard.routes.quests import router as quests_router
@@ -652,6 +653,7 @@ app.include_router(tools_router)
app.include_router(spark_router)
app.include_router(discord_router)
app.include_router(memory_router)
app.include_router(nexus_router)
app.include_router(grok_router)
app.include_router(models_router)
app.include_router(models_api_router)

View File

@@ -0,0 +1,168 @@
"""Nexus — Timmy's persistent conversational awareness space.
A conversational-only interface where Timmy maintains live memory context.
No tool use; pure conversation with memory integration and a teaching panel.
Routes:
GET /nexus — render nexus page with live memory sidebar
POST /nexus/chat — send a message; returns HTMX partial
POST /nexus/teach — inject a fact into Timmy's live memory
DELETE /nexus/history — clear the nexus conversation history
"""
import asyncio
import logging
from datetime import datetime, timezone
from fastapi import APIRouter, Form, Request
from fastapi.responses import HTMLResponse
from dashboard.templating import templates
from timmy.memory_system import (
get_memory_stats,
recall_personal_facts_with_ids,
search_memories,
store_personal_fact,
)
from timmy.session import _clean_response, chat, reset_session
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/nexus", tags=["nexus"])
_NEXUS_SESSION_ID = "nexus"
_MAX_MESSAGE_LENGTH = 10_000
# In-memory conversation log for the Nexus session (mirrors chat store pattern
# but is scoped to the Nexus so it won't pollute the main dashboard history).
_nexus_log: list[dict] = []
def _ts() -> str:
return datetime.now(timezone.utc).strftime("%H:%M:%S")
def _append_log(role: str, content: str) -> None:
_nexus_log.append({"role": role, "content": content, "timestamp": _ts()})
# Keep last 200 exchanges to bound memory usage
if len(_nexus_log) > 200:
del _nexus_log[:-200]
@router.get("", response_class=HTMLResponse)
async def nexus_page(request: Request):
"""Render the Nexus page with live memory context."""
stats = get_memory_stats()
facts = recall_personal_facts_with_ids()[:8]
return templates.TemplateResponse(
request,
"nexus.html",
{
"page_title": "Nexus",
"messages": list(_nexus_log),
"stats": stats,
"facts": facts,
},
)
@router.post("/chat", response_class=HTMLResponse)
async def nexus_chat(request: Request, message: str = Form(...)):
"""Conversational-only chat routed through the Nexus session.
Does not invoke tool-use approval flow — pure conversation with memory
context injected from Timmy's live memory store.
"""
message = message.strip()
if not message:
return HTMLResponse("")
if len(message) > _MAX_MESSAGE_LENGTH:
return templates.TemplateResponse(
request,
"partials/nexus_message.html",
{
"user_message": message[:80] + "",
"response": None,
"error": "Message too long (max 10 000 chars).",
"timestamp": _ts(),
"memory_hits": [],
},
)
ts = _ts()
# Fetch semantically relevant memories to surface in the sidebar
try:
memory_hits = await asyncio.to_thread(
search_memories, query=message, limit=4
)
except Exception as exc:
logger.warning("Nexus memory search failed: %s", exc)
memory_hits = []
# Conversational response — no tool approval flow
response_text: str | None = None
error_text: str | None = None
try:
raw = await chat(message, session_id=_NEXUS_SESSION_ID)
response_text = _clean_response(raw)
except Exception as exc:
logger.error("Nexus chat error: %s", exc)
error_text = "Timmy is unavailable right now. Check that Ollama is running."
_append_log("user", message)
if response_text:
_append_log("assistant", response_text)
return templates.TemplateResponse(
request,
"partials/nexus_message.html",
{
"user_message": message,
"response": response_text,
"error": error_text,
"timestamp": ts,
"memory_hits": memory_hits,
},
)
@router.post("/teach", response_class=HTMLResponse)
async def nexus_teach(request: Request, fact: str = Form(...)):
"""Inject a fact into Timmy's live memory from the Nexus teaching panel."""
fact = fact.strip()
if not fact:
return HTMLResponse("")
try:
await asyncio.to_thread(store_personal_fact, fact)
facts = await asyncio.to_thread(recall_personal_facts_with_ids)
facts = facts[:8]
except Exception as exc:
logger.error("Nexus teach error: %s", exc)
facts = []
return templates.TemplateResponse(
request,
"partials/nexus_facts.html",
{"facts": facts, "taught": fact},
)
@router.delete("/history", response_class=HTMLResponse)
async def nexus_clear_history(request: Request):
"""Clear the Nexus conversation history."""
_nexus_log.clear()
reset_session(session_id=_NEXUS_SESSION_ID)
return templates.TemplateResponse(
request,
"partials/nexus_message.html",
{
"user_message": None,
"response": "Nexus conversation cleared.",
"error": None,
"timestamp": _ts(),
"memory_hits": [],
},
)

View File

@@ -67,6 +67,7 @@
<div class="mc-nav-dropdown">
<button class="mc-test-link mc-dropdown-toggle" aria-expanded="false">INTEL &#x25BE;</button>
<div class="mc-dropdown-menu">
<a href="/nexus" class="mc-test-link">NEXUS</a>
<a href="/spark/ui" class="mc-test-link">SPARK</a>
<a href="/memory" class="mc-test-link">MEMORY</a>
<a href="/marketplace/ui" class="mc-test-link">MARKET</a>

View File

@@ -0,0 +1,122 @@
{% extends "base.html" %}
{% block title %}Nexus{% endblock %}
{% block extra_styles %}{% endblock %}
{% block content %}
<div class="container-fluid nexus-layout py-3">
<div class="nexus-header mb-3">
<div class="nexus-title">// NEXUS</div>
<div class="nexus-subtitle">
Persistent conversational awareness &mdash; always present, always learning.
</div>
</div>
<div class="nexus-grid">
<!-- ── LEFT: Conversation ────────────────────────────────── -->
<div class="nexus-chat-col">
<div class="card mc-panel nexus-chat-panel">
<div class="card-header mc-panel-header d-flex justify-content-between align-items-center">
<span>// CONVERSATION</span>
<button class="mc-btn mc-btn-sm"
hx-delete="/nexus/history"
hx-target="#nexus-chat-log"
hx-swap="beforeend"
hx-confirm="Clear nexus conversation?">
CLEAR
</button>
</div>
<div class="card-body p-2" id="nexus-chat-log">
{% for msg in messages %}
<div class="chat-message {{ 'user' if msg.role == 'user' else 'agent' }}">
<div class="msg-meta">
{{ 'YOU' if msg.role == 'user' else 'TIMMY' }} // {{ msg.timestamp }}
</div>
<div class="msg-body {% if msg.role == 'assistant' %}timmy-md{% endif %}">
{{ msg.content | e }}
</div>
</div>
{% else %}
<div class="nexus-empty-state">
Nexus is ready. Start a conversation — memories will surface in real time.
</div>
{% endfor %}
</div>
<div class="card-footer p-2">
<form hx-post="/nexus/chat"
hx-target="#nexus-chat-log"
hx-swap="beforeend"
hx-on::after-request="this.reset(); document.getElementById('nexus-chat-log').scrollTop = 999999;">
<div class="d-flex gap-2">
<input type="text"
name="message"
id="nexus-input"
class="mc-search-input flex-grow-1"
placeholder="Talk to Timmy..."
autocomplete="off"
required>
<button type="submit" class="mc-btn mc-btn-primary">SEND</button>
</div>
</form>
</div>
</div>
</div>
<!-- ── RIGHT: Memory sidebar ─────────────────────────────── -->
<div class="nexus-sidebar-col">
<!-- Live memory context (updated with each response) -->
<div class="card mc-panel nexus-memory-panel mb-3">
<div class="card-header mc-panel-header">
<span>// LIVE MEMORY</span>
<span class="badge ms-2" style="background:var(--purple-dim); color:var(--purple);">
{{ stats.total_entries }} stored
</span>
</div>
<div class="card-body p-2">
<div id="nexus-memory-panel" class="nexus-memory-hits">
<div class="nexus-memory-label">Relevant memories appear here as you chat.</div>
</div>
</div>
</div>
<!-- Teaching panel -->
<div class="card mc-panel nexus-teach-panel">
<div class="card-header mc-panel-header">// TEACH TIMMY</div>
<div class="card-body p-2">
<form hx-post="/nexus/teach"
hx-target="#nexus-teach-response"
hx-swap="innerHTML"
hx-on::after-request="this.reset()">
<div class="d-flex gap-2 mb-2">
<input type="text"
name="fact"
class="mc-search-input flex-grow-1"
placeholder="e.g. I prefer dark themes"
required>
<button type="submit" class="mc-btn mc-btn-primary">TEACH</button>
</div>
</form>
<div id="nexus-teach-response"></div>
<div class="nexus-facts-header mt-3">// KNOWN FACTS</div>
<ul class="nexus-facts-list" id="nexus-facts-list">
{% for fact in facts %}
<li class="nexus-fact-item">{{ fact.content | e }}</li>
{% else %}
<li class="nexus-fact-empty">No personal facts stored yet.</li>
{% endfor %}
</ul>
</div>
</div>
</div><!-- /sidebar -->
</div><!-- /nexus-grid -->
</div>
{% endblock %}

View File

@@ -0,0 +1,12 @@
{% if taught %}
<div class="nexus-taught-confirm">
✓ Taught: <em>{{ taught | e }}</em>
</div>
{% endif %}
<ul class="nexus-facts-list" id="nexus-facts-list" hx-swap-oob="true">
{% for fact in facts %}
<li class="nexus-fact-item">{{ fact.content | e }}</li>
{% else %}
<li class="nexus-fact-empty">No facts stored yet.</li>
{% endfor %}
</ul>

View File

@@ -0,0 +1,36 @@
{% if user_message %}
<div class="chat-message user">
<div class="msg-meta">YOU // {{ timestamp }}</div>
<div class="msg-body">{{ user_message | e }}</div>
</div>
{% endif %}
{% if response %}
<div class="chat-message agent">
<div class="msg-meta">TIMMY // {{ timestamp }}</div>
<div class="msg-body timmy-md">{{ response | e }}</div>
</div>
<script>
(function() {
var el = document.currentScript.previousElementSibling.querySelector('.timmy-md');
if (el && typeof marked !== 'undefined' && typeof DOMPurify !== 'undefined') {
el.innerHTML = DOMPurify.sanitize(marked.parse(el.textContent));
}
})();
</script>
{% elif error %}
<div class="chat-message error-msg">
<div class="msg-meta">SYSTEM // {{ timestamp }}</div>
<div class="msg-body">{{ error | e }}</div>
</div>
{% endif %}
{% if memory_hits %}
<div class="nexus-memory-hits" id="nexus-memory-panel" hx-swap-oob="true">
<div class="nexus-memory-label">// LIVE MEMORY CONTEXT</div>
{% for hit in memory_hits %}
<div class="nexus-memory-hit">
<span class="nexus-memory-type">{{ hit.memory_type }}</span>
<span class="nexus-memory-content">{{ hit.content | e }}</span>
</div>
{% endfor %}
</div>
{% endif %}