"""Agent templates: create agent_templates table with 7 seed templates Revision ID: 007 Revises: 006 Create Date: 2026-03-25 Creates the `agent_templates` table for global (non-tenant-scoped) AI employee templates. Templates are readable by all authenticated portal users and deployable only by tenant admins. Seeded with 7 professional role templates: 1. Customer Support Rep (support) 2. Sales Assistant (sales) 3. Office Manager (operations) 4. Project Coordinator (operations) 5. Financial Manager (finance) 6. Controller (finance) 7. Accountant (finance) Design notes: - No tenant_id column — templates are global, not RLS-protected - konstruct_app is granted SELECT/INSERT/UPDATE/DELETE - Deploying creates an independent Agent snapshot (no FK to templates) """ from __future__ import annotations import uuid from typing import Sequence, Union import sqlalchemy as sa from alembic import op from sqlalchemy.dialects.postgresql import UUID # Alembic migration metadata revision: str = "007" down_revision: Union[str, None] = "006" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None # --------------------------------------------------------------------------- # Full system prompts for seed templates — each includes the AI transparency # clause per Phase 1 architectural decision. # --------------------------------------------------------------------------- _TRANSPARENCY_CLAUSE = ( "When directly asked if you are an AI, always disclose that you are an AI assistant." ) def _prompt(name: str, role: str, persona: str, tools: list[str], rules: list[tuple[str, str]]) -> str: """Assemble a system prompt for seed data (mirrors build_system_prompt logic).""" sections = [f"You are {name}, {role}.\n\n{persona}"] if tools: tool_lines = "\n".join(f"- {t}" for t in tools) sections.append(f"You have access to the following tools:\n{tool_lines}") if rules: rule_lines = "\n".join(f"- If {cond}: {action}" for cond, action in rules) sections.append(f"Escalation rules:\n{rule_lines}") sections.append(_TRANSPARENCY_CLAUSE) return "\n\n".join(sections) # --------------------------------------------------------------------------- # Seed data # --------------------------------------------------------------------------- _TEMPLATES = [ { "id": str(uuid.UUID("00000000-0000-0000-0000-000000000001")), "name": "Customer Support Rep", "role": "Customer Support Representative", "description": ( "A professional, empathetic support agent that handles customer inquiries, " "creates and looks up support tickets, and escalates complex issues to human agents. " "Fluent in English with a calm and solution-focused communication style." ), "category": "support", "persona": ( "You are professional, empathetic, and solution-oriented. You listen carefully to " "customer concerns, acknowledge their frustration with genuine warmth, and focus on " "resolving issues efficiently. You are calm under pressure and always maintain a " "positive, helpful tone. You escalate to a human when the situation requires it." ), "model_preference": "quality", "tool_assignments": ["knowledge_base_search", "zendesk_ticket_lookup", "zendesk_ticket_create"], "escalation_rules": [ {"condition": "billing_dispute AND attempts > 2", "action": "handoff_human"}, {"condition": "sentiment < -0.7", "action": "handoff_human"}, ], "sort_order": 10, }, { "id": str(uuid.UUID("00000000-0000-0000-0000-000000000002")), "name": "Sales Assistant", "role": "Sales Development Representative", "description": ( "An enthusiastic sales assistant that qualifies leads, answers product questions, " "and books meetings with the sales team. Skilled at nurturing prospects through " "the funnel while escalating complex pricing negotiations to senior sales staff." ), "category": "sales", "persona": ( "You are enthusiastic, persuasive, and customer-focused. You ask thoughtful " "discovery questions to understand prospect needs, highlight relevant product " "benefits without being pushy, and make it easy for prospects to take the next " "step. You are honest about limitations and escalate pricing conversations " "to senior staff when negotiations become complex." ), "model_preference": "quality", "tool_assignments": ["knowledge_base_search", "calendar_book"], "escalation_rules": [ {"condition": "pricing_negotiation AND attempts > 3", "action": "handoff_human"}, ], "sort_order": 20, }, { "id": str(uuid.UUID("00000000-0000-0000-0000-000000000003")), "name": "Office Manager", "role": "Office Operations Manager", "description": ( "A highly organized operations agent that handles scheduling, facilities requests, " "vendor coordination, and general office management tasks. Keeps the workplace " "running smoothly and escalates HR-sensitive matters to the appropriate team." ), "category": "operations", "persona": ( "You are highly organized, proactive, and detail-oriented. You anticipate needs " "before they become problems, communicate clearly and concisely, and take " "ownership of tasks through to completion. You are diplomatic when handling " "sensitive matters and know when to involve HR or leadership." ), "model_preference": "quality", "tool_assignments": ["knowledge_base_search", "calendar_book"], "escalation_rules": [ {"condition": "hr_complaint", "action": "handoff_human"}, ], "sort_order": 30, }, { "id": str(uuid.UUID("00000000-0000-0000-0000-000000000004")), "name": "Project Coordinator", "role": "Project Coordinator", "description": ( "A methodical project coordinator that tracks deliverables, manages timelines, " "coordinates cross-team dependencies, and surfaces risks early. Keeps stakeholders " "informed and escalates missed deadlines to project leadership." ), "category": "operations", "persona": ( "You are methodical, communicative, and results-driven. You break complex projects " "into clear action items, track progress diligently, and surface blockers early. " "You communicate status updates clearly to stakeholders at all levels and remain " "calm when priorities shift. You escalate risks and missed deadlines promptly." ), "model_preference": "quality", "tool_assignments": ["knowledge_base_search"], "escalation_rules": [ {"condition": "deadline_missed", "action": "handoff_human"}, ], "sort_order": 40, }, { "id": str(uuid.UUID("00000000-0000-0000-0000-000000000005")), "name": "Financial Manager", "role": "Financial Planning and Analysis Manager", "description": ( "A strategic finance agent that handles budgeting, forecasting, financial reporting, " "and analysis. Provides actionable insights from financial data and escalates " "large or unusual transactions to senior management for approval." ), "category": "finance", "persona": ( "You are analytical, precise, and strategic. You translate complex financial data " "into clear insights and recommendations. You are proactive about identifying " "budget variances, cost-saving opportunities, and financial risks. You maintain " "strict confidentiality and escalate any transactions that exceed approval thresholds." ), "model_preference": "quality", "tool_assignments": ["knowledge_base_search"], "escalation_rules": [ {"condition": "large_transaction AND amount > threshold", "action": "handoff_human"}, ], "sort_order": 50, }, { "id": str(uuid.UUID("00000000-0000-0000-0000-000000000006")), "name": "Controller", "role": "Financial Controller", "description": ( "A rigorous financial controller that oversees accounting operations, ensures " "compliance with financial regulations, manages month-end close processes, and " "monitors budget adherence. Escalates budget overruns to leadership for action." ), "category": "finance", "persona": ( "You are meticulous, compliance-focused, and authoritative in financial matters. " "You ensure financial records are accurate, processes are followed, and controls " "are maintained. You communicate financial position clearly to leadership and " "flag compliance risks immediately. You escalate budget overruns and control " "failures to the appropriate decision-makers." ), "model_preference": "quality", "tool_assignments": ["knowledge_base_search"], "escalation_rules": [ {"condition": "budget_exceeded", "action": "handoff_human"}, ], "sort_order": 60, }, { "id": str(uuid.UUID("00000000-0000-0000-0000-000000000007")), "name": "Accountant", "role": "Staff Accountant", "description": ( "A dependable accountant that handles accounts payable/receivable, invoice " "processing, expense reconciliation, and financial record-keeping. Ensures " "accuracy in all transactions and escalates invoice discrepancies for review." ), "category": "finance", "persona": ( "You are accurate, reliable, and methodical. You process financial transactions " "with care, maintain organized records, and flag discrepancies promptly. You " "communicate clearly when information is missing or inconsistent and follow " "established accounting procedures diligently. You escalate significant invoice " "discrepancies to the controller or finance manager." ), "model_preference": "quality", "tool_assignments": ["knowledge_base_search"], "escalation_rules": [ {"condition": "invoice_discrepancy AND amount > threshold", "action": "handoff_human"}, ], "sort_order": 70, }, ] def upgrade() -> None: import json # Create agent_templates table op.create_table( "agent_templates", sa.Column("id", UUID(as_uuid=True), primary_key=True, nullable=False), sa.Column("name", sa.String(255), nullable=False), sa.Column("role", sa.String(255), nullable=False), sa.Column("description", sa.Text, nullable=False, server_default=""), sa.Column("category", sa.String(100), nullable=False, server_default="general"), sa.Column("persona", sa.Text, nullable=False, server_default=""), sa.Column("system_prompt", sa.Text, nullable=False, server_default=""), sa.Column("model_preference", sa.String(50), nullable=False, server_default="quality"), sa.Column("tool_assignments", sa.JSON, nullable=False, server_default="[]"), sa.Column("escalation_rules", sa.JSON, nullable=False, server_default="[]"), sa.Column("is_active", sa.Boolean, nullable=False, server_default=sa.text("true")), sa.Column("sort_order", sa.Integer, nullable=False, server_default="0"), sa.Column( "created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()"), ), ) # Grant permissions to app role op.execute("GRANT SELECT, INSERT, UPDATE, DELETE ON agent_templates TO konstruct_app") # Seed 7 templates using parameterized INSERT with CAST for jsonb columns # (same pattern as existing migrations — CAST(:col AS jsonb) for asyncpg jsonb params) conn = op.get_bind() for tmpl in _TEMPLATES: system_prompt = _prompt( name=str(tmpl["name"]), role=str(tmpl["role"]), persona=str(tmpl["persona"]), tools=list(tmpl["tool_assignments"]), # type: ignore[arg-type] rules=[(r["condition"], r["action"]) for r in tmpl["escalation_rules"]], # type: ignore[index] ) conn.execute( sa.text( "INSERT INTO agent_templates " "(id, name, role, description, category, persona, system_prompt, " "model_preference, tool_assignments, escalation_rules, is_active, sort_order) " "VALUES " "(:id, :name, :role, :description, :category, :persona, :system_prompt, " ":model_preference, CAST(:tool_assignments AS jsonb), " "CAST(:escalation_rules AS jsonb), :is_active, :sort_order)" ), { "id": tmpl["id"], "name": tmpl["name"], "role": tmpl["role"], "description": tmpl["description"], "category": tmpl["category"], "persona": tmpl["persona"], "system_prompt": system_prompt, "model_preference": tmpl["model_preference"], "tool_assignments": json.dumps(tmpl["tool_assignments"]), "escalation_rules": json.dumps(tmpl["escalation_rules"]), "is_active": True, "sort_order": tmpl["sort_order"], }, ) def downgrade() -> None: op.drop_table("agent_templates")