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.
@@ -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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: flit 3.12.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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