nullprotocol 0.2.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.
@@ -0,0 +1,292 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from collections.abc import Callable
5
+ from copy import deepcopy
6
+ from dataclasses import dataclass
7
+ from threading import Lock
8
+ from typing import Any, TypeVar
9
+ from uuid import UUID, uuid4
10
+ from weakref import WeakKeyDictionary, WeakValueDictionary
11
+
12
+ from jsonschema import Draft202012Validator, FormatChecker
13
+
14
+ from ._transport import identifier, json_body, request_id, scope_id, utf16_length
15
+ from ._types import Result
16
+
17
+ Knowledge = bool | dict[str, str]
18
+ ActionChoice = str | dict[str, str]
19
+
20
+
21
+ def object_field(body: dict[str, Any], field: str) -> dict[str, Any]:
22
+ from ._errors import ProtocolError
23
+
24
+ value = body.get(field)
25
+ if not isinstance(value, dict):
26
+ raise ProtocolError(f"Missing {field} in the server response.")
27
+ return value
28
+
29
+
30
+ def list_field(body: dict[str, Any], field: str) -> list[dict[str, Any]]:
31
+ from ._errors import ProtocolError
32
+
33
+ value = body.get(field)
34
+ if not isinstance(value, list) or not all(isinstance(row, dict) for row in value):
35
+ raise ProtocolError(f"Missing {field} in the server response.")
36
+ return value
37
+
38
+
39
+ def version(value: str) -> str:
40
+ try:
41
+ UUID(value)
42
+ except (ValueError, TypeError, AttributeError) as exc:
43
+ raise ValueError("Expected a UUID version or note ID.") from exc
44
+ return value
45
+
46
+
47
+ def checked_text(value: str, name: str, maximum: int) -> str:
48
+ if not isinstance(value, str) or not value.strip() or utf16_length(value) > maximum:
49
+ raise ValueError(
50
+ f"{name} must be a nonempty string of at most {maximum} UTF-16 characters."
51
+ )
52
+ if re.search(r"[\x00-\x1f\x7f-\x9f]", value):
53
+ raise ValueError(f"{name} must not contain control characters.")
54
+ return value
55
+
56
+
57
+ def operation_payload(
58
+ operation: str,
59
+ values: dict[str, Any],
60
+ scope: str | None,
61
+ knowledge: Knowledge | None,
62
+ context_keys: list[str] | None,
63
+ ) -> dict[str, Any]:
64
+ body = {"operation": operation, **values}
65
+ if scope is not None:
66
+ body["conversation"] = scope
67
+ if knowledge is not None:
68
+ if type(knowledge) is bool:
69
+ body["knowledge"] = knowledge
70
+ elif isinstance(knowledge, dict) and set(knowledge) == {"query"}:
71
+ query = knowledge["query"]
72
+ if (
73
+ not isinstance(query, str)
74
+ or not query.strip()
75
+ or utf16_length(query.strip()) > 2000
76
+ ):
77
+ raise ValueError(
78
+ "Knowledge query must have 1–2000 UTF-16 characters after trimming."
79
+ )
80
+ # The server returns knowledge_query_too_large if the byte limit is exceeded.
81
+ body["knowledge"] = {"query": query.strip()}
82
+ else:
83
+ raise ValueError("knowledge must be a bool or {'query': '...'}.")
84
+ if context_keys is not None:
85
+ if not isinstance(context_keys, list) or len(context_keys) > 16:
86
+ raise ValueError("context_keys must be a list of at most 16 keys.")
87
+ body["contextKeys"] = [identifier(key, "context key") for key in context_keys]
88
+ if len(json_body(body)) > 131072:
89
+ raise ValueError("The HTTP operation body exceeds 131072 UTF-8 bytes.")
90
+ return body
91
+
92
+
93
+ def summary_options(content: Any, max_length: int, focus: str | None) -> dict[str, Any]:
94
+ if type(max_length) is not int or not 1 <= max_length <= 10000:
95
+ raise ValueError("max_length must be an integer from 1 to 10000.")
96
+ body = {"content": content, "maxLength": max_length}
97
+ if focus is not None:
98
+ if not isinstance(focus, str) or utf16_length(focus) > 1000:
99
+ raise ValueError("focus must have at most 1000 UTF-16 characters.")
100
+ body["focus"] = focus
101
+ return body
102
+
103
+
104
+ def validation_options(
105
+ criteria: str, subject: Any, reference: Any, unset: object
106
+ ) -> dict[str, Any]:
107
+ if not isinstance(criteria, str) or not 1 <= utf16_length(criteria) <= 16384:
108
+ raise ValueError("criteria must have 1–16384 UTF-16 characters.")
109
+ body = {"criteria": criteria, "subject": subject}
110
+ if reference is not unset:
111
+ body["reference"] = reference
112
+ return body
113
+
114
+
115
+ def schema_validator(schema: dict[str, Any]) -> Draft202012Validator:
116
+ if not isinstance(schema, dict):
117
+ raise ValueError("input_schema must be a JSON Schema object.")
118
+ Draft202012Validator.check_schema(schema)
119
+ checker = FormatChecker()
120
+
121
+ def check(value: Any) -> None:
122
+ if not isinstance(value, dict):
123
+ return
124
+ for ref in ("$ref", "$dynamicRef", "$recursiveRef"):
125
+ if ref in value and not str(value[ref]).startswith("#"):
126
+ raise ValueError(
127
+ "Action schemas must use local references; remote refs are unsupported."
128
+ )
129
+ if "format" in value and value["format"] not in checker.checkers:
130
+ raise ValueError(f"Unsupported action schema format: {value['format']}")
131
+ # Descend through schema positions only. A property literally named
132
+ # format/$ref, an enum value or a default is ordinary application data.
133
+ for keyword in (
134
+ "properties",
135
+ "patternProperties",
136
+ "$defs",
137
+ "definitions",
138
+ "dependentSchemas",
139
+ ):
140
+ for child in value.get(keyword, {}).values():
141
+ check(child)
142
+ for keyword in ("allOf", "anyOf", "oneOf", "prefixItems"):
143
+ for child in value.get(keyword, []):
144
+ check(child)
145
+ for keyword in (
146
+ "items",
147
+ "additionalItems",
148
+ "additionalProperties",
149
+ "unevaluatedProperties",
150
+ "unevaluatedItems",
151
+ "contains",
152
+ "propertyNames",
153
+ "if",
154
+ "then",
155
+ "else",
156
+ "not",
157
+ "contentSchema",
158
+ ):
159
+ child = value.get(keyword)
160
+ if isinstance(child, list):
161
+ for item in child:
162
+ check(item)
163
+ else:
164
+ check(child)
165
+ for child in value.get("dependencies", {}).values():
166
+ if isinstance(child, dict):
167
+ check(child)
168
+
169
+ check(schema)
170
+ return Draft202012Validator(deepcopy(schema), format_checker=checker)
171
+
172
+
173
+ @dataclass
174
+ class RegisteredAction:
175
+ handler: Callable[..., Any]
176
+ description: str | None
177
+ schema: dict[str, Any] | None
178
+ validator: Draft202012Validator | None
179
+ guard: Callable[..., Any] | None
180
+
181
+
182
+ @dataclass
183
+ class MadeDecision:
184
+ snapshot: Result[Any]
185
+ input: Any
186
+ executed: bool = False
187
+
188
+
189
+ _AgentT = TypeVar("_AgentT", bound="AgentState")
190
+
191
+
192
+ class AgentState:
193
+ def __init__(self, agent_id: str, scope: str | None) -> None:
194
+ self.agent_id = identifier(agent_id, "agent ID")
195
+ self.scope = scope_id(scope)
196
+ self._actions: dict[str, RegisteredAction] = {}
197
+ self._decisions: WeakKeyDictionary[Result[Any], MadeDecision] = WeakKeyDictionary()
198
+ self._decision_ids: WeakValueDictionary[str, MadeDecision] = WeakValueDictionary()
199
+ self._lock = Lock()
200
+
201
+ @property
202
+ def _path(self) -> str:
203
+ return f"/agents/{self.agent_id}"
204
+
205
+ def register_action(
206
+ self: _AgentT,
207
+ name: str,
208
+ handler: Callable[..., Any],
209
+ *,
210
+ description: str | None = None,
211
+ input_schema: dict[str, Any] | None = None,
212
+ guard: Callable[..., Any] | None = None,
213
+ ) -> _AgentT:
214
+ checked_text(name, "Action name", 64)
215
+ if not callable(handler) or (guard is not None and not callable(guard)):
216
+ raise ValueError("handler and guard must be callable.")
217
+ if description is not None and (
218
+ not isinstance(description, str) or utf16_length(description) > 1000
219
+ ):
220
+ raise ValueError("Action description must have at most 1000 UTF-16 characters.")
221
+ validator = schema_validator(input_schema) if input_schema is not None else None
222
+ self._actions[name] = RegisteredAction(
223
+ handler,
224
+ description,
225
+ deepcopy(input_schema),
226
+ validator,
227
+ guard,
228
+ )
229
+ return self
230
+
231
+ def _choices(self, context: Any, actions: list[ActionChoice]) -> dict[str, Any]:
232
+ if not isinstance(actions, list) or not 1 <= len(actions) <= 32:
233
+ raise ValueError("actions must contain 1–32 action choices.")
234
+ choices, schemas = [], {}
235
+ for offered in actions:
236
+ if isinstance(offered, str):
237
+ name = offered
238
+ description = None
239
+ elif isinstance(offered, dict) and set(offered) <= {"action", "description"}:
240
+ name = offered.get("action", "")
241
+ description = offered.get("description")
242
+ else:
243
+ raise ValueError(
244
+ "Use action names or {'action': name, 'description': text} choices."
245
+ )
246
+ checked_text(name, "Action name", 64)
247
+ registered = self._actions.get(name)
248
+ if description is None and registered is not None:
249
+ description = registered.description
250
+ choice = {"action": name}
251
+ if description is not None:
252
+ if not isinstance(description, str) or utf16_length(description) > 1000:
253
+ raise ValueError("Action description must have at most 1000 UTF-16 characters.")
254
+ choice["description"] = description
255
+ choices.append(choice)
256
+ if registered is not None and registered.schema is not None:
257
+ schemas[name] = deepcopy(registered.schema)
258
+ # The HTTP endpoint accepts action descriptions, not schemas. Send schema
259
+ # contracts as structured task data and enforce them locally before execution.
260
+ prepared = {"task": context, "actionParameterSchemas": schemas} if schemas else context
261
+ return {"context": prepared, "actions": choices}
262
+
263
+ def _remember(self, result: Result[Any], original: Any) -> Result[Any]:
264
+ if result.success:
265
+ snapshot = Result.from_response(
266
+ {"result": result.raw, "operationId": result.operation_id},
267
+ result.request_id,
268
+ )
269
+ with self._lock:
270
+ key = result.operation_id or "request:" + result.request_id
271
+ made = self._decision_ids.get(key)
272
+ if made is None:
273
+ made = MadeDecision(snapshot, deepcopy(original))
274
+ self._decision_ids[key] = made
275
+ self._decisions[result] = made
276
+ return result
277
+
278
+ def _consume(self, result: Result[Any]) -> tuple[MadeDecision | None, str | None]:
279
+ if not isinstance(result, Result):
280
+ return None, "not_a_decision"
281
+ with self._lock:
282
+ made = self._decisions.get(result)
283
+ if made is None:
284
+ return None, "not_a_decision"
285
+ if made.executed:
286
+ return made, "already_executed"
287
+ made.executed = True
288
+ return made, None
289
+
290
+ @staticmethod
291
+ def _task_id(value: str | None) -> str:
292
+ return request_id(str(uuid4()) if value is None else value)
@@ -0,0 +1,43 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+
6
+ class NullProtocolError(Exception):
7
+ """Base SDK error. Failed model operations are returned as Result, not raised."""
8
+
9
+
10
+ class APIError(NullProtocolError):
11
+ """An HTTP failure. Keep request_id when retrying an uncertain run."""
12
+
13
+ def __init__(
14
+ self,
15
+ status_code: int,
16
+ code: str,
17
+ *,
18
+ request_id: str | None = None,
19
+ details: dict[str, Any] | None = None,
20
+ retry_after: str | None = None,
21
+ ) -> None:
22
+ self.status_code = status_code
23
+ self.code = code
24
+ self.request_id = request_id
25
+ self.details = details or {}
26
+ self.retry_after = retry_after
27
+ super().__init__(f"NullProtocol HTTP {status_code}: {code}")
28
+
29
+
30
+ class TransportError(NullProtocolError):
31
+ """The response was not received. A run may have completed: retain request_id."""
32
+
33
+ def __init__(self, message: str, *, request_id: str | None = None) -> None:
34
+ self.request_id = request_id
35
+ super().__init__(message)
36
+
37
+
38
+ class ProtocolError(NullProtocolError):
39
+ """The server response does not match the expected response contract."""
40
+
41
+ def __init__(self, message: str, *, request_id: str | None = None) -> None:
42
+ self.request_id = request_id
43
+ super().__init__(message)
@@ -0,0 +1,163 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from ._common import checked_text, list_field, object_field, version
6
+ from ._transport import AsyncTransport, Transport
7
+
8
+
9
+ def _fields(
10
+ name: str | None, text: str | None, *, update: bool = False, if_version: str | None = None
11
+ ) -> dict[str, Any]:
12
+ result: dict[str, Any] = {}
13
+ if name is not None or not update:
14
+ if not isinstance(name, str):
15
+ raise ValueError("Knowledge name must be text.")
16
+ result["name"] = checked_text(name.strip(), "Knowledge name", 120)
17
+ if text is not None or not update:
18
+ if not isinstance(text, str) or not text.strip() or "\0" in text:
19
+ raise ValueError("Knowledge text must be nonempty UTF-8 text without NUL characters.")
20
+ if len(text.encode("utf-8")) > 1048576:
21
+ raise ValueError("A Knowledge document holds up to 1 MiB of UTF-8 text.")
22
+ result["text"] = text
23
+ if update:
24
+ if not result:
25
+ raise ValueError("Supply a new name or text to update a Knowledge document.")
26
+ if not isinstance(if_version, str):
27
+ raise ValueError("if_version is required; read the document's current version first.")
28
+ result["ifVersion"] = version(if_version)
29
+ return result
30
+
31
+
32
+ class _Documents:
33
+ def __init__(self, transport: Transport, path: str, scope: str) -> None:
34
+ self._transport, self._path, self._scope = transport, path, scope
35
+
36
+ def _all(self) -> list[dict[str, Any]]:
37
+ return list_field(self._transport.request("GET", self._path), "sources")
38
+
39
+ def list(self) -> list[dict[str, Any]]:
40
+ """List documents owned by this scope. Connections are listed separately."""
41
+ return [source for source in self._all() if source.get("scope") == self._scope]
42
+
43
+ def get(self, source_id: str) -> dict[str, Any]:
44
+ return object_field(
45
+ self._transport.request("GET", self._path + "/" + version(source_id)), "source"
46
+ )
47
+
48
+ def add(self, *, name: str, text: str) -> dict[str, Any]:
49
+ """Save text and begin preparation. Inspect get(id)['state'] for readiness."""
50
+ return self._transport.request("POST", self._path, body=_fields(name, text))
51
+
52
+ def update(
53
+ self, source_id: str, *, if_version: str, name: str | None = None, text: str | None = None
54
+ ) -> dict[str, Any]:
55
+ return self._transport.request(
56
+ "PUT",
57
+ self._path + "/" + version(source_id),
58
+ body=_fields(name, text, update=True, if_version=if_version),
59
+ )
60
+
61
+ def delete(self, source_id: str, *, if_version: str) -> None:
62
+ self._transport.request(
63
+ "DELETE", self._path + "/" + version(source_id), body={"ifVersion": version(if_version)}
64
+ )
65
+
66
+ def retry(self, source_id: str) -> dict[str, Any]:
67
+ """Explicitly retry failed preparation; this can make embedding requests."""
68
+ return self._transport.request(
69
+ "POST", self._path + "/" + version(source_id) + "/retry", body={}
70
+ )
71
+
72
+
73
+ class KnowledgeLibrary(_Documents):
74
+ """The Space's shared library. Adding a source does not connect it to an agent."""
75
+
76
+ def __init__(self, transport: Transport) -> None:
77
+ super().__init__(transport, "/knowledge", "space")
78
+
79
+ def usage(self) -> dict[str, Any]:
80
+ """Embedding usage over 30 days; unknown sums remain None, known sums are strings."""
81
+ return object_field(self._transport.request("GET", self._path), "usage")
82
+
83
+
84
+ class AgentKnowledge(_Documents):
85
+ def __init__(self, transport: Transport, agent_id: str) -> None:
86
+ super().__init__(transport, f"/agents/{agent_id}/knowledge", "agent")
87
+ self._links = f"/agents/{agent_id}/knowledge-links"
88
+
89
+ def connections(self) -> list[dict[str, Any]]:
90
+ return [s for s in self._all() if s.get("scope") == "space" and s.get("attached")]
91
+
92
+ def connect(self, space_source_id: str) -> dict[str, Any]:
93
+ return self._transport.request("PUT", self._links + "/" + version(space_source_id), body={})
94
+
95
+ def disconnect(self, space_source_id: str) -> dict[str, Any]:
96
+ return self._transport.request(
97
+ "DELETE", self._links + "/" + version(space_source_id), body={}
98
+ )
99
+
100
+
101
+ class _AsyncDocuments:
102
+ def __init__(self, transport: AsyncTransport, path: str, scope: str) -> None:
103
+ self._transport, self._path, self._scope = transport, path, scope
104
+
105
+ async def _all(self) -> list[dict[str, Any]]:
106
+ return list_field(await self._transport.request("GET", self._path), "sources")
107
+
108
+ async def list(self) -> list[dict[str, Any]]:
109
+ return [source for source in await self._all() if source.get("scope") == self._scope]
110
+
111
+ async def get(self, source_id: str) -> dict[str, Any]:
112
+ return object_field(
113
+ await self._transport.request("GET", self._path + "/" + version(source_id)), "source"
114
+ )
115
+
116
+ async def add(self, *, name: str, text: str) -> dict[str, Any]:
117
+ return await self._transport.request("POST", self._path, body=_fields(name, text))
118
+
119
+ async def update(
120
+ self, source_id: str, *, if_version: str, name: str | None = None, text: str | None = None
121
+ ) -> dict[str, Any]:
122
+ return await self._transport.request(
123
+ "PUT",
124
+ self._path + "/" + version(source_id),
125
+ body=_fields(name, text, update=True, if_version=if_version),
126
+ )
127
+
128
+ async def delete(self, source_id: str, *, if_version: str) -> None:
129
+ await self._transport.request(
130
+ "DELETE", self._path + "/" + version(source_id), body={"ifVersion": version(if_version)}
131
+ )
132
+
133
+ async def retry(self, source_id: str) -> dict[str, Any]:
134
+ return await self._transport.request(
135
+ "POST", self._path + "/" + version(source_id) + "/retry", body={}
136
+ )
137
+
138
+
139
+ class AsyncKnowledgeLibrary(_AsyncDocuments):
140
+ def __init__(self, transport: AsyncTransport) -> None:
141
+ super().__init__(transport, "/knowledge", "space")
142
+
143
+ async def usage(self) -> dict[str, Any]:
144
+ return object_field(await self._transport.request("GET", self._path), "usage")
145
+
146
+
147
+ class AsyncAgentKnowledge(_AsyncDocuments):
148
+ def __init__(self, transport: AsyncTransport, agent_id: str) -> None:
149
+ super().__init__(transport, f"/agents/{agent_id}/knowledge", "agent")
150
+ self._links = f"/agents/{agent_id}/knowledge-links"
151
+
152
+ async def connections(self) -> list[dict[str, Any]]:
153
+ return [s for s in await self._all() if s.get("scope") == "space" and s.get("attached")]
154
+
155
+ async def connect(self, space_source_id: str) -> dict[str, Any]:
156
+ return await self._transport.request(
157
+ "PUT", self._links + "/" + version(space_source_id), body={}
158
+ )
159
+
160
+ async def disconnect(self, space_source_id: str) -> dict[str, Any]:
161
+ return await self._transport.request(
162
+ "DELETE", self._links + "/" + version(space_source_id), body={}
163
+ )
@@ -0,0 +1,189 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from ._common import list_field, object_field, version
6
+ from ._knowledge import AsyncKnowledgeLibrary, KnowledgeLibrary
7
+ from ._transport import _UNSET, AsyncTransport, Transport, identifier
8
+
9
+
10
+ def _context_body(value: Any, inclusion: str | None, if_version: Any) -> dict[str, Any]:
11
+ body = {"value": value}
12
+ if inclusion is not None:
13
+ if inclusion not in ("always", "selected"):
14
+ raise ValueError("inclusion must be 'always' or 'selected'.")
15
+ body["inclusion"] = inclusion
16
+ if if_version is not _UNSET:
17
+ body["ifVersion"] = None if if_version is None else version(if_version)
18
+ return body
19
+
20
+
21
+ class ContextStore:
22
+ def __init__(self, transport: Transport, path: str, scope: str | None = None) -> None:
23
+ self._transport, self._path = transport, path
24
+ self._params = {"conversation": scope} if scope is not None else None
25
+
26
+ def list(self) -> list[dict[str, Any]]:
27
+ return list_field(
28
+ self._transport.request("GET", self._path, params=self._params), "entries"
29
+ )
30
+
31
+ def get(self, key: str) -> dict[str, Any]:
32
+ return object_field(
33
+ self._transport.request(
34
+ "GET",
35
+ self._path + "/" + identifier(key, "context key"),
36
+ params=self._params,
37
+ ),
38
+ "entry",
39
+ )
40
+
41
+ def set(
42
+ self,
43
+ key: str,
44
+ value: Any,
45
+ *,
46
+ inclusion: str | None = None,
47
+ if_version: Any = _UNSET,
48
+ ) -> dict[str, Any]:
49
+ return object_field(
50
+ self._transport.request(
51
+ "PUT",
52
+ self._path + "/" + identifier(key, "context key"),
53
+ params=self._params,
54
+ body=_context_body(value, inclusion, if_version),
55
+ ),
56
+ "entry",
57
+ )
58
+
59
+ def delete(self, key: str) -> None:
60
+ self._transport.request(
61
+ "DELETE",
62
+ self._path + "/" + identifier(key, "context key"),
63
+ params=self._params,
64
+ )
65
+
66
+
67
+ class AsyncContextStore:
68
+ def __init__(self, transport: AsyncTransport, path: str, scope: str | None = None) -> None:
69
+ self._transport, self._path = transport, path
70
+ self._params = {"conversation": scope} if scope is not None else None
71
+
72
+ async def list(self) -> list[dict[str, Any]]:
73
+ return list_field(
74
+ await self._transport.request(
75
+ "GET",
76
+ self._path,
77
+ params=self._params,
78
+ ),
79
+ "entries",
80
+ )
81
+
82
+ async def get(self, key: str) -> dict[str, Any]:
83
+ return object_field(
84
+ await self._transport.request(
85
+ "GET",
86
+ self._path + "/" + identifier(key, "context key"),
87
+ params=self._params,
88
+ ),
89
+ "entry",
90
+ )
91
+
92
+ async def set(
93
+ self,
94
+ key: str,
95
+ value: Any,
96
+ *,
97
+ inclusion: str | None = None,
98
+ if_version: Any = _UNSET,
99
+ ) -> dict[str, Any]:
100
+ return object_field(
101
+ await self._transport.request(
102
+ "PUT",
103
+ self._path + "/" + identifier(key, "context key"),
104
+ params=self._params,
105
+ body=_context_body(value, inclusion, if_version),
106
+ ),
107
+ "entry",
108
+ )
109
+
110
+ async def delete(self, key: str) -> None:
111
+ await self._transport.request(
112
+ "DELETE",
113
+ self._path + "/" + identifier(key, "context key"),
114
+ params=self._params,
115
+ )
116
+
117
+
118
+ def _note(text: str, scope: str | None) -> dict[str, Any]:
119
+ if not isinstance(text, str) or not text.strip() or len(text.strip().encode()) > 4096:
120
+ raise ValueError("A note must be nonempty text of at most 4096 UTF-8 bytes.")
121
+ return {"text": text, **({"conversation": scope} if scope is not None else {})}
122
+
123
+
124
+ class MemoryStore:
125
+ def __init__(self, transport: Transport, path: str, scope: str | None) -> None:
126
+ self._transport, self._path, self._scope = transport, path, scope
127
+
128
+ def list(self) -> list[dict[str, Any]]:
129
+ return list_field(
130
+ self._transport.request(
131
+ "GET",
132
+ self._path,
133
+ params={"conversation": self._scope} if self._scope else None,
134
+ ),
135
+ "notes",
136
+ )
137
+
138
+ def add(self, text: str) -> dict[str, Any]:
139
+ return object_field(
140
+ self._transport.request(
141
+ "POST",
142
+ self._path,
143
+ body=_note(text, self._scope),
144
+ ),
145
+ "note",
146
+ )
147
+
148
+ def delete(self, note_id: str) -> None:
149
+ self._transport.request("DELETE", self._path + "/" + version(note_id))
150
+
151
+
152
+ class AsyncMemoryStore:
153
+ def __init__(self, transport: AsyncTransport, path: str, scope: str | None) -> None:
154
+ self._transport, self._path, self._scope = transport, path, scope
155
+
156
+ async def list(self) -> list[dict[str, Any]]:
157
+ return list_field(
158
+ await self._transport.request(
159
+ "GET",
160
+ self._path,
161
+ params={"conversation": self._scope} if self._scope else None,
162
+ ),
163
+ "notes",
164
+ )
165
+
166
+ async def add(self, text: str) -> dict[str, Any]:
167
+ return object_field(
168
+ await self._transport.request(
169
+ "POST",
170
+ self._path,
171
+ body=_note(text, self._scope),
172
+ ),
173
+ "note",
174
+ )
175
+
176
+ async def delete(self, note_id: str) -> None:
177
+ await self._transport.request("DELETE", self._path + "/" + version(note_id))
178
+
179
+
180
+ class SpaceContextStore(ContextStore):
181
+ def __init__(self, transport: Transport) -> None:
182
+ super().__init__(transport, "/context")
183
+ self.knowledge = KnowledgeLibrary(transport)
184
+
185
+
186
+ class AsyncSpaceContextStore(AsyncContextStore):
187
+ def __init__(self, transport: AsyncTransport) -> None:
188
+ super().__init__(transport, "/context")
189
+ self.knowledge = AsyncKnowledgeLibrary(transport)