This repository has been archived on 2026-03-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Timmy-time-dashboard/src/dashboard/routes/chat_api.py
2026-03-05 19:45:38 -05:00

161 lines
5.6 KiB
Python

"""JSON REST API for mobile / external chat clients.
Provides the same chat experience as the HTMX dashboard but over
a JSON interface that React Native (or any HTTP client) can consume.
Endpoints:
POST /api/chat — send a message, get the agent's reply
POST /api/upload — upload a file attachment
GET /api/chat/history — retrieve recent chat history
DELETE /api/chat/history — clear chat history
"""
import logging
import os
import uuid
from datetime import datetime
from fastapi import APIRouter, File, HTTPException, Request, UploadFile
from fastapi.responses import JSONResponse
from config import settings
from dashboard.store import message_log
from timmy.session import chat as agent_chat
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api", tags=["chat-api"])
_UPLOAD_DIR = os.path.join("data", "chat-uploads")
_MAX_UPLOAD_SIZE = 50 * 1024 * 1024 # 50 MB
# ── POST /api/chat ────────────────────────────────────────────────────────────
@router.post("/chat")
async def api_chat(request: Request):
"""Accept a JSON chat payload and return the agent's reply.
Request body:
{"messages": [{"role": "user"|"assistant", "content": "..."}]}
Response:
{"reply": "...", "timestamp": "HH:MM:SS"}
"""
try:
body = await request.json()
except Exception:
return JSONResponse(status_code=400, content={"error": "Invalid JSON"})
messages = body.get("messages")
if not messages or not isinstance(messages, list):
return JSONResponse(status_code=400, content={"error": "messages array is required"})
# Extract the latest user message text
last_user_msg = None
for msg in reversed(messages):
if msg.get("role") == "user":
content = msg.get("content", "")
# Handle multimodal content arrays — extract text parts
if isinstance(content, list):
text_parts = [
p.get("text", "") for p in content
if isinstance(p, dict) and p.get("type") == "text"
]
last_user_msg = " ".join(text_parts).strip()
else:
last_user_msg = str(content).strip()
break
if not last_user_msg:
return JSONResponse(status_code=400, content={"error": "No user message found"})
timestamp = datetime.now().strftime("%H:%M:%S")
try:
# Inject context (same pattern as the HTMX chat handler in agents.py)
now = datetime.now()
context_prefix = (
f"[System: Current date/time is "
f"{now.strftime('%A, %B %d, %Y at %I:%M %p')}]\n"
f"[System: Mobile client]\n\n"
)
response_text = agent_chat(
context_prefix + last_user_msg,
session_id="mobile",
)
message_log.append(role="user", content=last_user_msg, timestamp=timestamp, source="api")
message_log.append(role="agent", content=response_text, timestamp=timestamp, source="api")
return {"reply": response_text, "timestamp": timestamp}
except Exception as exc:
error_msg = f"Agent is offline: {exc}"
logger.error("api_chat error: %s", exc)
message_log.append(role="user", content=last_user_msg, timestamp=timestamp, source="api")
message_log.append(role="error", content=error_msg, timestamp=timestamp, source="api")
return JSONResponse(
status_code=503,
content={"error": error_msg, "timestamp": timestamp},
)
# ── POST /api/upload ──────────────────────────────────────────────────────────
@router.post("/upload")
async def api_upload(file: UploadFile = File(...)):
"""Accept a file upload and return its URL.
Response:
{"url": "/static/chat-uploads/...", "fileName": "...", "mimeType": "..."}
"""
os.makedirs(_UPLOAD_DIR, exist_ok=True)
suffix = uuid.uuid4().hex[:12]
safe_name = os.path.basename(file.filename or "upload")
stored_name = f"{suffix}-{safe_name}"
file_path = os.path.join(_UPLOAD_DIR, stored_name)
contents = await file.read()
if len(contents) > _MAX_UPLOAD_SIZE:
raise HTTPException(status_code=413, detail="File too large (max 50 MB)")
with open(file_path, "wb") as f:
f.write(contents)
# Return a URL the mobile app can reference
url = f"/uploads/{stored_name}"
return {
"url": url,
"fileName": file.filename or "upload",
"mimeType": file.content_type or "application/octet-stream",
}
# ── GET /api/chat/history ────────────────────────────────────────────────────
@router.get("/chat/history")
async def api_chat_history():
"""Return the in-memory chat history as JSON."""
return {
"messages": [
{
"role": msg.role,
"content": msg.content,
"timestamp": msg.timestamp,
"source": msg.source,
}
for msg in message_log.all()
]
}
# ── DELETE /api/chat/history ──────────────────────────────────────────────────
@router.delete("/chat/history")
async def api_clear_history():
"""Clear the in-memory chat history."""
message_log.clear()
return {"success": True}