#!/usr/bin/env python3 """Seed the knowledge base with SKEEN catalog and FAQ. Usage: python scripts/seed_knowledge.py """ import asyncio import sys import os sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from sqlalchemy.ext.asyncio import AsyncSession from src.infrastructure.db import AsyncSessionLocal, init_db from src.infrastructure.ai.rag import RAGStore, CREATE_KNOWLEDGE_TABLE_SQL from src.infrastructure.ai.openai_client import get_openai_client # SKEEN Catalog & FAQ Knowledge Base SKEEN_KNOWLEDGE = [ # --- SERVICIOS --- { "content": ( "Consulta Dermatológica Primera Vez — Precio: $1,500 MXN. " "Duración: 45 minutos. Incluye evaluación completa de piel, diagnóstico " "personalizado y propuesta de tratamiento. Requerido para todos los pacientes " "nuevos antes de cualquier procedimiento estético. Disponible con Dr. Ramos y Dr. Martínez." ), "category": "servicio", "source": "catalogo_servicios", }, { "content": ( "Consulta Dermatológica Subsecuente — Precio: $1,400 MXN. " "Duración: 30 minutos. Seguimiento de tratamientos en curso, ajuste de recetas " "y evaluación de resultados. Recomendada cada 4-6 semanas dependiendo del tratamiento." ), "category": "servicio", "source": "catalogo_servicios", }, { "content": ( "QUANTA EFELIDES (Láser Q-Switched) — Precio: $3,500 MXN por sesión. " "Duración: 60 minutos. Tratamiento láser para manchas solares, lentigos solares " "y lesiones pigmentadas. Requiere 3-5 sesiones. Contraindicado en pieles muy bronceadas. " "Disponible solo en sucursal Rosarito con Dr. Ramos." ), "category": "servicio", "source": "catalogo_servicios", }, { "content": ( "Depilación Láser IPL Bikini Brasileño — Precio: $1,200 MXN por sesión. " "Duración: 30 minutos. Tecnología IPL (Intense Pulsed Light) para reducción " "permanente de vello. Paquete de 6 sesiones con 15% de descuento ($6,120 MXN). " "Requiere evaluación previa. No apto para pieles fototipos V-VI." ), "category": "servicio", "source": "catalogo_servicios", }, { "content": ( "Toxina Botulínica DYSPORT — Precio: $2,800 MXN (área única: entrecejo, frente o patas de gallo). " "Duración: 30 minutos. Efecto visible a los 3-5 días, duración de 4-6 meses. " "Incluye valoración previa. Requiere firma de consentimiento informado. " "Dr. Ramos y Dr. Martínez disponibles." ), "category": "servicio", "source": "catalogo_servicios", }, { "content": ( "Retiro de Verrugas (Crioterapia / Electrocauterio) — Precio: $800 MXN (hasta 5 lesiones). " "Duración: 20-30 minutos. Método seguro y rápido para remover verrugas, " "lentigos seborreicos y acrocordones. No requiere tiempo de recuperación significativo. " "Si se requieren más de 5 lesiones, cotizar adicional." ), "category": "servicio", "source": "catalogo_servicios", }, { "content": ( "Ácido Hialurónico (Relleno Facial) — Precio desde $4,500 MXN por jeringa (1ml). " "Duración: 45 minutos. Restauración de volumen en pómulos, surcos nasogenianos, " "labios y mentón. Resultados inmediatos, duración 12-18 meses. Marca: Juvederm o Restylane. " "Incluye anestesia tópica. Solo con cita previa." ), "category": "servicio", "source": "catalogo_servicios", }, { "content": ( "Hydrafacial Deluxe — Precio: $1,800 MXN. Duración: 60 minutos. Limpieza profunda, " "exfoliación, extracción e hidratación en 3 pasos. Incluye serum antioxidante y péptidos. " "Recomendado mensual para mantenimiento de piel. Sin tiempo de recuperación." ), "category": "servicio", "source": "catalogo_servicios", }, # --- PRODUCTOS --- { "content": ( "Crema Hidratante SKEEN — Precio: $450 MXN. Presentación: 50ml. " "Hidratante facial con ácido hialurónico y niacinamida. Para todo tipo de piel. " "Uso diario mañana y noche. SKU: CH-001. Stock disponible." ), "category": "producto", "source": "catalogo_productos", }, { "content": ( "Serum Vitamina C SKEEN — Precio: $680 MXN. Presentación: 30ml. " "Concentración 15% de vitamina C estabilizada + vitamina E + ácido ferúlico. " "Antioxidante potente, unifica tono y reduce manchas. Uso matutino con protector solar. " "SKU: SVC-002. Stock disponible." ), "category": "producto", "source": "catalogo_productos", }, { "content": ( "Protector Solar SPF 50 SKEEN — Precio: $520 MXN. Presentación: 60ml. " "Filtro solar físico-químico, resistente al agua, no comedogénico. " "Acabado mate, ideal para uso diario y post-procedimientos. SKU: PS50-003. Stock disponible." ), "category": "producto", "source": "catalogo_productos", }, # --- PAQUETES --- { "content": ( "Paquete Depilación Láser IPL Completo — Precio: $18,000 MXN (12 sesiones). " "Incluye: axilas, bikini brasileño y medias piernas. Ahorro de $3,600 vs. precio individual. " "Vigencia: 18 meses desde primera sesión. No incluye consulta inicial (se cotiza separado)." ), "category": "paquete", "source": "catalogo_paquetes", }, { "content": ( "Paquete Rejuvenecimiento Facial — Precio: $12,500 MXN. Incluye: 3 Hydrafacial + " "1 sesión de Toxina Botulínica (área única) + Kit de skincare básico (Crema + Serum + SPF). " "Ahorro de $2,400. Vigencia: 12 meses. Ideal para mantenimiento antiedad." ), "category": "paquete", "source": "catalogo_paquetes", }, # --- FAQ --- { "content": ( "¿Cómo agendo una cita? Puedes agendar respondiendo a este chat con la fecha y hora " "que prefieras, o llamando al (664) 123-4567. También puedes visitarnos directamente. " "Horario: Lunes a Sábado 9:00-18:00, Domingos 10:00-14:00." ), "category": "faq", "source": "faq_general", }, { "content": ( "¿Qué métodos de pago aceptan? Efectivo (MXN y USD), tarjetas de crédito/débito, " "transferencias bancarias y pago con monedero electrónico SKEEN. " "No aceptamos cheques. Pagos en USD aplican tipo de cambio del día." ), "category": "faq", "source": "faq_general", }, { "content": ( "¿Cuál es la política de cancelación? Debes cancelar o reagendar con mínimo 24 horas " "de anticipación. Cancelaciones tardías o no-show pueden generar un cargo del 30% " "del valor de la consulta/procedimiento. Puedes cancelar por WhatsApp o teléfono." ), "category": "faq", "source": "faq_general", }, { "content": ( "¿Dónde están ubicados? Sucursal Rosarito: Blvd. Benito Juárez #1234, Zona Centro. " "Sucursal Tijuana: Av. Revolución #567, Zona Río. Ambas cuentan con estacionamiento. " "WhatsApp: (664) 123-4567 (ambas sucursales comparten línea)." ), "category": "faq", "source": "faq_general", }, { "content": ( "¿Qué es el Monedero Electrónico SKEEN? Es un sistema de saldo a favor donde acumulas " "dinero por compras y referidos. Puedes usarlo para pagar servicios, productos o paquetes. " "Consulta tu saldo respondiendo 'saldo' en este chat o en recepción. No tiene fecha de vencimiento." ), "category": "faq", "source": "faq_general", }, { "content": ( "¿Los tratamientos son seguros durante el embarazo? NO realizamos procedimientos estéticos " "invásivos durante el embarazo ni lactancia. Sí ofrecemos limpiezas faciales suaves e hidratación. " "Siempre informa a tu médico antes de cualquier tratamiento dermatológico." ), "category": "faq", "source": "faq_general", }, { "content": ( "¿Necesito cita para comprar productos? No, puedes comprar productos SKEEN sin cita previa " "en recepción de cualquier sucursal. También coordinamos envíos locales en Rosarito/Tijuana " "con costo de envío desde $80 MXN." ), "category": "faq", "source": "faq_general", }, ] async def seed_knowledge_base() -> None: """Populate the vector store with SKEEN knowledge.""" print("🌱 Seeding SKEEN Knowledge Base...") # Ensure tables exist async with AsyncSessionLocal() as session: # Create table if not exists from sqlalchemy import text statements = [s.strip() for s in CREATE_KNOWLEDGE_TABLE_SQL.strip().split(";") if s.strip()] for stmt in statements: await session.execute(text(stmt + ";")) await session.commit() async with AsyncSessionLocal() as session: rag = RAGStore(session) # Clear existing catalog data to avoid duplicates await rag.delete_by_source("catalogo_servicios") await rag.delete_by_source("catalogo_productos") await rag.delete_by_source("catalogo_paquetes") await rag.delete_by_source("faq_general") # Check if OpenAI is configured from src.config import settings openai_configured = bool(settings.OPENAI_API_KEY.get_secret_value()) and not settings.OPENAI_API_KEY.get_secret_value().startswith("sk-xxxxx") total = len(SKEEN_KNOWLEDGE) for i, item in enumerate(SKEEN_KNOWLEDGE, 1): try: doc_id = await rag.add_document( content=item["content"], category=item["category"], source=item["source"], ) print(f" [{i}/{total}] {item['category'].upper():12} → {doc_id[:8]}...") except Exception as exc: # Fallback: insert with zero vector if OpenAI fails from sqlalchemy import text import json zero_vector = "[" + ",".join(["0.0"] * settings.VECTOR_DIMENSION) + "]" result = await session.execute( text(""" INSERT INTO knowledge_chunks (id, content, metadata, category, source, embedding) VALUES (gen_random_uuid()::text, :content, :metadata, :category, :source, CAST(:embedding AS vector)) RETURNING id """), { "content": item["content"], "metadata": json.dumps({}), "category": item["category"], "source": item["source"], "embedding": zero_vector, }, ) row = result.mappings().first() doc_id = row["id"] if row else "unknown" print(f" [{i}/{total}] {item['category'].upper():12} → {doc_id[:8]}... (sin embedding)") if not openai_configured: print(f"\n⚠️ Knowledge base seeded with {total} documents BUT WITHOUT EMBEDDINGS.") print(" Set OPENAI_API_KEY in .env and re-run: python scripts/seed_knowledge.py") else: print(f"\n✅ Knowledge base seeded with {total} documents.") async def verify_search() -> None: """Quick verification search.""" from src.config import settings openai_configured = bool(settings.OPENAI_API_KEY.get_secret_value()) and not settings.OPENAI_API_KEY.get_secret_value().startswith("sk-xxxxx") if not openai_configured: print("\n🔍 Verification search skipped (OpenAI API key not configured).") print(" Re-run after setting OPENAI_API_KEY to test semantic search.") return print("\n🔍 Running verification searches...") async with AsyncSessionLocal() as session: rag = RAGStore(session) queries = [ "¿Cuánto cuesta la toxina botulínica?", "Quiero agendar una depilación láser", "¿Tienen protector solar?", "Cómo cancelo una cita", ] for q in queries: results = await rag.search(q, top_k=2) print(f"\n Q: {q}") for r in results: print(f" [{r['category']}] {r['content'][:100]}...") async def main() -> None: await seed_knowledge_base() await verify_search() if __name__ == "__main__": asyncio.run(main())