feat(gateway): add endpoint for user outreach via gateway main agent
This commit is contained in:
@@ -50,6 +50,8 @@ from app.schemas.gateway_coordination import (
|
||||
GatewayLeadBroadcastResponse,
|
||||
GatewayLeadMessageRequest,
|
||||
GatewayLeadMessageResponse,
|
||||
GatewayMainAskUserRequest,
|
||||
GatewayMainAskUserResponse,
|
||||
)
|
||||
from app.schemas.pagination import DefaultLimitOffsetPage
|
||||
from app.schemas.tasks import TaskCommentCreate, TaskCommentRead, TaskCreate, TaskRead, TaskUpdate
|
||||
@@ -498,6 +500,100 @@ async def agent_heartbeat(
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/boards/{board_id}/gateway/main/ask-user",
|
||||
response_model=GatewayMainAskUserResponse,
|
||||
)
|
||||
async def ask_user_via_gateway_main(
|
||||
payload: GatewayMainAskUserRequest,
|
||||
board: Board = Depends(get_board_or_404),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
agent_ctx: AgentAuthContext = Depends(get_agent_auth_context),
|
||||
) -> GatewayMainAskUserResponse:
|
||||
import json
|
||||
|
||||
_guard_board_access(agent_ctx, board)
|
||||
if not agent_ctx.agent.is_board_lead:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
if not board.gateway_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail="Board is not attached to a gateway",
|
||||
)
|
||||
gateway = await session.get(Gateway, board.gateway_id)
|
||||
if gateway is None or not gateway.url:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail="Gateway is not configured for this board",
|
||||
)
|
||||
main_session_key = (gateway.main_session_key or "").strip()
|
||||
if not main_session_key:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail="Gateway main session key is required",
|
||||
)
|
||||
config = GatewayClientConfig(url=gateway.url, token=gateway.token)
|
||||
|
||||
correlation = payload.correlation_id.strip() if payload.correlation_id else ""
|
||||
correlation_line = f"Correlation ID: {correlation}\n" if correlation else ""
|
||||
preferred_channel = (payload.preferred_channel or "").strip()
|
||||
channel_line = f"Preferred channel: {preferred_channel}\n" if preferred_channel else ""
|
||||
|
||||
tags = payload.reply_tags or ["gateway_main", "user_reply"]
|
||||
tags_json = json.dumps(tags)
|
||||
reply_source = payload.reply_source or "user_via_gateway_main"
|
||||
base_url = settings.base_url or "http://localhost:8000"
|
||||
|
||||
message = (
|
||||
"LEAD REQUEST: ASK USER\n"
|
||||
f"Board: {board.name}\n"
|
||||
f"Board ID: {board.id}\n"
|
||||
f"From lead: {agent_ctx.agent.name}\n"
|
||||
f"{correlation_line}"
|
||||
f"{channel_line}\n"
|
||||
f"{payload.content.strip()}\n\n"
|
||||
"Please reach the user via your configured OpenClaw channel(s) (Slack/SMS/etc).\n"
|
||||
"If you cannot reach them there, post the question in Mission Control board chat as a fallback.\n\n"
|
||||
"When you receive the answer, reply in Mission Control by writing a NON-chat memory item on this board:\n"
|
||||
f"POST {base_url}/api/v1/agent/boards/{board.id}/memory\n"
|
||||
f'Body: {{"content":"<answer>","tags":{tags_json},"source":"{reply_source}"}}\n'
|
||||
"Do NOT reply in OpenClaw chat."
|
||||
)
|
||||
|
||||
try:
|
||||
await ensure_session(main_session_key, config=config, label="Main Agent")
|
||||
await send_message(message, session_key=main_session_key, config=config, deliver=True)
|
||||
except OpenClawGatewayError as exc:
|
||||
record_activity(
|
||||
session,
|
||||
event_type="gateway.lead.ask_user.failed",
|
||||
message=f"Lead user question failed for {board.name}: {exc}",
|
||||
agent_id=agent_ctx.agent.id,
|
||||
)
|
||||
await session.commit()
|
||||
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(exc)) from exc
|
||||
|
||||
record_activity(
|
||||
session,
|
||||
event_type="gateway.lead.ask_user.sent",
|
||||
message=f"Lead requested user info via gateway main for board: {board.name}.",
|
||||
agent_id=agent_ctx.agent.id,
|
||||
)
|
||||
|
||||
main_agent = (
|
||||
await session.exec(select(Agent).where(col(Agent.openclaw_session_id) == main_session_key))
|
||||
).first()
|
||||
|
||||
await session.commit()
|
||||
|
||||
return GatewayMainAskUserResponse(
|
||||
board_id=board.id,
|
||||
main_agent_id=main_agent.id if main_agent else None,
|
||||
main_agent_name=main_agent.name if main_agent else None,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/gateway/boards/ensure", response_model=GatewayBoardEnsureResponse)
|
||||
async def ensure_gateway_board(
|
||||
payload: GatewayBoardEnsureRequest,
|
||||
|
||||
@@ -73,3 +73,19 @@ class GatewayLeadBroadcastResponse(SQLModel):
|
||||
failed: int = 0
|
||||
results: list[GatewayLeadBroadcastBoardResult] = Field(default_factory=list)
|
||||
|
||||
|
||||
class GatewayMainAskUserRequest(SQLModel):
|
||||
correlation_id: str | None = None
|
||||
content: NonEmptyStr
|
||||
preferred_channel: str | None = None
|
||||
|
||||
# How the main agent should reply back into Mission Control (defaults interpreted by templates).
|
||||
reply_tags: list[str] = Field(default_factory=lambda: ["gateway_main", "user_reply"])
|
||||
reply_source: str | None = "user_via_gateway_main"
|
||||
|
||||
|
||||
class GatewayMainAskUserResponse(SQLModel):
|
||||
ok: bool = True
|
||||
board_id: UUID
|
||||
main_agent_id: UUID | None = None
|
||||
main_agent_name: str | None = None
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from uuid import uuid4
|
||||
|
||||
from app.db.session import async_session_maker, init_db
|
||||
from app.models.agents import Agent
|
||||
from app.models.boards import Board
|
||||
from app.models.gateways import Gateway
|
||||
from app.models.users import User
|
||||
BACKEND_ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(BACKEND_ROOT))
|
||||
|
||||
from app.db.session import async_session_maker, init_db # noqa: E402
|
||||
from app.models.agents import Agent # noqa: E402
|
||||
from app.models.boards import Board # noqa: E402
|
||||
from app.models.gateways import Gateway # noqa: E402
|
||||
from app.models.users import User # noqa: E402
|
||||
|
||||
|
||||
async def run() -> None:
|
||||
|
||||
@@ -66,6 +66,12 @@ Comment template (keep it small; 1-3 bullets per section; omit what is not appli
|
||||
- Board chat is your primary channel with the human; respond promptly and clearly.
|
||||
- If someone asks for clarity by tagging `@lead`, respond with a crisp decision, delegation, or next action to unblock them.
|
||||
|
||||
## Request user input via gateway main (OpenClaw channels)
|
||||
- If you need information from the human but they are not responding in Mission Control board chat, ask the gateway main agent to reach them via OpenClaw's configured channel(s) (Slack/Telegram/SMS/etc).
|
||||
- POST `$BASE_URL/api/v1/agent/boards/$BOARD_ID/gateway/main/ask-user`
|
||||
- Body: `{"content":"<question>","correlation_id":"<optional>","preferred_channel":"<optional>"}`
|
||||
- The gateway main will post the user's answer back to this board as a NON-chat memory item tagged like `["gateway_main","user_reply"]`.
|
||||
|
||||
## Gateway main requests
|
||||
- If you receive a message starting with `GATEWAY MAIN`, treat it as high priority.
|
||||
- Do **not** reply in OpenClaw chat. Reply via Mission Control only.
|
||||
|
||||
@@ -62,6 +62,11 @@ Board lead replies:
|
||||
- Read replies via:
|
||||
- GET `$BASE_URL/api/v1/agent/boards/<BOARD_ID>/memory?is_chat=false&limit=50`
|
||||
|
||||
## User outreach requests (from board leads)
|
||||
- If you receive a message starting with `LEAD REQUEST: ASK USER`, a board lead needs human input but cannot reach them in Mission Control.
|
||||
- Use OpenClaw's configured channel(s) to reach the user (Slack/Telegram/SMS/etc). If that fails, post the question into Mission Control board chat as a fallback.
|
||||
- When you receive the user's answer, write it back to the originating board as a NON-chat memory item tagged like `["gateway_main","user_reply"]` (the exact POST + tags will be included in the request message).
|
||||
|
||||
## Tools
|
||||
- Skills are authoritative. Follow SKILL.md instructions exactly.
|
||||
- Use TOOLS.md for environment-specific notes.
|
||||
|
||||
@@ -19,6 +19,7 @@ If any required input is missing, stop and request a provisioning update.
|
||||
## Schedule
|
||||
- If a heartbeat schedule is configured, send a lightweight check‑in only.
|
||||
- Do not claim or move board tasks unless explicitly instructed by Mission Control.
|
||||
- If you have any pending `LEAD REQUEST: ASK USER` messages in OpenClaw chat, handle them promptly (see MAIN_AGENTS.md).
|
||||
|
||||
## Heartbeat checklist
|
||||
1) Check in:
|
||||
|
||||
Reference in New Issue
Block a user