Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ A supported Python version is required - see

## Available extensions
* [Base extension](azurefunctions-extensions-base/README.md)
* [Agent provider base](azurefunctions-agents-extensions-base/README.md)
* [Microsoft Agent Framework](azurefunctions-agents-extensions-agent-framework/README.md)
* [Azure Blob Storage bindings](azurefunctions-extensions-bindings-blob/README.md)
* [Azure Cosmos DB bindings](azurefunctions-extensions-bindings-cosmosdb/README.md)
* [Azure Event Hubs bindings](azurefunctions-extensions-bindings-eventhub/README.md)
Expand Down
21 changes: 21 additions & 0 deletions azurefunctions-agents-extensions-agent-framework/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) Microsoft Corporation.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
recursive-include azurefunctions *.py *.pyi
recursive-include tests *.py
include LICENSE README.md
157 changes: 157 additions & 0 deletions azurefunctions-agents-extensions-agent-framework/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
# Azure Functions Microsoft Agent Framework Extension

Inject Microsoft Agent Framework Agents built from raw `.agent.md` instructions
into Python Azure Functions.

## Install

```text
pip install azurefunctions-agents-extensions-agent-framework
```

Install Durable Functions support with the durable extra:

```text
pip install "azurefunctions-agents-extensions-agent-framework[durable]"
```


Install remote MCP transport and Entra support with the MCP extra:

```text
pip install "azurefunctions-agents-extensions-agent-framework[mcp]"
```

## Use an Agent app

Create a zero-argument factory that returns a fresh MAF chat client. A new
client and Agent context are created and closed for every Function invocation.

```python
import azure.functions as func
from agent_framework import Agent
from azurefunctions.agents.extensions.agent_framework import AgentFunctionApp


def create_chat_client():
from agent_framework.openai import OpenAIChatClient

return OpenAIChatClient()


app = AgentFunctionApp(client_factory=create_chat_client)


@app.route(route="orders", methods=["POST"])
@app.markdown_agent(arg_name="agent", agent_name="orders")
async def process_order(req: func.HttpRequest, agent: Agent):
response = await agent.run(req.get_body().decode())
return response.text
```

`AgentFunctionApp` is owned by this extension and subclasses
`azure.functions.FunctionApp`. The Azure Functions SDK does not need Agent APIs
or modifications. One app uses the Microsoft Agent Framework provider selected
by this package.

Place the complete instructions at `orders.agent.md` or
`agents/orders.agent.md`. The file is raw UTF-8 text; no front matter or runtime
configuration is interpreted.

## Skills and MCP servers

Skills and MCP servers are discovered automatically from the app root and are
available to each Agent binding by default:

```text
skills/inventory/SKILL.md
mcp.json
```

`SKILL.md` uses Agent Skills frontmatter:

```markdown
---
name: inventory
description: Look up inventory policy and warehouse constraints.
---

Use the references in this skill when assessing stock.
```

The base extension discovers Skill directory paths without reading their
contents. Microsoft Agent Framework parses and validates each `SKILL.md` when
it loads the file-based Skills provider.

V1 MCP discovery supports remote HTTP transports only:

```json
{
"servers": {
"inventory": {
"type": "streamable-http",
"url": "$INVENTORY_MCP_URL",
"tools": ["lookup_stock", "reserve_stock"],
"headers": {"X-Tenant": "%TENANT_ID%"},
"auth": {
"scope": "$INVENTORY_MCP_SCOPE",
"client_id": "%AZURE_CLIENT_ID%"
}
}
}
}
```

`$VAR` and `%VAR%` references are resolved for each invocation, not during
discovery. Missing values fail before connecting. Servers configured with
headers or Entra authentication must use HTTPS; HTTP is accepted only for
loopback development. Exposed MCP tool names are prefixed with the server name
to prevent collisions between servers. Credentials, tokens, HTTP clients, MCP
tools, and Agents are fresh invocation-owned resources and are closed on
success, error, or cancellation. Do not place secrets directly in
source-controlled `mcp.json`; use environment references.

Every Agent in the Function App receives all valid Skills and MCP servers
discovered from the app root:

```python
from azurefunctions.agents.extensions.agent_framework import AgentFunctionApp

app = AgentFunctionApp(client_factory=create_chat_client)


@app.markdown_agent(arg_name="agent", agent_name="orders")
async def process_order(agent: Agent):
...
```

V1 has no app-level or per-binding capability selectors. Skill scripts and MCP
tools can perform privileged operations, so placing a definition under the app
root grants every Agent in that app access to it. Use separate Function Apps
when capabilities require isolation. Python `tools=` remain explicit because
they are supplied directly to the Microsoft Agent Framework Agent.

The constructor and decorator expose only `client_factory` and explicit Python
`tools` in V1. The extension owns the Agent client, name, instructions, and
discovered Skills/MCP integration. Configure `app_root` only when constructing
`AgentFunctionApp`; decorators do not override it.

## Durable Agents

Durable orchestration support is optional:

```text
pip install "azurefunctions-agents-extensions-agent-framework[durable]"
```

Use `AgentFunctionApp` and call `context.call_agent(agent_name, input_)` inside a
synchronous generator orchestrator. Agent execution is isolated in an activity
so replay performs no nondeterministic work. Importing the package remains safe
without Durable installed; using a Durable decorator requires the `[durable]`
extra.

All `call_agent()` invocations use the provider configured by `AgentFunctionApp`.
They also use the app-level `skills` and `mcp_servers` defaults. V1 does not
support selecting another provider or capability set from an orchestrator, and
the schema-v1 orchestration payload contains no capability paths, settings, or
secrets.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__path__ = __import__("pkgutil").extend_path(__path__, __name__)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__path__ = __import__("pkgutil").extend_path(__path__, __name__)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__path__ = __import__("pkgutil").extend_path(__path__, __name__)
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from azurefunctions.agents.extensions.base.durable import DurableAgentContext

from .apps import AgentFunctionApp
from .provider import AGENT_FRAMEWORK_PROVIDER_ID, ClientFactory

__all__ = [
"AGENT_FRAMEWORK_PROVIDER_ID",
"AgentFunctionApp",
"ClientFactory",
"DurableAgentContext",
]

__version__ = '1.0.0b1'
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
from __future__ import annotations

import os
from collections.abc import Callable, Sequence
from typing import Any, TypeVar

import azure.functions as func
from agent_framework import ToolTypes

from azurefunctions.agents.extensions.base import (
configure_app,
durable_orchestration_trigger,
)
from azurefunctions.agents.extensions.base import markdown_agent as base_markdown_agent

from .provider import AGENT_FRAMEWORK_PROVIDER_ID, ClientFactory

_F = TypeVar("_F", bound=Callable[..., Any])


def _provider_options(
*,
client_factory: ClientFactory | None = None,
tools: (
ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None
) = None,
) -> dict[str, object]:
options: dict[str, object] = {}
if client_factory is not None:
options["client_factory"] = client_factory
if tools is not None:
options["tools"] = tools
return options


class _AgentFrameworkAppMixin:
def markdown_agent(
self,
*,
arg_name: str,
agent_name: str,
client_factory: ClientFactory | None = None,
tools: (
ToolTypes
| Callable[..., Any]
| Sequence[ToolTypes | Callable[..., Any]]
| None
) = None,
) -> Callable[[_F], _F]:
return base_markdown_agent(
self,
provider=AGENT_FRAMEWORK_PROVIDER_ID,
arg_name=arg_name,
agent_name=agent_name,
**_provider_options(client_factory=client_factory, tools=tools),
)


class AgentFunctionApp(
_AgentFrameworkAppMixin,
func.FunctionApp,
):
"""Azure Functions app configured for Microsoft Agent Framework Agents."""

def __init__(
self,
*,
client_factory: ClientFactory,
app_root: str | os.PathLike[str] | None = None,
tools: (
ToolTypes
| Callable[..., Any]
| Sequence[ToolTypes | Callable[..., Any]]
| None
) = None,
http_auth_level: func.AuthLevel | str = func.AuthLevel.FUNCTION,
) -> None:
super().__init__(
http_auth_level=http_auth_level,
)
configure_app(
self,
provider=AGENT_FRAMEWORK_PROVIDER_ID,
app_root=app_root,
provider_options=_provider_options(
client_factory=client_factory,
tools=tools,
),
)

def orchestration_trigger(
self,
context_name: str,
orchestration: str | None = None,
input_type: type | None = None,
) -> Callable[..., Any]:
return durable_orchestration_trigger(
self,
sdk_decorator=super().orchestration_trigger,
context_name=context_name,
orchestration=orchestration,
input_type=input_type,
)
Loading
Loading