Skip to content

Commit 59864ed

Browse files
committed
Add the check for Copilot access
1 parent da3e6d5 commit 59864ed

File tree

4 files changed

+285
-118
lines changed

4 files changed

+285
-118
lines changed

shell/agents/Microsoft.Azure.Agent/AzureAgent.cs

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -165,10 +165,26 @@ public async Task RefreshChatAsync(IShell shell, bool force)
165165
{
166166
host.WriteErrorLine("Operation cancelled. Please run '/refresh' to start a new conversation.");
167167
}
168-
catch (CredentialUnavailableException)
168+
catch (TokenRequestException e)
169169
{
170-
host.WriteErrorLine($"Failed to start a chat session: Access token not available.");
171-
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.");
170+
if (e.UserUnauthorized)
171+
{
172+
host.WriteLine("Sorry, you are not authorized to access Azure Copilot services.");
173+
host.WriteLine($"Details: {e.Message}");
174+
return;
175+
}
176+
177+
Exception inner = e.InnerException;
178+
if (inner is CredentialUnavailableException)
179+
{
180+
host.WriteErrorLine($"Failed to start a chat session: Access token not available.");
181+
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.");
182+
host.WriteErrorLine("Once you've successfully logged in, please run '/refresh' to start a new conversation");
183+
return;
184+
}
185+
186+
host.WriteErrorLine(e.Message);
187+
host.WriteErrorLine("Please try '/refresh' to start a new conversation.");
172188
}
173189
catch (Exception e)
174190
{
@@ -189,6 +205,12 @@ public async Task<bool> ChatAsync(string input, IShell shell)
189205
return true;
190206
}
191207

208+
if (!_chatSession.UserAuthorized)
209+
{
210+
host.WriteLine("\nSorry, you are not authorized to access Azure Copilot services.\n");
211+
return true;
212+
}
213+
192214
try
193215
{
194216
string query = $"{input}\n\n---\n\n{_instructions}";

shell/agents/Microsoft.Azure.Agent/ChatSession.cs

Lines changed: 96 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -5,31 +5,32 @@
55
using System.Text.Json.Nodes;
66

77
using AIShell.Abstraction;
8-
using Azure.Core;
9-
using Azure.Identity;
108
using Serilog;
119

1210
namespace Microsoft.Azure.Agent;
1311

1412
internal class ChatSession : IDisposable
1513
{
14+
private const string ACCESS_URL = "https://copilotweb.production.portalrp.azure.com/api/access?api-version=2024-09-01";
1615
private const string DL_TOKEN_URL = "https://copilotweb.production.portalrp.azure.com/api/conversations/start?api-version=2024-11-15";
17-
private const string REFRESH_TOKEN_URL = "https://directline.botframework.com/v3/directline/tokens/refresh";
1816
private const string CONVERSATION_URL = "https://directline.botframework.com/v3/directline/conversations";
1917

20-
private string _token;
18+
internal bool UserAuthorized { get; private set; }
19+
2120
private string _streamUrl;
2221
private string _conversationId;
2322
private string _conversationUrl;
24-
private DateTime _expireOn;
23+
private UserDirectLineToken _directLineToken;
2524
private AzureCopilotReceiver _copilotReceiver;
2625

2726
private readonly HttpClient _httpClient;
27+
private readonly UserAccessToken _accessToken;
2828
private readonly Dictionary<string, object> _flights;
2929

3030
internal ChatSession(HttpClient httpClient)
3131
{
3232
_httpClient = httpClient;
33+
_accessToken = new UserAccessToken();
3334

3435
// Keys and values for flights are from the portal request.
3536
_flights = new Dictionary<string, object>()
@@ -63,179 +64,146 @@ internal ChatSession(HttpClient httpClient)
6364

6465
internal async Task<string> RefreshAsync(IStatusContext context, bool force, CancellationToken cancellationToken)
6566
{
66-
if (_token is not null)
67+
if (_directLineToken is not null)
6768
{
6869
if (force)
6970
{
7071
// End the existing conversation.
7172
context.Status("Ending current chat ...");
72-
EndConversation();
73-
Reset();
73+
EndCurrentConversation();
7474
}
7575
else
7676
{
7777
try
7878
{
79-
context.Status("Refreshing token ...");
80-
await RenewTokenAsync(cancellationToken);
79+
context.Status("Refreshing access token ...");
80+
await _accessToken.CreateOrRenewTokenAsync(cancellationToken);
81+
82+
context.Status("Refreshing DirectLine token ...");
83+
await _directLineToken.RenewTokenAsync(_httpClient, cancellationToken);
84+
85+
// Tokens successfully refreshed.
8186
return null;
8287
}
88+
catch (OperationCanceledException)
89+
{
90+
throw;
91+
}
8392
catch (Exception)
8493
{
8594
// Refreshing failed. We will create a new chat session.
8695
}
8796
}
8897
}
8998

90-
_token = await GenerateTokenAsync(context, cancellationToken);
91-
return await OpenConversationAsync(context, cancellationToken);
99+
return await SetupNewChat(context, cancellationToken);
92100
}
93101

94102
private void Reset()
95103
{
96-
_token = null;
97104
_streamUrl = null;
98105
_conversationId = null;
99106
_conversationUrl = null;
100-
_expireOn = DateTime.MinValue;
107+
_directLineToken = null;
101108

109+
_accessToken.Reset();
102110
_copilotReceiver?.Dispose();
103111
_copilotReceiver = null;
104112
}
105113

106-
private async Task<string> GenerateTokenAsync(IStatusContext context, CancellationToken cancellationToken)
114+
private async Task<string> SetupNewChat(IStatusContext context, CancellationToken cancellationToken)
107115
{
108116
try
109117
{
110118
context.Status("Get Azure CLI login token ...");
111119
// Get an access token from the AzCLI login, using the specific audience guid.
112-
AccessToken accessToken = await new AzureCliCredential()
113-
.GetTokenAsync(
114-
new TokenRequestContext(["7000789f-b583-4714-ab18-aef39213018a/.default"]),
115-
cancellationToken);
120+
await _accessToken.CreateOrRenewTokenAsync(cancellationToken);
116121

117-
context.Status("Request for DirectLine token ...");
118-
StringContent content = new("{\"conversationType\": \"Chat\"}", Encoding.UTF8, Utils.JsonContentType);
119-
HttpRequestMessage request = new(HttpMethod.Post, DL_TOKEN_URL) { Content = content };
120-
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken.Token);
122+
context.Status("Check Copilot authorization ...");
123+
await CheckAuthorizationAsync(cancellationToken);
121124

122-
HttpResponseMessage response = await _httpClient.SendAsync(request, cancellationToken);
123-
response.EnsureSuccessStatusCode();
125+
context.Status("Request for DirectLine token ...");
126+
await GetInitialDLTokenAsync(cancellationToken);
124127

125-
using Stream stream = await response.Content.ReadAsStreamAsync(cancellationToken);
126-
var dlToken = JsonSerializer.Deserialize<DirectLineToken>(stream, Utils.JsonOptions);
127-
return dlToken.DirectLine.Token;
128+
context.Status("Start a new chat session ...");
129+
return await OpenConversationAsync(cancellationToken);
128130
}
129131
catch (Exception e)
130132
{
131-
if (e is not OperationCanceledException)
133+
if (e is not OperationCanceledException and TokenRequestException)
132134
{
133-
Telemetry.Trace(AzTrace.Exception("Failed to generate the initial DL token."), e);
135+
Telemetry.Trace(AzTrace.Exception("Failed to setup a new chat session."), e);
134136
}
135137

136138
Reset();
137139
throw;
138140
}
139141
}
140142

141-
private async Task<string> OpenConversationAsync(IStatusContext context, CancellationToken cancellationToken)
143+
private async Task CheckAuthorizationAsync(CancellationToken cancellationToken)
142144
{
143-
try
144-
{
145-
context.Status("Start a new chat session ...");
146-
HttpRequestMessage request = new(HttpMethod.Post, CONVERSATION_URL);
147-
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _token);
145+
HttpRequestMessage request = new(HttpMethod.Get, ACCESS_URL);
146+
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
147+
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _accessToken.Token);
148148

149-
HttpResponseMessage response = await _httpClient.SendAsync(request, cancellationToken);
150-
response.EnsureSuccessStatusCode();
151-
152-
using Stream content = await response.Content.ReadAsStreamAsync(cancellationToken);
153-
SessionPayload spl = JsonSerializer.Deserialize<SessionPayload>(content, Utils.JsonOptions);
154-
155-
_token = spl.Token;
156-
_conversationId = spl.ConversationId;
157-
_conversationUrl = $"{CONVERSATION_URL}/{_conversationId}/activities";
158-
_streamUrl = spl.StreamUrl;
159-
_expireOn = DateTime.UtcNow.AddSeconds(spl.ExpiresIn);
160-
_copilotReceiver = await AzureCopilotReceiver.CreateAsync(_streamUrl);
149+
HttpResponseMessage response = await _httpClient.SendAsync(request, cancellationToken);
150+
await response.EnsureSuccessStatusCodeForTokenRequest("Failed to check Copilot authorization.");
161151

162-
Log.Debug("[ChatSession] Conversation started. Id: {0}", _conversationId);
152+
using Stream stream = await response.Content.ReadAsStreamAsync(cancellationToken);
153+
var permission = JsonSerializer.Deserialize<CopilotPermission>(stream, Utils.JsonOptions);
154+
UserAuthorized = permission.Authorized;
163155

164-
while (true)
165-
{
166-
CopilotActivity activity = _copilotReceiver.Take(cancellationToken);
167-
if (activity.IsMessage && activity.IsFromCopilot && _copilotReceiver.Watermark is 0)
168-
{
169-
activity.ExtractMetadata(out _, out ConversationState conversationState);
170-
int chatNumber = conversationState.DailyConversationNumber;
171-
int requestNumber = conversationState.TurnNumber;
172-
return $"{activity.Text}\nThis is chat #{chatNumber}, request #{requestNumber}.\n";
173-
}
174-
}
175-
}
176-
catch (Exception e)
156+
if (!UserAuthorized)
177157
{
178-
if (e is not OperationCanceledException)
179-
{
180-
Telemetry.Trace(AzTrace.Exception("Failed to open conversation with the initial DL token."), e);
181-
}
182-
183-
Reset();
184-
throw;
158+
string message = $"Access token not authorized to access Azure Copilot. {permission.Message}";
159+
Telemetry.Trace(AzTrace.Exception(message));
160+
throw new TokenRequestException(message) { UserUnauthorized = true };
185161
}
186162
}
187163

188-
private TokenHealth CheckDLTokenHealth()
164+
private async Task GetInitialDLTokenAsync(CancellationToken cancellationToken)
189165
{
190-
ArgumentNullException.ThrowIfNull(_token, nameof(_token));
166+
StringContent content = new("{\"conversationType\": \"Chat\"}", Encoding.UTF8, Utils.JsonContentType);
167+
HttpRequestMessage request = new(HttpMethod.Post, DL_TOKEN_URL) { Content = content };
168+
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _accessToken.Token);
191169

192-
var now = DateTime.UtcNow;
193-
if (now > _expireOn || now.AddMinutes(2) >= _expireOn)
194-
{
195-
return TokenHealth.Expired;
196-
}
197-
198-
if (now.AddMinutes(10) < _expireOn)
199-
{
200-
return TokenHealth.Good;
201-
}
170+
HttpResponseMessage response = await _httpClient.SendAsync(request, cancellationToken);
171+
await response.EnsureSuccessStatusCodeForTokenRequest("Failed to generate the initial DL token.");
202172

203-
return TokenHealth.TimeToRefresh;
173+
using Stream stream = await response.Content.ReadAsStreamAsync(cancellationToken);
174+
var dlToken = JsonSerializer.Deserialize<DirectLineToken>(stream, Utils.JsonOptions);
175+
_directLineToken = new UserDirectLineToken(dlToken.DirectLine.Token, dlToken.DirectLine.TokenExpiryTimeInSeconds);
204176
}
205177

206-
private async Task RenewTokenAsync(CancellationToken cancellationToken)
178+
private async Task<string> OpenConversationAsync(CancellationToken cancellationToken)
207179
{
208-
TokenHealth health = CheckDLTokenHealth();
209-
if (health is TokenHealth.Expired)
210-
{
211-
Reset();
212-
throw new TokenRequestException("The chat session has expired. Please start a new chat session.");
213-
}
180+
HttpRequestMessage request = new(HttpMethod.Post, CONVERSATION_URL);
181+
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _directLineToken.Token);
214182

215-
if (health is TokenHealth.Good)
216-
{
217-
return;
218-
}
183+
HttpResponseMessage response = await _httpClient.SendAsync(request, cancellationToken);
184+
await response.EnsureSuccessStatusCodeForTokenRequest("Failed to open an conversation.");
219185

220-
try
221-
{
222-
HttpRequestMessage request = new(HttpMethod.Post, REFRESH_TOKEN_URL);
223-
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _token);
186+
using Stream content = await response.Content.ReadAsStreamAsync(cancellationToken);
187+
SessionPayload spl = JsonSerializer.Deserialize<SessionPayload>(content, Utils.JsonOptions);
224188

225-
var response = await _httpClient.SendAsync(request, cancellationToken);
226-
response.EnsureSuccessStatusCode();
189+
_conversationId = spl.ConversationId;
190+
_conversationUrl = $"{CONVERSATION_URL}/{_conversationId}/activities";
191+
_directLineToken = new UserDirectLineToken(spl.Token, spl.ExpiresIn);
192+
_streamUrl = spl.StreamUrl;
193+
_copilotReceiver = await AzureCopilotReceiver.CreateAsync(_streamUrl);
227194

228-
using Stream content = await response.Content.ReadAsStreamAsync(cancellationToken);
229-
RefreshDLToken dlToken = JsonSerializer.Deserialize<RefreshDLToken>(content, Utils.JsonOptions);
195+
Log.Debug("[ChatSession] Conversation started. Id: {0}", _conversationId);
230196

231-
_token = dlToken.Token;
232-
_expireOn = DateTime.UtcNow.AddSeconds(dlToken.ExpiresIn);
233-
}
234-
catch (Exception e) when (e is not OperationCanceledException)
197+
while (true)
235198
{
236-
Reset();
237-
Telemetry.Trace(AzTrace.Exception("Failed to refresh the DL token."), e);
238-
throw new TokenRequestException($"Failed to refresh the 'DirectLine' token: {e.Message}.", e);
199+
CopilotActivity activity = _copilotReceiver.Take(cancellationToken);
200+
if (activity.IsMessage && activity.IsFromCopilot && _copilotReceiver.Watermark is 0)
201+
{
202+
activity.ExtractMetadata(out _, out ConversationState conversationState);
203+
int chatNumber = conversationState.DailyConversationNumber;
204+
int requestNumber = conversationState.TurnNumber;
205+
return $"{activity.Text}\nThis is chat #{chatNumber}, request #{requestNumber}.\n";
206+
}
239207
}
240208
}
241209

@@ -280,7 +248,7 @@ private HttpRequestMessage PrepareForChat(string input)
280248
var content = new StringContent(json, Encoding.UTF8, Utils.JsonContentType);
281249
var request = new HttpRequestMessage(HttpMethod.Post, _conversationUrl) { Content = content };
282250

283-
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _token);
251+
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _directLineToken.Token);
284252
// This header is for server side telemetry to identify where the request comes from.
285253
request.Headers.Add("ClientType", "AIShell");
286254
return request;
@@ -299,9 +267,9 @@ private async Task<string> SendQueryToCopilot(string input, CancellationToken ca
299267
return contentObj["id"].ToString();
300268
}
301269

302-
private void EndConversation()
270+
private void EndCurrentConversation()
303271
{
304-
if (_token is null || CheckDLTokenHealth() is TokenHealth.Expired)
272+
if (_directLineToken is null || _directLineToken.CheckTokenHealth() is TokenHealth.Expired)
305273
{
306274
// Chat session already expired, no need to send request to end the conversation.
307275
return;
@@ -310,16 +278,24 @@ private void EndConversation()
310278
var content = new StringContent("{\"type\":\"endOfConversation\",\"from\":{\"id\":\"user\"}}", Encoding.UTF8, Utils.JsonContentType);
311279
var request = new HttpRequestMessage(HttpMethod.Post, _conversationUrl) { Content = content };
312280

313-
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _token);
281+
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _directLineToken.Token);
314282
_httpClient.Send(request, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None);
315283
}
316284

317285
internal async Task<CopilotResponse> GetChatResponseAsync(string input, IStatusContext context, CancellationToken cancellationToken)
318286
{
287+
if (_directLineToken is null)
288+
{
289+
throw new TokenRequestException("A chat session hasn't been setup yet.");
290+
}
291+
319292
try
320293
{
321-
context?.Status("Refreshing Token ...");
322-
await RenewTokenAsync(cancellationToken);
294+
context.Status("Refreshing access token ...");
295+
await _accessToken.CreateOrRenewTokenAsync(cancellationToken);
296+
297+
context.Status("Refreshing DirectLine token ...");
298+
await _directLineToken.RenewTokenAsync(_httpClient, cancellationToken);
323299

324300
context?.Status("Sending query ...");
325301
string activityId = await SendQueryToCopilot(input, cancellationToken);
@@ -370,11 +346,16 @@ internal async Task<CopilotResponse> GetChatResponseAsync(string input, IStatusC
370346
// TODO: we may need to notify azure copilot somehow about the cancellation.
371347
return null;
372348
}
349+
catch (TokenRequestException)
350+
{
351+
Reset();
352+
throw;
353+
}
373354
}
374355

375356
public void Dispose()
376357
{
377-
EndConversation();
358+
EndCurrentConversation();
378359
_copilotReceiver?.Dispose();
379360
}
380361
}

0 commit comments

Comments
 (0)