diff --git a/shell/AIShell.Abstraction/ILLMAgent.cs b/shell/AIShell.Abstraction/ILLMAgent.cs
index 29b09502..a4e3fb2f 100644
--- a/shell/AIShell.Abstraction/ILLMAgent.cs
+++ b/shell/AIShell.Abstraction/ILLMAgent.cs
@@ -137,11 +137,12 @@ public interface ILLMAgent : IDisposable
void Initialize(AgentConfig config);
///
- /// Refresh the current chat by starting a new chat session.
+ /// Refresh the current chat or force starting a new chat session.
/// This method allows an agent to reset chat states, interact with user for authentication, print welcome message, and more.
///
/// The interface for interacting with the shell.
- Task RefreshChatAsync(IShell shell);
+ /// Whether or not to force creating a new chat session.
+ Task RefreshChatAsync(IShell shell, bool force);
///
/// Initiates a chat with the AI, using the provided input and shell.
diff --git a/shell/AIShell.Kernel/Command/RefreshCommand.cs b/shell/AIShell.Kernel/Command/RefreshCommand.cs
index 65b52886..55c8222f 100644
--- a/shell/AIShell.Kernel/Command/RefreshCommand.cs
+++ b/shell/AIShell.Kernel/Command/RefreshCommand.cs
@@ -19,6 +19,6 @@ private void RefreshAction()
var shell = (Shell)Shell;
shell.ShowBanner();
shell.ShowLandingPage();
- shell.ActiveAgent.Impl.RefreshChatAsync(Shell).GetAwaiter().GetResult();
+ shell.ActiveAgent.Impl.RefreshChatAsync(Shell, force: true).GetAwaiter().GetResult();
}
}
diff --git a/shell/AIShell.Kernel/Shell.cs b/shell/AIShell.Kernel/Shell.cs
index bd2bdb6f..dd99b309 100644
--- a/shell/AIShell.Kernel/Shell.cs
+++ b/shell/AIShell.Kernel/Shell.cs
@@ -555,7 +555,7 @@ internal async Task RunREPLAsync()
if (_shouldRefresh)
{
_shouldRefresh = false;
- await agent?.Impl.RefreshChatAsync(this);
+ await agent?.Impl.RefreshChatAsync(this, force: false);
}
if (Regenerate)
@@ -670,7 +670,7 @@ internal async Task RunOnceAsync(string prompt)
try
{
- await _activeAgent.Impl.RefreshChatAsync(this);
+ await _activeAgent.Impl.RefreshChatAsync(this, force: false);
await _activeAgent.Impl.ChatAsync(prompt, this);
}
catch (OperationCanceledException)
diff --git a/shell/agents/AIShell.Azure.Agent/AzCLI/AzCLIAgent.cs b/shell/agents/AIShell.Azure.Agent/AzCLI/AzCLIAgent.cs
index d185f523..10afbd9d 100644
--- a/shell/agents/AIShell.Azure.Agent/AzCLI/AzCLIAgent.cs
+++ b/shell/agents/AIShell.Azure.Agent/AzCLI/AzCLIAgent.cs
@@ -101,7 +101,7 @@ public void OnUserAction(UserActionPayload actionPayload)
});
}
- public Task RefreshChatAsync(IShell shell)
+ public Task RefreshChatAsync(IShell shell, bool force)
{
// Reset the history so the subsequent chat can start fresh.
_chatService.ChatHistory.Clear();
diff --git a/shell/agents/AIShell.Azure.Agent/AzPS/AzPSAgent.cs b/shell/agents/AIShell.Azure.Agent/AzPS/AzPSAgent.cs
index 2b64d45d..6ea778b8 100644
--- a/shell/agents/AIShell.Azure.Agent/AzPS/AzPSAgent.cs
+++ b/shell/agents/AIShell.Azure.Agent/AzPS/AzPSAgent.cs
@@ -105,7 +105,7 @@ public void OnUserAction(UserActionPayload actionPayload)
});
}
- public Task RefreshChatAsync(IShell shell)
+ public Task RefreshChatAsync(IShell shell, bool force)
{
// Reset the history so the subsequent chat can start fresh.
_chatService.ChatHistory.Clear();
diff --git a/shell/agents/AIShell.Interpreter.Agent/Agent.cs b/shell/agents/AIShell.Interpreter.Agent/Agent.cs
index b2bbcbdb..62b0da85 100644
--- a/shell/agents/AIShell.Interpreter.Agent/Agent.cs
+++ b/shell/agents/AIShell.Interpreter.Agent/Agent.cs
@@ -72,7 +72,7 @@ public void Initialize(AgentConfig config)
}
///
- public Task RefreshChatAsync(IShell shell)
+ public Task RefreshChatAsync(IShell shell, bool force)
{
// Reload the setting file if needed.
ReloadSettings();
diff --git a/shell/agents/AIShell.Ollama.Agent/OllamaAgent.cs b/shell/agents/AIShell.Ollama.Agent/OllamaAgent.cs
index 15495273..36aa6140 100644
--- a/shell/agents/AIShell.Ollama.Agent/OllamaAgent.cs
+++ b/shell/agents/AIShell.Ollama.Agent/OllamaAgent.cs
@@ -87,7 +87,7 @@ public void OnUserAction(UserActionPayload actionPayload) {}
/// Refresh the current chat by starting a new chat session.
/// This method allows an agent to reset chat states, interact with user for authentication, print welcome message, and more.
///
- public Task RefreshChatAsync(IShell shell) => Task.CompletedTask;
+ public Task RefreshChatAsync(IShell shell, bool force) => Task.CompletedTask;
///
/// Main chat function that takes the users input and passes it to the LLM and renders it.
diff --git a/shell/agents/AIShell.OpenAI.Agent/Agent.cs b/shell/agents/AIShell.OpenAI.Agent/Agent.cs
index 105ee73e..4da54ccc 100644
--- a/shell/agents/AIShell.OpenAI.Agent/Agent.cs
+++ b/shell/agents/AIShell.OpenAI.Agent/Agent.cs
@@ -77,7 +77,7 @@ public void Initialize(AgentConfig config)
public void OnUserAction(UserActionPayload actionPayload) {}
///
- public Task RefreshChatAsync(IShell shell)
+ public Task RefreshChatAsync(IShell shell, bool force)
{
// Reload the setting file if needed.
ReloadSettings();
diff --git a/shell/agents/Microsoft.Azure.Agent/AzureAgent.cs b/shell/agents/Microsoft.Azure.Agent/AzureAgent.cs
index 132cfc54..034e84df 100644
--- a/shell/agents/Microsoft.Azure.Agent/AzureAgent.cs
+++ b/shell/agents/Microsoft.Azure.Agent/AzureAgent.cs
@@ -2,6 +2,7 @@
using System.Text;
using AIShell.Abstraction;
+using Azure.Identity;
using Serilog;
namespace Microsoft.Azure.Agent;
@@ -115,10 +116,39 @@ public void Initialize(AgentConfig config)
public bool CanAcceptFeedback(UserAction action) => false;
public void OnUserAction(UserActionPayload actionPayload) {}
- public async Task RefreshChatAsync(IShell shell)
+ public async Task RefreshChatAsync(IShell shell, bool force)
{
- // Refresh the chat session.
- await _chatSession.RefreshAsync(shell.Host, shell.CancellationToken);
+ IHost host = shell.Host;
+ CancellationToken cancellationToken = shell.CancellationToken;
+
+ try
+ {
+ string welcome = await host.RunWithSpinnerAsync(
+ status: "Initializing ...",
+ spinnerKind: SpinnerKind.Processing,
+ func: async context => await _chatSession.RefreshAsync(context, force, cancellationToken)
+ ).ConfigureAwait(false);
+
+ if (!string.IsNullOrEmpty(welcome))
+ {
+ host.WriteLine(welcome);
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ host.WriteErrorLine("Operation cancelled. Please run '/refresh' to start a new conversation.");
+ }
+ catch (CredentialUnavailableException)
+ {
+ host.WriteErrorLine($"Failed to start a chat session: Access token not available.");
+ host.WriteErrorLine($"The '{Name}' agent depends on the Azure CLI credential to acquire access token. Please run 'az login' from a command-line shell to setup account.");
+ }
+ catch (Exception e)
+ {
+ host.WriteErrorLine($"Failed to start a chat session: {e.Message}\n{e.StackTrace}")
+ .WriteErrorLine()
+ .WriteErrorLine("Please try '/refresh' to start a new conversation.");
+ }
}
public async Task ChatAsync(string input, IShell shell)
diff --git a/shell/agents/Microsoft.Azure.Agent/ChatSession.cs b/shell/agents/Microsoft.Azure.Agent/ChatSession.cs
index 1fa37442..17a60723 100644
--- a/shell/agents/Microsoft.Azure.Agent/ChatSession.cs
+++ b/shell/agents/Microsoft.Azure.Agent/ChatSession.cs
@@ -5,15 +5,17 @@
using System.Text.Json.Nodes;
using AIShell.Abstraction;
+using Azure.Core;
+using Azure.Identity;
using Serilog;
namespace Microsoft.Azure.Agent;
internal class ChatSession : IDisposable
{
- private const string DL_TOKEN_URL = "https://directline.botframework.com/v3/directline/tokens/generate";
+ private const string DL_TOKEN_URL = "https://copilotweb.production.portalrp.azure.com/api/conversations/start?api-version=2024-11-15";
private const string REFRESH_TOKEN_URL = "https://directline.botframework.com/v3/directline/tokens/refresh";
- internal const string CONVERSATION_URL = "https://directline.botframework.com/v3/directline/conversations";
+ private const string CONVERSATION_URL = "https://directline.botframework.com/v3/directline/conversations";
private string _token;
private string _streamUrl;
@@ -22,7 +24,6 @@ internal class ChatSession : IDisposable
private DateTime _expireOn;
private AzureCopilotReceiver _copilotReceiver;
- private readonly string _dl_secret;
private readonly HttpClient _httpClient;
private readonly Dictionary _flights;
@@ -30,7 +31,6 @@ internal class ChatSession : IDisposable
internal ChatSession(HttpClient httpClient)
{
- _dl_secret = Environment.GetEnvironmentVariable("DL_SECRET");
_httpClient = httpClient;
// Keys and values for flights are from the portal request.
@@ -59,33 +59,46 @@ internal ChatSession(HttpClient httpClient)
["chitchatprompt"] = true,
// TODO: the streaming is slow and not sending chunks, very clumsy for now.
// ["streamresponse"] = true,
+ // ["azurepluginstore"] = true,
};
}
- internal async Task RefreshAsync(IHost host, CancellationToken cancellationToken)
+ internal async Task RefreshAsync(IStatusContext context, bool force, CancellationToken cancellationToken)
{
- try
- {
- await GenerateTokenAsync(host, cancellationToken);
- await StartConversationAsync(host, cancellationToken);
- }
- catch (Exception e)
+ if (_token is not null)
{
- Reset();
- if (e is OperationCanceledException)
+ if (force)
{
- host.WriteErrorLine()
- .WriteErrorLine("Operation cancelled. Please run '/refresh' to start a new conversation.");
+ // End the existing conversation.
+ context.Status("End current chat ...");
+ EndConversation();
+ Reset();
}
else
{
- host.WriteErrorLine()
- .WriteErrorLine($"Failed to start a conversation due to the following error: {e.Message}\n{e.StackTrace}")
- .WriteErrorLine()
- .WriteErrorLine("Please try '/refresh' to start a new conversation.")
- .WriteErrorLine();
+ try
+ {
+ context.Status("Refresh DirectLine token ...");
+ await RenewTokenAsync(cancellationToken);
+ return null;
+ }
+ catch (Exception)
+ {
+ // Refreshing failed. We will create a new chat session.
+ }
}
}
+
+ try
+ {
+ _token = await GenerateTokenAsync(context, cancellationToken);
+ return await StartConversationAsync(context, cancellationToken);
+ }
+ catch (Exception)
+ {
+ Reset();
+ throw;
+ }
}
private void Reset()
@@ -100,43 +113,38 @@ private void Reset()
_copilotReceiver = null;
}
- private async Task GenerateTokenAsync(IHost host, CancellationToken cancellationToken)
+ private async Task GenerateTokenAsync(IStatusContext context, CancellationToken cancellationToken)
{
- // TODO: use spinner when generating token. Also use interaction for authentication is needed.
- string manualToken = Environment.GetEnvironmentVariable("DL_TOKEN");
- if (!string.IsNullOrEmpty(manualToken))
- {
- _token = manualToken;
- return;
- }
-
- if (string.IsNullOrEmpty(_dl_secret))
- {
- throw new TokenRequestException("You have to manually grab the Direct Line token from portal and set it to the environment variable 'DL_TOKEN' until we figure out authentication.");
- }
-
- // TODO: figure out how to get the token when copilot API is ready.
- HttpRequestMessage request = new(HttpMethod.Post, DL_TOKEN_URL);
- request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _dl_secret);
+ context.Status("Get Azure CLI login token ...");
+ // Get an access token from the AzCLI login, using the specific audience guid.
+ AccessToken accessToken = await new AzureCliCredential()
+ .GetTokenAsync(
+ new TokenRequestContext(["7000789f-b583-4714-ab18-aef39213018a/.default"]),
+ cancellationToken);
+
+ context.Status("Request for DirectLine token ...");
+ StringContent content = new("{\"conversationType\": \"Chat\"}", Encoding.UTF8, Utils.JsonContentType);
+ HttpRequestMessage request = new(HttpMethod.Post, DL_TOKEN_URL) { Content = content };
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken.Token);
HttpResponseMessage response = await _httpClient.SendAsync(request, cancellationToken);
response.EnsureSuccessStatusCode();
- Stream content = await response.Content.ReadAsStreamAsync(cancellationToken);
- TokenPayload tpl = JsonSerializer.Deserialize(content, Utils.JsonOptions);
-
- _token = tpl.Token;
+ using Stream stream = await response.Content.ReadAsStreamAsync(cancellationToken);
+ var dlToken = JsonSerializer.Deserialize(stream, Utils.JsonOptions);
+ return dlToken.DirectLine.Token;
}
- private async Task StartConversationAsync(IHost host, CancellationToken cancellationToken)
+ private async Task StartConversationAsync(IStatusContext context, CancellationToken cancellationToken)
{
+ context.Status("Start a new chat session ...");
HttpRequestMessage request = new(HttpMethod.Post, CONVERSATION_URL);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _token);
HttpResponseMessage response = await _httpClient.SendAsync(request, cancellationToken);
response.EnsureSuccessStatusCode();
- Stream content = await response.Content.ReadAsStreamAsync(cancellationToken);
+ using Stream content = await response.Content.ReadAsStreamAsync(cancellationToken);
SessionPayload spl = JsonSerializer.Deserialize(content, Utils.JsonOptions);
_token = spl.Token;
@@ -156,22 +164,39 @@ private async Task StartConversationAsync(IHost host, CancellationToken cancella
activity.ExtractMetadata(out _, out ConversationState conversationState);
int chatNumber = conversationState.DailyConversationNumber;
int requestNumber = conversationState.TurnNumber;
-
- host.WriteLine($"\n{activity.Text}\nThis is chat #{chatNumber}, request #{requestNumber}.\n");
- return;
+ return $"{activity.Text}\nThis is chat #{chatNumber}, request #{requestNumber}.\n";
}
}
}
- private void RenewToken(CancellationToken cancellationToken)
+ private TokenHealth CheckDLTokenHealth()
{
+ ArgumentNullException.ThrowIfNull(_token, nameof(_token));
+
var now = DateTime.UtcNow;
if (now > _expireOn || now.AddMinutes(2) >= _expireOn)
+ {
+ return TokenHealth.Expired;
+ }
+
+ if (now.AddMinutes(10) < _expireOn)
+ {
+ return TokenHealth.Good;
+ }
+
+ return TokenHealth.TimeToRefresh;
+ }
+
+ private async Task RenewTokenAsync(CancellationToken cancellationToken)
+ {
+ TokenHealth health = CheckDLTokenHealth();
+ if (health is TokenHealth.Expired)
{
Reset();
throw new TokenRequestException("The chat session has expired. Please start a new chat session.");
}
- else if (now.AddMinutes(10) < _expireOn)
+
+ if (health is TokenHealth.Good)
{
return;
}
@@ -181,14 +206,14 @@ private void RenewToken(CancellationToken cancellationToken)
HttpRequestMessage request = new(HttpMethod.Post, REFRESH_TOKEN_URL);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _token);
- var response = _httpClient.Send(request, cancellationToken);
+ var response = await _httpClient.SendAsync(request, cancellationToken);
response.EnsureSuccessStatusCode();
- Stream content = response.Content.ReadAsStream(cancellationToken);
- TokenPayload tpl = JsonSerializer.Deserialize(content, Utils.JsonOptions);
+ using Stream content = await response.Content.ReadAsStreamAsync(cancellationToken);
+ RefreshDLToken dlToken = JsonSerializer.Deserialize(content, Utils.JsonOptions);
- _token = tpl.Token;
- _expireOn = DateTime.UtcNow.AddSeconds(tpl.ExpiresIn);
+ _token = dlToken.Token;
+ _expireOn = DateTime.UtcNow.AddSeconds(dlToken.ExpiresIn);
}
catch (Exception e) when (e is not OperationCanceledException)
{
@@ -250,19 +275,34 @@ private async Task SendQueryToCopilot(string input, CancellationToken ca
response.EnsureSuccessStatusCode();
// Retrieve the activity id of this query.
- Stream content = await response.Content.ReadAsStreamAsync(cancellationToken);
+ using Stream content = await response.Content.ReadAsStreamAsync(cancellationToken);
JsonObject contentObj = JsonNode.Parse(content).AsObject();
return contentObj["id"].ToString();
}
+ private void EndConversation()
+ {
+ if (_token is null || CheckDLTokenHealth() is TokenHealth.Expired)
+ {
+ // Chat session already expired, no need to send request to end the conversation.
+ return;
+ }
+
+ var content = new StringContent("{\"type\":\"endOfConversation\",\"from\":{\"id\":\"user\"}}", Encoding.UTF8, Utils.JsonContentType);
+ var request = new HttpRequestMessage(HttpMethod.Post, _conversationUrl) { Content = content };
+
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _token);
+ _httpClient.Send(request, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None);
+ }
+
internal async Task GetChatResponseAsync(string input, IStatusContext context, CancellationToken cancellationToken)
{
try
{
- // context?.Status("Refreshing Token ...");
- // RenewToken(cancellationToken);
+ context?.Status("Refreshing Token ...");
+ await RenewTokenAsync(cancellationToken);
- context?.Status("Generating ...");
+ context?.Status("Sending query ...");
string activityId = await SendQueryToCopilot(input, cancellationToken);
while (true)
@@ -313,6 +353,7 @@ internal async Task GetChatResponseAsync(string input, IStatusC
public void Dispose()
{
+ EndConversation();
_copilotReceiver?.Dispose();
}
}
diff --git a/shell/agents/Microsoft.Azure.Agent/Command.cs b/shell/agents/Microsoft.Azure.Agent/Command.cs
index 6236e2e1..f79800f2 100644
--- a/shell/agents/Microsoft.Azure.Agent/Command.cs
+++ b/shell/agents/Microsoft.Azure.Agent/Command.cs
@@ -110,7 +110,7 @@ private void ReplaceAction()
{
// Add quotes for the value if needed.
value = value.Trim();
- if (value.StartsWith('-') || value.Contains(' '))
+ if (value.StartsWith('-') || value.Contains(' ') || value.Contains('|'))
{
value = $"\"{value}\"";
}
diff --git a/shell/agents/Microsoft.Azure.Agent/Schema.cs b/shell/agents/Microsoft.Azure.Agent/Schema.cs
index 6e60ec67..1424a927 100644
--- a/shell/agents/Microsoft.Azure.Agent/Schema.cs
+++ b/shell/agents/Microsoft.Azure.Agent/Schema.cs
@@ -51,7 +51,14 @@ internal static void NewSettingFile(string path)
}
}
-internal class TokenPayload
+internal enum TokenHealth
+{
+ Good,
+ TimeToRefresh,
+ Expired
+}
+
+internal class RefreshDLToken
{
public string ConversationId { get; set; }
public string Token { get; set; }
@@ -60,6 +67,21 @@ internal class TokenPayload
public int ExpiresIn { get; set; }
}
+internal class NewDLToken
+{
+ public string Endpoint { get; set; }
+ public string Token { get; set; }
+ public int TokenExpiryTimeInSeconds { get; set; }
+}
+
+internal class DirectLineToken
+{
+ public string Id { get; set; }
+ public DateTime CreatedAt { get; set; }
+ public DateTime LastModifiedAt { get; set; }
+ public NewDLToken DirectLine { get; set; }
+}
+
internal class SessionPayload
{
public string ConversationId { get; set; }
diff --git a/shell/shell.common.props b/shell/shell.common.props
index 0e1b3402..7e98aa6a 100644
--- a/shell/shell.common.props
+++ b/shell/shell.common.props
@@ -8,7 +8,7 @@
net8.0
enable
12.0
- 0.1.0-alpha.16
+ 0.1.0-alpha.19
true
true