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.
- galet_memory/__init__.py +94 -0
- galet_memory/curation.py +216 -0
- galet_memory/episodic/__init__.py +36 -0
- galet_memory/episodic/embedding_digest_recall.py +74 -0
- galet_memory/episodic/interface.py +82 -0
- galet_memory/episodic/management.py +150 -0
- galet_memory/episodic/sqlite.py +635 -0
- galet_memory/examples/__init__.py +1 -0
- galet_memory/examples/embedding_cli.py +138 -0
- galet_memory/examples/episodic_cli.py +172 -0
- galet_memory/examples/run_examples.py +147 -0
- galet_memory/galet_adapter.py +17 -0
- galet_memory/migrations/__init__.py +1 -0
- galet_memory/migrations/migrate_embeddings_to_vec0.py +177 -0
- galet_memory/migrations/migrate_vec0_canonical.py +221 -0
- galet_memory/migrations/migrate_vec_embeddings_v2.py +118 -0
- galet_memory/ports/__init__.py +41 -0
- galet_memory/ports/contexts.py +39 -0
- galet_memory/ports/embedding_cache.py +124 -0
- galet_memory/ports/embeddings.py +58 -0
- galet_memory/ports/sqlite_vec.py +308 -0
- galet_memory/ports/text.py +25 -0
- galet_memory/procedural/__init__.py +15 -0
- galet_memory/procedural/context_memory.py +74 -0
- galet_memory/procedural/interface.py +48 -0
- galet_memory/semantic/__init__.py +15 -0
- galet_memory/semantic/interface.py +45 -0
- galet_memory/semantic/vector_memory.py +107 -0
- galet_memory-0.1.0.dist-info/METADATA +186 -0
- galet_memory-0.1.0.dist-info/RECORD +34 -0
- galet_memory-0.1.0.dist-info/WHEEL +5 -0
- galet_memory-0.1.0.dist-info/entry_points.txt +3 -0
- galet_memory-0.1.0.dist-info/licenses/LICENSE +21 -0
- galet_memory-0.1.0.dist-info/top_level.txt +1 -0
galet_memory/__init__.py
ADDED
|
@@ -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
|
+
]
|
galet_memory/curation.py
ADDED
|
@@ -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
|