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.
memcode_sdk/types.py ADDED
@@ -0,0 +1,172 @@
1
+ """
2
+ Public data types returned by the Memcode SDK.
3
+
4
+ These are plain dataclasses / Pydantic models that mirror the API's
5
+ JSON responses but are fully typed for IDE auto-complete and safety.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass, field
11
+ from enum import Enum
12
+ from typing import Any, Dict, List, Optional
13
+
14
+
15
+ # ── Shared envelope ────────────────────────────────────────────────────────
16
+
17
+ class Status(str, Enum):
18
+ OK = "ok"
19
+ ERROR = "error"
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class APIEnvelope:
24
+ """Raw API envelope — internal use only."""
25
+ status: Status
26
+ data: Optional[Dict[str, Any]] = None
27
+ error: Optional[str] = None
28
+ request_id: Optional[str] = None
29
+ elapsed_ms: Optional[float] = None
30
+
31
+
32
+ # ── Health ─────────────────────────────────────────────────────────────────
33
+
34
+ @dataclass(frozen=True)
35
+ class HealthStatus:
36
+ status: str
37
+ pipelines_ready: bool
38
+ version: str = ""
39
+ uptime_seconds: Optional[float] = None
40
+ error: Optional[str] = None
41
+
42
+
43
+ # ── Ingest ─────────────────────────────────────────────────────────────────
44
+
45
+ @dataclass(frozen=True)
46
+ class OperationDetail:
47
+ type: str
48
+ content: str
49
+ reason: str
50
+
51
+
52
+ @dataclass(frozen=True)
53
+ class WeaverSummary:
54
+ succeeded: int = 0
55
+ skipped: int = 0
56
+ failed: int = 0
57
+
58
+
59
+ @dataclass(frozen=True)
60
+ class DomainResult:
61
+ confidence: float = 0.0
62
+ operations: List[OperationDetail] = field(default_factory=list)
63
+ weaver: Optional[WeaverSummary] = None
64
+
65
+
66
+ @dataclass(frozen=True)
67
+ class IngestResult:
68
+ """Returned by ``client.ingest()``."""
69
+ model: str = ""
70
+ classification: List[Any] = field(default_factory=list)
71
+ profile: Optional[DomainResult] = None
72
+ temporal: Optional[DomainResult] = None
73
+ summary: Optional[DomainResult] = None
74
+ image: Optional[DomainResult] = None
75
+ request_id: Optional[str] = None
76
+ elapsed_ms: Optional[float] = None
77
+
78
+
79
+ @dataclass(frozen=True)
80
+ class PersonalV2IngestResult:
81
+ """Durable receipt returned by personal ``/v2/memory/ingest``."""
82
+ job_id: str
83
+ status: str
84
+ created: bool
85
+ status_url: str
86
+ plan_id: Optional[str] = None
87
+ queued_for_batch: Optional[bool] = None
88
+ estimated_available_in_seconds: Optional[float] = None
89
+ request_id: Optional[str] = None
90
+ elapsed_ms: Optional[float] = None
91
+
92
+
93
+ @dataclass(frozen=True)
94
+ class PersonalV2IngestStatus:
95
+ job_id: str
96
+ status: str
97
+ job_type: Optional[str] = None
98
+ retry_count: int = 0
99
+ attempt_count: int = 0
100
+ max_attempts: int = 0
101
+ timeout_seconds: Optional[float] = None
102
+ workflow_id: Optional[str] = None
103
+ run_id: Optional[str] = None
104
+ progress: Optional[Dict[str, Any]] = None
105
+ error: Any = None
106
+ error_state: Optional[Dict[str, Any]] = None
107
+ result: Optional[Dict[str, Any]] = None
108
+ created_at: Optional[str] = None
109
+ updated_at: Optional[str] = None
110
+ started_at: Optional[str] = None
111
+ completed_at: Optional[str] = None
112
+ dead_lettered_at: Optional[str] = None
113
+ cancelled_at: Optional[str] = None
114
+ request_id: Optional[str] = None
115
+ elapsed_ms: Optional[float] = None
116
+
117
+
118
+ # ── Retrieve ───────────────────────────────────────────────────────────────
119
+
120
+ @dataclass(frozen=True)
121
+ class SourceRecord:
122
+ domain: str
123
+ content: str
124
+ score: float = 0.0
125
+ metadata: Dict[str, Any] = field(default_factory=dict)
126
+
127
+
128
+ @dataclass(frozen=True)
129
+ class RetrieveResult:
130
+ """Returned by ``client.retrieve()``."""
131
+ model: str = ""
132
+ answer: str = ""
133
+ sources: List[SourceRecord] = field(default_factory=list)
134
+ confidence: float = 0.0
135
+ request_id: Optional[str] = None
136
+ elapsed_ms: Optional[float] = None
137
+
138
+
139
+ @dataclass(frozen=True)
140
+ class PersonalV2RetrieveResult(RetrieveResult):
141
+ used_source_indices: List[int] = field(default_factory=list)
142
+ connection_learning_queued: bool = False
143
+ used_refs: List[str] = field(default_factory=list)
144
+ billing: Optional[Dict[str, Any]] = None
145
+
146
+
147
+ # ── Search ─────────────────────────────────────────────────────────────────
148
+
149
+ @dataclass(frozen=True)
150
+ class SearchResult:
151
+ """Returned by ``client.search()``."""
152
+ results: List[SourceRecord] = field(default_factory=list)
153
+ total: int = 0
154
+ request_id: Optional[str] = None
155
+ elapsed_ms: Optional[float] = None
156
+
157
+
158
+ @dataclass(frozen=True)
159
+ class HybridSearchResult:
160
+ """Returned by unified personal v2 search and its deprecated hybrid alias."""
161
+ memory_results: List[SourceRecord] = field(default_factory=list)
162
+ original_chunks: List[SourceRecord] = field(default_factory=list)
163
+ results: List[SourceRecord] = field(default_factory=list)
164
+ total: int = 0
165
+ original_storage_enabled: bool = False
166
+ billing: Optional[Dict[str, Any]] = None
167
+ request_id: Optional[str] = None
168
+ elapsed_ms: Optional[float] = None
169
+ # Keep additive fields last so existing positional construction remains
170
+ # source-compatible.
171
+ failed_domains: List[str] = field(default_factory=list)
172
+ partial: bool = False
@@ -0,0 +1,214 @@
1
+ """Synchronous 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, SyncTransport
10
+ from .v2_types import (
11
+ V2IngestResult,
12
+ V2IngestStatus,
13
+ V2MemorySource,
14
+ V2OperationCounts,
15
+ V2OriginalStorageStatus,
16
+ V2RetrieveResult,
17
+ V2SearchResult,
18
+ V2SourceLineage,
19
+ V2SourceProvenance,
20
+ V2SourceSpace,
21
+ )
22
+
23
+ _READ_SCOPES = {"inherited", "context_only"}
24
+ _SEARCH_MODES = {"default", "global"}
25
+
26
+
27
+ def _required(name: str, value: str) -> str:
28
+ normalized = str(value or "").strip()
29
+ if not normalized:
30
+ raise ValueError(f"MemcodeV2Client: {name} is required")
31
+ return normalized
32
+
33
+
34
+ def _top_k(value: int) -> int:
35
+ if isinstance(value, bool) or not isinstance(value, int) or not 1 <= value <= 100:
36
+ raise ValueError("MemcodeV2Client: top_k must be an integer between 1 and 100")
37
+ return value
38
+
39
+
40
+ def _scope(value: str) -> str:
41
+ if value not in _READ_SCOPES:
42
+ raise ValueError("MemcodeV2Client: scope must be inherited or context_only")
43
+ return value
44
+
45
+
46
+ def _search_mode(value: str) -> str:
47
+ if value not in _SEARCH_MODES:
48
+ raise ValueError("MemcodeV2Client: search_mode must be default or global")
49
+ return value
50
+
51
+
52
+ def _metadata(data: Dict[str, Any], request_id: Optional[str], elapsed_ms: Optional[float]) -> Dict[str, Any]:
53
+ return {**data, "request_id": request_id, "elapsed_ms": elapsed_ms}
54
+
55
+
56
+ def _parse_ingest(data: Dict[str, Any], request_id: Optional[str], elapsed_ms: Optional[float]) -> V2IngestResult:
57
+ return V2IngestResult(**_metadata(data, request_id, elapsed_ms))
58
+
59
+
60
+ def _parse_ingest_status(data: Dict[str, Any], request_id: Optional[str], elapsed_ms: Optional[float]) -> V2IngestStatus:
61
+ parsed = dict(data)
62
+ if parsed.get("operation_counts"):
63
+ parsed["operation_counts"] = V2OperationCounts(**parsed["operation_counts"])
64
+ if parsed.get("source"):
65
+ parsed["source"] = V2SourceLineage(**parsed["source"])
66
+ if parsed.get("original_storage"):
67
+ parsed["original_storage"] = V2OriginalStorageStatus(**parsed["original_storage"])
68
+ return V2IngestStatus(**_metadata(parsed, request_id, elapsed_ms))
69
+
70
+
71
+ def _parse_source(data: Dict[str, Any]) -> V2MemorySource:
72
+ parsed = dict(data)
73
+ parsed["space"] = V2SourceSpace(**parsed["space"])
74
+ parsed["provenance"] = V2SourceProvenance(**parsed["provenance"])
75
+ return V2MemorySource(**parsed)
76
+
77
+
78
+ def _parse_search(data: Dict[str, Any], request_id: Optional[str], elapsed_ms: Optional[float]) -> V2SearchResult:
79
+ parsed = dict(data)
80
+ parsed["results"] = [_parse_source(item) for item in parsed.get("results", [])]
81
+ return V2SearchResult(**_metadata(parsed, request_id, elapsed_ms))
82
+
83
+
84
+ def _parse_retrieve(data: Dict[str, Any], request_id: Optional[str], elapsed_ms: Optional[float]) -> V2RetrieveResult:
85
+ parsed = dict(data)
86
+ parsed["sources"] = [_parse_source(item) for item in parsed.get("sources", [])]
87
+ return V2RetrieveResult(**_metadata(parsed, request_id, elapsed_ms))
88
+
89
+
90
+ class MemcodeV2Client:
91
+ """Tenant-bound v2 client. Credentials determine the organization."""
92
+
93
+ def __init__(
94
+ self,
95
+ api_url: Optional[str] = None,
96
+ api_key: Optional[str] = None,
97
+ timeout: int = DEFAULT_TIMEOUT,
98
+ ) -> None:
99
+ resolved_url = api_url or os.getenv("MEMCODE_API_URL") or "http://localhost:8000"
100
+ resolved_key = _required("api_key", api_key or os.getenv("MEMCODE_API_KEY") or "")
101
+ self._transport = SyncTransport(resolved_url, resolved_key, timeout)
102
+
103
+ def ingest(
104
+ self,
105
+ *,
106
+ space_id: str,
107
+ content: str,
108
+ idempotency_key: str,
109
+ actor_id: Optional[str] = None,
110
+ title: Optional[str] = None,
111
+ occurred_at: Optional[str] = None,
112
+ metadata: Optional[Dict[str, Any]] = None,
113
+ tags: Optional[List[str]] = None,
114
+ ) -> V2IngestResult:
115
+ key = _required("idempotency_key", idempotency_key)
116
+ if len(key) > 256:
117
+ raise ValueError("MemcodeV2Client: idempotency_key cannot exceed 256 characters")
118
+ payload: Dict[str, Any] = {
119
+ "space_id": _required("space_id", space_id),
120
+ "content": _required("content", content),
121
+ }
122
+ for name, value in (("actor_id", actor_id), ("title", title), ("occurred_at", occurred_at)):
123
+ if value is not None:
124
+ payload[name] = _required(name, value)
125
+ if metadata is not None:
126
+ payload["metadata"] = metadata
127
+ if tags is not None:
128
+ payload["tags"] = list(dict.fromkeys(_required("tag", tag) for tag in tags))
129
+ env = self._transport.post(
130
+ "/v2/memory/ingest",
131
+ json=payload,
132
+ headers={"Idempotency-Key": key},
133
+ )
134
+ return _parse_ingest(env.data or {}, env.request_id, env.elapsed_ms)
135
+
136
+ def get_ingest_status(self, job_id: str) -> V2IngestStatus:
137
+ encoded = quote(_required("job_id", job_id), safe="")
138
+ env = self._transport.get(f"/v2/memory/ingest/{encoded}")
139
+ return _parse_ingest_status(env.data or {}, env.request_id, env.elapsed_ms)
140
+
141
+ def search(
142
+ self,
143
+ *,
144
+ context_space_id: str,
145
+ query: str,
146
+ actor_id: Optional[str] = None,
147
+ scope: str = "inherited",
148
+ search_mode: str = "default",
149
+ top_k: int = 10,
150
+ minimum_score: float = 0.0,
151
+ domains: Optional[List[str]] = None,
152
+ include_original_chunks: Optional[bool] = None,
153
+ original_top_k: Optional[int] = None,
154
+ ) -> V2SearchResult:
155
+ if isinstance(minimum_score, bool) or not 0 <= minimum_score <= 1:
156
+ raise ValueError("MemcodeV2Client: minimum_score must be between 0 and 1")
157
+ payload: Dict[str, Any] = {
158
+ "context_space_id": _required("context_space_id", context_space_id),
159
+ "query": _required("query", query),
160
+ "scope": _scope(scope),
161
+ "search_mode": _search_mode(search_mode),
162
+ "top_k": _top_k(top_k),
163
+ "minimum_score": minimum_score,
164
+ }
165
+ if actor_id is not None:
166
+ payload["actor_id"] = _required("actor_id", actor_id)
167
+ if domains is not None:
168
+ if not domains:
169
+ raise ValueError("MemcodeV2Client: domains cannot be empty")
170
+ payload["domains"] = list(
171
+ dict.fromkeys(_required("domain", domain) for domain in domains)
172
+ )
173
+ if include_original_chunks is not None:
174
+ if not isinstance(include_original_chunks, bool):
175
+ raise ValueError(
176
+ "MemcodeV2Client: include_original_chunks must be a boolean"
177
+ )
178
+ payload["include_original_chunks"] = include_original_chunks
179
+ if original_top_k is not None:
180
+ payload["original_top_k"] = _top_k(original_top_k)
181
+ env = self._transport.post("/v2/memory/search", json=payload)
182
+ return _parse_search(env.data or {}, env.request_id, env.elapsed_ms)
183
+
184
+ def retrieve(
185
+ self,
186
+ *,
187
+ context_space_id: str,
188
+ query: str,
189
+ actor_id: Optional[str] = None,
190
+ scope: str = "inherited",
191
+ top_k: int = 5,
192
+ ) -> V2RetrieveResult:
193
+ payload: Dict[str, Any] = {
194
+ "context_space_id": _required("context_space_id", context_space_id),
195
+ "query": _required("query", query),
196
+ "scope": _scope(scope),
197
+ "top_k": _top_k(top_k),
198
+ }
199
+ if actor_id is not None:
200
+ payload["actor_id"] = _required("actor_id", actor_id)
201
+ env = self._transport.post("/v2/memory/retrieve", json=payload)
202
+ return _parse_retrieve(env.data or {}, env.request_id, env.elapsed_ms)
203
+
204
+ def close(self) -> None:
205
+ self._transport.close()
206
+
207
+ def __enter__(self) -> "MemcodeV2Client":
208
+ return self
209
+
210
+ def __exit__(self, *exc: Any) -> None:
211
+ self.close()
212
+
213
+
214
+ MemoryV2Client = MemcodeV2Client
@@ -0,0 +1,116 @@
1
+ """Typed results for the tenant-bound Memcode v2 memory routes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import Any, Dict, List, Optional
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class V2OperationCounts:
11
+ total: int = 0
12
+ succeeded: int = 0
13
+ skipped: int = 0
14
+ failed: int = 0
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class V2SourceLineage:
19
+ type: str
20
+ id: Optional[str] = None
21
+ record_id: Optional[str] = None
22
+ version: Optional[str] = None
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class V2OriginalStorageStatus:
27
+ status: str
28
+ required: bool
29
+ indexed_chunks: int = 0
30
+ expected_chunks: Optional[int] = None
31
+ cleanup_required: bool = False
32
+ failure_stage: Optional[str] = None
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class V2IngestResult:
37
+ id: str
38
+ space_id: str
39
+ status: str
40
+ status_url: Optional[str] = None
41
+ request_id: Optional[str] = None
42
+ elapsed_ms: Optional[float] = None
43
+
44
+
45
+ @dataclass(frozen=True)
46
+ class V2IngestStatus:
47
+ id: str
48
+ space_id: str
49
+ status: str
50
+ updated_at: Optional[str] = None
51
+ operation_counts: Optional[V2OperationCounts] = None
52
+ produced_memory_ids: List[str] = field(default_factory=list)
53
+ affected_memory_ids: List[str] = field(default_factory=list)
54
+ source: Optional[V2SourceLineage] = None
55
+ original_storage: Optional[V2OriginalStorageStatus] = None
56
+ retryable: bool = False
57
+ error: Optional[str] = None
58
+ status_url: Optional[str] = None
59
+ request_id: Optional[str] = None
60
+ elapsed_ms: Optional[float] = None
61
+
62
+
63
+ @dataclass(frozen=True)
64
+ class V2SourceSpace:
65
+ id: str
66
+ kind: str
67
+ visibility: str
68
+ name: str
69
+
70
+
71
+ @dataclass(frozen=True)
72
+ class V2SourceProvenance:
73
+ type: str
74
+ source_id: Optional[str] = None
75
+ source_url: Optional[str] = None
76
+ source_record_id: Optional[str] = None
77
+ source_version: Optional[str] = None
78
+ promoted_from_space_id: Optional[str] = None
79
+ promoted_from_memory_id: Optional[str] = None
80
+
81
+
82
+ @dataclass(frozen=True)
83
+ class V2MemorySource:
84
+ id: str
85
+ content: str
86
+ score: float
87
+ metadata: Dict[str, Any]
88
+ space: V2SourceSpace
89
+ provenance: V2SourceProvenance
90
+ title: Optional[str] = None
91
+ domain: Optional[str] = None
92
+
93
+
94
+ @dataclass(frozen=True)
95
+ class V2SearchResult:
96
+ results: List[V2MemorySource] = field(default_factory=list)
97
+ total: int = 0
98
+ searched_space_ids: List[str] = field(default_factory=list)
99
+ failed_space_ids: List[str] = field(default_factory=list)
100
+ partial: bool = False
101
+ routing_fallback: bool = False
102
+ request_id: Optional[str] = None
103
+ elapsed_ms: Optional[float] = None
104
+
105
+
106
+ @dataclass(frozen=True)
107
+ class V2RetrieveResult:
108
+ answer: str = ""
109
+ sources: List[V2MemorySource] = field(default_factory=list)
110
+ confidence: float = 0.0
111
+ searched_space_ids: List[str] = field(default_factory=list)
112
+ failed_space_ids: List[str] = field(default_factory=list)
113
+ partial: bool = False
114
+ routing_fallback: bool = False
115
+ request_id: Optional[str] = None
116
+ elapsed_ms: Optional[float] = None