mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: [Breaking] Python: Respond with AgentRunResponse with serialized structured output (#2285)
* Respond with AgentRunResponse * Fix response_Format type * Address comments * Fix tests * Fix log * Addressed comments * Code cleanup * Use AgentTask vs Generator * Address comments * use lazy logging * fix mypy errors
This commit is contained in:
committed by
GitHub
Unverified
parent
0f2c5e6cb8
commit
306c81aef8
+2
-2
@@ -57,7 +57,7 @@ def single_agent_orchestration(context: DurableOrchestrationContext):
|
||||
|
||||
improved_prompt = (
|
||||
"Improve this further while keeping it under 25 words: "
|
||||
f"{initial.get('response', '').strip()}"
|
||||
f"{initial.text}"
|
||||
)
|
||||
|
||||
refined = yield writer.run(
|
||||
@@ -65,7 +65,7 @@ def single_agent_orchestration(context: DurableOrchestrationContext):
|
||||
thread=writer_thread,
|
||||
)
|
||||
|
||||
return refined.get("response", "")
|
||||
return refined.text
|
||||
|
||||
|
||||
# 5. HTTP endpoint to kick off the orchestration and return the status query URI.
|
||||
|
||||
+10
-4
@@ -10,8 +10,9 @@ Prerequisites: configure `AZURE_OPENAI_ENDPOINT`, `AZURE_OPENAI_CHAT_DEPLOYMENT_
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import Any
|
||||
from typing import Any, cast
|
||||
|
||||
from agent_framework import AgentRunResponse
|
||||
import azure.functions as func
|
||||
from agent_framework.azure import AgentFunctionApp, AzureOpenAIChatClient
|
||||
from azure.durable_functions import DurableOrchestrationClient, DurableOrchestrationContext
|
||||
@@ -63,14 +64,19 @@ def multi_agent_concurrent_orchestration(context: DurableOrchestrationContext):
|
||||
physicist_thread = physicist.get_new_thread()
|
||||
chemist_thread = chemist.get_new_thread()
|
||||
|
||||
# Create tasks from agent.run() calls
|
||||
physicist_task = physicist.run(messages=str(prompt), thread=physicist_thread)
|
||||
chemist_task = chemist.run(messages=str(prompt), thread=chemist_thread)
|
||||
|
||||
results = yield context.task_all([physicist_task, chemist_task])
|
||||
# Execute both tasks concurrently using task_all
|
||||
task_results = yield context.task_all([physicist_task, chemist_task])
|
||||
|
||||
physicist_result = cast(AgentRunResponse, task_results[0])
|
||||
chemist_result = cast(AgentRunResponse, task_results[1])
|
||||
|
||||
return {
|
||||
"physicist": results[0].get("response", ""),
|
||||
"chemist": results[1].get("response", ""),
|
||||
"physicist": physicist_result.text,
|
||||
"chemist": chemist_result.text,
|
||||
}
|
||||
|
||||
|
||||
|
||||
+2
-20
@@ -102,7 +102,7 @@ def spam_detection_orchestration(context: DurableOrchestrationContext):
|
||||
response_format=SpamDetectionResult,
|
||||
)
|
||||
|
||||
spam_result = cast(SpamDetectionResult, _coerce_structured(spam_result_raw, SpamDetectionResult))
|
||||
spam_result = cast(SpamDetectionResult, spam_result_raw.value)
|
||||
|
||||
if spam_result.is_spam:
|
||||
result = yield context.call_activity("handle_spam_email", spam_result.reason)
|
||||
@@ -123,7 +123,7 @@ def spam_detection_orchestration(context: DurableOrchestrationContext):
|
||||
response_format=EmailResponse,
|
||||
)
|
||||
|
||||
email_result = cast(EmailResponse, _coerce_structured(email_result_raw, EmailResponse))
|
||||
email_result = cast(EmailResponse, email_result_raw.value)
|
||||
|
||||
result = yield context.call_activity("send_email", email_result.response)
|
||||
return result
|
||||
@@ -231,24 +231,6 @@ def _build_status_url(request_url: str, instance_id: str, *, route: str) -> str:
|
||||
return f"{base_url}/api/{route}/status/{instance_id}"
|
||||
|
||||
|
||||
def _coerce_structured(result: Mapping[str, Any], model: type[BaseModel]) -> BaseModel:
|
||||
structured = result.get("structured_response") if isinstance(result, Mapping) else None
|
||||
if structured is not None:
|
||||
return model.model_validate(structured)
|
||||
|
||||
response_text = result.get("response") if isinstance(result, Mapping) else None
|
||||
if isinstance(response_text, str) and response_text.strip():
|
||||
try:
|
||||
parsed = json.loads(response_text)
|
||||
if isinstance(parsed, Mapping):
|
||||
return model.model_validate(parsed)
|
||||
except json.JSONDecodeError:
|
||||
logger.warning("[ConditionalOrchestration] Failed to parse agent JSON response; raising error.")
|
||||
|
||||
# If parsing failed, raise to surface the issue to the caller.
|
||||
raise ValueError(f"Agent response could not be parsed as {model.__name__}.")
|
||||
|
||||
|
||||
"""
|
||||
Expected response from `POST /api/spamdetection/run`:
|
||||
|
||||
|
||||
+12
-19
@@ -100,7 +100,12 @@ def content_generation_hitl_orchestration(context: DurableOrchestrationContext):
|
||||
thread=writer_thread,
|
||||
response_format=GeneratedContent,
|
||||
)
|
||||
content = _coerce_generated_content(initial_raw)
|
||||
|
||||
content = initial_raw.value
|
||||
logger.info("Type of content after extraction: %s", type(content))
|
||||
|
||||
if content is None or not isinstance(content, GeneratedContent):
|
||||
raise ValueError("Agent returned no content after extraction.")
|
||||
|
||||
attempt = 0
|
||||
while attempt < payload.max_review_attempts:
|
||||
@@ -142,7 +147,12 @@ def content_generation_hitl_orchestration(context: DurableOrchestrationContext):
|
||||
thread=writer_thread,
|
||||
response_format=GeneratedContent,
|
||||
)
|
||||
content = _coerce_generated_content(rewritten_raw)
|
||||
|
||||
rewritten_value = rewritten_raw.value
|
||||
if rewritten_value is None or not isinstance(rewritten_value, GeneratedContent):
|
||||
raise ValueError("Agent returned no content after rewrite.")
|
||||
|
||||
content = rewritten_value
|
||||
else:
|
||||
context.set_custom_status(
|
||||
f"Human approval timed out after {payload.approval_timeout_hours} hour(s). Treating as rejection."
|
||||
@@ -317,23 +327,6 @@ def _build_status_url(request_url: str, instance_id: str, *, route: str) -> str:
|
||||
return f"{base_url}/api/{route}/status/{instance_id}"
|
||||
|
||||
|
||||
def _coerce_generated_content(result: Mapping[str, Any]) -> GeneratedContent:
|
||||
structured = result.get("structured_response") if isinstance(result, Mapping) else None
|
||||
if structured is not None:
|
||||
return GeneratedContent.model_validate(structured)
|
||||
|
||||
response_text = result.get("response") if isinstance(result, Mapping) else None
|
||||
if isinstance(response_text, str) and response_text.strip():
|
||||
try:
|
||||
parsed = json.loads(response_text)
|
||||
if isinstance(parsed, Mapping):
|
||||
return GeneratedContent.model_validate(parsed)
|
||||
except json.JSONDecodeError:
|
||||
logger.warning("[HITL] Failed to parse agent JSON response; falling back to defaults.")
|
||||
|
||||
raise ValueError("Agent response could not be parsed as GeneratedContent.")
|
||||
|
||||
|
||||
def _parse_human_approval(raw: Any) -> HumanApproval:
|
||||
if isinstance(raw, Mapping):
|
||||
return HumanApproval.model_validate(raw)
|
||||
|
||||
Reference in New Issue
Block a user