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}