feat: real ERPNext Healthcare integration + setup tooling
- Replace all mock tools with real ERPNext Healthcare operations - ERPNextHealthcare class: patients, practitioners, appointments, schedules - check_availability queries real practitioner schedules from ERPNext - create_appointment finds/creates patient + validates conflicts + books in ERPNext - Add /api/v1/config/test endpoint to validate all service connections - Add scripts/validate_setup.py for CLI validation of Meta/OpenAI/ERPNext/DB - Add scripts/seed_knowledge.py with full SKEEN catalog (services, products, packages, FAQ) - Add tests for webhook, health, and WhatsApp client - Update main.py to include config router
This commit is contained in:
0
scripts/__init__.py
Normal file
0
scripts/__init__.py
Normal file
276
scripts/seed_knowledge.py
Normal file
276
scripts/seed_knowledge.py
Normal file
@@ -0,0 +1,276 @@
|
||||
#!/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
|
||||
await session.execute(text(CREATE_KNOWLEDGE_TABLE_SQL))
|
||||
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")
|
||||
|
||||
total = len(SKEEN_KNOWLEDGE)
|
||||
for i, item in enumerate(SKEEN_KNOWLEDGE, 1):
|
||||
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]}...")
|
||||
|
||||
print(f"\n✅ Knowledge base seeded with {total} documents.")
|
||||
|
||||
|
||||
async def verify_search() -> None:
|
||||
"""Quick verification search."""
|
||||
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())
|
||||
182
scripts/validate_setup.py
Normal file
182
scripts/validate_setup.py
Normal file
@@ -0,0 +1,182 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Validation script for SKEEN CRM Agent setup.
|
||||
|
||||
Run this after filling in .env to verify all connections work.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
|
||||
# Add project root to path
|
||||
import os
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from src.config import settings
|
||||
from src.infrastructure.db import engine
|
||||
from src.infrastructure.redis import get_redis
|
||||
from src.infrastructure.whatsapp.client import get_whatsapp_client
|
||||
from src.infrastructure.erpnext.client import get_erpnext_client
|
||||
from src.infrastructure.ai.openai_client import get_openai_client
|
||||
from src.infrastructure.ai.rag import RAGStore
|
||||
from sqlalchemy import text
|
||||
|
||||
|
||||
async def test_postgres() -> bool:
|
||||
"""Test PostgreSQL connection."""
|
||||
print("🐘 Testing PostgreSQL...")
|
||||
try:
|
||||
async with engine.connect() as conn:
|
||||
result = await conn.execute(text("SELECT version()"))
|
||||
version = result.scalar()
|
||||
print(f" ✅ PostgreSQL connected: {version[:50]}...")
|
||||
|
||||
# Check pgvector
|
||||
result = await conn.execute(text("SELECT * FROM pg_extension WHERE extname = 'vector'"))
|
||||
if result.fetchone():
|
||||
print(" ✅ pgvector extension installed")
|
||||
else:
|
||||
print(" ⚠️ pgvector extension NOT installed (run: CREATE EXTENSION vector)")
|
||||
return True
|
||||
except Exception as exc:
|
||||
print(f" ❌ PostgreSQL failed: {exc}")
|
||||
return False
|
||||
|
||||
|
||||
async def test_redis() -> bool:
|
||||
"""Test Redis connection."""
|
||||
print("🔴 Testing Redis...")
|
||||
try:
|
||||
redis = await get_redis()
|
||||
pong = await redis.ping()
|
||||
if pong:
|
||||
print(" ✅ Redis connected")
|
||||
return True
|
||||
return False
|
||||
except Exception as exc:
|
||||
print(f" ❌ Redis failed: {exc}")
|
||||
return False
|
||||
|
||||
|
||||
async def test_meta_whatsapp() -> bool:
|
||||
"""Test Meta WhatsApp Business API."""
|
||||
print("💬 Testing Meta WhatsApp API...")
|
||||
if not settings.META_ACCESS_TOKEN.get_secret_value():
|
||||
print(" ⚠️ META_ACCESS_TOKEN not set — skipping")
|
||||
return False
|
||||
|
||||
try:
|
||||
client = await get_whatsapp_client()
|
||||
profile = await client.get_business_profile()
|
||||
print(f" ✅ WhatsApp Business API connected")
|
||||
print(f" 📱 Phone Number ID: {settings.META_PHONE_NUMBER_ID}")
|
||||
return True
|
||||
except Exception as exc:
|
||||
print(f" ❌ WhatsApp API failed: {exc}")
|
||||
return False
|
||||
|
||||
|
||||
async def test_erpnext() -> bool:
|
||||
"""Test ERPNext connection."""
|
||||
print("🏥 Testing ERPNext...")
|
||||
if not settings.ERPNEXT_BASE_URL:
|
||||
print(" ⚠️ ERPNEXT_BASE_URL not set — skipping")
|
||||
return False
|
||||
|
||||
try:
|
||||
client = await get_erpnext_client()
|
||||
# Try to get the current user as a lightweight check
|
||||
from src.infrastructure.erpnext.healthcare import ERPNextHealthcare
|
||||
|
||||
hc = ERPNextHealthcare(client)
|
||||
practitioners = await hc.get_practitioners()
|
||||
print(f" ✅ ERPNext connected")
|
||||
print(f" 👨⚕️ Practitioners found: {len(practitioners)}")
|
||||
for p in practitioners[:3]:
|
||||
print(f" - {p.get('practitioner_name')} ({p.get('department')})")
|
||||
return True
|
||||
except Exception as exc:
|
||||
print(f" ❌ ERPNext failed: {exc}")
|
||||
return False
|
||||
|
||||
|
||||
async def test_openai() -> bool:
|
||||
"""Test OpenAI API."""
|
||||
print("🧠 Testing OpenAI...")
|
||||
if not settings.OPENAI_API_KEY.get_secret_value():
|
||||
print(" ⚠️ OPENAI_API_KEY not set — skipping")
|
||||
return False
|
||||
|
||||
try:
|
||||
client = await get_openai_client()
|
||||
# Quick embedding test
|
||||
embedding = await client.create_embedding("Hola SKEEN")
|
||||
print(f" ✅ OpenAI connected")
|
||||
print(f" 🤖 Model: {settings.OPENAI_MODEL}")
|
||||
print(f" 📐 Embedding dimensions: {len(embedding)}")
|
||||
return True
|
||||
except Exception as exc:
|
||||
print(f" ❌ OpenAI failed: {exc}")
|
||||
return False
|
||||
|
||||
|
||||
async def test_rag() -> bool:
|
||||
"""Test RAG vector store."""
|
||||
print("🔍 Testing RAG Vector Store...")
|
||||
try:
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from src.infrastructure.db import AsyncSessionLocal
|
||||
|
||||
async with AsyncSessionLocal() as session:
|
||||
rag = RAGStore(session)
|
||||
await rag.ensure_extension()
|
||||
print(" ✅ RAG vector store ready")
|
||||
return True
|
||||
except Exception as exc:
|
||||
print(f" ❌ RAG failed: {exc}")
|
||||
return False
|
||||
|
||||
|
||||
async def main() -> int:
|
||||
"""Run all validation checks."""
|
||||
print("=" * 60)
|
||||
print("🔧 SKEEN CRM Agent - Setup Validation")
|
||||
print("=" * 60)
|
||||
print()
|
||||
|
||||
results = []
|
||||
results.append(("PostgreSQL", await test_postgres()))
|
||||
print()
|
||||
results.append(("Redis", await test_redis()))
|
||||
print()
|
||||
results.append(("Meta WhatsApp", await test_meta_whatsapp()))
|
||||
print()
|
||||
results.append(("ERPNext", await test_erpnext()))
|
||||
print()
|
||||
results.append(("OpenAI", await test_openai()))
|
||||
print()
|
||||
results.append(("RAG Vector Store", await test_rag()))
|
||||
print()
|
||||
|
||||
print("=" * 60)
|
||||
print("📊 Summary")
|
||||
print("=" * 60)
|
||||
all_pass = True
|
||||
for name, passed in results:
|
||||
status = "✅ PASS" if passed else "❌ FAIL"
|
||||
print(f" {status} — {name}")
|
||||
if not passed:
|
||||
all_pass = False
|
||||
|
||||
print()
|
||||
if all_pass:
|
||||
print("🎉 All systems operational! Ready to receive WhatsApp messages.")
|
||||
return 0
|
||||
else:
|
||||
print("⚠️ Some services are not configured. Check .env and services.")
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
exit_code = asyncio.run(main())
|
||||
sys.exit(exit_code)
|
||||
Reference in New Issue
Block a user