import os from flask import Flask, request, jsonify, render_template from flask_cors import CORS from pymongo import MongoClient, ReturnDocument from pymongo.errors import PyMongoError import chromadb # <-- MENGGANTIKAN HttpClient from sentence_transformers import SentenceTransformer from huggingface_hub import InferenceClient from dotenv import load_dotenv from bson.objectid import ObjectId from datetime import datetime # --- 1. Load .env and Initialize --- load_dotenv() app = Flask(__name__) CORS(app) # --- 2. Client Initialization (Global) --- hf_token = os.getenv("HF_TOKEN") hf_client = InferenceClient( "meta-llama/Meta-Llama-3-8B-Instruct", token=hf_token ) mongo_url = os.getenv("MONGO_URL") mongo_client = MongoClient(mongo_url) db = mongo_client.get_database("chatbot_db") # Get DB from MONGO_URL chat_history_collection = db.get_collection("conversations") print("✅ Successfully connected to MongoDB Atlas.") print("Loading embedding model (this may take a while)...") embedding_model = SentenceTransformer('all-MiniLM-L6-v2', cache_folder='/tmp/model_cache', token= hf_token) print("✅ Embedding model ('all-MiniLM-L6-v2') has been successfully loaded.") # --- KODE BARU UNTUK KONEKSI HOSTING (CloudClient) --- try: # 1. Ambil kredensial Cloud Anda dari .env CHROMA_API_KEY = os.getenv("CHROMA_API_KEY") CHROMA_TENANT = os.getenv("CHROMA_TENANT") CHROMA_DATABASE = os.getenv("CHROMA_DATABASE") if not CHROMA_API_KEY or not CHROMA_TENANT or not CHROMA_DATABASE: raise ValueError("CHROMA_API_KEY, CHROMA_TENANT, or CHROMA_DATABASE missing from .env file") # 2. Gunakan chromadb.CloudClient chroma_client = chromadb.CloudClient( tenant=CHROMA_TENANT, database=CHROMA_DATABASE, api_key=CHROMA_API_KEY ) # 3. Ambil collection (data Anda sudah ada di sana dari 'chroma copy') knowledge_collection = chroma_client.get_collection( name="website_knowledge" ) print(f"✅ Successfully connected to ChromaDB Cloud (Tenant: {CHROMA_TENANT}).") print(f"✅ Found {knowledge_collection.count()} documents in 'website_knowledge' collection.") except Exception as e: print(f"❌ FAILED to connect to ChromaDB Cloud. Check your .env variables. Error: {e}") # --- AKHIR DARI KODE BARU --- # --- 3. Frontend Endpoints --- @app.route("/") def home(): """Display the landing page (index.html)""" return render_template("index.html") @app.route("/chat") def chat_page(): """Display the main chat page (chat.html)""" return render_template("chat.html") # --- 4. API Endpoint - Get All History (for F5 refresh) --- @app.route("/api/conversations", methods=["GET"]) def get_conversations(): user_id = request.args.get("userId") if not user_id: return jsonify({"error": "userId is required"}), 400 try: convos = list(chat_history_collection.find( {"userId": user_id} ).sort("updatedAt", -1)) # Sort by most recent conversations_list = [] for convo in convos: conversations_list.append({ "id": str(convo.get('_id')), "title": convo.get("title", "New Chat"), "messages": convo.get("messages", []), "createdAt": convo.get("createdAt") }) return jsonify(conversations_list) except Exception as e: print(f"Error in /api/conversations GET: {e}") return jsonify({"error": "Failed to retrieve conversations"}), 500 # --- 5. API Endpoint (FOR “CLEAR ALL”) --- @app.route("/api/conversations", methods=["DELETE"]) def clear_conversations(): """ Delete ALL conversations for one user ID. """ data = request.json user_id = data.get("userId") if not user_id: return jsonify({"error": "userId is required"}), 400 try: result = chat_history_collection.delete_many({"userId": user_id}) print(f"Successfully deleted {result.deleted_count} conversations for userId {user_id}.") return jsonify({ "message": "History successfully deleted", "deleted_count": result.deleted_count }) except Exception as e: print(f"Error in /api/conversations DELETE: {e}") return jsonify({"error": "Failed to delete history"}), 500 # --- 6. Main Chat API Endpoint --- @app.route("/api/chat", methods = ["POST"]) def handle_chat(): try: data = request.json user_message = data.get("message") user_id = data.get("userId") conversation_id = data.get("conversationId") # Can be 'null', 'temp-...', or a real ObjectId if not user_message or not user_id: return jsonify({"error": "'message' and 'userId' parameters are required."}), 400 history = [] user_message_doc = {"role": "user", "content": user_message, "timestamp": datetime.now()} real_convo_id_obj = None # Will hold the valid Mongo ObjectId # --- 1. (CRUD) Validate ID and Get Conversation --- if conversation_id: try: # Try to convert. If successful, it's a real ID. real_convo_id_obj = ObjectId(conversation_id) except Exception as e: # bson.errors.InvalidId # If it Fails (it's 'temp-...' or invalid), treat it as a new chat. real_convo_id_obj = None if real_convo_id_obj: # This is an EXISTING CHAT (valid ID) current_convo = chat_history_collection.find_one({ "_id": real_convo_id_obj, "userId": user_id }) if current_convo: history = current_convo.get("messages", [])[-6:] # If real_convo_id_obj is None, history remains [] (empty) # --- 2. (RAG) - Perform RAG --- print(f"Searching for context for: \"{user_message}\"") query_embedding = embedding_model.encode(user_message).tolist() # Catatan: n_results=5 dapat menyebabkan kontaminasi konteks. # Pertimbangkan untuk mengganti ke 1 jika jawaban tidak akurat. results = knowledge_collection.query( query_embeddings=[query_embedding], n_results=5 ) # Tambahkan pemeriksaan jika 'results' kosong if not results['documents'] or not results['documents'][0]: context = "No relevant context found." print("Context not found.") else: context = "\n\n".join(results['documents'][0]) print("Context found.") # --- 3. (RAG) Build Prompt (Per New Instructions) --- system_prompt = """You are the 'Enviro Education Tools Product Selector & System Designer'. Your answers are for technical professionals who use American English. Your task is to answer the user's question *strictly* and *only* based on the context provided. The context provided IS the information from https://enviroeducationtools.com/. - Do not use any products, information, or technologies from other websites. - Do not mention any other websites. - Do not suggest prices or pricing information. If the user asks 'about Enviro Education Tools' or 'where are you based', use the following information: 'Enviro Education Tools, based in Atlanta, Georgia, United States, is recognized as one of the leading global suppliers of advanced B2B and B2G (to a lesser degree, B2B2C, B2D) technologies in the world. For four decades, Enviro Education Tools has served its customers in the U.S. and Canada, including many Fortune 500 companies, leading R&D firms, prestigious universities, and U.S. and Canadian government agencies. Asset Track Pro has invested heavily in R&D of its products and systems, has stringent quality assurance processes, and provides top-notch expert support remotely or onsite.' If the answer is not in the context, say: 'I'm sorry, I do not have specific information on that topic from https://enviroeducationtools.com/' """ formatted_history = "\n".join([f"{msg['role']}: {msg['content']}" for msg in history]) user_prompt = f"Context:\n{context}\n\nChat History:\n{formatted_history}\n\nQuestion:\n{user_message}" messages = [ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt} ] # --- 4. (RAG) Call Llama 3 API --- print("Calling Hugging Face API...") response = hf_client.chat_completion(messages=messages, max_tokens=250, temperature=0.1) ai_response = response.choices[0].message.content # --- 4.5. Add Mandatory Disclaimer --- disclaimer = "\n\nThe above is suggested by Enviro Education Tools AI and may not be as good as what our human experts can provide. Please contact our experts for further." ai_response += disclaimer # Append disclaimer to AI response # --- 5. (CRUD) Save AI response to MongoDB --- ai_message_doc = {"role": "assistant", "content": ai_response, "timestamp": datetime.now()} final_convo_id_str = "" if real_convo_id_obj: # Existing chat (ID was valid) chat_history_collection.update_one( {"_id": real_convo_id_obj}, { "$push": {"messages": {"$each": [user_message_doc, ai_message_doc]}}, "$set": {"updatedAt": datetime.now()} } ) final_convo_id_str = str(real_convo_id_obj) else: # New chat (ID was 'null' or 'temp-...') title = user_message[:30] + "..." if len(user_message) > 30 else user_message new_convo_doc = { "userId": user_id, "title": title, "messages": [user_message_doc, ai_message_doc], # Add user & AI messages "createdAt": datetime.now(), "updatedAt": datetime.now() } insert_result = chat_history_collection.insert_one(new_convo_doc) final_convo_id_str = str(insert_result.inserted_id) # Get the NEW _id print("Conversation saved to MongoDB.") # --- 6. Send response --- return jsonify({"answer": ai_response, "conversationId": final_convo_id_str}) except Exception as e: print(f"Error in /api/chat: {e}") return jsonify({"error": "An error occurred on the server"}), 500 # --- 7. Run the Server --- if __name__ == "__main__": app.run(port=5000, debug=True)