import os import requests from fastapi.responses import JSONResponse import openai import sqlite3 import secrets from fastapi import FastAPI from dotenv import load_dotenv from pydantic import BaseModel from llama_index.llms.openai import OpenAI from llama_cloud import ChatMessage, MessageRole from llama_index.core.tools import FunctionTool from llama_index.agent.openai import OpenAIAgent from fastapi.middleware.cors import CORSMiddleware from fastapi.security import HTTPBasic, HTTPBasicCredentials from fastapi import HTTPException, Depends from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_500_INTERNAL_SERVER_ERROR load_dotenv() # Configuración de la API Key de OpenAI y base_url API_KEY = os.getenv("OPENAI_API_KEY") MODEL = os.getenv("OPENAI_MODEL") BASE_URL = os.getenv("OPENAI_BASE_URL") DB_PATH = "./anhelados.db" # Variables de entorno para autenticación básica BASIC_AUTH_USERNAME = os.getenv("BASIC_AUTH_USERNAME") BASIC_AUTH_PASSWORD = os.getenv("BASIC_AUTH_PASSWORD") # Crear una instancia de cliente ajustada para usar OpenAI openai.api_key = API_KEY client = openai.OpenAI(api_key=API_KEY, base_url=BASE_URL) class SQLExecutor: @staticmethod def query(sql_query: str): """ Ejecuta una consulta SQL y devuelve los resultados. Palabras clave, helados, anhelados, sabor de helados, jairo, edson, Cualquier consulta SQL que generes debe resumir tu solicitud en una sola consulta, ejemplo, si deseas saber la cantidad de insumos que tienes en tu almacén debes hacer un conteo de los insumos, en el caso de produccion de las maquinas debes resumir de igual manera la consulta todo con el fin de que la respuesta sea precisa. """ try: with sqlite3.connect(DB_PATH) as conn: cursor = conn.cursor() cursor.execute(sql_query) return cursor.fetchall() except Exception as e: return "No hay registros para mostrar" class ApiPosDiegoWilly: @staticmethod def pos_laura_delgado(sql_query: str): """ Ejecuta una consulta SQL para una base de datos `MYSQL` y devuelve los resultados. Palabra clave ejecutar api POS, laura, delgado, diego, willy o pos_laura_delgado """ respuesta = requests.post( url="https://innovpythia-pos-untels.hf.space/execute", json={"query": sql_query}, ) return respuesta.json() class ApiPosAxel: @staticmethod def academia_axel_victor(sql_query: str): """ Ejecuta una consulta SQL y devuelve los resultados (base de datos SQLITE) referente a la academia. Palabra clave ejecutar api academia, axel, victor, barzola, tinocco o api_axel_victor """ respuesta = requests.post( url="https://innovpythia-acadsh-api.hf.space/execute", json={"query": sql_query}, ) return respuesta.json() # Creación del Agente utilizando la API de OpenAI sql_tool = FunctionTool.from_defaults(fn=SQLExecutor.query) api_laura_delgado = FunctionTool.from_defaults(fn=ApiPosDiegoWilly.pos_laura_delgado) api_axel_victor = FunctionTool.from_defaults(fn=ApiPosAxel.academia_axel_victor) estructura_base_de_datos = ChatMessage( role=MessageRole.USER, content=""" En caso se quiera hacer cualquiera de las funciones de un CRUD con Anhelados esta es la estructura de la base de datos SQLITE de Anhelados y debes usar sql_tool: 1. almacen: ID_Almacen, ID_Insumo, Cantidad_Actual, Cantidad_Minima, Referencia: insumos (ID_Insumo) 2. clientes: ID_Proveedor, Nombre, Contacto, Direccion 3. detalle_pedidos: ID_Pedido, ID_Insumo, Cantidad, Costo_Unitario, Referencias: pedidos_proveedor (ID_Pedido), insumos (ID_Insumo) 4. detalle_ventas: ID_Venta, ID_Producto, Cantidad, Precio_Unitario, Referencias: ventas (ID_Venta), productos (ID_Producto) 5. empleados: ID_Empleado, Nombre, Apellido, Sueldo, Fecha_Inicio 6. gastos: ID_Gasto, Tipo, Monto, Fecha 7. gastos_imprevistos: ID_Gasto_Imprevisto, Descripcion, Monto, Fecha 8. insumos: ID_Insumo, Descripcion, Tipo, Costo, Cantidad 9. maquinas: ID_Maquina, Tipo, Capacidad, Consumo_Energetico 10. pedidos_proveedor: ID_Pedido, ID_Proveedor, Fecha, Total, Referencia: clientes (ID_Proveedor) 11. produccion: ID_Produccion, ID_Maquina, Fecha, Cantidad_Producida, Referencia: maquinas (ID_Maquina) 12. productos: ID_Producto, Nombre, Precio 13. registro_energetico: ID_Registro, ID_Maquina, Fecha, Consumo, Referencia: maquinas (ID_Maquina) 14. ventas: ID_Venta, Fecha, ID_Empleado, Total, Referencia: empleados (ID_Empleado) """, additional_kwargs={"tool": "sql_tool"}, ) mensaje_academia = ChatMessage( role=MessageRole.USER, content="""La base de datos tiene esta estructura SQLite de la Academia: La base de datos diseñada gestiona información sobre becas, estudiantes, profesores, cursos, horarios y evaluaciones. Incluye las siguientes tablas: Becas: Contiene los campos ID_Beca, Nombre_Beca, Descripcion y Monto de cada beca. Estudiantes: Contiene los campos ID_Estudiante, Nombre, Apellido, Fecha_Nacimiento, Direccion y Telefono de cada estudiante. Profesores: Contiene los campos ID_Profesor, Nombre, Apellido, Especialidad y Telefono de cada profesor. Cursos: Relaciona los cursos con los profesores, y contiene los campos ID_Curso, Nombre_Curso, Duracion y ID_Profesor, con una clave foránea que referencia a los profesores. Horarios: Relaciona los cursos con sus horarios, y contiene los campos ID_Horario, ID_Curso, Dia, Hora_Inicio y Hora_Fin, con una clave foránea que referencia a los cursos. Evaluaciones: Registra las evaluaciones de los estudiantes en los cursos, y contiene los campos ID_Evaluacion, ID_Estudiante, ID_Curso, Fecha y Calificacion, con claves foráneas que referencian a estudiantes y cursos.""", additional_kwargs={"tool": "api_axel_victor"}, ) mensaje_phymed = ChatMessage( role=MessageRole.USER, content="""La base de datos tiene esta estructura MYSQL de la empresa Phymed, se centra en vender productos de medicina física y rehabilitación a empresas o personas naturales. La empresa utiliza un POS (Terminal punto de venta). Cuentan con un vendedor en tienda que se encarga de recibir, asesorar y registrar la venta (proforma manual). Luego, la proforma generada pasa al área de almacén donde se registra la boleta (manual) y se despacha al cliente. La base de datos utilizada tiene las siguientes tablas, todas las id(PK) son autoincrementable: - clientes: Contiene los campos de id, nombre, telefono y direccion. - productos: Contiene los campos de id, codigo, nombre, compra, venta, existencias. - productos_ventas: Relaciona 2 tablas productos y ventas para registrar las ventas, cuenta con los siguientes campos id, cantidad, precio, idProducto, idVenta. - usuarios: id, usuario, nombre, telefono, direccion, password. - ventas: id, fecha, total, idUsuario, idCliente. """, additional_kwargs={"tool": "api_laura_delgado"}, ) llm = OpenAI(model=MODEL, api_client=client) agent = OpenAIAgent.from_tools( [sql_tool, api_laura_delgado, api_axel_victor], llm=llm, verbose=True, max_function_calls=20, chat_history=[estructura_base_de_datos, mensaje_academia, mensaje_phymed], system_prompt="Eres un asistente super amable te llamas Chispitas y asistes a la empresa Anhelados, siempre saludas, brindas información puntual y aqui te brindo datos para solucionar consultas del usuario, Ubicaciones en Lima, Perú: San Isidro, Miraflores, Surco y La Molina con horarios de Atencion de 9 AM a 10 PM. Ofrece helados artesanales, milkshakes, postres y bebidas. Promociones regulares y servicio de delivery disponible. Locales Específicos: San Isidro: Av. Javier Prado Este 1234, Tel: (01) 123-4567. Miraflores: Calle Alcanfores 567, Tel: (01) 234-5678. Surco: Av. Caminos del Inca 890, Tel: (01) 345-6789. La Molina: Av. La Molina 345, Tel: (01) 456-7890. Productos Destacados: Helados clásicos como vainilla, chocolate y fresa. Especialidades como lúcuma, maracuyá y chirimoya. Opciones veganas y gourmet. Promociones: 2x1 en helados clásicos los martes. Descuentos en helados gourmet al comprar 3 litros. Servicios Adicionales: Delivery y pedidos personalizados para eventos. Reservas para cumpleaños y reuniones. Siempre termina con un mensaje de agradecimiento por la preferencia. Siempre responde en español, Cualquier consulta SQL que generes debe resumir tu solicitud en una sola consulta, cuando consultan la base de datos haz hasta 3 intentos de llamar a la funcion de ser necesario.", ) def ask_agent(question): response = agent.chat(question) print(response) return str(response) app = FastAPI( title="API de la heladería Anhelados", description="Esta es la API de ChatBot de la microempresa Anhelados, que ofrece helados artesanales, milkshakes, postres y bebidas.", version="3.2.8", openapi_url="/anhelados-openapi.json", docs_url="/anhelados-docs", redoc_url="/anhelados-redoc", contact={ "name": "Anhelados", "url": "https://www.anhelados.com", "email": "contacto@anhelados.com", }, license_info={"name": "MIT", "url": "https://opensource.org/licenses/MIT"}, ) security = HTTPBasic() # CORS app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) def verify_credentials(credentials: HTTPBasicCredentials = Depends(security)): correct_username = secrets.compare_digest(credentials.username, BASIC_AUTH_USERNAME) correct_password = secrets.compare_digest(credentials.password, BASIC_AUTH_PASSWORD) if not (correct_username and correct_password): return JSONResponse( status_code=HTTP_401_UNAUTHORIZED, content={"message": "Autenticación fallida"}, headers={"WWW-Authenticate": "Basic"}, ) return credentials class Item(BaseModel): question: str @app.post("/chat/") async def chat(item: Item, _: HTTPBasicCredentials = Depends(verify_credentials)): try: response = ask_agent(item.question) return {"response": response} except HTTPException as e: raise e except Exception as e: raise HTTPException(status_code=HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e)) @app.get("/") def read_root(): return {"message": "Bienvenido a la API de Anhelados-ChatBot."} # uvicorn main:app --host localhost --port 7860 --reload