Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.venv
__pycache__
*.pyc
*.pyo
*.pyd
.Python
.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
FOUNDRY_PROJECT_ENDPOINT="..."
AZURE_AI_MODEL_DEPLOYMENT_NAME="..."
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
FROM python:3.12-slim

WORKDIR /app

# .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.
RUN apt-get update \
&& apt-get install -y --no-install-recommends wget ca-certificates libicu-dev \
&& wget -qO /tmp/dotnet-install.sh https://dot.net/v1/dotnet-install.sh \
&& chmod +x /tmp/dotnet-install.sh \
&& /tmp/dotnet-install.sh --runtime dotnet --channel 10.0 --install-dir /usr/share/dotnet \
&& ln -s /usr/share/dotnet/dotnet /usr/local/bin/dotnet \
&& rm -f /tmp/dotnet-install.sh \
&& rm -rf /var/lib/apt/lists/*
Comment thread
TaoChenOSU marked this conversation as resolved.

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"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
# 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).

> [!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.
Comment thread
TaoChenOSU marked this conversation as resolved.
Outdated

## 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.

## Option 1: Azure Developer CLI (`azd`)
Comment thread
TaoChenOSU marked this conversation as resolved.
Outdated

### Prerequisites

1. **Azure Developer CLI (`azd`)** — [Install azd](https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/install-azd)
2. Install the AI agent extension:
```bash
azd ext install microsoft.foundry
```
3. Authenticate:
```bash
azd auth login
```

### Initialize the agent project

No cloning required. Create a new folder and initialize from the manifest:

```bash
mkdir my-declarative-agent && cd my-declarative-agent

azd ai agent init -m https://github.com/microsoft-foundry/foundry-samples/blob/main/samples/python/hosted-agents/agent-framework/responses/09-declarative-customer-support/agent.manifest.yaml
```

Follow the prompts to configure your Foundry project and model deployment. If you don't have an existing Foundry project, `azd ai agent init` will guide you through creating one.

### Provision Azure resources (if needed)

If you don't already have a Foundry project and model deployment:

```bash
azd provision
```

### Run the agent locally

```bash
azd ai agent run
```

The agent host will start on `http://localhost:8088`.

### Invoke the local agent

In a separate terminal, from the project directory:

```bash
azd ai agent invoke --local "I have a problem"
```

A typical multi-turn session:

```bash
azd ai agent invoke --local "I have a problem"
# → "Could you tell me a bit more about what's going on?"

azd ai agent invoke --local "My laptop won't turn on"
# → "Connecting you with technical support..."
# → TechSupportAgent: "Let's start simple — is the charger LED on when plugged in?"

azd ai agent invoke --local "Yes the LED is on"
# → TechSupportAgent: "Great. Try a hard reset: hold the power button for 30 seconds..."
```

Or for billing:

```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?"
```

### Deploy to Foundry

Once tested locally, deploy to Microsoft Foundry:

```bash
azd deploy
```

For the full deployment guide, see [Deploy a hosted agent](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent).

### Invoke the deployed agent

```bash
azd ai agent invoke "I have a problem"
```

## Option 2: VS Code (Foundry Toolkit)

### Prerequisites

1. **VS Code** with the **[Foundry Toolkit](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.azure-ai-foundry)** extension installed.
2. Sign in to Azure in VS Code.

### Create the project

1. Open the Command Palette (`Ctrl+Shift+P`) and run **Foundry Toolkit: Create Hosted Agent**.
2. Select this sample from the gallery. The extension scaffolds the project into a new workspace and generates `agent.yaml`, `.env`, and `.vscode/tasks.json` + `launch.json` automatically.
3. Complete the **Foundry Project Setup** to pick the subscription and Foundry project (or create a new one).

### Run and debug the agent

Press **F5** to start the agent in debug mode. The agent host will start on `http://localhost:8088`.

### Test with Agent Inspector

1. Open the Command Palette (`Ctrl+Shift+P`) and run **Foundry Toolkit: Open Agent Inspector**.
2. The Inspector connects to the running agent. Send messages to chat and view streamed responses.

### Deploy to Foundry

1. Open the Command Palette (`Ctrl+Shift+P`) and run **Foundry Toolkit: Deploy Hosted Agent**. The extension opens a **Deploy Hosted Agent** wizard and reads `agent.yaml` to auto-populate settings.
2. If prompted, complete **Foundry Project Setup** to select subscription and project.
3. On the **Basics** tab, choose **Container** as the deployment method (this sample requires it — see the note above) and confirm the agent name.
4. On **Review + Deploy**, confirm runtime details, pick **CPU and Memory** size, and click **Deploy**.
5. After deployment, invoke the agent in the Agent Playground and stream live logs from the **Logs** tab.

## Next steps

- [Quickstart: Create a hosted agent](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent) — end-to-end walkthrough using `azd`
- [Manage hosted agents](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/manage-hosted-agent) — monitor and manage deployed agents
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
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}
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
# Copyright (c) Microsoft. All rights reserved.

import os
from pathlib import Path
from typing import 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 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={"response_format": TriageResponse, "store": False},
)
tech_support_agent = Agent(
client=client,
name="TechSupportAgent",
instructions=TECH_SUPPORT_INSTRUCTIONS,
default_options={"store": False},
)
billing_agent = Agent(
client=client,
name="BillingAgent",
instructions=BILLING_INSTRUCTIONS,
default_options={"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()
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
agent-framework-declarative
agent-framework-foundry
agent-framework-foundry-hosting>=1.0.0a260630

# debugpy enables local debugging of this agent with the Foundry Toolkit VS Code extension.
debugpy
Loading
Loading