Python: Move azurefunctions to azure for import (#2141)

* Move import to Azure

* fix mypy

* Update python/packages/azurefunctions/README.md

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Add missing types

* Address comments

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Laveesh Rohra
2025-11-12 12:13:25 -08:00
committed by GitHub
Unverified
parent 601b75a418
commit f3bf488735
20 changed files with 156 additions and 126 deletions
@@ -7,12 +7,11 @@ enabling durable, stateful AI agents deployed as Azure Function Apps.
from ._app import AgentFunctionApp
from ._callbacks import AgentCallbackContext, AgentResponseCallbackProtocol
from ._orchestration import DurableAIAgent, get_agent
from ._orchestration import DurableAIAgent
__all__ = [
"AgentCallbackContext",
"AgentFunctionApp",
"AgentResponseCallbackProtocol",
"DurableAIAgent",
"get_agent",
]
@@ -9,7 +9,7 @@ with Azure Durable Entities, enabling stateful and durable AI agent execution.
import json
import re
from collections.abc import Callable, Mapping
from typing import Any, cast
from typing import TYPE_CHECKING, Any, TypeVar, cast
import azure.durable_functions as df
import azure.functions as func
@@ -19,6 +19,7 @@ from ._callbacks import AgentResponseCallbackProtocol
from ._entities import create_agent_entity
from ._errors import IncomingRequestError
from ._models import AgentSessionId, RunRequest
from ._orchestration import AgentOrchestrationContextType, DurableAIAgent
from ._state import AgentState
logger = get_logger("agent_framework.azurefunctions")
@@ -30,18 +31,46 @@ WAIT_FOR_RESPONSE_FIELD: str = "wait_for_response"
WAIT_FOR_RESPONSE_HEADER: str = "x-ms-wait-for-response"
class AgentFunctionApp(df.DFApp):
EntityHandler = Callable[[df.DurableEntityContext], None]
HandlerT = TypeVar("HandlerT", bound=Callable[..., Any])
if TYPE_CHECKING:
class DFAppBase:
def __init__(self, http_auth_level: func.AuthLevel = func.AuthLevel.FUNCTION) -> None: ...
def function_name(self, name: str) -> Callable[[HandlerT], HandlerT]: ...
def route(self, route: str, methods: list[str]) -> Callable[[HandlerT], HandlerT]: ...
def durable_client_input(self, client_name: str) -> Callable[[HandlerT], HandlerT]: ...
def entity_trigger(self, context_name: str, entity_name: str) -> Callable[[EntityHandler], EntityHandler]: ...
def orchestration_trigger(self, context_name: str) -> Callable[[HandlerT], HandlerT]: ...
def activity_trigger(self, input_name: str) -> Callable[[HandlerT], HandlerT]: ...
else:
DFAppBase = df.DFApp # type: ignore[assignment]
class AgentFunctionApp(DFAppBase):
"""Main application class for creating durable agent function apps using Durable Entities.
This class uses Durable Entities pattern for agent execution, providing:
- Stateful agent conversations
- Conversation history management
- Signal-based operation invocation
- Better state management than orchestrations
Usage:
```python
from agent_framework.azurefunctions import AgentFunctionApp
Example:
-------
.. code-block:: python
from agent_framework.azure import AgentFunctionApp
from agent_framework.azure import AzureOpenAIAssistantsClient
# Create agents with unique names
@@ -64,9 +93,18 @@ class AgentFunctionApp(df.DFApp):
app = AgentFunctionApp()
app.add_agent(weather_agent)
app.add_agent(math_agent)
```
@app.orchestration_trigger(context_name="context")
def my_orchestration(context):
writer = app.get_agent(context, "WeatherAgent")
thread = writer.get_new_thread()
forecast_task = writer.run("What's the forecast?", thread=thread)
forecast = yield forecast_task
return forecast
This creates:
- HTTP trigger endpoint for each agent's requests (if enabled)
- Durable entity for each agent's state management and execution
- Full access to all Azure Functions capabilities
@@ -197,6 +235,30 @@ class AgentFunctionApp(df.DFApp):
logger.debug(f"[AgentFunctionApp] Agent '{name}' added successfully")
def get_agent(
self,
context: AgentOrchestrationContextType,
agent_name: str,
) -> DurableAIAgent:
"""Return a DurableAIAgent proxy for a registered agent.
Args:
context: Durable Functions orchestration context invoking the agent.
agent_name: Name of the agent registered on this app.
Raises:
ValueError: If the requested agent has not been registered.
Returns:
DurableAIAgent wrapper bound to the orchestration context.
"""
normalized_name = str(agent_name)
if normalized_name not in self.agents:
raise ValueError(f"Agent '{normalized_name}' is not registered with this app.")
return DurableAIAgent(context, normalized_name)
def _setup_agent_functions(
self,
agent: AgentProtocol,
@@ -232,9 +294,13 @@ class AgentFunctionApp(df.DFApp):
"""
run_function_name = self._build_function_name(agent_name, "http")
@self.function_name(run_function_name)
@self.route(route=f"agents/{agent_name}/run", methods=["POST"])
@self.durable_client_input(client_name="client")
function_name_decorator = self.function_name(run_function_name)
route_decorator = self.route(route=f"agents/{agent_name}/run", methods=["POST"])
durable_client_decorator = self.durable_client_input(client_name="client")
@function_name_decorator
@route_decorator
@durable_client_decorator
async def http_start(req: func.HttpRequest, client: df.DurableOrchestrationClient) -> func.HttpResponse:
"""HTTP trigger that calls a durable entity to execute the agent and returns the result.
@@ -379,8 +445,9 @@ class AgentFunctionApp(df.DFApp):
def _setup_health_route(self) -> None:
"""Register the optional health check route."""
health_route = self.route(route="health", methods=["GET"])
@self.route(route="health", methods=["GET"])
@health_route
def health_check(req: func.HttpRequest) -> func.HttpResponse:
"""Built-in health check endpoint."""
agent_info = [
@@ -643,8 +710,7 @@ class AgentFunctionApp(df.DFApp):
headers: dict[str, str] = {}
raw_headers = req.headers
if isinstance(raw_headers, Mapping):
headers_mapping = cast(Mapping[Any, Any], raw_headers)
for key, value in headers_mapping.items():
for key, value in raw_headers.items():
if value is not None:
headers[str(key).lower()] = str(value)
return headers
@@ -708,8 +774,7 @@ class AgentFunctionApp(df.DFApp):
header_value = None
raw_headers = req.headers
if isinstance(raw_headers, Mapping):
headers_mapping = cast(Mapping[Any, Any], raw_headers)
for key, value in headers_mapping.items():
for key, value in raw_headers.items():
if str(key).lower() == WAIT_FOR_RESPONSE_HEADER:
header_value = value
break
@@ -10,7 +10,7 @@ allows for long-running agent conversations.
import asyncio
import inspect
import json
from collections.abc import AsyncIterable
from collections.abc import AsyncIterable, Callable
from typing import Any, cast
import azure.durable_functions as df
@@ -340,7 +340,7 @@ class AgentEntity:
def create_agent_entity(
agent: AgentProtocol,
callback: AgentResponseCallbackProtocol | None = None,
):
) -> Callable[[df.DurableEntityContext], None]:
"""Factory function to create an agent entity class.
Args:
@@ -374,7 +374,7 @@ class AgentResponse:
def to_dict(self) -> dict[str, Any]:
"""Convert to dictionary for JSON serialization."""
result = {
result: dict[str, Any] = {
"message": self.message,
"thread_id": self.thread_id,
"status": self.status,
@@ -37,7 +37,7 @@ class DurableAIAgent(AgentProtocol):
yielded in orchestrations to wait for the entity call to complete.
Example usage in orchestration:
writer = get_agent(context, "WriterAgent")
writer = app.get_agent(context, "WriterAgent")
thread = writer.get_new_thread() # NOT yielded - returns immediately
response = yield writer.run( # Yielded - waits for entity call
@@ -104,7 +104,7 @@ class DurableAIAgent(AgentProtocol):
Example:
@app.orchestration_trigger(context_name="context")
def my_orchestration(context):
agent = get_agent(context, "MyAgent")
agent = app.get_agent(context, "MyAgent")
thread = agent.get_new_thread()
result = yield agent.run("Hello", thread=thread)
"""
@@ -209,27 +209,3 @@ class DurableAIAgent(AgentProtocol):
return "\n".join(cast(list[str], messages))
return self._messages_to_string(cast(list[ChatMessage], messages))
return str(messages)
def get_agent(context: AgentOrchestrationContextType, agent_name: str) -> DurableAIAgent:
"""Return a :class:`DurableAIAgent` proxy scoped to ``agent_name``.
Usage::
from agent_framework.azurefunctions import get_agent
@app.orchestration_trigger(context_name="context")
def my_orchestration(context: DurableOrchestrationContext):
writer = get_agent(context, "WriterAgent")
thread = writer.get_new_thread()
response = yield writer.run("Write a haiku", thread=thread)
Args:
context: The orchestration context provided by Durable Functions.
agent_name: Name of the durable agent entity to call.
Returns:
DurableAIAgent wrapper for the specified agent.
"""
return DurableAIAgent(context, agent_name)
@@ -25,7 +25,7 @@ class AgentState:
- Message counting
"""
def __init__(self):
def __init__(self) -> None:
"""Initialize empty agent state."""
self.conversation_history: list[ChatMessage] = []
self.last_response: str | None = None