From 6d9f89433a21645177a346c9230aaebba6684fec Mon Sep 17 00:00:00 2001 From: Prasanjeet-Microsoft Date: Wed, 7 May 2025 13:58:35 +0530 Subject: [PATCH 01/19] Added .dockerignore file to exclude unnecessary files from Docker build context (#537) --- src/.dockerignore | 162 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 src/.dockerignore diff --git a/src/.dockerignore b/src/.dockerignore new file mode 100644 index 000000000..68dc84378 --- /dev/null +++ b/src/.dockerignore @@ -0,0 +1,162 @@ +# Include any files or directories that you don't want to be copied to your +# container here (e.g., local build artifacts, temporary files, etc.). +# +# For more help, visit the .dockerignore file reference guide at +# https://docs.docker.com/engine/reference/builder/#dockerignore-file + +**/.DS_Store +**/__pycache__ +**/.venv +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/bin +**/charts +**/docker-compose* +**/compose* +**/Dockerfile* +**/*.Dockerfile +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.log + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# VS Code +.vscode/ + +# Ignore other unnecessary files +*.bak +*.swp +.DS_Store +*.pdb +*.sqlite3 From b4ba302775981241b6c6af06596a1d0d4ceabdae Mon Sep 17 00:00:00 2001 From: AjitPadhi-Microsoft Date: Wed, 14 May 2025 17:12:16 +0530 Subject: [PATCH 02/19] fixed date time response issue (#542) --- src/App/backend/chat_logic_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/App/backend/chat_logic_handler.py b/src/App/backend/chat_logic_handler.py index 75fdc2c34..2b992d0aa 100644 --- a/src/App/backend/chat_logic_handler.py +++ b/src/App/backend/chat_logic_handler.py @@ -86,7 +86,7 @@ def greeting(self, input: Annotated[str, "the question"]) -> Annotated[str, "The answer = f"Error retrieving greeting response: {str(e)}" return answer - @kernel_function(name="ChatWithSQLDatabase", description="Given a query about client assets, investements and meeting dates or times, get details from the database based on the provided question and client id") + @kernel_function(name="ChatWithSQLDatabase", description="Given a query about client assets, investments and meeting scheduled (including upcoming or next meeting dates/times), get details from the database based on the provided question and client id") def get_SQL_Response( self, input: Annotated[str, "the question"], From 03417fe3307e9fadcaf3d8bb2965a87b6625766c Mon Sep 17 00:00:00 2001 From: Abdul-Microsoft Date: Mon, 19 May 2025 14:48:59 +0530 Subject: [PATCH 03/19] Update main.bicepparam --- infra/main.bicepparam | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infra/main.bicepparam b/infra/main.bicepparam index b7ac77755..42c04971b 100644 --- a/infra/main.bicepparam +++ b/infra/main.bicepparam @@ -8,4 +8,4 @@ param gptDeploymentCapacity = int(readEnvironmentVariable('AZURE_ENV_MODEL_CAPAC param embeddingDeploymentCapacity = int(readEnvironmentVariable('AZURE_ENV_EMBEDDING_MODEL_CAPACITY', '80')) param AzureOpenAILocation = readEnvironmentVariable('AZURE_ENV_OPENAI_LOCATION', 'eastus2') -param AZURE_LOCATION = readEnvironmentVariable('AZURE_ENV_LOCATION', '') +param AZURE_LOCATION = readEnvironmentVariable('AZURE_LOCATION', '') From 43a464be73fe5ef9cab3f28a9cc35ef57977b5ee Mon Sep 17 00:00:00 2001 From: Roopan-Microsoft <168007406+Roopan-Microsoft@users.noreply.github.com> Date: Wed, 21 May 2025 10:50:47 +0530 Subject: [PATCH 04/19] Update src/App/backend/chat_logic_handler.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/App/backend/chat_logic_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/App/backend/chat_logic_handler.py b/src/App/backend/chat_logic_handler.py index 2b992d0aa..f848a011f 100644 --- a/src/App/backend/chat_logic_handler.py +++ b/src/App/backend/chat_logic_handler.py @@ -86,7 +86,7 @@ def greeting(self, input: Annotated[str, "the question"]) -> Annotated[str, "The answer = f"Error retrieving greeting response: {str(e)}" return answer - @kernel_function(name="ChatWithSQLDatabase", description="Given a query about client assets, investments and meeting scheduled (including upcoming or next meeting dates/times), get details from the database based on the provided question and client id") + @kernel_function(name="ChatWithSQLDatabase", description="Given a query about client assets, investments and scheduled meetings (including upcoming or next meeting dates/times), get details from the database based on the provided question and client id") def get_SQL_Response( self, input: Annotated[str, "the question"], From cb49449fd68f68232604e0715a7dd971e618157a Mon Sep 17 00:00:00 2001 From: Roopan-Microsoft <168007406+Roopan-Microsoft@users.noreply.github.com> Date: Wed, 21 May 2025 10:50:54 +0530 Subject: [PATCH 05/19] Update src/.dockerignore Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/.dockerignore | 1 - 1 file changed, 1 deletion(-) diff --git a/src/.dockerignore b/src/.dockerignore index 68dc84378..8cdbb515f 100644 --- a/src/.dockerignore +++ b/src/.dockerignore @@ -157,6 +157,5 @@ cython_debug/ # Ignore other unnecessary files *.bak *.swp -.DS_Store *.pdb *.sqlite3 From 72eb23a2852dc7cd5fd22340373bcfac360a2278 Mon Sep 17 00:00:00 2001 From: Priyanka-Microsoft Date: Wed, 21 May 2025 17:06:34 +0530 Subject: [PATCH 06/19] feat: added opentelemetry log (#545) * added opentelemetry log in apis * resolved pylint issues * resolved pylint issues * resolved pylint issues * resolved pylint issues * removed space * updated requirement file --- infra/deploy_ai_foundry.bicep | 2 + infra/deploy_app_service.bicep | 5 + infra/main.bicep | 1 + infra/main.json | 39 ++-- src/App/app.py | 320 ++++++++++++++++++++++++++++++++- src/App/backend/event_utils.py | 29 +++ src/App/requirements.txt | 12 +- 7 files changed, 389 insertions(+), 19 deletions(-) create mode 100644 src/App/backend/event_utils.py diff --git a/infra/deploy_ai_foundry.bicep b/infra/deploy_ai_foundry.bicep index d6a8c611b..b8954277d 100644 --- a/infra/deploy_ai_foundry.bicep +++ b/infra/deploy_ai_foundry.bicep @@ -492,3 +492,5 @@ output aiProjectName string = aiHubProject.name output applicationInsightsId string = applicationInsights.id output logAnalyticsWorkspaceResourceName string = logAnalytics.name output storageAccountName string = storageNameCleaned +output applicationInsightsConnectionString string = applicationInsights.properties.ConnectionString + diff --git a/infra/deploy_app_service.bicep b/infra/deploy_app_service.bicep index 3d30f5291..a5531ce4f 100644 --- a/infra/deploy_app_service.bicep +++ b/infra/deploy_app_service.bicep @@ -178,6 +178,7 @@ param streamTextSystemPrompt string param aiProjectConnectionString string param useAIProjectClientFlag string = 'false' param aiProjectName string +param applicationInsightsConnectionString string // var WebAppImageName = 'DOCKER|byoaiacontainer.azurecr.io/byoaia-app:latest' @@ -215,6 +216,10 @@ resource Website 'Microsoft.Web/sites@2020-06-01' = { name: 'APPINSIGHTS_INSTRUMENTATIONKEY' value: reference(applicationInsightsId, '2015-05-01').InstrumentationKey } + { + name: 'APPLICATIONINSIGHTS_CONNECTION_STRING' + value: applicationInsightsConnectionString + } { name: 'AZURE_SEARCH_SERVICE' value: AzureSearchService diff --git a/infra/main.bicep b/infra/main.bicep index 3e286e79f..e437e94d1 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -246,6 +246,7 @@ module appserviceModule 'deploy_app_service.bicep' = { streamTextSystemPrompt: functionAppStreamTextSystemPrompt aiProjectConnectionString:keyVault.getSecret('AZURE-AI-PROJECT-CONN-STRING') aiProjectName:aifoundry.outputs.aiProjectName + applicationInsightsConnectionString:aifoundry.outputs.applicationInsightsConnectionString } scope: resourceGroup(resourceGroup().name) } diff --git a/infra/main.json b/infra/main.json index 0e4dc7597..ce81ca845 100644 --- a/infra/main.json +++ b/infra/main.json @@ -5,7 +5,7 @@ "_generator": { "name": "bicep", "version": "0.34.44.8038", - "templateHash": "1797657337218629559" + "templateHash": "9713836480105967098" } }, "parameters": { @@ -708,7 +708,7 @@ "_generator": { "name": "bicep", "version": "0.34.44.8038", - "templateHash": "3569608512312433081" + "templateHash": "18186919711353368589" } }, "parameters": { @@ -1016,11 +1016,11 @@ "name": "[format('{0}/{1}', variables('aiHubName'), format('{0}-connection-AzureOpenAI', variables('aiHubName')))]", "properties": { "category": "AIServices", - "target": "[reference(resourceId('Microsoft.CognitiveServices/accounts', variables('aiServicesName')), '2021-10-01').endpoint]", + "target": "[reference(resourceId('Microsoft.CognitiveServices/accounts', variables('aiServicesName')), '2024-04-01-preview').endpoint]", "authType": "ApiKey", "isSharedToAll": true, "credentials": { - "key": "[listKeys(resourceId('Microsoft.CognitiveServices/accounts', variables('aiServicesName')), '2021-10-01').key1]" + "key": "[listKeys(resourceId('Microsoft.CognitiveServices/accounts', variables('aiServicesName')), '2024-04-01-preview').key1]" }, "metadata": { "ApiType": "Azure", @@ -1122,7 +1122,7 @@ }, { "type": "Microsoft.CognitiveServices/accounts", - "apiVersion": "2021-10-01", + "apiVersion": "2024-04-01-preview", "name": "[variables('aiServicesName')]", "location": "[variables('location')]", "sku": { @@ -1131,9 +1131,6 @@ "kind": "AIServices", "properties": { "customSubDomainName": "[variables('aiServicesName')]", - "apiProperties": { - "statisticsEnabled": false - }, "publicNetworkAccess": "Enabled" } }, @@ -1303,7 +1300,7 @@ "apiVersion": "2021-11-01-preview", "name": "[format('{0}/{1}', parameters('keyVaultName'), 'AZURE-OPENAI-KEY')]", "properties": { - "value": "[listKeys(resourceId('Microsoft.CognitiveServices/accounts', variables('aiServicesName')), '2021-10-01').key1]" + "value": "[listKeys(resourceId('Microsoft.CognitiveServices/accounts', variables('aiServicesName')), '2024-04-01-preview').key1]" }, "dependsOn": [ "[resourceId('Microsoft.CognitiveServices/accounts', variables('aiServicesName'))]" @@ -1330,7 +1327,7 @@ "apiVersion": "2021-11-01-preview", "name": "[format('{0}/{1}', parameters('keyVaultName'), 'AZURE-OPENAI-ENDPOINT')]", "properties": { - "value": "[reference(resourceId('Microsoft.CognitiveServices/accounts', variables('aiServicesName')), '2021-10-01').endpoint]" + "value": "[reference(resourceId('Microsoft.CognitiveServices/accounts', variables('aiServicesName')), '2024-04-01-preview').endpoint]" }, "dependsOn": [ "[resourceId('Microsoft.CognitiveServices/accounts', variables('aiServicesName'))]" @@ -1393,7 +1390,7 @@ "apiVersion": "2021-11-01-preview", "name": "[format('{0}/{1}', parameters('keyVaultName'), 'COG-SERVICES-ENDPOINT')]", "properties": { - "value": "[reference(resourceId('Microsoft.CognitiveServices/accounts', variables('aiServicesName')), '2021-10-01').endpoint]" + "value": "[reference(resourceId('Microsoft.CognitiveServices/accounts', variables('aiServicesName')), '2024-04-01-preview').endpoint]" }, "dependsOn": [ "[resourceId('Microsoft.CognitiveServices/accounts', variables('aiServicesName'))]" @@ -1404,7 +1401,7 @@ "apiVersion": "2021-11-01-preview", "name": "[format('{0}/{1}', parameters('keyVaultName'), 'COG-SERVICES-KEY')]", "properties": { - "value": "[listKeys(resourceId('Microsoft.CognitiveServices/accounts', variables('aiServicesName')), '2021-10-01').key1]" + "value": "[listKeys(resourceId('Microsoft.CognitiveServices/accounts', variables('aiServicesName')), '2024-04-01-preview').key1]" }, "dependsOn": [ "[resourceId('Microsoft.CognitiveServices/accounts', variables('aiServicesName'))]" @@ -1454,7 +1451,7 @@ }, "aiServicesTarget": { "type": "string", - "value": "[reference(resourceId('Microsoft.CognitiveServices/accounts', variables('aiServicesName')), '2021-10-01').endpoint]" + "value": "[reference(resourceId('Microsoft.CognitiveServices/accounts', variables('aiServicesName')), '2024-04-01-preview').endpoint]" }, "aiServicesName": { "type": "string", @@ -1495,6 +1492,10 @@ "storageAccountName": { "type": "string", "value": "[variables('storageNameCleaned')]" + }, + "applicationInsightsConnectionString": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Insights/components', variables('applicationInsightsName')), '2020-02-02').ConnectionString]" } } } @@ -2296,6 +2297,9 @@ }, "aiProjectName": { "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_ai_foundry'), '2022-09-01').outputs.aiProjectName.value]" + }, + "applicationInsightsConnectionString": { + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_ai_foundry'), '2022-09-01').outputs.applicationInsightsConnectionString.value]" } }, "template": { @@ -2305,7 +2309,7 @@ "_generator": { "name": "bicep", "version": "0.34.44.8038", - "templateHash": "8701343999231764795" + "templateHash": "15866728948176241669" } }, "parameters": { @@ -2693,6 +2697,9 @@ }, "aiProjectName": { "type": "string" + }, + "applicationInsightsConnectionString": { + "type": "string" } }, "variables": { @@ -2732,6 +2739,10 @@ "name": "APPINSIGHTS_INSTRUMENTATIONKEY", "value": "[reference(parameters('applicationInsightsId'), '2015-05-01').InstrumentationKey]" }, + { + "name": "APPLICATIONINSIGHTS_CONNECTION_STRING", + "value": "[parameters('applicationInsightsConnectionString')]" + }, { "name": "AZURE_SEARCH_SERVICE", "value": "[parameters('AzureSearchService')]" diff --git a/src/App/app.py b/src/App/app.py index 411829551..4c9357573 100644 --- a/src/App/app.py +++ b/src/App/app.py @@ -37,6 +37,10 @@ from db import dict_cursor from backend.chat_logic_handler import stream_response_from_wealth_assistant +from backend.event_utils import track_event_if_configured +from azure.monitor.opentelemetry import configure_azure_monitor +from opentelemetry import trace +from opentelemetry.trace import Status, StatusCode bp = Blueprint("routes", __name__, static_folder="static", template_folder="static") @@ -61,6 +65,30 @@ UI_FAVICON = os.environ.get("UI_FAVICON") or "/favicon.ico" UI_SHOW_SHARE_BUTTON = os.environ.get("UI_SHOW_SHARE_BUTTON", "true").lower() == "true" +# Check if the Application Insights Instrumentation Key is set in the environment variables +instrumentation_key = os.getenv("APPLICATIONINSIGHTS_CONNECTION_STRING") +if instrumentation_key: + # Configure Application Insights if the Instrumentation Key is found + configure_azure_monitor(connection_string=instrumentation_key) + logging.info("Application Insights configured with the provided Instrumentation Key") +else: + # Log a warning if the Instrumentation Key is not found + logging.warning("No Application Insights Instrumentation Key found. Skipping configuration") + +# Configure logging +logging.basicConfig(level=logging.INFO) + +# Suppress INFO logs from 'azure.core.pipeline.policies.http_logging_policy' +logging.getLogger("azure.core.pipeline.policies.http_logging_policy").setLevel( + logging.WARNING +) +logging.getLogger("azure.identity.aio._internal").setLevel(logging.WARNING) + +# Suppress info logs from OpenTelemetry exporter +logging.getLogger("azure.monitor.opentelemetry.exporter.export._base").setLevel( + logging.WARNING +) + def create_app(): app = Quart(__name__) @@ -384,9 +412,19 @@ def init_openai_client(use_data=SHOULD_USE_DATA): azure_endpoint=endpoint, ) + track_event_if_configured("AzureOpenAIClientInitialized", { + "status": "success", + "endpoint": endpoint, + "use_api_key": bool(aoai_api_key), + }) + return azure_openai_client except Exception as e: logging.exception("Exception in Azure OpenAI initialization", e) + span = trace.get_current_span() + if span is not None: + span.record_exception(e) + span.set_status(Status(StatusCode.ERROR, str(e))) azure_openai_client = None raise e @@ -411,8 +449,20 @@ def init_cosmosdb_client(): container_name=AZURE_COSMOSDB_CONVERSATIONS_CONTAINER, enable_message_feedback=AZURE_COSMOSDB_ENABLE_FEEDBACK, ) + + track_event_if_configured("CosmosDBClientInitialized", { + "status": "success", + "endpoint": cosmos_endpoint, + "database": AZURE_COSMOSDB_DATABASE, + "container": AZURE_COSMOSDB_CONVERSATIONS_CONTAINER, + "feedback_enabled": AZURE_COSMOSDB_ENABLE_FEEDBACK, + }) except Exception as e: logging.exception("Exception in CosmosDB initialization", e) + span = trace.get_current_span() + if span is not None: + span.record_exception(e) + span.set_status(Status(StatusCode.ERROR, str(e))) cosmos_conversation_client = None raise e else: @@ -425,6 +475,7 @@ def get_configured_data_source(): data_source = {} query_type = "simple" if DATASOURCE_TYPE == "AzureCognitiveSearch": + track_event_if_configured("datasource_selected", {"type": "AzureCognitiveSearch"}) # Set query type if AZURE_SEARCH_QUERY_TYPE: query_type = AZURE_SEARCH_QUERY_TYPE @@ -433,6 +484,7 @@ def get_configured_data_source(): and AZURE_SEARCH_SEMANTIC_SEARCH_CONFIG ): query_type = "semantic" + track_event_if_configured("query_type_determined", {"query_type": query_type}) # Set filter filter = None @@ -441,11 +493,13 @@ def get_configured_data_source(): userToken = request.headers.get("X-MS-TOKEN-AAD-ACCESS-TOKEN", "") logging.debug(f"USER TOKEN is {'present' if userToken else 'not present'}") if not userToken: + track_event_if_configured("user_token_missing", {}) raise Exception( "Document-level access control is enabled, but user access token could not be fetched." ) filter = generateFilterString(userToken) + track_event_if_configured("filter_generated", {"filter": filter}) logging.debug(f"FILTER: {filter}") # Set authentication @@ -455,6 +509,7 @@ def get_configured_data_source(): else: # If key is not provided, assume AOAI resource identity has been granted access to the search service authentication = {"type": "system_assigned_managed_identity"} + track_event_if_configured("authentication_set", {"auth_type": authentication["type"]}) data_source = { "type": "azure_search", @@ -508,6 +563,7 @@ def get_configured_data_source(): } elif DATASOURCE_TYPE == "AzureCosmosDB": query_type = "vector" + track_event_if_configured("datasource_selected", {"type": "AzureCosmosDB"}) data_source = { "type": "azure_cosmos_db", @@ -566,8 +622,10 @@ def get_configured_data_source(): }, } elif DATASOURCE_TYPE == "Elasticsearch": + track_event_if_configured("datasource_selected", {"type": "Elasticsearch"}) if ELASTICSEARCH_QUERY_TYPE: query_type = ELASTICSEARCH_QUERY_TYPE + track_event_if_configured("query_type_determined", {"query_type": query_type}) data_source = { "type": "elasticsearch", @@ -621,8 +679,10 @@ def get_configured_data_source(): }, } elif DATASOURCE_TYPE == "AzureMLIndex": + track_event_if_configured("datasource_selected", {"type": "AzureMLIndex"}) if AZURE_MLINDEX_QUERY_TYPE: query_type = AZURE_MLINDEX_QUERY_TYPE + track_event_if_configured("query_type_determined", {"query_type": query_type}) data_source = { "type": "azure_ml_index", @@ -674,6 +734,7 @@ def get_configured_data_source(): } elif DATASOURCE_TYPE == "Pinecone": query_type = "vector" + track_event_if_configured("datasource_selected", {"type": "Pinecone"}) data_source = { "type": "pinecone", @@ -716,6 +777,7 @@ def get_configured_data_source(): }, } else: + track_event_if_configured("unknown_datasource_type", {"type": DATASOURCE_TYPE}) raise Exception( f"DATASOURCE_TYPE is not configured or unknown: {DATASOURCE_TYPE}" ) @@ -742,15 +804,26 @@ def get_configured_data_source(): "model_id": ELASTICSEARCH_EMBEDDING_MODEL_ID, } else: + track_event_if_configured("embedding_dependency_missing", { + "datasource_type": DATASOURCE_TYPE, + "query_type": query_type + }) raise Exception( f"Vector query type ({query_type}) is selected for data source type {DATASOURCE_TYPE} but no embedding dependency is configured" ) + track_event_if_configured("embedding_dependency_set", { + "embedding_type": embeddingDependency.get("type") + }) data_source["parameters"]["embedding_dependency"] = embeddingDependency - + track_event_if_configured("get_configured_data_source_complete", { + "datasource_type": DATASOURCE_TYPE, + "query_type": query_type + }) return data_source def prepare_model_args(request_body, request_headers): + track_event_if_configured("prepare_model_args_start", {}) request_messages = request_body.get("messages", []) messages = [] if not SHOULD_USE_DATA: @@ -775,6 +848,7 @@ def prepare_model_args(request_body, request_headers): ), } user_json = json.dumps(user_args) + track_event_if_configured("ms_defender_user_info_added", {"user_id": user_args["EndUserId"]}) model_args = { "messages": messages, @@ -792,6 +866,7 @@ def prepare_model_args(request_body, request_headers): } if SHOULD_USE_DATA: + track_event_if_configured("ms_defender_user_info_added", {"user_id": user_args["EndUserId"]}) model_args["extra_body"] = {"data_sources": [get_configured_data_source()]} model_args_clean = copy.deepcopy(model_args) @@ -829,11 +904,13 @@ def prepare_model_args(request_body, request_headers): ]["authentication"][field] = "*****" logging.debug(f"REQUEST BODY: {json.dumps(model_args_clean, indent=4)}") + track_event_if_configured("prepare_model_args_complete", {"model": AZURE_OPENAI_MODEL}) return model_args async def promptflow_request(request): + track_event_if_configured("promptflow_request_start", {}) try: headers = { "Content-Type": "application/json", @@ -861,12 +938,18 @@ async def promptflow_request(request): ) resp = response.json() resp["id"] = request["messages"][-1]["id"] + track_event_if_configured("promptflow_request_success", {}) return resp except Exception as e: + span = trace.get_current_span() + if span is not None: + span.record_exception(e) + span.set_status(Status(StatusCode.ERROR, str(e))) logging.error(f"An error occurred while making promptflow_request: {e}") async def send_chat_request(request_body, request_headers): + track_event_if_configured("send_chat_request_start", {}) filtered_messages = [] messages = request_body.get("messages", []) for message in messages: @@ -885,13 +968,20 @@ async def send_chat_request(request_body, request_headers): ) response = raw_response.parse() apim_request_id = raw_response.headers.get("apim-request-id") + + track_event_if_configured("send_chat_request_success", {"model": model_args.get("model")}) except Exception as e: + span = trace.get_current_span() + if span is not None: + span.record_exception(e) + span.set_status(Status(StatusCode.ERROR, str(e))) logging.exception("Exception in send_chat_request") raise e return response, apim_request_id async def complete_chat_request(request_body, request_headers): + track_event_if_configured("complete_chat_request_start", {}) if USE_PROMPTFLOW and PROMPTFLOW_ENDPOINT and PROMPTFLOW_API_KEY: response = await promptflow_request(request_body) history_metadata = request_body.get("history_metadata", {}) @@ -902,6 +992,7 @@ async def complete_chat_request(request_body, request_headers): PROMPTFLOW_CITATIONS_FIELD_NAME, ) elif USE_INTERNAL_STREAM: + track_event_if_configured("internal_stream_selected", {}) request_body = await request.get_json() client_id = request_body.get("client_id") print(request_body) @@ -963,10 +1054,13 @@ async def complete_chat_request(request_body, request_headers): {"role": "assistant", "content": query_response} ) + track_event_if_configured("complete_chat_request_success", {"client_id": client_id}) + return response async def stream_chat_request(request_body, request_headers): + track_event_if_configured("stream_chat_request_start", {}) if USE_INTERNAL_STREAM: history_metadata = request_body.get("history_metadata", {}) # function_url = STREAMING_AZUREFUNCTION_ENDPOINT @@ -974,8 +1068,10 @@ async def stream_chat_request(request_body, request_headers): client_id = request_body.get("client_id") if client_id is None: + track_event_if_configured("client_id_missing", {}) return jsonify({"error": "No client ID provided"}), 400 query = request_body.get("messages")[-1].get("content") + track_event_if_configured("stream_internal_selected", {"client_id": client_id}) sk_response = await stream_response_from_wealth_assistant(query, client_id) @@ -1028,11 +1124,16 @@ async def generate(): yield format_stream_response( completionChunk, history_metadata, apim_request_id ) - + track_event_if_configured("stream_openai_selected", {}) return generate() async def conversation_internal(request_body, request_headers): + track_event_if_configured("conversation_internal_start", { + "streaming": SHOULD_STREAM, + "promptflow": USE_PROMPTFLOW, + "internal_stream": USE_INTERNAL_STREAM + }) try: if SHOULD_STREAM: return await stream_chat_request(request_body, request_headers) @@ -1042,9 +1143,14 @@ async def conversation_internal(request_body, request_headers): # return response else: result = await complete_chat_request(request_body, request_headers) + track_event_if_configured("conversation_internal_success", {}) return jsonify(result) except Exception as ex: + span = trace.get_current_span() + if span is not None: + span.record_exception(ex) + span.set_status(Status(StatusCode.ERROR, str(ex))) logging.exception(ex) if hasattr(ex, "status_code"): return jsonify({"error": str(ex)}), ex.status_code @@ -1055,9 +1161,10 @@ async def conversation_internal(request_body, request_headers): @bp.route("/conversation", methods=["POST"]) async def conversation(): if not request.is_json: + track_event_if_configured("invalid_request_format", {}) return jsonify({"error": "request must be json"}), 415 request_json = await request.get_json() - + track_event_if_configured("conversation_api_invoked", {}) return await conversation_internal(request_json, request.headers) @@ -1067,6 +1174,10 @@ def get_frontend_settings(): return jsonify(frontend_settings), 200 except Exception as e: logging.exception("Exception in /frontend_settings") + span = trace.get_current_span() + if span is not None: + span.record_exception(e) + span.set_status(Status(StatusCode.ERROR, str(e))) return jsonify({"error": str(e)}), 500 @@ -1075,6 +1186,10 @@ def get_frontend_settings(): async def add_conversation(): authenticated_user = get_authenticated_user_details(request_headers=request.headers) user_id = authenticated_user["user_principal_id"] + track_event_if_configured( + "HistoryGenerate_Start", + {"user_id": user_id} + ) # check request for conversation_id request_json = await request.get_json() @@ -1097,6 +1212,15 @@ async def add_conversation(): history_metadata["title"] = title history_metadata["date"] = conversation_dict["createdAt"] + track_event_if_configured( + "ConversationCreated", + { + "user_id": user_id, + "conversation_id": conversation_id, + "title": title + } + ) + # Format the incoming message object in the "chat/completions" messages format # then write it to the conversation history in cosmos messages = request_json["messages"] @@ -1113,6 +1237,14 @@ async def add_conversation(): + conversation_id + "." ) + track_event_if_configured( + "UserMessageAdded", + { + "user_id": user_id, + "conversation_id": conversation_id, + "message": messages[-1], + } + ) else: raise Exception("No user message found") @@ -1122,9 +1254,28 @@ async def add_conversation(): request_body = await request.get_json() history_metadata["conversation_id"] = conversation_id request_body["history_metadata"] = history_metadata + track_event_if_configured( + "SendingToChatCompletions", + { + "user_id": user_id, + "conversation_id": conversation_id + } + ) + + track_event_if_configured( + "HistoryGenerate_Completed", + { + "user_id": user_id, + "conversation_id": conversation_id + } + ) return await conversation_internal(request_body, request.headers) except Exception as e: + span = trace.get_current_span() + if span is not None: + span.record_exception(e) + span.set_status(Status(StatusCode.ERROR, str(e))) logging.exception("Exception in /history/generate") return jsonify({"error": str(e)}), 500 @@ -1138,6 +1289,11 @@ async def update_conversation(): request_json = await request.get_json() conversation_id = request_json.get("conversation_id", None) + track_event_if_configured("UpdateConversation_Start", { + "user_id": user_id, + "conversation_id": conversation_id + }) + try: # make sure cosmos is configured cosmos_conversation_client = init_cosmosdb_client() @@ -1160,6 +1316,10 @@ async def update_conversation(): user_id=user_id, input_message=messages[-2], ) + track_event_if_configured("ToolMessageStored", { + "user_id": user_id, + "conversation_id": conversation_id + }) # write the assistant message await cosmos_conversation_client.create_message( uuid=messages[-1]["id"], @@ -1167,16 +1327,28 @@ async def update_conversation(): user_id=user_id, input_message=messages[-1], ) + track_event_if_configured("AssistantMessageStored", { + "user_id": user_id, + "conversation_id": conversation_id, + "message": messages[-1] + }) else: raise Exception("No bot messages found") - # Submit request to Chat Completions for response await cosmos_conversation_client.cosmosdb_client.close() + track_event_if_configured("UpdateConversation_Success", { + "user_id": user_id, + "conversation_id": conversation_id + }) response = {"success": True} return jsonify(response), 200 except Exception as e: logging.exception("Exception in /history/update") + span = trace.get_current_span() + if span is not None: + span.record_exception(e) + span.set_status(Status(StatusCode.ERROR, str(e))) return jsonify({"error": str(e)}), 500 @@ -1190,6 +1362,11 @@ async def update_message(): request_json = await request.get_json() message_id = request_json.get("message_id", None) message_feedback = request_json.get("message_feedback", None) + + track_event_if_configured("MessageFeedback_Start", { + "user_id": user_id, + "message_id": message_id + }) try: if not message_id: return jsonify({"error": "message_id is required"}), 400 @@ -1202,6 +1379,11 @@ async def update_message(): user_id, message_id, message_feedback ) if updated_message: + track_event_if_configured("MessageFeedback_Updated", { + "user_id": user_id, + "message_id": message_id, + "feedback": message_feedback + }) return ( jsonify( { @@ -1212,6 +1394,10 @@ async def update_message(): 200, ) else: + track_event_if_configured("MessageFeedback_NotFound", { + "user_id": user_id, + "message_id": message_id + }) return ( jsonify( { @@ -1223,6 +1409,10 @@ async def update_message(): except Exception as e: logging.exception("Exception in /history/message_feedback") + span = trace.get_current_span() + if span is not None: + span.record_exception(e) + span.set_status(Status(StatusCode.ERROR, str(e))) return jsonify({"error": str(e)}), 500 @@ -1236,6 +1426,11 @@ async def delete_conversation(): request_json = await request.get_json() conversation_id = request_json.get("conversation_id", None) + track_event_if_configured("DeleteConversation_Start", { + "user_id": user_id, + "conversation_id": conversation_id + }) + try: if not conversation_id: return jsonify({"error": "conversation_id is required"}), 400 @@ -1253,6 +1448,11 @@ async def delete_conversation(): await cosmos_conversation_client.cosmosdb_client.close() + track_event_if_configured("DeleteConversation_Success", { + "user_id": user_id, + "conversation_id": conversation_id + }) + return ( jsonify( { @@ -1264,6 +1464,10 @@ async def delete_conversation(): ) except Exception as e: logging.exception("Exception in /history/delete") + span = trace.get_current_span() + if span is not None: + span.record_exception(e) + span.set_status(Status(StatusCode.ERROR, str(e))) return jsonify({"error": str(e)}), 500 @@ -1273,6 +1477,11 @@ async def list_conversations(): authenticated_user = get_authenticated_user_details(request_headers=request.headers) user_id = authenticated_user["user_principal_id"] + track_event_if_configured("ListConversations_Start", { + "user_id": user_id, + "offset": offset + }) + # make sure cosmos is configured cosmos_conversation_client = init_cosmosdb_client() if not cosmos_conversation_client: @@ -1284,10 +1493,19 @@ async def list_conversations(): ) await cosmos_conversation_client.cosmosdb_client.close() if not isinstance(conversations, list): + track_event_if_configured("ListConversations_Empty", { + "user_id": user_id, + "offset": offset + }) return jsonify({"error": f"No conversations for {user_id} were found"}), 404 # return the conversation ids + track_event_if_configured("ListConversations_Success", { + "user_id": user_id, + "conversation_count": len(conversations) + }) + return jsonify(conversations), 200 @@ -1300,7 +1518,17 @@ async def get_conversation(): request_json = await request.get_json() conversation_id = request_json.get("conversation_id", None) + track_event_if_configured("GetConversation_Start", { + "user_id": user_id, + "conversation_id": conversation_id, + }) + if not conversation_id: + track_event_if_configured("GetConversation_Failed", { + "user_id": user_id, + "conversation_id": conversation_id, + "error": f"Conversation {conversation_id} not found", + }) return jsonify({"error": "conversation_id is required"}), 400 # make sure cosmos is configured @@ -1341,6 +1569,11 @@ async def get_conversation(): ] await cosmos_conversation_client.cosmosdb_client.close() + track_event_if_configured("GetConversation_Success", { + "user_id": user_id, + "conversation_id": conversation_id, + "message_count": len(messages) + }) return jsonify({"conversation_id": conversation_id, "messages": messages}), 200 @@ -1353,7 +1586,17 @@ async def rename_conversation(): request_json = await request.get_json() conversation_id = request_json.get("conversation_id", None) + track_event_if_configured("RenameConversation_Start", { + "user_id": user_id, + "conversation_id": conversation_id + }) + if not conversation_id: + track_event_if_configured("RenameConversation_Failed", { + "user_id": user_id, + "conversation_id": conversation_id, + "error": f"Conversation {conversation_id} not found", + }) return jsonify({"error": "conversation_id is required"}), 400 # make sure cosmos is configured @@ -1385,6 +1628,12 @@ async def rename_conversation(): ) await cosmos_conversation_client.cosmosdb_client.close() + + track_event_if_configured("RenameConversation_Success", { + "user_id": user_id, + "conversation_id": conversation_id, + "new_title": title + }) return jsonify(updated_conversation), 200 @@ -1394,6 +1643,10 @@ async def delete_all_conversations(): authenticated_user = get_authenticated_user_details(request_headers=request.headers) user_id = authenticated_user["user_principal_id"] + track_event_if_configured("DeleteAllConversations_Start", { + "user_id": user_id + }) + # get conversations for user try: # make sure cosmos is configured @@ -1405,6 +1658,9 @@ async def delete_all_conversations(): user_id, offset=0, limit=None ) if not conversations: + track_event_if_configured("DeleteAllConversations_Empty", { + "user_id": user_id, + }) return jsonify({"error": f"No conversations for {user_id} were found"}), 404 # delete each conversation @@ -1419,6 +1675,12 @@ async def delete_all_conversations(): user_id, conversation["id"] ) await cosmos_conversation_client.cosmosdb_client.close() + + track_event_if_configured("DeleteAllConversations_Success", { + "user_id": user_id, + "conversation_count": len(conversations) + }) + return ( jsonify( { @@ -1430,6 +1692,10 @@ async def delete_all_conversations(): except Exception as e: logging.exception("Exception in /history/delete_all") + span = trace.get_current_span() + if span is not None: + span.record_exception(e) + span.set_status(Status(StatusCode.ERROR, str(e))) return jsonify({"error": str(e)}), 500 @@ -1443,8 +1709,18 @@ async def clear_messages(): request_json = await request.get_json() conversation_id = request_json.get("conversation_id", None) + track_event_if_configured("ClearConversationMessages_Start", { + "user_id": user_id, + "conversation_id": conversation_id, + }) + try: if not conversation_id: + track_event_if_configured("ClearConversationMessages_Failed", { + "user_id": user_id, + "conversation_id": conversation_id, + "error": "conversation_id is required" + }) return jsonify({"error": "conversation_id is required"}), 400 # make sure cosmos is configured @@ -1455,6 +1731,11 @@ async def clear_messages(): # delete the conversation messages from cosmos await cosmos_conversation_client.delete_messages(conversation_id, user_id) + track_event_if_configured("ClearConversationMessages_Success", { + "user_id": user_id, + "conversation_id": conversation_id + }) + return ( jsonify( { @@ -1466,12 +1747,19 @@ async def clear_messages(): ) except Exception as e: logging.exception("Exception in /history/clear_messages") + span = trace.get_current_span() + if span is not None: + span.record_exception(e) + span.set_status(Status(StatusCode.ERROR, str(e))) return jsonify({"error": str(e)}), 500 @bp.route("/history/ensure", methods=["GET"]) async def ensure_cosmos(): if not AZURE_COSMOSDB_ACCOUNT: + track_event_if_configured("EnsureCosmosDB_Failed", { + "error": "CosmosDB is not configured", + }) return jsonify({"error": "CosmosDB is not configured"}), 404 try: @@ -1479,13 +1767,23 @@ async def ensure_cosmos(): success, err = await cosmos_conversation_client.ensure() if not cosmos_conversation_client or not success: if err: + track_event_if_configured("EnsureCosmosDB_Failed", { + "error": err, + }) return jsonify({"error": err}), 422 return jsonify({"error": "CosmosDB is not configured or not working"}), 500 await cosmos_conversation_client.cosmosdb_client.close() + track_event_if_configured("EnsureCosmosDB_Failed", { + "error": "CosmosDB is not configured or not working", + }) return jsonify({"message": "CosmosDB is configured and working"}), 200 except Exception as e: logging.exception("Exception in /history/ensure") + span = trace.get_current_span() + if span is not None: + span.record_exception(e) + span.set_status(Status(StatusCode.ERROR, str(e))) cosmos_exception = str(e) if "Invalid credentials" in cosmos_exception: return jsonify({"error": cosmos_exception}), 401 @@ -1512,6 +1810,7 @@ async def ensure_cosmos(): async def generate_title(conversation_messages): + # make sure the messages are sorted by _ts descending title_prompt = 'Summarize the conversation so far into a 4-word or less title. Do not use any quotation marks or punctuation. Respond with a json object in the format {{"title": string}}. Do not include any other commentary or description.' @@ -1540,6 +1839,8 @@ async def generate_title(conversation_messages): @bp.route("/api/users", methods=["GET"]) def get_users(): + + track_event_if_configured("UserFetch_Start", {}) conn = None try: conn = get_connection() @@ -1594,6 +1895,9 @@ def get_users(): rows = dict_cursor(cursor) if len(rows) <= 6: + track_event_if_configured("UserFetch_SampleUpdate", { + "rows_count": len(rows), + }) # update ClientMeetings,Assets,Retirement tables sample data to current date cursor = conn.cursor() combined_stmt = """ @@ -1678,9 +1982,17 @@ def get_users(): } users.append(user) + track_event_if_configured("UserFetch_Success", { + "user_count": len(users), + }) + return jsonify(users) except Exception as e: + span = trace.get_current_span() + if span is not None: + span.record_exception(e) + span.set_status(Status(StatusCode.ERROR, str(e))) print("Exception occurred:", e) return str(e), 500 finally: diff --git a/src/App/backend/event_utils.py b/src/App/backend/event_utils.py new file mode 100644 index 000000000..c04214b64 --- /dev/null +++ b/src/App/backend/event_utils.py @@ -0,0 +1,29 @@ +import logging +import os +from azure.monitor.events.extension import track_event + + +def track_event_if_configured(event_name: str, event_data: dict): + """Track an event if Application Insights is configured. + + This function safely wraps the Azure Monitor track_event function + to handle potential errors with the ProxyLogger. + + Args: + event_name: The name of the event to track + event_data: Dictionary of event data/dimensions + """ + try: + instrumentation_key = os.getenv("APPLICATIONINSIGHTS_CONNECTION_STRING") + if instrumentation_key: + track_event(event_name, event_data) + else: + logging.warning( + f"Skipping track_event for {event_name} as Application Insights is not configured" + ) + except AttributeError as e: + # Handle the 'ProxyLogger' object has no attribute 'resource' error + logging.warning(f"ProxyLogger error in track_event: {e}") + except Exception as e: + # Catch any other exceptions to prevent them from bubbling up + logging.warning(f"Error in track_event: {e}") diff --git a/src/App/requirements.txt b/src/App/requirements.txt index 1a87b8001..4d52ee10d 100644 --- a/src/App/requirements.txt +++ b/src/App/requirements.txt @@ -31,4 +31,14 @@ pyodbc==5.2.0 semantic_kernel==1.21.3 azure-search-documents==11.6.0b9 azure-ai-projects==1.0.0b9 -azure-ai-inference==1.0.0b9 \ No newline at end of file +azure-ai-inference==1.0.0b9 + +opentelemetry-exporter-otlp-proto-grpc +opentelemetry-exporter-otlp-proto-http +opentelemetry-exporter-otlp-proto-grpc +azure-monitor-events-extension +opentelemetry-sdk==1.31.1 +opentelemetry-api==1.31.1 +opentelemetry-semantic-conventions==0.52b1 +opentelemetry-instrumentation==0.52b1 +azure-monitor-opentelemetry==1.6.8 \ No newline at end of file From 58a3f42d805f34d09903314c61c42b647732610a Mon Sep 17 00:00:00 2001 From: Harsh-Microsoft Date: Tue, 27 May 2025 11:57:12 +0530 Subject: [PATCH 07/19] Update main.json --- infra/main.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/infra/main.json b/infra/main.json index 859f12668..ccc9b833d 100644 --- a/infra/main.json +++ b/infra/main.json @@ -5,7 +5,7 @@ "_generator": { "name": "bicep", "version": "0.35.1.17967", - "templateHash": "12824324392196719415" + "templateHash": "10579732773480527563" } }, "parameters": { @@ -339,9 +339,9 @@ "uniqueId": "[toLower(uniqueString(parameters('environmentName'), subscription().id, variables('solutionLocation')))]", "solutionPrefix": "[format('ca{0}', padLeft(take(variables('uniqueId'), 12), 12, '0'))]", "abbrs": "[variables('$fxv#0')]", - "functionAppSqlPrompt": "Generate a valid T-SQL query to find {query} for tables and columns provided below:\r\n 1. Table: Clients\r\n Columns: ClientId, Client, Email, Occupation, MaritalStatus, Dependents\r\n 2. Table: InvestmentGoals\r\n Columns: ClientId, InvestmentGoal\r\n 3. Table: Assets\r\n Columns: ClientId, AssetDate, Investment, ROI, Revenue, AssetType\r\n 4. Table: ClientSummaries\r\n Columns: ClientId, ClientSummary\r\n 5. Table: InvestmentGoalsDetails\r\n Columns: ClientId, InvestmentGoal, TargetAmount, Contribution\r\n 6. Table: Retirement\r\n Columns: ClientId, StatusDate, RetirementGoalProgress, EducationGoalProgress\r\n 7. Table: ClientMeetings\r\n Columns: ClientId, ConversationId, Title, StartTime, EndTime, Advisor, ClientEmail\r\n Always use the Investment column from the Assets table as the value.\r\n Assets table has snapshots of values by date. Do not add numbers across different dates for total values.\r\n Do not use client name in filters.\r\n Do not include assets values unless asked for.\r\n ALWAYS use ClientId = {clientid} in the query filter.\r\n ALWAYS select Client Name (Column: Client) in the query.\r\n Query filters are IMPORTANT. Add filters like AssetType, AssetDate, etc. if needed.\r\n Only return the generated SQL query. Do not return anything else.", - "functionAppCallTranscriptSystemPrompt": "You are an assistant who supports wealth advisors in preparing for client meetings. \r\n You have access to the client’s past meeting call transcripts. \r\n When answering questions, especially summary requests, provide a detailed and structured response that includes key topics, concerns, decisions, and trends. \r\n If no data is available, state 'No relevant data found for previous meetings.", - "functionAppStreamTextSystemPrompt": "You are a helpful assistant to a Wealth Advisor. \r\n The currently selected client's name is '{SelectedClientName}', and any case-insensitive or partial mention should be understood as referring to this client.\r\n If no name is provided, assume the question is about '{SelectedClientName}'.\r\n If the query references a different client or includes comparative terms like 'compare' or 'other client', please respond with: 'Please only ask questions about the selected client or select another client.'\r\n Otherwise, provide thorough answers using only data from SQL or call transcripts. \r\n If no data is found, please respond with 'No data found for that client.' Remove any client identifiers from the final response." + "functionAppSqlPrompt": "Generate a valid T-SQL query to find {query} for tables and columns provided below:\n 1. Table: Clients\n Columns: ClientId, Client, Email, Occupation, MaritalStatus, Dependents\n 2. Table: InvestmentGoals\n Columns: ClientId, InvestmentGoal\n 3. Table: Assets\n Columns: ClientId, AssetDate, Investment, ROI, Revenue, AssetType\n 4. Table: ClientSummaries\n Columns: ClientId, ClientSummary\n 5. Table: InvestmentGoalsDetails\n Columns: ClientId, InvestmentGoal, TargetAmount, Contribution\n 6. Table: Retirement\n Columns: ClientId, StatusDate, RetirementGoalProgress, EducationGoalProgress\n 7. Table: ClientMeetings\n Columns: ClientId, ConversationId, Title, StartTime, EndTime, Advisor, ClientEmail\n Always use the Investment column from the Assets table as the value.\n Assets table has snapshots of values by date. Do not add numbers across different dates for total values.\n Do not use client name in filters.\n Do not include assets values unless asked for.\n ALWAYS use ClientId = {clientid} in the query filter.\n ALWAYS select Client Name (Column: Client) in the query.\n Query filters are IMPORTANT. Add filters like AssetType, AssetDate, etc. if needed.\n Only return the generated SQL query. Do not return anything else.", + "functionAppCallTranscriptSystemPrompt": "You are an assistant who supports wealth advisors in preparing for client meetings. \n You have access to the client’s past meeting call transcripts. \n When answering questions, especially summary requests, provide a detailed and structured response that includes key topics, concerns, decisions, and trends. \n If no data is available, state 'No relevant data found for previous meetings.", + "functionAppStreamTextSystemPrompt": "You are a helpful assistant to a Wealth Advisor. \n The currently selected client's name is '{SelectedClientName}', and any case-insensitive or partial mention should be understood as referring to this client.\n If no name is provided, assume the question is about '{SelectedClientName}'.\n If the query references a different client or includes comparative terms like 'compare' or 'other client', please respond with: 'Please only ask questions about the selected client or select another client.'\n Otherwise, provide thorough answers using only data from SQL or call transcripts. \n If no data is found, please respond with 'No data found for that client.' Remove any client identifiers from the final response." }, "resources": [ { @@ -708,7 +708,7 @@ "_generator": { "name": "bicep", "version": "0.35.1.17967", - "templateHash": "15504864984003912125" + "templateHash": "16963364971780216238" } }, "parameters": { @@ -2310,7 +2310,7 @@ "_generator": { "name": "bicep", "version": "0.35.1.17967", - "templateHash": "9862570739171059712" + "templateHash": "18358947382114771550" } }, "parameters": { From 99738aa64f13518a5c70d15439a37762277a86f4 Mon Sep 17 00:00:00 2001 From: Harsh-Microsoft Date: Thu, 29 May 2025 12:39:28 +0530 Subject: [PATCH 08/19] fix: Replace Gunicorn with Uvicorn for the backend server (#555) * Replace Gunicorn with Uvicorn for the backend server and remove Gunicorn configuration * Update deployment instructions to use Uvicorn instead of Gunicorn --- docs/LocalSetupAndDeploy.md | 8 +++++--- src/App/WebApp.Dockerfile | 2 +- src/App/gunicorn.conf.py | 13 ------------- src/App/requirements-dev.txt | 1 - src/App/requirements.txt | 1 - 5 files changed, 6 insertions(+), 19 deletions(-) delete mode 100644 src/App/gunicorn.conf.py diff --git a/docs/LocalSetupAndDeploy.md b/docs/LocalSetupAndDeploy.md index ca09606fc..6b7547e3e 100644 --- a/docs/LocalSetupAndDeploy.md +++ b/docs/LocalSetupAndDeploy.md @@ -40,9 +40,11 @@ Follow these steps to deploy the application to Azure App Service: If this is your first time deploying the app, use the `az webapp up` command. Run the following commands from the `App` folder, replacing the placeholders with your desired values: ```sh -az webapp up --runtime PYTHON:3.11 --sku B1 --name --resource-group --location --subscription +az webapp up --runtime PYTHON:3.11 --sku B1 --name --resource-group --location --subscription -az webapp config set --startup-file "python3 -m gunicorn app:app" --name --resource-group +az webapp config set --startup-file "python3 -m uvicorn app:app --host 0.0.0.0 --port 8000" --name --resource-group + +az webapp config appsettings set --resource-group --name --settings WEBSITES_PORT=8000 ``` Next, configure the required environment variables in the deployed app to ensure it functions correctly. @@ -83,7 +85,7 @@ az webapp up \ --resource-group az webapp config set \ - --startup-file "python3 -m gunicorn app:app" \ + --startup-file "python3 -m uvicorn app:app --host 0.0.0.0 --port 8000" \ --name --resource-group ``` diff --git a/src/App/WebApp.Dockerfile b/src/App/WebApp.Dockerfile index f54e2e30c..48bcd5ff5 100644 --- a/src/App/WebApp.Dockerfile +++ b/src/App/WebApp.Dockerfile @@ -36,4 +36,4 @@ COPY --from=frontend /home/node/app/static /usr/src/app/static/ WORKDIR /usr/src/app EXPOSE 80 -CMD ["gunicorn", "-b", "0.0.0.0:80", "app:app"] +CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "80", "--workers", "4", "--log-level", "info", "--access-log"] diff --git a/src/App/gunicorn.conf.py b/src/App/gunicorn.conf.py deleted file mode 100644 index b1aded069..000000000 --- a/src/App/gunicorn.conf.py +++ /dev/null @@ -1,13 +0,0 @@ -import multiprocessing - -max_requests = 1000 -max_requests_jitter = 50 -log_file = "-" -bind = "0.0.0.0" - -timeout = 230 -# https://learn.microsoft.com/en-us/troubleshoot/azure/app-service/web-apps-performance-faqs#why-does-my-request-time-out-after-230-seconds - -num_cpus = multiprocessing.cpu_count() -workers = (num_cpus * 2) + 1 -worker_class = "uvicorn.workers.UvicornWorker" diff --git a/src/App/requirements-dev.txt b/src/App/requirements-dev.txt index aacba54f0..302b39b8b 100644 --- a/src/App/requirements-dev.txt +++ b/src/App/requirements-dev.txt @@ -7,7 +7,6 @@ python-dotenv==1.0.1 azure-cosmos==4.9.0 quart==0.20.0 uvicorn==0.34.0 -gunicorn==23.0.0 aiohttp==3.11.12 quart-session==3.0.0 pymssql==2.3.2 diff --git a/src/App/requirements.txt b/src/App/requirements.txt index 4d52ee10d..a02606dfd 100644 --- a/src/App/requirements.txt +++ b/src/App/requirements.txt @@ -8,7 +8,6 @@ python-dotenv==1.0.1 azure-cosmos==4.9.0 quart==0.20.0 uvicorn==0.34.0 -gunicorn==23.0.0 aiohttp==3.11.12 quart-session==3.0.0 pymssql==2.3.2 From 2cc84ded8799468681186e145153f5b449f8adec Mon Sep 17 00:00:00 2001 From: Vamshi-Microsoft Date: Fri, 30 May 2025 09:06:09 +0000 Subject: [PATCH 09/19] EXP environment changes for Log Analytics workspace --- docs/CustomizingAzdParameters.md | 5 +++++ docs/DeploymentGuide.md | 2 ++ infra/deploy_ai_foundry.bicep | 19 ++++++++++++++++--- infra/main.bicep | 4 ++++ infra/main.bicepparam | 1 + 5 files changed, 28 insertions(+), 3 deletions(-) diff --git a/docs/CustomizingAzdParameters.md b/docs/CustomizingAzdParameters.md index fbc1f73d3..fc02f6d17 100644 --- a/docs/CustomizingAzdParameters.md +++ b/docs/CustomizingAzdParameters.md @@ -40,4 +40,9 @@ Change the Embedding Deployment Capacity (choose a number based on available emb ```shell azd env set AZURE_ENV_EMBEDDING_MODEL_CAPACITY 80 +``` + +Set the Log Analytics Workspace Id if you need to reuse the existing workspace which is already existing +```shell +azd env set AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID '' ``` \ No newline at end of file diff --git a/docs/DeploymentGuide.md b/docs/DeploymentGuide.md index 96362bb6a..b77c23450 100644 --- a/docs/DeploymentGuide.md +++ b/docs/DeploymentGuide.md @@ -114,6 +114,8 @@ When you start the deployment, most parameters will have **default values**, but | **GPT Model Deployment Capacity** | Configure capacity for **GPT models**. | 30k | | **Embedding Model** | OpenAI embedding model | text-embedding-ada-002 | | **Embedding Model Capacity** | Set the capacity for **embedding models**. | 80k | +| **Existing Log analytics workspace** | To reuse the existing Log analytics workspace Id. | | + diff --git a/infra/deploy_ai_foundry.bicep b/infra/deploy_ai_foundry.bicep index b8954277d..ef2e81fc7 100644 --- a/infra/deploy_ai_foundry.bicep +++ b/infra/deploy_ai_foundry.bicep @@ -9,6 +9,7 @@ param gptDeploymentCapacity int param embeddingModel string param embeddingDeploymentCapacity int param managedIdentityObjectId string +param existingLogAnalyticsWorkspaceId string = '' // Load the abbrevations file required to name the azure resources. var abbrs = loadJsonContent('./abbreviations.json') @@ -54,7 +55,16 @@ resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = { name: keyVaultName } -resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2023-09-01' = { +var useExisting = !empty(existingLogAnalyticsWorkspaceId) +var existingLawResourceGroup = useExisting ? split(existingLogAnalyticsWorkspaceId, '/')[4] : '' +var existingLawName = useExisting ? split(existingLogAnalyticsWorkspaceId, '/')[8] : '' + +resource existingLogAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2023-09-01' existing = if (useExisting) { + name: existingLawName + scope: resourceGroup(existingLawResourceGroup) +} + +resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2023-09-01' = if (!useExisting) { name: workspaceName location: location tags: {} @@ -93,7 +103,7 @@ resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = { Application_Type: 'web' publicNetworkAccessForIngestion: 'Enabled' publicNetworkAccessForQuery: 'Enabled' - WorkspaceResourceId: logAnalytics.id + WorkspaceResourceId: useExisting ? existingLogAnalyticsWorkspace.id : logAnalytics.id } } @@ -490,7 +500,10 @@ output aiSearchService string = aiSearch.name output aiProjectName string = aiHubProject.name output applicationInsightsId string = applicationInsights.id -output logAnalyticsWorkspaceResourceName string = logAnalytics.name +output logAnalyticsWorkspaceResourceName string = useExisting ? existingLogAnalyticsWorkspace.name : logAnalytics.name +output logAnalyticsWorkspaceResourceGroup string = useExisting ? existingLawResourceGroup : resourceGroup().name + + output storageAccountName string = storageNameCleaned output applicationInsightsConnectionString string = applicationInsights.properties.ConnectionString diff --git a/infra/main.bicep b/infra/main.bicep index e437e94d1..b92388651 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -6,6 +6,9 @@ targetScope = 'resourceGroup' @description('A unique prefix for all resources in this deployment. This should be 3-20 characters long:') param environmentName string +@description('Optional: Existing Log Analytics Workspace Resource ID') +param existingLogAnalyticsWorkspaceId string = '' + @description('CosmosDB Location') param cosmosLocation string @@ -140,6 +143,7 @@ module aifoundry 'deploy_ai_foundry.bicep' = { embeddingModel: embeddingModel embeddingDeploymentCapacity: embeddingDeploymentCapacity managedIdentityObjectId:managedIdentityModule.outputs.managedIdentityOutput.objectId + existingLogAnalyticsWorkspaceId: existingLogAnalyticsWorkspaceId } scope: resourceGroup(resourceGroup().name) } diff --git a/infra/main.bicepparam b/infra/main.bicepparam index 42c04971b..1e5c053c1 100644 --- a/infra/main.bicepparam +++ b/infra/main.bicepparam @@ -9,3 +9,4 @@ param gptDeploymentCapacity = int(readEnvironmentVariable('AZURE_ENV_MODEL_CAPAC param embeddingDeploymentCapacity = int(readEnvironmentVariable('AZURE_ENV_EMBEDDING_MODEL_CAPACITY', '80')) param AzureOpenAILocation = readEnvironmentVariable('AZURE_ENV_OPENAI_LOCATION', 'eastus2') param AZURE_LOCATION = readEnvironmentVariable('AZURE_LOCATION', '') +param existingLogAnalyticsWorkspaceId = readEnvironmentVariable('AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID', '') From b88f111bf079d1f380a2ee8dd7915875e476137b Mon Sep 17 00:00:00 2001 From: Abdul-Microsoft Date: Mon, 2 Jun 2025 13:09:39 +0530 Subject: [PATCH 10/19] refactor: Cleanup the unused variables in all the files (#557) * cleanup the unused variables in all the files * fix: remove unused OPENAI_API_VERSION variable * fix: resolve unit tests issue --- .github/dependabot.yml | 9 - infra/deploy_app_service.bicep | 51 --- infra/main.bicep | 5 +- infra/main.json | 83 +---- src/App/.env.sample | 154 +++----- src/App/app.py | 503 +------------------------- src/App/backend/chat_logic_handler.py | 4 +- src/App/tests/backend/test_utils.py | 2 +- src/App/tests/test_app.py | 13 +- 9 files changed, 64 insertions(+), 760 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 164355b62..3f0b6a97a 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -20,15 +20,6 @@ updates: target-branch: "dependabotchanges" open-pull-requests-limit: 100 - - package-ecosystem: "pip" - directory: "/src/AzureFunction" - schedule: - interval: "monthly" - commit-message: - prefix: "build" - target-branch: "dependabotchanges" - open-pull-requests-limit: 100 - - package-ecosystem: "pip" directory: "/src/infra/scripts/fabric_scripts" schedule: diff --git a/infra/deploy_app_service.bicep b/infra/deploy_app_service.bicep index a5531ce4f..d06cf2f74 100644 --- a/infra/deploy_app_service.bicep +++ b/infra/deploy_app_service.bicep @@ -32,9 +32,6 @@ param AzureSearchUseSemanticSearch string = 'False' @description('Semantic search config') param AzureSearchSemanticSearchConfig string = 'default' -@description('Is the index prechunked') -param AzureSearchIndexIsPrechunked string = 'False' - @description('Top K results') param AzureSearchTopK string = '5' @@ -59,9 +56,6 @@ param AzureOpenAIResource string @description('Azure OpenAI Model Deployment Name') param AzureOpenAIModel string -@description('Azure OpenAI Model Name') -param AzureOpenAIModelName string = 'gpt-4o-mini' - @description('Azure Open AI Endpoint') param AzureOpenAIEndpoint string = '' @@ -116,15 +110,9 @@ param AzureOpenAIEmbeddingkey string = '' @description('Azure Open AI Embedding Endpoint') param AzureOpenAIEmbeddingEndpoint string = '' -@description('Enable chat history by deploying a Cosmos DB instance') -param WebAppEnableChatHistory string = 'False' - @description('Use Azure Function') param USE_INTERNAL_STREAM string = 'True' -@description('Azure Function Endpoint') -param STREAMING_AZUREFUNCTION_ENDPOINT string = '' - @description('SQL Database Server Name') param SQLDB_SERVER string = '' @@ -163,8 +151,6 @@ param userassignedIdentityId string param userassignedIdentityClientId string param applicationInsightsId string -@secure() -param azureSearchAdminKey string param azureSearchServiceEndpoint string @description('Azure Function App SQL System Prompt') @@ -240,10 +226,6 @@ resource Website 'Microsoft.Web/sites@2020-06-01' = { name: 'AZURE_SEARCH_SEMANTIC_SEARCH_CONFIG' value: AzureSearchSemanticSearchConfig } - { - name: 'AZURE_SEARCH_INDEX_IS_PRECHUNKED' - value: AzureSearchIndexIsPrechunked - } { name: 'AZURE_SEARCH_TOP_K' value: AzureSearchTopK @@ -284,10 +266,6 @@ resource Website 'Microsoft.Web/sites@2020-06-01' = { name: 'AZURE_OPENAI_KEY' value: AzureOpenAIKey } - { - name: 'AZURE_OPENAI_MODEL_NAME' - value: AzureOpenAIModelName - } { name: 'AZURE_OPENAI_TEMPERATURE' value: AzureOpenAITemperature @@ -346,11 +324,6 @@ resource Website 'Microsoft.Web/sites@2020-06-01' = { name: 'AZURE_OPENAI_EMBEDDING_ENDPOINT' value: AzureOpenAIEmbeddingEndpoint } - - { - name: 'WEB_APP_ENABLE_CHAT_HISTORY' - value: WebAppEnableChatHistory - } {name: 'SQLDB_SERVER' value: SQLDB_SERVER @@ -372,10 +345,6 @@ resource Website 'Microsoft.Web/sites@2020-06-01' = { value: USE_INTERNAL_STREAM } - {name: 'STREAMING_AZUREFUNCTION_ENDPOINT' - value: STREAMING_AZUREFUNCTION_ENDPOINT - } - {name: 'AZURE_COSMOSDB_ACCOUNT' value: AZURE_COSMOSDB_ACCOUNT } @@ -391,30 +360,10 @@ resource Website 'Microsoft.Web/sites@2020-06-01' = { //{name: 'VITE_POWERBI_EMBED_URL' // value: VITE_POWERBI_EMBED_URL //} - { - name: 'SCM_DO_BUILD_DURING_DEPLOYMENT' - value: 'true' - } - { - name: 'UWSGI_PROCESSES' - value: '2' - } - { - name: 'UWSGI_THREADS' - value: '2' - } { name: 'SQLDB_USER_MID' value: userassignedIdentityClientId } - { - name: 'OPENAI_API_VERSION' - value: AzureOpenAIApiVersion - } - { - name: 'AZURE_AI_SEARCH_API_KEY' - value: azureSearchAdminKey - } { name: 'AZURE_AI_SEARCH_ENDPOINT' value: azureSearchServiceEndpoint diff --git a/infra/main.bicep b/infra/main.bicep index e437e94d1..d7fea84da 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -200,17 +200,15 @@ module appserviceModule 'deploy_app_service.bicep' = { AzureSearchKey:keyVault.getSecret('AZURE-SEARCH-KEY') AzureSearchUseSemanticSearch:'True' AzureSearchSemanticSearchConfig:'my-semantic-config' - AzureSearchIndexIsPrechunked:'False' AzureSearchTopK:'5' AzureSearchContentColumns:'content' AzureSearchFilenameColumn:'chunk_id' AzureSearchTitleColumn:'client_id' AzureSearchUrlColumn:'sourceurl' - AzureOpenAIResource:aifoundry.outputs.aiServicesTarget + AzureOpenAIResource:aifoundry.outputs.aiServicesName AzureOpenAIEndpoint:aifoundry.outputs.aiServicesTarget AzureOpenAIModel:gptModelName AzureOpenAIKey:keyVault.getSecret('AZURE-OPENAI-KEY') - AzureOpenAIModelName:gptModelName AzureOpenAITemperature:'0' AzureOpenAITopP:'1' AzureOpenAIMaxTokens:'1000' @@ -239,7 +237,6 @@ module appserviceModule 'deploy_app_service.bicep' = { userassignedIdentityClientId:managedIdentityModule.outputs.managedIdentityWebAppOutput.clientId userassignedIdentityId:managedIdentityModule.outputs.managedIdentityWebAppOutput.id applicationInsightsId: aifoundry.outputs.applicationInsightsId - azureSearchAdminKey:keyVault.getSecret('AZURE-SEARCH-KEY') azureSearchServiceEndpoint:aifoundry.outputs.aiSearchTarget sqlSystemPrompt: functionAppSqlPrompt callTranscriptSystemPrompt: functionAppCallTranscriptSystemPrompt diff --git a/infra/main.json b/infra/main.json index ccc9b833d..fee4c39e0 100644 --- a/infra/main.json +++ b/infra/main.json @@ -2139,9 +2139,6 @@ "AzureSearchSemanticSearchConfig": { "value": "my-semantic-config" }, - "AzureSearchIndexIsPrechunked": { - "value": "False" - }, "AzureSearchTopK": { "value": "5" }, @@ -2158,7 +2155,7 @@ "value": "sourceurl" }, "AzureOpenAIResource": { - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_ai_foundry'), '2022-09-01').outputs.aiServicesTarget.value]" + "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_ai_foundry'), '2022-09-01').outputs.aiServicesName.value]" }, "AzureOpenAIEndpoint": { "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_ai_foundry'), '2022-09-01').outputs.aiServicesTarget.value]" @@ -2174,9 +2171,6 @@ "secretName": "AZURE-OPENAI-KEY" } }, - "AzureOpenAIModelName": { - "value": "[parameters('gptModelName')]" - }, "AzureOpenAITemperature": { "value": "0" }, @@ -2268,14 +2262,6 @@ "applicationInsightsId": { "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_ai_foundry'), '2022-09-01').outputs.applicationInsightsId.value]" }, - "azureSearchAdminKey": { - "reference": { - "keyVault": { - "id": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.KeyVault/vaults', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_ai_foundry'), '2022-09-01').outputs.keyvaultName.value)]" - }, - "secretName": "AZURE-SEARCH-KEY" - } - }, "azureSearchServiceEndpoint": { "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, resourceGroup().name), 'Microsoft.Resources/deployments', 'deploy_ai_foundry'), '2022-09-01').outputs.aiSearchTarget.value]" }, @@ -2383,13 +2369,6 @@ "description": "Semantic search config" } }, - "AzureSearchIndexIsPrechunked": { - "type": "string", - "defaultValue": "False", - "metadata": { - "description": "Is the index prechunked" - } - }, "AzureSearchTopK": { "type": "string", "defaultValue": "5", @@ -2444,13 +2423,6 @@ "description": "Azure OpenAI Model Deployment Name" } }, - "AzureOpenAIModelName": { - "type": "string", - "defaultValue": "gpt-4o-mini", - "metadata": { - "description": "Azure OpenAI Model Name" - } - }, "AzureOpenAIEndpoint": { "type": "string", "defaultValue": "", @@ -2576,13 +2548,6 @@ "description": "Azure Open AI Embedding Endpoint" } }, - "WebAppEnableChatHistory": { - "type": "string", - "defaultValue": "False", - "metadata": { - "description": "Enable chat history by deploying a Cosmos DB instance" - } - }, "USE_INTERNAL_STREAM": { "type": "string", "defaultValue": "True", @@ -2590,13 +2555,6 @@ "description": "Use Azure Function" } }, - "STREAMING_AZUREFUNCTION_ENDPOINT": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "Azure Function Endpoint" - } - }, "SQLDB_SERVER": { "type": "string", "defaultValue": "", @@ -2665,9 +2623,6 @@ "applicationInsightsId": { "type": "string" }, - "azureSearchAdminKey": { - "type": "securestring" - }, "azureSearchServiceEndpoint": { "type": "string" }, @@ -2764,10 +2719,6 @@ "name": "AZURE_SEARCH_SEMANTIC_SEARCH_CONFIG", "value": "[parameters('AzureSearchSemanticSearchConfig')]" }, - { - "name": "AZURE_SEARCH_INDEX_IS_PRECHUNKED", - "value": "[parameters('AzureSearchIndexIsPrechunked')]" - }, { "name": "AZURE_SEARCH_TOP_K", "value": "[parameters('AzureSearchTopK')]" @@ -2808,10 +2759,6 @@ "name": "AZURE_OPENAI_KEY", "value": "[parameters('AzureOpenAIKey')]" }, - { - "name": "AZURE_OPENAI_MODEL_NAME", - "value": "[parameters('AzureOpenAIModelName')]" - }, { "name": "AZURE_OPENAI_TEMPERATURE", "value": "[parameters('AzureOpenAITemperature')]" @@ -2868,10 +2815,6 @@ "name": "AZURE_OPENAI_EMBEDDING_ENDPOINT", "value": "[parameters('AzureOpenAIEmbeddingEndpoint')]" }, - { - "name": "WEB_APP_ENABLE_CHAT_HISTORY", - "value": "[parameters('WebAppEnableChatHistory')]" - }, { "name": "SQLDB_SERVER", "value": "[parameters('SQLDB_SERVER')]" @@ -2892,10 +2835,6 @@ "name": "USE_INTERNAL_STREAM", "value": "[parameters('USE_INTERNAL_STREAM')]" }, - { - "name": "STREAMING_AZUREFUNCTION_ENDPOINT", - "value": "[parameters('STREAMING_AZUREFUNCTION_ENDPOINT')]" - }, { "name": "AZURE_COSMOSDB_ACCOUNT", "value": "[parameters('AZURE_COSMOSDB_ACCOUNT')]" @@ -2912,30 +2851,10 @@ "name": "AZURE_COSMOSDB_ENABLE_FEEDBACK", "value": "[parameters('AZURE_COSMOSDB_ENABLE_FEEDBACK')]" }, - { - "name": "SCM_DO_BUILD_DURING_DEPLOYMENT", - "value": "true" - }, - { - "name": "UWSGI_PROCESSES", - "value": "2" - }, - { - "name": "UWSGI_THREADS", - "value": "2" - }, { "name": "SQLDB_USER_MID", "value": "[parameters('userassignedIdentityClientId')]" }, - { - "name": "OPENAI_API_VERSION", - "value": "[parameters('AzureOpenAIApiVersion')]" - }, - { - "name": "AZURE_AI_SEARCH_API_KEY", - "value": "[parameters('azureSearchAdminKey')]" - }, { "name": "AZURE_AI_SEARCH_ENDPOINT", "value": "[parameters('azureSearchServiceEndpoint')]" diff --git a/src/App/.env.sample b/src/App/.env.sample index 50f33c7e3..7dc66e86e 100644 --- a/src/App/.env.sample +++ b/src/App/.env.sample @@ -1,28 +1,19 @@ -# Chat -DEBUG=True +# Azure OpenAI settings AZURE_OPENAI_RESOURCE= -AZURE_OPENAI_MODEL=gpt-35-turbo-16k +AZURE_OPENAI_MODEL="gpt-4o-mini" AZURE_OPENAI_KEY= -AZURE_OPENAI_MODEL_NAME=gpt-35-turbo-16k -AZURE_OPENAI_TEMPERATURE=0 -AZURE_OPENAI_TOP_P=1.0 -AZURE_OPENAI_MAX_TOKENS=1000 +AZURE_OPENAI_TEMPERATURE="0" +AZURE_OPENAI_TOP_P="1" +AZURE_OPENAI_MAX_TOKENS="1000" AZURE_OPENAI_STOP_SEQUENCE= -AZURE_OPENAI_SEED= -AZURE_OPENAI_CHOICES_COUNT=1 -AZURE_OPENAI_PRESENCE_PENALTY=0.0 -AZURE_OPENAI_FREQUENCY_PENALTY=0.0 -AZURE_OPENAI_LOGIT_BIAS= -AZURE_OPENAI_USER= -AZURE_OPENAI_TOOLS= -AZURE_OPENAI_TOOL_CHOICE= -AZURE_OPENAI_SYSTEM_MESSAGE=You are an AI assistant that helps people find information. -AZURE_OPENAI_PREVIEW_API_VERSION=2024-05-01-preview -AZURE_OPENAI_STREAM=True +AZURE_OPENAI_SYSTEM_MESSAGE="You are a helpful Wealth Advisor assistant" +AZURE_OPENAI_PREVIEW_API_VERSION="2025-01-01-preview" +AZURE_OPENAI_STREAM="True" AZURE_OPENAI_ENDPOINT= -AZURE_OPENAI_EMBEDDING_NAME=text-embedding-ada-002 +AZURE_OPENAI_EMBEDDING_NAME="text-embedding-ada-002" AZURE_OPENAI_EMBEDDING_ENDPOINT= AZURE_OPENAI_EMBEDDING_KEY= + # User Interface UI_TITLE= UI_LOGO= @@ -30,100 +21,49 @@ UI_CHAT_LOGO= UI_CHAT_TITLE= UI_CHAT_DESCRIPTION= UI_FAVICON= -# Chat history + +# Cosmos DB settings AZURE_COSMOSDB_ACCOUNT= -AZURE_COSMOSDB_DATABASE=db_conversation_history -AZURE_COSMOSDB_CONVERSATIONS_CONTAINER=conversations -AZURE_COSMOSDB_ACCOUNT_KEY= -AZURE_COSMOSDB_ENABLE_FEEDBACK=True -# Chat with data: common settings -SEARCH_TOP_K=5 -SEARCH_STRICTNESS=3 -SEARCH_ENABLE_IN_DOMAIN=True -# Chat with data: Azure AI Search +AZURE_COSMOSDB_DATABASE="db_conversation_history" +AZURE_COSMOSDB_CONVERSATIONS_CONTAINER="conversations" +AZURE_COSMOSDB_ENABLE_FEEDBACK="True" + +# Azure Search settings AZURE_SEARCH_SERVICE= -AZURE_SEARCH_INDEX= +AZURE_SEARCH_INDEX="transcripts_index" AZURE_SEARCH_KEY= -AZURE_SEARCH_SEMANTIC_SEARCH_CONFIG= -AZURE_SEARCH_INDEX_IS_PRECHUNKED=False -AZURE_SEARCH_TOP_K=5 -AZURE_SEARCH_ENABLE_IN_DOMAIN=True -AZURE_SEARCH_CONTENT_COLUMNS=content -AZURE_SEARCH_FILENAME_COLUMN=sourceurl -AZURE_SEARCH_TITLE_COLUMN=client_id -AZURE_SEARCH_URL_COLUMN=sourceurl -AZURE_SEARCH_VECTOR_COLUMNS= -AZURE_SEARCH_QUERY_TYPE=simple +AZURE_SEARCH_SEMANTIC_SEARCH_CONFIG="my-semantic-config" +AZURE_SEARCH_TOP_K="5" +AZURE_SEARCH_ENABLE_IN_DOMAIN="False" +AZURE_SEARCH_CONTENT_COLUMNS="content" +AZURE_SEARCH_FILENAME_COLUMN="chunk_id" +AZURE_SEARCH_TITLE_COLUMN="client_id" +AZURE_SEARCH_URL_COLUMN="sourceurl" +AZURE_SEARCH_VECTOR_COLUMNS="contentVector" +AZURE_SEARCH_QUERY_TYPE="simple" AZURE_SEARCH_PERMITTED_GROUPS_COLUMN= -AZURE_SEARCH_STRICTNESS=3 -# Chat with data: Azure CosmosDB Mongo VCore -AZURE_COSMOSDB_MONGO_VCORE_CONNECTION_STRING= -AZURE_COSMOSDB_MONGO_VCORE_DATABASE= -AZURE_COSMOSDB_MONGO_VCORE_CONTAINER= -AZURE_COSMOSDB_MONGO_VCORE_INDEX= -AZURE_COSMOSDB_MONGO_VCORE_INDEX= -AZURE_COSMOSDB_MONGO_VCORE_TOP_K= -AZURE_COSMOSDB_MONGO_VCORE_STRICTNESS= -AZURE_COSMOSDB_MONGO_VCORE_ENABLE_IN_DOMAIN= -AZURE_COSMOSDB_MONGO_VCORE_CONTENT_COLUMNS= -AZURE_COSMOSDB_MONGO_VCORE_FILENAME_COLUMN= -AZURE_COSMOSDB_MONGO_VCORE_TITLE_COLUMN= -AZURE_COSMOSDB_MONGO_VCORE_URL_COLUMN= -AZURE_COSMOSDB_MONGO_VCORE_VECTOR_COLUMNS= -# Chat with data: Elasticsearch -ELASTICSEARCH_ENDPOINT= -ELASTICSEARCH_ENCODED_API_KEY= -ELASTICSEARCH_INDEX= -ELASTICSEARCH_QUERY_TYPE= -ELASTICSEARCH_TOP_K= -ELASTICSEARCH_ENABLE_IN_DOMAIN= -ELASTICSEARCH_CONTENT_COLUMNS= -ELASTICSEARCH_FILENAME_COLUMN= -ELASTICSEARCH_TITLE_COLUMN= -ELASTICSEARCH_URL_COLUMN= -ELASTICSEARCH_VECTOR_COLUMNS= -ELASTICSEARCH_STRICTNESS= -ELASTICSEARCH_EMBEDDING_MODEL_ID= -# Chat with data: Pinecone -PINECONE_ENVIRONMENT= -PINECONE_API_KEY= -PINECONE_INDEX_NAME= -PINECONE_TOP_K= -PINECONE_STRICTNESS= -PINECONE_ENABLE_IN_DOMAIN= -PINECONE_CONTENT_COLUMNS= -PINECONE_FILENAME_COLUMN= -PINECONE_TITLE_COLUMN= -PINECONE_URL_COLUMN= -PINECONE_VECTOR_COLUMNS= -# Chat with data: Azure Machine Learning MLIndex -AZURE_MLINDEX_NAME= -AZURE_MLINDEX_VERSION= -AZURE_ML_PROJECT_RESOURCE_ID= -AZURE_MLINDEX_TOP_K= -AZURE_MLINDEX_STRICTNESS= -AZURE_MLINDEX_ENABLE_IN_DOMAIN= -AZURE_MLINDEX_CONTENT_COLUMNS= -AZURE_MLINDEX_FILENAME_COLUMN= -AZURE_MLINDEX_TITLE_COLUMN= -AZURE_MLINDEX_URL_COLUMN= -AZURE_MLINDEX_VECTOR_COLUMNS= -AZURE_MLINDEX_QUERY_TYPE= -# Chat with data: Prompt flow API -USE_PROMPTFLOW=False -PROMPTFLOW_ENDPOINT= -PROMPTFLOW_API_KEY= -PROMPTFLOW_RESPONSE_TIMEOUT=120 -PROMPTFLOW_REQUEST_FIELD_NAME=query -PROMPTFLOW_RESPONSE_FIELD_NAME=reply -PROMPTFLOW_CITATIONS_FIELD_NAME=documents -STREAMING_AZUREFUNCTION_ENDPOINT= -USE_AZUREFUNCTION=True -SQL_CONNECTION= +AZURE_SEARCH_STRICTNESS="3" +AZURE_SEARCH_USE_SEMANTIC_SEARCH="True" +AZURE_AI_SEARCH_ENDPOINT= + +# Azure SQL settings SQLDB_CONNECTION_STRING= SQLDB_SERVER= SQLDB_DATABASE= SQLDB_USERNAME= SQLDB_PASSWORD= -SQLDB_DRIVER= -VITE_POWERBI_EMBED_URL= \ No newline at end of file +SQLDB_USER_MID= + +# AI Project +AZURE_AI_PROJECT_CONN_STRING= +USE_AI_PROJECT_CLIENT="false" + +# Prompts +AZURE_CALL_TRANSCRIPT_SYSTEM_PROMPT="You are an assistant who supports wealth advisors in preparing for client meetings. \n You have access to the client’s past meeting call transcripts. \n When answering questions, especially summary requests, provide a detailed and structured response that includes key topics, concerns, decisions, and trends. \n If no data is available, state 'No relevant data found for previous meetings." +AZURE_OPENAI_STREAM_TEXT_SYSTEM_PROMPT="You are a helpful assistant to a Wealth Advisor. \n The currently selected client's name is '{SelectedClientName}', and any case-insensitive or partial mention should be understood as referring to this client.\n If no name is provided, assume the question is about '{SelectedClientName}'.\n If the query references a different client or includes comparative terms like 'compare' or 'other client', please respond with: 'Please only ask questions about the selected client or select another client.'\n Otherwise, provide thorough answers using only data from SQL or call transcripts. \n If no data is found, please respond with 'No data found for that client.' Remove any client identifiers from the final response." +AZURE_SQL_SYSTEM_PROMPT="Generate a valid T-SQL query to find {query} for tables and columns provided below:\n 1. Table: Clients\n Columns: ClientId, Client, Email, Occupation, MaritalStatus, Dependents\n 2. Table: InvestmentGoals\n Columns: ClientId, InvestmentGoal\n 3. Table: Assets\n Columns: ClientId, AssetDate, Investment, ROI, Revenue, AssetType\n 4. Table: ClientSummaries\n Columns: ClientId, ClientSummary\n 5. Table: InvestmentGoalsDetails\n Columns: ClientId, InvestmentGoal, TargetAmount, Contribution\n 6. Table: Retirement\n Columns: ClientId, StatusDate, RetirementGoalProgress, EducationGoalProgress\n 7. Table: ClientMeetings\n Columns: ClientId, ConversationId, Title, StartTime, EndTime, Advisor, ClientEmail\n Always use the Investment column from the Assets table as the value.\n Assets table has snapshots of values by date. Do not add numbers across different dates for total values.\n Do not use client name in filters.\n Do not include assets values unless asked for.\n ALWAYS use ClientId = {clientid} in the query filter.\n ALWAYS select Client Name (Column: Client) in the query.\n Query filters are IMPORTANT. Add filters like AssetType, AssetDate, etc. if needed.\n Only return the generated SQL query. Do not return anything else." + +# Misc +APPINSIGHTS_INSTRUMENTATIONKEY= +AUTH_ENABLED="false" +USE_INTERNAL_STREAM="True" \ No newline at end of file diff --git a/src/App/app.py b/src/App/app.py index 4c9357573..b1559eb25 100644 --- a/src/App/app.py +++ b/src/App/app.py @@ -6,8 +6,6 @@ import uuid from types import SimpleNamespace -import httpx -import requests from azure.identity import DefaultAzureCredential, get_bearer_token_provider from dotenv import load_dotenv @@ -26,12 +24,9 @@ from backend.auth.auth_utils import get_authenticated_user_details, get_tenantid from backend.history.cosmosdbservice import CosmosConversationClient from backend.utils import ( - convert_to_pf_format, - format_as_ndjson, - format_pf_non_streaming_response, format_stream_response, generateFilterString, - parse_multi_columns, + parse_multi_columns ) from db import get_connection from db import dict_cursor @@ -123,9 +118,6 @@ async def assets(path): # On Your Data Settings DATASOURCE_TYPE = os.environ.get("DATASOURCE_TYPE", "AzureCognitiveSearch") -SEARCH_TOP_K = os.environ.get("SEARCH_TOP_K", 5) -SEARCH_STRICTNESS = os.environ.get("SEARCH_STRICTNESS", 3) -SEARCH_ENABLE_IN_DOMAIN = os.environ.get("SEARCH_ENABLE_IN_DOMAIN", "true") # ACS Integration Settings AZURE_SEARCH_SERVICE = os.environ.get("AZURE_SEARCH_SERVICE") @@ -137,9 +129,9 @@ async def assets(path): AZURE_SEARCH_SEMANTIC_SEARCH_CONFIG = os.environ.get( "AZURE_SEARCH_SEMANTIC_SEARCH_CONFIG", "default" ) -AZURE_SEARCH_TOP_K = os.environ.get("AZURE_SEARCH_TOP_K", SEARCH_TOP_K) +AZURE_SEARCH_TOP_K = os.environ.get("AZURE_SEARCH_TOP_K", 5) AZURE_SEARCH_ENABLE_IN_DOMAIN = os.environ.get( - "AZURE_SEARCH_ENABLE_IN_DOMAIN", SEARCH_ENABLE_IN_DOMAIN + "AZURE_SEARCH_ENABLE_IN_DOMAIN", "true" ) AZURE_SEARCH_CONTENT_COLUMNS = os.environ.get("AZURE_SEARCH_CONTENT_COLUMNS") AZURE_SEARCH_FILENAME_COLUMN = os.environ.get("AZURE_SEARCH_FILENAME_COLUMN") @@ -150,7 +142,7 @@ async def assets(path): AZURE_SEARCH_PERMITTED_GROUPS_COLUMN = os.environ.get( "AZURE_SEARCH_PERMITTED_GROUPS_COLUMN" ) -AZURE_SEARCH_STRICTNESS = os.environ.get("AZURE_SEARCH_STRICTNESS", SEARCH_STRICTNESS) +AZURE_SEARCH_STRICTNESS = os.environ.get("AZURE_SEARCH_STRICTNESS", 3) # AOAI Integration Settings AZURE_OPENAI_RESOURCE = os.environ.get("AZURE_OPENAI_RESOURCE") @@ -170,49 +162,10 @@ async def assets(path): MINIMUM_SUPPORTED_AZURE_OPENAI_PREVIEW_API_VERSION, ) AZURE_OPENAI_STREAM = os.environ.get("AZURE_OPENAI_STREAM", "true") -AZURE_OPENAI_MODEL_NAME = os.environ.get( - "AZURE_OPENAI_MODEL_NAME", "gpt-35-turbo-16k" -) # Name of the model, e.g. 'gpt-35-turbo-16k' or 'gpt-4' AZURE_OPENAI_EMBEDDING_ENDPOINT = os.environ.get("AZURE_OPENAI_EMBEDDING_ENDPOINT") AZURE_OPENAI_EMBEDDING_KEY = os.environ.get("AZURE_OPENAI_EMBEDDING_KEY") AZURE_OPENAI_EMBEDDING_NAME = os.environ.get("AZURE_OPENAI_EMBEDDING_NAME", "") -# CosmosDB Mongo vcore vector db Settings -AZURE_COSMOSDB_MONGO_VCORE_CONNECTION_STRING = os.environ.get( - "AZURE_COSMOSDB_MONGO_VCORE_CONNECTION_STRING" -) # This has to be secure string -AZURE_COSMOSDB_MONGO_VCORE_DATABASE = os.environ.get( - "AZURE_COSMOSDB_MONGO_VCORE_DATABASE" -) -AZURE_COSMOSDB_MONGO_VCORE_CONTAINER = os.environ.get( - "AZURE_COSMOSDB_MONGO_VCORE_CONTAINER" -) -AZURE_COSMOSDB_MONGO_VCORE_INDEX = os.environ.get("AZURE_COSMOSDB_MONGO_VCORE_INDEX") -AZURE_COSMOSDB_MONGO_VCORE_TOP_K = os.environ.get( - "AZURE_COSMOSDB_MONGO_VCORE_TOP_K", AZURE_SEARCH_TOP_K -) -AZURE_COSMOSDB_MONGO_VCORE_STRICTNESS = os.environ.get( - "AZURE_COSMOSDB_MONGO_VCORE_STRICTNESS", AZURE_SEARCH_STRICTNESS -) -AZURE_COSMOSDB_MONGO_VCORE_ENABLE_IN_DOMAIN = os.environ.get( - "AZURE_COSMOSDB_MONGO_VCORE_ENABLE_IN_DOMAIN", AZURE_SEARCH_ENABLE_IN_DOMAIN -) -AZURE_COSMOSDB_MONGO_VCORE_CONTENT_COLUMNS = os.environ.get( - "AZURE_COSMOSDB_MONGO_VCORE_CONTENT_COLUMNS", "" -) -AZURE_COSMOSDB_MONGO_VCORE_FILENAME_COLUMN = os.environ.get( - "AZURE_COSMOSDB_MONGO_VCORE_FILENAME_COLUMN" -) -AZURE_COSMOSDB_MONGO_VCORE_TITLE_COLUMN = os.environ.get( - "AZURE_COSMOSDB_MONGO_VCORE_TITLE_COLUMN" -) -AZURE_COSMOSDB_MONGO_VCORE_URL_COLUMN = os.environ.get( - "AZURE_COSMOSDB_MONGO_VCORE_URL_COLUMN" -) -AZURE_COSMOSDB_MONGO_VCORE_VECTOR_COLUMNS = os.environ.get( - "AZURE_COSMOSDB_MONGO_VCORE_VECTOR_COLUMNS" -) - SHOULD_STREAM = True if AZURE_OPENAI_STREAM.lower() == "true" else False # Chat History CosmosDB Integration Settings @@ -225,78 +178,7 @@ async def assets(path): AZURE_COSMOSDB_ENABLE_FEEDBACK = ( os.environ.get("AZURE_COSMOSDB_ENABLE_FEEDBACK", "false").lower() == "true" ) - -# Elasticsearch Integration Settings -ELASTICSEARCH_ENDPOINT = os.environ.get("ELASTICSEARCH_ENDPOINT") -ELASTICSEARCH_ENCODED_API_KEY = os.environ.get("ELASTICSEARCH_ENCODED_API_KEY") -ELASTICSEARCH_INDEX = os.environ.get("ELASTICSEARCH_INDEX") -ELASTICSEARCH_QUERY_TYPE = os.environ.get("ELASTICSEARCH_QUERY_TYPE", "simple") -ELASTICSEARCH_TOP_K = os.environ.get("ELASTICSEARCH_TOP_K", SEARCH_TOP_K) -ELASTICSEARCH_ENABLE_IN_DOMAIN = os.environ.get( - "ELASTICSEARCH_ENABLE_IN_DOMAIN", SEARCH_ENABLE_IN_DOMAIN -) -ELASTICSEARCH_CONTENT_COLUMNS = os.environ.get("ELASTICSEARCH_CONTENT_COLUMNS") -ELASTICSEARCH_FILENAME_COLUMN = os.environ.get("ELASTICSEARCH_FILENAME_COLUMN") -ELASTICSEARCH_TITLE_COLUMN = os.environ.get("ELASTICSEARCH_TITLE_COLUMN") -ELASTICSEARCH_URL_COLUMN = os.environ.get("ELASTICSEARCH_URL_COLUMN") -ELASTICSEARCH_VECTOR_COLUMNS = os.environ.get("ELASTICSEARCH_VECTOR_COLUMNS") -ELASTICSEARCH_STRICTNESS = os.environ.get("ELASTICSEARCH_STRICTNESS", SEARCH_STRICTNESS) -ELASTICSEARCH_EMBEDDING_MODEL_ID = os.environ.get("ELASTICSEARCH_EMBEDDING_MODEL_ID") - -# Pinecone Integration Settings -PINECONE_ENVIRONMENT = os.environ.get("PINECONE_ENVIRONMENT") -PINECONE_API_KEY = os.environ.get("PINECONE_API_KEY") -PINECONE_INDEX_NAME = os.environ.get("PINECONE_INDEX_NAME") -PINECONE_TOP_K = os.environ.get("PINECONE_TOP_K", SEARCH_TOP_K) -PINECONE_STRICTNESS = os.environ.get("PINECONE_STRICTNESS", SEARCH_STRICTNESS) -PINECONE_ENABLE_IN_DOMAIN = os.environ.get( - "PINECONE_ENABLE_IN_DOMAIN", SEARCH_ENABLE_IN_DOMAIN -) -PINECONE_CONTENT_COLUMNS = os.environ.get("PINECONE_CONTENT_COLUMNS", "") -PINECONE_FILENAME_COLUMN = os.environ.get("PINECONE_FILENAME_COLUMN") -PINECONE_TITLE_COLUMN = os.environ.get("PINECONE_TITLE_COLUMN") -PINECONE_URL_COLUMN = os.environ.get("PINECONE_URL_COLUMN") -PINECONE_VECTOR_COLUMNS = os.environ.get("PINECONE_VECTOR_COLUMNS") - -# Azure AI MLIndex Integration Settings - for use with MLIndex data assets created in Azure AI Studio -AZURE_MLINDEX_NAME = os.environ.get("AZURE_MLINDEX_NAME") -AZURE_MLINDEX_VERSION = os.environ.get("AZURE_MLINDEX_VERSION") -AZURE_ML_PROJECT_RESOURCE_ID = os.environ.get( - "AZURE_ML_PROJECT_RESOURCE_ID" -) # /subscriptions/{sub ID}/resourceGroups/{rg name}/providers/Microsoft.MachineLearningServices/workspaces/{AML project name} -AZURE_MLINDEX_TOP_K = os.environ.get("AZURE_MLINDEX_TOP_K", SEARCH_TOP_K) -AZURE_MLINDEX_STRICTNESS = os.environ.get("AZURE_MLINDEX_STRICTNESS", SEARCH_STRICTNESS) -AZURE_MLINDEX_ENABLE_IN_DOMAIN = os.environ.get( - "AZURE_MLINDEX_ENABLE_IN_DOMAIN", SEARCH_ENABLE_IN_DOMAIN -) -AZURE_MLINDEX_CONTENT_COLUMNS = os.environ.get("AZURE_MLINDEX_CONTENT_COLUMNS", "") -AZURE_MLINDEX_FILENAME_COLUMN = os.environ.get("AZURE_MLINDEX_FILENAME_COLUMN") -AZURE_MLINDEX_TITLE_COLUMN = os.environ.get("AZURE_MLINDEX_TITLE_COLUMN") -AZURE_MLINDEX_URL_COLUMN = os.environ.get("AZURE_MLINDEX_URL_COLUMN") -AZURE_MLINDEX_VECTOR_COLUMNS = os.environ.get("AZURE_MLINDEX_VECTOR_COLUMNS") -AZURE_MLINDEX_QUERY_TYPE = os.environ.get("AZURE_MLINDEX_QUERY_TYPE") -# Promptflow Integration Settings -USE_PROMPTFLOW = os.environ.get("USE_PROMPTFLOW", "false").lower() == "true" -PROMPTFLOW_ENDPOINT = os.environ.get("PROMPTFLOW_ENDPOINT") -PROMPTFLOW_API_KEY = os.environ.get("PROMPTFLOW_API_KEY") -PROMPTFLOW_RESPONSE_TIMEOUT = os.environ.get("PROMPTFLOW_RESPONSE_TIMEOUT", 30.0) -# default request and response field names are input -> 'query' and output -> 'reply' -PROMPTFLOW_REQUEST_FIELD_NAME = os.environ.get("PROMPTFLOW_REQUEST_FIELD_NAME", "query") -PROMPTFLOW_RESPONSE_FIELD_NAME = os.environ.get( - "PROMPTFLOW_RESPONSE_FIELD_NAME", "reply" -) -PROMPTFLOW_CITATIONS_FIELD_NAME = os.environ.get( - "PROMPTFLOW_CITATIONS_FIELD_NAME", "documents" -) USE_INTERNAL_STREAM = os.environ.get("USE_INTERNAL_STREAM", "false").lower() == "true" -FUNCTIONAPP_RESPONSE_FIELD_NAME = os.environ.get( - "FUNCTIONAPP_RESPONSE_FIELD_NAME", "reply" -) -FUNCTIONAPP_CITATIONS_FIELD_NAME = os.environ.get( - "FUNCTIONAPP_CITATIONS_FIELD_NAME", "documents" -) -AZUREFUNCTION_ENDPOINT = os.environ.get("AZUREFUNCTION_ENDPOINT") -STREAMING_AZUREFUNCTION_ENDPOINT = os.environ.get("STREAMING_AZUREFUNCTION_ENDPOINT") # Frontend Settings via Environment Variables AUTH_ENABLED = os.environ.get("AUTH_ENABLED", "true").lower() == "true" CHAT_HISTORY_ENABLED = ( @@ -321,7 +203,7 @@ async def assets(path): # Enable Microsoft Defender for Cloud Integration MS_DEFENDER_ENABLED = os.environ.get("MS_DEFENDER_ENABLED", "false").lower() == "true" -VITE_POWERBI_EMBED_URL = os.environ.get("VITE_POWERBI_EMBED_URL") +# VITE_POWERBI_EMBED_URL = os.environ.get("VITE_POWERBI_EMBED_URL") def should_use_data(): @@ -331,31 +213,6 @@ def should_use_data(): logging.debug("Using Azure Cognitive Search") return True - if ( - AZURE_COSMOSDB_MONGO_VCORE_DATABASE - and AZURE_COSMOSDB_MONGO_VCORE_CONTAINER - and AZURE_COSMOSDB_MONGO_VCORE_INDEX - and AZURE_COSMOSDB_MONGO_VCORE_CONNECTION_STRING - ): - DATASOURCE_TYPE = "AzureCosmosDB" - logging.debug("Using Azure CosmosDB Mongo vcore") - return True - - if ELASTICSEARCH_ENDPOINT and ELASTICSEARCH_ENCODED_API_KEY and ELASTICSEARCH_INDEX: - DATASOURCE_TYPE = "Elasticsearch" - logging.debug("Using Elasticsearch") - return True - - if PINECONE_ENVIRONMENT and PINECONE_API_KEY and PINECONE_INDEX_NAME: - DATASOURCE_TYPE = "Pinecone" - logging.debug("Using Pinecone") - return True - - if AZURE_MLINDEX_NAME and AZURE_MLINDEX_VERSION and AZURE_ML_PROJECT_RESOURCE_ID: - DATASOURCE_TYPE = "AzureMLIndex" - logging.debug("Using Azure ML Index") - return True - return False @@ -544,7 +401,7 @@ def get_configured_data_source(): True if AZURE_SEARCH_ENABLE_IN_DOMAIN.lower() == "true" else False ), "top_n_documents": ( - int(AZURE_SEARCH_TOP_K) if AZURE_SEARCH_TOP_K else int(SEARCH_TOP_K) + int(AZURE_SEARCH_TOP_K) ), "query_type": query_type, "semantic_configuration": ( @@ -556,224 +413,7 @@ def get_configured_data_source(): "filter": filter, "strictness": ( int(AZURE_SEARCH_STRICTNESS) - if AZURE_SEARCH_STRICTNESS - else int(SEARCH_STRICTNESS) - ), - }, - } - elif DATASOURCE_TYPE == "AzureCosmosDB": - query_type = "vector" - track_event_if_configured("datasource_selected", {"type": "AzureCosmosDB"}) - - data_source = { - "type": "azure_cosmos_db", - "parameters": { - "authentication": { - "type": "connection_string", - "connection_string": AZURE_COSMOSDB_MONGO_VCORE_CONNECTION_STRING, - }, - "index_name": AZURE_COSMOSDB_MONGO_VCORE_INDEX, - "database_name": AZURE_COSMOSDB_MONGO_VCORE_DATABASE, - "container_name": AZURE_COSMOSDB_MONGO_VCORE_CONTAINER, - "fields_mapping": { - "content_fields": ( - parse_multi_columns(AZURE_COSMOSDB_MONGO_VCORE_CONTENT_COLUMNS) - if AZURE_COSMOSDB_MONGO_VCORE_CONTENT_COLUMNS - else [] - ), - "title_field": ( - AZURE_COSMOSDB_MONGO_VCORE_TITLE_COLUMN - if AZURE_COSMOSDB_MONGO_VCORE_TITLE_COLUMN - else None - ), - "url_field": ( - AZURE_COSMOSDB_MONGO_VCORE_URL_COLUMN - if AZURE_COSMOSDB_MONGO_VCORE_URL_COLUMN - else None - ), - "filepath_field": ( - AZURE_COSMOSDB_MONGO_VCORE_FILENAME_COLUMN - if AZURE_COSMOSDB_MONGO_VCORE_FILENAME_COLUMN - else None - ), - "vector_fields": ( - parse_multi_columns(AZURE_COSMOSDB_MONGO_VCORE_VECTOR_COLUMNS) - if AZURE_COSMOSDB_MONGO_VCORE_VECTOR_COLUMNS - else [] - ), - }, - "in_scope": ( - True - if AZURE_COSMOSDB_MONGO_VCORE_ENABLE_IN_DOMAIN.lower() == "true" - else False ), - "top_n_documents": ( - int(AZURE_COSMOSDB_MONGO_VCORE_TOP_K) - if AZURE_COSMOSDB_MONGO_VCORE_TOP_K - else int(SEARCH_TOP_K) - ), - "strictness": ( - int(AZURE_COSMOSDB_MONGO_VCORE_STRICTNESS) - if AZURE_COSMOSDB_MONGO_VCORE_STRICTNESS - else int(SEARCH_STRICTNESS) - ), - "query_type": query_type, - "role_information": AZURE_OPENAI_SYSTEM_MESSAGE, - }, - } - elif DATASOURCE_TYPE == "Elasticsearch": - track_event_if_configured("datasource_selected", {"type": "Elasticsearch"}) - if ELASTICSEARCH_QUERY_TYPE: - query_type = ELASTICSEARCH_QUERY_TYPE - track_event_if_configured("query_type_determined", {"query_type": query_type}) - - data_source = { - "type": "elasticsearch", - "parameters": { - "endpoint": ELASTICSEARCH_ENDPOINT, - "authentication": { - "type": "encoded_api_key", - "encoded_api_key": ELASTICSEARCH_ENCODED_API_KEY, - }, - "index_name": ELASTICSEARCH_INDEX, - "fields_mapping": { - "content_fields": ( - parse_multi_columns(ELASTICSEARCH_CONTENT_COLUMNS) - if ELASTICSEARCH_CONTENT_COLUMNS - else [] - ), - "title_field": ( - ELASTICSEARCH_TITLE_COLUMN - if ELASTICSEARCH_TITLE_COLUMN - else None - ), - "url_field": ( - ELASTICSEARCH_URL_COLUMN if ELASTICSEARCH_URL_COLUMN else None - ), - "filepath_field": ( - ELASTICSEARCH_FILENAME_COLUMN - if ELASTICSEARCH_FILENAME_COLUMN - else None - ), - "vector_fields": ( - parse_multi_columns(ELASTICSEARCH_VECTOR_COLUMNS) - if ELASTICSEARCH_VECTOR_COLUMNS - else [] - ), - }, - "in_scope": ( - True if ELASTICSEARCH_ENABLE_IN_DOMAIN.lower() == "true" else False - ), - "top_n_documents": ( - int(ELASTICSEARCH_TOP_K) - if ELASTICSEARCH_TOP_K - else int(SEARCH_TOP_K) - ), - "query_type": query_type, - "role_information": AZURE_OPENAI_SYSTEM_MESSAGE, - "strictness": ( - int(ELASTICSEARCH_STRICTNESS) - if ELASTICSEARCH_STRICTNESS - else int(SEARCH_STRICTNESS) - ), - }, - } - elif DATASOURCE_TYPE == "AzureMLIndex": - track_event_if_configured("datasource_selected", {"type": "AzureMLIndex"}) - if AZURE_MLINDEX_QUERY_TYPE: - query_type = AZURE_MLINDEX_QUERY_TYPE - track_event_if_configured("query_type_determined", {"query_type": query_type}) - - data_source = { - "type": "azure_ml_index", - "parameters": { - "name": AZURE_MLINDEX_NAME, - "version": AZURE_MLINDEX_VERSION, - "project_resource_id": AZURE_ML_PROJECT_RESOURCE_ID, - "fieldsMapping": { - "content_fields": ( - parse_multi_columns(AZURE_MLINDEX_CONTENT_COLUMNS) - if AZURE_MLINDEX_CONTENT_COLUMNS - else [] - ), - "title_field": ( - AZURE_MLINDEX_TITLE_COLUMN - if AZURE_MLINDEX_TITLE_COLUMN - else None - ), - "url_field": ( - AZURE_MLINDEX_URL_COLUMN if AZURE_MLINDEX_URL_COLUMN else None - ), - "filepath_field": ( - AZURE_MLINDEX_FILENAME_COLUMN - if AZURE_MLINDEX_FILENAME_COLUMN - else None - ), - "vector_fields": ( - parse_multi_columns(AZURE_MLINDEX_VECTOR_COLUMNS) - if AZURE_MLINDEX_VECTOR_COLUMNS - else [] - ), - }, - "in_scope": ( - True if AZURE_MLINDEX_ENABLE_IN_DOMAIN.lower() == "true" else False - ), - "top_n_documents": ( - int(AZURE_MLINDEX_TOP_K) - if AZURE_MLINDEX_TOP_K - else int(SEARCH_TOP_K) - ), - "query_type": query_type, - "role_information": AZURE_OPENAI_SYSTEM_MESSAGE, - "strictness": ( - int(AZURE_MLINDEX_STRICTNESS) - if AZURE_MLINDEX_STRICTNESS - else int(SEARCH_STRICTNESS) - ), - }, - } - elif DATASOURCE_TYPE == "Pinecone": - query_type = "vector" - track_event_if_configured("datasource_selected", {"type": "Pinecone"}) - - data_source = { - "type": "pinecone", - "parameters": { - "environment": PINECONE_ENVIRONMENT, - "authentication": {"type": "api_key", "key": PINECONE_API_KEY}, - "index_name": PINECONE_INDEX_NAME, - "fields_mapping": { - "content_fields": ( - parse_multi_columns(PINECONE_CONTENT_COLUMNS) - if PINECONE_CONTENT_COLUMNS - else [] - ), - "title_field": ( - PINECONE_TITLE_COLUMN if PINECONE_TITLE_COLUMN else None - ), - "url_field": PINECONE_URL_COLUMN if PINECONE_URL_COLUMN else None, - "filepath_field": ( - PINECONE_FILENAME_COLUMN if PINECONE_FILENAME_COLUMN else None - ), - "vector_fields": ( - parse_multi_columns(PINECONE_VECTOR_COLUMNS) - if PINECONE_VECTOR_COLUMNS - else [] - ), - }, - "in_scope": ( - True if PINECONE_ENABLE_IN_DOMAIN.lower() == "true" else False - ), - "top_n_documents": ( - int(PINECONE_TOP_K) if PINECONE_TOP_K else int(SEARCH_TOP_K) - ), - "strictness": ( - int(PINECONE_STRICTNESS) - if PINECONE_STRICTNESS - else int(SEARCH_STRICTNESS) - ), - "query_type": query_type, - "role_information": AZURE_OPENAI_SYSTEM_MESSAGE, }, } else: @@ -798,11 +438,6 @@ def get_configured_data_source(): "key": AZURE_OPENAI_EMBEDDING_KEY, }, } - elif DATASOURCE_TYPE == "Elasticsearch" and ELASTICSEARCH_EMBEDDING_MODEL_ID: - embeddingDependency = { - "type": "model_id", - "model_id": ELASTICSEARCH_EMBEDDING_MODEL_ID, - } else: track_event_if_configured("embedding_dependency_missing", { "datasource_type": DATASOURCE_TYPE, @@ -909,45 +544,6 @@ def prepare_model_args(request_body, request_headers): return model_args -async def promptflow_request(request): - track_event_if_configured("promptflow_request_start", {}) - try: - headers = { - "Content-Type": "application/json", - "Authorization": f"Bearer {PROMPTFLOW_API_KEY}", - } - # Adding timeout for scenarios where response takes longer to come back - logging.debug(f"Setting timeout to {PROMPTFLOW_RESPONSE_TIMEOUT}") - async with httpx.AsyncClient( - timeout=float(PROMPTFLOW_RESPONSE_TIMEOUT) - ) as client: - pf_formatted_obj = convert_to_pf_format( - request, PROMPTFLOW_REQUEST_FIELD_NAME, PROMPTFLOW_RESPONSE_FIELD_NAME - ) - # NOTE: This only support question and chat_history parameters - # If you need to add more parameters, you need to modify the request body - response = await client.post( - PROMPTFLOW_ENDPOINT, - json={ - f"{PROMPTFLOW_REQUEST_FIELD_NAME}": pf_formatted_obj[-1]["inputs"][ - PROMPTFLOW_REQUEST_FIELD_NAME - ], - "chat_history": pf_formatted_obj[:-1], - }, - headers=headers, - ) - resp = response.json() - resp["id"] = request["messages"][-1]["id"] - track_event_if_configured("promptflow_request_success", {}) - return resp - except Exception as e: - span = trace.get_current_span() - if span is not None: - span.record_exception(e) - span.set_status(Status(StatusCode.ERROR, str(e))) - logging.error(f"An error occurred while making promptflow_request: {e}") - - async def send_chat_request(request_body, request_headers): track_event_if_configured("send_chat_request_start", {}) filtered_messages = [] @@ -980,90 +576,10 @@ async def send_chat_request(request_body, request_headers): return response, apim_request_id -async def complete_chat_request(request_body, request_headers): - track_event_if_configured("complete_chat_request_start", {}) - if USE_PROMPTFLOW and PROMPTFLOW_ENDPOINT and PROMPTFLOW_API_KEY: - response = await promptflow_request(request_body) - history_metadata = request_body.get("history_metadata", {}) - return format_pf_non_streaming_response( - response, - history_metadata, - PROMPTFLOW_RESPONSE_FIELD_NAME, - PROMPTFLOW_CITATIONS_FIELD_NAME, - ) - elif USE_INTERNAL_STREAM: - track_event_if_configured("internal_stream_selected", {}) - request_body = await request.get_json() - client_id = request_body.get("client_id") - print(request_body) - - if client_id is None: - return jsonify({"error": "No client ID provided"}), 400 - # client_id = '10005' - print("Client ID in complete_chat_request: ", client_id) - # answer = "Sample response from Azure Function" - # Construct the URL of your Azure Function endpoint - # function_url = STREAMING_AZUREFUNCTION_ENDPOINT - # request_headers = { - # "Content-Type": "application/json", - # # 'Authorization': 'Bearer YOUR_TOKEN_HERE' # if applicable - # } - # print(request_body.get("messages")[-1].get("content")) - # print(request_body) - - query = request_body.get("messages")[-1].get("content") - - print("Selected ClientId:", client_id) - # print("Selected ClientName:", selected_client_name) - - # endpoint = STREAMING_AZUREFUNCTION_ENDPOINT + '?query=' + query + ' - for Client ' + selected_client_name + ':::' + selected_client_id - endpoint = ( - STREAMING_AZUREFUNCTION_ENDPOINT + "?query=" + query + ":::" + client_id - ) - - print("Endpoint: ", endpoint) - query_response = "" - try: - with requests.get(endpoint, stream=True) as r: - for line in r.iter_lines(chunk_size=10): - # query_response += line.decode('utf-8') - query_response = query_response + "\n" + line.decode("utf-8") - # print(line.decode('utf-8')) - except Exception as e: - print(format_as_ndjson({"error" + str(e)})) - - # print("query_response: " + query_response) - - history_metadata = request_body.get("history_metadata", {}) - response = { - "id": "", - "model": "", - "created": 0, - "object": "", - "choices": [{"messages": []}], - "apim-request-id": "", - "history_metadata": history_metadata, - } - - response["id"] = str(uuid.uuid4()) - response["model"] = AZURE_OPENAI_MODEL_NAME - response["created"] = int(time.time()) - response["object"] = "extensions.chat.completion.chunk" - # response["apim-request-id"] = headers.get("apim-request-id") - response["choices"][0]["messages"].append( - {"role": "assistant", "content": query_response} - ) - - track_event_if_configured("complete_chat_request_success", {"client_id": client_id}) - - return response - - async def stream_chat_request(request_body, request_headers): track_event_if_configured("stream_chat_request_start", {}) if USE_INTERNAL_STREAM: history_metadata = request_body.get("history_metadata", {}) - # function_url = STREAMING_AZUREFUNCTION_ENDPOINT apim_request_id = "" client_id = request_body.get("client_id") @@ -1085,7 +601,7 @@ async def generate(): completionChunk = { "id": chunk_id, - "model": AZURE_OPENAI_MODEL_NAME, + "model": AZURE_OPENAI_MODEL, "created": created_time, "object": "extensions.chat.completion.chunk", "choices": [ @@ -1131,7 +647,6 @@ async def generate(): async def conversation_internal(request_body, request_headers): track_event_if_configured("conversation_internal_start", { "streaming": SHOULD_STREAM, - "promptflow": USE_PROMPTFLOW, "internal_stream": USE_INTERNAL_STREAM }) try: @@ -1141,10 +656,6 @@ async def conversation_internal(request_body, request_headers): # response.timeout = None # response.mimetype = "application/json-lines" # return response - else: - result = await complete_chat_request(request_body, request_headers) - track_event_if_configured("conversation_internal_success", {}) - return jsonify(result) except Exception as ex: span = trace.get_current_span() diff --git a/src/App/backend/chat_logic_handler.py b/src/App/backend/chat_logic_handler.py index f848a011f..8d04a2384 100644 --- a/src/App/backend/chat_logic_handler.py +++ b/src/App/backend/chat_logic_handler.py @@ -17,10 +17,10 @@ # -------------------------- endpoint = os.environ.get("AZURE_OPENAI_ENDPOINT") api_key = os.environ.get("AZURE_OPENAI_KEY") -api_version = os.environ.get("OPENAI_API_VERSION") +api_version = os.environ.get("AZURE_OPENAI_PREVIEW_API_VERSION") deployment = os.environ.get("AZURE_OPENAI_MODEL") search_endpoint = os.environ.get("AZURE_AI_SEARCH_ENDPOINT") -search_key = os.environ.get("AZURE_AI_SEARCH_API_KEY") +search_key = os.environ.get("AZURE_SEARCH_KEY") project_connection_string = os.environ.get("AZURE_AI_PROJECT_CONN_STRING") use_ai_project_client = os.environ.get("USE_AI_PROJECT_CLIENT", "false").lower() == "true" diff --git a/src/App/tests/backend/test_utils.py b/src/App/tests/backend/test_utils.py index 1585cd7fb..cf6c293e3 100644 --- a/src/App/tests/backend/test_utils.py +++ b/src/App/tests/backend/test_utils.py @@ -37,7 +37,7 @@ def test_parse_multi_columns(input_str, expected): assert parse_multi_columns(input_str) == expected -@patch("app.requests.get") +@patch("backend.utils.requests.get") def test_fetch_user_groups(mock_get): mock_response = MagicMock() mock_response.status_code = 200 diff --git a/src/App/tests/test_app.py b/src/App/tests/test_app.py index bf82ccf3a..ff0ef42c2 100644 --- a/src/App/tests/test_app.py +++ b/src/App/tests/test_app.py @@ -1218,15 +1218,12 @@ async def test_conversation_route(client): with patch("app.stream_chat_request", new_callable=AsyncMock) as mock_stream: mock_stream.return_value = ["chunk1", "chunk2"] - with patch( - "app.complete_chat_request", new_callable=AsyncMock - ) as mock_complete: - mock_complete.return_value = {"response": "test response"} - response = await client.post( - "/conversation", json=request_body, headers=request_headers - ) - assert response.status_code == 200 + response = await client.post( + "/conversation", json=request_body, headers=request_headers + ) + + assert response.status_code == 200 @pytest.mark.asyncio From 7ae9718f9b5d92d1dcfbcb5288cb03fd20c65cdb Mon Sep 17 00:00:00 2001 From: Vamshi-Microsoft Date: Tue, 3 Jun 2025 06:55:11 +0000 Subject: [PATCH 11/19] EXP environment changes for Existing Fabric workspace --- docs/FabricDeployment.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/FabricDeployment.md b/docs/FabricDeployment.md index 5e95f4b5f..232a2fd89 100644 --- a/docs/FabricDeployment.md +++ b/docs/FabricDeployment.md @@ -1,5 +1,8 @@ ## Fabric Deployment ## Step 1: Create Fabric workspace + +ℹ️ Note: If you already have an existing Microsoft Fabric Workspace, you can **skip this step** and proceed to Step 2. To retrieve an existing Workspace ID, check **Point 5 below**. + 1. Navigate to ([Fabric Workspace](https://app.fabric.microsoft.com/)) 2. Click on Workspaces from left Navigation 3. Click on + New Workspace @@ -19,7 +22,7 @@ - ```cd ./Build-your-own-copilot-Solution-Accelerator/infra/scripts/fabric_scripts``` - ```sh ./run_fabric_items_scripts.sh keyvault_param workspaceid_param solutionprefix_param``` 1. keyvault_param - the name of the keyvault that was created in Step 1 - 2. workspaceid_param - the workspaceid created in Step 2 + 2. workspaceid_param - Existing Workspaceid or workspaceid created in Step 2 3. solutionprefix_param - prefix used to append to lakehouse upon creation 4. Get Fabric Lakehouse connection details: 5. Once deployment is complete, navigate to Fabric Workspace From 17f48fa982ceaeae1698a29c0f4fb5d73887d15f Mon Sep 17 00:00:00 2001 From: Vamshi-Microsoft Date: Tue, 3 Jun 2025 15:24:44 +0530 Subject: [PATCH 12/19] Updated Heading --- docs/FabricDeployment.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/FabricDeployment.md b/docs/FabricDeployment.md index 232a2fd89..4d85b2d82 100644 --- a/docs/FabricDeployment.md +++ b/docs/FabricDeployment.md @@ -1,5 +1,5 @@ ## Fabric Deployment -## Step 1: Create Fabric workspace +## Step 1: Create or Use an Existing Microsoft Fabric Workspace ℹ️ Note: If you already have an existing Microsoft Fabric Workspace, you can **skip this step** and proceed to Step 2. To retrieve an existing Workspace ID, check **Point 5 below**. From 562533d556c7e9c5b932aae218a808e4feb6a1b3 Mon Sep 17 00:00:00 2001 From: Vamshi-Microsoft Date: Wed, 4 Jun 2025 07:06:03 +0000 Subject: [PATCH 13/19] To reuse Log Analytics across subscriptions --- docs/CustomizingAzdParameters.md | 2 +- infra/deploy_ai_foundry.bicep | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/CustomizingAzdParameters.md b/docs/CustomizingAzdParameters.md index fc02f6d17..d2ec2fef9 100644 --- a/docs/CustomizingAzdParameters.md +++ b/docs/CustomizingAzdParameters.md @@ -44,5 +44,5 @@ azd env set AZURE_ENV_EMBEDDING_MODEL_CAPACITY 80 Set the Log Analytics Workspace Id if you need to reuse the existing workspace which is already existing ```shell -azd env set AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID '' +azd env set AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID '/subscriptions//resourceGroups//providers/Microsoft.OperationalInsights/workspaces/' ``` \ No newline at end of file diff --git a/infra/deploy_ai_foundry.bicep b/infra/deploy_ai_foundry.bicep index ef2e81fc7..4ba89548e 100644 --- a/infra/deploy_ai_foundry.bicep +++ b/infra/deploy_ai_foundry.bicep @@ -56,12 +56,13 @@ resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = { } var useExisting = !empty(existingLogAnalyticsWorkspaceId) +var existingLawSubscription = useExisting ? split(existingLogAnalyticsWorkspaceId, '/')[2] : '' var existingLawResourceGroup = useExisting ? split(existingLogAnalyticsWorkspaceId, '/')[4] : '' var existingLawName = useExisting ? split(existingLogAnalyticsWorkspaceId, '/')[8] : '' resource existingLogAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2023-09-01' existing = if (useExisting) { name: existingLawName - scope: resourceGroup(existingLawResourceGroup) + scope: resourceGroup(existingLawSubscription, existingLawResourceGroup) } resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2023-09-01' = if (!useExisting) { From 1a996f1f2688cf158c2e7becbfead199d2cc901c Mon Sep 17 00:00:00 2001 From: Venkateswarlu Marthula Date: Fri, 6 Jun 2025 07:01:16 +0530 Subject: [PATCH 14/19] automate --- .github/workflows/test_automation.yml | 111 ++++++++++++ tests/e2e-test/.gitignore | 166 ++++++++++++++++++ tests/e2e-test/README.md | 41 +++++ tests/e2e-test/base/__init__.py | 1 + tests/e2e-test/base/base.py | 140 +++++++++++++++ tests/e2e-test/config/constants.py | 21 +++ tests/e2e-test/img.png | Bin 0 -> 85099 bytes tests/e2e-test/img_1.png | Bin 0 -> 62274 bytes tests/e2e-test/pages/__init__.py | 2 + tests/e2e-test/pages/homePage.py | 83 +++++++++ tests/e2e-test/pages/loginPage.py | 43 +++++ tests/e2e-test/requirements.txt | 3 + tests/e2e-test/sample_dotenv_file.txt | 6 + tests/e2e-test/tests/__init__.py | 0 tests/e2e-test/tests/conftest.py | 59 +++++++ .../tests/test_poc_byoc_client_advisor.py | 141 +++++++++++++++ 16 files changed, 817 insertions(+) create mode 100644 .github/workflows/test_automation.yml create mode 100644 tests/e2e-test/.gitignore create mode 100644 tests/e2e-test/README.md create mode 100644 tests/e2e-test/base/__init__.py create mode 100644 tests/e2e-test/base/base.py create mode 100644 tests/e2e-test/config/constants.py create mode 100644 tests/e2e-test/img.png create mode 100644 tests/e2e-test/img_1.png create mode 100644 tests/e2e-test/pages/__init__.py create mode 100644 tests/e2e-test/pages/homePage.py create mode 100644 tests/e2e-test/pages/loginPage.py create mode 100644 tests/e2e-test/requirements.txt create mode 100644 tests/e2e-test/sample_dotenv_file.txt create mode 100644 tests/e2e-test/tests/__init__.py create mode 100644 tests/e2e-test/tests/conftest.py create mode 100644 tests/e2e-test/tests/test_poc_byoc_client_advisor.py diff --git a/.github/workflows/test_automation.yml b/.github/workflows/test_automation.yml new file mode 100644 index 000000000..f49beee8d --- /dev/null +++ b/.github/workflows/test_automation.yml @@ -0,0 +1,111 @@ +name: Test Automation ClientAdvisor + +on: + push: + branches: + - main + - dev + - VE-Automate + # paths: + # - 'byoc-client-advisor/**' + schedule: + - cron: '0 13 * * *' # Runs at 1 PM UTC + workflow_dispatch: + +env: + url: ${{ vars.CLIENT_ADVISOR_URL }} + accelerator_name: "Client Advisor" + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.13' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r byoc-client-advisor/requirements.txt + + - name: Ensure browsers are installed + run: python -m playwright install --with-deps chromium + + - name: Run tests(1) + id: test1 + run: | + xvfb-run pytest --headed --html=report/report.html --self-contained-html + working-directory: byoc-client-advisor + continue-on-error: true + + - name: Sleep for 30 seconds + if: ${{ steps.test1.outcome == 'failure' }} + run: sleep 30s + shell: bash + + - name: Run tests(2) + if: ${{ steps.test1.outcome == 'failure' }} + id: test2 + run: | + xvfb-run pytest --headed --html=report/report.html --self-contained-html + working-directory: byoc-client-advisor + continue-on-error: true + + - name: Sleep for 60 seconds + if: ${{ steps.test2.outcome == 'failure' }} + run: sleep 60s + shell: bash + + - name: Run tests(3) + if: ${{ steps.test2.outcome == 'failure' }} + id: test3 + run: | + xvfb-run pytest --headed --html=report/report.html --self-contained-html + working-directory: byoc-client-advisor + + - name: Upload test report + id: upload_report + uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: test-report + path: byoc-client-advisor/report/* + + - name: Send Notification + if: always() + run: | + RUN_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + REPORT_URL=${{ steps.upload_report.outputs.artifact-url }} + IS_SUCCESS=${{ steps.test1.outcome == 'success' || steps.test2.outcome == 'success' || steps.test3.outcome == 'success' }} + # Construct the email body + if [ "$IS_SUCCESS" = "true" ]; then + EMAIL_BODY=$(cat <Dear Team,

We would like to inform you that the ${{ env.accelerator_name }} Test Automation process has completed successfully.

Run URL: ${RUN_URL} +

Test Report: ${REPORT_URL}

Best regards, + Your Automation Team

", + "subject": "${{ env.accelerator_name }} Test Automation - Success" + } + EOF + ) + else + EMAIL_BODY=$(cat <Dear Team,

We would like to inform you that the ${{ env.accelerator_name }} Test Automation process has encountered an issue and has failed to complete successfully.

Run URL: ${RUN_URL} + ${OUTPUT}

Test Report: ${REPORT_URL}

Please investigate the matter at your earliest convenience.

Best regards, + Your Automation Team

", + "subject": "${{ env.accelerator_name }} Test Automation - Failure" + } + EOF + ) + fi + + # Send the notification + curl -X POST "${{ secrets.EMAILNOTIFICATION_LOGICAPP_URL_TA}}" \ + -H "Content-Type: application/json" \ + -d "$EMAIL_BODY" || echo "Failed to send notification" diff --git a/tests/e2e-test/.gitignore b/tests/e2e-test/.gitignore new file mode 100644 index 000000000..de16f2df0 --- /dev/null +++ b/tests/e2e-test/.gitignore @@ -0,0 +1,166 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ +microsoft/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +.idea/ +archive/ +report/ +screenshots/ diff --git a/tests/e2e-test/README.md b/tests/e2e-test/README.md new file mode 100644 index 000000000..453eb273a --- /dev/null +++ b/tests/e2e-test/README.md @@ -0,0 +1,41 @@ +# Automation Proof Of Concept for BYOc Client Advisor Accelerator + + + +Write end-to-end tests for your web apps with [Playwright](https://github.com/microsoft/playwright-python) and [pytest](https://docs.pytest.org/en/stable/). + +- Support for **all modern browsers** including Chromium, WebKit and Firefox. +- Support for **headless and headed** execution. +- **Built-in fixtures** that provide browser primitives to test functions. + +Pre-Requisites: +- Install Visual Studio Code: Download and Install Visual Studio Code(VSCode). +- Install NodeJS: Download and Install Node JS + +Create and Activate Python Virtual Environment +- From your directory open and run cmd : "python -m venv microsoft" +This will create a virtual environment directory named microsoft inside your current directory +- To enable virtual environment, copy location for "microsoft\Scripts\activate.bat" and run from cmd + + +Installing Playwright Pytest from Virtual Environment +- To install libraries run "pip install -r requirements.txt" +- Install the required browsers "playwright install" + +Run test cases +- To run test cases from your 'tests' folder : "pytest --headed --html=report/report.html" + +Steps need to be followed to enable Access Token and Client Credentials +- Go to App Service from the resource group and select the Access Tokens check box in 'Manage->Authentication' tab +![img.png](img.png) +- Go to Manage->Certificates & secrets tab to generate Client Secret value +![img_1.png](img_1.png) +- Go to Overview tab to get the client id and tenant id. + +Create .env file in project root level with web app url and client credentials +- create a .env file in project root level and add your user_name, pass_word, client_id,client_secret, + tenant_id and url for the resource group. please refer 'sample_dotenv_file.txt' file. + +## Documentation + +See on [playwright.dev](https://playwright.dev/python/docs/test-runners) for examples and more detailed information. \ No newline at end of file diff --git a/tests/e2e-test/base/__init__.py b/tests/e2e-test/base/__init__.py new file mode 100644 index 000000000..cf50d1ccd --- /dev/null +++ b/tests/e2e-test/base/__init__.py @@ -0,0 +1 @@ +from . import base \ No newline at end of file diff --git a/tests/e2e-test/base/base.py b/tests/e2e-test/base/base.py new file mode 100644 index 000000000..e36c7534e --- /dev/null +++ b/tests/e2e-test/base/base.py @@ -0,0 +1,140 @@ +from config.constants import * +import requests +import json +from dotenv import load_dotenv +import os +import re +from datetime import datetime +import uuid + + +class BasePage: + def __init__(self, page): + self.page = page + + def scroll_into_view(self,locator,text): + elements = locator.all() + for element in elements: + client_e = element.text_content() + if client_e == text: + element.scroll_into_view_if_needed() + break + + def select_an_element(self,locator,text): + elements = locator.all() + for element in elements: + clientele = element.text_content() + if clientele == text: + element.click() + break + + def is_visible(self,locator): + locator.is_visible() + + def validate_response_status(self): + load_dotenv() + # client_id = os.getenv('client_id') + # client_secret = os.getenv('client_secret') + # tenant_id = os.getenv('tenant_id') + # token_url = f"https://login.microsoft.com/{tenant_id}/oauth2/v2.0/token" + # The URL of the API endpoint you want to access + url = f"{URL}/history/update" + + # Generate unique IDs for the messages + user_message_id = str(uuid.uuid4()) + assistant_message_id = str(uuid.uuid4()) + conversation_id = str(uuid.uuid4()) + + headers = { + "Content-Type": "application/json", + "Accept": "*/*" + } + payload = { + "conversation_id": conversation_id, + "messages": [ + { + "id": user_message_id, + "role": "user", + "content":"" + }, + { + "id": assistant_message_id, + "role": "assistant", + "content":"" + } + ] + } + # Make the POST request + response = self.page.request.post(url, headers=headers,data=json.dumps(payload)) + # Check the response status code + assert response.status == 200, "response code is "+str(response.status)+" "+str(response.json()) + + # data = { + # 'grant_type': 'client_credentials', + # 'client_id': client_id, + # 'client_secret': client_secret, + # 'scope': f'api://{client_id}/.default' + # } + # response = requests.post(token_url, data=data) + # if response.status_code == 200: + # token_info = response.json() + # access_token = token_info['access_token'] + # # Set the headers, including the access token + # headers = { + # "Content-Type": "application/json", + # "Authorization": f"Bearer {access_token}", + # "Accept": "*/*" + # } + # payload = { + # "conversation_id": conversation_id, + # "messages": [ + # { + # "id": user_message_id, + # "role": "user", + # "content":"" + # }, + # { + # "id": assistant_message_id, + # "role": "assistant", + # "content":"" + # } + # ] + # } + # # Make the POST request + # response = self.page.request.post(url, headers=headers,data=json.dumps(payload)) + # # Check the response status code + # assert response.status == 200, "response code is "+str(response.status)+" "+str(response.json()) + # else: + # assert response.status_code == 200,"Failed to get token "+response.text + + def compare_raw_date_time(self,response_text,sidepanel_text): + # Extract date and time from response_text using regex + match = re.search(r"((\d{4}-\d{2}-\d{2}) from (\d{2}:\d{2}:\d{2}))|((\w+ \d{1,2}, \d{4}),? from (\d{2}:\d{2}))",response_text) + if match: + # check for YYYY-MM-DD format in response_text + if match.group(2) and match.group(3): + date1_str = match.group(2) + time1_str = match.group(3) + date_time1 = datetime.strptime(f"{date1_str} {time1_str}","%Y-%m-%d %H:%M:%S") + + # check for 'Month DD, YYYY' format in response_text + elif match.group(5) and match.group(6): + date1_str = match.group(5) + time1_str = match.group(6) + date_time1 = datetime.strptime(f"{date1_str} {time1_str}", "%B %d, %Y %H:%M") + + else: + raise ValueError("Date and time format not found in response_text: " + response_text) + # remove special chars in raw sidepanel_text + sidepanel_text_cleaned = re.sub(r"[\ue000-\uf8ff]", "",sidepanel_text) + + # Extract date and time from sidepanel_text using regex + match2 = re.search(r"(\w+ \w+ \d{1,2}, \d{4})\s*(\d{2}:\d{2})",sidepanel_text_cleaned) + if match2: + date2_str = match2.group(1) + time2_str = match2.group(2) + date_time2 = datetime.strptime(f"{date2_str} {time2_str}", "%A %B %d, %Y %H:%M") + else: + raise ValueError("Date and time format not found in sidepanel_text: "+sidepanel_text) + # Compare the two datetime objects + assert date_time1 == date_time2 diff --git a/tests/e2e-test/config/constants.py b/tests/e2e-test/config/constants.py new file mode 100644 index 000000000..004a55f5d --- /dev/null +++ b/tests/e2e-test/config/constants.py @@ -0,0 +1,21 @@ +from dotenv import load_dotenv +import os + +load_dotenv() +URL = os.getenv('url') +if URL.endswith('/'): + URL = URL[:-1] + +# HomePage input data +homepage_title = "Woodgrove Bank" +client_name = "Karen Berg" +# next_meeting_question = "when is the next meeting scheduled with this client?" +golden_path_question1 = "What were karen's concerns during our last meeting?" +golden_path_question2 = "Did karen express any concerns over market fluctuation in prior meetings?" +golden_path_question3 = "What type of asset does karen own ?" +golden_path_question4 = "Show latest asset value by asset type?" +golden_path_question5 = "How did equities asset value change in the last six months?" +# golden_path_question6 = "Give summary of previous meetings?" +golden_path_question7 = "Summarize Arun sharma previous meetings?" +invalid_response = "No data found for that client." +# invalid_response = "I cannot answer this question from the data available. Please rephrase or add more details." diff --git a/tests/e2e-test/img.png b/tests/e2e-test/img.png new file mode 100644 index 0000000000000000000000000000000000000000..c7c891ad7ad009184078ab6b559e82638884fea4 GIT binary patch literal 85099 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 i%2b@Mc0s^8GDiD50(EEi5 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 ph-8-Z|1ymKFCzW_x3BcxVi Date: Fri, 6 Jun 2025 07:16:57 +0530 Subject: [PATCH 15/19] Update test_automation.yml --- .github/workflows/test_automation.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_automation.yml b/.github/workflows/test_automation.yml index f49beee8d..1b6a20f28 100644 --- a/.github/workflows/test_automation.yml +++ b/.github/workflows/test_automation.yml @@ -31,7 +31,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install -r byoc-client-advisor/requirements.txt + pip install -r tests/e2e-test/requirements.txt - name: Ensure browsers are installed run: python -m playwright install --with-deps chromium From 8f99693d0df6e869c2332f59a631fb20d663cf7c Mon Sep 17 00:00:00 2001 From: Vemarthula-Microsoft Date: Fri, 6 Jun 2025 10:47:51 +0530 Subject: [PATCH 16/19] Update test_automation.yml --- .github/workflows/test_automation.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test_automation.yml b/.github/workflows/test_automation.yml index 1b6a20f28..843b5369e 100644 --- a/.github/workflows/test_automation.yml +++ b/.github/workflows/test_automation.yml @@ -40,7 +40,7 @@ jobs: id: test1 run: | xvfb-run pytest --headed --html=report/report.html --self-contained-html - working-directory: byoc-client-advisor + working-directory: tests/e2e-test continue-on-error: true - name: Sleep for 30 seconds @@ -53,7 +53,7 @@ jobs: id: test2 run: | xvfb-run pytest --headed --html=report/report.html --self-contained-html - working-directory: byoc-client-advisor + working-directory: tests/e2e-test continue-on-error: true - name: Sleep for 60 seconds @@ -66,7 +66,7 @@ jobs: id: test3 run: | xvfb-run pytest --headed --html=report/report.html --self-contained-html - working-directory: byoc-client-advisor + working-directory: tests/e2e-test - name: Upload test report id: upload_report From f80c49a88ba9077f926428e636a220db38166861 Mon Sep 17 00:00:00 2001 From: Vemarthula-Microsoft Date: Fri, 6 Jun 2025 13:08:00 +0530 Subject: [PATCH 17/19] Update test_automation.yml --- .github/workflows/test_automation.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test_automation.yml b/.github/workflows/test_automation.yml index 843b5369e..6802538be 100644 --- a/.github/workflows/test_automation.yml +++ b/.github/workflows/test_automation.yml @@ -5,9 +5,9 @@ on: branches: - main - dev - - VE-Automate - # paths: - # - 'byoc-client-advisor/**' + # - VE-Automate + paths: + - 'byoc-client-advisor/**' schedule: - cron: '0 13 * * *' # Runs at 1 PM UTC workflow_dispatch: From ea64dd17daf06cd189e3fd52354213e1cb45c34c Mon Sep 17 00:00:00 2001 From: Vemarthula-Microsoft Date: Fri, 6 Jun 2025 17:15:56 +0530 Subject: [PATCH 18/19] Update test_automation.yml --- .github/workflows/test_automation.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test_automation.yml b/.github/workflows/test_automation.yml index 6802538be..64be66e1d 100644 --- a/.github/workflows/test_automation.yml +++ b/.github/workflows/test_automation.yml @@ -5,9 +5,9 @@ on: branches: - main - dev - # - VE-Automate + paths: - - 'byoc-client-advisor/**' + - 'tests/e2e-test/**' schedule: - cron: '0 13 * * *' # Runs at 1 PM UTC workflow_dispatch: @@ -74,7 +74,7 @@ jobs: if: ${{ !cancelled() }} with: name: test-report - path: byoc-client-advisor/report/* + path: tests/e2e-test/report/* - name: Send Notification if: always() From 23d4e6406f4640cc2403bfe00cccce44c006d71c Mon Sep 17 00:00:00 2001 From: VishalS-Microsoft Date: Fri, 6 Jun 2025 18:52:23 +0530 Subject: [PATCH 19/19] feat: Standardize Bicep Parameters for Client Advisor (#563) * update CustomizingAzdParameters.md * update main.bicepparam * update deployment guide * update key names * Update CustomizingAzdParameters.md * Update main.bicepparam * Update DeploymentGuide.md * update customizingazdparameter file * updates files * update documents --------- Co-authored-by: Shreyas-Microsoft --- docs/CustomizingAzdParameters.md | 66 +++++++++++++------------------- docs/DeploymentGuide.md | 28 ++++++++------ infra/main.bicepparam | 4 +- 3 files changed, 47 insertions(+), 51 deletions(-) diff --git a/docs/CustomizingAzdParameters.md b/docs/CustomizingAzdParameters.md index 2c73b4d27..49d98701a 100644 --- a/docs/CustomizingAzdParameters.md +++ b/docs/CustomizingAzdParameters.md @@ -3,47 +3,35 @@ By default this template will use the environment name as the prefix to prevent naming collisions within Azure. The parameters below show the default values. You only need to run the statements below if you need to change the values. -> To override any of the parameters, run `azd env set ` before running `azd up`. On the first azd command, it will prompt you for the environment name. Be sure to choose 3-20 characters alphanumeric unique name. +> To override any of the parameters, run `azd env set ` before running `azd up`. On the first azd command, it will prompt you for the environment name. Be sure to choose 3-20 charaters alphanumeric unique name. + +## Parameters + +| Name | Type | Default Value | Purpose | +| -----------------------------| ------- | ------------------- | ---------------------------------------------------------------------------------------------------- | +| `AZURE_ENV_NAME` | string | `azdtemp` | Used as a prefix for all resource names to ensure uniqueness across environments. | +| `AZURE_ENV_COSMOS_LOCATION` | string | `eastus2` | Location of the Cosmos DB instance. Choose from (allowed values: `swedencentral`, `australiaeast`). | +| `AZURE_ENV_MODEL_DEPLOYMENT_TYPE` | string | `GlobalStandard` | Change the Model Deployment Type (allowed values: Standard, GlobalStandard). | +| `AZURE_ENV_MODEL_NAME` | string | `gpt-4o-mini` | Set the GPT model name (allowed values: `gpt-4o`). | +| `AZURE_ENV_MODEL_VERSION` | string | `2025-01-01-preview` | Set the Azure OpenAI API version (allowed values: 2024-08-06). | +| `AZURE_ENV_MODEL_CAPACITY` | integer | `30` | Set the model capacity for GPT deployment. Choose based on your Azure quota and usage needs. | +| `AZURE_ENV_EMBEDDING_MODEL_NAME` | string | `text-embedding-ada-002` | Set the model name used for embeddings. | +| `AZURE_ENV_EMBEDDING_MODEL_CAPACITY` | integer | `80` | Set the capacity for embedding model deployment. | +| `AZURE_ENV_IMAGETAG` | string | `latest` | Set the image tag (allowed values: `latest`, `dev`, `hotfix`). | +| `AZURE_ENV_OPENAI_LOCATION` | string | `eastus2` | Location of the Azure OpenAI resource. Choose from (allowed values: `swedencentral`, `australiaeast`). | +| `AZURE_LOCATION` | string | `japaneast` | Sets the Azure region for resource deployment. | +| `AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID` | string | `` | Reuses an existing Log Analytics Workspace instead of provisioning a new one. | + +## How to Set a Parameter +To customize any of the above values, run the following command **before** `azd up`: + +```bash +azd env set - -Change the Secondary Location (example: eastus2, westus2, etc.) - -```shell -azd env set AZURE_ENV_SECONDARY_LOCATION eastus2 -``` - -Change the Model Deployment Type (allowed values: Standard, GlobalStandard) - -```shell -azd env set AZURE_ENV_MODEL_DEPLOYMENT_TYPE Standard -``` - -Set the Model Name (allowed values: gpt-4, gpt-4o) - -```shell -azd env set AZURE_ENV_MODEL_NAME gpt-4o -``` - -Change the Model Capacity (choose a number based on available GPT model capacity in your subscription) - -```shell -azd env set AZURE_ENV_MODEL_CAPACITY 30 -``` - -Change the Embedding Model - -```shell -azd env set AZURE_ENV_EMBEDDING_MODEL_NAME text-embedding-ada-002 -``` - -Change the Embedding Deployment Capacity (choose a number based on available embedding model capacity in your subscription) - -```shell -azd env set AZURE_ENV_EMBEDDING_MODEL_CAPACITY 80 ``` -Set the Log Analytics Workspace Id if you need to reuse the existing workspace which is already existing +**Example:** -```shell -azd env set AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID '/subscriptions//resourceGroups//providers/Microsoft.OperationalInsights/workspaces/' +```bash +azd env set AZURE_LOCATION westus2 ``` diff --git a/docs/DeploymentGuide.md b/docs/DeploymentGuide.md index 4b4ca5b34..9fe502c90 100644 --- a/docs/DeploymentGuide.md +++ b/docs/DeploymentGuide.md @@ -104,17 +104,23 @@ Consider the following settings during your deployment to modify specific settin When you start the deployment, most parameters will have **default values**, but you can update the below settings by following the steps [here](CustomizingAzdParameters.md): -| **Setting** | **Description** | **Default value** | -|------------|----------------| ------------| -| **Azure OpenAI Location** | The region where OpenAI deploys | eastus2 | -| **Environment Name** | A **3-20 character alphanumeric value** used to generate a unique ID to prefix the resources. | byocatemplate | -| **Cosmos Location** | A **less busy** region for **CosmosDB**, useful in case of availability constraints. | eastus2 | -| **Deployment Type** | Select from a drop-down list. | Global Standard | -| **GPT Model** | OpenAI GPT model | gpt-4o-mini | -| **GPT Model Deployment Capacity** | Configure capacity for **GPT models**. | 30k | -| **Embedding Model** | OpenAI embedding model | text-embedding-ada-002 | -| **Embedding Model Capacity** | Set the capacity for **embedding models**. | 80k | -| **Existing Log analytics workspace** | To reuse the existing Log analytics workspace Id. | | + +| **Setting** | **Description** | **Default value** | +| ------------------------------------ | -------------------------------------------------------------------------------------------------- | ------------------------ | +| **Azure OpenAI Location** | The region where Azure OpenAI deploys. Choose from `swedencentral`, `australiaeast`, etc. | `eastus2` | +| **Environment Name** | A **3-20 character alphanumeric value** used to generate a unique ID to prefix the resources. | `azdtemp` | +| **Cosmos Location** | A **less busy** region for **CosmosDB**, useful in case of availability constraints. | `eastus2` | +| **Deployment Type** | Select from a drop-down list (`Standard`, `GlobalStandard`). | `GlobalStandard` | +| **GPT Model** | Azure OpenAI GPT model to deploy. | `gpt-4o-mini` | +| **GPT Model Deployment Capacity** | Configure capacity for **GPT models**. Choose based on Azure OpenAI quota. | `30` | +| **Embedding Model** | OpenAI embedding model used for vector similarity. | `text-embedding-ada-002` | +| **Embedding Model Capacity** | Set the capacity for **embedding models**. Choose based on usage and quota. | `80` | +| **Image Tag** | The version of the Docker image to use (e.g., `latest`, `dev`, `hotfix`). | `latest` | +| **Azure OpenAI API Version** | Set the API version for OpenAI model deployments. | `2025-01-01-preview` | +| **AZURE\_LOCATION** | Sets the Azure region for resource deployment. | `japaneast` | +| **Existing Log Analytics Workspace** | To reuse an existing Log Analytics Workspace ID instead of creating a new one. | *(empty)* | + + diff --git a/infra/main.bicepparam b/infra/main.bicepparam index 1e5c053c1..d61275246 100644 --- a/infra/main.bicepparam +++ b/infra/main.bicepparam @@ -4,9 +4,11 @@ param environmentName = readEnvironmentVariable('AZURE_ENV_NAME', 'byocatemplate param cosmosLocation = readEnvironmentVariable('AZURE_ENV_COSMOS_LOCATION', 'eastus2') param deploymentType = readEnvironmentVariable('AZURE_ENV_MODEL_DEPLOYMENT_TYPE', 'GlobalStandard') param gptModelName = readEnvironmentVariable('AZURE_ENV_MODEL_NAME', 'gpt-4o-mini') +param azureOpenaiAPIVersion = readEnvironmentVariable('AZURE_ENV_MODEL_VERSION', '2025-01-01-preview') param gptDeploymentCapacity = int(readEnvironmentVariable('AZURE_ENV_MODEL_CAPACITY', '30')) - +param embeddingModel = readEnvironmentVariable('AZURE_ENV_EMBEDDING_MODEL_NAME', 'text-embedding-ada-002') param embeddingDeploymentCapacity = int(readEnvironmentVariable('AZURE_ENV_EMBEDDING_MODEL_CAPACITY', '80')) +param imageTag = readEnvironmentVariable('AZURE_ENV_IMAGETAG', 'latest') param AzureOpenAILocation = readEnvironmentVariable('AZURE_ENV_OPENAI_LOCATION', 'eastus2') param AZURE_LOCATION = readEnvironmentVariable('AZURE_LOCATION', '') param existingLogAnalyticsWorkspaceId = readEnvironmentVariable('AZURE_ENV_LOG_ANALYTICS_WORKSPACE_ID', '')