"""REST endpoint for sending a message to an agent (non-streaming)."""
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from navi.api.deps import get_agent, get_orchestrator, get_session_store, require_user
from navi.auth import User
from navi.auth.deps import check_session_access
from navi.core import Agent, SessionStore
from navi.exceptions import MaxIterationsReached, NaviError, SessionNotFound
router = APIRouter(prefix="/sessions", tags=["messages"])
class SendMessageRequest(BaseModel):
content: str
@router.post("/{session_id}/messages")
async def send_message(
session_id: str,
body: SendMessageRequest,
agent: Annotated[Agent, Depends(get_agent)],
store: Annotated[SessionStore, Depends(get_session_store)],
user: Annotated[User, Depends(require_user)],
) -> dict:
session = await store.get(session_id)
if session is None:
raise HTTPException(status_code=404, detail="Session not found")
check_session_access(session, user)
# Guard against a second run on the same session (e.g. an active WS turn):
# mark_busy publishes "running" to is_running()/stop() the same way the
# WS path does. The lock is held only for the check-and-mark, not the run
# itself, so a long REST turn does not stall other requests.
orchestrator = get_orchestrator()
async with orchestrator.session_lock(session_id):
if orchestrator.is_running(session_id):
raise HTTPException(status_code=409, detail="Agent is already running for this session")
orchestrator.mark_busy(session_id)
# Set user context for tool sandboxing (reset in finally — these
# contextvars otherwise leak to the next request on the same task).
from navi.tools._internal.base import current_user_id as _uid_var, current_user_role as _role_var, current_user_info as _uinfo_var
uid_token = _uid_var.set(user.id)
role_token = _role_var.set(user.role)
uinfo_token = _uinfo_var.set(user.model_dump(mode="json"))
try:
reply = await agent.run(session_id, body.content)
return {"role": "assistant", "content": reply}
except SessionNotFound:
raise HTTPException(status_code=404, detail="Session not found")
except MaxIterationsReached as e:
raise HTTPException(status_code=500, detail=str(e))
except NaviError as e:
raise HTTPException(status_code=500, detail=str(e))
finally:
_uid_var.reset(uid_token)
_role_var.reset(role_token)
_uinfo_var.reset(uinfo_token)
await orchestrator.clear_busy(session_id)