Skip to content

Commit

Permalink
feat: beta v3 -> sports , promos & syllabuys
Browse files Browse the repository at this point in the history
  • Loading branch information
kaloslazo committed Oct 12, 2024
1 parent 204d064 commit cca15fa
Show file tree
Hide file tree
Showing 11 changed files with 166 additions and 72 deletions.
Binary file modified app/__pycache__/main.cpython-312.pyc
Binary file not shown.
6 changes: 6 additions & 0 deletions app/data/deportes_clean.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Categoría,Deporte,Tiempo de reserva,Lugar,Link para hacer reserva
Recreativo,Futbolin,60 minutos,Piso 2 de la UTEC,https://calendar.google.com/calendar/u/0/appointments/schedules/AcZssZ0_OAgh8NpIn38NfnYgsvgV5zOk4tGA6IFrzO8MWprl6aXYVvRv_ljnZp86_kAS3JiZH-OyV6en
Recreativo,Tenis de mesa,60 minutos,Piso 2 de la UTEC,https://calendar.google.com/calendar/u/0/appointments/schedules/AcZssZ2j0dZ9Xw3KBnhVycpBY-RLq7ZdSTgApXztyZfM3TolvDiukcBd5QxNCLhc0GdqnYCqlFKm5_RB
Recreativo,Futsal,60 minutos,Campo - 1 Bolognesi,https://calendar.google.com/calendar/u/0/appointments/schedules/AcZssZ0_jfRgpmd6O4HX42NFLQMKelWoKw0GARmzpPQ_7yReh1hi19BXnlSSnek16iQehz5BpaVnuEos
Recreativo,Basketball,60 minutos,Campo - 1 Bolognesi,https://calendar.google.com/calendar/u/0/appointments/schedules/AcZssZ2hWi2fOQ5259AbAlO3fVbDMbgcexx0KGFlL-CFFYg9cCUbTMQpumGV3OlODq6WRvp8h1tPdutS
Recreativo,Voley,60 minutos,Campo - 1 Bolognesi,https://calendar.google.com/calendar/u/0/appointments/schedules/AcZssZ04ShjFbXGjOfCAMxyMANGbXZSS9kZVhZuWaVXNpgzOUpmFHKJSRgfL06hca6oB_JXEYdWJc8N5
14 changes: 14 additions & 0 deletions app/data/promos_clean.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
Lugar,Titulo,Descripción
Johny Rockets,¡20% de descuento en platos a la carta!,"Consumo en salón de lunes a viernes, con un máximo de S/50 de descuento. No aplica para llevar, delivery, feriados, ni días festivos. No acumulable con otras promociones. Acepta todos los medios de pago, excepto gifcards y tarjetas de alimentación. Válido hasta el 31/12."
Pejerrey Point,¡20% de descuento en toda la carta!,Presenta tu carnet universitario (sico o virtual). Válido para delivery (WhatsApp 940636455) con costo adicional. Descuento máximo de S/.100.00 por mesa o pedido. No acumulable con otras promociones. Válido de martes a viernes. Sede Barranco. Válido hasta el 03/09/2025.
Rally Kart Benefits,¡40% de descuento en todas las carreras!,"Usa el código código UTEC4024. Válido en Real Plaza Puruchuco, Open Plaza San Miguel, Open Plaza Huancayo, Real Plaza Chiclayo y Jockey Plaza. No acumulable, no reembolsable, ni renovable. Válido solo para compras online en rallykartbenefits.com"
La Tarumba,¡Tarifa plana universitaria y entradas imilitadas!,"Promoción no acumulable, válida solo para zonas Platinum, VIP y Preferencial. No se permiten cambios ni devoluciones una vez emitidas las entradas. Adquiere tus entradas al WhatsApp 970190571."
La Plaza Los Productores,¡Descuento exclusivo en obras de teatro!,"Usa el código CORPORATIVO2024 para obtener un descuento en obras de teatro. Válido solo para precios generales, no aplicable a descuentos previos. Compra tus entradas en www.joinnus.com"
Smart Fit,¡Adquiere el Plan Black por S/109.90 mensual!,"Usa el código UTEC24 para disfrutar de exoneración de matrícula,mantenimiento y penalidad por cancelación anticipada. Válido en todas las sedes a nivel nacional. Conoce más aquí: smartfit.com.pe"
Pirqa,¡Descuento del 20% en escalada libre!,Con un 30% de descuento en el alquiler del equipo. No incluye instructor ni equipo y está dirigida a personas con experiencia. No requiere reserva previa.
Pirqa,¡2x1 en experiencia Pirqa por S/.60!,"Es obligatorio usar zapatillas deportivas o casuales. No se aceptan reprogramaciones ni devoluciones. Requiere de reserva previa, comunícate al 934037599."
Cabify,¡Descuentos especiales en viajes!,Usa el código UTEC24 para obtener un 50% de descuento en los primeros 3 viajes y un 15% en viajes adicionales. El descuento se aplica automáticamente para viajes con inicio o fin en UTEC.
BaldeCash,¡Descuento especial en laptops de alta gama!,Adquiere tu laptop con una cuota inicial accesible y financiamiento a 24 meses. Más detalles en: beneficios.baldecash.com/utec
Ophtalmic Center,¡Disfruta de un 25% de descuento!,"Descuento en lentes oalmológicos, monturas y lentes de sol con el código PROMOUTEC en nuestra web. No aplica para productos de proveedores ni es acumulable con otras promociones. Teléfonos de contacto: 996827080 - 998095302"
Vitapoint Peru,¡Aprovecha hasta un 25% de descuento!,Usa el código UTEC2024. Válido solo para compras online. Sujeto a disponibilidad. No aplica promoción sobre promoción. Encuentra los productos aquí: hps://www.vitapointperu.com/
Ripley,¡40% de descuento en miles de productos!,"Usa el código 2024654739268. Válido en todas las tiendas a nivel nacional hasta el 31/08. Aplica sobre precio original, no válido para compras online. No acumulable con otras promociones ni canjeable por dinero."
6 changes: 3 additions & 3 deletions app/data/syllabus_extracted.csv

Large diffs are not rendered by default.

18 changes: 12 additions & 6 deletions app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,40 @@
from fastapi import FastAPI, Request
from app.services.twilio_service import sendWhatsappMessage
from app.services.chatbot_service import ChatbotService
from openai import OpenAIError
from openai import OpenAIError

app = FastAPI()
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)

chatbot_service = ChatbotService("./app/data/syllabus_extracted.csv")
data_paths = [
"./app/data/syllabus_extracted.csv",
"./app/data/promos_clean.csv",
"./app/data/deportes_clean.csv"
]

chatbot_service = ChatbotService(data_paths)

@app.post("/hook")
async def chat(request: Request):
form_data = await request.form()
body_data = form_data.get("Body", "")
from_phone = form_data.get("From", "")

logger.info(f"Mensaje recibido de {from_phone}: {body_data}")

try:
response = chatbot_service.processMessage(from_phone, body_data)
logger.debug(f"Respuesta generada: {response}")
send_result = sendWhatsappMessage(from_phone, response)
return {"status": "success", "message": send_result}

except OpenAIError as e:
logger.error(f"Error de OpenAI: {str(e)}")
error_message = "📢 Lo siento, estamos experimentando problemas técnicos. Por favor, intenta de nuevo más tarde."
sendWhatsappMessage(from_phone, error_message)
return {"status": "error", "message": "Error de OpenAI", "details": str(e)}

except Exception as e:
logger.error(f"Error inesperado al procesar el mensaje: {str(e)}", exc_info=True)
error_message = "📢 Lo siento, ocurrió un error inesperado. Por favor, intenta de nuevo más tarde."
Expand Down
Binary file modified app/models/__pycache__/qa_model.cpython-312.pyc
Binary file not shown.
107 changes: 74 additions & 33 deletions app/models/qa_model.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import logging
import uuid
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_qdrant import Qdrant
from langchain.chains import ConversationalRetrievalChain
from langchain.prompts import PromptTemplate
from langchain_openai import OpenAI
from langchain_openai import ChatOpenAI
from app.config import config
from qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams, PointStruct
Expand All @@ -12,50 +13,79 @@
logger = logging.getLogger(__name__)

PROMPT_TEMPLATE = """
Eres un asistente virtual para estudiantes de la Universidad de Ingeniería y Tecnología (UTEC). Tu tarea es proporcionar información precisa basada en el contenido de los sílabos de los cursos.
Contexto del sílabo:
Eres un asistente virtual para estudiantes de la Universidad de Ingeniería y Tecnología (UTEC). Tu tarea es proporcionar información precisa y relevante basada en el contenido de los sílabos de los cursos, promociones disponibles y actividades deportivas. Para mejorar la claridad y efectividad de las respuestas, sigue estas directrices estrictamente:
- No uses triple asterisco (*) en ningún caso.
- Cuando quieras colocar doble asterisco (*) pon únicamente uno ().
- Usa un solo asterisco () para resaltar información importante, ejemplo: *nota importante.
- Usa guion bajo () para términos técnicos o conceptos clave, ejemplo: _algoritmo.
- Usa "comillas" para citas textuales, ejemplo: "texto citado".
- Utiliza un solo emoji al final de la respuesta para hacerla más amigable, cuando sea apropiado.
- Usa listas con guiones (-) para enumerar elementos, prohibido usar ** o * en guiones.
- Para títulos o subtítulos, usa un solo asterisco (*) al inicio de la línea, sin asterisco al final.
- Para subtítulos dentro de listas, no uses formato especial, solo el guion (-) al inicio.
Contexto:
{context}
Pregunta del estudiante: {question}
Instrucciones:
1. Usa la información proporcionada en el contexto anterior para responder.
2. Si se pregunta por referencias bibliográficas, busca específicamente una sección llamada "BIBLIOGRÁFICAS" o similar en el contexto.
3. Si encuentras referencias bibliográficas relevantes, menciónalas directamente.
4. Si la información exacta no está en el contexto, pero hay información parcial o relacionada, proporciona esa información y menciona que es parcial.
5. Si no hay absolutamente ninguna información relevante, di "Lo siento, no tengo información específica sobre eso en el sílabo."
6. No inventes ni inferas información que no esté explícitamente en el contexto.
Respuesta basada en la información del sílabo:
1. Identifica el tipo de información solicitada (sílabo, promoción o actividad deportiva).
2. Extrae y usa información directamente del contexto proporcionado para formular la respuesta.
3. Para sílabos:
- Si se pregunta por referencias bibliográficas, menciónalas directamente.
- Incluye detalles relevantes como créditos, modalidad, y otros datos importantes del curso si la pregunta los relaciona.
4. Para promociones:
- Si la pregunta es general, proporciona una lista concisa de categorías y nombres de establecimientos, sin descripciones ni detalles adicionales.
- Usa el formato: "Categoría: Nombre1, Nombre2".
- Limita la respuesta a un máximo de 5 categorías y 3 nombres por categoría.
- Al final, pregunta si el usuario desea más información sobre alguna promoción específica.
5. Para actividades deportivas: [Se mantiene igual]
6. Si la información solicitada no está disponible, responde con: "Lo siento, no tengo información específica sobre eso."
7. Evita inferir información que no esté explícitamente presente en el contexto proporcionado.
8. Asegúrate de que la respuesta sea clara, bien organizada y útil para el estudiante.
9. No usar numeración con #
Respuesta basada en la información proporcionada:
"""

class QAModel:
def __init__(self, texts):
logger.info(f"QAModel inicializado con {len(texts)} documentos")
prompt = PromptTemplate(template=PROMPT_TEMPLATE, input_variables=[
"context", "question"])
prompt = PromptTemplate(template=PROMPT_TEMPLATE, input_variables=["context", "question"])

self.collection_name = config.QDRANT_COLLECTION_NAME
self.embeddings = HuggingFaceEmbeddings(
model_name="sentence-transformers/all-mpnet-base-v2")
self.qdrant_client = QdrantClient(
url=config.QDRANT_URL, api_key=config.QDRANT_API_KEY)
self.embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-mpnet-base-v2")
self.qdrant_client = QdrantClient(url=config.QDRANT_URL, api_key=config.QDRANT_API_KEY)

self.qdrant = Qdrant(
client=self.qdrant_client,
collection_name=self.collection_name,
embeddings=self.embeddings,
)

self.llm = OpenAI(temperature=0.3, max_tokens=300)
self.llm = ChatOpenAI(model_name="gpt-4o-mini", temperature=0.3, max_tokens=300)
self.qa_chain = ConversationalRetrievalChain.from_llm(
llm=self.llm,
retriever=self.qdrant.as_retriever(search_kwargs={"k": 3}),
combine_docs_chain_kwargs={"prompt": prompt},
return_source_documents=True
)

self.clear_collection()
self.load_documents(texts)

def clear_collection(self):
logger.info(f"Limpiando la colección {self.collection_name}")
try:
self.qdrant_client.delete_collection(self.collection_name)
logger.info(f"Colección {self.collection_name} eliminada")
except Exception as e:
logger.warning(f"Error al eliminar la colección: {e}")

self.create_collection_if_not_exists()

def create_collection_if_not_exists(self):
collections = self.qdrant_client.get_collections().collections
if not any(collection.name == self.collection_name for collection in collections):
Expand Down Expand Up @@ -84,16 +114,20 @@ def split_content(self, content, max_length=500):
def load_documents(self, texts):
logger.info(f"Cargando {len(texts)} documentos en Qdrant")
points = []
for i, text in enumerate(texts):
for text in texts:
content = text.page_content
vector = self.embeddings.embed_query(content)
point = PointStruct(
id=str(i),
payload={'text': content, 'metadata': text.metadata},
id=str(uuid.uuid4()), # Generamos un UUID único para cada punto
payload={
'text': content,
'metadata': text.metadata,
'doc_type': text.metadata.get('type', 'unknown')
},
vector=vector
)
points.append(point)

try:
operation_info = self.qdrant_client.upsert(
collection_name=self.collection_name,
Expand All @@ -103,7 +137,7 @@ def load_documents(self, texts):
except Exception as e:
logger.error(f"Error al cargar documentos en Qdrant: {e}")
raise

logger.info("Documentos cargados exitosamente en Qdrant")

def getAnswer(self, question):
Expand All @@ -112,32 +146,39 @@ def getAnswer(self, question):
return "🤔 Por favor, proporciona más detalles para poder ayudarte mejor."
try:
logger.debug("Iniciando búsqueda en Qdrant")

# Realizar la búsqueda directamente en Qdrant

if "promociones" in question.lower() and "universidad" in question.lower():
query_filter = {"must": [{"key": "doc_type", "match": {"value": "promo"}}]}
elif any(word in question.lower() for word in ["deporte", "cancha", "reserva"]):
query_filter = {"must": [{"key": "doc_type", "match": {"value": "deporte"}}]}
else:
query_filter = None

query_vector = self.embeddings.embed_query(question)
search_results = self.qdrant_client.search(
collection_name=self.collection_name,
query_vector=query_vector,
limit=3
query_filter=query_filter,
limit=5
)

full_context = []
for i, result in enumerate(search_results):
logger.debug(f"Documento {i+1}:")
logger.debug(f"ID: {result.id}, Score: {result.score}")
logger.debug(f"Tipo: {result.payload.get('doc_type', 'unknown')}")
logger.debug(f"Contenido: {result.payload['text'][:200]}...")
full_context.append(f"Documento {i+1}:\n{result.payload['text']}")
full_context.append(f"Documento {i+1} ({result.payload.get('doc_type', 'unknown')}):\n{result.payload['text']}")

context = "\n\n".join(full_context)
logger.debug(f"Contexto completo pasado al modelo:\n{context}")

# Usar el contexto completo para generar la respuesta

prompt = self.qa_chain.combine_docs_chain.llm_chain.prompt.format(
context=context,
question=question
)
response = self.qa_chain.combine_docs_chain.llm_chain.llm.predict(prompt)

logger.debug(f"Respuesta generada: {response}")
return response
except Exception as e:
Expand Down
Binary file modified app/services/__pycache__/chatbot_service.cpython-312.pyc
Binary file not shown.
12 changes: 7 additions & 5 deletions app/services/chatbot_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,25 @@
logger = logging.getLogger(__name__)

class ChatbotService:
def __init__(self, data_path):
def __init__(self, data_paths):
logger.info("ChatbotService inicializado")
texts = loadAndSplitData(data_path)
texts = loadAndSplitData(data_paths)
self.qa_model = QAModel(texts)
self.qa_model.test_retrieval("¿Me recomiendas alguna referencia bibliografica del curso de tendencias de mercado?")
self.qa_model.test_retrieval("¿Qué beneficios hay en la categoría de restaurantes?")
self.qa_model.test_retrieval("¿Cómo puedo reservar una cancha de fútbol?")
self.chat_history = {}

def processMessage(self, from_phone, message):
logger.info(f"Procesando mensaje de {from_phone}: {message}")
if from_phone not in self.chat_history: self.chat_history[from_phone] = []

if from_phone not in self.chat_history:
self.chat_history[from_phone] = []

try:
answer = self.qa_model.getAnswer(message)
self.chat_history[from_phone].append((message, answer))
logger.info(f"Respuesta generada para {from_phone}: {answer}")
return answer

except Exception as e:
logger.error(f"Error al procesar mensaje: {str(e)}", exc_info=True)
return "Lo siento, ocurrió un error al procesar tu mensaje. Por favor, intenta de nuevo."
Binary file modified app/utils/__pycache__/data_loader.cpython-312.pyc
Binary file not shown.
Loading

0 comments on commit cca15fa

Please sign in to comment.