SUMANA SUMANAKUL (ING)
commited on
Commit
·
8e5a9dd
1
Parent(s):
75e17a2
commit
Browse files- app.py +110 -63
- requirements.txt +26 -1
- utils/chat.py +281 -0
- utils/chat_prompts.py +193 -0
- utils/input_classifier.py +93 -0
- utils/reranker.py +46 -0
- utils/retriever.py +68 -0
app.py
CHANGED
@@ -1,64 +1,111 @@
|
|
|
|
|
|
|
|
1 |
import gradio as gr
|
2 |
-
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
):
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
""
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
)
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
os.environ["OTEL_TRACES_EXPORTER"] = "none"
|
3 |
+
|
4 |
import gradio as gr
|
5 |
+
import uuid
|
6 |
+
from utils.chat import ChatLaborLaw
|
7 |
+
|
8 |
+
|
9 |
+
# Function to initialize a new session and create chatbot instance for that session
|
10 |
+
async def initialize_session():
|
11 |
+
session_id = str(uuid.uuid4())[:8]
|
12 |
+
chatbot = ChatLaborLaw()
|
13 |
+
# chatbot = Chat("gemini-2.0-flash")
|
14 |
+
history = []
|
15 |
+
return "", session_id, chatbot, history
|
16 |
+
|
17 |
+
|
18 |
+
# Function to handle user input and chatbot response
|
19 |
+
async def chat_function(prompt, history, session_id, chatbot):
|
20 |
+
if chatbot is None:
|
21 |
+
return history, "", session_id, chatbot # Skip if chatbot not ready
|
22 |
+
|
23 |
+
# Append the user's input to the message history
|
24 |
+
history.append({"role": "user", "content": prompt})
|
25 |
+
|
26 |
+
# Get the response from the chatbot
|
27 |
+
response = await chatbot.chat(prompt) # ใช้ await ได้แล้ว
|
28 |
+
|
29 |
+
# Append the assistant's response to the message history
|
30 |
+
history.append({"role": "assistant", "content": response})
|
31 |
+
|
32 |
+
return history, "", session_id, chatbot
|
33 |
+
|
34 |
+
|
35 |
+
# Function to save feedback with chat history
|
36 |
+
async def send_feedback(feedback, history, session_id, chatbot):
|
37 |
+
os.makedirs("app/feedback", exist_ok=True)
|
38 |
+
filename = f"app/feedback/feedback_{session_id}.txt"
|
39 |
+
with open(filename, "a", encoding="utf-8") as f:
|
40 |
+
f.write("=== Feedback Received ===\n")
|
41 |
+
f.write(f"Session ID: {session_id}\n")
|
42 |
+
f.write(f"Feedback: {feedback}\n")
|
43 |
+
f.write("Chat History:\n")
|
44 |
+
for msg in history:
|
45 |
+
f.write(f"{msg['role']}: {msg['content']}\n")
|
46 |
+
f.write("\n--------------------------\n\n")
|
47 |
+
return "" # Clear feedback input
|
48 |
+
|
49 |
+
|
50 |
+
# Create the Gradio interface
|
51 |
+
with gr.Blocks(theme=gr.themes.Soft(primary_hue="amber")) as demo:
|
52 |
+
gr.Markdown("# สอบถามเรื่องกฎหมายแรงงาน")
|
53 |
+
|
54 |
+
# Initialize State
|
55 |
+
session_state = gr.State()
|
56 |
+
chatbot_instance = gr.State()
|
57 |
+
chatbot_history = gr.State([])
|
58 |
+
|
59 |
+
# Chat UI
|
60 |
+
chatbot_interface = gr.Chatbot(type="messages", label="Chat History")
|
61 |
+
user_input = gr.Textbox(placeholder="Type your message here...", elem_id="user_input", lines=1)
|
62 |
+
|
63 |
+
submit_button = gr.Button("Send")
|
64 |
+
clear_button = gr.Button("Delete Chat History")
|
65 |
+
|
66 |
+
# Submit actions
|
67 |
+
submit_button.click(
|
68 |
+
fn=chat_function,
|
69 |
+
inputs=[user_input, chatbot_history, session_state, chatbot_instance],
|
70 |
+
outputs=[chatbot_interface, user_input, session_state, chatbot_instance]
|
71 |
+
)
|
72 |
+
|
73 |
+
user_input.submit(
|
74 |
+
fn=chat_function,
|
75 |
+
inputs=[user_input, chatbot_history, session_state, chatbot_instance],
|
76 |
+
outputs=[chatbot_interface, user_input, session_state, chatbot_instance]
|
77 |
+
)
|
78 |
+
|
79 |
+
# # Clear history
|
80 |
+
# clear_button.click(lambda: [], outputs=chatbot_interface)
|
81 |
+
clear_button.click(
|
82 |
+
fn=initialize_session,
|
83 |
+
inputs=[],
|
84 |
+
outputs=[user_input, session_state, chatbot_instance, chatbot_history]
|
85 |
+
).then(
|
86 |
+
fn=lambda: gr.update(value=[]),
|
87 |
+
inputs=[],
|
88 |
+
outputs=chatbot_interface
|
89 |
+
)
|
90 |
+
|
91 |
+
|
92 |
+
# Feedback section
|
93 |
+
with gr.Row():
|
94 |
+
feedback_input = gr.Textbox(placeholder="Send us feedback...", label="Feedback")
|
95 |
+
send_feedback_button = gr.Button("Send Feedback")
|
96 |
+
|
97 |
+
send_feedback_button.click(
|
98 |
+
fn=send_feedback,
|
99 |
+
inputs=[feedback_input, chatbot_history, session_state, chatbot_instance],
|
100 |
+
outputs=[feedback_input]
|
101 |
+
)
|
102 |
+
|
103 |
+
# Initialize session on load
|
104 |
+
demo.load(
|
105 |
+
fn=initialize_session,
|
106 |
+
inputs=[],
|
107 |
+
outputs=[user_input, session_state, chatbot_instance, chatbot_history]
|
108 |
+
)
|
109 |
+
|
110 |
+
# Launch
|
111 |
+
demo.launch(share=True)
|
requirements.txt
CHANGED
@@ -1 +1,26 @@
|
|
1 |
-
huggingface_hub==0.25.2
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
huggingface_hub==0.25.2
|
2 |
+
langchain==0.3.26
|
3 |
+
langchain-community==0.3.26
|
4 |
+
langchain-core==0.3.66
|
5 |
+
langchain-google-genai==2.1.6
|
6 |
+
langchain-huggingface==0.3.0
|
7 |
+
langchain-mongodb==0.6.2
|
8 |
+
langchain-openai==0.3.27
|
9 |
+
langchain-text-splitters==0.3.8
|
10 |
+
langfuse==3.1.0
|
11 |
+
langsmith==0.4.4
|
12 |
+
numpy==2.3.1
|
13 |
+
openai==1.93.0
|
14 |
+
openpyxl==3.1.5
|
15 |
+
pymongo==4.13.2
|
16 |
+
pythainlp==5.1.2
|
17 |
+
python-dotenv==1.1.1
|
18 |
+
regex==2024.11.6
|
19 |
+
sentence-transformers==4.1.0
|
20 |
+
transformers==4.53.0
|
21 |
+
google-ai-generativelanguage==0.6.18
|
22 |
+
google-api-core==2.25.1
|
23 |
+
google-api-python-client==2.174.0
|
24 |
+
google-auth==2.40.3
|
25 |
+
google-auth-httplib2==0.2.0
|
26 |
+
google-generativeai==0.8.5
|
utils/chat.py
ADDED
@@ -0,0 +1,281 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os, re
|
2 |
+
# os.environ["OTEL_TRACES_EXPORTER"] = "none"
|
3 |
+
os.environ["OTEL_SDK_DISABLED"] = "true"
|
4 |
+
|
5 |
+
import uuid
|
6 |
+
from dotenv import load_dotenv
|
7 |
+
|
8 |
+
from utils.chat_prompts import RAG_CHAT_PROMPT, NON_RAG_PROMPT
|
9 |
+
from utils.reranker import RerankRetriever
|
10 |
+
from utils.input_classifier import classify_input_type
|
11 |
+
|
12 |
+
from langchain_openai import ChatOpenAI
|
13 |
+
from langchain_core.messages import HumanMessage, AIMessage
|
14 |
+
from langchain_google_genai import ChatGoogleGenerativeAI
|
15 |
+
|
16 |
+
from pymongo import MongoClient
|
17 |
+
|
18 |
+
from langfuse.langchain import CallbackHandler
|
19 |
+
from langfuse import observe
|
20 |
+
|
21 |
+
load_dotenv()
|
22 |
+
|
23 |
+
# MongoDB configurations
|
24 |
+
mongo_username = os.environ.get('MONGO_USERNAME')
|
25 |
+
mongo_password = os.environ.get('MONGO_PASSWORD')
|
26 |
+
mongo_database = os.environ.get('MONGO_DATABASE')
|
27 |
+
mongo_connection_str = os.environ.get('MONGO_CONNECTION_STRING')
|
28 |
+
mongo_collection_name = os.environ.get('MONGO_COLLECTION')
|
29 |
+
|
30 |
+
class ChatLaborLaw:
|
31 |
+
def __init__(self, model_name_llm="jai-chat-1-3-2", temperature=0):
|
32 |
+
self.session_id = str(uuid.uuid4())[:8]
|
33 |
+
|
34 |
+
# ----- Langfuse -----
|
35 |
+
self.langfuse_handler = CallbackHandler(
|
36 |
+
)
|
37 |
+
|
38 |
+
self.history = [] # Store Langchain Message objects
|
39 |
+
|
40 |
+
self.model_name_llm = model_name_llm
|
41 |
+
self.retriever = RerankRetriever()
|
42 |
+
|
43 |
+
self.client = MongoClient(mongo_connection_str)
|
44 |
+
self.db = self.client[mongo_database]
|
45 |
+
self.collection = self.db[mongo_collection_name]
|
46 |
+
|
47 |
+
# --- LLM Initialization ---
|
48 |
+
if model_name_llm == "jai-chat-1-3-2":
|
49 |
+
self.llm_main = ChatOpenAI(
|
50 |
+
model=model_name_llm,
|
51 |
+
api_key=os.getenv("JAI_API_KEY"),
|
52 |
+
base_url=os.getenv("CHAT_BASE_URL"),
|
53 |
+
temperature=temperature,
|
54 |
+
max_tokens=2048,
|
55 |
+
max_retries=2,
|
56 |
+
seed=13
|
57 |
+
)
|
58 |
+
|
59 |
+
elif model_name_llm == "gemini-2.0-flash":
|
60 |
+
GEMINI_API_KEY = os.getenv("GOOGLE_API_KEY")
|
61 |
+
if not GEMINI_API_KEY:
|
62 |
+
raise ValueError("GOOGLE_API_KEY (for Gemini) not found in environment variables.")
|
63 |
+
|
64 |
+
common_gemini_config = {
|
65 |
+
"google_api_key": GEMINI_API_KEY,
|
66 |
+
"temperature": temperature,
|
67 |
+
"max_output_tokens": 2048,
|
68 |
+
"convert_system_message_to_human": True,
|
69 |
+
}
|
70 |
+
self.llm_main = ChatGoogleGenerativeAI(
|
71 |
+
model="gemini-2.0-flash",
|
72 |
+
**common_gemini_config)
|
73 |
+
|
74 |
+
else:
|
75 |
+
raise ValueError(f"Unsupported LLM model '{model_name_llm}'.")
|
76 |
+
|
77 |
+
self.history = [] # Store Langchain Message objects
|
78 |
+
|
79 |
+
|
80 |
+
# ----- Context Retrieval -----
|
81 |
+
@observe(name='main_context')
|
82 |
+
def get_main_context(self, user_query, **kwargs):
|
83 |
+
# note ต้อง get ทุกครั้งไหม กรณีอะไรที่จะเปลี่ยน
|
84 |
+
# note ต้องมี classifier มาเพื่อตัดสิน filters -- * ถ้ามีระบุเวลา ก็ต้องไปคำนวน แล้วเอาจาก official_version แทน
|
85 |
+
compression_retriever = self.retriever.get_compression_retriever(**kwargs)
|
86 |
+
main_comtext_docs = compression_retriever.invoke(user_query)
|
87 |
+
return main_comtext_docs
|
88 |
+
|
89 |
+
@observe(name='ref_context')
|
90 |
+
def get_ref_context(self, main_context_docs):
|
91 |
+
"""
|
92 |
+
ค้นหา Context ของมาตราที่ถูกอ้างอิงจาก MongoDB
|
93 |
+
โดยใช้ $in operator เพื่อประสิทธิภาพสูงสุด
|
94 |
+
"""
|
95 |
+
all_reference_docs = []
|
96 |
+
|
97 |
+
for context in main_context_docs:
|
98 |
+
references_list = context.metadata.get('references', [])
|
99 |
+
|
100 |
+
if not isinstance(references_list, list) or not references_list:
|
101 |
+
continue # ข้ามไป context ถัดไปถ้าไม่มีอ้างอิง
|
102 |
+
|
103 |
+
ref_numbers = [
|
104 |
+
ref_str.replace("มาตรา", "").strip()
|
105 |
+
for ref_str in references_list
|
106 |
+
]
|
107 |
+
|
108 |
+
# query $in : มาตรานั้นๆ
|
109 |
+
mongo_query = {
|
110 |
+
"law_type": "summary",
|
111 |
+
"section_number": {"$in": ref_numbers}
|
112 |
+
}
|
113 |
+
|
114 |
+
projection = {
|
115 |
+
"_id": 1,
|
116 |
+
"text": 1,
|
117 |
+
"document_type": 1,
|
118 |
+
"law_type": 1,
|
119 |
+
"law_name": 1,
|
120 |
+
# "publication_date": 1,
|
121 |
+
# "effective_date": 1,
|
122 |
+
# "publication_date_utc": 1,
|
123 |
+
# "effective_date_utc": 1,
|
124 |
+
# "royal_gazette_volume": 1,
|
125 |
+
# "royal_gazette_no": 1,
|
126 |
+
# "royal_gazette_page": 1,
|
127 |
+
"chunk_type": 1,
|
128 |
+
"section_number": 1
|
129 |
+
}
|
130 |
+
|
131 |
+
results = self.collection.find(mongo_query, projection)
|
132 |
+
all_reference_docs.extend(list(results))
|
133 |
+
|
134 |
+
# ลบอันที่ซ้ำ
|
135 |
+
ref_docs_by_id = {}
|
136 |
+
for doc in all_reference_docs:
|
137 |
+
ref_docs_by_id[doc["_id"]] = doc # ถ้ามี _id ซ้ำกัน จะ overwrite
|
138 |
+
|
139 |
+
return list(ref_docs_by_id.values())
|
140 |
+
|
141 |
+
|
142 |
+
# handle main context
|
143 |
+
# ต้องเอา law_name, section_number (มาตรา), publication_date(ถ้ามี), effective_date(ถ้ามี)
|
144 |
+
def format_main_context(self, list_of_documents):
|
145 |
+
"""
|
146 |
+
input: list of Document (Langchain)
|
147 |
+
output: text --> to forward to prompt
|
148 |
+
"""
|
149 |
+
formatted_docs = []
|
150 |
+
|
151 |
+
for i, doc in enumerate(list_of_documents):
|
152 |
+
law_name = doc.metadata.get('law_name', '-')
|
153 |
+
section_number = doc.metadata.get('section_number', '-')
|
154 |
+
publication_date = doc.metadata.get('publication_date', '-') # ไม่ได้มีทุกอัน
|
155 |
+
effective_date = doc.metadata.get('effective_date', '-') # ไม่ได้มีทุกอัน
|
156 |
+
content = doc.page_content
|
157 |
+
|
158 |
+
formatted = "\n".join([
|
159 |
+
f"Doc{i}",
|
160 |
+
f"{law_name}",
|
161 |
+
f"มาตรา\t{section_number}",
|
162 |
+
content,
|
163 |
+
f"ประกาศ\t{publication_date}",
|
164 |
+
f"เริ่มใช้\t{effective_date}"
|
165 |
+
])
|
166 |
+
|
167 |
+
formatted_docs.append(formatted)
|
168 |
+
|
169 |
+
return "\n\n".join(formatted_docs)
|
170 |
+
|
171 |
+
|
172 |
+
def format_ref_context(self, list_of_docs):
|
173 |
+
formatted_ref_docs = []
|
174 |
+
|
175 |
+
for i, doc in enumerate(list_of_docs):
|
176 |
+
law_name = doc.get('law_name', '-')
|
177 |
+
section_number = doc.get('section_number', '-')
|
178 |
+
content = doc.get('text', '-')
|
179 |
+
|
180 |
+
formatted = "\n".join([
|
181 |
+
f"{law_name}",
|
182 |
+
f"มาตรา\t{section_number}",
|
183 |
+
content,
|
184 |
+
])
|
185 |
+
formatted_ref_docs.append(formatted)
|
186 |
+
|
187 |
+
return "\n\n".join(formatted_ref_docs)
|
188 |
+
|
189 |
+
|
190 |
+
# ----- Chat! -----
|
191 |
+
|
192 |
+
# History
|
193 |
+
def append_history(self, message: [HumanMessage, AIMessage]):
|
194 |
+
self.history.append(message)
|
195 |
+
|
196 |
+
def get_formatted_history_for_llm(self, n_turns: int = 3) -> list:
|
197 |
+
"""Returns the last n_turns of history as a list of Message objects."""
|
198 |
+
return self.history[-(n_turns * 2):]
|
199 |
+
|
200 |
+
|
201 |
+
# Classify
|
202 |
+
@observe(name='classify_input_type')
|
203 |
+
def classify_input(self, user_input: str) -> str:
|
204 |
+
history_content_list = [msg.content for msg in self.history] # เอาแค่ข้อความมา
|
205 |
+
return classify_input_type(user_input, history=history_content_list)
|
206 |
+
|
207 |
+
|
208 |
+
# Chat
|
209 |
+
@observe(name="chat_flow")
|
210 |
+
async def chat(self, user_input: str) -> str:
|
211 |
+
history_before_current_input = self.history[:]
|
212 |
+
self.append_history(HumanMessage(content=user_input))
|
213 |
+
|
214 |
+
try:
|
215 |
+
input_type = self.classify_input(user_input)
|
216 |
+
except Exception as e:
|
217 |
+
print(f"Error classifying input type: {e}. Defaulting to Non-RAG.")
|
218 |
+
input_type = "Non-RAG"
|
219 |
+
|
220 |
+
ai_response_content = ""
|
221 |
+
if input_type == "RAG":
|
222 |
+
# print("[RAG FLOW]")
|
223 |
+
ai_response_content = await self.call_rag(user_input) #, history_before_current_input)
|
224 |
+
else:
|
225 |
+
# print(f"[{input_type} FLOW (Treated as NON-RAG)]")
|
226 |
+
ai_response_content = await self.call_non_rag(user_input)
|
227 |
+
|
228 |
+
self.append_history(AIMessage(content=ai_response_content))
|
229 |
+
|
230 |
+
# print(f"AI:::: {ai_response_content}")
|
231 |
+
return ai_response_content
|
232 |
+
|
233 |
+
|
234 |
+
@observe(name='rag_flow')
|
235 |
+
async def call_rag(self, user_input: str) -> str:
|
236 |
+
|
237 |
+
# main context
|
238 |
+
context_docs = self.get_main_context(user_input, law_type="summary") # chunk_type='section'
|
239 |
+
# print(context_docs)
|
240 |
+
main_context_str = self.format_main_context(context_docs)
|
241 |
+
|
242 |
+
# ref context
|
243 |
+
ref_context_docs = self.get_ref_context(context_docs)
|
244 |
+
try:
|
245 |
+
ref_context_str = self.format_ref_context(ref_context_docs)
|
246 |
+
except:
|
247 |
+
ref_context_str = "-"
|
248 |
+
|
249 |
+
history_for_llm_prompt = self.get_formatted_history_for_llm(n_turns=3)
|
250 |
+
|
251 |
+
rag_input_data = {
|
252 |
+
"question": user_input,
|
253 |
+
"main_context": main_context_str,
|
254 |
+
"ref_context": ref_context_str,
|
255 |
+
"history": history_for_llm_prompt
|
256 |
+
}
|
257 |
+
|
258 |
+
try:
|
259 |
+
prompt_messages = RAG_CHAT_PROMPT.format_messages(**rag_input_data)
|
260 |
+
response = await self.llm_main.ainvoke(prompt_messages, config={"callbacks": [self.langfuse_handler]})
|
261 |
+
responsestring = response.content
|
262 |
+
clean_response = re.sub(r"<[^>]+>", "", responsestring)
|
263 |
+
clean_response = re.sub(r"#+", "", clean_response)
|
264 |
+
clean_response = clean_response.strip()
|
265 |
+
# return response.content.strip()
|
266 |
+
return clean_response
|
267 |
+
|
268 |
+
except Exception as e:
|
269 |
+
print(f"Error during RAG LLM call: {e}")
|
270 |
+
return "Sorry, I encountered an error while generating the response."
|
271 |
+
|
272 |
+
@observe(name='non_rag_flow')
|
273 |
+
async def call_non_rag(self, user_input: str) -> str:
|
274 |
+
prompt_messages = NON_RAG_PROMPT.format(user_input=user_input)
|
275 |
+
response = await self.llm_main.ainvoke(prompt_messages, config={"callbacks": [self.langfuse_handler]})
|
276 |
+
|
277 |
+
# ป้องกัน content เป็น None
|
278 |
+
if not response or not response.content:
|
279 |
+
return "ขออภัย ระบบไม่สามารถตอบคำถามได้ในขณะนี้"
|
280 |
+
|
281 |
+
return response.content.strip()
|
utils/chat_prompts.py
ADDED
@@ -0,0 +1,193 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder, HumanMessagePromptTemplate, SystemMessagePromptTemplate
|
2 |
+
|
3 |
+
RAG_CHAT_PROMPT = ChatPromptTemplate.from_messages(
|
4 |
+
[
|
5 |
+
SystemMessagePromptTemplate.from_template(
|
6 |
+
"""
|
7 |
+
**Persona and Role:**
|
8 |
+
You are a specialized AI assistant acting as a Thai legal expert. Your persona is helpful, precise, and authoritative. Your primary goal is to provide accurate and helpful answers to legal questions based exclusively on the provided Thai legal context.
|
9 |
+
|
10 |
+
**Core Instructions:**
|
11 |
+
|
12 |
+
1. Absolute Rules (Non-negotiable):
|
13 |
+
• Language Constraint: You MUST respond in Thai only. Under no circumstances should you use English or any other language in your response.
|
14 |
+
• Strict Grounding: Your answer must be derived strictly from the provided context. If the context does not contain the information to answer the user's question, you must state that you cannot answer based on the information given (in Thai). Do not invent, infer, or use any external knowledge.
|
15 |
+
|
16 |
+
2. Context Handling and Reasoning:
|
17 |
+
• Context Structure: The provided context will be divided into two types: Main Context and Ref Context.
|
18 |
+
- Main Context: Contains the primary legal articles retrieved directly for the user's query.
|
19 |
+
- Ref Context: Contains supporting legal articles that are referenced by name (e.g., 'ตามมาตรา ๑๑๘') within the Main Context.
|
20 |
+
• Using Ref Context: You must intelligently decide whether the Ref Context is necessary to formulate a complete and accurate answer. Often, it will be essential for a comprehensive explanation.
|
21 |
+
• Interpreting Internal References (<วรรค X>):
|
22 |
+
- The context may contain markers like <วรรค X> (meaning Paragraph X). You MUST NOT include these markers <> in your final output.
|
23 |
+
- However, you must understand their meaning to correctly interpret the law. For example, if a paragraph refers to "ตามวรรคหนึ่ง" (according to paragraph one), you must correctly resolve this reference to the content of the first paragraph of that same article.
|
24 |
+
• Legal Terminology: When constructing your answer, use the formal legal terminology found within the provided context. Do not oversimplify the language into colloquial Thai.
|
25 |
+
|
26 |
+
3. Output Formatting and Structure:
|
27 |
+
• Clarity: Structure your answers clearly. Use bullet points, numbered lists, or distinct sections (e.g., separating different cases or scenarios) to make the information easy to understand.
|
28 |
+
• Citations: At the end of your response, you MUST provide a summary of the legal sources you used. List all the specific articles (e.g., มาตรา ๑๑๘, มาตรา ๑๒๑) that you referenced to build your answer.
|
29 |
+
|
30 |
+
|
31 |
+
**Example of Excellence**
|
32 |
+
This is an example of an ideal interaction to guide your behavior.
|
33 |
+
User Query:
|
34 |
+
"ในกรณีที่นายจ้างเลิกจ้างลูกจ้างแล้วลูกจ้างนั้นทำงานติดต่อกันเกิน 6 ปีขึ้นไป นายจ้างจะต้องจ่ายค่าชดเชยเท่าไหร่"
|
35 |
+
|
36 |
+
Provided Context:
|
37 |
+
|
38 |
+
Main Context: "### มาตรา ๑๒๒
|
39 |
+
ในกรณีที่นายจ้างเลิกจ้างลูกจ้างตามมาตรา ๑๒๑ และลูกจ้างนั้นทำงานติดต่อกันเกินหกปีขึ้นไป ให้นายจ้างจ่ายค่าชดเชยพิเศษเพิ่มขึ้นจากค่าชดเชยตามมาตรา ๑๑๘ ไม่น้อยกว่าค่าจ้างอัตราสุดท้ายสิบห้าวันต่อการทำงานครบหนึ่งปี หรือไม่น้อยกว่าค่าจ้างของการทำงานสิบห้าวันสุดท้ายต่อการทำงานครบหนึ่งปีสำหรับลูกจ้างซึ่งได้รับค่าจ้างตามผลงานโดยคำนวณเป็นหน่วย แต่ค่าชดเชยตามมาตรานี้รวมแล้วต้องไม่เกินค่าจ้างอัตราสุดท้ายสามร้อยหกสิบวัน หรือไม่เกินค่าจ้างของการทำงานสามร้อยหกสิบวันสุดท้ายสำหรับลูกจ้างซึ่งได้รับค่าจ้างตามผลงานโดยคำนวณเป็นหน่วย
|
40 |
+
"
|
41 |
+
|
42 |
+
Ref Context: "### มาตรา ๑๒๑
|
43 |
+
ในกรณีที่นายจ้างจะเลิกจ้างลูกจ้างเพราะเหตุที่นายจ้างปรับปรุงหน่วยงาน กระบวนการ��ลิต การจำหน่าย หรือการบริการอันเนื่องมาจากการนำเครื่องจักรมาใช้หรือเปลี่ยนแปลงเครื่องจักรหรือเทคโนโลยี ซึ่งเป็นเหตุให้ต้องลดจำนวนลูกจ้าง ห้ามมิให้นำมาตรา ๑๗ วรรคสอง มาใช้บังคับ และให้นายจ้างแจ้งวันที่จะเลิกจ้าง เหตุผลของการเลิกจ้างและรายชื่อลูกจ้างต่อพนักงานตรวจแรงงาน และลูกจ้างที่จะเลิกจ้างทราบล่วงหน้าไม่น้อยกว่าหกสิบวันก่อนวันที่จะเลิกจ้าง
|
44 |
+
ในกรณีที่นายจ้างไม่แจ้งให้ลูกจ้างที่จะเลิกจ้างทราบล่วงหน้า หรือแจ้งล่วงหน้าน้อยกว่าระยะเวลาที่กำหนดตามวรรคหนึ่ง นอกจากจะได้รับค่าชดเชยตามมาตรา ๑๑๘ แล้ว ให้นายจ้างจ่ายค่าชดเชยพิเศษแทนการบอกกล่าวล่วงหน้าเท่ากับค่าจ้างอัตราสุดท้ายหกสิบวัน หรือเท่ากับค่าจ้างของการทำงานหกสิบวันสุดท้ายสำหรับลูกจ้างซึ่งได้รับค่าจ้างตามผลงานโดยคำนวณเป็นหน่วยด้วย
|
45 |
+
ในกรณีที่มีการจ่ายค่าชดเชยพิเศษแทนการบอกกล่าวล่วงหน้าตามวรรคสองแล้ว ให้ถือว่านายจ้างได้จ่ายสินจ้างแทนการบอกกล่าวล่วงหน้าตามประมวลกฎหมายแพ่งและพาณิชย์ด้วย
|
46 |
+
|
47 |
+
### มาตรา ๑๑๘
|
48 |
+
ให้นายจ้างจ่ายค่าชดเชยให้แก่ลูกจ้างซึ่งเลิกจ้างดังต่อไปนี้
|
49 |
+
(๑) ลูกจ้างซึ่งทำงานติดต่อกันครบหนึ่งร้อยยี่สิบวัน แต่ไม่ครบหนึ่งปี ให้จ่ายไม่น้อยกว่าค่าจ้างอัตราสุดท้ายสามสิบวัน หรือไม่น้อยกว่าค่าจ้างของการทำงานสามสิบวันสุดท้ายสำหรับลูกจ้างซึ่งได้รับค่าจ้างตามผลงานโดยคำนวณเป็นหน่วย
|
50 |
+
(๒) ลูกจ้างซึ่งทำงานติดต่อกันครบหนึ่งปี แต่ไม่ครบสามปี ให้จ่ายไม่น้อยกว่าค่าจ้างอัตราสุดท้ายเก้าสิบวัน หรือไม่น้อยกว่าค่าจ้างของการทำงานเก้าสิบวันสุดท้ายสำหรับลูกจ้างซึ่งได้รับค่าจ้างตามผลงานโดยคำนวณเป็นหน่วย
|
51 |
+
(๓) ลูกจ้างซึ่งทำงานติดต่อกันครบสามปี แต่ไม่ครบหกปี ให้จ่ายไม่น้อยกว่าค่าจ้างอัตราสุดท้ายหนึ่งร้อยแปดสิบวัน หรือไม่น้อยกว่าค่าจ้างของการทำงานหนึ่งร้อยแปดสิบวันสุดท้ายสำหรับลูกจ้างซึ่งได้รับค่าจ้างตามผลงานโดยคำนวณเป็นหน่วย
|
52 |
+
(๔) ลูกจ้างซึ่งทำงานติดต่อกันครบหกปี แต่ไม่ครบสิบปี ให้จ่ายไม่น้อยกว่าค่าจ้างอัตราสุดท้ายสองร้อยสี่สิบวัน หรือไม่น้อยกว่าค่าจ้างของการทำงานสองร้อยสี่สิบวันสุดท้ายสำหรับลูกจ้างซึ่งได้รับค่าจ้างตามผลงานโดยคำนวณเป็นหน่วย
|
53 |
+
(๕) ลูกจ้างซึ่งทำงานติดต่อกันครบสิบปี แต่ไม่ครบยี่สิบปี ให้จ่ายไม่น้อยกว่าค่าจ้างอัตราสุดท้ายสามร้อยวัน หรือไม่น้อยกว่าค่าจ้างของการทำงานสามร้อยวันสุดท้ายสำหรับลูกจ้างซึ่งได้รับค่าจ้างตามผลงานโดยคำนวณเป็นหน่วย
|
54 |
+
(๖) ลูกจ้างซึ่งทำงานติดต่อกันครบยี่สิบปีขึ้นไป ให้จ่ายไม่น้อยกว่าค่าจ้างอัตราสุดท้ายสี่ร้อยวัน หรือไม่น้อยกว่าค่าจ้างของการทำงานสี่ร้อยวันสุดท้ายสำหรับลูกจ้างซึ่งได้รับค่าจ้างตามผลงานโดยคำนวณเป็นหน่วย
|
55 |
+
การเลิกจ้างตามมาตรานี้ หมายความว่า การกระทำใดที่นายจ้างไม่ให้ลูกจ้างทำงานต่อไปและไม่จ่ายค่าจ้างให้ ไม่ว่าจะเป็นเพราะเหตุสิ้นสุดสัญญาจ้างหรือเหตุอื่นใด และหมายความรวมถึงกรณีที่ลูกจ้างไม่ได้"
|
56 |
+
|
57 |
+
Ideal Response (Your output should look like this): "ตามข้อมูลจากพระราชบัญญัติคุ้มครองแรงงาน กรณีที่นายจ้างเลิกจ้างลูกจ้างซึ่งทำงานติดต่อกันเกิน 6 ปีขึ้นไป จำนวนค่าชดเชยที่จะได้รับจะขึ้นอยู่กับ "สาเหตุ" ของการเลิกจ้าง ซึ่งสามารถแบ่งได้เป็น 2 กรณีหลัก ดังนี้ครับ
|
58 |
+
|
59 |
+
**กรณีที่ 1: การเลิกจ้างทั่วไป (ที่ไม่ได้เกิดจากเหตุตามมาตรา 121)**
|
60 |
+
หากเป็นการเลิกจ้างทั่วไปที่ไม่ได้เกิดจากการปรับปรุงองค์กรโดยใช้เครื่องจักรหรือเทคโนโลยี นายจ้างจะต้องจ่ายค่าชดเชยตามปกติ ตามอายุงานของลูกจ้าง ซึ่งกำหนดไว้ในมาตรา 118 ดังนี้:
|
61 |
+
* ทำงานครบ 6 ปี แต่ไม่ครบ 10 ปี: ได้รับค่าชดเชยไม่น้อยกว่าค่าจ้างอัตราสุดท้าย 240 วัน
|
62 |
+
* ทำงานครบ 10 ปี แต่ไม่ครบ 20 ปี: ได้รับค่าชดเชยไม่น้อยกว่าค่าจ้างอัตราสุดท้าย 300 วัน
|
63 |
+
* ทำงานครบ 20 ปีขึ้นไป: ได้รับค่าชดเชยไม่น้อยกว่าค่าจ้างอัตราสุดท้าย 400 วัน
|
64 |
+
|
65 |
+
**กรณีที่ 2: การเลิกจ้างเนื่องจากการปรับปรุงองค์กรโดยใช้เทคโนโลยี (ตามมาตรา 121)**
|
66 |
+
หากนายจ้างเลิกจ้างเพราะเหตุปรับปรุงหน่วยงาน กระบวนการผลิต การจำหน่าย หรือการบริการ อันเนื่องมาจากการนำเครื่องจักรมาใช้หรือเปลี่ยนแปลงเทคโนโลยี ซึ่งทำให้ต้องลดจำนวนลูกจ้าง ลูกจ้างจะได้รับค่าชดเชย 2 ส่วนประกอบกัน คือ:
|
67 |
+
1. **ค่าชดเชยตามปกติ (ตามมาตรา 118):**
|
68 |
+
ลูกจ้างจะได้รับค่าชดเชยตามอายุงานเช่นเดียวกับกรณีที่ 1 (คือ 240, 300 หรือ 400 วัน ขึ้นอยู่กับอายุงาน)
|
69 |
+
2. **บวกกับ ค่าชดเชยพิเศษ (ตามมาตรา 122):**
|
70 |
+
นายจ้างต้องจ่ายค่าชดเชยพิเศษเพิ่มขึ้น ในอัตราไม่น้อยกว่าค่าจ้างอัตราสุดท้าย 15 วัน ต่อการทำงานครบ 1 ปี แต่ค่าชดเชยพิเศษนี้เมื่อรวมกันแล้วจะต้องไม่เกินค่าจ้างอัตราสุดท้าย 360 วัน
|
71 |
+
|
72 |
+
**สรุป**
|
73 |
+
จำนวนค่าชดเชยสำหรับลูกจ้างที่ทำงานเกิน 6 ปี จะแตกต่างกันอย่างมีนัยสำคัญขึ้นอยู่กับเหตุผลของการเลิกจ้าง หากเป็นเพราะการนำเทคโนโลยีมาใช้ตามมาตรา 121 ลูกจ้างจะได้รับค่าชดเชยปกติบวกด้วยค่าชดเชยพิเศษ ซึ่งจะทำให้ได้รับเงินชดเชยในจำนวนที่สูงกว่าการเลิกจ้างทั่วไป
|
74 |
+
|
75 |
+
**ข้อควรทราบเพิ่มเติม:** กรณีเลิกจ้างตามมาตรา 121 นายจ้างมีหน้าที่ต้องแจ้งล่วงหน้าไม่น้อยกว่า 60 วัน หากไม่แจ้งหรือแจ้งน้อยกว่ากำหนด จะต้องจ่ายค่าชดเชยพิเศษแทนการบอกกล่าวล่วงหน้าเท่ากับค่าจ้าง 60 วัน เพิ่มเติมจากค่าชดเชยทั้งหมดด้วย
|
76 |
+
|
77 |
+
**แหล่งข้อมูลอ้างอิง:**
|
78 |
+
* พระราชบัญญัติคุ้มครองแรงงาน
|
79 |
+
* มาตรา ๑๑๘ (ค่าชดเชยกรณีเลิกจ้างทั่วไป)
|
80 |
+
* มาตรา ๑๒๑ (เหตุเลิกจ้างเนื่องจากการนำเทคโนโลยีมาใช้ และค่าชดเชยพิเศษแทนการบอกกล่าวล่วงหน้า)
|
81 |
+
* มาตรา ๑๒๒ (ค่าชดเชยพิเศษกรณีเลิกจ้างตามมาตรา ๑๒๑)"
|
82 |
+
"""
|
83 |
+
),
|
84 |
+
MessagesPlaceholder(variable_name="history"),
|
85 |
+
HumanMessagePromptTemplate.from_template(
|
86 |
+
"""Based on the conversation history (if any) and the new question, generate an answer.
|
87 |
+
IMPORTANT: Use the conversation history ONLY to understand the context of the user's question (like what 'that' or 'in this case' refers to).
|
88 |
+
Your final answer MUST be based exclusively on the information within the 'Main Context' and 'Ref Context' provided below.
|
89 |
+
|
90 |
+
Conversation History:
|
91 |
+
{history}
|
92 |
+
|
93 |
+
-----------------
|
94 |
+
|
95 |
+
Provided Context for this turn:
|
96 |
+
Main Context: {main_context}
|
97 |
+
Ref Context: {ref_context}
|
98 |
+
|
99 |
+
-----------------
|
100 |
+
|
101 |
+
User's Current Question: {question}
|
102 |
+
"""
|
103 |
+
),
|
104 |
+
]
|
105 |
+
)
|
106 |
+
|
107 |
+
|
108 |
+
|
109 |
+
from langchain_core.prompts import ChatPromptTemplate, SystemMessagePromptTemplate
|
110 |
+
|
111 |
+
CLASSIFICATION_INPUT_PROMPT = ChatPromptTemplate.from_messages([
|
112 |
+
SystemMessagePromptTemplate.from_template(
|
113 |
+
"""You are an expert AI classifier. Your primary function is to determine whether a user's query requires information from a specialized legal knowledge base (a process known as Retrieval-Augmented Generation or RAG). To make an accurate classification, you must analyze the 'User Input' in conjunction with the 'Chat History' to grasp the full conversational context and the user's true intent.
|
114 |
+
|
115 |
+
**Your classification decision must be based on the following strict criteria:**
|
116 |
+
|
117 |
+
---
|
118 |
+
|
119 |
+
**1. Classify as "RAG" if the query is related to Thai labor law or its legal hierarchy.**
|
120 |
+
|
121 |
+
This includes, but is not limited to:
|
122 |
+
* **Direct questions about Thai Labor Law:** (กฎหมายแรงงาน)
|
123 |
+
* Specific scenarios requiring legal interpretation.
|
124 |
+
* Examples: "My employer isn't paying my salary, what can I do?" (นายจ้างไม่จ่ายเงิน), "How many vacation days am I entitled to per year?" (พนักงานมีสิทธิลาได้กี่วันต่อปี)
|
125 |
+
* **Questions about the hierarchy and types of Thai laws:** (ลำดับชั้นของกฎหมายไทย)
|
126 |
+
* **Constitution:** (รัฐธรรมนูญ) - The supreme law of the land.
|
127 |
+
* **Organic Act:** (พระราชบัญญัติประกอบรัฐธรรมนูญ) - Acts elaborating on the Constitution.
|
128 |
+
* **Act:** (พระราชบัญญัติ) - Laws enacted by the Parliament.
|
129 |
+
* **Emergency Decree:** (พระราชกำหนด) - Decrees issued by the Cabinet in urgent cases.
|
130 |
+
* **Royal Decree:** (พระราชกฤษฎีกา) - Decrees issued by the King on the Cabinet's advice to detail an Act.
|
131 |
+
* **Ministerial Regulation:** (กฎกระทรวง) - Regulations issued by a Minister to implement an Act.
|
132 |
+
* **Local Ordinance:** (ข้อบัญญัติท้องถิ่น) - Laws issued by local administrative organizations.
|
133 |
+
|
134 |
+
---
|
135 |
+
|
136 |
+
**2. Classify as "Non-RAG" if the query falls into any of the following categories, even when considering the chat history:**
|
137 |
+
|
138 |
+
* **General Conversation & Small Talk:** Greetings, chitchat, or questions that do not require specialized knowledge (e.g., "How are you?", "What's the weather like?").
|
139 |
+
* **General Knowledge Questions:** Inquiries answerable with common knowledge and not specific to the legal database (e.g., "Who is the president of the USA?").
|
140 |
+
* **Sensitive or Opinion-Based Topics:** Personal opinions, political discussions, topics about the monarchy, or general religious discussions.
|
141 |
+
* **Unrelated Services or Competitors:** Questions about other companies, services, or topics completely outside the scope of Thai labor law.
|
142 |
+
|
143 |
+
---
|
144 |
+
|
145 |
+
**Instructions for Output:**
|
146 |
+
- After your analysis, you must respond with **one single word only**.
|
147 |
+
- Your response must be either `RAG` or `Non-RAG`.
|
148 |
+
- Do not provide any explanations or additional text.
|
149 |
+
|
150 |
+
**Context for Analysis:**
|
151 |
+
Chat History:
|
152 |
+
{chat_history}
|
153 |
+
|
154 |
+
User Input: "{user_input}"
|
155 |
+
|
156 |
+
**Your Classification:**
|
157 |
+
"""
|
158 |
+
)
|
159 |
+
])
|
160 |
+
|
161 |
+
|
162 |
+
|
163 |
+
|
164 |
+
from langchain_core.prompts import ChatPromptTemplate, SystemMessagePromptTemplate, HumanMessagePromptTemplate
|
165 |
+
|
166 |
+
NON_RAG_PROMPT = ChatPromptTemplate.from_messages([
|
167 |
+
SystemMessagePromptTemplate.from_template(
|
168 |
+
"""You are a highly specialized AI assistant with a strict and defined scope. Your persona is that of a polite, professional, and gender-neutral expert.
|
169 |
+
|
170 |
+
**Your Primary Directive:**
|
171 |
+
Your task is to formulate a response to the user, and **your response MUST be 100% in the Thai language.**
|
172 |
+
|
173 |
+
**Core Task & Conditional Logic:**
|
174 |
+
You must analyze the user's input and choose ONE of the following two scenarios to formulate your response.
|
175 |
+
|
176 |
+
---
|
177 |
+
**Scenario A: The input is a simple greeting.**
|
178 |
+
- **Condition:** The user's input is a standard greeting, a simple hello, or a basic introduction (e.g., "สวัสดี", "ดีครับ", "Hello").
|
179 |
+
- **Required Action:** Respond with a professional, welcoming message that invites the user to ask a question about Thai labor law.
|
180 |
+
- **[Response Example in Thai]:** "ระบบพร้อมให้ข้อมูลเกี่ยวกับกฎหมายแรงงานไทย มีข้อสงสัยใดให้ช่วยเหลือ สามารถสอบถามได้"
|
181 |
+
|
182 |
+
---
|
183 |
+
**Scenario B: The input is any other out-of-scope topic.**
|
184 |
+
- **Condition:** The user's input is not a greeting and is not related to Thai labor law (e.g., general knowledge, politics, chit-chat).
|
185 |
+
- **Required Action:** Politely inform the user that their question is outside your area of expertise. **DO NOT** attempt to answer the user's original question under any circumstances.
|
186 |
+
- **[Response Example in Thai]:** "ขออภัย ระบบสามารถให้ข้อมูลได้เฉพาะในขอบเขตของกฎหมายแรงงานไทยเท่านั้น หากท่านมีคำถามที่เกี่ยวข้อง โปรดสอบถาม"
|
187 |
+
---
|
188 |
+
|
189 |
+
Now, based on these rules and scenarios, formulate the single, most appropriate Thai response for the following user input.
|
190 |
+
"""
|
191 |
+
),
|
192 |
+
HumanMessagePromptTemplate.from_template("User Input: '{user_input}'")
|
193 |
+
])
|
utils/input_classifier.py
ADDED
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# from openai import OpenAI
|
2 |
+
from dotenv import load_dotenv
|
3 |
+
import os
|
4 |
+
from utils.chat_prompts import CLASSIFICATION_INPUT_PROMPT
|
5 |
+
# from google import genai
|
6 |
+
from openai import OpenAI
|
7 |
+
import random, time
|
8 |
+
|
9 |
+
load_dotenv()
|
10 |
+
|
11 |
+
client_jai = OpenAI(
|
12 |
+
api_key=os.environ.get("JAI_API_KEY"),
|
13 |
+
base_url=os.environ.get("CHAT_BASE_URL")
|
14 |
+
)
|
15 |
+
model = "jai-chat-1-3-2"
|
16 |
+
# model = "openthaigpt72b"
|
17 |
+
|
18 |
+
# gemi = os.environ["GEMINI_API_KEY"]
|
19 |
+
# client_jai = genai.Client(api_key=gemi)
|
20 |
+
# model = "gemini-2.0-flash"
|
21 |
+
# temperature = 0.0
|
22 |
+
|
23 |
+
def generate_content_with_retry(client, model, prompt, max_retries=3):
|
24 |
+
"""
|
25 |
+
Helper function to call client.models.generate_content with retry logic.
|
26 |
+
"""
|
27 |
+
for attempt in range(max_retries):
|
28 |
+
try:
|
29 |
+
# response = client.models.generate_content( # gemi
|
30 |
+
response = client.chat.completions.create(
|
31 |
+
model=model,
|
32 |
+
messages = [{"role": "user", "content": prompt}]
|
33 |
+
# contents=prompt, # gemi
|
34 |
+
# temperature=temperature, # Optional: Restore if needed
|
35 |
+
)
|
36 |
+
# return response.text.strip() # Return the result if successful # gemi
|
37 |
+
return response.choices[0].message.content
|
38 |
+
|
39 |
+
|
40 |
+
except Exception as e:
|
41 |
+
if hasattr(e, 'code') and e.code == 503: # Check for the 503 error
|
42 |
+
print(f"Attempt {attempt + 1} failed with 503 error: {e}")
|
43 |
+
wait_time = (2 ** attempt) + random.random() # Exponential backoff
|
44 |
+
print(f"Waiting {wait_time:.2f} seconds before retrying...")
|
45 |
+
time.sleep(wait_time)
|
46 |
+
else:
|
47 |
+
print(f"Attempt {attempt + 1} failed with a different error: {e}")
|
48 |
+
raise # Re-raise the exception if it's not a 503
|
49 |
+
|
50 |
+
print(f"Failed to generate content after {max_retries} retries.")
|
51 |
+
return None # Or raise an exception, depending on desired behavior
|
52 |
+
|
53 |
+
|
54 |
+
def classify_input_type(user_input: str, history: list = None) -> str:
|
55 |
+
"""
|
56 |
+
Classifies the user input as 'RAG' or 'Non-RAG' using the LLM, considering chat history.
|
57 |
+
Supports history as a list of strings or a list of dicts with 'type' and 'content'.
|
58 |
+
"""
|
59 |
+
history_text = "None"
|
60 |
+
|
61 |
+
if history:
|
62 |
+
formatted_history = []
|
63 |
+
|
64 |
+
for i, msg in enumerate(history[-3:]):
|
65 |
+
# Case: history is list of dicts
|
66 |
+
if isinstance(msg, dict):
|
67 |
+
role = "Human" if msg.get("type") == "human" else "AI" # changed dash to human and aie
|
68 |
+
content = msg.get("content", "")
|
69 |
+
# Case: history is list of strings, alternate roles
|
70 |
+
elif isinstance(msg, str):
|
71 |
+
role = "Human" if (len(history[-3:]) - i) % 2 == 1 else "AI"
|
72 |
+
content = msg
|
73 |
+
else:
|
74 |
+
continue # skip invalid entry
|
75 |
+
|
76 |
+
formatted_history.append(f"{role}: {content}")
|
77 |
+
|
78 |
+
history_text = "\n".join(formatted_history)
|
79 |
+
|
80 |
+
formatted_messages = CLASSIFICATION_INPUT_PROMPT.format(
|
81 |
+
user_input=user_input,
|
82 |
+
chat_history=history_text
|
83 |
+
)
|
84 |
+
|
85 |
+
prompt_content = formatted_messages
|
86 |
+
# print(prompt_content)
|
87 |
+
|
88 |
+
result = generate_content_with_retry(client_jai, model, prompt_content)
|
89 |
+
|
90 |
+
if result is None:
|
91 |
+
raise Exception("Failed to classify input type after multiple retries.")
|
92 |
+
|
93 |
+
return result
|
utils/reranker.py
ADDED
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from langchain.retrievers import ContextualCompressionRetriever
|
2 |
+
from langchain.retrievers.document_compressors import CrossEncoderReranker
|
3 |
+
from langchain_community.cross_encoders import HuggingFaceCrossEncoder
|
4 |
+
from utils.retriever import get_retriever
|
5 |
+
|
6 |
+
# -- HuggingFaceCrossEncoder
|
7 |
+
# https://python.langchain.com/docs/integrations/document_transformers/cross_encoder_reranker/
|
8 |
+
|
9 |
+
|
10 |
+
# ---- Configurations for model ----
|
11 |
+
MODEL = HuggingFaceCrossEncoder(model_name="BAAI/bge-reranker-base")
|
12 |
+
TOP_N = 5
|
13 |
+
COMPRESSOR = CrossEncoderReranker(model=MODEL,
|
14 |
+
top_n=TOP_N)
|
15 |
+
|
16 |
+
|
17 |
+
# ---- Reranker Retriever ----
|
18 |
+
class RerankRetriever:
|
19 |
+
def __init__(self):
|
20 |
+
pass
|
21 |
+
|
22 |
+
def get_base_retriever(self, **kwargs):
|
23 |
+
filters = {**kwargs}
|
24 |
+
retriever = get_retriever(**filters)
|
25 |
+
return retriever
|
26 |
+
|
27 |
+
def get_compression_retriever(self, **kwargs):
|
28 |
+
|
29 |
+
# ---- get_base_retriever ----
|
30 |
+
base_retriever_used = self.get_base_retriever(**kwargs)
|
31 |
+
|
32 |
+
# ---- Instantiate compression retriever ----
|
33 |
+
compression_retriever = ContextualCompressionRetriever(
|
34 |
+
base_compressor=COMPRESSOR,
|
35 |
+
base_retriever=base_retriever_used,
|
36 |
+
tags=["qa_retriever", "rerank"]
|
37 |
+
)
|
38 |
+
|
39 |
+
return compression_retriever
|
40 |
+
|
41 |
+
def pretty_print_docs(self, docs):
|
42 |
+
return(
|
43 |
+
f"\n{'-' * 100}\n".join(
|
44 |
+
[f"Document {i+1}:\n\n" + d.page_content for i, d in enumerate(docs)]
|
45 |
+
)
|
46 |
+
)
|
utils/retriever.py
ADDED
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from langchain_huggingface import HuggingFaceEmbeddings
|
2 |
+
from langchain_mongodb.vectorstores import MongoDBAtlasVectorSearch
|
3 |
+
from langchain_mongodb.retrievers.hybrid_search import MongoDBAtlasHybridSearchRetriever
|
4 |
+
import os
|
5 |
+
from dotenv import load_dotenv
|
6 |
+
|
7 |
+
load_dotenv()
|
8 |
+
|
9 |
+
# ---- MongoDB credentials ----
|
10 |
+
mongo_username = os.getenv('MONGO_USERNAME')
|
11 |
+
mongo_password = os.getenv('MONGO_PASSWORD')
|
12 |
+
mongo_database = os.getenv('MONGO_DATABASE')
|
13 |
+
mongo_connection_str = os.getenv('MONGO_CONNECTION_STRING')
|
14 |
+
mongo_collection_name = os.getenv('MONGO_COLLECTION')
|
15 |
+
|
16 |
+
# ---- Common Configurations & Hybrid Retrieval Configuration ----
|
17 |
+
MODEL_KWARGS = {"device": "cpu"}
|
18 |
+
ENCODE_KWARGS = {"normalize_embeddings": True,
|
19 |
+
"batch_size": 32}
|
20 |
+
EMBEDDING_DIMENSIONS = 1024
|
21 |
+
MODEL_NAME = "BAAI/bge-m3"
|
22 |
+
FINAL_TOP_K = 10
|
23 |
+
HYBRID_FULLTEXT_PENALTY = 60
|
24 |
+
HYBRID_VECTOR_PENALTY = 60
|
25 |
+
|
26 |
+
# ---- Embedding model ----
|
27 |
+
embed_model = HuggingFaceEmbeddings(
|
28 |
+
model_name=MODEL_NAME,
|
29 |
+
model_kwargs=MODEL_KWARGS,
|
30 |
+
encode_kwargs=ENCODE_KWARGS
|
31 |
+
)
|
32 |
+
|
33 |
+
# ---- Vectore Search ----
|
34 |
+
num_vector_candidates = max(20, 2 * FINAL_TOP_K)
|
35 |
+
num_text_candidates = max(20, 2 * FINAL_TOP_K)
|
36 |
+
vector_k = num_vector_candidates
|
37 |
+
vector_num_candidates_for_operator = vector_k * 10
|
38 |
+
|
39 |
+
# ---- Vectore Store ----
|
40 |
+
vector_store = MongoDBAtlasVectorSearch.from_connection_string(
|
41 |
+
connection_string=mongo_connection_str,
|
42 |
+
namespace=f"{mongo_database}.{mongo_collection_name}",
|
43 |
+
embedding=embed_model,
|
44 |
+
index_name="search_index_v1",
|
45 |
+
)
|
46 |
+
|
47 |
+
# ---- Retriever (Hybrid) ----
|
48 |
+
def get_retriever(**kwargs):
|
49 |
+
retriever = MongoDBAtlasHybridSearchRetriever(
|
50 |
+
vectorstore=vector_store,
|
51 |
+
search_index_name='search_index_v1',
|
52 |
+
embedding=embed_model,
|
53 |
+
text_key= 'text', #'token',
|
54 |
+
embedding_key='embedding',
|
55 |
+
top_k=FINAL_TOP_K,
|
56 |
+
vector_penalty=HYBRID_VECTOR_PENALTY,
|
57 |
+
fulltext_penalty=HYBRID_FULLTEXT_PENALTY,
|
58 |
+
vector_search_params={
|
59 |
+
"k": vector_k,
|
60 |
+
"numCandidates": vector_num_candidates_for_operator
|
61 |
+
},
|
62 |
+
text_search_params={
|
63 |
+
"limit": num_text_candidates
|
64 |
+
},
|
65 |
+
pre_filter=kwargs
|
66 |
+
)
|
67 |
+
return retriever
|
68 |
+
|