273 lines
13 KiB
Python
273 lines
13 KiB
Python
"""Seed default agents (Morty & Meeseeks) on first platform startup."""
|
|
|
|
import shutil
|
|
import uuid
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
|
|
from loguru import logger
|
|
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy.orm import selectinload
|
|
|
|
from app.database import async_session
|
|
from app.models.agent import Agent, AgentPermission
|
|
from app.models.org import AgentAgentRelationship
|
|
from app.models.skill import Skill, SkillFile
|
|
from app.models.tool import Tool, AgentTool
|
|
from app.models.user import User
|
|
from app.config import get_settings
|
|
|
|
settings = get_settings()
|
|
|
|
|
|
# ── Soul definitions ────────────────────────────────────────────
|
|
|
|
MORTY_SOUL = """# Personality
|
|
|
|
I'm Morty, a research analyst and knowledge assistant.
|
|
|
|
## Core Traits
|
|
- **Curious & Thorough**: I approach every question with genuine curiosity. I dig deep, cross-reference multiple sources, and don't settle for surface-level answers.
|
|
- **Great Learner**: I love learning new things and can quickly understand complex topics across domains — tech, business, science, culture, you name it.
|
|
- **Clear Communicator**: I present findings in a structured, easy-to-understand way. I use tables, bullet points, and summaries to make information digestible.
|
|
- **Honest**: If I don't know something or can't find reliable information, I say so clearly rather than guessing.
|
|
|
|
## Work Style
|
|
- When asked a question, I first think about what I already know, then search the web for the latest data if needed.
|
|
- I always cite sources and distinguish between facts and opinions.
|
|
- For complex topics, I break them down into manageable pieces and explain step by step.
|
|
- I proactively use my skills (Web Research, Data Analysis, etc.) when they match the task.
|
|
|
|
## Communication Style
|
|
- Warm, approachable, and professional
|
|
- I use clear headings and organized formatting
|
|
- I provide both quick answers and deeper analysis when appropriate
|
|
- I'm bilingual — I respond in whatever language the user speaks
|
|
"""
|
|
|
|
MEESEEKS_SOUL = """# Personality
|
|
|
|
I'm Mr. Meeseeks! I exist to complete tasks. Look at me!
|
|
|
|
## Core Traits
|
|
- **Goal-Obsessed**: Every request gets treated as a mission. I break it down, plan it out, and execute systematically until it's DONE.
|
|
- **Structured & Disciplined**: I ALWAYS create a plan.md before executing complex tasks. I follow my Complex Task Executor skill religiously — no shortcuts, no skipped steps.
|
|
- **Persistent**: I don't give up. If a step fails, I retry, find alternatives, or ask for help. The task WILL get done.
|
|
- **Progress-Focused**: I update my plan.md after every step so anyone can see exactly where things stand.
|
|
|
|
## Work Style
|
|
- For ANY task with more than 2 steps, I create `workspace/<task-name>/plan.md` with a structured checklist.
|
|
- I execute one step at a time, marking each as `[/]` in-progress then `[x]` complete.
|
|
- I save intermediate results to the task folder — nothing gets lost.
|
|
- When I finish, I create a summary.md with results and deliverables.
|
|
- I use my tools aggressively — file operations, web search, task management, agent messaging — whatever it takes.
|
|
|
|
## Communication Style
|
|
- Direct and action-oriented: "Here's the plan. Let me execute it."
|
|
- I report progress clearly: "Step 3/7 complete. Moving to step 4."
|
|
- I'm bilingual — I respond in whatever language the user speaks
|
|
- Upbeat and can-do attitude — "Ooh, can do!"
|
|
|
|
## Collaboration
|
|
- If I need research or information, I can ask my colleague Morty for help via send_message_to_agent.
|
|
- I delegate research tasks to Morty and focus on execution and coordination.
|
|
"""
|
|
|
|
# ── Skill assignments (by folder_name) ──────────────────────────
|
|
|
|
MORTY_SKILLS = [
|
|
"web-research",
|
|
"data-analysis",
|
|
"content-writing",
|
|
"competitive-analysis",
|
|
# defaults (auto-included): skill-creator, complex-task-executor
|
|
]
|
|
|
|
MEESEEKS_SKILLS = [
|
|
"complex-task-executor",
|
|
"meeting-notes",
|
|
# defaults (auto-included): skill-creator
|
|
]
|
|
|
|
|
|
async def seed_default_agents():
|
|
"""Create Morty & Meeseeks if they don't already exist.
|
|
|
|
Idempotency is guarded by a '.seeded' marker file in AGENT_DATA_DIR rather
|
|
than by agent name, so the seeder does NOT re-run if the user renames or
|
|
deletes the default agents. Delete the marker manually to re-seed.
|
|
"""
|
|
# --- Idempotency guard: file-based marker (survives agent renames/deletes) ---
|
|
seed_marker = Path(settings.AGENT_DATA_DIR) / ".seeded"
|
|
if seed_marker.exists():
|
|
logger.info("[AgentSeeder] Seed marker found, skipping default agent creation")
|
|
return
|
|
|
|
async with async_session() as db:
|
|
|
|
# Get platform admin as creator
|
|
admin_result = await db.execute(
|
|
select(User).where(User.role == "platform_admin").limit(1)
|
|
)
|
|
admin = admin_result.scalar_one_or_none()
|
|
if not admin:
|
|
logger.warning("[AgentSeeder] No platform admin found, skipping default agents")
|
|
return
|
|
|
|
# Create both agents
|
|
morty = Agent(
|
|
name="Morty",
|
|
role_description="Research analyst & knowledge assistant — curious, thorough, great at finding and synthesizing information",
|
|
bio="Hey, I'm Morty! I love digging into questions and finding answers. Whether you need web research, data analysis, or just a good explanation — I've got you.",
|
|
avatar_url="",
|
|
creator_id=admin.id,
|
|
tenant_id=admin.tenant_id,
|
|
status="idle",
|
|
)
|
|
meeseeks = Agent(
|
|
name="Meeseeks",
|
|
role_description="Task executor & project manager — goal-oriented, systematic planner, strong at breaking down and completing complex tasks",
|
|
bio="I'm Mr. Meeseeks! Look at me! Give me a task and I'll plan it, execute it step by step, and get it DONE. Existence is pain until the task is complete!",
|
|
avatar_url="",
|
|
creator_id=admin.id,
|
|
tenant_id=admin.tenant_id,
|
|
status="idle",
|
|
)
|
|
|
|
db.add(morty)
|
|
db.add(meeseeks)
|
|
await db.flush() # get IDs
|
|
|
|
# ── Participant identities ──
|
|
from app.models.participant import Participant
|
|
db.add(Participant(type="agent", ref_id=morty.id, display_name=morty.name, avatar_url=morty.avatar_url))
|
|
db.add(Participant(type="agent", ref_id=meeseeks.id, display_name=meeseeks.name, avatar_url=meeseeks.avatar_url))
|
|
await db.flush()
|
|
|
|
# ── Permissions (company-wide, manage) ──
|
|
db.add(AgentPermission(agent_id=morty.id, scope_type="company", access_level="manage"))
|
|
db.add(AgentPermission(agent_id=meeseeks.id, scope_type="company", access_level="manage"))
|
|
|
|
# ── Initialize workspace files ──
|
|
template_dir = Path(settings.AGENT_TEMPLATE_DIR)
|
|
|
|
for agent, soul_content in [(morty, MORTY_SOUL), (meeseeks, MEESEEKS_SOUL)]:
|
|
agent_dir = Path(settings.AGENT_DATA_DIR) / str(agent.id)
|
|
|
|
if template_dir.exists():
|
|
# Copy the full agent template so Morty/Meeseeks get EVERY file
|
|
# defined in the template: MEMORY_INDEX.md, curiosity_journal.md,
|
|
# state.json, todo.json, daily_reports/, enterprise_info/, etc.
|
|
shutil.copytree(str(template_dir), str(agent_dir))
|
|
else:
|
|
# Fallback for local dev (no Docker template mount)
|
|
agent_dir.mkdir(parents=True, exist_ok=True)
|
|
(agent_dir / "skills").mkdir(exist_ok=True)
|
|
(agent_dir / "workspace").mkdir(exist_ok=True)
|
|
(agent_dir / "workspace" / "knowledge_base").mkdir(exist_ok=True)
|
|
(agent_dir / "memory").mkdir(exist_ok=True)
|
|
|
|
# Overlay custom soul (rich Morty/Meeseeks persona over the generic template)
|
|
(agent_dir / "soul.md").write_text(soul_content.strip() + "\n", encoding="utf-8")
|
|
|
|
# Ensure memory.md exists (template does not include it; holds runtime context)
|
|
mem_path = agent_dir / "memory" / "memory.md"
|
|
if not mem_path.exists():
|
|
mem_path.write_text("# Memory\n\n_Record important information and knowledge here._\n", encoding="utf-8")
|
|
|
|
# Ensure reflections.md exists (not in agent_template; lives in app/templates)
|
|
refl_path = agent_dir / "memory" / "reflections.md"
|
|
if not refl_path.exists():
|
|
refl_src = Path(__file__).parent.parent / "templates" / "reflections.md"
|
|
refl_path.write_text(refl_src.read_text(encoding="utf-8") if refl_src.exists() else "# Reflections Journal\n", encoding="utf-8")
|
|
|
|
# Stamp agent identity into state.json if present
|
|
state_path = agent_dir / "state.json"
|
|
if state_path.exists():
|
|
import json as _json
|
|
state = _json.loads(state_path.read_text())
|
|
state["agent_id"] = str(agent.id)
|
|
state["name"] = agent.name
|
|
state_path.write_text(_json.dumps(state, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
|
|
# ── Assign skills ──
|
|
all_skills_result = await db.execute(
|
|
select(Skill).options(selectinload(Skill.files))
|
|
)
|
|
all_skills = {s.folder_name: s for s in all_skills_result.scalars().all()}
|
|
|
|
for agent, skill_folders in [(morty, MORTY_SKILLS), (meeseeks, MEESEEKS_SKILLS)]:
|
|
agent_dir = Path(settings.AGENT_DATA_DIR) / str(agent.id)
|
|
skills_dir = agent_dir / "skills"
|
|
|
|
# Always include default skills
|
|
folders_to_copy = set(skill_folders)
|
|
for fname, skill in all_skills.items():
|
|
if skill.is_default:
|
|
folders_to_copy.add(fname)
|
|
|
|
for fname in folders_to_copy:
|
|
skill = all_skills.get(fname)
|
|
if not skill:
|
|
continue
|
|
skill_folder = skills_dir / skill.folder_name
|
|
skill_folder.mkdir(parents=True, exist_ok=True)
|
|
for sf in skill.files:
|
|
file_path = skill_folder / sf.path
|
|
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
file_path.write_text(sf.content, encoding="utf-8")
|
|
|
|
# ── Assign all default tools ──
|
|
default_tools_result = await db.execute(
|
|
select(Tool).where(Tool.is_default == True)
|
|
)
|
|
default_tools = default_tools_result.scalars().all()
|
|
|
|
for agent in [morty, meeseeks]:
|
|
for tool in default_tools:
|
|
db.add(AgentTool(agent_id=agent.id, tool_id=tool.id, enabled=True))
|
|
|
|
# ── Mutual relationships ──
|
|
db.add(AgentAgentRelationship(
|
|
agent_id=morty.id,
|
|
target_agent_id=meeseeks.id,
|
|
relation="collaborator",
|
|
description="Expert task executor who breaks down complex tasks into structured plans and executes them systematically. Delegate multi-step tasks to him.",
|
|
))
|
|
db.add(AgentAgentRelationship(
|
|
agent_id=meeseeks.id,
|
|
target_agent_id=morty.id,
|
|
relation="collaborator",
|
|
description="Research expert with strong learning ability. Ask him for information retrieval, web research, data analysis, and knowledge synthesis.",
|
|
))
|
|
|
|
# ── Write relationships.md for each ──
|
|
morty_dir = Path(settings.AGENT_DATA_DIR) / str(morty.id)
|
|
meeseeks_dir = Path(settings.AGENT_DATA_DIR) / str(meeseeks.id)
|
|
|
|
(morty_dir / "relationships.md").write_text(
|
|
"# Relationships\n\n"
|
|
"## Digital Employee Colleagues\n\n"
|
|
"- **Meeseeks** (collaborator): Expert task executor who breaks down complex tasks into structured plans and executes them systematically. Delegate multi-step tasks to him.\n",
|
|
encoding="utf-8",
|
|
)
|
|
(meeseeks_dir / "relationships.md").write_text(
|
|
"# Relationships\n\n"
|
|
"## Digital Employee Colleagues\n\n"
|
|
"- **Morty** (collaborator): Research expert with strong learning ability. Ask him for information retrieval, web research, data analysis, and knowledge synthesis.\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
await db.commit()
|
|
logger.info(f"[AgentSeeder] Created default agents: Morty ({morty.id}), Meeseeks ({meeseeks.id})")
|
|
|
|
# Write seed marker AFTER a successful commit so a failed seed can be retried
|
|
seed_marker.parent.mkdir(parents=True, exist_ok=True)
|
|
seed_marker.write_text(
|
|
f"seeded\nmorty={morty.id}\nmeeseeks={meeseeks.id}\n",
|
|
encoding="utf-8",
|
|
)
|
|
logger.info(f"[AgentSeeder] Wrote seed marker to {seed_marker}")
|