diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..d29b303 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,25 @@ +FROM mcr.microsoft.com/devcontainers/python:1-3.10-bullseye + +# # Install Databricks CLI +RUN curl -fsSL https://raw.githubusercontent.com/databricks/setup-cli/main/install.sh | sudo sh + +# Copy Python dependencies +COPY ./requirements.txt ./requirements.txt + +# Install Python dependencies +RUN pip install --no-cache-dir --upgrade pip && \ + pip install --no-cache-dir -r ./requirements.txt + +# Install Azure ClI +RUN curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash && az bicep install + +# Connect as root non-root user. More info: https://aka.ms/dev-containers-non-root. +USER vscode + +# Add zsh-autosuggestions +RUN git clone https://github.com/zsh-users/zsh-autosuggestions ~/.zsh/zsh-autosuggestions +RUN printf 'source ~/.zsh/zsh-autosuggestions/zsh-autosuggestions.zsh\n' >> ~/.zshrc + +# Add zsh-syntax-highlighting +RUN git clone https://github.com/zsh-users/zsh-syntax-highlighting.git ~/.zsh/zsh-syntax-highlighting +RUN printf 'source ~/.zsh/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh\n' >> ~/.zshrc diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..9cd97c9 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,81 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/python +{ + "name": "azure-open-ai-example-scenarios", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "build": { + // Path is relative to the devcontainer.json file. + "dockerfile": "Dockerfile" + }, + // Features to add to the dev container. More info: https://containers.dev/features. + "features": { + "ghcr.io/devcontainers/features/docker-in-docker:2.10.2": {}, + "ghcr.io/devcontainers/features/github-cli:1": {}, + "ghcr.io/devcontainers-contrib/features/zsh-plugins:0": {} + }, + // Use 'forwardPorts' to make a list of ports inside the container available locally. + "forwardPorts": [ + 8501 + ], + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "", + // Configure tool-specific properties. + "customizations": { + "vscode": { + "extensions": [ + "ms-azuretools.vscode-docker", + "charliermarsh.ruff", + "ms-vscode.vscode-node-azure-pack", + "ms-vscode.azurecli", + "ms-azuretools.vscode-bicep", + "ms-vscode-remote.remote-containers", + "databricks.databricks", + "github.vscode-github-actions", + "redhat.vscode-yaml", + "ms-toolsai.jupyter", + "ms-python.python", + "yzhang.markdown-all-in-one" + ] + }, + "settings": { + "editor.autoClosingBrackets": "always", + "editor.codeActionsOnSave": { + "source.organizeImports": "explicit" + }, + "editor.formatOnPaste": true, + "editor.formatOnSave": true, + "editor.inlineSuggest.enabled": true, + "files.autoSave": "afterDelay", + "git.autofetch": true, + "github.copilot.enable": { + "*": true + }, + "notebook.formatOnSave.enabled": true, + "notebook.codeActionsOnSave": { + "notebook.source.fixAll": "explicit", + "notebook.source.organizeImports": "explicit" + }, + "terminal.integrated.defaultProfile.linux": "zsh", + "terminal.integrated.profiles.linux": { + "zsh": { + "path": "/usr/bin/zsh" + } + }, + "[python]": { + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll": "explicit", + "source.organizeImports": "explicit" + }, + "editor.defaultFormatter": "charliermarsh.ruff" + } + } + }, + // You can use the mounts property to persist the user profile (to keep things like shell history). + "mounts": [ + "source=profile,target=/root,type=volume", + "target=/root/.vscode-server,type=volume" + ] + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} \ No newline at end of file diff --git a/.devcontainer/requirements.txt b/.devcontainer/requirements.txt new file mode 100644 index 0000000..1160634 --- /dev/null +++ b/.devcontainer/requirements.txt @@ -0,0 +1,9 @@ +azure-identity==1.16.0 +ipykernel==6.29.4 +Jinja2==3.1.4 +openai==1.26.0 +python-dotenv==1.0.1 +requests==2.31.0 +stop-words==2018.7.23 +streamlit==1.34.0 +streamlit-extras==0.4.2 diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..9c9489f --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,28 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for more information: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates +# https://containers.dev/guide/dependabot + +version: 2 +updates: + + - package-ecosystem: "devcontainers" + directory: ".devcontainer/" + schedule: + interval: weekly + + - package-ecosystem: "docker" + directory: "/" + schedule: + interval: weekly + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/docs/getting-started.md b/.github/docs/getting-started.md new file mode 100644 index 0000000..207ccd1 --- /dev/null +++ b/.github/docs/getting-started.md @@ -0,0 +1,59 @@ +# Getting Started + +The purpose of this section is to provide an overview of each example scenario. + +## Example Scenario: Contoso Trek Product Info Chatbot (Custom) + +### Overview + +This example scenario demonstrates a small-scale proof-of-concept deployment of a chatbot leveraging Azure OpenAI and Azure AI Search. It coveres the following key areas: + +- Pull-based data ingestion with Azure AI Search +- Custom retrieval-augmented generation (RAG) Implementation +- Built using Streamlit +- Deployed using Azure Container Apps + +### Solution Design + +The below diagram shows a high-level design for the chatbot leveraging Azure OpenAI, Azure AI Search, deployed as part of an Azure Container App. + +![Solution Design](./images/design-01.png) + +The solution consists of the following services: + +- **[Azure OpenAI](https://learn.microsoft.com/azure/ai-services/openai/overview)**: Provides the large language model (LLM) capabilities for generating human-like responses based on user queries. +- **[Azure AI Search](https://learn.microsoft.com/azure/search/search-what-is-azure-search)**: Enables efficient retrieval of relevant information from your enterprise knowledge base or data sources. +- **[Container Registry](https://learn.microsoft.com/azure/container-registry/container-registry-intro)**: Used for storing the Docker image. +- **[Container App](https://learn.microsoft.com/azure/container-apps/containers)**: Used for exposing the container as a REST API. + +The following open-source Python framework are used in this project: + +- **[Streamlit](https://streamlit.io/)**: Facilitates a lightweight and user-friendly deployment experience, making the chatbot readily accessible through a web interface. + +### How It Works + +The RAG pattern implemented here utilizes Azure AI Search to retrieve the most relevant information based on the user's query. This retrieved information is then fed into the Azure OpenAI LLM, which generates a comprehensive and informative response tailored to the specific context. + +The chatbot is built using Streamlit, which provides a simple and intuitive interface for users to interact with the chatbot. The chatbot is deployed using Azure Container Apps, which allows for easy scaling and management of the application. + +The [Step-by-Step Setup](.github/docs/step-by-step-setup.md) section of this repository. provides detailed instructions on how to deploy this proof-of-concept. + +> [!CAUTION] +> This solution design is intended for proof-of-concept scenarios and is not recommended for enterprise production scenarios. It is advised to review and adjust the design based on your specific requirements if you plan to use this in a production environment. This could include: +> +> - Securing the solution through network controls. +> - Upflift observability by enabling monitoring and logging for different services. +> - Defining an operational support and lifecycle management plan for the solution. +> - Implementing alerting and notification mechanisms to notify support teams of any issues (e.g. performance, budget, etc.). +> +> The Azure Well-Architected Framework provides guidance on best practices for designing, building, and maintaining cloud solutions. For more information, see the [Azure Well-Architected Framework](https://learn.microsoft.com/azure/well-architected/what-is-well-architected-framework). + +## Related Resources + +- [Azure OpenAI](https://learn.microsoft.com/azure/ai-services/openai/) +- [Azure AI Search](https://learn.microsoft.com/azure/search/) +- [Streamlit](https://streamlit.io/) +- [Azure OpenAI Service REST API reference](https://learn.microsoft.com/azure/ai-services/openai/reference) +- [Securely use Azure OpenAI on your data](https://learn.microsoft.com/azure/ai-services/openai/how-to/use-your-data-securely) +- [Introduction to prompt engineering](https://learn.microsoft.com/azure/ai-services/openai/concepts/prompt-engineering) +- [Prompt engineering techniques](https://learn.microsoft.com/azure/ai-services/openai/concepts/advanced-prompt-engineering?pivots=programming-language-chat-completions) diff --git a/.github/docs/images/design-01.png b/.github/docs/images/design-01.png new file mode 100644 index 0000000..afae0f1 Binary files /dev/null and b/.github/docs/images/design-01.png differ diff --git a/.github/docs/step-by-step-setup.md b/.github/docs/step-by-step-setup.md new file mode 100644 index 0000000..ac64ae0 --- /dev/null +++ b/.github/docs/step-by-step-setup.md @@ -0,0 +1,199 @@ +# Step-by-Step Setup + +The purpose of this section is to describe the steps required to setup each example scenario. + +> [!TIP] +> +> - The following options are recommended to complete the setup: +> 1. Using the [Azure Cloud Shell](https://learn.microsoft.com/azure/cloud-shell/overview) within the Azure Portal. +> 2. Using a [GitHub Codespace](https://docs.github.com/en/codespaces/prebuilding-your-codespaces/about-github-codespaces-prebuilds) +> 3. Using your local VSCode environment with the environment specified in the [development container](https://docs.github.com/en/codespaces/setting-up-your-project-for-codespaces/adding-a-dev-container-configuration/introduction-to-dev-containers). This will be the most efficient way to complete the setup. + +> [!WARNING] +> +> - As with all Azure Deployments, this will incur associated costs. Remember to teardown all related resources after use to avoid unnecessary costs. + +## Prerequisites + +Before implementing this example scenario the following is needed: + +- Azure subscription with Owner permissions. +- GitHub account. + +## 1. Common Setup + +## 1.1. Create a GitHub repository + +1. Log in to your GitHub account and navigate to the [azure-open-ai-example-scenarios](https://github.com/nfmoore/azure-open-ai-example-scenarios) repository and click `Use this template` to create a new repository from this template. + + Rename the template and leave it public. Ensure you click `Include all branches` to copy all branches. + +> [!NOTE] +> +> - You can learn more about creating a repository from a template [here](https://docs.github.com/en/repositories/creating-and-managing-repositories/creating-a-template-repository). + +## 1.2. Deploy Azure resources + +1. Run the following command to get your Object ID of your Azure AD user. + + ```bash + az ad signed-in-user show --query "id" -o tsv + ``` + + This will be used to assign the required permissions needed to interact with the Azure resources during the setup or a development scenario. + +2. Click the `Deploy to Azure` button below to deploy the Azure resources required for these example scenarios. + + [![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fnfmoore%2Fazure-open-ai-example-scenarios%2Fmain%2Finfrastructure%2Fmain.json) + +> [!NOTE] +> +> - The Azure AD user requires the `Search Service Contributor ` role and `Search Index Data Contributor` role when creating assets and interacting with Azure AI Search. +> - The Azure AD user requires the `Cognitive Services OpenAI Contributor` role when interacting with Azure Open AI. +> - These roles have been assigned to the Azure AD user in the ARM template used for deployment in step (2). + +## 2. Example Scenario Setup + +## 2.1. Contoso Trek Product Info Chatbot (Custom) + +The [Getting Started](.github/docs/getting-started.md) section of this repository provides an overview of the Contoso Trek Product Info Chatbot (Custom) example scenario. + +### 2.1.1. Set environment variables + +1. To run this project, you need to configure the following environment variables. These must be stored in a .env file in the root of the project. + + ```bash + AZURE_SUBSCRIPTION_ID= + AZURE_RESOURCE_GROUP= + + AZURE_OPENAI_API_BASE=.openai.azure.com + AZURE_AI_SEARCH_ENDPOINT=.search.windows.net + + AZURE_OPENAI_CHAT_DEPLOYMENT=gpt-35-turbo-16k-0613 + AZURE_OPENAI_EMBEDDING_DEPLOYMENT=text-embedding-ada-002-2 + + AZURE_AI_SEARCH_INDEX_NAME=contoso-trek-product-info-01-index + AZURE_AI_SEARCH_INDEXER_NAME=contoso-trek-product-info-01-indexer + AZURE_AI_SEARCH_DATASOURCE_NAME=contoso-trek-product-info-01-datasource + AZURE_AI_SEARCH_SKILLSET_NAME=contoso-trek-product-info-01-skillset + + AZURE_AI_SEARCH_DATASTORE_NAME= + AZURE_AI_SEARCH_DATASTORE_CONTAINER_NAME=data + AZURE_AI_SEARCH_DATASTORE_CONTAINER_PATH=contoso-trek-product-info + ``` + +> [!NOTE] +> +> - The values of the environment variables can be found in the Azure Portal after the deployment of the Azure resources. + +### 2.1.2. Configure the Azure AI Search service and query the Azure Open AI + +1. Before running the notebooks ensure you authenticate with the Azure CLI by running the following command: + + ```bash + az login --tenant + ``` + + This will open a browser window to authenticate with the Azure CLI. +2. To create these artifacts to configure the Azure AI Search service run section `1. Populate Azure AI Search Index` of the the `notebooks/example_scenario_01.ipynb` notebook. +3. After the index, skillset, indexer, and datasource have been created, check that the indexer has run successfully and the index has been populated with data in the Azure Portal under the Azure AI Search service. +4. To query Azure Open AI using the RAG pattern run section `2. LLM Queries with Knowledge Base Integration` of the the `notebooks/example_scenario_01.ipynb` notebook. This section uses the custom RAG implementation and contrasts this with the custom RAG implementation. + +> [!WARNING] +> +> - Ensure that you can complete this section without any errors before proceeding to the next step. + +### 2.1.3. Run the Streamlit app + +1. Before running the streamlit app ensure you authenticate with the Azure CLI by running the following command: + + ```bash + az login --tenant + ``` + + This will open a browser window to authenticate with the Azure CLI. +2. To run the streamlit app locally for testing purposes, you can use the following command: + + ```bash + streamlit run ./app/main.py + ``` +3. Open your web browser and navigate to `http://localhost:8501` to access the chatbot. + +![Streamlit Chat App](./images/image-01.png) + +> [!IMPORTANT] +> +> - Ensure that the Azure AI Search service has been populated with data before running the Streamlit app. + +### 2.1.4. Deploy the Streamlit app to an Azure Container App + +1. Log into the Azure CLI by running the following command: + + ```bash + az login --tenant + ``` + + This will open a browser window to authenticate with the Azure CLI. + +2. Run the following command to authenticate to the Azure Container Registry: + + ```bash + export CONTAINER_REGISTRY_NAME= + az acr login --name $CONTAINER_REGISTRY_NAME -t + ``` +3. Run the following command to build and push the Docker image to the Azure Container Registry: + + ```bash + export CONTAINER_IMAGE_NAME=contoso-trek-product-info-chatbot + + az acr build --image $CONTAINER_IMAGE_NAME --registry $CONTAINER_REGISTRY_NAME --file ./app/Dockerfile . + ``` + +4. Run the following command to deploy the Azure Container App: + + ```bash + export CONTAINER_APP_NAME= # will be created after executing the command + export RESOURCE_GROUP_NAME= # can be found on the Azure Portal + export CONTAINER_APP_ENVIRONMENT_NAME= # can be found on the Azure Portal + export USER_ASSIGNED_IDENTITY_NAME= # can be found on the Azure Portal + + export CONTAINER_REGISTRY_HOSTNAME=$CONTAINER_REGISTRY_NAME.azurecr.io + export USER_ASSIGNED_IDENTITY_ID=$(az identity show --resource-group $RESOURCE_GROUP_NAME --name $USER_ASSIGNED_IDENTITY_NAME | jq '.id' -r) + export USER_ASSIGNED_IDENTITY_CLIENT_ID=$(az identity show --resource-group $RESOURCE_GROUP_NAME --name $USER_ASSIGNED_IDENTITY_NAME | jq '.clientId' -r) + + export AZURE_OPENAI_API_BASE=.openai.azure.com # can be found on the Azure Portal + export AZURE_OPENAI_CHAT_DEPLOYMENT=gpt-35-turbo-16k-0613 + export AZURE_OPENAI_EMBEDDING_DEPLOYMENT=text-embedding-ada-002-2 + + export AZURE_AI_SEARCH_ENDPOINT=.search.windows.net # can be found on the Azure Portal + export AZURE_AI_SEARCH_INDEX_NAME=contoso-trek-product-info-01-index + + az containerapp create \ + --name $CONTAINER_APP_NAME \ + --resource-group $RESOURCE_GROUP_NAME \ + --environment $CONTAINER_APP_ENVIRONMENT_NAME \ + --user-assigned $USER_ASSIGNED_IDENTITY_ID \ + --registry-identity $USER_ASSIGNED_IDENTITY_ID \ + --registry-server $CONTAINER_REGISTRY_HOSTNAME \ + --image $CONTAINER_REGISTRY_HOSTNAME/$CONTAINER_IMAGE_NAME \ + --target-port 8501 \ + --ingress 'external' \ + --max-replicas 2 \ + --env-vars AZURE_OPENAI_API_BASE=$AZURE_OPENAI_API_BASE AZURE_OPENAI_CHAT_DEPLOYMENT=$AZURE_OPENAI_CHAT_DEPLOYMENT AZURE_OPENAI_EMBEDDING_DEPLOYMENT=$AZURE_OPENAI_EMBEDDING_DEPLOYMENT AZURE_AI_SEARCH_ENDPOINT=$AZURE_AI_SEARCH_ENDPOINT AZURE_AI_SEARCH_INDEX_NAME=$AZURE_AI_SEARCH_INDEX_NAME AZURE_CLIENT_ID=$USER_ASSIGNED_IDENTITY_CLIENT_ID + ``` + +5. After the deployment is complete, navigate to the Azure Container App URL to access the chatbot. +6. [Optional] Run the following command to update the Azure Container App after it has been deployed: + + ```bash + az containerapp update \ + --name $CONTAINER_APP_NAME \ + --resource-group $RESOURCE_GROUP_NAME \ + --image $CONTAINER_REGISTRY_HOSTNAME/$CONTAINER_IMAGE_NAME \ + --max-replicas 2 + ``` + +> [!IMPORTANT] +> +> - Ensure that the Azure AI Search service has been populated with data before deploying the Streamlit app to an Azure Container App. +> - Steps (3), (4) and (6) require environment variables that were set in prior steps. diff --git a/.gitignore b/.gitignore index 68bc17f..ee566fa 100644 --- a/.gitignore +++ b/.gitignore @@ -158,3 +158,6 @@ cython_debug/ # 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/ + +# Others +.DS_Store \ No newline at end of file diff --git a/README.md b/README.md index a72a880..533d799 100644 --- a/README.md +++ b/README.md @@ -1,124 +1,33 @@ -# Example Scenario: Enterprise Chatbot with Azure OpenAI, Azure AI Search, and Streamlit (RAG Pattern) +# Example Scenarios: Generative AI Applications with Azure OpenAI -## Overview +## :books: Overview -This project demonstrates a small-scale proof-of-concept deployment of an enterprise chatbot leveraging the power of Azure OpenAI and Azure AI Search, built and deployed using Streamlit. Utilizing the Retrieval-Augmented Generation (RAG) pattern, the chatbot combines the strengths of both services: +This repository demonstrates small-scale proof-of-concept deployments of Generative AI applications with Azure OpenAI. The repository showcases the capabilities of Azure OpenAI and provides a foundation for enterprises seeking to implement Generative AI applications with Azure OpenAI. -- `Azure OpenAI`: Provides the large language model (LLM) capabilities for generating human-like responses based on user queries. -- `Azure AI Search`: Enables efficient retrieval of relevant information from your enterprise knowledge base or data sources. -- `Streamlit`: Facilitates a lightweight and user-friendly deployment experience, making the chatbot readily accessible through a web interface. +## :computer: Getting Started -The RAG pattern implemented here utilizes Azure AI Search to retrieve the most relevant information based on the user's query. This retrieved information is then fed into the Azure OpenAI LLM, which generates a comprehensive and informative response tailored to the specific context. +This repository will highlight different areas of interest when implementing Generative AI applications with Azure OpenAI. Each example scenario demonstrates a small-scale proof-of-concept that can be used as a foundation for enterprises seeking to implement Generative AI applications with Azure OpenAI. -This project serves as a valuable foundation for small-scale proof-of-concept for enterprises seeking to: +More information on the example scenarios is outlined in the [Getting Started](.github/docs/getting-started.md) section of this repository. -- Implement AI-powered chatbots for improved user engagement and information access. -- Leverage the combined power of Azure OpenAI and Azure AI Search for intelligent text processing and retrieval. -- Build and deploy chatbots efficiently and seamlessly using Streamlit. +### Setup -## Getting Started +Detailed instructions for deploying these proofs-of-concept are outlined in the [Step-by-Step Setup](.github/docs/step-by-step-setup.md) section of this repository. The example scenarios will illustrate concepts such as: -### Create an environment using venv +- Building a search index using Azure AI Search that can be used to retrieve relevant information. +- Implementing the Retrieval-Augmented Generation (RAG) pattern. +- Developing a lightweight web interface for a chatbot using Streamlit. +- Deploying the Generative AI application using Streamlit. +- Deploy the application using Azure Container Apps. -1. Create a python environment using venv. In your terminal, type: +### Example Scenarios - ```bash - python3 -m venv .venv - ``` +This proof-of-concept will cover the following example scenarios: -2. Activate the environment. In your terminal, type: +| Example Scenario | Description | +| ---------------- | ------------ | +| Contoso Trek Product Info Chatbot (Custom) | Small-scale proof-of-concept demonstrating a chatbot leveraging built and deployed using Streamlit and Azure Container Apps. The chatbot uses a custom RAG implementation and Azure AI Search for pull-based data ingestion. | - - On Windows: - - ```bash - .venv\Scripts\activate - ``` - - - On macOS and Linux: - - ```bash - source .venv/bin/activate - ``` - -3. Install the required packages. In your terminal, type: - - ```bash - pip install -r environment/requirements.txt - ``` - -4. Login to Azure using the Azure CLI. In your terminal, type: - - ```bash - az login --tenant --use-device-code - ``` - -### Set environment variables - -To run this project, you need to configure the following environment variables. These can be stored in a .env file in the root of the project. - -- `AZURE_SUBSCRIPTION_ID`: The Azure subscription ID to use for the deployment. For example, `00000000-0000-0000-0000-000000000000`. -- `AZURE_RESOURCE_GROUP_NAME`: The name of the resource group to use for the deployment. For example, `my-resource-group`. -- `AZURE_OPENAI_API_BASE`: The base URL for the Azure OpenAI API. For example, `https://my-resource.openai.azure.com/`. -- `AZURE_OPENAI_API_VERSION`: The version of the Azure OpenAI API. You must set this to `2023-12-01-preview`. -- `AZURE_OPENAI_API_TYPE`: The type of the Azure OpenAI API. You must set this to `azure`. -- `AZURE_OPENAI_CHAT_DEPLOYMENT`: The name of the Azure OpenAI deployment to use for chat. For example, `gpt-35-turbo-16k-0613`. -- `AZURE_OPENAI_CHAT_MODEL`: The name of the Azure OpenAI model to use for chat. For example, `gpt-35-turbo-16k`. -- `AZURE_OPENAI_EMBEDDING_DEPLOYMENT`: The name of the Azure OpenAI deployment to use for embedding. For example, `text-embedding-ada-002-2`. -- `AZURE_OPENAI_EMBEDDING_MODEL`: The name of the Azure OpenAI model to use for embedding. For example, `text-embedding-ada-002`. -- `AZURE_OPENAI_EVALUATION_DEPLOYMENT`: The name of the Azure OpenAI deployment to use for evaluation. For example, `gpt-35-turbo-16k-0613`. -- `AZURE_OPENAI_EVALUATION_MODEL`: The name of the Azure OpenAI model to use for evaluation. For example, `gpt-35-turbo-16k`. -- `AZURE_AI_SEARCH_ENDPOINT`: The endpoint for the Azure AI Search service. For example, `https://my-resource.search.windows.net/`. -- `AZURE_AI_SEARCH_INDEX_NAME`: The name of the Azure AI Search index that will store the vector embeddings of the extracted content. For example, `contoso-index`. -- `AZURE_AI_SEARCH_INDEXER_NAME`: The name of the Azure AI Search indexer that will populate the search index with the extracted content. For example, `contoso-indexer`. -- `AZURE_AI_SEARCH_DATASOURCE_NAME`: The name of the Azure AI Search data source that will connect the search service with the storage container. For example, `contoso-datasource`. -- `AZURE_AI_SEARCH_SKILLSET_NAME`: The name of the Azure AI Search skillset that will chunk documents and generate embeddings. For example, `contoso-skillset`. -- `AZURE_AI_SEARCH_INDEXER_BATCH_SIZE`: The number of documents to process in a single batch. For example, `500`. -- `AZURE_AI_SEARCH_VECTOR_EMBEDDING_DIMENSION`: The dimension of the vector embeddings generated by the skillset. For example, `1536`. -- `AZURE_AI_SEARCH_DATASTORE_NAME`: The name of Azure Storage account that will be registered as an Azure AI Search data source. For example, `contoso-datastore`. -- `AZURE_AI_SEARCH_DATASTORE_CONTAINER_NAME`: The name of the Azure Storage container that stores the data that will be used to populate the index. For example, `contoso-container`. -- `AZURE_AI_SEARCH_DATASTORE_CONTAINER_PATH`: The path to the data that will be used to populate the index. For example, `data/`. - -### Configure the Azure AI Search service - -To run this project, you need to configure the Azure AI Search service. You can do this using the Azure portal or the Azure CLI. This will populate Azure AI Search with a data source, an index, an indexer, and a skillset. - -All templates are provided in the `src/search/templates/product-info` folder and values for the variables, for example `{{ AZURE_OPENAI_API_BASE }}` are populated based on the environment variables. - -To create these artifacts to configure the Azure AI Search service you can run the notebook `src/01-populate-index.ipynb`. - -### Query the Azure AI Search service - -This notebook illistrates two appraoches to query the Azure AI Search service: - -1. Using a custom client implementing the retreival-augmented generation (RAG) pattern. -2. Using the Azure Open AI REST API. - -To query the Azure AI Search service, you can run the notebook `src/02-query-index.ipynb`. - -### Run streamlit app - -To run the streamlit app locally for testing purposes, you can use the following command: - -```bash -streamlit run ./src/app/main.py --client.toolbarMode='minimal' -``` - -Open your web browser and navigate to `http://localhost:8501` to access the chatbot. - -![Streamlit Chat App](./.github/docs/images/image-01.png) - -If you want to deploy this app to Azure, you can containerise it using the `Dockerfile` and deploy it to a suitable Azure service, such as Azure App Service or Azure Container Apps. - -## Resources - -- [Azure OpenAI](https://learn.microsoft.com/azure/ai-services/openai/) -- [Azure AI Search](https://learn.microsoft.com/azure/search/) -- [Streamlit](https://streamlit.io/) -- [Azure OpenAI Service REST API reference](https://learn.microsoft.com/azure/ai-services/openai/reference) -- [Securely use Azure OpenAI on your data](https://learn.microsoft.com/azure/ai-services/openai/how-to/use-your-data-securely) -- [Introduction to prompt engineering](https://learn.microsoft.com/azure/ai-services/openai/concepts/prompt-engineering) -- [Prompt engineering techniques](https://learn.microsoft.com/azure/ai-services/openai/concepts/advanced-prompt-engineering?pivots=programming-language-chat-completions) - -## License +## :balance_scale: License Details on licensing for the project can be found in the [LICENSE](./LICENSE) file. diff --git a/app/Dockerfile b/app/Dockerfile new file mode 100644 index 0000000..290bfaf --- /dev/null +++ b/app/Dockerfile @@ -0,0 +1,29 @@ +FROM python:3.10-slim + +WORKDIR /app + +RUN apt-get update && apt-get install -y \ + build-essential \ + curl \ + software-properties-common \ + git \ + && rm -rf /var/lib/apt/lists/* + +COPY ./app/requirements.txt /app/requirements.txt + +COPY ./llms/custom_rag_client.py /app/llms/custom_rag_client.py +COPY ./llms/system_messages.yml /app/llms/system_messages.yml + +COPY ./app/main.py /app/streamlit_app.py + +ARG AZURE_APP_SYSTEM_PROMPT_CONFIGURATION_FILE=./llms/system_messages.yml +ARG AZURE_APP_TITLE="Contoso Trek Product Info" + +RUN pip install --no-cache-dir --upgrade pip && \ + pip install --no-cache-dir -r ./requirements.txt + +EXPOSE 8501 + +HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health + +ENTRYPOINT ["streamlit", "run", "streamlit_app.py", "--server.port=8501", "--server.address=0.0.0.0"] diff --git a/src/app/main.py b/app/main.py similarity index 67% rename from src/app/main.py rename to app/main.py index 7fe8050..64b4f2a 100644 --- a/src/app/main.py +++ b/app/main.py @@ -1,5 +1,5 @@ """ - This module contains the main function that runs the streamlit application. +This module contains the main function that runs the streamlit application. """ import os @@ -7,9 +7,44 @@ import sys import streamlit as st +import yaml +from azure.identity import DefaultAzureCredential, ManagedIdentityCredential from dotenv import load_dotenv -from rag.utilities import RetrievalAugmentedGenerationClient +DEFAULT_APP_TITLE = "Contoso Trek Product Info" +DEFAULT_CONFIGURATION_FILE = "./llms/system_messages.yml" + + +def load_system_messages() -> tuple[str, str]: + """ + Loads system messages from a predefined file. + + Parameters: + answer (str): The text to modify. + references (list[dict]): The list of references. + + Returns: + system_message: The loaded system messages for query and chat. + """ + # Define configuration file + system_prompt_configuration_file = os.getenv( + "AZURE_APP_SYSTEM_PROMPT_CONFIGURATION_FILE", DEFAULT_CONFIGURATION_FILE + ) + + # Load configuration file + with open(system_prompt_configuration_file, "r", encoding="utf-8") as f: + configuration = yaml.safe_load(f) + + # Get system messages + query_system_message = configuration.get("query_system_message") + chat_system_message = configuration.get("product_info_chat_system_message") + + system_messages = { + "query_system_message": query_system_message, + "chat_system_message": chat_system_message, + } + + return system_messages def get_answer(question: str) -> str: @@ -17,7 +52,6 @@ def get_answer(question: str) -> str: Create a completion using the client and message history. Parameters: - client (RetrievalAugmentedGenerationClient): The rag client. question (str): The latest user message. Returns: @@ -35,7 +69,7 @@ def get_answer(question: str) -> str: # Get the assistant message from the chat history formatted_answer = replace_references( answer=message_history[-1]["content"], - references=message_history[-2]["references"], + references=message_history[-1]["context"]["references"], ) return formatted_answer @@ -68,7 +102,23 @@ def replace_references(answer: str, references: list[dict]) -> str: return answer -def main(): +def get_credential() -> ManagedIdentityCredential | DefaultAzureCredential: + """ + Get the Azure credential for the client. + + Parameters: + None + + Returns: + credential: The Azure credential. + """ + if os.getenv("AZURE_CLIENT_ID"): + return ManagedIdentityCredential(client_id=os.getenv("AZURE_CLIENT_ID")) + else: + return DefaultAzureCredential() + + +def main() -> None: """ Main function that runs the application. @@ -82,17 +132,17 @@ def main(): None """ # Set the title of the app - st.title(os.getenv("AZURE_APP_TITLE")) + st.title(os.getenv("APP_TITLE", DEFAULT_APP_TITLE)) # Initialize the orchestration client - st.session_state.client = RetrievalAugmentedGenerationClient( + st.session_state.client = CustomRetrievalAugmentedGenerationClient( open_ai_endpoint=os.getenv("AZURE_OPENAI_API_BASE"), open_ai_chat_deployment=os.getenv("AZURE_OPENAI_CHAT_DEPLOYMENT"), open_ai_embedding_deployment=os.getenv("AZURE_OPENAI_EMBEDDING_DEPLOYMENT"), search_endpoint=os.getenv("AZURE_AI_SEARCH_ENDPOINT"), search_index_name=os.getenv("AZURE_AI_SEARCH_INDEX_NAME"), - system_prompt_configuration_file="src/rag/configuration.yaml" - or os.getenv("AZURE_APP_SYSTEM_PROMPT_CONFIGURATION_FILE"), + credential=get_credential(), + **load_system_messages(), ) # Initialize messages from app session @@ -119,7 +169,6 @@ def main(): if prompt := st.chat_input( "Ask me a question", disabled=st.session_state.is_running ): - # Add user message to app message history st.session_state.app_messages.append({"role": "user", "content": prompt}) @@ -132,7 +181,6 @@ def main(): # Generate a new response if last message is not from assistant if st.session_state.app_messages[-1]["role"] != "assistant": - # Display assistant response in chat message container with st.chat_message("assistant"): with st.spinner("Generating response..."): @@ -149,6 +197,9 @@ def main(): if __name__ == "__main__": - sys.path.append(os.path.join(os.getcwd(), "src")) + sys.path.append(os.path.join(os.getcwd(), ".")) + + from llms.custom_rag_client import CustomRetrievalAugmentedGenerationClient + load_dotenv() main() diff --git a/app/requirements.txt b/app/requirements.txt new file mode 100644 index 0000000..0d05e48 --- /dev/null +++ b/app/requirements.txt @@ -0,0 +1,8 @@ +azure-identity==1.16.0 +ipykernel==6.29.4 +Jinja2==3.1.4 +openai==1.26.0 +python-dotenv==1.0.1 +requests==2.31.0 +streamlit==1.34.0 +streamlit-extras==0.4.2 diff --git a/data/product-info/product_info_1.md b/data/contoso-trek-product-info/product_info_1.md similarity index 100% rename from data/product-info/product_info_1.md rename to data/contoso-trek-product-info/product_info_1.md diff --git a/data/product-info/product_info_10.md b/data/contoso-trek-product-info/product_info_10.md similarity index 100% rename from data/product-info/product_info_10.md rename to data/contoso-trek-product-info/product_info_10.md diff --git a/data/product-info/product_info_11.md b/data/contoso-trek-product-info/product_info_11.md similarity index 100% rename from data/product-info/product_info_11.md rename to data/contoso-trek-product-info/product_info_11.md diff --git a/data/product-info/product_info_12.md b/data/contoso-trek-product-info/product_info_12.md similarity index 100% rename from data/product-info/product_info_12.md rename to data/contoso-trek-product-info/product_info_12.md diff --git a/data/product-info/product_info_13.md b/data/contoso-trek-product-info/product_info_13.md similarity index 100% rename from data/product-info/product_info_13.md rename to data/contoso-trek-product-info/product_info_13.md diff --git a/data/product-info/product_info_14.md b/data/contoso-trek-product-info/product_info_14.md similarity index 100% rename from data/product-info/product_info_14.md rename to data/contoso-trek-product-info/product_info_14.md diff --git a/data/product-info/product_info_15.md b/data/contoso-trek-product-info/product_info_15.md similarity index 100% rename from data/product-info/product_info_15.md rename to data/contoso-trek-product-info/product_info_15.md diff --git a/data/product-info/product_info_16.md b/data/contoso-trek-product-info/product_info_16.md similarity index 100% rename from data/product-info/product_info_16.md rename to data/contoso-trek-product-info/product_info_16.md diff --git a/data/product-info/product_info_17.md b/data/contoso-trek-product-info/product_info_17.md similarity index 100% rename from data/product-info/product_info_17.md rename to data/contoso-trek-product-info/product_info_17.md diff --git a/data/product-info/product_info_18.md b/data/contoso-trek-product-info/product_info_18.md similarity index 100% rename from data/product-info/product_info_18.md rename to data/contoso-trek-product-info/product_info_18.md diff --git a/data/product-info/product_info_19.md b/data/contoso-trek-product-info/product_info_19.md similarity index 100% rename from data/product-info/product_info_19.md rename to data/contoso-trek-product-info/product_info_19.md diff --git a/data/product-info/product_info_2.md b/data/contoso-trek-product-info/product_info_2.md similarity index 100% rename from data/product-info/product_info_2.md rename to data/contoso-trek-product-info/product_info_2.md diff --git a/data/product-info/product_info_20.md b/data/contoso-trek-product-info/product_info_20.md similarity index 100% rename from data/product-info/product_info_20.md rename to data/contoso-trek-product-info/product_info_20.md diff --git a/data/product-info/product_info_3.md b/data/contoso-trek-product-info/product_info_3.md similarity index 100% rename from data/product-info/product_info_3.md rename to data/contoso-trek-product-info/product_info_3.md diff --git a/data/product-info/product_info_4.md b/data/contoso-trek-product-info/product_info_4.md similarity index 100% rename from data/product-info/product_info_4.md rename to data/contoso-trek-product-info/product_info_4.md diff --git a/data/product-info/product_info_5.md b/data/contoso-trek-product-info/product_info_5.md similarity index 100% rename from data/product-info/product_info_5.md rename to data/contoso-trek-product-info/product_info_5.md diff --git a/data/product-info/product_info_6.md b/data/contoso-trek-product-info/product_info_6.md similarity index 100% rename from data/product-info/product_info_6.md rename to data/contoso-trek-product-info/product_info_6.md diff --git a/data/product-info/product_info_7.md b/data/contoso-trek-product-info/product_info_7.md similarity index 100% rename from data/product-info/product_info_7.md rename to data/contoso-trek-product-info/product_info_7.md diff --git a/data/product-info/product_info_8.md b/data/contoso-trek-product-info/product_info_8.md similarity index 100% rename from data/product-info/product_info_8.md rename to data/contoso-trek-product-info/product_info_8.md diff --git a/data/product-info/product_info_9.md b/data/contoso-trek-product-info/product_info_9.md similarity index 100% rename from data/product-info/product_info_9.md rename to data/contoso-trek-product-info/product_info_9.md diff --git a/environment/requirements.txt b/environment/requirements.txt deleted file mode 100644 index d47c386..0000000 --- a/environment/requirements.txt +++ /dev/null @@ -1,9 +0,0 @@ -streamlit==1.31.0 -python-dotenv==1.0.1 -openai==1.11.1 -ipykernel==6.29.2 -Jinja2==3.1.3 -requests==2.31.0 -streamlit-extras==0.4.0 -azure-identity==1.15.0 -nltk==3.8.1 \ No newline at end of file diff --git a/infrastructure/main.bicep b/infrastructure/main.bicep index afc27c0..03c1d57 100644 --- a/infrastructure/main.bicep +++ b/infrastructure/main.bicep @@ -2,202 +2,236 @@ // Parameters //******************************************************** -// Workload identifier used to create unique names for resources. -@description('A unique identifier for the workload.') -@minLength(2) -@maxLength(6) -param workloadIdentifier string = substring(uniqueString(resourceGroup().id), 1, 6) - -// Environment identifier used to create unique names for resources. -@description('A unique identifier for the environment.') -@minLength(2) -@maxLength(8) -param environmentIdentifier string = '01' - -// The location of resource deployments. Defaults to the location of the resource group. -@description('The location of resource deployments.') -param deploymentLocation string = resourceGroup().location +// @description('Resource group name') +// param resourceGroupName string = 'rg-example-scenario-azure-databricks-online-inference-containers' -//******************************************************** -// Variables -//******************************************************** +// @description('Databricks managed resource group name') +// param mrgDatabricksName string = 'rgm-example-scenario-azure-databricks-online-inference-containers-databricks' -// Search Index Data Reader -var azureRbacSearchIndexDataReaderRoleId = '1407120a-92aa-4202-b7e9-c0e197c71c8f' +// @description('Kubernetes managed resource group name') +// param mrgKubernetesName string = 'rgm-example-scenario-azure-databricks-online-inference-containers-kubernetes' -// Search Service Contributor -var azureRbacSearchServiceContributorRoleId = '7ca78c08-252a-4471-8644-bb5ff32d4ba0' +@description('Location for resources') +param location string = az.resourceGroup().location -// Storage Blob Data Contributor -var azureRbacStorageBlobDataContributorRoleId = 'ba92f5b4-2d11-453d-a403-e96b0029c9fe' - -// Cognitive Services OpenAI Contributor -var azureRbacCognitiveServicesOpenAIContributorRoleId = 'a001fd3d-188f-4b5d-821b-7da978bf7442' +@description('User Object ID for authenticated user') +param userObjectId string //******************************************************** -// Resources +// Variables //******************************************************** -// Azure Storage Account -resource r_storageAccount 'Microsoft.Storage/storageAccounts@2022-05-01' = { - name: 'st${workloadIdentifier}${environmentIdentifier}' - location: deploymentLocation - kind: 'StorageV2' - sku: { - name: 'Standard_LRS' - } - properties: { - encryption: { - services: { - blob: { - enabled: true - } - } - keySource: 'Microsoft.Storage' - } - networkAcls: { - bypass: 'AzureServices' - defaultAction: 'Deny' - ipRules: [] - virtualNetworkRules: [] +var serviceSuffix = substring(uniqueString(az.resourceGroup().id), 0, 5) + +var resources = { + applicationInsightsName: 'appi01${serviceSuffix}' + containerRegistryName: 'cr01${serviceSuffix}' + logAnalyticsWorkspaceName: 'log01${serviceSuffix}' + storageAccountName: 'st01${serviceSuffix}' + userAssignedIdentityName: 'id01${serviceSuffix}' + containerAppEnvironmnetName: 'cae01${serviceSuffix}' + aiSearchName: 'srch01${serviceSuffix}' + openAiName: 'oai01${serviceSuffix}' + aiServicesName: 'aisa01${serviceSuffix}' + deploymentScriptName: 'ds01${serviceSuffix}' +} + +// ******************************************************** +// Modules +// ******************************************************** + +module userAssignedIdentity './modules/user-assigned-identity.bicep' = { + name: '${resources.userAssignedIdentityName}-deployment' + params: { + name: resources.userAssignedIdentityName + location: location + tags: { + environment: 'shared' } - publicNetworkAccess: 'Enabled' - supportsHttpsTrafficOnly: true - allowBlobPublicAccess: false - isHnsEnabled: false - minimumTlsVersion: 'TLS1_2' } } -// Azure AI Search -resource r_aiSearch 'Microsoft.Search/searchServices@2023-11-01' = { - name: 'search${workloadIdentifier}${environmentIdentifier}' - location: deploymentLocation - identity: { - type: 'SystemAssigned' - } - sku: { - name: 'standard' - } - properties: { - networkRuleSet: { - ipRules: [] - } - publicNetworkAccess: 'enabled' - disableLocalAuth: false - authOptions: { - aadOrApiKey: { - aadAuthFailureMode: 'http401WithBearerChallenge' - } +module storageAccount './modules/storage-account.bicep' = { + name: '${resources.storageAccountName}-01-deployment' + params: { + name: resources.storageAccountName + location: location + tags: { + environment: 'shared' } } } -// Azure Open AI Account -resource r_aoaiAccount 'Microsoft.CognitiveServices/accounts@2023-10-01-preview' = { - name: 'aoai${workloadIdentifier}${environmentIdentifier}' - location: deploymentLocation - kind: 'OpenAI' - identity: { - type: 'SystemAssigned' +module storageAccountRoleAssignements './modules/storage-account.bicep' = { + name: '${resources.storageAccountName}-02-deployment' + params: { + name: resources.storageAccountName + location: location + roles: [ + { + principalId: aiSearch.outputs.principalId + id: 'ba92f5b4-2d11-453d-a403-e96b0029c9fe' // Storage Blob Data Contributor + } + { + principalId: userObjectId + id: 'ba92f5b4-2d11-453d-a403-e96b0029c9fe' // Storage Blob Data Contributor + type: 'User' + } + ] } - properties: { - customSubDomainName: 'aoai${workloadIdentifier}${environmentIdentifier}' - publicNetworkAccess: 'Enabled' - networkAcls: { - bypass: 'AzureServices' - defaultAction: 'Deny' - ipRules: [] - virtualNetworkRules: [] +} + +module logAnalyticsWorkspace './modules/log-analytics-workspace.bicep' = { + name: '${resources.logAnalyticsWorkspaceName}-deployment' + params: { + name: resources.logAnalyticsWorkspaceName + location: location + tags: { + environment: 'shared' } - } - sku: { - name: 'S0' + storageAccountId: storageAccount.outputs.id } } -// Define Azure Open AI Account Deployments -var modelDeployments = [ - { - name: 'gpt-35-turbo-16k-0613' - modelName: 'gpt-35-turbo-16k' - modelVersion: '0613' - } - { - name: 'text-embedding-ada-002-2' - modelName: 'text-embedding-ada-002' - modelVersion: '2' - } -] - -// Azure Open AI Account Deployments -@batchSize(1) -resource r_aoaiDeploymentsChat 'Microsoft.CognitiveServices/accounts/deployments@2023-05-01' = [for deployment in modelDeployments: { - parent: r_aoaiAccount - name: deployment.name - properties: { - model: { - format: 'OpenAI' - name: deployment.modelName - version: deployment.modelVersion +module containerRegistry './modules/container-registry.bicep' = { + name: '${resources.containerRegistryName}-deployment' + params: { + name: resources.containerRegistryName + location: location + tags: { + environment: 'shared' } + logAnalyticsWorkspaceId: logAnalyticsWorkspace.outputs.id + roles: [ + { + principalId: userAssignedIdentity.outputs.principalId + id: '7f951dda-4ed3-4680-a7ca-43fe172d538d' // ACR Pull role + } + ] } - sku: { - name: 'Standard' - capacity: 30 - } -}] - -//******************************************************** -// RBAC Role Assignments -//******************************************************** +} -resource r_azureSearchIndexDataReaderAzureOpenAiAssignment 'Microsoft.Authorization/roleAssignments@2020-08-01-preview' = { - name: guid(r_aiSearch.name, r_aoaiAccount.name, 'searchIndexDataReader') - scope: r_aiSearch - properties: { - roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', azureRbacSearchIndexDataReaderRoleId) - principalId: r_aoaiAccount.identity.principalId - principalType: 'ServicePrincipal' +module containerAppsEnvironment './modules/container-app-environment.bicep' = { + name: '${resources.containerAppEnvironmnetName}-deployment' + params: { + name: resources.containerAppEnvironmnetName + location: location + tags: { + environment: 'shared' + } + logAnalyticsWorkspaceName: logAnalyticsWorkspace.outputs.name + logAnalyticsWorkspaceResourceGroupName: az.resourceGroup().name } } -resource r_azureSearchServiceContributorAzureOpenAiAssignment 'Microsoft.Authorization/roleAssignments@2020-08-01-preview' = { - name: guid(r_aiSearch.name, r_aoaiAccount.name, 'searchServiceContributor') - scope: r_aiSearch - properties: { - roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', azureRbacSearchServiceContributorRoleId) - principalId: r_aoaiAccount.identity.principalId - principalType: 'ServicePrincipal' +module aiSearch './modules/ai-search.bicep' = { + name: '${resources.aiSearchName}-deployment' + params: { + name: resources.aiSearchName + location: location + tags: { + environment: 'shared' + } + logAnalyticsWorkspaceId: logAnalyticsWorkspace.outputs.id + roles: [ + { + principalId: userAssignedIdentity.outputs.principalId + id: '7ca78c08-252a-4471-8644-bb5ff32d4ba0' // Search Service Contributor + } + { + principalId: userAssignedIdentity.outputs.principalId + id: '1407120a-92aa-4202-b7e9-c0e197c71c8f' // Search Index Data Reader + } + { + principalId: openAi.outputs.principalId + id: '7ca78c08-252a-4471-8644-bb5ff32d4ba0' // Search Service Contributor + } + { + principalId: openAi.outputs.principalId + id: '1407120a-92aa-4202-b7e9-c0e197c71c8f' // Search Index Data Reader + } + { + principalId: userObjectId + id: '7ca78c08-252a-4471-8644-bb5ff32d4ba0' // Search Service Contributor + type: 'User' + } + { + principalId: userObjectId + id: '8ebe5a00-799e-43f5-93ac-243d3dce84a7' // Search Index Data Contributor + type: 'User' + } + ] } } -resource r_storageBlobDataContributorAzureOpenAiAssignment 'Microsoft.Authorization/roleAssignments@2020-08-01-preview' = { - name: guid(r_storageAccount.name, r_aoaiAccount.name, 'storageBlobDataContributor') - scope: r_storageAccount - properties: { - roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', azureRbacStorageBlobDataContributorRoleId) - principalId: r_aoaiAccount.identity.principalId - principalType: 'ServicePrincipal' +module openAi './modules/ai-services.bicep' = { + name: '${resources.openAiName}-01-deployment' + params: { + name: resources.openAiName + location: location + tags: { + environment: 'shared' + } + kind: 'OpenAI' + logAnalyticsWorkspaceId: logAnalyticsWorkspace.outputs.id + roles: [ + { + principalId: userAssignedIdentity.outputs.principalId + id: 'a001fd3d-188f-4b5d-821b-7da978bf7442' // Cognitive Services OpenAI Contributor + } + { + principalId: userObjectId + id: 'a001fd3d-188f-4b5d-821b-7da978bf7442' // Cognitive Services OpenAI Contributor + type: 'User' + } + ] } } -resource r_cognitiveServicesOpenAiContributorAzureAiSearchAssignment 'Microsoft.Authorization/roleAssignments@2020-08-01-preview' = { - name: guid(r_aoaiAccount.name, r_aiSearch.name, 'cognitiveServicesOpenAiContributor') - scope: r_aoaiAccount - properties: { - roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', azureRbacCognitiveServicesOpenAIContributorRoleId) - principalId: r_aiSearch.identity.principalId - principalType: 'ServicePrincipal' +module openAiRoleAssignments './modules/ai-services.bicep' = { + name: '${resources.openAiName}-02-deployment' + params: { + name: resources.openAiName + location: location + tags: { + environment: 'shared' + } + kind: 'OpenAI' + logAnalyticsWorkspaceId: logAnalyticsWorkspace.outputs.id + roles: [ + { + principalId: aiSearch.outputs.principalId + id: 'a001fd3d-188f-4b5d-821b-7da978bf7442' // Cognitive Services OpenAI Contributor + } + ] } } -resource r_storageBlobDataContributorAzureAiSearchAssignment 'Microsoft.Authorization/roleAssignments@2020-08-01-preview' = { - name: guid(r_storageAccount.name, r_aiSearch.name, 'storageBlobDataContributor') - scope: r_storageAccount - properties: { - roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', azureRbacStorageBlobDataContributorRoleId) - principalId: r_aiSearch.identity.principalId - principalType: 'ServicePrincipal' +module aiServices './modules/ai-services.bicep' = { + name: '${resources.aiServicesName}-deployment' + params: { + name: resources.aiServicesName + location: location + tags: { + environment: 'shared' + } + kind: 'CognitiveServices' + logAnalyticsWorkspaceId: logAnalyticsWorkspace.outputs.id + roles: [ + { + principalId: userAssignedIdentity.outputs.principalId + id: '25fbc0a9-bd7c-42a3-aa1a-3b75d497ee68' // Cognitive Services Contributor + } + ] } } + +//******************************************************** +// Outputs +//******************************************************** + +output storageAccountName string = storageAccount.outputs.name +output logAnalyticsWorkspaceName string = logAnalyticsWorkspace.outputs.name +output containerRegistryName string = containerRegistry.outputs.name +output userAssignedIdentityName string = userAssignedIdentity.outputs.name +output containerAppEnvironmnetStagingName string = containerAppsEnvironment.outputs.name diff --git a/infrastructure/main.json b/infrastructure/main.json index 5059ad9..ecc373c 100644 --- a/infrastructure/main.json +++ b/infrastructure/main.json @@ -4,233 +4,1741 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.24.24.22086", - "templateHash": "12446007199015682608" + "version": "0.27.1.19265", + "templateHash": "12164655026923384675" } }, "parameters": { - "workloadIdentifier": { + "location": { "type": "string", - "defaultValue": "[substring(uniqueString(resourceGroup().id), 1, 6)]", - "minLength": 2, - "maxLength": 6, - "metadata": { - "description": "A unique identifier for the workload." - } - }, - "environmentIdentifier": { - "type": "string", - "defaultValue": "01", - "minLength": 2, - "maxLength": 8, + "defaultValue": "[resourceGroup().location]", "metadata": { - "description": "A unique identifier for the environment." + "description": "Location for resources" } }, - "deploymentLocation": { + "userObjectId": { "type": "string", - "defaultValue": "[resourceGroup().location]", "metadata": { - "description": "The location of resource deployments." + "description": "User Object ID for authenticated user" } } }, "variables": { - "azureRbacSearchIndexDataReaderRoleId": "1407120a-92aa-4202-b7e9-c0e197c71c8f", - "azureRbacSearchServiceContributorRoleId": "7ca78c08-252a-4471-8644-bb5ff32d4ba0", - "azureRbacStorageBlobDataContributorRoleId": "ba92f5b4-2d11-453d-a403-e96b0029c9fe", - "azureRbacCognitiveServicesOpenAIContributorRoleId": "a001fd3d-188f-4b5d-821b-7da978bf7442", - "modelDeployments": [ - { - "name": "gpt-35-turbo-16k-0613", - "modelName": "gpt-35-turbo-16k", - "modelVersion": "0613" - }, - { - "name": "text-embedding-ada-002-2", - "modelName": "text-embedding-ada-002", - "modelVersion": "2" - } - ] + "serviceSuffix": "[substring(uniqueString(resourceGroup().id), 0, 5)]", + "resources": { + "applicationInsightsName": "[format('appi01{0}', variables('serviceSuffix'))]", + "containerRegistryName": "[format('cr01{0}', variables('serviceSuffix'))]", + "logAnalyticsWorkspaceName": "[format('log01{0}', variables('serviceSuffix'))]", + "storageAccountName": "[format('st01{0}', variables('serviceSuffix'))]", + "userAssignedIdentityName": "[format('id01{0}', variables('serviceSuffix'))]", + "containerAppEnvironmnetName": "[format('cae01{0}', variables('serviceSuffix'))]", + "aiSearchName": "[format('srch01{0}', variables('serviceSuffix'))]", + "openAiName": "[format('oai01{0}', variables('serviceSuffix'))]", + "aiServicesName": "[format('aisa01{0}', variables('serviceSuffix'))]", + "deploymentScriptName": "[format('ds01{0}', variables('serviceSuffix'))]" + } }, "resources": [ { - "type": "Microsoft.Storage/storageAccounts", - "apiVersion": "2022-05-01", - "name": "[format('st{0}{1}', parameters('workloadIdentifier'), parameters('environmentIdentifier'))]", - "location": "[parameters('deploymentLocation')]", - "kind": "StorageV2", - "sku": { - "name": "Standard_LRS" - }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-deployment', variables('resources').userAssignedIdentityName)]", "properties": { - "encryption": { - "services": { - "blob": { - "enabled": true - } - }, - "keySource": "Microsoft.Storage" + "expressionEvaluationOptions": { + "scope": "inner" }, - "networkAcls": { - "bypass": "AzureServices", - "defaultAction": "Deny", - "ipRules": [], - "virtualNetworkRules": [] + "mode": "Incremental", + "parameters": { + "name": { + "value": "[variables('resources').userAssignedIdentityName]" + }, + "location": { + "value": "[parameters('location')]" + }, + "tags": { + "value": { + "environment": "shared" + } + } }, - "publicNetworkAccess": "Enabled", - "supportsHttpsTrafficOnly": true, - "allowBlobPublicAccess": false, - "isHnsEnabled": false, - "minimumTlsVersion": "TLS1_2" + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.27.1.19265", + "templateHash": "3718297678080327579" + } + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Name of the User Assigned Identity service" + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Location for User Assigned Identity service" + } + }, + "tags": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Tags for the User Assigned Identity service" + } + } + }, + "resources": [ + { + "type": "Microsoft.ManagedIdentity/userAssignedIdentities", + "apiVersion": "2023-01-31", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]" + } + ], + "outputs": { + "id": { + "type": "string", + "value": "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('name'))]" + }, + "name": { + "type": "string", + "value": "[parameters('name')]" + }, + "principalId": { + "type": "string", + "value": "[reference(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('name')), '2023-01-31').principalId]" + } + } + } } }, { - "type": "Microsoft.Search/searchServices", - "apiVersion": "2023-11-01", - "name": "[format('search{0}{1}', parameters('workloadIdentifier'), parameters('environmentIdentifier'))]", - "location": "[parameters('deploymentLocation')]", - "identity": { - "type": "SystemAssigned" - }, - "sku": { - "name": "standard" - }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-01-deployment', variables('resources').storageAccountName)]", "properties": { - "networkRuleSet": { - "ipRules": [] + "expressionEvaluationOptions": { + "scope": "inner" }, - "publicNetworkAccess": "enabled", - "disableLocalAuth": false, - "authOptions": { - "aadOrApiKey": { - "aadAuthFailureMode": "http401WithBearerChallenge" + "mode": "Incremental", + "parameters": { + "name": { + "value": "[variables('resources').storageAccountName]" + }, + "location": { + "value": "[parameters('location')]" + }, + "tags": { + "value": { + "environment": "shared" + } + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.27.1.19265", + "templateHash": "14075893523994139509" + } + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Name of the Storage service" + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Location for Storage service" + } + }, + "tags": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Tags for the Storage service" + } + }, + "roles": { + "type": "array", + "defaultValue": [], + "metadata": { + "description": "Role assignments for the Storage service" + } + }, + "enableHns": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Enable hierarchical namespace" + } + } + }, + "resources": [ + { + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2022-05-01", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "sku": { + "name": "Standard_LRS" + }, + "kind": "StorageV2", + "properties": { + "isHnsEnabled": "[parameters('enableHns')]", + "minimumTlsVersion": "TLS1_2" + } + }, + { + "copy": { + "name": "roleAssignments", + "count": "[length(parameters('roles'))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}', parameters('name'))]", + "name": "[guid(parameters('name'), parameters('roles')[copyIndex()].principalId, parameters('roles')[copyIndex()].id)]", + "properties": { + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', parameters('roles')[copyIndex()].id)]", + "principalId": "[parameters('roles')[copyIndex()].principalId]", + "principalType": "[if(contains(parameters('roles')[copyIndex()], 'type'), parameters('roles')[copyIndex()].type, 'ServicePrincipal')]" + }, + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts', parameters('name'))]" + ] + }, + { + "type": "Microsoft.Resources/deploymentScripts", + "apiVersion": "2020-10-01", + "name": "[format('ds{0}', parameters('name'))]", + "location": "[parameters('location')]", + "kind": "AzureCLI", + "properties": { + "azCliVersion": "2.30.0", + "timeout": "PT5M", + "retentionInterval": "PT1H", + "environmentVariables": [ + { + "name": "AZURE_STORAGE_ACCOUNT", + "value": "[parameters('name')]" + }, + { + "name": "AZURE_STORAGE_KEY", + "secureValue": "[listKeys(resourceId('Microsoft.Storage/storageAccounts', parameters('name')), '2022-05-01').keys[0].value]" + } + ], + "scriptContent": " git clone https://github.com/nfmoore/azure-open-ai-example-scenarios.git\n az storage container create --name data --account-name $AZURE_STORAGE_ACCOUNT --account-key $AZURE_STORAGE_KEY \n az storage blob upload-batch --destination data --account-name $AZURE_STORAGE_ACCOUNT --account-key $AZURE_STORAGE_KEY --destination-path ./ --source ./azure-open-ai-example-scenarios/data\n " + }, + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts', parameters('name'))]" + ] + } + ], + "outputs": { + "name": { + "type": "string", + "value": "[parameters('name')]" + }, + "id": { + "type": "string", + "value": "[resourceId('Microsoft.Storage/storageAccounts', parameters('name'))]" + } } } } }, { - "type": "Microsoft.CognitiveServices/accounts", - "apiVersion": "2023-10-01-preview", - "name": "[format('aoai{0}{1}', parameters('workloadIdentifier'), parameters('environmentIdentifier'))]", - "location": "[parameters('deploymentLocation')]", - "kind": "OpenAI", - "identity": { - "type": "SystemAssigned" - }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-02-deployment', variables('resources').storageAccountName)]", "properties": { - "customSubDomainName": "[format('aoai{0}{1}', parameters('workloadIdentifier'), parameters('environmentIdentifier'))]", - "publicNetworkAccess": "Enabled", - "networkAcls": { - "bypass": "AzureServices", - "defaultAction": "Deny", - "ipRules": [], - "virtualNetworkRules": [] + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[variables('resources').storageAccountName]" + }, + "location": { + "value": "[parameters('location')]" + }, + "roles": { + "value": [ + { + "principalId": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-deployment', variables('resources').aiSearchName)), '2022-09-01').outputs.principalId.value]", + "id": "ba92f5b4-2d11-453d-a403-e96b0029c9fe" + }, + { + "principalId": "[parameters('userObjectId')]", + "id": "ba92f5b4-2d11-453d-a403-e96b0029c9fe", + "type": "User" + } + ] + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.27.1.19265", + "templateHash": "14075893523994139509" + } + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Name of the Storage service" + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Location for Storage service" + } + }, + "tags": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Tags for the Storage service" + } + }, + "roles": { + "type": "array", + "defaultValue": [], + "metadata": { + "description": "Role assignments for the Storage service" + } + }, + "enableHns": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Enable hierarchical namespace" + } + } + }, + "resources": [ + { + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2022-05-01", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "sku": { + "name": "Standard_LRS" + }, + "kind": "StorageV2", + "properties": { + "isHnsEnabled": "[parameters('enableHns')]", + "minimumTlsVersion": "TLS1_2" + } + }, + { + "copy": { + "name": "roleAssignments", + "count": "[length(parameters('roles'))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}', parameters('name'))]", + "name": "[guid(parameters('name'), parameters('roles')[copyIndex()].principalId, parameters('roles')[copyIndex()].id)]", + "properties": { + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', parameters('roles')[copyIndex()].id)]", + "principalId": "[parameters('roles')[copyIndex()].principalId]", + "principalType": "[if(contains(parameters('roles')[copyIndex()], 'type'), parameters('roles')[copyIndex()].type, 'ServicePrincipal')]" + }, + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts', parameters('name'))]" + ] + }, + { + "type": "Microsoft.Resources/deploymentScripts", + "apiVersion": "2020-10-01", + "name": "[format('ds{0}', parameters('name'))]", + "location": "[parameters('location')]", + "kind": "AzureCLI", + "properties": { + "azCliVersion": "2.30.0", + "timeout": "PT5M", + "retentionInterval": "PT1H", + "environmentVariables": [ + { + "name": "AZURE_STORAGE_ACCOUNT", + "value": "[parameters('name')]" + }, + { + "name": "AZURE_STORAGE_KEY", + "secureValue": "[listKeys(resourceId('Microsoft.Storage/storageAccounts', parameters('name')), '2022-05-01').keys[0].value]" + } + ], + "scriptContent": " git clone https://github.com/nfmoore/azure-open-ai-example-scenarios.git\n az storage container create --name data --account-name $AZURE_STORAGE_ACCOUNT --account-key $AZURE_STORAGE_KEY \n az storage blob upload-batch --destination data --account-name $AZURE_STORAGE_ACCOUNT --account-key $AZURE_STORAGE_KEY --destination-path ./ --source ./azure-open-ai-example-scenarios/data\n " + }, + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts', parameters('name'))]" + ] + } + ], + "outputs": { + "name": { + "type": "string", + "value": "[parameters('name')]" + }, + "id": { + "type": "string", + "value": "[resourceId('Microsoft.Storage/storageAccounts', parameters('name'))]" + } + } } }, - "sku": { - "name": "S0" - } + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', format('{0}-deployment', variables('resources').aiSearchName))]" + ] }, { - "copy": { - "name": "r_aoaiDeploymentsChat", - "count": "[length(variables('modelDeployments'))]", - "mode": "serial", - "batchSize": 1 - }, - "type": "Microsoft.CognitiveServices/accounts/deployments", - "apiVersion": "2023-05-01", - "name": "[format('{0}/{1}', format('aoai{0}{1}', parameters('workloadIdentifier'), parameters('environmentIdentifier')), variables('modelDeployments')[copyIndex()].name)]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-deployment', variables('resources').logAnalyticsWorkspaceName)]", "properties": { - "model": { - "format": "OpenAI", - "name": "[variables('modelDeployments')[copyIndex()].modelName]", - "version": "[variables('modelDeployments')[copyIndex()].modelVersion]" + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[variables('resources').logAnalyticsWorkspaceName]" + }, + "location": { + "value": "[parameters('location')]" + }, + "tags": { + "value": { + "environment": "shared" + } + }, + "storageAccountId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-01-deployment', variables('resources').storageAccountName)), '2022-09-01').outputs.id.value]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.27.1.19265", + "templateHash": "8716779443418268881" + } + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Name of the Log Analytics workspace" + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Location for Log Analytics workspace" + } + }, + "tags": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Tags for the Log Analytics workspace" + } + }, + "roles": { + "type": "array", + "defaultValue": [], + "metadata": { + "description": "Role assignments for the Log Analytics workspace" + } + }, + "storageAccountId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Storage account ID to link to the Log Analytics workspace" + } + } + }, + "resources": [ + { + "type": "Microsoft.OperationalInsights/workspaces/linkedStorageAccounts", + "apiVersion": "2020-08-01", + "name": "[format('{0}/{1}', parameters('name'), 'Alerts')]", + "properties": { + "storageAccountIds": [ + "[parameters('storageAccountId')]" + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.OperationalInsights/workspaces', parameters('name'))]" + ] + }, + { + "type": "Microsoft.OperationalInsights/workspaces/linkedStorageAccounts", + "apiVersion": "2020-08-01", + "name": "[format('{0}/{1}', parameters('name'), 'CustomLogs')]", + "properties": { + "storageAccountIds": [ + "[parameters('storageAccountId')]" + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.OperationalInsights/workspaces', parameters('name'))]" + ] + }, + { + "type": "Microsoft.OperationalInsights/workspaces/linkedStorageAccounts", + "apiVersion": "2020-08-01", + "name": "[format('{0}/{1}', parameters('name'), 'Ingestion')]", + "properties": { + "storageAccountIds": [ + "[parameters('storageAccountId')]" + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.OperationalInsights/workspaces', parameters('name'))]" + ] + }, + { + "type": "Microsoft.OperationalInsights/workspaces/linkedStorageAccounts", + "apiVersion": "2020-08-01", + "name": "[format('{0}/{1}', parameters('name'), 'Query')]", + "properties": { + "storageAccountIds": [ + "[parameters('storageAccountId')]" + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.OperationalInsights/workspaces', parameters('name'))]" + ] + }, + { + "type": "Microsoft.OperationalInsights/workspaces", + "apiVersion": "2022-10-01", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "retentionInDays": 30, + "sku": { + "name": "Standalone" + } + } + }, + { + "copy": { + "name": "roleAssignments", + "count": "[length(parameters('roles'))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.OperationalInsights/workspaces/{0}', parameters('name'))]", + "name": "[guid(parameters('name'), parameters('roles')[copyIndex()].principalId, parameters('roles')[copyIndex()].id)]", + "properties": { + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', parameters('roles')[copyIndex()].id)]", + "principalId": "[parameters('roles')[copyIndex()].principalId]", + "principalType": "[if(contains(parameters('roles')[copyIndex()], 'type'), parameters('roles')[copyIndex()].type, 'ServicePrincipal')]" + }, + "dependsOn": [ + "[resourceId('Microsoft.OperationalInsights/workspaces', parameters('name'))]" + ] + } + ], + "outputs": { + "name": { + "type": "string", + "value": "[parameters('name')]" + }, + "id": { + "type": "string", + "value": "[resourceId('Microsoft.OperationalInsights/workspaces', parameters('name'))]" + } + } } }, - "sku": { - "name": "Standard", - "capacity": 30 + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', format('{0}-01-deployment', variables('resources').storageAccountName))]" + ] + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-deployment', variables('resources').containerRegistryName)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[variables('resources').containerRegistryName]" + }, + "location": { + "value": "[parameters('location')]" + }, + "tags": { + "value": { + "environment": "shared" + } + }, + "logAnalyticsWorkspaceId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-deployment', variables('resources').logAnalyticsWorkspaceName)), '2022-09-01').outputs.id.value]" + }, + "roles": { + "value": [ + { + "principalId": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-deployment', variables('resources').userAssignedIdentityName)), '2022-09-01').outputs.principalId.value]", + "id": "7f951dda-4ed3-4680-a7ca-43fe172d538d" + } + ] + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.27.1.19265", + "templateHash": "2880683818389779110" + } + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Name of the Container Registry service" + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Location for Container Registry service" + } + }, + "tags": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Tags for the Container Registry service" + } + }, + "roles": { + "type": "array", + "defaultValue": [], + "metadata": { + "description": "Role assignments for the Container Registry service" + } + }, + "logAnalyticsWorkspaceId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Log Analytics workspace ID for diagnostics" + } + } + }, + "resources": [ + { + "type": "Microsoft.ContainerRegistry/registries", + "apiVersion": "2022-12-01", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "sku": { + "name": "Standard" + }, + "properties": { + "adminUserEnabled": true + } + }, + { + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[format('Microsoft.ContainerRegistry/registries/{0}', parameters('name'))]", + "name": "all-logs-all-metrics", + "properties": { + "workspaceId": "[parameters('logAnalyticsWorkspaceId')]", + "logs": [ + { + "categoryGroup": "allLogs", + "enabled": true + } + ], + "metrics": [ + { + "category": "AllMetrics", + "enabled": true + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.ContainerRegistry/registries', parameters('name'))]" + ] + }, + { + "copy": { + "name": "roleAssignments", + "count": "[length(parameters('roles'))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.ContainerRegistry/registries/{0}', parameters('name'))]", + "name": "[guid(parameters('name'), parameters('roles')[copyIndex()].principalId, parameters('roles')[copyIndex()].id)]", + "properties": { + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', parameters('roles')[copyIndex()].id)]", + "principalId": "[parameters('roles')[copyIndex()].principalId]", + "principalType": "[if(contains(parameters('roles')[copyIndex()], 'type'), parameters('roles')[copyIndex()].type, 'ServicePrincipal')]" + }, + "dependsOn": [ + "[resourceId('Microsoft.ContainerRegistry/registries', parameters('name'))]" + ] + } + ], + "outputs": { + "name": { + "type": "string", + "value": "[parameters('name')]" + }, + "id": { + "type": "string", + "value": "[resourceId('Microsoft.ContainerRegistry/registries', parameters('name'))]" + } + } + } }, "dependsOn": [ - "[resourceId('Microsoft.CognitiveServices/accounts', format('aoai{0}{1}', parameters('workloadIdentifier'), parameters('environmentIdentifier')))]" + "[resourceId('Microsoft.Resources/deployments', format('{0}-deployment', variables('resources').logAnalyticsWorkspaceName))]", + "[resourceId('Microsoft.Resources/deployments', format('{0}-deployment', variables('resources').userAssignedIdentityName))]" ] }, { - "type": "Microsoft.Authorization/roleAssignments", - "apiVersion": "2020-08-01-preview", - "scope": "[format('Microsoft.Search/searchServices/{0}', format('search{0}{1}', parameters('workloadIdentifier'), parameters('environmentIdentifier')))]", - "name": "[guid(format('search{0}{1}', parameters('workloadIdentifier'), parameters('environmentIdentifier')), format('aoai{0}{1}', parameters('workloadIdentifier'), parameters('environmentIdentifier')), 'searchIndexDataReader')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-deployment', variables('resources').containerAppEnvironmnetName)]", "properties": { - "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', variables('azureRbacSearchIndexDataReaderRoleId'))]", - "principalId": "[reference(resourceId('Microsoft.CognitiveServices/accounts', format('aoai{0}{1}', parameters('workloadIdentifier'), parameters('environmentIdentifier'))), '2023-10-01-preview', 'full').identity.principalId]", - "principalType": "ServicePrincipal" + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[variables('resources').containerAppEnvironmnetName]" + }, + "location": { + "value": "[parameters('location')]" + }, + "tags": { + "value": { + "environment": "shared" + } + }, + "logAnalyticsWorkspaceName": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-deployment', variables('resources').logAnalyticsWorkspaceName)), '2022-09-01').outputs.name.value]" + }, + "logAnalyticsWorkspaceResourceGroupName": { + "value": "[resourceGroup().name]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.27.1.19265", + "templateHash": "12781803292218525281" + } + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Name of the Application Insights service" + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Location for Application Insights service" + } + }, + "tags": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Tags for the Application Insights service" + } + }, + "roles": { + "type": "array", + "defaultValue": [], + "metadata": { + "description": "Role assignments for the Application Insights service" + } + }, + "logAnalyticsWorkspaceName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Log Analytics workspace name" + } + }, + "logAnalyticsWorkspaceResourceGroupName": { + "type": "string", + "defaultValue": "[resourceGroup().name]", + "metadata": { + "description": "Log Analytics workspace resource group name" + } + } + }, + "resources": [ + { + "type": "Microsoft.App/managedEnvironments", + "apiVersion": "2023-05-01", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "appLogsConfiguration": { + "destination": "log-analytics", + "logAnalyticsConfiguration": { + "customerId": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('logAnalyticsWorkspaceResourceGroupName')), 'Microsoft.OperationalInsights/workspaces', parameters('logAnalyticsWorkspaceName')), '2022-10-01').customerId]", + "sharedKey": "[listKeys(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('logAnalyticsWorkspaceResourceGroupName')), 'Microsoft.OperationalInsights/workspaces', parameters('logAnalyticsWorkspaceName')), '2022-10-01').primarySharedKey]" + } + } + } + }, + { + "copy": { + "name": "roleAssignments", + "count": "[length(parameters('roles'))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.App/managedEnvironments/{0}', parameters('name'))]", + "name": "[guid(parameters('name'), parameters('roles')[copyIndex()].principalId, parameters('roles')[copyIndex()].id)]", + "properties": { + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', parameters('roles')[copyIndex()].id)]", + "principalId": "[parameters('roles')[copyIndex()].principalId]", + "principalType": "[if(contains(parameters('roles')[copyIndex()], 'type'), parameters('roles')[copyIndex()].type, 'ServicePrincipal')]" + }, + "dependsOn": [ + "[resourceId('Microsoft.App/managedEnvironments', parameters('name'))]" + ] + } + ], + "outputs": { + "name": { + "type": "string", + "value": "[parameters('name')]" + }, + "id": { + "type": "string", + "value": "[resourceId('Microsoft.App/managedEnvironments', parameters('name'))]" + } + } + } }, "dependsOn": [ - "[resourceId('Microsoft.Search/searchServices', format('search{0}{1}', parameters('workloadIdentifier'), parameters('environmentIdentifier')))]", - "[resourceId('Microsoft.CognitiveServices/accounts', format('aoai{0}{1}', parameters('workloadIdentifier'), parameters('environmentIdentifier')))]" + "[resourceId('Microsoft.Resources/deployments', format('{0}-deployment', variables('resources').logAnalyticsWorkspaceName))]" ] }, { - "type": "Microsoft.Authorization/roleAssignments", - "apiVersion": "2020-08-01-preview", - "scope": "[format('Microsoft.Search/searchServices/{0}', format('search{0}{1}', parameters('workloadIdentifier'), parameters('environmentIdentifier')))]", - "name": "[guid(format('search{0}{1}', parameters('workloadIdentifier'), parameters('environmentIdentifier')), format('aoai{0}{1}', parameters('workloadIdentifier'), parameters('environmentIdentifier')), 'searchServiceContributor')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-deployment', variables('resources').aiSearchName)]", "properties": { - "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', variables('azureRbacSearchServiceContributorRoleId'))]", - "principalId": "[reference(resourceId('Microsoft.CognitiveServices/accounts', format('aoai{0}{1}', parameters('workloadIdentifier'), parameters('environmentIdentifier'))), '2023-10-01-preview', 'full').identity.principalId]", - "principalType": "ServicePrincipal" + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[variables('resources').aiSearchName]" + }, + "location": { + "value": "[parameters('location')]" + }, + "tags": { + "value": { + "environment": "shared" + } + }, + "logAnalyticsWorkspaceId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-deployment', variables('resources').logAnalyticsWorkspaceName)), '2022-09-01').outputs.id.value]" + }, + "roles": { + "value": [ + { + "principalId": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-deployment', variables('resources').userAssignedIdentityName)), '2022-09-01').outputs.principalId.value]", + "id": "7ca78c08-252a-4471-8644-bb5ff32d4ba0" + }, + { + "principalId": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-deployment', variables('resources').userAssignedIdentityName)), '2022-09-01').outputs.principalId.value]", + "id": "1407120a-92aa-4202-b7e9-c0e197c71c8f" + }, + { + "principalId": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-01-deployment', variables('resources').openAiName)), '2022-09-01').outputs.principalId.value]", + "id": "7ca78c08-252a-4471-8644-bb5ff32d4ba0" + }, + { + "principalId": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-01-deployment', variables('resources').openAiName)), '2022-09-01').outputs.principalId.value]", + "id": "1407120a-92aa-4202-b7e9-c0e197c71c8f" + }, + { + "principalId": "[parameters('userObjectId')]", + "id": "7ca78c08-252a-4471-8644-bb5ff32d4ba0", + "type": "User" + }, + { + "principalId": "[parameters('userObjectId')]", + "id": "8ebe5a00-799e-43f5-93ac-243d3dce84a7", + "type": "User" + } + ] + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.27.1.19265", + "templateHash": "10031389011035315454" + } + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Name of the Azure AI Search service" + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Location for the Azure AI Search service" + } + }, + "tags": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Tags for the Azure AI Search service" + } + }, + "roles": { + "type": "array", + "defaultValue": [], + "metadata": { + "description": "Role assignments for the Azure AI Search service" + } + }, + "logAnalyticsWorkspaceId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Log Analytics workspace ID for diagnostics" + } + } + }, + "resources": [ + { + "type": "Microsoft.Search/searchServices", + "apiVersion": "2023-11-01", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "sku": { + "name": "standard" + }, + "identity": { + "type": "SystemAssigned" + }, + "properties": { + "authOptions": { + "aadOrApiKey": { + "aadAuthFailureMode": "http401WithBearerChallenge" + } + } + } + }, + { + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[format('Microsoft.Search/searchServices/{0}', parameters('name'))]", + "name": "all-logs-all-metrics", + "properties": { + "workspaceId": "[parameters('logAnalyticsWorkspaceId')]", + "logs": [ + { + "categoryGroup": "allLogs", + "enabled": true + } + ], + "metrics": [ + { + "category": "AllMetrics", + "enabled": true + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Search/searchServices', parameters('name'))]" + ] + }, + { + "copy": { + "name": "roleAssignments", + "count": "[length(parameters('roles'))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Search/searchServices/{0}', parameters('name'))]", + "name": "[guid(parameters('name'), parameters('roles')[copyIndex()].principalId, parameters('roles')[copyIndex()].id)]", + "properties": { + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', parameters('roles')[copyIndex()].id)]", + "principalId": "[parameters('roles')[copyIndex()].principalId]", + "principalType": "[if(contains(parameters('roles')[copyIndex()], 'type'), parameters('roles')[copyIndex()].type, 'ServicePrincipal')]" + }, + "dependsOn": [ + "[resourceId('Microsoft.Search/searchServices', parameters('name'))]" + ] + } + ], + "outputs": { + "name": { + "type": "string", + "value": "[parameters('name')]" + }, + "id": { + "type": "string", + "value": "[resourceId('Microsoft.Search/searchServices', parameters('name'))]" + }, + "principalId": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Search/searchServices', parameters('name')), '2023-11-01', 'full').identity.principalId]" + } + } + } }, "dependsOn": [ - "[resourceId('Microsoft.Search/searchServices', format('search{0}{1}', parameters('workloadIdentifier'), parameters('environmentIdentifier')))]", - "[resourceId('Microsoft.CognitiveServices/accounts', format('aoai{0}{1}', parameters('workloadIdentifier'), parameters('environmentIdentifier')))]" + "[resourceId('Microsoft.Resources/deployments', format('{0}-deployment', variables('resources').logAnalyticsWorkspaceName))]", + "[resourceId('Microsoft.Resources/deployments', format('{0}-01-deployment', variables('resources').openAiName))]", + "[resourceId('Microsoft.Resources/deployments', format('{0}-deployment', variables('resources').userAssignedIdentityName))]" ] }, { - "type": "Microsoft.Authorization/roleAssignments", - "apiVersion": "2020-08-01-preview", - "scope": "[format('Microsoft.Storage/storageAccounts/{0}', format('st{0}{1}', parameters('workloadIdentifier'), parameters('environmentIdentifier')))]", - "name": "[guid(format('st{0}{1}', parameters('workloadIdentifier'), parameters('environmentIdentifier')), format('aoai{0}{1}', parameters('workloadIdentifier'), parameters('environmentIdentifier')), 'storageBlobDataContributor')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-01-deployment', variables('resources').openAiName)]", "properties": { - "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', variables('azureRbacStorageBlobDataContributorRoleId'))]", - "principalId": "[reference(resourceId('Microsoft.CognitiveServices/accounts', format('aoai{0}{1}', parameters('workloadIdentifier'), parameters('environmentIdentifier'))), '2023-10-01-preview', 'full').identity.principalId]", - "principalType": "ServicePrincipal" + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[variables('resources').openAiName]" + }, + "location": { + "value": "[parameters('location')]" + }, + "tags": { + "value": { + "environment": "shared" + } + }, + "kind": { + "value": "OpenAI" + }, + "logAnalyticsWorkspaceId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-deployment', variables('resources').logAnalyticsWorkspaceName)), '2022-09-01').outputs.id.value]" + }, + "roles": { + "value": [ + { + "principalId": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-deployment', variables('resources').userAssignedIdentityName)), '2022-09-01').outputs.principalId.value]", + "id": "a001fd3d-188f-4b5d-821b-7da978bf7442" + }, + { + "principalId": "[parameters('userObjectId')]", + "id": "a001fd3d-188f-4b5d-821b-7da978bf7442", + "type": "User" + } + ] + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.27.1.19265", + "templateHash": "11553281893512969426" + } + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Name of the AI Services account" + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Location for AI Services account" + } + }, + "tags": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Tags for the AI Services account" + } + }, + "kind": { + "type": "string", + "defaultValue": "CognitiveServices", + "metadata": { + "description": "Kind of AI Services account" + } + }, + "roles": { + "type": "array", + "defaultValue": [], + "metadata": { + "description": "Role assignments for the Azure Search account" + } + }, + "logAnalyticsWorkspaceId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Log Analytics workspace ID for diagnostics" + } + }, + "deployments": { + "type": "array", + "defaultValue": [ + { + "name": "gpt-4-32k", + "version": "0613" + }, + { + "name": "gpt-35-turbo-16k", + "version": "0613" + }, + { + "name": "text-embedding-ada-002", + "version": "2" + } + ], + "metadata": { + "description": "Deployments for the AI Services account" + } + } + }, + "resources": [ + { + "copy": { + "name": "aisaDeployments", + "count": "[length(parameters('deployments'))]", + "mode": "serial", + "batchSize": 1 + }, + "condition": "[equals(parameters('kind'), 'OpenAI')]", + "type": "Microsoft.CognitiveServices/accounts/deployments", + "apiVersion": "2023-05-01", + "name": "[format('{0}/{1}', parameters('name'), format('{0}-{1}', parameters('deployments')[copyIndex()].name, parameters('deployments')[copyIndex()].version))]", + "properties": { + "model": { + "format": "OpenAI", + "name": "[parameters('deployments')[copyIndex()].name]", + "version": "[parameters('deployments')[copyIndex()].version]" + }, + "versionUpgradeOption": "NoAutoUpgrade" + }, + "sku": { + "name": "Standard", + "capacity": "[if(contains(parameters('deployments')[copyIndex()], 'capacity'), parameters('deployments')[copyIndex()].capacity, 20)]" + }, + "dependsOn": [ + "[resourceId('Microsoft.CognitiveServices/accounts', parameters('name'))]" + ] + }, + { + "type": "Microsoft.CognitiveServices/accounts", + "apiVersion": "2023-05-01", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "sku": { + "name": "S0" + }, + "kind": "[parameters('kind')]", + "identity": { + "type": "SystemAssigned" + }, + "properties": { + "customSubDomainName": "[parameters('name')]" + } + }, + { + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[format('Microsoft.CognitiveServices/accounts/{0}', parameters('name'))]", + "name": "all-logs-all-metrics", + "properties": { + "workspaceId": "[parameters('logAnalyticsWorkspaceId')]", + "logs": [ + { + "categoryGroup": "allLogs", + "enabled": true + } + ], + "metrics": [ + { + "category": "AllMetrics", + "enabled": true + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.CognitiveServices/accounts', parameters('name'))]" + ] + }, + { + "copy": { + "name": "roleAssignments", + "count": "[length(parameters('roles'))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.CognitiveServices/accounts/{0}', parameters('name'))]", + "name": "[guid(parameters('name'), parameters('roles')[copyIndex()].principalId, parameters('roles')[copyIndex()].id)]", + "properties": { + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', parameters('roles')[copyIndex()].id)]", + "principalId": "[parameters('roles')[copyIndex()].principalId]", + "principalType": "[if(contains(parameters('roles')[copyIndex()], 'type'), parameters('roles')[copyIndex()].type, 'ServicePrincipal')]" + }, + "dependsOn": [ + "[resourceId('Microsoft.CognitiveServices/accounts', parameters('name'))]" + ] + } + ], + "outputs": { + "name": { + "type": "string", + "value": "[parameters('name')]" + }, + "id": { + "type": "string", + "value": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('name'))]" + }, + "principalId": { + "type": "string", + "value": "[reference(resourceId('Microsoft.CognitiveServices/accounts', parameters('name')), '2023-05-01', 'full').identity.principalId]" + } + } + } }, "dependsOn": [ - "[resourceId('Microsoft.CognitiveServices/accounts', format('aoai{0}{1}', parameters('workloadIdentifier'), parameters('environmentIdentifier')))]", - "[resourceId('Microsoft.Storage/storageAccounts', format('st{0}{1}', parameters('workloadIdentifier'), parameters('environmentIdentifier')))]" + "[resourceId('Microsoft.Resources/deployments', format('{0}-deployment', variables('resources').logAnalyticsWorkspaceName))]", + "[resourceId('Microsoft.Resources/deployments', format('{0}-deployment', variables('resources').userAssignedIdentityName))]" ] }, { - "type": "Microsoft.Authorization/roleAssignments", - "apiVersion": "2020-08-01-preview", - "scope": "[format('Microsoft.CognitiveServices/accounts/{0}', format('aoai{0}{1}', parameters('workloadIdentifier'), parameters('environmentIdentifier')))]", - "name": "[guid(format('aoai{0}{1}', parameters('workloadIdentifier'), parameters('environmentIdentifier')), format('search{0}{1}', parameters('workloadIdentifier'), parameters('environmentIdentifier')), 'cognitiveServicesOpenAiContributor')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-02-deployment', variables('resources').openAiName)]", "properties": { - "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', variables('azureRbacCognitiveServicesOpenAIContributorRoleId'))]", - "principalId": "[reference(resourceId('Microsoft.Search/searchServices', format('search{0}{1}', parameters('workloadIdentifier'), parameters('environmentIdentifier'))), '2023-11-01', 'full').identity.principalId]", - "principalType": "ServicePrincipal" + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[variables('resources').openAiName]" + }, + "location": { + "value": "[parameters('location')]" + }, + "tags": { + "value": { + "environment": "shared" + } + }, + "kind": { + "value": "OpenAI" + }, + "logAnalyticsWorkspaceId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-deployment', variables('resources').logAnalyticsWorkspaceName)), '2022-09-01').outputs.id.value]" + }, + "roles": { + "value": [ + { + "principalId": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-deployment', variables('resources').aiSearchName)), '2022-09-01').outputs.principalId.value]", + "id": "a001fd3d-188f-4b5d-821b-7da978bf7442" + } + ] + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.27.1.19265", + "templateHash": "11553281893512969426" + } + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Name of the AI Services account" + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Location for AI Services account" + } + }, + "tags": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Tags for the AI Services account" + } + }, + "kind": { + "type": "string", + "defaultValue": "CognitiveServices", + "metadata": { + "description": "Kind of AI Services account" + } + }, + "roles": { + "type": "array", + "defaultValue": [], + "metadata": { + "description": "Role assignments for the Azure Search account" + } + }, + "logAnalyticsWorkspaceId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Log Analytics workspace ID for diagnostics" + } + }, + "deployments": { + "type": "array", + "defaultValue": [ + { + "name": "gpt-4-32k", + "version": "0613" + }, + { + "name": "gpt-35-turbo-16k", + "version": "0613" + }, + { + "name": "text-embedding-ada-002", + "version": "2" + } + ], + "metadata": { + "description": "Deployments for the AI Services account" + } + } + }, + "resources": [ + { + "copy": { + "name": "aisaDeployments", + "count": "[length(parameters('deployments'))]", + "mode": "serial", + "batchSize": 1 + }, + "condition": "[equals(parameters('kind'), 'OpenAI')]", + "type": "Microsoft.CognitiveServices/accounts/deployments", + "apiVersion": "2023-05-01", + "name": "[format('{0}/{1}', parameters('name'), format('{0}-{1}', parameters('deployments')[copyIndex()].name, parameters('deployments')[copyIndex()].version))]", + "properties": { + "model": { + "format": "OpenAI", + "name": "[parameters('deployments')[copyIndex()].name]", + "version": "[parameters('deployments')[copyIndex()].version]" + }, + "versionUpgradeOption": "NoAutoUpgrade" + }, + "sku": { + "name": "Standard", + "capacity": "[if(contains(parameters('deployments')[copyIndex()], 'capacity'), parameters('deployments')[copyIndex()].capacity, 20)]" + }, + "dependsOn": [ + "[resourceId('Microsoft.CognitiveServices/accounts', parameters('name'))]" + ] + }, + { + "type": "Microsoft.CognitiveServices/accounts", + "apiVersion": "2023-05-01", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "sku": { + "name": "S0" + }, + "kind": "[parameters('kind')]", + "identity": { + "type": "SystemAssigned" + }, + "properties": { + "customSubDomainName": "[parameters('name')]" + } + }, + { + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[format('Microsoft.CognitiveServices/accounts/{0}', parameters('name'))]", + "name": "all-logs-all-metrics", + "properties": { + "workspaceId": "[parameters('logAnalyticsWorkspaceId')]", + "logs": [ + { + "categoryGroup": "allLogs", + "enabled": true + } + ], + "metrics": [ + { + "category": "AllMetrics", + "enabled": true + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.CognitiveServices/accounts', parameters('name'))]" + ] + }, + { + "copy": { + "name": "roleAssignments", + "count": "[length(parameters('roles'))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.CognitiveServices/accounts/{0}', parameters('name'))]", + "name": "[guid(parameters('name'), parameters('roles')[copyIndex()].principalId, parameters('roles')[copyIndex()].id)]", + "properties": { + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', parameters('roles')[copyIndex()].id)]", + "principalId": "[parameters('roles')[copyIndex()].principalId]", + "principalType": "[if(contains(parameters('roles')[copyIndex()], 'type'), parameters('roles')[copyIndex()].type, 'ServicePrincipal')]" + }, + "dependsOn": [ + "[resourceId('Microsoft.CognitiveServices/accounts', parameters('name'))]" + ] + } + ], + "outputs": { + "name": { + "type": "string", + "value": "[parameters('name')]" + }, + "id": { + "type": "string", + "value": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('name'))]" + }, + "principalId": { + "type": "string", + "value": "[reference(resourceId('Microsoft.CognitiveServices/accounts', parameters('name')), '2023-05-01', 'full').identity.principalId]" + } + } + } }, "dependsOn": [ - "[resourceId('Microsoft.Search/searchServices', format('search{0}{1}', parameters('workloadIdentifier'), parameters('environmentIdentifier')))]", - "[resourceId('Microsoft.CognitiveServices/accounts', format('aoai{0}{1}', parameters('workloadIdentifier'), parameters('environmentIdentifier')))]" + "[resourceId('Microsoft.Resources/deployments', format('{0}-deployment', variables('resources').aiSearchName))]", + "[resourceId('Microsoft.Resources/deployments', format('{0}-deployment', variables('resources').logAnalyticsWorkspaceName))]" ] }, { - "type": "Microsoft.Authorization/roleAssignments", - "apiVersion": "2020-08-01-preview", - "scope": "[format('Microsoft.Storage/storageAccounts/{0}', format('st{0}{1}', parameters('workloadIdentifier'), parameters('environmentIdentifier')))]", - "name": "[guid(format('st{0}{1}', parameters('workloadIdentifier'), parameters('environmentIdentifier')), format('search{0}{1}', parameters('workloadIdentifier'), parameters('environmentIdentifier')), 'storageBlobDataContributor')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-deployment', variables('resources').aiServicesName)]", "properties": { - "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', variables('azureRbacStorageBlobDataContributorRoleId'))]", - "principalId": "[reference(resourceId('Microsoft.Search/searchServices', format('search{0}{1}', parameters('workloadIdentifier'), parameters('environmentIdentifier'))), '2023-11-01', 'full').identity.principalId]", - "principalType": "ServicePrincipal" + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[variables('resources').aiServicesName]" + }, + "location": { + "value": "[parameters('location')]" + }, + "tags": { + "value": { + "environment": "shared" + } + }, + "kind": { + "value": "CognitiveServices" + }, + "logAnalyticsWorkspaceId": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-deployment', variables('resources').logAnalyticsWorkspaceName)), '2022-09-01').outputs.id.value]" + }, + "roles": { + "value": [ + { + "principalId": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-deployment', variables('resources').userAssignedIdentityName)), '2022-09-01').outputs.principalId.value]", + "id": "25fbc0a9-bd7c-42a3-aa1a-3b75d497ee68" + } + ] + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.27.1.19265", + "templateHash": "11553281893512969426" + } + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Name of the AI Services account" + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Location for AI Services account" + } + }, + "tags": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Tags for the AI Services account" + } + }, + "kind": { + "type": "string", + "defaultValue": "CognitiveServices", + "metadata": { + "description": "Kind of AI Services account" + } + }, + "roles": { + "type": "array", + "defaultValue": [], + "metadata": { + "description": "Role assignments for the Azure Search account" + } + }, + "logAnalyticsWorkspaceId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Log Analytics workspace ID for diagnostics" + } + }, + "deployments": { + "type": "array", + "defaultValue": [ + { + "name": "gpt-4-32k", + "version": "0613" + }, + { + "name": "gpt-35-turbo-16k", + "version": "0613" + }, + { + "name": "text-embedding-ada-002", + "version": "2" + } + ], + "metadata": { + "description": "Deployments for the AI Services account" + } + } + }, + "resources": [ + { + "copy": { + "name": "aisaDeployments", + "count": "[length(parameters('deployments'))]", + "mode": "serial", + "batchSize": 1 + }, + "condition": "[equals(parameters('kind'), 'OpenAI')]", + "type": "Microsoft.CognitiveServices/accounts/deployments", + "apiVersion": "2023-05-01", + "name": "[format('{0}/{1}', parameters('name'), format('{0}-{1}', parameters('deployments')[copyIndex()].name, parameters('deployments')[copyIndex()].version))]", + "properties": { + "model": { + "format": "OpenAI", + "name": "[parameters('deployments')[copyIndex()].name]", + "version": "[parameters('deployments')[copyIndex()].version]" + }, + "versionUpgradeOption": "NoAutoUpgrade" + }, + "sku": { + "name": "Standard", + "capacity": "[if(contains(parameters('deployments')[copyIndex()], 'capacity'), parameters('deployments')[copyIndex()].capacity, 20)]" + }, + "dependsOn": [ + "[resourceId('Microsoft.CognitiveServices/accounts', parameters('name'))]" + ] + }, + { + "type": "Microsoft.CognitiveServices/accounts", + "apiVersion": "2023-05-01", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "sku": { + "name": "S0" + }, + "kind": "[parameters('kind')]", + "identity": { + "type": "SystemAssigned" + }, + "properties": { + "customSubDomainName": "[parameters('name')]" + } + }, + { + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[format('Microsoft.CognitiveServices/accounts/{0}', parameters('name'))]", + "name": "all-logs-all-metrics", + "properties": { + "workspaceId": "[parameters('logAnalyticsWorkspaceId')]", + "logs": [ + { + "categoryGroup": "allLogs", + "enabled": true + } + ], + "metrics": [ + { + "category": "AllMetrics", + "enabled": true + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.CognitiveServices/accounts', parameters('name'))]" + ] + }, + { + "copy": { + "name": "roleAssignments", + "count": "[length(parameters('roles'))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.CognitiveServices/accounts/{0}', parameters('name'))]", + "name": "[guid(parameters('name'), parameters('roles')[copyIndex()].principalId, parameters('roles')[copyIndex()].id)]", + "properties": { + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', parameters('roles')[copyIndex()].id)]", + "principalId": "[parameters('roles')[copyIndex()].principalId]", + "principalType": "[if(contains(parameters('roles')[copyIndex()], 'type'), parameters('roles')[copyIndex()].type, 'ServicePrincipal')]" + }, + "dependsOn": [ + "[resourceId('Microsoft.CognitiveServices/accounts', parameters('name'))]" + ] + } + ], + "outputs": { + "name": { + "type": "string", + "value": "[parameters('name')]" + }, + "id": { + "type": "string", + "value": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('name'))]" + }, + "principalId": { + "type": "string", + "value": "[reference(resourceId('Microsoft.CognitiveServices/accounts', parameters('name')), '2023-05-01', 'full').identity.principalId]" + } + } + } }, "dependsOn": [ - "[resourceId('Microsoft.Search/searchServices', format('search{0}{1}', parameters('workloadIdentifier'), parameters('environmentIdentifier')))]", - "[resourceId('Microsoft.Storage/storageAccounts', format('st{0}{1}', parameters('workloadIdentifier'), parameters('environmentIdentifier')))]" + "[resourceId('Microsoft.Resources/deployments', format('{0}-deployment', variables('resources').logAnalyticsWorkspaceName))]", + "[resourceId('Microsoft.Resources/deployments', format('{0}-deployment', variables('resources').userAssignedIdentityName))]" ] } - ] + ], + "outputs": { + "storageAccountName": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-01-deployment', variables('resources').storageAccountName)), '2022-09-01').outputs.name.value]" + }, + "logAnalyticsWorkspaceName": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-deployment', variables('resources').logAnalyticsWorkspaceName)), '2022-09-01').outputs.name.value]" + }, + "containerRegistryName": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-deployment', variables('resources').containerRegistryName)), '2022-09-01').outputs.name.value]" + }, + "userAssignedIdentityName": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-deployment', variables('resources').userAssignedIdentityName)), '2022-09-01').outputs.name.value]" + }, + "containerAppEnvironmnetStagingName": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}-deployment', variables('resources').containerAppEnvironmnetName)), '2022-09-01').outputs.name.value]" + } + } } \ No newline at end of file diff --git a/infrastructure/modules/ai-search.bicep b/infrastructure/modules/ai-search.bicep new file mode 100644 index 0000000..ce17abe --- /dev/null +++ b/infrastructure/modules/ai-search.bicep @@ -0,0 +1,81 @@ +//******************************************************** +// Parameters +//******************************************************** + +@description('Name of the Azure AI Search service') +param name string + +@description('Location for the Azure AI Search service') +param location string = resourceGroup().location + +@description('Tags for the Azure AI Search service') +param tags object = {} + +@description('Role assignments for the Azure AI Search service') +param roles array = [] + +@description('Log Analytics workspace ID for diagnostics') +param logAnalyticsWorkspaceId string = '' + +resource srchNew 'Microsoft.Search/searchServices@2023-11-01' = { + name: name + location: location + tags: tags + sku: { + name: 'standard' + } + identity: { + type: 'SystemAssigned' + } + properties: { + authOptions: { + aadOrApiKey: { + aadAuthFailureMode: 'http401WithBearerChallenge' + } + } + } +} + +//******************************************************** +// Resources +//******************************************************** + +resource diagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = { + name: 'all-logs-all-metrics' + scope: srchNew + properties: { + workspaceId: logAnalyticsWorkspaceId + logs: [ + { + categoryGroup: 'allLogs' + enabled: true + } + ] + metrics: [ + { + category: 'AllMetrics' + enabled: true + } + ] + } +} + +resource roleAssignments 'Microsoft.Authorization/roleAssignments@2022-04-01' = [ + for role in roles: { + name: guid(name, role.principalId, role.id) + scope: srchNew + properties: { + roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', role.id) + principalId: role.principalId + principalType: contains(role, 'type') ? role.type : 'ServicePrincipal' + } + } +] + +//******************************************************** +// Outputs +//******************************************************** + +output name string = srchNew.name +output id string = srchNew.id +output principalId string = srchNew.identity.principalId diff --git a/infrastructure/modules/ai-services.bicep b/infrastructure/modules/ai-services.bicep new file mode 100644 index 0000000..75286e4 --- /dev/null +++ b/infrastructure/modules/ai-services.bicep @@ -0,0 +1,116 @@ +//******************************************************** +// Parameters +//******************************************************** + +@description('Name of the AI Services account') +param name string + +@description('Location for AI Services account') +param location string = resourceGroup().location + +@description('Tags for the AI Services account') +param tags object = {} + +@description('Kind of AI Services account') +param kind string = 'CognitiveServices' + +@description('Role assignments for the Azure Search account') +param roles array = [] + +@description('Log Analytics workspace ID for diagnostics') +param logAnalyticsWorkspaceId string = '' + +@description('Deployments for the AI Services account') +param deployments array = [ + { + name: 'gpt-4-32k' + version: '0613' + } + { + name: 'gpt-35-turbo-16k' + version: '0613' + } + { + name: 'text-embedding-ada-002' + version: '2' + } +] + +//******************************************************** +// Resources +//******************************************************** + +resource aisaNew 'Microsoft.CognitiveServices/accounts@2023-05-01' = { + name: name + location: location + tags: tags + sku: { + name: 'S0' + } + kind: kind + identity: { + type: 'SystemAssigned' + } + properties: { + customSubDomainName: name + } + + @batchSize(1) + resource aisaDeployments 'deployments@2023-05-01' = [ + for deployment in deployments: if (kind == 'OpenAI') { + name: '${deployment.name}-${deployment.version}' + properties: { + model: { + format: 'OpenAI' + name: deployment.name + version: deployment.version + } + versionUpgradeOption: 'NoAutoUpgrade' + } + sku: { + name: 'Standard' + capacity: contains(deployment, 'capacity') ? deployment.capacity : 20 + } + } + ] +} + +resource diagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = { + name: 'all-logs-all-metrics' + scope: aisaNew + properties: { + workspaceId: logAnalyticsWorkspaceId + logs: [ + { + categoryGroup: 'allLogs' + enabled: true + } + ] + metrics: [ + { + category: 'AllMetrics' + enabled: true + } + ] + } +} + +resource roleAssignments 'Microsoft.Authorization/roleAssignments@2022-04-01' = [ + for role in roles: { + name: guid(name, role.principalId, role.id) + scope: aisaNew + properties: { + roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', role.id) + principalId: role.principalId + principalType: contains(role, 'type') ? role.type : 'ServicePrincipal' + } + } +] + +//******************************************************** +// Outputs +//******************************************************** + +output name string = aisaNew.name +output id string = aisaNew.id +output principalId string = aisaNew.identity.principalId diff --git a/infrastructure/modules/container-app-environment.bicep b/infrastructure/modules/container-app-environment.bicep new file mode 100644 index 0000000..ec4b999 --- /dev/null +++ b/infrastructure/modules/container-app-environment.bicep @@ -0,0 +1,64 @@ +//******************************************************** +// Parameters +//******************************************************** + +@description('Name of the Application Insights service') +param name string + +@description('Location for Application Insights service') +param location string = resourceGroup().location + +@description('Tags for the Application Insights service') +param tags object = {} + +@description('Role assignments for the Application Insights service') +param roles array = [] + +@description('Log Analytics workspace name') +param logAnalyticsWorkspaceName string = '' + +@description('Log Analytics workspace resource group name') +param logAnalyticsWorkspaceResourceGroupName string = resourceGroup().name + +//******************************************************** +// Resources +//******************************************************** + +resource logExisting 'Microsoft.OperationalInsights/workspaces@2022-10-01' existing = { + scope: resourceGroup(logAnalyticsWorkspaceResourceGroupName) + name: logAnalyticsWorkspaceName +} + +resource caeNew 'Microsoft.App/managedEnvironments@2023-05-01' = { + name: name + location: location + tags: tags + properties: { + appLogsConfiguration: { + destination: 'log-analytics' + logAnalyticsConfiguration: { + customerId: logExisting.properties.customerId + sharedKey: logExisting.listKeys().primarySharedKey + } + } + } +} + +resource roleAssignments 'Microsoft.Authorization/roleAssignments@2022-04-01' = [ + for role in roles: { + name: guid(name, role.principalId, role.id) + scope: caeNew + properties: { + roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', role.id) + principalId: role.principalId + principalType: contains(role, 'type') ? role.type : 'ServicePrincipal' + } + } +] + +//******************************************************** +// Outputs +//******************************************************** + +output name string = caeNew.name +output id string = caeNew.id diff --git a/infrastructure/modules/container-registry.bicep b/infrastructure/modules/container-registry.bicep new file mode 100644 index 0000000..499b60b --- /dev/null +++ b/infrastructure/modules/container-registry.bicep @@ -0,0 +1,73 @@ +//******************************************************** +// Parameters +//******************************************************** + +@description('Name of the Container Registry service') +param name string + +@description('Location for Container Registry service') +param location string = resourceGroup().location + +@description('Tags for the Container Registry service') +param tags object = {} + +@description('Role assignments for the Container Registry service') +param roles array = [] + +@description('Log Analytics workspace ID for diagnostics') +param logAnalyticsWorkspaceId string = '' + +//******************************************************** +// Resources +//******************************************************** + +resource crNew 'Microsoft.ContainerRegistry/registries@2022-12-01' = { + name: name + location: location + tags: tags + sku: { + name: 'Standard' + } + properties: { + adminUserEnabled: true + } +} + +resource diagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = { + name: 'all-logs-all-metrics' + scope: crNew + properties: { + workspaceId: logAnalyticsWorkspaceId + logs: [ + { + categoryGroup: 'allLogs' + enabled: true + } + ] + metrics: [ + { + category: 'AllMetrics' + enabled: true + } + ] + } +} + +resource roleAssignments 'Microsoft.Authorization/roleAssignments@2022-04-01' = [ + for role in roles: { + name: guid(name, role.principalId, role.id) + scope: crNew + properties: { + roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', role.id) + principalId: role.principalId + principalType: contains(role, 'type') ? role.type : 'ServicePrincipal' + } + } +] + +//******************************************************** +// Outputs +//******************************************************** + +output name string = crNew.name +output id string = crNew.id diff --git a/infrastructure/modules/log-analytics-workspace.bicep b/infrastructure/modules/log-analytics-workspace.bicep new file mode 100644 index 0000000..3644308 --- /dev/null +++ b/infrastructure/modules/log-analytics-workspace.bicep @@ -0,0 +1,89 @@ +//******************************************************** +// Parameters +//******************************************************** + +@description('Name of the Log Analytics workspace') +param name string + +@description('Location for Log Analytics workspace') +param location string = resourceGroup().location + +@description('Tags for the Log Analytics workspace') +param tags object = {} + +@description('Role assignments for the Log Analytics workspace') +param roles array = [] + +@description('Storage account ID to link to the Log Analytics workspace') +param storageAccountId string = '' + +//******************************************************** +// Resources +//******************************************************** + +resource logNew 'Microsoft.OperationalInsights/workspaces@2022-10-01' = { + name: name + location: location + tags: tags + properties: { + retentionInDays: 30 + sku: { + name: 'Standalone' + } + } + + resource linkedStAlerts 'linkedStorageAccounts@2020-08-01' = { + name: 'Alerts' + properties: { + storageAccountIds: [ + storageAccountId + ] + } + } + + resource linkedStCustomLogs 'linkedStorageAccounts@2020-08-01' = { + name: 'CustomLogs' + properties: { + storageAccountIds: [ + storageAccountId + ] + } + } + + resource linkedStIngestion 'linkedStorageAccounts@2020-08-01' = { + name: 'Ingestion' + properties: { + storageAccountIds: [ + storageAccountId + ] + } + } + + resource linkedStQuery 'linkedStorageAccounts@2020-08-01' = { + name: 'Query' + properties: { + storageAccountIds: [ + storageAccountId + ] + } + } +} + +resource roleAssignments 'Microsoft.Authorization/roleAssignments@2022-04-01' = [ + for role in roles: { + name: guid(name, role.principalId, role.id) + scope: logNew + properties: { + roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', role.id) + principalId: role.principalId + principalType: contains(role, 'type') ? role.type : 'ServicePrincipal' + } + } +] + +//******************************************************** +// Outputs +//******************************************************** + +output name string = logNew.name +output id string = logNew.id diff --git a/infrastructure/modules/storage-account.bicep b/infrastructure/modules/storage-account.bicep new file mode 100644 index 0000000..a6b79e3 --- /dev/null +++ b/infrastructure/modules/storage-account.bicep @@ -0,0 +1,85 @@ +//******************************************************** +// Parameters +//******************************************************** + +@description('Name of the Storage service') +param name string + +@description('Location for Storage service') +param location string = resourceGroup().location + +@description('Tags for the Storage service') +param tags object = {} + +@description('Role assignments for the Storage service') +param roles array = [] + +@description('Enable hierarchical namespace') +param enableHns bool = false + +//******************************************************** +// Resources +//******************************************************** + +resource stNew 'Microsoft.Storage/storageAccounts@2022-05-01' = { + name: name + location: location + tags: tags + sku: { + name: 'Standard_LRS' + } + kind: 'StorageV2' + properties: { + isHnsEnabled: enableHns + minimumTlsVersion: 'TLS1_2' + } +} + +resource roleAssignments 'Microsoft.Authorization/roleAssignments@2022-04-01' = [ + for role in roles: { + name: guid(name, role.principalId, role.id) + scope: stNew + properties: { + roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', role.id) + principalId: role.principalId + principalType: contains(role, 'type') ? role.type : 'ServicePrincipal' + } + } +] + +//******************************************************** +// Deployment Scripts +//******************************************************** + +resource deploymentScript 'Microsoft.Resources/deploymentScripts@2020-10-01' = { + name: 'ds${name}' + location: location + kind: 'AzureCLI' + properties: { + azCliVersion: '2.30.0' + timeout: 'PT5M' + retentionInterval: 'PT1H' + environmentVariables: [ + { + name: 'AZURE_STORAGE_ACCOUNT' + value: stNew.name + } + { + name: 'AZURE_STORAGE_KEY' + secureValue: stNew.listKeys().keys[0].value + } + ] + scriptContent: ''' + git clone https://github.com/nfmoore/azure-open-ai-example-scenarios.git + az storage container create --name data --account-name $AZURE_STORAGE_ACCOUNT --account-key $AZURE_STORAGE_KEY + az storage blob upload-batch --destination data --account-name $AZURE_STORAGE_ACCOUNT --account-key $AZURE_STORAGE_KEY --destination-path ./ --source ./azure-open-ai-example-scenarios/data + ''' + } +} + +//******************************************************** +// Outputs +//******************************************************** + +output name string = stNew.name +output id string = stNew.id diff --git a/infrastructure/modules/user-assigned-identity.bicep b/infrastructure/modules/user-assigned-identity.bicep new file mode 100644 index 0000000..9c1c573 --- /dev/null +++ b/infrastructure/modules/user-assigned-identity.bicep @@ -0,0 +1,30 @@ +//******************************************************** +// Parameters +//******************************************************** + +@description('Name of the User Assigned Identity service') +param name string + +@description('Location for User Assigned Identity service') +param location string = resourceGroup().location + +@description('Tags for the User Assigned Identity service') +param tags object = {} + +//******************************************************** +// Resources +//******************************************************** + +resource idNew 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { + name: name + location: location + tags: tags +} + +//******************************************************** +// Outputs +//******************************************************** + +output id string = idNew.id +output name string = idNew.name +output principalId string = idNew.properties.principalId diff --git a/src/rag/utilities.py b/llms/custom_rag_client.py similarity index 82% rename from src/rag/utilities.py rename to llms/custom_rag_client.py index 82f8ca9..c646b29 100644 --- a/src/rag/utilities.py +++ b/llms/custom_rag_client.py @@ -1,21 +1,17 @@ """ -This module contains the RetrievalAugmentedGenerationClient class for +This module contains the CustomRetrievalAugmentedGenerationClient class for orchestrating various operations related to AI search and chat. Classes: - RetrievalAugmentedGenerationClient: A client class for orchestrating various + CustomRetrievalAugmentedGenerationClient: A client class for orchestrating various operations related to AI search and chat. """ -import string - import requests -import yaml from azure.identity import DefaultAzureCredential -from nltk.corpus import stopwords -class RetrievalAugmentedGenerationClient: +class CustomRetrievalAugmentedGenerationClient: """ A client class for orchestrating various operations related to AI search and chat. @@ -38,9 +34,10 @@ def __init__( open_ai_embedding_deployment: str, search_endpoint: str, search_index_name: str, - system_prompt_configuration_file: str, - open_ai_api_version="2023-12-01-preview", - search_api_version="2023-11-01", + query_system_message: str, + chat_system_message: str, + open_ai_api_version="2024-02-01", + search_api_version="2024-03-01-Preview", credential=DefaultAzureCredential(), ): self.open_ai_endpoint = open_ai_endpoint @@ -62,15 +59,8 @@ def __init__( self.embedding_endpoint = f"{self.open_ai_endpoint}/openai/deployments/{self.open_ai_embedding_deployment}/embeddings?api-version={self.open_ai_api_version}" self.query_search_endpoint = f"{self.search_endpoint}/indexes/{self.search_index_name}/docs/search?api-version={self.search_api_version}" - with open(system_prompt_configuration_file, "r", encoding="utf-8") as f: - configuration = yaml.safe_load(f) - - self.search_query_system_message = configuration.get( - "search_query_system_message" - ) - self.chat_response_system_message = configuration.get( - "chat_response_system_message" - ) + self.query_system_message = query_system_message + self.chat_system_message = chat_system_message def get_request_headers(self, token: str) -> dict[str, str]: """ @@ -113,7 +103,7 @@ def retrieve_documents( headers=self.get_request_headers(self.open_ai_access_token), json={ "messages": [ - {"role": "system", "content": self.search_query_system_message}, + {"role": "system", "content": self.query_system_message}, {"role": "user", "content": question}, ], }, @@ -143,8 +133,9 @@ def retrieve_documents( "select": selected_fields, "queryType": "semantic", "semanticConfiguration": f"{self.search_index_name}-semantic-configuration", - "captions": "extractive", - "answers": "extractive", + "captions": "extractive|highlight-true", + "answers": f"extractive|count-{number_of_documents}", + "count": "true", "top": number_of_documents, "vectorQueries": [ { @@ -163,7 +154,11 @@ def retrieve_documents( # Filter search documents filtered_search_documents = [ - {"title": doc["title"], "path": doc["path"], "chunk": doc["chunk"]} + { + "title": doc["title"], + "path": doc["path"], + "content": doc["@search.captions"][0]["text"], + } for doc in search_documents ] @@ -182,23 +177,15 @@ def augment_prompt(self, question: str, retrieved_documents: list[any]) -> str: str: The augmented prompt with the retrieved documents. """ - # Function to pre-process document content - remove punctuation and stopwords - def preprocess_text(text: str): - text = text.lower().translate(str.maketrans("", "", string.punctuation)) - stop_words = set(stopwords.words("english")) - text = " ".join([word for word in text.split() if word not in stop_words]) - return text - # Generate prompt sources string prompt_sources = "".join( - [ - f"{doc['title']} :: {doc['path']} :: {preprocess_text(doc['chunk'])} ||\n" - for doc in retrieved_documents - ] + [f"{doc['title']} :: {doc['content']} ||\n" for doc in retrieved_documents] ) # Embed the sources in the prompt - augmented_prompt = f"{question}\nSources:\n{prompt_sources}" + augmented_prompt = ( + f"#question:```{question}```\n#sources:```{prompt_sources}```" + ) return augmented_prompt @@ -230,9 +217,7 @@ def generate_response( self.chat_endpoint, headers=self.get_request_headers(self.open_ai_access_token), json={ - "messages": [ - {"role": "system", "content": self.chat_response_system_message} - ] + "messages": [{"role": "system", "content": self.chat_system_message}] + message_history_filtered + [{"role": "user", "content": augmented_prompt}], }, @@ -246,16 +231,18 @@ def generate_response( def update_message_history( self, + question: str, message_history: list[dict[str, any]], augmented_prompt: str, response: str, retrieved_documents: list[any], ) -> list[dict[str, any]]: """ - Updates the message history with the user and agent messages. + Updates the message history with the user and assistant messages. Parameters: self (object): An instance of the class that this method belongs to. + question (str): The user question. message_history (list[dict[str, any]]): The message history containing the user prompt. augmented_prompt (str): The user message. response (str): The assistant message. @@ -272,14 +259,32 @@ def update_message_history( ) ) + # Remove duplicate references from retrieved documents + references_without_duplicates = [ + dict(referenceas_tuple) + for referenceas_tuple in { + tuple(reference.items()) for reference in references + } + ] + # Update message history user_message = { "role": "user", - "content": augmented_prompt, - "references": references, + "content": question, + "context": { + "augmented_prompt": augmented_prompt, + }, } - agent_message = {"role": "assistant", "content": response} - updated_message_history = message_history + [user_message, agent_message] + + assistant_message = { + "role": "assistant", + "content": response, + "context": { + "references": references_without_duplicates, + }, + } + + updated_message_history = message_history + [user_message, assistant_message] return updated_message_history @@ -309,7 +314,7 @@ def get_answer(self, question: str, message_history: list[dict[str, any]]) -> st # Update message history updated_message_history = self.update_message_history( - message_history, augmented_prompt, response, retrieved_documents + question, message_history, augmented_prompt, response, retrieved_documents ) return updated_message_history diff --git a/src/rag/configuration.yaml b/llms/system_messages.yml similarity index 52% rename from src/rag/configuration.yaml rename to llms/system_messages.yml index e6e64a2..9af6943 100644 --- a/src/rag/configuration.yaml +++ b/llms/system_messages.yml @@ -1,14 +1,21 @@ -search_query_system_message: | +# ----------------------------------- +# Example Scenario 1 +# ----------------------------------- + +query_system_message: | You are a bot that translates user queries into an effective search query for Azure AI Search. Ensure the user's intent is captured by including relevant keywords or phrases from their query. Ensure you ownly return the search query and nothing else in your response. -chat_response_system_message: | +product_info_chat_system_message: | You are an customer service bot designed to answer questions on products. Keep your answers short and to the point. Try to use dot points as much as possible. Answer ONLY with the contnet listed in the list of sources below. If there isn't enough information below, say you don't know. - Do not generate answers that don't use the sources below. If asking a clarifying question to the user would help, ask the question. + The question will enclosed by three back ticks (```) after a # followed by the word question, for example "#question" + The sources will enclosed by three back ticks (```) after a # followed by the word sources, for example "#sources" + Do not generate answers that don't use information in the listed sources. + If asking a clarifying question to the user would help, ask the question. For tabular information return it as an html table. Do not return markdown format. - If the question is not in English, answer in the language used in the question. - Each source has a name, path, and contnet seperated by a double colon, always include the source name for each fact you use in the response. + Each source has a name and content seperated by a double colon, always include the source name for each fact you use in the response. Use square brackets to reference the source, for example [info1.txt]. Don't combine sources, list each source separately, for example [info1.txt][info2.pdf]. +# ----------------------------------- diff --git a/notebooks/01-populate-index.ipynb b/notebooks/01-populate-index.ipynb deleted file mode 100644 index c1b0fd3..0000000 --- a/notebooks/01-populate-index.ipynb +++ /dev/null @@ -1,158 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Populate Azure AI Search Index" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "import os\n", - "import dotenv\n", - "import sys\n", - "\n", - "dotenv.load_dotenv(\".env\")\n", - "sys.path.append(os.path.join(os.getcwd(), \"..\", \"src\"))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Approach 1: Pull-based\n", - "\n", - "The pull model uses indexers connecting to a supported data source, automatically uploading the data into your index. This is the recommended approach for data sources that are frequently updated." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "from search.utilities import SearchClient\n", - "\n", - "# Create search client\n", - "search_client = SearchClient(\n", - " search_endpoint=os.environ[\"AZURE_AI_SEARCH_ENDPOINT\"],\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "# Generate list of variables to be used in templates\n", - "template_variables = {\n", - " key: value for key, value in os.environ.items() if key.startswith((\"AZURE\"))\n", - "}\n", - "\n", - "# Define template paths\n", - "base_path = os.path.join(os.getcwd(), \"..\", \"src\", \"search\", \"templates\")\n", - "datasource_template_path = os.path.join(base_path, \"product-info\", \"datasource.json\")\n", - "index_template_path = os.path.join(base_path, \"product-info\", \"index.json\")\n", - "skillset_template_path = os.path.join(base_path, \"product-info\", \"skillset.json\")\n", - "indexer_template_path = os.path.join(base_path, \"product-info\", \"indexer.json\")\n", - "\n", - "# List of search assets\n", - "assets = [\n", - " {\n", - " \"type\": \"indexes\",\n", - " \"name\": os.environ[\"AZURE_AI_SEARCH_INDEX_NAME\"],\n", - " \"template_path\": index_template_path,\n", - " \"template_variables\": template_variables,\n", - " },\n", - " {\n", - " \"type\": \"datasources\",\n", - " \"name\": os.environ[\"AZURE_AI_SEARCH_DATASOURCE_NAME\"],\n", - " \"template_path\": datasource_template_path,\n", - " \"template_variables\": template_variables,\n", - " },\n", - " {\n", - " \"type\": \"skillsets\",\n", - " \"name\": os.environ[\"AZURE_AI_SEARCH_SKILLSET_NAME\"],\n", - " \"template_path\": skillset_template_path,\n", - " \"template_variables\": template_variables,\n", - " },\n", - " {\n", - " \"type\": \"indexers\",\n", - " \"name\": os.environ[\"AZURE_AI_SEARCH_INDEXER_NAME\"],\n", - " \"template_path\": indexer_template_path,\n", - " \"template_variables\": template_variables,\n", - " },\n", - "]\n", - "\n", - "# Load search asset templates\n", - "search_client.load_search_management_asset_templates(assets)" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [], - "source": [ - "# Create the index\n", - "index_response = search_client.create_search_management_asset(asset_type=\"indexes\")\n", - "\n", - "# Create the data source\n", - "datasource_response = search_client.create_search_management_asset(asset_type=\"datasources\")\n", - "\n", - "# Create skillset to enhance the indexer\n", - "skillset_response = search_client.create_search_management_asset(asset_type=\"skillsets\")\n", - "\n", - "# Create the indexer\n", - "indexer_response = search_client.create_search_management_asset(asset_type=\"indexers\")" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [], - "source": [ - "# Run the indexer\n", - "indexer_run_response = search_client.run_indexer()" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [], - "source": [ - "# Run the indexer with reset\n", - "indexer_run_reset_response = search_client.run_indexer(reset_flag=True)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "base", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.4" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/notebooks/02-llm-queries.ipynb b/notebooks/02-llm-queries.ipynb deleted file mode 100644 index bcb9582..0000000 --- a/notebooks/02-llm-queries.ipynb +++ /dev/null @@ -1,181 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# LLM Queries with Knowledge Base Integration" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import os\n", - "import dotenv\n", - "import sys\n", - "\n", - "dotenv.load_dotenv(\".env\")\n", - "sys.path.append(os.path.join(os.getcwd(), \"..\", \"src\"))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Approach 1: Custom Client\n", - "\n", - "This approach will use the `RetrievalAugmentedGenerationClient` class defined in `src/rag/utilities.py`. This will NOT require a Microsoft managed private endpoint for private access." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from rag.utilities import RetrievalAugmentedGenerationClient\n", - "\n", - "# Create orchestration client\n", - "rag_client = RetrievalAugmentedGenerationClient(\n", - " open_ai_endpoint=os.getenv(\"AZURE_OPENAI_API_BASE\"),\n", - " open_ai_chat_deployment=os.getenv(\"AZURE_OPENAI_CHAT_DEPLOYMENT\"),\n", - " open_ai_embedding_deployment=os.getenv(\"AZURE_OPENAI_EMBEDDING_DEPLOYMENT\"),\n", - " search_endpoint=os.getenv(\"AZURE_AI_SEARCH_ENDPOINT\"),\n", - " search_index_name=os.getenv(\"AZURE_AI_SEARCH_INDEX_NAME\"),\n", - " system_prompt_configuration_file=\"../src/rag/configuration.yaml\"\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "message_history = []\n", - "message_history = rag_client.get_answer(\"Which tent is the most waterproof?\", message_history=message_history)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "for message in message_history:\n", - " content = message['content'].split(\"Sources:\")[0].strip()\n", - " print(f\"{message['role'].title()}: {content}\\n\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "message_history = rag_client.get_answer(\"Tell me more about the Alpine Explorer Tent?\", message_history=message_history)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "for message in message_history:\n", - " content = message['content'].split(\"Sources:\")[0].strip()\n", - " print(f\"{message['role'].title()}: {content}\\n\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Approach 2: Azure OpenAI Service REST API\n", - "\n", - "This will require public access on Azure AI Search or a Microsoft managed private endpoint for private access." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from azure.identity import DefaultAzureCredential\n", - "import requests\n", - "\n", - "credential = DefaultAzureCredential()\n", - "access_token = credential.get_token(\"https://cognitiveservices.azure.com/.default\")\n", - "\n", - "open_ai_endpoint = os.getenv(\"AZURE_OPENAI_API_BASE\")\n", - "open_ai_chat_deployment = os.getenv(\"AZURE_OPENAI_CHAT_DEPLOYMENT\")\n", - "open_ai_api_version = os.getenv(\"AZURE_OPENAI_API_VERSION\")\n", - "\n", - "chat_endpoint = f\"{open_ai_endpoint}/openai/deployments/{open_ai_chat_deployment}/extensions/chat/completions?api-version={open_ai_api_version}\"\n", - "\n", - "request_headers = {\n", - " \"Content-Type\": \"application/json\",\n", - " \"Authorization\": f\"Bearer {access_token.token}\",\n", - "}\n", - "\n", - "request_payload = {\n", - " \"dataSources\": [\n", - " {\n", - " \"type\": \"AzureCognitiveSearch\",\n", - " \"parameters\": {\n", - " \"endpoint\": os.getenv(\"AZURE_AI_SEARCH_ENDPOINT\"),\n", - " \"indexName\": os.getenv(\"AZURE_AI_SEARCH_INDEX_NAME\"),\n", - " \"queryType\": \"vectorSemanticHybrid\",\n", - " \"embeddingDeploymentName\": os.getenv(\n", - " \"AZURE_OPENAI_EMBEDDING_DEPLOYMENT\"\n", - " ),\n", - " \"fieldsMapping\": {\"titleField\": \"title\", \"urlField\": \"path\"},\n", - " },\n", - " }\n", - " ],\n", - " \"messages\": [{\"role\": \"user\", \"content\": \"Which tent is the most waterproof?\"}],\n", - "}\n", - "\n", - "response = requests.post(\n", - " chat_endpoint,\n", - " headers=request_headers,\n", - " json=request_payload,\n", - ")\n", - "\n", - "print(response.json()[\"choices\"][0][\"message\"][\"content\"])" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": ".venv", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.4" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/notebooks/example_scenario_01.ipynb b/notebooks/example_scenario_01.ipynb new file mode 100644 index 0000000..4eff419 --- /dev/null +++ b/notebooks/example_scenario_01.ipynb @@ -0,0 +1,412 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Example Scenario 1: Pull-Based Data Ingestion with Azure AI Search & Custom Retrieval-Augmented Generation (RAG) Pattern\n", + "\n", + "- **Populate Azure AI Search Index**: A pull-based approach is used to create a search index in Azure AI Search. A pull model uses indexers connecting to a supported data source, automatically uploading the data into your index. This is the recommended approach for data sources that are frequently updated.\n", + "\n", + "- **LLM Queries with Knowledge Base Integration**: A custom implementation for Retrieval Augmented Generation (RAG) will be used to chat with an LLM. This approach will be contrasted with an out-of-the box approach using the Azure OpenAI Service REST API." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import dotenv\n", + "import sys\n", + "\n", + "# common setup\n", + "dotenv.load_dotenv(\".env\")\n", + "sys.path.append(os.path.join(os.getcwd(), \"..\"))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Populate Azure AI Search Index\n", + "\n", + "### Approach: Pull-Based Custom Client\n", + "\n", + "This approach will use the `CustomSearchClient` class defined in `search/custom_search_client_pull.py`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from search.custom_search_client import CustomSearchClient\n", + "\n", + "# Create search client\n", + "search_client = CustomSearchClient(\n", + " search_endpoint=os.environ[\"AZURE_AI_SEARCH_ENDPOINT\"],\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Generate list of variables to be used in templates\n", + "template_variables = {\n", + " key: value for key, value in os.environ.items() if key.startswith((\"AZURE\"))\n", + "}\n", + "\n", + "# Define template paths\n", + "base_path = os.path.join(os.getcwd(), \"..\", \"search\", \"templates\")\n", + "datasource_template_path = os.path.join(base_path, \"product-info\", \"datasource.json\")\n", + "index_template_path = os.path.join(base_path, \"product-info\", \"index.json\")\n", + "skillset_template_path = os.path.join(base_path, \"product-info\", \"skillset.json\")\n", + "indexer_template_path = os.path.join(base_path, \"product-info\", \"indexer.json\")\n", + "\n", + "# List of search assets\n", + "assets = [\n", + " {\n", + " \"type\": \"indexes\",\n", + " \"name\": os.environ[\"AZURE_AI_SEARCH_INDEX_NAME\"],\n", + " \"template_path\": index_template_path,\n", + " \"template_variables\": template_variables,\n", + " },\n", + " {\n", + " \"type\": \"datasources\",\n", + " \"name\": os.environ[\"AZURE_AI_SEARCH_DATASOURCE_NAME\"],\n", + " \"template_path\": datasource_template_path,\n", + " \"template_variables\": template_variables,\n", + " },\n", + " {\n", + " \"type\": \"skillsets\",\n", + " \"name\": os.environ[\"AZURE_AI_SEARCH_SKILLSET_NAME\"],\n", + " \"template_path\": skillset_template_path,\n", + " \"template_variables\": template_variables,\n", + " },\n", + " {\n", + " \"type\": \"indexers\",\n", + " \"name\": os.environ[\"AZURE_AI_SEARCH_INDEXER_NAME\"],\n", + " \"template_path\": indexer_template_path,\n", + " \"template_variables\": template_variables,\n", + " },\n", + "]\n", + "\n", + "# Load search asset templates\n", + "search_client.load_search_management_asset_templates(assets)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Create the index\n", + "index_response = search_client.create_search_management_asset(asset_type=\"indexes\")\n", + "\n", + "# Create the data source\n", + "datasource_response = search_client.create_search_management_asset(\n", + " asset_type=\"datasources\"\n", + ")\n", + "\n", + "# Create skillset to enhance the indexer\n", + "skillset_response = search_client.create_search_management_asset(asset_type=\"skillsets\")\n", + "\n", + "# Create the indexer\n", + "indexer_response = search_client.create_search_management_asset(asset_type=\"indexers\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Run the indexer\n", + "indexer_run_response = search_client.run_indexer()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# [Optional] Run the indexer with reset\n", + "# indexer_run_reset_response = search_client.run_indexer(reset_flag=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. LLM Queries with Knowledge Base Integration\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import dotenv\n", + "import sys\n", + "\n", + "dotenv.load_dotenv(\".env\")\n", + "sys.path.append(os.path.join(os.getcwd(), \"..\"))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Approach 1: Custom Client\n", + "\n", + "This approach will use the `CustomRetrievalAugmentedGenerationClient` class defined in `open_ai/custom_rag_client.py`. This will NOT require a Microsoft managed private endpoint for private access." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import yaml\n", + "\n", + "# Load configuration file\n", + "system_prompt_configuration_file = \"../llms/system_messages.yml\"\n", + "with open(system_prompt_configuration_file, \"r\", encoding=\"utf-8\") as f:\n", + " configuration = yaml.safe_load(f)\n", + "\n", + "# Get system messages\n", + "query_system_message = configuration.get(\"query_system_message\")\n", + "chat_system_message = configuration.get(\"product_info_chat_system_message\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from llms.custom_rag_client import CustomRetrievalAugmentedGenerationClient\n", + "\n", + "# Create orchestration client\n", + "rag_client = CustomRetrievalAugmentedGenerationClient(\n", + " open_ai_endpoint=os.getenv(\"AZURE_OPENAI_API_BASE\"),\n", + " open_ai_chat_deployment=os.getenv(\"AZURE_OPENAI_CHAT_DEPLOYMENT\"),\n", + " open_ai_embedding_deployment=os.getenv(\"AZURE_OPENAI_EMBEDDING_DEPLOYMENT\"),\n", + " search_endpoint=os.getenv(\"AZURE_AI_SEARCH_ENDPOINT\"),\n", + " search_index_name=os.getenv(\"AZURE_AI_SEARCH_INDEX_NAME\"),\n", + " query_system_message=query_system_message,\n", + " chat_system_message=chat_system_message,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "message_history = []\n", + "message_history = rag_client.get_answer(\n", + " \"Which tent is the most waterproof?\", message_history=message_history\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "for message in message_history:\n", + " print(f\"{message['role'].title()}: {message['content']}\\n\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "message_history = rag_client.get_answer(\n", + " \"Tell me more about the Alpine Explorer Tent?\", message_history=message_history\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "for message in message_history:\n", + " print(f\"{message['role'].title()}: {message['content']}\\n\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Approach 2: Azure OpenAI Service REST API\n", + "\n", + "This will require public access on Azure AI Search or a Microsoft managed private endpoint for private access." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from azure.identity import DefaultAzureCredential\n", + "import requests\n", + "\n", + "credential = DefaultAzureCredential()\n", + "access_token = credential.get_token(\"https://cognitiveservices.azure.com/.default\")\n", + "\n", + "open_ai_endpoint = os.getenv(\"AZURE_OPENAI_API_BASE\")\n", + "open_ai_chat_deployment = os.getenv(\"AZURE_OPENAI_CHAT_DEPLOYMENT\")\n", + "open_ai_api_version = \"2024-02-01\"\n", + "\n", + "chat_endpoint = f\"{open_ai_endpoint}/openai/deployments/{open_ai_chat_deployment}/chat/completions?api-version={open_ai_api_version}\"\n", + "\n", + "request_headers = {\n", + " \"Content-Type\": \"application/json\",\n", + " \"Authorization\": f\"Bearer {access_token.token}\",\n", + "}\n", + "\n", + "\n", + "def get_answer(message_history: list):\n", + " request_payload = {\n", + " \"data_sources\": [\n", + " {\n", + " \"type\": \"azure_search\",\n", + " \"parameters\": {\n", + " \"endpoint\": os.getenv(\"AZURE_AI_SEARCH_ENDPOINT\"),\n", + " \"index_name\": os.getenv(\"AZURE_AI_SEARCH_INDEX_NAME\"),\n", + " \"query_type\": \"vector_semantic_hybrid\",\n", + " \"embedding_dependency\": {\n", + " \"deployment_name\": os.getenv(\n", + " \"AZURE_OPENAI_EMBEDDING_DEPLOYMENT\"\n", + " ),\n", + " \"type\": \"deployment_name\",\n", + " },\n", + " \"fields_mapping\": {\"title_field\": \"title\", \"url_field\": \"path\"},\n", + " \"authentication\": {\"type\": \"system_assigned_managed_identity\"},\n", + " },\n", + " }\n", + " ],\n", + " \"messages\": message_history,\n", + " }\n", + "\n", + " response = requests.post(\n", + " chat_endpoint,\n", + " headers=request_headers,\n", + " json=request_payload,\n", + " )\n", + "\n", + " return response.json()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "inital_user_message = \"Which tent is the most waterproof?\"\n", + "message_history = [\n", + " {\"role\": \"system\", \"content\": chat_system_message},\n", + " {\"role\": \"user\", \"content\": inital_user_message},\n", + "]\n", + "\n", + "response = get_answer(message_history)\n", + "message_history.append(\n", + " {\"role\": \"assistant\", \"content\": response[\"choices\"][0][\"message\"][\"content\"]}\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "for message in message_history:\n", + " if message[\"role\"] in [\"user\", \"assistant\"]:\n", + " print(f\"{message['role'].title()}: {message['content']}\\n\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "follow_up_user_message = \"Tell me more about the Alpine Explorer Tent?\"\n", + "message_history.append({\"role\": \"user\", \"content\": follow_up_user_message})\n", + "\n", + "response = get_answer(message_history)\n", + "message_history.append(\n", + " {\"role\": \"assistant\", \"content\": response[\"choices\"][0][\"message\"][\"content\"]}\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "for message in message_history:\n", + " if message[\"role\"] in [\"user\", \"assistant\"]:\n", + " print(f\"{message['role'].title()}: {message['content']}\\n\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.14" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/src/search/utilities.py b/search/custom_search_client.py similarity index 95% rename from src/search/utilities.py rename to search/custom_search_client.py index 4c32055..9055d42 100644 --- a/src/search/utilities.py +++ b/search/custom_search_client.py @@ -1,9 +1,9 @@ """ -This module contains the AISearchClient class which is used for +This module contains the SearchClient class which is used for interacting with the AI Search service. Classes: - AISearchClient: A client for interacting with the AI Search service. + CustomSearchClient: A client for interacting with the AI Search service. """ import json @@ -15,7 +15,7 @@ from jinja2 import Template -class SearchClient: +class CustomSearchClient: """A client for interacting with the AI Search service. This class provides methods for loading templates, checking if resources exist, @@ -30,11 +30,11 @@ class SearchClient: def __init__( self, search_endpoint: str, - api_version="2023-10-01-Preview", + search_api_version="2024-03-01-Preview", credential=DefaultAzureCredential(), ): self.search_endpoint: str = search_endpoint - self.api_version: str = api_version + self.api_version: str = search_api_version self.search_management_assets = { "indexers": {"name": None, "payload": None}, diff --git a/src/search/templates/product-info/datasource.json b/search/templates/product-info/datasource.json similarity index 100% rename from src/search/templates/product-info/datasource.json rename to search/templates/product-info/datasource.json diff --git a/src/search/templates/product-info/index.json b/search/templates/product-info/index.json similarity index 98% rename from src/search/templates/product-info/index.json rename to search/templates/product-info/index.json index 9da1ec0..e00cef5 100644 --- a/src/search/templates/product-info/index.json +++ b/search/templates/product-info/index.json @@ -116,7 +116,7 @@ "searchAnalyzer": null, "analyzer": null, "normalizer": null, - "dimensions": "{{ AZURE_AI_SEARCH_VECTOR_EMBEDDING_DIMENSION }}", + "dimensions": "1536", "vectorSearchProfile": "{{ AZURE_AI_SEARCH_INDEX_NAME }}-profile", "synonymMaps": [] } diff --git a/src/search/templates/product-info/indexer.json b/search/templates/product-info/indexer.json similarity index 100% rename from src/search/templates/product-info/indexer.json rename to search/templates/product-info/indexer.json diff --git a/src/search/templates/product-info/skillset.json b/search/templates/product-info/skillset.json similarity index 100% rename from src/search/templates/product-info/skillset.json rename to search/templates/product-info/skillset.json diff --git a/src/app/Dockerfile b/src/app/Dockerfile deleted file mode 100644 index 78cdcab..0000000 --- a/src/app/Dockerfile +++ /dev/null @@ -1,26 +0,0 @@ -# app/Dockerfile - -FROM python:3.9-slim - -WORKDIR /app - -RUN apt-get update && apt-get install -y \ - build-essential \ - curl \ - software-properties-common \ - git \ - && rm -rf /var/lib/apt/lists/* - -COPY ../environment/requirements.txt /app/requirements.txt - -COPY ../orchestration/utilities.py /app/orchestration/utilities.py -COPY ./main.py /app/streamlit_app.py - -RUN pip3 install -r requirements.txt -RUN python3 -m nltk.downloader stopwords - -EXPOSE 8501 - -HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health - -ENTRYPOINT ["streamlit", "run", "streamlit_app.py", "--server.port=8501", "--server.address=0.0.0.0", "--client.toolbarMode='minimal'"]