308 lines
10 KiB
Python
308 lines
10 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("data") or {}).get("agent_code")
|
|
for node in nodes
|
|
if isinstance(node, dict) and (node.get("data") or {}).get("agent_code")
|
|
}
|
|
node_by_id = {node.get("id"): node for node in nodes if isinstance(node, dict)}
|
|
node_types = {node.get("type") for node in nodes}
|
|
outgoing_edges: Dict[str, list[str]] = {}
|
|
for edge in edges:
|
|
if not isinstance(edge, dict):
|
|
continue
|
|
source = edge.get("source")
|
|
target = edge.get("target")
|
|
if source and target:
|
|
outgoing_edges.setdefault(source, []).append(target)
|
|
|
|
required_persona_fields = {
|
|
"role",
|
|
"skills",
|
|
"constraints",
|
|
"background",
|
|
"examples",
|
|
}
|
|
required_node_types = {"start", "end", "template", "parallel", "merge"}
|
|
multi_outgoing_node_types = {"parallel", "condition", "intent", "choice"}
|
|
|
|
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)
|
|
invalid_multi_outgoing = {
|
|
source: targets
|
|
for source, targets in outgoing_edges.items()
|
|
if len(targets) > 1
|
|
and (node_by_id.get(source) or {}).get("type") not in multi_outgoing_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 invalid_multi_outgoing
|
|
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"invalid_multi_outgoing={invalid_multi_outgoing}, "
|
|
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())
|