Update calendar_rag.py
Browse files- calendar_rag.py +434 -288
calendar_rag.py
CHANGED
@@ -3,7 +3,7 @@ from haystack.components.generators.openai import OpenAIGenerator
|
|
3 |
from haystack.components.builders import PromptBuilder
|
4 |
from haystack.components.embedders import SentenceTransformersDocumentEmbedder
|
5 |
from haystack.components.retrievers.in_memory import *
|
6 |
-
from haystack.document_stores.in_memory import
|
7 |
from haystack.utils import Secret
|
8 |
from pathlib import Path
|
9 |
import hashlib
|
@@ -14,6 +14,7 @@ import json
|
|
14 |
import logging
|
15 |
import re
|
16 |
import pickle
|
|
|
17 |
|
18 |
# Setup logging
|
19 |
logging.basicConfig(level=logging.INFO)
|
@@ -647,7 +648,6 @@ class CalendarDataProcessor:
|
|
647 |
transportation=transportation
|
648 |
))
|
649 |
except Exception as e:
|
650 |
-
print(f"Error processing contact data: {e}")
|
651 |
continue
|
652 |
|
653 |
return contact_details
|
@@ -710,25 +710,25 @@ class CalendarDataProcessor:
|
|
710 |
# Create course categories
|
711 |
structure = {
|
712 |
'หมวดวิชาปรับพื้นฐาน': CourseCategory( # Previously foundation_courses
|
713 |
-
description=
|
714 |
credits=foundation_data.get('metadata', {}).get('credits', 'non-credit'),
|
715 |
minimum_credits=None,
|
716 |
courses=foundation_courses
|
717 |
),
|
718 |
'หมวดวิชาบังคับ': CourseCategory( # Previously core_courses
|
719 |
-
description=
|
720 |
credits=0,
|
721 |
minimum_credits=core_data.get('minimum_requirement_credits'),
|
722 |
courses=core_courses
|
723 |
),
|
724 |
'หมวดวิชาเลือก': CourseCategory( # Previously elective_courses
|
725 |
-
description=
|
726 |
credits=0,
|
727 |
minimum_credits=elective_data.get('minimum_requirement_credits'),
|
728 |
courses=elective_courses
|
729 |
),
|
730 |
'หมวดวิชาการค้นคว้าอิสระ': CourseCategory( # Previously research_courses
|
731 |
-
description=
|
732 |
credits=0,
|
733 |
minimum_credits=research_data.get('minimum_requirement_credits'),
|
734 |
courses=research_courses
|
@@ -877,33 +877,52 @@ class HybridDocumentStore:
|
|
877 |
self.cache_manager.set_embedding_cache(text, embedding)
|
878 |
return embedding
|
879 |
|
880 |
-
def
|
881 |
-
"""
|
882 |
-
|
883 |
-
|
884 |
-
|
885 |
-
|
886 |
-
|
887 |
-
|
888 |
-
|
889 |
-
|
890 |
-
|
891 |
-
|
892 |
-
|
893 |
-
|
894 |
-
|
895 |
-
|
896 |
-
|
897 |
-
|
898 |
-
|
899 |
-
|
900 |
-
|
901 |
-
|
902 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
903 |
|
904 |
-
def add_events(self,
|
905 |
-
|
906 |
-
|
|
|
|
|
|
|
|
|
907 |
"""Add events and additional data with caching"""
|
908 |
documents = []
|
909 |
added_events = set() # Track added events to prevent duplicates
|
@@ -976,7 +995,6 @@ class HybridDocumentStore:
|
|
976 |
# Process course structure
|
977 |
if course_structure:
|
978 |
for course in course_structure:
|
979 |
-
self.course_data.append(course)
|
980 |
text = f"""
|
981 |
โครงสร้างหลักสูตร:
|
982 |
ชื่อหลักสูตร: {course.program_name}
|
@@ -985,23 +1003,33 @@ class HybridDocumentStore:
|
|
985 |
ระดับการศึกษา: {course.degree_level}
|
986 |
|
987 |
รายละเอียดโครงสร้าง:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
988 |
"""
|
989 |
-
for category_name, category in course.structure.items():
|
990 |
-
text += f"\n{category_name}:\n"
|
991 |
-
if category.description:
|
992 |
-
text += f"คำอธิบาย: {category.description}\n"
|
993 |
-
text += f"หน่วยกิต: {category.credits}\n"
|
994 |
-
if category.minimum_credits:
|
995 |
-
text += f"หน่วยกิตขั้นต่ำ: {category.minimum_credits}\n"
|
996 |
-
text += "รายวิชา:\n"
|
997 |
-
for course_item in category.courses:
|
998 |
-
text += f"- {course_item.code}: {course_item.title_th} ({course_item.title_en}) - {course_item.credits} หน่วยกิต\n"
|
999 |
|
1000 |
-
embedding = self._compute_embedding(text)
|
1001 |
doc = Document(
|
1002 |
id=self._generate_unique_id(),
|
1003 |
-
content=text,
|
1004 |
-
embedding=
|
1005 |
meta={'event_type': 'curriculum'}
|
1006 |
)
|
1007 |
documents.append(doc)
|
@@ -1010,23 +1038,107 @@ class HybridDocumentStore:
|
|
1010 |
if study_plans:
|
1011 |
for plan in study_plans:
|
1012 |
self.study_plan_data.append(plan)
|
1013 |
-
text = "แผนการศึกษา:\n"
|
1014 |
for year, semesters in plan.years.items():
|
1015 |
-
text += f"\nปีที่ {year}:\n"
|
1016 |
for semester, data in semesters.items():
|
1017 |
-
|
1018 |
-
|
1019 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1020 |
if 'courses' in data:
|
1021 |
for course in data['courses']:
|
1022 |
-
text += f"- {course['code']}: {course['title'].get('th', '')} ({course['title'].get('en', '')}) - {course['credits']}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1023 |
|
1024 |
-
embedding = self._compute_embedding(text)
|
1025 |
doc = Document(
|
1026 |
id=self._generate_unique_id(),
|
1027 |
-
content=
|
1028 |
-
embedding=
|
1029 |
-
meta={'event_type': '
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1030 |
)
|
1031 |
documents.append(doc)
|
1032 |
|
@@ -1045,10 +1157,12 @@ class HybridDocumentStore:
|
|
1045 |
|
1046 |
def hybrid_search(self,
|
1047 |
query: str,
|
1048 |
-
event_type: Optional[str] = None,
|
1049 |
-
|
1050 |
-
|
|
|
1051 |
weight_semantic: float = 0.5) -> List[Document]:
|
|
|
1052 |
"""Hybrid search combining semantic and lexical search results"""
|
1053 |
|
1054 |
cache_key = json.dumps({
|
@@ -1065,15 +1179,16 @@ class HybridDocumentStore:
|
|
1065 |
|
1066 |
# Get semantic search results
|
1067 |
query_embedding = self._compute_embedding(query)
|
1068 |
-
semantic_results = self.embedding_retriever.run(
|
1069 |
-
query_embedding=query_embedding
|
1070 |
-
)["documents"]
|
1071 |
|
1072 |
# Get BM25 results
|
1073 |
bm25_results = self.bm25_retriever.run(
|
1074 |
query=query
|
1075 |
)["documents"]
|
1076 |
|
|
|
|
|
|
|
1077 |
# Combine results using score fusion
|
1078 |
combined_results = self._merge_results(
|
1079 |
semantic_results=semantic_results,
|
@@ -1085,10 +1200,8 @@ class HybridDocumentStore:
|
|
1085 |
# Filter results based on metadata
|
1086 |
filtered_results = []
|
1087 |
for doc in combined_results:
|
1088 |
-
if event_type and doc.meta.get('event_type') != event_type:
|
1089 |
-
continue
|
1090 |
-
if semester and doc.meta.get('semester') != semester:
|
1091 |
-
continue
|
1092 |
filtered_results.append(doc)
|
1093 |
|
1094 |
final_results = filtered_results[:top_k]
|
@@ -1139,80 +1252,6 @@ class HybridDocumentStore:
|
|
1139 |
)
|
1140 |
|
1141 |
return sorted_docs[:top_k]
|
1142 |
-
|
1143 |
-
class AdvancedQueryProcessor:
|
1144 |
-
"""Process queries with better understanding"""
|
1145 |
-
|
1146 |
-
def __init__(self, config: PipelineConfig):
|
1147 |
-
self.generator = OpenAIGenerator(
|
1148 |
-
api_key=Secret.from_token(config.model.openai_api_key),
|
1149 |
-
model=config.model.openai_model
|
1150 |
-
)
|
1151 |
-
self.prompt_builder = PromptBuilder(
|
1152 |
-
template="""
|
1153 |
-
วิเคราะห์คำถามที่เกี่ยวข้องกับปฏิทินการศึกษา (ภาษาไทย):
|
1154 |
-
คำถาม: {{query}}
|
1155 |
-
|
1156 |
-
ระบุ:
|
1157 |
-
1. ประเภทของข้อมูลที่ต้องการค้นหา
|
1158 |
-
2. ภาคการศึกษาที่ระบุไว้ (ถ้ามี)
|
1159 |
-
3. คำสำคัญที่เกี่ยวข้อง
|
1160 |
-
|
1161 |
-
ให้ผลลัพธ์ในรูปแบบ JSON:
|
1162 |
-
{
|
1163 |
-
"event_type": "ลงทะเบียน|กำหน���เวลา|การสอบ|วิชาการ|วันหยุด",
|
1164 |
-
"semester": "ภาคการศึกษาที่ระบุ หรือ null",
|
1165 |
-
"key_terms": ["คำสำคัญ 3 คำที่สำคัญที่สุด"],
|
1166 |
-
"response_format": "รายการ|คำตอบเดียว|คำตอบละเอียด"
|
1167 |
-
}
|
1168 |
-
"""
|
1169 |
-
)
|
1170 |
-
|
1171 |
-
def _get_default_analysis(self, query: str) -> Dict[str, Any]:
|
1172 |
-
"""Return default analysis when processing fails"""
|
1173 |
-
logger.info("Returning default analysis")
|
1174 |
-
return {
|
1175 |
-
"original_query": query,
|
1176 |
-
"event_type": None,
|
1177 |
-
"semester": None,
|
1178 |
-
"key_terms": [],
|
1179 |
-
"response_format": "detailed"
|
1180 |
-
}
|
1181 |
-
|
1182 |
-
def process_query(self, query: str) -> Dict[str, Any]:
|
1183 |
-
"""Enhanced query processing with better error handling."""
|
1184 |
-
|
1185 |
-
try:
|
1186 |
-
result = self.prompt_builder.run(query=query)
|
1187 |
-
response = self.generator.run(prompt=result["prompt"])
|
1188 |
-
|
1189 |
-
if not response or not response.get("replies") or not response["replies"][0]:
|
1190 |
-
logger.warning("Received empty response from OpenAI")
|
1191 |
-
return self._get_default_analysis(query)
|
1192 |
-
|
1193 |
-
try:
|
1194 |
-
analysis = json.loads(response["replies"][0])
|
1195 |
-
except json.JSONDecodeError as je:
|
1196 |
-
return self._get_default_analysis(query)
|
1197 |
-
|
1198 |
-
# **Ensure course-related queries retrieve study plans & curricula**
|
1199 |
-
course_keywords = ['หน่วยกิต', 'วิชา', 'หลักสูตร', 'แผนการเรียน', 'วิชาเลือก', 'วิชาบังคับ', 'วิชาการค้นคว้า', 'วิชาหลัก']
|
1200 |
-
if any(keyword in query for keyword in course_keywords):
|
1201 |
-
analysis['event_type'] = 'curriculum'
|
1202 |
-
|
1203 |
-
# **Ensure fee-related queries retrieve tuition fee documents**
|
1204 |
-
fee_keywords = ['ค่าเทอม', 'ค่าธรรมเนียม', 'ค่าเรียน', 'ค่าปรับ']
|
1205 |
-
if any(keyword in query for keyword in fee_keywords):
|
1206 |
-
analysis['event_type'] = 'fees'
|
1207 |
-
|
1208 |
-
return {
|
1209 |
-
"original_query": query,
|
1210 |
-
**analysis
|
1211 |
-
}
|
1212 |
-
|
1213 |
-
except Exception as e:
|
1214 |
-
logger.error(f"Query processing failed: {str(e)}")
|
1215 |
-
return self._get_default_analysis(query)
|
1216 |
|
1217 |
class ResponseGenerator:
|
1218 |
"""Generate responses with better context utilization"""
|
@@ -1224,21 +1263,27 @@ class ResponseGenerator:
|
|
1224 |
)
|
1225 |
self.prompt_builder = PromptBuilder(
|
1226 |
template="""
|
1227 |
-
คุณเป็นที่ปรึกษาทางวิชาการ
|
1228 |
|
1229 |
คำถาม: {{query}}
|
1230 |
|
1231 |
-
|
1232 |
{% for doc in context %}
|
1233 |
-
---
|
|
|
|
|
1234 |
{{doc.content}}
|
1235 |
{% endfor %}
|
1236 |
|
1237 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
1238 |
|
1239 |
กรุณาตอบเป็นภาษาไทย:
|
1240 |
-
|
1241 |
-
ต้องบอกเสมอว่า **หากมีข้อสงสัยเพิ่มเติมสามารถสอบถามได้**
|
1242 |
"""
|
1243 |
)
|
1244 |
|
@@ -1248,12 +1293,12 @@ class ResponseGenerator:
|
|
1248 |
query_info: Dict[str, Any]) -> str:
|
1249 |
"""Generate response using retrieved documents"""
|
1250 |
try:
|
|
|
1251 |
result = self.prompt_builder.run(
|
1252 |
query=query,
|
1253 |
context=documents,
|
1254 |
format=query_info["response_format"]
|
1255 |
)
|
1256 |
-
|
1257 |
response = self.generator.run(prompt=result["prompt"])
|
1258 |
return response["replies"][0]
|
1259 |
|
@@ -1261,6 +1306,184 @@ class ResponseGenerator:
|
|
1261 |
logger.error(f"Response generation failed: {str(e)}")
|
1262 |
return "ขออภัย ไม่สามารถประมวลผลคำตอบได้ในขณะนี้"
|
1263 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1264 |
class AcademicCalendarRAG:
|
1265 |
"""Enhanced RAG system for academic calendar and program information"""
|
1266 |
|
@@ -1295,152 +1518,75 @@ class AcademicCalendarRAG:
|
|
1295 |
self.study_plans = self.data_processor.extract_program_study_plan(json_data)
|
1296 |
self.tuition_fees = self.data_processor.extract_fees(json_data)
|
1297 |
|
1298 |
-
self._add_calendar_events()
|
1299 |
-
self._add_program_info()
|
1300 |
-
|
1301 |
-
except Exception as e:
|
1302 |
-
logger.error(f"Error loading data: {str(e)}")
|
1303 |
-
raise
|
1304 |
-
|
1305 |
-
def _add_calendar_events(self):
|
1306 |
-
"""Add calendar events and other data to document store"""
|
1307 |
-
if self.calendar_events:
|
1308 |
self.document_store.add_events(
|
1309 |
events=self.calendar_events,
|
|
|
1310 |
contact_details=self.contact_details,
|
1311 |
course_structure=self.course_structure,
|
1312 |
-
study_plans=self.study_plans
|
|
|
1313 |
)
|
1314 |
-
|
1315 |
-
def _add_program_info(self):
|
1316 |
-
"""Enhanced method to add program-related information to document store"""
|
1317 |
-
if self.program_details:
|
1318 |
-
for detail in self.program_details:
|
1319 |
-
text = f"""
|
1320 |
-
ข้อมูลการสมัคร:
|
1321 |
-
เว็บไซต์รับสมัคร: {detail.application_info.application_portal}
|
1322 |
-
อีเมล: {detail.application_info.program_email}
|
1323 |
-
|
1324 |
-
เอกสารที่ต้องใช้:
|
1325 |
-
{self._format_required_docs(detail.required_documents)}
|
1326 |
-
|
1327 |
-
ขั้นตอนการส่งเอกสาร:
|
1328 |
-
{detail.submission_process}
|
1329 |
-
|
1330 |
-
ขั้นตอนการคัดเลือก:
|
1331 |
-
{self._format_selection_steps(detail.selection_process)}
|
1332 |
-
"""
|
1333 |
-
self.document_store.add_document(text, "program_details")
|
1334 |
-
|
1335 |
-
if self.tuition_fees:
|
1336 |
-
for fee in self.tuition_fees:
|
1337 |
-
text = f"""
|
1338 |
-
ค่าธรรมเนียมการศึกษา:
|
1339 |
-
ค่าเล่าเรียนปกติ: {fee.regular_fee.amount:,.2f} {fee.regular_fee.currency} {fee.regular_fee.period}
|
1340 |
-
ค่าปรับชำระล่าช้า: {fee.late_payment_fee.amount:,.2f} {fee.late_payment_fee.currency}
|
1341 |
-
"""
|
1342 |
-
self.document_store.add_document(text, "fees")
|
1343 |
-
|
1344 |
-
def _format_required_docs(self, docs: Dict) -> str:
|
1345 |
-
"""Format required documents information with detailed English proficiency requirements"""
|
1346 |
-
result = []
|
1347 |
-
|
1348 |
-
if 'mandatory' in docs:
|
1349 |
-
result.append("เอกสารที่ต้องใช้:")
|
1350 |
-
for doc in docs['mandatory'].values():
|
1351 |
-
result.append(f"- {doc.name}: {doc.description}")
|
1352 |
-
|
1353 |
-
if 'optional' in docs:
|
1354 |
-
result.append("\nเอกสารเพิ่มเติม:")
|
1355 |
-
for doc_key, doc in docs['optional'].items():
|
1356 |
-
if doc_key == 'english_proficiency':
|
1357 |
-
result.append(f"- {doc.name}")
|
1358 |
-
# Parse and format the accepted tests
|
1359 |
-
try:
|
1360 |
-
accepted_tests = eval(doc.description)
|
1361 |
-
result.append(" เกณฑ์คะแนนที่ยอมรับ:")
|
1362 |
-
for test, requirement in accepted_tests.items():
|
1363 |
-
result.append(f" * {test}: {requirement}")
|
1364 |
-
except:
|
1365 |
-
result.append(f" {doc.description}")
|
1366 |
-
|
1367 |
-
if doc.conditions:
|
1368 |
-
conditions = doc.conditions.split(', ')
|
1369 |
-
for condition in conditions:
|
1370 |
-
result.append(f" {condition}")
|
1371 |
-
else:
|
1372 |
-
desc = f"- {doc.name}"
|
1373 |
-
if doc.conditions:
|
1374 |
-
desc += f" ({doc.conditions})"
|
1375 |
-
result.append(desc)
|
1376 |
-
|
1377 |
-
return "\n".join(result)
|
1378 |
-
|
1379 |
-
def _format_selection_steps(self, steps: List[SelectionStep]) -> str:
|
1380 |
-
"""Format selection process steps"""
|
1381 |
-
return "\n".join(f"{step.step_number}. {step.description}" for step in steps)
|
1382 |
-
|
1383 |
-
def _get_fee_documents(self) -> List[Document]:
|
1384 |
-
"""Get fee-related documents"""
|
1385 |
-
if not self.tuition_fees:
|
1386 |
-
return []
|
1387 |
|
1388 |
-
|
1389 |
-
|
1390 |
-
|
1391 |
-
ค่าธรรมเนียมการศึกษา:
|
1392 |
-
- ค่าเล่าเรียน: {fee.regular_fee.amount:,.2f} {fee.regular_fee.currency} {fee.regular_fee.period}
|
1393 |
-
- ค่าปรับชำระล่าช้า: {fee.late_payment_fee.amount:,.2f} {fee.late_payment_fee.currency}
|
1394 |
-
"""
|
1395 |
-
doc = Document(
|
1396 |
-
content=text,
|
1397 |
-
meta={"event_type": "fees"}
|
1398 |
-
)
|
1399 |
-
documents.append(doc)
|
1400 |
-
|
1401 |
-
return documents
|
1402 |
|
1403 |
-
def process_query(self, query: str
|
1404 |
-
"""Process user query using hybrid retrieval"""
|
1405 |
-
|
1406 |
-
|
1407 |
-
|
1408 |
-
|
1409 |
-
|
1410 |
-
|
1411 |
-
|
1412 |
-
|
1413 |
-
|
1414 |
-
|
1415 |
-
|
1416 |
-
|
1417 |
-
|
1418 |
-
|
1419 |
-
|
1420 |
-
|
1421 |
-
documents
|
1422 |
-
|
1423 |
-
|
1424 |
-
|
1425 |
-
|
1426 |
-
|
1427 |
-
|
1428 |
-
|
1429 |
-
|
1430 |
-
|
1431 |
-
|
1432 |
-
|
1433 |
-
|
1434 |
-
|
1435 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1436 |
|
1437 |
-
except Exception as e:
|
1438 |
-
logger.error(f"Error processing query: {str(e)}")
|
1439 |
-
return {
|
1440 |
-
"query": query,
|
1441 |
-
"answer": "ขออภัย ไม่สามารถประมวลผลคำตอบได้ในขณะนี้",
|
1442 |
-
"error": str(e)
|
1443 |
-
}
|
1444 |
# def main():
|
1445 |
# """Main function demonstrating hybrid retrieval"""
|
1446 |
# try:
|
@@ -1459,8 +1605,8 @@ class AcademicCalendarRAG:
|
|
1459 |
# pipeline.load_data(raw_data)
|
1460 |
|
1461 |
# # Test queries with different semantic weights
|
1462 |
-
# queries = ["
|
1463 |
-
|
1464 |
# print("=" * 80)
|
1465 |
|
1466 |
# for query in queries:
|
@@ -1472,6 +1618,6 @@ class AcademicCalendarRAG:
|
|
1472 |
# except Exception as e:
|
1473 |
# logger.error(f"Pipeline execution failed: {str(e)}")
|
1474 |
# raise
|
1475 |
-
|
1476 |
# if __name__ == "__main__":
|
1477 |
# main()
|
|
|
3 |
from haystack.components.builders import PromptBuilder
|
4 |
from haystack.components.embedders import SentenceTransformersDocumentEmbedder
|
5 |
from haystack.components.retrievers.in_memory import *
|
6 |
+
from haystack.document_stores.in_memory import InMemoryDocumentStore
|
7 |
from haystack.utils import Secret
|
8 |
from pathlib import Path
|
9 |
import hashlib
|
|
|
14 |
import logging
|
15 |
import re
|
16 |
import pickle
|
17 |
+
import statistics
|
18 |
|
19 |
# Setup logging
|
20 |
logging.basicConfig(level=logging.INFO)
|
|
|
648 |
transportation=transportation
|
649 |
))
|
650 |
except Exception as e:
|
|
|
651 |
continue
|
652 |
|
653 |
return contact_details
|
|
|
710 |
# Create course categories
|
711 |
structure = {
|
712 |
'หมวดวิชาปรับพื้นฐาน': CourseCategory( # Previously foundation_courses
|
713 |
+
description="วิชาพื้นฐานที่จำเป็นต้องเรียน foundation courses รายวิชาปรับพื้นฐาน",
|
714 |
credits=foundation_data.get('metadata', {}).get('credits', 'non-credit'),
|
715 |
minimum_credits=None,
|
716 |
courses=foundation_courses
|
717 |
),
|
718 |
'หมวดวิชาบังคับ': CourseCategory( # Previously core_courses
|
719 |
+
description="วิชาบังคับ วิชาหลัก core courses รายวิชาที่ต้องเรียน",
|
720 |
credits=0,
|
721 |
minimum_credits=core_data.get('minimum_requirement_credits'),
|
722 |
courses=core_courses
|
723 |
),
|
724 |
'หมวดวิชาเลือก': CourseCategory( # Previously elective_courses
|
725 |
+
description="วิชาเลือก elective courses รายวิชาเลือก วิชาที่สามารถเลือกเรียนได้",
|
726 |
credits=0,
|
727 |
minimum_credits=elective_data.get('minimum_requirement_credits'),
|
728 |
courses=elective_courses
|
729 |
),
|
730 |
'หมวดวิชาการค้นคว้าอิสระ': CourseCategory( # Previously research_courses
|
731 |
+
description="วิชาค้นคว้าอิสระ research courses วิทยานิพนธ์",
|
732 |
credits=0,
|
733 |
minimum_credits=research_data.get('minimum_requirement_credits'),
|
734 |
courses=research_courses
|
|
|
877 |
self.cache_manager.set_embedding_cache(text, embedding)
|
878 |
return embedding
|
879 |
|
880 |
+
def _format_required_docs(self, docs: Dict) -> str:
|
881 |
+
"""Format required documents information with detailed English proficiency requirements"""
|
882 |
+
result = []
|
883 |
+
|
884 |
+
if 'mandatory' in docs:
|
885 |
+
result.append("เอกสารที่ต้องใช้:")
|
886 |
+
for doc in docs['mandatory'].values():
|
887 |
+
result.append(f"- {doc.name}: {doc.description}")
|
888 |
+
|
889 |
+
if 'optional' in docs:
|
890 |
+
result.append("\nเอกสารเพิ่มเติม:")
|
891 |
+
for doc_key, doc in docs['optional'].items():
|
892 |
+
if doc_key == 'english_proficiency':
|
893 |
+
result.append(f"- {doc.name}")
|
894 |
+
# Parse and format the accepted tests
|
895 |
+
try:
|
896 |
+
accepted_tests = eval(doc.description)
|
897 |
+
result.append(" เกณฑ์คะแนนที่ยอมรับ:")
|
898 |
+
for test, requirement in accepted_tests.items():
|
899 |
+
result.append(f" * {test}: {requirement}")
|
900 |
+
except:
|
901 |
+
result.append(f" {doc.description}")
|
902 |
+
|
903 |
+
if doc.conditions:
|
904 |
+
conditions = doc.conditions.split(', ')
|
905 |
+
for condition in conditions:
|
906 |
+
result.append(f" {condition}")
|
907 |
+
else:
|
908 |
+
desc = f"- {doc.name}"
|
909 |
+
if doc.conditions:
|
910 |
+
desc += f" ({doc.conditions})"
|
911 |
+
result.append(desc)
|
912 |
+
|
913 |
+
return "\n".join(result)
|
914 |
+
|
915 |
+
def _format_selection_steps(self, steps: List[SelectionStep]) -> str:
|
916 |
+
"""Format selection process steps"""
|
917 |
+
return "\n".join(f"{step.step_number}. {step.description}" for step in steps)
|
918 |
|
919 |
+
def add_events(self,
|
920 |
+
events: List[CalendarEvent],
|
921 |
+
contact_details: Optional[List[ContactDetail]] = None,
|
922 |
+
course_structure: Optional[List[CourseStructure]] = None,
|
923 |
+
study_plans: Optional[List[StudyPlan]] = None,
|
924 |
+
program_details: Optional[List[ProgramDetailInfo]] = None,
|
925 |
+
tuition_fees: Optional[List[TuitionFee]] = None):
|
926 |
"""Add events and additional data with caching"""
|
927 |
documents = []
|
928 |
added_events = set() # Track added events to prevent duplicates
|
|
|
995 |
# Process course structure
|
996 |
if course_structure:
|
997 |
for course in course_structure:
|
|
|
998 |
text = f"""
|
999 |
โครงสร้างหลักสูตร:
|
1000 |
ชื่อหลักสูตร: {course.program_name}
|
|
|
1003 |
ระดับการศึกษา: {course.degree_level}
|
1004 |
|
1005 |
รายละเอียดโครงสร้าง:
|
1006 |
+
|
1007 |
+
หมวดวิชาปรับพื้นฐาน/วิชาพื้นฐาน:
|
1008 |
+
คำอธิบาย: {course.structure['หมวดวิชาปรับพื้นฐาน'].description or 'ไม่ระบุ'}
|
1009 |
+
หน่วยกิต: {course.structure['หมวดวิชาปรับพื้นฐาน'].credits}
|
1010 |
+
รายวิชา:
|
1011 |
+
{' '.join([f'- {c.code}: {c.title_th} ({c.title_en}) - {c.credits} หน่วยกิต\n' for c in course.structure['หมวดวิชาปรับพื้นฐาน'].courses])}
|
1012 |
+
|
1013 |
+
หมวดวิชาบังคับ/วิชาหลัก:
|
1014 |
+
หน่วยกิตขั้นต่ำ: {course.structure['หมวดวิชาบังคับ'].minimum_credits}
|
1015 |
+
รายวิชา:
|
1016 |
+
{' '.join([f'- {c.code}: {c.title_th} ({c.title_en}) - {c.credits} หน่วยกิต\n' for c in course.structure['หมวดวิชาบังคับ'].courses])}
|
1017 |
+
|
1018 |
+
หมวดวิชาเลือก:
|
1019 |
+
หน่วยกิตขั้นต่ำ: {course.structure['หมวดวิชาเลือก'].minimum_credits}
|
1020 |
+
รายวิชา:
|
1021 |
+
{' '.join([f'- {c.code}: {c.title_th} ({c.title_en}) - {c.credits} หน่วยกิต\n' for c in course.structure['หมวดวิชาเลือก'].courses])}
|
1022 |
+
|
1023 |
+
หมวดวิชาการค้นคว้าอิสระ:
|
1024 |
+
หน่วยกิตขั้นต่ำ: {course.structure['หมวดวิชาการค้นคว้าอิสระ'].minimum_credits}
|
1025 |
+
รายวิชา:
|
1026 |
+
{' '.join([f'- {c.code}: {c.title_th} ({c.title_en}) - {c.credits} หน่วยกิต\n' for c in course.structure['หมวดวิชาการค้นคว้าอิสระ'].courses])}
|
1027 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1028 |
|
|
|
1029 |
doc = Document(
|
1030 |
id=self._generate_unique_id(),
|
1031 |
+
content=text.strip(),
|
1032 |
+
embedding=self._compute_embedding(text),
|
1033 |
meta={'event_type': 'curriculum'}
|
1034 |
)
|
1035 |
documents.append(doc)
|
|
|
1038 |
if study_plans:
|
1039 |
for plan in study_plans:
|
1040 |
self.study_plan_data.append(plan)
|
|
|
1041 |
for year, semesters in plan.years.items():
|
|
|
1042 |
for semester, data in semesters.items():
|
1043 |
+
# Convert year and semester format
|
1044 |
+
year_num = year.replace('year', '')
|
1045 |
+
semester_num = semester.replace('semester', '')
|
1046 |
+
|
1047 |
+
# Determine course type and translate to Thai
|
1048 |
+
course_type = data.get('metadata', {}).get('course_type', 'core')
|
1049 |
+
course_type_th = 'วิชาหลัก' if course_type == 'core' else 'วิชาเลือก'
|
1050 |
+
|
1051 |
+
# Calculate total credits
|
1052 |
+
total_credits = sum(course.get('credits', 0) for course in data.get('courses', []))
|
1053 |
+
|
1054 |
+
text = f"""แผนการศึกษา:
|
1055 |
+
ปี: {year_num}
|
1056 |
+
ภาคการศึกษา: {semester_num}
|
1057 |
+
ประเภทรายวิชา: {course_type_th} ({course_type})
|
1058 |
+
จำนวนหน่วยกิตรวม: {total_credits}
|
1059 |
+
|
1060 |
+
รายวิชาที่ต้องเรียน:"""
|
1061 |
+
|
1062 |
+
# Add courses
|
1063 |
if 'courses' in data:
|
1064 |
for course in data['courses']:
|
1065 |
+
text += f"\n- {course['code']}: {course['title'].get('th', '')} ({course['title'].get('en', '')}) - {course['credits']} หน่วยกิต"
|
1066 |
+
|
1067 |
+
embedding = self._compute_embedding(text)
|
1068 |
+
doc = Document(
|
1069 |
+
id=self._generate_unique_id(),
|
1070 |
+
content=text,
|
1071 |
+
embedding=embedding,
|
1072 |
+
meta={
|
1073 |
+
'event_type': 'study_plan',
|
1074 |
+
'year': year_num,
|
1075 |
+
'semester': semester_num,
|
1076 |
+
'course_type': course_type
|
1077 |
+
}
|
1078 |
+
)
|
1079 |
+
documents.append(doc)
|
1080 |
+
|
1081 |
+
if program_details:
|
1082 |
+
for detail in program_details:
|
1083 |
+
# Main application document
|
1084 |
+
app_text = f"""
|
1085 |
+
ข้อมูลการสมัคร:
|
1086 |
+
เว็บไซต์รับสมัคร: {detail.application_info.application_portal}
|
1087 |
+
อีเมล: {detail.application_info.program_email}
|
1088 |
+
|
1089 |
+
เอกสารที่ต้องใช้:
|
1090 |
+
{self._format_required_docs(detail.required_documents)}
|
1091 |
+
|
1092 |
+
ขั้นตอนการส่งเอกสาร:
|
1093 |
+
{detail.submission_process}
|
1094 |
+
|
1095 |
+
ขั้นตอนการคัดเลือก:
|
1096 |
+
{self._format_selection_steps(detail.selection_process)}
|
1097 |
+
"""
|
1098 |
|
|
|
1099 |
doc = Document(
|
1100 |
id=self._generate_unique_id(),
|
1101 |
+
content=app_text.strip(),
|
1102 |
+
embedding=self._compute_embedding(app_text),
|
1103 |
+
meta={'event_type': 'program_details'}
|
1104 |
+
)
|
1105 |
+
documents.append(doc)
|
1106 |
+
|
1107 |
+
# Create separate document for English proficiency requirements
|
1108 |
+
if 'optional' in detail.required_documents:
|
1109 |
+
eng_prof = next((doc for doc_key, doc in detail.required_documents['optional'].items()
|
1110 |
+
if doc_key == 'english_proficiency'), None)
|
1111 |
+
if eng_prof:
|
1112 |
+
eng_text = f"""
|
1113 |
+
ข้อกำหนดภาษาอังกฤษ:
|
1114 |
+
{eng_prof.name}
|
1115 |
+
รายละเอียด: {eng_prof.description}
|
1116 |
+
เงื่อนไข: {eng_prof.conditions}
|
1117 |
+
"""
|
1118 |
+
|
1119 |
+
eng_doc = Document(
|
1120 |
+
id=self._generate_unique_id(),
|
1121 |
+
content=eng_text.strip(),
|
1122 |
+
embedding=self._compute_embedding(eng_text),
|
1123 |
+
meta={
|
1124 |
+
'event_type': 'program_details' }
|
1125 |
+
)
|
1126 |
+
documents.append(eng_doc)
|
1127 |
+
|
1128 |
+
# Process tuition fees
|
1129 |
+
if tuition_fees:
|
1130 |
+
for fee in tuition_fees:
|
1131 |
+
fee_text = f"""
|
1132 |
+
ค่าธรรมเนียมการศึกษา:
|
1133 |
+
- ค่าเล่าเรียน: {fee.regular_fee.amount:,.2f} {fee.regular_fee.currency} {fee.regular_fee.period}
|
1134 |
+
- ค่าปรับชำระล่าช้า: {fee.late_payment_fee.amount:,.2f} {fee.late_payment_fee.currency}
|
1135 |
+
"""
|
1136 |
+
|
1137 |
+
doc = Document(
|
1138 |
+
id=self._generate_unique_id(),
|
1139 |
+
content=fee_text.strip(),
|
1140 |
+
embedding=self._compute_embedding(fee_text),
|
1141 |
+
meta={'event_type': 'fees'}
|
1142 |
)
|
1143 |
documents.append(doc)
|
1144 |
|
|
|
1157 |
|
1158 |
def hybrid_search(self,
|
1159 |
query: str,
|
1160 |
+
event_type: Optional[str] = None,
|
1161 |
+
detail_type: Optional[str] = None,
|
1162 |
+
semester: Optional[str] = None,
|
1163 |
+
top_k: int = 10,
|
1164 |
weight_semantic: float = 0.5) -> List[Document]:
|
1165 |
+
|
1166 |
"""Hybrid search combining semantic and lexical search results"""
|
1167 |
|
1168 |
cache_key = json.dumps({
|
|
|
1179 |
|
1180 |
# Get semantic search results
|
1181 |
query_embedding = self._compute_embedding(query)
|
1182 |
+
semantic_results = self.embedding_retriever.run(query_embedding=query_embedding)["documents"]
|
|
|
|
|
1183 |
|
1184 |
# Get BM25 results
|
1185 |
bm25_results = self.bm25_retriever.run(
|
1186 |
query=query
|
1187 |
)["documents"]
|
1188 |
|
1189 |
+
if event_type == "program_details":
|
1190 |
+
weight_semantic = 0.3 # Give more weight to keyword matching
|
1191 |
+
|
1192 |
# Combine results using score fusion
|
1193 |
combined_results = self._merge_results(
|
1194 |
semantic_results=semantic_results,
|
|
|
1200 |
# Filter results based on metadata
|
1201 |
filtered_results = []
|
1202 |
for doc in combined_results:
|
1203 |
+
if event_type and event_type != "program_details" and doc.meta.get('event_type') != event_type:
|
1204 |
+
continue # Keep only relevant event type unless it's program_details
|
|
|
|
|
1205 |
filtered_results.append(doc)
|
1206 |
|
1207 |
final_results = filtered_results[:top_k]
|
|
|
1252 |
)
|
1253 |
|
1254 |
return sorted_docs[:top_k]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1255 |
|
1256 |
class ResponseGenerator:
|
1257 |
"""Generate responses with better context utilization"""
|
|
|
1263 |
)
|
1264 |
self.prompt_builder = PromptBuilder(
|
1265 |
template="""
|
1266 |
+
คุณเป็นที่ปรึกษาทางวิชาการ กรุณาตอบคำถามต่อไปนี้โดยใช้ข้อมูลจากเอกสารที่ให้มาเท่านั้น
|
1267 |
|
1268 |
คำถาม: {{query}}
|
1269 |
|
1270 |
+
ข้อมูลที่เกี่ยวข้อง:
|
1271 |
{% for doc in context %}
|
1272 |
+
---
|
1273 |
+
ประเภท: {{doc.meta.event_type}}{% if doc.meta.detail_type %}, รายละเอียด: {{doc.meta.detail_type}}{% endif %}
|
1274 |
+
เนื้อหา:
|
1275 |
{{doc.content}}
|
1276 |
{% endfor %}
|
1277 |
|
1278 |
+
คำแนะนำในการตอบ:
|
1279 |
+
1. ตอบเฉพาะข้อมูลที่มีในเอกสารเท่านั้น
|
1280 |
+
2. หากไม่มีข้อมูลให้ตอบว่า "ขออภัย ไม่พบข้อมูลที่เกี่ยวข้องกับคำถามนี้"
|
1281 |
+
3. หากข้อมูลไม่ชัดเจนให้ระบุว่าข้อมูลอาจไม่ครบถ้วน
|
1282 |
+
4. จัดรูปแบบคำตอบให้อ่านง่าย ใช้หัวข้อและย่อหน้าตามความเหมาะสม
|
1283 |
+
5. สำหรับคำถามเกี่ยวกับข้อกำหนดภาษาอังกฤษหรือขั้นตอนการสมัคร ให้อธิบายข้อมูลอย่างละเอียด
|
1284 |
+
6. ใส่ข้อความ "หากมีข้อสงสัยเพิ่มเติม สามารถสอบถามได้" ท้ายคำตอบเสมอ
|
1285 |
|
1286 |
กรุณาตอบเป็นภาษาไทย:
|
|
|
|
|
1287 |
"""
|
1288 |
)
|
1289 |
|
|
|
1293 |
query_info: Dict[str, Any]) -> str:
|
1294 |
"""Generate response using retrieved documents"""
|
1295 |
try:
|
1296 |
+
print(query_info)
|
1297 |
result = self.prompt_builder.run(
|
1298 |
query=query,
|
1299 |
context=documents,
|
1300 |
format=query_info["response_format"]
|
1301 |
)
|
|
|
1302 |
response = self.generator.run(prompt=result["prompt"])
|
1303 |
return response["replies"][0]
|
1304 |
|
|
|
1306 |
logger.error(f"Response generation failed: {str(e)}")
|
1307 |
return "ขออภัย ไม่สามารถประมวลผลคำตอบได้ในขณะนี้"
|
1308 |
|
1309 |
+
class AdvancedQueryProcessor:
|
1310 |
+
"""Process queries with better understanding"""
|
1311 |
+
|
1312 |
+
def __init__(self, config: PipelineConfig):
|
1313 |
+
self.generator = OpenAIGenerator(
|
1314 |
+
api_key=Secret.from_token(config.model.openai_api_key),
|
1315 |
+
model=config.model.openai_model
|
1316 |
+
)
|
1317 |
+
self.prompt_builder = PromptBuilder(
|
1318 |
+
template="""
|
1319 |
+
คุณเป็นผู้ช่วย AI ที่เชี่ยวชาญด้านการศึกษาในประเทศไทย หน้าที่ของคุณคือการวิเคราะห์และจำแนกคำถามของผู้ใช้ให้ตรงกับหมวดหมู่ข้อมูลที่เหมาะสม ได้แก่:
|
1320 |
+
|
1321 |
+
1. **รายละเอียดโปรแกรมการศึกษา (program_details)**: ข้อมูลเกี่ยวกับหลักสูตร โปรแกรมการเรียนการสอน และโครงสร้างหลักสูตร
|
1322 |
+
2. **ข้อมูลการติดต่อ (contact)**: ข้อมูลการติดต่อของหน่วยงานหรือบุคคลที่เกี่ยวข้องในสถาบันการศึกษา
|
1323 |
+
3. **โครงสร้างหลักสูตร (curriculum)**: รายละเอียดเกี่ยวกับวิชาเรียน หน่วยกิต และแผนการศึกษา
|
1324 |
+
4. **ค่าเล่าเรียน (fees)**: ข้อมูลเกี่ยวกับค่าใช้จ่ายในการศึกษา ค่าธรรมเนียม และทุนการศึกษา
|
1325 |
+
5. **แผนการศึกษารายปี (study_plan)**: ข้อมูลแผนการเรียนแบ่งตามชั้นปีและภาคการศึกษา รายละเอียดรายวิชาที่ต้องลงทะเบียนในแต่ละเทอม และจำนวนหน่วยกิตรวม
|
1326 |
+
|
1327 |
+
**คำถาม**: {{query}}
|
1328 |
+
|
1329 |
+
**คำแนะนำในการวิเคราะห์**:
|
1330 |
+
- ตรวจสอบคำสำคัญในคำถามเพื่อระบุหมวดหมู่ที่สอดคล้อง
|
1331 |
+
- หากคำถามเกี่ยวข้องกับหลายหมวดหมู่ ให้จัดลำดับความสำคัญตามความต้องการของผู้ใช้
|
1332 |
+
- หากไม่สามารถระบุหมวดหมู่ได้อย่างชัดเจน ให้จัดหมวดหมู่เป็น "อื่นๆ" และระบุความไม่แน่นอน
|
1333 |
+
|
1334 |
+
**รูปแบบการตอบกลับ**:
|
1335 |
+
|
1336 |
+
หมายเหตุ:
|
1337 |
+
- รูปแบบปีการศึกษาที่ยอมรับ: "ปีที่ 1", "ปี 1", "ชั้นปีที่ 1"
|
1338 |
+
- รูปแบบภาคการศึกษาที่ยอมรับ: "เทอมที่ 1", "เทอม 1", "ภาคการศึกษาที่ 1"
|
1339 |
+
- หากข้อมูลไม่ครบ ให้ระบุค่าสำหรับฟิลด์ที่ขาดหายเป็น null พร้อมข้อความแจ้งความไม���แน่นอน
|
1340 |
+
|
1341 |
+
ให้ผลลัพธ์ในรูปแบบ JSON ตามโครงสร้าง:
|
1342 |
+
{
|
1343 |
+
"event_type": "program_details" | "contact" | "curriculum" | "fees" | "study_plan",
|
1344 |
+
"year": "ปีที่ X", // แปลงเป็นรูปแบบมาตรฐาน หรือ null หากไม่ระบุ
|
1345 |
+
"semester": "เทอมที่ X", // แปลงเป็นรูปแบบมาตรฐาน หรือ null หากไม่ระบุ
|
1346 |
+
"key_terms": ["คำสำคัญที่เกี่ยวข้อง"],
|
1347 |
+
"response_format": "detailed",
|
1348 |
+
"uncertainty": "low" // ระบุระดับความไม่แน่นอน (เช่น 'low', 'high')
|
1349 |
+
}
|
1350 |
+
|
1351 |
+
ตัวอย่าง:
|
1352 |
+
Input: "โปรแกรมการศึกษามีรายละเอียดอะไรบ้าง"
|
1353 |
+
Output: {
|
1354 |
+
"event_type": "program_details",
|
1355 |
+
"year": null,
|
1356 |
+
"semester": null,
|
1357 |
+
"key_terms": ["โปรแกรมการศึกษา", "รายละเอียด"],
|
1358 |
+
"response_format": "detailed",
|
1359 |
+
"uncertainty": "low"
|
1360 |
+
}
|
1361 |
+
|
1362 |
+
Input: "ฉันจะติดต่อภาควิชาได้อย่างไร"
|
1363 |
+
Output: {
|
1364 |
+
"event_type": "contact",
|
1365 |
+
"year": null,
|
1366 |
+
"semester": null,
|
1367 |
+
"key_terms": ["ติดต่อ", "ภาควิชา"],
|
1368 |
+
"response_format": "detailed",
|
1369 |
+
"uncertainty": "low"
|
1370 |
+
}
|
1371 |
+
|
1372 |
+
Input: "โครงสร้างหลักสูตรของปี 2 เป็นอย่างไร"
|
1373 |
+
Output: {
|
1374 |
+
"event_type": "curriculum",
|
1375 |
+
"year": "ปีที่ 2",
|
1376 |
+
"semester": null,
|
1377 |
+
"key_terms": ["โครงสร้างหลักสูตร"],
|
1378 |
+
"response_format": "detailed",
|
1379 |
+
"uncertainty": "low"
|
1380 |
+
}
|
1381 |
+
|
1382 |
+
Input: "ค่าเล่าเรียนสำหรับเทอม 1 เท่าไหร่"
|
1383 |
+
Output: {
|
1384 |
+
"event_type": "fees",
|
1385 |
+
"year": null,
|
1386 |
+
"semester": "เทอมที่ 1",
|
1387 |
+
"key_terms": ["ค่าเล่าเรียน", "เทอม 1"],
|
1388 |
+
"response_format": "detailed",
|
1389 |
+
"uncertainty": "low"
|
1390 |
+
}
|
1391 |
+
|
1392 |
+
Input: "ปี 1 เทอม 1 ต้องเรียนอะไรบ้าง"
|
1393 |
+
Output: {
|
1394 |
+
"event_type": "study_plan",
|
1395 |
+
"year": null,
|
1396 |
+
"semester": null,
|
1397 |
+
"key_terms": ["เรียนอะไร", "เทอม"],
|
1398 |
+
"response_format": "detailed",
|
1399 |
+
"uncertainty": "low"
|
1400 |
+
}
|
1401 |
+
|
1402 |
+
กรุณาตอบเป็นภาษาไทยและตรวจสอบให้แน่ใจว่า JSON มีโครงสร้างที่ถูกต้อง
|
1403 |
+
"""
|
1404 |
+
)
|
1405 |
+
|
1406 |
+
def normalize_year_semester(self, query: str) -> str:
|
1407 |
+
"""Normalize year and semester formats in queries"""
|
1408 |
+
# Year patterns
|
1409 |
+
year_patterns = {
|
1410 |
+
r'ปี\s*(\d+)': r'ปีที่ \1',
|
1411 |
+
r'ชั้นปีที่\s*(\d+)': r'ปีที่ \1',
|
1412 |
+
r'ปีการศึกษาที่\s*(\d+)': r'ปีที่ \1'
|
1413 |
+
}
|
1414 |
+
# Semester patterns
|
1415 |
+
semester_patterns = {
|
1416 |
+
r'เทอม\s*(\d+)': r'เทอมที่ \1',
|
1417 |
+
r'ภาคเรียนที่\s*(\d+)': r'เทอมที่ \1',
|
1418 |
+
r'ภาคการศึกษาที่\s*(\d+)': r'เทอมที่ \1'
|
1419 |
+
}
|
1420 |
+
normalized_query = query
|
1421 |
+
for pattern, replacement in year_patterns.items():
|
1422 |
+
normalized_query = re.sub(pattern, replacement, normalized_query)
|
1423 |
+
for pattern, replacement in semester_patterns.items():
|
1424 |
+
normalized_query = re.sub(pattern, replacement, normalized_query)
|
1425 |
+
return normalized_query
|
1426 |
+
|
1427 |
+
def normalize_query(self, query: str) -> str:
|
1428 |
+
"""เพิ่มการเปลี่ยนแปลงคำ (synonym mapping) เพื่อลดปัญหา Vocabulary Mismatch"""
|
1429 |
+
normalized_query = self.normalize_year_semester(query)
|
1430 |
+
# เพิ่ม mapping สำหรับคำที่มีความหมายเดียวกัน
|
1431 |
+
synonyms = {
|
1432 |
+
"วิชาเลือก": "หมวดวิชาเลือก"
|
1433 |
+
# สามารถเพิ่มคำอื่น ๆ ได้ตามต้องการ
|
1434 |
+
}
|
1435 |
+
for original, replacement in synonyms.items():
|
1436 |
+
normalized_query = normalized_query.replace(original, replacement)
|
1437 |
+
return normalized_query
|
1438 |
+
|
1439 |
+
def _get_default_analysis(self, query: str) -> Dict[str, Any]:
|
1440 |
+
logger.info("Returning default analysis")
|
1441 |
+
return {
|
1442 |
+
"original_query": query,
|
1443 |
+
"event_type": None,
|
1444 |
+
"semester": None,
|
1445 |
+
"key_terms": [],
|
1446 |
+
"response_format": "detailed"
|
1447 |
+
}
|
1448 |
+
|
1449 |
+
def process_query(self, query: str) -> Dict[str, Any]:
|
1450 |
+
"""Enhanced query processing with support for detail types and better categorization."""
|
1451 |
+
try:
|
1452 |
+
# ใช้ normalize_query ที่แก้ไขแล้วเพื่อให้คำค้นมีรูปแบบที่ตรงกับดัชนีข้อมูล
|
1453 |
+
normalized_query = self.normalize_query(query)
|
1454 |
+
result = self.prompt_builder.run(query=normalized_query)
|
1455 |
+
response = self.generator.run(prompt=result["prompt"])
|
1456 |
+
|
1457 |
+
if not response or not response.get("replies") or not response["replies"][0]:
|
1458 |
+
logger.warning("Received empty response from OpenAI")
|
1459 |
+
return self._get_default_analysis(query)
|
1460 |
+
|
1461 |
+
# ทำความสะอาด JSON string
|
1462 |
+
json_str = response["replies"][0]
|
1463 |
+
json_str = json_str.replace("```json", "").replace("```", "").strip()
|
1464 |
+
analysis = json.loads(json_str)
|
1465 |
+
|
1466 |
+
analysis['detail_type'] = None
|
1467 |
+
|
1468 |
+
# Enhanced categorization with detail types
|
1469 |
+
if any(keyword in query.lower() for keyword in ['ภาษาอังกฤษ', 'toefl', 'ielts', 'swu-set', 'โทอิค', 'คะแนนภาษา']):
|
1470 |
+
analysis['event_type'] = 'program_details'
|
1471 |
+
elif any(keyword in query.lower() for keyword in ['สมัคร', 'ขั้นตอน', 'วิธีการ', 'เอกสาร', 'หลักฐาน', 'admission']):
|
1472 |
+
analysis['event_type'] = 'program_details'
|
1473 |
+
analysis['detail_type'] = None
|
1474 |
+
elif any(keyword in query.lower() for keyword in ['ค่าเทอม', 'ค่าธรรมเนียม', 'ค่าเรียน', 'ค่าปรับ', 'ค่าใช้จ่าย']):
|
1475 |
+
analysis['event_type'] = 'fees'
|
1476 |
+
elif any(keyword in query.lower() for keyword in ['หน่วยกิต', 'วิชา', 'หลักสูตร', 'แผนการเรียน', 'วิชาเลือก', 'วิชาบังคับ', 'วิชาหลัก', 'หมวดวิชา']):
|
1477 |
+
analysis['event_type'] = 'curriculum'
|
1478 |
+
return {
|
1479 |
+
"original_query": query,
|
1480 |
+
**analysis
|
1481 |
+
}
|
1482 |
+
except Exception as e:
|
1483 |
+
logger.error(f"Query processing failed: {str(e)}")
|
1484 |
+
return self._get_default_analysis(query)
|
1485 |
+
|
1486 |
+
|
1487 |
class AcademicCalendarRAG:
|
1488 |
"""Enhanced RAG system for academic calendar and program information"""
|
1489 |
|
|
|
1518 |
self.study_plans = self.data_processor.extract_program_study_plan(json_data)
|
1519 |
self.tuition_fees = self.data_processor.extract_fees(json_data)
|
1520 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1521 |
self.document_store.add_events(
|
1522 |
events=self.calendar_events,
|
1523 |
+
program_details=self.program_details,
|
1524 |
contact_details=self.contact_details,
|
1525 |
course_structure=self.course_structure,
|
1526 |
+
study_plans=self.study_plans,
|
1527 |
+
tuition_fees=self.tuition_fees
|
1528 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1529 |
|
1530 |
+
except Exception as e:
|
1531 |
+
logger.error(f"Error loading data: {str(e)}")
|
1532 |
+
raise
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1533 |
|
1534 |
+
def process_query(self, query: str) -> Dict[str, Any]:
|
1535 |
+
"""Process user query using hybrid retrieval with dynamic retries."""
|
1536 |
+
max_attempts = 4 # Allow up to 4 attempts
|
1537 |
+
attempt = 0
|
1538 |
+
weight_values = [0.3, 0.7, 0.3, 0.7] # Switching semantic retrieval weight
|
1539 |
+
|
1540 |
+
while attempt < max_attempts:
|
1541 |
+
attempt += 1
|
1542 |
+
try:
|
1543 |
+
# Analyze query
|
1544 |
+
if attempt <= 2:
|
1545 |
+
query_info = self.query_processor.process_query(query) # Use normal query analysis for first 2 attempts
|
1546 |
+
else:
|
1547 |
+
query_info = self.query_processor._get_default_analysis(query) # Use default analysis for last 2 attempts
|
1548 |
+
logger.info(f"Retrying query processing (attempt {attempt}) with default analysis")
|
1549 |
+
|
1550 |
+
weight_semantic = weight_values[attempt - 1] # Adjust weight for semantic search dynamically
|
1551 |
+
|
1552 |
+
# Get relevant documents using hybrid search
|
1553 |
+
logger.info(f"Attempt {attempt}: Searching with weight_semantic={weight_semantic}")
|
1554 |
+
documents = self.document_store.hybrid_search(
|
1555 |
+
query=query,
|
1556 |
+
event_type=query_info.get("event_type"),
|
1557 |
+
detail_type=query_info.get("detail_type"),
|
1558 |
+
semester=query_info.get("semester"),
|
1559 |
+
top_k=self.config.retriever.top_k,
|
1560 |
+
weight_semantic=weight_semantic
|
1561 |
+
)
|
1562 |
+
|
1563 |
+
# Generate response
|
1564 |
+
response = self.response_generator.generate_response(
|
1565 |
+
query=query,
|
1566 |
+
documents=documents,
|
1567 |
+
query_info=query_info
|
1568 |
+
).strip()
|
1569 |
+
|
1570 |
+
# If response indicates no relevant information, retry with adjusted approach
|
1571 |
+
if "ขออภัย ไม่พบข้อมูลที่เกี่ยวข้อง" in response and attempt < max_attempts:
|
1572 |
+
continue # Try again with new weight or default analysis
|
1573 |
+
|
1574 |
+
return {
|
1575 |
+
"query": query,
|
1576 |
+
"answer": response,
|
1577 |
+
"relevant_docs": documents,
|
1578 |
+
"query_info": query_info
|
1579 |
+
}
|
1580 |
+
|
1581 |
+
except Exception as e:
|
1582 |
+
logger.error(f"Error processing query: {str(e)}")
|
1583 |
+
|
1584 |
+
return {
|
1585 |
+
"query": query,
|
1586 |
+
"answer": "ขออภัย ไม่สามารถประมวลผลคำตอบได้ในขณะนี้",
|
1587 |
+
"error": "Maximum retry attempts reached"
|
1588 |
+
}
|
1589 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1590 |
# def main():
|
1591 |
# """Main function demonstrating hybrid retrieval"""
|
1592 |
# try:
|
|
|
1605 |
# pipeline.load_data(raw_data)
|
1606 |
|
1607 |
# # Test queries with different semantic weights
|
1608 |
+
# queries = ["ค่าเทอมเท่าไหร่","เปิดเรียนวันไหน","ขั้นตอนการสมัครที่สาขานี้มีอะไรบ้าง","ต้องใช้ระดับภาษาอังกฤษเท่าไหร่ในการสมัครเรียนที่นี้","ถ้าจะไปติดต่อมาหลายต้องลง mrt อะไร","มีวิชาหลักเเละวิชาเลือกออะไรบ้าง", "ปีที่ 1 เทอม 1 ต้องเรียนอะไรบ้าง", "ปีที่ 2 เทอม 1 ต้องเรียนอะไรบ้าง"]
|
1609 |
+
# # queries = ["ขั้นตอนการสมัคเรียนที่สาขานี้มีอะไรบ้าง" ,"ต้องใช้ระดับภาษาอังกฤษเท่าไหร่ในการสมัครเรียนที่นี้"]
|
1610 |
# print("=" * 80)
|
1611 |
|
1612 |
# for query in queries:
|
|
|
1618 |
# except Exception as e:
|
1619 |
# logger.error(f"Pipeline execution failed: {str(e)}")
|
1620 |
# raise
|
1621 |
+
|
1622 |
# if __name__ == "__main__":
|
1623 |
# main()
|