memcode-sdk 2.3.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,104 @@
1
+ """
2
+ Memcode Python SDK — client library for the Memcode long-term memory API.
3
+
4
+ Quickstart::
5
+
6
+ from memcode_sdk import MemcodeClient
7
+
8
+ client = MemcodeClient(api_key="sk-...")
9
+ result = client.ingest(user_query="I love Python", user_id="u1")
10
+ answer = client.retrieve(query="hobbies", user_id="u1")
11
+ client.close()
12
+
13
+ Async::
14
+
15
+ from memcode_sdk import AsyncMemcodeClient
16
+
17
+ async with AsyncMemcodeClient(api_key="sk-...") as client:
18
+ result = await client.ingest(user_query="I love Python", user_id="u1")
19
+ """
20
+
21
+ from .client import MemcodeClient
22
+ from .async_client import AsyncMemcodeClient
23
+ from .v2_client import MemoryV2Client, MemcodeV2Client
24
+ from .async_v2_client import AsyncMemoryV2Client, AsyncMemcodeV2Client
25
+ from .types import (
26
+ DomainResult,
27
+ HealthStatus,
28
+ HybridSearchResult,
29
+ IngestResult,
30
+ OperationDetail,
31
+ PersonalV2IngestResult,
32
+ PersonalV2IngestStatus,
33
+ PersonalV2RetrieveResult,
34
+ RetrieveResult,
35
+ SearchResult,
36
+ SourceRecord,
37
+ WeaverSummary,
38
+ )
39
+ from .errors import (
40
+ MemcodeSDKError,
41
+ AuthenticationError,
42
+ ConnectionError,
43
+ NotReadyError,
44
+ RateLimitError,
45
+ ServerError,
46
+ ValidationError,
47
+ )
48
+ from .v2_types import (
49
+ V2IngestResult,
50
+ V2IngestStatus,
51
+ V2MemorySource,
52
+ V2OperationCounts,
53
+ V2OriginalStorageStatus,
54
+ V2RetrieveResult,
55
+ V2SearchResult,
56
+ V2SourceLineage,
57
+ V2SourceProvenance,
58
+ V2SourceSpace,
59
+ )
60
+
61
+ __author__ = "Memcode Team"
62
+ __version__ = "2.3.1"
63
+ __license__ = "Apache-2.0"
64
+
65
+ __all__ = [
66
+ # Clients
67
+ "MemcodeClient",
68
+ "AsyncMemcodeClient",
69
+ "MemcodeV2Client",
70
+ "MemoryV2Client",
71
+ "AsyncMemcodeV2Client",
72
+ "AsyncMemoryV2Client",
73
+ # Result types
74
+ "DomainResult",
75
+ "HealthStatus",
76
+ "HybridSearchResult",
77
+ "IngestResult",
78
+ "OperationDetail",
79
+ "PersonalV2IngestResult",
80
+ "PersonalV2IngestStatus",
81
+ "PersonalV2RetrieveResult",
82
+ "RetrieveResult",
83
+ "SearchResult",
84
+ "SourceRecord",
85
+ "WeaverSummary",
86
+ "V2IngestResult",
87
+ "V2IngestStatus",
88
+ "V2MemorySource",
89
+ "V2OperationCounts",
90
+ "V2OriginalStorageStatus",
91
+ "V2RetrieveResult",
92
+ "V2SearchResult",
93
+ "V2SourceLineage",
94
+ "V2SourceProvenance",
95
+ "V2SourceSpace",
96
+ # Errors
97
+ "MemcodeSDKError",
98
+ "AuthenticationError",
99
+ "ConnectionError",
100
+ "NotReadyError",
101
+ "RateLimitError",
102
+ "ServerError",
103
+ "ValidationError",
104
+ ]
memcode_sdk/_http.py ADDED
@@ -0,0 +1,162 @@
1
+ """
2
+ Internal HTTP transport layer for the Memcode SDK.
3
+
4
+ Handles request construction, response unpacking, and error mapping
5
+ for both sync (httpx.Client) and async (httpx.AsyncClient) transports.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ from typing import Any, Dict, Optional
12
+
13
+ import httpx
14
+
15
+ from .errors import (
16
+ AuthenticationError,
17
+ ConnectionError,
18
+ NotReadyError,
19
+ RateLimitError,
20
+ ServerError,
21
+ ValidationError,
22
+ MemcodeSDKError,
23
+ )
24
+ from .types import APIEnvelope, Status
25
+
26
+ logger = logging.getLogger("memcode.sdk.http")
27
+
28
+ DEFAULT_TIMEOUT = 120
29
+
30
+
31
+ def _build_headers(api_key: Optional[str]) -> Dict[str, str]:
32
+ headers: Dict[str, str] = {"Content-Type": "application/json"}
33
+ if api_key:
34
+ headers["Authorization"] = f"Bearer {api_key}"
35
+ return headers
36
+
37
+
38
+ def _map_error(status_code: int, body: Dict[str, Any]) -> MemcodeSDKError:
39
+ """Convert an API error response into a typed SDK exception."""
40
+ raw_message = body.get("error") or body.get("detail") or body.get("message")
41
+ if isinstance(raw_message, dict):
42
+ raw_message = raw_message.get("message") or raw_message.get("code")
43
+ if isinstance(raw_message, list):
44
+ raw_message = "; ".join(
45
+ str(item.get("msg", item)) if isinstance(item, dict) else str(item)
46
+ for item in raw_message
47
+ )
48
+ msg = str(raw_message or f"HTTP {status_code}")
49
+ request_id = body.get("request_id")
50
+
51
+ if status_code == 401:
52
+ return AuthenticationError(msg, status_code=status_code, request_id=request_id)
53
+ if status_code == 403:
54
+ return AuthenticationError(msg, status_code=status_code, request_id=request_id)
55
+ if status_code in {400, 422}:
56
+ return ValidationError(msg, status_code=status_code, request_id=request_id, details=body)
57
+ if status_code == 429:
58
+ return RateLimitError(
59
+ msg, status_code=status_code, request_id=request_id,
60
+ retry_after=int(body.get("retry_after", 60)),
61
+ )
62
+ if status_code == 503:
63
+ return NotReadyError(msg, status_code=status_code, request_id=request_id)
64
+ if status_code >= 500:
65
+ return ServerError(msg, status_code=status_code, request_id=request_id)
66
+
67
+ return MemcodeSDKError(msg, status_code=status_code, request_id=request_id)
68
+
69
+
70
+ def _unpack(response: httpx.Response) -> APIEnvelope:
71
+ """Parse and validate a raw HTTP response into an ``APIEnvelope``."""
72
+ try:
73
+ body = response.json()
74
+ except Exception:
75
+ if response.status_code >= 400:
76
+ raise _map_error(response.status_code, {"error": response.text[:500]})
77
+ raise MemcodeSDKError(f"Unparseable response (HTTP {response.status_code})")
78
+
79
+ if response.status_code >= 400:
80
+ raise _map_error(response.status_code, body)
81
+
82
+ status_raw = body.get("status", "ok")
83
+ status = Status.ERROR if status_raw == "error" else Status.OK
84
+
85
+ if status == Status.ERROR:
86
+ raise _map_error(response.status_code or 500, body)
87
+
88
+ return APIEnvelope(
89
+ status=status,
90
+ data=body.get("data"),
91
+ error=body.get("error"),
92
+ request_id=body.get("request_id"),
93
+ elapsed_ms=body.get("elapsed_ms"),
94
+ )
95
+
96
+
97
+ # ═══════════════════════════════════════════════════════════════════════════
98
+ # Sync transport
99
+ # ═══════════════════════════════════════════════════════════════════════════
100
+
101
+ class SyncTransport:
102
+ """Thin wrapper around ``httpx.Client``."""
103
+
104
+ def __init__(self, base_url: str, api_key: Optional[str], timeout: int) -> None:
105
+ self._client = httpx.Client(
106
+ base_url=base_url.rstrip("/"),
107
+ headers=_build_headers(api_key),
108
+ timeout=timeout,
109
+ )
110
+
111
+ def get(self, path: str, **kwargs) -> APIEnvelope:
112
+ try:
113
+ return _unpack(self._client.get(path, **kwargs))
114
+ except httpx.ConnectError as exc:
115
+ raise ConnectionError(f"Connection failed: {exc}") from exc
116
+ except httpx.TimeoutException as exc:
117
+ raise ConnectionError(f"Request timed out: {exc}") from exc
118
+
119
+ def post(self, path: str, *, json: Any = None, **kwargs) -> APIEnvelope:
120
+ try:
121
+ return _unpack(self._client.post(path, json=json, **kwargs))
122
+ except httpx.ConnectError as exc:
123
+ raise ConnectionError(f"Connection failed: {exc}") from exc
124
+ except httpx.TimeoutException as exc:
125
+ raise ConnectionError(f"Request timed out: {exc}") from exc
126
+
127
+ def close(self) -> None:
128
+ self._client.close()
129
+
130
+
131
+ # ═══════════════════════════════════════════════════════════════════════════
132
+ # Async transport
133
+ # ═══════════════════════════════════════════════════════════════════════════
134
+
135
+ class AsyncTransport:
136
+ """Thin wrapper around ``httpx.AsyncClient``."""
137
+
138
+ def __init__(self, base_url: str, api_key: Optional[str], timeout: int) -> None:
139
+ self._client = httpx.AsyncClient(
140
+ base_url=base_url.rstrip("/"),
141
+ headers=_build_headers(api_key),
142
+ timeout=timeout,
143
+ )
144
+
145
+ async def get(self, path: str, **kwargs) -> APIEnvelope:
146
+ try:
147
+ return _unpack(await self._client.get(path, **kwargs))
148
+ except httpx.ConnectError as exc:
149
+ raise ConnectionError(f"Connection failed: {exc}") from exc
150
+ except httpx.TimeoutException as exc:
151
+ raise ConnectionError(f"Request timed out: {exc}") from exc
152
+
153
+ async def post(self, path: str, *, json: Any = None, **kwargs) -> APIEnvelope:
154
+ try:
155
+ return _unpack(await self._client.post(path, json=json, **kwargs))
156
+ except httpx.ConnectError as exc:
157
+ raise ConnectionError(f"Connection failed: {exc}") from exc
158
+ except httpx.TimeoutException as exc:
159
+ raise ConnectionError(f"Request timed out: {exc}") from exc
160
+
161
+ async def close(self) -> None:
162
+ await self._client.aclose()
@@ -0,0 +1,277 @@
1
+ """
2
+ Asynchronous Memcode client.
3
+
4
+ Usage::
5
+
6
+ from memcode_sdk import AsyncMemcodeClient
7
+
8
+ async with AsyncMemcodeClient(api_url="http://localhost:8000") as client:
9
+ health = await client.ping()
10
+ result = await client.ingest(
11
+ user_query="I love hiking in the Rockies.",
12
+ user_id="user_42",
13
+ )
14
+ answer = await client.retrieve(query="hobbies", user_id="user_42")
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import os
20
+ from typing import Any, Dict, List, Optional
21
+
22
+ from ._http import DEFAULT_TIMEOUT, AsyncTransport
23
+ from .client import (
24
+ _append_legacy_user_id,
25
+ _optional_idempotency_headers,
26
+ _personal_v2_search_payload,
27
+ _parse_personal_v2_ingest,
28
+ _parse_personal_v2_retrieve,
29
+ _parse_personal_v2_status,
30
+ _parse_hybrid_search,
31
+ _parse_ingest,
32
+ _parse_retrieve,
33
+ _parse_search,
34
+ _personal_v2_ingest_payload,
35
+ _required_personal,
36
+ )
37
+ from .types import (
38
+ HealthStatus,
39
+ HybridSearchResult,
40
+ IngestResult,
41
+ PersonalV2IngestResult,
42
+ PersonalV2IngestStatus,
43
+ PersonalV2RetrieveResult,
44
+ RetrieveResult,
45
+ SearchResult,
46
+ )
47
+
48
+
49
+ class AsyncMemcodeClient:
50
+ """Asynchronous client for compatible v1 and advanced personal v2 APIs."""
51
+
52
+ def __init__(
53
+ self,
54
+ api_url: Optional[str] = None,
55
+ api_key: Optional[str] = None,
56
+ timeout: int = DEFAULT_TIMEOUT,
57
+ ) -> None:
58
+ self._api_url = (
59
+ api_url
60
+ or os.getenv("MEMCODE_API_URL")
61
+ or "http://localhost:8000"
62
+ )
63
+ self._api_key = api_key or os.getenv("MEMCODE_API_KEY") or ""
64
+ self._transport = AsyncTransport(self._api_url, self._api_key, timeout)
65
+
66
+ # ── Health ─────────────────────────────────────────────────────────
67
+
68
+ async def ping(self) -> HealthStatus:
69
+ try:
70
+ env = await self._transport.get("/health")
71
+ d = env.data or {}
72
+ except Exception:
73
+ return HealthStatus(status="unreachable", pipelines_ready=False)
74
+
75
+ return HealthStatus(
76
+ status=d.get("status", "unknown"),
77
+ pipelines_ready=d.get("pipelines_ready", False),
78
+ version=d.get("version", ""),
79
+ uptime_seconds=d.get("uptime_seconds"),
80
+ error=d.get("error"),
81
+ )
82
+
83
+ async def is_ready(self) -> bool:
84
+ return (await self.ping()).pipelines_ready
85
+
86
+ # ── Ingest ─────────────────────────────────────────────────────────
87
+
88
+ async def ingest(
89
+ self,
90
+ user_query: str,
91
+ user_id: str,
92
+ agent_response: str = "",
93
+ session_datetime: str = "",
94
+ image_url: str = "",
95
+ ) -> IngestResult:
96
+ """Ingest a conversation turn into long-term memory.
97
+
98
+ Args:
99
+ user_query: The user's message to memorize.
100
+ user_id: Unique user identifier.
101
+ agent_response: Assistant reply (improves summary extraction).
102
+ session_datetime: ISO-8601 datetime for temporal extraction.
103
+ image_url: URL or base64 data-URI of an attached image.
104
+ """
105
+ payload: Dict[str, Any] = {
106
+ "user_query": user_query,
107
+ "user_id": user_id,
108
+ }
109
+ if agent_response:
110
+ payload["agent_response"] = agent_response
111
+ if session_datetime:
112
+ payload["session_datetime"] = session_datetime
113
+ if image_url:
114
+ payload["image_url"] = image_url
115
+
116
+ env = await self._transport.post("/v1/memory/ingest", json=payload)
117
+ return _parse_ingest(env.data or {}, env.request_id, env.elapsed_ms)
118
+
119
+ async def ingest_v2(
120
+ self,
121
+ user_query: str,
122
+ user_id: Optional[str] = None,
123
+ agent_response: str = "",
124
+ session_datetime: str = "",
125
+ image_url: str = "",
126
+ effort_level: str = "low",
127
+ forget: bool = False,
128
+ idempotency_key: str = "",
129
+ ) -> PersonalV2IngestResult:
130
+ payload = _personal_v2_ingest_payload(
131
+ user_query,
132
+ user_id,
133
+ agent_response,
134
+ session_datetime,
135
+ image_url,
136
+ effort_level,
137
+ forget,
138
+ )
139
+ env = await self._transport.post(
140
+ "/v2/memory/ingest",
141
+ json=payload,
142
+ headers=_optional_idempotency_headers(idempotency_key),
143
+ )
144
+ return _parse_personal_v2_ingest(env.data or {}, env.request_id, env.elapsed_ms)
145
+
146
+ async def get_ingest_status_v2(self, job_id: str) -> PersonalV2IngestStatus:
147
+ from urllib.parse import quote
148
+
149
+ normalized = _required_personal("job_id", job_id)
150
+ env = await self._transport.get(
151
+ f"/v2/memory/ingest/{quote(normalized, safe='')}/status"
152
+ )
153
+ return _parse_personal_v2_status(env.data or {}, env.request_id, env.elapsed_ms)
154
+
155
+ # ── Retrieve ───────────────────────────────────────────────────────
156
+
157
+ async def retrieve(
158
+ self,
159
+ query: str,
160
+ user_id: str,
161
+ top_k: int = 5,
162
+ ) -> RetrieveResult:
163
+ """Answer a question using stored memories.
164
+
165
+ Args:
166
+ query: The question to answer.
167
+ user_id: User identifier.
168
+ top_k: Number of source records to consider.
169
+ """
170
+ env = await self._transport.post("/v1/memory/retrieve", json={
171
+ "query": query, "user_id": user_id, "top_k": top_k,
172
+ })
173
+ return _parse_retrieve(env.data or {}, env.request_id, env.elapsed_ms)
174
+
175
+ async def retrieve_v2(
176
+ self,
177
+ query: str,
178
+ user_id: Optional[str] = None,
179
+ top_k: int = 5,
180
+ ) -> PersonalV2RetrieveResult:
181
+ if isinstance(top_k, bool) or not isinstance(top_k, int) or not 1 <= top_k <= 50:
182
+ raise ValueError("MemcodeClient: top_k must be an integer between 1 and 50")
183
+ payload: Dict[str, Any] = {
184
+ "query": _required_personal("query", query),
185
+ "top_k": top_k,
186
+ }
187
+ _append_legacy_user_id(payload, user_id)
188
+ env = await self._transport.post("/v2/memory/retrieve", json=payload)
189
+ return _parse_personal_v2_retrieve(env.data or {}, env.request_id, env.elapsed_ms)
190
+
191
+ # ── Search ─────────────────────────────────────────────────────────
192
+
193
+ async def search(
194
+ self,
195
+ query: str,
196
+ user_id: str,
197
+ domains: Optional[List[str]] = None,
198
+ top_k: int = 10,
199
+ ) -> SearchResult:
200
+ """Raw semantic search across memory domains.
201
+
202
+ Args:
203
+ query: Natural-language search query.
204
+ user_id: User identifier.
205
+ domains: Subset of ``["profile", "temporal", "summary"]``.
206
+ top_k: Max results per domain.
207
+ """
208
+ payload: Dict[str, Any] = {
209
+ "query": query, "user_id": user_id, "top_k": top_k,
210
+ }
211
+ if domains is not None:
212
+ payload["domains"] = domains
213
+
214
+ env = await self._transport.post("/v1/memory/search", json=payload)
215
+ return _parse_search(env.data or {}, env.request_id, env.elapsed_ms)
216
+
217
+ async def hybrid_search(
218
+ self,
219
+ query: str,
220
+ user_id: Optional[str] = None,
221
+ domains: Optional[List[str]] = None,
222
+ memory_top_k: Optional[int] = None,
223
+ original_top_k: Optional[int] = None,
224
+ include_original_chunks: bool = True,
225
+ search_mode: str = "default",
226
+ top_k: Optional[int] = None,
227
+ minimum_score: float = 0.0,
228
+ ) -> HybridSearchResult:
229
+ """Deprecated alias for :meth:`search_v2`."""
230
+ return await self.search_v2(
231
+ query=query,
232
+ user_id=user_id,
233
+ domains=domains,
234
+ memory_top_k=memory_top_k,
235
+ original_top_k=original_top_k,
236
+ include_original_chunks=include_original_chunks,
237
+ search_mode=search_mode,
238
+ top_k=top_k,
239
+ minimum_score=minimum_score,
240
+ )
241
+
242
+ async def search_v2(
243
+ self,
244
+ query: str,
245
+ user_id: Optional[str] = None,
246
+ domains: Optional[List[str]] = None,
247
+ memory_top_k: Optional[int] = None,
248
+ original_top_k: Optional[int] = None,
249
+ include_original_chunks: bool = True,
250
+ search_mode: str = "default",
251
+ top_k: Optional[int] = None,
252
+ minimum_score: float = 0.0,
253
+ ) -> HybridSearchResult:
254
+ payload = _personal_v2_search_payload(
255
+ query=query,
256
+ user_id=user_id,
257
+ domains=domains,
258
+ memory_top_k=memory_top_k,
259
+ original_top_k=original_top_k,
260
+ include_original_chunks=include_original_chunks,
261
+ search_mode=search_mode,
262
+ top_k=top_k,
263
+ minimum_score=minimum_score,
264
+ )
265
+ env = await self._transport.post("/v2/memory/search", json=payload)
266
+ return _parse_hybrid_search(env.data or {}, env.request_id, env.elapsed_ms)
267
+
268
+ # ── Lifecycle ──────────────────────────────────────────────────────
269
+
270
+ async def close(self) -> None:
271
+ await self._transport.close()
272
+
273
+ async def __aenter__(self) -> "AsyncMemcodeClient":
274
+ return self
275
+
276
+ async def __aexit__(self, *exc) -> None:
277
+ await self.close()
@@ -0,0 +1,147 @@
1
+ """Asynchronous client for tenant-bound Memcode v2 memory operations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from typing import Any, Dict, List, Optional
7
+ from urllib.parse import quote
8
+
9
+ from ._http import DEFAULT_TIMEOUT, AsyncTransport
10
+ from .v2_client import (
11
+ _parse_ingest,
12
+ _parse_ingest_status,
13
+ _parse_retrieve,
14
+ _parse_search,
15
+ _required,
16
+ _scope,
17
+ _search_mode,
18
+ _top_k,
19
+ )
20
+ from .v2_types import V2IngestResult, V2IngestStatus, V2RetrieveResult, V2SearchResult
21
+
22
+
23
+ class AsyncMemcodeV2Client:
24
+ """Asynchronous tenant-bound v2 client."""
25
+
26
+ def __init__(
27
+ self,
28
+ api_url: Optional[str] = None,
29
+ api_key: Optional[str] = None,
30
+ timeout: int = DEFAULT_TIMEOUT,
31
+ ) -> None:
32
+ resolved_url = api_url or os.getenv("MEMCODE_API_URL") or "http://localhost:8000"
33
+ resolved_key = _required("api_key", api_key or os.getenv("MEMCODE_API_KEY") or "")
34
+ self._transport = AsyncTransport(resolved_url, resolved_key, timeout)
35
+
36
+ async def ingest(
37
+ self,
38
+ *,
39
+ space_id: str,
40
+ content: str,
41
+ idempotency_key: str,
42
+ actor_id: Optional[str] = None,
43
+ title: Optional[str] = None,
44
+ occurred_at: Optional[str] = None,
45
+ metadata: Optional[Dict[str, Any]] = None,
46
+ tags: Optional[List[str]] = None,
47
+ ) -> V2IngestResult:
48
+ key = _required("idempotency_key", idempotency_key)
49
+ if len(key) > 256:
50
+ raise ValueError("MemcodeV2Client: idempotency_key cannot exceed 256 characters")
51
+ payload: Dict[str, Any] = {
52
+ "space_id": _required("space_id", space_id),
53
+ "content": _required("content", content),
54
+ }
55
+ for name, value in (("actor_id", actor_id), ("title", title), ("occurred_at", occurred_at)):
56
+ if value is not None:
57
+ payload[name] = _required(name, value)
58
+ if metadata is not None:
59
+ payload["metadata"] = metadata
60
+ if tags is not None:
61
+ payload["tags"] = list(dict.fromkeys(_required("tag", tag) for tag in tags))
62
+ env = await self._transport.post(
63
+ "/v2/memory/ingest",
64
+ json=payload,
65
+ headers={"Idempotency-Key": key},
66
+ )
67
+ return _parse_ingest(env.data or {}, env.request_id, env.elapsed_ms)
68
+
69
+ async def get_ingest_status(self, job_id: str) -> V2IngestStatus:
70
+ encoded = quote(_required("job_id", job_id), safe="")
71
+ env = await self._transport.get(f"/v2/memory/ingest/{encoded}")
72
+ return _parse_ingest_status(env.data or {}, env.request_id, env.elapsed_ms)
73
+
74
+ async def search(
75
+ self,
76
+ *,
77
+ context_space_id: str,
78
+ query: str,
79
+ actor_id: Optional[str] = None,
80
+ scope: str = "inherited",
81
+ search_mode: str = "default",
82
+ top_k: int = 10,
83
+ minimum_score: float = 0.0,
84
+ domains: Optional[List[str]] = None,
85
+ include_original_chunks: Optional[bool] = None,
86
+ original_top_k: Optional[int] = None,
87
+ ) -> V2SearchResult:
88
+ if isinstance(minimum_score, bool) or not 0 <= minimum_score <= 1:
89
+ raise ValueError("MemcodeV2Client: minimum_score must be between 0 and 1")
90
+ payload: Dict[str, Any] = {
91
+ "context_space_id": _required("context_space_id", context_space_id),
92
+ "query": _required("query", query),
93
+ "scope": _scope(scope),
94
+ "search_mode": _search_mode(search_mode),
95
+ "top_k": _top_k(top_k),
96
+ "minimum_score": minimum_score,
97
+ }
98
+ if actor_id is not None:
99
+ payload["actor_id"] = _required("actor_id", actor_id)
100
+ if domains is not None:
101
+ if not domains:
102
+ raise ValueError("MemcodeV2Client: domains cannot be empty")
103
+ payload["domains"] = list(
104
+ dict.fromkeys(_required("domain", domain) for domain in domains)
105
+ )
106
+ if include_original_chunks is not None:
107
+ if not isinstance(include_original_chunks, bool):
108
+ raise ValueError(
109
+ "MemcodeV2Client: include_original_chunks must be a boolean"
110
+ )
111
+ payload["include_original_chunks"] = include_original_chunks
112
+ if original_top_k is not None:
113
+ payload["original_top_k"] = _top_k(original_top_k)
114
+ env = await self._transport.post("/v2/memory/search", json=payload)
115
+ return _parse_search(env.data or {}, env.request_id, env.elapsed_ms)
116
+
117
+ async def retrieve(
118
+ self,
119
+ *,
120
+ context_space_id: str,
121
+ query: str,
122
+ actor_id: Optional[str] = None,
123
+ scope: str = "inherited",
124
+ top_k: int = 5,
125
+ ) -> V2RetrieveResult:
126
+ payload: Dict[str, Any] = {
127
+ "context_space_id": _required("context_space_id", context_space_id),
128
+ "query": _required("query", query),
129
+ "scope": _scope(scope),
130
+ "top_k": _top_k(top_k),
131
+ }
132
+ if actor_id is not None:
133
+ payload["actor_id"] = _required("actor_id", actor_id)
134
+ env = await self._transport.post("/v2/memory/retrieve", json=payload)
135
+ return _parse_retrieve(env.data or {}, env.request_id, env.elapsed_ms)
136
+
137
+ async def close(self) -> None:
138
+ await self._transport.close()
139
+
140
+ async def __aenter__(self) -> "AsyncMemcodeV2Client":
141
+ return self
142
+
143
+ async def __aexit__(self, *exc: Any) -> None:
144
+ await self.close()
145
+
146
+
147
+ AsyncMemoryV2Client = AsyncMemcodeV2Client