fix(memory): keep a zero confidence from scoring as the default (#5586)

or 0.5 cannot distinguish an explicit 0.0 from an unset field, so a discounted fact got the default confidence weight in both the FTS5 document and the substring fallback, while the same row indexed through FTS5Retrieval.index_fact kept its zero weight.
This commit is contained in:
Grapette.L 2026-09-24 15:38:52 +08:00 committed by GitHub
parent 7c948a38c4
commit c77e8744fa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 38 additions and 2 deletions

View File

@ -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,

View File

@ -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]

View File

@ -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()