- Add AgentTemplate ORM model to tenant.py (global, not tenant-scoped) - Create migration 007 with agent_templates table and 7 seed templates - Create shared/prompts/system_prompt_builder.py with build_system_prompt() - AI transparency clause always present (non-negotiable per Phase 1 decision) - Unit tests pass (17 tests, all sections verified)
307 lines
14 KiB
Python
307 lines
14 KiB
Python
"""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")
|