- 移除 MinIOBackend 和复合存储后端,统一使用 StateBackend - 依赖 LangGraph checkpointer 自动恢复 state(attachments, files) - 前端适配 StateBackend 字典格式文件结构 - 优化 @提及 文件搜索逻辑,支持路径匹配
289 lines
9.4 KiB
Python
289 lines
9.4 KiB
Python
"""
|
||
测试附件上传和 agent state 获取的 API 脚本
|
||
|
||
使用方式:
|
||
cd /home/zwj/workspace/Yuxi-Know
|
||
docker compose exec api uv run python test/api/test_attachment_and_agent_state.py
|
||
|
||
或者本地运行:
|
||
python test/api/test_attachment_and_agent_state.py
|
||
"""
|
||
|
||
import asyncio
|
||
import contextlib
|
||
import os
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
# 添加项目根目录到 Python 路径
|
||
PROJECT_ROOT = Path(__file__).parent.parent.parent
|
||
sys.path.insert(0, str(PROJECT_ROOT))
|
||
|
||
# 加载 .env 文件
|
||
from dotenv import load_dotenv
|
||
load_dotenv(PROJECT_ROOT / ".env")
|
||
|
||
import httpx
|
||
import uuid
|
||
|
||
|
||
# API 配置
|
||
API_BASE_URL = os.getenv("API_BASE_URL", "http://localhost:5050")
|
||
|
||
# 测试账户配置
|
||
USERNAME = os.getenv("YUXI_SUPER_ADMIN_NAME", "zwj")
|
||
PASSWORD = os.getenv("YUXI_SUPER_ADMIN_PASSWORD", "zwj12138")
|
||
|
||
# 默认 Agent ID (需要根据实际情况修改,使用类名)
|
||
DEFAULT_AGENT_ID = "ChatbotAgent"
|
||
|
||
|
||
class APITester:
|
||
def __init__(self, base_url: str, username: str, password: str):
|
||
self.base_url = base_url
|
||
self.username = username
|
||
self.password = password
|
||
self.token: str | None = None
|
||
self.user_id: str | None = None
|
||
self.headers: dict | None = None
|
||
|
||
@contextlib.asynccontextmanager
|
||
async def _client(self, timeout: float = 30.0):
|
||
"""获取 HTTP 客户端"""
|
||
client = httpx.AsyncClient(timeout=timeout)
|
||
try:
|
||
yield client
|
||
finally:
|
||
await client.aclose()
|
||
|
||
async def login(self) -> bool:
|
||
"""登录获取 token"""
|
||
print(f"\n{'='*60}")
|
||
print(f"1. 正在登录: {self.username}")
|
||
print(f"{'='*60}")
|
||
|
||
async with self._client() as client:
|
||
response = await client.post(
|
||
f"{self.base_url}/api/auth/token",
|
||
data={"username": self.username, "password": self.password},
|
||
)
|
||
|
||
if response.status_code == 200:
|
||
data = response.json()
|
||
self.token = data.get("access_token")
|
||
self.user_id = str(data.get("user_id"))
|
||
self.headers = {"Authorization": f"Bearer {self.token}"}
|
||
print(f" ✓ 登录成功! user_id: {self.user_id}")
|
||
print(f" ✓ Token: {self.token[:50]}...")
|
||
return True
|
||
else:
|
||
print(f" ✗ 登录失败: {response.status_code} - {response.text}")
|
||
return False
|
||
|
||
async def create_thread(self, agent_id: str) -> str:
|
||
"""创建对话线程"""
|
||
print(f"\n{'='*60}")
|
||
print(f"2. 创建对话线程 (agent_id: {agent_id})")
|
||
print(f"{'='*60}")
|
||
|
||
async with self._client() as client:
|
||
response = await client.post(
|
||
f"{self.base_url}/api/chat/thread",
|
||
json={"agent_id": agent_id, "title": "API 测试对话"},
|
||
headers=self.headers,
|
||
)
|
||
|
||
if response.status_code == 200:
|
||
thread = response.json()
|
||
thread_id = thread.get("id")
|
||
print(f" ✓ 创建成功! thread_id: {thread_id}")
|
||
return thread_id
|
||
else:
|
||
print(f" ✗ 创建失败: {response.status_code} - {response.text}")
|
||
return ""
|
||
|
||
async def upload_attachment(self, thread_id: str, file_path: str) -> dict | None:
|
||
"""上传附件"""
|
||
print(f"\n{'='*60}")
|
||
print(f"3. 上传附件: {file_path}")
|
||
print(f"{'='*60}")
|
||
|
||
if not os.path.exists(file_path):
|
||
print(f" ✗ 文件不存在: {file_path}")
|
||
return None
|
||
|
||
async with self._client(timeout=60.0) as client:
|
||
with open(file_path, "rb") as f:
|
||
files = {"file": (os.path.basename(file_path), f)}
|
||
response = await client.post(
|
||
f"{self.base_url}/api/chat/thread/{thread_id}/attachments",
|
||
files=files,
|
||
headers=self.headers,
|
||
)
|
||
|
||
if response.status_code == 200:
|
||
attachment = response.json()
|
||
print(f" ✓ 上传成功!")
|
||
print(f" file_id: {attachment.get('file_id')}")
|
||
print(f" file_name: {attachment.get('file_name')}")
|
||
print(f" status: {attachment.get('status')}")
|
||
return attachment
|
||
else:
|
||
print(f" ✗ 上传失败: {response.status_code} - {response.text}")
|
||
return None
|
||
|
||
async def list_attachments(self, thread_id: str) -> list[dict]:
|
||
"""列出附件"""
|
||
print(f"\n{'='*60}")
|
||
print(f"4. 列出附件 (thread_id: {thread_id})")
|
||
print(f"{'='*60}")
|
||
|
||
async with self._client() as client:
|
||
response = await client.get(
|
||
f"{self.base_url}/api/chat/thread/{thread_id}/attachments",
|
||
headers=self.headers,
|
||
)
|
||
|
||
if response.status_code == 200:
|
||
data = response.json()
|
||
attachments = data.get("attachments", [])
|
||
print(f" ✓ 获取到 {len(attachments)} 个附件:")
|
||
for att in attachments:
|
||
print(f" - {att.get('file_name')}: {att.get('status')}")
|
||
return attachments
|
||
else:
|
||
print(f" ✗ 获取失败: {response.status_code} - {response.text}")
|
||
return []
|
||
|
||
async def get_agent_state(self, agent_id: str, thread_id: str) -> dict | None:
|
||
"""获取 agent state"""
|
||
print(f"\n{'='*60}")
|
||
print(f"5. 获取 Agent State (agent_id: {agent_id}, thread_id: {thread_id})")
|
||
print(f"{'='*60}")
|
||
|
||
async with self._client() as client:
|
||
response = await client.get(
|
||
f"{self.base_url}/api/chat/agent/{agent_id}/state",
|
||
params={"thread_id": thread_id},
|
||
headers=self.headers,
|
||
)
|
||
|
||
if response.status_code == 200:
|
||
state = response.json()
|
||
agent_state = state.get("agent_state", {})
|
||
print(f" ✓ 获取成功!")
|
||
print(f" files: {len(agent_state.get('files', {}))} 个")
|
||
print(f" todos: {len(agent_state.get('todos', []))} 个")
|
||
if agent_state.get("files"):
|
||
print(f" 文件列表:")
|
||
for path in agent_state["files"]:
|
||
file_info = agent_state["files"][path]
|
||
print(f" - {path}: {len(file_info.get('content', []))} 行")
|
||
return state
|
||
else:
|
||
print(f" ✗ 获取失败: {response.status_code} - {response.text}")
|
||
return None
|
||
|
||
async def send_chat_message(self, agent_id: str, thread_id: str, query: str) -> bool:
|
||
"""发送聊天消息(流式)"""
|
||
print(f"\n{'='*60}")
|
||
print(f"6. 发送聊天消息")
|
||
print(f"{'='*60}")
|
||
print(f" Query: {query}")
|
||
print(f" Thread ID: {thread_id}")
|
||
|
||
async with self._client(timeout=120.0) as client:
|
||
async with client.stream(
|
||
"POST",
|
||
f"{self.base_url}/api/chat/agent/{agent_id}",
|
||
json={"query": query, "config": {"thread_id": thread_id}},
|
||
headers=self.headers,
|
||
) as response:
|
||
print(f"\n 响应状态: {response.status_code}")
|
||
print(f" 响应内容:")
|
||
async for chunk in response.aiter_lines():
|
||
if chunk:
|
||
print(f" {chunk[:150]}...")
|
||
|
||
return response.status_code == 200
|
||
|
||
|
||
async def main():
|
||
"""主测试流程"""
|
||
print("\n" + "=" * 60)
|
||
print(" 附件上传与 Agent State API 测试")
|
||
print("=" * 60)
|
||
print(f"\nAPI 地址: {API_BASE_URL}")
|
||
print(f"测试账户: {USERNAME}")
|
||
|
||
tester = APITester(API_BASE_URL, USERNAME, PASSWORD)
|
||
|
||
# 1. 登录
|
||
if not await tester.login():
|
||
print("\n!!! 登录失败,测试终止 !!!")
|
||
return
|
||
|
||
# 2. 创建线程(使用默认 agent_id)
|
||
agent_id = DEFAULT_AGENT_ID
|
||
thread_id = await tester.create_thread(agent_id)
|
||
if not thread_id:
|
||
print("\n!!! 创建线程失败,测试终止 !!!")
|
||
return
|
||
|
||
# 3. 创建测试文件
|
||
test_content = """# 测试文档
|
||
|
||
这是一个用于 API 测试的 Markdown 文件。
|
||
|
||
## 主要内容
|
||
|
||
- 第一点
|
||
- 第二点
|
||
- 第三点
|
||
|
||
```python
|
||
def hello():
|
||
print("Hello, World!")
|
||
```
|
||
"""
|
||
test_file_path = f"/tmp/test_attachment_{uuid.uuid4().hex[:8]}.md"
|
||
with open(test_file_path, "w", encoding="utf-8") as f:
|
||
f.write(test_content)
|
||
print(f"\n 测试文件已创建: {test_file_path}")
|
||
|
||
# 4. 上传附件
|
||
attachment = await tester.upload_attachment(thread_id, test_file_path)
|
||
|
||
# 5. 列出附件
|
||
await tester.list_attachments(thread_id)
|
||
|
||
# 6. 获取 agent state (验证附件是否在 state 中)
|
||
if attachment:
|
||
print("\n 等待后端处理...")
|
||
await asyncio.sleep(2)
|
||
await tester.get_agent_state(agent_id, thread_id)
|
||
|
||
# 7. 发送聊天消息测试
|
||
await tester.send_chat_message(
|
||
agent_id,
|
||
thread_id,
|
||
"你好,请简单介绍一下你自己。"
|
||
)
|
||
|
||
# 8. 再次获取 agent state (验证 todos 等状态)
|
||
await asyncio.sleep(1)
|
||
await tester.get_agent_state(agent_id, thread_id)
|
||
|
||
# 清理测试文件
|
||
if os.path.exists(test_file_path):
|
||
os.remove(test_file_path)
|
||
print(f"\n 测试文件已清理: {test_file_path}")
|
||
|
||
print(f"\n{'='*60}")
|
||
print(" 测试完成!")
|
||
print(f"{'='*60}\n")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
asyncio.run(main())
|