feat: Audit, repair, and stabilize bot architecture

This commit addresses critical issues in the Talia Bot system, including fixing missing admin flows, correcting agenda privacy logic, implementing voice message transcription, and enforcing RAG guardrails.

Key changes include:
- Modified the onboarding module to dynamically generate admin menus from available JSON flows, making all admin functions accessible.
- Updated the agenda module to correctly use separate work and personal Google Calendar IDs, ensuring privacy and accurate availability.
- Implemented audio transcription using the OpenAI Whisper API, replacing placeholder logic and enabling multimodal interaction.
- Reworked the sales RAG module to prevent it from generating generic responses when it lacks sufficient context.

Additionally, this commit introduces comprehensive documentation as requested:
- `AGENTS.md`: Defines the roles and responsibilities of each system agent.
- `Agent_skills.md`: Details the technical capabilities and business rules for each agent.
- `plan_de_pruebas.md`: Provides a step-by-step test plan to verify the fixes.
- `reparacion_vs_refactor.md`: Outlines the immediate repairs performed and proposes a strategic, incremental plan for long-term architectural improvements.
This commit is contained in:
google-labs-jules[bot]
2025-12-21 20:29:55 +00:00
parent 9dc13dacb1
commit 81efa4babd
11 changed files with 571 additions and 39 deletions

View File

@@ -1,7 +1,8 @@
{
"id": "admin_idea_capture",
"role": "admin",
"trigger_button": "💡 Capturar Idea",
"name": "💡 Capturar Idea",
"trigger_button": "capture_idea",
"steps": [
{
"step_id": 0,

View File

@@ -1,7 +1,8 @@
{
"id": "admin_print_file",
"role": "admin",
"trigger_button": "🖨️ Imprimir",
"name": "🖨️ Imprimir Archivo",
"trigger_button": "print_file",
"steps": [
{
"step_id": 0,

View File

@@ -33,6 +33,7 @@ from talia_bot.modules.vikunja import vikunja_conv_handler, get_projects_list, g
from talia_bot.modules.printer import send_file_to_printer, check_print_status
from talia_bot.db import setup_database
from talia_bot.modules.flow_engine import FlowEngine
from talia_bot.modules.llm_engine import transcribe_audio
from talia_bot.scheduler import schedule_daily_summary
@@ -101,7 +102,7 @@ async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
logger.info(f"Usuario {chat_id} inició conversación con el rol: {user_role}")
# Obtenemos el texto y los botones de bienvenida desde el módulo de onboarding
response_text, reply_markup = onboarding_handle_start(user_role)
response_text, reply_markup = onboarding_handle_start(user_role, flow_engine)
# Respondemos al usuario
await update.message.reply_text(response_text, reply_markup=reply_markup)
@@ -120,9 +121,25 @@ async def text_and_voice_handler(update: Update, context: ContextTypes.DEFAULT_T
user_response = update.message.text
if update.message.voice:
# Here you would add the logic to transcribe the voice message
# For now, we'll just use a placeholder
user_response = "Voice message received (transcription not implemented yet)."
voice = update.message.voice
temp_dir = 'temp_files'
os.makedirs(temp_dir, exist_ok=True)
file_path = os.path.join(temp_dir, f"{voice.file_id}.ogg")
try:
voice_file = await context.bot.get_file(voice.file_id)
await voice_file.download_to_drive(file_path)
logger.info(f"Voice message saved to {file_path}")
user_response = transcribe_audio(file_path)
logger.info(f"Transcription result: '{user_response}'")
except Exception as e:
logger.error(f"Error during voice transcription: {e}")
user_response = "Error al procesar el mensaje de voz."
finally:
if os.path.exists(file_path):
os.remove(file_path)
result = flow_engine.handle_response(user_id, user_response)

View File

@@ -5,12 +5,14 @@
import datetime
import logging
from talia_bot.modules.calendar import get_events
from talia_bot.config import WORK_GOOGLE_CALENDAR_ID, PERSONAL_GOOGLE_CALENDAR_ID
logger = logging.getLogger(__name__)
async def get_agenda():
"""
Obtiene y muestra la agenda del usuario para el día actual desde Google Calendar.
Diferencia entre eventos de trabajo (visibles) y personales (bloqueos).
"""
try:
logger.info("Obteniendo agenda...")
@@ -18,24 +20,34 @@ async def get_agenda():
start_of_day = now.replace(hour=0, minute=0, second=0, microsecond=0)
end_of_day = start_of_day + datetime.timedelta(days=1)
logger.info(f"Buscando eventos desde {start_of_day} hasta {end_of_day}")
events = get_events(start_of_day, end_of_day)
logger.info(f"Buscando eventos de trabajo en {WORK_GOOGLE_CALENDAR_ID} y personales en {PERSONAL_GOOGLE_CALENDAR_ID}")
if not events:
logger.info("No se encontraron eventos.")
return "📅 *Agenda para Hoy*\n\nNo tienes eventos programados para hoy."
# Obtener eventos de trabajo (para mostrar)
work_events = get_events(start_of_day, end_of_day, calendar_id=WORK_GOOGLE_CALENDAR_ID)
# Obtener eventos personales (para comprobar bloqueos, no se muestran)
personal_events = get_events(start_of_day, end_of_day, calendar_id=PERSONAL_GOOGLE_CALENDAR_ID)
if not work_events and not personal_events:
logger.info("No se encontraron eventos de ningún tipo.")
return "📅 *Agenda para Hoy*\n\nTotalmente despejado. No hay eventos de trabajo ni personales."
agenda_text = "📅 *Agenda para Hoy*\n\n"
for event in events:
start = event["start"].get("dateTime", event["start"].get("date"))
# Formatear la hora si es posible
if "T" in start:
time_str = start.split("T")[1][:5]
else:
time_str = "Todo el día"
summary = event.get("summary", "(Sin título)")
agenda_text += f"• *{time_str}* - {summary}\n"
if not work_events:
agenda_text += "No tienes eventos de trabajo programados para hoy.\n"
else:
for event in work_events:
start = event["start"].get("dateTime", event["start"].get("date"))
if "T" in start:
time_str = start.split("T")[1][:5]
else:
time_str = "Todo el día"
summary = event.get("summary", "(Sin título)")
agenda_text += f"• *{time_str}* - {summary}\n"
if personal_events:
agenda_text += "\n🔒 Tienes tiempo personal bloqueado."
logger.info("Agenda obtenida con éxito.")
return agenda_text

View File

@@ -32,3 +32,25 @@ def get_smart_response(prompt):
except Exception as e:
# Si algo sale mal, devolvemos el error
return f"Ocurrió un error al comunicarse con OpenAI: {e}"
def transcribe_audio(audio_file_path):
"""
Transcribes an audio file using OpenAI's Whisper model.
Parameters:
- audio_file_path: The path to the audio file.
"""
if not OPENAI_API_KEY:
return "Error: OPENAI_API_KEY is not configured."
try:
client = openai.OpenAI(api_key=OPENAI_API_KEY)
with open(audio_file_path, "rb") as audio_file:
transcript = client.audio.transcriptions.create(
model="whisper-1",
file=audio_file
)
return transcript.text
except Exception as e:
return f"Error during audio transcription: {e}"

View File

@@ -4,14 +4,22 @@
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
def get_admin_menu():
def get_admin_menu(flow_engine):
"""Crea el menú de botones principal para los Administradores."""
keyboard = [
[InlineKeyboardButton("👑 Revisar Pendientes", callback_data='view_pending')],
[InlineKeyboardButton("📅 Agenda", callback_data='view_agenda')],
[InlineKeyboardButton(" NFC", callback_data='start_create_tag')],
[InlineKeyboardButton("▶️ Más opciones", callback_data='admin_menu')],
]
# Dynamic buttons from flows
if flow_engine:
for flow in flow_engine.flows:
if flow.get("role") == "admin" and "trigger_button" in flow and "name" in flow:
button = InlineKeyboardButton(flow["name"], callback_data=flow["trigger_button"])
keyboard.append([button])
keyboard.append([InlineKeyboardButton("▶️ Más opciones", callback_data='admin_menu')])
return InlineKeyboardMarkup(keyboard)
def get_admin_secondary_menu():
@@ -41,14 +49,14 @@ def get_client_menu():
]
return InlineKeyboardMarkup(keyboard)
def handle_start(user_role):
def handle_start(user_role, flow_engine=None):
"""
Decide qué mensaje y qué menú mostrar según el rol del usuario.
"""
welcome_message = "Hola, soy Talía. ¿En qué puedo ayudarte hoy?"
if user_role == "admin":
menu = get_admin_menu()
menu = get_admin_menu(flow_engine)
elif user_role == "crew":
menu = get_crew_menu()
else:

View File

@@ -41,19 +41,19 @@ def generate_sales_pitch(user_query, collected_data):
relevant_services = find_relevant_services(user_query, services)
if not relevant_services:
# Fallback to all services if no specific keywords match
context_str = "Aquí hay una descripción general de nuestros servicios:\n"
for service in services:
context_str += f"- **{service['service_name']}**: {service['description']}\n"
else:
context_str = "Según tus necesidades, aquí tienes algunos de nuestros servicios y ejemplos de lo que podemos hacer:\n"
for service in relevant_services:
context_str += f"\n**Servicio:** {service['service_name']}\n"
context_str += f"*Descripción:* {service['description']}\n"
if "work_examples" in service:
context_str += "*Ejemplos de trabajo:*\n"
for example in service["work_examples"]:
context_str += f" - {example}\n"
logger.warning(f"No se encontraron servicios relevantes para la consulta: '{user_query}'. No se generará respuesta.")
return ("Gracias por tu interés. Sin embargo, con la información proporcionada no he podido identificar "
"servicios específicos que se ajusten a tu necesidad. ¿Podrías describir tu proyecto con otras palabras "
"o dar más detalles sobre lo que buscas?")
context_str = "Según tus necesidades, aquí tienes algunos de nuestros servicios y ejemplos de lo que podemos hacer:\n"
for service in relevant_services:
context_str += f"\n**Servicio:** {service['service_name']}\n"
context_str += f"*Descripción:* {service['description']}\n"
if "work_examples" in service:
context_str += "*Ejemplos de trabajo:*\n"
for example in service["work_examples"]:
context_str += f" - {example}\n"
prompt = (
f"Eres Talía, una asistente de ventas experta y amigable. Un cliente potencial llamado "