maximem-synap-haystack 0.1.1__py3-none-any.whl
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.1.1.dist-info/METADATA +12 -0
- maximem_synap_haystack-0.1.1.dist-info/RECORD +7 -0
- maximem_synap_haystack-0.1.1.dist-info/WHEEL +5 -0
- maximem_synap_haystack-0.1.1.dist-info/top_level.txt +1 -0
- synap_haystack/__init__.py +6 -0
- synap_haystack/retriever.py +107 -0
- synap_haystack/writer.py +139 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: maximem-synap-haystack
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Synap memory integration for Haystack
|
|
5
|
+
License-Expression: Apache-2.0
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Requires-Dist: maximem-synap>=0.2.0
|
|
8
|
+
Requires-Dist: synap-integrations-common>=0.1.0
|
|
9
|
+
Requires-Dist: haystack-ai>=2.0
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
12
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
synap_haystack/__init__.py,sha256=Nnd-c_ki2ZLu3ohGPBFHYc3s3o2am2TybhVkG0_IEcI,201
|
|
2
|
+
synap_haystack/retriever.py,sha256=CNjLTbSt6EPBNVWMLTa_o8wV9e9F8HRkVwKFoF_bJXM,3529
|
|
3
|
+
synap_haystack/writer.py,sha256=4Dlypnb7aklpBrz6uGlCLOSGA-zE3rwn3I8W6BQNCTk,4784
|
|
4
|
+
maximem_synap_haystack-0.1.1.dist-info/METADATA,sha256=Ac3hVqhJ9s3fKjNOXN9PxMxJQiklW7gToLwkAl_MKZA,399
|
|
5
|
+
maximem_synap_haystack-0.1.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
6
|
+
maximem_synap_haystack-0.1.1.dist-info/top_level.txt,sha256=0Kv9DHCP1HE400j0_1FSJgUFTDyp1B8LdTF02mJFrKs,15
|
|
7
|
+
maximem_synap_haystack-0.1.1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
synap_haystack
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Synap retriever component for Haystack pipelines.
|
|
2
|
+
|
|
3
|
+
A Haystack @component that retrieves memory context from Synap
|
|
4
|
+
and returns it as a list of Haystack Documents.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Dict, List, Optional
|
|
9
|
+
|
|
10
|
+
from haystack import Document, component
|
|
11
|
+
|
|
12
|
+
from maximem_synap import MaximemSynapSDK
|
|
13
|
+
from synap_integrations_common import run_async, wrap_sdk_errors_async
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@component
|
|
19
|
+
class SynapRetriever:
|
|
20
|
+
"""Haystack component that retrieves memory from Synap.
|
|
21
|
+
|
|
22
|
+
Example::
|
|
23
|
+
|
|
24
|
+
retriever = SynapRetriever(sdk=sdk, user_id="u1")
|
|
25
|
+
pipeline.add_component("memory", retriever)
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
sdk: MaximemSynapSDK,
|
|
31
|
+
user_id: str,
|
|
32
|
+
customer_id: str = "",
|
|
33
|
+
conversation_id: Optional[str] = None,
|
|
34
|
+
mode: str = "accurate",
|
|
35
|
+
max_results: int = 20,
|
|
36
|
+
):
|
|
37
|
+
if sdk is None:
|
|
38
|
+
raise ValueError("SynapRetriever requires a non-None sdk")
|
|
39
|
+
if not user_id:
|
|
40
|
+
raise ValueError("SynapRetriever requires a non-empty user_id")
|
|
41
|
+
|
|
42
|
+
self.sdk = sdk
|
|
43
|
+
self.user_id = user_id
|
|
44
|
+
self.customer_id = customer_id
|
|
45
|
+
self.conversation_id = conversation_id
|
|
46
|
+
self.mode = mode
|
|
47
|
+
self.max_results = max_results
|
|
48
|
+
|
|
49
|
+
@component.output_types(documents=List[Document])
|
|
50
|
+
def run(self, query: str) -> Dict[str, List[Document]]:
|
|
51
|
+
return run_async(self._arun(query))
|
|
52
|
+
|
|
53
|
+
async def _arun(self, query: str) -> Dict[str, List[Document]]:
|
|
54
|
+
async with wrap_sdk_errors_async(
|
|
55
|
+
"haystack.SynapRetriever.run",
|
|
56
|
+
logger,
|
|
57
|
+
user_id=self.user_id,
|
|
58
|
+
query_len=len(query),
|
|
59
|
+
):
|
|
60
|
+
response = await self.sdk.fetch(
|
|
61
|
+
conversation_id=self.conversation_id,
|
|
62
|
+
user_id=self.user_id,
|
|
63
|
+
customer_id=self.customer_id or None,
|
|
64
|
+
search_query=[query],
|
|
65
|
+
max_results=self.max_results,
|
|
66
|
+
mode=self.mode,
|
|
67
|
+
include_conversation_context=False,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
docs: List[Document] = []
|
|
71
|
+
|
|
72
|
+
for fact in response.facts:
|
|
73
|
+
docs.append(Document(
|
|
74
|
+
content=fact.content,
|
|
75
|
+
meta={"type": "fact", "id": fact.id, "confidence": fact.confidence,
|
|
76
|
+
"scope": response.scope_map.get(fact.id, "")},
|
|
77
|
+
))
|
|
78
|
+
|
|
79
|
+
for pref in response.preferences:
|
|
80
|
+
docs.append(Document(
|
|
81
|
+
content=pref.content,
|
|
82
|
+
meta={"type": "preference", "id": pref.id, "strength": pref.strength,
|
|
83
|
+
"scope": response.scope_map.get(pref.id, "")},
|
|
84
|
+
))
|
|
85
|
+
|
|
86
|
+
for ep in response.episodes:
|
|
87
|
+
docs.append(Document(
|
|
88
|
+
content=ep.summary,
|
|
89
|
+
meta={"type": "episode", "id": ep.id, "significance": ep.significance,
|
|
90
|
+
"scope": response.scope_map.get(ep.id, "")},
|
|
91
|
+
))
|
|
92
|
+
|
|
93
|
+
for em in response.emotions:
|
|
94
|
+
docs.append(Document(
|
|
95
|
+
content=f"{em.emotion_type}: {em.context}",
|
|
96
|
+
meta={"type": "emotion", "id": em.id, "intensity": em.intensity,
|
|
97
|
+
"scope": response.scope_map.get(em.id, "")},
|
|
98
|
+
))
|
|
99
|
+
|
|
100
|
+
for te in response.temporal_events:
|
|
101
|
+
docs.append(Document(
|
|
102
|
+
content=te.content,
|
|
103
|
+
meta={"type": "temporal_event", "id": te.id,
|
|
104
|
+
"scope": response.scope_map.get(te.id, "")},
|
|
105
|
+
))
|
|
106
|
+
|
|
107
|
+
return {"documents": docs}
|
synap_haystack/writer.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Synap memory writer component for Haystack pipelines.
|
|
2
|
+
|
|
3
|
+
Records conversation messages to Synap for server-side extraction.
|
|
4
|
+
|
|
5
|
+
Previously this component caught every ingestion exception and demoted
|
|
6
|
+
it to a ``warning`` log line, then reported whatever happened to succeed
|
|
7
|
+
via ``written_count``. Hidden ingestion failures are dangerous: a
|
|
8
|
+
Haystack pipeline that looks green is not the same as a pipeline whose
|
|
9
|
+
memory ingestion actually worked.
|
|
10
|
+
|
|
11
|
+
New semantics:
|
|
12
|
+
|
|
13
|
+
- Track ``written_count`` and ``failed_count`` separately and expose
|
|
14
|
+
both as component outputs, so downstream components can branch on
|
|
15
|
+
partial failures.
|
|
16
|
+
- Also emit ``first_error`` (string) when at least one document fails,
|
|
17
|
+
so operators can see *what* went wrong without grepping logs.
|
|
18
|
+
- If **every** document fails, raise :class:`SynapIntegrationError`.
|
|
19
|
+
A 100% failure rate is not "partial" — it's a broken pipeline and
|
|
20
|
+
should stop.
|
|
21
|
+
- Per-document failures are logged at ``ERROR`` (not ``WARNING``) with
|
|
22
|
+
structured context.
|
|
23
|
+
- Documents with an unrecognized role are skipped and surfaced via
|
|
24
|
+
``skipped_count``.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import logging
|
|
28
|
+
from typing import Dict, List, Optional
|
|
29
|
+
|
|
30
|
+
from haystack import Document, component
|
|
31
|
+
|
|
32
|
+
from maximem_synap import MaximemSynapSDK
|
|
33
|
+
from synap_integrations_common import SynapIntegrationError, run_async
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
_VALID_ROLES = frozenset(("user", "assistant"))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@component
|
|
41
|
+
class SynapMemoryWriter:
|
|
42
|
+
"""Haystack component that writes conversation turns to Synap.
|
|
43
|
+
|
|
44
|
+
Accepts Documents where content is the message text and
|
|
45
|
+
meta["role"] is "user" or "assistant".
|
|
46
|
+
|
|
47
|
+
Example::
|
|
48
|
+
|
|
49
|
+
writer = SynapMemoryWriter(sdk=sdk, conversation_id="c1", user_id="u1")
|
|
50
|
+
pipeline.add_component("memory_writer", writer)
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def __init__(
|
|
54
|
+
self,
|
|
55
|
+
sdk: MaximemSynapSDK,
|
|
56
|
+
conversation_id: str,
|
|
57
|
+
user_id: str,
|
|
58
|
+
customer_id: str = "",
|
|
59
|
+
):
|
|
60
|
+
if sdk is None:
|
|
61
|
+
raise ValueError("SynapMemoryWriter requires a non-None sdk")
|
|
62
|
+
if not user_id:
|
|
63
|
+
raise ValueError("SynapMemoryWriter requires a non-empty user_id")
|
|
64
|
+
if not conversation_id:
|
|
65
|
+
raise ValueError(
|
|
66
|
+
"SynapMemoryWriter requires a non-empty conversation_id"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
self.sdk = sdk
|
|
70
|
+
self.conversation_id = conversation_id
|
|
71
|
+
self.user_id = user_id
|
|
72
|
+
self.customer_id = customer_id
|
|
73
|
+
|
|
74
|
+
@component.output_types(
|
|
75
|
+
written_count=int,
|
|
76
|
+
failed_count=int,
|
|
77
|
+
skipped_count=int,
|
|
78
|
+
first_error=Optional[str],
|
|
79
|
+
)
|
|
80
|
+
def run(self, documents: List[Document]) -> Dict[str, object]:
|
|
81
|
+
return run_async(self._arun(documents))
|
|
82
|
+
|
|
83
|
+
async def _arun(self, documents: List[Document]) -> Dict[str, object]:
|
|
84
|
+
written = 0
|
|
85
|
+
failed = 0
|
|
86
|
+
skipped = 0
|
|
87
|
+
first_error: Optional[str] = None
|
|
88
|
+
|
|
89
|
+
for doc in documents:
|
|
90
|
+
role = doc.meta.get("role", "user")
|
|
91
|
+
if role not in _VALID_ROLES:
|
|
92
|
+
skipped += 1
|
|
93
|
+
logger.info(
|
|
94
|
+
"SynapMemoryWriter: skipping document with unsupported "
|
|
95
|
+
"role=%r (expected one of %s)",
|
|
96
|
+
role,
|
|
97
|
+
sorted(_VALID_ROLES),
|
|
98
|
+
)
|
|
99
|
+
continue
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
await self.sdk.conversation.record_message(
|
|
103
|
+
conversation_id=self.conversation_id,
|
|
104
|
+
role=role,
|
|
105
|
+
content=doc.content,
|
|
106
|
+
user_id=self.user_id,
|
|
107
|
+
customer_id=self.customer_id,
|
|
108
|
+
)
|
|
109
|
+
written += 1
|
|
110
|
+
except Exception as exc: # noqa: BLE001 — boundary
|
|
111
|
+
failed += 1
|
|
112
|
+
logger.error(
|
|
113
|
+
"SynapMemoryWriter: record_message failed "
|
|
114
|
+
"conversation_id=%s role=%s error=%s",
|
|
115
|
+
self.conversation_id,
|
|
116
|
+
role,
|
|
117
|
+
exc,
|
|
118
|
+
exc_info=True,
|
|
119
|
+
)
|
|
120
|
+
if first_error is None:
|
|
121
|
+
first_error = f"{type(exc).__name__}: {exc}"
|
|
122
|
+
|
|
123
|
+
processed = written + failed
|
|
124
|
+
if processed > 0 and written == 0:
|
|
125
|
+
# Every single attempt failed — this is a broken pipeline,
|
|
126
|
+
# not a partial result. Stop loudly.
|
|
127
|
+
raise SynapIntegrationError(
|
|
128
|
+
"haystack.SynapMemoryWriter.run",
|
|
129
|
+
f"all {failed} record_message attempts failed; "
|
|
130
|
+
f"first error: {first_error}",
|
|
131
|
+
{"conversation_id": self.conversation_id, "failed": failed},
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
"written_count": written,
|
|
136
|
+
"failed_count": failed,
|
|
137
|
+
"skipped_count": skipped,
|
|
138
|
+
"first_error": first_error,
|
|
139
|
+
}
|