Files
project-lyra/intake/intake.py
2025-11-28 18:05:59 -05:00

161 lines
5.4 KiB
Python

from fastapi import FastAPI, Body, Query, BackgroundTasks
from collections import deque
from datetime import datetime
from uuid import uuid4
import requests
import os
import sys
# ─────────────────────────────
# Config
# ─────────────────────────────
SUMMARY_MODEL = os.getenv("SUMMARY_MODEL_NAME", "mistral-7b-instruct-v0.2.Q4_K_M.gguf")
SUMMARY_URL = os.getenv("SUMMARY_API_URL", "http://localhost:8080/v1/completions")
SUMMARY_MAX_TOKENS = int(os.getenv("SUMMARY_MAX_TOKENS", "200"))
SUMMARY_TEMPERATURE = float(os.getenv("SUMMARY_TEMPERATURE", "0.3"))
NEOMEM_API = os.getenv("NEOMEM_API")
NEOMEM_KEY = os.getenv("NEOMEM_KEY")
# ─────────────────────────────
# App + session buffer
# ─────────────────────────────
app = FastAPI()
SESSIONS = {}
@app.on_event("startup")
def banner():
print("🧩 Intake v0.2 booting...")
print(f" Model: {SUMMARY_MODEL}")
print(f" API: {SUMMARY_URL}")
sys.stdout.flush()
# ─────────────────────────────
# Helper: summarize exchanges
# ─────────────────────────────
def llm(prompt: str):
try:
resp = requests.post(
SUMMARY_URL,
json={
"model": SUMMARY_MODEL,
"prompt": prompt,
"max_tokens": SUMMARY_MAX_TOKENS,
"temperature": SUMMARY_TEMPERATURE,
},
timeout=30,
)
resp.raise_for_status()
return resp.json().get("choices", [{}])[0].get("text", "").strip()
except Exception as e:
return f"[Error summarizing: {e}]"
def summarize_simple(exchanges):
"""Simple factual summary of recent exchanges."""
text = ""
for e in exchanges:
text += f"User: {e['user_msg']}\nAssistant: {e['assistant_msg']}\n\n"
prompt = f"""
Summarize the following conversation between Brian (user) and Lyra (assistant).
Focus only on factual content. Avoid names, examples, story tone, or invented details.
{text}
Summary:
"""
return llm(prompt)
# ─────────────────────────────
# NeoMem push
# ─────────────────────────────
def push_to_neomem(summary: str, session_id: str):
if not NEOMEM_API:
return
headers = {"Content-Type": "application/json"}
if NEOMEM_KEY:
headers["Authorization"] = f"Bearer {NEOMEM_KEY}"
payload = {
"messages": [{"role": "assistant", "content": summary}],
"user_id": "brian",
"metadata": {
"source": "intake",
"session_id": session_id
}
}
try:
requests.post(
f"{NEOMEM_API}/memories",
json=payload,
headers=headers,
timeout=20
).raise_for_status()
print(f"🧠 NeoMem updated for {session_id}")
except Exception as e:
print(f"NeoMem push failed: {e}")
# ─────────────────────────────
# Background summarizer
# ─────────────────────────────
def bg_summarize(session_id: str):
try:
hopper = SESSIONS.get(session_id)
if not hopper:
return
buf = list(hopper["buffer"])
summary = summarize_simple(buf)
push_to_neomem(summary, session_id)
print(f"🧩 Summary generated for {session_id}")
except Exception as e:
print(f"Summarizer error: {e}")
# ─────────────────────────────
# Routes
# ─────────────────────────────
@app.post("/add_exchange")
def add_exchange(exchange: dict = Body(...), background_tasks: BackgroundTasks = None):
session_id = exchange.get("session_id") or f"sess-{uuid4().hex[:8]}"
exchange["session_id"] = session_id
exchange["timestamp"] = datetime.now().isoformat()
if session_id not in SESSIONS:
SESSIONS[session_id] = {
"buffer": deque(maxlen=200),
"created_at": datetime.now()
}
print(f"🆕 Hopper created: {session_id}")
SESSIONS[session_id]["buffer"].append(exchange)
if background_tasks:
background_tasks.add_task(bg_summarize, session_id)
print(f"⏩ Summarization queued for {session_id}")
return {"ok": True, "session_id": session_id}
@app.post("/close_session/{session_id}")
def close_session(session_id: str):
if session_id in SESSIONS:
del SESSIONS[session_id]
return {"ok": True, "closed": session_id}
@app.get("/summaries")
def get_summary(session_id: str = Query(...)):
hopper = SESSIONS.get(session_id)
if not hopper:
return {"summary_text": "(none)", "session_id": session_id}
summary = summarize_simple(list(hopper["buffer"]))
return {"summary_text": summary, "session_id": session_id}
@app.get("/health")
def health():
return {"ok": True, "model": SUMMARY_MODEL, "url": SUMMARY_URL}