mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
* .NET: Add a TODO AIContextProvider (#5233) * Add a TODO AIContextProvider * Add unit tests * Address PR comments * Address PR comments * Fix test after removing one tool * .NET: Add a ModeProvider for managing agent modes (#5247) * Add a ModeProvider for managing agent modes * Fix typo * Fix typo * Fix typo * Address PR comments * .NET: Add sample to show how to build a harness (#5268) * Add sample to show how to build a harness * Improve sample * Sample max output tokens and model * Fix encoding * Fix model name in readme * Address PR comments * .NET: Add context window size compaction strategy for harness (#5304) * Add context window size compaction strategy for harness * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Address PR comments --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * .NET: Add a file memory provider (#5315) * Add a file memory provider * Address PR comments * Fix review comments. * Add additional unit tests * Addressing PR comments. * .NET: Harness: Improve prompts and add FileSystem store (#5365) * Harness: Improve prompts and add FileSystem store * Address PR comments * .NET: Harness: Improve path validation (#5404) * Harness: Improve path validation * Address PR comments * .NET: Add always approve helpers, improve sample and fix bug (#5451) * Add always approve helpers, improve sample and fix bug * Address PR comments * .NET: Make Todo, Mode and FileMemory providers more configurable (#5477) * Make Todo, Mode and FileMemory providers more configurable * Address PR comments. * .NET: Add subagents provider and sample (#5518) * Add subagents provider and sample * Addressing PR comments. * .NET: Harness filememory index plus instructions consistency (#5540) * Add FileMemoryProvider index and improve instruction consistency * Address PR comments. * Address PR comments * Address PR comments. * Apply suggestion from @rogerbarreto Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> --------- Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com> * .NET: Refactor harness console to be more extensible and easy to understand with better UX (#5573) * Refactor harness console to be more extensible and easy to understand with better UX. * Fix formatting issues. * Allow multiple clarifications in one response * Address PR comments * .NET: Add FileAccessProvdider and concurrency fix for FileMemoryProvider (#5583) * Add FileAccessProvdider and concurrency fix for FileMemoryProvider * Address PR comments --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>
181 lines
9.6 KiB
C#
181 lines
9.6 KiB
C#
// Copyright (c) Microsoft. All rights reserved.
|
|
|
|
using System.Collections.Generic;
|
|
using System.ComponentModel;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.Extensions.AI;
|
|
using Microsoft.Shared.DiagnosticIds;
|
|
using Microsoft.Shared.Diagnostics;
|
|
|
|
namespace Microsoft.Agents.AI;
|
|
|
|
/// <summary>
|
|
/// An <see cref="AIContextProvider"/> that provides file access tools to an agent
|
|
/// for saving, reading, deleting, listing, and searching files.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// The <see cref="FileAccessProvider"/> gives agents the ability to work with files
|
|
/// in a folder that the user has granted access to. Unlike <see cref="FileMemoryProvider"/>,
|
|
/// which provides session-scoped memory that may be isolated per session, <see cref="FileAccessProvider"/>
|
|
/// operates on a shared, persistent folder whose contents are visible across sessions and agents.
|
|
/// This makes it suitable for reading input data, writing output artifacts, and working with
|
|
/// files that have a lifetime beyond any single agent session.
|
|
/// </para>
|
|
/// <para>
|
|
/// File access is mediated through a <see cref="AgentFileStore"/> abstraction, allowing pluggable
|
|
/// backends (in-memory, local file system, remote blob storage, etc.).
|
|
/// </para>
|
|
/// <para>
|
|
/// This provider exposes the following tools to the agent:
|
|
/// <list type="bullet">
|
|
/// <item><description><c>SaveFile</c> — Save a file with the given name and content.</description></item>
|
|
/// <item><description><c>ReadFile</c> — Read the content of a file by name.</description></item>
|
|
/// <item><description><c>DeleteFile</c> — Delete a file by name.</description></item>
|
|
/// <item><description><c>ListFiles</c> — List all file names.</description></item>
|
|
/// <item><description><c>SearchFiles</c> — Search file contents using a regular expression pattern.</description></item>
|
|
/// </list>
|
|
/// </para>
|
|
/// </remarks>
|
|
[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)]
|
|
public sealed class FileAccessProvider : AIContextProvider
|
|
{
|
|
private const string DefaultInstructions =
|
|
"""
|
|
## File Access
|
|
You have access to a shared file storage area via the `FileAccess_*` tools for reading, writing, and managing files.
|
|
These files persist beyond the current session and may be shared across sessions or agents.
|
|
Use these tools to read input data provided by the user, write output artifacts, and manage any files the user has asked you to work with.
|
|
|
|
- Never delete or overwrite existing files unless the user has explicitly asked you to do so.
|
|
""";
|
|
|
|
private readonly AgentFileStore _fileStore;
|
|
private readonly string _instructions;
|
|
private AITool[]? _tools;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="FileAccessProvider"/> class.
|
|
/// </summary>
|
|
/// <param name="fileStore">
|
|
/// The file store implementation used for storage operations.
|
|
/// The store should already be scoped to the desired folder or storage location.
|
|
/// </param>
|
|
/// <param name="options">Optional settings that control provider behavior. When <see langword="null"/>, defaults are used.</param>
|
|
/// <exception cref="System.ArgumentNullException">Thrown when <paramref name="fileStore"/> is <see langword="null"/>.</exception>
|
|
public FileAccessProvider(AgentFileStore fileStore, FileAccessProviderOptions? options = null)
|
|
{
|
|
Throw.IfNull(fileStore);
|
|
|
|
this._fileStore = fileStore;
|
|
this._instructions = options?.Instructions ?? DefaultInstructions;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override IReadOnlyList<string> StateKeys => [];
|
|
|
|
/// <inheritdoc />
|
|
protected override ValueTask<AIContext> ProvideAIContextAsync(InvokingContext context, CancellationToken cancellationToken = default)
|
|
{
|
|
return new ValueTask<AIContext>(new AIContext
|
|
{
|
|
Instructions = this._instructions,
|
|
Tools = this._tools ??= this.CreateTools(),
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// Save a file with the given name and content. By default, does not overwrite an existing file unless overwrite is set to true.
|
|
/// </summary>
|
|
/// <param name="fileName">The name of the file to save.</param>
|
|
/// <param name="content">The content to write to the file.</param>
|
|
/// <param name="overwrite">Whether to overwrite the file if it already exists.</param>
|
|
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
|
/// <returns>A confirmation message.</returns>
|
|
[Description("Save a file with the given name and content. By default, does not overwrite an existing file unless overwrite is set to true.")]
|
|
private async Task<string> SaveFileAsync(string fileName, string content, bool overwrite = false, CancellationToken cancellationToken = default)
|
|
{
|
|
string path = StorePaths.NormalizeRelativePath(fileName);
|
|
|
|
if (!overwrite && await this._fileStore.FileExistsAsync(path, cancellationToken).ConfigureAwait(false))
|
|
{
|
|
return $"File '{fileName}' already exists. To replace it, save again with overwrite set to true.";
|
|
}
|
|
|
|
await this._fileStore.WriteFileAsync(path, content, cancellationToken).ConfigureAwait(false);
|
|
return $"File '{fileName}' saved.";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Read the content of a file by name. Returns the file content or a message indicating the file was not found.
|
|
/// </summary>
|
|
/// <param name="fileName">The name of the file to read.</param>
|
|
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
|
/// <returns>The file content or a not-found message.</returns>
|
|
[Description("Read the content of a file by name. Returns the file content or a message indicating the file was not found.")]
|
|
private async Task<string> ReadFileAsync(string fileName, CancellationToken cancellationToken = default)
|
|
{
|
|
string path = StorePaths.NormalizeRelativePath(fileName);
|
|
string? content = await this._fileStore.ReadFileAsync(path, cancellationToken).ConfigureAwait(false);
|
|
return content ?? $"File '{fileName}' not found.";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Delete a file by name.
|
|
/// </summary>
|
|
/// <param name="fileName">The name of the file to delete.</param>
|
|
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
|
/// <returns>A confirmation or not-found message.</returns>
|
|
[Description("Delete a file by name.")]
|
|
private async Task<string> DeleteFileAsync(string fileName, CancellationToken cancellationToken = default)
|
|
{
|
|
string path = StorePaths.NormalizeRelativePath(fileName);
|
|
bool deleted = await this._fileStore.DeleteFileAsync(path, cancellationToken).ConfigureAwait(false);
|
|
return deleted ? $"File '{fileName}' deleted." : $"File '{fileName}' not found.";
|
|
}
|
|
|
|
/// <summary>
|
|
/// List all file names.
|
|
/// </summary>
|
|
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
|
/// <returns>A list of file names.</returns>
|
|
[Description("List all file names.")]
|
|
private async Task<List<string>> ListFilesAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
IReadOnlyList<string> fileNames = await this._fileStore.ListFilesAsync(string.Empty, cancellationToken).ConfigureAwait(false);
|
|
return new List<string>(fileNames);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Search file contents using a regular expression pattern (case-insensitive).
|
|
/// Optionally filter which files to search using a glob pattern.
|
|
/// </summary>
|
|
/// <param name="regexPattern">A regular expression pattern to match against file contents (case-insensitive).</param>
|
|
/// <param name="filePattern">An optional glob pattern to filter which files to search (e.g., "*.md", "research*"). Leave empty or omit to search all files.</param>
|
|
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
|
/// <returns>A list of search results with matching file names, snippets, and matching lines.</returns>
|
|
[Description("Search file contents using a regular expression pattern (case-insensitive). Optionally filter which files to search using a glob pattern (e.g., \"*.md\", \"research*\"). Returns matching file names, snippets, and matching lines with line numbers.")]
|
|
private async Task<List<FileSearchResult>> SearchFilesAsync(string regexPattern, string? filePattern = null, CancellationToken cancellationToken = default)
|
|
{
|
|
string? pattern = string.IsNullOrWhiteSpace(filePattern) ? null : filePattern;
|
|
IReadOnlyList<FileSearchResult> results = await this._fileStore.SearchFilesAsync(string.Empty, regexPattern, pattern, cancellationToken).ConfigureAwait(false);
|
|
return new List<FileSearchResult>(results);
|
|
}
|
|
|
|
private AITool[] CreateTools()
|
|
{
|
|
var serializerOptions = AgentJsonUtilities.DefaultOptions;
|
|
|
|
return
|
|
[
|
|
AIFunctionFactory.Create(this.SaveFileAsync, new AIFunctionFactoryOptions { Name = "FileAccess_SaveFile", SerializerOptions = serializerOptions }),
|
|
AIFunctionFactory.Create(this.ReadFileAsync, new AIFunctionFactoryOptions { Name = "FileAccess_ReadFile", SerializerOptions = serializerOptions }),
|
|
AIFunctionFactory.Create(this.DeleteFileAsync, new AIFunctionFactoryOptions { Name = "FileAccess_DeleteFile", SerializerOptions = serializerOptions }),
|
|
AIFunctionFactory.Create(this.ListFilesAsync, new AIFunctionFactoryOptions { Name = "FileAccess_ListFiles", SerializerOptions = serializerOptions }),
|
|
AIFunctionFactory.Create(this.SearchFilesAsync, new AIFunctionFactoryOptions { Name = "FileAccess_SearchFiles", SerializerOptions = serializerOptions }),
|
|
];
|
|
}
|
|
}
|