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.
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ synap_haystack
@@ -0,0 +1,6 @@
1
+ """Synap memory integration for Haystack."""
2
+
3
+ from synap_haystack.retriever import SynapRetriever
4
+ from synap_haystack.writer import SynapMemoryWriter
5
+
6
+ __all__ = ["SynapRetriever", "SynapMemoryWriter"]
@@ -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}
@@ -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
+ }