2026-01-21 22:31:03 +01:00
|
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
|
"""
|
|
|
|
|
|
多轮对话测试脚本
|
|
|
|
|
|
测试对话引导 Agent 和回忆录整理功能
|
|
|
|
|
|
"""
|
|
|
|
|
|
import asyncio
|
|
|
|
|
|
import json
|
|
|
|
|
|
import uuid
|
|
|
|
|
|
import httpx
|
|
|
|
|
|
import websockets
|
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
|
|
|
|
|
|
|
# 配置
|
|
|
|
|
|
BASE_URL = "http://localhost:8000"
|
|
|
|
|
|
WS_URL = "ws://localhost:8000"
|
|
|
|
|
|
|
|
|
|
|
|
# 测试用户信息
|
|
|
|
|
|
TEST_PHONE = f"138{uuid.uuid4().hex[:8]}" # 随机手机号避免冲突
|
|
|
|
|
|
TEST_PASSWORD = "test123456"
|
|
|
|
|
|
TEST_NICKNAME = "测试用户"
|
|
|
|
|
|
|
|
|
|
|
|
# 模拟用户的多轮对话内容(关于童年和教育阶段)
|
|
|
|
|
|
CONVERSATION_MESSAGES = [
|
|
|
|
|
|
# 童年阶段
|
|
|
|
|
|
"我出生在南方一个小镇,小时候跟奶奶住在一起。",
|
|
|
|
|
|
"奶奶家有个小院子,夏天的时候我们经常坐在院子里乘凉,她给我讲故事。",
|
|
|
|
|
|
"那段时光真的很美好,我记得奶奶总是给我做红烧肉,那是我最爱吃的菜。",
|
|
|
|
|
|
"小时候最开心的事就是过年,可以放鞭炮,还能收到压岁钱。",
|
|
|
|
|
|
|
|
|
|
|
|
# 教育阶段
|
|
|
|
|
|
"后来我去城里上学了,那是我第一次离开家,心里特别害怕。",
|
|
|
|
|
|
"初中的时候遇到了一个很好的语文老师,她鼓励我多读书,对我影响很大。",
|
|
|
|
|
|
"高考那年压力特别大,但最后还是考上了理想的大学。",
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ConversationTester:
|
|
|
|
|
|
"""对话测试器"""
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
|
|
self.token = None
|
|
|
|
|
|
self.user_id = None
|
|
|
|
|
|
self.conversation_id = str(uuid.uuid4())
|
|
|
|
|
|
|
|
|
|
|
|
async def register_or_login(self):
|
|
|
|
|
|
"""注册或登录用户"""
|
|
|
|
|
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
|
|
|
|
# 先尝试注册
|
|
|
|
|
|
print(f"\n📝 注册用户: {TEST_PHONE}")
|
|
|
|
|
|
resp = await client.post(
|
|
|
|
|
|
f"{BASE_URL}/api/auth/register",
|
|
|
|
|
|
json={
|
|
|
|
|
|
"phone": TEST_PHONE,
|
|
|
|
|
|
"password": TEST_PASSWORD,
|
|
|
|
|
|
"nickname": TEST_NICKNAME
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if resp.status_code == 201:
|
|
|
|
|
|
data = resp.json()
|
|
|
|
|
|
self.token = data["access_token"]
|
|
|
|
|
|
print(f"✅ 注册成功!")
|
|
|
|
|
|
elif resp.status_code == 400 and "已被注册" in resp.text:
|
|
|
|
|
|
# 已注册,尝试登录
|
|
|
|
|
|
print(f"ℹ️ 用户已存在,尝试登录...")
|
|
|
|
|
|
resp = await client.post(
|
|
|
|
|
|
f"{BASE_URL}/api/auth/login",
|
|
|
|
|
|
json={
|
|
|
|
|
|
"phone": TEST_PHONE,
|
|
|
|
|
|
"password": TEST_PASSWORD
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
if resp.status_code == 200:
|
|
|
|
|
|
data = resp.json()
|
|
|
|
|
|
self.token = data["access_token"]
|
|
|
|
|
|
print(f"✅ 登录成功!")
|
|
|
|
|
|
else:
|
|
|
|
|
|
raise Exception(f"登录失败: {resp.text}")
|
|
|
|
|
|
else:
|
|
|
|
|
|
raise Exception(f"注册失败: {resp.text}")
|
|
|
|
|
|
|
|
|
|
|
|
print(f"🔑 Token: {self.token[:30]}...")
|
|
|
|
|
|
|
|
|
|
|
|
async def get_memoir_state(self):
|
|
|
|
|
|
"""获取回忆录状态"""
|
|
|
|
|
|
async with httpx.AsyncClient(timeout=60.0) as client:
|
|
|
|
|
|
resp = await client.get(
|
|
|
|
|
|
f"{BASE_URL}/api/memoir-state",
|
|
|
|
|
|
headers={"Authorization": f"Bearer {self.token}"}
|
|
|
|
|
|
)
|
|
|
|
|
|
if resp.status_code != 200:
|
|
|
|
|
|
print(f" ⚠️ 状态API返回 {resp.status_code}: {resp.text[:200]}")
|
|
|
|
|
|
return {"current_stage": "unknown", "covered_stages": [], "slots": {}}
|
|
|
|
|
|
return resp.json()
|
|
|
|
|
|
|
|
|
|
|
|
async def get_chapters(self):
|
|
|
|
|
|
"""获取章节列表"""
|
|
|
|
|
|
async with httpx.AsyncClient(timeout=60.0) as client:
|
|
|
|
|
|
resp = await client.get(
|
|
|
|
|
|
f"{BASE_URL}/api/chapters",
|
|
|
|
|
|
headers={"Authorization": f"Bearer {self.token}"}
|
|
|
|
|
|
)
|
|
|
|
|
|
if resp.status_code != 200:
|
|
|
|
|
|
print(f" ⚠️ 章节API返回 {resp.status_code}: {resp.text[:200]}")
|
|
|
|
|
|
return []
|
|
|
|
|
|
return resp.json()
|
|
|
|
|
|
|
|
|
|
|
|
async def get_book(self):
|
|
|
|
|
|
"""获取回忆录信息"""
|
|
|
|
|
|
async with httpx.AsyncClient(timeout=60.0) as client:
|
|
|
|
|
|
resp = await client.get(
|
|
|
|
|
|
f"{BASE_URL}/api/books/current",
|
|
|
|
|
|
headers={"Authorization": f"Bearer {self.token}"}
|
|
|
|
|
|
)
|
|
|
|
|
|
if resp.status_code != 200:
|
|
|
|
|
|
print(f" ⚠️ 回忆录API返回 {resp.status_code}: {resp.text[:200]}")
|
|
|
|
|
|
return {"message": "获取失败"}
|
|
|
|
|
|
return resp.json()
|
|
|
|
|
|
|
2026-01-21 23:37:00 +01:00
|
|
|
|
async def get_tasks_status(self):
|
|
|
|
|
|
"""获取任务状态"""
|
|
|
|
|
|
async with httpx.AsyncClient(timeout=60.0) as client:
|
|
|
|
|
|
resp = await client.get(
|
|
|
|
|
|
f"{BASE_URL}/api/tasks/status",
|
|
|
|
|
|
headers={"Authorization": f"Bearer {self.token}"}
|
|
|
|
|
|
)
|
|
|
|
|
|
if resp.status_code != 200:
|
|
|
|
|
|
return {"total": 0, "all_completed": True, "tasks": []}
|
|
|
|
|
|
return resp.json()
|
|
|
|
|
|
|
|
|
|
|
|
async def clear_tasks(self):
|
|
|
|
|
|
"""清除任务记录"""
|
|
|
|
|
|
async with httpx.AsyncClient(timeout=60.0) as client:
|
|
|
|
|
|
await client.delete(
|
|
|
|
|
|
f"{BASE_URL}/api/tasks/clear",
|
|
|
|
|
|
headers={"Authorization": f"Bearer {self.token}"}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-01-21 22:31:03 +01:00
|
|
|
|
async def run_conversation(self):
|
|
|
|
|
|
"""运行多轮对话"""
|
|
|
|
|
|
print(f"\n🔗 连接 WebSocket: {self.conversation_id}")
|
|
|
|
|
|
|
|
|
|
|
|
ws_url = f"{WS_URL}/ws/conversation/{self.conversation_id}?token={self.token}"
|
|
|
|
|
|
|
|
|
|
|
|
async with websockets.connect(ws_url) as ws:
|
|
|
|
|
|
# 接收连接确认
|
|
|
|
|
|
msg = await ws.recv()
|
|
|
|
|
|
data = json.loads(msg)
|
|
|
|
|
|
print(f"✅ 连接成功: {data['type']}")
|
|
|
|
|
|
|
|
|
|
|
|
# 多轮对话
|
|
|
|
|
|
for i, user_message in enumerate(CONVERSATION_MESSAGES, 1):
|
|
|
|
|
|
print(f"\n{'='*60}")
|
|
|
|
|
|
print(f"📤 第 {i} 轮对话")
|
|
|
|
|
|
print(f"{'='*60}")
|
|
|
|
|
|
print(f"👤 用户: {user_message}")
|
|
|
|
|
|
|
|
|
|
|
|
# 发送消息
|
|
|
|
|
|
await ws.send(json.dumps({
|
|
|
|
|
|
"type": "text",
|
|
|
|
|
|
"data": {"text": user_message}
|
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
|
|
# 接收 Agent 回复(可能是多条消息)
|
|
|
|
|
|
try:
|
|
|
|
|
|
while True:
|
|
|
|
|
|
msg = await asyncio.wait_for(ws.recv(), timeout=30)
|
|
|
|
|
|
data = json.loads(msg)
|
|
|
|
|
|
if data["type"] == "agent_response":
|
|
|
|
|
|
msg_data = data['data']
|
|
|
|
|
|
total = msg_data.get('total', 1)
|
|
|
|
|
|
index = msg_data.get('index', 0)
|
|
|
|
|
|
print(f"🤖 Agent: {msg_data['text']}")
|
|
|
|
|
|
# 如果是最后一条消息,退出循环
|
|
|
|
|
|
if index >= total - 1:
|
|
|
|
|
|
break
|
|
|
|
|
|
elif data["type"] == "error":
|
|
|
|
|
|
print(f"❌ 错误: {data['data']['message']}")
|
|
|
|
|
|
break
|
|
|
|
|
|
else:
|
|
|
|
|
|
break
|
|
|
|
|
|
except asyncio.TimeoutError:
|
|
|
|
|
|
print("⏰ 等待响应超时")
|
|
|
|
|
|
|
|
|
|
|
|
# 短暂等待,模拟真实对话节奏
|
|
|
|
|
|
await asyncio.sleep(1)
|
|
|
|
|
|
|
|
|
|
|
|
# 结束对话
|
|
|
|
|
|
print(f"\n{'='*60}")
|
|
|
|
|
|
print("📭 结束对话")
|
|
|
|
|
|
print(f"{'='*60}")
|
|
|
|
|
|
|
|
|
|
|
|
await ws.send(json.dumps({
|
|
|
|
|
|
"type": "end_conversation",
|
|
|
|
|
|
"conversation_id": self.conversation_id
|
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
# 结束时会触发 process_conversation_segments,可能需要更长时间
|
|
|
|
|
|
msg = await asyncio.wait_for(ws.recv(), timeout=60)
|
|
|
|
|
|
data = json.loads(msg)
|
|
|
|
|
|
if data['type'] == 'error':
|
|
|
|
|
|
print(f"❌ 结束对话错误: {data['data'].get('message', 'unknown')}")
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(f"✅ 对话结束: {data['type']}")
|
|
|
|
|
|
except asyncio.TimeoutError:
|
|
|
|
|
|
print("⏰ 等待结束确认超时(但后台处理可能仍在进行)")
|
|
|
|
|
|
|
2026-01-21 23:37:00 +01:00
|
|
|
|
async def wait_for_processing(self, max_wait_seconds: int = 300, check_interval: int = 3):
|
|
|
|
|
|
"""
|
|
|
|
|
|
等待后台处理完成
|
|
|
|
|
|
通过查询 Celery 任务状态来判断处理是否完成
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
max_wait_seconds: 最大等待时间(秒),默认 5 分钟
|
|
|
|
|
|
check_interval: 检查间隔(秒)
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
是否在超时前完成
|
|
|
|
|
|
"""
|
|
|
|
|
|
print(f"\n⏳ 等待后台任务完成(最多 {max_wait_seconds} 秒)...")
|
|
|
|
|
|
print(" 提示: 通过 Celery 任务状态 API 追踪任务进度")
|
|
|
|
|
|
|
|
|
|
|
|
start_time = asyncio.get_event_loop().time()
|
|
|
|
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
|
elapsed = asyncio.get_event_loop().time() - start_time
|
|
|
|
|
|
|
|
|
|
|
|
if elapsed >= max_wait_seconds:
|
|
|
|
|
|
print(f"\n⚠️ 已等待 {max_wait_seconds} 秒,超时退出")
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
# 检查任务状态
|
|
|
|
|
|
tasks_status = await self.get_tasks_status()
|
|
|
|
|
|
total = tasks_status.get("total", 0)
|
|
|
|
|
|
pending = tasks_status.get("pending", 0)
|
|
|
|
|
|
running = tasks_status.get("running", 0)
|
|
|
|
|
|
success = tasks_status.get("success", 0)
|
|
|
|
|
|
failure = tasks_status.get("failure", 0)
|
|
|
|
|
|
all_completed = tasks_status.get("all_completed", False)
|
|
|
|
|
|
|
|
|
|
|
|
# 同时检查章节内容
|
|
|
|
|
|
chapters = await self.get_chapters()
|
|
|
|
|
|
chapter_count = len(chapters)
|
|
|
|
|
|
total_content_length = sum(len(ch.get('content', '')) for ch in chapters)
|
|
|
|
|
|
|
|
|
|
|
|
status_str = f"📊 总:{total} 等待:{pending} 运行:{running} 成功:{success} 失败:{failure}"
|
|
|
|
|
|
content_str = f"📚 章节:{chapter_count} 内容:{total_content_length}字符"
|
|
|
|
|
|
print(f" [{int(elapsed):3d}s] {status_str} | {content_str}")
|
|
|
|
|
|
|
|
|
|
|
|
# 判断是否完成:
|
|
|
|
|
|
# 1. 有任务且全部完成
|
|
|
|
|
|
# 2. 或者没有任务但有章节内容(兼容旧逻辑)
|
|
|
|
|
|
if total > 0 and all_completed:
|
|
|
|
|
|
print(f"\n✅ 所有任务已完成!共 {total} 个任务,等待 {int(elapsed)} 秒")
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
# 如果没有任务记录,等待一会儿任务提交
|
|
|
|
|
|
if total == 0 and elapsed < 15:
|
|
|
|
|
|
await asyncio.sleep(check_interval)
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
# 如果长时间没有任务但有内容,也认为完成
|
|
|
|
|
|
if total == 0 and chapter_count > 0 and elapsed > 30:
|
|
|
|
|
|
print(f"\n✅ 无待处理任务,已有 {chapter_count} 个章节。等待 {int(elapsed)} 秒")
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
await asyncio.sleep(check_interval)
|
|
|
|
|
|
|
2026-01-21 22:31:03 +01:00
|
|
|
|
async def check_results(self):
|
|
|
|
|
|
"""检查回忆录生成结果"""
|
|
|
|
|
|
print(f"\n{'='*60}")
|
|
|
|
|
|
print("📊 检查结果")
|
|
|
|
|
|
print(f"{'='*60}")
|
|
|
|
|
|
|
2026-01-21 23:37:00 +01:00
|
|
|
|
# 等待后台处理完成(使用智能轮询)
|
|
|
|
|
|
await self.wait_for_processing(max_wait_seconds=180, check_interval=5)
|
2026-01-21 22:31:03 +01:00
|
|
|
|
|
|
|
|
|
|
# 获取回忆录状态
|
|
|
|
|
|
print("\n📋 回忆录状态:")
|
|
|
|
|
|
state = await self.get_memoir_state()
|
|
|
|
|
|
print(f" 当前阶段: {state.get('current_stage', 'N/A')}")
|
|
|
|
|
|
print(f" 已完成阶段: {state.get('covered_stages', [])}")
|
|
|
|
|
|
|
|
|
|
|
|
# 显示已填充的 slots
|
|
|
|
|
|
slots = state.get('slots', {})
|
|
|
|
|
|
for stage, stage_slots in slots.items():
|
|
|
|
|
|
filled = [k for k, v in stage_slots.items() if v.get('snippet')]
|
|
|
|
|
|
if filled:
|
|
|
|
|
|
print(f" {stage} 已填充: {filled}")
|
|
|
|
|
|
for slot_name in filled:
|
|
|
|
|
|
snippet = stage_slots[slot_name].get('snippet', '')
|
|
|
|
|
|
if snippet:
|
|
|
|
|
|
print(f" - {slot_name}: {snippet[:50]}...")
|
|
|
|
|
|
|
|
|
|
|
|
# 获取章节
|
|
|
|
|
|
print("\n📚 生成的章节:")
|
|
|
|
|
|
chapters = await self.get_chapters()
|
|
|
|
|
|
if chapters:
|
|
|
|
|
|
for ch in chapters:
|
|
|
|
|
|
is_new = "🆕" if ch.get("is_new") else ""
|
2026-01-21 23:37:00 +01:00
|
|
|
|
content_len = len(ch.get('content', ''))
|
|
|
|
|
|
print(f" {is_new} [{ch.get('category', 'N/A')}] {ch.get('title', 'N/A')} ({content_len} 字符)")
|
2026-01-21 22:31:03 +01:00
|
|
|
|
else:
|
|
|
|
|
|
print(" (暂无章节)")
|
|
|
|
|
|
|
|
|
|
|
|
# 获取回忆录
|
|
|
|
|
|
print("\n📖 回忆录信息:")
|
|
|
|
|
|
book = await self.get_book()
|
|
|
|
|
|
if "message" not in book:
|
|
|
|
|
|
print(f" 标题: {book.get('title', 'N/A')}")
|
|
|
|
|
|
print(f" 总字数: {book.get('total_words', 0)}")
|
|
|
|
|
|
print(f" 有更新: {'是' if book.get('has_update') else '否'}")
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(f" {book.get('message', 'N/A')}")
|
|
|
|
|
|
|
|
|
|
|
|
# 显示回忆录完整内容
|
|
|
|
|
|
if chapters:
|
|
|
|
|
|
print(f"\n{'='*60}")
|
|
|
|
|
|
print("📜 回忆录完整内容")
|
|
|
|
|
|
print(f"{'='*60}")
|
|
|
|
|
|
for ch in chapters:
|
|
|
|
|
|
category = ch.get('category', 'N/A')
|
|
|
|
|
|
title = ch.get('title', '未命名章节')
|
|
|
|
|
|
content = ch.get('content', '')
|
|
|
|
|
|
|
|
|
|
|
|
print(f"\n{'─'*60}")
|
2026-01-21 23:37:00 +01:00
|
|
|
|
print(f"【{title}】({category})")
|
2026-01-21 22:31:03 +01:00
|
|
|
|
print(f"{'─'*60}")
|
|
|
|
|
|
if content:
|
|
|
|
|
|
print(content)
|
|
|
|
|
|
else:
|
|
|
|
|
|
print("(暂无内容)")
|
|
|
|
|
|
print(f"\n{'='*60}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def main():
|
|
|
|
|
|
"""主函数"""
|
|
|
|
|
|
print("=" * 60)
|
|
|
|
|
|
print("🎭 Life Echo 多轮对话测试")
|
|
|
|
|
|
print(f"⏰ 开始时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
|
|
|
|
|
print("=" * 60)
|
|
|
|
|
|
|
|
|
|
|
|
tester = ConversationTester()
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
# 1. 注册/登录
|
|
|
|
|
|
await tester.register_or_login()
|
|
|
|
|
|
|
2026-01-21 23:37:00 +01:00
|
|
|
|
# 2. 清除旧的任务记录
|
|
|
|
|
|
await tester.clear_tasks()
|
|
|
|
|
|
print("\n🧹 已清除旧的任务记录")
|
|
|
|
|
|
|
|
|
|
|
|
# 3. 查看初始状态
|
2026-01-21 22:31:03 +01:00
|
|
|
|
print("\n📋 初始回忆录状态:")
|
|
|
|
|
|
state = await tester.get_memoir_state()
|
|
|
|
|
|
print(f" 当前阶段: {state.get('current_stage', 'N/A')}")
|
|
|
|
|
|
|
2026-01-21 23:37:00 +01:00
|
|
|
|
# 4. 运行多轮对话
|
2026-01-21 22:31:03 +01:00
|
|
|
|
await tester.run_conversation()
|
|
|
|
|
|
|
2026-01-21 23:37:00 +01:00
|
|
|
|
# 5. 检查结果
|
2026-01-21 22:31:03 +01:00
|
|
|
|
await tester.check_results()
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"\n❌ 测试失败: {e}")
|
|
|
|
|
|
import traceback
|
|
|
|
|
|
traceback.print_exc()
|
|
|
|
|
|
|
|
|
|
|
|
print("\n" + "=" * 60)
|
|
|
|
|
|
print(f"⏰ 结束时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
|
|
|
|
|
print("=" * 60)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
|
asyncio.run(main())
|