Skip to main content
Glama
README.md
<div align="center">

# vnlaw-kg

**Biến văn bản luật thành knowledge graph tra cứu được — rồi cắm thẳng vào AI của bạn.**

[![CI](https://github.com/handonn2000/vnlawkg/actions/workflows/ci.yml/badge.svg)](https://github.com/handonn2000/vnlawkg/actions/workflows/ci.yml)
[![PyPI](https://img.shields.io/pypi/v/vnlaw-kg.svg)](https://pypi.org/project/vnlaw-kg/)
[![Python](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://pypi.org/project/vnlaw-kg/)
[![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)
[![Tests](https://img.shields.io/badge/tests-99%20passed-brightgreen.svg)](tests/)
[![Zero deps](https://img.shields.io/badge/core%20deps-0-brightgreen.svg)](pyproject.toml)

*Nạp PDF luật vào → nhận graph có cấu trúc → AI trả lời kèm trích dẫn nguyên văn, không bịa số điều.*

</div>

---

## Vấn đề

Hỏi một LLM *"mức phạt vi phạm hợp đồng thương mại tối đa là bao nhiêu?"* — nó thường trả lời đúng **8%**, nhưng **dẫn sai số điều**. Trong pháp lý, sai một số điều là mất toàn bộ độ tin cậy.

`vnlaw-kg` buộc AI **đọc trước khi trích**. Mọi kết quả đều kèm nguyên văn, mã tra lại được, và trạng thái hiệu lực.

## Demo

```console
$ vnlaw-kg ingest luat-thuong-mai.pdf bo-luat-dan-su.pdf --doc-id LTM BLDS --db kho.db

Đã nạp 2 tài liệu → kho.db
  LTM:  324 điều,  684 khoản, độ phủ 99.9%
  BLDS: 689 điều, 1418 khoản, độ phủ 100.0%
  Graph: 3439 node / 3720 cạnh
```

```console
$ vnlaw-kg search "mức phạt vi phạm hợp đồng tối đa" --db kho.db -k 2

### Điều 301 — LTM  (điểm 0.0333)
    Mức phạt vi phạm
    Mức phạt đối với vi phạm nghĩa vụ hợp đồng hoặc tổng mức phạt đối với
    nhiều vi phạm do các bên thỏa thuận trong hợp đồng, nhưng không quá 8%
    giá trị phần nghĩa vụ hợp đồng bị vi phạm, trừ trường hợp quy định tại
    Điều 266 của Luật này.

### Điều 300 — LTM  (điểm 0.0323)
    Phạt vi phạm
    ...
```

Điều đã bãi bỏ **không im lặng biến mất** — nó nói rõ ai đã bãi bỏ và từ bao giờ:

```console
$ vnlaw-kg article LTM 102 --db kho.db

=== Điều 102 — LTM ===
Vị trí: Chương IV. XÚC TIẾN THƯƠNG MẠI > Mục 2
Trạng thái: ĐÃ BỊ BÃI BỎ

Điều 102. (được bãi bỏ)

✎ Lịch sử sửa đổi:
   • bãi bỏ bởi 75/2025/QH15 — Mục này bao gồm các điều 102...116 được bãi bỏ
     theo khoản 2 Điều 2 của Luật số 75/2025/QH15, hiệu lực từ 01/01/2026.
```

## Trực quan hóa

Graph không chỉ để tra cứu — nó cho thấy **cụm quy phạm nào phải đọc cùng nhau**:

![Mạng dẫn chiếu](docs/reference-network.svg)

```bash
vnlaw-kg viz --db kho.db -o graph.html     # đồ thị tương tác, mở bằng browser
vnlaw-kg viz --db kho.db --format svg -o network.svg
```

## Cài đặt

```bash
pip install vnlaw-kg                 # lõi — KHÔNG phụ thuộc gì
pip install "vnlaw-kg[all]"          # + TF-IDF, PDF, DOCX, HTML
```

Lõi chỉ dùng SQLite + FTS5 có sẵn trong Python, chạy **hoàn toàn ngoại tuyến** — quan trọng với hồ sơ pháp lý nhạy cảm. CI kiểm tra điều này mỗi lần commit.

| Extra | Thêm gì | Khi nào cần |
|---|---|---|
| `[search]` | scikit-learn | truy hồi TF-IDF — **khuyến nghị** |
| `[embeddings]` | sentence-transformers | câu hỏi trò chuyện, không dùng từ của luật |
| `[pdf]` | pypdf | máy không có `pdftotext` (poppler) |
| `[docx]` | python-docx | đọc file Word |
| `[graph]` | kuzu | truy vấn graph nâng cao khi nạp nhiều luật |

## Cắm vào AI qua MCP

Đây là cách dùng chính. Thêm vào `claude_desktop_config.json` (hoặc cấu hình MCP của Cursor / Cline):

```json
{
  "mcpServers": {
    "vnlaw-kg": {
      "command": "vnlaw-kg",
      "args": ["mcp", "--db", "/duong/dan/kho.db"]
    }
  }
}
```

AI nhận 5 tool: `search_law`, `get_article`, `get_concept`, `get_outline`, `list_documents`. Từ đó bạn hỏi bằng ngôn ngữ tự nhiên và nó tự tra graph thay vì đoán theo trí nhớ.

## Use case

| Ai | Dùng để làm gì |
|---|---|
| **Luật sư / pháp chế** | Tra nhanh cụm quy phạm liên quan; kiểm tra điều đã bị bãi bỏ trước khi viện dẫn vào hợp đồng |
| **Dev làm sản phẩm legal-tech** | Backend RAG có cấu trúc, thay vì chunk PDF thô rồi cầu may |
| **Doanh nghiệp** | Nạp nội quy, quy chế nội bộ + luật liên quan vào một graph, cho nhân viên hỏi đáp |
| **Nghiên cứu / giảng dạy** | Phân tích mạng dẫn chiếu, đo mức độ trung tâm của từng chế định |
| **Người dùng AI cá nhân** | Cho Claude/Cursor một nguồn luật đáng tin thay vì để nó bịa |

## Benchmark

Đo trên Bộ luật Dân sự 2015 + Luật Thương mại 2005 (VBHN 2025), MacBook M-series:

**Tốc độ**

| Thao tác | Thời gian |
|---|---|
| Nạp 2 luật (1.013 điều, 2.102 khoản) | **0,42s** |
| Nạp kèm chỉ mục TF-IDF | **1,36s** |
| Tìm kiếm (p50 / p95) | **1ms / 3ms** |
| Tra một điều | **0,1ms** |
| Kho SQLite | 6,8 MB |
| Nạp vào KuzuDB bằng `COPY` | **0,09s** (so với 55s nếu `CREATE` từng dòng) |

**Độ chính xác parse** — đối chiếu với bản dữ liệu đã kiểm chứng thủ công qua nhiều nguồn:

| | Điều | Khoản | Điều bãi bỏ | Độ phủ nội dung |
|---|---|---|---|---|
| Bộ luật Dân sự 2015 | **689/689** | **1.418/1.418** | — | **100%** |
| Luật Thương mại 2005 | **324/324** | **684/684** | **23/23** | **99,9%** |
| Cạnh `REFERS_TO` | **243/243** (trùng 100% bản curated tay) | | | |

**Chất lượng truy hồi** — 19 câu hỏi pháp lý có đáp án biết trước ([`tests/test_retrieval_quality.py`](tests/test_retrieval_quality.py)):

| Chỉ số | Kết quả |
|---|---|
| Hit@1 | 11/19 = 58% |
| Hit@3 | 18/19 = **95%** |
| Hit@5 | 19/19 = **100%** |

> Con số lấy từ test chạy trong CI, không phải ước lượng — chạy lại bằng `pytest tests/test_retrieval_quality.py`. Bộ 19 câu còn nhỏ nên hãy đọc nó như *mức sàn chống hồi quy*, không phải điểm tuyệt đối; mở rộng bộ câu hỏi là việc đầu tiên trong [hướng phát triển](#hướng-phát-triển).
>
> Câu hỏi định nghĩa (*"thương nhân là gì"*) được định tuyến riêng qua cạnh `DEFINES`: nếu chỉ khớp từ khóa thì nó rớt khỏi top-10, vì cụm "thương nhân" xuất hiện ở hàng trăm điều.

## Tự kiểm chất lượng — không cần viết test tay

Parse sai kiểu **âm thầm nuốt mất nội dung** là rủi ro lớn nhất: không con số đếm nào lộ ra. Nên mỗi lần nạp, thư viện tự chạy các **bất biến cấu trúc** đúng với mọi văn bản:

- **Độ phủ ký tự** — bao nhiêu phần trăm nội dung nguồn thực sự vào graph
- **Liên tục số điều** — thiếu điều giữa dải là dấu hiệu sót cả trang
- **Giải dẫn chiếu** — *"Điều 266 của Luật này"* có trỏ tới điều có thật không
- **Round-trip** — lưu ra rồi đọc lại không đổi nội dung
- Trùng số điều, điều rỗng, khoản đánh số bất thường

```console
$ vnlaw-kg check ban-trich-mot-phan.pdf

✘ [GAP_ARTICLE]   Thiếu 215 số điều trong dải 1–319.
✘ [DANGLING_REF]  2 dẫn chiếu nội bộ trỏ tới điều không tồn tại (giải được 88,2%).
```

Nó phát hiện file là bản trích thiếu — **không cần ai dạy trước** về luật đó.

## Dùng như thư viện

```python
from vnlawkg import ingest, open_store

ingest("bo-luat-dan-su.pdf", db="kho.db")

with open_store("kho.db") as st:
    for hit in st.search("lãi suất cho vay tối đa", k=5):
        print(f"Điều {hit.num} ({hit.doc}) — {hit.title}")

    art = st.article("BLDS", "468")
    print(art["text"])
    for n in st.neighbors(art["id"], "REFERS_TO", "in"):
        print("được dẫn chiếu bởi:", n["props"]["cite"])
```

## Hỗ trợ điều bổ sung (`Điều 12a`)

Luật sửa đổi chèn điều mới bằng hậu tố chữ: `Điều 12a` nằm giữa `Điều 12` và `Điều 13`. Nhiều công cụ đọc số điều bằng `int` nên `Điều 12a` thành `12` và **ghi đè** `Điều 12` thật, không báo lỗi. Ở đây số điều là chuỗi có khóa sắp xếp riêng, nên hai điều tồn tại độc lập.

## Kiến trúc

```
tài liệu (PDF/DOCX/TXT/MD/HTML)
   │  loaders.py      — trích text: pdftotext > pypdf, OCR tùy chọn
   ▼
dialects/vietnam.py   — Phần/Chương/Mục/Tiểu mục/Điều/Khoản/Điểm,
   │                    footnote sửa đổi, điều & mục bị bãi bỏ
   ▼
model.py              — Document / Article / Clause / Point (trung lập)
   │
   ├─ validate.py     — bất biến cấu trúc, báo cáo ERROR/WARN/INFO
   ▼
graph.py              — dẫn chiếu, khái niệm, sửa đổi, liên kết chéo tài liệu
   ▼
store.py              — SQLite + FTS5 (+ TF-IDF / embeddings), hợp nhất RRF
   │
   ├─ cli.py    ├─ mcp_server.py   ├─ visualize.py   └─ exporters.py
   │  dòng lệnh │  cho AI           HTML/SVG          JSON/Cypher/GraphML/Kuzu
```

### Thêm hệ thống pháp luật khác

Cấu trúc Phần/Chương/Mục/Điều là đặc thù Việt Nam, nên phần nhận dạng tách thành **dialect plugin**:

```python
from vnlawkg import register

@register
class MyDialect:
    name = "xx"
    description = "..."
    def detect(self, text: str) -> float: ...      # điểm tin cậy 0..1
    def parse(self, text, *, doc_id, metadata=None): ...
```

Gói bên thứ ba đăng ký qua entry point `legalkg.dialects` là dùng được ngay, không cần sửa lõi.

## Xuất sang công cụ khác

```bash
vnlaw-kg export --db kho.db --format cypher  -o kg.cypher    # Neo4j
vnlaw-kg export --db kho.db --format graphml -o kg.graphml   # Gephi / yEd
```

Với KuzuDB, bản xuất sinh CSV + `COPY` nên chạy được truy vấn mà SQL phẳng làm rất tệ:

```cypher
-- Điều nào CÒN hiệu lực nhưng vẫn dẫn chiếu tới điều ĐÃ BỊ BÃI BỎ?
MATCH (a:Ent)-[e:Rel]->(b:Ent)
WHERE e.rel='REFERS_TO' AND a.status='active' AND b.status='repealed'
RETURN a.cite, b.cite;
```

## Giới hạn cần biết

- **Chỉ trích xuất được thứ có trong văn bản.** Không có án lệ, không có nghị định hướng dẫn trừ khi bạn tự nạp.
- **Dẫn chiếu dạng chữ** (*"theo quy định của pháp luật về đất đai"*) không nối được thành cạnh vì không nêu số điều.
- **Không phiên bản hóa theo thời gian**: graph phản ánh đúng bản văn bạn nạp. Muốn tra luật *tại thời điểm ký hợp đồng năm 2019* thì phải nạp bản hợp nhất tương ứng.
- **OCR luôn có sai sót.** Với PDF scan, hãy đọc kỹ báo cáo tự kiểm và đối chiếu vài điều mẫu.
- **Đây là công cụ tra cứu, không phải tư vấn pháp lý.** Luôn đối chiếu bản gốc trước khi dùng vào việc quan trọng.

## Hướng phát triển

Đóng góp cho bất kỳ mục nào đều được hoan nghênh — mục có 🎯 là chỗ dễ bắt đầu nhất.

**Ngắn hạn**
- 🎯 Mở rộng bộ câu hỏi benchmark lên 100+ câu, phủ nhiều chế định hơn
- 🎯 Bắt dẫn chiếu tới **khoản/điểm cụ thể** (`khoản 2 Điều 301`) thành cạnh riêng, không chỉ tới cấp điều
- Nhận diện dẫn chiếu dạng chữ ("pháp luật về đất đai") và nối tới `ExternalDocument`
- Cải thiện Hit@1 bằng rerank: dùng cạnh `DEFINES`/`REFERS_TO` làm tín hiệu xếp hạng

**Trung hạn**
- **Phiên bản hóa theo thời gian** — truy vấn "luật này quy định thế nào vào ngày 01/06/2019", dựng từ chuỗi văn bản hợp nhất
- **Diff giữa hai bản hợp nhất** — chỉ ra chính xác điều nào đổi, đổi thế nào
- Dialect cho **nghị định, thông tư, quyết định** (cấu trúc hơi khác luật)
- Nạp **án lệ** và nối tới điều luật mà bản án viện dẫn
- Web UI để tải file lên và duyệt graph, cho người không dùng dòng lệnh

**Dài hạn**
- Dialect cho hệ thống pháp luật khác (EU directives, US Code) qua entry point có sẵn
- Phát hiện **xung đột quy phạm**: hai điều cùng điều chỉnh một quan hệ nhưng khác nội dung
- Suy luận trên graph: tự gợi ý cụm điều cần đọc cho một tình huống, dựa trên đường đi dẫn chiếu
- Gói dữ liệu dựng sẵn cho các luật phổ biến, tải về là dùng ngay

## Đóng góp

```bash
git clone https://github.com/handonn2000/vnlawkg && cd vnlawkg
pip install -e ".[dev]"
pytest -q && ruff check src tests
```

Hữu ích nhất: **mẫu văn bản làm hỏng parser** (kèm file tái hiện) — mỗi định dạng lạ được vá là công cụ chắc chắn hơn một chút.

## Giấy phép

[Apache-2.0](LICENSE) — dùng được cho mục đích thương mại, có điều khoản bảo hộ sáng chế.

---

<div align="center">
<sub>Xây bằng công cụ mã nguồn mở · Không gửi dữ liệu đi đâu · Chạy được hoàn toàn ngoại tuyến</sub>
</div>