""" Web entry point of the chatbot — Flask server with an e-commerce interface. Replaces the terminal loop of main.py with a 3-panel web interface: - Product showcase (cards with thumbnail) - Assistant chat (message bubbles) - Debug terminal (real-time agent logs) Endpoints: - GET / -> main HTML page - POST /api/chat -> sends a message to the graph, returns JSON - POST /api/upload -> uploads an image for visual search - GET /images/ -> serves a thumbnail from artifacts/images/ """ import io import os import sys import tempfile import contextlib import uuid os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE" from flask import Flask, request, jsonify, render_template, send_from_directory from langchain_core.messages import HumanMessage from utils.config import DEFAULT_THREAD_ID, IMAGES_DIR from utils.i18n import t from graph import app as langgraph_app flask_app = Flask(__name__) # Disable cache for development (templates update without restarting) flask_app.config["SEND_FILE_MAX_AGE_DEFAULT"] = 0 # LangGraph session. The thread_id is regenerated on each page load # (GET /), so a browser refresh resets the context and starts # a new conversation. Within the same page the multi-turn context # stays active. _session = {"thread_id": DEFAULT_THREAD_ID} @flask_app.route("/") def index(): """Serves the main HTML page. Each load/refresh generates a new thread_id, resetting the conversational context: it restarts from a clean search. """ _session["thread_id"] = uuid.uuid4().hex response = flask_app.make_response(render_template("index.html")) response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate" return response @flask_app.route("/api/chat", methods=["POST"]) def chat(): """Receives a message, passes it to the LangGraph graph, returns structured JSON.""" data = request.get_json() message = data.get("message", "").strip() if not message: return jsonify({"error": t("api_empty_message")}), 400 return _invoke_graph(message) @flask_app.route("/api/upload", methods=["POST"]) def upload(): """Receives an image, saves it to temp, passes it to the graph as a path.""" if "image" not in request.files: return jsonify({"error": t("api_no_image")}), 400 file = request.files["image"] text = request.form.get("message", "").strip() # Save the image to a temporary directory suffix = os.path.splitext(file.filename)[1] or ".jpg" tmp = tempfile.NamedTemporaryFile(delete=False, suffix=suffix, dir=tempfile.gettempdir()) file.save(tmp.name) tmp.close() # Compose the message: image path + optional text if text: message = f"{tmp.name} {text}" else: message = tmp.name return _invoke_graph(message) @flask_app.route("/images/") def serve_image(filename): """Serves the product thumbnails from artifacts/images/.""" return send_from_directory(IMAGES_DIR, filename) @flask_app.route("/product_image/") def serve_product_image(asin): """Serves a product image by finding the file with the correct extension. The Amazon dataset contains images in different formats (jpg, png, gif). Instead of hardcoding .jpg in the frontend, this endpoint looks up the real extension. """ for ext in (".jpg", ".png", ".gif", ".jpeg", ".webp"): candidate = os.path.join(IMAGES_DIR, asin + ext) if os.path.isfile(candidate): return send_from_directory(IMAGES_DIR, asin + ext) return "Image not found", 404 def _invoke_graph(message): """Calls the LangGraph graph capturing stdout for the debug log.""" # Capture all the agents' print() output debug_buffer = io.StringIO() config = {"configurable": {"thread_id": _session["thread_id"]}} try: with contextlib.redirect_stdout(debug_buffer): result = langgraph_app.invoke( {"messages": [HumanMessage(content=message)]}, config=config, ) reply = result["messages"][-1].content products = result.get("products") or [] agent = result.get("decision", "") anchor = result.get("anchor") debug_log = debug_buffer.getvalue() return jsonify({ "reply": reply, "products": products, "debug_log": debug_log, "agent": agent, "anchor": anchor, }) except Exception as e: return jsonify({ "reply": t("api_system_error", error=e), "products": [], "debug_log": debug_buffer.getvalue(), "agent": "error", }), 500 if __name__ == "__main__": print("\n--- WEB SERVER READY ---") print("Open http://127.0.0.1:5000 in your browser\n") flask_app.run(debug=False, port=5000)