From 7cc333a8622fa33e5d44b7b25da48ef8d6b04579 Mon Sep 17 00:00:00 2001 From: qzl Date: Wed, 4 Mar 2026 10:49:59 +0800 Subject: [PATCH] docs: add agent architecture simplification implementation plan --- .../2026-03-04-simplify-agent-architecture.md | 844 ++++++++++++++++++ 1 file changed, 844 insertions(+) create mode 100644 docs/plans/2026-03-04-simplify-agent-architecture.md diff --git a/docs/plans/2026-03-04-simplify-agent-architecture.md b/docs/plans/2026-03-04-simplify-agent-architecture.md new file mode 100644 index 0000000..186e86d --- /dev/null +++ b/docs/plans/2026-03-04-simplify-agent-architecture.md @@ -0,0 +1,844 @@ +# Agent Architecture Simplification Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Simplify agent configuration by removing redundant user_agents table and renaming user_agent_catalog to system_agents + +**Architecture:** Delete user_agents table (including memories.agent_id dependency), rename user_agent_catalog to system_agents, update all references in code + +**Tech Stack:** Python 3.11+, SQLAlchemy, Alembic, PostgreSQL + +--- + +## Prerequisites + +- [ ] Current branch: dev +- [ ] No uncommitted changes +- [ ] Docker services running (Supabase local) + +## Task 1: Create Database Migration + +**Files:** +- Create: `backend/alembic/versions/20260304_simplify_agent_architecture.py` + +**Step 1: Create migration file** + +Run: `cd backend && uv run alembic revision -m "simplify_agent_architecture"` + +Expected: New migration file created with revision ID + +**Step 2: Write migration upgrade logic** + +Edit the generated migration file with this complete upgrade function: + +```python +def upgrade() -> None: + # 1. Delete memories.agent_id dependencies + op.drop_constraint("fk_memories_agent_id", "memories", type_="foreignkey") + op.drop_constraint("chk_memory_type_agent_id", "memories", type_="check") + op.execute("DROP INDEX IF EXISTS ix_memories_agent_type_status") + op.drop_column("memories", "agent_id") + + # 2. Delete user_agents table + _drop_rls("user_agents") + + op.drop_constraint("fk_user_agents_updated_by", "user_agents", type_="foreignkey") + op.drop_constraint("fk_user_agents_created_by", "user_agents", type_="foreignkey") + op.drop_constraint("fk_user_agents_llm_id", "user_agents", type_="foreignkey") + op.drop_constraint("fk_user_agents_user_id", "user_agents", type_="foreignkey") + op.drop_constraint("chk_agent_type", "user_agents", type_="check") + op.drop_constraint("uq_user_agents_user_id_agent_type", "user_agents", type_="unique") + + op.execute("DROP INDEX IF EXISTS ix_user_agents_status") + op.execute("DROP INDEX IF EXISTS ix_user_agents_agent_type") + + op.drop_table("user_agents") + + # 3. Rename user_agent_catalog to system_agents + _drop_rls("user_agent_catalog") + + op.rename_table("user_agent_catalog", "system_agents") + + op.execute( + "ALTER TABLE system_agents RENAME CONSTRAINT fk_user_agent_catalog_llm_id " + "TO fk_system_agents_llm_id" + ) + op.execute( + "ALTER TABLE system_agents RENAME CONSTRAINT chk_user_agent_catalog_status " + "TO chk_system_agents_status" + ) + + _enable_rls("system_agents") + + # 4. Update trigger + op.execute("DROP TRIGGER IF EXISTS on_auth_user_created ON auth.users") + op.execute("DROP FUNCTION IF EXISTS public.create_profile_for_new_user()") + + op.execute(""" + CREATE OR REPLACE FUNCTION public.create_profile_for_new_user() + RETURNS trigger + LANGUAGE plpgsql + SECURITY DEFINER + SET search_path = public + AS $$ + BEGIN + INSERT INTO public.profiles (id, username, avatar_url, bio, settings, created_at, updated_at) + VALUES ( + NEW.id, + COALESCE( + NEW.raw_user_meta_data ->> 'username', + split_part(NEW.email, '@', 1), + 'user_' || substring(NEW.id::text, 1, 8) + ), + NULL, + NULL, + '{"agent_prompts": {}}'::jsonb, + now(), + now() + ) + ON CONFLICT (id) DO NOTHING; + + RETURN NEW; + END; + $$ + """) + + op.execute(""" + CREATE TRIGGER on_auth_user_created + AFTER INSERT ON auth.users + FOR EACH ROW EXECUTE FUNCTION public.create_profile_for_new_user() + """) + + # 5. Update existing profiles.settings + op.execute(""" + UPDATE profiles + SET settings = jsonb_set( + COALESCE(settings, '{}'::jsonb), + '{agent_prompts}', + '{}'::jsonb + ) + WHERE NOT settings ? 'agent_prompts' + """) +``` + +**Step 3: Write migration downgrade logic** + +Add this complete downgrade function: + +```python +def downgrade() -> None: + # 1. Revert trigger + op.execute("DROP TRIGGER IF EXISTS on_auth_user_created ON auth.users") + op.execute("DROP FUNCTION IF EXISTS public.create_profile_for_new_user()") + + op.execute(""" + CREATE OR REPLACE FUNCTION public.create_profile_for_new_user() + RETURNS trigger + LANGUAGE plpgsql + SECURITY DEFINER + SET search_path = public + AS $$ + BEGIN + INSERT INTO public.profiles (id, username, avatar_url, bio, settings, created_at, updated_at) + VALUES ( + NEW.id, + COALESCE( + NEW.raw_user_meta_data ->> 'username', + split_part(NEW.email, '@', 1), + 'user_' || substring(NEW.id::text, 1, 8) + ), + NULL, + NULL, + '{}'::jsonb, + now(), + now() + ) + ON CONFLICT (id) DO NOTHING; + + INSERT INTO public.user_agents (id, user_id, llm_id, agent_type, config, status, created_by, updated_by) + SELECT + gen_random_uuid(), + NEW.id, + uac.llm_id, + uac.agent_type, + uac.config, + uac.status, + NEW.id, + NEW.id + FROM public.user_agent_catalog uac; + + RETURN NEW; + END; + $$ + """) + + op.execute(""" + CREATE TRIGGER on_auth_user_created + AFTER INSERT ON auth.users + FOR EACH ROW EXECUTE FUNCTION public.create_profile_for_new_user() + """) + + # 2. Revert rename: system_agents -> user_agent_catalog + _drop_rls("system_agents") + + op.rename_table("system_agents", "user_agent_catalog") + + op.execute( + "ALTER TABLE user_agent_catalog RENAME CONSTRAINT fk_system_agents_llm_id " + "TO fk_user_agent_catalog_llm_id" + ) + op.execute( + "ALTER TABLE user_agent_catalog RENAME CONSTRAINT chk_system_agents_status " + "TO chk_user_agent_catalog_status" + ) + + _enable_rls("user_agent_catalog") + + # 3. Recreate user_agents table + op.create_table( + "user_agents", + sa.Column("id", sa.UUID(), nullable=False), + sa.Column("user_id", sa.UUID(), nullable=False), + sa.Column("llm_id", sa.UUID(), nullable=False), + sa.Column("agent_type", sa.String(length=20), nullable=False), + sa.Column( + "config", + postgresql.JSONB(astext_type=sa.Text()), + server_default="{}", + nullable=False, + ), + sa.Column("status", sa.String(length=20), nullable=False), + sa.Column("created_by", sa.UUID(), nullable=True), + sa.Column("updated_by", sa.UUID(), nullable=True), + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=False, + ), + sa.Column( + "updated_at", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=False, + ), + sa.Column("deleted_at", sa.DateTime(timezone=True), nullable=True), + sa.PrimaryKeyConstraint("id"), + ) + + op.create_unique_constraint( + "uq_user_agents_user_id_agent_type", + "user_agents", + ["user_id", "agent_type"] + ) + + op.execute( + "CREATE INDEX ix_user_agents_agent_type ON user_agents (agent_type)" + ) + op.execute( + "CREATE INDEX ix_user_agents_status ON user_agents (status)" + ) + + op.execute( + "ALTER TABLE user_agents ADD CONSTRAINT chk_agent_type " + "CHECK (agent_type IN ('INTENT_RECOGNITION', 'TASK_EXECUTION', 'RESULT_REPORTING'))" + ) + + op.create_foreign_key( + "fk_user_agents_user_id", + "user_agents", + "users", + ["user_id"], + ["id"], + referent_schema="auth", + ondelete="CASCADE", + ) + op.create_foreign_key( + "fk_user_agents_llm_id", + "user_agents", + "llms", + ["llm_id"], + ["id"], + ondelete="RESTRICT", + ) + op.create_foreign_key( + "fk_user_agents_created_by", + "user_agents", + "users", + ["created_by"], + ["id"], + referent_schema="auth", + ondelete="SET NULL", + ) + op.create_foreign_key( + "fk_user_agents_updated_by", + "user_agents", + "users", + ["updated_by"], + ["id"], + referent_schema="auth", + ondelete="SET NULL", + ) + + _enable_rls("user_agents") + + # 4. Recreate memories.agent_id + op.add_column( + "memories", + sa.Column("agent_id", sa.UUID(), nullable=True) + ) + + op.create_foreign_key( + "fk_memories_agent_id", + "memories", + "user_agents", + ["agent_id"], + ["id"], + ondelete="CASCADE", + ) + + op.execute( + "CREATE INDEX ix_memories_agent_type_status ON memories (agent_id, memory_type, status)" + ) + + op.execute( + "ALTER TABLE memories ADD CONSTRAINT chk_memory_type_agent_id " + "CHECK ((memory_type = 'work' AND agent_id IS NOT NULL) OR " + "(memory_type = 'user' AND agent_id IS NULL))" + ) +``` + +**Step 4: Add helper functions** + +Add these helper functions at the end of the migration file: + +```python +def _enable_rls(table_name: str) -> None: + for role in ["anon", "authenticated"]: + for action in ["select", "insert", "update", "delete"]: + op.execute( + f"DROP POLICY IF EXISTS {role}_{action}_{table_name} ON {table_name}" + ) + op.execute(f"ALTER TABLE {table_name} ENABLE ROW LEVEL SECURITY") + for role in ["anon", "authenticated"]: + op.execute( + f"CREATE POLICY {role}_select_{table_name} ON {table_name} " + f"FOR SELECT TO {role} USING (false)" + ) + op.execute( + f"CREATE POLICY {role}_insert_{table_name} ON {table_name} " + f"FOR INSERT TO {role} WITH CHECK (false)" + ) + op.execute( + f"CREATE POLICY {role}_update_{table_name} ON {table_name} " + f"FOR UPDATE TO {role} USING (false) WITH CHECK (false)" + ) + op.execute( + f"CREATE POLICY {role}_delete_{table_name} ON {table_name} " + f"FOR DELETE TO {role} USING (false)" + ) + + +def _drop_rls(table_name: str) -> None: + for role in ["anon", "authenticated"]: + op.execute(f"DROP POLICY IF EXISTS {role}_delete_{table_name} ON {table_name}") + op.execute(f"DROP POLICY IF EXISTS {role}_update_{table_name} ON {table_name}") + op.execute(f"DROP POLICY IF EXISTS {role}_insert_{table_name} ON {table_name}") + op.execute(f"DROP POLICY IF EXISTS {role}_select_{table_name} ON {table_name}") + op.execute(f"ALTER TABLE {table_name} DISABLE ROW LEVEL SECURITY") +``` + +**Step 5: Verify migration file** + +Check that all imports are correct: + +```python +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql +``` + +**Step 6: Commit migration** + +```bash +git add backend/alembic/versions/20260304_simplify_agent_architecture.py +git commit -m "feat(db): add migration to simplify agent architecture" +``` + +--- + +## Task 2: Delete UserAgents Model + +**Files:** +- Delete: `backend/src/models/user_agents.py` +- Modify: `backend/src/models/__init__.py` + +**Step 1: Remove import from models/__init__.py** + +Edit `backend/src/models/__init__.py`: + +Remove these lines: +```python +from models.user_agents import UserAgent +``` + +And remove `"UserAgent"` from `__all__` list. + +**Step 2: Delete user_agents.py file** + +```bash +rm backend/src/models/user_agents.py +``` + +**Step 3: Verify no other imports** + +Run: `cd backend && grep -r "from models.user_agents" src/` + +Expected: No results (or only in __init__.py which we already fixed) + +**Step 4: Commit** + +```bash +git add backend/src/models/user_agents.py backend/src/models/__init__.py +git commit -m "refactor(models): remove UserAgents model" +``` + +--- + +## Task 3: Rename UserAgentCatalog to SystemAgents + +**Files:** +- Rename: `backend/src/models/user_agent_catalog.py` → `backend/src/models/system_agents.py` +- Modify: `backend/src/models/__init__.py` + +**Step 1: Rename model file** + +```bash +mv backend/src/models/user_agent_catalog.py backend/src/models/system_agents.py +``` + +**Step 2: Update class name in system_agents.py** + +Edit `backend/src/models/system_agents.py`: + +Change: +```python +class UserAgentCatalog(TimestampMixin, Base): + __tablename__: str = "user_agent_catalog" +``` + +To: +```python +class SystemAgents(TimestampMixin, Base): + __tablename__: str = "system_agents" +``` + +**Step 3: Update imports in models/__init__.py** + +Edit `backend/src/models/__init__.py`: + +Change: +```python +from models.user_agent_catalog import UserAgentCatalog +``` + +To: +```python +from models.system_agents import SystemAgents +``` + +And change `"UserAgentCatalog"` to `"SystemAgents"` in `__all__` list. + +**Step 4: Commit** + +```bash +git add backend/src/models/ +git commit -m "refactor(models): rename UserAgentCatalog to SystemAgents" +``` + +--- + +## Task 4: Update Configuration Files + +**Files:** +- Rename: `backend/src/core/config/static/database/user_agent_catalog.yaml` + → `backend/src/core/config/static/database/system_agents.yaml` +- Modify: `backend/src/core/config/initial/init_data.py` + +**Step 1: Rename YAML file** + +```bash +mv backend/src/core/config/static/database/user_agent_catalog.yaml \ + backend/src/core/config/static/database/system_agents.yaml +``` + +**Step 2: Update init_data.py imports** + +Edit `backend/src/core/config/initial/init_data.py`: + +Change: +```python +from models.user_agent_catalog import UserAgentCatalog +``` + +To: +```python +from models.system_agents import SystemAgents +``` + +**Step 3: Update Pydantic models** + +Change: +```python +class UserAgentCatalogSeed(BaseModel): + agent_type: str + llm_model_code: str + status: str + config: dict[str, Any] + + +class UserAgentCatalogYaml(BaseModel): + agents: list[UserAgentCatalogSeed] +``` + +To: +```python +class SystemAgentsSeed(BaseModel): + agent_type: str + llm_model_code: str + status: str + config: dict[str, Any] + + +class SystemAgentsYaml(BaseModel): + agents: list[SystemAgentsSeed] +``` + +**Step 4: Update path function** + +Change: +```python +def _default_user_agent_catalog_path() -> Path: + return ( + Path(__file__).resolve().parents[1] + / "static" + / "database" + / "user_agent_catalog.yaml" + ) +``` + +To: +```python +def _default_system_agents_path() -> Path: + return ( + Path(__file__).resolve().parents[1] + / "static" + / "database" + / "system_agents.yaml" + ) +``` + +**Step 5: Update load function** + +Change: +```python +def load_user_agent_catalog(catalog_path: Path | None = None) -> dict[str, Any]: + path = catalog_path or _default_user_agent_catalog_path() + with path.open("r", encoding="utf-8") as file: + loaded = yaml.safe_load(file) or {} + if not isinstance(loaded, dict): + raise ValueError(f"Invalid user agent catalog format: {path}") + raw_agents = loaded.get("agents", []) + if not isinstance(raw_agents, list): + raise ValueError(f"Invalid user agent catalog agents section: {path}") + try: + parsed = UserAgentCatalogYaml.model_validate({"agents": list(raw_agents)}) + except ValidationError as exc: + raise ValueError(f"Invalid user agent catalog data: {path}") from exc + + return parsed.model_dump() +``` + +To: +```python +def load_system_agents(catalog_path: Path | None = None) -> dict[str, Any]: + path = catalog_path or _default_system_agents_path() + with path.open("r", encoding="utf-8") as file: + loaded = yaml.safe_load(file) or {} + if not isinstance(loaded, dict): + raise ValueError(f"Invalid system agents format: {path}") + raw_agents = loaded.get("agents", []) + if not isinstance(raw_agents, list): + raise ValueError(f"Invalid system agents agents section: {path}") + try: + parsed = SystemAgentsYaml.model_validate({"agents": list(raw_agents)}) + except ValidationError as exc: + raise ValueError(f"Invalid system agents data: {path}") from exc + + return parsed.model_dump() +``` + +**Step 6: Update upsert function** + +Change: +```python +async def _upsert_user_agent_catalog( + session: AsyncSession, + *, + agent_type: str, + llm_id: uuid.UUID, + status: str, + config: dict[str, Any], +) -> None: + result = await session.execute( + select(UserAgentCatalog).where(UserAgentCatalog.agent_type == agent_type) + ) + catalog_entry = result.scalar_one_or_none() + + if catalog_entry is None: + session.add( + UserAgentCatalog( + agent_type=agent_type, + llm_id=llm_id, + status=status, + config=config, + ) + ) + else: + catalog_entry.llm_id = llm_id + catalog_entry.status = status + catalog_entry.config = config +``` + +To: +```python +async def _upsert_system_agents( + session: AsyncSession, + *, + agent_type: str, + llm_id: uuid.UUID, + status: str, + config: dict[str, Any], +) -> None: + result = await session.execute( + select(SystemAgents).where(SystemAgents.agent_type == agent_type) + ) + catalog_entry = result.scalar_one_or_none() + + if catalog_entry is None: + session.add( + SystemAgents( + agent_type=agent_type, + llm_id=llm_id, + status=status, + config=config, + ) + ) + else: + catalog_entry.llm_id = llm_id + catalog_entry.status = status + catalog_entry.config = config +``` + +**Step 7: Update initialize function** + +Change: +```python +async def initialize_user_agent_catalog() -> None: + """Initialize user agent catalog from YAML.""" + catalog = load_user_agent_catalog() + + async with AsyncSessionLocal() as session: + async with session.begin(): + for agent in catalog["agents"]: + result = await session.execute( + select(Llm).where(Llm.model_code == agent["llm_model_code"]) + ) + llm = result.scalar_one_or_none() + if llm is None: + raise RuntimeError( + f"LLM model '{agent['llm_model_code']}' not found for agent type '{agent['agent_type']}'" + ) + + await _upsert_user_agent_catalog( + session, + agent_type=agent["agent_type"], + llm_id=llm.id, + status=agent["status"], + config=agent["config"], + ) + + logger.info("Initialized user agent catalog") +``` + +To: +```python +async def initialize_system_agents() -> None: + """Initialize system agents from YAML.""" + catalog = load_system_agents() + + async with AsyncSessionLocal() as session: + async with session.begin(): + for agent in catalog["agents"]: + result = await session.execute( + select(Llm).where(Llm.model_code == agent["llm_model_code"]) + ) + llm = result.scalar_one_or_none() + if llm is None: + raise RuntimeError( + f"LLM model '{agent['llm_model_code']}' not found for agent type '{agent['agent_type']}'" + ) + + await _upsert_system_agents( + session, + agent_type=agent["agent_type"], + llm_id=llm.id, + status=agent["status"], + config=agent["config"], + ) + + logger.info("Initialized system agents") +``` + +**Step 8: Update initialize_data function** + +Change: +```python +async def initialize_data() -> bool: + """Initialize bootstrap data.""" + await initialize_llm_catalog() + await initialize_user_agent_catalog() + + return True +``` + +To: +```python +async def initialize_data() -> bool: + """Initialize bootstrap data.""" + await initialize_llm_catalog() + await initialize_system_agents() + + return True +``` + +**Step 9: Commit** + +```bash +git add backend/src/core/config/ +git commit -m "refactor(config): rename user_agent_catalog to system_agents" +``` + +--- + +## Task 5: Run Migration + +**Step 1: Run migration** + +```bash +cd backend && uv run alembic upgrade head +``` + +Expected: Migration runs successfully + +**Step 2: Verify tables** + +Connect to database and check: +- `user_agents` table should NOT exist +- `system_agents` table should exist +- `memories.agent_id` column should NOT exist + +**Step 3: Test downgrade (optional but recommended)** + +```bash +cd backend && uv run alembic downgrade -1 +``` + +Expected: Previous migration restored + +**Step 4: Re-run upgrade** + +```bash +cd backend && uv run alembic upgrade head +``` + +Expected: Migration runs successfully again + +--- + +## Task 6: Run Tests and Linting + +**Step 1: Run type checking** + +```bash +cd backend && uv run basedpyright src/ +``` + +Expected: No errors + +**Step 2: Run linting** + +```bash +cd backend && uv run ruff check src/ +``` + +Expected: No errors + +**Step 3: Run tests** + +```bash +cd backend && uv run pytest tests/ +``` + +Expected: All tests pass + +**Step 4: Fix any failures** + +If any tests fail due to UserAgent references, update them to use SystemAgents. + +--- + +## Task 7: Final Verification + +**Step 1: Search for any remaining references** + +```bash +cd backend && grep -r "user_agents" src/ --include="*.py" +cd backend && grep -r "UserAgent" src/ --include="*.py" +``` + +Expected: No results (except in migration files) + +**Step 2: Test new user registration** + +Start the backend server and register a new user. Verify: +- Profile is created +- No user_agents records are created +- profiles.settings contains `agent_prompts: {}` + +**Step 3: Commit final changes** + +```bash +git add . +git commit -m "feat: complete agent architecture simplification" +``` + +--- + +## Success Criteria + +- [ ] Migration runs successfully (upgrade and downgrade) +- [ ] No UserAgent model references in code +- [ ] SystemAgents model works correctly +- [ ] All tests pass +- [ ] Linting passes +- [ ] Type checking passes +- [ ] New user registration works without user_agents + +## Notes + +- Keep the design document updated if any changes are made during implementation +- Test migration thoroughly before deploying to production +- Backup database before running migration in production