Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
e7e68fe
Python: Add Scaffolding for Durable AzureFunctions package to Agent F…
larohra Nov 3, 2025
0808fd2
Merge branch 'main' into feature-azure-functions
dmytrostruk Nov 5, 2025
1d5677b
Merge branch 'main' into feature-azure-functions
dmytrostruk Nov 5, 2025
5686a00
.NET: Durable extension: initial src and unit tests (#1900)
cgillum Nov 5, 2025
1762cda
Python: Add Durable Agent Wrapper code (#1913)
larohra Nov 6, 2025
90742ba
Azure Functions .NET samples (#1939)
cgillum Nov 6, 2025
754491c
Python: Add Unit tests for Azurefunctions package (#1976)
larohra Nov 7, 2025
40b6def
.NET: [Feature Branch] Migrate state schema updates and support for a…
cgillum Nov 7, 2025
0aa8d30
Python: Add more samples for Azure Functions (#1980)
larohra Nov 7, 2025
304b809
.NET: [Feature Branch] Durable Task extension integration tests (#2017)
cgillum Nov 10, 2025
916b51f
.NET: [Feature Branch] Update OpenAI config for integration tests (#2…
cgillum Nov 10, 2025
4eb31f1
Python: Add Integration tests for AzureFunctions (#2020)
larohra Nov 11, 2025
17f6cc3
.NET: [Feature Branch] Update dotnet-build-and-test.yml to support in…
cgillum Nov 11, 2025
246bf07
Fix DTS startup issue and improve logging (#2103)
cgillum Nov 11, 2025
3eda97a
.NET: [Feature Branch] Introduce Azure OpenAI config for .NET pipelin…
cgillum Nov 12, 2025
66f2dd6
Merge branch 'main' into feature-azure-functions
cgillum Nov 12, 2025
3ddbf06
Fix uv.lock after merge
cgillum Nov 12, 2025
46df859
Python: Add README for Azure Functions samples setup (#2100)
anirudhgarg Nov 12, 2025
cc13e1f
Fix or remove broken markdown file links (#2115)
cgillum Nov 12, 2025
586238c
Merge branch 'main' into feature-azure-functions
cgillum Nov 12, 2025
2329bd4
.NET: [Feature Branch] Update HTTP API to be consistent across langua…
cgillum Nov 12, 2025
ebab25b
Python: Fix AzureFunctions Integration Tests (#2116)
larohra Nov 12, 2025
ff28066
Python: Fix Http Schema (#2112)
larohra Nov 12, 2025
601b75a
.NET: Remove IsPackable=false in preparation for nuget release (#2142)
cgillum Nov 12, 2025
f3bf488
Python: Move `azurefunctions` to `azure` for import (#2141)
larohra Nov 12, 2025
3fc355e
Update python/packages/azurefunctions/pyproject.toml
larohra Nov 12, 2025
8345b00
Update python/packages/azurefunctions/agent_framework_azurefunctions/…
larohra Nov 12, 2025
de0f804
Merge branch 'main' into feature-azure-functions
larohra Nov 12, 2025
513a971
Fix imports
larohra Nov 12, 2025
46035af
Address PR feedback from westey-m (#2150)
cgillum Nov 12, 2025
bb78afd
Schema changes for azure functions
gavin-aguiar Nov 12, 2025
4515ca1
Fixed serialization bug
gavin-aguiar Nov 13, 2025
b39d092
update to camel case
hallvictoria Nov 13, 2025
d374581
Adding logs
hallvictoria Nov 13, 2025
09fef9c
merge with main
hallvictoria Nov 14, 2025
f85bd9e
Merge branch 'main' of https://github.com/microsoft/agent-framework i…
hallvictoria Nov 14, 2025
bf22bab
sync uv.lock
hallvictoria Nov 14, 2025
045dc66
Updated schema
hallvictoria Nov 14, 2025
e3a06c7
Merged with main
gavin-aguiar Nov 14, 2025
e612614
Fixed deserialization bug
gavin-aguiar Nov 15, 2025
ceb1999
Merge branch 'main' into gaaguiar/schema_changes
gavin-aguiar Nov 17, 2025
ee54b66
Merge branch 'main' into gaaguiar/schema_changes
gavin-aguiar Nov 17, 2025
6b9829a
Fixed tests
gavin-aguiar Nov 17, 2025
befde8d
Merge branch 'main' of https://github.com/microsoft/agent-framework i…
gavin-aguiar Nov 17, 2025
fc0cfc7
Addressed comments
gavin-aguiar Nov 17, 2025
feb06ec
Merge branch 'gaaguiar/schema_changes' of https://github.com/microsof…
gavin-aguiar Nov 17, 2025
aacc3a6
Merge branch 'main' into gaaguiar/schema_changes
gavin-aguiar Nov 17, 2025
256d5db
Fixed mypy errors
gavin-aguiar Nov 17, 2025
1951d9c
Merge branch 'gaaguiar/schema_changes' of https://github.com/microsof…
gavin-aguiar Nov 17, 2025
a9cd3ff
Fixed bug in responsetype and authorName
gavin-aguiar Nov 18, 2025
8d35499
Merge branch 'main' into gaaguiar/schema_changes
gavin-aguiar Nov 18, 2025
a3b9934
Addressed feedback
gavin-aguiar Nov 18, 2025
511a813
Merge branch 'gaaguiar/schema_changes' of https://github.com/microsof…
gavin-aguiar Nov 18, 2025
3cd7c4f
Addressed more feedback
gavin-aguiar Nov 18, 2025
4a5d029
Python: Addressing comments for #2151 (#2315)
larohra Nov 19, 2025
63964b4
Fixed remaining snake_case properties
gavin-aguiar Nov 19, 2025
43acc12
Merge branch 'gaaguiar/schema_changes' of https://github.com/microsof…
gavin-aguiar Nov 19, 2025
8ddf167
Fixed remaining snake_case properties
gavin-aguiar Nov 19, 2025
58b85c8
Merge branch 'main' into gaaguiar/schema_changes
gavin-aguiar Nov 19, 2025
89cf49d
Merge branch 'gaaguiar/schema_changes' of https://github.com/microsof…
gavin-aguiar Nov 19, 2025
0fb791b
Fixed mypy errors
gavin-aguiar Nov 19, 2025
fe7ef9a
Minor changes
larohra Nov 19, 2025
8c16919
Merge branch 'gaaguiar/schema_changes' of https://github.com/microsof…
larohra Nov 19, 2025
98c1209
revert tool names
larohra Nov 19, 2025
e54279c
Fixed mypy errors
larohra Nov 19, 2025
ebf63d4
Merge branch 'main' into gaaguiar/schema_changes
gavin-aguiar Nov 19, 2025
4b7086b
Merge branch 'main' into gaaguiar/schema_changes
gavin-aguiar Nov 20, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,8 @@ agents.md
# AI
.claude/
WARP.md
**/memory-bank/
**/projectBrief.md

# Azurite storage emulator files
*/__azurite_db_blob__.json
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,27 +16,29 @@
from agent_framework import AgentProtocol, get_logger

from ._callbacks import AgentResponseCallbackProtocol
from ._constants import (
DEFAULT_MAX_POLL_RETRIES,
DEFAULT_POLL_INTERVAL_SECONDS,
MIMETYPE_APPLICATION_JSON,
MIMETYPE_TEXT_PLAIN,
REQUEST_RESPONSE_FORMAT_JSON,
REQUEST_RESPONSE_FORMAT_TEXT,
THREAD_ID_FIELD,
THREAD_ID_HEADER,
WAIT_FOR_RESPONSE_FIELD,
WAIT_FOR_RESPONSE_HEADER,
)
from ._durable_agent_state import DurableAgentState
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")

THREAD_ID_FIELD: str = "thread_id"
RESPONSE_FORMAT_JSON: str = "json"
RESPONSE_FORMAT_TEXT: str = "text"
WAIT_FOR_RESPONSE_FIELD: str = "wait_for_response"
WAIT_FOR_RESPONSE_HEADER: str = "x-ms-wait-for-response"


EntityHandler = Callable[[df.DurableEntityContext], None]
HandlerT = TypeVar("HandlerT", bound=Callable[..., Any])

DEFAULT_MAX_POLL_RETRIES: int = 30
DEFAULT_POLL_INTERVAL_SECONDS: float = 1.0

if TYPE_CHECKING:

class DFAppBase:
Expand Down Expand Up @@ -317,11 +319,11 @@ async def http_start(req: func.HttpRequest, client: df.DurableOrchestrationClien
"""
logger.debug(f"[HTTP Trigger] Received request on route: /api/agents/{agent_name}/run")

response_format: str = RESPONSE_FORMAT_JSON
request_response_format: str = REQUEST_RESPONSE_FORMAT_JSON
thread_id: str | None = None

try:
req_body, message, response_format = self._parse_incoming_request(req)
req_body, message, request_response_format = self._parse_incoming_request(req)
thread_id = self._resolve_thread_id(req=req, req_body=req_body)
wait_for_response = self._should_wait_for_response(req=req, req_body=req_body)

Expand All @@ -334,7 +336,7 @@ async def http_start(req: func.HttpRequest, client: df.DurableOrchestrationClien
return self._create_http_response(
payload={"error": "Message is required"},
status_code=400,
response_format=response_format,
request_response_format=request_response_format,
thread_id=thread_id,
)

Expand All @@ -351,6 +353,7 @@ async def http_start(req: func.HttpRequest, client: df.DurableOrchestrationClien
message,
thread_id,
correlation_id,
request_response_format,
)
logger.debug("Signalling entity %s with request: %s", entity_instance_id, run_request)
await client.signal_entity(entity_instance_id, "run_agent", run_request)
Expand All @@ -370,7 +373,7 @@ async def http_start(req: func.HttpRequest, client: df.DurableOrchestrationClien
return self._create_http_response(
payload=result,
status_code=200 if result.get("status") == "success" else 500,
response_format=response_format,
request_response_format=request_response_format,
thread_id=thread_id,
)

Expand All @@ -383,7 +386,7 @@ async def http_start(req: func.HttpRequest, client: df.DurableOrchestrationClien
return self._create_http_response(
payload=accepted_response,
status_code=202,
response_format=response_format,
request_response_format=request_response_format,
thread_id=thread_id,
)

Expand All @@ -392,23 +395,23 @@ async def http_start(req: func.HttpRequest, client: df.DurableOrchestrationClien
return self._create_http_response(
payload={"error": str(exc)},
status_code=exc.status_code,
response_format=response_format,
request_response_format=request_response_format,
thread_id=thread_id,
)
except ValueError as exc:
logger.error(f"[HTTP Trigger] Invalid JSON: {exc!s}")
return self._create_http_response(
payload={"error": "Invalid JSON"},
status_code=400,
response_format=response_format,
request_response_format=request_response_format,
thread_id=thread_id,
)
except Exception as exc:
logger.error(f"[HTTP Trigger] Error: {exc!s}", exc_info=True)
return self._create_http_response(
payload={"error": str(exc)},
status_code=500,
response_format=response_format,
request_response_format=request_response_format,
thread_id=thread_id,
)

Expand Down Expand Up @@ -466,7 +469,7 @@ def health_check(req: func.HttpRequest) -> func.HttpResponse:
return func.HttpResponse(
json.dumps({"status": "healthy", "agents": agent_info, "agent_count": len(self.agents)}),
status_code=200,
mimetype="application/json",
mimetype=MIMETYPE_APPLICATION_JSON,
)

_ = health_check
Expand All @@ -491,7 +494,7 @@ async def _read_cached_state(
self,
client: df.DurableOrchestrationClient,
entity_instance_id: df.EntityId,
) -> AgentState | None:
) -> DurableAgentState | None:
state_response = await client.read_entity_state(entity_instance_id)
if not state_response or not state_response.entity_exists:
return None
Expand All @@ -502,9 +505,7 @@ async def _read_cached_state(

typed_state_payload = cast(dict[str, Any], state_payload)

agent_state = AgentState()
agent_state.restore_state(typed_state_payload)
return agent_state
return DurableAgentState.from_dict(typed_state_payload)

async def _get_response_from_entity(
self,
Expand Down Expand Up @@ -580,31 +581,58 @@ async def _poll_entity_for_response(

return result

async def _build_timeout_result(self, message: str, thread_id: str, correlation_id: str) -> dict[str, Any]:
"""Create the timeout response."""
return {
"response": "Agent is still processing or timed out...",
def _build_response_payload(
self,
*,
response: str | None,
message: str,
thread_id: str,
status: str,
correlation_id: str,
extra_fields: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""Create a consistent response structure and allow optional extra fields."""
payload = {
"response": response,
"message": message,
THREAD_ID_FIELD: thread_id,
"status": "timeout",
"status": status,
"correlation_id": correlation_id,
}
if extra_fields:
payload.update(extra_fields)
return payload

async def _build_timeout_result(self, message: str, thread_id: str, correlation_id: str) -> dict[str, Any]:
"""Create the timeout response."""
return self._build_response_payload(
response="Agent is still processing or timed out...",
message=message,
thread_id=thread_id,
status="timeout",
correlation_id=correlation_id,
)

def _build_success_result(
self, response_data: dict[str, Any], message: str, thread_id: str, correlation_id: str, state: AgentState
self, response_data: dict[str, Any], message: str, thread_id: str, correlation_id: str, state: DurableAgentState
) -> dict[str, Any]:
"""Build the success result returned to the HTTP caller."""
return {
"response": response_data.get("content"),
"message": message,
THREAD_ID_FIELD: thread_id,
"status": "success",
"message_count": response_data.get("message_count", state.message_count),
"correlation_id": correlation_id,
}
return self._build_response_payload(
response=response_data.get("content"),
message=message,
thread_id=thread_id,
status="success",
correlation_id=correlation_id,
extra_fields={"message_count": response_data.get("message_count", state.message_count)},
)

def _build_request_data(
self, req_body: dict[str, Any], message: str, thread_id: str, correlation_id: str
self,
req_body: dict[str, Any],
message: str,
thread_id: str,
correlation_id: str,
request_response_format: str,
) -> dict[str, Any]:
"""Create the durable entity request payload."""
enable_tool_calls_value = req_body.get("enable_tool_calls")
Expand All @@ -613,6 +641,7 @@ def _build_request_data(
return RunRequest(
message=message,
role=req_body.get("role"),
request_response_format=request_response_format,
response_format=req_body.get("response_format"),
enable_tool_calls=enable_tool_calls,
thread_id=thread_id,
Expand All @@ -621,23 +650,23 @@ def _build_request_data(

def _build_accepted_response(self, message: str, thread_id: str, correlation_id: str) -> dict[str, Any]:
"""Build the response returned when not waiting for completion."""
return {
"response": "Agent request accepted",
"message": message,
THREAD_ID_FIELD: thread_id,
"status": "accepted",
"correlation_id": correlation_id,
}
return self._build_response_payload(
response="Agent request accepted",
message=message,
thread_id=thread_id,
status="accepted",
correlation_id=correlation_id,
)

def _create_http_response(
self,
payload: dict[str, Any] | str,
status_code: int,
response_format: str,
request_response_format: str,
thread_id: str | None,
) -> func.HttpResponse:
"""Create the HTTP response using helper serializers for clarity."""
if response_format == RESPONSE_FORMAT_TEXT:
if request_response_format == REQUEST_RESPONSE_FORMAT_TEXT:
return self._build_plain_text_response(payload=payload, status_code=status_code, thread_id=thread_id)

return self._build_json_response(payload=payload, status_code=status_code)
Expand All @@ -650,13 +679,13 @@ def _build_plain_text_response(
) -> func.HttpResponse:
"""Return a plain-text response with optional thread identifier header."""
body_text = payload if isinstance(payload, str) else self._convert_payload_to_text(payload)
headers = {"x-ms-thread-id": thread_id} if thread_id is not None else None
return func.HttpResponse(body_text, status_code=status_code, mimetype="text/plain", headers=headers)
headers = {THREAD_ID_HEADER: thread_id} if thread_id is not None else None
return func.HttpResponse(body_text, status_code=status_code, mimetype=MIMETYPE_TEXT_PLAIN, headers=headers)

def _build_json_response(self, payload: dict[str, Any] | str, status_code: int) -> func.HttpResponse:
"""Return the JSON response, serializing dictionaries as needed."""
body_json = payload if isinstance(payload, str) else json.dumps(payload)
return func.HttpResponse(body_json, status_code=status_code, mimetype="application/json")
return func.HttpResponse(body_json, status_code=status_code, mimetype=MIMETYPE_APPLICATION_JSON)

def _convert_payload_to_text(self, payload: dict[str, Any]) -> str:
"""Convert a structured payload into a human-readable text response."""
Expand Down Expand Up @@ -702,18 +731,19 @@ def _parse_incoming_request(self, req: func.HttpRequest) -> tuple[dict[str, Any]
normalized_content_type = self._extract_content_type(headers)
body_parser, body_format = self._select_body_parser(normalized_content_type)
prefers_json = self._accepts_json_response(headers)
response_format = self._select_response_format(body_format=body_format, prefers_json=prefers_json)
request_response_format = self._select_request_response_format(
body_format=body_format, prefers_json=prefers_json
)

req_body, message = body_parser(req)
return req_body, message, response_format
return req_body, message, request_response_format

def _extract_normalized_headers(self, req: func.HttpRequest) -> dict[str, str]:
"""Create a lowercase header mapping from the incoming request."""
headers: dict[str, str] = {}
raw_headers = req.headers
if isinstance(raw_headers, Mapping):
header_mapping: Mapping[str, Any] = cast(Mapping[str, Any], raw_headers)
for key, value in header_mapping.items():
for key, value in raw_headers.items():
if value is not None:
headers[str(key).lower()] = str(value)
return headers
Expand All @@ -729,9 +759,9 @@ def _select_body_parser(
normalized_content_type: str,
) -> tuple[Callable[[func.HttpRequest], tuple[dict[str, Any], str]], str]:
"""Choose the body parser and declared body format."""
if normalized_content_type in {"application/json"} or normalized_content_type.endswith("+json"):
return self._parse_json_body, RESPONSE_FORMAT_JSON
return self._parse_text_body, RESPONSE_FORMAT_TEXT
if normalized_content_type in {MIMETYPE_APPLICATION_JSON} or normalized_content_type.endswith("+json"):
return self._parse_json_body, REQUEST_RESPONSE_FORMAT_JSON
return self._parse_text_body, REQUEST_RESPONSE_FORMAT_TEXT

@staticmethod
def _accepts_json_response(headers: dict[str, str]) -> bool:
Expand All @@ -742,16 +772,16 @@ def _accepts_json_response(headers: dict[str, str]) -> bool:

for value in accept_header.split(","):
media_type = value.split(";")[0].strip().lower()
if media_type == "application/json":
if media_type == MIMETYPE_APPLICATION_JSON:
return True
return False

@staticmethod
def _select_response_format(body_format: str, prefers_json: bool) -> str:
def _select_request_response_format(body_format: str, prefers_json: bool) -> str:
"""Combine body format and accept preference to determine response format."""
if body_format == RESPONSE_FORMAT_JSON or prefers_json:
return RESPONSE_FORMAT_JSON
return RESPONSE_FORMAT_TEXT
if body_format == REQUEST_RESPONSE_FORMAT_JSON or prefers_json:
return REQUEST_RESPONSE_FORMAT_JSON
return REQUEST_RESPONSE_FORMAT_TEXT

@staticmethod
def _parse_json_body(req: func.HttpRequest) -> tuple[dict[str, Any], str]:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Copyright (c) Microsoft. All rights reserved.

"""Constants for Azure Functions Agent Framework integration."""

# Supported request/response formats and MIME types
REQUEST_RESPONSE_FORMAT_JSON: str = "json"
REQUEST_RESPONSE_FORMAT_TEXT: str = "text"
MIMETYPE_APPLICATION_JSON: str = "application/json"
MIMETYPE_TEXT_PLAIN: str = "text/plain"

# Field and header names
THREAD_ID_FIELD: str = "thread_id"
THREAD_ID_HEADER: str = "x-ms-thread-id"
WAIT_FOR_RESPONSE_FIELD: str = "wait_for_response"
WAIT_FOR_RESPONSE_HEADER: str = "x-ms-wait-for-response"

# Polling configuration
DEFAULT_MAX_POLL_RETRIES: int = 30
DEFAULT_POLL_INTERVAL_SECONDS: float = 1.0
Loading