claudqunwang Cursor commited on
Commit
9a4828d
·
1 Parent(s): 4af5c8c

Add Weaviate index builder Gradio app

Browse files

Co-authored-by: Cursor <cursoragent@cursor.com>

Files changed (4) hide show
  1. HF_SPACE_SETUP.md +106 -0
  2. README.md +74 -50
  3. app.py +209 -310
  4. requirements.txt +5 -2
HF_SPACE_SETUP.md ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 在 Hugging Face Space 上构建 Weaviate 索引
2
+
3
+ ## 方案概述
4
+
5
+ 由于本地网络环境可能存在 SSL 连接问题,我们可以在 Hugging Face Space 上运行索引构建,利用 HF Space 更稳定的网络环境。
6
+
7
+ ## 步骤
8
+
9
+ ### 1. 准备 GenAICoursesDB Space
10
+
11
+ 如果你还没有创建这个 Space:
12
+
13
+ 1. 访问 https://huggingface.co/spaces
14
+ 2. 点击 "Create new Space"
15
+ 3. 设置:
16
+ - **Space name**: `GenAICoursesDB`(或你喜欢的名称)
17
+ - **SDK**: `Gradio`
18
+ - **Hardware**: `CPU basic`(足够使用)
19
+ - **Visibility**: `Public` 或 `Private`
20
+
21
+ ### 2. 上传代码和文件
22
+
23
+ #### 方式 A:通过 Git(推荐)
24
+
25
+ ```bash
26
+ # 克隆你的 Space(如果还没有)
27
+ git clone https://huggingface.co/spaces/YOUR_USERNAME/GenAICoursesDB
28
+ cd GenAICoursesDB
29
+
30
+ # 从本地项目复制文件
31
+ cp /path/to/AI_Agent_Clare-main/hf_space/GenAICoursesDB_space/app.py .
32
+ cp /path/to/AI_Agent_Clare-main/hf_space/GenAICoursesDB_space/requirements.txt .
33
+ cp /path/to/AI_Agent_Clare-main/hf_space/GenAICoursesDB_space/README.md .
34
+
35
+ # 上传 GENAI COURSES(使用 Git LFS,因为文件可能很大)
36
+ git lfs install
37
+ git lfs track "GENAI COURSES/**"
38
+ cp -r /path/to/AI_Agent_Clare-main/GENAI\ COURSES .
39
+
40
+ git add .
41
+ git commit -m "Add Weaviate index builder app"
42
+ git push
43
+ ```
44
+
45
+ #### 方式 B:通过 Web 界面上传
46
+
47
+ 1. 访问你的 Space 页面
48
+ 2. 点击 "Files" 标签
49
+ 3. 上传以下文件:
50
+ - `app.py`
51
+ - `requirements.txt`
52
+ - `README.md`
53
+ - `GENAI COURSES` 文件夹(可能需要压缩为 zip)
54
+
55
+ ### 3. 配置 Secrets
56
+
57
+ 访问 Space Settings → Secrets,添加:
58
+
59
+ | Secret 名称 | 值 | 说明 |
60
+ |------------|-----|------|
61
+ | `OPENAI_API_KEY` | `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` | OpenAI API Key |
62
+ | `WEAVIATE_URL` | `https://iydyvd4wqnekotfiftma.c0.us-west3.gcp.weaviate.cloud` | Weaviate Cloud REST 地址 |
63
+ | `WEAVIATE_API_KEY` | `your-weaviate-api-key` | Weaviate API Key |
64
+ | `WEAVIATE_COLLECTION` | `GenAICourses` | Collection 名称(可选,默认值) |
65
+ | `EMBEDDING_PROVIDER` | `openai` | Embedding 提供商(可选,默认值) |
66
+
67
+ ### 4. 运行索引构建
68
+
69
+ 1. Space 会自动构建并启动
70
+ 2. 访问 Space 页面,你会看到 Gradio 界面
71
+ 3. 点击 "🚀 开始构建索引" 按钮
72
+ 4. 等待构建完成(可能需要 5-15 分钟)
73
+
74
+ ### 5. 验证结果
75
+
76
+ 构建完成后,界面会显示:
77
+ ```
78
+ ✅ 索引构建成功!
79
+ 当前 object count = [数量]
80
+ ```
81
+
82
+ 你也可以在 Weaviate Console 中验证:
83
+ 1. 访问你的 Weaviate Cloud Console
84
+ 2. 查看 `GenAICourses` collection
85
+ 3. 确认 object count 与构建结果一致
86
+
87
+ ## 优势
88
+
89
+ ✅ **网络稳定**: HF Space 的网络环境通常比本地更稳定
90
+ ✅ **无需下载**: 直接在 HF Space 上完成 embedding 和上传
91
+ ✅ **易于使用**: Gradio 界面,一键操作
92
+ ✅ **实时进度**: 可以看到构建进度和状态
93
+
94
+ ## 注意事项
95
+
96
+ ⚠️ **文件大小**: 如果 `GENAI COURSES` 文件夹很大(>1GB),建议使用 Git LFS
97
+ ⚠️ **构建时间**: 768 个文档块大约需要 5-15 分钟
98
+ ⚠️ **API 费用**: 使用 OpenAI API 会产生费用(约 $0.01-0.05)
99
+
100
+ ## 后续步骤
101
+
102
+ 索引构建完成后,ClareVoice Space 就可以直接使用 Weaviate 进行检索了。确保 ClareVoice Space 的 Secrets 中也配置了:
103
+ - `WEAVIATE_URL`
104
+ - `WEAVIATE_API_KEY`
105
+ - `WEAVIATE_COLLECTION`
106
+ - `OPENAI_API_KEY`(用于检索时的 embedding)
README.md CHANGED
@@ -1,71 +1,95 @@
1
- ---
2
- title: GenAICoursesDB
3
- emoji: 🏆
4
- colorFrom: purple
5
- colorTo: yellow
6
- sdk: gradio
7
- sdk_version: 6.5.1
8
- app_file: app.py
9
- pinned: false
10
- ---
11
 
12
- Space 用 **LlamaIndex** GENAI 课程资料构建向量数据库提供问答界面。Embedding 可选 **OpenAI(付费)** 或 **Hugging Face 开源模型(免费)**
13
 
14
- ## 成本与免费方案
15
 
16
- - **OpenAI embedding**(`text-embedding-3-small`):约 **$0.02 / 100 token**。你当前约 149 个文档块,粗算约 **几万 token**,建一次索引大约 **&lt; 0.01 美元**(不到 1 美分);之后每次提问只多一次短句 embedding,可忽略。
17
- - **免费方案**:在 Space 的 **Settings → Variables** 里添加 `EMBEDDING_PROVIDER` = `huggingface`,即用本地 **sentence-transformers** 做 embedding,**不花 OpenAI 钱**;提问时只返回检索到的原文(不调用 LLM)。可选变量 `HF_EMBEDDING_MODEL`(默认 `sentence-transformers/all-MiniLM-L6-v2`),可改为如 `BAAI/bge-small-en-v1.5` 等。
18
 
19
- ## 为什么课程文件放 Dataset?
20
 
21
- Hugging Face Spaces 对大二进制文件(如 `.pdf/.docx`)推送有限制,因此课程文件存放在 **Dataset**,
22
- Space 在启动时从 Dataset 下载课程目录后再构建/加载索引。
 
 
 
23
 
24
- ## 需要配置的 Secrets / Variables
25
 
26
- - **OpenAI 案**`OPENAI_API_KEY`(Settings → Secrets)
27
- - **免费方案**:`EMBEDDING_PROVIDER` = `huggingface`(Settings → Variables),无需 API Key 即可建索引与检索
28
- - **可选**:`GENAI_COURSES_DATASET_ID`(默认:`claudqunwang/genai-courses-data`)
29
- - **可选**:`GENAI_COURSES_DATASET_SUBDIR`(默认:`GENAI COURSES`)
30
- - **可选**:`HF_EMBEDDING_MODEL`(免费方案时生效,默认:`sentence-transformers/all-MiniLM-L6-v2`)
31
- - **可选(方案 A)**:`INDEX_DATASET_ID`(预构建索引 Dataset,设置后启动时直接下载,无需 embedding)
32
 
33
- ## 使用方式
34
 
35
- 打开 Space 页面后直接提问即可。
 
 
36
 
37
- ### 方案 A:预构建索引(启动快,无需每次 embedding)
 
38
 
39
- 若配置了预构建索引,Space 启动时**直接下载索引**,几秒内就绪,不再做 embedding。
 
 
 
 
40
 
41
- **步骤**
42
 
43
- 1. **创建索引 Dataset**:在 [huggingface.co/datasets](https://huggingface.co/datasets) 点击 **Create new dataset**,名称如 `genai-courses-index`。
 
 
44
 
45
- 2. **本地构建并上传**(需有 `GENAI COURSES` 目录):
46
- ```bash
47
- cd hf_space/GenAICoursesDB_space
48
- # 设置与 Space 一致(推荐用免费 embedding)
49
- export EMBEDDING_PROVIDER=huggingface
50
- # 若用默认 Dataset,可设置 INDEX_DATASET_ID=你的用户名/genai-courses-index
51
- python build_and_upload_index.py
52
- ```
53
 
54
- 3. **Space 配置**:在 GenAICoursesDB Space → **Settings → Variables** 添加:
55
- - `INDEX_DATASET_ID` = `你的用户名/genai-courses-index`
 
 
 
56
 
57
- 4. **重启 Space**:下次启动将从 Dataset 下载索引,无需重新 embedding。
58
 
59
- **未配置预构建时**:首次启动或勾选“强制重建索引”会从课程 Dataset 下载并构建,耗时较长(需做 Embedding)。
 
 
 
 
60
 
61
- ## Clare 方案三:外部调用 retrieve 接口
62
 
63
- Clare 等应用可通过 `gradio_client` 调用本 Space 的 **retrieve** 接口,获取课程检索结果作为 RAG 上下文:
 
 
 
64
 
65
- ```python
66
- from gradio_client import Client
67
- client = Client("claudqunwang/GenAICoursesDB")
68
- chunks = client.predict("Module 7 Lab 6 主讲什么?", api_name="/retrieve")
69
- ```
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
 
71
- Clare 中:设置环境变量 `GENAI_COURSES_SPACE=claudqunwang/GenAICoursesDB` 即可启
 
 
1
+ # Weaviate 索引构建工具(Hugging Face Space 版)
 
 
 
 
 
 
 
 
 
2
 
3
+ Hugging Face Space 上使OpenAI API 进行 embedding,直接上传到 Weaviate Cloud
4
 
5
+ ## 🚀 快速开始
6
 
7
+ ### 1. Hugging Face Space 中配置 Secrets
 
8
 
9
+ 访问你的 Space Settings → Secrets,添加以下环境变量:
10
 
11
+ - **`OPENAI_API_KEY`**: `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`
12
+ - **`WEAVIATE_URL`**: 你的 Weaviate Cloud REST 地址(例如:`https://xxx.c0.us-west3.gcp.weaviate.cloud`)
13
+ - **`WEAVIATE_API_KEY`**: 你的 Weaviate API Key
14
+ - **`WEAVIATE_COLLECTION`**: Collection 名称(默认:`GenAICourses`)
15
+ - **`EMBEDDING_PROVIDER`**: `openai` 或 `huggingface`(默认:`openai`)
16
 
17
+ ### 2. 上传 GENAI COURSES 文件夹
18
 
19
+ 有两种
 
 
 
 
 
20
 
21
+ #### 方式 A:通过 Git LFS 上传(推荐)
22
 
23
+ ```bash
24
+ # 在本地项目目录
25
+ cd hf_space/GenAICoursesDB_space
26
 
27
+ # GENAI COURSES 复制到 Space 目录
28
+ cp -r ../../GENAI\ COURSES .
29
 
30
+ # 提交并推送
31
+ git add GENAI\ COURSES
32
+ git commit -m "Add GENAI COURSES for indexing"
33
+ git push
34
+ ```
35
 
36
+ #### 方式 B通过 HF Space 的文件上传功能
37
 
38
+ 1. 访问你的 Space 页面
39
+ 2. 点击 "Files" 标签
40
+ 3. 上传 `GENAI COURSES` 文件夹(可能需要压缩为 zip 后上传,然后在 Space 中解压)
41
 
42
+ ### 3. 运行索引构建
 
 
 
 
 
 
 
43
 
44
+ 1. 访问你的 Space 页面
45
+ 2. Gradio 界面中:
46
+ - 选择是否清空旧索引(推荐勾选)
47
+ - 点击 "🚀 开始构建索引" 按钮
48
+ - 等待构建完成(可能需要几分钟)
49
 
50
+ ## 📋 功能说明
51
 
52
+ - 使用 OpenAI `text-embedding-3-small` 进行 embedding
53
+ - ✅ 自动读取 `GENAI COURSES` 目录下的所有文档(.md, .pdf, .txt, .py, .ipynb, .docx)
54
+ - ✅ 直接上传到 Weaviate Cloud(无需下载)
55
+ - ✅ 实时显示构建进度
56
+ - ✅ 自动验证索引构建结果
57
 
58
+ ## 🔧 技术细节
59
 
60
+ - **Embedding 模型**: OpenAI `text-embedding-3-small`(1536 维)
61
+ - **向量数据库**: Weaviate Cloud
62
+ - **文档处理**: LlamaIndex SimpleDirectoryReader
63
+ - **界面**: Gradio
64
 
65
+ ## ⚠️ 注意事项
66
+
67
+ 1. **文件大小限制**: Hugging Face Space 有文件大小限制,如果 `GENAI COURSES` 太大,可能需要使用 Git LFS
68
+ 2. **构建时间**: 768 个文档块大约需5-15 分钟,取决于网络速度
69
+ 3. **网络稳定性**: HF Space 的网络通常比本地更稳定,适合处理大量文档
70
+ 4. **成本**: 使用 OpenAI API 会产生费用,768 个文档块大约需要 $0.01-0.05(取决于文档长度)
71
+
72
+ ## 🐛 故障排除
73
+
74
+ ### 错误:课程目录不存在
75
+ - 确保 `GENAI COURSES` 文件夹已上传到 Space 根目录
76
+ - 检查文件夹名称是否正确(区分大小写)
77
+
78
+ ### 错误:OPENAI_API_KEY 未设置
79
+ - 检查 Space Settings → Secrets 中是否已添加 `OPENAI_API_KEY`
80
+ - 确保 Secret 名称完全匹配(区分大小写)
81
+
82
+ ### 错误:Weaviate 连接失败
83
+ - 检查 `WEAVIATE_URL` 格式是否正确(应以 `https://` 开头)
84
+ - 验证 `WEAVIATE_API_KEY` 是否有效
85
+ - 确认网络连接正常
86
+
87
+ ### 构建成功但 object count = 0
88
+ - 检查 Weaviate Console 中的 collection 名称是否匹配
89
+ - 确认使用的是同一 Weaviate 集群和账号
90
+ - 等待几秒钟后再次检查(可能有延迟)
91
+
92
+ ## 📚 相关文档
93
 
94
+ - `build_weaviate_index.py`: 命令行版本的索引构建脚本(于本地运行)
95
+ - `app.py`: Gradio 应用(用于 HF Space)
app.py CHANGED
@@ -1,331 +1,230 @@
 
 
 
 
1
  import os
2
- from pathlib import Path
3
- from typing import Optional, Tuple
4
-
5
  import gradio as gr
6
- from dotenv import load_dotenv
7
- from huggingface_hub import HfApi, hf_hub_download
8
- from llama_index.core import (
9
- Settings,
10
- SimpleDirectoryReader,
11
- StorageContext,
12
- VectorStoreIndex,
13
- load_index_from_storage,
14
- )
15
- from llama_index.embeddings.openai import OpenAIEmbedding
16
-
17
-
18
- load_dotenv()
19
-
20
-
21
- DATASET_ID = (os.getenv("GENAI_COURSES_DATASET_ID") or "claudqunwang/genai-courses-data").strip()
22
- DATASET_SUBDIR = (os.getenv("GENAI_COURSES_DATASET_SUBDIR") or "GENAI COURSES").strip()
23
 
24
- # 方案 A:预构建索引 Dataset。若设Space 启动时直接下载加载,无需重新 embedding
25
- INDEX_DATASET_ID = (os.getenv("INDEX_DATASET_ID") or "claudqunwang/genai-courses-index").strip()
 
 
 
 
26
 
27
- # Hugging Face Spaces 没有持久化磁盘时,每次重启可能需要重建索引
28
- PERSIST_DIR = Path(os.getenv("GENAI_INDEX_DIR") or "/tmp/genai_courses_index").resolve()
 
29
 
30
- # 可选:使用免费开源 embedding,不花 OpenAI 钱。设为 "huggingface" 时用本地 HF 模型
31
- EMBEDDING_PROVIDER = (os.getenv("EMBEDDING_PROVIDER") or "openai").strip().lower()
32
- HF_EMBEDDING_MODEL = (os.getenv("HF_EMBEDDING_MODEL") or "sentence-transformers/all-MiniLM-L6-v2").strip()
33
 
34
 
35
- def _setup_embed_model():
36
- if EMBEDDING_PROVIDER == "huggingface":
37
- try:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  from llama_index.embeddings.huggingface import HuggingFaceEmbedding
39
- return HuggingFaceEmbedding(model_name=HF_EMBEDDING_MODEL)
40
- except Exception as e:
41
- raise RuntimeError(
42
- f"无法加载 Hugging Face 免费 embedding({HF_EMBEDDING_MODEL}):{e!r}\n"
43
- "请确认已安装: pip install llama-index-embeddings-huggingface sentence-transformers"
44
  )
45
- return OpenAIEmbedding(model="text-embedding-3-small")
46
-
47
-
48
- Settings.embed_model = _setup_embed_model()
49
-
50
-
51
- def _ensure_openai_key():
52
- """仅在使用 OpenAI embedding 或默认 query 引擎时需要。"""
53
- if EMBEDDING_PROVIDER == "huggingface":
54
- return # 建索引用 HF,不强制要求 OpenAI Key
55
- key = (os.getenv("OPENAI_API_KEY") or "").strip()
56
- if not key:
57
- raise RuntimeError("OPENAI_API_KEY 未设置。请到 Space: Settings → Secrets 添加 OPENAI_API_KEY;或设置 EMBEDDING_PROVIDER=huggingface 使用免费 embedding。")
58
-
59
-
60
- def _get_courses_dir() -> Path:
61
- """
62
- 从 Dataset 下载课程文件到本地临时目录,并返回实际目录路径。
63
- 这里**完全绕开 snapshot_download**,避免某些环境下出现的
64
- “No files found ... GENAI COURSES” 之类缓存异常。
65
-
66
- 实现思路:
67
- 1. 用 HfApi.list_repo_files 列出 Dataset 中的所有文件路径;
68
- 2. 过滤出属于 DATASET_SUBDIR 下的文件;
69
- 3. 通过 hf_hub_download 逐个拉到 /tmp/genai_courses_data,并还原子目录结构。
70
- """
71
- api = HfApi()
72
- try:
73
- all_files = api.list_repo_files(repo_id=DATASET_ID, repo_type="dataset")
74
- except Exception as e:
75
- raise RuntimeError(f"无法列出 Dataset 文件({DATASET_ID}):{e!r}")
76
-
77
- if not all_files:
78
- raise RuntimeError(f"Dataset {DATASET_ID!r} 为空,请确认上传了课程文件。")
79
-
80
- # 归一化子目录名,兼容空格/大小写差异
81
- sub_norm = "".join(DATASET_SUBDIR.strip().lower().split("/")).replace(" ", "")
82
-
83
- def _belongs_to_subdir(path: str) -> bool:
84
- # path 形如 "GENAI COURSES/Module 1/...docx"
85
- if "/" not in path:
86
- return False
87
- top = path.split("/", 1)[0]
88
- top_norm = "".join(top.strip().lower().split("/")).replace(" ", "")
89
- return top_norm == sub_norm
90
-
91
- course_files = [p for p in all_files if _belongs_to_subdir(p)]
92
- if not course_files:
93
- raise RuntimeError(
94
- "在 Dataset 中没有找到课程子目录。\n"
95
- f"- Dataset: {DATASET_ID!r}\n"
96
- f"- 期望子目录: {DATASET_SUBDIR!r}\n"
97
- f"- 实际顶层内容示例: {[p.split('/',1)[0] for p in all_files[:20]]!r}"
98
  )
99
-
100
- local_root = Path("/tmp/genai_courses_data")
101
- local_root.mkdir(parents=True, exist_ok=True)
102
-
103
- for rel_path in course_files:
104
- # 将文件下载到对应的本地路径(保持目录结构)
105
- local_path = local_root / rel_path
106
- local_path.parent.mkdir(parents=True, exist_ok=True)
107
  try:
108
- downloaded = hf_hub_download(
109
- repo_id=DATASET_ID,
110
- repo_type="dataset",
111
- filename=rel_path,
112
- local_dir=str(local_root),
113
- local_dir_use_symlinks=False,
 
 
 
 
 
 
 
 
 
 
 
114
  )
115
- except Exception as e:
116
- print(f"[download] failed for {rel_path}: {e!r}")
117
- continue
118
- # hf_hub_download 已经写入 local_dir 下对应文件,这里确保路径存在
119
- _ = downloaded
120
-
121
- courses_dir = local_root / DATASET_SUBDIR
122
- if not courses_dir.exists():
123
- # 有些情况下 DATASET_SUBDIR 大写/空格不完全一致,再做一次自动探测
124
- candidates = [p for p in local_root.iterdir() if p.is_dir()]
125
- if candidates:
126
- # 选第一个目录作为课程根目录因为我们只往属于该子目录的文件里写
127
- courses_dir = candidates[0]
128
-
129
- if not courses_dir.exists():
130
- raise FileNotFoundError(
131
- "课程目录下载失败,请检查 Dataset 结构。\n"
132
- f"- Dataset: {DATASET_ID!r}\n"
133
- f"- 期望子目录: {DATASET_SUBDIR!r}\n"
134
- f"- 本地根目录: {str(local_root)!r}"
135
- )
136
-
137
- return courses_dir
138
-
139
-
140
- def _download_and_load_prebuilt_index() -> Tuple[bool, Optional["VectorStoreIndex"]]:
141
- """方案 A:从 INDEX_DATASET_ID 下载预构建索引并加载到 PERSIST_DIR。成功返回 True。"""
142
- if not INDEX_DATASET_ID:
143
- return False, None
144
- try:
145
- import shutil
146
- if PERSIST_DIR.exists():
147
- shutil.rmtree(PERSIST_DIR)
148
- PERSIST_DIR.mkdir(parents=True, exist_ok=True)
149
- from huggingface_hub import snapshot_download
150
- snapshot_download(
151
- repo_id=INDEX_DATASET_ID,
152
- repo_type="dataset",
153
- local_dir=str(PERSIST_DIR),
154
- local_dir_use_symlinks=False,
155
- )
156
- storage_context = StorageContext.from_defaults(persist_dir=str(PERSIST_DIR))
157
- idx = load_index_from_storage(storage_context)
158
- print(f"[index] 已从预构建索引加载: {INDEX_DATASET_ID}")
159
- return True, idx
160
  except Exception as e:
161
- print(f"[index] 预构建索引加载失败: {repr(e)}")
162
- return False, None
163
-
164
-
165
- def get_index(force_rebuild: bool = False) -> VectorStoreIndex:
166
- _ensure_openai_key()
167
-
168
- if PERSIST_DIR.exists() and not force_rebuild:
169
- try:
170
- storage_context = StorageContext.from_defaults(persist_dir=str(PERSIST_DIR))
171
- index = load_index_from_storage(storage_context)
172
- return index
173
- except Exception as e:
174
- print(f"[index] load failed, rebuilding: {repr(e)}")
175
-
176
- # 方案 A:优先尝试从预构建索引 Dataset 下载
177
- if not force_rebuild and INDEX_DATASET_ID:
178
- ok, idx = _download_and_load_prebuilt_index()
179
- if ok and idx is not None:
180
- return idx
181
- if PERSIST_DIR.exists():
182
- try:
183
- import shutil
184
- shutil.rmtree(PERSIST_DIR)
185
- except Exception:
186
- pass
187
- PERSIST_DIR.mkdir(parents=True, exist_ok=True)
188
-
189
- courses_dir = _get_courses_dir()
190
- print(f"[index] building from: {courses_dir}")
191
-
192
- reader = SimpleDirectoryReader(
193
- input_dir=str(courses_dir),
194
- recursive=True,
195
- required_exts=[".md", ".pdf", ".txt", ".py", ".ipynb", ".docx"],
196
  )
197
- documents = reader.load_data()
198
- print(f"[index] loaded {len(documents)} docs/chunks, embedding now...")
199
-
200
- index = VectorStoreIndex.from_documents(documents)
201
- PERSIST_DIR.mkdir(parents=True, exist_ok=True)
202
- index.storage_context.persist(persist_dir=str(PERSIST_DIR))
203
- return index
204
-
205
-
206
- INDEX: VectorStoreIndex | None = None
207
- INDEX_ERR: str | None = None
208
-
209
-
210
- def warmup():
211
- global INDEX, INDEX_ERR
212
- try:
213
- INDEX = get_index(force_rebuild=False)
214
- INDEX_ERR = None
215
- except Exception as e:
216
- INDEX = None
217
- INDEX_ERR = repr(e)
218
-
219
-
220
- warmup()
221
-
222
-
223
- def _retrieve_nodes(question: str, top_k: int = 5) -> list:
224
- """内部:用 Retriever 检索,返回 Node 列表。Clare 调retrieve_chunks 时复用。"""
225
- global INDEX, INDEX_ERR
226
- if not question or not question.strip():
227
- return []
228
- if INDEX is None:
229
- try:
230
- INDEX = get_index(force_rebuild=False)
231
- INDEX_ERR = None
232
- except Exception as e:
233
- INDEX = None
234
- INDEX_ERR = repr(e)
235
- if INDEX is None:
236
- return []
237
- retriever = INDEX.as_retriever(similarity_top_k=top_k)
238
- return retriever.retrieve(question)
239
-
240
-
241
- def ask(question: str, rebuild: bool) -> str:
242
- global INDEX, INDEX_ERR
243
- if not question or not question.strip():
244
- return "请先输入一个问题。"
245
-
246
- if rebuild or INDEX is None:
247
- try:
248
- INDEX = get_index(force_rebuild=True)
249
- INDEX_ERR = None
250
- except Exception as e:
251
- INDEX = None
252
- INDEX_ERR = repr(e)
253
-
254
- if INDEX is None:
255
- return f"索引不可用:{INDEX_ERR or 'unknown error'}"
256
-
257
- # 使用免费 HuggingFace embedding 时,用 Retriever 直接检索,不创建 QueryEngine,避免触发 Settings.llm(无需安装 llama-index-llms-openai)
258
- if EMBEDDING_PROVIDER == "huggingface":
259
- nodes = _retrieve_nodes(question, top_k=5)
260
- parts = [node.get_content() for node in nodes]
261
- return "---\n\n".join(parts) if parts else "未检索到相关内容。"
262
- qe = INDEX.as_query_engine()
263
- resp = qe.query(question)
264
- return str(resp)
265
-
266
-
267
- def retrieve_chunks(question: str, top_k: int = 5) -> str:
268
- """
269
- 仅检索,不生成回答。供 Clare 等外部调用:返回检索到的课程片段,作为 RAG context。
270
- Gradio api_name="retrieve" 暴露此接口。
271
- """
272
- nodes = _retrieve_nodes(question, top_k=top_k)
273
- parts = [node.get_content() for node in nodes]
274
- return "\n\n---\n\n".join(parts) if parts else ""
275
-
276
-
277
- def status_md() -> str:
278
- emb_line = f"- **Embedding**: `{EMBEDDING_PROVIDER}` (免费)" if EMBEDDING_PROVIDER == "huggingface" else f"- **Embedding**: OpenAI (付费)"
279
- idx_src = f"- **索引来源**: 预构建 `{INDEX_DATASET_ID}`" if INDEX_DATASET_ID else "- **索引来源**: 运行时构建"
280
- if INDEX is not None:
281
- return (
282
- "✅ **Index ready**\n\n"
283
- f"- **Dataset**: `{DATASET_ID}`\n"
284
- f"- **Subdir**: `{DATASET_SUBDIR}`\n"
285
- f"{idx_src}\n"
286
- f"{emb_line}\n"
287
- f"- **Index dir**: `{str(PERSIST_DIR)}`\n"
288
  )
289
- return (
290
- "⚠️ **Index not ready**\n\n"
291
- f"- **Dataset**: `{DATASET_ID}`\n"
292
- f"- **Subdir**: `{DATASET_SUBDIR}`\n"
293
- f"{idx_src}\n"
294
- f"{emb_line}\n"
295
- f"- **Index dir**: `{str(PERSIST_DIR)}`\n\n"
296
- f"Error: `{INDEX_ERR or 'unknown'}`"
297
  )
298
-
299
-
300
- with gr.Blocks() as demo:
301
- gr.Markdown("# 📚 GENAI COURSES 向量知识库(Dataset 版)")
302
- gr.Markdown(
303
- "说明:课程文件不放在 Space 仓库里,而是放在 Dataset;Space 启动时会从 Dataset 下载并构建索引。"
 
304
  )
305
-
306
- status = gr.Markdown(value=status_md())
307
-
308
- with gr.Row():
309
- question = gr.Textbox(label="问题", placeholder="例如:Module 7 的 Lab 6 主要讲什么?")
310
- rebuild = gr.Checkbox(label="强制重建索引(慢,会重新做 Embedding)", value=False)
311
-
312
- out = gr.Markdown(label="回答")
313
- btn = gr.Button("提问")
314
- btn.click(fn=ask, inputs=[question, rebuild], outputs=out).then(fn=status_md, inputs=None, outputs=status)
315
-
316
- # Clare 调用:仅检索,不生成回答。gradio_client 用 api_name="retrieve" 调用
317
- with gr.Accordion("API(Clare 等外部��用)", open=False):
318
- api_question = gr.Textbox(label="检索问题", placeholder="输入问题,返回检索到的课程片段")
319
- api_out = gr.Textbox(label="检索结果(原始文本)", lines=8)
320
- api_btn = gr.Button("Retrieve")
321
- api_btn.click(
322
- fn=retrieve_chunks,
323
- inputs=[api_question],
324
- outputs=api_out,
325
- api_name="retrieve",
326
  )
327
 
328
 
329
  if __name__ == "__main__":
330
- demo.launch()
331
-
 
1
+ """
2
+ Hugging Face Space 应用:在 HF Space 上运行 Weaviate 索引构建
3
+ 使用 OpenAI API 进行 embedding,直接上传到 Weaviate Cloud
4
+ """
5
  import os
 
 
 
6
  import gradio as gr
7
+ from pathlib import Path
8
+ import threading
9
+ import time
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
+ # 从环境变量读取配(HF Space Secrets)
12
+ OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "").strip()
13
+ WEAVIATE_URL = os.getenv("WEAVIATE_URL", "").strip()
14
+ WEAVIATE_API_KEY = os.getenv("WEAVIATE_API_KEY", "").strip()
15
+ WEAVIATE_COLLECTION = os.getenv("WEAVIATE_COLLECTION", "GenAICourses").strip()
16
+ EMBEDDING_PROVIDER = os.getenv("EMBEDDING_PROVIDER", "openai").strip().lower()
17
 
18
+ # 课程文档路径(需要上传到 HF Space)
19
+ SCRIPT_DIR = Path(__file__).resolve().parent
20
+ COURSES_DIR = SCRIPT_DIR / "GENAI COURSES"
21
 
22
+ # 全局状态
23
+ build_status = {"running": False, "progress": "", "error": None, "result": None}
 
24
 
25
 
26
+ def build_index_worker(clear_first: bool, progress_callback=None):
27
+ """后台工作线程:构建索引"""
28
+ global build_status
29
+
30
+ try:
31
+ build_status["running"] = True
32
+ build_status["error"] = None
33
+ build_status["progress"] = "开始构建索引..."
34
+
35
+ # 检查配置
36
+ if not OPENAI_API_KEY:
37
+ raise RuntimeError("请在 HF Space Settings → Secrets 中添加 OPENAI_API_KEY")
38
+ if not WEAVIATE_URL or not WEAVIATE_API_KEY:
39
+ raise RuntimeError("请在 HF Space Settings → Secrets 中添加 WEAVIATE_URL 和 WEAVIATE_API_KEY")
40
+
41
+ # 检查课程目录
42
+ if not COURSES_DIR.exists():
43
+ raise FileNotFoundError(
44
+ f"课程目录不存在:{COURSES_DIR}\n"
45
+ "请将 GENAI COURSES 文件夹上传到 Space 的根目录"
46
+ )
47
+
48
+ # 导入依赖
49
+ build_status["progress"] = "加载依赖库..."
50
+ from llama_index.core import SimpleDirectoryReader, VectorStoreIndex, Settings
51
+ from llama_index.core import StorageContext
52
+ from llama_index.vector_stores.weaviate import WeaviateVectorStore
53
+ import weaviate
54
+ from weaviate.classes.init import Auth
55
+
56
+ # 设置 embedding
57
+ build_status["progress"] = "配置 embedding 模型..."
58
+ if EMBEDDING_PROVIDER == "openai":
59
+ from llama_index.embeddings.openai import OpenAIEmbedding
60
+ Settings.embed_model = OpenAIEmbedding(
61
+ model="text-embedding-3-small",
62
+ api_key=OPENAI_API_KEY,
63
+ )
64
+ else:
65
  from llama_index.embeddings.huggingface import HuggingFaceEmbedding
66
+ Settings.embed_model = HuggingFaceEmbedding(
67
+ model_name="sentence-transformers/all-MiniLM-L6-v2"
 
 
 
68
  )
69
+
70
+ # 连接 Weaviate
71
+ build_status["progress"] = "连接 Weaviate Cloud..."
72
+ url = WEAVIATE_URL
73
+ if not url.startswith("http"):
74
+ url = "https://" + url
75
+
76
+ client = weaviate.connect_to_weaviate_cloud(
77
+ cluster_url=url,
78
+ auth_credentials=Auth.api_key(WEAVIATE_API_KEY),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  )
80
+
81
+ if not client.is_ready():
82
+ raise RuntimeError("Weaviate 连接失败")
83
+
 
 
 
 
84
  try:
85
+ # 清空旧 collection(如果需要)
86
+ if clear_first:
87
+ build_status["progress"] = f"删除旧 collection: {WEAVIATE_COLLECTION}..."
88
+ try:
89
+ if hasattr(client.collections, "delete"):
90
+ client.collections.delete(WEAVIATE_COLLECTION)
91
+ build_status["progress"] = "旧 collection 已删除"
92
+ except Exception as e:
93
+ if "404" not in str(e) and "not found" not in str(e).lower():
94
+ build_status["progress"] = f"删除旧 collection 时警告: {e}"
95
+
96
+ # 读取文档
97
+ build_status["progress"] = f"读取课程目录: {COURSES_DIR}..."
98
+ reader = SimpleDirectoryReader(
99
+ input_dir=str(COURSES_DIR),
100
+ recursive=True,
101
+ required_exts=[".md", ".pdf", ".txt", ".py", ".ipynb", ".docx"],
102
  )
103
+ documents = reader.load_data()
104
+ build_status["progress"] = f"已加载 {len(documents)} 个文档块"
105
+
106
+ # 创建 vector store
107
+ build_status["progress"] = "创建 Weaviate vector store..."
108
+ vector_store = WeaviateVectorStore(
109
+ weaviate_client=client,
110
+ index_name=WEAVIATE_COLLECTION,
111
+ )
112
+ storage_context = StorageContext.from_defaults(vector_store=vector_store)
113
+
114
+ # 构建索引这会自动进行 embedding 并上传
115
+ build_status["progress"] = f"正在 embedding 并上传到 Weaviate (collection={WEAVIATE_COLLECTION})...\n这可能需要几分钟时间,请耐心等待..."
116
+ index = VectorStoreIndex.from_documents(
117
+ documents,
118
+ storage_context=storage_context,
119
+ )
120
+
121
+ # 等待 batch 提交完成
122
+ time.sleep(3)
123
+
124
+ # 验证
125
+ build_status["progress"] = "验证索引..."
126
+ coll = client.collections.get(WEAVIATE_COLLECTION)
127
+ agg = coll.aggregate.over_all(total_count=True)
128
+ n = agg.total_count
129
+
130
+ build_status["result"] = f"✅ 索引构建成功!\n当前 object count = {n}"
131
+ build_status["progress"] = build_status["result"]
132
+
133
+ finally:
134
+ client.close()
135
+
 
 
 
 
 
 
 
 
 
 
 
 
136
  except Exception as e:
137
+ build_status["error"] = str(e)
138
+ build_status["progress"] = f"❌ 错误: {str(e)}"
139
+ finally:
140
+ build_status["running"] = False
141
+
142
+
143
+ def start_build(clear_first: bool):
144
+ """启动索引构建"""
145
+ global build_status
146
+
147
+ if build_status["running"]:
148
+ return "⚠️ 索引构建正在进行中,请等待完成..."
149
+
150
+ # 重置状态
151
+ build_status = {"running": False, "progress": "", "error": None, "result": None}
152
+
153
+ # 启动后台线程
154
+ thread = threading.Thread(
155
+ target=build_index_worker,
156
+ args=(clear_first,),
157
+ daemon=True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
  )
159
+ thread.start()
160
+
161
+ return "🚀 索引构建已启动,请查看下方进度..."
162
+
163
+
164
+ def get_progress():
165
+ """获取当前进度"""
166
+ if build_status["running"]:
167
+ return build_status["progress"] or "处理中..."
168
+ elif build_status["error"]:
169
+ return f"❌ 错误: {build_status['error']}"
170
+ elif build_status["result"]:
171
+ return build_status["result"]
172
+ else:
173
+ return "等待开始..."
174
+
175
+
176
+ # Gradio 界面
177
+ with gr.Blocks(title="Weaviate 索引构建工具") as app:
178
+ gr.Markdown("""
179
+ # 🔍 Weaviate 索引构建工具
180
+
181
+ 在 Hugging Face Space 上使用 OpenAI API 进行 embedding,并直接上传到 Weaviate Cloud。
182
+
183
+ ## 配置要求
184
+
185
+ 请在 **Settings Secrets** 中添加以下环境变量:
186
+ - `OPENAI_API_KEY`: OpenAI API Key( embedding)
187
+ - `WEAVIATE_URL`: Weaviate Cloud REST 地址
188
+ - `WEAVIATE_API_KEY`: Weaviate API Key
189
+ - `WEAVIATE_COLLECTION`: Collection 名称(默认: GenAICourses)
190
+ - `EMBEDDING_PROVIDER`: openai 或 huggingface(默认: openai)
191
+
192
+ ## 使用步骤
193
+
194
+ 1. 确保已将 `GENAI COURSES` 文件夹上传到 Space 根目录
195
+ 2. 点击下方按钮开始构建索引
196
+ 3. 等待构建完成(可能需要几分钟)
197
+ """)
198
+
199
+ with gr.Row():
200
+ clear_first = gr.Checkbox(
201
+ label="清空旧索引后重建",
202
+ value=True,
203
+ info="如果勾选,会先删除旧的 collection 再重建"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
204
  )
205
+ build_btn = gr.Button("🚀 开始构建索引", variant="primary", size="lg")
206
+
207
+ progress_output = gr.Textbox(
208
+ label="构建进度",
209
+ lines=10,
210
+ interactive=False,
211
+ value="等待开始..."
 
212
  )
213
+
214
+ # 自动刷新进度
215
+ app.load(
216
+ fn=get_progress,
217
+ inputs=[],
218
+ outputs=progress_output,
219
+ every=2, # 每2秒刷新一次
220
  )
221
+
222
+ build_btn.click(
223
+ fn=start_build,
224
+ inputs=[clear_first],
225
+ outputs=progress_output,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
226
  )
227
 
228
 
229
  if __name__ == "__main__":
230
+ app.launch()
 
requirements.txt CHANGED
@@ -1,7 +1,6 @@
1
  gradio>=5.0.0
2
  python-dotenv>=1.0.0
3
  openai>=1.44.0
4
- huggingface_hub>=0.23.0
5
 
6
  llama-index-core>=0.10.0
7
  llama-index-embeddings-openai>=0.1.0
@@ -9,7 +8,11 @@ llama-index-embeddings-openai>=0.1.0
9
  llama-index-embeddings-huggingface>=0.1.0
10
  sentence-transformers>=2.2.0
11
 
12
- # Readers for common course files
 
 
 
 
13
  pypdf
14
  python-docx
15
  nbformat
 
1
  gradio>=5.0.0
2
  python-dotenv>=1.0.0
3
  openai>=1.44.0
 
4
 
5
  llama-index-core>=0.10.0
6
  llama-index-embeddings-openai>=0.1.0
 
8
  llama-index-embeddings-huggingface>=0.1.0
9
  sentence-transformers>=2.2.0
10
 
11
+ # Weaviate Cloud 向量库
12
+ llama-index-vector-stores-weaviate>=0.2.0
13
+ weaviate-client>=4.0.0
14
+
15
+ # Readers for common course files(仅 build_weaviate_index.py 需要)
16
  pypdf
17
  python-docx
18
  nbformat