hungnha commited on
Commit
9681056
·
1 Parent(s): 4ff2e4d

fix chunking

Browse files
config/docling.yaml DELETED
@@ -1,9 +0,0 @@
1
- output_dir: data/docling_output
2
- use_ocr: false
3
- timeout: 300
4
-
5
- # PDF pipeline options
6
- pdf_pipeline:
7
- do_table_structure: true
8
- do_picture_classification: false
9
- do_picture_description: false
 
 
 
 
 
 
 
 
 
 
config/embedding.yaml DELETED
@@ -1,9 +0,0 @@
1
- # SiliconFlow Embedding Configuration
2
- api_base_url: https://api.siliconflow.com/v1
3
-
4
- # Embedding Model (Qwen3-Embedding-4B)
5
- # Rate limit L0: 2000 RPM, 1M TPM
6
- embedding:
7
- model_name: Qwen/Qwen3-Embedding-4B
8
- dimension: 2048
9
- batch_size: 16 # Giảm từ 32 → 16 để tránh rate limit
 
 
 
 
 
 
 
 
 
 
config/evaluation.yaml DELETED
@@ -1,22 +0,0 @@
1
- # Evaluation Configuration
2
-
3
- # LLM model for generating answers and scoring metrics
4
- llm:
5
- model: qwen/qwen3-32b
6
- temperature: 0
7
- timeout: 30
8
- max_retries: 2
9
-
10
- # Retrieval settings
11
- retrieval:
12
- top_k: 5
13
- initial_k: 100
14
-
15
- # Test data
16
- data:
17
- csv_path: data/data.csv
18
- sample_size: 0 # 0 = all samples
19
-
20
- # Output
21
- output:
22
- dir: evaluation/results
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
config/gradio.yaml DELETED
@@ -1,12 +0,0 @@
1
- # Gradio App Configuration
2
-
3
- # LLM settings (Groq API)
4
- llm:
5
- model: llama-3.1-8b-instant
6
- temperature: 0.0
7
- max_tokens: 4096
8
-
9
- # Server settings
10
- server:
11
- host: "0.0.0.0"
12
- port: 7860
 
 
 
 
 
 
 
 
 
 
 
 
 
config/retrieval.yaml DELETED
@@ -1,15 +0,0 @@
1
- # Retrieval Configuration
2
-
3
- # Reranker (SiliconFlow Qwen3-Reranker-4B)
4
- # Rate limit L0: 2000 RPM, 1M TPM
5
- reranker:
6
- api_base_url: https://api.siliconflow.com/v1
7
- model_name: Qwen/Qwen3-Reranker-4B
8
- top_n: 5
9
-
10
- # Hybrid Search Settings
11
- hybrid:
12
- initial_k: 50
13
- top_k: 5 # Số docs cuối cùng trả về
14
- vector_weight: 0.5 # Trọng số cho vector search
15
- bm25_weight: 0.5 # Trọng số cho BM25
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
config/vector_db.yaml DELETED
@@ -1,2 +0,0 @@
1
- persist_dir: data/chroma
2
- collection_name: hust_rag_collection
 
 
 
core/embeddings/chunk.py CHANGED
@@ -3,148 +3,498 @@ from __future__ import annotations
3
  import re
4
  from dataclasses import dataclass
5
  from pathlib import Path
6
- from typing import Any, Dict, List, Tuple
7
 
8
- import frontmatter
9
- from langchain_text_splitters import RecursiveCharacterTextSplitter
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
 
12
  @dataclass
13
- class ChunkConfig:
14
- max_chars: int = 1300
15
- overlap: int = 250
16
- min_chars: int = 100
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
 
 
 
 
18
 
19
- def _extract_sections(body: str) -> List[Tuple[str, str, List[str]]]:
20
- heading_re = re.compile(r"^(#+)\s+(.+?)\s*$", re.MULTILINE)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
 
22
- headings = [(m.start(), len(m.group(1)), m.group(2).strip()) for m in heading_re.finditer(body)]
 
 
 
23
 
24
- if not headings:
25
- return [("General", body.strip(), [])]
26
 
27
- sections = []
28
- stack: List[Tuple[int, str]] = []
 
29
 
30
- for i, (pos, level, title) in enumerate(headings):
31
- start = pos
32
- end = headings[i + 1][0] if i + 1 < len(headings) else len(body)
33
-
34
- heading_line_end = body.find("\n", pos)
35
- if heading_line_end == -1:
36
- heading_line_end = len(body)
37
- content = body[heading_line_end:end].strip()
38
-
39
- while stack and stack[-1][0] >= level:
40
- stack.pop()
41
-
42
- breadcrumbs = [s[1] for s in stack]
43
- stack.append((level, title))
44
-
45
- sections.append((title, content, breadcrumbs))
46
 
47
- return sections
 
 
 
 
48
 
49
 
50
- def chunk_markdown(
51
- md_text: str,
52
- *,
53
- source_path: str | Path | None = None,
54
- config: ChunkConfig | None = None,
55
- ) -> List[Dict[str, Any]]:
 
 
 
56
 
57
- cfg = config or ChunkConfig()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
 
59
- # Parse frontmatter
60
- post = frontmatter.loads(md_text or "")
61
- base_meta = dict(post.metadata or {})
62
- body = post.content or ""
63
 
64
- # Add source info
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  if source_path:
66
  p = Path(source_path)
67
- base_meta.setdefault("source_path", str(p))
68
- base_meta.setdefault("source_file", base_meta.get("source_file") or p.name)
69
- base_meta.setdefault("source_basename", p.name)
70
-
71
- doc_type = str(base_meta.get("document_type") or "").strip()
72
- if doc_type:
73
- base_meta.setdefault("type", doc_type)
74
-
75
- text_splitter = RecursiveCharacterTextSplitter(
76
- chunk_size=cfg.max_chars,
77
- chunk_overlap=cfg.overlap,
78
- separators=["\n\n", "\n", ". ", "; ", ", ", " ", ""],
79
- length_function=len,
80
- )
81
 
82
- sections = _extract_sections(body)
83
- raw_chunks: List[Dict[str, Any]] = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
 
85
- for title, content, breadcrumbs in sections:
86
- section_title = " > ".join(breadcrumbs + [title]) if breadcrumbs else title
 
 
 
 
 
87
 
88
- if len(content) > cfg.max_chars:
89
- sub_texts = text_splitter.split_text(content)
 
 
 
 
 
 
 
 
 
 
 
90
  else:
91
- sub_texts = [content] if content else []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
 
93
- for text in sub_texts:
94
- text = text.strip()
95
- if not text:
96
- continue
97
-
98
- md = dict(base_meta)
99
- md["section"] = section_title
 
 
 
 
 
 
 
100
 
101
- if "Học phần" in text:
102
- match = re.search(
103
- r"Học\s*phần\s+(.+?)\s*\(\s*m[ãa]\s+([^\)]+)\)",
104
- text, flags=re.IGNORECASE | re.DOTALL,
105
- )
106
- if match:
107
- md["course_name"] = " ".join(match.group(1).split())
108
- md["course_code"] = " ".join(match.group(2).split())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
 
110
- raw_chunks.append({"content": text, "metadata": md})
 
 
 
 
 
 
 
111
 
112
- chunks: List[Dict[str, Any]] = []
 
113
  i = 0
114
- while i < len(raw_chunks):
115
- current = raw_chunks[i]
 
116
 
117
- if len(current["content"]) < cfg.min_chars and i + 1 < len(raw_chunks):
118
- next_chunk = raw_chunks[i + 1]
119
- merged_content = current["content"] + "\n\n" + next_chunk["content"]
120
-
121
- # Combine section names if different
122
- if current["metadata"]["section"] != next_chunk["metadata"]["section"]:
123
- merged_section = f"{current['metadata']['section']} + {next_chunk['metadata']['section']}"
124
- else:
125
- merged_section = current["metadata"]["section"]
126
-
127
- merged_md = dict(next_chunk["metadata"])
128
- merged_md["section"] = merged_section
129
-
130
- chunks.append({"content": merged_content, "metadata": merged_md})
131
- i += 2 # Skip next chunk since we merged it
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
  else:
133
- chunks.append(current)
134
- i += 1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
 
136
- # Add chunk indices
137
- for idx, chunk in enumerate(chunks):
138
- chunk["metadata"]["chunk_index"] = idx
 
 
 
 
 
 
 
 
 
 
 
 
139
 
140
- return chunks
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
 
142
 
143
- def chunk_markdown_file(
144
- path: str | Path,
145
- *,
146
- config: ChunkConfig | None = None
147
- ) -> List[Dict[str, Any]]:
148
- """Chunk a Markdown file."""
 
 
 
 
149
  p = Path(path)
150
- return chunk_markdown(p.read_text(encoding="utf-8"), source_path=p, config=config)
 
 
 
 
 
 
 
3
  import re
4
  from dataclasses import dataclass
5
  from pathlib import Path
6
+ from typing import List, Tuple
7
 
8
+ from dotenv import load_dotenv
9
+ from llama_index.core import Document
10
+ from llama_index.core.node_parser import MarkdownNodeParser, SentenceSplitter
11
+ from llama_index.core.schema import BaseNode, TextNode
12
+
13
+ # Load environment variables
14
+ load_dotenv()
15
+
16
+ # Regex pattern for extracting course information
17
+ COURSE_INFO_PATTERN = re.compile(
18
+ r"Học\s*phần\s+(.+?)\s*\(\s*m[ãa]\s+([^\)]+)\)",
19
+ flags=re.IGNORECASE | re.DOTALL,
20
+ )
21
+
22
+ # Chunking configuration
23
+ CHUNK_SIZE = 1500
24
+ CHUNK_OVERLAP = 150
25
+ MIN_CHUNK_SIZE = 50
26
+ TABLE_ROWS_PER_CHUNK = 15 # Max rows per table chunk (excluding header)
27
 
28
 
29
  @dataclass
30
+ class TableBlock:
31
+ """Represents a markdown table with its position in text."""
32
+ start_idx: int
33
+ end_idx: int
34
+ header_lines: str # First 2 lines: header + separator
35
+ data_rows: List[str] # All data rows
36
+
37
+ @property
38
+ def full_table(self) -> str:
39
+ return self.header_lines + "\n".join(self.data_rows)
40
+
41
+ def get_chunks(self, max_rows: int = TABLE_ROWS_PER_CHUNK) -> List[str]:
42
+ """Split table into chunks, each containing header + subset of rows."""
43
+ if len(self.data_rows) <= max_rows:
44
+ return [self.full_table]
45
+
46
+ chunks = []
47
+ for i in range(0, len(self.data_rows), max_rows):
48
+ rows_subset = self.data_rows[i:i + max_rows]
49
+ chunk = self.header_lines + "\n".join(rows_subset)
50
+ chunks.append(chunk)
51
+ return chunks
52
+
53
 
54
+ def _is_table_row(line: str) -> bool:
55
+ """Check if a line is a markdown table row."""
56
+ stripped = line.strip()
57
+ return stripped.startswith("|") and stripped.endswith("|") and stripped.count("|") >= 2
58
 
59
+
60
+ def _is_separator_row(line: str) -> bool:
61
+ """Check if a line is a table separator (|---|---|)."""
62
+ stripped = line.strip()
63
+ if not _is_table_row(line):
64
+ return False
65
+ # Remove pipes and check if remaining is mostly dashes/colons
66
+ content = stripped.replace("|", "").replace("-", "").replace(":", "").replace(" ", "")
67
+ return len(content) == 0
68
+
69
+
70
+ def _is_real_header_row(line: str) -> bool:
71
+ """
72
+ Check if a table row is a real header (contains text labels) vs data row (contains numbers).
73
+ Real headers typically have column names like 'TT', 'MÃ SỐ', 'TÊN', etc.
74
+ Data rows typically start with numbers like '1', '26', '64'.
75
+ """
76
+ stripped = line.strip()
77
+ if not _is_table_row(line):
78
+ return False
79
 
80
+ # Get first cell content
81
+ cells = [c.strip() for c in stripped.split("|") if c.strip()]
82
+ if not cells:
83
+ return False
84
 
85
+ first_cell = cells[0]
 
86
 
87
+ # If first cell is purely numeric, it's likely a data row
88
+ if first_cell.isdigit():
89
+ return False
90
 
91
+ # If first cell contains common header keywords, it's a header
92
+ header_keywords = ['TT', 'STT', 'MÃ', 'TÊN', 'KHỐI', 'SỐ', 'ID', 'NO', '#']
93
+ first_cell_upper = first_cell.upper()
94
+ for keyword in header_keywords:
95
+ if keyword in first_cell_upper:
96
+ return True
 
 
 
 
 
 
 
 
 
 
97
 
98
+ # If first cell is short text (1-3 words) and not a number, likely header
99
+ if len(first_cell.split()) <= 3 and not any(c.isdigit() for c in first_cell[:2]):
100
+ return True
101
+
102
+ return False
103
 
104
 
105
+ def _extract_tables(text: str) -> Tuple[List[TableBlock], str]:
106
+ """
107
+ Extract all markdown tables from text.
108
+ Tables without proper headers inherit header from previous table.
109
+ Returns: (list of TableBlock, text with tables replaced by placeholders)
110
+ """
111
+ lines = text.split("\n")
112
+ tables: List[TableBlock] = []
113
+ last_valid_header: str | None = None
114
 
115
+ i = 0
116
+ while i < len(lines):
117
+ # Look for table: either header+separator or data+separator pattern
118
+ if i + 1 < len(lines) and _is_table_row(lines[i]) and _is_separator_row(lines[i + 1]):
119
+ table_start_line = i
120
+ potential_header = lines[i]
121
+ separator_row = lines[i + 1]
122
+
123
+ # Check if this is a real header or a data row
124
+ if _is_real_header_row(potential_header):
125
+ # This is a proper header
126
+ header_lines = potential_header + "\n" + separator_row + "\n"
127
+ last_valid_header = header_lines
128
+ data_start = i + 2
129
+ else:
130
+ # This row looks like data, not header
131
+ # Use last valid header if available, otherwise create generic one
132
+ if last_valid_header:
133
+ header_lines = last_valid_header
134
+ else:
135
+ # Create a generic header based on column count
136
+ col_count = potential_header.count("|") - 1
137
+ generic_header = "| " + " | ".join([f"Col{j+1}" for j in range(col_count)]) + " |"
138
+ generic_sep = "| " + " | ".join(["---"] * col_count) + " |"
139
+ header_lines = generic_header + "\n" + generic_sep + "\n"
140
+
141
+ # Include the "header" row as first data row since it's actually data
142
+ data_start = i
143
+
144
+ # Collect all data rows (including separator if we started from data row)
145
+ data_rows = []
146
+ j = data_start
147
+ while j < len(lines):
148
+ if _is_table_row(lines[j]) and not _is_separator_row(lines[j]):
149
+ data_rows.append(lines[j])
150
+ j += 1
151
+ elif _is_separator_row(lines[j]):
152
+ # Skip separator rows in the middle of table
153
+ j += 1
154
+ else:
155
+ break
156
+
157
+ if data_rows: # Valid table with at least one data row
158
+ # Calculate character positions
159
+ start_idx = sum(len(lines[k]) + 1 for k in range(table_start_line))
160
+ end_idx = sum(len(lines[k]) + 1 for k in range(j))
161
+
162
+ tables.append(TableBlock(
163
+ start_idx=start_idx,
164
+ end_idx=end_idx,
165
+ header_lines=header_lines,
166
+ data_rows=data_rows,
167
+ ))
168
+ i = j
169
+ else:
170
+ i += 1
171
+ else:
172
+ i += 1
173
 
174
+ # Create text with placeholders
175
+ if not tables:
176
+ return tables, text
 
177
 
178
+ result_lines = []
179
+ table_idx = 0
180
+ i = 0
181
+
182
+ while i < len(lines):
183
+ if table_idx < len(tables):
184
+ # Check if we're at a table start (header+sep or data+sep)
185
+ if i + 1 < len(lines) and _is_table_row(lines[i]) and _is_separator_row(lines[i + 1]):
186
+ # Skip all table rows (including separators in the middle)
187
+ j = i
188
+ while j < len(lines):
189
+ if _is_table_row(lines[j]) or _is_separator_row(lines[j]):
190
+ j += 1
191
+ else:
192
+ break
193
+
194
+ # Add placeholder
195
+ result_lines.append(f"__TABLE_PLACEHOLDER_{table_idx}__")
196
+ table_idx += 1
197
+ i = j
198
+ continue
199
+
200
+ result_lines.append(lines[i])
201
+ i += 1
202
+
203
+ return tables, "\n".join(result_lines)
204
+
205
+
206
+ def _get_markdown_parser() -> MarkdownNodeParser:
207
+ return MarkdownNodeParser()
208
+
209
+
210
+ def _get_sentence_splitter() -> SentenceSplitter:
211
+ return SentenceSplitter(
212
+ chunk_size=CHUNK_SIZE,
213
+ chunk_overlap=CHUNK_OVERLAP,
214
+ )
215
+
216
+
217
+ def _extract_course_metadata(text: str) -> dict[str, str]:
218
+ """Extract course name and code from Vietnamese text."""
219
+ metadata: dict[str, str] = {}
220
+
221
+ if "Học phần" in text:
222
+ match = COURSE_INFO_PATTERN.search(text)
223
+ if match:
224
+ metadata["course_name"] = " ".join(match.group(1).split())
225
+ metadata["course_code"] = " ".join(match.group(2).split())
226
+
227
+ return metadata
228
+
229
+
230
+ def _enrich_node_metadata(
231
+ node: BaseNode,
232
+ source_path: str | Path | None = None,
233
+ ) -> None:
234
+ """Add source and course metadata to node."""
235
  if source_path:
236
  p = Path(source_path)
237
+ node.metadata["source_path"] = str(p)
238
+ node.metadata["source_file"] = p.name
 
 
 
 
 
 
 
 
 
 
 
 
239
 
240
+ text = node.get_content()
241
+ course_meta = _extract_course_metadata(text)
242
+ node.metadata.update(course_meta)
243
+
244
+
245
+ def _has_markdown_table(text: str) -> bool:
246
+ """Check if text contains a markdown table."""
247
+ lines = text.split("\n")
248
+ for i in range(len(lines) - 1):
249
+ if _is_table_row(lines[i]) and _is_separator_row(lines[i + 1]):
250
+ return True
251
+ return False
252
+
253
+
254
+ def _merge_small_nodes(nodes: List[BaseNode]) -> List[BaseNode]:
255
+ """Merge nodes smaller than MIN_CHUNK_SIZE with next node."""
256
+ if not nodes:
257
+ return []
258
 
259
+ merged_nodes: List[BaseNode] = []
260
+ i = 0
261
+
262
+ while i < len(nodes):
263
+ current = nodes[i]
264
+ current_content = current.get_content()
265
+ current_len = len(current_content)
266
 
267
+ if current_len < MIN_CHUNK_SIZE and i + 1 < len(nodes):
268
+ next_node = nodes[i + 1]
269
+ next_content = next_node.get_content()
270
+ merged_content = current_content + "\n\n" + next_content
271
+ merged_metadata = current.metadata.copy()
272
+ merged_metadata.update(next_node.metadata)
273
+
274
+ merged_node = TextNode(
275
+ text=merged_content,
276
+ metadata=merged_metadata,
277
+ )
278
+ merged_nodes.append(merged_node)
279
+ i += 2
280
  else:
281
+ merged_nodes.append(current)
282
+ i += 1
283
+
284
+ return merged_nodes
285
+
286
+
287
+ def _chunk_text_section(text: str, metadata: dict) -> List[BaseNode]:
288
+ """Chunk a text section (no tables) using SentenceSplitter."""
289
+ if len(text) <= CHUNK_SIZE:
290
+ return [TextNode(text=text, metadata=metadata.copy())]
291
+
292
+ splitter = _get_sentence_splitter()
293
+ doc = Document(text=text, metadata=metadata.copy())
294
+ nodes = splitter.get_nodes_from_documents([doc])
295
+ return nodes
296
+
297
+
298
+ def _chunk_with_table_awareness(
299
+ doc: Document,
300
+ source_path: str | Path | None = None,
301
+ ) -> List[BaseNode]:
302
+ """
303
+ Chunk markdown with table boundary awareness.
304
+
305
+ Strategy:
306
+ 1. Extract all tables from document
307
+ 2. Split remaining text by markdown headings
308
+ 3. Further split large text sections with SentenceSplitter
309
+ 4. Split large tables by rows (keeping header in each chunk)
310
+ 5. Reassemble in original order
311
+ """
312
+ text = doc.get_content()
313
+ base_metadata = doc.metadata.copy()
314
+
315
+ # Step 1: Extract tables
316
+ tables, text_with_placeholders = _extract_tables(text)
317
+
318
+ if not tables:
319
+ # No tables - use simple chunking
320
+ return _chunk_simple(doc, source_path)
321
+
322
+ # Step 2: Split text by markdown headings
323
+ md_parser = _get_markdown_parser()
324
+ placeholder_doc = Document(text=text_with_placeholders, metadata=base_metadata.copy())
325
+ heading_nodes = md_parser.get_nodes_from_documents([placeholder_doc])
326
+
327
+ # Step 3: Process each heading node
328
+ all_nodes: List[BaseNode] = []
329
+
330
+ for node in heading_nodes:
331
+ node_text = node.get_content()
332
+ node_metadata = node.metadata.copy()
333
 
334
+ # Check if this node contains table placeholders
335
+ placeholder_pattern = re.compile(r"__TABLE_PLACEHOLDER_(\d+)__")
336
+ matches = list(placeholder_pattern.finditer(node_text))
337
+
338
+ if not matches:
339
+ # No tables in this node - chunk normally
340
+ if len(node_text) > CHUNK_SIZE:
341
+ sub_nodes = _chunk_text_section(node_text, node_metadata)
342
+ all_nodes.extend(sub_nodes)
343
+ else:
344
+ all_nodes.append(TextNode(text=node_text, metadata=node_metadata))
345
+ else:
346
+ # Has tables - split around placeholders
347
+ last_end = 0
348
 
349
+ for match in matches:
350
+ # Text before placeholder
351
+ text_before = node_text[last_end:match.start()].strip()
352
+ if text_before:
353
+ if len(text_before) > CHUNK_SIZE:
354
+ sub_nodes = _chunk_text_section(text_before, node_metadata)
355
+ all_nodes.extend(sub_nodes)
356
+ elif len(text_before) >= MIN_CHUNK_SIZE:
357
+ all_nodes.append(TextNode(text=text_before, metadata=node_metadata.copy()))
358
+
359
+ # Table chunks
360
+ table_idx = int(match.group(1))
361
+ if table_idx < len(tables):
362
+ table = tables[table_idx]
363
+ table_chunks = table.get_chunks(TABLE_ROWS_PER_CHUNK)
364
+
365
+ for i, chunk in enumerate(table_chunks):
366
+ table_metadata = node_metadata.copy()
367
+ table_metadata["is_table"] = True
368
+ if len(table_chunks) > 1:
369
+ table_metadata["table_part"] = f"{i + 1}/{len(table_chunks)}"
370
+ all_nodes.append(TextNode(text=chunk, metadata=table_metadata))
371
+
372
+ last_end = match.end()
373
 
374
+ # Text after last placeholder
375
+ text_after = node_text[last_end:].strip()
376
+ if text_after:
377
+ if len(text_after) > CHUNK_SIZE:
378
+ sub_nodes = _chunk_text_section(text_after, node_metadata)
379
+ all_nodes.extend(sub_nodes)
380
+ elif len(text_after) >= MIN_CHUNK_SIZE:
381
+ all_nodes.append(TextNode(text=text_after, metadata=node_metadata.copy()))
382
 
383
+ # Step 4: Merge small nodes (but not tables)
384
+ final_nodes: List[BaseNode] = []
385
  i = 0
386
+ while i < len(all_nodes):
387
+ current = all_nodes[i]
388
+ is_table = current.metadata.get("is_table", False)
389
 
390
+ if not is_table and len(current.get_content()) < MIN_CHUNK_SIZE:
391
+ # Try to merge with next non-table node
392
+ if i + 1 < len(all_nodes):
393
+ next_node = all_nodes[i + 1]
394
+ next_is_table = next_node.metadata.get("is_table", False)
395
+
396
+ if not next_is_table:
397
+ merged_content = current.get_content() + "\n\n" + next_node.get_content()
398
+ merged_metadata = current.metadata.copy()
399
+ merged_metadata.update(next_node.metadata)
400
+ final_nodes.append(TextNode(text=merged_content, metadata=merged_metadata))
401
+ i += 2
402
+ continue
403
+
404
+ final_nodes.append(current)
405
+ i += 1
406
+
407
+ # Step 5: Enrich metadata
408
+ for node in final_nodes:
409
+ _enrich_node_metadata(node, source_path)
410
+
411
+ return final_nodes
412
+
413
+
414
+ def _chunk_simple(doc: Document, source_path: str | Path | None = None) -> List[BaseNode]:
415
+ """Simple chunking for documents without tables."""
416
+ md_parser = _get_markdown_parser()
417
+ heading_nodes = md_parser.get_nodes_from_documents([doc])
418
+
419
+ sentence_splitter = _get_sentence_splitter()
420
+ split_nodes: List[BaseNode] = []
421
+
422
+ for node in heading_nodes:
423
+ content_len = len(node.get_content())
424
+ if content_len > CHUNK_SIZE:
425
+ sub_nodes = sentence_splitter.get_nodes_from_documents(
426
+ [Document(text=node.get_content(), metadata=node.metadata.copy())]
427
+ )
428
+ split_nodes.extend(sub_nodes)
429
  else:
430
+ split_nodes.append(node)
431
+
432
+ merged_nodes = _merge_small_nodes(split_nodes)
433
+
434
+ for node in merged_nodes:
435
+ _enrich_node_metadata(node, source_path)
436
+
437
+ return merged_nodes
438
+
439
+
440
+ def chunk_markdown(
441
+ text: str,
442
+ source_path: str | Path | None = None,
443
+ ) -> List[BaseNode]:
444
+ """
445
+ Chunk markdown text with table-aware splitting.
446
 
447
+ Features:
448
+ - Preserves complete tables in chunks
449
+ - Splits large tables by rows while keeping headers
450
+ - Uses markdown heading structure for text sections
451
+ - Merges small chunks for better context
452
+
453
+ Args:
454
+ text: Markdown text to chunk
455
+ source_path: Optional source file path for metadata
456
+
457
+ Returns:
458
+ List of TextNode chunks with metadata
459
+ """
460
+ if not text or not text.strip():
461
+ return []
462
 
463
+ doc = Document(text=text)
464
+
465
+ if source_path:
466
+ p = Path(source_path)
467
+ doc.metadata["source_path"] = str(p)
468
+ doc.metadata["source_file"] = p.name
469
+
470
+ # Use table-aware chunking
471
+ if _has_markdown_table(text):
472
+ all_nodes = _chunk_with_table_awareness(doc, source_path)
473
+ else:
474
+ all_nodes = _chunk_simple(doc, source_path)
475
+
476
+ # Add chunk index
477
+ for idx, node in enumerate(all_nodes):
478
+ node.metadata["chunk_index"] = idx
479
+
480
+ return all_nodes
481
 
482
 
483
+ def chunk_markdown_file(path: str | Path) -> List[BaseNode]:
484
+ """
485
+ Chunk a markdown file with table-aware splitting.
486
+
487
+ Args:
488
+ path: Path to markdown file
489
+
490
+ Returns:
491
+ List of TextNode chunks with metadata
492
+ """
493
  p = Path(path)
494
+ if not p.exists():
495
+ raise FileNotFoundError(f"File not found: {p}")
496
+ if not p.is_file():
497
+ raise ValueError(f"Path is not a file: {p}")
498
+
499
+ text = p.read_text(encoding="utf-8")
500
+ return chunk_markdown(text, source_path=p)
core/embeddings/embedding_model.py CHANGED
@@ -2,39 +2,20 @@ from __future__ import annotations
2
  import os
3
  import logging
4
  from dataclasses import dataclass
5
- from pathlib import Path
6
  from typing import List, Sequence
7
  import numpy as np
8
  from openai import OpenAI
9
  from langchain_core.embeddings import Embeddings
10
- from utils.helpers import read_yaml
11
  import time
12
  logger = logging.getLogger(__name__)
13
 
14
 
15
  @dataclass
16
  class EmbeddingConfig:
17
- api_base_url: str
18
- model: str
19
- dimension: int
20
- batch_size: int
21
-
22
- @staticmethod
23
- def default_yaml_path() -> Path:
24
- return Path(__file__).resolve().parents[2] / "config" / "embedding.yaml"
25
-
26
- @classmethod
27
- def from_yaml(cls, path: str | Path | None = None) -> "EmbeddingConfig":
28
- cfg_path = Path(path) if path is not None else cls.default_yaml_path()
29
- data = read_yaml(cfg_path)
30
- emb = data["embedding"]
31
-
32
- return cls(
33
- api_base_url=data["api_base_url"],
34
- model=emb["model_name"],
35
- dimension=emb["dimension"],
36
- batch_size=emb["batch_size"],
37
- )
38
 
39
 
40
  _embed_config: EmbeddingConfig | None = None
@@ -42,7 +23,7 @@ _embed_config: EmbeddingConfig | None = None
42
  def get_embedding_config() -> EmbeddingConfig:
43
  global _embed_config
44
  if _embed_config is None:
45
- _embed_config = EmbeddingConfig.from_yaml()
46
  return _embed_config
47
 
48
 
 
2
  import os
3
  import logging
4
  from dataclasses import dataclass
 
5
  from typing import List, Sequence
6
  import numpy as np
7
  from openai import OpenAI
8
  from langchain_core.embeddings import Embeddings
 
9
  import time
10
  logger = logging.getLogger(__name__)
11
 
12
 
13
  @dataclass
14
  class EmbeddingConfig:
15
+ api_base_url: str = "https://api.siliconflow.com/v1"
16
+ model: str = "Qwen/Qwen3-Embedding-4B"
17
+ dimension: int = 2048
18
+ batch_size: int = 16
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
 
20
 
21
  _embed_config: EmbeddingConfig | None = None
 
23
  def get_embedding_config() -> EmbeddingConfig:
24
  global _embed_config
25
  if _embed_config is None:
26
+ _embed_config = EmbeddingConfig()
27
  return _embed_config
28
 
29
 
core/embeddings/generator.py ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+ import os
3
+ from pathlib import Path
4
+ from typing import Any, Dict, Generator, List, Optional, TYPE_CHECKING
5
+
6
+ from groq import Groq
7
+
8
+ if TYPE_CHECKING:
9
+ from core.embeddings.retrival import Retriever
10
+
11
+
12
+ SYSTEM_PROMPT = """Bạn là Trợ lý học vụ Đại học Bách khoa Hà Nội.
13
+
14
+ ## NGUYÊN TẮC:
15
+ 1. Chỉ trả lời dựa trên CONTEXT được cung cấp. Không suy đoán, không bổ sung thông tin ngoài CONTEXT.
16
+ 2. Nếu trong CONTEXT có nội dung về "Hiệu lực thi hành" hoặc "Điều khoản chuyển tiếp", hãy nêu rõ phạm vi áp dụng (theo khóa hoặc thời gian) đúng như nội dung đã nêu. Nếu CONTEXT không đề cập, không tự suy luận.
17
+ 3. Nếu CONTEXT chứa nhiều văn bản khác nhau, ưu tiên nội dung được nêu là đang áp dụng, hoặc có ghi rõ thời điểm hiệu lực. Không tự xác định văn bản mới/cũ nếu CONTEXT không nói rõ.
18
+ 4. Cuối câu trả lời, trích dẫn nguồn đúng theo tài liệu xuất hiện trong CONTEXT. Không tự tạo nguồn.
19
+ 5. Nếu không tìm thấy thông tin trong CONTEXT, trả lời: "Không tìm thấy thông tin trong dữ liệu hiện có."
20
+ """
21
+
22
+
23
+ def build_context(results: List[Dict[str, Any]], max_chars: int = 5000) -> str:
24
+ """Build context string from retrieval results."""
25
+ parts = []
26
+ for i, r in enumerate(results, 1):
27
+ section = r.get("metadata", {}).get("section", "N/A")
28
+ content = r.get("content", "").strip()[:600]
29
+ parts.append(f"[TÀI LIỆU {i}]\n- Phần/Điều: {section}\n{content}")
30
+
31
+ context = "\n---\n".join(parts)
32
+ return context[:max_chars] if len(context) > max_chars else context
33
+
34
+
35
+ class RAGGenerator:
36
+ """RAG Answer Generator using Groq LLM."""
37
+
38
+ def __init__(
39
+ self,
40
+ retriever: "Retriever",
41
+ llm_model: str = "qwen/qwen3-32b",
42
+ temperature: float = 0.0,
43
+ max_tokens: int = 4096,
44
+ max_context_chars: int = 5000,
45
+ groq_client: Optional[Groq] = None,
46
+ ):
47
+ self._retriever = retriever
48
+ self._llm_model = llm_model
49
+ self._temperature = temperature
50
+ self._max_tokens = max_tokens
51
+ self._max_context_chars = max_context_chars
52
+ api_key = (os.getenv("GROQ_API_KEY") or "").strip()
53
+ if not api_key and groq_client is None:
54
+ raise RuntimeError("Missing GROQ_API_KEY")
55
+ self._groq = groq_client or Groq(api_key=api_key)
56
+
57
+ def _build_prompt(self, question: str, context: str) -> str:
58
+ """Build prompt with system prompt, context, and question."""
59
+ return f"{SYSTEM_PROMPT}\n\n## CONTEXT:\n{context}\n\n## CÂU HỎI: {question}\n\n## TRẢ LỜI:"
60
+
61
+ def generate(self, question: str, k: int = 5, initial_k: int = 20) -> Dict[str, Any]:
62
+ """Generate answer (non-streaming, for evaluation)."""
63
+ results = self._retriever.search_with_rerank(question, k=k, initial_k=initial_k)
64
+
65
+ if not results:
66
+ return {
67
+ "answer": "Không tìm thấy thông tin trong dữ liệu hiện có.",
68
+ "contexts": [],
69
+ "results": [],
70
+ }
71
+
72
+ context = build_context(results, self._max_context_chars)
73
+ prompt = self._build_prompt(question, context)
74
+
75
+ completion = self._groq.chat.completions.create(
76
+ model=self._llm_model,
77
+ messages=[{"role": "user", "content": prompt}],
78
+ temperature=self._temperature,
79
+ max_completion_tokens=self._max_tokens,
80
+ )
81
+
82
+ return {
83
+ "answer": completion.choices[0].message.content or "",
84
+ "contexts": [r.get("content", "")[:1000] for r in results],
85
+ "results": results,
86
+ }
87
+
88
+ def generate_stream(
89
+ self, question: str, k: int = 5, initial_k: int = 20
90
+ ) -> Generator[str, None, None]:
91
+ """Generate answer with streaming (for Gradio)."""
92
+ results = self._retriever.search_with_rerank(question, k=k, initial_k=initial_k)
93
+
94
+ if not results:
95
+ yield "Không tìm thấy thông tin trong dữ liệu hiện có."
96
+ return
97
+
98
+ context = build_context(results, self._max_context_chars)
99
+ prompt = self._build_prompt(question, context)
100
+
101
+ completion = self._groq.chat.completions.create(
102
+ model=self._llm_model,
103
+ messages=[{"role": "user", "content": prompt}],
104
+ temperature=self._temperature,
105
+ max_completion_tokens=self._max_tokens,
106
+ stream=True,
107
+ )
108
+
109
+ acc = ""
110
+ for chunk in completion:
111
+ delta = getattr(chunk.choices[0].delta, "content", "") or ""
112
+ if delta:
113
+ acc += delta
114
+ yield acc
core/embeddings/retrival.py CHANGED
@@ -2,7 +2,6 @@ from __future__ import annotations
2
  import os
3
  import logging
4
  from dataclasses import dataclass
5
- from pathlib import Path
6
  from typing import Any, Dict, List, Optional, Sequence, TYPE_CHECKING
7
  import requests
8
  from pydantic import Field
@@ -12,7 +11,6 @@ from langchain_core.documents.compressor import BaseDocumentCompressor
12
  from langchain_classic.retrievers import ContextualCompressionRetriever
13
  from langchain_classic.retrievers.ensemble import EnsembleRetriever
14
  from langchain_community.retrievers import BM25Retriever
15
- from utils.helpers import read_yaml
16
 
17
  if TYPE_CHECKING:
18
  from core.embeddings.vector_store import ChromaVectorDB
@@ -22,34 +20,13 @@ logger = logging.getLogger(__name__)
22
 
23
  @dataclass
24
  class RetrievalConfig:
25
- rerank_api_base_url: str
26
- rerank_model: str
27
- rerank_top_n: int
28
- initial_k: int
29
- top_k: int
30
- vector_weight: float
31
- bm25_weight: float
32
-
33
- @staticmethod
34
- def default_yaml_path() -> Path:
35
- return Path(__file__).resolve().parents[2] / "config" / "retrieval.yaml"
36
-
37
- @classmethod
38
- def from_yaml(cls, path: str | Path | None = None) -> "RetrievalConfig":
39
- cfg_path = Path(path) if path is not None else cls.default_yaml_path()
40
- data = read_yaml(cfg_path)
41
- rerank = data["reranker"]
42
- hybrid = data["hybrid"]
43
-
44
- return cls(
45
- rerank_api_base_url=rerank["api_base_url"],
46
- rerank_model=rerank["model_name"],
47
- rerank_top_n=rerank["top_n"],
48
- initial_k=hybrid["initial_k"],
49
- top_k=hybrid["top_k"],
50
- vector_weight=hybrid["vector_weight"],
51
- bm25_weight=hybrid["bm25_weight"],
52
- )
53
 
54
 
55
  _retrieval_config: RetrievalConfig | None = None
@@ -57,7 +34,7 @@ _retrieval_config: RetrievalConfig | None = None
57
  def get_retrieval_config() -> RetrievalConfig:
58
  global _retrieval_config
59
  if _retrieval_config is None:
60
- _retrieval_config = RetrievalConfig.from_yaml()
61
  return _retrieval_config
62
 
63
 
@@ -134,6 +111,8 @@ class SiliconFlowReranker(BaseDocumentCompressor):
134
  logger.error(f"Rerank error: {e}")
135
  return list(documents)
136
 
 
 
137
 
138
  class Retriever:
139
  def __init__(
@@ -184,8 +163,8 @@ class Retriever:
184
  return None
185
 
186
  def _init_ensemble_retriever(self) -> EnsembleRetriever:
187
- retrievers = [self._vector_retriever]
188
- weights = [1.0]
189
 
190
  if self._bm25_retriever:
191
  retrievers.append(self._bm25_retriever)
 
2
  import os
3
  import logging
4
  from dataclasses import dataclass
 
5
  from typing import Any, Dict, List, Optional, Sequence, TYPE_CHECKING
6
  import requests
7
  from pydantic import Field
 
11
  from langchain_classic.retrievers import ContextualCompressionRetriever
12
  from langchain_classic.retrievers.ensemble import EnsembleRetriever
13
  from langchain_community.retrievers import BM25Retriever
 
14
 
15
  if TYPE_CHECKING:
16
  from core.embeddings.vector_store import ChromaVectorDB
 
20
 
21
  @dataclass
22
  class RetrievalConfig:
23
+ rerank_api_base_url: str = "https://api.siliconflow.com/v1"
24
+ rerank_model: str = "Qwen/Qwen3-Reranker-4B"
25
+ rerank_top_n: int = 5
26
+ initial_k: int = 50
27
+ top_k: int = 5
28
+ vector_weight: float = 0.5
29
+ bm25_weight: float = 0.5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
 
31
 
32
  _retrieval_config: RetrievalConfig | None = None
 
34
  def get_retrieval_config() -> RetrievalConfig:
35
  global _retrieval_config
36
  if _retrieval_config is None:
37
+ _retrieval_config = RetrievalConfig()
38
  return _retrieval_config
39
 
40
 
 
111
  logger.error(f"Rerank error: {e}")
112
  return list(documents)
113
 
114
+ return list(documents)
115
+
116
 
117
  class Retriever:
118
  def __init__(
 
163
  return None
164
 
165
  def _init_ensemble_retriever(self) -> EnsembleRetriever:
166
+ retrievers: List[Any] = [self._vector_retriever]
167
+ weights: List[float] = [1.0]
168
 
169
  if self._bm25_retriever:
170
  retrievers.append(self._bm25_retriever)
core/embeddings/vector_store.py CHANGED
@@ -1,49 +1,24 @@
1
  from __future__ import annotations
2
  import json
3
  import logging
4
- from dataclasses import dataclass
5
  from pathlib import Path
6
  from typing import Any, Dict, List, Optional, Sequence
7
  from langchain_core.documents import Document
8
  from langchain_chroma import Chroma
9
  from core.hash_file.hash_file import HashProcessor
10
- from utils.helpers import read_yaml
11
 
12
  logger = logging.getLogger(__name__)
13
 
14
 
15
  @dataclass
16
  class ChromaConfig:
17
- persist_dir: str
18
- collection_name: str
19
-
20
- @staticmethod
21
- def default_yaml_path() -> Path:
22
- return Path(__file__).resolve().parents[2] / "config" / "vector_db.yaml"
23
-
24
- @classmethod
25
- def from_yaml(cls, path: str | Path | None = None) -> "ChromaConfig":
26
- cfg_path = Path(path) if path is not None else cls.default_yaml_path()
27
- if not cfg_path.exists():
28
- raise FileNotFoundError(f"Vector DB config not found: {cfg_path}")
29
-
30
- data = read_yaml(cfg_path) or {}
31
- if not isinstance(data, dict):
32
- raise ValueError(f"Invalid config format: {cfg_path}")
33
-
34
- required = {"persist_dir", "collection_name"}
35
- missing = [k for k in required if k not in data]
36
- if missing:
37
- raise KeyError(f"Missing keys in {cfg_path}: {', '.join(missing)}")
38
-
39
- cfg = cls(
40
- persist_dir=str(data["persist_dir"]),
41
- collection_name=str(data["collection_name"]),
42
- )
43
- p = Path(cfg.persist_dir)
44
- if not p.is_absolute():
45
- cfg.persist_dir = str((cfg_path.parent.parent / p).resolve())
46
- return cfg
47
 
48
 
49
  class ChromaVectorDB:
@@ -53,7 +28,7 @@ class ChromaVectorDB:
53
  config: ChromaConfig | None = None,
54
  ):
55
  self.embedder = embedder
56
- self.config = config or ChromaConfig.from_yaml()
57
  self._hasher = HashProcessor(verbose=False)
58
 
59
  self._vs = Chroma(
 
1
  from __future__ import annotations
2
  import json
3
  import logging
4
+ from dataclasses import dataclass, field
5
  from pathlib import Path
6
  from typing import Any, Dict, List, Optional, Sequence
7
  from langchain_core.documents import Document
8
  from langchain_chroma import Chroma
9
  from core.hash_file.hash_file import HashProcessor
 
10
 
11
  logger = logging.getLogger(__name__)
12
 
13
 
14
  @dataclass
15
  class ChromaConfig:
16
+ def _default_persist_dir() -> str:
17
+ repo_root = Path(__file__).resolve().parents[2]
18
+ return str((repo_root / "data" / "chroma").resolve())
19
+
20
+ persist_dir: str = field(default_factory=_default_persist_dir)
21
+ collection_name: str = "hust_rag_collection"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
 
23
 
24
  class ChromaVectorDB:
 
28
  config: ChromaConfig | None = None,
29
  ):
30
  self.embedder = embedder
31
+ self.config = config or ChromaConfig()
32
  self._hasher = HashProcessor(verbose=False)
33
 
34
  self._vs = Chroma(
core/gradio/gradio_rag_qwen.py CHANGED
@@ -12,8 +12,6 @@ REPO_ROOT = Path(__file__).resolve().parents[2]
12
  if str(REPO_ROOT) not in sys.path:
13
  sys.path.insert(0, str(REPO_ROOT))
14
 
15
- from utils.helpers import read_yaml
16
-
17
 
18
  @dataclass
19
  class GradioConfig:
@@ -23,23 +21,6 @@ class GradioConfig:
23
  server_host: str
24
  server_port: int
25
 
26
- @staticmethod
27
- def default_yaml_path() -> Path:
28
- return Path(__file__).resolve().parents[2] / "config" / "gradio.yaml"
29
-
30
- @classmethod
31
- def from_yaml(cls, path: str | Path | None = None) -> "GradioConfig":
32
- cfg_path = Path(path) if path is not None else cls.default_yaml_path()
33
- data = read_yaml(cfg_path)
34
-
35
- return cls(
36
- llm_model=data["llm"]["model"],
37
- llm_temperature=data["llm"]["temperature"],
38
- llm_max_tokens=data["llm"]["max_tokens"],
39
- server_host=data["server"]["host"],
40
- server_port=data["server"]["port"],
41
- )
42
-
43
 
44
  def _load_env() -> None:
45
  dotenv_path = find_dotenv(usecwd=True) or ""
@@ -53,7 +34,13 @@ from core.embeddings.retrival import Retriever, get_retrieval_config
53
  _load_env()
54
 
55
  # Load all configs
56
- GRADIO_CFG = GradioConfig.from_yaml()
 
 
 
 
 
 
57
  RETRIEVAL_CFG = get_retrieval_config()
58
 
59
 
@@ -73,12 +60,9 @@ def _init_resources() -> None:
73
 
74
  print(" Đang khởi tạo Database & Re-ranker...")
75
 
76
- cfg = EmbeddingConfig.from_yaml()
77
- emb = QwenEmbeddings(cfg)
78
 
79
- db_cfg = ChromaConfig.from_yaml()
80
- db_cfg.persist_dir = os.getenv("PERSIST_DIR", db_cfg.persist_dir)
81
- db_cfg.collection_name = os.getenv("COLLECTION", db_cfg.collection_name)
82
 
83
  STATE.db = ChromaVectorDB(
84
  embedder=emb,
@@ -194,7 +178,7 @@ def rag_chat(message: str, history: List[Dict[str, str]] | None = None):
194
  # Create Gradio interface
195
  demo = gr.ChatInterface(
196
  fn=rag_chat,
197
- title="🎓 HUST RAG Assistant",
198
  description="Trợ lý học vụ Đại học Bách khoa Hà Nội",
199
  examples=[
200
  "Điều kiện tốt nghiệp đại học là gì?",
 
12
  if str(REPO_ROOT) not in sys.path:
13
  sys.path.insert(0, str(REPO_ROOT))
14
 
 
 
15
 
16
  @dataclass
17
  class GradioConfig:
 
21
  server_host: str
22
  server_port: int
23
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
 
25
  def _load_env() -> None:
26
  dotenv_path = find_dotenv(usecwd=True) or ""
 
34
  _load_env()
35
 
36
  # Load all configs
37
+ GRADIO_CFG = GradioConfig(
38
+ llm_model="qwen/qwen3-32b",
39
+ llm_temperature=0.0,
40
+ llm_max_tokens=4096,
41
+ server_host="127.0.0.1",
42
+ server_port=7860,
43
+ )
44
  RETRIEVAL_CFG = get_retrieval_config()
45
 
46
 
 
60
 
61
  print(" Đang khởi tạo Database & Re-ranker...")
62
 
63
+ emb = QwenEmbeddings(EmbeddingConfig())
 
64
 
65
+ db_cfg = ChromaConfig()
 
 
66
 
67
  STATE.db = ChromaVectorDB(
68
  embedder=emb,
 
178
  # Create Gradio interface
179
  demo = gr.ChatInterface(
180
  fn=rag_chat,
181
+ title="HUST RAG Assistant",
182
  description="Trợ lý học vụ Đại học Bách khoa Hà Nội",
183
  examples=[
184
  "Điều kiện tốt nghiệp đại học là gì?",
data/data_process/quyet_dinh/{06_ Quy định ngoại ngữ từ K70_chính quy_final.md → tieng_anh/06_ Quy định ngoại ngữ từ K70_chính quy_final.md} RENAMED
@@ -29,39 +29,32 @@ Việc lựa chọn tiêu chí phân loại trong số các tiêu chí trên do
29
  ## Điều 3 . Công nhận, xét miễn học phần ngoại ngữ cơ bản
30
 
31
  1. Các học phần NNCB của các CTĐT được xếp vào khối kiến thức ngoại ngữ bổ trợ tăng cường và không tính tín chỉ trong CTĐT chính khóa . Danh sách các học phần NNCB của các CTĐT được quy định tại các Phụ lục tương ứng kèm theo Quy định này .
32
- 2. 2 . Trong quá trình học tập, nếu sinh viên đạt được chứng chỉ ngoại ngữ theo quy định sẽ được xét miễn các học phần NNCB tương ứng với trình độ đạt được. Kết quả miễn học được ghi bằng ký hiệu "R" trong dữ liệu học tập và có giá trị toàn khóa kể từ ngày nhập điểm R.
33
  3. Chứng chỉ ngoại ngữ để xét miễn học phần NNCB phải còn hạn theo quy định tính tới thời điểm nộp đơn xét miễn. Danh mục chứng chỉ và học phần NNCB được miễn tương ứng được quy định tại các Phụ lục tương ứng kèm theo Quy định này .
34
- 4. 4 . Các chứng chỉ ngoại ngữ quốc gia, quốc tế phải được thẩm định nguồn gốc và cập nhật mức điểm lên hệ thống phần mềm quản lý đào tạo trước thời điểm nộp đơn đề nghị xét miễn học phần NNCB .
35
- 5. 5 . Thời gian tiếp nhận đơn đề nghị xét miễn học phần NNCB muộn nhất là 2 tuần trước khi bắt đầu học kỳ theo Khung kế hoạch năm học .
36
 
37
  ## Điều 4 . Lộ trình học tập các học phần ngoại ngữ cơ bản
38
-
39
  Lộ trình học tập đối với các học phần NNCB được áp dụng để đảm bảo năng lực ngoại ngữ của sinh viên được tăng cường dần qua các học kỳ và được quy định như sau:
40
-
41
- - 1 . Trong học kỳ 1 học kỳ 2, Ban Đào tạo xếp lớp học các học phần NNCB cho sinh viên dựa trên kết quả phân loại trình độ ngoại ngữ đầu vào theo quy định tại Điều 2.
42
- - 2 . Từ học kỳ 3 trở đi, sinh viên tự đăng ký học song hành các học phần NNCB cùng v ới các học phần khác trong CTĐT, bảo đảm tiến độ đáp ứng chuẩn ngoại ngữ theo từng học kỳ quy định tại các Phụ lục tương ứng kèm theo Quy định này.
43
  3. Việc đăng ký học song hành các học phần NNCB tương ứng ở mỗi kỳ kể từ kỳ 3 trở đi (trừ trường hợp đã học hoặc được miễn học phần) là điều kiện bắt buộc để được đăng ký học tập các học phần khác trong CTĐT.
44
  4. Sinh viên không được phép hủy các học phần NNCB đã đăng ký học song hành, trừ trường hợp xin rút hoặc chủ động rút toàn bộ đăng ký học tập trong học kỳ .
45
 
46
  ## Điều 5. Yêu cầu về chuẩn ngoại ngữ đầu ra
47
-
48
  1. Yêu cầu về chuẩn ngoại ngữ đầu ra được quy định cụ thể cho từng CTĐT tại các Phụ lục tương ứng kèm theo Quy định này.
49
-
50
  2. Chứng chỉ tiếng Anh phải đánh giá đầy đủ 4 kỹ năng nghe, nói, đọc , viết; đồng thời phải được cấp trong vòng 2 năm tính đến thời điểm xét chuẩn ngoại ngữ đầu ra và còn hiệu lực tại thời điểm xét.
51
 
52
  ## Điều 6. Các trường hợp ngoại lệ
53
-
54
  Các trường hợp ngoại lệ sẽ do Ban Đào tạo đề xuất Giám đốc Đại học Bách khoa Hà Nội xem xét quyết định .
55
 
56
  ## Điều 7. Hiệu lực thi hành
57
-
58
  1. Quy định này được áp dụng từ học kỳ 1 năm học 2025-2026 đối với sinh viên đại học hình thức chính quy và các CTĐT kể từ khóa 70 trở về sau, trừ các quy định sau:
59
  - a) Bảng quy đổi tương đương các chứng chỉ ngoại ngữ áp dụng cho toàn bộ sinh viên các khóa, đối với các chứng chỉ được cấp kể từ ngày Quy định này có hiệu lực;
60
  - b) Đối với sinh viên các khóa từ 69 trở về trước, nếu mức quy đổi tương đương cùng một chứng chỉ ngoại ngữ theo Quy định này khác với mức quy đổi theo quy định trước đây thì áp dụng mức quy đổi cao hơn trong hai quy định; mức cao hơn được xác định theo Khung năng lực ngoại ngữ 6 bậc dùng cho Việt Nam.
61
- 2. Các quy định hiện hành về ngoại ngữ đối với sinh viên và CTĐT từ khóa 69 trở v trước (trừ quy định về quy đổi tương đương chứng chỉ) vẫn tiếp tục có hiệu lực thi hành cho đến thời điểm kết thúc khóa đào tạo.
62
 
63
  ## Phụ lục I
64
-
65
  ## DANH MỤC CÁC CHƯƠNG TRÌNH ĐÀO TẠO VÀ NGOẠI NGỮ CHÍNH YÊU CẦU
66
 
67
  1. Các chương trình đào tạo chuẩn
 
29
  ## Điều 3 . Công nhận, xét miễn học phần ngoại ngữ cơ bản
30
 
31
  1. Các học phần NNCB của các CTĐT được xếp vào khối kiến thức ngoại ngữ bổ trợ tăng cường và không tính tín chỉ trong CTĐT chính khóa . Danh sách các học phần NNCB của các CTĐT được quy định tại các Phụ lục tương ứng kèm theo Quy định này .
32
+ 2. Trong quá trình học tập, nếu sinh viên đạt được chứng chỉ ngoại ngữ theo quy định sẽ được xét miễn các học phần NNCB tương ứng với trình độ đạt được. Kết quả miễn học được ghi bằng ký hiệu "R" trong dữ liệu học tập và có giá trị toàn khóa kể từ ngày nhập điểm R.
33
  3. Chứng chỉ ngoại ngữ để xét miễn học phần NNCB phải còn hạn theo quy định tính tới thời điểm nộp đơn xét miễn. Danh mục chứng chỉ và học phần NNCB được miễn tương ứng được quy định tại các Phụ lục tương ứng kèm theo Quy định này .
34
+ 4. Các chứng chỉ ngoại ngữ quốc gia, quốc tế phải được thẩm định nguồn gốc và cập nhật mức điểm lên hệ thống phần mềm quản lý đào tạo trước thời điểm nộp đơn đề nghị xét miễn học phần NNCB .
35
+ 5. Thời gian tiếp nhận đơn đề nghị xét miễn học phần NNCB muộn nhất là 2 tuần trước khi bắt đầu học kỳ theo Khung kế hoạch năm học .
36
 
37
  ## Điều 4 . Lộ trình học tập các học phần ngoại ngữ cơ bản
 
38
  Lộ trình học tập đối với các học phần NNCB được áp dụng để đảm bảo năng lực ngoại ngữ của sinh viên được tăng cường dần qua các học kỳ và được quy định như sau:
39
+ 1. Trong học kỳ 1 và học kỳ 2, Ban Đào tạo xếp lớp học các học phần NNCB cho sinh viên dựa trên kết quả phân loại trình độ ngoại ngữ đầu vào theo quy định tại Điều 2.
40
+ 2. Từ học kỳ 3 trở đi, sinh viên tự đăng học song hành các học phần NNCB cùng v ới các học phần khác trong CTĐT, bảo đảm tiến độ đáp ứng chuẩn ngoại ngữ theo từng học kỳ quy định tại các Phụ lục tương ứng kèm theo Quy định này.
 
41
  3. Việc đăng ký học song hành các học phần NNCB tương ứng ở mỗi kỳ kể từ kỳ 3 trở đi (trừ trường hợp đã học hoặc được miễn học phần) là điều kiện bắt buộc để được đăng ký học tập các học phần khác trong CTĐT.
42
  4. Sinh viên không được phép hủy các học phần NNCB đã đăng ký học song hành, trừ trường hợp xin rút hoặc chủ động rút toàn bộ đăng ký học tập trong học kỳ .
43
 
44
  ## Điều 5. Yêu cầu về chuẩn ngoại ngữ đầu ra
 
45
  1. Yêu cầu về chuẩn ngoại ngữ đầu ra được quy định cụ thể cho từng CTĐT tại các Phụ lục tương ứng kèm theo Quy định này.
 
46
  2. Chứng chỉ tiếng Anh phải đánh giá đầy đủ 4 kỹ năng nghe, nói, đọc , viết; đồng thời phải được cấp trong vòng 2 năm tính đến thời điểm xét chuẩn ngoại ngữ đầu ra và còn hiệu lực tại thời điểm xét.
47
 
48
  ## Điều 6. Các trường hợp ngoại lệ
 
49
  Các trường hợp ngoại lệ sẽ do Ban Đào tạo đề xuất Giám đốc Đại học Bách khoa Hà Nội xem xét quyết định .
50
 
51
  ## Điều 7. Hiệu lực thi hành
 
52
  1. Quy định này được áp dụng từ học kỳ 1 năm học 2025-2026 đối với sinh viên đại học hình thức chính quy và các CTĐT kể từ khóa 70 trở về sau, trừ các quy định sau:
53
  - a) Bảng quy đổi tương đương các chứng chỉ ngoại ngữ áp dụng cho toàn bộ sinh viên các khóa, đối với các chứng chỉ được cấp kể từ ngày Quy định này có hiệu lực;
54
  - b) Đối với sinh viên các khóa từ 69 trở về trước, nếu mức quy đổi tương đương cùng một chứng chỉ ngoại ngữ theo Quy định này khác với mức quy đổi theo quy định trước đây thì áp dụng mức quy đổi cao hơn trong hai quy định; mức cao hơn được xác định theo Khung năng lực ngoại ngữ 6 bậc dùng cho Việt Nam.
55
+ 2. Các quy định hiện hành về ngoại ngữ đối với sinh viên và CTĐT từ khóa 69 trở về trước (trừ quy định về quy đổi tương đương chứng chỉ) vẫn tiếp tục có hiệu lực thi hành cho đến thời điểm kết thúc khóa đào tạo.
56
 
57
  ## Phụ lục I
 
58
  ## DANH MỤC CÁC CHƯƠNG TRÌNH ĐÀO TẠO VÀ NGOẠI NGỮ CHÍNH YÊU CẦU
59
 
60
  1. Các chương trình đào tạo chuẩn
data/data_process/quyet_dinh/tieng_anh/QD_ngoai_ngu_tu_K65_CQ_final.md ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # QUY ĐỊNH Phân loại trình độ đầu vào, chương trình môn học và chuẩn ngoại ngữ yêu cầu đối với sinh viên đại học hệ chính quy
2
+
3
+ ## Điều 1. Phạm vi và đối tượng áp dụng
4
+ 1. Văn bản này quy định về công tác tổ chức đánh giá và phân loại trình độ ngoại ngữ đầu vào; chương trình môn học ngoại ngữ; điều kiện được miễn học các học phần ngoại ngữ; chuẩn ngoại ngữ yêu cầu theo tín chỉ lũy tích và chuẩn ngoại ngữ đầu ra.
5
+ 2. Quy định này áp dụng cho sinh viên đại học hệ chính quy, không thuộc ngành Ngôn ngữ Anh của Trường Đại học Bách khoa Hà Nội.
6
+ 3. Quy định này không áp dụng cho sinh viên là người nước ngoài đang học tại Trường Đại học Bách khoa Hà Nội.
7
+
8
+ ## Điều 2. Phân loại trình độ đầu vào và phân lớp học ngoại ngữ
9
+ 1. Căn cứ phân loại trình độ ngoại ngữ đầu vào với sinh viên thuộc chương trình đào tạo có yêu cầu chuẩn đầu ra là tiếng Anh:
10
+ - a) Đối với các sinh viên thuộc chương trình đào tạo chuẩn: phân loại dựa trên điểm thi môn tiếng Anh của kỳ thi tốt nghiệp THPT tại năm nhập học hoặc chứng chỉ tiếng Anh quốc tế.
11
+ - b) Đối với các sinh viên thuộc các chương trình đào tạo tiên tiến, tài năng và chương trình hợp tác quốc tế: phân loại dựa trên kết quả kiểm tra tiếng Anh nội bộ đầu khóa học hoặc chứng chỉ tiếng Anh quốc tế.
12
+ 2. Căn cứ phân loại trình độ ngoại ngữ đầu vào với sinh viên thuộc chương trình đào tạo có yêu cầu chuẩn đầu ra là ngoại ngữ khác tiếng Anh: phân loại dựa trên điểm thi môn ngoại ngữ tương ứng của kỳ thi tốt nghiệp THPT tại năm nhập học hoặc chứng chỉ quốc tế của ngôn ngữ tương ứng.
13
+ 3. Căn cứ trên kết quả phân loại trình độ ngoại ngữ đầu vào, sinh viên được xét miễn các học phần theo quy định và được xếp lớp học ngoại ngữ tương ứng.
14
+ 4. Sinh viên không đáp ứng đủ điều kiện phân loại trình độ ngoại ngữ theo mục 1 hoặc mục 2 của điều này sẽ phải học toàn bộ chương trình ngoại ngữ theo quy định.
15
+
16
+ ## Điều 3. Công nhận, xét miễn học phần ngoại ngữ
17
+ 1. Tất cả các học phần ngoại ngữ tương ứng với từng chương trình đào tạo là các học phần bắt buộc. Nhà trường sẽ đăng ký các học phần ngoại ngữ theo kế hoạch học tập chuẩn cho các sinh viên.
18
+ 2. Trong quá trình học tập, nếu sinh viên đạt được chứng chỉ ngoại ngữ theo quy định sẽ được xét miễn các học phần ngoại ngữ tương ứng với trình độ đạt được. Các học phần được miễn sẽ ghi điểm R trong bảng điểm.
19
+ 3. Chứng chỉ ngoại ngữ để xét miễn học phần phải còn hạn theo quy định tính tới thời điểm nộp đơn xét miễn. Danh sách chứng chỉ và các học phần được miễn được ghi trong các phụ lục tương ứng.
20
+ 4. Toàn bộ các chứng chỉ ngoại ngữ quốc tế phải được thẩm định nguồn gốc và cập nhật mức điểm lên hệ thống phần mềm lý đào tạo trước thời điểm nộp đơn xét miễn học phần.
21
+ 5. Thời gian tiếp nhận đơn xét miễn học phần ngoại ngữ muộn nhất là 2 tuần trước khi học kỳ bắt đầu theo Biểu đồ kế hoạch học tập của năm học.
22
+
23
+ ## Điều 4. Yêu cầu về chuẩn ngoại ngữ theo tín chỉ tích lũy và chuẩn ngoại ngữ đầu ra
24
+ 1. Yêu cầu về chuẩn ngoại ngữ theo số lượng tín chỉ tích lũy và chuẩn ngoại ngữ đầu ra được quy định cụ thể cho từng chương trình đào tạo tại các phụ lục tương ứng, được ban hành kèm quy định này.
25
+ 2. Kết quả thi các kỳ thi ngoại ngữ nội bộ của Trường Đại học Bách khoa Hà Nội được công nhận để xét điều kiện chuẩn ngoại ngữ theo tín chỉ tích lũy và chuẩn ngoại ngữ đầu ra. Riêng với các sinh viên thuộc chương trình đào tạo tiên tiến, chương trình đào tạo tài năng và chương trình đào tạo hợp tác quốc tế phải có chứng chỉ ngoại ngữ quốc tế để được xét công nhận đạt chuẩn ngoại ngữ đầu ra.
26
+ 3. Sinh viên được tham dự các kỳ thi Tiếng Anh nội bộ của Trường nhiều lần và được công nhận điểm thi cao nhất trong các lần thi. Kết quả này có giá trị công nhận trong vòng 2 năm kể từ ngày thi.
27
+
28
+ ## Điều 5. Hạn chế khối lượng học tập trong trường hợp không đạt chuẩn ngoại ngữ theo tín chỉ tích lũy và điều kiện gỡ bỏ hạn chế
29
+ 1. Sinh viên không đạt chuẩn ngoại ngữ quy định theo số tín chỉ tích lũy sẽ chỉ được đăng ký tối đa 14 TC và tối thiểu 8 TC cho một học kỳ chính, tương ứng với quy định tại Điều 19 của Quy chế đào tạo chính quy.
30
+ 2. Điều kiện gỡ bỏ hạn chế khối lượng học tập:
31
+ a) Chỉ áp dụng với sinh viên các chương trình đào tạo chuẩn, đào tạo tài năng, đào tạo tiên tiến và quốc tế với chuẩn ngoại ngữ đầu ra là tiếng Anh và đang ở trạng thái bị hạn chế khối lượng học tập theo quy định tại mục 1 của điều này.
32
+ b) Sinh viên được gỡ bỏ mức hạn chế khối lượng học tập hoặc được tiếp tục học tập nếu đã đăng ký học tại Trường ít nhất 90 tiết học các học phần tiếng Anh bổ trợ. Các học phần tiếng Anh bổ trợ được đăng ký học phải đảm bảo tương đương với trình độ tiếng Anh yêu cầu trong học kỳ cần gỡ bỏ hạn chế khối lượng học tập.
33
+ c) Yêu cầu về chuyên cần khi tham gia học các học phần tiếng Anh bổ trợ: sinh viên cần đảm bảo tham dự tối thiểu 80% số tiết học theo yêu cầu của mỗi học phần. Những sinh viên không đảm bảo điều kiện chuyên cần với bất cứ học phần tiếng Anh bổ trợ nào đã đăng ký sẽ bị áp dụng mức hạn chế khối lượng học tập ở học kỳ kế tiếp.
34
+ d) Điều kiện gỡ bỏ giới hạn khối lượng học tập không áp dụng cho các trường hợp sinh viên bị cảnh báo học tập, xét nhận đồ án tốt nghiệp/khóa luận tốt nghiệp, xét tốt nghiệp.
35
+
36
+ ## Điều 6. Các trường hợp ngoại lệ
37
+ Các trường hợp ngoại lệ sẽ do đơn vị chức năng quản lý đào tạo đề xuất để Hiệu trưởng quyết định, căn cứ các quy định hiện hành của Bộ Giáo dục và Đào tạo.
38
+
39
+ ## Điều 7. Hiệu lực thi hành
40
+ 1. Quy định này được áp dụng từ năm học 2020–2021 đối với các chương trình đào tạo kể từ khóa 65 trở về sau.
41
+ 2. Quy định hiện hành về ngoại ngữ đối với các chương trình đào tạo từ khóa 64 trở về trước vẫn tiếp tục còn hiệu lực thi hành cho đến thời điểm kết thúc khóa đào tạo.
data/data_process/quyet_dinh/{QD_ngoai_ngu_tu_K68_CQ_final.md → tieng_anh/QD_ngoai_ngu_tu_K68_CQ_final.md} RENAMED
@@ -2,18 +2,17 @@
2
  document_type: "quyet_dinh"
3
  title: "QUYẾT ĐỊNH Về việc ban hành Quy định về phân loại trình độ đầu vào, chương trình môn học và chuẩn ngoại ngữ yêu cầu đối với sinh viên đại học hệ chính quy từ k68"
4
  issued_year: "2024"
 
5
  ---
6
 
7
  # QUYẾT ĐỊNH Về việc ban hành Quy định về phân loại trình độ đầu vào, chương trình môn học và chuẩn ngoại ngữ yêu cầu đối với sinh viên đại học hệ chính quy từ k68
8
 
9
  ## Điều 1. Phạm vi và đối tượng áp dụng
10
-
11
  1. Văn bản này quy định về công tác tổ chức đánh giá và phân loại trình độ ngoại ngữ đầu vào; chương trình môn học ngoại ngữ; điều kiện được miễn học các học phần ngoại ngữ; chuẩn ngoại ngữ yêu cầu theo số lượng tín chỉ tích lũy và chuẩn ngoại ngữ đầu ra.
12
  2. Quy định này áp dụng cho sinh viên đại học hệ chính quy , không thuộc ngành Ngôn ngữ Anh của Đại học Bách khoa Hà Nội .
13
  3. Quy định này không áp dụng cho sinh viên là người nước ngoài đang học tại Đại học Bách khoa Hà Nội.
14
 
15
  ## Điều 2. Phân loại trình độ đầu vào và phân lớp học ngoại ngữ
16
-
17
  1. Căn cứ phân loại trình độ ngoại ngữ đầu vào với sinh viên thuộc chương trình đào tạo có yêu cầu chuẩn đầu ra là tiếng Anh dựa trên một hoặc nhiều tiêu chí, bao gồm:
18
  + Dựa trên kết quả kiểm tra tiếng Anh đầu khóa;
19
  + Hoặc dựa trên chứng chỉ tiếng Anh quốc gia, quốc tế;
@@ -26,21 +25,19 @@ Việc lựa chọn tiêu chí phân loại cụ thể trong số các tiêu ch
26
  4. Sinh viên không đáp ứng điều kiện phân loại trình độ ngoại ngữ theo khoản 1 hoặc khoản 2 của điều này sẽ phải học toàn bộ các học phần ngoại ngữ tương ứng với từng chương trình đào tạo theo lộ trình quy định.
27
 
28
  ## Điều 3 . Công nhận, xét miễn học phần ngoại ngữ
29
-
30
  1. Tất cả các học phần ngoại ngữ tương ứng với từng chương trình đào tạo là các học phần bắt buộc. Lộ trình học tập đối với các học phần ngoại ngữ được quy định như sau:
31
-
32
  - a. Ban Đào tạo sẽ đăng ký các học phần ngoại ngữ đối với 2 học kỳ đầu cho sinh viên dựa theo kết quả phân loại trình độ ngoại ngữ đầu vào .
33
  - b. Học kỳ thứ 3: dành cho sinh viên để tự đăng ký học lại các học phần ngoại ngữ không đạt ở 2 học kỳ đầu (nếu có) và chủ động thi chứng chỉ ngoại ngữ.
34
  - c. Từ kỳ thứ 4 trở đi: Ban Đào tạo sẽ thực hiện việc đăng ký bắt buộc các học phần ngoại ngữ đối với các sinh viên không đạt chuẩn ngoại ngữ theo số lượng tín chỉ tích lũy .
35
- 4. 2 . Trong quá trình học tập, nếu sinh viên đạt được chứng chỉ ngoại ngữ theo quy định sẽ được xét miễn các học phần ngoại ngữ tương ứng với trình độ đạt được. Các học phần được miễn sẽ được ghi điểm R trong bảng điểm.
36
  3. Chứng chỉ ngoại ngữ để xét miễn học phần phải còn hạn theo quy định tính tới thời điểm nộp đơn xét miễn. Danh sách chứng chỉ và các học phần được miễn được ghi trong các phụ lục tương ứng kèm theo Quy định này .
37
- 6. 4 . Toàn bộ các chứng chỉ ngoại ngữ quốc gia, quốc tế phải được thẩm định nguồn g ốc và cập nhật mức điểm lên hệ thống phần mềm quản lý đào tạo trước thời điểm nộp đơn đề nghị xét miễn học phần ngoại ngữ .
38
- 7. 5 . Thời gian tiếp nhận đơn đề nghị xét miễn học phần ngoại ngữ muộn nhất là 2 tu ần trước khi bắt đầu học kỳ theo Khung kế hoạch thời gian của năm học .
39
 
40
  ## Điều 4. Yêu cầu về chuẩn ngoại ngữ theo tín chỉ tích lũy và chuẩn ngoại ngữ đầu ra
41
 
42
  1. Yêu cầu về chuẩn ngoại ngữ theo số lượng tín chỉ tích lũy và chuẩn ngoại ngữ đầu ra được quy định cụ thể cho từng chương trình đào tạo tại các phụ lục tương ứng kèm theo Quy định này .
43
- 2. Kết quả thi các kỳ thi ngoại ngữ nội bộ của Đại học Bách khoa Hà Nội được công nhận để xét điều kiện chuẩn ngoại ngữ theo số lượng tín chỉ tích lũy .
44
  3. Sinh viên được tham dự các kỳ thi tiếng Anh nội bộ của Đại học Bách khoa Hà Nội nhiều lần và được công nhận điểm thi cao nhất trong các lần thi. Kết quả thi này có giá trị công nhận trong vòng 2 năm kể từ ngày thi.
45
  4. 4 . Sinh viên thuộc các chương trình đào tạo Elitech (bao gồm chương trình tiên tiến, chương trình đào tạo tài năng và chương trình đào tạo hợp tác quốc tế) có ngoại ngữ yêu cầu là tiếng Anh và sinh viên thuộc các chương trình đào tạo chuẩn phải có chứng chỉ tiếng Anh quốc tế 4 kỹ năng hoặc chứng chỉ VSTEP tương đương để được xét công nhận đạt chuẩn ngoại ngữ đầu ra. Các chứng chỉ tiếng Anh có giá trị công nhận trong vòng 2 năm kể từ ngày cấp .
46
 
 
2
  document_type: "quyet_dinh"
3
  title: "QUYẾT ĐỊNH Về việc ban hành Quy định về phân loại trình độ đầu vào, chương trình môn học và chuẩn ngoại ngữ yêu cầu đối với sinh viên đại học hệ chính quy từ k68"
4
  issued_year: "2024"
5
+
6
  ---
7
 
8
  # QUYẾT ĐỊNH Về việc ban hành Quy định về phân loại trình độ đầu vào, chương trình môn học và chuẩn ngoại ngữ yêu cầu đối với sinh viên đại học hệ chính quy từ k68
9
 
10
  ## Điều 1. Phạm vi và đối tượng áp dụng
 
11
  1. Văn bản này quy định về công tác tổ chức đánh giá và phân loại trình độ ngoại ngữ đầu vào; chương trình môn học ngoại ngữ; điều kiện được miễn học các học phần ngoại ngữ; chuẩn ngoại ngữ yêu cầu theo số lượng tín chỉ tích lũy và chuẩn ngoại ngữ đầu ra.
12
  2. Quy định này áp dụng cho sinh viên đại học hệ chính quy , không thuộc ngành Ngôn ngữ Anh của Đại học Bách khoa Hà Nội .
13
  3. Quy định này không áp dụng cho sinh viên là người nước ngoài đang học tại Đại học Bách khoa Hà Nội.
14
 
15
  ## Điều 2. Phân loại trình độ đầu vào và phân lớp học ngoại ngữ
 
16
  1. Căn cứ phân loại trình độ ngoại ngữ đầu vào với sinh viên thuộc chương trình đào tạo có yêu cầu chuẩn đầu ra là tiếng Anh dựa trên một hoặc nhiều tiêu chí, bao gồm:
17
  + Dựa trên kết quả kiểm tra tiếng Anh đầu khóa;
18
  + Hoặc dựa trên chứng chỉ tiếng Anh quốc gia, quốc tế;
 
25
  4. Sinh viên không đáp ứng điều kiện phân loại trình độ ngoại ngữ theo khoản 1 hoặc khoản 2 của điều này sẽ phải học toàn bộ các học phần ngoại ngữ tương ứng với từng chương trình đào tạo theo lộ trình quy định.
26
 
27
  ## Điều 3 . Công nhận, xét miễn học phần ngoại ngữ
 
28
  1. Tất cả các học phần ngoại ngữ tương ứng với từng chương trình đào tạo là các học phần bắt buộc. Lộ trình học tập đối với các học phần ngoại ngữ được quy định như sau:
 
29
  - a. Ban Đào tạo sẽ đăng ký các học phần ngoại ngữ đối với 2 học kỳ đầu cho sinh viên dựa theo kết quả phân loại trình độ ngoại ngữ đầu vào .
30
  - b. Học kỳ thứ 3: dành cho sinh viên để tự đăng ký học lại các học phần ngoại ngữ không đạt ở 2 học kỳ đầu (nếu có) và chủ động thi chứng chỉ ngoại ngữ.
31
  - c. Từ kỳ thứ 4 trở đi: Ban Đào tạo sẽ thực hiện việc đăng ký bắt buộc các học phần ngoại ngữ đối với các sinh viên không đạt chuẩn ngoại ngữ theo số lượng tín chỉ tích lũy .
32
+ 2. Trong quá trình học tập, nếu sinh viên đạt được chứng chỉ ngoại ngữ theo quy định sẽ được xét miễn các học phần ngoại ngữ tương ứng với trình độ đạt được. Các học phần được miễn sẽ được ghi điểm R trong bảng điểm.
33
  3. Chứng chỉ ngoại ngữ để xét miễn học phần phải còn hạn theo quy định tính tới thời điểm nộp đơn xét miễn. Danh sách chứng chỉ và các học phần được miễn được ghi trong các phụ lục tương ứng kèm theo Quy định này .
34
+ 4. Toàn bộ các chứng chỉ ngoại ngữ quốc gia, quốc tế phải được thẩm định nguồn g ốc và cập nhật mức điểm lên hệ thống phần mềm quản lý đào tạo trước thời điểm nộp đơn đề nghị xét miễn học phần ngoại ngữ .
35
+ 5. Thời gian tiếp nhận đơn đề nghị xét miễn học phần ngoại ngữ muộn nhất là 2 tu ần trước khi bắt đầu học kỳ theo Khung kế hoạch thời gian của năm học .
36
 
37
  ## Điều 4. Yêu cầu về chuẩn ngoại ngữ theo tín chỉ tích lũy và chuẩn ngoại ngữ đầu ra
38
 
39
  1. Yêu cầu về chuẩn ngoại ngữ theo số lượng tín chỉ tích lũy và chuẩn ngoại ngữ đầu ra được quy định cụ thể cho từng chương trình đào tạo tại các phụ lục tương ứng kèm theo Quy định này .
40
+ 2. Kết quả thi các kỳ thi ngoại ngữ nội bộ của Đại học Bách khoa Hà Nội được công n hận để xét điều kiện chuẩn ngoại ngữ theo số lượng tín chỉ tích lũy .
41
  3. Sinh viên được tham dự các kỳ thi tiếng Anh nội bộ của Đại học Bách khoa Hà Nội nhiều lần và được công nhận điểm thi cao nhất trong các lần thi. Kết quả thi này có giá trị công nhận trong vòng 2 năm kể từ ngày thi.
42
  4. 4 . Sinh viên thuộc các chương trình đào tạo Elitech (bao gồm chương trình tiên tiến, chương trình đào tạo tài năng và chương trình đào tạo hợp tác quốc tế) có ngoại ngữ yêu cầu là tiếng Anh và sinh viên thuộc các chương trình đào tạo chuẩn phải có chứng chỉ tiếng Anh quốc tế 4 kỹ năng hoặc chứng chỉ VSTEP tương đương để được xét công nhận đạt chuẩn ngoại ngữ đầu ra. Các chứng chỉ tiếng Anh có giá trị công nhận trong vòng 2 năm kể từ ngày cấp .
43
 
data/data_raw/quyet_dinh/quy-dinh-chuan-ngoai-ngu-2021.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b1529ce07003568dd8458b84b149d0d681eb1b35ab485e0c3e61ca931b0a0223
3
+ size 12550193
data/qa_generated.jsonl ADDED
The diff for this file is too large to render. See raw diff
 
evaluation/eval_utils.py ADDED
@@ -0,0 +1,174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+ import os
3
+ import re
4
+ import sys
5
+ import csv
6
+ import json
7
+ from pathlib import Path
8
+ from datetime import datetime
9
+ from typing import Any
10
+ from dotenv import find_dotenv, load_dotenv
11
+
12
+ REPO_ROOT = Path(__file__).resolve().parents[1]
13
+ if str(REPO_ROOT) not in sys.path:
14
+ sys.path.insert(0, str(REPO_ROOT))
15
+ load_dotenv(find_dotenv(usecwd=True))
16
+
17
+ from core.embeddings.embedding_model import SiliconFlowConfig, QwenEmbeddings
18
+ from core.embeddings.vector_store import ChromaConfig, ChromaVectorDB
19
+ from core.embeddings.retrival import Retriever
20
+ from core.embeddings.generator import RAGGenerator
21
+
22
+
23
+ def strip_thinking(text: str) -> str:
24
+ """Remove <think>...</think> tags from model output."""
25
+ return re.sub(r'<think>.*?</think>\s*', '', text, flags=re.DOTALL).strip()
26
+
27
+
28
+ def load_config() -> dict:
29
+ return {
30
+ "llm": {
31
+ "model": os.getenv("EVAL_LLM_MODEL", "nex-agi/DeepSeek-V3.1-Nex-N1"),
32
+ "temperature": float(os.getenv("EVAL_LLM_TEMPERATURE", "0")),
33
+ "timeout": int(os.getenv("EVAL_LLM_TIMEOUT", "30")),
34
+ "max_retries": int(os.getenv("EVAL_LLM_MAX_RETRIES", "2")),
35
+ "max_tokens": int(os.getenv("EVAL_LLM_MAX_TOKENS", "4096")),
36
+ },
37
+ "retrieval": {
38
+ "top_k": int(os.getenv("EVAL_TOP_K", "5")),
39
+ "initial_k": int(os.getenv("EVAL_INITIAL_K", "100")),
40
+ },
41
+ "data": {
42
+ "csv_path": os.getenv("EVAL_CSV_PATH", "data/data.csv"),
43
+ "sample_size": int(os.getenv("EVAL_SAMPLE_SIZE", "0")),
44
+ },
45
+ "output": {
46
+ "dir": os.getenv("EVAL_OUTPUT_DIR", "evaluation/results"),
47
+ },
48
+ }
49
+
50
+
51
+ def load_csv_data(csv_path: str, sample_size: int = 0) -> tuple[list, list]:
52
+ questions, ground_truths = [], []
53
+
54
+ with open(csv_path, 'r', encoding='utf-8') as f:
55
+ for row in csv.DictReader(f):
56
+ if row.get('question') and row.get('ground_truth'):
57
+ questions.append(row['question'])
58
+ ground_truths.append(row['ground_truth'])
59
+
60
+ if sample_size > 0:
61
+ questions = questions[:sample_size]
62
+ ground_truths = ground_truths[:sample_size]
63
+
64
+ return questions, ground_truths
65
+
66
+
67
+ def init_rag_components(config: dict) -> tuple[RAGGenerator, dict]:
68
+ emb_cfg = SiliconFlowConfig()
69
+ qwen_embeddings = QwenEmbeddings(emb_cfg)
70
+
71
+ db_cfg = ChromaConfig()
72
+ db = ChromaVectorDB(embedder=qwen_embeddings, config=db_cfg)
73
+ retriever = Retriever(vector_db=db)
74
+
75
+ # RAGGenerator uses Groq with llama-3.1-8b-instant (default)
76
+ rag_generator = RAGGenerator(retriever=retriever)
77
+
78
+ info = {
79
+ "gen_model": rag_generator._llm_model, # Get actual model from RAGGenerator
80
+ "eval_model": config["llm"]["model"],
81
+ "embed_model": emb_cfg.model,
82
+ "collection": db_cfg.collection_name,
83
+ "doc_count": db.count(),
84
+ "embeddings": qwen_embeddings,
85
+ }
86
+
87
+ return rag_generator, info
88
+
89
+
90
+ def generate_answers(
91
+ rag_generator: RAGGenerator,
92
+ questions: list,
93
+ top_k: int = 5,
94
+ initial_k: int = 100,
95
+ ) -> tuple[list, list]:
96
+ answers, contexts = [], []
97
+
98
+ for i, q in enumerate(questions):
99
+ try:
100
+ result = rag_generator.generate(q, k=top_k, initial_k=initial_k)
101
+ # Strip <think>...</think> tags from answer
102
+ answer = strip_thinking(result["answer"])
103
+ answers.append(answer)
104
+ contexts.append(result["contexts"])
105
+ except Exception as e:
106
+ print(f" Error at {i+1}: {e}")
107
+ answers.append("Không thể trả lời.")
108
+ contexts.append([])
109
+ print(f" [{i+1}/{len(questions)}] Done")
110
+
111
+ return answers, contexts
112
+
113
+
114
+ def save_eval_report(
115
+ output_dir: Path,
116
+ eval_type: str,
117
+ questions: list,
118
+ answers: list,
119
+ ground_truths: list,
120
+ contexts: list,
121
+ scores_per_sample: list,
122
+ avg_scores: dict,
123
+ config: dict,
124
+ ) -> Path:
125
+ """Save evaluation report to JSON."""
126
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
127
+ output_dir.mkdir(parents=True, exist_ok=True)
128
+
129
+ json_path = output_dir / f"{eval_type}_eval_{timestamp}.json"
130
+
131
+ data = {
132
+ "timestamp": timestamp,
133
+ "eval_type": eval_type,
134
+ "config": config,
135
+ "avg_scores": avg_scores,
136
+ "samples": [
137
+ {
138
+ "idx": i + 1,
139
+ "question": q,
140
+ "answer": a,
141
+ "ground_truth": gt[0] if isinstance(gt, list) else gt,
142
+ "contexts": ctx,
143
+ "scores": scores,
144
+ }
145
+ for i, (q, a, gt, ctx, scores) in enumerate(
146
+ zip(questions, answers, ground_truths, contexts, scores_per_sample)
147
+ )
148
+ ],
149
+ }
150
+
151
+ with open(json_path, 'w', encoding='utf-8') as f:
152
+ json.dump(data, f, ensure_ascii=False, indent=2)
153
+
154
+ print(f" Saved: {json_path}")
155
+ return json_path
156
+
157
+
158
+ def print_scores(title: str, scores: dict, metrics: list | None = None):
159
+ """Print scores with progress bar."""
160
+ import math
161
+ print(f"\n[{title}]")
162
+
163
+ metrics = metrics or list(scores.keys())
164
+ for metric in metrics:
165
+ if metric not in scores:
166
+ print(f" {metric:25} [N/A]")
167
+ continue
168
+
169
+ score = float(scores[metric])
170
+ if math.isnan(score):
171
+ print(f" {metric:25} [FAILED - NaN]")
172
+ else:
173
+ bar = "#" * int(score * 20) + "-" * (20 - int(score * 20))
174
+ print(f" {metric:25} [{bar}] {score:.4f}")
evaluation/ragas_eval.py CHANGED
@@ -2,358 +2,110 @@ from __future__ import annotations
2
  import os
3
  import sys
4
  import json
5
- import csv
6
  from pathlib import Path
7
  from datetime import datetime
 
8
  from dotenv import find_dotenv, load_dotenv
9
 
10
  REPO_ROOT = Path(__file__).resolve().parents[1]
11
  if str(REPO_ROOT) not in sys.path:
12
  sys.path.insert(0, str(REPO_ROOT))
13
  load_dotenv(find_dotenv(usecwd=True))
 
14
  from datasets import Dataset
15
- from langchain_groq import ChatGroq
16
  from ragas import evaluate
17
  from ragas.metrics import faithfulness, answer_relevancy, context_precision, context_recall
18
  from ragas.llms import LangchainLLMWrapper
19
  from ragas.embeddings import LangchainEmbeddingsWrapper
20
  from ragas.run_config import RunConfig
21
- from core.embeddings.embedding_model import SiliconFlowConfig, QwenEmbeddings
22
- from core.embeddings.vector_store import ChromaConfig, ChromaVectorDB
23
- from core.embeddings.retrival import Retriever
24
- from utils.helpers import read_yaml
25
- from rouge_score import rouge_scorer
26
-
27
 
28
- def load_config():
29
- cfg_path = REPO_ROOT / "config" / "evaluation.yaml"
30
- return read_yaml(cfg_path)
31
 
32
 
33
- def create_dataset_from_csv(csv_path: str, sample_size: int = 0) -> tuple:
34
- """Load test data và tạo RAGAS dataset."""
35
- questions = []
36
- ground_truths = []
37
 
38
- with open(csv_path, 'r', encoding='utf-8') as f:
39
- reader = csv.DictReader(f)
40
- for row in reader:
41
- if row.get('question') and row.get('ground_truth'):
42
- questions.append(row['question'])
43
- ground_truths.append([row['ground_truth']]) # RAGAS expects list
44
 
45
- if sample_size > 0:
46
- questions = questions[:sample_size]
47
- ground_truths = ground_truths[:sample_size]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
 
49
- dataset = Dataset.from_dict({
 
50
  "question": questions,
 
 
51
  "ground_truth": ground_truths,
52
  })
53
 
54
- return dataset, questions, ground_truths
55
-
56
-
57
- def calculate_rouge_scores(answers: list, ground_truths: list) -> tuple:
58
- """Tính ROUGE-1, ROUGE-2, ROUGE-L scores cho từng sample và trung bình."""
59
- scorer = rouge_scorer.RougeScorer(['rouge1', 'rouge2', 'rougeL'], use_stemmer=False)
60
-
61
- per_sample_scores = []
62
-
63
- for answer, gt_list in zip(answers, ground_truths):
64
- gt = gt_list[0] if isinstance(gt_list, list) else gt_list
65
- if not answer.strip() or not gt.strip():
66
- per_sample_scores.append({"rouge1": 0.0, "rouge2": 0.0, "rougeL": 0.0})
67
- continue
68
-
69
- scores = scorer.score(gt, answer)
70
- per_sample_scores.append({
71
- "rouge1": scores['rouge1'].fmeasure,
72
- "rouge2": scores['rouge2'].fmeasure,
73
- "rougeL": scores['rougeL'].fmeasure,
74
- })
75
-
76
- # Tính trung bình
77
- avg_scores = {
78
- "rouge1": sum(s["rouge1"] for s in per_sample_scores) / len(per_sample_scores) if per_sample_scores else 0.0,
79
- "rouge2": sum(s["rouge2"] for s in per_sample_scores) / len(per_sample_scores) if per_sample_scores else 0.0,
80
- "rougeL": sum(s["rougeL"] for s in per_sample_scores) / len(per_sample_scores) if per_sample_scores else 0.0,
81
- }
82
 
83
- return avg_scores, per_sample_scores
84
-
85
-
86
- def save_detailed_report(
87
- output_dir: Path,
88
- questions: list,
89
- answers: list,
90
- ground_truths: list,
91
- contexts: list,
92
- ragas_results,
93
- rouge_per_sample: list,
94
- avg_scores: dict,
95
- config: dict,
96
- ):
97
- """Lưu báo cáo chi tiết từng câu hỏi."""
98
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
99
- output_dir.mkdir(parents=True, exist_ok=True)
100
-
101
- # 1. Lưu CSV chi tiết từng câu
102
- csv_path = output_dir / f"eval_details_{timestamp}.csv"
103
- with open(csv_path, 'w', encoding='utf-8', newline='') as f:
104
- writer = csv.writer(f)
105
- writer.writerow([
106
- "idx", "question", "answer", "ground_truth",
107
- "contexts_count", "rouge1", "rouge2", "rougeL"
108
- ])
109
-
110
- for i, (q, a, gt, ctx, rouge) in enumerate(zip(
111
- questions, answers, ground_truths, contexts, rouge_per_sample
112
- )):
113
- gt_str = gt[0] if isinstance(gt, list) else gt
114
- writer.writerow([
115
- i + 1,
116
- q[:200], # Truncate for readability
117
- a[:200],
118
- gt_str[:200],
119
- len(ctx),
120
- f"{rouge['rouge1']:.4f}",
121
- f"{rouge['rouge2']:.4f}",
122
- f"{rouge['rougeL']:.4f}",
123
- ])
124
-
125
- print(f" CSV details: {csv_path}")
126
-
127
- # 2. Lưu JSON đầy đủ
128
- json_path = output_dir / f"eval_full_{timestamp}.json"
129
- full_data = {
130
- "timestamp": timestamp,
131
- "config": config,
132
- "avg_scores": avg_scores,
133
- "samples": []
134
- }
135
-
136
- for i, (q, a, gt, ctx, rouge) in enumerate(zip(
137
- questions, answers, ground_truths, contexts, rouge_per_sample
138
- )):
139
- full_data["samples"].append({
140
- "idx": i + 1,
141
- "question": q,
142
- "answer": a,
143
- "ground_truth": gt[0] if isinstance(gt, list) else gt,
144
- "contexts": ctx,
145
- "rouge": rouge,
146
- })
147
 
148
  with open(json_path, 'w', encoding='utf-8') as f:
149
- json.dump(full_data, f, ensure_ascii=False, indent=2)
150
-
151
- print(f" JSON full: {json_path}")
152
-
153
- # 3. Lưu summary scores
154
- summary_path = output_dir / f"eval_summary_{timestamp}.json"
155
- with open(summary_path, 'w', encoding='utf-8') as f:
156
  json.dump({
157
  "timestamp": timestamp,
158
- "sample_count": len(questions),
159
- "scores": avg_scores,
160
- "config": config,
 
 
 
161
  }, f, ensure_ascii=False, indent=2)
162
 
163
- print(f" Summary: {summary_path}")
164
-
165
- return csv_path, json_path, summary_path
166
-
167
-
168
- def run_ragas_evaluation(sample_size: int = 10, output_dir: str = None):
169
- """Main RAGAS evaluation function."""
170
- print("=" * 60)
171
- print("RAGAS + ROUGE EVALUATION")
172
- print("=" * 60)
173
-
174
- # Load config
175
- config = load_config()
176
- llm_config = config["llm"]
177
- retrieval_config = config["retrieval"]
178
- data_config = config["data"]
179
- output_config = config.get("output", {})
180
-
181
- output_dir = Path(output_dir or REPO_ROOT / output_config.get("dir", "evaluation/results"))
182
-
183
- print(f"\n[Config]")
184
- print(f" LLM: {llm_config['model']}")
185
- print(f" Top-K: {retrieval_config['top_k']}")
186
- print(f" Initial-K: {retrieval_config['initial_k']}")
187
- print(f" Output: {output_dir}")
188
-
189
- # Initialize LLM
190
- api_key = os.getenv("GROQ_API_KEY")
191
- if not api_key:
192
- raise ValueError("Missing GROQ_API_KEY in .env")
193
-
194
- chat_groq = ChatGroq(
195
- model=llm_config["model"],
196
- api_key=api_key,
197
- temperature=llm_config["temperature"],
198
- timeout=llm_config["timeout"],
199
- max_retries=llm_config["max_retries"],
200
- )
201
-
202
- evaluator_llm = LangchainLLMWrapper(chat_groq)
203
- print(f" LLM initialized: {llm_config['model']}")
204
-
205
- # Initialize Embeddings
206
- emb_cfg = SiliconFlowConfig.from_yaml()
207
- qwen_embeddings = QwenEmbeddings(emb_cfg)
208
- evaluator_embeddings = LangchainEmbeddingsWrapper(qwen_embeddings)
209
- print(f" Embeddings: {emb_cfg.embed_model}")
210
-
211
- # Initialize Retriever
212
- db_cfg = ChromaConfig.from_yaml()
213
- db = ChromaVectorDB(embedder=qwen_embeddings, config=db_cfg)
214
- retriever = Retriever(vector_db=db)
215
- print(f" Vector DB: {db_cfg.collection_name} ({db.count()} docs)")
216
-
217
- # Load test data
218
- csv_path = str(REPO_ROOT / data_config["csv_path"])
219
- sample_size = sample_size or data_config.get("sample_size", 10)
220
-
221
- base_dataset, questions, ground_truths = create_dataset_from_csv(csv_path, sample_size)
222
- print(f"\n[Data]")
223
- print(f" Loaded {len(base_dataset)} samples from {csv_path}")
224
-
225
- # Generate answers and retrieve contexts
226
- print("\n[Generating answers and retrieving contexts...]")
227
-
228
- answers = []
229
- contexts = []
230
-
231
- for i, question in enumerate(questions):
232
- # Retrieve contexts
233
- results = retriever.search_with_rerank(
234
- question,
235
- k=retrieval_config["top_k"],
236
- initial_k=retrieval_config["initial_k"]
237
- )
238
- ctx_list = [r.get("content", "")[:1000] for r in results if r.get("content")]
239
- contexts.append(ctx_list)
240
-
241
- # Generate answer
242
- context_str = "\n\n".join([f"[{j+1}] {ctx[:500]}" for j, ctx in enumerate(ctx_list)])
243
- prompt = f"""Bạn là Trợ lý học vụ của HUST. Trả lời ngắn gọn dựa trên CONTEXT.
244
-
245
- CONTEXT:
246
- {context_str}
247
-
248
- CÂU HỎI: {question}
249
-
250
- TRẢ LỜI:"""
251
-
252
- try:
253
- response = chat_groq.invoke(prompt)
254
- answers.append(response.content)
255
- except Exception as e:
256
- print(f" Error at {i+1}: {e}")
257
- answers.append("Không thể trả lời.")
258
-
259
- print(f" [{i+1}/{len(questions)}] Done")
260
-
261
- ragas_dataset = Dataset.from_dict({
262
- "question": questions, # Tên cột chuẩn
263
- "answer": answers, # Tên cột chuẩn
264
- "contexts": contexts, # Tên cột chuẩn
265
- "ground_truth": [gt[0] for gt in ground_truths], # Tên cột chuẩn - QUAN TRỌNG cho context_recall
266
- })
267
-
268
- metrics = [faithfulness, answer_relevancy, context_precision, context_recall]
269
-
270
- print(f"\n[Running RAGAS evaluation...]")
271
- print(f" Metrics: faithfulness, answer_relevancy, context_precision, context_recall")
272
- print(f" max_workers=4 (để tránh Groq rate limit)")
273
-
274
- try:
275
- run_config = RunConfig(
276
- max_workers=4, # Giới hạn concurrent requests
277
- timeout=120, # Tăng timeout
278
- )
279
-
280
- results = evaluate(
281
- dataset=ragas_dataset,
282
- metrics=metrics,
283
- llm=evaluator_llm,
284
- embeddings=evaluator_embeddings,
285
- raise_exceptions=False,
286
- run_config=run_config,
287
- )
288
-
289
- # Tính ROUGE scores
290
- print("\n[Calculating ROUGE scores...]")
291
- rouge_avg, rouge_per_sample = calculate_rouge_scores(answers, ground_truths)
292
-
293
- # Merge all scores
294
- all_avg_scores = dict(results)
295
- all_avg_scores.update(rouge_avg)
296
-
297
- # In kết quả
298
- print("\n" + "=" * 60)
299
- print("EVALUATION RESULTS")
300
- print("=" * 60)
301
-
302
- print("\n[RAGAS Metrics - LLM-based]")
303
- for metric in ["faithfulness", "answer_relevancy", "context_precision", "context_recall"]:
304
- if metric in results:
305
- score = float(results[metric])
306
- bar = "#" * int(score * 20) + "-" * (20 - int(score * 20))
307
- print(f" {metric:25} [{bar}] {score:.4f}")
308
-
309
- print("\n[ROUGE Metrics - Text-based]")
310
- for metric, score in rouge_avg.items():
311
- bar = "#" * int(float(score) * 20) + "-" * (20 - int(float(score) * 20))
312
- print(f" {metric:25} [{bar}] {score:.4f}")
313
-
314
- print("\n" + "=" * 60)
315
-
316
- # Lưu báo cáo chi tiết
317
- print("\n[Saving detailed reports...]")
318
- save_detailed_report(
319
- output_dir=output_dir,
320
- questions=questions,
321
- answers=answers,
322
- ground_truths=ground_truths,
323
- contexts=contexts,
324
- ragas_results=results,
325
- rouge_per_sample=rouge_per_sample,
326
- avg_scores=all_avg_scores,
327
- config={
328
- "llm_model": llm_config["model"],
329
- "embed_model": emb_cfg.embed_model,
330
- "top_k": retrieval_config["top_k"],
331
- "initial_k": retrieval_config["initial_k"],
332
- "sample_size": len(questions),
333
- },
334
- )
335
-
336
- print("\n Evaluation complete!")
337
-
338
- return all_avg_scores
339
-
340
- except Exception as e:
341
- print(f"\nRAGAS evaluation error: {e}")
342
- print("\nTroubleshooting:")
343
- print("1. pip install ragas --upgrade")
344
- print("2. Check Groq API key and rate limits")
345
- print("3. Try reducing --samples")
346
- import traceback
347
- traceback.print_exc()
348
- return None
349
 
350
 
351
  if __name__ == "__main__":
352
  import argparse
353
-
354
- parser = argparse.ArgumentParser(description="RAGAS + ROUGE Evaluation")
355
- parser.add_argument("--samples", type=int, default=10, help="Number of samples")
356
- parser.add_argument("--output", type=str, default=None, help="Output directory")
357
  args = parser.parse_args()
358
-
359
- run_ragas_evaluation(sample_size=args.samples, output_dir=args.output)
 
2
  import os
3
  import sys
4
  import json
 
5
  from pathlib import Path
6
  from datetime import datetime
7
+ from typing import Optional
8
  from dotenv import find_dotenv, load_dotenv
9
 
10
  REPO_ROOT = Path(__file__).resolve().parents[1]
11
  if str(REPO_ROOT) not in sys.path:
12
  sys.path.insert(0, str(REPO_ROOT))
13
  load_dotenv(find_dotenv(usecwd=True))
14
+
15
  from datasets import Dataset
16
+ from langchain_openai import ChatOpenAI
17
  from ragas import evaluate
18
  from ragas.metrics import faithfulness, answer_relevancy, context_precision, context_recall
19
  from ragas.llms import LangchainLLMWrapper
20
  from ragas.embeddings import LangchainEmbeddingsWrapper
21
  from ragas.run_config import RunConfig
 
 
 
 
 
 
22
 
23
+ from evaluation.eval_utils import load_config, load_csv_data, init_rag_components, generate_answers
 
 
24
 
25
 
26
+ def run_ragas_evaluation(sample_size: int = 10, output_dir: Optional[str] = None) -> dict:
27
+ config = load_config()
28
+ out_path = Path(output_dir) if output_dir else REPO_ROOT / config["output"]["dir"]
 
29
 
30
+ # Init components
31
+ rag_gen, info = init_rag_components(config)
 
 
 
 
32
 
33
+ # LLM for RAGAS evaluation
34
+ api_key = os.getenv("SILICONFLOW_API_KEY", "")
35
+ if not api_key:
36
+ raise ValueError("Missing SILICONFLOW_API_KEY")
37
+
38
+ evaluator_llm = LangchainLLMWrapper(ChatOpenAI(
39
+ model=config["llm"]["model"],
40
+ api_key=lambda: api_key, # type: ignore
41
+ base_url="https://api.siliconflow.com/v1",
42
+ temperature=config["llm"]["temperature"],
43
+ ))
44
+ evaluator_embeddings = LangchainEmbeddingsWrapper(info["embeddings"])
45
+
46
+ # Load data & generate answers
47
+ questions, ground_truths = load_csv_data(
48
+ str(REPO_ROOT / config["data"]["csv_path"]),
49
+ sample_size or config["data"]["sample_size"]
50
+ )
51
+ answers, contexts = generate_answers(
52
+ rag_gen, questions,
53
+ config["retrieval"]["top_k"],
54
+ config["retrieval"]["initial_k"]
55
+ )
56
 
57
+ # Run RAGAS
58
+ ragas_dataset = Dataset.from_dict({
59
  "question": questions,
60
+ "answer": answers,
61
+ "contexts": contexts,
62
  "ground_truth": ground_truths,
63
  })
64
 
65
+ results = evaluate(
66
+ dataset=ragas_dataset,
67
+ metrics=[faithfulness, answer_relevancy, context_precision, context_recall],
68
+ llm=evaluator_llm,
69
+ embeddings=evaluator_embeddings,
70
+ raise_exceptions=False,
71
+ run_config=RunConfig(max_workers=2, timeout=300), # Reduced workers, increased timeout
72
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
 
74
+ # Save JSON
75
+ out_path.mkdir(parents=True, exist_ok=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
77
+ json_path = out_path / f"ragas_{timestamp}.json"
78
+
79
+ # Convert results to dict
80
+ scores: dict = {}
81
+ if hasattr(results, "scores"):
82
+ scores = results.scores # type: ignore
83
+ elif hasattr(results, "__getitem__"):
84
+ for k in ["faithfulness", "answer_relevancy", "context_precision", "context_recall"]:
85
+ try:
86
+ scores[k] = float(results[k]) # type: ignore
87
+ except (KeyError, TypeError):
88
+ pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
 
90
  with open(json_path, 'w', encoding='utf-8') as f:
 
 
 
 
 
 
 
91
  json.dump({
92
  "timestamp": timestamp,
93
+ "config": {"sample_size": len(questions), **{k: v for k, v in info.items() if k != "embeddings"}},
94
+ "scores": scores,
95
+ "samples": [
96
+ {"question": q, "answer": a, "ground_truth": gt, "contexts": ctx}
97
+ for q, a, gt, ctx in zip(questions, answers, ground_truths, contexts)
98
+ ]
99
  }, f, ensure_ascii=False, indent=2)
100
 
101
+ print(f"Saved: {json_path}")
102
+ return scores
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
 
104
 
105
  if __name__ == "__main__":
106
  import argparse
107
+ parser = argparse.ArgumentParser()
108
+ parser.add_argument("--samples", type=int, default=10)
109
+ parser.add_argument("--output", type=str, default=None)
 
110
  args = parser.parse_args()
111
+ run_ragas_evaluation(args.samples, args.output)
 
evaluation/results/ragas_20260103_223452.json ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "timestamp": "20260103_223452",
3
+ "config": {
4
+ "sample_size": 2,
5
+ "gen_model": "qwen/qwen3-32b",
6
+ "eval_model": "nex-agi/DeepSeek-V3.1-Nex-N1",
7
+ "embed_model": "Qwen/Qwen3-Embedding-4B",
8
+ "collection": "hust_rag_collection",
9
+ "doc_count": 1908
10
+ },
11
+ "scores": [
12
+ {
13
+ "faithfulness": 0.75,
14
+ "answer_relevancy": 0.5568355005961533,
15
+ "context_precision": 0.99999999995,
16
+ "context_recall": 1.0
17
+ },
18
+ {
19
+ "faithfulness": 1.0,
20
+ "answer_relevancy": 0.7365395710155277,
21
+ "context_precision": 0.99999999998,
22
+ "context_recall": 1.0
23
+ }
24
+ ],
25
+ "samples": [
26
+ {
27
+ "question": "Chương trình đào tạo ngành Kỹ thuật Cơ điện tử được xây dựng dựa trên những cơ sở nào?",
28
+ "answer": "Chương trình đào tạo ngành Kỹ thuật Cơ điện tử được xây dựng trên cơ sở phát triển chương trình đào tạo ngành Cơ điện tử năm 2009, kết hợp với sự tham khảo chương trình đào tạo ngành Cơ điện tử của các trường đại học nổi tiếng như Stanford, Chico (Mỹ), Sibaura (Nhật Bản), Đại học Quốc gia Đài Loan (NTU)... và đã được kiểm định theo tiêu chuẩn AUN-QA năm 2017. \n\nNguồn: [TÀI LIỆU 1]",
29
+ "ground_truth": "Chương trình đào tạo ngành Kỹ thuật Cơ điện tử được xây dựng trên cơ sở phát triển chương trình năm 2009, kết hợp tham khảo chương trình đào tạo của các trường đại học nổi tiếng trên thế giới như Stanford, Chico (Hoa Kỳ), Sibaura (Nhật Bản) và Đại học Quốc gia Đài Loan (NTU), đồng thời được kiểm định theo tiêu chuẩn AUN-QA năm 2017.",
30
+ "contexts": [
31
+ "Chương trình đào tạo ngành Cơ điện tử hiện nay được xây dựng trên cơ sở phát triển chương trình đào tạo ngành Cơ điện tử năm 2009 kết hợp với sự tham khảo chương trình đào tạo ngành Cơ điện tử của các trường đại học nổi tiếng trên thế giới như Stanford, Chico (Koa Kỳ), Sibaura (Nhật Bản), Đại học Quốc gia Đài Loan (NTU)…; Chương trình được kiểm định theo tiêu chuẩn AUN -QA năm 2017;\nSinh viên theo học ngành này sẽ được trang bị các kiến thức cơ sở và chuyên ngành vững chắc, có kỹ năng nghề nghiệp và năng lực nghiên cứu, khả năng làm việc và sáng tạo trong mọi môi trường lao động để giải quyết những vấn đề liên quan đến nghiên cứu thiết kế, chế tạo thiết bị, hệ thống cơ điện tử và vận hành các hệ thống sản xuất công nghiệp, nhanh chóng thích ứng với môi trường làm việc của cuộc cách mạng công nghiệp 4.0.",
32
+ "Chương trình tiên tiến Kỹ thuật Cơ điện tử được xây dựng trên cơ sở phát triển chương trình đào tạo ngành Cơ điện tử của Trường Đại học Chico (Mỹ) kết hợp với sự tham khảo chương trình đào tạo của Đại học Sibaura (Nhật Bản) và Đại học Quốc gia Đài Loan (NTU). Chương trình đào tạo đã được kiểm định theo tiêu chuẩn AUN -QA năm 2017.\nSinh viên theo học ngành này sẽ được trang bị các kiến thức cơ sở và chuyên ngành vững chắc, có kỹ năng nghề nghiệp và năng lực nghiên cứu, khả năng làm việc và sáng tạo trong mọi môi trường lao động để giải quyết những vấn đề liên quan đến thiết kế, chế tạo thiết bị, hệ thống cơ điện tử và vận hành hệ thống sản xuất công nghiệp, nhanh chóng thích ứng với môi trường làm việc của cuộc cách mạng công nghiệp 4.0.\nNgôn ngữ đào tạo chính: Tiếng Anh.",
33
+ "Chương trình đào tạo được xây và dựng phát triển theo các chương trình đào tạo của Mỹ phù hợp với thực tiễn Việt Nam. Sinh viên có thể chọn 1 trong hai hướng: Kỹ thuật điều khiển và tự động hóa hoặc Hệ thống điện.\n\nTheo học chương trình tiên tiến Kỹ thuật điều khiển -Tự động hóa và Hệ thống điện Lý thuyết mạch điện – – điện tử, sinh viên được trang bị kiến thức về: Kỹ thuật đo lường và các hệ thống cảm biến thông minh; Các hệ thống mạng truyền thông công nghiệp; Kỹ thuật lập trình các chip vi điều khiển, vi xử lý; Các phương pháp điều khiển truyền thống và hiện đại; Lập trình tự động hóa các dây chuyền sản xuất công nghiệp; Điều khiển điện tử công suất và truyền động điện; Kỹ thuật điều khiển Robot; Kỹ thuật mạng nơron và trí tuệ nhân tạo (AI); Lưới điện và hệ thống điện; Bảo vệ và ổn định hệ thống điện.\n\nNgôn ng��� giảng dạy: Tiếng Anh",
34
+ "Với chương trình đào tạo linh hoạt, mềm dẻo, được xây dựng trên cơ sở tham khảo chương trình đào tạo ngành Kỹ thuật Y sinh của trường Đại học Wisconsin, một trong các trường có uy tín hàng đầu của Hoa Kỳ về đào tạo kỹ sư Kỹ thuật Y sinh, Chương trình Tiên tiến Kỹ thuật Y sinh cho phép sinh viên tiếp cận được các thành tựu phát triển mới nhất trong lĩnh vực Kỹ thuật Y sinh; cung cấp cho sinh viên các kiến thức kỹ thuật cơ bản và chuyên sâu, phát triển suy luận, nghiên cứu giải quyết các vấn đề chung của khoa học kỹ thuật và Y sinh.\n\nChương trình tiên tiến Kỹ thuật Y sinh được giảng dạy hoàn toàn bằng tiếng Anh và do các giảng viên của trường ĐHBK Hà Nội và các giảng viên nước ngoài tham gia giảng dạy.",
35
+ "1. Ngành đào tạo (sau đây gọi tắt là ngành) là một lĩnh vực chuyên môn rộng, có mã số trong Danh mục thống kê ngành đào tạo của giáo dục đại học do Bộ GDĐT quản lý 4 .\n2. Chương trình đào tạo (CTĐT) được xây dựng theo đơn vị tín chỉ (TC) và là bản thiết kế cho toàn bộ quá trình đào tạo của một ngành. Chương trình thể hiện rõ trình độ đào tạo; đối tượng đào tạo, điều kiện nhập học và điều kiện tốt nghiệp; mục tiêu đào tạo, chuẩn kiến thức, kỹ năng, mức tự chủ và trách nhiệm của người học khi tốt nghiệp; nội dung (chương trình giảng dạy); kế hoạch đào tạo theo thời gian học tập chuẩn; phương pháp và hình thức đào tạo; cách thức đánh giá kết quả học tập; các điều kiện thực hiện chương trình."
36
+ ]
37
+ },
38
+ {
39
+ "question": "Sinh viên tốt nghiệp ngành Kỹ thuật Cơ điện tử đạt được những kiến thức chuyên môn gì?",
40
+ "answer": "Sinh viên tốt nghiệp ngành Kỹ thuật Cơ điện tử đạt được các kiến thức chuyên môn sau: \n1. Có kiến thức chuyên môn rộng và vững chắc, thích ứng tốt với công việc phù hợp, chú trọng khả năng áp dụng kiến thức cơ sở và cốt lõi ngành Cơ điện tử kết hợp công cụ hiện đại để nghiên cứu, thiết kế, chế tạo, xây dựng và vận hành hệ thống/quá trình/sản phẩm Cơ điện tử. \n2. Có cơ sở kỹ thuật vững chắc và kiến thức chuyên môn sâu, kỹ năng thực hành nghề nghiệp, đủ năng lực thiết kế, chế tạo trong lĩnh vực sản xuất thiết bị, phương tiện động lực, hệ thống và thiết bị năng lượng, điều khiển thủy lực. \n3. Có năng lực thiết kế, chế tạo trong lĩnh vực sản xuất ô tô, máy động lực, xe chuyên dụng thế hệ mới (ô tô điện, hybrid điện - động cơ đốt trong, pin nhiên liệu, ô tô thông minh) và hình thành ý tưởng, thiết kế, thực hiện, vận hành hệ thống trong doanh nghiệp và xã hội. \n\nChương trình đào tạo được xây dựng dựa trên sự kết hợp từ các chương trình của Đại học Chico (Mỹ), Đại học Sibaura (Nhật Bản) và Đại học Quốc gia Đài Loan (NTU), kiểm định theo tiêu chuẩn AUN-QA năm 2017. \n\nNguồn: [TÀI LIỆU 1], [TÀI LIỆU 2], [TÀI LIỆU 3], [TÀI LIỆU 4], [TÀI LIỆU 5].",
41
+ "ground_truth": "Sinh viên tốt nghiệp có kiến thức chuyên môn rộng và vững chắc, có khả năng áp dụng kiến thức cơ sở và cốt lõi của ngành Cơ điện tử kết hợp với việc sử dụng các công cụ hiện đại để nghiên cứu, thiết kế, chế tạo, xây dựng và vận hành các hệ thống, quá trình và sản phẩm Cơ điện tử.",
42
+ "contexts": [
43
+ "Có kiến thức chuyên môn rộng và vững chắc, thích ứng tốt với những công việc phù hợp với ngành, chú trọng khả năng áp dụng kiến thức cơ sở và cốt lõi ngành Cơ điện tử kết hợp khả năng sử dụng công cụ hiện đại để nghiên cứu, thiết kế, chế tạo, xây dựng và vận hành các hệ thống/quá trình/sản phẩm cơ điện tử.",
44
+ "Có kiến thức chuyên môn rộng và vững chắc, thích ứng tốt với những công việc phù hợp với ngành, chú trọng khả năng áp dụng kiến thức cơ sở và cốt lõi ngành Cơ điện tử kết hợp khả năng sử dụng công cụ hiện đại để nghiên cứu, thiết kế, chế tạo, xây dựng và vận hành các hệ thống/quá trình/sản phẩm Cơ điện tử.",
45
+ "Sinh viên có cơ sở kỹ thuật vững chắc và kiến thức chuyên môn sâu, có kỹ năng thực hành nghề nghiệp, đủ năng lực thiết kế, chế tạo trong lĩnh vực sản xuất thiết bị, phương tiện động lực, hệ thống và thiết bị năng lượng, điều khiển thủy lực.",
46
+ "Chương trình tiên tiến Kỹ thuật Cơ điện tử được xây dựng trên cơ sở phát triển chương trình đào tạo ngành Cơ điện tử của Trường Đại học Chico (Mỹ) kết hợp với sự tham khảo chương trình đào tạo của Đại học Sibaura (Nhật Bản) và Đại học Quốc gia Đài Loan (NTU). Chương trình đào tạo đã được kiểm định theo tiêu chuẩn AUN -QA năm 2017.\nSinh viên theo học ngành này sẽ được trang bị các kiến thức cơ sở và chuyên ngành vững chắc, có kỹ năng nghề nghiệp và năng lực nghiên cứu, khả năng làm việc và sáng tạo trong mọi môi trường lao động để giải quyết những vấn đề liên quan đến thiết kế, chế tạo thiết bị, hệ thống cơ điện tử và vận hành hệ thống sản xuất công nghiệp, nhanh chóng thích ứng với môi trường làm việc của cuộc cách mạng công nghiệp 4.0.\nNgôn ngữ đào tạo chính: Tiếng Anh.",
47
+ "Sinh viên tốt nghiệp có kiến thức cơ sở kỹ thuật vững chắc và kiến thức chuyên môn sâu, có kỹ năng thực hành nghề nghiệp, đủ năng lực thiết kế, chế tạo trong lĩnh vực sản xuất ô tô, máy động lực, xe chuyên dụng thế hệ mới như ô tô điện, hybrid điện -động cơ đốt trong, pin nhiên liệu và ô tô thông minh; Có năng lực hình thành ý tưởng, thiết kế, thực hiện và vận hành các hệ thống trong doanh nghiệp và xã hội."
48
+ ]
49
+ }
50
+ ]
51
+ }
evaluation/results/rouge_eval_20260103_222829.json ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "timestamp": "20260103_222829",
3
+ "eval_type": "rouge",
4
+ "config": {
5
+ "gen_model": "qwen/qwen3-32b",
6
+ "eval_model": "nex-agi/DeepSeek-V3.1-Nex-N1",
7
+ "embed_model": "Qwen/Qwen3-Embedding-4B",
8
+ "collection": "hust_rag_collection",
9
+ "doc_count": 1908
10
+ },
11
+ "avg_scores": {
12
+ "rouge1": 0.7116580310880829,
13
+ "rouge2": 0.6484613914373089,
14
+ "rougeL": 0.6656146961846444
15
+ },
16
+ "samples": [
17
+ {
18
+ "idx": 1,
19
+ "question": "Chương trình đào tạo ngành Kỹ thuật Cơ điện tử được xây dựng dựa trên những cơ sở nào?",
20
+ "answer": "Chương trình đào tạo ngành Kỹ thuật Cơ điện tử được xây dựng trên cơ sở phát triển chương trình đào tạo ngành Cơ điện tử năm 2009, kết hợp với sự tham khảo chương trình đào tạo của các trường đại học nổi tiếng như Stanford, Chico (Mỹ), Sibaura (Nhật Bản), Đại học Quốc gia Đài Loan (NTU)... và đã được kiểm định theo tiêu chuẩn AUN-QA năm 2017. \n\nNguồn: [TÀI LIỆU 1]",
21
+ "ground_truth": "Chương trình đào tạo ngành Kỹ thuật Cơ điện tử được xây dựng trên cơ sở phát triển chương trình năm 2009, kết hợp tham khảo chương trình đào tạo của các trường đại học nổi tiếng trên thế giới như Stanford, Chico (Hoa Kỳ), Sibaura (Nhật Bản) và Đại học Quốc gia Đài Loan (NTU), đồng thời được kiểm định theo tiêu chuẩn AUN-QA năm 2017.",
22
+ "contexts": [
23
+ "Chương trình đào tạo ngành Cơ điện tử hiện nay được xây dựng trên cơ sở phát triển chương trình đào tạo ngành Cơ điện tử năm 2009 kết hợp với sự tham khảo chương trình đào tạo ngành Cơ điện tử của các trường đại học nổi tiếng trên thế giới như Stanford, Chico (Koa Kỳ), Sibaura (Nhật Bản), Đại học Quốc gia Đài Loan (NTU)…; Chương trình được kiểm định theo tiêu chuẩn AUN -QA năm 2017;\nSinh viên theo học ngành này sẽ được trang bị các kiến thức cơ sở và chuyên ngành vững chắc, có kỹ năng nghề nghiệp và năng lực nghiên cứu, khả năng làm việc và sáng tạo trong mọi môi trường lao động để giải quyết những vấn đề liên quan đến nghiên cứu thiết kế, chế tạo thiết bị, hệ thống cơ điện tử và vận hành các hệ thống sản xuất công nghiệp, nhanh chóng thích ứng với môi trường làm việc của cuộc cách mạng công nghiệp 4.0.",
24
+ "Chương trình tiên tiến Kỹ thuật Cơ điện tử được xây dựng trên cơ sở phát triển chương trình đào tạo ngành Cơ điện tử của Trường Đại học Chico (Mỹ) kết hợp với sự tham khảo chương trình đào tạo của Đại học Sibaura (Nhật Bản) và Đại học Quốc gia Đài Loan (NTU). Chương trình đào tạo đã được kiểm định theo tiêu chuẩn AUN -QA năm 2017.\nSinh viên theo học ngành này sẽ được trang bị các kiến thức cơ sở và chuyên ngành vững chắc, có kỹ năng nghề nghiệp và năng lực nghiên cứu, khả năng làm việc và sáng tạo trong mọi môi trường lao động để giải quyết những vấn đề liên quan đến thiết kế, chế tạo thiết bị, hệ thống cơ điện tử và vận hành hệ thống sản xuất công nghiệp, nhanh chóng thích ứng với môi trường làm việc của cuộc cách mạng công nghiệp 4.0.\nNgôn ngữ đào tạo chính: Tiếng Anh.",
25
+ "Chương trình đào tạo được xây và dựng phát triển theo các chương trình đào tạo của Mỹ phù hợp với thực tiễn Việt Nam. Sinh viên có thể chọn 1 trong hai hướng: Kỹ thuật điều khiển và tự động hóa hoặc Hệ thống điện.\n\nTheo học chương trình tiên tiến Kỹ thuật điều khiển -Tự động hóa và Hệ thống điện Lý thuyết mạch điện – – điện tử, sinh viên được trang bị kiến thức về: Kỹ thuật đo lường và các hệ thống cảm biến thông minh; Các hệ thống mạng truyền thông công nghiệp; Kỹ thuật lập trình các chip vi điều khiển, vi xử lý; Các phương pháp điều khiển truyền thống và hiện đại; Lập trình tự động hóa các dây chuyền sản xuất công nghiệp; Điều khiển điện tử công suất và truyền động điện; Kỹ thuật điều khiển Robot; Kỹ thuật mạng nơron và trí tuệ nhân tạo (AI); Lưới điện và hệ thống điện; Bảo vệ và ổn định hệ thống điện.\n\nNgôn ngữ giảng dạy: Tiếng Anh",
26
+ "Với chương trình đào tạo linh hoạt, mềm dẻo, được xây dựng trên cơ sở tham khảo chương trình đào tạo ngành Kỹ thuật Y sinh của trường Đại học Wisconsin, một trong các trường có uy tín hàng đầu của Hoa Kỳ về đào tạo kỹ sư Kỹ thuật Y sinh, Chương trình Tiên tiến Kỹ thuật Y sinh cho phép sinh viên tiếp cận được các thành tựu phát triển mới nhất trong lĩnh vực Kỹ thuật Y sinh; cung cấp cho sinh viên các kiến thức kỹ thuật cơ bản và chuyên sâu, phát triển suy luận, nghiên cứu giải quyết các vấn đề chung của khoa học kỹ thuật và Y sinh.\n\nChương trình tiên tiến Kỹ thuật Y sinh được giảng dạy hoàn toàn bằng tiếng Anh và do các giảng viên của trường ĐHBK Hà Nội và các giảng viên nước ngoài tham gia giảng dạy.",
27
+ "1. Ngành đào tạo (sau đây gọi tắt là ngành) là một lĩnh vực chuyên môn rộng, có mã số trong Danh mục thống kê ngành đào tạo của giáo dục đại học do Bộ GDĐT quản lý 4 .\n2. Chương trình đào tạo (CTĐT) được xây dựng theo đơn vị tín chỉ (TC) và là bản thiết kế cho toàn bộ quá trình đào tạo của một ngành. Chương trình thể hiện rõ trình độ đào tạo; đối tượng đào tạo, điều kiện nhập học và điều kiện tốt nghiệp; mục tiêu đào tạo, chuẩn kiến thức, kỹ năng, mức tự chủ và trách nhiệm của người học khi tốt nghiệp; nội dung (chương trình giảng dạy); kế hoạch đào tạo theo thời gian học tập chuẩn; phương pháp và hình thức đào tạo; cách thức đánh giá kết quả học tập; các điều kiện thực hiện chương trình."
28
+ ],
29
+ "scores": {
30
+ "rouge1": 0.8999999999999999,
31
+ "rouge2": 0.8073394495412844,
32
+ "rougeL": 0.8545454545454546
33
+ }
34
+ },
35
+ {
36
+ "idx": 2,
37
+ "question": "Sinh viên tốt nghiệp ngành Kỹ thuật Cơ điện tử đạt được những kiến thức chuyên môn gì?",
38
+ "answer": "Sinh viên tốt nghiệp ngành Kỹ thuật Cơ điện tử đạt được các kiến thức chuyên môn sau: \n1. Kiến thức cơ sở kỹ thuật vững chắc và chuyên môn sâu, có khả năng áp dụng kiến thức cơ sở và cốt lõi ngành Cơ điện tử kết hợp với công cụ hiện đại để nghiên cứu, thiết kế, chế tạo, xây dựng và vận hành các hệ thống/quá trình/sản phẩm Cơ điện tử (Tài liệu 1, 2, 3, 4). \n2. Năng lực thiết kế, chế tạo trong lĩnh vực sản xuất thiết bị, phương tiện động lực, hệ thống và thiết bị năng lượng, điều khiển thủy lực (Tài liệu 3). \n3. Khả năng hình thành ý tưởng, thiết kế, thực hiện và vận hành hệ thống trong lĩnh vực sản xuất ô tô, máy động lực, xe chuyên dụng thế hệ mới (ô tô điện, hybrid điện - động cơ đốt trong, pin nhiên liệu, ô tô thông minh) (Tài liệu 5). \n\nChương trình đào tạo đã được kiểm định theo tiêu chuẩn AUN-QA năm 2017 (Tài liệu 4).",
39
+ "ground_truth": "Sinh viên tốt nghiệp có kiến thức chuyên môn rộng và vững chắc, có khả năng áp dụng kiến thức cơ sở và cốt lõi của ngành Cơ điện tử kết hợp với việc sử dụng các công cụ hiện đại để nghiên cứu, thiết kế, chế tạo, xây dựng và vận hành các hệ thống, quá trình và sản phẩm Cơ điện tử.",
40
+ "contexts": [
41
+ "Có kiến thức chuyên môn rộng và vững chắc, thích ứng tốt với những công việc phù hợp với ngành, chú trọng khả năng áp dụng kiến thức cơ sở và cốt lõi ngành Cơ điện tử kết hợp khả năng sử dụng công cụ hiện đại để nghiên cứu, thiết kế, chế tạo, xây dựng và vận hành các hệ thống/quá trình/sản phẩm cơ điện tử.",
42
+ "Có kiến thức chuyên môn rộng và vững chắc, thích ứng tốt với những công việc phù hợp với ngành, chú trọng khả năng áp dụng kiến thức cơ sở và cốt lõi ngành Cơ điện tử kết hợp khả năng sử dụng công cụ hiện đại để nghiên cứu, thiết kế, chế tạo, xây dựng và vận hành các hệ thống/quá trình/sản phẩm Cơ điện tử.",
43
+ "Sinh viên có cơ sở kỹ thuật vững chắc và kiến thức chuyên môn sâu, có kỹ năng thực hành nghề nghiệp, đủ năng lực thiết kế, chế tạo trong lĩnh vực sản xuất thiết bị, phương tiện động lực, hệ thống và thiết bị năng lượng, điều khiển thủy lực.",
44
+ "Chương trình tiên tiến Kỹ thuật Cơ điện tử được xây dựng trên cơ sở phát triển chương trình đào tạo ngành Cơ điện tử của Trường Đại học Chico (Mỹ) kết hợp với sự tham khảo chương trình đào tạo của Đại học Sibaura (Nhật Bản) và Đại học Quốc gia Đài Loan (NTU). Chương trình đào tạo đã được kiểm định theo tiêu chuẩn AUN -QA năm 2017.\nSinh viên theo học ngành này sẽ được trang bị các kiến thức cơ sở và chuyên ngành vững chắc, có kỹ năng nghề nghiệp và năng lực nghiên cứu, khả năng làm việc và sáng tạo trong mọi môi trường lao động để giải quyết những vấn đề liên quan đến thiết kế, chế tạo thiết bị, hệ thống cơ điện tử và vận hành hệ thống sản xuất công nghiệp, nhanh chóng thích ứng với môi trường làm việc của cuộc cách mạng công nghiệp 4.0.\nNgôn ngữ đào tạo chính: Tiếng Anh.",
45
+ "Sinh viên tốt nghiệp có kiến thức cơ sở kỹ thuật vững chắc và kiến thức chuyên môn sâu, có kỹ năng thực hành nghề nghiệp, đủ năng lực thiết kế, chế tạo trong lĩnh vực sản xuất ô tô, máy động lực, xe chuyên dụng thế hệ mới như ô tô điện, hybrid điện -động cơ đốt trong, pin nhiên liệu và ô tô thông minh; Có năng lực hình thành ý tưởng, thiết kế, thực hiện và vận hành các hệ thống trong doanh nghiệp và xã hội."
46
+ ],
47
+ "scores": {
48
+ "rouge1": 0.5233160621761658,
49
+ "rouge2": 0.4895833333333333,
50
+ "rougeL": 0.4766839378238343
51
+ }
52
+ }
53
+ ]
54
+ }
evaluation/rouge_eval.py ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+ import argparse
3
+ from pathlib import Path
4
+ from typing import Optional
5
+
6
+ from rouge_score import rouge_scorer
7
+
8
+ from evaluation.eval_utils import (
9
+ REPO_ROOT, load_config, load_csv_data, init_rag_components,
10
+ generate_answers, save_eval_report, print_scores,
11
+ )
12
+
13
+
14
+ def calculate_rouge_scores(answers: list, ground_truths: list) -> tuple[dict, list]:
15
+ scorer = rouge_scorer.RougeScorer(['rouge1', 'rouge2', 'rougeL'], use_stemmer=False)
16
+ per_sample = []
17
+
18
+ for answer, gt in zip(answers, ground_truths):
19
+ gt = gt[0] if isinstance(gt, list) else gt
20
+ if not answer.strip() or not gt.strip():
21
+ per_sample.append({"rouge1": 0.0, "rouge2": 0.0, "rougeL": 0.0})
22
+ continue
23
+
24
+ scores = scorer.score(gt, answer)
25
+ per_sample.append({k: scores[k].fmeasure for k in ['rouge1', 'rouge2', 'rougeL']})
26
+
27
+ avg = {k: sum(s[k] for s in per_sample) / len(per_sample) for k in ['rouge1', 'rouge2', 'rougeL']} if per_sample else {}
28
+ return avg, per_sample
29
+
30
+
31
+ def run_rouge_evaluation(sample_size: int = 10, output_dir: Optional[str] = None) -> dict:
32
+ print("=" * 60 + "\nROUGE EVALUATION\n" + "=" * 60)
33
+
34
+ config = load_config()
35
+ out_path = Path(output_dir) if output_dir else REPO_ROOT / config.get("output", {}).get("dir", "evaluation/results")
36
+
37
+ print("\n[Initializing...]")
38
+ rag_gen, info = init_rag_components(config)
39
+ print(f" Gen LLM: {info['gen_model']}, Embeddings: {info['embed_model']}")
40
+ print(f" Vector DB: {info['collection']} ({info['doc_count']} docs)")
41
+
42
+ csv_path = str(REPO_ROOT / config["data"]["csv_path"])
43
+ sample_size = sample_size or config["data"].get("sample_size", 10)
44
+ questions, ground_truths = load_csv_data(csv_path, sample_size)
45
+ print(f"\n[Data] Loaded {len(questions)} samples")
46
+
47
+ print("\n[Generating answers...]")
48
+ answers, contexts = generate_answers(
49
+ rag_gen, questions,
50
+ top_k=config["retrieval"]["top_k"],
51
+ initial_k=config["retrieval"]["initial_k"],
52
+ )
53
+
54
+ print("\n[Calculating ROUGE scores...]")
55
+ avg_scores, per_sample = calculate_rouge_scores(answers, ground_truths)
56
+
57
+ print("\n" + "=" * 60 + "\nROUGE RESULTS\n" + "=" * 60)
58
+ print_scores("ROUGE Metrics", avg_scores)
59
+
60
+ save_eval_report(
61
+ out_path, "rouge", questions, answers, ground_truths, contexts,
62
+ per_sample, avg_scores, {k: v for k, v in info.items() if k != "embeddings"},
63
+ )
64
+
65
+ print("\n ROUGE Evaluation complete!")
66
+ return avg_scores
67
+
68
+
69
+ if __name__ == "__main__":
70
+ parser = argparse.ArgumentParser(description="ROUGE Evaluation")
71
+ parser.add_argument("--samples", type=int, default=10, help="Number of samples (0=all)")
72
+ parser.add_argument("--output", type=str, default=None, help="Output directory")
73
+ args = parser.parse_args()
74
+ run_rouge_evaluation(sample_size=args.samples, output_dir=args.output)
requirements.txt CHANGED
@@ -1,4 +1,5 @@
1
  langchain
 
2
  langchain-chroma
3
  chromadb
4
  datasets
 
1
  langchain
2
+ langchain-openai
3
  langchain-chroma
4
  chromadb
5
  datasets
scripts/rag.py CHANGED
@@ -37,13 +37,13 @@ def main():
37
  print("=" * 60)
38
 
39
  print("\n[1/4] Initializing embedder...")
40
- emb_cfg = EmbeddingConfig.from_yaml()
41
  emb = QwenEmbeddings(emb_cfg)
42
  print(f" ✓ Model: {emb_cfg.model}")
43
  print(f" ✓ API: {emb_cfg.api_base_url}")
44
 
45
  print("\n[2/4] Initializing ChromaDB...")
46
- db_cfg = ChromaConfig.from_yaml()
47
  db = ChromaVectorDB(embedder=emb, config=db_cfg)
48
  old_count = db.count()
49
  print(f" ✓ Collection: {db_cfg.collection_name}")
 
37
  print("=" * 60)
38
 
39
  print("\n[1/4] Initializing embedder...")
40
+ emb_cfg = EmbeddingConfig()
41
  emb = QwenEmbeddings(emb_cfg)
42
  print(f" ✓ Model: {emb_cfg.model}")
43
  print(f" ✓ API: {emb_cfg.api_base_url}")
44
 
45
  print("\n[2/4] Initializing ChromaDB...")
46
+ db_cfg = ChromaConfig()
47
  db = ChromaVectorDB(embedder=emb, config=db_cfg)
48
  old_count = db.count()
49
  print(f" ✓ Collection: {db_cfg.collection_name}")
scripts/run_eval.py ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+ import argparse
3
+ import sys
4
+ from pathlib import Path
5
+ from datetime import datetime
6
+
7
+ REPO_ROOT = Path(__file__).resolve().parents[1]
8
+ if str(REPO_ROOT) not in sys.path:
9
+ sys.path.insert(0, str(REPO_ROOT))
10
+
11
+
12
+ def print_header(mode: str):
13
+ """Print evaluation header."""
14
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
15
+ print("\n" + "=" * 70)
16
+ print(f"{'RAG EVALUATION':^70}")
17
+ print(f"{'Mode: ' + mode.upper():^70}")
18
+ print(f"{timestamp:^70}")
19
+ print("=" * 70)
20
+
21
+ def run_rouge_only(samples: int, output_dir: str):
22
+ from evaluation.rouge_eval import run_rouge_evaluation
23
+ return run_rouge_evaluation(sample_size=samples, output_dir=output_dir)
24
+
25
+
26
+ def run_ragas_only(samples: int, output_dir: str):
27
+ from evaluation.ragas_eval import run_ragas_evaluation
28
+ return run_ragas_evaluation(sample_size=samples, output_dir=output_dir)
29
+
30
+
31
+ def run_all(samples: int, output_dir: str):
32
+ print("\n" + "-" * 70)
33
+ print("STEP 1/2: ROUGE EVALUATION")
34
+ print("-" * 70)
35
+ rouge_scores = run_rouge_only(samples, output_dir)
36
+
37
+ print("\n" + "-" * 70)
38
+ print("STEP 2/2: RAGAS EVALUATION")
39
+ print("-" * 70)
40
+ ragas_scores = run_ragas_only(samples, output_dir)
41
+
42
+ # Combine and print final summary
43
+ print("\n" + "=" * 70)
44
+ print(f"{'FINAL EVALUATION SUMMARY':^70}")
45
+ print("=" * 70)
46
+
47
+ if rouge_scores:
48
+ print("\n[ROUGE Metrics - Text-based]")
49
+ for metric, score in rouge_scores.items():
50
+ bar = "#" * int(float(score) * 20) + "-" * (20 - int(float(score) * 20))
51
+ print(f" {metric:25} [{bar}] {score:.4f}")
52
+
53
+ if ragas_scores:
54
+ print("\n[RAGAS Metrics - LLM-based]")
55
+ import math
56
+ for metric in ["faithfulness", "answer_relevancy", "context_precision", "context_recall"]:
57
+ if metric in ragas_scores:
58
+ score = float(ragas_scores[metric])
59
+ if not math.isnan(score):
60
+ bar = "#" * int(score * 20) + "-" * (20 - int(score * 20))
61
+ print(f" {metric:25} [{bar}] {score:.4f}")
62
+ else:
63
+ print(f" {metric:25} [FAILED]")
64
+
65
+ print("\n" + "=" * 70)
66
+ print(" All evaluations complete!")
67
+
68
+ return {"rouge": rouge_scores, "ragas": ragas_scores}
69
+
70
+
71
+ def main():
72
+ parser = argparse.ArgumentParser(
73
+ description="RAG Evaluation Script - Run ROUGE and/or RAGAS metrics",
74
+ formatter_class=argparse.RawDescriptionHelpFormatter,
75
+ epilog="""
76
+ Examples:
77
+ python evaluation/run_eval.py --mode all --samples 10
78
+ python evaluation/run_eval.py --mode rouge --samples 20
79
+ python evaluation/run_eval.py --mode ragas --samples 5
80
+ python evaluation/run_eval.py --mode all --samples 0 # All samples
81
+
82
+ Evaluation modes:
83
+ rouge - Text-based metrics (ROUGE-1, ROUGE-2, ROUGE-L)
84
+ ragas - LLM-based metrics (Faithfulness, Answer Relevancy, etc.)
85
+ all - Both ROUGE and RAGAS
86
+ """
87
+ )
88
+
89
+ parser.add_argument(
90
+ "--mode",
91
+ type=str,
92
+ default="all",
93
+ choices=["rouge", "ragas", "all"],
94
+ help="Evaluation mode: rouge, ragas, or all (default: all)"
95
+ )
96
+ parser.add_argument(
97
+ "--samples",
98
+ type=int,
99
+ default=10,
100
+ help="Number of samples to evaluate (0 = all samples, default: 10)"
101
+ )
102
+ parser.add_argument(
103
+ "--output",
104
+ type=str,
105
+ default=None,
106
+ help="Output directory for results (default: evaluation/results)"
107
+ )
108
+
109
+ args = parser.parse_args()
110
+
111
+ print_header(args.mode)
112
+ print(f"\n[Configuration]")
113
+ print(f" Mode: {args.mode}")
114
+ print(f" Samples: {args.samples if args.samples > 0 else 'ALL'}")
115
+ print(f" Output: {args.output or 'evaluation/results'}")
116
+
117
+ if args.mode == "rouge":
118
+ run_rouge_only(args.samples, args.output)
119
+ elif args.mode == "ragas":
120
+ run_ragas_only(args.samples, args.output)
121
+ else:
122
+ run_all(args.samples, args.output)
123
+
124
+
125
+ if __name__ == "__main__":
126
+ main()
test/llama.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from llama_parse import LlamaParse, ResultType
3
+ from dotenv import load_dotenv
4
+
5
+ load_dotenv()
6
+
7
+ def test_llamaparse(file_path: str):
8
+ parser = LlamaParse(
9
+ result_type=ResultType.MD, # Định dạng Markdown giúp giữ cấu trúc bảng cực tốt
10
+ num_workers=4, # Chạy song song để tăng tốc
11
+ verbose=True,
12
+ language="vi" # Cấu hình ngôn ngữ tiếng Việt
13
+ )
14
+
15
+ print(f"--- Đang bắt đầu xử lý file: {file_path} ---")
16
+
17
+ # Thực hiện trích xuất dữ liệu
18
+ documents = parser.load_data(file_path)
19
+
20
+ if documents:
21
+ # In thử 1000 ký tự đầu tiên để kiểm tra
22
+ print("\n--- Kết quả trích xuất mẫu ---")
23
+ print(documents[0].text[:1000])
24
+
25
+ # Lưu kết quả ra file .md để bạn xem cấu trúc bảng có bị lệch không
26
+ output_name = "data/parsed_llama_result.md"
27
+ with open(output_name, "w", encoding="utf-8") as f:
28
+ for doc in documents:
29
+ f.write(doc.text + "\n\n")
30
+ print(f"\n--- Đã lưu toàn bộ nội dung vào: {output_name} ---")
31
+ else:
32
+ print("Lỗi: Không tìm thấy nội dung trong file.")
33
+
34
+ if __name__ == "__main__":
35
+ # Thay đường dẫn đến file PDF quy chế của bạn vào đây
36
+ PATH_TO_PDF = "data/data_raw/quyet_dinh/quy-dinh-chuan-ngoai-ngu-2021.pdf"
37
+
38
+ api_key = os.getenv("LLAMA_CLOUD_API_KEY")
39
+ if not api_key:
40
+ print("Lỗi: Bạn chưa cấu hình LLAMA_CLOUD_API_KEY trong file .env")
41
+ else:
42
+ test_llamaparse(PATH_TO_PDF)
test/test_chunk.py CHANGED
@@ -1,37 +1,47 @@
1
  import sys
2
  sys.path.insert(0, "/home/bahung/DoAn")
3
 
4
- from core.embeddings.chunk import chunk_markdown_file, ChunkConfig
5
 
6
- test_file = "data/data_process/quy_che/QCDT_2025_5445_QD-DHBK.md"
7
 
8
  print("=" * 70)
9
- print(f"📄 File: {test_file}")
10
  print("=" * 70)
11
 
12
- chunks = chunk_markdown_file(test_file, config=ChunkConfig())
 
13
 
14
- print(f"\n📊 Total chunks: {len(chunks)}\n")
15
 
16
- for i, chunk in enumerate(chunks):
 
 
 
17
  print(f"\n{'─' * 70}")
18
- print(f"🔹 CHUNK #{i}")
19
- print(f" Section: {chunk['metadata'].get('section', 'N/A')}")
20
- print(f" Length: {len(chunk['content'])} chars")
 
 
21
  print(f"{'─' * 70}")
22
- content_preview = chunk['content'][:200]
23
- if len(chunk['content']) > 200:
24
  content_preview += "..."
25
  print(content_preview)
26
 
27
  with open("test_chunk.md", "w", encoding="utf-8") as f:
28
- for i, chunk in enumerate(chunks):
29
- f.write(f"# CHUNK {i}\n")
 
 
 
 
30
  f.write("**Metadata:**\n")
31
- for key, value in chunk['metadata'].items():
32
  f.write(f"- {key}: {value}\n")
33
  f.write("\n**Content:**\n")
34
- f.write(chunk['content'])
35
  f.write("\n\n---\n\n")
36
 
37
- print("\n Done")
 
1
  import sys
2
  sys.path.insert(0, "/home/bahung/DoAn")
3
 
4
+ from core.embeddings.chunk import chunk_markdown_file
5
 
6
+ test_file = "data/docling_output/chuong_trinh_dao_tao/1.1. Kỹ thuật Cơ điện tử.md"
7
 
8
  print("=" * 70)
9
+ print(f" File: {test_file}")
10
  print("=" * 70)
11
 
12
+ # Now returns List[BaseNode] instead of List[Dict]
13
+ nodes = chunk_markdown_file(test_file)
14
 
15
+ print(f"\n Total nodes: {len(nodes)}\n")
16
 
17
+ for i, node in enumerate(nodes):
18
+ content = node.get_content()
19
+ metadata = node.metadata
20
+
21
  print(f"\n{'─' * 70}")
22
+ print(f" NODE #{i}")
23
+ print(f" Type: {type(node).__name__}")
24
+ print(f" Length: {len(content)} chars")
25
+ if metadata:
26
+ print(f" Metadata: {metadata}")
27
  print(f"{'─' * 70}")
28
+ content_preview = content[:200]
29
+ if len(content) > 200:
30
  content_preview += "..."
31
  print(content_preview)
32
 
33
  with open("test_chunk.md", "w", encoding="utf-8") as f:
34
+ for i, node in enumerate(nodes):
35
+ content = node.get_content()
36
+ metadata = node.metadata
37
+
38
+ f.write(f"# NODE {i}\n")
39
+ f.write(f"**Type:** {type(node).__name__}\n\n")
40
  f.write("**Metadata:**\n")
41
+ for key, value in metadata.items():
42
  f.write(f"- {key}: {value}\n")
43
  f.write("\n**Content:**\n")
44
+ f.write(content)
45
  f.write("\n\n---\n\n")
46
 
47
+ print("\n Done")
test_chunk.md CHANGED
The diff for this file is too large to render. See raw diff