Skip to content

Commit 87fdcaa

Browse files
committed
feat(sessions): list_sessions 支持 user_id 为 None 时获取全部 session
list_sessions 的 user_id 参数改为可选,传 None 时返回指定 app 下 所有用户的会话列表(不含 events)。InMemory/SQL/Redis/EvalSessionService 实现统一调整,并补充中英文文档用法说明及单元测试。
1 parent cbb6979 commit 87fdcaa

10 files changed

Lines changed: 119 additions & 30 deletions

docs/mkdocs/en/session.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,11 +109,18 @@ session = await session_service.get_session(
109109

110110
**List Sessions**:
111111
```python
112+
# Specify user_id: returns all sessions for that user (without events)
112113
session_list = await session_service.list_sessions(
113114
app_name="my_app",
114115
user_id="user_001"
115116
)
116-
# Returns ListSessionsResponse, containing all sessions for the user (without events)
117+
118+
# user_id=None: returns sessions across all users for the app
119+
all_session_list = await session_service.list_sessions(
120+
app_name="my_app",
121+
user_id=None
122+
)
123+
# Returns ListSessionsResponse, containing matching sessions (without events)
117124
```
118125

119126
**Delete Session**:
@@ -128,7 +135,7 @@ await session_service.delete_session(
128135
**Implementation Logic** (`_base_session_service.py`):
129136
- `create_session`: Creates a session, separates and stores app/user/session state
130137
- `get_session`: Retrieves a session, merges app/user/session state, applies event filtering
131-
- `list_sessions`: Lists sessions (excludes events to reduce data transfer)
138+
- `list_sessions`: Lists sessions (excludes events to reduce data transfer); passing `user_id=None` returns sessions across all users for the app
132139
- `delete_session`: Deletes a session and its associated data
133140

134141
---
@@ -611,6 +618,7 @@ session = await session_service.get_session(
611618
)
612619

613620
# List existing Sessions
621+
# Specify user_id to return that user's sessions; user_id=None returns sessions across all users for the app
614622
session_list = await session_service.list_sessions(
615623
app_name=app_name,
616624
user_id=user_id

docs/mkdocs/zh/session.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,11 +109,18 @@ session = await session_service.get_session(
109109

110110
**列出会话**
111111
```python
112+
# 指定 user_id:返回该用户的所有会话(不含 events)
112113
session_list = await session_service.list_sessions(
113114
app_name="my_app",
114115
user_id="user_001"
115116
)
116-
# 返回 ListSessionsResponse,包含该用户的所有会话(不含 events)
117+
118+
# user_id 为 None:返回该 app 下所有用户的会话
119+
all_session_list = await session_service.list_sessions(
120+
app_name="my_app",
121+
user_id=None
122+
)
123+
# 返回 ListSessionsResponse,包含符合条件的所有会话(不含 events)
117124
```
118125

119126
**删除会话**
@@ -128,7 +135,7 @@ await session_service.delete_session(
128135
**实现逻辑**`_base_session_service.py`):
129136
- `create_session`:创建会话,分离并存储 app/user/session 状态
130137
- `get_session`:获取会话,合并 app/user/session 状态,应用事件过滤
131-
- `list_sessions`:列出会话列表(不包含 events,减少数据传输)
138+
- `list_sessions`:列出会话列表(不包含 events,减少数据传输)`user_id``None` 时返回该 app 下所有用户的会话
132139
- `delete_session`:删除会话及其关联数据
133140

134141
---
@@ -611,6 +618,7 @@ session = await session_service.get_session(
611618
)
612619

613620
# 列出存在的 Session
621+
# 指定 user_id 返回该用户的会话;user_id=None 返回该 app 下所有用户的会话
614622
session_list = await session_service.list_sessions(
615623
app_name=app_name,
616624
user_id=user_id

tests/sessions/test_in_memory_session_service.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,23 @@ async def test_list_nonexistent_user(self):
251251
assert result.sessions == []
252252
await svc.close()
253253

254+
async def test_list_all_users_when_user_id_none(self):
255+
svc = InMemorySessionService(session_config=_make_session_config())
256+
await svc.create_session(app_name="app", user_id="user1", session_id="s1")
257+
await svc.create_session(app_name="app", user_id="user2", session_id="s2")
258+
result = await svc.list_sessions(app_name="app", user_id=None)
259+
ids = sorted(s.id for s in result.sessions)
260+
assert ids == ["s1", "s2"]
261+
await svc.close()
262+
263+
async def test_list_all_users_filtered_by_app(self):
264+
svc = InMemorySessionService(session_config=_make_session_config())
265+
await svc.create_session(app_name="app1", user_id="user1", session_id="s1")
266+
await svc.create_session(app_name="app2", user_id="user1", session_id="s2")
267+
result = await svc.list_sessions(app_name="app1", user_id=None)
268+
assert [s.id for s in result.sessions] == ["s1"]
269+
await svc.close()
270+
254271

255272
# ---------------------------------------------------------------------------
256273
# InMemorySessionService — delete_session

tests/sessions/test_redis_session_service.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,15 @@ async def test_list_sessions_have_no_events(self):
210210
assert s.historical_events == []
211211
await svc.close()
212212

213+
async def test_list_all_users_when_user_id_none(self):
214+
svc = _create_service()
215+
await svc.create_session(app_name="app", user_id="user1", session_id="s1")
216+
await svc.create_session(app_name="app", user_id="user2", session_id="s2")
217+
result = await svc.list_sessions(app_name="app", user_id=None)
218+
ids = sorted(s.id for s in result.sessions)
219+
assert ids == ["s1", "s2"]
220+
await svc.close()
221+
213222

214223
class TestRedisDeleteSession:
215224
async def test_delete_existing(self):

tests/sessions/test_sql_session_service.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,15 @@ async def test_list_sessions_have_no_events(self):
286286
assert s.historical_events == []
287287
await svc.close()
288288

289+
async def test_list_all_users_when_user_id_none(self):
290+
svc = await _create_service()
291+
await svc.create_session(app_name="app", user_id="user1", session_id="s1")
292+
await svc.create_session(app_name="app", user_id="user2", session_id="s2")
293+
result = await svc.list_sessions(app_name="app", user_id=None)
294+
ids = sorted(s.id for s in result.sessions)
295+
assert ids == ["s1", "s2"]
296+
await svc.close()
297+
289298

290299
# ---------------------------------------------------------------------------
291300
# SqlSessionService — delete_session

trpc_agent_sdk/abc/_session_service.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,19 @@ async def get_session(
9292
"""Gets a session."""
9393

9494
@abstractmethod
95-
async def list_sessions(self, *, app_name: str, user_id: str) -> ListSessionsResponse:
96-
"""Lists all the sessions."""
95+
async def list_sessions(
96+
self,
97+
*,
98+
app_name: str,
99+
user_id: Optional[str] = None,
100+
) -> ListSessionsResponse:
101+
"""Lists all the sessions.
102+
103+
Args:
104+
app_name: the name of the app.
105+
user_id: the id of the user. When None, lists sessions across all
106+
users for the given app.
107+
"""
97108

98109
@abstractmethod
99110
async def delete_session(self, *, app_name: str, user_id: str, session_id: str) -> None:

trpc_agent_sdk/evaluation/_eval_session_service.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ async def get_session(
7171
)
7272

7373
@override
74-
async def list_sessions(self, *, app_name: str, user_id: str):
74+
async def list_sessions(self, *, app_name: str, user_id: Optional[str] = None):
7575
return await self._inner.list_sessions(app_name=app_name, user_id=user_id)
7676

7777
@override

trpc_agent_sdk/sessions/_in_memory_session_service.py

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -183,26 +183,32 @@ async def get_session(
183183
return self._merge_state(app_state, user_state, copied_session)
184184

185185
@override
186-
async def list_sessions(self, *, app_name: str, user_id: str) -> ListSessionsResponse:
186+
async def list_sessions(self, *, app_name: str, user_id: Optional[str] = None) -> ListSessionsResponse:
187187
empty_response = ListSessionsResponse()
188-
if app_name not in self._sessions:
188+
app_sessions = self._sessions.get(app_name)
189+
if not app_sessions:
189190
return empty_response
190-
if user_id not in self._sessions[app_name]:
191+
192+
if user_id is None:
193+
user_sessions = app_sessions
194+
elif user_id in app_sessions:
195+
user_sessions = {user_id: app_sessions[user_id]}
196+
else:
191197
return empty_response
192198

199+
app_state = self._get_app_state(app_name)
193200
sessions_without_events = []
194-
for session_id in self._sessions[app_name][user_id].keys():
195-
session = self._get_session(app_name, user_id, session_id)
196-
if session is None:
197-
continue
198-
199-
copied_session = copy.deepcopy(session)
200-
copied_session.events = []
201-
copied_session.historical_events = []
202-
app_state = self._get_app_state(app_name)
203-
user_state = self._get_user_state(app_name, user_id)
204-
copied_session = self._merge_state(app_state, user_state, copied_session)
205-
sessions_without_events.append(copied_session)
201+
for uid, session_dict in user_sessions.items():
202+
user_state = self._get_user_state(app_name, uid)
203+
for session_id in session_dict.keys():
204+
session = self._get_session(app_name, uid, session_id)
205+
if session is None:
206+
continue
207+
208+
copied_session = copy.deepcopy(session)
209+
copied_session.events = []
210+
copied_session.historical_events = []
211+
sessions_without_events.append(self._merge_state(app_state, user_state, copied_session))
206212
return ListSessionsResponse(sessions=sessions_without_events)
207213

208214
@override

trpc_agent_sdk/sessions/_redis_session_service.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,24 @@
3636
from ._utils import user_state_key
3737

3838

39+
def _session_key_prefix(app_name: str, user_id: Optional[str] = None) -> str:
40+
"""Generate a Redis key prefix for listing sessions.
41+
42+
When user_id is None, the prefix matches sessions across all users for the
43+
given app; otherwise it is scoped to the specific user.
44+
45+
Args:
46+
app_name: Application name
47+
user_id: Optional user identifier
48+
49+
Returns:
50+
Formatted session key prefix with a trailing wildcard.
51+
"""
52+
if user_id is None:
53+
return f"session:{app_name}:*"
54+
return f"session:{app_name}:{user_id}:*"
55+
56+
3957
class RedisSessionService(BaseSessionService):
4058
"""A Redis implementation of the session service.
4159
@@ -129,18 +147,17 @@ async def get_session(
129147
return self._merge_state(app_state, user_state, session)
130148

131149
@override
132-
async def list_sessions(self, *, app_name: str, user_id: str) -> ListSessionsResponse:
150+
async def list_sessions(self, *, app_name: str, user_id: Optional[str] = None) -> ListSessionsResponse:
133151
async with self._redis_storage.create_db_session() as redis_session:
134-
pattern = session_key(app_name, user_id, "*")
152+
pattern = _session_key_prefix(app_name, user_id)
135153
command = RedisCommand(method='keys', args=(pattern, ))
136154
keys = await self._redis_storage.execute_command(redis_session, command)
137155

138156
if not keys:
139157
return ListSessionsResponse()
140158

141-
# Get app and user state once for all sessions
159+
# Get app state once for all sessions
142160
app_state = await self._get_app_state(redis_session, app_name)
143-
user_state = await self._get_user_state(redis_session, app_name, user_id)
144161

145162
sessions_without_events = []
146163
for key in keys:
@@ -150,6 +167,7 @@ async def list_sessions(self, *, app_name: str, user_id: str) -> ListSessionsRes
150167
storage_session.events = []
151168
storage_session.historical_events = []
152169
# Merge state
170+
user_state = await self._get_user_state(redis_session, app_name, storage_session.user_id)
153171
storage_session = self._merge_state(app_state, user_state, storage_session)
154172
sessions_without_events.append(storage_session)
155173

trpc_agent_sdk/sessions/_sql_session_service.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -489,15 +489,17 @@ async def get_session(
489489
return self.filter_events(session)
490490

491491
@override
492-
async def list_sessions(self, *, app_name: str, user_id: str) -> ListSessionsResponse:
492+
async def list_sessions(self, *, app_name: str, user_id: Optional[str] = None) -> ListSessionsResponse:
493493
async with self._sql_storage.create_db_session() as sql_session:
494-
filters = [StorageSession.app_name == app_name, StorageSession.user_id == user_id]
494+
filters = [StorageSession.app_name == app_name]
495+
if user_id is not None:
496+
filters.append(StorageSession.user_id == user_id)
495497
conditions = SqlCondition(filters=filters)
496-
session_key = SqlKey(key=(app_name, user_id), storage_cls=StorageSession)
498+
session_key = SqlKey(key=(app_name, user_id) if user_id is not None else (app_name,),
499+
storage_cls=StorageSession)
497500
results: List[StorageSession] = await self._sql_storage.query(sql_session, session_key, conditions)
498501

499502
app_state = await self._get_app_state(sql_session, app_name)
500-
user_state = await self._get_user_state(sql_session, app_name, user_id)
501503

502504
sessions = []
503505
for storage_session in results:
@@ -508,6 +510,7 @@ async def list_sessions(self, *, app_name: str, user_id: str) -> ListSessionsRes
508510

509511
storage_session.events = []
510512

513+
user_state = await self._get_user_state(sql_session, app_name, storage_session.user_id)
511514
merged_state = merge_state(
512515
StateStorageEntry(app_state_delta=app_state,
513516
user_state_delta=user_state,

0 commit comments

Comments
 (0)