prismlib-plus 0.7.0__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.
Files changed (56) hide show
  1. prism/__init__.py +9 -0
  2. prism/api/__init__.py +83 -0
  3. prism/api/auth.py +127 -0
  4. prism/api/consumer.py +573 -0
  5. prism/api/integrations/__init__.py +30 -0
  6. prism/api/integrations/langgraph.py +382 -0
  7. prism/api/mcp.py +341 -0
  8. prism/api/multi_provider.py +315 -0
  9. prism/api/provider.py +496 -0
  10. prism/api/schema.py +296 -0
  11. prism/bridge/__init__.py +32 -0
  12. prism/bridge/vector.py +704 -0
  13. prism/cache/__init__.py +54 -0
  14. prism/cache/cache.py +637 -0
  15. prism/cache/embedder.py +438 -0
  16. prism/cache/metrics.py +273 -0
  17. prism/cache/store.py +370 -0
  18. prism/cluster/__init__.py +20 -0
  19. prism/cluster/alerts.py +550 -0
  20. prism/cluster/cache.py +480 -0
  21. prism/cluster/health.py +87 -0
  22. prism/cluster/node.py +288 -0
  23. prism/cluster/transport.py +81 -0
  24. prism/enterprise/__init__.py +5 -0
  25. prism/enterprise/app.py +70 -0
  26. prism/ffi/__init__.py +34 -0
  27. prism/ffi/bindings.py +1055 -0
  28. prism/ffi/grpc_client.py +121 -0
  29. prism/lib/__init__.py +17 -0
  30. prism/lib/fabric.py +1141 -0
  31. prism/lib/lang.py +531 -0
  32. prism/lib/resonance.py +691 -0
  33. prism/observability/__init__.py +110 -0
  34. prism/observability/otel.py +85 -0
  35. prism/observability/prometheus.py +11 -0
  36. prism/security/__init__.py +17 -0
  37. prism/security/audit.py +80 -0
  38. prism/security/rate_limit.py +74 -0
  39. prism/security/tls.py +48 -0
  40. prism/wrapper/__init__.py +49 -0
  41. prism/wrapper/config.py +132 -0
  42. prism/wrapper/daemon.py +261 -0
  43. prism/wrapper/grpc_server.py +248 -0
  44. prism/wrapper/interceptor.py +543 -0
  45. prism/wrapper/main.py +5 -0
  46. prism/wrapper/proto/__init__.py +0 -0
  47. prism/wrapper/proto/chorus_pb2.py +71 -0
  48. prism/wrapper/proto/chorus_pb2_grpc.py +454 -0
  49. prism/wrapper/publisher.py +255 -0
  50. prism/wrapper/row_events.py +129 -0
  51. prism/wrapper/subscribe_server.py +78 -0
  52. prismlib_plus-0.7.0.dist-info/METADATA +748 -0
  53. prismlib_plus-0.7.0.dist-info/RECORD +56 -0
  54. prismlib_plus-0.7.0.dist-info/WHEEL +5 -0
  55. prismlib_plus-0.7.0.dist-info/entry_points.txt +2 -0
  56. prismlib_plus-0.7.0.dist-info/top_level.txt +1 -0
prism/api/consumer.py ADDED
@@ -0,0 +1,573 @@
1
+ """
2
+ prism.api.consumer — PrismAPIClient and LangGraphTool
3
+ ======================================================
4
+
5
+ The consumer side of PrismAPI. An agent (e.g. in LangGraph) uses
6
+ PrismAPIClient instead of a plain HTTP client to query a PrismAPIProvider
7
+ endpoint. The difference:
8
+
9
+ Plain HTTP client:
10
+ agent → HTTP GET /search?q=... → JSON body with text
11
+ agent → embed(text) → float32 vector
12
+ agent → retrieval step with vector
13
+ Cost: 1 HTTP call + 1 embedding call per search
14
+
15
+ PrismAPIClient:
16
+ agent → CHORUS API_REQUEST (query vector) → API_RESPONSE (float32 vectors)
17
+ agent → retrieval step with vectors (already embedded + projected)
18
+ Cost: 1 CHORUS frame round-trip, 0 embedding calls on the consumer side
19
+
20
+ The embedding call is not eliminated globally — the PROVIDER embeds the
21
+ content once when it indexes it. What is eliminated is the CONSUMER re-
22
+ embedding on every retrieval. At scale, a consumer that handles 1,000 queries
23
+ per second and gets 10 results per query avoids 10,000 embedding API calls
24
+ per second.
25
+
26
+ In-process vs networked
27
+ -----------------------
28
+ PrismAPIClient works in two modes:
29
+
30
+ Networked (production):
31
+ Pass `host` and `port` pointing at a server running ASGIAdapter.
32
+ Frames travel over HTTP(S) with Content-Type: application/x-chorus-frame.
33
+ (Full gRPC support is on the roadmap; HTTP transport is sufficient for
34
+ throughput at typical retrieval scales.)
35
+
36
+ In-process (loopback):
37
+ Pass a PrismAPIProvider directly as `loopback_provider`.
38
+ Frames are serialised and deserialised in memory — identical wire path,
39
+ no network involved. Used in benchmarks and tests.
40
+ """
41
+
42
+ from __future__ import annotations
43
+
44
+ import http.client
45
+ import logging
46
+ import time
47
+ import uuid
48
+ from dataclasses import dataclass, field
49
+ from typing import Any, Callable, Optional, Sequence
50
+
51
+ import numpy as np
52
+
53
+ from prism.lib.fabric import CHORUSFrame, FabricConfig, FrameType, TensorCipher
54
+ from prism.lib.lang import PrismProjector, ProjectionConfig
55
+ from prism.api.schema import (
56
+ APIRequest,
57
+ APIResponse,
58
+ Embedder,
59
+ ExactSidecar,
60
+ SemanticItem,
61
+ unpack_response_payload,
62
+ )
63
+
64
+ logger = logging.getLogger(__name__)
65
+
66
+
67
+ # ---------------------------------------------------------------------------
68
+ # Retry configuration
69
+ # ---------------------------------------------------------------------------
70
+
71
+
72
+ @dataclass
73
+ class RetryConfig:
74
+ """
75
+ Controls retry behaviour for the production HTTP client.
76
+
77
+ Attributes
78
+ ----------
79
+ max_retries:
80
+ Maximum number of retry attempts after the initial failure (0 = no retry).
81
+ backoff_base:
82
+ Base sleep duration in seconds. Sleep doubles each retry:
83
+ backoff_base, 2×, 4× ... Capped at backoff_max.
84
+ backoff_max:
85
+ Maximum sleep between retries.
86
+ timeout_connect:
87
+ TCP connect timeout in seconds.
88
+ timeout_read:
89
+ Socket read timeout in seconds.
90
+ """
91
+
92
+ max_retries: int = 3
93
+ backoff_base: float = 0.5
94
+ backoff_max: float = 8.0
95
+ timeout_connect: float = 5.0
96
+ timeout_read: float = 30.0
97
+
98
+ @property
99
+ def timeout(self) -> float:
100
+ """Total socket timeout (connect + read)."""
101
+ return self.timeout_connect + self.timeout_read
102
+
103
+
104
+ # ---------------------------------------------------------------------------
105
+ # Errors
106
+ # ---------------------------------------------------------------------------
107
+
108
+
109
+ class ConsumerError(Exception):
110
+ """Base error for PrismAPIClient operations."""
111
+
112
+
113
+ class FrameTypeError(ConsumerError):
114
+ """Raised when the server returns an unexpected frame type."""
115
+
116
+
117
+ class TransportError(ConsumerError):
118
+ """Raised on network-level failures."""
119
+
120
+
121
+ # ---------------------------------------------------------------------------
122
+ # PrismAPIClient
123
+ # ---------------------------------------------------------------------------
124
+
125
+
126
+ class PrismAPIClient:
127
+ """
128
+ Connects to a PrismAPIProvider endpoint and retrieves float32 vectors
129
+ plus exact sidecar metadata — no re-embedding on the consumer side.
130
+
131
+ Parameters
132
+ ----------
133
+ projector:
134
+ PrismProjector for the consumer's tenant. Used to project the
135
+ query embedding into the provider's target_dim space before sending.
136
+ embedder:
137
+ Embedder used for the query text. The consumer still embeds its
138
+ OWN query — what it avoids is embedding the RESULTS it gets back.
139
+ loopback_provider:
140
+ If provided, frames are exchanged in-process (for tests / benchmarks).
141
+ Mutually exclusive with host/port.
142
+ host, port:
143
+ Remote PrismAPIProvider address for networked mode.
144
+ source_field:
145
+ Label to attach to returned SemanticItems (informational only).
146
+ """
147
+
148
+ def __init__(
149
+ self,
150
+ projector: PrismProjector,
151
+ embedder: Embedder,
152
+ loopback_provider: Optional[Any] = None, # PrismAPIProvider
153
+ host: str = "localhost",
154
+ port: int = 9100,
155
+ source_field: str = "body",
156
+ retry: Optional[RetryConfig] = None,
157
+ chorus_path: str = "/chorus/search",
158
+ api_key: Optional[str] = None,
159
+ bearer_token: Optional[str] = None,
160
+ ) -> None:
161
+ self._projector = projector
162
+ self._embedder = embedder
163
+ self._loopback = loopback_provider
164
+ self._host = host
165
+ self._port = port
166
+ self._source_field = source_field
167
+ self._retry = retry or RetryConfig()
168
+ self._chorus_path = chorus_path
169
+ self._api_key = api_key
170
+ self._bearer_token = bearer_token
171
+
172
+ # Persistent HTTP connection (keep-alive, reused across requests)
173
+ self._conn: Optional[http.client.HTTPConnection] = None
174
+
175
+ # Cipher for signing outbound request frames
176
+ dim = projector._cfg.target_dim
177
+ self._cipher = TensorCipher(dim=dim, ttl_seconds=3600.0)
178
+ self._cipher.rotate_key()
179
+ self._seq = 0
180
+
181
+ # ------------------------------------------------------------------
182
+ # Connection management
183
+ # ------------------------------------------------------------------
184
+
185
+ def _get_conn(self) -> http.client.HTTPConnection:
186
+ """Return a live persistent connection, creating one if needed."""
187
+ if self._conn is None:
188
+ self._conn = http.client.HTTPConnection(
189
+ self._host,
190
+ self._port,
191
+ timeout=self._retry.timeout,
192
+ )
193
+ return self._conn
194
+
195
+ def _close_conn(self) -> None:
196
+ if self._conn is not None:
197
+ try:
198
+ self._conn.close()
199
+ except Exception:
200
+ pass
201
+ self._conn = None
202
+
203
+ def close(self) -> None:
204
+ """Close the persistent HTTP connection."""
205
+ self._close_conn()
206
+
207
+ def __enter__(self) -> "PrismAPIClient":
208
+ return self
209
+
210
+ def __exit__(self, *_: Any) -> None:
211
+ self.close()
212
+
213
+ def health_check(self) -> bool:
214
+ """
215
+ Ping the server's /health endpoint.
216
+
217
+ Returns True if the server responds 200 OK, False otherwise.
218
+ """
219
+ try:
220
+ conn = self._get_conn()
221
+ conn.request("GET", "/health")
222
+ resp = conn.getresponse()
223
+ resp.read()
224
+ return resp.status == 200
225
+ except Exception:
226
+ self._close_conn()
227
+ return False
228
+
229
+ # ------------------------------------------------------------------
230
+ # Main query interface
231
+ # ------------------------------------------------------------------
232
+
233
+ def query(
234
+ self,
235
+ query_text: str,
236
+ top_k: int = 10,
237
+ extra_context: Optional[dict[str, Any]] = None,
238
+ ) -> APIResponse:
239
+ """
240
+ Embed ``query_text``, send as a CHORUS API_REQUEST, return an
241
+ APIResponse with pre-projected float32 vectors.
242
+
243
+ The consumer embeds the query once. The provider returns already-
244
+ projected result vectors — no second embed() call is made here.
245
+
246
+ Parameters
247
+ ----------
248
+ query_text:
249
+ The agent's natural-language query.
250
+ top_k:
251
+ Number of results to request.
252
+ extra_context:
253
+ Additional query parameters forwarded to the provider.
254
+
255
+ Returns
256
+ -------
257
+ APIResponse — results are (SemanticItem, ExactSidecar) pairs.
258
+ Use response.vectors for a stacked (N, dim) array ready for
259
+ PrismResonance retrieval.
260
+ """
261
+ from prism.observability.otel import trace_span
262
+
263
+ with trace_span(
264
+ "prism.api.client.query",
265
+ host=self._host,
266
+ port=self._port,
267
+ loopback=self._loopback is not None,
268
+ ):
269
+ t0 = time.perf_counter()
270
+
271
+ # Embed query (one call — the only embedding on the consumer side)
272
+ raw_emb = self._embedder.embed([query_text])[0] # (embed_dim,)
273
+ envelope = self._projector.project(raw_emb)
274
+ query_vec = envelope.vector # (target_dim,) float32
275
+
276
+ context: dict[str, Any] = {
277
+ "query_text": query_text,
278
+ "top_k": top_k,
279
+ **(extra_context or {}),
280
+ }
281
+ request = APIRequest(query_vector=query_vec, context=context)
282
+
283
+ # Exchange frames
284
+ resp_frame = self._exchange(request)
285
+
286
+ # Decode response
287
+ raw_results = resp_frame.decode_api_response()
288
+ result_pairs = unpack_response_payload(raw_results, source_field=self._source_field)
289
+
290
+ latency_ms = (time.perf_counter() - t0) * 1000.0
291
+ return APIResponse(
292
+ results=result_pairs,
293
+ provider_id="remote" if self._loopback is None else self._loopback._provider_id,
294
+ request_id=request.request_id,
295
+ latency_ms=latency_ms,
296
+ embedding_calls_saved=len(result_pairs),
297
+ )
298
+
299
+ def query_vector(
300
+ self,
301
+ query_vector: np.ndarray,
302
+ top_k: int = 10,
303
+ extra_context: Optional[dict[str, Any]] = None,
304
+ ) -> APIResponse:
305
+ """
306
+ Send a pre-computed query vector (already in target_dim space)
307
+ without any embedding step.
308
+
309
+ Use this when the agent already has a vector (e.g. from a previous
310
+ retrieval step) and wants to find similar content from the provider.
311
+ Zero embedding calls.
312
+ """
313
+ t0 = time.perf_counter()
314
+
315
+ context: dict[str, Any] = {"top_k": top_k, **(extra_context or {})}
316
+ request = APIRequest(query_vector=query_vector, context=context)
317
+ resp_frame = self._exchange(request)
318
+ raw_results = resp_frame.decode_api_response()
319
+ result_pairs = unpack_response_payload(raw_results, source_field=self._source_field)
320
+
321
+ latency_ms = (time.perf_counter() - t0) * 1000.0
322
+ return APIResponse(
323
+ results=result_pairs,
324
+ provider_id="remote" if self._loopback is None else self._loopback._provider_id,
325
+ request_id=request.request_id,
326
+ latency_ms=latency_ms,
327
+ embedding_calls_saved=len(result_pairs),
328
+ )
329
+
330
+ # ------------------------------------------------------------------
331
+ # Frame exchange
332
+ # ------------------------------------------------------------------
333
+
334
+ def _exchange(self, request: APIRequest) -> CHORUSFrame:
335
+ """
336
+ Serialise the request as a CHORUSFrame and return the response frame.
337
+
338
+ In loopback mode: in-process frame serialisation → provider → frame bytes.
339
+ In networked mode: HTTP POST with Content-Type: application/x-chorus-frame.
340
+ """
341
+ key = self._cipher._active_key # type: ignore[union-attr]
342
+
343
+ # Build request frame
344
+ req_frame = CHORUSFrame.from_api_request(
345
+ key_id=key.key_id,
346
+ seq=self._seq,
347
+ watermark=b"\x00" * 32, # consumer watermark — provider verifies its own
348
+ query_vector=request.query_vector,
349
+ context=request.context,
350
+ )
351
+ self._seq += 1
352
+
353
+ if self._loopback is not None:
354
+ return self._loopback_exchange(req_frame, request.context)
355
+ return self._http_exchange(req_frame)
356
+
357
+ def _loopback_exchange(
358
+ self, req_frame: CHORUSFrame, context: dict[str, Any]
359
+ ) -> CHORUSFrame:
360
+ """In-process exchange: serialise → provider.handle → deserialise."""
361
+ # Wire path: to_bytes() → from_bytes() exercises full serialisation
362
+ wire_bytes = req_frame.to_bytes()
363
+ decoded_req = CHORUSFrame.from_bytes(wire_bytes)
364
+ query_vec, ctx = decoded_req.decode_api_request()
365
+
366
+ query_text = ctx.get("query_text", "")
367
+ top_k = int(ctx.get("top_k", 10))
368
+
369
+ # Invoke the loopback provider directly
370
+ result_dicts = self._loopback._invoke_handler(query_text, query_vec, top_k)
371
+ resp_frame = self._loopback.as_chorus_frame(result_dicts)
372
+
373
+ # Round-trip through bytes to include wire overhead in timing
374
+ return CHORUSFrame.from_bytes(resp_frame.to_bytes())
375
+
376
+ def _http_exchange(self, req_frame: CHORUSFrame) -> CHORUSFrame:
377
+ """
378
+ Networked exchange over HTTP with retry and persistent connection.
379
+
380
+ Uses http.client.HTTPConnection with keep-alive for connection reuse.
381
+ Retries on transient errors (ConnectionError, timeout) with exponential
382
+ backoff. Re-establishes connection after any transport failure.
383
+ """
384
+ data = req_frame.to_bytes()
385
+ headers = {
386
+ "Content-Type": "application/x-chorus-frame",
387
+ "Content-Length": str(len(data)),
388
+ "Connection": "keep-alive",
389
+ }
390
+ if self._api_key:
391
+ headers["X-API-Key"] = self._api_key
392
+ if self._bearer_token:
393
+ headers["Authorization"] = f"Bearer {self._bearer_token}"
394
+
395
+ last_exc: Optional[Exception] = None
396
+ for attempt in range(self._retry.max_retries + 1):
397
+ if attempt > 0:
398
+ sleep_s = min(
399
+ self._retry.backoff_base * (2 ** (attempt - 1)),
400
+ self._retry.backoff_max,
401
+ )
402
+ logger.warning(
403
+ "PrismAPIClient: retry %d/%d after %.1fs (error: %s)",
404
+ attempt,
405
+ self._retry.max_retries,
406
+ sleep_s,
407
+ last_exc,
408
+ )
409
+ time.sleep(sleep_s)
410
+
411
+ try:
412
+ conn = self._get_conn()
413
+ conn.request("POST", self._chorus_path, body=data, headers=headers)
414
+ resp = conn.getresponse()
415
+ body = resp.read()
416
+
417
+ if resp.status != 200:
418
+ raise TransportError(
419
+ f"Server returned HTTP {resp.status}: {body[:200]}"
420
+ )
421
+
422
+ frame = CHORUSFrame.from_bytes(body)
423
+ if frame.frame_type != FrameType.API_RESPONSE:
424
+ raise FrameTypeError(
425
+ f"Expected API_RESPONSE, got {frame.frame_type.name}"
426
+ )
427
+ return frame
428
+
429
+ except (FrameTypeError, TransportError):
430
+ # Non-retryable protocol errors — surface immediately
431
+ raise
432
+ except Exception as exc:
433
+ last_exc = exc
434
+ # Connection-level error — drop and reconnect on next attempt
435
+ self._close_conn()
436
+
437
+ raise TransportError(
438
+ f"HTTP exchange failed after {self._retry.max_retries + 1} attempts: {last_exc}"
439
+ ) from last_exc
440
+
441
+
442
+ # ---------------------------------------------------------------------------
443
+ # LangGraphTool — thin adapter for LangGraph agent nodes
444
+ # ---------------------------------------------------------------------------
445
+
446
+
447
+ class LangGraphTool:
448
+ """
449
+ Exposes PrismAPIClient as a LangGraph-compatible tool node.
450
+
451
+ The agent calls the tool by name; the tool queries the PrismAPIProvider
452
+ and returns a dict with ``vectors`` (np.ndarray) and ``results``
453
+ (list of sidecar dicts) that the agent can use directly.
454
+
455
+ Usage in a LangGraph graph::
456
+
457
+ from prism.api.consumer import LangGraphTool
458
+
459
+ tool = LangGraphTool(
460
+ name="semantic_search",
461
+ description="Search the knowledge base by semantic meaning.",
462
+ client=my_prism_client,
463
+ )
464
+
465
+ # In a LangGraph node:
466
+ result = tool.invoke({"query": "how does inflation affect bonds?"})
467
+ # result["vectors"] — np.ndarray (N, 64), ready for PrismResonance
468
+ # result["sidecars"] — list of exact metadata dicts
469
+ # result["top_k"] — int, number of results
470
+
471
+ LangGraph integration (optional import)::
472
+
473
+ # If langgraph is installed, tool.as_langgraph_node() returns a node
474
+ # function compatible with StateGraph.add_node().
475
+ node = tool.as_langgraph_node()
476
+ """
477
+
478
+ def __init__(
479
+ self,
480
+ name: str,
481
+ description: str,
482
+ client: PrismAPIClient,
483
+ top_k: int = 10,
484
+ ) -> None:
485
+ self.name = name
486
+ self.description = description
487
+ self._client = client
488
+ self._top_k = top_k
489
+
490
+ def invoke(self, input_dict: dict[str, Any]) -> dict[str, Any]:
491
+ """
492
+ Synchronous tool invocation.
493
+
494
+ Parameters
495
+ ----------
496
+ input_dict:
497
+ Must contain "query" (str). Optional: "top_k" (int).
498
+
499
+ Returns
500
+ -------
501
+ dict with keys:
502
+ "vectors" — np.ndarray (N, dim), stacked result vectors
503
+ "sidecars" — list[dict], exact metadata for each result
504
+ "top_k" — int, actual number of results returned
505
+ "latency_ms" — float, end-to-end latency
506
+ """
507
+ query = str(input_dict.get("query", ""))
508
+ top_k = int(input_dict.get("top_k", self._top_k))
509
+ if not query:
510
+ return {"vectors": np.empty((0,), np.float32), "sidecars": [], "top_k": 0}
511
+
512
+ response = self._client.query(query, top_k=top_k)
513
+ return {
514
+ "vectors": response.vectors,
515
+ "sidecars": response.sidecars,
516
+ "top_k": len(response.results),
517
+ "latency_ms": response.latency_ms,
518
+ "embedding_calls_saved": response.embedding_calls_saved,
519
+ }
520
+
521
+ def as_langgraph_node(self) -> Callable:
522
+ """
523
+ Return a LangGraph node function.
524
+
525
+ Requires: pip install langgraph
526
+ The returned function signature is ``node(state: dict) -> dict``,
527
+ compatible with ``StateGraph.add_node(name, node)``.
528
+ """
529
+ try:
530
+ from langgraph.graph import StateGraph # type: ignore[import] # noqa: F401
531
+ except ImportError:
532
+ logger.warning(
533
+ "langgraph not installed — as_langgraph_node() returns a plain "
534
+ "callable usable as a node function, but graph registration "
535
+ "requires: pip install langgraph"
536
+ )
537
+
538
+ tool = self
539
+
540
+ def node(state: dict[str, Any]) -> dict[str, Any]:
541
+ query = state.get("query", state.get("input", ""))
542
+ result = tool.invoke({"query": query})
543
+ return {**state, "prismapi_result": result}
544
+
545
+ node.__name__ = self.name
546
+ return node
547
+
548
+ # ------------------------------------------------------------------
549
+ # Tool schema for MCP / OpenAI function-calling
550
+ # ------------------------------------------------------------------
551
+
552
+ @property
553
+ def tool_schema(self) -> dict[str, Any]:
554
+ """JSON schema describing this tool for MCP or function-calling."""
555
+ return {
556
+ "name": self.name,
557
+ "description": self.description,
558
+ "parameters": {
559
+ "type": "object",
560
+ "properties": {
561
+ "query": {
562
+ "type": "string",
563
+ "description": "Natural-language search query.",
564
+ },
565
+ "top_k": {
566
+ "type": "integer",
567
+ "description": "Number of results to return.",
568
+ "default": self._top_k,
569
+ },
570
+ },
571
+ "required": ["query"],
572
+ },
573
+ }
@@ -0,0 +1,30 @@
1
+ """
2
+ prism.api.integrations — Framework adapters for PrismAPI
3
+ =========================================================
4
+
5
+ Available integrations:
6
+
7
+ from prism.api.integrations.langgraph import (
8
+ PrismRetrieverNode,
9
+ MultiProviderRetrieverNode,
10
+ create_retriever_node,
11
+ )
12
+
13
+ from prism.api.integrations.langchain import PrismRetriever
14
+
15
+ Each integration is an optional import — the framework dependency is not
16
+ required at install time. Missing framework raises ImportError with a
17
+ clear install instruction.
18
+ """
19
+
20
+ from prism.api.integrations.langgraph import (
21
+ MultiProviderRetrieverNode,
22
+ PrismRetrieverNode,
23
+ create_retriever_node,
24
+ )
25
+
26
+ __all__ = [
27
+ "PrismRetrieverNode",
28
+ "MultiProviderRetrieverNode",
29
+ "create_retriever_node",
30
+ ]