Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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,30 @@
#
# This workflow demonstrates accepting file-based input as workflow input.
#
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
127 changes: 127 additions & 0 deletions dotnet/samples/03-workflows/Declarative/FileInput/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// 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()
{
// Initialize configuration
IConfiguration configuration = Application.InitializeConfig();
Uri foundryEndpoint = new(configuration.GetValue(Application.Settings.FoundryEndpoint));

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

string filePath = Path.Combine(AppContext.BaseDirectory, "ProductBrief.txt");
await using UploadedFile uploadedFile = await UploadInputFileAsync(foundryEndpoint, filePath);

// 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(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 async Task<UploadedFile> UploadInputFileAsync(Uri foundryEndpoint, string filePath)
{
// 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(filePath);
OpenAIFile openAIFile = await fileClient.UploadFileAsync(
fileStream,
Path.GetFileName(filePath),
FileUploadPurpose.Assistants).ConfigureAwait(false);

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

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

private static ChatMessage CreateInputMessage(string fileId)
{
return new ChatMessage(
ChatRole.User,
[
new TextContent("Summarize the attached file for a launch announcement. File name: ProductBrief.txt"),
new HostedFileContent(fileId),
]);
}

private sealed record UploadedFile(OpenAIFileClient FileClient, string FileId) : IAsyncDisposable
{
public async ValueTask DisposeAsync()
{
try
{
await this.FileClient.DeleteFileAsync(this.FileId).ConfigureAwait(false);
}
catch (Exception ex)
{
Console.Error.WriteLine($"Unable to delete uploaded file {this.FileId}: {ex.Message}");
}
}
}
}
15 changes: 15 additions & 0 deletions dotnet/samples/03-workflows/Declarative/FileInput/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Declarative workflow file input

This sample demonstrates how to provide file-based input to a declarative workflow. It uploads the bundled `ProductBrief.txt` 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
```

The sample always uploads `ProductBrief.txt` from this project. The program creates a `ChatMessage` whose content includes the prompt and uploaded file reference, then 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.
12 changes: 12 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,15 @@ 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
```

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