# app/init_db.py """ Async database initialization script. Creates the first superuser if configured and doesn't already exist. Seeds default agent types (production data) and demo data (when DEMO_MODE is enabled). """ import asyncio import json import logging import random from datetime import UTC, date, datetime, timedelta from pathlib import Path from sqlalchemy import select, text from sqlalchemy.ext.asyncio import AsyncSession from app.core.config import settings from app.core.database import SessionLocal, engine from app.crud.syndarix.agent_type import agent_type as agent_type_crud from app.crud.user import user as user_crud from app.models.organization import Organization from app.models.syndarix import AgentInstance, AgentType, Issue, Project, Sprint from app.models.syndarix.enums import ( AgentStatus, AutonomyLevel, ClientMode, IssuePriority, IssueStatus, IssueType, ProjectComplexity, ProjectStatus, SprintStatus, ) from app.models.user import User from app.models.user_organization import UserOrganization from app.schemas.syndarix import AgentTypeCreate from app.schemas.users import UserCreate logger = logging.getLogger(__name__) # Data file paths DATA_DIR = Path(__file__).parent.parent / "data" DEFAULT_AGENT_TYPES_PATH = DATA_DIR / "default_agent_types.json" DEMO_DATA_PATH = DATA_DIR / "demo_data.json" async def init_db() -> User | None: """ Initialize database with first superuser if settings are configured and user doesn't exist. Returns: The created or existing superuser, or None if creation fails """ # Use default values if not set in environment variables superuser_email = settings.FIRST_SUPERUSER_EMAIL or "admin@example.com" default_password = "AdminPassword123!" if settings.DEMO_MODE: default_password = "AdminPass1234!" superuser_password = settings.FIRST_SUPERUSER_PASSWORD or default_password if not settings.FIRST_SUPERUSER_EMAIL or not settings.FIRST_SUPERUSER_PASSWORD: logger.warning( "First superuser credentials not configured in settings. " f"Using defaults: {superuser_email}" ) async with SessionLocal() as session: try: # Check if superuser already exists existing_user = await user_crud.get_by_email(session, email=superuser_email) if existing_user: logger.info(f"Superuser already exists: {existing_user.email}") else: # Create superuser if doesn't exist user_in = UserCreate( email=superuser_email, password=superuser_password, first_name="Admin", last_name="User", is_superuser=True, ) existing_user = await user_crud.create(session, obj_in=user_in) await session.commit() await session.refresh(existing_user) logger.info(f"Created first superuser: {existing_user.email}") # ALWAYS load default agent types (production data) await load_default_agent_types(session) # Only load demo data if in demo mode if settings.DEMO_MODE: await load_demo_data(session) return existing_user except Exception as e: await session.rollback() logger.error(f"Error initializing database: {e}") raise def _load_json_file(path: Path): with open(path) as f: return json.load(f) async def load_default_agent_types(session: AsyncSession) -> None: """ Load default agent types from JSON file. These are production defaults - created only if they don't exist, never overwritten. This allows users to customize agent types without worrying about server restarts. """ if not DEFAULT_AGENT_TYPES_PATH.exists(): logger.warning( f"Default agent types file not found: {DEFAULT_AGENT_TYPES_PATH}" ) return try: data = await asyncio.to_thread(_load_json_file, DEFAULT_AGENT_TYPES_PATH) for agent_type_data in data: slug = agent_type_data["slug"] # Check if agent type already exists existing = await agent_type_crud.get_by_slug(session, slug=slug) if existing: logger.debug(f"Agent type already exists: {agent_type_data['name']}") continue # Create the agent type agent_type_in = AgentTypeCreate( name=agent_type_data["name"], slug=slug, description=agent_type_data.get("description"), expertise=agent_type_data.get("expertise", []), personality_prompt=agent_type_data["personality_prompt"], primary_model=agent_type_data["primary_model"], fallback_models=agent_type_data.get("fallback_models", []), model_params=agent_type_data.get("model_params", {}), mcp_servers=agent_type_data.get("mcp_servers", []), tool_permissions=agent_type_data.get("tool_permissions", {}), is_active=agent_type_data.get("is_active", True), # Category and display fields category=agent_type_data.get("category"), icon=agent_type_data.get("icon", "bot"), color=agent_type_data.get("color", "#3B82F6"), sort_order=agent_type_data.get("sort_order", 0), typical_tasks=agent_type_data.get("typical_tasks", []), collaboration_hints=agent_type_data.get("collaboration_hints", []), ) await agent_type_crud.create(session, obj_in=agent_type_in) logger.info(f"Created default agent type: {agent_type_data['name']}") logger.info("Default agent types loaded successfully") except Exception as e: logger.error(f"Error loading default agent types: {e}") raise async def load_demo_data(session: AsyncSession) -> None: """ Load demo data from JSON file. Only runs when DEMO_MODE is enabled. Creates demo organizations, users, projects, sprints, agent instances, and issues. """ if not DEMO_DATA_PATH.exists(): logger.warning(f"Demo data file not found: {DEMO_DATA_PATH}") return try: data = await asyncio.to_thread(_load_json_file, DEMO_DATA_PATH) # Build lookup maps for FK resolution org_map: dict[str, Organization] = {} user_map: dict[str, User] = {} project_map: dict[str, Project] = {} sprint_map: dict[str, Sprint] = {} # key: "project_slug:sprint_number" agent_type_map: dict[str, AgentType] = {} agent_instance_map: dict[ str, AgentInstance ] = {} # key: "project_slug:agent_name" # ======================== # 1. Create Organizations # ======================== for org_data in data.get("organizations", []): org_result = await session.execute( select(Organization).where(Organization.slug == org_data["slug"]) ) existing_org = org_result.scalar_one_or_none() if not existing_org: org = Organization( name=org_data["name"], slug=org_data["slug"], description=org_data.get("description"), is_active=True, ) session.add(org) await session.flush() org_map[str(org.slug)] = org logger.info(f"Created demo organization: {org.name}") else: org_map[str(existing_org.slug)] = existing_org # ======================== # 2. Create Users # ======================== for user_data in data.get("users", []): existing_user = await user_crud.get_by_email( session, email=user_data["email"] ) if not existing_user: user_in = UserCreate( email=user_data["email"], password=user_data["password"], first_name=user_data["first_name"], last_name=user_data["last_name"], is_superuser=user_data["is_superuser"], is_active=user_data.get("is_active", True), ) user = await user_crud.create(session, obj_in=user_in) # Randomize created_at for demo data (last 30 days) days_ago = random.randint(0, 30) # noqa: S311 random_time = datetime.now(UTC) - timedelta(days=days_ago) random_time = random_time.replace( hour=random.randint(0, 23), # noqa: S311 minute=random.randint(0, 59), # noqa: S311 ) await session.execute( text( "UPDATE users SET created_at = :created_at, is_active = :is_active WHERE id = :user_id" ), { "created_at": random_time, "is_active": user_data.get("is_active", True), "user_id": user.id, }, ) logger.info( f"Created demo user: {user.email} (created {days_ago} days ago)" ) # Add to organization if specified org_slug = user_data.get("organization_slug") role = user_data.get("role") if org_slug and org_slug in org_map and role: org = org_map[org_slug] member = UserOrganization( user_id=user.id, organization_id=org.id, role=role ) session.add(member) logger.info(f"Added {user.email} to {org.name} as {role}") user_map[str(user.email)] = user else: user_map[str(existing_user.email)] = existing_user logger.debug(f"Demo user already exists: {existing_user.email}") await session.flush() # Add admin user to map with special "__admin__" key # This allows demo data to reference the admin user as owner superuser_email = settings.FIRST_SUPERUSER_EMAIL or "admin@example.com" admin_user = await user_crud.get_by_email(session, email=superuser_email) if admin_user: user_map["__admin__"] = admin_user user_map[str(admin_user.email)] = admin_user logger.debug(f"Added admin user to map: {admin_user.email}") # ======================== # 3. Load Agent Types Map (for FK resolution) # ======================== agent_types_result = await session.execute(select(AgentType)) for at in agent_types_result.scalars().all(): agent_type_map[str(at.slug)] = at # ======================== # 4. Create Projects # ======================== for project_data in data.get("projects", []): project_result = await session.execute( select(Project).where(Project.slug == project_data["slug"]) ) existing_project = project_result.scalar_one_or_none() if not existing_project: # Resolve owner email to user ID owner_id = None owner_email = project_data.get("owner_email") if owner_email and owner_email in user_map: owner_id = user_map[owner_email].id project = Project( name=project_data["name"], slug=project_data["slug"], description=project_data.get("description"), owner_id=owner_id, autonomy_level=AutonomyLevel( project_data.get("autonomy_level", "milestone") ), status=ProjectStatus(project_data.get("status", "active")), complexity=ProjectComplexity( project_data.get("complexity", "medium") ), client_mode=ClientMode(project_data.get("client_mode", "auto")), settings=project_data.get("settings", {}), ) session.add(project) await session.flush() project_map[str(project.slug)] = project logger.info(f"Created demo project: {project.name}") else: project_map[str(existing_project.slug)] = existing_project logger.debug(f"Demo project already exists: {existing_project.name}") # ======================== # 5. Create Sprints # ======================== for sprint_data in data.get("sprints", []): project_slug = sprint_data["project_slug"] sprint_number = sprint_data["number"] sprint_key = f"{project_slug}:{sprint_number}" if project_slug not in project_map: logger.warning(f"Project not found for sprint: {project_slug}") continue sprint_project = project_map[project_slug] # Check if sprint exists sprint_result = await session.execute( select(Sprint).where( Sprint.project_id == sprint_project.id, Sprint.number == sprint_number, ) ) existing_sprint = sprint_result.scalar_one_or_none() if not existing_sprint: sprint = Sprint( project_id=sprint_project.id, name=sprint_data["name"], number=sprint_number, goal=sprint_data.get("goal"), start_date=date.fromisoformat(sprint_data["start_date"]), end_date=date.fromisoformat(sprint_data["end_date"]), status=SprintStatus(sprint_data.get("status", "planned")), planned_points=sprint_data.get("planned_points"), ) session.add(sprint) await session.flush() sprint_map[sprint_key] = sprint logger.info( f"Created demo sprint: {sprint.name} for {sprint_project.name}" ) else: sprint_map[sprint_key] = existing_sprint logger.debug(f"Demo sprint already exists: {existing_sprint.name}") # ======================== # 6. Create Agent Instances # ======================== for agent_data in data.get("agent_instances", []): project_slug = agent_data["project_slug"] agent_type_slug = agent_data["agent_type_slug"] agent_name = agent_data["name"] agent_key = f"{project_slug}:{agent_name}" if project_slug not in project_map: logger.warning(f"Project not found for agent: {project_slug}") continue if agent_type_slug not in agent_type_map: logger.warning(f"Agent type not found: {agent_type_slug}") continue agent_project = project_map[project_slug] agent_type = agent_type_map[agent_type_slug] # Check if agent instance exists (by name within project) agent_result = await session.execute( select(AgentInstance).where( AgentInstance.project_id == agent_project.id, AgentInstance.name == agent_name, ) ) existing_agent = agent_result.scalar_one_or_none() if not existing_agent: agent_instance = AgentInstance( project_id=agent_project.id, agent_type_id=agent_type.id, name=agent_name, status=AgentStatus(agent_data.get("status", "idle")), current_task=agent_data.get("current_task"), ) session.add(agent_instance) await session.flush() agent_instance_map[agent_key] = agent_instance logger.info( f"Created demo agent: {agent_name} ({agent_type.name}) " f"for {agent_project.name}" ) else: agent_instance_map[agent_key] = existing_agent logger.debug(f"Demo agent already exists: {existing_agent.name}") # ======================== # 7. Create Issues # ======================== for issue_data in data.get("issues", []): project_slug = issue_data["project_slug"] if project_slug not in project_map: logger.warning(f"Project not found for issue: {project_slug}") continue issue_project = project_map[project_slug] # Check if issue exists (by title within project - simple heuristic) issue_result = await session.execute( select(Issue).where( Issue.project_id == issue_project.id, Issue.title == issue_data["title"], ) ) existing_issue = issue_result.scalar_one_or_none() if not existing_issue: # Resolve sprint sprint_id = None sprint_number = issue_data.get("sprint_number") if sprint_number: sprint_key = f"{project_slug}:{sprint_number}" if sprint_key in sprint_map: sprint_id = sprint_map[sprint_key].id # Resolve assigned agent assigned_agent_id = None assigned_agent_name = issue_data.get("assigned_agent_name") if assigned_agent_name: agent_key = f"{project_slug}:{assigned_agent_name}" if agent_key in agent_instance_map: assigned_agent_id = agent_instance_map[agent_key].id issue = Issue( project_id=issue_project.id, sprint_id=sprint_id, type=IssueType(issue_data.get("type", "task")), title=issue_data["title"], body=issue_data.get("body", ""), status=IssueStatus(issue_data.get("status", "open")), priority=IssuePriority(issue_data.get("priority", "medium")), labels=issue_data.get("labels", []), story_points=issue_data.get("story_points"), assigned_agent_id=assigned_agent_id, ) session.add(issue) logger.info(f"Created demo issue: {issue.title[:50]}...") else: logger.debug( f"Demo issue already exists: {existing_issue.title[:50]}..." ) await session.commit() logger.info("Demo data loaded successfully") except Exception as e: await session.rollback() logger.error(f"Error loading demo data: {e}") raise async def main(): """Main entry point for database initialization.""" # Configure logging to show info logs logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", ) try: user = await init_db() if user: print("Database initialized successfully") print(f"Superuser: {user.email}") else: print("Failed to initialize database") except Exception as e: print(f"Error initializing database: {e}") raise finally: # Close the engine await engine.dispose() if __name__ == "__main__": asyncio.run(main())