refactor: 重构 Agent 模块为 AgentScope,删除旧版 CrewAI/LiteLLM 实现
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
|
||||
from core.agentscope.persistence.user_context_cache import UserContextCache
|
||||
from core.agentscope.schemas.user_context import (
|
||||
UserAgentContext,
|
||||
parse_profile_settings,
|
||||
)
|
||||
|
||||
|
||||
class _FakeRedis:
|
||||
def __init__(self) -> None:
|
||||
self.store: dict[str, dict[str, str]] = {}
|
||||
self.set_store: dict[str, set[str]] = {}
|
||||
self.expire_calls: list[tuple[str, int]] = []
|
||||
self.delete_calls: list[str] = []
|
||||
self.hincrby_calls: list[tuple[str, str, int]] = []
|
||||
|
||||
async def hgetall(self, key: str) -> dict[str, str]:
|
||||
return dict(self.store.get(key, {}))
|
||||
|
||||
async def hset(self, key: str, mapping: dict[str, str]) -> int:
|
||||
self.store[key] = dict(mapping)
|
||||
return 1
|
||||
|
||||
async def hincrby(self, key: str, field: str, amount: int = 1) -> int:
|
||||
self.hincrby_calls.append((key, field, amount))
|
||||
data = self.store.setdefault(key, {})
|
||||
current = int(data.get(field, "0"))
|
||||
next_value = current + amount
|
||||
data[field] = str(next_value)
|
||||
return next_value
|
||||
|
||||
async def expire(self, key: str, seconds: int) -> int:
|
||||
self.expire_calls.append((key, seconds))
|
||||
return 1
|
||||
|
||||
async def delete(self, *keys: str) -> int:
|
||||
for key in keys:
|
||||
self.delete_calls.append(key)
|
||||
self.store.pop(key, None)
|
||||
self.set_store.pop(key, None)
|
||||
return len(keys)
|
||||
|
||||
async def sadd(self, key: str, *values: str) -> int:
|
||||
bucket = self.set_store.setdefault(key, set())
|
||||
before = len(bucket)
|
||||
for value in values:
|
||||
bucket.add(value)
|
||||
return len(bucket) - before
|
||||
|
||||
async def smembers(self, key: str) -> set[str]:
|
||||
return set(self.set_store.get(key, set()))
|
||||
|
||||
|
||||
class _BrokenRedis:
|
||||
async def hgetall(self, key: str) -> dict[str, str]:
|
||||
del key
|
||||
raise RuntimeError("redis down")
|
||||
|
||||
async def hset(self, key: str, mapping: dict[str, str]) -> int:
|
||||
del key, mapping
|
||||
raise RuntimeError("redis down")
|
||||
|
||||
async def hincrby(self, key: str, field: str, amount: int = 1) -> int:
|
||||
del key, field, amount
|
||||
raise RuntimeError("redis down")
|
||||
|
||||
async def expire(self, key: str, seconds: int) -> int:
|
||||
del key, seconds
|
||||
raise RuntimeError("redis down")
|
||||
|
||||
async def delete(self, *keys: str) -> int:
|
||||
del keys
|
||||
raise RuntimeError("redis down")
|
||||
|
||||
async def sadd(self, key: str, *values: str) -> int:
|
||||
del key, values
|
||||
raise RuntimeError("redis down")
|
||||
|
||||
async def smembers(self, key: str) -> set[str]:
|
||||
del key
|
||||
raise RuntimeError("redis down")
|
||||
|
||||
|
||||
def _build_context() -> UserAgentContext:
|
||||
return UserAgentContext(
|
||||
user_id=uuid4(),
|
||||
username="demo-user",
|
||||
bio="demo bio",
|
||||
settings=parse_profile_settings({"preferences": {"ai_language": "en-US"}}),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_user_context_cache_set_and_get_hit() -> None:
|
||||
redis = _FakeRedis()
|
||||
cache = UserContextCache(
|
||||
client=redis,
|
||||
key_prefix="agent:user-context",
|
||||
ttl_seconds=600,
|
||||
max_turns=3,
|
||||
)
|
||||
session_id = uuid4()
|
||||
context = _build_context()
|
||||
|
||||
await cache.set(session_id=session_id, context=context)
|
||||
loaded = await cache.get(session_id=session_id)
|
||||
|
||||
assert loaded is not None
|
||||
assert loaded.user_id == context.user_id
|
||||
assert loaded.username == "demo-user"
|
||||
assert redis.expire_calls == [
|
||||
(f"agent:user-context:{session_id}", 600),
|
||||
(f"agent:user-context:sessions:{context.user_id}", 600),
|
||||
]
|
||||
assert redis.hincrby_calls == [
|
||||
(f"agent:user-context:{session_id}", "turns_used", 1)
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_user_context_cache_invalidate_user_deletes_all_sessions() -> None:
|
||||
redis = _FakeRedis()
|
||||
cache = UserContextCache(
|
||||
client=redis,
|
||||
key_prefix="agent:user-context",
|
||||
ttl_seconds=600,
|
||||
max_turns=3,
|
||||
)
|
||||
context = _build_context()
|
||||
s1 = uuid4()
|
||||
s2 = uuid4()
|
||||
|
||||
await cache.set(session_id=s1, context=context)
|
||||
await cache.set(session_id=s2, context=context)
|
||||
|
||||
deleted = await cache.invalidate_user(user_id=context.user_id)
|
||||
|
||||
assert deleted == 2
|
||||
assert f"agent:user-context:{s1}" in redis.delete_calls
|
||||
assert f"agent:user-context:{s2}" in redis.delete_calls
|
||||
assert f"agent:user-context:sessions:{context.user_id}" in redis.delete_calls
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_user_context_cache_degrades_gracefully_on_redis_error() -> None:
|
||||
cache = UserContextCache(
|
||||
client=_BrokenRedis(),
|
||||
key_prefix="agent:user-context",
|
||||
ttl_seconds=600,
|
||||
max_turns=3,
|
||||
)
|
||||
session_id = uuid4()
|
||||
context = _build_context()
|
||||
|
||||
loaded = await cache.get(session_id=session_id)
|
||||
await cache.set(session_id=session_id, context=context)
|
||||
|
||||
assert loaded is None
|
||||
Reference in New Issue
Block a user