From 5d0825511a78c09e2f4b699535e2d1b6ba2c6270 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E6=85=8E=E4=BA=A8?= <2219014054@qq.com> Date: Sun, 17 May 2026 22:19:52 +0800 Subject: [PATCH] fix: include prepend context in gateway recall response MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 袁慎亨 <2219014054@qq.com> --- conftest.py | 55 +++++++++++++++++++ .../memory/memory_tencentdb/__init__.py | 13 ++++- .../tests/test_recall_context_fields.py | 43 +++++++++++++++ src/gateway/server.test.ts | 42 ++++++++++++++ src/gateway/server.ts | 33 ++++++++--- src/gateway/types.ts | 2 + 6 files changed, 179 insertions(+), 9 deletions(-) create mode 100644 conftest.py create mode 100644 hermes-plugin/memory/memory_tencentdb/tests/test_recall_context_fields.py create mode 100644 src/gateway/server.test.ts diff --git a/conftest.py b/conftest.py new file mode 100644 index 0000000..b1bf0d5 --- /dev/null +++ b/conftest.py @@ -0,0 +1,55 @@ +"""Pytest import helpers for the standalone plugin checkout.""" + +from __future__ import annotations + +import importlib +import importlib.util +import os +import pathlib +import sys +import types + + +_PROJECT_ROOT = pathlib.Path(__file__).resolve().parent +_HERMES_PLUGIN_ROOT = _PROJECT_ROOT / "hermes-plugin" +if str(_HERMES_PLUGIN_ROOT) not in sys.path: + sys.path.insert(0, str(_HERMES_PLUGIN_ROOT)) + + +def _add_real_hermes_agent_root() -> None: + candidates = [] + env_root = os.environ.get("HERMES_AGENT_ROOT") + if env_root: + candidates.append(pathlib.Path(env_root)) + candidates.append(_PROJECT_ROOT.parent / "hermes-agent") + + for candidate in candidates: + if (candidate / "agent").is_dir() and str(candidate) not in sys.path: + sys.path.insert(0, str(candidate)) + + +def _real_memory_provider_available() -> bool: + if "agent.memory_provider" in sys.modules: + return True + _add_real_hermes_agent_root() + try: + spec = importlib.util.find_spec("agent.memory_provider") + except ModuleNotFoundError: + return False + if spec is None: + return False + importlib.import_module("agent.memory_provider") + return True + + +if not _real_memory_provider_available(): + agent_module = types.ModuleType("agent") + memory_provider_module = types.ModuleType("agent.memory_provider") + + class MemoryProvider: + pass + + memory_provider_module.MemoryProvider = MemoryProvider + agent_module.memory_provider = memory_provider_module + sys.modules.setdefault("agent", agent_module) + sys.modules["agent.memory_provider"] = memory_provider_module diff --git a/hermes-plugin/memory/memory_tencentdb/__init__.py b/hermes-plugin/memory/memory_tencentdb/__init__.py index 2be6c29..0a03640 100644 --- a/hermes-plugin/memory/memory_tencentdb/__init__.py +++ b/hermes-plugin/memory/memory_tencentdb/__init__.py @@ -275,6 +275,17 @@ def _coerce_limit( return value +def _recall_context_from_response(result: Dict[str, Any]) -> str: + """Prefer split Gateway recall fields, falling back to legacy context.""" + if "appendSystemContext" in result or "prependContext" in result: + parts = [ + result.get("appendSystemContext") or "", + result.get("prependContext") or "", + ] + return "\n\n".join(part for part in parts if part) + return result.get("context", "") or "" + + # --------------------------------------------------------------------------- # Tool schemas # --------------------------------------------------------------------------- @@ -804,7 +815,7 @@ def prefetch(self, query: str, *, session_id: str = "") -> str: session_key=effective_session, user_id=self._user_id, ) - context = result.get("context", "") + context = _recall_context_from_response(result) self._record_success() if context: return f"## memory-tencentdb Memory\n{context}" diff --git a/hermes-plugin/memory/memory_tencentdb/tests/test_recall_context_fields.py b/hermes-plugin/memory/memory_tencentdb/tests/test_recall_context_fields.py new file mode 100644 index 0000000..54b9564 --- /dev/null +++ b/hermes-plugin/memory/memory_tencentdb/tests/test_recall_context_fields.py @@ -0,0 +1,43 @@ +"""Tests for Gateway /recall context field compatibility.""" + +from __future__ import annotations + +from unittest.mock import MagicMock + + +from memory.memory_tencentdb import MemoryTencentdbProvider + + +def _ready_provider() -> MemoryTencentdbProvider: + provider = MemoryTencentdbProvider() + provider._client = MagicMock() + provider._gateway_available = True + provider._session_id = "test-session" + provider._user_id = "test-user" + provider._ensure_alive_for_request = MagicMock(return_value=True) + return provider + + +def test_prefetch_prefers_split_context_fields_over_legacy_context(): + provider = _ready_provider() + provider._client.recall.return_value = { + "appendSystemContext": "stable system context", + "prependContext": "dynamic L1 context", + "context": "stale legacy context", + } + + assert provider.prefetch("hello") == ( + "## memory-tencentdb Memory\n" + "stable system context\n\n" + "dynamic L1 context" + ) + + +def test_prefetch_reads_legacy_context_when_split_fields_absent(): + provider = _ready_provider() + provider._client.recall.return_value = {"context": "legacy context"} + + assert provider.prefetch("hello") == ( + "## memory-tencentdb Memory\n" + "legacy context" + ) diff --git a/src/gateway/server.test.ts b/src/gateway/server.test.ts new file mode 100644 index 0000000..f98cf75 --- /dev/null +++ b/src/gateway/server.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, it } from "vitest"; +import type { RecallResult } from "../core/types.js"; +import { buildRecallResponse } from "./server.js"; + +describe("buildRecallResponse", () => { + it("keeps prepend-only recall context in the legacy context field", () => { + const result: RecallResult = { + prependContext: "L1 dynamic memory", + recalledL1Memories: [ + { content: "L1 dynamic memory", score: 0.91, type: "episodic" }, + ], + recallStrategy: "hybrid", + }; + + expect(buildRecallResponse(result)).toEqual({ + context: "L1 dynamic memory", + prependContext: "L1 dynamic memory", + strategy: "hybrid", + memory_count: 1, + }); + }); + + it("returns split recall fields and joins legacy context as append then prepend", () => { + const result: RecallResult = { + appendSystemContext: "stable system context", + prependContext: "dynamic L1 context", + recalledL1Memories: [ + { content: "first", score: 0.87, type: "episodic" }, + { content: "second", score: 0.74, type: "instruction" }, + ], + recallStrategy: "keyword", + }; + + expect(buildRecallResponse(result)).toEqual({ + context: "stable system context\n\ndynamic L1 context", + appendSystemContext: "stable system context", + prependContext: "dynamic L1 context", + strategy: "keyword", + memory_count: 2, + }); + }); +}); diff --git a/src/gateway/server.ts b/src/gateway/server.ts index bd7d0a0..8175aec 100644 --- a/src/gateway/server.ts +++ b/src/gateway/server.ts @@ -38,7 +38,7 @@ import type { SeedResponse, GatewayErrorResponse, } from "./types.js"; -import type { Logger } from "../core/types.js"; +import type { Logger, RecallResult } from "../core/types.js"; import { validateAndNormalizeRaw, fillTimestamps, SeedValidationError } from "../core/seed/input.js"; import { executeSeed } from "../core/seed/seed-runtime.js"; import type { SeedProgress } from "../core/seed/types.js"; @@ -46,6 +46,23 @@ import type { SeedProgress } from "../core/seed/types.js"; const TAG = "[tdai-gateway]"; const VERSION = "0.1.0"; +function joinRecallContext(appendSystemContext?: string, prependContext?: string): string { + return [appendSystemContext, prependContext] + .filter((part): part is string => Boolean(part)) + .join("\n\n"); +} + +export function buildRecallResponse(result: RecallResult): RecallResponse { + const { appendSystemContext, prependContext } = result; + return { + context: joinRecallContext(appendSystemContext, prependContext), + ...(prependContext !== undefined ? { prependContext } : {}), + ...(appendSystemContext !== undefined ? { appendSystemContext } : {}), + strategy: result.recallStrategy, + memory_count: result.recalledL1Memories?.length ?? 0, + }; +} + // ============================ // Console logger (for standalone gateway — no OpenClaw logger available) // ============================ @@ -238,14 +255,14 @@ export class TdaiGateway { const startMs = Date.now(); const result = await this.core.handleBeforeRecall(body.query, body.session_key); const elapsed = Date.now() - startMs; + const response = buildRecallResponse(result); - this.logger.info(`Recall completed in ${elapsed}ms: context=${(result.appendSystemContext?.length ?? 0)} chars`); - - const response: RecallResponse = { - context: result.appendSystemContext ?? "", - strategy: result.recallStrategy, - memory_count: result.recalledL1Memories?.length ?? 0, - }; + this.logger.info( + `Recall completed in ${elapsed}ms: ` + + `appendSystemContext=${response.appendSystemContext?.length ?? 0} chars, ` + + `prependContext=${response.prependContext?.length ?? 0} chars, ` + + `context=${response.context.length} chars`, + ); sendJson(res, 200, response); } diff --git a/src/gateway/types.ts b/src/gateway/types.ts index 50b2ff4..3741e52 100644 --- a/src/gateway/types.ts +++ b/src/gateway/types.ts @@ -37,6 +37,8 @@ export interface RecallRequest { export interface RecallResponse { context: string; + prependContext?: string; + appendSystemContext?: string; strategy?: string; memory_count?: number; }