From 17c1ace73feca4d5cff417b1d90101ea5acda95b Mon Sep 17 00:00:00 2001 From: denniszielke <11569044+denniszielke@users.noreply.github.com> Date: Fri, 31 May 2024 21:04:44 +0200 Subject: [PATCH] Added Azure AI Search Updated python to 3.12 Changed notebooks Added deployment scripts for AI Implemented Managed Identity Auth --- README.md | 80 ++--- azd-hooks/deploy.sh | 8 +- infra/ai/search.bicep | 35 +++ infra/app/phase1.bicep | 15 +- infra/app/phaseX.bicep | 15 +- infra/core/host/container-app-upsert.bicep | 2 + infra/core/host/container-app.bicep | 16 +- .../host/container-apps-environment.bicep | 16 - infra/core/host/container-apps.bicep | 2 - infra/core/security/search-access.bicep | 20 ++ infra/main.bicep | 22 +- requirements.txt | 18 +- src-agents/phase1/Dockerfile | 2 +- src-agents/phase1/notebook_p1.ipynb | 19 +- src-agents/phase1/requirements.txt | 18 +- src-agents/phase2/Dockerfile | 2 +- src-agents/phase2/data/movies/movie.md | 17 -- src-agents/phase2/main.py | 27 ++ src-agents/phase2/movies.csv | 3 + src-agents/phase2/notebook_p2.ipynb | 274 +++++++++++++----- src-agents/phase2/requirements.txt | 18 +- src-agents/phase3/notebook_p3.ipynb | 2 - src-agents/phase4/notebook_p4.ipynb | 8 +- src-agents/phase5/notebook_p5.ipynb | 7 +- 24 files changed, 418 insertions(+), 228 deletions(-) create mode 100644 infra/ai/search.bicep create mode 100644 infra/core/security/search-access.bicep delete mode 100644 src-agents/phase2/data/movies/movie.md create mode 100644 src-agents/phase2/movies.csv diff --git a/README.md b/README.md index 41cfcfe..15f06dd 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ bash ./azd-hooks/deploy.sh phase1 $AZURE_ENV_NAME ### Test the deployed resource ``` -PHASE1_URL="https://phase1.calmbush-f12187c5.swedencentral.azurecontainerapps.io" +PHASE1_URL="https://phase1..swedencentral.azurecontainerapps.io" curl -X 'POST' \ "$PHASE1_URL/ask" \ @@ -73,8 +73,11 @@ uvicorn main:app --reload Test the api with: ``` +URL='http://localhost:8000' +URL='https://phase1..uksouth.azurecontainerapps.io' + curl -X 'POST' \ - 'http://localhost:8000/ask' \ + "$URL/ask" \ -H 'accept: application/json' \ -H 'Content-Type: application/json' \ -d '{ @@ -85,70 +88,53 @@ curl -X 'POST' \ ``` -## Deploy resources for Phase X +## Deploy resources for Phase 1 Run the following script ``` azd env get-values | grep AZURE_ENV_NAME +source <(azd env get-values | grep AZURE_ENV_NAME) bash ./azd-hooks/deploy.sh phase1 $AZURE_ENV_NAME ``` +All the other phases work the same. -## Connect to Qdrant +## Connect to Azure AI Search The deployment will automatically inject the following environment variables into each running container: ``` -QDRANT_PORT=6333 -QDRANT_HOST=qdrant -QDRANT_ENDPOINT=qdrant:6333 -QDRANT_PASSWORD= +AZURE_AI_SEARCH_NAME= +AZURE_AI_SEARCH_ENDPOINT= +AZURE_AI_SEARCH_KEY= ``` -Here is some sample code that you can use to interact with the deployed Qdrant instance. +Here is some sample code that you can use to interact with the deployed Azure AI Search instance. ``` -import os -import openai -from langchain.document_loaders import DirectoryLoader, UnstructuredMarkdownLoader -from langchain_openai import AzureOpenAIEmbeddings - -from langchain_openai import AzureOpenAIEmbeddings -# Create an Embeddings Instance of Azure OpenAI -embeddings = AzureOpenAIEmbeddings( - azure_deployment = os.getenv("AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME"), - openai_api_version = os.getenv("AZURE_OPENAI_VERSION"), - model= os.getenv("AZURE_OPENAI_EMBEDDING_MODEL") -) - -# load your data -data_dir = "data/movies" -documents = DirectoryLoader(path=data_dir, glob="*.md", show_progress=True, loader_cls=UnstructuredMarkdownLoader).load() +from azure.core.credentials import AzureKeyCredential +credential = AzureKeyCredential(os.environ["AZURE_AI_SEARCH_KEY"]) if len(os.environ["AZURE_AI_SEARCH_KEY"]) > 0 else DefaultAzureCredential() -#create chunks -text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0) -document_chunks = text_splitter.split_documents(documents) +from azure.search.documents import SearchClient -from langchain.vectorstores import Qdrant +index_name = "movies-semantic-index" -url = os.getenv('REDIS_PORT') -qdrant = Qdrant.from_documents( - data, - embeddings, - url=url, - prefer_grpc=False, - collection_name="movies", +search_client = SearchClient( + os.environ["AZURE_AI_SEARCH_ENDPOINT"], + azure_ai_search_index_name, + AzureKeyCredential(azure_ai_search_api_key) ) -vectorstore = qdrant +query = "What are the best movies about superheroes?" -query = "Can you suggest similar movies to The Matrix?" - -query_results = qdrant.similarity_search(query) - -for doc in query_results: - print(doc.metadata['source']) +results = list(search_client.search( + search_text=query, + query_type="simple", + include_total_count=True, + top=5 +)) + ``` ## Connect to Redis @@ -167,7 +153,6 @@ Here is some sample code that you can use to interact with the deployed redis in ``` import redis import os -import openai # Redis connection details redis_host = os.getenv('REDIS_HOST') @@ -180,11 +165,4 @@ conn = redis.Redis(host=redis_host, port=redis_port, password=redis_password, en if conn.ping(): print("Connected to Redis") -query = "Who is iron man?" - -# Vectorize the query using OpenAI's text-embedding-ada-002 model -print("Vectorizing query...") -embedding = openai.Embedding.create(input=query, model=os.getenv("AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME")) -query_vector = embedding["data"][0]["embedding"] - ``` \ No newline at end of file diff --git a/azd-hooks/deploy.sh b/azd-hooks/deploy.sh index 29d4eb6..a5498e8 100644 --- a/azd-hooks/deploy.sh +++ b/azd-hooks/deploy.sh @@ -38,6 +38,7 @@ AZURE_CONTAINER_REGISTRY_NAME=$(az resource list -g $RESOURCE_GROUP --resource-t OPENAI_NAME=$(az resource list -g $RESOURCE_GROUP --resource-type "Microsoft.CognitiveServices/accounts" --query "[0].name" -o tsv) ENVIRONMENT_NAME=$(az resource list -g $RESOURCE_GROUP --resource-type "Microsoft.App/managedEnvironments" --query "[0].name" -o tsv) IDENTITY_NAME=$(az resource list -g $RESOURCE_GROUP --resource-type "Microsoft.ManagedIdentity/userAssignedIdentities" --query "[0].name" -o tsv) +SEARCH_NAME=$(az resource list -g $RESOURCE_GROUP --resource-type "Microsoft.Search/searchServices" --query "[0].name" -o tsv) SERVICE_NAME=$PHASE AZURE_SUBSCRIPTION_ID=$(az account show --query id -o tsv) @@ -47,6 +48,7 @@ echo "openai name: $OPENAI_NAME" echo "environment name: $ENVIRONMENT_NAME" echo "identity name: $IDENTITY_NAME" echo "service name: $SERVICE_NAME" +echo "search name: $SEARCH_NAME" CONTAINER_APP_EXISTS=$(az resource list -g $RESOURCE_GROUP --resource-type "Microsoft.App/containerApps" --query "[?contains(name, '$SERVICE_NAME')].id" -o tsv) EXISTS="false" @@ -61,7 +63,9 @@ fi az acr build --subscription ${AZURE_SUBSCRIPTION_ID} --registry ${AZURE_CONTAINER_REGISTRY_NAME} --image $SERVICE_NAME:latest ./src-agents/$SERVICE_NAME IMAGE_NAME="${AZURE_CONTAINER_REGISTRY_NAME}.azurecr.io/$SERVICE_NAME:latest" -az deployment group create -g $RESOURCE_GROUP -f ./infra/app/phaseX.bicep \ +URI=$(az deployment group create -g $RESOURCE_GROUP -f ./infra/app/phaseX.bicep \ -p name=$SERVICE_NAME -p location=$LOCATION -p containerAppsEnvironmentName=$ENVIRONMENT_NAME \ -p containerRegistryName=$AZURE_CONTAINER_REGISTRY_NAME -p applicationInsightsName=$APPINSIGHTS_NAME \ - -p openaiName=$OPENAI_NAME -p identityName=$IDENTITY_NAME -p imageName=$IMAGE_NAME -p exists=$EXISTS \ No newline at end of file + -p openaiName=$OPENAI_NAME -p searchName=$SEARCH_NAME -p identityName=$IDENTITY_NAME -p imageName=$IMAGE_NAME -p exists=$EXISTS --query properties.outputs.uri.value) + +echo "deployment uri: $URI" \ No newline at end of file diff --git a/infra/ai/search.bicep b/infra/ai/search.bicep new file mode 100644 index 0000000..f909680 --- /dev/null +++ b/infra/ai/search.bicep @@ -0,0 +1,35 @@ +param name string +param location string = resourceGroup().location +param tags object = {} + +resource search 'Microsoft.Search/searchServices@2023-11-01' = { + name: name + location: location + sku: { + name: 'standard' + } + tags: tags + properties: { + authOptions: { + aadOrApiKey: { + aadAuthFailureMode: 'http401WithBearerChallenge' + } + } + disableLocalAuth: false + encryptionWithCmk: { + enforcement: 'Unspecified' + } + hostingMode: 'Default' + networkRuleSet: { + ipRules: [] + bypass: 'None' + } + partitionCount: 1 + publicNetworkAccess: 'Enabled' + replicaCount: 1 + } +} + +output searchName string = search.name +output searchEndpoint string = 'https://${search.name}.search.windows.net' +output searchAdminKey string = listAdminKeys(search.id, '2023-11-01').primaryKey diff --git a/infra/app/phase1.bicep b/infra/app/phase1.bicep index 79a072b..41495e0 100644 --- a/infra/app/phase1.bicep +++ b/infra/app/phase1.bicep @@ -22,6 +22,8 @@ param containerRegistryName string param serviceName string = 'phase1' param imageName string param openaiApiVersion string +param searchName string +param searchEndpoint string resource apiIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { name: identityName @@ -40,6 +42,7 @@ module app '../core/host/container-app-upsert.bicep' = { openaiName: openaiName containerAppsEnvironmentName: containerAppsEnvironmentName containerRegistryName: containerRegistryName + searchName: searchName env: [ { name: 'AZURE_CLIENT_ID' @@ -49,10 +52,14 @@ module app '../core/host/container-app-upsert.bicep' = { name: 'APPLICATIONINSIGHTS_CONNECTION_STRING' value: applicationInsights.properties.ConnectionString } - // { - // name: 'AZURE_OPENAI_API_KEY' - // value: openaiApiKey - // } + { + name: 'AZURE_AI_SEARCH_NAME' + value: searchName + } + { + name: 'AZURE_AI_SEARCH_ENDPOINT' + value: searchEndpoint + } { name: 'AZURE_OPENAI_ENDPOINT' value: openaiEndpoint diff --git a/infra/app/phaseX.bicep b/infra/app/phaseX.bicep index 23154c5..5e6555e 100644 --- a/infra/app/phaseX.bicep +++ b/infra/app/phaseX.bicep @@ -7,6 +7,7 @@ param applicationInsightsName string param identityName string param openaiName string param imageName string +param searchName string var tags = { 'azd-env-name': containerAppsEnvironmentName } var completionDeploymentModelName = 'gpt-35-turbo' @@ -35,6 +36,7 @@ module app '../core/host/container-app-upsert.bicep' = { openaiName: openaiName containerAppsEnvironmentName: containerAppsEnvironmentName containerRegistryName: containerRegistryName + searchName: searchName env: [ { name: 'AZURE_CLIENT_ID' @@ -44,10 +46,14 @@ module app '../core/host/container-app-upsert.bicep' = { name: 'APPLICATIONINSIGHTS_CONNECTION_STRING' value: applicationInsights.properties.ConnectionString } - // { - // name: 'AZURE_OPENAI_API_KEY' - // value: openaiApiKey - // } + { + name: 'AZURE_AI_SEARCH_NAME' + value: searchName + } + { + name: 'AZURE_AI_SEARCH_ENDPOINT' + value: 'https://${searchName}.search.windows.net' + } { name: 'AZURE_OPENAI_ENDPOINT' value: openaiEndpoint @@ -77,3 +83,4 @@ output SERVICE_API_IDENTITY_PRINCIPAL_ID string = apiIdentity.properties.princip output SERVICE_API_NAME string = app.outputs.name output SERVICE_API_URI string = app.outputs.uri output SERVICE_API_IMAGE_NAME string = app.outputs.imageName +output uri string = app.outputs.uri diff --git a/infra/core/host/container-app-upsert.bicep b/infra/core/host/container-app-upsert.bicep index 7d0c875..d0c156e 100644 --- a/infra/core/host/container-app-upsert.bicep +++ b/infra/core/host/container-app-upsert.bicep @@ -12,6 +12,7 @@ param external bool = true param targetPort int = 8080 param exists bool param openaiName string +param searchName string @description('User assigned identity name') param identityName string = '' @@ -44,6 +45,7 @@ module app 'container-app.bicep' = { imageName: imageName targetPort: targetPort openaiName: openaiName + searchName: searchName } } diff --git a/infra/core/host/container-app.bicep b/infra/core/host/container-app.bicep index f88b394..b3cab50 100644 --- a/infra/core/host/container-app.bicep +++ b/infra/core/host/container-app.bicep @@ -11,6 +11,7 @@ param external bool = true param imageName string param targetPort int = 80 param openaiName string +param searchName string @description('User assigned identity name') param identityName string = '' @@ -41,6 +42,14 @@ module containerRegistryAccess '../security/registry-access.bicep' = { } } +module searchAccess '../security/search-access.bicep' = { + name: '${deployment().name}-search-access' + params: { + searchName: searchName + principalId: userIdentity.properties.principalId + } +} + resource app 'Microsoft.App/containerApps@2023-04-01-preview' = { name: name location: location @@ -73,9 +82,6 @@ resource app 'Microsoft.App/containerApps@2023-04-01-preview' = { } template: { serviceBinds : [ - { - serviceId: qdrant.id - } { serviceId: redis.id } @@ -107,9 +113,7 @@ resource redis 'Microsoft.App/containerApps@2023-04-01-preview' existing = { name: 'redis' } -resource qdrant 'Microsoft.App/containerApps@2023-04-01-preview' existing = { - name: 'qdrant' -} + output defaultDomain string = containerAppsEnvironment.properties.defaultDomain output imageName string = imageName diff --git a/infra/core/host/container-apps-environment.bicep b/infra/core/host/container-apps-environment.bicep index 054acec..b580684 100644 --- a/infra/core/host/container-apps-environment.bicep +++ b/infra/core/host/container-apps-environment.bicep @@ -19,20 +19,6 @@ resource containerAppsEnvironment 'Microsoft.App/managedEnvironments@2022-03-01' } } -resource qdrant 'Microsoft.App/containerApps@2023-04-01-preview' = { - name: 'qdrant' - location: location - tags: tags - properties: { - environmentId: containerAppsEnvironment.id - configuration: { - service: { - type: 'qdrant' - } - } - } -} - resource redis 'Microsoft.App/containerApps@2023-04-01-preview' = { name: 'redis' location: location @@ -53,5 +39,3 @@ resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2022-10 output defaultDomain string = containerAppsEnvironment.properties.defaultDomain output name string = containerAppsEnvironment.name -output redisEndpoint string = redis.properties.configuration.ingress.fqdn -output qdrantEndpoint string = qdrant.properties.configuration.ingress.fqdn diff --git a/infra/core/host/container-apps.bicep b/infra/core/host/container-apps.bicep index 7b45673..245ed7f 100644 --- a/infra/core/host/container-apps.bicep +++ b/infra/core/host/container-apps.bicep @@ -29,5 +29,3 @@ output defaultDomain string = containerAppsEnvironment.outputs.defaultDomain output environmentName string = containerAppsEnvironment.outputs.name output registryLoginServer string = containerRegistry.outputs.loginServer output registryName string = containerRegistry.outputs.name -output redisEndpoint string = containerAppsEnvironment.outputs.redisEndpoint -output qdrantEndpoint string = containerAppsEnvironment.outputs.qdrantEndpoint diff --git a/infra/core/security/search-access.bicep b/infra/core/security/search-access.bicep new file mode 100644 index 0000000..d8aac8f --- /dev/null +++ b/infra/core/security/search-access.bicep @@ -0,0 +1,20 @@ +param searchName string +param principalId string + +// https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles +// Search Index Data Reader +var openAiUserRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '1407120a-92aa-4202-b7e9-c0e197c71c8f') + +resource searchAccess 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + scope: search // Use when specifying a scope that is different than the deployment scope + name: guid(subscription().id, resourceGroup().id, principalId, openAiUserRole) + properties: { + roleDefinitionId: openAiUserRole + principalType: 'ServicePrincipal' + principalId: principalId + } +} + +resource search 'Microsoft.Search/searchServices@2023-11-01' existing = { + name: searchName +} diff --git a/infra/main.bicep b/infra/main.bicep index c88b28a..060b282 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -83,6 +83,8 @@ module phase1 './app/phase1.bicep' = { containerAppsEnvironmentName: containerApps.outputs.environmentName containerRegistryName: containerApps.outputs.registryName openaiName: openai.outputs.openaiName + searchName: search.outputs.searchName + searchEndpoint: search.outputs.searchEndpoint openaiApiVersion: openaiApiVersion openaiEndpoint: openai.outputs.openaiEndpoint completionDeploymentName: completionDeploymentModelName @@ -90,7 +92,7 @@ module phase1 './app/phase1.bicep' = { } } -// Monitor application with Azure Monitor +// Azure OpenAI Model module openai './ai/openai.bicep' = { name: 'openai' scope: resourceGroup @@ -103,6 +105,17 @@ module openai './ai/openai.bicep' = { } } +// Azure AI Search +module search './ai/search.bicep' = { + name: 'search' + scope: resourceGroup + params: { + location: location + tags: tags + name: !empty(openaiName) ? openaiName : '${abbrs.searchSearchServices}${resourceToken}' + } +} + // Monitor application with Azure Monitor module monitoring './core/monitor/monitoring.bicep' = { name: 'monitoring' @@ -135,7 +148,6 @@ output AZURE_OPENAI_COMPLETION_DEPLOYMENT_NAME string = completionDeploymentMode output AZURE_OPENAI_EMBEDDING_MODEL string = embeddingModelName output AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME string = embeddingDeploymentModelName output PHASE1_URL string = phase1.outputs.SERVICE_API_URI -output QDRANT_ENDPOINT string = containerApps.outputs.qdrantEndpoint -output QDRANT_PASSWORD string = '' -output REDIS_ENDPOINT string = containerApps.outputs.redisEndpoint -output REDIS_PASSWORD string = '' +output AZURE_AI_SEARCH_NAME string = search.outputs.searchName +output AZURE_AI_SEARCH_ENDPOINT string = search.outputs.searchEndpoint +output AZURE_AI_SEARCH_KEY string = search.outputs.searchAdminKey diff --git a/requirements.txt b/requirements.txt index a84d6d1..07e2736 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,12 +1,14 @@ langchain==0.2.0 -langchain-community==0.2.0 -langchain-openai==0.1.7 -openai==1.30.1 -pydantic==2.7.1 +langchain-community==0.2.1 +langchain-core==0.2.3 +langchain-openai==0.1.8 +openai==1.30.5 +pydantic==2.6.4 python-dotenv==1.0.1 -quadrant==1.0 -requests==2.32.0 tiktoken==0.7.0 markdown==3.6 -unstructured==0.14.2 - +unstructured==0.14.3 +azure-search-documents==11.4.0 +azure-identity==1.15.0 +uvicorn==0.23.2 +fastapi==0.110.0 \ No newline at end of file diff --git a/src-agents/phase1/Dockerfile b/src-agents/phase1/Dockerfile index fe3679d..f2a5035 100644 --- a/src-agents/phase1/Dockerfile +++ b/src-agents/phase1/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.11 +FROM python:3.12 COPY . . diff --git a/src-agents/phase1/notebook_p1.ipynb b/src-agents/phase1/notebook_p1.ipynb index 634073e..21df468 100644 --- a/src-agents/phase1/notebook_p1.ipynb +++ b/src-agents/phase1/notebook_p1.ipynb @@ -18,12 +18,7 @@ "metadata": {}, "outputs": [], "source": [ - "import json\n", - "import requests\n", "import os\n", - "import openai\n", - "from enum import Enum\n", - "from pydantic import BaseModel\n", "from openai import AzureOpenAI\n", "from dotenv import load_dotenv\n", "\n", @@ -60,12 +55,12 @@ "metadata": {}, "outputs": [], "source": [ - "# response = client.chat.completions.create(\n", - "# model = model_name, \n", - "# messages = [{\"role\" : \"assistant\", \"content\" : \"The one thing I love more than anything else is \"}],\n", - "# )\n", + "response = client.chat.completions.create(\n", + " model = model_name, \n", + " messages = [{\"role\" : \"assistant\", \"content\" : \"The one thing I love more than anything else is \"}],\n", + ")\n", "\n", - "# print(response)\n" + "print(response.choices[0].message.content)" ] }, { @@ -81,6 +76,8 @@ "metadata": {}, "outputs": [], "source": [ + "from enum import Enum\n", + "from pydantic import BaseModel\n", "\n", "class QuestionType(str, Enum):\n", " multiple_choice = \"multiple_choice\"\n", @@ -186,7 +183,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.9" + "version": "3.12.3" } }, "nbformat": 4, diff --git a/src-agents/phase1/requirements.txt b/src-agents/phase1/requirements.txt index abc54a4..07e2736 100644 --- a/src-agents/phase1/requirements.txt +++ b/src-agents/phase1/requirements.txt @@ -1,6 +1,14 @@ -fastapi==0.110.0 -uvicorn==0.23.2 +langchain==0.2.0 +langchain-community==0.2.1 +langchain-core==0.2.3 +langchain-openai==0.1.8 +openai==1.30.5 pydantic==2.6.4 -openai==1.14.2 -python-dotenv==1.0.0 -azure-identity==1.15.0 \ No newline at end of file +python-dotenv==1.0.1 +tiktoken==0.7.0 +markdown==3.6 +unstructured==0.14.3 +azure-search-documents==11.4.0 +azure-identity==1.15.0 +uvicorn==0.23.2 +fastapi==0.110.0 \ No newline at end of file diff --git a/src-agents/phase2/Dockerfile b/src-agents/phase2/Dockerfile index fe3679d..f2a5035 100644 --- a/src-agents/phase2/Dockerfile +++ b/src-agents/phase2/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.11 +FROM python:3.12 COPY . . diff --git a/src-agents/phase2/data/movies/movie.md b/src-agents/phase2/data/movies/movie.md deleted file mode 100644 index b4c4924..0000000 --- a/src-agents/phase2/data/movies/movie.md +++ /dev/null @@ -1,17 +0,0 @@ -# Ant-Man and the Smorgs: Smorgmania - -## Overview - - Super-Hero partners Smorg Smorisson , along with with Smorg McLean and Jenny Smorgth , find themselves exploring the Smorgs Realm, interacting with strange new Smorgss and embarking on an Smorg adventure that will push them beyond the limits of what a Smorgs thought possible. - -## Details - -**Release Date:** 2026-02-15 - -**Genres:** Action, Adventure, Science Fiction, Smorgsart - -**Popularity:** 11525.418 - -**Vote Average:** 9.5 - -**Keywords:** hero, ant, sequel, superhero, based on comic, family, superhero team, aftercreditsstinger, duringcreditsstinger, smorgs diff --git a/src-agents/phase2/main.py b/src-agents/phase2/main.py index 54d560b..d70e818 100644 --- a/src-agents/phase2/main.py +++ b/src-agents/phase2/main.py @@ -5,6 +5,8 @@ from enum import Enum from openai import AzureOpenAI from azure.identity import DefaultAzureCredential, get_bearer_token_provider +from azure.search.documents import SearchClient +from azure.core.credentials import AzureKeyCredential app = FastAPI() @@ -44,6 +46,20 @@ class Answer(BaseModel): ) deployment_name = os.getenv("AZURE_OPENAI_COMPLETION_DEPLOYMENT_NAME") +index_name = "movies-semantic-index" +service_endpoint = os.getenv("AZURE_AI_SEARCH_ENDPOINT") + +credential = None +if "AZURE_AI_SEARCH_KEY" in os.environ: + credential = AzureKeyCredential(os.environ["AZURE_AI_SEARCH_KEY"]) +else: + credential = DefaultAzureCredential() + +search_client = SearchClient( + service_endpoint, + index_name, + credential +) @app.get("/") async def root(): @@ -61,6 +77,17 @@ async def ask_question(ask: Ask): Ask a question """ + # This is not using a semantic search, but a simple search + results = list(search_client.search( + search_text=ask.question, + query_type="simple", + include_total_count=True, + top=5 + )) + + print('Search results:') + print(results) + # Send a completion call to generate an answer print('Sending a request to openai') start_phrase = ask.question diff --git a/src-agents/phase2/movies.csv b/src-agents/phase2/movies.csv new file mode 100644 index 0000000..f95cabb --- /dev/null +++ b/src-agents/phase2/movies.csv @@ -0,0 +1,3 @@ +id;genre;title;release_year;vote_count;overview;runtime;tagline +1;Science;Ant-Man and the Smorgs: Smorgmania;2026;9;Super-Hero partners Smorg Smorisson , along with with Smorg McLean and Jenny Smorgth , find themselves exploring the Smorgs Realm, interacting with strange new Smorgss and embarking on an Smorg adventure that will push them beyond the limits of what a Smorgs thought possible.;20;Smorghs kick but of Antman +2;Romance;Ant-Man and the Smorgs: Smorghboath;2026;1;Super-Hero partners Smorg Smorisson , along with with Smorg McLean and Jenny Smorgth , find themselves exploring on the Smorgh sailing boat;20;Smorghs sail the sea \ No newline at end of file diff --git a/src-agents/phase2/notebook_p2.ipynb b/src-agents/phase2/notebook_p2.ipynb index a046c7e..42e47ef 100644 --- a/src-agents/phase2/notebook_p2.ipynb +++ b/src-agents/phase2/notebook_p2.ipynb @@ -16,39 +16,111 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Found Azure OpenAI API Base Endpoint: https://cog-fkplarx5db7we.openai.azure.com/\n" - ] - } - ], + "outputs": [], "source": [ - "import json\n", - "import requests\n", "import os\n", - "import openai\n", - "from enum import Enum\n", - "from pydantic import BaseModel\n", - "from openai import AzureOpenAI\n", "from dotenv import load_dotenv\n", "\n", "# Load environment variables\n", "if load_dotenv():\n", " print(\"Found Azure OpenAI API Base Endpoint: \" + os.getenv(\"AZURE_OPENAI_ENDPOINT\"))\n", "else: \n", - " print(\"Azure OpenAI API Base Endpoint not found. Have you configured the .env file?\")\n", - " \n", - "API_KEY = os.getenv(\"AZURE_OPENAI_API_KEY\")\n", - "API_VERSION = os.getenv(\"OPENAI_API_VERSION\")\n", - "RESOURCE_ENDPOINT = os.getenv(\"AZURE_OPENAI_ENDPOINT\")\n", + " print(\"Azure OpenAI API Base Endpoint not found. Have you configured the .env file?\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Create vector search index\n", "\n", - "deployment_name = os.getenv(\"AZURE_OPENAI_COMPLETION_DEPLOYMENT_NAME\")\n", - "model_name = os.getenv(\"AZURE_OPENAI_COMPLETION_MODEL\")" + "Create your search index schema and vector search configuration" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from azure.identity import DefaultAzureCredential\n", + "from azure.core.credentials import AzureKeyCredential\n", + "\n", + "from azure.search.documents.indexes import SearchIndexClient\n", + "from azure.search.documents.indexes.models import (\n", + " SimpleField,\n", + " SearchFieldDataType,\n", + " SearchableField,\n", + " SearchField,\n", + " VectorSearch,\n", + " HnswAlgorithmConfiguration,\n", + " VectorSearchProfile,\n", + " SemanticConfiguration,\n", + " SemanticPrioritizedFields,\n", + " SemanticField,\n", + " SemanticSearch,\n", + " SearchIndex\n", + "\n", + ")\n", + "\n", + "credential = AzureKeyCredential(os.environ[\"AZURE_AI_SEARCH_KEY\"]) if len(os.environ[\"AZURE_AI_SEARCH_KEY\"]) > 0 else DefaultAzureCredential()\n", + "\n", + "index_name = \"movies-semantic-index\"\n", + "\n", + "index_client = SearchIndexClient(\n", + " endpoint=os.environ[\"AZURE_AI_SEARCH_ENDPOINT\"], \n", + " credential=credential\n", + ")\n", + "\n", + "# Create a search index with the fields and a vector field which we will fill with a vector based on the overview field\n", + "fields = [\n", + " SimpleField(name=\"id\", type=SearchFieldDataType.String, key=True, sortable=True, filterable=True, facetable=True),\n", + " SearchableField(name=\"genre\", type=SearchFieldDataType.String),\n", + " SearchableField(name=\"title\", type=SearchFieldDataType.String),\n", + " SearchableField(name=\"release_year\", type=SearchFieldDataType.Int32),\n", + " SearchableField(name=\"vote_count\", type=SearchFieldDataType.Int32),\n", + " SearchableField(name=\"overview\", type=SearchFieldDataType.String),\n", + " SearchableField(name=\"runtime\", type=SearchFieldDataType.Int32),\n", + " SearchableField(name=\"tagline\", type=SearchFieldDataType.String),\n", + " SearchField(name=\"vector\", type=SearchFieldDataType.Collection(SearchFieldDataType.Single),\n", + " searchable=True, vector_search_dimensions=1536, vector_search_profile_name=\"myHnswProfile\"),\n", + "]\n", + "\n", + "# Configure the vector search configuration \n", + "vector_search = VectorSearch(\n", + " algorithms=[\n", + " HnswAlgorithmConfiguration(\n", + " name=\"myHnsw\"\n", + " )\n", + " ],\n", + " profiles=[\n", + " VectorSearchProfile(\n", + " name=\"myHnswProfile\",\n", + " algorithm_configuration_name=\"myHnsw\",\n", + " )\n", + " ]\n", + ")\n", + "\n", + "# Configure the semantic search configuration to prefer title and tagline fields over overview\n", + "semantic_config = SemanticConfiguration(\n", + " name=\"movies-semantic-config\",\n", + " prioritized_fields=SemanticPrioritizedFields(\n", + " title_field=SemanticField(field_name=\"title\"),\n", + " keywords_fields=[SemanticField(field_name=\"tagline\")],\n", + " content_fields=[SemanticField(field_name=\"overview\")]\n", + " )\n", + ")\n", + "\n", + "# Create the semantic settings with the configuration\n", + "semantic_search = SemanticSearch(configurations=[semantic_config])\n", + "\n", + "# Create the search index with the semantic settings\n", + "index = SearchIndex(name=index_name, fields=fields,\n", + " vector_search=vector_search, semantic_search=semantic_search)\n", + "result = index_client.create_or_update_index(index)\n", + "print(f' {result.name} created')" ] }, { @@ -60,10 +132,12 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ + "from enum import Enum\n", + "from pydantic import BaseModel\n", "\n", "class QuestionType(str, Enum):\n", " multiple_choice = \"multiple_choice\"\n", @@ -93,40 +167,14 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "100%|██████████| 1/1 [00:00<00:00, 131.69it/s]" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "The latest Ant-Man movie, \"Ant-Man and the Smorgs: Smorgmania,\" was released on February 15, 2026. It is an action-adventure science fiction film, featuring superhero partners Smorg Smorisson, Smorg McLean, and Jenny Smorgth. In this movie, they explore the Smorgs Realm and encounter strange new Smorgs, embarking on an adventurous journey that pushes their limits. The movie has gained popularity with an average vote of 9.5.\n" - ] - } - ], + "outputs": [], "source": [ - "from langchain.document_loaders import DirectoryLoader, UnstructuredMarkdownLoader\n", - "from langchain_openai import AzureOpenAIEmbeddings\n", - "from langchain.vectorstores import Qdrant\n", - "\n", "import os\n", - "from openai import AzureOpenAI\n", - "from langchain.vectorstores import Qdrant\n", - "\n", + "import csv\n", + "from langchain_openai import AzureOpenAIEmbeddings\n", + "from azure.search.documents import SearchClient\n", "\n", "# use an embeddingsmodel to create embeddings\n", "embeddings_model = AzureOpenAIEmbeddings( \n", @@ -135,7 +183,67 @@ " model= os.getenv(\"AZURE_OPENAI_EMBEDDING_MODEL\")\n", ")\n", "\n", - "AZURE_OPENAI_VERSION=\"2024-02-01\"\n", + "# 1. define function to parse csv row and create embedding for overview text\n", + "def parseRow(row: list[str]):\n", + " return dict([\n", + " (\"id\", row[0]),\n", + " (\"genre\", row[1]),\n", + " (\"title\", row[2]),\n", + " (\"release_year\", row[3]),\n", + " (\"vote_count\", row[4]),\n", + " (\"overview\", row[5]),\n", + " (\"runtime\", row[6]),\n", + " (\"tagline\", row[7]),\n", + " (\"vector\", embeddings_model.embed_query(row[5]))\n", + " ])\n", + "\n", + "# 2. load movies from csv\n", + "movies = []\n", + "with open('./movies.csv') as csv_file:\n", + " csv_reader = csv.reader(csv_file, delimiter=';')\n", + " line_count = 0\n", + " for row in csv_reader:\n", + " if line_count == 0:\n", + " print(f'Column names are {\", \".join(row)}')\n", + " line_count += 1\n", + " else:\n", + " movie = parseRow(row)\n", + " print(movie)\n", + " movies.append(movie)\n", + " line_count += 1\n", + " print(f'Processed {line_count} lines.')\n", + "print('Loaded %s movies.' % len(movies))\n", + "\n", + "\n", + "# 3. upload documents to vector store\n", + "search_client = SearchClient(\n", + " endpoint=os.environ[\"AZURE_AI_SEARCH_ENDPOINT\"], \n", + " index_name=index_name,\n", + " credential=credential\n", + ")\n", + "\n", + "result = search_client.upload_documents(movies)\n", + "print(f\"Successfully loaded {len(movies)} movies into Azure AI Search index.\")\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Query index and create a response" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from openai import AzureOpenAI\n", + "from azure.search.documents.models import (\n", + " VectorizedQuery\n", + ")\n", "\n", "client = AzureOpenAI(\n", " api_key = os.getenv(\"AZURE_OPENAI_API_KEY\"), \n", @@ -143,33 +251,43 @@ " azure_endpoint = os.getenv(\"AZURE_OPENAI_ENDPOINT\")\n", " )\n", "\n", - "# 1. load data into qdrant db in aca or locally\n", - "# load your data\n", - "data_dir = \"data/movies\"\n", - "documents = DirectoryLoader(path=data_dir, glob=\"*.md\", show_progress=True, loader_cls=UnstructuredMarkdownLoader).load()\n", - "\n", - "# use local quadrant (for debugging)\n", - "qdrant = Qdrant.from_documents(\n", - " documents,\n", - " embeddings_model,\n", - " location=\":memory:\",\n", - " collection_name=\"movies\",\n", + "deployment_name = os.getenv(\"AZURE_OPENAI_COMPLETION_DEPLOYMENT_NAME\")\n", + "model_name = os.getenv(\"AZURE_OPENAI_COMPLETION_MODEL\")\n", + "\n", + "index_client = SearchClient(\n", + " endpoint=os.environ[\"AZURE_AI_SEARCH_ENDPOINT\"], \n", + " index_name=index_name,\n", + " credential=credential\n", ")\n", "\n", - "# use quadrant in aca\n", - "# # --> aus Env, URL von Quadrant\n", - "# url = \"http://localhost:6333\"\n", - "# qdrant = Qdrant.from_documents(documents,embeddings_model,url=url,prefer_grpc=False,collection_name=\"my_movies\",)\n", + "question = \"Tell me about the latest Ant Man movie. When was it released?\"\n", + "\n", + "# create a vectorized query based on the question\n", + "vector = VectorizedQuery(vector=embeddings_model.embed_query(question), k_nearest_neighbors=5, fields=\"vector\")\n", + "\n", + "\n", + "# create search client to retrieve movies from the vector store\n", + "found_docs = list(search_client.search(\n", + " search_text=None,\n", + " query_type=\"semantic\",\n", + " semantic_configuration_name=\"movies-semantic-config\",\n", + " vector_queries=[vector],\n", + " select=[\"title\", \"genre\", \"overview\"],\n", + " top=5\n", + "))\n", + "\n", + "# print the found documents and the field that were selected\n", + "for doc in found_docs:\n", + " print(\"Movie: {}\".format(doc[\"title\"]))\n", + " print(\"Genre: {}\".format(doc[\"genre\"]))\n", + " print(\"----------\")\n", "\n", "\n", - "# 2. find relevant chunks from vector db\n", - "question = \"Tell me about the latest Ant Man movie. When was it released?\"\n", - "found_docs = qdrant.similarity_search(question)\n", "found_docs_as_text = \" \"\n", "for elem in enumerate(found_docs, start=1): \n", - " found_docs_as_text += \" \"+ elem[1].page_content\n", + " found_docs_as_text += \" \"+ \"Movie Title: {}\".format(doc[\"title\"]) +\" \"+ \"Movie story: {}\".format(doc[\"overview\"])\n", "\n", - "# 3. ask llm based on releveant chunks\n", + "# augment the question with the found documents and ask the LLM to generate a response\n", "system_prompt = \"You are an assistant to the user, you are given some context below, please answer the query of the user with as detail as possible\"\n", "\n", "parameters = [system_prompt, ' Context:', found_docs_as_text , ' Question:', question]\n", @@ -180,7 +298,7 @@ " messages = [{\"role\" : \"assistant\", \"content\" : joined_parameters}],\n", " )\n", "\n", - "print (response.choices[0].message.content)\n" + "print (response.choices[0].message.content)" ] }, { @@ -263,7 +381,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.9" + "version": "3.12.3" } }, "nbformat": 4, diff --git a/src-agents/phase2/requirements.txt b/src-agents/phase2/requirements.txt index abc54a4..07e2736 100644 --- a/src-agents/phase2/requirements.txt +++ b/src-agents/phase2/requirements.txt @@ -1,6 +1,14 @@ -fastapi==0.110.0 -uvicorn==0.23.2 +langchain==0.2.0 +langchain-community==0.2.1 +langchain-core==0.2.3 +langchain-openai==0.1.8 +openai==1.30.5 pydantic==2.6.4 -openai==1.14.2 -python-dotenv==1.0.0 -azure-identity==1.15.0 \ No newline at end of file +python-dotenv==1.0.1 +tiktoken==0.7.0 +markdown==3.6 +unstructured==0.14.3 +azure-search-documents==11.4.0 +azure-identity==1.15.0 +uvicorn==0.23.2 +fastapi==0.110.0 \ No newline at end of file diff --git a/src-agents/phase3/notebook_p3.ipynb b/src-agents/phase3/notebook_p3.ipynb index 25d548f..93cde2b 100644 --- a/src-agents/phase3/notebook_p3.ipynb +++ b/src-agents/phase3/notebook_p3.ipynb @@ -20,9 +20,7 @@ "outputs": [], "source": [ "import json\n", - "import requests\n", "import os\n", - "import openai\n", "from enum import Enum\n", "from pydantic import BaseModel\n", "from openai import AzureOpenAI\n", diff --git a/src-agents/phase4/notebook_p4.ipynb b/src-agents/phase4/notebook_p4.ipynb index 59a0983..0d93837 100644 --- a/src-agents/phase4/notebook_p4.ipynb +++ b/src-agents/phase4/notebook_p4.ipynb @@ -27,13 +27,9 @@ } ], "source": [ - "import json\n", - "import requests\n", "import os\n", - "import openai\n", "import tiktoken\n", - "from enum import Enum\n", - "from pydantic import BaseModel\n", + "\n", "from openai import AzureOpenAI\n", "from dotenv import load_dotenv\n", "\n", @@ -70,6 +66,8 @@ "metadata": {}, "outputs": [], "source": [ + "from enum import Enum\n", + "from pydantic import BaseModel\n", "\n", "class QuestionType(str, Enum):\n", " multiple_choice = \"multiple_choice\"\n", diff --git a/src-agents/phase5/notebook_p5.ipynb b/src-agents/phase5/notebook_p5.ipynb index 40e157c..a548903 100644 --- a/src-agents/phase5/notebook_p5.ipynb +++ b/src-agents/phase5/notebook_p5.ipynb @@ -19,13 +19,8 @@ "metadata": {}, "outputs": [], "source": [ - "import json\n", - "import requests\n", "import os\n", - "import openai\n", "import tiktoken\n", - "from enum import Enum\n", - "from pydantic import BaseModel\n", "from openai import AzureOpenAI\n", "from dotenv import load_dotenv\n", "\n", @@ -62,6 +57,8 @@ "metadata": {}, "outputs": [], "source": [ + "from enum import Enum\n", + "from pydantic import BaseModel\n", "\n", "class QuestionType(str, Enum):\n", " multiple_choice = \"multiple_choice\"\n",