galet-memory 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 (34) hide show
  1. galet_memory/__init__.py +94 -0
  2. galet_memory/curation.py +216 -0
  3. galet_memory/episodic/__init__.py +36 -0
  4. galet_memory/episodic/embedding_digest_recall.py +74 -0
  5. galet_memory/episodic/interface.py +82 -0
  6. galet_memory/episodic/management.py +150 -0
  7. galet_memory/episodic/sqlite.py +635 -0
  8. galet_memory/examples/__init__.py +1 -0
  9. galet_memory/examples/embedding_cli.py +138 -0
  10. galet_memory/examples/episodic_cli.py +172 -0
  11. galet_memory/examples/run_examples.py +147 -0
  12. galet_memory/galet_adapter.py +17 -0
  13. galet_memory/migrations/__init__.py +1 -0
  14. galet_memory/migrations/migrate_embeddings_to_vec0.py +177 -0
  15. galet_memory/migrations/migrate_vec0_canonical.py +221 -0
  16. galet_memory/migrations/migrate_vec_embeddings_v2.py +118 -0
  17. galet_memory/ports/__init__.py +41 -0
  18. galet_memory/ports/contexts.py +39 -0
  19. galet_memory/ports/embedding_cache.py +124 -0
  20. galet_memory/ports/embeddings.py +58 -0
  21. galet_memory/ports/sqlite_vec.py +308 -0
  22. galet_memory/ports/text.py +25 -0
  23. galet_memory/procedural/__init__.py +15 -0
  24. galet_memory/procedural/context_memory.py +74 -0
  25. galet_memory/procedural/interface.py +48 -0
  26. galet_memory/semantic/__init__.py +15 -0
  27. galet_memory/semantic/interface.py +45 -0
  28. galet_memory/semantic/vector_memory.py +107 -0
  29. galet_memory-0.1.0.dist-info/METADATA +186 -0
  30. galet_memory-0.1.0.dist-info/RECORD +34 -0
  31. galet_memory-0.1.0.dist-info/WHEEL +5 -0
  32. galet_memory-0.1.0.dist-info/entry_points.txt +3 -0
  33. galet_memory-0.1.0.dist-info/licenses/LICENSE +21 -0
  34. galet_memory-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,94 @@
1
+ """Provider-neutral memory abstractions and implementations."""
2
+
3
+ from .episodic import (
4
+ EmbeddingDigestRecall,
5
+ EventScope,
6
+ EpisodicCurationRequest,
7
+ EpisodicCurationResult,
8
+ EpisodicConcurrencyError,
9
+ EpisodicDigest,
10
+ EpisodicEvent,
11
+ EpisodicMemory,
12
+ EpisodicMemoryManager,
13
+ EpisodicMemoryRequest,
14
+ EpisodicMemoryResult,
15
+ EpisodicSession,
16
+ EpisodicSessionQuery,
17
+ EpisodicCompatibilityError,
18
+ SqliteEpisodicMemory,
19
+ )
20
+ from .curation import (
21
+ CurationConflictError,
22
+ CurationError,
23
+ CurationResult,
24
+ CurationService,
25
+ CurationSessionNotFoundError,
26
+ CurationStorageError,
27
+ DigestGenerationError,
28
+ DigestGenerationRequest,
29
+ DigestGenerator,
30
+ )
31
+ from .procedural import (
32
+ ContextProceduralMemory,
33
+ ProceduralMemory,
34
+ ProceduralMemoryRequest,
35
+ ProceduralMemoryResult,
36
+ ProceduralSkill,
37
+ )
38
+ from .semantic import (
39
+ SemanticDocument,
40
+ SemanticMemory,
41
+ SemanticMemoryRequest,
42
+ SemanticMemoryResult,
43
+ VectorSemanticMemory,
44
+ )
45
+ from .ports import (
46
+ CachingEmbeddingProvider,
47
+ EmbeddingCache,
48
+ EmbeddingCacheInfo,
49
+ EmbeddingCacheKey,
50
+ )
51
+
52
+ __version__ = "0.1.0.dev0"
53
+
54
+ __all__ = [
55
+ "CachingEmbeddingProvider",
56
+ "ContextProceduralMemory",
57
+ "CurationConflictError",
58
+ "CurationError",
59
+ "CurationResult",
60
+ "CurationService",
61
+ "CurationSessionNotFoundError",
62
+ "CurationStorageError",
63
+ "DigestGenerationError",
64
+ "DigestGenerationRequest",
65
+ "DigestGenerator",
66
+ "EmbeddingDigestRecall",
67
+ "EmbeddingCache",
68
+ "EmbeddingCacheInfo",
69
+ "EmbeddingCacheKey",
70
+ "EventScope",
71
+ "EpisodicCurationRequest",
72
+ "EpisodicCurationResult",
73
+ "EpisodicConcurrencyError",
74
+ "EpisodicDigest",
75
+ "EpisodicEvent",
76
+ "EpisodicMemory",
77
+ "EpisodicMemoryManager",
78
+ "EpisodicMemoryRequest",
79
+ "EpisodicMemoryResult",
80
+ "EpisodicSession",
81
+ "EpisodicSessionQuery",
82
+ "EpisodicCompatibilityError",
83
+ "SqliteEpisodicMemory",
84
+ "ProceduralMemory",
85
+ "ProceduralMemoryRequest",
86
+ "ProceduralMemoryResult",
87
+ "ProceduralSkill",
88
+ "SemanticDocument",
89
+ "SemanticMemory",
90
+ "SemanticMemoryRequest",
91
+ "SemanticMemoryResult",
92
+ "VectorSemanticMemory",
93
+ "__version__",
94
+ ]
@@ -0,0 +1,216 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Literal, Optional, Protocol, Sequence, runtime_checkable
5
+ from uuid import uuid4
6
+
7
+ from .episodic import EpisodicEvent, EpisodicMemoryManager, EpisodicSession
8
+ from .episodic.management import EpisodicConcurrencyError
9
+
10
+
11
+ class CurationError(RuntimeError):
12
+ """Base class for neutral curation failures."""
13
+
14
+
15
+ class CurationSessionNotFoundError(CurationError):
16
+ """The session is missing or is not owned by the requested account."""
17
+
18
+
19
+ class DigestGenerationError(CurationError):
20
+ """The digest generator failed or returned no usable text."""
21
+
22
+
23
+ class CurationConflictError(CurationError):
24
+ """The session changed after the digest snapshot was taken."""
25
+
26
+
27
+ class CurationStorageError(CurationError):
28
+ """The episodic store failed outside a recognised conflict."""
29
+
30
+
31
+ @dataclass(frozen=True)
32
+ class DigestGenerationRequest:
33
+ session_id: str
34
+ account_name: str
35
+ agent_name: str
36
+ friendly_name: str
37
+ events: Sequence[EpisodicEvent]
38
+ max_chars: int = 32000
39
+
40
+
41
+ @runtime_checkable
42
+ class DigestGenerator(Protocol):
43
+ def generate(self, request: DigestGenerationRequest) -> str:
44
+ """Return digest text without changing storage."""
45
+ ...
46
+
47
+
48
+ @dataclass(frozen=True)
49
+ class CurationResult:
50
+ action: Literal["digest", "archive"]
51
+ session_id: str
52
+ digest: str
53
+ boundary_event: Optional[EpisodicEvent] = None
54
+ idempotency_key: str = ""
55
+
56
+
57
+ class CurationService:
58
+ """Generate digests and establish non-destructive archive boundaries."""
59
+
60
+ def __init__(
61
+ self,
62
+ episodic_store: EpisodicMemoryManager,
63
+ digest_generator: DigestGenerator,
64
+ ) -> None:
65
+ self.episodic_store = episodic_store
66
+ self.digest_generator = digest_generator
67
+
68
+ def produce_digest(
69
+ self,
70
+ *,
71
+ account_name: str,
72
+ session_id: str,
73
+ max_chars: int = 32000,
74
+ ) -> CurationResult:
75
+ session = self._load_owned_active_session(account_name, session_id)
76
+ digest = self._generate(session, max_chars=max_chars)
77
+ return CurationResult(
78
+ action="digest",
79
+ session_id=session.session_id,
80
+ digest=digest,
81
+ )
82
+
83
+ def archive(
84
+ self,
85
+ *,
86
+ account_name: str,
87
+ session_id: str,
88
+ max_chars: int = 32000,
89
+ idempotency_key: Optional[str] = None,
90
+ ) -> CurationResult:
91
+ operation_key = idempotency_key or str(uuid4())
92
+ session = self._load_owned_active_session(account_name, session_id)
93
+ existing = self._find_idempotent_boundary(
94
+ account_name, session_id, operation_key
95
+ )
96
+ if existing is not None:
97
+ return self._archive_result(session_id, existing, operation_key)
98
+
99
+ expected_tail = session.events[-1].event_id if session.events else None
100
+ digest = self._generate(session, max_chars=max_chars)
101
+ boundary = EpisodicEvent(
102
+ role="system",
103
+ actor="curation",
104
+ kind="session_digest",
105
+ content=digest,
106
+ metadata={
107
+ "visibility_boundary": True,
108
+ "curation_version": 1,
109
+ "idempotency_key": operation_key,
110
+ },
111
+ )
112
+ try:
113
+ stored = self.episodic_store.append_event_if_tail(
114
+ session_id,
115
+ boundary,
116
+ expected_last_event_id=expected_tail,
117
+ )
118
+ except EpisodicConcurrencyError as exc:
119
+ existing = self._find_idempotent_boundary(
120
+ account_name, session_id, operation_key
121
+ )
122
+ if existing is not None:
123
+ return self._archive_result(session_id, existing, operation_key)
124
+ raise CurationConflictError(str(exc)) from exc
125
+ except Exception as exc:
126
+ raise CurationStorageError(
127
+ f"failed to append digest boundary for session {session_id}"
128
+ ) from exc
129
+ return self._archive_result(session_id, stored, operation_key)
130
+
131
+ def _load_owned_active_session(
132
+ self, account_name: str, session_id: str
133
+ ) -> EpisodicSession:
134
+ try:
135
+ session = self.episodic_store.get_session(
136
+ session_id, include_events=True, event_scope="active"
137
+ )
138
+ except Exception as exc:
139
+ raise CurationStorageError(
140
+ f"failed to load session {session_id}"
141
+ ) from exc
142
+ if session is None or session.account_name != account_name:
143
+ raise CurationSessionNotFoundError(
144
+ f"session not found for account: {session_id}"
145
+ )
146
+ return session
147
+
148
+ def _generate(self, session: EpisodicSession, *, max_chars: int) -> str:
149
+ if max_chars <= 0:
150
+ raise ValueError("max_chars must be greater than zero")
151
+ request = DigestGenerationRequest(
152
+ session_id=session.session_id,
153
+ account_name=session.account_name,
154
+ agent_name=session.agent_name,
155
+ friendly_name=session.friendly_name or "",
156
+ events=tuple(session.events),
157
+ max_chars=max_chars,
158
+ )
159
+ try:
160
+ digest = self.digest_generator.generate(request)
161
+ except Exception as exc:
162
+ raise DigestGenerationError("digest generation failed") from exc
163
+ if not isinstance(digest, str) or not digest.strip():
164
+ raise DigestGenerationError("digest generator returned empty text")
165
+ return digest.strip()
166
+
167
+ def _find_idempotent_boundary(
168
+ self, account_name: str, session_id: str, idempotency_key: str
169
+ ) -> Optional[EpisodicEvent]:
170
+ try:
171
+ session = self.episodic_store.get_session(
172
+ session_id, include_events=True, event_scope="all"
173
+ )
174
+ except Exception as exc:
175
+ raise CurationStorageError(
176
+ f"failed to inspect session {session_id}"
177
+ ) from exc
178
+ if session is None or session.account_name != account_name:
179
+ raise CurationSessionNotFoundError(
180
+ f"session not found for account: {session_id}"
181
+ )
182
+ for event in reversed(session.events):
183
+ if (
184
+ event.kind == "session_digest"
185
+ and event.metadata.get("visibility_boundary") is True
186
+ and event.metadata.get("idempotency_key") == idempotency_key
187
+ ):
188
+ return event
189
+ return None
190
+
191
+ @staticmethod
192
+ def _archive_result(
193
+ session_id: str,
194
+ boundary: EpisodicEvent,
195
+ idempotency_key: str,
196
+ ) -> CurationResult:
197
+ return CurationResult(
198
+ action="archive",
199
+ session_id=session_id,
200
+ digest=str(boundary.content),
201
+ boundary_event=boundary,
202
+ idempotency_key=idempotency_key,
203
+ )
204
+
205
+
206
+ __all__ = [
207
+ "CurationConflictError",
208
+ "CurationError",
209
+ "CurationResult",
210
+ "CurationService",
211
+ "CurationSessionNotFoundError",
212
+ "CurationStorageError",
213
+ "DigestGenerationError",
214
+ "DigestGenerationRequest",
215
+ "DigestGenerator",
216
+ ]
@@ -0,0 +1,36 @@
1
+ from .interface import (
2
+ EpisodicDigest,
3
+ EpisodicEvent,
4
+ EpisodicMemory,
5
+ EpisodicMemoryRequest,
6
+ EpisodicMemoryResult,
7
+ )
8
+ from .management import (
9
+ EventScope,
10
+ EpisodicCurationRequest,
11
+ EpisodicCurationResult,
12
+ EpisodicConcurrencyError,
13
+ EpisodicMemoryManager,
14
+ EpisodicSession,
15
+ EpisodicSessionQuery,
16
+ )
17
+ from .embedding_digest_recall import EmbeddingDigestRecall
18
+ from .sqlite import EpisodicCompatibilityError, SqliteEpisodicMemory
19
+
20
+ __all__ = [
21
+ "EventScope",
22
+ "EpisodicCurationRequest",
23
+ "EpisodicCurationResult",
24
+ "EpisodicConcurrencyError",
25
+ "EpisodicDigest",
26
+ "EmbeddingDigestRecall",
27
+ "EpisodicEvent",
28
+ "EpisodicMemory",
29
+ "EpisodicMemoryManager",
30
+ "EpisodicMemoryRequest",
31
+ "EpisodicMemoryResult",
32
+ "EpisodicSession",
33
+ "EpisodicSessionQuery",
34
+ "EpisodicCompatibilityError",
35
+ "SqliteEpisodicMemory",
36
+ ]
@@ -0,0 +1,74 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+
5
+ from .interface import EpisodicDigest, EpisodicMemoryRequest
6
+ from ..ports import EmbeddingIndex, EmbeddingProvider, TextLoader
7
+
8
+
9
+ class EmbeddingDigestRecall:
10
+ def __init__(
11
+ self,
12
+ *,
13
+ embeddings: EmbeddingProvider,
14
+ index: EmbeddingIndex,
15
+ text_loader: TextLoader,
16
+ namespaces: list[str] | None = None,
17
+ score_threshold: float = 0.25,
18
+ embedding_model: str = "text-embedding-3-small",
19
+ ) -> None:
20
+ self.embeddings = embeddings
21
+ self.index = index
22
+ self.text_loader = text_loader
23
+ self.namespaces = list(namespaces or ["digests"])
24
+ self.score_threshold = score_threshold
25
+ self.embedding_model = embedding_model
26
+
27
+ def __call__(self, request: EpisodicMemoryRequest) -> list[EpisodicDigest]:
28
+ query = request.query.strip()
29
+ if not query:
30
+ return []
31
+ try:
32
+ vector = self.embeddings.embed(
33
+ [query], model=self.embedding_model
34
+ )[0]
35
+ matches = self.index.query(
36
+ account_name=request.account_name,
37
+ namespaces=self.namespaces,
38
+ vector=vector,
39
+ limit=request.digest_top_k,
40
+ )
41
+ digests: list[EpisodicDigest] = []
42
+ for match in matches:
43
+ if match.score < self.score_threshold:
44
+ continue
45
+ metadata = dict(match.record.metadata)
46
+ path = metadata.get("path")
47
+ if not path:
48
+ continue
49
+ snippet = self.text_loader.load(
50
+ path, max_chars=request.digest_max_chars
51
+ )
52
+ if not snippet.text.strip():
53
+ continue
54
+ digests.append(
55
+ EpisodicDigest(
56
+ session_id=match.record.source_id,
57
+ snippet=snippet.text,
58
+ score=float(match.score),
59
+ truncated=snippet.truncated,
60
+ metadata={
61
+ **metadata,
62
+ "namespace": "digests",
63
+ "embedding_model": self.embedding_model,
64
+ },
65
+ )
66
+ )
67
+ return digests
68
+ except Exception as exc:
69
+ logging.warning(
70
+ "EmbeddingDigestRecall: failed for account=%s: %s",
71
+ request.account_name,
72
+ exc,
73
+ )
74
+ return []
@@ -0,0 +1,82 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from dataclasses import dataclass, field
5
+ from datetime import datetime
6
+ from typing import Any, Dict, List, Optional
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class EpisodicMemoryRequest:
11
+ """Request for conversational or experiential memory."""
12
+
13
+ account_name: str
14
+ agent_name: str
15
+ conversation_id: str = ""
16
+ query: str = ""
17
+ max_events: int = 6
18
+ token_budget: Optional[int] = None
19
+ digest_top_k: int = 3
20
+ digest_max_chars: int = 3000
21
+ event_kinds: Optional[List[str]] = None
22
+ include_session_metadata: bool = True
23
+ include_recent_history: bool = True
24
+ include_archived_digests: bool = True
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class EpisodicEvent:
29
+ """Provider-neutral representation of a stored event."""
30
+
31
+ role: str
32
+ content: Any
33
+ kind: str = ""
34
+ actor: str = ""
35
+ event_id: str = ""
36
+ created_at: Optional[datetime] = None
37
+ metadata: Dict[str, Any] = field(default_factory=dict)
38
+
39
+
40
+ @dataclass(frozen=True)
41
+ class EpisodicDigest:
42
+ session_id: str
43
+ snippet: str
44
+ score: float = 0.0
45
+ truncated: bool = False
46
+ metadata: Dict[str, Any] = field(default_factory=dict)
47
+
48
+
49
+ @dataclass
50
+ class EpisodicMemoryResult:
51
+ session_id: str = ""
52
+ session_user_id: str = ""
53
+ session_account_name: str = ""
54
+ session_agent_name: str = ""
55
+ session_context_name: str = ""
56
+ session_type: str = ""
57
+ session_participants: List[str] = field(default_factory=list)
58
+ session_friendly_name: str = ""
59
+ session_tags: List[str] = field(default_factory=list)
60
+ session_updated_at: Optional[datetime] = None
61
+ events: List[EpisodicEvent] = field(default_factory=list)
62
+ digests: List[EpisodicDigest] = field(default_factory=list)
63
+ dropped_event_count: int = 0
64
+ metadata: Dict[str, Any] = field(default_factory=dict)
65
+
66
+
67
+ class EpisodicMemory(ABC):
68
+ """Prompt-time access to current and archived episodic memory."""
69
+
70
+ @abstractmethod
71
+ def recall(self, request: EpisodicMemoryRequest) -> EpisodicMemoryResult:
72
+ raise NotImplementedError
73
+
74
+ @abstractmethod
75
+ def save_overflow_digest(
76
+ self,
77
+ *,
78
+ account_name: str,
79
+ conversation_id: str,
80
+ snippet: str,
81
+ ) -> Optional[str]:
82
+ raise NotImplementedError
@@ -0,0 +1,150 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from dataclasses import dataclass, field
5
+ from datetime import datetime
6
+ from typing import Any, Dict, List, Literal, Optional
7
+
8
+ from .interface import EpisodicEvent
9
+
10
+
11
+ EventScope = Literal["active", "all", "archived"]
12
+
13
+
14
+ class EpisodicConcurrencyError(RuntimeError):
15
+ """Raised when a conditional write observes a different event tail."""
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class EpisodicSessionQuery:
20
+ account_name: str
21
+ agent_name: str = ""
22
+ query: str = ""
23
+ limit: int = 20
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class EpisodicSession:
28
+ session_id: str
29
+ account_name: str
30
+ agent_name: str
31
+ user_id: str = ""
32
+ friendly_name: Optional[str] = None
33
+ context_name: Optional[str] = None
34
+ session_type: str = "user"
35
+ participants: List[str] = field(default_factory=list)
36
+ links: Dict[str, Any] = field(default_factory=dict)
37
+ created_at: Optional[datetime] = None
38
+ updated_at: Optional[datetime] = None
39
+ tags: List[str] = field(default_factory=list)
40
+ metadata: Dict[str, Any] = field(default_factory=dict)
41
+ events: List[EpisodicEvent] = field(default_factory=list)
42
+
43
+
44
+ @dataclass(frozen=True)
45
+ class EpisodicCurationRequest:
46
+ account_name: str
47
+ session_id: str = ""
48
+ friendly_name: str = ""
49
+ mode: str = "filter"
50
+ preview: bool = True
51
+ publish: bool = False
52
+ template_name: str = "default"
53
+ curation_rules: Dict[str, Any] = field(default_factory=dict)
54
+ max_chars: int = 32000
55
+
56
+
57
+ @dataclass(frozen=True)
58
+ class EpisodicCurationResult:
59
+ status: str
60
+ session_id: str = ""
61
+ note_text: str = ""
62
+ output_path: str = ""
63
+ summary: Dict[str, Any] = field(default_factory=dict)
64
+ error: str = ""
65
+
66
+
67
+ class EpisodicMemoryManager(ABC):
68
+ """Provider-neutral episodic session and event store."""
69
+
70
+ @abstractmethod
71
+ def create_session(
72
+ self,
73
+ *,
74
+ account_name: str,
75
+ agent_name: str,
76
+ user_id: Optional[str] = None,
77
+ session_id: Optional[str] = None,
78
+ friendly_name: Optional[str] = None,
79
+ context_name: Optional[str] = None,
80
+ tags: Optional[List[str]] = None,
81
+ session_type: str = "user",
82
+ participants: Optional[List[str]] = None,
83
+ links: Optional[Dict[str, Any]] = None,
84
+ metadata: Optional[Dict[str, Any]] = None,
85
+ ) -> EpisodicSession:
86
+ raise NotImplementedError
87
+
88
+ @abstractmethod
89
+ def get_session(
90
+ self,
91
+ session_id: str,
92
+ *,
93
+ include_events: bool = True,
94
+ event_scope: EventScope = "active",
95
+ ) -> Optional[EpisodicSession]:
96
+ raise NotImplementedError
97
+
98
+ @abstractmethod
99
+ def list_sessions(self, query: EpisodicSessionQuery) -> List[EpisodicSession]:
100
+ raise NotImplementedError
101
+
102
+ @abstractmethod
103
+ def session_exists(self, session_id: str) -> bool:
104
+ raise NotImplementedError
105
+
106
+ @abstractmethod
107
+ def append_event(
108
+ self, session_id: str, event: EpisodicEvent
109
+ ) -> EpisodicEvent:
110
+ raise NotImplementedError
111
+
112
+ @abstractmethod
113
+ def append_event_if_tail(
114
+ self,
115
+ session_id: str,
116
+ event: EpisodicEvent,
117
+ *,
118
+ expected_last_event_id: Optional[str],
119
+ ) -> EpisodicEvent:
120
+ """Append atomically only when the current event tail is expected."""
121
+ raise NotImplementedError
122
+
123
+ @abstractmethod
124
+ def add_events(
125
+ self, session_id: str, events: List[EpisodicEvent]
126
+ ) -> List[EpisodicEvent]:
127
+ raise NotImplementedError
128
+
129
+ @abstractmethod
130
+ def link_event(
131
+ self,
132
+ correlation_id: Optional[str],
133
+ session_id: str,
134
+ event_id: str,
135
+ ) -> None:
136
+ raise NotImplementedError
137
+
138
+ @abstractmethod
139
+ def update_session(
140
+ self, session_id: str, patch: Dict[str, Any]
141
+ ) -> EpisodicSession:
142
+ raise NotImplementedError
143
+
144
+ @abstractmethod
145
+ def reset_session(self, session_id: str) -> None:
146
+ raise NotImplementedError
147
+
148
+ @abstractmethod
149
+ def delete_session(self, session_id: str) -> None:
150
+ raise NotImplementedError