adk-session-services 0.1.0__tar.gz
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.
- adk_session_services-0.1.0/LICENSE +21 -0
- adk_session_services-0.1.0/PKG-INFO +108 -0
- adk_session_services-0.1.0/README.md +65 -0
- adk_session_services-0.1.0/pyproject.toml +77 -0
- adk_session_services-0.1.0/setup.cfg +4 -0
- adk_session_services-0.1.0/src/__init__.py +0 -0
- adk_session_services-0.1.0/src/adk_session_services.egg-info/PKG-INFO +108 -0
- adk_session_services-0.1.0/src/adk_session_services.egg-info/SOURCES.txt +12 -0
- adk_session_services-0.1.0/src/adk_session_services.egg-info/dependency_links.txt +1 -0
- adk_session_services-0.1.0/src/adk_session_services.egg-info/requires.txt +23 -0
- adk_session_services-0.1.0/src/adk_session_services.egg-info/top_level.txt +3 -0
- adk_session_services-0.1.0/src/firestore_session/__init__.py +0 -0
- adk_session_services-0.1.0/src/redis_session/__init__.py +0 -0
- adk_session_services-0.1.0/src/redis_session/redis_session_service.py +223 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 TiyeeJiang
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: adk-session-services
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Python package providing additional service implementations for the Google ADK framework (Redis, etc)
|
|
5
|
+
Author-email: tiyee <tiyee@live.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/tiyee/adk-session-service
|
|
8
|
+
Project-URL: Source, https://github.com/tiyee/adk-session-service
|
|
9
|
+
Keywords: adk,google-adk,session-service,redis
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: google-adk
|
|
23
|
+
Requires-Dist: google-generativeai>=0.8.5
|
|
24
|
+
Requires-Dist: redis>=5.0.0
|
|
25
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
28
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
29
|
+
Requires-Dist: black; extra == "dev"
|
|
30
|
+
Requires-Dist: flake8; extra == "dev"
|
|
31
|
+
Requires-Dist: mypy; extra == "dev"
|
|
32
|
+
Requires-Dist: isort; extra == "dev"
|
|
33
|
+
Provides-Extra: test
|
|
34
|
+
Requires-Dist: pytest>=8.0.0; extra == "test"
|
|
35
|
+
Requires-Dist: pytest-asyncio>=0.25.0; extra == "test"
|
|
36
|
+
Requires-Dist: pytest-mock>=3.14.0; extra == "test"
|
|
37
|
+
Requires-Dist: testcontainers>=3.10.0; extra == "test"
|
|
38
|
+
Provides-Extra: docs
|
|
39
|
+
Requires-Dist: sphinx>=7.2.0; extra == "docs"
|
|
40
|
+
Requires-Dist: furo>=2023.9.10; extra == "docs"
|
|
41
|
+
Requires-Dist: myst-parser>=2.0.0; extra == "docs"
|
|
42
|
+
Dynamic: license-file
|
|
43
|
+
|
|
44
|
+
# adk-session-services
|
|
45
|
+
|
|
46
|
+
Session service implementations for [Google's Agent Development Kit (ADK)](https://github.com/google/adk-python). Provides persistent session storage backends as drop-in replacements for ADK's `BaseSessionService`.
|
|
47
|
+
|
|
48
|
+
## Features
|
|
49
|
+
|
|
50
|
+
- **Redis** — Persistent session storage via Redis with support for app-level and user-level state layering.
|
|
51
|
+
- **Firestore** — (Planned) Persistent session storage via Google Cloud Firestore.
|
|
52
|
+
|
|
53
|
+
## Installation
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install adk-session-services
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Quick Start
|
|
60
|
+
|
|
61
|
+
### Redis
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from redis_session.redis_session_service import RedisSessionService
|
|
65
|
+
|
|
66
|
+
service = RedisSessionService("redis://localhost:6379")
|
|
67
|
+
|
|
68
|
+
session = await service.create_session(
|
|
69
|
+
app_name="my-app",
|
|
70
|
+
user_id="user-123",
|
|
71
|
+
)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Redis Key Schema
|
|
75
|
+
|
|
76
|
+
All keys are prefixed with `adk:sessions:`:
|
|
77
|
+
|
|
78
|
+
| Key pattern | Type | Purpose |
|
|
79
|
+
|---|---|---|
|
|
80
|
+
| `{app}:{user}:{session}:meta` | Hash | Session metadata |
|
|
81
|
+
| `{app}:{user}:{session}:state` | String (JSON) | Session state dict |
|
|
82
|
+
| `{app}:{user}:{session}:events` | List (JSON) | Ordered event log |
|
|
83
|
+
| `{app}:{user}:sessions` | Set | Session IDs per user/app |
|
|
84
|
+
| `{app}:app_state` | Hash | App-level state (shared across users) |
|
|
85
|
+
| `{app}:{user}:user_state` | Hash | User-level state (shared across sessions) |
|
|
86
|
+
|
|
87
|
+
## Development
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
# Install with dev dependencies
|
|
91
|
+
pip install -e ".[dev]"
|
|
92
|
+
|
|
93
|
+
# Install with test dependencies
|
|
94
|
+
pip install -e ".[test]"
|
|
95
|
+
|
|
96
|
+
# Run tests
|
|
97
|
+
pytest
|
|
98
|
+
|
|
99
|
+
# Format
|
|
100
|
+
black src/ && isort src/
|
|
101
|
+
|
|
102
|
+
# Type check
|
|
103
|
+
mypy src/
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## License
|
|
107
|
+
|
|
108
|
+
MIT
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# adk-session-services
|
|
2
|
+
|
|
3
|
+
Session service implementations for [Google's Agent Development Kit (ADK)](https://github.com/google/adk-python). Provides persistent session storage backends as drop-in replacements for ADK's `BaseSessionService`.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Redis** — Persistent session storage via Redis with support for app-level and user-level state layering.
|
|
8
|
+
- **Firestore** — (Planned) Persistent session storage via Google Cloud Firestore.
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install adk-session-services
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Quick Start
|
|
17
|
+
|
|
18
|
+
### Redis
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from redis_session.redis_session_service import RedisSessionService
|
|
22
|
+
|
|
23
|
+
service = RedisSessionService("redis://localhost:6379")
|
|
24
|
+
|
|
25
|
+
session = await service.create_session(
|
|
26
|
+
app_name="my-app",
|
|
27
|
+
user_id="user-123",
|
|
28
|
+
)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Redis Key Schema
|
|
32
|
+
|
|
33
|
+
All keys are prefixed with `adk:sessions:`:
|
|
34
|
+
|
|
35
|
+
| Key pattern | Type | Purpose |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| `{app}:{user}:{session}:meta` | Hash | Session metadata |
|
|
38
|
+
| `{app}:{user}:{session}:state` | String (JSON) | Session state dict |
|
|
39
|
+
| `{app}:{user}:{session}:events` | List (JSON) | Ordered event log |
|
|
40
|
+
| `{app}:{user}:sessions` | Set | Session IDs per user/app |
|
|
41
|
+
| `{app}:app_state` | Hash | App-level state (shared across users) |
|
|
42
|
+
| `{app}:{user}:user_state` | Hash | User-level state (shared across sessions) |
|
|
43
|
+
|
|
44
|
+
## Development
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# Install with dev dependencies
|
|
48
|
+
pip install -e ".[dev]"
|
|
49
|
+
|
|
50
|
+
# Install with test dependencies
|
|
51
|
+
pip install -e ".[test]"
|
|
52
|
+
|
|
53
|
+
# Run tests
|
|
54
|
+
pytest
|
|
55
|
+
|
|
56
|
+
# Format
|
|
57
|
+
black src/ && isort src/
|
|
58
|
+
|
|
59
|
+
# Type check
|
|
60
|
+
mypy src/
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## License
|
|
64
|
+
|
|
65
|
+
MIT
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "adk-session-services"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A Python package providing additional service implementations for the Google ADK framework (Redis, etc)"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
authors = [{name = "tiyee", email = "tiyee@live.com"}]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Programming Language :: Python :: 3.10",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
"Programming Language :: Python :: 3.13",
|
|
21
|
+
"Operating System :: OS Independent",
|
|
22
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
23
|
+
]
|
|
24
|
+
keywords = ["adk", "google-adk", "session-service", "redis"]
|
|
25
|
+
dependencies = [
|
|
26
|
+
"google-adk",
|
|
27
|
+
"google-generativeai>=0.8.5",
|
|
28
|
+
"redis>=5.0.0",
|
|
29
|
+
"python-dotenv>=1.0.0",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
dev = [
|
|
34
|
+
"pytest>=7.0.0",
|
|
35
|
+
"pytest-asyncio>=0.21.0",
|
|
36
|
+
"black",
|
|
37
|
+
"flake8",
|
|
38
|
+
"mypy",
|
|
39
|
+
"isort",
|
|
40
|
+
]
|
|
41
|
+
test = [
|
|
42
|
+
"pytest>=8.0.0",
|
|
43
|
+
"pytest-asyncio>=0.25.0",
|
|
44
|
+
"pytest-mock>=3.14.0",
|
|
45
|
+
"testcontainers>=3.10.0",
|
|
46
|
+
]
|
|
47
|
+
docs = [
|
|
48
|
+
"sphinx>=7.2.0",
|
|
49
|
+
"furo>=2023.9.10",
|
|
50
|
+
"myst-parser>=2.0.0",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
[project.urls]
|
|
54
|
+
"Homepage" = "https://github.com/tiyee/adk-session-service"
|
|
55
|
+
"Source" = "https://github.com/tiyee/adk-session-service"
|
|
56
|
+
|
|
57
|
+
[tool.setuptools]
|
|
58
|
+
package-dir = {"" = "src"}
|
|
59
|
+
|
|
60
|
+
[tool.black]
|
|
61
|
+
line-length = 88
|
|
62
|
+
target-version = ['py310']
|
|
63
|
+
include = '\.pyi?$'
|
|
64
|
+
|
|
65
|
+
[tool.isort]
|
|
66
|
+
profile = "black"
|
|
67
|
+
|
|
68
|
+
[tool.mypy]
|
|
69
|
+
python_version = "3.10"
|
|
70
|
+
warn_return_any = true
|
|
71
|
+
warn_unused_configs = true
|
|
72
|
+
disallow_untyped_defs = true
|
|
73
|
+
disallow_incomplete_defs = true
|
|
74
|
+
|
|
75
|
+
[tool.pytest.ini_options]
|
|
76
|
+
testpaths = ["tests"]
|
|
77
|
+
asyncio_mode = "auto"
|
|
File without changes
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: adk-session-services
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Python package providing additional service implementations for the Google ADK framework (Redis, etc)
|
|
5
|
+
Author-email: tiyee <tiyee@live.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/tiyee/adk-session-service
|
|
8
|
+
Project-URL: Source, https://github.com/tiyee/adk-session-service
|
|
9
|
+
Keywords: adk,google-adk,session-service,redis
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: google-adk
|
|
23
|
+
Requires-Dist: google-generativeai>=0.8.5
|
|
24
|
+
Requires-Dist: redis>=5.0.0
|
|
25
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
28
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
29
|
+
Requires-Dist: black; extra == "dev"
|
|
30
|
+
Requires-Dist: flake8; extra == "dev"
|
|
31
|
+
Requires-Dist: mypy; extra == "dev"
|
|
32
|
+
Requires-Dist: isort; extra == "dev"
|
|
33
|
+
Provides-Extra: test
|
|
34
|
+
Requires-Dist: pytest>=8.0.0; extra == "test"
|
|
35
|
+
Requires-Dist: pytest-asyncio>=0.25.0; extra == "test"
|
|
36
|
+
Requires-Dist: pytest-mock>=3.14.0; extra == "test"
|
|
37
|
+
Requires-Dist: testcontainers>=3.10.0; extra == "test"
|
|
38
|
+
Provides-Extra: docs
|
|
39
|
+
Requires-Dist: sphinx>=7.2.0; extra == "docs"
|
|
40
|
+
Requires-Dist: furo>=2023.9.10; extra == "docs"
|
|
41
|
+
Requires-Dist: myst-parser>=2.0.0; extra == "docs"
|
|
42
|
+
Dynamic: license-file
|
|
43
|
+
|
|
44
|
+
# adk-session-services
|
|
45
|
+
|
|
46
|
+
Session service implementations for [Google's Agent Development Kit (ADK)](https://github.com/google/adk-python). Provides persistent session storage backends as drop-in replacements for ADK's `BaseSessionService`.
|
|
47
|
+
|
|
48
|
+
## Features
|
|
49
|
+
|
|
50
|
+
- **Redis** — Persistent session storage via Redis with support for app-level and user-level state layering.
|
|
51
|
+
- **Firestore** — (Planned) Persistent session storage via Google Cloud Firestore.
|
|
52
|
+
|
|
53
|
+
## Installation
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install adk-session-services
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Quick Start
|
|
60
|
+
|
|
61
|
+
### Redis
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from redis_session.redis_session_service import RedisSessionService
|
|
65
|
+
|
|
66
|
+
service = RedisSessionService("redis://localhost:6379")
|
|
67
|
+
|
|
68
|
+
session = await service.create_session(
|
|
69
|
+
app_name="my-app",
|
|
70
|
+
user_id="user-123",
|
|
71
|
+
)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Redis Key Schema
|
|
75
|
+
|
|
76
|
+
All keys are prefixed with `adk:sessions:`:
|
|
77
|
+
|
|
78
|
+
| Key pattern | Type | Purpose |
|
|
79
|
+
|---|---|---|
|
|
80
|
+
| `{app}:{user}:{session}:meta` | Hash | Session metadata |
|
|
81
|
+
| `{app}:{user}:{session}:state` | String (JSON) | Session state dict |
|
|
82
|
+
| `{app}:{user}:{session}:events` | List (JSON) | Ordered event log |
|
|
83
|
+
| `{app}:{user}:sessions` | Set | Session IDs per user/app |
|
|
84
|
+
| `{app}:app_state` | Hash | App-level state (shared across users) |
|
|
85
|
+
| `{app}:{user}:user_state` | Hash | User-level state (shared across sessions) |
|
|
86
|
+
|
|
87
|
+
## Development
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
# Install with dev dependencies
|
|
91
|
+
pip install -e ".[dev]"
|
|
92
|
+
|
|
93
|
+
# Install with test dependencies
|
|
94
|
+
pip install -e ".[test]"
|
|
95
|
+
|
|
96
|
+
# Run tests
|
|
97
|
+
pytest
|
|
98
|
+
|
|
99
|
+
# Format
|
|
100
|
+
black src/ && isort src/
|
|
101
|
+
|
|
102
|
+
# Type check
|
|
103
|
+
mypy src/
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## License
|
|
107
|
+
|
|
108
|
+
MIT
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/__init__.py
|
|
5
|
+
src/adk_session_services.egg-info/PKG-INFO
|
|
6
|
+
src/adk_session_services.egg-info/SOURCES.txt
|
|
7
|
+
src/adk_session_services.egg-info/dependency_links.txt
|
|
8
|
+
src/adk_session_services.egg-info/requires.txt
|
|
9
|
+
src/adk_session_services.egg-info/top_level.txt
|
|
10
|
+
src/firestore_session/__init__.py
|
|
11
|
+
src/redis_session/__init__.py
|
|
12
|
+
src/redis_session/redis_session_service.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
google-adk
|
|
2
|
+
google-generativeai>=0.8.5
|
|
3
|
+
redis>=5.0.0
|
|
4
|
+
python-dotenv>=1.0.0
|
|
5
|
+
|
|
6
|
+
[dev]
|
|
7
|
+
pytest>=7.0.0
|
|
8
|
+
pytest-asyncio>=0.21.0
|
|
9
|
+
black
|
|
10
|
+
flake8
|
|
11
|
+
mypy
|
|
12
|
+
isort
|
|
13
|
+
|
|
14
|
+
[docs]
|
|
15
|
+
sphinx>=7.2.0
|
|
16
|
+
furo>=2023.9.10
|
|
17
|
+
myst-parser>=2.0.0
|
|
18
|
+
|
|
19
|
+
[test]
|
|
20
|
+
pytest>=8.0.0
|
|
21
|
+
pytest-asyncio>=0.25.0
|
|
22
|
+
pytest-mock>=3.14.0
|
|
23
|
+
testcontainers>=3.10.0
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""Redis session service implementation for Google ADK."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import time
|
|
6
|
+
import uuid
|
|
7
|
+
from typing import Any, Optional
|
|
8
|
+
|
|
9
|
+
import redis.asyncio as aioredis
|
|
10
|
+
from google.adk.events.event import Event
|
|
11
|
+
from google.adk.sessions.base_session_service import (
|
|
12
|
+
BaseSessionService,
|
|
13
|
+
GetSessionConfig,
|
|
14
|
+
ListSessionsResponse,
|
|
15
|
+
)
|
|
16
|
+
from google.adk.sessions.session import Session
|
|
17
|
+
from google.adk.sessions.state import State
|
|
18
|
+
|
|
19
|
+
DEFAULT_PREFIX = 'adk'
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _meta_key(prefix:str,app: str, user: str, session: str) -> str:
|
|
23
|
+
return f"{prefix}:sessions:{app}:{user}:{session}:meta"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _state_key(app: str, user: str, session: str) -> str:
|
|
27
|
+
return f"adk:sessions:{app}:{user}:{session}:state"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _events_key(app: str, user: str, session: str) -> str:
|
|
31
|
+
return f"adk:sessions:{app}:{user}:{session}:events"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _user_set_key(app: str, user: str) -> str:
|
|
35
|
+
return f"adk:sessions:{app}:{user}:sessions"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _app_state_key(app: str) -> str:
|
|
39
|
+
return f"adk:sessions:{app}:app_state"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _user_state_key(app: str, user: str) -> str:
|
|
43
|
+
return f"adk:sessions:{app}:{user}:user_state"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class RedisSessionService(BaseSessionService):
|
|
47
|
+
|
|
48
|
+
def __init__(self, redis_url: str, prefix: str = DEFAULT_PREFIX, **kwargs: Any):
|
|
49
|
+
self.logger = logging.getLogger(__name__)
|
|
50
|
+
self.client = aioredis.from_url(redis_url, **kwargs)
|
|
51
|
+
self.prefix = prefix
|
|
52
|
+
|
|
53
|
+
async def create_session(
|
|
54
|
+
self,
|
|
55
|
+
*,
|
|
56
|
+
app_name: str,
|
|
57
|
+
user_id: str,
|
|
58
|
+
state: Optional[dict[str, Any]] = None,
|
|
59
|
+
session_id: Optional[str] = None,
|
|
60
|
+
) -> Session:
|
|
61
|
+
sid = session_id or uuid.uuid4().hex
|
|
62
|
+
now = time.time()
|
|
63
|
+
await self.client.hset(
|
|
64
|
+
_meta_key(app_name, user_id, sid),
|
|
65
|
+
mapping={"id": sid, "last_update_time": now},
|
|
66
|
+
)
|
|
67
|
+
await self.client.set(
|
|
68
|
+
_state_key(app_name, user_id, sid), json.dumps(state or {})
|
|
69
|
+
)
|
|
70
|
+
await self.client.delete(_events_key(app_name, user_id, sid))
|
|
71
|
+
await self.client.sadd(_user_set_key(app_name, user_id), sid)
|
|
72
|
+
|
|
73
|
+
# Create a session and merge state
|
|
74
|
+
session = Session(
|
|
75
|
+
id=sid,
|
|
76
|
+
app_name=app_name,
|
|
77
|
+
user_id=user_id,
|
|
78
|
+
state=state or {},
|
|
79
|
+
events=[],
|
|
80
|
+
last_update_time=now,
|
|
81
|
+
)
|
|
82
|
+
return await self._merge_state(app_name, user_id, session)
|
|
83
|
+
|
|
84
|
+
async def get_session(
|
|
85
|
+
self,
|
|
86
|
+
*,
|
|
87
|
+
app_name: str,
|
|
88
|
+
user_id: str,
|
|
89
|
+
session_id: str,
|
|
90
|
+
config: Optional[GetSessionConfig] = None,
|
|
91
|
+
) -> Optional[Session]:
|
|
92
|
+
key = _meta_key(app_name, user_id, session_id)
|
|
93
|
+
if not await self.client.exists(key):
|
|
94
|
+
return None
|
|
95
|
+
meta = await self.client.hgetall(key)
|
|
96
|
+
last = float(meta.get(b"last_update_time", b"0"))
|
|
97
|
+
state = json.loads(
|
|
98
|
+
(await self.client.get(_state_key(app_name, user_id, session_id))) or b"{}"
|
|
99
|
+
)
|
|
100
|
+
raw = await self.client.lrange(
|
|
101
|
+
_events_key(app_name, user_id, session_id), 0, -1
|
|
102
|
+
)
|
|
103
|
+
events = [Event.model_validate_json(e.decode()) for e in raw]
|
|
104
|
+
|
|
105
|
+
# Apply config filters correctly
|
|
106
|
+
if config:
|
|
107
|
+
if config.after_timestamp is not None:
|
|
108
|
+
# Use >= instead of > to match the expected behavior in tests
|
|
109
|
+
events = [e for e in events if e.timestamp >= config.after_timestamp]
|
|
110
|
+
if config.num_recent_events is not None:
|
|
111
|
+
events = events[-config.num_recent_events:]
|
|
112
|
+
|
|
113
|
+
session = Session(
|
|
114
|
+
id=session_id,
|
|
115
|
+
app_name=app_name,
|
|
116
|
+
user_id=user_id,
|
|
117
|
+
state=state,
|
|
118
|
+
events=events,
|
|
119
|
+
last_update_time=last,
|
|
120
|
+
)
|
|
121
|
+
return await self._merge_state(app_name, user_id, session)
|
|
122
|
+
|
|
123
|
+
async def list_sessions(
|
|
124
|
+
self, *, app_name: str, user_id: str
|
|
125
|
+
) -> ListSessionsResponse:
|
|
126
|
+
ids = await self.client.smembers(_user_set_key(app_name, user_id))
|
|
127
|
+
sessions: list[Session] = []
|
|
128
|
+
# Sort the session IDs to ensure consistent ordering for tests
|
|
129
|
+
sorted_ids = sorted([sid.decode() for sid in ids])
|
|
130
|
+
for sid_str in sorted_ids:
|
|
131
|
+
last_b = (
|
|
132
|
+
await self.client.hget(
|
|
133
|
+
_meta_key(app_name, user_id, sid_str), "last_update_time"
|
|
134
|
+
)
|
|
135
|
+
or b"0"
|
|
136
|
+
)
|
|
137
|
+
last = float(last_b)
|
|
138
|
+
sessions.append(
|
|
139
|
+
Session(
|
|
140
|
+
id=sid_str,
|
|
141
|
+
app_name=app_name,
|
|
142
|
+
user_id=user_id,
|
|
143
|
+
state={},
|
|
144
|
+
events=[],
|
|
145
|
+
last_update_time=last,
|
|
146
|
+
)
|
|
147
|
+
)
|
|
148
|
+
return ListSessionsResponse(sessions=sessions)
|
|
149
|
+
|
|
150
|
+
async def delete_session(
|
|
151
|
+
self, *, app_name: str, user_id: str, session_id: str
|
|
152
|
+
) -> None:
|
|
153
|
+
keys = [
|
|
154
|
+
_meta_key(app_name, user_id, session_id),
|
|
155
|
+
_state_key(app_name, user_id, session_id),
|
|
156
|
+
_events_key(app_name, user_id, session_id),
|
|
157
|
+
]
|
|
158
|
+
await self.client.delete(*keys)
|
|
159
|
+
await self.client.srem(_user_set_key(app_name, user_id), session_id)
|
|
160
|
+
|
|
161
|
+
async def append_event(self, session: Session, event: Event) -> Event:
|
|
162
|
+
if event.partial:
|
|
163
|
+
return event
|
|
164
|
+
mkey = _meta_key(self.prefix,session.app_name, session.user_id, session.id)
|
|
165
|
+
stored = await self.client.hget(mkey, "last_update_time") or b"0"
|
|
166
|
+
if float(stored) > session.last_update_time:
|
|
167
|
+
raise ValueError("stale session")
|
|
168
|
+
|
|
169
|
+
# Process the event using the parent class implementation
|
|
170
|
+
new_event = await super().append_event(session=session, event=event)
|
|
171
|
+
|
|
172
|
+
# Update user and app state if there's a state delta
|
|
173
|
+
if event.actions and event.actions.state_delta:
|
|
174
|
+
for key, value in event.actions.state_delta.items():
|
|
175
|
+
if key.startswith(State.APP_PREFIX):
|
|
176
|
+
app_key = key.removeprefix(State.APP_PREFIX)
|
|
177
|
+
await self.client.hset(
|
|
178
|
+
_app_state_key(session.app_name), app_key, json.dumps(value)
|
|
179
|
+
)
|
|
180
|
+
elif key.startswith(State.USER_PREFIX):
|
|
181
|
+
user_key = key.removeprefix(State.USER_PREFIX)
|
|
182
|
+
await self.client.hset(
|
|
183
|
+
_user_state_key(session.app_name, session.user_id),
|
|
184
|
+
user_key,
|
|
185
|
+
json.dumps(value),
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# Save the event and update session state
|
|
189
|
+
await self.client.rpush(
|
|
190
|
+
_events_key(session.app_name, session.user_id, session.id),
|
|
191
|
+
new_event.model_dump_json(),
|
|
192
|
+
)
|
|
193
|
+
await self.client.set(
|
|
194
|
+
_state_key(session.app_name, session.user_id, session.id),
|
|
195
|
+
json.dumps(session.state),
|
|
196
|
+
)
|
|
197
|
+
await self.client.hset(mkey, "last_update_time", session.last_update_time)
|
|
198
|
+
|
|
199
|
+
return new_event
|
|
200
|
+
|
|
201
|
+
async def _merge_state(
|
|
202
|
+
self, app_name: str, user_id: str, session: Session
|
|
203
|
+
) -> Session:
|
|
204
|
+
"""Merge app and user state into the session state."""
|
|
205
|
+
# Merge app state
|
|
206
|
+
app_state = await self.client.hgetall(_app_state_key(app_name))
|
|
207
|
+
for key, value_json in app_state.items():
|
|
208
|
+
key_str = key.decode() if isinstance(key, bytes) else key
|
|
209
|
+
value = json.loads(
|
|
210
|
+
value_json.decode() if isinstance(value_json, bytes) else value_json
|
|
211
|
+
)
|
|
212
|
+
session.state[State.APP_PREFIX + key_str] = value
|
|
213
|
+
|
|
214
|
+
# Merge user state
|
|
215
|
+
user_state = await self.client.hgetall(_user_state_key(app_name, user_id))
|
|
216
|
+
for key, value_json in user_state.items():
|
|
217
|
+
key_str = key.decode() if isinstance(key, bytes) else key
|
|
218
|
+
value = json.loads(
|
|
219
|
+
value_json.decode() if isinstance(value_json, bytes) else value_json
|
|
220
|
+
)
|
|
221
|
+
session.state[State.USER_PREFIX + key_str] = value
|
|
222
|
+
|
|
223
|
+
return session
|