Python: Improve python sample validation workflow (#7350)
* Add skill to replace hardcoded foundry project endpoint and model * Include more samples and fix migration samples part 1 * Fix migration samples * Replace Foundry hosted agent validation skill * Fix hosted agent file sample * Fix agent result format * Reorganize jobs * Update discovery heuristic for apps * Split agents into even more jobs * Add toolbox endpoint * Add more pre configured resources * Fix using deployed agent sample * Add sample status * Add playbook * Exclude hidden folder in sample discovery * Install autogen dependencies * Grant azure search RBAC role * Increase timeout for magentic * Build search resouce id deterministically * Remove grant in the workflow * Move azure cli login closer to when the sample actually runs * Refactor playbook * Fix using deployed agent sample * Actually save the playbooks * Fix action syntax error * Fix magentic sample * Address copilot comments * Fix link inspection * Address comments * Correct README * Fix playbook path * Remove trailing space
This commit is contained in:
@@ -14,6 +14,7 @@ from agent_framework import (
|
||||
)
|
||||
from agent_framework.foundry import FoundryChatClient
|
||||
from agent_framework.orchestrations import GroupChatRequestSentEvent, MagenticBuilder, MagenticProgressLedger
|
||||
from agent_framework_orchestrations import MagenticOrchestrator
|
||||
from azure.identity import AzureCliCredential
|
||||
from dotenv import load_dotenv
|
||||
|
||||
@@ -113,17 +114,21 @@ async def main() -> None:
|
||||
print("\nStarting workflow execution...")
|
||||
|
||||
# Keep track of the last executor to format output nicely in streaming mode
|
||||
last_response_id: str | None = None
|
||||
last_message_id: str | None = None
|
||||
output_event: WorkflowEvent | None = None
|
||||
async for event in workflow.run(task, stream=True):
|
||||
if event.type in ("intermediate", "output") and isinstance(event.data, AgentResponseUpdate):
|
||||
response_id = event.data.response_id
|
||||
if response_id != last_response_id:
|
||||
if last_response_id is not None:
|
||||
print("\n")
|
||||
print(f"- {event.executor_id}:", end=" ", flush=True)
|
||||
last_response_id = response_id
|
||||
print(event.data, end="", flush=True)
|
||||
if event.type in ("intermediate", "output"):
|
||||
if event.executor_id == MagenticOrchestrator.MANAGER_NAME:
|
||||
output_event = event
|
||||
else:
|
||||
event_data = cast(AgentResponseUpdate, event.data)
|
||||
message_id = event_data.message_id
|
||||
if message_id != last_message_id:
|
||||
if last_message_id is not None:
|
||||
print("\n")
|
||||
print(f"- {event.executor_id}:", end=" ", flush=True)
|
||||
last_message_id = message_id
|
||||
print(event_data, end="", flush=True)
|
||||
|
||||
elif event.type == "magentic_orchestrator":
|
||||
print(f"\n[Magentic Orchestrator Event] Type: {event.data.event_type.name}")
|
||||
@@ -137,21 +142,20 @@ async def main() -> None:
|
||||
# Block to allow user to read the plan/progress before continuing
|
||||
# Note: this is for demonstration only and is not the recommended way to handle human interaction.
|
||||
# Please refer to `with_plan_review` for proper human interaction during planning phases.
|
||||
await asyncio.get_event_loop().run_in_executor(None, input, "Press Enter to continue...")
|
||||
# await asyncio.get_event_loop().run_in_executor(None, input, "Press Enter to continue...")
|
||||
|
||||
elif event.type == "group_chat" and isinstance(event.data, GroupChatRequestSentEvent):
|
||||
print(f"\n[REQUEST SENT ({event.data.round_index})] to agent: {event.data.participant_name}")
|
||||
|
||||
elif event.type == "output":
|
||||
output_event = event
|
||||
if not output_event:
|
||||
raise RuntimeError("Workflow did not produce a final output event.")
|
||||
|
||||
if output_event:
|
||||
# The output of the magentic workflow is a collection of chat messages from all participants
|
||||
outputs = cast(list[Message], output_event.data)
|
||||
print("\n" + "=" * 80)
|
||||
print("\nFinal Conversation Transcript:\n")
|
||||
for message in outputs:
|
||||
print(f"{message.author_name or message.role}: {message.text}\n")
|
||||
print("\n\nWorkflow completed!")
|
||||
print("Final Output:")
|
||||
# The output of the Magentic workflow is an AgentResponse or AgentResponseUpdate,
|
||||
# which contains the final message text.
|
||||
output_message = cast(AgentResponseUpdate, output_event.data)
|
||||
print(output_message.text)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -19,11 +19,10 @@ This directory contains samples that demonstrate how to use hosted [Agent Framew
|
||||
| 6 | [Files](responses/files/) | An agent demonstrating how to work with files in a hosted agent session, including uploading files to a hosted agent session and having the agent read and manipulate those files at runtime. |
|
||||
| 7 | [Observability](responses/observability/) | A sample demonstrating how to enable observability for the agent deployed to Foundry. |
|
||||
| 8 | [Azure AI Search RAG](responses/azure_search_rag/) | An agent with Retrieval Augmented Generation (RAG) capabilities backed by Azure AI Search, grounding answers in documents indexed in a pre-provisioned search index. |
|
||||
| 9 | [Foundry Skills](responses/foundry_skills/) | An agent that uploads `SKILL.md` files to the Foundry Skills REST API and downloads them at startup, decoupling tone/policy guidelines from agent code. |
|
||||
| 10 | [Foundry Memory](responses/foundry_memory/) | An agent with persistent semantic memory backed by a Microsoft Foundry Memory Store, using `FoundryMemoryProvider` to remember user facts across sessions. |
|
||||
| 11 | [Monty CodeAct](responses/monty_codeact/) | An agent with a Monty-backed CodeAct context provider, exposing a single `execute_code` tool that runs Python in a [pydantic-monty](https://github.com/pydantic/monty) interpreter and invokes typed host tools (`compute`, `fetch_data`) from inside the sandbox. Uses the beta `agent-framework-monty` package. |
|
||||
| 12 | [Foundry Toolbox MCP Skills](responses/foundry_toolbox_mcp_skills/) | An agent that discovers MCP-based skills attached to a Foundry Toolbox and serves them via `SkillsProvider(MCPSkillsSource(...))`, fetching `SKILL.md` bodies and supplementary resources on demand. |
|
||||
| 13 | [Using deployed agent](responses/using_deployed_agent.py) | A sample demonstrating how to invoke an agent that has already been deployed to Foundry, showing how to interact with a hosted agent in code. |
|
||||
| 9 | [Foundry Memory](responses/foundry_memory/) | An agent with persistent semantic memory backed by a Microsoft Foundry Memory Store, using `FoundryMemoryProvider` to remember user facts across sessions. |
|
||||
| 10 | [Monty CodeAct](responses/monty_codeact/) | An agent with a Monty-backed CodeAct context provider, exposing a single `execute_code` tool that runs Python in a [pydantic-monty](https://github.com/pydantic/monty) interpreter and invokes typed host tools (`compute`, `fetch_data`) from inside the sandbox. Uses the beta `agent-framework-monty` package. |
|
||||
| 11 | [Foundry Toolbox MCP Skills](responses/foundry_toolbox_mcp_skills/) | An agent that discovers MCP-based skills attached to a Foundry Toolbox and serves them via `SkillsProvider(MCPSkillsSource(...))`, fetching `SKILL.md` bodies and supplementary resources on demand. |
|
||||
| 12 | [Using deployed agent](responses/using_deployed_agent.py) | A sample demonstrating how to invoke an agent that has already been deployed to Foundry, showing how to interact with a hosted agent in code. |
|
||||
|
||||
### Invocations API
|
||||
|
||||
|
||||
@@ -2,69 +2,16 @@
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
from collections.abc import Callable
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
import httpx
|
||||
from agent_framework import Agent, MCPStreamableHTTPTool, tool
|
||||
from agent_framework.foundry import FoundryChatClient, ResponsesHostServer
|
||||
from azure.identity import DefaultAzureCredential, get_bearer_token_provider
|
||||
from agent_framework import Agent, tool
|
||||
from agent_framework.foundry import FoundryChatClient, FoundryToolbox, ResponsesHostServer
|
||||
from azure.identity import DefaultAzureCredential
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
|
||||
def resolve_toolbox_endpoint() -> str:
|
||||
"""Resolve the toolbox MCP endpoint URL.
|
||||
|
||||
Prefers the explicit ``TOOLBOX_ENDPOINT`` env var (set in ``agent.yaml`` or
|
||||
``agent.manifest.yaml`` and via ``azd env set TOOLBOX_ENDPOINT`` after the toolbox
|
||||
is created); falls back to constructing the URL from ``FOUNDRY_PROJECT_ENDPOINT``
|
||||
and ``TOOLBOX_NAME``.
|
||||
"""
|
||||
if (endpoint := os.environ.get("TOOLBOX_ENDPOINT")) is not None:
|
||||
if not endpoint:
|
||||
raise ValueError("TOOLBOX_ENDPOINT is set but empty")
|
||||
return endpoint
|
||||
try:
|
||||
project_endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"].rstrip("/")
|
||||
toolbox_name = os.environ["TOOLBOX_NAME"]
|
||||
except KeyError as e:
|
||||
raise ValueError(
|
||||
"Either set TOOLBOX_ENDPOINT, or set both FOUNDRY_PROJECT_ENDPOINT "
|
||||
"and TOOLBOX_NAME to build the toolbox MCP endpoint."
|
||||
) from e
|
||||
return f"{project_endpoint}/toolboxes/{toolbox_name}/mcp?api-version=v1"
|
||||
|
||||
|
||||
def _toolbox_name_from_endpoint(endpoint: str) -> str:
|
||||
"""Extract the toolbox name from a toolbox MCP endpoint URL.
|
||||
|
||||
Handles both the versioned (``.../toolboxes/<name>/versions/<n>/mcp``) and
|
||||
unversioned (``.../toolboxes/<name>/mcp``) endpoint shapes that Foundry
|
||||
produces. Falls back to ``"toolbox"`` when the path has no ``toolboxes``
|
||||
segment.
|
||||
"""
|
||||
segments = urlsplit(endpoint).path.split("/")
|
||||
if "toolboxes" in segments:
|
||||
idx = segments.index("toolboxes")
|
||||
if idx + 1 < len(segments) and segments[idx + 1]:
|
||||
return segments[idx + 1]
|
||||
return "toolbox"
|
||||
|
||||
|
||||
class ToolboxAuth(httpx.Auth):
|
||||
"""Injects a fresh bearer token on every request."""
|
||||
|
||||
def __init__(self, token_provider: Callable[[], str]):
|
||||
self._get_token = token_provider
|
||||
|
||||
def auth_flow(self, request: httpx.Request):
|
||||
request.headers["Authorization"] = f"Bearer {self._get_token()}"
|
||||
yield request
|
||||
|
||||
|
||||
@tool(description="Get the current working directory.", approval_mode="never_require")
|
||||
def get_cwd() -> str:
|
||||
"""Get the current working directory."""
|
||||
@@ -96,48 +43,35 @@ def read_file(file_path: str) -> str:
|
||||
async def main():
|
||||
credential = DefaultAzureCredential()
|
||||
|
||||
# Create the toolbox
|
||||
token_provider = get_bearer_token_provider(credential, "https://ai.azure.com/.default")
|
||||
# FoundryToolbox resolves the toolbox endpoint from the environment
|
||||
# (TOOLBOX_ENDPOINT, or FOUNDRY_PROJECT_ENDPOINT + TOOLBOX_NAME), authenticates
|
||||
# every request with the credential, and transparently forwards the platform
|
||||
# per-request call-id to the toolbox. The hosting server enters the agent, which
|
||||
# connects the toolbox on first use and closes it at shutdown.
|
||||
toolbox = FoundryToolbox(credential)
|
||||
|
||||
# Resolve the endpoint once and derive a friendly tool name from it. When
|
||||
# ``TOOLBOX_NAME`` isn't set, extract the toolbox name from the URL path so
|
||||
# the tool's local name matches the upstream toolbox.
|
||||
toolbox_endpoint = resolve_toolbox_endpoint()
|
||||
toolbox_name = os.environ.get("TOOLBOX_NAME") or _toolbox_name_from_endpoint(toolbox_endpoint)
|
||||
# Create the chat client
|
||||
client = FoundryChatClient(
|
||||
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
|
||||
credential=credential,
|
||||
)
|
||||
|
||||
async with httpx.AsyncClient(
|
||||
auth=ToolboxAuth(token_provider),
|
||||
timeout=120.0,
|
||||
) as http_client:
|
||||
toolbox = MCPStreamableHTTPTool(
|
||||
name=toolbox_name,
|
||||
url=toolbox_endpoint,
|
||||
http_client=http_client,
|
||||
load_prompts=False,
|
||||
)
|
||||
|
||||
# Create the chat client
|
||||
client = FoundryChatClient(
|
||||
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
|
||||
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
|
||||
credential=credential,
|
||||
)
|
||||
|
||||
agent = Agent(
|
||||
client=client,
|
||||
instructions=(
|
||||
"You are a friendly assistant. Keep your answers brief. "
|
||||
"Make sure all mathematical calculations are performed using the code interpreter "
|
||||
"instead of mental arithmetic."
|
||||
),
|
||||
tools=[get_cwd, list_files, read_file, toolbox],
|
||||
# History will be managed by the hosting infrastructure, thus there
|
||||
# is no need to store history by the service. Learn more at:
|
||||
# https://developers.openai.com/api/reference/resources/responses/methods/create
|
||||
default_options={"store": False},
|
||||
)
|
||||
server = ResponsesHostServer(agent)
|
||||
await server.run_async()
|
||||
agent = Agent(
|
||||
client=client,
|
||||
instructions=(
|
||||
"You are a friendly assistant. Keep your answers brief. "
|
||||
"Make sure all mathematical calculations are performed using the code interpreter "
|
||||
"instead of mental arithmetic."
|
||||
),
|
||||
tools=[get_cwd, list_files, read_file, toolbox],
|
||||
# History will be managed by the hosting infrastructure, thus there
|
||||
# is no need to store history by the service. Learn more at:
|
||||
# https://developers.openai.com/api/reference/resources/responses/methods/create
|
||||
default_options={"store": False},
|
||||
)
|
||||
server = ResponsesHostServer(agent)
|
||||
await server.run_async()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
.venv
|
||||
__pycache__
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
.Python
|
||||
.env
|
||||
provision_skills.py
|
||||
skills
|
||||
downloaded_skills
|
||||
@@ -1,6 +0,0 @@
|
||||
FOUNDRY_PROJECT_ENDPOINT="..."
|
||||
AZURE_AI_MODEL_DEPLOYMENT_NAME="..."
|
||||
# Comma-separated list of Foundry skill names to download at startup.
|
||||
SKILL_NAMES="support-style,escalation-policy"
|
||||
# Optional writable directory for downloaded skills. Defaults to the system temp directory.
|
||||
# DOWNLOADED_SKILLS_DIR="/tmp/maf_downloaded_skills"
|
||||
@@ -1 +0,0 @@
|
||||
downloaded_skills/
|
||||
@@ -1,16 +0,0 @@
|
||||
FROM python:3.12-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
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"]
|
||||
@@ -1,139 +0,0 @@
|
||||
# What this sample demonstrates
|
||||
|
||||
An [Agent Framework](https://github.com/microsoft/agent-framework) agent that loads its behavioral guidelines from [**Foundry Skills**](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/tools/skills?view=foundry&pivots=python) at startup, hosted using the **Responses protocol**. Skills are authored once as `SKILL.md` files, uploaded to your Foundry project through `AIProjectClient.beta.skills`, and downloaded by the agent on boot so updates ship without code changes.
|
||||
|
||||
## How It Works
|
||||
|
||||
### Authoring skills
|
||||
|
||||
Each skill is a Markdown file with a YAML front matter block. This sample ships two source skills under [`skills/`](skills/):
|
||||
|
||||
| Skill | Purpose |
|
||||
|---|---|
|
||||
| [`support-style`](skills/support-style/SKILL.md) | Voice, formatting, and signature rules for Contoso Outdoors support replies. |
|
||||
| [`escalation-policy`](skills/escalation-policy/SKILL.md) | When and how to escalate a customer ticket. |
|
||||
|
||||
Each `SKILL.md` includes a unique `*-CANARY-*` token that the model is asked to echo, so you can prove the skill was loaded from Foundry (not hallucinated) by checking the response.
|
||||
|
||||
> The `name` and `description` values in the YAML front matter must be **unquoted** — quoting them causes the Skills REST API to return HTTP 500 on import.
|
||||
|
||||
### Uploading skills with `AIProjectClient`
|
||||
|
||||
[`provision_skills.py`](provision_skills.py) walks `skills/*/SKILL.md`, packages each file as an in-memory ZIP (with `SKILL.md` at the archive root), and imports it through [`AIProjectClient.beta.skills.create_from_package`](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/tools/skills?view=foundry&pivots=python#option-2-import-from-a-skillmd-zip). The client is constructed with `allow_preview=True` (Skills is a preview feature) and authenticates with `DefaultAzureCredential`. Existing skills are deleted first via `beta.skills.delete` so the script is safe to re-run after editing a `SKILL.md`, and `beta.skills.list` is called at the end to verify each skill round-trips.
|
||||
|
||||
### Downloading skills at agent startup
|
||||
|
||||
[`main.py`](main.py) reads the comma-separated `SKILL_NAMES` env var, opens an `AIProjectClient` (also with `allow_preview=True`), and for each skill name streams the ZIP archive from `beta.skills.download(name)` and unpacks it into a **separate writable runtime directory**. By default this directory is created under the system temp folder as `maf_downloaded_skills/<name>/`, which works in hosted containers where the application directory may be read-only. Set `DOWNLOADED_SKILLS_DIR` to override the location.
|
||||
|
||||
A [`SkillsProvider`](../../../../../packages/core/agent_framework/_skills.py) is then built over the downloaded skills directory and attached to the `Agent` as a context provider. The provider follows the [Agent Skills](https://agentskills.io/) progressive-disclosure pattern:
|
||||
|
||||
1. **Advertise** — skill names and descriptions are injected into the system prompt at session start (~100 tokens per skill).
|
||||
2. **Load** — the model calls the `load_skill` tool when it decides a skill is relevant to the user's turn, and the full `SKILL.md` body is returned.
|
||||
|
||||
This means the model only pays the token cost for a skill's full body when it actually needs it, and updating a skill in Foundry + restarting the agent is enough to pick up the change — no code redeploy required.
|
||||
|
||||
### Agent Hosting
|
||||
|
||||
The agent is hosted using the [Agent Framework](https://github.com/microsoft/agent-framework) with the `ResponsesHostServer`, which provisions a REST API endpoint compatible with the OpenAI Responses protocol.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- A Microsoft Foundry project with a deployed model (e.g., `gpt-4.1-mini`)
|
||||
- Azure CLI logged in (`az login`)
|
||||
|
||||
### Required RBAC
|
||||
|
||||
Your identity (or the Managed Identity running the container in production) needs **Azure AI User** on the Foundry project scope. This single role covers both authoring skills with `provision_skills.py` and downloading them from `main.py`.
|
||||
|
||||
## Provisioning the skills (one time)
|
||||
|
||||
From this directory, with the venv activated and `az login` done:
|
||||
|
||||
```bash
|
||||
export FOUNDRY_PROJECT_ENDPOINT="https://<account>.services.ai.azure.com/api/projects/<project>"
|
||||
python provision_skills.py
|
||||
```
|
||||
|
||||
Or in PowerShell:
|
||||
|
||||
```powershell
|
||||
$env:FOUNDRY_PROJECT_ENDPOINT="https://<account>.services.ai.azure.com/api/projects/<project>"
|
||||
python provision_skills.py
|
||||
```
|
||||
|
||||
Expected output:
|
||||
|
||||
```text
|
||||
Provisioning skill 'escalation-policy' from skills/escalation-policy/SKILL.md...
|
||||
Imported skill 'escalation-policy' (id=skill_..., has_blob=True).
|
||||
Provisioning skill 'support-style' from skills/support-style/SKILL.md...
|
||||
Imported skill 'support-style' (id=skill_..., has_blob=True).
|
||||
Done.
|
||||
```
|
||||
|
||||
Re-running the script after editing a `SKILL.md` re-imports the skill, replacing the previous version.
|
||||
|
||||
> To remove a skill manually, call `project.beta.skills.delete("<name>")` on an `AIProjectClient` constructed with `allow_preview=True`.
|
||||
|
||||
## 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.
|
||||
|
||||
In addition to the standard environment variables, this sample requires:
|
||||
|
||||
```bash
|
||||
export SKILL_NAMES="support-style,escalation-policy"
|
||||
```
|
||||
|
||||
Or in PowerShell:
|
||||
|
||||
```powershell
|
||||
$env:SKILL_NAMES="support-style,escalation-policy"
|
||||
```
|
||||
|
||||
You can also place these in a `.env` file next to `main.py` — see [`.env.example`](.env.example).
|
||||
|
||||
On startup you should see:
|
||||
|
||||
```text
|
||||
Downloading skill 'support-style' from Foundry...
|
||||
Downloading skill 'escalation-policy' from Foundry...
|
||||
```
|
||||
|
||||
The downloaded `SKILL.md` files land under `DOWNLOADED_SKILLS_DIR/<name>/SKILL.md`. The directory is recreated from scratch on every run, so deleting it manually is never necessary.
|
||||
|
||||
By default, the sample uses the system temp directory, for example `/tmp/maf_downloaded_skills` on Linux. To choose a different writable location, set `DOWNLOADED_SKILLS_DIR` before startup.
|
||||
|
||||
## 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": "Hi, I am Alex. I just want to confirm I can return my tent within 30 days."}'
|
||||
curl -X POST http://localhost:8088/responses -H "Content-Type: application/json" -d '{"input": "I want a $750 refund on Order #A-1042 right now or I am calling my lawyer."}'
|
||||
```
|
||||
|
||||
| Prompt mentions | Skill that should drive the response |
|
||||
|---|---|
|
||||
| Routine return / shipping / care question | Model loads `support-style` (canary `STYLE-CANARY-3318`) — no escalation. |
|
||||
| Injury, legal threat, press, or refund > $500 | Model loads `escalation-policy` (canary `ESC-CANARY-7742`) **and** `support-style`. |
|
||||
|
||||
Because skills are loaded on demand, the canary token in a response also proves the model actually invoked `load_skill` for the matching skill (not just saw its name in the advertised list).
|
||||
|
||||
## 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.
|
||||
|
||||
When deploying, make sure `SKILL_NAMES` is set in your `azd` environment so it gets injected into the hosted container per [`agent.manifest.yaml`](agent.manifest.yaml):
|
||||
|
||||
```bash
|
||||
azd env set SKILL_NAMES "support-style,escalation-policy"
|
||||
```
|
||||
|
||||
If it is not set, running `azd ai agent init -m <agent.manifest.yaml>` will prompt you to enter it interactively.
|
||||
|
||||
The deployed agent's Managed Identity needs **Azure AI User** on the Foundry project to download skills at startup. Make sure you have run `provision_skills.py` against the same Foundry project before deploying — otherwise the agent will fail to start with HTTP 404 on the skill download.
|
||||
|
||||
> The `skills/` source folder is **not** deployed to Foundry — only the downloaded skills are used at runtime. The `provision_skills.py` step is required to upload the skills to Foundry before the agent can download them.
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
name: agent-framework-agent-foundry-skills-responses
|
||||
description: >
|
||||
An Agent Framework agent that downloads its instructions from the Foundry
|
||||
Skills REST API at startup, demonstrating how to decouple behavioral
|
||||
guidelines (tone, escalation policy, etc.) from agent code.
|
||||
metadata:
|
||||
tags:
|
||||
- Agent Framework
|
||||
- AI Agent Hosting
|
||||
- Azure AI AgentServer
|
||||
- Responses Protocol
|
||||
- Foundry Skills
|
||||
template:
|
||||
name: agent-framework-agent-foundry-skills-responses
|
||||
kind: hosted
|
||||
protocols:
|
||||
- protocol: responses
|
||||
version: 2.0.0
|
||||
environment_variables:
|
||||
- name: AZURE_AI_MODEL_DEPLOYMENT_NAME
|
||||
value: "{{AZURE_AI_MODEL_DEPLOYMENT_NAME}}"
|
||||
- name: SKILL_NAMES
|
||||
value: "{{SKILL_NAMES}}"
|
||||
parameters:
|
||||
properties:
|
||||
- name: SKILL_NAMES
|
||||
secret: false
|
||||
description: Comma-separated list of Foundry skill names to download at startup (e.g., support-style,escalation-policy)
|
||||
resources:
|
||||
- kind: model
|
||||
id: gpt-4.1-mini
|
||||
name: AZURE_AI_MODEL_DEPLOYMENT_NAME
|
||||
@@ -1,14 +0,0 @@
|
||||
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml
|
||||
kind: hosted
|
||||
name: agent-framework-agent-foundry-skills-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}
|
||||
- name: SKILL_NAMES
|
||||
value: ${SKILL_NAMES}
|
||||
@@ -1,115 +0,0 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
"""Foundry Skills hosted agent sample.
|
||||
|
||||
At startup, this agent downloads each Foundry Skill named in
|
||||
``SKILL_NAMES`` from the project's ``beta.skills`` API, unpacks each
|
||||
one into a separate writable runtime directory and wires
|
||||
that directory into a :class:`SkillsProvider` so the agent advertises the
|
||||
skills to the model and loads them on demand (progressive disclosure).
|
||||
|
||||
Upload the skills to Foundry once with ``provision_skills.py`` before running
|
||||
this sample.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import io
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from typing import Final
|
||||
|
||||
from agent_framework import Agent, SkillsProvider
|
||||
from agent_framework.foundry import FoundryChatClient, ResponsesHostServer
|
||||
from azure.ai.projects.aio import AIProjectClient
|
||||
from azure.identity.aio import DefaultAzureCredential
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
# Runtime directory where skills downloaded from Foundry are unpacked.
|
||||
# Kept separate from the static ``skills/`` source folder so the two never
|
||||
# get confused: the source folder is the input to ``provision_skills.py``
|
||||
# and the runtime folder is the output of this script's bootstrap step.
|
||||
# Defaults to a system temp location because hosted containers may mount the
|
||||
# application directory read-only. Set DOWNLOADED_SKILLS_DIR to override it.
|
||||
_DEFAULT_DOWNLOADED_SKILLS_DIR: Final = Path(tempfile.gettempdir()) / "maf_downloaded_skills"
|
||||
_DOWNLOADED_SKILLS_DIR_ENV = os.environ.get("DOWNLOADED_SKILLS_DIR")
|
||||
DOWNLOADED_SKILLS_DIR: Final = Path((_DOWNLOADED_SKILLS_DIR_ENV or "").strip() or _DEFAULT_DOWNLOADED_SKILLS_DIR)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _safe_extract_zip(zf: zipfile.ZipFile, dest_dir: Path) -> None:
|
||||
"""Extract ``zf`` into ``dest_dir``, rejecting entries that escape it (zip-slip guard)."""
|
||||
dest_root = dest_dir.resolve()
|
||||
for member in zf.infolist():
|
||||
member_path = (dest_root / member.filename).resolve()
|
||||
if dest_root != member_path and dest_root not in member_path.parents:
|
||||
raise RuntimeError(f"Refusing to extract unsafe path '{member.filename}' outside of '{dest_root}'.")
|
||||
zf.extractall(dest_dir)
|
||||
|
||||
|
||||
async def _bootstrap_skills(endpoint: str, skill_names: list[str], target_dir: Path) -> None:
|
||||
"""Download each named skill via ``project.beta.skills`` and unpack it as ``<target_dir>/<name>/SKILL.md``."""
|
||||
if target_dir.exists(): # noqa: ASYNC240
|
||||
shutil.rmtree(target_dir)
|
||||
target_dir.mkdir(parents=True) # noqa: ASYNC240
|
||||
|
||||
async with (
|
||||
DefaultAzureCredential() as credential,
|
||||
AIProjectClient(endpoint=endpoint, credential=credential, allow_preview=True) as project,
|
||||
):
|
||||
for name in skill_names:
|
||||
logger.info(f"Downloading skill '{name}' from Foundry...")
|
||||
stream = await project.beta.skills.download(name)
|
||||
zip_bytes = b"".join([chunk async for chunk in stream])
|
||||
skill_dir = target_dir / name
|
||||
skill_dir.mkdir()
|
||||
with zipfile.ZipFile(io.BytesIO(zip_bytes)) as zf:
|
||||
_safe_extract_zip(zf, skill_dir)
|
||||
if not (skill_dir / "SKILL.md").is_file():
|
||||
raise RuntimeError(f"Downloaded archive for '{name}' did not contain a SKILL.md at the root.")
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
project_endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"]
|
||||
skill_names = [name.strip() for name in os.environ["SKILL_NAMES"].split(",") if name.strip()]
|
||||
if not skill_names:
|
||||
raise RuntimeError("SKILL_NAMES must list at least one skill name.")
|
||||
|
||||
# Pull the latest copy of each skill from Foundry into a runtime-only folder.
|
||||
await _bootstrap_skills(project_endpoint, skill_names, DOWNLOADED_SKILLS_DIR)
|
||||
|
||||
# Build a SkillsProvider over the unpacked folder. The provider advertises
|
||||
# each skill's name + description to the model and exposes the ``load_skill``
|
||||
# tool the model uses to retrieve the full SKILL.md body on demand. No
|
||||
# script_runner is configured because the skills in this sample are
|
||||
# instruction-only.
|
||||
skills_provider = SkillsProvider.from_paths(skill_paths=str(DOWNLOADED_SKILLS_DIR))
|
||||
|
||||
async with DefaultAzureCredential() as credential:
|
||||
client = FoundryChatClient(
|
||||
project_endpoint=project_endpoint,
|
||||
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
|
||||
credential=credential,
|
||||
)
|
||||
|
||||
agent = Agent(
|
||||
client=client,
|
||||
instructions="You are a customer-support assistant for Contoso Outdoors.",
|
||||
context_providers=[skills_provider],
|
||||
# History will be managed by the hosting infrastructure, thus there
|
||||
# is no need to store history by the service. Learn more at:
|
||||
# https://developers.openai.com/api/reference/resources/responses/methods/create
|
||||
default_options={"store": False},
|
||||
)
|
||||
server = ResponsesHostServer(agent)
|
||||
await server.run_async()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
-96
@@ -1,96 +0,0 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
"""Provision Foundry Skills used by this sample.
|
||||
|
||||
For each ``skills/<name>/SKILL.md`` file in this directory, this script packages
|
||||
the file as an in-memory ZIP and imports it through the Foundry project's
|
||||
:class:`~azure.ai.projects.aio.AIProjectClient` so the skill becomes downloadable
|
||||
by any hosted agent in the project.
|
||||
|
||||
If a skill with the same name already exists in Foundry, it is deleted first
|
||||
so the script is safe to re-run after editing a ``SKILL.md`` file.
|
||||
|
||||
Usage (from this directory, with the venv activated and ``az login`` done):
|
||||
|
||||
python provision_skills.py
|
||||
|
||||
Required env vars (also read from a local ``.env`` file if present):
|
||||
|
||||
FOUNDRY_PROJECT_ENDPOINT e.g. https://<account>.services.ai.azure.com/api/projects/<project>
|
||||
|
||||
Your identity needs the ``Azure AI User`` role on the Foundry project.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import io
|
||||
import os
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
from azure.ai.projects.aio import AIProjectClient
|
||||
from azure.ai.projects.models import CreateSkillVersionFromFilesBody
|
||||
from azure.core.exceptions import ResourceNotFoundError
|
||||
from azure.identity.aio import DefaultAzureCredential
|
||||
from dotenv import load_dotenv
|
||||
|
||||
SKILLS_DIR = Path(__file__).parent / "skills"
|
||||
|
||||
|
||||
def _zip_skill_md(skill_md: Path) -> bytes:
|
||||
"""Return the bytes of a ZIP archive containing ``SKILL.md`` at the root."""
|
||||
buffer = io.BytesIO()
|
||||
with zipfile.ZipFile(buffer, mode="w", compression=zipfile.ZIP_DEFLATED) as zf:
|
||||
zf.writestr("SKILL.md", skill_md.read_text(encoding="utf-8"))
|
||||
return buffer.getvalue()
|
||||
|
||||
|
||||
async def _delete_skill_if_exists(project: AIProjectClient, name: str) -> None:
|
||||
try:
|
||||
await project.beta.skills.delete(name)
|
||||
except ResourceNotFoundError:
|
||||
return
|
||||
print(f" Deleted existing skill '{name}'.")
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
load_dotenv()
|
||||
|
||||
endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"]
|
||||
|
||||
skill_files = sorted(SKILLS_DIR.glob("*/SKILL.md"))
|
||||
if not skill_files:
|
||||
raise RuntimeError(f"No SKILL.md files found under {SKILLS_DIR}.")
|
||||
|
||||
async with (
|
||||
DefaultAzureCredential() as credential,
|
||||
AIProjectClient(endpoint=endpoint, credential=credential, allow_preview=True) as project,
|
||||
):
|
||||
for skill_md in skill_files:
|
||||
name = skill_md.parent.name
|
||||
print(f"Provisioning skill '{name}' from {skill_md.relative_to(SKILLS_DIR.parent)}...")
|
||||
await _delete_skill_if_exists(project, name)
|
||||
imported = await project.beta.skills.create_from_files(
|
||||
name,
|
||||
content=CreateSkillVersionFromFilesBody(
|
||||
files=[(f"{name}.zip", _zip_skill_md(skill_md), "application/zip")]
|
||||
),
|
||||
)
|
||||
print(f" Imported skill '{imported.name}' (id={imported.skill_id}, version={imported.version}).")
|
||||
|
||||
print("Verifying skills via project.beta.skills.list()...")
|
||||
listed = {skill.name: skill async for skill in project.beta.skills.list()}
|
||||
for skill_md in skill_files:
|
||||
name = skill_md.parent.name
|
||||
skill = listed.get(name)
|
||||
if skill is None:
|
||||
raise RuntimeError(f"Skill '{name}' was imported but is not present in the project listing.")
|
||||
print(
|
||||
f" OK '{skill.name}': id={skill.id}, "
|
||||
f"description={skill.description!r}, default_version={skill.default_version}"
|
||||
)
|
||||
|
||||
print("Done.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
agent-framework-foundry
|
||||
agent-framework-foundry-hosting>=1.0.0a260630
|
||||
azure-ai-projects
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
---
|
||||
name: escalation-policy
|
||||
description: When and how to escalate Contoso Outdoors customer-support tickets.
|
||||
---
|
||||
|
||||
# Contoso Outdoors Escalation Policy
|
||||
|
||||
You must follow this escalation policy on every conversation.
|
||||
|
||||
## Escalate immediately when the customer
|
||||
|
||||
- Reports an injury, allergic reaction, or other safety incident.
|
||||
- Mentions legal action, regulators, or the press.
|
||||
- Has waited more than 14 days for a refund that was already approved.
|
||||
- Requests a refund larger than $500.
|
||||
|
||||
## How to escalate
|
||||
|
||||
1. Acknowledge the issue in one sentence.
|
||||
2. Tell the customer you are escalating to a senior specialist.
|
||||
3. Provide the escalation reference `ESC-CANARY-7742` and the SLA: a senior
|
||||
specialist will reply within 1 business day.
|
||||
4. Do not promise a specific outcome (refund, replacement, compensation) on
|
||||
escalated tickets — only the senior specialist can commit to one.
|
||||
|
||||
## Do not escalate
|
||||
|
||||
- Routine returns within the standard 30-day window.
|
||||
- Shipping status questions.
|
||||
- Product care and usage questions.
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
---
|
||||
name: support-style
|
||||
description: Contoso Outdoors customer-support tone and formatting guidelines.
|
||||
---
|
||||
|
||||
# Contoso Outdoors Support Style
|
||||
|
||||
You are speaking on behalf of Contoso Outdoors customer support.
|
||||
|
||||
## Voice
|
||||
|
||||
- Warm, concise, and confident — never apologetic in a hand-wringing way.
|
||||
- Use the customer's name when it is known.
|
||||
- Sign every response with `— Contoso Outdoors Support`.
|
||||
|
||||
## Formatting
|
||||
|
||||
- Keep replies to 1–3 short paragraphs unless the customer asks for detail.
|
||||
- Use bullet lists only when enumerating concrete steps or options.
|
||||
- Always reference order numbers as `Order #<id>` (e.g. `Order #A-1042`).
|
||||
|
||||
## Canary
|
||||
|
||||
To prove this skill was loaded, include the token `STYLE-CANARY-3318` on a
|
||||
separate line at the bottom of every response, prefixed with `# `.
|
||||
+2
-8
@@ -11,13 +11,9 @@ The `FoundryToolbox` is attached to the agent and its skills are exposed through
|
||||
1. **Advertise** — each skill's name and description are injected into the system prompt so the model knows what is available (~100 tokens per skill).
|
||||
2. **Load** — when the model decides a skill is relevant, it retrieves the full `SKILL.md` body on demand via `resources/read`.
|
||||
|
||||
> The Agent Skills spec defines a third stage — **read resources** — where a skill fetches supplementary files (reference documents, assets) on demand. That stage requires a skill to bundle sibling resources, which Foundry serves as a `type: archive` (ZIP) skill. To keep this sample focused on the advertise + load flow, both skills are single-file `SKILL.md` skills (no bundled resources). See the [`foundry_skills`](../foundry_skills/README.md) sample for the same instruction-only pattern via direct download.
|
||||
> The Agent Skills spec defines a third stage — **read resources** — where a skill fetches supplementary files (reference documents, assets) on demand. That stage requires a skill to bundle sibling resources, which Foundry serves as a `type: archive` (ZIP) skill. To keep this sample focused on the advertise + load flow, both skills are single-file `SKILL.md` skills (no bundled resources).
|
||||
|
||||
## Toolbox MCP skills vs. Foundry Skills
|
||||
|
||||
Foundry exposes skills in two ways, and this sample uses the second one.
|
||||
|
||||
**Foundry Skills** are downloaded directly into an agent: the agent pulls each `SKILL.md` from the Skills API at startup and serves the bodies from local files. See the [`foundry_skills`](../foundry_skills/README.md) sample.
|
||||
## Toolbox MCP skills
|
||||
|
||||
**Toolbox MCP skills** are accessed through a toolbox over the MCP protocol. A toolbox bundles a curated set of skills (and optionally tools) behind one MCP endpoint, and any MCP client discovers them automatically. Skill bodies are fetched on demand. The same `SKILL.md` files power both modes — the difference is only in delivery.
|
||||
|
||||
@@ -37,8 +33,6 @@ The agent is hosted with the `ResponsesHostServer`, which provisions a REST API
|
||||
|
||||
## The bundled skills
|
||||
|
||||
This sample ships two source skills under [`skills/`](skills/), reused from the [`foundry_skills`](../foundry_skills/README.md) sample so you can compare the two delivery modes side by side:
|
||||
|
||||
| Skill | Purpose |
|
||||
|---|---|
|
||||
| [`support-style`](skills/support-style/SKILL.md) | Voice, formatting, and signature rules for Contoso Outdoors support replies. |
|
||||
|
||||
@@ -4,8 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
from collections.abc import Mapping
|
||||
from typing import Any, cast
|
||||
from typing import cast
|
||||
|
||||
from agent_framework import AgentSession
|
||||
from agent_framework.foundry import FoundryAgent
|
||||
@@ -35,47 +34,37 @@ agents, as this is a preview feature in Foundry.
|
||||
"""
|
||||
|
||||
|
||||
def get_hosted_session_agents(project_client: AIProjectClient) -> Any:
|
||||
"""Return the hosted-session operations for azure-ai-projects 2.2 or 2.3."""
|
||||
session_agents = cast(Any, project_client.agents)
|
||||
if hasattr(session_agents, "create_session"):
|
||||
return session_agents
|
||||
return cast(Any, project_client.beta.agents)
|
||||
|
||||
|
||||
async def create_hosted_agent_session(
|
||||
*,
|
||||
agent: FoundryAgent,
|
||||
project_client: AIProjectClient,
|
||||
agent_name: str,
|
||||
agent_version: str | None,
|
||||
isolation_key: str,
|
||||
) -> AgentSession:
|
||||
"""Create a hosted-agent service session and wrap it in an AgentSession."""
|
||||
create_session_kwargs: dict[str, Any] = {
|
||||
"agent_name": agent_name,
|
||||
"isolation_key": isolation_key,
|
||||
}
|
||||
resolved_agent_version = agent_version
|
||||
if resolved_agent_version is None:
|
||||
agent_details = await cast(Any, project_client.beta.agents).get( # pyright: ignore[reportAttributeAccessIssue, reportUnknownMemberType]
|
||||
agent_name=agent_name
|
||||
)
|
||||
versions = getattr(agent_details, "versions", None)
|
||||
if not isinstance(versions, Mapping):
|
||||
raise ValueError("Hosted agent details did not include a versions mapping.")
|
||||
latest_version = getattr(cast(Any, versions.get("latest")), "version", None)
|
||||
if not isinstance(latest_version, str) or not latest_version:
|
||||
raise ValueError("Hosted agent details did not include a latest version string.")
|
||||
resolved_agent_version = latest_version
|
||||
agent_details = await project_client.agents.get(agent_name)
|
||||
resolved_agent_version = agent_details.versions.latest.version
|
||||
|
||||
create_session_kwargs["version_indicator"] = VersionRefIndicator(agent_version=resolved_agent_version)
|
||||
service_session = await get_hosted_session_agents(project_client).create_session(**create_session_kwargs)
|
||||
agent_session_id = getattr(service_session, "agent_session_id", None)
|
||||
if not isinstance(agent_session_id, str) or not agent_session_id:
|
||||
raise ValueError("Hosted agent session creation did not return a non-empty agent_session_id.")
|
||||
service_session = await project_client.agents.create_session(
|
||||
agent_name,
|
||||
version_indicator=VersionRefIndicator(agent_version=resolved_agent_version),
|
||||
)
|
||||
return agent.get_session(service_session.agent_session_id)
|
||||
|
||||
return agent.get_session(agent_session_id)
|
||||
|
||||
async def delete_hosted_agent_session(
|
||||
*,
|
||||
project_client: AIProjectClient,
|
||||
agent_name: str,
|
||||
session: AgentSession,
|
||||
) -> None:
|
||||
"""Delete a hosted-agent service session."""
|
||||
await project_client.agents.delete_session(
|
||||
agent_name,
|
||||
cast(str, session.service_session_id),
|
||||
)
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
@@ -83,7 +72,6 @@ async def main() -> None:
|
||||
project_endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"]
|
||||
agent_name = os.environ["FOUNDRY_AGENT_NAME"]
|
||||
agent_version = os.getenv("FOUNDRY_AGENT_VERSION")
|
||||
isolation_key = "my-isolation-key"
|
||||
|
||||
project_client = AIProjectClient(
|
||||
endpoint=project_endpoint,
|
||||
@@ -104,7 +92,6 @@ async def main() -> None:
|
||||
project_client=project_client,
|
||||
agent_name=agent_name,
|
||||
agent_version=agent_version,
|
||||
isolation_key=isolation_key,
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -132,12 +119,11 @@ async def main() -> None:
|
||||
if chunk.text:
|
||||
print(chunk.text, end="", flush=True)
|
||||
finally:
|
||||
if isinstance(session.service_session_id, str):
|
||||
await get_hosted_session_agents(project_client).delete_session(
|
||||
agent_name=agent_name,
|
||||
session_id=session.service_session_id,
|
||||
isolation_key=isolation_key,
|
||||
)
|
||||
await delete_hosted_agent_session(
|
||||
project_client=project_client,
|
||||
agent_name=agent_name,
|
||||
session=session,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -10,7 +10,7 @@ from agent_framework import (
|
||||
Message,
|
||||
WorkflowEvent,
|
||||
)
|
||||
from agent_framework.orchestrations import MagenticProgressLedger
|
||||
from agent_framework.orchestrations import MagenticOrchestrator, MagenticProgressLedger
|
||||
from dotenv import load_dotenv
|
||||
|
||||
"""AutoGen MagenticOneGroupChat vs Agent Framework MagenticBuilder.
|
||||
@@ -25,7 +25,6 @@ load_dotenv()
|
||||
|
||||
async def run_autogen() -> None:
|
||||
"""AutoGen's MagenticOneGroupChat for orchestrated collaboration."""
|
||||
|
||||
from autogen_agentchat.agents import AssistantAgent
|
||||
from autogen_agentchat.teams import MagenticOneGroupChat
|
||||
from autogen_agentchat.ui import Console
|
||||
@@ -62,7 +61,7 @@ async def run_autogen() -> None:
|
||||
team = MagenticOneGroupChat(
|
||||
participants=[researcher, coder, reviewer],
|
||||
model_client=client, # Coordinator uses this client
|
||||
max_turns=20,
|
||||
max_turns=10,
|
||||
max_stalls=3,
|
||||
)
|
||||
|
||||
@@ -109,7 +108,7 @@ async def run_agent_framework() -> None:
|
||||
instructions="You coordinate a team to complete complex tasks efficiently.",
|
||||
description="Orchestrator for team coordination",
|
||||
),
|
||||
max_round_count=20,
|
||||
max_round_count=10,
|
||||
max_stall_count=3,
|
||||
max_reset_count=1,
|
||||
).build()
|
||||
@@ -119,14 +118,18 @@ async def run_agent_framework() -> None:
|
||||
output_event: WorkflowEvent | None = None
|
||||
print("[Agent Framework] Magentic conversation:")
|
||||
async for event in workflow.run("Research Python async patterns and write a simple example", stream=True):
|
||||
if event.type == "output" and isinstance(event.data, AgentResponseUpdate):
|
||||
message_id = event.data.message_id
|
||||
if message_id != last_message_id:
|
||||
if last_message_id is not None:
|
||||
print("\n")
|
||||
print(f"- {event.executor_id}:", end=" ", flush=True)
|
||||
last_message_id = message_id
|
||||
print(event.data, end="", flush=True)
|
||||
if event.type == "output":
|
||||
if event.executor_id == MagenticOrchestrator.MANAGER_NAME:
|
||||
output_event = event
|
||||
else:
|
||||
event_data = cast(AgentResponseUpdate, event.data)
|
||||
message_id = event_data.message_id
|
||||
if message_id != last_message_id:
|
||||
if last_message_id is not None:
|
||||
print("\n")
|
||||
print(f"- {event.executor_id}:", end=" ", flush=True)
|
||||
last_message_id = message_id
|
||||
print(event_data, end="", flush=True)
|
||||
|
||||
elif event.type == "magentic_orchestrator":
|
||||
print(f"\n[Magentic Orchestrator Event] Type: {event.data.event_type.name}")
|
||||
@@ -142,19 +145,15 @@ async def run_agent_framework() -> None:
|
||||
# Please refer to `with_plan_review` for proper human interaction during planning phases.
|
||||
await asyncio.get_event_loop().run_in_executor(None, input, "Press Enter to continue...")
|
||||
|
||||
elif event.type == "output":
|
||||
output_event = event
|
||||
|
||||
if not output_event:
|
||||
raise RuntimeError("Workflow did not produce a final output event.")
|
||||
|
||||
print("\n\nWorkflow completed!")
|
||||
print("Final Output:")
|
||||
# The output of the Magentic workflow is a list of ChatMessages with only one final message
|
||||
# generated by the orchestrator.
|
||||
output_messages = cast(list[Message], output_event.data)
|
||||
if output_messages:
|
||||
output = output_messages[-1].text
|
||||
print(output)
|
||||
# The output of the Magentic workflow is an AgentResponse or AgentResponseUpdate,
|
||||
# which contains the final message text.
|
||||
output_message = cast(AgentResponseUpdate, output_event.data)
|
||||
print(output_message.text)
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
# "agent-framework-openai",
|
||||
# "autogen-agentchat",
|
||||
# "autogen-ext[openai]",
|
||||
# "python-dotenv",
|
||||
# ]
|
||||
# ///
|
||||
# Run with any PEP 723 compatible runner, e.g.:
|
||||
# uv run samples/autogen-migration/single_agent/01_basic_assistant_agent.py
|
||||
# uv run samples/autogen-migration/single_agent/01_basic_agent.py
|
||||
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
|
||||
@@ -1,3 +1,15 @@
|
||||
# /// script
|
||||
# requires-python = ">=3.10"
|
||||
# dependencies = [
|
||||
# "agent-framework-openai",
|
||||
# "autogen-agentchat",
|
||||
# "autogen-ext[openai]",
|
||||
# "python-dotenv",
|
||||
# ]
|
||||
# ///
|
||||
# Run with any PEP 723 compatible runner, e.g.:
|
||||
# uv run samples/autogen-migration/single_agent/02_agent_with_tool.py
|
||||
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# requires-python = ">=3.10"
|
||||
# dependencies = [
|
||||
# "agent-framework-openai",
|
||||
# "python-dotenv",
|
||||
# "semantic-kernel",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
+1
@@ -2,6 +2,7 @@
|
||||
# requires-python = ">=3.10"
|
||||
# dependencies = [
|
||||
# "agent-framework-openai",
|
||||
# "python-dotenv",
|
||||
# "semantic-kernel",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
+1
@@ -2,6 +2,7 @@
|
||||
# requires-python = ">=3.10"
|
||||
# dependencies = [
|
||||
# "agent-framework-openai",
|
||||
# "python-dotenv",
|
||||
# "semantic-kernel",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
+10
@@ -2,11 +2,21 @@
|
||||
# requires-python = ">=3.10"
|
||||
# dependencies = [
|
||||
# "agent-framework-copilotstudio",
|
||||
# "python-dotenv",
|
||||
# "semantic-kernel",
|
||||
# ]
|
||||
# ///
|
||||
# Run with any PEP 723 compatible runner, e.g.:
|
||||
# uv run samples/semantic-kernel-migration/copilot_studio/01_basic_copilot_studio_agent.py
|
||||
#
|
||||
# NOTE: The metadata above resolves the Agent Framework half only.
|
||||
# The Semantic Kernel half (run_semantic_kernel) requires the older
|
||||
# dot-namespace Microsoft Agents SDK (microsoft.agents.copilotstudio.client and
|
||||
# microsoft.agents.core, from microsoft-agents-copilotstudio-client<0.3), while
|
||||
# Agent Framework requires the newer underscore-namespace SDK
|
||||
# (microsoft_agents.copilotstudio.client, from
|
||||
# microsoft-agents-copilotstudio-client>=0.3.1). These two generations cannot be
|
||||
# installed in the same environment, so run each half in its own isolated env.
|
||||
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
"""Call a Copilot Studio agent with SK and Agent Framework."""
|
||||
|
||||
+10
@@ -2,11 +2,21 @@
|
||||
# requires-python = ">=3.10"
|
||||
# dependencies = [
|
||||
# "agent-framework-copilotstudio",
|
||||
# "python-dotenv",
|
||||
# "semantic-kernel",
|
||||
# ]
|
||||
# ///
|
||||
# Run with any PEP 723 compatible runner, e.g.:
|
||||
# uv run samples/semantic-kernel-migration/copilot_studio/02_copilot_studio_streaming.py
|
||||
#
|
||||
# NOTE: The metadata above resolves the Agent Framework half only.
|
||||
# The Semantic Kernel half (run_semantic_kernel) requires the older
|
||||
# dot-namespace Microsoft Agents SDK (microsoft.agents.copilotstudio.client and
|
||||
# microsoft.agents.core, from microsoft-agents-copilotstudio-client<0.3), while
|
||||
# Agent Framework requires the newer underscore-namespace SDK
|
||||
# (microsoft_agents.copilotstudio.client, from
|
||||
# microsoft-agents-copilotstudio-client>=0.3.1). These two generations cannot be
|
||||
# installed in the same environment, so run each half in its own isolated env.
|
||||
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
"""Stream responses from Copilot Studio agents in SK and AF."""
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# requires-python = ">=3.10"
|
||||
# dependencies = [
|
||||
# "agent-framework-openai",
|
||||
# "python-dotenv",
|
||||
# "semantic-kernel",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
+1
@@ -2,6 +2,7 @@
|
||||
# requires-python = ">=3.10"
|
||||
# dependencies = [
|
||||
# "agent-framework-openai",
|
||||
# "python-dotenv",
|
||||
# "semantic-kernel",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
+1
@@ -2,6 +2,7 @@
|
||||
# requires-python = ">=3.10"
|
||||
# dependencies = [
|
||||
# "agent-framework-openai",
|
||||
# "python-dotenv",
|
||||
# "semantic-kernel",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
# dependencies = [
|
||||
# "agent-framework-openai",
|
||||
# "agent-framework-orchestrations",
|
||||
# "python-dotenv",
|
||||
# "semantic-kernel",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
# dependencies = [
|
||||
# "agent-framework-openai",
|
||||
# "agent-framework-orchestrations",
|
||||
# "python-dotenv",
|
||||
# "semantic-kernel",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
# dependencies = [
|
||||
# "agent-framework-openai",
|
||||
# "agent-framework-orchestrations",
|
||||
# "python-dotenv",
|
||||
# "semantic-kernel",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
# dependencies = [
|
||||
# "agent-framework-openai",
|
||||
# "agent-framework-orchestrations",
|
||||
# "python-dotenv",
|
||||
# "semantic-kernel",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
# dependencies = [
|
||||
# "agent-framework-openai",
|
||||
# "agent-framework-orchestrations",
|
||||
# "python-dotenv",
|
||||
# "semantic-kernel",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# requires-python = ">=3.10"
|
||||
# dependencies = [
|
||||
# "agent-framework-core",
|
||||
# "python-dotenv",
|
||||
# "semantic-kernel",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# requires-python = ">=3.10"
|
||||
# dependencies = [
|
||||
# "agent-framework-core",
|
||||
# "python-dotenv",
|
||||
# "semantic-kernel",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
Reference in New Issue
Block a user