Spec del guion híbrido (slots fijos + bloque abierto) y de la capa que clasifica/abstrae el texto de gustos en inputs del presupuesto, con cuatro palancas (material, extras, render, ajustes etiquetados) y clasificador keyless. Enfoque A: pre+post alrededor de computeBudget, motor intacto. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
8.5 KiB
Guion del agente de voz + capa de preferencias — Diseño
Fecha: 2026-05-31
Estado: aprobado (brainstorming), pendiente de plan de implementación
Superficie: funnel B2C (mvp/b2c), bloque de llamada del agente (RF-C) + panel (RF-D)
Objetivo
Plantear el guion del agente de voz para que recoja datos adicionales sobre los gustos del cliente, y una capa que clasifique y abstraiga ese texto libre en inputs que el motor de presupuesto pueda usar — mejorando la precisión y el acercamiento estético del presupuesto sin romper la trazabilidad partida-a-partida del motor.
Decisiones de alcance (validadas)
- Cuatro palancas por las que los gustos afectan al presupuesto:
- Selección de material + calidad por categoría (vía catálogo).
- Detección de elementos/extras → partidas nuevas.
- Descriptores de render.
- Modificadores de € etiquetados y trazables (nunca % opaco).
- Estilo de guion: híbrido — slots fijos + bloque abierto de gustos.
- Capa de clasificación: clasificador keyless funcional + esquema durable, con costura para enchufar GPT-4o en F2.
- Arquitectura: Enfoque A — pre + post alrededor de
computeBudget; el motor queda intacto.
Arquitectura
RawCallData
→ abstractPreferences(raw, catalog) → AbstractedPreferences
→ mergeIntoBudgetInputs(prefs, lead) → BudgetInputs [pre]
→ computeBudget(inputs, config, catalog) → BudgetResult [motor intacto]
→ applyPreferences(result, prefs) → BudgetResult final [post: elementos + ajustes]
El motor de presupuesto (src/budget) no se modifica. La extracción y la aplicación de extras viven en un módulo nuevo src/lib/voice, con interfaces bien definidas.
Componentes
1. Contrato de datos — src/lib/voice/preferences.ts
import type { Calidad, CategoriaMaterial, TipoReforma } from '@/budget/types';
export interface RawCallData {
tipoReforma: TipoReforma;
m2Suelo: number | null;
calidad: Calidad | null;
estructural: boolean | null;
urgencia: 'alta' | 'media' | 'baja' | null;
presupuestoTarget: number | null; // céntimos
tasteText: string; // bloque abierto del guion
}
export interface PreferenceExtra { // palanca 2 → se convierte en partida
key: string; // 'isla_cocina'
label: string; // 'Isla de cocina'
importe: number; // céntimos (base, antes de factor zona)
}
export interface PreferenceAjuste { // palanca 4 → ajuste etiquetado y trazable
label: string;
tipo: 'factor' | 'fijo';
valor: number; // factor (1.15) o céntimos
motivo: string; // visible en el panel
}
export interface AbstractedPreferences {
calidadGlobal: Calidad;
materialSelections: Partial<Record<CategoriaMaterial, string>>; // catalogItemId
estructural: boolean;
urgencia: 'alta' | 'media' | 'baja' | null;
presupuestoTarget: number | null;
elementos: PreferenceExtra[];
estiloRender: string[];
ajustes: PreferenceAjuste[];
confianza: 'baja' | 'media' | 'alta';
resumen: string; // 1-2 frases para el panel
camposFaltantes: string[]; // RF-C-15 completeness
}
Notas:
- No hay "calidad por categoría" separada: es expresable vía
materialSelections(cadaCatalogItemlleva sucalidad). - Los modificadores (palanca 4) son siempre
PreferenceAjusteconlabel+motivo. - Los
elementosse inyectan como partidas trascomputeBudget, reusando quePartidaResult.keyadmite string libre.
2. Guion — src/lib/voice/script.ts
buildScript(reformistaConfig, lead) → estructura de bloques/turnos (RF-C-14, generado desde la config del reformista).
- Bloque 0 — Preámbulo legal (literal, obligatorio): identificación IA + aviso de grabación (RF-C-12, RNF-LEG-05). Si responde "no" → cierre educado +
consent_revoked(RF-C-13). - Bloque 1 — Confirmación: tipo y m² del form.
- Bloque 2 — Slots fijos (uno a uno, opciones acotadas): calidad (básica/media/premium), estructural (sí/no), urgencia (alta/media/baja), presupuesto target (opcional).
- Bloque 3 — Bloque abierto de gustos: pregunta ancla "Cuéntame cómo te lo imaginas: estilo, colores, materiales… y si hay algún capricho que no quieras que falte." + hasta 2 repreguntas guiadas (materiales, elemento estrella). Todo alimenta
tasteText. - Bloque 4 — Cierre + completeness (RF-C-15/16): repregunta dirigida si faltan campos críticos; tope 8 min → info parcial; despedida.
Hoy el orquestador usa la estructura para construir una transcripción realista; en F2 se traduce al prompt de Retell sin redeploy.
3. Clasificador — src/lib/voice/extractor.ts + src/lib/voice/lexicon.ts
export interface PreferenceExtractor {
extract(raw: RawCallData, catalog: CatalogItem[]): AbstractedPreferences;
}
// hoy: DeterministicExtractor | F2: GPT4oExtractor (misma interfaz)
DeterministicExtractor (keyless):
- Normaliza
tasteText(minúsculas, sin tildes) y lo trocea en señales. - Calidad:
CALIDAD_LEXICON(premium/básica/media). Gana el slot explícito si existe. - Materiales por categoría: matchea keywords contra
nombre+descriptorRenderde losCatalogItemdel tenant; elige item de la calidad detectada; devuelvecatalogItemId. - Elementos/extras:
ELEMENTOS_LEXICONpor tipo de reforma →PreferenceExtracon importe base versionado en la tabla (no inventado en runtime). - Estructural:
ESTRUCTURAL_LEXICON(tirar muro, mover el baño, abrir la cocina) → refuerza el slot. - Estilo render:
ESTILO_LEXICON→estiloRender[], que se concatena conmaterialesRenderdel motor. - Ajustes etiquetados: reglas explícitas (encimera de mármol/cuarzo →
PreferenceAjustecon motivo). - Confianza + camposFaltantes: alta si slots críticos + señales claras; baja si texto pobre. Lista campos RF-C-15 ausentes.
Léxicos en español, en lexicon.ts, ampliables y unit-testeables.
4. Aplicación — src/lib/voice/apply.ts
mergeIntoBudgetInputs(prefs, lead): BudgetInputs— vuelca calidad,materialSelections,estructural, provincia/m² del lead.applyPreferences(result, prefs): BudgetResult— añadeprefs.elementoscomo partidas, aplicaprefs.ajustes(factor o fijo) recalculandosubtotal/total/rango, y concatenaprefs.estiloRenderamaterialesRender. Cada extra/ajuste queda como partida o aviso con label visible.
Integración en el funnel
FotosUploader.tsx: + select urgencia, + checkbox estructural, + input target, + textarea "Cuéntanos cómo lo imaginas" (stand-in keyless del bloque 3; cierra hueco RF-C-15).actions.ts(guardarDetallesYFotos): persiste los campos nuevos en el lead.orchestrator.ts: construyeRawCallDatadesde el lead →abstractPreferences→mergeIntoBudgetInputs→computeBudget→applyPreferences. La transcripción simulada pasa a generarse desdebuildScript+ respuestas reales.schema.ts: campos nuevos enleads(urgencia,presupuestoTarget,tasteText) + snapshot deAbstractedPreferences.- Panel detalle (
panel/[id]/page.tsx): sección "Preferencias detectadas" conresumen,elementos,ajustes(con motivo) yestiloRender. El reformista edita extras vía elConceptosEditorya existente.
Manejo de errores
tasteTextvacío →AbstractedPreferencesconconfianza: 'baja', sin elementos/ajustes,materialSelectionspor defecto; el presupuesto sigue calculándose con los slots.- Sin match de material en catálogo → se omite esa categoría (cae al default del motor), no se inventa item.
- Ajuste sin importe resoluble → se descarta y se anota en
camposFaltantes.
Testing (TDD)
extractor.test.ts: textos de gusto conocidos →AbstractedPreferencesesperadas (calidad, materiales, elementos, estilo, estructural, confianza).apply.test.ts:BudgetResult+ prefs → partidas extra correctas y total con ajustes (±1 €).script.test.ts:buildScriptincluye el preámbulo legal literal y todos los bloques.
Compliance
RF-C-12 (aviso de grabación), RNF-LEG-05 (identificación IA), RF-C-13 (revoca consentimiento), RF-C-16 (tope 8 min), RF-C-15 (7 campos obligatorios).
Fuera de alcance
- Llamada real (Retell), telefonía (Zadarma), transcripción/Vision real (GPT-4o): F2 con claves. El
GPT4oExtractorse deja como costura, no se implementa. - 3 versiones B/M/P del presupuesto y refinamiento por lenguaje natural post-envío: F1.5.