280 lines
9.2 KiB
Python
280 lines
9.2 KiB
Python
#!/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("agent_code")
|
|
for node in nodes
|
|
if isinstance(node, dict) and node.get("agent_code")
|
|
}
|
|
node_types = {node.get("type") for node in nodes}
|
|
|
|
required_agent_codes = {
|
|
"multica_product_manager",
|
|
"business_requirements_analyst",
|
|
"system_architect",
|
|
"frontend_engineer",
|
|
"backend_engineer",
|
|
"qa_engineer",
|
|
"project_manager",
|
|
}
|
|
required_persona_fields = {
|
|
"role",
|
|
"skills",
|
|
"constraints",
|
|
"background",
|
|
"examples",
|
|
}
|
|
required_node_types = {"start", "end", "condition", "template", "parallel", "merge"}
|
|
|
|
missing_agents = sorted(required_agent_codes - agent_codes)
|
|
extra_agents = sorted(agent_codes - required_agent_codes)
|
|
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_agents
|
|
or extra_agents
|
|
or 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_agents={missing_agents}, "
|
|
f"extra_agents={extra_agents}, "
|
|
f"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 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
|
|
for agent_fixture in fixture["agents"]:
|
|
payload = build_agent_payload(agent_fixture, workflow_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())
|