setup.py 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. #!/usr/bin/env python3
  2. """
  3. qaagent67lite 一键部署脚本
  4. 用法:
  5. # 1. 创建 instance.yml 指定数据目录
  6. # 2. 将原始文件放入 {baseDir}/raw/
  7. # 3. 运行:
  8. python scripts/setup.py
  9. 自动执行:
  10. Step 1: 提取文本 (PDF/Word/PPT → txt)
  11. Step 2: 建 BM25 索引 (关键词检索)
  12. Step 3: Wiki 编译 (LLM 知识编译)
  13. Step 4: 验证
  14. """
  15. import json
  16. import os
  17. import sys
  18. import time
  19. from pathlib import Path
  20. SCRIPT_DIR = Path(__file__).resolve().parent
  21. AGENT_DIR = SCRIPT_DIR.parent
  22. PROJECT_ROOT = AGENT_DIR.parent.parent
  23. sys.path.insert(0, str(PROJECT_ROOT))
  24. sys.path.insert(0, str(AGENT_DIR.parent))
  25. def load_config():
  26. """加载配置 (agent-config.yml + instance.yml)"""
  27. import yaml
  28. config = {}
  29. agent_config = AGENT_DIR / "agent-config.yml"
  30. if agent_config.exists():
  31. with open(agent_config) as f:
  32. config = yaml.safe_load(f) or {}
  33. # instance.yml 覆盖
  34. for candidate in [
  35. os.environ.get("INSTANCE_CONFIG", ""),
  36. str(SCRIPT_DIR / "instance.yml"),
  37. str(AGENT_DIR / "instance.yml"),
  38. ]:
  39. if candidate and Path(candidate).is_file():
  40. with open(candidate) as f:
  41. instance = yaml.safe_load(f) or {}
  42. # 深度合并
  43. for k, v in instance.items():
  44. if k in config and isinstance(config[k], dict) and isinstance(v, dict):
  45. config[k].update(v)
  46. else:
  47. config[k] = v
  48. break
  49. return config
  50. def setup():
  51. cfg = load_config()
  52. knowledge = cfg.get("knowledge", {})
  53. wiki_cfg = cfg.get("wiki", {})
  54. base_dir = Path(knowledge.get("baseDir", "./knowledge"))
  55. raw_dir = Path(knowledge.get("rawDir", str(base_dir / "raw")))
  56. processed_dir = Path(knowledge.get("processedDir", str(base_dir / "processed")))
  57. wiki_dir = Path(wiki_cfg.get("dir", "./wiki"))
  58. print(f"{'='*60}")
  59. print(f" qaagent67lite 一键部署")
  60. print(f"{'='*60}")
  61. print(f" 知识库: {base_dir}")
  62. print(f" 原始文件: {raw_dir}")
  63. print(f" Wiki: {wiki_dir}")
  64. print()
  65. # 检查原始文件
  66. if not raw_dir.exists():
  67. print(f" ❌ 原始文件目录不存在: {raw_dir}")
  68. print(f" 请先将文件放入 {raw_dir}/")
  69. return
  70. raw_files = list(raw_dir.rglob("*"))
  71. raw_files = [f for f in raw_files if f.is_file() and not f.name.startswith(".")]
  72. print(f" 📁 发现 {len(raw_files)} 个原始文件")
  73. if len(raw_files) == 0:
  74. print(f" ❌ 无文件可处理")
  75. return
  76. if len(raw_files) > 500:
  77. print(f" ⚠️ 文件数 > 500,建议使用 qaagent67lambda (带向量检索)")
  78. # Step 1: 提取文本
  79. print(f"\n 📄 Step 1/3: 提取文本...")
  80. processed_dir.mkdir(parents=True, exist_ok=True)
  81. extracted = 0
  82. for f in raw_files:
  83. out_path = processed_dir / f"{f.stem}.txt"
  84. if out_path.exists():
  85. extracted += 1
  86. continue
  87. try:
  88. if f.suffix.lower() == ".pdf":
  89. _extract_pdf(f, out_path)
  90. elif f.suffix.lower() in (".docx", ".doc"):
  91. _extract_docx(f, out_path)
  92. elif f.suffix.lower() in (".txt", ".md", ".csv", ".json", ".yaml", ".yml"):
  93. import shutil
  94. shutil.copy2(f, out_path)
  95. else:
  96. continue
  97. extracted += 1
  98. except Exception as e:
  99. print(f" ⚠️ {f.name}: {e}")
  100. print(f" ✅ 提取完成: {extracted} 个文件")
  101. # Step 2: BM25 索引
  102. print(f"\n 🔍 Step 2/3: 建 BM25 索引...")
  103. t0 = time.time()
  104. try:
  105. _build_bm25_index(processed_dir, base_dir, cfg)
  106. print(f" ✅ BM25 索引完成 ({time.time()-t0:.1f}s)")
  107. except Exception as e:
  108. print(f" ❌ BM25 索引失败: {e}")
  109. # Step 3: Wiki 编译
  110. print(f"\n 📚 Step 3/3: Wiki 编译...")
  111. t0 = time.time()
  112. try:
  113. _wiki_compile(processed_dir, wiki_dir)
  114. print(f" ✅ Wiki 编译完成 ({time.time()-t0:.1f}s)")
  115. except Exception as e:
  116. print(f" ⚠️ Wiki 编译失败 (可后续补): {e}")
  117. # 验证
  118. print(f"\n{'='*60}")
  119. print(f" ✅ 部署完成!")
  120. print(f" BM25 索引: {base_dir / 'rag_index.json'}")
  121. wiki_pages = len(list(wiki_dir.rglob("*.md"))) if wiki_dir.exists() else 0
  122. print(f" Wiki 页面: {wiki_pages}")
  123. print(f"\n 启动: python3 -m agentpaas agent create --name lite-qa \\")
  124. print(f" --config agentexample/qaagent67lite/agent-config.yml")
  125. print(f" python3 -m agentpaas chat lite-qa")
  126. print(f"{'='*60}")
  127. def _extract_pdf(pdf_path, out_path):
  128. """PDF → txt"""
  129. try:
  130. import PyPDF2
  131. with open(pdf_path, "rb") as f:
  132. reader = PyPDF2.PdfReader(f)
  133. text = "\n\n".join(
  134. page.extract_text() or "" for page in reader.pages
  135. )
  136. with open(out_path, "w", encoding="utf-8") as f:
  137. f.write(text)
  138. except ImportError:
  139. # 降级到 pdftotext CLI
  140. import subprocess
  141. subprocess.run(["pdftotext", str(pdf_path), str(out_path)],
  142. capture_output=True, timeout=30)
  143. def _extract_docx(docx_path, out_path):
  144. """Word → txt"""
  145. try:
  146. import docx
  147. doc = docx.Document(str(docx_path))
  148. text = "\n\n".join(p.text for p in doc.paragraphs if p.text.strip())
  149. with open(out_path, "w", encoding="utf-8") as f:
  150. f.write(text)
  151. except ImportError:
  152. raise RuntimeError("需要 python-docx: pip install python-docx")
  153. def _build_bm25_index(processed_dir, base_dir, cfg):
  154. """构建 BM25 索引 (简化版,不需要 embedding)"""
  155. import re
  156. from collections import Counter
  157. chunk_size = cfg.get("rag", {}).get("chunkSize", 512)
  158. chunk_overlap = cfg.get("rag", {}).get("chunkOverlap", 64)
  159. chunks = []
  160. keywords_index = {}
  161. for txt_file in sorted(processed_dir.glob("*.txt")):
  162. with open(txt_file, "r", encoding="utf-8", errors="replace") as f:
  163. content = f.read()
  164. if not content.strip():
  165. continue
  166. # 分块
  167. file_chunks = _chunk_text(content, chunk_size, chunk_overlap)
  168. for i, chunk in enumerate(file_chunks):
  169. chunk_id = f"{txt_file.stem}_chunk_{i}"
  170. chunks.append({
  171. "source": txt_file.stem,
  172. "chunk_id": chunk_id,
  173. "text": chunk,
  174. })
  175. # 关键词索引
  176. words = re.findall(r'[\u4e00-\u9fff]+|[a-zA-Z]+', chunk.lower())
  177. word_counts = Counter(words)
  178. for word, count in word_counts.items():
  179. if len(word) >= 2:
  180. if word not in keywords_index:
  181. keywords_index[word] = []
  182. keywords_index[word].append({
  183. "chunk_id": chunk_id,
  184. "count": count,
  185. })
  186. # 保存
  187. index_path = base_dir / "rag_index.json"
  188. with open(index_path, "w", encoding="utf-8") as f:
  189. json.dump({"chunks": chunks, "total": len(chunks)}, f, ensure_ascii=False)
  190. kw_path = base_dir / "rag_index.keywords.json"
  191. with open(kw_path, "w", encoding="utf-8") as f:
  192. json.dump(keywords_index, f, ensure_ascii=False)
  193. print(f" {len(chunks)} 个文本块, {len(keywords_index)} 个关键词")
  194. def _chunk_text(text, size=512, overlap=64):
  195. """按字符数分块"""
  196. chunks = []
  197. start = 0
  198. while start < len(text):
  199. end = start + size
  200. chunk = text[start:end]
  201. if chunk.strip():
  202. chunks.append(chunk.strip())
  203. start = end - overlap
  204. return chunks
  205. def _wiki_compile(processed_dir, wiki_dir):
  206. """调用 WikiIngest 编译 wiki"""
  207. from lambdagent.builtin_tools.wiki_tools import wiki_ingest
  208. wiki_dir.mkdir(parents=True, exist_ok=True)
  209. txt_files = sorted(processed_dir.glob("*.txt"))
  210. total = len(txt_files)
  211. done = 0
  212. for txt_file in txt_files:
  213. try:
  214. wiki_ingest(json.dumps({
  215. "path": str(txt_file),
  216. "wiki_root": str(wiki_dir),
  217. }))
  218. done += 1
  219. if done % 10 == 0:
  220. print(f" Wiki: {done}/{total} ({done*100//total}%)")
  221. except Exception as e:
  222. print(f" ⚠️ {txt_file.name}: {e}")
  223. print(f" Wiki 编译: {done}/{total} 完成")
  224. if __name__ == "__main__":
  225. setup()