import json import logging from datetime import datetime from fastapi import APIRouter, Form, Request from fastapi.responses import HTMLResponse from dashboard.store import message_log from dashboard.templating import templates from timmy.session import _clean_response, chat_with_tools, continue_chat from timmy.tool_safety import ( format_action_description, get_impact_level, ) logger = logging.getLogger(__name__) router = APIRouter(prefix="/agents", tags=["agents"]) MAX_MESSAGE_LENGTH = 10_000 # chars — reject before hitting the model # In-memory store for paused runs (approval_id -> run context). # Each entry holds the RunOutput, the RunRequirement ref, and tool metadata. _pending_runs: dict[str, dict] = {} @router.get("") async def list_agents(): """Return registered agents.""" from config import settings return { "agents": [ { "id": "default", "name": settings.agent_name, "status": "idle", "capabilities": "chat,reasoning,research,planning", "type": "local", "model": settings.ollama_model, "backend": "ollama", "version": "1.0.0", } ] } @router.get("/default/panel", response_class=HTMLResponse) async def agent_panel(request: Request): """Chat panel — for HTMX main-panel swaps.""" return templates.TemplateResponse(request, "partials/agent_panel_chat.html", {"agent": None}) @router.get("/default/history", response_class=HTMLResponse) async def get_history(request: Request): return templates.TemplateResponse( request, "partials/history.html", {"messages": message_log.all()}, ) @router.delete("/default/history", response_class=HTMLResponse) async def clear_history(request: Request): message_log.clear() return templates.TemplateResponse( request, "partials/history.html", {"messages": []}, ) @router.post("/default/chat", response_class=HTMLResponse) async def chat_agent(request: Request, message: str = Form(...)): """Chat — synchronous response with native Agno tool confirmation.""" message = message.strip() if not message: from fastapi import HTTPException raise HTTPException(status_code=400, detail="Message cannot be empty") if len(message) > MAX_MESSAGE_LENGTH: from fastapi import HTTPException raise HTTPException(status_code=422, detail="Message too long") timestamp = datetime.now().strftime("%H:%M:%S") response_text = None error_text = None try: run_output = await chat_with_tools(message) except Exception as exc: logger.error("Chat error: %s", exc) error_text = f"Chat error: {exc}" run_output = None # Check if Agno paused the run for tool confirmation tool_actions = [] if run_output is not None: status = getattr(run_output, "status", None) is_paused = status == "PAUSED" or str(status) == "RunStatus.paused" if is_paused and getattr(run_output, "active_requirements", None): for req in run_output.active_requirements: if getattr(req, "needs_confirmation", False): te = req.tool_execution tool_name = getattr(te, "tool_name", "unknown") tool_args = getattr(te, "tool_args", {}) or {} from timmy.approvals import create_item item = create_item( title=f"Dashboard: {tool_name}", description=format_action_description(tool_name, tool_args), proposed_action=json.dumps({"tool": tool_name, "args": tool_args}), impact=get_impact_level(tool_name), ) _pending_runs[item.id] = { "run_output": run_output, "requirement": req, "tool_name": tool_name, "tool_args": tool_args, } tool_actions.append( { "approval_id": item.id, "tool_name": tool_name, "description": format_action_description(tool_name, tool_args), "impact": get_impact_level(tool_name), } ) raw_content = run_output.content if hasattr(run_output, "content") else "" response_text = _clean_response(raw_content or "") if not response_text and not tool_actions: response_text = None # let error template show if needed message_log.append(role="user", content=message, timestamp=timestamp, source="browser") if response_text: message_log.append( role="agent", content=response_text, timestamp=timestamp, source="browser" ) elif error_text: message_log.append(role="error", content=error_text, timestamp=timestamp, source="browser") return templates.TemplateResponse( request, "partials/chat_message.html", { "user_message": message, "response": response_text, "error": error_text, "timestamp": timestamp, "task_id": None, "queue_info": None, "tool_actions": tool_actions, }, ) @router.post("/default/tool/{approval_id}/approve", response_class=HTMLResponse) async def approve_tool(request: Request, approval_id: str): """Confirm a paused tool and resume execution via Agno.""" from timmy.approvals import approve pending = _pending_runs.pop(approval_id, None) if not pending: return HTMLResponse( "
Action not found or already processed.
", status_code=404, ) approve(approval_id) tool_name = pending["tool_name"] # Confirm the requirement — Agno will execute the tool on continue_run req = pending["requirement"] req.confirm() try: result_run = await continue_chat(pending["run_output"]) # Extract tool result from the resumed run tool_result = "" for te in getattr(result_run, "tools", None) or []: if getattr(te, "tool_name", None) == tool_name and getattr(te, "result", None): tool_result = te.result break if not tool_result: tool_result = getattr(result_run, "content", None) or "Tool executed successfully." except Exception as exc: logger.error("Tool execution failed: %s", exc) tool_result = f"Error: {exc}" return templates.TemplateResponse( request, "partials/chat_tool_result.html", { "approval_id": approval_id, "tool_name": tool_name, "status": "approved", "result": str(tool_result)[:2000], }, ) @router.post("/default/tool/{approval_id}/reject", response_class=HTMLResponse) async def reject_tool(request: Request, approval_id: str): """Reject a pending tool action.""" from timmy.approvals import reject pending = _pending_runs.pop(approval_id, None) tool_name = "action" if pending: tool_name = pending["tool_name"] req = pending["requirement"] req.reject(note="User rejected from dashboard") # Resume so the agent knows the tool was rejected try: await continue_chat(pending["run_output"]) except Exception as exc: logger.warning("Agent tool rejection error: %s", exc) pass reject(approval_id) return templates.TemplateResponse( request, "partials/chat_tool_result.html", { "approval_id": approval_id, "tool_name": tool_name, "status": "rejected", "result": "", }, )