Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
1 change: 1 addition & 0 deletions dotnet/agent-framework-dotnet.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@
<Project Path="samples/03-workflows/Declarative/CustomerSupport/CustomerSupport.csproj" />
<Project Path="samples/03-workflows/Declarative/DeepResearch/DeepResearch.csproj" />
<Project Path="samples/03-workflows/Declarative/ExecuteWorkflow/ExecuteWorkflow.csproj" />
<Project Path="samples/03-workflows/Declarative/FileInput/FileInput.csproj" />
Comment thread
baywet marked this conversation as resolved.
<Project Path="samples/03-workflows/Declarative/FunctionTools/FunctionTools.csproj" />
<Project Path="samples/03-workflows/Declarative/HostedWorkflow/HostedWorkflow.csproj" />
<Project Path="samples/03-workflows/Declarative/InputArguments/InputArguments.csproj" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<PropertyGroup>
<InjectIsExternalInitOnLegacy>true</InjectIsExternalInitOnLegacy>
<InjectSharedFoundryAgents>true</InjectSharedFoundryAgents>
<InjectSharedWorkflowsExecution>true</InjectSharedWorkflowsExecution>
<InjectSharedWorkflowsSettings>true</InjectSharedWorkflowsSettings>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Azure.AI.Projects" />
<PackageReference Include="Microsoft.Extensions.Configuration" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="Microsoft.Extensions.Logging" />
<PackageReference Include="OpenAI" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Workflows.Declarative\Microsoft.Agents.AI.Workflows.Declarative.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Workflows.Declarative.Foundry\Microsoft.Agents.AI.Workflows.Declarative.Foundry.csproj" />
</ItemGroup>

<ItemGroup>
<None Include="FileInput.yaml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Include="ProductBrief.txt">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#
# This workflow demonstrates accepting file-based input as workflow input.
#
# Example input:
# dotnet run ProductBrief.txt "Summarize this product brief for a launch announcement."
#
kind: Workflow
trigger:

kind: OnConversationStart
id: workflow_demo
actions:

# Show that the workflow can inspect the text portion of the message.
# The uploaded file is already attached to the conversation and is available
# to agent-backed actions that use System.ConversationId.
- kind: SendActivity
id: announce_file_input
activity: |-
Received file-based workflow input.

Prompt:
{System.LastMessage.Text}

# Invoke an agent in the original conversation. The workflow root already added
# the file-bearing user message to this conversation before the first action ran.
- kind: InvokeAzureAgent
id: summarize_file
conversationId: =System.ConversationId
agent:
name: FileInputAgent
output:
autoSend: true
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
Product: Contoso Trail Bottle

The Contoso Trail Bottle is a lightweight stainless-steel water bottle designed
for hikers, commuters, and students. It keeps drinks cold for 24 hours, fits in
standard backpack side pockets, and uses a leak-resistant twist lid.

Audience:
- Weekend hikers
- Urban commuters
- Students who want a durable reusable bottle

Key differentiators:
- Recycled stainless-steel body
- Dishwasher-safe lid
- Replaceable silicone gasket
- Optional clip loop for backpacks
145 changes: 145 additions & 0 deletions dotnet/samples/03-workflows/Declarative/FileInput/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
// Copyright (c) Microsoft. All rights reserved.

using Azure.AI.Projects;
using Azure.AI.Projects.Agents;
using Azure.Identity;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Configuration;
using OpenAI.Files;
using Shared.Foundry;
using Shared.Workflows;

namespace Demo.Workflows.Declarative.FileInput;

/// <summary>
/// Demonstrate how to provide file-based input to a declarative workflow.
/// </summary>
/// <remarks>
/// See the README.md file in this folder and the parent folder (../README.md) for
/// detailed information about the configuration required to run this sample.
/// </remarks>
internal sealed class Program
{
public static async Task Main(string[] args)
{
// Initialize configuration
IConfiguration configuration = Application.InitializeConfig();
Uri foundryEndpoint = new(configuration.GetValue(Application.Settings.FoundryEndpoint));

// Ensure sample agents exist in Foundry.
await CreateAgentAsync(foundryEndpoint, configuration);

FileWorkflowInput workflowInput = ParseWorkflowInput(args);
await using UploadedFile uploadedFile = await UploadInputFileAsync(foundryEndpoint, workflowInput);

// Create the workflow factory. This class demonstrates how to initialize a
// declarative workflow from a YAML file. Once the workflow is created, it
// can be executed just like any regular workflow.
WorkflowFactory workflowFactory = new("FileInput.yaml", foundryEndpoint);

// Execute the workflow with a ChatMessage that contains both text and an uploaded
// file reference. Agent-backed actions can use the same workflow conversation to
// access the file.
WorkflowRunner runner = new();
await runner.ExecuteAsync(workflowFactory.CreateWorkflow, CreateInputMessage(workflowInput, uploadedFile.FileId));
}

private static async Task CreateAgentAsync(Uri foundryEndpoint, IConfiguration configuration)
{
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
AIProjectClient aiProjectClient = new(foundryEndpoint, new DefaultAzureCredential());

await aiProjectClient.CreateAgentAsync(
agentName: "FileInputAgent",
agentDefinition: DefineFileInputAgent(configuration),
agentDescription: "Summarizes files provided as declarative workflow input.");
}

private static DeclarativeAgentDefinition DefineFileInputAgent(IConfiguration configuration) =>
new(configuration.GetValue(Application.Settings.FoundryModel))
{
Instructions =
"""
You summarize files that are provided as user input to a workflow.

When a file is attached, inspect the file content and provide:
- A short summary
- Important facts or entities
- One suggested follow-up question

If no file content is available, explain that you did not receive a file.
"""
};

private static FileWorkflowInput ParseWorkflowInput(string[] args)
{
string filePath = args.FirstOrDefault() ?? Path.Combine(AppContext.BaseDirectory, "ProductBrief.txt");
Comment thread
baywet marked this conversation as resolved.
Outdated
if (!Path.IsPathFullyQualified(filePath))
{
filePath = Path.GetFullPath(filePath);
}

if (!File.Exists(filePath))
{
throw new FileNotFoundException($"Unable to locate input file: {filePath}", filePath);
}

string prompt =
args.Length > 1 ?
string.Join(' ', args.Skip(1)) :
"Summarize the attached file for a launch announcement.";

return new FileWorkflowInput(filePath, prompt);
}

private static async Task<UploadedFile> UploadInputFileAsync(Uri foundryEndpoint, FileWorkflowInput input)
{
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
AIProjectClient aiProjectClient = new(foundryEndpoint, new DefaultAzureCredential());
OpenAIFileClient fileClient = aiProjectClient.GetProjectOpenAIClient().GetOpenAIFileClient();

using FileStream fileStream = File.OpenRead(input.FilePath);
OpenAIFile uploadedFile = await fileClient.UploadFileAsync(
fileStream,
Path.GetFileName(input.FilePath),
FileUploadPurpose.Assistants).ConfigureAwait(false);

Console.ForegroundColor = ConsoleColor.Cyan;
try
{
Console.WriteLine($"FILE: {uploadedFile.Id}");
}
finally
{
Console.ResetColor();
}

return new UploadedFile(fileClient, uploadedFile.Id);
}

private static ChatMessage CreateInputMessage(FileWorkflowInput input, string fileId)
{
string fileName = Path.GetFileName(input.FilePath);

return new ChatMessage(
ChatRole.User,
[
new TextContent($"{input.Prompt} File name: {fileName}"),
new HostedFileContent(fileId),
]);
}

private sealed record FileWorkflowInput(string FilePath, string Prompt);

private sealed record UploadedFile(OpenAIFileClient FileClient, string FileId) : IAsyncDisposable
{
public async ValueTask DisposeAsync()
{
await this.FileClient.DeleteFileAsync(this.FileId).ConfigureAwait(false);
}
}
}
21 changes: 21 additions & 0 deletions dotnet/samples/03-workflows/Declarative/FileInput/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Declarative workflow file input

This sample demonstrates how to provide file-based input to a declarative workflow. It uploads a local file to the Foundry project, converts the uploaded file reference into a `ChatMessage` with both `TextContent` and `HostedFileContent`, then starts a YAML-defined workflow with that message.

The workflow displays `System.LastMessage.Text`, then invokes a Foundry-backed agent in the same workflow conversation so the uploaded file is available to the agent.

## Run the sample

Configure the common declarative workflow settings described in the parent [README](../README.md), then run:

```pwsh
dotnet run
```

By default the sample uses `ProductBrief.txt` from this project. To provide a different file and prompt:

```pwsh
dotnet run "C:\path\to\document.pdf" "Summarize this document for an executive audience."
```

The important part is that the file is not passed as plain text. The program uploads the file, creates a `ChatMessage` whose content includes the prompt and uploaded file reference, and starts the workflow with that message. The YAML invokes the agent with `conversationId: =System.ConversationId` so the agent sees the same conversation item that contains the file.
13 changes: 13 additions & 0 deletions dotnet/samples/03-workflows/Declarative/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,16 @@ To run the sampes from the command line:
dotnet run c:/myworkflows/Marketing.yaml
```
> The sample will allow for interactive input in the absence of an input argument.

### File-based input

The `FileInput` sample demonstrates starting a declarative workflow with a `ChatMessage`
that contains an uploaded file reference, not just text:

```pwsh
cd dotnet/samples/03-workflows/Declarative/FileInput
dotnet run
dotnet run "C:\path\to\document.pdf" "Summarize this document for an executive audience."
Comment thread
baywet marked this conversation as resolved.
Outdated
```

See [FileInput](./FileInput/) for details.
11 changes: 11 additions & 0 deletions dotnet/src/Shared/Workflows/Execution/WorkflowRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,17 @@ public WorkflowRunner(params IEnumerable<AIFunction> functions)
}

public async Task ExecuteAsync(Func<Workflow> workflowProvider, string input)
{
await this.ExecuteCoreAsync(workflowProvider, input).ConfigureAwait(false);
}

public async Task ExecuteAsync(Func<Workflow> workflowProvider, ChatMessage input)
{
await this.ExecuteCoreAsync(workflowProvider, input).ConfigureAwait(false);
}

private async Task ExecuteCoreAsync<TInput>(Func<Workflow> workflowProvider, TInput input)
where TInput : notnull
{
// Reset EOF flag so a reused WorkflowRunner instance handles stdin correctly on each run.
this._stdinEof = false;
Expand Down
Loading