Initial commit

This commit is contained in:
2026-05-12 09:41:56 +08:00
commit 572283e101
936 changed files with 133949 additions and 0 deletions
@@ -0,0 +1,26 @@
# PowerRAG (RAGFlow) SDK demo config
# SDK endpoint (from your docker-compose env: SVR_HTTP_PORT=9380)
RAGFLOW_BASE_URL=http://127.0.0.1:9380
# SDK API key (format: ragflow-...; created via /v1/api/new_token)
RAGFLOW_API_KEY=ragflow-REPLACE_ME
# Optional: override dataset name created by the demo
RAGFLOW_DATASET_NAME=powerrag_text_qa_demo
# Optional: override embedding model for dataset creation (recommended to leave empty and use tenant default)
# Format: <model>@<factory>
# Example:
# RAGFLOW_EMBEDDING_MODEL=text-embedding-3-small@OpenAI
RAGFLOW_EMBEDDING_MODEL=
# -----------------------------
# Optional: embedding provider config (used by the README “API 配置 embedding” steps)
# -----------------------------
# Use the factory/model name shown by your PowerRAG UI/API.
EMB_FACTORY=REPLACE_ME
EMB_MODEL=REPLACE_ME
EMB_API_BASE=REPLACE_ME
EMB_API_KEY=REPLACE_ME
@@ -0,0 +1,46 @@
"""
PowerRAG (RAGFlow) SDK Demo configuration.
This module follows the `code/` directory convention:
- Provide a small config object
- Load `.env` automatically (if present)
"""
from __future__ import annotations
import os
from dataclasses import dataclass
from dotenv import load_dotenv
load_dotenv()
def _bool_env(name: str, default: bool = False) -> bool:
raw = os.getenv(name)
if raw is None:
return default
raw = raw.strip().lower()
if raw in {"1", "true", "yes", "y", "on"}:
return True
if raw in {"0", "false", "no", "n", "off"}:
return False
return default
@dataclass(frozen=True)
class PowerRAGDemoConfig:
base_url: str = os.getenv("RAGFLOW_BASE_URL", "http://127.0.0.1:9380").strip()
api_key: str = os.getenv("RAGFLOW_API_KEY", "").strip()
dataset_name: str = os.getenv("RAGFLOW_DATASET_NAME", "powerrag_text_qa_demo").strip()
embedding_model: str = os.getenv("RAGFLOW_EMBEDDING_MODEL", "").strip()
top_k: int = int(os.getenv("RAGFLOW_TOP_K", "5"))
candidate_k: int = int(os.getenv("RAGFLOW_CANDIDATE_K", "1024"))
similarity_threshold: float = float(os.getenv("RAGFLOW_SIMILARITY_THRESHOLD", "0.2"))
vector_similarity_weight: float = float(os.getenv("RAGFLOW_VECTOR_SIMILARITY_WEIGHT", "0.3"))
keyword: bool = _bool_env("RAGFLOW_KEYWORD", False)
DEFAULT_CONFIG = PowerRAGDemoConfig()
@@ -0,0 +1,165 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import os
import sys
from pathlib import Path
from typing import Any
from config import DEFAULT_CONFIG
def _env(name: str, default: str | None = None) -> str | None:
value = os.getenv(name)
if value is None or value.strip() == "":
return default
return value.strip()
def _require(value: str | None, hint: str) -> str:
if value is None or value.strip() == "":
raise SystemExit(hint)
return value.strip()
def _read_bytes(path: Path) -> bytes:
try:
return path.read_bytes()
except FileNotFoundError:
raise SystemExit(f"File not found: {path}")
def _safe_get(obj: Any, attr: str, default: Any = None) -> Any:
try:
return getattr(obj, attr)
except Exception:
return default
def main(argv: list[str]) -> int:
parser = argparse.ArgumentParser(
description="PowerRAG (RAGFlow) SDK demo: upload Markdown, parse, retrieve top-k chunks.",
)
parser.add_argument("--file", type=Path, required=True, help="Markdown file path, e.g. ./data/sample.md")
parser.add_argument("--question", type=str, required=True, help="User question for retrieval")
parser.add_argument("--top-k", type=int, default=DEFAULT_CONFIG.top_k, help="How many chunks to return (mapped to page_size)")
parser.add_argument(
"--embedding-model",
type=str,
default=DEFAULT_CONFIG.embedding_model or _env("RAGFLOW_EMBEDDING_MODEL"),
help=(
"Embedding model string in '<model>@<factory>' format. "
"If omitted, server tenant default is used."
),
)
parser.add_argument("--candidate-k", type=int, default=DEFAULT_CONFIG.candidate_k, help="RAGFlow.retrieve(top_k=...) candidate pool size")
parser.add_argument("--similarity-threshold", type=float, default=DEFAULT_CONFIG.similarity_threshold, help="Filter chunks below this similarity")
parser.add_argument("--vector-similarity-weight", type=float, default=DEFAULT_CONFIG.vector_similarity_weight, help="Weight of vector similarity in hybrid score")
parser.add_argument("--keyword", action="store_true", default=DEFAULT_CONFIG.keyword, help="Enable keyword matching (hybrid retrieval)")
parser.add_argument("--dataset-name", type=str, default=DEFAULT_CONFIG.dataset_name, help="Dataset name to create")
parser.add_argument(
"--base-url",
type=str,
default=DEFAULT_CONFIG.base_url or _env("RAGFLOW_BASE_URL") or _env("POWERRAG_BASE_URL") or _env("BASE_URL"),
help="RAGFlow/PowerRAG base_url (or env RAGFLOW_BASE_URL / POWERRAG_BASE_URL / BASE_URL)",
)
parser.add_argument(
"--api-key",
type=str,
default=DEFAULT_CONFIG.api_key or _env("RAGFLOW_API_KEY") or _env("POWERRAG_API_KEY") or _env("API_KEY"),
help="RAGFlow/PowerRAG api_key (or env RAGFLOW_API_KEY / POWERRAG_API_KEY / API_KEY)",
)
parser.add_argument("--cleanup", action="store_true", help="Delete created dataset after finishing")
args = parser.parse_args(argv)
base_url = _require(args.base_url, "Missing base_url. Use --base-url or set env RAGFLOW_BASE_URL.")
api_key = _require(args.api_key, "Missing api_key. Use --api-key or set env RAGFLOW_API_KEY.")
if args.top_k <= 0:
raise SystemExit("--top-k must be > 0")
if args.candidate_k <= 0:
raise SystemExit("--candidate-k must be > 0")
blob = _read_bytes(args.file)
display_name = args.file.name
if not display_name.lower().endswith(".md"):
display_name = f"{display_name}.md"
try:
from ragflow_sdk import RAGFlow # type: ignore
except Exception as e:
raise SystemExit(
"Failed to import ragflow_sdk. Install dependencies first:\n"
" pip install -r requirements.txt\n"
f"Original error: {e}"
)
rag = RAGFlow(api_key=api_key, base_url=base_url)
dataset_kwargs: dict[str, Any] = {"name": args.dataset_name}
if args.embedding_model:
dataset_kwargs["embedding_model"] = args.embedding_model
dataset = rag.create_dataset(**dataset_kwargs)
try:
docs = dataset.upload_documents([{"display_name": display_name, "blob": blob}])
if not docs:
raise SystemExit("Upload succeeded but no document returned by SDK.")
doc = docs[0]
parse_results = dataset.parse_documents([doc.id])
# parse_results: list[tuple[doc_id, status, success_count, failure_count]] (per API ref)
print("Parse results:")
print(parse_results)
if parse_results and isinstance(parse_results, list):
statuses = {r[1] for r in parse_results if isinstance(r, (list, tuple)) and len(r) >= 2}
if statuses and statuses != {"DONE"}:
raise SystemExit(
"Document parsing failed (status not DONE). "
"Most common cause is missing/unauthorized embedding model.\n"
"Try:\n"
" - set tenant default embedding model in UI or via /v1/user/set_tenant_info, OR\n"
" - rerun with --embedding-model '<model>@<factory>' (must be supported & configured for the tenant)\n"
"If it still fails, check PowerRAG logs inside the container (task executor) for the detailed error.\n"
)
chunks = rag.retrieve(
question=args.question,
dataset_ids=[dataset.id],
document_ids=[doc.id],
page=1,
page_size=args.top_k,
similarity_threshold=args.similarity_threshold,
vector_similarity_weight=args.vector_similarity_weight,
top_k=args.candidate_k,
keyword=args.keyword,
)
print("\nRetrieved chunks:")
if not chunks:
print("(empty)")
return 0
for i, c in enumerate(chunks, start=1):
similarity = _safe_get(c, "similarity")
vector_similarity = _safe_get(c, "vector_similarity")
term_similarity = _safe_get(c, "term_similarity")
content = _safe_get(c, "content", "")
content_preview = (content or "").strip().replace("\n", " ")
if len(content_preview) > 260:
content_preview = content_preview[:260] + ""
print(f"{i:02d}. similarity={similarity} vector={vector_similarity} term={term_similarity}")
print(f" {content_preview}")
return 0
finally:
if args.cleanup:
try:
rag.delete_datasets(ids=[dataset.id])
except Exception as e:
print(f"Warning: failed to cleanup dataset {dataset.id}: {e}", file=sys.stderr)
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))
@@ -0,0 +1,3 @@
ragflow-sdk
python-dotenv
@@ -0,0 +1,5 @@
1) 这个 demo 的验收标准是什么?
2) 餐厅排队系统里,如果顾客过号,通常怎么处理?
3) 已发货未签收的退款规则是什么?
4) 如何估算排队等待时间?
@@ -0,0 +1,40 @@
# PowerRAG 文本问答 Demo · 示例文档
## 1. 项目背景
本示例用于演示:上传一份 Markdown 文档 → 服务端自动解析与分块 → 基于问题检索相关 chunks。
## 2. 关键概念
- **分块(Chunk)**:把长文切成多个小段,便于向量化与检索。
- **向量检索(Vector Search)**:把文本映射到向量空间,通过相似度找到相关片段。
- **Top-k**:返回最相关的 k 个片段。
## 3. 规则与约束
1) 只有当“检索到的 chunks 与问题语义相关”时,才算成功。
2) 本 demo 不要求大模型生成最终回答(可选)。
## 4. 示例内容:餐厅排队系统
我们要做一个餐厅排队系统,核心流程如下:
1. 顾客在前台取号,系统生成排队号(例如 A001)。
2. 服务员在就餐区空位出现时叫号,顾客到号后入座。
3. 如果顾客过号,可选择重新排队或延后若干位。
4. 系统需要支持查询当前排队情况,以及某个号码前面还有多少人。
### 4.1 常见问题
- “过号后怎么处理?”:可以延后或重新取号,策略由门店决定。
- “如何估算等待时间?”:可以用平均翻台时间 × 前方人数估算。
- “如何处理多人同时取号?”:需要对取号操作加锁或用原子自增保证顺序。
## 5. 示例内容:退款规则
退款规则如下:
- 未发货:可全额退款。
- 已发货未签收:可申请退款,但需要承担退货运费。
- 已签收:7 天内可退货退款;超过 7 天视情况处理。
Binary file not shown.

After

Width:  |  Height:  |  Size: 268 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

@@ -0,0 +1,404 @@
# PowerRAG SDK 文本问答检索 Demo
## 一、这篇专题要解决什么问题?
很多同学做 RAG 时会先把注意力放在“怎么让大模型回答得更像人”。但只要检索没找对上下文,生成再花哨也只是“把错讲得更顺”。
这个专题做一件更朴素、也更值得先掌握的事:
> **只做检索,不做生成。**
你会把一份 Markdown 文档交给服务端,让服务端完成解析、切分、向量化,然后用问题去做 Top‑K 检索,拿回最相关的原文片段(chunks)。
**验收标准也很直接**Top‑K chunks 是否与问题语义相关(不要求最终答案)。
本专题目录结构:
- `readme.md`:本文(教学文档)
- `images/`:配图
- `code/`:可运行脚本与配置(`main.py``config.py``.env.example``requirements.txt`
- `data/`:可复现样例数据(`sample.md` + `questions.txt`
---
## 二、技术方案:从 Markdown 到 TopK chunks(图文讲清楚)
下面这张图展示了端到端链路,也基本对应 `code/main.py` 的执行顺序。
<div align="center">
<img src="images/10_1_1.webp" alt="端到端流程图:上传→解析/切分→向量化→Top-K 检索" width="100%" />
<p>图 10.1: 端到端流程(本 demo 只验收检索结果,不要求生成最终回答)</p>
</div>
为了避免“看完图还是不知道自己要做什么”,这里把图 10.1 的关键节点按顺序讲清楚(你可以边对照图边往下读):
**1)本地输入:Markdown 文档**
你可以直接用本专题提供的 `data/sample.md`。这份文件故意写得短:包含“排队规则”和“退款规则”,方便你用不同问题去验证检索是否命中。
**2Upload:上传到 dataset**
上传不是“把文本发过去就结束”,它的意义在于:服务端要把这份文档纳入某个 **dataset**(容器)里,后续切分出来的 chunks、embedding、索引都挂在这个容器下面。
**3Parse/Chunk:解析 + 切分**
这一步会把 Markdown 解析成可检索的文本结构,并按服务端策略切成多个 chunk。
> ⚠️ 图里标了一个常见失败点:如果你的 tenant 没有配置默认 embedding`embd_id` 为空或未授权),解析任务可能直接 FAIL。
**4Embedding:向量化**
每个 chunk 会被映射成向量(embedding)。这一步是向量检索的前提——没有向量,后面就谈不上“语义相似”。
**5)写入向量库/索引**
chunk + embedding 会写入向量索引(图里叫 Vector Store / Index)。
**6Retrieve TopK:检索并返回 chunks**
输入一个问题(question),服务端从索引里找出最相关的 K 个 chunk,并把这些原文片段返回给你。本 demo 的验收就看这里:**返回的 chunks 是否包含你期望的规则段落**。
---
到这里,你应该已经能把这条链路从头到尾“顺着说一遍”了:
> 文档上传 → 服务端解析/切分/向量化 → 写入索引 → 问题检索 → 返回 Top‑K chunks。
但很多初学者还有一个常见困惑:**这些名词到底对应什么对象?我拿到的结果到底是谁?**
所以下面我们换一个视角:不再看“流程”,而是看“对象之间的关系”。
---
再看图 10.2(对象关系)。这张图的目的只有一个:把“你上传的文件”和“检索返回的结果”彻底区分开。
很多同学第一次用 RAG 平台 SDK,会把这些概念混在一起。你只要记住:
- **dataset**:容器(装很多文档)
- **document**:你上传的那份文件
- **chunk**:文档切分出来的文本片段(检索返回的就是它)
<div align="center">
<img src="images/10_1_2.webp" alt="对象关系图:dataset-document-chunk-embedding 与 Top-K 返回" width="100%" />
<p>图 10.2: 对象关系与返回结构(检索返回的核心对象是 chunk</p>
</div>
图 10.2 里最容易忽略、但最关键的一点是:**检索返回的是 chunk,不是 document。**
- document 是“你上传的整份文件”
- chunk 是“切分后的片段”,它才是检索、重排、压缩、最终拼上下文的基本单位
所以你在终端里看到的 Top‑K 结果,应该是一段段原文片段,而不是整篇 Markdown。
> 💡 小白自检:我怎么判断“这段 chunk 就是我想要的那段”?
>
> 很简单:用你自己的语言把问题再复述一遍,然后在返回的 chunk 里找“能直接支撑答案的原文句子”。
> 例如你问“已发货未签收能不能退款”,chunk 里应当出现“已发货未签收:可申请退款,但需要承担退货运费”这一类关键句。
---
## 三、实现思路:从零写一版“最小检索脚本”(带代码块)
先给一个“最小骨架”(你可以把它当作伪代码,但它基本就是 `code/main.py` 的主干):
```python
rag = RAGFlow(api_key=..., base_url=...)
# 1) 创建 dataset(容器)
dataset = rag.create_dataset(name=...)
# 2) 上传文档(拿到 doc.id
doc = dataset.upload_documents([{...}])[0]
# 3) 解析/切分/向量化(失败大多发生在这里)
parse_results = dataset.parse_documents([doc.id])
# 4) 检索 Top-K chunks(验收点)
chunks = rag.retrieve(question=..., dataset_ids=[dataset.id], document_ids=[doc.id], page_size=top_k)
```
下面把每一步展开讲清楚(并配上代码片段)。
### 3.1 参数与配置:先让脚本可复现
先从命令行参数入手,理解脚本“能调什么”。`code/main.py` 里最常用的是这几个:
```python
parser.add_argument("--file", type=Path, required=True)
parser.add_argument("--question", type=str, required=True)
parser.add_argument("--top-k", type=int, default=DEFAULT_CONFIG.top_k)
parser.add_argument("--dataset-name", type=str, default=DEFAULT_CONFIG.dataset_name)
parser.add_argument("--base-url", type=str, default=DEFAULT_CONFIG.base_url)
parser.add_argument("--api-key", type=str, default=DEFAULT_CONFIG.api_key)
```
- `--file`:你要上传哪份 Markdown
- `--question`:你想验证的提问
- `--top-k`:返回多少个 chunk
- `--dataset-name`:本次创建/使用的数据集名字
- `--base-url/--api-key`PowerRAG 服务端地址与 SDK token
这几个参数足够让你完成“换文档、换问题、调 Top‑K、连不同服务端”这四类最常见实验。
> 💡 小白自检:为什么这里既支持命令行参数,又支持 `.env`?
>
> 因为这两种场景都很常见:
>
> - 你本地调试时,喜欢用 `.env` 固定住 base_url/api_key
> - 你改参数做实验时,喜欢命令行直接覆盖(不用反复改文件)
### 3.2 初始化 SDK:先连上再说
```python
from ragflow_sdk import RAGFlow
rag = RAGFlow(api_key=api_key, base_url=base_url)
```
这里没有花活:就是把请求的 base_url 和 token 配好。
### 3.3 创建 dataset:把文档放进“一个篮子里”
```python
dataset_kwargs = {"name": args.dataset_name}
if args.embedding_model:
dataset_kwargs["embedding_model"] = args.embedding_model
dataset = rag.create_dataset(**dataset_kwargs)
```
为什么要先有 dataset?因为“上传/解析/检索”都需要一个边界。
你不希望每次检索都在整个租户的所有文档里搜;你希望“只在这次实验的文档集合里搜”。
> 💡 小白自检:能不能不建 dataset,直接上传然后检索?
>
> 取决于平台能力。但在 PowerRAG/RAGFlow 这类系统里,dataset 是“组织边界”。
> 没有边界,检索要么全库搜(不可控),要么压根没有地方挂索引。
### 3.4 上传 document:得到 doc.id,后面都靠它
```python
docs = dataset.upload_documents([
{"display_name": display_name, "blob": blob}
])
doc = docs[0]
```
上传成功后,SDK 会返回一个 document 对象(至少包含 `doc.id`)。
后续的 parse 和 retrieve 都要用它来限定范围。
> 💡 小白自检:为什么要限定 `document_ids=[doc.id]`
>
> 因为你这次实验只关心“这份文档”的检索效果。
> 如果不限定,dataset 里有多份文档时,你可能会检索到别的文档的 chunk,导致结果看起来“跑偏”。
### 3.5 解析 / 切分 / 向量化:最容易踩坑的一步
```python
parse_results = dataset.parse_documents([doc.id])
print("Parse results:")
print(parse_results)
```
脚本会把 parse 的状态打印出来,并且做了一个很直接的判断:
```python
statuses = {r[1] for r in parse_results if isinstance(r, (list, tuple)) and len(r) >= 2}
if statuses and statuses != {"DONE"}:
raise SystemExit("Document parsing failed (status not DONE)...")
```
你可以把它理解为“验收关卡”:
- **DONE**:说明服务端已经把文档切成 chunk,并完成(或至少开始完成)向量化与索引写入
- **FAIL/其他状态**:先别着急改代码,优先排查 tenant 默认 embedding
> 经验:`Model(@None) not authorized` 基本就是在提示“默认 embedding 没配/没权限”。
> 💡 小白自检:为什么 embedding 配置会影响“解析(parse)”?
>
> 因为这里的 parse 往往不是“纯语法解析 Markdown”,而是一条“解析 → 切分 → 向量化 → 写索引”的流水线任务。
> embedding 不可用时,流水线中途失败,平台就会把整个任务标为 FAIL。
### 3.6 检索 Top‑K:你真正要验收的结果
```python
chunks = rag.retrieve(
question=args.question,
dataset_ids=[dataset.id],
document_ids=[doc.id],
page=1,
page_size=args.top_k,
similarity_threshold=args.similarity_threshold,
vector_similarity_weight=args.vector_similarity_weight,
top_k=args.candidate_k,
keyword=args.keyword,
)
```
这里有两个点值得你留意(也是很多人调参的入口):
- `page_size=args.top_k`:你最终想看多少条 chunk
- `similarity_threshold`:太高会过滤掉结果导致空,太低会混进无关段落
最后脚本会把每条 chunk 的内容预览打印出来:
```python
for i, c in enumerate(chunks, start=1):
content = _safe_get(c, "content", "")
print(f"{i:02d}. {content[:260]}")
```
你要做的“人工验收”也很简单:看看这几段文字是不是回答问题所需的那几段原文。
> 💡 小白自检:Top‑K 是不是越大越好?
>
> 不是。Top‑K 太大容易把无关 chunk 混进来;太小又可能漏掉关键段落。
> 教学 demo 里一般用 3~8 都够用。
---
### 3.7 先跑通一次(最短路径)
> ⚠️ 注意:解析/向量化依赖 embedding。如果你的 tenant 没有配置默认 embedding`embd_id` 为空或未授权),解析阶段会 FAIL。不要先怀疑 Python。
```bash
# 1) 安装依赖
cd Extra-chapter/PowerRAG-SDK-Text-QA/code
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
# 2) 配置 .env(在 code/ 目录下)
cp .env.example .env
# 3) 回到专题根目录运行(data/ 路径更直观)
cd ..
python code/main.py \
--file data/sample.md \
--question "已发货未签收的退款规则是什么?" \
--top-k 5 \
--cleanup
```
你会看到两段关键输出:
1. `Parse results`:解析/分块状态(期望 `DONE`
2. `Retrieved chunks`TopK chunks 的内容预览
---
---
## 四、经验总结与坑点(把时间花在对的地方)
很多时候问题不在“你写的 Python”,而在“服务端是不是已经把 embedding 产出来了”。
<div align="center">
<img src="images/10_1_3.webp" alt="简化时序图:上传→解析→写入索引→Top-K 检索→返回 chunks" width="100%" />
<p>图 10.3: code/main.py 与服务端 API 的交互顺序(简化版)</p>
</div>
如果你只记一条顺序,就记这句:
> **先上传 → 再解析(产出 chunk+embedding)→ 最后检索(返回 chunk**
很多“为什么检索不到”的问题,本质是解析还没成功,索引里根本没有向量。
---
### 4.1 Parse results 是 FAIL
优先检查 tenant 的默认 embedding`embd_id`)是否已配置且可用。典型错误:
- `Model(@None) not authorized`
- `Parse results: ... FAIL ...`
如果已经配置仍失败,直接看 task executor 日志最省时间:
```bash
docker exec powerrag-powerrag-1 sh -lc 'tail -n 200 /ragflow/logs/task_executor_* | tail -n 200'
```
### 4.2 401/403token 类型搞混
PowerRAG 常见会同时出现两类 token:
- Web 层 `AUTH`(用于 `/v1/*`
- SDK 的 `ragflow-...` token(用于 `/api/v1/*`,通常写在 `Authorization: Bearer <ragflow-...>`
如果你看到 401/403,先确认 token 类型和接口前缀是否匹配。
---
---
## 附录:用 API 配默认 embedding + 生成 ragflow token(重操作区)
> 这部分是“环境/账号/服务端配置”,放到附录,避免主线被淹没。
### A1. 用 API 配好 embedding(通用)
这一步需要一个 Web 层的 `AUTH``/v1/*` 使用),它和 SDK 的 `ragflow-...` key 不是一回事。
你可以把 embedding 配置写进 `.env`(见 `.env.example``EMB_*`),下面命令会读取 `EMB_FACTORY/EMB_MODEL/EMB_API_BASE/EMB_API_KEY`
#### A1.1 获取 `AUTH`(注册并从响应头拿 Authorization
PowerRAG 的 `/v1/user/register` 要求 password 先用服务端的 RSA public key 加密。最省事的方式是在容器内调用它自带的加密函数:
```bash
BASE_URL="http://127.0.0.1:9380"
ENC_PW="$(docker exec powerrag-powerrag-1 sh -lc 'python - <<"PY"\nfrom api.utils.crypt import crypt\nprint(crypt("powerrag"))\nPY')"
EMAIL="powerrag.demo.$(date +%s)@example.com"
AUTH="$(curl -sS -D - -o /dev/null -X POST "$BASE_URL/v1/user/register" \
-H 'Content-Type: application/json' \
-d "{\"nickname\":\"demo\",\"email\":\"$EMAIL\",\"password\":\"$ENC_PW\"}" \
| awk 'BEGIN{IGNORECASE=1} /^authorization:/{print $2}' | tr -d '\r')"
```
#### A1.2 绑定 embedding 的外部 API
> 注意:`max_tokens` 需要显式传,否则可能报数据库字段错误。
```bash
curl -sS -X POST "$BASE_URL/v1/llm/add_llm" \
-H "Authorization: $AUTH" \
-H 'Content-Type: application/json' \
-d '{
"llm_factory": "'"${EMB_FACTORY}"'",
"model_type": "embedding",
"llm_name": "'"${EMB_MODEL}"'",
"api_base": "'"${EMB_API_BASE}"'",
"api_key": "'"${EMB_API_KEY}"'",
"max_tokens": 8192
}'
```
#### A1.3 设置 tenant 默认 `embd_id`
```bash
TENANT_ID="$(curl -sS -H "Authorization: $AUTH" "$BASE_URL/v1/user/tenant_info" | python -c 'import sys,json; print(json.load(sys.stdin)["data"]["tenant_id"])')"
curl -sS -X POST "$BASE_URL/v1/user/set_tenant_info" \
-H "Authorization: $AUTH" \
-H 'Content-Type: application/json' \
-d "{\"tenant_id\":\"$TENANT_ID\",\"llm_id\":\"\",\"embd_id\":\"${EMB_MODEL}@${EMB_FACTORY}\",\"asr_id\":\"\",\"img2txt_id\":\"\"}"
```
### A2. 生成 SDK 的 `ragflow-...` api_key
SDK 接口在 `/api/v1/*`,它不认 `AUTH`,需要 `ragflow-...` 这种 token(放在 header`Authorization: Bearer <ragflow-...>`)。
`AUTH` 创建一个 SDK key
```bash
DIALOG_ID="$(python -c 'import uuid; print(uuid.uuid4().hex)')"
API_KEY="$(curl -sS -X POST "$BASE_URL/v1/api/new_token" \
-H "Authorization: $AUTH" \
-H 'Content-Type: application/json' \
-d "{\"dialog_id\":\"$DIALOG_ID\"}" \
| python -c 'import sys,json; print(json.load(sys.stdin)["data"]["token"])')"
echo "$API_KEY"
```