117 lines
3.6 KiB
Python
117 lines
3.6 KiB
Python
"""Chat routes: POST /chat (REST fallback) and POST /chat/embed (text → vector).
|
|
|
|
WebSocket chat is handled by the unified device WS endpoint (/api/v1/ws/device).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import uuid
|
|
from typing import Literal
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from fastapi.responses import JSONResponse
|
|
from pydantic import BaseModel
|
|
|
|
from app.api.deps import get_current_user
|
|
from app.core.brief_agent import run_home_brief, run_project_brief
|
|
from app.core.deep_agent import run_home
|
|
from app.core.llm import embed
|
|
from app.core.memory_middleware import MemoryMiddleware
|
|
from app.db import async_session
|
|
from app.schemas import ChatRequest, UserProfile
|
|
|
|
router = APIRouter(prefix="/chat", tags=["chat"])
|
|
|
|
|
|
# ── Embed helpers ─────────────────────────────────────────────────────────
|
|
|
|
|
|
class _EmbedRequest(BaseModel):
|
|
text: str
|
|
|
|
|
|
class _EmbedResponse(BaseModel):
|
|
vector: list[float]
|
|
|
|
|
|
# ── Endpoints ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
@router.post("")
|
|
async def chat(
|
|
body: ChatRequest,
|
|
current_user: UserProfile = Depends(get_current_user),
|
|
) -> JSONResponse:
|
|
"""REST fallback for home chat when websocket streaming is unavailable."""
|
|
response = await run_home(
|
|
user_id=current_user.id,
|
|
message=body.message,
|
|
context=body.context.model_dump(),
|
|
)
|
|
return JSONResponse(content={"response": response})
|
|
|
|
|
|
class _BriefRequest(BaseModel):
|
|
mode: Literal["home", "project"]
|
|
project_id: str | None = None
|
|
|
|
|
|
class _BriefResponse(BaseModel):
|
|
response: str
|
|
|
|
|
|
@router.post("/brief", response_model=_BriefResponse)
|
|
async def brief(
|
|
body: _BriefRequest,
|
|
current_user: UserProfile = Depends(get_current_user),
|
|
) -> _BriefResponse:
|
|
"""REST fallback for brief when the device WebSocket is not ready."""
|
|
if body.mode == "project":
|
|
if not body.project_id:
|
|
raise HTTPException(status_code=422, detail="project_id required for project mode")
|
|
try:
|
|
uuid.UUID(body.project_id)
|
|
except ValueError:
|
|
raise HTTPException(status_code=422, detail="project_id must be a valid UUID")
|
|
|
|
request_id = str(uuid.uuid4())
|
|
async with async_session() as db:
|
|
memory = MemoryMiddleware(db)
|
|
memory_context = await memory.enrich_context(
|
|
current_user.id,
|
|
"",
|
|
trace_id=request_id,
|
|
session_id=request_id,
|
|
)
|
|
|
|
context: dict = {
|
|
"_debug": {"request_id": request_id, "user_id": current_user.id},
|
|
**memory_context,
|
|
}
|
|
|
|
chunks: list[str] = []
|
|
if body.mode == "project":
|
|
stream = run_project_brief(current_user.id, body.project_id, context) # type: ignore[arg-type]
|
|
else:
|
|
stream = run_home_brief(current_user.id, context)
|
|
|
|
async for event_type, data in stream:
|
|
if event_type == "token" and data:
|
|
chunks.append(str(data))
|
|
|
|
return _BriefResponse(response="".join(chunks))
|
|
|
|
|
|
@router.post("/embed", response_model=_EmbedResponse)
|
|
async def embed_text(
|
|
body: _EmbedRequest,
|
|
current_user: UserProfile = Depends(get_current_user),
|
|
) -> _EmbedResponse:
|
|
"""Generate a 1536-dim embedding vector for the given text.
|
|
|
|
Uses ``text-embedding-3-small`` via OpenAI. Auth required (JWT).
|
|
Used by Electron (vectordb.ts) for local note search.
|
|
"""
|
|
vector = await embed(body.text)
|
|
return _EmbedResponse(vector=vector)
|