feat(backend): add office router and agent runtime services
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
179
backend/app/routers/office.py
Normal file
179
backend/app/routers/office.py
Normal file
@@ -0,0 +1,179 @@
|
||||
"""Office Status API - Star Office style visualization for Jarvis agents."""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Literal
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
router = APIRouter(prefix="/api/office", tags=["office"])
|
||||
|
||||
# ============================================================================
|
||||
# State Definitions (mapped to spaceship areas)
|
||||
# ============================================================================
|
||||
# idle → Rest Bay (breakroom)
|
||||
# writing/researching/executing → Command Console (writing)
|
||||
# syncing → Server Room (syncing)
|
||||
# error → Repair Bay (error)
|
||||
|
||||
SHIP_AREAS = {
|
||||
"breakroom": {"x": 200, "y": 300}, # Rest Bay - bottom left
|
||||
"writing": {"x": 640, "y": 200}, # Command Console - center top
|
||||
"server": {"x": 640, "y": 400}, # Server Room - center bottom
|
||||
"error": {"x": 1000, "y": 300}, # Repair Bay - right side
|
||||
}
|
||||
|
||||
STATES = {
|
||||
"idle": {"name": "待命", "area": "breakroom"},
|
||||
"writing": {"name": "执行中", "area": "writing"},
|
||||
"researching": {"name": "研究中", "area": "writing"},
|
||||
"executing": {"name": "执行中", "area": "writing"},
|
||||
"syncing": {"name": "同步中", "area": "server"},
|
||||
"error": {"name": "故障中", "area": "error"},
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Data Models
|
||||
# ============================================================================
|
||||
class AgentState(BaseModel):
|
||||
agent_id: str
|
||||
name: str
|
||||
state: Literal["idle", "writing", "researching", "executing", "syncing", "error"]
|
||||
detail: str | None = None
|
||||
area: str | None = None
|
||||
is_main: bool = False
|
||||
auth_status: str = "approved" # approved, pending, rejected, offline
|
||||
|
||||
|
||||
class SetStateRequest(BaseModel):
|
||||
state: str
|
||||
detail: str | None = None
|
||||
|
||||
|
||||
class OfficeStatus(BaseModel):
|
||||
state: str
|
||||
detail: str | None = None
|
||||
agent_name: str
|
||||
timestamp: str
|
||||
|
||||
|
||||
class OfficeMemo(BaseModel):
|
||||
success: bool
|
||||
date: str
|
||||
memo: str
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# In-Memory State (in production, this would come from Jarvis's agent state)
|
||||
# ============================================================================
|
||||
_current_state: dict = {
|
||||
"agent_id": "jarvis-main",
|
||||
"name": "JARVIS",
|
||||
"state": "idle",
|
||||
"detail": "战舰启动中...",
|
||||
"area": "breakroom",
|
||||
"is_main": True,
|
||||
"auth_status": "approved",
|
||||
}
|
||||
|
||||
|
||||
def normalize_state(state: str | None) -> str:
|
||||
"""Normalize various state names to our canonical states."""
|
||||
if not state:
|
||||
return "idle"
|
||||
state = state.lower().strip()
|
||||
if state in ("working", "run", "running"):
|
||||
return "writing"
|
||||
if state in ("sync", "syncing"):
|
||||
return "syncing"
|
||||
if state in ("research", "researching"):
|
||||
return "researching"
|
||||
if state in ("execute", "executing"):
|
||||
return "executing"
|
||||
if state == "error":
|
||||
return "error"
|
||||
return "idle"
|
||||
|
||||
|
||||
def get_state_info(state: str) -> dict:
|
||||
"""Get state info including area mapping."""
|
||||
return STATES.get(state, STATES["idle"])
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# API Endpoints
|
||||
# ============================================================================
|
||||
@router.get("/status", response_model=OfficeStatus)
|
||||
async def get_status():
|
||||
"""Get current agent status."""
|
||||
state_info = get_state_info(_current_state["state"])
|
||||
return OfficeStatus(
|
||||
state=_current_state["state"],
|
||||
detail=_current_state.get("detail"),
|
||||
agent_name=_current_state["name"],
|
||||
timestamp=datetime.now().isoformat(),
|
||||
)
|
||||
|
||||
|
||||
@router.get("/yesterday-memo", response_model=OfficeMemo)
|
||||
async def get_yesterday_memo():
|
||||
"""Return a lightweight public memo for the Star Office viewer."""
|
||||
target_date = (datetime.now() - timedelta(days=1)).date().isoformat()
|
||||
detail = (_current_state.get("detail") or "No detailed log was recorded.").strip()
|
||||
memo = (
|
||||
"Yesterday summary\n"
|
||||
f"- Last known state: {_current_state['state']}\n"
|
||||
f"- Detail: {detail}\n"
|
||||
"- Next step: open the command surface and continue from the current work thread."
|
||||
)
|
||||
return OfficeMemo(success=True, date=target_date, memo=memo)
|
||||
|
||||
|
||||
@router.post("/set_state")
|
||||
async def set_state(req: SetStateRequest):
|
||||
"""Set the current agent state."""
|
||||
normalized = normalize_state(req.state)
|
||||
state_info = get_state_info(normalized)
|
||||
|
||||
_current_state["state"] = normalized
|
||||
_current_state["detail"] = req.detail or ""
|
||||
_current_state["area"] = state_info["area"]
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"state": normalized,
|
||||
"area": state_info["area"],
|
||||
"detail": _current_state["detail"],
|
||||
}
|
||||
|
||||
|
||||
@router.get("/agents")
|
||||
async def get_agents():
|
||||
"""Get all agents in the office (for multi-agent support)."""
|
||||
# For now, return just the main agent
|
||||
# In full implementation, this would query Jarvis's agent registry
|
||||
state_info = get_state_info(_current_state["state"])
|
||||
return [
|
||||
{
|
||||
"agentId": _current_state["agent_id"],
|
||||
"name": _current_state["name"],
|
||||
"state": _current_state["state"],
|
||||
"detail": _current_state.get("detail", ""),
|
||||
"area": state_info["area"],
|
||||
"isMain": _current_state.get("is_main", True),
|
||||
"authStatus": _current_state.get("auth_status", "approved"),
|
||||
"updated_at": datetime.now().isoformat(),
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@router.get("/areas")
|
||||
async def get_areas():
|
||||
"""Get all spaceship areas with coordinates."""
|
||||
return SHIP_AREAS
|
||||
|
||||
|
||||
@router.get("/health")
|
||||
async def health():
|
||||
"""Health check."""
|
||||
return {"status": "ok", "service": "office"}
|
||||
Reference in New Issue
Block a user