| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271 |
- #!/usr/bin/env python3
- """
- qaagent67lite 一键部署脚本
- 用法:
- # 1. 创建 instance.yml 指定数据目录
- # 2. 将原始文件放入 {baseDir}/raw/
- # 3. 运行:
- python scripts/setup.py
- 自动执行:
- Step 1: 提取文本 (PDF/Word/PPT → txt)
- Step 2: 建 BM25 索引 (关键词检索)
- Step 3: Wiki 编译 (LLM 知识编译)
- Step 4: 验证
- """
- import json
- import os
- import sys
- import time
- from pathlib import Path
- SCRIPT_DIR = Path(__file__).resolve().parent
- AGENT_DIR = SCRIPT_DIR.parent
- PROJECT_ROOT = AGENT_DIR.parent.parent
- sys.path.insert(0, str(PROJECT_ROOT))
- sys.path.insert(0, str(AGENT_DIR.parent))
- def load_config():
- """加载配置 (agent-config.yml + instance.yml)"""
- import yaml
- config = {}
- agent_config = AGENT_DIR / "agent-config.yml"
- if agent_config.exists():
- with open(agent_config) as f:
- config = yaml.safe_load(f) or {}
- # instance.yml 覆盖
- for candidate in [
- os.environ.get("INSTANCE_CONFIG", ""),
- str(SCRIPT_DIR / "instance.yml"),
- str(AGENT_DIR / "instance.yml"),
- ]:
- if candidate and Path(candidate).is_file():
- with open(candidate) as f:
- instance = yaml.safe_load(f) or {}
- # 深度合并
- for k, v in instance.items():
- if k in config and isinstance(config[k], dict) and isinstance(v, dict):
- config[k].update(v)
- else:
- config[k] = v
- break
- return config
- def setup():
- cfg = load_config()
- knowledge = cfg.get("knowledge", {})
- wiki_cfg = cfg.get("wiki", {})
- base_dir = Path(knowledge.get("baseDir", "./knowledge"))
- raw_dir = Path(knowledge.get("rawDir", str(base_dir / "raw")))
- processed_dir = Path(knowledge.get("processedDir", str(base_dir / "processed")))
- wiki_dir = Path(wiki_cfg.get("dir", "./wiki"))
- print(f"{'='*60}")
- print(f" qaagent67lite 一键部署")
- print(f"{'='*60}")
- print(f" 知识库: {base_dir}")
- print(f" 原始文件: {raw_dir}")
- print(f" Wiki: {wiki_dir}")
- print()
- # 检查原始文件
- if not raw_dir.exists():
- print(f" ❌ 原始文件目录不存在: {raw_dir}")
- print(f" 请先将文件放入 {raw_dir}/")
- return
- raw_files = list(raw_dir.rglob("*"))
- raw_files = [f for f in raw_files if f.is_file() and not f.name.startswith(".")]
- print(f" 📁 发现 {len(raw_files)} 个原始文件")
- if len(raw_files) == 0:
- print(f" ❌ 无文件可处理")
- return
- if len(raw_files) > 500:
- print(f" ⚠️ 文件数 > 500,建议使用 qaagent67lambda (带向量检索)")
- # Step 1: 提取文本
- print(f"\n 📄 Step 1/3: 提取文本...")
- processed_dir.mkdir(parents=True, exist_ok=True)
- extracted = 0
- for f in raw_files:
- out_path = processed_dir / f"{f.stem}.txt"
- if out_path.exists():
- extracted += 1
- continue
- try:
- if f.suffix.lower() == ".pdf":
- _extract_pdf(f, out_path)
- elif f.suffix.lower() in (".docx", ".doc"):
- _extract_docx(f, out_path)
- elif f.suffix.lower() in (".txt", ".md", ".csv", ".json", ".yaml", ".yml"):
- import shutil
- shutil.copy2(f, out_path)
- else:
- continue
- extracted += 1
- except Exception as e:
- print(f" ⚠️ {f.name}: {e}")
- print(f" ✅ 提取完成: {extracted} 个文件")
- # Step 2: BM25 索引
- print(f"\n 🔍 Step 2/3: 建 BM25 索引...")
- t0 = time.time()
- try:
- _build_bm25_index(processed_dir, base_dir, cfg)
- print(f" ✅ BM25 索引完成 ({time.time()-t0:.1f}s)")
- except Exception as e:
- print(f" ❌ BM25 索引失败: {e}")
- # Step 3: Wiki 编译
- print(f"\n 📚 Step 3/3: Wiki 编译...")
- t0 = time.time()
- try:
- _wiki_compile(processed_dir, wiki_dir)
- print(f" ✅ Wiki 编译完成 ({time.time()-t0:.1f}s)")
- except Exception as e:
- print(f" ⚠️ Wiki 编译失败 (可后续补): {e}")
- # 验证
- print(f"\n{'='*60}")
- print(f" ✅ 部署完成!")
- print(f" BM25 索引: {base_dir / 'rag_index.json'}")
- wiki_pages = len(list(wiki_dir.rglob("*.md"))) if wiki_dir.exists() else 0
- print(f" Wiki 页面: {wiki_pages}")
- print(f"\n 启动: python3 -m agentpaas agent create --name lite-qa \\")
- print(f" --config agentexample/qaagent67lite/agent-config.yml")
- print(f" python3 -m agentpaas chat lite-qa")
- print(f"{'='*60}")
- def _extract_pdf(pdf_path, out_path):
- """PDF → txt"""
- try:
- import PyPDF2
- with open(pdf_path, "rb") as f:
- reader = PyPDF2.PdfReader(f)
- text = "\n\n".join(
- page.extract_text() or "" for page in reader.pages
- )
- with open(out_path, "w", encoding="utf-8") as f:
- f.write(text)
- except ImportError:
- # 降级到 pdftotext CLI
- import subprocess
- subprocess.run(["pdftotext", str(pdf_path), str(out_path)],
- capture_output=True, timeout=30)
- def _extract_docx(docx_path, out_path):
- """Word → txt"""
- try:
- import docx
- doc = docx.Document(str(docx_path))
- text = "\n\n".join(p.text for p in doc.paragraphs if p.text.strip())
- with open(out_path, "w", encoding="utf-8") as f:
- f.write(text)
- except ImportError:
- raise RuntimeError("需要 python-docx: pip install python-docx")
- def _build_bm25_index(processed_dir, base_dir, cfg):
- """构建 BM25 索引 (简化版,不需要 embedding)"""
- import re
- from collections import Counter
- chunk_size = cfg.get("rag", {}).get("chunkSize", 512)
- chunk_overlap = cfg.get("rag", {}).get("chunkOverlap", 64)
- chunks = []
- keywords_index = {}
- for txt_file in sorted(processed_dir.glob("*.txt")):
- with open(txt_file, "r", encoding="utf-8", errors="replace") as f:
- content = f.read()
- if not content.strip():
- continue
- # 分块
- file_chunks = _chunk_text(content, chunk_size, chunk_overlap)
- for i, chunk in enumerate(file_chunks):
- chunk_id = f"{txt_file.stem}_chunk_{i}"
- chunks.append({
- "source": txt_file.stem,
- "chunk_id": chunk_id,
- "text": chunk,
- })
- # 关键词索引
- words = re.findall(r'[\u4e00-\u9fff]+|[a-zA-Z]+', chunk.lower())
- word_counts = Counter(words)
- for word, count in word_counts.items():
- if len(word) >= 2:
- if word not in keywords_index:
- keywords_index[word] = []
- keywords_index[word].append({
- "chunk_id": chunk_id,
- "count": count,
- })
- # 保存
- index_path = base_dir / "rag_index.json"
- with open(index_path, "w", encoding="utf-8") as f:
- json.dump({"chunks": chunks, "total": len(chunks)}, f, ensure_ascii=False)
- kw_path = base_dir / "rag_index.keywords.json"
- with open(kw_path, "w", encoding="utf-8") as f:
- json.dump(keywords_index, f, ensure_ascii=False)
- print(f" {len(chunks)} 个文本块, {len(keywords_index)} 个关键词")
- def _chunk_text(text, size=512, overlap=64):
- """按字符数分块"""
- chunks = []
- start = 0
- while start < len(text):
- end = start + size
- chunk = text[start:end]
- if chunk.strip():
- chunks.append(chunk.strip())
- start = end - overlap
- return chunks
- def _wiki_compile(processed_dir, wiki_dir):
- """调用 WikiIngest 编译 wiki"""
- from lambdagent.builtin_tools.wiki_tools import wiki_ingest
- wiki_dir.mkdir(parents=True, exist_ok=True)
- txt_files = sorted(processed_dir.glob("*.txt"))
- total = len(txt_files)
- done = 0
- for txt_file in txt_files:
- try:
- wiki_ingest(json.dumps({
- "path": str(txt_file),
- "wiki_root": str(wiki_dir),
- }))
- done += 1
- if done % 10 == 0:
- print(f" Wiki: {done}/{total} ({done*100//total}%)")
- except Exception as e:
- print(f" ⚠️ {txt_file.name}: {e}")
- print(f" Wiki 编译: {done}/{total} 完成")
- if __name__ == "__main__":
- setup()
|