diff --git a/backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/retrieval.py b/backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/retrieval.py index dca66aa7d3..a619f8e0d0 100644 --- a/backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/retrieval.py +++ b/backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/retrieval.py @@ -578,11 +578,13 @@ class FTS5RetrievalAdapter: payload = dict(fact) payload["scope"] = {"userId": scope.get("userId"), "agentName": scope.get("agentName")} source = payload.get("source") + confidence = payload.get("confidence") return { "fact_id": self._document_id(fact_id, scope), "content": content, "category": str(payload.get("category") or "context"), - "confidence": float(payload.get("confidence") or 0.5), + # 0.0 is a persisted confidence storage._normalize_fact accepts; only unset/null defaults. + "confidence": 0.5 if confidence is None else float(confidence), "created_at": payload.get("createdAt") if isinstance(payload.get("createdAt"), str) else None, "scope_user": scope_user, "scope_agent": scope_agent, diff --git a/backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/storage.py b/backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/storage.py index f79233fbb8..490beb971c 100644 --- a/backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/storage.py +++ b/backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/storage.py @@ -1822,7 +1822,10 @@ class FileMemoryStorage(MemoryStorage): for fact in facts: content = fact.get("content") if isinstance(content, str) and query_lower in content.lower(): - results.append({"fact": fact, "score": float(fact.get("confidence") or 0.5), "matchType": "substring"}) + # 0.0 is a persisted confidence _normalize_fact accepts; only unset/null defaults. + confidence = fact.get("confidence") + score = 0.5 if confidence is None else float(confidence) + results.append({"fact": fact, "score": score, "matchType": "substring"}) results.sort(key=lambda result: result["score"], reverse=True) return results[:top_k] diff --git a/backend/tests/test_memory_retrieval_adapter.py b/backend/tests/test_memory_retrieval_adapter.py index bef09cf50a..b47cb0dea9 100644 --- a/backend/tests/test_memory_retrieval_adapter.py +++ b/backend/tests/test_memory_retrieval_adapter.py @@ -287,3 +287,34 @@ def test_deermem_create_and_restart_use_retrieval_adapter(tmp_path: Path) -> Non restarted = DeerMem(backend_config=config) assert any(fact["id"] == fact_id for fact in restarted.search("restart retrieval", user_id="alice")) + + +def test_indexed_zero_confidence_keeps_its_retrieval_weight(tmp_path: Path) -> None: + adapter = FTS5RetrievalAdapter(tmp_path / "facts.sqlite3") + scope = {"userId": "alice", "agentName": "agent-a"} + try: + adapter.upsert({**_fact("zero", "confidence weight probe"), "confidence": 0.0}, scope=scope, path="") + adapter.upsert({**_fact("half", "confidence weight probe"), "confidence": 0.5}, scope=scope, path="") + unset_fact = _fact("unset", "confidence weight probe") + del unset_fact["confidence"] + adapter.upsert(unset_fact, scope=scope, path="") + scores = {item["fact"]["id"]: item["score"] for item in adapter.search("confidence", scopes=[scope], top_k=5, mode="fts5", filters=None)} + + assert scores["zero"] < scores["half"] + # A missing confidence still falls back to the default, so only the explicit 0.0 is discounted. + assert scores["unset"] == pytest.approx(scores["half"]) + finally: + adapter.close() + + +def test_substring_fallback_keeps_zero_confidence_weight(tmp_path: Path) -> None: + storage = FileMemoryStorage(DeerMemConfig(storage_path=str(tmp_path))) + scope = {"userId": "alice", "agentName": "agent-a"} + try: + storage.upsert_fact({**_fact("zero", "substring confidence probe"), "confidence": 0.0}, user_id="alice", agent_name="agent-a") + storage.upsert_fact({**_fact("half", "substring confidence probe"), "confidence": 0.5}, user_id="alice", agent_name="agent-a") + scores = {item["fact"]["id"]: item["score"] for item in storage.search_facts("substring", scopes=[scope], top_k=5)} + + assert scores == {"zero": 0.0, "half": 0.5} + finally: + storage.close()