RouteKitAI 0.1.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 (64) hide show
  1. routekitai/__init__.py +53 -0
  2. routekitai/cli/__init__.py +18 -0
  3. routekitai/cli/main.py +40 -0
  4. routekitai/cli/replay.py +80 -0
  5. routekitai/cli/run.py +95 -0
  6. routekitai/cli/serve.py +966 -0
  7. routekitai/cli/test_agent.py +178 -0
  8. routekitai/cli/trace.py +209 -0
  9. routekitai/cli/trace_analyze.py +120 -0
  10. routekitai/cli/trace_search.py +126 -0
  11. routekitai/core/__init__.py +58 -0
  12. routekitai/core/agent.py +325 -0
  13. routekitai/core/errors.py +49 -0
  14. routekitai/core/hooks.py +174 -0
  15. routekitai/core/memory.py +54 -0
  16. routekitai/core/message.py +132 -0
  17. routekitai/core/model.py +91 -0
  18. routekitai/core/policies.py +373 -0
  19. routekitai/core/policy.py +85 -0
  20. routekitai/core/policy_adapter.py +133 -0
  21. routekitai/core/runtime.py +1403 -0
  22. routekitai/core/tool.py +148 -0
  23. routekitai/core/tools.py +180 -0
  24. routekitai/evals/__init__.py +13 -0
  25. routekitai/evals/dataset.py +75 -0
  26. routekitai/evals/metrics.py +101 -0
  27. routekitai/evals/runner.py +184 -0
  28. routekitai/graphs/__init__.py +12 -0
  29. routekitai/graphs/executors.py +457 -0
  30. routekitai/graphs/graph.py +164 -0
  31. routekitai/memory/__init__.py +13 -0
  32. routekitai/memory/episodic.py +242 -0
  33. routekitai/memory/kv.py +34 -0
  34. routekitai/memory/retrieval.py +192 -0
  35. routekitai/memory/vector.py +700 -0
  36. routekitai/memory/working.py +66 -0
  37. routekitai/message.py +29 -0
  38. routekitai/model.py +48 -0
  39. routekitai/observability/__init__.py +21 -0
  40. routekitai/observability/analyzer.py +314 -0
  41. routekitai/observability/exporters/__init__.py +10 -0
  42. routekitai/observability/exporters/base.py +30 -0
  43. routekitai/observability/exporters/jsonl.py +81 -0
  44. routekitai/observability/exporters/otel.py +119 -0
  45. routekitai/observability/spans.py +111 -0
  46. routekitai/observability/streaming.py +117 -0
  47. routekitai/observability/trace.py +144 -0
  48. routekitai/providers/__init__.py +9 -0
  49. routekitai/providers/anthropic.py +227 -0
  50. routekitai/providers/azure_openai.py +243 -0
  51. routekitai/providers/local.py +196 -0
  52. routekitai/providers/openai.py +321 -0
  53. routekitai/py.typed +0 -0
  54. routekitai/sandbox/__init__.py +12 -0
  55. routekitai/sandbox/filesystem.py +131 -0
  56. routekitai/sandbox/network.py +142 -0
  57. routekitai/sandbox/permissions.py +70 -0
  58. routekitai/tool.py +33 -0
  59. routekitai-0.1.0.dist-info/METADATA +328 -0
  60. routekitai-0.1.0.dist-info/RECORD +64 -0
  61. routekitai-0.1.0.dist-info/WHEEL +5 -0
  62. routekitai-0.1.0.dist-info/entry_points.txt +2 -0
  63. routekitai-0.1.0.dist-info/licenses/LICENSE +21 -0
  64. routekitai-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,325 @@
1
+ """Agent primitive for RouteKit."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import uuid
7
+ from collections.abc import AsyncIterator
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from pydantic import BaseModel, Field
12
+
13
+ from routekitai.core.hooks import ToolFilter
14
+ from routekitai.core.memory import Memory
15
+ from routekitai.core.message import Message
16
+ from routekitai.core.model import Model
17
+ from routekitai.core.policy import Policy
18
+ from routekitai.core.policy_adapter import PolicyAdapter
19
+ from routekitai.core.runtime import Runtime
20
+ from routekitai.core.tool import Tool
21
+ from routekitai.graphs.graph import Graph
22
+ from routekitai.observability.trace import Trace, TraceEvent
23
+
24
+
25
+ class RunResult(BaseModel):
26
+ """Result of an agent run."""
27
+
28
+ output: Message = Field(..., description="Final output message")
29
+ trace_id: str = Field(..., description="Trace ID for this run")
30
+ final_state: dict[str, Any] = Field(default_factory=dict, description="Final agent state")
31
+ messages: list[Message] = Field(default_factory=list, description="All messages in the run")
32
+
33
+
34
+ class Agent(BaseModel):
35
+ """Agent with model, tools, policy, and memory.
36
+
37
+ Provides a clean API for creating and running agents.
38
+ """
39
+
40
+ model_config = {"arbitrary_types_allowed": True}
41
+
42
+ name: str = Field(..., description="Agent name")
43
+ model: Model = Field(..., description="Model used by the agent")
44
+ tools: list[Tool] = Field(default_factory=list, description="Tools available to the agent")
45
+ policy: Policy | dict[str, Any] | None = Field(
46
+ default=None, description="Agent policy or policy configuration"
47
+ )
48
+ memory: Memory | None = Field(default=None, description="Agent memory system")
49
+ tool_filter: ToolFilter | None = Field(
50
+ default=None, description="Agent-level tool allow/deny list"
51
+ )
52
+ trace_dir: Path | None = Field(default=None, description="Directory for trace files")
53
+
54
+ def __init__(self, **data: Any) -> None:
55
+ """Initialize agent with optional runtime."""
56
+ super().__init__(**data)
57
+ # Create internal runtime if not provided
58
+ if not hasattr(self, "_runtime"):
59
+ trace_dir = data.get("trace_dir")
60
+ if trace_dir is None:
61
+ trace_dir = Path(".routekit") / "traces"
62
+ self._runtime = Runtime(trace_dir=trace_dir)
63
+ self._runtime.register_agent(self)
64
+
65
+ async def run(self, prompt: str, **kwargs: Any) -> RunResult:
66
+ """Run the agent with a prompt.
67
+
68
+ Args:
69
+ prompt: User prompt
70
+ **kwargs: Additional run parameters
71
+
72
+ Returns:
73
+ RunResult with output, trace_id, final_state, and messages
74
+
75
+ Raises:
76
+ PolicyError: If agent policy fails
77
+ """
78
+ # Convert policy to PolicyAdapter if needed
79
+ policy_adapter = None
80
+ if self.policy:
81
+ if isinstance(self.policy, Policy):
82
+ policy_adapter = PolicyAdapter(self.policy)
83
+ elif isinstance(self.policy, dict):
84
+ # Legacy dict-based policy config - convert to appropriate policy
85
+ from routekitai.core.policies import ReActPolicy
86
+
87
+ policy_adapter = PolicyAdapter(ReActPolicy(**self.policy))
88
+
89
+ # Run via runtime
90
+ return await self._runtime.run(self.name, prompt, policy=policy_adapter, **kwargs)
91
+
92
+ async def run_stream(self, prompt: str, **kwargs: Any) -> AsyncIterator[dict[str, Any]]:
93
+ """Run the agent with streaming updates.
94
+
95
+ Yields trace events and progress updates in real-time.
96
+
97
+ Args:
98
+ prompt: User prompt
99
+ **kwargs: Additional run parameters
100
+
101
+ Yields:
102
+ Dicts with event information:
103
+ - type: Event type (trace_event, progress_update, result)
104
+ - data: Event data
105
+ - result: Final RunResult (only in last event)
106
+
107
+ Examples:
108
+ >>> async for event in agent.run_stream("Hello"):
109
+ ... if event["type"] == "trace_event":
110
+ ... print(f"Event: {event['data']['type']}")
111
+ ... elif event["type"] == "progress_update":
112
+ ... print(f"Progress: {event['data']['progress_percent']}%")
113
+ """
114
+ # Use queues to collect events and progress updates
115
+ trace_queue: asyncio.Queue[TraceEvent] = asyncio.Queue()
116
+ progress_queue: asyncio.Queue[dict[str, Any]] = asyncio.Queue()
117
+ result_queue: asyncio.Queue[RunResult] = asyncio.Queue()
118
+
119
+ def trace_callback(event: TraceEvent) -> None:
120
+ trace_queue.put_nowait(event)
121
+
122
+ def progress_callback(progress: dict[str, Any]) -> None:
123
+ progress_queue.put_nowait(progress)
124
+
125
+ # Convert policy to PolicyAdapter if needed
126
+ policy_adapter = None
127
+ if self.policy:
128
+ if isinstance(self.policy, Policy):
129
+ policy_adapter = PolicyAdapter(self.policy)
130
+ elif isinstance(self.policy, dict):
131
+ from routekitai.core.policies import ReActPolicy
132
+
133
+ policy_adapter = PolicyAdapter(ReActPolicy(**self.policy))
134
+
135
+ # Add callbacks to runtime
136
+ self._runtime.add_progress_callback(progress_callback)
137
+
138
+ # Create a trace to capture events
139
+ trace_id = str(uuid.uuid4())
140
+ trace = Trace(trace_id=trace_id, metadata={"agent": self.name, "prompt": prompt})
141
+ trace.add_event_callback(trace_callback)
142
+
143
+ # Run agent in background task
144
+ async def run_agent() -> None:
145
+ try:
146
+ result = await self._runtime.run(self.name, prompt, policy=policy_adapter, **kwargs)
147
+ result_queue.put_nowait(result)
148
+ except Exception as e:
149
+ # Put error in result queue
150
+ result_queue.put_nowait(None) # type: ignore[arg-type]
151
+ trace_queue.put_nowait(
152
+ TraceEvent(
153
+ type="error",
154
+ timestamp=asyncio.get_event_loop().time(),
155
+ data={"error": str(e), "error_type": type(e).__name__},
156
+ )
157
+ )
158
+
159
+ run_task = asyncio.create_task(run_agent())
160
+
161
+ try:
162
+ # Stream events as they arrive
163
+ while True:
164
+ # Check all queues with timeout
165
+ done, pending = await asyncio.wait(
166
+ [
167
+ asyncio.create_task(trace_queue.get()),
168
+ asyncio.create_task(progress_queue.get()),
169
+ asyncio.create_task(result_queue.get()),
170
+ run_task,
171
+ ],
172
+ return_when=asyncio.FIRST_COMPLETED,
173
+ timeout=0.1,
174
+ )
175
+
176
+ if not done:
177
+ continue
178
+
179
+ for task in done:
180
+ try:
181
+ result = await task
182
+ if task == run_task:
183
+ # Agent run completed
184
+ if result is not None:
185
+ # Type guard: result should be RunResult when task is run_task
186
+ from routekitai.core.agent import RunResult
187
+
188
+ assert isinstance(result, RunResult), (
189
+ "Expected RunResult from run_task"
190
+ )
191
+ yield {
192
+ "type": "result",
193
+ "result": {
194
+ "output": result.output.model_dump(mode="json"),
195
+ "trace_id": result.trace_id,
196
+ "final_state": result.final_state,
197
+ "messages": [
198
+ msg.model_dump(mode="json") for msg in result.messages
199
+ ],
200
+ },
201
+ }
202
+ return
203
+ elif isinstance(result, TraceEvent):
204
+ yield {
205
+ "type": "trace_event",
206
+ "data": {
207
+ "type": result.type,
208
+ "timestamp": result.timestamp,
209
+ "data": result.data,
210
+ },
211
+ }
212
+ elif isinstance(result, dict):
213
+ yield {"type": "progress_update", "data": result}
214
+ except asyncio.CancelledError:
215
+ return
216
+ except Exception:
217
+ pass
218
+
219
+ # Cancel pending tasks
220
+ for task in pending:
221
+ task.cancel()
222
+
223
+ finally:
224
+ # Clean up callbacks
225
+ self._runtime.remove_progress_callback(progress_callback)
226
+ if not run_task.done():
227
+ run_task.cancel()
228
+ try:
229
+ await run_task
230
+ except asyncio.CancelledError:
231
+ pass
232
+
233
+ def run_sync(self, prompt: str, **kwargs: Any) -> RunResult:
234
+ """Synchronous wrapper for agent.run().
235
+
236
+ Args:
237
+ prompt: User prompt
238
+ **kwargs: Additional run parameters
239
+
240
+ Returns:
241
+ RunResult with output, trace_id, final_state, and messages
242
+
243
+ Raises:
244
+ RuntimeError: If called from within an async context
245
+ """
246
+ try:
247
+ asyncio.get_running_loop()
248
+ # We're in an async context - need to use a different approach
249
+ # Try to use nest_asyncio if available, otherwise raise error
250
+ try:
251
+ import nest_asyncio
252
+
253
+ nest_asyncio.apply()
254
+ return asyncio.run(self.run(prompt, **kwargs))
255
+ except ImportError:
256
+ raise RuntimeError(
257
+ "Cannot use run_sync() in an async context. "
258
+ "Either use 'await agent.run()' or install nest_asyncio: pip install nest-asyncio"
259
+ ) from None
260
+ except RuntimeError:
261
+ # No event loop exists, safe to use asyncio.run
262
+ return asyncio.run(self.run(prompt, **kwargs))
263
+
264
+ @classmethod
265
+ def with_fake_model(
266
+ cls,
267
+ name: str,
268
+ responses: list[str | dict[str, Any]],
269
+ tools: list[Tool] | None = None,
270
+ ) -> Agent:
271
+ """Create agent with FakeModel for testing.
272
+
273
+ Args:
274
+ name: Agent name
275
+ responses: List of responses to queue in FakeModel
276
+ tools: Optional list of tools
277
+
278
+ Returns:
279
+ Agent instance with FakeModel
280
+
281
+ Examples:
282
+ >>> agent = Agent.with_fake_model("test", ["Hello!", "World!"])
283
+ >>> result = await agent.run("Hi")
284
+ """
285
+ from routekitai.providers.local import FakeModel
286
+
287
+ model = FakeModel(name=f"{name}_model")
288
+ for response in responses:
289
+ model.add_response(response)
290
+ return cls(name=name, model=model, tools=tools or [])
291
+
292
+ @classmethod
293
+ def with_graph_policy(
294
+ cls,
295
+ name: str,
296
+ graph: Graph,
297
+ model: Model,
298
+ runtime: Runtime | None = None,
299
+ **kwargs: Any,
300
+ ) -> Agent:
301
+ """Create agent with graph policy.
302
+
303
+ Args:
304
+ name: Agent name
305
+ graph: Graph to execute
306
+ model: Model instance
307
+ runtime: Optional runtime (will create one if not provided)
308
+ **kwargs: Additional agent parameters
309
+
310
+ Returns:
311
+ Agent instance with GraphPolicy
312
+
313
+ Examples:
314
+ >>> from routekitai.graphs import Graph, GraphNode, NodeType
315
+ >>> graph = Graph(name="test", entry_node="start", nodes=[...])
316
+ >>> agent = Agent.with_graph_policy("graph_agent", graph, model)
317
+ """
318
+
319
+ from routekitai.core.policies import GraphPolicy
320
+
321
+ if runtime is None:
322
+ runtime = Runtime(trace_dir=kwargs.get("trace_dir"))
323
+
324
+ policy = GraphPolicy(graph=graph, runtime=runtime)
325
+ return cls(name=name, model=model, policy=policy, **kwargs)
@@ -0,0 +1,49 @@
1
+ """Well-typed exceptions for RouteKit."""
2
+
3
+ from typing import Any
4
+
5
+
6
+ class RouteKitError(Exception):
7
+ """Base exception for all routkitai errors."""
8
+
9
+ def __init__(self, message: str, context: dict[str, Any] | None = None) -> None:
10
+ """Initialize error with message and optional context.
11
+
12
+ Args:
13
+ message: Error message
14
+ context: Optional context dictionary (trace_id, agent_name, step_id, etc.)
15
+ """
16
+ super().__init__(message)
17
+ self.message = message
18
+ self.context = context or {}
19
+
20
+ def __str__(self) -> str:
21
+ """Return error message with context if available."""
22
+ if self.context:
23
+ context_str = ", ".join(f"{k}={v}" for k, v in self.context.items())
24
+ return f"{self.message} (context: {context_str})"
25
+ return self.message
26
+
27
+
28
+ class ModelError(RouteKitError):
29
+ """Error raised when model operations fail."""
30
+
31
+ pass
32
+
33
+
34
+ class ToolError(RouteKitError):
35
+ """Error raised when tool execution fails."""
36
+
37
+ pass
38
+
39
+
40
+ class PolicyError(RouteKitError):
41
+ """Error raised when agent policy fails."""
42
+
43
+ pass
44
+
45
+
46
+ class RuntimeError(RouteKitError):
47
+ """Error raised when runtime operations fail."""
48
+
49
+ pass
@@ -0,0 +1,174 @@
1
+ """Policy hooks for routkitai runtime."""
2
+
3
+ import re
4
+ from collections.abc import Callable
5
+ from typing import Any
6
+
7
+ from pydantic import BaseModel, Field
8
+
9
+
10
+ class PIIRedactionHook(BaseModel):
11
+ """PII redaction hook for redacting sensitive information.
12
+
13
+ Uses regex patterns to identify and redact PII like emails and phone numbers.
14
+ """
15
+
16
+ redact_emails: bool = Field(default=True, description="Redact email addresses")
17
+ redact_phones: bool = Field(default=True, description="Redact phone numbers")
18
+ redact_patterns: list[tuple[str, str]] = Field(
19
+ default_factory=list, description="Custom (pattern, replacement) tuples"
20
+ )
21
+ replacement: str = Field(
22
+ default="[REDACTED]", description="Replacement string for redacted content"
23
+ )
24
+
25
+ # Compiled regex patterns
26
+ _email_pattern: re.Pattern[str] | None = None
27
+ _phone_pattern: re.Pattern[str] | None = None
28
+
29
+ def __init__(self, **kwargs: Any) -> None:
30
+ """Initialize PII redaction hook."""
31
+ super().__init__(**kwargs)
32
+ if self.redact_emails:
33
+ self._email_pattern = re.compile(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b")
34
+ if self.redact_phones:
35
+ # Matches various phone formats
36
+ self._phone_pattern = re.compile(
37
+ r"(\+?\d{1,3}[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}"
38
+ )
39
+
40
+ def redact(self, text: str) -> str:
41
+ """Redact PII from text.
42
+
43
+ Args:
44
+ text: Text to redact
45
+
46
+ Returns:
47
+ Text with PII redacted
48
+ """
49
+ result = text
50
+
51
+ if self._email_pattern:
52
+ result = self._email_pattern.sub(self.replacement, result)
53
+
54
+ if self._phone_pattern:
55
+ result = self._phone_pattern.sub(self.replacement, result)
56
+
57
+ # Apply custom patterns
58
+ for pattern, replacement in self.redact_patterns:
59
+ result = re.sub(pattern, replacement, result)
60
+
61
+ return result
62
+
63
+ def redact_dict(self, data: dict[str, Any]) -> dict[str, Any]:
64
+ """Recursively redact PII from a dictionary.
65
+
66
+ Args:
67
+ data: Dictionary to redact
68
+
69
+ Returns:
70
+ Dictionary with PII redacted
71
+ """
72
+ redacted: dict[str, Any] = {}
73
+ for key, value in data.items():
74
+ if isinstance(value, str):
75
+ redacted[key] = self.redact(value)
76
+ elif isinstance(value, dict):
77
+ redacted[key] = self.redact_dict(value)
78
+ elif isinstance(value, list):
79
+ redacted[key] = [
80
+ self.redact(item) if isinstance(item, str) else item for item in value
81
+ ]
82
+ else:
83
+ redacted[key] = value
84
+ return redacted
85
+
86
+
87
+ class ToolFilter(BaseModel):
88
+ """Tool allow/deny list filter."""
89
+
90
+ allowed_tools: list[str] | None = Field(
91
+ default=None, description="List of allowed tool names (None = allow all)"
92
+ )
93
+ denied_tools: list[str] = Field(default_factory=list, description="List of denied tool names")
94
+
95
+ def is_allowed(self, tool_name: str) -> bool:
96
+ """Check if a tool is allowed.
97
+
98
+ Args:
99
+ tool_name: Name of the tool
100
+
101
+ Returns:
102
+ True if tool is allowed, False otherwise
103
+ """
104
+ # Deny list takes precedence
105
+ if tool_name in self.denied_tools:
106
+ return False
107
+
108
+ # If allow list is set, tool must be in it
109
+ if self.allowed_tools is not None:
110
+ return tool_name in self.allowed_tools
111
+
112
+ # Default: allow if not in deny list
113
+ return True
114
+
115
+
116
+ class ApprovalGate(BaseModel):
117
+ """Approval gate for blocking tools until approved.
118
+
119
+ Uses a callback function to determine if a tool should be approved.
120
+ """
121
+
122
+ approval_callback: Callable[[str, dict[str, Any]], bool] | None = Field(
123
+ default=None, description="Callback(tool_name, tool_args) -> bool"
124
+ )
125
+ require_approval_for_permissions: list[str] = Field(
126
+ default_factory=list, description="Permissions that require approval"
127
+ )
128
+
129
+ def requires_approval(
130
+ self, tool_name: str, tool_args: dict[str, Any], tool_permissions: list[str]
131
+ ) -> bool:
132
+ """Check if a tool requires approval.
133
+
134
+ Args:
135
+ tool_name: Name of the tool
136
+ tool_args: Tool arguments
137
+ tool_permissions: Tool permissions
138
+
139
+ Returns:
140
+ True if approval is required
141
+ """
142
+ # Check if tool has permissions that require approval
143
+ if any(perm in self.require_approval_for_permissions for perm in tool_permissions):
144
+ return True
145
+
146
+ # Check callback if provided
147
+ if self.approval_callback:
148
+ return not self.approval_callback(tool_name, tool_args)
149
+
150
+ return False
151
+
152
+ def is_approved(self, tool_name: str, tool_args: dict[str, Any]) -> bool:
153
+ """Check if a tool is approved.
154
+
155
+ Args:
156
+ tool_name: Name of the tool
157
+ tool_args: Tool arguments
158
+
159
+ Returns:
160
+ True if approved, False otherwise
161
+ """
162
+ if self.approval_callback:
163
+ return self.approval_callback(tool_name, tool_args)
164
+ return True # Default: approved if no callback
165
+
166
+
167
+ class PolicyHooks(BaseModel):
168
+ """Collection of policy hooks for runtime."""
169
+
170
+ pii_redaction: PIIRedactionHook | None = Field(
171
+ default=None, description="PII redaction hook for messages and tool arguments"
172
+ )
173
+ tool_filter: ToolFilter | None = Field(default=None, description="Tool allow/deny list")
174
+ approval_gate: ApprovalGate | None = Field(default=None, description="Approval gate for tools")
@@ -0,0 +1,54 @@
1
+ """Memory interface for routkitai agents."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Any
5
+
6
+
7
+ class Memory(ABC):
8
+ """Base interface for agent memory systems."""
9
+
10
+ @abstractmethod
11
+ async def get(self, key: str) -> Any:
12
+ """Get value by key.
13
+
14
+ Args:
15
+ key: Key to retrieve
16
+
17
+ Returns:
18
+ Stored value or None if not found
19
+ """
20
+ raise NotImplementedError("Subclasses must implement get")
21
+
22
+ @abstractmethod
23
+ async def set(self, key: str, value: Any) -> None:
24
+ """Set value by key.
25
+
26
+ Args:
27
+ key: Key to set
28
+ value: Value to store
29
+ """
30
+ raise NotImplementedError("Subclasses must implement set")
31
+
32
+ @abstractmethod
33
+ async def append(self, event: dict[str, Any]) -> None:
34
+ """Append an event to memory.
35
+
36
+ Args:
37
+ event: Event dictionary to append
38
+ """
39
+ raise NotImplementedError("Subclasses must implement append")
40
+
41
+ async def search(self, query: str, k: int = 5) -> list[dict[str, Any]]:
42
+ """Search memory (optional for retrieval memory).
43
+
44
+ Args:
45
+ query: Search query
46
+ k: Number of results to return
47
+
48
+ Returns:
49
+ List of matching results
50
+
51
+ Raises:
52
+ NotImplementedError: If search is not supported
53
+ """
54
+ raise NotImplementedError("Search not supported by this memory type")