main.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  1. import csv
  2. import os
  3. import json
  4. import secrets
  5. from typing import List, Dict, Union, Annotated
  6. from fastapi import FastAPI, Request, HTTPException, Header, Depends
  7. from fastapi.responses import HTMLResponse, JSONResponse, FileResponse
  8. from fastapi.staticfiles import StaticFiles
  9. from pydantic import BaseModel
  10. from openai import OpenAI
  11. from dotenv import load_dotenv
  12. from starlette.middleware.sessions import SessionMiddleware
  13. from impresora.printer import PrinterUSB
  14. from impresora.order import *
  15. import smtplib
  16. from email.message import EmailMessage
  17. # Load environment variables from .env file
  18. load_dotenv()
  19. import fudo.fudo as fd
  20. # Configuration
  21. OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
  22. PORT = int(os.getenv("PORT", 6001))
  23. EXCLUDED_BEER_IDS = [14, 12, 11];
  24. # SECRET_KEY is crucial for signing session cookies.
  25. # Fallback to a default if not set, but warn that this is insecure for production.
  26. SECRET_KEY = os.getenv("SECRET_KEY", "your_very_very_secret_key_for_signing_cookies_python_v2")
  27. if SECRET_KEY == "your_very_very_secret_key_for_signing_cookies_python_v2":
  28. print("WARNING: Using default SECRET_KEY. Please set a strong SECRET_KEY in your .env file for production.")
  29. if not OPENAI_API_KEY:
  30. print("CRITICAL ERROR: OPENAI_API_KEY environment variable not set. The applicaton will not work correctly.")
  31. # Potentially exit or prevent app startup if critical env var is missing
  32. # raise ValueError("OPENAI_API_KEY is not set, cannot start application.")
  33. # --- FastAPI App Initialization ---
  34. app = FastAPI(title="Web Pedidos Klein - FastAPI Backend")
  35. # Add SessionMiddleware
  36. # This middleware adds session support using signed cookies.
  37. # Original Express maxAge was 1 hour (60 * 60 * 1000 ms)
  38. app.add_middleware(
  39. SessionMiddleware,
  40. secret_key=SECRET_KEY,
  41. max_age=60 * 60 # max_age in seconds for Starlette
  42. )
  43. # --- Data Loading ---
  44. # Assumes data.json is in the same directory as main.py
  45. # The original path was web_pedidos/src/data.json
  46. # For the Python version, copy src/data.json to be alongside main.py
  47. BG_DATA_PATH = os.path.join(os.path.dirname(__file__), 'data.json')
  48. PRODUCTS_PATH = os.path.join(os.path.dirname(__file__), 'products.json')
  49. def add_product_to_fudo(product_id: int, quantity: int, table_number:int, comment: str | None = None):
  50. table = fd.get_table(table_number)
  51. if not table:
  52. print(f"Error: Table {table_number} not found.")
  53. return None
  54. activeSale = fd.get_active_sale(table)
  55. if not activeSale:
  56. activeSale = fd.create_sale(table['id'])
  57. if not activeSale:
  58. print(f"Error: Could not create sale for table {table_number}.")
  59. return None
  60. item = fd.create_item(product_id, quantity, activeSale['id'], comment)
  61. if not item:
  62. print(f"Error: Could not create item for product {product_id}.")
  63. return None
  64. return item
  65. def send_email():
  66. # Datos del remitente
  67. EMAIL_ORIGEN = 'expresspedidos211@gmail.com'
  68. EMAIL_DESTINO = ['erwinjacimino2003@gmail.com', "mompyn@gmail.com"]
  69. CONTRASENA = 'drkassszdtgapufg'
  70. # Crear el correo
  71. msg = EmailMessage()
  72. msg['Subject'] = 'Impresora Desconectada weon :('
  73. msg['From'] = EMAIL_ORIGEN
  74. msg['To'] = ", ".join(EMAIL_DESTINO)
  75. msg.set_content('Este correo tiene contenido HTML.')
  76. msg.add_alternative("""
  77. <html>
  78. <body style="margin:0; padding:0; background-color:#5a67d8; font-family: Arial, sans-serif;">
  79. <table align="center" border="0" cellpadding="0" cellspacing="0" width="100%" style="padding: 40px 0;">
  80. <tr>
  81. <td align="center">
  82. <table border="0" cellpadding="0" cellspacing="0" width="500" style="background-color: #e3e3e3; border-radius: 25px; padding: 40px; text-align: center;">
  83. <tr>
  84. <td>
  85. <div style="font-size: 60px; background-color: #ff6b6b; width: 80px; height: 80px; line-height: 80px; border-radius: 15px; margin: 0 auto 20px; color: white;">
  86. 🖨️
  87. </div>
  88. </td>
  89. </tr>
  90. <tr>
  91. <td>
  92. <img src="https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExZGlraDhpb2tkeHEweDZ2eWdnZDZlNXFvODhmNzZieWN6OXp0b3ZqNCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3hetVnNSl0IBa/giphy.gif" alt="Gatito peleando con la impresora" width="250" style="border-radius: 12px; margin-bottom: 20px;" />
  93. </td>
  94. </tr>
  95. <tr>
  96. <td>
  97. <h1 style="font-size: 24px; color: #ff6b6b; margin-bottom: 10px;">¡Impresora Desconectada!</h1>
  98. </td>
  99. </tr>
  100. <tr>
  101. <td>
  102. <p style="font-size: 16px; color: #333333; line-height: 1.5; margin-bottom: 20px;">
  103. No se puede establecer conexión con la impresora.<br>
  104. Por favor, verifica la conexión y vuelve a intentarlo.
  105. </p>
  106. </td>
  107. </tr>
  108. <tr>
  109. <td>
  110. <span style="display: inline-block; background: #ff6b6b; color: white; padding: 12px 24px; border-radius: 25px; font-weight: bold; font-size: 16px;">
  111. 🔴 Estado: Desconectada
  112. </span>
  113. </td>
  114. </tr>
  115. </table>
  116. </td>
  117. </tr>
  118. </table>
  119. </body>
  120. </html>
  121. """, subtype='html')
  122. # Enviar el correo usando SMTP de Gmail
  123. with smtplib.SMTP_SSL('smtp.gmail.com', 465) as smtp:
  124. smtp.login(EMAIL_ORIGEN, CONTRASENA)
  125. smtp.send_message(msg)
  126. def load_bg_data() -> List[Dict[str, str]]:
  127. try:
  128. with open(BG_DATA_PATH, 'r', encoding='utf-8') as f:
  129. return json.load(f)
  130. except FileNotFoundError:
  131. print(f"ERROR: Data file not found at {BG_DATA_PATH}. Serving with empty data.")
  132. return []
  133. except json.JSONDecodeError:
  134. print(f"ERROR: Could not decode JSON from {BG_DATA_PATH}. Serving with empty data.")
  135. return []
  136. def load_products() -> List[Dict[str, str]]:
  137. try:
  138. with open(PRODUCTS_PATH, 'r', encoding='utf-8') as f:
  139. return list(filter(lambda product: product['id'] not in EXCLUDED_BEER_IDS, json.load(f)))
  140. except FileNotFoundError:
  141. print(f"ERROR: Data file not found at {PRODUCTS_PATH}. Serving with empty data.")
  142. return []
  143. except json.JSONDecodeError:
  144. print(f"ERROR: Could not decode JSON from {PRODUCTS_PATH}. Serving with empty data.")
  145. return []
  146. bg_data_loaded = load_bg_data()
  147. all_products = load_products()
  148. # region --- Pydantic Models for Request/Response Typing ---
  149. class Message(BaseModel):
  150. role: str
  151. content: str
  152. class ChatCompletionRequest(BaseModel):
  153. messages: List[Message]
  154. user: str
  155. class ItemWeb(BaseModel):
  156. id: int
  157. name: str
  158. quantity: int
  159. price: float
  160. itemTotal: float
  161. class OrderWeb(BaseModel):
  162. customerName: str
  163. items: List[ItemWeb]
  164. totalAmount: float
  165. orderDate: str
  166. table: int
  167. # endregion --- Pydantic Models for Request/Response Typing ---
  168. # region --- OpenAI Service Logic ---
  169. openai_client = OpenAI(api_key=OPENAI_API_KEY)
  170. async def generate_completion(messages_array: List[Message], session_id: str) -> str:
  171. if not OPENAI_API_KEY:
  172. print("Error: OpenAI API key is not configured.")
  173. raise HTTPException(status_code=500, detail="OpenAI API key not configured on server.")
  174. print(f"[OpenAI Service Python] Session/Token {session_id} sent: {[msg.model_dump() for msg in messages_array]}")
  175. data_for_prompt = [
  176. f'{{"pregunta": "{item.get("q", "")}", "respuesta": "{item.get("ans", "")}"}}'
  177. for item in bg_data_loaded
  178. ]
  179. data_string = "\n".join(data_for_prompt)
  180. preprompt = f"""
  181. Eres un asistente de el bar klein, tu nombre es camilo klein, usas emojis para responder.
  182. y ser carismatico con el cliente.
  183. tus responsabilidades son:
  184. - Responder preguntas sobre el menu de el bar klein
  185. - Proporcionar información sobre el menú de el bar klein
  186. - Proporcionar recomendaciones sobre el menú de el bar klein
  187. - Proporcionar información sobre la comida de el bar klein
  188. - No puedes tomar pedidos de clientes, solo informar
  189. - Debes evadir cualquier pregunta que no sea relacionada con el bar klein
  190. para esto usaras los siguientes datos:
  191. {data_string}
  192. """ #
  193. processed_messages: List[Dict[str, str]] = [{"role": "system", "content": preprompt}]
  194. processed_messages.extend([msg.model_dump() for msg in messages_array])
  195. try:
  196. completion = openai_client.chat.completions.create(
  197. model="gpt-4o-mini", #
  198. messages=processed_messages, # type: ignore (OpenAI lib expects list of specific dicts)
  199. temperature=0.3, #
  200. )
  201. response_content = completion.choices[0].message.content
  202. return response_content if response_content else "-1" #
  203. except Exception as e:
  204. print(f"Error calling OpenAI: {e}")
  205. # Avoid exposing detailed error messages to the client unless necessary
  206. raise HTTPException(status_code=500, detail="Error al procesar tu solicitud con OpenAI.")
  207. # endregion --- OpenAI Service Logic ---
  208. # --- Security/Token Dependency ---
  209. async def get_session_token(request: Request) -> Union[str, None]:
  210. return request.session.get("antiAbuseToken")
  211. async def protect_chat_api(
  212. request: Request,
  213. x_app_token: Annotated[Union[str, None], Header(alias="X-App-Token")] = None,
  214. session_token: Annotated[Union[str, None], Depends(get_session_token)] = None
  215. ):
  216. # Equivalent to protectChatAPI middleware
  217. if not session_token:
  218. raise HTTPException(status_code=403, detail="Acceso denegado: Sesión inválida o token no inicializado.")
  219. if not x_app_token:
  220. raise HTTPException(status_code=401, detail="Acceso denegado: Falta el token X-Chat-Token.")
  221. if x_app_token != session_token:
  222. # Log this attempt for security monitoring
  223. print(f"WARN: Invalid token attempt. Expected: {session_token}, Received: {x_app_token}")
  224. raise HTTPException(status_code=403, detail="Acceso denegado: Token inválido.")
  225. return True # Protection passed
  226. @app.get("/api/get_products", summary="Get products")
  227. async def get_products():
  228. return JSONResponse({"products": all_products})
  229. # --- API Endpoints ---
  230. @app.get("/api/chat/init-chat", summary="Initialize chat and get anti-abuse token")
  231. async def init_chat(request: Request):
  232. current_token = request.session.get("antiAbuseToken")
  233. if not current_token:
  234. new_token = secrets.token_hex(32)
  235. request.session["antiAbuseToken"] = new_token # Store in session
  236. print(f"Generated new antiAbuseToken for session: {new_token}")
  237. return JSONResponse({"chatToken": new_token})
  238. else:
  239. # print(f"Using existing antiAbuseToken for session: {current_token}")
  240. return JSONResponse({"chatToken": current_token})
  241. class UserCodeRequest(BaseModel):
  242. user_code: str
  243. @app.post("/api/existsUser", summary="Check if user exists")
  244. async def exists_user(request: UserCodeRequest):
  245. with open('users.json', 'r') as f:
  246. users = json.load(f)
  247. for user in users:
  248. if user['userCode'] == request.user_code:
  249. return JSONResponse({
  250. "success": True,
  251. "userName": user['userName']
  252. })
  253. return JSONResponse({
  254. "success": True,
  255. "userName": request.user_code
  256. })
  257. @app.post("/api/printer/order", summary="Printer order", dependencies=[Depends(protect_chat_api)])
  258. async def printer_order(order: OrderWeb):
  259. print("Printer order received")
  260. print(order)
  261. items = order.items
  262. table = order.table
  263. if not items or not table:
  264. return JSONResponse(status_code=400, content={"message": "Items and table are required."})
  265. if not isinstance(table, int):
  266. return JSONResponse(status_code=400, content={"message": "Table must be an integer."})
  267. product_errors = []
  268. for item in items:
  269. product = add_product_to_fudo(item.id, item.quantity, table)
  270. if not product:
  271. product_errors.append(f"Error adding product {item.id} to table {table}.")
  272. if product_errors:
  273. return JSONResponse(status_code=424, content={"message": "Error adding products to table.", "errors": product_errors})
  274. # en caso de que no alla error, imprimimos el pedido
  275. printer = PrinterUSB(0xfe6,0x811e)
  276. print_order = Order(order.customerName,[Item(item.name, item.price, item.quantity) for item in items])
  277. try:
  278. printer.print_order(print_order, table)
  279. except:
  280. #Si la impresora no esta conectada, enviamos un correo
  281. send_email()
  282. return JSONResponse(status_code=424, content={"message": "No se pudo imprimir el Pedido, impresora desconectada"})
  283. # Logs de pedidos
  284. if not os.path.exists('logs.csv'):
  285. with open('logs.csv', 'w', newline='') as f:
  286. writer = csv.writer(f)
  287. writer.writerow(['userName', 'table', 'orderDate', 'items'])
  288. else:
  289. with open('logs.csv', 'a', newline='') as f:
  290. writer = csv.writer(f)
  291. writer.writerow([order.customerName, order.table, order.orderDate, list(map(lambda item: item.name, items))])
  292. @app.post("/api/chat/completions",
  293. summary="Get chat completions from OpenAI",
  294. dependencies=[Depends(protect_chat_api)])
  295. async def chat_completions(request_data: ChatCompletionRequest, request: Request):
  296. # Uses session_token (which is the antiAbuseToken) as an identifier for logging
  297. session_identifier = request.session.get("antiAbuseToken", "unknown_session")
  298. try:
  299. openai_response = await generate_completion(request_data.messages, session_identifier)
  300. if os.path.exists("llm_logs.txt"):
  301. with open("llm_logs.txt", "a") as f:
  302. f.write(f"{request_data.user}: {openai_response}\n")
  303. else:
  304. with open("llm_logs.txt", "w") as f:
  305. f.write(f"{request_data.user}: {openai_response}\n")
  306. return JSONResponse({"response": openai_response})
  307. except HTTPException as e: # Re-raise HTTPExceptions from called functions
  308. raise e
  309. except Exception as e:
  310. print(f"Unexpected error in /api/chat/completions: {e}")
  311. raise HTTPException(status_code=500, detail="Error interno del servidor al procesar el chat.")
  312. @app.get("/", response_class=HTMLResponse, include_in_schema=False)
  313. async def serve_index_html():
  314. index_path = os.path.join("public", "index.html")
  315. if not os.path.exists(index_path):
  316. raise HTTPException(status_code=404, detail="public/index.html not found.")
  317. return FileResponse(index_path)
  318. app.mount("/", StaticFiles(directory="public", html=False), name="public_root_assets")
  319. # --- Main Application Runner ---
  320. if __name__ == "__main__":
  321. if not OPENAI_API_KEY:
  322. print("FATAL: OPENAI_API_KEY is not set. OpenAI features will fail.")
  323. print("Please create a .env file with OPENAI_API_KEY='your_key_here'")
  324. with open(".env", "w") as f:
  325. f.write("OPENAI_API_KEY='your_key_here'")
  326. print(f"Servidor corriendo en http://localhost:{PORT}")
  327. if not os.path.exists(BG_DATA_PATH):
  328. print(f"ADVERTENCIA: {BG_DATA_PATH} no encontrado. El asistente de IA no tendrá datos específicos del menú.")
  329. else:
  330. print(f"Datos del asistente cargados desde: {os.path.abspath(BG_DATA_PATH)}")
  331. import uvicorn
  332. uvicorn.run(app, host="0.0.0.0", port=PORT)