feat: use main agent for onboarding and auto-create lead

This commit is contained in:
Abhimanyu Saharan
2026-02-05 15:26:35 +05:30
parent 8e3b1b9d0d
commit ad4a4c5767

View File

@@ -3,15 +3,23 @@ from __future__ import annotations
import json import json
import re import re
from datetime import datetime from datetime import datetime
from uuid import uuid4
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from sqlmodel import Session, select from sqlmodel import Session, select
from app.api.deps import get_board_or_404, require_admin_auth from app.api.deps import get_board_or_404, require_admin_auth
from app.core.agent_tokens import generate_agent_token, hash_agent_token
from app.core.auth import AuthContext from app.core.auth import AuthContext
from app.db.session import get_session from app.db.session import get_session
from app.integrations.openclaw_gateway import GatewayConfig as GatewayClientConfig from app.integrations.openclaw_gateway import GatewayConfig as GatewayClientConfig
from app.integrations.openclaw_gateway import OpenClawGatewayError, ensure_session, get_chat_history, send_message from app.integrations.openclaw_gateway import (
OpenClawGatewayError,
ensure_session,
get_chat_history,
send_message,
)
from app.models.agents import Agent
from app.models.board_onboarding import BoardOnboardingSession from app.models.board_onboarding import BoardOnboardingSession
from app.models.boards import Board from app.models.boards import Board
from app.models.gateways import Gateway from app.models.gateways import Gateway
@@ -22,12 +30,10 @@ from app.schemas.board_onboarding import (
BoardOnboardingStart, BoardOnboardingStart,
) )
from app.schemas.boards import BoardRead from app.schemas.boards import BoardRead
from app.services.agent_provisioning import DEFAULT_HEARTBEAT_CONFIG, provision_agent
router = APIRouter(prefix="/boards/{board_id}/onboarding", tags=["board-onboarding"]) router = APIRouter(prefix="/boards/{board_id}/onboarding", tags=["board-onboarding"])
SESSION_PREFIX = "agent:main:onboarding:"
def _extract_json(text: str) -> dict[str, object] | None: def _extract_json(text: str) -> dict[str, object] | None:
try: try:
return json.loads(text.strip()) return json.loads(text.strip())
@@ -82,17 +88,77 @@ def _get_assistant_messages(history: object) -> list[str]:
return messages return messages
def _gateway_config(session: Session, board: Board) -> GatewayClientConfig: def _gateway_config(session: Session, board: Board) -> tuple[Gateway, GatewayClientConfig]:
if not board.gateway_id: if not board.gateway_id:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY) raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)
gateway = session.get(Gateway, board.gateway_id) gateway = session.get(Gateway, board.gateway_id)
if gateway is None or not gateway.url: if gateway is None or not gateway.url or not gateway.main_session_key:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY) raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)
return GatewayClientConfig(url=gateway.url, token=gateway.token) return gateway, GatewayClientConfig(url=gateway.url, token=gateway.token)
def _session_key(board: Board) -> str: def _build_session_key(agent_name: str) -> str:
return f"{SESSION_PREFIX}{board.id}" slug = re.sub(r"[^a-z0-9]+", "-", agent_name.lower()).strip("-")
return f"agent:{slug or uuid4().hex}:main"
def _lead_agent_name(board: Board) -> str:
return f"{board.name} Lead"
async def _ensure_lead_agent(
session: Session,
board: Board,
gateway: Gateway,
config: GatewayClientConfig,
auth: AuthContext,
) -> Agent:
existing = session.exec(
select(Agent)
.where(Agent.board_id == board.id)
.where(Agent.is_board_lead.is_(True))
).first()
if existing:
return existing
agent = Agent(
name=_lead_agent_name(board),
status="provisioning",
board_id=board.id,
is_board_lead=True,
heartbeat_config=DEFAULT_HEARTBEAT_CONFIG.copy(),
identity_profile={
"role": "Board Lead",
"communication_style": "direct, concise, practical",
"emoji": ":compass:",
},
)
raw_token = generate_agent_token()
agent.agent_token_hash = hash_agent_token(raw_token)
agent.provision_requested_at = datetime.utcnow()
agent.provision_action = "provision"
agent.openclaw_session_id = _build_session_key(agent.name)
session.add(agent)
session.commit()
session.refresh(agent)
try:
await provision_agent(agent, board, gateway, raw_token, auth.user, action="provision")
await ensure_session(agent.openclaw_session_id, config=config, label=agent.name)
await send_message(
(
f"Hello {agent.name}. Your workspace has been provisioned.\n\n"
"Start the agent, run BOOT.md, and if BOOTSTRAP.md exists run it once "
"then delete it. Begin heartbeats after startup."
),
session_key=agent.openclaw_session_id,
config=config,
deliver=True,
)
except OpenClawGatewayError:
# Best-effort provisioning. Board confirmation should still succeed.
pass
return agent
@router.get("", response_model=BoardOnboardingRead) @router.get("", response_model=BoardOnboardingRead)
@@ -126,20 +192,20 @@ async def start_onboarding(
if onboarding: if onboarding:
return onboarding return onboarding
config = _gateway_config(session, board) gateway, config = _gateway_config(session, board)
session_key = _session_key(board) session_key = gateway.main_session_key
prompt = ( prompt = (
"BOARD ONBOARDING REQUEST\n\n" "BOARD ONBOARDING REQUEST\n\n"
f"Board Name: {board.name}\n" f"Board Name: {board.name}\n"
"You are the lead agent. Ask the user 3-6 focused questions to clarify their goal.\n" "You are the main agent. Ask the user 3-6 focused questions to clarify their goal.\n"
"Return questions as JSON: {\"question\": \"...\", \"options\": [...]}.\n" "Return questions as JSON: {\"question\": \"...\", \"options\": [...]}.\n"
"When you have enough info, return JSON: {\"status\": \"complete\", \"board_type\": \"goal\"|\"general\", " "When you have enough info, return JSON: {\"status\": \"complete\", \"board_type\": \"goal\"|\"general\", "
"\"objective\": \"...\", \"success_metrics\": {...}, \"target_date\": \"YYYY-MM-DD\"}." "\"objective\": \"...\", \"success_metrics\": {...}, \"target_date\": \"YYYY-MM-DD\"}."
) )
try: try:
await ensure_session(session_key, config=config, label=f"Onboarding {board.name}") await ensure_session(session_key, config=config, label="Main Agent")
await send_message(prompt, session_key=session_key, config=config, deliver=True) await send_message(prompt, session_key=session_key, config=config, deliver=False)
except OpenClawGatewayError as exc: except OpenClawGatewayError as exc:
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(exc)) from exc raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(exc)) from exc
@@ -170,7 +236,7 @@ async def answer_onboarding(
if onboarding is None: if onboarding is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
config = _gateway_config(session, board) _, config = _gateway_config(session, board)
answer_text = payload.answer answer_text = payload.answer
if payload.other_text: if payload.other_text:
answer_text = f"{payload.answer}: {payload.other_text}" answer_text = f"{payload.answer}: {payload.other_text}"
@@ -181,9 +247,9 @@ async def answer_onboarding(
) )
try: try:
await ensure_session(onboarding.session_key, config=config, label=f"Onboarding {board.name}") await ensure_session(onboarding.session_key, config=config, label="Main Agent")
await send_message( await send_message(
answer_text, session_key=onboarding.session_key, config=config, deliver=True answer_text, session_key=onboarding.session_key, config=config, deliver=False
) )
history = await get_chat_history(onboarding.session_key, config=config) history = await get_chat_history(onboarding.session_key, config=config)
except OpenClawGatewayError as exc: except OpenClawGatewayError as exc:
@@ -209,7 +275,7 @@ async def answer_onboarding(
@router.post("/confirm", response_model=BoardRead) @router.post("/confirm", response_model=BoardRead)
def confirm_onboarding( async def confirm_onboarding(
payload: BoardOnboardingConfirm, payload: BoardOnboardingConfirm,
board: Board = Depends(get_board_or_404), board: Board = Depends(get_board_or_404),
session: Session = Depends(get_session), session: Session = Depends(get_session),
@@ -233,8 +299,10 @@ def confirm_onboarding(
onboarding.status = "confirmed" onboarding.status = "confirmed"
onboarding.updated_at = datetime.utcnow() onboarding.updated_at = datetime.utcnow()
gateway, config = _gateway_config(session, board)
session.add(board) session.add(board)
session.add(onboarding) session.add(onboarding)
session.commit() session.commit()
session.refresh(board) session.refresh(board)
await _ensure_lead_agent(session, board, gateway, config, auth)
return board return board