mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: update FoundryAgent for hosted agent sessions (#5447)
* fixes to FoundryAgent to connect to new hosted agents Co-authored-by: Copilot <copilot@github.com> * fix mypy Co-authored-by: Copilot <copilot@github.com> * Python: remove Foundry service session helpers Remove the public hosted-agent service session CRUD helpers from FoundryAgent and drop the related feature-stage inventory entry. Update the hosted-agent sample to create and delete service sessions directly through the preview AIProjectClient APIs, and tighten a few test harnesses surfaced by full workspace validation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix from merge * fix hosted env detection Co-authored-by: Copilot <copilot@github.com> * reverted sample update * fix tests and code Co-authored-by: Copilot <copilot@github.com> * remove aenter * skipping some tests Co-authored-by: Copilot <copilot@github.com> --------- Co-authored-by: Copilot <copilot@github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
63c0a51797
commit
62e02da698
@@ -172,12 +172,7 @@ class ResponsesHostServer(ResponsesAgentServerHost):
|
||||
self._agent = agent
|
||||
self.response_handler(self._handle_response) # pyright: ignore[reportUnknownMemberType]
|
||||
|
||||
@staticmethod
|
||||
def _is_streaming_request(request: CreateResponse) -> bool:
|
||||
"""Check if the request is a streaming request."""
|
||||
return request.stream is not None and request.stream is True
|
||||
|
||||
def _handle_response(
|
||||
async def _handle_response(
|
||||
self,
|
||||
request: CreateResponse,
|
||||
context: ResponseContext,
|
||||
@@ -186,11 +181,10 @@ class ResponsesHostServer(ResponsesAgentServerHost):
|
||||
"""Handle the creation of a response."""
|
||||
if self._is_workflow_agent:
|
||||
# Workflow agents are handled differently because they require checkpoint restoration
|
||||
return self._handle_workflow_agent(request, context)
|
||||
return self._handle_inner_workflow(request, context)
|
||||
return self._handle_inner_agent(request, context)
|
||||
|
||||
return self._handle_regular_agent(request, context)
|
||||
|
||||
async def _handle_regular_agent(
|
||||
async def _handle_inner_agent(
|
||||
self,
|
||||
request: CreateResponse,
|
||||
context: ResponseContext,
|
||||
@@ -200,25 +194,24 @@ class ResponsesHostServer(ResponsesAgentServerHost):
|
||||
input_messages = _items_to_messages(input_items)
|
||||
|
||||
history = await context.get_history()
|
||||
messages: list[str | Content | Message] = [*_output_items_to_messages(history), *input_messages]
|
||||
run_kwargs: dict[str, Any] = {"messages": [*_output_items_to_messages(history), *input_messages]}
|
||||
is_streaming_request = request.stream is not None and request.stream is True
|
||||
|
||||
chat_options, are_options_set = _to_chat_options(request)
|
||||
|
||||
is_streaming_request = self._is_streaming_request(request)
|
||||
response_event_stream = ResponseEventStream(response_id=context.response_id, model=request.model)
|
||||
|
||||
yield response_event_stream.emit_created()
|
||||
yield response_event_stream.emit_in_progress()
|
||||
|
||||
if are_options_set and not isinstance(self._agent, RawAgent):
|
||||
logger.warning("Agent doesn't support runtime options. They will be ignored.")
|
||||
else:
|
||||
run_kwargs["options"] = chat_options
|
||||
|
||||
if not is_streaming_request:
|
||||
# Run the agent in non-streaming mode
|
||||
if isinstance(self._agent, RawAgent):
|
||||
raw_agent = cast("RawAgent[Any]", self._agent) # type: ignore[redundant-cast] # pyright: ignore[reportUnknownMemberType]
|
||||
response = await raw_agent.run(messages, stream=False, options=chat_options)
|
||||
else:
|
||||
if are_options_set:
|
||||
logger.warning("Agent doesn't support runtime options. They will be ignored.")
|
||||
response = await self._agent.run(messages, stream=False)
|
||||
response = await self._agent.run(stream=False, **run_kwargs) # type: ignore[reportUnknownMemberType]
|
||||
|
||||
for message in response.messages:
|
||||
for content in message.contents:
|
||||
@@ -228,20 +221,12 @@ class ResponsesHostServer(ResponsesAgentServerHost):
|
||||
yield response_event_stream.emit_completed()
|
||||
return
|
||||
|
||||
# Run the agent in streaming mode
|
||||
if isinstance(self._agent, RawAgent):
|
||||
raw_agent = cast("RawAgent[Any]", self._agent) # type: ignore[redundant-cast] # pyright: ignore[reportUnknownMemberType]
|
||||
response_stream = raw_agent.run(messages, stream=True, options=chat_options)
|
||||
else:
|
||||
if are_options_set:
|
||||
logger.warning("Agent doesn't support runtime options. They will be ignored.")
|
||||
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(response_event_stream)
|
||||
|
||||
async for update in response_stream:
|
||||
# Run the agent in streaming mode
|
||||
async for update in self._agent.run(stream=True, **run_kwargs): # type: ignore[reportUnknownMemberType]
|
||||
for content in update.contents:
|
||||
for event in tracker.handle(content):
|
||||
yield event
|
||||
@@ -256,7 +241,7 @@ class ResponsesHostServer(ResponsesAgentServerHost):
|
||||
|
||||
yield response_event_stream.emit_completed()
|
||||
|
||||
async def _handle_workflow_agent(
|
||||
async def _handle_inner_workflow(
|
||||
self,
|
||||
request: CreateResponse,
|
||||
context: ResponseContext,
|
||||
@@ -269,8 +254,7 @@ class ResponsesHostServer(ResponsesAgentServerHost):
|
||||
"""
|
||||
input_items = await context.get_input_items()
|
||||
input_messages = _items_to_messages(input_items)
|
||||
|
||||
is_streaming_request = self._is_streaming_request(request)
|
||||
is_streaming_request = request.stream is not None and request.stream is True
|
||||
|
||||
_, are_options_set = _to_chat_options(request)
|
||||
if are_options_set:
|
||||
@@ -311,7 +295,8 @@ class ResponsesHostServer(ResponsesAgentServerHost):
|
||||
response_event_stream = ResponseEventStream(response_id=context.response_id, model=request.model)
|
||||
|
||||
# Create a new checkpoint storage for this response based on the following rules:
|
||||
# - If no previous response ID or conversation ID is provided, create a new checkpoint storage for this response
|
||||
# - If no previous response ID or conversation ID is provided,
|
||||
# create a new checkpoint storage for this response
|
||||
# - If a previous response ID is provided, create a new checkpoint storage for this response
|
||||
# - If a conversation ID is provided, reuse the existing checkpoint storage for the conversation
|
||||
context_id = context.conversation_id or context.response_id
|
||||
@@ -333,14 +318,12 @@ class ResponsesHostServer(ResponsesAgentServerHost):
|
||||
yield response_event_stream.emit_completed()
|
||||
return
|
||||
|
||||
# Run the agent in streaming mode
|
||||
response_stream = self._agent.run(input_messages, stream=True, checkpoint_storage=checkpoint_storage)
|
||||
|
||||
# Track the current active output item builder for streaming;
|
||||
# lazily created on matching content, closed when a different type arrives.
|
||||
tracker = _OutputItemTracker(response_event_stream)
|
||||
|
||||
async for update in response_stream:
|
||||
# Run the workflow agent in streaming mode
|
||||
async for update in self._agent.run(input_messages, stream=True, checkpoint_storage=checkpoint_storage):
|
||||
for content in update.contents:
|
||||
for event in tracker.handle(content):
|
||||
yield event
|
||||
@@ -355,7 +338,6 @@ class ResponsesHostServer(ResponsesAgentServerHost):
|
||||
|
||||
await self._delete_not_latest_checkpoints(checkpoint_storage, self._agent.workflow.name)
|
||||
yield response_event_stream.emit_completed()
|
||||
return
|
||||
|
||||
@staticmethod
|
||||
async def _delete_not_latest_checkpoints(checkpoint_storage: FileCheckpointStorage, workflow_name: str) -> None:
|
||||
|
||||
@@ -41,9 +41,10 @@ def _make_agent(
|
||||
*,
|
||||
response: AgentResponse | None = None,
|
||||
stream_updates: list[AgentResponseUpdate] | None = None,
|
||||
raw_agent: bool = True,
|
||||
) -> MagicMock:
|
||||
"""Create a mock agent implementing SupportsAgentRun."""
|
||||
agent = MagicMock(spec=RawAgent)
|
||||
agent = MagicMock(spec=RawAgent) if raw_agent else MagicMock()
|
||||
agent.id = "test-agent"
|
||||
agent.name = "Test Agent"
|
||||
agent.description = "A mock agent for testing"
|
||||
@@ -267,10 +268,18 @@ class TestNonStreaming:
|
||||
|
||||
async def test_chat_options_forwarded(self) -> None:
|
||||
agent = _make_agent(
|
||||
response=AgentResponse(messages=[Message(role="assistant", contents=[Content.from_text("ok")])])
|
||||
response=AgentResponse(messages=[Message(role="assistant", contents=[Content.from_text("ok")])]),
|
||||
raw_agent=True,
|
||||
)
|
||||
server = _make_server(agent)
|
||||
resp = await _post(server, stream=False, temperature=0.5, top_p=0.9, max_output_tokens=1024)
|
||||
resp = await _post(
|
||||
server,
|
||||
stream=False,
|
||||
temperature=0.5,
|
||||
top_p=0.9,
|
||||
max_output_tokens=1024,
|
||||
parallel_tool_calls=True,
|
||||
)
|
||||
|
||||
assert resp.status_code == 200
|
||||
agent.run.assert_awaited_once()
|
||||
@@ -280,6 +289,7 @@ class TestNonStreaming:
|
||||
assert options["temperature"] == 0.5
|
||||
assert options["top_p"] == 0.9
|
||||
assert options["max_tokens"] == 1024
|
||||
assert options["allow_multiple_tool_calls"] is True
|
||||
|
||||
|
||||
# endregion
|
||||
@@ -289,6 +299,31 @@ class TestNonStreaming:
|
||||
|
||||
|
||||
class TestStreaming:
|
||||
async def test_chat_options_forwarded(self) -> None:
|
||||
agent = _make_agent(
|
||||
stream_updates=[AgentResponseUpdate(contents=[Content.from_text("ok")], role="assistant")],
|
||||
raw_agent=True,
|
||||
)
|
||||
server = _make_server(agent)
|
||||
resp = await _post(
|
||||
server,
|
||||
stream=True,
|
||||
temperature=0.5,
|
||||
top_p=0.9,
|
||||
max_output_tokens=1024,
|
||||
parallel_tool_calls=True,
|
||||
)
|
||||
|
||||
assert resp.status_code == 200
|
||||
agent.run.assert_called_once()
|
||||
call_kwargs = agent.run.call_args.kwargs
|
||||
assert call_kwargs["stream"] is True
|
||||
options = call_kwargs["options"]
|
||||
assert options["temperature"] == 0.5
|
||||
assert options["top_p"] == 0.9
|
||||
assert options["max_tokens"] == 1024
|
||||
assert options["allow_multiple_tool_calls"] is True
|
||||
|
||||
async def test_basic_text_streaming(self) -> None:
|
||||
agent = _make_agent(
|
||||
stream_updates=[
|
||||
@@ -1426,7 +1461,7 @@ class TestMultiTurnMixedContent:
|
||||
assert body["status"] == "completed"
|
||||
|
||||
# Verify agent received text + image
|
||||
messages = agent.run.call_args.args[0]
|
||||
messages = agent.run.call_args.kwargs["messages"]
|
||||
assert len(messages) == 1
|
||||
assert messages[0].role == "user"
|
||||
assert len(messages[0].contents) == 2
|
||||
@@ -1464,7 +1499,7 @@ class TestMultiTurnMixedContent:
|
||||
body = resp.json()
|
||||
assert body["status"] == "completed"
|
||||
|
||||
messages = agent.run.call_args.args[0]
|
||||
messages = agent.run.call_args.kwargs["messages"]
|
||||
assert len(messages) == 1
|
||||
assert len(messages[0].contents) == 2
|
||||
assert messages[0].contents[0].type == "text"
|
||||
@@ -1501,7 +1536,7 @@ class TestMultiTurnMixedContent:
|
||||
body = resp.json()
|
||||
assert body["status"] == "completed"
|
||||
|
||||
messages = agent.run.call_args.args[0]
|
||||
messages = agent.run.call_args.kwargs["messages"]
|
||||
assert len(messages) == 1
|
||||
assert len(messages[0].contents) == 2
|
||||
assert messages[0].contents[0].type == "text"
|
||||
@@ -1542,7 +1577,7 @@ class TestMultiTurnMixedContent:
|
||||
body = resp.json()
|
||||
assert body["status"] == "completed"
|
||||
|
||||
messages = agent.run.call_args.args[0]
|
||||
messages = agent.run.call_args.kwargs["messages"]
|
||||
assert len(messages) == 3
|
||||
assert messages[0].role == "user"
|
||||
assert messages[0].contents[0].type == "text"
|
||||
@@ -1591,7 +1626,7 @@ class TestMultiTurnMixedContent:
|
||||
assert body2["status"] == "completed"
|
||||
|
||||
# Verify second call receives history from turn 1 + text+image input
|
||||
second_call_messages = agent.run.call_args_list[1].args[0]
|
||||
second_call_messages = agent.run.call_args_list[1].kwargs["messages"]
|
||||
# History: output message from turn 1 ("Send me an image")
|
||||
# Input: message with text + image
|
||||
assert len(second_call_messages) >= 2
|
||||
@@ -1652,7 +1687,7 @@ class TestMultiTurnMixedContent:
|
||||
assert resp2.json()["status"] == "completed"
|
||||
|
||||
# Verify turn 2 received history including function call/result
|
||||
second_call_messages = agent.run.call_args_list[1].args[0]
|
||||
second_call_messages = agent.run.call_args_list[1].kwargs["messages"]
|
||||
roles = [m.role for m in second_call_messages]
|
||||
assert "assistant" in roles
|
||||
assert "tool" in roles
|
||||
@@ -1703,7 +1738,7 @@ class TestMultiTurnMixedContent:
|
||||
assert resp2.json()["status"] == "completed"
|
||||
|
||||
# Verify history includes the reasoning and text from turn 1
|
||||
second_call_messages = agent.run.call_args_list[1].args[0]
|
||||
second_call_messages = agent.run.call_args_list[1].kwargs["messages"]
|
||||
assert len(second_call_messages) >= 2 # history + new input
|
||||
|
||||
async def test_multi_turn_with_mixed_content_and_streaming(self) -> None:
|
||||
@@ -1795,7 +1830,7 @@ class TestMultiTurnMixedContent:
|
||||
body = resp.json()
|
||||
assert body["status"] == "completed"
|
||||
|
||||
messages = agent.run.call_args.args[0]
|
||||
messages = agent.run.call_args.kwargs["messages"]
|
||||
assert len(messages) == 2
|
||||
assert messages[0].role == "user"
|
||||
assert messages[0].contents[0].type == "text"
|
||||
@@ -1867,7 +1902,7 @@ class TestMultiTurnMixedContent:
|
||||
assert resp3.json()["status"] == "completed"
|
||||
|
||||
# Verify turn 3 received full history from turns 1+2 plus new image input
|
||||
third_call_messages = agent.run.call_args_list[2].args[0]
|
||||
third_call_messages = agent.run.call_args_list[2].kwargs["messages"]
|
||||
# Should have: history from turn 1 (assistant text) + history from turn 2
|
||||
# (function_call, function_call_output, text) + new input (text + image)
|
||||
assert len(third_call_messages) >= 5
|
||||
@@ -1918,7 +1953,7 @@ class TestMultiTurnMixedContent:
|
||||
body = resp.json()
|
||||
assert body["status"] == "completed"
|
||||
|
||||
messages = agent.run.call_args.args[0]
|
||||
messages = agent.run.call_args.kwargs["messages"]
|
||||
assert len(messages) == 1
|
||||
assert len(messages[0].contents) == 2
|
||||
assert messages[0].contents[0].type == "text"
|
||||
@@ -1982,7 +2017,7 @@ class TestMultiTurnMixedContent:
|
||||
assert resp2.json()["status"] == "completed"
|
||||
|
||||
# Verify turn 2 received history from turn 1 + new text+file input
|
||||
second_call_messages = agent.run.call_args_list[1].args[0]
|
||||
second_call_messages = agent.run.call_args_list[1].kwargs["messages"]
|
||||
assert len(second_call_messages) >= 2
|
||||
|
||||
# History should include the assistant response from turn 1
|
||||
@@ -2050,7 +2085,7 @@ class TestMultiTurnMixedContent:
|
||||
assert resp2.json()["status"] == "completed"
|
||||
|
||||
# Verify turn 2 received history with function call + new text+image
|
||||
second_call_messages = agent.run.call_args_list[1].args[0]
|
||||
second_call_messages = agent.run.call_args_list[1].kwargs["messages"]
|
||||
# History should contain function_call and function_result from turn 1
|
||||
fc_contents = [
|
||||
c for m in second_call_messages if m.role == "assistant" for c in m.contents if c.type == "function_call"
|
||||
|
||||
Reference in New Issue
Block a user