@maximem/synap-js-sdk 0.3.0 → 0.4.1
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.
- package/MIGRATING.md +147 -0
- package/README.md +95 -82
- package/bin/synap-js-sdk.js +45 -122
- package/dist/anticipation-cache-ARYEEKMU.cjs +10 -0
- package/dist/anticipation-cache-ARYEEKMU.cjs.map +1 -0
- package/dist/anticipation-cache-BMVF5sXb.d.cts +190 -0
- package/dist/anticipation-cache-BMVF5sXb.d.ts +190 -0
- package/dist/anticipation-cache-H7HEVWQC.js +10 -0
- package/dist/anticipation-cache-H7HEVWQC.js.map +1 -0
- package/dist/chunk-5SC3ELNK.js +856 -0
- package/dist/chunk-5SC3ELNK.js.map +1 -0
- package/dist/chunk-6LDUDKHS.js +832 -0
- package/dist/chunk-6LDUDKHS.js.map +1 -0
- package/dist/chunk-HR5Y3KXS.cjs +172 -0
- package/dist/chunk-HR5Y3KXS.cjs.map +1 -0
- package/dist/chunk-QBCVMPGK.js +28 -0
- package/dist/chunk-QBCVMPGK.js.map +1 -0
- package/dist/chunk-RB57I2IQ.cjs +832 -0
- package/dist/chunk-RB57I2IQ.cjs.map +1 -0
- package/dist/chunk-T3NZBIV2.js +172 -0
- package/dist/chunk-T3NZBIV2.js.map +1 -0
- package/dist/chunk-VXRCBMCY.cjs +856 -0
- package/dist/chunk-VXRCBMCY.cjs.map +1 -0
- package/dist/chunk-ZKOJDUN4.cjs +28 -0
- package/dist/chunk-ZKOJDUN4.cjs.map +1 -0
- package/dist/env-55ULHNJZ.cjs +11 -0
- package/dist/env-55ULHNJZ.cjs.map +1 -0
- package/dist/env-QCUKKW36.js +11 -0
- package/dist/env-QCUKKW36.js.map +1 -0
- package/dist/grpc/index.cjs +87 -0
- package/dist/grpc/index.cjs.map +1 -0
- package/dist/grpc/index.d.cts +162 -0
- package/dist/grpc/index.d.ts +162 -0
- package/dist/grpc/index.js +87 -0
- package/dist/grpc/index.js.map +1 -0
- package/dist/index.cjs +2702 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1491 -0
- package/dist/index.d.ts +1491 -0
- package/dist/index.js +2702 -0
- package/dist/index.js.map +1 -0
- package/dist/stream-client-ME5N5NK4.js +7 -0
- package/dist/stream-client-ME5N5NK4.js.map +1 -0
- package/dist/stream-client-ZN6ADZBR.cjs +7 -0
- package/dist/stream-client-ZN6ADZBR.cjs.map +1 -0
- package/package.json +68 -23
- package/.env.example +0 -39
- package/bridge/synap_bridge.py +0 -768
- package/src/bridge-manager.js +0 -262
- package/src/errors.js +0 -168
- package/src/index.js +0 -13
- package/src/runtime.js +0 -161
- package/src/setup-typescript.js +0 -161
- package/src/synap-client.js +0 -464
- package/types/index.d.ts +0 -475
package/bridge/synap_bridge.py
DELETED
|
@@ -1,768 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Synap SDK Bridge: JSON-RPC over stdin/stdout for the Node.js wrapper.
|
|
3
|
-
|
|
4
|
-
Protocol:
|
|
5
|
-
stdin -> {"id": 1, "method": "init", "params": {...}}\n
|
|
6
|
-
stdout <- {"id": 1, "result": {...}, "error": null}\n
|
|
7
|
-
Methods:
|
|
8
|
-
init, add_memory, create_memory, record_message, search_memory, get_memories,
|
|
9
|
-
fetch_user_context, fetch_customer_context, fetch_client_context,
|
|
10
|
-
get_context_for_prompt, delete_memory, shutdown
|
|
11
|
-
"""
|
|
12
|
-
|
|
13
|
-
import asyncio
|
|
14
|
-
import json
|
|
15
|
-
import logging
|
|
16
|
-
import sys
|
|
17
|
-
import time
|
|
18
|
-
import traceback
|
|
19
|
-
from typing import Dict, List, Optional
|
|
20
|
-
from uuid import UUID
|
|
21
|
-
|
|
22
|
-
# Keep stdout clean for protocol responses.
|
|
23
|
-
logging.basicConfig(
|
|
24
|
-
stream=sys.stderr,
|
|
25
|
-
level=logging.INFO,
|
|
26
|
-
format="%(asctime)s %(name)s %(levelname)s %(message)s",
|
|
27
|
-
)
|
|
28
|
-
logger = logging.getLogger("synap.bridge")
|
|
29
|
-
|
|
30
|
-
try:
|
|
31
|
-
from maximem_synap import MaximemSynapSDK
|
|
32
|
-
except Exception as import_error: # pragma: no cover
|
|
33
|
-
sys.stderr.write(
|
|
34
|
-
"Failed to import maximem_synap. Run `synap-js-sdk setup` first.\n"
|
|
35
|
-
)
|
|
36
|
-
sys.stderr.write(f"Import error: {import_error}\n")
|
|
37
|
-
raise
|
|
38
|
-
|
|
39
|
-
sdk = None
|
|
40
|
-
user_memory_ids: Dict[str, List[str]] = {}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def ms_since(start: float) -> int:
|
|
44
|
-
return int((time.perf_counter() - start) * 1000)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def append_step(timings: List[dict], step: str, started: float) -> None:
|
|
48
|
-
timings.append({"step": step, "ms": ms_since(started)})
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def write_response(obj: dict) -> None:
|
|
52
|
-
sys.stdout.write(json.dumps(obj, default=str) + "\n")
|
|
53
|
-
sys.stdout.flush()
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def tracking_key(user_id: str, customer_id: Optional[str]) -> str:
|
|
57
|
-
"""Scope tracked memory IDs by both customer and user."""
|
|
58
|
-
return f"{customer_id or ''}::{user_id}"
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def serialize_context_response(context) -> dict:
|
|
62
|
-
"""Serialize a Python ContextResponse for the JS bridge."""
|
|
63
|
-
payload = context.model_dump(mode="json")
|
|
64
|
-
payload["raw_response"] = context.raw if hasattr(context, "raw") else {}
|
|
65
|
-
return payload
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
def serialize_context_for_prompt_response(response) -> dict:
|
|
69
|
-
"""Serialize a Python ContextForPromptResponse for the JS bridge."""
|
|
70
|
-
return response.model_dump(mode="json")
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
def flatten_context_items(context) -> List[dict]:
|
|
74
|
-
"""Convert typed context collections into a flat memory list."""
|
|
75
|
-
items: List[dict] = []
|
|
76
|
-
for fact in context.facts:
|
|
77
|
-
items.append({
|
|
78
|
-
"id": fact.id,
|
|
79
|
-
"memory": fact.content,
|
|
80
|
-
"score": fact.confidence,
|
|
81
|
-
"source": fact.source,
|
|
82
|
-
"metadata": fact.metadata,
|
|
83
|
-
"context_type": "fact",
|
|
84
|
-
"event_date": str(fact.event_date) if getattr(fact, "event_date", None) else None,
|
|
85
|
-
"valid_until": str(fact.valid_until) if getattr(fact, "valid_until", None) else None,
|
|
86
|
-
"temporal_category": getattr(fact, "temporal_category", None),
|
|
87
|
-
"temporal_confidence": getattr(fact, "temporal_confidence", 0.0),
|
|
88
|
-
})
|
|
89
|
-
for preference in context.preferences:
|
|
90
|
-
items.append({
|
|
91
|
-
"id": preference.id,
|
|
92
|
-
"memory": preference.content,
|
|
93
|
-
"score": preference.strength,
|
|
94
|
-
"source": getattr(preference, "source", ""),
|
|
95
|
-
"metadata": preference.metadata,
|
|
96
|
-
"context_type": "preference",
|
|
97
|
-
"event_date": str(preference.event_date) if getattr(preference, "event_date", None) else None,
|
|
98
|
-
"valid_until": str(preference.valid_until) if getattr(preference, "valid_until", None) else None,
|
|
99
|
-
"temporal_category": getattr(preference, "temporal_category", None),
|
|
100
|
-
"temporal_confidence": getattr(preference, "temporal_confidence", 0.0),
|
|
101
|
-
})
|
|
102
|
-
for episode in context.episodes:
|
|
103
|
-
items.append({
|
|
104
|
-
"id": episode.id,
|
|
105
|
-
"memory": episode.summary,
|
|
106
|
-
"score": episode.significance,
|
|
107
|
-
"metadata": episode.metadata,
|
|
108
|
-
"context_type": "episode",
|
|
109
|
-
"event_date": str(episode.event_date) if getattr(episode, "event_date", None) else None,
|
|
110
|
-
"valid_until": str(episode.valid_until) if getattr(episode, "valid_until", None) else None,
|
|
111
|
-
"temporal_category": getattr(episode, "temporal_category", None),
|
|
112
|
-
"temporal_confidence": getattr(episode, "temporal_confidence", 0.0),
|
|
113
|
-
})
|
|
114
|
-
for emotion in context.emotions:
|
|
115
|
-
items.append({
|
|
116
|
-
"id": emotion.id,
|
|
117
|
-
"memory": emotion.context,
|
|
118
|
-
"score": emotion.intensity,
|
|
119
|
-
"metadata": emotion.metadata,
|
|
120
|
-
"context_type": "emotion",
|
|
121
|
-
"event_date": str(emotion.event_date) if getattr(emotion, "event_date", None) else None,
|
|
122
|
-
"valid_until": str(emotion.valid_until) if getattr(emotion, "valid_until", None) else None,
|
|
123
|
-
"temporal_category": getattr(emotion, "temporal_category", None),
|
|
124
|
-
"temporal_confidence": getattr(emotion, "temporal_confidence", 0.0),
|
|
125
|
-
})
|
|
126
|
-
for event in getattr(context, "temporal_events", []):
|
|
127
|
-
items.append({
|
|
128
|
-
"id": event.id,
|
|
129
|
-
"memory": event.content,
|
|
130
|
-
"score": event.temporal_confidence,
|
|
131
|
-
"source": event.source,
|
|
132
|
-
"metadata": event.metadata,
|
|
133
|
-
"context_type": "temporal_event",
|
|
134
|
-
"event_date": str(event.event_date) if event.event_date else None,
|
|
135
|
-
"valid_until": str(event.valid_until) if event.valid_until else None,
|
|
136
|
-
"temporal_category": event.temporal_category,
|
|
137
|
-
"temporal_confidence": event.temporal_confidence,
|
|
138
|
-
})
|
|
139
|
-
return items
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
def messages_to_text(messages: List[dict]) -> str:
|
|
143
|
-
lines: List[str] = []
|
|
144
|
-
for message in messages:
|
|
145
|
-
content = (message.get("content") or "").strip()
|
|
146
|
-
if not content:
|
|
147
|
-
continue
|
|
148
|
-
role = "Assistant" if message.get("role") == "assistant" else "User"
|
|
149
|
-
lines.append(f"{role}: {content}")
|
|
150
|
-
return "\n".join(lines)
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
def on_anticipated_context(bundle: dict) -> None:
|
|
154
|
-
logger.info(
|
|
155
|
-
"Anticipated context bundle received: %s",
|
|
156
|
-
json.dumps(bundle, default=str)[:300],
|
|
157
|
-
)
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
async def handle_init(params: dict) -> dict:
|
|
161
|
-
global sdk
|
|
162
|
-
|
|
163
|
-
handler_start = time.perf_counter()
|
|
164
|
-
timings: List[dict] = []
|
|
165
|
-
|
|
166
|
-
instance_id = params.get("instance_id", "")
|
|
167
|
-
api_key = params.get("api_key")
|
|
168
|
-
|
|
169
|
-
step = time.perf_counter()
|
|
170
|
-
sdk = MaximemSynapSDK(
|
|
171
|
-
instance_id=instance_id,
|
|
172
|
-
api_key=api_key,
|
|
173
|
-
_force_new=True,
|
|
174
|
-
)
|
|
175
|
-
append_step(timings, "construct_sdk", step)
|
|
176
|
-
|
|
177
|
-
config_kwargs = {"log_level": "DEBUG", "cache_backend": "sqlite"}
|
|
178
|
-
|
|
179
|
-
if params.get("base_url"):
|
|
180
|
-
config_kwargs["api_base_url"] = params["base_url"]
|
|
181
|
-
if params.get("grpc_host"):
|
|
182
|
-
config_kwargs["grpc_host"] = params["grpc_host"]
|
|
183
|
-
if params.get("grpc_port"):
|
|
184
|
-
config_kwargs["grpc_port"] = int(params["grpc_port"])
|
|
185
|
-
if "grpc_use_tls" in params:
|
|
186
|
-
config_kwargs["grpc_use_tls"] = bool(params["grpc_use_tls"])
|
|
187
|
-
|
|
188
|
-
step = time.perf_counter()
|
|
189
|
-
sdk.configure(**config_kwargs)
|
|
190
|
-
append_step(timings, "configure_sdk", step)
|
|
191
|
-
|
|
192
|
-
step = time.perf_counter()
|
|
193
|
-
await sdk.initialize()
|
|
194
|
-
append_step(timings, "initialize_sdk", step)
|
|
195
|
-
|
|
196
|
-
grpc_listening = False
|
|
197
|
-
|
|
198
|
-
step = time.perf_counter()
|
|
199
|
-
try:
|
|
200
|
-
await sdk.instance.listen(
|
|
201
|
-
on_context=on_anticipated_context,
|
|
202
|
-
on_reconnect=lambda attempt: logger.info(
|
|
203
|
-
"gRPC reconnect attempt %d", attempt
|
|
204
|
-
),
|
|
205
|
-
on_disconnect=lambda reason: logger.warning(
|
|
206
|
-
"gRPC disconnected: %s", reason
|
|
207
|
-
),
|
|
208
|
-
)
|
|
209
|
-
grpc_listening = sdk.instance.is_listening
|
|
210
|
-
except Exception as exc: # non-fatal
|
|
211
|
-
logger.warning("gRPC listen failed (non-fatal): %s", exc)
|
|
212
|
-
append_step(timings, "start_grpc_listener", step)
|
|
213
|
-
|
|
214
|
-
return {
|
|
215
|
-
"success": True,
|
|
216
|
-
"instance_id": instance_id,
|
|
217
|
-
"grpc_listening": grpc_listening,
|
|
218
|
-
"bridgeTiming": {
|
|
219
|
-
"python_total_ms": ms_since(handler_start),
|
|
220
|
-
"steps": timings,
|
|
221
|
-
},
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
async def handle_add_memory(params: dict) -> dict:
|
|
226
|
-
handler_start = time.perf_counter()
|
|
227
|
-
timings: List[dict] = []
|
|
228
|
-
|
|
229
|
-
user_id = params["user_id"]
|
|
230
|
-
customer_id = params.get("customer_id")
|
|
231
|
-
messages = params["messages"]
|
|
232
|
-
conversation_id = params.get("conversation_id")
|
|
233
|
-
session_id = params.get("session_id")
|
|
234
|
-
|
|
235
|
-
if not customer_id:
|
|
236
|
-
raise ValueError("customer_id is required")
|
|
237
|
-
|
|
238
|
-
step = time.perf_counter()
|
|
239
|
-
transcript = messages_to_text(messages)
|
|
240
|
-
append_step(timings, "build_transcript", step)
|
|
241
|
-
|
|
242
|
-
if not transcript:
|
|
243
|
-
return {
|
|
244
|
-
"success": True,
|
|
245
|
-
"latencyMs": 0,
|
|
246
|
-
"rawResponse": {"note": "Empty input; skipped"},
|
|
247
|
-
"note": "No text content to ingest",
|
|
248
|
-
"bridgeTiming": {
|
|
249
|
-
"python_total_ms": ms_since(handler_start),
|
|
250
|
-
"steps": timings,
|
|
251
|
-
},
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
start = time.perf_counter()
|
|
255
|
-
|
|
256
|
-
step = time.perf_counter()
|
|
257
|
-
mode = params.get("mode", "long-range")
|
|
258
|
-
document_type = params.get("document_type", "ai-chat-conversation")
|
|
259
|
-
document_id = params.get("document_id")
|
|
260
|
-
document_created_at = params.get("document_created_at")
|
|
261
|
-
metadata = params.get("metadata")
|
|
262
|
-
|
|
263
|
-
create_kwargs: dict = {
|
|
264
|
-
"document": transcript,
|
|
265
|
-
"document_type": document_type,
|
|
266
|
-
"user_id": user_id,
|
|
267
|
-
"customer_id": customer_id,
|
|
268
|
-
"mode": mode,
|
|
269
|
-
}
|
|
270
|
-
if document_id is not None:
|
|
271
|
-
create_kwargs["document_id"] = document_id
|
|
272
|
-
if document_created_at is not None:
|
|
273
|
-
create_kwargs["document_created_at"] = document_created_at
|
|
274
|
-
if metadata is not None:
|
|
275
|
-
create_kwargs["metadata"] = metadata
|
|
276
|
-
|
|
277
|
-
create_result = await sdk.memories.create(**create_kwargs)
|
|
278
|
-
append_step(timings, "memories_create", step)
|
|
279
|
-
|
|
280
|
-
ingestion_id = create_result.ingestion_id
|
|
281
|
-
|
|
282
|
-
step = time.perf_counter()
|
|
283
|
-
if sdk.instance.is_listening:
|
|
284
|
-
for message in messages:
|
|
285
|
-
content = (message.get("content") or "").strip()
|
|
286
|
-
if not content:
|
|
287
|
-
continue
|
|
288
|
-
try:
|
|
289
|
-
role = message.get("role", "user")
|
|
290
|
-
await sdk.instance.send_message(
|
|
291
|
-
content=content,
|
|
292
|
-
role=role,
|
|
293
|
-
conversation_id=conversation_id,
|
|
294
|
-
user_id=user_id,
|
|
295
|
-
customer_id=customer_id,
|
|
296
|
-
session_id=session_id,
|
|
297
|
-
event_type="assistant_message" if role == "assistant" else "user_message",
|
|
298
|
-
metadata=message.get("metadata"),
|
|
299
|
-
)
|
|
300
|
-
except Exception as exc:
|
|
301
|
-
logger.debug("gRPC send_message failed (non-fatal): %s", exc)
|
|
302
|
-
append_step(timings, "grpc_send_messages", step)
|
|
303
|
-
|
|
304
|
-
step = time.perf_counter()
|
|
305
|
-
try:
|
|
306
|
-
final_status = await sdk.memories.wait_for_completion(
|
|
307
|
-
ingestion_id,
|
|
308
|
-
timeout_seconds=60,
|
|
309
|
-
poll_interval_seconds=2,
|
|
310
|
-
)
|
|
311
|
-
except TimeoutError:
|
|
312
|
-
append_step(timings, "wait_for_completion_timeout", step)
|
|
313
|
-
return {
|
|
314
|
-
"success": True,
|
|
315
|
-
"latencyMs": ms_since(start),
|
|
316
|
-
"rawResponse": {"ingestion_id": str(ingestion_id), "status": "timeout"},
|
|
317
|
-
"note": "Ingestion timed out after 60s and may still complete later",
|
|
318
|
-
"bridgeTiming": {
|
|
319
|
-
"python_total_ms": ms_since(handler_start),
|
|
320
|
-
"steps": timings,
|
|
321
|
-
},
|
|
322
|
-
}
|
|
323
|
-
append_step(timings, "wait_for_completion", step)
|
|
324
|
-
|
|
325
|
-
memory_ids = [str(memory_id) for memory_id in (final_status.memory_ids or [])]
|
|
326
|
-
if memory_ids:
|
|
327
|
-
user_memory_ids.setdefault(tracking_key(user_id, customer_id), []).extend(memory_ids)
|
|
328
|
-
|
|
329
|
-
return {
|
|
330
|
-
"success": final_status.status.value != "failed",
|
|
331
|
-
"latencyMs": ms_since(start),
|
|
332
|
-
"rawResponse": {
|
|
333
|
-
"ingestion_id": str(ingestion_id),
|
|
334
|
-
"status": final_status.status.value,
|
|
335
|
-
"memories_created": final_status.memories_created,
|
|
336
|
-
"memory_ids": memory_ids,
|
|
337
|
-
},
|
|
338
|
-
"note": f"Ingestion {final_status.status.value}; created {final_status.memories_created}",
|
|
339
|
-
"bridgeTiming": {
|
|
340
|
-
"python_total_ms": ms_since(handler_start),
|
|
341
|
-
"steps": timings,
|
|
342
|
-
},
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
async def handle_search_memory(params: dict) -> dict:
|
|
347
|
-
handler_start = time.perf_counter()
|
|
348
|
-
timings: List[dict] = []
|
|
349
|
-
|
|
350
|
-
user_id = params["user_id"]
|
|
351
|
-
customer_id = params.get("customer_id")
|
|
352
|
-
query = params["query"]
|
|
353
|
-
max_results = params.get("max_results", 10)
|
|
354
|
-
mode = params.get("mode", "fast")
|
|
355
|
-
conversation_id = params.get("conversation_id")
|
|
356
|
-
types = params.get("types", ["all"])
|
|
357
|
-
|
|
358
|
-
start = time.perf_counter()
|
|
359
|
-
|
|
360
|
-
step = time.perf_counter()
|
|
361
|
-
context = await sdk.user.context.fetch(
|
|
362
|
-
user_id=user_id,
|
|
363
|
-
customer_id=customer_id,
|
|
364
|
-
conversation_id=conversation_id,
|
|
365
|
-
search_query=[query],
|
|
366
|
-
max_results=max_results,
|
|
367
|
-
types=types,
|
|
368
|
-
mode=mode,
|
|
369
|
-
)
|
|
370
|
-
append_step(timings, "context_fetch", step)
|
|
371
|
-
|
|
372
|
-
step = time.perf_counter()
|
|
373
|
-
results = flatten_context_items(context)
|
|
374
|
-
append_step(timings, "map_context_results", step)
|
|
375
|
-
|
|
376
|
-
return {
|
|
377
|
-
"success": True,
|
|
378
|
-
"latencyMs": ms_since(start),
|
|
379
|
-
"results": results,
|
|
380
|
-
"resultsCount": len(results),
|
|
381
|
-
"rawResponse": context.raw if hasattr(context, "raw") else {},
|
|
382
|
-
"source": context.metadata.source if context.metadata else "unknown",
|
|
383
|
-
"bridgeTiming": {
|
|
384
|
-
"python_total_ms": ms_since(handler_start),
|
|
385
|
-
"steps": timings,
|
|
386
|
-
},
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
async def handle_get_memories(params: dict) -> dict:
|
|
391
|
-
handler_start = time.perf_counter()
|
|
392
|
-
timings: List[dict] = []
|
|
393
|
-
|
|
394
|
-
user_id = params["user_id"]
|
|
395
|
-
customer_id = params.get("customer_id")
|
|
396
|
-
mode = params.get("mode", "fast")
|
|
397
|
-
conversation_id = params.get("conversation_id")
|
|
398
|
-
max_results = params.get("max_results", 100)
|
|
399
|
-
types = params.get("types", ["all"])
|
|
400
|
-
|
|
401
|
-
start = time.perf_counter()
|
|
402
|
-
|
|
403
|
-
step = time.perf_counter()
|
|
404
|
-
context = await sdk.user.context.fetch(
|
|
405
|
-
user_id=user_id,
|
|
406
|
-
customer_id=customer_id,
|
|
407
|
-
conversation_id=conversation_id,
|
|
408
|
-
search_query=[],
|
|
409
|
-
max_results=max_results,
|
|
410
|
-
types=types,
|
|
411
|
-
mode=mode,
|
|
412
|
-
)
|
|
413
|
-
append_step(timings, "context_fetch_all", step)
|
|
414
|
-
|
|
415
|
-
step = time.perf_counter()
|
|
416
|
-
memories = flatten_context_items(context)
|
|
417
|
-
append_step(timings, "map_memories", step)
|
|
418
|
-
|
|
419
|
-
return {
|
|
420
|
-
"success": True,
|
|
421
|
-
"latencyMs": ms_since(start),
|
|
422
|
-
"memories": memories,
|
|
423
|
-
"memoriesCount": len(memories),
|
|
424
|
-
"totalCount": len(memories),
|
|
425
|
-
"rawResponse": context.raw if hasattr(context, "raw") else {},
|
|
426
|
-
"source": context.metadata.source if context.metadata else "unknown",
|
|
427
|
-
"bridgeTiming": {
|
|
428
|
-
"python_total_ms": ms_since(handler_start),
|
|
429
|
-
"steps": timings,
|
|
430
|
-
},
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
async def handle_fetch_user_context(params: dict) -> dict:
|
|
435
|
-
handler_start = time.perf_counter()
|
|
436
|
-
timings: List[dict] = []
|
|
437
|
-
start = time.perf_counter()
|
|
438
|
-
|
|
439
|
-
step = time.perf_counter()
|
|
440
|
-
context = await sdk.user.context.fetch(
|
|
441
|
-
user_id=params["user_id"],
|
|
442
|
-
customer_id=params.get("customer_id"),
|
|
443
|
-
conversation_id=params.get("conversation_id"),
|
|
444
|
-
search_query=params.get("search_query"),
|
|
445
|
-
max_results=params.get("max_results", 10),
|
|
446
|
-
types=params.get("types"),
|
|
447
|
-
mode=params.get("mode", "fast"),
|
|
448
|
-
)
|
|
449
|
-
append_step(timings, "context_fetch", step)
|
|
450
|
-
|
|
451
|
-
return {
|
|
452
|
-
"success": True,
|
|
453
|
-
"latencyMs": ms_since(start),
|
|
454
|
-
"context": serialize_context_response(context),
|
|
455
|
-
"bridgeTiming": {
|
|
456
|
-
"python_total_ms": ms_since(handler_start),
|
|
457
|
-
"steps": timings,
|
|
458
|
-
},
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
async def handle_fetch_customer_context(params: dict) -> dict:
|
|
463
|
-
handler_start = time.perf_counter()
|
|
464
|
-
timings: List[dict] = []
|
|
465
|
-
start = time.perf_counter()
|
|
466
|
-
|
|
467
|
-
step = time.perf_counter()
|
|
468
|
-
context = await sdk.customer.context.fetch(
|
|
469
|
-
customer_id=params["customer_id"],
|
|
470
|
-
conversation_id=params.get("conversation_id"),
|
|
471
|
-
search_query=params.get("search_query"),
|
|
472
|
-
max_results=params.get("max_results", 10),
|
|
473
|
-
types=params.get("types"),
|
|
474
|
-
mode=params.get("mode", "fast"),
|
|
475
|
-
)
|
|
476
|
-
append_step(timings, "context_fetch", step)
|
|
477
|
-
|
|
478
|
-
return {
|
|
479
|
-
"success": True,
|
|
480
|
-
"latencyMs": ms_since(start),
|
|
481
|
-
"context": serialize_context_response(context),
|
|
482
|
-
"bridgeTiming": {
|
|
483
|
-
"python_total_ms": ms_since(handler_start),
|
|
484
|
-
"steps": timings,
|
|
485
|
-
},
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
async def handle_fetch_client_context(params: dict) -> dict:
|
|
490
|
-
handler_start = time.perf_counter()
|
|
491
|
-
timings: List[dict] = []
|
|
492
|
-
start = time.perf_counter()
|
|
493
|
-
|
|
494
|
-
step = time.perf_counter()
|
|
495
|
-
context = await sdk.client.context.fetch(
|
|
496
|
-
conversation_id=params.get("conversation_id"),
|
|
497
|
-
search_query=params.get("search_query"),
|
|
498
|
-
max_results=params.get("max_results", 10),
|
|
499
|
-
types=params.get("types"),
|
|
500
|
-
mode=params.get("mode", "fast"),
|
|
501
|
-
)
|
|
502
|
-
append_step(timings, "context_fetch", step)
|
|
503
|
-
|
|
504
|
-
return {
|
|
505
|
-
"success": True,
|
|
506
|
-
"latencyMs": ms_since(start),
|
|
507
|
-
"context": serialize_context_response(context),
|
|
508
|
-
"bridgeTiming": {
|
|
509
|
-
"python_total_ms": ms_since(handler_start),
|
|
510
|
-
"steps": timings,
|
|
511
|
-
},
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
async def handle_get_context_for_prompt(params: dict) -> dict:
|
|
516
|
-
handler_start = time.perf_counter()
|
|
517
|
-
timings: List[dict] = []
|
|
518
|
-
start = time.perf_counter()
|
|
519
|
-
|
|
520
|
-
step = time.perf_counter()
|
|
521
|
-
response = await sdk.conversation.get_context_for_prompt(
|
|
522
|
-
conversation_id=params["conversation_id"],
|
|
523
|
-
style=params.get("style", "structured"),
|
|
524
|
-
)
|
|
525
|
-
append_step(timings, "get_context_for_prompt", step)
|
|
526
|
-
|
|
527
|
-
return {
|
|
528
|
-
"success": True,
|
|
529
|
-
"latencyMs": ms_since(start),
|
|
530
|
-
"context_for_prompt": serialize_context_for_prompt_response(response),
|
|
531
|
-
"bridgeTiming": {
|
|
532
|
-
"python_total_ms": ms_since(handler_start),
|
|
533
|
-
"steps": timings,
|
|
534
|
-
},
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
async def handle_delete_memory(params: dict) -> dict:
|
|
539
|
-
handler_start = time.perf_counter()
|
|
540
|
-
timings: List[dict] = []
|
|
541
|
-
|
|
542
|
-
user_id = params["user_id"]
|
|
543
|
-
customer_id = params.get("customer_id")
|
|
544
|
-
memory_id = params.get("memory_id")
|
|
545
|
-
|
|
546
|
-
start = time.perf_counter()
|
|
547
|
-
|
|
548
|
-
step = time.perf_counter()
|
|
549
|
-
if memory_id:
|
|
550
|
-
await sdk.memories.delete(UUID(memory_id))
|
|
551
|
-
append_step(timings, "delete_single_memory", step)
|
|
552
|
-
return {
|
|
553
|
-
"success": True,
|
|
554
|
-
"latencyMs": ms_since(start),
|
|
555
|
-
"deletedCount": 1,
|
|
556
|
-
"rawResponse": {"deleted": 1},
|
|
557
|
-
"bridgeTiming": {
|
|
558
|
-
"python_total_ms": ms_since(handler_start),
|
|
559
|
-
"steps": timings,
|
|
560
|
-
},
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
if not customer_id:
|
|
564
|
-
raise ValueError("customer_id is required when memory_id is not provided")
|
|
565
|
-
|
|
566
|
-
tracked_ids = user_memory_ids.get(tracking_key(user_id, customer_id), [])
|
|
567
|
-
if not tracked_ids:
|
|
568
|
-
return {
|
|
569
|
-
"success": True,
|
|
570
|
-
"latencyMs": 0,
|
|
571
|
-
"deletedCount": 0,
|
|
572
|
-
"rawResponse": None,
|
|
573
|
-
"note": "No tracked memory IDs for this user",
|
|
574
|
-
"bridgeTiming": {
|
|
575
|
-
"python_total_ms": ms_since(handler_start),
|
|
576
|
-
"steps": timings,
|
|
577
|
-
},
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
last_error: Optional[str] = None
|
|
581
|
-
|
|
582
|
-
step = time.perf_counter()
|
|
583
|
-
for tracked_id in tracked_ids:
|
|
584
|
-
try:
|
|
585
|
-
await sdk.memories.delete(UUID(tracked_id))
|
|
586
|
-
except Exception as exc:
|
|
587
|
-
last_error = str(exc)
|
|
588
|
-
append_step(timings, "delete_tracked_memories", step)
|
|
589
|
-
|
|
590
|
-
user_memory_ids.pop(tracking_key(user_id, customer_id), None)
|
|
591
|
-
|
|
592
|
-
if last_error:
|
|
593
|
-
return {
|
|
594
|
-
"success": False,
|
|
595
|
-
"latencyMs": ms_since(start),
|
|
596
|
-
"error": last_error,
|
|
597
|
-
"bridgeTiming": {
|
|
598
|
-
"python_total_ms": ms_since(handler_start),
|
|
599
|
-
"steps": timings,
|
|
600
|
-
},
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
return {
|
|
604
|
-
"success": True,
|
|
605
|
-
"latencyMs": ms_since(start),
|
|
606
|
-
"deletedCount": len(tracked_ids),
|
|
607
|
-
"rawResponse": {"deleted": len(tracked_ids)},
|
|
608
|
-
"note": f"Deleted {len(tracked_ids)} memories",
|
|
609
|
-
"bridgeTiming": {
|
|
610
|
-
"python_total_ms": ms_since(handler_start),
|
|
611
|
-
"steps": timings,
|
|
612
|
-
},
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
async def handle_shutdown(_params: dict) -> dict:
|
|
617
|
-
handler_start = time.perf_counter()
|
|
618
|
-
timings: List[dict] = []
|
|
619
|
-
|
|
620
|
-
if sdk:
|
|
621
|
-
step = time.perf_counter()
|
|
622
|
-
try:
|
|
623
|
-
await sdk.instance.stop_listening()
|
|
624
|
-
except Exception:
|
|
625
|
-
pass
|
|
626
|
-
append_step(timings, "stop_listener", step)
|
|
627
|
-
|
|
628
|
-
step = time.perf_counter()
|
|
629
|
-
await sdk.shutdown()
|
|
630
|
-
append_step(timings, "sdk_shutdown", step)
|
|
631
|
-
return {
|
|
632
|
-
"success": True,
|
|
633
|
-
"bridgeTiming": {
|
|
634
|
-
"python_total_ms": ms_since(handler_start),
|
|
635
|
-
"steps": timings,
|
|
636
|
-
},
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
async def handle_record_message(params: dict) -> dict:
|
|
641
|
-
"""Record a single conversation turn (registers the conversation_id).
|
|
642
|
-
|
|
643
|
-
Mirrors sdk.conversation.record_message(...) so the JS client can expose
|
|
644
|
-
the same namespaced surface the integration packages duck-type against.
|
|
645
|
-
"""
|
|
646
|
-
handler_start = time.perf_counter()
|
|
647
|
-
timings: List[dict] = []
|
|
648
|
-
start = time.perf_counter()
|
|
649
|
-
step = time.perf_counter()
|
|
650
|
-
result = await sdk.conversation.record_message(
|
|
651
|
-
conversation_id=params["conversation_id"],
|
|
652
|
-
role=params["role"],
|
|
653
|
-
content=params["content"],
|
|
654
|
-
user_id=params["user_id"],
|
|
655
|
-
customer_id=params["customer_id"],
|
|
656
|
-
session_id=params.get("session_id"),
|
|
657
|
-
metadata=params.get("metadata"),
|
|
658
|
-
)
|
|
659
|
-
append_step(timings, "record_message", step)
|
|
660
|
-
return {
|
|
661
|
-
"success": True,
|
|
662
|
-
"latencyMs": ms_since(start),
|
|
663
|
-
"result": result,
|
|
664
|
-
"bridgeTiming": {"python_total_ms": ms_since(handler_start), "steps": timings},
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
async def handle_create_memory(params: dict) -> dict:
|
|
669
|
-
"""Create a durable memory from a single document string.
|
|
670
|
-
|
|
671
|
-
Mirrors sdk.memories.create(document=...). Distinct from add_memory,
|
|
672
|
-
which builds a transcript from a messages array.
|
|
673
|
-
"""
|
|
674
|
-
handler_start = time.perf_counter()
|
|
675
|
-
timings: List[dict] = []
|
|
676
|
-
start = time.perf_counter()
|
|
677
|
-
create_kwargs: dict = {"document": params["document"]}
|
|
678
|
-
for key in ("user_id", "customer_id", "document_type", "document_id",
|
|
679
|
-
"document_created_at", "mode", "metadata"):
|
|
680
|
-
if params.get(key) is not None:
|
|
681
|
-
create_kwargs[key] = params[key]
|
|
682
|
-
step = time.perf_counter()
|
|
683
|
-
result = await sdk.memories.create(**create_kwargs)
|
|
684
|
-
append_step(timings, "memories_create", step)
|
|
685
|
-
status = result.status.value if hasattr(result.status, "value") else result.status
|
|
686
|
-
return {
|
|
687
|
-
"success": True,
|
|
688
|
-
"latencyMs": ms_since(start),
|
|
689
|
-
"result": {
|
|
690
|
-
"ingestion_id": str(result.ingestion_id),
|
|
691
|
-
"document_id": result.document_id,
|
|
692
|
-
"status": status,
|
|
693
|
-
},
|
|
694
|
-
"bridgeTiming": {"python_total_ms": ms_since(handler_start), "steps": timings},
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
HANDLERS = {
|
|
699
|
-
"init": handle_init,
|
|
700
|
-
"add_memory": handle_add_memory,
|
|
701
|
-
"create_memory": handle_create_memory,
|
|
702
|
-
"record_message": handle_record_message,
|
|
703
|
-
"search_memory": handle_search_memory,
|
|
704
|
-
"get_memories": handle_get_memories,
|
|
705
|
-
"fetch_user_context": handle_fetch_user_context,
|
|
706
|
-
"fetch_customer_context": handle_fetch_customer_context,
|
|
707
|
-
"fetch_client_context": handle_fetch_client_context,
|
|
708
|
-
"get_context_for_prompt": handle_get_context_for_prompt,
|
|
709
|
-
"delete_memory": handle_delete_memory,
|
|
710
|
-
"shutdown": handle_shutdown,
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
async def main() -> None:
|
|
715
|
-
logger.info("Synap bridge starting")
|
|
716
|
-
|
|
717
|
-
reader = asyncio.StreamReader()
|
|
718
|
-
protocol = asyncio.StreamReaderProtocol(reader)
|
|
719
|
-
await asyncio.get_event_loop().connect_read_pipe(lambda: protocol, sys.stdin)
|
|
720
|
-
|
|
721
|
-
while True:
|
|
722
|
-
line = await reader.readline()
|
|
723
|
-
if not line:
|
|
724
|
-
logger.info("stdin closed; shutting down bridge")
|
|
725
|
-
if sdk and getattr(sdk, "_initialized", False):
|
|
726
|
-
await handle_shutdown({})
|
|
727
|
-
break
|
|
728
|
-
|
|
729
|
-
payload = line.decode().strip()
|
|
730
|
-
if not payload:
|
|
731
|
-
continue
|
|
732
|
-
|
|
733
|
-
try:
|
|
734
|
-
request = json.loads(payload)
|
|
735
|
-
except json.JSONDecodeError as exc:
|
|
736
|
-
write_response({"id": None, "result": None, "error": f"Invalid JSON: {exc}"})
|
|
737
|
-
continue
|
|
738
|
-
|
|
739
|
-
req_id = request.get("id")
|
|
740
|
-
method = request.get("method")
|
|
741
|
-
params = request.get("params", {})
|
|
742
|
-
|
|
743
|
-
handler = HANDLERS.get(method)
|
|
744
|
-
if not handler:
|
|
745
|
-
write_response({"id": req_id, "result": None, "error": f"Unknown method: {method}"})
|
|
746
|
-
continue
|
|
747
|
-
|
|
748
|
-
try:
|
|
749
|
-
handler_started = time.perf_counter()
|
|
750
|
-
result = await handler(params)
|
|
751
|
-
if isinstance(result, dict):
|
|
752
|
-
bridge_timing = result.get("bridgeTiming")
|
|
753
|
-
if isinstance(bridge_timing, dict):
|
|
754
|
-
bridge_timing.setdefault("python_total_ms", ms_since(handler_started))
|
|
755
|
-
bridge_timing.setdefault("steps", [])
|
|
756
|
-
else:
|
|
757
|
-
result["bridgeTiming"] = {
|
|
758
|
-
"python_total_ms": ms_since(handler_started),
|
|
759
|
-
"steps": [],
|
|
760
|
-
}
|
|
761
|
-
write_response({"id": req_id, "result": result, "error": None})
|
|
762
|
-
except Exception as exc:
|
|
763
|
-
logger.error("Handler error for %s: %s", method, traceback.format_exc())
|
|
764
|
-
write_response({"id": req_id, "result": None, "error": str(exc), "error_type": type(exc).__name__})
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
if __name__ == "__main__":
|
|
768
|
-
asyncio.run(main())
|