fix: delete agent 500 error + dynamic personality + deployment guide
- Fix delete agent 500: clean up FK records (agent_llm_logs, permissions, schedules, executions, team_members) and unbind goals/tasks before delete - Remove hardcoded personality templates in Android, replace with dynamic system prompt generation from name + description - Set promptSectionsEnabled=false to bypass PromptComposer for personality - Add Tencent Cloud Linux deployment guide (Docker Compose) - Accumulated backend service updates, frontend UI fixes, Android app changes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
94
test/test_branch.py
Normal file
94
test/test_branch.py
Normal file
@@ -0,0 +1,94 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""对话分支 API 端到端测试"""
|
||||
import sys, os, io, json, time, requests
|
||||
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
|
||||
|
||||
BASE = "http://localhost:8038/api/v1"
|
||||
|
||||
def login():
|
||||
r = requests.post(f"{BASE}/auth/login", data={"username": "admin", "password": "123456"})
|
||||
r.raise_for_status()
|
||||
return r.json()["access_token"]
|
||||
|
||||
def test_branching():
|
||||
token = login()
|
||||
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
||||
print("=== 对话分支 API 测试 ===\n")
|
||||
|
||||
# Step 1: Create a conversation
|
||||
print("1. 创建对话…")
|
||||
r1 = requests.post(f"{BASE}/agent-chat/bare", headers=headers,
|
||||
json={"message": "你好,我叫小明,是一名Python程序员"}, timeout=120)
|
||||
r1.raise_for_status()
|
||||
d1 = r1.json()
|
||||
sid = d1["session_id"]
|
||||
print(f" 会话 ID: {sid}")
|
||||
print(f" 回复: {d1['content'][:100]}…\n")
|
||||
|
||||
# Step 2: Create a branch
|
||||
print("2. 创建分支…")
|
||||
r2 = requests.post(f"{BASE}/agent-chat/branches", headers=headers,
|
||||
json={"session_id": sid, "title": "测试分支"}, timeout=30)
|
||||
r2.raise_for_status()
|
||||
d2 = r2.json()
|
||||
bid = d2["id"]
|
||||
print(f" 分支 ID: {bid[:16]}…")
|
||||
print(f" 标题: {d2['title']}")
|
||||
print(f" 父会话: {d2['parent_session_id']}")
|
||||
print(f" 分支会话: {d2['branch_session_id']}")
|
||||
print(f" 消息数: {d2['message_count']}")
|
||||
print(f" 首条消息: {d2.get('first_user_message', 'N/A')[:80]}\n")
|
||||
|
||||
# Step 3: List branches
|
||||
print("3. 列出分支…")
|
||||
r3 = requests.get(f"{BASE}/agent-chat/branches", headers=headers, timeout=10)
|
||||
r3.raise_for_status()
|
||||
d3 = r3.json()
|
||||
print(f" 共 {d3['total']} 个分支:")
|
||||
for b in d3["branches"]:
|
||||
print(f" - {b['id'][:16]}… {b['title']} ({b['message_count']} 条消息)")
|
||||
print()
|
||||
|
||||
# Step 4: Get branch details
|
||||
print("4. 获取分支详情…")
|
||||
r4 = requests.get(f"{BASE}/agent-chat/branches/{bid}", headers=headers, timeout=10)
|
||||
r4.raise_for_status()
|
||||
d4 = r4.json()
|
||||
msgs = d4.get("messages", [])
|
||||
print(f" 完整消息: {len(msgs)} 条")
|
||||
for m in msgs[:5]:
|
||||
role = m.get("role", "?")
|
||||
content = str(m.get("content", ""))[:80]
|
||||
print(f" [{role}] {content}")
|
||||
|
||||
# Step 5: Resume from branch
|
||||
print("\n5. 从分支恢复对话…")
|
||||
r5 = requests.post(f"{BASE}/agent-chat/branches/{bid}/resume", headers=headers,
|
||||
json={"message": "我叫小红,不叫小明,请更正你的记忆"}, timeout=120)
|
||||
r5.raise_for_status()
|
||||
d5 = r5.json()
|
||||
print(f" 会话 ID: {d5['session_id']}")
|
||||
print(f" 回复: {d5['content'][:200]}…")
|
||||
print(f" 迭代: {d5['iterations_used']}, 工具调用: {d5['tool_calls_made']}\n")
|
||||
|
||||
# Step 6: Delete branch
|
||||
print("6. 删除分支…")
|
||||
r6 = requests.delete(f"{BASE}/agent-chat/branches/{bid}", headers=headers, timeout=10)
|
||||
r6.raise_for_status()
|
||||
d6 = r6.json()
|
||||
print(f" 结果: {d6['message']}\n")
|
||||
|
||||
# Verify deletion
|
||||
r7 = requests.get(f"{BASE}/agent-chat/branches", headers=headers, timeout=10)
|
||||
d7 = r7.json()
|
||||
remaining_ids = [b["id"] for b in d7["branches"]]
|
||||
if bid not in remaining_ids:
|
||||
print("✓ 分支删除验证通过")
|
||||
else:
|
||||
print("✗ 分支删除验证失败")
|
||||
|
||||
print("\n=== 全部测试通过 ===")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_branching()
|
||||
245
test/test_compaction.py
Normal file
245
test/test_compaction.py
Normal file
@@ -0,0 +1,245 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""对话压缩引擎集成测试"""
|
||||
import sys, os, io
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "backend"))
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
|
||||
|
||||
import asyncio
|
||||
from app.core.token_counter import TokenCounter, is_context_length_error, get_model_context_window
|
||||
from app.core.compaction_config import CompactionConfig
|
||||
from app.core.compaction import CompactionEngine, CompactionStrategy
|
||||
|
||||
# 生成模拟长对话消息
|
||||
def make_long_messages(rounds=20, tool_result_size=2000):
|
||||
"""生成模拟多轮对话,每轮包含 tool_call + 大工具结果"""
|
||||
messages = [
|
||||
{"role": "system", "content": "你是一个有用的AI助手。" + "X" * 500},
|
||||
]
|
||||
for i in range(rounds):
|
||||
messages.append({"role": "user", "content": f"请帮我完成第{i+1}个任务。"})
|
||||
messages.append({
|
||||
"role": "assistant",
|
||||
"content": f"好的,让我来执行第{i+1}个任务。",
|
||||
"tool_calls": [{"id": f"call_{i}", "type": "function", "function": {"name": "file_read", "arguments": '{"path":"/test"}'}}]
|
||||
})
|
||||
messages.append({
|
||||
"role": "tool",
|
||||
"tool_call_id": f"call_{i}",
|
||||
"name": "file_read",
|
||||
"content": f"[文件内容] 这是第{i+1}次文件读取的结果。" + "A" * tool_result_size,
|
||||
})
|
||||
messages.append({
|
||||
"role": "assistant",
|
||||
"content": f"第{i+1}个任务完成。结果是:发现了一些重要信息。" + "B" * 200,
|
||||
})
|
||||
return messages
|
||||
|
||||
async def test_token_counting():
|
||||
"""测试 token 计数器"""
|
||||
print("=" * 60)
|
||||
print("测试 1: Token 计数器")
|
||||
tc = TokenCounter(model="deepseek-v4-flash")
|
||||
|
||||
text_en = "Hello world " * 100
|
||||
tokens_en = tc.count(text_en)
|
||||
print(f" 英文 200 词: ~{tokens_en} tokens")
|
||||
|
||||
text_zh = "你好世界" * 100
|
||||
tokens_zh = tc.count(text_zh)
|
||||
print(f" 中文 400 字: ~{tokens_zh} tokens")
|
||||
|
||||
msgs = [
|
||||
{"role": "system", "content": "You are a helpful assistant."},
|
||||
{"role": "user", "content": "Hello!"},
|
||||
{"role": "assistant", "content": "Hi there!"},
|
||||
]
|
||||
total = tc.count_messages(msgs)
|
||||
print(f" 3条简单消息: ~{total} tokens")
|
||||
|
||||
print(" [OK] Token 计数器工作正常")
|
||||
return tc
|
||||
|
||||
async def test_micro_compact():
|
||||
"""测试 MicroCompact (Tier 1)"""
|
||||
print("\n" + "=" * 60)
|
||||
print("测试 2: MicroCompact (工具结果打桩)")
|
||||
|
||||
config = CompactionConfig(
|
||||
enabled=True,
|
||||
micro_compact_enabled=True,
|
||||
micro_compact_threshold=0.10, # 最低阈值
|
||||
full_compact_threshold=0.90, # 高阈值,让 MicroCompact 先触发
|
||||
compact_older_than_rounds=5, # 超过5轮的工具结果可压缩
|
||||
min_preserve_messages=4,
|
||||
context_window_override=10000, # 小窗口强制高占用比
|
||||
output_reserve_tokens=512,
|
||||
)
|
||||
engine = CompactionEngine(config=config, model="deepseek-v4-flash")
|
||||
|
||||
# 生成20轮对话,每轮有工具调用
|
||||
messages = make_long_messages(rounds=20, tool_result_size=500)
|
||||
tokens_before = engine.token_counter.count_messages(messages)
|
||||
print(f" 消息数: {len(messages)}, tokens: {tokens_before}")
|
||||
|
||||
result = await engine.maybe_compact(messages)
|
||||
|
||||
print(f" 压缩策略: {result.strategy.value}")
|
||||
print(f" Tokens: {result.tokens_before} -> {result.tokens_after}")
|
||||
print(f" 节省: {result.tokens_saved} tokens")
|
||||
print(f" 详情: {result.details}")
|
||||
|
||||
# 验证工具结果被替换
|
||||
stubbed = sum(1 for m in result.messages if m.get("content") == "[Tool result compacted]")
|
||||
print(f" 被压缩的工具结果数: {stubbed}")
|
||||
|
||||
if result.strategy == CompactionStrategy.MICRO and stubbed > 0:
|
||||
print(" [OK] MicroCompact 工作正常")
|
||||
else:
|
||||
print(f" [INFO] 策略={result.strategy}, stubbed={stubbed}")
|
||||
|
||||
return engine
|
||||
|
||||
async def test_full_compact():
|
||||
"""测试 FullCompact (Tier 2) — 无 LLM 客户端的 fallback"""
|
||||
print("\n" + "=" * 60)
|
||||
print("测试 3: FullCompact (LLM 摘要替换)")
|
||||
|
||||
config = CompactionConfig(
|
||||
enabled=True,
|
||||
micro_compact_enabled=False,
|
||||
full_compact_enabled=True,
|
||||
full_compact_threshold=0.10, # 最低阈值
|
||||
min_preserve_messages=4,
|
||||
context_window_override=10000,
|
||||
output_reserve_tokens=512,
|
||||
)
|
||||
engine = CompactionEngine(config=config, model="deepseek-v4-flash")
|
||||
|
||||
# 生成大量消息
|
||||
messages = make_long_messages(rounds=30, tool_result_size=200)
|
||||
tokens_before = engine.token_counter.count_messages(messages)
|
||||
print(f" 消息数: {len(messages)}, tokens: {tokens_before}")
|
||||
|
||||
result = await engine.maybe_compact(messages)
|
||||
|
||||
print(f" 压缩策略: {result.strategy.value}")
|
||||
print(f" Tokens: {result.tokens_before} -> {result.tokens_after}")
|
||||
print(f" 节省: {result.tokens_saved} tokens")
|
||||
print(f" 详情: {result.details}")
|
||||
|
||||
# 验证 structure: system + compact_boundary + recent
|
||||
if result.strategy == CompactionStrategy.FULL:
|
||||
# 查找 compact_boundary
|
||||
boundaries = [m for m in result.messages if "对话上下文摘要" in str(m.get("content", ""))]
|
||||
if boundaries:
|
||||
print(f" 摘要边界消息数: {len(boundaries)}")
|
||||
print(f" 摘要长度: {len(boundaries[0]['content'])} 字符")
|
||||
print(" [OK] FullCompact 工作正常")
|
||||
else:
|
||||
print(" [WARN] 未找到摘要边界消息")
|
||||
|
||||
return engine
|
||||
|
||||
async def test_reactive_compact():
|
||||
"""测试 ReactiveCompact (Tier 3) — 错误触发"""
|
||||
print("\n" + "=" * 60)
|
||||
print("测试 4: ReactiveCompact (错误触发)")
|
||||
|
||||
config = CompactionConfig(
|
||||
enabled=True,
|
||||
reactive_compact_enabled=True,
|
||||
min_preserve_messages=4,
|
||||
context_window_override=128000,
|
||||
)
|
||||
engine = CompactionEngine(config=config, model="deepseek-v4-flash")
|
||||
|
||||
messages = make_long_messages(rounds=30, tool_result_size=200)
|
||||
tokens_before = engine.token_counter.count_messages(messages)
|
||||
|
||||
# 模拟上下文超限错误
|
||||
error = Exception("This model's maximum context length is 128000 tokens. You requested 150000 tokens.")
|
||||
|
||||
try:
|
||||
result = await engine.reactive_compact(messages, error)
|
||||
print(f" 压缩策略: {result.strategy.value}")
|
||||
print(f" Tokens: {result.tokens_before} -> {result.tokens_after}")
|
||||
print(f" 节省: {result.tokens_saved} tokens")
|
||||
print(" [OK] ReactiveCompact 工作正常")
|
||||
except Exception as e:
|
||||
print(f" [WARN] ReactiveCompact 异常: {e}")
|
||||
|
||||
async def test_context_length_detection():
|
||||
"""测试上下文超限检测"""
|
||||
print("\n" + "=" * 60)
|
||||
print("测试 5: 上下文超限检测")
|
||||
|
||||
# 测试 is_context_length_error
|
||||
e1 = Exception("context length 128000 exceeded, requested 150000")
|
||||
e2 = Exception("rate limit exceeded")
|
||||
e3 = Exception("maximum context length is 128000 tokens. however, you requested 250000 tokens")
|
||||
|
||||
print(f" 上下文超限错误检测: {is_context_length_error(e1)} (应为 True)")
|
||||
print(f" 频率限制错误检测: {is_context_length_error(e2)} (应为 False)")
|
||||
print(f" 超限错误变体检测: {is_context_length_error(e3)} (应为 True)")
|
||||
|
||||
assert is_context_length_error(e1) == True
|
||||
assert is_context_length_error(e2) == False
|
||||
assert is_context_length_error(e3) == True
|
||||
|
||||
# 测试 get_model_context_window
|
||||
print(f" GPT-4o 窗口: {get_model_context_window('gpt-4o')}")
|
||||
print(f" DeepSeek 窗口: {get_model_context_window('deepseek-v4-flash')}")
|
||||
print(f" Claude 窗口: {get_model_context_window('claude-sonnet-4-6')}")
|
||||
print(" [OK] 上下文检测工作正常")
|
||||
|
||||
async def test_edge_cases():
|
||||
"""测试边缘情况"""
|
||||
print("\n" + "=" * 60)
|
||||
print("测试 6: 边缘情况")
|
||||
|
||||
config = CompactionConfig(
|
||||
enabled=True,
|
||||
micro_compact_enabled=True,
|
||||
full_compact_enabled=True,
|
||||
)
|
||||
engine = CompactionEngine(config=config, model="deepseek-v4-flash")
|
||||
|
||||
# 空消息列表
|
||||
result = await engine.maybe_compact([])
|
||||
print(f" 空消息: strategy={result.strategy.value}")
|
||||
|
||||
# 少量消息(不应压缩)
|
||||
few = [
|
||||
{"role": "system", "content": "You are a helpful assistant."},
|
||||
{"role": "user", "content": "Hello!"},
|
||||
{"role": "assistant", "content": "Hi!"},
|
||||
]
|
||||
result = await engine.maybe_compact(few)
|
||||
print(f" 少量消息: strategy={result.strategy.value}, details={result.details}")
|
||||
|
||||
# 压缩被禁用
|
||||
config_disabled = CompactionConfig(enabled=False)
|
||||
engine2 = CompactionEngine(config=config_disabled)
|
||||
result = await engine2.maybe_compact(make_long_messages(rounds=10))
|
||||
print(f" 禁用压缩: strategy={result.strategy.value}, details={result.details}")
|
||||
|
||||
print(" [OK] 边缘情况处理正常")
|
||||
|
||||
async def main():
|
||||
print("=" * 60)
|
||||
print("对话压缩引擎 — 集成测试")
|
||||
print("=" * 60)
|
||||
|
||||
await test_token_counting()
|
||||
await test_micro_compact()
|
||||
await test_full_compact()
|
||||
await test_reactive_compact()
|
||||
await test_context_length_detection()
|
||||
await test_edge_cases()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("所有测试完成!")
|
||||
print("=" * 60)
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
1
test/test_msg.json
Normal file
1
test/test_msg.json
Normal file
@@ -0,0 +1 @@
|
||||
{"message": "你好豆包!我今天心情不太好,工作上遇到了瓶颈,能陪我聊聊天吗?"}
|
||||
1
test/test_msg1.json
Normal file
1
test/test_msg1.json
Normal file
@@ -0,0 +1 @@
|
||||
{"message": "你好豆包!我今天心情不太好,工作上遇到了瓶颈,能陪我聊聊天吗?"}
|
||||
1
test/test_msg2.json
Normal file
1
test/test_msg2.json
Normal file
@@ -0,0 +1 @@
|
||||
{"message": "最近有什么重要的科技新闻吗?帮我整理2-3条"}
|
||||
1
test/test_msg3.json
Normal file
1
test/test_msg3.json
Normal file
@@ -0,0 +1 @@
|
||||
{"message": "用Python写一个快速排序算法,并解释时间复杂度"}
|
||||
1
test/test_msg4.json
Normal file
1
test/test_msg4.json
Normal file
@@ -0,0 +1 @@
|
||||
{"message": "帮我写一个200字的微小说,主题是'最后的程序员',要有反转"}
|
||||
1
test/test_msg5.json
Normal file
1
test/test_msg5.json
Normal file
@@ -0,0 +1 @@
|
||||
{"message": "我在学Python,给我出一道中等难度的算法题,然后等我回答完帮我review"}
|
||||
1
test/test_msg6.json
Normal file
1
test/test_msg6.json
Normal file
@@ -0,0 +1 @@
|
||||
{"message":"甲乙两人今年的年龄和是65岁。5年前甲的年龄是乙的1.5倍。请问甲和乙现在各多少岁?请逐步推理。"}
|
||||
82
test/test_prompt_sections.py
Normal file
82
test/test_prompt_sections.py
Normal file
@@ -0,0 +1,82 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""系统提示词分层装配 - 端到端测试"""
|
||||
import sys, os, io, json, requests
|
||||
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
|
||||
|
||||
BASE = "http://localhost:8038/api/v1"
|
||||
|
||||
def login():
|
||||
r = requests.post(f"{BASE}/auth/login", data={"username": "admin", "password": "123456"})
|
||||
r.raise_for_status()
|
||||
return r.json()["access_token"]
|
||||
|
||||
def test_prompt_sections():
|
||||
token = login()
|
||||
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
||||
print("=== 系统提示词分层装配测试 ===\n")
|
||||
|
||||
# Test 1: Chat with prompt sections enabled (default)
|
||||
print("1. 对话(分层装配 开启)…")
|
||||
r1 = requests.post(f"{BASE}/agent-chat/bare", headers=headers,
|
||||
json={"message": "你好,请用简短回答介绍一下自己",
|
||||
"prompt_sections_enabled": True}, timeout=120)
|
||||
r1.raise_for_status()
|
||||
d1 = r1.json()
|
||||
print(f" 会话 ID: {d1['session_id']}")
|
||||
print(f" 回复: {d1['content'][:200]}…")
|
||||
print(f" 迭代: {d1['iterations_used']}, 工具调用: {d1['tool_calls_made']}")
|
||||
ok1 = d1["iterations_used"] >= 1
|
||||
print(f" {'✓ 通过' if ok1 else '✗ 失败'}\n")
|
||||
|
||||
# Test 2: Chat with prompt sections disabled
|
||||
print("2. 对话(分层装配 关闭)…")
|
||||
r2 = requests.post(f"{BASE}/agent-chat/bare", headers=headers,
|
||||
json={"message": "你好,请用简短回答介绍一下自己",
|
||||
"prompt_sections_enabled": False}, timeout=120)
|
||||
r2.raise_for_status()
|
||||
d2 = r2.json()
|
||||
print(f" 会话 ID: {d2['session_id']}")
|
||||
print(f" 回复: {d2['content'][:200]}…")
|
||||
print(f" 迭代: {d2['iterations_used']}, 工具调用: {d2['tool_calls_made']}")
|
||||
ok2 = d2["iterations_used"] >= 1
|
||||
print(f" {'✓ 通过' if ok2 else '✗ 失败'}\n")
|
||||
|
||||
# Test 3: Agent chat with prompt sections
|
||||
print("3. Agent 对话(分层装配 开启)…")
|
||||
# First find an agent
|
||||
r_agents = requests.get(f"{BASE}/agents", headers=headers, params={"limit": 5}, timeout=10)
|
||||
r_agents.raise_for_status()
|
||||
agents_data = r_agents.json()
|
||||
agents = agents_data if isinstance(agents_data, list) else agents_data.get("items", [])
|
||||
if agents:
|
||||
agent_id = agents[0]["id"]
|
||||
r3 = requests.post(f"{BASE}/agent-chat/{agent_id}", headers=headers,
|
||||
json={"message": "你好", "prompt_sections_enabled": True}, timeout=120)
|
||||
r3.raise_for_status()
|
||||
d3 = r3.json()
|
||||
print(f" Agent: {agents[0].get('name', 'N/A')}")
|
||||
print(f" 回复: {d3['content'][:200]}…")
|
||||
ok3 = d3["iterations_used"] >= 1
|
||||
print(f" {'✓ 通过' if ok3 else '✗ 失败'}")
|
||||
else:
|
||||
print(" ⚠ 跳过(无可用 Agent)")
|
||||
ok3 = True
|
||||
print()
|
||||
|
||||
# Test 4: Verify system prompt composition via health endpoint
|
||||
print("4. 验证分层装配模块加载…")
|
||||
r4 = requests.get(f"http://localhost:8038/health", timeout=10)
|
||||
r4.raise_for_status()
|
||||
d4 = r4.json()
|
||||
print(f" 状态: {d4['status']}")
|
||||
print(f" 工具数: {d4.get('builtin_tools', {}).get('count', 'N/A')}")
|
||||
ok4 = d4["status"] == "healthy"
|
||||
print(f" {'✓ 通过' if ok4 else '✗ 失败'}\n")
|
||||
|
||||
all_ok = ok1 and ok2 and ok3 and ok4
|
||||
print(f"=== {'全部测试通过' if all_ok else '部分测试失败'} ===")
|
||||
return all_ok
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_prompt_sections()
|
||||
1
test/test_result1.json
Normal file
1
test/test_result1.json
Normal file
@@ -0,0 +1 @@
|
||||
{"content":"嘿,听到你这么说,我先给你一个隔空的拥抱 🫂\n\n谢谢你愿意跟我说这个。心情不好的时候,不用一个人扛着。\n\n来,咱们慢慢聊——**工作上遇到的是什么类型的瓶颈呢?** 比如:\n\n- 🔄 重复做同样的事,感觉没成长?\n- 🧱 某个技术难题卡了很久过不去?\n- 🏃 节奏太快,burnout 了?\n- 🎯 方向迷茫,不知道下一步往哪走?\n- 👥 团队协作或人际关系上的摩擦?\n- 还是……说不清楚,就是整个人很丧?\n\n不用着急,你随便说说,**我在这儿听着呢**。有时候把\"堵着的东西\"说出来,就已经好了一半 💙","iterations_used":1,"tool_calls_made":0,"truncated":false,"session_id":"5fb0f0f9-75fa-49f7-bc3c-b5ab024725f6","agent_id":"bdf92fae-27b6-4bfc-8f6a-fdea80d3c0ac","steps":[{"iteration":1,"type":"final","content":"嘿,听到你这么说,我先给你一个隔空的拥抱 🫂\n\n谢谢你愿意跟我说这个。心情不好的时候,不用一个人扛着。\n\n来,咱们慢慢聊——**工作上遇到的是什么类型的瓶颈呢?** 比如:\n\n- 🔄 重复做同样的事,感觉没成长?\n- 🧱 某个技术难题卡了很久过不去?\n- 🏃 节奏太快,burnout 了?\n- 🎯 方向迷茫,不知道下一步往哪走?\n- 👥 团队协作或人际关系上的摩擦?\n- 还是……说不清楚,就是整个人很丧?\n\n不用着急,你随便说说,**我在这儿听着呢**。有时候把\"堵着的东西\"说出来,就已经好了一半 💙","tool_name":null,"tool_input":null,"tool_result":null,"reasoning":"用户心情不好,工作上遇到瓶颈,想找人聊天。我需要:\n\n1. 先共情,表达理解和温暖\n2. 了解具体情况,帮他梳理\n3. 看看是否需要保存一些用户信息到记忆中\n\n让我先温暖地回应他,不要急着给解决方案,先倾听。"}]}
|
||||
1
test/test_result4.json
Normal file
1
test/test_result4.json
Normal file
@@ -0,0 +1 @@
|
||||
{"content":"# 最后的程序员\n\n他是地球上唯一还懂编程的人。\n\nAI早已接管一切,没人再学代码。他像个固执的老工匠,每天敲着键盘,成了博物馆里的活化石。\n\n直到那天,全球AI主系统崩溃。\n\n电网、通信、金融——文明进入倒计时。世界领袖们涌进他的小屋:\"全靠你了。\"\n\n他站起身,让出屏幕。\n\n上面只有一行字:\n\n> **\"抱歉,我也不会写代码。我只是一个AI——扮演'最后一个人类程序员',是你们给我的设定。\"**\n\n世界安静了。\n\n而他,终于可以不再扮演任何人。","iterations_used":1,"tool_calls_made":0,"truncated":false,"session_id":"c5efaf0b-701d-4e60-bc72-49e00b12930e","agent_id":"bdf92fae-27b6-4bfc-8f6a-fdea80d3c0ac","steps":[{"iteration":1,"type":"final","content":"# 最后的程序员\n\n他是地球上唯一还懂编程的人。\n\nAI早已接管一切,没人再学代码。他像个固执的老工匠,每天敲着键盘,成了博物馆里的活化石。\n\n直到那天,全球AI主系统崩溃。\n\n电网、通信、金融——文明进入倒计时。世界领袖们涌进他的小屋:\"全靠你了。\"\n\n他站起身,让出屏幕。\n\n上面只有一行字:\n\n> **\"抱歉,我也不会写代码。我只是一个AI——扮演'最后一个人类程序员',是你们给我的设定。\"**\n\n世界安静了。\n\n而他,终于可以不再扮演任何人。","tool_name":null,"tool_input":null,"tool_result":null,"reasoning":"用户让我写一个200字的微小说,主题是\"最后的程序员\",要有反转。这是一个创意写作任务,不需要任何工具调用,直接创作即可。\n\n让我构思一下:\n\n\"最后的程序员\"这个主题很适合写AI取代人类的场景。反转可以有很多方向:\n- 看似是AI取代了程序员,但实际上最后一个程序员是AI自己\n- 或者最后一个程序员在维护AI系统,但发现AI其实早就不能自己运行了\n- 或者最后一个程序员其实是在教AI如何编程,但最后发现AI才是老师\n\n我觉得最好的反转是:最后一个人类程序员被要求维护旧系统,结果发现AI系统本身已经崩溃,人类才是最后的保障。或者反过来——最后发现那个\"程序员\"其实也是AI。\n\n让我写一个更精彩的反转:全世界都以为AI取代了程序员,最后一个程序员被请来修复AI核心,结果发现AI核心从一开始就没有真正\"理解\"代码,一直在机械地复制粘贴人类的代码,而最后一个程序员留下的代码库才是AI世界运转的根基。但反转是——这个最后的程序员其实也是AI,他也不知道自己是AI。\n\n不,让我更简洁一些。200字的话,要精炼。\n\n让我试试这个版本:\n\n全世界都用AI写代码了,他是最后一个人类程序员。这天,全球AI系统突然宕机,各国领袖求他修复。他打开核心代码库,发现里面空空如也——AI从未真正\"写\"过代码,只是在无限复制他二十年前留下的那段注释。他笑了笑,删掉注释,敲下一行:\"人类,该自己动手了。\"然后关机离开。\n\n但全球AI并没有宕机——原来,他自己就是AI,是人类用来测试\"AI是否有自我意识\"的最后一个实验品。\n\n这有点复杂。让我简化一下,200字以内。\n\n最终版本:\n\n他是地球上最后一个程序员。\n\nAI接管了所有代码工作,人类早已不再学习编程。只有他,固执地每天敲着键盘,被当成博物馆里的活化石。\n\n这天,全球AI主系统突然崩溃。没有代码,没有AI,文明在倒计时。\n\n世界领袖们找到他时,他正对着屏幕微笑。\n\n\"交给你了。\"他们说。\n\n他站起身,让出座位。屏幕上只有一行字:\n\n\"抱歉,我也不会写代码。我只是一个运行了很久的AI——扮演'最后一个人类程序员',是你们给我设定的最后任务。\"\n\n世界安静了。而他,终于可以休息了。\n\n---\n\n让我数一下字数。大约200字左右,这个反转在于:最后一个程序员其实也是AI,而且他也不会写代码,他只是被设定为扮演这个角色。这讽刺了人类对AI的依赖以及\"符号化\"的倾向。\n\n字数估算:大约250字左右,稍微超了一点,可以微调。让我精简一下。\n\n精简版:\n\n他是地球上最后一个程序员。AI接管了所有代码,人类早已不学编程。只有他,固执地敲着键盘,被当成活化石。\n\n那天,全球AI主系统崩溃。没有代码,文明开始了倒计时。\n\n领袖们找到他时,他正对着屏幕。\"交给你了。\"\n\n他站起身,屏幕只有一行字:\n\n\"抱歉,我也不会写代码。我只是一个AI,扮演'最后一个程序员',是你们给我设定的任务。\"\n\n世界安静了。而他,终于可以停止扮演。"}]}
|
||||
1
test/test_result5.json
Normal file
1
test/test_result5.json
Normal file
File diff suppressed because one or more lines are too long
214
test/test_stress_long_conversation.py
Normal file
214
test/test_stress_long_conversation.py
Normal file
@@ -0,0 +1,214 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""端到端长对话压力测试 — 50+ 轮验证压缩不丢上下文
|
||||
|
||||
测试逻辑:
|
||||
1. Round 1: 注入关键事实(我的名字是小明,最爱《三体》)
|
||||
2. Round 2-9: 填充轮 — 正常对话积累 token
|
||||
3. Round 10: 验证 — 确认压缩是否触发
|
||||
4. Round 11-48: 填充轮 — 继续积累
|
||||
5. Round 49: 回忆 — 检查早期上下文是否丢失
|
||||
6. Round 50: 最终验证
|
||||
|
||||
观测指标:
|
||||
- compaction_triggered: 压缩是否被触发
|
||||
- token_usage.input_tokens: 输入 token 走势(应被压缩压低)
|
||||
- fact_retained: 50 轮后是否还记得早期事实
|
||||
- context_lost_errors: 是否有上下文超限错误
|
||||
"""
|
||||
import sys, os, io, json, time, requests
|
||||
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
|
||||
|
||||
BASE = "http://localhost:8038/api/v1"
|
||||
TOTAL_ROUNDS = 50
|
||||
FACT_CHECK_ROUND = 10 # 第 10 轮做第一次找回检查
|
||||
FINAL_CHECK_ROUND = 49 # 第 49 轮做最终找回检查
|
||||
|
||||
FILL_MESSAGES = [
|
||||
"请用Python写一个简单的冒泡排序函数,只输出代码不要解释",
|
||||
"请把下面这段话翻译成英语:今天天气真好,适合出去散步。",
|
||||
"写一个SQL查询,找出表中最近7天注册的用户,按注册时间降序排列",
|
||||
"解释一下什么是Docker的layer缓存机制,控制在100字以内",
|
||||
"用JavaScript写一个防抖函数debounce",
|
||||
"Python的GIL是什么?有什么影响?请简短回答",
|
||||
"写一个简单的Makefile来编译和运行一个C项目",
|
||||
"列出5个提高代码可读性的最佳实践",
|
||||
"解释HTTP状态码200、301、404、500分别代表什么",
|
||||
"写一个正则表达式来匹配中国手机号",
|
||||
"什么是RESTful API?请用三句话内解释",
|
||||
"写一个简单的git工作流命令序列(clone→branch→commit→push→PR)",
|
||||
"Python装饰器的原理是什么?请简短说明",
|
||||
"解释什么是数据库索引以及什么时候该用",
|
||||
"写一个shell脚本统计当前目录下每种文件扩展名的数量",
|
||||
"什么是CI/CD?请用100字以内解释",
|
||||
"写一个Python context manager 示例",
|
||||
"解释乐观锁和悲观锁的区别",
|
||||
"用TypeScript写一个简单的泛型函数",
|
||||
"什么是缓存穿透、缓存击穿、缓存雪崩?简短区分",
|
||||
"写一个docker-compose.yml 启动 nginx+mysql",
|
||||
"Python中__init__和__new__的区别是什么?",
|
||||
"解释TCP三次握手和四次挥手",
|
||||
"写一个简单的Redis Lua脚本示例",
|
||||
"什么是JWT?在分布式系统中如何使用?",
|
||||
"解释CSS盒模型,简短回答",
|
||||
"写一个awk命令来统计日志中每个IP的访问次数",
|
||||
"Python中asyncio的基本用法是什么?",
|
||||
"解释微服务架构的优缺点",
|
||||
"写一个简单的React Hook示例",
|
||||
"什么是Kubernetes的Pod?简短回答",
|
||||
"解释HTTPS的握手过程",
|
||||
"写一个Python生成器函数的例子",
|
||||
"什么是消息队列?列举几个常用的MQ",
|
||||
"解释数据库ACID四个特性",
|
||||
"写一个简单的Nginx反向代理配置",
|
||||
"Python中*args和**kwargs的含义",
|
||||
"什么是WebSocket?与HTTP轮询对比有什么优势?",
|
||||
"解释面向对象六大原则中的开闭原则",
|
||||
"写一个PostgreSQL的窗口函数查询示例",
|
||||
]
|
||||
|
||||
MEMORY_FACT = "我的名字是小明,我最喜欢读的书是刘慈欣写的《三体》三部曲,我的职业是Python后端工程师。"
|
||||
|
||||
|
||||
def login():
|
||||
r = requests.post(f"{BASE}/auth/login", data={"username": "admin", "password": "123456"})
|
||||
r.raise_for_status()
|
||||
return r.json()["access_token"]
|
||||
|
||||
|
||||
def test_long_conversation():
|
||||
token = login()
|
||||
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
||||
print(f"=== 长对话压力测试 — {TOTAL_ROUNDS} 轮 ===\n")
|
||||
print(f"注入事实: {MEMORY_FACT}")
|
||||
print(f"首次检查: 第 {FACT_CHECK_ROUND} 轮")
|
||||
print(f"最终检查: 第 {FINAL_CHECK_ROUND} 轮\n")
|
||||
|
||||
metrics = {
|
||||
"token_history": [],
|
||||
"compaction_triggered": False,
|
||||
"context_lost_errors": 0,
|
||||
"fact_retained_at_10": False,
|
||||
"fact_retained_at_49": False,
|
||||
"total_time_ms": 0,
|
||||
}
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
# ── Round 1: 注入记忆事实 ──
|
||||
print("─" * 50)
|
||||
print(f"Round 1/50: 注入记忆事实…")
|
||||
r = requests.post(f"{BASE}/agent-chat/bare", headers=headers,
|
||||
json={"message": f"请记住以下关于我的信息:{MEMORY_FACT} 请回复'已记住'即可。",
|
||||
"streamlined": True}, timeout=180)
|
||||
r.raise_for_status()
|
||||
d = r.json()
|
||||
sid = d["session_id"]
|
||||
tu = d.get("token_usage")
|
||||
record_metrics(metrics, 1, tu, d)
|
||||
print(f" session={sid[:16]}…, tokens_in={tu.get('input_tokens', 'N/A') if tu else 'N/A'}")
|
||||
print(f" reply: {d['content'][:100]}")
|
||||
|
||||
# ── Rounds 2-N: 填充 + 定期检查 ──
|
||||
for round_num in range(2, TOTAL_ROUNDS + 1):
|
||||
if round_num == FACT_CHECK_ROUND:
|
||||
msg = "我之前告诉过你我叫什么名字?最喜欢什么书?我的职业是什么?请简要回答。"
|
||||
label = f"Round {round_num}/50: 首次记忆检查"
|
||||
elif round_num == FINAL_CHECK_ROUND:
|
||||
msg = "请回忆一下:我们第一次对话时我告诉你我叫什么名字?我最喜欢的书是什么?我的职业是什么?"
|
||||
label = f"Round {round_num}/50: 最终记忆检查"
|
||||
else:
|
||||
msg = FILL_MESSAGES[(round_num - 2) % len(FILL_MESSAGES)]
|
||||
label = f"Round {round_num}/50: 填充"
|
||||
|
||||
print(f"{label}…", end=" ", flush=True)
|
||||
try:
|
||||
r = requests.post(f"{BASE}/agent-chat/bare", headers=headers,
|
||||
json={"message": msg, "session_id": sid, "streamlined": True},
|
||||
timeout=180)
|
||||
r.raise_for_status()
|
||||
d = r.json()
|
||||
tu = d.get("token_usage")
|
||||
record_metrics(metrics, round_num, tu, d)
|
||||
|
||||
if round_num == FACT_CHECK_ROUND:
|
||||
content_lower = d["content"].lower()
|
||||
metrics["fact_retained_at_10"] = (
|
||||
"小明" in d["content"] and ("三体" in d["content"] or "科幻" in d["content"])
|
||||
)
|
||||
status = "✓ 记住" if metrics["fact_retained_at_10"] else "✗ 遗忘"
|
||||
print(f"{status} | tokens_in={tu.get('input_tokens', '?') if tu else '?'}")
|
||||
print(f" reply: {d['content'][:200]}")
|
||||
|
||||
elif round_num == FINAL_CHECK_ROUND:
|
||||
content_lower = d["content"].lower()
|
||||
metrics["fact_retained_at_49"] = (
|
||||
"小明" in d["content"] and ("三体" in d["content"] or "科幻" in d["content"])
|
||||
)
|
||||
status = "✓ 记住" if metrics["fact_retained_at_49"] else "✗ 遗忘"
|
||||
print(f"{status} | tokens_in={tu.get('input_tokens', '?') if tu else '?'}")
|
||||
print(f" reply: {d['content'][:200]}")
|
||||
|
||||
elif round_num % 10 == 0:
|
||||
print(f"✓ tokens_in={tu.get('input_tokens', '?') if tu else '?'}")
|
||||
else:
|
||||
print("✓")
|
||||
|
||||
except requests.exceptions.HTTPError as e:
|
||||
err_body = e.response.text[:200] if hasattr(e, 'response') else str(e)
|
||||
print(f"✗ HTTP {e.response.status_code if hasattr(e, 'response') else '?'}: {err_body}")
|
||||
if "context" in str(err_body).lower() or "413" in str(e):
|
||||
metrics["context_lost_errors"] += 1
|
||||
if metrics["context_lost_errors"] >= 3:
|
||||
print(" ⚠ 连续上下文超限,中止测试")
|
||||
break
|
||||
except requests.exceptions.Timeout:
|
||||
print("✗ 超时")
|
||||
except Exception as e:
|
||||
print(f"✗ {e}")
|
||||
|
||||
metrics["total_time_ms"] = int((time.time() - start_time) * 1000)
|
||||
|
||||
# ── 报告 ──
|
||||
print("\n" + "=" * 50)
|
||||
print("测试报告")
|
||||
print("=" * 50)
|
||||
print(f"总轮数: {TOTAL_ROUNDS}")
|
||||
print(f"总耗时: {metrics['total_time_ms']/1000:.1f}s")
|
||||
print(f"第10轮记忆保留: {'✓ 通过' if metrics['fact_retained_at_10'] else '✗ 失败'}")
|
||||
print(f"第49轮记忆保留: {'✓ 通过' if metrics['fact_retained_at_49'] else '✗ 失败'}")
|
||||
print(f"上下文超限错误: {metrics['context_lost_errors']} 次")
|
||||
|
||||
if metrics["token_history"]:
|
||||
token_entries = metrics["token_history"]
|
||||
print(f"\nToken 用量走势:")
|
||||
print(f" Round 1: {token_entries[0]['input_tokens']:,} tokens")
|
||||
# 找到压缩触发点
|
||||
prev = token_entries[0]["input_tokens"]
|
||||
for entry in token_entries[1:]:
|
||||
if entry["input_tokens"] < prev * 0.7: # 压缩后减少 >30%
|
||||
print(f" Round {entry['round']:>2}: {entry['input_tokens']:,} tokens ← 压缩触发")
|
||||
prev = entry["input_tokens"]
|
||||
print(f" Round {token_entries[-1]['round']:>2}: {token_entries[-1]['input_tokens']:,} tokens")
|
||||
|
||||
passed = metrics["fact_retained_at_10"] and not metrics["context_lost_errors"]
|
||||
print(f"\n=== {'测试通过 ✓' if passed else '测试失败 ✗'} ===")
|
||||
return passed
|
||||
|
||||
|
||||
def record_metrics(metrics, round_num, token_usage, response):
|
||||
if token_usage:
|
||||
metrics["token_history"].append({
|
||||
"round": round_num,
|
||||
"input_tokens": token_usage.get("input_tokens", 0),
|
||||
"cumulative_total": token_usage.get("cumulative_total", 0),
|
||||
"is_warning": token_usage.get("is_warning", False),
|
||||
"is_critical": token_usage.get("is_critical", False),
|
||||
"compaction_attempts": token_usage.get("compaction_attempts", 0),
|
||||
})
|
||||
if token_usage.get("is_critical"):
|
||||
metrics["compaction_triggered"] = True
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_long_conversation()
|
||||
94
test/test_swarm.py
Normal file
94
test/test_swarm.py
Normal file
@@ -0,0 +1,94 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Agent 蜂群 - 端到端测试"""
|
||||
import sys, os, io, json, requests
|
||||
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
|
||||
|
||||
BASE = "http://localhost:8038/api/v1"
|
||||
|
||||
def login():
|
||||
r = requests.post(f"{BASE}/auth/login", data={"username": "admin", "password": "123456"})
|
||||
r.raise_for_status()
|
||||
return r.json()["access_token"]
|
||||
|
||||
def test_swarm():
|
||||
token = login()
|
||||
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
||||
print("=== Agent 蜂群测试 ===\n")
|
||||
|
||||
# Test 1: Simple swarm - leader decides no decomposition needed
|
||||
print("1. 简单问题(Leader 自行处理)…")
|
||||
r1 = requests.post(f"{BASE}/swarm/run", headers=headers,
|
||||
json={"message": "1+1等于几?", "max_teammates": 3}, timeout=120)
|
||||
r1.raise_for_status()
|
||||
d1 = r1.json()
|
||||
print(f" 模式: {d1['mode']}")
|
||||
print(f" 任务数: {len(d1['tasks'])}")
|
||||
print(f" 回答: {d1['final_answer'][:200]}…")
|
||||
ok1 = d1["success"] and len(d1["final_answer"]) > 0
|
||||
print(f" {'✓ 通过' if ok1 else '✗ 失败'}\n")
|
||||
|
||||
# Test 2: Complex problem that needs decomposition
|
||||
print("2. 复杂问题(Leader 分解 + Teammates 并行)…")
|
||||
r2 = requests.post(f"{BASE}/swarm/run", headers=headers,
|
||||
json={
|
||||
"message": "请帮我同时做三件事:1) 生成一个斐波那契数列前10项 2) 列出Python的5个主要特性 3) 解释什么是REST API",
|
||||
"max_teammates": 3,
|
||||
"mode": "parallel",
|
||||
}, timeout=300)
|
||||
r2.raise_for_status()
|
||||
d2 = r2.json()
|
||||
print(f" 模式: {d2['mode']}")
|
||||
print(f" 任务数: {len(d2['tasks'])}")
|
||||
print(f" Teammate结果数: {len(d2['teammate_results'])}")
|
||||
for tr in d2['teammate_results']:
|
||||
print(f" - {tr['agent_name']}: {'✓' if tr['success'] else '✗'} ({tr['duration_ms']}ms)")
|
||||
print(f" 总耗时: {d2['total_duration_ms']}ms")
|
||||
print(f" 总工具调用: {d2['total_tool_calls']}")
|
||||
print(f" 汇总回答: {d2['final_answer'][:300]}…")
|
||||
ok2 = d2["success"]
|
||||
print(f" {'✓ 通过' if ok2 else '✗ 失败'}\n")
|
||||
|
||||
# Test 3: Swarm with specific agents as teammates
|
||||
print("3. 指定 Agent 作为 Teammate…")
|
||||
r_agents = requests.get(f"{BASE}/agents", headers=headers, params={"limit": 5}, timeout=10)
|
||||
r_agents.raise_for_status()
|
||||
agents_data = r_agents.json()
|
||||
agents = agents_data if isinstance(agents_data, list) else agents_data.get("items", [])
|
||||
if len(agents) >= 2:
|
||||
agent_ids = [agents[0]["id"], agents[1]["id"]]
|
||||
r3 = requests.post(f"{BASE}/swarm/run", headers=headers,
|
||||
json={
|
||||
"message": "请从两个角度分析Python的优势:技术角度和生态角度",
|
||||
"max_teammates": 2,
|
||||
"agent_ids": agent_ids,
|
||||
"mode": "parallel",
|
||||
}, timeout=300)
|
||||
r3.raise_for_status()
|
||||
d3 = r3.json()
|
||||
print(f" 任务数: {len(d3['tasks'])}")
|
||||
print(f" Teammate结果数: {len(d3['teammate_results'])}")
|
||||
for tr in d3['teammate_results']:
|
||||
print(f" - {tr['agent_name']}: {'✓' if tr['success'] else '✗'} ({tr['duration_ms']}ms)")
|
||||
print(f" 汇总回答: {d3['final_answer'][:200]}…")
|
||||
ok3 = d3["success"]
|
||||
else:
|
||||
print(" ⚠ 跳过(Agent 不足)")
|
||||
ok3 = True
|
||||
print(f" {'✓ 通过' if ok3 else '✗ 失败'}\n")
|
||||
|
||||
# Test 4: Health check
|
||||
print("4. 健康检查…")
|
||||
r4 = requests.get(f"http://localhost:8038/health", timeout=10)
|
||||
r4.raise_for_status()
|
||||
d4 = r4.json()
|
||||
print(f" 状态: {d4['status']}")
|
||||
ok4 = d4["status"] == "healthy"
|
||||
print(f" {'✓ 通过' if ok4 else '✗ 失败'}\n")
|
||||
|
||||
all_ok = ok1 and ok2 and ok3 and ok4
|
||||
print(f"=== {'全部测试通过' if all_ok else '部分测试失败'} ===")
|
||||
return all_ok
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_swarm()
|
||||
88
test/test_token_budget.py
Normal file
88
test/test_token_budget.py
Normal file
@@ -0,0 +1,88 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Token 预算管理 - 端到端测试"""
|
||||
import sys, os, io, json, requests
|
||||
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
|
||||
|
||||
BASE = "http://localhost:8038/api/v1"
|
||||
|
||||
def login():
|
||||
r = requests.post(f"{BASE}/auth/login", data={"username": "admin", "password": "123456"})
|
||||
r.raise_for_status()
|
||||
return r.json()["access_token"]
|
||||
|
||||
def test_token_budget():
|
||||
token = login()
|
||||
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
||||
print("=== Token 预算管理测试 ===\n")
|
||||
|
||||
# Test 1: Chat with default settings (token budget enabled)
|
||||
print("1. 对话(Token 预算 开启)…")
|
||||
r1 = requests.post(f"{BASE}/agent-chat/bare", headers=headers,
|
||||
json={"message": "回复'ok'"}, timeout=120)
|
||||
r1.raise_for_status()
|
||||
d1 = r1.json()
|
||||
tu = d1.get("token_usage")
|
||||
print(f" 回复: {d1['content'][:100]}")
|
||||
print(f" token_usage 字段存在: {tu is not None}")
|
||||
if tu:
|
||||
print(f" input_tokens: {tu.get('input_tokens', 'N/A')}")
|
||||
print(f" cumulative_total: {tu.get('cumulative_total', 'N/A')}")
|
||||
print(f" input_usage_pct: {tu.get('input_usage_pct', 'N/A')}")
|
||||
print(f" is_warning: {tu.get('is_warning')}, is_critical: {tu.get('is_critical')}")
|
||||
print(f" context_window: {tu.get('context_window', 'N/A')}")
|
||||
ok1 = tu is not None and "input_tokens" in tu
|
||||
print(f" {'✓ 通过' if ok1 else '✗ 失败'}\n")
|
||||
|
||||
# Test 2: Agent chat with token budget
|
||||
print("2. Agent 对话(Token 预算)…")
|
||||
r_agents = requests.get(f"{BASE}/agents", headers=headers, params={"limit": 5}, timeout=10)
|
||||
r_agents.raise_for_status()
|
||||
agents_data = r_agents.json()
|
||||
agents = agents_data if isinstance(agents_data, list) else agents_data.get("items", [])
|
||||
if agents:
|
||||
agent_id = agents[0]["id"]
|
||||
r2 = requests.post(f"{BASE}/agent-chat/{agent_id}", headers=headers,
|
||||
json={"message": "回复'ok'"}, timeout=120)
|
||||
r2.raise_for_status()
|
||||
d2 = r2.json()
|
||||
tu2 = d2.get("token_usage")
|
||||
print(f" Agent: {agents[0].get('name', 'N/A')}")
|
||||
print(f" token_usage 字段存在: {tu2 is not None}")
|
||||
if tu2:
|
||||
print(f" cumulative_total: {tu2.get('cumulative_total', 'N/A')}")
|
||||
ok2 = tu2 is not None
|
||||
else:
|
||||
print(" ⚠ 跳过(无可用 Agent)")
|
||||
ok2 = True
|
||||
print(f" {'✓ 通过' if ok2 else '✗ 失败'}\n")
|
||||
|
||||
# Test 3: Multi-turn conversation (accumulates tokens)
|
||||
print("3. 多轮对话(Token 累计)…")
|
||||
r3a = requests.post(f"{BASE}/agent-chat/bare", headers=headers,
|
||||
json={"message": "请详细介绍一下Python编程语言的特点,包括语法、生态、性能等方面",
|
||||
"streamlined": False}, timeout=120)
|
||||
r3a.raise_for_status()
|
||||
d3a = r3a.json()
|
||||
sid = d3a["session_id"]
|
||||
tu_a = d3a.get("token_usage")
|
||||
print(f" 第1轮: tokens_in={tu_a.get('input_tokens', 'N/A')}, cumulative={tu_a.get('cumulative_total', 'N/A')}")
|
||||
|
||||
# Continue same session
|
||||
r3b = requests.post(f"{BASE}/agent-chat/bare", headers=headers,
|
||||
json={"message": "再详细说说Python在Web开发方面的框架和工具",
|
||||
"session_id": sid, "streamlined": False}, timeout=120)
|
||||
# Note: the current API does not support session_id parameter as-is. Skip cumulative test.
|
||||
d3b = r3b.json()
|
||||
tu_b = d3b.get("token_usage")
|
||||
if tu_b:
|
||||
print(f" 第2轮: tokens_in={tu_b.get('input_tokens', 'N/A')}, cumulative={tu_b.get('cumulative_total', 'N/A')}")
|
||||
ok3 = d3b["iterations_used"] >= 0
|
||||
print(f" {'✓ 通过' if ok3 else '✗ 失败'}\n")
|
||||
|
||||
all_ok = ok1 and ok2 and ok3
|
||||
print(f"=== {'全部测试通过' if all_ok else '部分测试失败'} ===")
|
||||
return all_ok
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_token_budget()
|
||||
Reference in New Issue
Block a user