Skip to content
Open
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
4 changes: 4 additions & 0 deletions dotnet/agent-framework-dotnet.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,9 @@
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/">
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/HostedFoundryAgent.csproj" />
</Folder>
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/">
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/HostedFiles.csproj" />
</Folder>
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/">
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/HostedLocalTools.csproj" />
</Folder>
Expand All @@ -332,6 +335,7 @@
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/HostedWorkflowSimple.csproj" />
</Folder>
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/">
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SessionFilesClient/SessionFilesClient.csproj" />
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent/SimpleAgent.csproj" />
</Folder>
<Folder Name="/Samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
**/bin
**/obj
**/.vs
**/.vscode
.env
*.user
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
AZURE_AI_PROJECT_ENDPOINT=<your-azure-ai-project-endpoint>
ASPNETCORE_URLS=http://+:8088
ASPNETCORE_ENVIRONMENT=Development
AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
AZURE_BEARER_TOKEN=DefaultAzureCredential
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Use the official .NET 10.0 ASP.NET runtime as a parent image
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
WORKDIR /app

FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY . .
RUN dotnet restore
RUN dotnet publish -c Release -o /app/publish

# Final stage
FROM base AS final
WORKDIR /app
COPY --from=build /app/publish .
EXPOSE 8088
ENV ASPNETCORE_URLS=http://+:8088
ENTRYPOINT ["dotnet", "HostedFiles.dll"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Dockerfile for contributors building from the agent-framework repository source.
#
# This project uses ProjectReference to the local Microsoft.Agents.AI.Foundry source,
# which means a standard multi-stage Docker build cannot resolve dependencies outside
# this folder. Instead, pre-publish the app targeting the container runtime and copy
# the output into the container:
#
# dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out
# docker build -f Dockerfile.contributor -t hosted-files .
# docker run --rm -p 8088:8088 -e AGENT_NAME=hosted-files -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN --env-file .env hosted-files
#
# For end-users consuming the NuGet package (not ProjectReference), use the standard
# Dockerfile which performs a full dotnet restore + publish inside the container.
FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final
WORKDIR /app
COPY out/ .
EXPOSE 8088
ENV ASPNETCORE_URLS=http://+:8088
ENTRYPOINT ["dotnet", "HostedFiles.dll"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFrameworks>net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<CentralPackageTransitivePinningEnabled>false</CentralPackageTransitivePinningEnabled>
<RootNamespace>HostedFiles</RootNamespace>
<AssemblyName>HostedFiles</AssemblyName>
<NoWarn>$(NoWarn);</NoWarn>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Azure.AI.Projects" VersionOverride="2.1.0-beta.1" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="DotNetEnv" />
</ItemGroup>

<!-- For contributors: uses ProjectReference to build against local source -->
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Foundry.Hosting\Microsoft.Agents.AI.Foundry.Hosting.csproj" />
</ItemGroup>

<!-- For end-users: uncomment the PackageReference below and remove the ProjectReference above
<ItemGroup>
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.0.0" />
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.0.0" />
</ItemGroup>
-->

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
// Copyright (c) Microsoft. All rights reserved.

// Hosted Files Agent - A hosted agent that reads files from the per-session
// $HOME sandbox volume using local C# function tools.
//
// In Foundry hosted-agent mode, every session is backed by an isolated
// micro-VM with a persistent $HOME directory. Files uploaded to the session
// (via the AgentSessionFiles SDK or `azd ai agent files upload`) appear under
// $HOME and can be read by tools running inside the agent process.
//
// Required environment variables:
// AZURE_AI_PROJECT_ENDPOINT - Azure AI Foundry project endpoint
// AZURE_AI_MODEL_DEPLOYMENT_NAME - Model deployment name (default: gpt-4o)
//
// Optional:
// AGENT_NAME - Agent name (default: hosted-files)

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

// Load .env file if present (for local development)
Env.TraversePath().Load();

string endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT")
?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o";

// Use a chained credential: try a temporary dev token first (for local Docker debugging),
// then fall back to DefaultAzureCredential (for local dev via dotnet run / managed identity in production).
TokenCredential credential = new ChainedTokenCredential(
new DevTemporaryTokenCredential(),
new DefaultAzureCredential());

// ── Tools: read files from the session $HOME volume ──────────────────────────

// $HOME resolves to the per-session sandbox volume on Foundry. Locally it
// resolves to the OS user profile, which lets the sample run unmodified
// during development.
string Home() =>
Environment.GetEnvironmentVariable("HOME")
?? Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile);

[Description("Get the absolute path of the session home directory ($HOME).")]
string GetHomeDirectory() => Home();

[Description("List files and directories under the given path inside the session sandbox. Pass an empty string to list $HOME.")]
string[] ListFiles(
[Description("Path relative to $HOME (or absolute). Empty string means $HOME.")] string path)
{
try
{
string target = ResolveSessionPath(path);
return Directory.EnumerateFileSystemEntries(target).ToArray();
}
catch (Exception ex)
{
return [$"Error listing '{path}': {ex.Message}"];
}
}

[Description("Read the full text contents of a file inside the session sandbox.")]
string ReadFile(
[Description("Path relative to $HOME (or absolute) of the file to read.")] string path)
{
try
{
string target = ResolveSessionPath(path);
return File.ReadAllText(target);
}
catch (Exception ex)
{
return $"Error reading '{path}': {ex.Message}";
}
}

string ResolveSessionPath(string path)
{
if (string.IsNullOrWhiteSpace(path))
{
return Home();
}

Comment thread
rogerbarreto marked this conversation as resolved.
Outdated
return Path.IsPathRooted(path) ? path : Path.Combine(Home(), path);
}
Comment thread
rogerbarreto marked this conversation as resolved.
Outdated

// ── Create and host the agent ────────────────────────────────────────────────

AIAgent agent = new AIProjectClient(new Uri(endpoint), credential)
.AsAIAgent(
model: deploymentName,
instructions: """
You are a friendly assistant that helps users inspect and summarise
files stored in the session sandbox at $HOME.

Always answer file-related questions by calling the available tools
(GetHomeDirectory, ListFiles, ReadFile). Do not guess file paths or
contents — read the file before answering.

Quote numbers and figures verbatim from the file rather than
paraphrasing them.
""",
name: Environment.GetEnvironmentVariable("AGENT_NAME") ?? "hosted-files",
description: "Hosted agent that reads files from the per-session $HOME volume",
tools:
[
AIFunctionFactory.Create(GetHomeDirectory),
AIFunctionFactory.Create(ListFiles),
AIFunctionFactory.Create(ReadFile),
]);

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddFoundryResponses(agent);

var app = builder.Build();
app.MapFoundryResponses();

if (app.Environment.IsDevelopment())
{
app.MapFoundryResponses("openai/v1");
}

app.Run();

// ── DevTemporaryTokenCredential ───────────────────────────────────────────────

/// <summary>
/// A <see cref="TokenCredential"/> for local Docker debugging only.
/// Reads a pre-fetched bearer token from the <c>AZURE_BEARER_TOKEN</c> environment variable
/// once at startup. This should NOT be used in production.
///
/// Generate a token on your host and pass it to the container:
/// export AZURE_BEARER_TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv)
/// docker run -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN ...
/// </summary>
internal sealed class DevTemporaryTokenCredential : TokenCredential
{
private const string EnvironmentVariable = "AZURE_BEARER_TOKEN";
private readonly string? _token;

public DevTemporaryTokenCredential()
{
this._token = Environment.GetEnvironmentVariable(EnvironmentVariable);
}

public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken)
=> this.GetAccessToken();

public override ValueTask<AccessToken> GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken)
=> new(this.GetAccessToken());

private AccessToken GetAccessToken()
{
if (string.IsNullOrEmpty(this._token) || this._token == "DefaultAzureCredential")
{
throw new CredentialUnavailableException($"{EnvironmentVariable} environment variable is not set.");
}

return new AccessToken(this._token, DateTimeOffset.UtcNow.AddHours(1));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# Hosted-Files

A hosted agent that reads files from the **per-session `$HOME` sandbox volume**. Each Foundry hosted-agent session is backed by an isolated micro-VM with its own persistent `$HOME`. Files uploaded to a session appear there and can be read by tools running inside the agent process.

The agent exposes three local C# function tools:

| Tool | Description |
|------|-------------|
| `GetHomeDirectory` | Returns the absolute path of `$HOME` for the current session. |
| `ListFiles` | Lists files and directories under a given path inside the sandbox. |
| `ReadFile` | Reads the full text contents of a file inside the sandbox. |

Companion sample: [`Using-Samples/SessionFilesClient`](../Using-Samples/SessionFilesClient/) — a REPL that uploads, lists, downloads, and deletes session files using the alpha `Azure.AI.Projects.AgentSessionFiles` SDK, then chats with this agent.

## Prerequisites

- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)
- An Azure AI Foundry project with a deployed model (e.g., `gpt-4o`)
- Azure CLI logged in (`az login`)

## Configuration

Copy the template and fill in your project endpoint:

```bash
cp .env.example .env
```

Edit `.env`:

```env
AZURE_AI_PROJECT_ENDPOINT=https://<your-account>.services.ai.azure.com/api/projects/<your-project>
ASPNETCORE_URLS=http://+:8088
ASPNETCORE_ENVIRONMENT=Development
AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
```

> `.env` is gitignored. The `.env.example` template is checked in as a reference.

## Running directly (contributors)

```bash
cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files
AGENT_NAME=hosted-files dotnet run
```

The agent starts on `http://localhost:8088`.

## Sending files to the agent

Files must be uploaded into the session's `$HOME` before the agent can read them. The bundled sample file is [`resources/contoso_q1_2026_report.txt`](./resources/contoso_q1_2026_report.txt).

### Code-first (recommended for demos)

Use the companion REPL [`SessionFilesClient`](../Using-Samples/SessionFilesClient/), which exercises the alpha `Azure.AI.Projects.AgentSessionFiles` SDK directly:

```bash
cd ../Using-Samples/SessionFilesClient
AGENT_NAME=hosted-files AGENT_ENDPOINT=http://localhost:8088 dotnet run
Comment thread
rogerbarreto marked this conversation as resolved.
Outdated

> upload ../../Hosted-Files/resources/contoso_q1_2026_report.txt
> ls
> What was Contoso's Q1 2026 total revenue? Quote the figure verbatim.
```

### CLI-first (parity with Python sample)

Using the Azure Developer CLI:

```bash
azd ai agent invoke "Hi!" # creates a session
azd ai agent files upload -f resources/contoso_q1_2026_report.txt
azd ai agent invoke "What was Contoso's Q1 2026 total revenue? Quote the figure verbatim."
```

The `--session-id` flag selects a specific session; without it the CLI uploads to the most recently active session. Run `azd ai agent files upload -h` for the full set of options.

## Running with Docker

This project uses `ProjectReference`, so use `Dockerfile.contributor` which takes a pre-published output:

```bash
dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out
docker build -f Dockerfile.contributor -t hosted-files .

export AZURE_BEARER_TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv)
docker run --rm -p 8088:8088 \
-e AGENT_NAME=hosted-files \
-e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN \
--env-file .env \
hosted-files
```

## NuGet package users

If consuming the Agent Framework as a NuGet package, use the standard `Dockerfile` instead of `Dockerfile.contributor` and switch the `ProjectReference` entries in `HostedFiles.csproj` to `PackageReference` (commented section in the csproj).

## How session files work

| Layer | Lifetime | Notes |
|-------|----------|-------|
| `$HOME` | Lifetime of the session (TTL: 30 days) | Persists across invocations within a session. |
| `/tmp` | Container process | Use for non-persistent scratch space. |
| Conversation | Indefinite | Stored separately from `$HOME`. |

Each Foundry hosted-agent session = one container = one `$HOME`. Files uploaded with `AgentSessionFiles.UploadSessionFileAsync(agentName, sessionId, sessionStoragePath, localPath)` land at `$HOME/<sessionStoragePath>`. The agent's tools resolve relative paths against `$HOME`.
Loading
Loading