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,174 @@
1
+ """MongoDB-based memory service implementation using PyMongo."""
2
+
3
+ import json
4
+ import logging
5
+ from typing import Optional, List
6
+ import re
7
+ from datetime import datetime
8
+
9
+ try:
10
+ from pymongo import MongoClient
11
+ from pymongo.errors import PyMongoError
12
+ from bson import ObjectId
13
+ except ImportError:
14
+ raise ImportError(
15
+ "PyMongo is required for MongoMemoryService. "
16
+ "Install it with: pip install pymongo"
17
+ )
18
+
19
+ from google.genai import types
20
+ from .base_custom_memory_service import BaseCustomMemoryService
21
+
22
+
23
+ logger = logging.getLogger('google_adk_extras.' + __name__)
24
+
25
+
26
+ class MongoMemoryService(BaseCustomMemoryService):
27
+ """MongoDB-based memory service implementation."""
28
+
29
+ def __init__(self, connection_string: str, database_name: str = "adk_memory"):
30
+ """Initialize the MongoDB memory service.
31
+
32
+ Args:
33
+ connection_string: MongoDB connection string
34
+ database_name: Name of the database to use
35
+ """
36
+ super().__init__()
37
+ self.connection_string = connection_string
38
+ self.database_name = database_name
39
+ self.client: Optional[MongoClient] = None
40
+ self.db: Optional[object] = None
41
+
42
+ async def _initialize_impl(self) -> None:
43
+ """Initialize the MongoDB connection."""
44
+ try:
45
+ self.client = MongoClient(self.connection_string)
46
+ self.db = self.client[self.database_name]
47
+
48
+ # Create indexes for efficient querying
49
+ memory_collection = self.db["memory_entries"]
50
+ memory_collection.create_index([("app_name", 1), ("user_id", 1)])
51
+ memory_collection.create_index([("search_terms", 1)])
52
+ memory_collection.create_index([("app_name", 1), ("user_id", 1), ("search_terms", 1)])
53
+ except PyMongoError as e:
54
+ raise RuntimeError(f"Failed to initialize MongoDB memory service: {e}")
55
+
56
+ async def _cleanup_impl(self) -> None:
57
+ """Clean up MongoDB connections."""
58
+ if self.client:
59
+ self.client.close()
60
+ self.client = None
61
+ self.db = None
62
+
63
+ def _serialize_content(self, content: types.Content) -> dict:
64
+ """Serialize Content object to dictionary."""
65
+ try:
66
+ return content.to_json_dict()
67
+ except (TypeError, ValueError) as e:
68
+ raise ValueError(f"Failed to serialize content: {e}")
69
+
70
+ def _deserialize_content(self, content_dict: dict) -> types.Content:
71
+ """Deserialize Content object from dictionary."""
72
+ try:
73
+ return types.Content(**content_dict)
74
+ except (TypeError, ValueError) as e:
75
+ raise ValueError(f"Failed to deserialize content: {e}")
76
+
77
+ def _extract_text_from_content(self, content: types.Content) -> str:
78
+ """Extract text content from a Content object for storage and search."""
79
+ if not content or not content.parts:
80
+ return ""
81
+
82
+ text_parts = []
83
+ for part in content.parts:
84
+ if part.text:
85
+ text_parts.append(part.text)
86
+
87
+ return " ".join(text_parts)
88
+
89
+ def _extract_search_terms(self, text: str) -> List[str]:
90
+ """Extract search terms from text content."""
91
+ # Extract words from text and convert to lowercase
92
+ words = re.findall(r'[A-Za-z]+', text.lower())
93
+ # Return unique words as a list
94
+ return sorted(set(words))
95
+
96
+ async def _add_session_to_memory_impl(self, session: "Session") -> None:
97
+ """Implementation of adding a session to memory."""
98
+ if not self.db:
99
+ raise RuntimeError("Service not initialized")
100
+
101
+ try:
102
+ memory_collection = self.db["memory_entries"]
103
+
104
+ # Add each event in the session as a separate memory entry
105
+ for event in session.events:
106
+ if not event.content or not event.content.parts:
107
+ continue
108
+
109
+ # Extract text content and search terms
110
+ text_content = self._extract_text_from_content(event.content)
111
+ search_terms = self._extract_search_terms(text_content)
112
+
113
+ # Create memory document
114
+ memory_document = {
115
+ "app_name": session.app_name,
116
+ "user_id": session.user_id,
117
+ "content": self._serialize_content(event.content),
118
+ "author": event.author,
119
+ "timestamp": datetime.fromtimestamp(event.timestamp) if event.timestamp else None,
120
+ "text_content": text_content,
121
+ "search_terms": search_terms
122
+ }
123
+
124
+ # Save to MongoDB
125
+ memory_collection.insert_one(memory_document)
126
+
127
+ except PyMongoError as e:
128
+ raise RuntimeError(f"Failed to add session to memory: {e}")
129
+
130
+ async def _search_memory_impl(
131
+ self, *, app_name: str, user_id: str, query: str
132
+ ) -> "SearchMemoryResponse":
133
+ """Implementation of searching memory."""
134
+ from google.adk.memory.base_memory_service import SearchMemoryResponse
135
+ from google.adk.memory.memory_entry import MemoryEntry
136
+
137
+ if not self.db:
138
+ raise RuntimeError("Service not initialized")
139
+
140
+ try:
141
+ memory_collection = self.db["memory_entries"]
142
+
143
+ # Extract search terms from query
144
+ query_terms = self._extract_search_terms(query)
145
+
146
+ if not query_terms:
147
+ # If no searchable terms in query, return empty response
148
+ return SearchMemoryResponse(memories=[])
149
+
150
+ # Build query - find entries that match any of the query words
151
+ # Using MongoDB's $in operator to match any of the search terms
152
+ mongo_query = {
153
+ "app_name": app_name,
154
+ "user_id": user_id,
155
+ "search_terms": {"$in": query_terms}
156
+ }
157
+
158
+ # Execute query
159
+ cursor = memory_collection.find(mongo_query)
160
+
161
+ # Convert to MemoryEntry objects
162
+ memories = []
163
+ for doc in cursor:
164
+ content = self._deserialize_content(doc["content"])
165
+ memory_entry = MemoryEntry(
166
+ content=content,
167
+ author=doc.get("author"),
168
+ timestamp=doc["timestamp"].isoformat() if doc.get("timestamp") else None
169
+ )
170
+ memories.append(memory_entry)
171
+
172
+ return SearchMemoryResponse(memories=memories)
173
+ except PyMongoError as e:
174
+ raise RuntimeError(f"Failed to search memory: {e}")
@@ -0,0 +1,188 @@
1
+ """Redis-based memory service implementation using redis-py."""
2
+
3
+ import json
4
+ import logging
5
+ from typing import Optional, List
6
+ import re
7
+ from datetime import datetime
8
+
9
+ try:
10
+ import redis
11
+ from redis.exceptions import RedisError
12
+ except ImportError:
13
+ raise ImportError(
14
+ "Redis-py is required for RedisMemoryService. "
15
+ "Install it with: pip install redis"
16
+ )
17
+
18
+ from google.genai import types
19
+ from .base_custom_memory_service import BaseCustomMemoryService
20
+
21
+
22
+ logger = logging.getLogger('google_adk_extras.' + __name__)
23
+
24
+
25
+ class RedisMemoryService(BaseCustomMemoryService):
26
+ """Redis-based memory service implementation."""
27
+
28
+ def __init__(self, host: str = "localhost", port: int = 6379, db: int = 0):
29
+ """Initialize the Redis memory service.
30
+
31
+ Args:
32
+ host: Redis server host
33
+ port: Redis server port
34
+ db: Redis database number
35
+ """
36
+ super().__init__()
37
+ self.host = host
38
+ self.port = port
39
+ self.db = db
40
+ self.client: Optional[redis.Redis] = None
41
+
42
+ async def _initialize_impl(self) -> None:
43
+ """Initialize the Redis connection."""
44
+ try:
45
+ self.client = redis.Redis(host=self.host, port=self.port, db=self.db)
46
+ # Test connection
47
+ self.client.ping()
48
+ except RedisError as e:
49
+ raise RuntimeError(f"Failed to initialize Redis memory service: {e}")
50
+
51
+ async def _cleanup_impl(self) -> None:
52
+ """Clean up Redis connections."""
53
+ if self.client:
54
+ self.client.close()
55
+ self.client = None
56
+
57
+ def _serialize_content(self, content: types.Content) -> str:
58
+ """Serialize Content object to JSON string."""
59
+ try:
60
+ return json.dumps(content.to_json_dict())
61
+ except (TypeError, ValueError) as e:
62
+ raise ValueError(f"Failed to serialize content: {e}")
63
+
64
+ def _deserialize_content(self, content_str: str) -> types.Content:
65
+ """Deserialize Content object from JSON string."""
66
+ try:
67
+ content_dict = json.loads(content_str) if content_str else {}
68
+ return types.Content(**content_dict)
69
+ except (TypeError, ValueError) as e:
70
+ raise ValueError(f"Failed to deserialize content: {e}")
71
+
72
+ def _extract_text_from_content(self, content: types.Content) -> str:
73
+ """Extract text content from a Content object for storage and search."""
74
+ if not content or not content.parts:
75
+ return ""
76
+
77
+ text_parts = []
78
+ for part in content.parts:
79
+ if part.text:
80
+ text_parts.append(part.text)
81
+
82
+ return " ".join(text_parts)
83
+
84
+ def _extract_search_terms(self, text: str) -> List[str]:
85
+ """Extract search terms from text content."""
86
+ # Extract words from text and convert to lowercase
87
+ words = re.findall(r'[A-Za-z]+', text.lower())
88
+ # Return unique words as a list
89
+ return sorted(set(words))
90
+
91
+ def _get_user_key(self, app_name: str, user_id: str) -> str:
92
+ """Generate Redis key for user's memory entries."""
93
+ return f"memory:{app_name}:{user_id}"
94
+
95
+ async def _add_session_to_memory_impl(self, session: "Session") -> None:
96
+ """Implementation of adding a session to memory."""
97
+ if not self.client:
98
+ raise RuntimeError("Service not initialized")
99
+
100
+ try:
101
+ user_key = self._get_user_key(session.app_name, session.user_id)
102
+
103
+ # Add each event in the session as a separate memory entry
104
+ for event in session.events:
105
+ if not event.content or not event.content.parts:
106
+ continue
107
+
108
+ # Extract text content and search terms
109
+ text_content = self._extract_text_from_content(event.content)
110
+ search_terms = self._extract_search_terms(text_content)
111
+
112
+ # Create memory entry
113
+ memory_entry = {
114
+ "id": f"{session.id}:{event.timestamp}",
115
+ "content": self._serialize_content(event.content),
116
+ "author": event.author,
117
+ "timestamp": event.timestamp,
118
+ "text_content": text_content,
119
+ "search_terms": search_terms
120
+ }
121
+
122
+ # Store in Redis with a score based on timestamp for ordering
123
+ score = event.timestamp if event.timestamp else 0
124
+ self.client.zadd(user_key, {json.dumps(memory_entry): score})
125
+
126
+ except RedisError as e:
127
+ raise RuntimeError(f"Failed to add session to memory: {e}")
128
+ except (TypeError, ValueError) as e:
129
+ raise RuntimeError(f"Failed to serialize memory entry: {e}")
130
+
131
+ async def _search_memory_impl(
132
+ self, *, app_name: str, user_id: str, query: str
133
+ ) -> "SearchMemoryResponse":
134
+ """Implementation of searching memory."""
135
+ from google.adk.memory.base_memory_service import SearchMemoryResponse
136
+ from google.adk.memory.memory_entry import MemoryEntry
137
+
138
+ if not self.client:
139
+ raise RuntimeError("Service not initialized")
140
+
141
+ try:
142
+ user_key = self._get_user_key(app_name, user_id)
143
+
144
+ # Extract search terms from query
145
+ query_terms = self._extract_search_terms(query)
146
+
147
+ if not query_terms:
148
+ # If no searchable terms in query, return empty response
149
+ return SearchMemoryResponse(memories=[])
150
+
151
+ # Get all memory entries for the user
152
+ memory_entries = self.client.zrange(user_key, 0, -1, withscores=True)
153
+
154
+ # Filter entries that match any of the query terms
155
+ matching_memories = []
156
+ for entry_data, _ in memory_entries:
157
+ try:
158
+ entry = json.loads(entry_data)
159
+
160
+ # Check if any query term matches the search terms in this entry
161
+ entry_search_terms = entry.get("search_terms", [])
162
+ if any(term in entry_search_terms for term in query_terms):
163
+ matching_memories.append(entry)
164
+ except (TypeError, ValueError):
165
+ # Skip invalid entries
166
+ continue
167
+
168
+ # Convert to MemoryEntry objects
169
+ memories = []
170
+ for entry in matching_memories:
171
+ content = self._deserialize_content(entry["content"])
172
+ # Format timestamp as ISO string
173
+ timestamp_str = None
174
+ if entry.get("timestamp"):
175
+ timestamp_str = datetime.fromtimestamp(entry["timestamp"]).isoformat()
176
+
177
+ memory_entry = MemoryEntry(
178
+ content=content,
179
+ author=entry.get("author"),
180
+ timestamp=timestamp_str
181
+ )
182
+ memories.append(memory_entry)
183
+
184
+ return SearchMemoryResponse(memories=memories)
185
+ except RedisError as e:
186
+ raise RuntimeError(f"Failed to search memory: {e}")
187
+ except (TypeError, ValueError) as e:
188
+ raise RuntimeError(f"Failed to deserialize memory entry: {e}")
@@ -0,0 +1,213 @@
1
+ """SQL-based memory service implementation using SQLAlchemy."""
2
+
3
+ import json
4
+ import logging
5
+ from typing import Optional, List
6
+ import re
7
+ from datetime import datetime, timezone
8
+
9
+ try:
10
+ from sqlalchemy import create_engine, Column, String, Text, DateTime, Integer, Index
11
+ from sqlalchemy.orm import declarative_base, sessionmaker
12
+ from sqlalchemy.exc import SQLAlchemyError
13
+ except ImportError:
14
+ raise ImportError(
15
+ "SQLAlchemy is required for SQLMemoryService. "
16
+ "Install it with: pip install sqlalchemy"
17
+ )
18
+
19
+ from google.genai import types
20
+ from .base_custom_memory_service import BaseCustomMemoryService
21
+
22
+
23
+ logger = logging.getLogger('google_adk_extras.' + __name__)
24
+
25
+ # Use the modern declarative_base import
26
+ Base = declarative_base()
27
+
28
+
29
+ class SQLMemoryModel(Base):
30
+ """SQLAlchemy model for storing memory entries."""
31
+ __tablename__ = 'adk_memory_entries'
32
+
33
+ # Auto-incrementing ID
34
+ id = Column(Integer, primary_key=True, autoincrement=True)
35
+
36
+ # Identifiers
37
+ app_name = Column(String, nullable=False, index=True)
38
+ user_id = Column(String, nullable=False, index=True)
39
+
40
+ # Content data
41
+ content_json = Column(Text, nullable=False) # JSON string of Content object
42
+ author = Column(String, nullable=True)
43
+ timestamp = Column(DateTime(timezone=True), nullable=True)
44
+
45
+ # Search optimization
46
+ text_content = Column(Text, nullable=False) # Extracted text for searching
47
+ search_terms = Column(Text, nullable=False) # Space-separated search terms
48
+
49
+ # Composite index for efficient querying
50
+ __table_args__ = (
51
+ Index('idx_app_user_query', 'app_name', 'user_id', 'search_terms'),
52
+ )
53
+
54
+
55
+ class SQLMemoryService(BaseCustomMemoryService):
56
+ """SQL-based memory service implementation."""
57
+
58
+ def __init__(self, database_url: str):
59
+ """Initialize the SQL memory service.
60
+
61
+ Args:
62
+ database_url: Database connection string (e.g., 'sqlite:///memory.db')
63
+ """
64
+ super().__init__()
65
+ self.database_url = database_url
66
+ self.engine: Optional[object] = None
67
+ self.session_local: Optional[object] = None
68
+
69
+ async def _initialize_impl(self) -> None:
70
+ """Initialize the database connection and create tables."""
71
+ try:
72
+ self.engine = create_engine(self.database_url)
73
+ Base.metadata.create_all(self.engine)
74
+ self.session_local = sessionmaker(
75
+ autocommit=False,
76
+ autoflush=False,
77
+ bind=self.engine
78
+ )
79
+ except SQLAlchemyError as e:
80
+ raise RuntimeError(f"Failed to initialize SQL memory service: {e}")
81
+
82
+ async def _cleanup_impl(self) -> None:
83
+ """Clean up database connections."""
84
+ if self.engine:
85
+ self.engine.dispose()
86
+ self.engine = None
87
+ self.session_local = None
88
+
89
+ def _get_db_session(self):
90
+ """Get a database session."""
91
+ if not self.session_local:
92
+ raise RuntimeError("Service not initialized")
93
+ return self.session_local()
94
+
95
+ def _serialize_content(self, content: types.Content) -> str:
96
+ """Serialize Content object to JSON string."""
97
+ try:
98
+ return json.dumps(content.to_json_dict())
99
+ except (TypeError, ValueError) as e:
100
+ raise ValueError(f"Failed to serialize content: {e}")
101
+
102
+ def _deserialize_content(self, content_str: str) -> types.Content:
103
+ """Deserialize Content object from JSON string."""
104
+ try:
105
+ content_dict = json.loads(content_str) if content_str else {}
106
+ return types.Content(**content_dict)
107
+ except (TypeError, ValueError) as e:
108
+ raise ValueError(f"Failed to deserialize content: {e}")
109
+
110
+ def _extract_text_from_content(self, content: types.Content) -> str:
111
+ """Extract text content from a Content object for storage and search."""
112
+ if not content or not content.parts:
113
+ return ""
114
+
115
+ text_parts = []
116
+ for part in content.parts:
117
+ if part.text:
118
+ text_parts.append(part.text)
119
+
120
+ return " ".join(text_parts)
121
+
122
+ def _extract_search_terms(self, text: str) -> str:
123
+ """Extract search terms from text content."""
124
+ # Extract words from text and convert to lowercase
125
+ words = re.findall(r'[A-Za-z]+', text.lower())
126
+ # Return space-separated unique words
127
+ return " ".join(sorted(set(words)))
128
+
129
+ async def _add_session_to_memory_impl(self, session: "Session") -> None:
130
+ """Implementation of adding a session to memory."""
131
+ db_session = self._get_db_session()
132
+ try:
133
+ # Add each event in the session as a separate memory entry
134
+ for event in session.events:
135
+ if not event.content or not event.content.parts:
136
+ continue
137
+
138
+ # Extract text content and search terms
139
+ text_content = self._extract_text_from_content(event.content)
140
+ search_terms = self._extract_search_terms(text_content)
141
+
142
+ # Create memory model
143
+ db_memory = SQLMemoryModel(
144
+ app_name=session.app_name,
145
+ user_id=session.user_id,
146
+ content_json=self._serialize_content(event.content),
147
+ author=event.author,
148
+ timestamp=datetime.fromtimestamp(event.timestamp, tz=timezone.utc) if event.timestamp else None,
149
+ text_content=text_content,
150
+ search_terms=search_terms
151
+ )
152
+
153
+ # Save to database
154
+ db_session.add(db_memory)
155
+
156
+ db_session.commit()
157
+ except SQLAlchemyError as e:
158
+ db_session.rollback()
159
+ raise RuntimeError(f"Failed to add session to memory: {e}")
160
+ finally:
161
+ db_session.close()
162
+
163
+ async def _search_memory_impl(
164
+ self, *, app_name: str, user_id: str, query: str
165
+ ) -> "SearchMemoryResponse":
166
+ """Implementation of searching memory."""
167
+ from google.adk.memory.base_memory_service import SearchMemoryResponse
168
+ from google.adk.memory.memory_entry import MemoryEntry
169
+
170
+ db_session = self._get_db_session()
171
+ try:
172
+ # Extract search terms from query
173
+ query_terms = self._extract_search_terms(query)
174
+ query_words = query_terms.split() if query_terms else []
175
+
176
+ if not query_words:
177
+ # If no searchable terms in query, return empty response
178
+ return SearchMemoryResponse(memories=[])
179
+
180
+ # Build query - find entries that match any of the query words
181
+ db_query = db_session.query(SQLMemoryModel).filter(
182
+ SQLMemoryModel.app_name == app_name,
183
+ SQLMemoryModel.user_id == user_id
184
+ )
185
+
186
+ # Add search term filters - use OR logic to match any query word
187
+ from sqlalchemy import or_
188
+ conditions = []
189
+ for word in query_words:
190
+ conditions.append(SQLMemoryModel.search_terms.contains(word))
191
+
192
+ if conditions:
193
+ db_query = db_query.filter(or_(*conditions))
194
+
195
+ # Execute query
196
+ db_memories = db_query.all()
197
+
198
+ # Convert to MemoryEntry objects
199
+ memories = []
200
+ for db_memory in db_memories:
201
+ content = self._deserialize_content(db_memory.content_json)
202
+ memory_entry = MemoryEntry(
203
+ content=content,
204
+ author=db_memory.author,
205
+ timestamp=db_memory.timestamp.isoformat() if db_memory.timestamp else None
206
+ )
207
+ memories.append(memory_entry)
208
+
209
+ return SearchMemoryResponse(memories=memories)
210
+ except SQLAlchemyError as e:
211
+ raise RuntimeError(f"Failed to search memory: {e}")
212
+ finally:
213
+ db_session.close()