Skip to content

Commit 46e9249

Browse files
committed
feat(super_agent): add list_conversations_async method
This commit introduces the `list_conversations_async` method to both the `SuperAgent` and template classes. The method allows listing conversations asynchronously with optional metadata filtering. By default, it filters by agent runtime name unless explicitly overridden. Key changes include: - Added async implementation in `agent.py`, `__agent_async_template.py`, and API modules - Includes proper error handling and type validation - Maintains backward compatibility through synchronous placeholder methods that raise NotImplementedError Co-developed-by: Aone Copilot <noreply@alibaba-inc.com> Signed-off-by: Sodawyx <sodawyx@126.com>
1 parent cbc8fe9 commit 46e9249

6 files changed

Lines changed: 440 additions & 0 deletions

File tree

agentrun/super_agent/__agent_async_template.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,41 @@ def invoke(
118118
) -> InvokeStream:
119119
raise NotImplementedError(_SYNC_UNSUPPORTED_MSG)
120120

121+
async def list_conversations_async(
122+
self,
123+
*,
124+
metadata: Optional[Dict[str, Any]] = None,
125+
config: Optional[Config] = None,
126+
) -> List[ConversationInfo]:
127+
"""GET /conversations → ``List[ConversationInfo]``.
128+
129+
默认按 ``{"agentRuntimeName": self.name}`` 过滤 (仅返回当前 agent 的会话);
130+
传入 ``metadata={}`` 或自定义 dict 可覆盖默认过滤条件。
131+
"""
132+
cfg = self._resolve_config(config)
133+
api = SuperAgentDataAPI(self.name, config=cfg)
134+
effective_metadata = (
135+
{"agentRuntimeName": self.name} if metadata is None else metadata
136+
)
137+
raw_list = await api.list_conversations_async(
138+
metadata=effective_metadata, config=cfg
139+
)
140+
return [
141+
_conversation_info_from_dict(
142+
raw,
143+
fallback_conversation_id=str(raw.get("conversationId") or ""),
144+
)
145+
for raw in raw_list
146+
]
147+
148+
def list_conversations(
149+
self,
150+
*,
151+
metadata: Optional[Dict[str, Any]] = None,
152+
config: Optional[Config] = None,
153+
) -> List[ConversationInfo]:
154+
raise NotImplementedError(_SYNC_UNSUPPORTED_MSG)
155+
121156
async def get_conversation_async(
122157
self,
123158
conversation_id: str,

agentrun/super_agent/agent.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,41 @@ def invoke(
126126
) -> InvokeStream:
127127
raise NotImplementedError(_SYNC_UNSUPPORTED_MSG)
128128

129+
async def list_conversations_async(
130+
self,
131+
*,
132+
metadata: Optional[Dict[str, Any]] = None,
133+
config: Optional[Config] = None,
134+
) -> List[ConversationInfo]:
135+
"""GET /conversations → ``List[ConversationInfo]``.
136+
137+
默认按 ``{"agentRuntimeName": self.name}`` 过滤 (仅返回当前 agent 的会话);
138+
传入 ``metadata={}`` 或自定义 dict 可覆盖默认过滤条件。
139+
"""
140+
cfg = self._resolve_config(config)
141+
api = SuperAgentDataAPI(self.name, config=cfg)
142+
effective_metadata = (
143+
{"agentRuntimeName": self.name} if metadata is None else metadata
144+
)
145+
raw_list = await api.list_conversations_async(
146+
metadata=effective_metadata, config=cfg
147+
)
148+
return [
149+
_conversation_info_from_dict(
150+
raw,
151+
fallback_conversation_id=str(raw.get("conversationId") or ""),
152+
)
153+
for raw in raw_list
154+
]
155+
156+
def list_conversations(
157+
self,
158+
*,
159+
metadata: Optional[Dict[str, Any]] = None,
160+
config: Optional[Config] = None,
161+
) -> List[ConversationInfo]:
162+
raise NotImplementedError(_SYNC_UNSUPPORTED_MSG)
163+
129164
async def get_conversation_async(
130165
self,
131166
conversation_id: str,

agentrun/super_agent/api/__data_async_template.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,55 @@ def stream(
193193
) -> Iterator[SSEEvent]:
194194
raise NotImplementedError(_SYNC_UNSUPPORTED_MSG)
195195

196+
async def list_conversations_async(
197+
self,
198+
metadata: Optional[Dict[str, Any]] = None,
199+
config: Optional[Config] = None,
200+
) -> List[Dict[str, Any]]:
201+
"""GET /conversations → 返回服务端 ``data.conversations`` 数组 (缺失时返回 [])。
202+
203+
``metadata`` 若非 None, 会以 JSON 编码后通过 ``metadata`` query 参数下发;
204+
服务端按该 metadata 过滤 (例如 ``{"agentRuntimeName": "..."}``)。
205+
不传则由服务端按当前 sub uid 过滤。
206+
"""
207+
cfg = Config.with_configs(self.config, config)
208+
query: Optional[Dict[str, Any]] = None
209+
if metadata is not None:
210+
query = {"metadata": json.dumps(metadata, ensure_ascii=False)}
211+
url = self.with_path("conversations", query=query, config=cfg)
212+
_, signed_headers, _ = self.auth(
213+
url=url,
214+
method="GET",
215+
headers=cfg.get_headers(),
216+
config=cfg,
217+
)
218+
logger.debug("super_agent list_conversations request: GET %s", url)
219+
async with httpx.AsyncClient(timeout=cfg.get_timeout()) as client:
220+
resp = await client.get(url, headers=signed_headers)
221+
resp.raise_for_status()
222+
payload = resp.json() if resp.text else {}
223+
logger.debug(
224+
"super_agent list_conversations response: status=%d payload=%s",
225+
resp.status_code,
226+
payload,
227+
)
228+
if not isinstance(payload, dict):
229+
return []
230+
data = payload.get("data")
231+
if not isinstance(data, dict):
232+
return []
233+
raw_list = data.get("conversations")
234+
if not isinstance(raw_list, list):
235+
return []
236+
return [item for item in raw_list if isinstance(item, dict)]
237+
238+
def list_conversations(
239+
self,
240+
metadata: Optional[Dict[str, Any]] = None,
241+
config: Optional[Config] = None,
242+
) -> List[Dict[str, Any]]:
243+
raise NotImplementedError(_SYNC_UNSUPPORTED_MSG)
244+
196245
async def get_conversation_async(
197246
self,
198247
conversation_id: str,

agentrun/super_agent/api/data.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,55 @@ def stream(
199199
) -> Iterator[SSEEvent]:
200200
raise NotImplementedError(_SYNC_UNSUPPORTED_MSG)
201201

202+
async def list_conversations_async(
203+
self,
204+
metadata: Optional[Dict[str, Any]] = None,
205+
config: Optional[Config] = None,
206+
) -> List[Dict[str, Any]]:
207+
"""GET /conversations → 返回服务端 ``data.conversations`` 数组 (缺失时返回 [])。
208+
209+
``metadata`` 若非 None, 会以 JSON 编码后通过 ``metadata`` query 参数下发;
210+
服务端按该 metadata 过滤 (例如 ``{"agentRuntimeName": "..."}``)。
211+
不传则由服务端按当前 sub uid 过滤。
212+
"""
213+
cfg = Config.with_configs(self.config, config)
214+
query: Optional[Dict[str, Any]] = None
215+
if metadata is not None:
216+
query = {"metadata": json.dumps(metadata, ensure_ascii=False)}
217+
url = self.with_path("conversations", query=query, config=cfg)
218+
_, signed_headers, _ = self.auth(
219+
url=url,
220+
method="GET",
221+
headers=cfg.get_headers(),
222+
config=cfg,
223+
)
224+
logger.debug("super_agent list_conversations request: GET %s", url)
225+
async with httpx.AsyncClient(timeout=cfg.get_timeout()) as client:
226+
resp = await client.get(url, headers=signed_headers)
227+
resp.raise_for_status()
228+
payload = resp.json() if resp.text else {}
229+
logger.debug(
230+
"super_agent list_conversations response: status=%d payload=%s",
231+
resp.status_code,
232+
payload,
233+
)
234+
if not isinstance(payload, dict):
235+
return []
236+
data = payload.get("data")
237+
if not isinstance(data, dict):
238+
return []
239+
raw_list = data.get("conversations")
240+
if not isinstance(raw_list, list):
241+
return []
242+
return [item for item in raw_list if isinstance(item, dict)]
243+
244+
def list_conversations(
245+
self,
246+
metadata: Optional[Dict[str, Any]] = None,
247+
config: Optional[Config] = None,
248+
) -> List[Dict[str, Any]]:
249+
raise NotImplementedError(_SYNC_UNSUPPORTED_MSG)
250+
202251
async def get_conversation_async(
203252
self,
204253
conversation_id: str,

tests/unittests/super_agent/test_agent.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,97 @@ async def test_delete_conversation_async_returns_none():
226226
assert await _make_agent().delete_conversation_async("c") is None
227227

228228

229+
# ─── list_conversations_async ──────────────────────────────
230+
231+
232+
async def test_list_conversations_async_default_filters_by_agent_name():
233+
"""不传 metadata 时, 默认按 ``{"agentRuntimeName": self.name}`` 过滤."""
234+
instance = MagicMock()
235+
instance.list_conversations_async = AsyncMock(return_value=[])
236+
factory = MagicMock(return_value=instance)
237+
with patch("agentrun.super_agent.agent.SuperAgentDataAPI", factory):
238+
agent = SuperAgent(name="my-agent")
239+
result = await agent.list_conversations_async()
240+
assert result == []
241+
assert instance.list_conversations_async.await_args.kwargs["metadata"] == {
242+
"agentRuntimeName": "my-agent"
243+
}
244+
245+
246+
async def test_list_conversations_async_explicit_metadata_passthrough():
247+
instance = MagicMock()
248+
instance.list_conversations_async = AsyncMock(return_value=[])
249+
factory = MagicMock(return_value=instance)
250+
with patch("agentrun.super_agent.agent.SuperAgentDataAPI", factory):
251+
agent = _make_agent()
252+
await agent.list_conversations_async(metadata={"foo": "bar"})
253+
assert instance.list_conversations_async.await_args.kwargs["metadata"] == {
254+
"foo": "bar"
255+
}
256+
257+
258+
async def test_list_conversations_async_empty_metadata_overrides_default():
259+
"""传入空 dict 明确表示「不按 agent 过滤」, SDK MUST 不再注入默认值."""
260+
instance = MagicMock()
261+
instance.list_conversations_async = AsyncMock(return_value=[])
262+
factory = MagicMock(return_value=instance)
263+
with patch("agentrun.super_agent.agent.SuperAgentDataAPI", factory):
264+
agent = _make_agent()
265+
await agent.list_conversations_async(metadata={})
266+
assert instance.list_conversations_async.await_args.kwargs["metadata"] == {}
267+
268+
269+
async def test_list_conversations_async_returns_conversation_info_list():
270+
instance = MagicMock()
271+
instance.list_conversations_async = AsyncMock(
272+
return_value=[
273+
{
274+
"conversationId": "c1",
275+
"agentId": "ag",
276+
"title": "first",
277+
"createdAt": 1,
278+
"updatedAt": 2,
279+
"messages": [{"role": "user", "content": "hi"}],
280+
},
281+
{
282+
"conversationId": "c2",
283+
"agentId": "ag",
284+
"title": "second",
285+
"messages": [],
286+
},
287+
]
288+
)
289+
factory = MagicMock(return_value=instance)
290+
with patch("agentrun.super_agent.agent.SuperAgentDataAPI", factory):
291+
result = await _make_agent().list_conversations_async()
292+
assert [c.conversation_id for c in result] == ["c1", "c2"]
293+
assert result[0].title == "first"
294+
assert len(result[0].messages) == 1
295+
assert result[0].messages[0].content == "hi"
296+
assert result[1].title == "second"
297+
298+
299+
async def test_list_conversations_async_empty_list():
300+
instance = MagicMock()
301+
instance.list_conversations_async = AsyncMock(return_value=[])
302+
factory = MagicMock(return_value=instance)
303+
with patch("agentrun.super_agent.agent.SuperAgentDataAPI", factory):
304+
assert await _make_agent().list_conversations_async() == []
305+
306+
307+
async def test_list_conversations_async_item_missing_conversation_id_uses_empty_fallback():
308+
"""对单条会话里 ``conversationId`` 缺失的情况, fallback 保持空串 (不会用 agent name)."""
309+
instance = MagicMock()
310+
instance.list_conversations_async = AsyncMock(
311+
return_value=[{"agentId": "ag"}]
312+
)
313+
factory = MagicMock(return_value=instance)
314+
with patch("agentrun.super_agent.agent.SuperAgentDataAPI", factory):
315+
result = await _make_agent().list_conversations_async()
316+
assert len(result) == 1
317+
assert result[0].conversation_id == ""
318+
319+
229320
# ─── sync methods → NotImplementedError ─────────────────────
230321

231322

@@ -237,6 +328,8 @@ def test_sync_methods_not_implemented():
237328
agent.get_conversation("c")
238329
with pytest.raises(NotImplementedError):
239330
agent.delete_conversation("c")
331+
with pytest.raises(NotImplementedError):
332+
agent.list_conversations()
240333

241334

242335
def test_invoke_async_signature_only_messages_and_conversation_id():

0 commit comments

Comments
 (0)