refactor: standardize runtime annotation types across multiple files
This commit is contained in:
@@ -63,7 +63,11 @@ from app.schemas.tasks import (
|
|||||||
TaskUpdate,
|
TaskUpdate,
|
||||||
)
|
)
|
||||||
from app.services.activity_log import record_activity
|
from app.services.activity_log import record_activity
|
||||||
from app.services.board_leads import ensure_board_lead_agent
|
from app.services.board_leads import (
|
||||||
|
LeadAgentOptions,
|
||||||
|
LeadAgentRequest,
|
||||||
|
ensure_board_lead_agent,
|
||||||
|
)
|
||||||
from app.services.task_dependencies import (
|
from app.services.task_dependencies import (
|
||||||
blocked_by_dependency_ids,
|
blocked_by_dependency_ids,
|
||||||
dependency_status_by_id,
|
dependency_status_by_id,
|
||||||
@@ -113,6 +117,29 @@ class SoulUpdateRequest(SQLModel):
|
|||||||
reason: str | None = None
|
reason: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class AgentTaskListFilters(SQLModel):
|
||||||
|
"""Query filters for board task listing in agent routes."""
|
||||||
|
|
||||||
|
status_filter: str | None = None
|
||||||
|
assigned_agent_id: UUID | None = None
|
||||||
|
unassigned: bool | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def _task_list_filters(
|
||||||
|
status_filter: str | None = TASK_STATUS_QUERY,
|
||||||
|
assigned_agent_id: UUID | None = None,
|
||||||
|
unassigned: bool | None = None,
|
||||||
|
) -> AgentTaskListFilters:
|
||||||
|
return AgentTaskListFilters(
|
||||||
|
status_filter=status_filter,
|
||||||
|
assigned_agent_id=assigned_agent_id,
|
||||||
|
unassigned=unassigned,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
TASK_LIST_FILTERS_DEP = Depends(_task_list_filters)
|
||||||
|
|
||||||
|
|
||||||
def _actor(agent_ctx: AgentAuthContext) -> ActorContext:
|
def _actor(agent_ctx: AgentAuthContext) -> ActorContext:
|
||||||
return ActorContext(actor_type="agent", agent=agent_ctx.agent)
|
return ActorContext(actor_type="agent", agent=agent_ctx.agent)
|
||||||
|
|
||||||
@@ -217,19 +244,16 @@ async def list_agents(
|
|||||||
statement = statement.where(Agent.board_id == agent_ctx.agent.board_id)
|
statement = statement.where(Agent.board_id == agent_ctx.agent.board_id)
|
||||||
elif board_id:
|
elif board_id:
|
||||||
statement = statement.where(Agent.board_id == board_id)
|
statement = statement.where(Agent.board_id == board_id)
|
||||||
get_gateway_main_session_keys = (
|
main_session_keys = await agents_api.get_gateway_main_session_keys(session)
|
||||||
agents_api._get_gateway_main_session_keys # noqa: SLF001
|
|
||||||
)
|
|
||||||
to_agent_read = agents_api._to_agent_read # noqa: SLF001
|
|
||||||
with_computed_status = agents_api._with_computed_status # noqa: SLF001
|
|
||||||
|
|
||||||
main_session_keys = await get_gateway_main_session_keys(session)
|
|
||||||
statement = statement.order_by(col(Agent.created_at).desc())
|
statement = statement.order_by(col(Agent.created_at).desc())
|
||||||
|
|
||||||
def _transform(items: Sequence[Any]) -> Sequence[Any]:
|
def _transform(items: Sequence[Any]) -> Sequence[Any]:
|
||||||
agents = cast(Sequence[Agent], items)
|
agents = cast(Sequence[Agent], items)
|
||||||
return [
|
return [
|
||||||
to_agent_read(with_computed_status(agent), main_session_keys)
|
agents_api.to_agent_read(
|
||||||
|
agents_api.with_computed_status(agent),
|
||||||
|
main_session_keys,
|
||||||
|
)
|
||||||
for agent in agents
|
for agent in agents
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -237,10 +261,8 @@ async def list_agents(
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/boards/{board_id}/tasks", response_model=DefaultLimitOffsetPage[TaskRead])
|
@router.get("/boards/{board_id}/tasks", response_model=DefaultLimitOffsetPage[TaskRead])
|
||||||
async def list_tasks( # noqa: PLR0913
|
async def list_tasks(
|
||||||
status_filter: str | None = TASK_STATUS_QUERY,
|
filters: AgentTaskListFilters = TASK_LIST_FILTERS_DEP,
|
||||||
assigned_agent_id: UUID | None = None,
|
|
||||||
unassigned: bool | None = None,
|
|
||||||
board: Board = BOARD_DEP,
|
board: Board = BOARD_DEP,
|
||||||
session: AsyncSession = SESSION_DEP,
|
session: AsyncSession = SESSION_DEP,
|
||||||
agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
|
agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
|
||||||
@@ -248,9 +270,9 @@ async def list_tasks( # noqa: PLR0913
|
|||||||
"""List tasks on a board with optional status and assignment filters."""
|
"""List tasks on a board with optional status and assignment filters."""
|
||||||
_guard_board_access(agent_ctx, board)
|
_guard_board_access(agent_ctx, board)
|
||||||
return await tasks_api.list_tasks(
|
return await tasks_api.list_tasks(
|
||||||
status_filter=status_filter,
|
status_filter=filters.status_filter,
|
||||||
assigned_agent_id=assigned_agent_id,
|
assigned_agent_id=filters.assigned_agent_id,
|
||||||
unassigned=unassigned,
|
unassigned=filters.unassigned,
|
||||||
board=board,
|
board=board,
|
||||||
session=session,
|
session=session,
|
||||||
actor=_actor(agent_ctx),
|
actor=_actor(agent_ctx),
|
||||||
@@ -336,10 +358,7 @@ async def create_task(
|
|||||||
session,
|
session,
|
||||||
)
|
)
|
||||||
if assigned_agent:
|
if assigned_agent:
|
||||||
notify_agent_on_task_assign = (
|
await tasks_api.notify_agent_on_task_assign(
|
||||||
tasks_api._notify_agent_on_task_assign # noqa: SLF001
|
|
||||||
)
|
|
||||||
await notify_agent_on_task_assign(
|
|
||||||
session=session,
|
session=session,
|
||||||
board=board,
|
board=board,
|
||||||
task=task,
|
task=task,
|
||||||
@@ -821,11 +840,13 @@ async def message_gateway_board_lead(
|
|||||||
board = await _require_gateway_board(session, gateway=gateway, board_id=board_id)
|
board = await _require_gateway_board(session, gateway=gateway, board_id=board_id)
|
||||||
lead, lead_created = await ensure_board_lead_agent(
|
lead, lead_created = await ensure_board_lead_agent(
|
||||||
session,
|
session,
|
||||||
|
request=LeadAgentRequest(
|
||||||
board=board,
|
board=board,
|
||||||
gateway=gateway,
|
gateway=gateway,
|
||||||
config=config,
|
config=config,
|
||||||
user=None,
|
user=None,
|
||||||
action="provision",
|
options=LeadAgentOptions(action="provision"),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
if not lead.openclaw_session_id:
|
if not lead.openclaw_session_id:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -932,11 +953,13 @@ async def broadcast_gateway_lead_message(
|
|||||||
try:
|
try:
|
||||||
lead, _lead_created = await ensure_board_lead_agent(
|
lead, _lead_created = await ensure_board_lead_agent(
|
||||||
session,
|
session,
|
||||||
|
request=LeadAgentRequest(
|
||||||
board=board,
|
board=board,
|
||||||
gateway=gateway,
|
gateway=gateway,
|
||||||
config=config,
|
config=config,
|
||||||
user=None,
|
user=None,
|
||||||
action="provision",
|
options=LeadAgentOptions(action="provision"),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
lead_session_key = _require_lead_session_key(lead)
|
lead_session_key = _require_lead_session_key(lead)
|
||||||
message = (
|
message = (
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -3,9 +3,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import re
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
from uuid import uuid4
|
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
@@ -19,7 +17,6 @@ from app.api.deps import (
|
|||||||
require_admin_auth,
|
require_admin_auth,
|
||||||
require_admin_or_agent,
|
require_admin_or_agent,
|
||||||
)
|
)
|
||||||
from app.core.agent_tokens import generate_agent_token, hash_agent_token
|
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from app.core.time import utcnow
|
from app.core.time import utcnow
|
||||||
from app.db.session import get_session
|
from app.db.session import get_session
|
||||||
@@ -29,7 +26,6 @@ from app.integrations.openclaw_gateway import (
|
|||||||
ensure_session,
|
ensure_session,
|
||||||
send_message,
|
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.gateways import Gateway
|
from app.models.gateways import Gateway
|
||||||
from app.schemas.board_onboarding import (
|
from app.schemas.board_onboarding import (
|
||||||
@@ -43,7 +39,11 @@ from app.schemas.board_onboarding import (
|
|||||||
BoardOnboardingUserProfile,
|
BoardOnboardingUserProfile,
|
||||||
)
|
)
|
||||||
from app.schemas.boards import BoardRead
|
from app.schemas.boards import BoardRead
|
||||||
from app.services.agent_provisioning import DEFAULT_HEARTBEAT_CONFIG, provision_agent
|
from app.services.board_leads import (
|
||||||
|
LeadAgentOptions,
|
||||||
|
LeadAgentRequest,
|
||||||
|
ensure_board_lead_agent,
|
||||||
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||||
@@ -72,93 +72,85 @@ async def _gateway_config(
|
|||||||
return gateway, GatewayClientConfig(url=gateway.url, token=gateway.token)
|
return gateway, GatewayClientConfig(url=gateway.url, token=gateway.token)
|
||||||
|
|
||||||
|
|
||||||
def _build_session_key(agent_name: str) -> str:
|
def _parse_draft_user_profile(
|
||||||
slug = re.sub(r"[^a-z0-9]+", "-", agent_name.lower()).strip("-")
|
draft_goal: object,
|
||||||
return f"agent:{slug or uuid4().hex}:main"
|
) -> BoardOnboardingUserProfile | None:
|
||||||
|
if not isinstance(draft_goal, dict):
|
||||||
|
return None
|
||||||
def _lead_agent_name(_board: Board) -> str:
|
raw_profile = draft_goal.get("user_profile")
|
||||||
return "Lead Agent"
|
if raw_profile is None:
|
||||||
|
return None
|
||||||
|
|
||||||
def _lead_session_key(board: Board) -> str:
|
|
||||||
return f"agent:lead-{board.id}:main"
|
|
||||||
|
|
||||||
|
|
||||||
async def _ensure_lead_agent( # noqa: PLR0913
|
|
||||||
session: AsyncSession,
|
|
||||||
board: Board,
|
|
||||||
gateway: Gateway,
|
|
||||||
config: GatewayClientConfig,
|
|
||||||
auth: AuthContext,
|
|
||||||
*,
|
|
||||||
agent_name: str | None = None,
|
|
||||||
identity_profile: dict[str, str] | None = None,
|
|
||||||
) -> Agent:
|
|
||||||
existing = (
|
|
||||||
await Agent.objects.filter_by(board_id=board.id)
|
|
||||||
.filter(col(Agent.is_board_lead).is_(True))
|
|
||||||
.first(session)
|
|
||||||
)
|
|
||||||
if existing:
|
|
||||||
desired_name = agent_name or _lead_agent_name(board)
|
|
||||||
if existing.name != desired_name:
|
|
||||||
existing.name = desired_name
|
|
||||||
session.add(existing)
|
|
||||||
await session.commit()
|
|
||||||
await session.refresh(existing)
|
|
||||||
return existing
|
|
||||||
|
|
||||||
merged_identity_profile = {
|
|
||||||
"role": "Board Lead",
|
|
||||||
"communication_style": "direct, concise, practical",
|
|
||||||
"emoji": ":gear:",
|
|
||||||
}
|
|
||||||
if identity_profile:
|
|
||||||
merged_identity_profile.update(
|
|
||||||
{
|
|
||||||
key: value.strip()
|
|
||||||
for key, value in identity_profile.items()
|
|
||||||
if value.strip()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
agent = Agent(
|
|
||||||
name=agent_name or _lead_agent_name(board),
|
|
||||||
status="provisioning",
|
|
||||||
board_id=board.id,
|
|
||||||
is_board_lead=True,
|
|
||||||
heartbeat_config=DEFAULT_HEARTBEAT_CONFIG.copy(),
|
|
||||||
identity_profile=merged_identity_profile,
|
|
||||||
)
|
|
||||||
raw_token = generate_agent_token()
|
|
||||||
agent.agent_token_hash = hash_agent_token(raw_token)
|
|
||||||
agent.provision_requested_at = utcnow()
|
|
||||||
agent.provision_action = "provision"
|
|
||||||
agent.openclaw_session_id = _lead_session_key(board)
|
|
||||||
session.add(agent)
|
|
||||||
await session.commit()
|
|
||||||
await session.refresh(agent)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await provision_agent(
|
return BoardOnboardingUserProfile.model_validate(raw_profile)
|
||||||
agent, board, gateway, raw_token, auth.user, action="provision",
|
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
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_user_profile(
|
||||||
|
auth: AuthContext,
|
||||||
|
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")
|
||||||
|
|
||||||
|
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",
|
||||||
)
|
)
|
||||||
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)
|
||||||
@@ -400,7 +392,7 @@ async def agent_onboarding_update(
|
|||||||
|
|
||||||
|
|
||||||
@router.post("/confirm", response_model=BoardRead)
|
@router.post("/confirm", response_model=BoardRead)
|
||||||
async def confirm_onboarding( # noqa: C901, PLR0912, PLR0915
|
async def confirm_onboarding(
|
||||||
payload: BoardOnboardingConfirm,
|
payload: BoardOnboardingConfirm,
|
||||||
board: Board = BOARD_USER_WRITE_DEP,
|
board: Board = BOARD_USER_WRITE_DEP,
|
||||||
session: AsyncSession = SESSION_DEP,
|
session: AsyncSession = SESSION_DEP,
|
||||||
@@ -425,73 +417,26 @@ async def confirm_onboarding( # noqa: C901, PLR0912, PLR0915
|
|||||||
onboarding.status = "confirmed"
|
onboarding.status = "confirmed"
|
||||||
onboarding.updated_at = utcnow()
|
onboarding.updated_at = utcnow()
|
||||||
|
|
||||||
user_profile: BoardOnboardingUserProfile | None = None
|
user_profile = _parse_draft_user_profile(onboarding.draft_goal)
|
||||||
lead_agent: BoardOnboardingLeadAgentDraft | None = None
|
if _apply_user_profile(auth, user_profile) and auth.user is not None:
|
||||||
if isinstance(onboarding.draft_goal, dict):
|
|
||||||
raw_profile = onboarding.draft_goal.get("user_profile")
|
|
||||||
if raw_profile is not None:
|
|
||||||
try:
|
|
||||||
user_profile = BoardOnboardingUserProfile.model_validate(raw_profile)
|
|
||||||
except ValidationError:
|
|
||||||
user_profile = None
|
|
||||||
raw_lead = onboarding.draft_goal.get("lead_agent")
|
|
||||||
if raw_lead is not None:
|
|
||||||
try:
|
|
||||||
lead_agent = BoardOnboardingLeadAgentDraft.model_validate(raw_lead)
|
|
||||||
except ValidationError:
|
|
||||||
lead_agent = None
|
|
||||||
|
|
||||||
if auth.user and user_profile:
|
|
||||||
changed = False
|
|
||||||
if user_profile.preferred_name is not None:
|
|
||||||
auth.user.preferred_name = user_profile.preferred_name
|
|
||||||
changed = True
|
|
||||||
if user_profile.pronouns is not None:
|
|
||||||
auth.user.pronouns = user_profile.pronouns
|
|
||||||
changed = True
|
|
||||||
if user_profile.timezone is not None:
|
|
||||||
auth.user.timezone = user_profile.timezone
|
|
||||||
changed = True
|
|
||||||
if user_profile.notes is not None:
|
|
||||||
auth.user.notes = user_profile.notes
|
|
||||||
changed = True
|
|
||||||
if user_profile.context is not None:
|
|
||||||
auth.user.context = user_profile.context
|
|
||||||
changed = True
|
|
||||||
if changed:
|
|
||||||
session.add(auth.user)
|
session.add(auth.user)
|
||||||
|
|
||||||
lead_identity_profile: dict[str, str] = {}
|
lead_agent = _parse_draft_lead_agent(onboarding.draft_goal)
|
||||||
lead_name: str | None = None
|
lead_options = _lead_agent_options(lead_agent)
|
||||||
if lead_agent:
|
|
||||||
lead_name = lead_agent.name
|
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
gateway, config = await _gateway_config(session, board)
|
gateway, config = await _gateway_config(session, board)
|
||||||
session.add(board)
|
session.add(board)
|
||||||
session.add(onboarding)
|
session.add(onboarding)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
await session.refresh(board)
|
await session.refresh(board)
|
||||||
await _ensure_lead_agent(
|
await ensure_board_lead_agent(
|
||||||
session,
|
session,
|
||||||
board,
|
request=LeadAgentRequest(
|
||||||
gateway,
|
board=board,
|
||||||
config,
|
gateway=gateway,
|
||||||
auth,
|
config=config,
|
||||||
agent_name=lead_name,
|
user=auth.user,
|
||||||
identity_profile=lead_identity_profile or None,
|
options=lead_options,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
return board
|
return board
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ from app.schemas.gateways import (
|
|||||||
from app.schemas.pagination import DefaultLimitOffsetPage
|
from app.schemas.pagination import DefaultLimitOffsetPage
|
||||||
from app.services.agent_provisioning import (
|
from app.services.agent_provisioning import (
|
||||||
DEFAULT_HEARTBEAT_CONFIG,
|
DEFAULT_HEARTBEAT_CONFIG,
|
||||||
|
MainAgentProvisionRequest,
|
||||||
|
ProvisionOptions,
|
||||||
provision_main_agent,
|
provision_main_agent,
|
||||||
)
|
)
|
||||||
from app.services.template_sync import (
|
from app.services.template_sync import (
|
||||||
@@ -187,7 +189,15 @@ async def _ensure_main_agent(
|
|||||||
await session.commit()
|
await session.commit()
|
||||||
await session.refresh(agent)
|
await session.refresh(agent)
|
||||||
try:
|
try:
|
||||||
await provision_main_agent(agent, gateway, raw_token, auth.user, action=action)
|
await provision_main_agent(
|
||||||
|
agent,
|
||||||
|
MainAgentProvisionRequest(
|
||||||
|
gateway=gateway,
|
||||||
|
auth_token=raw_token,
|
||||||
|
user=auth.user,
|
||||||
|
options=ProvisionOptions(action=action),
|
||||||
|
),
|
||||||
|
)
|
||||||
await ensure_session(
|
await ensure_session(
|
||||||
gateway.main_session_key,
|
gateway.main_session_key,
|
||||||
config=GatewayClientConfig(url=gateway.url, token=gateway.token),
|
config=GatewayClientConfig(url=gateway.url, token=gateway.token),
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime # noqa: TCH003
|
from datetime import datetime
|
||||||
from uuid import UUID, uuid4
|
from uuid import UUID, uuid4
|
||||||
|
|
||||||
from sqlmodel import Field
|
from sqlmodel import Field
|
||||||
@@ -10,6 +10,8 @@ from sqlmodel import Field
|
|||||||
from app.core.time import utcnow
|
from app.core.time import utcnow
|
||||||
from app.models.base import QueryModel
|
from app.models.base import QueryModel
|
||||||
|
|
||||||
|
RUNTIME_ANNOTATION_TYPES = (datetime,)
|
||||||
|
|
||||||
|
|
||||||
class ActivityEvent(QueryModel, table=True):
|
class ActivityEvent(QueryModel, table=True):
|
||||||
"""Discrete activity event tied to tasks and agents."""
|
"""Discrete activity event tied to tasks and agents."""
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime # noqa: TCH003
|
from datetime import datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from uuid import UUID, uuid4
|
from uuid import UUID, uuid4
|
||||||
|
|
||||||
@@ -12,6 +12,8 @@ from sqlmodel import Field
|
|||||||
from app.core.time import utcnow
|
from app.core.time import utcnow
|
||||||
from app.models.base import QueryModel
|
from app.models.base import QueryModel
|
||||||
|
|
||||||
|
RUNTIME_ANNOTATION_TYPES = (datetime,)
|
||||||
|
|
||||||
|
|
||||||
class Agent(QueryModel, table=True):
|
class Agent(QueryModel, table=True):
|
||||||
"""Agent configuration and lifecycle state persisted in the database."""
|
"""Agent configuration and lifecycle state persisted in the database."""
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime # noqa: TCH003
|
from datetime import datetime
|
||||||
from uuid import UUID, uuid4
|
from uuid import UUID, uuid4
|
||||||
|
|
||||||
from sqlalchemy import JSON, Column
|
from sqlalchemy import JSON, Column
|
||||||
@@ -11,6 +11,8 @@ from sqlmodel import Field
|
|||||||
from app.core.time import utcnow
|
from app.core.time import utcnow
|
||||||
from app.models.base import QueryModel
|
from app.models.base import QueryModel
|
||||||
|
|
||||||
|
RUNTIME_ANNOTATION_TYPES = (datetime,)
|
||||||
|
|
||||||
|
|
||||||
class Approval(QueryModel, table=True):
|
class Approval(QueryModel, table=True):
|
||||||
"""Approval request and decision metadata for gated operations."""
|
"""Approval request and decision metadata for gated operations."""
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime # noqa: TCH003
|
from datetime import datetime
|
||||||
from uuid import UUID, uuid4
|
from uuid import UUID, uuid4
|
||||||
|
|
||||||
from sqlalchemy import JSON, Column
|
from sqlalchemy import JSON, Column
|
||||||
@@ -11,6 +11,8 @@ from sqlmodel import Field
|
|||||||
from app.core.time import utcnow
|
from app.core.time import utcnow
|
||||||
from app.models.base import QueryModel
|
from app.models.base import QueryModel
|
||||||
|
|
||||||
|
RUNTIME_ANNOTATION_TYPES = (datetime,)
|
||||||
|
|
||||||
|
|
||||||
class BoardGroupMemory(QueryModel, table=True):
|
class BoardGroupMemory(QueryModel, table=True):
|
||||||
"""Persisted memory items associated with a board group."""
|
"""Persisted memory items associated with a board group."""
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime # noqa: TCH003
|
from datetime import datetime
|
||||||
from uuid import UUID, uuid4
|
from uuid import UUID, uuid4
|
||||||
|
|
||||||
from sqlmodel import Field
|
from sqlmodel import Field
|
||||||
@@ -10,6 +10,8 @@ from sqlmodel import Field
|
|||||||
from app.core.time import utcnow
|
from app.core.time import utcnow
|
||||||
from app.models.tenancy import TenantScoped
|
from app.models.tenancy import TenantScoped
|
||||||
|
|
||||||
|
RUNTIME_ANNOTATION_TYPES = (datetime,)
|
||||||
|
|
||||||
|
|
||||||
class BoardGroup(TenantScoped, table=True):
|
class BoardGroup(TenantScoped, table=True):
|
||||||
"""Logical grouping container for boards within an organization."""
|
"""Logical grouping container for boards within an organization."""
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime # noqa: TCH003
|
from datetime import datetime
|
||||||
from uuid import UUID, uuid4
|
from uuid import UUID, uuid4
|
||||||
|
|
||||||
from sqlalchemy import JSON, Column
|
from sqlalchemy import JSON, Column
|
||||||
@@ -11,6 +11,8 @@ from sqlmodel import Field
|
|||||||
from app.core.time import utcnow
|
from app.core.time import utcnow
|
||||||
from app.models.base import QueryModel
|
from app.models.base import QueryModel
|
||||||
|
|
||||||
|
RUNTIME_ANNOTATION_TYPES = (datetime,)
|
||||||
|
|
||||||
|
|
||||||
class BoardMemory(QueryModel, table=True):
|
class BoardMemory(QueryModel, table=True):
|
||||||
"""Persisted memory item attached directly to a board."""
|
"""Persisted memory item attached directly to a board."""
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime # noqa: TCH003
|
from datetime import datetime
|
||||||
from uuid import UUID, uuid4
|
from uuid import UUID, uuid4
|
||||||
|
|
||||||
from sqlalchemy import JSON, Column
|
from sqlalchemy import JSON, Column
|
||||||
@@ -11,6 +11,8 @@ from sqlmodel import Field
|
|||||||
from app.core.time import utcnow
|
from app.core.time import utcnow
|
||||||
from app.models.base import QueryModel
|
from app.models.base import QueryModel
|
||||||
|
|
||||||
|
RUNTIME_ANNOTATION_TYPES = (datetime,)
|
||||||
|
|
||||||
|
|
||||||
class BoardOnboardingSession(QueryModel, table=True):
|
class BoardOnboardingSession(QueryModel, table=True):
|
||||||
"""Persisted onboarding conversation and draft goal data for a board."""
|
"""Persisted onboarding conversation and draft goal data for a board."""
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime # noqa: TCH003
|
from datetime import datetime
|
||||||
from uuid import UUID, uuid4
|
from uuid import UUID, uuid4
|
||||||
|
|
||||||
from sqlalchemy import JSON, Column
|
from sqlalchemy import JSON, Column
|
||||||
@@ -11,6 +11,8 @@ from sqlmodel import Field
|
|||||||
from app.core.time import utcnow
|
from app.core.time import utcnow
|
||||||
from app.models.tenancy import TenantScoped
|
from app.models.tenancy import TenantScoped
|
||||||
|
|
||||||
|
RUNTIME_ANNOTATION_TYPES = (datetime,)
|
||||||
|
|
||||||
|
|
||||||
class Board(TenantScoped, table=True):
|
class Board(TenantScoped, table=True):
|
||||||
"""Primary board entity grouping tasks, agents, and goal metadata."""
|
"""Primary board entity grouping tasks, agents, and goal metadata."""
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime # noqa: TCH003
|
from datetime import datetime
|
||||||
from uuid import UUID, uuid4
|
from uuid import UUID, uuid4
|
||||||
|
|
||||||
from sqlmodel import Field
|
from sqlmodel import Field
|
||||||
@@ -10,6 +10,8 @@ from sqlmodel import Field
|
|||||||
from app.core.time import utcnow
|
from app.core.time import utcnow
|
||||||
from app.models.base import QueryModel
|
from app.models.base import QueryModel
|
||||||
|
|
||||||
|
RUNTIME_ANNOTATION_TYPES = (datetime,)
|
||||||
|
|
||||||
|
|
||||||
class Gateway(QueryModel, table=True):
|
class Gateway(QueryModel, table=True):
|
||||||
"""Configured external gateway endpoint and authentication settings."""
|
"""Configured external gateway endpoint and authentication settings."""
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime # noqa: TCH003
|
from datetime import datetime
|
||||||
from uuid import UUID, uuid4
|
from uuid import UUID, uuid4
|
||||||
|
|
||||||
from sqlalchemy import UniqueConstraint
|
from sqlalchemy import UniqueConstraint
|
||||||
@@ -11,6 +11,8 @@ from sqlmodel import Field
|
|||||||
from app.core.time import utcnow
|
from app.core.time import utcnow
|
||||||
from app.models.base import QueryModel
|
from app.models.base import QueryModel
|
||||||
|
|
||||||
|
RUNTIME_ANNOTATION_TYPES = (datetime,)
|
||||||
|
|
||||||
|
|
||||||
class OrganizationBoardAccess(QueryModel, table=True):
|
class OrganizationBoardAccess(QueryModel, table=True):
|
||||||
"""Member-specific board permissions within an organization."""
|
"""Member-specific board permissions within an organization."""
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime # noqa: TCH003
|
from datetime import datetime
|
||||||
from uuid import UUID, uuid4
|
from uuid import UUID, uuid4
|
||||||
|
|
||||||
from sqlalchemy import UniqueConstraint
|
from sqlalchemy import UniqueConstraint
|
||||||
@@ -11,6 +11,8 @@ from sqlmodel import Field
|
|||||||
from app.core.time import utcnow
|
from app.core.time import utcnow
|
||||||
from app.models.base import QueryModel
|
from app.models.base import QueryModel
|
||||||
|
|
||||||
|
RUNTIME_ANNOTATION_TYPES = (datetime,)
|
||||||
|
|
||||||
|
|
||||||
class OrganizationInviteBoardAccess(QueryModel, table=True):
|
class OrganizationInviteBoardAccess(QueryModel, table=True):
|
||||||
"""Invite-specific board permissions applied after invite acceptance."""
|
"""Invite-specific board permissions applied after invite acceptance."""
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime # noqa: TCH003
|
from datetime import datetime
|
||||||
from uuid import UUID, uuid4
|
from uuid import UUID, uuid4
|
||||||
|
|
||||||
from sqlalchemy import UniqueConstraint
|
from sqlalchemy import UniqueConstraint
|
||||||
@@ -11,6 +11,8 @@ from sqlmodel import Field
|
|||||||
from app.core.time import utcnow
|
from app.core.time import utcnow
|
||||||
from app.models.base import QueryModel
|
from app.models.base import QueryModel
|
||||||
|
|
||||||
|
RUNTIME_ANNOTATION_TYPES = (datetime,)
|
||||||
|
|
||||||
|
|
||||||
class OrganizationInvite(QueryModel, table=True):
|
class OrganizationInvite(QueryModel, table=True):
|
||||||
"""Invitation record granting prospective organization access."""
|
"""Invitation record granting prospective organization access."""
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime # noqa: TCH003
|
from datetime import datetime
|
||||||
from uuid import UUID, uuid4
|
from uuid import UUID, uuid4
|
||||||
|
|
||||||
from sqlalchemy import UniqueConstraint
|
from sqlalchemy import UniqueConstraint
|
||||||
@@ -11,6 +11,8 @@ from sqlmodel import Field
|
|||||||
from app.core.time import utcnow
|
from app.core.time import utcnow
|
||||||
from app.models.base import QueryModel
|
from app.models.base import QueryModel
|
||||||
|
|
||||||
|
RUNTIME_ANNOTATION_TYPES = (datetime,)
|
||||||
|
|
||||||
|
|
||||||
class OrganizationMember(QueryModel, table=True):
|
class OrganizationMember(QueryModel, table=True):
|
||||||
"""Membership row linking a user to an organization and permissions."""
|
"""Membership row linking a user to an organization and permissions."""
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime # noqa: TCH003
|
from datetime import datetime
|
||||||
from uuid import UUID, uuid4
|
from uuid import UUID, uuid4
|
||||||
|
|
||||||
from sqlalchemy import UniqueConstraint
|
from sqlalchemy import UniqueConstraint
|
||||||
@@ -11,6 +11,8 @@ from sqlmodel import Field
|
|||||||
from app.core.time import utcnow
|
from app.core.time import utcnow
|
||||||
from app.models.base import QueryModel
|
from app.models.base import QueryModel
|
||||||
|
|
||||||
|
RUNTIME_ANNOTATION_TYPES = (datetime,)
|
||||||
|
|
||||||
|
|
||||||
class Organization(QueryModel, table=True):
|
class Organization(QueryModel, table=True):
|
||||||
"""Top-level organization tenant record."""
|
"""Top-level organization tenant record."""
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime # noqa: TCH003
|
from datetime import datetime
|
||||||
from uuid import UUID, uuid4
|
from uuid import UUID, uuid4
|
||||||
|
|
||||||
from sqlalchemy import CheckConstraint, UniqueConstraint
|
from sqlalchemy import CheckConstraint, UniqueConstraint
|
||||||
@@ -11,6 +11,8 @@ from sqlmodel import Field
|
|||||||
from app.core.time import utcnow
|
from app.core.time import utcnow
|
||||||
from app.models.tenancy import TenantScoped
|
from app.models.tenancy import TenantScoped
|
||||||
|
|
||||||
|
RUNTIME_ANNOTATION_TYPES = (datetime,)
|
||||||
|
|
||||||
|
|
||||||
class TaskDependency(TenantScoped, table=True):
|
class TaskDependency(TenantScoped, table=True):
|
||||||
"""Directed dependency edge between two tasks in the same board."""
|
"""Directed dependency edge between two tasks in the same board."""
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime # noqa: TCH003
|
from datetime import datetime
|
||||||
from uuid import UUID, uuid4
|
from uuid import UUID, uuid4
|
||||||
|
|
||||||
from sqlmodel import Field
|
from sqlmodel import Field
|
||||||
@@ -10,6 +10,8 @@ from sqlmodel import Field
|
|||||||
from app.core.time import utcnow
|
from app.core.time import utcnow
|
||||||
from app.models.base import QueryModel
|
from app.models.base import QueryModel
|
||||||
|
|
||||||
|
RUNTIME_ANNOTATION_TYPES = (datetime,)
|
||||||
|
|
||||||
|
|
||||||
class TaskFingerprint(QueryModel, table=True):
|
class TaskFingerprint(QueryModel, table=True):
|
||||||
"""Hashed task-content fingerprint associated with a board and task."""
|
"""Hashed task-content fingerprint associated with a board and task."""
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime # noqa: TCH003
|
from datetime import datetime
|
||||||
from uuid import UUID, uuid4
|
from uuid import UUID, uuid4
|
||||||
|
|
||||||
from sqlmodel import Field
|
from sqlmodel import Field
|
||||||
@@ -10,6 +10,8 @@ from sqlmodel import Field
|
|||||||
from app.core.time import utcnow
|
from app.core.time import utcnow
|
||||||
from app.models.tenancy import TenantScoped
|
from app.models.tenancy import TenantScoped
|
||||||
|
|
||||||
|
RUNTIME_ANNOTATION_TYPES = (datetime,)
|
||||||
|
|
||||||
|
|
||||||
class Task(TenantScoped, table=True):
|
class Task(TenantScoped, table=True):
|
||||||
"""Board-scoped task entity with ownership, status, and timing fields."""
|
"""Board-scoped task entity with ownership, status, and timing fields."""
|
||||||
|
|||||||
@@ -2,11 +2,13 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime # noqa: TCH003
|
from datetime import datetime
|
||||||
from uuid import UUID # noqa: TCH003
|
from uuid import UUID
|
||||||
|
|
||||||
from sqlmodel import SQLModel
|
from sqlmodel import SQLModel
|
||||||
|
|
||||||
|
RUNTIME_ANNOTATION_TYPES = (datetime, UUID)
|
||||||
|
|
||||||
|
|
||||||
class ActivityEventRead(SQLModel):
|
class ActivityEventRead(SQLModel):
|
||||||
"""Serialized activity event payload returned by activity endpoints."""
|
"""Serialized activity event payload returned by activity endpoints."""
|
||||||
|
|||||||
@@ -2,15 +2,16 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime # noqa: TCH003
|
from datetime import datetime
|
||||||
from typing import Literal, Self
|
from typing import Literal, Self
|
||||||
from uuid import UUID # noqa: TCH003
|
from uuid import UUID
|
||||||
|
|
||||||
from pydantic import model_validator
|
from pydantic import model_validator
|
||||||
from sqlmodel import SQLModel
|
from sqlmodel import SQLModel
|
||||||
|
|
||||||
ApprovalStatus = Literal["pending", "approved", "rejected"]
|
ApprovalStatus = Literal["pending", "approved", "rejected"]
|
||||||
STATUS_REQUIRED_ERROR = "status is required"
|
STATUS_REQUIRED_ERROR = "status is required"
|
||||||
|
RUNTIME_ANNOTATION_TYPES = (datetime, UUID)
|
||||||
|
|
||||||
|
|
||||||
class ApprovalBase(SQLModel):
|
class ApprovalBase(SQLModel):
|
||||||
|
|||||||
@@ -3,10 +3,12 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from uuid import UUID # noqa: TCH003
|
from uuid import UUID
|
||||||
|
|
||||||
from sqlmodel import SQLModel
|
from sqlmodel import SQLModel
|
||||||
|
|
||||||
|
RUNTIME_ANNOTATION_TYPES = (UUID,)
|
||||||
|
|
||||||
|
|
||||||
class BoardGroupHeartbeatApply(SQLModel):
|
class BoardGroupHeartbeatApply(SQLModel):
|
||||||
"""Request payload for heartbeat policy updates."""
|
"""Request payload for heartbeat policy updates."""
|
||||||
|
|||||||
@@ -2,12 +2,14 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime # noqa: TCH003
|
from datetime import datetime
|
||||||
from uuid import UUID # noqa: TCH003
|
from uuid import UUID
|
||||||
|
|
||||||
from sqlmodel import SQLModel
|
from sqlmodel import SQLModel
|
||||||
|
|
||||||
from app.schemas.common import NonEmptyStr # noqa: TCH001
|
from app.schemas.common import NonEmptyStr
|
||||||
|
|
||||||
|
RUNTIME_ANNOTATION_TYPES = (datetime, UUID, NonEmptyStr)
|
||||||
|
|
||||||
|
|
||||||
class BoardGroupMemoryCreate(SQLModel):
|
class BoardGroupMemoryCreate(SQLModel):
|
||||||
|
|||||||
@@ -2,11 +2,13 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime # noqa: TCH003
|
from datetime import datetime
|
||||||
from uuid import UUID # noqa: TCH003
|
from uuid import UUID
|
||||||
|
|
||||||
from sqlmodel import SQLModel
|
from sqlmodel import SQLModel
|
||||||
|
|
||||||
|
RUNTIME_ANNOTATION_TYPES = (datetime, UUID)
|
||||||
|
|
||||||
|
|
||||||
class BoardGroupBase(SQLModel):
|
class BoardGroupBase(SQLModel):
|
||||||
"""Shared board-group fields for create/read operations."""
|
"""Shared board-group fields for create/read operations."""
|
||||||
|
|||||||
@@ -2,12 +2,14 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime # noqa: TCH003
|
from datetime import datetime
|
||||||
from uuid import UUID # noqa: TCH003
|
from uuid import UUID
|
||||||
|
|
||||||
from sqlmodel import SQLModel
|
from sqlmodel import SQLModel
|
||||||
|
|
||||||
from app.schemas.common import NonEmptyStr # noqa: TCH001
|
from app.schemas.common import NonEmptyStr
|
||||||
|
|
||||||
|
RUNTIME_ANNOTATION_TYPES = (datetime, UUID, NonEmptyStr)
|
||||||
|
|
||||||
|
|
||||||
class BoardMemoryCreate(SQLModel):
|
class BoardMemoryCreate(SQLModel):
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime # noqa: TCH003
|
from datetime import datetime
|
||||||
from typing import Self
|
from typing import Self
|
||||||
from uuid import UUID # noqa: TCH003
|
from uuid import UUID
|
||||||
|
|
||||||
from pydantic import model_validator
|
from pydantic import model_validator
|
||||||
from sqlmodel import SQLModel
|
from sqlmodel import SQLModel
|
||||||
@@ -13,6 +13,7 @@ _ERR_GOAL_FIELDS_REQUIRED = (
|
|||||||
"Confirmed goal boards require objective and success_metrics"
|
"Confirmed goal boards require objective and success_metrics"
|
||||||
)
|
)
|
||||||
_ERR_GATEWAY_REQUIRED = "gateway_id is required"
|
_ERR_GATEWAY_REQUIRED = "gateway_id is required"
|
||||||
|
RUNTIME_ANNOTATION_TYPES = (datetime, UUID)
|
||||||
|
|
||||||
|
|
||||||
class BoardBase(SQLModel):
|
class BoardBase(SQLModel):
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from sqlmodel import SQLModel
|
from sqlmodel import SQLModel
|
||||||
|
|
||||||
from app.schemas.common import NonEmptyStr # noqa: TCH001
|
from app.schemas.common import NonEmptyStr
|
||||||
|
|
||||||
|
RUNTIME_ANNOTATION_TYPES = (NonEmptyStr,)
|
||||||
|
|
||||||
|
|
||||||
class GatewaySessionMessageRequest(SQLModel):
|
class GatewaySessionMessageRequest(SQLModel):
|
||||||
|
|||||||
@@ -3,11 +3,13 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
from uuid import UUID # noqa: TCH003
|
from uuid import UUID
|
||||||
|
|
||||||
from sqlmodel import Field, SQLModel
|
from sqlmodel import Field, SQLModel
|
||||||
|
|
||||||
from app.schemas.common import NonEmptyStr # noqa: TCH001
|
from app.schemas.common import NonEmptyStr
|
||||||
|
|
||||||
|
RUNTIME_ANNOTATION_TYPES = (UUID, NonEmptyStr)
|
||||||
|
|
||||||
|
|
||||||
def _lead_reply_tags() -> list[str]:
|
def _lead_reply_tags() -> list[str]:
|
||||||
|
|||||||
@@ -2,12 +2,14 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime # noqa: TCH003
|
from datetime import datetime
|
||||||
from uuid import UUID # noqa: TCH003
|
from uuid import UUID
|
||||||
|
|
||||||
from pydantic import field_validator
|
from pydantic import field_validator
|
||||||
from sqlmodel import Field, SQLModel
|
from sqlmodel import Field, SQLModel
|
||||||
|
|
||||||
|
RUNTIME_ANNOTATION_TYPES = (datetime, UUID)
|
||||||
|
|
||||||
|
|
||||||
class GatewayBase(SQLModel):
|
class GatewayBase(SQLModel):
|
||||||
"""Shared gateway fields used across create/read payloads."""
|
"""Shared gateway fields used across create/read payloads."""
|
||||||
|
|||||||
@@ -2,11 +2,13 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime # noqa: TCH003
|
from datetime import datetime
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
|
|
||||||
from sqlmodel import SQLModel
|
from sqlmodel import SQLModel
|
||||||
|
|
||||||
|
RUNTIME_ANNOTATION_TYPES = (datetime,)
|
||||||
|
|
||||||
|
|
||||||
class DashboardSeriesPoint(SQLModel):
|
class DashboardSeriesPoint(SQLModel):
|
||||||
"""Single numeric time-series point."""
|
"""Single numeric time-series point."""
|
||||||
|
|||||||
@@ -2,11 +2,13 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime # noqa: TCH003
|
from datetime import datetime
|
||||||
from uuid import UUID # noqa: TCH003
|
from uuid import UUID
|
||||||
|
|
||||||
from sqlmodel import Field, SQLModel
|
from sqlmodel import Field, SQLModel
|
||||||
|
|
||||||
|
RUNTIME_ANNOTATION_TYPES = (datetime, UUID)
|
||||||
|
|
||||||
|
|
||||||
class OrganizationRead(SQLModel):
|
class OrganizationRead(SQLModel):
|
||||||
"""Organization payload returned by read endpoints."""
|
"""Organization payload returned by read endpoints."""
|
||||||
|
|||||||
@@ -2,17 +2,20 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime # noqa: TCH003
|
from datetime import datetime
|
||||||
from typing import Literal, Self
|
from typing import Literal, Self
|
||||||
from uuid import UUID # noqa: TCH003
|
from uuid import UUID
|
||||||
|
|
||||||
from pydantic import field_validator, model_validator
|
from pydantic import field_validator, model_validator
|
||||||
from sqlmodel import Field, SQLModel
|
from sqlmodel import Field, SQLModel
|
||||||
|
|
||||||
from app.schemas.common import NonEmptyStr # noqa: TCH001
|
from app.schemas.common import NonEmptyStr
|
||||||
|
|
||||||
TaskStatus = Literal["inbox", "in_progress", "review", "done"]
|
TaskStatus = Literal["inbox", "in_progress", "review", "done"]
|
||||||
STATUS_REQUIRED_ERROR = "status is required"
|
STATUS_REQUIRED_ERROR = "status is required"
|
||||||
|
# Keep these symbols as runtime globals so Pydantic can resolve
|
||||||
|
# deferred annotations reliably.
|
||||||
|
RUNTIME_ANNOTATION_TYPES = (datetime, UUID, NonEmptyStr)
|
||||||
|
|
||||||
|
|
||||||
class TaskBase(SQLModel):
|
class TaskBase(SQLModel):
|
||||||
|
|||||||
@@ -2,10 +2,12 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from uuid import UUID # noqa: TCH003
|
from uuid import UUID
|
||||||
|
|
||||||
from sqlmodel import SQLModel
|
from sqlmodel import SQLModel
|
||||||
|
|
||||||
|
RUNTIME_ANNOTATION_TYPES = (UUID,)
|
||||||
|
|
||||||
|
|
||||||
class UserBase(SQLModel):
|
class UserBase(SQLModel):
|
||||||
"""Common user profile fields shared across user payload schemas."""
|
"""Common user profile fields shared across user payload schemas."""
|
||||||
|
|||||||
@@ -2,18 +2,28 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime # noqa: TCH003
|
from datetime import datetime
|
||||||
from uuid import UUID # noqa: TCH003
|
from uuid import UUID
|
||||||
|
|
||||||
from sqlmodel import Field, SQLModel
|
from sqlmodel import Field, SQLModel
|
||||||
|
|
||||||
from app.schemas.agents import AgentRead # noqa: TCH001
|
from app.schemas.agents import AgentRead
|
||||||
from app.schemas.approvals import ApprovalRead # noqa: TCH001
|
from app.schemas.approvals import ApprovalRead
|
||||||
from app.schemas.board_groups import BoardGroupRead # noqa: TCH001
|
from app.schemas.board_groups import BoardGroupRead
|
||||||
from app.schemas.board_memory import BoardMemoryRead # noqa: TCH001
|
from app.schemas.board_memory import BoardMemoryRead
|
||||||
from app.schemas.boards import BoardRead # noqa: TCH001
|
from app.schemas.boards import BoardRead
|
||||||
from app.schemas.tasks import TaskRead
|
from app.schemas.tasks import TaskRead
|
||||||
|
|
||||||
|
RUNTIME_ANNOTATION_TYPES = (
|
||||||
|
datetime,
|
||||||
|
UUID,
|
||||||
|
AgentRead,
|
||||||
|
ApprovalRead,
|
||||||
|
BoardGroupRead,
|
||||||
|
BoardMemoryRead,
|
||||||
|
BoardRead,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TaskCardRead(TaskRead):
|
class TaskCardRead(TaskRead):
|
||||||
"""Task read model enriched with assignee and approval counters."""
|
"""Task read model enriched with assignee and approval counters."""
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import hashlib
|
|||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
|
from dataclasses import dataclass, field
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING, Any, cast
|
from typing import TYPE_CHECKING, Any, cast
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
@@ -88,6 +89,36 @@ MAIN_TEMPLATE_MAP = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class ProvisionOptions:
|
||||||
|
"""Toggles controlling provisioning write/reset behavior."""
|
||||||
|
|
||||||
|
action: str = "provision"
|
||||||
|
force_bootstrap: bool = False
|
||||||
|
reset_session: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class AgentProvisionRequest:
|
||||||
|
"""Inputs required to provision a board-scoped agent."""
|
||||||
|
|
||||||
|
board: Board
|
||||||
|
gateway: Gateway
|
||||||
|
auth_token: str
|
||||||
|
user: User | None
|
||||||
|
options: ProvisionOptions = field(default_factory=ProvisionOptions)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class MainAgentProvisionRequest:
|
||||||
|
"""Inputs required to provision a gateway main agent."""
|
||||||
|
|
||||||
|
gateway: Gateway
|
||||||
|
auth_token: str
|
||||||
|
user: User | None
|
||||||
|
options: ProvisionOptions = field(default_factory=ProvisionOptions)
|
||||||
|
|
||||||
|
|
||||||
def _repo_root() -> Path:
|
def _repo_root() -> Path:
|
||||||
return Path(__file__).resolve().parents[3]
|
return Path(__file__).resolve().parents[3]
|
||||||
|
|
||||||
@@ -114,31 +145,48 @@ def _agent_id_from_session_key(session_key: str | None) -> str | None:
|
|||||||
return agent_id or None
|
return agent_id or None
|
||||||
|
|
||||||
|
|
||||||
def _extract_agent_id(payload: object) -> str | None: # noqa: C901
|
def _clean_str(value: object) -> str | None:
|
||||||
def _from_list(items: object) -> str | None:
|
if isinstance(value, str) and value.strip():
|
||||||
|
return value.strip()
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_agent_id_from_item(item: object) -> str | None:
|
||||||
|
if isinstance(item, str):
|
||||||
|
return _clean_str(item)
|
||||||
|
if not isinstance(item, dict):
|
||||||
|
return None
|
||||||
|
for key in ("id", "agentId", "agent_id"):
|
||||||
|
agent_id = _clean_str(item.get(key))
|
||||||
|
if agent_id:
|
||||||
|
return agent_id
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_agent_id_from_list(items: object) -> str | None:
|
||||||
if not isinstance(items, list):
|
if not isinstance(items, list):
|
||||||
return None
|
return None
|
||||||
for item in items:
|
for item in items:
|
||||||
if isinstance(item, str) and item.strip():
|
agent_id = _extract_agent_id_from_item(item)
|
||||||
return item.strip()
|
if agent_id:
|
||||||
if not isinstance(item, dict):
|
return agent_id
|
||||||
continue
|
|
||||||
for key in ("id", "agentId", "agent_id"):
|
|
||||||
raw = item.get(key)
|
|
||||||
if isinstance(raw, str) and raw.strip():
|
|
||||||
return raw.strip()
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_agent_id(payload: object) -> str | None:
|
||||||
|
default_keys = ("defaultId", "default_id", "defaultAgentId", "default_agent_id")
|
||||||
|
collection_keys = ("agents", "items", "list", "data")
|
||||||
|
|
||||||
if isinstance(payload, list):
|
if isinstance(payload, list):
|
||||||
return _from_list(payload)
|
return _extract_agent_id_from_list(payload)
|
||||||
if not isinstance(payload, dict):
|
if not isinstance(payload, dict):
|
||||||
return None
|
return None
|
||||||
for key in ("defaultId", "default_id", "defaultAgentId", "default_agent_id"):
|
for key in default_keys:
|
||||||
raw = payload.get(key)
|
agent_id = _clean_str(payload.get(key))
|
||||||
if isinstance(raw, str) and raw.strip():
|
if agent_id:
|
||||||
return raw.strip()
|
return agent_id
|
||||||
for key in ("agents", "items", "list", "data"):
|
for key in collection_keys:
|
||||||
agent_id = _from_list(payload.get(key))
|
agent_id = _extract_agent_id_from_list(payload.get(key))
|
||||||
if agent_id:
|
if agent_id:
|
||||||
return agent_id
|
return agent_id
|
||||||
return None
|
return None
|
||||||
@@ -523,42 +571,44 @@ async def _patch_gateway_agent_list(
|
|||||||
await openclaw_call("config.patch", params, config=config)
|
await openclaw_call("config.patch", params, config=config)
|
||||||
|
|
||||||
|
|
||||||
async def patch_gateway_agent_heartbeats( # noqa: C901
|
async def _gateway_config_agent_list(
|
||||||
gateway: Gateway,
|
config: GatewayClientConfig,
|
||||||
*,
|
) -> tuple[str | None, list[object]]:
|
||||||
entries: list[tuple[str, str, dict[str, Any]]],
|
|
||||||
) -> None:
|
|
||||||
"""Patch multiple agent heartbeat configs in a single gateway config.patch call.
|
|
||||||
|
|
||||||
Each entry is (agent_id, workspace_path, heartbeat_dict).
|
|
||||||
"""
|
|
||||||
if not gateway.url:
|
|
||||||
msg = "Gateway url is required"
|
|
||||||
raise OpenClawGatewayError(msg)
|
|
||||||
config = GatewayClientConfig(url=gateway.url, token=gateway.token)
|
|
||||||
cfg = await openclaw_call("config.get", config=config)
|
cfg = await openclaw_call("config.get", config=config)
|
||||||
if not isinstance(cfg, dict):
|
if not isinstance(cfg, dict):
|
||||||
msg = "config.get returned invalid payload"
|
msg = "config.get returned invalid payload"
|
||||||
raise OpenClawGatewayError(msg)
|
raise OpenClawGatewayError(msg)
|
||||||
base_hash = cfg.get("hash")
|
|
||||||
data = cfg.get("config") or cfg.get("parsed") or {}
|
data = cfg.get("config") or cfg.get("parsed") or {}
|
||||||
if not isinstance(data, dict):
|
if not isinstance(data, dict):
|
||||||
msg = "config.get returned invalid config"
|
msg = "config.get returned invalid config"
|
||||||
raise OpenClawGatewayError(msg)
|
raise OpenClawGatewayError(msg)
|
||||||
|
|
||||||
agents_section = data.get("agents") or {}
|
agents_section = data.get("agents") or {}
|
||||||
lst = agents_section.get("list") or []
|
agents_list = agents_section.get("list") or []
|
||||||
if not isinstance(lst, list):
|
if not isinstance(agents_list, list):
|
||||||
msg = "config agents.list is not a list"
|
msg = "config agents.list is not a list"
|
||||||
raise OpenClawGatewayError(msg)
|
raise OpenClawGatewayError(msg)
|
||||||
|
return cfg.get("hash"), agents_list
|
||||||
|
|
||||||
entry_by_id: dict[str, tuple[str, dict[str, Any]]] = {
|
|
||||||
|
def _heartbeat_entry_map(
|
||||||
|
entries: list[tuple[str, str, dict[str, Any]]],
|
||||||
|
) -> dict[str, tuple[str, dict[str, Any]]]:
|
||||||
|
return {
|
||||||
agent_id: (workspace_path, heartbeat)
|
agent_id: (workspace_path, heartbeat)
|
||||||
for agent_id, workspace_path, heartbeat in entries
|
for agent_id, workspace_path, heartbeat in entries
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _updated_agent_list(
|
||||||
|
raw_list: list[object],
|
||||||
|
entry_by_id: dict[str, tuple[str, dict[str, Any]]],
|
||||||
|
) -> list[object]:
|
||||||
updated_ids: set[str] = set()
|
updated_ids: set[str] = set()
|
||||||
new_list: list[dict[str, Any]] = []
|
new_list: list[object] = []
|
||||||
for raw_entry in lst:
|
|
||||||
|
for raw_entry in raw_list:
|
||||||
if not isinstance(raw_entry, dict):
|
if not isinstance(raw_entry, dict):
|
||||||
new_list.append(raw_entry)
|
new_list.append(raw_entry)
|
||||||
continue
|
continue
|
||||||
@@ -566,6 +616,7 @@ async def patch_gateway_agent_heartbeats( # noqa: C901
|
|||||||
if not isinstance(agent_id, str) or agent_id not in entry_by_id:
|
if not isinstance(agent_id, str) or agent_id not in entry_by_id:
|
||||||
new_list.append(raw_entry)
|
new_list.append(raw_entry)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
workspace_path, heartbeat = entry_by_id[agent_id]
|
workspace_path, heartbeat = entry_by_id[agent_id]
|
||||||
new_entry = dict(raw_entry)
|
new_entry = dict(raw_entry)
|
||||||
new_entry["workspace"] = workspace_path
|
new_entry["workspace"] = workspace_path
|
||||||
@@ -580,6 +631,26 @@ async def patch_gateway_agent_heartbeats( # noqa: C901
|
|||||||
{"id": agent_id, "workspace": workspace_path, "heartbeat": heartbeat},
|
{"id": agent_id, "workspace": workspace_path, "heartbeat": heartbeat},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return new_list
|
||||||
|
|
||||||
|
|
||||||
|
async def patch_gateway_agent_heartbeats(
|
||||||
|
gateway: Gateway,
|
||||||
|
*,
|
||||||
|
entries: list[tuple[str, str, dict[str, Any]]],
|
||||||
|
) -> None:
|
||||||
|
"""Patch multiple agent heartbeat configs in a single gateway config.patch call.
|
||||||
|
|
||||||
|
Each entry is (agent_id, workspace_path, heartbeat_dict).
|
||||||
|
"""
|
||||||
|
if not gateway.url:
|
||||||
|
msg = "Gateway url is required"
|
||||||
|
raise OpenClawGatewayError(msg)
|
||||||
|
config = GatewayClientConfig(url=gateway.url, token=gateway.token)
|
||||||
|
base_hash, raw_list = await _gateway_config_agent_list(config)
|
||||||
|
entry_by_id = _heartbeat_entry_map(entries)
|
||||||
|
new_list = _updated_agent_list(raw_list, entry_by_id)
|
||||||
|
|
||||||
patch = {"agents": {"list": new_list}}
|
patch = {"agents": {"list": new_list}}
|
||||||
params = {"raw": json.dumps(patch)}
|
params = {"raw": json.dumps(patch)}
|
||||||
if base_hash:
|
if base_hash:
|
||||||
@@ -656,18 +727,52 @@ async def _get_gateway_agent_entry(
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
async def provision_agent( # noqa: C901, PLR0912, PLR0913
|
def _should_include_bootstrap(
|
||||||
agent: Agent,
|
|
||||||
board: Board,
|
|
||||||
gateway: Gateway,
|
|
||||||
auth_token: str,
|
|
||||||
user: User | None,
|
|
||||||
*,
|
*,
|
||||||
action: str = "provision",
|
action: str,
|
||||||
force_bootstrap: bool = False,
|
force_bootstrap: bool,
|
||||||
reset_session: bool = False,
|
existing_files: dict[str, dict[str, Any]],
|
||||||
|
) -> bool:
|
||||||
|
if action != "update" or force_bootstrap:
|
||||||
|
return True
|
||||||
|
if not existing_files:
|
||||||
|
return False
|
||||||
|
entry = existing_files.get("BOOTSTRAP.md")
|
||||||
|
return not (entry and entry.get("missing") is True)
|
||||||
|
|
||||||
|
|
||||||
|
async def _set_agent_files(
|
||||||
|
*,
|
||||||
|
agent_id: str,
|
||||||
|
rendered: dict[str, str],
|
||||||
|
existing_files: dict[str, dict[str, Any]],
|
||||||
|
client_config: GatewayClientConfig,
|
||||||
|
) -> None:
|
||||||
|
for name, content in rendered.items():
|
||||||
|
if content == "":
|
||||||
|
continue
|
||||||
|
if name in PRESERVE_AGENT_EDITABLE_FILES:
|
||||||
|
entry = existing_files.get(name)
|
||||||
|
if entry and entry.get("missing") is not True:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
await openclaw_call(
|
||||||
|
"agents.files.set",
|
||||||
|
{"agentId": agent_id, "name": name, "content": content},
|
||||||
|
config=client_config,
|
||||||
|
)
|
||||||
|
except OpenClawGatewayError as exc:
|
||||||
|
if "unsupported file" in str(exc).lower():
|
||||||
|
continue
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
async def provision_agent(
|
||||||
|
agent: Agent,
|
||||||
|
request: AgentProvisionRequest,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Provision or update a regular board agent workspace."""
|
"""Provision or update a regular board agent workspace."""
|
||||||
|
gateway = request.gateway
|
||||||
if not gateway.url:
|
if not gateway.url:
|
||||||
return
|
return
|
||||||
if not gateway.workspace_root:
|
if not gateway.workspace_root:
|
||||||
@@ -682,18 +787,21 @@ async def provision_agent( # noqa: C901, PLR0912, PLR0913
|
|||||||
heartbeat = _heartbeat_config(agent)
|
heartbeat = _heartbeat_config(agent)
|
||||||
await _patch_gateway_agent_list(agent_id, workspace_path, heartbeat, client_config)
|
await _patch_gateway_agent_list(agent_id, workspace_path, heartbeat, client_config)
|
||||||
|
|
||||||
context = _build_context(agent, board, gateway, auth_token, user)
|
context = _build_context(
|
||||||
|
agent,
|
||||||
|
request.board,
|
||||||
|
gateway,
|
||||||
|
request.auth_token,
|
||||||
|
request.user,
|
||||||
|
)
|
||||||
supported = set(await _supported_gateway_files(client_config))
|
supported = set(await _supported_gateway_files(client_config))
|
||||||
supported.update({"USER.md", "SELF.md", "AUTONOMY.md"})
|
supported.update({"USER.md", "SELF.md", "AUTONOMY.md"})
|
||||||
existing_files = await _gateway_agent_files_index(agent_id, client_config)
|
existing_files = await _gateway_agent_files_index(agent_id, client_config)
|
||||||
include_bootstrap = True
|
include_bootstrap = _should_include_bootstrap(
|
||||||
if action == "update" and not force_bootstrap:
|
action=request.options.action,
|
||||||
if not existing_files:
|
force_bootstrap=request.options.force_bootstrap,
|
||||||
include_bootstrap = False
|
existing_files=existing_files,
|
||||||
else:
|
)
|
||||||
entry = existing_files.get("BOOTSTRAP.md")
|
|
||||||
if entry and entry.get("missing") is True:
|
|
||||||
include_bootstrap = False
|
|
||||||
|
|
||||||
rendered = _render_agent_files(
|
rendered = _render_agent_files(
|
||||||
context,
|
context,
|
||||||
@@ -710,41 +818,22 @@ async def provision_agent( # noqa: C901, PLR0912, PLR0913
|
|||||||
with suppress(OSError):
|
with suppress(OSError):
|
||||||
# Local workspace may not be writable/available; fall back to gateway API.
|
# Local workspace may not be writable/available; fall back to gateway API.
|
||||||
_ensure_workspace_file(workspace_path, name, content, overwrite=False)
|
_ensure_workspace_file(workspace_path, name, content, overwrite=False)
|
||||||
for name, content in rendered.items():
|
await _set_agent_files(
|
||||||
if content == "":
|
agent_id=agent_id,
|
||||||
continue
|
rendered=rendered,
|
||||||
if name in PRESERVE_AGENT_EDITABLE_FILES:
|
existing_files=existing_files,
|
||||||
# Never overwrite; only provision if missing.
|
client_config=client_config,
|
||||||
entry = existing_files.get(name)
|
|
||||||
if entry and entry.get("missing") is not True:
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
await openclaw_call(
|
|
||||||
"agents.files.set",
|
|
||||||
{"agentId": agent_id, "name": name, "content": content},
|
|
||||||
config=client_config,
|
|
||||||
)
|
)
|
||||||
except OpenClawGatewayError as exc:
|
if request.options.reset_session:
|
||||||
# Gateways may restrict file names. Skip unsupported files rather than
|
|
||||||
# failing provisioning for the entire agent.
|
|
||||||
if "unsupported file" in str(exc).lower():
|
|
||||||
continue
|
|
||||||
raise
|
|
||||||
if reset_session:
|
|
||||||
await _reset_session(session_key, client_config)
|
await _reset_session(session_key, client_config)
|
||||||
|
|
||||||
|
|
||||||
async def provision_main_agent( # noqa: C901, PLR0912, PLR0913
|
async def provision_main_agent(
|
||||||
agent: Agent,
|
agent: Agent,
|
||||||
gateway: Gateway,
|
request: MainAgentProvisionRequest,
|
||||||
auth_token: str,
|
|
||||||
user: User | None,
|
|
||||||
*,
|
|
||||||
action: str = "provision",
|
|
||||||
force_bootstrap: bool = False,
|
|
||||||
reset_session: bool = False,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Provision or update the gateway main agent workspace."""
|
"""Provision or update the gateway main agent workspace."""
|
||||||
|
gateway = request.gateway
|
||||||
if not gateway.url:
|
if not gateway.url:
|
||||||
return
|
return
|
||||||
if not gateway.main_session_key:
|
if not gateway.main_session_key:
|
||||||
@@ -763,18 +852,15 @@ async def provision_main_agent( # noqa: C901, PLR0912, PLR0913
|
|||||||
msg = "Unable to resolve gateway main agent id"
|
msg = "Unable to resolve gateway main agent id"
|
||||||
raise OpenClawGatewayError(msg)
|
raise OpenClawGatewayError(msg)
|
||||||
|
|
||||||
context = _build_main_context(agent, gateway, auth_token, user)
|
context = _build_main_context(agent, gateway, request.auth_token, request.user)
|
||||||
supported = set(await _supported_gateway_files(client_config))
|
supported = set(await _supported_gateway_files(client_config))
|
||||||
supported.update({"USER.md", "SELF.md", "AUTONOMY.md"})
|
supported.update({"USER.md", "SELF.md", "AUTONOMY.md"})
|
||||||
existing_files = await _gateway_agent_files_index(agent_id, client_config)
|
existing_files = await _gateway_agent_files_index(agent_id, client_config)
|
||||||
include_bootstrap = action != "update" or force_bootstrap
|
include_bootstrap = _should_include_bootstrap(
|
||||||
if action == "update" and not force_bootstrap:
|
action=request.options.action,
|
||||||
if not existing_files:
|
force_bootstrap=request.options.force_bootstrap,
|
||||||
include_bootstrap = False
|
existing_files=existing_files,
|
||||||
else:
|
)
|
||||||
entry = existing_files.get("BOOTSTRAP.md")
|
|
||||||
if entry and entry.get("missing") is True:
|
|
||||||
include_bootstrap = False
|
|
||||||
|
|
||||||
rendered = _render_agent_files(
|
rendered = _render_agent_files(
|
||||||
context,
|
context,
|
||||||
@@ -783,24 +869,13 @@ async def provision_main_agent( # noqa: C901, PLR0912, PLR0913
|
|||||||
include_bootstrap=include_bootstrap,
|
include_bootstrap=include_bootstrap,
|
||||||
template_overrides=MAIN_TEMPLATE_MAP,
|
template_overrides=MAIN_TEMPLATE_MAP,
|
||||||
)
|
)
|
||||||
for name, content in rendered.items():
|
await _set_agent_files(
|
||||||
if content == "":
|
agent_id=agent_id,
|
||||||
continue
|
rendered=rendered,
|
||||||
if name in PRESERVE_AGENT_EDITABLE_FILES:
|
existing_files=existing_files,
|
||||||
entry = existing_files.get(name)
|
client_config=client_config,
|
||||||
if entry and entry.get("missing") is not True:
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
await openclaw_call(
|
|
||||||
"agents.files.set",
|
|
||||||
{"agentId": agent_id, "name": name, "content": content},
|
|
||||||
config=client_config,
|
|
||||||
)
|
)
|
||||||
except OpenClawGatewayError as exc:
|
if request.options.reset_session:
|
||||||
if "unsupported file" in str(exc).lower():
|
|
||||||
continue
|
|
||||||
raise
|
|
||||||
if reset_session:
|
|
||||||
await _reset_session(gateway.main_session_key, client_config)
|
await _reset_session(gateway.main_session_key, client_config)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
from sqlmodel import col, select
|
from sqlmodel import col, select
|
||||||
@@ -15,7 +16,12 @@ from app.integrations.openclaw_gateway import (
|
|||||||
send_message,
|
send_message,
|
||||||
)
|
)
|
||||||
from app.models.agents import Agent
|
from app.models.agents import Agent
|
||||||
from app.services.agent_provisioning import DEFAULT_HEARTBEAT_CONFIG, provision_agent
|
from app.services.agent_provisioning import (
|
||||||
|
DEFAULT_HEARTBEAT_CONFIG,
|
||||||
|
AgentProvisionRequest,
|
||||||
|
ProvisionOptions,
|
||||||
|
provision_agent,
|
||||||
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||||
@@ -35,18 +41,34 @@ def lead_agent_name(_: Board) -> str:
|
|||||||
return "Lead Agent"
|
return "Lead Agent"
|
||||||
|
|
||||||
|
|
||||||
async def ensure_board_lead_agent( # noqa: PLR0913
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class LeadAgentOptions:
|
||||||
|
"""Optional overrides for board-lead provisioning behavior."""
|
||||||
|
|
||||||
|
agent_name: str | None = None
|
||||||
|
identity_profile: dict[str, str] | None = None
|
||||||
|
action: str = "provision"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class LeadAgentRequest:
|
||||||
|
"""Inputs required to ensure or provision a board lead agent."""
|
||||||
|
|
||||||
|
board: Board
|
||||||
|
gateway: Gateway
|
||||||
|
config: GatewayClientConfig
|
||||||
|
user: User | None
|
||||||
|
options: LeadAgentOptions = field(default_factory=LeadAgentOptions)
|
||||||
|
|
||||||
|
|
||||||
|
async def ensure_board_lead_agent(
|
||||||
session: AsyncSession,
|
session: AsyncSession,
|
||||||
*,
|
*,
|
||||||
board: Board,
|
request: LeadAgentRequest,
|
||||||
gateway: Gateway,
|
|
||||||
config: GatewayClientConfig,
|
|
||||||
user: User | None,
|
|
||||||
agent_name: str | None = None,
|
|
||||||
identity_profile: dict[str, str] | None = None,
|
|
||||||
action: str = "provision",
|
|
||||||
) -> tuple[Agent, bool]:
|
) -> tuple[Agent, bool]:
|
||||||
"""Ensure a board has a lead agent; return `(agent, created)`."""
|
"""Ensure a board has a lead agent; return `(agent, created)`."""
|
||||||
|
board = request.board
|
||||||
|
config_options = request.options
|
||||||
existing = (
|
existing = (
|
||||||
await session.exec(
|
await session.exec(
|
||||||
select(Agent)
|
select(Agent)
|
||||||
@@ -55,7 +77,7 @@ async def ensure_board_lead_agent( # noqa: PLR0913
|
|||||||
)
|
)
|
||||||
).first()
|
).first()
|
||||||
if existing:
|
if existing:
|
||||||
desired_name = agent_name or lead_agent_name(board)
|
desired_name = config_options.agent_name or lead_agent_name(board)
|
||||||
changed = False
|
changed = False
|
||||||
if existing.name != desired_name:
|
if existing.name != desired_name:
|
||||||
existing.name = desired_name
|
existing.name = desired_name
|
||||||
@@ -76,17 +98,17 @@ async def ensure_board_lead_agent( # noqa: PLR0913
|
|||||||
"communication_style": "direct, concise, practical",
|
"communication_style": "direct, concise, practical",
|
||||||
"emoji": ":gear:",
|
"emoji": ":gear:",
|
||||||
}
|
}
|
||||||
if identity_profile:
|
if config_options.identity_profile:
|
||||||
merged_identity_profile.update(
|
merged_identity_profile.update(
|
||||||
{
|
{
|
||||||
key: value.strip()
|
key: value.strip()
|
||||||
for key, value in identity_profile.items()
|
for key, value in config_options.identity_profile.items()
|
||||||
if value.strip()
|
if value.strip()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
agent = Agent(
|
agent = Agent(
|
||||||
name=agent_name or lead_agent_name(board),
|
name=config_options.agent_name or lead_agent_name(board),
|
||||||
status="provisioning",
|
status="provisioning",
|
||||||
board_id=board.id,
|
board_id=board.id,
|
||||||
is_board_lead=True,
|
is_board_lead=True,
|
||||||
@@ -94,7 +116,7 @@ async def ensure_board_lead_agent( # noqa: PLR0913
|
|||||||
identity_profile=merged_identity_profile,
|
identity_profile=merged_identity_profile,
|
||||||
openclaw_session_id=lead_session_key(board),
|
openclaw_session_id=lead_session_key(board),
|
||||||
provision_requested_at=utcnow(),
|
provision_requested_at=utcnow(),
|
||||||
provision_action=action,
|
provision_action=config_options.action,
|
||||||
)
|
)
|
||||||
raw_token = generate_agent_token()
|
raw_token = generate_agent_token()
|
||||||
agent.agent_token_hash = hash_agent_token(raw_token)
|
agent.agent_token_hash = hash_agent_token(raw_token)
|
||||||
@@ -103,11 +125,20 @@ async def ensure_board_lead_agent( # noqa: PLR0913
|
|||||||
await session.refresh(agent)
|
await session.refresh(agent)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await provision_agent(agent, board, gateway, raw_token, user, action=action)
|
await provision_agent(
|
||||||
|
agent,
|
||||||
|
AgentProvisionRequest(
|
||||||
|
board=board,
|
||||||
|
gateway=request.gateway,
|
||||||
|
auth_token=raw_token,
|
||||||
|
user=request.user,
|
||||||
|
options=ProvisionOptions(action=config_options.action),
|
||||||
|
),
|
||||||
|
)
|
||||||
if agent.openclaw_session_id:
|
if agent.openclaw_session_id:
|
||||||
await ensure_session(
|
await ensure_session(
|
||||||
agent.openclaw_session_id,
|
agent.openclaw_session_id,
|
||||||
config=config,
|
config=request.config,
|
||||||
label=agent.name,
|
label=agent.name,
|
||||||
)
|
)
|
||||||
await send_message(
|
await send_message(
|
||||||
@@ -118,7 +149,7 @@ async def ensure_board_lead_agent( # noqa: PLR0913
|
|||||||
"then delete it. Begin heartbeats after startup."
|
"then delete it. Begin heartbeats after startup."
|
||||||
),
|
),
|
||||||
session_key=agent.openclaw_session_id,
|
session_key=agent.openclaw_session_id,
|
||||||
config=config,
|
config=request.config,
|
||||||
deliver=True,
|
deliver=True,
|
||||||
)
|
)
|
||||||
except OpenClawGatewayError:
|
except OpenClawGatewayError:
|
||||||
|
|||||||
@@ -2,9 +2,10 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
import time
|
import time
|
||||||
import xml.etree.ElementTree as ET
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from html import unescape
|
||||||
from typing import Final
|
from typing import Final
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
@@ -14,6 +15,10 @@ SOULS_DIRECTORY_SITEMAP_URL: Final[str] = f"{SOULS_DIRECTORY_BASE_URL}/sitemap.x
|
|||||||
|
|
||||||
_SITEMAP_TTL_SECONDS: Final[int] = 60 * 60
|
_SITEMAP_TTL_SECONDS: Final[int] = 60 * 60
|
||||||
_SOUL_URL_MIN_PARTS: Final[int] = 6
|
_SOUL_URL_MIN_PARTS: Final[int] = 6
|
||||||
|
_LOC_PATTERN: Final[re.Pattern[str]] = re.compile(
|
||||||
|
r"<(?:[A-Za-z0-9_]+:)?loc>(.*?)</(?:[A-Za-z0-9_]+:)?loc>",
|
||||||
|
flags=re.IGNORECASE | re.DOTALL,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
@dataclass(frozen=True, slots=True)
|
||||||
@@ -36,17 +41,10 @@ class SoulRef:
|
|||||||
|
|
||||||
def _parse_sitemap_soul_refs(sitemap_xml: str) -> list[SoulRef]:
|
def _parse_sitemap_soul_refs(sitemap_xml: str) -> list[SoulRef]:
|
||||||
"""Parse sitemap XML and extract valid souls.directory handle/slug refs."""
|
"""Parse sitemap XML and extract valid souls.directory handle/slug refs."""
|
||||||
try:
|
# Extract <loc> values without XML entity expansion.
|
||||||
# Souls sitemap is fetched from a known trusted host in this service flow.
|
|
||||||
root = ET.fromstring(sitemap_xml) # noqa: S314
|
|
||||||
except ET.ParseError:
|
|
||||||
return []
|
|
||||||
|
|
||||||
# Handle both namespaced and non-namespaced sitemap XML.
|
|
||||||
urls = [
|
urls = [
|
||||||
loc.text.strip()
|
unescape(match.group(1)).strip()
|
||||||
for loc in root.iter()
|
for match in _LOC_PATTERN.finditer(sitemap_xml)
|
||||||
if loc.tag.endswith("loc") and loc.text
|
|
||||||
]
|
]
|
||||||
|
|
||||||
refs: list[SoulRef] = []
|
refs: list[SoulRef] = []
|
||||||
|
|||||||
@@ -28,7 +28,13 @@ from app.models.boards import Board
|
|||||||
from app.models.gateways import Gateway
|
from app.models.gateways import Gateway
|
||||||
from app.models.users import User
|
from app.models.users import User
|
||||||
from app.schemas.gateways import GatewayTemplatesSyncError, GatewayTemplatesSyncResult
|
from app.schemas.gateways import GatewayTemplatesSyncError, GatewayTemplatesSyncResult
|
||||||
from app.services.agent_provisioning import provision_agent, provision_main_agent
|
from app.services.agent_provisioning import (
|
||||||
|
AgentProvisionRequest,
|
||||||
|
MainAgentProvisionRequest,
|
||||||
|
ProvisionOptions,
|
||||||
|
provision_agent,
|
||||||
|
provision_main_agent,
|
||||||
|
)
|
||||||
|
|
||||||
_TOOLS_KV_RE = re.compile(r"^(?P<key>[A-Z0-9_]+)=(?P<value>.*)$")
|
_TOOLS_KV_RE = re.compile(r"^(?P<key>[A-Z0-9_]+)=(?P<value>.*)$")
|
||||||
SESSION_KEY_PARTS_MIN = 2
|
SESSION_KEY_PARTS_MIN = 2
|
||||||
@@ -480,13 +486,17 @@ async def _sync_one_agent(
|
|||||||
async def _do_provision() -> None:
|
async def _do_provision() -> None:
|
||||||
await provision_agent(
|
await provision_agent(
|
||||||
agent,
|
agent,
|
||||||
board,
|
AgentProvisionRequest(
|
||||||
ctx.gateway,
|
board=board,
|
||||||
auth_token,
|
gateway=ctx.gateway,
|
||||||
ctx.options.user,
|
auth_token=auth_token,
|
||||||
|
user=ctx.options.user,
|
||||||
|
options=ProvisionOptions(
|
||||||
action="update",
|
action="update",
|
||||||
force_bootstrap=ctx.options.force_bootstrap,
|
force_bootstrap=ctx.options.force_bootstrap,
|
||||||
reset_session=ctx.options.reset_sessions,
|
reset_session=ctx.options.reset_sessions,
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
await _with_gateway_retry(_do_provision, backoff=ctx.backoff)
|
await _with_gateway_retry(_do_provision, backoff=ctx.backoff)
|
||||||
@@ -564,12 +574,16 @@ async def _sync_main_agent(
|
|||||||
async def _do_provision_main() -> None:
|
async def _do_provision_main() -> None:
|
||||||
await provision_main_agent(
|
await provision_main_agent(
|
||||||
main_agent,
|
main_agent,
|
||||||
ctx.gateway,
|
MainAgentProvisionRequest(
|
||||||
token,
|
gateway=ctx.gateway,
|
||||||
ctx.options.user,
|
auth_token=token,
|
||||||
|
user=ctx.options.user,
|
||||||
|
options=ProvisionOptions(
|
||||||
action="update",
|
action="update",
|
||||||
force_bootstrap=ctx.options.force_bootstrap,
|
force_bootstrap=ctx.options.force_bootstrap,
|
||||||
reset_session=ctx.options.reset_sessions,
|
reset_session=ctx.options.reset_sessions,
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
await _with_gateway_retry(_do_provision_main, backoff=ctx.backoff)
|
await _with_gateway_retry(_do_provision_main, backoff=ctx.backoff)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib
|
||||||
import sys
|
import sys
|
||||||
from logging.config import fileConfig
|
from logging.config import fileConfig
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -14,8 +15,8 @@ PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
|||||||
if str(PROJECT_ROOT) not in sys.path:
|
if str(PROJECT_ROOT) not in sys.path:
|
||||||
sys.path.append(str(PROJECT_ROOT))
|
sys.path.append(str(PROJECT_ROOT))
|
||||||
|
|
||||||
from app import models # noqa: E402,F401
|
importlib.import_module("app.models")
|
||||||
from app.core.config import settings # noqa: E402
|
settings = importlib.import_module("app.core.config").settings
|
||||||
|
|
||||||
config = context.config
|
config = context.config
|
||||||
configure_logger = config.attributes.get("configure_logger", True)
|
configure_logger = config.attributes.get("configure_logger", True)
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ Create Date: 2026-02-09 00:41:55.760624
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
# ruff: noqa: INP001
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
import sqlmodel
|
import sqlmodel
|
||||||
from alembic import op
|
from alembic import op
|
||||||
@@ -20,8 +19,17 @@ branch_labels = None
|
|||||||
depends_on = None
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None: # noqa: PLR0915
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
"""Create initial schema objects."""
|
"""Create initial schema objects."""
|
||||||
|
_upgrade_part_1()
|
||||||
|
_upgrade_part_2()
|
||||||
|
_upgrade_part_3()
|
||||||
|
_upgrade_part_4()
|
||||||
|
|
||||||
|
|
||||||
|
def _upgrade_part_1() -> None:
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
op.create_table(
|
op.create_table(
|
||||||
"organizations",
|
"organizations",
|
||||||
@@ -183,6 +191,9 @@ def upgrade() -> None: # noqa: PLR0915
|
|||||||
op.f("ix_boards_organization_id"), "boards", ["organization_id"], unique=False,
|
op.f("ix_boards_organization_id"), "boards", ["organization_id"], unique=False,
|
||||||
)
|
)
|
||||||
op.create_index(op.f("ix_boards_slug"), "boards", ["slug"], unique=False)
|
op.create_index(op.f("ix_boards_slug"), "boards", ["slug"], unique=False)
|
||||||
|
|
||||||
|
|
||||||
|
def _upgrade_part_2() -> None:
|
||||||
op.create_table(
|
op.create_table(
|
||||||
"organization_invites",
|
"organization_invites",
|
||||||
sa.Column("id", sa.Uuid(), nullable=False),
|
sa.Column("id", sa.Uuid(), nullable=False),
|
||||||
@@ -366,6 +377,9 @@ def upgrade() -> None: # noqa: PLR0915
|
|||||||
unique=False,
|
unique=False,
|
||||||
)
|
)
|
||||||
op.create_index(op.f("ix_agents_status"), "agents", ["status"], unique=False)
|
op.create_index(op.f("ix_agents_status"), "agents", ["status"], unique=False)
|
||||||
|
|
||||||
|
|
||||||
|
def _upgrade_part_3() -> None:
|
||||||
op.create_table(
|
op.create_table(
|
||||||
"board_memory",
|
"board_memory",
|
||||||
sa.Column("id", sa.Uuid(), nullable=False),
|
sa.Column("id", sa.Uuid(), nullable=False),
|
||||||
@@ -532,6 +546,9 @@ def upgrade() -> None: # noqa: PLR0915
|
|||||||
)
|
)
|
||||||
op.create_index(op.f("ix_tasks_priority"), "tasks", ["priority"], unique=False)
|
op.create_index(op.f("ix_tasks_priority"), "tasks", ["priority"], unique=False)
|
||||||
op.create_index(op.f("ix_tasks_status"), "tasks", ["status"], unique=False)
|
op.create_index(op.f("ix_tasks_status"), "tasks", ["status"], unique=False)
|
||||||
|
|
||||||
|
|
||||||
|
def _upgrade_part_4() -> None:
|
||||||
op.create_table(
|
op.create_table(
|
||||||
"activity_events",
|
"activity_events",
|
||||||
sa.Column("id", sa.Uuid(), nullable=False),
|
sa.Column("id", sa.Uuid(), nullable=False),
|
||||||
@@ -686,8 +703,14 @@ def upgrade() -> None: # noqa: PLR0915
|
|||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None: # noqa: PLR0915
|
def downgrade() -> None:
|
||||||
"""Drop initial schema objects."""
|
"""Drop initial schema objects."""
|
||||||
|
_downgrade_part_1()
|
||||||
|
_downgrade_part_2()
|
||||||
|
_downgrade_part_3()
|
||||||
|
|
||||||
|
|
||||||
|
def _downgrade_part_1() -> None:
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
op.drop_index(
|
op.drop_index(
|
||||||
op.f("ix_task_fingerprints_fingerprint_hash"), table_name="task_fingerprints",
|
op.f("ix_task_fingerprints_fingerprint_hash"), table_name="task_fingerprints",
|
||||||
@@ -745,6 +768,9 @@ def downgrade() -> None: # noqa: PLR0915
|
|||||||
op.drop_index(op.f("ix_board_memory_is_chat"), table_name="board_memory")
|
op.drop_index(op.f("ix_board_memory_is_chat"), table_name="board_memory")
|
||||||
op.drop_index(op.f("ix_board_memory_board_id"), table_name="board_memory")
|
op.drop_index(op.f("ix_board_memory_board_id"), table_name="board_memory")
|
||||||
op.drop_table("board_memory")
|
op.drop_table("board_memory")
|
||||||
|
|
||||||
|
|
||||||
|
def _downgrade_part_2() -> None:
|
||||||
op.drop_index(op.f("ix_agents_status"), table_name="agents")
|
op.drop_index(op.f("ix_agents_status"), table_name="agents")
|
||||||
op.drop_index(op.f("ix_agents_provision_confirm_token_hash"), table_name="agents")
|
op.drop_index(op.f("ix_agents_provision_confirm_token_hash"), table_name="agents")
|
||||||
op.drop_index(op.f("ix_agents_provision_action"), table_name="agents")
|
op.drop_index(op.f("ix_agents_provision_action"), table_name="agents")
|
||||||
@@ -795,6 +821,9 @@ def downgrade() -> None: # noqa: PLR0915
|
|||||||
op.drop_index(op.f("ix_boards_board_type"), table_name="boards")
|
op.drop_index(op.f("ix_boards_board_type"), table_name="boards")
|
||||||
op.drop_index(op.f("ix_boards_board_group_id"), table_name="boards")
|
op.drop_index(op.f("ix_boards_board_group_id"), table_name="boards")
|
||||||
op.drop_table("boards")
|
op.drop_table("boards")
|
||||||
|
|
||||||
|
|
||||||
|
def _downgrade_part_3() -> None:
|
||||||
op.drop_index(
|
op.drop_index(
|
||||||
op.f("ix_board_group_memory_is_chat"), table_name="board_group_memory",
|
op.f("ix_board_group_memory_is_chat"), table_name="board_group_memory",
|
||||||
)
|
)
|
||||||
|
|||||||
1
backend/migrations/versions/__init__.py
Normal file
1
backend/migrations/versions/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Alembic migration version modules."""
|
||||||
@@ -9,11 +9,11 @@ from pathlib import Path
|
|||||||
BACKEND_ROOT = Path(__file__).resolve().parents[1]
|
BACKEND_ROOT = Path(__file__).resolve().parents[1]
|
||||||
sys.path.insert(0, str(BACKEND_ROOT))
|
sys.path.insert(0, str(BACKEND_ROOT))
|
||||||
|
|
||||||
from app.main import app # noqa: E402
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
"""Generate `openapi.json` from the FastAPI app definition."""
|
"""Generate `openapi.json` from the FastAPI app definition."""
|
||||||
|
from app.main import app
|
||||||
|
|
||||||
# Importing the FastAPI app does not run lifespan hooks,
|
# Importing the FastAPI app does not run lifespan hooks,
|
||||||
# so this does not require a DB.
|
# so this does not require a DB.
|
||||||
out_path = BACKEND_ROOT / "openapi.json"
|
out_path = BACKEND_ROOT / "openapi.json"
|
||||||
|
|||||||
@@ -10,15 +10,15 @@ from uuid import uuid4
|
|||||||
BACKEND_ROOT = Path(__file__).resolve().parents[1]
|
BACKEND_ROOT = Path(__file__).resolve().parents[1]
|
||||||
sys.path.insert(0, str(BACKEND_ROOT))
|
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:
|
async def run() -> None:
|
||||||
"""Populate the local database with a demo gateway, board, user, and agent."""
|
"""Populate the local database with a demo gateway, board, user, and agent."""
|
||||||
|
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
|
||||||
|
|
||||||
await init_db()
|
await init_db()
|
||||||
async with async_session_maker() as session:
|
async with async_session_maker() as session:
|
||||||
demo_workspace_root = BACKEND_ROOT / ".tmp" / "openclaw-demo"
|
demo_workspace_root = BACKEND_ROOT / ".tmp" / "openclaw-demo"
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
# ruff: noqa: INP001
|
|
||||||
"""CLI script to sync template files into gateway agent workspaces."""
|
"""CLI script to sync template files into gateway agent workspaces."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -12,10 +11,6 @@ from uuid import UUID
|
|||||||
BACKEND_ROOT = Path(__file__).resolve().parents[1]
|
BACKEND_ROOT = Path(__file__).resolve().parents[1]
|
||||||
sys.path.insert(0, str(BACKEND_ROOT))
|
sys.path.insert(0, str(BACKEND_ROOT))
|
||||||
|
|
||||||
from app.db.session import async_session_maker # noqa: E402
|
|
||||||
from app.models.gateways import Gateway # noqa: E402
|
|
||||||
from app.services.template_sync import sync_gateway_templates # noqa: E402
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_args() -> argparse.Namespace:
|
def _parse_args() -> argparse.Namespace:
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
@@ -59,6 +54,13 @@ def _parse_args() -> argparse.Namespace:
|
|||||||
|
|
||||||
|
|
||||||
async def _run() -> int:
|
async def _run() -> int:
|
||||||
|
from app.db.session import async_session_maker
|
||||||
|
from app.models.gateways import Gateway
|
||||||
|
from app.services.template_sync import (
|
||||||
|
GatewayTemplateSyncOptions,
|
||||||
|
sync_gateway_templates,
|
||||||
|
)
|
||||||
|
|
||||||
args = _parse_args()
|
args = _parse_args()
|
||||||
gateway_id = UUID(args.gateway_id)
|
gateway_id = UUID(args.gateway_id)
|
||||||
board_id = UUID(args.board_id) if args.board_id else None
|
board_id = UUID(args.board_id) if args.board_id else None
|
||||||
@@ -72,12 +74,14 @@ async def _run() -> int:
|
|||||||
result = await sync_gateway_templates(
|
result = await sync_gateway_templates(
|
||||||
session,
|
session,
|
||||||
gateway,
|
gateway,
|
||||||
|
options=GatewayTemplateSyncOptions(
|
||||||
user=None,
|
user=None,
|
||||||
include_main=bool(args.include_main),
|
include_main=bool(args.include_main),
|
||||||
reset_sessions=bool(args.reset_sessions),
|
reset_sessions=bool(args.reset_sessions),
|
||||||
rotate_tokens=bool(args.rotate_tokens),
|
rotate_tokens=bool(args.rotate_tokens),
|
||||||
force_bootstrap=bool(args.force_bootstrap),
|
force_bootstrap=bool(args.force_bootstrap),
|
||||||
board_id=board_id,
|
board_id=board_id,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
sys.stdout.write(f"gateway_id={result.gateway_id}\n")
|
sys.stdout.write(f"gateway_id={result.gateway_id}\n")
|
||||||
|
|||||||
Reference in New Issue
Block a user