mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-07 12:47:55 -10:00
Merge pull request #6112 from MervinPraison/claude/issue-5051-20261028
fix: exclude compaction summary from session search scoring
This commit is contained in:
commit
2c6bdb9bca
3 files changed
+68
-4
No files matched your search
@@ -57,7 +57,8 @@ class SqliteSessionStore(DefaultSessionStore):
|
||||
# Bumped whenever ``_flatten`` changes what content lands in the index, so
|
||||
# a store built by an older release rebuilds its FTS rows once on upgrade.
|
||||
# v2: archived_messages included (Issue #5031).
|
||||
INDEX_CONTENT_VERSION = 2
|
||||
# v3: compaction summaries excluded (Issue #5051).
|
||||
INDEX_CONTENT_VERSION = 3
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -235,11 +236,16 @@ class SqliteSessionStore(DefaultSessionStore):
|
||||
Includes ``archived_messages`` (turns rolled out of the active window
|
||||
by ``retention="compact"``) ahead of the active ``messages`` so a
|
||||
compaction re-index preserves — rather than deletes — the raw turns,
|
||||
keeping the whole conversation searchable (Issue #5031).
|
||||
keeping the whole conversation searchable (Issue #5031). Compaction
|
||||
summaries are skipped: search never scores them, so indexing them
|
||||
would only spend the candidate allowance on misses (Issue #5051).
|
||||
"""
|
||||
parts = []
|
||||
for msg in (*session.archived_messages, *session.messages):
|
||||
content = getattr(msg, "content", "")
|
||||
metadata = getattr(msg, "metadata", None)
|
||||
if isinstance(metadata, dict) and metadata.get("compaction"):
|
||||
continue
|
||||
if content:
|
||||
parts.append(str(content))
|
||||
return "\n".join(parts)
|
||||
@@ -926,7 +932,7 @@ class SqliteSessionStore(DefaultSessionStore):
|
||||
if not isinstance(msg, dict):
|
||||
continue
|
||||
content = str(msg.get("content", ""))
|
||||
if not content:
|
||||
if not content or self._is_compaction_summary(msg):
|
||||
continue
|
||||
lowered = content.lower()
|
||||
score = 0.0
|
||||
|
||||
@@ -2401,6 +2401,17 @@ class DefaultSessionStore:
|
||||
snippet = snippet + "…"
|
||||
return snippet
|
||||
|
||||
@staticmethod
|
||||
def _is_compaction_summary(msg: Dict[str, Any]) -> bool:
|
||||
"""True for the synthetic ``retention="compact"`` rollup message.
|
||||
|
||||
The summary paraphrases archived turns that are scanned verbatim, so
|
||||
it is kept for context/replay but excluded from scoring — otherwise a
|
||||
session's rank would depend on how often it was compacted (Issue #5051).
|
||||
"""
|
||||
metadata = msg.get("metadata")
|
||||
return isinstance(metadata, dict) and bool(metadata.get("compaction"))
|
||||
|
||||
@staticmethod
|
||||
def _searchable_messages(data: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||
"""Project a persisted session into the list of messages to scan.
|
||||
@@ -2477,7 +2488,7 @@ class DefaultSessionStore:
|
||||
if not isinstance(msg, dict):
|
||||
continue
|
||||
content = str(msg.get("content", ""))
|
||||
if not content:
|
||||
if not content or self._is_compaction_summary(msg):
|
||||
continue
|
||||
lowered = content.lower()
|
||||
score = 0.0
|
||||
|
||||
@@ -332,6 +332,53 @@ class TestArchivedRecall:
|
||||
hits = reopened.search("zx-9271-alpha")
|
||||
assert [h.session_id for h in hits] == ["old"]
|
||||
|
||||
@pytest.mark.parametrize("store_cls", [DefaultSessionStore, SqliteSessionStore])
|
||||
def test_compaction_summary_not_scored(self, tmp_dir, store_cls):
|
||||
# Issue #5051: the synthetic summary paraphrases archived turns that are
|
||||
# already scored verbatim, so compaction count must not change rank.
|
||||
from praisonaiagents.session.store import SessionData, SessionMessage
|
||||
|
||||
store = store_cls(session_dir=tmp_dir)
|
||||
turn = ("user", "deploy token zx-9271-alpha")
|
||||
self._seed_with_archive(store, "plain", archived=[turn], active=[])
|
||||
compacted = SessionData(session_id="compacted")
|
||||
compacted.archived_messages.append(SessionMessage(role=turn[0], content=turn[1]))
|
||||
compacted.messages.append(
|
||||
SessionMessage(
|
||||
role="system",
|
||||
content="[Session Summary]\nuser: deploy token zx-9271-alpha",
|
||||
metadata={"compaction": True, "compacted_count": 1},
|
||||
)
|
||||
)
|
||||
store._save_session(compacted)
|
||||
|
||||
hits = {h.session_id: h for h in store.search("zx-9271-alpha")}
|
||||
assert hits["plain"].score == hits["compacted"].score
|
||||
hit = hits["compacted"]
|
||||
anchor = next(m for m in hit.messages if m["index"] == hit.anchor_index)
|
||||
assert anchor["archived"]
|
||||
|
||||
def test_summary_only_matches_do_not_exhaust_candidates(self, tmp_dir):
|
||||
# Unscored summaries must not be indexed, or they spend the bounded
|
||||
# candidate allowance and hide a real match (Issue #5051, Greptile P1).
|
||||
from praisonaiagents.session.store import SessionData, SessionMessage
|
||||
|
||||
store = SqliteSessionStore(session_dir=tmp_dir)
|
||||
for i in range(6):
|
||||
data = SessionData(session_id=f"compacted-{i}")
|
||||
data.archived_messages.append(SessionMessage(role="user", content="hello"))
|
||||
data.messages.append(
|
||||
SessionMessage(
|
||||
role="system",
|
||||
content="[Session Summary]\n" + "pelican " * 20,
|
||||
metadata={"compaction": True},
|
||||
)
|
||||
)
|
||||
store._save_session(data)
|
||||
self._seed_with_archive(store, "real", archived=[], active=[("user", "a pelican flew by")])
|
||||
|
||||
assert [h.session_id for h in store.search("pelican", limit=1)] == ["real"]
|
||||
|
||||
|
||||
class TestIndexedSessionRoute:
|
||||
"""Indexed gateway/agent routing lookups (Issue #2956)."""
|
||||
|
||||
Reference in new issue
Block a user