// 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;
///
/// An that provides file access tools to an agent
/// for saving, reading, deleting, listing, and searching files.
///
///
///
/// The gives agents the ability to work with files
/// in a folder that the user has granted access to. Unlike ,
/// which provides session-scoped memory that may be isolated per session,
/// 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.
///
///
/// File access is mediated through a abstraction, allowing pluggable
/// backends (in-memory, local file system, remote blob storage, etc.).
///
///
/// This provider exposes the following tools to the agent:
///
/// - SaveFile — Save a file with the given name and content.
/// - ReadFile — Read the content of a file by name.
/// - DeleteFile — Delete a file by name.
/// - ListFiles — List all file names.
/// - SearchFiles — Search file contents using a regular expression pattern.
///
///
///
[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;
///
/// Initializes a new instance of the class.
///
///
/// The file store implementation used for storage operations.
/// The store should already be scoped to the desired folder or storage location.
///
/// Optional settings that control provider behavior. When , defaults are used.
/// Thrown when is .
public FileAccessProvider(AgentFileStore fileStore, FileAccessProviderOptions? options = null)
{
Throw.IfNull(fileStore);
this._fileStore = fileStore;
this._instructions = options?.Instructions ?? DefaultInstructions;
}
///
public override IReadOnlyList StateKeys => [];
///
protected override ValueTask ProvideAIContextAsync(InvokingContext context, CancellationToken cancellationToken = default)
{
return new ValueTask(new AIContext
{
Instructions = this._instructions,
Tools = this._tools ??= this.CreateTools(),
});
}
///
/// Save a file with the given name and content. By default, does not overwrite an existing file unless overwrite is set to true.
///
/// The name of the file to save.
/// The content to write to the file.
/// Whether to overwrite the file if it already exists.
/// A token to cancel the operation.
/// A confirmation message.
[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 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.";
}
///
/// Read the content of a file by name. Returns the file content or a message indicating the file was not found.
///
/// The name of the file to read.
/// A token to cancel the operation.
/// The file content or a not-found message.
[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 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.";
}
///
/// Delete a file by name.
///
/// The name of the file to delete.
/// A token to cancel the operation.
/// A confirmation or not-found message.
[Description("Delete a file by name.")]
private async Task 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.";
}
///
/// List all file names.
///
/// A token to cancel the operation.
/// A list of file names.
[Description("List all file names.")]
private async Task> ListFilesAsync(CancellationToken cancellationToken = default)
{
IReadOnlyList fileNames = await this._fileStore.ListFilesAsync(string.Empty, cancellationToken).ConfigureAwait(false);
return new List(fileNames);
}
///
/// Search file contents using a regular expression pattern (case-insensitive).
/// Optionally filter which files to search using a glob pattern.
///
/// A regular expression pattern to match against file contents (case-insensitive).
/// An optional glob pattern to filter which files to search (e.g., "*.md", "research*"). Leave empty or omit to search all files.
/// A token to cancel the operation.
/// A list of search results with matching file names, snippets, and matching lines.
[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> SearchFilesAsync(string regexPattern, string? filePattern = null, CancellationToken cancellationToken = default)
{
string? pattern = string.IsNullOrWhiteSpace(filePattern) ? null : filePattern;
IReadOnlyList results = await this._fileStore.SearchFilesAsync(string.Empty, regexPattern, pattern, cancellationToken).ConfigureAwait(false);
return new List(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 }),
];
}
}