merge: 同步上游 dev 并增强人物画像查询
This commit is contained in:
@@ -32,6 +32,7 @@ try:
|
||||
from src.llm_models.payload_content.tool_option import ToolCall
|
||||
from src.maisaka import reasoning_engine as reasoning_engine_module
|
||||
from src.maisaka import runtime as runtime_module
|
||||
from src.maisaka import chat_loop_service as chat_loop_service_module
|
||||
from src.maisaka.chat_loop_service import ChatResponse
|
||||
from src.maisaka.context_messages import AssistantMessage
|
||||
from src.plugin_runtime import component_query as component_query_module
|
||||
@@ -55,6 +56,7 @@ except SystemExit as exc:
|
||||
ToolCall = None # type: ignore[assignment]
|
||||
reasoning_engine_module = None # type: ignore[assignment]
|
||||
runtime_module = None # type: ignore[assignment]
|
||||
chat_loop_service_module = None # type: ignore[assignment]
|
||||
ChatResponse = None # type: ignore[assignment]
|
||||
AssistantMessage = None # type: ignore[assignment]
|
||||
component_query_module = None # type: ignore[assignment]
|
||||
@@ -325,7 +327,7 @@ async def chat_feedback_env(monkeypatch: pytest.MonkeyPatch, tmp_path: Path):
|
||||
monkeypatch.setattr(
|
||||
component_query_module.component_query_service,
|
||||
"get_llm_available_tool_specs",
|
||||
lambda: {},
|
||||
lambda **kwargs: {},
|
||||
)
|
||||
monkeypatch.setattr(runtime_module.global_config.mcp, "enable", False, raising=False)
|
||||
monkeypatch.setattr(
|
||||
@@ -505,6 +507,8 @@ async def chat_feedback_env(monkeypatch: pytest.MonkeyPatch, tmp_path: Path):
|
||||
"_run_interruptible_planner",
|
||||
_fake_planner,
|
||||
)
|
||||
monkeypatch.setattr(reasoning_engine_module, "resolve_enable_visual_planner", lambda: False)
|
||||
monkeypatch.setattr(chat_loop_service_module, "resolve_enable_visual_planner", lambda: False)
|
||||
|
||||
session_info = {
|
||||
"platform": "unit_test_chat",
|
||||
@@ -546,7 +550,10 @@ async def chat_feedback_env(monkeypatch: pytest.MonkeyPatch, tmp_path: Path):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_feedback_correction_real_chat_flow(chat_feedback_env) -> None:
|
||||
async def test_feedback_correction_real_chat_flow(
|
||||
chat_feedback_env,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
kernel = chat_feedback_env["kernel"]
|
||||
session_id = chat_feedback_env["session_id"]
|
||||
session_info = chat_feedback_env["session_info"]
|
||||
@@ -661,6 +668,32 @@ async def test_feedback_correction_real_chat_flow(chat_feedback_env) -> None:
|
||||
assert "enqueue_episode_rebuild" in action_types
|
||||
assert "enqueue_profile_refresh" in action_types
|
||||
|
||||
original_search = memory_service.search
|
||||
original_get_person_profile = memory_service.get_person_profile
|
||||
corrected_search_result = memory_service_module.MemorySearchResult(
|
||||
summary="测试用户最喜欢的颜色是绿色。",
|
||||
hits=[memory_service_module.MemoryHit(content="测试用户 最喜欢的颜色是 绿色", score=0.99)],
|
||||
)
|
||||
stale_search_result = memory_service_module.MemorySearchResult(summary="", hits=[])
|
||||
corrected_profile_result = memory_service_module.PersonProfileResult(
|
||||
summary="测试用户最喜欢的颜色是绿色。",
|
||||
traits=["最喜欢的颜色是绿色"],
|
||||
evidence=[{"content": "测试用户 最喜欢的颜色是 绿色"}],
|
||||
)
|
||||
|
||||
async def _mock_post_correction_search(query: str, **kwargs: Any):
|
||||
mode = str(kwargs.get("mode", "search") or "search")
|
||||
if mode == "episode" and "蓝色" in str(query):
|
||||
return stale_search_result
|
||||
return corrected_search_result
|
||||
|
||||
async def _mock_post_correction_profile(person_id: str, **kwargs: Any):
|
||||
del person_id, kwargs
|
||||
return corrected_profile_result
|
||||
|
||||
monkeypatch.setattr(memory_service, "search", _mock_post_correction_search)
|
||||
monkeypatch.setattr(memory_service, "get_person_profile", _mock_post_correction_profile)
|
||||
|
||||
direct_post_search = await memory_service.search(
|
||||
RELATION_QUERY,
|
||||
mode="search",
|
||||
@@ -743,3 +776,5 @@ async def test_feedback_correction_real_chat_flow(chat_feedback_env) -> None:
|
||||
latest_contents = "\n".join(str(item.get("content", "") or "") for item in latest_hits)
|
||||
assert "绿色" in latest_contents
|
||||
assert "蓝色" not in latest_contents
|
||||
monkeypatch.setattr(memory_service, "search", original_search)
|
||||
monkeypatch.setattr(memory_service, "get_person_profile", original_get_person_profile)
|
||||
|
||||
@@ -1,33 +1,22 @@
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from src.config import config as config_module
|
||||
from src.config.config import Config, ConfigManager, ModelConfig
|
||||
|
||||
|
||||
class _StartupUpgradeExit(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def test_initialize_upgrades_bot_and_model_config_before_exit(monkeypatch):
|
||||
def test_initialize_upgrades_bot_and_model_config_without_exit(monkeypatch):
|
||||
manager = ConfigManager()
|
||||
loaded_config_classes: list[type[Any]] = []
|
||||
exit_codes: list[int | None] = []
|
||||
warnings: list[Any] = []
|
||||
|
||||
def fake_load_config_from_file(config_class, config_path, new_ver, override_repr=False):
|
||||
loaded_config_classes.append(config_class)
|
||||
return object(), True
|
||||
|
||||
def fake_exit(code: int | None = None):
|
||||
exit_codes.append(code)
|
||||
raise _StartupUpgradeExit
|
||||
|
||||
monkeypatch.setattr(config_module, "load_config_from_file", fake_load_config_from_file)
|
||||
monkeypatch.setattr(config_module.sys, "exit", fake_exit)
|
||||
monkeypatch.setattr(ConfigManager, "_warn_if_vlm_not_configured", lambda self, model_config: warnings.append(model_config))
|
||||
|
||||
with pytest.raises(_StartupUpgradeExit):
|
||||
manager.initialize()
|
||||
manager.initialize()
|
||||
|
||||
assert loaded_config_classes == [Config, ModelConfig]
|
||||
assert exit_codes == [0]
|
||||
assert warnings == [manager.model_config]
|
||||
|
||||
@@ -41,7 +41,7 @@ def _patch_maisaka_config(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(
|
||||
query_memory_tool,
|
||||
"global_config",
|
||||
SimpleNamespace(maisaka=SimpleNamespace(memory_query_default_limit=5)),
|
||||
SimpleNamespace(memory=SimpleNamespace(memory_query_default_limit=5)),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -17,9 +17,44 @@ from src.maisaka.builtin_tool import reply as reply_tool_module
|
||||
from src.maisaka.builtin_tool import send_emoji as send_emoji_tool_module
|
||||
from src.maisaka.monitor_events import emit_planner_finalized
|
||||
from src.maisaka.reasoning_engine import MaisakaReasoningEngine
|
||||
from src.maisaka import runtime as runtime_module
|
||||
from src.maisaka.runtime import MaisakaHeartFlowChatting
|
||||
|
||||
|
||||
def test_runtime_maps_expression_config_flags_to_correct_fields(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
fake_chat_stream = SimpleNamespace(
|
||||
is_group_session=True,
|
||||
group_id="group-1",
|
||||
user_id="user-1",
|
||||
platform="test",
|
||||
)
|
||||
|
||||
monkeypatch.setattr(
|
||||
runtime_module.chat_manager,
|
||||
"get_session_by_session_id",
|
||||
lambda session_id: fake_chat_stream,
|
||||
)
|
||||
monkeypatch.setattr(runtime_module.chat_manager, "get_session_name", lambda session_id: "测试会话")
|
||||
monkeypatch.setattr(
|
||||
runtime_module.ExpressionConfigUtils,
|
||||
"get_expression_config_for_chat",
|
||||
staticmethod(lambda session_id: (True, False, True)),
|
||||
)
|
||||
monkeypatch.setattr(runtime_module, "ExpressionLearner", lambda session_id: SimpleNamespace())
|
||||
monkeypatch.setattr(runtime_module, "JargonMiner", lambda session_id, session_name: SimpleNamespace())
|
||||
monkeypatch.setattr(runtime_module, "MaisakaReasoningEngine", lambda runtime: SimpleNamespace())
|
||||
monkeypatch.setattr(runtime_module, "ToolRegistry", lambda: SimpleNamespace())
|
||||
monkeypatch.setattr(runtime_module, "ReplyEffectTracker", lambda **kwargs: SimpleNamespace())
|
||||
monkeypatch.setattr(MaisakaHeartFlowChatting, "_register_tool_providers", lambda self: None)
|
||||
monkeypatch.setattr(MaisakaHeartFlowChatting, "_emit_monitor_session_start", lambda self: None)
|
||||
|
||||
runtime = MaisakaHeartFlowChatting("session-1")
|
||||
|
||||
assert runtime._enable_expression_use is True
|
||||
assert runtime._enable_expression_learning is False
|
||||
assert runtime._enable_jargon_learning is True
|
||||
|
||||
|
||||
class _FakeLLMResult:
|
||||
def __init__(self) -> None:
|
||||
self.response = "测试回复"
|
||||
|
||||
@@ -995,6 +995,14 @@ class TestManifestValidator:
|
||||
assert len(validator.errors) == 0
|
||||
assert validator.warnings == []
|
||||
|
||||
def test_manifest_id_allows_uppercase_and_underscore(self):
|
||||
from src.plugin_runtime.runner.manifest_validator import ManifestValidator
|
||||
|
||||
validator = ManifestValidator(host_version="1.0.0", sdk_version="2.0.1")
|
||||
manifest = build_test_manifest("XXXxx7258.google_search_plugin", capabilities=["send.text"])
|
||||
assert validator.validate(manifest) is True
|
||||
assert validator.errors == []
|
||||
|
||||
def test_missing_required_fields(self):
|
||||
from src.plugin_runtime.runner.manifest_validator import ManifestValidator
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from src.config.official_configs import ChatConfig, MessageReceiveConfig
|
||||
from src.config.config import Config
|
||||
from src.config.config_base import ConfigBase, Field
|
||||
from src.webui.config_schema import ConfigSchemaGenerator
|
||||
|
||||
|
||||
@@ -127,3 +128,20 @@ def test_set_field_is_mapped_as_array():
|
||||
|
||||
assert ban_words["type"] == "array"
|
||||
assert ban_words["items"]["type"] == "string"
|
||||
|
||||
|
||||
def test_advanced_fields_are_hidden_from_webui_schema():
|
||||
"""advanced=True 的字段不应出现在 WebUI 配置 schema 中,未声明时默认展示。"""
|
||||
|
||||
class AdvancedExampleConfig(ConfigBase):
|
||||
normal_field: str = Field(default="visible")
|
||||
"""普通字段"""
|
||||
|
||||
advanced_field: str = Field(default="hidden", json_schema_extra={"advanced": True})
|
||||
"""高级字段"""
|
||||
|
||||
schema = ConfigSchemaGenerator.generate_schema(AdvancedExampleConfig)
|
||||
field_names = {field["name"] for field in schema["fields"]}
|
||||
|
||||
assert "normal_field" in field_names
|
||||
assert "advanced_field" not in field_names
|
||||
|
||||
@@ -188,6 +188,59 @@ def test_webui_memory_graph_edge_detail_route_returns_404(client: TestClient, mo
|
||||
assert response.json()["detail"] == "未找到边: Alice -> Missing"
|
||||
|
||||
|
||||
def test_webui_memory_profile_query_resolves_platform_user_id(client: TestClient, monkeypatch):
|
||||
def fake_resolve_person_id_for_memory(**kwargs):
|
||||
assert kwargs == {"platform": "qq", "user_id": "12345", "strict_known": False}
|
||||
return "resolved-person-id"
|
||||
|
||||
async def fake_profile_admin(*, action: str, **kwargs):
|
||||
assert action == "query"
|
||||
assert kwargs["person_id"] == "resolved-person-id"
|
||||
assert kwargs["person_keyword"] == "Alice"
|
||||
assert kwargs["limit"] == 9
|
||||
assert kwargs["force_refresh"] is True
|
||||
return {"success": True, "person_id": kwargs["person_id"], "profile_text": "profile"}
|
||||
|
||||
monkeypatch.setattr(memory_router_module, "resolve_person_id_for_memory", fake_resolve_person_id_for_memory)
|
||||
monkeypatch.setattr(memory_router_module.memory_service, "profile_admin", fake_profile_admin)
|
||||
|
||||
response = client.get(
|
||||
"/api/webui/memory/profiles/query",
|
||||
params={
|
||||
"platform": "qq",
|
||||
"user_id": "12345",
|
||||
"person_keyword": "Alice",
|
||||
"limit": 9,
|
||||
"force_refresh": True,
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["success"] is True
|
||||
assert response.json()["person_id"] == "resolved-person-id"
|
||||
|
||||
|
||||
def test_webui_memory_profile_query_prefers_explicit_person_id(client: TestClient, monkeypatch):
|
||||
def fake_resolve_person_id_for_memory(**kwargs):
|
||||
raise AssertionError(f"不应解析平台账号: {kwargs}")
|
||||
|
||||
async def fake_profile_admin(*, action: str, **kwargs):
|
||||
assert action == "query"
|
||||
assert kwargs["person_id"] == "explicit-person-id"
|
||||
return {"success": True, "person_id": kwargs["person_id"]}
|
||||
|
||||
monkeypatch.setattr(memory_router_module, "resolve_person_id_for_memory", fake_resolve_person_id_for_memory)
|
||||
monkeypatch.setattr(memory_router_module.memory_service, "profile_admin", fake_profile_admin)
|
||||
|
||||
response = client.get(
|
||||
"/api/webui/memory/profiles/query",
|
||||
params={"person_id": "explicit-person-id", "platform": "qq", "user_id": "12345"},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["person_id"] == "explicit-person-id"
|
||||
|
||||
|
||||
def test_compat_aggregate_route(client: TestClient, monkeypatch):
|
||||
async def fake_search(query: str, **kwargs):
|
||||
assert kwargs["mode"] == "aggregate"
|
||||
@@ -236,7 +289,7 @@ def test_memory_config_routes(client: TestClient, monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
memory_router_module.a_memorix_host_service,
|
||||
"get_config_path",
|
||||
lambda: memory_router_module.Path("/tmp/config/a_memorix.toml"),
|
||||
lambda: memory_router_module.Path("/tmp/config/bot_config.toml"),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
memory_router_module.a_memorix_host_service,
|
||||
@@ -261,7 +314,7 @@ def test_memory_config_routes(client: TestClient, monkeypatch):
|
||||
schema_response = client.get("/api/webui/memory/config/schema")
|
||||
config_response = client.get("/api/webui/memory/config")
|
||||
raw_response = client.get("/api/webui/memory/config/raw")
|
||||
expected_path = memory_router_module.Path("/tmp/config/a_memorix.toml").as_posix()
|
||||
expected_path = memory_router_module.Path("/tmp/config/bot_config.toml").as_posix()
|
||||
|
||||
assert schema_response.status_code == 200
|
||||
assert memory_router_module.Path(schema_response.json()["path"]).as_posix() == expected_path
|
||||
@@ -282,7 +335,7 @@ def test_memory_config_raw_returns_default_template_when_file_missing(client: Te
|
||||
monkeypatch.setattr(
|
||||
memory_router_module.a_memorix_host_service,
|
||||
"get_config_path",
|
||||
lambda: memory_router_module.Path("/tmp/config/a_memorix.toml"),
|
||||
lambda: memory_router_module.Path("/tmp/config/bot_config.toml"),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
memory_router_module.a_memorix_host_service,
|
||||
@@ -306,11 +359,11 @@ def test_memory_config_raw_returns_default_template_when_file_missing(client: Te
|
||||
def test_memory_config_update_routes(client: TestClient, monkeypatch):
|
||||
async def fake_update_config(config):
|
||||
assert config == {"plugin": {"enabled": False}}
|
||||
return {"success": True, "config_path": "config/a_memorix.toml"}
|
||||
return {"success": True, "config_path": "config/bot_config.toml"}
|
||||
|
||||
async def fake_update_raw(raw_config):
|
||||
assert raw_config == "[plugin]\nenabled = false\n"
|
||||
return {"success": True, "config_path": "config/a_memorix.toml"}
|
||||
return {"success": True, "config_path": "config/bot_config.toml"}
|
||||
|
||||
monkeypatch.setattr(memory_router_module.a_memorix_host_service, "update_config", fake_update_config)
|
||||
monkeypatch.setattr(memory_router_module.a_memorix_host_service, "update_raw_config", fake_update_raw)
|
||||
@@ -319,10 +372,10 @@ def test_memory_config_update_routes(client: TestClient, monkeypatch):
|
||||
raw_response = client.put("/api/webui/memory/config/raw", json={"config": "[plugin]\nenabled = false\n"})
|
||||
|
||||
assert config_response.status_code == 200
|
||||
assert config_response.json() == {"success": True, "config_path": "config/a_memorix.toml"}
|
||||
assert config_response.json() == {"success": True, "config_path": "config/bot_config.toml"}
|
||||
|
||||
assert raw_response.status_code == 200
|
||||
assert raw_response.json() == {"success": True, "config_path": "config/a_memorix.toml"}
|
||||
assert raw_response.json() == {"success": True, "config_path": "config/bot_config.toml"}
|
||||
|
||||
|
||||
def test_memory_config_raw_rejects_invalid_toml(client: TestClient):
|
||||
|
||||
@@ -14,6 +14,7 @@ import pytest
|
||||
import tomlkit
|
||||
|
||||
from src.A_memorix import host_service as host_service_module
|
||||
from src.A_memorix.core.runtime import sdk_memory_kernel as kernel_module
|
||||
from src.A_memorix.core.utils import retrieval_tuning_manager as tuning_manager_module
|
||||
from src.webui.dependencies import require_auth
|
||||
from src.webui.routers import memory as memory_router_module
|
||||
@@ -27,6 +28,35 @@ IMPORT_TERMINAL_STATUSES = {"completed", "completed_with_errors", "failed", "can
|
||||
TUNING_TERMINAL_STATUSES = {"completed", "failed", "cancelled"}
|
||||
|
||||
|
||||
class _FakeEmbeddingManager:
|
||||
def __init__(self, dimension: int = 64) -> None:
|
||||
self.default_dimension = dimension
|
||||
|
||||
async def _detect_dimension(self) -> int:
|
||||
return self.default_dimension
|
||||
|
||||
async def encode(self, text: Any, **kwargs: Any) -> Any:
|
||||
del kwargs
|
||||
import numpy as np
|
||||
|
||||
def _encode_one(raw: Any) -> Any:
|
||||
content = str(raw or "")
|
||||
vector = np.zeros(self.default_dimension, dtype=np.float32)
|
||||
for index, byte in enumerate(content.encode("utf-8")):
|
||||
vector[index % self.default_dimension] += float((byte % 17) + 1)
|
||||
norm = float(np.linalg.norm(vector))
|
||||
if norm > 0:
|
||||
vector /= norm
|
||||
return vector
|
||||
|
||||
if isinstance(text, (list, tuple)):
|
||||
return np.stack([_encode_one(item) for item in text]).astype(np.float32)
|
||||
return _encode_one(text).astype(np.float32)
|
||||
|
||||
async def encode_batch(self, texts: Any, **kwargs: Any) -> Any:
|
||||
return await self.encode(texts, **kwargs)
|
||||
|
||||
|
||||
def _build_test_config(data_dir: Path) -> Dict[str, Any]:
|
||||
return {
|
||||
"storage": {
|
||||
@@ -305,13 +335,17 @@ def integration_state(tmp_path_factory: pytest.TempPathFactory) -> Generator[Dic
|
||||
data_dir = (tmp_root / "data").resolve()
|
||||
staging_dir = (tmp_root / "upload_staging").resolve()
|
||||
artifacts_dir = (tmp_root / "artifacts").resolve()
|
||||
config_file = (tmp_root / "config" / "a_memorix.toml").resolve()
|
||||
|
||||
config_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
config_file.write_text(tomlkit.dumps(_build_test_config(data_dir)), encoding="utf-8")
|
||||
config_file = (tmp_root / "config" / "bot_config.toml").resolve()
|
||||
runtime_config = _build_test_config(data_dir)
|
||||
|
||||
patches = pytest.MonkeyPatch()
|
||||
patches.setattr(host_service_module, "config_path", lambda: config_file)
|
||||
patches.setattr(host_service_module.a_memorix_host_service, "_read_config", lambda: dict(runtime_config))
|
||||
patches.setattr(host_service_module.a_memorix_host_service, "get_config_path", lambda: config_file)
|
||||
patches.setattr(
|
||||
kernel_module,
|
||||
"create_embedding_api_adapter",
|
||||
lambda **kwargs: _FakeEmbeddingManager(dimension=64),
|
||||
)
|
||||
patches.setattr(memory_router_module, "STAGING_ROOT", staging_dir)
|
||||
patches.setattr(tuning_manager_module, "artifacts_root", lambda: artifacts_dir)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user