|
2 | 2 |
|
3 | 3 | import logging |
4 | 4 | from collections.abc import Callable |
5 | | -from typing import TYPE_CHECKING, Any, Literal |
| 5 | +from typing import TYPE_CHECKING, Any, Literal, cast |
6 | 6 |
|
7 | 7 | from openai import AsyncOpenAI |
8 | 8 |
|
| 9 | +from ..items import TResponseInputItem |
9 | 10 | from ..models._openai_shared import get_default_openai_client |
10 | 11 | from ..run_internal.items import normalize_input_items_for_api |
11 | 12 | from .openai_conversations_session import OpenAIConversationsSession |
|
16 | 17 | ) |
17 | 18 |
|
18 | 19 | if TYPE_CHECKING: |
19 | | - from ..items import TResponseInputItem |
20 | 20 | from .session import Session |
21 | 21 |
|
22 | 22 | logger = logging.getLogger("openai-agents.openai.compaction") |
@@ -213,19 +213,8 @@ async def run_compaction(self, args: OpenAIResponsesCompactionArgs | None = None |
213 | 213 |
|
214 | 214 | compacted = await self.client.responses.compact(**compact_kwargs) |
215 | 215 |
|
| 216 | + output_items = _normalize_compaction_output_items(compacted.output or []) |
216 | 217 | await self.underlying_session.clear_session() |
217 | | - output_items: list[TResponseInputItem] = [] |
218 | | - if compacted.output: |
219 | | - for item in compacted.output: |
220 | | - if isinstance(item, dict): |
221 | | - output_items.append(item) |
222 | | - else: |
223 | | - # Suppress Pydantic literal warnings: responses.compact can return |
224 | | - # user-style input_text content inside ResponseOutputMessage. |
225 | | - output_items.append( |
226 | | - item.model_dump(exclude_unset=True, warnings=False) # type: ignore |
227 | | - ) |
228 | | - |
229 | 218 | output_items = _strip_orphaned_assistant_ids(output_items) |
230 | 219 |
|
231 | 220 | if output_items: |
@@ -339,6 +328,101 @@ def _strip_orphaned_assistant_ids( |
339 | 328 | return cleaned |
340 | 329 |
|
341 | 330 |
|
| 331 | +def _normalize_compaction_output_items(items: list[Any]) -> list[TResponseInputItem]: |
| 332 | + """Normalize compacted output into replay-safe Responses input items.""" |
| 333 | + output_items: list[TResponseInputItem] = [] |
| 334 | + for item in items: |
| 335 | + if isinstance(item, dict): |
| 336 | + output_item = item |
| 337 | + else: |
| 338 | + # Suppress Pydantic literal warnings: responses.compact can return |
| 339 | + # user-style input_text content inside ResponseOutputMessage. |
| 340 | + output_item = item.model_dump(exclude_unset=True, warnings=False) |
| 341 | + |
| 342 | + if ( |
| 343 | + isinstance(output_item, dict) |
| 344 | + and output_item.get("type") == "message" |
| 345 | + and output_item.get("role") == "user" |
| 346 | + ): |
| 347 | + output_items.append(_normalize_compaction_user_message(output_item)) |
| 348 | + continue |
| 349 | + |
| 350 | + output_items.append(cast(TResponseInputItem, output_item)) |
| 351 | + return output_items |
| 352 | + |
| 353 | + |
| 354 | +def _normalize_compaction_user_message(item: dict[str, Any]) -> TResponseInputItem: |
| 355 | + """Normalize compacted user message content before it is reused as input.""" |
| 356 | + content = item.get("content") |
| 357 | + if not isinstance(content, list): |
| 358 | + return cast(TResponseInputItem, item) |
| 359 | + |
| 360 | + normalized_content: list[Any] = [] |
| 361 | + for content_item in content: |
| 362 | + if not isinstance(content_item, dict): |
| 363 | + normalized_content.append(content_item) |
| 364 | + continue |
| 365 | + |
| 366 | + content_type = content_item.get("type") |
| 367 | + if content_type == "input_image": |
| 368 | + normalized_content.append(_normalize_compaction_input_image(content_item)) |
| 369 | + elif content_type == "input_file": |
| 370 | + normalized_content.append(_normalize_compaction_input_file(content_item)) |
| 371 | + else: |
| 372 | + normalized_content.append(content_item) |
| 373 | + |
| 374 | + normalized_item = dict(item) |
| 375 | + normalized_item["content"] = normalized_content |
| 376 | + return cast(TResponseInputItem, normalized_item) |
| 377 | + |
| 378 | + |
| 379 | +def _normalize_compaction_input_image(content_item: dict[str, Any]) -> dict[str, Any]: |
| 380 | + """Return a valid replay shape for a compacted Responses image input.""" |
| 381 | + normalized = {"type": "input_image"} |
| 382 | + |
| 383 | + image_url = content_item.get("image_url") |
| 384 | + file_id = content_item.get("file_id") |
| 385 | + if isinstance(image_url, str) and image_url: |
| 386 | + normalized["image_url"] = image_url |
| 387 | + elif isinstance(file_id, str) and file_id: |
| 388 | + normalized["file_id"] = file_id |
| 389 | + else: |
| 390 | + raise ValueError("Compaction input_image item missing image_url or file_id.") |
| 391 | + |
| 392 | + detail = content_item.get("detail") |
| 393 | + if isinstance(detail, str) and detail: |
| 394 | + normalized["detail"] = detail |
| 395 | + |
| 396 | + return normalized |
| 397 | + |
| 398 | + |
| 399 | +def _normalize_compaction_input_file(content_item: dict[str, Any]) -> dict[str, Any]: |
| 400 | + """Return a valid replay shape for a compacted Responses file input.""" |
| 401 | + normalized = {"type": "input_file"} |
| 402 | + |
| 403 | + file_data = content_item.get("file_data") |
| 404 | + file_url = content_item.get("file_url") |
| 405 | + file_id = content_item.get("file_id") |
| 406 | + if isinstance(file_data, str) and file_data: |
| 407 | + normalized["file_data"] = file_data |
| 408 | + elif isinstance(file_url, str) and file_url: |
| 409 | + normalized["file_url"] = file_url |
| 410 | + elif isinstance(file_id, str) and file_id: |
| 411 | + normalized["file_id"] = file_id |
| 412 | + else: |
| 413 | + raise ValueError("Compaction input_file item missing file_data, file_url, or file_id.") |
| 414 | + |
| 415 | + filename = content_item.get("filename") |
| 416 | + if isinstance(filename, str) and filename: |
| 417 | + normalized["filename"] = filename |
| 418 | + |
| 419 | + detail = content_item.get("detail") |
| 420 | + if isinstance(detail, str) and detail: |
| 421 | + normalized["detail"] = detail |
| 422 | + |
| 423 | + return normalized |
| 424 | + |
| 425 | + |
342 | 426 | def _normalize_compaction_session_items( |
343 | 427 | items: list[TResponseInputItem], |
344 | 428 | ) -> list[TResponseInputItem]: |
|
0 commit comments