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,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()
|