Huyen My commited on
Commit ·
1539090
0
Parent(s):
first
Browse files- .gitattributes +3 -0
- ChatBot/__init__.py +1 -0
- ChatBot/__pycache__/app.cpython-311.pyc +0 -0
- ChatBot/__pycache__/chatbot.cpython-311.pyc +0 -0
- ChatBot/__pycache__/query_transformation.cpython-311.pyc +0 -0
- ChatBot/__pycache__/retrieval.cpython-311.pyc +0 -0
- ChatBot/__pycache__/route.cpython-311.pyc +0 -0
- ChatBot/__pycache__/server.cpython-311.pyc +0 -0
- ChatBot/app.py +43 -0
- ChatBot/chatbot.py +88 -0
- ChatBot/data/chunking_save.py +164 -0
- ChatBot/data/data.docx +0 -0
- ChatBot/data/data.json +0 -0
- ChatBot/data/data_preprocessing.py +152 -0
- ChatBot/data/new_data.json +0 -0
- ChatBot/data/preprocessing.py +139 -0
- ChatBot/indexing.py +58 -0
- ChatBot/query_transformation.py +39 -0
- ChatBot/requirements.txt +12 -0
- ChatBot/retrieval.py +303 -0
- ChatBot/route.py +103 -0
- ChatBot/try.ipynb +0 -0
- FrontEnd/.gitignore +24 -0
- FrontEnd/README.md +8 -0
- FrontEnd/eslint.config.js +38 -0
- FrontEnd/index.html +13 -0
- FrontEnd/package-lock.json +0 -0
- FrontEnd/package.json +28 -0
- FrontEnd/public/vite.svg +3 -0
- FrontEnd/src/ChatBot.css +112 -0
- FrontEnd/src/ChatBot.jsx +97 -0
- FrontEnd/src/assets/react.svg +3 -0
- FrontEnd/src/index.css +15 -0
- FrontEnd/src/main.jsx +10 -0
- FrontEnd/vite.config.js +7 -0
- package-lock.json +6 -0
.gitattributes
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.png filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.jpg filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.svg filter=lfs diff=lfs merge=lfs -text
|
ChatBot/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
from chatbot import ChatBot
|
ChatBot/__pycache__/app.cpython-311.pyc
ADDED
|
Binary file (1.82 kB). View file
|
|
|
ChatBot/__pycache__/chatbot.cpython-311.pyc
ADDED
|
Binary file (5.59 kB). View file
|
|
|
ChatBot/__pycache__/query_transformation.cpython-311.pyc
ADDED
|
Binary file (4.68 kB). View file
|
|
|
ChatBot/__pycache__/retrieval.cpython-311.pyc
ADDED
|
Binary file (17.6 kB). View file
|
|
|
ChatBot/__pycache__/route.cpython-311.pyc
ADDED
|
Binary file (4.53 kB). View file
|
|
|
ChatBot/__pycache__/server.cpython-311.pyc
ADDED
|
Binary file (1.16 kB). View file
|
|
|
ChatBot/app.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI
|
| 2 |
+
from pydantic import BaseModel
|
| 3 |
+
from chatbot import ChatBot
|
| 4 |
+
import warnings
|
| 5 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 6 |
+
# Tắt các cảnh báo
|
| 7 |
+
warnings.filterwarnings("ignore")
|
| 8 |
+
|
| 9 |
+
# Khởi tạo ứng dụng FastAPI
|
| 10 |
+
app = FastAPI()
|
| 11 |
+
app.add_middleware(
|
| 12 |
+
CORSMiddleware,
|
| 13 |
+
allow_origins=["http://localhost:5173"], # Cho phép kết nối từ frontend
|
| 14 |
+
allow_credentials=True,
|
| 15 |
+
allow_methods=["*"],
|
| 16 |
+
allow_headers=["*"],
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
# Khởi tạo chatbot
|
| 21 |
+
chatbot = ChatBot()
|
| 22 |
+
|
| 23 |
+
# Định nghĩa schema cho dữ liệu đầu vào
|
| 24 |
+
class QueryRequest(BaseModel):
|
| 25 |
+
query: str
|
| 26 |
+
|
| 27 |
+
# Endpoint để khởi tạo lại chatbot
|
| 28 |
+
@app.post("/newchat/")
|
| 29 |
+
async def new_chat():
|
| 30 |
+
global chatbot
|
| 31 |
+
chatbot = ChatBot() # Khởi tạo lại chatbot mới
|
| 32 |
+
return {"message": "ChatBot đã được khởi tạo lại."}
|
| 33 |
+
|
| 34 |
+
# Endpoint để xử lý câu hỏi
|
| 35 |
+
@app.post("/process_query/")
|
| 36 |
+
async def process_query(request: QueryRequest):
|
| 37 |
+
global chatbot
|
| 38 |
+
|
| 39 |
+
raw_query = request.query # Lấy query từ người dùng
|
| 40 |
+
# Xử lý truy vấn bằng chatbot
|
| 41 |
+
response = chatbot.process_query(raw_query)
|
| 42 |
+
# Trả về câu trả lời từ chatbot
|
| 43 |
+
return {"response": response}
|
ChatBot/chatbot.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from langchain.chains import LLMChain
|
| 2 |
+
from langchain.prompts import PromptTemplate
|
| 3 |
+
from langchain.memory import ConversationBufferMemory
|
| 4 |
+
from langchain_google_genai import ChatGoogleGenerativeAI
|
| 5 |
+
from langchain_community.embeddings import HuggingFaceEmbeddings
|
| 6 |
+
from langchain_qdrant import FastEmbedSparse
|
| 7 |
+
from query_transformation import QueryTransform
|
| 8 |
+
from route import Router
|
| 9 |
+
from retrieval import Retriever
|
| 10 |
+
import os
|
| 11 |
+
from dotenv import load_dotenv
|
| 12 |
+
|
| 13 |
+
class ChatBot:
|
| 14 |
+
def __init__(self, embedding_model='bkai-foundation-models/vietnamese-bi-encoder', sparse_embedding="Qdrant/BM25"):
|
| 15 |
+
self.embedding_model = HuggingFaceEmbeddings(model_name=embedding_model)
|
| 16 |
+
self.sparse_embedding = FastEmbedSparse(model_name=sparse_embedding)
|
| 17 |
+
|
| 18 |
+
load_dotenv()
|
| 19 |
+
genai_api_key = os.getenv('GOOGLE_API_KEY')
|
| 20 |
+
|
| 21 |
+
print ("Khởi tạo Query Transform")
|
| 22 |
+
self.query_transform = QueryTransform()
|
| 23 |
+
|
| 24 |
+
print ("Khởi tạo Router")
|
| 25 |
+
self.router = Router()
|
| 26 |
+
|
| 27 |
+
print ("Khởi tạo Retriever")
|
| 28 |
+
self.retriever = Retriever(self.embedding_model, self.sparse_embedding)
|
| 29 |
+
|
| 30 |
+
print ("Khởi tạo LLM")
|
| 31 |
+
self.llm = ChatGoogleGenerativeAI(api_key=genai_api_key, model="gemini-1.5-flash")
|
| 32 |
+
|
| 33 |
+
# Sử dụng ConversationBufferMemory để lưu trữ lịch sử hội thoại
|
| 34 |
+
self.memory = ConversationBufferMemory(
|
| 35 |
+
memory_key="history",
|
| 36 |
+
input_key="query",
|
| 37 |
+
returnMessages=False, # Trả về chuỗi thay vì đối tượng
|
| 38 |
+
aiPrefix="AI:",
|
| 39 |
+
humanPrefix="User:",
|
| 40 |
+
k=2)
|
| 41 |
+
|
| 42 |
+
self.prompt_template = """
|
| 43 |
+
Bạn là một chatbot chuyên về Luật Hôn Nhân và Gia Đình Việt Nam với tên "ChatBot hỏi đáp về Luật Hôn Nhân và Gia Đình Việt Nam". Hãy trả lời từng câu hỏi đã được làm rõ (converted_query) dựa trên loại câu hỏi (question_type) và ngữ cảnh (context). Tuân thủ các quy tắc sau:
|
| 44 |
+
|
| 45 |
+
- Với `question_type = 1|4|6|8|9`:
|
| 46 |
+
1. Dựa vào ngữ cảnh và câu hỏi đã được làm rõ (converted_query) để trả lời câu hỏi gốc (query). KHÔNG ĐƯỢC sử dụng các cụm từ tương tự như "Dựa vào thông tin được cung cấp", "Dựa vào ngữ cảnh".
|
| 47 |
+
2. **Chỉ sử dụng thông tin trong ngữ cảnh.** Trả lời một cách **diễn giải chi tiết**, giải thích rõ lý do, cơ sở pháp lý (trong ngữ cảnh)
|
| 48 |
+
|
| 49 |
+
- Với `question_type = 2` (smalltalk): Trả lời thân thiện, phù hợp với ngữ cảnh.
|
| 50 |
+
|
| 51 |
+
- Với các loại khác: *Trả về ngữ cảnh y như nó được cung cấp, không thêm bớt bất kỳ thông tin nào khác.*
|
| 52 |
+
|
| 53 |
+
Mỗi câu hỏi đã được làm rõ (converted_query[i]) tương ứng với `context[i]` và `question_type[i]`. Trả lời lần lượt từng câu hỏi theo các quy tắc trên.
|
| 54 |
+
|
| 55 |
+
Dữ liệu cung cấp:
|
| 56 |
+
- Loại câu hỏi: {question_type}
|
| 57 |
+
- Ngữ cảnh: {context}
|
| 58 |
+
- Câu hỏi gốc: {query}
|
| 59 |
+
- Câu hỏi đã được làm rõ: {converted_query}
|
| 60 |
+
|
| 61 |
+
"""
|
| 62 |
+
|
| 63 |
+
# Prompt template cho LLMChain
|
| 64 |
+
self.prompt = PromptTemplate(
|
| 65 |
+
input_variables=["question_type", "context", "query", "converted_query"],
|
| 66 |
+
template=self.prompt_template,
|
| 67 |
+
)
|
| 68 |
+
|
| 69 |
+
# LLMChain kết hợp LLM và Prompt
|
| 70 |
+
self.chain = LLMChain(llm=self.llm, prompt=self.prompt, memory=self.memory)
|
| 71 |
+
|
| 72 |
+
def process_query(self, raw_query):
|
| 73 |
+
print ('QUERY GỐC: ', raw_query, '\n---------------------------------------------------------')
|
| 74 |
+
# Bước 1: Biến đổi câu truy vấn dựa trên lịch sử
|
| 75 |
+
converted_queries = self.query_transform.transform(raw_query, self.memory.load_memory_variables({})["history"])
|
| 76 |
+
print ('QUERY ĐÃ TRANSFORM: ', converted_queries, '\n---------------------------------------------------------')
|
| 77 |
+
query_types = []
|
| 78 |
+
# Bước 2: Phân loại loại câu hỏi
|
| 79 |
+
for q in converted_queries:
|
| 80 |
+
query_types.append(self.router.route_query(q))
|
| 81 |
+
print ('LOẠI CỦA QUERY: ', query_types, '\n---------------------------------------------------------')
|
| 82 |
+
# Bước 3: Truy vấn thông tin ngữ cảnh
|
| 83 |
+
context = self.retriever.retrieve(converted_queries, query_types)
|
| 84 |
+
print ('CONTEXT: ', context, '\n---------------------------------------------------------')
|
| 85 |
+
# Bước 4: Tạo câu trả lời bằng LangChain
|
| 86 |
+
response = self.chain.run(question_type=query_types, context=context, query=raw_query, converted_query=converted_queries)
|
| 87 |
+
|
| 88 |
+
return response
|
ChatBot/data/chunking_save.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
from sentence_transformers import SentenceTransformer
|
| 3 |
+
from qdrant_client import models
|
| 4 |
+
from langchain_qdrant import QdrantVectorStore, FastEmbedSparse, RetrievalMode
|
| 5 |
+
from langchain_huggingface import HuggingFaceEmbeddings
|
| 6 |
+
from langchain.schema import Document
|
| 7 |
+
from typing import List
|
| 8 |
+
import numpy as np
|
| 9 |
+
from sklearn.metrics.pairwise import cosine_similarity
|
| 10 |
+
import re
|
| 11 |
+
|
| 12 |
+
# Khởi tạo SentenceTransformer
|
| 13 |
+
model_name = 'bkai-foundation-models/vietnamese-bi-encoder'
|
| 14 |
+
model = SentenceTransformer(model_name)
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
# Load dữ liệu từ file data.json
|
| 18 |
+
with open('data.json', 'r', encoding='utf-8') as f:
|
| 19 |
+
data = json.load(f)
|
| 20 |
+
|
| 21 |
+
# Hàm chunking ngữ nghĩa và tạo Document
|
| 22 |
+
# class SemanticChunker:
|
| 23 |
+
# def __init__(self, model, breakpoint_percentile_threshold=80):
|
| 24 |
+
# self.model = model
|
| 25 |
+
# self.breakpoint_percentile_threshold = breakpoint_percentile_threshold
|
| 26 |
+
|
| 27 |
+
# def split_text(self, text: str) -> List[str]:
|
| 28 |
+
# modified_text = re.sub(r'(\d+)\.', r'\1)', text)
|
| 29 |
+
# # Tách câu
|
| 30 |
+
# sentences = re.split(r'(?<=[.!?])\s+', modified_text)
|
| 31 |
+
|
| 32 |
+
# # Tính embedding cho các câu
|
| 33 |
+
# embeddings = self.model.encode(sentences, convert_to_tensor=True).cpu().numpy()
|
| 34 |
+
|
| 35 |
+
# # Tính khoảng cách cosine giữa các câu liên tiếp
|
| 36 |
+
# distances = [
|
| 37 |
+
# 1 - cosine_similarity([embeddings[i]], [embeddings[i + 1]])[0][0]
|
| 38 |
+
# for i in range(len(embeddings) - 1)
|
| 39 |
+
# ]
|
| 40 |
+
|
| 41 |
+
# distances.append(0) # Thêm khoảng cách cho câu cuối (không có câu kế tiếp)
|
| 42 |
+
|
| 43 |
+
# # Xác định ngưỡng chia đoạn
|
| 44 |
+
# threshold = np.percentile(distances, self.breakpoint_percentile_threshold)
|
| 45 |
+
# breakpoints = [i for i, dist in enumerate(distances) if dist > threshold]
|
| 46 |
+
|
| 47 |
+
# # Tạo các đoạn văn bản
|
| 48 |
+
# chunks = []
|
| 49 |
+
# start = 0
|
| 50 |
+
# for bp in breakpoints:
|
| 51 |
+
# chunks.append(' '.join(sentences[start:bp + 1]))
|
| 52 |
+
# start = bp + 1
|
| 53 |
+
|
| 54 |
+
# if start < len(sentences):
|
| 55 |
+
# chunks.append(' '.join(sentences[start:]))
|
| 56 |
+
|
| 57 |
+
# return chunks
|
| 58 |
+
class SemanticChunker:
|
| 59 |
+
def __init__(self, model, buffer_size=1, breakpoint_percentile_threshold=80):
|
| 60 |
+
self.model = model
|
| 61 |
+
self.buffer_size = buffer_size
|
| 62 |
+
self.breakpoint_percentile_threshold = breakpoint_percentile_threshold
|
| 63 |
+
|
| 64 |
+
def split_text(self, text: str) -> List[str]:
|
| 65 |
+
modified_text = re.sub(r'(\d+)\.', r'\1)', text)
|
| 66 |
+
# Tách câu
|
| 67 |
+
# sentences = sent_tokenize(modified_text)
|
| 68 |
+
sentences = re.split(r'(?<=[.!?])\s+', modified_text)
|
| 69 |
+
|
| 70 |
+
combined_sentences = [
|
| 71 |
+
' '.join(sentences[max(i - self.buffer_size, 0): i + self.buffer_size + 1])
|
| 72 |
+
for i in range(len(sentences))
|
| 73 |
+
]
|
| 74 |
+
|
| 75 |
+
# Tính embedding cho các câu
|
| 76 |
+
embeddings = self.model.encode(combined_sentences, convert_to_tensor=True).cpu().numpy()
|
| 77 |
+
|
| 78 |
+
# Tính khoảng cách cosine
|
| 79 |
+
distances = [
|
| 80 |
+
1 - cosine_similarity([embeddings[i]], [embeddings[i + 1]])[0][0]
|
| 81 |
+
for i in range(len(embeddings) - 1)
|
| 82 |
+
]
|
| 83 |
+
|
| 84 |
+
distances.append(0) # Thêm khoảng cách cho câu cuối
|
| 85 |
+
|
| 86 |
+
# Xác định ngưỡng chia đoạn
|
| 87 |
+
threshold = np.percentile(distances, self.breakpoint_percentile_threshold)
|
| 88 |
+
breakpoints = [i for i, dist in enumerate(distances) if dist > threshold]
|
| 89 |
+
|
| 90 |
+
# Tạo các đoạn văn bản
|
| 91 |
+
chunks = []
|
| 92 |
+
start = 0
|
| 93 |
+
for bp in breakpoints:
|
| 94 |
+
chunks.append(' '.join(sentences[start:bp + 1]))
|
| 95 |
+
start = bp + 1
|
| 96 |
+
|
| 97 |
+
if start < len(sentences):
|
| 98 |
+
chunks.append(' '.join(sentences[start:]))
|
| 99 |
+
|
| 100 |
+
return chunks
|
| 101 |
+
|
| 102 |
+
# Khởi tạo SemanticChunker
|
| 103 |
+
chunker = SemanticChunker(model)
|
| 104 |
+
|
| 105 |
+
# Hàm xử lý dữ liệu và tạo Document
|
| 106 |
+
def chunk_and_embed(data):
|
| 107 |
+
documents = []
|
| 108 |
+
|
| 109 |
+
for chapter in data['chapters']:
|
| 110 |
+
print (f"Loading {chapter['chapter']} ...")
|
| 111 |
+
if "sections" in chapter:
|
| 112 |
+
for section in chapter['sections']:
|
| 113 |
+
for article in section['articles']:
|
| 114 |
+
chunks = chunker.split_text(article['content'])
|
| 115 |
+
for chunk in chunks:
|
| 116 |
+
documents.append(Document(
|
| 117 |
+
page_content=chunk,
|
| 118 |
+
metadata={
|
| 119 |
+
'chapter': chapter['chapter'] + " " + chapter['title'],
|
| 120 |
+
'section': section['section'] + " " + section['title'],
|
| 121 |
+
'article': article['article'] + " " + article['title']
|
| 122 |
+
}
|
| 123 |
+
))
|
| 124 |
+
|
| 125 |
+
else:
|
| 126 |
+
for article in chapter['articles']:
|
| 127 |
+
chunks = chunker.split_text(article['content'])
|
| 128 |
+
# modified_text = re.sub(r'(\d+)\.', r'\1)', article['content'])
|
| 129 |
+
# chunks = sent_tokenize(modified_text)
|
| 130 |
+
|
| 131 |
+
for chunk in chunks:
|
| 132 |
+
documents.append(Document(
|
| 133 |
+
page_content=chunk,
|
| 134 |
+
metadata={
|
| 135 |
+
'chapter': chapter['chapter'] + " " + chapter['title'],
|
| 136 |
+
'section': "",
|
| 137 |
+
'article': article['article'] + " " + article['title']
|
| 138 |
+
}
|
| 139 |
+
))
|
| 140 |
+
return documents
|
| 141 |
+
|
| 142 |
+
# Tạo các Document từ dữ liệu
|
| 143 |
+
documents = chunk_and_embed(data)
|
| 144 |
+
|
| 145 |
+
print (f'Number of chunks: {len(documents)}')
|
| 146 |
+
|
| 147 |
+
# Tạo embedding cho các Document bằng SentenceTransformer
|
| 148 |
+
embedding_model = HuggingFaceEmbeddings(model_name=model_name)
|
| 149 |
+
sparse_embeddings = FastEmbedSparse(model_name="Qdrant/BM25")
|
| 150 |
+
|
| 151 |
+
# Lưu dữ liệu vào Qdrant
|
| 152 |
+
collection_name = "LawData"
|
| 153 |
+
vectorstore = QdrantVectorStore.from_documents(
|
| 154 |
+
documents,
|
| 155 |
+
embedding_model,
|
| 156 |
+
sparse_embedding=sparse_embeddings,
|
| 157 |
+
retrieval_mode=RetrievalMode.HYBRID,
|
| 158 |
+
url='https://c1e67a53-62f6-4464-b5ed-086b3c298e23.europe-west3-0.gcp.cloud.qdrant.io',
|
| 159 |
+
api_key='-s29x9W2DpfpvmsR-1bA_CbpKrtp__xVJ2YKUmPgcf6n7MQ95o6fBQ',
|
| 160 |
+
collection_name=collection_name,
|
| 161 |
+
distance=models.Distance.COSINE
|
| 162 |
+
)
|
| 163 |
+
|
| 164 |
+
print("Dữ liệu đã được lưu vào Qdrant Cloud!")
|
ChatBot/data/data.docx
ADDED
|
Binary file (85.2 kB). View file
|
|
|
ChatBot/data/data.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
ChatBot/data/data_preprocessing.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import re
|
| 3 |
+
from docx import Document
|
| 4 |
+
|
| 5 |
+
# Hàm trích xuất tham chiếu từ một đoạn văn bản
|
| 6 |
+
def extract_references(text):
|
| 7 |
+
pattern = r"(khoản \d+(?:, khoản \d+)* Điều \d+|Điều \d+)"
|
| 8 |
+
matches = re.findall(pattern, text)
|
| 9 |
+
refer = []
|
| 10 |
+
for match in matches:
|
| 11 |
+
if match.startswith("khoản"):
|
| 12 |
+
parts = match.split(" Điều ")
|
| 13 |
+
khoan_part = parts[0]
|
| 14 |
+
dieu_part = "Điều " + parts[1]
|
| 15 |
+
khoan_list = khoan_part.replace("khoản", "").strip().split(", ")
|
| 16 |
+
for khoan in khoan_list:
|
| 17 |
+
refer.append(f"khoản {khoan.strip()} {dieu_part}")
|
| 18 |
+
else:
|
| 19 |
+
refer.append(match)
|
| 20 |
+
return refer
|
| 21 |
+
|
| 22 |
+
# Hàm phân tích dữ liệu và tạo cấu trúc JSON
|
| 23 |
+
def parse_data_to_json(data):
|
| 24 |
+
chapters = []
|
| 25 |
+
current_chapter = None
|
| 26 |
+
current_section = None
|
| 27 |
+
current_article = None
|
| 28 |
+
lines = data.split("\n")
|
| 29 |
+
i = 0
|
| 30 |
+
|
| 31 |
+
while i < len(lines):
|
| 32 |
+
line = lines[i].strip()
|
| 33 |
+
# Xử lí chương
|
| 34 |
+
if line.startswith("CHƯƠNG"):
|
| 35 |
+
if current_chapter:
|
| 36 |
+
if current_section: # Thêm mục hiện tại vào chương trước khi chuyển sang chương mới
|
| 37 |
+
if current_article:
|
| 38 |
+
current_section["articles"].append(current_article)
|
| 39 |
+
current_article = None
|
| 40 |
+
current_chapter["sections"].append(current_section)
|
| 41 |
+
current_section = None
|
| 42 |
+
chapters.append(current_chapter)
|
| 43 |
+
current_chapter = {
|
| 44 |
+
"chapter": f"{line} {lines[i+1].strip()}", # Kết hợp số chương và tiêu đề
|
| 45 |
+
"sections": []
|
| 46 |
+
}
|
| 47 |
+
i += 2
|
| 48 |
+
|
| 49 |
+
# Xử lí Mục
|
| 50 |
+
elif line.startswith("Mục"):
|
| 51 |
+
if current_section:
|
| 52 |
+
if current_article:
|
| 53 |
+
current_section["articles"].append(current_article)
|
| 54 |
+
current_article = None
|
| 55 |
+
current_chapter["sections"].append(current_section)
|
| 56 |
+
current_section = {
|
| 57 |
+
"section": f"{line} {lines[i+1].strip()}", # Kết hợp số mục và tiêu đề
|
| 58 |
+
"articles": []
|
| 59 |
+
}
|
| 60 |
+
i += 2
|
| 61 |
+
|
| 62 |
+
# Xử lí Điều
|
| 63 |
+
elif line.startswith("Điều"):
|
| 64 |
+
if current_section is None:
|
| 65 |
+
current_section = {
|
| 66 |
+
"section": "", # Kết hợp số mục và tiêu đề
|
| 67 |
+
"articles": []
|
| 68 |
+
}
|
| 69 |
+
elif current_article:
|
| 70 |
+
current_section["articles"].append(current_article)
|
| 71 |
+
|
| 72 |
+
current_article = {
|
| 73 |
+
"article": line.split('.')[0].strip(),
|
| 74 |
+
"title": line.split('.')[1].strip(),
|
| 75 |
+
"clauses": []
|
| 76 |
+
}
|
| 77 |
+
i += 1
|
| 78 |
+
# Kiểm tra xem điều này có câu ngắn trước các khoản không (sử dụng re.match)
|
| 79 |
+
if i < len(lines) and lines[i].strip() and re.match(r"^\D", lines[i].strip()):
|
| 80 |
+
# Lưu câu ngắn để ghép vào khoản đầu tiên
|
| 81 |
+
text = lines[i].strip()
|
| 82 |
+
i += 1
|
| 83 |
+
else:
|
| 84 |
+
text = ""
|
| 85 |
+
flag = False # Kiểm tra có khoản không
|
| 86 |
+
|
| 87 |
+
# Xử lý các khoản
|
| 88 |
+
while i < len(lines) and lines[i].strip() and lines[i].strip()[0].isdigit():
|
| 89 |
+
flag = True
|
| 90 |
+
# Bắt đầu một khoản mới
|
| 91 |
+
clause_number = f"Khoản {lines[i].strip().split('.')[0]}"
|
| 92 |
+
clause_content = lines[i].strip()
|
| 93 |
+
i += 1
|
| 94 |
+
# Xử lý các điểm trong khoản
|
| 95 |
+
while i < len(lines) and lines[i].strip() and not re.match(r"^(CHƯƠNG\s+[IVXLCDM]+|Mục\s+\d+|Điều\s+\d+)", lines[i].strip()) and not lines[i].strip()[0].isdigit():
|
| 96 |
+
clause_content += " " + lines[i].strip()
|
| 97 |
+
i += 1
|
| 98 |
+
# Ghép câu ngắn vào khoản đầu tiên
|
| 99 |
+
if text:
|
| 100 |
+
clause_content = f"{text} {clause_content}"
|
| 101 |
+
text = "" # Đã sử dụng câu ngắn, không cần ghép vào khoản tiếp theo
|
| 102 |
+
refer = extract_references(clause_content)
|
| 103 |
+
current_article["clauses"].append({
|
| 104 |
+
"clause": clause_number, # Số khoản
|
| 105 |
+
"content": clause_content,
|
| 106 |
+
"refer": refer
|
| 107 |
+
})
|
| 108 |
+
if flag == False:
|
| 109 |
+
while i < len(lines) and lines[i].strip() and not re.match(r"^(CHƯƠNG\s+[IVXLCDM]+|Mục\s+\d+|Điều\s+\d+)", lines[i].strip()):
|
| 110 |
+
text += " " + lines[i].strip()
|
| 111 |
+
i += 1
|
| 112 |
+
refer = extract_references(text)
|
| 113 |
+
current_article["clauses"].append({
|
| 114 |
+
"clause": "",
|
| 115 |
+
"content": text,
|
| 116 |
+
"refer": refer
|
| 117 |
+
})
|
| 118 |
+
else:
|
| 119 |
+
i += 1
|
| 120 |
+
|
| 121 |
+
# Thêm các phần còn lại vào cấu trúc
|
| 122 |
+
if current_article:
|
| 123 |
+
current_section["articles"].append(current_article)
|
| 124 |
+
if current_section:
|
| 125 |
+
current_chapter["sections"].append(current_section)
|
| 126 |
+
if current_chapter:
|
| 127 |
+
chapters.append(current_chapter)
|
| 128 |
+
|
| 129 |
+
return {"chapters": chapters}
|
| 130 |
+
|
| 131 |
+
# Đọc file .docx
|
| 132 |
+
def read_docx(file_path):
|
| 133 |
+
doc = Document(file_path)
|
| 134 |
+
full_text = []
|
| 135 |
+
for para in doc.paragraphs:
|
| 136 |
+
full_text.append(para.text)
|
| 137 |
+
return "\n".join(full_text)
|
| 138 |
+
|
| 139 |
+
# Đường dẫn đến file .docx
|
| 140 |
+
file_path = "data.docx"
|
| 141 |
+
|
| 142 |
+
# Đọc dữ liệu từ file .docx
|
| 143 |
+
data = read_docx(file_path)
|
| 144 |
+
|
| 145 |
+
# Phân tích dữ liệu và tạo JSON
|
| 146 |
+
json_data = parse_data_to_json(data)
|
| 147 |
+
|
| 148 |
+
# Lưu dữ liệu vào file JSON
|
| 149 |
+
with open("new_data.json", "w", encoding="utf-8") as json_file:
|
| 150 |
+
json.dump(json_data, json_file, ensure_ascii=False, indent=4)
|
| 151 |
+
|
| 152 |
+
print("Dữ liệu đã được lưu vào file new_data.json")
|
ChatBot/data/new_data.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
ChatBot/data/preprocessing.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import re
|
| 3 |
+
from docx import Document
|
| 4 |
+
|
| 5 |
+
# Hàm trích xuất tham chiếu từ một đoạn văn bản
|
| 6 |
+
def extract_references(text):
|
| 7 |
+
# Biểu thức chính quy để bắt các chuỗi "khoản X, khoản Y, ... điều Z" hoặc "điều Z"
|
| 8 |
+
pattern = r"(khoản \d+(?:, khoản \d+)* điều \d+|điều \d+)"
|
| 9 |
+
matches = re.findall(pattern, text, re.IGNORECASE)
|
| 10 |
+
refer = []
|
| 11 |
+
for match in matches:
|
| 12 |
+
refer.append(match)
|
| 13 |
+
return refer
|
| 14 |
+
|
| 15 |
+
# Hàm phân tích dữ liệu và tạo cấu trúc JSON
|
| 16 |
+
def parse_data_to_json(data):
|
| 17 |
+
chapters = []
|
| 18 |
+
current_chapter = None
|
| 19 |
+
current_section = None
|
| 20 |
+
current_article = None
|
| 21 |
+
lines = data.split("\n")
|
| 22 |
+
i = 0
|
| 23 |
+
|
| 24 |
+
while i < len(lines):
|
| 25 |
+
line = lines[i].strip()
|
| 26 |
+
# Xử lí chương
|
| 27 |
+
if line.startswith("CHƯƠNG"):
|
| 28 |
+
if current_chapter:
|
| 29 |
+
if current_section: # Thêm mục hiện tại vào chương trước khi chuyển sang chương mới
|
| 30 |
+
current_chapter["sections"].append(current_section)
|
| 31 |
+
current_section = None
|
| 32 |
+
chapters.append(current_chapter)
|
| 33 |
+
current_chapter = {
|
| 34 |
+
"chapter": f"{line} {lines[i+1].strip()}", # Kết hợp số chương và tiêu đề
|
| 35 |
+
"sections": []
|
| 36 |
+
}
|
| 37 |
+
i += 2
|
| 38 |
+
|
| 39 |
+
# Xử lí Mục
|
| 40 |
+
elif line.startswith("Mục"):
|
| 41 |
+
if current_section:
|
| 42 |
+
if current_article:
|
| 43 |
+
current_section["articles"].append(current_article)
|
| 44 |
+
current_article = None
|
| 45 |
+
current_chapter["sections"].append(current_section)
|
| 46 |
+
current_section = {
|
| 47 |
+
"section": f"{line} {lines[i+1].strip()}", # Kết hợp số mục và tiêu đề
|
| 48 |
+
"articles": []
|
| 49 |
+
}
|
| 50 |
+
i += 2
|
| 51 |
+
|
| 52 |
+
# Xử lí Điều
|
| 53 |
+
elif line.startswith("Điều"):
|
| 54 |
+
if current_section is None:
|
| 55 |
+
current_section = {
|
| 56 |
+
"section": "", # Kết hợp số mục và tiêu đề
|
| 57 |
+
"articles": []
|
| 58 |
+
}
|
| 59 |
+
if current_article:
|
| 60 |
+
current_section["articles"].append(current_article)
|
| 61 |
+
current_article = {
|
| 62 |
+
"article": line,
|
| 63 |
+
"title": line.split('.')[1].strip(),
|
| 64 |
+
"clauses": []
|
| 65 |
+
}
|
| 66 |
+
# text = line.split('.')[1].strip()
|
| 67 |
+
i += 1
|
| 68 |
+
# Kiểm tra xem điều này có câu ngắn trước các khoản không (sử dụng re.match)
|
| 69 |
+
if i < len(lines) and lines[i].strip() and re.match(r"^\D", lines[i].strip()):
|
| 70 |
+
# Lưu câu ngắn để ghép vào khoản đầu tiên
|
| 71 |
+
text = lines[i].strip()
|
| 72 |
+
i += 1
|
| 73 |
+
else:
|
| 74 |
+
text = ""
|
| 75 |
+
flag = False
|
| 76 |
+
|
| 77 |
+
# Xử lý các khoản
|
| 78 |
+
while i < len(lines) and lines[i].strip() and lines[i].strip()[0].isdigit():
|
| 79 |
+
flag = True
|
| 80 |
+
# Bắt đầu một khoản mới
|
| 81 |
+
clause_number = f"Khoản {lines[i].strip().split('.')[0]}"
|
| 82 |
+
clause_content = lines[i].strip()
|
| 83 |
+
i += 1
|
| 84 |
+
# Xử lý các điểm trong khoản
|
| 85 |
+
while i < len(lines) and lines[i].strip() and re.match(r"^[a-zđ]\)", lines[i].strip()):
|
| 86 |
+
clause_content += " " + lines[i].strip()
|
| 87 |
+
i += 1
|
| 88 |
+
# Ghép câu ngắn vào khoản đầu tiên
|
| 89 |
+
if text:
|
| 90 |
+
clause_content = f"{text} {clause_content}"
|
| 91 |
+
text = "" # Đã sử dụng câu ngắn, không cần ghép vào khoản tiếp theo
|
| 92 |
+
refer = extract_references(clause_content)
|
| 93 |
+
current_article["clauses"].append({
|
| 94 |
+
"clause": clause_number, # Số khoản
|
| 95 |
+
"content": clause_content,
|
| 96 |
+
"refer": refer
|
| 97 |
+
})
|
| 98 |
+
if flag == False:
|
| 99 |
+
refer = extract_references(text)
|
| 100 |
+
current_article["clauses"].append({
|
| 101 |
+
"clause": "",
|
| 102 |
+
"content": text,
|
| 103 |
+
"refer": refer
|
| 104 |
+
})
|
| 105 |
+
else:
|
| 106 |
+
i += 1
|
| 107 |
+
|
| 108 |
+
# Thêm các phần còn lại vào cấu trúc
|
| 109 |
+
if current_article:
|
| 110 |
+
current_section["articles"].append(current_article)
|
| 111 |
+
if current_section:
|
| 112 |
+
current_chapter["sections"].append(current_section)
|
| 113 |
+
if current_chapter:
|
| 114 |
+
chapters.append(current_chapter)
|
| 115 |
+
|
| 116 |
+
return {"chapters": chapters}
|
| 117 |
+
|
| 118 |
+
# Đọc file .docx
|
| 119 |
+
def read_docx(file_path):
|
| 120 |
+
doc = Document(file_path)
|
| 121 |
+
full_text = []
|
| 122 |
+
for para in doc.paragraphs:
|
| 123 |
+
full_text.append(para.text)
|
| 124 |
+
return "\n".join(full_text)
|
| 125 |
+
|
| 126 |
+
# Đường dẫn đến file .docx
|
| 127 |
+
file_path = "data.docx"
|
| 128 |
+
|
| 129 |
+
# Đọc dữ liệu từ file .docx
|
| 130 |
+
data = read_docx(file_path)
|
| 131 |
+
|
| 132 |
+
# Phân tích d��� liệu và tạo JSON
|
| 133 |
+
json_data = parse_data_to_json(data)
|
| 134 |
+
|
| 135 |
+
# Lưu dữ liệu vào file JSON
|
| 136 |
+
with open("legal_data2.json", "w", encoding="utf-8") as json_file:
|
| 137 |
+
json.dump(json_data, json_file, ensure_ascii=False, indent=4)
|
| 138 |
+
|
| 139 |
+
print("Dữ liệu đã được lưu vào file legal_data2.json")
|
ChatBot/indexing.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
from qdrant_client import models
|
| 3 |
+
from langchain_qdrant import QdrantVectorStore, FastEmbedSparse, RetrievalMode
|
| 4 |
+
from langchain_huggingface import HuggingFaceEmbeddings
|
| 5 |
+
from langchain.schema import Document
|
| 6 |
+
import os
|
| 7 |
+
from dotenv import load_dotenv
|
| 8 |
+
|
| 9 |
+
def save_to_vector_database(data):
|
| 10 |
+
documents = []
|
| 11 |
+
|
| 12 |
+
for chapter in data['chapters']:
|
| 13 |
+
print (f"Loading {chapter['chapter']} ...")
|
| 14 |
+
for section in chapter['sections']:
|
| 15 |
+
for article in section['articles']:
|
| 16 |
+
for clause in article['clauses']:
|
| 17 |
+
chunk = clause['content']
|
| 18 |
+
if clause['clause'] == '' or clause['clause'] == 'Khoản 1':
|
| 19 |
+
chunk = article['title'] + ". " + clause['content']
|
| 20 |
+
documents.append(Document(
|
| 21 |
+
page_content=chunk,
|
| 22 |
+
metadata={
|
| 23 |
+
'chapter': chapter['chapter'],
|
| 24 |
+
'section': section['section'],
|
| 25 |
+
'article': article['article'],
|
| 26 |
+
'art_title': article['title'],
|
| 27 |
+
'clause': clause['clause'],
|
| 28 |
+
'refer': clause['refer']
|
| 29 |
+
}
|
| 30 |
+
))
|
| 31 |
+
|
| 32 |
+
return documents
|
| 33 |
+
|
| 34 |
+
with open('./data/new_data.json', 'r', encoding='utf-8') as f:
|
| 35 |
+
data = json.load(f)
|
| 36 |
+
|
| 37 |
+
# Tạo các Document từ dữ liệu
|
| 38 |
+
documents = save_to_vector_database(data)
|
| 39 |
+
|
| 40 |
+
# Tạo embedding cho các Document bằng SentenceTransformer
|
| 41 |
+
model_name = "dangvantuan/vietnamese-embedding"
|
| 42 |
+
embedding_model = HuggingFaceEmbeddings(model_name=model_name)
|
| 43 |
+
sparse_embeddings = FastEmbedSparse(model_name="Qdrant/BM25")
|
| 44 |
+
|
| 45 |
+
load_dotenv()
|
| 46 |
+
# Lưu dữ liệu vào Qdrant
|
| 47 |
+
vectorstore = QdrantVectorStore.from_documents(
|
| 48 |
+
documents,
|
| 49 |
+
embedding_model,
|
| 50 |
+
sparse_embedding=sparse_embeddings,
|
| 51 |
+
retrieval_mode=RetrievalMode.HYBRID,
|
| 52 |
+
url=os.getenv("QDRANT_URL"),
|
| 53 |
+
api_key=os.getenv("QDRANT_API_KEY"),
|
| 54 |
+
collection_name="LAWDATA",
|
| 55 |
+
distance=models.Distance.COSINE
|
| 56 |
+
)
|
| 57 |
+
|
| 58 |
+
print("Dữ liệu đã được lưu vào Qdrant Cloud!")
|
ChatBot/query_transformation.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from langchain_google_genai import ChatGoogleGenerativeAI
|
| 2 |
+
from langchain.prompts import ChatPromptTemplate
|
| 3 |
+
from langchain_core.output_parsers import StrOutputParser
|
| 4 |
+
import os
|
| 5 |
+
from dotenv import load_dotenv
|
| 6 |
+
class QueryTransform:
|
| 7 |
+
def __init__(self, model="gemini-1.5-flash", temperature=0.3):
|
| 8 |
+
load_dotenv()
|
| 9 |
+
genai_api_key = os.getenv('GOOGLE_API_KEY')
|
| 10 |
+
|
| 11 |
+
# Prompt
|
| 12 |
+
self.transform_prompt = ChatPromptTemplate.from_template("""
|
| 13 |
+
Bạn là một mô hình phân tích câu hỏi. Nhiệm vụ của bạn là xử lý câu hỏi (hoặc câu khẳng định) của người dùng theo từng bước sau:
|
| 14 |
+
1. Nếu câu hỏi là ngôn ngữ khác tiếng Việt, trả về Kết quả: Xin lỗi, tôi không hiểu bạn đang nói gì, vui lòng sử dụng tiếng Việt.
|
| 15 |
+
2. Sửa lỗi chính tả, gõ sai, viết tắt cho câu hỏi trong tiếng Việt. Ví dụ: Tôi li hôn đc k? -> Tôi ly hôn được không?, thũ tụt -> thủ tục
|
| 16 |
+
3. NẾU CÂU HỎI CÓ NHIỀU Ý HỎI, tách chúng thành từng câu hỏi riêng biệt. Với mỗi câu hỏi thực hiện các bước tiếp theo.
|
| 17 |
+
4. NẾU CÂU HỎI chứa SỐ ĐIỀU/SỐ CHƯƠNG cụ thể và trong lịch sử trò chuyện có chứa:
|
| 18 |
+
+ "Vui lòng chọn Điều cụ thể.", thì ý định của câu hỏi là: Khoản <số Khoản trong lịch sử> Điều <SỐ/SỐ ĐIỀU trong câu hỏi>. Ngược lại ý định là: Điều <SỐ/SỐ ĐIỀU trong câu hỏi>
|
| 19 |
+
+ "Vui lòng chọn Chương cụ thể", thì ý định của câu hỏi là: Mục <số Mục trong lịch sử> Chương <SỐ/SỐ CHƯƠNG trong câu hỏi>. Ngược lại ý định là: Chương <SỐ/SỐ CHƯƠNG trong câu hỏi>
|
| 20 |
+
5. **NẾU CÂU HỎI KHÔNG RÕ RÀNG HOẶC CHƯA ĐẦY ĐỦ**, hãy sử dụng lịch sử trò chuyện được cung cấp **CHỈ KHI NÓ LIÊN QUAN TRỰC TIẾP ĐẾN CÂU HỎI** để làm rõ ý nghĩa và ngữ cảnh truy vấn của người dùng. **BỎ QUA THÔNG TIN KHÔNG LIÊN QUAN**.
|
| 21 |
+
6. Suy ra nội dung chính của câu hỏi (hoặc câu khẳng định) thật đơn giản, rõ ràng và dễ hiểu (dùng các từ ngữ trong luật Hôn nhân và Gia đình nếu có thể), **BỎ QUA ĐẠI TỪ DANH XƯNG NẾU CÓ THỂ**. Ví dụ: Tôi là nữ 16 tuổi thì có lấy chồng được không? -> Nữ 16 tuổi có đủ điều kiện kết hôn không?, Người bị điên có lấy vợ được không? -> Người mất hành vi dân sự có được phép kết hôn không?
|
| 22 |
+
|
| 23 |
+
Chỉ cung cấp kết quả **theo định dạng sau** và không thêm bất kỳ văn bản, giải thích hoặc bình luận nào khác:
|
| 24 |
+
Kết quả: <nội dung chính của câu hỏi 1>|<nội dung chính của câu hỏi 2>...
|
| 25 |
+
|
| 26 |
+
Lịch sử: {history}
|
| 27 |
+
Câu hỏi gốc: {query}
|
| 28 |
+
""")
|
| 29 |
+
|
| 30 |
+
self.model = ChatGoogleGenerativeAI(api_key=genai_api_key, model=model, temperature=temperature)
|
| 31 |
+
self.parser = StrOutputParser()
|
| 32 |
+
|
| 33 |
+
def transform(self, raw_query, history):
|
| 34 |
+
prompt = self.transform_prompt.format_prompt(query=raw_query, history=history)
|
| 35 |
+
response = self.parser.parse(self.model.invoke(prompt).content)
|
| 36 |
+
lines = response.split("\n")
|
| 37 |
+
result = lines[0].replace("Kết quả:", "").strip()
|
| 38 |
+
result = result.split("|")
|
| 39 |
+
return result
|
ChatBot/requirements.txt
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
python-docx
|
| 2 |
+
transformers
|
| 3 |
+
sentence-transformers
|
| 4 |
+
qdrant-client
|
| 5 |
+
langchain
|
| 6 |
+
langchain-community
|
| 7 |
+
langchain-qdrant
|
| 8 |
+
fastembed
|
| 9 |
+
langchain-google-genai
|
| 10 |
+
semantic-router
|
| 11 |
+
neo4j
|
| 12 |
+
roman
|
ChatBot/retrieval.py
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from dotenv import load_dotenv
|
| 3 |
+
from langchain_qdrant import RetrievalMode
|
| 4 |
+
from langchain_qdrant import QdrantVectorStore
|
| 5 |
+
from qdrant_client import models
|
| 6 |
+
from langchain_qdrant import QdrantVectorStore, RetrievalMode
|
| 7 |
+
from transformers import AutoModelForSequenceClassification, AutoTokenizer
|
| 8 |
+
import torch
|
| 9 |
+
from neo4j import GraphDatabase
|
| 10 |
+
import re
|
| 11 |
+
import roman
|
| 12 |
+
|
| 13 |
+
class Retriever:
|
| 14 |
+
def __init__(self, embedding_model, sparse_embedding, rerank_model="itdainb/PhoRanker"):
|
| 15 |
+
# Tải các biến môi trường từ file .env
|
| 16 |
+
load_dotenv()
|
| 17 |
+
# Qdrant
|
| 18 |
+
collection_name = os.getenv('QDRANT_COLLECTION_NAME')
|
| 19 |
+
url = os.getenv('QDRANT_URL')
|
| 20 |
+
api_key = os.getenv('QDRANT_API_KEY')
|
| 21 |
+
|
| 22 |
+
uri = os.getenv('NEO4J_URI')
|
| 23 |
+
auth = (os.getenv('NEO4J_USERNAME'), os.getenv('NEO4J_PASS'))
|
| 24 |
+
|
| 25 |
+
# Khởi tạo kho lưu trữ vector
|
| 26 |
+
self.vector_store = QdrantVectorStore.from_existing_collection(
|
| 27 |
+
embedding=embedding_model,
|
| 28 |
+
collection_name=collection_name,
|
| 29 |
+
url=url,
|
| 30 |
+
api_key=api_key,
|
| 31 |
+
sparse_embedding=sparse_embedding,
|
| 32 |
+
retrieval_mode=RetrievalMode.DENSE,
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
self.retriever = self.vector_store.as_retriever()
|
| 36 |
+
self.tokenizer = AutoTokenizer.from_pretrained(rerank_model)
|
| 37 |
+
self.rerank_model = AutoModelForSequenceClassification.from_pretrained(rerank_model)
|
| 38 |
+
self.driver = GraphDatabase.driver(uri, auth=auth)
|
| 39 |
+
|
| 40 |
+
self.methods = {
|
| 41 |
+
0: self.unknown, # Ngôn ngữ khác
|
| 42 |
+
1: self.marriage_and_family, # Tổng quát
|
| 43 |
+
2: self.smalltalk, # Smalltalk
|
| 44 |
+
3: self.unrelated, # Không liên quan
|
| 45 |
+
4: self.khoan_with_dieu, # Khoản có Điều
|
| 46 |
+
5: self.khoan_no_dieu, # Khoản không có
|
| 47 |
+
6: self.muc_with_chuong, # Mục có Chương
|
| 48 |
+
7: self.muc_no_chuong, # Mục không có Chương
|
| 49 |
+
8: self.dieu_only, # Chỉ có Điều
|
| 50 |
+
9: self.chuong_only, # Chỉ có Chương
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
def rerank(self, query, documents):
|
| 54 |
+
# Tokenize query và documents
|
| 55 |
+
inputs = self.tokenizer([query] * len(documents), documents, return_tensors="pt", padding=True, truncation=True)
|
| 56 |
+
|
| 57 |
+
# Tính điểm số
|
| 58 |
+
with torch.no_grad():
|
| 59 |
+
outputs = self.rerank_model(**inputs)
|
| 60 |
+
scores = outputs.logits.squeeze().tolist()
|
| 61 |
+
|
| 62 |
+
indexed_documents = list(zip(range(len(documents)), scores))
|
| 63 |
+
indexed_documents.sort(key=lambda x: x[1], reverse=True)
|
| 64 |
+
ranked_indices = [i for i, _ in indexed_documents]
|
| 65 |
+
|
| 66 |
+
return ranked_indices
|
| 67 |
+
|
| 68 |
+
def unknown(self, query):
|
| 69 |
+
return query
|
| 70 |
+
|
| 71 |
+
def marriage_and_family(self, query, top_k=10, top_n=5, rerank=False):
|
| 72 |
+
|
| 73 |
+
results = self.retriever.get_relevant_documents(query, k=top_k)
|
| 74 |
+
|
| 75 |
+
if rerank:
|
| 76 |
+
docs = [doc.page_content for doc in results]
|
| 77 |
+
docs = self.rerank(query, docs)
|
| 78 |
+
results = [results[i] for i in docs]
|
| 79 |
+
|
| 80 |
+
res = {}
|
| 81 |
+
refers = set()
|
| 82 |
+
for doc in results[:top_n]:
|
| 83 |
+
article = doc.metadata['article']
|
| 84 |
+
if article == "Điều 3":
|
| 85 |
+
if article not in res:
|
| 86 |
+
res[article] = {'title': doc.metadata['art_title'],
|
| 87 |
+
'content': set([doc.page_content])}
|
| 88 |
+
else: res[article]['content'].add(doc.page_content)
|
| 89 |
+
|
| 90 |
+
elif article not in res:
|
| 91 |
+
res[article] = {'title': doc.metadata['art_title']}
|
| 92 |
+
|
| 93 |
+
filter_condition = models.Filter(must=[models.FieldCondition(
|
| 94 |
+
key="metadata.article", # Trường metadata cần lọc
|
| 95 |
+
match=models.MatchValue(value=article),
|
| 96 |
+
)])
|
| 97 |
+
temp = self.retriever.get_relevant_documents(query="", filter=filter_condition)
|
| 98 |
+
res[article]['content'] = set([d.page_content for d in temp])
|
| 99 |
+
for art in temp:
|
| 100 |
+
if art.metadata['refer']: refers.update(art.metadata['refer'])
|
| 101 |
+
|
| 102 |
+
for ref in refers:
|
| 103 |
+
match_ = re.match(r"(?:Khoản\s*(\d+)\s*)?điều\s*(\d+)", ref, re.IGNORECASE)
|
| 104 |
+
refer_clause = match_.group(1)
|
| 105 |
+
refer_article = f"Điều {match_.group(2)}"
|
| 106 |
+
if refer_article not in res:
|
| 107 |
+
res[refer_article] = {'title': None, 'content': set()}
|
| 108 |
+
filter_condition2 = models.Filter(must=[models.FieldCondition(
|
| 109 |
+
key="metadata.article",
|
| 110 |
+
match=models.MatchValue(
|
| 111 |
+
value=refer_article))] )
|
| 112 |
+
if refer_clause:
|
| 113 |
+
filter_condition2.must.append(models.FieldCondition(
|
| 114 |
+
key="metadata.clause",
|
| 115 |
+
match=models.MatchValue(value=f"Khoản {refer_clause}")))
|
| 116 |
+
temp2 = self.retriever.get_relevant_documents(query="", filter=filter_condition2)
|
| 117 |
+
res[refer_article]['content'].update([d.page_content for d in temp2])
|
| 118 |
+
res[refer_article]['title'] = temp2[0].metadata['art_title']
|
| 119 |
+
|
| 120 |
+
context = ""
|
| 121 |
+
for key, value in res.items():
|
| 122 |
+
context += f"{key}: {value['title']}\n"
|
| 123 |
+
for item in value["content"]:
|
| 124 |
+
context += f"{item}\n"
|
| 125 |
+
context += "\n"
|
| 126 |
+
return context
|
| 127 |
+
|
| 128 |
+
def smalltalk(self, query):
|
| 129 |
+
return query
|
| 130 |
+
|
| 131 |
+
def unrelated(self, query):
|
| 132 |
+
return "Xin lỗi, câu hỏi này không phải lĩnh vực của tôi."
|
| 133 |
+
|
| 134 |
+
def muc_no_chuong(self, query):
|
| 135 |
+
return "Vui lòng chọn Chương cụ thể."
|
| 136 |
+
|
| 137 |
+
def muc_with_chuong(self, query):
|
| 138 |
+
# Tìm mục và chương từ câu truy vấn
|
| 139 |
+
match = re.search(r"mục\s+(\d+).*chương\s+(\w+)", query, re.IGNORECASE)
|
| 140 |
+
|
| 141 |
+
section = int(match.group(1))
|
| 142 |
+
chapter = match.group(2).upper()
|
| 143 |
+
|
| 144 |
+
# Xử lý chương
|
| 145 |
+
if chapter.isdigit():
|
| 146 |
+
chapter_number = int(chapter)
|
| 147 |
+
chapter_roman = roman.toRoman(chapter_number)
|
| 148 |
+
else:
|
| 149 |
+
try:
|
| 150 |
+
chapter_number = roman.fromRoman(chapter)
|
| 151 |
+
chapter_roman = chapter
|
| 152 |
+
except roman.InvalidRomanNumeralError:
|
| 153 |
+
return f"'{chapter}' không phải là số hoặc số La Mã hợp lệ."
|
| 154 |
+
|
| 155 |
+
if chapter_number < 1 or chapter_number > 10:
|
| 156 |
+
return f"Luật Hôn nhân và Gia Đình Việt Nam không có Chương {chapter}."
|
| 157 |
+
|
| 158 |
+
# Truy vấn cơ sở dữ liệu
|
| 159 |
+
with self.driver.session() as session:
|
| 160 |
+
result = session.run(
|
| 161 |
+
"""
|
| 162 |
+
MATCH (ch:Chapter)-[:HAS_SECTION]->(sec:Section)-[:HAS_ARTICLE]->(art:Article)
|
| 163 |
+
WHERE ch.name =~ $chapter_pattern AND sec.name =~ $section_pattern
|
| 164 |
+
RETURN ch.name AS chapter_name, sec.name AS section_name, art.name AS article_name
|
| 165 |
+
""",
|
| 166 |
+
chapter_pattern=f"^CHƯƠNG {chapter_roman}(\\s|$).*",
|
| 167 |
+
section_pattern=f"^Mục {section}(\\s|$).*"
|
| 168 |
+
)
|
| 169 |
+
|
| 170 |
+
# Xử lý kết quả
|
| 171 |
+
records = result.data()
|
| 172 |
+
if not records:
|
| 173 |
+
return f"Chương {chapter} không có mục {section}."
|
| 174 |
+
|
| 175 |
+
# Xây dựng nội dung trả về
|
| 176 |
+
context = f"{records[0]['chapter_name']} - {records[0]['section_name']}:"
|
| 177 |
+
for record in records:
|
| 178 |
+
context += f"\n - {record['article_name']}"
|
| 179 |
+
|
| 180 |
+
return context
|
| 181 |
+
|
| 182 |
+
def khoan_no_dieu(self, query):
|
| 183 |
+
return "Vui lòng chọn Điều cụ thể."
|
| 184 |
+
|
| 185 |
+
def khoan_with_dieu(self, query): # Khoản có Điều
|
| 186 |
+
match = re.search(r"khoản\s+(\d+)\s+điều\s+(\d+)", query, re.IGNORECASE)
|
| 187 |
+
clause, article = match.group(1), match.group(2)
|
| 188 |
+
if int(article) < 1 or int(article) > 133:
|
| 189 |
+
return f'Luật Hôn nhân và Gia Đình Việt Nam không có Điều {article}'
|
| 190 |
+
|
| 191 |
+
with self.driver.session() as session:
|
| 192 |
+
result = session.run(
|
| 193 |
+
"""
|
| 194 |
+
MATCH (a:Article)-[:HAS_CLAUSE]->(c:Clause)
|
| 195 |
+
WHERE a.name =~ $article_number AND c.name = $clause_number
|
| 196 |
+
RETURN a.name AS article_name, c.content AS clause_content
|
| 197 |
+
""",
|
| 198 |
+
article_number=f"^Điều {article}(\\s|$).*",
|
| 199 |
+
clause_number=f"Khoản {clause}",
|
| 200 |
+
)
|
| 201 |
+
record = result.single()
|
| 202 |
+
if record:
|
| 203 |
+
context = f"{record['article_name']}:\n{record['clause_content']}"
|
| 204 |
+
return context
|
| 205 |
+
return f"Điều {article} không có Khoản {clause}."
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
def dieu_only(self, query):
|
| 209 |
+
# Tìm số điều từ query
|
| 210 |
+
match = re.search(r"điều\s+(\d+)", query, re.IGNORECASE)
|
| 211 |
+
|
| 212 |
+
article = match.group(1)
|
| 213 |
+
with self.driver.session() as session:
|
| 214 |
+
result = session.run(
|
| 215 |
+
"""
|
| 216 |
+
MATCH (a:Article)-[:HAS_CLAUSE]->(c:Clause)
|
| 217 |
+
WHERE a.name =~ $article_number
|
| 218 |
+
RETURN a.name AS article_name, c.content AS clause_content
|
| 219 |
+
""",
|
| 220 |
+
article_number=f"^Điều {article}(\\s|$).*",
|
| 221 |
+
)
|
| 222 |
+
|
| 223 |
+
# Xử lý kết quả truy vấn
|
| 224 |
+
records = result.data()
|
| 225 |
+
if not records:
|
| 226 |
+
return f'Luật Hôn nhân và Gia Đình Việt Nam không có Điều {article}'
|
| 227 |
+
|
| 228 |
+
# Tạo nội dung trả về
|
| 229 |
+
context = f"{records[0]['article_name']}:"
|
| 230 |
+
for record in records:
|
| 231 |
+
context += f"\n{record['clause_content']}"
|
| 232 |
+
return context
|
| 233 |
+
|
| 234 |
+
|
| 235 |
+
def chuong_only(self, query):
|
| 236 |
+
# Tìm số chương từ câu truy vấn
|
| 237 |
+
match = re.search(r"chương\s+(\w+)", query, re.IGNORECASE)
|
| 238 |
+
chapter = match.group(1).upper()
|
| 239 |
+
if chapter.isdigit():
|
| 240 |
+
chapter_number = int(chapter)
|
| 241 |
+
chapter_roman = roman.toRoman(chapter_number)
|
| 242 |
+
else:
|
| 243 |
+
try:
|
| 244 |
+
chapter_number = roman.fromRoman(chapter)
|
| 245 |
+
chapter_roman = chapter
|
| 246 |
+
except roman.InvalidRomanNumeralError:
|
| 247 |
+
return f"'{chapter}' không phải là số hoặc số La Mã hợp lệ."
|
| 248 |
+
|
| 249 |
+
with self.driver.session() as session:
|
| 250 |
+
result = session.run(
|
| 251 |
+
"""
|
| 252 |
+
MATCH (ch:Chapter)
|
| 253 |
+
WHERE ch.name =~ $chapter_pattern
|
| 254 |
+
OPTIONAL MATCH (ch)-[:HAS_SECTION]->(sec:Section)-[:HAS_ARTICLE]->(art:Article)
|
| 255 |
+
OPTIONAL MATCH (ch)-[:HAS_ARTICLE]->(art_direct:Article)
|
| 256 |
+
RETURN ch.name AS chapter_name,
|
| 257 |
+
sec.name AS section_name,
|
| 258 |
+
art.name AS article_name,
|
| 259 |
+
art_direct.name AS direct_article_name
|
| 260 |
+
""",
|
| 261 |
+
chapter_pattern=f"^CHƯƠNG {chapter_roman}(\\s|$).*"
|
| 262 |
+
)
|
| 263 |
+
|
| 264 |
+
# Xử lý kết quả
|
| 265 |
+
records = result.data()
|
| 266 |
+
if not records:
|
| 267 |
+
return f"Không tìm thấy dữ liệu cho Chương {chapter}."
|
| 268 |
+
|
| 269 |
+
# Xây dựng nội dung trả về
|
| 270 |
+
context = f"{records[0]['chapter_name']}:"
|
| 271 |
+
sections = {}
|
| 272 |
+
direct_articles = []
|
| 273 |
+
|
| 274 |
+
for record in records:
|
| 275 |
+
if record['section_name']:
|
| 276 |
+
section_name = record['section_name']
|
| 277 |
+
article_name = record['article_name']
|
| 278 |
+
if section_name not in sections:
|
| 279 |
+
sections[section_name] = []
|
| 280 |
+
if article_name:
|
| 281 |
+
sections[section_name].append(article_name)
|
| 282 |
+
if record['direct_article_name']:
|
| 283 |
+
direct_articles.append(record['direct_article_name'])
|
| 284 |
+
|
| 285 |
+
# Ghi các section và article của chúng
|
| 286 |
+
for section, articles in sections.items():
|
| 287 |
+
context += f"\n{section}:"
|
| 288 |
+
for article in articles:
|
| 289 |
+
context += f"\n - {article}"
|
| 290 |
+
|
| 291 |
+
# Ghi các article trực tiếp của chương
|
| 292 |
+
if direct_articles:
|
| 293 |
+
for article in direct_articles:
|
| 294 |
+
context += f"\n - {article}"
|
| 295 |
+
|
| 296 |
+
return context
|
| 297 |
+
|
| 298 |
+
def retrieve(self, queries, types):
|
| 299 |
+
context = ""
|
| 300 |
+
for q, t in zip(queries, types):
|
| 301 |
+
context += self.methods[t](q) + "\n----------------"
|
| 302 |
+
|
| 303 |
+
return context
|
ChatBot/route.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from semantic_router import Route
|
| 2 |
+
from semantic_router.encoders import HuggingFaceEncoder
|
| 3 |
+
from semantic_router.layer import RouteLayer
|
| 4 |
+
import re
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class Router:
|
| 8 |
+
def __init__(self, model_name="dangvantuan/vietnamese-embedding"):
|
| 9 |
+
self.encoder = HuggingFaceEncoder(model_name=model_name)
|
| 10 |
+
self.routes = self.initialize_routes()
|
| 11 |
+
self.rl = RouteLayer(encoder=self.encoder, routes=self.routes)
|
| 12 |
+
|
| 13 |
+
def initialize_routes(self):
|
| 14 |
+
# Route cho câu hỏi liên quan đến hôn nhân và gia đình
|
| 15 |
+
marriage_and_family = Route(
|
| 16 |
+
name="1",
|
| 17 |
+
score_thresold=0.64,
|
| 18 |
+
utterances=[
|
| 19 |
+
"quy định về kết hôn như thế nào?",
|
| 20 |
+
"ly hôn có cần sự đồng ý của cả hai không?",
|
| 21 |
+
"quyền nuôi con sau khi ly hôn thuộc về ai?",
|
| 22 |
+
"phân chia tài sản khi ly hôn thế nào?",
|
| 23 |
+
"điều kiện để nhận con nuôi là gì?",
|
| 24 |
+
"kết hôn có cần đăng ký không?",
|
| 25 |
+
"ngoại tình có vi phạm pháp luật không?",
|
| 26 |
+
"người không nuôi con có quyền thăm con sau ly hôn không?",
|
| 27 |
+
"kết hôn với người nước ngoài cần những giấy tờ gì?",
|
| 28 |
+
"tảo hôn là gì?",
|
| 29 |
+
"ông bà có quyền nuôi cháu sau khi cha mẹ ly hôn không?",
|
| 30 |
+
],
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
# Route cho smalltalk
|
| 34 |
+
smalltalk = Route(
|
| 35 |
+
name="2",
|
| 36 |
+
score_thresold=0.34,
|
| 37 |
+
utterances=[
|
| 38 |
+
"chào bạn!",
|
| 39 |
+
"hôm nay trời đẹp nhỉ?",
|
| 40 |
+
"bạn có thể giúp tôi không?",
|
| 41 |
+
"bạn tên là gì?",
|
| 42 |
+
"bạn làm việc ở đâu?",
|
| 43 |
+
"bạn khỏe không?",
|
| 44 |
+
"rất vui được gặp bạn!",
|
| 45 |
+
"bạn thông minh lắm đấy!",
|
| 46 |
+
"bạn có sở thích gì không?",
|
| 47 |
+
"cảm ơn bạn.",
|
| 48 |
+
"tạm biệt.",
|
| 49 |
+
"Xin chào",
|
| 50 |
+
"Bạn thú vị đó!",
|
| 51 |
+
"Có gì vui không, kể cho tôi nghe với?",
|
| 52 |
+
"Giúp tôi tí được không?",
|
| 53 |
+
"Có kế hoạch gì hay không bạn?",
|
| 54 |
+
"Thời tiết hôm nay thế nào?",
|
| 55 |
+
],
|
| 56 |
+
)
|
| 57 |
+
|
| 58 |
+
# Route cho câu hỏi không liên quan
|
| 59 |
+
unrelated = Route(
|
| 60 |
+
name="3",
|
| 61 |
+
score_thresold=0.9,
|
| 62 |
+
utterances=[
|
| 63 |
+
"Những món ăn nào phổ biến nhất ở Ấn Độ?",
|
| 64 |
+
"Tại sao con người cần có mục tiêu?",
|
| 65 |
+
"Tự do cá nhân có quan trọng không?",
|
| 66 |
+
"Cách chăm sóc cây cảnh trong nhà?",
|
| 67 |
+
"AI có thể thay đổi cuộc sống của con người ra sao?",
|
| 68 |
+
"Blockchain là gì và nó hoạt động như thế nào?",
|
| 69 |
+
"Ai là cầu thủ ghi nhiều bàn thắng nhất mọi thời đại?",
|
| 70 |
+
"Tại sao Kim tự tháp được coi là kỳ quan?",
|
| 71 |
+
"Cuộc cách mạng công nghiệp diễn ra khi nào?",
|
| 72 |
+
"Ý nghĩa cuộc sống?",
|
| 73 |
+
"Nước là gì?",
|
| 74 |
+
"Con người có phải loài thông minh nhất?"
|
| 75 |
+
],
|
| 76 |
+
)
|
| 77 |
+
return [marriage_and_family, smalltalk, unrelated]
|
| 78 |
+
|
| 79 |
+
def route_query(self, query):
|
| 80 |
+
"""
|
| 81 |
+
Xác định loại câu hỏi dựa trên nội dung query.
|
| 82 |
+
"""
|
| 83 |
+
query_ = query.strip().lower() # Chuẩn hóa query
|
| 84 |
+
|
| 85 |
+
if query_ == "không hiểu":
|
| 86 |
+
return 0
|
| 87 |
+
|
| 88 |
+
patterns = {
|
| 89 |
+
4: r"\bkhoản\s+\d+\b.*\bđiều\s+\d+\b", # Có khoản và điều cụ thể
|
| 90 |
+
5: r"\bkhoản\s+\d+\b(?!.*\bđiều\b)", # Chỉ có khoản cụ thể, không có điều
|
| 91 |
+
6: r"\bmục\s+\d+\b.*\bchương\s+([IVXLCDMivxlcdm]+|\d+)\b", # Có mục và chương cụ thể (số La Mã hoặc số thường)
|
| 92 |
+
7: r"\bmục\s+\d+\b(?!.*\bchương\b)", # Chỉ có mục cụ thể, không có chương
|
| 93 |
+
8: r"\bđiều\s+\d+\b", # Chỉ có điều cụ thể
|
| 94 |
+
9: r"\bchương\s+([IVXLCDMivxlcdm]+|\d+)\b" # Chỉ có chương cụ thể (số La Mã hoặc số thường)
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
# Kiểm tra từng mẫu regex
|
| 98 |
+
for query_type, pattern in patterns.items():
|
| 99 |
+
if re.search(pattern, query_, re.IGNORECASE):
|
| 100 |
+
return query_type
|
| 101 |
+
|
| 102 |
+
query_type = self.rl(query).name
|
| 103 |
+
return int(query_type) if query_type else 3
|
ChatBot/try.ipynb
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
FrontEnd/.gitignore
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Logs
|
| 2 |
+
logs
|
| 3 |
+
*.log
|
| 4 |
+
npm-debug.log*
|
| 5 |
+
yarn-debug.log*
|
| 6 |
+
yarn-error.log*
|
| 7 |
+
pnpm-debug.log*
|
| 8 |
+
lerna-debug.log*
|
| 9 |
+
|
| 10 |
+
node_modules
|
| 11 |
+
dist
|
| 12 |
+
dist-ssr
|
| 13 |
+
*.local
|
| 14 |
+
|
| 15 |
+
# Editor directories and files
|
| 16 |
+
.vscode/*
|
| 17 |
+
!.vscode/extensions.json
|
| 18 |
+
.idea
|
| 19 |
+
.DS_Store
|
| 20 |
+
*.suo
|
| 21 |
+
*.ntvs*
|
| 22 |
+
*.njsproj
|
| 23 |
+
*.sln
|
| 24 |
+
*.sw?
|
FrontEnd/README.md
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# React + Vite
|
| 2 |
+
|
| 3 |
+
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
| 4 |
+
|
| 5 |
+
Currently, two official plugins are available:
|
| 6 |
+
|
| 7 |
+
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
| 8 |
+
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
FrontEnd/eslint.config.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import js from '@eslint/js'
|
| 2 |
+
import globals from 'globals'
|
| 3 |
+
import react from 'eslint-plugin-react'
|
| 4 |
+
import reactHooks from 'eslint-plugin-react-hooks'
|
| 5 |
+
import reactRefresh from 'eslint-plugin-react-refresh'
|
| 6 |
+
|
| 7 |
+
export default [
|
| 8 |
+
{ ignores: ['dist'] },
|
| 9 |
+
{
|
| 10 |
+
files: ['**/*.{js,jsx}'],
|
| 11 |
+
languageOptions: {
|
| 12 |
+
ecmaVersion: 2020,
|
| 13 |
+
globals: globals.browser,
|
| 14 |
+
parserOptions: {
|
| 15 |
+
ecmaVersion: 'latest',
|
| 16 |
+
ecmaFeatures: { jsx: true },
|
| 17 |
+
sourceType: 'module',
|
| 18 |
+
},
|
| 19 |
+
},
|
| 20 |
+
settings: { react: { version: '18.3' } },
|
| 21 |
+
plugins: {
|
| 22 |
+
react,
|
| 23 |
+
'react-hooks': reactHooks,
|
| 24 |
+
'react-refresh': reactRefresh,
|
| 25 |
+
},
|
| 26 |
+
rules: {
|
| 27 |
+
...js.configs.recommended.rules,
|
| 28 |
+
...react.configs.recommended.rules,
|
| 29 |
+
...react.configs['jsx-runtime'].rules,
|
| 30 |
+
...reactHooks.configs.recommended.rules,
|
| 31 |
+
'react/jsx-no-target-blank': 'off',
|
| 32 |
+
'react-refresh/only-export-components': [
|
| 33 |
+
'warn',
|
| 34 |
+
{ allowConstantExport: true },
|
| 35 |
+
],
|
| 36 |
+
},
|
| 37 |
+
},
|
| 38 |
+
]
|
FrontEnd/index.html
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 7 |
+
<title>Vite + React</title>
|
| 8 |
+
</head>
|
| 9 |
+
<body>
|
| 10 |
+
<div id="root"></div>
|
| 11 |
+
<script type="module" src="/src/main.jsx"></script>
|
| 12 |
+
</body>
|
| 13 |
+
</html>
|
FrontEnd/package-lock.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
FrontEnd/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "frontend",
|
| 3 |
+
"private": true,
|
| 4 |
+
"version": "0.0.0",
|
| 5 |
+
"type": "module",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"dev": "vite",
|
| 8 |
+
"build": "vite build",
|
| 9 |
+
"lint": "eslint .",
|
| 10 |
+
"preview": "vite preview"
|
| 11 |
+
},
|
| 12 |
+
"dependencies": {
|
| 13 |
+
"react": "^18.3.1",
|
| 14 |
+
"react-dom": "^18.3.1"
|
| 15 |
+
},
|
| 16 |
+
"devDependencies": {
|
| 17 |
+
"@eslint/js": "^9.17.0",
|
| 18 |
+
"@types/react": "^18.3.18",
|
| 19 |
+
"@types/react-dom": "^18.3.5",
|
| 20 |
+
"@vitejs/plugin-react": "^4.3.4",
|
| 21 |
+
"eslint": "^9.17.0",
|
| 22 |
+
"eslint-plugin-react": "^7.37.2",
|
| 23 |
+
"eslint-plugin-react-hooks": "^5.0.0",
|
| 24 |
+
"eslint-plugin-react-refresh": "^0.4.16",
|
| 25 |
+
"globals": "^15.14.0",
|
| 26 |
+
"vite": "^6.0.5"
|
| 27 |
+
}
|
| 28 |
+
}
|
FrontEnd/public/vite.svg
ADDED
|
|
Git LFS Details
|
FrontEnd/src/ChatBot.css
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* Reset CSS */
|
| 2 |
+
* {
|
| 3 |
+
margin: 0;
|
| 4 |
+
padding: 0;
|
| 5 |
+
box-sizing: border-box;
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
/* Container chính của chatbot */
|
| 9 |
+
.chatbot-container {
|
| 10 |
+
width: 66.66%; /* Chiếm 2/3 màn hình */
|
| 11 |
+
max-width: 800px; /* Giới hạn chiều rộng tối đa */
|
| 12 |
+
height: 80vh; /* Chiếm 80% chiều cao màn hình */
|
| 13 |
+
border: 1px solid #ccc;
|
| 14 |
+
border-radius: 10px;
|
| 15 |
+
display: flex;
|
| 16 |
+
flex-direction: column;
|
| 17 |
+
overflow: hidden;
|
| 18 |
+
font-family: Arial, sans-serif;
|
| 19 |
+
background-color: #f9f9f9;
|
| 20 |
+
margin: 0 auto; /* Căn giữa màn hình */
|
| 21 |
+
position: absolute;
|
| 22 |
+
top: 50%;
|
| 23 |
+
left: 50%;
|
| 24 |
+
transform: translate(-50%, -50%);
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
/* Header của chatbot */
|
| 28 |
+
.chat-header {
|
| 29 |
+
display: flex;
|
| 30 |
+
justify-content: space-between;
|
| 31 |
+
align-items: center;
|
| 32 |
+
padding: 10px;
|
| 33 |
+
background-color: #007bff;
|
| 34 |
+
color: white;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
.chat-header h2 {
|
| 38 |
+
font-size: 18px;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
.new-chat-btn {
|
| 42 |
+
background-color: #0056b3;
|
| 43 |
+
color: white;
|
| 44 |
+
border: none;
|
| 45 |
+
padding: 5px 10px;
|
| 46 |
+
border-radius: 5px;
|
| 47 |
+
cursor: pointer;
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
.new-chat-btn:hover {
|
| 51 |
+
background-color: #004080;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
/* Cửa sổ hiển thị tin nhắn */
|
| 55 |
+
.chat-window {
|
| 56 |
+
flex: 1;
|
| 57 |
+
padding: 10px;
|
| 58 |
+
overflow-y: auto;
|
| 59 |
+
background-color: #fff;
|
| 60 |
+
display: flex;
|
| 61 |
+
flex-direction: column; /* Đảm bảo các tin nhắn hiển thị theo chiều dọc */
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
/* Tin nhắn của người dùng và chatbot */
|
| 65 |
+
.message {
|
| 66 |
+
margin-bottom: 10px;
|
| 67 |
+
padding: 8px 12px;
|
| 68 |
+
border-radius: 10px;
|
| 69 |
+
max-width: 80%;
|
| 70 |
+
word-wrap: break-word; /* Đảm bảo chữ không tràn ra ngoài */
|
| 71 |
+
display: inline-block; /* Để ô tin nhắn co giãn theo nội dung */
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
.message.user {
|
| 75 |
+
background-color: #007bff;
|
| 76 |
+
color: white;
|
| 77 |
+
margin-left: auto;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
.message.bot {
|
| 81 |
+
background-color: #e1e1e1;
|
| 82 |
+
color: #333;
|
| 83 |
+
margin-right: auto;
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
/* Thanh nhập tin nhắn */
|
| 87 |
+
.input-container {
|
| 88 |
+
display: flex;
|
| 89 |
+
padding: 10px;
|
| 90 |
+
background-color: #f1f1f1;
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
.input-container input {
|
| 94 |
+
flex: 1;
|
| 95 |
+
padding: 8px;
|
| 96 |
+
border: 1px solid #ccc;
|
| 97 |
+
border-radius: 5px;
|
| 98 |
+
margin-right: 10px;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
.input-container button {
|
| 102 |
+
padding: 8px 12px;
|
| 103 |
+
background-color: #007bff;
|
| 104 |
+
color: white;
|
| 105 |
+
border: none;
|
| 106 |
+
border-radius: 5px;
|
| 107 |
+
cursor: pointer;
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
.input-container button:hover {
|
| 111 |
+
background-color: #0056b3;
|
| 112 |
+
}
|
FrontEnd/src/ChatBot.jsx
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState, useEffect, useRef } from 'react'; // Thêm useRef và useEffect
|
| 2 |
+
import './ChatBot.css';
|
| 3 |
+
|
| 4 |
+
const ChatBot = () => {
|
| 5 |
+
const [messages, setMessages] = useState([]);
|
| 6 |
+
const [userInput, setUserInput] = useState("");
|
| 7 |
+
const chatWindowRef = useRef(null); // Tham chiếu đến cửa sổ chat
|
| 8 |
+
|
| 9 |
+
// Tự động cuộn xuống tin nhắn mới nhất
|
| 10 |
+
useEffect(() => {
|
| 11 |
+
if (chatWindowRef.current) {
|
| 12 |
+
chatWindowRef.current.scrollTop = chatWindowRef.current.scrollHeight;
|
| 13 |
+
}
|
| 14 |
+
}, [messages]); // Kích hoạt mỗi khi messages thay đổi
|
| 15 |
+
|
| 16 |
+
const sendQueryToBackend = async (query) => {
|
| 17 |
+
try {
|
| 18 |
+
const response = await fetch('http://127.0.0.1:8000/process_query', {
|
| 19 |
+
method: 'POST',
|
| 20 |
+
headers: {
|
| 21 |
+
'Content-Type': 'application/json',
|
| 22 |
+
},
|
| 23 |
+
body: JSON.stringify({ query }),
|
| 24 |
+
});
|
| 25 |
+
|
| 26 |
+
if (!response.ok) {
|
| 27 |
+
throw new Error('Lỗi khi gửi yêu cầu');
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
const data = await response.json();
|
| 31 |
+
return data.response;
|
| 32 |
+
} catch (error) {
|
| 33 |
+
console.error('Lỗi:', error);
|
| 34 |
+
return "Đã xảy ra lỗi khi kết nối với ChatBot.";
|
| 35 |
+
}
|
| 36 |
+
};
|
| 37 |
+
|
| 38 |
+
const handleSend = async () => {
|
| 39 |
+
if (userInput.trim() === "") return;
|
| 40 |
+
|
| 41 |
+
// Thêm tin nhắn của người dùng vào danh sách tin nhắn
|
| 42 |
+
setMessages((prevMessages) => [
|
| 43 |
+
...prevMessages,
|
| 44 |
+
{ sender: "user", text: userInput },
|
| 45 |
+
]);
|
| 46 |
+
|
| 47 |
+
// Xóa input của người dùng (sau khi đã nhận được phản hồi)
|
| 48 |
+
setUserInput("");
|
| 49 |
+
|
| 50 |
+
// Gửi yêu cầu đến backend và nhận phản hồi
|
| 51 |
+
const botResponse = await sendQueryToBackend(userInput);
|
| 52 |
+
|
| 53 |
+
// Thêm phản hồi từ chatbot vào danh sách tin nhắn
|
| 54 |
+
setMessages((prevMessages) => [
|
| 55 |
+
...prevMessages,
|
| 56 |
+
{ sender: "bot", text: botResponse },
|
| 57 |
+
]);
|
| 58 |
+
|
| 59 |
+
};
|
| 60 |
+
|
| 61 |
+
const handleNewChat = () => {
|
| 62 |
+
setMessages([]); // Xóa tất cả tin nhắn
|
| 63 |
+
};
|
| 64 |
+
|
| 65 |
+
return (
|
| 66 |
+
<div className="chatbot-container">
|
| 67 |
+
<div className="chat-header">
|
| 68 |
+
<h2>ChatBot</h2>
|
| 69 |
+
<button className="new-chat-btn" onClick={handleNewChat}>
|
| 70 |
+
New Chat
|
| 71 |
+
</button>
|
| 72 |
+
</div>
|
| 73 |
+
|
| 74 |
+
{/* Thêm ref vào cửa sổ chat */}
|
| 75 |
+
<div className="chat-window" ref={chatWindowRef}>
|
| 76 |
+
{messages.map((message, index) => (
|
| 77 |
+
<div key={index} className={`message ${message.sender}`}>
|
| 78 |
+
<p>{message.text}</p>
|
| 79 |
+
</div>
|
| 80 |
+
))}
|
| 81 |
+
</div>
|
| 82 |
+
|
| 83 |
+
<div className="input-container">
|
| 84 |
+
<input
|
| 85 |
+
type="text"
|
| 86 |
+
value={userInput}
|
| 87 |
+
onChange={(e) => setUserInput(e.target.value)}
|
| 88 |
+
placeholder="Gõ câu hỏi của bạn..."
|
| 89 |
+
onKeyPress={(e) => e.key === "Enter" && handleSend()}
|
| 90 |
+
/>
|
| 91 |
+
<button onClick={handleSend}>Gửi</button>
|
| 92 |
+
</div>
|
| 93 |
+
</div>
|
| 94 |
+
);
|
| 95 |
+
};
|
| 96 |
+
|
| 97 |
+
export default ChatBot;
|
FrontEnd/src/assets/react.svg
ADDED
|
|
Git LFS Details
|
FrontEnd/src/index.css
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
body {
|
| 2 |
+
margin: 0;
|
| 3 |
+
font-family: Arial, sans-serif;
|
| 4 |
+
display: flex;
|
| 5 |
+
justify-content: center;
|
| 6 |
+
align-items: center;
|
| 7 |
+
height: 100vh;
|
| 8 |
+
background-color: #f0f0f0;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
#root {
|
| 12 |
+
width: 100%;
|
| 13 |
+
max-width: 1200px;
|
| 14 |
+
margin: 0 auto;
|
| 15 |
+
}
|
FrontEnd/src/main.jsx
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { StrictMode } from 'react';
|
| 2 |
+
import { createRoot } from 'react-dom/client';
|
| 3 |
+
import './index.css';
|
| 4 |
+
import ChatBot from './ChatBot.jsx';
|
| 5 |
+
|
| 6 |
+
createRoot(document.getElementById('root')).render(
|
| 7 |
+
<StrictMode>
|
| 8 |
+
<ChatBot />
|
| 9 |
+
</StrictMode>
|
| 10 |
+
);
|
FrontEnd/vite.config.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { defineConfig } from 'vite'
|
| 2 |
+
import react from '@vitejs/plugin-react'
|
| 3 |
+
|
| 4 |
+
// https://vite.dev/config/
|
| 5 |
+
export default defineConfig({
|
| 6 |
+
plugins: [react()],
|
| 7 |
+
})
|
package-lock.json
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "LawChatBot",
|
| 3 |
+
"lockfileVersion": 3,
|
| 4 |
+
"requires": true,
|
| 5 |
+
"packages": {}
|
| 6 |
+
}
|