From c4d0259974e6a3e6588b8efcc5d2bcdac5530d00 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 27 Aug 2026 07:24:41 -0400 Subject: [PATCH 1/7] docs: adds a workflow file input example in dotnet Signed-off-by: Vincent Biret --- dotnet/agent-framework-dotnet.slnx | 1 + .../Declarative/FileInput/FileInput.csproj | 42 ++++++ .../Declarative/FileInput/FileInput.yaml | 40 ++++++ .../Declarative/FileInput/ProductBrief.txt | 16 +++ .../Declarative/FileInput/Program.cs | 134 ++++++++++++++++++ .../Declarative/FileInput/README.md | 21 +++ .../03-workflows/Declarative/README.md | 13 ++ .../Workflows/Execution/WorkflowRunner.cs | 11 ++ 8 files changed, 278 insertions(+) create mode 100644 dotnet/samples/03-workflows/Declarative/FileInput/FileInput.csproj create mode 100644 dotnet/samples/03-workflows/Declarative/FileInput/FileInput.yaml create mode 100644 dotnet/samples/03-workflows/Declarative/FileInput/ProductBrief.txt create mode 100644 dotnet/samples/03-workflows/Declarative/FileInput/Program.cs create mode 100644 dotnet/samples/03-workflows/Declarative/FileInput/README.md diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index 227175d03da..6f5fa9d46a6 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -242,6 +242,7 @@ + diff --git a/dotnet/samples/03-workflows/Declarative/FileInput/FileInput.csproj b/dotnet/samples/03-workflows/Declarative/FileInput/FileInput.csproj new file mode 100644 index 00000000000..622f3a33de6 --- /dev/null +++ b/dotnet/samples/03-workflows/Declarative/FileInput/FileInput.csproj @@ -0,0 +1,42 @@ + + + + Exe + net10.0 + enable + enable + + + + true + true + true + true + + + + + + + + + + + + + + + + + + + + + Always + + + Always + + + + diff --git a/dotnet/samples/03-workflows/Declarative/FileInput/FileInput.yaml b/dotnet/samples/03-workflows/Declarative/FileInput/FileInput.yaml new file mode 100644 index 00000000000..346d98984af --- /dev/null +++ b/dotnet/samples/03-workflows/Declarative/FileInput/FileInput.yaml @@ -0,0 +1,40 @@ +# +# 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: + + # Capture the complete incoming ChatMessage, including non-text content. + - kind: SetVariable + id: capture_input_message + variable: Local.InputMessage + value: =System.LastMessage + + # Show that the workflow can inspect the message text and content collection. + - kind: SendActivity + id: announce_file_input + activity: |- + Received file-based workflow input. + + Prompt: + {System.LastMessage.Text} + + Content item count: + {CountRows(System.LastMessage.Content)} + + # 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 diff --git a/dotnet/samples/03-workflows/Declarative/FileInput/ProductBrief.txt b/dotnet/samples/03-workflows/Declarative/FileInput/ProductBrief.txt new file mode 100644 index 00000000000..74777e0bb03 --- /dev/null +++ b/dotnet/samples/03-workflows/Declarative/FileInput/ProductBrief.txt @@ -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 diff --git a/dotnet/samples/03-workflows/Declarative/FileInput/Program.cs b/dotnet/samples/03-workflows/Declarative/FileInput/Program.cs new file mode 100644 index 00000000000..13b338b5643 --- /dev/null +++ b/dotnet/samples/03-workflows/Declarative/FileInput/Program.cs @@ -0,0 +1,134 @@ +// 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 Shared.Foundry; +using Shared.Workflows; + +namespace Demo.Workflows.Declarative.FileInput; + +/// +/// Demonstrate how to provide file-based input to a declarative workflow. +/// +/// +/// 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. +/// +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); + + // 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 file content. + // The workflow can inspect the message through System.LastMessage and forward it + // to agent-backed actions. + WorkflowRunner runner = new(); + await runner.ExecuteAsync(workflowFactory.CreateWorkflow, CreateInputMessage(workflowInput)); + } + + 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"); + 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 ChatMessage CreateInputMessage(FileWorkflowInput input) + { + string fileName = Path.GetFileName(input.FilePath); + string mediaType = InferMediaType(input.FilePath); + byte[] fileBytes = File.ReadAllBytes(input.FilePath); + string fileDataUri = $"data:{mediaType};base64,{Convert.ToBase64String(fileBytes)}"; + + return new ChatMessage( + ChatRole.User, + [ + new TextContent($"{input.Prompt} File name: {fileName}"), + new DataContent(fileDataUri) + { + Name = fileName, + }, + ]); + } + + private static string InferMediaType(string filePath) + { + string extension = Path.GetExtension(filePath); + return extension.ToUpperInvariant() switch + { + ".CSV" => "text/csv", + ".GIF" => "image/gif", + ".HTML" or ".HTM" => "text/html", + ".JPEG" or ".JPG" => "image/jpeg", + ".JSON" => "application/json", + ".MD" => "text/markdown", + ".PDF" => "application/pdf", + ".PNG" => "image/png", + ".TXT" => "text/plain", + ".WEBP" => "image/webp", + ".XML" => "application/xml", + _ => "application/octet-stream", + }; + } + + private sealed record FileWorkflowInput(string FilePath, string Prompt); +} diff --git a/dotnet/samples/03-workflows/Declarative/FileInput/README.md b/dotnet/samples/03-workflows/Declarative/FileInput/README.md new file mode 100644 index 00000000000..23a484e7069 --- /dev/null +++ b/dotnet/samples/03-workflows/Declarative/FileInput/README.md @@ -0,0 +1,21 @@ +# Declarative workflow file input + +This sample demonstrates how to provide file-based input to a declarative workflow. It converts a local file into a `ChatMessage` with both `TextContent` and `DataContent`, then starts a YAML-defined workflow with that message. + +The workflow captures `System.LastMessage`, displays the message text and content count, and forwards the complete message to a Foundry-backed 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 creates a `ChatMessage` whose content includes the prompt and the file bytes, so the declarative workflow can access the input through `System.LastMessage` and pass the same message to downstream actions. diff --git a/dotnet/samples/03-workflows/Declarative/README.md b/dotnet/samples/03-workflows/Declarative/README.md index 1fe87a6a782..c13cd863282 100644 --- a/dotnet/samples/03-workflows/Declarative/README.md +++ b/dotnet/samples/03-workflows/Declarative/README.md @@ -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 file bytes, 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." +``` + +See [FileInput](./FileInput/) for details. diff --git a/dotnet/src/Shared/Workflows/Execution/WorkflowRunner.cs b/dotnet/src/Shared/Workflows/Execution/WorkflowRunner.cs index b5573ec80e1..c0c3f882104 100644 --- a/dotnet/src/Shared/Workflows/Execution/WorkflowRunner.cs +++ b/dotnet/src/Shared/Workflows/Execution/WorkflowRunner.cs @@ -54,6 +54,17 @@ public WorkflowRunner(params IEnumerable functions) } public async Task ExecuteAsync(Func workflowProvider, string input) + { + await this.ExecuteCoreAsync(workflowProvider, input).ConfigureAwait(false); + } + + public async Task ExecuteAsync(Func workflowProvider, ChatMessage input) + { + await this.ExecuteCoreAsync(workflowProvider, input).ConfigureAwait(false); + } + + private async Task ExecuteCoreAsync(Func workflowProvider, TInput input) + where TInput : notnull { // Reset EOF flag so a reused WorkflowRunner instance handles stdin correctly on each run. this._stdinEof = false; From bc0eb76c806257b937fbbeeba1fdc5a9b9dc764c Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 27 Aug 2026 08:43:03 -0400 Subject: [PATCH 2/7] fix: switches to a hosted file since model doesn't support the data content ones Signed-off-by: Vincent Biret --- .../Declarative/FileInput/FileInput.csproj | 1 + .../Declarative/FileInput/FileInput.yaml | 13 +--- .../Declarative/FileInput/Program.cs | 71 +++++++++++-------- .../Declarative/FileInput/README.md | 6 +- .../03-workflows/Declarative/README.md | 2 +- 5 files changed, 49 insertions(+), 44 deletions(-) diff --git a/dotnet/samples/03-workflows/Declarative/FileInput/FileInput.csproj b/dotnet/samples/03-workflows/Declarative/FileInput/FileInput.csproj index 622f3a33de6..530b9d1bda3 100644 --- a/dotnet/samples/03-workflows/Declarative/FileInput/FileInput.csproj +++ b/dotnet/samples/03-workflows/Declarative/FileInput/FileInput.csproj @@ -23,6 +23,7 @@ + diff --git a/dotnet/samples/03-workflows/Declarative/FileInput/FileInput.yaml b/dotnet/samples/03-workflows/Declarative/FileInput/FileInput.yaml index 346d98984af..a6f13d9b981 100644 --- a/dotnet/samples/03-workflows/Declarative/FileInput/FileInput.yaml +++ b/dotnet/samples/03-workflows/Declarative/FileInput/FileInput.yaml @@ -11,13 +11,9 @@ trigger: id: workflow_demo actions: - # Capture the complete incoming ChatMessage, including non-text content. - - kind: SetVariable - id: capture_input_message - variable: Local.InputMessage - value: =System.LastMessage - - # Show that the workflow can inspect the message text and content collection. + # 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: |- @@ -26,9 +22,6 @@ trigger: Prompt: {System.LastMessage.Text} - Content item count: - {CountRows(System.LastMessage.Content)} - # 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 diff --git a/dotnet/samples/03-workflows/Declarative/FileInput/Program.cs b/dotnet/samples/03-workflows/Declarative/FileInput/Program.cs index 13b338b5643..7c97a122a86 100644 --- a/dotnet/samples/03-workflows/Declarative/FileInput/Program.cs +++ b/dotnet/samples/03-workflows/Declarative/FileInput/Program.cs @@ -5,6 +5,7 @@ using Azure.Identity; using Microsoft.Extensions.AI; using Microsoft.Extensions.Configuration; +using OpenAI.Files; using Shared.Foundry; using Shared.Workflows; @@ -29,17 +30,18 @@ public static async Task Main(string[] args) 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 file content. - // The workflow can inspect the message through System.LastMessage and forward it - // to agent-backed actions. + // 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)); + await runner.ExecuteAsync(workflowFactory.CreateWorkflow, CreateInputMessage(workflowInput, uploadedFile.FileId)); } private static async Task CreateAgentAsync(Uri foundryEndpoint, IConfiguration configuration) @@ -92,43 +94,52 @@ private static FileWorkflowInput ParseWorkflowInput(string[] args) return new FileWorkflowInput(filePath, prompt); } - private static ChatMessage CreateInputMessage(FileWorkflowInput input) + private static async Task 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); - string mediaType = InferMediaType(input.FilePath); - byte[] fileBytes = File.ReadAllBytes(input.FilePath); - string fileDataUri = $"data:{mediaType};base64,{Convert.ToBase64String(fileBytes)}"; return new ChatMessage( ChatRole.User, [ new TextContent($"{input.Prompt} File name: {fileName}"), - new DataContent(fileDataUri) - { - Name = fileName, - }, + new HostedFileContent(fileId), ]); } - private static string InferMediaType(string filePath) + private sealed record FileWorkflowInput(string FilePath, string Prompt); + + private sealed record UploadedFile(OpenAIFileClient FileClient, string FileId) : IAsyncDisposable { - string extension = Path.GetExtension(filePath); - return extension.ToUpperInvariant() switch + public async ValueTask DisposeAsync() { - ".CSV" => "text/csv", - ".GIF" => "image/gif", - ".HTML" or ".HTM" => "text/html", - ".JPEG" or ".JPG" => "image/jpeg", - ".JSON" => "application/json", - ".MD" => "text/markdown", - ".PDF" => "application/pdf", - ".PNG" => "image/png", - ".TXT" => "text/plain", - ".WEBP" => "image/webp", - ".XML" => "application/xml", - _ => "application/octet-stream", - }; + await this.FileClient.DeleteFileAsync(this.FileId).ConfigureAwait(false); + } } - - private sealed record FileWorkflowInput(string FilePath, string Prompt); } diff --git a/dotnet/samples/03-workflows/Declarative/FileInput/README.md b/dotnet/samples/03-workflows/Declarative/FileInput/README.md index 23a484e7069..c5fdbfb0bc9 100644 --- a/dotnet/samples/03-workflows/Declarative/FileInput/README.md +++ b/dotnet/samples/03-workflows/Declarative/FileInput/README.md @@ -1,8 +1,8 @@ # Declarative workflow file input -This sample demonstrates how to provide file-based input to a declarative workflow. It converts a local file into a `ChatMessage` with both `TextContent` and `DataContent`, then starts a YAML-defined workflow with that message. +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 captures `System.LastMessage`, displays the message text and content count, and forwards the complete message to a Foundry-backed agent. +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 @@ -18,4 +18,4 @@ By default the sample uses `ProductBrief.txt` from this project. To provide a di 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 creates a `ChatMessage` whose content includes the prompt and the file bytes, so the declarative workflow can access the input through `System.LastMessage` and pass the same message to downstream actions. +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. diff --git a/dotnet/samples/03-workflows/Declarative/README.md b/dotnet/samples/03-workflows/Declarative/README.md index c13cd863282..47c63b6baef 100644 --- a/dotnet/samples/03-workflows/Declarative/README.md +++ b/dotnet/samples/03-workflows/Declarative/README.md @@ -101,7 +101,7 @@ To run the sampes from the command line: ### File-based input The `FileInput` sample demonstrates starting a declarative workflow with a `ChatMessage` -that contains file bytes, not just text: +that contains an uploaded file reference, not just text: ```pwsh cd dotnet/samples/03-workflows/Declarative/FileInput From 860c36de9e655800db7f59d07e19209e61501829 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 27 Aug 2026 12:59:32 +0000 Subject: [PATCH 3/7] fix: simplify declarative file input sample Co-authored-by: baywet <7905502+baywet@users.noreply.github.com> --- .../Declarative/FileInput/FileInput.csproj | 1 - .../Declarative/FileInput/FileInput.yaml | 12 +-- .../Declarative/FileInput/Program.cs | 84 ++----------------- .../Declarative/FileInput/README.md | 12 +-- 4 files changed, 14 insertions(+), 95 deletions(-) diff --git a/dotnet/samples/03-workflows/Declarative/FileInput/FileInput.csproj b/dotnet/samples/03-workflows/Declarative/FileInput/FileInput.csproj index 530b9d1bda3..622f3a33de6 100644 --- a/dotnet/samples/03-workflows/Declarative/FileInput/FileInput.csproj +++ b/dotnet/samples/03-workflows/Declarative/FileInput/FileInput.csproj @@ -23,7 +23,6 @@ - diff --git a/dotnet/samples/03-workflows/Declarative/FileInput/FileInput.yaml b/dotnet/samples/03-workflows/Declarative/FileInput/FileInput.yaml index a6f13d9b981..01c41f01f18 100644 --- a/dotnet/samples/03-workflows/Declarative/FileInput/FileInput.yaml +++ b/dotnet/samples/03-workflows/Declarative/FileInput/FileInput.yaml @@ -1,8 +1,5 @@ # -# This workflow demonstrates accepting file-based input as workflow input. -# -# Example input: -# dotnet run ProductBrief.txt "Summarize this product brief for a launch announcement." +# This workflow demonstrates accepting a text file's content as workflow input. # kind: Workflow trigger: @@ -11,9 +8,7 @@ trigger: 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. + # Show that the workflow can inspect the text message. - kind: SendActivity id: announce_file_input activity: |- @@ -22,8 +17,7 @@ trigger: 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. + # Invoke an agent in the original conversation. - kind: InvokeAzureAgent id: summarize_file conversationId: =System.ConversationId diff --git a/dotnet/samples/03-workflows/Declarative/FileInput/Program.cs b/dotnet/samples/03-workflows/Declarative/FileInput/Program.cs index 7c97a122a86..3420eff0418 100644 --- a/dotnet/samples/03-workflows/Declarative/FileInput/Program.cs +++ b/dotnet/samples/03-workflows/Declarative/FileInput/Program.cs @@ -5,7 +5,6 @@ using Azure.Identity; using Microsoft.Extensions.AI; using Microsoft.Extensions.Configuration; -using OpenAI.Files; using Shared.Foundry; using Shared.Workflows; @@ -20,7 +19,7 @@ namespace Demo.Workflows.Declarative.FileInput; /// internal sealed class Program { - public static async Task Main(string[] args) + public static async Task Main() { // Initialize configuration IConfiguration configuration = Application.InitializeConfig(); @@ -29,19 +28,14 @@ public static async Task Main(string[] args) // 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. + // Execute the workflow with the content from the bundled text file. WorkflowRunner runner = new(); - await runner.ExecuteAsync(workflowFactory.CreateWorkflow, CreateInputMessage(workflowInput, uploadedFile.FileId)); + await runner.ExecuteAsync(workflowFactory.CreateWorkflow, CreateInputMessage()); } private static async Task CreateAgentAsync(Uri foundryEndpoint, IConfiguration configuration) @@ -62,84 +56,22 @@ private static DeclarativeAgentDefinition DefineFileInputAgent(IConfiguration co { Instructions = """ - You summarize files that are provided as user input to a workflow. + You summarize product briefs provided as user input to a workflow. - When a file is attached, inspect the file content and provide: + 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"); - 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 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) + private static ChatMessage CreateInputMessage() { - string fileName = Path.GetFileName(input.FilePath); - + string productBrief = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "ProductBrief.txt")); return new ChatMessage( ChatRole.User, [ - new TextContent($"{input.Prompt} File name: {fileName}"), - new HostedFileContent(fileId), + new TextContent($"Summarize this product brief for a launch announcement:{Environment.NewLine}{Environment.NewLine}{productBrief}"), ]); } - - 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); - } - } } diff --git a/dotnet/samples/03-workflows/Declarative/FileInput/README.md b/dotnet/samples/03-workflows/Declarative/FileInput/README.md index c5fdbfb0bc9..7dcc9322c50 100644 --- a/dotnet/samples/03-workflows/Declarative/FileInput/README.md +++ b/dotnet/samples/03-workflows/Declarative/FileInput/README.md @@ -1,8 +1,8 @@ # 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. +This sample demonstrates how to provide file-based input to a declarative workflow. It reads the bundled `ProductBrief.txt` file and creates a `ChatMessage` with the file content before starting a YAML-defined workflow. -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. +The workflow displays `System.LastMessage.Text`, then invokes a Foundry-backed agent in the same workflow conversation. ## Run the sample @@ -12,10 +12,4 @@ Configure the common declarative workflow settings described in the parent [READ 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. +The sample always uses `ProductBrief.txt` from this project. Its contents are included in the workflow's input message. The YAML invokes the agent with `conversationId: =System.ConversationId` so the agent sees the same conversation item. From b83d2f21035850db74ec2bbe87841cf2a87dad90 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 27 Aug 2026 13:04:48 +0000 Subject: [PATCH 4/7] fix: upload fixed file input fixture Co-authored-by: baywet <7905502+baywet@users.noreply.github.com> --- .../Declarative/FileInput/FileInput.csproj | 1 + .../Declarative/FileInput/FileInput.yaml | 9 ++- .../Declarative/FileInput/Program.cs | 57 ++++++++++++++++--- .../Declarative/FileInput/README.md | 6 +- 4 files changed, 60 insertions(+), 13 deletions(-) diff --git a/dotnet/samples/03-workflows/Declarative/FileInput/FileInput.csproj b/dotnet/samples/03-workflows/Declarative/FileInput/FileInput.csproj index 622f3a33de6..530b9d1bda3 100644 --- a/dotnet/samples/03-workflows/Declarative/FileInput/FileInput.csproj +++ b/dotnet/samples/03-workflows/Declarative/FileInput/FileInput.csproj @@ -23,6 +23,7 @@ + diff --git a/dotnet/samples/03-workflows/Declarative/FileInput/FileInput.yaml b/dotnet/samples/03-workflows/Declarative/FileInput/FileInput.yaml index 01c41f01f18..ca7d1eff621 100644 --- a/dotnet/samples/03-workflows/Declarative/FileInput/FileInput.yaml +++ b/dotnet/samples/03-workflows/Declarative/FileInput/FileInput.yaml @@ -1,5 +1,5 @@ # -# This workflow demonstrates accepting a text file's content as workflow input. +# This workflow demonstrates accepting file-based input as workflow input. # kind: Workflow trigger: @@ -8,7 +8,9 @@ trigger: id: workflow_demo actions: - # Show that the workflow can inspect the text message. + # 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: |- @@ -17,7 +19,8 @@ trigger: Prompt: {System.LastMessage.Text} - # Invoke an agent in the original conversation. + # 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 diff --git a/dotnet/samples/03-workflows/Declarative/FileInput/Program.cs b/dotnet/samples/03-workflows/Declarative/FileInput/Program.cs index 3420eff0418..2fd68c7c31a 100644 --- a/dotnet/samples/03-workflows/Declarative/FileInput/Program.cs +++ b/dotnet/samples/03-workflows/Declarative/FileInput/Program.cs @@ -5,6 +5,7 @@ using Azure.Identity; using Microsoft.Extensions.AI; using Microsoft.Extensions.Configuration; +using OpenAI.Files; using Shared.Foundry; using Shared.Workflows; @@ -28,14 +29,19 @@ public static async Task Main() // 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 the content from the bundled text file. + // 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()); + await runner.ExecuteAsync(workflowFactory.CreateWorkflow, CreateInputMessage(uploadedFile.FileId)); } private static async Task CreateAgentAsync(Uri foundryEndpoint, IConfiguration configuration) @@ -56,22 +62,59 @@ private static DeclarativeAgentDefinition DefineFileInputAgent(IConfiguration co { Instructions = """ - You summarize product briefs provided as user input to a workflow. + You summarize files that are provided as user input to a workflow. - Provide: + 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 ChatMessage CreateInputMessage() + private static async Task 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 uploadedFile = await fileClient.UploadFileAsync( + fileStream, + Path.GetFileName(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(string fileId) { - string productBrief = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "ProductBrief.txt")); return new ChatMessage( ChatRole.User, [ - new TextContent($"Summarize this product brief for a launch announcement:{Environment.NewLine}{Environment.NewLine}{productBrief}"), + 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() + { + await this.FileClient.DeleteFileAsync(this.FileId).ConfigureAwait(false); + } + } } diff --git a/dotnet/samples/03-workflows/Declarative/FileInput/README.md b/dotnet/samples/03-workflows/Declarative/FileInput/README.md index 7dcc9322c50..85a6eb490b5 100644 --- a/dotnet/samples/03-workflows/Declarative/FileInput/README.md +++ b/dotnet/samples/03-workflows/Declarative/FileInput/README.md @@ -1,8 +1,8 @@ # Declarative workflow file input -This sample demonstrates how to provide file-based input to a declarative workflow. It reads the bundled `ProductBrief.txt` file and creates a `ChatMessage` with the file content before starting a YAML-defined workflow. +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. +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 @@ -12,4 +12,4 @@ Configure the common declarative workflow settings described in the parent [READ dotnet run ``` -The sample always uses `ProductBrief.txt` from this project. Its contents are included in the workflow's input message. The YAML invokes the agent with `conversationId: =System.ConversationId` so the agent sees the same conversation item. +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. From 9a20b3025d294e5a6a48e115abbd9555e7c67836 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 27 Aug 2026 13:05:57 +0000 Subject: [PATCH 5/7] fix: preserve workflow result on cleanup failure Co-authored-by: baywet <7905502+baywet@users.noreply.github.com> --- .../03-workflows/Declarative/FileInput/Program.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/dotnet/samples/03-workflows/Declarative/FileInput/Program.cs b/dotnet/samples/03-workflows/Declarative/FileInput/Program.cs index 2fd68c7c31a..19cb05f5b2c 100644 --- a/dotnet/samples/03-workflows/Declarative/FileInput/Program.cs +++ b/dotnet/samples/03-workflows/Declarative/FileInput/Program.cs @@ -114,7 +114,14 @@ private sealed record UploadedFile(OpenAIFileClient FileClient, string FileId) : { public async ValueTask DisposeAsync() { - await this.FileClient.DeleteFileAsync(this.FileId).ConfigureAwait(false); + try + { + await this.FileClient.DeleteFileAsync(this.FileId).ConfigureAwait(false); + } + catch (Exception ex) + { + Console.Error.WriteLine($"Unable to delete uploaded file {this.FileId}: {ex.Message}"); + } } } } From b4decdcab7125a46d92615ad13c7a34626a23e1e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 27 Aug 2026 13:07:03 +0000 Subject: [PATCH 6/7] refactor: clarify uploaded file naming Co-authored-by: baywet <7905502+baywet@users.noreply.github.com> --- .../samples/03-workflows/Declarative/FileInput/Program.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dotnet/samples/03-workflows/Declarative/FileInput/Program.cs b/dotnet/samples/03-workflows/Declarative/FileInput/Program.cs index 19cb05f5b2c..06d09b2385e 100644 --- a/dotnet/samples/03-workflows/Declarative/FileInput/Program.cs +++ b/dotnet/samples/03-workflows/Declarative/FileInput/Program.cs @@ -82,7 +82,7 @@ private static async Task UploadInputFileAsync(Uri foundryEndpoint OpenAIFileClient fileClient = aiProjectClient.GetProjectOpenAIClient().GetOpenAIFileClient(); using FileStream fileStream = File.OpenRead(filePath); - OpenAIFile uploadedFile = await fileClient.UploadFileAsync( + OpenAIFile openAIFile = await fileClient.UploadFileAsync( fileStream, Path.GetFileName(filePath), FileUploadPurpose.Assistants).ConfigureAwait(false); @@ -90,14 +90,14 @@ private static async Task UploadInputFileAsync(Uri foundryEndpoint Console.ForegroundColor = ConsoleColor.Cyan; try { - Console.WriteLine($"FILE: {uploadedFile.Id}"); + Console.WriteLine($"FILE: {openAIFile.Id}"); } finally { Console.ResetColor(); } - return new UploadedFile(fileClient, uploadedFile.Id); + return new UploadedFile(fileClient, openAIFile.Id); } private static ChatMessage CreateInputMessage(string fileId) From 1a8639032b06ec86069dafde00054f5fada78323 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 27 Aug 2026 09:09:25 -0400 Subject: [PATCH 7/7] docs: removes outdated command --- dotnet/samples/03-workflows/Declarative/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/dotnet/samples/03-workflows/Declarative/README.md b/dotnet/samples/03-workflows/Declarative/README.md index 47c63b6baef..745d4fe122b 100644 --- a/dotnet/samples/03-workflows/Declarative/README.md +++ b/dotnet/samples/03-workflows/Declarative/README.md @@ -106,7 +106,6 @@ 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." ``` See [FileInput](./FileInput/) for details.