-
Notifications
You must be signed in to change notification settings - Fork 2k
Python: Add FHA declarative workflow sample #6897
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
TaoChenOSU
wants to merge
3
commits into
main
Choose a base branch
from
taochen/add-fha-declarative-wf-sample
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
7 changes: 7 additions & 0 deletions
7
.../04-hosting/foundry-hosted-agents/responses/13_declarative_customer_support/.dockerignore
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| .venv | ||
| __pycache__ | ||
| *.pyc | ||
| *.pyo | ||
| *.pyd | ||
| .Python | ||
| .env |
2 changes: 2 additions & 0 deletions
2
...s/04-hosting/foundry-hosted-agents/responses/13_declarative_customer_support/.env.example
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| FOUNDRY_PROJECT_ENDPOINT="..." | ||
| AZURE_AI_MODEL_DEPLOYMENT_NAME="..." |
33 changes: 33 additions & 0 deletions
33
...les/04-hosting/foundry-hosted-agents/responses/13_declarative_customer_support/Dockerfile
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| # .NET 10 runtime is required by the `powerfx` package, which | ||
| # agent-framework-declarative uses to evaluate `=...` expressions in the | ||
| # workflow YAML (e.g. =Local.Triage.NeedsClarification). Without it, | ||
| # ConditionGroup evaluation raises and the workflow produces no output. | ||
| FROM mcr.microsoft.com/dotnet/runtime:10.0 AS dotnet | ||
|
|
||
| FROM python:3.12-slim | ||
|
|
||
| WORKDIR /app | ||
|
|
||
| # libicu is required by the .NET runtime for globalization support. | ||
| RUN apt-get update \ | ||
| && apt-get install -y --no-install-recommends ca-certificates libicu-dev \ | ||
| && rm -rf /var/lib/apt/lists/* | ||
|
|
||
| # Copy the pinned .NET runtime from the official image. | ||
| COPY --from=dotnet /usr/share/dotnet /usr/share/dotnet | ||
| RUN ln -s /usr/share/dotnet/dotnet /usr/local/bin/dotnet | ||
|
|
||
| ENV DOTNET_ROOT=/usr/share/dotnet | ||
|
|
||
| COPY . user_agent/ | ||
| WORKDIR /app/user_agent | ||
|
|
||
| RUN if [ -f requirements.txt ]; then \ | ||
| pip install -r requirements.txt; \ | ||
| else \ | ||
| echo "No requirements.txt found"; \ | ||
| fi | ||
|
|
||
| EXPOSE 8088 | ||
|
|
||
| CMD ["python", "main.py"] | ||
55 changes: 55 additions & 0 deletions
55
...sting/foundry-hosted-agents/responses/13_declarative_customer_support/README.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| # What this sample demonstrates | ||
|
|
||
| A realistic **multi-turn** [Agent Framework](https://github.com/microsoft/agent-framework) **declarative workflow** — defined entirely in YAML — hosted using the **Responses protocol**. It shows how a declarative workflow that invokes multiple Foundry-hosted agents can run end-to-end on every user turn while reading the prior conversation through `Conversation.messages` (populated automatically by `Workflow.as_agent()`). | ||
|
|
||
| > Read more about declarative workflows in the [Agent Framework documentation](https://learn.microsoft.com/en-us/agent-framework/workflows/declarative/?pivots=programming-language-python) and about workflow-as-an-agent in the [Workflow as an Agent documentation](https://learn.microsoft.com/en-us/agent-framework/workflows/as-agents?pivots=programming-language-python). | ||
|
|
||
| ## How It Works | ||
|
|
||
| ### The Workflow | ||
|
|
||
| [`workflow.yaml`](workflow.yaml) describes a customer-support triage flow: | ||
|
|
||
| 1. `InvokeAzureAgent: TriageAgent` — looks at the full conversation so far and emits a structured `TriageResponse` (`Category`, `NeedsClarification`, `ClarificationQuestion`, `Reply`). | ||
| 2. `ConditionGroup` routes on the triage decision: | ||
| - **NeedsClarification** → `SendActivity` asks one focused follow-up question and ends the turn. | ||
| - **Category = "Technical"** → `SendActivity` confirms the handoff, then `InvokeAzureAgent: TechSupportAgent` answers with `autoSend: true` so its reply streams directly to the caller. | ||
| - **Category = "Billing"** → same pattern, routed to `BillingAgent`. | ||
| - **else** → `SendActivity` returns the triage agent's `Reply` directly (good for greetings or general questions). | ||
|
|
||
| Each user message re-runs the workflow from the trigger. Because `Workflow.as_agent()` populates `Conversation.messages` with the prior turns of the conversation, every `InvokeAzureAgent` call sees the full history — which is what makes the triage decision and the specialist follow-ups coherent across turns. | ||
|
|
||
| ### Agent Hosting | ||
|
|
||
| [`main.py`](main.py) builds three `Agent` instances on top of a shared `FoundryChatClient` (one per workflow role), registers them with the `WorkflowFactory` so the YAML's `InvokeAzureAgent` actions can resolve them by name, loads the workflow, wraps it with `.as_agent(...)`, and hands the agent to `ResponsesHostServer`, which provisions a REST API endpoint compatible with the OpenAI Responses protocol. | ||
|
|
||
| The triage agent is configured with `response_format=TriageResponse` (a Pydantic model) so the workflow can read its structured fields via `Local.Triage.*`. The specialist agents are plain text and use `autoSend: true` to deliver their reply straight to the caller. | ||
|
|
||
| ## Running the Agent Host | ||
|
|
||
| Follow the instructions in the [Running the Agent Host Locally](../../README.md#running-the-agent-host-locally) section of the README in the parent directory to run the agent host. | ||
|
|
||
| ## Interacting with the agent | ||
|
|
||
| > Depending on how you run the agent host, you can invoke the agent using `curl` (`Invoke-WebRequest` in PowerShell) or `azd`. Please refer to the [parent README](../../README.md) for more details. Use this README for sample queries you can send to the agent. | ||
|
|
||
| Send a POST request to the server with a JSON body containing an `"input"` field to interact with the agent. For example: | ||
|
|
||
| ```bash | ||
| curl -X POST http://localhost:8088/responses -H "Content-Type: application/json" -d '{"input": "I have a problem"}' | ||
| ``` | ||
|
|
||
| Invoke with `azd`: | ||
|
|
||
| ```bash | ||
| azd ai agent invoke --local "I was double-charged this month" | ||
| # → "Connecting you with billing support..." | ||
| # → BillingAgent: "I'm sorry about that. Can you share the last 4 digits of the card on file?" | ||
| ``` | ||
|
|
||
| ## Deploying the Agent to Foundry | ||
|
|
||
| To host the agent on Foundry, follow the instructions in the [Deploying the Agent to Foundry](../../README.md#deploying-the-agent-to-foundry) section of the README in the parent directory. | ||
|
|
||
| > [!IMPORTANT] | ||
| > Deploy this sample as a **container** (not Code/ZIP). Its declarative workflow uses Power Fx, which needs the .NET runtime included in the `Dockerfile`. Choose **Container** in every deploy flow. |
25 changes: 25 additions & 0 deletions
25
...sting/foundry-hosted-agents/responses/13_declarative_customer_support/agent.manifest.yaml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| name: agent-framework-declarative-customer-support-responses | ||
| description: > | ||
| A multi-turn Agent Framework declarative (YAML-defined) customer-support | ||
| triage workflow hosted by Foundry. | ||
| metadata: | ||
| tags: | ||
| - Agent Framework | ||
| - AI Agent Hosting | ||
| - Azure AI AgentServer | ||
| - Responses Protocol | ||
| - Declarative Workflow | ||
| - Multi-turn | ||
| template: | ||
| name: agent-framework-declarative-customer-support-responses | ||
| kind: hosted | ||
| protocols: | ||
| - protocol: responses | ||
| version: 2.0.0 | ||
| environment_variables: | ||
| - name: AZURE_AI_MODEL_DEPLOYMENT_NAME | ||
| value: "{{AZURE_AI_MODEL_DEPLOYMENT_NAME}}" | ||
| resources: | ||
| - kind: model | ||
| id: gpt-5.4-mini | ||
| name: AZURE_AI_MODEL_DEPLOYMENT_NAME |
12 changes: 12 additions & 0 deletions
12
...les/04-hosting/foundry-hosted-agents/responses/13_declarative_customer_support/agent.yaml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| # yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml | ||
| kind: hosted | ||
| name: agent-framework-declarative-customer-support-responses | ||
| protocols: | ||
| - protocol: responses | ||
| version: 2.0.0 | ||
| resources: | ||
| cpu: '0.25' | ||
| memory: '0.5Gi' | ||
| environment_variables: | ||
| - name: AZURE_AI_MODEL_DEPLOYMENT_NAME | ||
| value: ${AZURE_AI_MODEL_DEPLOYMENT_NAME} |
145 changes: 145 additions & 0 deletions
145
...amples/04-hosting/foundry-hosted-agents/responses/13_declarative_customer_support/main.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,145 @@ | ||
| # Copyright (c) Microsoft. All rights reserved. | ||
|
|
||
| import os | ||
| from pathlib import Path | ||
| from typing import Any, Literal | ||
|
|
||
| from agent_framework import Agent | ||
| from agent_framework.foundry import FoundryChatClient | ||
| from agent_framework_declarative import WorkflowFactory | ||
| from agent_framework_foundry_hosting import ResponsesHostServer | ||
| from agent_framework_openai import OpenAIChatOptions | ||
| from azure.identity import DefaultAzureCredential | ||
| from dotenv import load_dotenv | ||
| from pydantic import BaseModel, Field | ||
|
|
||
| # Load environment variables from .env file | ||
| load_dotenv() | ||
|
|
||
|
|
||
| # --- Structured triage response -------------------------------------------------- | ||
|
|
||
|
|
||
| class TriageResponse(BaseModel): | ||
| """Triage decision produced from the conversation so far.""" | ||
|
|
||
| Category: Literal["Technical", "Billing", "General"] = Field( | ||
| description=( | ||
| "The best category for the user's request. " | ||
| "Use 'Technical' for hardware/software/network issues, " | ||
| "'Billing' for invoices/subscriptions/refunds, and " | ||
| "'General' for anything else (greetings, FAQs, small talk)." | ||
| ), | ||
| ) | ||
| NeedsClarification: bool = Field( | ||
| description=( | ||
| "True if you cannot confidently classify the request yet and " | ||
| "need to ask the user one focused follow-up question." | ||
| ), | ||
| ) | ||
| ClarificationQuestion: str = Field( | ||
| default="", | ||
| description=( | ||
| "A single, polite follow-up question to ask the user. " | ||
| "Required when NeedsClarification is true; otherwise empty." | ||
| ), | ||
| ) | ||
| Reply: str = Field( | ||
| default="", | ||
| description=( | ||
| "A natural-language reply to the user. Used when Category is 'General'; otherwise may be left empty." | ||
| ), | ||
| ) | ||
|
|
||
|
|
||
| # --- Agent instructions ---------------------------------------------------------- | ||
|
|
||
| TRIAGE_INSTRUCTIONS = """ | ||
| You are the front-line triage agent for a customer support workflow. | ||
|
|
||
| You will see the full conversation so far. Decide whether to: | ||
| - Ask the user one focused follow-up question (set NeedsClarification = true), or | ||
| - Route the conversation to the right specialist by setting Category, or | ||
| - Answer directly for general/small-talk requests via Reply. | ||
|
|
||
| Be efficient: do not ask a clarification if a category is already clear. | ||
| """.strip() | ||
|
|
||
| TECH_SUPPORT_INSTRUCTIONS = """ | ||
| You are a senior technical support specialist. The conversation history shows | ||
| what the user has told you so far and which steps were already attempted. | ||
|
|
||
| Provide one concrete next troubleshooting step at a time, then wait for the | ||
| user's response. Be concise and friendly. If the issue appears resolved, | ||
| congratulate the user and ask if there's anything else. | ||
| """.strip() | ||
|
|
||
| BILLING_INSTRUCTIONS = """ | ||
| You are a customer billing specialist. The conversation history shows what | ||
| the user has asked. | ||
|
|
||
| Help the user with invoice, subscription, refund, and payment-method | ||
| questions. If you need account details (e.g., last 4 of card, account email), | ||
| ask for them one at a time. Keep responses short and polite. | ||
| """.strip() | ||
|
|
||
|
|
||
| # --- Host setup ------------------------------------------------------------------ | ||
|
|
||
|
|
||
| def main() -> None: | ||
| workflow_path = Path(__file__).parent / "workflow.yaml" | ||
|
|
||
| client = FoundryChatClient( | ||
| project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], | ||
| model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"], | ||
| credential=DefaultAzureCredential(), | ||
| ) | ||
|
|
||
| # The workflow's InvokeAzureAgent actions reference these agents by name. | ||
| triage_agent = Agent( | ||
| client=client, | ||
| name="TriageAgent", | ||
| instructions=TRIAGE_INSTRUCTIONS, | ||
| default_options=OpenAIChatOptions[Any](response_format=TriageResponse, store=False), | ||
| ) | ||
| tech_support_agent = Agent( | ||
| client=client, | ||
| name="TechSupportAgent", | ||
| instructions=TECH_SUPPORT_INSTRUCTIONS, | ||
| default_options=OpenAIChatOptions(store=False), | ||
| ) | ||
| billing_agent = Agent( | ||
| client=client, | ||
| name="BillingAgent", | ||
| instructions=BILLING_INSTRUCTIONS, | ||
| default_options=OpenAIChatOptions(store=False), | ||
| ) | ||
|
|
||
| factory = WorkflowFactory( | ||
| agents={ | ||
| "TriageAgent": triage_agent, | ||
| "TechSupportAgent": tech_support_agent, | ||
| "BillingAgent": billing_agent, | ||
| }, | ||
| ) | ||
|
|
||
| workflow = factory.create_workflow_from_yaml_path(str(workflow_path)) | ||
|
|
||
| # Wrap the declarative workflow as an AIAgent so it can be served behind | ||
| # the Responses protocol. Each user turn re-runs the workflow with the | ||
| # full conversation history available via Conversation.messages. | ||
| workflow_agent = workflow.as_agent( | ||
| name="declarative-customer-support", | ||
| description=( | ||
| "A multi-turn customer-support triage workflow that routes " | ||
| "between technical and billing specialists based on the " | ||
| "conversation history." | ||
| ), | ||
| ) | ||
|
|
||
| ResponsesHostServer(workflow_agent).run() | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() |
7 changes: 7 additions & 0 deletions
7
...-hosting/foundry-hosted-agents/responses/13_declarative_customer_support/requirements.txt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| agent-framework-declarative | ||
| agent-framework-foundry | ||
| agent-framework-foundry-hosting>=1.0.0a260630 | ||
| agent-framework-openai | ||
|
|
||
| # debugpy enables local debugging of this agent with the Foundry Toolkit VS Code extension. | ||
| debugpy |
89 changes: 89 additions & 0 deletions
89
.../04-hosting/foundry-hosted-agents/responses/13_declarative_customer_support/workflow.yaml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| # A multi-turn customer-support triage workflow defined declaratively. | ||
| # | ||
| # Each user message re-runs the workflow with the full conversation history | ||
| # available to the invoked agents via Conversation.messages. The TriageAgent | ||
| # decides on every turn whether to ask a follow-up question, route to a | ||
| # technical specialist, route to a billing specialist, or close the ticket | ||
| # with a general response. | ||
|
|
||
| kind: Workflow | ||
| trigger: | ||
| kind: OnConversationStart | ||
| id: customer_support_triage | ||
| actions: | ||
|
|
||
| # Look at the full conversation so far and decide what to do. | ||
| # The agent's structured response drives the routing below. | ||
| # autoSend=false keeps the raw structured JSON out of the user-facing | ||
| # output stream — the ConditionGroup below is what produces the reply. | ||
| - kind: InvokeAzureAgent | ||
| id: triage | ||
| agent: | ||
| name: TriageAgent | ||
| output: | ||
| autoSend: false | ||
| responseObject: Local.Triage | ||
|
|
||
| # Branch on the triage decision. | ||
| - kind: ConditionGroup | ||
| id: route | ||
| conditions: | ||
|
|
||
| # Not enough information yet — ask the user a targeted follow-up. | ||
| - condition: =Local.Triage.NeedsClarification | ||
| id: ask_followup | ||
| actions: | ||
| - kind: SendActivity | ||
| id: send_followup | ||
| activity: | ||
| text: =Local.Triage.ClarificationQuestion | ||
| - kind: GotoAction | ||
| id: end_after_followup | ||
| actionId: all_done | ||
|
|
||
| # Technical issue — hand off to the technical specialist. | ||
| # autoSend streams the agent's reply directly to the caller. | ||
| - condition: =Local.Triage.Category = "Technical" | ||
| id: route_technical | ||
| actions: | ||
| - kind: SendActivity | ||
| id: log_technical | ||
| activity: | ||
| text: "Connecting you with technical support..." | ||
| - kind: InvokeAzureAgent | ||
| id: tech_support | ||
| agent: | ||
| name: TechSupportAgent | ||
| output: | ||
| autoSend: true | ||
| - kind: GotoAction | ||
| id: end_after_technical | ||
| actionId: all_done | ||
|
|
||
| # Billing issue — hand off to the billing specialist. | ||
| - condition: =Local.Triage.Category = "Billing" | ||
| id: route_billing | ||
| actions: | ||
| - kind: SendActivity | ||
| id: log_billing | ||
| activity: | ||
| text: "Connecting you with billing support..." | ||
| - kind: InvokeAzureAgent | ||
| id: billing_support | ||
| agent: | ||
| name: BillingAgent | ||
| output: | ||
| autoSend: true | ||
| - kind: GotoAction | ||
| id: end_after_billing | ||
| actionId: all_done | ||
|
|
||
| # Default: a general question or chit-chat — send the triage reply. | ||
| elseActions: | ||
| - kind: SendActivity | ||
| id: send_general | ||
| activity: | ||
| text: =Local.Triage.Reply | ||
|
|
||
| - kind: EndWorkflow | ||
| id: all_done |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.