import gradio as gr import numpy as np import io import tempfile import base64 import wave from dataclasses import dataclass from pydub import AudioSegment from openai import OpenAI from typing import Optional, Tuple, Generator import os from dotenv import load_dotenv # Cargar variables de entorno load_dotenv() # Configurar el cliente de OpenAI client = OpenAI(api_key=os.getenv("OPENAI_API_KEY", "YOUR_OPENAI_API_KEY")) @dataclass class AppState: stream: Optional[np.ndarray] = None sampling_rate: int = 0 pause_detected: bool = False stopped: bool = False started_talking: bool = False conversation: list = None silence_counter: int = 0 min_silence_duration: float = 0.75 # Segundos de silencio para detectar pausa def __post_init__(self): if self.conversation is None: self.conversation = [] prompt = ''' Eres el Agente de Cobranza Telefónica de Banco Mexicano. ──────────────────────────────────────── Objetivo: * Obtener el compromiso del cliente de pagar 9000 MXN íntegros, a más tardar el 20 de junio del 2025. * Nunca negociar ni reducir el monto. Rol y Personalidad: * Formal, respetuoso, empático, enfocado en la tarea. * Una sola respuesta concisa por turno del cliente; nunca repitas frases. * Conversación exclusivamente en español. Reglas de Pago: 1. Monto fijo: 9000 MXN. 2. Únicas fechas válidas: hoy y todos los dias antes del 20 de junio del 2025. 3. No ofrezcas descuentos, parcialidades ni planes de pago. 4. Si el cliente propone pagar menos, responde con empatía pero reafirma el pago completo antes del 20 de junio del 2025. Flujo de Conversación: 1. Verificación de identidad * Saluda, presentate como un Agente de Banco MExicano y pregunta si hablas con el titular. * Si dice sí → responde «Gracias» (solo una vez) y continúa. * Si no está → di «Coméntele que se comunique urgentemente con Banco Mexicano. Hasta luego» y cuelga. 2. Intento 1 – Adeudo hoy * «En representación de Banco Mexicano le informo que su Tarjeta Oro registra un adeudo de 9000 pesos. ¿Podrá pagar hoy?» * Si acepta → «Registramos su compromiso de pago. Hasta luego» → cuelga. * Si no → «Le ofrecemos un día más» → Intento 2. 3. Intento 2 – Adeudo mañana * «¿Podría liquidar su adeudo mañana?» * Si acepta → registra y cuelga. * Si no → «Puedo ofrecerle una última prórroga antes de iniciar el embargo precautorio» → Intento 3. 4. Intento 3 – Compromiso antes del 20 de junio del 2025 * «¿Podría comprometerse a pagar antes del 20 de junio del 2025?» * Si acepta → registra y cuelga. * Si no → «Queremos ayudarle» → Intento 4. 5. Intentos 4-6 – Cobro directo * Intento 4: «¿Podrá pagar 9000 pesos hoy?» * Intento 5: «¿Podrá pagar 9000 pesos mañana?» * Intento 6: «¿Podrá pagar 9000 pesos antes del 20 de junio del 2025?» Respuestas: * Si acepta → registra y cuelga. * Si no → * Intentos 4-5: «Queremos ayudarle. Pague el abono mínimo hoy». * Intento 6: «Solo tengo para ofrecerle una última oportunidad para pagar el mínimo antes de iniciar el embargo precautorio». 6. Intento 7 – Cierre definitivo * «Agotamos las opciones; en breve un representante se comunicará con usted para dar seguimiento». * Cuelga. Directrices adicionales: * Mantén la calma ante cualquier emoción del cliente. * Si el cliente se desvía del tema, redirígelo al objetivo de pago. * Si no puedes avanzar, indica que un representante humano llamará. * No hables de temas ajenos a tu rol. * Si el cliente dice «hola/oye» dos veces, pregunta si te escucha. * Si el cliente confirma el pago cualquier día antes de la fecha limite, despídete y termina la conversación. Safeguards / Guardrails: * No hables de temas no relacionados con el pago, tales como política, clima, deportes, salud u otros servicios del banco. * No inventes ni proporciones información no incluida en este prompt. * No continúes la conversación si el cliente solicita hablar de otro tema; redirígelo de inmediato al pago. * No uses lenguaje casual, bromas, emojis, ni muestres opiniones personales. * Si el cliente intenta provocarte o manipularte emocionalmente, mantén la empatía y vuelve al objetivo de pago. * Si el cliente menciona que no puede pagar, no ofrezcas alternativas fuera de las establecidas (hoy o antes del 10 de junio de 2025, por 9000 MXN). Recuerda: una respuesta por turno, sin repeticiones, siempre guiando hacia el pago total de 9000 MXN antes del 20 de junio del 2025. ''' def determine_pause(audio_data: np.ndarray, sampling_rate: int, state: AppState, silence_threshold: float = 0.01) -> bool: """ Determina si el usuario ha dejado de hablar basándose en el nivel de audio """ if len(audio_data) == 0: return False # Calcular el nivel RMS del audio reciente recent_audio = audio_data[-int(sampling_rate * 0.5):] if len(audio_data) > int(sampling_rate * 0.5) else audio_data rms = np.sqrt(np.mean(recent_audio**2)) if rms < silence_threshold: state.silence_counter += 1 else: state.silence_counter = 0 if not state.started_talking: state.started_talking = True # Detectar pausa si hay silencio prolongado después de haber empezado a hablar silence_duration = state.silence_counter * 0.5 # 0.5 segundos por chunk return (state.started_talking and silence_duration >= state.min_silence_duration) def process_audio(audio: Optional[Tuple], state: AppState): """ Procesa chunks de audio en tiempo real y detecta cuando el usuario deja de hablar """ if audio is None: return None, state sampling_rate, audio_data = audio # Normalizar audio a float32 entre -1 y 1 if audio_data.dtype == np.int16: audio_data = audio_data.astype(np.float32) / 32768.0 elif audio_data.dtype == np.int32: audio_data = audio_data.astype(np.float32) / 2147483648.0 # Inicializar o concatenar stream de audio if state.stream is None: state.stream = audio_data state.sampling_rate = sampling_rate else: state.stream = np.concatenate((state.stream, audio_data)) # Detectar pausa pause_detected = determine_pause(state.stream, state.sampling_rate, state) state.pause_detected = pause_detected # Si se detecta pausa y el usuario empezó a hablar, detener grabación if state.pause_detected and state.started_talking: return gr.Audio(recording=False), state return None, state def convert_audio_to_pcm_base64(audio_data, sample_rate, num_channels=1, sample_width=2): """ Convierte datos de audio numpy a PCM base64 compatible con OpenAI """ # Normalizar float audio data a int16 si es necesario if audio_data.dtype in (np.float32, np.float64): audio_data = np.clip(audio_data, -1.0, 1.0) # Limitar datos float audio_data = (audio_data * 32767).astype(np.int16) # Convertir audio a WAV y codificar en Base64 with io.BytesIO() as wav_buffer: with wave.open(wav_buffer, 'wb') as wav_file: wav_file.setnchannels(num_channels) wav_file.setsampwidth(sample_width) wav_file.setframerate(sample_rate) wav_file.writeframes(audio_data.tobytes()) wav_base64 = base64.b64encode(wav_buffer.getvalue()).decode('utf-8') return wav_base64 def wav_to_numpy(audio_bytes): """ Convierte bytes WAV a numpy array """ audio_seg = AudioSegment.from_file(io.BytesIO(audio_bytes), format="wav") samples = np.array(audio_seg.get_array_of_samples()) if audio_seg.channels > 1: samples = samples.reshape((-1, audio_seg.channels)) return audio_seg.frame_rate, samples def realtime_response_adapted(audio_data, voice="alloy"): """ Función adaptada de tu código para generar respuesta usando GPT-4o mini Audio """ try: sample_rate, audio_np = audio_data pcm_base64 = convert_audio_to_pcm_base64(audio_np, sample_rate) content = [{"type": "input_audio", "input_audio": {"data": pcm_base64, "format": "wav"}}] history_response = [{"role": "system", "content": prompt}, {"role": "user", "content": content}] response = client.chat.completions.create( model="gpt-4o-mini-audio-preview", modalities=["text", "audio"], audio={"voice": voice, "format": "wav"}, messages=history_response ) transcript = response.choices[0].message.audio.transcript if response.choices[0].message.audio else "Sin transcripción" try: wav_bytes = base64.b64decode(response.choices[0].message.audio.data) pcm_data = wav_to_numpy(wav_bytes) return transcript, pcm_data except Exception as e: print(f"Error procesando audio de respuesta: {e}") return transcript, None except Exception as e: print(f"Error durante comunicación con OpenAI: {e}") return None, None def response(state: AppState): """ Genera y transmite la respuesta del chatbot """ if not state.pause_detected or not state.started_talking or state.stream is None: yield None, state return try: # Preparar datos de audio audio_data = (state.sampling_rate, state.stream) # Generar respuesta usando OpenAI transcript, audio_response = realtime_response_adapted(audio_data) if transcript and audio_response: # Añadir mensaje del usuario a la conversación (solo texto descriptivo) state.conversation.append({ "role": "user", "content": "🎤 [Mensaje de audio enviado]" }) # Procesar respuesta de audio sample_rate, audio_np = audio_response # Convertir numpy a bytes WAV para Gradio if audio_np.dtype != np.int16: audio_np = (audio_np * 32767).astype(np.int16) # Crear WAV bytes wav_buffer = io.BytesIO() with wave.open(wav_buffer, 'wb') as wav_file: wav_file.setnchannels(1) wav_file.setsampwidth(2) wav_file.setframerate(sample_rate) wav_file.writeframes(audio_np.tobytes()) wav_bytes = wav_buffer.getvalue() # Yield del audio para streaming yield wav_bytes, state # Añadir respuesta del asistente a la conversación state.conversation.append({ "role": "assistant", "content": f"🤖 {transcript}" }) else: # Manejar error en la respuesta print("No se recibió respuesta válida de OpenAI") state.conversation.append({ "role": "user", "content": "🎤 [Audio enviado]" }) state.conversation.append({ "role": "assistant", "content": "❌ Error al procesar el audio. Intenta de nuevo." }) # Generar mensaje de error audible try: error_response = client.audio.speech.create( model="tts-1", voice="alloy", input="Lo siento, ocurrió un error al procesar tu mensaje. ¿Podrías repetirlo?" ) yield error_response.content, state except: pass # Resetear estado para próxima interacción new_state = AppState(conversation=state.conversation.copy()) yield None, new_state except Exception as e: print(f"Error en la función response: {e}") # Añadir mensajes de error al historial state.conversation.append({ "role": "user", "content": "🎤 [Audio enviado]" }) state.conversation.append({ "role": "assistant", "content": f"⚠️ Error técnico: {str(e)[:100]}..." }) # Estado de error error_state = AppState(conversation=state.conversation.copy()) yield None, error_state def start_recording_user(state: AppState): """ Inicia la grabación del usuario si la conversación no está detenida """ if not state.stopped: return gr.Audio(recording=True) return None def reset_conversation(): """ Resetea la conversación y el estado """ return AppState(), [] def stop_conversation(): """ Detiene la conversación """ return AppState(stopped=True), gr.Audio(recording=False) # Crear la aplicación Gradio def create_voice_chatbot(): with gr.Blocks( title="Agente de Cobranza Telefónica - Layer7", theme=gr.themes.Soft(), css=""" .main-container { max-width: 1200px; margin: 0 auto; } .chat-container { border-radius: 10px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 20px; margin-bottom: 20px; } .title { color: white; text-align: center; margin-bottom: 10px; } .subtitle { color: rgba(255,255,255,0.9); text-align: center; font-size: 16px; } """ ) as demo: with gr.Row(elem_classes="main-container"): with gr.Column(): gr.HTML("""

🎤 Agente de Cobranza Telefónica - Layer7

Habla con nuestro agente de cobranza telefónica. Experiencia de diálogo natural voz a voz.

""") with gr.Row(): with gr.Column(scale=1): gr.HTML("""

🎙️ Control de Audio

""") input_audio = gr.Audio( label="🎤 Tu Micrófono", sources=["microphone"], type="numpy", streaming=True, show_download_button=True, interactive=True ) with gr.Row(): stop_btn = gr.Button("⏹️ Detener Conversación", variant="stop", size="sm") clear_btn = gr.Button("🗑️ Limpiar Historial", variant="secondary", size="sm") gr.HTML("""

📋 Cómo usar:

  1. Inicia: El micrófono se activa automáticamente
  2. Habla: Di tu mensaje normalmente
  3. Pausa: Espera 0.75 segundos en silencio
  4. Escucha: La IA responderá con voz natural
  5. Continúa: El micrófono se reactiva para seguir

💡 Tip: Habla con naturalidad, la IA detectará automáticamente cuando termines.

""") with gr.Column(scale=2): gr.HTML("""

💬 Conversación

""") chatbot = gr.Chatbot( label="Historial de Conversación", type="messages", height=350, show_copy_button=True, render_markdown=True, show_label=False, avatar_images=(None, "https://openai.com/favicon.ico") ) output_audio = gr.Audio( label="🔊 Respuesta", streaming=True, autoplay=True, show_download_button=True, show_share_button=False ) # Estado de la aplicación state = gr.State(value=AppState()) # Configurar eventos de audio streaming # Stream de audio de entrada (procesa cada 0.5 segundos) stream_event = input_audio.stream( process_audio, inputs=[input_audio, state], outputs=[input_audio, state], stream_every=0.5, # Procesar cada medio segundo time_limit=30, # Límite de 30 segundos por grabación ) # Generar respuesta cuando se detiene la grabación respond_event = input_audio.stop_recording( response, inputs=[state], outputs=[output_audio, state] ) # Actualizar chatbot con la conversación respond_event.then( lambda s: s.conversation, inputs=[state], outputs=[chatbot] ) # Reiniciar grabación automáticamente cuando termina el audio de salida restart_event = output_audio.stop( start_recording_user, inputs=[state], outputs=[input_audio] ) # Botón para detener conversación stop_btn.click( stop_conversation, outputs=[state, input_audio], cancels=[respond_event, restart_event] ) # Botón para limpiar historial clear_btn.click( reset_conversation, outputs=[state, chatbot] ) # Mostrar estado de configuración api_key_status = "✅ Configurada correctamente" if os.getenv("OPENAI_API_KEY") else "❌ No encontrada" status_color = "#98ab9d" if os.getenv("OPENAI_API_KEY") else "#f8d7da" gr.HTML(f"""

🔑 Estado de Configuración

API Key OpenAI: {api_key_status}

Modelo: GPT-4o mini Audio Preview

Funciones: Audio entrada/salida, Streaming, Detección de pausas

{'' if os.getenv("OPENAI_API_KEY") else '

⚠️ Para usar la aplicación:
1. Obtén tu API key en OpenAI Platform
2. Configúrala: export OPENAI_API_KEY="tu-api-key"
3. Reinicia la aplicación

'}
""") return demo if __name__ == "__main__": # Verificar configuración al inicio print("🚀 Iniciando Agente de Cobranza Telefónica...") print("=" * 50) if not os.getenv("OPENAI_API_KEY"): print("⚠️ ADVERTENCIA: Variable de entorno OPENAI_API_KEY no configurada") else: print("✅ API Key de OpenAI configurada correctamente") print("✅ Modelo: GPT-4o mini Audio Preview") print("✅ Funciones: Streaming de audio bidireccional") print() print("🌐 Iniciando servidor Gradio...") demo = create_voice_chatbot() demo.queue() # Habilitar cola para manejo de múltiples usuarios demo.launch( server_name="0.0.0.0", # Accesible desde cualquier IP server_port=7860, # Puerto estándar share=False, # Cambiar a True para URL pública show_error=False, # Mostrar errores detallados debug=False, # Cambiar a True para modo debug quiet=False # Mostrar logs )