Strip all images from session on vision-rejection, not just the latest (#553)
This commit is contained in:
@@ -16,7 +16,7 @@ from docker import errors as docker_errors # type: ignore[import-untyped, unuse
|
|||||||
from openai import APIError
|
from openai import APIError
|
||||||
|
|
||||||
from strix.core.inputs import child_initial_input
|
from strix.core.inputs import child_initial_input
|
||||||
from strix.core.sessions import open_agent_session, strip_latest_image_from_session
|
from strix.core.sessions import open_agent_session, strip_all_images_from_session
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -384,14 +384,14 @@ async def _run_cycle( # noqa: PLR0912, PLR0915
|
|||||||
and getattr(exc, "status_code", None) in _INPUT_REJECTION_CODES
|
and getattr(exc, "status_code", None) in _INPUT_REJECTION_CODES
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
stripped = await strip_latest_image_from_session(session)
|
stripped = await strip_all_images_from_session(session)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("image-strip recovery failed for %s", agent_id)
|
logger.exception("image-strip recovery failed for %s", agent_id)
|
||||||
stripped = False
|
stripped = False
|
||||||
if stripped:
|
if stripped:
|
||||||
image_strips += 1
|
image_strips += 1
|
||||||
logger.info(
|
logger.info(
|
||||||
"Stripped latest image from %s session after rejection; retrying (%d)",
|
"Stripped images from %s session after rejection; retrying (%d)",
|
||||||
agent_id,
|
agent_id,
|
||||||
image_strips,
|
image_strips,
|
||||||
)
|
)
|
||||||
|
|||||||
+34
-19
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import TYPE_CHECKING, cast
|
import contextlib
|
||||||
|
from typing import TYPE_CHECKING, Any, cast
|
||||||
|
|
||||||
from agents.memory import SQLiteSession
|
from agents.memory import SQLiteSession
|
||||||
|
|
||||||
@@ -22,29 +23,43 @@ def open_agent_session(agent_id: str, path: Path) -> SQLiteSession:
|
|||||||
_IMAGE_REJECTED_TEXT = "[image rejected by the model]"
|
_IMAGE_REJECTED_TEXT = "[image rejected by the model]"
|
||||||
|
|
||||||
|
|
||||||
async def strip_latest_image_from_session(session: Session) -> bool:
|
async def strip_all_images_from_session(session: Session) -> bool:
|
||||||
items = await session.get_items()
|
items = await session.get_items()
|
||||||
if not items:
|
if not items:
|
||||||
return False
|
return False
|
||||||
latest = items[-1]
|
|
||||||
if not isinstance(latest, dict) or latest.get("type") != "function_call_output":
|
rebuilt: list[Any] = []
|
||||||
return False
|
changed = False
|
||||||
output = latest.get("output")
|
for item in items:
|
||||||
if not isinstance(output, list):
|
item_dict = cast("dict[str, Any]", item) if isinstance(item, dict) else None
|
||||||
return False
|
if (
|
||||||
if not any(isinstance(b, dict) and b.get("type") == "input_image" for b in output):
|
item_dict is not None
|
||||||
return False
|
and item_dict.get("type") == "function_call_output"
|
||||||
await session.pop_item()
|
and isinstance(item_dict.get("output"), list)
|
||||||
await session.add_items(
|
and any(
|
||||||
cast(
|
isinstance(b, dict) and b.get("type") == "input_image" for b in item_dict["output"]
|
||||||
"list[TResponseInputItem]",
|
)
|
||||||
[
|
):
|
||||||
|
rebuilt.append(
|
||||||
{
|
{
|
||||||
"type": "function_call_output",
|
"type": "function_call_output",
|
||||||
"call_id": latest.get("call_id"),
|
"call_id": item_dict.get("call_id"),
|
||||||
"output": [{"type": "input_text", "text": _IMAGE_REJECTED_TEXT}],
|
"output": [{"type": "input_text", "text": _IMAGE_REJECTED_TEXT}],
|
||||||
},
|
},
|
||||||
],
|
)
|
||||||
),
|
changed = True
|
||||||
)
|
else:
|
||||||
|
rebuilt.append(item)
|
||||||
|
|
||||||
|
if not changed:
|
||||||
|
return False
|
||||||
|
|
||||||
|
rebuilt_items = cast("list[TResponseInputItem]", rebuilt)
|
||||||
|
await session.clear_session()
|
||||||
|
try:
|
||||||
|
await session.add_items(rebuilt_items)
|
||||||
|
except Exception:
|
||||||
|
with contextlib.suppress(Exception):
|
||||||
|
await session.add_items(rebuilt_items)
|
||||||
|
raise
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -13,5 +13,5 @@ returns it as an image content block for vision-capable models.
|
|||||||
`containers/docker-entrypoint.sh`).
|
`containers/docker-entrypoint.sh`).
|
||||||
- **Skill:** screenshot workflow lives in `strix/skills/tooling/agent_browser.md`.
|
- **Skill:** screenshot workflow lives in `strix/skills/tooling/agent_browser.md`.
|
||||||
- **Recovery:** vision-not-supported model rejections are auto-recovered
|
- **Recovery:** vision-not-supported model rejections are auto-recovered
|
||||||
via `strix.core.sessions.strip_latest_image_from_session`, invoked from
|
via `strix.core.sessions.strip_all_images_from_session`, invoked from
|
||||||
`strix/core/execution.py`.
|
`strix/core/execution.py`.
|
||||||
|
|||||||
Reference in New Issue
Block a user