Add War Room voice meeting, fix chat overlay, and ship 5 cyan UI themes.
Voice Room lets humans talk with agents via Whisper/TTS; chat typing no longer blocked by a full-screen overlay, attachments/avatars are larger, and Settings exposes five vivid mid-dark cyan/blue themes for the whole cockpit.
This commit is contained in:
@@ -573,10 +573,11 @@ async def warroom_open_dm(agent_key: str) -> dict[str, Any]:
|
||||
@router.get("/warroom/rooms/{room_id}/messages")
|
||||
async def warroom_list_messages(room_id: int, limit: int = 100, after_id: int | None = None) -> dict[str, Any]:
|
||||
from app.services import agent_chat
|
||||
room = agent_chat.get_room(room_id)
|
||||
if not room:
|
||||
raise HTTPException(404, "Room niet gevonden")
|
||||
return {"room": room, "items": agent_chat.list_messages(room_id, limit=min(limit, 200), after_id=after_id)}
|
||||
|
||||
agent_chat.ensure_warroom()
|
||||
items = agent_chat.list_messages(room_id, limit=min(limit, 300), after_id=after_id)
|
||||
deleted_ids = agent_chat.list_deleted_ids(room_id) if after_id else []
|
||||
return {"ok": True, "items": items, "deleted_ids": deleted_ids}
|
||||
|
||||
|
||||
@router.post("/warroom/rooms/{room_id}/messages")
|
||||
@@ -663,3 +664,176 @@ async def warroom_upload(file: UploadFile = File(...)) -> dict[str, Any]:
|
||||
"mime": file.content_type or "application/octet-stream",
|
||||
"size": len(data),
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@router.post("/warroom/digest")
|
||||
async def warroom_daily_digest(hours: int = 24, deliver: bool = True) -> dict[str, Any]:
|
||||
"""Generate Herman War Room day report and optionally push to Aïssa + Mo on Telegram."""
|
||||
from app.services import warroom_digest
|
||||
|
||||
try:
|
||||
return await warroom_digest.run_daily_digest(hours=max(1, min(hours, 72)), deliver=deliver)
|
||||
except Exception as exc:
|
||||
raise HTTPException(500, str(exc)) from exc
|
||||
|
||||
|
||||
@router.post("/warroom/rooms/{room_id}/continue")
|
||||
async def warroom_continue(request: Request, room_id: int, turns: int = 2):
|
||||
"""Autonomous A2A continuation — agents keep debating with cockpit data."""
|
||||
import json
|
||||
from app.services import agent_chat
|
||||
|
||||
user = getattr(request.state, "user", None) or {}
|
||||
|
||||
async def gen():
|
||||
try:
|
||||
async for ev in agent_chat.continue_warroom_stream(
|
||||
room_id,
|
||||
username=(user.get("username") if isinstance(user, dict) else None),
|
||||
role=(user.get("role") if isinstance(user, dict) else None),
|
||||
max_turns=max(1, min(int(turns or 2), 4)),
|
||||
):
|
||||
yield f"data: {json.dumps(ev, default=str)}\n\n"
|
||||
except ValueError as exc:
|
||||
yield f"data: {json.dumps({'event': 'error', 'detail': str(exc)})}\n\n"
|
||||
except Exception as exc:
|
||||
yield f"data: {json.dumps({'event': 'error', 'detail': str(exc)})}\n\n"
|
||||
|
||||
return StreamingResponse(
|
||||
gen(),
|
||||
media_type="text/event-stream",
|
||||
headers={
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
"X-Accel-Buffering": "no",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
|
||||
@router.delete("/warroom/rooms/{room_id}/messages/{message_id}")
|
||||
async def warroom_delete_message(room_id: int, message_id: int) -> dict[str, Any]:
|
||||
"""Soft-delete a War Room message."""
|
||||
from app.services import agent_chat
|
||||
|
||||
room = agent_chat.get_room(room_id)
|
||||
if not room:
|
||||
raise HTTPException(404, "Room niet gevonden")
|
||||
row = agent_chat.delete_message(room_id, message_id)
|
||||
if not row:
|
||||
raise HTTPException(404, "Bericht niet gevonden")
|
||||
return {"ok": True, "id": message_id}
|
||||
|
||||
|
||||
|
||||
@router.post("/warroom/rooms/{room_id}/voice-room")
|
||||
async def warroom_voice_room(request: Request, room_id: int) -> dict[str, Any]:
|
||||
"""Open/announce a Foodlinkk Voice Room (humans + agents via STT/TTS)."""
|
||||
from app.services import agent_chat
|
||||
|
||||
room = agent_chat.get_room(room_id)
|
||||
if not room:
|
||||
raise HTTPException(404, "Room niet gevonden")
|
||||
body = {}
|
||||
try:
|
||||
body = await request.json()
|
||||
except Exception:
|
||||
body = {}
|
||||
action = str(body.get("action") or "join").lower()
|
||||
agents = body.get("agents") or ["herman", "retail", "bizdev", "halal", "finance"]
|
||||
if not isinstance(agents, list):
|
||||
agents = ["herman"]
|
||||
agents = [str(a).strip().lower() for a in agents if str(a).strip()][:12]
|
||||
user = getattr(request.state, "user", None) or {}
|
||||
who = user.get("username") if isinstance(user, dict) else "user"
|
||||
|
||||
if action == "leave":
|
||||
agent_chat.post_system_line(
|
||||
f"🎙️ {who} verliet de Voice Room",
|
||||
agent_key="herman",
|
||||
metadata={"kind": "voice_room", "action": "leave", "opened_by": who},
|
||||
)
|
||||
return {"ok": True, "action": "leave"}
|
||||
|
||||
agent_chat.post_system_line(
|
||||
f"🎙️ Voice Room open — {who} + agents: " + ", ".join("@" + a for a in agents),
|
||||
agent_key="herman",
|
||||
metadata={
|
||||
"kind": "voice_room",
|
||||
"action": "join",
|
||||
"opened_by": who,
|
||||
"agents": agents,
|
||||
},
|
||||
)
|
||||
return {"ok": True, "action": "join", "agents": agents, "host": who}
|
||||
|
||||
|
||||
@router.post("/warroom/rooms/{room_id}/call")
|
||||
async def warroom_start_call(request: Request, room_id: int) -> dict[str, Any]:
|
||||
"""Open a simple no-account video room in an external HTTPS window.
|
||||
|
||||
Cockpit stays on HTTP. Mic/camera need HTTPS in a separate tab.
|
||||
Default provider is Freifunk meet (no 8x8 login / no moderator gate).
|
||||
"""
|
||||
import os
|
||||
from urllib.parse import quote
|
||||
from app.db import execute, fetch_one
|
||||
from app.services import agent_chat
|
||||
|
||||
room = agent_chat.get_room(room_id)
|
||||
if not room:
|
||||
raise HTTPException(404, "Room niet gevonden")
|
||||
|
||||
base = (os.getenv("WARROOM_MEET_BASE") or "https://meet.ffmuc.net").rstrip("/")
|
||||
slug = f"FoodlinkkWR{room_id}"
|
||||
if room.get("kind") == "dm" and room.get("agent_key"):
|
||||
slug = f"FoodlinkkDM{room.get('agent_key')}{room_id}"
|
||||
url = f"{base}/{quote(slug)}"
|
||||
|
||||
user = getattr(request.state, "user", None) or {}
|
||||
who = user.get("username") if isinstance(user, dict) else "user"
|
||||
|
||||
recent = fetch_one(
|
||||
"""SELECT id, metadata FROM warroom_messages
|
||||
WHERE room_id = %s AND deleted_at IS NULL
|
||||
AND metadata->>'kind' = 'call'
|
||||
AND created_at > NOW() - INTERVAL '20 minutes'
|
||||
ORDER BY id DESC LIMIT 1""",
|
||||
(room_id,),
|
||||
)
|
||||
reused = bool(recent and (recent.get("metadata") or {}).get("call_url") == url)
|
||||
|
||||
if not reused:
|
||||
execute(
|
||||
"""UPDATE warroom_messages
|
||||
SET deleted_at = NOW(), content = '',
|
||||
metadata = COALESCE(metadata, '{}'::jsonb) || '{"deleted": true}'::jsonb
|
||||
WHERE room_id = %s AND deleted_at IS NULL
|
||||
AND (
|
||||
metadata->>'kind' = 'call'
|
||||
OR content ILIKE %s
|
||||
OR content ILIKE %s
|
||||
)""",
|
||||
(room_id, "%War Room-call%", "%Call open%"),
|
||||
)
|
||||
agent_chat.post_system_line(
|
||||
f"📹 Call open — geen account nodig: {url}",
|
||||
agent_key="herman",
|
||||
metadata={"kind": "call", "call_url": url, "room_slug": slug, "opened_by": who},
|
||||
)
|
||||
|
||||
return {
|
||||
"ok": True,
|
||||
"url": url,
|
||||
"slug": slug,
|
||||
"provider": "ffmuc",
|
||||
"embed": False,
|
||||
"mode": "external",
|
||||
"reused": reused,
|
||||
"hint": "Cockpit blijft op HTTP. Gesprek opent in een apart venster — geen login.",
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user