2026-02-09 15:49:50 +05:30
|
|
|
"""Board onboarding endpoints for user/agent collaboration."""
|
|
|
|
|
|
2026-02-05 14:43:25 +05:30
|
|
|
from __future__ import annotations
|
|
|
|
|
|
2026-02-06 02:43:08 +05:30
|
|
|
import logging
|
2026-02-09 15:49:50 +05:30
|
|
|
from typing import TYPE_CHECKING
|
2026-02-05 14:43:25 +05:30
|
|
|
|
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, status
|
2026-02-06 21:56:16 +05:30
|
|
|
from pydantic import ValidationError
|
2026-02-09 02:04:14 +05:30
|
|
|
from sqlmodel import col
|
2026-02-05 14:43:25 +05:30
|
|
|
|
2026-02-08 21:16:26 +05:30
|
|
|
from app.api.deps import (
|
|
|
|
|
ActorContext,
|
|
|
|
|
get_board_for_user_read,
|
|
|
|
|
get_board_for_user_write,
|
|
|
|
|
get_board_or_404,
|
|
|
|
|
require_admin_auth,
|
|
|
|
|
require_admin_or_agent,
|
|
|
|
|
)
|
2026-02-05 16:01:10 +05:30
|
|
|
from app.core.config import settings
|
2026-02-06 16:12:04 +05:30
|
|
|
from app.core.time import utcnow
|
2026-02-05 14:43:25 +05:30
|
|
|
from app.db.session import get_session
|
|
|
|
|
from app.integrations.openclaw_gateway import GatewayConfig as GatewayClientConfig
|
2026-02-09 20:44:05 +05:30
|
|
|
from app.integrations.openclaw_gateway import OpenClawGatewayError, ensure_session, send_message
|
2026-02-05 14:43:25 +05:30
|
|
|
from app.models.board_onboarding import BoardOnboardingSession
|
|
|
|
|
from app.models.gateways import Gateway
|
|
|
|
|
from app.schemas.board_onboarding import (
|
2026-02-06 16:12:04 +05:30
|
|
|
BoardOnboardingAgentComplete,
|
|
|
|
|
BoardOnboardingAgentUpdate,
|
2026-02-07 00:21:44 +05:30
|
|
|
BoardOnboardingAnswer,
|
2026-02-05 14:43:25 +05:30
|
|
|
BoardOnboardingConfirm,
|
2026-02-06 21:56:16 +05:30
|
|
|
BoardOnboardingLeadAgentDraft,
|
2026-02-05 14:43:25 +05:30
|
|
|
BoardOnboardingRead,
|
|
|
|
|
BoardOnboardingStart,
|
2026-02-06 21:56:16 +05:30
|
|
|
BoardOnboardingUserProfile,
|
2026-02-05 14:43:25 +05:30
|
|
|
)
|
|
|
|
|
from app.schemas.boards import BoardRead
|
2026-02-09 20:44:05 +05:30
|
|
|
from app.services.board_leads import LeadAgentOptions, LeadAgentRequest, ensure_board_lead_agent
|
2026-02-10 00:45:15 +05:30
|
|
|
from app.services.gateway_agents import gateway_agent_session_key
|
2026-02-05 14:43:25 +05:30
|
|
|
|
2026-02-09 15:49:50 +05:30
|
|
|
if TYPE_CHECKING:
|
|
|
|
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
|
|
|
|
|
|
|
|
|
from app.core.auth import AuthContext
|
|
|
|
|
from app.models.boards import Board
|
|
|
|
|
|
2026-02-05 14:43:25 +05:30
|
|
|
router = APIRouter(prefix="/boards/{board_id}/onboarding", tags=["board-onboarding"])
|
2026-02-05 19:06:32 +05:30
|
|
|
logger = logging.getLogger(__name__)
|
2026-02-09 15:49:50 +05:30
|
|
|
BOARD_USER_READ_DEP = Depends(get_board_for_user_read)
|
|
|
|
|
BOARD_USER_WRITE_DEP = Depends(get_board_for_user_write)
|
|
|
|
|
BOARD_OR_404_DEP = Depends(get_board_or_404)
|
|
|
|
|
SESSION_DEP = Depends(get_session)
|
|
|
|
|
ACTOR_DEP = Depends(require_admin_or_agent)
|
|
|
|
|
ADMIN_AUTH_DEP = Depends(require_admin_auth)
|
2026-02-05 14:43:25 +05:30
|
|
|
|
|
|
|
|
|
2026-02-06 16:12:04 +05:30
|
|
|
async def _gateway_config(
|
2026-02-09 20:44:05 +05:30
|
|
|
session: AsyncSession,
|
|
|
|
|
board: Board,
|
2026-02-06 16:12:04 +05:30
|
|
|
) -> tuple[Gateway, GatewayClientConfig]:
|
2026-02-05 14:43:25 +05:30
|
|
|
if not board.gateway_id:
|
|
|
|
|
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)
|
2026-02-09 02:04:14 +05:30
|
|
|
gateway = await Gateway.objects.by_id(board.gateway_id).first(session)
|
2026-02-10 00:45:15 +05:30
|
|
|
if gateway is None or not gateway.url:
|
2026-02-05 14:43:25 +05:30
|
|
|
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)
|
2026-02-05 15:26:35 +05:30
|
|
|
return gateway, GatewayClientConfig(url=gateway.url, token=gateway.token)
|
|
|
|
|
|
|
|
|
|
|
2026-02-09 17:24:21 +05:30
|
|
|
def _parse_draft_user_profile(
|
|
|
|
|
draft_goal: object,
|
|
|
|
|
) -> BoardOnboardingUserProfile | None:
|
|
|
|
|
if not isinstance(draft_goal, dict):
|
|
|
|
|
return None
|
|
|
|
|
raw_profile = draft_goal.get("user_profile")
|
|
|
|
|
if raw_profile is None:
|
|
|
|
|
return None
|
|
|
|
|
try:
|
|
|
|
|
return BoardOnboardingUserProfile.model_validate(raw_profile)
|
|
|
|
|
except ValidationError:
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _parse_draft_lead_agent(
|
|
|
|
|
draft_goal: object,
|
|
|
|
|
) -> BoardOnboardingLeadAgentDraft | None:
|
|
|
|
|
if not isinstance(draft_goal, dict):
|
|
|
|
|
return None
|
|
|
|
|
raw_lead = draft_goal.get("lead_agent")
|
|
|
|
|
if raw_lead is None:
|
|
|
|
|
return None
|
|
|
|
|
try:
|
|
|
|
|
return BoardOnboardingLeadAgentDraft.model_validate(raw_lead)
|
|
|
|
|
except ValidationError:
|
|
|
|
|
return None
|
2026-02-05 15:26:35 +05:30
|
|
|
|
|
|
|
|
|
2026-02-09 17:24:21 +05:30
|
|
|
def _apply_user_profile(
|
2026-02-05 15:26:35 +05:30
|
|
|
auth: AuthContext,
|
2026-02-09 17:24:21 +05:30
|
|
|
profile: BoardOnboardingUserProfile | None,
|
|
|
|
|
) -> bool:
|
|
|
|
|
if auth.user is None or profile is None:
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
changed = False
|
|
|
|
|
if profile.preferred_name is not None:
|
|
|
|
|
auth.user.preferred_name = profile.preferred_name
|
|
|
|
|
changed = True
|
|
|
|
|
if profile.pronouns is not None:
|
|
|
|
|
auth.user.pronouns = profile.pronouns
|
|
|
|
|
changed = True
|
|
|
|
|
if profile.timezone is not None:
|
|
|
|
|
auth.user.timezone = profile.timezone
|
|
|
|
|
changed = True
|
|
|
|
|
if profile.notes is not None:
|
|
|
|
|
auth.user.notes = profile.notes
|
|
|
|
|
changed = True
|
|
|
|
|
if profile.context is not None:
|
|
|
|
|
auth.user.context = profile.context
|
|
|
|
|
changed = True
|
|
|
|
|
return changed
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _lead_agent_options(
|
|
|
|
|
lead_agent: BoardOnboardingLeadAgentDraft | None,
|
|
|
|
|
) -> LeadAgentOptions:
|
|
|
|
|
if lead_agent is None:
|
|
|
|
|
return LeadAgentOptions(action="provision")
|
2026-02-06 21:56:16 +05:30
|
|
|
|
2026-02-09 17:24:21 +05:30
|
|
|
lead_identity_profile: dict[str, str] = {}
|
|
|
|
|
if lead_agent.identity_profile:
|
|
|
|
|
lead_identity_profile.update(lead_agent.identity_profile)
|
|
|
|
|
if lead_agent.autonomy_level:
|
|
|
|
|
lead_identity_profile["autonomy_level"] = lead_agent.autonomy_level
|
|
|
|
|
if lead_agent.verbosity:
|
|
|
|
|
lead_identity_profile["verbosity"] = lead_agent.verbosity
|
|
|
|
|
if lead_agent.output_format:
|
|
|
|
|
lead_identity_profile["output_format"] = lead_agent.output_format
|
|
|
|
|
if lead_agent.update_cadence:
|
|
|
|
|
lead_identity_profile["update_cadence"] = lead_agent.update_cadence
|
|
|
|
|
if lead_agent.custom_instructions:
|
|
|
|
|
lead_identity_profile["custom_instructions"] = lead_agent.custom_instructions
|
|
|
|
|
|
|
|
|
|
return LeadAgentOptions(
|
|
|
|
|
agent_name=lead_agent.name,
|
|
|
|
|
identity_profile=lead_identity_profile or None,
|
|
|
|
|
action="provision",
|
2026-02-05 15:26:35 +05:30
|
|
|
)
|
2026-02-05 14:43:25 +05:30
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("", response_model=BoardOnboardingRead)
|
2026-02-06 16:12:04 +05:30
|
|
|
async def get_onboarding(
|
2026-02-09 15:49:50 +05:30
|
|
|
board: Board = BOARD_USER_READ_DEP,
|
|
|
|
|
session: AsyncSession = SESSION_DEP,
|
2026-02-05 14:43:25 +05:30
|
|
|
) -> BoardOnboardingSession:
|
2026-02-09 15:49:50 +05:30
|
|
|
"""Get the latest onboarding session for a board."""
|
2026-02-06 16:12:04 +05:30
|
|
|
onboarding = (
|
2026-02-09 02:04:14 +05:30
|
|
|
await BoardOnboardingSession.objects.filter_by(board_id=board.id)
|
|
|
|
|
.order_by(col(BoardOnboardingSession.updated_at).desc())
|
|
|
|
|
.first(session)
|
|
|
|
|
)
|
2026-02-05 14:43:25 +05:30
|
|
|
if onboarding is None:
|
|
|
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
|
|
|
|
return onboarding
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/start", response_model=BoardOnboardingRead)
|
|
|
|
|
async def start_onboarding(
|
2026-02-09 15:49:50 +05:30
|
|
|
_payload: BoardOnboardingStart,
|
|
|
|
|
board: Board = BOARD_USER_WRITE_DEP,
|
|
|
|
|
session: AsyncSession = SESSION_DEP,
|
2026-02-05 14:43:25 +05:30
|
|
|
) -> BoardOnboardingSession:
|
2026-02-10 00:45:15 +05:30
|
|
|
"""Start onboarding and send instructions to the gateway agent."""
|
2026-02-06 16:12:04 +05:30
|
|
|
onboarding = (
|
2026-02-09 02:04:14 +05:30
|
|
|
await BoardOnboardingSession.objects.filter_by(board_id=board.id)
|
|
|
|
|
.filter(col(BoardOnboardingSession.status) == "active")
|
|
|
|
|
.first(session)
|
|
|
|
|
)
|
2026-02-05 14:43:25 +05:30
|
|
|
if onboarding:
|
|
|
|
|
return onboarding
|
|
|
|
|
|
2026-02-06 16:12:04 +05:30
|
|
|
gateway, config = await _gateway_config(session, board)
|
2026-02-10 00:45:15 +05:30
|
|
|
session_key = gateway_agent_session_key(gateway)
|
2026-02-05 16:01:10 +05:30
|
|
|
base_url = settings.base_url or "http://localhost:8000"
|
2026-02-05 14:43:25 +05:30
|
|
|
prompt = (
|
|
|
|
|
"BOARD ONBOARDING REQUEST\n\n"
|
|
|
|
|
f"Board Name: {board.name}\n"
|
2026-02-10 00:45:15 +05:30
|
|
|
"You are the gateway agent. Ask the user 6-10 focused questions total:\n"
|
2026-02-06 21:56:16 +05:30
|
|
|
"- 3-6 questions to clarify the board goal.\n"
|
2026-02-09 16:23:41 +05:30
|
|
|
"- 1 question to choose a unique name for the board lead agent "
|
|
|
|
|
"(first-name style).\n"
|
|
|
|
|
"- 2-4 questions to capture the user's preferences for how the board "
|
|
|
|
|
"lead should work\n"
|
2026-02-06 21:56:16 +05:30
|
|
|
" (communication style, autonomy, update cadence, and output formatting).\n"
|
2026-02-09 16:23:41 +05:30
|
|
|
'- Always include a final question (and only once): "Anything else we '
|
|
|
|
|
'should know?"\n'
|
2026-02-07 12:33:00 +05:30
|
|
|
" (constraints, context, preferences). This MUST be the last question.\n"
|
|
|
|
|
' Provide an option like "Yes (I\'ll type it)" so they can enter free-text.\n'
|
|
|
|
|
" Do NOT ask for additional context on earlier questions.\n"
|
2026-02-09 16:23:41 +05:30
|
|
|
" Only include a free-text option on earlier questions if a typed "
|
|
|
|
|
"answer is necessary;\n"
|
|
|
|
|
' when you do, make the option label include "I\'ll type it" '
|
|
|
|
|
'(e.g., "Other (I\'ll type it)").\n'
|
|
|
|
|
'- If the user sends an "Additional context" message later, incorporate '
|
|
|
|
|
"it and resend status=complete\n"
|
2026-02-07 03:40:28 +05:30
|
|
|
" to update the draft (until the user confirms).\n"
|
2026-02-05 19:06:32 +05:30
|
|
|
"Do NOT respond in OpenClaw chat.\n"
|
|
|
|
|
"All onboarding responses MUST be sent to Mission Control via API.\n"
|
2026-02-05 16:01:10 +05:30
|
|
|
f"Mission Control base URL: {base_url}\n"
|
2026-02-05 19:06:32 +05:30
|
|
|
"Use the AUTH_TOKEN from USER.md or TOOLS.md and pass it as X-Agent-Token.\n"
|
|
|
|
|
"Onboarding response endpoint:\n"
|
|
|
|
|
f"POST {base_url}/api/v1/agent/boards/{board.id}/onboarding\n"
|
|
|
|
|
"QUESTION example (send JSON body exactly as shown):\n"
|
2026-02-06 02:43:08 +05:30
|
|
|
f'curl -s -X POST "{base_url}/api/v1/agent/boards/{board.id}/onboarding" '
|
|
|
|
|
'-H "X-Agent-Token: $AUTH_TOKEN" '
|
|
|
|
|
'-H "Content-Type: application/json" '
|
2026-02-09 16:23:41 +05:30
|
|
|
'-d \'{"question":"...","options":[{"id":"1","label":"..."},'
|
|
|
|
|
'{"id":"2","label":"..."}]}\'\n'
|
2026-02-05 19:06:32 +05:30
|
|
|
"COMPLETION example (send JSON body exactly as shown):\n"
|
2026-02-06 02:43:08 +05:30
|
|
|
f'curl -s -X POST "{base_url}/api/v1/agent/boards/{board.id}/onboarding" '
|
|
|
|
|
'-H "X-Agent-Token: $AUTH_TOKEN" '
|
|
|
|
|
'-H "Content-Type: application/json" '
|
2026-02-09 16:23:41 +05:30
|
|
|
'-d \'{"status":"complete","board_type":"goal","objective":"...",'
|
|
|
|
|
'"success_metrics":{"metric":"...","target":"..."},'
|
|
|
|
|
'"target_date":"YYYY-MM-DD",'
|
|
|
|
|
'"user_profile":{"preferred_name":"...","pronouns":"...",'
|
|
|
|
|
'"timezone":"...","notes":"...","context":"..."},'
|
|
|
|
|
'"lead_agent":{"name":"Ava","identity_profile":{"role":"Board Lead",'
|
|
|
|
|
'"communication_style":"direct, concise, practical","emoji":":gear:"},'
|
|
|
|
|
'"autonomy_level":"balanced","verbosity":"concise",'
|
|
|
|
|
'"output_format":"bullets","update_cadence":"daily",'
|
|
|
|
|
'"custom_instructions":"..."}}\'\n'
|
2026-02-06 21:56:16 +05:30
|
|
|
"ENUMS:\n"
|
|
|
|
|
"- board_type: goal | general\n"
|
|
|
|
|
"- lead_agent.autonomy_level: ask_first | balanced | autonomous\n"
|
|
|
|
|
"- lead_agent.verbosity: concise | balanced | detailed\n"
|
|
|
|
|
"- lead_agent.output_format: bullets | mixed | narrative\n"
|
|
|
|
|
"- lead_agent.update_cadence: asap | hourly | daily | weekly\n"
|
2026-02-09 16:23:41 +05:30
|
|
|
"QUESTION FORMAT (one question per response, no arrays, no markdown, "
|
|
|
|
|
"no extra text):\n"
|
2026-02-06 02:43:08 +05:30
|
|
|
'{"question":"...","options":[{"id":"1","label":"..."},{"id":"2","label":"..."}]}\n'
|
2026-02-05 19:06:32 +05:30
|
|
|
"Do NOT wrap questions in a list. Do NOT add commentary.\n"
|
2026-02-06 21:56:16 +05:30
|
|
|
"When you have enough info, send one final response with status=complete.\n"
|
2026-02-09 16:23:41 +05:30
|
|
|
"The completion payload must include board_type. If board_type=goal, "
|
|
|
|
|
"include objective + success_metrics.\n"
|
|
|
|
|
"Also include user_profile + lead_agent to configure the board lead's "
|
|
|
|
|
"working style.\n"
|
2026-02-05 14:43:25 +05:30
|
|
|
)
|
|
|
|
|
|
|
|
|
|
try:
|
2026-02-10 00:45:15 +05:30
|
|
|
await ensure_session(session_key, config=config, label="Gateway Agent")
|
2026-02-09 15:49:50 +05:30
|
|
|
await send_message(
|
2026-02-09 20:44:05 +05:30
|
|
|
prompt,
|
|
|
|
|
session_key=session_key,
|
|
|
|
|
config=config,
|
|
|
|
|
deliver=False,
|
2026-02-09 15:49:50 +05:30
|
|
|
)
|
2026-02-05 14:43:25 +05:30
|
|
|
except OpenClawGatewayError as exc:
|
2026-02-09 15:49:50 +05:30
|
|
|
raise HTTPException(
|
2026-02-09 20:44:05 +05:30
|
|
|
status_code=status.HTTP_502_BAD_GATEWAY,
|
|
|
|
|
detail=str(exc),
|
2026-02-09 15:49:50 +05:30
|
|
|
) from exc
|
2026-02-05 14:43:25 +05:30
|
|
|
|
|
|
|
|
onboarding = BoardOnboardingSession(
|
|
|
|
|
board_id=board.id,
|
|
|
|
|
session_key=session_key,
|
|
|
|
|
status="active",
|
2026-02-09 15:49:50 +05:30
|
|
|
messages=[
|
|
|
|
|
{"role": "user", "content": prompt, "timestamp": utcnow().isoformat()},
|
|
|
|
|
],
|
2026-02-05 14:43:25 +05:30
|
|
|
)
|
|
|
|
|
session.add(onboarding)
|
2026-02-06 16:12:04 +05:30
|
|
|
await session.commit()
|
|
|
|
|
await session.refresh(onboarding)
|
2026-02-05 14:43:25 +05:30
|
|
|
return onboarding
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/answer", response_model=BoardOnboardingRead)
|
|
|
|
|
async def answer_onboarding(
|
|
|
|
|
payload: BoardOnboardingAnswer,
|
2026-02-09 15:49:50 +05:30
|
|
|
board: Board = BOARD_USER_WRITE_DEP,
|
|
|
|
|
session: AsyncSession = SESSION_DEP,
|
2026-02-05 14:43:25 +05:30
|
|
|
) -> BoardOnboardingSession:
|
2026-02-10 00:45:15 +05:30
|
|
|
"""Send a user onboarding answer to the gateway agent."""
|
2026-02-06 16:12:04 +05:30
|
|
|
onboarding = (
|
2026-02-09 02:04:14 +05:30
|
|
|
await BoardOnboardingSession.objects.filter_by(board_id=board.id)
|
|
|
|
|
.order_by(col(BoardOnboardingSession.updated_at).desc())
|
|
|
|
|
.first(session)
|
|
|
|
|
)
|
2026-02-05 14:43:25 +05:30
|
|
|
if onboarding is None:
|
|
|
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
|
|
|
|
|
2026-02-06 16:12:04 +05:30
|
|
|
_, config = await _gateway_config(session, board)
|
2026-02-05 14:43:25 +05:30
|
|
|
answer_text = payload.answer
|
|
|
|
|
if payload.other_text:
|
|
|
|
|
answer_text = f"{payload.answer}: {payload.other_text}"
|
|
|
|
|
|
2026-02-05 19:06:32 +05:30
|
|
|
messages = list(onboarding.messages or [])
|
2026-02-09 15:49:50 +05:30
|
|
|
messages.append(
|
|
|
|
|
{"role": "user", "content": answer_text, "timestamp": utcnow().isoformat()},
|
|
|
|
|
)
|
2026-02-05 14:43:25 +05:30
|
|
|
|
|
|
|
|
try:
|
2026-02-10 00:45:15 +05:30
|
|
|
await ensure_session(onboarding.session_key, config=config, label="Gateway Agent")
|
2026-02-05 14:43:25 +05:30
|
|
|
await send_message(
|
2026-02-09 15:49:50 +05:30
|
|
|
answer_text,
|
|
|
|
|
session_key=onboarding.session_key,
|
|
|
|
|
config=config,
|
|
|
|
|
deliver=False,
|
2026-02-05 14:43:25 +05:30
|
|
|
)
|
|
|
|
|
except OpenClawGatewayError as exc:
|
2026-02-09 15:49:50 +05:30
|
|
|
raise HTTPException(
|
2026-02-09 20:44:05 +05:30
|
|
|
status_code=status.HTTP_502_BAD_GATEWAY,
|
|
|
|
|
detail=str(exc),
|
2026-02-09 15:49:50 +05:30
|
|
|
) from exc
|
2026-02-05 14:43:25 +05:30
|
|
|
|
2026-02-05 19:06:32 +05:30
|
|
|
onboarding.messages = messages
|
2026-02-06 16:12:04 +05:30
|
|
|
onboarding.updated_at = utcnow()
|
2026-02-05 19:06:32 +05:30
|
|
|
session.add(onboarding)
|
2026-02-06 16:12:04 +05:30
|
|
|
await session.commit()
|
|
|
|
|
await session.refresh(onboarding)
|
2026-02-05 19:06:32 +05:30
|
|
|
return onboarding
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/agent", response_model=BoardOnboardingRead)
|
2026-02-06 16:12:04 +05:30
|
|
|
async def agent_onboarding_update(
|
|
|
|
|
payload: BoardOnboardingAgentUpdate,
|
2026-02-09 15:49:50 +05:30
|
|
|
board: Board = BOARD_OR_404_DEP,
|
|
|
|
|
session: AsyncSession = SESSION_DEP,
|
|
|
|
|
actor: ActorContext = ACTOR_DEP,
|
2026-02-05 19:06:32 +05:30
|
|
|
) -> BoardOnboardingSession:
|
2026-02-10 00:45:15 +05:30
|
|
|
"""Store onboarding updates submitted by the gateway agent."""
|
2026-02-05 19:06:32 +05:30
|
|
|
if actor.actor_type != "agent" or actor.agent is None:
|
|
|
|
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
|
|
|
|
|
agent = actor.agent
|
|
|
|
|
if agent.board_id is not None:
|
|
|
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
|
|
|
|
|
|
|
|
|
if board.gateway_id:
|
2026-02-09 02:04:14 +05:30
|
|
|
gateway = await Gateway.objects.by_id(board.gateway_id).first(session)
|
2026-02-09 15:49:50 +05:30
|
|
|
if (
|
|
|
|
|
gateway
|
2026-02-10 02:58:58 +05:30
|
|
|
and (agent.gateway_id != gateway.id or agent.board_id is not None)
|
2026-02-09 15:49:50 +05:30
|
|
|
):
|
|
|
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
2026-02-05 19:06:32 +05:30
|
|
|
|
2026-02-06 16:12:04 +05:30
|
|
|
onboarding = (
|
2026-02-09 02:04:14 +05:30
|
|
|
await BoardOnboardingSession.objects.filter_by(board_id=board.id)
|
|
|
|
|
.order_by(col(BoardOnboardingSession.updated_at).desc())
|
|
|
|
|
.first(session)
|
|
|
|
|
)
|
2026-02-05 19:06:32 +05:30
|
|
|
if onboarding is None:
|
|
|
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
|
|
|
|
if onboarding.status == "confirmed":
|
|
|
|
|
raise HTTPException(status_code=status.HTTP_409_CONFLICT)
|
|
|
|
|
|
|
|
|
|
messages = list(onboarding.messages or [])
|
2026-02-06 16:12:04 +05:30
|
|
|
now = utcnow().isoformat()
|
|
|
|
|
payload_text = payload.model_dump_json(exclude_none=True)
|
|
|
|
|
payload_data = payload.model_dump(mode="json", exclude_none=True)
|
2026-02-05 19:06:32 +05:30
|
|
|
logger.info(
|
|
|
|
|
"onboarding.agent.update board_id=%s agent_id=%s payload=%s",
|
|
|
|
|
board.id,
|
|
|
|
|
agent.id,
|
|
|
|
|
payload_text,
|
|
|
|
|
)
|
2026-02-06 16:12:04 +05:30
|
|
|
if isinstance(payload, BoardOnboardingAgentComplete):
|
|
|
|
|
onboarding.draft_goal = payload_data
|
2026-02-05 19:06:32 +05:30
|
|
|
onboarding.status = "completed"
|
2026-02-09 15:49:50 +05:30
|
|
|
messages.append(
|
|
|
|
|
{"role": "assistant", "content": payload_text, "timestamp": now},
|
|
|
|
|
)
|
2026-02-05 19:06:32 +05:30
|
|
|
else:
|
2026-02-09 15:49:50 +05:30
|
|
|
messages.append(
|
|
|
|
|
{"role": "assistant", "content": payload_text, "timestamp": now},
|
|
|
|
|
)
|
2026-02-05 14:43:25 +05:30
|
|
|
|
|
|
|
|
onboarding.messages = messages
|
2026-02-06 16:12:04 +05:30
|
|
|
onboarding.updated_at = utcnow()
|
2026-02-05 14:43:25 +05:30
|
|
|
session.add(onboarding)
|
2026-02-06 16:12:04 +05:30
|
|
|
await session.commit()
|
|
|
|
|
await session.refresh(onboarding)
|
2026-02-05 19:06:32 +05:30
|
|
|
logger.info(
|
|
|
|
|
"onboarding.agent.update stored board_id=%s messages_count=%s status=%s",
|
|
|
|
|
board.id,
|
|
|
|
|
len(onboarding.messages or []),
|
|
|
|
|
onboarding.status,
|
|
|
|
|
)
|
2026-02-05 14:43:25 +05:30
|
|
|
return onboarding
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/confirm", response_model=BoardRead)
|
2026-02-09 17:24:21 +05:30
|
|
|
async def confirm_onboarding(
|
2026-02-05 14:43:25 +05:30
|
|
|
payload: BoardOnboardingConfirm,
|
2026-02-09 15:49:50 +05:30
|
|
|
board: Board = BOARD_USER_WRITE_DEP,
|
|
|
|
|
session: AsyncSession = SESSION_DEP,
|
|
|
|
|
auth: AuthContext = ADMIN_AUTH_DEP,
|
2026-02-05 14:43:25 +05:30
|
|
|
) -> Board:
|
2026-02-09 15:49:50 +05:30
|
|
|
"""Confirm onboarding results and provision the board lead agent."""
|
2026-02-06 16:12:04 +05:30
|
|
|
onboarding = (
|
2026-02-09 02:04:14 +05:30
|
|
|
await BoardOnboardingSession.objects.filter_by(board_id=board.id)
|
|
|
|
|
.order_by(col(BoardOnboardingSession.updated_at).desc())
|
|
|
|
|
.first(session)
|
|
|
|
|
)
|
2026-02-05 14:43:25 +05:30
|
|
|
if onboarding is None:
|
|
|
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
|
|
|
|
|
|
|
|
|
board.board_type = payload.board_type
|
|
|
|
|
board.objective = payload.objective
|
|
|
|
|
board.success_metrics = payload.success_metrics
|
|
|
|
|
board.target_date = payload.target_date
|
|
|
|
|
board.goal_confirmed = True
|
|
|
|
|
board.goal_source = "lead_agent_onboarding"
|
|
|
|
|
|
|
|
|
|
onboarding.status = "confirmed"
|
2026-02-06 16:12:04 +05:30
|
|
|
onboarding.updated_at = utcnow()
|
2026-02-05 14:43:25 +05:30
|
|
|
|
2026-02-09 17:24:21 +05:30
|
|
|
user_profile = _parse_draft_user_profile(onboarding.draft_goal)
|
|
|
|
|
if _apply_user_profile(auth, user_profile) and auth.user is not None:
|
|
|
|
|
session.add(auth.user)
|
2026-02-06 21:56:16 +05:30
|
|
|
|
2026-02-09 17:24:21 +05:30
|
|
|
lead_agent = _parse_draft_lead_agent(onboarding.draft_goal)
|
|
|
|
|
lead_options = _lead_agent_options(lead_agent)
|
2026-02-06 21:56:16 +05:30
|
|
|
|
2026-02-06 16:12:04 +05:30
|
|
|
gateway, config = await _gateway_config(session, board)
|
2026-02-05 14:43:25 +05:30
|
|
|
session.add(board)
|
|
|
|
|
session.add(onboarding)
|
2026-02-06 16:12:04 +05:30
|
|
|
await session.commit()
|
|
|
|
|
await session.refresh(board)
|
2026-02-09 17:24:21 +05:30
|
|
|
await ensure_board_lead_agent(
|
2026-02-06 21:56:16 +05:30
|
|
|
session,
|
2026-02-09 17:24:21 +05:30
|
|
|
request=LeadAgentRequest(
|
|
|
|
|
board=board,
|
|
|
|
|
gateway=gateway,
|
|
|
|
|
config=config,
|
|
|
|
|
user=auth.user,
|
|
|
|
|
options=lead_options,
|
|
|
|
|
),
|
2026-02-06 21:56:16 +05:30
|
|
|
)
|
2026-02-05 14:43:25 +05:30
|
|
|
return board
|