feat: API routes — speakers, rooms, search, RAG

Full implementations replacing stubs: speakers CRUD with utterance count,
rooms list + mute/unmute via pipeline, FTS5-based search, RAG /ask and
/summary endpoints. 15 route tests (8 new) all passing; 35 total.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-28 10:46:24 -05:00
parent 4def6a526c
commit 59014f6fe7
8 changed files with 221 additions and 4 deletions
+62 -2
View File
@@ -1,6 +1,66 @@
from fastapi import APIRouter
import sqlite3
import httpx
from typing import Optional
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from app.shared.config import Settings
def make_router(settings, db_factory):
class AskRequest(BaseModel):
question: str
date: Optional[str] = None
def make_router(settings: Settings, db_factory):
router = APIRouter(prefix="/api")
@router.post("/ask")
def ask(body: AskRequest):
db: sqlite3.Connection = db_factory()
rows = db.execute("""
SELECT u.start_time, u.transcript, r.name as room_name, s.name as speaker_name
FROM utterances u
JOIN rooms r ON u.room_id = r.id
LEFT JOIN speakers s ON u.speaker_id = s.id
WHERE date(u.start_time) = date(?)
ORDER BY u.start_time ASC
LIMIT 200
""", (body.date or "now",)).fetchall()
context_lines = [
f"[{r['room_name']} {r['start_time']}] {r['speaker_name'] or 'Unknown'}: {r['transcript']}"
for r in rows
]
context = "\n".join(context_lines) if context_lines else "(no transcripts for this date)"
prompt = (
f"You are a helpful assistant with access to household conversation transcripts.\n\n"
f"Transcripts:\n{context}\n\n"
f"Question: {body.question}\n\n"
f"Answer based only on the transcripts above. Cite the time and room when relevant."
)
resp = httpx.post(
f"{settings.ollama_url}/api/chat",
json={
"model": settings.ollama_model,
"messages": [{"role": "user", "content": prompt}],
"stream": False,
},
timeout=60.0,
)
resp.raise_for_status()
answer = resp.json()["message"]["content"]
return {"answer": answer, "context_count": len(rows)}
@router.get("/summary/{date}")
def get_summary(date: str):
db: sqlite3.Connection = db_factory()
row = db.execute(
"SELECT * FROM daily_summaries WHERE date=?", (date,)
).fetchone()
if not row:
raise HTTPException(404, f"No summary for {date}")
return dict(row)
return router
+32 -2
View File
@@ -1,6 +1,36 @@
from fastapi import APIRouter
import sqlite3
import httpx
from fastapi import APIRouter, HTTPException
from app.shared.config import Settings
def make_router(settings, db_factory):
def make_router(settings: Settings, db_factory):
router = APIRouter(prefix="/api/rooms")
@router.get("")
def list_rooms():
db: sqlite3.Connection = db_factory()
rows = db.execute(
"SELECT id, name, device_label, is_active, last_seen, created_at FROM rooms ORDER BY name"
).fetchall()
return [dict(r) for r in rows]
@router.post("/{room_id}/mute")
def mute_room(room_id: int):
db: sqlite3.Connection = db_factory()
row = db.execute("SELECT name FROM rooms WHERE id=?", (room_id,)).fetchone()
if not row:
raise HTTPException(404, "Room not found")
httpx.post(f"{settings.pipeline_internal_url}/internal/rooms/{row['name']}/mute")
return {"ok": True, "room": row["name"]}
@router.post("/{room_id}/unmute")
def unmute_room(room_id: int):
db: sqlite3.Connection = db_factory()
row = db.execute("SELECT name FROM rooms WHERE id=?", (room_id,)).fetchone()
if not row:
raise HTTPException(404, "Room not found")
httpx.post(f"{settings.pipeline_internal_url}/internal/rooms/{row['name']}/unmute")
return {"ok": True, "room": row["name"]}
return router
+20
View File
@@ -1,6 +1,26 @@
import sqlite3
from typing import Optional
from fastapi import APIRouter
def make_router(settings, db_factory):
router = APIRouter(prefix="/api")
@router.get("/search")
def search(q: str, date: Optional[str] = None):
if not q.strip():
return []
db: sqlite3.Connection = db_factory()
rows = db.execute("""
SELECT u.*, r.name as room_name, s.name as speaker_name
FROM utterances_fts fts
JOIN utterances u ON u.id = fts.rowid
JOIN rooms r ON u.room_id = r.id
LEFT JOIN speakers s ON u.speaker_id = s.id
WHERE utterances_fts MATCH ?
ORDER BY u.start_time DESC
LIMIT 100
""", (q.strip(),)).fetchall()
return [dict(r) for r in rows]
return router
+28
View File
@@ -1,6 +1,34 @@
import sqlite3
from fastapi import APIRouter
from pydantic import BaseModel
class CreateSpeakerRequest(BaseModel):
name: str
def make_router(settings, db_factory):
router = APIRouter(prefix="/api/speakers")
@router.get("")
def list_speakers():
db: sqlite3.Connection = db_factory()
rows = db.execute("""
SELECT s.id, s.name, s.created_at,
COUNT(u.id) as utterance_count
FROM speakers s
LEFT JOIN utterances u ON u.speaker_id = s.id
GROUP BY s.id
ORDER BY s.name
""").fetchall()
return [dict(r) for r in rows]
@router.post("")
def create_speaker(body: CreateSpeakerRequest):
db: sqlite3.Connection = db_factory()
cur = db.execute("INSERT INTO speakers (name) VALUES (?)", (body.name,))
db.commit()
row = db.execute("SELECT * FROM speakers WHERE id=?", (cur.lastrowid,)).fetchone()
return dict(row)
return router