forked from microsoft/typechat.net
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathAgent.cs
175 lines (156 loc) · 6.95 KB
/
Agent.cs
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
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.TypeChat.LanguageModels;
namespace Microsoft.TypeChat.Dialog;
/// <summary>
/// An Agent that uses a JsonTranslator to create strongly typed responses to user requests.
/// The agent also optionally retrieves context for each interaction from the supplied context provider.
/// </summary>
public class Agent<T> : IAgent
{
JsonTranslator<T> _translator;
IContextProvider? _contextProvider;
Prompt _instructions;
int _maxPromptLength;
/// <summary>
/// Create a new Agent that uses the given language model
/// The agent .
/// </summary>
/// <param name="model">language model the agent uses</param>
/// <param name="contextProvider">an optional context provider</param>
public Agent(ILanguageModel model, IContextProvider? contextProvider = null)
: this(new JsonTranslator<T>(model), contextProvider)
{
}
/// <summary>
/// Create a new Agent that uses the given language model
/// By default, the agent will use an in-memory transient message history.
/// </summary>
/// <param name="translator">Translator to use</param>
/// <param name="contextProvider">Customize an object to use to capture this agent's interaction history</param>
/// <exception cref="ArgumentNullException">If translator is null</exception>
public Agent(JsonTranslator<T> translator, IContextProvider? contextProvider = null)
{
ArgumentVerify.ThrowIfNull(translator, nameof(translator));
_translator = translator;
_instructions = new Prompt();
// By default, only use 1/2 the estimated # of characters the model supports.. for prompts
// the Agent sends
_maxPromptLength = translator.Model.ModelInfo.MaxCharCount / 2;
_contextProvider = contextProvider;
}
/// <summary>
/// Instructions to the model: these let you customize how the Agent will behave
/// </summary>
public Prompt Instructions => _instructions;
/// <summary>
/// The translator being used by the Agent to go to strongly typed messages
/// </summary>
public JsonTranslator<T> Translator => _translator;
/// <summary>
/// The maximum number of characters to put in a request prompt. The prompt contains
/// - the user's request
/// - any automatically collected context, from history
/// </summary>
public int MaxRequestPromptLength
{
get => _maxPromptLength;
set => _maxPromptLength = value;
}
/// <summary>
/// Get response for the given request
/// </summary>
/// <param name="requestMessage">request message</param>
/// <param name="cancelToken">optional cancellation token</param>
/// <returns>response message</returns>
public async Task<Message> GetResponseMessageAsync(Message requestMessage, CancellationToken cancelToken = default)
{
ArgumentVerify.ThrowIfNull(requestMessage, nameof(requestMessage));
string requestText = requestMessage.GetText();
string preparedRequestText = requestText;
//
// Prepare the actual message to send to the model
//
Message preparedRequestMessage = await PrepareRequestAsync(requestMessage, cancelToken).ConfigureAwait(false);
if (!object.ReferenceEquals(preparedRequestMessage, requestMessage))
{
preparedRequestText = preparedRequestMessage.GetText();
}
//
// Prepare the context to send. For context building, use the original request text
//
Prompt context = await BuildContextAsync(requestText, preparedRequestText.Length, cancelToken).ConfigureAwait(false);
//
// Translate
//
T response = await _translator.TranslateAsync(preparedRequestText, context, null, cancelToken).ConfigureAwait(false);
Message responseMessage = Message.FromAssistant(response);
await ReceivedResponseAsync(requestMessage, preparedRequestMessage, responseMessage).ConfigureAwait(false);
return responseMessage;
}
/// <summary>
/// Get a response to the given message
/// </summary>
/// <param name="request">request</param>
/// <param name="cancelToken">optional cancel token</param>
/// <returns>Response object</returns>
public async Task<T> GetResponseAsync(Message request, CancellationToken cancelToken = default)
{
Message response = await GetResponseMessageAsync(request, cancelToken).ConfigureAwait(false);
return response.GetBody<T>();
}
async Task<Prompt> BuildContextAsync(string requestText, int actualRequestLength, CancellationToken cancelToken)
{
PromptBuilder builder = CreateBuilder(_maxPromptLength - actualRequestLength);
// Add any preamble
builder.AddRange(_instructions);
//
// If a context provider is available, inject additional context
//
if (_contextProvider is not null)
{
var context = _contextProvider.GetContextAsync(requestText, cancelToken);
builder.Add(PromptSection.Instruction("IMPORTANT CONTEXT for the user request:"));
await AppendContextAsync(builder, context).ConfigureAwait(false);
}
return builder.Prompt;
}
/// <summary>
/// Override to customize the actual message sent to the model. Several scenarios involve
/// transforming the user's message in various ways first
/// By default, the request message is sent to the model as is
/// </summary>
/// <param name="request">request message</param>
/// <param name="cancelToken">cancel </param>
/// <returns>Actual text to send to the AI</returns>
protected virtual Task<Message> PrepareRequestAsync(Message request, CancellationToken cancelToken)
{
return Task.FromResult(request);
}
/// <summary>
/// Override to customize how user context is added to the given prompt builder
/// Since the builder will limit the # of characters, you may want to do some pre processing
/// </summary>
/// <param name="builder">builder to append to</param>
/// <param name="context">context to append</param>
/// <returns></returns>
protected virtual Task<bool> AppendContextAsync(PromptBuilder builder, IAsyncEnumerable<IPromptSection> context)
{
return builder.AddRangeAsync(context);
}
/// <summary>
/// Invoked when a valid response was received - the response is placed in the message body
/// </summary>
/// <param name="request">request message</param>
/// <param name="preparedRequest">the prepared request that was actually used in translation</param>
/// <param name="response">response message</param>
/// <returns></returns>
protected virtual Task ReceivedResponseAsync(Message request, Message preparedRequest, Message response)
{
return Task.CompletedTask;
}
PromptBuilder CreateBuilder(int maxLength)
{
// Future: Pool these
return new PromptBuilder(maxLength);
}
}