Commit
·
41e57de
1
Parent(s):
563f29b
- .DS_Store +0 -0
- Dockerfile +16 -53
- Procfile +1 -0
- README.md +3 -12
- __pycache__/gemini_handler.cpython-312.pyc +0 -0
- __pycache__/gemini_handler.cpython-313.pyc +0 -0
- app.py +394 -0
- apps.py +372 -0
- config.yaml +34 -0
- gemini_handler.py +559 -0
- requirements.txt +12 -0
- source/faiss_index_drug.bin +3 -0
- source/faiss_index_vn.bin +3 -0
- source/merged_df.csv +0 -0
- source/merged_df_vn.csv +0 -0
- source/sentence_embeddings.pkl +3 -0
- source/sentence_embeddings_vn.pkl +3 -0
- static/style.css +267 -0
- templates/dashboard.html +0 -0
- templates/demo.html +0 -0
- templates/index.html +356 -0
- templates/login.html +101 -0
- templates/register.html +106 -0
.DS_Store
ADDED
Binary file (8.2 kB). View file
|
|
Dockerfile
CHANGED
@@ -1,60 +1,23 @@
|
|
1 |
-
|
|
|
2 |
|
3 |
-
|
|
|
4 |
|
5 |
-
|
6 |
-
|
7 |
|
8 |
-
|
9 |
-
|
10 |
|
11 |
-
#
|
12 |
-
|
13 |
-
gpg -o /usr/share/keyrings/mongodb-server-7.0.gpg \
|
14 |
-
--dearmor
|
15 |
|
16 |
-
|
|
|
17 |
|
18 |
-
|
19 |
-
|
20 |
-
rm -rf /var/lib/apt/lists/*
|
21 |
|
22 |
-
#
|
23 |
-
|
24 |
-
|
25 |
-
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
26 |
-
nodejs && \
|
27 |
-
rm -rf /var/lib/apt/lists/*
|
28 |
-
|
29 |
-
# image setup
|
30 |
-
RUN useradd -m -u 1000 user
|
31 |
-
|
32 |
-
RUN mkdir /app
|
33 |
-
RUN chown -R 1000:1000 /app
|
34 |
-
RUN mkdir /data
|
35 |
-
RUN chown -R 1000:1000 /data
|
36 |
-
|
37 |
-
# Switch to the "user" user
|
38 |
-
USER user
|
39 |
-
|
40 |
-
ENV HOME=/home/user \
|
41 |
-
PATH=/home/user/.local/bin:$PATH
|
42 |
-
|
43 |
-
RUN npm config set prefix /home/user/.local
|
44 |
-
RUN npm install -g dotenv-cli
|
45 |
-
|
46 |
-
|
47 |
-
# copy chat-ui from base image
|
48 |
-
COPY --from=base --chown=1000 /app/node_modules /app/node_modules
|
49 |
-
COPY --from=base --chown=1000 /app/package.json /app/package.json
|
50 |
-
COPY --from=base --chown=1000 /app/build /app/build
|
51 |
-
|
52 |
-
COPY --from=base --chown=1000 /app/.env /app/.env
|
53 |
-
COPY --chown=1000 .env.local /app/.env.local
|
54 |
-
|
55 |
-
COPY --chown=1000 entrypoint.sh /app/entrypoint.sh
|
56 |
-
|
57 |
-
RUN chmod +x /app/entrypoint.sh
|
58 |
-
|
59 |
-
# entrypoint
|
60 |
-
ENTRYPOINT [ "/app/entrypoint.sh" ]
|
|
|
1 |
+
# Use the official Python 3.9 image from the Docker Hub
|
2 |
+
FROM python:3.9-slim
|
3 |
|
4 |
+
# Set the working directory in the container
|
5 |
+
WORKDIR /app
|
6 |
|
7 |
+
# Copy the requirements.txt file into the container at /app
|
8 |
+
COPY requirements.txt /app/
|
9 |
|
10 |
+
# Install the dependencies from requirements.txt
|
11 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
12 |
|
13 |
+
# Copy the rest of the application code into the container
|
14 |
+
COPY . /app/
|
|
|
|
|
15 |
|
16 |
+
# Expose the port your app will run on (assuming Flask/Django app listens on port 5000)
|
17 |
+
EXPOSE 5000
|
18 |
|
19 |
+
# Command to run the app with Gunicorn (for production use)
|
20 |
+
CMD ["gunicorn", "app:app", "--bind", "0.0.0.0:$PORT"]
|
|
|
21 |
|
22 |
+
# For development (uncomment the following line if you're in development mode):
|
23 |
+
# CMD ["python", "app.py"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Procfile
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
web: gunicorn app:app --bind 0.0.0.0:$PORT
|
README.md
CHANGED
@@ -1,13 +1,4 @@
|
|
1 |
-
|
2 |
-
|
3 |
-
emoji: 🚀
|
4 |
-
colorFrom: indigo
|
5 |
-
colorTo: blue
|
6 |
-
sdk: docker
|
7 |
-
pinned: false
|
8 |
-
app_port: 3000
|
9 |
-
suggested_hardware: a10g-small
|
10 |
-
short_description: This is a system support to recommend drugs based on symptom
|
11 |
-
---
|
12 |
|
13 |
-
|
|
|
1 |
+
RUN APP:
|
2 |
+
pip install -r requirements.txt
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
3 |
|
4 |
+
python app.py
|
__pycache__/gemini_handler.cpython-312.pyc
ADDED
Binary file (27.1 kB). View file
|
|
__pycache__/gemini_handler.cpython-313.pyc
ADDED
Binary file (27.7 kB). View file
|
|
app.py
ADDED
@@ -0,0 +1,394 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from flask import Flask, request, jsonify, render_template,make_response,redirect, url_for, session
|
2 |
+
from flask_pymongo import PyMongo
|
3 |
+
from bson.objectid import ObjectId
|
4 |
+
import bcrypt
|
5 |
+
from flask_cors import CORS
|
6 |
+
from sentence_transformers import SentenceTransformer, util
|
7 |
+
import faiss
|
8 |
+
import numpy as np
|
9 |
+
import json
|
10 |
+
import os
|
11 |
+
import pickle
|
12 |
+
import pandas as pd
|
13 |
+
import google.generativeai as genai
|
14 |
+
from datetime import datetime
|
15 |
+
import uuid
|
16 |
+
from gemini_handler import GeminiHandler, GenerationConfig, Strategy, KeyRotationStrategy
|
17 |
+
from dotenv import load_dotenv
|
18 |
+
# Load biến môi trường từ file .env
|
19 |
+
load_dotenv()
|
20 |
+
GEMINI_API_KEYS = os.getenv("GEMINI_API_KEYS").split(",")
|
21 |
+
|
22 |
+
# Khởi tạo Flask app
|
23 |
+
app = Flask(__name__)
|
24 |
+
|
25 |
+
app.secret_key = 'AIzaSyCvlZ63Nkt5NpjdmxYPAsG8Qskex6usCFw'
|
26 |
+
app.config['MONGO_URI'] = 'mongodb+srv://admin:[email protected]/drug_recom'
|
27 |
+
mongo = PyMongo(app)
|
28 |
+
|
29 |
+
|
30 |
+
|
31 |
+
|
32 |
+
CORS(app)
|
33 |
+
|
34 |
+
genai.configure(api_key="AIzaSyCvlZ63Nkt5NpjdmxYPAsG8Qskex6usCFw")
|
35 |
+
|
36 |
+
# Tải FAISS index
|
37 |
+
faiss_index = faiss.read_index("source/faiss_index_vn.bin")
|
38 |
+
|
39 |
+
# Tải embeddings
|
40 |
+
with open("source/sentence_embeddings_vn.pkl", "rb") as f:
|
41 |
+
disease_embeddings = pickle.load(f)
|
42 |
+
|
43 |
+
# Khởi tạo mô hình Sentence Transformer
|
44 |
+
model_em = SentenceTransformer('hiieu/halong_embedding')
|
45 |
+
# Đọc dữ liệu bệnh
|
46 |
+
merged_df = pd.read_csv('source/merged_df_vn.csv')
|
47 |
+
|
48 |
+
|
49 |
+
def get_disease_and_generate_prompt(symptoms_input, faiss_index, model_em, merged_df, top_k=5):
|
50 |
+
# 1. Mã hóa triệu chứng đầu vào
|
51 |
+
input_embedding = model_em.encode([symptoms_input], convert_to_tensor=True)
|
52 |
+
|
53 |
+
# 2. Tìm kiếm top_k trong FAISS Index
|
54 |
+
distances, indices = faiss_index.search(np.array(input_embedding.cpu().numpy()), k=top_k)
|
55 |
+
|
56 |
+
# Lấy danh sách các bệnh và điểm tương ứng
|
57 |
+
top_diseases = [(merged_df.iloc[idx], score) for idx, score in zip(indices[0], distances[0])]
|
58 |
+
|
59 |
+
# 3. Mã hóa thông tin của top_k bệnh để đánh giá lại
|
60 |
+
candidate_embeddings = model_em.encode(
|
61 |
+
[disease['Information'] for disease, _ in top_diseases],
|
62 |
+
convert_to_tensor=True
|
63 |
+
)
|
64 |
+
|
65 |
+
# 4. Tính độ tương đồng cosine giữa triệu chứng đầu vào và các ứng viên
|
66 |
+
scores = util.cos_sim(input_embedding, candidate_embeddings).squeeze()
|
67 |
+
|
68 |
+
# 5. Sắp xếp lại danh sách ứng viên dựa trên điểm similarity
|
69 |
+
ranked_indices = scores.argsort(descending=True)
|
70 |
+
best_match = top_diseases[ranked_indices[0].item()][0] # Ứng viên tốt nhất sau re-ranking
|
71 |
+
|
72 |
+
# 6. Chuyển thông tin bệnh tốt nhất thành danh sách
|
73 |
+
result_list = [
|
74 |
+
f"Patient Symptoms: {symptoms_input}",
|
75 |
+
f"similarity: {scores}",
|
76 |
+
# f"disease: {best_match['Disease']}",
|
77 |
+
f"symptoms: {best_match['Symptoms']}",
|
78 |
+
f"medications: {best_match['Medication']}",
|
79 |
+
f"diets: {best_match['Diet']}",
|
80 |
+
f"workouts: {best_match['workout']}",
|
81 |
+
f"precautions: {best_match.get('Precaution_1', '')}, {best_match.get('Precaution_2', '')}, {best_match.get('Precaution_3', '')}, {best_match.get('Precaution_4', '')}",
|
82 |
+
]
|
83 |
+
#print(result_list)
|
84 |
+
return result_list
|
85 |
+
|
86 |
+
|
87 |
+
|
88 |
+
def parse_contexts(raw_contexts):
|
89 |
+
"""Convert raw context strings into structured dictionaries."""
|
90 |
+
structured_contexts = []
|
91 |
+
temp_context = {}
|
92 |
+
for context in raw_contexts:
|
93 |
+
# Check for each expected piece of information and assign it to the dictionary
|
94 |
+
if context.startswith("Patient Symptoms:"):
|
95 |
+
temp_context["patient_symptoms"] = context.replace("Patient Symptoms:", "").strip()
|
96 |
+
elif context.startswith("disease"):
|
97 |
+
temp_context["disease"] = context.replace("disease", "").strip()
|
98 |
+
elif context.startswith("symptoms:"):
|
99 |
+
temp_context["symptoms"] = context.replace("symptoms:", "").strip().split(", ")
|
100 |
+
elif context.startswith("medications:"):
|
101 |
+
temp_context["medications"] = context.replace("medications:", "").strip().split(", ")
|
102 |
+
elif context.startswith("diets:"):
|
103 |
+
temp_context["diets"] = context.replace("diets:", "").strip().split(", ")
|
104 |
+
elif context.startswith("workouts:"):
|
105 |
+
temp_context["workouts"] = context.replace("workouts:", "").strip().split(", ")
|
106 |
+
elif context.startswith("precautions:"):
|
107 |
+
temp_context["precautions"] = context.replace("precautions:", "").strip().split(", ")
|
108 |
+
|
109 |
+
# When all necessary fields are collected, add to the list and reset temp_context
|
110 |
+
if len(temp_context) == 7:
|
111 |
+
structured_contexts.append(temp_context)
|
112 |
+
temp_context = {}
|
113 |
+
|
114 |
+
return structured_contexts
|
115 |
+
|
116 |
+
prompt_template = (
|
117 |
+
"### Hệ thống:"
|
118 |
+
"Bạn đang nhận được một yêu cầu tư vấn y tế. Dưới đây là thông tin cần thiết để bạn đưa ra câu trả lời chính xác, dễ hiểu và hữu ích cho người dùng."
|
119 |
+
"Hãy tập trung vào việc cung cấp tên thuốc phù hợp và hướng dẫn rõ ràng để giúp người dùng áp dụng dễ dàng."
|
120 |
+
"### Hướng dẫn:"
|
121 |
+
"{instruction}\n\n"
|
122 |
+
|
123 |
+
"### Thông tin y tế:\n"
|
124 |
+
"{input}\n\n"
|
125 |
+
|
126 |
+
"### Câu trả lời:\n"
|
127 |
+
"{output}"
|
128 |
+
)
|
129 |
+
|
130 |
+
def get_prompt(question, raw_contexts):
|
131 |
+
if not raw_contexts:
|
132 |
+
raise ValueError("Danh sách thông tin y tế không được để trống.")
|
133 |
+
|
134 |
+
# Xử lý dữ liệu đầu vào thành dạng dễ đọc
|
135 |
+
contexts = parse_contexts(raw_contexts)
|
136 |
+
|
137 |
+
context = "".join([
|
138 |
+
f"\n<b>📌 Trường hợp {i+1}:</b>\n"
|
139 |
+
#f"- <b>Bệnh:</b> {x.get('disease', 'Chưa xác định')}\n"
|
140 |
+
f"- <b>Triệu chứng:</b> {', '.join(map(str, x.get('symptoms', [])))}\n"
|
141 |
+
f"- <b>Thuốc đề xuất:</b> <i>{', '.join(map(str, x.get('medications', [])))}</i>\n"
|
142 |
+
f"- <b>Chế độ ăn uống:</b> {', '.join(map(str, x.get('diets', [])))}\n"
|
143 |
+
f"- <b>Bài tập hỗ trợ:</b> {', '.join(map(str, x.get('workouts', [])))}\n"
|
144 |
+
f"- <b>Lưu ý quan trọng:</b> {', '.join(map(str, x.get('precautions', [])))}\n"
|
145 |
+
for i, x in enumerate(contexts)
|
146 |
+
])
|
147 |
+
|
148 |
+
instruction = (
|
149 |
+
"💊 Bạn là một dược sĩ có kinh nghiệm lâu năm. Hãy cung cấp câu trả lời <b>đầy đủ, chính xác</b>, "
|
150 |
+
"dễ hiểu và tập trung vào <b>tư vấn thuốc</b> cho người dùng.\n"
|
151 |
+
"🔹 Trình bày câu trả lời theo danh sách số thứ tự (1️⃣, 2️⃣, 3️⃣...) để dễ đọc.\n"
|
152 |
+
"🔹 Định dạng rõ ràng: <b>in đậm</b> những điểm quan trọng, <i>in nghiêng</i> tên thuốc, <b>in đậm</b> các lưu ý quan trọng, "
|
153 |
+
"sử dụng dấu gạch đầu dòng (-) để liệt kê thông tin."
|
154 |
+
)
|
155 |
+
|
156 |
+
input_text = (
|
157 |
+
"🩺 Dựa trên thông tin y tế sau đây, hãy trả lời câu hỏi của người dùng:\n"
|
158 |
+
f"{context}\n"
|
159 |
+
"❓ <b>Câu hỏi:</b> " + question + "\n"
|
160 |
+
"📌 <b>Yêu cầu:</b> Hãy trả lời theo bố cục số thứ tự, dễ đọc, ngắn gọn nhưng đầy đủ, giúp người dùng dễ áp dụng.\n"
|
161 |
+
"📋 <b>Định dạng:</b>\n"
|
162 |
+
"- <b>In đậm</b> cho thông tin quan trọng\n"
|
163 |
+
"- <i>In nghiêng</i> cho tên thuốc\n"
|
164 |
+
"- <b>In đậm</b> cho các lưu ý đặc biệt\n"
|
165 |
+
"- Dấu gạch đầu dòng (-) để trình bày rõ ràng khi liệt kê"
|
166 |
+
)
|
167 |
+
|
168 |
+
prompt = prompt_template.format(
|
169 |
+
instruction=instruction,
|
170 |
+
input=input_text,
|
171 |
+
output='' # AI sẽ tự điền câu trả lời
|
172 |
+
)
|
173 |
+
|
174 |
+
return prompt
|
175 |
+
|
176 |
+
@app.route("/send_message", methods=["POST"])
|
177 |
+
def send_message():
|
178 |
+
try:
|
179 |
+
data = request.json
|
180 |
+
print("📩 Received Data:", data) # Debug log
|
181 |
+
|
182 |
+
# Kiểm tra request JSON hợp lệ
|
183 |
+
if not data or "message" not in data:
|
184 |
+
return jsonify({"error": "Dữ liệu không hợp lệ!"}), 400
|
185 |
+
|
186 |
+
message = data.get("message", "").strip()
|
187 |
+
conversation_id = data.get("conversation_id", "")
|
188 |
+
|
189 |
+
if not message:
|
190 |
+
return jsonify({"error": "Câu hỏi không hợp lệ!"}), 400
|
191 |
+
|
192 |
+
# Nếu không có conversation_id, tạo cuộc trò chuyện mới
|
193 |
+
if not conversation_id:
|
194 |
+
conversation_id = str(uuid.uuid4())
|
195 |
+
session["conversation_id"] = conversation_id
|
196 |
+
mongo.db.chat_history.insert_one({
|
197 |
+
"conversation_id": conversation_id,
|
198 |
+
"name": "Chưa có tên",
|
199 |
+
"messages": [],
|
200 |
+
"created_at": datetime.now().strftime("%d/%m/%Y %H:%M:%S")
|
201 |
+
})
|
202 |
+
|
203 |
+
# Gọi hàm lấy dữ liệu context
|
204 |
+
context_data = get_disease_and_generate_prompt(message, faiss_index, model_em, merged_df, top_k=2)
|
205 |
+
prompt = get_prompt(message, context_data)
|
206 |
+
|
207 |
+
# Get API keys từ environment
|
208 |
+
api_keys = os.getenv('GEMINI_API_KEYS')
|
209 |
+
if not api_keys:
|
210 |
+
raise ValueError("GEMINI_API_KEYS environment variable is not set")
|
211 |
+
|
212 |
+
|
213 |
+
# Round-robin strategy
|
214 |
+
handler = GeminiHandler(
|
215 |
+
config_path="config.yaml",
|
216 |
+
content_strategy=Strategy.ROUND_ROBIN,
|
217 |
+
key_strategy=KeyRotationStrategy.SMART_COOLDOWN
|
218 |
+
)
|
219 |
+
|
220 |
+
# Generate content
|
221 |
+
response_new = handler.generate_content(
|
222 |
+
prompt=prompt,
|
223 |
+
model_name="gemini-2.0-flash-thinking-exp-1219",
|
224 |
+
return_stats=True # Get key usage stats
|
225 |
+
)
|
226 |
+
|
227 |
+
# Process response
|
228 |
+
# if response_new['success']:
|
229 |
+
# print(response_new['text'])
|
230 |
+
# else:
|
231 |
+
# print(f"Generation failed: {response_new['error']}")
|
232 |
+
|
233 |
+
# # Gửi prompt đến model Gemini
|
234 |
+
# model = genai.GenerativeModel("gemini-1.5-flash", generation_config=generation_config)
|
235 |
+
# chat_session = model.start_chat(history=[])
|
236 |
+
# response = chat_session.send_message(prompt)
|
237 |
+
|
238 |
+
# Lấy nội dung phản hồi từ bot
|
239 |
+
try:
|
240 |
+
text_response = response_new['text']
|
241 |
+
except Exception as e:
|
242 |
+
print(f"⚠️ Error getting response: {e}")
|
243 |
+
return jsonify({"error": "Lỗi khi lấy phản hồi từ mô hình!"}), 500
|
244 |
+
|
245 |
+
# Cấu trúc tin nhắn
|
246 |
+
timestamp = datetime.now().strftime("%d/%m/%Y %H:%M:%S")
|
247 |
+
user_message = {"text": message, "timestamp": timestamp, "sender": "user"}
|
248 |
+
bot_message = {"text": text_response, "timestamp": timestamp, "sender": "bot"}
|
249 |
+
|
250 |
+
# Cập nhật cuộc trò chuyện
|
251 |
+
mongo.db.chat_history.update_one(
|
252 |
+
{"conversation_id": conversation_id},
|
253 |
+
{"$push": {"messages": {"$each": [user_message, bot_message]}}},
|
254 |
+
upsert=True
|
255 |
+
)
|
256 |
+
|
257 |
+
# Trả về phản hồi JSON
|
258 |
+
return jsonify({
|
259 |
+
"conversation_id": conversation_id,
|
260 |
+
"status": "sent",
|
261 |
+
"bot_reply": text_response,
|
262 |
+
"timestamp": timestamp
|
263 |
+
})
|
264 |
+
|
265 |
+
except Exception as e:
|
266 |
+
print(f"🚨 Error in /query: {str(e)}")
|
267 |
+
return jsonify({"error": f"Lỗi xử lý: {str(e)}"}), 500
|
268 |
+
|
269 |
+
|
270 |
+
@app.route("/start_conversation", methods=["POST"])
|
271 |
+
def start_conversation():
|
272 |
+
try:
|
273 |
+
data = request.json
|
274 |
+
message = data.get("message", "").strip()
|
275 |
+
|
276 |
+
if not message:
|
277 |
+
return jsonify({"error": "Tin nhắn không hợp lệ!"}), 400
|
278 |
+
|
279 |
+
conversation_id = str(uuid.uuid4()) # Tạo conversation_id mới
|
280 |
+
session["conversation_id"] = conversation_id
|
281 |
+
|
282 |
+
mongo.db.chat_history.insert_one({
|
283 |
+
"conversation_id": conversation_id,
|
284 |
+
"name": message,
|
285 |
+
"messages": [],
|
286 |
+
"created_at": datetime.now().strftime("%d/%m/%Y %H:%M:%S")
|
287 |
+
})
|
288 |
+
|
289 |
+
return jsonify({"conversation_id": conversation_id})
|
290 |
+
|
291 |
+
except Exception as e:
|
292 |
+
print(f"🚨 Error in /start_conversation: {str(e)}")
|
293 |
+
return jsonify({"error": f"Lỗi xử lý: {str(e)}"}), 500
|
294 |
+
|
295 |
+
|
296 |
+
@app.route("/all_history", methods=["GET"])
|
297 |
+
def get_all_history():
|
298 |
+
"""Lấy danh sách tất cả cuộc trò chuyện"""
|
299 |
+
chats = list(mongo.db.chat_history.find({}, {"_id": 0, "messages": 0}))
|
300 |
+
return jsonify(chats)
|
301 |
+
|
302 |
+
|
303 |
+
@app.route("/conversation/<conversation_id>", methods=["GET"])
|
304 |
+
def get_conversation(conversation_id):
|
305 |
+
"""Lấy nội dung cuộc trò chuyện theo ID"""
|
306 |
+
chat = mongo.db.chat_history.find_one({"conversation_id": conversation_id}, {"_id": 0})
|
307 |
+
if not chat:
|
308 |
+
return jsonify({"error": "Cuộc trò chuyện không tồn tại"}), 404
|
309 |
+
return jsonify(chat)
|
310 |
+
|
311 |
+
@app.route('/get_conversations', methods=['GET'])
|
312 |
+
def get_conversations():
|
313 |
+
# Lấy tất cả cuộc trò chuyện từ MongoDB
|
314 |
+
conversations = list(mongo.db.chat_history.find({}, {"_id": 0}))
|
315 |
+
|
316 |
+
return jsonify([{
|
317 |
+
"conversation_id": conv.get("conversation_id", "N/A"),
|
318 |
+
"name": conv.get("name", "Chưa có tên"),
|
319 |
+
"bot_messages": [msg["text"] for msg in conv.get("messages", []) if msg.get("sender") == "bot"], # Chỉ lấy text của bot
|
320 |
+
"created_at": conv.get("created_at", "Không rõ ngày")
|
321 |
+
} for conv in conversations])
|
322 |
+
|
323 |
+
@app.route('/get_messages', methods=['GET'])
|
324 |
+
def get_messages():
|
325 |
+
conversation_id = request.args.get("conversation_id")
|
326 |
+
|
327 |
+
if not conversation_id:
|
328 |
+
return jsonify({"error": "Thiếu conversation_id"}), 400
|
329 |
+
|
330 |
+
# Truy vấn cuộc trò chuyện theo conversation_id
|
331 |
+
conversation = mongo.db.chat_history.find_one(
|
332 |
+
{"conversation_id": conversation_id},
|
333 |
+
{"_id": 0, "messages": 1,"name":1, "timestamp":1}
|
334 |
+
)
|
335 |
+
|
336 |
+
if conversation:
|
337 |
+
return jsonify(conversation) # Trả về danh sách tin nhắn
|
338 |
+
else:
|
339 |
+
return jsonify({"error": "Không tìm thấy cuộc trò chuyện"}), 404
|
340 |
+
|
341 |
+
|
342 |
+
|
343 |
+
@app.route("/")
|
344 |
+
def index():
|
345 |
+
return render_template('index.html')
|
346 |
+
|
347 |
+
@app.route('/register', methods=['GET', 'POST'])
|
348 |
+
def register():
|
349 |
+
if request.method == 'POST':
|
350 |
+
fullname = request.form['fullname']
|
351 |
+
username = request.form['username']
|
352 |
+
password = request.form['password']
|
353 |
+
hash_password = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())
|
354 |
+
|
355 |
+
# Insert user into the 'user' collection
|
356 |
+
mongo.db.user.insert_one({'fullname': fullname,'username': username, 'password': hash_password})
|
357 |
+
return redirect(url_for('login'))
|
358 |
+
return render_template('register.html')
|
359 |
+
|
360 |
+
@app.route('/login', methods=['GET', 'POST'])
|
361 |
+
def login():
|
362 |
+
if request.method == 'POST':
|
363 |
+
username = request.form['username']
|
364 |
+
password = request.form['password']
|
365 |
+
|
366 |
+
# Find user in the 'user' collection
|
367 |
+
user = mongo.db.user.find_one({'username': username})
|
368 |
+
|
369 |
+
# Kiểm tra nếu người dùng tồn tại và mật khẩu đúng
|
370 |
+
if user and bcrypt.checkpw(password.encode('utf-8'), user['password']):
|
371 |
+
# Lưu fullname của người dùng vào session
|
372 |
+
session['user'] = user.get('fullname', 'Unknown User') # Đảm bảo lấy giá trị 'fullname' nếu có
|
373 |
+
return redirect(url_for('dashboard'))
|
374 |
+
else:
|
375 |
+
return 'Invalid username or password'
|
376 |
+
return render_template('login.html')
|
377 |
+
|
378 |
+
@app.route("/dashboard")
|
379 |
+
def dashboard():
|
380 |
+
if 'user' in session:
|
381 |
+
current_time = datetime.now().strftime("%d/%m/%Y %H:%M:%S")
|
382 |
+
username = session["user"]
|
383 |
+
return render_template('dashboard.html',username=username,current_time=current_time)
|
384 |
+
return redirect(url_for('login'))
|
385 |
+
|
386 |
+
@app.route('/logout')
|
387 |
+
def logout():
|
388 |
+
session.pop('user', None)
|
389 |
+
return redirect(url_for('login'))
|
390 |
+
|
391 |
+
|
392 |
+
|
393 |
+
if __name__ == "__main__":
|
394 |
+
app.run(debug=True)
|
apps.py
ADDED
@@ -0,0 +1,372 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from flask import Flask, request, jsonify, render_template,make_response,redirect, url_for, session
|
2 |
+
from flask_pymongo import PyMongo
|
3 |
+
from bson.objectid import ObjectId
|
4 |
+
import bcrypt
|
5 |
+
from flask_cors import CORS
|
6 |
+
from sentence_transformers import SentenceTransformer, util
|
7 |
+
import faiss
|
8 |
+
import numpy as np
|
9 |
+
import json
|
10 |
+
import os
|
11 |
+
import pickle
|
12 |
+
import pandas as pd
|
13 |
+
import google.generativeai as genai
|
14 |
+
from datetime import datetime
|
15 |
+
import uuid
|
16 |
+
|
17 |
+
# Khởi tạo Flask app
|
18 |
+
app = Flask(__name__)
|
19 |
+
|
20 |
+
app.secret_key = 'AIzaSyCvlZ63Nkt5NpjdmxYPAsG8Qskex6usCFw'
|
21 |
+
app.config['MONGO_URI'] = 'mongodb+srv://hoangsontruonghcm:[email protected]/drug_recom'
|
22 |
+
mongo = PyMongo(app)
|
23 |
+
|
24 |
+
|
25 |
+
|
26 |
+
|
27 |
+
CORS(app)
|
28 |
+
|
29 |
+
genai.configure(api_key="AIzaSyCvlZ63Nkt5NpjdmxYPAsG8Qskex6usCFw")
|
30 |
+
|
31 |
+
# Tải FAISS index
|
32 |
+
faiss_index = faiss.read_index("source/faiss_index_vn.bin")
|
33 |
+
|
34 |
+
# Tải embeddings
|
35 |
+
with open("source/sentence_embeddings_vn.pkl", "rb") as f:
|
36 |
+
disease_embeddings = pickle.load(f)
|
37 |
+
|
38 |
+
# Khởi tạo mô hình Sentence Transformer
|
39 |
+
model_em = SentenceTransformer('hiieu/halong_embedding')
|
40 |
+
# Đọc dữ liệu bệnh
|
41 |
+
merged_df = pd.read_csv('source/merged_df_vn.csv')
|
42 |
+
|
43 |
+
|
44 |
+
def get_disease_and_generate_prompt(symptoms_input, faiss_index, model_em, merged_df, top_k=5):
|
45 |
+
# 1. Mã hóa triệu chứng đầu vào
|
46 |
+
input_embedding = model_em.encode([symptoms_input], convert_to_tensor=True)
|
47 |
+
|
48 |
+
# 2. Tìm kiếm top_k trong FAISS Index
|
49 |
+
distances, indices = faiss_index.search(np.array(input_embedding.cpu().numpy()), k=top_k)
|
50 |
+
|
51 |
+
# Lấy danh sách các bệnh và điểm tương ứng
|
52 |
+
top_diseases = [(merged_df.iloc[idx], score) for idx, score in zip(indices[0], distances[0])]
|
53 |
+
|
54 |
+
# 3. Mã hóa thông tin của top_k bệnh để đánh giá lại
|
55 |
+
candidate_embeddings = model_em.encode(
|
56 |
+
[disease['Information'] for disease, _ in top_diseases],
|
57 |
+
convert_to_tensor=True
|
58 |
+
)
|
59 |
+
|
60 |
+
# 4. Tính độ tương đồng cosine giữa triệu chứng đầu vào và các ứng viên
|
61 |
+
scores = util.cos_sim(input_embedding, candidate_embeddings).squeeze()
|
62 |
+
|
63 |
+
# 5. Sắp xếp lại danh sách ứng viên dựa trên điểm similarity
|
64 |
+
ranked_indices = scores.argsort(descending=True)
|
65 |
+
best_match = top_diseases[ranked_indices[0].item()][0] # Ứng viên tốt nhất sau re-ranking
|
66 |
+
|
67 |
+
# 6. Chuyển thông tin bệnh tốt nhất thành danh sách
|
68 |
+
result_list = [
|
69 |
+
f"Patient Symptoms: {symptoms_input}",
|
70 |
+
f"similarity: {scores}",
|
71 |
+
f"disease: {best_match['Disease']}",
|
72 |
+
f"symptoms: {best_match['Symptoms']}",
|
73 |
+
f"medications: {best_match['Medication']}",
|
74 |
+
f"diets: {best_match['Diet']}",
|
75 |
+
f"workouts: {best_match['workout']}",
|
76 |
+
f"precautions: {best_match.get('Precaution_1', '')}, {best_match.get('Precaution_2', '')}, {best_match.get('Precaution_3', '')}, {best_match.get('Precaution_4', '')}",
|
77 |
+
]
|
78 |
+
|
79 |
+
return result_list
|
80 |
+
|
81 |
+
|
82 |
+
|
83 |
+
def parse_contexts(raw_contexts):
|
84 |
+
"""Convert raw context strings into structured dictionaries."""
|
85 |
+
structured_contexts = []
|
86 |
+
temp_context = {}
|
87 |
+
for context in raw_contexts:
|
88 |
+
# Check for each expected piece of information and assign it to the dictionary
|
89 |
+
if context.startswith("Patient Symptoms:"):
|
90 |
+
temp_context["patient_symptoms"] = context.replace("Patient Symptoms:", "").strip()
|
91 |
+
elif context.startswith("disease"):
|
92 |
+
temp_context["disease"] = context.replace("disease", "").strip()
|
93 |
+
elif context.startswith("symptoms:"):
|
94 |
+
temp_context["symptoms"] = context.replace("symptoms:", "").strip().split(", ")
|
95 |
+
elif context.startswith("medications:"):
|
96 |
+
temp_context["medications"] = context.replace("medications:", "").strip().split(", ")
|
97 |
+
elif context.startswith("diets:"):
|
98 |
+
temp_context["diets"] = context.replace("diets:", "").strip().split(", ")
|
99 |
+
elif context.startswith("workouts:"):
|
100 |
+
temp_context["workouts"] = context.replace("workouts:", "").strip().split(", ")
|
101 |
+
elif context.startswith("precautions:"):
|
102 |
+
temp_context["precautions"] = context.replace("precautions:", "").strip().split(", ")
|
103 |
+
|
104 |
+
# When all necessary fields are collected, add to the list and reset temp_context
|
105 |
+
if len(temp_context) == 7:
|
106 |
+
structured_contexts.append(temp_context)
|
107 |
+
temp_context = {}
|
108 |
+
|
109 |
+
return structured_contexts
|
110 |
+
|
111 |
+
prompt_template = (
|
112 |
+
"### Hệ thống:"
|
113 |
+
"Bạn đang nhận được một yêu cầu tư vấn y tế. Dưới đây là thông tin cần thiết để bạn đưa ra câu trả lời chính xác, dễ hiểu và hữu ích cho người dùng."
|
114 |
+
"Hãy tập trung vào việc cung cấp tên thuốc phù hợp và hướng dẫn rõ ràng để giúp người dùng áp dụng dễ dàng."
|
115 |
+
"### Hướng dẫn:"
|
116 |
+
"{instruction}\n\n"
|
117 |
+
|
118 |
+
"### Thông tin y tế:\n"
|
119 |
+
"{input}\n\n"
|
120 |
+
|
121 |
+
"### Câu trả lời:\n"
|
122 |
+
"{output}"
|
123 |
+
)
|
124 |
+
|
125 |
+
def get_prompt(question, raw_contexts):
|
126 |
+
if not raw_contexts:
|
127 |
+
raise ValueError("Danh sách thông tin y tế không được để trống.")
|
128 |
+
|
129 |
+
# Xử lý dữ liệu đầu vào thành dạng dễ đọc
|
130 |
+
contexts = parse_contexts(raw_contexts)
|
131 |
+
|
132 |
+
context = "".join([
|
133 |
+
f"\n<b>📌 Trường hợp {i+1}:</b>\n"
|
134 |
+
f"- <b>Bệnh:</b> {x.get('disease', 'Chưa xác định')}\n"
|
135 |
+
f"- <b>Triệu chứng:</b> {', '.join(map(str, x.get('symptoms', [])))}\n"
|
136 |
+
f"- <b>Thuốc đề xuất:</b> <i>{', '.join(map(str, x.get('medications', [])))}</i>\n"
|
137 |
+
f"- <b>Chế độ ăn uống:</b> {', '.join(map(str, x.get('diets', [])))}\n"
|
138 |
+
f"- <b>Bài tập hỗ trợ:</b> {', '.join(map(str, x.get('workouts', [])))}\n"
|
139 |
+
f"- <b>Lưu ý quan trọng:</b> {', '.join(map(str, x.get('precautions', [])))}\n"
|
140 |
+
for i, x in enumerate(contexts)
|
141 |
+
])
|
142 |
+
|
143 |
+
instruction = (
|
144 |
+
"💊 Bạn là một dược sĩ có kinh nghiệm lâu năm. Hãy cung cấp câu trả lời <b>đầy đủ, chính xác</b>, "
|
145 |
+
"dễ hiểu và tập trung vào <b>tư vấn thuốc</b> cho người dùng.\n"
|
146 |
+
"🔹 Trình bày câu trả lời theo danh sách số thứ tự (1️⃣, 2️⃣, 3️⃣...) để dễ đọc.\n"
|
147 |
+
"🔹 Định dạng rõ ràng: <b>in đậm</b> những điểm quan trọng, <i>in nghiêng</i> tên thuốc, <b>in đậm</b> các lưu ý quan trọng, "
|
148 |
+
"sử dụng dấu gạch đầu dòng (-) để liệt kê thông tin."
|
149 |
+
)
|
150 |
+
|
151 |
+
input_text = (
|
152 |
+
"🩺 Dựa trên thông tin y tế sau đây, hãy trả lời câu hỏi của người dùng:\n"
|
153 |
+
f"{context}\n"
|
154 |
+
"❓ <b>Câu hỏi:</b> " + question + "\n"
|
155 |
+
"📌 <b>Yêu cầu:</b> Hãy trả lời theo bố cục số thứ tự, dễ đọc, ngắn gọn nhưng đầy đủ, giúp người dùng dễ áp dụng.\n"
|
156 |
+
"📋 <b>Định dạng:</b>\n"
|
157 |
+
"- <b>In đậm</b> cho thông tin quan trọng\n"
|
158 |
+
"- <i>In nghiêng</i> cho tên thuốc\n"
|
159 |
+
"- <b>In đậm</b> cho các lưu ý đặc biệt\n"
|
160 |
+
"- Dấu gạch đầu dòng (-) để trình bày rõ ràng khi liệt kê"
|
161 |
+
)
|
162 |
+
|
163 |
+
prompt = prompt_template.format(
|
164 |
+
instruction=instruction,
|
165 |
+
input=input_text,
|
166 |
+
output='' # AI sẽ tự điền câu trả lời
|
167 |
+
)
|
168 |
+
|
169 |
+
return prompt
|
170 |
+
|
171 |
+
@app.route("/send_message", methods=["POST"])
|
172 |
+
def send_message():
|
173 |
+
try:
|
174 |
+
data = request.json
|
175 |
+
print("📩 Received Data:", data) # Debug log
|
176 |
+
|
177 |
+
# Kiểm tra request JSON hợp lệ
|
178 |
+
if not data or "message" not in data:
|
179 |
+
return jsonify({"error": "Dữ liệu không hợp lệ!"}), 400
|
180 |
+
|
181 |
+
message = data.get("message", "").strip()
|
182 |
+
conversation_id = data.get("conversation_id", "")
|
183 |
+
|
184 |
+
if not message:
|
185 |
+
return jsonify({"error": "Câu hỏi không hợp lệ!"}), 400
|
186 |
+
|
187 |
+
# Nếu không có conversation_id, tạo cuộc trò chuyện mới
|
188 |
+
if not conversation_id:
|
189 |
+
conversation_id = str(uuid.uuid4())
|
190 |
+
session["conversation_id"] = conversation_id
|
191 |
+
mongo.db.chat_history.insert_one({
|
192 |
+
"conversation_id": conversation_id,
|
193 |
+
"name": "Chưa có tên",
|
194 |
+
"messages": [],
|
195 |
+
"created_at": datetime.now().strftime("%d/%m/%Y %H:%M:%S")
|
196 |
+
})
|
197 |
+
|
198 |
+
# Gọi hàm lấy dữ liệu context
|
199 |
+
context_data = get_disease_and_generate_prompt(message, faiss_index, model_em, merged_df, top_k=5)
|
200 |
+
prompt = get_prompt(message, context_data)
|
201 |
+
|
202 |
+
# Cấu hình model Gemini
|
203 |
+
generation_config = {
|
204 |
+
"temperature": 0.2,
|
205 |
+
"top_p": 0.6,
|
206 |
+
"top_k": 40,
|
207 |
+
"max_output_tokens": 3000,
|
208 |
+
"response_mime_type": "text/plain",
|
209 |
+
}
|
210 |
+
|
211 |
+
# Gửi prompt đến model Gemini
|
212 |
+
model = genai.GenerativeModel("gemini-1.5-flash", generation_config=generation_config)
|
213 |
+
chat_session = model.start_chat(history=[])
|
214 |
+
response = chat_session.send_message(prompt)
|
215 |
+
|
216 |
+
# Lấy nội dung phản hồi từ bot
|
217 |
+
try:
|
218 |
+
text_response = response.candidates[0].content.parts[0].text
|
219 |
+
except Exception as e:
|
220 |
+
print(f"⚠️ Error getting response: {e}")
|
221 |
+
return jsonify({"error": "Lỗi khi lấy phản hồi từ mô hình!"}), 500
|
222 |
+
|
223 |
+
# Cấu trúc tin nhắn
|
224 |
+
timestamp = datetime.now().strftime("%d/%m/%Y %H:%M:%S")
|
225 |
+
user_message = {"text": message, "timestamp": timestamp, "sender": "user"}
|
226 |
+
bot_message = {"text": text_response, "timestamp": timestamp, "sender": "bot"}
|
227 |
+
|
228 |
+
# Cập nhật cuộc trò chuyện
|
229 |
+
mongo.db.chat_history.update_one(
|
230 |
+
{"conversation_id": conversation_id},
|
231 |
+
{"$push": {"messages": {"$each": [user_message, bot_message]}}},
|
232 |
+
upsert=True
|
233 |
+
)
|
234 |
+
|
235 |
+
# Trả về phản hồi JSON
|
236 |
+
return jsonify({
|
237 |
+
"conversation_id": conversation_id,
|
238 |
+
"status": "sent",
|
239 |
+
"bot_reply": text_response,
|
240 |
+
"timestamp": timestamp
|
241 |
+
})
|
242 |
+
|
243 |
+
except Exception as e:
|
244 |
+
print(f"🚨 Error in /query: {str(e)}")
|
245 |
+
return jsonify({"error": f"Lỗi xử lý: {str(e)}"}), 500
|
246 |
+
|
247 |
+
|
248 |
+
@app.route("/start_conversation", methods=["POST"])
|
249 |
+
def start_conversation():
|
250 |
+
try:
|
251 |
+
data = request.json
|
252 |
+
message = data.get("message", "").strip()
|
253 |
+
|
254 |
+
if not message:
|
255 |
+
return jsonify({"error": "Tin nhắn không hợp lệ!"}), 400
|
256 |
+
|
257 |
+
conversation_id = str(uuid.uuid4()) # Tạo conversation_id mới
|
258 |
+
session["conversation_id"] = conversation_id
|
259 |
+
|
260 |
+
mongo.db.chat_history.insert_one({
|
261 |
+
"conversation_id": conversation_id,
|
262 |
+
"name": message,
|
263 |
+
"messages": [],
|
264 |
+
"created_at": datetime.now().strftime("%d/%m/%Y %H:%M:%S")
|
265 |
+
})
|
266 |
+
|
267 |
+
return jsonify({"conversation_id": conversation_id})
|
268 |
+
|
269 |
+
except Exception as e:
|
270 |
+
print(f"🚨 Error in /start_conversation: {str(e)}")
|
271 |
+
return jsonify({"error": f"Lỗi xử lý: {str(e)}"}), 500
|
272 |
+
|
273 |
+
|
274 |
+
@app.route("/all_history", methods=["GET"])
|
275 |
+
def get_all_history():
|
276 |
+
"""Lấy danh sách tất cả cuộc trò chuyện"""
|
277 |
+
chats = list(mongo.db.chat_history.find({}, {"_id": 0, "messages": 0}))
|
278 |
+
return jsonify(chats)
|
279 |
+
|
280 |
+
|
281 |
+
@app.route("/conversation/<conversation_id>", methods=["GET"])
|
282 |
+
def get_conversation(conversation_id):
|
283 |
+
"""Lấy nội dung cuộc trò chuyện theo ID"""
|
284 |
+
chat = mongo.db.chat_history.find_one({"conversation_id": conversation_id}, {"_id": 0})
|
285 |
+
if not chat:
|
286 |
+
return jsonify({"error": "Cuộc trò chuyện không tồn tại"}), 404
|
287 |
+
return jsonify(chat)
|
288 |
+
|
289 |
+
@app.route('/get_conversations', methods=['GET'])
|
290 |
+
def get_conversations():
|
291 |
+
# Lấy tất cả cuộc trò chuyện từ MongoDB
|
292 |
+
conversations = list(mongo.db.chat_history.find({}, {"_id": 0}))
|
293 |
+
|
294 |
+
return jsonify([{
|
295 |
+
"conversation_id": conv.get("conversation_id", "N/A"),
|
296 |
+
"name": conv.get("name", "Chưa có tên"),
|
297 |
+
"bot_messages": [msg["text"] for msg in conv.get("messages", []) if msg.get("sender") == "bot"], # Chỉ lấy text của bot
|
298 |
+
"created_at": conv.get("created_at", "Không rõ ngày")
|
299 |
+
} for conv in conversations])
|
300 |
+
|
301 |
+
@app.route('/get_messages', methods=['GET'])
|
302 |
+
def get_messages():
|
303 |
+
conversation_id = request.args.get("conversation_id")
|
304 |
+
|
305 |
+
if not conversation_id:
|
306 |
+
return jsonify({"error": "Thiếu conversation_id"}), 400
|
307 |
+
|
308 |
+
# Truy vấn cuộc trò chuyện theo conversation_id
|
309 |
+
conversation = mongo.db.chat_history.find_one(
|
310 |
+
{"conversation_id": conversation_id},
|
311 |
+
{"_id": 0, "messages": 1,"name":1, "timestamp":1}
|
312 |
+
)
|
313 |
+
|
314 |
+
if conversation:
|
315 |
+
return jsonify(conversation) # Trả về danh sách tin nhắn
|
316 |
+
else:
|
317 |
+
return jsonify({"error": "Không tìm thấy cuộc trò chuyện"}), 404
|
318 |
+
|
319 |
+
|
320 |
+
|
321 |
+
@app.route("/")
|
322 |
+
def index():
|
323 |
+
return render_template('index.html')
|
324 |
+
|
325 |
+
@app.route('/register', methods=['GET', 'POST'])
|
326 |
+
def register():
|
327 |
+
if request.method == 'POST':
|
328 |
+
fullname = request.form['fullname']
|
329 |
+
username = request.form['username']
|
330 |
+
password = request.form['password']
|
331 |
+
hash_password = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())
|
332 |
+
|
333 |
+
# Insert user into the 'user' collection
|
334 |
+
mongo.db.user.insert_one({'fullname': fullname,'username': username, 'password': hash_password})
|
335 |
+
return redirect(url_for('login'))
|
336 |
+
return render_template('register.html')
|
337 |
+
|
338 |
+
@app.route('/login', methods=['GET', 'POST'])
|
339 |
+
def login():
|
340 |
+
if request.method == 'POST':
|
341 |
+
username = request.form['username']
|
342 |
+
password = request.form['password']
|
343 |
+
|
344 |
+
# Find user in the 'user' collection
|
345 |
+
user = mongo.db.user.find_one({'username': username})
|
346 |
+
|
347 |
+
# Kiểm tra nếu người dùng tồn tại và mật khẩu đúng
|
348 |
+
if user and bcrypt.checkpw(password.encode('utf-8'), user['password']):
|
349 |
+
# Lưu fullname của người dùng vào session
|
350 |
+
session['user'] = user.get('fullname', 'Unknown User') # Đảm bảo lấy giá trị 'fullname' nếu có
|
351 |
+
return redirect(url_for('dashboard'))
|
352 |
+
else:
|
353 |
+
return 'Invalid username or password'
|
354 |
+
return render_template('login.html')
|
355 |
+
|
356 |
+
@app.route("/dashboard")
|
357 |
+
def dashboard():
|
358 |
+
if 'user' in session:
|
359 |
+
current_time = datetime.now().strftime("%d/%m/%Y %H:%M:%S")
|
360 |
+
username = session["user"]
|
361 |
+
return render_template('dashboard.html',username=username,current_time=current_time)
|
362 |
+
return redirect(url_for('login'))
|
363 |
+
|
364 |
+
@app.route('/logout')
|
365 |
+
def logout():
|
366 |
+
session.pop('user', None)
|
367 |
+
return redirect(url_for('login'))
|
368 |
+
|
369 |
+
|
370 |
+
|
371 |
+
if __name__ == "__main__":
|
372 |
+
app.run(debug=True)
|
config.yaml
ADDED
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
gemini:
|
2 |
+
# Required: API Keys
|
3 |
+
api_keys:
|
4 |
+
- "AIzaSyBZy7wnuLbRpngTyuplRz1FNjpP8uxttVw"
|
5 |
+
- "AIzaSyCvlZ63Nkt5NpjdmxYPAsG8Qskex6usCFw"
|
6 |
+
- "AIzaSyB4BlhbVDHupKtExi59btmX5Y5Nkm0eN7g"
|
7 |
+
|
8 |
+
# Optional: Generation Settings
|
9 |
+
generation:
|
10 |
+
temperature: 0.7
|
11 |
+
top_p: 1.0
|
12 |
+
top_k: 40
|
13 |
+
max_output_tokens: 8192
|
14 |
+
stop_sequences: []
|
15 |
+
response_mime_type: "text/plain"
|
16 |
+
|
17 |
+
# Optional: Rate Limiting
|
18 |
+
rate_limits:
|
19 |
+
requests_per_minute: 60
|
20 |
+
reset_window: 60 # seconds
|
21 |
+
|
22 |
+
# Optional: Strategies
|
23 |
+
strategies:
|
24 |
+
content: "fallback" # round_robin, fallback, retry
|
25 |
+
key_rotation: "smart_cooldown" # smart_cooldown, sequential, round_robin, least_used
|
26 |
+
|
27 |
+
# Optional: Retry Settings
|
28 |
+
retry:
|
29 |
+
max_attempts: 3
|
30 |
+
delay: 30 # seconds
|
31 |
+
|
32 |
+
# Optional: Model Settings
|
33 |
+
default_model: "gemini-2.0-flash-exp"
|
34 |
+
system_instruction: null # Custom system prompt
|
gemini_handler.py
ADDED
@@ -0,0 +1,559 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from abc import ABC, abstractmethod
|
2 |
+
import google.generativeai as genai
|
3 |
+
import time
|
4 |
+
import os
|
5 |
+
import yaml
|
6 |
+
from typing import List, Dict, Any, Optional, Tuple, Union
|
7 |
+
from enum import Enum
|
8 |
+
from dataclasses import dataclass
|
9 |
+
from itertools import cycle
|
10 |
+
from pathlib import Path
|
11 |
+
|
12 |
+
@dataclass
|
13 |
+
class GenerationConfig:
|
14 |
+
"""Configuration for model generation parameters."""
|
15 |
+
temperature: float = 1.0
|
16 |
+
top_p: float = 1.0
|
17 |
+
top_k: int = 40
|
18 |
+
max_output_tokens: int = 8192
|
19 |
+
stop_sequences: Optional[List[str]] = None
|
20 |
+
response_mime_type: str = "text/plain"
|
21 |
+
|
22 |
+
def to_dict(self) -> Dict[str, Any]:
|
23 |
+
"""Convert config to dictionary, excluding None values."""
|
24 |
+
return {k: v for k, v in self.__dict__.items() if v is not None}
|
25 |
+
|
26 |
+
|
27 |
+
@dataclass
|
28 |
+
class ModelResponse:
|
29 |
+
"""Represents a standardized response from any model."""
|
30 |
+
success: bool
|
31 |
+
model: str
|
32 |
+
text: str = ""
|
33 |
+
error: str = ""
|
34 |
+
time: float = 0.0
|
35 |
+
attempts: int = 1
|
36 |
+
api_key_index: int = 0
|
37 |
+
|
38 |
+
|
39 |
+
class Strategy(Enum):
|
40 |
+
"""Available content generation strategies."""
|
41 |
+
ROUND_ROBIN = "round_robin"
|
42 |
+
FALLBACK = "fallback"
|
43 |
+
RETRY = "retry"
|
44 |
+
|
45 |
+
|
46 |
+
class KeyRotationStrategy(Enum):
|
47 |
+
"""Available key rotation strategies."""
|
48 |
+
SEQUENTIAL = "sequential"
|
49 |
+
ROUND_ROBIN = "round_robin"
|
50 |
+
LEAST_USED = "least_used"
|
51 |
+
SMART_COOLDOWN = "smart_cooldown"
|
52 |
+
|
53 |
+
|
54 |
+
@dataclass
|
55 |
+
class KeyStats:
|
56 |
+
"""Track usage statistics for each API key."""
|
57 |
+
uses: int = 0
|
58 |
+
last_used: float = 0
|
59 |
+
failures: int = 0
|
60 |
+
rate_limited_until: float = 0
|
61 |
+
|
62 |
+
|
63 |
+
class ConfigLoader:
|
64 |
+
"""Handles loading configuration from various sources."""
|
65 |
+
|
66 |
+
@staticmethod
|
67 |
+
def load_api_keys(config_path: Optional[Union[str, Path]] = None) -> List[str]:
|
68 |
+
"""
|
69 |
+
Load API keys from multiple sources in priority order:
|
70 |
+
1. YAML config file if provided
|
71 |
+
2. Environment variables (GEMINI_API_KEYS as comma-separated string)
|
72 |
+
3. Single GEMINI_API_KEY environment variable
|
73 |
+
"""
|
74 |
+
# Try loading from YAML config
|
75 |
+
if config_path:
|
76 |
+
try:
|
77 |
+
with open(config_path, 'r') as f:
|
78 |
+
config = yaml.safe_load(f)
|
79 |
+
if config and 'gemini' in config and 'api_keys' in config['gemini']:
|
80 |
+
keys = config['gemini']['api_keys']
|
81 |
+
if isinstance(keys, list) and all(isinstance(k, str) for k in keys):
|
82 |
+
return keys
|
83 |
+
except Exception as e:
|
84 |
+
print(f"Warning: Failed to load config from {config_path}: {e}")
|
85 |
+
|
86 |
+
# Try loading from GEMINI_API_KEYS environment variable
|
87 |
+
api_keys_str = os.getenv('GEMINI_API_KEYS')
|
88 |
+
if api_keys_str:
|
89 |
+
keys = [k.strip() for k in api_keys_str.split(',') if k.strip()]
|
90 |
+
if keys:
|
91 |
+
return keys
|
92 |
+
|
93 |
+
# Try loading single API key
|
94 |
+
single_key = os.getenv('GEMINI_API_KEY')
|
95 |
+
if single_key:
|
96 |
+
return [single_key]
|
97 |
+
|
98 |
+
raise ValueError(
|
99 |
+
"No API keys found. Please provide keys via config file, "
|
100 |
+
"GEMINI_API_KEYS environment variable (comma-separated), "
|
101 |
+
"or GEMINI_API_KEY environment variable."
|
102 |
+
)
|
103 |
+
|
104 |
+
|
105 |
+
class ModelConfig:
|
106 |
+
"""Configuration for model settings."""
|
107 |
+
def __init__(self):
|
108 |
+
self.models = [
|
109 |
+
"gemini-2.0-flash-exp",
|
110 |
+
"gemini-1.5-pro",
|
111 |
+
"learnlm-1.5-pro-experimental",
|
112 |
+
"gemini-exp-1206",
|
113 |
+
"gemini-exp-1121",
|
114 |
+
"gemini-exp-1114",
|
115 |
+
"gemini-2.0-flash-thinking-exp-1219",
|
116 |
+
"gemini-1.5-flash"
|
117 |
+
]
|
118 |
+
self.max_retries = 3
|
119 |
+
self.retry_delay = 30
|
120 |
+
self.default_model = "gemini-2.0-flash-exp"
|
121 |
+
|
122 |
+
|
123 |
+
class KeyRotationManager:
|
124 |
+
"""Enhanced key rotation manager with multiple strategies."""
|
125 |
+
def __init__(
|
126 |
+
self,
|
127 |
+
api_keys: List[str],
|
128 |
+
strategy: KeyRotationStrategy = KeyRotationStrategy.ROUND_ROBIN,
|
129 |
+
rate_limit: int = 60,
|
130 |
+
reset_window: int = 60
|
131 |
+
):
|
132 |
+
if not api_keys:
|
133 |
+
raise ValueError("At least one API key must be provided")
|
134 |
+
|
135 |
+
self.api_keys = api_keys
|
136 |
+
self.strategy = strategy
|
137 |
+
self.rate_limit = rate_limit
|
138 |
+
self.reset_window = reset_window
|
139 |
+
|
140 |
+
# Initialize tracking
|
141 |
+
self.key_stats = {i: KeyStats() for i in range(len(api_keys))}
|
142 |
+
self._key_cycle = cycle(range(len(api_keys)))
|
143 |
+
self.current_index = 0
|
144 |
+
|
145 |
+
def _is_key_available(self, key_index: int) -> bool:
|
146 |
+
"""Check if a key is available based on rate limits and cooldown."""
|
147 |
+
stats = self.key_stats[key_index]
|
148 |
+
current_time = time.time()
|
149 |
+
|
150 |
+
if current_time < stats.rate_limited_until:
|
151 |
+
return False
|
152 |
+
|
153 |
+
if current_time - stats.last_used > self.reset_window:
|
154 |
+
stats.uses = 0
|
155 |
+
|
156 |
+
return stats.uses < self.rate_limit
|
157 |
+
|
158 |
+
def _get_sequential_key(self) -> Tuple[str, int]:
|
159 |
+
"""Get next key using sequential strategy."""
|
160 |
+
start_index = self.current_index
|
161 |
+
|
162 |
+
while True:
|
163 |
+
if self._is_key_available(self.current_index):
|
164 |
+
key_index = self.current_index
|
165 |
+
self.current_index = (self.current_index + 1) % len(self.api_keys)
|
166 |
+
return self.api_keys[key_index], key_index
|
167 |
+
|
168 |
+
self.current_index = (self.current_index + 1) % len(self.api_keys)
|
169 |
+
if self.current_index == start_index:
|
170 |
+
self._handle_all_keys_busy()
|
171 |
+
|
172 |
+
def _get_round_robin_key(self) -> Tuple[str, int]:
|
173 |
+
"""Get next key using round-robin strategy."""
|
174 |
+
start_index = next(self._key_cycle)
|
175 |
+
current_index = start_index
|
176 |
+
|
177 |
+
while True:
|
178 |
+
if self._is_key_available(current_index):
|
179 |
+
return self.api_keys[current_index], current_index
|
180 |
+
|
181 |
+
current_index = next(self._key_cycle)
|
182 |
+
if current_index == start_index:
|
183 |
+
self._handle_all_keys_busy()
|
184 |
+
|
185 |
+
def _get_least_used_key(self) -> Tuple[str, int]:
|
186 |
+
"""Get key with lowest usage count."""
|
187 |
+
while True:
|
188 |
+
available_keys = [
|
189 |
+
(idx, stats) for idx, stats in self.key_stats.items()
|
190 |
+
if self._is_key_available(idx)
|
191 |
+
]
|
192 |
+
|
193 |
+
if available_keys:
|
194 |
+
key_index, _ = min(available_keys, key=lambda x: x[1].uses)
|
195 |
+
return self.api_keys[key_index], key_index
|
196 |
+
|
197 |
+
self._handle_all_keys_busy()
|
198 |
+
|
199 |
+
def _get_smart_cooldown_key(self) -> Tuple[str, int]:
|
200 |
+
"""Get key using smart cooldown strategy."""
|
201 |
+
while True:
|
202 |
+
current_time = time.time()
|
203 |
+
available_keys = [
|
204 |
+
(idx, stats) for idx, stats in self.key_stats.items()
|
205 |
+
if current_time >= stats.rate_limited_until and self._is_key_available(idx)
|
206 |
+
]
|
207 |
+
|
208 |
+
if available_keys:
|
209 |
+
key_index, _ = min(
|
210 |
+
available_keys,
|
211 |
+
key=lambda x: (x[1].failures, -(current_time - x[1].last_used))
|
212 |
+
)
|
213 |
+
return self.api_keys[key_index], key_index
|
214 |
+
|
215 |
+
self._handle_all_keys_busy()
|
216 |
+
|
217 |
+
def _handle_all_keys_busy(self) -> None:
|
218 |
+
"""Handle situation when all keys are busy."""
|
219 |
+
current_time = time.time()
|
220 |
+
any_reset = False
|
221 |
+
|
222 |
+
for idx, stats in self.key_stats.items():
|
223 |
+
if current_time - stats.last_used > self.reset_window:
|
224 |
+
stats.uses = 0
|
225 |
+
any_reset = True
|
226 |
+
|
227 |
+
if not any_reset:
|
228 |
+
time.sleep(1)
|
229 |
+
|
230 |
+
def get_next_key(self) -> Tuple[str, int]:
|
231 |
+
"""Get next available API key based on selected strategy."""
|
232 |
+
strategy_methods = {
|
233 |
+
KeyRotationStrategy.SEQUENTIAL: self._get_sequential_key,
|
234 |
+
KeyRotationStrategy.ROUND_ROBIN: self._get_round_robin_key,
|
235 |
+
KeyRotationStrategy.LEAST_USED: self._get_least_used_key,
|
236 |
+
KeyRotationStrategy.SMART_COOLDOWN: self._get_smart_cooldown_key
|
237 |
+
}
|
238 |
+
|
239 |
+
method = strategy_methods.get(self.strategy)
|
240 |
+
if not method:
|
241 |
+
raise ValueError(f"Unknown strategy: {self.strategy}")
|
242 |
+
|
243 |
+
api_key, key_index = method()
|
244 |
+
|
245 |
+
stats = self.key_stats[key_index]
|
246 |
+
stats.uses += 1
|
247 |
+
stats.last_used = time.time()
|
248 |
+
|
249 |
+
return api_key, key_index
|
250 |
+
|
251 |
+
def mark_success(self, key_index: int) -> None:
|
252 |
+
"""Mark successful API call."""
|
253 |
+
if 0 <= key_index < len(self.api_keys):
|
254 |
+
self.key_stats[key_index].failures = 0
|
255 |
+
|
256 |
+
def mark_rate_limited(self, key_index: int) -> None:
|
257 |
+
"""Mark API key as rate limited."""
|
258 |
+
if 0 <= key_index < len(self.api_keys):
|
259 |
+
stats = self.key_stats[key_index]
|
260 |
+
stats.failures += 1
|
261 |
+
stats.rate_limited_until = time.time() + self.reset_window
|
262 |
+
stats.uses = self.rate_limit
|
263 |
+
|
264 |
+
|
265 |
+
class ResponseHandler:
|
266 |
+
"""Handles and processes model responses."""
|
267 |
+
@staticmethod
|
268 |
+
def process_response(
|
269 |
+
response: Any,
|
270 |
+
model_name: str,
|
271 |
+
start_time: float,
|
272 |
+
key_index: int
|
273 |
+
) -> ModelResponse:
|
274 |
+
"""Process and validate model response."""
|
275 |
+
try:
|
276 |
+
if hasattr(response, 'candidates') and response.candidates:
|
277 |
+
finish_reason = response.candidates[0].finish_reason
|
278 |
+
if finish_reason == 4: # Copyright material
|
279 |
+
return ModelResponse(
|
280 |
+
success=False,
|
281 |
+
model=model_name,
|
282 |
+
error='Copyright material detected in response',
|
283 |
+
time=time.time() - start_time,
|
284 |
+
api_key_index=key_index
|
285 |
+
)
|
286 |
+
|
287 |
+
return ModelResponse(
|
288 |
+
success=True,
|
289 |
+
model=model_name,
|
290 |
+
text=response.text,
|
291 |
+
time=time.time() - start_time,
|
292 |
+
api_key_index=key_index
|
293 |
+
)
|
294 |
+
except Exception as e:
|
295 |
+
if "The `response.text` quick accessor requires the response to contain a valid `Part`" in str(e):
|
296 |
+
return ModelResponse(
|
297 |
+
success=False,
|
298 |
+
model=model_name,
|
299 |
+
error='No valid response parts available',
|
300 |
+
time=time.time() - start_time,
|
301 |
+
api_key_index=key_index
|
302 |
+
)
|
303 |
+
raise
|
304 |
+
|
305 |
+
|
306 |
+
class ContentStrategy(ABC):
|
307 |
+
"""Abstract base class for content generation strategies."""
|
308 |
+
def __init__(
|
309 |
+
self,
|
310 |
+
config: ModelConfig,
|
311 |
+
key_manager: KeyRotationManager,
|
312 |
+
system_instruction: Optional[str] = None,
|
313 |
+
generation_config: Optional[GenerationConfig] = None
|
314 |
+
):
|
315 |
+
self.config = config
|
316 |
+
self.key_manager = key_manager
|
317 |
+
self.system_instruction = system_instruction
|
318 |
+
self.generation_config = generation_config or GenerationConfig()
|
319 |
+
|
320 |
+
@abstractmethod
|
321 |
+
def generate(self, prompt: str, model_name: str) -> ModelResponse:
|
322 |
+
"""Generate content using the specific strategy."""
|
323 |
+
pass
|
324 |
+
|
325 |
+
def _try_generate(self, model_name: str, prompt: str, start_time: float) -> ModelResponse:
|
326 |
+
"""Helper method for generating content with key rotation."""
|
327 |
+
api_key, key_index = self.key_manager.get_next_key()
|
328 |
+
try:
|
329 |
+
genai.configure(api_key=api_key)
|
330 |
+
model = genai.GenerativeModel(
|
331 |
+
model_name=model_name,
|
332 |
+
generation_config=self.generation_config.to_dict(),
|
333 |
+
system_instruction=self.system_instruction
|
334 |
+
)
|
335 |
+
response = model.generate_content(prompt)
|
336 |
+
|
337 |
+
result = ResponseHandler.process_response(response, model_name, start_time, key_index)
|
338 |
+
if result.success:
|
339 |
+
self.key_manager.mark_success(key_index)
|
340 |
+
return result
|
341 |
+
|
342 |
+
except Exception as e:
|
343 |
+
if "429" in str(e):
|
344 |
+
self.key_manager.mark_rate_limited(key_index)
|
345 |
+
return ModelResponse(
|
346 |
+
success=False,
|
347 |
+
model=model_name,
|
348 |
+
error=str(e),
|
349 |
+
time=time.time() - start_time,
|
350 |
+
api_key_index=key_index
|
351 |
+
)
|
352 |
+
|
353 |
+
|
354 |
+
class RoundRobinStrategy(ContentStrategy):
|
355 |
+
"""Round robin implementation of content generation."""
|
356 |
+
def __init__(self, *args, **kwargs):
|
357 |
+
super().__init__(*args, **kwargs)
|
358 |
+
self._current_index = 0
|
359 |
+
|
360 |
+
def _get_next_model(self) -> str:
|
361 |
+
"""Get next model in round-robin fashion."""
|
362 |
+
model = self.config.models[self._current_index]
|
363 |
+
self._current_index = (self._current_index + 1) % len(self.config.models)
|
364 |
+
return model
|
365 |
+
|
366 |
+
def generate(self, prompt: str, _: str) -> ModelResponse:
|
367 |
+
start_time = time.time()
|
368 |
+
|
369 |
+
for _ in range(len(self.config.models)):
|
370 |
+
model_name = self._get_next_model()
|
371 |
+
result = self._try_generate(model_name, prompt, start_time)
|
372 |
+
if result.success or 'Copyright' in result.error:
|
373 |
+
return result
|
374 |
+
|
375 |
+
return ModelResponse(
|
376 |
+
success=False,
|
377 |
+
model='all_models_failed',
|
378 |
+
error='All models failed (rate limited or copyright issues)',
|
379 |
+
time=time.time() - start_time
|
380 |
+
)
|
381 |
+
|
382 |
+
|
383 |
+
class FallbackStrategy(ContentStrategy):
|
384 |
+
"""Fallback implementation of content generation."""
|
385 |
+
def generate(self, prompt: str, start_model: str) -> ModelResponse:
|
386 |
+
start_time = time.time()
|
387 |
+
|
388 |
+
try:
|
389 |
+
start_index = self.config.models.index(start_model)
|
390 |
+
except ValueError:
|
391 |
+
return ModelResponse(
|
392 |
+
success=False,
|
393 |
+
model=start_model,
|
394 |
+
error=f"Model {start_model} not found in available models",
|
395 |
+
time=time.time() - start_time
|
396 |
+
)
|
397 |
+
|
398 |
+
for model_name in self.config.models[start_index:]:
|
399 |
+
result = self._try_generate(model_name, prompt, start_time)
|
400 |
+
if result.success or 'Copyright' in result.error:
|
401 |
+
return result
|
402 |
+
|
403 |
+
return ModelResponse(
|
404 |
+
success=False,
|
405 |
+
model='all_models_failed',
|
406 |
+
error='All models failed (rate limited or copyright issues)',
|
407 |
+
time=time.time() - start_time
|
408 |
+
)
|
409 |
+
|
410 |
+
|
411 |
+
class RetryStrategy(ContentStrategy):
|
412 |
+
"""Retry implementation of content generation."""
|
413 |
+
def generate(self, prompt: str, model_name: str) -> ModelResponse:
|
414 |
+
start_time = time.time()
|
415 |
+
|
416 |
+
for attempt in range(self.config.max_retries):
|
417 |
+
result = self._try_generate(model_name, prompt, start_time)
|
418 |
+
result.attempts = attempt + 1
|
419 |
+
|
420 |
+
if result.success or 'Copyright' in result.error:
|
421 |
+
return result
|
422 |
+
|
423 |
+
if attempt < self.config.max_retries - 1:
|
424 |
+
print(f"Error encountered. Waiting {self.config.retry_delay}s... "
|
425 |
+
f"(Attempt {attempt + 1}/{self.config.max_retries})")
|
426 |
+
time.sleep(self.config.retry_delay)
|
427 |
+
|
428 |
+
return ModelResponse(
|
429 |
+
success=False,
|
430 |
+
model=model_name,
|
431 |
+
error='Max retries exceeded',
|
432 |
+
time=time.time() - start_time,
|
433 |
+
attempts=self.config.max_retries
|
434 |
+
)
|
435 |
+
|
436 |
+
|
437 |
+
class GeminiHandler:
|
438 |
+
"""Main handler class for Gemini API interactions."""
|
439 |
+
def __init__(
|
440 |
+
self,
|
441 |
+
api_keys: Optional[List[str]] = None,
|
442 |
+
config_path: Optional[Union[str, Path]] = None,
|
443 |
+
content_strategy: Strategy = Strategy.ROUND_ROBIN,
|
444 |
+
key_strategy: KeyRotationStrategy = KeyRotationStrategy.ROUND_ROBIN,
|
445 |
+
system_instruction: Optional[str] = None,
|
446 |
+
generation_config: Optional[GenerationConfig] = None
|
447 |
+
):
|
448 |
+
"""
|
449 |
+
Initialize GeminiHandler with flexible configuration options.
|
450 |
+
|
451 |
+
Args:
|
452 |
+
api_keys: Optional list of API keys
|
453 |
+
config_path: Optional path to YAML config file
|
454 |
+
content_strategy: Strategy for content generation
|
455 |
+
key_strategy: Strategy for key rotation
|
456 |
+
system_instruction: Optional system instruction
|
457 |
+
generation_config: Optional generation configuration
|
458 |
+
"""
|
459 |
+
# Load API keys from provided list or config sources
|
460 |
+
self.api_keys = api_keys or ConfigLoader.load_api_keys(config_path)
|
461 |
+
|
462 |
+
self.config = ModelConfig()
|
463 |
+
self.key_manager = KeyRotationManager(
|
464 |
+
api_keys=self.api_keys,
|
465 |
+
strategy=key_strategy,
|
466 |
+
rate_limit=60,
|
467 |
+
reset_window=60
|
468 |
+
)
|
469 |
+
self.system_instruction = system_instruction
|
470 |
+
self.generation_config = generation_config
|
471 |
+
self._strategy = self._create_strategy(content_strategy)
|
472 |
+
|
473 |
+
def _create_strategy(self, strategy: Strategy) -> ContentStrategy:
|
474 |
+
"""Factory method to create appropriate strategy."""
|
475 |
+
strategies = {
|
476 |
+
Strategy.ROUND_ROBIN: RoundRobinStrategy,
|
477 |
+
Strategy.FALLBACK: FallbackStrategy,
|
478 |
+
Strategy.RETRY: RetryStrategy
|
479 |
+
}
|
480 |
+
|
481 |
+
strategy_class = strategies.get(strategy)
|
482 |
+
if not strategy_class:
|
483 |
+
raise ValueError(f"Unknown strategy: {strategy}")
|
484 |
+
|
485 |
+
return strategy_class(
|
486 |
+
config=self.config,
|
487 |
+
key_manager=self.key_manager,
|
488 |
+
system_instruction=self.system_instruction,
|
489 |
+
generation_config=self.generation_config
|
490 |
+
)
|
491 |
+
|
492 |
+
def generate_content(
|
493 |
+
self,
|
494 |
+
prompt: str,
|
495 |
+
model_name: Optional[str] = None,
|
496 |
+
return_stats: bool = False
|
497 |
+
) -> Dict[str, Any]:
|
498 |
+
"""
|
499 |
+
Generate content using the selected strategies.
|
500 |
+
|
501 |
+
Args:
|
502 |
+
prompt: The input prompt for content generation
|
503 |
+
model_name: Optional specific model to use (default: None)
|
504 |
+
return_stats: Whether to include key usage statistics (default: False)
|
505 |
+
|
506 |
+
Returns:
|
507 |
+
Dictionary containing generation results and optionally key statistics
|
508 |
+
"""
|
509 |
+
if not model_name:
|
510 |
+
model_name = self.config.default_model
|
511 |
+
|
512 |
+
response = self._strategy.generate(prompt, model_name)
|
513 |
+
result = response.__dict__
|
514 |
+
|
515 |
+
if return_stats:
|
516 |
+
result["key_stats"] = {
|
517 |
+
idx: {
|
518 |
+
"uses": stats.uses,
|
519 |
+
"last_used": stats.last_used,
|
520 |
+
"failures": stats.failures,
|
521 |
+
"rate_limited_until": stats.rate_limited_until
|
522 |
+
}
|
523 |
+
for idx, stats in self.key_manager.key_stats.items()
|
524 |
+
}
|
525 |
+
|
526 |
+
return result
|
527 |
+
|
528 |
+
def get_key_stats(self, key_index: Optional[int] = None) -> Dict[int, Dict[str, Any]]:
|
529 |
+
"""
|
530 |
+
Get current key usage statistics.
|
531 |
+
|
532 |
+
Args:
|
533 |
+
key_index: Optional specific key index to get stats for
|
534 |
+
|
535 |
+
Returns:
|
536 |
+
Dictionary of key statistics
|
537 |
+
"""
|
538 |
+
if key_index is not None:
|
539 |
+
if 0 <= key_index < len(self.key_manager.api_keys):
|
540 |
+
stats = self.key_manager.key_stats[key_index]
|
541 |
+
return {
|
542 |
+
key_index: {
|
543 |
+
"uses": stats.uses,
|
544 |
+
"last_used": stats.last_used,
|
545 |
+
"failures": stats.failures,
|
546 |
+
"rate_limited_until": stats.rate_limited_until
|
547 |
+
}
|
548 |
+
}
|
549 |
+
raise ValueError(f"Invalid key index: {key_index}")
|
550 |
+
|
551 |
+
return {
|
552 |
+
idx: {
|
553 |
+
"uses": stats.uses,
|
554 |
+
"last_used": stats.last_used,
|
555 |
+
"failures": stats.failures,
|
556 |
+
"rate_limited_until": stats.rate_limited_until
|
557 |
+
}
|
558 |
+
for idx, stats in self.key_manager.key_stats.items()
|
559 |
+
}
|
requirements.txt
ADDED
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
Flask
|
2 |
+
flask_pymongo
|
3 |
+
flask_cors
|
4 |
+
bcrypt
|
5 |
+
sentence-transformers
|
6 |
+
faiss-cpu
|
7 |
+
numpy
|
8 |
+
pandas
|
9 |
+
google-generativeai
|
10 |
+
python-dotenv
|
11 |
+
pymongo
|
12 |
+
gunicorn
|
source/faiss_index_drug.bin
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:ea0955d2743718823cbf92570fc5ba7004bc0d6ba9457ed88df8df65becc2c66
|
3 |
+
size 568365
|
source/faiss_index_vn.bin
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:40ea00c1b969a484fa7c4f0eae61d569f8f663c1140b5fd6bbb950eb08184d31
|
3 |
+
size 1136685
|
source/merged_df.csv
ADDED
The diff for this file is too large to render.
See raw diff
|
|
source/merged_df_vn.csv
ADDED
The diff for this file is too large to render.
See raw diff
|
|
source/sentence_embeddings.pkl
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:a561406ed04d8a7227231108dfd66ba8a7cf5a9da4a5a9d6e598de7438ab67c4
|
3 |
+
size 568483
|
source/sentence_embeddings_vn.pkl
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:38fad112ffbfd8eb359e55b5e308ffb9feb5e19ee8391779db8619ab6a3a87e4
|
3 |
+
size 1136803
|
static/style.css
ADDED
@@ -0,0 +1,267 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
:root {
|
2 |
+
--primary: #202123;
|
3 |
+
--overhover: hsla(240, 9%, 59%, 0.1);
|
4 |
+
}
|
5 |
+
|
6 |
+
* {
|
7 |
+
margin: 0;
|
8 |
+
padding: 0;
|
9 |
+
font-family: courier;
|
10 |
+
}
|
11 |
+
|
12 |
+
nav {
|
13 |
+
height: 50px;
|
14 |
+
background: var(--primary);
|
15 |
+
color: white;
|
16 |
+
display: none;
|
17 |
+
justify-content: space-between;
|
18 |
+
align-items: center;
|
19 |
+
position: fixed;
|
20 |
+
width: 100%;
|
21 |
+
}
|
22 |
+
|
23 |
+
nav div {
|
24 |
+
margin: 0 20px;
|
25 |
+
display: inline-block;
|
26 |
+
overflow: hidden;
|
27 |
+
white-space: nowrap;
|
28 |
+
text-overflow: ellipsis;
|
29 |
+
}
|
30 |
+
|
31 |
+
.ham-menu {
|
32 |
+
background: var(--primary);
|
33 |
+
color: white;
|
34 |
+
border: none;
|
35 |
+
cursor: pointer;
|
36 |
+
}
|
37 |
+
|
38 |
+
.ham-menu span {
|
39 |
+
display: block;
|
40 |
+
width: 25px;
|
41 |
+
height: 3px;
|
42 |
+
margin-bottom: 5px;
|
43 |
+
position: relative;
|
44 |
+
background: black;
|
45 |
+
border-radius: 3px;
|
46 |
+
z-index: 1;
|
47 |
+
transform-origin: 4px 0px;
|
48 |
+
transition: transform 0.5s cubic-bezier(0.77, 0.2, 0.05, 1),
|
49 |
+
background 0.5s cubic-bezier(0.77, 0.2, 0.05, 1), opacity 0.55s ease;
|
50 |
+
}
|
51 |
+
|
52 |
+
.ham-menu:hover span {
|
53 |
+
background: white;
|
54 |
+
}
|
55 |
+
|
56 |
+
#main {
|
57 |
+
display: flex;
|
58 |
+
}
|
59 |
+
|
60 |
+
/*Side Nav*/
|
61 |
+
#sidenav {
|
62 |
+
height: 100vh;
|
63 |
+
width: 20rem;
|
64 |
+
background-color: var(--primary);
|
65 |
+
font-size: 1rem;
|
66 |
+
color: white;
|
67 |
+
position: fixed;
|
68 |
+
left: 0;
|
69 |
+
top: 0;
|
70 |
+
margin: 0;
|
71 |
+
z-index: 11;
|
72 |
+
}
|
73 |
+
|
74 |
+
#sidenav .sidenav-content {
|
75 |
+
padding: 0.5rem;
|
76 |
+
}
|
77 |
+
|
78 |
+
#close {
|
79 |
+
position: absolute;
|
80 |
+
right: -40px;
|
81 |
+
background: var(--primary);
|
82 |
+
color: white;
|
83 |
+
border: 1px solid grey;
|
84 |
+
border-radius: 5px;
|
85 |
+
cursor: pointer;
|
86 |
+
display: none;
|
87 |
+
width: 40px;
|
88 |
+
height: 40px;
|
89 |
+
}
|
90 |
+
#close span {
|
91 |
+
width: 40px;
|
92 |
+
height: 40px;
|
93 |
+
position: relative;
|
94 |
+
}
|
95 |
+
#close span:before,
|
96 |
+
#close span:after {
|
97 |
+
position: absolute;
|
98 |
+
content: " ";
|
99 |
+
top: -16px;
|
100 |
+
left: -1px;
|
101 |
+
height: 33px;
|
102 |
+
width: 3px;
|
103 |
+
background-color: black;
|
104 |
+
}
|
105 |
+
#close span:before {
|
106 |
+
transform: rotate(45deg);
|
107 |
+
}
|
108 |
+
#close span:after {
|
109 |
+
transform: rotate(-45deg);
|
110 |
+
}
|
111 |
+
|
112 |
+
#close:hover span:after,
|
113 |
+
#close:hover span:before {
|
114 |
+
background: white;
|
115 |
+
}
|
116 |
+
|
117 |
+
#new-chat-btn {
|
118 |
+
background: var(--primary);
|
119 |
+
color: white;
|
120 |
+
border: 1px solid grey;
|
121 |
+
border-radius: 5px;
|
122 |
+
width: 100%;
|
123 |
+
cursor: pointer;
|
124 |
+
text-align: start;
|
125 |
+
padding: 0.7rem;
|
126 |
+
font-size: inherit;
|
127 |
+
}
|
128 |
+
|
129 |
+
#new-chat-btn:hover {
|
130 |
+
background: var(--overhover);
|
131 |
+
}
|
132 |
+
|
133 |
+
.new-chat {
|
134 |
+
cursor: pointer;
|
135 |
+
}
|
136 |
+
|
137 |
+
.saved-chats {
|
138 |
+
margin: 0.5rem 0;
|
139 |
+
}
|
140 |
+
|
141 |
+
.saved-chats p {
|
142 |
+
padding: 0.8rem;
|
143 |
+
margin: 3px 0;
|
144 |
+
border-radius: 5px;
|
145 |
+
cursor: pointer;
|
146 |
+
}
|
147 |
+
|
148 |
+
.saved-chats p:hover {
|
149 |
+
background: var(--overhover);
|
150 |
+
}
|
151 |
+
|
152 |
+
.saved-chats .selected {
|
153 |
+
padding: 0.8rem;
|
154 |
+
background: var(--overhover);
|
155 |
+
margin: 3px 0;
|
156 |
+
border-radius: 5px;
|
157 |
+
}
|
158 |
+
|
159 |
+
.config {
|
160 |
+
margin: 0.5rem 0;
|
161 |
+
position: absolute;
|
162 |
+
bottom: 0;
|
163 |
+
width: inherit;
|
164 |
+
background: var(--primary);
|
165 |
+
}
|
166 |
+
|
167 |
+
.config p {
|
168 |
+
padding: 0.7rem;
|
169 |
+
cursor: pointer;
|
170 |
+
margin: 5px 0;
|
171 |
+
border-radius: 5px;
|
172 |
+
}
|
173 |
+
|
174 |
+
.config p:hover {
|
175 |
+
background: var(--overhover);
|
176 |
+
}
|
177 |
+
|
178 |
+
.config hr {
|
179 |
+
width: 19rem;
|
180 |
+
}
|
181 |
+
|
182 |
+
/*Main Content Body*/
|
183 |
+
#content-body {
|
184 |
+
height: 100vh;
|
185 |
+
background-color: white;
|
186 |
+
text-align: center;
|
187 |
+
margin-left: 20rem;
|
188 |
+
flex: 1;
|
189 |
+
}
|
190 |
+
|
191 |
+
#messages {
|
192 |
+
padding-bottom: 100px;
|
193 |
+
}
|
194 |
+
|
195 |
+
.message-div {
|
196 |
+
display: flex;
|
197 |
+
margin: 10px auto;
|
198 |
+
max-width: 800px;
|
199 |
+
justify-content: start;
|
200 |
+
}
|
201 |
+
|
202 |
+
.user-message {
|
203 |
+
background: ;
|
204 |
+
padding: 10px;
|
205 |
+
}
|
206 |
+
|
207 |
+
.gpt-message {
|
208 |
+
background: rgba(247, 247, 248);
|
209 |
+
padding: 10px;
|
210 |
+
}
|
211 |
+
|
212 |
+
.message-profile-pic {
|
213 |
+
margin-right: 20px;
|
214 |
+
}
|
215 |
+
|
216 |
+
.message-content {
|
217 |
+
text-align: start;
|
218 |
+
margin-top: 5px;
|
219 |
+
}
|
220 |
+
|
221 |
+
.message-content p {
|
222 |
+
margin-bottom: 20px;
|
223 |
+
}
|
224 |
+
|
225 |
+
#chat-section {
|
226 |
+
width: -webkit-fill-available;
|
227 |
+
height: 130px;
|
228 |
+
background-image: linear-gradient(to bottom, transparent 10%, white 90%);
|
229 |
+
position: fixed;
|
230 |
+
bottom: 0;
|
231 |
+
}
|
232 |
+
|
233 |
+
#chat-section div {
|
234 |
+
max-width: 800px;
|
235 |
+
margin: 2rem auto;
|
236 |
+
padding: 0 20px;
|
237 |
+
}
|
238 |
+
|
239 |
+
#chat-section input {
|
240 |
+
padding: 0.9rem;
|
241 |
+
border-radius: 5px;
|
242 |
+
border: 0.1px solid grey;
|
243 |
+
width: 90%;
|
244 |
+
font-size: 20px;
|
245 |
+
box-shadow: 0 0 7px 0px grey;
|
246 |
+
}
|
247 |
+
|
248 |
+
#chat-section input:focus {
|
249 |
+
outline: none;
|
250 |
+
}
|
251 |
+
|
252 |
+
@media screen and (max-width: 800px) {
|
253 |
+
nav {
|
254 |
+
display: flex;
|
255 |
+
}
|
256 |
+
#close {
|
257 |
+
display: block;
|
258 |
+
}
|
259 |
+
#sidenav {
|
260 |
+
position: fixed;
|
261 |
+
left: -400px;
|
262 |
+
}
|
263 |
+
#content-body {
|
264 |
+
margin: 50px 0 0 0;
|
265 |
+
}
|
266 |
+
}
|
267 |
+
|
templates/dashboard.html
ADDED
The diff for this file is too large to render.
See raw diff
|
|
templates/demo.html
ADDED
The diff for this file is too large to render.
See raw diff
|
|
templates/index.html
ADDED
@@ -0,0 +1,356 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
<!DOCTYPE html>
|
3 |
+
<html>
|
4 |
+
|
5 |
+
<head>
|
6 |
+
<!-- Basic -->
|
7 |
+
<meta charset="utf-8" />
|
8 |
+
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
9 |
+
<!-- Mobile Metas -->
|
10 |
+
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
|
11 |
+
<!-- Site Metas -->
|
12 |
+
<meta name="keywords" content="" />
|
13 |
+
<meta name="description" content="" />
|
14 |
+
<meta name="author" content="" />
|
15 |
+
<base href="https://themewagon.github.io/orthoc/">
|
16 |
+
<link rel="shortcut icon" href="images/favicon.png" type="">
|
17 |
+
|
18 |
+
<title> Orthoc </title>
|
19 |
+
|
20 |
+
<!-- bootstrap core css -->
|
21 |
+
<link rel="stylesheet" type="text/css" href="css/bootstrap.css" />
|
22 |
+
|
23 |
+
<!-- fonts style -->
|
24 |
+
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700;900&display=swap" rel="stylesheet">
|
25 |
+
|
26 |
+
<!--owl slider stylesheet -->
|
27 |
+
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/OwlCarousel2/2.3.4/assets/owl.carousel.min.css" />
|
28 |
+
|
29 |
+
<!-- font awesome style -->
|
30 |
+
<link href="css/font-awesome.min.css" rel="stylesheet" />
|
31 |
+
|
32 |
+
<!-- Custom styles for this template -->
|
33 |
+
<link href="css/style.css" rel="stylesheet" />
|
34 |
+
<!-- responsive style -->
|
35 |
+
<link href="css/responsive.css" rel="stylesheet" />
|
36 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css" integrity="sha512-Evv84Mr4kqVGRNSgIGL/F/aIDqQb7xQ2vcrdIwxfjThSH8CSR7PBEakCr51Ck+w+/U6swU2Im1vVX0SVk9ABhg==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
37 |
+
|
38 |
+
</head>
|
39 |
+
|
40 |
+
<body>
|
41 |
+
|
42 |
+
<div class="hero_area">
|
43 |
+
|
44 |
+
<div class="hero_bg_box">
|
45 |
+
<img src="images/hero-bg.png" alt="">
|
46 |
+
</div>
|
47 |
+
|
48 |
+
<!-- header section strats -->
|
49 |
+
<header class="header_section">
|
50 |
+
<div class="container">
|
51 |
+
<nav class="navbar navbar-expand-lg custom_nav-container ">
|
52 |
+
<a class="navbar-brand" href="index.html">
|
53 |
+
<span>
|
54 |
+
Orthoc
|
55 |
+
</span>
|
56 |
+
</a>
|
57 |
+
|
58 |
+
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
59 |
+
<span class=""> </span>
|
60 |
+
</button>
|
61 |
+
|
62 |
+
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
63 |
+
<ul class="navbar-nav">
|
64 |
+
<li class="nav-item active">
|
65 |
+
<a class="nav-link" href="http://127.0.0.1:5000/">Trang chủ <span class="sr-only">(current)</span></a>
|
66 |
+
</li>
|
67 |
+
<li class="nav-item">
|
68 |
+
<a class="nav-link" href="http://127.0.0.1:5000/register"> Đăng ký </a>
|
69 |
+
</li>
|
70 |
+
<li class="nav-item">
|
71 |
+
<a class="nav-link" href="http://127.0.0.1:5000/login">Đăng nhập</a>
|
72 |
+
</li>
|
73 |
+
<form class="form-inline">
|
74 |
+
<button class="btn my-2 my-sm-0 nav_search-btn" type="submit">
|
75 |
+
<i class="fa fa-search" aria-hidden="true"></i>
|
76 |
+
</button>
|
77 |
+
</form>
|
78 |
+
</ul>
|
79 |
+
</div>
|
80 |
+
</nav>
|
81 |
+
</div>
|
82 |
+
</header>
|
83 |
+
<!-- end header section -->
|
84 |
+
<!-- slider section -->
|
85 |
+
<section class="slider_section ">
|
86 |
+
<div id="customCarousel1" class="carousel slide" data-ride="carousel">
|
87 |
+
<div class="carousel-inner">
|
88 |
+
<div class="carousel-item active">
|
89 |
+
<div class="container ">
|
90 |
+
<div class="row">
|
91 |
+
<div class="col-md-7">
|
92 |
+
<div class="detail-box">
|
93 |
+
<h1>
|
94 |
+
Hệ Thống Gợi Ý Thuốc Dựa Trên Triệu Chứng
|
95 |
+
</h1>
|
96 |
+
<p>
|
97 |
+
Hệ thống được phát triển hướng tới việc cải thiện khả năng cá nhân hóa trong kê đơn thuốc, vốn là một thách thức lớn đối với các phương pháp truyền thống dựa trên triệu chứng bề mặt. Thay vì chỉ dừng lại ở việc phân tích các triệu chứng đơn lẻ, hệ thống sẽ tích hợp thông tin từ các nguồn dữ liệu đa chiều để đưa ra những khuyến nghị thuốc tối ưu, phù hợp với từng trường hợp cụ thể.
|
98 |
+
</p>
|
99 |
+
<div class="btn-box">
|
100 |
+
<a href="http://127.0.0.1:5000/login" class="btn1">
|
101 |
+
Đăng nhập
|
102 |
+
</a>
|
103 |
+
</div>
|
104 |
+
</div>
|
105 |
+
</div>
|
106 |
+
</div>
|
107 |
+
</div>
|
108 |
+
</div>
|
109 |
+
<!-- <div class="carousel-item ">
|
110 |
+
<div class="container ">
|
111 |
+
<div class="row">
|
112 |
+
<div class="col-md-7">
|
113 |
+
<div class="detail-box">
|
114 |
+
<h1>
|
115 |
+
We Provide Best Healthcare
|
116 |
+
</h1>
|
117 |
+
<p>
|
118 |
+
Explicabo esse amet tempora quibusdam laudantium, laborum eaque magnam fugiat hic? Esse dicta aliquid error repudiandae earum suscipit fugiat molestias, veniam, vel architecto veritatis delectus repellat modi impedit sequi.
|
119 |
+
</p>
|
120 |
+
<div class="btn-box">
|
121 |
+
<a href="" class="btn1">
|
122 |
+
Read More
|
123 |
+
</a>
|
124 |
+
</div>
|
125 |
+
</div>
|
126 |
+
</div>
|
127 |
+
</div>
|
128 |
+
</div>
|
129 |
+
</div>
|
130 |
+
<div class="carousel-item">
|
131 |
+
<div class="container ">
|
132 |
+
<div class="row">
|
133 |
+
<div class="col-md-7">
|
134 |
+
<div class="detail-box">
|
135 |
+
<h1>
|
136 |
+
We Provide Best Healthcare
|
137 |
+
</h1>
|
138 |
+
<p>
|
139 |
+
Explicabo esse amet tempora quibusdam laudantium, laborum eaque magnam fugiat hic? Esse dicta aliquid error repudiandae earum suscipit fugiat molestias, veniam, vel architecto veritatis delectus repellat modi impedit sequi.
|
140 |
+
</p>
|
141 |
+
<div class="btn-box">
|
142 |
+
<a href="" class="btn1">
|
143 |
+
Read More
|
144 |
+
</a>
|
145 |
+
</div>
|
146 |
+
</div>
|
147 |
+
</div>
|
148 |
+
</div>
|
149 |
+
</div>
|
150 |
+
</div> -->
|
151 |
+
</div>
|
152 |
+
<!-- <ol class="carousel-indicators">
|
153 |
+
<li data-target="#customCarousel1" data-slide-to="0" class="active"></li>
|
154 |
+
<li data-target="#customCarousel1" data-slide-to="1"></li>
|
155 |
+
<li data-target="#customCarousel1" data-slide-to="2"></li>
|
156 |
+
</ol> -->
|
157 |
+
</div>
|
158 |
+
|
159 |
+
</section>
|
160 |
+
<!-- end slider section -->
|
161 |
+
</div>
|
162 |
+
|
163 |
+
|
164 |
+
<!-- department section -->
|
165 |
+
|
166 |
+
<section class="department_section layout_padding">
|
167 |
+
<div class="department_container">
|
168 |
+
<div class="container ">
|
169 |
+
<div class="heading_container heading_center">
|
170 |
+
<h2>
|
171 |
+
CHUYÊN ĐỀ NGHIÊN CỨU 2
|
172 |
+
</h2>
|
173 |
+
<p>
|
174 |
+
Mô hình gợi ý thuốc dựa trên triệu chứng
|
175 |
+
</p>
|
176 |
+
</div>
|
177 |
+
<div class="row">
|
178 |
+
<div class="col-md-3">
|
179 |
+
<div class="box ">
|
180 |
+
<div class="img-box">
|
181 |
+
<img src="images/s1.png" alt="">
|
182 |
+
</div>
|
183 |
+
<div class="detail-box">
|
184 |
+
<h5>
|
185 |
+
Mô tả đề tài
|
186 |
+
</h5>
|
187 |
+
<p>
|
188 |
+
Mô hình gợi ý thuốc dựa trên triệu chứng giúp hỗ trợ chẩn đoán và tối ưu hóa điều trị bằng trí tuệ nhân tạo.
|
189 |
+
</p>
|
190 |
+
</div>
|
191 |
+
</div>
|
192 |
+
</div>
|
193 |
+
<div class="col-md-3">
|
194 |
+
<div class="box ">
|
195 |
+
<div class="img-box">
|
196 |
+
<img src="images/s2.png" alt="">
|
197 |
+
</div>
|
198 |
+
<div class="detail-box">
|
199 |
+
<h5>
|
200 |
+
Mục tiêu thực hiện
|
201 |
+
</h5>
|
202 |
+
<p>
|
203 |
+
Tạo ra mô hình gợi ý thuốc dựa trên triệu chứng bằng trí tuệ nhân tạo, nhằm hỗ trợ bác sĩ và bệnh nhân.
|
204 |
+
</p>
|
205 |
+
</div>
|
206 |
+
</div>
|
207 |
+
</div>
|
208 |
+
<div class="col-md-3">
|
209 |
+
<div class="box ">
|
210 |
+
<div class="img-box">
|
211 |
+
<img src="images/s3.png" alt="">
|
212 |
+
</div>
|
213 |
+
<div class="detail-box">
|
214 |
+
<h5>
|
215 |
+
Đối tượng nghiên cứu
|
216 |
+
</h5>
|
217 |
+
<p>
|
218 |
+
Mô hình gợi ý thuốc dựa trên triệu chứng, sử dụng trí tuệ nhân tạo trong hỗ trợ chẩn đoán và điều trị.
|
219 |
+
</p>
|
220 |
+
</div>
|
221 |
+
</div>
|
222 |
+
</div>
|
223 |
+
<div class="col-md-3">
|
224 |
+
<div class="box ">
|
225 |
+
<div class="img-box">
|
226 |
+
<img src="images/s4.png" alt="">
|
227 |
+
</div>
|
228 |
+
<div class="detail-box">
|
229 |
+
<h5>
|
230 |
+
Phạm vi nghiên cứu
|
231 |
+
</h5>
|
232 |
+
<p>
|
233 |
+
Tập trung vào việc thu thập và xử lý dữ liệu triệu chứng - thuốc từ nguồn y khoa đáng tin.
|
234 |
+
</p>
|
235 |
+
</div>
|
236 |
+
</div>
|
237 |
+
</div>
|
238 |
+
</div>
|
239 |
+
<!-- <div class="btn-box">
|
240 |
+
<a href="">
|
241 |
+
View All
|
242 |
+
</a>
|
243 |
+
</div> -->
|
244 |
+
</div>
|
245 |
+
</div>
|
246 |
+
</section>
|
247 |
+
|
248 |
+
<!-- end department section -->
|
249 |
+
|
250 |
+
<!-- footer section -->
|
251 |
+
<footer class="footer_section">
|
252 |
+
<div class="container">
|
253 |
+
<div class="row">
|
254 |
+
<div class="col-md-6 col-lg-4 footer_col">
|
255 |
+
<div class="footer_contact">
|
256 |
+
<h4>
|
257 |
+
Thông tin chuyên đề
|
258 |
+
</h4>
|
259 |
+
<div class="contact_link_box">
|
260 |
+
<a href="">
|
261 |
+
<i class="fa fa-user" aria-hidden="true"></i>
|
262 |
+
<span>
|
263 |
+
TS Trần Thanh Phước
|
264 |
+
</span>
|
265 |
+
</a>
|
266 |
+
<a href="">
|
267 |
+
<i class="fa fa-users" aria-hidden="true"></i>
|
268 |
+
<span>
|
269 |
+
Nguyễn Trần Quỳnh Như - 241805005
|
270 |
+
</span>
|
271 |
+
</a>
|
272 |
+
<a href="">
|
273 |
+
<i class="fa fa-users" aria-hidden="true"></i>
|
274 |
+
<span>
|
275 |
+
Trần Thành Trung - 241805005
|
276 |
+
</span>
|
277 |
+
</a>
|
278 |
+
<a href="">
|
279 |
+
<i class="fa-solid fa-school" aria-hidden="true"></i>
|
280 |
+
<span>
|
281 |
+
Lớp: 24180501
|
282 |
+
</span>
|
283 |
+
</a>
|
284 |
+
<a href="">
|
285 |
+
<i class="fa-solid fa-calendar-days" aria-hidden="true"></i>
|
286 |
+
<span>
|
287 |
+
Khóa: 2024-2026
|
288 |
+
</span>
|
289 |
+
</a>
|
290 |
+
</div>
|
291 |
+
</div>
|
292 |
+
|
293 |
+
</div>
|
294 |
+
<div class="col-md-6 col-lg-4 footer_col">
|
295 |
+
<div class="footer_detail">
|
296 |
+
<h4>
|
297 |
+
Giới thiệu
|
298 |
+
</h4>
|
299 |
+
<p>
|
300 |
+
Hệ thống hướng tới cải thiện khả năng cá nhân hóa trong kê đơn thuốc bằng cách tích hợp dữ liệu đa chiều, thay vì chỉ dựa trên triệu chứng bề mặt, nhằm đưa ra khuyến nghị tối ưu cho từng trường hợp.
|
301 |
+
</p>
|
302 |
+
</div>
|
303 |
+
</div>
|
304 |
+
<div class="col-md-6 col-lg-3 mx-auto footer_col">
|
305 |
+
<div class="footer_link_box">
|
306 |
+
<h4>
|
307 |
+
Links
|
308 |
+
</h4>
|
309 |
+
<div class="footer_links">
|
310 |
+
<a class="active" href="http://127.0.0.1:5000/">
|
311 |
+
Trang chủ
|
312 |
+
</a>
|
313 |
+
<a class="" href="http://127.0.0.1:5000/register">
|
314 |
+
Đăng ký
|
315 |
+
</a>
|
316 |
+
<a class="" href="http://127.0.0.1:5000/login">
|
317 |
+
Đăng nhập
|
318 |
+
</a>
|
319 |
+
</div>
|
320 |
+
</div>
|
321 |
+
</div>
|
322 |
+
|
323 |
+
</div>
|
324 |
+
<div class="footer-info">
|
325 |
+
<p>
|
326 |
+
© <span id="displayYear"></span> All Rights Reserved By
|
327 |
+
<a href="">QuynhNhu<br><br></a>
|
328 |
+
© <span id="displayYear"></span> Distributed By
|
329 |
+
<a href="">QuynhNhu</a>
|
330 |
+
</p>
|
331 |
+
|
332 |
+
</div>
|
333 |
+
</div>
|
334 |
+
</footer>
|
335 |
+
<!-- footer section -->
|
336 |
+
|
337 |
+
<!-- jQery -->
|
338 |
+
<script type="text/javascript" src="js/jquery-3.4.1.min.js"></script>
|
339 |
+
<!-- popper js -->
|
340 |
+
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous">
|
341 |
+
</script>
|
342 |
+
<!-- bootstrap js -->
|
343 |
+
<script type="text/javascript" src="js/bootstrap.js"></script>
|
344 |
+
<!-- owl slider -->
|
345 |
+
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/OwlCarousel2/2.3.4/owl.carousel.min.js">
|
346 |
+
</script>
|
347 |
+
<!-- custom js -->
|
348 |
+
<script type="text/javascript" src="js/custom.js"></script>
|
349 |
+
<!-- Google Map -->
|
350 |
+
<script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyCh39n5U-4IoWpsVGUHWdqB6puEkhRLdmI&callback=myMap">
|
351 |
+
</script>
|
352 |
+
<!-- End Google Map -->
|
353 |
+
|
354 |
+
</body>
|
355 |
+
|
356 |
+
</html>
|
templates/login.html
ADDED
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
<!doctype html>
|
3 |
+
<html lang="en">
|
4 |
+
|
5 |
+
<head>
|
6 |
+
|
7 |
+
<meta charset="utf-8" />
|
8 |
+
<title>Log in | Chaton - Responsive Bootstrap 5 Chat App</title>
|
9 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
10 |
+
<meta content="Chaton - Responsive Chat App Template in HTML. A fully featured HTML chat messenger template in Bootstrap 5" name="description" />
|
11 |
+
<meta name="keywords" content="Chaton chat template, chat, web chat template, chat status, chat template, communication, discussion, group chat, message, messenger template, status" />
|
12 |
+
<meta content="Themesdesign" name="author" />
|
13 |
+
<!-- App favicon -->
|
14 |
+
<link rel="shortcut icon" href="https://themesdesign.in/chaton/layouts/assets/images/favicon.ico" id="tabIcon">
|
15 |
+
|
16 |
+
<!-- Bootstrap Css -->
|
17 |
+
<link href="https://themesdesign.in/chaton/layouts/assets/css/bootstrap.min.css" id="bootstrap-style" rel="stylesheet" type="text/css" />
|
18 |
+
<!-- Icons Css -->
|
19 |
+
<link href="https://themesdesign.in/chaton/layouts/assets/css/icons.min.css" rel="stylesheet" type="text/css" />
|
20 |
+
<!-- App Css-->
|
21 |
+
<link href="https://themesdesign.in/chaton/layouts/assets/css/app.min.css" id="app-style" rel="stylesheet" type="text/css" />
|
22 |
+
|
23 |
+
</head>
|
24 |
+
|
25 |
+
<body>
|
26 |
+
<div class="auth-bg">
|
27 |
+
<div class="container p-0">
|
28 |
+
<div class="row justify-content-center g-0">
|
29 |
+
<div class="col-xl-9 col-lg-8">
|
30 |
+
<div class="authentication-page-content shadow-lg">
|
31 |
+
<div class="d-flex flex-column h-100 px-4 pt-4">
|
32 |
+
<div class="row justify-content-center">
|
33 |
+
<div class="col-sm-8 col-lg-6 col-xl-6">
|
34 |
+
|
35 |
+
<div class="py-md-5 py-4">
|
36 |
+
|
37 |
+
<div class="text-center mb-5">
|
38 |
+
<h3>Xin chào!</h3>
|
39 |
+
<p class="text-muted">Chào mừng đến với trang đăng nhập</p>
|
40 |
+
</div>
|
41 |
+
|
42 |
+
<form action="http://127.0.0.1:5000/login" method="POST">
|
43 |
+
<div class="mb-3">
|
44 |
+
<label for="username" class="form-label">Tên đăng nhập</label>
|
45 |
+
<input type="text" name="username" class="form-control" id="username" placeholder="Enter username">
|
46 |
+
</div>
|
47 |
+
|
48 |
+
<div class="mb-3">
|
49 |
+
<label for="userpassword" class="form-label">Mật khẩu</label>
|
50 |
+
<div class="position-relative auth-pass-inputgroup mb-3">
|
51 |
+
<input type="password" name="password" class="form-control pe-5" placeholder="Enter Password" id="password-input">
|
52 |
+
<button class="btn btn-link position-absolute end-0 top-0 text-decoration-none text-muted" type="button" id="password-addon"><i class="ri-eye-fill align-middle"></i></button>
|
53 |
+
</div>
|
54 |
+
</div>
|
55 |
+
|
56 |
+
|
57 |
+
<div class="text-center mt-4">
|
58 |
+
<button class="btn btn-primary w-100" type="submit">Đăng nhập</button>
|
59 |
+
</div>
|
60 |
+
|
61 |
+
</form><!-- end form -->
|
62 |
+
</div>
|
63 |
+
</div><!-- end col -->
|
64 |
+
</div><!-- end row -->
|
65 |
+
|
66 |
+
<div class="row">
|
67 |
+
<div class="col-xl-12">
|
68 |
+
<div class="text-center text-muted p-4">
|
69 |
+
<p class="mb-0">©
|
70 |
+
<script>document.write(new Date().getFullYear())</script> Chaton. Crafted with <i class="mdi mdi-heart text-danger"></i> by Themesdesign
|
71 |
+
</p>
|
72 |
+
</div>
|
73 |
+
</div><!-- end col -->
|
74 |
+
</div><!-- end row -->
|
75 |
+
|
76 |
+
</div>
|
77 |
+
</div>
|
78 |
+
</div>
|
79 |
+
<!-- end col -->
|
80 |
+
</div>
|
81 |
+
<!-- end row -->
|
82 |
+
</div>
|
83 |
+
<!-- end container-fluid -->
|
84 |
+
</div>
|
85 |
+
<!-- end auth bg -->
|
86 |
+
|
87 |
+
<!-- JAVASCRIPT -->
|
88 |
+
<script src="https://themesdesign.in/chaton/layouts/assets/libs/bootstrap/js/bootstrap.bundle.min.js"></script>
|
89 |
+
<script src="https://themesdesign.in/chaton/layouts/assets/libs/simplebar/simplebar.min.js"></script>
|
90 |
+
<script src="https://themesdesign.in/chaton/layouts/assets/libs/node-waves/waves.min.js"></script>
|
91 |
+
<!-- password addon -->
|
92 |
+
<script src="https://themesdesign.in/chaton/layouts/assets/js/pages/password-addon.init.js"></script>
|
93 |
+
|
94 |
+
<!-- theme-style init -->
|
95 |
+
<script src="https://themesdesign.in/chaton/layouts/assets/js/pages/theme-style.init.js"></script>
|
96 |
+
|
97 |
+
<script src="https://themesdesign.in/chaton/layouts/assets/js/app.js"></script>
|
98 |
+
|
99 |
+
</body>
|
100 |
+
|
101 |
+
</html>
|
templates/register.html
ADDED
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
<!doctype html>
|
3 |
+
<html lang="en">
|
4 |
+
|
5 |
+
<head>
|
6 |
+
|
7 |
+
<meta charset="utf-8" />
|
8 |
+
<title>Log in | Chaton - Responsive Bootstrap 5 Chat App</title>
|
9 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
10 |
+
<meta content="Chaton - Responsive Chat App Template in HTML. A fully featured HTML chat messenger template in Bootstrap 5" name="description" />
|
11 |
+
<meta name="keywords" content="Chaton chat template, chat, web chat template, chat status, chat template, communication, discussion, group chat, message, messenger template, status" />
|
12 |
+
<meta content="Themesdesign" name="author" />
|
13 |
+
<!-- App favicon -->
|
14 |
+
<link rel="shortcut icon" href="https://themesdesign.in/chaton/layouts/assets/images/favicon.ico" id="tabIcon">
|
15 |
+
|
16 |
+
<!-- Bootstrap Css -->
|
17 |
+
<link href="https://themesdesign.in/chaton/layouts/assets/css/bootstrap.min.css" id="bootstrap-style" rel="stylesheet" type="text/css" />
|
18 |
+
<!-- Icons Css -->
|
19 |
+
<link href="https://themesdesign.in/chaton/layouts/assets/css/icons.min.css" rel="stylesheet" type="text/css" />
|
20 |
+
<!-- App Css-->
|
21 |
+
<link href="https://themesdesign.in/chaton/layouts/assets/css/app.min.css" id="app-style" rel="stylesheet" type="text/css" />
|
22 |
+
|
23 |
+
</head>
|
24 |
+
|
25 |
+
<body>
|
26 |
+
<div class="auth-bg">
|
27 |
+
<div class="container p-0">
|
28 |
+
<div class="row justify-content-center g-0">
|
29 |
+
<div class="col-xl-9 col-lg-8">
|
30 |
+
<div class="authentication-page-content shadow-lg">
|
31 |
+
<div class="d-flex flex-column h-100 px-4 pt-4">
|
32 |
+
<div class="row justify-content-center">
|
33 |
+
<div class="col-sm-8 col-lg-6 col-xl-6">
|
34 |
+
|
35 |
+
<div class="py-md-5 py-4">
|
36 |
+
|
37 |
+
<div class="text-center mb-5">
|
38 |
+
<h3>Xin chào!</h3>
|
39 |
+
<p class="text-muted">Hãy đăng ký ngay</p>
|
40 |
+
</div>
|
41 |
+
|
42 |
+
<form action="http://127.0.0.1:5000/register" method="POST">
|
43 |
+
|
44 |
+
<div class="mb-3">
|
45 |
+
<label for="fullname" class="form-label">Họ và tên</label>
|
46 |
+
<input type="text" name="fullname" class="form-control" id="username" placeholder="Enter fullname">
|
47 |
+
</div>
|
48 |
+
|
49 |
+
<div class="mb-3">
|
50 |
+
<label for="username" class="form-label">Tên đăng nhập</label>
|
51 |
+
<input type="text" name="username" class="form-control" id="username" placeholder="Enter username">
|
52 |
+
</div>
|
53 |
+
|
54 |
+
<div class="mb-3">
|
55 |
+
<label for="userpassword" class="form-label">Mật khẩu</label>
|
56 |
+
<div class="position-relative auth-pass-inputgroup mb-3">
|
57 |
+
<input type="password" name="password" class="form-control pe-5" placeholder="Enter Password" id="password-input">
|
58 |
+
<button class="btn btn-link position-absolute end-0 top-0 text-decoration-none text-muted" type="button" id="password-addon"><i class="ri-eye-fill align-middle"></i></button>
|
59 |
+
</div>
|
60 |
+
</div>
|
61 |
+
|
62 |
+
<div class="text-center mt-4">
|
63 |
+
<button class="btn btn-primary w-100" type="submit">Đăng ký</button>
|
64 |
+
</div>
|
65 |
+
|
66 |
+
</form><!-- end form -->
|
67 |
+
</div>
|
68 |
+
</div><!-- end col -->
|
69 |
+
</div><!-- end row -->
|
70 |
+
|
71 |
+
<div class="row">
|
72 |
+
<div class="col-xl-12">
|
73 |
+
<div class="text-center text-muted p-4">
|
74 |
+
<p class="mb-0">©
|
75 |
+
<script>document.write(new Date().getFullYear())</script> Chaton. Crafted with <i class="mdi mdi-heart text-danger"></i> by Themesdesign
|
76 |
+
</p>
|
77 |
+
</div>
|
78 |
+
</div><!-- end col -->
|
79 |
+
</div><!-- end row -->
|
80 |
+
|
81 |
+
</div>
|
82 |
+
</div>
|
83 |
+
</div>
|
84 |
+
<!-- end col -->
|
85 |
+
</div>
|
86 |
+
<!-- end row -->
|
87 |
+
</div>
|
88 |
+
<!-- end container-fluid -->
|
89 |
+
</div>
|
90 |
+
<!-- end auth bg -->
|
91 |
+
|
92 |
+
<!-- JAVASCRIPT -->
|
93 |
+
<script src="https://themesdesign.in/chaton/layouts/assets/libs/bootstrap/js/bootstrap.bundle.min.js"></script>
|
94 |
+
<script src="https://themesdesign.in/chaton/layouts/assets/libs/simplebar/simplebar.min.js"></script>
|
95 |
+
<script src="https://themesdesign.in/chaton/layouts/assets/libs/node-waves/waves.min.js"></script>
|
96 |
+
<!-- password addon -->
|
97 |
+
<script src="https://themesdesign.in/chaton/layouts/assets/js/pages/password-addon.init.js"></script>
|
98 |
+
|
99 |
+
<!-- theme-style init -->
|
100 |
+
<script src="https://themesdesign.in/chaton/layouts/assets/js/pages/theme-style.init.js"></script>
|
101 |
+
|
102 |
+
<script src="https://themesdesign.in/chaton/layouts/assets/js/app.js"></script>
|
103 |
+
|
104 |
+
</body>
|
105 |
+
|
106 |
+
</html>
|