Skip to content

Commit

Permalink
Merge the az-cli and az-ps agent changes to the main branch (#224)
Browse files Browse the repository at this point in the history
* Update Azure agents based off the refactored main branch
* Minor update in doc
* Update `az-cli` agent to use the new request/response schemas (#195)
* Merge the parameter injection prototype into the `azureAgents` branch (#208)
  1. Add the `/replace` command
  2. Retrieve data locally with 2 tiers of data source to assist user to input values for placeholders.
      - The 1st tier is the cache data generated from parsing the AzCLI help content. We the description and static values for a parameter from this data source.
      - The 2nd tier is the AzCLI tab completion, to get the dynamic values for a parameter, such as `--resource-group`.
  3. Fix a bug when handling remote query.
  4. Fix the data retrieval code to handle the case when the generated command is not AzCLI command
  5. Send pseudo values to the server handler instead of real values from user to avoid any privacy issue. After receiving response, the agent will replace all pseudo values with the real values before displaying to the user.

* A few improvements to parameter injection (#213)
  1. Do not include 'placeholderSet' in a history entry when its value is null.
  2. Reduce the fake wait time when replacing user values from 2.5s to 2s.
  3. Change the format for a pseudo value from '__replace_<cnt>_v__' to '__pseudo_<cnt>_v__' to avoid confusion caused to GPT by the word 'replace'.

* Update the endpoint URL for `az-ps` and the connection string for application insights (#211)
* Small update to improve the diagnosis when an agent throws exception (#215)
* Update build accordingly
  • Loading branch information
daxian-dbw authored Sep 10, 2024
1 parent d30bf52 commit 2fe79f4
Show file tree
Hide file tree
Showing 21 changed files with 2,523 additions and 56 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ these are the supported agents:

Agent README files:

- [`az-cli` & `az-ps`][13]
- [`openai-gpt`][08]
- [`ollama`][06]
- [`interpreter`][07]
Expand Down Expand Up @@ -158,3 +159,4 @@ bugs, suggestions, or feedback.
[10]: https://github.com/PowerShell/ProjectMercury/issues
[11]: https://learn.microsoft.com/powershell/scripting/install/installing-powershell
[12]: ./docs/SECURITY.md
[13]: ./shell/agents/AIShell.Azure.Agent/README.md
12 changes: 10 additions & 2 deletions build.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ function Start-Build
[string] $Runtime = [NullString]::Value,

[Parameter()]
[ValidateSet('openai-gpt', 'interpreter', 'ollama')]
[ValidateSet('openai-gpt', 'az-agent', 'interpreter', 'ollama')]
[string[]] $AgentToInclude,

[Parameter()]
Expand All @@ -40,7 +40,7 @@ function Start-Build
if (-not $AgentToInclude) {
$agents = $metadata.AgentsToInclude
$AgentToInclude = if ($agents -eq "*") {
@('openai-gpt', 'interpreter', 'ollama')
@('openai-gpt', 'az-agent', 'interpreter', 'ollama')
} else {
$agents.Split(",", [System.StringSplitOptions]::TrimEntries)
Write-Verbose "Include agents specified in Metadata.json"
Expand All @@ -63,6 +63,7 @@ function Start-Build
$module_dir = Join-Path $shell_dir "AIShell.Integration"

$openai_agent_dir = Join-Path $agent_dir "AIShell.OpenAI.Agent"
$az_agent_dir = Join-Path $agent_dir "AIShell.Azure.Agent"
$interpreter_agent_dir = Join-Path $agent_dir "AIShell.Interpreter.Agent"
$ollama_agent_dir = Join-Path $agent_dir "AIShell.Ollama.Agent"

Expand All @@ -73,6 +74,7 @@ function Start-Build
$module_help_dir= Join-Path $PSScriptRoot "docs" "cmdlets"

$openai_out_dir = Join-Path $app_out_dir "agents" "AIShell.OpenAI.Agent"
$az_out_dir = Join-Path $app_out_dir "agents" "AIShell.Azure.Agent"
$interpreter_out_dir = Join-Path $app_out_dir "agents" "AIShell.Interpreter.Agent"
$ollama_out_dir = Join-Path $app_out_dir "agents" "AIShell.Ollama.Agent"

Expand All @@ -93,6 +95,12 @@ function Start-Build
dotnet publish $openai_csproj -c $Configuration -o $openai_out_dir
}

if ($LASTEXITCODE -eq 0 -and $AgentToInclude -contains 'az-agent') {
Write-Host "`n[Build the Azure agents ...]`n" -ForegroundColor Green
$az_csproj = GetProjectFile $az_agent_dir
dotnet publish $az_csproj -c $Configuration -o $az_out_dir
}

if ($LASTEXITCODE -eq 0 -and $AgentToInclude -contains 'interpreter') {
Write-Host "`n[Build the Interpreter agent ...]`n" -ForegroundColor Green
$interpreter_csproj = GetProjectFile $interpreter_agent_dir
Expand Down
2 changes: 2 additions & 0 deletions docs/FAQ.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ agents:

Agent README files:

- [`az-cli` & `az-ps`][05]
- [`openai-gpt`][04]
- [`ollama`][02]
- [`interpreter`][03]
Expand All @@ -44,3 +45,4 @@ documentation for your terminal application to see if it supports this feature.
[02]: ../shell/agents/AIShell.Ollama.Agent/README.md
[03]: ../shell/agents/AIShell.Interpreter.Agent/README.md
[04]: ../shell/agents/AIShell.OpenAI.Agent/README.md
[05]: ../shell/agents/AIShell.Azure.Agent/README.md
71 changes: 46 additions & 25 deletions shell/AIShell.Abstraction/IHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,14 +93,20 @@ public interface IHost
/// <param name="elements">Label and value pairs to be rendered for the object.</param>
void RenderList<T>(T source, IList<IRenderElement<T>> elements);

/// <summary>
/// Render a divider with the passed-in text.
/// </summary>
/// <param name="text">A brief caption for the subsequent section.</param>
void RenderDivider(string text);

/// <summary>
/// Run an asynchronouse task with a spinner on the console showing the task in progress.
/// </summary>
/// <typeparam name="T">The return type of the asynchronouse task.</typeparam>
/// <param name="func">The asynchronouse task.</param>
/// <param name="status">The status message to be shown.</param>
/// <returns>The returned result of <paramref name="func"/></returns>
Task<T> RunWithSpinnerAsync<T>(Func<Task<T>> func, string status);
Task<T> RunWithSpinnerAsync<T>(Func<Task<T>> func, string status, SpinnerKind? spinnerKind);

/// <summary>
/// Run an asynchronouse task with a spinner on the console showing the task in progress.
Expand All @@ -110,12 +116,22 @@ public interface IHost
/// <param name="func">The asynchronouse task, which can change the status of the spinner.</param>
/// <param name="status">The initial status message to be shown.</param>
/// <returns>The returned result of <paramref name="func"/></returns>
Task<T> RunWithSpinnerAsync<T>(Func<IStatusContext, Task<T>> func, string status);
Task<T> RunWithSpinnerAsync<T>(Func<IStatusContext, Task<T>> func, string status, SpinnerKind? spinnerKind);

/// <summary>
/// Run an asynchronouse task with the default spinner and the default status message.
/// </summary>
Task<T> RunWithSpinnerAsync<T>(Func<Task<T>> func) => RunWithSpinnerAsync(func, "Generating...", SpinnerKind.Generating);

/// <summary>
/// Run an asynchronouse task with a spinner with the default status message.
/// Run an asynchronouse task with the default spinner and the specified status message.
/// </summary>
Task<T> RunWithSpinnerAsync<T>(Func<Task<T>> func) => RunWithSpinnerAsync(func, "Generating...");
Task<T> RunWithSpinnerAsync<T>(Func<Task<T>> func, string status) => RunWithSpinnerAsync(func, status, SpinnerKind.Generating);

/// <summary>
/// Run an asynchronouse task that allows changing the status message with the default spinner and the specified initial status message.
/// </summary>
Task<T> RunWithSpinnerAsync<T>(Func<IStatusContext, Task<T>> func, string status) => RunWithSpinnerAsync(func, status, SpinnerKind.Generating);

/// <summary>
/// Prompt for selection asynchronously.
Expand Down Expand Up @@ -173,9 +189,9 @@ Task<string> PromptForTextAsync(string prompt, bool optional, CancellationToken
/// Prompt for the user to input the value for an argument placeholder.
/// </summary>
/// <param name="argInfo">Information about the argument placeholder.</param>
/// <param name="cancellationToken">Token to cancel operation.</param>
/// <param name="printCaption">Indicates if the caption, such as the description and restriction, should be printed.</param>
/// <returns></returns>
string PromptForArgument(ArgumentInfo argInfo, CancellationToken cancellationToken);
string PromptForArgument(ArgumentInfo argInfo, bool printCaption);
}

/// <summary>
Expand All @@ -189,19 +205,38 @@ public interface IStatusContext
void Status(string status);
}

/// <summary>
/// Enum type for the kind of spinner to use.
/// </summary>
public enum SpinnerKind
{
/// <summary>
/// This spinner indicates text is being generated.
/// It should be used when generating response in chat.
/// This is the default spinner kind used by the host.
/// </summary>
Generating,

/// <summary>
/// This spinner indicates a general task processing.
/// It should be used in all other cases, such as loading data, etc.
/// </summary>
Processing,
}

/// <summary>
/// Information about an argument placeholder.
/// </summary>
public sealed class ArgumentInfo
public class ArgumentInfo
{
/// <summary>
/// Type of the argument data.
/// </summary>
public enum DataType
{
String,
Int,
Bool,
@string,
@int,
@bool,
}

/// <summary>
Expand All @@ -224,18 +259,13 @@ public enum DataType
/// </summary>
public DataType Type { get; }

/// <summary>
/// Gets a value indicating whether the user must choose from the suggestions.
/// </summary>
public bool MustChooseFromSuggestions { get; }

/// <summary>
/// Gets the list of suggestions for the argument.
/// </summary>
public IList<string> Suggestions { get; }

public ArgumentInfo(string name, string description, DataType dataType)
: this(name, description, restriction: null, dataType, mustChooseFromSuggestions: false, suggestions: null)
: this(name, description, restriction: null, dataType, suggestions: null)
{
}

Expand All @@ -244,24 +274,15 @@ public ArgumentInfo(
string description,
string restriction,
DataType dataType,
bool mustChooseFromSuggestions,
IList<string> suggestions)
{
ArgumentException.ThrowIfNullOrEmpty(name);
ArgumentException.ThrowIfNullOrEmpty(description);

if (mustChooseFromSuggestions && (suggestions is null || suggestions.Count < 2))
{
throw new ArgumentException(
$"A suggestion list with at least 2 items is required when '{nameof(MustChooseFromSuggestions)}' is true.",
nameof(suggestions));
}

Name = name;
Description = description;
Restriction = restriction;
Type = dataType;
MustChooseFromSuggestions = mustChooseFromSuggestions;
Suggestions = suggestions;
}
}
68 changes: 42 additions & 26 deletions shell/AIShell.Kernel/Host.cs
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,18 @@ public void RenderList<T>(T source, IList<IRenderElement<T>> elements)
}

/// <inheritdoc/>
public async Task<T> RunWithSpinnerAsync<T>(Func<Task<T>> func, string status = null)
public void RenderDivider(string text)
{
ArgumentException.ThrowIfNullOrEmpty(text);
RequireStdoutOrStderr(operation: "render divider");

AnsiConsole.Write(new Rule($"[yellow]{text.EscapeMarkup()}[/]")
.RuleStyle("grey")
.LeftJustified());
}

/// <inheritdoc/>
public async Task<T> RunWithSpinnerAsync<T>(Func<Task<T>> func, string status = null, SpinnerKind? spinnerKind = null)
{
if (_outputRedirected && _errorRedirected)
{
Expand All @@ -333,7 +344,7 @@ public async Task<T> RunWithSpinnerAsync<T>(Func<Task<T>> func, string status =
return await ansiConsole
.Status()
.AutoRefresh(true)
.Spinner(AsciiLetterSpinner.Default)
.Spinner(GetSpinner(spinnerKind))
.SpinnerStyle(new Style(Color.Olive))
.StartAsync(
$"[italic slowblink]{status.EscapeMarkup()}[/]",
Expand All @@ -347,7 +358,7 @@ public async Task<T> RunWithSpinnerAsync<T>(Func<Task<T>> func, string status =
}

/// <inheritdoc/>
public async Task<T> RunWithSpinnerAsync<T>(Func<IStatusContext, Task<T>> func, string status)
public async Task<T> RunWithSpinnerAsync<T>(Func<IStatusContext, Task<T>> func, string status, SpinnerKind? spinnerKind = null)
{
if (_outputRedirected && _errorRedirected)
{
Expand All @@ -370,7 +381,7 @@ public async Task<T> RunWithSpinnerAsync<T>(Func<IStatusContext, Task<T>> func,
return await ansiConsole
.Status()
.AutoRefresh(true)
.Spinner(AsciiLetterSpinner.Default)
.Spinner(GetSpinner(spinnerKind))
.SpinnerStyle(new Style(Color.Olive))
.StartAsync(
$"[italic slowblink]{status.EscapeMarkup()}[/]",
Expand Down Expand Up @@ -468,50 +479,45 @@ public async Task<string> PromptForTextAsync(string prompt, bool optional, IList
}

/// <inheritdoc/>
public string PromptForArgument(ArgumentInfo argInfo, CancellationToken cancellationToken)
public string PromptForArgument(ArgumentInfo argInfo, bool printCaption)
{
WriteLine($"{argInfo.Name}: {argInfo.Description}.");
if (!string.IsNullOrEmpty(argInfo.Restriction))
if (printCaption)
{
WriteLine(argInfo.Restriction);
}
WriteLine(argInfo.Type is ArgumentInfo.DataType.@string
? argInfo.Description
: $"{argInfo.Description}. Value type: {argInfo.Type}");

if (argInfo.Type is ArgumentInfo.DataType.Bool || argInfo.Suggestions?.Count is 2)
{
return PromptForTextAsync(
prompt: ">",
optional: false,
choices: argInfo.Suggestions ?? ["ture", "flase"],
cancellationToken: cancellationToken).GetAwaiter().GetResult();
if (!string.IsNullOrEmpty(argInfo.Restriction))
{
WriteLine(argInfo.Restriction);
}
}

if (argInfo.MustChooseFromSuggestions)
var suggestions = argInfo.Suggestions;
if (argInfo.Type is ArgumentInfo.DataType.@bool)
{
string value = PromptForSelectionAsync(
title: "Choose the value from the below list:",
choices: argInfo.Suggestions,
cancellationToken: cancellationToken).GetAwaiter().GetResult();
WriteLine($"> {value}");
return value;
suggestions ??= ["ture", "flase"];
}

var options = PSConsoleReadLine.GetOptions();
var oldAddToHistoryHandler = options.AddToHistoryHandler;
var oldReadLineHelper = options.ReadLineHelper;
var oldPredictionView = options.PredictionViewStyle;
var oldPredictionSource = options.PredictionSource;

var newOptions = new SetPSReadLineOption
{
ReadLineHelper = new PromptHelper(argInfo.Suggestions),
AddToHistoryHandler = c => AddToHistoryOption.SkipAdding,
ReadLineHelper = new PromptHelper(suggestions),
PredictionSource = PredictionSource.Plugin,
PredictionViewStyle = PredictionViewStyle.ListView,
};

try
{
Write("> ");
Markup($"[lime]{argInfo.Name}[/]: ");
PSConsoleReadLine.SetOptions(newOptions);
string value = PSConsoleReadLine.ReadLine();
string value = PSConsoleReadLine.ReadLine(CancellationToken.None);
if (Console.CursorLeft is not 0)
{
// Ctrl+c was pressed by the user.
Expand All @@ -523,6 +529,7 @@ public string PromptForArgument(ArgumentInfo argInfo, CancellationToken cancella
}
finally
{
newOptions.AddToHistoryHandler = oldAddToHistoryHandler;
newOptions.ReadLineHelper = oldReadLineHelper;
newOptions.PredictionSource = oldPredictionSource;
newOptions.PredictionViewStyle = oldPredictionView;
Expand All @@ -549,6 +556,15 @@ internal void RenderReferenceText(string header, string content)
AnsiConsole.WriteLine();
}

private static Spinner GetSpinner(SpinnerKind? kind)
{
return kind switch
{
SpinnerKind.Processing => Spinner.Known.Default,
_ => AsciiLetterSpinner.Default,
};
}

/// <summary>
/// Throw exception if standard input is redirected.
/// </summary>
Expand Down
4 changes: 2 additions & 2 deletions shell/AIShell.Kernel/Shell.cs
Original file line number Diff line number Diff line change
Expand Up @@ -604,7 +604,7 @@ internal async Task RunREPLAsync()
{
// Write out the remote query, in the same style as user typing.
Host.Markup($"\n>> Remote Query Received:\n");
Host.MarkupLine($"[teal]{input}[/]");
Host.MarkupLine($"[teal]{input.EscapeMarkup()}[/]");
}
else
{
Expand Down Expand Up @@ -736,7 +736,7 @@ Task<int> find_agent_op() => orchestrator.FindAgentForPrompt(
}

Host.WriteErrorLine()
.WriteErrorLine($"Agent failed to generate a response: {ex.Message}")
.WriteErrorLine($"Agent failed to generate a response: {ex.Message}\n{ex.StackTrace}")
.WriteErrorLine();
}
}
Expand Down
2 changes: 1 addition & 1 deletion shell/AIShell.Kernel/Utility/ReadLineHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,7 @@ internal class PromptHelper : IReadLineHelper
internal PromptHelper(IList<string> candidates)
{
_candidates = candidates;
_predictorName = "completion";
_predictorName = "suggestion";
_predictorId = new Guid(GUID);
}

Expand Down
Loading

0 comments on commit 2fe79f4

Please sign in to comment.