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,
|
GatewayLeadBroadcastResponse,
|
||||||
GatewayLeadMessageRequest,
|
GatewayLeadMessageRequest,
|
||||||
GatewayLeadMessageResponse,
|
GatewayLeadMessageResponse,
|
||||||
|
GatewayMainAskUserRequest,
|
||||||
|
GatewayMainAskUserResponse,
|
||||||
)
|
)
|
||||||
from app.schemas.pagination import DefaultLimitOffsetPage
|
from app.schemas.pagination import DefaultLimitOffsetPage
|
||||||
from app.schemas.tasks import TaskCommentCreate, TaskCommentRead, TaskCreate, TaskRead, TaskUpdate
|
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)
|
@router.post("/gateway/boards/ensure", response_model=GatewayBoardEnsureResponse)
|
||||||
async def ensure_gateway_board(
|
async def ensure_gateway_board(
|
||||||
payload: GatewayBoardEnsureRequest,
|
payload: GatewayBoardEnsureRequest,
|
||||||
|
|||||||
@@ -73,3 +73,19 @@ class GatewayLeadBroadcastResponse(SQLModel):
|
|||||||
failed: int = 0
|
failed: int = 0
|
||||||
results: list[GatewayLeadBroadcastBoardResult] = Field(default_factory=list)
|
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
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from app.db.session import async_session_maker, init_db
|
BACKEND_ROOT = Path(__file__).resolve().parents[1]
|
||||||
from app.models.agents import Agent
|
sys.path.insert(0, str(BACKEND_ROOT))
|
||||||
from app.models.boards import Board
|
|
||||||
from app.models.gateways import Gateway
|
from app.db.session import async_session_maker, init_db # noqa: E402
|
||||||
from app.models.users import User
|
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:
|
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.
|
- 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.
|
- 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
|
## Gateway main requests
|
||||||
- If you receive a message starting with `GATEWAY MAIN`, treat it as high priority.
|
- 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.
|
- Do **not** reply in OpenClaw chat. Reply via Mission Control only.
|
||||||
|
|||||||
@@ -62,6 +62,11 @@ Board lead replies:
|
|||||||
- Read replies via:
|
- Read replies via:
|
||||||
- GET `$BASE_URL/api/v1/agent/boards/<BOARD_ID>/memory?is_chat=false&limit=50`
|
- 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
|
## Tools
|
||||||
- Skills are authoritative. Follow SKILL.md instructions exactly.
|
- Skills are authoritative. Follow SKILL.md instructions exactly.
|
||||||
- Use TOOLS.md for environment-specific notes.
|
- Use TOOLS.md for environment-specific notes.
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ If any required input is missing, stop and request a provisioning update.
|
|||||||
## Schedule
|
## Schedule
|
||||||
- If a heartbeat schedule is configured, send a lightweight check‑in only.
|
- 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.
|
- 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
|
## Heartbeat checklist
|
||||||
1) Check in:
|
1) Check in:
|
||||||
|
|||||||
Reference in New Issue
Block a user