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.
@@ -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}")