diff --git a/.gitignore b/.gitignore index 60c768f..682258b 100644 --- a/.gitignore +++ b/.gitignore @@ -158,7 +158,6 @@ cython_debug/ .vscode/ # Google Service Account Credentials -*.json !credentials.example.json google_key.json diff --git a/README.md b/README.md index 206299f..4d28a55 100644 --- a/README.md +++ b/README.md @@ -1,73 +1,34 @@ # 🤖 Talia Bot: Asistente Personal & Orquestador de Negocio -Talia no es un simple chatbot; es un Middleware de Inteligencia Artificial alojado en un VPS que orquesta las operaciones diarias de administración, logística y ventas. Actúa como el puente central entre usuarios en Telegram y servicios críticos como Vikunja (Gestión de Proyectos), Google Calendar y Hardware de Impresión remota. +Talia es un **Middleware de Inteligencia Artificial** diseñado para orquestar operaciones de negocio a través de Telegram. Funciona como un asistente personal que responde a roles de usuario específicos, conectando servicios externos como **Vikunja (Gestión de Proyectos)** y **Google Calendar** en una única interfaz conversacional. --- -## 🚀 Concepto Central: Enrutamiento por Identidad +## 🚀 Concepto Central: Arquitectura Modular y Roles de Usuario -La característica core de Talia es su capacidad de cambiar de personalidad y permisos dinámicamente basándose en el Telegram ID del usuario: +La funcionalidad del bot se basa en dos pilares: -| Rol | Icono | Descripción | Permisos | -| :------ | :---: | :------------------ | :-------------------------------------------------------------------------------- | -| **Admin** | 👑 | Dueño / Gerente | God Mode: Gestión total de proyectos, bloqueos de calendario, generación de identidad NFC e impresión. | -| **Crew** | 👷 | Equipo Operativo | Limitado: Solicitud de agenda (validada), asignación de tareas, impresión de documentos. | -| **Cliente** | 👤 | Usuario Público | Ventas: Embudo de captación, consulta de servicios (RAG) y agendamiento comercial. | +1. **Enrutamiento por Identidad**: El bot identifica a cada usuario por su Telegram ID y le asigna un rol (`admin`, `crew`, `client`). Cada rol tiene acceso a un conjunto diferente de funcionalidades y menús, definidos en una base de datos SQLite. +2. **Motor de Flujos de Conversación**: En lugar de código rígido, las conversaciones se definen como "flujos" en archivos **JSON** (`talia_bot/data/flows/`). Un motor central (`flow_engine.py`) interpreta estos archivos para guiar al usuario a través de una serie de preguntas y respuestas, haciendo que el sistema sea altamente escalable y fácil de mantener. + +| Rol | Icono | Descripción | Permisos Clave | +| :------ | :---: | :------------------ | :-------------------------------------------------------------------------- | +| **Admin** | 👑 | Dueño / Gerente | Control total: gestión de proyectos, agenda, y configuración del sistema. | +| **Crew** | 👷 | Equipo Operativo | Funciones limitadas: solicitud de agenda, impresión de documentos. | +| **Cliente** | 👤 | Usuario Externo | Embudo de ventas: captación de datos y presentación de servicios. | --- -## 🛠️ Arquitectura Técnica +## 🛠️ Arquitectura Técnica Simplificada -El sistema sigue un flujo modular: +El sistema opera con el siguiente flujo: -1. **Input**: Telegram (Texto o Audio). -2. **STT**: Whisper (Conversión de Audio a Texto). -3. **Router**: Verificación de ID contra la base de datos de usuarios. -4. **Cerebro (LLM)**: OpenAI (Fase 1) / Google Gemini (Fase 2). -5. **Tools**: - * **Vikunja API**: Lectura/Escritura de tareas con filtrado de privacidad. - * **Google Calendar API**: Gestión de tiempos y reglas de disponibilidad. - * **SMTP/IMAP**: Comunicación bidireccional con impresoras. - * **NFC Gen**: Codificación Base64 para tags físicos. - ---- - -## 📋 Flujos de Trabajo (Features) - -### 1. 👑 Gestión Admin (Proyectos & Identidad) - -* **Proyectos (Vikunja)**: - * Resumen inteligente de estatus de proyectos. - * Comandos naturales: *"Marca el proyecto de web como terminado y comenta que se envió factura"*. -* **Wizard de Identidad (NFC)**: - * Flujo paso a paso para dar de alta colaboradores. - * Genera JSON de registro y String Base64 listo para escribir en Tags NFC. - * Inputs: Nombre, ID Empleado, Sucursal (Botones), Telegram ID. - -### 2. 👷 Gestión Crew (Agenda & Tareas) - -* **Solicitud de Tiempo (Wizard)**: - * Solicita espacios de 1 a 4 horas. - * **Reglas de Negocio**: - * No permite fechas > 3 meses a futuro. - * **Gatekeeper**: Verifica Google Calendar. Si hay evento "Privado" del Admin, rechaza automáticamente. -* **Modo Buzón (Vikunja)**: - * Crea tareas asignadas al Admin. - * **Privacidad**: Solo pueden consultar el estatus de tareas creadas por ellos mismos. - -### 3. 🖨️ Sistema de Impresión Remota (Print Loop) - -* Permite enviar archivos desde Telegram a la impresora física de la oficina. -* **Envío (SMTP)**: El bot envía el documento a un correo designado. -* **Tracking**: El asunto del correo lleva un hash único: `PJ:{uuid}#TID:{telegram_id}`. -* **Confirmación (IMAP Listener)**: Un proceso en background escucha la respuesta de la impresora y notifica al usuario en Telegram. - -### 4. 👤 Ventas Automáticas (RAG) - -* Identifica usuarios nuevos (no registrados en la DB). -* Captura datos (Lead Magnet). -* Analiza ideas de clientes usando `servicios.json` (Base de conocimiento). -* Ofrece citas de ventas mediante link de Calendly. +1. **Recepción de Mensajes**: `main.py` recibe todos los inputs (texto, botones, comandos) desde Telegram. +2. **Identificación de Usuario**: Se consulta la base de datos (`users.db`) para obtener el rol del usuario. +3. **Dispatching de Acciones**: + * Si el usuario no está en una conversación, se le muestra un menú de botones basado en los flujos JSON disponibles para su rol. + * Si el usuario ya está en una conversación, el `flow_engine.py` gestiona la respuesta. +4. **Ejecución de Módulos**: El motor de flujos invoca módulos específicos (`vikunja.py`, `calendar.py`, etc.) para interactuar con APIs externas según sea necesario. --- @@ -75,79 +36,96 @@ El sistema sigue un flujo modular: ### Prerrequisitos -* Python 3.10+ +* Python 3.9+ +* Docker y Docker Compose * Cuenta de Telegram Bot (@BotFather) * Instancia de Vikunja (Self-hosted) -* Cuenta de Servicio Google Cloud (Calendar API) -* Servidor de Correo (SMTP/IMAP) +* Credenciales de Cuenta de Servicio de Google Cloud (para Calendar API) -### 1. Clonar y Entorno Virtual +### 1. Clonar y Configurar el Entorno ```bash -git clone https://github.com/marcogll/talia_bot_mg.git -cd talia_bot_mg -python -m venv venv -source venv/bin/activate # Windows: venv\Scripts\activate -pip install -r requirements.txt +# Clona el repositorio oficial +git clone https://github.com/marcogll/talia_bot.git +cd talia_bot + +# Copia el archivo de ejemplo para las variables de entorno +cp .env.example .env ``` ### 2. Variables de Entorno (`.env`) -Crea un archivo `.env` en la raíz con la siguiente estructura: +Abre el archivo `.env` y rellena las siguientes variables. **No subas este archivo a Git.** ```env -# --- TELEGRAM & SECURITY --- -TELEGRAM_BOT_TOKEN=tu_token_telegram +# Token de tu bot de Telegram +TELEGRAM_TOKEN=tu_token_telegram + +# Tu Telegram ID numérico para permisos de administrador ADMIN_ID=tu_telegram_id -# --- AI CORE --- +# Clave de API de OpenAI (si se usa) OPENAI_API_KEY=sk-... -# --- INTEGRACIONES --- -VIKUNJA_API_URL=https://tuservidor.com/api/v1 +# URL y Token de tu instancia de Vikunja +VIKUNJA_API_URL=https://tu_vikunja.com/api/v1 VIKUNJA_TOKEN=tu_token_vikunja -GOOGLE_CREDENTIALS_PATH=./data/credentials.json -# --- PRINT SERVICE --- -SMTP_SERVER=smtp.hostinger.com -SMTP_PORT=465 -SMTP_USER=print.service@vanityexperience.mx -SMTP_PASS=tu_password_seguro -IMAP_SERVER=imap.hostinger.com +# ID del Calendario de Google a gestionar +CALENDAR_ID=tu_id_de_calendario@group.calendar.google.com + +# Ruta al archivo de credenciales de Google Cloud. +# Este archivo debe estar en el directorio raíz y se llama 'google_key.json' por defecto. +GOOGLE_SERVICE_ACCOUNT_FILE=./google_key.json ``` -### 3. Estructura de Datos +### 3. Estructura de Datos y Credenciales -Asegúrate de tener los archivos base en `talia_bot/data/`: -* `servicios.json`: Catálogo de servicios para el RAG de ventas. -* `credentials.json`: Credenciales de Google Cloud. -* `users.db`: Base de datos SQLite. +* **Base de Datos**: La base de datos `users.db` se creará automáticamente si no existe. Para asignar roles, debes agregar manualmente los Telegram IDs en la tabla `users`. +* **Credenciales de Google**: Coloca tu archivo de credenciales de la cuenta de servicio de Google Cloud en el directorio raíz del proyecto y renómbralo a `google_key.json`. **El archivo `.gitignore` ya está configurado para ignorar este archivo y proteger tus claves.** +* **Flujos de Conversación**: Para modificar o añadir flujos, edita los archivos JSON en `talia_bot/data/flows/`. + +### 4. Ejecución con Docker + +La forma más sencilla de levantar el bot es usando Docker Compose: + +```bash +docker-compose up --build +``` --- ## 📂 Estructura del Proyecto ```text -talia_bot_mg/ +talia_bot/ +├── .env # (Local) Variables de entorno y secretos +├── .env.example # Plantilla de variables de entorno +├── .gitignore # Archivos ignorados por Git +├── Dockerfile # Define el contenedor de la aplicación +├── docker-compose.yml # Orquesta el servicio del bot +├── google_key.json # (Local) Credenciales de Google Cloud +├── requirements.txt # Dependencias de Python ├── talia_bot/ -│ ├── main.py # Entry Point y Router de Identidad -│ ├── db.py # Gestión de la base de datos -│ ├── config.py # Carga de variables de entorno -│ ├── modules/ -│ │ ├── identity.py # Lógica de Roles y Permisos -│ │ ├── llm_engine.py # Cliente OpenAI/Gemini -│ │ ├── vikunja.py # API Manager para Tareas -│ │ ├── calendar.py # Google Calendar Logic & Rules -│ │ ├── printer.py # SMTP/IMAP Loop -│ │ └── sales_rag.py # Lógica de Ventas y Servicios -│ └── data/ -│ ├── servicios.json # Base de conocimiento -│ ├── credentials.json # Credenciales de Google -│ └── users.db # Base de datos de usuarios -├── .env.example # Plantilla de variables de entorno -├── requirements.txt # Dependencias -├── Dockerfile # Configuración del contenedor -└── docker-compose.yml # Orquestador de Docker +│ ├── __init__.py +│ ├── main.py # Entry point, dispatcher y handlers de Telegram +│ ├── db.py # Lógica de la base de datos SQLite +│ ├── config.py # Carga y validación de variables de entorno +│ ├── scheduler.py # (Futuro) Tareas programadas +│ ├── webhook_client.py # (Futuro) Cliente para webhooks externos +│ ├── data/ +│ │ ├── flows/ # Directorio con flujos de conversación en JSON +│ │ ├── services.json # Base de conocimiento para ventas +│ │ └── users.db # Base de datos de usuarios +│ └── modules/ +│ ├── __init__.py +│ ├── flow_engine.py # Motor que interpreta los flujos JSON +│ ├── calendar.py # Integración con Google Calendar API +│ ├── vikunja.py # Integración con Vikunja API +│ ├── onboarding.py # Lógica para el alta de nuevos usuarios +│ ├── llm_engine.py # (Opcional) Cliente para OpenAI/Gemini +│ └── ... (otros módulos) +└── ... ``` --- diff --git a/talia_bot/data/flows/admin_block_agenda.json b/talia_bot/data/flows/admin_block_agenda.json new file mode 100644 index 0000000..5598962 --- /dev/null +++ b/talia_bot/data/flows/admin_block_agenda.json @@ -0,0 +1,25 @@ +{ + "id": "admin_block_agenda", + "role": "admin", + "trigger_button": "🛑 Bloquear Agenda", + "steps": [ + { + "step_id": 0, + "variable": "BLOCK_DATE", + "question": "Necesito bloquear la agenda. ¿Para cuándo?", + "options": ["Hoy", "Mañana"] + }, + { + "step_id": 1, + "variable": "BLOCK_TIME", + "question": "Dame el horario exacto que necesitas bloquear (ej. 'de 2pm a 4pm').", + "input_type": "text" + }, + { + "step_id": 2, + "variable": "BLOCK_TITLE", + "question": "Finalmente, dame una breve descripción o motivo del bloqueo.", + "input_type": "text" + } + ] +} diff --git a/talia_bot/data/flows/admin_check_agenda.json b/talia_bot/data/flows/admin_check_agenda.json new file mode 100644 index 0000000..01300c3 --- /dev/null +++ b/talia_bot/data/flows/admin_check_agenda.json @@ -0,0 +1,19 @@ +{ + "id": "admin_check_agenda", + "role": "admin", + "trigger_button": "📅 Revisar Agenda", + "steps": [ + { + "step_id": 0, + "variable": "AGENDA_TIMEFRAME", + "question": "Consultando el oráculo del tiempo... ⏳", + "options": ["📅 Hoy", "🔮 Mañana"] + }, + { + "step_id": 1, + "variable": "AGENDA_ACTION", + "question": "Aquí tienes tu realidad: {CALENDAR_DATA}", + "options": ["✅ Todo bien", "🛑 Bloquear Espacio"] + } + ] +} diff --git a/talia_bot/data/flows/admin_create_nfc_tag.json b/talia_bot/data/flows/admin_create_nfc_tag.json new file mode 100644 index 0000000..56e3d92 --- /dev/null +++ b/talia_bot/data/flows/admin_create_nfc_tag.json @@ -0,0 +1,25 @@ +{ + "id": "admin_create_nfc_tag", + "role": "admin", + "trigger_button": "➕ Crear Tag NFC", + "steps": [ + { + "step_id": 0, + "variable": "NFC_ACTION_TYPE", + "question": "Creemos un nuevo tag NFC. ¿Qué acción quieres que dispare?", + "options": ["Iniciar Flujo", "URL Estática"] + }, + { + "step_id": 1, + "variable": "NFC_FLOW_CHOICE", + "question": "Okay, ¿qué flujo debería iniciar este tag?", + "input_type": "dynamic_keyboard_flows" + }, + { + "step_id": 2, + "variable": "NFC_CONFIRM", + "question": "Perfecto. Cuando acerques tu teléfono a este tag, se iniciará el flujo '{flow_name}'. Aquí tienes los datos para escribir en el tag: {NFC_DATA}", + "options": ["✅ Hecho"] + } + ] +} diff --git a/talia_bot/data/flows/admin_idea_capture.json b/talia_bot/data/flows/admin_idea_capture.json new file mode 100644 index 0000000..bd564d9 --- /dev/null +++ b/talia_bot/data/flows/admin_idea_capture.json @@ -0,0 +1,25 @@ +{ + "id": "admin_idea_capture", + "role": "admin", + "trigger_button": "💡 Capturar Idea", + "steps": [ + { + "step_id": 0, + "variable": "IDEA_CONTENT", + "question": "Te escucho. 💡 Las ideas vuelan...", + "input_type": "text_or_audio" + }, + { + "step_id": 1, + "variable": "IDEA_CATEGORY", + "question": "¿En qué cajón mental guardamos esto?", + "options": ["💰 Negocio", "📹 Contenido", "👤 Personal"] + }, + { + "step_id": 2, + "variable": "IDEA_ACTION", + "question": "¿Cuál es el plan de ataque?", + "options": ["✅ Crear Tarea", "📓 Guardar Nota"] + } + ] +} diff --git a/talia_bot/data/flows/admin_print_file.json b/talia_bot/data/flows/admin_print_file.json new file mode 100644 index 0000000..cdf9a63 --- /dev/null +++ b/talia_bot/data/flows/admin_print_file.json @@ -0,0 +1,13 @@ +{ + "id": "admin_print_file", + "role": "admin", + "trigger_button": "🖨️ Imprimir", + "steps": [ + { + "step_id": 0, + "variable": "UPLOAD_FILE", + "question": "Por favor, envíame el archivo que quieres imprimir.", + "input_type": "document" + } + ] +} diff --git a/talia_bot/data/flows/admin_project_management.json b/talia_bot/data/flows/admin_project_management.json new file mode 100644 index 0000000..63bd775 --- /dev/null +++ b/talia_bot/data/flows/admin_project_management.json @@ -0,0 +1,31 @@ +{ + "id": "admin_project_management", + "role": "admin", + "trigger_button": "🏗️ Ver Proyectos", + "steps": [ + { + "step_id": 0, + "variable": "PROJECT_SELECT", + "question": "Aquí está el tablero de ajedrez...", + "input_type": "dynamic_keyboard_vikunja_projects" + }, + { + "step_id": 1, + "variable": "TASK_SELECT", + "question": "Has seleccionado el proyecto {project_name}. ¿Qué quieres hacer?", + "input_type": "dynamic_keyboard_vikunja_tasks" + }, + { + "step_id": 2, + "variable": "ACTION_TYPE", + "question": "¿Cuál es la jugada?", + "options": ["🔄 Actualizar Estatus", "💬 Agregar Comentario"] + }, + { + "step_id": 3, + "variable": "UPDATE_CONTENT", + "question": "Adelante. Soy todo oídos.", + "input_type": "text_or_audio" + } + ] +} diff --git a/talia_bot/data/flows/client_sales_funnel.json b/talia_bot/data/flows/client_sales_funnel.json new file mode 100644 index 0000000..daaa074 --- /dev/null +++ b/talia_bot/data/flows/client_sales_funnel.json @@ -0,0 +1,25 @@ +{ + "id": "client_sales_funnel", + "role": "client", + "trigger_automatic": true, + "steps": [ + { + "step_id": 0, + "variable": "CLIENT_NAME", + "question": "Hola. Soy Talia, la mano derecha de Armando. ✨Él está ocupado creando, pero yo soy la puerta de entrada. ¿Con quién tengo el gusto?", + "input_type": "text" + }, + { + "step_id": 1, + "variable": "CLIENT_INDUSTRY", + "question": "Mucho gusto, {user_name}. Para entender mejor tus necesidades, ¿cuál es el giro de tu negocio o tu industria?", + "options": ["🍽️ Restaurantes", "🩺 Salud", "🛍️ Retail", "อื่น ๆ"] + }, + { + "step_id": 2, + "variable": "IDEA_PITCH", + "question": "Excelente. Ahora, el escenario es tuyo. 🎤 Cuéntame sobre tu proyecto o la idea que tienes en mente. No te guardes nada. Puedes escribirlo o, si prefieres, enviarme una nota de voz.", + "input_type": "text_or_audio" + } + ] +} diff --git a/talia_bot/data/flows/crew_print_file.json b/talia_bot/data/flows/crew_print_file.json new file mode 100644 index 0000000..ed9d376 --- /dev/null +++ b/talia_bot/data/flows/crew_print_file.json @@ -0,0 +1,13 @@ +{ + "id": "crew_print_file", + "role": "crew", + "trigger_button": "🖨️ Imprimir", + "steps": [ + { + "step_id": 0, + "variable": "UPLOAD_FILE", + "question": "Claro, envíame el archivo que necesitas imprimir y yo me encargo.", + "input_type": "document" + } + ] +} diff --git a/talia_bot/data/flows/crew_request_time.json b/talia_bot/data/flows/crew_request_time.json new file mode 100644 index 0000000..0e2a706 --- /dev/null +++ b/talia_bot/data/flows/crew_request_time.json @@ -0,0 +1,31 @@ +{ + "id": "crew_request_time", + "role": "crew", + "trigger_button": "📅 Solicitar Agenda", + "steps": [ + { + "step_id": 0, + "variable": "REQUEST_TYPE", + "question": "Para usar la agenda del estudio, necesito que seas preciso.", + "options": ["🎥 Grabación", "🎙️ Locución", "🎬 Edición", "🛠️ Mantenimiento"] + }, + { + "step_id": 1, + "variable": "REQUEST_DATE", + "question": "¿Para cuándo necesitas el espacio?", + "options": ["Hoy", "Mañana", "Esta Semana"] + }, + { + "step_id": 2, + "variable": "REQUEST_TIME", + "question": "Dame el horario exacto que necesitas (ej. 'de 10am a 2pm').", + "input_type": "text" + }, + { + "step_id": 3, + "variable": "REQUEST_JUSTIFICATION", + "question": "Entendido. Antes de confirmar, necesito que me expliques brevemente el plan o el motivo para justificar el bloqueo del espacio. Puedes escribirlo o enviarme un audio.", + "input_type": "text_or_audio" + } + ] +} diff --git a/talia_bot/data/flows/crew_secret_onboarding.json b/talia_bot/data/flows/crew_secret_onboarding.json new file mode 100644 index 0000000..b5b39dd --- /dev/null +++ b/talia_bot/data/flows/crew_secret_onboarding.json @@ -0,0 +1,37 @@ +{ + "id": "crew_secret_onboarding", + "role": "crew", + "trigger_command": "/abracadabra", + "steps": [ + { + "step_id": 0, + "variable": "ONBOARD_START", + "question": "Vaya, vaya... Parece que conoces el comando secreto. 🎩. Antes de continuar, necesito saber tu nombre completo.", + "input_type": "text" + }, + { + "step_id": 1, + "variable": "ONBOARD_ORIGIN", + "question": "Un placer, {user_name}. ¿Cuál es tu base de operaciones principal?", + "options": ["🏢 Office", "✨ Aura"] + }, + { + "step_id": 2, + "variable": "ONBOARD_EMAIL", + "question": "Perfecto. Ahora necesito tu correo electrónico de la empresa.", + "input_type": "text" + }, + { + "step_id": 3, + "variable": "ONBOARD_PHONE", + "question": "Y por último, tu número de teléfono.", + "input_type": "text" + }, + { + "step_id": 4, + "variable": "ONBOARD_CONFIRM", + "question": "Gracias. He enviado una notificación al Administrador para que apruebe tu acceso. En cuanto lo haga, tendrás acceso completo. ¡Bienvenido a bordo!", + "options": ["✅ Entendido"] + } + ] +} diff --git a/talia_bot/data/services.json b/talia_bot/data/services.json new file mode 100644 index 0000000..bfe4943 --- /dev/null +++ b/talia_bot/data/services.json @@ -0,0 +1,22 @@ +[ + { + "service_name": "Web Development for Restaurants", + "description": "Custom websites and online ordering systems for restaurants, helping you reach more customers and streamline your operations.", + "keywords": ["restaurant", "food", "online ordering", "website", "restaurantes", "comida"] + }, + { + "service_name": "Patient Management Systems for Healthcare", + "description": "A secure and efficient software solution for managing patient records, appointments, and billing in medical clinics.", + "keywords": ["healthcare", "medical", "patient", "clinic", "salud", "médico", "pacientes"] + }, + { + "service_name": "Content Creation & Social Media Strategy", + "description": "Engaging content packages and social media management to build your brand's online presence and connect with your audience.", + "keywords": ["content creation", "social media", "marketing", "branding", "contenido", "redes sociales"] + }, + { + "service_name": "General Business Consulting", + "description": "Strategic consulting to help you optimize business processes, identify growth opportunities, and improve overall performance.", + "keywords": ["business", "consulting", "strategy", "growth", "negocio", "consultoría"] + } +] diff --git a/talia_bot/modules/flow_engine.py b/talia_bot/modules/flow_engine.py new file mode 100644 index 0000000..7165751 --- /dev/null +++ b/talia_bot/modules/flow_engine.py @@ -0,0 +1,129 @@ +# talia_bot/modules/flow_engine.py +import json +import logging +import os +from talia_bot.db import get_db_connection + +logger = logging.getLogger(__name__) + +class FlowEngine: + def __init__(self): + self.flows = self._load_flows() + + def _load_flows(self): + """Loads all individual flow JSON files from the flows directory.""" + flows_dir = 'talia_bot/data/flows' + loaded_flows = [] + try: + if not os.path.exists(flows_dir): + logger.error(f"Flows directory not found at '{flows_dir}'") + return [] + + for filename in os.listdir(flows_dir): + if filename.endswith('.json'): + file_path = os.path.join(flows_dir, filename) + try: + with open(file_path, 'r', encoding='utf-8') as f: + flow_data = json.load(f) + if 'role' not in flow_data: + logger.warning(f"Flow {filename} is missing a 'role' key. Skipping.") + continue + loaded_flows.append(flow_data) + except json.JSONDecodeError: + logger.error(f"Error decoding JSON from {filename}.") + except Exception as e: + logger.error(f"Error loading flow from {filename}: {e}") + + logger.info(f"Successfully loaded {len(loaded_flows)} flows.") + return loaded_flows + + except Exception as e: + logger.error(f"Failed to load flows from directory {flows_dir}: {e}") + return [] + + def get_flow(self, flow_id): + """Retrieves a specific flow by its ID.""" + return next((flow for flow in self.flows if flow.get('id') == flow_id), None) + + def get_conversation_state(self, user_id): + """Gets the current conversation state for a user from the database.""" + conn = get_db_connection() + cursor = conn.cursor() + cursor.execute("SELECT flow_id, current_step_id, collected_data FROM conversations WHERE user_id = ?", (user_id,)) + state = cursor.fetchone() + conn.close() + if state: + return { + "flow_id": state['flow_id'], + "current_step_id": state['current_step_id'], + "collected_data": json.loads(state['collected_data']) if state['collected_data'] else {} + } + return None + + def start_flow(self, user_id, flow_id): + """Starts a new flow for a user.""" + flow = self.get_flow(flow_id) + if not flow or 'steps' not in flow or not flow['steps']: + logger.error(f"Flow '{flow_id}' is invalid or has no steps.") + return None + + initial_step = flow['steps'][0] + self.update_conversation_state(user_id, flow_id, initial_step['step_id'], {}) + return initial_step + + def update_conversation_state(self, user_id, flow_id, step_id, collected_data): + """Creates or updates the conversation state in the database.""" + conn = get_db_connection() + cursor = conn.cursor() + cursor.execute(""" + INSERT OR REPLACE INTO conversations (user_id, flow_id, current_step_id, collected_data) + VALUES (?, ?, ?, ?) + """, (user_id, flow_id, step_id, json.dumps(collected_data))) + conn.commit() + conn.close() + + def handle_response(self, user_id, response_data): + """ + Handles a user's response, saves the data, and returns the next action. + """ + state = self.get_conversation_state(user_id) + if not state: + return {"status": "error", "message": "No conversation state found."} + + flow = self.get_flow(state['flow_id']) + if not flow: + return {"status": "error", "message": f"Flow '{state['flow_id']}' not found."} + + current_step = next((step for step in flow['steps'] if step['step_id'] == state['current_step_id']), None) + if not current_step: + self.end_flow(user_id) + return {"status": "error", "message": "Current step not found in flow."} + + # Save the user's response using the 'variable' key from the step definition + variable_name = current_step.get('variable') + + if variable_name: + state['collected_data'][variable_name] = response_data + else: + # Fallback for steps without a 'variable' key + logger.warning(f"Step {current_step['step_id']} in flow {flow['id']} has no 'variable' defined. Saving with default key.") + state['collected_data'][f"step_{current_step['step_id']}_response"] = response_data + + + next_step_id = state['current_step_id'] + 1 + next_step = next((step for step in flow['steps'] if step['step_id'] == next_step_id), None) + + if next_step: + self.update_conversation_state(user_id, state['flow_id'], next_step_id, state['collected_data']) + return {"status": "in_progress", "step": next_step} + else: + self.end_flow(user_id) + return {"status": "complete", "flow_id": flow['id'], "data": state['collected_data']} + + def end_flow(self, user_id): + """Ends a flow for a user by deleting their conversation state.""" + conn = get_db_connection() + cursor = conn.cursor() + cursor.execute("DELETE FROM conversations WHERE user_id = ?", (user_id,)) + conn.commit() + conn.close()