File size: 10,678 Bytes
85f4d1b
 
 
 
 
353025f
85f4d1b
 
 
 
 
 
39ff7ae
85f4d1b
 
 
 
39ff7ae
85f4d1b
 
 
 
 
 
 
 
c840d73
85f4d1b
39ff7ae
85f4d1b
c840d73
14c4536
942f7d6
 
c840d73
85f4d1b
353025f
85f4d1b
353025f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85f4d1b
353025f
85f4d1b
353025f
 
 
85f4d1b
353025f
 
f86ad4d
c840d73
85f4d1b
 
39ff7ae
f86ad4d
 
 
 
39ff7ae
85f4d1b
 
c840d73
85f4d1b
 
 
 
39ff7ae
85f4d1b
 
 
 
c840d73
85f4d1b
 
 
 
 
39ff7ae
85f4d1b
 
 
 
 
c840d73
 
85f4d1b
c840d73
85f4d1b
 
 
39ff7ae
85f4d1b
 
 
 
39ff7ae
85f4d1b
 
 
c840d73
85f4d1b
39ff7ae
85f4d1b
 
 
c840d73
39ff7ae
85f4d1b
c840d73
85f4d1b
 
 
 
 
 
c840d73
85f4d1b
 
c840d73
85f4d1b
 
 
c840d73
85f4d1b
c840d73
85f4d1b
c840d73
 
 
 
 
 
 
 
 
85f4d1b
c840d73
85f4d1b
 
 
 
c840d73
85f4d1b
c840d73
39ff7ae
85f4d1b
353025f
 
 
85f4d1b
 
c840d73
85f4d1b
353025f
 
 
 
 
 
 
 
85f4d1b
353025f
c840d73
 
 
 
 
 
 
 
 
 
 
 
85f4d1b
 
 
 
 
 
 
c840d73
39ff7ae
85f4d1b
 
 
c840d73
 
 
 
 
85f4d1b
 
c840d73
 
 
85f4d1b
c840d73
85f4d1b
 
 
 
 
c840d73
 
 
85f4d1b
 
 
 
c840d73
85f4d1b
 
 
 
c840d73
85f4d1b
c840d73
85f4d1b
c840d73
 
85f4d1b
 
c840d73
 
85f4d1b
39ff7ae
85f4d1b
39ff7ae
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
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)