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.
@@ -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"