diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/agents/memory/providers/hindsight.py b/offline-backend-20260512/backend/packages/harness/deerflow/agents/memory/providers/hindsight.py index 592c5581..3319bb73 100644 --- a/offline-backend-20260512/backend/packages/harness/deerflow/agents/memory/providers/hindsight.py +++ b/offline-backend-20260512/backend/packages/harness/deerflow/agents/memory/providers/hindsight.py @@ -20,8 +20,8 @@ HTTP 客户端方式接入一个用户自起的 Hindsight Docker 实例 (``local document_id 与轮次计数 ====================== 本 provider 被 ``build_memory_manager`` 每轮重新构造 —— 是无状态的。因此: -- ``document_id`` 由 ``thread_id`` **确定性**派生,同一 thread 的所有轮次 retain - 都落到同一个 Hindsight 文档,累积而非每轮碎片化成新文档。 +- ``document_id`` 每次 ``initialize`` 用带时间戳的新值,每轮对话的 retain 落到 + 各自独立的 Hindsight 文档。 - ``retain_every_n_turns`` 的轮次判定依赖外部传入的 ``turn_index`` (由 ``MemoryMiddleware`` 按对话历史算出);未传时退回进程内 ``_turn_counter``。 @@ -168,14 +168,14 @@ class HindsightProvider(MemoryProvider): return name.strip("-") or "deerflow" def _reset_retain_batch(self) -> None: - """重置 retain 批次状态。 + """重置 retain 批次状态 (新的 document_id + 轮次清零)。 - ``document_id`` 由 ``thread_id`` **确定性**派生 —— provider 虽然被每轮 - 重新构造,同一 thread 的所有轮次仍落到同一个 Hindsight 文档,累积成一份 - 而不是碎片化成一轮一个文档。无 thread 时 (例如 memory 工具镜像) 用统一 - 的兜底 id。轮次计数清零,仅作为 ``turn_index`` 缺失时的退路。 + 每次 ``initialize`` 用带时间戳的新 ``document_id`` —— 每轮对话的 retain + 落到各自独立的 Hindsight 文档。轮次计数清零,仅作为 ``turn_index`` 缺失 + 时的退路。 """ - self._document_id = f"thread-{self._thread_id}" if self._thread_id else "thread-default" + ts = datetime.now().strftime("%Y%m%d_%H%M%S_%f") + self._document_id = f"{self._thread_id or 'thread'}-{ts}" self._turn_counter = 0 def shutdown(self) -> None: diff --git a/offline-backend-20260512/backend/tests/test_hindsight_provider.py b/offline-backend-20260512/backend/tests/test_hindsight_provider.py index aa7d0449..da1cfe1b 100644 --- a/offline-backend-20260512/backend/tests/test_hindsight_provider.py +++ b/offline-backend-20260512/backend/tests/test_hindsight_provider.py @@ -108,16 +108,7 @@ class TestBankResolution: class TestDocumentId: - """document_id 必须由 thread_id 确定性派生 —— provider 每轮重建后仍稳定。""" - - def test_deterministic_from_thread_id(self): - # 同一 thread,两个独立构造的 provider 必须算出同一个 document_id - p1 = HindsightProvider(HindsightMemoryConfig(api_url="http://x")) - p1.initialize(user_id="u", agent_id="a", thread_id="t-123", base_dir="") - p2 = HindsightProvider(HindsightMemoryConfig(api_url="http://x")) - p2.initialize(user_id="u", agent_id="a", thread_id="t-123", base_dir="") - assert p1._document_id == p2._document_id - assert "t-123" in p1._document_id + """document_id 每次 initialize 带时间戳 —— 每轮对话落到各自独立的文档。""" def test_different_threads_get_different_doc(self): p1 = HindsightProvider(HindsightMemoryConfig(api_url="http://x")) @@ -126,28 +117,34 @@ class TestDocumentId: p2.initialize(user_id="u", agent_id="a", thread_id="t-2", base_dir="") assert p1._document_id != p2._document_id + def test_doc_id_carries_thread_prefix(self): + p = HindsightProvider(HindsightMemoryConfig(api_url="http://x")) + p.initialize(user_id="u", agent_id="a", thread_id="t-123", base_dir="") + # document_id 形如 "t-123-{时间戳}" + assert p._document_id.startswith("t-123-") + def test_no_thread_id_uses_fallback(self): p = HindsightProvider(HindsightMemoryConfig(api_url="http://x")) p.initialize(user_id="u", agent_id="a", thread_id="", base_dir="") - assert p._document_id == "thread-default" + # 无 thread 时前缀塌缩成 "thread-" + assert p._document_id.startswith("thread-") def test_retain_writes_thread_document_id(self): - # sync_turn 落盘时带的 document_id 应是 thread 确定性 id,不含时间戳 + # sync_turn 落盘时带的 document_id 形如 "t1-{时间戳}" client = _FakeClient() provider = _make_provider(client) # thread_id="t1" provider.sync_turn("用户说啥", "助手答啥") - assert client.retained[0]["document_id"] == "thread-t1" + assert client.retained[0]["document_id"].startswith("t1-") - def test_recreated_providers_share_one_document(self): - # 模拟真实场景:同一 thread 连续 3 轮,每轮 provider 重建, - # 三轮 retain 必须落到同一个文档。 + def test_recreated_providers_get_distinct_documents(self): + # 同一 thread 连续 3 轮、每轮 provider 重建 —— 各自落到独立文档 doc_ids: set[str] = set() for turn in range(1, 4): client = _FakeClient() provider = _make_provider(client) # 每轮重建,thread_id 固定 t1 provider.sync_turn(f"u{turn}", f"a{turn}") doc_ids.add(client.retained[0]["document_id"]) - assert doc_ids == {"thread-t1"} + assert len(doc_ids) == 3 class TestPrefetchRecall: @@ -334,7 +331,6 @@ class TestSessionSwitch: assert provider._thread_id == "t2" assert provider._turn_counter == 0 assert provider._document_id != old_doc - assert provider._document_id == "thread-t2" class TestMisc: