File size: 8,314 Bytes
8eaa451
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
"""
Prepare external drug markdown files into DeepMed RAG-ready dataset format.

Usage:
  python prepare_rag_drug_data.py --source "e:/AI/Thuốc" --target "e:/AI/DeepMed/backend/data"

This script:
  1) Copies and normalizes files from:
       - <source>/Thuốc nội bộ -> <target>/thông tin thuốc nội bộ
       - <source>/cảnh giác dược -> <target>/cảnh giác dược
  2) Cleans markdown noise (widget/image-only lines, redundant separators)
  3) Ensures each drug markdown begins with:
       # <DRUG_NAME>
       Hoạt chất: <value or Chưa cập nhật>
  4) Optionally removes long hash suffixes in filenames.
"""

from __future__ import annotations

import argparse
import hashlib
import os
import re
import shutil
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, Iterable, List, Optional, Tuple

HASH_SUFFIX_RE = re.compile(r"\s+[0-9a-f]{20,}(?=\.md$)", re.IGNORECASE)
MULTI_SPACE_RE = re.compile(r"[ \t]{2,}")
EMPTY_LINE_RE = re.compile(r"\n{3,}")
HEADING_RE = re.compile(r"^#\s+(.+)$", re.MULTILINE)
ACTIVE_RE = re.compile(r"^\s*Hoạt\s*chất\s*:\s*(.+)$", re.IGNORECASE | re.MULTILINE)
WIDGET_LINK_RE = re.compile(r"^\s*\[https?://widgetbox\.app/.*\]\(https?://widgetbox\.app/.*\)\s*$", re.IGNORECASE)
IMAGE_LINE_RE = re.compile(r"^\s*!\[[^\]]*\]\([^\)]*\)\s*$")
HORIZONTAL_RULE_RE = re.compile(r"^\s*---\s*$")


@dataclass
class SyncStats:
    copied: int = 0
    skipped: int = 0
    errors: int = 0


def _clean_filename(name: str, remove_hash_suffix: bool = True) -> str:
    cleaned = name.strip()
    if remove_hash_suffix and cleaned.lower().endswith(".md"):
        cleaned = HASH_SUFFIX_RE.sub("", cleaned)
    cleaned = MULTI_SPACE_RE.sub(" ", cleaned)
    return cleaned


def _safe_output_path(dest_dir: Path, desired_name: str, content: str) -> Path:
    out = dest_dir / desired_name
    if not out.exists():
        return out

    digest = hashlib.md5(content.encode("utf-8", errors="ignore")).hexdigest()[:8]
    stem = out.stem
    suffix = out.suffix
    return dest_dir / f"{stem}__{digest}{suffix}"


def _normalize_newlines(text: str) -> str:
    return text.replace("\r\n", "\n").replace("\r", "\n")


def _strip_noise_lines(text: str) -> str:
    lines = []
    for raw in text.split("\n"):
        line = raw.replace("\xa0", " ").rstrip()

        if WIDGET_LINK_RE.match(line):
            continue
        if IMAGE_LINE_RE.match(line):
            continue
        if HORIZONTAL_RULE_RE.match(line):
            continue

        lines.append(line)

    normalized = "\n".join(lines)
    normalized = EMPTY_LINE_RE.sub("\n\n", normalized)
    return normalized.strip()


def _extract_heading(content: str) -> Optional[str]:
    m = HEADING_RE.search(content)
    if not m:
        return None
    return m.group(1).strip()


def _extract_active_ingredient(content: str) -> Optional[str]:
    m = ACTIVE_RE.search(content)
    if not m:
        return None
    return m.group(1).strip()


def _ensure_rag_header(content: str, fallback_name: str) -> str:
    heading = _extract_heading(content) or fallback_name
    active = _extract_active_ingredient(content) or "Chưa cập nhật"

    body = content
    body = HEADING_RE.sub("", body, count=1).lstrip("\n")
    body = ACTIVE_RE.sub("", body, count=1).lstrip("\n")

    header = f"# {heading}\n\nHoạt chất: {active}\n"
    out = f"{header}\n{body.strip()}\n"
    out = EMPTY_LINE_RE.sub("\n\n", out)
    return out


def _read_text(path: Path) -> str:
    return path.read_text(encoding="utf-8", errors="ignore")


def _write_text(path: Path, content: str) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(content, encoding="utf-8", newline="\n")


def _iter_files(root: Path) -> Iterable[Path]:
    for p in root.rglob("*"):
        if p.is_file() and p.suffix.lower() in {".md", ".pdf", ".docx", ".txt", ".csv", ".xlsx", ".xls"}:
            yield p


def _normalize_markdown_for_drug(md_text: str, fallback_drug_name: str) -> str:
    text = _normalize_newlines(md_text)
    text = _strip_noise_lines(text)
    text = _ensure_rag_header(text, fallback_drug_name)
    return text


def _sync_folder(
    source: Path,
    target: Path,
    remove_hash_suffix: bool,
    drug_mode: bool,
    flatten_tree: bool = False,
) -> SyncStats:
    stats = SyncStats()

    if not source.exists():
        print(f"[WARN] Source folder not found: {source}")
        return stats

    target.mkdir(parents=True, exist_ok=True)

    seen_names: Dict[str, int] = {}

    for src in _iter_files(source):
        try:
            rel = src.relative_to(source)
            clean_name = _clean_filename(src.name, remove_hash_suffix=remove_hash_suffix)

            base_target_dir = target if flatten_tree else (target / rel.parent)
            base_target_dir.mkdir(parents=True, exist_ok=True)

            if src.suffix.lower() == ".md":
                raw = _read_text(src)
                if drug_mode:
                    fallback = Path(clean_name).stem
                    cleaned = _normalize_markdown_for_drug(raw, fallback)
                else:
                    cleaned = _strip_noise_lines(_normalize_newlines(raw)) + "\n"

                out_path = _safe_output_path(base_target_dir, clean_name, cleaned)
                if out_path.exists():
                    existing = _read_text(out_path)
                    if existing == cleaned:
                        stats.skipped += 1
                        continue
                _write_text(out_path, cleaned)
            else:
                out_path = base_target_dir / clean_name
                if out_path.exists() and out_path.stat().st_size == src.stat().st_size:
                    stats.skipped += 1
                    continue
                shutil.copy2(src, out_path)

            seen_names[out_path.name] = seen_names.get(out_path.name, 0) + 1
            stats.copied += 1
        except Exception as exc:
            stats.errors += 1
            print(f"[ERROR] Failed: {src} -> {exc}")

    dup_count = sum(1 for n in seen_names.values() if n > 1)
    if dup_count:
        print(f"[WARN] Found {dup_count} duplicate output names; hash suffix applied.")

    return stats


def run(source_root: Path, target_root: Path, remove_hash_suffix: bool = True) -> Tuple[SyncStats, SyncStats]:
    source_drug = source_root / "Thuốc nội bộ"
    source_alert = source_root / "cảnh giác dược"

    target_drug = target_root / "thông tin thuốc nội bộ"
    target_alert = target_root / "cảnh giác dược"

    print("=" * 72)
    print(f"Source root: {source_root}")
    print(f"Target root: {target_root}")
    print("=" * 72)

    drug_stats = _sync_folder(
        source=source_drug,
        target=target_drug,
        remove_hash_suffix=remove_hash_suffix,
        drug_mode=True,
    )

    alert_stats = _sync_folder(
        source=source_alert,
        target=target_alert,
        remove_hash_suffix=remove_hash_suffix,
        drug_mode=False,
        flatten_tree=True,
    )

    print("=" * 72)
    print("Sync completed")
    print(
        "Drug files: copied={0.copied}, skipped={0.skipped}, errors={0.errors} | "
        "Alert files: copied={1.copied}, skipped={1.skipped}, errors={1.errors}".format(
            drug_stats, alert_stats
        )
    )
    print("=" * 72)

    return drug_stats, alert_stats


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="Prepare Thuốc files for DeepMed RAG")
    parser.add_argument(
        "--source",
        default=r"e:/AI/Thuốc",
        help="Path to external Thuốc folder",
    )
    parser.add_argument(
        "--target",
        default=r"e:/AI/DeepMed/backend/data",
        help="Path to DeepMed backend data folder",
    )
    parser.add_argument(
        "--keep-hash-suffix",
        action="store_true",
        help="Keep long hash suffixes in markdown filenames",
    )
    return parser.parse_args()


def main() -> None:
    args = parse_args()
    source_root = Path(args.source).resolve()
    target_root = Path(args.target).resolve()

    run(
        source_root=source_root,
        target_root=target_root,
        remove_hash_suffix=not args.keep_hash_suffix,
    )


if __name__ == "__main__":
    main()