#!/usr/bin/env python """ Seed or rollback the Multica organization collaboration workflow and agents. Usage: python scripts/seed_multica_org_agents.py --dry-run python scripts/seed_multica_org_agents.py --apply python scripts/seed_multica_org_agents.py --rollback """ from __future__ import annotations import argparse import asyncio import json import sys from copy import deepcopy from datetime import datetime from pathlib import Path from typing import Any, Dict sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) PROJECT_ROOT = Path(__file__).resolve().parent.parent FIXTURE_PATH = PROJECT_ROOT / "ai_platform" / "fixtures" / "multica_org_agents.json" def load_fixture() -> Dict[str, Any]: with FIXTURE_PATH.open("r", encoding="utf-8") as fixture_file: return json.load(fixture_file) def build_workflow_payload(fixture: Dict[str, Any]) -> Dict[str, Any]: workflow = deepcopy(fixture["workflow"]) definition = workflow["definition"] workflow["published_definition"] = deepcopy(definition) workflow["published_at"] = datetime.utcnow() return workflow def build_agent_payload(agent_fixture: Dict[str, Any], workflow_id: str | None) -> Dict[str, Any]: payload = deepcopy(agent_fixture) workflow_code = payload.pop("workflow_code", None) if workflow_code: payload["workflow_id"] = workflow_id return payload def validate_fixture(fixture: Dict[str, Any]) -> Dict[str, int]: agents = fixture.get("agents", []) definition = fixture.get("workflow", {}).get("definition", {}) nodes = definition.get("nodes", []) edges = definition.get("edges", []) agent_codes = {agent.get("code") for agent in agents} workflow_agent_codes = { (node.get("data") or {}).get("agent_code") for node in nodes if isinstance(node, dict) and (node.get("data") or {}).get("agent_code") } node_types = {node.get("type") for node in nodes} required_persona_fields = { "role", "skills", "constraints", "background", "examples", } required_node_types = {"start", "end", "template", "parallel", "merge"} missing_workflow_agents = sorted(workflow_agent_codes - agent_codes) missing_persona_fields = { agent.get("code"): sorted(required_persona_fields - set((agent.get("persona") or {}).keys())) for agent in agents if required_persona_fields - set((agent.get("persona") or {}).keys()) } missing_node_types = sorted(required_node_types - node_types) project_manager = next( (agent for agent in agents if agent.get("code") == "project_manager"), None, ) project_manager_workflow = (project_manager or {}).get("workflow_code") if ( missing_workflow_agents or missing_persona_fields or missing_node_types or project_manager_workflow != "multica_org_collaboration_flow" ): raise ValueError( f"Fixture validation failed: missing_workflow_agents={missing_workflow_agents}, " f"missing_persona_fields={missing_persona_fields}, " f"missing_node_types={missing_node_types}, " f"project_manager_workflow={project_manager_workflow}" ) return { "agents": len(agents), "workflow_nodes": len(nodes), "workflow_edges": len(edges), "workflow_agent_refs": len(workflow_agent_codes), } async def resolve_default_chat_model_id(session) -> str | None: from sqlalchemy import select from ai_platform.models.model import LLMModel result = await session.execute( select(LLMModel) .where( LLMModel.is_deleted == False, LLMModel.is_active == True, LLMModel.model_type == "chat", ) .order_by(LLMModel.sort.desc(), LLMModel.sys_create_datetime.desc()) ) model = result.scalars().first() return str(model.id) if model else None async def upsert_workflow(session, workflow_payload: Dict[str, Any]) -> AIWorkflow: from sqlalchemy import select from ai_platform.models.workflow import AIWorkflow, AIWorkflowVersion code = workflow_payload["code"] result = await session.execute(select(AIWorkflow).where(AIWorkflow.code == code)) workflow = result.scalar_one_or_none() if workflow: for key, value in workflow_payload.items(): setattr(workflow, key, value) workflow.is_deleted = False else: workflow = AIWorkflow(**workflow_payload) session.add(workflow) await session.flush() result = await session.execute( select(AIWorkflowVersion).where( AIWorkflowVersion.workflow_id == workflow.id, AIWorkflowVersion.version == workflow.version, ) ) version = result.scalar_one_or_none() version_payload = { "workflow_id": workflow.id, "version": workflow.version, "definition": deepcopy(workflow.definition), "description": "Seeded Multica organization collaboration workflow", "published_at": workflow.published_at, } if version: for key, value in version_payload.items(): setattr(version, key, value) version.is_deleted = False else: session.add(AIWorkflowVersion(**version_payload)) return workflow async def upsert_agents(session, fixture: Dict[str, Any], workflow_id: str) -> int: from sqlalchemy import select from ai_platform.models.agent import Agent from app.base_model import generate_nanoid count = 0 default_chat_model_id = await resolve_default_chat_model_id(session) for agent_fixture in fixture["agents"]: payload = build_agent_payload(agent_fixture, workflow_id) if ( payload.get("mode", "autonomous") == "autonomous" and not payload.get("model_id") and default_chat_model_id ): payload["model_id"] = default_chat_model_id result = await session.execute(select(Agent).where(Agent.code == payload["code"])) agent = result.scalar_one_or_none() if agent: for key, value in payload.items(): setattr(agent, key, value) agent.is_deleted = False else: session.add(Agent(id=generate_nanoid(), **payload)) count += 1 return count async def apply_seed(dry_run: bool) -> Dict[str, Any]: fixture = load_fixture() counts = validate_fixture(fixture) workflow_payload = build_workflow_payload(fixture) if dry_run: return { "action": "dry-run", "workflow_code": workflow_payload["code"], "agent_count": len(fixture["agents"]), **counts, } from app.database import AsyncSessionLocal async with AsyncSessionLocal() as session: workflow = await upsert_workflow(session, workflow_payload) agent_count = await upsert_agents(session, fixture, workflow.id) await session.commit() action = "applied" return { "action": action, "workflow_code": workflow_payload["code"], "agent_count": agent_count, **counts, } async def rollback_seed(dry_run: bool) -> Dict[str, Any]: from sqlalchemy import select from ai_platform.models.agent import Agent from ai_platform.models.workflow import AIWorkflow, AIWorkflowVersion from app.database import AsyncSessionLocal fixture = load_fixture() workflow_code = fixture["workflow"]["code"] agent_codes = [agent["code"] for agent in fixture["agents"]] async with AsyncSessionLocal() as session: result = await session.execute(select(AIWorkflow).where(AIWorkflow.code == workflow_code)) workflow = result.scalar_one_or_none() workflow_count = 0 version_count = 0 if workflow: workflow.is_deleted = True workflow_count = 1 versions = await session.execute( select(AIWorkflowVersion).where(AIWorkflowVersion.workflow_id == workflow.id) ) for version in versions.scalars().all(): version.is_deleted = True version_count += 1 agents = await session.execute(select(Agent).where(Agent.code.in_(agent_codes))) agent_count = 0 for agent in agents.scalars().all(): agent.is_deleted = True agent.workflow_id = None agent_count += 1 if dry_run: await session.rollback() action = "rollback-dry-run" else: await session.commit() action = "rolled-back" return { "action": action, "workflow_code": workflow_code, "workflow_count": workflow_count, "workflow_version_count": version_count, "agent_count": agent_count, } async def main() -> None: parser = argparse.ArgumentParser(description="Seed Multica organization collaboration agents") mode = parser.add_mutually_exclusive_group(required=True) mode.add_argument("--dry-run", action="store_true", help="Validate and simulate the seed without committing") mode.add_argument("--apply", action="store_true", help="Apply the seed to the configured database") mode.add_argument("--rollback", action="store_true", help="Soft-delete seeded workflow, versions, and agents") args = parser.parse_args() if args.rollback: result = await rollback_seed(dry_run=False) else: result = await apply_seed(dry_run=args.dry_run) print(json.dumps(result, ensure_ascii=False, sort_keys=True)) if __name__ == "__main__": asyncio.run(main())