1
1
// Copyright (c) Microsoft. All rights reserved.
2
2
3
- namespace MinimalApi . Services ;
3
+ using Azure . Core ;
4
+ using Microsoft . SemanticKernel . ChatCompletion ;
5
+ using Microsoft . SemanticKernel . Connectors . OpenAI ;
6
+ using Microsoft . SemanticKernel . Embeddings ;
4
7
8
+ namespace MinimalApi . Services ;
9
+ #pragma warning disable SKEXP0011 // Mark members as static
10
+ #pragma warning disable SKEXP0001 // Mark members as static
5
11
public class ReadRetrieveReadChatService
6
12
{
7
13
private readonly ISearchService _searchClient ;
8
- private readonly IKernel _kernel ;
14
+ private readonly Kernel _kernel ;
9
15
private readonly IConfiguration _configuration ;
16
+ private readonly IComputerVisionService ? _visionService ;
17
+ private readonly TokenCredential ? _tokenCredential ;
10
18
11
19
public ReadRetrieveReadChatService (
12
20
ISearchService searchClient ,
13
21
OpenAIClient client ,
14
- IConfiguration configuration )
22
+ IConfiguration configuration ,
23
+ IComputerVisionService ? visionService = null ,
24
+ TokenCredential ? tokenCredential = null )
15
25
{
16
26
_searchClient = searchClient ;
17
- var deployedModelName = configuration [ "AzureOpenAiChatGptDeployment" ] ;
18
- ArgumentNullException . ThrowIfNullOrWhiteSpace ( deployedModelName ) ;
27
+ var kernelBuilder = Kernel . CreateBuilder ( ) ;
19
28
20
- var kernelBuilder = Kernel . Builder . WithAzureChatCompletionService ( deployedModelName , client ) ;
21
- var embeddingModelName = configuration [ "AzureOpenAiEmbeddingDeployment" ] ;
22
- if ( ! string . IsNullOrEmpty ( embeddingModelName ) )
29
+ if ( configuration [ "UseAOAI" ] != "true" )
23
30
{
24
- var endpoint = configuration [ "AzureOpenAiServiceEndpoint" ] ;
25
- ArgumentNullException . ThrowIfNullOrWhiteSpace ( endpoint ) ;
26
- kernelBuilder = kernelBuilder . WithAzureTextEmbeddingGenerationService ( embeddingModelName , endpoint , new DefaultAzureCredential ( ) ) ;
31
+ var deployment = configuration [ "OpenAiChatGptDeployment" ] ;
32
+ ArgumentNullException . ThrowIfNullOrWhiteSpace ( deployment ) ;
33
+ kernelBuilder = kernelBuilder . AddOpenAIChatCompletion ( deployment , client ) ;
34
+
35
+ var embeddingModelName = configuration [ "OpenAiEmbeddingDeployment" ] ;
36
+ ArgumentNullException . ThrowIfNullOrWhiteSpace ( embeddingModelName ) ;
37
+ kernelBuilder = kernelBuilder . AddOpenAITextEmbeddingGeneration ( embeddingModelName , client ) ;
38
+ }
39
+ else
40
+ {
41
+ var deployedModelName = configuration [ "AzureOpenAiChatGptDeployment" ] ;
42
+ ArgumentNullException . ThrowIfNullOrWhiteSpace ( deployedModelName ) ;
43
+ var embeddingModelName = configuration [ "AzureOpenAiEmbeddingDeployment" ] ;
44
+ if ( ! string . IsNullOrEmpty ( embeddingModelName ) )
45
+ {
46
+ var endpoint = configuration [ "AzureOpenAiServiceEndpoint" ] ;
47
+ ArgumentNullException . ThrowIfNullOrWhiteSpace ( endpoint ) ;
48
+ kernelBuilder = kernelBuilder . AddAzureOpenAITextEmbeddingGeneration ( embeddingModelName , endpoint , tokenCredential ?? new DefaultAzureCredential ( ) ) ;
49
+ kernelBuilder = kernelBuilder . AddAzureOpenAIChatCompletion ( deployedModelName , endpoint , tokenCredential ?? new DefaultAzureCredential ( ) ) ;
50
+ }
27
51
}
52
+
28
53
_kernel = kernelBuilder . Build ( ) ;
29
54
_configuration = configuration ;
55
+ _visionService = visionService ;
56
+ _tokenCredential = tokenCredential ;
30
57
}
31
58
32
59
public async Task < ApproachResponse > ReplyAsync (
@@ -39,8 +66,8 @@ public async Task<ApproachResponse> ReplyAsync(
39
66
var useSemanticRanker = overrides ? . SemanticRanker ?? false ;
40
67
var excludeCategory = overrides ? . ExcludeCategory ?? null ;
41
68
var filter = excludeCategory is null ? null : $ "category ne '{ excludeCategory } '";
42
- IChatCompletion chat = _kernel . GetService < IChatCompletion > ( ) ;
43
- ITextEmbeddingGeneration ? embedding = _kernel . GetService < ITextEmbeddingGeneration > ( ) ;
69
+ var chat = _kernel . GetRequiredService < IChatCompletionService > ( ) ;
70
+ var embedding = _kernel . GetRequiredService < ITextEmbeddingGenerationService > ( ) ;
44
71
float [ ] ? embeddings = null ;
45
72
var question = history . LastOrDefault ( ) ? . User is { } userQuestion
46
73
? userQuestion
@@ -55,24 +82,19 @@ public async Task<ApproachResponse> ReplyAsync(
55
82
string ? query = null ;
56
83
if ( overrides ? . RetrievalMode != RetrievalMode . Vector )
57
84
{
58
- var getQueryChat = chat . CreateNewChat ( @"You are a helpful AI assistant, generate search query for followup question.
85
+ var getQueryChat = new ChatHistory ( @"You are a helpful AI assistant, generate search query for followup question.
59
86
Make your respond simple and precise. Return the query only, do not return any other text.
60
87
e.g.
61
88
Northwind Health Plus AND standard plan.
62
89
standard plan AND dental AND employee benefit.
63
90
" ) ;
64
91
65
92
getQueryChat . AddUserMessage ( question ) ;
66
- var result = await chat . GetChatCompletionsAsync (
93
+ var result = await chat . GetChatMessageContentAsync (
67
94
getQueryChat ,
68
95
cancellationToken : cancellationToken ) ;
69
96
70
- if ( result . Count != 1 )
71
- {
72
- throw new InvalidOperationException ( "Failed to get search query" ) ;
73
- }
74
-
75
- query = result [ 0 ] . ModelResult . GetOpenAIChatResult ( ) . Choice . Message . Content ;
97
+ query = result . Content ?? throw new InvalidOperationException ( "Failed to get search query" ) ;
76
98
}
77
99
78
100
// step 2
@@ -89,12 +111,19 @@ standard plan AND dental AND employee benefit.
89
111
documentContents = string . Join ( "\r " , documentContentList . Select ( x => $ "{ x . Title } :{ x . Content } ") ) ;
90
112
}
91
113
92
- Console . WriteLine ( documentContents ) ;
114
+ // step 2.5
115
+ // retrieve images if _visionService is available
116
+ SupportingImageRecord [ ] ? images = default ;
117
+ if ( _visionService is not null )
118
+ {
119
+ var queryEmbeddings = await _visionService . VectorizeTextAsync ( query ?? question , cancellationToken ) ;
120
+ images = await _searchClient . QueryImagesAsync ( query , queryEmbeddings . vector , overrides , cancellationToken ) ;
121
+ }
122
+
93
123
// step 3
94
124
// put together related docs and conversation history to generate answer
95
- var answerChat = chat . CreateNewChat (
96
- "You are a system assistant who helps the company employees with their healthcare " +
97
- "plan questions, and questions about the employee handbook. Be brief in your answers" ) ;
125
+ var answerChat = new ChatHistory (
126
+ "You are a system assistant who helps the company employees with their questions. Be brief in your answers" ) ;
98
127
99
128
// add chat history
100
129
foreach ( var turn in history )
@@ -106,22 +135,56 @@ standard plan AND dental AND employee benefit.
106
135
}
107
136
}
108
137
109
- // format prompt
110
- answerChat . AddUserMessage ( @$ " ## Source ##
138
+
139
+ if ( images != null )
140
+ {
141
+ var prompt = @$ "## Source ##
142
+ { documentContents }
143
+ ## End ##
144
+
145
+ Answer question based on available source and images.
146
+ Your answer needs to be a json object with answer and thoughts field.
147
+ Don't put your answer between ```json and ```, return the json string directly. e.g {{""answer"": ""I don't know"", ""thoughts"": ""I don't know""}}" ;
148
+
149
+ var tokenRequestContext = new TokenRequestContext ( new [ ] { "https://storage.azure.com/.default" } ) ;
150
+ var sasToken = await ( _tokenCredential ? . GetTokenAsync ( tokenRequestContext , cancellationToken ) ?? throw new InvalidOperationException ( "Failed to get token" ) ) ;
151
+ var sasTokenString = sasToken . Token ;
152
+ var imageUrls = images . Select ( x => $ "{ x . Url } ?{ sasTokenString } ") . ToArray ( ) ;
153
+ var collection = new ChatMessageContentItemCollection ( ) ;
154
+ collection . Add ( new TextContent ( prompt ) ) ;
155
+ foreach ( var imageUrl in imageUrls )
156
+ {
157
+ collection . Add ( new ImageContent ( new Uri ( imageUrl ) ) ) ;
158
+ }
159
+
160
+ answerChat . AddUserMessage ( collection ) ;
161
+ }
162
+ else
163
+ {
164
+ var prompt = @$ " ## Source ##
111
165
{ documentContents }
112
166
## End ##
113
167
114
168
You answer needs to be a json object with the following format.
115
169
{{
116
170
""answer"": // the answer to the question, add a source reference to the end of each sentence. e.g. Apple is a fruit [reference1.pdf][reference2.pdf]. If no source available, put the answer as I don't know.
117
171
""thoughts"": // brief thoughts on how you came up with the answer, e.g. what sources you used, what you thought about, etc.
118
- }}" ) ;
172
+ }}" ;
173
+ answerChat . AddUserMessage ( prompt ) ;
174
+ }
175
+
176
+ var promptExecutingSetting = new OpenAIPromptExecutionSettings
177
+ {
178
+ MaxTokens = 1024 ,
179
+ Temperature = overrides ? . Temperature ?? 0.7 ,
180
+ } ;
119
181
120
182
// get answer
121
- var answer = await chat . GetChatCompletionsAsync (
183
+ var answer = await chat . GetChatMessageContentAsync (
122
184
answerChat ,
185
+ promptExecutingSetting ,
123
186
cancellationToken : cancellationToken ) ;
124
- var answerJson = answer [ 0 ] . ModelResult . GetOpenAIChatResult ( ) . Choice . Message . Content ;
187
+ var answerJson = answer . Content ?? throw new InvalidOperationException ( "Failed to get search query" ) ;
125
188
var answerObject = JsonSerializer . Deserialize < JsonElement > ( answerJson ) ;
126
189
var ans = answerObject . GetProperty ( "answer" ) . GetString ( ) ?? throw new InvalidOperationException ( "Failed to get answer" ) ;
127
190
var thoughts = answerObject . GetProperty ( "thoughts" ) . GetString ( ) ?? throw new InvalidOperationException ( "Failed to get thoughts" ) ;
@@ -130,7 +193,7 @@ You answer needs to be a json object with the following format.
130
193
// add follow up questions if requested
131
194
if ( overrides ? . SuggestFollowupQuestions is true )
132
195
{
133
- var followUpQuestionChat = chat . CreateNewChat ( @"You are a helpful AI assistant" ) ;
196
+ var followUpQuestionChat = new ChatHistory ( @"You are a helpful AI assistant" ) ;
134
197
followUpQuestionChat . AddUserMessage ( $@ "Generate three follow-up question based on the answer you just generated.
135
198
# Answer
136
199
{ ans }
@@ -144,11 +207,11 @@ Return the follow-up question as a json string list.
144
207
""What is the out-of-pocket maximum?""
145
208
]" ) ;
146
209
147
- var followUpQuestions = await chat . GetChatCompletionsAsync (
210
+ var followUpQuestions = await chat . GetChatMessageContentAsync (
148
211
followUpQuestionChat ,
149
212
cancellationToken : cancellationToken ) ;
150
213
151
- var followUpQuestionsJson = followUpQuestions [ 0 ] . ModelResult . GetOpenAIChatResult ( ) . Choice . Message . Content ;
214
+ var followUpQuestionsJson = followUpQuestions . Content ?? throw new InvalidOperationException ( "Failed to get search query" ) ;
152
215
var followUpQuestionsObject = JsonSerializer . Deserialize < JsonElement > ( followUpQuestionsJson ) ;
153
216
var followUpQuestionsList = followUpQuestionsObject . EnumerateArray ( ) . Select ( x => x . GetString ( ) ) . ToList ( ) ;
154
217
foreach ( var followUpQuestion in followUpQuestionsList )
@@ -158,7 +221,7 @@ Return the follow-up question as a json string list.
158
221
}
159
222
return new ApproachResponse (
160
223
DataPoints : documentContentList ,
161
- Images : null ,
224
+ Images : images ,
162
225
Answer : ans ,
163
226
Thoughts : thoughts ,
164
227
CitationBaseUrl : _configuration . ToCitationBaseUrl ( ) ) ;
0 commit comments