mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
[BREAKING] Python: Add InvokeFunctionTool action for declarative workflows (#3716)
* add(declarative): Declarative workflow InvokeFunctionTool feature * Cleanup * Address PR feedback * Remove InvokeTool kind, consolidate to InvokeFunctionTool * Fix sample locations * pin azure-ai-projects to 2.0.0b3 due to breaking changes
This commit is contained in:
committed by
GitHub
Unverified
parent
f77f40b987
commit
40d2fac29c
@@ -127,16 +127,18 @@ Orchestration-focused samples (Sequential, Concurrent, Handoff, GroupChat, Magen
|
||||
|
||||
YAML-based declarative workflows allow you to define multi-agent orchestration patterns without writing Python code. See the [declarative workflows README](./declarative/README.md) for more details on YAML workflow syntax and available actions.
|
||||
|
||||
| Sample | File | Concepts |
|
||||
| -------------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------- |
|
||||
| Conditional Workflow | [declarative/conditional_workflow/](./declarative/conditional_workflow/) | Nested conditional branching based on user input |
|
||||
| Customer Support | [declarative/customer_support/](./declarative/customer_support/) | Multi-agent customer support with routing |
|
||||
| Deep Research | [declarative/deep_research/](./declarative/deep_research/) | Research workflow with planning, searching, and synthesis |
|
||||
| Function Tools | [declarative/function_tools/](./declarative/function_tools/) | Invoking Python functions from declarative workflows |
|
||||
| Human-in-Loop | [declarative/human_in_loop/](./declarative/human_in_loop/) | Interactive workflows that request user input |
|
||||
| Marketing | [declarative/marketing/](./declarative/marketing/) | Marketing content generation workflow |
|
||||
| Simple Workflow | [declarative/simple_workflow/](./declarative/simple_workflow/) | Basic workflow with variable setting, conditionals, and loops |
|
||||
| Student Teacher | [declarative/student_teacher/](./declarative/student_teacher/) | Student-teacher interaction pattern |
|
||||
| Sample | File | Concepts |
|
||||
|---|---|---|
|
||||
| Agent to Function Tool | [declarative/agent_to_function_tool/](./declarative/agent_to_function_tool/) | Chain agent output to InvokeFunctionTool actions |
|
||||
| Conditional Workflow | [declarative/conditional_workflow/](./declarative/conditional_workflow/) | Nested conditional branching based on user input |
|
||||
| Customer Support | [declarative/customer_support/](./declarative/customer_support/) | Multi-agent customer support with routing |
|
||||
| Deep Research | [declarative/deep_research/](./declarative/deep_research/) | Research workflow with planning, searching, and synthesis |
|
||||
| Function Tools | [declarative/function_tools/](./declarative/function_tools/) | Invoking Python functions from declarative workflows |
|
||||
| Human-in-Loop | [declarative/human_in_loop/](./declarative/human_in_loop/) | Interactive workflows that request user input |
|
||||
| Invoke Function Tool | [declarative/invoke_function_tool/](./declarative/invoke_function_tool/) | Call registered Python functions with InvokeFunctionTool |
|
||||
| Marketing | [declarative/marketing/](./declarative/marketing/) | Marketing content generation workflow |
|
||||
| Simple Workflow | [declarative/simple_workflow/](./declarative/simple_workflow/) | Basic workflow with variable setting, conditionals, and loops |
|
||||
| Student Teacher | [declarative/student_teacher/](./declarative/student_teacher/) | Student-teacher interaction pattern |
|
||||
|
||||
### resources
|
||||
|
||||
|
||||
@@ -69,6 +69,9 @@ actions:
|
||||
- `InvokeAzureAgent` - Call an Azure AI agent
|
||||
- `InvokePromptAgent` - Call a local prompt agent
|
||||
|
||||
### Tool Invocation
|
||||
- `InvokeFunctionTool` - Call a registered Python function
|
||||
|
||||
### Human-in-Loop
|
||||
- `Question` - Request user input
|
||||
- `WaitForInput` - Pause for external input
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
"""Agent to Function Tool sample - demonstrates chaining agent output to function tools.
|
||||
|
||||
This sample shows how to:
|
||||
1. Use InvokeAzureAgent to analyze user input with an AI model
|
||||
2. Pass the agent's structured output to InvokeFunctionTool actions
|
||||
3. Chain multiple function tools to process and transform data
|
||||
|
||||
The workflow:
|
||||
1. Takes a user order request as input
|
||||
2. Uses an Azure agent to extract structured order data (item, quantity, details)
|
||||
3. Passes the extracted data to a function tool that calculates the order total
|
||||
4. Uses another function tool to format the final confirmation message
|
||||
|
||||
Run with:
|
||||
python -m samples.03-workflows.declarative.agent_to_function_tool.main
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from agent_framework.azure import AzureOpenAIChatClient
|
||||
from agent_framework.declarative import WorkflowFactory
|
||||
from azure.identity import AzureCliCredential
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
# Pricing data for the order calculation
|
||||
ITEM_PRICES = {
|
||||
"pizza": {"small": 10.99, "medium": 14.99, "large": 18.99, "default": 14.99},
|
||||
"burger": {"small": 6.99, "medium": 8.99, "large": 10.99, "default": 8.99},
|
||||
"salad": {"small": 7.99, "medium": 9.99, "large": 11.99, "default": 9.99},
|
||||
"sandwich": {"small": 6.99, "medium": 8.99, "large": 10.99, "default": 8.99},
|
||||
"pasta": {"small": 11.99, "medium": 14.99, "large": 17.99, "default": 14.99},
|
||||
}
|
||||
|
||||
EXTRAS_PRICES = {
|
||||
"extra cheese": 2.00,
|
||||
"bacon": 2.50,
|
||||
"avocado": 1.50,
|
||||
"mushrooms": 1.00,
|
||||
"pepperoni": 2.00,
|
||||
}
|
||||
|
||||
# Agent instructions for order analysis
|
||||
ORDER_ANALYSIS_INSTRUCTIONS = """You are an order analysis assistant. Analyze the customer's order request and extract:
|
||||
- item: what they want to order (e.g., "pizza", "burger", "salad")
|
||||
- quantity: how many (as a number, default to 1 if not specified)
|
||||
- details: any special requests, modifications, or size (e.g., "large", "extra cheese")
|
||||
- delivery_address: where to deliver (if mentioned, otherwise empty string)
|
||||
|
||||
Always respond with valid JSON matching the required format."""
|
||||
|
||||
|
||||
# Pydantic model for structured agent output
|
||||
class OrderAnalysis(BaseModel):
|
||||
"""Structured output from the order analysis agent."""
|
||||
|
||||
item: str = Field(description="The food item being ordered (e.g., pizza, burger)")
|
||||
quantity: int = Field(description="Number of items ordered", default=1)
|
||||
details: str = Field(description="Special requests, size, or modifications")
|
||||
delivery_address: str = Field(description="Delivery address if provided, empty string otherwise", default="")
|
||||
|
||||
|
||||
def calculate_order_total(order_data: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Calculate the total cost of an order based on the agent's structured analysis.
|
||||
|
||||
Args:
|
||||
order_data: Structured dict from the agent containing order analysis.
|
||||
|
||||
Returns:
|
||||
Dictionary with pricing breakdown.
|
||||
"""
|
||||
# Handle case where order_data might be None or invalid
|
||||
if not order_data or not isinstance(order_data, dict):
|
||||
return {
|
||||
"error": f"Invalid order data: {order_data}",
|
||||
"subtotal": 0.0,
|
||||
"tax": 0.0,
|
||||
"delivery_fee": 0.0,
|
||||
"total": 0.0,
|
||||
}
|
||||
|
||||
item = str(order_data.get("item", "")).lower()
|
||||
quantity = int(order_data.get("quantity", 1))
|
||||
details = str(order_data.get("details", "")).lower()
|
||||
has_delivery = bool(order_data.get("delivery_address"))
|
||||
|
||||
# Determine size from details
|
||||
size = "default"
|
||||
for s in ["small", "medium", "large"]:
|
||||
if s in details:
|
||||
size = s
|
||||
break
|
||||
|
||||
# Get base price for item
|
||||
item_key = None
|
||||
for key in ITEM_PRICES:
|
||||
if key in item:
|
||||
item_key = key
|
||||
break
|
||||
|
||||
unit_price = ITEM_PRICES[item_key].get(size, ITEM_PRICES[item_key]["default"]) if item_key else 12.99
|
||||
|
||||
# Calculate extras
|
||||
extras_total = 0.0
|
||||
applied_extras: list[dict[str, Any]] = []
|
||||
for extra, price in EXTRAS_PRICES.items():
|
||||
if extra in details:
|
||||
extras_total += price * quantity
|
||||
applied_extras.append({"name": extra, "price": price})
|
||||
|
||||
# Calculate totals
|
||||
subtotal = (unit_price * quantity) + extras_total
|
||||
tax = round(subtotal * 0.08, 2) # 8% tax
|
||||
delivery_fee = 5.00 if has_delivery else 0.0
|
||||
total = round(subtotal + tax + delivery_fee, 2)
|
||||
|
||||
return {
|
||||
"item": item,
|
||||
"quantity": quantity,
|
||||
"size": size if size != "default" else "regular",
|
||||
"unit_price": unit_price,
|
||||
"extras": applied_extras,
|
||||
"extras_total": extras_total,
|
||||
"subtotal": round(subtotal, 2),
|
||||
"tax": tax,
|
||||
"delivery_fee": delivery_fee,
|
||||
"total": total,
|
||||
"has_delivery": has_delivery,
|
||||
}
|
||||
|
||||
|
||||
def format_order_confirmation(order_data: dict[str, Any], order_calculation: dict[str, Any]) -> str:
|
||||
"""Format a human-readable order confirmation message.
|
||||
|
||||
Args:
|
||||
order_data: Structured dict from the agent with order details.
|
||||
order_calculation: Pricing calculation from calculate_order_total.
|
||||
|
||||
Returns:
|
||||
Formatted confirmation message.
|
||||
"""
|
||||
calc = order_calculation
|
||||
|
||||
# Handle error case
|
||||
if "error" in calc:
|
||||
return f"Sorry, we couldn't process your order: {calc['error']}"
|
||||
|
||||
# Build the confirmation message
|
||||
qty = int(calc.get("quantity", 1))
|
||||
size = calc.get("size", "regular").title()
|
||||
item = calc.get("item", "item").title()
|
||||
lines = [
|
||||
"=" * 50,
|
||||
"ORDER CONFIRMATION",
|
||||
"=" * 50,
|
||||
"",
|
||||
f"Item: {qty}x {size} {item}",
|
||||
f"Unit Price: ${calc.get('unit_price', 0):.2f}",
|
||||
]
|
||||
|
||||
# Add extras if any
|
||||
extras = calc.get("extras", [])
|
||||
if extras:
|
||||
lines.append("\nExtras:")
|
||||
for extra in extras:
|
||||
lines.append(f" + {extra['name'].title()}: ${extra['price']:.2f} each")
|
||||
lines.append(f" Extras Total: ${calc.get('extras_total', 0):.2f}")
|
||||
|
||||
lines.extend([
|
||||
"",
|
||||
"-" * 30,
|
||||
f"Subtotal: ${calc.get('subtotal', 0):.2f}",
|
||||
f"Tax (8%): ${calc.get('tax', 0):.2f}",
|
||||
])
|
||||
|
||||
if calc.get("has_delivery"):
|
||||
delivery_address = order_data.get("delivery_address", "Address provided") if order_data else "Address provided"
|
||||
lines.extend([
|
||||
f"Delivery Fee: ${calc.get('delivery_fee', 0):.2f}",
|
||||
f"Delivery To: {delivery_address}",
|
||||
])
|
||||
|
||||
lines.extend([
|
||||
"-" * 30,
|
||||
f"TOTAL: ${calc.get('total', 0):.2f}",
|
||||
"=" * 50,
|
||||
"",
|
||||
"Thank you for your order!",
|
||||
])
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
async def main():
|
||||
"""Run the agent to function tool workflow."""
|
||||
# Create Azure OpenAI client
|
||||
chat_client = AzureOpenAIChatClient(credential=AzureCliCredential())
|
||||
|
||||
# Create the order analysis agent with structured output
|
||||
order_analysis_agent = chat_client.as_agent(
|
||||
name="OrderAnalysisAgent",
|
||||
instructions=ORDER_ANALYSIS_INSTRUCTIONS,
|
||||
default_options={"response_format": OrderAnalysis},
|
||||
)
|
||||
|
||||
# Agent registry
|
||||
agents = {
|
||||
"OrderAnalysisAgent": order_analysis_agent,
|
||||
}
|
||||
|
||||
# Get the path to the workflow YAML file
|
||||
workflow_path = Path(__file__).parent / "workflow.yaml"
|
||||
|
||||
# Create the workflow factory with agents and tools
|
||||
factory = (
|
||||
WorkflowFactory(agents=agents)
|
||||
.register_tool("calculate_order_total", calculate_order_total)
|
||||
.register_tool("format_order_confirmation", format_order_confirmation)
|
||||
)
|
||||
|
||||
# Create the workflow from the YAML definition
|
||||
workflow = factory.create_workflow_from_yaml_path(workflow_path)
|
||||
|
||||
print("=" * 60)
|
||||
print("Agent to Function Tool Workflow Demo")
|
||||
print("=" * 60)
|
||||
print()
|
||||
print("This workflow demonstrates:")
|
||||
print(" 1. Using InvokeAzureAgent to analyze user input")
|
||||
print(" 2. Passing agent's structured output to InvokeFunctionTool")
|
||||
print(" 3. Chaining multiple function tools together")
|
||||
print()
|
||||
|
||||
# Test with different order inputs
|
||||
test_queries = [
|
||||
"I want to order 3 large pizzas with extra cheese for delivery to 123 Main St",
|
||||
"2 medium burgers with bacon please",
|
||||
"Can I get a small salad with avocado and mushrooms, pick up",
|
||||
]
|
||||
|
||||
for query in test_queries:
|
||||
print("-" * 60)
|
||||
print(f"Input: {query}")
|
||||
print("-" * 60)
|
||||
|
||||
# Run the workflow with streaming to capture output
|
||||
try:
|
||||
async for event in workflow.run(query, stream=True):
|
||||
if event.type == "output" and isinstance(event.data, str):
|
||||
print(event.data, end="", flush=True)
|
||||
except Exception as e:
|
||||
print(f"\nWorkflow error: {type(e).__name__}: {e}")
|
||||
|
||||
print("\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,56 @@
|
||||
# Agent to Function Tool Workflow
|
||||
#
|
||||
# This workflow demonstrates chaining an agent invocation with a function tool.
|
||||
# The agent analyzes user input, and the function tool processes the agent's output.
|
||||
#
|
||||
# Flow:
|
||||
# 1. Receive user query
|
||||
# 2. Invoke an Azure agent to analyze the query and extract structured data
|
||||
# 3. Pass the agent's structured output to a function tool for processing
|
||||
# 4. Return the final result
|
||||
#
|
||||
# Example input:
|
||||
# I want to order 3 large pizzas with extra cheese for delivery to 123 Main St
|
||||
|
||||
kind: Workflow
|
||||
trigger:
|
||||
|
||||
kind: OnConversationStart
|
||||
id: agent_to_function_tool_demo
|
||||
actions:
|
||||
|
||||
# Invoke the order analysis agent to extract structured order data
|
||||
- kind: InvokeAzureAgent
|
||||
id: analyze_order
|
||||
agent:
|
||||
name: OrderAnalysisAgent
|
||||
input:
|
||||
messages: =Workflow.Inputs.input
|
||||
output:
|
||||
response: Local.agentResponse
|
||||
responseObject: Local.orderData
|
||||
|
||||
# Invoke a function tool to calculate order total using the agent's output
|
||||
- kind: InvokeFunctionTool
|
||||
id: calculate_order
|
||||
functionName: calculate_order_total
|
||||
arguments:
|
||||
order_data: =Local.orderData
|
||||
output:
|
||||
result: Local.orderCalculation
|
||||
|
||||
# Invoke another function tool to format the final confirmation
|
||||
- kind: InvokeFunctionTool
|
||||
id: format_confirmation
|
||||
functionName: format_order_confirmation
|
||||
arguments:
|
||||
order_data: =Local.orderData
|
||||
order_calculation: =Local.orderCalculation
|
||||
output:
|
||||
result: Local.confirmation
|
||||
|
||||
# Send the final confirmation to the user
|
||||
- kind: SendActivity
|
||||
id: send_confirmation
|
||||
activity:
|
||||
text: =Local.confirmation
|
||||
@@ -0,0 +1,116 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
"""Invoke Function Tool sample - demonstrates InvokeFunctionTool workflow actions.
|
||||
|
||||
This sample shows how to:
|
||||
1. Define Python functions that can be called from workflows
|
||||
2. Register functions with WorkflowFactory.register_tool()
|
||||
3. Use the InvokeFunctionTool action in YAML to invoke registered functions
|
||||
4. Pass arguments using expression syntax (=Local.variable)
|
||||
5. Capture function output in workflow variables
|
||||
|
||||
Run with:
|
||||
python -m samples.03-workflows.declarative.invoke_function_tool.main
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from agent_framework.declarative import WorkflowFactory
|
||||
|
||||
|
||||
# Define the function tools that will be registered with the workflow
|
||||
def get_weather(location: str, unit: str = "F") -> dict[str, Any]:
|
||||
"""Get weather information for a location.
|
||||
|
||||
This is a mock function that returns simulated weather data.
|
||||
In a real application, this would call a weather API.
|
||||
|
||||
Args:
|
||||
location: The city or location to get weather for.
|
||||
unit: Temperature unit ("F" for Fahrenheit, "C" for Celsius).
|
||||
|
||||
Returns:
|
||||
Dictionary with weather information.
|
||||
"""
|
||||
# Simulated weather data
|
||||
weather_data = {
|
||||
"Seattle": {"temp": 55, "condition": "rainy"},
|
||||
"New York": {"temp": 70, "condition": "partly cloudy"},
|
||||
"Los Angeles": {"temp": 85, "condition": "sunny"},
|
||||
"Chicago": {"temp": 60, "condition": "windy"},
|
||||
}
|
||||
|
||||
data = weather_data.get(location, {"temp": 72, "condition": "unknown"})
|
||||
|
||||
# Convert to Celsius if requested
|
||||
temp = data["temp"]
|
||||
if unit.upper() == "C":
|
||||
temp = round((temp - 32) * 5 / 9) # type: ignore
|
||||
|
||||
return {
|
||||
"location": location,
|
||||
"temp": temp,
|
||||
"unit": unit.upper(),
|
||||
"condition": data["condition"],
|
||||
}
|
||||
|
||||
|
||||
def format_message(template: str, data: dict[str, Any]) -> str:
|
||||
"""Format a message template with data.
|
||||
|
||||
Args:
|
||||
template: A string template with {key} placeholders.
|
||||
data: Dictionary of values to substitute.
|
||||
|
||||
Returns:
|
||||
Formatted message string.
|
||||
"""
|
||||
try:
|
||||
return template.format(**data)
|
||||
except KeyError as e:
|
||||
return f"Error formatting message: missing key {e}"
|
||||
|
||||
|
||||
async def main():
|
||||
"""Run the invoke function tool workflow."""
|
||||
# Get the path to the workflow YAML file
|
||||
workflow_path = Path(__file__).parent / "workflow.yaml"
|
||||
|
||||
# Create the workflow factory and register our tool functions
|
||||
factory = (
|
||||
WorkflowFactory().register_tool("get_weather", get_weather).register_tool("format_message", format_message)
|
||||
)
|
||||
|
||||
# Create the workflow from the YAML definition
|
||||
workflow = factory.create_workflow_from_yaml_path(workflow_path)
|
||||
|
||||
print("=" * 60)
|
||||
print("Invoke Function Tool Workflow Demo")
|
||||
print("=" * 60)
|
||||
|
||||
# Test with different inputs - both location and unit must be provided
|
||||
# as the workflow expects them in Workflow.Inputs
|
||||
test_inputs = [
|
||||
{"location": "Seattle", "unit": "F"},
|
||||
{"location": "New York", "unit": "C"},
|
||||
{"location": "Los Angeles", "unit": "F"},
|
||||
{"location": "Chicago", "unit": "C"},
|
||||
]
|
||||
|
||||
for inputs in test_inputs:
|
||||
print(f"\nInput: {inputs}")
|
||||
print("-" * 40)
|
||||
|
||||
# Run the workflow
|
||||
events = await workflow.run(inputs)
|
||||
|
||||
# Get the outputs
|
||||
outputs = events.get_outputs()
|
||||
for output in outputs:
|
||||
print(f"Output: {output}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,51 @@
|
||||
# Invoke Function Tool Workflow
|
||||
|
||||
name: invoke_function_tool_demo
|
||||
description: Demonstrates the InvokeFunctionTool action for invoking registered functions
|
||||
|
||||
actions:
|
||||
# Set up input location
|
||||
- kind: SetValue
|
||||
id: set_location
|
||||
path: Local.location
|
||||
value: =If(IsBlank(inputs.location), "Seattle", inputs.location)
|
||||
|
||||
# Set up temperature unit
|
||||
- kind: SetValue
|
||||
id: set_unit
|
||||
path: Local.unit
|
||||
value: =If(IsBlank(inputs.unit), "F", inputs.unit)
|
||||
|
||||
# Invoke the get_weather function tool
|
||||
- kind: InvokeFunctionTool
|
||||
id: invoke_weather
|
||||
functionName: get_weather
|
||||
arguments:
|
||||
location: =Local.location
|
||||
unit: =Local.unit
|
||||
output:
|
||||
messages: Local.weatherToolCallItems
|
||||
result: Local.weatherInfo
|
||||
autoSend: true
|
||||
|
||||
# Format a human-readable message using another function
|
||||
- kind: InvokeFunctionTool
|
||||
id: format_output
|
||||
functionName: format_message
|
||||
arguments:
|
||||
template: "The weather in {location} is {temp}°{unit}"
|
||||
data: =Local.weatherInfo
|
||||
output:
|
||||
result: Local.formattedMessage
|
||||
|
||||
# Output the result
|
||||
- kind: SendActivity
|
||||
id: send_weather
|
||||
activity:
|
||||
text: =Local.formattedMessage
|
||||
|
||||
# Store the result in workflow outputs
|
||||
- kind: SetValue
|
||||
id: set_output
|
||||
path: Workflow.Outputs.weather
|
||||
value: =Local.weatherInfo
|
||||
Reference in New Issue
Block a user