Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
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

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ services:
ai-project:
host: azure.ai.project
deployments:
- name: gpt-5.4-mini
- name: gpt-6-sol
model:
format: OpenAI
name: gpt-5.4-mini
version: '2026-03-17'
name: gpt-6-sol
version: '2026-09-22'
sku:
name: GlobalStandard
capacity: 10
Expand Down Expand Up @@ -41,7 +41,7 @@ services:
- protocol: responses
version: 2.0.0
env:
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-5.4-mini}
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-6-sol}
TOOLBOX_NAME: browser-automation-tools
container:
resources:
Expand All @@ -64,5 +64,3 @@ hooks:
posix:
shell: sh
run: hooks/postdeploy.sh
Comment thread
mohitpavan marked this conversation as resolved.
infra:
provider: microsoft.foundry
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ $toolboxName = "browser-automation-tools"
$toolboxBody = @{
tools = @(
@{
type = "browser_automation_preview"
browser_automation_preview = @{
type = "browser_automation"
browser_automation = @{
connection = @{
project_connection_id = $connectionId
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ CONNECTION_ID="${PROJECT_ID}/connections/browserautomation"
TOOLBOX_NAME="browser-automation-tools"
TOOLBOX_BODY=$(printf '{
"tools": [{
"type": "browser_automation_preview",
"browser_automation_preview": {
"type": "browser_automation",
"browser_automation": {
"connection": { "project_connection_id": "%s" }
}
}]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,9 @@ resource connectionForNewWorkspace 'Microsoft.CognitiveServices/accounts/project
name: connectionName
properties: {
category: 'PlaywrightWorkspace'
target: '${replace(playwrightWorkspace.properties.dataplaneUri, 'https://', 'wss://')}/browsers'
target: '${replace(playwrightWorkspace.properties.dataplaneUri, '.api.playwright.microsoft.com', '.mcp.playwright.microsoft.com')}/mcp'
Comment thread
mohitpavan marked this conversation as resolved.
authType: authType
audience: 'https://management.core.windows.net'
audience: 'https://mcp.playwright.microsoft.com'
isSharedToAll: true
credentials: null
metadata: {
Expand All @@ -83,9 +83,9 @@ resource connectionForExistingWorkspace 'Microsoft.CognitiveServices/accounts/pr
name: connectionName
properties: {
category: 'PlaywrightWorkspace'
target: '${replace(existingPwwLookup.outputs.dataplaneUri, 'https://', 'wss://')}/browsers'
target: '${replace(existingPwwLookup.outputs.dataplaneUri, '.api.playwright.microsoft.com', '.mcp.playwright.microsoft.com')}/mcp'
authType: authType
audience: 'https://management.core.windows.net'
audience: 'https://mcp.playwright.microsoft.com'
isSharedToAll: true
credentials: authType == 'ApiKey' ? {
key: apiKey
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,3 @@
# Required
FOUNDRY_PROJECT_ENDPOINT=https://<account>.services.ai.azure.com/api/projects/<project>
AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4.1

# Optional — override the default toolbox name
TOOLBOX_NAME=browser-automation-tools

# Timeouts
BROWSER_AGENT_PLAYWRIGHT_CLI_TIMEOUT_SECONDS=180
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,6 @@ WORKDIR /app
EXPOSE 8088
ENV ASPNETCORE_URLS=http://+:8088

# Install Node.js and @playwright/cli
RUN apt-get update && apt-get install -y curl && \
curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \
apt-get install -y nodejs && \
npm install -g @playwright/cli@latest && \
playwright-cli install --skills && \
apt-get clean && rm -rf /var/lib/apt/lists/*

FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY . .
Expand Down
Original file line number Diff line number Diff line change
@@ -1,117 +1,86 @@
// Copyright (c) Microsoft. All rights reserved.

/*
* Browser Automation — Agent Framework (MAF) hosted agent for C#
*
* Browser automation agent that uses Foundry Toolbox to provision a remote
* Chromium browser and playwright-cli to drive it. Demonstrates:
* - MAF AIAgent with AddFoundryToolboxes for automatic MCP tool discovery
* - Function invocation middleware to intercept create_session results
* - AgentSkillsProvider for progressive skill disclosure
* - LiveViewUrlMiddleware: injects live_view_url post-call
*
* Required environment variables:
* FOUNDRY_PROJECT_ENDPOINT — Foundry project endpoint (auto-injected)
* AZURE_AI_MODEL_DEPLOYMENT_NAME — Model deployment name
*
* Optional:
* BROWSER_AGENT_PLAYWRIGHT_CLI_TIMEOUT_SECONDS — CLI timeout (default: 180)
*/

using Azure.AI.Projects;
using Azure.Core;
using Azure.Identity;
using BrowserAutomation;
using DotNetEnv;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Foundry.Hosting;
using Microsoft.Extensions.AI;
using BrowserAutomation;

Env.NoClobber().TraversePath().Load();

// ── Configuration ────────────────────────────────────────────────────────────
var projectEndpoint = new Uri(Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
var projectEndpoint = new Uri(
Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
?? Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_ENDPOINT")
?? Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT")
?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT environment variable is not set."));
var deployment = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME")
?? throw new InvalidOperationException(
"FOUNDRY_PROJECT_ENDPOINT environment variable is not set."));
var deployment =
Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME")
?? Environment.GetEnvironmentVariable("BROWSER_AGENT_MODEL")
?? throw new InvalidOperationException("AZURE_AI_MODEL_DEPLOYMENT_NAME environment variable is not set.");
?? throw new InvalidOperationException(
"AZURE_AI_MODEL_DEPLOYMENT_NAME environment variable is not set.");
var toolboxName = Environment.GetEnvironmentVariable("TOOLBOX_NAME")
?? "browser-automation-tools";

var toolboxName = "browser-automation-tools";
var playwrightCliTimeout = int.TryParse(Environment.GetEnvironmentVariable("BROWSER_AGENT_PLAYWRIGHT_CLI_TIMEOUT_SECONDS"), out var t) ? t : 180;

// Ensure FOUNDRY_AGENT_TOOLSET_ENDPOINT is set — platform doesn't always inject it
if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("FOUNDRY_AGENT_TOOLSET_ENDPOINT")))
if (string.IsNullOrEmpty(
Environment.GetEnvironmentVariable("FOUNDRY_AGENT_TOOLSET_ENDPOINT")))
{
var toolsetEndpoint = $"{projectEndpoint.ToString().TrimEnd('/')}/toolboxes";
Environment.SetEnvironmentVariable("FOUNDRY_AGENT_TOOLSET_ENDPOINT", toolsetEndpoint);
Environment.SetEnvironmentVariable(
"FOUNDRY_AGENT_TOOLSET_ENDPOINT",
$"{projectEndpoint.ToString().TrimEnd('/')}/toolboxes");
}

// ── Skills directory ─────────────────────────────────────────────────────────
var skillsDir = Path.Combine(AppContext.BaseDirectory, "skills");
if (!Directory.Exists(skillsDir))
skillsDir = Path.Combine(Directory.GetCurrentDirectory(), "skills");

// ── System prompt (loaded from prompts/base.md) ──────────────────────────────
var promptsDir = Path.Combine(AppContext.BaseDirectory, "prompts");
if (!Directory.Exists(promptsDir))
promptsDir = Path.Combine(Directory.GetCurrentDirectory(), "prompts");
var systemPrompt = File.ReadAllText(Path.Combine(promptsDir, "base.md"));

// ── Local tools (playwright-cli + get_live_view_url) ─────────────────────────
List<AITool> localTools =
[
Tools.MakeRunPlaywrightCli(playwrightCliTimeout),
Tools.MakeCloseBrowserSession(playwrightCliTimeout),
Tools.MakeGetLiveViewUrl(),
];

// ── Skills provider (progressive disclosure, no approval gate) ───────────────
var skillsProvider = new AgentSkillsProvider(skillsDir, options: new AgentSkillsProviderOptions
{
DisableLoadSkillApproval = true,
DisableRunSkillScriptApproval = true,
});
var skillsProvider = new AgentSkillsProvider(
skillsDir,
options: new AgentSkillsProviderOptions
{
DisableLoadSkillApproval = true,
DisableRunSkillScriptApproval = true,
});

// ── Create agent ─────────────────────────────────────────────────────────────
// Toolbox MCP tools (create_session etc.) are injected automatically by
// AddFoundryToolboxes via the hosting layer — no manual McpClient needed.
AIAgent baseAgent = new AIProjectClient(projectEndpoint, new DefaultAzureCredential())
AIAgent baseAgent = new AIProjectClient(
projectEndpoint,
new DefaultAzureCredential())
.AsAIAgent(new ChatClientAgentOptions
{
Name = "browser-automation",
Description = "A browser automation agent using Playwright via Foundry Toolbox",
Description = "A browser automation agent using Azure Playwright tools",
ChatOptions = new ChatOptions
{
ModelId = deployment,
Instructions = systemPrompt,
Tools = localTools,
},
AIContextProviders = [skillsProvider],
});

// ── Middleware pipeline ──────────────────────────────────────────────────────
// 1. Function invocation middleware: intercepts create_session results to store
// cdp_url + live_view_url server-side (model never sees them).
// 2. Agent-level middleware: injects live_view_url into response post-call.
var agent = baseAgent
.AsBuilder()
.Use(Middlewares.FunctionInvocationMiddleware)
.Use(Middlewares.LiveViewUrlMiddleware, Middlewares.LiveViewUrlStreamingMiddleware)
.Use(
Middlewares.LiveViewUrlMiddleware,
Middlewares.LiveViewUrlStreamingMiddleware)
.Build();

// ── Host setup ───────────────────────────────────────────────────────────────
var builder = AgentHost.CreateBuilder(args);
builder.Services.AddFoundryResponses(agent);

// Pass a credential that forces https://ai.azure.com/.default scope for toolbox auth.
// The framework's FoundryToolboxBearerTokenHandler uses cognitiveservices.azure.com which
// some regions reject. This wrapper overrides the scope while using DefaultAzureCredential.
var toolboxCredential = new ToolboxScopedCredential(new DefaultAzureCredential());

builder.Services.AddFoundryToolboxes(toolboxCredential, opt => opt.ApiVersion = "v1", toolboxName);
builder.RegisterProtocol("responses", endpoints => endpoints.MapFoundryResponses());
builder.Services.AddFoundryToolboxes(
new ToolboxScopedCredential(new DefaultAzureCredential()),
options => options.ApiVersion = "v1",
toolboxName);
builder.RegisterProtocol(
"responses",
endpoints => endpoints.MapFoundryResponses());

var app = builder.Build();
app.Run();
Loading
Loading