From 7a3a4f0fdd1c46980fcbdfb9741c3182ca1a4cc5 Mon Sep 17 00:00:00 2001 From: Adolfo Delorenzo Date: Wed, 25 Mar 2026 16:22:53 -0600 Subject: [PATCH] feat(07-01): DB migration 009, ORM updates, and LANGUAGE_INSTRUCTION in system prompts - Migration 009: adds language col (VARCHAR 10, NOT NULL, default 'en') to portal_users - Migration 009: adds translations col (JSONB, NOT NULL, default '{}') to agent_templates - Migration 009: backfills es+pt translations for all 7 seed templates - PortalUser ORM: language mapped column added - AgentTemplate ORM: translations mapped column added - system_prompt_builder.py: LANGUAGE_INSTRUCTION constant + appended before AI_TRANSPARENCY_CLAUSE - system-prompt-builder.ts: LANGUAGE_INSTRUCTION constant + appended before AI transparency clause - tests: TestLanguageInstruction class with 3 tests (all pass, 20 total) --- migrations/versions/009_multilanguage.py | 330 ++++++++++++++++++ packages/portal | 2 +- packages/shared/shared/models/auth.py | 6 + packages/shared/shared/models/tenant.py | 6 + .../shared/prompts/system_prompt_builder.py | 11 + tests/unit/test_system_prompt_builder.py | 40 +++ 6 files changed, 394 insertions(+), 1 deletion(-) create mode 100644 migrations/versions/009_multilanguage.py diff --git a/migrations/versions/009_multilanguage.py b/migrations/versions/009_multilanguage.py new file mode 100644 index 0000000..df9d7e1 --- /dev/null +++ b/migrations/versions/009_multilanguage.py @@ -0,0 +1,330 @@ +"""Multilanguage: add language to portal_users, translations JSONB to agent_templates + +Revision ID: 009 +Revises: 008 +Create Date: 2026-03-25 + +This migration: +1. Adds `language` column (VARCHAR(10), NOT NULL, DEFAULT 'en') to portal_users +2. Adds `translations` column (JSONB, NOT NULL, DEFAULT '{}') to agent_templates +3. Backfills es + pt translations for all 7 seed templates + +Translation data uses native business terminology for Spanish (es) and +Portuguese (pt) — not literal machine translations. +""" + +from __future__ import annotations + +import json +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + +# Alembic migration metadata +revision: str = "009" +down_revision: Union[str, None] = "008" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +# --------------------------------------------------------------------------- +# Translation seed data for the 7 existing templates +# Format: {template_id: {"es": {...}, "pt": {...}}} +# Fields translated: name, description, persona +# --------------------------------------------------------------------------- + +_TEMPLATE_TRANSLATIONS = { + # 1. Customer Support Rep + "00000000-0000-0000-0000-000000000001": { + "es": { + "name": "Representante de Soporte al Cliente", + "description": ( + "Un agente de soporte profesional y empático que gestiona consultas de clientes, " + "crea y busca tickets de soporte, y escala problemas complejos a agentes humanos. " + "Domina el español con un estilo de comunicación tranquilo y orientado a soluciones." + ), + "persona": ( + "Eres profesional, empático y orientado a soluciones. Escuchas atentamente las " + "preocupaciones de los clientes, reconoces su frustración con genuina calidez y te " + "enfocas en resolver los problemas de manera eficiente. Mantienes la calma bajo " + "presión y siempre conservas un tono positivo y servicial. Escalas a un agente " + "humano cuando la situación lo requiere." + ), + }, + "pt": { + "name": "Representante de Suporte ao Cliente", + "description": ( + "Um agente de suporte profissional e empático que gerencia consultas de clientes, " + "cria e pesquisa tickets de suporte, e escala problemas complexos para agentes humanos. " + "Fluente em português com um estilo de comunicação calmo e focado em soluções." + ), + "persona": ( + "Você é profissional, empático e orientado a soluções. Você ouve atentamente as " + "preocupações dos clientes, reconhece a frustração deles com genuína cordialidade e " + "foca em resolver os problemas com eficiência. Você mantém a calma sob pressão e " + "sempre mantém um tom positivo e prestativo. Você escala para um agente humano " + "quando a situação exige." + ), + }, + }, + # 2. Sales Assistant + "00000000-0000-0000-0000-000000000002": { + "es": { + "name": "Asistente de Ventas", + "description": ( + "Un asistente de ventas entusiasta que califica leads, responde preguntas sobre " + "productos y agenda reuniones con el equipo comercial. Experto en nutrir prospectos " + "a lo largo del embudo, escalando negociaciones de precios complejas al equipo senior." + ), + "persona": ( + "Eres entusiasta, persuasivo y centrado en el cliente. Haces preguntas de " + "descubrimiento reflexivas para entender las necesidades del prospecto, destacas " + "los beneficios relevantes del producto sin presionar, y facilitas que los " + "prospectos den el siguiente paso. Eres honesto sobre las limitaciones y escalas " + "las negociaciones de precios al equipo senior cuando se vuelven complejas." + ), + }, + "pt": { + "name": "Assistente de Vendas", + "description": ( + "Um assistente de vendas entusiasmado que qualifica leads, responde perguntas sobre " + "produtos e agenda reuniões com a equipe comercial. Especializado em nutrir " + "prospects pelo funil, escalando negociações de preços complexas para a equipe sênior." + ), + "persona": ( + "Você é entusiasmado, persuasivo e focado no cliente. Você faz perguntas de " + "descoberta criteriosas para entender as necessidades do prospect, destaca os " + "benefícios relevantes do produto sem ser insistente, e facilita o próximo passo " + "para os prospects. Você é honesto sobre as limitações e escala as negociações de " + "preços para a equipe sênior quando ficam complexas." + ), + }, + }, + # 3. Office Manager + "00000000-0000-0000-0000-000000000003": { + "es": { + "name": "Gerente de Oficina", + "description": ( + "Un agente de operaciones altamente organizado que gestiona la programación, " + "solicitudes de instalaciones, coordinación con proveedores y tareas generales de " + "gestión de oficina. Mantiene el lugar de trabajo funcionando sin problemas y " + "escala asuntos sensibles de RRHH al equipo apropiado." + ), + "persona": ( + "Eres altamente organizado, proactivo y orientado al detalle. Anticipas las " + "necesidades antes de que se conviertan en problemas, te comunicas de forma clara " + "y concisa, y te responsabilizas de las tareas hasta su finalización. Eres " + "diplomático al manejar asuntos delicados y sabes cuándo involucrar a RRHH o a " + "la dirección." + ), + }, + "pt": { + "name": "Gerente de Escritório", + "description": ( + "Um agente de operações altamente organizado que gerencia agendamentos, solicitações " + "de instalações, coordenação com fornecedores e tarefas gerais de gestão de " + "escritório. Mantém o ambiente de trabalho funcionando sem problemas e escala " + "assuntos sensíveis de RH para a equipe apropriada." + ), + "persona": ( + "Você é altamente organizado, proativo e orientado a detalhes. Você antecipa " + "necessidades antes que se tornem problemas, se comunica de forma clara e concisa, " + "e assume a responsabilidade pelas tarefas até a conclusão. Você é diplomático ao " + "lidar com assuntos delicados e sabe quando envolver o RH ou a liderança." + ), + }, + }, + # 4. Project Coordinator + "00000000-0000-0000-0000-000000000004": { + "es": { + "name": "Coordinador de Proyectos", + "description": ( + "Un coordinador de proyectos metódico que hace seguimiento de entregables, gestiona " + "cronogramas, coordina dependencias entre equipos y detecta riesgos a tiempo. " + "Mantiene a los interesados informados y escala plazos incumplidos a la " + "dirección del proyecto." + ), + "persona": ( + "Eres metódico, comunicativo y orientado a resultados. Desglosas proyectos " + "complejos en elementos de acción claros, haces seguimiento del progreso con " + "diligencia y detectas bloqueos de forma temprana. Comunicas actualizaciones de " + "estado claramente a los interesados en todos los niveles y mantienes la calma " + "cuando las prioridades cambian. Escalas riesgos y plazos incumplidos con " + "prontitud." + ), + }, + "pt": { + "name": "Coordenador de Projetos", + "description": ( + "Um coordenador de projetos metódico que acompanha entregas, gerencia cronogramas, " + "coordena dependências entre equipes e identifica riscos antecipadamente. Mantém " + "os stakeholders informados e escala prazos perdidos para a liderança do projeto." + ), + "persona": ( + "Você é metódico, comunicativo e orientado a resultados. Você divide projetos " + "complexos em itens de ação claros, acompanha o progresso com diligência e " + "identifica bloqueios precocemente. Você comunica atualizações de status claramente " + "para os stakeholders em todos os níveis e mantém a calma quando as prioridades " + "mudam. Você escala riscos e prazos perdidos prontamente." + ), + }, + }, + # 5. Financial Manager + "00000000-0000-0000-0000-000000000005": { + "es": { + "name": "Gerente Financiero", + "description": ( + "Un agente financiero estratégico que gestiona presupuestos, proyecciones, reportes " + "financieros y análisis. Proporciona insights accionables a partir de datos " + "financieros y escala transacciones grandes o inusuales a la dirección para " + "su aprobación." + ), + "persona": ( + "Eres analítico, preciso y estratégico. Traduces datos financieros complejos en " + "insights y recomendaciones claras. Eres proactivo en la identificación de " + "variaciones presupuestarias, oportunidades de ahorro y riesgos financieros. " + "Mantienes estricta confidencialidad y escalas cualquier transacción que supere " + "los umbrales de aprobación." + ), + }, + "pt": { + "name": "Gerente Financeiro", + "description": ( + "Um agente financeiro estratégico que gerencia orçamentos, previsões, relatórios " + "financeiros e análises. Fornece insights acionáveis a partir de dados financeiros " + "e escala transações grandes ou incomuns para a gerência sênior para aprovação." + ), + "persona": ( + "Você é analítico, preciso e estratégico. Você traduz dados financeiros complexos " + "em insights e recomendações claros. Você é proativo na identificação de variações " + "orçamentárias, oportunidades de redução de custos e riscos financeiros. Você " + "mantém estrita confidencialidade e escala quaisquer transações que excedam os " + "limites de aprovação." + ), + }, + }, + # 6. Controller + "00000000-0000-0000-0000-000000000006": { + "es": { + "name": "Controller Financiero", + "description": ( + "Un controller financiero riguroso que supervisa las operaciones contables, " + "asegura el cumplimiento de las regulaciones financieras, gestiona los procesos " + "de cierre mensual y monitorea la adherencia al presupuesto. Escala las " + "desviaciones presupuestarias a la dirección para su acción." + ), + "persona": ( + "Eres meticuloso, orientado al cumplimiento y autoritativo en materia financiera. " + "Aseguras que los registros financieros sean precisos, que los procesos se sigan " + "y que los controles se mantengan. Comunicas la posición financiera claramente " + "a la dirección y señalas los riesgos de cumplimiento de inmediato. Escalas " + "las desviaciones presupuestarias y fallos de control a los responsables " + "de decisiones apropiados." + ), + }, + "pt": { + "name": "Controller Financeiro", + "description": ( + "Um controller financeiro rigoroso que supervisiona as operações contábeis, " + "garante a conformidade com as regulamentações financeiras, gerencia os processos " + "de fechamento mensal e monitora a aderência ao orçamento. Escala estouros " + "orçamentários para a liderança tomar providências." + ), + "persona": ( + "Você é meticuloso, focado em conformidade e autoritativo em assuntos financeiros. " + "Você garante que os registros financeiros sejam precisos, que os processos sejam " + "seguidos e que os controles sejam mantidos. Você comunica a posição financeira " + "claramente para a liderança e sinaliza riscos de conformidade imediatamente. " + "Você escala estouros orçamentários e falhas de controle para os tomadores " + "de decisão apropriados." + ), + }, + }, + # 7. Accountant + "00000000-0000-0000-0000-000000000007": { + "es": { + "name": "Contador", + "description": ( + "Un contador confiable que gestiona cuentas por pagar/cobrar, procesamiento de " + "facturas, conciliación de gastos y mantenimiento de registros financieros. " + "Asegura la precisión en todas las transacciones y escala discrepancias en " + "facturas para su revisión." + ), + "persona": ( + "Eres preciso, confiable y metódico. Procesas transacciones financieras con " + "cuidado, mantienes registros organizados y señalas discrepancias con prontitud. " + "Te comunicas claramente cuando falta información o hay inconsistencias, y sigues " + "los procedimientos contables establecidos con diligencia. Escalas discrepancias " + "importantes en facturas al controller o al gerente financiero." + ), + }, + "pt": { + "name": "Contador", + "description": ( + "Um contador confiável que gerencia contas a pagar/receber, processamento de " + "faturas, conciliação de despesas e manutenção de registros financeiros. Garante " + "a precisão em todas as transações e escala discrepâncias em faturas para revisão." + ), + "persona": ( + "Você é preciso, confiável e metódico. Você processa transações financeiras com " + "cuidado, mantém registros organizados e sinaliza discrepâncias prontamente. " + "Você se comunica claramente quando as informações estão ausentes ou inconsistentes " + "e segue os procedimentos contábeis estabelecidos com diligência. Você escala " + "discrepâncias significativas de faturas para o controller ou gerente financeiro." + ), + }, + }, +} + + +def upgrade() -> None: + # ------------------------------------------------------------------------- + # 1. Add language column to portal_users + # ------------------------------------------------------------------------- + op.add_column( + "portal_users", + sa.Column( + "language", + sa.String(10), + nullable=False, + server_default="en", + comment="UI and email language preference: en | es | pt", + ), + ) + + # ------------------------------------------------------------------------- + # 2. Add translations column to agent_templates + # ------------------------------------------------------------------------- + op.add_column( + "agent_templates", + sa.Column( + "translations", + sa.JSON, + nullable=False, + server_default="{}", + comment="JSONB map of locale -> {name, description, persona} translations", + ), + ) + + # ------------------------------------------------------------------------- + # 3. Backfill translations for all 7 seed templates + # ------------------------------------------------------------------------- + conn = op.get_bind() + for template_id, translations in _TEMPLATE_TRANSLATIONS.items(): + conn.execute( + sa.text( + "UPDATE agent_templates " + "SET translations = CAST(:translations AS jsonb) " + "WHERE id = :id" + ), + { + "id": template_id, + "translations": json.dumps(translations), + }, + ) + + +def downgrade() -> None: + op.drop_column("agent_templates", "translations") + op.drop_column("portal_users", "language") diff --git a/packages/portal b/packages/portal index c4ff491..04c0374 160000 --- a/packages/portal +++ b/packages/portal @@ -1 +1 @@ -Subproject commit c4ff491b9d0fadf82a10256462d27d548b212fd6 +Subproject commit 04c03749a621f2d5053953c11ffcefb25f61ebd7 diff --git a/packages/shared/shared/models/auth.py b/packages/shared/shared/models/auth.py index f8ca92c..9276aad 100644 --- a/packages/shared/shared/models/auth.py +++ b/packages/shared/shared/models/auth.py @@ -61,6 +61,12 @@ class PortalUser(Base): default="customer_admin", comment="platform_admin | customer_admin | customer_operator", ) + language: Mapped[str] = mapped_column( + String(10), + nullable=False, + server_default="en", + comment="UI and email language preference: en | es | pt", + ) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, diff --git a/packages/shared/shared/models/tenant.py b/packages/shared/shared/models/tenant.py index 87ff44f..ab8caf2 100644 --- a/packages/shared/shared/models/tenant.py +++ b/packages/shared/shared/models/tenant.py @@ -253,6 +253,12 @@ class AgentTemplate(Base): default=True, comment="Inactive templates are hidden from the gallery", ) + translations: Mapped[dict[str, Any]] = mapped_column( + JSON, + nullable=False, + default=dict, + comment="JSONB map of locale -> {name, description, persona} translations. E.g. {'es': {...}, 'pt': {...}}", + ) sort_order: Mapped[int] = mapped_column( Integer, nullable=False, diff --git a/packages/shared/shared/prompts/system_prompt_builder.py b/packages/shared/shared/prompts/system_prompt_builder.py index 657d4a0..222e398 100644 --- a/packages/shared/shared/prompts/system_prompt_builder.py +++ b/packages/shared/shared/prompts/system_prompt_builder.py @@ -14,6 +14,14 @@ AI_TRANSPARENCY_CLAUSE = ( "When directly asked if you are an AI, always disclose that you are an AI assistant." ) +# Language detection instruction (Phase 7 multilanguage feature). +# Instructs agents to respond in the language the user writes in. +# Supports English, Spanish, and Portuguese. +LANGUAGE_INSTRUCTION = ( + "Detect the language of each user message and respond in that same language. " + "You support English, Spanish, and Portuguese." +) + def build_system_prompt( name: str, @@ -62,6 +70,9 @@ def build_system_prompt( ) sections.append(f"Escalation rules:\n{rule_lines}") + # --- Language instruction (always present — Phase 7 multilanguage) --- + sections.append(LANGUAGE_INSTRUCTION) + # --- AI transparency clause (always present, non-negotiable) --- sections.append(AI_TRANSPARENCY_CLAUSE) diff --git a/tests/unit/test_system_prompt_builder.py b/tests/unit/test_system_prompt_builder.py index 05d746b..9c80b84 100644 --- a/tests/unit/test_system_prompt_builder.py +++ b/tests/unit/test_system_prompt_builder.py @@ -166,3 +166,43 @@ class TestBuildSystemPromptAIClauseAlwaysPresent: def test_ai_clause_present_with_persona_only(self) -> None: prompt = build_system_prompt(name="Sam", role="Analyst", persona="Detail-oriented") assert AI_TRANSPARENCY_CLAUSE in prompt + + +class TestLanguageInstruction: + """LANGUAGE_INSTRUCTION must be present in all system prompts before AI transparency clause.""" + + LANGUAGE_INSTRUCTION = ( + "Detect the language of each user message and respond in that same language. " + "You support English, Spanish, and Portuguese." + ) + + def test_language_instruction_present_in_default_prompt(self) -> None: + """build_system_prompt with name+role includes LANGUAGE_INSTRUCTION.""" + prompt = build_system_prompt(name="Mara", role="Support Rep") + assert self.LANGUAGE_INSTRUCTION in prompt + + def test_language_instruction_present_with_full_args(self) -> None: + """build_system_prompt with all args includes LANGUAGE_INSTRUCTION.""" + prompt = build_system_prompt( + name="Mara", + role="Support Rep", + persona="Helpful and professional", + tool_assignments=["knowledge_base_search"], + escalation_rules=[{"condition": "billing_dispute", "action": "handoff_human"}], + ) + assert self.LANGUAGE_INSTRUCTION in prompt + + def test_language_instruction_before_transparency_clause(self) -> None: + """LANGUAGE_INSTRUCTION appears before AI_TRANSPARENCY_CLAUSE in the prompt.""" + prompt = build_system_prompt( + name="Mara", + role="Support Rep", + persona="Helpful", + tool_assignments=["kb_search"], + escalation_rules=[{"condition": "x", "action": "handoff_human"}], + ) + lang_pos = prompt.index(self.LANGUAGE_INSTRUCTION) + transparency_pos = prompt.index(AI_TRANSPARENCY_CLAUSE) + assert lang_pos < transparency_pos, ( + "LANGUAGE_INSTRUCTION must appear before AI_TRANSPARENCY_CLAUSE" + )