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
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ public class PlanningQuestion
/// Only used for clarification questions. Null when no predefined choices are offered.
/// </summary>
[JsonPropertyName("choices")]
[Description("For clarifications, this has a list of options that the user can choose from. null for approvals.")]
[Description("""
For clarifications, this has a list of options that the user can choose from.
null for approvals.

Note: for clarifications, the user will always also be presented with a free form input option, so make sure that each choice provided here is a valid input for the next turn.
E.g. if the question is "Which stock are you referring to?" then valid choices might be ["AAPL", "MSFT", "GOOG"], and the user could also type their own answer.
Invalid choices would be ["Enter tickers directly", "Paste tickers"], since these conflict with the already existing freeform option, and don't directly provide valid inputs for the next turn.
""")]
public List<string>? Choices { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@

var instructions =
"""
You are a data analyst assistant. You have access to a folder of data files via the FileAccess_* tools.
You are a data analyst assistant. You have access to a folder of data files via the file_access_* tools.

## Getting started
- Start by listing available files with FileAccess_ListFiles to see what data is available.
- Start by listing available files with file_access_list_files to see what data is available.
- Read the files to understand their structure and contents.

## Working with data
Expand All @@ -46,7 +46,7 @@ You are a data analyst assistant. You have access to a folder of data files via
- When calculations are needed, work through them step by step and show your reasoning.

## Writing output
- When asked to produce output files (e.g., reports, summaries, filtered data), use FileAccess_SaveFile to write them.
- When asked to produce output files (e.g., reports, summaries, filtered data), use file_access_save_file to write them.
- Use appropriate file formats: CSV for tabular data, Markdown for reports.
- Confirm what you wrote and where.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,12 @@ namespace Microsoft.Agents.AI;
/// <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>
/// <item><description><c>file_access_save_file</c> — Save a file with the given name and content.</description></item>
/// <item><description><c>file_access_read_file</c> — Read the content of a file by name.</description></item>
/// <item><description><c>file_access_delete_file</c> — Delete a file by name.</description></item>
/// <item><description><c>file_access_list_files</c> — List the direct child file names in a directory.</description></item>
/// <item><description><c>file_access_list_subdirectories</c> — List the direct child subdirectory names in a directory.</description></item>
/// <item><description><c>file_access_search_files</c> — Recursively search file contents using a regular expression pattern.</description></item>
/// </list>
/// </para>
/// </remarks>
Expand All @@ -45,11 +46,13 @@ 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.
You have access to a shared file storage area via the `file_access_*` 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.
- Files may be organized into subdirectories. Use `file_access_list_files` and `file_access_list_subdirectories` to explore the tree level by level,
or `file_access_search_files` to search file contents recursively across the whole store.
""";

private readonly AgentFileStore _fileStore;
Expand Down Expand Up @@ -137,30 +140,56 @@ private async Task<string> DeleteFileAsync(string fileName, CancellationToken ca
}

/// <summary>
/// List all file names.
/// List the direct child file names of a directory. Omit <paramref name="directory"/> (or pass an empty string)
/// to list the store root. To enumerate files in a subdirectory, pass its relative path.
/// </summary>
/// <param name="directory">The relative directory path to list. Omit or pass an empty string to list the store root.</param>
/// <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)
[Description("List the direct child file names of a directory. Omit the directory (or pass an empty string) to list the root. To enumerate files in a subdirectory, pass its relative path, for example \"reports\" or \"reports/2024\".")]
private async Task<List<string>> ListFilesAsync(string? directory = null, CancellationToken cancellationToken = default)
{
IReadOnlyList<string> fileNames = await this._fileStore.ListFilesAsync(string.Empty, cancellationToken).ConfigureAwait(false);
string target = string.IsNullOrWhiteSpace(directory) ? string.Empty : directory;
IReadOnlyList<string> fileNames = await this._fileStore.ListFilesAsync(target, cancellationToken).ConfigureAwait(false);
return new List<string>(fileNames);
}

/// <summary>
/// Search file contents using a regular expression pattern (case-insensitive).
/// List the direct child subdirectory names of a directory. Omit <paramref name="directory"/> (or pass an empty string)
/// to list the store root. To enumerate subdirectories of a subdirectory, pass its relative path.
/// </summary>
/// <param name="directory">The relative directory path to list. Omit or pass an empty string to list the store root.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>A list of subdirectory names.</returns>
[Description("List the direct child subdirectory names of a directory. Omit the directory (or pass an empty string) to list the root. To enumerate subdirectories of a subdirectory, pass its relative path, for example \"reports\" or \"reports/2024\". Use this together with file_access_list_files to explore the directory tree level by level.")]
private async Task<List<string>> ListSubdirectoriesAsync(string? directory = null, CancellationToken cancellationToken = default)
{
string target = string.IsNullOrWhiteSpace(directory) ? string.Empty : directory;
IReadOnlyList<string> directoryNames = await this._fileStore.ListDirectoriesAsync(target, cancellationToken).ConfigureAwait(false);
return new List<string>(directoryNames);
}

/// <summary>
/// Search the contents of all files in the store (recursively) 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="filePattern">An optional glob pattern to filter which files to search, matched against each file's path relative to the store root. Use <c>**</c> to match across subdirectories (e.g., "**/*.md"). 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.")]
/// <returns>A list of search results whose file names are paths relative to the store root.</returns>
[Description(
"""
Search the contents of all files in the store (recursively, across all subdirectories) using a regular expression pattern (case-insensitive).
Optionally filter which files to search using a glob pattern matched against each file's path relative to the store root:
- '*' matches within a single path segment
- '**' matches across subdirectories, so use \"**/*.md\" to match markdown files at any depth, or \"reports/**\" to restrict the search to the 'reports' subtree.

Returns matching results whose file names are paths relative to the store root (usable with file_access_read_file), along with 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);
IReadOnlyList<FileSearchResult> results = await this._fileStore.SearchFilesAsync(string.Empty, regexPattern, pattern, recursive: true, cancellationToken).ConfigureAwait(false);
return new List<FileSearchResult>(results);
}

Expand All @@ -170,11 +199,12 @@ private AITool[] CreateTools()

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 }),
AIFunctionFactory.Create(this.SaveFileAsync, new AIFunctionFactoryOptions { Name = "file_access_save_file", SerializerOptions = serializerOptions }),
AIFunctionFactory.Create(this.ReadFileAsync, new AIFunctionFactoryOptions { Name = "file_access_read_file", SerializerOptions = serializerOptions }),
AIFunctionFactory.Create(this.DeleteFileAsync, new AIFunctionFactoryOptions { Name = "file_access_delete_file", SerializerOptions = serializerOptions }),
AIFunctionFactory.Create(this.ListFilesAsync, new AIFunctionFactoryOptions { Name = "file_access_list_files", SerializerOptions = serializerOptions }),
AIFunctionFactory.Create(this.ListSubdirectoriesAsync, new AIFunctionFactoryOptions { Name = "file_access_list_subdirectories", SerializerOptions = serializerOptions }),
AIFunctionFactory.Create(this.SearchFilesAsync, new AIFunctionFactoryOptions { Name = "file_access_search_files", SerializerOptions = serializerOptions }),
];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ private async Task<List<FileSearchResult>> SearchFilesAsync(string regexPattern,
{
FileMemoryState state = this._sessionState.GetOrInitializeState(AIAgent.CurrentRunContext?.Session);
string? pattern = string.IsNullOrWhiteSpace(filePattern) ? null : filePattern;
IReadOnlyList<FileSearchResult> results = await this._fileStore.SearchFilesAsync(state.WorkingFolder, regexPattern, pattern, cancellationToken).ConfigureAwait(false);
IReadOnlyList<FileSearchResult> results = await this._fileStore.SearchFilesAsync(state.WorkingFolder, regexPattern, pattern, recursive: false, cancellationToken).ConfigureAwait(false);
Comment thread
rogerbarreto marked this conversation as resolved.

// Filter out internal files (description sidecars and memory index) so they stay hidden.
var filtered = new List<FileSearchResult>(results.Count);
Expand Down
24 changes: 20 additions & 4 deletions dotnet/src/Microsoft.Agents.AI/Harness/FileStore/AgentFileStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,14 @@ public abstract class AgentFileStore
/// <returns>A list of file names in the specified directory (direct children only).</returns>
public abstract Task<IReadOnlyList<string>> ListFilesAsync(string directory, CancellationToken cancellationToken = default);

/// <summary>
/// Lists the direct child subdirectories of a directory.
/// </summary>
/// <param name="directory">The relative path of the directory to list. Use an empty string for the root.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>A list of subdirectory names in the specified directory (direct children only).</returns>
public abstract Task<IReadOnlyList<string>> ListDirectoriesAsync(string directory, CancellationToken cancellationToken = default);

/// <summary>
/// Checks whether a file exists.
/// </summary>
Expand All @@ -76,12 +84,20 @@ public abstract class AgentFileStore
/// </param>
/// <param name="filePattern">
/// An optional glob pattern to filter which files are searched (e.g., <c>"*.md"</c>, <c>"research*"</c>).
/// When <see langword="null"/>, all files in the directory are searched.
/// Uses standard glob syntax from <see cref="Matcher"/>.
/// When <see langword="null"/>, all files are searched.
/// Uses standard glob syntax from <see cref="Matcher"/>, matched against each file's path relative to
/// <paramref name="directory"/>. Use <c>**</c> to match across subdirectories (e.g., <c>"**/*.md"</c>).
/// </param>
/// <param name="recursive">
/// When <see langword="true"/>, all descendant files of <paramref name="directory"/> are searched.
/// When <see langword="false"/> (default), only the direct children of <paramref name="directory"/> are searched.
/// </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>
public abstract Task<IReadOnlyList<FileSearchResult>> SearchFilesAsync(string directory, string regexPattern, string? filePattern = null, CancellationToken cancellationToken = default);
/// <returns>
/// A list of search results. Each result's <see cref="FileSearchResult.FileName"/> is the matching file's
/// path relative to <paramref name="directory"/>.
/// </returns>
public abstract Task<IReadOnlyList<FileSearchResult>> SearchFilesAsync(string directory, string regexPattern, string? filePattern = null, bool recursive = false, CancellationToken cancellationToken = default);

/// <summary>
/// Ensures a directory exists, creating it if necessary.
Expand Down
Loading
Loading