agent-framework-hosting-responses 1.0.0a260709__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agent_framework_hosting_responses-1.0.0a260709/LICENSE +21 -0
- agent_framework_hosting_responses-1.0.0a260709/PKG-INFO +78 -0
- agent_framework_hosting_responses-1.0.0a260709/README.md +51 -0
- agent_framework_hosting_responses-1.0.0a260709/agent_framework_hosting_responses/__init__.py +29 -0
- agent_framework_hosting_responses-1.0.0a260709/agent_framework_hosting_responses/_parsing.py +938 -0
- agent_framework_hosting_responses-1.0.0a260709/pyproject.toml +86 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Microsoft Corporation.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agent-framework-hosting-responses
|
|
3
|
+
Version: 1.0.0a260709
|
|
4
|
+
Summary: OpenAI Responses-shaped helpers for agent-framework-hosting.
|
|
5
|
+
Author-email: Microsoft <af-support@microsoft.com>
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Typing :: Typed
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: agent-framework-core>=1.11.0,<2
|
|
20
|
+
Requires-Dist: agent-framework-hosting==1.0.0a260709
|
|
21
|
+
Requires-Dist: openai>=1.99.0,<3
|
|
22
|
+
Project-URL: homepage, https://aka.ms/agent-framework
|
|
23
|
+
Project-URL: issues, https://github.com/microsoft/agent-framework/issues
|
|
24
|
+
Project-URL: release_notes, https://github.com/microsoft/agent-framework/releases?q=tag%3Apython-1&expanded=true
|
|
25
|
+
Project-URL: source, https://github.com/microsoft/agent-framework/tree/main/python
|
|
26
|
+
|
|
27
|
+
# agent-framework-hosting-responses
|
|
28
|
+
|
|
29
|
+
OpenAI Responses-shaped helpers for app-owned Agent Framework hosting.
|
|
30
|
+
|
|
31
|
+
This package provides the Responses-specific conversion layer:
|
|
32
|
+
|
|
33
|
+
- `responses_to_run(...)` — convert a Responses request body into Agent
|
|
34
|
+
Framework run values.
|
|
35
|
+
- `responses_session_id(...)` — extract a prior `resp_*` response id or
|
|
36
|
+
`conv_*` conversation id from the request body when present.
|
|
37
|
+
- `create_response_id(...)` — mint a Responses-shaped response id.
|
|
38
|
+
- `responses_from_run(...)` — convert an `AgentResponse` into a
|
|
39
|
+
Responses-compatible JSON payload.
|
|
40
|
+
- `responses_from_streaming_run(...)` — convert an Agent Framework
|
|
41
|
+
`ResponseStream` into Responses-compatible SSE events.
|
|
42
|
+
|
|
43
|
+
FastAPI/Starlette/Django/Azure Functions code owns route registration,
|
|
44
|
+
authentication, status codes, response construction, and background work.
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from agent_framework_hosting import AgentState
|
|
48
|
+
from agent_framework_hosting_responses import (
|
|
49
|
+
create_response_id,
|
|
50
|
+
responses_from_run,
|
|
51
|
+
responses_session_id,
|
|
52
|
+
responses_to_run,
|
|
53
|
+
)
|
|
54
|
+
from fastapi import Body, FastAPI
|
|
55
|
+
from fastapi.responses import JSONResponse
|
|
56
|
+
|
|
57
|
+
app = FastAPI()
|
|
58
|
+
state = AgentState(agent)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@app.post("/responses")
|
|
62
|
+
async def responses(body: dict = Body(...)) -> JSONResponse:
|
|
63
|
+
run = responses_to_run(body)
|
|
64
|
+
session_id = responses_session_id(body)
|
|
65
|
+
response_id = create_response_id()
|
|
66
|
+
session = await state.get_or_create_session(session_id or response_id)
|
|
67
|
+
result = await (await state.get_target()).run(
|
|
68
|
+
run["messages"],
|
|
69
|
+
session=session,
|
|
70
|
+
options=run["options"],
|
|
71
|
+
)
|
|
72
|
+
await state.set_session(response_id, session)
|
|
73
|
+
return JSONResponse(responses_from_run(result, response_id=response_id, session_id=session_id))
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The base execution-state helpers live in
|
|
77
|
+
[`agent-framework-hosting`](https://pypi.org/project/agent-framework-hosting/).
|
|
78
|
+
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# agent-framework-hosting-responses
|
|
2
|
+
|
|
3
|
+
OpenAI Responses-shaped helpers for app-owned Agent Framework hosting.
|
|
4
|
+
|
|
5
|
+
This package provides the Responses-specific conversion layer:
|
|
6
|
+
|
|
7
|
+
- `responses_to_run(...)` — convert a Responses request body into Agent
|
|
8
|
+
Framework run values.
|
|
9
|
+
- `responses_session_id(...)` — extract a prior `resp_*` response id or
|
|
10
|
+
`conv_*` conversation id from the request body when present.
|
|
11
|
+
- `create_response_id(...)` — mint a Responses-shaped response id.
|
|
12
|
+
- `responses_from_run(...)` — convert an `AgentResponse` into a
|
|
13
|
+
Responses-compatible JSON payload.
|
|
14
|
+
- `responses_from_streaming_run(...)` — convert an Agent Framework
|
|
15
|
+
`ResponseStream` into Responses-compatible SSE events.
|
|
16
|
+
|
|
17
|
+
FastAPI/Starlette/Django/Azure Functions code owns route registration,
|
|
18
|
+
authentication, status codes, response construction, and background work.
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from agent_framework_hosting import AgentState
|
|
22
|
+
from agent_framework_hosting_responses import (
|
|
23
|
+
create_response_id,
|
|
24
|
+
responses_from_run,
|
|
25
|
+
responses_session_id,
|
|
26
|
+
responses_to_run,
|
|
27
|
+
)
|
|
28
|
+
from fastapi import Body, FastAPI
|
|
29
|
+
from fastapi.responses import JSONResponse
|
|
30
|
+
|
|
31
|
+
app = FastAPI()
|
|
32
|
+
state = AgentState(agent)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@app.post("/responses")
|
|
36
|
+
async def responses(body: dict = Body(...)) -> JSONResponse:
|
|
37
|
+
run = responses_to_run(body)
|
|
38
|
+
session_id = responses_session_id(body)
|
|
39
|
+
response_id = create_response_id()
|
|
40
|
+
session = await state.get_or_create_session(session_id or response_id)
|
|
41
|
+
result = await (await state.get_target()).run(
|
|
42
|
+
run["messages"],
|
|
43
|
+
session=session,
|
|
44
|
+
options=run["options"],
|
|
45
|
+
)
|
|
46
|
+
await state.set_session(response_id, session)
|
|
47
|
+
return JSONResponse(responses_from_run(result, response_id=response_id, session_id=session_id))
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The base execution-state helpers live in
|
|
51
|
+
[`agent-framework-hosting`](https://pypi.org/project/agent-framework-hosting/).
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Copyright (c) Microsoft. All rights reserved.
|
|
2
|
+
|
|
3
|
+
"""OpenAI Responses-shaped helpers for app-owned Agent Framework hosting."""
|
|
4
|
+
|
|
5
|
+
import importlib.metadata
|
|
6
|
+
|
|
7
|
+
from ._parsing import (
|
|
8
|
+
create_response_id,
|
|
9
|
+
messages_from_responses_input,
|
|
10
|
+
responses_from_run,
|
|
11
|
+
responses_from_streaming_run,
|
|
12
|
+
responses_session_id,
|
|
13
|
+
responses_to_run,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
__version__ = importlib.metadata.version(__name__)
|
|
18
|
+
except importlib.metadata.PackageNotFoundError:
|
|
19
|
+
__version__ = "0.0.0"
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"__version__",
|
|
23
|
+
"create_response_id",
|
|
24
|
+
"messages_from_responses_input",
|
|
25
|
+
"responses_from_run",
|
|
26
|
+
"responses_from_streaming_run",
|
|
27
|
+
"responses_session_id",
|
|
28
|
+
"responses_to_run",
|
|
29
|
+
]
|
|
@@ -0,0 +1,938 @@
|
|
|
1
|
+
# Copyright (c) Microsoft. All rights reserved.
|
|
2
|
+
|
|
3
|
+
"""Parsing helpers for the OpenAI Responses-API request body.
|
|
4
|
+
|
|
5
|
+
The Responses API accepts ``input`` as either a string or a list of "input
|
|
6
|
+
items". An item is either a content part (``input_text`` / ``input_image``
|
|
7
|
+
/ ``input_file``) or a message envelope ``{type: "message", role,
|
|
8
|
+
content: [...]}``. We translate that into an Agent Framework ``Message``
|
|
9
|
+
list and remap the generation-control fields the API also carries into
|
|
10
|
+
``ChatOptions``-shaped keys. App-owned route code decides which options to
|
|
11
|
+
pass through to ``agent.run(...)`` and which request-owned fields to drop.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import time
|
|
18
|
+
import uuid
|
|
19
|
+
from collections.abc import AsyncIterator, Mapping, Sequence
|
|
20
|
+
from typing import Any, cast
|
|
21
|
+
|
|
22
|
+
from agent_framework import AgentResponse, AgentResponseUpdate, ChatOptions, Content, Message, ResponseStream
|
|
23
|
+
from agent_framework_hosting import AgentRunArgs
|
|
24
|
+
from openai.types.responses import (
|
|
25
|
+
Response as OpenAIResponse,
|
|
26
|
+
)
|
|
27
|
+
from openai.types.responses import (
|
|
28
|
+
ResponseFunctionToolCall,
|
|
29
|
+
ResponseFunctionToolCallOutputItem,
|
|
30
|
+
ResponseInputFile,
|
|
31
|
+
ResponseInputImage,
|
|
32
|
+
ResponseInputText,
|
|
33
|
+
ResponseOutputItem,
|
|
34
|
+
ResponseOutputMessage,
|
|
35
|
+
ResponseOutputText,
|
|
36
|
+
)
|
|
37
|
+
from pydantic import TypeAdapter, ValidationError
|
|
38
|
+
|
|
39
|
+
_RESPONSE_OUTPUT_ITEM_ADAPTER: TypeAdapter[Any] = TypeAdapter(ResponseOutputItem)
|
|
40
|
+
|
|
41
|
+
# OpenAI Responses field name → Agent Framework ChatOptions field name.
|
|
42
|
+
_RESPONSES_OPTION_REMAP = {
|
|
43
|
+
"max_output_tokens": "max_tokens",
|
|
44
|
+
"parallel_tool_calls": "allow_multiple_tool_calls",
|
|
45
|
+
}
|
|
46
|
+
# Fields the Responses transport owns; they are consumed separately and must
|
|
47
|
+
# not also appear in options.
|
|
48
|
+
_RESPONSES_RUN_TRANSPORT_KEYS = frozenset({"input", "stream", "previous_response_id", "conversation_id"})
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _content_from_input_item(item: Mapping[str, Any]) -> Content:
|
|
52
|
+
"""Convert a single OpenAI Responses ``input`` item into a :class:`Content` part.
|
|
53
|
+
|
|
54
|
+
Handles the ``input_text``/``output_text``/``text`` text variants,
|
|
55
|
+
``input_image`` URL references, and ``input_file`` references via either
|
|
56
|
+
a public URL or a hosted ``file_id``. Raises ``ValueError`` for any
|
|
57
|
+
unsupported item type so the surrounding parser can return a 422.
|
|
58
|
+
"""
|
|
59
|
+
item_type = item.get("type")
|
|
60
|
+
if item_type in ("input_text", "output_text", "text"):
|
|
61
|
+
return Content.from_text(text=str(item.get("text", "")))
|
|
62
|
+
if item_type == "input_image":
|
|
63
|
+
image_url: Any = item.get("image_url")
|
|
64
|
+
if isinstance(image_url, Mapping):
|
|
65
|
+
image_url = cast("Mapping[str, Any]", image_url).get("url")
|
|
66
|
+
if not isinstance(image_url, str):
|
|
67
|
+
raise ValueError("input_image requires `image_url`")
|
|
68
|
+
return Content.from_uri(uri=image_url, media_type="image/*")
|
|
69
|
+
if item_type == "input_file":
|
|
70
|
+
if (uri := item.get("file_url")) and isinstance(uri, str):
|
|
71
|
+
return Content.from_uri(uri=uri, media_type=item.get("mime_type"))
|
|
72
|
+
if file_id := item.get("file_id"):
|
|
73
|
+
return Content(type="hosted_file", file_id=str(file_id))
|
|
74
|
+
raise ValueError("input_file requires `file_url` or `file_id`")
|
|
75
|
+
raise ValueError(f"Unsupported Responses input content type: {item_type!r}")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def messages_from_responses_input(value: Any) -> list[Message]:
|
|
79
|
+
"""Translate ``input`` (string or list of items) into :class:`Message` objects."""
|
|
80
|
+
if isinstance(value, str):
|
|
81
|
+
return [Message("user", [Content.from_text(text=value)])]
|
|
82
|
+
if not isinstance(value, list) or not value:
|
|
83
|
+
raise ValueError("`input` must be a non-empty string or list")
|
|
84
|
+
|
|
85
|
+
messages: list[Message] = []
|
|
86
|
+
pending_user_parts: list[Content] = []
|
|
87
|
+
|
|
88
|
+
def flush() -> None:
|
|
89
|
+
"""Emit any buffered loose user content as a single user message."""
|
|
90
|
+
if pending_user_parts:
|
|
91
|
+
messages.append(Message("user", list(pending_user_parts)))
|
|
92
|
+
pending_user_parts.clear()
|
|
93
|
+
|
|
94
|
+
for item in cast("list[Any]", value):
|
|
95
|
+
if not isinstance(item, Mapping):
|
|
96
|
+
raise ValueError("each `input` item must be an object")
|
|
97
|
+
item_map = cast("Mapping[str, Any]", item)
|
|
98
|
+
if item_map.get("type") == "message":
|
|
99
|
+
flush()
|
|
100
|
+
role = str(item_map.get("role") or "user")
|
|
101
|
+
content: Any = item_map.get("content") or []
|
|
102
|
+
parts: list[Content]
|
|
103
|
+
if isinstance(content, str):
|
|
104
|
+
parts = [Content.from_text(text=content)]
|
|
105
|
+
elif isinstance(content, list):
|
|
106
|
+
parts = []
|
|
107
|
+
for content_item in cast("list[Any]", content):
|
|
108
|
+
if not isinstance(content_item, Mapping):
|
|
109
|
+
raise ValueError("each message `content` item must be an object")
|
|
110
|
+
parts.append(_content_from_input_item(cast("Mapping[str, Any]", content_item)))
|
|
111
|
+
else:
|
|
112
|
+
raise ValueError("message `content` must be a string or list")
|
|
113
|
+
messages.append(Message(role, parts))
|
|
114
|
+
else:
|
|
115
|
+
pending_user_parts.append(_content_from_input_item(item_map))
|
|
116
|
+
|
|
117
|
+
flush()
|
|
118
|
+
if not messages:
|
|
119
|
+
raise ValueError("`input` produced no messages")
|
|
120
|
+
return messages
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def create_response_id() -> str:
|
|
124
|
+
"""Create a Responses-shaped response id."""
|
|
125
|
+
return f"resp_{uuid.uuid4().hex}"
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def responses_session_id(body: Mapping[str, Any]) -> str | None:
|
|
129
|
+
"""Return the Responses session id from request body, if present.
|
|
130
|
+
|
|
131
|
+
The returned value can be a ``resp_*`` previous response id or a ``conv_*``
|
|
132
|
+
conversation id. Callers choose whether this request-derived value is
|
|
133
|
+
trusted for their route and deployment.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
body: OpenAI Responses-shaped request body.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
Previous response id, conversation id, or ``None``.
|
|
140
|
+
"""
|
|
141
|
+
previous_response_id = body.get("previous_response_id")
|
|
142
|
+
if isinstance(previous_response_id, str) and previous_response_id:
|
|
143
|
+
return previous_response_id
|
|
144
|
+
conversation_id = body.get("conversation_id")
|
|
145
|
+
if isinstance(conversation_id, str) and conversation_id:
|
|
146
|
+
return conversation_id
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def responses_to_run(body: Mapping[str, Any]) -> AgentRunArgs:
|
|
151
|
+
"""Convert a Responses request body into Agent Framework run values.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
body: OpenAI Responses-shaped request body.
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
Arguments corresponding to ``Agent.run``.
|
|
158
|
+
|
|
159
|
+
Raises:
|
|
160
|
+
ValueError: If the request body has invalid ``input``.
|
|
161
|
+
"""
|
|
162
|
+
messages = messages_from_responses_input(body.get("input"))
|
|
163
|
+
options: dict[str, Any] = {}
|
|
164
|
+
for key, value in body.items():
|
|
165
|
+
if key in _RESPONSES_RUN_TRANSPORT_KEYS or value is None:
|
|
166
|
+
continue
|
|
167
|
+
options[_RESPONSES_OPTION_REMAP.get(key, key)] = value
|
|
168
|
+
return AgentRunArgs(
|
|
169
|
+
messages=messages,
|
|
170
|
+
options=cast("ChatOptions[Any]", options),
|
|
171
|
+
stream=bool(body.get("stream", False)),
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def responses_from_run(
|
|
176
|
+
result: AgentResponse[Any],
|
|
177
|
+
*,
|
|
178
|
+
response_id: str,
|
|
179
|
+
session_id: str | None = None,
|
|
180
|
+
) -> dict[str, Any]:
|
|
181
|
+
"""Convert an Agent Framework response into a Responses payload.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
result: Agent response returned by a run.
|
|
185
|
+
|
|
186
|
+
Keyword Args:
|
|
187
|
+
response_id: Id for the response being created.
|
|
188
|
+
session_id: Optional prior ``resp_*`` or ``conv_*`` session id. When it
|
|
189
|
+
is a conversation id, the helper renders it in the Responses
|
|
190
|
+
conversation field.
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
Responses-compatible JSON payload.
|
|
194
|
+
"""
|
|
195
|
+
output_items = _result_to_output_items(result, status="completed")
|
|
196
|
+
response_kwargs: dict[str, Any] = {
|
|
197
|
+
"id": response_id,
|
|
198
|
+
"object": "response",
|
|
199
|
+
"created_at": int(time.time()),
|
|
200
|
+
"status": "completed",
|
|
201
|
+
"model": _model_from_result(result),
|
|
202
|
+
"output": output_items,
|
|
203
|
+
"parallel_tool_calls": False,
|
|
204
|
+
"tool_choice": "auto",
|
|
205
|
+
"tools": [],
|
|
206
|
+
"metadata": {},
|
|
207
|
+
}
|
|
208
|
+
if session_id is not None and session_id.startswith("conv_"):
|
|
209
|
+
response_kwargs["conversation"] = {"id": session_id}
|
|
210
|
+
return _response_payload(OpenAIResponse(**response_kwargs))
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _model_from_update(update: AgentResponseUpdate) -> str | None:
|
|
214
|
+
"""Best-effort model id from one streamed update's raw representation.
|
|
215
|
+
|
|
216
|
+
``AgentResponse.from_updates`` does not carry a chunk's raw representation
|
|
217
|
+
forward onto the finalized response (see ``_finalize_response`` in core),
|
|
218
|
+
so ``_model_from_result`` can never find a model for a streamed result.
|
|
219
|
+
Each ``AgentResponseUpdate`` still has its own raw chat chunk, which
|
|
220
|
+
usually reports the model, so the streaming SSE helper captures it here
|
|
221
|
+
instead.
|
|
222
|
+
"""
|
|
223
|
+
raw = update.raw_representation
|
|
224
|
+
model = getattr(raw, "model", None)
|
|
225
|
+
return model if isinstance(model, str) and model else None
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _model_from_result(result: Any) -> str:
|
|
229
|
+
model = getattr(result, "model", None)
|
|
230
|
+
if isinstance(model, str) and model:
|
|
231
|
+
return model
|
|
232
|
+
raw = getattr(result, "raw_representation", None)
|
|
233
|
+
raw_model = getattr(raw, "model", None)
|
|
234
|
+
if isinstance(raw_model, str) and raw_model:
|
|
235
|
+
return raw_model
|
|
236
|
+
additional_properties = getattr(result, "additional_properties", None)
|
|
237
|
+
if isinstance(additional_properties, Mapping):
|
|
238
|
+
additional_model = cast(Mapping[str, Any], additional_properties).get("model")
|
|
239
|
+
if isinstance(additional_model, str) and additional_model:
|
|
240
|
+
return additional_model
|
|
241
|
+
return "agent"
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _result_to_output_items(result: Any, *, status: str) -> list[ResponseOutputItem]:
|
|
245
|
+
"""Render an agent or workflow result as Responses output items."""
|
|
246
|
+
messages = getattr(result, "messages", None)
|
|
247
|
+
if isinstance(messages, Sequence) and not isinstance(messages, (str, bytes, bytearray)):
|
|
248
|
+
return _messages_to_output_items(cast("Sequence[Any]", messages), status=status)
|
|
249
|
+
|
|
250
|
+
if isinstance(result, Message):
|
|
251
|
+
return _messages_to_output_items([result], status=status)
|
|
252
|
+
if isinstance(result, Content):
|
|
253
|
+
return _contents_to_output_items([result], status=status)
|
|
254
|
+
|
|
255
|
+
get_outputs = getattr(result, "get_outputs", None)
|
|
256
|
+
if callable(get_outputs):
|
|
257
|
+
output_items: list[ResponseOutputItem] = []
|
|
258
|
+
for output in cast("Sequence[Any]", get_outputs()):
|
|
259
|
+
output_items.extend(_output_to_output_items(output, status=status))
|
|
260
|
+
return output_items
|
|
261
|
+
|
|
262
|
+
text = getattr(result, "text", None)
|
|
263
|
+
if isinstance(text, str):
|
|
264
|
+
return _text_output_items(text, status=status)
|
|
265
|
+
return _text_output_items(_result_to_text(result), status=status)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _output_to_output_items(output: Any, *, status: str) -> list[ResponseOutputItem]:
|
|
269
|
+
if isinstance(output, Message):
|
|
270
|
+
return _messages_to_output_items([output], status=status)
|
|
271
|
+
if isinstance(output, Content):
|
|
272
|
+
return _contents_to_output_items([output], status=status)
|
|
273
|
+
messages = getattr(output, "messages", None)
|
|
274
|
+
if isinstance(messages, Sequence) and not isinstance(messages, (str, bytes, bytearray)):
|
|
275
|
+
return _messages_to_output_items(cast("Sequence[Any]", messages), status=status)
|
|
276
|
+
text = getattr(output, "text", None)
|
|
277
|
+
if isinstance(text, str):
|
|
278
|
+
return _text_output_items(text, status=status)
|
|
279
|
+
return _text_output_items(str(output), status=status)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _messages_to_output_items(messages: Sequence[Any], *, status: str) -> list[ResponseOutputItem]:
|
|
283
|
+
output_items: list[ResponseOutputItem] = []
|
|
284
|
+
message_contents: list[Content] = []
|
|
285
|
+
|
|
286
|
+
for message in messages:
|
|
287
|
+
if not isinstance(message, Message):
|
|
288
|
+
if message_contents:
|
|
289
|
+
output_items.extend(_contents_to_output_items(message_contents, status=status))
|
|
290
|
+
message_contents.clear()
|
|
291
|
+
output_items.extend(_output_to_output_items(message, status=status))
|
|
292
|
+
continue
|
|
293
|
+
message_contents.extend(message.contents)
|
|
294
|
+
|
|
295
|
+
if message_contents:
|
|
296
|
+
output_items.extend(_contents_to_output_items(message_contents, status=status))
|
|
297
|
+
|
|
298
|
+
return output_items
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _contents_to_output_items(
|
|
302
|
+
contents: Sequence[Content],
|
|
303
|
+
*,
|
|
304
|
+
status: str,
|
|
305
|
+
seen_raw_items: dict[tuple[str, str], int] | None = None,
|
|
306
|
+
) -> list[ResponseOutputItem]:
|
|
307
|
+
output_items: list[ResponseOutputItem] = []
|
|
308
|
+
message_content: list[Any] = []
|
|
309
|
+
seen: dict[tuple[str, str], int] = seen_raw_items if seen_raw_items is not None else {}
|
|
310
|
+
|
|
311
|
+
def flush_message() -> None:
|
|
312
|
+
if not message_content:
|
|
313
|
+
return
|
|
314
|
+
output_items.append(_message_output_item(message_content, status=status))
|
|
315
|
+
message_content.clear()
|
|
316
|
+
|
|
317
|
+
content_list = list(contents)
|
|
318
|
+
index = 0
|
|
319
|
+
while index < len(content_list):
|
|
320
|
+
content = content_list[index]
|
|
321
|
+
raw_item = _raw_response_output_item(content.raw_representation)
|
|
322
|
+
if raw_item is not None:
|
|
323
|
+
raw_key = _response_output_item_key(raw_item)
|
|
324
|
+
if raw_key in seen:
|
|
325
|
+
output_items[seen[raw_key]] = raw_item
|
|
326
|
+
else:
|
|
327
|
+
flush_message()
|
|
328
|
+
seen[raw_key] = len(output_items)
|
|
329
|
+
output_items.append(raw_item)
|
|
330
|
+
index += 1
|
|
331
|
+
continue
|
|
332
|
+
|
|
333
|
+
next_content = content_list[index + 1] if index + 1 < len(content_list) else None
|
|
334
|
+
if _is_matching_code_interpreter_result(content, next_content):
|
|
335
|
+
flush_message()
|
|
336
|
+
output_items.append(_code_interpreter_output_item(content, status=status, result_content=next_content))
|
|
337
|
+
index += 2
|
|
338
|
+
continue
|
|
339
|
+
if _is_matching_image_generation_result(content, next_content):
|
|
340
|
+
flush_message()
|
|
341
|
+
output_items.append(_image_generation_output_item(content, status=status, result_content=next_content))
|
|
342
|
+
index += 2
|
|
343
|
+
continue
|
|
344
|
+
if _is_matching_mcp_result(content, next_content):
|
|
345
|
+
flush_message()
|
|
346
|
+
output_items.append(_mcp_call_output_item(content, status=status, result_content=next_content))
|
|
347
|
+
index += 2
|
|
348
|
+
continue
|
|
349
|
+
|
|
350
|
+
match content.type:
|
|
351
|
+
case "text":
|
|
352
|
+
message_content.append(_message_text_content(content))
|
|
353
|
+
case "text_reasoning":
|
|
354
|
+
flush_message()
|
|
355
|
+
output_items.append(_reasoning_output_item(content, status=status))
|
|
356
|
+
case "function_call":
|
|
357
|
+
flush_message()
|
|
358
|
+
output_items.append(_function_call_output_item(content, status=status))
|
|
359
|
+
case "function_result":
|
|
360
|
+
flush_message()
|
|
361
|
+
output_items.append(_function_result_output_item(content, status=status))
|
|
362
|
+
case "code_interpreter_tool_call" | "code_interpreter_tool_result":
|
|
363
|
+
flush_message()
|
|
364
|
+
output_items.append(_code_interpreter_output_item(content, status=status))
|
|
365
|
+
case "image_generation_tool_call" | "image_generation_tool_result":
|
|
366
|
+
flush_message()
|
|
367
|
+
output_items.append(_image_generation_output_item(content, status=status))
|
|
368
|
+
case "mcp_server_tool_call":
|
|
369
|
+
flush_message()
|
|
370
|
+
output_items.append(_mcp_call_output_item(content, status=status))
|
|
371
|
+
case "mcp_server_tool_result":
|
|
372
|
+
flush_message()
|
|
373
|
+
output_items.append(_mcp_result_output_item(content, status=status))
|
|
374
|
+
case "shell_tool_call":
|
|
375
|
+
flush_message()
|
|
376
|
+
output_items.append(_shell_call_output_item(content, status=status))
|
|
377
|
+
case "shell_tool_result":
|
|
378
|
+
flush_message()
|
|
379
|
+
output_items.append(_shell_result_output_item(content, status=status))
|
|
380
|
+
case "function_approval_request":
|
|
381
|
+
flush_message()
|
|
382
|
+
output_items.append(_function_approval_request_output_item(content))
|
|
383
|
+
case "function_approval_response":
|
|
384
|
+
flush_message()
|
|
385
|
+
output_items.append(_function_approval_response_output_item(content))
|
|
386
|
+
case "data" | "uri" | "hosted_file":
|
|
387
|
+
flush_message()
|
|
388
|
+
output_items.append(_media_content_output_item(content, status=status))
|
|
389
|
+
case "error":
|
|
390
|
+
message_content.append(ResponseOutputText(type="output_text", text=str(content), annotations=[]))
|
|
391
|
+
case _:
|
|
392
|
+
flush_message()
|
|
393
|
+
output_items.extend(_text_output_items(json.dumps(content.to_dict(), default=str), status=status))
|
|
394
|
+
index += 1
|
|
395
|
+
|
|
396
|
+
flush_message()
|
|
397
|
+
return output_items
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def _is_matching_code_interpreter_result(content: Content, next_content: Content | None) -> bool:
|
|
401
|
+
return (
|
|
402
|
+
content.type == "code_interpreter_tool_call"
|
|
403
|
+
and next_content is not None
|
|
404
|
+
and next_content.type == "code_interpreter_tool_result"
|
|
405
|
+
and content.call_id == next_content.call_id
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def _is_matching_image_generation_result(content: Content, next_content: Content | None) -> bool:
|
|
410
|
+
return (
|
|
411
|
+
content.type == "image_generation_tool_call"
|
|
412
|
+
and next_content is not None
|
|
413
|
+
and next_content.type == "image_generation_tool_result"
|
|
414
|
+
and content.image_id == next_content.image_id
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _is_matching_mcp_result(content: Content, next_content: Content | None) -> bool:
|
|
419
|
+
return (
|
|
420
|
+
content.type == "mcp_server_tool_call"
|
|
421
|
+
and next_content is not None
|
|
422
|
+
and next_content.type == "mcp_server_tool_result"
|
|
423
|
+
and content.call_id == next_content.call_id
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def _message_status(status: str) -> str:
|
|
428
|
+
return status if status in ("in_progress", "completed", "incomplete") else "incomplete"
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def _text_output_items(text: str, *, status: str, message_id: str | None = None) -> list[ResponseOutputItem]:
|
|
432
|
+
return [
|
|
433
|
+
_message_output_item(
|
|
434
|
+
[ResponseOutputText(type="output_text", text=text, annotations=[])],
|
|
435
|
+
status=status,
|
|
436
|
+
message_id=message_id,
|
|
437
|
+
)
|
|
438
|
+
]
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def _message_output_item(content: Sequence[Any], *, status: str, message_id: str | None = None) -> ResponseOutputItem:
|
|
442
|
+
return cast(
|
|
443
|
+
ResponseOutputItem,
|
|
444
|
+
ResponseOutputMessage(
|
|
445
|
+
id=message_id or f"msg_{uuid.uuid4().hex}",
|
|
446
|
+
type="message",
|
|
447
|
+
role="assistant",
|
|
448
|
+
status=_message_status(status), # type: ignore[arg-type]
|
|
449
|
+
content=list(content),
|
|
450
|
+
),
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def _message_text_content(content: Content) -> Any:
|
|
455
|
+
raw_type = _raw_type(content.raw_representation)
|
|
456
|
+
if raw_type in ("output_text", "refusal"):
|
|
457
|
+
return content.raw_representation
|
|
458
|
+
return ResponseOutputText(type="output_text", text=content.text or "", annotations=[])
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def _reasoning_output_item(content: Content, *, status: str) -> ResponseOutputItem:
|
|
462
|
+
item_data: dict[str, Any] = {
|
|
463
|
+
"id": content.id or f"rs_{uuid.uuid4().hex}",
|
|
464
|
+
"type": "reasoning",
|
|
465
|
+
"summary": [],
|
|
466
|
+
"status": _message_status(status),
|
|
467
|
+
}
|
|
468
|
+
if content.text:
|
|
469
|
+
item_data["content"] = [{"type": "reasoning_text", "text": content.text}]
|
|
470
|
+
if content.protected_data:
|
|
471
|
+
item_data["encrypted_content"] = content.protected_data
|
|
472
|
+
return _response_output_item(item_data)
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def _function_call_output_item(content: Content, *, status: str) -> ResponseOutputItem:
|
|
476
|
+
return cast(
|
|
477
|
+
ResponseOutputItem,
|
|
478
|
+
ResponseFunctionToolCall(
|
|
479
|
+
id=content.additional_properties.get("fc_id") if content.additional_properties else None,
|
|
480
|
+
type="function_call",
|
|
481
|
+
call_id=content.call_id or f"call_{uuid.uuid4().hex}",
|
|
482
|
+
name=content.name or "tool",
|
|
483
|
+
arguments=_arguments_to_str(content.arguments),
|
|
484
|
+
status=_message_status(status), # type: ignore[arg-type]
|
|
485
|
+
),
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def _function_result_output_item(content: Content, *, status: str) -> ResponseOutputItem:
|
|
490
|
+
if content.exception:
|
|
491
|
+
output: str | list[Any] = content.exception
|
|
492
|
+
elif output_parts := _content_parts_to_input_items(content.items):
|
|
493
|
+
output = output_parts
|
|
494
|
+
elif isinstance(content.result, str):
|
|
495
|
+
output = content.result
|
|
496
|
+
elif content.result is None:
|
|
497
|
+
output = ""
|
|
498
|
+
else:
|
|
499
|
+
output = json.dumps(content.result, default=str)
|
|
500
|
+
return cast(
|
|
501
|
+
ResponseOutputItem,
|
|
502
|
+
ResponseFunctionToolCallOutputItem(
|
|
503
|
+
id=f"fcout_{uuid.uuid4().hex}",
|
|
504
|
+
type="function_call_output",
|
|
505
|
+
call_id=content.call_id or f"call_{uuid.uuid4().hex}",
|
|
506
|
+
output=output,
|
|
507
|
+
status=_message_status(status), # type: ignore[arg-type]
|
|
508
|
+
),
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def _code_interpreter_output_item(
|
|
513
|
+
content: Content,
|
|
514
|
+
*,
|
|
515
|
+
status: str,
|
|
516
|
+
result_content: Content | None = None,
|
|
517
|
+
) -> ResponseOutputItem:
|
|
518
|
+
output_parts: list[dict[str, Any]] = []
|
|
519
|
+
outputs_value: Any = result_content.outputs if result_content is not None else content.outputs
|
|
520
|
+
if isinstance(outputs_value, Sequence) and not isinstance(outputs_value, (str, bytes, bytearray)):
|
|
521
|
+
for item in cast(Sequence[Any], outputs_value):
|
|
522
|
+
if isinstance(item, Content) and item.type == "text":
|
|
523
|
+
output_parts.append({"type": "logs", "logs": item.text or ""})
|
|
524
|
+
elif isinstance(item, Content) and item.type in ("data", "uri") and item.uri:
|
|
525
|
+
output_parts.append({"type": "image", "url": item.uri})
|
|
526
|
+
|
|
527
|
+
return _response_output_item({
|
|
528
|
+
"id": _content_item_id(content, result_content) or f"ci_{uuid.uuid4().hex}",
|
|
529
|
+
"type": "code_interpreter_call",
|
|
530
|
+
"code": _content_sequence_text(content.inputs),
|
|
531
|
+
"container_id": str(_content_property(content, result_content, "container_id") or "agent_framework"),
|
|
532
|
+
"outputs": output_parts or None,
|
|
533
|
+
"status": _code_interpreter_status(status),
|
|
534
|
+
})
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
def _image_generation_output_item(
|
|
538
|
+
content: Content,
|
|
539
|
+
*,
|
|
540
|
+
status: str,
|
|
541
|
+
result_content: Content | None = None,
|
|
542
|
+
) -> ResponseOutputItem:
|
|
543
|
+
result_source = result_content.outputs if result_content is not None else content.outputs
|
|
544
|
+
image_id = content.image_id or (result_content.image_id if result_content is not None else None)
|
|
545
|
+
return _response_output_item({
|
|
546
|
+
"id": image_id or f"ig_{uuid.uuid4().hex}",
|
|
547
|
+
"type": "image_generation_call",
|
|
548
|
+
"result": _image_generation_result(result_source),
|
|
549
|
+
"status": _image_generation_status(status),
|
|
550
|
+
})
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def _mcp_call_output_item(
|
|
554
|
+
content: Content,
|
|
555
|
+
*,
|
|
556
|
+
status: str,
|
|
557
|
+
result_content: Content | None = None,
|
|
558
|
+
) -> ResponseOutputItem:
|
|
559
|
+
return _response_output_item({
|
|
560
|
+
"id": content.call_id or f"mcp_{uuid.uuid4().hex}",
|
|
561
|
+
"type": "mcp_call",
|
|
562
|
+
"server_label": content.server_name or "default",
|
|
563
|
+
"name": content.tool_name or "tool",
|
|
564
|
+
"arguments": _arguments_to_str(content.arguments),
|
|
565
|
+
"output": _stringify_output(result_content.output) if result_content is not None else None,
|
|
566
|
+
"status": _mcp_status(status),
|
|
567
|
+
})
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
def _mcp_result_output_item(content: Content, *, status: str) -> ResponseOutputItem:
|
|
571
|
+
return _response_output_item({
|
|
572
|
+
"id": content.call_id or f"mcp_{uuid.uuid4().hex}",
|
|
573
|
+
"type": "mcp_call",
|
|
574
|
+
"server_label": content.server_name or "default",
|
|
575
|
+
"name": content.tool_name or "tool",
|
|
576
|
+
"arguments": "",
|
|
577
|
+
"output": _stringify_output(content.output),
|
|
578
|
+
"status": _mcp_status(status),
|
|
579
|
+
})
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def _shell_call_output_item(content: Content, *, status: str) -> ResponseOutputItem:
|
|
583
|
+
return _response_output_item({
|
|
584
|
+
"id": content.additional_properties.get("item_id") or f"shell_{uuid.uuid4().hex}",
|
|
585
|
+
"type": "shell_call",
|
|
586
|
+
"call_id": content.call_id or f"call_{uuid.uuid4().hex}",
|
|
587
|
+
"action": {
|
|
588
|
+
"commands": content.commands or [],
|
|
589
|
+
"timeout_ms": content.timeout_ms,
|
|
590
|
+
"max_output_length": content.max_output_length,
|
|
591
|
+
},
|
|
592
|
+
"environment": {"type": "local"},
|
|
593
|
+
"status": _message_status(status),
|
|
594
|
+
})
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
def _shell_result_output_item(content: Content, *, status: str) -> ResponseOutputItem:
|
|
598
|
+
outputs: list[dict[str, Any]] = []
|
|
599
|
+
outputs_value: Any = content.outputs
|
|
600
|
+
if isinstance(outputs_value, Sequence) and not isinstance(outputs_value, (str, bytes, bytearray)):
|
|
601
|
+
for item in cast(Sequence[Any], outputs_value):
|
|
602
|
+
if not isinstance(item, Content):
|
|
603
|
+
continue
|
|
604
|
+
outcome = {"type": "timeout"} if item.timed_out else {"type": "exit", "exit_code": item.exit_code or 0}
|
|
605
|
+
outputs.append({"stdout": item.stdout or "", "stderr": item.stderr or "", "outcome": outcome})
|
|
606
|
+
|
|
607
|
+
return _response_output_item({
|
|
608
|
+
"id": content.additional_properties.get("item_id") or f"shellout_{uuid.uuid4().hex}",
|
|
609
|
+
"type": "shell_call_output",
|
|
610
|
+
"call_id": content.call_id or f"call_{uuid.uuid4().hex}",
|
|
611
|
+
"output": outputs,
|
|
612
|
+
"max_output_length": content.max_output_length,
|
|
613
|
+
"status": _message_status(status),
|
|
614
|
+
})
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
def _function_approval_request_output_item(content: Content) -> ResponseOutputItem:
|
|
618
|
+
function_call = content.function_call
|
|
619
|
+
return _response_output_item({
|
|
620
|
+
"id": content.id or f"approval_{uuid.uuid4().hex}",
|
|
621
|
+
"type": "mcp_approval_request",
|
|
622
|
+
"server_label": (
|
|
623
|
+
function_call.additional_properties.get("server_label", "agent_framework")
|
|
624
|
+
if function_call is not None
|
|
625
|
+
else "agent_framework"
|
|
626
|
+
),
|
|
627
|
+
"name": function_call.name if function_call is not None and function_call.name else "tool",
|
|
628
|
+
"arguments": _arguments_to_str(function_call.arguments if function_call is not None else None),
|
|
629
|
+
})
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def _function_approval_response_output_item(content: Content) -> ResponseOutputItem:
|
|
633
|
+
return _response_output_item({
|
|
634
|
+
"id": content.id or f"approval_{uuid.uuid4().hex}",
|
|
635
|
+
"type": "mcp_approval_response",
|
|
636
|
+
"approval_request_id": content.id or "",
|
|
637
|
+
"approve": bool(content.approved),
|
|
638
|
+
})
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
def _media_content_output_item(content: Content, *, status: str) -> ResponseOutputItem:
|
|
642
|
+
parts = _content_parts_to_input_items([content])
|
|
643
|
+
if parts:
|
|
644
|
+
return cast(
|
|
645
|
+
ResponseOutputItem,
|
|
646
|
+
ResponseFunctionToolCallOutputItem(
|
|
647
|
+
id=f"content_{uuid.uuid4().hex}",
|
|
648
|
+
type="function_call_output",
|
|
649
|
+
call_id=f"content_{uuid.uuid4().hex}",
|
|
650
|
+
output=parts,
|
|
651
|
+
status=_message_status(status), # type: ignore[arg-type]
|
|
652
|
+
),
|
|
653
|
+
)
|
|
654
|
+
return _text_output_items(json.dumps(content.to_dict(), default=str), status=status)[0]
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
def _content_parts_to_input_items(contents: Sequence[Content] | None) -> list[Any]:
|
|
658
|
+
if not contents:
|
|
659
|
+
return []
|
|
660
|
+
|
|
661
|
+
parts: list[Any] = []
|
|
662
|
+
for content in contents:
|
|
663
|
+
match content.type:
|
|
664
|
+
case "text":
|
|
665
|
+
parts.append(ResponseInputText(type="input_text", text=content.text or ""))
|
|
666
|
+
case "data" | "uri":
|
|
667
|
+
if not content.uri:
|
|
668
|
+
continue
|
|
669
|
+
if _is_image_content(content):
|
|
670
|
+
parts.append(ResponseInputImage(type="input_image", image_url=content.uri, detail="auto"))
|
|
671
|
+
else:
|
|
672
|
+
parts.append(ResponseInputFile(type="input_file", file_url=content.uri))
|
|
673
|
+
case "hosted_file":
|
|
674
|
+
if content.file_id:
|
|
675
|
+
parts.append(ResponseInputFile(type="input_file", file_id=content.file_id))
|
|
676
|
+
case _:
|
|
677
|
+
parts.append(ResponseInputText(type="input_text", text=json.dumps(content.to_dict(), default=str)))
|
|
678
|
+
return parts
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
def _content_sequence_text(contents: Sequence[Content] | None) -> str | None:
|
|
682
|
+
if not contents:
|
|
683
|
+
return None
|
|
684
|
+
text = "".join(content.text or "" for content in contents if content.type == "text")
|
|
685
|
+
return text or None
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
def _is_image_content(content: Content) -> bool:
|
|
689
|
+
media_type = content.media_type or ""
|
|
690
|
+
if media_type.startswith("image/"):
|
|
691
|
+
return True
|
|
692
|
+
return (content.uri or "").startswith("data:image/")
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
def _image_generation_result(outputs: Any) -> str | None:
|
|
696
|
+
if isinstance(outputs, Content):
|
|
697
|
+
return _image_generation_content_result(outputs)
|
|
698
|
+
if isinstance(outputs, Sequence) and not isinstance(outputs, (str, bytes, bytearray)):
|
|
699
|
+
for output in cast(Sequence[Any], outputs):
|
|
700
|
+
if isinstance(output, Content) and (result := _image_generation_content_result(output)):
|
|
701
|
+
return result
|
|
702
|
+
if isinstance(outputs, str):
|
|
703
|
+
return outputs
|
|
704
|
+
return None
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
def _image_generation_content_result(content: Content) -> str | None:
|
|
708
|
+
uri = content.uri
|
|
709
|
+
if not uri:
|
|
710
|
+
return None
|
|
711
|
+
if ";base64," in uri:
|
|
712
|
+
return uri.split(";base64,", 1)[1]
|
|
713
|
+
return uri
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
def _content_item_id(content: Content, result_content: Content | None = None) -> str | None:
|
|
717
|
+
item_id = content.additional_properties.get("item_id")
|
|
718
|
+
if isinstance(item_id, str) and item_id:
|
|
719
|
+
return item_id
|
|
720
|
+
if result_content is not None:
|
|
721
|
+
result_item_id = result_content.additional_properties.get("item_id")
|
|
722
|
+
if isinstance(result_item_id, str) and result_item_id:
|
|
723
|
+
return result_item_id
|
|
724
|
+
return content.call_id or (result_content.call_id if result_content is not None else None)
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
def _content_property(content: Content, result_content: Content | None, key: str) -> Any:
|
|
728
|
+
if key in content.additional_properties:
|
|
729
|
+
return content.additional_properties[key]
|
|
730
|
+
if result_content is not None and key in result_content.additional_properties:
|
|
731
|
+
return result_content.additional_properties[key]
|
|
732
|
+
return None
|
|
733
|
+
|
|
734
|
+
|
|
735
|
+
def _code_interpreter_status(status: str) -> str:
|
|
736
|
+
if status in ("in_progress", "completed", "incomplete", "failed"):
|
|
737
|
+
return status
|
|
738
|
+
return "incomplete"
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
def _image_generation_status(status: str) -> str:
|
|
742
|
+
if status in ("in_progress", "completed", "failed"):
|
|
743
|
+
return status
|
|
744
|
+
return "failed"
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
def _mcp_status(status: str) -> str:
|
|
748
|
+
if status in ("in_progress", "completed", "incomplete", "failed"):
|
|
749
|
+
return status
|
|
750
|
+
return "incomplete"
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
def _arguments_to_str(arguments: Any | None) -> str:
|
|
754
|
+
if arguments is None:
|
|
755
|
+
return ""
|
|
756
|
+
if isinstance(arguments, str):
|
|
757
|
+
return arguments
|
|
758
|
+
return json.dumps(arguments, default=str)
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
def _stringify_output(output: Any) -> str:
|
|
762
|
+
if output is None:
|
|
763
|
+
return ""
|
|
764
|
+
if isinstance(output, str):
|
|
765
|
+
return output
|
|
766
|
+
if isinstance(output, Sequence) and not isinstance(output, (str, bytes, bytearray)):
|
|
767
|
+
return "".join(_stringify_output(item) for item in cast(Sequence[Any], output))
|
|
768
|
+
return json.dumps(output, default=str)
|
|
769
|
+
|
|
770
|
+
|
|
771
|
+
def _raw_response_output_item(raw: Any) -> ResponseOutputItem | None:
|
|
772
|
+
if _raw_type(raw) is None:
|
|
773
|
+
return None
|
|
774
|
+
try:
|
|
775
|
+
return cast(ResponseOutputItem, _RESPONSE_OUTPUT_ITEM_ADAPTER.validate_python(raw))
|
|
776
|
+
except ValidationError:
|
|
777
|
+
return None
|
|
778
|
+
|
|
779
|
+
|
|
780
|
+
def _response_output_item(value: Mapping[str, Any]) -> ResponseOutputItem:
|
|
781
|
+
return cast(ResponseOutputItem, _RESPONSE_OUTPUT_ITEM_ADAPTER.validate_python(value))
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
def _response_output_item_key(item: ResponseOutputItem) -> tuple[str, str]:
|
|
785
|
+
item_type = _raw_type(item) or "unknown"
|
|
786
|
+
item_id = getattr(item, "id", None) or getattr(item, "call_id", None)
|
|
787
|
+
if isinstance(item_id, str) and item_id:
|
|
788
|
+
return item_type, item_id
|
|
789
|
+
return item_type, str(id(item))
|
|
790
|
+
|
|
791
|
+
|
|
792
|
+
def _raw_type(raw: Any) -> str | None:
|
|
793
|
+
raw_type = getattr(raw, "type", None)
|
|
794
|
+
if isinstance(raw_type, str):
|
|
795
|
+
return raw_type
|
|
796
|
+
if isinstance(raw, Mapping):
|
|
797
|
+
mapping_type = cast(Mapping[str, Any], raw).get("type")
|
|
798
|
+
if isinstance(mapping_type, str):
|
|
799
|
+
return mapping_type
|
|
800
|
+
return None
|
|
801
|
+
|
|
802
|
+
|
|
803
|
+
def _result_to_text(result: Any) -> str:
|
|
804
|
+
text = getattr(result, "text", None)
|
|
805
|
+
if isinstance(text, str):
|
|
806
|
+
return text
|
|
807
|
+
get_outputs = getattr(result, "get_outputs", None)
|
|
808
|
+
if callable(get_outputs):
|
|
809
|
+
return "".join(_output_to_text(output) for output in cast(Sequence[Any], get_outputs()))
|
|
810
|
+
return str(result)
|
|
811
|
+
|
|
812
|
+
|
|
813
|
+
def _output_to_text(output: Any) -> str:
|
|
814
|
+
text = getattr(output, "text", None)
|
|
815
|
+
if isinstance(text, str):
|
|
816
|
+
return text
|
|
817
|
+
return str(output)
|
|
818
|
+
|
|
819
|
+
|
|
820
|
+
def _response_payload(response: OpenAIResponse) -> dict[str, Any]:
|
|
821
|
+
payload = response.model_dump(mode="json", exclude_none=True)
|
|
822
|
+
created_at = payload.get("created_at")
|
|
823
|
+
if isinstance(created_at, float):
|
|
824
|
+
payload["created_at"] = int(created_at)
|
|
825
|
+
return payload
|
|
826
|
+
|
|
827
|
+
|
|
828
|
+
def _sse_event(event_type: str, payload: Mapping[str, Any]) -> str:
|
|
829
|
+
"""Format one Server-Sent Event."""
|
|
830
|
+
return f"event: {event_type}\ndata: {_json_dumps(payload)}\n\n"
|
|
831
|
+
|
|
832
|
+
|
|
833
|
+
def _json_dumps(payload: Mapping[str, Any]) -> str:
|
|
834
|
+
"""Serialize a Responses SSE payload."""
|
|
835
|
+
return json.dumps(payload, separators=(",", ":"))
|
|
836
|
+
|
|
837
|
+
|
|
838
|
+
async def responses_from_streaming_run(
|
|
839
|
+
stream: ResponseStream[AgentResponseUpdate, AgentResponse[Any]],
|
|
840
|
+
*,
|
|
841
|
+
response_id: str,
|
|
842
|
+
session_id: str | None = None,
|
|
843
|
+
) -> AsyncIterator[str]:
|
|
844
|
+
"""Convert an Agent Framework response stream into Responses SSE events.
|
|
845
|
+
|
|
846
|
+
Args:
|
|
847
|
+
stream: Agent Framework response stream returned by ``agent.run(...,
|
|
848
|
+
stream=True)``.
|
|
849
|
+
|
|
850
|
+
Keyword Args:
|
|
851
|
+
response_id: Id for the response being created.
|
|
852
|
+
session_id: Optional prior ``resp_*`` or ``conv_*`` session id.
|
|
853
|
+
|
|
854
|
+
Yields:
|
|
855
|
+
Server-Sent Event strings.
|
|
856
|
+
"""
|
|
857
|
+
yield _sse_event(
|
|
858
|
+
"response.created",
|
|
859
|
+
{
|
|
860
|
+
"type": "response.created",
|
|
861
|
+
"response": {
|
|
862
|
+
"id": response_id,
|
|
863
|
+
"object": "response",
|
|
864
|
+
"created_at": int(time.time()),
|
|
865
|
+
"status": "in_progress",
|
|
866
|
+
"model": "agent",
|
|
867
|
+
"output": [],
|
|
868
|
+
},
|
|
869
|
+
},
|
|
870
|
+
)
|
|
871
|
+
|
|
872
|
+
model: str | None = None
|
|
873
|
+
updates: list[AgentResponseUpdate] = []
|
|
874
|
+
try:
|
|
875
|
+
async for update in stream:
|
|
876
|
+
updates.append(update)
|
|
877
|
+
if model is None:
|
|
878
|
+
model = _model_from_update(update)
|
|
879
|
+
if update.text:
|
|
880
|
+
yield _sse_event(
|
|
881
|
+
"response.output_text.delta",
|
|
882
|
+
{
|
|
883
|
+
"type": "response.output_text.delta",
|
|
884
|
+
"delta": update.text,
|
|
885
|
+
},
|
|
886
|
+
)
|
|
887
|
+
|
|
888
|
+
final = await stream.get_final_response()
|
|
889
|
+
payload = responses_from_run(final, response_id=response_id, session_id=session_id)
|
|
890
|
+
if model is not None:
|
|
891
|
+
# The finalized `AgentResponse` never carries a raw representation
|
|
892
|
+
# (see `_model_from_update`), so prefer the model observed on the
|
|
893
|
+
# stream's own chunks over `responses_from_run`'s "agent" fallback.
|
|
894
|
+
payload["model"] = model
|
|
895
|
+
yield _sse_event(
|
|
896
|
+
"response.completed",
|
|
897
|
+
{
|
|
898
|
+
"type": "response.completed",
|
|
899
|
+
"response": payload,
|
|
900
|
+
},
|
|
901
|
+
)
|
|
902
|
+
except Exception as exc:
|
|
903
|
+
partial_text = "".join(update.text for update in updates if update.text)
|
|
904
|
+
response_kwargs: dict[str, Any] = {
|
|
905
|
+
"id": response_id,
|
|
906
|
+
"object": "response",
|
|
907
|
+
"created_at": int(time.time()),
|
|
908
|
+
"status": "failed",
|
|
909
|
+
"model": model or "agent",
|
|
910
|
+
"output": _text_output_items(partial_text, status="failed"),
|
|
911
|
+
"parallel_tool_calls": False,
|
|
912
|
+
"tool_choice": "auto",
|
|
913
|
+
"tools": [],
|
|
914
|
+
"metadata": {},
|
|
915
|
+
"error": {
|
|
916
|
+
"code": "server_error",
|
|
917
|
+
"message": str(exc),
|
|
918
|
+
},
|
|
919
|
+
}
|
|
920
|
+
if session_id is not None and session_id.startswith("conv_"):
|
|
921
|
+
response_kwargs["conversation"] = {"id": session_id}
|
|
922
|
+
yield _sse_event(
|
|
923
|
+
"response.failed",
|
|
924
|
+
{
|
|
925
|
+
"type": "response.failed",
|
|
926
|
+
"response": _response_payload(OpenAIResponse(**response_kwargs)),
|
|
927
|
+
},
|
|
928
|
+
)
|
|
929
|
+
|
|
930
|
+
|
|
931
|
+
__all__ = [
|
|
932
|
+
"create_response_id",
|
|
933
|
+
"messages_from_responses_input",
|
|
934
|
+
"responses_from_run",
|
|
935
|
+
"responses_from_streaming_run",
|
|
936
|
+
"responses_session_id",
|
|
937
|
+
"responses_to_run",
|
|
938
|
+
]
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "agent-framework-hosting-responses"
|
|
3
|
+
description = "OpenAI Responses-shaped helpers for agent-framework-hosting."
|
|
4
|
+
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
version = "1.0.0a260709"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
urls.homepage = "https://aka.ms/agent-framework"
|
|
10
|
+
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
|
11
|
+
urls.release_notes = "https://github.com/microsoft/agent-framework/releases?q=tag%3Apython-1&expanded=true"
|
|
12
|
+
urls.issues = "https://github.com/microsoft/agent-framework/issues"
|
|
13
|
+
classifiers = [
|
|
14
|
+
"License :: OSI Approved :: MIT License",
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.10",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Programming Language :: Python :: 3.13",
|
|
22
|
+
"Programming Language :: Python :: 3.14",
|
|
23
|
+
"Typing :: Typed",
|
|
24
|
+
]
|
|
25
|
+
dependencies = [
|
|
26
|
+
"agent-framework-core>=1.11.0,<2",
|
|
27
|
+
"agent-framework-hosting==1.0.0a260709",
|
|
28
|
+
"openai>=1.99.0,<3",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[dependency-groups]
|
|
32
|
+
dev = [
|
|
33
|
+
"fastapi>=0.115.0,<0.138.1",
|
|
34
|
+
"httpx>=0.28.1",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[tool.uv]
|
|
38
|
+
prerelease = "if-necessary-or-explicit"
|
|
39
|
+
environments = [
|
|
40
|
+
"sys_platform == 'darwin'",
|
|
41
|
+
"sys_platform == 'linux'",
|
|
42
|
+
"sys_platform == 'win32'"
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
[tool.uv-dynamic-versioning]
|
|
46
|
+
fallback-version = "0.0.0"
|
|
47
|
+
|
|
48
|
+
[tool.pytest.ini_options]
|
|
49
|
+
testpaths = 'tests'
|
|
50
|
+
addopts = "-ra -q -r fEX"
|
|
51
|
+
asyncio_mode = "auto"
|
|
52
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
53
|
+
filterwarnings = []
|
|
54
|
+
timeout = 120
|
|
55
|
+
markers = [
|
|
56
|
+
"integration: marks tests as integration tests that require external services",
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
[tool.ruff]
|
|
60
|
+
extend = "../../pyproject.toml"
|
|
61
|
+
|
|
62
|
+
[tool.coverage.run]
|
|
63
|
+
omit = [
|
|
64
|
+
"**/__init__.py"
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
[tool.pyright]
|
|
68
|
+
extends = "../../pyproject.toml"
|
|
69
|
+
include = ["agent_framework_hosting_responses"]
|
|
70
|
+
exclude = ['tests']
|
|
71
|
+
|
|
72
|
+
[tool.bandit]
|
|
73
|
+
targets = ["agent_framework_hosting_responses"]
|
|
74
|
+
exclude_dirs = ["tests"]
|
|
75
|
+
|
|
76
|
+
[tool.poe]
|
|
77
|
+
executor.type = "uv"
|
|
78
|
+
include = "../../shared_tasks.toml"
|
|
79
|
+
|
|
80
|
+
[tool.poe.tasks.test]
|
|
81
|
+
help = "Run the default unit test suite for this package."
|
|
82
|
+
cmd = 'pytest -m "not integration" --cov=agent_framework_hosting_responses --cov-report=term-missing:skip-covered tests'
|
|
83
|
+
|
|
84
|
+
[build-system]
|
|
85
|
+
requires = ["flit-core >= 3.11,<4.0"]
|
|
86
|
+
build-backend = "flit_core.buildapi"
|