Metadata-Version: 2.5
Name: ragjson
Version: 0.5.1
Summary: 遵循RAG-Storage-Spec的知识库构建工具
Requires-Python: >=3.12
Requires-Dist: langchain-text-splitters==1.1.3
Requires-Dist: markitdown[all]==0.1.8
Requires-Dist: mcp==1.30.0
Requires-Dist: openai==3.28.0
Requires-Dist: pyjwt==2.15.0
Requires-Dist: zvec==0.7.0
Description-Content-Type: text/markdown

# ragjson

遵循 [RAG-Storage-Spec](docs/RAG-Storage-Spec.md) 规范的基于文件系统的 RAG 知识库构建工具。

将文档解析为 Markdown、切片、生成向量嵌入，并以 `.ragjson` 格式存储到文件系统中 —— 零外部数据库依赖。

## 特性

- **零外部依赖**：仅依赖操作系统文件系统，无需向量数据库或中间件
- **文档解析**：内置 markitdown 解析引擎
- **文本切片**：支持 RecursiveCharacterTextSplitter（默认）和 MarkdownTextSplitter
- **Embedding**：兼容 OpenAI API 的向量生成
- **原子写入**：write-to-tmp + rename 保证写入安全性
- **增量更新**：基于 MD5 对比，仅处理变更文件
- **目录级并发**：不同目录可并行处理，目录内 flock 互斥

## 安装

```bash
# 全局安装为独立命令（推荐，无需 venv）
uv tool install ragjson
```

或在项目虚拟环境中使用：

```bash
uv venv
source .venv/bin/activate
uv pip install ragjson
```

本地开发模式（从源码安装）：

```bash
uv venv
uv pip install -e .
source .venv/bin/activate
```

## 配置

在用户 home 目录下创建 `~/.rag.config` 文件（JSON 格式）：

```json
{
  "embedding_model": {
    "base_url": "https://api.example.com/v1",
    "api_key": "sk-your-token",
    "model_name": "text-embedding-3-small",
    "dimension": 1536
  },
  "splitter": {
    "chunk_size": 1000,
    "chunk_overlap": 200
  },
  "vector_database": {
    "db_url": "zvec:/data/vectordb",
    "index_type": "flat"
  }
}
```

| 配置项 | 说明 |
|--------|------|
| `embedding_model.base_url` | OpenAI 兼容 API 的地址 |
| `embedding_model.api_key` | API 密钥 |
| `embedding_model.model_name` | Embedding 模型名称 |
| `embedding_model.dimension` | 向量维度 |
| `splitter.chunk_size` | 切片最大字符数 |
| `splitter.chunk_overlap` | 切片重叠字符数 |
| `vector_database.db_url` | 向量数据库目录（collection 在其下的 `zvec-db1/`） |
| `vector_database.index_type` | 索引类型：`flat` / `hnsw` |

## 使用方法

### 查看状态

```bash
# 当前目录
rag status

# 指定目录
rag status ./docs

# 递归扫描子目录
rag status ./docs -r

# 单文件
rag status ./docs/readme.md
```

输出说明：
- 🟢 **EMBEDDED** — 已完成嵌入且 MD5 一致，无需更新
- 🟢 **TRACKED** — 子目录存在且被跟踪
- 🔴 **NEW / CHANGED** — 新文件或 MD5 已变更，需要更新
- 🟡 **DELETED / DIR_DELETED / SLICING / EMBEDDING** — 中间状态或已标记删除

### 增量更新

仅处理 MD5 发生变化的文件，执行完整流水线：**解析 → 切片 → 嵌入 → 写入**

```bash
# 使用默认切片器 (recursive)
rag update ./docs

# 递归更新子目录
rag update ./docs -r

# 使用 Markdown 切片器
rag update ./docs --splitter markdown

# 指定并发数
rag update ./docs -r -j 4

# 强制重新处理所有文件（跳过 MD5 检查）
rag update ./docs -f

# 单文件更新
rag update ./docs/readme.md

# 单文件强制更新
rag update ./docs/readme.md -f
```

参数说明：

| 参数 | 说明 | 默认值 |
|------|------|--------|
| `-r` | 递归扫描子目录 | 否 |
| `-f` | 强制重新处理所有文件（跳过 MD5 检查） | 否 |
| `-j N` | 并行处理的目录数 | CPU 核心数 |
| `--splitter` | 切片器类型：`recursive` / `markdown` | `recursive` |

### 导出

将 `.ragjson` 文件打包为 tar.gz 归档：

```bash
rag export backup.tar.gz              # 仅当前目录
rag export backup.tar.gz -r           # 递归包含子目录
rag export backup.tar.gz ./docs/a.md  # 导出单文件
```

### 清理 .rag 目录

```bash
# 清理当前目录
rm -rf .rag

# 递归清理所有子目录
find . -name ".rag" -type d -exec rm -rf {} +
```

### 索引同步

将向量同步到向量数据库：

```bash
# 增量同步（消费 .mushroom 变更通知）
rag index

# 递归增量同步
rag index -r

# 全量同步（扫描所有 EMBEDDED 的 .ragjson，不依赖 mushroom）
rag index -f

# 递归全量同步
rag index -f -r

# 单文件补偿索引（需要 -f）
rag index -f ./docs/readme.md
```

参数说明：

| 参数 | 说明 | 默认值 |
|------|------|--------|
| `-r` | 递归扫描子目录 | 否 |
| `-f` | 全量同步（跳过 mushroom，扫描所有 .ragjson） | 否 |

向量数据库路径和索引类型在 `~/.rag.config` 的 `vector_database` 节配置。

### 孤儿向量检测

扫描向量数据库中已删除文件残留的孤儿向量：

```bash
# 扫描当前目录下的孤儿向量（仅列出，不删除）
rag orphans

# 扫描指定路径下的孤儿向量
rag orphans /root/ragjson-data

# 扫描并一键清除孤儿向量
rag orphans --clean
```

参数说明：

| 参数 | 说明 | 默认值 |
|------|------|--------|
| `path` | 扫描路径前缀 | `.`（当前目录） |
| `--clean` | 检测后删除孤儿向量 | 否 |

### MCP Server

启动 MCP Server（stdio 模式），供外部 MCP Client（如 Claude Desktop、Cursor）连接：

```bash
# 启动 server
rag mcp
```

MCP Client 配置示例（Claude Desktop `claude_desktop_config.json`）：

```json
{
  "mcpServers": {
    "ragjson": {
      "command": "rag",
      "args": ["mcp"]
    }
  }
}
```

提供的能力：

| 类型 | 名称 | 说明 |
|------|------|------|
| Tool | `query(q, topn)` | 语义搜索知识库 |
| Resource | `ragjson://directory/{path}` | 目录文件列表（渐进浏览） |
| Resource | `ragjson://file/{name}` | 单文件元数据 |
| Resource | `ragjson://chunks/{name}` | 文件切片详情 |
| Resource | `ragjson://orphans/{path}` | 目录下孤儿向量检测 |

**注意**：Resource 的路径参数需要 URL 编码（FastMCP 的 `[^/]+` 正则不支持 `/`）：

```
原始路径: /root/ragjson-data/data
编码后:   %2Froot%2Fragjson-data%2Fdata

URI 示例: ragjson://directory/%2Froot%2Fragjson-data%2Fdata
```

## 目录结构

执行 `rag update` 后的目录结构示例：

```
docs/
├── .rag/                          # 控制目录
│   ├── tmp/                       # 原子写入暂存区
│   ├── readme.md.ragjson          # 文件条目（readme.md 的元数据）
│   ├── manual.pdf.ragjson         # 文件条目（manual.pdf 的元数据）
│   └── appendix.ragjson           # 目录条目（跟踪 appendix/ 子目录）
├── readme.md                      # 源文件
├── manual.pdf                     # 源文件
└── appendix/                      # 被跟踪的子目录
    └── ...
```

## `.ragjson` 文件格式

### 文件条目

```json
{
  "ragjson_version": "2.4",
  "processing": {
    "status": "EMBEDDED",
    "create_dt": "2026-07-14T10:00:00Z",
    "modify_dt": "2026-07-14T12:00:00Z"
  },
  "source": {
    "file_name": "readme.md",
    "md5_hash": "d41d8cd98f00b204e9800998ecf8427e",
    "size_bytes": 2048,
    "mtime": 1710508800,
    "inode": 12345678
  },
  "embedding": {
    "model_name": "text-embedding-3-small",
    "dimension": 1536
  },
  "chunks": [
    {
      "content": "切片完整原文内容...",
      "content_hash": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
      "overlap_size": 0,
      "vector_base64": "P0A/0D9A/0D9A/0D9A=="
    }
  ]
}
```

### 目录条目

用于跟踪子目录的存在性，格式极简：

```json
{"ragjson_version": "2.4"}
```

## 支持的文件类型

`.pdf` `.docx` `.doc` `.pptx` `.ppt` `.xlsx` `.xls` `.md` `.txt` `.html`

## 状态流转

```
INITIAL → CHUNKING → CHUNKED → EMBEDDING → EMBEDDED
                                            ↓
    (源文件变更) ←─────────────────── INITIAL (回退)

    源文件删除：任意状态 → rename .ragjson → .ragjson.deleted
    源文件恢复：.ragjson.deleted → rename .ragjson → (update 检测 MD5)
```

## 目录级别跟踪

`rag update` 自动跟踪子目录的存在性：

- **新建子目录** → 在父目录的 `.rag/` 下创建目录条目（`{dirname}.ragjson`）
- **删除子目录** → rename 为 `.ragjson.deleted`，mushroom 记录 `dir_delete` 事件
- **恢复子目录** → rename 回 `.ragjson`，mushroom 记录 `dir_recover` 事件

`rag index` 对目录事件的处理：
- `dir_delete` → 按路径前缀（`LIKE`）批量删除向量数据库中的孤儿向量
- `dir_recover` → 提示用户清除旧 `.rag` 数据后重新 update 和 index

`rag status` 显示目录跟踪状态：
- 🟢 **TRACKED** — 子目录存在且被跟踪
- 🟡 **DIR_DELETED** — 子目录已被删除

## 项目架构

```
src/ragjson/
├── commands/     # CLI 命令（status, update, export, index, mcp）
├── embed/        # Embedding 生成（OpenAI 兼容 API）
├── index/        # 向量索引（zvec backend，可扩展）
├── mcp/          # MCP Server（stdio 传输，tool + resource）
├── parser/       # 文档解析（markitdown backend，可扩展）
├── splitter/     # 文本切片（recursive / markdown）
├── store/        # 存储层（ragjson 文件、原子操作、mushroom）
├── config.py     # ~/.rag.config 配置加载
├── utils.py      # 工具函数（MD5、向量编码、颜色输出）
└── cli.py        # CLI 入口
```

## 向量数据库 Collection Schema

zvec collection `ragjson` 的字段定义：

| 字段 | 类型 | 索引 | 说明 |
|------|------|------|------|
| `source_path` | STRING | InvertIndex | 文件完整绝对路径 |
| `source_dir` | STRING | InvertIndex | 文件所在目录 |
| `content` | STRING | — | 切片文本内容 |
| `content_hash` | STRING | — | 切片内容 MD5 |
| `embedding` | VECTOR_FP32 | Flat/HNSW | 向量嵌入 |

文档 ID 使用 `uuid5(namespace, f"{source_path}#{chunk_index}")` 生成：

- **确定性**：相同 `source_path` + `chunk_index` → 相同 UUID
- **跨后端一致**：zvec、pgvector 等后端使用统一 ID 格式
- **支持任意字符**：中文、特殊符号等文件名均可正确处理

**升级提示**：

从 0.4.x 升级到 0.5.0 后，向量数据库 schema 变更（`source` → `source_path`，新增 `source_dir`）：

```bash
rag index -f -r  # 必须重新索引
```

从 0.3.x 升级到 0.4.0 后：
- 规范版本从 v2.3 升至 v2.4，新增目录级别跟踪能力
- 文件类 `.ragjson` 格式不变，现有数据无需迁移
- `rag update -r` 会自动为子目录创建跟踪条目

从 0.3.1 升级到 0.3.2 后（uuid5 文档 ID）：
- 需要重新索引所有文档：`rag index -f -r`

## 开发

```bash
# 克隆项目
git clone <repo>
cd ragjson

# 创建虚拟环境并安装开发版本
uv venv
uv pip install -e ".[index,mcp]"
source .venv/bin/activate

# 运行 CLI
rag --help
```

## 许可证

MIT
