mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: Complete durableagent package (#3058)
* Add worker and clients * Clean code and refactor common code * Implement sample * Add sample * Update readmes * Fix tests * Fix tests * Update requirements * Fix typo * Address comments * use response.text
This commit is contained in:
committed by
GitHub
Unverified
parent
a5b36dc379
commit
e3eff65a6b
+2
-7
@@ -10,6 +10,7 @@ Prerequisites: configure `AZURE_OPENAI_ENDPOINT`, `AZURE_OPENAI_CHAT_DEPLOYMENT_
|
||||
|
||||
import json
|
||||
import logging
|
||||
from collections.abc import Generator
|
||||
from typing import Any
|
||||
|
||||
import azure.functions as func
|
||||
@@ -44,7 +45,7 @@ app = AgentFunctionApp(agents=[_create_writer_agent()], enable_health_check=True
|
||||
|
||||
# 4. Orchestration that runs the agent sequentially on a shared thread for chaining behaviour.
|
||||
@app.orchestration_trigger(context_name="context")
|
||||
def single_agent_orchestration(context: DurableOrchestrationContext):
|
||||
def single_agent_orchestration(context: DurableOrchestrationContext) -> Generator[Any, Any, str]:
|
||||
"""Run the writer agent twice on the same thread to mirror chaining behaviour."""
|
||||
|
||||
writer = app.get_agent(context, WRITER_AGENT_NAME)
|
||||
@@ -116,12 +117,6 @@ async def get_orchestration_status(
|
||||
)
|
||||
|
||||
status = await client.get_status(instance_id)
|
||||
if status is None:
|
||||
return func.HttpResponse(
|
||||
body=json.dumps({"error": "Instance not found"}),
|
||||
status_code=404,
|
||||
mimetype="application/json",
|
||||
)
|
||||
|
||||
response_data: dict[str, Any] = {
|
||||
"instanceId": status.instance_id,
|
||||
|
||||
+2
-7
@@ -10,6 +10,7 @@ Prerequisites: configure `AZURE_OPENAI_ENDPOINT`, `AZURE_OPENAI_CHAT_DEPLOYMENT_
|
||||
|
||||
import json
|
||||
import logging
|
||||
from collections.abc import Generator
|
||||
from typing import Any, cast
|
||||
|
||||
from agent_framework import AgentRunResponse
|
||||
@@ -51,7 +52,7 @@ app.add_agent(agents[1])
|
||||
|
||||
# 4. Durable Functions orchestration that runs both agents in parallel.
|
||||
@app.orchestration_trigger(context_name="context")
|
||||
def multi_agent_concurrent_orchestration(context: DurableOrchestrationContext):
|
||||
def multi_agent_concurrent_orchestration(context: DurableOrchestrationContext) -> Generator[Any, Any, dict[str, str]]:
|
||||
"""Fan out to two domain-specific agents and aggregate their responses."""
|
||||
|
||||
prompt = context.get_input()
|
||||
@@ -137,12 +138,6 @@ async def get_orchestration_status(
|
||||
)
|
||||
|
||||
status = await client.get_status(instance_id)
|
||||
if status is None:
|
||||
return func.HttpResponse(
|
||||
body=json.dumps({"error": "Instance not found"}),
|
||||
status_code=404,
|
||||
mimetype="application/json",
|
||||
)
|
||||
|
||||
response_data: dict[str, Any] = {
|
||||
"instanceId": status.instance_id,
|
||||
|
||||
+4
-10
@@ -11,7 +11,7 @@ Functions host."""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from collections.abc import Mapping
|
||||
from collections.abc import Generator, Mapping
|
||||
from typing import Any, cast
|
||||
|
||||
import azure.functions as func
|
||||
@@ -74,7 +74,7 @@ def send_email(message: str) -> str:
|
||||
|
||||
# 4. Orchestration validates input, runs agents, and branches on spam results.
|
||||
@app.orchestration_trigger(context_name="context")
|
||||
def spam_detection_orchestration(context: DurableOrchestrationContext):
|
||||
def spam_detection_orchestration(context: DurableOrchestrationContext) -> Generator[Any, Any, str]:
|
||||
payload_raw = context.get_input()
|
||||
if not isinstance(payload_raw, Mapping):
|
||||
raise ValueError("Email data is required")
|
||||
@@ -105,7 +105,7 @@ def spam_detection_orchestration(context: DurableOrchestrationContext):
|
||||
spam_result = cast(SpamDetectionResult, spam_result_raw.value)
|
||||
|
||||
if spam_result.is_spam:
|
||||
result = yield context.call_activity("handle_spam_email", spam_result.reason)
|
||||
result = yield context.call_activity("handle_spam_email", spam_result.reason) # type: ignore[misc]
|
||||
return result
|
||||
|
||||
email_thread = email_agent.get_new_thread()
|
||||
@@ -125,7 +125,7 @@ def spam_detection_orchestration(context: DurableOrchestrationContext):
|
||||
|
||||
email_result = cast(EmailResponse, email_result_raw.value)
|
||||
|
||||
result = yield context.call_activity("send_email", email_result.response)
|
||||
result = yield context.call_activity("send_email", email_result.response) # type: ignore[misc]
|
||||
return result
|
||||
|
||||
|
||||
@@ -196,12 +196,6 @@ async def get_orchestration_status(
|
||||
)
|
||||
|
||||
status = await client.get_status(instance_id)
|
||||
if status is None:
|
||||
return func.HttpResponse(
|
||||
body=json.dumps({"error": "Instance not found"}),
|
||||
status_code=404,
|
||||
mimetype="application/json",
|
||||
)
|
||||
|
||||
response_data: dict[str, Any] = {
|
||||
"instanceId": status.instance_id,
|
||||
|
||||
+8
-8
@@ -10,7 +10,7 @@ either `AZURE_OPENAI_API_KEY` or sign in with Azure CLI before running `func sta
|
||||
|
||||
import json
|
||||
import logging
|
||||
from collections.abc import Mapping
|
||||
from collections.abc import Generator, Mapping
|
||||
from datetime import timedelta
|
||||
from typing import Any
|
||||
|
||||
@@ -62,7 +62,7 @@ app = AgentFunctionApp(agents=[_create_writer_agent()], enable_health_check=True
|
||||
|
||||
# 3. Activities encapsulate external work for review notifications and publishing.
|
||||
@app.activity_trigger(input_name="content")
|
||||
def notify_user_for_approval(content: dict) -> None:
|
||||
def notify_user_for_approval(content: Any) -> None:
|
||||
model = GeneratedContent.model_validate(content)
|
||||
logger.info("NOTIFICATION: Please review the following content for approval:")
|
||||
logger.info("Title: %s", model.title or "(untitled)")
|
||||
@@ -71,7 +71,7 @@ def notify_user_for_approval(content: dict) -> None:
|
||||
|
||||
|
||||
@app.activity_trigger(input_name="content")
|
||||
def publish_content(content: dict) -> None:
|
||||
def publish_content(content: Any) -> None:
|
||||
model = GeneratedContent.model_validate(content)
|
||||
logger.info("PUBLISHING: Content has been published successfully:")
|
||||
logger.info("Title: %s", model.title or "(untitled)")
|
||||
@@ -80,7 +80,7 @@ def publish_content(content: dict) -> None:
|
||||
|
||||
# 4. Orchestration loops until the human approves, times out, or attempts are exhausted.
|
||||
@app.orchestration_trigger(context_name="context")
|
||||
def content_generation_hitl_orchestration(context: DurableOrchestrationContext):
|
||||
def content_generation_hitl_orchestration(context: DurableOrchestrationContext) -> Generator[Any, Any, dict[str, str]]:
|
||||
payload_raw = context.get_input()
|
||||
if not isinstance(payload_raw, Mapping):
|
||||
raise ValueError("Content generation input is required")
|
||||
@@ -102,7 +102,7 @@ def content_generation_hitl_orchestration(context: DurableOrchestrationContext):
|
||||
)
|
||||
|
||||
content = initial_raw.value
|
||||
logger.info("Type of content after extraction: %s", type(content))
|
||||
logger.info("Type of content after extraction: %s", type(content)) # type: ignore[misc]
|
||||
|
||||
if content is None or not isinstance(content, GeneratedContent):
|
||||
raise ValueError("Agent returned no content after extraction.")
|
||||
@@ -114,7 +114,7 @@ def content_generation_hitl_orchestration(context: DurableOrchestrationContext):
|
||||
f"Requesting human feedback. Iteration #{attempt}. Timeout: {payload.approval_timeout_hours} hour(s)."
|
||||
)
|
||||
|
||||
yield context.call_activity("notify_user_for_approval", content.model_dump())
|
||||
yield context.call_activity("notify_user_for_approval", content.model_dump()) # type: ignore[misc]
|
||||
|
||||
approval_task = context.wait_for_external_event(HUMAN_APPROVAL_EVENT)
|
||||
timeout_task = context.create_timer(
|
||||
@@ -129,7 +129,7 @@ def content_generation_hitl_orchestration(context: DurableOrchestrationContext):
|
||||
|
||||
if approval_payload.approved:
|
||||
context.set_custom_status("Content approved by human reviewer. Publishing content...")
|
||||
yield context.call_activity("publish_content", content.model_dump())
|
||||
yield context.call_activity("publish_content", content.model_dump()) # type: ignore[misc]
|
||||
context.set_custom_status(
|
||||
f"Content published successfully at {context.current_utc_datetime:%Y-%m-%dT%H:%M:%S}"
|
||||
)
|
||||
@@ -287,7 +287,7 @@ async def get_orchestration_status(
|
||||
)
|
||||
|
||||
# Check if status is None or if the instance doesn't exist (runtime_status is None)
|
||||
if status is None or getattr(status, "runtime_status", None) is None:
|
||||
if getattr(status, "runtime_status", None) is None:
|
||||
return func.HttpResponse(
|
||||
body=json.dumps({"error": "Instance not found."}),
|
||||
status_code=404,
|
||||
|
||||
Reference in New Issue
Block a user