diff --git a/backend/app/api/agent.py b/backend/app/api/agent.py index 78c2ecac..57bc6603 100644 --- a/backend/app/api/agent.py +++ b/backend/app/api/agent.py @@ -1,7 +1,7 @@ from __future__ import annotations -from uuid import UUID import asyncio +from uuid import UUID from fastapi import APIRouter, Depends, HTTPException, Query, status from sqlmodel import Session, select @@ -14,28 +14,18 @@ from app.api import tasks as tasks_api from app.api.deps import ActorContext, get_board_or_404, get_task_or_404 from app.core.agent_auth import AgentAuthContext, get_agent_auth_context from app.db.session import get_session -from app.integrations.openclaw_gateway import ( - GatewayConfig as GatewayClientConfig, - OpenClawGatewayError, - ensure_session, - send_message, -) +from app.integrations.openclaw_gateway import GatewayConfig as GatewayClientConfig +from app.integrations.openclaw_gateway import OpenClawGatewayError, ensure_session, send_message from app.models.agents import Agent -from app.models.tasks import Task from app.models.boards import Board from app.models.gateways import Gateway +from app.models.tasks import Task +from app.schemas.agents import AgentCreate, AgentHeartbeatCreate, AgentNudge, AgentRead from app.schemas.approvals import ApprovalCreate, ApprovalRead from app.schemas.board_memory import BoardMemoryCreate, BoardMemoryRead from app.schemas.board_onboarding import BoardOnboardingRead from app.schemas.boards import BoardRead -from app.schemas.tasks import ( - TaskCommentCreate, - TaskCommentRead, - TaskCreate, - TaskRead, - TaskUpdate, -) -from app.schemas.agents import AgentCreate, AgentHeartbeatCreate, AgentNudge, AgentRead +from app.schemas.tasks import TaskCommentCreate, TaskCommentRead, TaskCreate, TaskRead, TaskUpdate from app.services.activity_log import record_activity router = APIRouter(prefix="/agent", tags=["agent"]) @@ -98,9 +88,7 @@ def list_agents( agents = list(session.exec(statement)) main_session_keys = agents_api._get_gateway_main_session_keys(session) return [ - agents_api._to_agent_read( - agents_api._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 ] @@ -351,6 +339,7 @@ def nudge_agent( detail="message is required", ) config = _gateway_config(session, board) + async def _send() -> None: await ensure_session(target.openclaw_session_id, config=config, label=target.name) await send_message( diff --git a/backend/app/api/agents.py b/backend/app/api/agents.py index 2857fc00..2d1862cd 100644 --- a/backend/app/api/agents.py +++ b/backend/app/api/agents.py @@ -1,9 +1,9 @@ from __future__ import annotations -import re -from datetime import datetime, timedelta, timezone import asyncio import json +import re +from datetime import datetime, timedelta, timezone from uuid import UUID, uuid4 from fastapi import APIRouter, Depends, HTTPException, Query, Request, status @@ -23,7 +23,13 @@ from app.models.agents import Agent from app.models.boards import Board from app.models.gateways import Gateway from app.models.tasks import Task -from app.schemas.agents import AgentCreate, AgentHeartbeat, AgentHeartbeatCreate, AgentRead, AgentUpdate +from app.schemas.agents import ( + AgentCreate, + AgentHeartbeat, + AgentHeartbeatCreate, + AgentRead, + AgentUpdate, +) from app.services.activity_log import record_activity from app.services.agent_provisioning import ( DEFAULT_HEARTBEAT_CONFIG, @@ -159,14 +165,10 @@ def _to_agent_read(agent: Agent, main_session_keys: set[str]) -> AgentRead: return model.model_copy(update={"is_gateway_main": _is_gateway_main(agent, main_session_keys)}) -def _find_gateway_for_main_session( - session: Session, session_key: str | None -) -> Gateway | None: +def _find_gateway_for_main_session(session: Session, session_key: str | None) -> Gateway | None: if not session_key: return None - return session.exec( - select(Gateway).where(Gateway.main_session_key == session_key) - ).first() + return session.exec(select(Gateway).where(Gateway.main_session_key == session_key)).first() async def _ensure_gateway_session( @@ -193,9 +195,7 @@ def _with_computed_status(agent: Agent) -> Agent: def _serialize_agent(agent: Agent, main_session_keys: set[str]) -> dict[str, object]: - return _to_agent_read( - _with_computed_status(agent), main_session_keys - ).model_dump(mode="json") + return _to_agent_read(_with_computed_status(agent), main_session_keys).model_dump(mode="json") def _fetch_agent_events( @@ -206,15 +206,12 @@ def _fetch_agent_events( statement = select(Agent) if board_id: statement = statement.where(col(Agent.board_id) == board_id) - statement = ( - statement.where( - or_( - col(Agent.updated_at) >= since, - col(Agent.last_seen_at) >= since, - ) + statement = statement.where( + or_( + col(Agent.updated_at) >= since, + col(Agent.last_seen_at) >= since, ) - .order_by(asc(col(Agent.updated_at))) - ) + ).order_by(asc(col(Agent.updated_at))) return list(session.exec(statement)) @@ -257,10 +254,7 @@ def list_agents( ) -> list[Agent]: agents = list(session.exec(select(Agent))) main_session_keys = _get_gateway_main_session_keys(session) - return [ - _to_agent_read(_with_computed_status(agent), main_session_keys) - for agent in agents - ] + return [_to_agent_read(_with_computed_status(agent), main_session_keys) for agent in agents] @router.get("/stream") @@ -336,9 +330,7 @@ async def create_agent( data["identity_template"] = None if data.get("soul_template") == "": data["soul_template"] = None - data["identity_profile"] = _normalize_identity_profile( - data.get("identity_profile") - ) + data["identity_profile"] = _normalize_identity_profile(data.get("identity_profile")) agent = Agent.model_validate(data) agent.status = "provisioning" raw_token = generate_agent_token() @@ -441,9 +433,7 @@ async def update_agent( if updates.get("soul_template") == "": updates["soul_template"] = None if "identity_profile" in updates: - updates["identity_profile"] = _normalize_identity_profile( - updates.get("identity_profile") - ) + updates["identity_profile"] = _normalize_identity_profile(updates.get("identity_profile")) if not updates and not force and make_main is None: main_session_keys = _get_gateway_main_session_keys(session) return _to_agent_read(_with_computed_status(agent), main_session_keys) @@ -823,9 +813,7 @@ def delete_agent( ) ) session.execute( - update(ActivityEvent) - .where(col(ActivityEvent.agent_id) == agent.id) - .values(agent_id=None) + update(ActivityEvent).where(col(ActivityEvent.agent_id) == agent.id).values(agent_id=None) ) session.delete(agent) session.commit() diff --git a/backend/app/api/approvals.py b/backend/app/api/approvals.py index 0331151a..76abdacd 100644 --- a/backend/app/api/approvals.py +++ b/backend/app/api/approvals.py @@ -1,8 +1,8 @@ from __future__ import annotations -from datetime import datetime, timezone import asyncio import json +from datetime import datetime, timezone from uuid import UUID from fastapi import APIRouter, Depends, HTTPException, Query, Request, status @@ -42,9 +42,7 @@ def _approval_updated_at(approval: Approval) -> datetime: def _serialize_approval(approval: Approval) -> dict[str, object]: - return ApprovalRead.model_validate( - approval, from_attributes=True - ).model_dump(mode="json") + return ApprovalRead.model_validate(approval, from_attributes=True).model_dump(mode="json") def _fetch_approval_events( @@ -103,9 +101,7 @@ async def stream_approvals( while True: if await request.is_disconnected(): break - approvals = await run_in_threadpool( - _fetch_approval_events, board.id, last_seen - ) + approvals = await run_in_threadpool(_fetch_approval_events, board.id, last_seen) for approval in approvals: updated_at = _approval_updated_at(approval) if updated_at > last_seen: diff --git a/backend/app/api/board_memory.py b/backend/app/api/board_memory.py index 176ec154..6fdb50d2 100644 --- a/backend/app/api/board_memory.py +++ b/backend/app/api/board_memory.py @@ -1,9 +1,9 @@ from __future__ import annotations -from datetime import datetime, timezone import asyncio import json import re +from datetime import datetime, timezone from fastapi import APIRouter, Depends, HTTPException, Query, Request from sqlmodel import Session, col, select @@ -13,12 +13,8 @@ from starlette.concurrency import run_in_threadpool from app.api.deps import ActorContext, get_board_or_404, require_admin_or_agent from app.core.config import settings from app.db.session import engine, get_session -from app.integrations.openclaw_gateway import ( - GatewayConfig as GatewayClientConfig, - OpenClawGatewayError, - ensure_session, - send_message, -) +from app.integrations.openclaw_gateway import GatewayConfig as GatewayClientConfig +from app.integrations.openclaw_gateway import OpenClawGatewayError, ensure_session, send_message from app.models.agents import Agent from app.models.board_memory import BoardMemory from app.models.gateways import Gateway @@ -46,9 +42,7 @@ def _parse_since(value: str | None) -> datetime | None: def _serialize_memory(memory: BoardMemory) -> dict[str, object]: - return BoardMemoryRead.model_validate( - memory, from_attributes=True - ).model_dump(mode="json") + return BoardMemoryRead.model_validate(memory, from_attributes=True).model_dump(mode="json") def _extract_mentions(message: str) -> set[str]: @@ -162,6 +156,7 @@ def _notify_chat_targets( except OpenClawGatewayError: continue + @router.get("", response_model=list[BoardMemoryRead]) def list_board_memory( limit: int = Query(default=50, ge=1, le=200), @@ -201,9 +196,7 @@ async def stream_board_memory( while True: if await request.is_disconnected(): break - memories = await run_in_threadpool( - _fetch_memory_events, board.id, last_seen - ) + memories = await run_in_threadpool(_fetch_memory_events, board.id, last_seen) for memory in memories: if memory.created_at > last_seen: last_seen = memory.created_at diff --git a/backend/app/api/board_onboarding.py b/backend/app/api/board_onboarding.py index 8ec593bb..023702c8 100644 --- a/backend/app/api/board_onboarding.py +++ b/backend/app/api/board_onboarding.py @@ -1,12 +1,12 @@ from __future__ import annotations import json +import logging import re from datetime import datetime from uuid import uuid4 from fastapi import APIRouter, Depends, HTTPException, status -import logging from sqlmodel import Session, select from app.api.deps import ActorContext, get_board_or_404, require_admin_auth, require_admin_or_agent @@ -33,7 +33,6 @@ router = APIRouter(prefix="/boards/{board_id}/onboarding", tags=["board-onboardi logger = logging.getLogger(__name__) - def _gateway_config(session: Session, board: Board) -> tuple[Gateway, GatewayClientConfig]: if not board.gateway_id: raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY) @@ -64,9 +63,7 @@ async def _ensure_lead_agent( auth: AuthContext, ) -> Agent: existing = session.exec( - select(Agent) - .where(Agent.board_id == board.id) - .where(Agent.is_board_lead.is_(True)) + select(Agent).where(Agent.board_id == board.id).where(Agent.is_board_lead.is_(True)) ).first() if existing: if existing.name != _lead_agent_name(board): @@ -161,21 +158,21 @@ async def start_onboarding( "Onboarding response endpoint:\n" f"POST {base_url}/api/v1/agent/boards/{board.id}/onboarding\n" "QUESTION example (send JSON body exactly as shown):\n" - f"curl -s -X POST \"{base_url}/api/v1/agent/boards/{board.id}/onboarding\" " - "-H \"X-Agent-Token: $AUTH_TOKEN\" " - "-H \"Content-Type: application/json\" " - "-d '{\"question\":\"...\",\"options\":[{\"id\":\"1\",\"label\":\"...\"},{\"id\":\"2\",\"label\":\"...\"}]}'\n" + f'curl -s -X POST "{base_url}/api/v1/agent/boards/{board.id}/onboarding" ' + '-H "X-Agent-Token: $AUTH_TOKEN" ' + '-H "Content-Type: application/json" ' + '-d \'{"question":"...","options":[{"id":"1","label":"..."},{"id":"2","label":"..."}]}\'\n' "COMPLETION example (send JSON body exactly as shown):\n" - f"curl -s -X POST \"{base_url}/api/v1/agent/boards/{board.id}/onboarding\" " - "-H \"X-Agent-Token: $AUTH_TOKEN\" " - "-H \"Content-Type: application/json\" " - "-d '{\"status\":\"complete\",\"board_type\":\"goal\",\"objective\":\"...\",\"success_metrics\":{...},\"target_date\":\"YYYY-MM-DD\"}'\n" + f'curl -s -X POST "{base_url}/api/v1/agent/boards/{board.id}/onboarding" ' + '-H "X-Agent-Token: $AUTH_TOKEN" ' + '-H "Content-Type: application/json" ' + '-d \'{"status":"complete","board_type":"goal","objective":"...","success_metrics":{...},"target_date":"YYYY-MM-DD"}\'\n' "QUESTION FORMAT (one question per response, no arrays, no markdown, no extra text):\n" - "{\"question\":\"...\",\"options\":[{\"id\":\"1\",\"label\":\"...\"},{\"id\":\"2\",\"label\":\"...\"}]}\n" + '{"question":"...","options":[{"id":"1","label":"..."},{"id":"2","label":"..."}]}\n' "Do NOT wrap questions in a list. Do NOT add commentary.\n" "When you have enough info, return JSON ONLY (via API):\n" - "{\"status\":\"complete\",\"board_type\":\"goal\"|\"general\",\"objective\":\"...\"," - "\"success_metrics\":{...},\"target_date\":\"YYYY-MM-DD\"}." + '{"status":"complete","board_type":"goal"|"general","objective":"...",' + '"success_metrics":{...},"target_date":"YYYY-MM-DD"}.' ) try: diff --git a/backend/app/api/boards.py b/backend/app/api/boards.py index 26cd1b7f..75a6663d 100644 --- a/backend/app/api/boards.py +++ b/backend/app/api/boards.py @@ -214,9 +214,7 @@ def delete_board( session.execute(delete(Approval).where(col(Approval.board_id) == board.id)) session.execute(delete(BoardMemory).where(col(BoardMemory.board_id) == board.id)) session.execute( - delete(BoardOnboardingSession).where( - col(BoardOnboardingSession.board_id) == board.id - ) + delete(BoardOnboardingSession).where(col(BoardOnboardingSession.board_id) == board.id) ) session.execute(delete(Task).where(col(Task.board_id) == board.id)) session.delete(board) diff --git a/backend/app/api/gateways.py b/backend/app/api/gateways.py index 8c6efc94..0ac8d8e1 100644 --- a/backend/app/api/gateways.py +++ b/backend/app/api/gateways.py @@ -1,12 +1,11 @@ from __future__ import annotations +from datetime import datetime from uuid import UUID from fastapi import APIRouter, Depends, HTTPException, status from sqlmodel import Session, select -from datetime import datetime - from app.core.agent_tokens import generate_agent_token, hash_agent_token from app.core.auth import AuthContext, get_auth_context from app.db.session import get_session @@ -306,7 +305,9 @@ async def _ensure_main_agent( try: await provision_main_agent(agent, gateway, raw_token, auth.user, action=action) await ensure_session( - gateway.main_session_key, config=GatewayClientConfig(url=gateway.url, token=gateway.token), label=agent.name + gateway.main_session_key, + config=GatewayClientConfig(url=gateway.url, token=gateway.token), + label=agent.name, ) await send_message( ( diff --git a/backend/app/api/tasks.py b/backend/app/api/tasks.py index 65ba17c5..4637fa5a 100644 --- a/backend/app/api/tasks.py +++ b/backend/app/api/tasks.py @@ -1,17 +1,17 @@ from __future__ import annotations -from datetime import datetime, timezone import asyncio import json import re from collections import deque +from datetime import datetime, timezone from uuid import UUID from fastapi import APIRouter, Depends, HTTPException, Query, Request, status +from sqlalchemy import asc, delete, desc +from sqlmodel import Session, col, select from sse_starlette.sse import EventSourceResponse from starlette.concurrency import run_in_threadpool -from sqlalchemy import asc, desc, delete -from sqlmodel import Session, col, select from app.api.deps import ( ActorContext, @@ -22,18 +22,14 @@ from app.api.deps import ( ) from app.core.auth import AuthContext from app.db.session import engine, get_session -from app.integrations.openclaw_gateway import ( - GatewayConfig as GatewayClientConfig, - OpenClawGatewayError, - ensure_session, - send_message, -) +from app.integrations.openclaw_gateway import GatewayConfig as GatewayClientConfig +from app.integrations.openclaw_gateway import OpenClawGatewayError, ensure_session, send_message from app.models.activity_events import ActivityEvent from app.models.agents import Agent from app.models.boards import Board from app.models.gateways import Gateway -from app.models.tasks import Task from app.models.task_fingerprints import TaskFingerprint +from app.models.tasks import Task from app.schemas.tasks import TaskCommentCreate, TaskCommentRead, TaskCreate, TaskRead, TaskUpdate from app.services.activity_log import record_activity @@ -151,9 +147,7 @@ def _fetch_task_events( since: datetime, ) -> list[tuple[ActivityEvent, Task | None]]: with Session(engine) as session: - task_ids = list( - session.exec(select(Task.id).where(col(Task.board_id) == board_id)) - ) + task_ids = list(session.exec(select(Task.id).where(col(Task.board_id) == board_id))) if not task_ids: return [] statement = ( @@ -270,9 +264,7 @@ def _notify_lead_on_task_create( task: Task, ) -> None: lead = session.exec( - select(Agent) - .where(Agent.board_id == board.id) - .where(Agent.is_board_lead.is_(True)) + select(Agent).where(Agent.board_id == board.id).where(Agent.is_board_lead.is_(True)) ).first() if lead is None or not lead.openclaw_session_id: return @@ -329,9 +321,7 @@ def _notify_lead_on_task_unassigned( task: Task, ) -> None: lead = session.exec( - select(Agent) - .where(Agent.board_id == board.id) - .where(Agent.is_board_lead.is_(True)) + select(Agent).where(Agent.board_id == board.id).where(Agent.is_board_lead.is_(True)) ).first() if lead is None or not lead.openclaw_session_id: return @@ -639,11 +629,7 @@ def update_task( task=task, ) if task.assigned_agent_id and task.assigned_agent_id != previous_assigned: - if ( - actor.actor_type == "agent" - and actor.agent - and task.assigned_agent_id == actor.agent.id - ): + if actor.actor_type == "agent" and actor.agent and task.assigned_agent_id == actor.agent.id: return task assigned_agent = session.get(Agent, task.assigned_agent_id) if assigned_agent: @@ -740,11 +726,7 @@ def create_task_comment( snippet = payload.message.strip() if len(snippet) > 500: snippet = f"{snippet[:497]}..." - actor_name = ( - actor.agent.name - if actor.actor_type == "agent" and actor.agent - else "User" - ) + actor_name = actor.agent.name if actor.actor_type == "agent" and actor.agent else "User" for agent in targets.values(): if not agent.openclaw_session_id: continue diff --git a/backend/app/core/agent_auth.py b/backend/app/core/agent_auth.py index 2538461c..4a676c26 100644 --- a/backend/app/core/agent_auth.py +++ b/backend/app/core/agent_auth.py @@ -1,10 +1,10 @@ from __future__ import annotations +import logging from dataclasses import dataclass from typing import Literal from fastapi import Depends, Header, HTTPException, Request, status -import logging from sqlmodel import Session, col, select from app.core.agent_tokens import verify_agent_token diff --git a/backend/app/core/config.py b/backend/app/core/config.py index 63718fb4..75426ee6 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -31,5 +31,4 @@ class Settings(BaseSettings): log_use_utc: bool = False - settings = Settings() diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index 3ed49dbd..3b0adf03 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -5,8 +5,8 @@ from app.models.board_memory import BoardMemory from app.models.board_onboarding import BoardOnboardingSession from app.models.boards import Board from app.models.gateways import Gateway -from app.models.tasks import Task from app.models.task_fingerprints import TaskFingerprint +from app.models.tasks import Task from app.models.users import User __all__ = [ diff --git a/backend/app/schemas/boards.py b/backend/app/schemas/boards.py index 3ea5a462..59223cb2 100644 --- a/backend/app/schemas/boards.py +++ b/backend/app/schemas/boards.py @@ -24,9 +24,7 @@ class BoardCreate(BoardBase): def validate_goal_fields(self): if self.board_type == "goal" and self.goal_confirmed: if not self.objective or not self.success_metrics: - raise ValueError( - "Confirmed goal boards require objective and success_metrics" - ) + raise ValueError("Confirmed goal boards require objective and success_metrics") return self diff --git a/backend/app/services/agent_provisioning.py b/backend/app/services/agent_provisioning.py index f0017324..256f1f68 100644 --- a/backend/app/services/agent_provisioning.py +++ b/backend/app/services/agent_provisioning.py @@ -10,11 +10,7 @@ from jinja2 import Environment, FileSystemLoader, StrictUndefined, select_autoes from app.core.config import settings from app.integrations.openclaw_gateway import GatewayConfig as GatewayClientConfig -from app.integrations.openclaw_gateway import ( - OpenClawGatewayError, - ensure_session, - openclaw_call, -) +from app.integrations.openclaw_gateway import OpenClawGatewayError, ensure_session, openclaw_call from app.models.agents import Agent from app.models.boards import Board from app.models.gateways import Gateway @@ -304,9 +300,7 @@ def _render_agent_files( ) heartbeat_path = _templates_root() / heartbeat_template if heartbeat_path.exists(): - rendered[name] = ( - env.get_template(heartbeat_template).render(**context).strip() - ) + rendered[name] = env.get_template(heartbeat_template).render(**context).strip() continue override = overrides.get(name) if override: @@ -400,7 +394,9 @@ async def _remove_gateway_agent_list( if not isinstance(lst, list): raise OpenClawGatewayError("config agents.list is not a list") - new_list = [entry for entry in lst if not (isinstance(entry, dict) and entry.get("id") == agent_id)] + new_list = [ + entry for entry in lst if not (isinstance(entry, dict) and entry.get("id") == agent_id) + ] if len(new_list) == len(lst): return patch = {"agents": {"list": new_list}}