chore: import upstream snapshot with attribution
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import importlib.metadata
|
||||
|
||||
from ._app import AgentFunctionApp
|
||||
|
||||
try:
|
||||
__version__ = importlib.metadata.version(__name__)
|
||||
except importlib.metadata.PackageNotFoundError:
|
||||
__version__ = "0.0.0" # Fallback for development mode
|
||||
|
||||
__all__ = [
|
||||
"AgentFunctionApp",
|
||||
"__version__",
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,181 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
"""Runner context for Azure Functions activity execution.
|
||||
|
||||
This module provides the CapturingRunnerContext class that captures messages
|
||||
and events produced during executor execution within Azure Functions activities.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from copy import copy
|
||||
from typing import Any
|
||||
|
||||
from agent_framework import (
|
||||
CheckpointStorage,
|
||||
RunnerContext,
|
||||
WorkflowCheckpoint,
|
||||
WorkflowEvent,
|
||||
WorkflowMessage,
|
||||
)
|
||||
from agent_framework._workflows._runner_context import YieldOutputClassifier, YieldOutputEventType
|
||||
from agent_framework._workflows._state import State
|
||||
|
||||
|
||||
class CapturingRunnerContext(RunnerContext):
|
||||
"""A RunnerContext implementation that captures messages and events for Azure Functions activities.
|
||||
|
||||
This context is designed for executing standard Executors within Azure Functions activities.
|
||||
It captures all messages and events produced during execution without requiring durable
|
||||
entity storage, allowing the results to be returned to the orchestrator.
|
||||
|
||||
Unlike InProcRunnerContext, this implementation does NOT support checkpointing
|
||||
(always returns False for has_checkpointing). The orchestrator manages state
|
||||
coordination; this context just captures execution output.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the capturing runner context."""
|
||||
self._messages: dict[str, list[WorkflowMessage]] = {}
|
||||
self._event_queue: asyncio.Queue[WorkflowEvent] = asyncio.Queue()
|
||||
self._pending_request_info_events: dict[str, WorkflowEvent[Any]] = {}
|
||||
self._workflow_id: str | None = None
|
||||
self._streaming: bool = False
|
||||
self._yield_output_classifier: YieldOutputClassifier = lambda _executor_id: "output"
|
||||
|
||||
# region Messaging
|
||||
|
||||
async def send_message(self, message: WorkflowMessage) -> None:
|
||||
"""Capture a message sent by an executor."""
|
||||
self._messages.setdefault(message.source_id, [])
|
||||
self._messages[message.source_id].append(message)
|
||||
|
||||
async def drain_messages(self) -> dict[str, list[WorkflowMessage]]:
|
||||
"""Drain and return all captured messages."""
|
||||
messages = copy(self._messages)
|
||||
self._messages.clear()
|
||||
return messages
|
||||
|
||||
async def has_messages(self) -> bool:
|
||||
"""Check if there are any captured messages."""
|
||||
return bool(self._messages)
|
||||
|
||||
# endregion Messaging
|
||||
|
||||
# region Events
|
||||
|
||||
async def add_event(self, event: WorkflowEvent) -> None:
|
||||
"""Capture an event produced during execution."""
|
||||
await self._event_queue.put(event)
|
||||
|
||||
async def drain_events(self) -> list[WorkflowEvent]:
|
||||
"""Drain all currently queued events without blocking."""
|
||||
events: list[WorkflowEvent] = []
|
||||
while True:
|
||||
try:
|
||||
events.append(self._event_queue.get_nowait())
|
||||
except asyncio.QueueEmpty:
|
||||
break
|
||||
return events
|
||||
|
||||
async def has_events(self) -> bool:
|
||||
"""Check if there are any queued events."""
|
||||
return not self._event_queue.empty()
|
||||
|
||||
async def next_event(self) -> WorkflowEvent:
|
||||
"""Wait for and return the next event."""
|
||||
return await self._event_queue.get()
|
||||
|
||||
# endregion Events
|
||||
|
||||
# region Checkpointing (not supported in activity context)
|
||||
|
||||
def has_checkpointing(self) -> bool:
|
||||
"""Checkpointing is not supported in activity context."""
|
||||
return False
|
||||
|
||||
def set_runtime_checkpoint_storage(self, storage: CheckpointStorage) -> None:
|
||||
"""No-op: checkpointing not supported in activity context."""
|
||||
pass
|
||||
|
||||
def clear_runtime_checkpoint_storage(self) -> None:
|
||||
"""No-op: checkpointing not supported in activity context."""
|
||||
pass
|
||||
|
||||
async def create_checkpoint(
|
||||
self,
|
||||
workflow_name: str,
|
||||
graph_signature_hash: str,
|
||||
state: State,
|
||||
previous_checkpoint_id: str | None,
|
||||
iteration_count: int,
|
||||
metadata: dict[str, Any] | None = None,
|
||||
) -> str:
|
||||
"""Checkpointing not supported in activity context."""
|
||||
raise NotImplementedError("Checkpointing is not supported in Azure Functions activity context")
|
||||
|
||||
async def load_checkpoint(self, checkpoint_id: str) -> WorkflowCheckpoint | None:
|
||||
"""Checkpointing not supported in activity context."""
|
||||
raise NotImplementedError("Checkpointing is not supported in Azure Functions activity context")
|
||||
|
||||
async def apply_checkpoint(self, checkpoint: WorkflowCheckpoint) -> None:
|
||||
"""Checkpointing not supported in activity context."""
|
||||
raise NotImplementedError("Checkpointing is not supported in Azure Functions activity context")
|
||||
|
||||
# endregion Checkpointing
|
||||
|
||||
# region Workflow Configuration
|
||||
|
||||
def set_workflow_id(self, workflow_id: str) -> None:
|
||||
"""Set the workflow ID."""
|
||||
self._workflow_id = workflow_id
|
||||
|
||||
def reset_for_new_run(self) -> None:
|
||||
"""Reset the context for a new run."""
|
||||
self._messages.clear()
|
||||
self._event_queue = asyncio.Queue()
|
||||
self._pending_request_info_events.clear()
|
||||
self._streaming = False
|
||||
|
||||
def set_streaming(self, streaming: bool) -> None:
|
||||
"""Set streaming mode (not used in activity context)."""
|
||||
self._streaming = streaming
|
||||
|
||||
def is_streaming(self) -> bool:
|
||||
"""Check if streaming mode is enabled (always False in activity context)."""
|
||||
return self._streaming
|
||||
|
||||
def set_yield_output_classifier(self, classifier: YieldOutputClassifier) -> None:
|
||||
"""Set the classifier used by WorkflowContext.yield_output()."""
|
||||
self._yield_output_classifier = classifier
|
||||
|
||||
def classify_yielded_output(self, executor_id: str) -> YieldOutputEventType | None:
|
||||
"""Classify an executor's yield_output payload as output, intermediate, or hidden."""
|
||||
return self._yield_output_classifier(executor_id)
|
||||
|
||||
# endregion Workflow Configuration
|
||||
|
||||
# region Request Info Events
|
||||
|
||||
async def add_request_info_event(self, event: WorkflowEvent[Any]) -> None:
|
||||
"""Add a request_info WorkflowEvent and track it for correlation."""
|
||||
self._pending_request_info_events[event.request_id] = event
|
||||
await self.add_event(event)
|
||||
|
||||
async def send_request_info_response(self, request_id: str, response: Any) -> None:
|
||||
"""Send a response correlated to a pending request.
|
||||
|
||||
Note: This is not supported in activity context since human-in-the-loop
|
||||
scenarios require orchestrator-level coordination.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"send_request_info_response is not supported in Azure Functions activity context. "
|
||||
"Human-in-the-loop scenarios should be handled at the orchestrator level."
|
||||
)
|
||||
|
||||
async def get_pending_request_info_events(self) -> dict[str, WorkflowEvent[Any]]:
|
||||
"""Get the mapping of request IDs to their corresponding request_info events."""
|
||||
return dict(self._pending_request_info_events)
|
||||
|
||||
# endregion Request Info Events
|
||||
@@ -0,0 +1,118 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
"""Durable Entity for Agent Execution.
|
||||
|
||||
This module defines a durable entity that manages agent state and execution.
|
||||
Using entities instead of orchestrations provides better state management and
|
||||
allows for long-running agent conversations.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from collections.abc import Callable
|
||||
from typing import Any, cast
|
||||
|
||||
import azure.durable_functions as df
|
||||
from agent_framework import SupportsAgentRun
|
||||
from agent_framework_durabletask import (
|
||||
AgentEntity,
|
||||
AgentEntityStateProviderMixin,
|
||||
AgentResponseCallbackProtocol,
|
||||
run_agent_coroutine,
|
||||
)
|
||||
|
||||
logger = logging.getLogger("agent_framework.azurefunctions")
|
||||
|
||||
|
||||
class AzureFunctionEntityStateProvider(AgentEntityStateProviderMixin):
|
||||
"""Azure Functions Durable Entity state provider for AgentEntity.
|
||||
|
||||
This class utilizes the Durable Entity context from `azure-functions-durable` package
|
||||
to get and set the state of the agent entity.
|
||||
"""
|
||||
|
||||
def __init__(self, context: df.DurableEntityContext) -> None:
|
||||
self._context = context
|
||||
|
||||
def _get_state_dict(self) -> dict[str, Any]:
|
||||
raw_state = self._context.get_state(lambda: {})
|
||||
if not isinstance(raw_state, dict):
|
||||
return {}
|
||||
return cast(dict[str, Any], raw_state)
|
||||
|
||||
def _set_state_dict(self, state: dict[str, Any]) -> None:
|
||||
self._context.set_state(state)
|
||||
|
||||
def _get_thread_id_from_entity(self) -> str:
|
||||
return str(self._context.entity_key)
|
||||
|
||||
|
||||
def create_agent_entity(
|
||||
agent: SupportsAgentRun,
|
||||
callback: AgentResponseCallbackProtocol | None = None,
|
||||
) -> Callable[[df.DurableEntityContext], None]:
|
||||
"""Factory function to create an agent entity class.
|
||||
|
||||
Args:
|
||||
agent: The Microsoft Agent Framework agent instance (must implement SupportsAgentRun)
|
||||
callback: Optional callback invoked during streaming and final responses
|
||||
|
||||
Returns:
|
||||
Entity function configured with the agent
|
||||
"""
|
||||
|
||||
async def _entity_coroutine(context: df.DurableEntityContext) -> None:
|
||||
"""Async handler that executes the entity operations."""
|
||||
try:
|
||||
logger.debug("[entity_function] Entity triggered")
|
||||
logger.debug("[entity_function] Operation: %s", context.operation_name)
|
||||
|
||||
state_provider = AzureFunctionEntityStateProvider(context)
|
||||
entity = AgentEntity(agent, callback, state_provider=state_provider)
|
||||
|
||||
operation = context.operation_name
|
||||
|
||||
if operation == "run" or operation == "run_agent":
|
||||
input_data: Any = context.get_input()
|
||||
|
||||
request: str | dict[str, Any]
|
||||
if isinstance(input_data, dict) and "message" in input_data:
|
||||
request = cast(dict[str, Any], input_data)
|
||||
else:
|
||||
# Fall back to treating input as message string
|
||||
request = "" if input_data is None else str(cast(object, input_data))
|
||||
|
||||
result = await entity.run(request)
|
||||
context.set_result(result.to_dict())
|
||||
|
||||
elif operation == "reset":
|
||||
entity.reset()
|
||||
context.set_result({"status": "reset"})
|
||||
|
||||
else:
|
||||
logger.error("[entity_function] Unknown operation: %s", operation)
|
||||
context.set_result({"error": f"Unknown operation: {operation}"})
|
||||
|
||||
logger.info("[entity_function] Operation %s completed successfully", operation)
|
||||
|
||||
except Exception as exc:
|
||||
logger.exception("[entity_function] Error executing entity operation %s", exc)
|
||||
context.set_result({"error": str(exc), "status": "error"})
|
||||
|
||||
def entity_function(context: df.DurableEntityContext) -> None:
|
||||
"""Synchronous wrapper invoked by the Durable Functions runtime.
|
||||
|
||||
All agent coroutines run on a single process-wide persistent event loop
|
||||
(see ``run_agent_coroutine``). This keeps async resources created by
|
||||
shared agent clients/credentials bound to a live loop across every
|
||||
invocation, preventing cross-loop hangs when the host dispatches
|
||||
successive entity operations onto different worker threads.
|
||||
"""
|
||||
try:
|
||||
run_agent_coroutine(_entity_coroutine(context))
|
||||
except Exception as exc: # pragma: no cover - defensive logging
|
||||
logger.error("[entity_function] Unexpected error executing entity: %s", exc, exc_info=True)
|
||||
context.set_result({"error": str(exc), "status": "error"})
|
||||
|
||||
return entity_function
|
||||
@@ -0,0 +1,11 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
"""Custom exception types for the durable agent framework."""
|
||||
|
||||
|
||||
class IncomingRequestError(ValueError):
|
||||
"""Raised when an incoming HTTP request cannot be parsed or validated."""
|
||||
|
||||
def __init__(self, message: str, status_code: int = 400) -> None:
|
||||
super().__init__(message)
|
||||
self.status_code = status_code
|
||||
@@ -0,0 +1,222 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
"""Orchestration Support for Durable Agents.
|
||||
|
||||
This module provides support for using agents inside Durable Function orchestrations.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from collections.abc import Callable
|
||||
from typing import TYPE_CHECKING, Any, TypeAlias
|
||||
|
||||
import azure.durable_functions as df
|
||||
from agent_framework import AgentSession
|
||||
from agent_framework_durabletask import (
|
||||
DurableAgentExecutor,
|
||||
RunRequest,
|
||||
ensure_response_format,
|
||||
load_agent_response,
|
||||
)
|
||||
from azure.durable_functions.models import TaskBase
|
||||
from azure.durable_functions.models.actions.NoOpAction import NoOpAction
|
||||
from azure.durable_functions.models.Task import CompoundTask, TaskState
|
||||
from pydantic import BaseModel
|
||||
|
||||
logger = logging.getLogger("agent_framework.azurefunctions")
|
||||
|
||||
CompoundActionConstructor: TypeAlias = Callable[[list[Any]], Any] | None
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from azure.durable_functions import DurableOrchestrationContext
|
||||
|
||||
class _TypedCompoundTask(CompoundTask):
|
||||
_first_error: Any
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
tasks: list[TaskBase],
|
||||
compound_action_constructor: CompoundActionConstructor = None,
|
||||
) -> None: ...
|
||||
|
||||
AgentOrchestrationContextType: TypeAlias = DurableOrchestrationContext
|
||||
else:
|
||||
AgentOrchestrationContextType = Any
|
||||
_TypedCompoundTask = CompoundTask
|
||||
|
||||
|
||||
class PreCompletedTask(TaskBase):
|
||||
"""A simple task that is already completed with a result.
|
||||
|
||||
Used for fire-and-forget mode where we want to return immediately
|
||||
with an acceptance response without waiting for entity processing.
|
||||
"""
|
||||
|
||||
def __init__(self, result: Any):
|
||||
"""Initialize with a completed result.
|
||||
|
||||
Args:
|
||||
result: The result value for this completed task
|
||||
"""
|
||||
# Initialize with a NoOp action since we don't need actual orchestration actions
|
||||
super().__init__(-1, NoOpAction())
|
||||
# Immediately mark as completed with the result
|
||||
self.set_value(is_error=False, value=result)
|
||||
|
||||
|
||||
class AgentTask(_TypedCompoundTask):
|
||||
"""A custom Task that wraps entity calls and provides typed AgentResponse results.
|
||||
|
||||
This task wraps the underlying entity call task and intercepts its completion
|
||||
to convert the raw result into a typed AgentResponse object.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
entity_task: TaskBase,
|
||||
response_format: type[BaseModel] | None,
|
||||
correlation_id: str,
|
||||
):
|
||||
"""Initialize the AgentTask.
|
||||
|
||||
Args:
|
||||
entity_task: The underlying entity call task
|
||||
response_format: Optional Pydantic model for response parsing
|
||||
correlation_id: Correlation ID for logging
|
||||
"""
|
||||
# Set instance variables BEFORE calling super().__init__
|
||||
# because super().__init__ may trigger try_set_value for pre-completed tasks
|
||||
self._response_format = response_format
|
||||
self._correlation_id = correlation_id
|
||||
|
||||
super().__init__([entity_task])
|
||||
|
||||
# Override action_repr to expose the inner task's action directly
|
||||
# This ensures compatibility with ReplaySchema V3 which expects Action objects.
|
||||
self.action_repr = entity_task.action_repr
|
||||
|
||||
# Also copy the task ID to match the entity task's identity
|
||||
self.id = entity_task.id
|
||||
|
||||
def try_set_value(self, child: TaskBase) -> None:
|
||||
"""Transition the AgentTask to a terminal state and set its value to `AgentResponse`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
child : TaskBase
|
||||
The entity call task that just completed
|
||||
"""
|
||||
if child.state is TaskState.SUCCEEDED:
|
||||
# Delegate to parent class for standard completion logic
|
||||
if len(self.pending_tasks) == 0:
|
||||
# Transform the raw result before setting it
|
||||
raw_result = child.result
|
||||
logger.debug(
|
||||
"[AgentTask] Converting raw result for correlation_id %s",
|
||||
self._correlation_id,
|
||||
)
|
||||
|
||||
try:
|
||||
response = load_agent_response(raw_result)
|
||||
|
||||
if self._response_format is not None:
|
||||
ensure_response_format(
|
||||
self._response_format,
|
||||
self._correlation_id,
|
||||
response,
|
||||
)
|
||||
|
||||
# Set the typed AgentResponse as this task's result
|
||||
self.set_value(is_error=False, value=response)
|
||||
except Exception as e:
|
||||
logger.exception(
|
||||
"[AgentTask] Failed to convert result for correlation_id: %s",
|
||||
self._correlation_id,
|
||||
)
|
||||
self.set_value(is_error=True, value=e)
|
||||
else:
|
||||
# If error not handled by the parent, set it explicitly.
|
||||
if self._first_error is None:
|
||||
self._first_error = child.result
|
||||
self.set_value(is_error=True, value=self._first_error)
|
||||
|
||||
|
||||
class AzureFunctionsAgentExecutor(DurableAgentExecutor[AgentTask]):
|
||||
"""Executor that executes durable agents inside Azure Functions orchestrations."""
|
||||
|
||||
def __init__(self, context: AgentOrchestrationContextType):
|
||||
self.context = context
|
||||
|
||||
def generate_unique_id(self) -> str:
|
||||
return str(self.context.new_uuid())
|
||||
|
||||
def get_run_request(
|
||||
self,
|
||||
message: str,
|
||||
*,
|
||||
options: dict[str, Any] | None = None,
|
||||
) -> RunRequest:
|
||||
"""Get the current run request from the orchestration context.
|
||||
|
||||
Args:
|
||||
message: The message to send to the agent
|
||||
options: Optional options dictionary. Supported keys include
|
||||
``response_format``, ``enable_tool_calls``, and ``wait_for_response``.
|
||||
Additional keys are forwarded to the agent execution.
|
||||
|
||||
Returns:
|
||||
RunRequest: The current run request
|
||||
|
||||
Raises:
|
||||
ValueError: If wait_for_response=False (not supported in orchestrations)
|
||||
"""
|
||||
# Create a copy to avoid modifying the caller's dict
|
||||
|
||||
request = super().get_run_request(message, options=options)
|
||||
request.orchestration_id = self.context.instance_id
|
||||
return request
|
||||
|
||||
def run_durable_agent(
|
||||
self,
|
||||
agent_name: str,
|
||||
run_request: RunRequest,
|
||||
session: AgentSession | None = None,
|
||||
) -> AgentTask:
|
||||
|
||||
# Resolve session
|
||||
session_id = self._create_session_id(agent_name, session)
|
||||
|
||||
entity_id = df.EntityId(
|
||||
name=session_id.entity_name,
|
||||
key=session_id.key,
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
"[AzureFunctionsAgentProvider] correlation_id: %s entity_id: %s session_id: %s",
|
||||
run_request.correlation_id,
|
||||
entity_id,
|
||||
session_id,
|
||||
)
|
||||
|
||||
# Branch based on wait_for_response
|
||||
if not run_request.wait_for_response:
|
||||
# Fire-and-forget mode: signal entity and return pre-completed task
|
||||
logger.debug(
|
||||
"[AzureFunctionsAgentExecutor] Fire-and-forget mode: signaling entity (correlation: %s)",
|
||||
run_request.correlation_id,
|
||||
)
|
||||
self.context.signal_entity(entity_id, "run", run_request.to_dict())
|
||||
|
||||
# Create acceptance response using base class helper
|
||||
acceptance_response = self._create_acceptance_response(run_request.correlation_id)
|
||||
|
||||
# Create a pre-completed task with the acceptance response
|
||||
entity_task = PreCompletedTask(acceptance_response)
|
||||
else:
|
||||
# Blocking mode: call entity and wait for response
|
||||
entity_task = self.context.call_entity(entity_id, "run", run_request.to_dict())
|
||||
|
||||
return AgentTask(
|
||||
entity_task=entity_task,
|
||||
response_format=run_request.response_format,
|
||||
correlation_id=run_request.correlation_id,
|
||||
)
|
||||
@@ -0,0 +1,83 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
"""Workflow Execution for Durable Functions.
|
||||
|
||||
This module provides the Azure Functions entry point for workflow orchestration.
|
||||
The actual orchestration logic lives in the shared module
|
||||
``agent_framework_durabletask._workflows.orchestrator`` and is host-agnostic.
|
||||
This module re-exports the public API and provides the AF-specific
|
||||
``run_workflow_orchestrator`` wrapper that creates an
|
||||
:class:`AzureFunctionsWorkflowContext` before delegating.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from collections.abc import Generator
|
||||
from typing import Any
|
||||
|
||||
from agent_framework import Workflow
|
||||
from agent_framework_durabletask._workflows.orchestrator import (
|
||||
SOURCE_HITL_RESPONSE,
|
||||
SOURCE_ORCHESTRATOR,
|
||||
SOURCE_WORKFLOW_START,
|
||||
ExecutorResult,
|
||||
PendingHITLRequest,
|
||||
TaskMetadata,
|
||||
TaskType,
|
||||
_extract_message_content, # pyright: ignore[reportPrivateUsage]
|
||||
build_agent_executor_response,
|
||||
execute_hitl_response_handler,
|
||||
route_message_through_edge_groups,
|
||||
)
|
||||
from agent_framework_durabletask._workflows.orchestrator import (
|
||||
run_workflow_orchestrator as _run_workflow_orchestrator_shared,
|
||||
)
|
||||
from azure.durable_functions import DurableOrchestrationContext
|
||||
|
||||
from ._workflow_af_context import AzureFunctionsWorkflowContext
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Re-export shared symbols for backward compatibility
|
||||
__all__ = [
|
||||
"SOURCE_HITL_RESPONSE",
|
||||
"SOURCE_ORCHESTRATOR",
|
||||
"SOURCE_WORKFLOW_START",
|
||||
"ExecutorResult",
|
||||
"PendingHITLRequest",
|
||||
"TaskMetadata",
|
||||
"TaskType",
|
||||
"_extract_message_content",
|
||||
"build_agent_executor_response",
|
||||
"execute_hitl_response_handler",
|
||||
"route_message_through_edge_groups",
|
||||
"run_workflow_orchestrator",
|
||||
]
|
||||
|
||||
|
||||
def run_workflow_orchestrator(
|
||||
context: DurableOrchestrationContext,
|
||||
workflow: Workflow,
|
||||
initial_message: Any,
|
||||
shared_state: dict[str, Any] | None = None,
|
||||
) -> Generator[Any, Any, list[Any] | dict[str, Any]]:
|
||||
"""Azure Functions wrapper around the shared workflow orchestrator.
|
||||
|
||||
Creates an :class:`AzureFunctionsWorkflowContext` and delegates to the
|
||||
host-agnostic :func:`run_workflow_orchestrator` in the durabletask package.
|
||||
|
||||
Args:
|
||||
context: The Azure Functions ``DurableOrchestrationContext``.
|
||||
workflow: The MAF Workflow instance to execute.
|
||||
initial_message: Initial message to send to the start executor.
|
||||
shared_state: Optional dict for cross-executor state sharing.
|
||||
|
||||
Returns:
|
||||
For a top-level run, the list of workflow outputs collected from executor
|
||||
activities. For a sub-workflow run, a result envelope ``{"outputs": [...],
|
||||
"events": [...]}`` so the parent can bubble nested progress (see the shared
|
||||
``run_workflow_orchestrator`` in the durabletask package).
|
||||
"""
|
||||
af_ctx = AzureFunctionsWorkflowContext(context)
|
||||
return _run_workflow_orchestrator_shared(af_ctx, workflow, initial_message, shared_state)
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
"""Azure Functions adapter for WorkflowOrchestrationContext.
|
||||
|
||||
Wraps ``azure.durable_functions.DurableOrchestrationContext`` to satisfy the
|
||||
:class:`~agent_framework_durabletask.WorkflowOrchestrationContext` protocol.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from agent_framework_durabletask import AgentSessionId, DurableAgentSession, DurableAIAgent
|
||||
from azure.durable_functions import DurableOrchestrationContext
|
||||
|
||||
from ._orchestration import AzureFunctionsAgentExecutor
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AzureFunctionsWorkflowContext:
|
||||
"""Adapter that maps ``DurableOrchestrationContext`` to ``WorkflowOrchestrationContext``."""
|
||||
|
||||
def __init__(self, context: DurableOrchestrationContext) -> None:
|
||||
self._context = context
|
||||
|
||||
# -- Properties -----------------------------------------------------------
|
||||
|
||||
@property
|
||||
def instance_id(self) -> str:
|
||||
# Typed local (not cast): mypy sees the untyped context as Any, while
|
||||
# pyright sees a concrete str - the annotation satisfies both.
|
||||
instance_id: str = self._context.instance_id
|
||||
return instance_id
|
||||
|
||||
@property
|
||||
def is_replaying(self) -> bool:
|
||||
is_replaying: bool = self._context.is_replaying
|
||||
return is_replaying
|
||||
|
||||
@property
|
||||
def supports_event_streaming(self) -> bool:
|
||||
# The Azure Functions host has no workflow event-streaming endpoint, and its
|
||||
# Durable Functions custom status is capped at 16 KB by the WebJobs extension.
|
||||
# Publishing the accumulating event log would overflow that cap and fail the
|
||||
# orchestrator, so events are omitted; state, pending HITL requests, and the
|
||||
# final output remain available via the workflow status endpoint.
|
||||
return False
|
||||
|
||||
@property
|
||||
def current_utc_datetime(self) -> datetime:
|
||||
current: datetime = self._context.current_utc_datetime
|
||||
return current
|
||||
|
||||
# -- Agent / Activity dispatch --------------------------------------------
|
||||
|
||||
def prepare_agent_task(self, executor_id: str, message: str, orchestration_instance_id: str) -> Any:
|
||||
session_id = AgentSessionId(name=executor_id, key=orchestration_instance_id)
|
||||
session = DurableAgentSession(durable_session_id=session_id)
|
||||
az_executor = AzureFunctionsAgentExecutor(self._context)
|
||||
agent = DurableAIAgent(az_executor, executor_id)
|
||||
return agent.run(message, session=session)
|
||||
|
||||
def prepare_activity_task(self, activity_name: str, input_json: str) -> Any:
|
||||
orchestration_context: Any = self._context
|
||||
return orchestration_context.call_activity(activity_name, input_json)
|
||||
|
||||
def call_sub_orchestrator(self, name: str, input: Any, instance_id: str | None = None) -> Any:
|
||||
orchestration_context: Any = self._context
|
||||
return orchestration_context.call_sub_orchestrator(name, input_=input, instance_id=instance_id)
|
||||
|
||||
# -- Composite tasks ------------------------------------------------------
|
||||
|
||||
def task_all(self, tasks: list[Any]) -> Any:
|
||||
return self._context.task_all(tasks)
|
||||
|
||||
def task_any(self, tasks: list[Any]) -> Any:
|
||||
return self._context.task_any(tasks)
|
||||
|
||||
# -- External events / timers ---------------------------------------------
|
||||
|
||||
def wait_for_external_event(self, name: str) -> Any:
|
||||
return self._context.wait_for_external_event(name)
|
||||
|
||||
def create_timer(self, fire_at: datetime) -> Any:
|
||||
return self._context.create_timer(fire_at)
|
||||
|
||||
# -- Status / utility -----------------------------------------------------
|
||||
|
||||
def set_custom_status(self, status: Any) -> None:
|
||||
self._context.set_custom_status(status)
|
||||
|
||||
def new_uuid(self) -> str:
|
||||
new_uuid: str = self._context.new_uuid()
|
||||
return new_uuid
|
||||
|
||||
def cancel_task(self, task: Any) -> None:
|
||||
cancel_fn = getattr(task, "cancel", None)
|
||||
if callable(cancel_fn):
|
||||
cancel_fn()
|
||||
|
||||
def get_task_result(self, task: Any) -> Any:
|
||||
return getattr(task, "result", None)
|
||||
Reference in New Issue
Block a user