google-adk-extras 0.1.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.
- google_adk_extras/__init__.py +1 -0
- google_adk_extras/artifacts/__init__.py +15 -0
- google_adk_extras/artifacts/base_custom_artifact_service.py +207 -0
- google_adk_extras/artifacts/local_folder_artifact_service.py +242 -0
- google_adk_extras/artifacts/mongo_artifact_service.py +221 -0
- google_adk_extras/artifacts/s3_artifact_service.py +323 -0
- google_adk_extras/artifacts/sql_artifact_service.py +255 -0
- google_adk_extras/memory/__init__.py +15 -0
- google_adk_extras/memory/base_custom_memory_service.py +90 -0
- google_adk_extras/memory/mongo_memory_service.py +174 -0
- google_adk_extras/memory/redis_memory_service.py +188 -0
- google_adk_extras/memory/sql_memory_service.py +213 -0
- google_adk_extras/memory/yaml_file_memory_service.py +176 -0
- google_adk_extras/py.typed +0 -0
- google_adk_extras/sessions/__init__.py +13 -0
- google_adk_extras/sessions/base_custom_session_service.py +183 -0
- google_adk_extras/sessions/mongo_session_service.py +243 -0
- google_adk_extras/sessions/redis_session_service.py +271 -0
- google_adk_extras/sessions/sql_session_service.py +308 -0
- google_adk_extras/sessions/yaml_file_session_service.py +245 -0
- google_adk_extras-0.1.1.dist-info/METADATA +175 -0
- google_adk_extras-0.1.1.dist-info/RECORD +25 -0
- google_adk_extras-0.1.1.dist-info/WHEEL +5 -0
- google_adk_extras-0.1.1.dist-info/licenses/LICENSE +202 -0
- google_adk_extras-0.1.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,176 @@
|
|
1
|
+
"""YAML file-based memory service implementation."""
|
2
|
+
|
3
|
+
import os
|
4
|
+
import json
|
5
|
+
import logging
|
6
|
+
from typing import Optional, List
|
7
|
+
from pathlib import Path
|
8
|
+
import re
|
9
|
+
from datetime import datetime
|
10
|
+
|
11
|
+
import yaml
|
12
|
+
|
13
|
+
from google.genai import types
|
14
|
+
from .base_custom_memory_service import BaseCustomMemoryService
|
15
|
+
|
16
|
+
|
17
|
+
logger = logging.getLogger('google_adk_extras.' + __name__)
|
18
|
+
|
19
|
+
|
20
|
+
class YamlFileMemoryService(BaseCustomMemoryService):
|
21
|
+
"""YAML file-based memory service implementation."""
|
22
|
+
|
23
|
+
def __init__(self, base_directory: str = "./memory"):
|
24
|
+
"""Initialize the YAML file memory service.
|
25
|
+
|
26
|
+
Args:
|
27
|
+
base_directory: Base directory for storing memory files
|
28
|
+
"""
|
29
|
+
super().__init__()
|
30
|
+
self.base_directory = Path(base_directory)
|
31
|
+
# Create base directory if it doesn't exist
|
32
|
+
self.base_directory.mkdir(parents=True, exist_ok=True)
|
33
|
+
|
34
|
+
async def _initialize_impl(self) -> None:
|
35
|
+
"""Initialize the file system memory service."""
|
36
|
+
# Ensure base directory exists
|
37
|
+
self.base_directory.mkdir(parents=True, exist_ok=True)
|
38
|
+
|
39
|
+
async def _cleanup_impl(self) -> None:
|
40
|
+
"""Clean up resources (no cleanup needed for file-based service)."""
|
41
|
+
pass
|
42
|
+
|
43
|
+
def _get_memory_directory(self, app_name: str, user_id: str) -> Path:
|
44
|
+
"""Generate directory path for memory entries."""
|
45
|
+
directory = self.base_directory / app_name / user_id
|
46
|
+
directory.mkdir(parents=True, exist_ok=True)
|
47
|
+
return directory
|
48
|
+
|
49
|
+
def _get_memory_file_path(self, app_name: str, user_id: str, memory_id: str) -> Path:
|
50
|
+
"""Generate file path for a memory entry."""
|
51
|
+
directory = self._get_memory_directory(app_name, user_id)
|
52
|
+
return directory / f"{memory_id}.yaml"
|
53
|
+
|
54
|
+
def _serialize_content(self, content: types.Content) -> dict:
|
55
|
+
"""Serialize Content object to dictionary."""
|
56
|
+
try:
|
57
|
+
return content.to_json_dict()
|
58
|
+
except (TypeError, ValueError) as e:
|
59
|
+
raise ValueError(f"Failed to serialize content: {e}")
|
60
|
+
|
61
|
+
def _deserialize_content(self, content_dict: dict) -> types.Content:
|
62
|
+
"""Deserialize Content object from dictionary."""
|
63
|
+
try:
|
64
|
+
return types.Content(**content_dict)
|
65
|
+
except (TypeError, ValueError) as e:
|
66
|
+
raise ValueError(f"Failed to deserialize content: {e}")
|
67
|
+
|
68
|
+
def _extract_text_from_content(self, content: types.Content) -> str:
|
69
|
+
"""Extract text content from a Content object for storage and search."""
|
70
|
+
if not content or not content.parts:
|
71
|
+
return ""
|
72
|
+
|
73
|
+
text_parts = []
|
74
|
+
for part in content.parts:
|
75
|
+
if part.text:
|
76
|
+
text_parts.append(part.text)
|
77
|
+
|
78
|
+
return " ".join(text_parts)
|
79
|
+
|
80
|
+
def _extract_search_terms(self, text: str) -> List[str]:
|
81
|
+
"""Extract search terms from text content."""
|
82
|
+
# Extract words from text and convert to lowercase
|
83
|
+
words = re.findall(r'[A-Za-z]+', text.lower())
|
84
|
+
# Return unique words as a list
|
85
|
+
return sorted(set(words))
|
86
|
+
|
87
|
+
async def _add_session_to_memory_impl(self, session: "Session") -> None:
|
88
|
+
"""Implementation of adding a session to memory."""
|
89
|
+
try:
|
90
|
+
# Add each event in the session as a separate memory entry
|
91
|
+
for event in session.events:
|
92
|
+
if not event.content or not event.content.parts:
|
93
|
+
continue
|
94
|
+
|
95
|
+
# Extract text content and search terms
|
96
|
+
text_content = self._extract_text_from_content(event.content)
|
97
|
+
search_terms = self._extract_search_terms(text_content)
|
98
|
+
|
99
|
+
# Generate memory ID
|
100
|
+
memory_id = f"{session.id}_{event.timestamp}"
|
101
|
+
|
102
|
+
# Create memory entry
|
103
|
+
memory_entry = {
|
104
|
+
"id": memory_id,
|
105
|
+
"app_name": session.app_name,
|
106
|
+
"user_id": session.user_id,
|
107
|
+
"content": self._serialize_content(event.content),
|
108
|
+
"author": event.author,
|
109
|
+
"timestamp": event.timestamp,
|
110
|
+
"text_content": text_content,
|
111
|
+
"search_terms": search_terms
|
112
|
+
}
|
113
|
+
|
114
|
+
# Save to YAML file
|
115
|
+
file_path = self._get_memory_file_path(session.app_name, session.user_id, memory_id)
|
116
|
+
with open(file_path, 'w') as f:
|
117
|
+
yaml.dump(memory_entry, f, default_flow_style=False, allow_unicode=True)
|
118
|
+
|
119
|
+
except Exception as e:
|
120
|
+
raise RuntimeError(f"Failed to add session to memory: {e}")
|
121
|
+
|
122
|
+
async def _search_memory_impl(
|
123
|
+
self, *, app_name: str, user_id: str, query: str
|
124
|
+
) -> "SearchMemoryResponse":
|
125
|
+
"""Implementation of searching memory."""
|
126
|
+
from google.adk.memory.base_memory_service import SearchMemoryResponse
|
127
|
+
from google.adk.memory.memory_entry import MemoryEntry
|
128
|
+
|
129
|
+
try:
|
130
|
+
# Extract search terms from query
|
131
|
+
query_terms = self._extract_search_terms(query)
|
132
|
+
|
133
|
+
if not query_terms:
|
134
|
+
# If no searchable terms in query, return empty response
|
135
|
+
return SearchMemoryResponse(memories=[])
|
136
|
+
|
137
|
+
# Get memory directory for this user
|
138
|
+
memory_directory = self._get_memory_directory(app_name, user_id)
|
139
|
+
|
140
|
+
# Find all memory files for this user
|
141
|
+
memory_files = list(memory_directory.glob("*.yaml"))
|
142
|
+
|
143
|
+
# Filter entries that match any of the query terms
|
144
|
+
matching_memories = []
|
145
|
+
for file_path in memory_files:
|
146
|
+
try:
|
147
|
+
with open(file_path, 'r') as f:
|
148
|
+
entry = yaml.safe_load(f)
|
149
|
+
|
150
|
+
# Check if any query term matches the search terms in this entry
|
151
|
+
entry_search_terms = entry.get("search_terms", [])
|
152
|
+
if any(term in entry_search_terms for term in query_terms):
|
153
|
+
matching_memories.append(entry)
|
154
|
+
except (yaml.YAMLError, IOError):
|
155
|
+
# Skip invalid files
|
156
|
+
continue
|
157
|
+
|
158
|
+
# Convert to MemoryEntry objects
|
159
|
+
memories = []
|
160
|
+
for entry in matching_memories:
|
161
|
+
content = self._deserialize_content(entry["content"])
|
162
|
+
# Format timestamp as ISO string
|
163
|
+
timestamp_str = None
|
164
|
+
if entry.get("timestamp"):
|
165
|
+
timestamp_str = datetime.fromtimestamp(entry["timestamp"]).isoformat()
|
166
|
+
|
167
|
+
memory_entry = MemoryEntry(
|
168
|
+
content=content,
|
169
|
+
author=entry.get("author"),
|
170
|
+
timestamp=timestamp_str
|
171
|
+
)
|
172
|
+
memories.append(memory_entry)
|
173
|
+
|
174
|
+
return SearchMemoryResponse(memories=memories)
|
175
|
+
except Exception as e:
|
176
|
+
raise RuntimeError(f"Failed to search memory: {e}")
|
File without changes
|
@@ -0,0 +1,13 @@
|
|
1
|
+
"""Custom session service implementations for Google ADK."""
|
2
|
+
|
3
|
+
from .sql_session_service import SQLSessionService
|
4
|
+
from .mongo_session_service import MongoSessionService
|
5
|
+
from .redis_session_service import RedisSessionService
|
6
|
+
from .yaml_file_session_service import YamlFileSessionService
|
7
|
+
|
8
|
+
__all__ = [
|
9
|
+
"SQLSessionService",
|
10
|
+
"MongoSessionService",
|
11
|
+
"RedisSessionService",
|
12
|
+
"YamlFileSessionService",
|
13
|
+
]
|
@@ -0,0 +1,183 @@
|
|
1
|
+
"""Base class for custom session services."""
|
2
|
+
|
3
|
+
import abc
|
4
|
+
from typing import Any, Optional
|
5
|
+
|
6
|
+
from google.adk.sessions.base_session_service import BaseSessionService
|
7
|
+
from google.adk.sessions.session import Session
|
8
|
+
from google.adk.events.event import Event
|
9
|
+
from google.adk.sessions.base_session_service import GetSessionConfig, ListSessionsResponse
|
10
|
+
|
11
|
+
|
12
|
+
class BaseCustomSessionService(BaseSessionService, abc.ABC):
|
13
|
+
"""Base class for custom session services with common functionality."""
|
14
|
+
|
15
|
+
def __init__(self):
|
16
|
+
"""Initialize the base custom session service."""
|
17
|
+
super().__init__()
|
18
|
+
self._initialized = False
|
19
|
+
|
20
|
+
async def initialize(self) -> None:
|
21
|
+
"""Initialize the session service.
|
22
|
+
|
23
|
+
This method should be called before using the service to ensure
|
24
|
+
any required setup (database connections, etc.) is complete.
|
25
|
+
"""
|
26
|
+
if not self._initialized:
|
27
|
+
await self._initialize_impl()
|
28
|
+
self._initialized = True
|
29
|
+
|
30
|
+
@abc.abstractmethod
|
31
|
+
async def _initialize_impl(self) -> None:
|
32
|
+
"""Implementation of service initialization.
|
33
|
+
|
34
|
+
This method should handle any setup required for the service to function,
|
35
|
+
such as database connections, creating tables, etc.
|
36
|
+
"""
|
37
|
+
pass
|
38
|
+
|
39
|
+
async def cleanup(self) -> None:
|
40
|
+
"""Clean up resources used by the session service.
|
41
|
+
|
42
|
+
This method should be called when the service is no longer needed
|
43
|
+
to ensure proper cleanup of resources.
|
44
|
+
"""
|
45
|
+
if self._initialized:
|
46
|
+
await self._cleanup_impl()
|
47
|
+
self._initialized = False
|
48
|
+
|
49
|
+
@abc.abstractmethod
|
50
|
+
async def _cleanup_impl(self) -> None:
|
51
|
+
"""Implementation of service cleanup.
|
52
|
+
|
53
|
+
This method should handle any cleanup required for the service,
|
54
|
+
such as closing database connections.
|
55
|
+
"""
|
56
|
+
pass
|
57
|
+
|
58
|
+
async def create_session(
|
59
|
+
self,
|
60
|
+
*,
|
61
|
+
app_name: str,
|
62
|
+
user_id: str,
|
63
|
+
state: Optional[dict[str, Any]] = None,
|
64
|
+
session_id: Optional[str] = None,
|
65
|
+
) -> Session:
|
66
|
+
"""Create a new session."""
|
67
|
+
if not self._initialized:
|
68
|
+
await self.initialize()
|
69
|
+
return await self._create_session_impl(
|
70
|
+
app_name=app_name,
|
71
|
+
user_id=user_id,
|
72
|
+
state=state,
|
73
|
+
session_id=session_id,
|
74
|
+
)
|
75
|
+
|
76
|
+
async def get_session(
|
77
|
+
self,
|
78
|
+
*,
|
79
|
+
app_name: str,
|
80
|
+
user_id: str,
|
81
|
+
session_id: str,
|
82
|
+
config: Optional[GetSessionConfig] = None,
|
83
|
+
) -> Optional[Session]:
|
84
|
+
"""Get a session by ID."""
|
85
|
+
if not self._initialized:
|
86
|
+
await self.initialize()
|
87
|
+
return await self._get_session_impl(
|
88
|
+
app_name=app_name,
|
89
|
+
user_id=user_id,
|
90
|
+
session_id=session_id,
|
91
|
+
config=config,
|
92
|
+
)
|
93
|
+
|
94
|
+
async def list_sessions(
|
95
|
+
self,
|
96
|
+
*,
|
97
|
+
app_name: str,
|
98
|
+
user_id: str
|
99
|
+
) -> ListSessionsResponse:
|
100
|
+
"""List all sessions for a user."""
|
101
|
+
if not self._initialized:
|
102
|
+
await self.initialize()
|
103
|
+
return await self._list_sessions_impl(
|
104
|
+
app_name=app_name,
|
105
|
+
user_id=user_id,
|
106
|
+
)
|
107
|
+
|
108
|
+
async def delete_session(
|
109
|
+
self,
|
110
|
+
*,
|
111
|
+
app_name: str,
|
112
|
+
user_id: str,
|
113
|
+
session_id: str
|
114
|
+
) -> None:
|
115
|
+
"""Delete a session."""
|
116
|
+
if not self._initialized:
|
117
|
+
await self.initialize()
|
118
|
+
await self._delete_session_impl(
|
119
|
+
app_name=app_name,
|
120
|
+
user_id=user_id,
|
121
|
+
session_id=session_id,
|
122
|
+
)
|
123
|
+
|
124
|
+
async def append_event(self, session: Session, event: Event) -> Event:
|
125
|
+
"""Append an event to a session."""
|
126
|
+
if not self._initialized:
|
127
|
+
await self.initialize()
|
128
|
+
# Update the session object
|
129
|
+
await super().append_event(session=session, event=event)
|
130
|
+
session.last_update_time = event.timestamp
|
131
|
+
# Update the storage
|
132
|
+
await self._append_event_impl(session=session, event=event)
|
133
|
+
return event
|
134
|
+
|
135
|
+
@abc.abstractmethod
|
136
|
+
async def _create_session_impl(
|
137
|
+
self,
|
138
|
+
*,
|
139
|
+
app_name: str,
|
140
|
+
user_id: str,
|
141
|
+
state: Optional[dict[str, Any]] = None,
|
142
|
+
session_id: Optional[str] = None,
|
143
|
+
) -> Session:
|
144
|
+
"""Implementation of session creation."""
|
145
|
+
pass
|
146
|
+
|
147
|
+
@abc.abstractmethod
|
148
|
+
async def _get_session_impl(
|
149
|
+
self,
|
150
|
+
*,
|
151
|
+
app_name: str,
|
152
|
+
user_id: str,
|
153
|
+
session_id: str,
|
154
|
+
config: Optional[GetSessionConfig] = None,
|
155
|
+
) -> Optional[Session]:
|
156
|
+
"""Implementation of session retrieval."""
|
157
|
+
pass
|
158
|
+
|
159
|
+
@abc.abstractmethod
|
160
|
+
async def _list_sessions_impl(
|
161
|
+
self,
|
162
|
+
*,
|
163
|
+
app_name: str,
|
164
|
+
user_id: str
|
165
|
+
) -> ListSessionsResponse:
|
166
|
+
"""Implementation of session listing."""
|
167
|
+
pass
|
168
|
+
|
169
|
+
@abc.abstractmethod
|
170
|
+
async def _delete_session_impl(
|
171
|
+
self,
|
172
|
+
*,
|
173
|
+
app_name: str,
|
174
|
+
user_id: str,
|
175
|
+
session_id: str
|
176
|
+
) -> None:
|
177
|
+
"""Implementation of session deletion."""
|
178
|
+
pass
|
179
|
+
|
180
|
+
@abc.abstractmethod
|
181
|
+
async def _append_event_impl(self, session: Session, event: Event) -> None:
|
182
|
+
"""Implementation of event appending."""
|
183
|
+
pass
|
@@ -0,0 +1,243 @@
|
|
1
|
+
"""MongoDB-based session service implementation."""
|
2
|
+
|
3
|
+
import json
|
4
|
+
import time
|
5
|
+
import uuid
|
6
|
+
from typing import Any, Optional, Dict
|
7
|
+
|
8
|
+
try:
|
9
|
+
from pymongo import MongoClient
|
10
|
+
from pymongo.errors import PyMongoError
|
11
|
+
except ImportError:
|
12
|
+
raise ImportError(
|
13
|
+
"PyMongo is required for MongoSessionService. "
|
14
|
+
"Install it with: pip install pymongo"
|
15
|
+
)
|
16
|
+
|
17
|
+
from google.adk.sessions.session import Session
|
18
|
+
from google.adk.events.event import Event
|
19
|
+
from google.adk.sessions.base_session_service import GetSessionConfig, ListSessionsResponse
|
20
|
+
|
21
|
+
from .base_custom_session_service import BaseCustomSessionService
|
22
|
+
|
23
|
+
|
24
|
+
class MongoSessionService(BaseCustomSessionService):
|
25
|
+
"""MongoDB-based session service implementation."""
|
26
|
+
|
27
|
+
def __init__(self, connection_string: str, database_name: str = "adk_sessions"):
|
28
|
+
"""Initialize the MongoDB session service.
|
29
|
+
|
30
|
+
Args:
|
31
|
+
connection_string: MongoDB connection string
|
32
|
+
database_name: Name of the database to use
|
33
|
+
"""
|
34
|
+
super().__init__()
|
35
|
+
self.connection_string = connection_string
|
36
|
+
self.database_name = database_name
|
37
|
+
self.client: Optional[MongoClient] = None
|
38
|
+
self.db = None
|
39
|
+
self.collection = None
|
40
|
+
|
41
|
+
async def _initialize_impl(self) -> None:
|
42
|
+
"""Initialize the MongoDB connection."""
|
43
|
+
try:
|
44
|
+
self.client = MongoClient(self.connection_string)
|
45
|
+
self.db = self.client[self.database_name]
|
46
|
+
self.collection = self.db.sessions
|
47
|
+
|
48
|
+
# Create indexes for better performance
|
49
|
+
self.collection.create_index([("app_name", 1), ("user_id", 1)])
|
50
|
+
self.collection.create_index("id")
|
51
|
+
except PyMongoError as e:
|
52
|
+
raise RuntimeError(f"Failed to initialize MongoDB session service: {e}")
|
53
|
+
|
54
|
+
async def _cleanup_impl(self) -> None:
|
55
|
+
"""Clean up MongoDB connections."""
|
56
|
+
if self.client:
|
57
|
+
self.client.close()
|
58
|
+
self.client = None
|
59
|
+
self.db = None
|
60
|
+
self.collection = None
|
61
|
+
|
62
|
+
def _serialize_events(self, events: list[Event]) -> list[Dict]:
|
63
|
+
"""Serialize events to dictionaries."""
|
64
|
+
return [event.model_dump() for event in events]
|
65
|
+
|
66
|
+
def _deserialize_events(self, event_dicts: list[Dict]) -> list[Event]:
|
67
|
+
"""Deserialize events from dictionaries."""
|
68
|
+
return [Event(**event_dict) for event_dict in event_dicts]
|
69
|
+
|
70
|
+
async def _create_session_impl(
|
71
|
+
self,
|
72
|
+
*,
|
73
|
+
app_name: str,
|
74
|
+
user_id: str,
|
75
|
+
state: Optional[dict[str, Any]] = None,
|
76
|
+
session_id: Optional[str] = None,
|
77
|
+
) -> Session:
|
78
|
+
"""Implementation of session creation."""
|
79
|
+
try:
|
80
|
+
# Generate session ID if not provided
|
81
|
+
session_id = session_id or str(uuid.uuid4())
|
82
|
+
|
83
|
+
# Create session object
|
84
|
+
session = Session(
|
85
|
+
id=session_id,
|
86
|
+
app_name=app_name,
|
87
|
+
user_id=user_id,
|
88
|
+
state=state or {},
|
89
|
+
events=[],
|
90
|
+
last_update_time=time.time()
|
91
|
+
)
|
92
|
+
|
93
|
+
# Create document for MongoDB
|
94
|
+
document = {
|
95
|
+
"_id": session_id,
|
96
|
+
"id": session_id,
|
97
|
+
"app_name": app_name,
|
98
|
+
"user_id": user_id,
|
99
|
+
"state": session.state,
|
100
|
+
"events": self._serialize_events(session.events),
|
101
|
+
"last_update_time": session.last_update_time
|
102
|
+
}
|
103
|
+
|
104
|
+
# Insert into MongoDB
|
105
|
+
self.collection.insert_one(document)
|
106
|
+
|
107
|
+
return session
|
108
|
+
except PyMongoError as e:
|
109
|
+
raise RuntimeError(f"Failed to create session: {e}")
|
110
|
+
|
111
|
+
async def _get_session_impl(
|
112
|
+
self,
|
113
|
+
*,
|
114
|
+
app_name: str,
|
115
|
+
user_id: str,
|
116
|
+
session_id: str,
|
117
|
+
config: Optional[GetSessionConfig] = None,
|
118
|
+
) -> Optional[Session]:
|
119
|
+
"""Implementation of session retrieval."""
|
120
|
+
try:
|
121
|
+
# Retrieve from MongoDB
|
122
|
+
document = self.collection.find_one({
|
123
|
+
"_id": session_id,
|
124
|
+
"app_name": app_name,
|
125
|
+
"user_id": user_id
|
126
|
+
})
|
127
|
+
|
128
|
+
if not document:
|
129
|
+
return None
|
130
|
+
|
131
|
+
# Create session object
|
132
|
+
session = Session(
|
133
|
+
id=document["id"],
|
134
|
+
app_name=document["app_name"],
|
135
|
+
user_id=document["user_id"],
|
136
|
+
state=document["state"],
|
137
|
+
events=self._deserialize_events(document["events"]),
|
138
|
+
last_update_time=document["last_update_time"]
|
139
|
+
)
|
140
|
+
|
141
|
+
# Apply config filters if provided
|
142
|
+
if config:
|
143
|
+
if config.num_recent_events:
|
144
|
+
session.events = session.events[-config.num_recent_events:]
|
145
|
+
if config.after_timestamp:
|
146
|
+
filtered_events = [
|
147
|
+
event for event in session.events
|
148
|
+
if event.timestamp >= config.after_timestamp
|
149
|
+
]
|
150
|
+
session.events = filtered_events
|
151
|
+
|
152
|
+
return session
|
153
|
+
except PyMongoError as e:
|
154
|
+
raise RuntimeError(f"Failed to get session: {e}")
|
155
|
+
|
156
|
+
async def _list_sessions_impl(
|
157
|
+
self,
|
158
|
+
*,
|
159
|
+
app_name: str,
|
160
|
+
user_id: str
|
161
|
+
) -> ListSessionsResponse:
|
162
|
+
"""Implementation of session listing."""
|
163
|
+
try:
|
164
|
+
# Retrieve all sessions for user (without events for performance)
|
165
|
+
cursor = self.collection.find(
|
166
|
+
{
|
167
|
+
"app_name": app_name,
|
168
|
+
"user_id": user_id
|
169
|
+
},
|
170
|
+
{
|
171
|
+
"_id": 1,
|
172
|
+
"id": 1,
|
173
|
+
"app_name": 1,
|
174
|
+
"user_id": 1,
|
175
|
+
"state": 1,
|
176
|
+
"last_update_time": 1
|
177
|
+
}
|
178
|
+
)
|
179
|
+
|
180
|
+
# Create session objects without events
|
181
|
+
sessions = []
|
182
|
+
for document in cursor:
|
183
|
+
session = Session(
|
184
|
+
id=document["id"],
|
185
|
+
app_name=document["app_name"],
|
186
|
+
user_id=document["user_id"],
|
187
|
+
state=document["state"],
|
188
|
+
events=[], # Empty events for listing
|
189
|
+
last_update_time=document["last_update_time"]
|
190
|
+
)
|
191
|
+
sessions.append(session)
|
192
|
+
|
193
|
+
return ListSessionsResponse(sessions=sessions)
|
194
|
+
except PyMongoError as e:
|
195
|
+
raise RuntimeError(f"Failed to list sessions: {e}")
|
196
|
+
|
197
|
+
async def _delete_session_impl(
|
198
|
+
self,
|
199
|
+
*,
|
200
|
+
app_name: str,
|
201
|
+
user_id: str,
|
202
|
+
session_id: str
|
203
|
+
) -> None:
|
204
|
+
"""Implementation of session deletion."""
|
205
|
+
try:
|
206
|
+
# Delete from MongoDB
|
207
|
+
self.collection.delete_one({
|
208
|
+
"_id": session_id,
|
209
|
+
"app_name": app_name,
|
210
|
+
"user_id": user_id
|
211
|
+
})
|
212
|
+
except PyMongoError as e:
|
213
|
+
raise RuntimeError(f"Failed to delete session: {e}")
|
214
|
+
|
215
|
+
async def _append_event_impl(self, session: Session, event: Event) -> None:
|
216
|
+
"""Implementation of event appending."""
|
217
|
+
try:
|
218
|
+
# Prepare update data
|
219
|
+
update_data = {
|
220
|
+
"$set": {
|
221
|
+
"events": self._serialize_events(session.events),
|
222
|
+
"last_update_time": session.last_update_time
|
223
|
+
}
|
224
|
+
}
|
225
|
+
|
226
|
+
# Apply state changes from event if present
|
227
|
+
if event.actions and event.actions.state_delta:
|
228
|
+
update_data["$set"]["state"] = session.state
|
229
|
+
|
230
|
+
# Update session in MongoDB
|
231
|
+
result = self.collection.update_one(
|
232
|
+
{
|
233
|
+
"_id": session.id,
|
234
|
+
"app_name": session.app_name,
|
235
|
+
"user_id": session.user_id
|
236
|
+
},
|
237
|
+
update_data
|
238
|
+
)
|
239
|
+
|
240
|
+
if result.matched_count == 0:
|
241
|
+
raise ValueError(f"Session {session.id} not found")
|
242
|
+
except PyMongoError as e:
|
243
|
+
raise RuntimeError(f"Failed to append event: {e}")
|