Skip to content

Commit 700c1ee

Browse files
authored
Merge pull request #78 from pi-dal/feat/issue-75
feat: add PowerRAG SDK text QA retrieval demo
2 parents 32df604 + bd1ce1a commit 700c1ee

11 files changed

Lines changed: 690 additions & 1 deletion

File tree

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# PowerRAG (RAGFlow) SDK demo config
2+
3+
# SDK endpoint (from your docker-compose env: SVR_HTTP_PORT=9380)
4+
RAGFLOW_BASE_URL=http://127.0.0.1:9380
5+
6+
# SDK API key (format: ragflow-...; created via /v1/api/new_token)
7+
RAGFLOW_API_KEY=ragflow-REPLACE_ME
8+
9+
# Optional: override dataset name created by the demo
10+
RAGFLOW_DATASET_NAME=powerrag_text_qa_demo
11+
12+
# Optional: override embedding model for dataset creation (recommended to leave empty and use tenant default)
13+
# Format: <model>@<factory>
14+
# Example:
15+
# RAGFLOW_EMBEDDING_MODEL=text-embedding-3-small@OpenAI
16+
RAGFLOW_EMBEDDING_MODEL=
17+
18+
# -----------------------------
19+
# Optional: embedding provider config (used by the README “API 配置 embedding” steps)
20+
# -----------------------------
21+
22+
# Use the factory/model name shown by your PowerRAG UI/API.
23+
EMB_FACTORY=REPLACE_ME
24+
EMB_MODEL=REPLACE_ME
25+
EMB_API_BASE=REPLACE_ME
26+
EMB_API_KEY=REPLACE_ME
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"""
2+
PowerRAG (RAGFlow) SDK Demo configuration.
3+
4+
This module follows the `code/` directory convention:
5+
- Provide a small config object
6+
- Load `.env` automatically (if present)
7+
"""
8+
9+
from __future__ import annotations
10+
11+
import os
12+
from dataclasses import dataclass
13+
14+
from dotenv import load_dotenv
15+
16+
load_dotenv()
17+
18+
19+
def _bool_env(name: str, default: bool = False) -> bool:
20+
raw = os.getenv(name)
21+
if raw is None:
22+
return default
23+
raw = raw.strip().lower()
24+
if raw in {"1", "true", "yes", "y", "on"}:
25+
return True
26+
if raw in {"0", "false", "no", "n", "off"}:
27+
return False
28+
return default
29+
30+
31+
@dataclass(frozen=True)
32+
class PowerRAGDemoConfig:
33+
base_url: str = os.getenv("RAGFLOW_BASE_URL", "http://127.0.0.1:9380").strip()
34+
api_key: str = os.getenv("RAGFLOW_API_KEY", "").strip()
35+
dataset_name: str = os.getenv("RAGFLOW_DATASET_NAME", "powerrag_text_qa_demo").strip()
36+
embedding_model: str = os.getenv("RAGFLOW_EMBEDDING_MODEL", "").strip()
37+
38+
top_k: int = int(os.getenv("RAGFLOW_TOP_K", "5"))
39+
candidate_k: int = int(os.getenv("RAGFLOW_CANDIDATE_K", "1024"))
40+
similarity_threshold: float = float(os.getenv("RAGFLOW_SIMILARITY_THRESHOLD", "0.2"))
41+
vector_similarity_weight: float = float(os.getenv("RAGFLOW_VECTOR_SIMILARITY_WEIGHT", "0.3"))
42+
keyword: bool = _bool_env("RAGFLOW_KEYWORD", False)
43+
44+
45+
DEFAULT_CONFIG = PowerRAGDemoConfig()
46+
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
#!/usr/bin/env python3
2+
from __future__ import annotations
3+
4+
import argparse
5+
import os
6+
import sys
7+
from pathlib import Path
8+
from typing import Any
9+
10+
from config import DEFAULT_CONFIG
11+
12+
13+
def _env(name: str, default: str | None = None) -> str | None:
14+
value = os.getenv(name)
15+
if value is None or value.strip() == "":
16+
return default
17+
return value.strip()
18+
19+
20+
def _require(value: str | None, hint: str) -> str:
21+
if value is None or value.strip() == "":
22+
raise SystemExit(hint)
23+
return value.strip()
24+
25+
26+
def _read_bytes(path: Path) -> bytes:
27+
try:
28+
return path.read_bytes()
29+
except FileNotFoundError:
30+
raise SystemExit(f"File not found: {path}")
31+
32+
33+
def _safe_get(obj: Any, attr: str, default: Any = None) -> Any:
34+
try:
35+
return getattr(obj, attr)
36+
except Exception:
37+
return default
38+
39+
40+
def main(argv: list[str]) -> int:
41+
parser = argparse.ArgumentParser(
42+
description="PowerRAG (RAGFlow) SDK demo: upload Markdown, parse, retrieve top-k chunks.",
43+
)
44+
parser.add_argument("--file", type=Path, required=True, help="Markdown file path, e.g. ./data/sample.md")
45+
parser.add_argument("--question", type=str, required=True, help="User question for retrieval")
46+
parser.add_argument("--top-k", type=int, default=DEFAULT_CONFIG.top_k, help="How many chunks to return (mapped to page_size)")
47+
parser.add_argument(
48+
"--embedding-model",
49+
type=str,
50+
default=DEFAULT_CONFIG.embedding_model or _env("RAGFLOW_EMBEDDING_MODEL"),
51+
help=(
52+
"Embedding model string in '<model>@<factory>' format. "
53+
"If omitted, server tenant default is used."
54+
),
55+
)
56+
parser.add_argument("--candidate-k", type=int, default=DEFAULT_CONFIG.candidate_k, help="RAGFlow.retrieve(top_k=...) candidate pool size")
57+
parser.add_argument("--similarity-threshold", type=float, default=DEFAULT_CONFIG.similarity_threshold, help="Filter chunks below this similarity")
58+
parser.add_argument("--vector-similarity-weight", type=float, default=DEFAULT_CONFIG.vector_similarity_weight, help="Weight of vector similarity in hybrid score")
59+
parser.add_argument("--keyword", action="store_true", default=DEFAULT_CONFIG.keyword, help="Enable keyword matching (hybrid retrieval)")
60+
parser.add_argument("--dataset-name", type=str, default=DEFAULT_CONFIG.dataset_name, help="Dataset name to create")
61+
parser.add_argument(
62+
"--base-url",
63+
type=str,
64+
default=DEFAULT_CONFIG.base_url or _env("RAGFLOW_BASE_URL") or _env("POWERRAG_BASE_URL") or _env("BASE_URL"),
65+
help="RAGFlow/PowerRAG base_url (or env RAGFLOW_BASE_URL / POWERRAG_BASE_URL / BASE_URL)",
66+
)
67+
parser.add_argument(
68+
"--api-key",
69+
type=str,
70+
default=DEFAULT_CONFIG.api_key or _env("RAGFLOW_API_KEY") or _env("POWERRAG_API_KEY") or _env("API_KEY"),
71+
help="RAGFlow/PowerRAG api_key (or env RAGFLOW_API_KEY / POWERRAG_API_KEY / API_KEY)",
72+
)
73+
parser.add_argument("--cleanup", action="store_true", help="Delete created dataset after finishing")
74+
75+
args = parser.parse_args(argv)
76+
77+
base_url = _require(args.base_url, "Missing base_url. Use --base-url or set env RAGFLOW_BASE_URL.")
78+
api_key = _require(args.api_key, "Missing api_key. Use --api-key or set env RAGFLOW_API_KEY.")
79+
80+
if args.top_k <= 0:
81+
raise SystemExit("--top-k must be > 0")
82+
if args.candidate_k <= 0:
83+
raise SystemExit("--candidate-k must be > 0")
84+
85+
blob = _read_bytes(args.file)
86+
display_name = args.file.name
87+
if not display_name.lower().endswith(".md"):
88+
display_name = f"{display_name}.md"
89+
90+
try:
91+
from ragflow_sdk import RAGFlow # type: ignore
92+
except Exception as e:
93+
raise SystemExit(
94+
"Failed to import ragflow_sdk. Install dependencies first:\n"
95+
" pip install -r requirements.txt\n"
96+
f"Original error: {e}"
97+
)
98+
99+
rag = RAGFlow(api_key=api_key, base_url=base_url)
100+
101+
dataset_kwargs: dict[str, Any] = {"name": args.dataset_name}
102+
if args.embedding_model:
103+
dataset_kwargs["embedding_model"] = args.embedding_model
104+
dataset = rag.create_dataset(**dataset_kwargs)
105+
try:
106+
docs = dataset.upload_documents([{"display_name": display_name, "blob": blob}])
107+
if not docs:
108+
raise SystemExit("Upload succeeded but no document returned by SDK.")
109+
doc = docs[0]
110+
111+
parse_results = dataset.parse_documents([doc.id])
112+
# parse_results: list[tuple[doc_id, status, success_count, failure_count]] (per API ref)
113+
print("Parse results:")
114+
print(parse_results)
115+
if parse_results and isinstance(parse_results, list):
116+
statuses = {r[1] for r in parse_results if isinstance(r, (list, tuple)) and len(r) >= 2}
117+
if statuses and statuses != {"DONE"}:
118+
raise SystemExit(
119+
"Document parsing failed (status not DONE). "
120+
"Most common cause is missing/unauthorized embedding model.\n"
121+
"Try:\n"
122+
" - set tenant default embedding model in UI or via /v1/user/set_tenant_info, OR\n"
123+
" - rerun with --embedding-model '<model>@<factory>' (must be supported & configured for the tenant)\n"
124+
"If it still fails, check PowerRAG logs inside the container (task executor) for the detailed error.\n"
125+
)
126+
127+
chunks = rag.retrieve(
128+
question=args.question,
129+
dataset_ids=[dataset.id],
130+
document_ids=[doc.id],
131+
page=1,
132+
page_size=args.top_k,
133+
similarity_threshold=args.similarity_threshold,
134+
vector_similarity_weight=args.vector_similarity_weight,
135+
top_k=args.candidate_k,
136+
keyword=args.keyword,
137+
)
138+
139+
print("\nRetrieved chunks:")
140+
if not chunks:
141+
print("(empty)")
142+
return 0
143+
144+
for i, c in enumerate(chunks, start=1):
145+
similarity = _safe_get(c, "similarity")
146+
vector_similarity = _safe_get(c, "vector_similarity")
147+
term_similarity = _safe_get(c, "term_similarity")
148+
content = _safe_get(c, "content", "")
149+
content_preview = (content or "").strip().replace("\n", " ")
150+
if len(content_preview) > 260:
151+
content_preview = content_preview[:260] + "…"
152+
print(f"{i:02d}. similarity={similarity} vector={vector_similarity} term={term_similarity}")
153+
print(f" {content_preview}")
154+
155+
return 0
156+
finally:
157+
if args.cleanup:
158+
try:
159+
rag.delete_datasets(ids=[dataset.id])
160+
except Exception as e:
161+
print(f"Warning: failed to cleanup dataset {dataset.id}: {e}", file=sys.stderr)
162+
163+
164+
if __name__ == "__main__":
165+
raise SystemExit(main(sys.argv[1:]))
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
ragflow-sdk
2+
python-dotenv
3+
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
1) 这个 demo 的验收标准是什么?
2+
2) 餐厅排队系统里,如果顾客过号,通常怎么处理?
3+
3) 已发货未签收的退款规则是什么?
4+
4) 如何估算排队等待时间?
5+
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# PowerRAG 文本问答 Demo · 示例文档
2+
3+
## 1. 项目背景
4+
5+
本示例用于演示:上传一份 Markdown 文档 → 服务端自动解析与分块 → 基于问题检索相关 chunks。
6+
7+
## 2. 关键概念
8+
9+
- **分块(Chunk)**:把长文切成多个小段,便于向量化与检索。
10+
- **向量检索(Vector Search)**:把文本映射到向量空间,通过相似度找到相关片段。
11+
- **Top-k**:返回最相关的 k 个片段。
12+
13+
## 3. 规则与约束
14+
15+
1) 只有当“检索到的 chunks 与问题语义相关”时,才算成功。
16+
2) 本 demo 不要求大模型生成最终回答(可选)。
17+
18+
## 4. 示例内容:餐厅排队系统
19+
20+
我们要做一个餐厅排队系统,核心流程如下:
21+
22+
1. 顾客在前台取号,系统生成排队号(例如 A001)。
23+
2. 服务员在就餐区空位出现时叫号,顾客到号后入座。
24+
3. 如果顾客过号,可选择重新排队或延后若干位。
25+
4. 系统需要支持查询当前排队情况,以及某个号码前面还有多少人。
26+
27+
### 4.1 常见问题
28+
29+
- “过号后怎么处理?”:可以延后或重新取号,策略由门店决定。
30+
- “如何估算等待时间?”:可以用平均翻台时间 × 前方人数估算。
31+
- “如何处理多人同时取号?”:需要对取号操作加锁或用原子自增保证顺序。
32+
33+
## 5. 示例内容:退款规则
34+
35+
退款规则如下:
36+
37+
- 未发货:可全额退款。
38+
- 已发货未签收:可申请退款,但需要承担退货运费。
39+
- 已签收:7 天内可退货退款;超过 7 天视情况处理。
40+
269 KB
Loading
325 KB
Loading
190 KB
Loading

0 commit comments

Comments
 (0)