feat(memory-gateway): 引入 Memory Gateway 配置、凭据存储和服务编排

* 新增 MemoryGatewayConfig 和 MemoryConfig dataclass,用于配置管理。
* 实现 MemoryGatewayUserCredential 和 MemoryGatewayCredentialStore,用于处理用户凭据。
* 创建 MemoryGatewayService,用于管理与 Memory Gateway 的交互。
* 开发用于记忆设置的 JSON 配置文件。
* 增强单元测试,覆盖新功能,包括凭据存储和服务行为。
* 更新 entrypoint 和实例创建脚本,以初始化 Memory Gateway 用户存储。
This commit is contained in:
2026-06-16 13:36:18 +08:00
parent e9e57bdb07
commit 269661afff
26 changed files with 788 additions and 160 deletions

View File

@ -12,6 +12,39 @@ from beaver.interfaces.web.app import create_app, _reload_agent_config
from beaver.services.agent_service import AgentService
def test_load_config_reads_shared_memory_config(tmp_path, monkeypatch: pytest.MonkeyPatch) -> None:
config_path = tmp_path / "config.json"
config_path.write_text(json.dumps({}), encoding="utf-8")
memory_config_path = tmp_path / "memory-config.json"
memory_config_path.write_text(
json.dumps(
{
"memory": {
"mode": "hybrid",
"gateway": {
"baseUrl": "http://172.19.207.37:8010",
"appId": "default",
"projectId": "default",
"scope": ["current_chat", "resources", "all_user_memory"],
"topK": 8,
"timeoutSeconds": 10,
},
}
}
),
encoding="utf-8",
)
monkeypatch.setenv("BEAVER_MEMORY_CONFIG_PATH", str(memory_config_path))
config = load_config(config_path=config_path)
assert config.memory.mode == "hybrid"
assert config.memory.gateway.base_url == "http://172.19.207.37:8010"
assert config.memory.gateway.scope == ["current_chat", "resources", "all_user_memory"]
assert config.memory.gateway.top_k == 8
assert config.memory.gateway.timeout_seconds == 10
def test_load_config_reads_current_instance_shape(tmp_path) -> None:
config_path = tmp_path / "config.json"
config_path.write_text(
@ -477,17 +510,25 @@ def test_load_config_adds_managed_local_mcp_servers(tmp_path) -> None:
assert "beaver.interfaces.mcp.tools_server" in local.args
def test_missing_memory_config_defaults_to_implicit_hybrid(tmp_path) -> None:
def test_missing_memory_config_defaults_to_implicit_hybrid(
tmp_path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setenv("BEAVER_MEMORY_CONFIG_PATH", str(tmp_path / "missing-memory.json"))
config = load_config(config_path=tmp_path / "missing.json")
assert config.memory.mode == "hybrid"
assert config.memory.explicit is False
assert config.memory.gateway.scope == ["current_chat", "resources"]
assert config.memory.gateway.scope == ["current_chat", "resources", "all_user_memory"]
def test_load_config_reads_explicit_curated_memory_mode(tmp_path) -> None:
def test_load_config_reads_explicit_curated_memory_mode(
tmp_path, monkeypatch: pytest.MonkeyPatch
) -> None:
config_path = tmp_path / "config.json"
config_path.write_text(json.dumps({"memory": {"mode": "curated"}}), encoding="utf-8")
config_path.write_text(json.dumps({}), encoding="utf-8")
memory_config_path = tmp_path / "memory-config.json"
memory_config_path.write_text(json.dumps({"memory": {"mode": "curated"}}), encoding="utf-8")
monkeypatch.setenv("BEAVER_MEMORY_CONFIG_PATH", str(memory_config_path))
config = load_config(config_path=config_path)
@ -495,17 +536,19 @@ def test_load_config_reads_explicit_curated_memory_mode(tmp_path) -> None:
assert config.memory.explicit is True
def test_load_config_reads_explicit_hybrid_gateway_settings(tmp_path) -> None:
def test_load_config_reads_explicit_hybrid_gateway_settings(
tmp_path, monkeypatch: pytest.MonkeyPatch
) -> None:
config_path = tmp_path / "config.json"
config_path.write_text(
config_path.write_text(json.dumps({}), encoding="utf-8")
memory_config_path = tmp_path / "memory-config.json"
memory_config_path.write_text(
json.dumps(
{
"memory": {
"mode": "hybrid",
"gateway": {
"baseUrl": "http://127.0.0.1:8010",
"userId": "gateway-user",
"userKey": "uk_secret",
"appId": "beaver",
"projectId": "sandbox",
"scope": ["current_chat", "resources"],
@ -517,14 +560,13 @@ def test_load_config_reads_explicit_hybrid_gateway_settings(tmp_path) -> None:
),
encoding="utf-8",
)
monkeypatch.setenv("BEAVER_MEMORY_CONFIG_PATH", str(memory_config_path))
config = load_config(config_path=config_path)
assert config.memory.mode == "hybrid"
assert config.memory.explicit is True
assert config.memory.gateway.base_url == "http://127.0.0.1:8010"
assert config.memory.gateway.user_id == "gateway-user"
assert config.memory.gateway.user_key == "uk_secret"
assert config.memory.gateway.app_id == "beaver"
assert config.memory.gateway.project_id == "sandbox"
assert config.memory.gateway.scope == ["current_chat", "resources"]
@ -532,41 +574,33 @@ def test_load_config_reads_explicit_hybrid_gateway_settings(tmp_path) -> None:
assert config.memory.gateway.timeout_seconds == 12.5
def test_explicit_hybrid_requires_gateway_credentials_without_leaking_secret(tmp_path) -> None:
def test_explicit_hybrid_requires_gateway_base_url(tmp_path, monkeypatch: pytest.MonkeyPatch) -> None:
config_path = tmp_path / "config.json"
config_path.write_text(
json.dumps(
{
"memory": {
"mode": "hybrid",
"gateway": {
"baseUrl": "http://127.0.0.1:8010",
"userKey": "uk_super_secret",
},
}
}
),
config_path.write_text(json.dumps({}), encoding="utf-8")
memory_config_path = tmp_path / "memory-config.json"
memory_config_path.write_text(
json.dumps({"memory": {"mode": "hybrid", "gateway": {"appId": "beaver"}}}),
encoding="utf-8",
)
monkeypatch.setenv("BEAVER_MEMORY_CONFIG_PATH", str(memory_config_path))
with pytest.raises(ValueError) as exc_info:
load_config(config_path=config_path)
assert "userId" in str(exc_info.value)
assert "uk_super_secret" not in str(exc_info.value)
assert "baseUrl" in str(exc_info.value)
def test_hybrid_memory_rejects_unknown_scope(tmp_path) -> None:
def test_hybrid_memory_rejects_unknown_scope(tmp_path, monkeypatch: pytest.MonkeyPatch) -> None:
config_path = tmp_path / "config.json"
config_path.write_text(
config_path.write_text(json.dumps({}), encoding="utf-8")
memory_config_path = tmp_path / "memory-config.json"
memory_config_path.write_text(
json.dumps(
{
"memory": {
"mode": "hybrid",
"gateway": {
"baseUrl": "http://127.0.0.1:8010",
"userId": "gateway-user",
"userKey": "uk_secret",
"scope": ["current_chat", "unknown"],
},
}
@ -574,22 +608,23 @@ def test_hybrid_memory_rejects_unknown_scope(tmp_path) -> None:
),
encoding="utf-8",
)
monkeypatch.setenv("BEAVER_MEMORY_CONFIG_PATH", str(memory_config_path))
with pytest.raises(ValueError, match="scope"):
load_config(config_path=config_path)
def test_hybrid_memory_rejects_empty_scope(tmp_path) -> None:
def test_hybrid_memory_rejects_empty_scope(tmp_path, monkeypatch: pytest.MonkeyPatch) -> None:
config_path = tmp_path / "config.json"
config_path.write_text(
config_path.write_text(json.dumps({}), encoding="utf-8")
memory_config_path = tmp_path / "memory-config.json"
memory_config_path.write_text(
json.dumps(
{
"memory": {
"mode": "hybrid",
"gateway": {
"baseUrl": "http://127.0.0.1:8010",
"userId": "gateway-user",
"userKey": "uk_secret",
"scope": [],
},
}
@ -597,6 +632,7 @@ def test_hybrid_memory_rejects_empty_scope(tmp_path) -> None:
),
encoding="utf-8",
)
monkeypatch.setenv("BEAVER_MEMORY_CONFIG_PATH", str(memory_config_path))
with pytest.raises(ValueError, match="scope"):
load_config(config_path=config_path)
@ -610,18 +646,21 @@ def test_hybrid_memory_rejects_empty_scope(tmp_path) -> None:
({"timeoutSeconds": 0}, "timeoutSeconds"),
],
)
def test_hybrid_memory_rejects_invalid_limits(tmp_path, gateway_override, expected_error) -> None:
def test_hybrid_memory_rejects_invalid_limits(
tmp_path, gateway_override, expected_error, monkeypatch: pytest.MonkeyPatch
) -> None:
config_path = tmp_path / "config.json"
config_path.write_text(json.dumps({}), encoding="utf-8")
gateway = {
"baseUrl": "http://127.0.0.1:8010",
"userId": "gateway-user",
"userKey": "uk_secret",
**gateway_override,
}
config_path.write_text(
memory_config_path = tmp_path / "memory-config.json"
memory_config_path.write_text(
json.dumps({"memory": {"mode": "hybrid", "gateway": gateway}}),
encoding="utf-8",
)
monkeypatch.setenv("BEAVER_MEMORY_CONFIG_PATH", str(memory_config_path))
with pytest.raises(ValueError, match=expected_error):
load_config(config_path=config_path)