forked from PowerShell/AIShell
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathAgent.cs
More file actions
338 lines (293 loc) · 12.8 KB
/
Agent.cs
File metadata and controls
338 lines (293 loc) · 12.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
using System.ClientModel;
using System.Text;
using System.Text.Json;
using AIShell.Abstraction;
using OpenAI.Chat;
namespace AIShell.OpenAI.Agent;
public sealed class OpenAIAgent : ILLMAgent
{
public string Name => "openai-gpt";
public string SettingFile { private set; get; }
public string Description
{
get
{
// Changes in setting could affect the agent description.
ReloadSettings();
return _description;
}
}
private const string SettingFileName = "openai.agent.json";
private bool _reloadSettings;
private bool _isDisposed;
private string _configRoot;
private string _historyRoot;
private string _description;
private Settings _settings;
private FileSystemWatcher _watcher;
private ChatService _chatService;
/// <summary>
/// Gets the settings.
/// </summary>
internal Settings Settings => _settings;
/// <inheritdoc/>
public void Dispose()
{
if (_isDisposed)
{
return;
}
GC.SuppressFinalize(this);
_watcher.Dispose();
_isDisposed = true;
}
/// <inheritdoc/>
public void Initialize(AgentConfig config)
{
_configRoot = config.ConfigurationRoot;
_historyRoot = Path.Combine(_configRoot, "history");
if (!Directory.Exists(_historyRoot))
{
Directory.CreateDirectory(_historyRoot);
}
SettingFile = Path.Combine(_configRoot, SettingFileName);
_settings = ReadSettings();
_chatService = new ChatService(_historyRoot, _settings);
if (_settings is null)
{
// Create the setting file with examples to serve as a template for user to update.
NewExampleSettingFile();
}
UpdateDescription();
_watcher = new FileSystemWatcher(_configRoot, SettingFileName)
{
NotifyFilter = NotifyFilters.LastWrite,
EnableRaisingEvents = true,
};
_watcher.Changed += OnSettingFileChange;
}
/// <inheritdoc/>
public IEnumerable<CommandBase> GetCommands() => [new GPTCommand(this)];
/// <inheritdoc/>
public bool CanAcceptFeedback(UserAction action) => false;
/// <inheritdoc/>
public void OnUserAction(UserActionPayload actionPayload) { }
/// <inheritdoc/>
public Task RefreshChatAsync(IShell shell, bool force)
{
if (force)
{
// Reload the setting file if needed.
ReloadSettings();
// Reset the history so the subsequent chat can start fresh.
_chatService.ChatHistory.Clear();
}
return Task.CompletedTask;
}
/// <inheritdoc/>
public async Task<bool> ChatAsync(string input, IShell shell)
{
IHost host = shell.Host;
CancellationToken token = shell.CancellationToken;
// Reload the setting file if needed.
ReloadSettings();
bool checkPass = await SelfCheck(host, token);
if (!checkPass)
{
host.MarkupWarningLine($"[[{Name}]]: Cannot serve the query due to the missing configuration. Please properly update the setting file.");
return checkPass;
}
IAsyncEnumerator<StreamingChatCompletionUpdate> response = await host
.RunWithSpinnerAsync(
() => _chatService.GetStreamingChatResponseAsync(input, token)
).ConfigureAwait(false);
if (response is not null)
{
StreamingChatCompletionUpdate update = null;
using var streamingRender = host.NewStreamRender(token);
try
{
do
{
update = response.Current;
if (update.ContentUpdate.Count > 0)
{
streamingRender.Refresh(update.ContentUpdate[0].Text);
}
}
while (await response.MoveNextAsync().ConfigureAwait(continueOnCapturedContext: false));
}
catch (OperationCanceledException)
{
update = null;
}
if (update is null)
{
_chatService.CalibrateChatHistory(usage: null, response: null);
}
else
{
string responseContent = streamingRender.AccumulatedContent;
_chatService.CalibrateChatHistory(update.Usage, new AssistantChatMessage(responseContent));
}
}
return checkPass;
}
internal void UpdateDescription()
{
const string DefaultDescription = """
This agent is designed to provide a flexible platform for interacting with OpenAI services (Azure OpenAI or the public OpenAI) through one or more customly defined GPT instances.
{0}:
1. Run '/agent config' to open the setting file.
2. {1}. See details at
https://aka.ms/aish/openai
3. Run '/refresh' to apply the new settings.
If you would like to learn more about deploying your own Azure OpenAI Service please see https://aka.ms/AIShell/DeployAOAI.
""";
if (_settings is null || _settings.GPTs.Count is 0)
{
string error = "The agent is currently not ready to serve queries, because there is no GPT defined. Please follow the steps below to configure the setting file properly before using this agent";
string action = "Define the GPT(s)";
_description = string.Format(DefaultDescription, error, action);
return;
}
if (_settings.Active is null)
{
string error = "Multiple GPTs are defined but the active GPT is not specified. You will be prompted to choose from the available GPTs when sending the first query. Or, if you want to set the active GPT in configuration, please follow the steps below";
string action = "Set the 'Active' key";
_description = string.Format(DefaultDescription, error, action);
return;
}
GPT active = _settings.Active;
_description = $"Active GPT: {active.Name}. {active.Description}";
}
internal void ReloadSettings()
{
if (_reloadSettings)
{
_reloadSettings = false;
var settings = ReadSettings();
if (settings is null)
{
return;
}
_settings = settings;
_chatService.RefreshSettings(_settings);
UpdateDescription();
}
}
private async Task<bool> SelfCheck(IHost host, CancellationToken token)
{
if (_settings is null)
{
return false;
}
bool checkPass = await _settings.SelfCheck(host, token);
if (_settings.Dirty)
{
try
{
_watcher.EnableRaisingEvents = false;
SaveSettings(_settings);
_settings.MarkClean();
}
finally
{
_watcher.EnableRaisingEvents = true;
}
}
return checkPass;
}
private Settings ReadSettings()
{
Settings settings = null;
FileInfo file = new(SettingFile);
if (file.Exists)
{
try
{
using var stream = file.OpenRead();
var data = JsonSerializer.Deserialize(stream, SourceGenerationContext.Default.ConfigData);
settings = new Settings(data);
}
catch (Exception e)
{
throw new InvalidDataException($"Parsing settings from '{SettingFile}' failed with the following error: {e.Message}", e);
}
}
return settings;
}
private void SaveSettings(Settings config)
{
using var stream = new FileStream(SettingFile, FileMode.Create, FileAccess.Write, FileShare.None);
JsonSerializer.Serialize(stream, config.ToConfigData(), SourceGenerationContext.Default.ConfigData);
}
private void OnSettingFileChange(object sender, FileSystemEventArgs e)
{
if (e.ChangeType is WatcherChangeTypes.Changed)
{
_reloadSettings = true;
}
}
private void NewExampleSettingFile()
{
string SampleContent = $$"""
{
// Declare GPT instances.
"GPTs": [
/* --- uncomment the examples below and update as appropriate ---
//
// To use the Azure OpenAI service:
// - Set `Endpoint` to the endpoint of your Azure OpenAI service,
// or the endpoint to the Azure API Management service if you are using it as a gateway.
// - Set `Deployment` to the deployment name of your Azure OpenAI service.
// - Set `ModelName` to the name of the model used for your deployment, e.g. "gpt-4-0613".
// - Set `Key` to the access key of your Azure OpenAI service,
// or the key of the Azure API Management service if you are using it as a gateway.
// For example:
{
"Name": "ps-az-gpt4",
"Description": "A GPT instance with expertise in PowerShell scripting and command line utilities. Use gpt-4 running in Azure.",
"Endpoint": "<insert your Azure OpenAI endpoint>",
"Deployment": "<insert your deployment name>",
"ModelName": "<insert the model name>", // required field to infer properties of the service, such as token limit.
"Key": "<insert your key>",
"SystemPrompt": "1. You are a helpful and friendly assistant with expertise in PowerShell scripting and command line.\n2. Assume user is using the operating system `{{Utils.OS}}` unless otherwise specified.\n3. Use the `code block` syntax in markdown to encapsulate any part in responses that is code, YAML, JSON or XML, but not table.\n4. When encapsulating command line code, use '```powershell' if it's PowerShell command; use '```sh' if it's non-PowerShell CLI command.\n5. When generating CLI commands, never ever break a command into multiple lines. Instead, always list all parameters and arguments of the command on the same line.\n6. Please keep the response concise but to the point. Do not overexplain."
},
// To use the public OpenAI service:
// - Ignore the `Endpoint` and `Deployment` keys.
// - Set `ModelName` to the name of the model to be used.
// - Set `Key` to be the OpenAI access token.
// For example:
{
"Name": "ps-gpt4o",
"Description": "A GPT instance with expertise in PowerShell scripting and command line utilities. Use gpt-4o running in OpenAI.",
"ModelName": "gpt-4o",
"Key": "<insert your key>",
"SystemPrompt": "1. You are a helpful and friendly assistant with expertise in PowerShell scripting and command line.\n2. Assume user is using the operating system `Windows 11` unless otherwise specified.\n3. Use the `code block` syntax in markdown to encapsulate any part in responses that is code, YAML, JSON or XML, but not table.\n4. When encapsulating command line code, use '```powershell' if it's PowerShell command; use '```sh' if it's non-PowerShell CLI command.\n5. When generating CLI commands, never ever break a command into multiple lines. Instead, always list all parameters and arguments of the command on the same line.\n6. Please keep the response concise but to the point. Do not overexplain."
},
// To use Azure OpenAI service with Entra ID authentication:
// - Set `Endpoint` to the endpoint of your Azure OpenAI service.
// - Set `Deployment` to the deployment name of your Azure OpenAI service.
// - Set `ModelName` to the name of the model used for your deployment, e.g. "gpt-4o".
// - Set `AuthType` to "EntraID" to use Azure AD credentials.
// For example:
{
"Name": "ps-az-entraId",
"Description": "A GPT instance with expertise in PowerShell scripting using Entra ID authentication.",
"Endpoint": "<insert your Azure OpenAI endpoint>",
"Deployment": "<insert your deployment name>",
"ModelName": "gpt-4o",
"AuthType": "EntraID",
"SystemPrompt": "1. You are a helpful and friendly assistant with expertise in PowerShell scripting and command line."
}
*/
],
// Specify the default GPT instance to use for user query.
// For example: "ps-az-gpt4"
"Active": null
}
""";
File.WriteAllText(SettingFile, SampleContent, Encoding.UTF8);
}
}