diff --git a/src/dashboard/routes/agents.py b/src/dashboard/routes/agents.py index 6c9ebfb7..d87a01a2 100644 --- a/src/dashboard/routes/agents.py +++ b/src/dashboard/routes/agents.py @@ -71,21 +71,20 @@ async def clear_history(request: Request): ) -@router.post("/default/chat", response_class=HTMLResponse) -async def chat_agent(request: Request, message: str = Form(...)): - """Chat — synchronous response with native Agno tool confirmation.""" +def _validate_message(message: str) -> str: + """Strip and validate chat input; raise HTTPException on bad input.""" + from fastapi import HTTPException + 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") + return message - # Record user activity so the thinking engine knows we're not idle + +def _record_user_activity() -> None: + """Notify the thinking engine that the user is active.""" try: from timmy.thinking import thinking_engine @@ -93,6 +92,67 @@ async def chat_agent(request: Request, message: str = Form(...)): except Exception: logger.debug("Failed to record user input for thinking engine") + +def _extract_tool_actions(run_output) -> list[dict]: + """If Agno paused the run for tool confirmation, build approval items.""" + from timmy.approvals import create_item + + tool_actions: list[dict] = [] + status = getattr(run_output, "status", None) + is_paused = status == "PAUSED" or str(status) == "RunStatus.paused" + + if not (is_paused and getattr(run_output, "active_requirements", None)): + return tool_actions + + for req in run_output.active_requirements: + if not getattr(req, "needs_confirmation", False): + continue + te = req.tool_execution + tool_name = getattr(te, "tool_name", "unknown") + tool_args = getattr(te, "tool_args", {}) or {} + + 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), + } + ) + return tool_actions + + +def _log_exchange( + message: str, response_text: str | None, error_text: str | None, timestamp: str +) -> None: + """Append user message and agent/error reply to the in-memory log.""" + 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") + + +@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 = _validate_message(message) + _record_user_activity() + timestamp = datetime.now().strftime("%H:%M:%S") response_text = None error_text = None @@ -104,54 +164,15 @@ async def chat_agent(request: Request, message: str = Form(...)): error_text = f"Chat error: {exc}" run_output = None - # Check if Agno paused the run for tool confirmation - tool_actions = [] + tool_actions: list[dict] = [] 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), - } - ) - + tool_actions = _extract_tool_actions(run_output) 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 + response_text = None - 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") + _log_exchange(message, response_text, error_text, timestamp) return templates.TemplateResponse( request,