mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68b93641b6 | ||
|
|
2b251d904f |
@@ -24,7 +24,6 @@
|
||||
],
|
||||
"words": [
|
||||
"aeiou",
|
||||
"agentserver",
|
||||
"agui",
|
||||
"aiplatform",
|
||||
"azuredocindex",
|
||||
|
||||
@@ -10,6 +10,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
### Changed
|
||||
- **agent-framework-azure-cosmos**: [BREAKING] `CosmosCheckpointStorage` now uses restricted pickle deserialization by default, matching `FileCheckpointStorage` behavior. If your checkpoints contain application-defined types, pass them via `allowed_checkpoint_types=["my_app.models:MyState"]`. ([#5200](https://github.com/microsoft/agent-framework/issues/5200))
|
||||
|
||||
## [devui-1.0.0b260414] - 2026-04-14
|
||||
|
||||
### Fixed
|
||||
- **agent-framework-devui**: Fix streaming memory growth in DevUI frontend ([#5221](https://github.com/microsoft/agent-framework/pull/5221))
|
||||
|
||||
## [1.0.1] - 2026-04-09
|
||||
|
||||
### Added
|
||||
|
||||
@@ -26,28 +26,6 @@ USER_AGENT_KEY: Final[str] = "User-Agent"
|
||||
HTTP_USER_AGENT: Final[str] = "agent-framework-python"
|
||||
AGENT_FRAMEWORK_USER_AGENT = f"{HTTP_USER_AGENT}/{version_info}" # type: ignore[has-type]
|
||||
|
||||
_user_agent_prefixes: list[str] = []
|
||||
|
||||
|
||||
def append_to_user_agent(prefix: str) -> None:
|
||||
"""Prepend a prefix to the agent framework user agent string.
|
||||
|
||||
This is useful for hosting layers that want to identify themselves in telemetry.
|
||||
Duplicate prefixes are ignored.
|
||||
|
||||
Args:
|
||||
prefix: The prefix to prepend (e.g. "foundry-hosting").
|
||||
"""
|
||||
if prefix and prefix not in _user_agent_prefixes:
|
||||
_user_agent_prefixes.append(prefix)
|
||||
|
||||
|
||||
def _get_user_agent() -> str:
|
||||
"""Return the full user agent string including any prepended prefixes."""
|
||||
if not _user_agent_prefixes:
|
||||
return AGENT_FRAMEWORK_USER_AGENT
|
||||
return f"{'/'.join(_user_agent_prefixes)}/{AGENT_FRAMEWORK_USER_AGENT}"
|
||||
|
||||
|
||||
def prepend_agent_framework_to_user_agent(headers: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
"""Prepend "agent-framework" to the User-Agent in the headers.
|
||||
@@ -79,9 +57,12 @@ def prepend_agent_framework_to_user_agent(headers: dict[str, Any] | None = None)
|
||||
"""
|
||||
if not IS_TELEMETRY_ENABLED:
|
||||
return headers or {}
|
||||
user_agent = _get_user_agent()
|
||||
if not headers:
|
||||
return {USER_AGENT_KEY: user_agent}
|
||||
headers[USER_AGENT_KEY] = f"{user_agent} {headers[USER_AGENT_KEY]}" if USER_AGENT_KEY in headers else user_agent
|
||||
return {USER_AGENT_KEY: AGENT_FRAMEWORK_USER_AGENT}
|
||||
headers[USER_AGENT_KEY] = (
|
||||
f"{AGENT_FRAMEWORK_USER_AGENT} {headers[USER_AGENT_KEY]}"
|
||||
if USER_AGENT_KEY in headers
|
||||
else AGENT_FRAMEWORK_USER_AGENT
|
||||
)
|
||||
|
||||
return headers
|
||||
|
||||
@@ -4,7 +4,7 @@ description = "Debug UI for Microsoft Agent Framework with OpenAI-compatible API
|
||||
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "1.0.0b260409"
|
||||
version = "1.0.0b260414"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://github.com/microsoft/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) Microsoft Corporation.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE
|
||||
@@ -1,11 +0,0 @@
|
||||
# Foundry Hosting
|
||||
|
||||
This package provides the integration of Agent Framework agents and workflows with the Foundry Agent Server, which can be hosted on Foundry infrastructure.
|
||||
|
||||
## Responses
|
||||
|
||||
TODO
|
||||
|
||||
## Invocations
|
||||
|
||||
TODO
|
||||
@@ -1,13 +0,0 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import importlib.metadata
|
||||
|
||||
from ._invocations import InvocationsHostServer
|
||||
from ._responses import ResponsesHostServer
|
||||
|
||||
try:
|
||||
__version__ = importlib.metadata.version(__name__)
|
||||
except importlib.metadata.PackageNotFoundError:
|
||||
__version__ = "0.0.0"
|
||||
|
||||
__all__ = ["InvocationsHostServer", "ResponsesHostServer"]
|
||||
@@ -1,75 +0,0 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from agent_framework import AgentSession, BaseAgent, SupportsAgentRun
|
||||
from agent_framework._telemetry import append_to_user_agent
|
||||
from azure.ai.agentserver.invocations import InvocationAgentServerHost
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import JSONResponse, Response, StreamingResponse
|
||||
from typing_extensions import Any, AsyncGenerator, Optional
|
||||
|
||||
|
||||
class InvocationsHostServer(InvocationAgentServerHost):
|
||||
"""An invocations server host for an agent."""
|
||||
|
||||
USER_AGENT_PREFIX = "foundry-hosting"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
agent: BaseAgent,
|
||||
*,
|
||||
openapi_spec: Optional[dict[str, Any]] = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Initialize an InvocationsHostServer.
|
||||
|
||||
Args:
|
||||
agent: The agent to handle responses for.
|
||||
openapi_spec: The OpenAPI specification for the server.
|
||||
**kwargs: Additional keyword arguments.
|
||||
|
||||
This host will expect the request to be a JSON body with a "message" field.
|
||||
The response from the host will be a JSON object with a "response" field containing
|
||||
the agent's response and a "session_id" field containing the session ID.
|
||||
"""
|
||||
super().__init__(openapi_spec=openapi_spec, **kwargs)
|
||||
|
||||
if not isinstance(agent, SupportsAgentRun):
|
||||
raise TypeError("Agent must support the SupportsAgentRun interface")
|
||||
|
||||
append_to_user_agent(self.USER_AGENT_PREFIX)
|
||||
self._agent = agent
|
||||
self._sessions: dict[str, AgentSession] = {}
|
||||
self.invoke_handler(self._handle_invoke) # pyright: ignore[reportUnknownMemberType]
|
||||
|
||||
async def _handle_invoke(self, request: Request) -> Response:
|
||||
"""Invoke the agent with the given request."""
|
||||
data = await request.json()
|
||||
session_id: str = request.state.session_id
|
||||
|
||||
stream = data.get("stream", False)
|
||||
user_message = data.get("message", None)
|
||||
if user_message is None:
|
||||
error = "Missing 'message' in request"
|
||||
if stream:
|
||||
return StreamingResponse(content=error, status_code=400)
|
||||
return Response(content=error, status_code=400)
|
||||
|
||||
session = self._sessions.setdefault(session_id, AgentSession(session_id=session_id))
|
||||
|
||||
if stream:
|
||||
|
||||
async def stream_response() -> AsyncGenerator[str]:
|
||||
async for update in self._agent.run(user_message, session=session, stream=True):
|
||||
yield update.text
|
||||
|
||||
return StreamingResponse(
|
||||
stream_response(),
|
||||
media_type="text/event-stream",
|
||||
headers={"Cache-Control": "no-cache", "Connection": "keep-alive"},
|
||||
)
|
||||
|
||||
response = await self._agent.run([user_message], session=session, stream=stream)
|
||||
return JSONResponse({
|
||||
"response": response.text,
|
||||
"session_id": session_id,
|
||||
})
|
||||
@@ -1,585 +0,0 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
from collections.abc import AsyncIterable, AsyncIterator, Generator, Mapping
|
||||
|
||||
from agent_framework import ChatOptions, Content, HistoryProvider, Message, RawAgent, SupportsAgentRun
|
||||
from agent_framework._telemetry import append_to_user_agent
|
||||
from azure.ai.agentserver.responses import (
|
||||
ResponseContext,
|
||||
ResponseEventStream,
|
||||
ResponseProviderProtocol,
|
||||
ResponsesServerOptions,
|
||||
)
|
||||
from azure.ai.agentserver.responses.hosting import ResponsesAgentServerHost
|
||||
from azure.ai.agentserver.responses.models import (
|
||||
ComputerScreenshotContent,
|
||||
CreateResponse,
|
||||
FunctionCallOutputItemParam,
|
||||
FunctionShellAction,
|
||||
FunctionShellCallOutputContent,
|
||||
FunctionShellCallOutputExitOutcome,
|
||||
LocalEnvironmentResource,
|
||||
MessageContent,
|
||||
MessageContentInputFileContent,
|
||||
MessageContentInputImageContent,
|
||||
MessageContentInputTextContent,
|
||||
MessageContentOutputTextContent,
|
||||
MessageContentReasoningTextContent,
|
||||
MessageContentRefusalContent,
|
||||
OutputItem,
|
||||
OutputItemFunctionToolCall,
|
||||
OutputItemMessage,
|
||||
OutputItemOutputMessage,
|
||||
OutputItemReasoningItem,
|
||||
OutputMessageContent,
|
||||
OutputMessageContentOutputTextContent,
|
||||
OutputMessageContentRefusalContent,
|
||||
ResponseStreamEvent,
|
||||
SummaryTextContent,
|
||||
TextContent,
|
||||
)
|
||||
from azure.ai.agentserver.responses.streaming._builders import (
|
||||
OutputItemFunctionCallBuilder,
|
||||
OutputItemMcpCallBuilder,
|
||||
OutputItemMessageBuilder,
|
||||
OutputItemReasoningItemBuilder,
|
||||
ReasoningSummaryPartBuilder,
|
||||
TextContentBuilder,
|
||||
)
|
||||
from typing_extensions import Any, Sequence, cast
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ResponsesHostServer(ResponsesAgentServerHost):
|
||||
"""A responses server host for an agent."""
|
||||
|
||||
USER_AGENT_PREFIX = "foundry-hosting"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
agent: SupportsAgentRun,
|
||||
*,
|
||||
prefix: str = "",
|
||||
options: ResponsesServerOptions | None = None,
|
||||
store: ResponseProviderProtocol | None = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Initialize a ResponsesHostServer.
|
||||
|
||||
Args:
|
||||
agent: The agent to handle responses for.
|
||||
prefix: The URL prefix for the server.
|
||||
options: Optional server options.
|
||||
store: Optional response store.
|
||||
**kwargs: Additional keyword arguments.
|
||||
|
||||
Note:
|
||||
The agent must not have a history provider with `load_messages=True`,
|
||||
because history is managed by the hosting infrastructure.
|
||||
"""
|
||||
super().__init__(prefix=prefix, options=options, store=store, **kwargs)
|
||||
|
||||
for provider in getattr(agent, "context_providers", []):
|
||||
if isinstance(provider, HistoryProvider) and provider.load_messages:
|
||||
raise RuntimeError(
|
||||
"There shouldn't be a history provider with `load_messages=True` already present. "
|
||||
"History is managed by the hosting infrastructure."
|
||||
)
|
||||
self._agent = agent
|
||||
|
||||
self.response_handler(self._handler) # pyright: ignore[reportUnknownMemberType]
|
||||
|
||||
# Append the user agent prefix for telemetry purposes
|
||||
append_to_user_agent(self.USER_AGENT_PREFIX)
|
||||
|
||||
async def _handler(
|
||||
self,
|
||||
request: CreateResponse,
|
||||
context: ResponseContext,
|
||||
cancellation_signal: asyncio.Event,
|
||||
) -> AsyncIterable[ResponseStreamEvent | dict[str, Any]]:
|
||||
"""Handle the creation of a response."""
|
||||
input_text = await context.get_input_text()
|
||||
history = await context.get_history()
|
||||
messages = [*_to_messages(history), input_text]
|
||||
|
||||
chat_options = _to_chat_options(request)
|
||||
|
||||
stream = ResponseEventStream(response_id=context.response_id, model=request.model)
|
||||
|
||||
yield stream.emit_created()
|
||||
yield stream.emit_in_progress()
|
||||
|
||||
if request.stream is None or request.stream is False:
|
||||
# Run the agent in non-streaming mode
|
||||
if isinstance(self._agent, RawAgent):
|
||||
raw_agent = cast("RawAgent[Any]", self._agent) # pyright: ignore[reportUnknownMemberType]
|
||||
response = await raw_agent.run(messages, stream=False, options=chat_options)
|
||||
else:
|
||||
response = await self._agent.run(messages, stream=False)
|
||||
|
||||
for message in response.messages:
|
||||
for content in message.contents:
|
||||
async for item in _to_outputs(stream, content):
|
||||
yield item
|
||||
|
||||
yield stream.emit_completed()
|
||||
return
|
||||
|
||||
# Start the streaming response
|
||||
if isinstance(self._agent, RawAgent):
|
||||
raw_agent = cast("RawAgent[Any]", self._agent) # pyright: ignore[reportUnknownMemberType]
|
||||
response_stream = raw_agent.run(messages, stream=True, options=chat_options)
|
||||
else:
|
||||
response_stream = self._agent.run(messages, stream=True)
|
||||
|
||||
# Track the current active output item builder for streaming;
|
||||
# lazily created on matching content, closed when a different type arrives.
|
||||
tracker = _OutputItemTracker(stream)
|
||||
|
||||
async for update in response_stream:
|
||||
for content in update.contents:
|
||||
for event in tracker.handle(content):
|
||||
yield event
|
||||
if tracker.needs_async:
|
||||
async for item in _to_outputs(stream, content):
|
||||
yield item
|
||||
tracker.needs_async = False
|
||||
|
||||
# Close any remaining active builder
|
||||
for event in tracker.close():
|
||||
yield event
|
||||
|
||||
yield stream.emit_completed()
|
||||
|
||||
|
||||
# region Active Builder State
|
||||
|
||||
|
||||
class _OutputItemTracker:
|
||||
"""Tracks the current active output item builder during streaming.
|
||||
|
||||
Handles lazy creation, delta emission, and closing of streaming builders
|
||||
for text messages, reasoning, function calls, and MCP calls.
|
||||
"""
|
||||
|
||||
_DELTA_TYPES = frozenset({"text", "text_reasoning", "function_call", "mcp_server_tool_call"})
|
||||
|
||||
def __init__(self, stream: ResponseEventStream) -> None:
|
||||
self._stream = stream
|
||||
self._active_type: str | None = None
|
||||
self._active_id: str | None = None
|
||||
# Accumulated delta text for the current active builder
|
||||
self._accumulated: list[str] = []
|
||||
# Builder state — only one is active at a time
|
||||
self._message_item: OutputItemMessageBuilder | None = None
|
||||
self._text_content: TextContentBuilder | None = None
|
||||
self._reasoning_item: OutputItemReasoningItemBuilder | None = None
|
||||
self._summary_part: ReasoningSummaryPartBuilder | None = None
|
||||
self._fc_builder: OutputItemFunctionCallBuilder | None = None
|
||||
self._mcp_builder: OutputItemMcpCallBuilder | None = None
|
||||
self.needs_async = False
|
||||
|
||||
def handle(self, content: Content) -> Generator[ResponseStreamEvent, None, None]:
|
||||
"""Process a content item, yielding sync events.
|
||||
|
||||
Sets ``needs_async = True`` if the caller must also drain an
|
||||
async ``_to_outputs`` call for this content.
|
||||
"""
|
||||
if content.type == "text" and content.text is not None:
|
||||
if self._active_type != "text":
|
||||
yield from self._close()
|
||||
yield from self._open_message()
|
||||
assert self._text_content is not None # noqa: S101
|
||||
self._accumulated.append(content.text)
|
||||
yield self._text_content.emit_delta(content.text)
|
||||
|
||||
elif content.type == "text_reasoning" and content.text is not None:
|
||||
if self._active_type != "text_reasoning":
|
||||
yield from self._close()
|
||||
yield from self._open_reasoning()
|
||||
assert self._summary_part is not None # noqa: S101
|
||||
self._accumulated.append(content.text)
|
||||
yield self._summary_part.emit_text_delta(content.text)
|
||||
|
||||
elif content.type == "function_call" and content.call_id is not None:
|
||||
if self._active_type != "function_call" or self._active_id != content.call_id:
|
||||
yield from self._close()
|
||||
yield from self._open_function_call(content)
|
||||
assert self._fc_builder is not None # noqa: S101
|
||||
args_str = _arguments_to_str(content.arguments)
|
||||
self._accumulated.append(args_str)
|
||||
yield self._fc_builder.emit_arguments_delta(args_str)
|
||||
|
||||
elif content.type == "mcp_server_tool_call" and content.tool_name:
|
||||
key = f"{content.server_name or 'default'}::{content.tool_name}"
|
||||
if self._active_type != "mcp_server_tool_call" or self._active_id != key:
|
||||
yield from self._close()
|
||||
yield from self._open_mcp_call(content)
|
||||
assert self._mcp_builder is not None # noqa: S101
|
||||
args_str = _arguments_to_str(content.arguments)
|
||||
self._accumulated.append(args_str)
|
||||
yield self._mcp_builder.emit_arguments_delta(args_str)
|
||||
|
||||
else:
|
||||
yield from self._close()
|
||||
self.needs_async = True
|
||||
|
||||
def close(self) -> Generator[ResponseStreamEvent, None, None]:
|
||||
"""Close any remaining active builder."""
|
||||
yield from self._close()
|
||||
|
||||
# -- Private open/close helpers --
|
||||
|
||||
def _open_message(self) -> Generator[ResponseStreamEvent, None, None]:
|
||||
self._message_item = self._stream.add_output_item_message()
|
||||
self._text_content = self._message_item.add_text_content()
|
||||
self._active_type = "text"
|
||||
self._active_id = None
|
||||
yield self._message_item.emit_added()
|
||||
yield self._text_content.emit_added()
|
||||
|
||||
def _open_reasoning(self) -> Generator[ResponseStreamEvent, None, None]:
|
||||
self._reasoning_item = self._stream.add_output_item_reasoning_item()
|
||||
self._summary_part = self._reasoning_item.add_summary_part()
|
||||
self._active_type = "text_reasoning"
|
||||
self._active_id = None
|
||||
yield self._reasoning_item.emit_added()
|
||||
yield self._summary_part.emit_added()
|
||||
|
||||
def _open_function_call(self, content: Content) -> Generator[ResponseStreamEvent, None, None]:
|
||||
self._fc_builder = self._stream.add_output_item_function_call(
|
||||
name=content.name or "",
|
||||
call_id=content.call_id or "",
|
||||
)
|
||||
self._active_type = "function_call"
|
||||
self._active_id = content.call_id
|
||||
yield self._fc_builder.emit_added()
|
||||
|
||||
def _open_mcp_call(self, content: Content) -> Generator[ResponseStreamEvent, None, None]:
|
||||
self._mcp_builder = self._stream.add_output_item_mcp_call(
|
||||
server_label=content.server_name or "default",
|
||||
name=content.tool_name or "",
|
||||
)
|
||||
self._active_type = "mcp_server_tool_call"
|
||||
self._active_id = f"{content.server_name or 'default'}::{content.tool_name}"
|
||||
yield self._mcp_builder.emit_added()
|
||||
|
||||
def _close(self) -> Generator[ResponseStreamEvent, None, None]:
|
||||
accumulated = "".join(self._accumulated)
|
||||
|
||||
if self._active_type == "text" and self._text_content and self._message_item:
|
||||
yield self._text_content.emit_text_done(accumulated)
|
||||
yield self._text_content.emit_done()
|
||||
yield self._message_item.emit_done()
|
||||
self._text_content = None
|
||||
self._message_item = None
|
||||
|
||||
elif self._active_type == "text_reasoning" and self._summary_part and self._reasoning_item:
|
||||
yield self._summary_part.emit_text_done(accumulated)
|
||||
yield self._summary_part.emit_done()
|
||||
yield self._reasoning_item.emit_done()
|
||||
self._summary_part = None
|
||||
self._reasoning_item = None
|
||||
|
||||
elif self._active_type == "function_call" and self._fc_builder:
|
||||
yield self._fc_builder.emit_arguments_done(accumulated)
|
||||
yield self._fc_builder.emit_done()
|
||||
self._fc_builder = None
|
||||
|
||||
elif self._active_type == "mcp_server_tool_call" and self._mcp_builder:
|
||||
yield self._mcp_builder.emit_arguments_done(accumulated)
|
||||
yield self._mcp_builder.emit_completed()
|
||||
yield self._mcp_builder.emit_done()
|
||||
self._mcp_builder = None
|
||||
|
||||
self._active_type = None
|
||||
self._active_id = None
|
||||
self._accumulated.clear()
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
|
||||
# region Option Conversion
|
||||
|
||||
|
||||
def _to_chat_options(request: CreateResponse) -> ChatOptions:
|
||||
"""Converts a CreateResponse request to ChatOptions.
|
||||
|
||||
Args:
|
||||
request (CreateResponse): The request to convert.
|
||||
|
||||
Returns:
|
||||
ChatOptions: The converted ChatOptions.
|
||||
"""
|
||||
chat_options = ChatOptions()
|
||||
|
||||
if request.temperature is not None:
|
||||
chat_options["temperature"] = request.temperature
|
||||
if request.top_p is not None:
|
||||
chat_options["top_p"] = request.top_p
|
||||
if request.max_output_tokens is not None:
|
||||
chat_options["max_tokens"] = request.max_output_tokens
|
||||
if request.parallel_tool_calls is not None:
|
||||
chat_options["allow_multiple_tool_calls"] = request.parallel_tool_calls
|
||||
|
||||
return chat_options
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
|
||||
# region Input Message Conversion
|
||||
|
||||
|
||||
def _to_messages(history: Sequence[OutputItem]) -> list[Message]:
|
||||
"""Converts a sequence of OutputItem objects to a list of Message objects.
|
||||
|
||||
Args:
|
||||
history (Sequence[OutputItem]): The sequence of OutputItem objects to convert.
|
||||
|
||||
Returns:
|
||||
list[Message]: The list of Message objects.
|
||||
"""
|
||||
messages: list[Message] = []
|
||||
for item in history:
|
||||
messages.append(_to_message(item))
|
||||
return messages
|
||||
|
||||
|
||||
def _to_message(item: OutputItem) -> Message:
|
||||
"""Converts an OutputItem to a Message.
|
||||
|
||||
Args:
|
||||
item (OutputItem): The OutputItem to convert.
|
||||
|
||||
Returns:
|
||||
Message: The converted Message.
|
||||
|
||||
Raises:
|
||||
ValueError: If the OutputItem type is not supported.
|
||||
"""
|
||||
if item.type == "output_message":
|
||||
msg = cast(OutputItemOutputMessage, item)
|
||||
contents = [_convert_output_message_content(part) for part in msg.content]
|
||||
return Message(role=msg.role, contents=contents)
|
||||
|
||||
if item.type == "message":
|
||||
msg = cast(OutputItemMessage, item)
|
||||
contents = [_convert_message_content(part) for part in msg.content]
|
||||
return Message(role=msg.role, contents=contents)
|
||||
|
||||
if item.type == "function_call":
|
||||
fc = cast(OutputItemFunctionToolCall, item)
|
||||
return Message(
|
||||
role="assistant",
|
||||
contents=[Content.from_function_call(fc.call_id, fc.name, arguments=fc.arguments)],
|
||||
)
|
||||
|
||||
if item.type == "function_call_output":
|
||||
fco = cast(FunctionCallOutputItemParam, item)
|
||||
output = fco.output if isinstance(fco.output, str) else str(fco.output)
|
||||
return Message(
|
||||
role="tool",
|
||||
contents=[Content.from_function_result(fco.call_id, result=output)],
|
||||
)
|
||||
|
||||
if item.type == "reasoning":
|
||||
reasoning = cast(OutputItemReasoningItem, item)
|
||||
contents: list[Content] = []
|
||||
if reasoning.summary:
|
||||
for summary in reasoning.summary:
|
||||
contents.append(Content.from_text(summary.text))
|
||||
return Message(role="assistant", contents=contents)
|
||||
|
||||
raise ValueError(f"Unsupported OutputItem type: {item.type}")
|
||||
|
||||
|
||||
def _convert_output_message_content(content: OutputMessageContent) -> Content:
|
||||
"""Converts an OutputMessageContent to a Content object.
|
||||
|
||||
Args:
|
||||
content (OutputMessageContent): The OutputMessageContent to convert.
|
||||
|
||||
Returns:
|
||||
Content: The converted Content object.
|
||||
|
||||
Raises:
|
||||
ValueError: If the OutputMessageContent type is not supported.
|
||||
"""
|
||||
if content.type == "output_text":
|
||||
text_content = cast(OutputMessageContentOutputTextContent, content)
|
||||
return Content.from_text(text_content.text)
|
||||
if content.type == "refusal":
|
||||
refusal_content = cast(OutputMessageContentRefusalContent, content)
|
||||
return Content.from_text(refusal_content.refusal)
|
||||
|
||||
raise ValueError(f"Unsupported OutputMessageContent type: {content.type}")
|
||||
|
||||
|
||||
def _convert_message_content(content: MessageContent) -> Content:
|
||||
"""Converts a MessageContent to a Content object.
|
||||
|
||||
Args:
|
||||
content (MessageContent): The MessageContent to convert.
|
||||
|
||||
Returns:
|
||||
Content: The converted Content object.
|
||||
|
||||
Raises:
|
||||
ValueError: If the MessageContent type is not supported.
|
||||
"""
|
||||
if content.type == "input_text":
|
||||
input_text = cast(MessageContentInputTextContent, content)
|
||||
return Content.from_text(input_text.text)
|
||||
if content.type == "output_text":
|
||||
output_text = cast(MessageContentOutputTextContent, content)
|
||||
return Content.from_text(output_text.text)
|
||||
if content.type == "text":
|
||||
text = cast(TextContent, content)
|
||||
return Content.from_text(text.text)
|
||||
if content.type == "summary_text":
|
||||
summary = cast(SummaryTextContent, content)
|
||||
return Content.from_text(summary.text)
|
||||
if content.type == "refusal":
|
||||
refusal = cast(MessageContentRefusalContent, content)
|
||||
return Content.from_text(refusal.refusal)
|
||||
if content.type == "reasoning_text":
|
||||
reasoning = cast(MessageContentReasoningTextContent, content)
|
||||
return Content.from_text_reasoning(text=reasoning.text)
|
||||
if content.type == "input_image":
|
||||
image = cast(MessageContentInputImageContent, content)
|
||||
if image.image_url:
|
||||
return Content.from_uri(image.image_url)
|
||||
if image.file_id:
|
||||
return Content.from_hosted_file(image.file_id)
|
||||
if content.type == "input_file":
|
||||
file = cast(MessageContentInputFileContent, content)
|
||||
if file.file_url:
|
||||
return Content.from_uri(file.file_url)
|
||||
if file.file_id:
|
||||
return Content.from_hosted_file(file.file_id, name=file.filename)
|
||||
if content.type == "computer_screenshot":
|
||||
screenshot = cast(ComputerScreenshotContent, content)
|
||||
return Content.from_uri(screenshot.image_url)
|
||||
|
||||
raise ValueError(f"Unsupported MessageContent type: {content.type}")
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
# region Output Item Conversion
|
||||
|
||||
|
||||
def _arguments_to_str(arguments: str | Mapping[str, Any] | None) -> str:
|
||||
"""Convert arguments to a JSON string.
|
||||
|
||||
Args:
|
||||
arguments: The arguments to convert, can be a string, mapping, or None.
|
||||
|
||||
Returns:
|
||||
The arguments as a JSON string.
|
||||
"""
|
||||
if arguments is None:
|
||||
return ""
|
||||
if isinstance(arguments, str):
|
||||
return arguments
|
||||
return json.dumps(arguments)
|
||||
|
||||
|
||||
async def _to_outputs(stream: ResponseEventStream, content: Content) -> AsyncIterator[ResponseStreamEvent]:
|
||||
"""Converts a Content object to an async sequence of ResponseStreamEvent objects.
|
||||
|
||||
Args:
|
||||
stream: The ResponseEventStream to use for building events.
|
||||
content: The Content to convert.
|
||||
|
||||
Yields:
|
||||
ResponseStreamEvent: The converted event objects.
|
||||
|
||||
Raises:
|
||||
ValueError: If the Content type is not supported.
|
||||
"""
|
||||
if content.type == "text" and content.text is not None:
|
||||
async for event in stream.aoutput_item_message(content.text):
|
||||
yield event
|
||||
elif content.type == "text_reasoning" and content.text is not None:
|
||||
async for event in stream.aoutput_item_reasoning_item(content.text):
|
||||
yield event
|
||||
elif content.type == "function_call":
|
||||
async for event in stream.aoutput_item_function_call(
|
||||
content.name, # type: ignore[arg-type]
|
||||
content.call_id, # type: ignore[arg-type]
|
||||
_arguments_to_str(content.arguments),
|
||||
):
|
||||
yield event
|
||||
elif content.type == "function_result":
|
||||
async for event in stream.aoutput_item_function_call_output(
|
||||
content.call_id, # type: ignore[arg-type]
|
||||
str(content.result or ""),
|
||||
):
|
||||
yield event
|
||||
elif content.type == "image_generation_tool_result" and content.outputs is not None:
|
||||
async for event in stream.aoutput_item_image_gen_call(str(content.outputs)):
|
||||
yield event
|
||||
elif content.type == "mcp_server_tool_call":
|
||||
mcp_call = stream.add_output_item_mcp_call(
|
||||
server_label=content.server_name or "default",
|
||||
name=content.tool_name or "",
|
||||
)
|
||||
yield mcp_call.emit_added()
|
||||
async for event in mcp_call.aarguments(_arguments_to_str(content.arguments)):
|
||||
yield event
|
||||
yield mcp_call.emit_completed()
|
||||
yield mcp_call.emit_done()
|
||||
elif content.type == "mcp_server_tool_result":
|
||||
output = (
|
||||
content.output
|
||||
if isinstance(content.output, str)
|
||||
else str(content.output)
|
||||
if content.output is not None
|
||||
else ""
|
||||
)
|
||||
async for event in stream.aoutput_item_custom_tool_call_output(content.call_id or "", output):
|
||||
yield event
|
||||
elif content.type == "shell_tool_call":
|
||||
action = FunctionShellAction(commands=content.commands or [], timeout_ms=0, max_output_length=0)
|
||||
async for event in stream.aoutput_item_function_shell_call(
|
||||
content.call_id or "",
|
||||
action,
|
||||
LocalEnvironmentResource(),
|
||||
status=content.status or "completed",
|
||||
):
|
||||
yield event
|
||||
elif content.type == "shell_tool_result":
|
||||
output_items: list[FunctionShellCallOutputContent] = []
|
||||
if content.outputs:
|
||||
for out in content.outputs:
|
||||
exit_code = getattr(out, "exit_code", None)
|
||||
output_items.append(
|
||||
FunctionShellCallOutputContent(
|
||||
stdout=getattr(out, "stdout", "") or "",
|
||||
stderr=getattr(out, "stderr", "") or "",
|
||||
outcome=FunctionShellCallOutputExitOutcome(exit_code=exit_code if exit_code is not None else 0),
|
||||
)
|
||||
)
|
||||
async for event in stream.aoutput_item_function_shell_call_output(
|
||||
content.call_id or "",
|
||||
output_items,
|
||||
status=content.status or "completed",
|
||||
max_output_length=content.max_output_length,
|
||||
):
|
||||
yield event
|
||||
else:
|
||||
# Log a warning for unsupported content types instead of raising an error to avoid breaking the response stream.
|
||||
logger.warning(f"Content type '{content.type}' is not supported yet.")
|
||||
|
||||
|
||||
# endregion
|
||||
@@ -1,99 +0,0 @@
|
||||
[project]
|
||||
name = "agent-framework-foundry-hosting"
|
||||
description = "Foundry Hosting integration for Microsoft Agent Framework."
|
||||
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "1.0.0a260402"
|
||||
license-files = ["LICENSE"]
|
||||
urls.homepage = "https://aka.ms/agent-framework"
|
||||
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
|
||||
urls.release_notes = "https://github.com/microsoft/agent-framework/releases?q=tag%3Apython-1&expanded=true"
|
||||
urls.issues = "https://github.com/microsoft/agent-framework/issues"
|
||||
classifiers = [
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Development Status :: 4 - Alpha",
|
||||
"Intended Audience :: Developers",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
"Programming Language :: Python :: 3.14",
|
||||
"Typing :: Typed",
|
||||
]
|
||||
dependencies = [
|
||||
"agent-framework-core>=1.0.0,<2",
|
||||
"azure-ai-agentserver-core==2.0.0b1",
|
||||
"azure-ai-agentserver-responses==1.0.0b1",
|
||||
"azure-ai-agentserver-invocations==1.0.0b1",
|
||||
]
|
||||
|
||||
[tool.uv]
|
||||
prerelease = "if-necessary-or-explicit"
|
||||
environments = [
|
||||
"sys_platform == 'darwin'",
|
||||
"sys_platform == 'linux'",
|
||||
"sys_platform == 'win32'"
|
||||
]
|
||||
|
||||
[tool.uv-dynamic-versioning]
|
||||
fallback-version = "0.0.0"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = 'tests'
|
||||
addopts = "-ra -q -r fEX"
|
||||
asyncio_mode = "auto"
|
||||
asyncio_default_fixture_loop_scope = "function"
|
||||
filterwarnings = []
|
||||
timeout = 120
|
||||
markers = [
|
||||
"integration: marks tests as integration tests that require external services",
|
||||
]
|
||||
|
||||
[tool.ruff]
|
||||
extend = "../../pyproject.toml"
|
||||
|
||||
[tool.coverage.run]
|
||||
omit = [
|
||||
"**/__init__.py"
|
||||
]
|
||||
|
||||
[tool.pyright]
|
||||
extends = "../../pyproject.toml"
|
||||
include = ["agent_framework_foundry_hosting"]
|
||||
exclude = ['tests']
|
||||
|
||||
[tool.mypy]
|
||||
plugins = ['pydantic.mypy']
|
||||
strict = true
|
||||
python_version = "3.10"
|
||||
ignore_missing_imports = true
|
||||
disallow_untyped_defs = true
|
||||
no_implicit_optional = true
|
||||
check_untyped_defs = true
|
||||
warn_return_any = true
|
||||
show_error_codes = true
|
||||
warn_unused_ignores = false
|
||||
disallow_incomplete_defs = true
|
||||
disallow_untyped_decorators = true
|
||||
|
||||
[tool.bandit]
|
||||
targets = ["agent_framework_foundry_hosting"]
|
||||
exclude_dirs = ["tests"]
|
||||
|
||||
[tool.poe]
|
||||
executor.type = "uv"
|
||||
include = "../../shared_tasks.toml"
|
||||
|
||||
[tool.poe.tasks.mypy]
|
||||
help = "Run MyPy for this package."
|
||||
cmd = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_foundry_hosting"
|
||||
|
||||
[tool.poe.tasks.test]
|
||||
help = "Run the default unit test suite for this package."
|
||||
cmd = 'pytest -m "not integration" --cov=agent_framework_foundry_hosting --cov-report=term-missing:skip-covered tests'
|
||||
|
||||
[build-system]
|
||||
requires = ["flit-core >= 3.11,<4.0"]
|
||||
build-backend = "flit_core.buildapi"
|
||||
@@ -1,524 +0,0 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
"""HTTP round-trip tests for ResponsesHostServer.
|
||||
|
||||
These tests exercise the full HTTP pipeline using httpx.AsyncClient with
|
||||
ASGITransport — no real server process is started. Requests go through
|
||||
the Starlette routing stack, the Responses API middleware, and arrive at
|
||||
the registered _handle_create handler.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from collections.abc import AsyncIterator
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
from agent_framework import (
|
||||
AgentResponse,
|
||||
AgentResponseUpdate,
|
||||
Content,
|
||||
HistoryProvider,
|
||||
Message,
|
||||
RawAgent,
|
||||
ResponseStream,
|
||||
)
|
||||
from azure.ai.agentserver.responses import InMemoryResponseProvider
|
||||
from typing_extensions import Any
|
||||
|
||||
from agent_framework_foundry_hosting import ResponsesHostServer
|
||||
|
||||
# region Helpers
|
||||
|
||||
|
||||
def _make_agent(
|
||||
*,
|
||||
response: AgentResponse | None = None,
|
||||
stream_updates: list[AgentResponseUpdate] | None = None,
|
||||
) -> MagicMock:
|
||||
"""Create a mock agent implementing SupportsAgentRun."""
|
||||
agent = MagicMock(spec=RawAgent)
|
||||
agent.id = "test-agent"
|
||||
agent.name = "Test Agent"
|
||||
agent.description = "A mock agent for testing"
|
||||
agent.context_providers = []
|
||||
|
||||
if response is not None:
|
||||
|
||||
async def run_non_streaming(*args: Any, **kwargs: Any) -> AgentResponse:
|
||||
return response
|
||||
|
||||
agent.run = AsyncMock(side_effect=run_non_streaming)
|
||||
|
||||
if stream_updates is not None:
|
||||
|
||||
async def _stream_gen() -> AsyncIterator[AgentResponseUpdate]:
|
||||
for update in stream_updates:
|
||||
yield update
|
||||
|
||||
def run_streaming(*args: Any, **kwargs: Any) -> Any:
|
||||
if kwargs.get("stream"):
|
||||
return ResponseStream(_stream_gen()) # type: ignore
|
||||
raise NotImplementedError("Only streaming is configured on this mock")
|
||||
|
||||
agent.run = MagicMock(side_effect=run_streaming)
|
||||
|
||||
return agent
|
||||
|
||||
|
||||
def _make_server(agent: MagicMock, **kwargs: Any) -> ResponsesHostServer:
|
||||
"""Create a ResponsesHostServer with an in-memory store."""
|
||||
return ResponsesHostServer(agent, store=InMemoryResponseProvider(), **kwargs)
|
||||
|
||||
|
||||
async def _post(
|
||||
server: ResponsesHostServer,
|
||||
*,
|
||||
input_text: str = "Hello",
|
||||
model: str = "test-model",
|
||||
stream: bool = False,
|
||||
temperature: float | None = None,
|
||||
top_p: float | None = None,
|
||||
max_output_tokens: int | None = None,
|
||||
parallel_tool_calls: bool | None = None,
|
||||
) -> httpx.Response:
|
||||
"""Send a POST /responses request through the ASGI transport."""
|
||||
payload: dict[str, Any] = {"model": model, "input": input_text, "stream": stream}
|
||||
if temperature is not None:
|
||||
payload["temperature"] = temperature
|
||||
if top_p is not None:
|
||||
payload["top_p"] = top_p
|
||||
if max_output_tokens is not None:
|
||||
payload["max_output_tokens"] = max_output_tokens
|
||||
if parallel_tool_calls is not None:
|
||||
payload["parallel_tool_calls"] = parallel_tool_calls
|
||||
|
||||
transport = httpx.ASGITransport(app=server)
|
||||
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
|
||||
return await client.post("/responses", json=payload)
|
||||
|
||||
|
||||
def _parse_sse_events(body: str) -> list[dict[str, Any]]:
|
||||
"""Parse SSE text into a list of event dicts with 'event' and 'data' keys."""
|
||||
events: list[dict[str, Any]] = []
|
||||
current_event: str | None = None
|
||||
current_data_lines: list[str] = []
|
||||
|
||||
for line in body.split("\n"):
|
||||
if line.startswith("event: "):
|
||||
current_event = line[len("event: ") :]
|
||||
elif line.startswith("data: "):
|
||||
current_data_lines.append(line[len("data: ") :])
|
||||
elif line.strip() == "" and current_event is not None:
|
||||
data_str = "\n".join(current_data_lines)
|
||||
try:
|
||||
data = json.loads(data_str)
|
||||
except json.JSONDecodeError:
|
||||
data = data_str
|
||||
events.append({"event": current_event, "data": data})
|
||||
current_event = None
|
||||
current_data_lines = []
|
||||
|
||||
return events
|
||||
|
||||
|
||||
def _sse_event_types(events: list[dict[str, Any]]) -> list[str]:
|
||||
"""Extract event type strings from parsed SSE events."""
|
||||
return [e["event"] for e in events]
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
|
||||
# region Initialization
|
||||
|
||||
|
||||
class TestResponsesHostServerInit:
|
||||
def test_init_basic(self) -> None:
|
||||
agent = _make_agent(
|
||||
response=AgentResponse(messages=[Message(role="assistant", contents=[Content.from_text("hi")])])
|
||||
)
|
||||
server = _make_server(agent)
|
||||
assert server is not None
|
||||
|
||||
def test_init_rejects_history_provider_with_load_messages(self) -> None:
|
||||
hp = HistoryProvider(source_id="test", load_messages=True)
|
||||
agent = _make_agent(
|
||||
response=AgentResponse(messages=[Message(role="assistant", contents=[Content.from_text("hi")])])
|
||||
)
|
||||
agent.context_providers = [hp]
|
||||
with pytest.raises(RuntimeError, match="history provider"):
|
||||
ResponsesHostServer(agent)
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
|
||||
# region Health Check
|
||||
|
||||
|
||||
class TestHealthCheck:
|
||||
async def test_readiness(self) -> None:
|
||||
agent = _make_agent(
|
||||
response=AgentResponse(messages=[Message(role="assistant", contents=[Content.from_text("hi")])])
|
||||
)
|
||||
server = _make_server(agent)
|
||||
transport = httpx.ASGITransport(app=server)
|
||||
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
|
||||
resp = await client.get("/readiness")
|
||||
assert resp.status_code == 200
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
|
||||
# region Non-streaming
|
||||
|
||||
|
||||
class TestNonStreaming:
|
||||
async def test_basic_text_response(self) -> None:
|
||||
agent = _make_agent(
|
||||
response=AgentResponse(messages=[Message(role="assistant", contents=[Content.from_text("Hello!")])])
|
||||
)
|
||||
server = _make_server(agent)
|
||||
resp = await _post(server, input_text="Hi", stream=False)
|
||||
|
||||
assert resp.status_code == 200
|
||||
assert "application/json" in resp.headers["content-type"]
|
||||
|
||||
body = resp.json()
|
||||
assert body["object"] == "response"
|
||||
assert body["status"] == "completed"
|
||||
assert len(body["output"]) > 0
|
||||
|
||||
# Find the message output item with our text
|
||||
text_found = False
|
||||
for item in body["output"]:
|
||||
assert item["type"] == "message"
|
||||
for part in item.get("content", []):
|
||||
if part.get("type") == "output_text" and part.get("text") == "Hello!":
|
||||
text_found = True
|
||||
assert text_found, f"Expected 'Hello!' in output, got: {body['output']}"
|
||||
|
||||
async def test_function_call_and_result(self) -> None:
|
||||
agent = _make_agent(
|
||||
response=AgentResponse(
|
||||
messages=[
|
||||
Message(
|
||||
role="assistant",
|
||||
contents=[Content.from_function_call("call_1", "get_weather", arguments='{"loc": "NYC"}')],
|
||||
),
|
||||
Message(role="tool", contents=[Content.from_function_result("call_1", result="sunny")]),
|
||||
Message(role="assistant", contents=[Content.from_text("The weather is sunny!")]),
|
||||
]
|
||||
)
|
||||
)
|
||||
server = _make_server(agent)
|
||||
resp = await _post(server, stream=False)
|
||||
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert body["status"] == "completed"
|
||||
|
||||
types = [item["type"] for item in body["output"]]
|
||||
assert "function_call" in types
|
||||
assert "function_call_output" in types
|
||||
assert "message" in types
|
||||
|
||||
async def test_reasoning_content(self) -> None:
|
||||
agent = _make_agent(
|
||||
response=AgentResponse(
|
||||
messages=[
|
||||
Message(
|
||||
role="assistant",
|
||||
contents=[
|
||||
Content.from_text_reasoning(text="Let me think..."),
|
||||
Content.from_text("The answer is 42"),
|
||||
],
|
||||
),
|
||||
]
|
||||
)
|
||||
)
|
||||
server = _make_server(agent)
|
||||
resp = await _post(server, stream=False)
|
||||
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert body["status"] == "completed"
|
||||
|
||||
types = [item["type"] for item in body["output"]]
|
||||
assert "reasoning" in types
|
||||
assert "message" in types
|
||||
|
||||
async def test_empty_response(self) -> None:
|
||||
agent = _make_agent(response=AgentResponse(messages=[]))
|
||||
server = _make_server(agent)
|
||||
resp = await _post(server, stream=False)
|
||||
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert body["status"] == "completed"
|
||||
|
||||
async def test_chat_options_forwarded(self) -> None:
|
||||
agent = _make_agent(
|
||||
response=AgentResponse(messages=[Message(role="assistant", contents=[Content.from_text("ok")])])
|
||||
)
|
||||
server = _make_server(agent)
|
||||
resp = await _post(server, stream=False, temperature=0.5, top_p=0.9, max_output_tokens=1024)
|
||||
|
||||
assert resp.status_code == 200
|
||||
agent.run.assert_awaited_once()
|
||||
call_kwargs = agent.run.call_args.kwargs
|
||||
assert call_kwargs["stream"] is False
|
||||
options = call_kwargs["options"]
|
||||
assert options["temperature"] == 0.5
|
||||
assert options["top_p"] == 0.9
|
||||
assert options["max_tokens"] == 1024
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
|
||||
# region Streaming
|
||||
|
||||
|
||||
class TestStreaming:
|
||||
async def test_basic_text_streaming(self) -> None:
|
||||
agent = _make_agent(
|
||||
stream_updates=[
|
||||
AgentResponseUpdate(contents=[Content.from_text("Hello ")], role="assistant"),
|
||||
AgentResponseUpdate(contents=[Content.from_text("world!")], role="assistant"),
|
||||
]
|
||||
)
|
||||
server = _make_server(agent)
|
||||
resp = await _post(server, stream=True)
|
||||
|
||||
assert resp.status_code == 200
|
||||
assert "text/event-stream" in resp.headers["content-type"]
|
||||
|
||||
events = _parse_sse_events(resp.text)
|
||||
types = _sse_event_types(events)
|
||||
|
||||
assert types[0] == "response.created"
|
||||
assert types[1] == "response.in_progress"
|
||||
assert types[-1] == "response.completed"
|
||||
assert "response.output_text.delta" in types
|
||||
assert types.count("response.output_text.delta") == 2
|
||||
assert "response.output_text.done" in types
|
||||
|
||||
# Verify the accumulated text in the done event
|
||||
done_events = [e for e in events if e["event"] == "response.output_text.done"]
|
||||
assert len(done_events) == 1
|
||||
assert done_events[0]["data"]["text"] == "Hello world!"
|
||||
|
||||
async def test_function_call_streaming(self) -> None:
|
||||
agent = _make_agent(
|
||||
stream_updates=[
|
||||
AgentResponseUpdate(
|
||||
contents=[Content.from_function_call("call_1", "search", arguments='{"q":')],
|
||||
role="assistant",
|
||||
),
|
||||
AgentResponseUpdate(
|
||||
contents=[Content.from_function_call("call_1", "search", arguments=' "hello"}')],
|
||||
role="assistant",
|
||||
),
|
||||
]
|
||||
)
|
||||
server = _make_server(agent)
|
||||
resp = await _post(server, stream=True)
|
||||
|
||||
assert resp.status_code == 200
|
||||
events = _parse_sse_events(resp.text)
|
||||
types = _sse_event_types(events)
|
||||
|
||||
assert types[0] == "response.created"
|
||||
assert types[-1] == "response.completed"
|
||||
assert types.count("response.function_call_arguments.delta") == 2
|
||||
assert "response.function_call_arguments.done" in types
|
||||
|
||||
# Verify accumulated arguments
|
||||
args_done = [e for e in events if e["event"] == "response.function_call_arguments.done"]
|
||||
assert len(args_done) == 1
|
||||
assert args_done[0]["data"]["arguments"] == '{"q": "hello"}'
|
||||
|
||||
async def test_alternating_text_and_function_call(self) -> None:
|
||||
agent = _make_agent(
|
||||
stream_updates=[
|
||||
# Text deltas
|
||||
AgentResponseUpdate(contents=[Content.from_text("Let me ")], role="assistant"),
|
||||
AgentResponseUpdate(contents=[Content.from_text("search...")], role="assistant"),
|
||||
# Function call argument deltas
|
||||
AgentResponseUpdate(
|
||||
contents=[Content.from_function_call("call_1", "search", arguments='{"q":')],
|
||||
role="assistant",
|
||||
),
|
||||
AgentResponseUpdate(
|
||||
contents=[Content.from_function_call("call_1", "search", arguments=' "x"}')],
|
||||
role="assistant",
|
||||
),
|
||||
# More text deltas
|
||||
AgentResponseUpdate(contents=[Content.from_text("Found ")], role="assistant"),
|
||||
AgentResponseUpdate(contents=[Content.from_text("it!")], role="assistant"),
|
||||
]
|
||||
)
|
||||
server = _make_server(agent)
|
||||
resp = await _post(server, stream=True)
|
||||
|
||||
assert resp.status_code == 200
|
||||
events = _parse_sse_events(resp.text)
|
||||
types = _sse_event_types(events)
|
||||
|
||||
assert types[0] == "response.created"
|
||||
assert types[-1] == "response.completed"
|
||||
|
||||
# 4 text deltas + 2 function call argument deltas
|
||||
assert types.count("response.output_text.delta") == 4
|
||||
assert types.count("response.function_call_arguments.delta") == 2
|
||||
|
||||
# 3 distinct output items (text, fc, text)
|
||||
assert types.count("response.output_item.added") == 3
|
||||
assert types.count("response.output_item.done") == 3
|
||||
|
||||
# Verify accumulated content
|
||||
text_done = [e for e in events if e["event"] == "response.output_text.done"]
|
||||
assert len(text_done) == 2
|
||||
assert text_done[0]["data"]["text"] == "Let me search..."
|
||||
assert text_done[1]["data"]["text"] == "Found it!"
|
||||
|
||||
args_done = [e for e in events if e["event"] == "response.function_call_arguments.done"]
|
||||
assert len(args_done) == 1
|
||||
assert args_done[0]["data"]["arguments"] == '{"q": "x"}'
|
||||
|
||||
async def test_reasoning_then_text_streaming(self) -> None:
|
||||
agent = _make_agent(
|
||||
stream_updates=[
|
||||
# Reasoning deltas
|
||||
AgentResponseUpdate(contents=[Content.from_text_reasoning(text="Let me ")], role="assistant"),
|
||||
AgentResponseUpdate(contents=[Content.from_text_reasoning(text="think...")], role="assistant"),
|
||||
# Text deltas
|
||||
AgentResponseUpdate(contents=[Content.from_text("The answer ")], role="assistant"),
|
||||
AgentResponseUpdate(contents=[Content.from_text("is 42")], role="assistant"),
|
||||
]
|
||||
)
|
||||
server = _make_server(agent)
|
||||
resp = await _post(server, stream=True)
|
||||
|
||||
assert resp.status_code == 200
|
||||
events = _parse_sse_events(resp.text)
|
||||
types = _sse_event_types(events)
|
||||
|
||||
assert types[0] == "response.created"
|
||||
assert types[-1] == "response.completed"
|
||||
# Reasoning + text = 2 output items
|
||||
assert types.count("response.output_item.added") == 2
|
||||
assert types.count("response.output_item.done") == 2
|
||||
assert types.count("response.output_text.delta") == 2
|
||||
|
||||
# Verify accumulated text
|
||||
text_done = [e for e in events if e["event"] == "response.output_text.done"]
|
||||
assert len(text_done) == 1
|
||||
assert text_done[0]["data"]["text"] == "The answer is 42"
|
||||
|
||||
async def test_empty_streaming(self) -> None:
|
||||
agent = _make_agent(stream_updates=[])
|
||||
server = _make_server(agent)
|
||||
resp = await _post(server, stream=True)
|
||||
|
||||
assert resp.status_code == 200
|
||||
events = _parse_sse_events(resp.text)
|
||||
types = _sse_event_types(events)
|
||||
|
||||
assert types == ["response.created", "response.in_progress", "response.completed"]
|
||||
|
||||
async def test_mixed_contents_in_single_update(self) -> None:
|
||||
"""Text and function call in one update switches builder mid-update."""
|
||||
agent = _make_agent(
|
||||
stream_updates=[
|
||||
AgentResponseUpdate(
|
||||
contents=[
|
||||
Content.from_text("Let me search"),
|
||||
Content.from_function_call("call_1", "search", arguments='{"q": "test"}'),
|
||||
],
|
||||
role="assistant",
|
||||
),
|
||||
]
|
||||
)
|
||||
server = _make_server(agent)
|
||||
resp = await _post(server, stream=True)
|
||||
|
||||
assert resp.status_code == 200
|
||||
events = _parse_sse_events(resp.text)
|
||||
types = _sse_event_types(events)
|
||||
|
||||
assert "response.output_text.delta" in types
|
||||
assert "response.output_text.done" in types
|
||||
assert "response.function_call_arguments.delta" in types
|
||||
assert "response.function_call_arguments.done" in types
|
||||
|
||||
async def test_different_function_call_ids_produce_separate_items(self) -> None:
|
||||
agent = _make_agent(
|
||||
stream_updates=[
|
||||
AgentResponseUpdate(
|
||||
contents=[Content.from_function_call("call_1", "func_a", arguments='{"x":1}')],
|
||||
role="assistant",
|
||||
),
|
||||
AgentResponseUpdate(
|
||||
contents=[Content.from_function_call("call_2", "func_b", arguments='{"y":2}')],
|
||||
role="assistant",
|
||||
),
|
||||
]
|
||||
)
|
||||
server = _make_server(agent)
|
||||
resp = await _post(server, stream=True)
|
||||
|
||||
assert resp.status_code == 200
|
||||
events = _parse_sse_events(resp.text)
|
||||
types = _sse_event_types(events)
|
||||
|
||||
# Two separate function call items
|
||||
assert types.count("response.output_item.added") == 2
|
||||
assert types.count("response.function_call_arguments.done") == 2
|
||||
|
||||
async def test_mcp_tool_call_streaming(self) -> None:
|
||||
agent = _make_agent(
|
||||
stream_updates=[
|
||||
AgentResponseUpdate(
|
||||
contents=[
|
||||
Content(
|
||||
type="mcp_server_tool_call",
|
||||
server_name="my_server",
|
||||
tool_name="search",
|
||||
arguments='{"query":',
|
||||
)
|
||||
],
|
||||
role="assistant",
|
||||
),
|
||||
AgentResponseUpdate(
|
||||
contents=[
|
||||
Content(
|
||||
type="mcp_server_tool_call",
|
||||
server_name="my_server",
|
||||
tool_name="search",
|
||||
arguments=' "test"}',
|
||||
)
|
||||
],
|
||||
role="assistant",
|
||||
),
|
||||
]
|
||||
)
|
||||
server = _make_server(agent)
|
||||
resp = await _post(server, stream=True)
|
||||
|
||||
assert resp.status_code == 200
|
||||
events = _parse_sse_events(resp.text)
|
||||
types = _sse_event_types(events)
|
||||
|
||||
assert types[0] == "response.created"
|
||||
assert types[-1] == "response.completed"
|
||||
assert "response.output_item.added" in types
|
||||
assert "response.output_item.done" in types
|
||||
|
||||
|
||||
# endregion
|
||||
@@ -1161,7 +1161,16 @@ class RawOpenAIChatClient( # type: ignore[misc]
|
||||
# First turn: prepend instructions as system message
|
||||
messages = prepend_instructions_to_messages(list(messages), instructions, role="system")
|
||||
# Continuation turn: instructions already exist in conversation context, skip prepending
|
||||
request_input = self._prepare_messages_for_openai(messages)
|
||||
request_uses_service_side_storage = False
|
||||
for key in ("conversation_id", "previous_response_id", "conversation"):
|
||||
value = options.get(key)
|
||||
if isinstance(value, str) and value:
|
||||
request_uses_service_side_storage = True
|
||||
break
|
||||
request_input = self._prepare_messages_for_openai(
|
||||
messages,
|
||||
request_uses_service_side_storage=request_uses_service_side_storage,
|
||||
)
|
||||
if not request_input:
|
||||
raise ChatClientInvalidRequestException("Messages are required for chat completions")
|
||||
conversation_id = options.get("conversation_id")
|
||||
@@ -1235,7 +1244,12 @@ class RawOpenAIChatClient( # type: ignore[misc]
|
||||
raise ValueError("model must be a non-empty string")
|
||||
options["model"] = self.model
|
||||
|
||||
def _prepare_messages_for_openai(self, chat_messages: Sequence[Message]) -> list[dict[str, Any]]:
|
||||
def _prepare_messages_for_openai(
|
||||
self,
|
||||
chat_messages: Sequence[Message],
|
||||
*,
|
||||
request_uses_service_side_storage: bool = True,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Prepare the chat messages for a request.
|
||||
|
||||
Allowing customization of the key names for role/author, and optionally overriding the role.
|
||||
@@ -1248,31 +1262,27 @@ class RawOpenAIChatClient( # type: ignore[misc]
|
||||
|
||||
Args:
|
||||
chat_messages: The chat history to prepare.
|
||||
request_uses_service_side_storage: Whether this request continues a service-managed
|
||||
response/conversation and can safely reference service-scoped response items.
|
||||
|
||||
Returns:
|
||||
The prepared chat messages for a request.
|
||||
"""
|
||||
list_of_list = [self._prepare_message_for_openai(message) for message in chat_messages]
|
||||
list_of_list = [
|
||||
self._prepare_message_for_openai(
|
||||
message,
|
||||
request_uses_service_side_storage=request_uses_service_side_storage,
|
||||
)
|
||||
for message in chat_messages
|
||||
]
|
||||
# Flatten the list of lists into a single list
|
||||
return list(chain.from_iterable(list_of_list))
|
||||
|
||||
@staticmethod
|
||||
def _message_replays_provider_context(message: Message) -> bool:
|
||||
"""Return whether the message came from provider-attributed replay context.
|
||||
|
||||
Responses ``fc_id`` values are response-scoped and only valid while replaying
|
||||
the same live tool loop. Once a message comes back through a context provider
|
||||
(for example, loaded session history), that message is historical input and
|
||||
must not reuse the original response-scoped ``fc_id``.
|
||||
"""
|
||||
additional_properties = getattr(message, "additional_properties", None)
|
||||
if not additional_properties:
|
||||
return False
|
||||
return "_attribution" in additional_properties
|
||||
|
||||
def _prepare_message_for_openai(
|
||||
self,
|
||||
message: Message,
|
||||
*,
|
||||
request_uses_service_side_storage: bool = True,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Prepare a chat message for the OpenAI Responses API format."""
|
||||
all_messages: list[dict[str, Any]] = []
|
||||
@@ -1280,34 +1290,63 @@ class RawOpenAIChatClient( # type: ignore[misc]
|
||||
"type": "message",
|
||||
"role": message.role,
|
||||
}
|
||||
additional_properties = message.additional_properties
|
||||
replays_local_storage = "_attribution" in additional_properties
|
||||
uses_service_side_storage = request_uses_service_side_storage and not replays_local_storage
|
||||
# Reasoning items are only valid in input when they directly preceded a function_call
|
||||
# in the same response. Including a reasoning item that preceded a text response
|
||||
# in the same response. Including a reasoning item that preceded a text response
|
||||
# (i.e. no function_call in the same message) causes an API error:
|
||||
# "reasoning was provided without its required following item."
|
||||
#
|
||||
# Local storage is stricter: response-scoped reasoning items (rs_*) cannot be replayed
|
||||
# back to the service unless that message is using service-side storage.
|
||||
# In that mode we omit reasoning items and rely on function call + tool output replay.
|
||||
has_function_call = any(c.type == "function_call" for c in message.contents)
|
||||
for content in message.contents:
|
||||
match content.type:
|
||||
case "text_reasoning":
|
||||
if not has_function_call:
|
||||
if not uses_service_side_storage or not has_function_call:
|
||||
continue # reasoning not followed by a function_call is invalid in input
|
||||
reasoning = self._prepare_content_for_openai(message.role, content, message=message)
|
||||
reasoning = self._prepare_content_for_openai(
|
||||
message.role,
|
||||
content,
|
||||
replays_local_storage=replays_local_storage,
|
||||
)
|
||||
if reasoning:
|
||||
all_messages.append(reasoning)
|
||||
case "function_result":
|
||||
new_args: dict[str, Any] = {}
|
||||
new_args.update(self._prepare_content_for_openai(message.role, content, message=message))
|
||||
new_args.update(
|
||||
self._prepare_content_for_openai(
|
||||
message.role,
|
||||
content,
|
||||
replays_local_storage=replays_local_storage,
|
||||
)
|
||||
)
|
||||
if new_args:
|
||||
all_messages.append(new_args)
|
||||
case "function_call":
|
||||
function_call = self._prepare_content_for_openai(message.role, content, message=message)
|
||||
function_call = self._prepare_content_for_openai(
|
||||
message.role,
|
||||
content,
|
||||
replays_local_storage=replays_local_storage,
|
||||
)
|
||||
if function_call:
|
||||
all_messages.append(function_call)
|
||||
case "function_approval_response" | "function_approval_request":
|
||||
prepared = self._prepare_content_for_openai(message.role, content, message=message)
|
||||
prepared = self._prepare_content_for_openai(
|
||||
message.role,
|
||||
content,
|
||||
replays_local_storage=replays_local_storage,
|
||||
)
|
||||
if prepared:
|
||||
all_messages.append(prepared)
|
||||
case _:
|
||||
prepared_content = self._prepare_content_for_openai(message.role, content, message=message)
|
||||
prepared_content = self._prepare_content_for_openai(
|
||||
message.role,
|
||||
content,
|
||||
replays_local_storage=replays_local_storage,
|
||||
)
|
||||
if prepared_content:
|
||||
if "content" not in args:
|
||||
args["content"] = []
|
||||
@@ -1321,7 +1360,7 @@ class RawOpenAIChatClient( # type: ignore[misc]
|
||||
role: Role | str,
|
||||
content: Content,
|
||||
*,
|
||||
message: Message | None = None,
|
||||
replays_local_storage: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
"""Prepare content for the OpenAI Responses API format."""
|
||||
role = Role(role)
|
||||
@@ -1401,11 +1440,7 @@ class RawOpenAIChatClient( # type: ignore[misc]
|
||||
logger.warning(f"FunctionCallContent missing call_id for function '{content.name}'")
|
||||
return {}
|
||||
fc_id = content.call_id
|
||||
if (
|
||||
message is not None
|
||||
and not self._message_replays_provider_context(message)
|
||||
and content.additional_properties
|
||||
):
|
||||
if not replays_local_storage and content.additional_properties:
|
||||
live_fc_id = content.additional_properties.get("fc_id")
|
||||
if isinstance(live_fc_id, str) and live_fc_id:
|
||||
fc_id = live_fc_id
|
||||
|
||||
@@ -1015,6 +1015,84 @@ async def test_shell_call_is_invoked_as_local_shell_function_loop() -> None:
|
||||
assert len(local_shell_outputs) == 0
|
||||
|
||||
|
||||
async def test_tool_loop_store_false_omits_reasoning_items_from_second_request() -> None:
|
||||
"""Stateless tool-loop replay must omit response-scoped reasoning items."""
|
||||
client = OpenAIChatClient(model="test-model", api_key="test-key")
|
||||
|
||||
mock_response1 = MagicMock()
|
||||
mock_response1.output_parsed = None
|
||||
mock_response1.metadata = {}
|
||||
mock_response1.usage = None
|
||||
mock_response1.id = "resp-1"
|
||||
mock_response1.model = "test-model"
|
||||
mock_response1.created_at = 1000000000
|
||||
mock_response1.status = "completed"
|
||||
mock_response1.finish_reason = "tool_calls"
|
||||
mock_response1.incomplete = None
|
||||
mock_response1.conversation = None
|
||||
|
||||
mock_reasoning_item = MagicMock()
|
||||
mock_reasoning_item.type = "reasoning"
|
||||
mock_reasoning_item.id = "rs_local_only"
|
||||
mock_reasoning_item.content = []
|
||||
mock_reasoning_item.summary = []
|
||||
mock_reasoning_item.encrypted_content = None
|
||||
|
||||
mock_function_call_item = MagicMock()
|
||||
mock_function_call_item.type = "function_call"
|
||||
mock_function_call_item.id = "fc_tool123"
|
||||
mock_function_call_item.call_id = "call_123"
|
||||
mock_function_call_item.name = "get_weather"
|
||||
mock_function_call_item.arguments = '{"location":"Amsterdam"}'
|
||||
mock_function_call_item.status = "completed"
|
||||
|
||||
mock_response1.output = [mock_reasoning_item, mock_function_call_item]
|
||||
|
||||
mock_response2 = MagicMock()
|
||||
mock_response2.output_parsed = None
|
||||
mock_response2.metadata = {}
|
||||
mock_response2.usage = None
|
||||
mock_response2.id = "resp-2"
|
||||
mock_response2.model = "test-model"
|
||||
mock_response2.created_at = 1000000001
|
||||
mock_response2.status = "completed"
|
||||
mock_response2.finish_reason = "stop"
|
||||
mock_response2.incomplete = None
|
||||
mock_response2.conversation = None
|
||||
|
||||
mock_text_item = MagicMock()
|
||||
mock_text_item.type = "message"
|
||||
mock_text_content = MagicMock()
|
||||
mock_text_content.type = "output_text"
|
||||
mock_text_content.text = "The weather in Amsterdam is sunny."
|
||||
mock_text_item.content = [mock_text_content]
|
||||
mock_response2.output = [mock_text_item]
|
||||
|
||||
with patch.object(client.client.responses, "create", side_effect=[mock_response1, mock_response2]) as mock_create:
|
||||
response = await client.get_response(
|
||||
messages=[Message(role="user", contents=["What's the weather in Amsterdam?"])],
|
||||
options={
|
||||
"store": False,
|
||||
"tools": [get_weather],
|
||||
"tool_choice": {"mode": "required", "required_function_name": "get_weather"},
|
||||
},
|
||||
)
|
||||
|
||||
assert response.text == "The weather in Amsterdam is sunny."
|
||||
assert mock_create.call_count == 2
|
||||
|
||||
second_call_input = mock_create.call_args_list[1].kwargs["input"]
|
||||
assert not any(item.get("type") == "reasoning" for item in second_call_input)
|
||||
|
||||
function_calls = [item for item in second_call_input if item.get("type") == "function_call"]
|
||||
assert len(function_calls) == 1
|
||||
assert function_calls[0]["id"] == "fc_tool123"
|
||||
|
||||
function_outputs = [item for item in second_call_input if item.get("type") == "function_call_output"]
|
||||
assert len(function_outputs) == 1
|
||||
assert function_outputs[0]["call_id"] == "call_123"
|
||||
|
||||
|
||||
def test_response_content_creation_with_shell_call() -> None:
|
||||
"""Test _parse_response_from_openai with shell_call output."""
|
||||
client = OpenAIChatClient(model="test-model", api_key="test-key")
|
||||
@@ -3221,6 +3299,164 @@ async def test_prepare_options_store_parameter_handling() -> None:
|
||||
assert "previous_response_id" not in options
|
||||
|
||||
|
||||
async def test_prepare_options_store_false_omits_reasoning_items_for_stateless_replay() -> None:
|
||||
client = OpenAIChatClient(model="test-model", api_key="test-key")
|
||||
messages = [
|
||||
Message(role="user", contents=[Content.from_text(text="search for hotels")]),
|
||||
Message(
|
||||
role="assistant",
|
||||
contents=[
|
||||
Content.from_text_reasoning(
|
||||
id="rs_test123",
|
||||
text="I need to search for hotels",
|
||||
additional_properties={"status": "completed"},
|
||||
),
|
||||
Content.from_function_call(
|
||||
call_id="call_1",
|
||||
name="search_hotels",
|
||||
arguments='{"city": "Paris"}',
|
||||
additional_properties={"fc_id": "fc_test456"},
|
||||
),
|
||||
],
|
||||
),
|
||||
Message(
|
||||
role="tool",
|
||||
contents=[
|
||||
Content.from_function_result(
|
||||
call_id="call_1",
|
||||
result="Found 3 hotels in Paris",
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
||||
options = await client._prepare_options(messages, ChatOptions(store=False)) # type: ignore[arg-type]
|
||||
|
||||
assert not any(item.get("type") == "reasoning" for item in options["input"])
|
||||
assert any(item.get("type") == "function_call" for item in options["input"])
|
||||
assert any(item.get("type") == "function_call_output" for item in options["input"])
|
||||
|
||||
|
||||
async def test_prepare_options_with_conversation_id_keeps_reasoning_items() -> None:
|
||||
client = OpenAIChatClient(model="test-model", api_key="test-key")
|
||||
messages = [
|
||||
Message(role="user", contents=[Content.from_text(text="search for hotels")]),
|
||||
Message(
|
||||
role="assistant",
|
||||
contents=[
|
||||
Content.from_text_reasoning(
|
||||
id="rs_test123",
|
||||
text="I need to search for hotels",
|
||||
additional_properties={"status": "completed"},
|
||||
),
|
||||
Content.from_function_call(
|
||||
call_id="call_1",
|
||||
name="search_hotels",
|
||||
arguments='{"city": "Paris"}',
|
||||
additional_properties={"fc_id": "fc_test456"},
|
||||
),
|
||||
],
|
||||
),
|
||||
Message(
|
||||
role="tool",
|
||||
contents=[
|
||||
Content.from_function_result(
|
||||
call_id="call_1",
|
||||
result="Found 3 hotels in Paris",
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
||||
options = await client._prepare_options(
|
||||
messages,
|
||||
ChatOptions(store=False, conversation_id="resp_prev123"), # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
reasoning_items = [item for item in options["input"] if item.get("type") == "reasoning"]
|
||||
assert len(reasoning_items) == 1
|
||||
assert reasoning_items[0]["id"] == "rs_test123"
|
||||
assert options["previous_response_id"] == "resp_prev123"
|
||||
|
||||
|
||||
async def test_prepare_options_with_conversation_id_omits_reasoning_items_for_attributed_replay() -> None:
|
||||
client = OpenAIChatClient(model="test-model", api_key="test-key")
|
||||
messages = [
|
||||
Message(role="user", contents=[Content.from_text(text="search for hotels")]),
|
||||
Message(
|
||||
role="assistant",
|
||||
contents=[
|
||||
Content.from_text_reasoning(
|
||||
id="rs_history123",
|
||||
text="I need to search history for hotels",
|
||||
additional_properties={"status": "completed"},
|
||||
),
|
||||
Content.from_function_call(
|
||||
call_id="call_history",
|
||||
name="search_hotels",
|
||||
arguments='{"city": "Paris"}',
|
||||
additional_properties={"fc_id": "fc_history456"},
|
||||
),
|
||||
],
|
||||
additional_properties={"_attribution": {"source_id": "history", "source_type": "InMemoryHistoryProvider"}},
|
||||
),
|
||||
Message(
|
||||
role="tool",
|
||||
contents=[
|
||||
Content.from_function_result(
|
||||
call_id="call_history",
|
||||
result="Found 3 hotels in Paris",
|
||||
),
|
||||
],
|
||||
),
|
||||
Message(
|
||||
role="assistant",
|
||||
contents=[
|
||||
Content.from_text_reasoning(
|
||||
id="rs_live123",
|
||||
text="I should refine the search for a live follow-up",
|
||||
additional_properties={"status": "completed"},
|
||||
),
|
||||
Content.from_function_call(
|
||||
call_id="call_live",
|
||||
name="search_hotels",
|
||||
arguments='{"city": "London"}',
|
||||
additional_properties={"fc_id": "fc_live456"},
|
||||
),
|
||||
],
|
||||
),
|
||||
Message(
|
||||
role="tool",
|
||||
contents=[
|
||||
Content.from_function_result(
|
||||
call_id="call_live",
|
||||
result="Found 4 hotels in London",
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
||||
options = await client._prepare_options(
|
||||
messages,
|
||||
ChatOptions(store=False, conversation_id="resp_prev123"), # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
reasoning_items = [item for item in options["input"] if item.get("type") == "reasoning"]
|
||||
assert [item["id"] for item in reasoning_items] == ["rs_live123"]
|
||||
assert any(
|
||||
item.get("type") == "function_call" and item.get("call_id") == "call_history" for item in options["input"]
|
||||
)
|
||||
assert any(item.get("type") == "function_call" and item.get("call_id") == "call_live" for item in options["input"])
|
||||
assert any(
|
||||
item.get("type") == "function_call_output" and item.get("call_id") == "call_history"
|
||||
for item in options["input"]
|
||||
)
|
||||
assert any(
|
||||
item.get("type") == "function_call_output" and item.get("call_id") == "call_live" for item in options["input"]
|
||||
)
|
||||
assert options["previous_response_id"] == "resp_prev123"
|
||||
|
||||
|
||||
def _create_mock_responses_text_response(*, response_id: str) -> MagicMock:
|
||||
mock_response = MagicMock()
|
||||
mock_response.id = response_id
|
||||
|
||||
@@ -79,7 +79,6 @@ agent-framework-declarative = { workspace = true }
|
||||
agent-framework-devui = { workspace = true }
|
||||
agent-framework-durabletask = { workspace = true }
|
||||
agent-framework-foundry = { workspace = true }
|
||||
agent-framework-foundry-hosting = { workspace = true }
|
||||
agent-framework-foundry-local = { workspace = true }
|
||||
agent-framework-gemini = { workspace = true }
|
||||
agent-framework-github-copilot = { workspace = true }
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
# Basic example of hosting an agent with the `invocations` API
|
||||
|
||||
Run the following command to start the server:
|
||||
|
||||
```bash
|
||||
python main.py
|
||||
```
|
||||
|
||||
Send a POST request to the server with a JSON body containing a "message" field to interact with the agent. For example:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8088/invocations -H "Content-Type: application/json" -d '{"message": "Hi!"}'
|
||||
```
|
||||
@@ -1,36 +0,0 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import os
|
||||
|
||||
from agent_framework import Agent
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from agent_framework_foundry_hosting import InvocationsHostServer
|
||||
from azure.identity import AzureCliCredential
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
|
||||
def main():
|
||||
client = FoundryChatClient(
|
||||
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
model=os.environ["FOUNDRY_MODEL"],
|
||||
credential=AzureCliCredential(),
|
||||
)
|
||||
|
||||
agent = Agent(
|
||||
client=client,
|
||||
instructions="You are a friendly assistant. Keep your answers brief.",
|
||||
# History will be managed by the hosting infrastructure, thus there
|
||||
# is no need to store history by the service. Learn more at:
|
||||
# https://developers.openai.com/api/reference/resources/responses/methods/create
|
||||
default_options={"store": False},
|
||||
)
|
||||
|
||||
server = InvocationsHostServer(agent)
|
||||
server.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,2 +0,0 @@
|
||||
agent-framework
|
||||
agent-framework-foundry-hosting
|
||||
@@ -1,6 +0,0 @@
|
||||
.venv
|
||||
__pycache__
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
.Python
|
||||
@@ -1,2 +0,0 @@
|
||||
FOUNDRY_PROJECT_ENDPOINT="..."
|
||||
MODEL_DEPLOYMENT_NAME="..."
|
||||
@@ -1,33 +0,0 @@
|
||||
# Basic example of hosting an agent with the `responses` API
|
||||
|
||||
This agent only contains an instruction (personal). It's the most basic agent with an LLM and no tools.
|
||||
|
||||
## Interacting with the agent
|
||||
|
||||
Send a POST request to the server with a JSON body containing a "message" field to interact with the agent. For example:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8088/responses -H "Content-Type: application/json" -d '{"input": "Hi"}'
|
||||
```
|
||||
|
||||
### Invoke with `azd`
|
||||
|
||||
```bash
|
||||
azd ai agent invoke --local "Hi"
|
||||
```
|
||||
|
||||
## Multi-turn conversation
|
||||
|
||||
To have a multi-turn conversation with the agent, include the previous response id in the request body. For example:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8088/responses -H "Content-Type: application/json" -d '{"input": "How are you?", "previous_response_id": "REPLACE_WITH_PREVIOUS_RESPONSE_ID"}'
|
||||
```
|
||||
|
||||
Invoke with `azd`:
|
||||
|
||||
```bash
|
||||
azd ai agent invoke --local "Hi!" --conversation-id "my_conv"
|
||||
|
||||
azd ai agent invoke --local "How are you?" --conversation-id "my_conv"
|
||||
```
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
name: agent-framework-agent-basic
|
||||
description: >
|
||||
A basic Agent Framework agent hosted by Foundry.
|
||||
metadata:
|
||||
tags:
|
||||
- Agent Framework
|
||||
- AI Agent Hosting
|
||||
- Azure AI AgentServer
|
||||
- Responses Protocol
|
||||
- Streaming
|
||||
template:
|
||||
name: agent-framework-agent-basic
|
||||
kind: hosted
|
||||
protocols:
|
||||
- protocol: responses
|
||||
version: 1.0.0
|
||||
environment_variables:
|
||||
- name: MODEL_DEPLOYMENT_NAME
|
||||
value: "{{MODEL_DEPLOYMENT_NAME}}"
|
||||
resources:
|
||||
- kind: model
|
||||
id: gpt-4.1-mini
|
||||
name: MODEL_DEPLOYMENT_NAME
|
||||
@@ -1,8 +0,0 @@
|
||||
kind: hosted
|
||||
name: agent-framework-agent-basic
|
||||
protocols:
|
||||
- protocol: responses
|
||||
version: v0.1.0
|
||||
resources:
|
||||
cpu: "0.25"
|
||||
memory: 0.5Gi
|
||||
@@ -1,37 +0,0 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import os
|
||||
|
||||
from agent_framework import Agent
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from agent_framework_foundry_hosting import ResponsesHostServer
|
||||
from azure.ai.agentserver.responses import InMemoryResponseProvider
|
||||
from azure.identity import AzureCliCredential
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
|
||||
def main():
|
||||
client = FoundryChatClient(
|
||||
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
model=os.environ["MODEL_DEPLOYMENT_NAME"],
|
||||
credential=AzureCliCredential(),
|
||||
)
|
||||
|
||||
agent = Agent(
|
||||
client=client,
|
||||
instructions="You are a friendly assistant. Keep your answers brief.",
|
||||
# History will be managed by the hosting infrastructure, thus there
|
||||
# is no need to store history by the service. Learn more at:
|
||||
# https://developers.openai.com/api/reference/resources/responses/methods/create
|
||||
default_options={"store": False},
|
||||
)
|
||||
|
||||
server = ResponsesHostServer(agent, store=InMemoryResponseProvider())
|
||||
server.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,2 +0,0 @@
|
||||
agent-framework
|
||||
agent-framework-foundry-hosting
|
||||
@@ -1,6 +0,0 @@
|
||||
.venv
|
||||
__pycache__
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
.Python
|
||||
@@ -1,2 +0,0 @@
|
||||
FOUNDRY_PROJECT_ENDPOINT="..."
|
||||
MODEL_DEPLOYMENT_NAME="..."
|
||||
@@ -1,23 +0,0 @@
|
||||
# Basic example of hosting an agent with the `responses` API and local tools
|
||||
|
||||
This agent is equipped with with a function tool and a local shell tool.
|
||||
|
||||
> We recommend deploying this sample on a local container or to Foundry Hosting because the agent has access to a local shell tool, which can run arbitrary commands on the machine.
|
||||
|
||||
## Interacting with the agent
|
||||
|
||||
Send a POST request to the server with a JSON body containing a "message" field to interact with the agent. For example:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8088/responses -H "Content-Type: application/json" -d '{"input": "What is the weather in Seattle?"}'
|
||||
|
||||
curl -X POST http://localhost:8088/responses -H "Content-Type: application/json" -d '{"input": "List the files in the current directory."}'
|
||||
```
|
||||
|
||||
Invoke with `azd`:
|
||||
|
||||
```bash
|
||||
azd ai agent invoke --local "What is the weather in Seattle?"
|
||||
|
||||
azd ai agent invoke --local "List the files in the current directory."
|
||||
```
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
name: agent-framework-agent-with-local-tools
|
||||
description: >
|
||||
An Agent Framework agent with local tools hosted by Foundry.
|
||||
metadata:
|
||||
tags:
|
||||
- Agent Framework
|
||||
- AI Agent Hosting
|
||||
- Azure AI AgentServer
|
||||
- Responses Protocol
|
||||
- Streaming
|
||||
template:
|
||||
name: agent-framework-agent-with-local-tools
|
||||
kind: hosted
|
||||
protocols:
|
||||
- protocol: responses
|
||||
version: 1.0.0
|
||||
environment_variables:
|
||||
- name: MODEL_DEPLOYMENT_NAME
|
||||
value: "{{MODEL_DEPLOYMENT_NAME}}"
|
||||
resources:
|
||||
- kind: model
|
||||
id: gpt-4.1-mini
|
||||
name: MODEL_DEPLOYMENT_NAME
|
||||
@@ -1,8 +0,0 @@
|
||||
kind: hosted
|
||||
name: agent-framework-agent-with-local-tools
|
||||
protocols:
|
||||
- protocol: responses
|
||||
version: v0.1.0
|
||||
resources:
|
||||
cpu: "0.25"
|
||||
memory: 0.5Gi
|
||||
@@ -1,75 +0,0 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
from random import randint
|
||||
|
||||
from agent_framework import Agent, tool
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from agent_framework_foundry_hosting import ResponsesHostServer
|
||||
from azure.ai.agentserver.responses import InMemoryResponseProvider
|
||||
from azure.identity import AzureCliCredential
|
||||
from dotenv import load_dotenv
|
||||
from pydantic import Field
|
||||
from typing_extensions import Annotated
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
|
||||
@tool(approval_mode="never_require")
|
||||
def get_weather(
|
||||
location: Annotated[str, Field(description="The location to get the weather for.")],
|
||||
) -> str:
|
||||
"""Get the weather for a given location."""
|
||||
conditions = ["sunny", "cloudy", "rainy", "stormy"]
|
||||
return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C."
|
||||
|
||||
|
||||
@tool(approval_mode="always_require")
|
||||
def run_bash(command: str) -> str:
|
||||
"""Execute a shell command locally and return stdout, stderr, and exit code."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
command,
|
||||
shell=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
)
|
||||
parts: list[str] = []
|
||||
if result.stdout:
|
||||
parts.append(result.stdout)
|
||||
if result.stderr:
|
||||
parts.append(f"stderr: {result.stderr}")
|
||||
parts.append(f"exit_code: {result.returncode}")
|
||||
return "\n".join(parts)
|
||||
except subprocess.TimeoutExpired:
|
||||
return "Command timed out after 30 seconds"
|
||||
except Exception as e:
|
||||
return f"Error executing command: {e}"
|
||||
|
||||
|
||||
def main():
|
||||
client = FoundryChatClient(
|
||||
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
model=os.environ["MODEL_DEPLOYMENT_NAME"],
|
||||
credential=AzureCliCredential(),
|
||||
)
|
||||
|
||||
agent = Agent(
|
||||
client=client,
|
||||
instructions="You are a friendly assistant. Keep your answers brief.",
|
||||
tools=[get_weather, run_bash],
|
||||
# History will be managed by the hosting infrastructure, thus there
|
||||
# is no need to store history by the service. Learn more at:
|
||||
# https://developers.openai.com/api/reference/resources/responses/methods/create
|
||||
default_options={"store": False},
|
||||
)
|
||||
|
||||
server = ResponsesHostServer(agent, store=InMemoryResponseProvider())
|
||||
server.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
-2
@@ -1,2 +0,0 @@
|
||||
agent-framework
|
||||
agent-framework-foundry-hosting
|
||||
@@ -1,6 +0,0 @@
|
||||
.venv
|
||||
__pycache__
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
.Python
|
||||
@@ -1,4 +0,0 @@
|
||||
FOUNDRY_PROJECT_ENDPOINT="..."
|
||||
MODEL_DEPLOYMENT_NAME="..."
|
||||
FOUNDRY_AGENT_TOOLBOX_NAME="..."
|
||||
GITHUB_PAT="..."
|
||||
@@ -1,19 +0,0 @@
|
||||
# Basic example of hosting an agent with the `responses` API and a remote MCP
|
||||
|
||||
This agent is equipped with a GitHub MCP server and a Foundry Toolbox, which are both remote MCPs.
|
||||
|
||||
> Note that there are other ways to interact with Foundry toolboxes. Using it as a MCP is just one of the options.
|
||||
|
||||
## Interacting with the agent
|
||||
|
||||
Send a POST request to the server with a JSON body containing a "message" field to interact with the agent. For example:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8088/responses -H "Content-Type: application/json" -d '{"input": "List all the repositories I own on GitHub."}'
|
||||
```
|
||||
|
||||
Invoke with `azd`:
|
||||
|
||||
```bash
|
||||
azd ai agent invoke --local "List all the repositories I own on GitHub."
|
||||
```
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
name: agent-framework-agent-with-remote-mcp-tools
|
||||
description: >
|
||||
An Agent Framework agent with remote MCP tools hosted by Foundry.
|
||||
metadata:
|
||||
tags:
|
||||
- Agent Framework
|
||||
- AI Agent Hosting
|
||||
- Azure AI AgentServer
|
||||
- Responses Protocol
|
||||
- Streaming
|
||||
template:
|
||||
name: agent-framework-agent-with-remote-mcp-tools
|
||||
kind: hosted
|
||||
protocols:
|
||||
- protocol: responses
|
||||
version: 1.0.0
|
||||
environment_variables:
|
||||
- name: MODEL_DEPLOYMENT_NAME
|
||||
value: "{{MODEL_DEPLOYMENT_NAME}}"
|
||||
- name: GITHUB_PAT
|
||||
value: ${GITHUB_PAT}
|
||||
- name: FOUNDRY_AGENT_TOOLBOX_NAME
|
||||
value: ${FOUNDRY_AGENT_TOOLBOX_NAME}
|
||||
resources:
|
||||
- kind: model
|
||||
id: gpt-4.1-mini
|
||||
name: MODEL_DEPLOYMENT_NAME
|
||||
@@ -1,8 +0,0 @@
|
||||
kind: hosted
|
||||
name: agent-framework-agent-with-remote-mcp-tools
|
||||
protocols:
|
||||
- protocol: responses
|
||||
version: v0.1.0
|
||||
resources:
|
||||
cpu: "0.25"
|
||||
memory: 0.5Gi
|
||||
@@ -1,77 +0,0 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import os
|
||||
|
||||
import httpx
|
||||
from agent_framework import Agent, MCPStreamableHTTPTool
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from agent_framework_foundry_hosting import ResponsesHostServer
|
||||
from azure.ai.agentserver.responses import InMemoryResponseProvider
|
||||
from azure.identity import AzureCliCredential
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
|
||||
class ToolboxAuth(httpx.Auth):
|
||||
"""httpx Auth that injects a fresh bearer token on every request."""
|
||||
|
||||
def auth_flow(self, request: httpx.Request):
|
||||
credential = AzureCliCredential()
|
||||
token = credential.get_token("https://ai.azure.com/.default").token
|
||||
request.headers["Authorization"] = f"Bearer {token}"
|
||||
yield request
|
||||
|
||||
|
||||
def main():
|
||||
client = FoundryChatClient(
|
||||
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
model=os.environ["MODEL_DEPLOYMENT_NAME"],
|
||||
credential=AzureCliCredential(),
|
||||
)
|
||||
|
||||
# Foundry Toolbox as a MCP tool
|
||||
project_endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"]
|
||||
toolbox_name = os.environ["FOUNDRY_AGENT_TOOLBOX_NAME"]
|
||||
toolbox_endpoint = f"{project_endpoint.rstrip('/')}/toolboxes/{toolbox_name}/mcp?api-version=v1"
|
||||
http_client = httpx.AsyncClient(auth=ToolboxAuth(), headers={"Foundry-Features": "Toolboxes=V1Preview"})
|
||||
foundry_mcp_tool = MCPStreamableHTTPTool(
|
||||
name="toolbox",
|
||||
url=toolbox_endpoint,
|
||||
http_client=http_client,
|
||||
load_prompts=False,
|
||||
)
|
||||
|
||||
# GitHub MCP server
|
||||
github_pat = os.environ["GITHUB_PAT"]
|
||||
if not github_pat:
|
||||
raise ValueError(
|
||||
"GITHUB_PAT environment variable must be set. Create a token at https://github.com/settings/tokens"
|
||||
)
|
||||
|
||||
github_mcp_tool = client.get_mcp_tool(
|
||||
name="GitHub",
|
||||
url="https://api.githubcopilot.com/mcp/",
|
||||
headers={
|
||||
"Authorization": f"Bearer {github_pat}",
|
||||
},
|
||||
approval_mode="never_require",
|
||||
)
|
||||
|
||||
agent = Agent(
|
||||
client=client,
|
||||
instructions="You are a friendly assistant. Keep your answers brief.",
|
||||
tools=[foundry_mcp_tool, github_mcp_tool],
|
||||
# History will be managed by the hosting infrastructure, thus there
|
||||
# is no need to store history by the service. Learn more at:
|
||||
# https://developers.openai.com/api/reference/resources/responses/methods/create
|
||||
default_options={"store": False},
|
||||
)
|
||||
|
||||
server = ResponsesHostServer(agent, store=InMemoryResponseProvider())
|
||||
server.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
-2
@@ -1,2 +0,0 @@
|
||||
agent-framework
|
||||
agent-framework-foundry-hosting
|
||||
@@ -1,6 +0,0 @@
|
||||
.venv
|
||||
__pycache__
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
.Python
|
||||
@@ -1,2 +0,0 @@
|
||||
FOUNDRY_PROJECT_ENDPOINT="..."
|
||||
MODEL_DEPLOYMENT_NAME="..."
|
||||
@@ -1,17 +0,0 @@
|
||||
# Basic example of hosting an agent with the `responses` API and a workflow
|
||||
|
||||
This sample demonstrates how to host a workflow using the `responses` API.
|
||||
|
||||
## Interacting with the agent
|
||||
|
||||
Send a POST request to the server with a JSON body containing a "message" field to interact with the agent. For example:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8088/responses -H "Content-Type: application/json" -d '{"input": "Create a slogan for a new electric SUV that is affordable and fun to drive."}'
|
||||
```
|
||||
|
||||
Invoke with `azd`:
|
||||
|
||||
```bash
|
||||
azd ai agent invoke --local "List all the repositories I own on GitHub."
|
||||
```
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
name: agent-framework-workflows
|
||||
description: >
|
||||
An Agent Framework workflow hosted by Foundry.
|
||||
metadata:
|
||||
tags:
|
||||
- Agent Framework
|
||||
- AI Agent Hosting
|
||||
- Azure AI AgentServer
|
||||
- Responses Protocol
|
||||
- Streaming
|
||||
template:
|
||||
name: agent-framework-workflows
|
||||
kind: hosted
|
||||
protocols:
|
||||
- protocol: responses
|
||||
version: 1.0.0
|
||||
environment_variables:
|
||||
- name: MODEL_DEPLOYMENT_NAME
|
||||
value: "{{MODEL_DEPLOYMENT_NAME}}"
|
||||
resources:
|
||||
- kind: model
|
||||
id: gpt-4.1-mini
|
||||
name: MODEL_DEPLOYMENT_NAME
|
||||
@@ -1,8 +0,0 @@
|
||||
kind: hosted
|
||||
name: agent-framework-workflows
|
||||
protocols:
|
||||
- protocol: responses
|
||||
version: v0.1.0
|
||||
resources:
|
||||
cpu: "0.25"
|
||||
memory: 0.5Gi
|
||||
@@ -1,66 +0,0 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import os
|
||||
|
||||
from agent_framework import Agent
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from agent_framework.orchestrations import GroupChatBuilder, GroupChatState
|
||||
from agent_framework_foundry_hosting import ResponsesHostServer
|
||||
from azure.ai.agentserver.responses import InMemoryResponseProvider
|
||||
from azure.identity import AzureCliCredential
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
|
||||
def round_robin_selector(state: GroupChatState) -> str:
|
||||
"""A round-robin selector function that picks the next speaker based on the current round index."""
|
||||
|
||||
participant_names = list(state.participants.keys())
|
||||
return participant_names[state.current_round % len(participant_names)]
|
||||
|
||||
|
||||
def main():
|
||||
client = FoundryChatClient(
|
||||
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
model=os.environ["MODEL_DEPLOYMENT_NAME"],
|
||||
credential=AzureCliCredential(),
|
||||
)
|
||||
|
||||
writer_agent = Agent(
|
||||
client=client,
|
||||
instructions=(
|
||||
"You are an excellent content writer. You create new content and edit contents based on the feedback."
|
||||
),
|
||||
name="writer",
|
||||
)
|
||||
|
||||
reviewer_agent = Agent(
|
||||
client=client,
|
||||
instructions=(
|
||||
"You are an excellent content reviewer."
|
||||
"Provide actionable feedback to the writer about the provided content."
|
||||
"Provide the feedback in the most concise manner possible."
|
||||
),
|
||||
name="reviewer",
|
||||
)
|
||||
|
||||
workflow_agent = (
|
||||
GroupChatBuilder(
|
||||
participants=[writer_agent, reviewer_agent],
|
||||
# Set a hard termination condition to stop after 4 messages:
|
||||
# User message + writer message + reviewer message + writer message
|
||||
termination_condition=lambda conversation: len(conversation) >= 4,
|
||||
selection_func=round_robin_selector,
|
||||
)
|
||||
.build()
|
||||
.as_agent()
|
||||
)
|
||||
|
||||
server = ResponsesHostServer(workflow_agent, store=InMemoryResponseProvider())
|
||||
server.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
-2
@@ -1,2 +0,0 @@
|
||||
agent-framework
|
||||
agent-framework-foundry-hosting
|
||||
@@ -1,65 +0,0 @@
|
||||
# Hosting agents with Foundry Hosting and the `responses` API
|
||||
|
||||
This folder contains a list of samples that show how to host agents using the `responses` API and deploy them to Foundry Hosting.
|
||||
|
||||
| Sample | Description |
|
||||
| --- | --- |
|
||||
| [01_basic](./01_basic) | A basic example of hosting an agent with the `responses` API and carrying on a multi-turn conversation. |
|
||||
| [02_local_tools](./02_local_tools) | An example of hosting an agent with the `responses` API and local tools including a function tool and a local shell tool. |
|
||||
| [03_remote_mcp](./03_remote_mcp) | An example of hosting an agent with the `responses` API and remote MCPs, including a GitHub MCP server and a Foundry Toolboox. |
|
||||
| [04_workflows](./04_workflows) | An example of hosting a workflow with the `responses` API. |
|
||||
|
||||
## Running the server locally
|
||||
|
||||
Navigate to the sample directory and run the following command to start the server:
|
||||
|
||||
```bash
|
||||
python main.py
|
||||
```
|
||||
|
||||
## Interacting with the agent
|
||||
|
||||
There two ways to interact with the agent: sending HTTP requests to the server or using the `azd` CLI:
|
||||
|
||||
### Invoke with `azd`
|
||||
|
||||
```bash
|
||||
azd ai agent invoke --local "Hi"
|
||||
```
|
||||
|
||||
### Sending HTTP requests
|
||||
|
||||
Send a POST request to the server with a JSON body containing a "message" field to interact with the agent. For example:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8088/responses -H "Content-Type: application/json" -d '{"input": "Hi"}'
|
||||
```
|
||||
|
||||
> See the individual samples for more examples of interacting with the agent.
|
||||
|
||||
## Deploying to a Docker container
|
||||
|
||||
Navigate to the sample directory and build the Docker image:
|
||||
|
||||
```bash
|
||||
docker build -t hosted-agent-sample .
|
||||
```
|
||||
|
||||
Run the container, passing in the required environment variables:
|
||||
|
||||
```bash
|
||||
docker run -p 8088:8088 \
|
||||
-e FOUNDRY_PROJECT_ENDPOINT=<your-endpoint> \
|
||||
-e FOUNDRY_MODEL=<your-model> \
|
||||
hosted-agent-sample
|
||||
```
|
||||
|
||||
The server will be available at `http://localhost:8088`. You can send requests using the same `curl` command shown above.
|
||||
|
||||
## Deploying to Foundry
|
||||
|
||||
TODO
|
||||
|
||||
## Using the deployed agent in Agent Framework
|
||||
|
||||
After deploying the agent, you can also try to use the agent in Agent Framework. Refer to the [using_deployed_agent.py](./using_deployed_agent.py) sample for an example of how to do this.
|
||||
@@ -1,50 +0,0 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
|
||||
from agent_framework import Agent, AgentResponse, AgentResponseUpdate, ResponseStream
|
||||
from agent_framework.openai import OpenAIChatClient
|
||||
from typing_extensions import Any
|
||||
|
||||
"""
|
||||
This script demonstrates how to talk to a deployed agent using the OpenAIChatClient.
|
||||
|
||||
Depending on where you have deployed your agent (local or Foundry Hosting), you may
|
||||
need to change the base_url when initializing the OpenAIChatClient.
|
||||
"""
|
||||
|
||||
|
||||
async def print_streaming_response(streaming_response: ResponseStream[AgentResponseUpdate, AgentResponse[Any]]) -> None:
|
||||
async for chunk in streaming_response:
|
||||
if chunk.text:
|
||||
print(chunk.text, end="", flush=True)
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
agent = Agent(client=OpenAIChatClient(base_url="http://localhost:8088"))
|
||||
session = agent.create_session()
|
||||
|
||||
# First turn
|
||||
query = "Hi!"
|
||||
print(f"User: {query}")
|
||||
print("Agent: ", end="", flush=True)
|
||||
streaming_response = agent.run(query, session=session, stream=True)
|
||||
await print_streaming_response(streaming_response)
|
||||
|
||||
# Second turn
|
||||
query = "You name is Javis. What can you do?"
|
||||
print(f"\nUser: {query}")
|
||||
print("Agent: ", end="", flush=True)
|
||||
streaming_response = agent.run(query, session=session, stream=True)
|
||||
await print_streaming_response(streaming_response)
|
||||
|
||||
# Third turn
|
||||
query = "What is your name?"
|
||||
print(f"\nUser: {query}")
|
||||
print("Agent: ", end="", flush=True)
|
||||
streaming_response = agent.run(query, session=session, stream=True)
|
||||
await print_streaming_response(streaming_response)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,145 @@
|
||||
# Hosted Agent Samples
|
||||
|
||||
These samples demonstrate how to build and host AI agents in Python using the [Azure AI AgentServer SDK](https://pypi.org/project/azure-ai-agentserver-agentframework/) together with Microsoft Agent Framework. Each sample runs locally as a hosted agent and includes `Dockerfile` and `agent.yaml` assets for deployment to Microsoft Foundry.
|
||||
|
||||
## Samples
|
||||
|
||||
| Sample | Description |
|
||||
| ----------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
|
||||
| [`agent_with_hosted_mcp`](./agent_with_hosted_mcp/) | Hosted MCP tool that connects to Microsoft Learn via `https://learn.microsoft.com/api/mcp` |
|
||||
| [`agent_with_text_search_rag`](./agent_with_text_search_rag/) | Retrieval-augmented generation using a custom `ContextProvider` with Contoso Outdoors sample data |
|
||||
| [`agents_in_workflow`](./agents_in_workflow/) | Concurrent workflow that combines researcher, marketer, and legal specialist agents |
|
||||
| [`agent_with_local_tools`](./agent_with_local_tools/) | Local Python tool execution for Seattle hotel search |
|
||||
| [`writer_reviewer_agents_in_workflow`](./writer_reviewer_agents_in_workflow/) | Writer/Reviewer workflow using `FoundryChatClient` |
|
||||
|
||||
## Common Prerequisites
|
||||
|
||||
Before running any sample, ensure you have:
|
||||
|
||||
1. Python 3.10 or later
|
||||
2. [Azure CLI](https://learn.microsoft.com/cli/azure/install-azure-cli) installed
|
||||
3. An Azure OpenAI resource or a Microsoft Foundry project with a chat model deployment
|
||||
|
||||
### Authenticate with Azure CLI
|
||||
|
||||
All samples rely on Azure credentials. For local development, the simplest approach is Azure CLI authentication:
|
||||
|
||||
```powershell
|
||||
az login
|
||||
az account show
|
||||
```
|
||||
|
||||
## Running a Sample
|
||||
|
||||
Each sample folder contains its own `requirements.txt`. Run commands from the specific sample directory you want to try.
|
||||
|
||||
### Recommended: `uv`
|
||||
|
||||
The sample dependencies include preview packages, so allow prerelease installs:
|
||||
|
||||
```powershell
|
||||
cd <sample-directory>
|
||||
uv venv .venv
|
||||
uv pip install --prerelease=allow -r requirements.txt
|
||||
uv run main.py
|
||||
```
|
||||
|
||||
### Alternative: `venv`
|
||||
|
||||
Windows PowerShell:
|
||||
|
||||
```powershell
|
||||
cd <sample-directory>
|
||||
python -m venv .venv
|
||||
.\.venv\Scripts\Activate.ps1
|
||||
pip install -r requirements.txt
|
||||
python main.py
|
||||
```
|
||||
|
||||
macOS/Linux:
|
||||
|
||||
```bash
|
||||
cd <sample-directory>
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
python main.py
|
||||
```
|
||||
|
||||
Each sample starts a hosted agent locally on `http://localhost:8088/`.
|
||||
|
||||
## Environment Variable Setup
|
||||
|
||||
You can either export variables in your shell or create a local `.env` file in the sample directory.
|
||||
|
||||
Example `.env` for Azure OpenAI samples:
|
||||
|
||||
```dotenv
|
||||
AZURE_OPENAI_ENDPOINT=https://<your-openai-resource>.openai.azure.com/
|
||||
AZURE_OPENAI_MODEL=gpt-4.1
|
||||
```
|
||||
|
||||
Example `.env` for Foundry project samples:
|
||||
|
||||
```dotenv
|
||||
FOUNDRY_PROJECT_ENDPOINT=https://<your-resource>.services.ai.azure.com/api/projects/<your-project>
|
||||
FOUNDRY_MODEL=gpt-4.1
|
||||
```
|
||||
|
||||
## Interacting with the Agent
|
||||
|
||||
After starting a sample, send requests to the Responses endpoint.
|
||||
|
||||
PowerShell:
|
||||
|
||||
```powershell
|
||||
$body = @{
|
||||
input = "Your question here"
|
||||
stream = $false
|
||||
} | ConvertTo-Json
|
||||
|
||||
Invoke-RestMethod -Uri "http://localhost:8088/responses" -Method Post -Body $body -ContentType "application/json"
|
||||
```
|
||||
|
||||
curl:
|
||||
|
||||
```bash
|
||||
curl -sS -H "Content-Type: application/json" -X POST http://localhost:8088/responses \
|
||||
-d '{"input":"Your question here","stream":false}'
|
||||
```
|
||||
|
||||
Example prompts by sample:
|
||||
|
||||
| Sample | Example input |
|
||||
| ------------------------------------ | ---------------------------------------------------------------------------- |
|
||||
| `agent_with_hosted_mcp` | `What does Microsoft Learn say about managed identities in Azure?` |
|
||||
| `agent_with_text_search_rag` | `What is Contoso Outdoors' return policy for refunds?` |
|
||||
| `agents_in_workflow` | `Create a launch strategy for a budget-friendly electric SUV.` |
|
||||
| `agent_with_local_tools` | `Find me Seattle hotels from 2025-03-15 to 2025-03-18 under $200 per night.` |
|
||||
| `writer_reviewer_agents_in_workflow` | `Write a slogan for a new affordable electric SUV.` |
|
||||
|
||||
## Deploying to Microsoft Foundry
|
||||
|
||||
Each sample includes a `Dockerfile` and `agent.yaml` for deployment. For deployment steps, follow the hosted agents guidance in Microsoft Foundry:
|
||||
|
||||
- [Hosted agents overview](https://learn.microsoft.com/en-us/azure/ai-foundry/agents/concepts/hosted-agents)
|
||||
- [Create a hosted agent with CLI](https://learn.microsoft.com/en-us/azure/ai-foundry/agents/concepts/hosted-agents?tabs=cli#create-a-hosted-agent)
|
||||
- [Create a hosted agent in Visual Studio Code](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/vs-code-agents-workflow-pro-code?tabs=windows-powershell&pivots=python)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Missing Azure credentials
|
||||
|
||||
If startup fails with authentication errors, run `az login` and verify the selected subscription with `az account show`.
|
||||
|
||||
### Preview package install issues
|
||||
|
||||
These samples depend on preview packages such as `azure-ai-agentserver-agentframework`. Use `uv pip install --prerelease=allow -r requirements.txt` or `pip install -r requirements.txt`.
|
||||
|
||||
### ARM64 container images fail after deployment
|
||||
|
||||
If you build images locally on ARM64 hardware such as Apple Silicon, build for `linux/amd64`:
|
||||
|
||||
```bash
|
||||
docker build --platform=linux/amd64 -t image .
|
||||
```
|
||||
@@ -0,0 +1,30 @@
|
||||
# Unique identifier/name for this agent
|
||||
name: agent-with-hosted-mcp
|
||||
# Brief description of what this agent does
|
||||
description: >
|
||||
An AI agent that uses Azure OpenAI with a Hosted Model Context Protocol (MCP) server.
|
||||
The agent answers questions by searching Microsoft Learn documentation using MCP tools.
|
||||
metadata:
|
||||
# Categorization tags for organizing and discovering agents
|
||||
authors:
|
||||
- Microsoft Agent Framework Team
|
||||
tags:
|
||||
- Azure AI AgentServer
|
||||
- Microsoft Agent Framework
|
||||
- Model Context Protocol
|
||||
- MCP
|
||||
template:
|
||||
name: agent-with-hosted-mcp
|
||||
# The type of agent - "hosted" for HOBO, "container" for COBO
|
||||
kind: hosted
|
||||
protocols:
|
||||
- protocol: responses
|
||||
environment_variables:
|
||||
- name: AZURE_OPENAI_ENDPOINT
|
||||
value: ${AZURE_OPENAI_ENDPOINT}
|
||||
- name: AZURE_OPENAI_MODEL
|
||||
value: "{{chat}}"
|
||||
resources:
|
||||
- kind: model
|
||||
id: gpt-4o-mini
|
||||
name: chat
|
||||
@@ -0,0 +1,34 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from agent_framework import Agent
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from azure.ai.agentserver.agentframework import from_agent_framework # pyright: ignore[reportUnknownVariableType]
|
||||
from azure.identity import AzureCliCredential
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
|
||||
def main():
|
||||
client = FoundryChatClient(credential=AzureCliCredential())
|
||||
|
||||
# Create MCP tool configuration as dict
|
||||
mcp_tool = client.get_mcp_tool(
|
||||
name="Microsoft_Learn_MCP",
|
||||
url="https://learn.microsoft.com/api/mcp",
|
||||
)
|
||||
|
||||
# Create an Agent using the Azure OpenAI Chat Client with a MCP Tool that connects to Microsoft Learn MCP
|
||||
agent = Agent(
|
||||
client=client,
|
||||
name="DocsAgent",
|
||||
instructions="You are a helpful assistant that can help with microsoft documentation questions.",
|
||||
tools=[mcp_tool],
|
||||
)
|
||||
# Run the agent as a hosted agent
|
||||
from_agent_framework(agent).run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,2 @@
|
||||
azure-ai-agentserver-agentframework==1.0.0b16
|
||||
agent-framework
|
||||
@@ -0,0 +1,66 @@
|
||||
# Virtual environments
|
||||
.venv/
|
||||
venv/
|
||||
env/
|
||||
.python-version
|
||||
|
||||
# Environment files with secrets
|
||||
.env
|
||||
.env.*
|
||||
*.local
|
||||
|
||||
# Python build artifacts
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# Testing
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
htmlcov/
|
||||
.pytest_cache/
|
||||
.mypy_cache/
|
||||
|
||||
# IDE and OS files
|
||||
.DS_Store
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Foundry config
|
||||
.foundry/
|
||||
build-source-*/
|
||||
|
||||
# Git
|
||||
.git/
|
||||
.gitignore
|
||||
|
||||
# Docker
|
||||
.dockerignore
|
||||
|
||||
# Documentation
|
||||
docs/
|
||||
*.md
|
||||
!README.md
|
||||
LICENSE
|
||||
@@ -0,0 +1,3 @@
|
||||
# IMPORTANT: Never commit .env to version control - add it to .gitignore
|
||||
FOUNDRY_PROJECT_ENDPOINT=
|
||||
FOUNDRY_MODEL=
|
||||
+5
-5
@@ -1,11 +1,11 @@
|
||||
FROM python:3.12-slim
|
||||
FROM python:3.14-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY . user_agent/
|
||||
WORKDIR /app/user_agent
|
||||
COPY ./ .
|
||||
|
||||
RUN if [ -f requirements.txt ]; then \
|
||||
RUN pip install --upgrade pip && \
|
||||
if [ -f requirements.txt ]; then \
|
||||
pip install -r requirements.txt; \
|
||||
else \
|
||||
echo "No requirements.txt found"; \
|
||||
@@ -13,4 +13,4 @@ RUN if [ -f requirements.txt ]; then \
|
||||
|
||||
EXPOSE 8088
|
||||
|
||||
CMD ["python", "main.py"]
|
||||
CMD ["python", "main.py"]
|
||||
@@ -0,0 +1,162 @@
|
||||
**IMPORTANT!** All samples and other resources made available in this GitHub repository ("samples") are designed to assist in accelerating development of agents, solutions, and agent workflows for various scenarios. Review all provided resources and carefully test output behavior in the context of your use case. AI responses may be inaccurate and AI actions should be monitored with human oversight. Learn more in the transparency documents for [Agent Service](https://learn.microsoft.com/en-us/azure/ai-foundry/responsible-ai/agents/transparency-note) and [Agent Framework](https://github.com/microsoft/agent-framework/blob/main/TRANSPARENCY_FAQ.md).
|
||||
|
||||
Agents, solutions, or other output you create may be subject to legal and regulatory requirements, may require licenses, or may not be suitable for all industries, scenarios, or use cases. By using any sample, you are acknowledging that any output created using those samples are solely your responsibility, and that you will comply with all applicable laws, regulations, and relevant safety standards, terms of service, and codes of conduct.
|
||||
|
||||
Third-party samples contained in this folder are subject to their own designated terms, and they have not been tested or verified by Microsoft or its affiliates.
|
||||
|
||||
Microsoft has no responsibility to you or others with respect to any of these samples or any resulting output.
|
||||
|
||||
# What this sample demonstrates
|
||||
|
||||
This sample demonstrates a **key advantage of code-based hosted agents**:
|
||||
|
||||
- **Local Python tool execution** - Run custom Python functions as agent tools
|
||||
|
||||
Code-based agents can execute **any Python code** you write. This sample includes a Seattle Hotel Agent with a `get_available_hotels` tool that searches for available hotels based on check-in/check-out dates and budget preferences.
|
||||
|
||||
The agent is hosted using the [Azure AI AgentServer SDK](https://pypi.org/project/azure-ai-agentserver-agentframework/) and can be deployed to Microsoft Foundry using the Azure Developer CLI.
|
||||
|
||||
## How It Works
|
||||
|
||||
### Local Tools Integration
|
||||
|
||||
In [main.py](main.py), the agent uses a local Python function (`get_available_hotels`) that simulates a hotel availability API. This demonstrates how code-based agents can execute custom server-side logic that prompt agents cannot access.
|
||||
|
||||
The tool accepts:
|
||||
|
||||
- **check_in_date** - Check-in date in YYYY-MM-DD format
|
||||
- **check_out_date** - Check-out date in YYYY-MM-DD format
|
||||
- **max_price** - Maximum price per night in USD (optional, defaults to $500)
|
||||
|
||||
### Agent Hosting
|
||||
|
||||
The agent is hosted using the [Azure AI AgentServer SDK](https://pypi.org/project/azure-ai-agentserver-agentframework/),
|
||||
which provisions a REST API endpoint compatible with the OpenAI Responses protocol.
|
||||
|
||||
### Agent Deployment
|
||||
|
||||
The hosted agent can be deployed to Microsoft Foundry using the Azure Developer CLI [ai agent](https://learn.microsoft.com/en-us/azure/ai-foundry/agents/concepts/hosted-agents?view=foundry&tabs=cli#create-a-hosted-agent) extension.
|
||||
|
||||
## Running the Agent Locally
|
||||
|
||||
### Prerequisites
|
||||
|
||||
Before running this sample, ensure you have:
|
||||
|
||||
1. **Microsoft Foundry Project**
|
||||
- Project created in [Microsoft Foundry](https://learn.microsoft.com/en-us/azure/ai-foundry/what-is-foundry?view=foundry#microsoft-foundry-portals)
|
||||
- Chat model deployed (e.g., `gpt-4o` or `gpt-4.1`)
|
||||
- Note your project endpoint URL and model deployment name
|
||||
|
||||
2. **Azure CLI**
|
||||
- Installed and authenticated
|
||||
- Run `az login` and verify with `az account show`
|
||||
|
||||
3. **Python 3.10 or higher**
|
||||
- Verify your version: `python --version`
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Set the following environment variables (matching `agent.yaml`):
|
||||
|
||||
- `FOUNDRY_PROJECT_ENDPOINT` - Your Microsoft Foundry project endpoint URL (required)
|
||||
- `FOUNDRY_MODEL` - The deployment name for your chat model (defaults to `gpt-4.1-mini`)
|
||||
|
||||
This sample loads environment variables from a local `.env` file if present.
|
||||
|
||||
Create a `.env` file in this directory with the following content:
|
||||
|
||||
```
|
||||
FOUNDRY_PROJECT_ENDPOINT=https://<your-resource>.services.ai.azure.com/api/projects/<your-project>
|
||||
FOUNDRY_MODEL=gpt-4.1-mini
|
||||
```
|
||||
|
||||
Or set them via PowerShell:
|
||||
|
||||
```powershell
|
||||
# Replace with your actual values
|
||||
$env:FOUNDRY_PROJECT_ENDPOINT="https://<your-resource>.services.ai.azure.com/api/projects/<your-project>"
|
||||
$env:FOUNDRY_MODEL="gpt-4.1-mini"
|
||||
```
|
||||
|
||||
### Running the Sample
|
||||
|
||||
**Recommended (`uv`):**
|
||||
|
||||
We recommend using [uv](https://docs.astral.sh/uv/) to create and manage the virtual environment for this sample.
|
||||
|
||||
```bash
|
||||
uv venv .venv
|
||||
uv pip install --prerelease=allow -r requirements.txt
|
||||
uv run main.py
|
||||
```
|
||||
|
||||
The sample depends on preview packages, so `--prerelease=allow` is required when installing with `uv`.
|
||||
|
||||
**Alternative (`venv`):**
|
||||
|
||||
If you do not have `uv` installed, you can use Python's built-in `venv` module instead:
|
||||
|
||||
**Windows (PowerShell):**
|
||||
|
||||
```powershell
|
||||
python -m venv .venv
|
||||
.\.venv\Scripts\Activate.ps1
|
||||
pip install -r requirements.txt
|
||||
python main.py
|
||||
```
|
||||
|
||||
**macOS/Linux:**
|
||||
|
||||
```bash
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
python main.py
|
||||
```
|
||||
|
||||
This will start the hosted agent locally on `http://localhost:8088/`.
|
||||
|
||||
### Interacting with the Agent
|
||||
|
||||
**PowerShell (Windows):**
|
||||
|
||||
```powershell
|
||||
$body = @{
|
||||
input = "I need a hotel in Seattle from 2025-03-15 to 2025-03-18, budget under $200 per night"
|
||||
stream = $false
|
||||
} | ConvertTo-Json
|
||||
|
||||
Invoke-RestMethod -Uri http://localhost:8088/responses -Method Post -Body $body -ContentType "application/json"
|
||||
```
|
||||
|
||||
**Bash/curl (Linux/macOS):**
|
||||
|
||||
```bash
|
||||
curl -sS -H "Content-Type: application/json" -X POST http://localhost:8088/responses \
|
||||
-d '{"input": "Find me hotels in Seattle for March 20-23, 2025 under $200 per night","stream":false}'
|
||||
```
|
||||
|
||||
The agent will use the `get_available_hotels` tool to search for available hotels matching your criteria.
|
||||
|
||||
### Deploying the Agent to Microsoft Foundry
|
||||
|
||||
To deploy your agent to Microsoft Foundry, follow the comprehensive deployment guide at https://learn.microsoft.com/en-us/azure/ai-foundry/agents/concepts/hosted-agents?view=foundry&tabs=cli
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Images built on Apple Silicon or other ARM64 machines do not work on our service
|
||||
|
||||
We **recommend using `azd` cloud build**, which always builds images with the correct architecture.
|
||||
|
||||
If you choose to **build locally**, and your machine is **not `linux/amd64`** (for example, an Apple Silicon Mac), the image will **not be compatible with our service**, causing runtime failures.
|
||||
|
||||
**Fix for local builds**
|
||||
|
||||
Use this command to build the image locally:
|
||||
|
||||
```shell
|
||||
docker build --platform=linux/amd64 -t image .
|
||||
```
|
||||
|
||||
This forces the image to be built for the required `amd64` architecture.
|
||||
@@ -0,0 +1,27 @@
|
||||
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml
|
||||
|
||||
kind: hosted
|
||||
name: agent-with-local-tools
|
||||
# Brief description of what this agent does
|
||||
description: >
|
||||
A travel assistant agent that helps users find hotels in Seattle.
|
||||
Demonstrates local Python tool execution - a key advantage of code-based
|
||||
hosted agents over prompt agents.
|
||||
metadata:
|
||||
# Categorization tags for organizing and discovering agents
|
||||
authors:
|
||||
- Microsoft
|
||||
tags:
|
||||
- Azure AI AgentServer
|
||||
- Microsoft Agent Framework
|
||||
- Local Tools
|
||||
- Travel Assistant
|
||||
- Hotel Search
|
||||
protocols:
|
||||
- protocol: responses
|
||||
version: v1
|
||||
environment_variables:
|
||||
- name: FOUNDRY_PROJECT_ENDPOINT
|
||||
value: ${FOUNDRY_PROJECT_ENDPOINT}
|
||||
- name: FOUNDRY_MODEL
|
||||
value: ${FOUNDRY_MODEL}
|
||||
@@ -0,0 +1,144 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
"""
|
||||
Seattle Hotel Agent - A simple agent with a tool to find hotels in Seattle.
|
||||
Uses Microsoft Agent Framework with Azure AI Foundry.
|
||||
Ready for deployment to Foundry Hosted Agent service.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
from datetime import datetime
|
||||
from typing import Annotated
|
||||
|
||||
from agent_framework import Agent
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from azure.ai.agentserver.agentframework import from_agent_framework
|
||||
from azure.identity.aio import AzureCliCredential, ManagedIdentityCredential
|
||||
|
||||
# Configure these for your Foundry project
|
||||
# Read the explicit variables present in the .env file
|
||||
FOUNDRY_PROJECT_ENDPOINT = os.getenv("FOUNDRY_PROJECT_ENDPOINT") # e.g., "https://<project>.services.ai.azure.com"
|
||||
FOUNDRY_MODEL = os.getenv("FOUNDRY_MODEL", "gpt-4.1-mini") # Your model deployment name e.g., "gpt-4.1-mini"
|
||||
|
||||
|
||||
# Simulated hotel data for Seattle
|
||||
SEATTLE_HOTELS = [
|
||||
{
|
||||
"name": "Contoso Suites",
|
||||
"price_per_night": 189,
|
||||
"rating": 4.5,
|
||||
"location": "Downtown",
|
||||
},
|
||||
{
|
||||
"name": "Fabrikam Residences",
|
||||
"price_per_night": 159,
|
||||
"rating": 4.2,
|
||||
"location": "Pike Place Market",
|
||||
},
|
||||
{
|
||||
"name": "Alpine Ski House",
|
||||
"price_per_night": 249,
|
||||
"rating": 4.7,
|
||||
"location": "Seattle Center",
|
||||
},
|
||||
{
|
||||
"name": "Margie's Travel Lodge",
|
||||
"price_per_night": 219,
|
||||
"rating": 4.4,
|
||||
"location": "Waterfront",
|
||||
},
|
||||
{
|
||||
"name": "Northwind Inn",
|
||||
"price_per_night": 139,
|
||||
"rating": 4.0,
|
||||
"location": "Capitol Hill",
|
||||
},
|
||||
{
|
||||
"name": "Relecloud Hotel",
|
||||
"price_per_night": 99,
|
||||
"rating": 3.8,
|
||||
"location": "University District",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def get_available_hotels(
|
||||
check_in_date: Annotated[str, "Check-in date in YYYY-MM-DD format"],
|
||||
check_out_date: Annotated[str, "Check-out date in YYYY-MM-DD format"],
|
||||
max_price: Annotated[int, "Maximum price per night in USD (optional)"] = 500,
|
||||
) -> str:
|
||||
"""
|
||||
Get available hotels in Seattle for the specified dates.
|
||||
This simulates a call to a fake hotel availability API.
|
||||
"""
|
||||
try:
|
||||
# Parse dates
|
||||
check_in = datetime.strptime(check_in_date, "%Y-%m-%d")
|
||||
check_out = datetime.strptime(check_out_date, "%Y-%m-%d")
|
||||
|
||||
# Validate dates
|
||||
if check_out <= check_in:
|
||||
return "Error: Check-out date must be after check-in date."
|
||||
|
||||
nights = (check_out - check_in).days
|
||||
|
||||
# Filter hotels by price
|
||||
available_hotels = [hotel for hotel in SEATTLE_HOTELS if hotel["price_per_night"] <= max_price]
|
||||
|
||||
if not available_hotels:
|
||||
return f"No hotels found in Seattle within your budget of ${max_price}/night."
|
||||
|
||||
# Build response
|
||||
result = f"Available hotels in Seattle from {check_in_date} to {check_out_date} ({nights} nights):\n\n"
|
||||
|
||||
for hotel in available_hotels:
|
||||
total_cost = hotel["price_per_night"] * nights
|
||||
result += f"**{hotel['name']}**\n"
|
||||
result += f" Location: {hotel['location']}\n"
|
||||
result += f" Rating: {hotel['rating']}/5\n"
|
||||
result += f" ${hotel['price_per_night']}/night (Total: ${total_cost})\n\n"
|
||||
|
||||
return result
|
||||
|
||||
except ValueError as e:
|
||||
return f"Error parsing dates. Please use YYYY-MM-DD format. Details: {str(e)}"
|
||||
|
||||
|
||||
def get_credential():
|
||||
"""Will use Managed Identity when running in Azure, otherwise falls back to Azure CLI Credential."""
|
||||
return ManagedIdentityCredential() if os.getenv("MSI_ENDPOINT") else AzureCliCredential()
|
||||
|
||||
|
||||
async def main():
|
||||
"""Main function to run the agent as a web server."""
|
||||
async with get_credential() as credential:
|
||||
client = FoundryChatClient(
|
||||
project_endpoint=FOUNDRY_PROJECT_ENDPOINT,
|
||||
model=FOUNDRY_MODEL,
|
||||
credential=credential,
|
||||
)
|
||||
agent = Agent(
|
||||
client=client,
|
||||
name="SeattleHotelAgent",
|
||||
instructions="""You are a helpful travel assistant specializing in finding hotels in Seattle, Washington.
|
||||
|
||||
When a user asks about hotels in Seattle:
|
||||
1. Ask for their check-in and check-out dates if not provided
|
||||
2. Ask about their budget preferences if not mentioned
|
||||
3. Use the get_available_hotels tool to find available options
|
||||
4. Present the results in a friendly, informative way
|
||||
5. Offer to help with additional questions about the hotels or Seattle
|
||||
|
||||
Be conversational and helpful. If users ask about things outside of Seattle hotels,
|
||||
politely let them know you specialize in Seattle hotel recommendations.""",
|
||||
tools=[get_available_hotels],
|
||||
)
|
||||
|
||||
print("Seattle Hotel Agent Server running on http://localhost:8088")
|
||||
server = from_agent_framework(agent)
|
||||
await server.run_async()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,2 @@
|
||||
azure-ai-agentserver-agentframework==1.0.0b16
|
||||
agent-framework-foundry
|
||||
@@ -0,0 +1,33 @@
|
||||
# Unique identifier/name for this agent
|
||||
name: agent-with-text-search-rag
|
||||
# Brief description of what this agent does
|
||||
description: >
|
||||
An AI agent that uses a ContextProvider for retrieval augmented generation (RAG) capabilities.
|
||||
The agent runs searches against an external knowledge base before each model invocation and
|
||||
injects the results into the model context. It can answer questions about Contoso Outdoors
|
||||
policies and products, including return policies, refunds, shipping options, and product care
|
||||
instructions such as tent maintenance.
|
||||
metadata:
|
||||
# Categorization tags for organizing and discovering agents
|
||||
authors:
|
||||
- Microsoft Agent Framework Team
|
||||
tags:
|
||||
- Azure AI AgentServer
|
||||
- Microsoft Agent Framework
|
||||
- Retrieval-Augmented Generation
|
||||
- RAG
|
||||
template:
|
||||
name: agent-with-text-search-rag
|
||||
# The type of agent - "hosted" for HOBO, "container" for COBO
|
||||
kind: hosted
|
||||
protocols:
|
||||
- protocol: responses
|
||||
environment_variables:
|
||||
- name: AZURE_OPENAI_ENDPOINT
|
||||
value: ${AZURE_OPENAI_ENDPOINT}
|
||||
- name: AZURE_OPENAI_MODEL
|
||||
value: "{{chat}}"
|
||||
resources:
|
||||
- kind: model
|
||||
id: gpt-4o-mini
|
||||
name: chat
|
||||
@@ -0,0 +1,123 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import json
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from agent_framework import Agent, AgentSession, ContextProvider, Message, SessionContext
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from azure.ai.agentserver.agentframework import from_agent_framework # pyright: ignore[reportUnknownVariableType]
|
||||
from azure.identity import DefaultAzureCredential
|
||||
from dotenv import load_dotenv
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
from typing import override
|
||||
else:
|
||||
from typing_extensions import override
|
||||
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
|
||||
@dataclass
|
||||
class TextSearchResult:
|
||||
source_name: str
|
||||
source_link: str
|
||||
text: str
|
||||
|
||||
|
||||
class TextSearchContextProvider(ContextProvider):
|
||||
"""A simple context provider that simulates text search results based on keywords in the user's message."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__("text-search")
|
||||
|
||||
def _get_most_recent_message(self, messages: list[Message]) -> Message:
|
||||
"""Helper method to extract the most recent message from the input."""
|
||||
if messages:
|
||||
return messages[-1]
|
||||
raise ValueError("No messages provided")
|
||||
|
||||
@override
|
||||
async def before_run(
|
||||
self,
|
||||
*,
|
||||
agent: Any,
|
||||
session: AgentSession | None,
|
||||
context: SessionContext,
|
||||
state: dict[str, Any],
|
||||
) -> None:
|
||||
messages = context.get_messages()
|
||||
if not messages:
|
||||
return
|
||||
message = self._get_most_recent_message(messages)
|
||||
query = message.text.lower()
|
||||
|
||||
results: list[TextSearchResult] = []
|
||||
if "return" in query and "refund" in query:
|
||||
results.append(
|
||||
TextSearchResult(
|
||||
source_name="Contoso Outdoors Return Policy",
|
||||
source_link="https://contoso.com/policies/returns",
|
||||
text=(
|
||||
"Customers may return any item within 30 days of delivery. "
|
||||
"Items should be unused and include original packaging. "
|
||||
"Refunds are issued to the original payment method within 5 business days of inspection."
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
if "shipping" in query:
|
||||
results.append(
|
||||
TextSearchResult(
|
||||
source_name="Contoso Outdoors Shipping Guide",
|
||||
source_link="https://contoso.com/help/shipping",
|
||||
text=(
|
||||
"Standard shipping is free on orders over $50 and typically arrives in 3-5 business days "
|
||||
"within the continental United States. Expedited options are available at checkout."
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
if "tent" in query or "fabric" in query:
|
||||
results.append(
|
||||
TextSearchResult(
|
||||
source_name="TrailRunner Tent Care Instructions",
|
||||
source_link="https://contoso.com/manuals/trailrunner-tent",
|
||||
text=(
|
||||
"Clean the tent fabric with lukewarm water and a non-detergent soap. "
|
||||
"Allow it to air dry completely before storage and avoid prolonged UV "
|
||||
"exposure to extend the lifespan of the waterproof coating."
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
if not results:
|
||||
return
|
||||
|
||||
context.extend_messages(
|
||||
self.source_id,
|
||||
[Message(role="user", contents=["\n\n".join(json.dumps(result.__dict__, indent=2) for result in results)])],
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
# Create an Agent using the Azure OpenAI Chat Client
|
||||
agent = Agent(
|
||||
client=FoundryChatClient(credential=DefaultAzureCredential()),
|
||||
name="SupportSpecialist",
|
||||
instructions=(
|
||||
"You are a helpful support specialist for Contoso Outdoors. "
|
||||
"Answer questions using the provided context and cite the source document when available."
|
||||
),
|
||||
context_providers=[TextSearchContextProvider()],
|
||||
)
|
||||
|
||||
# Run the agent as a hosted agent
|
||||
from_agent_framework(agent).run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,2 @@
|
||||
azure-ai-agentserver-agentframework==1.0.0b3
|
||||
agent-framework
|
||||
@@ -0,0 +1,28 @@
|
||||
# Unique identifier/name for this agent
|
||||
name: agents-in-workflow
|
||||
# Brief description of what this agent does
|
||||
description: >
|
||||
A workflow agent that responds to product launch strategy inquiries by concurrently leveraging insights from three specialized agents.
|
||||
metadata:
|
||||
# Categorization tags for organizing and discovering agents
|
||||
authors:
|
||||
- Microsoft Agent Framework Team
|
||||
tags:
|
||||
- Azure AI AgentServer
|
||||
- Microsoft Agent Framework
|
||||
- Workflows
|
||||
template:
|
||||
name: agents-in-workflow
|
||||
# The type of agent - "hosted" for HOBO, "container" for COBO
|
||||
kind: hosted
|
||||
protocols:
|
||||
- protocol: responses
|
||||
environment_variables:
|
||||
- name: AZURE_OPENAI_ENDPOINT
|
||||
value: ${AZURE_OPENAI_ENDPOINT}
|
||||
- name: AZURE_OPENAI_MODEL
|
||||
value: "{{chat}}"
|
||||
resources:
|
||||
- kind: model
|
||||
id: gpt-4o-mini
|
||||
name: chat
|
||||
@@ -0,0 +1,52 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from agent_framework import Agent
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from agent_framework_orchestrations import ConcurrentBuilder
|
||||
from azure.ai.agentserver.agentframework import from_agent_framework
|
||||
from azure.identity import DefaultAzureCredential # pyright: ignore[reportUnknownVariableType]
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
|
||||
def main():
|
||||
# Create agents
|
||||
researcher = Agent(
|
||||
client=FoundryChatClient(credential=DefaultAzureCredential()),
|
||||
instructions=(
|
||||
"You're an expert market and product researcher. "
|
||||
"Given a prompt, provide concise, factual insights, opportunities, and risks."
|
||||
),
|
||||
name="researcher",
|
||||
)
|
||||
marketer = Agent(
|
||||
client=FoundryChatClient(credential=DefaultAzureCredential()),
|
||||
instructions=(
|
||||
"You're a creative marketing strategist. "
|
||||
"Craft compelling value propositions and target messaging aligned to the prompt."
|
||||
),
|
||||
name="marketer",
|
||||
)
|
||||
legal = Agent(
|
||||
client=FoundryChatClient(credential=DefaultAzureCredential()),
|
||||
instructions=(
|
||||
"You're a cautious legal/compliance reviewer. "
|
||||
"Highlight constraints, disclaimers, and policy concerns based on the prompt."
|
||||
),
|
||||
name="legal",
|
||||
)
|
||||
|
||||
# Build a concurrent workflow
|
||||
workflow = ConcurrentBuilder(participants=[researcher, marketer, legal]).build()
|
||||
|
||||
# Convert the workflow to an agent
|
||||
workflow_agent = Agent(client=workflow)
|
||||
|
||||
# Run the agent as a hosted agent
|
||||
from_agent_framework(workflow_agent).run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,2 @@
|
||||
azure-ai-agentserver-agentframework==1.0.0b3
|
||||
agent-framework
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
# Build artifacts
|
||||
bin/
|
||||
obj/
|
||||
|
||||
# IDE and editor files
|
||||
.vs/
|
||||
.vscode/
|
||||
*.user
|
||||
*.suo
|
||||
.foundry/
|
||||
|
||||
# Source control
|
||||
.git/
|
||||
|
||||
# Documentation
|
||||
README.md
|
||||
|
||||
# Ignore files
|
||||
.gitignore
|
||||
.dockerignore
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.temp
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Package manager directories
|
||||
node_modules/
|
||||
packages/
|
||||
|
||||
# Test results
|
||||
TestResults/
|
||||
*.trx
|
||||
|
||||
# Coverage reports
|
||||
coverage/
|
||||
*.coverage
|
||||
*.coveragexml
|
||||
|
||||
# Local development config
|
||||
appsettings.Development.json
|
||||
.env
|
||||
|
||||
.venv/
|
||||
__pycache__/
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
# IMPORTANT: Never commit .env to version control - add it to .gitignore
|
||||
FOUNDRY_PROJECT_ENDPOINT=
|
||||
FOUNDRY_MODEL=
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
FROM python:3.14-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ./ .
|
||||
|
||||
RUN pip install --upgrade pip && \
|
||||
if [ -f requirements.txt ]; then \
|
||||
pip install -r requirements.txt; \
|
||||
else \
|
||||
echo "No requirements.txt found"; \
|
||||
fi
|
||||
|
||||
EXPOSE 8088
|
||||
|
||||
CMD ["python", "main.py"]
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
**IMPORTANT!** All samples and other resources made available in this GitHub repository ("samples") are designed to assist in accelerating development of agents, solutions, and agent workflows for various scenarios. Review all provided resources and carefully test output behavior in the context of your use case. AI responses may be inaccurate and AI actions should be monitored with human oversight. Learn more in the transparency documents for [Agent Service](https://learn.microsoft.com/en-us/azure/ai-foundry/responsible-ai/agents/transparency-note) and [Agent Framework](https://github.com/microsoft/agent-framework/blob/main/TRANSPARENCY_FAQ.md).
|
||||
|
||||
Agents, solutions, or other output you create may be subject to legal and regulatory requirements, may require licenses, or may not be suitable for all industries, scenarios, or use cases. By using any sample, you are acknowledging that any output created using those samples are solely your responsibility, and that you will comply with all applicable laws, regulations, and relevant safety standards, terms of service, and codes of conduct.
|
||||
|
||||
Third-party samples contained in this folder are subject to their own designated terms, and they have not been tested or verified by Microsoft or its affiliates.
|
||||
|
||||
Microsoft has no responsibility to you or others with respect to any of these samples or any resulting output.
|
||||
|
||||
# What this sample demonstrates
|
||||
|
||||
This sample demonstrates a **key advantage of code-based hosted agents**:
|
||||
|
||||
- **Agents in Workflows** - Use AI agents as executors within a workflow pipeline
|
||||
|
||||
Code-based agents can execute **any Python code** you write. This sample includes a multi-agent workflow where Writer and Reviewer agents collaborate to draft content and provide review feedback.
|
||||
|
||||
The agent is hosted using the [Azure AI AgentServer SDK](https://pypi.org/project/azure-ai-agentserver-agentframework/) and can be deployed to Microsoft Foundry using the Azure Developer CLI.
|
||||
|
||||
## How It Works
|
||||
|
||||
### Agents in Workflows
|
||||
|
||||
This sample demonstrates the integration of AI agents within a workflow pipeline. The workflow operates as follows:
|
||||
|
||||
1. **Writer Agent** - Drafts content
|
||||
2. **Reviewer Agent** - Reviews the draft and provides concise, actionable feedback
|
||||
|
||||
### Agent Hosting
|
||||
|
||||
The agent workflow is hosted using the [Azure AI AgentServer SDK](https://pypi.org/project/azure-ai-agentserver-agentframework/),
|
||||
which provisions a REST API endpoint compatible with the OpenAI Responses protocol.
|
||||
|
||||
### Agent Deployment
|
||||
|
||||
The hosted agent workflow can be deployed to Microsoft Foundry using the Azure Developer CLI [ai agent](https://learn.microsoft.com/en-us/azure/ai-foundry/agents/concepts/hosted-agents?view=foundry&tabs=cli#create-a-hosted-agent) extension.
|
||||
|
||||
## Running the Agent Locally
|
||||
|
||||
### Prerequisites
|
||||
|
||||
Before running this sample, ensure you have:
|
||||
|
||||
1. **Microsoft Foundry Project**
|
||||
- Project created in [Microsoft Foundry](https://learn.microsoft.com/en-us/azure/ai-foundry/what-is-foundry?view=foundry#microsoft-foundry-portals)
|
||||
- Chat model deployed (e.g., `gpt-4o` or `gpt-4.1`)
|
||||
- Note your project endpoint URL and model deployment name
|
||||
|
||||
2. **Azure CLI**
|
||||
- Installed and authenticated
|
||||
- Run `az login` and verify with `az account show`
|
||||
|
||||
3. **Python 3.10 or higher**
|
||||
- Verify your version: `python --version`
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Set the following environment variables (matching `agent.yaml`):
|
||||
|
||||
- `FOUNDRY_PROJECT_ENDPOINT` - Your Microsoft Foundry project endpoint URL (required)
|
||||
- `FOUNDRY_MODEL` - The deployment name for your chat model (defaults to `gpt-4.1-mini`)
|
||||
|
||||
This sample loads environment variables from a local `.env` file if present.
|
||||
|
||||
Create a `.env` file in this directory with the following content:
|
||||
|
||||
```
|
||||
FOUNDRY_PROJECT_ENDPOINT=https://<your-resource>.services.ai.azure.com/api/projects/<your-project>
|
||||
FOUNDRY_MODEL=gpt-4.1-mini
|
||||
```
|
||||
|
||||
Or set them via PowerShell:
|
||||
|
||||
```powershell
|
||||
# Replace with your actual values
|
||||
$env:FOUNDRY_PROJECT_ENDPOINT="https://<your-resource>.services.ai.azure.com/api/projects/<your-project>"
|
||||
$env:FOUNDRY_MODEL="gpt-4.1-mini"
|
||||
```
|
||||
|
||||
### Running the Sample
|
||||
|
||||
**Recommended (`uv`):**
|
||||
|
||||
We recommend using [uv](https://docs.astral.sh/uv/) to create and manage the virtual environment for this sample.
|
||||
|
||||
```bash
|
||||
uv venv .venv
|
||||
uv pip install --prerelease=allow -r requirements.txt
|
||||
uv run main.py
|
||||
```
|
||||
|
||||
The sample depends on preview packages, so `--prerelease=allow` is required when installing with `uv`.
|
||||
|
||||
**Alternative (`venv`):**
|
||||
|
||||
If you do not have `uv` installed, you can use Python's built-in `venv` module instead:
|
||||
|
||||
**Windows (PowerShell):**
|
||||
|
||||
```powershell
|
||||
python -m venv .venv
|
||||
.\.venv\Scripts\Activate.ps1
|
||||
pip install -r requirements.txt
|
||||
python main.py
|
||||
```
|
||||
|
||||
**macOS/Linux:**
|
||||
|
||||
```bash
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
python main.py
|
||||
```
|
||||
|
||||
This will start the hosted agent locally on `http://localhost:8088/`.
|
||||
|
||||
### Interacting with the Agent
|
||||
|
||||
**PowerShell (Windows):**
|
||||
|
||||
```powershell
|
||||
$body = @{
|
||||
input = "Create a slogan for a new electric SUV that is affordable and fun to drive."
|
||||
stream = $false
|
||||
} | ConvertTo-Json
|
||||
|
||||
Invoke-RestMethod -Uri http://localhost:8088/responses -Method Post -Body $body -ContentType "application/json"
|
||||
```
|
||||
|
||||
**Bash/curl (Linux/macOS):**
|
||||
|
||||
```bash
|
||||
curl -sS -H "Content-Type: application/json" -X POST http://localhost:8088/responses \
|
||||
-d '{"input": "Create a slogan for a new electric SUV that is affordable and fun to drive.","stream":false}'
|
||||
```
|
||||
|
||||
### Deploying the Agent to Microsoft Foundry
|
||||
|
||||
To deploy your agent to Microsoft Foundry, follow the comprehensive deployment guide at https://learn.microsoft.com/en-us/azure/ai-foundry/agents/concepts/hosted-agents?view=foundry&tabs=cli
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Images built on Apple Silicon or other ARM64 machines do not work on our service
|
||||
|
||||
We **recommend using `azd` cloud build**, which always builds images with the correct architecture.
|
||||
|
||||
If you choose to **build locally**, and your machine is **not `linux/amd64`** (for example, an Apple Silicon Mac), the image will **not be compatible with our service**, causing runtime failures.
|
||||
|
||||
**Fix for local builds**
|
||||
|
||||
Use this command to build the image locally:
|
||||
|
||||
```shell
|
||||
docker build --platform=linux/amd64 -t image .
|
||||
```
|
||||
|
||||
This forces the image to be built for the required `amd64` architecture.
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml
|
||||
|
||||
kind: hosted
|
||||
name: writer-reviewer-agents-in-workflow
|
||||
description: >
|
||||
A multi-agent workflow featuring a Writer and Reviewer that collaborate
|
||||
to create and refine content.
|
||||
metadata:
|
||||
authors:
|
||||
- Microsoft
|
||||
tags:
|
||||
- Azure AI AgentServer
|
||||
- Microsoft Agent Framework
|
||||
- Multi-Agent Workflow
|
||||
- Writer-Reviewer
|
||||
- Content Creation
|
||||
protocols:
|
||||
- protocol: responses
|
||||
version: v1
|
||||
environment_variables:
|
||||
- name: FOUNDRY_PROJECT_ENDPOINT
|
||||
value: ${FOUNDRY_PROJECT_ENDPOINT}
|
||||
- name: FOUNDRY_MODEL
|
||||
value: ${FOUNDRY_MODEL}
|
||||
@@ -0,0 +1,71 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from agent_framework import Agent, WorkflowBuilder
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from azure.ai.agentserver.agentframework import from_agent_framework
|
||||
from azure.identity.aio import AzureCliCredential, ManagedIdentityCredential
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv(override=True)
|
||||
|
||||
# Configure these for your Foundry project
|
||||
# Read the explicit variables present in the .env file
|
||||
FOUNDRY_PROJECT_ENDPOINT = os.getenv(
|
||||
"FOUNDRY_PROJECT_ENDPOINT"
|
||||
) # e.g., "https://<project>.services.ai.azure.com/api/projects/<project-name>"
|
||||
FOUNDRY_MODEL = os.getenv("FOUNDRY_MODEL", "gpt-4.1-mini") # Your model deployment name e.g., "gpt-4.1-mini"
|
||||
|
||||
|
||||
def get_credential():
|
||||
"""Will use Managed Identity when running in Azure, otherwise falls back to Azure CLI Credential."""
|
||||
return ManagedIdentityCredential() if os.getenv("MSI_ENDPOINT") else AzureCliCredential()
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def create_agents():
|
||||
async with get_credential() as credential:
|
||||
client = FoundryChatClient(
|
||||
project_endpoint=FOUNDRY_PROJECT_ENDPOINT,
|
||||
model=FOUNDRY_MODEL,
|
||||
credential=credential,
|
||||
)
|
||||
writer = Agent(
|
||||
client=client,
|
||||
name="Writer",
|
||||
instructions="You are an excellent content writer. You create new content and edit contents based on the feedback.",
|
||||
)
|
||||
reviewer = Agent(
|
||||
client=client,
|
||||
name="Reviewer",
|
||||
instructions="You are an excellent content reviewer. Provide actionable feedback to the writer about the provided content in the most concise manner possible.",
|
||||
)
|
||||
yield writer, reviewer
|
||||
|
||||
|
||||
def create_workflow(writer, reviewer):
|
||||
workflow = WorkflowBuilder(start_executor=writer).add_edge(writer, reviewer).build()
|
||||
return Agent(
|
||||
client=workflow,
|
||||
)
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""
|
||||
The writer and reviewer multi-agent workflow.
|
||||
|
||||
Environment variables required:
|
||||
- FOUNDRY_PROJECT_ENDPOINT: Your Microsoft Foundry project endpoint
|
||||
- FOUNDRY_MODEL: Your Microsoft Foundry model deployment name
|
||||
"""
|
||||
|
||||
async with create_agents() as (writer, reviewer):
|
||||
agent = create_workflow(writer, reviewer)
|
||||
await from_agent_framework(agent).run_async()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
azure-ai-agentserver-agentframework==1.0.0b16
|
||||
agent-framework-foundry
|
||||
Generated
+7
-164
@@ -42,7 +42,6 @@ members = [
|
||||
"agent-framework-devui",
|
||||
"agent-framework-durabletask",
|
||||
"agent-framework-foundry",
|
||||
"agent-framework-foundry-hosting",
|
||||
"agent-framework-foundry-local",
|
||||
"agent-framework-gemini",
|
||||
"agent-framework-github-copilot",
|
||||
@@ -417,7 +416,7 @@ dev = [{ name = "types-pyyaml", specifier = "==6.0.12.20250915" }]
|
||||
|
||||
[[package]]
|
||||
name = "agent-framework-devui"
|
||||
version = "1.0.0b260409"
|
||||
version = "1.0.0b260414"
|
||||
source = { editable = "packages/devui" }
|
||||
dependencies = [
|
||||
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
@@ -499,25 +498,6 @@ requires-dist = [
|
||||
{ name = "azure-ai-projects", specifier = ">=2.0.0,<3.0" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "agent-framework-foundry-hosting"
|
||||
version = "1.0.0a260402"
|
||||
source = { editable = "packages/foundry_hosting" }
|
||||
dependencies = [
|
||||
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "azure-ai-agentserver-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "azure-ai-agentserver-invocations", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "azure-ai-agentserver-responses", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "agent-framework-core", editable = "packages/core" },
|
||||
{ name = "azure-ai-agentserver-core", specifier = "==2.0.0b1" },
|
||||
{ name = "azure-ai-agentserver-invocations", specifier = "==1.0.0b1" },
|
||||
{ name = "azure-ai-agentserver-responses", specifier = "==1.0.0b1" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "agent-framework-foundry-local"
|
||||
version = "1.0.0b260409"
|
||||
@@ -1032,50 +1012,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "azure-ai-agentserver-core"
|
||||
version = "2.0.0b1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "azure-monitor-opentelemetry-exporter", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "hypercorn", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "opentelemetry-api", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "opentelemetry-exporter-otlp-proto-grpc", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "opentelemetry-sdk", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "starlette", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/50/a5/8007cc0cbb004290998182f123d6151676fd3cdefaf0bddb2394a6a98278/azure_ai_agentserver_core-2.0.0b1.tar.gz", hash = "sha256:a762186a027586f5c365c096c3fc6ae7dac3b53a3b00b523f0fd9c1d9b8e7bc7", size = 37035, upload-time = "2026-04-15T19:09:40.022Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/1c/e917df68bb92239816f5bf177d6885a04c27a51d77f23608b49c60654325/azure_ai_agentserver_core-2.0.0b1-py3-none-any.whl", hash = "sha256:85c3e4470c30451bc122aa7fe009778514b6f0d52897562f4104ce5f9e382ba5", size = 24317, upload-time = "2026-04-15T19:09:41.197Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "azure-ai-agentserver-invocations"
|
||||
version = "1.0.0b1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "azure-ai-agentserver-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b4/33/207f901d484646764b5d9eec3f3a02a90658c32aa78821d0221323b01081/azure_ai_agentserver_invocations-1.0.0b1.tar.gz", hash = "sha256:800dd39e32f6e58c0bc56fe74bb77bf4ccda77e133c4eaaa69d7cd8b0bf77a03", size = 29957, upload-time = "2026-04-15T19:35:31.015Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/52/02/e1dfa6747de27163c16bc4bfbb7aeac2d2bb6565e85ed92a6675ccee1ff8/azure_ai_agentserver_invocations-1.0.0b1-py3-none-any.whl", hash = "sha256:3efa15cc6011ba203760048b0153bd711a75adb56fe9b2a34565145a0fa82b09", size = 11376, upload-time = "2026-04-15T19:35:32.067Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "azure-ai-agentserver-responses"
|
||||
version = "1.0.0b1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "aiohttp", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "azure-ai-agentserver-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "azure-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "isodate", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cf/68/9a500cf1869d416810c3d856e483d707890686124adf0d35ff5179f59a4a/azure_ai_agentserver_responses-1.0.0b1.tar.gz", hash = "sha256:8af679bc0369f3e2637348b571942b5b41a1bccc5f261d965211e590a191049b", size = 361804, upload-time = "2026-04-15T19:34:19.404Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/41/e8/cf0ac9673b9d8952fd03ee3de9d6d3d51320942ae31b72cdc503f9892d2f/azure_ai_agentserver_responses-1.0.0b1-py3-none-any.whl", hash = "sha256:4d42534b5b6e523219e92d837efde48f76e03882f7013d1fbb4af21e47c6469b", size = 253262, upload-time = "2026-04-15T19:34:21.069Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "azure-ai-inference"
|
||||
version = "1.0.0b9"
|
||||
@@ -1188,23 +1124,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/49/9a/417b3a533e01953a7c618884df2cb05a71e7b68bdbce4fbdb62349d2a2e8/azure_identity-1.25.3-py3-none-any.whl", hash = "sha256:f4d0b956a8146f30333e071374171f3cfa7bdb8073adb8c3814b65567aa7447c", size = 192138, upload-time = "2026-03-13T01:12:22.951Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "azure-monitor-opentelemetry-exporter"
|
||||
version = "1.0.0b51"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "azure-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "azure-identity", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "msrest", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "opentelemetry-api", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "opentelemetry-sdk", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "psutil", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bc/a4/a6cd2d389bc1009300bcd57c9e2ace4b7e7ae1e5dc0bda415ee803629cf2/azure_monitor_opentelemetry_exporter-1.0.0b51.tar.gz", hash = "sha256:a6171c34326bcd6216938bb40d715c15f1f22984ac1986fc97231336d8ac4c3c", size = 319837, upload-time = "2026-04-06T21:45:46.378Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/1a/6b0b7a6181b42709103a65a676c89fd5055cb1d1b281ebe10c49254a170f/azure_monitor_opentelemetry_exporter-1.0.0b51-py2.py3-none-any.whl", hash = "sha256:6572cac11f96e3b18ae1187cb35cf3b40d0004655dae8048896c41c765bea530", size = 242104, upload-time = "2026-04-06T21:45:47.856Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "azure-search-documents"
|
||||
version = "11.7.0b2"
|
||||
@@ -2487,6 +2406,7 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/38/3f/9859f655d11901e7b2996c6e3d33e0caa9a1d4572c3bc61ed0faa64b2f4c/greenlet-3.3.2-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9bc885b89709d901859cf95179ec9f6bb67a3d2bb1f0e88456461bd4b7f8fd0d", size = 277747, upload-time = "2026-02-20T20:16:21.325Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/07/cb284a8b5c6498dbd7cba35d31380bb123d7dceaa7907f606c8ff5993cbf/greenlet-3.3.2-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b568183cf65b94919be4438dc28416b234b678c608cafac8874dfeeb2a9bbe13", size = 579202, upload-time = "2026-02-20T20:47:28.955Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/45/67922992b3a152f726163b19f890a85129a992f39607a2a53155de3448b8/greenlet-3.3.2-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:527fec58dc9f90efd594b9b700662ed3fb2493c2122067ac9c740d98080a620e", size = 590620, upload-time = "2026-02-20T20:55:55.581Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/5f/6e2a7d80c353587751ef3d44bb947f0565ec008a2e0927821c007e96d3a7/greenlet-3.3.2-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:508c7f01f1791fbc8e011bd508f6794cb95397fdb198a46cb6635eb5b78d85a7", size = 602132, upload-time = "2026-02-20T21:02:43.261Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/55/9f1ebb5a825215fadcc0f7d5073f6e79e3007e3282b14b22d6aba7ca6cb8/greenlet-3.3.2-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ad0c8917dd42a819fe77e6bdfcb84e3379c0de956469301d9fd36427a1ca501f", size = 591729, upload-time = "2026-02-20T20:20:58.395Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/b4/21f5455773d37f94b866eb3cf5caed88d6cea6dd2c6e1f9c34f463cba3ec/greenlet-3.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:97245cc10e5515dbc8c3104b2928f7f02b6813002770cfaffaf9a6e0fc2b94ef", size = 1551946, upload-time = "2026-02-20T20:49:31.102Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/68/91f061a926abead128fe1a87f0b453ccf07368666bd59ffa46016627a930/greenlet-3.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8c1fdd7d1b309ff0da81d60a9688a8bd044ac4e18b250320a96fc68d31c209ca", size = 1618494, upload-time = "2026-02-20T20:21:06.541Z" },
|
||||
@@ -2494,6 +2414,7 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/47/16400cb42d18d7a6bb46f0626852c1718612e35dcb0dffa16bbaffdf5dd2/greenlet-3.3.2-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:c56692189a7d1c7606cb794be0a8381470d95c57ce5be03fb3d0ef57c7853b86", size = 278890, upload-time = "2026-02-20T20:19:39.263Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/90/42762b77a5b6aa96cd8c0e80612663d39211e8ae8a6cd47c7f1249a66262/greenlet-3.3.2-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ebd458fa8285960f382841da585e02201b53a5ec2bac6b156fc623b5ce4499f", size = 581120, upload-time = "2026-02-20T20:47:30.161Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/6f/f3d64f4fa0a9c7b5c5b3c810ff1df614540d5aa7d519261b53fba55d4df9/greenlet-3.3.2-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a443358b33c4ec7b05b79a7c8b466f5d275025e750298be7340f8fc63dff2a55", size = 594363, upload-time = "2026-02-20T20:55:56.965Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/8b/1430a04657735a3f23116c2e0d5eb10220928846e4537a938a41b350bed6/greenlet-3.3.2-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4375a58e49522698d3e70cc0b801c19433021b5c37686f7ce9c65b0d5c8677d2", size = 605046, upload-time = "2026-02-20T21:02:45.234Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/83/3e06a52aca8128bdd4dcd67e932b809e76a96ab8c232a8b025b2850264c5/greenlet-3.3.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e2cd90d413acbf5e77ae41e5d3c9b3ac1d011a756d7284d7f3f2b806bbd6358", size = 594156, upload-time = "2026-02-20T20:20:59.955Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/79/0de5e62b873e08fe3cef7dbe84e5c4bc0e8ed0c7ff131bccb8405cd107c8/greenlet-3.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:442b6057453c8cb29b4fb36a2ac689382fc71112273726e2423f7f17dc73bf99", size = 1554649, upload-time = "2026-02-20T20:49:32.293Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/00/32d30dee8389dc36d42170a9c66217757289e2afb0de59a3565260f38373/greenlet-3.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:45abe8eb6339518180d5a7fa47fa01945414d7cca5ecb745346fc6a87d2750be", size = 1619472, upload-time = "2026-02-20T20:21:07.966Z" },
|
||||
@@ -2502,6 +2423,7 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/ab/1608e5a7578e62113506740b88066bf09888322a311cff602105e619bd87/greenlet-3.3.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ac8d61d4343b799d1e526db579833d72f23759c71e07181c2d2944e429eb09cd", size = 280358, upload-time = "2026-02-20T20:17:43.971Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/23/0eae412a4ade4e6623ff7626e38998cb9b11e9ff1ebacaa021e4e108ec15/greenlet-3.3.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ceec72030dae6ac0c8ed7591b96b70410a8be370b6a477b1dbc072856ad02bd", size = 601217, upload-time = "2026-02-20T20:47:31.462Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/16/5b1678a9c07098ecb9ab2dd159fafaf12e963293e61ee8d10ecb55273e5e/greenlet-3.3.2-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2a5be83a45ce6188c045bcc44b0ee037d6a518978de9a5d97438548b953a1ac", size = 611792, upload-time = "2026-02-20T20:55:58.423Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/c5/cc09412a29e43406eba18d61c70baa936e299bc27e074e2be3806ed29098/greenlet-3.3.2-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ae9e21c84035c490506c17002f5c8ab25f980205c3e61ddb3a2a2a2e6c411fcb", size = 626250, upload-time = "2026-02-20T21:02:46.596Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/1f/5155f55bd71cabd03765a4aac9ac446be129895271f73872c36ebd4b04b6/greenlet-3.3.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e99d1749147ac21dde49b99c9abffcbc1e2d55c67501465ef0930d6e78e070", size = 613875, upload-time = "2026-02-20T20:21:01.102Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/dd/845f249c3fcd69e32df80cdab059b4be8b766ef5830a3d0aa9d6cad55beb/greenlet-3.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c956a19350e2c37f2c48b336a3afb4bff120b36076d9d7fb68cb44e05d95b79", size = 1571467, upload-time = "2026-02-20T20:49:33.495Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/50/2649fe21fcc2b56659a452868e695634722a6655ba245d9f77f5656010bf/greenlet-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6c6f8ba97d17a1e7d664151284cb3315fc5f8353e75221ed4324f84eb162b395", size = 1640001, upload-time = "2026-02-20T20:21:09.154Z" },
|
||||
@@ -2510,6 +2432,7 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/48/f8b875fa7dea7dd9b33245e37f065af59df6a25af2f9561efa8d822fde51/greenlet-3.3.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:aa6ac98bdfd716a749b84d4034486863fd81c3abde9aa3cf8eff9127981a4ae4", size = 279120, upload-time = "2026-02-20T20:19:01.9Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/8d/9771d03e7a8b1ee456511961e1b97a6d77ae1dea4a34a5b98eee706689d3/greenlet-3.3.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab0c7e7901a00bc0a7284907273dc165b32e0d109a6713babd04471327ff7986", size = 603238, upload-time = "2026-02-20T20:47:32.873Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/0e/4223c2bbb63cd5c97f28ffb2a8aee71bdfb30b323c35d409450f51b91e3e/greenlet-3.3.2-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d248d8c23c67d2291ffd47af766e2a3aa9fa1c6703155c099feb11f526c63a92", size = 614219, upload-time = "2026-02-20T20:55:59.817Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/2b/4d012a69759ac9d77210b8bfb128bc621125f5b20fc398bce3940d036b1c/greenlet-3.3.2-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ccd21bb86944ca9be6d967cf7691e658e43417782bce90b5d2faeda0ff78a7dd", size = 628268, upload-time = "2026-02-20T21:02:48.024Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/34/259b28ea7a2a0c904b11cd36c79b8cef8019b26ee5dbe24e73b469dea347/greenlet-3.3.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6997d360a4e6a4e936c0f9625b1c20416b8a0ea18a8e19cabbefc712e7397ab", size = 616774, upload-time = "2026-02-20T20:21:02.454Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/03/996c2d1689d486a6e199cb0f1cf9e4aa940c500e01bdf201299d7d61fa69/greenlet-3.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64970c33a50551c7c50491671265d8954046cb6e8e2999aacdd60e439b70418a", size = 1571277, upload-time = "2026-02-20T20:49:34.795Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/c4/2570fc07f34a39f2caf0bf9f24b0a1a0a47bc2e8e465b2c2424821389dfc/greenlet-3.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1a9172f5bf6bd88e6ba5a84e0a68afeac9dc7b6b412b245dd64f52d83c81e55b", size = 1640455, upload-time = "2026-02-20T20:21:10.261Z" },
|
||||
@@ -2518,6 +2441,7 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/ae/8bffcbd373b57a5992cd077cbe8858fff39110480a9d50697091faea6f39/greenlet-3.3.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8d1658d7291f9859beed69a776c10822a0a799bc4bfe1bd4272bb60e62507dab", size = 279650, upload-time = "2026-02-20T20:18:00.783Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/c0/45f93f348fa49abf32ac8439938726c480bd96b2a3c6f4d949ec0124b69f/greenlet-3.3.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18cb1b7337bca281915b3c5d5ae19f4e76d35e1df80f4ad3c1a7be91fadf1082", size = 650295, upload-time = "2026-02-20T20:47:34.036Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/de/dd7589b3f2b8372069ab3e4763ea5329940fc7ad9dcd3e272a37516d7c9b/greenlet-3.3.2-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2e47408e8ce1c6f1ceea0dffcdf6ebb85cc09e55c7af407c99f1112016e45e9", size = 662163, upload-time = "2026-02-20T20:56:01.295Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/ac/85804f74f1ccea31ba518dcc8ee6f14c79f73fe36fa1beba38930806df09/greenlet-3.3.2-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e3cb43ce200f59483eb82949bf1835a99cf43d7571e900d7c8d5c62cdf25d2f9", size = 675371, upload-time = "2026-02-20T21:02:49.664Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/d8/09bfa816572a4d83bccd6750df1926f79158b1c36c5f73786e26dbe4ee38/greenlet-3.3.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63d10328839d1973e5ba35e98cccbca71b232b14051fd957b6f8b6e8e80d0506", size = 664160, upload-time = "2026-02-20T20:21:04.015Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/cf/56832f0c8255d27f6c35d41b5ec91168d74ec721d85f01a12131eec6b93c/greenlet-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e4ab3cfb02993c8cc248ea73d7dae6cec0253e9afa311c9b37e603ca9fad2ce", size = 1619181, upload-time = "2026-02-20T20:49:36.052Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/23/b90b60a4aabb4cec0796e55f25ffbfb579a907c3898cd2905c8918acaa16/greenlet-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94ad81f0fd3c0c0681a018a976e5c2bd2ca2d9d94895f23e7bb1af4e8af4e2d5", size = 1687713, upload-time = "2026-02-20T20:21:11.684Z" },
|
||||
@@ -2526,6 +2450,7 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/98/6d/8f2ef704e614bcf58ed43cfb8d87afa1c285e98194ab2cfad351bf04f81e/greenlet-3.3.2-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:e26e72bec7ab387ac80caa7496e0f908ff954f31065b0ffc1f8ecb1338b11b54", size = 286617, upload-time = "2026-02-20T20:19:29.856Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/0d/93894161d307c6ea237a43988f27eba0947b360b99ac5239ad3fe09f0b47/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b466dff7a4ffda6ca975979bab80bdadde979e29fc947ac3be4451428d8b0e4", size = 655189, upload-time = "2026-02-20T20:47:35.742Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/2c/d2d506ebd8abcb57386ec4f7ba20f4030cbe56eae541bc6fd6ef399c0b41/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8bddc5b73c9720bea487b3bffdb1840fe4e3656fba3bd40aa1489e9f37877ff", size = 658225, upload-time = "2026-02-20T20:56:02.527Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/67/8197b7e7e602150938049d8e7f30de1660cfb87e4c8ee349b42b67bdb2e1/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:59b3e2c40f6706b05a9cd299c836c6aa2378cabe25d021acd80f13abf81181cf", size = 666581, upload-time = "2026-02-20T21:02:51.526Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/30/3a09155fbf728673a1dea713572d2d31159f824a37c22da82127056c44e4/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b26b0f4428b871a751968285a1ac9648944cea09807177ac639b030bddebcea4", size = 657907, upload-time = "2026-02-20T20:21:05.259Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/fd/d05a4b7acd0154ed758797f0a43b4c0962a843bedfe980115e842c5b2d08/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1fb39a11ee2e4d94be9a76671482be9398560955c9e568550de0224e41104727", size = 1618857, upload-time = "2026-02-20T20:49:37.309Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/e1/50ee92a5db521de8f35075b5eff060dd43d39ebd46c2181a2042f7070385/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:20154044d9085151bc309e7689d6f7ba10027f8f5a8c0676ad398b951913d89e", size = 1680010, upload-time = "2026-02-20T20:21:13.427Z" },
|
||||
@@ -2791,25 +2716,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/ae/8a3a16ea4d202cb641b51d2681bdd3d482c1c592d7570b3fa264730829ce/huggingface_hub-1.8.0-py3-none-any.whl", hash = "sha256:d3eb5047bd4e33c987429de6020d4810d38a5bef95b3b40df9b17346b7f353f2", size = 625208, upload-time = "2026-03-25T16:01:26.603Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hypercorn"
|
||||
version = "0.18.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "exceptiongroup", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" },
|
||||
{ name = "h11", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "h2", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "priority", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "taskgroup", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" },
|
||||
{ name = "tomli", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" },
|
||||
{ name = "typing-extensions", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" },
|
||||
{ name = "wsproto", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/44/01/39f41a014b83dd5c795217362f2ca9071cf243e6a75bdcd6cd5b944658cc/hypercorn-0.18.0.tar.gz", hash = "sha256:d63267548939c46b0247dc8e5b45a9947590e35e64ee73a23c074aa3cf88e9da", size = 68420, upload-time = "2025-11-08T13:54:04.78Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/93/35/850277d1b17b206bd10874c8a9a3f52e059452fb49bb0d22cbb908f6038b/hypercorn-0.18.0-py3-none-any.whl", hash = "sha256:225e268f2c1c2f28f6d8f6db8f40cb8c992963610c5725e13ccfcddccb24b1cd", size = 61640, upload-time = "2025-11-08T13:54:03.202Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyperframe"
|
||||
version = "6.1.0"
|
||||
@@ -3746,22 +3652,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/75/bd9b7bb966668920f06b200e84454c8f3566b102183bc55c5473d96cb2b9/msal_extensions-1.3.1-py3-none-any.whl", hash = "sha256:96d3de4d034504e969ac5e85bae8106c8373b5c6568e4c8fa7af2eca9dbe6bca", size = 20583, upload-time = "2025-03-14T23:51:03.016Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "msrest"
|
||||
version = "0.7.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "azure-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "certifi", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "isodate", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "requests-oauthlib", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/68/77/8397c8fb8fc257d8ea0fa66f8068e073278c65f05acb17dcb22a02bfdc42/msrest-0.7.1.zip", hash = "sha256:6e7661f46f3afd88b75667b7187a92829924446c7ea1d169be8c4bb7eeb788b9", size = 175332, upload-time = "2022-06-13T22:41:25.111Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/15/cf/f2966a2638144491f8696c27320d5219f48a072715075d168b31d3237720/msrest-0.7.1-py3-none-any.whl", hash = "sha256:21120a810e1233e5e6cc7fe40b474eeb4ec6f757a15d7cf86702c369f9567c32", size = 85384, upload-time = "2022-06-13T22:41:22.42Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "multidict"
|
||||
version = "6.7.1"
|
||||
@@ -4863,15 +4753,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/53/05/9cca1708bb8c65264124eb4b04251e0f65ce5bfc707080bb6b492d5a0df7/prek-0.3.8-py3-none-win_arm64.whl", hash = "sha256:a2614647aeafa817a5802ccb9561e92eedc20dcf840639a1b00826e2c2442515", size = 5190872, upload-time = "2026-03-23T08:23:29.463Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "priority"
|
||||
version = "2.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f5/3c/eb7c35f4dcede96fca1842dac5f4f5d15511aa4b52f3a961219e68ae9204/priority-2.0.0.tar.gz", hash = "sha256:c965d54f1b8d0d0b19479db3924c7c36cf672dbf2aec92d43fbdaf4492ba18c0", size = 24792, upload-time = "2021-06-27T10:15:05.487Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/5f/82c8074f7e84978129347c2c6ec8b6c59f3584ff1a20bc3c940a3e061790/priority-2.0.0-py3-none-any.whl", hash = "sha256:6f8eefce5f3ad59baf2c080a664037bb4725cd0a790d53d59ab4059288faf6aa", size = 8946, upload-time = "2021-06-27T10:15:03.856Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "propcache"
|
||||
version = "0.4.1"
|
||||
@@ -5811,19 +5692,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "requests-oauthlib"
|
||||
version = "2.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "oauthlib", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650, upload-time = "2024-03-22T20:32:29.939Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179, upload-time = "2024-03-22T20:32:28.055Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rich"
|
||||
version = "13.9.4"
|
||||
@@ -6531,19 +6399,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl", hash = "sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3", size = 39814, upload-time = "2026-03-04T18:55:31.284Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "taskgroup"
|
||||
version = "0.2.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "exceptiongroup", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" },
|
||||
{ name = "typing-extensions", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f0/8d/e218e0160cc1b692e6e0e5ba34e8865dbb171efeb5fc9a704544b3020605/taskgroup-0.2.2.tar.gz", hash = "sha256:078483ac3e78f2e3f973e2edbf6941374fbea81b9c5d0a96f51d297717f4752d", size = 11504, upload-time = "2025-01-03T09:24:13.761Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/b1/74babcc824a57904e919f3af16d86c08b524c0691504baf038ef2d7f655c/taskgroup-0.2.2-py2.py3-none-any.whl", hash = "sha256:e2c53121609f4ae97303e9ea1524304b4de6faf9eb2c9280c7f87976479a52fb", size = 14237, upload-time = "2025-01-03T09:24:11.41Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tau2"
|
||||
version = "0.0.1"
|
||||
@@ -7243,18 +7098,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wsproto"
|
||||
version = "1.3.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "h11", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c7/79/12135bdf8b9c9367b8701c2c19a14c913c120b882d50b014ca0d38083c2c/wsproto-1.3.2.tar.gz", hash = "sha256:b86885dcf294e15204919950f666e06ffc6c7c114ca900b060d6e16293528294", size = 50116, upload-time = "2025-11-20T18:18:01.871Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl", hash = "sha256:61eea322cdf56e8cc904bd3ad7573359a242ba65688716b0710a5eb12beab584", size = 24405, upload-time = "2025-11-20T18:18:00.454Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yarl"
|
||||
version = "1.23.0"
|
||||
|
||||
Reference in New Issue
Block a user