Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 120 additions & 29 deletions python/packages/openai/agent_framework_openai/_chat_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,72 @@ class OpenAIChatOptions(ChatOptions[ResponseFormatT], Generic[ResponseFormatT],
# endregion


# region Helpers


def _annotations_to_output_text(annotations: Sequence[Annotation] | None) -> list[dict[str, Any]]:
"""Convert framework `Annotation` objects to Responses API `output_text` annotation dicts.

Citations from `file_search`, `code_interpreter` file paths, and url citations all collapse
to `Annotation(type="citation", ...)` in the framework. The original API form is recovered
here so assistant messages roundtrip cleanly through history forwarding.
"""
if not annotations:
return []
out: list[dict[str, Any]] = []
for annotation in annotations:
if annotation.get("type") != "citation":
continue
props = annotation.get("additional_properties") or {}
regions = annotation.get("annotated_regions") or []
first_region = regions[0] if regions else None
file_id = annotation.get("file_id")
url = annotation.get("url")
title = annotation.get("title")
container_id = props.get("container_id")

if container_id and file_id and first_region is not None:
entry: dict[str, Any] = {
"type": "container_file_citation",
"container_id": container_id,
"file_id": file_id,
"start_index": first_region.get("start_index"),
"end_index": first_region.get("end_index"),
}
if url:
entry["filename"] = url
out.append(entry)
elif first_region is not None and url and not file_id:
out.append({
"type": "url_citation",
"url": url,
"title": title or "",
"start_index": first_region.get("start_index"),
"end_index": first_region.get("end_index"),
Comment thread
moonbox3 marked this conversation as resolved.
Outdated
})
Comment thread
moonbox3 marked this conversation as resolved.
Outdated
elif file_id and url:
entry = {
"type": "file_citation",
"file_id": file_id,
"filename": url,
}
if (idx := props.get("index")) is not None:
entry["index"] = idx
out.append(entry)
elif file_id:
entry = {
"type": "file_path",
"file_id": file_id,
}
if (idx := props.get("index")) is not None:
entry["index"] = idx
out.append(entry)
return out


# endregion


# region ResponsesClient


Expand Down Expand Up @@ -1374,7 +1440,7 @@ def _prepare_content_for_openai(
return {
"type": "output_text",
"text": content.text,
"annotations": [],
"annotations": _annotations_to_output_text(getattr(content, "annotations", None)),
}
return {
"type": "input_text",
Expand Down Expand Up @@ -1522,6 +1588,13 @@ def _prepare_content_for_openai(
"approve": content.approved,
}
case "hosted_file":
# `input_file` is an input-only content type in the Responses API and is rejected
# inside an assistant message. Hosted-file content on an assistant message
# represents a citation produced by a hosted tool (e.g., file_search) and cannot be
# meaningfully replayed as input — drop it. The accompanying text annotations carry
# the citation context for round-tripping.
if role == "assistant":
return {}
return {
"type": "input_file",
"file_id": content.file_id,
Expand Down Expand Up @@ -2502,45 +2575,63 @@ def _get_ann_value(key: str) -> Any:

ann_type = _get_ann_value("type")
ann_file_id = _get_ann_value("file_id")
# Hosted-file citations attach as text annotations (matching the non-streaming path)
# so they don't roundtrip as standalone `input_file` items in assistant history.
if ann_type == "file_path":
if ann_file_id:
annotation_obj = Annotation(
type="citation",
file_id=str(ann_file_id),
additional_properties={
"annotation_index": event.annotation_index,
"index": _get_ann_value("index"),
},
raw_representation=annotation,
)
contents.append(
Content.from_hosted_file(
file_id=str(ann_file_id),
additional_properties={
"annotation_index": event.annotation_index,
"index": _get_ann_value("index"),
},
raw_representation=event,
)
Content.from_text(text="", annotations=[annotation_obj], raw_representation=event)
Comment thread
moonbox3 marked this conversation as resolved.
)
elif ann_type == "file_citation":
if ann_file_id:
ann_filename = _get_ann_value("filename")
annotation_obj = Annotation(
type="citation",
file_id=str(ann_file_id),
url=ann_filename,
additional_properties={
"annotation_index": event.annotation_index,
"index": _get_ann_value("index"),
},
raw_representation=annotation,
)
contents.append(
Content.from_hosted_file(
file_id=str(ann_file_id),
additional_properties={
"annotation_index": event.annotation_index,
"filename": _get_ann_value("filename"),
"index": _get_ann_value("index"),
},
raw_representation=event,
)
Content.from_text(text="", annotations=[annotation_obj], raw_representation=event)
)
elif ann_type == "container_file_citation":
if ann_file_id:
ann_filename = _get_ann_value("filename")
ann_start = _get_ann_value("start_index")
ann_end = _get_ann_value("end_index")
annotation_obj = Annotation(
type="citation",
file_id=str(ann_file_id),
url=ann_filename,
additional_properties={
"annotation_index": event.annotation_index,
"container_id": _get_ann_value("container_id"),
},
raw_representation=annotation,
)
if ann_start is not None and ann_end is not None:
annotation_obj["annotated_regions"] = [
TextSpanRegion(
type="text_span",
start_index=ann_start,
end_index=ann_end,
)
]
contents.append(
Content.from_hosted_file(
file_id=str(ann_file_id),
additional_properties={
"annotation_index": event.annotation_index,
"container_id": _get_ann_value("container_id"),
"filename": _get_ann_value("filename"),
"start_index": _get_ann_value("start_index"),
"end_index": _get_ann_value("end_index"),
},
raw_representation=event,
)
Content.from_text(text="", annotations=[annotation_obj], raw_representation=event)
)
elif ann_type == "url_citation":
ann_url = _get_ann_value("url")
Expand Down
Loading
Loading