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.
@@ -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"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
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,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
@@ -0,0 +1,3 @@
1
+ __init__
2
+ firestore_session
3
+ redis_session
@@ -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