maximem-synap-haystack 0.2.0__tar.gz → 0.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {maximem_synap_haystack-0.2.0 → maximem_synap_haystack-0.2.2}/PKG-INFO +3 -3
- {maximem_synap_haystack-0.2.0 → maximem_synap_haystack-0.2.2}/maximem_synap_haystack.egg-info/PKG-INFO +3 -3
- {maximem_synap_haystack-0.2.0 → maximem_synap_haystack-0.2.2}/maximem_synap_haystack.egg-info/SOURCES.txt +3 -0
- maximem_synap_haystack-0.2.2/maximem_synap_haystack.egg-info/requires.txt +7 -0
- {maximem_synap_haystack-0.2.0 → maximem_synap_haystack-0.2.2}/pyproject.toml +2 -2
- {maximem_synap_haystack-0.2.0 → maximem_synap_haystack-0.2.2}/synap_haystack/__init__.py +17 -2
- maximem_synap_haystack-0.2.2/synap_haystack/hooks.py +127 -0
- {maximem_synap_haystack-0.2.0 → maximem_synap_haystack-0.2.2}/synap_haystack/short_term.py +17 -5
- {maximem_synap_haystack-0.2.0 → maximem_synap_haystack-0.2.2}/synap_haystack/store.py +303 -31
- {maximem_synap_haystack-0.2.0 → maximem_synap_haystack-0.2.2}/synap_haystack/writer.py +16 -7
- maximem_synap_haystack-0.2.2/tests/test_default_path.py +215 -0
- maximem_synap_haystack-0.2.2/tests/test_hooks.py +103 -0
- maximem_synap_haystack-0.2.2/tests/test_retriever.py +291 -0
- maximem_synap_haystack-0.2.2/tests/test_short_term.py +261 -0
- maximem_synap_haystack-0.2.2/tests/test_store.py +789 -0
- maximem_synap_haystack-0.2.2/tests/test_writer.py +214 -0
- maximem_synap_haystack-0.2.0/maximem_synap_haystack.egg-info/requires.txt +0 -7
- maximem_synap_haystack-0.2.0/tests/test_retriever.py +0 -66
- maximem_synap_haystack-0.2.0/tests/test_short_term.py +0 -112
- maximem_synap_haystack-0.2.0/tests/test_store.py +0 -112
- maximem_synap_haystack-0.2.0/tests/test_writer.py +0 -35
- {maximem_synap_haystack-0.2.0 → maximem_synap_haystack-0.2.2}/maximem_synap_haystack.egg-info/dependency_links.txt +0 -0
- {maximem_synap_haystack-0.2.0 → maximem_synap_haystack-0.2.2}/maximem_synap_haystack.egg-info/top_level.txt +0 -0
- {maximem_synap_haystack-0.2.0 → maximem_synap_haystack-0.2.2}/setup.cfg +0 -0
- {maximem_synap_haystack-0.2.0 → maximem_synap_haystack-0.2.2}/synap_haystack/retriever.py +0 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: maximem-synap-haystack
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Synap memory integration for Haystack — SynapMemoryStore + retriever/writer components
|
|
5
5
|
License-Expression: Apache-2.0
|
|
6
6
|
Requires-Python: >=3.11
|
|
7
|
-
Requires-Dist: maximem-synap>=0.
|
|
8
|
-
Requires-Dist: maximem-synap-integrations-common>=0.
|
|
7
|
+
Requires-Dist: maximem-synap>=0.5.3
|
|
8
|
+
Requires-Dist: maximem-synap-integrations-common>=0.2.3
|
|
9
9
|
Requires-Dist: haystack-ai>=2.0
|
|
10
10
|
Provides-Extra: dev
|
|
11
11
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: maximem-synap-haystack
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Synap memory integration for Haystack — SynapMemoryStore + retriever/writer components
|
|
5
5
|
License-Expression: Apache-2.0
|
|
6
6
|
Requires-Python: >=3.11
|
|
7
|
-
Requires-Dist: maximem-synap>=0.
|
|
8
|
-
Requires-Dist: maximem-synap-integrations-common>=0.
|
|
7
|
+
Requires-Dist: maximem-synap>=0.5.3
|
|
8
|
+
Requires-Dist: maximem-synap-integrations-common>=0.2.3
|
|
9
9
|
Requires-Dist: haystack-ai>=2.0
|
|
10
10
|
Provides-Extra: dev
|
|
11
11
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
@@ -5,10 +5,13 @@ maximem_synap_haystack.egg-info/dependency_links.txt
|
|
|
5
5
|
maximem_synap_haystack.egg-info/requires.txt
|
|
6
6
|
maximem_synap_haystack.egg-info/top_level.txt
|
|
7
7
|
synap_haystack/__init__.py
|
|
8
|
+
synap_haystack/hooks.py
|
|
8
9
|
synap_haystack/retriever.py
|
|
9
10
|
synap_haystack/short_term.py
|
|
10
11
|
synap_haystack/store.py
|
|
11
12
|
synap_haystack/writer.py
|
|
13
|
+
tests/test_default_path.py
|
|
14
|
+
tests/test_hooks.py
|
|
12
15
|
tests/test_retriever.py
|
|
13
16
|
tests/test_short_term.py
|
|
14
17
|
tests/test_store.py
|
|
@@ -4,11 +4,11 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "maximem-synap-haystack"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.2"
|
|
8
8
|
description = "Synap memory integration for Haystack — SynapMemoryStore + retriever/writer components"
|
|
9
9
|
requires-python = ">=3.11"
|
|
10
10
|
license = "Apache-2.0"
|
|
11
|
-
dependencies = ["maximem-synap>=0.
|
|
11
|
+
dependencies = ["maximem-synap>=0.5.3", "maximem-synap-integrations-common>=0.2.3", "haystack-ai>=2.0"]
|
|
12
12
|
|
|
13
13
|
[project.optional-dependencies]
|
|
14
14
|
dev = ["pytest>=7.0", "pytest-asyncio>=0.21"]
|
|
@@ -5,12 +5,20 @@ Store-centric, matching the official ``mem0-haystack`` shape:
|
|
|
5
5
|
- :class:`SynapMemoryStore` — the memory store (a plain object, not a component)
|
|
6
6
|
that owns all Synap SDK interaction. ``add_memories`` / ``search_memories`` /
|
|
7
7
|
``search_memories_as_single_message`` / ``delete_*`` + ``to_dict`` / ``from_dict``.
|
|
8
|
+
It is also a Haystack ``ChatMessageStore`` (``write_messages`` /
|
|
9
|
+
``retrieve_messages`` / ``count_messages``), so an experimental ``Agent`` takes it
|
|
10
|
+
as ``chat_message_store=`` and loads Synap's compaction and the turns after it
|
|
11
|
+
instead of a ten-message window. Turns are written on the gRPC stream, which
|
|
12
|
+
the first one opens (``SYNAP_AUTO_LISTEN=false`` turns that off).
|
|
8
13
|
- :class:`SynapMemoryRetriever` — ``@component`` returning ``ChatMessage``s
|
|
9
14
|
(Mem0-shaped chat read path).
|
|
10
15
|
- :class:`SynapRetriever` — ``@component`` returning ``Document``s (RAG read path).
|
|
11
16
|
- :class:`SynapMemoryWriter` — ``@component`` that records conversation turns.
|
|
12
|
-
- :class:`
|
|
13
|
-
|
|
17
|
+
- :class:`SynapHistoryHook` / :class:`SynapRecordHook` — hooks for haystack-ai 3's
|
|
18
|
+
``Agent(hooks=...)``: Synap's compaction in place of the earlier turns
|
|
19
|
+
(``before_llm``) and the run recorded on the stream (``after_run``).
|
|
20
|
+
- :class:`SynapShortTermContext` — ``@component`` emitting Synap's compaction of
|
|
21
|
+
the conversation for a prompt builder.
|
|
14
22
|
|
|
15
23
|
The components accept a ready ``store=...`` or an ``sdk=...`` + scope kwargs.
|
|
16
24
|
"""
|
|
@@ -20,10 +28,17 @@ from synap_haystack.retriever import SynapMemoryRetriever, SynapRetriever
|
|
|
20
28
|
from synap_haystack.writer import SynapMemoryWriter
|
|
21
29
|
from synap_haystack.short_term import SynapShortTermContext
|
|
22
30
|
|
|
31
|
+
try: # needs haystack-ai 3's Agent hooks only at run time; importing is safe
|
|
32
|
+
from synap_haystack.hooks import SynapHistoryHook, SynapRecordHook
|
|
33
|
+
except ImportError: # pragma: no cover
|
|
34
|
+
SynapHistoryHook = SynapRecordHook = None # type: ignore[assignment,misc]
|
|
35
|
+
|
|
23
36
|
__all__ = [
|
|
24
37
|
"SynapMemoryStore",
|
|
25
38
|
"SynapMemoryRetriever",
|
|
26
39
|
"SynapRetriever",
|
|
27
40
|
"SynapMemoryWriter",
|
|
28
41
|
"SynapShortTermContext",
|
|
42
|
+
"SynapHistoryHook",
|
|
43
|
+
"SynapRecordHook",
|
|
29
44
|
]
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Synap's compaction in place of a Haystack agent's earlier turns, via hooks.
|
|
2
|
+
|
|
3
|
+
Haystack 3's ``Agent`` has no chat-message store. It takes ``hooks``: objects run
|
|
4
|
+
at points in its loop that mutate the live ``State`` in place. That is where its
|
|
5
|
+
own compaction lives (``haystack.hooks.compaction.CompactionHook``), and it is
|
|
6
|
+
where Synap's goes:
|
|
7
|
+
|
|
8
|
+
- :class:`SynapHistoryHook` on ``before_llm``: once Synap has compacted the
|
|
9
|
+
conversation, every turn before the current one in ``state["messages"]`` is
|
|
10
|
+
replaced by Synap's compaction (a system message) and the turns after it.
|
|
11
|
+
Until then, and whenever Synap cannot answer, the messages are left alone.
|
|
12
|
+
The agent's own compaction hooks can stay installed: with a short history they
|
|
13
|
+
do not fire, and they are the fallback.
|
|
14
|
+
- :class:`SynapRecordHook` on ``after_run``: records the run's turns on Synap's
|
|
15
|
+
gRPC stream (opened by the first one) and reports its tool calls, results and
|
|
16
|
+
reasoning, through :meth:`SynapMemoryStore.write_messages`.
|
|
17
|
+
|
|
18
|
+
::
|
|
19
|
+
|
|
20
|
+
store = SynapMemoryStore(sdk, user_id="alice")
|
|
21
|
+
agent = Agent(
|
|
22
|
+
chat_generator=...,
|
|
23
|
+
hooks={
|
|
24
|
+
"before_llm": [SynapHistoryHook(store, "conv-1")],
|
|
25
|
+
"after_run": [SynapRecordHook(store, "conv-1")],
|
|
26
|
+
},
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
On haystack-ai 2.x use ``SynapMemoryStore`` as the experimental ``Agent``'s
|
|
30
|
+
``chat_message_store`` instead; this module needs the 3.x ``Agent.hooks``.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import logging
|
|
36
|
+
from typing import Any, Dict
|
|
37
|
+
|
|
38
|
+
from haystack.core.serialization import default_from_dict, default_to_dict
|
|
39
|
+
from haystack.dataclasses import ChatMessage, ChatRole
|
|
40
|
+
|
|
41
|
+
from synap_integrations_common import (
|
|
42
|
+
fetch_compacted_history,
|
|
43
|
+
run_async,
|
|
44
|
+
splice_history,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
from synap_haystack.store import RECORDED_KEY, SynapMemoryStore
|
|
48
|
+
|
|
49
|
+
logger = logging.getLogger(__name__)
|
|
50
|
+
|
|
51
|
+
_ROLES = {ChatRole.USER: "user", ChatRole.ASSISTANT: "assistant", ChatRole.SYSTEM: "system"}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _role_of(message: ChatMessage) -> str:
|
|
55
|
+
return _ROLES.get(message.role, "other")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _text_of(message: ChatMessage) -> str:
|
|
59
|
+
return message.text or ""
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _replace(_current: Any, new: Any) -> Any:
|
|
63
|
+
return new
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class _StoreHook:
|
|
67
|
+
"""Shared construction and serialization for the two hooks."""
|
|
68
|
+
|
|
69
|
+
def __init__(self, store: SynapMemoryStore, chat_history_id: str) -> None:
|
|
70
|
+
if store is None:
|
|
71
|
+
raise ValueError(f"{type(self).__name__} requires a store")
|
|
72
|
+
if not chat_history_id or not str(chat_history_id).strip():
|
|
73
|
+
raise ValueError(f"{type(self).__name__} requires a non-empty chat_history_id")
|
|
74
|
+
self.store = store
|
|
75
|
+
self.chat_history_id = chat_history_id
|
|
76
|
+
|
|
77
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
78
|
+
return default_to_dict(self, store=self.store.to_dict(), chat_history_id=self.chat_history_id)
|
|
79
|
+
|
|
80
|
+
@classmethod
|
|
81
|
+
def from_dict(cls, data: Dict[str, Any]) -> "_StoreHook":
|
|
82
|
+
init = dict(data.get("init_parameters", {}))
|
|
83
|
+
init["store"] = SynapMemoryStore.from_dict(init["store"])
|
|
84
|
+
return default_from_dict(cls, {**data, "init_parameters": init})
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class SynapHistoryHook(_StoreHook):
|
|
88
|
+
"""``before_llm`` hook: Synap's compaction in place of the earlier turns."""
|
|
89
|
+
|
|
90
|
+
def run(self, state: Any) -> None:
|
|
91
|
+
run_async(self.run_async(state))
|
|
92
|
+
|
|
93
|
+
async def run_async(self, state: Any) -> None:
|
|
94
|
+
messages = list(state.get("messages") or [])
|
|
95
|
+
if not any(_role_of(m) == "user" for m in messages):
|
|
96
|
+
return
|
|
97
|
+
history = await fetch_compacted_history(self.store.sdk, self.chat_history_id)
|
|
98
|
+
if history is None:
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
def make(role: str, text: str) -> ChatMessage:
|
|
102
|
+
if role == "system":
|
|
103
|
+
return ChatMessage.from_system(text)
|
|
104
|
+
meta = {RECORDED_KEY: self.chat_history_id} # Synap already has it
|
|
105
|
+
return (ChatMessage.from_assistant(text, meta=meta) if role == "assistant"
|
|
106
|
+
else ChatMessage.from_user(text, meta=meta))
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
spliced = splice_history(messages, history, role_of=_role_of, text_of=_text_of,
|
|
110
|
+
make_message=make)
|
|
111
|
+
except Exception: # noqa: BLE001 — the agent's own messages still stand
|
|
112
|
+
logger.warning("synap: could not splice the compacted history", exc_info=True)
|
|
113
|
+
return
|
|
114
|
+
state.set("messages", spliced, handler_override=_replace)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class SynapRecordHook(_StoreHook):
|
|
118
|
+
"""``after_run`` hook: record the run on Synap, each message once."""
|
|
119
|
+
|
|
120
|
+
def run(self, state: Any) -> None:
|
|
121
|
+
self.store.write_messages(self.chat_history_id, list(state.get("messages") or []))
|
|
122
|
+
|
|
123
|
+
async def run_async(self, state: Any) -> None:
|
|
124
|
+
await self.store.awrite_messages(self.chat_history_id, list(state.get("messages") or []))
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
__all__ = ["SynapHistoryHook", "SynapRecordHook"]
|
|
@@ -5,8 +5,14 @@ Mirrors the LangGraph template, adapted to Haystack's
|
|
|
5
5
|
``sdk.conversation.context.get_context_for_prompt`` (cache-first behind
|
|
6
6
|
``SYNAP_SDK_ST_AUTHORITATIVE``).
|
|
7
7
|
|
|
8
|
-
Exposes :class:`SynapShortTermContext` — a component that emits
|
|
9
|
-
|
|
8
|
+
Exposes :class:`SynapShortTermContext` — a component that emits Synap's
|
|
9
|
+
compaction of the conversation, as a string, on its ``synap_st`` output. It is
|
|
10
|
+
the compaction only: the turns after it are the ones your pipeline already
|
|
11
|
+
passes (a chat history, an agent's messages), and Synap's ``formatted_context``
|
|
12
|
+
carries them too, so a block built from it showed every recent turn twice. Until
|
|
13
|
+
the conversation has been compacted the block is empty. (An agent should use
|
|
14
|
+
``SynapMemoryStore`` as its ``chat_message_store`` instead, which replaces the
|
|
15
|
+
history it loads.)
|
|
10
16
|
Wire it before your prompt builder, then reference ``{synap_st}`` (or
|
|
11
17
|
your prompt builder's variable name) in the template. The component
|
|
12
18
|
re-fetches on every pipeline run, so ST stays fresh.
|
|
@@ -18,7 +24,7 @@ Quality contract identical to the LangGraph adapter:
|
|
|
18
24
|
raise abort the whole pipeline. The default ``on_error="fallback"``
|
|
19
25
|
emits the bare user system text (or empty string) on SDK failure,
|
|
20
26
|
with an ERROR log. ``on_error="raise"`` surfaces failures.
|
|
21
|
-
-
|
|
27
|
+
- No compaction yet is a no-op — never wipes the user's system text.
|
|
22
28
|
"""
|
|
23
29
|
|
|
24
30
|
from __future__ import annotations
|
|
@@ -141,8 +147,14 @@ class SynapShortTermContext:
|
|
|
141
147
|
conversation_id=self.conversation_id,
|
|
142
148
|
style=self.style,
|
|
143
149
|
)
|
|
144
|
-
|
|
145
|
-
|
|
150
|
+
compaction = getattr(response, "context_block", None)
|
|
151
|
+
if getattr(response, "available", True) is not False and isinstance(compaction, str) \
|
|
152
|
+
and compaction.strip():
|
|
153
|
+
parts = [compaction.strip()]
|
|
154
|
+
found = getattr(response, "history_block", None)
|
|
155
|
+
if isinstance(found, str) and found.strip():
|
|
156
|
+
parts.append(found.strip())
|
|
157
|
+
st_block = "\n\n".join(parts)
|
|
146
158
|
except SynapIntegrationError:
|
|
147
159
|
if self.on_error == "raise":
|
|
148
160
|
raise
|