1
+ import dataclasses
1
2
import io
2
3
import json
3
4
import logging
4
5
import mimetypes
5
6
import os
6
7
from pathlib import Path
7
- from typing import AsyncGenerator
8
+ from typing import AsyncGenerator , cast
8
9
9
10
from azure .core .exceptions import ResourceNotFoundError
10
11
from azure .identity .aio import DefaultAzureCredential , get_bearer_token_provider
12
+ from azure .keyvault .secrets .aio import SecretClient
11
13
from azure .monitor .opentelemetry import configure_azure_monitor
12
14
from azure .search .documents .aio import SearchClient
13
15
from azure .storage .blob .aio import BlobServiceClient
28
30
)
29
31
from quart_cors import cors
30
32
33
+ from approaches .approach import Approach
31
34
from approaches .chatreadretrieveread import ChatReadRetrieveReadApproach
35
+ from approaches .chatreadretrievereadvision import ChatReadRetrieveReadVisionApproach
32
36
from approaches .retrievethenread import RetrieveThenReadApproach
37
+ from approaches .retrievethenreadvision import RetrieveThenReadVisionApproach
33
38
from core .authentication import AuthenticationHelper
34
39
40
+ CONFIG_OPENAI_TOKEN = "openai_token"
41
+ CONFIG_CREDENTIAL = "azure_credential"
35
42
CONFIG_ASK_APPROACH = "ask_approach"
43
+ CONFIG_ASK_VISION_APPROACH = "ask_vision_approach"
44
+ CONFIG_CHAT_VISION_APPROACH = "chat_vision_approach"
36
45
CONFIG_CHAT_APPROACH = "chat_approach"
37
46
CONFIG_BLOB_CONTAINER_CLIENT = "blob_container_client"
38
47
CONFIG_AUTH_CLIENT = "auth_client"
48
+ CONFIG_GPT4V_DEPLOYED = "gpt4v_deployed"
39
49
CONFIG_SEARCH_CLIENT = "search_client"
40
50
CONFIG_OPENAI_CLIENT = "openai_client"
41
51
ERROR_MESSAGE = """The app encountered an error processing your request.
@@ -121,7 +131,12 @@ async def ask():
121
131
auth_helper = current_app .config [CONFIG_AUTH_CLIENT ]
122
132
context ["auth_claims" ] = await auth_helper .get_auth_claims_if_enabled (request .headers )
123
133
try :
124
- approach = current_app .config [CONFIG_ASK_APPROACH ]
134
+ use_gpt4v = context .get ("overrides" , {}).get ("use_gpt4v" , False )
135
+ approach : Approach
136
+ if use_gpt4v and CONFIG_ASK_VISION_APPROACH in current_app .config :
137
+ approach = cast (Approach , current_app .config [CONFIG_ASK_VISION_APPROACH ])
138
+ else :
139
+ approach = cast (Approach , current_app .config [CONFIG_ASK_APPROACH ])
125
140
r = await approach .run (
126
141
request_json ["messages" ], context = context , session_state = request_json .get ("session_state" )
127
142
)
@@ -130,13 +145,20 @@ async def ask():
130
145
return error_response (error , "/ask" )
131
146
132
147
148
+ class JSONEncoder (json .JSONEncoder ):
149
+ def default (self , o ):
150
+ if dataclasses .is_dataclass (o ):
151
+ return dataclasses .asdict (o )
152
+ return super ().default (o )
153
+
154
+
133
155
async def format_as_ndjson (r : AsyncGenerator [dict , None ]) -> AsyncGenerator [str , None ]:
134
156
try :
135
157
async for event in r :
136
- yield json .dumps (event , ensure_ascii = False ) + "\n "
137
- except Exception as e :
138
- logging .exception ("Exception while generating response stream: %s" , e )
139
- yield json .dumps (error_dict (e ))
158
+ yield json .dumps (event , ensure_ascii = False , cls = JSONEncoder ) + "\n "
159
+ except Exception as error :
160
+ logging .exception ("Exception while generating response stream: %s" , error )
161
+ yield json .dumps (error_dict (error ))
140
162
141
163
142
164
@bp .route ("/chat" , methods = ["POST" ])
@@ -147,8 +169,15 @@ async def chat():
147
169
context = request_json .get ("context" , {})
148
170
auth_helper = current_app .config [CONFIG_AUTH_CLIENT ]
149
171
context ["auth_claims" ] = await auth_helper .get_auth_claims_if_enabled (request .headers )
172
+
150
173
try :
151
- approach = current_app .config [CONFIG_CHAT_APPROACH ]
174
+ use_gpt4v = context .get ("overrides" , {}).get ("use_gpt4v" , False )
175
+ approach : Approach
176
+ if use_gpt4v and CONFIG_CHAT_VISION_APPROACH in current_app .config :
177
+ approach = cast (Approach , current_app .config [CONFIG_CHAT_VISION_APPROACH ])
178
+ else :
179
+ approach = cast (Approach , current_app .config [CONFIG_CHAT_APPROACH ])
180
+
152
181
result = await approach .run (
153
182
request_json ["messages" ],
154
183
stream = request_json .get ("stream" , False ),
@@ -173,21 +202,31 @@ def auth_setup():
173
202
return jsonify (auth_helper .get_auth_setup_for_client ())
174
203
175
204
205
+ @bp .route ("/config" , methods = ["GET" ])
206
+ def config ():
207
+ return jsonify ({"showGPT4VOptions" : current_app .config [CONFIG_GPT4V_DEPLOYED ]})
208
+
209
+
176
210
@bp .before_app_serving
177
211
async def setup_clients ():
178
212
# Replace these with your own values, either in environment variables or directly here
179
213
AZURE_STORAGE_ACCOUNT = os .environ ["AZURE_STORAGE_ACCOUNT" ]
180
214
AZURE_STORAGE_CONTAINER = os .environ ["AZURE_STORAGE_CONTAINER" ]
181
215
AZURE_SEARCH_SERVICE = os .environ ["AZURE_SEARCH_SERVICE" ]
182
216
AZURE_SEARCH_INDEX = os .environ ["AZURE_SEARCH_INDEX" ]
217
+ VISION_SECRET_NAME = os .getenv ("VISION_SECRET_NAME" )
218
+ AZURE_KEY_VAULT_NAME = os .getenv ("AZURE_KEY_VAULT_NAME" )
183
219
# Shared by all OpenAI deployments
184
220
OPENAI_HOST = os .getenv ("OPENAI_HOST" , "azure" )
185
221
OPENAI_CHATGPT_MODEL = os .environ ["AZURE_OPENAI_CHATGPT_MODEL" ]
186
222
OPENAI_EMB_MODEL = os .getenv ("AZURE_OPENAI_EMB_MODEL_NAME" , "text-embedding-ada-002" )
187
223
# Used with Azure OpenAI deployments
188
224
AZURE_OPENAI_SERVICE = os .getenv ("AZURE_OPENAI_SERVICE" )
225
+ AZURE_OPENAI_GPT4V_DEPLOYMENT = os .environ .get ("AZURE_OPENAI_GPT4V_DEPLOYMENT" )
226
+ AZURE_OPENAI_GPT4V_MODEL = os .environ .get ("AZURE_OPENAI_GPT4V_MODEL" )
189
227
AZURE_OPENAI_CHATGPT_DEPLOYMENT = os .getenv ("AZURE_OPENAI_CHATGPT_DEPLOYMENT" ) if OPENAI_HOST == "azure" else None
190
228
AZURE_OPENAI_EMB_DEPLOYMENT = os .getenv ("AZURE_OPENAI_EMB_DEPLOYMENT" ) if OPENAI_HOST == "azure" else None
229
+ AZURE_VISION_ENDPOINT = os .getenv ("AZURE_VISION_ENDPOINT" , "" )
191
230
# Used only with non-Azure OpenAI deployments
192
231
OPENAI_API_KEY = os .getenv ("OPENAI_API_KEY" )
193
232
OPENAI_ORGANIZATION = os .getenv ("OPENAI_ORGANIZATION" )
@@ -204,6 +243,8 @@ async def setup_clients():
204
243
AZURE_SEARCH_QUERY_LANGUAGE = os .getenv ("AZURE_SEARCH_QUERY_LANGUAGE" , "en-us" )
205
244
AZURE_SEARCH_QUERY_SPELLER = os .getenv ("AZURE_SEARCH_QUERY_SPELLER" , "lexicon" )
206
245
246
+ USE_GPT4V = os .getenv ("USE_GPT4V" , "" ).lower () == "true"
247
+
207
248
# Use the current user identity to authenticate with Azure OpenAI, AI Search and Blob Storage (no secrets needed,
208
249
# just use 'az login' locally, and managed identity when deployed on Azure). If you need to use keys, use separate AzureKeyCredential instances with the
209
250
# keys for each service
@@ -231,6 +272,15 @@ async def setup_clients():
231
272
)
232
273
blob_container_client = blob_client .get_container_client (AZURE_STORAGE_CONTAINER )
233
274
275
+ vision_key = None
276
+ if VISION_SECRET_NAME and AZURE_KEY_VAULT_NAME : # Cognitive vision keys are stored in keyvault
277
+ key_vault_client = SecretClient (
278
+ vault_url = f"https://{ AZURE_KEY_VAULT_NAME } .vault.azure.net" , credential = azure_credential
279
+ )
280
+ vision_secret = await key_vault_client .get_secret (VISION_SECRET_NAME )
281
+ vision_key = vision_secret .value
282
+ await key_vault_client .close ()
283
+
234
284
# Used by the OpenAI SDK
235
285
openai_client : AsyncOpenAI
236
286
@@ -253,6 +303,8 @@ async def setup_clients():
253
303
current_app .config [CONFIG_BLOB_CONTAINER_CLIENT ] = blob_container_client
254
304
current_app .config [CONFIG_AUTH_CLIENT ] = auth_helper
255
305
306
+ current_app .config [CONFIG_GPT4V_DEPLOYED ] = bool (USE_GPT4V )
307
+
256
308
# Various approaches to integrate GPT and external knowledge, most applications will use a single one of these patterns
257
309
# or some derivative, here we include several for exploration purposes
258
310
current_app .config [CONFIG_ASK_APPROACH ] = RetrieveThenReadApproach (
@@ -268,6 +320,42 @@ async def setup_clients():
268
320
query_speller = AZURE_SEARCH_QUERY_SPELLER ,
269
321
)
270
322
323
+ if AZURE_OPENAI_GPT4V_MODEL :
324
+ if vision_key is None :
325
+ raise ValueError ("Vision key must be set (in Key Vault) to use the vision approach." )
326
+
327
+ current_app .config [CONFIG_ASK_VISION_APPROACH ] = RetrieveThenReadVisionApproach (
328
+ search_client = search_client ,
329
+ openai_client = openai_client ,
330
+ blob_container_client = blob_container_client ,
331
+ vision_endpoint = AZURE_VISION_ENDPOINT ,
332
+ vision_key = vision_key ,
333
+ gpt4v_deployment = AZURE_OPENAI_GPT4V_DEPLOYMENT ,
334
+ gpt4v_model = AZURE_OPENAI_GPT4V_MODEL ,
335
+ embedding_model = OPENAI_EMB_MODEL ,
336
+ embedding_deployment = AZURE_OPENAI_EMB_DEPLOYMENT ,
337
+ sourcepage_field = KB_FIELDS_SOURCEPAGE ,
338
+ content_field = KB_FIELDS_CONTENT ,
339
+ query_language = AZURE_SEARCH_QUERY_LANGUAGE ,
340
+ query_speller = AZURE_SEARCH_QUERY_SPELLER ,
341
+ )
342
+
343
+ current_app .config [CONFIG_CHAT_VISION_APPROACH ] = ChatReadRetrieveReadVisionApproach (
344
+ search_client = search_client ,
345
+ openai_client = openai_client ,
346
+ blob_container_client = blob_container_client ,
347
+ vision_endpoint = AZURE_VISION_ENDPOINT ,
348
+ vision_key = vision_key ,
349
+ gpt4v_deployment = AZURE_OPENAI_GPT4V_DEPLOYMENT ,
350
+ gpt4v_model = AZURE_OPENAI_GPT4V_MODEL ,
351
+ embedding_model = OPENAI_EMB_MODEL ,
352
+ embedding_deployment = AZURE_OPENAI_EMB_DEPLOYMENT ,
353
+ sourcepage_field = KB_FIELDS_SOURCEPAGE ,
354
+ content_field = KB_FIELDS_CONTENT ,
355
+ query_language = AZURE_SEARCH_QUERY_LANGUAGE ,
356
+ query_speller = AZURE_SEARCH_QUERY_SPELLER ,
357
+ )
358
+
271
359
current_app .config [CONFIG_CHAT_APPROACH ] = ChatReadRetrieveReadApproach (
272
360
search_client = search_client ,
273
361
openai_client = openai_client ,
@@ -282,6 +370,12 @@ async def setup_clients():
282
370
)
283
371
284
372
373
+ @bp .after_app_serving
374
+ async def close_clients ():
375
+ await current_app .config [CONFIG_SEARCH_CLIENT ].close ()
376
+ await current_app .config [CONFIG_BLOB_CONTAINER_CLIENT ].close ()
377
+
378
+
285
379
def create_app ():
286
380
app = Quart (__name__ )
287
381
app .register_blueprint (bp )
0 commit comments