mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: Added MCP tool support for azure functions package. (#2385)
* Python: Add Scaffolding for Durable AzureFunctions package to Agent Framework (#1823) * Add scafolding * update readme * add code owners and label * update owners * .NET: Durable extension: initial src and unit tests (#1900) * Python: Add Durable Agent Wrapper code (#1913) * add initial changes * Move code and add single sample * Update logger * Remove unused code * address PR comments * cleanup code and address comments --------- Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> * Azure Functions .NET samples (#1939) * Python: Add Unit tests for Azurefunctions package (#1976) * Add Unit tests for Azurefunctions * remove duplicate import * .NET: [Feature Branch] Migrate state schema updates and support for agents as MCP tools (#1979) * Python: Add more samples for Azure Functions (#1980) * Move all samples * fix comments * remove dead lines * Make samples simpler * Agents as MCP tools * Removed unused files and updated sample * .NET: [Feature Branch] Durable Task extension integration tests (#2017) * .NET: [Feature Branch] Update OpenAI config for integration tests (#2063) * Python: Add Integration tests for AzureFunctions (#2020) * Add Integration tests * Remove DTS extension * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Add pyi file for type safety * Add samples in readme * Updated all readme instructions * Address comments * Update readmes * Fix requirements * Address comments --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Addressed copilot feedback * Minor refactoring and added tests * Updated mcp sample * Fixed broken link in readme * Addressed copilot comments * Addressed feedback * Updated property to enable_mcp_tool_trigger --------- Co-authored-by: Laveesh Rohra <larohra@microsoft.com> Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> Co-authored-by: Chris Gillum <cgillum@microsoft.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
b8260ae24c
commit
b1c210c9d8
@@ -9,6 +9,7 @@ with Azure Durable Entities, enabling stateful and durable AI agent execution.
|
||||
import json
|
||||
import re
|
||||
from collections.abc import Callable, Mapping
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Any, TypeVar, cast
|
||||
|
||||
import azure.durable_functions as df
|
||||
@@ -39,6 +40,22 @@ logger = get_logger("agent_framework.azurefunctions")
|
||||
EntityHandler = Callable[[df.DurableEntityContext], None]
|
||||
HandlerT = TypeVar("HandlerT", bound=Callable[..., Any])
|
||||
|
||||
|
||||
@dataclass
|
||||
class AgentMetadata:
|
||||
"""Metadata for a registered agent.
|
||||
|
||||
Attributes:
|
||||
agent: The agent instance implementing AgentProtocol
|
||||
http_endpoint_enabled: Whether HTTP endpoint is enabled for this agent
|
||||
mcp_tool_enabled: Whether MCP tool endpoint is enabled for this agent
|
||||
"""
|
||||
|
||||
agent: AgentProtocol
|
||||
http_endpoint_enabled: bool
|
||||
mcp_tool_enabled: bool
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
class DFAppBase:
|
||||
@@ -56,6 +73,15 @@ if TYPE_CHECKING:
|
||||
|
||||
def activity_trigger(self, input_name: str) -> Callable[[HandlerT], HandlerT]: ...
|
||||
|
||||
def mcp_tool_trigger(
|
||||
self,
|
||||
arg_name: str,
|
||||
tool_name: str,
|
||||
description: str,
|
||||
tool_properties: str,
|
||||
data_type: func.DataType,
|
||||
) -> Callable[[HandlerT], HandlerT]: ...
|
||||
|
||||
else:
|
||||
DFAppBase = df.DFApp # type: ignore[assignment]
|
||||
|
||||
@@ -117,14 +143,15 @@ class AgentFunctionApp(DFAppBase):
|
||||
agents: Dictionary of agent name to AgentProtocol instance
|
||||
enable_health_check: Whether health check endpoint is enabled
|
||||
enable_http_endpoints: Whether HTTP endpoints are created for agents
|
||||
enable_mcp_tool_trigger: Whether MCP tool triggers are created for agents
|
||||
max_poll_retries: Maximum polling attempts when waiting for responses
|
||||
poll_interval_seconds: Delay (seconds) between polling attempts
|
||||
"""
|
||||
|
||||
agents: dict[str, AgentProtocol]
|
||||
_agent_metadata: dict[str, AgentMetadata]
|
||||
enable_health_check: bool
|
||||
enable_http_endpoints: bool
|
||||
agent_http_endpoint_flags: dict[str, bool]
|
||||
enable_mcp_tool_trigger: bool
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -134,6 +161,7 @@ class AgentFunctionApp(DFAppBase):
|
||||
enable_http_endpoints: bool = True,
|
||||
max_poll_retries: int = DEFAULT_MAX_POLL_RETRIES,
|
||||
poll_interval_seconds: float = DEFAULT_POLL_INTERVAL_SECONDS,
|
||||
enable_mcp_tool_trigger: bool = False,
|
||||
default_callback: AgentResponseCallbackProtocol | None = None,
|
||||
):
|
||||
"""Initialize the AgentFunctionApp.
|
||||
@@ -142,6 +170,8 @@ class AgentFunctionApp(DFAppBase):
|
||||
:param http_auth_level: HTTP authentication level (default: ``func.AuthLevel.FUNCTION``).
|
||||
:param enable_health_check: Enable the built-in health check endpoint (default: ``True``).
|
||||
:param enable_http_endpoints: Enable HTTP endpoints for agents (default: ``True``).
|
||||
:param enable_mcp_tool_trigger: Enable MCP tool triggers for agents (default: ``False``).
|
||||
When enabled, agents will be exposed as MCP tools that can be invoked by MCP-compatible clients.
|
||||
:param max_poll_retries: Maximum polling attempts when waiting for a response.
|
||||
Defaults to ``DEFAULT_MAX_POLL_RETRIES``.
|
||||
:param poll_interval_seconds: Delay in seconds between polling attempts.
|
||||
@@ -155,11 +185,11 @@ class AgentFunctionApp(DFAppBase):
|
||||
# Initialize parent DFApp
|
||||
super().__init__(http_auth_level=http_auth_level)
|
||||
|
||||
# Initialize agents dictionary
|
||||
self.agents = {}
|
||||
self.agent_http_endpoint_flags = {}
|
||||
# Initialize agent metadata dictionary
|
||||
self._agent_metadata = {}
|
||||
self.enable_health_check = enable_health_check
|
||||
self.enable_http_endpoints = enable_http_endpoints
|
||||
self.enable_mcp_tool_trigger = enable_mcp_tool_trigger
|
||||
self.default_callback = default_callback
|
||||
|
||||
try:
|
||||
@@ -186,11 +216,21 @@ class AgentFunctionApp(DFAppBase):
|
||||
|
||||
logger.debug("[AgentFunctionApp] Initialization complete")
|
||||
|
||||
@property
|
||||
def agents(self) -> dict[str, AgentProtocol]:
|
||||
"""Returns dict of agent names to agent instances.
|
||||
|
||||
Returns:
|
||||
Dictionary mapping agent names to their AgentProtocol instances.
|
||||
"""
|
||||
return {name: metadata.agent for name, metadata in self._agent_metadata.items()}
|
||||
|
||||
def add_agent(
|
||||
self,
|
||||
agent: AgentProtocol,
|
||||
callback: AgentResponseCallbackProtocol | None = None,
|
||||
enable_http_endpoint: bool | None = None,
|
||||
enable_mcp_tool_trigger: bool | None = None,
|
||||
) -> None:
|
||||
"""Add an agent to the function app after initialization.
|
||||
|
||||
@@ -198,8 +238,10 @@ class AgentFunctionApp(DFAppBase):
|
||||
agent: The Microsoft Agent Framework agent instance (must implement AgentProtocol)
|
||||
The agent must have a 'name' attribute.
|
||||
callback: Optional callback invoked during agent execution
|
||||
enable_http_endpoint: Optional flag that overrides the app-level
|
||||
HTTP endpoint setting for this agent
|
||||
enable_http_endpoint: Optional flag to enable/disable HTTP endpoint for this agent.
|
||||
The app level enable_http_endpoints setting will override this setting.
|
||||
enable_mcp_tool_trigger: Optional flag to enable/disable MCP tool trigger for this agent.
|
||||
The app level enable_mcp_tool_trigger setting will override this setting.
|
||||
|
||||
Raises:
|
||||
ValueError: If the agent doesn't have a 'name' attribute or if an agent
|
||||
@@ -210,12 +252,17 @@ class AgentFunctionApp(DFAppBase):
|
||||
if name is None:
|
||||
raise ValueError("Agent does not have a 'name' attribute. All agents must have a 'name' attribute.")
|
||||
|
||||
if name in self.agents:
|
||||
if name in self._agent_metadata:
|
||||
raise ValueError(f"Agent with name '{name}' is already registered. Each agent must have a unique name.")
|
||||
|
||||
effective_enable_http_endpoint = (
|
||||
self.enable_http_endpoints if enable_http_endpoint is None else self._coerce_to_bool(enable_http_endpoint)
|
||||
)
|
||||
effective_enable_mcp_endpoint = (
|
||||
self.enable_mcp_tool_trigger
|
||||
if enable_mcp_tool_trigger is None
|
||||
else self._coerce_to_bool(enable_mcp_tool_trigger)
|
||||
)
|
||||
|
||||
logger.debug(f"[AgentFunctionApp] Adding agent: {name}")
|
||||
logger.debug(f"[AgentFunctionApp] Route: /api/agents/{name}")
|
||||
@@ -224,17 +271,21 @@ class AgentFunctionApp(DFAppBase):
|
||||
"enabled" if effective_enable_http_endpoint else "disabled",
|
||||
name,
|
||||
)
|
||||
logger.debug(
|
||||
f"[AgentFunctionApp] MCP tool trigger: {'enabled' if effective_enable_mcp_endpoint else 'disabled'}"
|
||||
)
|
||||
|
||||
self.agents[name] = agent
|
||||
self.agent_http_endpoint_flags[name] = effective_enable_http_endpoint
|
||||
# Store agent metadata
|
||||
self._agent_metadata[name] = AgentMetadata(
|
||||
agent=agent,
|
||||
http_endpoint_enabled=effective_enable_http_endpoint,
|
||||
mcp_tool_enabled=effective_enable_mcp_endpoint,
|
||||
)
|
||||
|
||||
effective_callback = callback or self.default_callback
|
||||
|
||||
self._setup_agent_functions(
|
||||
agent,
|
||||
name,
|
||||
effective_callback,
|
||||
effective_enable_http_endpoint,
|
||||
agent, name, effective_callback, effective_enable_http_endpoint, effective_enable_mcp_endpoint
|
||||
)
|
||||
|
||||
logger.debug(f"[AgentFunctionApp] Agent '{name}' added successfully")
|
||||
@@ -258,7 +309,7 @@ class AgentFunctionApp(DFAppBase):
|
||||
"""
|
||||
normalized_name = str(agent_name)
|
||||
|
||||
if normalized_name not in self.agents:
|
||||
if normalized_name not in self._agent_metadata:
|
||||
raise ValueError(f"Agent '{normalized_name}' is not registered with this app.")
|
||||
|
||||
return DurableAIAgent(context, normalized_name)
|
||||
@@ -269,15 +320,16 @@ class AgentFunctionApp(DFAppBase):
|
||||
agent_name: str,
|
||||
callback: AgentResponseCallbackProtocol | None,
|
||||
enable_http_endpoint: bool,
|
||||
enable_mcp_tool_trigger: bool,
|
||||
) -> None:
|
||||
"""Set up the HTTP trigger and entity for a specific agent.
|
||||
"""Set up the HTTP trigger, entity, and MCP tool trigger for a specific agent.
|
||||
|
||||
Args:
|
||||
agent: The agent instance
|
||||
agent_name: The name to use for routing and entity registration
|
||||
callback: Optional callback to receive response updates
|
||||
enable_http_endpoint: Whether the HTTP run route is enabled for
|
||||
this agent
|
||||
enable_http_endpoint: Whether to create HTTP endpoint
|
||||
enable_mcp_tool_trigger: Whether to create MCP tool trigger
|
||||
"""
|
||||
logger.debug(f"[AgentFunctionApp] Setting up functions for agent '{agent_name}'...")
|
||||
|
||||
@@ -290,6 +342,12 @@ class AgentFunctionApp(DFAppBase):
|
||||
)
|
||||
self._setup_agent_entity(agent, agent_name, callback)
|
||||
|
||||
if enable_mcp_tool_trigger:
|
||||
agent_description = agent.description
|
||||
self._setup_mcp_tool_trigger(agent_name, agent_description)
|
||||
else:
|
||||
logger.debug(f"[AgentFunctionApp] MCP tool trigger disabled for agent '{agent_name}'")
|
||||
|
||||
def _setup_http_run_route(self, agent_name: str) -> None:
|
||||
"""Register the POST route that triggers agent execution.
|
||||
|
||||
@@ -448,6 +506,159 @@ class AgentFunctionApp(DFAppBase):
|
||||
entity_function.__name__ = entity_name_with_prefix
|
||||
self.entity_trigger(context_name="context", entity_name=entity_name_with_prefix)(entity_function)
|
||||
|
||||
def _setup_mcp_tool_trigger(self, agent_name: str, agent_description: str | None) -> None:
|
||||
"""Register an MCP tool trigger for an agent using Azure Functions native MCP support.
|
||||
|
||||
This creates a native Azure Functions MCP tool trigger that exposes the agent
|
||||
as an MCP tool, allowing it to be invoked by MCP-compatible clients.
|
||||
|
||||
Args:
|
||||
agent_name: The agent name (used as the MCP tool name)
|
||||
agent_description: Optional description for the MCP tool (shown to clients)
|
||||
"""
|
||||
mcp_function_name = self._build_function_name(agent_name, "mcptool")
|
||||
|
||||
# Define tool properties as JSON (MCP tool parameters)
|
||||
tool_properties = json.dumps([
|
||||
{
|
||||
"propertyName": "query",
|
||||
"propertyType": "string",
|
||||
"description": "The query to send to the agent.",
|
||||
"isRequired": True,
|
||||
"isArray": False,
|
||||
},
|
||||
{
|
||||
"propertyName": "threadId",
|
||||
"propertyType": "string",
|
||||
"description": "Optional thread identifier for conversation continuity.",
|
||||
"isRequired": False,
|
||||
"isArray": False,
|
||||
},
|
||||
])
|
||||
|
||||
function_name_decorator = self.function_name(mcp_function_name)
|
||||
mcp_tool_decorator = self.mcp_tool_trigger(
|
||||
arg_name="context",
|
||||
tool_name=agent_name,
|
||||
description=agent_description or f"Interact with {agent_name} agent",
|
||||
tool_properties=tool_properties,
|
||||
data_type=func.DataType.UNDEFINED,
|
||||
)
|
||||
durable_client_decorator = self.durable_client_input(client_name="client")
|
||||
|
||||
@function_name_decorator
|
||||
@mcp_tool_decorator
|
||||
@durable_client_decorator
|
||||
async def mcp_tool_handler(context: str, client: df.DurableOrchestrationClient) -> str:
|
||||
"""Handle MCP tool invocation for the agent.
|
||||
|
||||
Args:
|
||||
context: MCP tool invocation context containing arguments (query, threadId)
|
||||
client: Durable orchestration client for entity communication
|
||||
|
||||
Returns:
|
||||
Agent response text
|
||||
"""
|
||||
logger.debug("[MCP Tool Trigger] Received invocation for agent: %s", agent_name)
|
||||
return await self._handle_mcp_tool_invocation(agent_name=agent_name, context=context, client=client)
|
||||
|
||||
logger.debug("[AgentFunctionApp] Registered MCP tool trigger for agent: %s", agent_name)
|
||||
|
||||
async def _handle_mcp_tool_invocation(
|
||||
self, agent_name: str, context: str, client: df.DurableOrchestrationClient
|
||||
) -> str:
|
||||
"""Handle an MCP tool invocation.
|
||||
|
||||
This method processes MCP tool requests and delegates to the agent entity.
|
||||
|
||||
Args:
|
||||
agent_name: Name of the agent being invoked
|
||||
context: MCP tool invocation context as a JSON string
|
||||
client: Durable orchestration client
|
||||
|
||||
Returns:
|
||||
Agent response text
|
||||
|
||||
Raises:
|
||||
ValueError: If required arguments are missing or context is invalid JSON
|
||||
RuntimeError: If agent execution fails
|
||||
"""
|
||||
logger.debug("[MCP Tool Handler] Processing invocation for agent '%s'", agent_name)
|
||||
|
||||
# Parse JSON context string
|
||||
try:
|
||||
parsed_context = json.loads(context)
|
||||
except json.JSONDecodeError as e:
|
||||
raise ValueError(f"Invalid MCP context format: {e}") from e
|
||||
|
||||
# Extract arguments from MCP context
|
||||
arguments = parsed_context.get("arguments", {}) if isinstance(parsed_context, dict) else {}
|
||||
|
||||
# Validate required 'query' argument
|
||||
query = arguments.get("query")
|
||||
if not query or not isinstance(query, str):
|
||||
raise ValueError("MCP Tool invocation is missing required 'query' argument of type string.")
|
||||
|
||||
# Extract optional threadId
|
||||
thread_id = arguments.get("threadId")
|
||||
|
||||
# Create or parse session ID
|
||||
if thread_id and isinstance(thread_id, str) and thread_id.strip():
|
||||
try:
|
||||
session_id = AgentSessionId.parse(thread_id)
|
||||
except ValueError as e:
|
||||
logger.warning(
|
||||
"Failed to parse AgentSessionId from thread_id '%s': %s. Falling back to new session ID.",
|
||||
thread_id,
|
||||
e,
|
||||
)
|
||||
session_id = AgentSessionId(name=agent_name, key=thread_id)
|
||||
else:
|
||||
# Generate new session ID
|
||||
session_id = AgentSessionId.with_random_key(agent_name)
|
||||
|
||||
# Build entity instance ID
|
||||
entity_instance_id = session_id.to_entity_id()
|
||||
|
||||
# Create run request
|
||||
correlation_id = self._generate_unique_id()
|
||||
run_request = self._build_request_data(
|
||||
req_body={"message": query, "role": "user"},
|
||||
message=query,
|
||||
thread_id=str(session_id),
|
||||
correlation_id=correlation_id,
|
||||
request_response_format=REQUEST_RESPONSE_FORMAT_TEXT,
|
||||
)
|
||||
|
||||
query_preview = query[:50] + "..." if len(query) > 50 else query
|
||||
logger.info("[MCP Tool] Invoking agent '%s' with query: %s", agent_name, query_preview)
|
||||
|
||||
# Signal entity to run agent
|
||||
await client.signal_entity(entity_instance_id, "run_agent", run_request)
|
||||
|
||||
# Poll for response (similar to HTTP handler)
|
||||
try:
|
||||
result = await self._get_response_from_entity(
|
||||
client=client,
|
||||
entity_instance_id=entity_instance_id,
|
||||
correlation_id=correlation_id,
|
||||
message=query,
|
||||
thread_id=str(session_id),
|
||||
)
|
||||
|
||||
# Extract and return response text
|
||||
if result.get("status") == "success":
|
||||
response_text = str(result.get("response", "No response"))
|
||||
logger.info("[MCP Tool] Agent '%s' responded successfully", agent_name)
|
||||
return response_text
|
||||
error_msg = result.get("error", "Unknown error")
|
||||
logger.error("[MCP Tool] Agent '%s' execution failed: %s", agent_name, error_msg)
|
||||
raise RuntimeError(f"Agent execution failed: {error_msg}")
|
||||
|
||||
except Exception as exc:
|
||||
logger.error("[MCP Tool] Error invoking agent '%s': %s", agent_name, exc, exc_info=True)
|
||||
raise
|
||||
|
||||
def _setup_health_route(self) -> None:
|
||||
"""Register the optional health check route."""
|
||||
health_route = self.route(route="health", methods=["GET"])
|
||||
@@ -458,16 +669,14 @@ class AgentFunctionApp(DFAppBase):
|
||||
agent_info = [
|
||||
{
|
||||
"name": name,
|
||||
"type": type(agent).__name__,
|
||||
"http_endpoint_enabled": self.agent_http_endpoint_flags.get(
|
||||
name,
|
||||
self.enable_http_endpoints,
|
||||
),
|
||||
"type": type(metadata.agent).__name__,
|
||||
"http_endpoint_enabled": metadata.http_endpoint_enabled,
|
||||
"mcp_tool_enabled": metadata.mcp_tool_enabled,
|
||||
}
|
||||
for name, agent in self.agents.items()
|
||||
for name, metadata in self._agent_metadata.items()
|
||||
]
|
||||
return func.HttpResponse(
|
||||
json.dumps({"status": "healthy", "agents": agent_info, "agent_count": len(self.agents)}),
|
||||
json.dumps({"status": "healthy", "agents": agent_info, "agent_count": len(self._agent_metadata)}),
|
||||
status_code=200,
|
||||
mimetype=MIMETYPE_APPLICATION_JSON,
|
||||
)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
"""Unit tests for AgentFunctionApp."""
|
||||
|
||||
import json
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import Any, TypeVar
|
||||
from unittest.mock import ANY, AsyncMock, Mock, patch
|
||||
@@ -87,7 +88,7 @@ class TestAgentFunctionAppInit:
|
||||
app.add_agent(mock_agent, callback=specific_callback)
|
||||
|
||||
setup_mock.assert_called_once()
|
||||
_, _, passed_callback, enable_http_endpoint = setup_mock.call_args[0]
|
||||
_, _, passed_callback, enable_http_endpoint, enable_mcp_tool_trigger = setup_mock.call_args[0]
|
||||
assert passed_callback is specific_callback
|
||||
assert enable_http_endpoint is True
|
||||
|
||||
@@ -103,7 +104,7 @@ class TestAgentFunctionAppInit:
|
||||
app.add_agent(mock_agent)
|
||||
|
||||
setup_mock.assert_called_once()
|
||||
_, _, passed_callback, enable_http_endpoint = setup_mock.call_args[0]
|
||||
_, _, passed_callback, enable_http_endpoint, enable_mcp_tool_trigger = setup_mock.call_args[0]
|
||||
assert passed_callback is default_callback
|
||||
assert enable_http_endpoint is True
|
||||
|
||||
@@ -118,7 +119,7 @@ class TestAgentFunctionAppInit:
|
||||
AgentFunctionApp(agents=[mock_agent], default_callback=default_callback)
|
||||
|
||||
setup_mock.assert_called_once()
|
||||
_, _, passed_callback, enable_http_endpoint = setup_mock.call_args[0]
|
||||
_, _, passed_callback, enable_http_endpoint, enable_mcp_tool_trigger = setup_mock.call_args[0]
|
||||
assert passed_callback is default_callback
|
||||
assert enable_http_endpoint is True
|
||||
|
||||
@@ -239,7 +240,7 @@ class TestAgentFunctionAppSetup:
|
||||
|
||||
http_route_mock.assert_called_once_with("OverrideAgent")
|
||||
agent_entity_mock.assert_called_once_with(mock_agent, "OverrideAgent", ANY)
|
||||
assert app.agent_http_endpoint_flags["OverrideAgent"] is True
|
||||
assert app._agent_metadata["OverrideAgent"].http_endpoint_enabled is True
|
||||
|
||||
def test_agent_override_disables_http_route_when_app_enabled(self) -> None:
|
||||
"""Agent-level override should disable HTTP route even when app enables it."""
|
||||
@@ -256,7 +257,7 @@ class TestAgentFunctionAppSetup:
|
||||
|
||||
http_route_mock.assert_not_called()
|
||||
agent_entity_mock.assert_called_once_with(mock_agent, "DisabledOverride", ANY)
|
||||
assert app.agent_http_endpoint_flags["DisabledOverride"] is False
|
||||
assert app._agent_metadata["DisabledOverride"].http_endpoint_enabled is False
|
||||
|
||||
def test_multiple_apps_independent(self) -> None:
|
||||
"""Test that multiple AgentFunctionApp instances are independent."""
|
||||
@@ -797,5 +798,271 @@ class TestHttpRunRoute:
|
||||
client.signal_entity.assert_not_called()
|
||||
|
||||
|
||||
class TestMCPToolEndpoint:
|
||||
"""Test suite for MCP tool endpoint functionality."""
|
||||
|
||||
def test_init_with_mcp_tool_endpoint_enabled(self) -> None:
|
||||
"""Test initialization with MCP tool endpoint enabled."""
|
||||
mock_agent = Mock()
|
||||
mock_agent.name = "TestAgent"
|
||||
|
||||
app = AgentFunctionApp(agents=[mock_agent], enable_mcp_tool_trigger=True)
|
||||
|
||||
assert app.enable_mcp_tool_trigger is True
|
||||
|
||||
def test_init_with_mcp_tool_endpoint_disabled(self) -> None:
|
||||
"""Test initialization with MCP tool endpoint disabled (default)."""
|
||||
mock_agent = Mock()
|
||||
mock_agent.name = "TestAgent"
|
||||
|
||||
app = AgentFunctionApp(agents=[mock_agent])
|
||||
|
||||
assert app.enable_mcp_tool_trigger is False
|
||||
|
||||
def test_add_agent_with_mcp_tool_trigger_enabled(self) -> None:
|
||||
"""Test adding an agent with MCP tool trigger explicitly enabled."""
|
||||
mock_agent = Mock()
|
||||
mock_agent.name = "MCPAgent"
|
||||
mock_agent.description = "Test MCP Agent"
|
||||
|
||||
with patch.object(AgentFunctionApp, "_setup_agent_functions") as setup_mock:
|
||||
app = AgentFunctionApp()
|
||||
app.add_agent(mock_agent, enable_mcp_tool_trigger=True)
|
||||
|
||||
setup_mock.assert_called_once()
|
||||
_, _, _, _, enable_mcp = setup_mock.call_args[0]
|
||||
assert enable_mcp is True
|
||||
|
||||
def test_add_agent_with_mcp_tool_trigger_disabled(self) -> None:
|
||||
"""Test adding an agent with MCP tool trigger explicitly disabled."""
|
||||
mock_agent = Mock()
|
||||
mock_agent.name = "NoMCPAgent"
|
||||
|
||||
with patch.object(AgentFunctionApp, "_setup_agent_functions") as setup_mock:
|
||||
app = AgentFunctionApp(enable_mcp_tool_trigger=True)
|
||||
app.add_agent(mock_agent, enable_mcp_tool_trigger=False)
|
||||
|
||||
setup_mock.assert_called_once()
|
||||
_, _, _, _, enable_mcp = setup_mock.call_args[0]
|
||||
assert enable_mcp is False
|
||||
|
||||
def test_agent_override_enables_mcp_when_app_disabled(self) -> None:
|
||||
"""Test that per-agent override can enable MCP when app-level is disabled."""
|
||||
mock_agent = Mock()
|
||||
mock_agent.name = "OverrideAgent"
|
||||
|
||||
with patch.object(AgentFunctionApp, "_setup_mcp_tool_trigger") as mcp_setup_mock:
|
||||
app = AgentFunctionApp(enable_mcp_tool_trigger=False)
|
||||
app.add_agent(mock_agent, enable_mcp_tool_trigger=True)
|
||||
|
||||
mcp_setup_mock.assert_called_once()
|
||||
|
||||
def test_agent_override_disables_mcp_when_app_enabled(self) -> None:
|
||||
"""Test that per-agent override can disable MCP when app-level is enabled."""
|
||||
mock_agent = Mock()
|
||||
mock_agent.name = "NoOverrideAgent"
|
||||
|
||||
with patch.object(AgentFunctionApp, "_setup_mcp_tool_trigger") as mcp_setup_mock:
|
||||
app = AgentFunctionApp(enable_mcp_tool_trigger=True)
|
||||
app.add_agent(mock_agent, enable_mcp_tool_trigger=False)
|
||||
|
||||
mcp_setup_mock.assert_not_called()
|
||||
|
||||
def test_setup_mcp_tool_trigger_registers_decorators(self) -> None:
|
||||
"""Test that _setup_mcp_tool_trigger registers the correct decorators."""
|
||||
mock_agent = Mock()
|
||||
mock_agent.name = "MCPToolAgent"
|
||||
mock_agent.description = "Test MCP Tool"
|
||||
|
||||
app = AgentFunctionApp()
|
||||
|
||||
# Mock the decorators
|
||||
with (
|
||||
patch.object(app, "function_name") as func_name_mock,
|
||||
patch.object(app, "mcp_tool_trigger") as mcp_trigger_mock,
|
||||
patch.object(app, "durable_client_input") as client_mock,
|
||||
):
|
||||
# Setup mock decorator chain
|
||||
func_name_mock.return_value = lambda f: f
|
||||
mcp_trigger_mock.return_value = lambda f: f
|
||||
client_mock.return_value = lambda f: f
|
||||
|
||||
app._setup_mcp_tool_trigger(mock_agent.name, mock_agent.description)
|
||||
|
||||
# Verify decorators were called with correct parameters
|
||||
func_name_mock.assert_called_once()
|
||||
mcp_trigger_mock.assert_called_once_with(
|
||||
arg_name="context",
|
||||
tool_name=mock_agent.name,
|
||||
description=mock_agent.description,
|
||||
tool_properties=ANY,
|
||||
data_type=func.DataType.UNDEFINED,
|
||||
)
|
||||
client_mock.assert_called_once_with(client_name="client")
|
||||
|
||||
def test_setup_mcp_tool_trigger_uses_default_description(self) -> None:
|
||||
"""Test that _setup_mcp_tool_trigger uses default description when none provided."""
|
||||
mock_agent = Mock()
|
||||
mock_agent.name = "NoDescAgent"
|
||||
|
||||
app = AgentFunctionApp()
|
||||
|
||||
with (
|
||||
patch.object(app, "function_name", return_value=lambda f: f),
|
||||
patch.object(app, "mcp_tool_trigger") as mcp_trigger_mock,
|
||||
patch.object(app, "durable_client_input", return_value=lambda f: f),
|
||||
):
|
||||
mcp_trigger_mock.return_value = lambda f: f
|
||||
|
||||
app._setup_mcp_tool_trigger(mock_agent.name, None)
|
||||
|
||||
# Verify default description was used
|
||||
call_args = mcp_trigger_mock.call_args
|
||||
assert call_args[1]["description"] == f"Interact with {mock_agent.name} agent"
|
||||
|
||||
async def test_handle_mcp_tool_invocation_with_json_string(self) -> None:
|
||||
"""Test _handle_mcp_tool_invocation with JSON string context."""
|
||||
mock_agent = Mock()
|
||||
mock_agent.name = "TestAgent"
|
||||
|
||||
app = AgentFunctionApp(agents=[mock_agent])
|
||||
client = AsyncMock()
|
||||
|
||||
# Mock the entity response
|
||||
mock_state = Mock()
|
||||
mock_state.entity_state = {
|
||||
"schemaVersion": "1.0.0",
|
||||
"data": {"conversationHistory": []},
|
||||
}
|
||||
client.read_entity_state.return_value = mock_state
|
||||
|
||||
# Create JSON string context
|
||||
context = '{"arguments": {"query": "test query", "threadId": "test-thread"}}'
|
||||
|
||||
with patch.object(app, "_get_response_from_entity") as get_response_mock:
|
||||
get_response_mock.return_value = {"status": "success", "response": "Test response"}
|
||||
|
||||
result = await app._handle_mcp_tool_invocation("TestAgent", context, client)
|
||||
|
||||
assert result == "Test response"
|
||||
get_response_mock.assert_called_once()
|
||||
|
||||
async def test_handle_mcp_tool_invocation_with_json_context(self) -> None:
|
||||
"""Test _handle_mcp_tool_invocation with JSON string context."""
|
||||
mock_agent = Mock()
|
||||
mock_agent.name = "TestAgent"
|
||||
|
||||
app = AgentFunctionApp(agents=[mock_agent])
|
||||
client = AsyncMock()
|
||||
|
||||
# Mock the entity response
|
||||
mock_state = Mock()
|
||||
mock_state.entity_state = {
|
||||
"schemaVersion": "1.0.0",
|
||||
"data": {"conversationHistory": []},
|
||||
}
|
||||
client.read_entity_state.return_value = mock_state
|
||||
|
||||
# Create JSON string context
|
||||
context = json.dumps({"arguments": {"query": "test query", "threadId": "test-thread"}})
|
||||
|
||||
with patch.object(app, "_get_response_from_entity") as get_response_mock:
|
||||
get_response_mock.return_value = {"status": "success", "response": "Test response"}
|
||||
|
||||
result = await app._handle_mcp_tool_invocation("TestAgent", context, client)
|
||||
|
||||
assert result == "Test response"
|
||||
get_response_mock.assert_called_once()
|
||||
|
||||
async def test_handle_mcp_tool_invocation_missing_query(self) -> None:
|
||||
"""Test _handle_mcp_tool_invocation raises ValueError when query is missing."""
|
||||
mock_agent = Mock()
|
||||
mock_agent.name = "TestAgent"
|
||||
|
||||
app = AgentFunctionApp(agents=[mock_agent])
|
||||
client = AsyncMock()
|
||||
|
||||
# Context missing query (as JSON string)
|
||||
context = json.dumps({"arguments": {}})
|
||||
|
||||
with pytest.raises(ValueError, match="missing required 'query' argument"):
|
||||
await app._handle_mcp_tool_invocation("TestAgent", context, client)
|
||||
|
||||
async def test_handle_mcp_tool_invocation_invalid_json(self) -> None:
|
||||
"""Test _handle_mcp_tool_invocation raises ValueError for invalid JSON."""
|
||||
mock_agent = Mock()
|
||||
mock_agent.name = "TestAgent"
|
||||
|
||||
app = AgentFunctionApp(agents=[mock_agent])
|
||||
client = AsyncMock()
|
||||
|
||||
# Invalid JSON string
|
||||
context = "not valid json"
|
||||
|
||||
with pytest.raises(ValueError, match="Invalid MCP context format"):
|
||||
await app._handle_mcp_tool_invocation("TestAgent", context, client)
|
||||
|
||||
async def test_handle_mcp_tool_invocation_runtime_error(self) -> None:
|
||||
"""Test _handle_mcp_tool_invocation raises RuntimeError when agent fails."""
|
||||
mock_agent = Mock()
|
||||
mock_agent.name = "TestAgent"
|
||||
|
||||
app = AgentFunctionApp(agents=[mock_agent])
|
||||
client = AsyncMock()
|
||||
|
||||
# Mock the entity response
|
||||
mock_state = Mock()
|
||||
mock_state.entity_state = {
|
||||
"schemaVersion": "1.0.0",
|
||||
"data": {"conversationHistory": []},
|
||||
}
|
||||
client.read_entity_state.return_value = mock_state
|
||||
|
||||
context = '{"arguments": {"query": "test query"}}'
|
||||
|
||||
with patch.object(app, "_get_response_from_entity") as get_response_mock:
|
||||
get_response_mock.return_value = {"status": "failed", "error": "Agent error"}
|
||||
|
||||
with pytest.raises(RuntimeError, match="Agent execution failed"):
|
||||
await app._handle_mcp_tool_invocation("TestAgent", context, client)
|
||||
|
||||
def test_health_check_includes_mcp_tool_enabled(self) -> None:
|
||||
"""Test that health check endpoint includes mcp_tool_enabled field."""
|
||||
mock_agent = Mock()
|
||||
mock_agent.name = "HealthAgent"
|
||||
|
||||
app = AgentFunctionApp(agents=[mock_agent], enable_mcp_tool_trigger=True)
|
||||
|
||||
# Capture the health check handler function
|
||||
captured_handler = None
|
||||
|
||||
def capture_decorator(*args, **kwargs):
|
||||
def decorator(func):
|
||||
nonlocal captured_handler
|
||||
captured_handler = func
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
with patch.object(app, "route", side_effect=capture_decorator):
|
||||
app._setup_health_route()
|
||||
|
||||
# Verify we captured the handler
|
||||
assert captured_handler is not None
|
||||
|
||||
# Call the health handler
|
||||
request = Mock()
|
||||
response = captured_handler(request)
|
||||
|
||||
# Verify response includes mcp_tool_enabled
|
||||
import json
|
||||
|
||||
body = json.loads(response.get_body().decode("utf-8"))
|
||||
assert "agents" in body
|
||||
assert len(body["agents"]) == 1
|
||||
assert "mcp_tool_enabled" in body["agents"][0]
|
||||
assert body["agents"][0]["mcp_tool_enabled"] is True
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v", "--tb=short"])
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
# Agent as MCP Tool Sample
|
||||
|
||||
This sample demonstrates how to configure AI agents to be accessible as both HTTP endpoints and [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) tools, enabling flexible integration patterns for AI agent consumption.
|
||||
|
||||
## Key Concepts Demonstrated
|
||||
|
||||
- **Multi-trigger Agent Configuration**: Configure agents to support HTTP triggers, MCP tool triggers, or both
|
||||
- **Microsoft Agent Framework Integration**: Use the framework to define AI agents with specific roles and capabilities
|
||||
- **Flexible Agent Registration**: Register agents with customizable trigger configurations
|
||||
- **MCP Server Hosting**: Expose agents as MCP tools for consumption by MCP-compatible clients
|
||||
|
||||
## Sample Architecture
|
||||
|
||||
This sample creates three agents with different trigger configurations:
|
||||
|
||||
| Agent | Role | HTTP Trigger | MCP Tool Trigger | Description |
|
||||
|-------|------|--------------|------------------|-------------|
|
||||
| **Joker** | Comedy specialist | ✅ Enabled | ❌ Disabled | Accessible only via HTTP requests |
|
||||
| **StockAdvisor** | Financial data | ❌ Disabled | ✅ Enabled | Accessible only as MCP tool |
|
||||
| **PlantAdvisor** | Indoor plant recommendations | ✅ Enabled | ✅ Enabled | Accessible via both HTTP and MCP |
|
||||
|
||||
## Environment Setup
|
||||
|
||||
See the [README.md](../README.md) file in the parent directory for complete setup instructions, including:
|
||||
|
||||
- Prerequisites installation
|
||||
- Azure OpenAI configuration
|
||||
- Durable Task Scheduler setup
|
||||
- Storage emulator configuration
|
||||
|
||||
## Configuration
|
||||
|
||||
Update your `local.settings.json` with your Azure OpenAI credentials:
|
||||
|
||||
```json
|
||||
{
|
||||
"Values": {
|
||||
"AZURE_OPENAI_ENDPOINT": "https://your-resource.openai.azure.com/",
|
||||
"AZURE_OPENAI_CHAT_DEPLOYMENT_NAME": "your-deployment-name",
|
||||
"AZURE_OPENAI_KEY": "your-api-key-if-not-using-rbac"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Running the Sample
|
||||
|
||||
1. **Start the Function App**:
|
||||
```bash
|
||||
cd python/samples/getting_started/azure_functions/08_mcp_server
|
||||
func start
|
||||
```
|
||||
|
||||
2. **Note the MCP Server Endpoint**: When the app starts, you'll see the MCP server endpoint in the terminal output. It will look like:
|
||||
```
|
||||
MCP server endpoint: http://localhost:7071/runtime/webhooks/mcp
|
||||
```
|
||||
|
||||
## Testing MCP Tool Integration
|
||||
|
||||
### Using MCP Inspector
|
||||
|
||||
1. Install the [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector)
|
||||
2. Connect using the MCP server endpoint from your terminal output
|
||||
3. Select **"Streamable HTTP"** as the transport method
|
||||
4. Test the available MCP tools:
|
||||
- `StockAdvisor` - Available only as MCP tool
|
||||
- `PlantAdvisor` - Available as both HTTP and MCP tool
|
||||
|
||||
### Using Other MCP Clients
|
||||
|
||||
Any MCP-compatible client can connect to the server endpoint and utilize the exposed agent tools. The agents will appear as callable tools within the MCP protocol.
|
||||
|
||||
## Testing HTTP Endpoints
|
||||
|
||||
For agents with HTTP triggers enabled (Joker and PlantAdvisor), you can test them using curl:
|
||||
|
||||
```bash
|
||||
# Test Joker agent (HTTP only)
|
||||
curl -X POST http://localhost:7071/api/agents/Joker/run \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"message": "Tell me a joke"}'
|
||||
|
||||
# Test PlantAdvisor agent (HTTP and MCP)
|
||||
curl -X POST http://localhost:7071/api/agents/PlantAdvisor/run \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"message": "Recommend an indoor plant"}'
|
||||
```
|
||||
|
||||
Note: StockAdvisor does not have HTTP endpoints and is only accessible via MCP tool triggers.
|
||||
|
||||
## Expected Output
|
||||
|
||||
**HTTP Responses** will be returned directly to your HTTP client.
|
||||
|
||||
**MCP Tool Responses** will be visible in:
|
||||
- The terminal where `func start` is running
|
||||
- Your MCP client interface
|
||||
- The DTS dashboard at `http://localhost:8080` (if using Durable Task Scheduler)
|
||||
|
||||
## Health Check
|
||||
|
||||
Check the health endpoint to see which agents have which triggers enabled:
|
||||
|
||||
```bash
|
||||
curl http://localhost:7071/api/health
|
||||
```
|
||||
|
||||
Expected response:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "healthy",
|
||||
"agents": [
|
||||
{
|
||||
"name": "Joker",
|
||||
"type": "Agent",
|
||||
"http_endpoint_enabled": true,
|
||||
"mcp_tool_enabled": false
|
||||
},
|
||||
{
|
||||
"name": "StockAdvisor",
|
||||
"type": "Agent",
|
||||
"http_endpoint_enabled": false,
|
||||
"mcp_tool_enabled": true
|
||||
},
|
||||
{
|
||||
"name": "PlantAdvisor",
|
||||
"type": "Agent",
|
||||
"http_endpoint_enabled": true,
|
||||
"mcp_tool_enabled": true
|
||||
}
|
||||
],
|
||||
"agent_count": 3
|
||||
}
|
||||
```
|
||||
|
||||
## Code Structure
|
||||
|
||||
The sample shows how to enable MCP tool triggers with flexible agent configuration:
|
||||
|
||||
```python
|
||||
from agent_framework.azure import AgentFunctionApp, AzureOpenAIChatClient
|
||||
|
||||
# Create Azure OpenAI Chat Client
|
||||
chat_client = AzureOpenAIChatClient()
|
||||
|
||||
# Define agents with different roles
|
||||
joker_agent = chat_client.create_agent(
|
||||
name="Joker",
|
||||
instructions="You are good at telling jokes.",
|
||||
)
|
||||
|
||||
stock_agent = chat_client.create_agent(
|
||||
name="StockAdvisor",
|
||||
instructions="Check stock prices.",
|
||||
)
|
||||
|
||||
plant_agent = chat_client.create_agent(
|
||||
name="PlantAdvisor",
|
||||
instructions="Recommend plants.",
|
||||
description="Get plant recommendations.",
|
||||
)
|
||||
|
||||
# Create the AgentFunctionApp
|
||||
app = AgentFunctionApp(enable_health_check=True)
|
||||
|
||||
# Configure agents with different trigger combinations:
|
||||
# HTTP trigger only (default)
|
||||
app.add_agent(joker_agent)
|
||||
|
||||
# MCP tool trigger only (HTTP disabled)
|
||||
app.add_agent(stock_agent, enable_http_endpoint=False, enable_mcp_tool_trigger=True)
|
||||
|
||||
# Both HTTP and MCP tool triggers enabled
|
||||
app.add_agent(plant_agent, enable_http_endpoint=True, enable_mcp_tool_trigger=True)
|
||||
```
|
||||
|
||||
This automatically creates the following endpoints based on agent configuration:
|
||||
- `POST /api/agents/{AgentName}/run` - HTTP endpoint (when `enable_http_endpoint=True`)
|
||||
- MCP tool triggers for agents with `enable_mcp_tool_trigger=True`
|
||||
- `GET /api/health` - Health check endpoint showing agent configurations
|
||||
|
||||
## Learn More
|
||||
|
||||
- [Model Context Protocol Documentation](https://modelcontextprotocol.io/)
|
||||
- [Microsoft Agent Framework Documentation](https://github.com/microsoft/agent-framework)
|
||||
- [Azure Functions Documentation](https://learn.microsoft.com/azure/azure-functions/)
|
||||
@@ -0,0 +1,63 @@
|
||||
"""
|
||||
Example showing how to configure AI agents with different trigger configurations.
|
||||
|
||||
This sample demonstrates how to configure agents to be accessible as both HTTP endpoints
|
||||
and Model Context Protocol (MCP) tools, enabling flexible integration patterns for AI agent
|
||||
consumption.
|
||||
|
||||
Key concepts demonstrated:
|
||||
- Multi-trigger Agent Configuration: Configure agents to support HTTP triggers, MCP tool triggers, or both
|
||||
- Microsoft Agent Framework Integration: Use the framework to define AI agents with specific roles
|
||||
- Flexible Agent Registration: Register agents with customizable trigger configurations
|
||||
|
||||
This sample creates three agents with different trigger configurations:
|
||||
- Joker: HTTP trigger only (default)
|
||||
- StockAdvisor: MCP tool trigger only (HTTP disabled)
|
||||
- PlantAdvisor: Both HTTP and MCP tool triggers enabled
|
||||
|
||||
Required environment variables:
|
||||
- AZURE_OPENAI_ENDPOINT: Your Azure OpenAI endpoint
|
||||
- AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: Your Azure OpenAI deployment name
|
||||
|
||||
Authentication uses AzureCliCredential (Azure Identity).
|
||||
"""
|
||||
|
||||
from agent_framework.azure import AgentFunctionApp, AzureOpenAIChatClient
|
||||
|
||||
# Create Azure OpenAI Chat Client
|
||||
# This uses AzureCliCredential for authentication (requires 'az login')
|
||||
chat_client = AzureOpenAIChatClient()
|
||||
|
||||
# Define three AI agents with different roles
|
||||
# Agent 1: Joker - HTTP trigger only (default)
|
||||
agent1 = chat_client.create_agent(
|
||||
name="Joker",
|
||||
instructions="You are good at telling jokes.",
|
||||
)
|
||||
|
||||
# Agent 2: StockAdvisor - MCP tool trigger only
|
||||
agent2 = chat_client.create_agent(
|
||||
name="StockAdvisor",
|
||||
instructions="Check stock prices.",
|
||||
)
|
||||
|
||||
# Agent 3: PlantAdvisor - Both HTTP and MCP tool triggers
|
||||
agent3 = chat_client.create_agent(
|
||||
name="PlantAdvisor",
|
||||
instructions="Recommend plants.",
|
||||
description="Get plant recommendations.",
|
||||
)
|
||||
|
||||
# Create the AgentFunctionApp with selective trigger configuration
|
||||
app = AgentFunctionApp(
|
||||
enable_health_check=True,
|
||||
)
|
||||
|
||||
# Agent 1: HTTP trigger only (default)
|
||||
app.add_agent(agent1)
|
||||
|
||||
# Agent 2: Disable HTTP trigger, enable MCP tool trigger only
|
||||
app.add_agent(agent2, enable_http_endpoint=False, enable_mcp_tool_trigger=True)
|
||||
|
||||
# Agent 3: Enable both HTTP and MCP tool triggers
|
||||
app.add_agent(agent3, enable_http_endpoint=True, enable_mcp_tool_trigger=True)
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"version": "2.0",
|
||||
"extensionBundle": {
|
||||
"id": "Microsoft.Azure.Functions.ExtensionBundle",
|
||||
"version": "[4.*, 5.0.0)"
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"IsEncrypted": false,
|
||||
"Values": {
|
||||
"FUNCTIONS_WORKER_RUNTIME": "python",
|
||||
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
|
||||
"DURABLE_TASK_SCHEDULER_CONNECTION_STRING": "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None",
|
||||
"AZURE_OPENAI_ENDPOINT": "<AZURE_OPENAI_ENDPOINT>",
|
||||
"AZURE_OPENAI_CHAT_DEPLOYMENT_NAME": "<AZURE_OPENAI_CHAT_DEPLOYMENT_NAME>"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
agent-framework-azurefunctions
|
||||
azure-identity
|
||||
Reference in New Issue
Block a user