Python: (samples): adopt AzureOpenAIResponsesClient, reorganize orchestration examples, and fix workflow/orchestration bugs (#3873)

* adopt AzureOpenAIResponsesClient, reorganize orchestration examples, and fix workflow/orchestration bugs

* Updates

* add comment
This commit is contained in:
Evan Mattson
2026-02-12 19:46:58 +09:00
committed by GitHub
Unverified
parent 8457533c69
commit 1b10b051fd
73 changed files with 1612 additions and 686 deletions
@@ -21,6 +21,7 @@ existing observability and streaming semantics continue to apply.
from __future__ import annotations
import inspect
import json
import logging
import sys
from collections import OrderedDict
@@ -393,6 +394,76 @@ class AgentBasedGroupChatOrchestrator(BaseGroupChatOrchestrator):
)
self._increment_round()
@staticmethod
def _parse_last_json_object(text: str) -> AgentOrchestrationOutput | None:
"""Best-effort parser for concatenated JSON and return the last object.
Stop-gap workaround:
In some runs, the orchestrator manager text can contain multiple JSON objects
concatenated back-to-back (for example: `{...}{...}`), which causes
`model_validate_json` to fail with trailing characters. Until the root cause
is fully understood and fixed, decode sequential top-level JSON values and
validate the last one.
"""
decoder = json.JSONDecoder()
index = 0
parsed: Any | None = None
while index < len(text):
while index < len(text) and text[index].isspace():
index += 1
if index >= len(text):
break
parsed, index = decoder.raw_decode(text, index)
if parsed is None:
return None
return AgentOrchestrationOutput.model_validate(parsed)
@classmethod
def _parse_agent_output(cls, agent_response: Any) -> AgentOrchestrationOutput:
"""Parse manager output with defensive fallbacks.
Preferred path is structured output (`agent_response.value`) when available.
If only text is available, first attempt strict JSON parsing and then apply a
temporary concatenated-JSON fallback as a stop-gap.
"""
try:
structured_value = agent_response.value
except Exception:
structured_value = None
if structured_value is not None:
return AgentOrchestrationOutput.model_validate(structured_value)
text_candidates: list[str] = []
for message in reversed(agent_response.messages):
if message.role == "assistant" and message.text.strip():
text_candidates.append(message.text.strip())
break
response_text = agent_response.text.strip()
if response_text and response_text not in text_candidates:
text_candidates.append(response_text)
last_error: Exception | None = None
for candidate in text_candidates:
try:
return AgentOrchestrationOutput.model_validate_json(candidate)
except Exception as ex:
last_error = ex
try:
# Stop-gap fallback for rare cases where multiple JSON objects are
# returned in one text payload (concatenated with no separator).
parsed = cls._parse_last_json_object(candidate)
if parsed is not None:
return parsed
except Exception as ex:
last_error = ex
raise ValueError("Failed to parse agent orchestration output.") from last_error
async def _invoke_agent(self) -> AgentOrchestrationOutput:
"""Invoke the orchestrator agent to determine the next speaker and termination."""
@@ -404,7 +475,7 @@ class AgentBasedGroupChatOrchestrator(BaseGroupChatOrchestrator):
options={"response_format": AgentOrchestrationOutput},
)
# Parse and validate the structured output
agent_orchestration_output = AgentOrchestrationOutput.model_validate_json(agent_response.text)
agent_orchestration_output = self._parse_agent_output(agent_response)
if not agent_orchestration_output.terminate and not agent_orchestration_output.next_speaker:
raise ValueError("next_speaker must be provided if not terminating the conversation.")
@@ -121,6 +121,51 @@ class StubManagerAgent(Agent):
)
class ConcatenatedJsonManagerAgent(Agent):
"""Manager agent that emits concatenated JSON in a single assistant message."""
def __init__(self) -> None:
super().__init__(client=MockChatClient(), name="concat_manager", description="Concatenated JSON manager")
self._call_count = 0
async def run(
self,
messages: str | Message | Sequence[str | Message] | None = None,
*,
thread: AgentThread | None = None,
**kwargs: Any,
) -> AgentResponse:
if self._call_count == 0:
self._call_count += 1
return AgentResponse(
messages=[
Message(
role="assistant",
text=(
'{"terminate": false, "reason": "invalid candidate", '
'"next_speaker": "unknown", "final_message": null} '
'{"terminate": false, "reason": "pick known participant", '
'"next_speaker": "agent", "final_message": null}'
),
author_name=self.name,
)
]
)
return AgentResponse(
messages=[
Message(
role="assistant",
text=(
'{"terminate": true, "reason": "Task complete", '
'"next_speaker": null, "final_message": "concatenated manager final"}'
),
author_name=self.name,
)
]
)
def make_sequence_selector() -> Callable[[GroupChatState], str]:
state_counter = {"value": 0}
@@ -221,6 +266,29 @@ async def test_group_chat_as_agent_accepts_conversation() -> None:
assert response.messages, "Expected agent conversation output"
async def test_agent_manager_handles_concatenated_json_output() -> None:
manager = ConcatenatedJsonManagerAgent()
worker = StubAgent("agent", "worker response")
workflow = GroupChatBuilder(
participants=[worker],
orchestrator_agent=manager,
).build()
outputs: list[list[Message]] = []
async for event in workflow.run("coordinate task", stream=True):
if event.type == "output":
data = event.data
if isinstance(data, list):
outputs.append(cast(list[Message], data))
assert outputs
conversation = outputs[-1]
assert any(msg.author_name == "agent" and msg.text == "worker response" for msg in conversation)
assert conversation[-1].author_name == manager.name
assert conversation[-1].text == "concatenated manager final"
# Comprehensive tests for group chat functionality
@@ -366,6 +366,11 @@ async def test_magentic_checkpoint_resume_round_trip():
assert checkpoints
checkpoints.sort(key=lambda cp: cp.timestamp)
resume_checkpoint = checkpoints[-1]
loaded_checkpoint = await storage.load(resume_checkpoint.checkpoint_id)
assert loaded_checkpoint is not None
# Regression check: checkpoints with pending request_info must include executor state.
assert "_executor_state" in loaded_checkpoint.state
assert "magentic_orchestrator" in loaded_checkpoint.state["_executor_state"]
manager2 = FakeManager()
wf_resume = MagenticBuilder(
@@ -378,7 +383,7 @@ async def test_magentic_checkpoint_resume_round_trip():
completed: WorkflowEvent | None = None
req_event = None
async for event in wf_resume.run(
resume_checkpoint.checkpoint_id,
checkpoint_id=resume_checkpoint.checkpoint_id,
stream=True,
):
if event.type == "request_info" and event.request_type is MagenticPlanReviewRequest: