Skip to content

Commit 8c02f85

Browse files
committed
refactor: simplify handoff image_urls data flow
1 parent 27706d8 commit 8c02f85

2 files changed

Lines changed: 42 additions & 63 deletions

File tree

astrbot/core/astr_agent_tool_exec.py

Lines changed: 36 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -68,38 +68,26 @@ def _is_supported_image_ref(cls, image_ref: str) -> bool:
6868
return ext in cls._ALLOWED_IMAGE_EXTENSIONS
6969

7070
@classmethod
71-
def _coerce_image_urls(cls, image_urls: T.Any) -> list[T.Any]:
72-
if image_urls is None:
73-
return []
74-
if isinstance(image_urls, str):
75-
return [image_urls]
76-
if isinstance(image_urls, (Sequence, AbstractSet)) and not isinstance(
77-
image_urls, (str, bytes, bytearray)
71+
async def _collect_handoff_image_urls(
72+
cls,
73+
run_context: ContextWrapper[AstrAgentContext],
74+
image_urls_raw: T.Any,
75+
) -> list[str]:
76+
candidates: list[T.Any] = []
77+
if image_urls_raw is None:
78+
pass
79+
elif isinstance(image_urls_raw, str):
80+
candidates.append(image_urls_raw)
81+
elif isinstance(image_urls_raw, (Sequence, AbstractSet)) and not isinstance(
82+
image_urls_raw, (str, bytes, bytearray)
7883
):
79-
return list(image_urls)
80-
logger.warning(
81-
"Unsupported image_urls type in handoff tool args: %s",
82-
type(image_urls).__name__,
83-
)
84-
return []
85-
86-
@classmethod
87-
def _filter_supported_image_urls(cls, candidates: list[T.Any]) -> list[str]:
88-
normalized = normalize_and_dedupe_strings(candidates)
89-
sanitized = [item for item in normalized if cls._is_supported_image_ref(item)]
90-
dropped_count = len(normalized) - len(sanitized)
91-
if dropped_count > 0:
84+
candidates.extend(image_urls_raw)
85+
else:
9286
logger.warning(
93-
"Dropped %d invalid image_urls entries in handoff tool args.",
94-
dropped_count,
87+
"Unsupported image_urls type in handoff tool args: %s",
88+
type(image_urls_raw).__name__,
9589
)
96-
return sanitized
9790

98-
@classmethod
99-
async def _iter_event_image_paths(
100-
cls, run_context: ContextWrapper[AstrAgentContext]
101-
) -> list[str]:
102-
paths: list[str] = []
10391
event = getattr(run_context.context, "event", None)
10492
message_obj = getattr(event, "message_obj", None)
10593
message = getattr(message_obj, "message", None)
@@ -110,26 +98,24 @@ async def _iter_event_image_paths(
11098
try:
11199
path = await component.convert_to_file_path()
112100
if path and cls._is_supported_image_ref(path):
113-
paths.append(path)
101+
candidates.append(path)
114102
except Exception as e:
115103
logger.error(
116104
"Failed to convert handoff image component at index %d: %s",
117105
idx,
118106
e,
119107
exc_info=True,
120108
)
121-
return paths
122109

123-
@classmethod
124-
async def _prepare_handoff_image_urls(
125-
cls,
126-
run_context: ContextWrapper[AstrAgentContext],
127-
image_urls: T.Any,
128-
) -> list[str]:
129-
candidates = cls._coerce_image_urls(image_urls)
130-
event_paths = await cls._iter_event_image_paths(run_context)
131-
candidates.extend(event_paths)
132-
return cls._filter_supported_image_urls(candidates)
110+
normalized = normalize_and_dedupe_strings(candidates)
111+
sanitized = [item for item in normalized if cls._is_supported_image_ref(item)]
112+
dropped_count = len(normalized) - len(sanitized)
113+
if dropped_count > 0:
114+
logger.warning(
115+
"Dropped %d invalid image_urls entries in handoff tool args.",
116+
dropped_count,
117+
)
118+
return sanitized
133119

134120
@classmethod
135121
async def execute(cls, tool, run_context, **tool_args):
@@ -151,7 +137,7 @@ async def execute(cls, tool, run_context, **tool_args):
151137
):
152138
yield r
153139
return
154-
async for r in cls._execute_handoff(tool, run_context, tool_args):
140+
async for r in cls._execute_handoff(tool, run_context, **tool_args):
155141
yield r
156142
return
157143

@@ -254,14 +240,17 @@ async def _execute_handoff(
254240
cls,
255241
tool: HandoffTool,
256242
run_context: ContextWrapper[AstrAgentContext],
257-
tool_args: dict[str, T.Any],
243+
**tool_args: T.Any,
258244
):
259245
input_ = tool_args.get("input")
260-
image_urls = await cls._prepare_handoff_image_urls(
246+
image_urls = await cls._collect_handoff_image_urls(
261247
run_context,
262248
tool_args.get("image_urls"),
263249
)
264-
tool_args["image_urls"] = image_urls
250+
effective_tool_args: dict[str, T.Any] = {
251+
**tool_args,
252+
"image_urls": image_urls,
253+
}
265254

266255
# Build handoff toolset from registered tools plus runtime computer tools.
267256
toolset = cls._build_handoff_toolset(run_context, tool.agent.tools)
@@ -295,7 +284,7 @@ async def _execute_handoff(
295284
event=event,
296285
chat_provider_id=prov_id,
297286
prompt=input_,
298-
image_urls=image_urls,
287+
image_urls=effective_tool_args["image_urls"],
299288
system_prompt=tool.agent.instructions,
300289
tools=toolset,
301290
contexts=contexts,
@@ -360,9 +349,8 @@ async def _do_handoff_background(
360349
) -> None:
361350
"""Run the subagent handoff and, on completion, wake the main agent."""
362351
result_text = ""
363-
prepared_tool_args = dict(tool_args)
364352
try:
365-
async for r in cls._execute_handoff(tool, run_context, prepared_tool_args):
353+
async for r in cls._execute_handoff(tool, run_context, **tool_args):
366354
if isinstance(r, mcp.types.CallToolResult):
367355
for content in r.content:
368356
if isinstance(content, mcp.types.TextContent):
@@ -379,7 +367,7 @@ async def _do_handoff_background(
379367
task_id=task_id,
380368
tool_name=tool.name,
381369
result_text=result_text,
382-
tool_args=prepared_tool_args,
370+
tool_args=tool_args,
383371
note=(
384372
event.get_extra("background_note")
385373
or f"Background task for subagent '{tool.agent.name}' finished."

tests/unit/test_astr_agent_tool_exec.py

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ def _build_run_context(message_components: list[object] | None = None):
3030

3131

3232
@pytest.mark.asyncio
33-
async def test_prepare_handoff_image_urls_normalizes_filters_and_appends_event_image(
33+
async def test_collect_handoff_image_urls_normalizes_filters_and_appends_event_image(
3434
monkeypatch: pytest.MonkeyPatch,
3535
):
3636
async def _fake_convert_to_file_path(self):
@@ -46,7 +46,7 @@ async def _fake_convert_to_file_path(self):
4646
123,
4747
)
4848

49-
image_urls = await FunctionToolExecutor._prepare_handoff_image_urls(
49+
image_urls = await FunctionToolExecutor._collect_handoff_image_urls(
5050
run_context,
5151
image_urls_input,
5252
)
@@ -59,7 +59,7 @@ async def _fake_convert_to_file_path(self):
5959

6060

6161
@pytest.mark.asyncio
62-
async def test_prepare_handoff_image_urls_skips_failed_event_image_conversion(
62+
async def test_collect_handoff_image_urls_skips_failed_event_image_conversion(
6363
monkeypatch: pytest.MonkeyPatch,
6464
):
6565
async def _fake_convert_to_file_path(self):
@@ -68,7 +68,7 @@ async def _fake_convert_to_file_path(self):
6868
monkeypatch.setattr(Image, "convert_to_file_path", _fake_convert_to_file_path)
6969

7070
run_context = _build_run_context([Image(file="file:///tmp/original.png")])
71-
image_urls = await FunctionToolExecutor._prepare_handoff_image_urls(
71+
image_urls = await FunctionToolExecutor._collect_handoff_image_urls(
7272
run_context,
7373
["https://example.com/a.png"],
7474
)
@@ -82,23 +82,14 @@ async def test_do_handoff_background_reports_prepared_image_urls(
8282
):
8383
captured: dict = {}
8484

85-
async def _unexpected_prepare(cls, run_context, image_urls):
86-
raise AssertionError("background path should not pre-prepare image urls")
87-
88-
async def _fake_execute_handoff(cls, tool, run_context, tool_args):
89-
tool_args["image_urls"] = ["prepared://image.png"]
85+
async def _fake_execute_handoff(cls, tool, run_context, **tool_args):
9086
yield mcp.types.CallToolResult(
9187
content=[mcp.types.TextContent(type="text", text="ok")]
9288
)
9389

9490
async def _fake_wake(cls, run_context, **kwargs):
9591
captured.update(kwargs)
9692

97-
monkeypatch.setattr(
98-
FunctionToolExecutor,
99-
"_prepare_handoff_image_urls",
100-
classmethod(_unexpected_prepare),
101-
)
10293
monkeypatch.setattr(
10394
FunctionToolExecutor,
10495
"_execute_handoff",
@@ -119,4 +110,4 @@ async def _fake_wake(cls, run_context, **kwargs):
119110
image_urls="https://example.com/raw.png",
120111
)
121112

122-
assert captured["tool_args"]["image_urls"] == ["prepared://image.png"]
113+
assert captured["tool_args"]["image_urls"] == "https://example.com/raw.png"

0 commit comments

Comments
 (0)