agent-framework-foundry-hosting 1.0.0a260421__py3-none-any.whl
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_foundry_hosting/__init__.py +13 -0
- agent_framework_foundry_hosting/_invocations.py +80 -0
- agent_framework_foundry_hosting/_responses.py +983 -0
- agent_framework_foundry_hosting-1.0.0a260421.dist-info/METADATA +31 -0
- agent_framework_foundry_hosting-1.0.0a260421.dist-info/RECORD +7 -0
- agent_framework_foundry_hosting-1.0.0a260421.dist-info/WHEEL +4 -0
- agent_framework_foundry_hosting-1.0.0a260421.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Copyright (c) Microsoft. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import importlib.metadata
|
|
4
|
+
|
|
5
|
+
from ._invocations import InvocationsHostServer
|
|
6
|
+
from ._responses import ResponsesHostServer
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
__version__ = importlib.metadata.version(__name__)
|
|
10
|
+
except importlib.metadata.PackageNotFoundError:
|
|
11
|
+
__version__ = "0.0.0"
|
|
12
|
+
|
|
13
|
+
__all__ = ["InvocationsHostServer", "ResponsesHostServer"]
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Copyright (c) Microsoft. All rights reserved.
|
|
2
|
+
|
|
3
|
+
from agent_framework import AgentSession, BaseAgent, SupportsAgentRun
|
|
4
|
+
from agent_framework._telemetry import user_agent_prefix
|
|
5
|
+
from azure.ai.agentserver.invocations import InvocationAgentServerHost
|
|
6
|
+
from starlette.requests import Request
|
|
7
|
+
from starlette.responses import JSONResponse, Response, StreamingResponse
|
|
8
|
+
from typing_extensions import Any, AsyncGenerator
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class InvocationsHostServer(InvocationAgentServerHost):
|
|
12
|
+
"""An invocations server host for an agent."""
|
|
13
|
+
|
|
14
|
+
USER_AGENT_PREFIX = "foundry-hosting"
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
agent: BaseAgent,
|
|
19
|
+
*,
|
|
20
|
+
openapi_spec: dict[str, Any] | None = None,
|
|
21
|
+
**kwargs: Any,
|
|
22
|
+
) -> None:
|
|
23
|
+
"""Initialize an InvocationsHostServer.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
agent: The agent to handle responses for.
|
|
27
|
+
openapi_spec: The OpenAPI specification for the server.
|
|
28
|
+
**kwargs: Additional keyword arguments.
|
|
29
|
+
|
|
30
|
+
This host will expect the request to be a JSON body with a "message" field.
|
|
31
|
+
The response from the host will be a JSON object with a "response" field containing
|
|
32
|
+
the agent's response and a "session_id" field containing the session ID.
|
|
33
|
+
"""
|
|
34
|
+
super().__init__(openapi_spec=openapi_spec, **kwargs)
|
|
35
|
+
|
|
36
|
+
if not isinstance(agent, SupportsAgentRun):
|
|
37
|
+
raise TypeError("Agent must support the SupportsAgentRun interface")
|
|
38
|
+
|
|
39
|
+
self._agent = agent
|
|
40
|
+
self._sessions: dict[str, AgentSession] = {}
|
|
41
|
+
self.invoke_handler(self._handle_invoke) # pyright: ignore[reportUnknownMemberType]
|
|
42
|
+
|
|
43
|
+
async def _handle_invoke(self, request: Request) -> Response:
|
|
44
|
+
"""Invoke the agent with the given request."""
|
|
45
|
+
with user_agent_prefix(self.USER_AGENT_PREFIX):
|
|
46
|
+
return await self._handle_invoke_inner(request)
|
|
47
|
+
|
|
48
|
+
async def _handle_invoke_inner(self, request: Request) -> Response:
|
|
49
|
+
"""Core invoke handler logic."""
|
|
50
|
+
data = await request.json()
|
|
51
|
+
session_id: str = request.state.session_id
|
|
52
|
+
|
|
53
|
+
stream = data.get("stream", False)
|
|
54
|
+
user_message = data.get("message", None)
|
|
55
|
+
if user_message is None:
|
|
56
|
+
error = "Missing 'message' in request"
|
|
57
|
+
if stream:
|
|
58
|
+
return StreamingResponse(content=error, status_code=400)
|
|
59
|
+
return Response(content=error, status_code=400)
|
|
60
|
+
|
|
61
|
+
session = self._sessions.setdefault(session_id, AgentSession(session_id=session_id))
|
|
62
|
+
|
|
63
|
+
if stream:
|
|
64
|
+
|
|
65
|
+
async def stream_response() -> AsyncGenerator[str]:
|
|
66
|
+
async for update in self._agent.run(user_message, session=session, stream=True):
|
|
67
|
+
if update.text:
|
|
68
|
+
yield update.text
|
|
69
|
+
|
|
70
|
+
return StreamingResponse(
|
|
71
|
+
stream_response(),
|
|
72
|
+
media_type="text/event-stream",
|
|
73
|
+
headers={"Cache-Control": "no-cache", "Connection": "keep-alive"},
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
response = await self._agent.run([user_message], session=session, stream=stream)
|
|
77
|
+
return JSONResponse({
|
|
78
|
+
"response": response.text,
|
|
79
|
+
"session_id": session_id,
|
|
80
|
+
})
|
|
@@ -0,0 +1,983 @@
|
|
|
1
|
+
# Copyright (c) Microsoft. All rights reserved.
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
from collections.abc import AsyncIterable, AsyncIterator, Generator, Mapping, Sequence
|
|
10
|
+
from typing import cast
|
|
11
|
+
|
|
12
|
+
from agent_framework import (
|
|
13
|
+
ChatOptions,
|
|
14
|
+
Content,
|
|
15
|
+
ContextProvider,
|
|
16
|
+
FileCheckpointStorage,
|
|
17
|
+
HistoryProvider,
|
|
18
|
+
Message,
|
|
19
|
+
RawAgent,
|
|
20
|
+
SupportsAgentRun,
|
|
21
|
+
WorkflowAgent,
|
|
22
|
+
)
|
|
23
|
+
from agent_framework._telemetry import user_agent_prefix
|
|
24
|
+
from azure.ai.agentserver.responses import (
|
|
25
|
+
ResponseContext,
|
|
26
|
+
ResponseEventStream,
|
|
27
|
+
ResponseProviderProtocol,
|
|
28
|
+
ResponsesServerOptions,
|
|
29
|
+
)
|
|
30
|
+
from azure.ai.agentserver.responses.hosting import ResponsesAgentServerHost
|
|
31
|
+
from azure.ai.agentserver.responses.models import (
|
|
32
|
+
ComputerScreenshotContent,
|
|
33
|
+
CreateResponse,
|
|
34
|
+
FunctionCallOutputItemParam,
|
|
35
|
+
FunctionShellAction,
|
|
36
|
+
FunctionShellCallOutputContent,
|
|
37
|
+
FunctionShellCallOutputExitOutcome,
|
|
38
|
+
LocalEnvironmentResource,
|
|
39
|
+
MessageContent,
|
|
40
|
+
MessageContentInputFileContent,
|
|
41
|
+
MessageContentInputImageContent,
|
|
42
|
+
MessageContentInputTextContent,
|
|
43
|
+
MessageContentOutputTextContent,
|
|
44
|
+
MessageContentReasoningTextContent,
|
|
45
|
+
MessageContentRefusalContent,
|
|
46
|
+
OAuthConsentRequestOutputItem,
|
|
47
|
+
OutputItem,
|
|
48
|
+
OutputItemApplyPatchToolCall,
|
|
49
|
+
OutputItemApplyPatchToolCallOutput,
|
|
50
|
+
OutputItemCodeInterpreterToolCall,
|
|
51
|
+
OutputItemComputerToolCall,
|
|
52
|
+
OutputItemComputerToolCallOutputResource,
|
|
53
|
+
OutputItemCustomToolCall,
|
|
54
|
+
OutputItemCustomToolCallOutput,
|
|
55
|
+
OutputItemFileSearchToolCall,
|
|
56
|
+
OutputItemFunctionShellCall,
|
|
57
|
+
OutputItemFunctionShellCallOutput,
|
|
58
|
+
OutputItemFunctionToolCall,
|
|
59
|
+
OutputItemImageGenToolCall,
|
|
60
|
+
OutputItemLocalShellToolCall,
|
|
61
|
+
OutputItemLocalShellToolCallOutput,
|
|
62
|
+
OutputItemMcpApprovalRequest,
|
|
63
|
+
OutputItemMcpApprovalResponseResource,
|
|
64
|
+
OutputItemMcpToolCall,
|
|
65
|
+
OutputItemMessage,
|
|
66
|
+
OutputItemOutputMessage,
|
|
67
|
+
OutputItemReasoningItem,
|
|
68
|
+
OutputItemWebSearchToolCall,
|
|
69
|
+
OutputMessageContent,
|
|
70
|
+
OutputMessageContentOutputTextContent,
|
|
71
|
+
OutputMessageContentRefusalContent,
|
|
72
|
+
ResponseStreamEvent,
|
|
73
|
+
StructuredOutputsOutputItem,
|
|
74
|
+
SummaryTextContent,
|
|
75
|
+
TextContent,
|
|
76
|
+
)
|
|
77
|
+
from azure.ai.agentserver.responses.streaming._builders import (
|
|
78
|
+
OutputItemFunctionCallBuilder,
|
|
79
|
+
OutputItemMcpCallBuilder,
|
|
80
|
+
OutputItemMessageBuilder,
|
|
81
|
+
OutputItemReasoningItemBuilder,
|
|
82
|
+
ReasoningSummaryPartBuilder,
|
|
83
|
+
TextContentBuilder,
|
|
84
|
+
)
|
|
85
|
+
from typing_extensions import Any
|
|
86
|
+
|
|
87
|
+
logger = logging.getLogger(__name__)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class ResponsesHostServer(ResponsesAgentServerHost):
|
|
91
|
+
"""A responses server host for an agent."""
|
|
92
|
+
|
|
93
|
+
USER_AGENT_PREFIX = "foundry-hosting"
|
|
94
|
+
# TODO(@taochen): Allow a different checkpoint storage that stores checkpoints externally
|
|
95
|
+
CHECKPOINT_STORAGE_PATH = "/.checkpoints"
|
|
96
|
+
|
|
97
|
+
def __init__(
|
|
98
|
+
self,
|
|
99
|
+
agent: SupportsAgentRun,
|
|
100
|
+
*,
|
|
101
|
+
prefix: str = "",
|
|
102
|
+
options: ResponsesServerOptions | None = None,
|
|
103
|
+
store: ResponseProviderProtocol | None = None,
|
|
104
|
+
**kwargs: Any,
|
|
105
|
+
) -> None:
|
|
106
|
+
"""Initialize a ResponsesHostServer.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
agent: The agent to handle responses for.
|
|
110
|
+
prefix: The URL prefix for the server.
|
|
111
|
+
options: Optional server options.
|
|
112
|
+
store: Optional response store.
|
|
113
|
+
**kwargs: Additional keyword arguments.
|
|
114
|
+
|
|
115
|
+
Note:
|
|
116
|
+
1. The agent must not have a history provider with `load_messages=True`,
|
|
117
|
+
because history is managed by the hosting infrastructure.
|
|
118
|
+
2. The agent must not have any context providers that maintain context
|
|
119
|
+
in memory, because the hosting environment may get deactivated between
|
|
120
|
+
requests, and any in-memory context would be lost.
|
|
121
|
+
"""
|
|
122
|
+
super().__init__(prefix=prefix, options=options, store=store, **kwargs)
|
|
123
|
+
|
|
124
|
+
for provider in getattr(agent, "context_providers", []):
|
|
125
|
+
if isinstance(provider, HistoryProvider) and provider.load_messages:
|
|
126
|
+
raise RuntimeError(
|
|
127
|
+
"There shouldn't be a history provider with `load_messages=True` already present. "
|
|
128
|
+
"History is managed by the hosting infrastructure."
|
|
129
|
+
)
|
|
130
|
+
provider = cast(ContextProvider, provider)
|
|
131
|
+
logger.warning(
|
|
132
|
+
"Context provider %s is present. If it maintains context in memory, "
|
|
133
|
+
"the context may be lost between requests. Use with caution.",
|
|
134
|
+
provider.source_id,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
self._is_workflow_agent = False
|
|
138
|
+
self._checkpoint_storage_path = None
|
|
139
|
+
if isinstance(agent, WorkflowAgent):
|
|
140
|
+
if agent.workflow._runner_context.has_checkpointing(): # pyright: ignore[reportPrivateUsage]
|
|
141
|
+
raise RuntimeError(
|
|
142
|
+
"There should not be a checkpoint storage already present in the workflow agent. "
|
|
143
|
+
"The hosting infrastructure will manage checkpoints instead."
|
|
144
|
+
)
|
|
145
|
+
self._checkpoint_storage_path = (
|
|
146
|
+
self.CHECKPOINT_STORAGE_PATH
|
|
147
|
+
if self.config.is_hosted
|
|
148
|
+
else os.path.join(os.getcwd(), self.CHECKPOINT_STORAGE_PATH.lstrip("/"))
|
|
149
|
+
)
|
|
150
|
+
self._is_workflow_agent = True
|
|
151
|
+
|
|
152
|
+
self._agent = agent
|
|
153
|
+
self.response_handler(self._handler) # pyright: ignore[reportUnknownMemberType]
|
|
154
|
+
|
|
155
|
+
@staticmethod
|
|
156
|
+
def _is_streaming_request(request: CreateResponse) -> bool:
|
|
157
|
+
"""Check if the request is a streaming request."""
|
|
158
|
+
return request.stream is not None and request.stream is True
|
|
159
|
+
|
|
160
|
+
async def _handler(
|
|
161
|
+
self,
|
|
162
|
+
request: CreateResponse,
|
|
163
|
+
context: ResponseContext,
|
|
164
|
+
cancellation_signal: asyncio.Event,
|
|
165
|
+
) -> AsyncIterable[ResponseStreamEvent | dict[str, Any]]:
|
|
166
|
+
"""Handle the creation of a response."""
|
|
167
|
+
with user_agent_prefix(self.USER_AGENT_PREFIX):
|
|
168
|
+
async for event in self._handle_inner(request, context, cancellation_signal):
|
|
169
|
+
yield event
|
|
170
|
+
|
|
171
|
+
async def _handle_inner(
|
|
172
|
+
self,
|
|
173
|
+
request: CreateResponse,
|
|
174
|
+
context: ResponseContext,
|
|
175
|
+
cancellation_signal: asyncio.Event,
|
|
176
|
+
) -> AsyncIterable[ResponseStreamEvent | dict[str, Any]]:
|
|
177
|
+
"""Core handler logic."""
|
|
178
|
+
if self._is_workflow_agent:
|
|
179
|
+
# Workflow agents are handled differently because they require checkpoint restoration
|
|
180
|
+
async for event in self._handle_workflow_agent(request, context, cancellation_signal):
|
|
181
|
+
yield event
|
|
182
|
+
return
|
|
183
|
+
|
|
184
|
+
input_text = await context.get_input_text()
|
|
185
|
+
history = await context.get_history()
|
|
186
|
+
messages: list[str | Content | Message] = [*_to_messages(history), input_text]
|
|
187
|
+
|
|
188
|
+
chat_options, are_options_set = _to_chat_options(request)
|
|
189
|
+
|
|
190
|
+
is_streaming_request = self._is_streaming_request(request)
|
|
191
|
+
response_event_stream = ResponseEventStream(response_id=context.response_id, model=request.model)
|
|
192
|
+
|
|
193
|
+
yield response_event_stream.emit_created()
|
|
194
|
+
yield response_event_stream.emit_in_progress()
|
|
195
|
+
|
|
196
|
+
if not is_streaming_request:
|
|
197
|
+
# Run the agent in non-streaming mode
|
|
198
|
+
if isinstance(self._agent, RawAgent):
|
|
199
|
+
raw_agent = cast("RawAgent[Any]", self._agent) # type: ignore[redundant-cast] # pyright: ignore[reportUnknownMemberType]
|
|
200
|
+
response = await raw_agent.run(messages, stream=False, options=chat_options)
|
|
201
|
+
else:
|
|
202
|
+
if are_options_set:
|
|
203
|
+
logger.warning("Agent doesn't support runtime options. They will be ignored.")
|
|
204
|
+
response = await self._agent.run(messages, stream=False)
|
|
205
|
+
|
|
206
|
+
for message in response.messages:
|
|
207
|
+
for content in message.contents:
|
|
208
|
+
async for item in _to_outputs(response_event_stream, content):
|
|
209
|
+
yield item
|
|
210
|
+
|
|
211
|
+
yield response_event_stream.emit_completed()
|
|
212
|
+
return
|
|
213
|
+
|
|
214
|
+
# Run the agent in streaming mode
|
|
215
|
+
if isinstance(self._agent, RawAgent):
|
|
216
|
+
raw_agent = cast("RawAgent[Any]", self._agent) # type: ignore[redundant-cast] # pyright: ignore[reportUnknownMemberType]
|
|
217
|
+
response_stream = raw_agent.run(messages, stream=True, options=chat_options)
|
|
218
|
+
else:
|
|
219
|
+
if are_options_set:
|
|
220
|
+
logger.warning("Agent doesn't support runtime options. They will be ignored.")
|
|
221
|
+
response_stream = self._agent.run(messages, stream=True)
|
|
222
|
+
|
|
223
|
+
# Track the current active output item builder for streaming;
|
|
224
|
+
# lazily created on matching content, closed when a different type arrives.
|
|
225
|
+
tracker = _OutputItemTracker(response_event_stream)
|
|
226
|
+
|
|
227
|
+
async for update in response_stream:
|
|
228
|
+
for content in update.contents:
|
|
229
|
+
for event in tracker.handle(content):
|
|
230
|
+
yield event
|
|
231
|
+
if tracker.needs_async:
|
|
232
|
+
async for item in _to_outputs(response_event_stream, content):
|
|
233
|
+
yield item
|
|
234
|
+
tracker.needs_async = False
|
|
235
|
+
|
|
236
|
+
# Close any remaining active builder
|
|
237
|
+
for event in tracker.close():
|
|
238
|
+
yield event
|
|
239
|
+
|
|
240
|
+
yield response_event_stream.emit_completed()
|
|
241
|
+
|
|
242
|
+
async def _handle_workflow_agent(
|
|
243
|
+
self,
|
|
244
|
+
request: CreateResponse,
|
|
245
|
+
context: ResponseContext,
|
|
246
|
+
cancellation_signal: asyncio.Event,
|
|
247
|
+
) -> AsyncIterable[ResponseStreamEvent | dict[str, Any]]:
|
|
248
|
+
"""Handle the creation of a response for a workflow agent.
|
|
249
|
+
|
|
250
|
+
Why this is required:
|
|
251
|
+
The sandbox may be deactivated after some period of inactivity, and only data managed
|
|
252
|
+
by the hosting infrastructure or files will be preserved upon deactivation.
|
|
253
|
+
"""
|
|
254
|
+
input_text = await context.get_input_text()
|
|
255
|
+
is_streaming_request = self._is_streaming_request(request)
|
|
256
|
+
|
|
257
|
+
_, are_options_set = _to_chat_options(request)
|
|
258
|
+
if are_options_set:
|
|
259
|
+
logger.warning("Workflow agent doesn't support runtime options. They will be ignored.")
|
|
260
|
+
|
|
261
|
+
if request.previous_response_id is not None and context.conversation_id is not None:
|
|
262
|
+
raise RuntimeError("Previous response ID cannot be used in conjunction with conversation ID.")
|
|
263
|
+
context_id = request.previous_response_id or context.conversation_id
|
|
264
|
+
|
|
265
|
+
# The following should never happen due to the checks above.
|
|
266
|
+
# This is for type safety and defensive programming.
|
|
267
|
+
if self._checkpoint_storage_path is None:
|
|
268
|
+
raise RuntimeError("Checkpoint storage path is not configured for workflow agent.")
|
|
269
|
+
if not isinstance(self._agent, WorkflowAgent):
|
|
270
|
+
raise RuntimeError("Agent is not a workflow agent.")
|
|
271
|
+
|
|
272
|
+
# Restore from the latest checkpoint if available, otherwise start with an empty history
|
|
273
|
+
if context_id is not None:
|
|
274
|
+
checkpoint_storage = FileCheckpointStorage(os.path.join(self._checkpoint_storage_path, context_id))
|
|
275
|
+
latest_checkpoint = await checkpoint_storage.get_latest(workflow_name=self._agent.workflow.name)
|
|
276
|
+
if latest_checkpoint is not None:
|
|
277
|
+
if not is_streaming_request:
|
|
278
|
+
_ = await self._agent.run(
|
|
279
|
+
stream=False,
|
|
280
|
+
checkpoint_id=latest_checkpoint.checkpoint_id,
|
|
281
|
+
checkpoint_storage=checkpoint_storage,
|
|
282
|
+
)
|
|
283
|
+
else:
|
|
284
|
+
# Consume the streaming or the invocation will result in a no-op
|
|
285
|
+
async for _ in self._agent.run(
|
|
286
|
+
stream=True,
|
|
287
|
+
checkpoint_id=latest_checkpoint.checkpoint_id,
|
|
288
|
+
checkpoint_storage=checkpoint_storage,
|
|
289
|
+
):
|
|
290
|
+
pass
|
|
291
|
+
|
|
292
|
+
# Now run the agent with the latest input
|
|
293
|
+
response_event_stream = ResponseEventStream(response_id=context.response_id, model=request.model)
|
|
294
|
+
|
|
295
|
+
# Create a new checkpoint storage for this response based on the following rules:
|
|
296
|
+
# - If no previous response ID or conversation ID is provided, create a new checkpoint storage for this response
|
|
297
|
+
# - If a previous response ID is provided, create a new checkpoint storage for this response
|
|
298
|
+
# - If a conversation ID is provided, reuse the existing checkpoint storage for the conversation
|
|
299
|
+
context_id = context.conversation_id or context.response_id
|
|
300
|
+
checkpoint_storage = FileCheckpointStorage(os.path.join(self._checkpoint_storage_path, context_id))
|
|
301
|
+
|
|
302
|
+
yield response_event_stream.emit_created()
|
|
303
|
+
yield response_event_stream.emit_in_progress()
|
|
304
|
+
|
|
305
|
+
if not is_streaming_request:
|
|
306
|
+
# Run the agent in non-streaming mode
|
|
307
|
+
response = await self._agent.run(input_text, stream=False, checkpoint_storage=checkpoint_storage)
|
|
308
|
+
|
|
309
|
+
for message in response.messages:
|
|
310
|
+
for content in message.contents:
|
|
311
|
+
async for item in _to_outputs(response_event_stream, content):
|
|
312
|
+
yield item
|
|
313
|
+
|
|
314
|
+
await self._delete_not_latest_checkpoints(checkpoint_storage, self._agent.workflow.name)
|
|
315
|
+
yield response_event_stream.emit_completed()
|
|
316
|
+
return
|
|
317
|
+
|
|
318
|
+
# Run the agent in streaming mode
|
|
319
|
+
response_stream = self._agent.run(input_text, stream=True, checkpoint_storage=checkpoint_storage)
|
|
320
|
+
|
|
321
|
+
# Track the current active output item builder for streaming;
|
|
322
|
+
# lazily created on matching content, closed when a different type arrives.
|
|
323
|
+
tracker = _OutputItemTracker(response_event_stream)
|
|
324
|
+
|
|
325
|
+
async for update in response_stream:
|
|
326
|
+
for content in update.contents:
|
|
327
|
+
for event in tracker.handle(content):
|
|
328
|
+
yield event
|
|
329
|
+
if tracker.needs_async:
|
|
330
|
+
async for item in _to_outputs(response_event_stream, content):
|
|
331
|
+
yield item
|
|
332
|
+
tracker.needs_async = False
|
|
333
|
+
|
|
334
|
+
# Close any remaining active builder
|
|
335
|
+
for event in tracker.close():
|
|
336
|
+
yield event
|
|
337
|
+
|
|
338
|
+
await self._delete_not_latest_checkpoints(checkpoint_storage, self._agent.workflow.name)
|
|
339
|
+
yield response_event_stream.emit_completed()
|
|
340
|
+
return
|
|
341
|
+
|
|
342
|
+
@staticmethod
|
|
343
|
+
async def _delete_not_latest_checkpoints(checkpoint_storage: FileCheckpointStorage, workflow_name: str) -> None:
|
|
344
|
+
"""Delete all checkpoints except the latest one.
|
|
345
|
+
|
|
346
|
+
We only need the last checkpoint for each invocation.
|
|
347
|
+
"""
|
|
348
|
+
latest_checkpoint = await checkpoint_storage.get_latest(workflow_name=workflow_name)
|
|
349
|
+
if latest_checkpoint is not None:
|
|
350
|
+
all_checkpoints = await checkpoint_storage.list_checkpoints(workflow_name=workflow_name)
|
|
351
|
+
for checkpoint in all_checkpoints:
|
|
352
|
+
if checkpoint.checkpoint_id != latest_checkpoint.checkpoint_id:
|
|
353
|
+
await checkpoint_storage.delete(checkpoint.checkpoint_id)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
# region Active Builder State
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
class _OutputItemTracker:
|
|
360
|
+
"""Tracks the current active output item builder during streaming.
|
|
361
|
+
|
|
362
|
+
Handles lazy creation, delta emission, and closing of streaming builders
|
|
363
|
+
for text messages, reasoning, function calls, and MCP calls.
|
|
364
|
+
"""
|
|
365
|
+
|
|
366
|
+
_DELTA_TYPES = frozenset({"text", "text_reasoning", "function_call", "mcp_server_tool_call"})
|
|
367
|
+
|
|
368
|
+
def __init__(self, stream: ResponseEventStream) -> None:
|
|
369
|
+
self._stream = stream
|
|
370
|
+
self._active_type: str | None = None
|
|
371
|
+
self._active_id: str | None = None
|
|
372
|
+
# Accumulated delta text for the current active builder
|
|
373
|
+
self._accumulated: list[str] = []
|
|
374
|
+
# Builder state — only one is active at a time
|
|
375
|
+
self._message_item: OutputItemMessageBuilder | None = None
|
|
376
|
+
self._text_content: TextContentBuilder | None = None
|
|
377
|
+
self._reasoning_item: OutputItemReasoningItemBuilder | None = None
|
|
378
|
+
self._summary_part: ReasoningSummaryPartBuilder | None = None
|
|
379
|
+
self._fc_builder: OutputItemFunctionCallBuilder | None = None
|
|
380
|
+
self._mcp_builder: OutputItemMcpCallBuilder | None = None
|
|
381
|
+
self.needs_async = False
|
|
382
|
+
|
|
383
|
+
def handle(self, content: Content) -> Generator[ResponseStreamEvent]:
|
|
384
|
+
"""Process a content item, yielding sync events.
|
|
385
|
+
|
|
386
|
+
Sets ``needs_async = True`` if the caller must also drain an
|
|
387
|
+
async ``_to_outputs`` call for this content.
|
|
388
|
+
"""
|
|
389
|
+
if content.type == "text" and content.text is not None:
|
|
390
|
+
if self._active_type != "text":
|
|
391
|
+
yield from self._close()
|
|
392
|
+
yield from self._open_message()
|
|
393
|
+
self._accumulated.append(content.text)
|
|
394
|
+
if self._text_content is not None:
|
|
395
|
+
yield self._text_content.emit_delta(content.text)
|
|
396
|
+
|
|
397
|
+
elif content.type == "text_reasoning" and content.text is not None:
|
|
398
|
+
if self._active_type != "text_reasoning":
|
|
399
|
+
yield from self._close()
|
|
400
|
+
yield from self._open_reasoning()
|
|
401
|
+
self._accumulated.append(content.text)
|
|
402
|
+
if self._summary_part is not None:
|
|
403
|
+
yield self._summary_part.emit_text_delta(content.text)
|
|
404
|
+
|
|
405
|
+
elif content.type == "function_call" and content.call_id is not None:
|
|
406
|
+
if self._active_type != "function_call" or self._active_id != content.call_id:
|
|
407
|
+
yield from self._close()
|
|
408
|
+
yield from self._open_function_call(content)
|
|
409
|
+
args_str = _arguments_to_str(content.arguments)
|
|
410
|
+
self._accumulated.append(args_str)
|
|
411
|
+
if self._fc_builder is not None:
|
|
412
|
+
yield self._fc_builder.emit_arguments_delta(args_str)
|
|
413
|
+
|
|
414
|
+
elif content.type == "mcp_server_tool_call" and content.tool_name:
|
|
415
|
+
key = f"{content.server_name or 'default'}::{content.tool_name}"
|
|
416
|
+
if self._active_type != "mcp_server_tool_call" or self._active_id != key:
|
|
417
|
+
yield from self._close()
|
|
418
|
+
yield from self._open_mcp_call(content)
|
|
419
|
+
args_str = _arguments_to_str(content.arguments)
|
|
420
|
+
self._accumulated.append(args_str)
|
|
421
|
+
if self._mcp_builder is not None:
|
|
422
|
+
yield self._mcp_builder.emit_arguments_delta(args_str)
|
|
423
|
+
|
|
424
|
+
else:
|
|
425
|
+
yield from self._close()
|
|
426
|
+
self.needs_async = True
|
|
427
|
+
|
|
428
|
+
def close(self) -> Generator[ResponseStreamEvent]:
|
|
429
|
+
"""Close any remaining active builder."""
|
|
430
|
+
yield from self._close()
|
|
431
|
+
|
|
432
|
+
# -- Private open/close helpers --
|
|
433
|
+
|
|
434
|
+
def _open_message(self) -> Generator[ResponseStreamEvent]:
|
|
435
|
+
self._message_item = self._stream.add_output_item_message()
|
|
436
|
+
self._text_content = self._message_item.add_text_content()
|
|
437
|
+
self._active_type = "text"
|
|
438
|
+
self._active_id = None
|
|
439
|
+
yield self._message_item.emit_added()
|
|
440
|
+
yield self._text_content.emit_added()
|
|
441
|
+
|
|
442
|
+
def _open_reasoning(self) -> Generator[ResponseStreamEvent]:
|
|
443
|
+
self._reasoning_item = self._stream.add_output_item_reasoning_item()
|
|
444
|
+
self._summary_part = self._reasoning_item.add_summary_part()
|
|
445
|
+
self._active_type = "text_reasoning"
|
|
446
|
+
self._active_id = None
|
|
447
|
+
yield self._reasoning_item.emit_added()
|
|
448
|
+
yield self._summary_part.emit_added()
|
|
449
|
+
|
|
450
|
+
def _open_function_call(self, content: Content) -> Generator[ResponseStreamEvent]:
|
|
451
|
+
self._fc_builder = self._stream.add_output_item_function_call(
|
|
452
|
+
name=content.name or "",
|
|
453
|
+
call_id=content.call_id or "",
|
|
454
|
+
)
|
|
455
|
+
self._active_type = "function_call"
|
|
456
|
+
self._active_id = content.call_id
|
|
457
|
+
yield self._fc_builder.emit_added()
|
|
458
|
+
|
|
459
|
+
def _open_mcp_call(self, content: Content) -> Generator[ResponseStreamEvent]:
|
|
460
|
+
self._mcp_builder = self._stream.add_output_item_mcp_call(
|
|
461
|
+
server_label=content.server_name or "default",
|
|
462
|
+
name=content.tool_name or "",
|
|
463
|
+
)
|
|
464
|
+
self._active_type = "mcp_server_tool_call"
|
|
465
|
+
self._active_id = f"{content.server_name or 'default'}::{content.tool_name}"
|
|
466
|
+
yield self._mcp_builder.emit_added()
|
|
467
|
+
|
|
468
|
+
def _close(self) -> Generator[ResponseStreamEvent]:
|
|
469
|
+
accumulated = "".join(self._accumulated)
|
|
470
|
+
|
|
471
|
+
if self._active_type == "text" and self._text_content and self._message_item:
|
|
472
|
+
yield self._text_content.emit_text_done(accumulated)
|
|
473
|
+
yield self._text_content.emit_done()
|
|
474
|
+
yield self._message_item.emit_done()
|
|
475
|
+
self._text_content = None
|
|
476
|
+
self._message_item = None
|
|
477
|
+
|
|
478
|
+
elif self._active_type == "text_reasoning" and self._summary_part and self._reasoning_item:
|
|
479
|
+
yield self._summary_part.emit_text_done(accumulated)
|
|
480
|
+
yield self._summary_part.emit_done()
|
|
481
|
+
yield self._reasoning_item.emit_done()
|
|
482
|
+
self._summary_part = None
|
|
483
|
+
self._reasoning_item = None
|
|
484
|
+
|
|
485
|
+
elif self._active_type == "function_call" and self._fc_builder:
|
|
486
|
+
yield self._fc_builder.emit_arguments_done(accumulated)
|
|
487
|
+
yield self._fc_builder.emit_done()
|
|
488
|
+
self._fc_builder = None
|
|
489
|
+
|
|
490
|
+
elif self._active_type == "mcp_server_tool_call" and self._mcp_builder:
|
|
491
|
+
yield self._mcp_builder.emit_arguments_done(accumulated)
|
|
492
|
+
yield self._mcp_builder.emit_completed()
|
|
493
|
+
yield self._mcp_builder.emit_done()
|
|
494
|
+
self._mcp_builder = None
|
|
495
|
+
|
|
496
|
+
self._active_type = None
|
|
497
|
+
self._active_id = None
|
|
498
|
+
self._accumulated.clear()
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
# endregion
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
# region Option Conversion
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def _to_chat_options(request: CreateResponse) -> tuple[ChatOptions, bool]:
|
|
508
|
+
"""Converts a CreateResponse request to ChatOptions.
|
|
509
|
+
|
|
510
|
+
Args:
|
|
511
|
+
request (CreateResponse): The request to convert.
|
|
512
|
+
|
|
513
|
+
Returns:
|
|
514
|
+
ChatOptions: The converted ChatOptions.
|
|
515
|
+
bool: Whether any options were set.
|
|
516
|
+
|
|
517
|
+
"""
|
|
518
|
+
chat_options = ChatOptions()
|
|
519
|
+
are_options_set = False
|
|
520
|
+
|
|
521
|
+
if request.temperature is not None:
|
|
522
|
+
chat_options["temperature"] = request.temperature
|
|
523
|
+
are_options_set = True
|
|
524
|
+
if request.top_p is not None:
|
|
525
|
+
chat_options["top_p"] = request.top_p
|
|
526
|
+
are_options_set = True
|
|
527
|
+
if request.max_output_tokens is not None:
|
|
528
|
+
chat_options["max_tokens"] = request.max_output_tokens
|
|
529
|
+
are_options_set = True
|
|
530
|
+
if request.parallel_tool_calls is not None:
|
|
531
|
+
chat_options["allow_multiple_tool_calls"] = request.parallel_tool_calls
|
|
532
|
+
are_options_set = True
|
|
533
|
+
|
|
534
|
+
return chat_options, are_options_set
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
# endregion
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
# region Input Message Conversion
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def _to_messages(history: Sequence[OutputItem]) -> list[Message]:
|
|
544
|
+
"""Converts a sequence of OutputItem objects to a list of Message objects.
|
|
545
|
+
|
|
546
|
+
Args:
|
|
547
|
+
history (Sequence[OutputItem]): The sequence of OutputItem objects to convert.
|
|
548
|
+
|
|
549
|
+
Returns:
|
|
550
|
+
list[Message]: The list of Message objects.
|
|
551
|
+
"""
|
|
552
|
+
messages: list[Message] = []
|
|
553
|
+
for item in history:
|
|
554
|
+
messages.append(_to_message(item))
|
|
555
|
+
return messages
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def _to_message(item: OutputItem) -> Message:
|
|
559
|
+
"""Converts an OutputItem to a Message.
|
|
560
|
+
|
|
561
|
+
Args:
|
|
562
|
+
item (OutputItem): The OutputItem to convert.
|
|
563
|
+
|
|
564
|
+
Returns:
|
|
565
|
+
Message: The converted Message.
|
|
566
|
+
|
|
567
|
+
Raises:
|
|
568
|
+
ValueError: If the OutputItem type is not supported.
|
|
569
|
+
"""
|
|
570
|
+
if item.type == "output_message":
|
|
571
|
+
output_msg = cast(OutputItemOutputMessage, item)
|
|
572
|
+
return Message(
|
|
573
|
+
role=output_msg.role, contents=[_convert_output_message_content(part) for part in output_msg.content]
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
if item.type == "message":
|
|
577
|
+
msg = cast(OutputItemMessage, item)
|
|
578
|
+
return Message(role=msg.role, contents=[_convert_message_content(part) for part in msg.content])
|
|
579
|
+
|
|
580
|
+
if item.type == "function_call":
|
|
581
|
+
fc = cast(OutputItemFunctionToolCall, item)
|
|
582
|
+
return Message(
|
|
583
|
+
role="assistant",
|
|
584
|
+
contents=[Content.from_function_call(fc.call_id, fc.name, arguments=fc.arguments)],
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
if item.type == "function_call_output":
|
|
588
|
+
fco = cast(FunctionCallOutputItemParam, item)
|
|
589
|
+
output = fco.output if isinstance(fco.output, str) else str(fco.output)
|
|
590
|
+
return Message(
|
|
591
|
+
role="tool",
|
|
592
|
+
contents=[Content.from_function_result(fco.call_id, result=output)],
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
if item.type == "reasoning":
|
|
596
|
+
reasoning = cast(OutputItemReasoningItem, item)
|
|
597
|
+
contents: list[Content] = []
|
|
598
|
+
if reasoning.summary:
|
|
599
|
+
for summary in reasoning.summary:
|
|
600
|
+
contents.append(Content.from_text(summary.text))
|
|
601
|
+
return Message(role="assistant", contents=contents)
|
|
602
|
+
|
|
603
|
+
if item.type == "mcp_call":
|
|
604
|
+
mcp = cast(OutputItemMcpToolCall, item)
|
|
605
|
+
return Message(
|
|
606
|
+
role="assistant",
|
|
607
|
+
contents=[
|
|
608
|
+
Content.from_mcp_server_tool_call(
|
|
609
|
+
mcp.id,
|
|
610
|
+
mcp.name,
|
|
611
|
+
server_name=mcp.server_label,
|
|
612
|
+
arguments=mcp.arguments,
|
|
613
|
+
)
|
|
614
|
+
],
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
if item.type == "mcp_approval_request":
|
|
618
|
+
mcp_req = cast(OutputItemMcpApprovalRequest, item)
|
|
619
|
+
mcp_call_content = Content.from_mcp_server_tool_call(
|
|
620
|
+
mcp_req.id,
|
|
621
|
+
mcp_req.name,
|
|
622
|
+
server_name=mcp_req.server_label,
|
|
623
|
+
arguments=mcp_req.arguments,
|
|
624
|
+
)
|
|
625
|
+
return Message(
|
|
626
|
+
role="assistant",
|
|
627
|
+
contents=[Content.from_function_approval_request(mcp_req.id, mcp_call_content)],
|
|
628
|
+
)
|
|
629
|
+
|
|
630
|
+
if item.type == "mcp_approval_response":
|
|
631
|
+
mcp_resp = cast(OutputItemMcpApprovalResponseResource, item)
|
|
632
|
+
# Build a placeholder function_call Content since the original call details are not available
|
|
633
|
+
placeholder_content = Content.from_function_call(mcp_resp.approval_request_id, "mcp_approval")
|
|
634
|
+
return Message(
|
|
635
|
+
role="user",
|
|
636
|
+
contents=[Content.from_function_approval_response(mcp_resp.approve, mcp_resp.id, placeholder_content)],
|
|
637
|
+
)
|
|
638
|
+
|
|
639
|
+
if item.type == "code_interpreter_call":
|
|
640
|
+
ci = cast(OutputItemCodeInterpreterToolCall, item)
|
|
641
|
+
return Message(
|
|
642
|
+
role="assistant",
|
|
643
|
+
contents=[Content.from_code_interpreter_tool_call(call_id=ci.id)],
|
|
644
|
+
)
|
|
645
|
+
|
|
646
|
+
if item.type == "image_generation_call":
|
|
647
|
+
ig = cast(OutputItemImageGenToolCall, item)
|
|
648
|
+
return Message(
|
|
649
|
+
role="assistant",
|
|
650
|
+
contents=[Content.from_image_generation_tool_call(image_id=ig.id)],
|
|
651
|
+
)
|
|
652
|
+
|
|
653
|
+
if item.type == "shell_call":
|
|
654
|
+
sc = cast(OutputItemFunctionShellCall, item)
|
|
655
|
+
return Message(
|
|
656
|
+
role="assistant",
|
|
657
|
+
contents=[
|
|
658
|
+
Content.from_shell_tool_call(
|
|
659
|
+
call_id=sc.call_id,
|
|
660
|
+
commands=sc.action.commands,
|
|
661
|
+
status=str(sc.status),
|
|
662
|
+
)
|
|
663
|
+
],
|
|
664
|
+
)
|
|
665
|
+
|
|
666
|
+
if item.type == "shell_call_output":
|
|
667
|
+
sco = cast(OutputItemFunctionShellCallOutput, item)
|
|
668
|
+
outputs = [
|
|
669
|
+
Content.from_shell_command_output(
|
|
670
|
+
stdout=out.stdout or "",
|
|
671
|
+
stderr=out.stderr or "",
|
|
672
|
+
exit_code=getattr(out.outcome, "exit_code", None) if hasattr(out, "outcome") else None,
|
|
673
|
+
)
|
|
674
|
+
for out in (sco.output or [])
|
|
675
|
+
]
|
|
676
|
+
return Message(
|
|
677
|
+
role="tool",
|
|
678
|
+
contents=[
|
|
679
|
+
Content.from_shell_tool_result(
|
|
680
|
+
call_id=sco.call_id,
|
|
681
|
+
outputs=outputs,
|
|
682
|
+
max_output_length=sco.max_output_length,
|
|
683
|
+
)
|
|
684
|
+
],
|
|
685
|
+
)
|
|
686
|
+
|
|
687
|
+
if item.type == "local_shell_call":
|
|
688
|
+
lsc = cast(OutputItemLocalShellToolCall, item)
|
|
689
|
+
commands = lsc.action.command if hasattr(lsc.action, "command") and lsc.action.command else []
|
|
690
|
+
return Message(
|
|
691
|
+
role="assistant",
|
|
692
|
+
contents=[
|
|
693
|
+
Content.from_shell_tool_call(
|
|
694
|
+
call_id=lsc.call_id,
|
|
695
|
+
commands=commands,
|
|
696
|
+
status=str(lsc.status),
|
|
697
|
+
)
|
|
698
|
+
],
|
|
699
|
+
)
|
|
700
|
+
|
|
701
|
+
if item.type == "local_shell_call_output":
|
|
702
|
+
lsco = cast(OutputItemLocalShellToolCallOutput, item)
|
|
703
|
+
return Message(
|
|
704
|
+
role="tool",
|
|
705
|
+
contents=[
|
|
706
|
+
Content.from_shell_tool_result(
|
|
707
|
+
call_id=lsco.id,
|
|
708
|
+
outputs=[Content.from_shell_command_output(stdout=lsco.output)],
|
|
709
|
+
)
|
|
710
|
+
],
|
|
711
|
+
)
|
|
712
|
+
|
|
713
|
+
if item.type == "file_search_call":
|
|
714
|
+
fs = cast(OutputItemFileSearchToolCall, item)
|
|
715
|
+
return Message(
|
|
716
|
+
role="assistant",
|
|
717
|
+
contents=[
|
|
718
|
+
Content.from_function_call(
|
|
719
|
+
fs.id,
|
|
720
|
+
"file_search",
|
|
721
|
+
arguments=json.dumps({"queries": fs.queries}),
|
|
722
|
+
)
|
|
723
|
+
],
|
|
724
|
+
)
|
|
725
|
+
|
|
726
|
+
if item.type == "web_search_call":
|
|
727
|
+
ws = cast(OutputItemWebSearchToolCall, item)
|
|
728
|
+
return Message(
|
|
729
|
+
role="assistant",
|
|
730
|
+
contents=[Content.from_function_call(ws.id, "web_search")],
|
|
731
|
+
)
|
|
732
|
+
|
|
733
|
+
if item.type == "computer_call":
|
|
734
|
+
cc = cast(OutputItemComputerToolCall, item)
|
|
735
|
+
return Message(
|
|
736
|
+
role="assistant",
|
|
737
|
+
contents=[
|
|
738
|
+
Content.from_function_call(
|
|
739
|
+
cc.call_id,
|
|
740
|
+
"computer_use",
|
|
741
|
+
arguments=str(cc.action),
|
|
742
|
+
)
|
|
743
|
+
],
|
|
744
|
+
)
|
|
745
|
+
|
|
746
|
+
if item.type == "computer_call_output":
|
|
747
|
+
cco = cast(OutputItemComputerToolCallOutputResource, item)
|
|
748
|
+
return Message(
|
|
749
|
+
role="tool",
|
|
750
|
+
contents=[Content.from_function_result(cco.call_id, result=str(cco.output))],
|
|
751
|
+
)
|
|
752
|
+
|
|
753
|
+
if item.type == "custom_tool_call":
|
|
754
|
+
ct = cast(OutputItemCustomToolCall, item)
|
|
755
|
+
return Message(
|
|
756
|
+
role="assistant",
|
|
757
|
+
contents=[Content.from_function_call(ct.call_id, ct.name, arguments=ct.input)],
|
|
758
|
+
)
|
|
759
|
+
|
|
760
|
+
if item.type == "custom_tool_call_output":
|
|
761
|
+
cto = cast(OutputItemCustomToolCallOutput, item)
|
|
762
|
+
output = cto.output if isinstance(cto.output, str) else str(cto.output)
|
|
763
|
+
return Message(
|
|
764
|
+
role="tool",
|
|
765
|
+
contents=[Content.from_function_result(cto.call_id, result=output)],
|
|
766
|
+
)
|
|
767
|
+
|
|
768
|
+
if item.type == "apply_patch_call":
|
|
769
|
+
ap = cast(OutputItemApplyPatchToolCall, item)
|
|
770
|
+
return Message(
|
|
771
|
+
role="assistant",
|
|
772
|
+
contents=[
|
|
773
|
+
Content.from_function_call(
|
|
774
|
+
ap.call_id,
|
|
775
|
+
"apply_patch",
|
|
776
|
+
arguments=str(ap.operation),
|
|
777
|
+
)
|
|
778
|
+
],
|
|
779
|
+
)
|
|
780
|
+
|
|
781
|
+
if item.type == "apply_patch_call_output":
|
|
782
|
+
apo = cast(OutputItemApplyPatchToolCallOutput, item)
|
|
783
|
+
return Message(
|
|
784
|
+
role="tool",
|
|
785
|
+
contents=[Content.from_function_result(apo.call_id, result=apo.output or "")],
|
|
786
|
+
)
|
|
787
|
+
|
|
788
|
+
if item.type == "oauth_consent_request":
|
|
789
|
+
oauth = cast(OAuthConsentRequestOutputItem, item)
|
|
790
|
+
return Message(
|
|
791
|
+
role="assistant",
|
|
792
|
+
contents=[Content.from_oauth_consent_request(oauth.consent_link)],
|
|
793
|
+
)
|
|
794
|
+
|
|
795
|
+
if item.type == "structured_outputs":
|
|
796
|
+
so = cast(StructuredOutputsOutputItem, item)
|
|
797
|
+
text = json.dumps(so.output) if not isinstance(so.output, str) else so.output
|
|
798
|
+
return Message(role="assistant", contents=[Content.from_text(text)])
|
|
799
|
+
|
|
800
|
+
raise ValueError(f"Unsupported OutputItem type: {item.type}")
|
|
801
|
+
|
|
802
|
+
|
|
803
|
+
def _convert_output_message_content(content: OutputMessageContent) -> Content:
|
|
804
|
+
"""Converts an OutputMessageContent to a Content object.
|
|
805
|
+
|
|
806
|
+
Args:
|
|
807
|
+
content (OutputMessageContent): The OutputMessageContent to convert.
|
|
808
|
+
|
|
809
|
+
Returns:
|
|
810
|
+
Content: The converted Content object.
|
|
811
|
+
|
|
812
|
+
Raises:
|
|
813
|
+
ValueError: If the OutputMessageContent type is not supported.
|
|
814
|
+
"""
|
|
815
|
+
if content.type == "output_text":
|
|
816
|
+
text_content = cast(OutputMessageContentOutputTextContent, content)
|
|
817
|
+
return Content.from_text(text_content.text)
|
|
818
|
+
if content.type == "refusal":
|
|
819
|
+
refusal_content = cast(OutputMessageContentRefusalContent, content)
|
|
820
|
+
return Content.from_text(refusal_content.refusal)
|
|
821
|
+
|
|
822
|
+
raise ValueError(f"Unsupported OutputMessageContent type: {content.type}")
|
|
823
|
+
|
|
824
|
+
|
|
825
|
+
def _convert_message_content(content: MessageContent) -> Content:
|
|
826
|
+
"""Converts a MessageContent to a Content object.
|
|
827
|
+
|
|
828
|
+
Args:
|
|
829
|
+
content (MessageContent): The MessageContent to convert.
|
|
830
|
+
|
|
831
|
+
Returns:
|
|
832
|
+
Content: The converted Content object.
|
|
833
|
+
|
|
834
|
+
Raises:
|
|
835
|
+
ValueError: If the MessageContent type is not supported.
|
|
836
|
+
"""
|
|
837
|
+
if content.type == "input_text":
|
|
838
|
+
input_text = cast(MessageContentInputTextContent, content)
|
|
839
|
+
return Content.from_text(input_text.text)
|
|
840
|
+
if content.type == "output_text":
|
|
841
|
+
output_text = cast(MessageContentOutputTextContent, content)
|
|
842
|
+
return Content.from_text(output_text.text)
|
|
843
|
+
if content.type == "text":
|
|
844
|
+
text = cast(TextContent, content)
|
|
845
|
+
return Content.from_text(text.text)
|
|
846
|
+
if content.type == "summary_text":
|
|
847
|
+
summary = cast(SummaryTextContent, content)
|
|
848
|
+
return Content.from_text(summary.text)
|
|
849
|
+
if content.type == "refusal":
|
|
850
|
+
refusal = cast(MessageContentRefusalContent, content)
|
|
851
|
+
return Content.from_text(refusal.refusal)
|
|
852
|
+
if content.type == "reasoning_text":
|
|
853
|
+
reasoning = cast(MessageContentReasoningTextContent, content)
|
|
854
|
+
return Content.from_text_reasoning(text=reasoning.text)
|
|
855
|
+
if content.type == "input_image":
|
|
856
|
+
image = cast(MessageContentInputImageContent, content)
|
|
857
|
+
if image.image_url:
|
|
858
|
+
return Content.from_uri(image.image_url)
|
|
859
|
+
if image.file_id:
|
|
860
|
+
return Content.from_hosted_file(image.file_id)
|
|
861
|
+
if content.type == "input_file":
|
|
862
|
+
file = cast(MessageContentInputFileContent, content)
|
|
863
|
+
if file.file_url:
|
|
864
|
+
return Content.from_uri(file.file_url)
|
|
865
|
+
if file.file_id:
|
|
866
|
+
return Content.from_hosted_file(file.file_id, name=file.filename)
|
|
867
|
+
if content.type == "computer_screenshot":
|
|
868
|
+
screenshot = cast(ComputerScreenshotContent, content)
|
|
869
|
+
return Content.from_uri(screenshot.image_url)
|
|
870
|
+
|
|
871
|
+
raise ValueError(f"Unsupported MessageContent type: {content.type}")
|
|
872
|
+
|
|
873
|
+
|
|
874
|
+
# endregion
|
|
875
|
+
|
|
876
|
+
# region Output Item Conversion
|
|
877
|
+
|
|
878
|
+
|
|
879
|
+
def _arguments_to_str(arguments: str | Mapping[str, Any] | None) -> str:
|
|
880
|
+
"""Convert arguments to a JSON string.
|
|
881
|
+
|
|
882
|
+
Args:
|
|
883
|
+
arguments: The arguments to convert, can be a string, mapping, or None.
|
|
884
|
+
|
|
885
|
+
Returns:
|
|
886
|
+
The arguments as a JSON string.
|
|
887
|
+
"""
|
|
888
|
+
if arguments is None:
|
|
889
|
+
return ""
|
|
890
|
+
if isinstance(arguments, str):
|
|
891
|
+
return arguments
|
|
892
|
+
return json.dumps(arguments)
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
async def _to_outputs(stream: ResponseEventStream, content: Content) -> AsyncIterator[ResponseStreamEvent]:
|
|
896
|
+
"""Converts a Content object to an async sequence of ResponseStreamEvent objects.
|
|
897
|
+
|
|
898
|
+
Args:
|
|
899
|
+
stream: The ResponseEventStream to use for building events.
|
|
900
|
+
content: The Content to convert.
|
|
901
|
+
|
|
902
|
+
Yields:
|
|
903
|
+
ResponseStreamEvent: The converted event objects.
|
|
904
|
+
|
|
905
|
+
Raises:
|
|
906
|
+
ValueError: If the Content type is not supported.
|
|
907
|
+
"""
|
|
908
|
+
if content.type == "text" and content.text is not None:
|
|
909
|
+
async for event in stream.aoutput_item_message(content.text):
|
|
910
|
+
yield event
|
|
911
|
+
elif content.type == "text_reasoning" and content.text is not None:
|
|
912
|
+
async for event in stream.aoutput_item_reasoning_item(content.text):
|
|
913
|
+
yield event
|
|
914
|
+
elif content.type == "function_call":
|
|
915
|
+
async for event in stream.aoutput_item_function_call(
|
|
916
|
+
content.name, # type: ignore[arg-type]
|
|
917
|
+
content.call_id, # type: ignore[arg-type]
|
|
918
|
+
_arguments_to_str(content.arguments),
|
|
919
|
+
):
|
|
920
|
+
yield event
|
|
921
|
+
elif content.type == "function_result":
|
|
922
|
+
async for event in stream.aoutput_item_function_call_output(
|
|
923
|
+
content.call_id, # type: ignore[arg-type]
|
|
924
|
+
str(content.result or ""),
|
|
925
|
+
):
|
|
926
|
+
yield event
|
|
927
|
+
elif content.type == "image_generation_tool_result" and content.outputs is not None:
|
|
928
|
+
async for event in stream.aoutput_item_image_gen_call(str(content.outputs)):
|
|
929
|
+
yield event
|
|
930
|
+
elif content.type == "mcp_server_tool_call":
|
|
931
|
+
mcp_call = stream.add_output_item_mcp_call(
|
|
932
|
+
server_label=content.server_name or "default",
|
|
933
|
+
name=content.tool_name or "",
|
|
934
|
+
)
|
|
935
|
+
yield mcp_call.emit_added()
|
|
936
|
+
async for event in mcp_call.aarguments(_arguments_to_str(content.arguments)):
|
|
937
|
+
yield event
|
|
938
|
+
yield mcp_call.emit_completed()
|
|
939
|
+
yield mcp_call.emit_done()
|
|
940
|
+
elif content.type == "mcp_server_tool_result":
|
|
941
|
+
output = (
|
|
942
|
+
content.output
|
|
943
|
+
if isinstance(content.output, str)
|
|
944
|
+
else str(content.output)
|
|
945
|
+
if content.output is not None
|
|
946
|
+
else ""
|
|
947
|
+
)
|
|
948
|
+
async for event in stream.aoutput_item_custom_tool_call_output(content.call_id or "", output):
|
|
949
|
+
yield event
|
|
950
|
+
elif content.type == "shell_tool_call":
|
|
951
|
+
action = FunctionShellAction(commands=content.commands or [], timeout_ms=0, max_output_length=0)
|
|
952
|
+
async for event in stream.aoutput_item_function_shell_call(
|
|
953
|
+
content.call_id or "",
|
|
954
|
+
action,
|
|
955
|
+
LocalEnvironmentResource(),
|
|
956
|
+
status=content.status or "completed",
|
|
957
|
+
):
|
|
958
|
+
yield event
|
|
959
|
+
elif content.type == "shell_tool_result":
|
|
960
|
+
output_items: list[FunctionShellCallOutputContent] = []
|
|
961
|
+
if content.outputs:
|
|
962
|
+
for out in content.outputs:
|
|
963
|
+
exit_code = getattr(out, "exit_code", None)
|
|
964
|
+
output_items.append(
|
|
965
|
+
FunctionShellCallOutputContent(
|
|
966
|
+
stdout=getattr(out, "stdout", "") or "",
|
|
967
|
+
stderr=getattr(out, "stderr", "") or "",
|
|
968
|
+
outcome=FunctionShellCallOutputExitOutcome(exit_code=exit_code if exit_code is not None else 0),
|
|
969
|
+
)
|
|
970
|
+
)
|
|
971
|
+
async for event in stream.aoutput_item_function_shell_call_output(
|
|
972
|
+
content.call_id or "",
|
|
973
|
+
output_items,
|
|
974
|
+
status=content.status or "completed",
|
|
975
|
+
max_output_length=content.max_output_length,
|
|
976
|
+
):
|
|
977
|
+
yield event
|
|
978
|
+
else:
|
|
979
|
+
# Log a warning for unsupported content types instead of raising an error to avoid breaking the response stream.
|
|
980
|
+
logger.warning(f"Content type '{content.type}' is not supported yet. This is usually safe to ignore.")
|
|
981
|
+
|
|
982
|
+
|
|
983
|
+
# endregion
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agent-framework-foundry-hosting
|
|
3
|
+
Version: 1.0.0a260421
|
|
4
|
+
Summary: Foundry Hosting integration for Microsoft Agent Framework.
|
|
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.1.0,<2
|
|
20
|
+
Requires-Dist: azure-ai-agentserver-core==2.0.0b2
|
|
21
|
+
Requires-Dist: azure-ai-agentserver-responses==1.0.0b4
|
|
22
|
+
Requires-Dist: azure-ai-agentserver-invocations==1.0.0b2
|
|
23
|
+
Project-URL: homepage, https://aka.ms/agent-framework
|
|
24
|
+
Project-URL: issues, https://github.com/microsoft/agent-framework/issues
|
|
25
|
+
Project-URL: release_notes, https://github.com/microsoft/agent-framework/releases?q=tag%3Apython-1&expanded=true
|
|
26
|
+
Project-URL: source, https://github.com/microsoft/agent-framework/tree/main/python
|
|
27
|
+
|
|
28
|
+
# Foundry Hosting
|
|
29
|
+
|
|
30
|
+
This package provides the integration of Agent Framework agents and workflows with the Foundry Agent Server, which can be hosted on Foundry infrastructure.
|
|
31
|
+
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
agent_framework_foundry_hosting/__init__.py,sha256=XGYSd5Y18zJxLyn_bM3U3WfhIk0Pp-y61icAAAVrRvw,363
|
|
2
|
+
agent_framework_foundry_hosting/_invocations.py,sha256=IJekqjZ7kFJRt_mXiaFHDNM79oFWygeVLIzjpMqqWW0,3153
|
|
3
|
+
agent_framework_foundry_hosting/_responses.py,sha256=sb3wT82I9gjHFy_gTKrMAWSQaPwRtYJkGd3dj3rYVTc,38650
|
|
4
|
+
agent_framework_foundry_hosting-1.0.0a260421.dist-info/licenses/LICENSE,sha256=ws_MuBL-SCEBqPBFl9_FqZkaaydIJmxHrJG2parhU4M,1141
|
|
5
|
+
agent_framework_foundry_hosting-1.0.0a260421.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
|
|
6
|
+
agent_framework_foundry_hosting-1.0.0a260421.dist-info/METADATA,sha256=aqpneBmC2dEUjDi9hPeJXSY7ma6Rj2PVQGgNuz2xP0E,1465
|
|
7
|
+
agent_framework_foundry_hosting-1.0.0a260421.dist-info/RECORD,,
|
|
@@ -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
|