From 99468d3772d3139bd095e1fc2928706a85860119 Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Wed, 2 Sep 2026 13:09:20 -0500 Subject: [PATCH 01/12] Add pluggable Agent application APIs --- README.md | 7 + azure/functions/__init__.py | 6 +- azure/functions/decorators/__init__.py | 4 +- azure/functions/decorators/function_app.py | 88 +++++++++++++ docs/ProgModelSpec.pyi | 37 +++++- tests/decorators/test_agents.py | 144 +++++++++++++++++++++ 6 files changed, 282 insertions(+), 4 deletions(-) create mode 100644 tests/decorators/test_agents.py diff --git a/README.md b/README.md index efc6c3f4..07048fb6 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,13 @@ _What's available?_ - Triggers / Bindings : Blob, Cosmos DB, Event Grid, Event Hub, HTTP, Kafka, MySQL, Queue, ServiceBus, SQL, Timer, and Warmup - Create a Python Function on Linux using a custom docker image - Triggers / Bindings : Custom binding support +- Pluggable markdown Agent injection through provider extension packages + +Agent APIs are provider-neutral and add no binding metadata. Install a provider +package such as `azurefunctions-extensions-agents-framework`, then use +`FunctionApp.markdown_agent(provider=...)`, `AiApp`, or `DurableAiApp`. Durable +support is installed through the provider package's `[durable]` extra and is +not imported by the core SDK. #### Get Started diff --git a/azure/functions/__init__.py b/azure/functions/__init__.py index e267d450..19ecdf0a 100644 --- a/azure/functions/__init__.py +++ b/azure/functions/__init__.py @@ -7,7 +7,7 @@ from ._eventgrid import CloudEvent, EventGridEvent, EventGridOutputEvent from ._cosmosdb import Document, DocumentList from ._http import HttpRequest, HttpResponse -from .decorators import (FunctionApp, Function, Blueprint, +from .decorators import (AiApp, DurableAiApp, FunctionApp, Function, Blueprint, DecoratorApi, DataType, AuthLevel, Cardinality, AccessRights, HttpMethod, AsgiFunctionApp, WsgiFunctionApp, @@ -94,6 +94,8 @@ # PyStein implementation 'FunctionApp', + 'AiApp', + 'DurableAiApp', 'Function', 'FunctionRegister', 'DecoratorApi', @@ -117,4 +119,4 @@ 'mcp_content', ) -__version__ = '2.3.0' +__version__ = '2.4.0b1' diff --git a/azure/functions/decorators/__init__.py b/azure/functions/decorators/__init__.py index beaf7ff6..64d75a5c 100644 --- a/azure/functions/decorators/__init__.py +++ b/azure/functions/decorators/__init__.py @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. from .core import Cardinality, AccessRights, CosmosDBChangeFeedMode -from .function_app import FunctionApp, Function, DecoratorApi, DataType, \ +from .function_app import AiApp, DurableAiApp, FunctionApp, Function, DecoratorApi, DataType, \ AuthLevel, Blueprint, ExternalHttpFunctionApp, AsgiFunctionApp, \ WsgiFunctionApp, FunctionRegister, TriggerApi, BindingApi, \ SettingsApi, BlobSource, McpPropertyType @@ -10,6 +10,8 @@ __all__ = [ 'FunctionApp', + 'AiApp', + 'DurableAiApp', 'Function', 'FunctionRegister', 'DecoratorApi', diff --git a/azure/functions/decorators/function_app.py b/azure/functions/decorators/function_app.py index fbe241b4..9e3195ee 100644 --- a/azure/functions/decorators/function_app.py +++ b/azure/functions/decorators/function_app.py @@ -4,6 +4,7 @@ import asyncio import dataclasses import functools +import importlib import inspect import json import logging @@ -64,6 +65,31 @@ MySqlTrigger +def _agent_provider_distribution(provider: str) -> str: + normalized = provider.replace('_', '-') + if normalized.startswith('agent-'): + normalized = normalized.removeprefix('agent-') + return f'azurefunctions-extensions-agents-{normalized}' + + +def _load_agents_base(provider: str): + try: + return importlib.import_module('azurefunctions.extensions.agents_base') + except ModuleNotFoundError as exc: + missing_base_modules = { + 'azurefunctions', + 'azurefunctions.extensions', + 'azurefunctions.extensions.agents_base', + } + if exc.name not in missing_base_modules: + raise + distribution = _agent_provider_distribution(provider) + raise ImportError( + f"Agent provider {provider!r} is not installed. " + f"Install {distribution!r}." + ) from exc + + class Function(object): """ The function object represents a function in Function App. It @@ -4540,6 +4566,68 @@ def __init__(self, """ super().__init__(auth_level=http_auth_level) + def markdown_agent(self, *, provider: str, **kwargs): + """Inject a provider Agent built from a markdown definition.""" + agents_base = _load_agents_base(provider) + return agents_base.markdown_agent(self, provider=provider, **kwargs) + + +class AiApp(FunctionApp): + """FunctionApp configured for one pluggable Agent provider.""" + + def __init__(self, + http_auth_level: Union[AuthLevel, str] = AuthLevel.FUNCTION, + *, provider: str, app_root=None, **provider_options): + super().__init__(http_auth_level=http_auth_level) + self._agent_provider = provider + agents_base = _load_agents_base(provider) + agents_base.configure_app( + self, + provider=provider, + app_root=app_root, + provider_options=provider_options, + ) + + def markdown_agent(self, *, provider: Optional[str] = None, **kwargs): + selected_provider = provider or self._agent_provider + return super().markdown_agent(provider=selected_provider, **kwargs) + + +class DurableAiApp(AiApp): + """AiApp with optional replay-safe Durable Agent orchestration.""" + + def __init__(self, + http_auth_level: Union[AuthLevel, str] = AuthLevel.FUNCTION, + *, provider: str, app_root=None, **provider_options): + super().__init__( + http_auth_level=http_auth_level, + provider=provider, + app_root=app_root, + **provider_options, + ) + try: + _load_agents_base(provider).configure_durable_app(self) + except ModuleNotFoundError as exc: + if exc.name != 'azure.durable_functions': + raise + distribution = _agent_provider_distribution(provider) + raise ImportError( + f"Durable Agent support is not installed. " + f"Install {distribution + '[durable]'!r}." + ) from exc + + def orchestration_trigger(self, context_name: str, + orchestration: Optional[str] = None, + input_type: Optional[type] = None): + agents_base = _load_agents_base(self._agent_provider) + return agents_base.durable_orchestration_trigger( + self, + sdk_decorator=super().orchestration_trigger, + context_name=context_name, + orchestration=orchestration, + input_type=input_type, + ) + class Blueprint(TriggerApi, BindingApi, SettingsApi): """ diff --git a/docs/ProgModelSpec.pyi b/docs/ProgModelSpec.pyi index b9c1d60a..fbcc9466 100644 --- a/docs/ProgModelSpec.pyi +++ b/docs/ProgModelSpec.pyi @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. from abc import ABC -from typing import Callable, Dict, List, Optional, Union, Iterable +from typing import Any, Callable, Dict, List, Optional, Union, Iterable from azure.functions import AsgiMiddleware, WsgiMiddleware from azure.functions.decorators.core import Binding, BlobSource, Trigger, DataType, \ @@ -1329,6 +1329,41 @@ class FunctionApp(FunctionRegister, TriggerApi, BindingApi): """ pass + def markdown_agent(self, *, provider: str, **kwargs: Any) -> Callable: + """Inject an Agent supplied by a provider extension. + + :param provider: Registered Agent provider ID. + :param kwargs: Provider and markdown binding options. + :return: Decorator function. + """ + pass + + +class AiApp(FunctionApp): + """FunctionApp configured for one Agent provider.""" + + def __init__(self, + http_auth_level: Union[AuthLevel, str] = AuthLevel.FUNCTION, + *, provider: str, app_root=None, + **provider_options: Any): + """Configure an app with a provider and immutable defaults.""" + pass + + def markdown_agent(self, *, provider: Optional[str] = None, + **kwargs: Any) -> Callable: + """Inject an Agent using the configured provider by default.""" + pass + + +class DurableAiApp(AiApp): + """AiApp with optional replay-safe Durable Agent orchestration.""" + + def orchestration_trigger(self, context_name: str, + orchestration: Optional[str] = None, + input_type: Optional[type] = None) -> Callable: + """Register an orchestrator with a Durable Agent context.""" + pass + class BluePrint(TriggerApi, BindingApi, SettingsApi): """Functions container class where all the functions diff --git a/tests/decorators/test_agents.py b/tests/decorators/test_agents.py new file mode 100644 index 00000000..bd5e0cd8 --- /dev/null +++ b/tests/decorators/test_agents.py @@ -0,0 +1,144 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import unittest +from unittest.mock import patch + +import azure.functions as func +from azure.functions.decorators.function_app import ( + AiApp, + DurableAiApp, + FunctionApp, +) + + +class TestAgentApps(unittest.TestCase): + + @patch('azure.functions.decorators.function_app._load_agents_base') + def test_function_app_markdown_agent_delegates_exact_arguments( + self, load_agents_base): + agents_base = load_agents_base.return_value + decorator = object() + agents_base.markdown_agent.return_value = decorator + app = FunctionApp() + + result = app.markdown_agent( + provider='agent_framework', + arg_name='agent', + agent_name='orders', + client_factory='factory', + ) + + self.assertIs(result, decorator) + agents_base.markdown_agent.assert_called_once_with( + app, + provider='agent_framework', + arg_name='agent', + agent_name='orders', + client_factory='factory', + ) + + @patch('azure.functions.decorators.function_app._load_agents_base') + def test_ai_app_configures_provider_and_defaults(self, load_agents_base): + agents_base = load_agents_base.return_value + + app = AiApp( + provider='agent_framework', + app_root='app', + client_factory='factory', + ) + + agents_base.configure_app.assert_called_once_with( + app, + provider='agent_framework', + app_root='app', + provider_options={'client_factory': 'factory'}, + ) + + @patch('azure.functions.decorators.function_app._load_agents_base') + def test_ai_app_markdown_agent_uses_configured_provider( + self, load_agents_base): + agents_base = load_agents_base.return_value + app = AiApp(provider='agent_framework') + agents_base.reset_mock() + + app.markdown_agent(arg_name='agent', agent_name='orders') + + agents_base.markdown_agent.assert_called_once_with( + app, + provider='agent_framework', + arg_name='agent', + agent_name='orders', + ) + + @patch('azure.functions.decorators.function_app._load_agents_base') + def test_durable_ai_app_configures_durable_support( + self, load_agents_base): + agents_base = load_agents_base.return_value + + app = DurableAiApp(provider='agent_framework') + + agents_base.configure_durable_app.assert_called_once_with(app) + + @patch('azure.functions.decorators.function_app._load_agents_base') + def test_durable_orchestration_delegates_to_base( + self, load_agents_base): + agents_base = load_agents_base.return_value + sentinel = object() + agents_base.durable_orchestration_trigger.return_value = sentinel + app = DurableAiApp(provider='agent_framework') + + result = app.orchestration_trigger( + context_name='context', + orchestration='orders', + ) + + self.assertIs(result, sentinel) + call = agents_base.durable_orchestration_trigger.call_args + self.assertIs(call.args[0], app) + self.assertEqual(call.kwargs['context_name'], 'context') + self.assertEqual(call.kwargs['orchestration'], 'orders') + self.assertIsNone(call.kwargs['input_type']) + self.assertTrue(callable(call.kwargs['sdk_decorator'])) + + def test_agent_apps_are_public(self): + self.assertIs(func.AiApp, AiApp) + self.assertIs(func.DurableAiApp, DurableAiApp) + + @patch('azure.functions.decorators.function_app.importlib.import_module') + def test_missing_base_reports_provider_install(self, import_module): + import_module.side_effect = ModuleNotFoundError( + "No module named 'azurefunctions.extensions.agents_base'", + name='azurefunctions.extensions.agents_base', + ) + + with self.assertRaisesRegex( + ImportError, + 'azurefunctions-extensions-agents-framework'): + FunctionApp().markdown_agent(provider='agent_framework') + + @patch('azure.functions.decorators.function_app.importlib.import_module') + def test_provider_import_error_is_not_rewritten(self, import_module): + import_module.side_effect = ModuleNotFoundError( + "No module named 'provider_dependency'", + name='provider_dependency', + ) + + with self.assertRaisesRegex(ModuleNotFoundError, 'provider_dependency'): + FunctionApp().markdown_agent(provider='agent_framework') + + @patch('azure.functions.decorators.function_app._load_agents_base') + def test_missing_durable_reports_provider_extra(self, load_agents_base): + agents_base = load_agents_base.return_value + agents_base.configure_durable_app.side_effect = ModuleNotFoundError( + "No module named 'azure.durable_functions'", + name='azure.durable_functions', + ) + + with self.assertRaisesRegex( + ImportError, + r'azurefunctions-extensions-agents-framework\[durable\]'): + DurableAiApp(provider='agent_framework') + + +if __name__ == '__main__': + unittest.main() From ad4caaf7687c88f6fe2269828c07a16b983047c2 Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Wed, 2 Sep 2026 13:15:23 -0500 Subject: [PATCH 02/12] remove version bump --- azure/functions/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure/functions/__init__.py b/azure/functions/__init__.py index 19ecdf0a..caca2775 100644 --- a/azure/functions/__init__.py +++ b/azure/functions/__init__.py @@ -119,4 +119,4 @@ 'mcp_content', ) -__version__ = '2.4.0b1' +__version__ = '2.3.0' From 03a3fa746aaae9b42eaf84ec4d3fa6f29b798e12 Mon Sep 17 00:00:00 2001 From: hallvictoria <59299039+hallvictoria@users.noreply.github.com> Date: Wed, 2 Sep 2026 14:38:55 -0500 Subject: [PATCH 03/12] Apply batched suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- azure/functions/decorators/function_app.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/azure/functions/decorators/function_app.py b/azure/functions/decorators/function_app.py index 9e3195ee..893cbacd 100644 --- a/azure/functions/decorators/function_app.py +++ b/azure/functions/decorators/function_app.py @@ -4566,7 +4566,8 @@ def __init__(self, """ super().__init__(auth_level=http_auth_level) - def markdown_agent(self, *, provider: str, **kwargs): + def markdown_agent(self, *, provider: str, + **kwargs: Any) -> Callable[..., Any]: """Inject a provider Agent built from a markdown definition.""" agents_base = _load_agents_base(provider) return agents_base.markdown_agent(self, provider=provider, **kwargs) @@ -4588,8 +4589,9 @@ def __init__(self, provider_options=provider_options, ) - def markdown_agent(self, *, provider: Optional[str] = None, **kwargs): - selected_provider = provider or self._agent_provider + def markdown_agent(self, *, provider: Optional[str] = None, + **kwargs: Any) -> Callable[..., Any]: + selected_provider = self._agent_provider if provider is None else provider return super().markdown_agent(provider=selected_provider, **kwargs) From aa856ada78445cb4d716fd40f3450dcb4f1f0617 Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Wed, 2 Sep 2026 14:52:33 -0500 Subject: [PATCH 04/12] rename --- azure/functions/decorators/function_app.py | 5 +++-- tests/decorators/test_agents.py | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/azure/functions/decorators/function_app.py b/azure/functions/decorators/function_app.py index 9e3195ee..bd3eb231 100644 --- a/azure/functions/decorators/function_app.py +++ b/azure/functions/decorators/function_app.py @@ -74,12 +74,13 @@ def _agent_provider_distribution(provider: str) -> str: def _load_agents_base(provider: str): try: - return importlib.import_module('azurefunctions.extensions.agents_base') + return importlib.import_module('azurefunctions.extensions.agents.base') except ModuleNotFoundError as exc: missing_base_modules = { 'azurefunctions', 'azurefunctions.extensions', - 'azurefunctions.extensions.agents_base', + 'azurefunctions.extensions.agents', + 'azurefunctions.extensions.agents.base', } if exc.name not in missing_base_modules: raise diff --git a/tests/decorators/test_agents.py b/tests/decorators/test_agents.py index bd5e0cd8..1db778a7 100644 --- a/tests/decorators/test_agents.py +++ b/tests/decorators/test_agents.py @@ -107,8 +107,8 @@ def test_agent_apps_are_public(self): @patch('azure.functions.decorators.function_app.importlib.import_module') def test_missing_base_reports_provider_install(self, import_module): import_module.side_effect = ModuleNotFoundError( - "No module named 'azurefunctions.extensions.agents_base'", - name='azurefunctions.extensions.agents_base', + "No module named 'azurefunctions.extensions.agents.base'", + name='azurefunctions.extensions.agents.base', ) with self.assertRaisesRegex( From 8aadfd81ad8ac4668bd89900383c7c3ecd62abf6 Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Thu, 3 Sep 2026 10:01:35 -0500 Subject: [PATCH 05/12] per agent provider --- README.md | 4 ++- azure/functions/decorators/function_app.py | 11 +++++++ tests/decorators/test_agents.py | 38 ++++++++++++++++++++++ 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 07048fb6..832884b6 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,9 @@ Agent APIs are provider-neutral and add no binding metadata. Install a provider package such as `azurefunctions-extensions-agents-framework`, then use `FunctionApp.markdown_agent(provider=...)`, `AiApp`, or `DurableAiApp`. Durable support is installed through the provider package's `[durable]` extra and is -not imported by the core SDK. +not imported by the core SDK. Each Agent binding may select a different +installed provider; `AiApp` supplies a default. Configure reusable defaults for +additional providers with `FunctionApp.configure_agent_provider()`. #### Get Started diff --git a/azure/functions/decorators/function_app.py b/azure/functions/decorators/function_app.py index 5697c999..6c67620a 100644 --- a/azure/functions/decorators/function_app.py +++ b/azure/functions/decorators/function_app.py @@ -4573,6 +4573,17 @@ def markdown_agent(self, *, provider: str, agents_base = _load_agents_base(provider) return agents_base.markdown_agent(self, provider=provider, **kwargs) + def configure_agent_provider(self, *, provider: str, app_root=None, + **provider_options: Any) -> None: + """Configure an Agent provider for reuse, including Durable calls.""" + agents_base = _load_agents_base(provider) + agents_base.configure_agent_provider( + self, + provider=provider, + app_root=app_root, + provider_options=provider_options, + ) + class AiApp(FunctionApp): """FunctionApp configured for one pluggable Agent provider.""" diff --git a/tests/decorators/test_agents.py b/tests/decorators/test_agents.py index 1db778a7..9635d782 100644 --- a/tests/decorators/test_agents.py +++ b/tests/decorators/test_agents.py @@ -70,6 +70,44 @@ def test_ai_app_markdown_agent_uses_configured_provider( agent_name='orders', ) + @patch('azure.functions.decorators.function_app._load_agents_base') + def test_ai_app_markdown_agent_can_override_configured_provider( + self, load_agents_base): + agents_base = load_agents_base.return_value + app = AiApp(provider='agent_framework') + agents_base.reset_mock() + + app.markdown_agent( + provider='langgraph', + arg_name='agent', + agent_name='researcher', + ) + + agents_base.markdown_agent.assert_called_once_with( + app, + provider='langgraph', + arg_name='agent', + agent_name='researcher', + ) + + @patch('azure.functions.decorators.function_app._load_agents_base') + def test_configure_agent_provider_delegates_defaults(self, load_agents_base): + agents_base = load_agents_base.return_value + app = FunctionApp() + + app.configure_agent_provider( + provider='langgraph', + app_root='app', + recursion_limit=10, + ) + + agents_base.configure_agent_provider.assert_called_once_with( + app, + provider='langgraph', + app_root='app', + provider_options={'recursion_limit': 10}, + ) + @patch('azure.functions.decorators.function_app._load_agents_base') def test_durable_ai_app_configures_durable_support( self, load_agents_base): From 3c6cd09a489ecdb17504be514aa4b61f00b7993e Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Thu, 3 Sep 2026 10:05:20 -0500 Subject: [PATCH 06/12] refactoring --- azure/functions/decorators/_agents.py | 29 ++++++++++++++++++++++ azure/functions/decorators/function_app.py | 28 +-------------------- tests/decorators/test_agents.py | 4 +-- 3 files changed, 32 insertions(+), 29 deletions(-) create mode 100644 azure/functions/decorators/_agents.py diff --git a/azure/functions/decorators/_agents.py b/azure/functions/decorators/_agents.py new file mode 100644 index 00000000..b2bbc7f1 --- /dev/null +++ b/azure/functions/decorators/_agents.py @@ -0,0 +1,29 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +import importlib + + +def _agent_provider_distribution(provider: str) -> str: + normalized = provider.replace('_', '-') + if normalized.startswith('agent-'): + normalized = normalized.removeprefix('agent-') + return f'azurefunctions-extensions-agents-{normalized}' + + +def _load_agents_base(provider: str): + try: + return importlib.import_module('azurefunctions.extensions.agents.base') + except ModuleNotFoundError as exc: + missing_base_modules = { + 'azurefunctions', + 'azurefunctions.extensions', + 'azurefunctions.extensions.agents', + 'azurefunctions.extensions.agents.base', + } + if exc.name not in missing_base_modules: + raise + distribution = _agent_provider_distribution(provider) + raise ImportError( + f"Agent provider {provider!r} is not installed. " + f"Install {distribution!r}." + ) from exc diff --git a/azure/functions/decorators/function_app.py b/azure/functions/decorators/function_app.py index 6c67620a..76ce508a 100644 --- a/azure/functions/decorators/function_app.py +++ b/azure/functions/decorators/function_app.py @@ -4,7 +4,6 @@ import asyncio import dataclasses import functools -import importlib import inspect import json import logging @@ -63,32 +62,7 @@ from .._http_wsgi import WsgiMiddleware, Context from azure.functions.decorators.mysql import MySqlInput, MySqlOutput, \ MySqlTrigger - - -def _agent_provider_distribution(provider: str) -> str: - normalized = provider.replace('_', '-') - if normalized.startswith('agent-'): - normalized = normalized.removeprefix('agent-') - return f'azurefunctions-extensions-agents-{normalized}' - - -def _load_agents_base(provider: str): - try: - return importlib.import_module('azurefunctions.extensions.agents.base') - except ModuleNotFoundError as exc: - missing_base_modules = { - 'azurefunctions', - 'azurefunctions.extensions', - 'azurefunctions.extensions.agents', - 'azurefunctions.extensions.agents.base', - } - if exc.name not in missing_base_modules: - raise - distribution = _agent_provider_distribution(provider) - raise ImportError( - f"Agent provider {provider!r} is not installed. " - f"Install {distribution!r}." - ) from exc +from ._agents import _agent_provider_distribution, _load_agents_base class Function(object): diff --git a/tests/decorators/test_agents.py b/tests/decorators/test_agents.py index 9635d782..b3fc226d 100644 --- a/tests/decorators/test_agents.py +++ b/tests/decorators/test_agents.py @@ -142,7 +142,7 @@ def test_agent_apps_are_public(self): self.assertIs(func.AiApp, AiApp) self.assertIs(func.DurableAiApp, DurableAiApp) - @patch('azure.functions.decorators.function_app.importlib.import_module') + @patch('azure.functions.decorators._agents.importlib.import_module') def test_missing_base_reports_provider_install(self, import_module): import_module.side_effect = ModuleNotFoundError( "No module named 'azurefunctions.extensions.agents.base'", @@ -154,7 +154,7 @@ def test_missing_base_reports_provider_install(self, import_module): 'azurefunctions-extensions-agents-framework'): FunctionApp().markdown_agent(provider='agent_framework') - @patch('azure.functions.decorators.function_app.importlib.import_module') + @patch('azure.functions.decorators._agents.importlib.import_module') def test_provider_import_error_is_not_rewritten(self, import_module): import_module.side_effect = ModuleNotFoundError( "No module named 'provider_dependency'", From 9a9db8104c0837d54ed96f014de1ec4fd28d8e1c Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Thu, 3 Sep 2026 10:26:47 -0500 Subject: [PATCH 07/12] v1 for durable --- README.md | 4 +-- azure/functions/decorators/_agents.py | 12 +++------ azure/functions/decorators/function_app.py | 18 +++---------- tests/decorators/test_agents.py | 30 +++++++++------------- 4 files changed, 20 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 832884b6..10c8688a 100644 --- a/README.md +++ b/README.md @@ -29,8 +29,8 @@ package such as `azurefunctions-extensions-agents-framework`, then use `FunctionApp.markdown_agent(provider=...)`, `AiApp`, or `DurableAiApp`. Durable support is installed through the provider package's `[durable]` extra and is not imported by the core SDK. Each Agent binding may select a different -installed provider; `AiApp` supplies a default. Configure reusable defaults for -additional providers with `FunctionApp.configure_agent_provider()`. +installed provider; `AiApp` supplies a default, which all Durable Agent calls +use. #### Get Started diff --git a/azure/functions/decorators/_agents.py b/azure/functions/decorators/_agents.py index b2bbc7f1..6b312bee 100644 --- a/azure/functions/decorators/_agents.py +++ b/azure/functions/decorators/_agents.py @@ -2,6 +2,8 @@ # Licensed under the MIT License. import importlib +_AGENTS_BASE_MODULE = 'azurefunctions.extensions.agents.base' + def _agent_provider_distribution(provider: str) -> str: normalized = provider.replace('_', '-') @@ -12,16 +14,8 @@ def _agent_provider_distribution(provider: str) -> str: def _load_agents_base(provider: str): try: - return importlib.import_module('azurefunctions.extensions.agents.base') + return importlib.import_module(_AGENTS_BASE_MODULE) except ModuleNotFoundError as exc: - missing_base_modules = { - 'azurefunctions', - 'azurefunctions.extensions', - 'azurefunctions.extensions.agents', - 'azurefunctions.extensions.agents.base', - } - if exc.name not in missing_base_modules: - raise distribution = _agent_provider_distribution(provider) raise ImportError( f"Agent provider {provider!r} is not installed. " diff --git a/azure/functions/decorators/function_app.py b/azure/functions/decorators/function_app.py index 76ce508a..d5c522b0 100644 --- a/azure/functions/decorators/function_app.py +++ b/azure/functions/decorators/function_app.py @@ -11,7 +11,7 @@ from abc import ABC from datetime import time -from typing import Any, Callable, Dict, List, Optional, Union, \ +from typing import Any, Callable, cast, Dict, List, Optional, Union, \ Iterable from azure.functions.decorators.blob import BlobTrigger, BlobInput, BlobOutput @@ -4545,18 +4545,8 @@ def markdown_agent(self, *, provider: str, **kwargs: Any) -> Callable[..., Any]: """Inject a provider Agent built from a markdown definition.""" agents_base = _load_agents_base(provider) - return agents_base.markdown_agent(self, provider=provider, **kwargs) - - def configure_agent_provider(self, *, provider: str, app_root=None, - **provider_options: Any) -> None: - """Configure an Agent provider for reuse, including Durable calls.""" - agents_base = _load_agents_base(provider) - agents_base.configure_agent_provider( - self, - provider=provider, - app_root=app_root, - provider_options=provider_options, - ) + return cast(Callable[..., Any], agents_base.markdown_agent( + self, provider=provider, **kwargs)) class AiApp(FunctionApp): @@ -4596,8 +4586,6 @@ def __init__(self, try: _load_agents_base(provider).configure_durable_app(self) except ModuleNotFoundError as exc: - if exc.name != 'azure.durable_functions': - raise distribution = _agent_provider_distribution(provider) raise ImportError( f"Durable Agent support is not installed. " diff --git a/tests/decorators/test_agents.py b/tests/decorators/test_agents.py index b3fc226d..d613f1bc 100644 --- a/tests/decorators/test_agents.py +++ b/tests/decorators/test_agents.py @@ -90,24 +90,6 @@ def test_ai_app_markdown_agent_can_override_configured_provider( agent_name='researcher', ) - @patch('azure.functions.decorators.function_app._load_agents_base') - def test_configure_agent_provider_delegates_defaults(self, load_agents_base): - agents_base = load_agents_base.return_value - app = FunctionApp() - - app.configure_agent_provider( - provider='langgraph', - app_root='app', - recursion_limit=10, - ) - - agents_base.configure_agent_provider.assert_called_once_with( - app, - provider='langgraph', - app_root='app', - provider_options={'recursion_limit': 10}, - ) - @patch('azure.functions.decorators.function_app._load_agents_base') def test_durable_ai_app_configures_durable_support( self, load_agents_base): @@ -154,6 +136,18 @@ def test_missing_base_reports_provider_install(self, import_module): 'azurefunctions-extensions-agents-framework'): FunctionApp().markdown_agent(provider='agent_framework') + @patch('azure.functions.decorators._agents.importlib.import_module') + def test_missing_base_parent_reports_provider_install(self, import_module): + import_module.side_effect = ModuleNotFoundError( + "No module named 'azurefunctions'", + name='azurefunctions', + ) + + with self.assertRaisesRegex( + ImportError, + 'azurefunctions-extensions-agents-framework'): + FunctionApp().markdown_agent(provider='agent_framework') + @patch('azure.functions.decorators._agents.importlib.import_module') def test_provider_import_error_is_not_rewritten(self, import_module): import_module.side_effect = ModuleNotFoundError( From eb8c11957fd4e515d001a60ec591990f8b673753 Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Thu, 3 Sep 2026 10:55:30 -0500 Subject: [PATCH 08/12] fix test --- tests/decorators/test_agents.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/tests/decorators/test_agents.py b/tests/decorators/test_agents.py index d613f1bc..5523eb9e 100644 --- a/tests/decorators/test_agents.py +++ b/tests/decorators/test_agents.py @@ -125,17 +125,22 @@ def test_agent_apps_are_public(self): self.assertIs(func.DurableAiApp, DurableAiApp) @patch('azure.functions.decorators._agents.importlib.import_module') - def test_missing_base_reports_provider_install(self, import_module): + def test_missing_provider_reports_extension_install(self, import_module): import_module.side_effect = ModuleNotFoundError( "No module named 'azurefunctions.extensions.agents.base'", name='azurefunctions.extensions.agents.base', ) - with self.assertRaisesRegex( - ImportError, - 'azurefunctions-extensions-agents-framework'): + with self.assertRaises(ImportError) as raised: FunctionApp().markdown_agent(provider='agent_framework') + self.assertEqual( + str(raised.exception), + "Agent provider 'agent_framework' is not installed. " + "Install 'azurefunctions-extensions-agents-framework'.", + ) + self.assertIs(raised.exception.__cause__, import_module.side_effect) + @patch('azure.functions.decorators._agents.importlib.import_module') def test_missing_base_parent_reports_provider_install(self, import_module): import_module.side_effect = ModuleNotFoundError( @@ -149,15 +154,19 @@ def test_missing_base_parent_reports_provider_install(self, import_module): FunctionApp().markdown_agent(provider='agent_framework') @patch('azure.functions.decorators._agents.importlib.import_module') - def test_provider_import_error_is_not_rewritten(self, import_module): + def test_provider_import_error_reports_extension_install(self, import_module): import_module.side_effect = ModuleNotFoundError( "No module named 'provider_dependency'", name='provider_dependency', ) - with self.assertRaisesRegex(ModuleNotFoundError, 'provider_dependency'): + with self.assertRaisesRegex( + ImportError, + 'azurefunctions-extensions-agents-framework') as raised: FunctionApp().markdown_agent(provider='agent_framework') + self.assertIs(raised.exception.__cause__, import_module.side_effect) + @patch('azure.functions.decorators.function_app._load_agents_base') def test_missing_durable_reports_provider_extra(self, load_agents_base): agents_base = load_agents_base.return_value From 0d032934e6defa89b76470edfb4fd9b3354eea0b Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Thu, 3 Sep 2026 15:00:37 -0500 Subject: [PATCH 09/12] simplify --- azure/functions/decorators/function_app.py | 6 ++---- tests/decorators/test_agents.py | 20 ++++++++------------ 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/azure/functions/decorators/function_app.py b/azure/functions/decorators/function_app.py index d5c522b0..dc3a87d2 100644 --- a/azure/functions/decorators/function_app.py +++ b/azure/functions/decorators/function_app.py @@ -4565,10 +4565,8 @@ def __init__(self, provider_options=provider_options, ) - def markdown_agent(self, *, provider: Optional[str] = None, - **kwargs: Any) -> Callable[..., Any]: - selected_provider = self._agent_provider if provider is None else provider - return super().markdown_agent(provider=selected_provider, **kwargs) + def markdown_agent(self, **kwargs: Any) -> Callable[..., Any]: + return super().markdown_agent(provider=self._agent_provider, **kwargs) class DurableAiApp(AiApp): diff --git a/tests/decorators/test_agents.py b/tests/decorators/test_agents.py index 5523eb9e..b1ec2fbc 100644 --- a/tests/decorators/test_agents.py +++ b/tests/decorators/test_agents.py @@ -71,24 +71,20 @@ def test_ai_app_markdown_agent_uses_configured_provider( ) @patch('azure.functions.decorators.function_app._load_agents_base') - def test_ai_app_markdown_agent_can_override_configured_provider( + def test_ai_app_markdown_agent_rejects_provider_override( self, load_agents_base): agents_base = load_agents_base.return_value app = AiApp(provider='agent_framework') agents_base.reset_mock() - app.markdown_agent( - provider='langgraph', - arg_name='agent', - agent_name='researcher', - ) + with self.assertRaisesRegex(TypeError, "multiple values for keyword"): + app.markdown_agent( + provider='langgraph', + arg_name='agent', + agent_name='researcher', + ) - agents_base.markdown_agent.assert_called_once_with( - app, - provider='langgraph', - arg_name='agent', - agent_name='researcher', - ) + agents_base.markdown_agent.assert_not_called() @patch('azure.functions.decorators.function_app._load_agents_base') def test_durable_ai_app_configures_durable_support( From 136986a217cd4843f37dfaf7adb565bb05706137 Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Tue, 8 Sep 2026 11:40:00 -0500 Subject: [PATCH 10/12] feedback --- README.md | 4 +- azure/functions/__init__.py | 6 +-- azure/functions/decorators/__init__.py | 6 +-- azure/functions/decorators/_agents.py | 12 +++++- azure/functions/decorators/function_app.py | 8 ++-- docs/ProgModelSpec.pyi | 6 +-- tests/decorators/test_agents.py | 43 +++++++++++++++------- 7 files changed, 54 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 10c8688a..02543aab 100644 --- a/README.md +++ b/README.md @@ -26,10 +26,10 @@ _What's available?_ Agent APIs are provider-neutral and add no binding metadata. Install a provider package such as `azurefunctions-extensions-agents-framework`, then use -`FunctionApp.markdown_agent(provider=...)`, `AiApp`, or `DurableAiApp`. Durable +`FunctionApp.markdown_agent(provider=...)`, `AIApp`, or `DurableAIApp`. Durable support is installed through the provider package's `[durable]` extra and is not imported by the core SDK. Each Agent binding may select a different -installed provider; `AiApp` supplies a default, which all Durable Agent calls +installed provider; `AIApp` supplies a default, which all Durable Agent calls use. #### Get Started diff --git a/azure/functions/__init__.py b/azure/functions/__init__.py index caca2775..93b5fe9f 100644 --- a/azure/functions/__init__.py +++ b/azure/functions/__init__.py @@ -7,7 +7,7 @@ from ._eventgrid import CloudEvent, EventGridEvent, EventGridOutputEvent from ._cosmosdb import Document, DocumentList from ._http import HttpRequest, HttpResponse -from .decorators import (AiApp, DurableAiApp, FunctionApp, Function, Blueprint, +from .decorators import (AIApp, DurableAIApp, FunctionApp, Function, Blueprint, DecoratorApi, DataType, AuthLevel, Cardinality, AccessRights, HttpMethod, AsgiFunctionApp, WsgiFunctionApp, @@ -94,8 +94,8 @@ # PyStein implementation 'FunctionApp', - 'AiApp', - 'DurableAiApp', + 'AIApp', + 'DurableAIApp', 'Function', 'FunctionRegister', 'DecoratorApi', diff --git a/azure/functions/decorators/__init__.py b/azure/functions/decorators/__init__.py index 64d75a5c..3422e5f2 100644 --- a/azure/functions/decorators/__init__.py +++ b/azure/functions/decorators/__init__.py @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. from .core import Cardinality, AccessRights, CosmosDBChangeFeedMode -from .function_app import AiApp, DurableAiApp, FunctionApp, Function, DecoratorApi, DataType, \ +from .function_app import AIApp, DurableAIApp, FunctionApp, Function, DecoratorApi, DataType, \ AuthLevel, Blueprint, ExternalHttpFunctionApp, AsgiFunctionApp, \ WsgiFunctionApp, FunctionRegister, TriggerApi, BindingApi, \ SettingsApi, BlobSource, McpPropertyType @@ -10,8 +10,8 @@ __all__ = [ 'FunctionApp', - 'AiApp', - 'DurableAiApp', + 'AIApp', + 'DurableAIApp', 'Function', 'FunctionRegister', 'DecoratorApi', diff --git a/azure/functions/decorators/_agents.py b/azure/functions/decorators/_agents.py index 6b312bee..19e888de 100644 --- a/azure/functions/decorators/_agents.py +++ b/azure/functions/decorators/_agents.py @@ -3,6 +3,7 @@ import importlib _AGENTS_BASE_MODULE = 'azurefunctions.extensions.agents.base' +_agents_base = None def _agent_provider_distribution(provider: str) -> str: @@ -12,10 +13,17 @@ def _agent_provider_distribution(provider: str) -> str: return f'azurefunctions-extensions-agents-{normalized}' +def _import_agents_base(): + global _agents_base + if _agents_base is None: + _agents_base = importlib.import_module(_AGENTS_BASE_MODULE) + return _agents_base + + def _load_agents_base(provider: str): try: - return importlib.import_module(_AGENTS_BASE_MODULE) - except ModuleNotFoundError as exc: + return _import_agents_base() + except ImportError as exc: distribution = _agent_provider_distribution(provider) raise ImportError( f"Agent provider {provider!r} is not installed. " diff --git a/azure/functions/decorators/function_app.py b/azure/functions/decorators/function_app.py index dc3a87d2..22dd5cc5 100644 --- a/azure/functions/decorators/function_app.py +++ b/azure/functions/decorators/function_app.py @@ -4549,7 +4549,7 @@ def markdown_agent(self, *, provider: str, self, provider=provider, **kwargs)) -class AiApp(FunctionApp): +class AIApp(FunctionApp): """FunctionApp configured for one pluggable Agent provider.""" def __init__(self, @@ -4569,8 +4569,8 @@ def markdown_agent(self, **kwargs: Any) -> Callable[..., Any]: return super().markdown_agent(provider=self._agent_provider, **kwargs) -class DurableAiApp(AiApp): - """AiApp with optional replay-safe Durable Agent orchestration.""" +class DurableAIApp(AIApp): + """AIApp with optional replay-safe Durable Agent orchestration.""" def __init__(self, http_auth_level: Union[AuthLevel, str] = AuthLevel.FUNCTION, @@ -4583,7 +4583,7 @@ def __init__(self, ) try: _load_agents_base(provider).configure_durable_app(self) - except ModuleNotFoundError as exc: + except ImportError as exc: distribution = _agent_provider_distribution(provider) raise ImportError( f"Durable Agent support is not installed. " diff --git a/docs/ProgModelSpec.pyi b/docs/ProgModelSpec.pyi index fbcc9466..44939e88 100644 --- a/docs/ProgModelSpec.pyi +++ b/docs/ProgModelSpec.pyi @@ -1339,7 +1339,7 @@ class FunctionApp(FunctionRegister, TriggerApi, BindingApi): pass -class AiApp(FunctionApp): +class AIApp(FunctionApp): """FunctionApp configured for one Agent provider.""" def __init__(self, @@ -1355,8 +1355,8 @@ class AiApp(FunctionApp): pass -class DurableAiApp(AiApp): - """AiApp with optional replay-safe Durable Agent orchestration.""" +class DurableAIApp(AIApp): + """AIApp with optional replay-safe Durable Agent orchestration.""" def orchestration_trigger(self, context_name: str, orchestration: Optional[str] = None, diff --git a/tests/decorators/test_agents.py b/tests/decorators/test_agents.py index b1ec2fbc..6ef3ad36 100644 --- a/tests/decorators/test_agents.py +++ b/tests/decorators/test_agents.py @@ -4,15 +4,30 @@ from unittest.mock import patch import azure.functions as func +from azure.functions.decorators import _agents from azure.functions.decorators.function_app import ( - AiApp, - DurableAiApp, + AIApp, + DurableAIApp, FunctionApp, ) class TestAgentApps(unittest.TestCase): + @patch('azure.functions.decorators._agents.importlib.import_module') + def test_agents_base_import_is_cached_across_providers(self, import_module): + agents_base = object() + import_module.return_value = agents_base + + with patch.object(_agents, '_agents_base', None): + self.assertIs( + _agents._load_agents_base('agent_framework'), agents_base) + self.assertIs( + _agents._load_agents_base('other_provider'), agents_base) + + import_module.assert_called_once_with( + 'azurefunctions.extensions.agents.base') + @patch('azure.functions.decorators.function_app._load_agents_base') def test_function_app_markdown_agent_delegates_exact_arguments( self, load_agents_base): @@ -41,7 +56,7 @@ def test_function_app_markdown_agent_delegates_exact_arguments( def test_ai_app_configures_provider_and_defaults(self, load_agents_base): agents_base = load_agents_base.return_value - app = AiApp( + app = AIApp( provider='agent_framework', app_root='app', client_factory='factory', @@ -58,7 +73,7 @@ def test_ai_app_configures_provider_and_defaults(self, load_agents_base): def test_ai_app_markdown_agent_uses_configured_provider( self, load_agents_base): agents_base = load_agents_base.return_value - app = AiApp(provider='agent_framework') + app = AIApp(provider='agent_framework') agents_base.reset_mock() app.markdown_agent(arg_name='agent', agent_name='orders') @@ -74,7 +89,7 @@ def test_ai_app_markdown_agent_uses_configured_provider( def test_ai_app_markdown_agent_rejects_provider_override( self, load_agents_base): agents_base = load_agents_base.return_value - app = AiApp(provider='agent_framework') + app = AIApp(provider='agent_framework') agents_base.reset_mock() with self.assertRaisesRegex(TypeError, "multiple values for keyword"): @@ -91,7 +106,7 @@ def test_durable_ai_app_configures_durable_support( self, load_agents_base): agents_base = load_agents_base.return_value - app = DurableAiApp(provider='agent_framework') + app = DurableAIApp(provider='agent_framework') agents_base.configure_durable_app.assert_called_once_with(app) @@ -101,7 +116,7 @@ def test_durable_orchestration_delegates_to_base( agents_base = load_agents_base.return_value sentinel = object() agents_base.durable_orchestration_trigger.return_value = sentinel - app = DurableAiApp(provider='agent_framework') + app = DurableAIApp(provider='agent_framework') result = app.orchestration_trigger( context_name='context', @@ -117,12 +132,12 @@ def test_durable_orchestration_delegates_to_base( self.assertTrue(callable(call.kwargs['sdk_decorator'])) def test_agent_apps_are_public(self): - self.assertIs(func.AiApp, AiApp) - self.assertIs(func.DurableAiApp, DurableAiApp) + self.assertIs(func.AIApp, AIApp) + self.assertIs(func.DurableAIApp, DurableAIApp) @patch('azure.functions.decorators._agents.importlib.import_module') def test_missing_provider_reports_extension_install(self, import_module): - import_module.side_effect = ModuleNotFoundError( + import_module.side_effect = ImportError( "No module named 'azurefunctions.extensions.agents.base'", name='azurefunctions.extensions.agents.base', ) @@ -139,7 +154,7 @@ def test_missing_provider_reports_extension_install(self, import_module): @patch('azure.functions.decorators._agents.importlib.import_module') def test_missing_base_parent_reports_provider_install(self, import_module): - import_module.side_effect = ModuleNotFoundError( + import_module.side_effect = ImportError( "No module named 'azurefunctions'", name='azurefunctions', ) @@ -151,7 +166,7 @@ def test_missing_base_parent_reports_provider_install(self, import_module): @patch('azure.functions.decorators._agents.importlib.import_module') def test_provider_import_error_reports_extension_install(self, import_module): - import_module.side_effect = ModuleNotFoundError( + import_module.side_effect = ImportError( "No module named 'provider_dependency'", name='provider_dependency', ) @@ -166,7 +181,7 @@ def test_provider_import_error_reports_extension_install(self, import_module): @patch('azure.functions.decorators.function_app._load_agents_base') def test_missing_durable_reports_provider_extra(self, load_agents_base): agents_base = load_agents_base.return_value - agents_base.configure_durable_app.side_effect = ModuleNotFoundError( + agents_base.configure_durable_app.side_effect = ImportError( "No module named 'azure.durable_functions'", name='azure.durable_functions', ) @@ -174,7 +189,7 @@ def test_missing_durable_reports_provider_extra(self, load_agents_base): with self.assertRaisesRegex( ImportError, r'azurefunctions-extensions-agents-framework\[durable\]'): - DurableAiApp(provider='agent_framework') + DurableAIApp(provider='agent_framework') if __name__ == '__main__': From 589088d21c29b82c3e81d50a745f9060314c3be4 Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Tue, 8 Sep 2026 13:29:08 -0500 Subject: [PATCH 11/12] Rename to agents-extension --- README.md | 2 +- azure/functions/decorators/_agents.py | 4 +--- tests/decorators/test_agents.py | 23 +++++++++++++---------- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 02543aab..c44c0b74 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ _What's available?_ - Pluggable markdown Agent injection through provider extension packages Agent APIs are provider-neutral and add no binding metadata. Install a provider -package such as `azurefunctions-extensions-agents-framework`, then use +package such as `azurefunctions-agents-extension-agent-framework`, then use `FunctionApp.markdown_agent(provider=...)`, `AIApp`, or `DurableAIApp`. Durable support is installed through the provider package's `[durable]` extra and is not imported by the core SDK. Each Agent binding may select a different diff --git a/azure/functions/decorators/_agents.py b/azure/functions/decorators/_agents.py index 19e888de..f07b9faf 100644 --- a/azure/functions/decorators/_agents.py +++ b/azure/functions/decorators/_agents.py @@ -8,9 +8,7 @@ def _agent_provider_distribution(provider: str) -> str: normalized = provider.replace('_', '-') - if normalized.startswith('agent-'): - normalized = normalized.removeprefix('agent-') - return f'azurefunctions-extensions-agents-{normalized}' + return f'azurefunctions-agents-extension-{normalized}' def _import_agents_base(): diff --git a/tests/decorators/test_agents.py b/tests/decorators/test_agents.py index 6ef3ad36..13faf70f 100644 --- a/tests/decorators/test_agents.py +++ b/tests/decorators/test_agents.py @@ -148,7 +148,7 @@ def test_missing_provider_reports_extension_install(self, import_module): self.assertEqual( str(raised.exception), "Agent provider 'agent_framework' is not installed. " - "Install 'azurefunctions-extensions-agents-framework'.", + "Install 'azurefunctions-agents-extension-agent-framework'.", ) self.assertIs(raised.exception.__cause__, import_module.side_effect) @@ -158,10 +158,11 @@ def test_missing_base_parent_reports_provider_install(self, import_module): "No module named 'azurefunctions'", name='azurefunctions', ) + expected_distribution = ( + 'azurefunctions-agents-extension-agent-framework' + ) - with self.assertRaisesRegex( - ImportError, - 'azurefunctions-extensions-agents-framework'): + with self.assertRaisesRegex(ImportError, expected_distribution): FunctionApp().markdown_agent(provider='agent_framework') @patch('azure.functions.decorators._agents.importlib.import_module') @@ -170,10 +171,11 @@ def test_provider_import_error_reports_extension_install(self, import_module): "No module named 'provider_dependency'", name='provider_dependency', ) + expected_distribution = ( + 'azurefunctions-agents-extension-agent-framework' + ) - with self.assertRaisesRegex( - ImportError, - 'azurefunctions-extensions-agents-framework') as raised: + with self.assertRaisesRegex(ImportError, expected_distribution) as raised: FunctionApp().markdown_agent(provider='agent_framework') self.assertIs(raised.exception.__cause__, import_module.side_effect) @@ -185,10 +187,11 @@ def test_missing_durable_reports_provider_extra(self, load_agents_base): "No module named 'azure.durable_functions'", name='azure.durable_functions', ) + expected_distribution = ( + r'azurefunctions-agents-extension-agent-framework\[durable\]' + ) - with self.assertRaisesRegex( - ImportError, - r'azurefunctions-extensions-agents-framework\[durable\]'): + with self.assertRaisesRegex(ImportError, expected_distribution): DurableAIApp(provider='agent_framework') From 216d72389cc24e47fb409bcb300ab2fb30d43b4c Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Wed, 9 Sep 2026 10:38:41 -0500 Subject: [PATCH 12/12] Rename to AgentFunctionApp and AgentDFApp --- README.md | 4 +-- azure/functions/__init__.py | 6 ++--- azure/functions/decorators/__init__.py | 6 ++--- azure/functions/decorators/function_app.py | 6 ++--- docs/ProgModelSpec.pyi | 6 ++--- tests/decorators/test_agents.py | 29 +++++++++++----------- 6 files changed, 29 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index c44c0b74..7f8c926c 100644 --- a/README.md +++ b/README.md @@ -26,10 +26,10 @@ _What's available?_ Agent APIs are provider-neutral and add no binding metadata. Install a provider package such as `azurefunctions-agents-extension-agent-framework`, then use -`FunctionApp.markdown_agent(provider=...)`, `AIApp`, or `DurableAIApp`. Durable +`FunctionApp.markdown_agent(provider=...)`, `AgentFunctionApp`, or `AgentDFApp`. Durable support is installed through the provider package's `[durable]` extra and is not imported by the core SDK. Each Agent binding may select a different -installed provider; `AIApp` supplies a default, which all Durable Agent calls +installed provider; `AgentFunctionApp` supplies a default, which all Durable Agent calls use. #### Get Started diff --git a/azure/functions/__init__.py b/azure/functions/__init__.py index 93b5fe9f..cbc96a31 100644 --- a/azure/functions/__init__.py +++ b/azure/functions/__init__.py @@ -7,7 +7,7 @@ from ._eventgrid import CloudEvent, EventGridEvent, EventGridOutputEvent from ._cosmosdb import Document, DocumentList from ._http import HttpRequest, HttpResponse -from .decorators import (AIApp, DurableAIApp, FunctionApp, Function, Blueprint, +from .decorators import (AgentDFApp, AgentFunctionApp, FunctionApp, Function, Blueprint, DecoratorApi, DataType, AuthLevel, Cardinality, AccessRights, HttpMethod, AsgiFunctionApp, WsgiFunctionApp, @@ -94,8 +94,8 @@ # PyStein implementation 'FunctionApp', - 'AIApp', - 'DurableAIApp', + 'AgentFunctionApp', + 'AgentDFApp', 'Function', 'FunctionRegister', 'DecoratorApi', diff --git a/azure/functions/decorators/__init__.py b/azure/functions/decorators/__init__.py index 3422e5f2..2ac45204 100644 --- a/azure/functions/decorators/__init__.py +++ b/azure/functions/decorators/__init__.py @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. from .core import Cardinality, AccessRights, CosmosDBChangeFeedMode -from .function_app import AIApp, DurableAIApp, FunctionApp, Function, DecoratorApi, DataType, \ +from .function_app import AgentDFApp, AgentFunctionApp, FunctionApp, Function, DecoratorApi, DataType, \ AuthLevel, Blueprint, ExternalHttpFunctionApp, AsgiFunctionApp, \ WsgiFunctionApp, FunctionRegister, TriggerApi, BindingApi, \ SettingsApi, BlobSource, McpPropertyType @@ -10,8 +10,8 @@ __all__ = [ 'FunctionApp', - 'AIApp', - 'DurableAIApp', + 'AgentFunctionApp', + 'AgentDFApp', 'Function', 'FunctionRegister', 'DecoratorApi', diff --git a/azure/functions/decorators/function_app.py b/azure/functions/decorators/function_app.py index 22dd5cc5..e562315a 100644 --- a/azure/functions/decorators/function_app.py +++ b/azure/functions/decorators/function_app.py @@ -4549,7 +4549,7 @@ def markdown_agent(self, *, provider: str, self, provider=provider, **kwargs)) -class AIApp(FunctionApp): +class AgentFunctionApp(FunctionApp): """FunctionApp configured for one pluggable Agent provider.""" def __init__(self, @@ -4569,8 +4569,8 @@ def markdown_agent(self, **kwargs: Any) -> Callable[..., Any]: return super().markdown_agent(provider=self._agent_provider, **kwargs) -class DurableAIApp(AIApp): - """AIApp with optional replay-safe Durable Agent orchestration.""" +class AgentDFApp(AgentFunctionApp): + """AgentFunctionApp with optional replay-safe Durable Agent orchestration.""" def __init__(self, http_auth_level: Union[AuthLevel, str] = AuthLevel.FUNCTION, diff --git a/docs/ProgModelSpec.pyi b/docs/ProgModelSpec.pyi index 44939e88..56ebc325 100644 --- a/docs/ProgModelSpec.pyi +++ b/docs/ProgModelSpec.pyi @@ -1339,7 +1339,7 @@ class FunctionApp(FunctionRegister, TriggerApi, BindingApi): pass -class AIApp(FunctionApp): +class AgentFunctionApp(FunctionApp): """FunctionApp configured for one Agent provider.""" def __init__(self, @@ -1355,8 +1355,8 @@ class AIApp(FunctionApp): pass -class DurableAIApp(AIApp): - """AIApp with optional replay-safe Durable Agent orchestration.""" +class AgentDFApp(AgentFunctionApp): + """AgentFunctionApp with optional replay-safe Durable Agent orchestration.""" def orchestration_trigger(self, context_name: str, orchestration: Optional[str] = None, diff --git a/tests/decorators/test_agents.py b/tests/decorators/test_agents.py index 13faf70f..130a2129 100644 --- a/tests/decorators/test_agents.py +++ b/tests/decorators/test_agents.py @@ -6,8 +6,8 @@ import azure.functions as func from azure.functions.decorators import _agents from azure.functions.decorators.function_app import ( - AIApp, - DurableAIApp, + AgentDFApp, + AgentFunctionApp, FunctionApp, ) @@ -53,10 +53,11 @@ def test_function_app_markdown_agent_delegates_exact_arguments( ) @patch('azure.functions.decorators.function_app._load_agents_base') - def test_ai_app_configures_provider_and_defaults(self, load_agents_base): + def test_agent_function_app_configures_provider_and_defaults( + self, load_agents_base): agents_base = load_agents_base.return_value - app = AIApp( + app = AgentFunctionApp( provider='agent_framework', app_root='app', client_factory='factory', @@ -70,10 +71,10 @@ def test_ai_app_configures_provider_and_defaults(self, load_agents_base): ) @patch('azure.functions.decorators.function_app._load_agents_base') - def test_ai_app_markdown_agent_uses_configured_provider( + def test_agent_function_app_markdown_agent_uses_configured_provider( self, load_agents_base): agents_base = load_agents_base.return_value - app = AIApp(provider='agent_framework') + app = AgentFunctionApp(provider='agent_framework') agents_base.reset_mock() app.markdown_agent(arg_name='agent', agent_name='orders') @@ -86,10 +87,10 @@ def test_ai_app_markdown_agent_uses_configured_provider( ) @patch('azure.functions.decorators.function_app._load_agents_base') - def test_ai_app_markdown_agent_rejects_provider_override( + def test_agent_function_app_markdown_agent_rejects_provider_override( self, load_agents_base): agents_base = load_agents_base.return_value - app = AIApp(provider='agent_framework') + app = AgentFunctionApp(provider='agent_framework') agents_base.reset_mock() with self.assertRaisesRegex(TypeError, "multiple values for keyword"): @@ -102,11 +103,11 @@ def test_ai_app_markdown_agent_rejects_provider_override( agents_base.markdown_agent.assert_not_called() @patch('azure.functions.decorators.function_app._load_agents_base') - def test_durable_ai_app_configures_durable_support( + def test_agent_df_app_configures_durable_support( self, load_agents_base): agents_base = load_agents_base.return_value - app = DurableAIApp(provider='agent_framework') + app = AgentDFApp(provider='agent_framework') agents_base.configure_durable_app.assert_called_once_with(app) @@ -116,7 +117,7 @@ def test_durable_orchestration_delegates_to_base( agents_base = load_agents_base.return_value sentinel = object() agents_base.durable_orchestration_trigger.return_value = sentinel - app = DurableAIApp(provider='agent_framework') + app = AgentDFApp(provider='agent_framework') result = app.orchestration_trigger( context_name='context', @@ -132,8 +133,8 @@ def test_durable_orchestration_delegates_to_base( self.assertTrue(callable(call.kwargs['sdk_decorator'])) def test_agent_apps_are_public(self): - self.assertIs(func.AIApp, AIApp) - self.assertIs(func.DurableAIApp, DurableAIApp) + self.assertIs(func.AgentFunctionApp, AgentFunctionApp) + self.assertIs(func.AgentDFApp, AgentDFApp) @patch('azure.functions.decorators._agents.importlib.import_module') def test_missing_provider_reports_extension_install(self, import_module): @@ -192,7 +193,7 @@ def test_missing_durable_reports_provider_extra(self, load_agents_base): ) with self.assertRaisesRegex(ImportError, expected_distribution): - DurableAIApp(provider='agent_framework') + AgentDFApp(provider='agent_framework') if __name__ == '__main__':