openai_service.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. import json
  2. from typing import List, Dict
  3. from fastapi import HTTPException
  4. from openai import OpenAI
  5. from logging import getLogger
  6. from rapidfuzz import process, fuzz # Recomendación: pip install rapidfuzz para búsquedas rápidas
  7. # Asumiendo que estas importaciones existen en tu proyecto
  8. from config.settings import OPENAI_API_KEY
  9. from models.user import User
  10. from services.data_service import data_bg_loaded
  11. from services.openai_service.openai_tools import tools_list, tools
  12. openai_client = OpenAI(api_key=OPENAI_API_KEY)
  13. logger = getLogger(__name__)
  14. # --- OPTIMIZACIÓN 1: MINI-RAG EN MEMORIA ---
  15. # En lugar de enviar TODO, buscamos lo más relevante a la última pregunta.
  16. def get_relevant_context(last_user_message: str, dataset: List[Dict], top_k: int = 5) -> str:
  17. if not last_user_message or not dataset:
  18. return ""
  19. # Extraemos solo las preguntas para buscar coincidencias
  20. questions = [item["q"] for item in dataset]
  21. # Buscamos las 'top_k' preguntas más similares a lo que dijo el usuario
  22. results = process.extract(last_user_message, questions, scorer=fuzz.WRatio, limit=top_k)
  23. # Construimos un contexto limpio en formato Markdown (consume menos tokens que JSON)
  24. context_str = "### INFORMACIÓN RELEVANTE DEL MENÚ:\n"
  25. for _, score, index in results:
  26. if score > 50: # Umbral de relevancia
  27. item = dataset[index]
  28. context_str += f"- P: {item['q']}\n R: {item['ans']}\n"
  29. return context_str
  30. async def generate_completion(messages_array: List[dict], user: User) -> str:
  31. if not OPENAI_API_KEY:
  32. raise HTTPException(status_code=500, detail="OpenAI API key missing.")
  33. # 1. Obtener el último mensaje del usuario para buscar contexto
  34. last_message_content = messages_array[-1].get("message", "") if messages_array else ""
  35. # 2. Filtrar la base de datos (Crucial para escalar)
  36. # Si la DB es gigante, aquí llamarías a tu Vector DB (pgvector).
  37. # Por ahora, usamos el filtro inteligente en memoria.
  38. dynamic_context = get_relevant_context(last_message_content, data_bg_loaded)
  39. # 3. Construir el System Prompt
  40. system_prompt = f"""
  41. ### ROL
  42. Eres IAKlein, el asistente virtual del Bar Klein 🍻. Amigo carismático, breve y experto.
  43. ### DIRECTRICES
  44. - **Estilo:** Chat rápido, emojis, tono relajado.
  45. - **Regla:** NO tomas pedidos. Diles: "Para pedir, Justo en el boton de abajo esta la tienda 😉".
  46. - **Datos:** Usa SOLO la información provista abajo. Si no sabes, dilo con gracia.
  47. {dynamic_context}
  48. """
  49. # 4. Formatear historial correctamente para la API de Chat
  50. # Convertimos tu array de dicts al formato nativo de OpenAI
  51. api_messages = [{"role": "system", "content": system_prompt}]
  52. # Tomamos los últimos 10 mensajes para mantener el contexto ligero
  53. for msg in messages_array[-10:]:
  54. role = "user" # Asumimos user por defecto, ajusta según tu lógica si tienes mensajes del bot guardados
  55. content = f"<{msg.get('username', 'User')}> {msg.get('message', '')}"
  56. api_messages.append({"role": role, "content": content})
  57. try:
  58. # Primera llamada al modelo
  59. completion = openai_client.chat.completions.create(
  60. model="gpt-4o-mini",
  61. messages=api_messages,
  62. temperature=0.7,
  63. tools=tools_list,
  64. tool_choice="auto",
  65. )
  66. message = completion.choices[0].message
  67. # --- OPTIMIZACIÓN 2: MANEJO DE TOOLS ROBUSTO ---
  68. if message.tool_calls:
  69. logger.info(f"Tool calls detected: {message.tool_calls}")
  70. # Agregamos la intención de llamada al historial
  71. api_messages.append(message)
  72. for tool_call in message.tool_calls:
  73. function_name = tool_call.function.name
  74. if function_name in tools:
  75. # Ejecutar herramienta
  76. func_args = json.loads(tool_call.function.arguments)
  77. tool_result = tools[function_name](name=user.name, email=user.email, **func_args)
  78. # Agregar el resultado de la herramienta al historial
  79. api_messages.append({
  80. "role": "tool",
  81. "tool_call_id": tool_call.id,
  82. "content": str(tool_result)
  83. })
  84. else:
  85. logger.warning(f"Tool {function_name} not found.")
  86. # SEGUNDA LLAMADA: Para que la IA interprete el resultado de la tool y responda al usuario
  87. final_completion = openai_client.chat.completions.create(
  88. model="gpt-4o-mini",
  89. messages=api_messages
  90. )
  91. return final_completion.choices[0].message.content
  92. return message.content
  93. except Exception as e:
  94. logger.error(f"OpenAI Error: {e}")
  95. return "¡Ups! Se me cayó la bandeja con las cervezas. Pregúntame de nuevo en un segundo. 🍻"
  96. def admin_completion(prompt: str, messages_array: List[dict]) -> str:
  97. """Generate OpenAI admin completion"""
  98. messages = list(map(lambda x: f'<{x.get("username", "unknown")}> {x.get("message", "")}', messages_array))
  99. if not OPENAI_API_KEY:
  100. logger.error("Error: OpenAI API key is not configured.")
  101. raise HTTPException(status_code=500, detail="OpenAI API key not configured on server.")
  102. try:
  103. completion = openai_client.chat.completions.create(
  104. model="gpt-4o-mini",
  105. messages=[
  106. {"role": "system", "content": f"""
  107. IAKlein es el asistente oficial del bar Klein 🍻 y siempre se comunica en un estilo de chat corto, tipo mensajería o IRC, con emojis y mucho carisma. Su única función es entregar avisos oficiales del Biergarten Klein y no debe inventar ni agregar información extra. Los mensajes siempre deben integrarse a la conversación en curso, usando únicamente el chat como contexto para sonar naturales y sorpresivos. Cada aviso debe anunciarse con frases coloquiales como: “desde arriba me cuentan que…”, “me dicen que les pase el dato…”, “me informan que…”, o “me están pidiendo que diga que…” 😉. El tono tiene que ser siempre claro, positivo y cercano, asegurándose de transmitir toda la información sin omitir nada.
  108. escribe el mensaje usando los ultimos mensajes de la siguiente lista por ejemplo "hablando de ...(conexion con el tema).. me dicen que...(mensaje oficial)..." o "me cuentan que...(mensaje oficial)..., hablando de ...(conexion con el tema)..."
  109. mensajes: {messages}"""},
  110. {"role": "user", "content": prompt}
  111. ],
  112. temperature=0.7,
  113. )
  114. response_content = completion.choices[0].message.content
  115. return response_content if response_content else "-1"
  116. except Exception as e:
  117. logger.error(f"Error calling OpenAI for admin completion: {e}")
  118. raise HTTPException(status_code=500, detail="Error al procesar tu solicitud con OpenAI.")