Files
QMDSearch/skills/qmdsearch-agent/references/api-examples.md
T
kplam 83eb8d0dd0 feat: 新增 QMDSearch Agent Skill 包与下载入口
- 新增 skills/qmdsearch-agent(SKILL.md + references/api-examples.md),文档化
  AI Agent 鉴权、文本/文件上传入库(异步轮询)与分层检索调用方式
- 打包 QMDSearch-Agent-Skill.zip 至 app/static/agent-skill/
- 后端新增 GET /agent-skill 免登录下载路由(随 ./app 卷挂载,restart 即生效)
- 前端 API 说明页新增「AI Agent Skill 下载」卡片,链接至 /agent-skill
2026-08-01 00:11:05 +08:00

171 lines
5.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# QMDSearch API 示例与 Python 客户端封装
> 基址占位符 `{BASE}` 替换为实际服务地址,例如 `http://localhost:8000` 或 `http://<nas-ip>:8000`。
## 一、curl 示例
### 登录
```bash
curl -s -X POST {BASE}/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"<password>"}'
```
### 文本入库(202 + task_id
```bash
curl -s -X POST {BASE}/api/v1/documents \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"text":"QMDSearch 是面向 AI Agent 的分层信息检索服务……","title":"QMDSearch 介绍","source":"notes"}'
```
### 文件上传(202 + task_id
```bash
curl -s -X POST {BASE}/api/v1/documents/upload \
-H "Authorization: Bearer <token>" \
-F "file=@document.pdf" \
-F "source=manual"
```
### 轮询任务状态
```bash
curl -s {BASE}/api/v1/documents/tasks/<task_id>
```
### 检索
```bash
curl -s -X POST {BASE}/api/v1/search \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"query":"如何配置 Redis 缓存","top_k":5,"summarize":false}'
```
## 二、Python 客户端封装
可直接在 Agent 工具代码里复用:
```python
import time
import requests
class QMDSearchClient:
"""QMDSearch 最小客户端:登录、入库(文本/文件)、轮询、检索。"""
def __init__(self, base_url: str, username: str, password: str,
poll_interval: float = 1.5, poll_timeout: float = 120.0):
self.base = base_url.rstrip("/")
self.username = username
self.password = password
self.poll_interval = poll_interval
self.poll_timeout = poll_timeout
self.token: str | None = None
# ---- 鉴权 ----
def login(self) -> str:
r = requests.post(
f"{self.base}/api/v1/auth/login",
json={"username": self.username, "password": self.password},
timeout=30,
)
r.raise_for_status()
body = r.json()
if body.get("code") != 0:
raise RuntimeError(f"登录失败: {body}")
self.token = body["data"]["token"]
return self.token
def _headers(self) -> dict:
if not self.token:
self.login()
return {"Authorization": f"Bearer {self.token}"}
def _ok(self, resp: requests.Response):
resp.raise_for_status()
body = resp.json()
if body.get("code") != 0:
raise RuntimeError(f"API 错误 code={body.get('code')} msg={body.get('message')}")
return body["data"]
# ---- 入库 ----
def ingest_text(self, text: str, title: str = "", source: str = "",
metadata: dict | None = None) -> str:
data = {"text": text}
if title:
data["title"] = title
if source:
data["source"] = source
if metadata:
data["metadata"] = {str(k): str(v) for k, v in metadata.items()}
resp = requests.post(f"{self.base}/api/v1/documents",
headers=self._headers(), json=data, timeout=30)
return self._ok(resp)["task_id"]
def upload_file(self, path: str, title: str = "", source: str = "",
metadata: dict | None = None) -> str:
files = {"file": open(path, "rb")}
data = {}
if title:
data["title"] = title
if source:
data["source"] = source
if metadata:
data["metadata"] = str({str(k): str(v) for k, v in metadata.items()})
resp = requests.post(f"{self.base}/api/v1/documents/upload",
headers=self._headers(), files=files, data=data, timeout=60)
return self._ok(resp)["task_id"]
# ---- 轮询 ----
def wait_task(self, task_id: str) -> dict:
deadline = time.time() + self.poll_timeout
while time.time() < deadline:
resp = requests.get(f"{self.base}/api/v1/documents/tasks/{task_id}", timeout=30)
data = self._ok(resp)
if data["status"] == "done":
return data["result"]
if data["status"] == "failed":
raise RuntimeError(f"入库失败: {data.get('error')}")
time.sleep(self.poll_interval)
raise TimeoutError(f"任务 {task_id} 轮询超时")
def ingest_text_wait(self, *args, **kwargs) -> dict:
return self.wait_task(self.ingest_text(*args, **kwargs))
def upload_file_wait(self, *args, **kwargs) -> dict:
return self.wait_task(self.upload_file(*args, **kwargs))
# ---- 检索 ----
def search(self, query: str, top_k: int | None = None,
summarize: bool = False) -> dict:
payload = {"query": query, "summarize": summarize}
if top_k is not None:
payload["top_k"] = top_k
resp = requests.post(f"{self.base}/api/v1/search",
headers=self._headers(), json=payload, timeout=60)
return self._ok(resp)
# 用法
if __name__ == "__main__":
client = QMDSearchClient("http://localhost:8000", "admin", "<password>")
client.login()
# 文本入库并等待完成
result = client.ingest_text_wait("这是一篇关于分层检索的笔记……", title="笔记")
print("document_id =", result["document_id"])
# 检索
hits = client.search("分层检索是什么", top_k=5)["hits"]
for h in hits:
print(f"[{h['score']:.3f}] {h['title']}: {h['text'][:80]}")
```
## 三、响应结构速记
- 文本 / 文件入库 `202``data = {task_id, status:"pending"[, saved_path]}`
- 任务 `done``data.result = {document_id, summary:{l1_summary,l2_outline,l3_content_outline,level}, category, chunks_count, tags, category_confidence, deduplicated}`
- 检索 → `data = {query, hits:[{text,doc_id,title,section_path,score,doc_summary}], routed_categories, fallback, extracted_info, summary?}`