mirror of
https://github.com/TencentCloud/TencentDB-Agent-Memory
synced 2026-07-10 20:34:30 +00:00
231 lines
10 KiB
Python
231 lines
10 KiB
Python
"""
|
||
SDK E2E Test — 打远端验证所有 v2 API 接口可用性
|
||
|
||
环境变量(从 .env 读或手动 export):
|
||
E2E_ENDPOINT — Gateway 地址
|
||
E2E_API_KEY — Bearer Token
|
||
E2E_SERVICE_ID — x-tdai-service-id
|
||
|
||
运行:
|
||
cd sdk/python
|
||
E2E_ENDPOINT=... E2E_API_KEY=... E2E_SERVICE_ID=... python tests/test_e2e.py
|
||
"""
|
||
|
||
import os
|
||
import sys
|
||
import time
|
||
|
||
# Load .env from project root
|
||
try:
|
||
from dotenv import load_dotenv
|
||
load_dotenv(os.path.join(os.path.dirname(__file__), "..", "..", "..", ".env"))
|
||
except ImportError:
|
||
pass # dotenv not installed, rely on env vars
|
||
|
||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||
|
||
from tencentdb_agent_memory import MemoryClient
|
||
|
||
ENDPOINT = os.environ.get("E2E_ENDPOINT", "http://127.0.0.1:8420")
|
||
API_KEY = os.environ.get("E2E_API_KEY", "test-key")
|
||
SERVICE_ID = os.environ.get("E2E_SERVICE_ID", "test-service")
|
||
ENABLE_DELETE = os.environ.get("E2E_ENABLE_DELETE", "0") == "1"
|
||
|
||
TS = int(time.time() * 1000)
|
||
SESSION_ID = f"sdk-e2e-py-{TS}"
|
||
|
||
passed = 0
|
||
failed = 0
|
||
failures: list[str] = []
|
||
|
||
|
||
def assert_ok(condition: bool, msg: str):
|
||
global passed, failed
|
||
if condition:
|
||
passed += 1
|
||
print(f" ✅ {msg}")
|
||
else:
|
||
failed += 1
|
||
failures.append(msg)
|
||
print(f" ❌ {msg}")
|
||
|
||
|
||
def section(name: str):
|
||
print(f"\n━━━ {name} ━━━")
|
||
|
||
|
||
def main():
|
||
print(f"\n🧪 TencentDB Agent Memory Python SDK E2E")
|
||
print(f" Endpoint: {ENDPOINT}")
|
||
print(f" ServiceId: {SERVICE_ID}")
|
||
print(f" Session: {SESSION_ID}")
|
||
|
||
client = MemoryClient(
|
||
endpoint=ENDPOINT,
|
||
api_key=API_KEY,
|
||
service_id=SERVICE_ID,
|
||
timeout=30,
|
||
)
|
||
|
||
# ════════════════════════════════════════════
|
||
# L0 Conversation
|
||
# ════════════════════════════════════════════
|
||
section("L0 Conversation")
|
||
|
||
# add
|
||
add_result = client.add_conversation(SESSION_ID, [
|
||
{"role": "user", "content": "Python SDK E2E 测试消息 1"},
|
||
{"role": "assistant", "content": "收到了,这是回复"},
|
||
{"role": "user", "content": "Python SDK E2E 测试消息 2"},
|
||
])
|
||
assert_ok(add_result["total_count"] == 3, f"conversation/add: total_count={add_result['total_count']}")
|
||
assert_ok(len(add_result["accepted_ids"]) == 3, f"conversation/add: accepted_ids={len(add_result['accepted_ids'])}")
|
||
|
||
# query
|
||
query_result = client.query_conversation(session_id=SESSION_ID, limit=10)
|
||
assert_ok(query_result["total"] >= 3, f"conversation/query: total={query_result['total']}")
|
||
assert_ok(len(query_result["messages"]) >= 3, f"conversation/query: messages={len(query_result['messages'])}")
|
||
|
||
# search
|
||
search_result = client.search_conversation("Python SDK E2E", limit=5)
|
||
assert_ok(len(search_result["messages"]) > 0, f"conversation/search: hits={len(search_result['messages'])}")
|
||
|
||
# delete — only when E2E_ENABLE_DELETE=1
|
||
if ENABLE_DELETE:
|
||
del_result = client.delete_conversation(session_id=SESSION_ID)
|
||
assert_ok(del_result["deleted_count"] >= 3, f"conversation/delete: deleted_count={del_result['deleted_count']}")
|
||
|
||
# verify delete
|
||
after_del = client.query_conversation(session_id=SESSION_ID)
|
||
assert_ok(after_del["total"] == 0, f"conversation/query after delete: total={after_del['total']}")
|
||
else:
|
||
print(" ℹ️ 跳过 conversation/delete (E2E_ENABLE_DELETE!=1)")
|
||
|
||
# ════════════════════════════════════════════
|
||
# L1 Atomic
|
||
# ════════════════════════════════════════════
|
||
section("L1 Atomic")
|
||
|
||
# query
|
||
atomic_query = client.query_atomic(limit=5)
|
||
assert_ok(atomic_query["total"] >= 0, f"atomic/query: total={atomic_query['total']}")
|
||
|
||
# search
|
||
atomic_search = client.search_atomic("测试", limit=5)
|
||
assert_ok(isinstance(atomic_search["items"], list), f"atomic/search: items is list (len={len(atomic_search['items'])})")
|
||
|
||
# update + revert (if items exist)
|
||
if atomic_query["items"]:
|
||
target = atomic_query["items"][0]
|
||
original_content = target["content"]
|
||
updated_content = f"{original_content} [SDK E2E {TS}]"
|
||
|
||
update_result = client.update_atomic(target["id"], updated_content)
|
||
assert_ok(bool(update_result.get("updated_at")), f"atomic/update: updated_at={update_result.get('updated_at')}")
|
||
|
||
# read back to verify timestamp marker
|
||
after_update = client.query_atomic(limit=50)
|
||
found = next((i for i in after_update["items"] if i["id"] == target["id"]), None)
|
||
assert_ok(found is not None and f"[SDK E2E {TS}]" in found["content"], f"atomic/update verify: marker [SDK E2E {TS}] found")
|
||
|
||
# revert
|
||
client.update_atomic(target["id"], original_content)
|
||
print(" ℹ️ 已还原 atomic content")
|
||
|
||
# delete (if E2E_ENABLE_DELETE and >= 2 items)
|
||
if ENABLE_DELETE and len(atomic_query["items"]) >= 2:
|
||
last_item = atomic_query["items"][-1]
|
||
del_result = client.delete_atomic([last_item["id"]])
|
||
assert_ok(del_result.get("deleted_count", 0) >= 1, f"atomic/delete: deleted_count={del_result.get('deleted_count')}")
|
||
|
||
# verify gone
|
||
import time; time.sleep(2)
|
||
after_atomic_del = client.query_atomic(limit=50)
|
||
still_exists = any(i["id"] == last_item["id"] for i in after_atomic_del["items"])
|
||
assert_ok(not still_exists, f"atomic/delete verify: id={last_item['id']} not found after delete")
|
||
elif not ENABLE_DELETE:
|
||
print(" ℹ️ 跳过 atomic/delete (E2E_ENABLE_DELETE!=1)")
|
||
else:
|
||
print(" ℹ️ L1 记忆不足 2 条,跳过 atomic/delete")
|
||
else:
|
||
print(" ℹ️ 暂无 L1 记忆,跳过 update")
|
||
|
||
# ════════════════════════════════════════════
|
||
# L2 Scenario
|
||
# ════════════════════════════════════════════
|
||
section("L2 Scenario")
|
||
|
||
# ls
|
||
ls_result = client.list_scenarios()
|
||
assert_ok(ls_result["total"] >= 0, f"scenario/ls: total={ls_result['total']}")
|
||
assert_ok(isinstance(ls_result["entries"], list), "scenario/ls: entries is list")
|
||
|
||
if ls_result["entries"]:
|
||
first_file = next((e for e in ls_result["entries"] if not e["path"].endswith("/")), None)
|
||
if first_file:
|
||
# read
|
||
read_result = client.read_scenario(first_file["path"])
|
||
assert_ok(bool(read_result.get("content")), f"scenario/read \"{first_file['path']}\": content.length={len(read_result.get('content', ''))}")
|
||
assert_ok(bool(read_result.get("updated_at")), "scenario/read: has updated_at")
|
||
|
||
# write
|
||
import re
|
||
content_no_meta = re.sub(r"^-----META-START-----[\s\S]*?-----META-END-----\n*", "", read_result["content"])
|
||
marker = f"\n<!-- Python SDK E2E {TS} -->"
|
||
write_result = client.write_scenario(first_file["path"], content_no_meta + marker, summary="Python SDK E2E")
|
||
assert_ok(bool(write_result.get("updated_at")), f"scenario/write: updated_at={write_result.get('updated_at')}")
|
||
|
||
# verify
|
||
verify_read = client.read_scenario(first_file["path"])
|
||
assert_ok("Python SDK E2E" in verify_read["content"], "scenario/read verify: marker found")
|
||
else:
|
||
print(" ℹ️ 暂无 scenario 文件,跳过 read/write")
|
||
|
||
# ════════════════════════════════════════════
|
||
# L3 Core (Persona)
|
||
# ════════════════════════════════════════════
|
||
section("L3 Core")
|
||
|
||
# read
|
||
core_read = client.read_core()
|
||
if core_read.get("content"):
|
||
assert_ok(len(core_read["content"]) > 0, f"core/read: content.length={len(core_read['content'])}")
|
||
|
||
# write
|
||
core_marker = f"\n\n<!-- Python SDK E2E marker {TS} -->"
|
||
core_write = client.write_core(core_read["content"] + core_marker)
|
||
assert_ok(bool(core_write.get("updated_at")), f"core/write: updated_at={core_write.get('updated_at')}")
|
||
|
||
# verify
|
||
core_verify = client.read_core()
|
||
assert_ok("Python SDK E2E marker" in core_verify["content"], "core/read verify: marker found")
|
||
else:
|
||
print(" ℹ️ 暂无 persona,跳过 core/write")
|
||
|
||
# ════════════════════════════════════════════
|
||
# Summary
|
||
# ════════════════════════════════════════════
|
||
total = passed + failed
|
||
print("")
|
||
if failed == 0:
|
||
print("\033[42m\033[30m \033[0m")
|
||
print(f"\033[42m\033[30m ✅ ALL {total} TESTS PASSED \033[0m")
|
||
print("\033[42m\033[30m \033[0m")
|
||
else:
|
||
print("\033[41m\033[37m \033[0m")
|
||
print(f"\033[41m\033[37m ❌ {failed} / {total} TESTS FAILED \033[0m")
|
||
print("\033[41m\033[37m \033[0m")
|
||
print("")
|
||
print(" \033[31m┌─── Failures ───────────────────────────────┐\033[0m")
|
||
for f in failures:
|
||
print(f" \033[31m│\033[0m • {f}")
|
||
print(" \033[31m└────────────────────────────────────────────┘\033[0m")
|
||
print("")
|
||
|
||
client.close()
|
||
sys.exit(1 if failed > 0 else 0)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|