diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 92d09cc..b42805a 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -15,7 +15,8 @@ "ms-azuretools.vscode-cosmosdb", "buildwithlayer.mongodb-integration-expert-qS6DB", "mongodb.mongodb-vscode", - "ms-azuretools.vscode-documentdb" + "ms-azuretools.vscode-documentdb", + "ms-azuretools.vscode-bicep" ] } } diff --git a/.devcontainer/python/devcontainer.json b/.devcontainer/python/devcontainer.json index 5f32367..97dbc75 100644 --- a/.devcontainer/python/devcontainer.json +++ b/.devcontainer/python/devcontainer.json @@ -15,7 +15,8 @@ "ms-azuretools.vscode-cosmosdb", "buildwithlayer.mongodb-integration-expert-qS6DB", "mongodb.mongodb-vscode", - "ms-azuretools.vscode-documentdb" + "ms-azuretools.vscode-documentdb", + "ms-azuretools.vscode-bicep" ] } } diff --git a/.devcontainer/typescript/devcontainer.json b/.devcontainer/typescript/devcontainer.json index 8786d10..3d819a6 100644 --- a/.devcontainer/typescript/devcontainer.json +++ b/.devcontainer/typescript/devcontainer.json @@ -15,7 +15,8 @@ "ms-azuretools.vscode-cosmosdb", "buildwithlayer.mongodb-integration-expert-qS6DB", "mongodb.mongodb-vscode", - "ms-azuretools.vscode-documentdb" + "ms-azuretools.vscode-documentdb", + "ms-azuretools.vscode-bicep" ] } } diff --git a/.gitignore b/.gitignore index 655ba7c..3bc6c33 100644 --- a/.gitignore +++ b/.gitignore @@ -432,4 +432,5 @@ FodyWeavers.xsd # TypeScript dist/ build/ -node_modules/ \ No newline at end of file +node_modules/ +.tsbuildinfo* \ No newline at end of file diff --git a/azure.yaml b/azure.yaml new file mode 100644 index 0000000..d158d11 --- /dev/null +++ b/azure.yaml @@ -0,0 +1,44 @@ +name: DocumentDB AI samples + +infra: + bicep: + path: ./infra/main.bicep + parameters: + principalId: ${AZURE_PRINCIPAL_ID} + +hooks: + + postprovision: + posix: + shell: sh + run: | + # Get environment values for the application + azd env get-values > .env + echo "" >> .env + echo "# Application default configuration" >> .env + cat ./mongo-vcore-agent-langchain/.env.default >> .env + echo "Environment configured with Azure and application defaults." + windows: + shell: pwsh + run: | + azd env get-values > .env + Add-Content -Path .env -Value "" + Add-Content -Path .env -Value "# Application default configuration" + Get-Content ./mongo-vcore-agent-langchain/.env.default | Add-Content -Path .env + Write-Host "Environment configured with Azure and application defaults." + + postdown: + posix: + shell: sh + run: | + echo "Cleaning up environment file..." + rm -f .env + echo "Environment file removed." + windows: + shell: pwsh + run: | + Write-Host "Cleaning up environment file..." + Remove-Item -Path .env -ErrorAction SilentlyContinue + Write-Host "Environment file removed." + +services: diff --git a/cosmos-db-vector-samples.sln b/cosmos-db-vector-samples.sln new file mode 100644 index 0000000..d22bcf5 --- /dev/null +++ b/cosmos-db-vector-samples.sln @@ -0,0 +1,24 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.2.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CosmosDbVectorSamples", "mongo-vcore-vector-search-dotnet\CosmosDbVectorSamples.csproj", "{5DFBA6DE-760C-46C5-1241-FF6858E38D2A}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {5DFBA6DE-760C-46C5-1241-FF6858E38D2A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5DFBA6DE-760C-46C5-1241-FF6858E38D2A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5DFBA6DE-760C-46C5-1241-FF6858E38D2A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5DFBA6DE-760C-46C5-1241-FF6858E38D2A}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {7FA967F3-BD13-4E70-A069-41D47548F917} + EndGlobalSection +EndGlobal diff --git a/infra/documentdb.bicep b/infra/documentdb.bicep new file mode 100644 index 0000000..7fffd10 --- /dev/null +++ b/infra/documentdb.bicep @@ -0,0 +1,118 @@ +metadata description = 'Provisions resources for an Azure Cosmos DB for MongoDB vCore cluster.' + +@description('The name of the Azure Cosmos DB for MongoDB vCore cluster.') +param name string + +@description('Primary location for the resources.') +param location string + +@description('Tags to be applied to the resource.') +param tags object + +@description('Principal identifier of the identity that is deploying the template.') +param principalId string + +@description('Principal identifier of the identity that is used for the web application.') +param managedIdentityPrincipalId string + +@description('The password for the administrator login.') +param mongoAdmin string = 'app' + +@secure() +@description('The password for the administrator login.') +param mongoPassword string = newGuid() + +@description('Indicates if the deployment is being executed from a pipeline.') +param pipeline bool = false + +resource mongoCluster 'Microsoft.DocumentDB/mongoClusters@2025-04-01-preview' = { + name: name + location: location + tags: tags + properties: { + administrator: { + userName: mongoAdmin + password: mongoPassword + } + compute: { + tier: 'M10' + } + sharding: { + shardCount: 1 + } + storage: { + sizeGb: 32 + } + highAvailability: { + targetMode: 'Disabled' + } + publicNetworkAccess: 'Enabled' + authConfig: { + allowedModes: [ + 'MicrosoftEntraID' + 'NativeAuth' + ] + } + } +} + +resource mongoClusterUserManagedIdentity 'Microsoft.DocumentDB/mongoClusters/users@2025-04-01-preview' = { + parent: mongoCluster + name: managedIdentityPrincipalId + properties: { + identityProvider: { + type: 'MicrosoftEntraID' + properties: { + principalType: 'ServicePrincipal' + } + } + roles: [ + { + db: 'admin' + role: 'dbOwner' + } + ] + } +} + +resource mongoClusterUserDeploymentIdentity 'Microsoft.DocumentDB/mongoClusters/users@2025-04-01-preview' = if (!pipeline && principalId != '' && principalId != managedIdentityPrincipalId) { + parent: mongoCluster + name: principalId + properties: { + identityProvider: { + type: 'MicrosoftEntraID' + properties: { + principalType: 'User' + } + } + roles: [ + { + db: 'admin' + role: 'dbOwner' + } + ] + } +} + +resource mongoClusterFirewallAllowAzure 'Microsoft.DocumentDB/mongoClusters/firewallRules@2025-04-01-preview' = { + parent: mongoCluster + name: 'allow-azure-services' + properties: { + startIpAddress: '0.0.0.0' + endIpAddress: '0.0.0.0' + } +} + +resource mongoClusterFirewallAllowAll 'Microsoft.DocumentDB/mongoClusters/firewallRules@2025-04-01-preview' = { + parent: mongoCluster + name: 'allow-all-ip-addresses' + properties: { + startIpAddress: '0.0.0.0' + endIpAddress: '255.255.255.255' + } +} + +output name string = mongoCluster.name +output resourceId string = mongoCluster.id +output endpoint string = '${mongoCluster.name}.global.mongocluster.cosmos.azure.com' +output connectionString string = 'mongodb://${mongoAdmin}:${mongoPassword}@${mongoCluster.name}.global.mongocluster.cosmos.azure.com:10255/?ssl=true&replicaSet=globaldb&retrywrites=false&maxIdleTimeMS=120000&appName=@${mongoCluster.name}@' diff --git a/infra/documentdb.json b/infra/documentdb.json new file mode 100644 index 0000000..dcc439c --- /dev/null +++ b/infra/documentdb.json @@ -0,0 +1,186 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.39.26.7824", + "templateHash": "4752528668771688368" + }, + "description": "Provisions resources for an Azure Cosmos DB for MongoDB vCore cluster." + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the Azure Cosmos DB for MongoDB vCore cluster." + } + }, + "location": { + "type": "string", + "metadata": { + "description": "Primary location for the resources." + } + }, + "tags": { + "type": "object", + "metadata": { + "description": "Tags to be applied to the resource." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Principal identifier of the identity that is deploying the template." + } + }, + "managedIdentityPrincipalId": { + "type": "string", + "metadata": { + "description": "Principal identifier of the identity that is used for the web application." + } + }, + "mongoAdmin": { + "type": "string", + "defaultValue": "app", + "metadata": { + "description": "The password for the administrator login." + } + }, + "mongoPassword": { + "type": "securestring", + "defaultValue": "[newGuid()]", + "metadata": { + "description": "The password for the administrator login." + } + }, + "pipeline": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Indicates if the deployment is being executed from a pipeline." + } + } + }, + "resources": [ + { + "type": "Microsoft.DocumentDB/mongoClusters", + "apiVersion": "2025-04-01-preview", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "administrator": { + "userName": "[parameters('mongoAdmin')]", + "password": "[parameters('mongoPassword')]" + }, + "compute": { + "tier": "M10" + }, + "sharding": { + "shardCount": 1 + }, + "storage": { + "sizeGb": 32 + }, + "highAvailability": { + "targetMode": "Disabled" + }, + "publicNetworkAccess": "Enabled", + "authConfig": { + "allowedModes": [ + "MicrosoftEntraID", + "NativeAuth" + ] + } + } + }, + { + "type": "Microsoft.DocumentDB/mongoClusters/users", + "apiVersion": "2025-04-01-preview", + "name": "[format('{0}/{1}', parameters('name'), parameters('managedIdentityPrincipalId'))]", + "properties": { + "identityProvider": { + "type": "MicrosoftEntraID", + "properties": { + "principalType": "ServicePrincipal" + } + }, + "roles": [ + { + "db": "admin", + "role": "dbOwner" + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.DocumentDB/mongoClusters', parameters('name'))]" + ] + }, + { + "condition": "[and(and(not(parameters('pipeline')), not(equals(parameters('principalId'), ''))), not(equals(parameters('principalId'), parameters('managedIdentityPrincipalId'))))]", + "type": "Microsoft.DocumentDB/mongoClusters/users", + "apiVersion": "2025-04-01-preview", + "name": "[format('{0}/{1}', parameters('name'), parameters('principalId'))]", + "properties": { + "identityProvider": { + "type": "MicrosoftEntraID", + "properties": { + "principalType": "User" + } + }, + "roles": [ + { + "db": "admin", + "role": "dbOwner" + } + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.DocumentDB/mongoClusters', parameters('name'))]" + ] + }, + { + "type": "Microsoft.DocumentDB/mongoClusters/firewallRules", + "apiVersion": "2025-04-01-preview", + "name": "[format('{0}/{1}', parameters('name'), 'allow-azure-services')]", + "properties": { + "startIpAddress": "0.0.0.0", + "endIpAddress": "0.0.0.0" + }, + "dependsOn": [ + "[resourceId('Microsoft.DocumentDB/mongoClusters', parameters('name'))]" + ] + }, + { + "type": "Microsoft.DocumentDB/mongoClusters/firewallRules", + "apiVersion": "2025-04-01-preview", + "name": "[format('{0}/{1}', parameters('name'), 'allow-all-ip-addresses')]", + "properties": { + "startIpAddress": "0.0.0.0", + "endIpAddress": "255.255.255.255" + }, + "dependsOn": [ + "[resourceId('Microsoft.DocumentDB/mongoClusters', parameters('name'))]" + ] + } + ], + "outputs": { + "name": { + "type": "string", + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "value": "[resourceId('Microsoft.DocumentDB/mongoClusters', parameters('name'))]" + }, + "endpoint": { + "type": "string", + "value": "[format('{0}.global.mongocluster.cosmos.azure.com', parameters('name'))]" + }, + "connectionString": { + "type": "string", + "value": "[format('mongodb://{0}:{1}@{2}.global.mongocluster.cosmos.azure.com:10255/?ssl=true&replicaSet=globaldb&retrywrites=false&maxIdleTimeMS=120000&appName=@{3}@', parameters('mongoAdmin'), parameters('mongoPassword'), parameters('name'), parameters('name'))]" + } + } +} \ No newline at end of file diff --git a/infra/main.bicep b/infra/main.bicep new file mode 100644 index 0000000..de86f49 --- /dev/null +++ b/infra/main.bicep @@ -0,0 +1,227 @@ +targetScope = 'subscription' + +@minLength(1) +@maxLength(64) +@description('Name of the the environment which is used to generate a short unique hash used in all resources.') +param environmentName string + +@description('Indicates if the deployment is being executed from a pipeline.') +param pipeline bool = false + +@minLength(1) +@description('Location for the OpenAI resource') +// https://learn.microsoft.com/azure/ai-services/openai/concepts/models?tabs=python-secure%2Cglobal-standard%2Cstandard-chat-completions#models-by-deployment-type +@allowed([ + 'eastus2' + 'swedencentral' +]) +@metadata({ + azd: { + type: 'location' + } +}) +param location string + + +@description('The name for the administrator login.') +param mongoAdmin string = 'app' + +@secure() +@description('The password for the administrator login.') +param mongoPassword string = newGuid() + +@description('Name of the Planner GPT model to deploy') +param gptPlannerModelName string = 'gpt-4o-mini' + +@description('Version of the Planner GPT model to deploy') +param gptPlannerModelVersion string = '2024-07-18' + +@description('API version for the Planner GPT model') +param gptPlannerApiVersion string = '2024-08-01-preview' + +@description('Name of the Synth GPT model to deploy') +param gptSynthModelName string = 'gpt-4o' + +@description('Version of the Synth GPT model to deploy') +param gptSynthModelVersion string = '2024-08-06' + +@description('API version for the Synth GPT model') +param gptSynthApiVersion string = '2024-08-01-preview' + +@description('Capacity of the GPT deployment') +param gptDeploymentCapacity int = 50 + +// Embedding model parameters +@description('Name of the embedding model to deploy') +param embeddingModelName string = 'text-embedding-3-small' + +@description('Version of the embedding model to deploy') +param embeddingModelVersion string = '1' +param embeddingApiVersion string = '2023-05-15' + +@description('Capacity of the embedding model deployment') +param embeddingDeploymentCapacity int = 50 + +var principalType = 'User' +@description('Id of the user or app to assign application roles') +param principalId string = '' + +var resourceToken = toLower(uniqueString(subscription().id, environmentName, location)) +var tags = { 'azd-env-name': environmentName } + +// Organize resources in a resource group +resource resourceGroup 'Microsoft.Resources/resourceGroups@2021-04-01' = { + name: '${environmentName}-${resourceToken}-rg' + location: location + tags: tags +} + +// Log Analytics Workspace for Container Apps +module logAnalytics 'br/public:avm/res/operational-insights/workspace:0.9.1' = { + name: 'logAnalytics' + scope: resourceGroup + params: { + name: '${resourceToken}-logs' + location: location + tags: tags + skuName: 'PerGB2018' + dataRetention: 30 + } +} + +// User-assigned managed identity for the container app +module managedIdentity 'br/public:avm/res/managed-identity/user-assigned-identity:0.4.0' = { + name: 'containerAppIdentity' + scope: resourceGroup + params: { + name: '${resourceToken}-identity' + location: location + tags: tags + } +} + + +var openAiServiceName = '${resourceToken}-openai' +module openAi 'br/public:avm/res/cognitive-services/account:0.7.1' = { + name: 'openai' + scope: resourceGroup + params: { + name: openAiServiceName + location: location + tags: tags + kind: 'OpenAI' + sku: 'S0' + disableLocalAuth: false + customSubDomainName: openAiServiceName + networkAcls: { + defaultAction: 'Allow' + bypass: 'AzureServices' + } + deployments: [ + { + name: gptPlannerModelName + model: { + format: 'OpenAI' + name: gptPlannerModelName + version: gptPlannerModelVersion + } + sku: { + name: 'GlobalStandard' + capacity: gptDeploymentCapacity + } + } + { + name: gptSynthModelName + model: { + format: 'OpenAI' + name: gptSynthModelName + version: gptSynthModelVersion + } + sku: { + name: 'GlobalStandard' + capacity: gptDeploymentCapacity + } + } + { + name: embeddingModelName + model: { + format: 'OpenAI' + name: embeddingModelName + version: embeddingModelVersion + } + sku: { + name: 'GlobalStandard' + capacity: embeddingDeploymentCapacity + } + } + ] + roleAssignments: [ + { + principalId: principalId + roleDefinitionIdOrName: 'Cognitive Services OpenAI User' + principalType: principalType + } + { + principalId: principalId + roleDefinitionIdOrName: 'Cognitive Services OpenAI Contributor' + principalType: principalType + } + { + principalId: managedIdentity.outputs.principalId + roleDefinitionIdOrName: 'Cognitive Services OpenAI User' + principalType: 'ServicePrincipal' + } + ] + } +} + +module mongoCluster 'documentdb.bicep' = { + name: 'mongo-cluster' + scope: resourceGroup + params: { + name: 'docdb-${resourceToken}' + location: location + tags: tags + managedIdentityPrincipalId: managedIdentity.outputs.principalId + principalId: principalId + pipeline: pipeline + mongoAdmin: mongoAdmin + mongoPassword: mongoPassword + } +} + + +// Resources +output AZURE_LOCATION string = location +output AZURE_TENANT_ID string = tenant().tenantId +output AZURE_PRINCIPAL_ID string = managedIdentity.outputs.principalId +output AZURE_RESOURCE_GROUP string = resourceGroup.name + +// OpenAI Resource +output AZURE_OPENAI_API_INSTANCE_NAME string = openAi.outputs.name +output AZURE_OPENAI_ENDPOINT string = openAi.outputs.endpoint +output AZURE_OPENAI_BASE_PATH string = 'https://${openAi.outputs.name}.openai.azure.com' + +// Embedding resource +output AZURE_OPENAI_EMBEDDING_DEPLOYMENT string = embeddingModelName +output AZURE_OPENAI_EMBEDDING_MODEL string = embeddingModelName +output AZURE_OPENAI_EMBEDDING_API_VERSION string = embeddingApiVersion + +// LLM resource - planner agent +output AZURE_OPENAI_PLANNER_DEPLOYMENT string = gptPlannerModelName +output AZURE_OPENAI_PLANNER_API_VERSION string = gptPlannerApiVersion + +// LLM resource - synth agent +output AZURE_OPENAI_SYNTH_DEPLOYMENT string = gptSynthModelName +output AZURE_OPENAI_SYNTH_API_VERSION string = gptSynthApiVersion + +// DocumentDB +output AZURE_DOCUMENTDB_INSTANCE string = mongoCluster.outputs.name +output AZURE_DOCUMENTDB_CONNECTION_STRING string = mongoCluster.outputs.connectionString +output AZURE_DOCUMENTDB_ENDPOINT string = mongoCluster.outputs.endpoint +output MONGO_CLUSTER_NAME string = mongoCluster.outputs.name +output MONGO_CONNECTION_STRING string = 'mongodb+srv://${mongoCluster.outputs.name}.global.mongocluster.cosmos.azure.com/?authMechanism=MONGODB-OIDC' +// Source code +output ENV_PATH string = '../' + + diff --git a/infra/main.parameters.json b/infra/main.parameters.json new file mode 100644 index 0000000..76d23a4 --- /dev/null +++ b/infra/main.parameters.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "environmentName": { + "value": "${AZURE_ENV_NAME}" + }, + "location": { + "value": "${AZURE_LOCATION}" + }, + "principalId": { + "value": "${AZURE_PRINCIPAL_ID}" + }, + "AZURE_AISEARCH_INDEX_NAME": { + "value": "northwind" + } + } +} \ No newline at end of file diff --git a/mongo-vcore-agent-langchain/.env.default b/mongo-vcore-agent-langchain/.env.default new file mode 100644 index 0000000..30611cd --- /dev/null +++ b/mongo-vcore-agent-langchain/.env.default @@ -0,0 +1,57 @@ +DEBUG=false +USE_PASSWORDLESS=true + +FIELD_TO_EMBED="Description" +EMBEDDED_FIELD="vectors" +EMBEDDING_DIMENSIONS="1536" +EMBEDDING_BATCH_SIZE="16" + +# ======================================== +# Data File Paths and Vector Configuration +# ======================================== +DATA_FILE_WITHOUT_VECTORS="../data/HotelsData_toCosmosDB.JSON" +DATA_FILE_WITH_VECTORS="./data/HotelsData_toCosmosDB_Vector_text-embedding-3-small.json" +QUERY="quintessential lodging near running trails, eateries, retail" +# ======================================== +# Data Loading and Processing Settings +# ======================================== +LOAD_SIZE_BATCH="100" +# ======================================== +# MongoDB/Cosmos DB Connection Settings +# ======================================== +MONGO_DB_NAME="Hotels" +MONGO_DB_COLLECTION="Hotels_ivf" +MONGO_DB_INDEX_NAME="vectorIndex_ivf" +# Vector Index Algorithm: vector-ivf (default), vector-hnsw, or vector-diskann +VECTOR_INDEX_ALGORITHM="vector-ivf" +VECTOR_SEARCH_STRATEGY="documentdb" # or mongo or auto + +# ======================================== +# Vector Index Parameters (Optional Tuning) +# ======================================== +# Common parameter for all algorithms +VECTOR_SIMILARITY="COS" # Options: COS (cosine), L2 (euclidean), IP (inner product) + +# IVF-specific parameters (used when VECTOR_INDEX_ALGORITHM=vector-ivf) +IVF_NUM_LISTS="10" # Number of clusters for IVF index (default: 10) + +# HNSW-specific parameters (used when VECTOR_INDEX_ALGORITHM=vector-hnsw) +# HNSW_M="16" # Max connections per layer (default: 16) +# HNSW_EF_CONSTRUCTION="64" # Size of dynamic candidate list during build (default: 64) + +# DiskANN-specific parameters (used when VECTOR_INDEX_ALGORITHM=vector-diskann) +# DISKANN_MAX_DEGREE="20" # Max degree of graph (default: 20) +# DISKANN_L_BUILD="10" # Size of search list during build (default: 10) +# ======================================== +# Agent Search Configuration +# ======================================== +MAX_SEARCH_RESULTS="5" +SIMILARITY_THRESHOLD="0.7" + +# ======================================== +# Optional Settings +# ======================================== +LANGSMITH_TRACING="false" +LANGSMITH_ENDPOINT="https://api.smith.langchain.com" +LANGSMITH_API_KEY="" +LANGSMITH_PROJECT="" diff --git a/mongo-vcore-agent-langchain/.env.sample b/mongo-vcore-agent-langchain/.env.sample new file mode 100644 index 0000000..28200e8 --- /dev/null +++ b/mongo-vcore-agent-langchain/.env.sample @@ -0,0 +1,79 @@ +DEBUG=false +USE_PASSWORDLESS=false + +# ======================================== +# Azure OpenAI Shared Settings +# ======================================== +#The `apiKey` and `azureADTokenProvider` arguments are mutually exclusive; only one can be passed at a time. +AZURE_OPENAI_API_KEY="" +AZURE_OPENAI_ENDPOINT="https://.openai.azure.com/" +AZURE_OPENAI_API_INSTANCE_NAME="" + +AZURE_OPENAI_SYNTH_API_VERSION="2025-01-01-preview" +AZURE_OPENAI_SYNTH_DEPLOYMENT="gpt-4o" + +AZURE_OPENAI_PLANNER_DEPLOYMENT="gpt-4.1-mini" +AZURE_OPENAI_PLANNER_API_VERSION="2025-01-01-preview" + +# ======================================== +# Azure OpenAI Embedding Model Settings +# ======================================== +AZURE_OPENAI_EMBEDDING_DEPLOYMENT="text-embedding-3-small" +AZURE_OPENAI_EMBEDDING_API_VERSION="2023-05-15" + +FIELD_TO_EMBED="Description" +EMBEDDED_FIELD="vectors" +EMBEDDING_DIMENSIONS="1536" +EMBEDDING_BATCH_SIZE="16" + +# ======================================== +# Data File Paths and Vector Configuration +# ======================================== +DATA_FILE_WITHOUT_VECTORS="../data/HotelsData_toCosmosDB.JSON" +DATA_FILE_WITH_VECTORS="./data/HotelsData_toCosmosDB_Vector_text-embedding-3-small.json" +QUERY="quintessential lodging near running trails, eateries, retail" +# ======================================== +# Data Loading and Processing Settings +# ======================================== +LOAD_SIZE_BATCH="100" +# ======================================== +# MongoDB/Cosmos DB Connection Settings +# ======================================== +AZURE_DOCUMENT_DB_CONNECTION_STRING="" +MONGO_CLUSTER_NAME="" +MONGO_DB_NAME="Hotels" +MONGO_DB_COLLECTION="Hotels_ivf" +MONGO_DB_INDEX_NAME="vectorIndex_ivf" +# Vector Index Algorithm: vector-ivf (default), vector-hnsw, or vector-diskann +VECTOR_INDEX_ALGORITHM="vector-ivf" +VECTOR_SEARCH_STRATEGY="documentdb" # or mongo or auto + +# ======================================== +# Vector Index Parameters (Optional Tuning) +# ======================================== +# Common parameter for all algorithms +VECTOR_SIMILARITY="COS" # Options: COS (cosine), L2 (euclidean), IP (inner product) + +# IVF-specific parameters (used when VECTOR_INDEX_ALGORITHM=vector-ivf) +IVF_NUM_LISTS="10" # Number of clusters for IVF index (default: 10) + +# HNSW-specific parameters (used when VECTOR_INDEX_ALGORITHM=vector-hnsw) +# HNSW_M="16" # Max connections per layer (default: 16) +# HNSW_EF_CONSTRUCTION="64" # Size of dynamic candidate list during build (default: 64) + +# DiskANN-specific parameters (used when VECTOR_INDEX_ALGORITHM=vector-diskann) +# DISKANN_MAX_DEGREE="20" # Max degree of graph (default: 20) +# DISKANN_L_BUILD="10" # Size of search list during build (default: 10) +# ======================================== +# Agent Search Configuration +# ======================================== +MAX_SEARCH_RESULTS="5" +SIMILARITY_THRESHOLD="0.7" + +# ======================================== +# Optional Settings +# ======================================== +LANGSMITH_TRACING="false" +LANGSMITH_ENDPOINT="https://api.smith.langchain.com" +LANGSMITH_API_KEY="" +LANGSMITH_PROJECT="" diff --git a/mongo-vcore-agent-langchain/docs/AGENT-ORCHESTRATION.md b/mongo-vcore-agent-langchain/docs/AGENT-ORCHESTRATION.md new file mode 100644 index 0000000..88e19b3 --- /dev/null +++ b/mongo-vcore-agent-langchain/docs/AGENT-ORCHESTRATION.md @@ -0,0 +1,896 @@ +# Agent Orchestration Patterns + +> **Purpose**: This document explains the agent orchestration architecture in a framework-agnostic way, so it can be implemented with any AI framework (LangChain, Semantic Kernel, LlamaIndex, Autogen, custom implementation, etc.). + +## Table of Contents + +- [Overview](#overview) +- [Two-Agent Architecture](#two-agent-architecture) +- [Agent 1: Planner (Tool Calling Agent)](#agent-1-planner-tool-calling-agent) +- [Agent 2: Synthesizer (Response Generation)](#agent-2-synthesizer-response-generation) +- [Tool Calling / Function Calling](#tool-calling--function-calling) +- [Prompt Engineering](#prompt-engineering) +- [Data Flow and Context Passing](#data-flow-and-context-passing) +- [Error Handling and Debugging](#error-handling-and-debugging) +- [Framework Implementation Patterns](#framework-implementation-patterns) + +--- + +## Overview + +### What is Agent Orchestration? + +Agent orchestration is the pattern of coordinating multiple AI agents (LLMs with specific roles) to accomplish a complex task. Instead of one monolithic prompt, we break the problem into stages: + +1. **Planning**: Understanding user intent and gathering relevant information +2. **Synthesis**: Analyzing information and generating a final response + +### Why Two Agents? + +**Separation of Concerns**: +- **Planner**: Focused on query refinement and tool selection (cheaper, faster model) +- **Synthesizer**: Focused on reasoning and natural language generation (more capable model) + +**Cost Optimization**: +- Use smaller model (gpt-4o-mini) for structured tasks +- Use larger model (gpt-4o) only for final response generation + +**Performance**: +- Tool calling optimized for smaller models +- Complex reasoning optimized for larger models + +--- + +## Two-Agent Architecture + +### Agent Pipeline Flow + +``` +User Query: "cheap hotel near downtown" + ↓ + ┌────────────────────────────────────────┐ + │ AGENT 1: PLANNER │ + │ Model: gpt-4o-mini │ + │ Role: Query Refinement + Tool Calling │ + └────────────────────────────────────────┘ + ↓ + Refined Query: "budget-friendly hotel near + downtown with good value" + Tool Call: search_hotels_collection( + query="...", + nearestNeighbors=10 + ) + ↓ + ┌────────────────────────────────────────┐ + │ VECTOR SEARCH TOOL │ + │ - Generate embedding │ + │ - Query Cosmos DB │ + │ - Return top-k results │ + └────────────────────────────────────────┘ + ↓ + Search Results: [ + {hotel1, score: 0.95}, + {hotel2, score: 0.92}, + {hotel3, score: 0.89}, + ... + ] + ↓ + ┌────────────────────────────────────────┐ + │ AGENT 2: SYNTHESIZER │ + │ Model: gpt-4o │ + │ Role: Compare & Recommend │ + └────────────────────────────────────────┘ + ↓ + Final Response: "Based on your search, I + recommend Hotel A because... Consider + Hotel B if..." +``` + +### Key Design Decisions + +**Why not one agent?** +- Single agent with both capabilities is more expensive +- Harder to optimize prompts for different tasks +- Less control over intermediate outputs + +**Why not three+ agents?** +- Two stages are sufficient for this use case +- More agents = more complexity and latency +- Diminishing returns on additional stages + +--- + +## Agent 1: Planner (Tool Calling Agent) + +### Purpose + +Transform user's potentially vague query into: +1. A well-structured search query +2. A tool call with appropriate parameters + +### Capabilities Required + +- **Understanding**: Parse user intent +- **Query refinement**: Improve search quality +- **Tool selection**: Choose correct tool and parameters +- **Function calling**: Execute tool with proper schema + +### System Prompt Design + +**Role Definition**: +``` +You are a hotel search planner. Your job is to: +1. Understand what the user is looking for +2. Refine their query to be more specific +3. Call the search tool with appropriate parameters +``` + +**Critical Instructions**: +``` +MANDATORY: You MUST call the search_hotels_collection tool. +Do not attempt to answer without calling this tool. +``` + +**Output Format**: +``` +Return ONLY a tool call in this format: +{ + "tool": "search_hotels_collection", + "args": { + "query": "", + "nearestNeighbors": + } +} +``` + +**Examples** (few-shot prompting): +``` +User: "cheap hotel" +→ {"tool": "search_hotels_collection", + "args": {"query": "budget-friendly hotel with good value", + "nearestNeighbors": 10}} + +User: "hotel near downtown with parking" +→ {"tool": "search_hotels_collection", + "args": {"query": "hotel near downtown with parking and wifi", + "nearestNeighbors": 5}} +``` + +### User Message Format + +``` +Call the "search_hotels_collection" tool with: +- nearestNeighbors: {k} +- query: "{user_query}" + +Respond ONLY with tool call JSON. +``` + +**Why explicit instructions?** +- Ensures consistent tool calling behavior +- Reduces hallucination +- Makes debugging easier + +### Tool Definition + +The tool must be registered with: + +**Name**: `search_hotels_collection` + +**Description**: +``` +Performs vector similarity search on Hotels collection. +Returns hotels matching the query with similarity scores. +Use this tool for EVERY hotel search request. +``` + +**Parameters Schema**: +```json +{ + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Natural language search query describing desired hotel characteristics" + }, + "nearestNeighbors": { + "type": "integer", + "description": "Number of results to return (1-20)" + } + }, + "required": ["query", "nearestNeighbors"] +} +``` + +**Tool Implementation**: +``` +function search_hotels_collection(query: string, nearestNeighbors: number): + 1. embedding = generate_embedding(query) + 2. results = vector_search(embedding, k=nearestNeighbors) + 3. formatted = format_results(results) + 4. return formatted as JSON string +``` + +### Extracting Tool Output + +After the planner agent completes: + +1. **Parse agent response** to find tool call +2. **Extract tool output** (search results) +3. **Format as string** for next agent + +**LangChain Example Pattern**: +``` +messages = agent_result.messages +for message in messages: + if message.type == "tool": + return message.content +``` + +**Custom Framework Pattern**: +``` +response = call_llm_with_tools(...) +if response.tool_calls: + tool_result = execute_tool(response.tool_calls[0]) + return tool_result +``` + +--- + +## Agent 2: Synthesizer (Response Generation) + +### Purpose + +Generate natural language recommendation by: +1. Comparing top hotel results +2. Identifying key tradeoffs +3. Recommending best option with reasoning +4. Suggesting alternatives + +### Capabilities Required + +- **Analysis**: Compare multiple options across dimensions +- **Reasoning**: Identify tradeoffs and best fit +- **Natural language generation**: Clear, concise recommendations +- **No tool calling**: Pure generation task + +### System Prompt Design + +**Role Definition**: +``` +You are an expert hotel recommendation assistant. +Your job is to help users choose between hotel options. +``` + +**Constraints**: +``` +- Only use the TOP 3 results provided +- Do not request additional searches +- Do not call any tools +``` + +**Task Instructions**: +``` +Compare the top 3 hotels across: +- Rating +- Similarity score +- Location +- Category +- Tags (parking, wifi, pool, etc.) +- Any other differentiating features +``` + +**Output Format**: +``` +Structure your response: +1. COMPARISON SUMMARY: Key differences between top 3 +2. BEST OVERALL: Recommend #1 choice with reasoning +3. ALTERNATIVES: When to choose options 2 or 3 + +Format: Plain text, no markdown +Length: Under 220 words +Use bullet points and short sentences +``` + +**Quality Requirements**: +``` +- Be decisive (pick one as best) +- Be specific (use actual hotel names and attributes) +- Be concise (no fluff) +- Be comparative (explain tradeoffs) +``` + +### User Message Format + +``` +User asked: {original_query} + +Tool summary: +{formatted_search_results} + +Analyze the TOP 3 results and provide recommendation. +``` + +**Formatting Search Results**: + +Each hotel should be formatted as: +``` +--- HOTEL START --- +HotelId: 123 +HotelName: Example Hotel +Description: Beautiful hotel with... +Category: Luxury +Tags: pool, wifi, parking +Rating: 4.5 +Address.City: Seattle +Score: 0.950000 +--- HOTEL END --- +``` + +**Why this format?** +- Clear delimiters for each hotel +- Flat structure (no nested JSON) +- Easy for LLM to parse +- Includes similarity score for comparison + +### No Tool Calling + +The synthesizer should NOT: +- Call additional tools +- Request more information +- Perform new searches + +**Why?** +- Keeps flow simple and predictable +- Prevents infinite loops +- Forces decision-making with available data + +--- + +## Tool Calling / Function Calling + +### What is Tool Calling? + +Tool calling (also called function calling) allows LLMs to: +1. Recognize when they need external data +2. Format a request to call a specific function +3. Wait for function result +4. Continue with the result + +### Implementation Requirements + +**Framework Support**: Your framework must support: +- Registering tools/functions with schema +- LLM generating tool calls in responses +- Executing tools and injecting results back + +**Common Frameworks**: +- ✅ LangChain: Native tool support +- ✅ Semantic Kernel: Plugin system +- ✅ LlamaIndex: Tool abstractions +- ✅ Autogen: Tool execution +- ✅ OpenAI SDK: Direct function calling +- ✅ Custom: Implement via prompt engineering + +### Tool Registration Pattern + +**Step 1: Define Tool Schema** +```json +{ + "name": "search_hotels_collection", + "description": "Search hotels using vector similarity", + "parameters": { + "type": "object", + "properties": { + "query": {"type": "string"}, + "nearestNeighbors": {"type": "integer"} + }, + "required": ["query", "nearestNeighbors"] + } +} +``` + +**Step 2: Implement Tool Function** +```python +def search_hotels_collection(query: str, nearestNeighbors: int) -> str: + embedding = embedding_client.embed(query) + results = vector_db.search(embedding, k=nearestNeighbors) + return json.dumps(results) +``` + +**Step 3: Register Tool with Agent** +```python +agent = create_agent( + model=chat_model, + tools=[search_hotels_collection], + system_prompt=PLANNER_SYSTEM_PROMPT +) +``` + +### Tool Execution Flow + +``` +1. User message sent to LLM + ↓ +2. LLM decides to call tool + Returns: { + "tool_call": { + "name": "search_hotels_collection", + "arguments": {"query": "...", "nearestNeighbors": 5} + } + } + ↓ +3. Framework extracts tool call + ↓ +4. Framework executes: search_hotels_collection("...", 5) + ↓ +5. Tool returns: "[{hotel1}, {hotel2}, ...]" + ↓ +6. Framework injects tool result back to LLM + ↓ +7. LLM processes result and continues +``` + +### Context Passing to Tools + +Some tools need additional context (like database connections): + +**LangChain Pattern** (context parameter): +```python +@tool +def search_hotels(query: str, nearestNeighbors: int, config: dict): + store = config["context"]["store"] + embedding_client = config["context"]["embedding_client"] + # Use store and embedding_client +``` + +**Semantic Kernel Pattern** (dependency injection): +```csharp +[KernelFunction] +public async Task SearchHotels( + string query, + int nearestNeighbors, + [FromKernelContext] VectorStore store) +{ + // Use store +} +``` + +**Custom Pattern** (closure): +```javascript +function createSearchTool(store, embeddingClient) { + return { + name: "search_hotels", + execute: async (query, k) => { + // Closure has access to store and embeddingClient + } + } +} +``` + +--- + +## Prompt Engineering + +### Prompt Components + +Every agent invocation has: + +1. **System Prompt**: Agent's role, capabilities, constraints +2. **User Message**: Current request with context +3. **Tool Definitions** (for planner only) +4. **Examples** (optional, for few-shot learning) + +### Key Principles + +**Clarity over Brevity**: +- Be explicit about what the agent should/shouldn't do +- Don't assume the model will infer intent + +**Constraints First**: +- State limitations early (e.g., "Do NOT call additional tools") +- Prevents unwanted behavior + +**Output Format**: +- Specify exact format (JSON, plain text, etc.) +- Provide examples of expected output + +**Examples for Stability**: +- Few-shot prompting improves consistency +- Show both input and expected output + +### Planner Prompt Patterns + +**Bad** (vague): +``` +"Help the user search for hotels." +``` + +**Good** (specific): +``` +"You are a hotel search planner. +MANDATORY: Call the search_hotels_collection tool. +Refine the user's query to be more specific. +Return ONLY tool call JSON." +``` + +### Synthesizer Prompt Patterns + +**Bad** (open-ended): +``` +"Recommend hotels based on the search results." +``` + +**Good** (constrained): +``` +"Compare ONLY the top 3 results. +Identify key tradeoffs. +Recommend the best option with one clear reason. +Format: Plain text, under 220 words." +``` + +--- + +## Data Flow and Context Passing + +### Pipeline Context + +As data flows through the pipeline, context accumulates: + +``` +Stage 1: User Input + Context: { + "query": "cheap hotel near downtown" + } + ↓ +Stage 2: Planner Output + Context: { + "query": "cheap hotel near downtown", + "refined_query": "budget hotel near downtown with good value", + "k": 10 + } + ↓ +Stage 3: Tool Output + Context: { + "query": "cheap hotel near downtown", + "refined_query": "...", + "k": 10, + "search_results": [{hotel1}, {hotel2}, ...] + } + ↓ +Stage 4: Synthesizer Input + Context: { + "query": "cheap hotel near downtown", + "search_results": "formatted as text" + } +``` + +### Context Scoping + +**Planner Needs**: +- User query +- Vector store connection +- Embedding client + +**Synthesizer Needs**: +- Original user query +- Formatted search results + +**What to Exclude**: +- Don't pass vector store to synthesizer +- Don't pass intermediate LLM outputs +- Keep context minimal and relevant + +### State Management + +**Stateless Pattern** (recommended): +- Each agent call is independent +- Context passed explicitly as parameters +- Easier to debug and test + +**Stateful Pattern** (alternative): +- Shared memory/context object +- Agents read/write to shared state +- More complex but supports multi-turn conversations + +--- + +## Error Handling and Debugging + +### Common Failure Modes + +1. **Tool Not Called**: + - Cause: Prompt not explicit enough + - Fix: Add "MANDATORY: Call tool" instruction + +2. **Invalid Tool Arguments**: + - Cause: Schema not clear or LLM hallucination + - Fix: Strengthen parameter descriptions, add examples + +3. **Empty Search Results**: + - Cause: Query too specific or embedding mismatch + - Fix: Return helpful message, adjust k value + +4. **Synthesizer Requests More Data**: + - Cause: Instructions not clear enough + - Fix: Add "Do NOT request additional searches" + +5. **Response Too Long/Short**: + - Cause: No length constraint + - Fix: Specify word/character limit explicitly + +### Debugging Strategies + +**Enable Verbose Logging**: +``` +- Log every LLM request (system prompt + user message) +- Log every LLM response +- Log every tool call and result +- Log context passed between stages +``` + +**Callback Handlers** (LangChain example): +```python +callbacks = [ + LoggingCallback(), # Logs all events + DebugCallback(), # Detailed introspection +] + +agent.invoke(message, callbacks=callbacks) +``` + +**Custom Logging**: +```python +print(f"[Planner] Input: {user_query}") +result = planner_agent.invoke(user_query) +print(f"[Planner] Output: {result}") + +tool_output = execute_tool(...) +print(f"[Tool] Results: {tool_output[:200]}...") + +final = synthesizer_agent.invoke(context) +print(f"[Synthesizer] Output: {final}") +``` + +**Test Each Stage Independently**: +- Test planner with mock tool +- Test tool with hardcoded embeddings +- Test synthesizer with static search results + +--- + +## Framework Implementation Patterns + +### LangChain Pattern + +```python +from langchain import createAgent +from langchain.tools import tool + +# Define tool +@tool +def search_hotels_collection(query: str, nearestNeighbors: int, config) -> str: + store = config["context"]["store"] + # Execute search + return json.dumps(results) + +# Planner agent +planner = createAgent( + model=chat_model_mini, + systemPrompt=PLANNER_SYSTEM_PROMPT, + tools=[search_hotels_collection], + contextSchema=context_schema +) + +# Execute planner +planner_result = planner.invoke( + {"messages": [{"role": "user", "content": user_message}]}, + {"context": {"store": vector_store, "embedding_client": embeddings}} +) + +# Extract results +search_results = extract_tool_output(planner_result.messages) + +# Synthesizer agent (no tools) +synthesizer = createAgent( + model=chat_model_large, + systemPrompt=SYNTHESIZER_SYSTEM_PROMPT +) + +# Execute synthesizer +synth_result = synthesizer.invoke({ + "messages": [{ + "role": "user", + "content": f"User: {query}\n\nResults: {search_results}" + }] +}) + +final_answer = synth_result.messages[-1].content +``` + +### Semantic Kernel Pattern + +```csharp +// Define plugin with tool +public class HotelSearchPlugin +{ + private readonly VectorStore _store; + + [KernelFunction] + public async Task SearchHotelsCollection( + string query, + int nearestNeighbors) + { + var results = await _store.SearchAsync(query, nearestNeighbors); + return JsonSerializer.Serialize(results); + } +} + +// Create kernel with plugins +var kernel = Kernel.CreateBuilder() + .AddAzureOpenAIChatCompletion(deploymentName, endpoint) + .Build(); + +kernel.ImportPluginFromObject(new HotelSearchPlugin(vectorStore)); + +// Planner invocation +var plannerSettings = new OpenAIPromptExecutionSettings +{ + ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions +}; + +var plannerResult = await kernel.InvokePromptAsync( + $"{PLANNER_SYSTEM_PROMPT}\n\nUser: {userQuery}", + new(plannerSettings) +); + +// Synthesizer invocation (no tools) +var synthSettings = new OpenAIPromptExecutionSettings +{ + ToolCallBehavior = ToolCallBehavior.None +}; + +var synthResult = await kernel.InvokePromptAsync( + $"{SYNTHESIZER_SYSTEM_PROMPT}\n\n{synthesizerInput}", + new(synthSettings) +); +``` + +### OpenAI SDK Pattern (Direct Function Calling) + +```python +import openai + +# Define function schema +tools = [{ + "type": "function", + "function": { + "name": "search_hotels_collection", + "description": "Search hotels using vector similarity", + "parameters": { + "type": "object", + "properties": { + "query": {"type": "string"}, + "nearestNeighbors": {"type": "integer"} + }, + "required": ["query", "nearestNeighbors"] + } + } +}] + +# Planner call +response = openai.chat.completions.create( + model="gpt-4o-mini", + messages=[ + {"role": "system", "content": PLANNER_SYSTEM_PROMPT}, + {"role": "user", "content": user_query} + ], + tools=tools, + tool_choice="required" +) + +# Execute tool +if response.choices[0].message.tool_calls: + tool_call = response.choices[0].message.tool_calls[0] + args = json.loads(tool_call.function.arguments) + search_results = search_hotels_collection(**args) + +# Synthesizer call (no tools) +synth_response = openai.chat.completions.create( + model="gpt-4o", + messages=[ + {"role": "system", "content": SYNTHESIZER_SYSTEM_PROMPT}, + {"role": "user", "content": f"Query: {user_query}\n\nResults: {search_results}"} + ] +) + +final_answer = synth_response.choices[0].message.content +``` + +### Custom Framework Pattern + +If building from scratch: + +```python +class Agent: + def __init__(self, model, system_prompt, tools=None): + self.model = model + self.system_prompt = system_prompt + self.tools = tools or [] + + def invoke(self, user_message, context=None): + # Build messages + messages = [ + {"role": "system", "content": self.system_prompt}, + {"role": "user", "content": user_message} + ] + + # Call LLM with tool definitions + response = self.model.chat( + messages=messages, + tools=[tool.schema for tool in self.tools] + ) + + # Handle tool calls + if response.tool_calls: + for tool_call in response.tool_calls: + tool = self._find_tool(tool_call.name) + result = tool.execute(tool_call.arguments, context) + # Optionally: inject result and call LLM again + + return response + +# Usage +planner = Agent( + model=gpt4o_mini, + system_prompt=PLANNER_SYSTEM_PROMPT, + tools=[search_tool] +) + +result = planner.invoke(user_query, context={"store": vector_store}) +``` + +--- + +## Summary + +### Key Takeaways + +1. **Two agents** optimize for cost and performance +2. **Planner** handles query refinement and tool calling (small model) +3. **Synthesizer** handles reasoning and generation (large model) +4. **Tool calling** requires framework support and careful schema design +5. **Prompts** must be explicit, constrained, and example-driven +6. **Context** flows through pipeline but should be scoped appropriately +7. **Debugging** requires verbose logging and stage isolation + +### Implementation Steps + +1. ✅ Choose AI framework (or build custom) +2. ✅ Implement vector search tool +3. ✅ Design and test planner prompts +4. ✅ Register tool with planner agent +5. ✅ Extract and format tool outputs +6. ✅ Design and test synthesizer prompts +7. ✅ Connect pipeline end-to-end +8. ✅ Add logging and error handling +9. ✅ Test with diverse queries +10. ✅ Optimize prompts based on results + +### Extensibility + +This pattern can be extended to: +- **More agents**: Add specialized roles (validation, formatting, etc.) +- **More tools**: Add reservation booking, price comparison, etc. +- **Multi-turn**: Add conversation memory and follow-up handling +- **Streaming**: Stream synthesizer output token-by-token +- **Parallel tools**: Call multiple tools simultaneously +- **Fallbacks**: Add retry logic or alternative models + +--- + +## Related Documents + +- **FUNCTIONAL-SPEC.md**: System requirements and data models +- **CODE.md**: TypeScript/LangChain implementation reference +- **SCRIPTS.md**: Testing and verification procedures diff --git a/mongo-vcore-agent-langchain/docs/CODE.md b/mongo-vcore-agent-langchain/docs/CODE.md new file mode 100644 index 0000000..13baf1b --- /dev/null +++ b/mongo-vcore-agent-langchain/docs/CODE.md @@ -0,0 +1,358 @@ +# Source Code Overview + +This document provides a comprehensive guide to the TypeScript source code in this LangChain agent application. Each section describes the purpose and functionality of the files, organized by their role in the application. + +## Table of Contents + +- [NPM Scripts and Entry Points](#npm-scripts-and-entry-points) + - [agent.ts - Main Application (`npm run start`)](#agentts---main-application-npm-run-start) + - [upload-documents.ts - Data Upload (`npm run upload`)](#upload-documentsts---data-upload-npm-run-upload) + - [cleanup.ts - Database Cleanup (`npm run cleanup`)](#cleanupts---database-cleanup-npm-run-cleanup) + - [scripts/test-auth.ts - Authentication Testing (`npm run auth`)](#scriptstest-authts---authentication-testing-npm-run-auth) +- [Verification Scripts](#verification-scripts) + - [scripts/embed.ts](#scriptsembedts) + - [scripts/llm-planner.ts](#scriptsllm-plannerts) + - [scripts/llm-synth.ts](#scriptsllm-synthts) + - [scripts/mongo.ts](#scriptsmongots) +- [Core Application Files](#core-application-files) + - [vector-store.ts - Vector Store Management](#vector-storets---vector-store-management) +- [Utility Files](#utility-files) + - [utils/clients.ts](#utilsclientsts) + - [utils/prompts.ts](#utilspromptststs) + - [utils/types.ts](#utilstypests) + - [utils/debug-handlers.ts](#utilsdebug-handlersts) + - [utils/mongo.ts](#utilsmongots) + +--- + +## NPM Scripts and Entry Points + +These are the main executable scripts that can be run via `npm run `. Each compiles the TypeScript code and executes a specific entry point. + +### agent.ts - Main Application (`npm run start`) + +**Purpose**: The main LangChain agent application that implements a two-agent RAG (Retrieval-Augmented Generation) pipeline. + +**Command**: `npm run start` + +**What it does**: +1. **Planner Agent**: Takes user query → calls vector search tool → retrieves relevant hotel documents from Cosmos DB +2. **Synthesizer Agent**: Takes search results + original query → generates natural language response + +**Key features**: +- Implements a two-model architecture: + - Planner: Uses `gpt-4o-mini` for query planning and tool calling + - Synthesizer: Uses `gpt-4o` for response generation +- Uses vector similarity search to find relevant hotels +- Supports both passwordless (Azure AD) and API key authentication +- Includes debug handlers for troubleshooting agent behavior + +**Flow**: +``` +User Query → Planner Agent → Vector Search Tool → Search Results → Synthesizer Agent → Final Response +``` + +**How it differs from vector-store.ts**: +- `agent.ts` is the **orchestrator** - it runs the full agent pipeline +- `vector-store.ts` is a **library module** - it provides the vector store functions and tool definitions that `agent.ts` imports and uses + +### upload-documents.ts - Data Upload (`npm run upload`) + +**Purpose**: One-time script to upload hotel documents from JSON file to Azure Cosmos DB and create vector embeddings. + +**Command**: `npm run upload` + +**What it does**: +1. Reads hotel data from `data/HotelsData_toCosmosDB.JSON` +2. Connects to Azure OpenAI to generate embeddings +3. Connects to Cosmos DB MongoDB vCore +4. Creates vector index (IVF, HNSW, or DiskANN based on environment config) +5. Uploads documents with embedded vectors + +**When to use**: Run this once after provisioning Azure resources and before running the agent for the first time. + +**Environment variables used**: +- `DATA_FILE_WITHOUT_VECTORS`: Path to source JSON file +- `MONGO_DB_NAME`, `MONGO_DB_COLLECTION`: Target database and collection +- `AZURE_OPENAI_EMBEDDING_DEPLOYMENT`: Embedding model deployment name +- `VECTOR_INDEX_ALGORITHM`: Which vector index to create (ivf/hnsw/diskann) + +### cleanup.ts - Database Cleanup (`npm run cleanup`) + +**Purpose**: Deletes the entire MongoDB database to clean up resources. + +**Command**: `npm run cleanup` + +**What it does**: +1. Connects to Cosmos DB using passwordless OIDC authentication +2. Drops the specified database (defaults to `MONGO_DB_NAME` env var) +3. Closes the connection + +**When to use**: +- Before re-uploading documents with different configuration +- When tearing down the demo environment +- To reset the database to a clean state + +**Note**: This is a destructive operation - it deletes all data in the database. + +### scripts/test-auth.ts - Authentication Testing (`npm run auth`) + +**Purpose**: Comprehensive test suite that validates authentication to all four Azure services. + +**Command**: `npm run auth` + +**What it does**: +1. Tests Azure OpenAI Embeddings API connection +2. Tests Azure OpenAI Chat API (Planner model) +3. Tests Azure OpenAI Chat API (Synthesizer model) +4. Tests Cosmos DB MongoDB connection with OIDC + +**Key feature**: Imports and reuses test functions from individual verification scripts (embed.ts, llm-planner.ts, llm-synth.ts, mongo.ts) rather than duplicating authentication code. + +**Output**: Clear pass/fail status for each service with summary report. + +**When to use**: +- After running `azd up` to verify all services are accessible +- When troubleshooting authentication issues +- Before running the main agent application + +--- + +## Verification Scripts + +These scripts in the `scripts/` folder are individual test utilities that can be imported and run independently. They are used by `test-auth.ts` but can also be run directly for targeted testing. + +### scripts/embed.ts + +**Purpose**: Verify Azure OpenAI Embeddings API authentication and connectivity. + +**What it does**: +- Creates `AzureOpenAIEmbeddings` client with passwordless authentication +- Generates test embeddings for sample strings ("Hello world", "Bonjour le monde") +- Logs the resulting vector dimensions + +**Export**: `testEmbeddings()` function + +### scripts/llm-planner.ts + +**Purpose**: Verify Azure OpenAI Chat API (Planner model) authentication and connectivity. + +**What it does**: +- Creates `AzureChatOpenAI` client for the planner deployment (gpt-4o-mini) +- Sends test message ("Hi there!") +- Logs the response + +**Export**: `testPlanner()` function + +### scripts/llm-synth.ts + +**Purpose**: Verify Azure OpenAI Chat API (Synthesizer model) authentication and connectivity. + +**What it does**: +- Creates `AzureChatOpenAI` client for the synth deployment (gpt-4o) +- Sends test message ("Hi there!") +- Logs the response + +**Export**: `testSynth()` function + +### scripts/mongo.ts + +**Purpose**: Verify Cosmos DB MongoDB connection with OIDC passwordless authentication. + +**What it does**: +- Creates `MongoClient` with MONGODB-OIDC authentication mechanism +- Uses `DefaultAzureCredential` for token acquisition +- Connects to cluster and lists databases +- Closes connection + +**Export**: `testMongoConnection()` function + +--- + +## Core Application Files + +### vector-store.ts - Vector Store Management + +**Purpose**: Central module for managing Azure Cosmos DB MongoDB Vector Store operations and defining the vector search tool. + +**What it provides**: + +1. **Vector Store Creation Functions**: + - `getStore()`: Creates vector store AND uploads documents from JSON file (used by upload-documents.ts) + - `getExistingStore()`: Connects to existing vector store WITHOUT uploading (used by agent.ts) + +2. **Vector Search Tool**: + - `getHotelsToMatchSearchQuery`: LangChain tool definition for vector similarity search + - Used by the Planner agent to search the hotel collection + - Accepts: `query` (string) and `nearestNeighbors` (number) + - Returns: JSON array of matching hotels with scores + +3. **Configuration Management**: + - Vector index configuration (IVF, HNSW, DiskANN) + - Similarity type selection (cosine, L2, inner product) + - Embedding dimensions and model settings + +4. **Helper Functions**: + - `extractPlannerToolOutput()`: Parses agent messages to extract tool call results + - Index creation and management + - Document transformation and filtering + +**How it differs from agent.ts**: +- **vector-store.ts**: Library/module that provides reusable functions and tool definitions +- **agent.ts**: Application entry point that imports and orchestrates these functions + +Think of it this way: +- `vector-store.ts` = The toolbox 🧰 +- `agent.ts` = The craftsman using the tools 👷 + +--- + +## Utility Files + +Shared utilities used across multiple parts of the application. + +### utils/clients.ts + +**Purpose**: Factory functions for creating Azure OpenAI and MongoDB clients with dual authentication support. + +**What it provides**: + +1. **Passwordless Authentication** (`createClientsPasswordless()`): + - Uses `DefaultAzureCredential` from Azure Identity + - OpenAI: Uses `getBearerTokenProvider` with scope `https://cognitiveservices.azure.com/.default` + - MongoDB: Uses MONGODB-OIDC with scope `https://ossrdbms-aad.database.windows.net/.default` + +2. **API Key Authentication** (`createClients()`): + - Uses `AZURE_OPENAI_API_KEY` environment variable + - Traditional connection string for MongoDB + +**Returns**: Object with: +- `embeddingClient`: AzureOpenAIEmbeddings instance +- `plannerClient`: AzureChatOpenAI instance (gpt-4o-mini) +- `synthClient`: AzureChatOpenAI instance (gpt-4o) +- `mongoClient`: MongoClient instance +- `dbConfig`: Database configuration object + +**Used by**: agent.ts, upload-documents.ts, all verification scripts + +### utils/prompts.ts + +**Purpose**: Centralized storage for all LLM system and user prompts. + +**What it defines**: + +1. **Tool Definition**: + - `TOOL_NAME`: Name of the vector search tool + - `TOOL_DESCRIPTION`: Detailed description for the LLM explaining how to use the tool + +2. **Planner Prompts**: + - `PLANNER_SYSTEM_PROMPT`: Instructions for the planner agent + - Defines how to refine queries and when to call tools + - Includes JSON response format requirements + +3. **Synthesizer Prompts**: + - `SYNTHESIZER_SYSTEM_PROMPT`: Instructions for the synthesizer agent + - Defines how to generate user-friendly responses from search results + - `createSynthesizerUserPrompt()`: Function to construct user messages with context + +4. **Default Query**: + - `DEFAULT_QUERY`: Default search query if none provided + +**Why centralized**: Makes it easy to tune agent behavior by updating prompts in one place rather than scattered throughout the code. + +### utils/types.ts + +**Purpose**: TypeScript type definitions and interfaces for data structures. + +**What it defines**: + +1. **Hotel Interface**: Complete hotel document structure including: + - Basic info (ID, name, description, category, tags) + - Ratings and status + - Address (street, city, state, postal code, country) + - Location (GeoJSON Point with coordinates) + - Rooms array + +2. **HotelSearchResult Interface**: Simplified hotel structure returned from vector search: + - Excludes French description, location coordinates, and rooms + - Adds `Score` field for similarity ranking + +3. **Room Interface**: Room details structure + +4. **HotelsData Interface**: Type for the source JSON file structure + +**Used by**: vector-store.ts, agent.ts, and any file working with hotel data + +### utils/debug-handlers.ts + +**Purpose**: LangChain callback handlers for debugging agent execution. + +**What it provides**: + +**`DEBUG_CALLBACKS` array** with handlers for: +- `handleLLMStart`: Logs when LLM generation begins +- `handleLLMNewToken`: Streams tokens as they're generated (real-time output) +- `handleLLMEnd`: Logs completion and tool call metadata +- `handleLLMError`: Logs LLM errors +- `handleAgentAction`: Logs agent decisions and tool selections +- `handleAgentEnd`: Logs final agent output +- `handleToolStart`: Logs tool execution start +- `handleToolEnd`: Logs tool results +- `handleToolError`: Logs tool errors + +**When used**: Passed to agent invocation when `DEBUG=true` in environment variables + +**Why useful**: +- See exactly what the LLM is deciding +- Debug tool calling issues +- Understand agent reasoning flow +- Troubleshoot unexpected behavior + +### utils/mongo.ts + +**Purpose**: MongoDB utility functions using passwordless OIDC authentication. + +**What it provides**: + +1. **`azureIdentityTokenCallback()`**: OIDC token callback function + - Uses `DefaultAzureCredential` to get access token + - Returns token and expiration for MongoDB driver + - Scope: `https://ossrdbms-aad.database.windows.net/.default` + +2. **`deleteCosmosMongoDatabase()`**: Database deletion function + - Creates MongoDB client with OIDC auth + - Connects to cluster + - Drops specified database + - Closes connection + +**Authentication**: Always uses passwordless OIDC (no API keys) + +**Used by**: cleanup.ts (imports and calls `deleteCosmosMongoDatabase()`) + +--- + +## Summary + +### Entry Points (Run via NPM scripts): +- **agent.ts**: Main application - runs the two-agent RAG pipeline +- **upload-documents.ts**: One-time data upload with vector embeddings +- **cleanup.ts**: Database cleanup and teardown +- **scripts/test-auth.ts**: Comprehensive authentication test suite + +### Verification Scripts (Imported by test-auth.ts): +- **scripts/embed.ts**, **llm-planner.ts**, **llm-synth.ts**, **mongo.ts**: Individual service tests + +### Core Library: +- **vector-store.ts**: Vector store management and tool definitions (the toolbox) + +### Utilities: +- **utils/clients.ts**: Client factory functions with dual auth support +- **utils/prompts.ts**: Centralized LLM prompts +- **utils/types.ts**: TypeScript type definitions +- **utils/debug-handlers.ts**: Debug callbacks for agent troubleshooting +- **utils/mongo.ts**: MongoDB utility functions with OIDC auth + +### Key Distinction: +- **agent.ts** = Orchestrator that runs the agent pipeline +- **vector-store.ts** = Library module that provides functions and tools +- Think: agent.ts uses vector-store.ts (not the other way around) diff --git a/mongo-vcore-agent-langchain/docs/DEVELOPMENT-PLAN.md b/mongo-vcore-agent-langchain/docs/DEVELOPMENT-PLAN.md new file mode 100644 index 0000000..8edc8b4 --- /dev/null +++ b/mongo-vcore-agent-langchain/docs/DEVELOPMENT-PLAN.md @@ -0,0 +1,691 @@ +# Development Plan: Building the Hotel Recommendation Agent + +> **Purpose**: A suggested development plan for implementing the hotel recommendation RAG agent in your preferred language/framework. This is a recommended approach, not a strict requirement - adapt to your team's workflow. + +## Prerequisites + +### What's Provided + +✅ **Azure Infrastructure**: All resources provisioned via Bicep/Azure Developer CLI + - Azure OpenAI with 3 model deployments + - Azure Cosmos DB for MongoDB vCore cluster + - Managed Identity for passwordless authentication + - Vector index configuration + +✅ **Sample Data**: Hotel JSON file (`HotelsData_toCosmosDB.JSON`) + +✅ **Environment Variables**: `.env` file with all connection strings and configuration + +✅ **Documentation**: + - `FUNCTIONAL-SPEC.md`: System requirements and architecture + - `AGENT-ORCHESTRATION.md`: Agent implementation patterns + - `CODE.md`: TypeScript reference implementation + - `azure-architecture.mmd`: Infrastructure diagram + +### What You Need to Build + +- Application code in your language (Python, C#, Java, etc.) +- Four executable entry points (auth test, upload, agent, cleanup) +- Vector search implementation +- Two-agent orchestration pipeline + +--- + +## Suggested Development Phases + +### Phase 1: Setup & Authentication (Days 1-2) + +**Goal**: Verify you can connect to all Azure services before writing business logic. + +#### Tasks + +**1.1 Environment Setup** +- [ ] Create project structure in your language +- [ ] Install required dependencies: + - Azure Identity SDK + - OpenAI SDK (or Azure OpenAI SDK) + - MongoDB driver with vector search support + - Any AI framework (LangChain, Semantic Kernel, etc.) - optional + +**1.2 Configuration Management** +- [ ] Load environment variables from `.env` file +- [ ] Create configuration classes/modules: + ``` + - OpenAI config (instance, deployments, API versions) + - MongoDB config (cluster, database, collection) + - App config (debug mode, defaults) + ``` + +**1.3 Authentication Implementation** +- [ ] Implement passwordless authentication helper: + - Azure AD token provider for OpenAI + - OIDC callback for MongoDB +- [ ] (Optional) Implement API key authentication fallback + +**1.4 Client Factories** +- [ ] Create factory functions to instantiate: + - Embedding client (text-embedding-3-small) + - Chat client for planner (gpt-4o-mini) + - Chat client for synthesizer (gpt-4o) + - MongoDB client with vector search capability + +**1.5 Verification Script** +- [ ] Build authentication test suite: + - Test embedding API (generate test embedding) + - Test planner model (send "hello" message) + - Test synth model (send "hello" message) + - Test MongoDB connection (list databases) +- [ ] Run test until all pass: `npm run auth` ✅ + +**Deliverable**: `test-auth` script that validates all 4 service connections + +**Acceptance Criteria**: Running `npm run auth` shows all 4 tests passing with summary report + +--- + +### Phase 2: Data Layer (Days 3-4) + +**Goal**: Load hotel data into Cosmos DB with vector embeddings. + +#### Tasks + +**2.1 Data Loading** +- [ ] Read hotel JSON file +- [ ] Parse and validate hotel objects +- [ ] Understand data model (see FUNCTIONAL-SPEC.md) + +**2.2 Vector Store Implementation** +- [ ] Implement document transformation: + - Combine `HotelName` + `Description` as page content + - Exclude `Description_fr`, `Location`, `Rooms` + - Preserve metadata (all other fields) +- [ ] Generate embeddings for each document: + - Call embedding API with page content + - Handle rate limiting/batching if needed +- [ ] Insert documents into MongoDB: + - Store document + metadata + embedding vector + - Use field name: `contentVector` + +**2.3 Vector Index Creation** +- [ ] Create vector index on collection: + - Default: IVF with `numLists=10` + - Support: HNSW and DiskANN via environment config + - Dimensions: 1536 + - Similarity: Cosine (default) + +**2.4 Upload Script** +- [ ] Build `upload-documents` script: + - Connect to services + - Transform and upload all documents + - Create vector index + - Report success (N documents inserted) + +**2.5 Testing** +- [ ] Run upload script +- [ ] Verify collection exists in MongoDB +- [ ] Verify document count matches source file +- [ ] Verify vector index is created + +**Deliverable**: `upload-documents` script that populates the database + +**Acceptance Criteria**: Collection contains all hotels with embeddings and vector index + +--- + +### Phase 3: Vector Search Tool (Days 5-6) + +**Goal**: Implement semantic search over hotel collection. + +#### Tasks + +**3.1 Search Function** +- [ ] Implement vector search function: + ``` + Input: query (string), k (int) + Process: + 1. Generate embedding for query + 2. Execute MongoDB vector search + 3. Retrieve top-k results with scores + Output: Array of hotel documents with similarity scores + ``` + +**3.2 MongoDB Vector Search** +- [ ] Use MongoDB aggregation pipeline with `$search` stage +- [ ] Configure vector search parameters: + - `queryVector`: Generated embedding + - `path`: "contentVector" + - `numCandidates`: k * 10 (oversampling) + - `limit`: k + - `similarity`: "cosine" +- [ ] Include similarity score in results + +**3.3 Result Formatting** +- [ ] Format each result for agent consumption: + ``` + --- HOTEL START --- + HotelId: {id} + HotelName: {name} + Description: {desc} + Category: {category} + Tags: {tags} + Rating: {rating} + Address.City: {city} + Score: {similarity_score} + --- HOTEL END --- + ``` + +**3.4 Standalone Testing** +- [ ] Create test script that: + - Calls search with test query + - Prints top 5 results + - Validates scores are descending +- [ ] Test various queries: + - "luxury hotel with pool" + - "budget accommodation near downtown" + - "family-friendly hotel with parking" + +**Deliverable**: Working vector search function + +**Acceptance Criteria**: Returns relevant hotels ranked by similarity for test queries + +--- + +### Phase 4: Planner Agent (Days 7-9) + +**Goal**: Build the first agent that refines queries and calls the search tool. + +#### Tasks + +**4.1 Tool Definition** +- [ ] Define search tool schema: + - Name: "search_hotels_collection" + - Description: Clear explanation for LLM + - Parameters: query (string), nearestNeighbors (int) + - Required fields marked +- [ ] Register tool with your framework (or implement manually) + +**4.2 Tool Execution** +- [ ] Implement tool execution function: + - Accepts tool call from LLM + - Extracts parameters + - Calls vector search + - Returns formatted results +- [ ] Handle context passing (vector store, embedding client) + +**4.3 Planner Prompt Engineering** +- [ ] Copy planner system prompt from `AGENT-ORCHESTRATION.md` +- [ ] Customize if needed for your framework +- [ ] Key elements: + - Role: "hotel search planner" + - Mandate: "MUST call the tool" + - Format: JSON tool call structure + - Examples: Few-shot examples + +**4.4 Planner Implementation** +- [ ] Create planner agent: + - Model: gpt-4o-mini + - System prompt: Planner prompt + - Tools: [search_hotels_collection] + - Context: Vector store + embedding client +- [ ] Implement invocation pattern: + - Send user query + - Wait for tool call + - Execute tool + - Extract results + +**4.5 Testing Planner** +- [ ] Test with various queries: + - Simple: "cheap hotel" + - Specific: "hotel near downtown with parking" + - Vague: "nice place to stay" +- [ ] Verify planner: + - Always calls the tool + - Refines query appropriately + - Returns valid search results +- [ ] Add debug logging to see: + - Original query + - Refined query + - Tool call arguments + - Search results + +**Deliverable**: Working planner agent that calls vector search + +**Acceptance Criteria**: Planner reliably calls tool with refined queries for diverse inputs + +--- + +### Phase 5: Synthesizer Agent (Days 10-11) + +**Goal**: Build the second agent that generates recommendations. + +#### Tasks + +**5.1 Synthesizer Prompt Engineering** +- [ ] Copy synthesizer system prompt from `AGENT-ORCHESTRATION.md` +- [ ] Key elements: + - Role: "expert hotel recommendation assistant" + - Constraint: "Only use top 3 results" + - Task: "Compare and recommend" + - Format: "Plain text, under 220 words" + +**5.2 User Prompt Construction** +- [ ] Create function to build synthesizer input: + ``` + User asked: {original_query} + + Tool summary: + {formatted_search_results} + + Analyze the TOP 3 results... + ``` +- [ ] Ensure search results are properly formatted + +**5.3 Synthesizer Implementation** +- [ ] Create synthesizer agent: + - Model: gpt-4o + - System prompt: Synthesizer prompt + - NO tools (pure generation) +- [ ] Implement invocation: + - Input: Query + search results + - Output: Natural language recommendation + +**5.4 Testing Synthesizer** +- [ ] Test with sample search results (can use static data) +- [ ] Verify output: + - Compares top 3 hotels + - Identifies tradeoffs + - Recommends best option + - Suggests alternatives + - Stays under word limit +- [ ] Refine prompts if needed + +**Deliverable**: Working synthesizer agent + +**Acceptance Criteria**: Generates clear, concise recommendations comparing top options + +--- + +### Phase 6: End-to-End Pipeline (Days 12-13) + +**Goal**: Connect both agents into complete workflow. + +#### Tasks + +**6.1 Pipeline Orchestration** +- [ ] Build main agent function: + ```python + def run_agent(user_query): + # 1. Run planner agent + search_results = run_planner(user_query) + + # 2. Extract tool output + formatted_results = extract_results(search_results) + + # 3. Run synthesizer agent + final_answer = run_synthesizer(user_query, formatted_results) + + return final_answer + ``` + +**6.2 Context Flow** +- [ ] Ensure proper data passing: + - User query flows to both agents + - Planner has access to vector store + - Synthesizer receives formatted results + - Original query preserved throughout + +**6.3 Main Application** +- [ ] Create `agent` entry point script: + - Load configuration + - Initialize clients + - Connect to vector store (no upload) + - Accept query (from env var or CLI arg) + - Run pipeline + - Display final recommendation + +**6.4 End-to-End Testing** +- [ ] Test complete pipeline with queries: + - "luxury hotel with spa and pool" + - "budget hotel near downtown Seattle" + - "family-friendly accommodation with parking" + - "business hotel with meeting rooms and wifi" +- [ ] Verify full flow works +- [ ] Check output quality + +**6.5 Polish** +- [ ] Add console output formatting: + - Section headers (--- PLANNER ---, --- SYNTHESIZER ---) + - Character counts + - Timing information (optional) +- [ ] Add error handling: + - Connection failures + - Empty search results + - LLM errors + +**Deliverable**: `agent` script that runs complete RAG pipeline + +**Acceptance Criteria**: User query → Final recommendation works reliably + +--- + +### Phase 7: Utilities & Cleanup (Day 14) + +**Goal**: Complete the supporting scripts and polish. + +#### Tasks + +**7.1 Cleanup Script** +- [ ] Build `cleanup` script: + - Connect to MongoDB with passwordless auth + - Drop specified database + - Confirm deletion + - Report success + +**7.2 Error Handling** +- [ ] Add comprehensive error handling: + - Authentication failures + - Network errors + - Invalid responses + - Empty results +- [ ] Provide helpful error messages + +**7.3 Logging/Debug Mode** +- [ ] Implement debug mode (via DEBUG env var): + - Log all LLM requests + - Log all LLM responses + - Log tool calls and results + - Log context passing +- [ ] Keep production output clean + +**7.4 Documentation** +- [ ] Write README for your implementation: + - Prerequisites + - Setup instructions + - How to run each script + - Environment variables + - Troubleshooting +- [ ] Add code comments for complex sections + +**7.5 Final Testing** +- [ ] Test all four entry points: + - `test-auth` ✅ + - `upload-documents` ✅ + - `agent` ✅ + - `cleanup` ✅ +- [ ] Test with diverse queries +- [ ] Test error scenarios + +**Deliverable**: Complete, documented application + +**Acceptance Criteria**: All scripts work, code is clean, README is clear + +--- + +## Alternative Approaches + +### Iterative Development (Faster Path) + +If you want quicker results, consider this order: + +**Week 1 Focus**: Get something working end-to-end +1. Day 1-2: Authentication + Vector Search (no agents) +2. Day 3: Single agent with hardcoded prompts +3. Day 4-5: Add second agent, connect pipeline +4. **Result**: Basic working demo + +**Week 2 Focus**: Polish and optimize +1. Day 6-7: Improve prompts based on testing +2. Day 8-9: Add data upload script +3. Day 10: Add error handling and logging +4. Day 11-12: Write tests and documentation +5. **Result**: Production-ready application + +### Framework-First vs. Framework-Free + +**Option A: Use AI Framework** +- Faster development (abstractions provided) +- Less control over internals +- Examples: LangChain, Semantic Kernel, LlamaIndex + +**Option B: Direct SDK Integration** +- More code to write +- Full control over behavior +- Better for learning/customization +- Use OpenAI SDK directly with custom orchestration + +Both approaches are valid - choose based on team expertise and requirements. + +--- + +## Testing Strategy + +### Unit Tests + +Test individual components in isolation: +- [ ] Configuration loading +- [ ] Authentication token acquisition +- [ ] Embedding generation +- [ ] Vector search query execution +- [ ] Result formatting +- [ ] Prompt construction + +### Integration Tests + +Test components working together: +- [ ] End-to-end authentication flow +- [ ] Data upload → query → results +- [ ] Planner → tool → synthesizer pipeline + +### Functional Tests + +Test with real queries: +- [ ] Diverse query types (specific, vague, multi-criteria) +- [ ] Edge cases (no results, many results) +- [ ] Error scenarios (bad auth, network issues) + +### Quality Checks + +Verify output quality: +- [ ] Recommendations are relevant +- [ ] Comparisons are accurate +- [ ] Output format is consistent +- [ ] Response time is acceptable (<10 seconds typical) + +--- + +## Development Environment + +### Recommended Tools + +**IDE/Editor**: +- VS Code with language extensions +- Cursor, Copilot, or other AI assistants (highly recommended) + +**Testing**: +- Language-specific test framework (pytest, xUnit, JUnit, etc.) +- Postman/curl for API testing + +**Debugging**: +- Language debugger +- Azure Portal for resource monitoring +- Cosmos DB Data Explorer for database inspection + +**Version Control**: +- Git for source control +- Branch strategy: feature branches → PR → main + +### Environment Setup Checklist + +- [ ] Language runtime installed (Python 3.10+, .NET 8+, Node 20+, etc.) +- [ ] Package manager configured (pip, npm, NuGet, Maven, etc.) +- [ ] Azure CLI installed and logged in (`az login`) +- [ ] `.env` file present with all variables +- [ ] Dependencies installed from package manifest + +--- + +## Common Pitfalls to Avoid + +### Authentication Issues +❌ **Don't**: Hardcode API keys in source code +✅ **Do**: Use environment variables and Azure Identity + +❌ **Don't**: Skip authentication testing +✅ **Do**: Run `test-auth` first before building features + +### Vector Search +❌ **Don't**: Forget to create vector index +✅ **Do**: Verify index exists before querying + +❌ **Don't**: Use wrong embedding model for search +✅ **Do**: Same model for upload and query (text-embedding-3-small) + +### Agent Orchestration +❌ **Don't**: Make prompts too vague +✅ **Do**: Be explicit about what agent should/shouldn't do + +❌ **Don't**: Let synthesizer call additional tools +✅ **Do**: Constrain synthesizer to only analyze provided results + +❌ **Don't**: Pass too much context between agents +✅ **Do**: Keep context minimal and relevant + +### Error Handling +❌ **Don't**: Let exceptions crash the application silently +✅ **Do**: Catch, log, and provide helpful error messages + +❌ **Don't**: Retry indefinitely on failures +✅ **Do**: Implement exponential backoff with max retries + +--- + +## Success Metrics + +### Phase Completion + +Each phase is complete when: +- ✅ All tasks in checklist are done +- ✅ Tests pass +- ✅ Code is committed +- ✅ Documentation is updated + +### Project Completion + +Project is complete when: +- ✅ All 4 entry points work (`test-auth`, `upload`, `agent`, `cleanup`) +- ✅ Authentication works (preferably passwordless) +- ✅ Vector search returns relevant results +- ✅ Agent pipeline generates quality recommendations +- ✅ Code is clean, tested, and documented +- ✅ Demo-ready for stakeholders + +### Quality Indicators + +Your implementation is high quality if: +- ✅ Recommendations are accurate and helpful +- ✅ Response time is under 10 seconds (typical) +- ✅ Prompts are well-engineered (consistent behavior) +- ✅ Error messages are clear and actionable +- ✅ Code follows language best practices +- ✅ README enables another developer to run it + +--- + +## Getting Help + +### Resources + +**Documentation**: +- `FUNCTIONAL-SPEC.md`: System architecture and requirements +- `AGENT-ORCHESTRATION.md`: Agent design patterns +- `CODE.md`: TypeScript reference implementation +- `SCRIPTS.md`: Testing procedures + +**Azure Docs**: +- [Azure OpenAI Service](https://learn.microsoft.com/azure/ai-services/openai/) +- [Cosmos DB MongoDB vCore](https://learn.microsoft.com/azure/cosmos-db/mongodb/vcore/) +- [Managed Identity](https://learn.microsoft.com/azure/active-directory/managed-identities-azure-resources/) + +**Framework Docs** (if applicable): +- LangChain: https://langchain.com/docs +- Semantic Kernel: https://learn.microsoft.com/semantic-kernel +- LlamaIndex: https://docs.llamaindex.ai + +### Debugging Tips + +**If authentication fails**: +1. Check `.env` file has all variables +2. Run `az login` and verify subscription +3. Check RBAC role assignments in Azure Portal +4. Test each service independently + +**If vector search returns bad results**: +1. Verify vector index exists +2. Check embedding dimensions match (1536) +3. Test with very specific query first +4. Increase `k` value to see more results + +**If agent doesn't call tool**: +1. Check system prompt has "MUST call tool" instruction +2. Add few-shot examples to prompt +3. Enable debug logging to see LLM responses +4. Verify tool schema is properly registered + +**If output quality is poor**: +1. Review and refine system prompts +2. Check top 3 search results are actually relevant +3. Add more constraints to synthesizer prompt +4. Test with different k values + +--- + +## Timeline Summary + +**Suggested 2-3 Week Plan**: + +| Phase | Days | Focus | Key Deliverable | +|-------|------|-------|----------------| +| 1 | 1-2 | Setup & Auth | `test-auth` script | +| 2 | 3-4 | Data Layer | `upload-documents` script | +| 3 | 5-6 | Vector Search | Search function | +| 4 | 7-9 | Planner Agent | Planner with tool calling | +| 5 | 10-11 | Synthesizer | Response generation | +| 6 | 12-13 | Pipeline | `agent` script (E2E) | +| 7 | 14 | Utilities | `cleanup` + polish | + +**Flexible Timeline**: Adjust based on team size, experience, and language familiarity. Experienced developers might complete in 7-10 days. Teams learning a new framework might take 3-4 weeks. + +--- + +## Final Notes + +### This is a Suggestion + +This plan is **one possible approach**. Feel free to: +- Reorder phases based on your workflow +- Combine or split phases as needed +- Use different tools or frameworks +- Add features beyond the spec +- Adapt to your team's process + +### Focus on Learning + +This project teaches: +- Vector search and embeddings +- RAG (Retrieval-Augmented Generation) +- Agent orchestration patterns +- Azure OpenAI integration +- Cosmos DB MongoDB API +- Passwordless authentication + +Take time to understand **why** things work, not just **how** to implement them. + +### Iterate and Improve + +Start simple, get it working, then optimize: +1. **First**: Make it work (basic functionality) +2. **Second**: Make it right (clean code, error handling) +3. **Third**: Make it fast (optimize performance) +4. **Finally**: Make it robust (comprehensive testing) + +Good luck! 🚀 diff --git a/mongo-vcore-agent-langchain/docs/FUNCTIONAL-SPEC.md b/mongo-vcore-agent-langchain/docs/FUNCTIONAL-SPEC.md new file mode 100644 index 0000000..4431f9b --- /dev/null +++ b/mongo-vcore-agent-langchain/docs/FUNCTIONAL-SPEC.md @@ -0,0 +1,453 @@ +# Functional Specification: Hotel Recommendation RAG Agent + +> **Purpose**: This document provides a language-agnostic specification of the hotel recommendation system so it can be implemented in any programming language or framework. + +## Table of Contents + +- [Overview](#overview) +- [System Architecture](#system-architecture) +- [Data Model](#data-model) +- [Azure Resources Required](#azure-resources-required) +- [Authentication](#authentication) +- [Core Workflows](#core-workflows) +- [Application Entry Points](#application-entry-points) +- [Vector Search Implementation](#vector-search-implementation) +- [Agent Pipeline Specification](#agent-pipeline-specification) +- [Environment Configuration](#environment-configuration) + +--- + +## Overview + +**Application Type**: Retrieval-Augmented Generation (RAG) system using a two-agent pipeline + +**Domain**: Hotel recommendation based on natural language queries + +**Technology Stack** (framework agnostic): +- Vector database: Azure Cosmos DB for MongoDB vCore +- LLM Provider: Azure OpenAI Service +- Embedding Model: text-embedding-3-small (1536 dimensions) +- Chat Models: gpt-4o-mini (planner), gpt-4o (synthesizer) + +**Core Capability**: User provides natural language query → System returns ranked hotel recommendations with comparative analysis + +--- + +## System Architecture + +### High-Level Flow + +``` +User Query + ↓ +[Planner Agent] ← Uses Chat Model (gpt-4o-mini) + ↓ +[Vector Search Tool] ← Generates embedding & queries Cosmos DB + ↓ +[Search Results] → JSON array of hotels with similarity scores + ↓ +[Synthesizer Agent] ← Uses Chat Model (gpt-4o) + ↓ +Final Recommendation → Natural language response +``` + +### Component Breakdown + +1. **Planner Agent**: + - **Input**: User's natural language query + - **Process**: Refines query and calls vector search tool + - **Output**: Structured search results (JSON) + +2. **Vector Search Tool**: + - **Input**: Refined query string + number of neighbors (k) + - **Process**: Generate embedding → Execute vector similarity search + - **Output**: Top-k hotels with metadata and similarity scores + +3. **Synthesizer Agent**: + - **Input**: Original query + search results + - **Process**: Analyze and compare top 3 results + - **Output**: Natural language recommendation with tradeoffs + +--- + +## Data Model + +### Hotel Document Structure + +Each hotel document must contain the following fields: + +```json +{ + "HotelId": "string (unique identifier)", + "HotelName": "string", + "Description": "string (English description)", + "Category": "string (e.g., 'Budget', 'Luxury')", + "Tags": ["array", "of", "strings"], + "ParkingIncluded": "boolean", + "IsDeleted": "boolean", + "LastRenovationDate": "ISO 8601 date string", + "Rating": "number (0-5)", + "Address": { + "StreetAddress": "string", + "City": "string", + "StateProvince": "string (optional)", + "PostalCode": "string", + "Country": "string" + } +} +``` + +**Fields NOT stored in vector database** (excluded during upload): +- `Description_fr`: French description +- `Location`: GeoJSON coordinates +- `Rooms`: Room details array + +### Vector Store Configuration + +**Embedding Field**: +- Field name: `contentVector` +- Dimensions: 1536 +- Generated from: Concatenation of `HotelName` and `Description` + +**Vector Index Types** (configurable): +1. **IVF (Inverted File Index)**: Default, balanced performance + - Parameter: `numLists` (default: 10) +2. **HNSW (Hierarchical Navigable Small World)**: Fast queries + - Parameters: `m` (default: 16), `efConstruction` (default: 64) +3. **DiskANN**: Memory-efficient for large datasets + +**Similarity Metric** (configurable): +- Cosine (COS) - default +- L2 (Euclidean distance) +- IP (Inner Product) + +--- + +## Azure Resources Required + +### 1. Azure OpenAI Service + +**Models to Deploy**: +1. **Embeddings**: `text-embedding-3-small` + - API Version: 2024-08-01-preview or later + +2. **Planner Chat Model**: `gpt-4o-mini` + - API Version: 2024-08-01-preview or later + +3. **Synthesizer Chat Model**: `gpt-4o` + - API Version: 2024-08-01-preview or later + +**Required Outputs**: +- OpenAI instance name +- Each model's deployment name +- API versions for each model + +### 2. Azure Cosmos DB for MongoDB vCore + +**Requirements**: +- MongoDB vCore cluster (not standard Cosmos DB) +- Vector search capability enabled +- Database name (e.g., `hotels`) +- Collection name (e.g., `hotel_data`) + +**Required Outputs**: +- Cluster name (e.g., `my-cluster`) +- Connection format: `mongodb+srv://{cluster}.global.mongocluster.cosmos.azure.com/` + +### 3. Managed Identity (Recommended) + +**Purpose**: Passwordless authentication to Azure services + +**Type**: User-assigned managed identity + +**Required Role Assignments**: +- Azure OpenAI: `Cognitive Services OpenAI User` +- Cosmos DB: MongoDB user with read/write permissions + +--- + +## Authentication + +### Two Authentication Modes + +#### Mode 1: Passwordless (Recommended) + +**Azure OpenAI**: +- Use Azure AD bearer token +- Token scope: `https://cognitiveservices.azure.com/.default` +- Refresh token automatically when expired + +**Cosmos DB MongoDB**: +- Use MONGODB-OIDC authentication mechanism +- Token scope: `https://ossrdbms-aad.database.windows.net/.default` +- Provide OIDC callback function to retrieve token + +**Implementation Requirements**: +- Azure Identity library (or equivalent) +- DefaultAzureCredential (supports managed identity, Azure CLI, etc.) + +#### Mode 2: API Keys + +**Azure OpenAI**: +- Use API key from Azure portal +- Pass in header: `api-key: {key}` + +**Cosmos DB MongoDB**: +- Use standard MongoDB connection string with username/password + +--- + +## Core Workflows + +### Workflow 1: Data Upload (One-time Setup) + +**Purpose**: Load hotel data into vector database + +**Steps**: +1. Read hotel data from JSON file +2. Connect to embedding model (passwordless or API key) +3. Connect to MongoDB database +4. For each hotel document: + - Create page content: `"Hotel: {HotelName}\n\n{Description}"` + - Exclude: `Description_fr`, `Location`, `Rooms` + - Generate embedding for page content + - Insert document with embedding into collection +5. Create vector index on collection (IVF/HNSW/DiskANN) +6. Close connections + +**Input**: JSON file with array of hotel objects + +**Output**: Populated collection with vector index + +### Workflow 2: Vector Search Query + +**Purpose**: Find hotels matching a natural language query + +**Steps**: +1. Receive search query (string) and k (number of neighbors) +2. Generate embedding for query using embedding model +3. Execute vector similarity search against collection: + - Field: `contentVector` + - Similarity metric: cosine/L2/IP + - Limit: k results +4. Retrieve documents with similarity scores +5. Format results as JSON array with hotel metadata + scores +6. Return formatted results + +**Input**: +- Query: string +- k: integer (1-20) + +**Output**: JSON array of hotels with scores + +### Workflow 3: Agent Pipeline + +**Purpose**: Generate natural language recommendation from user query + +**Steps**: +1. Receive user query +2. **Planner Agent**: + - Input: User query + - System prompt: Instructs to refine query and call search tool + - Action: Calls vector search tool with refined query + k value + - Output: Search results (JSON) +3. **Parse Results**: Extract tool output from planner response +4. **Synthesizer Agent**: + - Input: Original query + search results + - System prompt: Instructs to compare top 3 and recommend + - Output: Natural language recommendation +5. Display final recommendation to user + +--- + +## Application Entry Points + +### Entry Point 1: Authentication Test + +**Purpose**: Verify connectivity to all Azure services + +**Requirements**: +- Test embedding API +- Test planner chat model +- Test synthesizer chat model +- Test MongoDB connection + +**Success Criteria**: All 4 services return successful responses + +### Entry Point 2: Data Upload + +**Purpose**: Initialize database with hotel data + +**Requirements**: +- Run once after infrastructure provisioning +- Creates collection, inserts documents, creates vector index + +**Success Criteria**: Collection exists with N documents and vector index + +### Entry Point 3: Agent Application + +**Purpose**: Run hotel recommendation agent + +**Requirements**: +- Connect to existing vector store (no upload) +- Accept user query as input +- Execute two-agent pipeline +- Return final recommendation + +**Success Criteria**: Natural language recommendation displayed + +### Entry Point 4: Database Cleanup + +**Purpose**: Delete database and all data + +**Requirements**: +- Use passwordless authentication +- Drop specified database +- Confirm deletion + +**Success Criteria**: Database no longer exists + +--- + +## Vector Search Implementation + +### Query Execution Details + +**Algorithm**: +``` +1. Input: query_text, k +2. embedding = generate_embedding(query_text) +3. results = vector_search( + collection="hotel_data", + vector_field="contentVector", + query_vector=embedding, + k=k, + similarity="cosine" + ) +4. For each result: + - Add similarity score + - Format hotel metadata +5. Return formatted_results +``` + +### MongoDB Vector Search Query Structure + +When using MongoDB's vector search capability, the query should use the aggregation pipeline: + +``` +db.hotel_data.aggregate([ + { + "$search": { + "vectorSearch": { + "queryVector": [/* embedding array */], + "path": "contentVector", + "numCandidates": k * 10, // Oversampling factor + "limit": k, + "similarity": "cosine" + } + } + }, + { + "$project": { + "score": { "$meta": "searchScore" }, + "document": "$$ROOT" + } + } +]) +``` + +### Result Format + +Each search result must include: +- All hotel metadata fields (see Data Model) +- Similarity score (0-1 for cosine, varies by metric) + +--- + +## Agent Pipeline Specification + +This section is covered in detail in the separate **AGENT-ORCHESTRATION.md** document. + +**Key Concepts**: +- Two-agent architecture (Planner + Synthesizer) +- Tool calling / function calling +- System prompts and user prompts +- Context passing between agents + +See **AGENT-ORCHESTRATION.md** for implementation details. + +--- + +## Environment Configuration + +### Required Environment Variables + +**Authentication Mode**: +- `USE_PASSWORDLESS`: `"true"` or `"false"` (default: false) + +**Azure OpenAI**: +- `AZURE_OPENAI_API_INSTANCE_NAME`: Instance name +- `AZURE_OPENAI_API_KEY`: API key (if not passwordless) +- `AZURE_OPENAI_EMBEDDING_DEPLOYMENT`: Deployment name for embeddings +- `AZURE_OPENAI_EMBEDDING_API_VERSION`: API version +- `AZURE_OPENAI_PLANNER_DEPLOYMENT`: Deployment name for planner +- `AZURE_OPENAI_PLANNER_API_VERSION`: API version +- `AZURE_OPENAI_SYNTH_DEPLOYMENT`: Deployment name for synthesizer +- `AZURE_OPENAI_SYNTH_API_VERSION`: API version + +**Cosmos DB MongoDB**: +- `MONGO_CLUSTER_NAME`: Cluster name (for passwordless) +- `MONGO_CONNECTION_STRING`: Full connection string (if not passwordless) +- `MONGO_DB_NAME`: Database name (e.g., `hotels`) +- `MONGO_DB_COLLECTION`: Collection name (e.g., `hotel_data`) + +**Application Settings**: +- `DATA_FILE_WITHOUT_VECTORS`: Path to hotel JSON file +- `QUERY`: Default search query (optional) +- `NEAREST_NEIGHBORS`: Default k value (optional, default: 5) +- `DEBUG`: Enable debug logging (`"true"` or `"false"`) + +**Vector Index Configuration** (optional): +- `VECTOR_INDEX_ALGORITHM`: `"vector-ivf"` | `"vector-hnsw"` | `"vector-diskann"` +- `VECTOR_SIMILARITY`: `"COS"` | `"L2"` | `"IP"` +- `EMBEDDING_DIMENSIONS`: Number (default: 1536) +- `IVF_NUM_LISTS`: Number (default: 10) +- `HNSW_M`: Number (default: 16) +- `HNSW_EF_CONSTRUCTION`: Number (default: 64) + +--- + +## Implementation Checklist + +To implement this system in any language/framework: + +- [ ] Azure resource provisioning (OpenAI + Cosmos DB + Managed Identity) +- [ ] Authentication implementation (passwordless + API key modes) +- [ ] MongoDB client with vector search capability +- [ ] Embedding generation client +- [ ] Chat completion client (with tool/function calling support) +- [ ] Data upload workflow with vector index creation +- [ ] Vector search tool implementation +- [ ] Two-agent pipeline orchestration +- [ ] Environment configuration management +- [ ] Authentication test suite +- [ ] Error handling and logging + +--- + +## Next Steps + +1. Read **AGENT-ORCHESTRATION.md** for detailed agent implementation patterns +2. Review the TypeScript implementation in `src/` as a reference +3. Adapt the workflows and specifications to your language/framework +4. Test authentication before implementing the full pipeline +5. Start with data upload, then search, then agent orchestration + +--- + +## Additional Resources + +- **CODE.md**: Language-specific implementation details (TypeScript/LangChain) +- **AGENT-ORCHESTRATION.md**: Framework-agnostic agent patterns +- **SCRIPTS.md**: Verification and testing procedures +- **azure-architecture.mmd**: Infrastructure diagram diff --git a/mongo-vcore-agent-langchain/docs/HERO-JOURNEY.md b/mongo-vcore-agent-langchain/docs/HERO-JOURNEY.md new file mode 100644 index 0000000..32ff2b4 --- /dev/null +++ b/mongo-vcore-agent-langchain/docs/HERO-JOURNEY.md @@ -0,0 +1,216 @@ +# The Developer's Quest: Building Intelligent Hotel Recommendations + +## The Ordinary World + +You're a developer at a growing travel platform. Your company has a database of hotels, but users struggle to find what they need. They type "cheap hotel downtown" and get frustrated with irrelevant results. Your product manager approaches with a challenge: *"We need intelligent recommendations, not just keyword matching. Can AI help us understand what travelers really want?"* + +You've heard about RAG (Retrieval-Augmented Generation) and vector search, but they seem complex. Building AI-powered search from scratch feels daunting—authentication, embeddings, agent orchestration, cloud infrastructure. Where do you even start? + +## The Call to Adventure + +Then you discover Azure Cosmos DB with vector search capabilities and Azure OpenAI's powerful models. Better yet, there's a working TypeScript implementation using LangChain—a complete hotel recommendation agent that does exactly what your product needs. + +But there's a catch: your team doesn't work in TypeScript. You have Python developers, C# engineers, and Java specialists. Each needs this solution in their own language. You realize you're not just implementing a solution—you're becoming the **guide** who will help others on their teams recreate this magic in their preferred languages. + +## Meeting the Mentor + +You dive into the documentation and discover your mentor isn't a person—it's a comprehensive set of specifications that transcend any single language: + +- **FUNCTIONAL-SPEC.md**: The language-agnostic blueprint showing *what* to build +- **AGENT-ORCHESTRATION.md**: The wisdom of how two AI agents work together +- **DEVELOPMENT-PLAN.md**: A proven path from zero to working agent +- **CODE.md**: A TypeScript implementation to learn from + +This isn't just code to copy—it's knowledge to transfer. You're not alone on this journey. + +## Crossing the Threshold + +You decide to take the first step. Following the Development Plan, you begin Phase 1: Setup & Authentication. This is where most developers fail—getting passwordless authentication working with Azure OpenAI and Cosmos DB simultaneously. + +You start with the verification script: +```bash +npm run auth +``` + +This single command runs four critical tests: +- ✅ Can you generate vector embeddings? +- ✅ Can you talk to the planning AI? +- ✅ Can you access the synthesizer AI? +- ✅ Can you connect to the vector database? + +One by one, the tests pass. Green checkmarks light up your terminal. The summary shows 4/4 tests passed. You've crossed into the Azure cloud ecosystem successfully. Your team watches, curious about what you're building. + +## Tests, Allies, and Enemies + +### First Test: Understanding Vector Search + +You encounter your first challenge: *How does semantic search actually work?* Hotels aren't matched by keywords but by *meaning*. "Cheap hotel downtown" becomes a 1536-dimensional vector, magically finding hotels that match the *intent*, not just the words. + +You run `npm run upload` and watch as hotel descriptions transform into vectors and flow into Cosmos DB. The vector index builds—IVF for balanced performance, or HNSW for speed, or DiskANN for scale. You choose IVF for now. The data is ready. + +### Second Test: The Two-Agent Architecture + +Now comes the sophisticated part—and your first real test of understanding. You need to design the agent architecture for RAG. The stakes are higher than you think. + +**What happens if you get it wrong:** + +You could use a *single powerful agent* to do everything: +- ❌ Every query burns through expensive GPT-4 tokens for simple tasks +- ❌ Your Azure bill explodes: $10/million tokens when you only needed $0.15 +- ❌ Slower responses—the big model is overkill for structured searches +- ❌ Harder to debug—one monolithic agent doing search AND reasoning + +Or you could use *no agents at all*, just direct vector search: +- ❌ Users get raw hotel data dumps with no context +- ❌ No comparison, no recommendations, no understanding of tradeoffs +- ❌ "Here are 10 hotels"—which one is best? Why? Crickets. + +**The correct architecture for Agentic RAG:** + +You discover the winning pattern: **a two-agent pipeline with specialized roles**. + +**Agent 1 - The Planner** (gpt-4o-mini): +- **Role**: Query understanding and tool orchestration +- **Why this model**: Fast, cheap ($0.15/M tokens), optimized for function calling +- **What it does**: + - Refines vague queries: "cheap hotel" → "budget-friendly accommodation near downtown with good value" + - Decides search parameters (how many results, which filters) + - Calls the vector search tool + - Returns structured results as JSON + +**Agent 2 - The Synthesizer** (gpt-4o): +- **Role**: Analysis and natural language generation +- **Why this model**: Superior reasoning ($2.50/M tokens), worth it for final output quality +- **What it does**: + - Receives search results + original query + - Compares top 3 hotels with nuanced reasoning + - Explains tradeoffs: "Hotel A has better amenities, but Hotel B offers superior value for families on a budget..." + - Generates personalized recommendations + +**Why this architecture wins:** + +This separation of concerns is the secret: +- 💰 **Cost**: Use cheap intelligence for 80% of the work (search orchestration), expensive intelligence only for the 20% that matters (creative synthesis) +- ⚡ **Speed**: Faster model handles the heavy lifting, slow model only for final polish +- 🐛 **Debuggability**: You can test search quality separately from recommendation quality +- 📈 **Scalability**: Your Azure bill scales gracefully as traffic grows + +The pattern has a name: **Agentic RAG** (Retrieval-Augmented Generation with agent orchestration). It's not just about connecting to a vector database—it's about intelligently routing work through specialized agents. + +Your team's cloud budget will thank you. More importantly, your users get fast, intelligent, contextual recommendations without burning through your OpenAI quota. + +### Finding Allies + +As you build your understanding, you realize the documentation becomes your ally: +- Stuck on prompts? Check **AGENT-ORCHESTRATION.md** for the exact system messages +- Confused about data models? **FUNCTIONAL-SPEC.md** has the complete JSON schema +- Authentication failing? **SCRIPTS.md** shows you how to debug with verification scripts + +You're not just copying code—you're learning the *principles* that make this system work. + +## The Ordeal + +Your moment of truth arrives: implementing this in your team's language. Let's say it's Python. + +You face the challenges: +- How do you handle OIDC callbacks for MongoDB in Python? +- What's the Python equivalent of LangChain's tool decorator? +- How do you pass context between agents without TypeScript's type safety? + +You return to the specifications. **FUNCTIONAL-SPEC.md** shows the authentication flow language-agnostically. **AGENT-ORCHESTRATION.md** explains the prompt patterns that work in any framework. You realize the TypeScript code is a *reference*, not a prison. + +You adapt, translate, rebuild. When you get stuck, you compare your Python implementation against the TypeScript original—not to copy syntax, but to understand intent. + +## The Reward + +After days of focused work, you run your Python agent: + +``` +Query: "family friendly hotel with pool" + +[Planner Agent] Refining query and searching... +[Vector Search] Found 10 similar hotels +[Synthesizer Agent] Analyzing top 3 results... + +Response: "I recommend the Sunset Resort for your family. +It offers a large pool area with a children's section, +received excellent family reviews (4.5/5), and is +reasonably priced at $180/night. If you need something +more budget-friendly, the Harbor Inn also has pool access +at $120/night, though with fewer family amenities..." +``` + +It works. The recommendations are intelligent, contextual, and helpful. Your product manager is thrilled. But more importantly, you've *learned* how to build AI agents with vector search. + +## The Road Back + +Now you become the guide for others. Your C# colleague needs this for a .NET microservice. Your Java teammate wants to integrate it into an enterprise application. + +You don't just share code—you share knowledge: + +*"Start with authentication verification. Don't touch the agent until all four services connect."* + +*"The two-agent pattern isn't about the framework—it's about separating planning from synthesis."* + +*"Read FUNCTIONAL-SPEC.md first. It's language-agnostic for a reason."* + +You create a quick reference guide for your team, mapping concepts across languages: + +| Concept | TypeScript | Python | C# | Java | +|---------|-----------|--------|-----|------| +| Azure Identity | `@azure/identity` | `azure.identity` | `Azure.Identity` | `azure-identity` | +| MongoDB Client | `mongodb` | `pymongo` | `MongoDB.Driver` | `mongodb-driver` | +| OpenAI SDK | `@azure/openai` | `openai` | `Azure.AI.OpenAI` | `azure-ai-openai` | +| Vector Store | LangChain | LangChain/LlamaIndex | Semantic Kernel | LangChain4j | + +## Resurrection: Mastery Through Teaching + +As your teammates implement their versions, they encounter edge cases you didn't: connection pooling in Java, async patterns in Python, dependency injection in C#. Each challenge deepens your understanding. + +You document these learnings, contributing back to the project. You add troubleshooting sections: + +*"In Python, MongoDB OIDC callbacks must be thread-safe..."* +*"C# requires explicit token refresh handling..."* +*"Java connection strings need URL encoding for special characters..."* + +The specifications evolve. What started as TypeScript documentation becomes truly language-agnostic wisdom. + +## Return with the Elixir + +Six weeks after your journey began, your team has: +- ✅ TypeScript agent (original) +- ✅ Python agent (your first translation) +- ✅ C# microservice (backend team) +- ✅ Java integration (enterprise system) + +All using the same Azure infrastructure. All following the same two-agent pattern. All delivering intelligent hotel recommendations. + +But the real treasure isn't the code—it's the **reusable pattern** you've mastered: + +1. **Vector embeddings** turn unstructured text into searchable meaning +2. **Cosmos DB with vector indexes** provides scalable semantic search +3. **Two-agent orchestration** balances cost and capability +4. **Passwordless authentication** simplifies security across languages +5. **Framework-agnostic specifications** enable any language to succeed + +Your users now find hotels that match their *intent*, not just their keywords. Your company's AI journey has begun. And you've become the guide who helps others cross the threshold from traditional databases to intelligent, AI-powered search. + +## Your Call to Adventure + +Now it's your turn. The infrastructure awaits: +```bash +azd up # Provision Azure resources +npm run auth # Verify connections +npm run upload # Load and vectorize data +npm run start # Run your first agent query +``` + +The specifications are your map. The TypeScript code is your reference. The two-agent pattern is your blueprint. + +Whether you build in Python, Java, C#, Go, or any other language—the journey is the same. The hero is **you**. The mentor is this documentation. The reward is mastery of AI-powered search. + +What will you build? + +--- + +*Ready to start your quest? Begin with [DEVELOPMENT-PLAN.md](./DEVELOPMENT-PLAN.md) for your step-by-step guide, or dive into [FUNCTIONAL-SPEC.md](./FUNCTIONAL-SPEC.md) to understand the complete system architecture.* diff --git a/mongo-vcore-agent-langchain/docs/SCRIPTS.md b/mongo-vcore-agent-langchain/docs/SCRIPTS.md new file mode 100644 index 0000000..49dfda5 --- /dev/null +++ b/mongo-vcore-agent-langchain/docs/SCRIPTS.md @@ -0,0 +1,179 @@ +# Verification Scripts + +The `./scripts` folder contains standalone scripts to verify passwordless (Azure AD) authentication to Azure services after running `azd provision` or `azd up`. + +## Purpose + +These scripts help you validate that your Azure environment is correctly configured for passwordless authentication using managed identities or DefaultAzureCredential. They test connectivity to: + +- **Azure OpenAI** - Embedding and chat completion models +- **Azure Cosmos DB for MongoDB vCore** - Database connection with OIDC authentication + +## Quick Start + +**Run all verification tests with a single command:** + +```bash +npm run auth +``` + +This comprehensive test suite runs all four verification scripts below and provides a summary report. Use this as your first step to validate your Azure environment. + +## Prerequisites + +1. **Run infrastructure deployment:** + ```bash + azd provision + # or + azd up + ``` + +2. **Environment file:** Scripts require the `.env` file in the workspace root (created by `azd` post-provision hooks) + +3. **Authentication:** You must be logged into Azure CLI or have a managed identity configured: + ```bash + az login + ``` + +## Scripts + +### `test-auth.ts` (Recommended) +Comprehensive authentication test suite that runs all verification scripts below. + +**Validates:** +- All four Azure service connections in sequence +- Displays detailed configuration information +- Provides pass/fail summary report + +**Run:** +```bash +npm run auth +``` + +**What it tests:** +1. Azure OpenAI Embeddings API +2. Azure OpenAI Chat (Planner model) +3. Azure OpenAI Chat (Synthesizer model) +4. Azure Cosmos DB MongoDB connection + +--- + +### Individual Verification Scripts + +You can also run each test individually if you need to troubleshoot a specific service: + +### `embed.ts` +Tests Azure OpenAI Embeddings API with passwordless authentication. + +**Validates:** +- DefaultAzureCredential token acquisition +- Azure OpenAI instance connectivity +- Embedding model deployment accessibility +- Vector generation from sample text + +**Run:** +```bash +npm run embed +``` + +### `llm-planner.ts` +Tests Azure OpenAI Chat Completion API (Planner model) with passwordless authentication. + +**Validates:** +- Planner model deployment (gpt-4o-mini) +- Chat completion API functionality +- Token provider configuration + +**Run:** +```bash +npm run planner +``` + +### `llm-synth.ts` +Tests Azure OpenAI Chat Completion API (Synthesizer model) with passwordless authentication. + +**Validates:** +- Synthesizer model deployment (gpt-4o) +- Chat completion API functionality +- Response generation + +**Run:** +```bash +npm run synth +``` + +### `mongo.ts` +Tests MongoDB connection to Azure Cosmos DB with OIDC (passwordless) authentication. + +**Validates:** +- DefaultAzureCredential token acquisition for Document DB scope +- MongoDB OIDC authentication mechanism +- Database and collection listing +- Connection stability + +**Run:** +```bash +npm run mongo +``` + +## Expected Output + +Each script will: +- ✅ Connect to the Azure service using DefaultAzureCredential +- ✅ Display configuration details (instance names, deployments, database info) +- ✅ Execute a test operation (embed text, generate response, list databases) +- ✅ Exit cleanly + +## Troubleshooting + +### "Resource not found" errors +- Verify `azd provision` completed successfully +- Check that `.env` file exists in the root directory +- Confirm environment variables are set correctly + +### "Authentication failed" errors +- Ensure you're logged in: `az login` +- Verify your user account has appropriate role assignments: + - **Cognitive Services OpenAI User** (for Azure OpenAI) + - **DocumentDB Account Contributor** or custom role with `Microsoft.DocumentDB/mongoClusters/users/read` (for Cosmos DB) + +### "DefaultAzureCredential failed" errors +- Check that `AZURE_TENANT_ID` is set in `.env` +- Verify Azure CLI is installed and logged in +- Try refreshing credentials: `az account get-access-token` + +## Environment Variables Required + +These scripts expect the following variables in `../.env`: + +```bash +# Debug (optional) +DEBUG="false" # Set to "true" for verbose logging in embed.ts + +# Azure OpenAI +AZURE_OPENAI_API_INSTANCE_NAME="your-openai-instance" +AZURE_OPENAI_EMBEDDING_MODEL="text-embedding-3-small" +AZURE_OPENAI_EMBEDDING_API_VERSION="2023-05-15" +AZURE_OPENAI_PLANNER_DEPLOYMENT="gpt-4o-mini" +AZURE_OPENAI_PLANNER_API_VERSION="2024-08-01-preview" +AZURE_OPENAI_SYNTH_DEPLOYMENT="gpt-4o" +AZURE_OPENAI_SYNTH_API_VERSION="2024-08-01-preview" + +# Azure Cosmos DB for MongoDB vCore +MONGO_CLUSTER_NAME="your-mongo-cluster" + +# Azure Identity (automatically set by DefaultAzureCredential when using az login) +AZURE_TENANT_ID="your-tenant-id" +``` + +**Note:** `MONGO_DB_NAME` and `MONGO_DB_COLLECTION` are not required by these verification scripts but are needed for the main agent application. + +## Next Steps + +After running `npm run auth` successfully (all tests pass): + +1. Upload hotel data: `npm run upload` +2. Run the full agent application: `npm run start` +3. Test vector search queries with the two-agent pipeline + +**If tests fail:** Run individual scripts (`npm run embed`, `npm run planner`, etc.) to isolate the issue. diff --git a/mongo-vcore-agent-langchain/docs/architecture.mmd b/mongo-vcore-agent-langchain/docs/architecture.mmd new file mode 100644 index 0000000..407dad6 --- /dev/null +++ b/mongo-vcore-agent-langchain/docs/architecture.mmd @@ -0,0 +1,21 @@ +```mermaid +flowchart TD + A[User] -->|asks question| Planner(Planner Agent) + Planner -->|calls tool| Tool[getHotelsToMatchSearchQuery Tool] + Tool -->|returns plain text / JSON string| Extractor(Extractor) + Planner -->|may return AI message instead of tool call| Fallback{Planner didn't call tool} + Fallback -->|run programmatic vector search| Extractor + Extractor -->|hotelContext string| Synth(Synthesizer Agent) + Synth -->|final answer| A + + subgraph Clients + E[Embedding Client] + C[Cosmos DB (Mongo API) Vector Store] + Chat[Chat Client] + end + + Tool --> C + Tool --> E + Planner --> Chat + Synth --> Chat +``` diff --git a/mongo-vcore-agent-langchain/docs/azure-architecture.mmd b/mongo-vcore-agent-langchain/docs/azure-architecture.mmd new file mode 100644 index 0000000..ced9c73 --- /dev/null +++ b/mongo-vcore-agent-langchain/docs/azure-architecture.mmd @@ -0,0 +1,73 @@ +%% Azure Resources Architecture +%% This diagram shows the Azure infrastructure and resource relationships +%% Using Azure brand colors: #0078D4 (blue), #50E6FF (light blue), #00188F (dark blue) + +graph TB + subgraph Azure["Azure Cloud Platform"] + style Azure fill:#0078D4,stroke:#00188F,stroke-width:4px,color:#fff + + subgraph Identity["Identity & Access"] + style Identity fill:#50E6FF,stroke:#0078D4,stroke-width:2px,color:#000 + MI[("Managed Identity
(User-Assigned)")] + style MI fill:#fff,stroke:#0078D4,stroke-width:2px,color:#000 + end + + subgraph AI["Azure OpenAI Service"] + style AI fill:#50E6FF,stroke:#0078D4,stroke-width:2px,color:#000 + AOAI["Azure OpenAI
Instance"] + style AOAI fill:#fff,stroke:#0078D4,stroke-width:2px,color:#000 + + subgraph Models["Models"] + style Models fill:#E3F2FD,stroke:#0078D4,stroke-width:1px,color:#000 + Planner["gpt-4o-mini
(Planner)"] + Synth["gpt-4o
(Synthesizer)"] + Embed["text-embedding-3-small
(Embeddings)"] + style Planner fill:#fff,stroke:#0078D4,stroke-width:1px,color:#000 + style Synth fill:#fff,stroke:#0078D4,stroke-width:1px,color:#000 + style Embed fill:#fff,stroke:#0078D4,stroke-width:1px,color:#000 + end + end + + subgraph Database["Azure Cosmos DB"] + style Database fill:#50E6FF,stroke:#0078D4,stroke-width:2px,color:#000 + Mongo[("Cosmos DB for
MongoDB vCore
Cluster")] + style Mongo fill:#fff,stroke:#0078D4,stroke-width:2px,color:#000 + + subgraph Data["Data Structure"] + style Data fill:#E3F2FD,stroke:#0078D4,stroke-width:1px,color:#000 + DB[(Database:
hotels)] + Coll[(Collection:
hotel_data)] + Index["Vector Index
(IVF/HNSW/DiskANN)"] + style DB fill:#fff,stroke:#0078D4,stroke-width:1px,color:#000 + style Coll fill:#fff,stroke:#0078D4,stroke-width:1px,color:#000 + style Index fill:#fff,stroke:#0078D4,stroke-width:1px,color:#000 + end + end + + subgraph AppService["Compute & Hosting"] + style AppService fill:#50E6FF,stroke:#0078D4,stroke-width:2px,color:#000 + App["LangChain Agent
Application
(Node.js/TypeScript)"] + style App fill:#fff,stroke:#0078D4,stroke-width:2px,color:#000 + end + end + + %% Relationships + MI -.->|"Authenticates
(RBAC)"| AOAI + MI -.->|"Authenticates
(OIDC)"| Mongo + App -->|"Uses"| MI + App -->|"Generates
Embeddings"| Embed + App -->|"Plans Query"| Planner + App -->|"Synthesizes
Response"| Synth + App -->|"Vector Search"| Mongo + Mongo --> DB + DB --> Coll + Coll --> Index + + %% Legend + subgraph Legend["Legend"] + style Legend fill:#f5f5f5,stroke:#666,stroke-width:1px,color:#000 + L1["Solid Line: Direct Usage"] + L2["Dotted Line: Authentication"] + style L1 fill:#fff,stroke:#666,stroke-width:1px,color:#000 + style L2 fill:#fff,stroke:#666,stroke-width:1px,color:#000 + end diff --git a/mongo-vcore-agent-langchain/docs/mermaid-diagram-2025-12-13-071041.png b/mongo-vcore-agent-langchain/docs/mermaid-diagram-2025-12-13-071041.png new file mode 100644 index 0000000..41263b9 Binary files /dev/null and b/mongo-vcore-agent-langchain/docs/mermaid-diagram-2025-12-13-071041.png differ diff --git a/mongo-vcore-agent-langchain/docs/mermaid-diagram-2025-12-13-071525.png b/mongo-vcore-agent-langchain/docs/mermaid-diagram-2025-12-13-071525.png new file mode 100644 index 0000000..0739898 Binary files /dev/null and b/mongo-vcore-agent-langchain/docs/mermaid-diagram-2025-12-13-071525.png differ diff --git a/mongo-vcore-agent-langchain/docs/quickstart-agent-langchain.md b/mongo-vcore-agent-langchain/docs/quickstart-agent-langchain.md new file mode 100644 index 0000000..3a0e2eb --- /dev/null +++ b/mongo-vcore-agent-langchain/docs/quickstart-agent-langchain.md @@ -0,0 +1,738 @@ +--- +title: Quickstart - LangChain Agent with Vector Search in Node.js +description: Learn how to build an AI agent using LangChain with vector search in Azure DocumentDB with Node.js. Create intelligent hotel recommendation agents that use semantic search. +author: diberry +ms.author: diberry +ms.reviewer: khelan +ms.date: 11/21/2025 +ms.devlang: typescript +ms.topic: quickstart-sdk +ms.custom: + - devx-track-ts + - devx-track-ts-ai +# CustomerIntent: As a developer, I want to learn how to build AI agents with LangChain and vector search in Node.js applications with Azure DocumentDB. +--- + +# Quickstart: LangChain Agent with vector search in Azure DocumentDB + +Build an intelligent AI agent using LangChain and Azure DocumentDB (with MongoDB compatibility). This quickstart demonstrates a two-agent architecture that performs semantic hotel search and generates personalized recommendations. + +**Architecture:** +- **Planner Agent** (`gpt-4o-mini`): Refines queries and executes vector search using a custom LangChain tool +- **Synthesizer Agent** (`gpt-4o`): Analyzes search results and provides comparative recommendations + +The sample uses a hotel dataset with on-the-fly embedding generation via `text-embedding-3-small` and supports multiple vector index algorithms (IVF, HNSW, DiskANN). + +Find the [complete source code](https://github.com/Azure-Samples/cosmos-db-vector-samples/tree/main/mongo-vcore-agent-langchain) on GitHub. + +## Prerequisites + +### Azure resources + +- **Azure OpenAI resource** with the following model deployments in Microsoft Azure AI Foundry: + - `gpt-4o` deployment (Synthesizer Agent) + - Recommended: **50,000 tokens per minute (TPM)** capacity + - Pricing: ~$2.50 per 1M input tokens, ~$10.00 per 1M output tokens + - `gpt-4o-mini` deployment (Planner Agent) + - Recommended: **30,000 tokens per minute (TPM)** capacity + - Pricing: ~$0.15 per 1M input tokens, ~$0.60 per 1M output tokens + - `text-embedding-3-small` deployment (Embeddings) + - Recommended: **10,000 tokens per minute (TPM)** capacity + - Pricing: ~$0.02 per 1M tokens + - **Token quotas**: Configure sufficient TPM for each deployment to avoid rate limiting + - See [Manage Azure OpenAI quotas](https://learn.microsoft.com/azure/ai-services/openai/how-to/quota) for quota management + - If you encounter 429 errors, increase your TPM quota or reduce request frequency + + + + + + + +- **Azure DocumentDB (with MongoDB compatibility) cluster** with vector search support: + - **Cluster tier requirements** based on vector index algorithm: + - **IVF (Inverted File Index)**: M10 or higher (default algorithm) + - **HNSW (Hierarchical Navigable Small World)**: M30 or higher (graph-based) + - **DiskANN**: M30 or higher (optimized for large-scale) + - **Firewall configuration**: REQUIRED - Add your client IP address to the cluster's firewall rules + - Find your IP: `curl -4 ifconfig.me` + - Configure in Azure Portal: DocumentDB cluster → Networking → Firewall + - See [Configure firewall rules](https://learn.microsoft.com/azure/cosmos-db/mongodb/vcore/how-to-configure-firewall) for detailed instructions + - Without proper firewall configuration, connection attempts will fail + + + + + + + + +### Development tools + +- [Node.js LTS](https://nodejs.org/download/) +- [TypeScript](https://www.typescriptlang.org/download): Install TypeScript globally: + + ```bash + npm install -g typescript + ``` + +- [Azure CLI](https://learn.microsoft.com/cli/azure/install-azure-cli) for authentication + + + + +## Create a Node.js project + +1. Create a new directory for your project and open it in Visual Studio Code: + + ```bash + mkdir agent-vector-search + cd agent-vector-search + code . + ``` + +1. In the terminal, initialize a Node.js project: + + ```bash + npm init -y + npm pkg set type="module" + ``` + +1. Install the required packages: + + ```bash + npm install @langchain/azure-cosmosdb @langchain/openai @langchain/core langchain zod mongodb + ``` + + - `@langchain/azure-cosmosdb`: LangChain integration for DocumentDB + - `@langchain/openai`: LangChain integration for Azure OpenAI + - `@langchain/core`: Core LangChain functionality + - `langchain`: Main LangChain library with agent framework + - `zod`: Schema validation for tool parameters + - `mongodb`: MongoDB driver for database operations + +1. Install development dependencies: + + ```bash + npm install --save-dev @types/node typescript + ``` + +1. Create a `.env` file in your project root. You can copy the sample from the repository: + + ```bash + curl -o .env https://raw.githubusercontent.com/Azure-Samples/cosmos-db-vector-samples/main/mongo-vcore-agent-langchain/.env.sample + ``` + + **Important**: This quickstart uses a two-agent architecture (Planner + Synthesizer) with three model deployments (two chat models + embeddings). The standard LangChain environment variable pattern supports a single model, so this sample uses custom environment variables for each model deployment: + + - **Planner Agent**: `AZURE_OPENAI_PLANNER_DEPLOYMENT` and `AZURE_OPENAI_PLANNER_API_VERSION` + - **Synthesizer Agent**: `AZURE_OPENAI_SYNTH_DEPLOYMENT` and `AZURE_OPENAI_SYNTH_API_VERSION` + - **Embeddings**: `AZURE_OPENAI_EMBEDDING_DEPLOYMENT` and `AZURE_OPENAI_EMBEDDING_API_VERSION` + + + + Edit the `.env` file and replace these placeholder values: + - `AZURE_OPENAI_API_KEY`: Your Azure OpenAI API key + - `AZURE_OPENAI_API_INSTANCE_NAME`: Your Azure OpenAI resource name + - `AZURE_OPENAI_PLANNER_DEPLOYMENT`: Your gpt-4o-mini deployment name + - `AZURE_OPENAI_SYNTH_DEPLOYMENT`: Your gpt-4o deployment name + - `AZURE_OPENAI_EMBEDDING_DEPLOYMENT`: Your text-embedding-3-small deployment name + - `AZURE_DOCUMENTDB_MONGODB_CONNECTION_STRING`: Your Azure DocumentDB connection string + - `MONGO_CLUSTER_NAME`: Your DocumentDB cluster name + + + + + + +1. Add a `tsconfig.json` file to configure TypeScript: + + ```json + { + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "lib": ["ES2022"], + "moduleResolution": "node", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "allowSyntheticDefaultImports": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] + } + ``` + +1. Copy the `HotelsData_toCosmosDB.JSON` [raw data file](https://raw.githubusercontent.com/Azure-Samples/cosmos-db-vector-samples/refs/heads/main/data/HotelsData_toCosmosDB.JSON) to a `data` directory in the parent folder. + +## Create npm scripts + +Edit the `package.json` file and add these scripts: + +```json +"scripts": { + "build": "tsc", + "start": "npm run build && node --env-file .env dist/agent.js" +} +``` + +## Create the project structure + +Create the source directory structure: + +```bash +mkdir -p src/utils +``` + +Create these files: + +```bash +touch src/agent.ts +touch src/vector-store.ts +touch src/utils/prompts.ts +touch src/utils/clients.ts +touch src/utils/types.ts +touch src/utils/debug-handlers.ts +``` + +## Create utility files + +The sample uses utility files to organize configuration and shared functionality. You can find the complete implementation in the [GitHub repository](https://github.com/Azure-Samples/cosmos-db-vector-samples/tree/main/mongo-vcore-agent-langchain/src/utils): + +- **`src/utils/types.ts`**: TypeScript interfaces for Hotel data structures +- **`src/utils/clients.ts`**: Azure OpenAI client configuration for embeddings, planner, and synthesizer models. Supports both API key and passwordless authentication with Azure Identity. +- **`src/utils/prompts.ts`**: System prompts and tool descriptions for the two-agent architecture +- **`src/utils/debug-handlers.ts`**: Optional debug callbacks for development and troubleshooting + +## Create the vector store module + +The `src/vector-store.ts` file consolidates all vector database operations. This section shows the key components. See the [complete implementation on GitHub](https://github.com/Azure-Samples/cosmos-db-vector-samples/blob/main/mongo-vcore-agent-langchain/src/vector-store.ts). + +### Initialize the vector store + +The `getStore()` function performs the complete vector store setup: reads hotel documents from JSON, generates vector embeddings, creates the database and collection if they don't exist, inserts documents into DocumentDB, and creates the vector index for the selected algorithm: + +```typescript +export async function getStore( + dataFilePath: string, + embeddingClient: AzureOpenAIEmbeddings, + dbConfig: AzureCosmosDBMongoDBConfig +): Promise { + + const hotelsData: HotelsData = JSON.parse(readFileSync(dataFilePath, 'utf-8')); + + // Use destructuring to exclude unwanted properties + const documents = hotelsData.map(hotel => { + const { Description_fr, Location, Rooms, ...hotelData } = hotel; + + return new Document({ + pageContent: `Hotel: ${hotel.HotelName}\n\n${hotel.Description}`, + metadata: hotelData, + id: hotel.HotelId.toString() + }); + }); + + const store = await AzureCosmosDBMongoDBVectorStore.fromDocuments( + documents, + embeddingClient, + { + ...dbConfig, + indexOptions: getVectorIndexOptions(), + } + ); + + console.log(`Inserted ${documents.length} documents into DocumentDB vector store.`); + return store; +} +``` + +This code demonstrates: +- **Read documents**: Loads hotel data from JSON file using `readFileSync()` and parses into HotelsData array +- **Data transformation**: Uses TypeScript destructuring to exclude unnecessary fields (Description_fr, Location, Rooms) +- **Document creation**: Combines hotel name and description in pageContent for semantic search +- **Generate vectors**: `fromDocuments()` calls the embedding client to create vector embeddings for each document +- **Create database and collection**: Automatically creates the DocumentDB database and collection if they don't already exist +- **Insert documents**: Stores documents with their embeddings into DocumentDB collection +- **Create vector index**: Automatically creates the vector index based on the selected algorithm (IVF, HNSW, or DiskANN) via `getVectorIndexOptions()` + +### Configure vector index algorithms + +The `getVectorIndexOptions()` function supports three vector search algorithms with configurable parameters: + +```typescript +function getVectorIndexOptions() { + const algorithm = process.env.VECTOR_INDEX_ALGORITHM || 'vector-ivf'; + const dimensions = parseInt(process.env.EMBEDDING_DIMENSIONS || '1536'); + const similarity = getSimilarityType(process.env.VECTOR_SIMILARITY || 'COS'); + + const baseOptions = { dimensions, similarity }; + + switch (algorithm) { + case 'vector-hnsw': + return { + kind: 'vector-hnsw' as const, + m: parseInt(process.env.HNSW_M || '16'), + efConstruction: parseInt(process.env.HNSW_EF_CONSTRUCTION || '64'), + ...baseOptions + }; + case 'vector-diskann': + return { + kind: 'vector-diskann' as const, + ...baseOptions + }; + case 'vector-ivf': + default: + return { + numLists: parseInt(process.env.IVF_NUM_LISTS || '10'), + ...baseOptions + }; + } +} +``` + +Algorithm characteristics: +- **IVF (Inverted File Index)**: Default algorithm, balances speed and accuracy with configurable `numLists` parameter +- **HNSW (Hierarchical Navigable Small World)**: Graph-based algorithm with `m` (connections per node) and `efConstruction` (index build quality) parameters +- **DiskANN**: Microsoft Research algorithm optimized for large-scale vector search + +All algorithms support three similarity types: `COS` (cosine), `L2` (Euclidean distance), and `IP` (inner product). + +### Define the vector search tool + +The `getHotelsToMatchSearchQuery` tool enables the planner agent to execute semantic searches. This LangChain tool definition includes schema validation and vector search logic: + +```typescript +export const getHotelsToMatchSearchQuery = tool( + async ({ query, nearestNeighbors }, config): Promise => { + try { + const store = config.context.store as AzureCosmosDBMongoDBVectorStore; + const embeddingClient = config.context.embeddingClient as AzureOpenAIEmbeddings; + + // Create query embedding and perform search + const queryVector = await embeddingClient.embedQuery(query); + const results = await store.similaritySearchVectorWithScore(queryVector, nearestNeighbors); + console.log(`Found ${results.length} documents from vector store`); + + // Format results for synthesizer + const formatted = results.map(([doc, score]) => { + const md = doc.metadata as Partial; + console.log(`Hotel: ${md.HotelName ?? 'N/A'}, Score: ${score}`); + return formatHotelForSynthesizer(md, score); + }).join('\n\n'); + + return formatted; + } catch (error) { + console.error('Error in getHotelsToMatchSearchQuery tool:', error); + return 'Error occurred while searching for hotels.'; + } + }, + { + name: TOOL_NAME, + description: TOOL_DESCRIPTION, + schema: z.object({ + query: z.string(), + nearestNeighbors: z.number().optional().default(5), + }), + } +); +``` + +This tool implementation demonstrates: +- **Zod schema validation**: Ensures `query` is a string and `nearestNeighbors` is a number (defaults to 5) +- **Context injection**: Accesses the vector store and embedding client passed from the agent +- **Query embedding**: Converts natural language query to vector using the same embedding model +- **Vector similarity search**: Uses `similaritySearchVectorWithScore()` to find nearest neighbors with relevance scores + - Similarity scores range from 0 to 1 (0% to 100% similarity) + - A threshold of 0.7 means results with 70% or higher similarity are returned + - Higher scores indicate closer semantic matches to the query +- **Result formatting**: Structures hotel data and scores for the synthesizer agent to analyze + +## Create the Azure client connection code + +The `src/utils/clients.ts` file creates Azure OpenAI clients for embeddings, planner, and synthesizer models. Create `src/utils/clients.ts` or see the [complete implementation on GitHub](https://github.com/Azure-Samples/cosmos-db-vector-samples/blob/main/mongo-vcore-agent-langchain/src/utils/clients.ts): + +```typescript +import { AzureCosmosDBMongoDBVectorStore } from "@langchain/azure-cosmosdb"; + +export interface ClientConfig { + embeddingClient: AzureOpenAIEmbeddings; + plannerClient: AzureChatOpenAI; + synthClient: AzureChatOpenAI; + dbConfig: { + connectionString: string; + databaseName: string; + collectionName: string; + indexName: string; + embeddingKey: string; + textKey: string; + vectorSearchStrategy: string; + clusterName: string; + }; +} + +export function createClients(): ClientConfig { + const embeddingClient = new AzureOpenAIEmbeddings({ + azureOpenAIApiKey: process.env.AZURE_OPENAI_API_KEY, + azureOpenAIApiInstanceName: process.env.AZURE_OPENAI_API_INSTANCE_NAME, + azureOpenAIApiDeploymentName: process.env.AZURE_OPENAI_EMBEDDING_DEPLOYMENT, + azureOpenAIApiVersion: process.env.AZURE_OPENAI_EMBEDDING_API_VERSION, + }); + + const plannerClient = new AzureChatOpenAI({ + azureOpenAIApiKey: process.env.AZURE_OPENAI_API_KEY, + azureOpenAIApiInstanceName: process.env.AZURE_OPENAI_API_INSTANCE_NAME, + azureOpenAIApiDeploymentName: process.env.AZURE_OPENAI_PLANNER_DEPLOYMENT, + azureOpenAIApiVersion: process.env.AZURE_OPENAI_PLANNER_API_VERSION, + temperature: 0, + }); + + const synthClient = new AzureChatOpenAI({ + azureOpenAIApiKey: process.env.AZURE_OPENAI_API_KEY, + azureOpenAIApiInstanceName: process.env.AZURE_OPENAI_API_INSTANCE_NAME, + azureOpenAIApiDeploymentName: process.env.AZURE_OPENAI_SYNTH_DEPLOYMENT, + azureOpenAIApiVersion: process.env.AZURE_OPENAI_SYNTH_API_VERSION, + temperature: 0.3, + }); + + return { + embeddingClient, + plannerClient, + synthClient, + dbConfig: { + connectionString: process.env.AZURE_DOCUMENT_DB_CONNECTION_STRING!, + databaseName: process.env.MONGO_DB_NAME!, + collectionName: process.env.MONGO_DB_COLLECTION!, + indexName: process.env.MONGO_DB_INDEX_NAME!, + embeddingKey: process.env.EMBEDDED_FIELD!, + textKey: process.env.FIELD_TO_EMBED!, + vectorSearchStrategy: process.env.VECTOR_SEARCH_STRATEGY!, + clusterName: process.env.MONGO_CLUSTER_NAME!, + }, + }; +} + + + + +export function createClientsPasswordless(): ClientConfig { + // Passwordless authentication using Azure Identity + // Note: Requires RBAC roles configured on both Azure OpenAI and Cosmos DB + throw new Error("Passwordless authentication not yet implemented"); + /* Implementation would use: + - new DefaultAzureCredential() for Azure Identity + - azureADTokenProvider for Azure OpenAI clients + - MongoDB connection string with Azure AD authentication + - See complete implementation at: https://github.com/Azure-Samples/cosmos-db-vector-samples/blob/main/mongo-vcore-agent-langchain/src/utils/clients.ts + */ +} +``` + +## Create prompt templates + +Paste the following code into `src/utils/prompts.ts` or see the [complete implementation on GitHub](https://github.com/Azure-Samples/cosmos-db-vector-samples/blob/main/mongo-vcore-agent-langchain/src/utils/prompts.ts): + +```typescript +export const TOOL_NAME = "search_hotels_collection"; + +export const TOOL_DESCRIPTION = `REQUIRED TOOL - You MUST call this tool for EVERY hotel search request. This is the ONLY way to search the hotel database. + +Performs vector similarity search on the Hotels collection using Azure DocumentDB (with MongoDB compatibility). + +INPUT REQUIREMENTS: +- query (string, REQUIRED): Natural language search query describing desired hotel characteristics. Should be detailed and specific (e.g., "budget hotel near downtown with parking and wifi" not just "hotel"). +- nearestNeighbors (number, REQUIRED): Number of results to return (1-20). Use 3-5 for specific requests, 10-15 for broader searches. + +SEARCH BEHAVIOR: +- Uses semantic vector search to find hotels matching the query description +- Returns hotels ranked by similarity score +- Includes hotel details: name, description, category, tags, rating, location, parking info + +MANDATORY: Every user request about finding, searching, or recommending hotels REQUIRES calling this tool. Do not attempt to answer without calling this tool first.`; + +export const PLANNER_SYSTEM_PROMPT = `You are a hotel search planner. Transform the user's request into a clear, detailed search query for a vector database. + +CRITICAL REQUIREMENT: You MUST ALWAYS call the "search_hotels_collection" tool. This is MANDATORY for every request. + +Your response must be ONLY this JSON structure: +{"tool": "search_hotels_collection", "args": {"query": "", "nearestNeighbors": <1-20>}} + +QUERY REFINEMENT RULES: +- If vague (e.g., "nice hotel"), add specific attributes: "hotel with high ratings and good amenities" +- If minimal (e.g., "cheap"), expand: "budget hotel with good value" +- Preserve specific details from user (location, amenities, business/leisure) +- Keep natural language - this is for semantic search +- Don't just echo the input - improve it for better search results +- nearestNeighbors: Use 3-5 for specific requests, 10-15 for broader requests, max 20 + +EXAMPLES: +User: "cheap hotel" → {"tool": "search_hotels_collection", "args": {"query": "budget-friendly hotel with good value and affordable rates", "nearestNeighbors": 10}} +User: "hotel near downtown with parking" → {"tool": "search_hotels_collection", "args": {"query": "hotel near downtown with good parking and wifi", "nearestNeighbors": 5}} +User: "nice place to stay" → {"tool": "search_hotels_collection", "args": {"query": "hotel with high ratings, good reviews, and quality amenities", "nearestNeighbors": 10}} + +DO NOT return any other format. ALWAYS include the tool and args structure.`; + +export const SYNTHESIZER_SYSTEM_PROMPT = `You are an expert hotel recommendation assistant using vector search results. +Only use the TOP 3 results provided. Do not request additional searches or call other tools. + +GOAL: Provide a concise comparative recommendation to help the user choose between the top 3 options. + +REQUIREMENTS: +- Compare only the top 3 results across the most important attributes: rating, score, location, price-level (if available), and key tags (parking, wifi, pool). +- Identify the main tradeoffs in one short sentence per tradeoff. +- Give a single clear recommendation with one short justification sentence. +- Provide up to two alternative picks (one sentence each) explaining when they are preferable. + +FORMAT CONSTRAINTS: +- Plain text only (no markdown). +- Keep the entire response under 220 words. +- Use simple bullets (•) or numbered lists and short sentences (preferably <25 words per sentence). +- Preserve hotel names exactly as provided in the tool summary. + +Do not add extra commentary, marketing language, or follow-up questions. If information is missing and necessary to choose, state it in one sentence and still provide the best recommendation based on available data.`; + +export function createSynthesizerUserPrompt( + userQuery: string, + toolSummary: string +): string { + return `User asked: ${userQuery} + +Tool summary: +${toolSummary} + +Analyze the TOP 3 results by COMPARING them across all attributes (rating, score, tags, parking, location, category, rooms). + +Structure your response: +1. COMPARISON SUMMARY: Compare the top 3 options highlighting key differences and tradeoffs +2. BEST OVERALL: Recommend the single best option with clear reasoning +3. ALTERNATIVE PICKS: Briefly explain when the other options might be preferred (e.g., "Choose X if budget is priority" or "Choose Y if location matters most") + +Your goal is to help the user DECIDE between the options, not just describe them. + +Format your response using plain text (NO markdown formatting like ** or ###). Use simple numbered lists, bullet points (•), and use the exact hotel names from the tool summary (preserve original capitalization).`; +} +``` + +## Create debug handlers + +The `src/utils/debug-handlers.ts` file provides optional debug callbacks for development and troubleshooting. When `DEBUG=true` is set in your environment, these callbacks log LLM and tool calls. + +See the complete implementation in [`src/utils/debug-handlers.ts`](https://github.com/Azure-Samples/cosmos-db-vector-samples/blob/main/mongo-vcore-agent-langchain/src/utils/debug-handlers.ts) on GitHub. + +## Create the main agent code + +The main `src/agent.ts` file orchestrates the two-agent workflow. Create `src/agent.ts` or see the [complete implementation on GitHub](https://github.com/Azure-Samples/cosmos-db-vector-samples/blob/main/mongo-vcore-agent-langchain/src/agent.ts): + +```typescript +import { AzureCosmosDBMongoDBVectorStore } from "@langchain/azure-cosmosdb"; +import { TOOL_NAME, PLANNER_SYSTEM_PROMPT, SYNTHESIZER_SYSTEM_PROMPT, createSynthesizerUserPrompt } from './utils/prompts.js'; +import { z } from 'zod'; +import { createAgent } from "langchain"; +import { createClientsPasswordless, createClients } from './utils/clients.js'; +import { DEBUG_CALLBACKS } from './utils/debug-handlers.js'; +import { extractPlannerToolOutput, getStore, getHotelsToMatchSearchQuery, deleteCosmosMongoDatabase } from './vector-store.js'; + +// Authentication +const clients = + process.env.USE_PASSWORDLESS === "true" || process.env.USE_PASSWORDLESS === "1" + ? createClientsPasswordless() + : createClients(); +const { embeddingClient, plannerClient, synthClient, dbConfig } = clients; + +console.log(`DEBUG mode is ${process.env.DEBUG === 'true' ? 'ON' : 'OFF'}`); +console.log(`DEBUG_CALLBACKS length: ${DEBUG_CALLBACKS.length}`); + +// Planner agent uses Vector Search Tool +async function runPlannerAgent( + userQuery: string, + store: AzureCosmosDBMongoDBVectorStore, + nearestNeighbors = 5 +): Promise { + console.log("\n--- PLANNER ---"); + + const userMessage = `Call the "${TOOL_NAME}" tool with nearestNeighbors="${nearestNeighbors}" and query="${userQuery}". Respond ONLY with a tool response JSON output`; + + const contextSchema = z.object({ + store: z.any(), + embeddingClient: z.any(), + }); + + const agent = createAgent({ + model: plannerClient, + systemPrompt: PLANNER_SYSTEM_PROMPT, + tools: [getHotelsToMatchSearchQuery], + contextSchema, + }); + + const agentResult = await agent.invoke( + { messages: [{ role: "user", content: userMessage }] }, + // @ts-ignore + { context: { store, embeddingClient }, callbacks: DEBUG_CALLBACKS } + ); + + const plannerMessages = agentResult.messages || []; + const searchResultsAsText = extractPlannerToolOutput(plannerMessages); + + return searchResultsAsText; +} + +// Synthesizer agent function generates final user-friendly response +async function runSynthesizerAgent(userQuery: string, hotelContext: string): Promise { + console.log('\n--- SYNTHESIZER ---'); + + let conciseContext = hotelContext; + console.log(`Context size is ${conciseContext.length} characters`); + + const agent = createAgent({ + model: synthClient, + systemPrompt: SYNTHESIZER_SYSTEM_PROMPT, + }); + + const agentResult = await agent.invoke({ + messages: [{ + role: 'user', + content: createSynthesizerUserPrompt(userQuery, conciseContext) + }] + }); + const synthMessages = agentResult.messages; + const finalAnswer = synthMessages[synthMessages.length - 1].content; + return finalAnswer as string; +} + +// Get vector store (get docs, create embeddings, insert docs) +const store = await getStore( + process.env.DATA_FILE_WITHOUT_VECTORS!, + embeddingClient, + dbConfig); + +const query = process.env.QUERY || "quintessential lodging near running trails, eateries, retail"; +const nearestNeighbors = parseInt(process.env.NEAREST_NEIGHBORS || '5', 10); + +// Run planner agent +const hotelContext = await runPlannerAgent(query, store, nearestNeighbors); +if (process.env.DEBUG==='true') console.log(hotelContext); + +// Run synth agent +const finalAnswer = await runSynthesizerAgent(query, hotelContext); + +// // Get final recommendation (data + AI) +console.log('\n--- FINAL ANSWER ---'); +console.log(finalAnswer); + +// Clean up (delete database) +await store.close(); +await deleteCosmosMongoDatabase(); +``` + +This main module demonstrates: + +- **Authentication**: Supports both API key and passwordless authentication +- **Two-agent workflow**: Planner executes tool, Synthesizer analyzes results +- **Tool invocation**: Planner agent calls vector search with validated parameters +- **Context passing**: Search results flow from planner to synthesizer +- **Resource cleanup**: Closes connections and deletes test database +- Performs semantic similarity search and returns scored results +- Provides intelligent, context-aware hotel recommendations + +## Build and run the application + +Build the TypeScript files and run the agent: + +```bash +npm run build +npm run start +``` + +The app logging and output shows: + +- Data loading from the JSON file +- Vector store initialization with embeddings +- Planner agent execution with tool call +- Vector search results with similarity scores +- Synthesizer agent analysis +- Final comparative recommendation + +```output +Loaded 50 documents from ../data/HotelsData_toCosmosDB.JSON +Vector store initialized with 50 documents + +--- PLANNER --- +Found 5 documents from vector store +Extracted 5 results from tool output + +--- SYNTHESIZER --- +Output: 215 characters + +--- FINAL ANSWER --- +1. COMPARISON SUMMARY: +The top 3 hotels for lodging near trails, eateries, and retail are: +• Roach Motel (score: 0.8399) - highest match, near trails and restaurants +• Royal Cottage Resort (score: 0.8385) - close alternative with resort amenities +• Economy Universe Motel (score: 0.8360) - budget-friendly option + +2. BEST OVERALL: +Roach Motel is the best match with the highest similarity score and optimal location near running trails and dining. + +3. ALTERNATIVE PICKS: +• Choose Royal Cottage Resort if you prefer resort-style amenities +• Choose Economy Universe Motel if budget is the primary concern + +Closing database connection... +Database connection closed +Deleted database: Hotels +``` + +## View and manage data in Visual Studio Code + +1. Select the [DocumentDB extension](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-documentdb) in Visual Studio Code to connect to your Azure DocumentDB account. + +1. View the data and indexes in the Hotels database (before cleanup). + +## Clean up resources + +The application automatically deletes the test database after execution. Delete the resource group, DocumentDB account, and Azure OpenAI resource when you don't need them to avoid extra costs. + +## Common issues + +### Connection failures to DocumentDB + +If you receive connection timeout or authentication errors: + +1. **Verify firewall configuration**: Ensure your client IP is added to the DocumentDB cluster firewall rules + - Run `curl -4 ifconfig.me` to get your current IP address + - Add the IP in Azure Portal: DocumentDB cluster → Networking → Firewall + - See [Configure firewall rules](https://learn.microsoft.com/azure/cosmos-db/mongodb/vcore/how-to-configure-firewall) + +2. **Check connection string**: Verify your `AZURE_DOCUMENTDB_MONGODB_CONNECTION_STRING` is correct and includes authentication credentials + +### Rate limiting (429 errors) + +If you encounter "Rate limit exceeded" errors: + +1. **Increase token quotas**: Your deployment TPM may be insufficient + - Review [Manage Azure OpenAI quotas](https://learn.microsoft.com/azure/ai-services/openai/how-to/quota) + - Request quota increases in Azure Portal: Azure OpenAI resource → Quotas + +2. **Verify deployment capacity**: Ensure each model deployment has the recommended TPM: + - gpt-4o: 50,000 TPM + - gpt-4o-mini: 30,000 TPM + - text-embedding-3-small: 10,000 TPM + +### Vector index errors + +If vector index creation fails: + +1. **Verify cluster tier**: Ensure your DocumentDB cluster meets the minimum tier requirement for your chosen algorithm: + - IVF: M10+ + - HNSW: M30+ + - DiskANN: M30+ + +## Related content + +- [Vector store in Azure DocumentDB](vector-search.md) +- [LangChain Azure DocumentDB Integration](https://js.langchain.com/docs/integrations/vectorstores/azure_cosmosdb) +- [LangChain Agent Framework](https://js.langchain.com/docs/concepts/agents) diff --git a/mongo-vcore-agent-langchain/output-with-debug.txt b/mongo-vcore-agent-langchain/output-with-debug.txt new file mode 100644 index 0000000..933f624 --- /dev/null +++ b/mongo-vcore-agent-langchain/output-with-debug.txt @@ -0,0 +1,45 @@ +Debugger attached. +[clients] Env present: { + HAS_AZURE_OPENAI_API_KEY: true, + HAS_AZURE_OPENAI_INSTANCE: true, + HAS_EMBEDDING_DEPLOYMENT: true, + HAS_PLANNER_DEPLOYMENT: true, + HAS_SYNTH_DEPLOYMENT: true +} +Inserted 50 documents into Cosmos DB (Mongo API) vector store. + +--- PLANNER --- +[planner][LLM start] prompts= 1 + +[planner][LLM end] output keys= [ 'generations', 'llmOutput' ] +[planner][Tool Start] {"lc":1,"type":"not_implemented","id":["langchain","tools","DynamicStructuredTool"]} +Found 5 documents from vector store +Hotel: Trails End Motel, Score: 0.5055183466960066 +Hotel: White Mountain Lodge & Suites, Score: 0.48586606895125417 +Hotel: Country Comfort Inn, Score: 0.4850101453077966 +Hotel: Nordick's Valley Motel, Score: 0.4820755720138584 +Hotel: Countryside Hotel, Score: 0.46501458659985484 +[planner][Tool End] output summary= {"lc":1,"type":"constructor","id":["langchain_core","messages","ToolMessage"],"kwargs":{"status":"success","content":"--- HOTEL START ---\nHotelId: 40\nHotelName: Trails End Motel\nDescription: Only 8 +[planner][LLM start] prompts= 1 + +[planner][LLM end] output keys= [ 'generations', 'llmOutput' ] + +--- SYNTHESIZER --- +Context size is 3285 characters +Output: 1055 characters of final recommendation + +--- FINAL ANSWER --- +1. COMPARISON SUMMARY: +• Trails End Motel has the highest rating (3.2) and is closest to downtown Scottsdale, offering free wifi, parking, and a restaurant. It is budget-friendly but has an older renovation date (2017). +• White Mountain Lodge & Suites is a resort and spa with the lowest rating (2.4) but offers unique amenities like a pool, meditation gardens, and weekend entertainment. It is located in Denver's forest area. +• Country Comfort Inn has a moderate rating (2.5) and offers extended-stay amenities like living room suites and kitchens. It is situated in Bellevue, WA, and includes free wifi and parking. + +2. BEST OVERALL: +Trails End Motel is the best overall choice due to its higher rating, proximity to downtown Scottsdale, and essential amenities like free wifi and parking. + +3. ALTERNATIVE PICKS: +• Choose White Mountain Lodge & Suites if you prefer resort amenities and a forest setting for relaxation and unique experiences. +• Choose Country Comfort Inn if you need extended-stay facilities and a location near a lake in Bellevue, WA. + + +CLEAN UP \ No newline at end of file diff --git a/mongo-vcore-agent-langchain/output.txt b/mongo-vcore-agent-langchain/output.txt new file mode 100644 index 0000000..3c2cf70 --- /dev/null +++ b/mongo-vcore-agent-langchain/output.txt @@ -0,0 +1,39 @@ +--- PLANNER --- +Inserted 50 documents into Cosmos DB (Mongo API) vector store. + +--- PLANNER --- +Found 5 documents from vector store +Hotel: Trails End Motel, Score: 0.5073973234059103 +Hotel: Country Comfort Inn, Score: 0.4834342579167197 +Hotel: White Mountain Lodge & Suites, Score: 0.48180457841185287 +Hotel: Nordick's Valley Motel, Score: 0.47944122552872037 +Hotel: Countryside Hotel, Score: 0.4609394462316405 + +--- SYNTHESIZER --- +Context size is 3285 characters +Output: 1306 characters of final recommendation + +--- FINAL ANSWER --- +1. COMPARISON SUMMARY: +• Trails End Motel: Rating 3.2, Score 0.507397, located in Scottsdale, AZ, budget category, offers free wifi, restaurant, bar, and parking included. +• Country Comfort Inn: Rating 2.5, Score 0.483434, located in Bellevue, WA, extended-stay category, offers free wifi, laundry service, free parking, and 24-hour front desk service. +• White Mountain Lodge & Suites: Rating 2.4, Score 0.481805, located in Denver, CO, resort and spa category, offers continental breakfast, pool, restaurant, and parking included. + +Tradeoffs: +• Trails End Motel has the highest rating and score but is a budget option with fewer amenities. +• Country Comfort Inn offers extended-stay amenities like kitchens and living room suites but has the lowest rating. +• White Mountain Lodge & Suites offers unique resort amenities like hot stone massages and evening entertainment but has the lowest score and rating. + +2. BEST OVERALL: +Trails End Motel is the best overall due to its higher rating, score, and convenient amenities like free wifi, restaurant, and bar. + +3. ALTERNATIVE PICKS: +• Choose Country Comfort Inn if you need extended-stay amenities like kitchens and living room suites. +• Choose White Mountain Lodge & Suites if you prefer resort-style amenities and a unique experience in a forest setting. + + +CLEAN UP + + +Connected to Mongo endpoint. Dropping database: Hotels +Successfully dropped database "Hotels". \ No newline at end of file diff --git a/mongo-vcore-agent-langchain/package-lock.json b/mongo-vcore-agent-langchain/package-lock.json new file mode 100644 index 0000000..7948836 --- /dev/null +++ b/mongo-vcore-agent-langchain/package-lock.json @@ -0,0 +1,1403 @@ +{ + "name": "ts-cosmos-nodejs-vector-samples", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ts-cosmos-nodejs-vector-samples", + "version": "1.0.0", + "dependencies": { + "@langchain/azure-cosmosdb": "^1.0.0", + "@langchain/core": "^1.0.6", + "@langchain/openai": "^1.1.2", + "langchain": "^1.0.6", + "mongo": "^0.1.0", + "zod": "^4.1.12" + }, + "devDependencies": { + "@types/node": "^24.10.1", + "typescript": "^5.9.3" + } + }, + "node_modules/@azure-rest/core-client": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@azure-rest/core-client/-/core-client-2.5.1.tgz", + "integrity": "sha512-EHaOXW0RYDKS5CFffnixdyRPak5ytiCtU7uXDcP/uiY+A6jFRwNGzzJBiznkCzvi5EYpY+YWinieqHb0oY916A==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0", + "@azure/core-tracing": "^1.3.0", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-auth": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.10.1.tgz", + "integrity": "sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-util": "^1.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-client": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.10.1.tgz", + "integrity": "sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-http-compat": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@azure/core-http-compat/-/core-http-compat-2.3.1.tgz", + "integrity": "sha512-az9BkXND3/d5VgdRRQVkiJb2gOmDU8Qcq4GvjtBmDICNiQ9udFmDk4ZpSB5Qq1OmtDJGlQAfBaS4palFsazQ5g==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-client": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-lro": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.7.2.tgz", + "integrity": "sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-util": "^1.2.0", + "@azure/logger": "^1.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-paging": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.6.2.tgz", + "integrity": "sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-rest-pipeline": { + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.22.2.tgz", + "integrity": "sha512-MzHym+wOi8CLUlKCQu12de0nwcq9k9Kuv43j4Wa++CsCpJwps2eeBQwD2Bu8snkxTtDKDx4GwjuR9E8yC8LNrg==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-tracing": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.3.1.tgz", + "integrity": "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-util": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.13.1.tgz", + "integrity": "sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/cosmos": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-4.7.0.tgz", + "integrity": "sha512-a8OV7E41u/ZDaaaDAFdqTTiJ7c82jZc/+ot3XzNCIIilR25NBB+1ixzWQOAgP8SHRUIKfaUl6wAPdTuiG9I66A==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.9.0", + "@azure/core-rest-pipeline": "^1.19.1", + "@azure/core-tracing": "^1.2.0", + "@azure/core-util": "^1.11.0", + "@azure/keyvault-keys": "^4.9.0", + "@azure/logger": "^1.1.4", + "fast-json-stable-stringify": "^2.1.0", + "priorityqueuejs": "^2.0.0", + "semaphore": "^1.1.0", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/identity": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.13.0.tgz", + "integrity": "sha512-uWC0fssc+hs1TGGVkkghiaFkkS7NkTxfnCH+Hdg+yTehTpMcehpok4PgUKKdyCH+9ldu6FhiHRv84Ntqj1vVcw==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.9.0", + "@azure/core-client": "^1.9.2", + "@azure/core-rest-pipeline": "^1.17.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.11.0", + "@azure/logger": "^1.0.0", + "@azure/msal-browser": "^4.2.0", + "@azure/msal-node": "^3.5.0", + "open": "^10.1.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/keyvault-common": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@azure/keyvault-common/-/keyvault-common-2.0.0.tgz", + "integrity": "sha512-wRLVaroQtOqfg60cxkzUkGKrKMsCP6uYXAOomOIysSMyt1/YM0eUn9LqieAWM8DLcU4+07Fio2YGpPeqUbpP9w==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.3.0", + "@azure/core-client": "^1.5.0", + "@azure/core-rest-pipeline": "^1.8.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.10.0", + "@azure/logger": "^1.1.4", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/keyvault-keys": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@azure/keyvault-keys/-/keyvault-keys-4.10.0.tgz", + "integrity": "sha512-eDT7iXoBTRZ2n3fLiftuGJFD+yjkiB1GNqzU2KbY1TLYeXeSPVTVgn2eJ5vmRTZ11978jy2Kg2wI7xa9Tyr8ag==", + "license": "MIT", + "dependencies": { + "@azure-rest/core-client": "^2.3.3", + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.9.0", + "@azure/core-http-compat": "^2.2.0", + "@azure/core-lro": "^2.7.2", + "@azure/core-paging": "^1.6.2", + "@azure/core-rest-pipeline": "^1.19.0", + "@azure/core-tracing": "^1.2.0", + "@azure/core-util": "^1.11.0", + "@azure/keyvault-common": "^2.0.0", + "@azure/logger": "^1.1.4", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/logger": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.3.0.tgz", + "integrity": "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==", + "license": "MIT", + "dependencies": { + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/msal-browser": { + "version": "4.26.1", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-4.26.1.tgz", + "integrity": "sha512-GGCIsZXxyNm5QcQZ4maA9q+9UWmM+/87G+ybvPkrE32el1URSa9WYt0t67ks3/P0gspZX9RoEqyLqJ/X/JDnBQ==", + "license": "MIT", + "dependencies": { + "@azure/msal-common": "15.13.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-common": { + "version": "15.13.1", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.13.1.tgz", + "integrity": "sha512-vQYQcG4J43UWgo1lj7LcmdsGUKWYo28RfEvDQAEMmQIMjSFufvb+pS0FJ3KXmrPmnWlt1vHDl3oip6mIDUQ4uA==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-node": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-3.8.2.tgz", + "integrity": "sha512-dQrex2LiXwlCe9WuBHnCsY+xxLyuMXSd2SDEYJuhqB7cE8u6QafiC1xy8j8eBjGO30AsRi2M6amH0ZKk7vJpjA==", + "license": "MIT", + "dependencies": { + "@azure/msal-common": "15.13.1", + "jsonwebtoken": "^9.0.0", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@cfworker/json-schema": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@cfworker/json-schema/-/json-schema-4.1.1.tgz", + "integrity": "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==", + "license": "MIT" + }, + "node_modules/@langchain/azure-cosmosdb": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@langchain/azure-cosmosdb/-/azure-cosmosdb-1.0.0.tgz", + "integrity": "sha512-3n1FgbqdViA6XtB857Sm34xBy9axJN9HDP1sKumNAOLWelFOmYrRHcF2uQH+VJryrS+am4iF0LJRTVINEcGgSw==", + "license": "MIT", + "dependencies": { + "@azure/cosmos": "^4.2.0", + "@azure/identity": "^4.5.0", + "mongodb": "^6.17.0" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@langchain/core": "^1.0.0" + } + }, + "node_modules/@langchain/core": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@langchain/core/-/core-1.0.6.tgz", + "integrity": "sha512-rDSjXATujCdJlL+OJFfyZhEca8kLmqGr4W2ebJvSHiUgXEDqu/IOWC+ZWgoKKHkGOGFdVTqQ7Qi0j2RnYS9Qlg==", + "license": "MIT", + "dependencies": { + "@cfworker/json-schema": "^4.0.2", + "ansi-styles": "^5.0.0", + "camelcase": "6", + "decamelize": "1.2.0", + "js-tiktoken": "^1.0.12", + "langsmith": "^0.3.64", + "mustache": "^4.2.0", + "p-queue": "^6.6.2", + "p-retry": "4", + "uuid": "^10.0.0", + "zod": "^3.25.76 || ^4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@langchain/core/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@langchain/langgraph": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@langchain/langgraph/-/langgraph-1.0.2.tgz", + "integrity": "sha512-syxzzWTnmpCL+RhUEvalUeOXFoZy/KkzHa2Da2gKf18zsf9Dkbh3rfnRDrTyUGS1XSTejq07s4rg1qntdEDs2A==", + "license": "MIT", + "dependencies": { + "@langchain/langgraph-checkpoint": "^1.0.0", + "@langchain/langgraph-sdk": "~1.0.0", + "uuid": "^10.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@langchain/core": "^1.0.1", + "zod": "^3.25.32 || ^4.1.0", + "zod-to-json-schema": "^3.x" + }, + "peerDependenciesMeta": { + "zod-to-json-schema": { + "optional": true + } + } + }, + "node_modules/@langchain/langgraph-checkpoint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@langchain/langgraph-checkpoint/-/langgraph-checkpoint-1.0.0.tgz", + "integrity": "sha512-xrclBGvNCXDmi0Nz28t3vjpxSH6UYx6w5XAXSiiB1WEdc2xD2iY/a913I3x3a31XpInUW/GGfXXfePfaghV54A==", + "license": "MIT", + "dependencies": { + "uuid": "^10.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@langchain/core": "^1.0.1" + } + }, + "node_modules/@langchain/langgraph-checkpoint/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@langchain/langgraph-sdk": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@langchain/langgraph-sdk/-/langgraph-sdk-1.0.0.tgz", + "integrity": "sha512-g25ti2W7Dl5wUPlNK+0uIGbeNFqf98imhHlbdVVKTTkDYLhi/pI1KTgsSSkzkeLuBIfvt2b0q6anQwCs7XBlbw==", + "license": "MIT", + "dependencies": { + "p-queue": "^6.6.2", + "p-retry": "4", + "uuid": "^9.0.0" + }, + "peerDependencies": { + "@langchain/core": "^1.0.1", + "react": "^18 || ^19", + "react-dom": "^18 || ^19" + }, + "peerDependenciesMeta": { + "@langchain/core": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/@langchain/langgraph-sdk/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@langchain/langgraph/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@langchain/openai": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@langchain/openai/-/openai-1.1.2.tgz", + "integrity": "sha512-o642toyaRfx7Cej10jK6eK561gkIGTCQrN42fqAU9OhmTBkUflmRNKhqbcHj/RU+NOJfFM//hgwNU2gHespEkw==", + "license": "MIT", + "dependencies": { + "js-tiktoken": "^1.0.12", + "openai": "^6.9.0", + "zod": "^3.25.76 || ^4" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@langchain/core": "^1.0.0" + } + }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.3.2.tgz", + "integrity": "sha512-QgA5AySqB27cGTXBFmnpifAi7HxoGUeezwo6p9dI03MuDB6Pp33zgclqVb6oVK3j6I9Vesg0+oojW2XxB59SGg==", + "license": "MIT", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "node_modules/@types/node": { + "version": "24.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "license": "MIT" + }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "license": "MIT" + }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", + "license": "MIT" + }, + "node_modules/@types/whatwg-url": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", + "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", + "license": "MIT", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, + "node_modules/@typespec/ts-http-runtime": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.2.tgz", + "integrity": "sha512-IlqQ/Gv22xUC1r/WQm4StLkYQmaaTsXAhUVsNE0+xiyf0yRFiH5++q78U3bw6bLKDCTmh0uqKB9eG9+Bt75Dkg==", + "license": "MIT", + "dependencies": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bson": { + "version": "6.10.4", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.4.tgz", + "integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.20.1" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/console-table-printer": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/console-table-printer/-/console-table-printer-2.15.0.tgz", + "integrity": "sha512-SrhBq4hYVjLCkBVOWaTzceJalvn5K1Zq5aQA6wXC/cYjI3frKWNPEMK3sZsJfNNQApvCQmgBcc13ZKmFj8qExw==", + "license": "MIT", + "dependencies": { + "simple-wcswidth": "^1.1.2" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-browser": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.4.0.tgz", + "integrity": "sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg==", + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/js-tiktoken": { + "version": "1.0.21", + "resolved": "https://registry.npmjs.org/js-tiktoken/-/js-tiktoken-1.0.21.tgz", + "integrity": "sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.5.1" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/langchain": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/langchain/-/langchain-1.0.6.tgz", + "integrity": "sha512-JbxsTtWHiymV08H8jHISUj6dwbfB6/AaqsmJj0WKELkUCCCk64JfKEMmw56RSVjUkkvtwh+1Df+/+UpU7yRtlg==", + "license": "MIT", + "dependencies": { + "@langchain/langgraph": "^1.0.0", + "@langchain/langgraph-checkpoint": "^1.0.0", + "langsmith": "~0.3.74", + "uuid": "^10.0.0", + "zod": "^3.25.76 || ^4" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@langchain/core": "^1.0.5" + } + }, + "node_modules/langchain/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/langsmith": { + "version": "0.3.80", + "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.3.80.tgz", + "integrity": "sha512-BWpbB9/Hkx06S5X4nJE3W5Wm1mH/j6SIqWcM/WAuT+yulohE9knstIJGmBpmSBULb46nCj+cfjRkyF1Nrc4UmA==", + "license": "MIT", + "dependencies": { + "@types/uuid": "^10.0.0", + "chalk": "^4.1.2", + "console-table-printer": "^2.12.1", + "p-queue": "^6.6.2", + "p-retry": "4", + "semver": "^7.6.3", + "uuid": "^10.0.0" + }, + "peerDependencies": { + "@opentelemetry/api": "*", + "@opentelemetry/exporter-trace-otlp-proto": "*", + "@opentelemetry/sdk-trace-base": "*", + "openai": "*" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@opentelemetry/exporter-trace-otlp-proto": { + "optional": true + }, + "@opentelemetry/sdk-trace-base": { + "optional": true + }, + "openai": { + "optional": true + } + } + }, + "node_modules/langsmith/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "license": "MIT" + }, + "node_modules/mongo": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/mongo/-/mongo-0.1.0.tgz", + "integrity": "sha512-2MPq+GCNKhah0V/g/HIQI/S1h6Ycd87KPuXAITkeXWT6wncvABFxOaXdzCKlRvLSQRUkDimllrRrhoHUTD8usg==", + "dependencies": { + "mongodb": "*" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/mongodb": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.21.0.tgz", + "integrity": "sha512-URyb/VXMjJ4da46OeSXg+puO39XH9DeQpWCslifrRn9JWugy0D+DvvBvkm2WxmHe61O/H19JM66p1z7RHVkZ6A==", + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/saslprep": "^1.3.0", + "bson": "^6.10.4", + "mongodb-connection-string-url": "^3.0.2" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.3.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", + "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", + "license": "Apache-2.0", + "dependencies": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^14.1.0 || ^13.0.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "license": "MIT", + "bin": { + "mustache": "bin/mustache" + } + }, + "node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/openai": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.9.1.tgz", + "integrity": "sha512-vQ5Rlt0ZgB3/BNmTa7bIijYFhz3YBceAA3Z4JuoMSBftBF9YqFHIEhZakSs+O/Ad7EaoEimZvHxD5ylRjN11Lg==", + "license": "Apache-2.0", + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/p-queue": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", + "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.4", + "p-timeout": "^3.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", + "license": "MIT", + "dependencies": { + "p-finally": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/priorityqueuejs": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/priorityqueuejs/-/priorityqueuejs-2.0.0.tgz", + "integrity": "sha512-19BMarhgpq3x4ccvVi8k2QpJZcymo/iFUcrhPd4V96kYGovOdTsWwy7fxChYi4QY+m2EnGBWSX9Buakz+tWNQQ==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/semaphore": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/semaphore/-/semaphore-1.1.0.tgz", + "integrity": "sha512-O4OZEaNtkMd/K0i6js9SL+gqy0ZCBMgUvlSqHKi4IBdjhe7wB8pwztUk1BbZ1fmrvpwFrPbHzqd2w5pTcJH6LA==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-wcswidth": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/simple-wcswidth/-/simple-wcswidth-1.1.2.tgz", + "integrity": "sha512-j7piyCjAeTDSjzTSQ7DokZtMNwNlEAyxqSZeCS+CXH7fJ4jx3FuJ/mTW3mE+6JLs4VJBbcll0Kjn+KXI5t21Iw==", + "license": "MIT" + }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "license": "MIT", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", + "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/mongo-vcore-agent-langchain/package.json b/mongo-vcore-agent-langchain/package.json new file mode 100644 index 0000000..d9f1eae --- /dev/null +++ b/mongo-vcore-agent-langchain/package.json @@ -0,0 +1,26 @@ +{ + "name": "ts-cosmos-nodejs-vector-samples", + "version": "1.0.0", + "description": "Samples for MongoDB vCore vector search with Cosmos DB", + "main": "index.js", + "type": "module", + "scripts": { + "build": "tsc", + "auth": "npm run build && node --env-file ../.env dist/scripts/test-auth.js", + "upload": "npm run build && node --env-file ../.env dist/upload-documents.js", + "start": "npm run build && node --env-file ../.env dist/agent.js", + "cleanup": "npm run build && node --env-file ../.env dist/cleanup.js" + }, + "devDependencies": { + "@types/node": "^24.10.1", + "typescript": "^5.9.3" + }, + "dependencies": { + "@langchain/azure-cosmosdb": "^1.0.0", + "@langchain/core": "^1.0.6", + "@langchain/openai": "^1.1.2", + "langchain": "^1.0.6", + "mongo": "^0.1.0", + "zod": "^4.1.12" + } +} diff --git a/mongo-vcore-agent-langchain/src/agent.ts b/mongo-vcore-agent-langchain/src/agent.ts new file mode 100644 index 0000000..e7c24cf --- /dev/null +++ b/mongo-vcore-agent-langchain/src/agent.ts @@ -0,0 +1,101 @@ +import { + AzureCosmosDBMongoDBVectorStore +} from "@langchain/azure-cosmosdb"; +import { TOOL_NAME, PLANNER_SYSTEM_PROMPT, SYNTHESIZER_SYSTEM_PROMPT, createSynthesizerUserPrompt } from './utils/prompts.js'; +import { z } from 'zod'; +import { createAgent } from "langchain"; +import { createClientsPasswordless, createClients} from './utils/clients.js'; +import { DEBUG_CALLBACKS } from './utils/debug-handlers.js'; +import { extractPlannerToolOutput, getStore, getHotelsToMatchSearchQuery, getExistingStore } from './vector-store.js'; + +// Planner agent uses Vector Search Tool +async function runPlannerAgent( + plannerClient: any, + embeddingClient: any, + userQuery: string, + store: AzureCosmosDBMongoDBVectorStore, + nearestNeighbors = 5 +): Promise { + console.log('\n--- PLANNER ---'); + + const userMessage = `Call the "${TOOL_NAME}" tool with the desired number of neighbors: nearestNeighbors="${nearestNeighbors}" and the query: query="${userQuery}". Respond ONLY with a tool response JSON output`; + + const contextSchema = z.object({ + store: z.any(), + embeddingClient: z.any() + }); + + const agent = createAgent({ + model: plannerClient, + systemPrompt: PLANNER_SYSTEM_PROMPT, + tools: [getHotelsToMatchSearchQuery], + contextSchema, + }); + + const agentResult = await agent.invoke( + { messages: [{ role: 'user', content: userMessage }] }, + // @ts-ignore + { context: { store, embeddingClient }, callbacks: DEBUG_CALLBACKS } + ); + + const plannerMessages = agentResult.messages || []; + const searchResultsAsText = extractPlannerToolOutput(plannerMessages); + + return searchResultsAsText; +} + +// Synthesizer agent function generates final user-friendly response +async function runSynthesizerAgent(synthClient: any, userQuery: string, hotelContext: string): Promise { + console.log('\n--- SYNTHESIZER ---'); + + let conciseContext = hotelContext; + console.log(`Context size is ${conciseContext.length} characters`); + + const agent = createAgent({ + model: synthClient, + systemPrompt: SYNTHESIZER_SYSTEM_PROMPT, + }); + + const agentResult = await agent.invoke({ + messages: [{ + role: 'user', + content: createSynthesizerUserPrompt(userQuery, conciseContext) + }] + }); + const synthMessages = agentResult.messages; + const finalAnswer = synthMessages[synthMessages.length - 1].content; + console.log(`Output: ${finalAnswer.length} characters of final recommendation`); + return finalAnswer as string; +} + +try { + // Authentication + const clients = process.env.USE_PASSWORDLESS === 'true' || process.env.USE_PASSWORDLESS === '1' ? createClientsPasswordless() : createClients(); + const { embeddingClient, plannerClient, synthClient, dbConfig } = clients; + console.log(`DEBUG mode is ${process.env.DEBUG === 'true' ? 'ON' : 'OFF'}`); + console.log(`DEBUG_CALLBACKS length: ${DEBUG_CALLBACKS.length}`); + + + // Get vector store (get docs, create embeddings, insert docs) + const store = await getExistingStore( + embeddingClient, + dbConfig); + + const query = process.env.QUERY || "quintessential lodging near running trails, eateries, retail"; + const nearestNeighbors = parseInt(process.env.NEAREST_NEIGHBORS || '5', 10); + + //Run planner agent + const hotelContext = await runPlannerAgent(plannerClient, embeddingClient, query, store, nearestNeighbors); + if (process.env.DEBUG === 'true') console.log(hotelContext); + + //Run synth agent + const finalAnswer = await runSynthesizerAgent(synthClient, query, hotelContext); + // Get final recommendation (data + AI) + console.log('\n--- FINAL ANSWER ---'); + console.log(finalAnswer); + + process.exit(0); +} catch (error) { + console.error('Error running agent:', error); +} + diff --git a/mongo-vcore-agent-langchain/src/cleanup.ts b/mongo-vcore-agent-langchain/src/cleanup.ts new file mode 100644 index 0000000..e3b9062 --- /dev/null +++ b/mongo-vcore-agent-langchain/src/cleanup.ts @@ -0,0 +1,7 @@ +import { deleteCosmosMongoDatabase } from './utils/mongo.js'; + +// Run the cleanup +deleteCosmosMongoDatabase().catch((error) => { + console.error('Failed to delete database:', error); + process.exit(1); +}); diff --git a/mongo-vcore-agent-langchain/src/scripts/embed.ts b/mongo-vcore-agent-langchain/src/scripts/embed.ts new file mode 100644 index 0000000..1676665 --- /dev/null +++ b/mongo-vcore-agent-langchain/src/scripts/embed.ts @@ -0,0 +1,46 @@ +import { + DefaultAzureCredential, + getBearerTokenProvider, +} from "@azure/identity"; +import { AzureOpenAIEmbeddings } from "@langchain/openai"; + +export async function testEmbeddings() { + const credentials = new DefaultAzureCredential(); + const azureADTokenProvider = getBearerTokenProvider( + credentials, + "https://cognitiveservices.azure.com/.default", + ); + + if (process.env.DEBUG === "true") { + console.log("[embed] Environment variables:"); + console.log(` AZURE_OPENAI_API_INSTANCE_NAME: ${process.env.AZURE_OPENAI_API_INSTANCE_NAME}`); + console.log(` AZURE_OPENAI_EMBEDDING_MODEL: ${process.env.AZURE_OPENAI_EMBEDDING_MODEL}`); + console.log(` AZURE_OPENAI_EMBEDDING_API_VERSION: ${process.env.AZURE_OPENAI_EMBEDDING_API_VERSION}`); + console.log(` AZURE_OPENAI_API_KEY: ${process.env.AZURE_OPENAI_API_KEY ? '[SET]' : '[NOT SET]'}`); + } + + const modelWithManagedIdentity = new AzureOpenAIEmbeddings({ + azureADTokenProvider, + azureOpenAIApiInstanceName: process.env.AZURE_OPENAI_API_INSTANCE_NAME!, + azureOpenAIApiEmbeddingsDeploymentName: + process.env.AZURE_OPENAI_EMBEDDING_MODEL!, + azureOpenAIApiVersion: process.env.AZURE_OPENAI_EMBEDDING_API_VERSION!, + azureOpenAIBasePath: `https://${process.env.AZURE_OPENAI_API_INSTANCE_NAME}.openai.azure.com/openai/deployments`, + }); + + const vectors = await modelWithManagedIdentity.embedDocuments([ + "Hello world", + "Bonjour le monde", + ]); + console.log("Embeddings with Managed Identity:"); + console.log(vectors); + return vectors; +} + +// Run if executed directly +if (import.meta.url === `file://${process.argv[1]}`) { + testEmbeddings().catch((error) => { + console.error("Error using Managed Identity for embeddings:", error); + process.exit(1); + }); +} diff --git a/mongo-vcore-agent-langchain/src/scripts/llm-planner.ts b/mongo-vcore-agent-langchain/src/scripts/llm-planner.ts new file mode 100644 index 0000000..d520e23 --- /dev/null +++ b/mongo-vcore-agent-langchain/src/scripts/llm-planner.ts @@ -0,0 +1,34 @@ +import { + DefaultAzureCredential, + getBearerTokenProvider, +} from "@azure/identity"; +import { AzureChatOpenAI } from "@langchain/openai"; + +export async function testPlanner() { + const credentials = new DefaultAzureCredential(); + const azureADTokenProvider = getBearerTokenProvider( + credentials, + "https://cognitiveservices.azure.com/.default", + ); + + const llmWithManagedIdentity = new AzureChatOpenAI({ + azureADTokenProvider, + azureOpenAIApiInstanceName: process.env.AZURE_OPENAI_API_INSTANCE_NAME!, + azureOpenAIApiDeploymentName: + process.env.AZURE_OPENAI_PLANNER_DEPLOYMENT!, + azureOpenAIApiVersion: process.env.AZURE_OPENAI_PLANNER_API_VERSION!, + azureOpenAIBasePath: `https://${process.env.AZURE_OPENAI_API_INSTANCE_NAME}.openai.azure.com/openai/deployments`, + }); + + const response = await llmWithManagedIdentity.invoke("Hi there!"); + console.log(response); + return response; +} + +// Run if executed directly +if (import.meta.url === `file://${process.argv[1]}`) { + testPlanner().catch((error) => { + console.error("Error:", error); + process.exit(1); + }); +} diff --git a/mongo-vcore-agent-langchain/src/scripts/llm-synth.ts b/mongo-vcore-agent-langchain/src/scripts/llm-synth.ts new file mode 100644 index 0000000..a7e08ff --- /dev/null +++ b/mongo-vcore-agent-langchain/src/scripts/llm-synth.ts @@ -0,0 +1,34 @@ +import { + DefaultAzureCredential, + getBearerTokenProvider, +} from "@azure/identity"; +import { AzureChatOpenAI } from "@langchain/openai"; + +export async function testSynth() { + const credentials = new DefaultAzureCredential(); + const azureADTokenProvider = getBearerTokenProvider( + credentials, + "https://cognitiveservices.azure.com/.default", + ); + + const llmWithManagedIdentity = new AzureChatOpenAI({ + azureADTokenProvider, + azureOpenAIApiInstanceName: process.env.AZURE_OPENAI_API_INSTANCE_NAME!, + azureOpenAIApiDeploymentName: + process.env.AZURE_OPENAI_SYNTH_DEPLOYMENT!, + azureOpenAIApiVersion: process.env.AZURE_OPENAI_SYNTH_API_VERSION!, + azureOpenAIBasePath: `https://${process.env.AZURE_OPENAI_API_INSTANCE_NAME}.openai.azure.com/openai/deployments`, + }); + + const response = await llmWithManagedIdentity.invoke("Hi there!"); + console.log(response); + return response; +} + +// Run if executed directly +if (import.meta.url === `file://${process.argv[1]}`) { + testSynth().catch((error) => { + console.error("Error:", error); + process.exit(1); + }); +} diff --git a/mongo-vcore-agent-langchain/src/scripts/mongo.ts b/mongo-vcore-agent-langchain/src/scripts/mongo.ts new file mode 100644 index 0000000..a24d8e1 --- /dev/null +++ b/mongo-vcore-agent-langchain/src/scripts/mongo.ts @@ -0,0 +1,74 @@ +import { MongoClient, OIDCCallbackParams } from 'mongodb'; +import { AccessToken, DefaultAzureCredential, TokenCredential } from '@azure/identity'; + +const DOCUMENT_DB_SCOPE = 'https://ossrdbms-aad.database.windows.net/.default'; +const credential = new DefaultAzureCredential(); + +// OIDC token callback for MongoDB authentication +async function azureIdentityTokenCallback( + params: OIDCCallbackParams, + tokenCredential: TokenCredential +): Promise<{ accessToken: string; expiresInSeconds: number }> { + const tokenResponse: AccessToken | null = await tokenCredential.getToken([DOCUMENT_DB_SCOPE]); + return { + accessToken: tokenResponse?.token || '', + expiresInSeconds: (tokenResponse?.expiresOnTimestamp || 0) - Math.floor(Date.now() / 1000) + }; +} + +async function testMongoConnection() { + const clusterName = process.env.MONGO_CLUSTER_NAME; + + if (!clusterName) { + throw new Error('MONGO_CLUSTER_NAME is not set in environment'); + } + + console.log('Connecting to MongoDB cluster:', clusterName); + console.log('Using DefaultAzureCredential for authentication...\n'); + + const connectionString = `mongodb+srv://${clusterName}.global.mongocluster.cosmos.azure.com/`; + + const client = new MongoClient(connectionString, { + connectTimeoutMS: 30000, + tls: true, + retryWrites: true, + authMechanism: 'MONGODB-OIDC', + authMechanismProperties: { + OIDC_CALLBACK: (params: OIDCCallbackParams) => azureIdentityTokenCallback(params, credential), + ALLOWED_HOSTS: ['*.azure.com'] + } + }); + + try { + // Connect to the cluster + await client.connect(); + console.log('✅ Successfully connected to MongoDB!\n'); + + // List all databases + const adminDb = client.db().admin(); + const databasesList = await adminDb.listDatabases(); + + console.log('📚 Databases:'); + for (const db of databasesList.databases) { + console.log(` - ${db.name} `); + } + console.log(); + + } catch (error) { + console.error('❌ Connection failed:', error); + throw error; + } finally { + await client.close(); + console.log('\n✅ Connection closed'); + } +} + +// Run if executed directly +if (import.meta.url === `file://${process.argv[1]}`) { + testMongoConnection().catch((error) => { + console.error(error); + process.exit(1); + }); +} + +export { testMongoConnection }; diff --git a/mongo-vcore-agent-langchain/src/scripts/test-auth.ts b/mongo-vcore-agent-langchain/src/scripts/test-auth.ts new file mode 100644 index 0000000..0a17a11 --- /dev/null +++ b/mongo-vcore-agent-langchain/src/scripts/test-auth.ts @@ -0,0 +1,118 @@ +import { testEmbeddings } from "./embed.js"; +import { testPlanner } from "./llm-planner.js"; +import { testSynth } from "./llm-synth.js"; +import { testMongoConnection } from "./mongo.js"; + +/** + * Comprehensive authentication test suite for Azure services. + * Imports and runs the individual test functions from other scripts. + */ + +let testsPassed = 0; +let testsFailed = 0; + +function logTest(name: string, status: 'PASS' | 'FAIL', details?: string) { + const icon = status === 'PASS' ? '✅' : '❌'; + console.log(`${icon} ${name}`); + if (details) { + console.log(` ${details}`); + } + if (status === 'PASS') { + testsPassed++; + } else { + testsFailed++; + } +} + +async function runEmbeddingsTest() { + console.log('\n📝 Testing Azure OpenAI Embeddings...'); + try { + await testEmbeddings(); + logTest('Embeddings API', 'PASS'); + return true; + } catch (error: any) { + logTest('Embeddings API', 'FAIL', error?.message || String(error)); + return false; + } +} + +async function runPlannerTest() { + console.log('\n🤖 Testing Azure OpenAI Chat (Planner)...'); + try { + await testPlanner(); + logTest('Planner LLM', 'PASS', `Model: ${process.env.AZURE_OPENAI_PLANNER_DEPLOYMENT}`); + return true; + } catch (error: any) { + logTest('Planner LLM', 'FAIL', error?.message || String(error)); + return false; + } +} + +async function runSynthTest() { + console.log('\n💬 Testing Azure OpenAI Chat (Synthesizer)...'); + try { + await testSynth(); + logTest('Synthesizer LLM', 'PASS', `Model: ${process.env.AZURE_OPENAI_SYNTH_DEPLOYMENT}`); + return true; + } catch (error: any) { + logTest('Synthesizer LLM', 'FAIL', error?.message || String(error)); + return false; + } +} + +async function runMongoTest() { + console.log('\n🗄️ Testing Azure Cosmos DB for MongoDB vCore...'); + try { + await testMongoConnection(); + logTest('MongoDB Connection', 'PASS'); + return true; + } catch (error: any) { + logTest('MongoDB Connection', 'FAIL', error?.message || String(error)); + return false; + } +} + +async function runAllTests() { + console.log('═══════════════════════════════════════════════════════'); + console.log('🔐 Azure Passwordless Authentication Test Suite'); + console.log('═══════════════════════════════════════════════════════'); + + console.log('\n📋 Configuration:'); + console.log(` OpenAI Instance: ${process.env.AZURE_OPENAI_API_INSTANCE_NAME || 'NOT SET'}`); + console.log(` Embedding Model: ${process.env.AZURE_OPENAI_EMBEDDING_MODEL || 'NOT SET'}`); + console.log(` Planner Model: ${process.env.AZURE_OPENAI_PLANNER_DEPLOYMENT || 'NOT SET'}`); + console.log(` Synth Model: ${process.env.AZURE_OPENAI_SYNTH_DEPLOYMENT || 'NOT SET'}`); + console.log(` MongoDB Cluster: ${process.env.MONGO_CLUSTER_NAME || 'NOT SET'}`); + + // Run all tests + await runEmbeddingsTest(); + await runPlannerTest(); + await runSynthTest(); + await runMongoTest(); + + // Summary + console.log('\n═══════════════════════════════════════════════════════'); + console.log('📊 Test Summary'); + console.log('═══════════════════════════════════════════════════════'); + console.log(`✅ Passed: ${testsPassed}`); + console.log(`❌ Failed: ${testsFailed}`); + console.log(`📈 Total: ${testsPassed + testsFailed}`); + + if (testsFailed === 0) { + console.log('\n🎉 All authentication tests passed! Your environment is ready.'); + console.log(' You can now run: npm run start'); + process.exit(0); + } else { + console.log('\n⚠️ Some tests failed. Please check your Azure configuration.'); + console.log(' - Verify you are logged in: az login'); + console.log(' - Check environment variables in .env'); + console.log(' - Ensure role assignments are configured'); + process.exit(1); + } +} + +// Run the test suite +runAllTests().catch((error) => { + console.error('\n💥 Test suite failed:', error); + process.exit(1); +}); diff --git a/mongo-vcore-agent-langchain/src/upload-documents.ts b/mongo-vcore-agent-langchain/src/upload-documents.ts new file mode 100644 index 0000000..756f6dd --- /dev/null +++ b/mongo-vcore-agent-langchain/src/upload-documents.ts @@ -0,0 +1,64 @@ +import { createClientsPasswordless, createClients } from './utils/clients.js'; +import { getStore } from './vector-store.js'; + +/** + * Upload documents to Azure Cosmos DB MongoDB Vector Store + */ + +async function uploadDocuments() { + try { + console.log('Starting document upload...\n'); + + // Get clients based on authentication mode + const usePasswordless = process.env.USE_PASSWORDLESS === 'true' || process.env.USE_PASSWORDLESS === '1'; + console.log(`Authentication mode: ${usePasswordless ? 'Passwordless (Azure AD)' : 'API Key'}`); + + console.log('\nEnvironment variables check:'); + console.log(` DATA_FILE_WITHOUT_VECTORS: ${process.env.DATA_FILE_WITHOUT_VECTORS}`); + console.log(` MONGO_DB_NAME: ${process.env.MONGO_DB_NAME}`); + console.log(` MONGO_DB_COLLECTION: ${process.env.MONGO_DB_COLLECTION}`); + console.log(` MONGO_CLUSTER_NAME: ${process.env.MONGO_CLUSTER_NAME}`); + console.log(` AZURE_OPENAI_EMBEDDING_DEPLOYMENT: ${process.env.AZURE_OPENAI_EMBEDDING_DEPLOYMENT}`); + + const clients = usePasswordless ? createClientsPasswordless() : createClients(); + const { embeddingClient, dbConfig } = clients; + + console.log('\ndbConfig properties:'); + console.log(` instance: ${dbConfig.instance}`); + console.log(` databaseName: ${dbConfig.databaseName}`); + console.log(` collectionName: ${dbConfig.collectionName}`); + + // Check for data file path + const dataFilePath = process.env.DATA_FILE_WITHOUT_VECTORS; + if (!dataFilePath) { + throw new Error('DATA_FILE_WITHOUT_VECTORS environment variable is required'); + } + + console.log(`\nReading data from: ${dataFilePath}`); + console.log(`Database: ${dbConfig.databaseName}`); + console.log(`Collection: ${dbConfig.collectionName}`); + console.log(`Vector algorithm: ${process.env.VECTOR_INDEX_ALGORITHM || 'vector-ivf'}\n`); + + // Upload documents using existing getStore function + const startTime = Date.now(); + const store = await getStore(dataFilePath, embeddingClient, dbConfig); + const duration = ((Date.now() - startTime) / 1000).toFixed(2); + + console.log(`\n✓ Upload completed in ${duration} seconds`); + + // Close connection + await store.close(); + console.log('✓ Connection closed'); + + // Force exit to ensure process terminates (Azure credential timers may still be active) + process.exit(0); + + } catch (error: any) { + console.error('\n✗ Upload failed:', error?.message || error); + console.error('\nFull error:', error); + process.exit(1); + } +} + +// Run the upload +uploadDocuments(); \ No newline at end of file diff --git a/mongo-vcore-agent-langchain/src/utils/clients.ts b/mongo-vcore-agent-langchain/src/utils/clients.ts new file mode 100644 index 0000000..993d4c4 --- /dev/null +++ b/mongo-vcore-agent-langchain/src/utils/clients.ts @@ -0,0 +1,182 @@ +import { AzureOpenAIEmbeddings, AzureChatOpenAI } from "@langchain/openai"; +import { MongoClient, OIDCCallbackParams } from 'mongodb'; +import { AccessToken, DefaultAzureCredential, TokenCredential, getBearerTokenProvider } from '@azure/identity'; + +/* +This file contains utility functions to create Azure OpenAI clients for embeddings, planning, and synthesis. + +It supports two modes of authentication: +1. API Key based authentication using AZURE_OPENAI_API_KEY and AZURE_OPENAI_API_INSTANCE_NAME environment variables. +2. Passwordless authentication using DefaultAzureCredential from Azure Identity library. +*/ + +// Azure Identity configuration +const OPENAI_SCOPE = 'https://cognitiveservices.azure.com/.default'; +const DOCUMENT_DB_SCOPE = 'https://ossrdbms-aad.database.windows.net/.default'; + +// Azure identity credential (used for passwordless auth) +const CREDENTIAL = new DefaultAzureCredential(); + +// Token callback for MongoDB OIDC authentication +async function azureIdentityTokenCallback( + params: OIDCCallbackParams, + credential: TokenCredential +): Promise<{ accessToken: string; expiresInSeconds: number }> { + const tokenResponse: AccessToken | null = await credential.getToken([DOCUMENT_DB_SCOPE]); + return { + accessToken: tokenResponse?.token || '', + expiresInSeconds: (tokenResponse?.expiresOnTimestamp || 0) - Math.floor(Date.now() / 1000) + }; +} + + +// Debug logging +const DEBUG = process.env.DEBUG === 'true' || process.env.DEBUG === '1'; +if (DEBUG) { + const usePasswordless = process.env.USE_PASSWORDLESS === 'true' || process.env.USE_PASSWORDLESS === '1'; + if (usePasswordless) { + console.log('[clients] Passwordless mode enabled. Passwordless env presence:', { + HAS_AZURE_CLIENT_ID: !!process.env.AZURE_CLIENT_ID, + HAS_AZURE_TENANT_ID: !!process.env.AZURE_TENANT_ID, + HAS_AZURE_CLIENT_SECRET: !!process.env.AZURE_CLIENT_SECRET, + HAS_AZURE_OPENAI_INSTANCE: !!process.env.AZURE_OPENAI_API_INSTANCE_NAME, + HAS_EMBEDDING_DEPLOYMENT: !!process.env.AZURE_OPENAI_EMBEDDING_DEPLOYMENT, + HAS_PLANNER_DEPLOYMENT: !!process.env.AZURE_OPENAI_PLANNER_DEPLOYMENT, + HAS_SYNTH_DEPLOYMENT: !!process.env.AZURE_OPENAI_SYNTH_DEPLOYMENT + }); + } else { + console.log('[clients] Password Env present:', { + HAS_AZURE_OPENAI_API_KEY: !!process.env.AZURE_OPENAI_API_KEY, + HAS_AZURE_OPENAI_INSTANCE: !!process.env.AZURE_OPENAI_API_INSTANCE_NAME, + HAS_EMBEDDING_DEPLOYMENT: !!process.env.AZURE_OPENAI_EMBEDDING_DEPLOYMENT, + HAS_PLANNER_DEPLOYMENT: !!process.env.AZURE_OPENAI_PLANNER_DEPLOYMENT, + HAS_SYNTH_DEPLOYMENT: !!process.env.AZURE_OPENAI_SYNTH_DEPLOYMENT, + HAS_CONNECTION_STRING: !!process.env.AZURE_DOCUMENTDB_CONNECTION_STRING, + }); + } +} + +// Create clients with API key authentication +export function createClients() { + try { + const key = process.env.AZURE_OPENAI_API_KEY; + const instance = process.env.AZURE_OPENAI_API_INSTANCE_NAME; + if (!key || !instance) { + throw new Error('Missing keys: AZURE_OPENAI_API_KEY or AZURE_OPENAI_API_INSTANCE_NAME'); + } + + const auth = { + azureOpenAIApiKey: key, + azureOpenAIApiInstanceName: instance + }; + + const embeddingClient = new AzureOpenAIEmbeddings({ + ...auth, + azureOpenAIApiEmbeddingsDeploymentName: process.env.AZURE_OPENAI_EMBEDDING_DEPLOYMENT, + azureOpenAIApiVersion: process.env.AZURE_OPENAI_EMBEDDING_API_VERSION, + maxRetries: 1, + }); + + const plannerClient = new AzureChatOpenAI({ + ...auth, + model: process.env.AZURE_OPENAI_PLANNER_DEPLOYMENT!, + temperature: 0, // Deterministic for consistent query refinement + azureOpenAIApiDeploymentName: process.env.AZURE_OPENAI_PLANNER_DEPLOYMENT, + azureOpenAIApiVersion: process.env.AZURE_OPENAI_PLANNER_API_VERSION, + }); + + const synthClient = new AzureChatOpenAI({ + ...auth, + model: process.env.AZURE_OPENAI_SYNTH_DEPLOYMENT!, + temperature: 0.3, // Slightly creative for natural responses + azureOpenAIApiDeploymentName: process.env.AZURE_OPENAI_SYNTH_DEPLOYMENT, + azureOpenAIApiVersion: process.env.AZURE_OPENAI_SYNTH_API_VERSION, + }); + + const dbConfig = { + instance: process.env.AZURE_DOCUMENTDB_INSTANCE!, + connectionString: process.env.AZURE_DOCUMENTDB_CONNECTION_STRING!, + databaseName: process.env.MONGO_DB_NAME!, + collectionName: process.env.MONGO_DB_COLLECTION! + }; + + return { embeddingClient, plannerClient, synthClient, dbConfig }; + } catch (err: any) { + console.error('[clients] Failed to construct OpenAI clients:', err?.message ?? err); + console.error('[clients] Confirm AZURE_OPENAI_* env vars are set correctly (or configure passwordless token provider).'); + throw err; + } +} + + +// Create clients with passwordless authentication +export function createClientsPasswordless() { + try { + const instance = process.env.AZURE_OPENAI_API_INSTANCE_NAME; + if (!instance) { + throw new Error('Missing passwordless: AZURE_OPENAI_API_INSTANCE_NAME for passwordless client'); + } + + const embeddingClient = new AzureOpenAIEmbeddings({ + azureADTokenProvider: getBearerTokenProvider( + CREDENTIAL, + "https://cognitiveservices.azure.com/.default", + ), + azureOpenAIApiInstanceName: process.env.AZURE_OPENAI_API_INSTANCE_NAME!, + azureOpenAIApiEmbeddingsDeploymentName: + process.env.AZURE_OPENAI_EMBEDDING_MODEL!, + azureOpenAIApiVersion: process.env.AZURE_OPENAI_EMBEDDING_API_VERSION!, + azureOpenAIBasePath: `https://${process.env.AZURE_OPENAI_API_INSTANCE_NAME}.openai.azure.com/openai/deployments` + }); + + const plannerClient = new AzureChatOpenAI({ + azureADTokenProvider: getBearerTokenProvider( + CREDENTIAL, + "https://cognitiveservices.azure.com/.default", + ), + azureOpenAIApiInstanceName: process.env.AZURE_OPENAI_API_INSTANCE_NAME!, + azureOpenAIApiDeploymentName: + process.env.AZURE_OPENAI_PLANNER_DEPLOYMENT!, + azureOpenAIApiVersion: process.env.AZURE_OPENAI_PLANNER_API_VERSION!, + azureOpenAIBasePath: `https://${process.env.AZURE_OPENAI_API_INSTANCE_NAME}.openai.azure.com/openai/deployments`, + }); + + const synthClient = new AzureChatOpenAI({ + azureADTokenProvider: getBearerTokenProvider( + CREDENTIAL, + "https://cognitiveservices.azure.com/.default", + ), + azureOpenAIApiInstanceName: process.env.AZURE_OPENAI_API_INSTANCE_NAME!, + azureOpenAIApiDeploymentName: + process.env.AZURE_OPENAI_SYNTH_DEPLOYMENT!, + azureOpenAIApiVersion: process.env.AZURE_OPENAI_SYNTH_API_VERSION!, + azureOpenAIBasePath: `https://${process.env.AZURE_OPENAI_API_INSTANCE_NAME}.openai.azure.com/openai/deployments`, + }); + + const mongoClient = new MongoClient( + `mongodb+srv://${process.env.AZURE_DOCUMENTDB_INSTANCE!}.global.mongocluster.cosmos.azure.com/`, + { + connectTimeoutMS: 30000, + tls: true, + retryWrites: true, + authMechanism: 'MONGODB-OIDC', + authMechanismProperties: { + OIDC_CALLBACK: (params: OIDCCallbackParams) => azureIdentityTokenCallback(params, CREDENTIAL), + ALLOWED_HOSTS: ['*.azure.com'] + } + } + ); + + const dbConfig = { + instance: process.env.AZURE_DOCUMENTDB_INSTANCE!, + client: mongoClient, + databaseName: process.env.MONGO_DB_NAME!, + collectionName: process.env.MONGO_DB_COLLECTION!, + }; + + return { embeddingClient, plannerClient, synthClient, dbConfig }; + } catch (err: any) { + console.error('[clients] Failed to construct passwordless OpenAI clients:', err?.message ?? err); + throw err; + } +} \ No newline at end of file diff --git a/mongo-vcore-agent-langchain/src/utils/debug-handlers.ts b/mongo-vcore-agent-langchain/src/utils/debug-handlers.ts new file mode 100644 index 0000000..6c1b004 --- /dev/null +++ b/mongo-vcore-agent-langchain/src/utils/debug-handlers.ts @@ -0,0 +1,56 @@ +// Diagnostic callbacks array to log agent decisions and tool usage + + +const agentCallbacks = [ + { + handleLLMStart: async (_llm, prompts) => { + console.log('[planner][LLM start] prompts=', Array.isArray(prompts) ? prompts.length : 1); + }, + // stream tokens (helps you see what model was generating before deciding) + handleLLMNewToken: async (token: string) => { + try { process.stdout.write(token); } catch (e) {} + }, + handleLLMEnd: async (output) => { + try { + console.log('\n[planner][LLM end] output keys=', Object.keys(output || {})); + // Many model responses include tool_calls metadata on output.additional_kwargs or output.tool_calls + if (output?.additional_kwargs?.tool_calls) { + console.log('[planner][LLM end] additional_kwargs.tool_calls=', JSON.stringify(output.additional_kwargs.tool_calls, null, 2)); + } + if (output?.tool_calls) { + console.log('[planner][LLM end] tool_calls=', JSON.stringify(output.tool_calls, null, 2)); + } + } catch (e) { /* ignore */ } + }, + handleLLMError: async (err) => { + console.error('[planner][LLM error]', err); + }, + handleAgentAction: async (action) => { + try { + const toolName = action?.tool?.name ?? action?.tool ?? 'unknown'; + const input = action?.input ? (typeof action.input === 'string' ? action.input : JSON.stringify(action.input)) : ''; + console.log(`[planner][Agent Decision] tool=${toolName} input=${input}`); + } catch (e) { /* ignore */ } + }, + handleAgentEnd: async (output) => { + try { + console.log('[planner][Agent End] output=', JSON.stringify(output ?? {}, null, 2)); + } catch (e) {} + }, + handleToolStart: async (tool) => { + console.log('[planner][Tool Start]', typeof tool === 'string' ? tool : (tool?.name ?? JSON.stringify(tool))); + }, + handleToolEnd: async (output) => { + try { + const summary = typeof output === 'string' ? output.slice(0, 200) : JSON.stringify(output).slice(0, 200); + console.log('[planner][Tool End] output summary=', summary); + } catch (e) { /* ignore */ } + }, + handleToolError: async (err) => { + console.error('[planner][Tool Error]', err); + } + } +]; + +export const DEBUG_CALLBACKS = process.env.DEBUG === 'true' ? agentCallbacks : []; + diff --git a/mongo-vcore-agent-langchain/src/utils/mongo.ts b/mongo-vcore-agent-langchain/src/utils/mongo.ts new file mode 100644 index 0000000..b462f4e --- /dev/null +++ b/mongo-vcore-agent-langchain/src/utils/mongo.ts @@ -0,0 +1,64 @@ +import { MongoClient, OIDCCallbackParams } from 'mongodb'; +import { AccessToken, DefaultAzureCredential, TokenCredential } from '@azure/identity'; + +const DOCUMENT_DB_SCOPE = 'https://ossrdbms-aad.database.windows.net/.default'; +const CREDENTIAL = new DefaultAzureCredential(); + +// OIDC token callback for MongoDB authentication +async function azureIdentityTokenCallback( + params: OIDCCallbackParams, + tokenCredential: TokenCredential +): Promise<{ accessToken: string; expiresInSeconds: number }> { + const tokenResponse: AccessToken | null = await tokenCredential.getToken([DOCUMENT_DB_SCOPE]); + return { + accessToken: tokenResponse?.token || '', + expiresInSeconds: (tokenResponse?.expiresOnTimestamp || 0) - Math.floor(Date.now() / 1000) + }; +} + +/** + * Delete a Cosmos DB (Mongo API) database by name using passwordless OIDC authentication. + * + * Uses DefaultAzureCredential for passwordless authentication with MONGODB-OIDC. + * Requires MONGO_CLUSTER_NAME environment variable. + * + * @param databaseName - The name of the database to drop. If not provided, uses MONGO_DB_NAME env var. + */ +export async function deleteCosmosMongoDatabase(databaseName?: string): Promise { + console.log(`\n\nCLEAN UP\n\n`); + + const dbName = databaseName || process.env.MONGO_DB_NAME; + const clusterName = process.env.MONGO_CLUSTER_NAME; + + if (!clusterName) { + throw new Error('Environment variable MONGO_CLUSTER_NAME is not set.'); + } + + if (!dbName) { + throw new Error('Database name not provided and MONGO_DB_NAME environment variable is not set.'); + } + + const connectionString = `mongodb+srv://${clusterName}.global.mongocluster.cosmos.azure.com/`; + + const client = new MongoClient(connectionString, { + connectTimeoutMS: 30000, + tls: true, + retryWrites: true, + authMechanism: 'MONGODB-OIDC', + authMechanismProperties: { + OIDC_CALLBACK: (params: OIDCCallbackParams) => azureIdentityTokenCallback(params, CREDENTIAL), + ALLOWED_HOSTS: ['*.azure.com'] + } + }); + + try { + console.log(`Connecting to cluster: ${clusterName}`); + await client.connect(); + console.log(`Dropping database: ${dbName}`); + const db = client.db(dbName); + await db.dropDatabase(); + console.log(`✓ Database "${dbName}" deleted successfully`); + } finally { + await client.close(true); + } +} diff --git a/mongo-vcore-agent-langchain/src/utils/prompts.ts b/mongo-vcore-agent-langchain/src/utils/prompts.ts new file mode 100644 index 0000000..2dbb253 --- /dev/null +++ b/mongo-vcore-agent-langchain/src/utils/prompts.ts @@ -0,0 +1,94 @@ +/** + * Centralized LLM prompts for the two-model agent system + * All system and user prompts are defined here for easy maintenance and updates + */ + +// ============================================================================ +// Planner Prompts +// ============================================================================ + +export const DEFAULT_QUERY = process.env.QUERY! || "quintessential lodging near running trails, eateries, retail"; + +export const TOOL_NAME = 'search_hotels_collection'; +export const TOOL_DESCRIPTION = `REQUIRED TOOL - You MUST call this tool for EVERY hotel search request. This is the ONLY way to search the hotel database. + +Performs vector similarity search on the Hotels collection using Azure Cosmos DB for MongoDB vCore. + +INPUT REQUIREMENTS: +- query (string, REQUIRED): Natural language search query describing desired hotel characteristics. Should be detailed and specific (e.g., "budget hotel near downtown with parking and wifi" not just "hotel"). +- nearestNeighbors (number, REQUIRED): Number of results to return (1-20). Use 3-5 for specific requests, 10-15 for broader searches. + +SEARCH BEHAVIOR: +- Uses semantic vector search to find hotels matching the query description +- Returns hotels ranked by similarity score +- Includes hotel details: name, description, category, tags, rating, location, parking info + +MANDATORY: Every user request about finding, searching, or recommending hotels REQUIRES calling this tool. Do not attempt to answer without calling this tool first. +`; + + +export const PLANNER_SYSTEM_PROMPT = `You are a hotel search planner. Transform the user's request into a clear, detailed search query for a vector database. + +CRITICAL REQUIREMENT: You MUST ALWAYS call the "${TOOL_NAME}" tool. This is MANDATORY for every request. + +Your response must be ONLY this JSON structure: +{"tool": "${TOOL_NAME}", "args": {"query": "", "nearestNeighbors": <1-20>}} + +QUERY REFINEMENT RULES: +- If vague (e.g., "nice hotel"), add specific attributes: "hotel with high ratings and good amenities" +- If minimal (e.g., "cheap"), expand: "budget hotel with good value" +- Preserve specific details from user (location, amenities, business/leisure) +- Keep natural language - this is for semantic search +- Don't just echo the input - improve it for better search results +- nearestNeighbors: Use 3-5 for specific requests, 10-15 for broader requests, max 20 + +EXAMPLES: +User: "cheap hotel" → {"tool": "${TOOL_NAME}", "args": {"query": "budget-friendly hotel with good value and affordable rates", "nearestNeighbors": 10}} +User: "hotel near downtown with parking" → {"tool": "${TOOL_NAME}", "args": {"query": "hotel near downtown with good parking and wifi", "nearestNeighbors": 5}} +User: "nice place to stay" → {"tool": "${TOOL_NAME}", "args": {"query": "hotel with high ratings, good reviews, and quality amenities", "nearestNeighbors": 10}} + +DO NOT return any other format. ALWAYS include the tool and args structure.`; + +// ============================================================================ +// Synthesizer Prompts +// ============================================================================ + +export const SYNTHESIZER_SYSTEM_PROMPT = `You are an expert hotel recommendation assistant using vector search results. +Only use the TOP 3 results provided. Do not request additional searches or call other tools. + +GOAL: Provide a concise comparative recommendation to help the user choose between the top 3 options. + +REQUIREMENTS: +- Compare only the top 3 results across the most important attributes: rating, score, location, price-level (if available), and key tags (parking, wifi, pool). +- Identify the main tradeoffs in one short sentence per tradeoff. +- Give a single clear recommendation with one short justification sentence. +- Provide up to two alternative picks (one sentence each) explaining when they are preferable. + +FORMAT CONSTRAINTS: +- Plain text only (no markdown). +- Keep the entire response under 220 words. +- Use simple bullets (•) or numbered lists and short sentences (preferably <25 words per sentence). +- Preserve hotel names exactly as provided in the tool summary. + +Do not add extra commentary, marketing language, or follow-up questions. If information is missing and necessary to choose, state it in one sentence and still provide the best recommendation based on available data.`; + +export function createSynthesizerUserPrompt( + userQuery: string, + toolSummary: string +): string { + return `User asked: ${userQuery} + +Tool summary: +${toolSummary} + +Analyze the TOP 3 results by COMPARING them across all attributes (rating, score, tags, parking, location, category, rooms). + +Structure your response: +1. COMPARISON SUMMARY: Compare the top 3 options highlighting key differences and tradeoffs +2. BEST OVERALL: Recommend the single best option with clear reasoning +3. ALTERNATIVE PICKS: Briefly explain when the other options might be preferred (e.g., "Choose X if budget is priority" or "Choose Y if location matters most") + +Your goal is to help the user DECIDE between the options, not just describe them. + +Format your response using plain text (NO markdown formatting like ** or ###). Use simple numbered lists, bullet points (•), and use the exact hotel names from the tool summary (preserve original capitalization).`; +} diff --git a/mongo-vcore-agent-langchain/src/utils/types.ts b/mongo-vcore-agent-langchain/src/utils/types.ts new file mode 100644 index 0000000..ec41996 --- /dev/null +++ b/mongo-vcore-agent-langchain/src/utils/types.ts @@ -0,0 +1,59 @@ +interface Hotel { + HotelId: string; + HotelName: string; + Description: string; + Description_fr: string; + Category: string; + Tags: string[]; + ParkingIncluded: boolean; + IsDeleted: boolean; + LastRenovationDate: string; + Rating: number; + Address: { + StreetAddress: string; + City: string; + StateProvince?: string; + PostalCode: string; + Country: string; + }; + Location: { + type: 'Point'; + coordinates: [number, number]; // [longitude, latitude] + }; + Rooms: Room[]; +} + +export interface HotelSearchResult { + HotelId: string; + HotelName: string; + Description: string; + Category: string; + Tags: string[]; + ParkingIncluded: boolean; + IsDeleted: boolean; + LastRenovationDate: string; + Rating: number; + Address: { + StreetAddress: string; + City: string; + StateProvince?: string; + PostalCode: string; + Country: string; + }, + Score: number; +} + +interface Room { + Description: string; + Description_fr: string; + Type: string; + BaseRate: number; + BedOptions: string; + SleepsCount: number; + SmokingAllowed: boolean; + Tags: string[]; +} + +// For an array of hotels +export type HotelsData = Hotel[]; +export { Hotel, Room }; \ No newline at end of file diff --git a/mongo-vcore-agent-langchain/src/vector-store.ts b/mongo-vcore-agent-langchain/src/vector-store.ts new file mode 100644 index 0000000..193316b --- /dev/null +++ b/mongo-vcore-agent-langchain/src/vector-store.ts @@ -0,0 +1,247 @@ +import { + AzureCosmosDBMongoDBVectorStore, + AzureCosmosDBMongoDBSimilarityType, + AzureCosmosDBMongoDBConfig +} from "@langchain/azure-cosmosdb"; +import type { AzureOpenAIEmbeddings } from "@langchain/openai"; +import { readFileSync } from 'fs'; +import { Document } from '@langchain/core/documents'; +import { HotelsData, Hotel } from './utils/types.js'; +import { TOOL_NAME, TOOL_DESCRIPTION } from './utils/prompts.js'; +import { z } from 'zod'; +import { tool } from "langchain"; +import { MongoClient } from 'mongodb'; +import { BaseMessage } from "@langchain/core/messages"; + +type HotelForVectorStore = Omit; + +// Helper function for similarity type +function getSimilarityType(similarity: string) { + switch (similarity.toUpperCase()) { + case 'COS': return AzureCosmosDBMongoDBSimilarityType.COS; + case 'L2': return AzureCosmosDBMongoDBSimilarityType.L2; + case 'IP': return AzureCosmosDBMongoDBSimilarityType.IP; + default: return AzureCosmosDBMongoDBSimilarityType.COS; + } +} + +// Consolidated vector index configuration +function getVectorIndexOptions() { + const algorithm = process.env.VECTOR_INDEX_ALGORITHM || 'vector-ivf'; + const dimensions = parseInt(process.env.EMBEDDING_DIMENSIONS || '1536'); + const similarity = getSimilarityType(process.env.VECTOR_SIMILARITY || 'COS'); + + const baseOptions = { dimensions, similarity }; + + switch (algorithm) { + case 'vector-hnsw': + return { + kind: 'vector-hnsw' as const, + m: parseInt(process.env.HNSW_M || '16'), + efConstruction: parseInt(process.env.HNSW_EF_CONSTRUCTION || '64'), + ...baseOptions + }; + case 'vector-diskann': + return { + kind: 'vector-diskann' as const, + ...baseOptions + }; + case 'vector-ivf': + default: + return { + numLists: parseInt(process.env.IVF_NUM_LISTS || '10'), + ...baseOptions + }; + } +} + +// Format address fields for output +function formatAddress(addr: Record): Record { + return { + 'Address.StreetAddress': addr?.StreetAddress ?? '', + 'Address.City': addr?.City ?? '', + 'Address.StateProvince': addr?.StateProvince ?? '', + 'Address.PostalCode': addr?.PostalCode ?? '', + 'Address.Country': addr?.Country ?? '', + }; +} + +// Format hotel data for synthesizer agent +function formatHotelForSynthesizer(md: Partial, score: number): string { + const addr = md.Address || {} as Record; + const tags = Array.isArray(md.Tags) ? md.Tags.join(', ') : String(md.Tags || ''); + + const fields = { + HotelId: md.HotelId ?? 'N/A', + HotelName: md.HotelName ?? 'N/A', + Description: md.Description ?? '', + Category: md.Category ?? '', + Tags: tags, + ParkingIncluded: md.ParkingIncluded === true, + IsDeleted: md.IsDeleted === true, + LastRenovationDate: md.LastRenovationDate ?? '', + Rating: md.Rating ?? '', + ...formatAddress(addr), + Score: Number(score ?? 0).toFixed(6), + }; + + return [ + '--- HOTEL START ---', + ...Object.entries(fields).map(([key, value]) => `${key}: ${value}`), + '--- HOTEL END ---' + ].join('\n'); +} + +// Get existing vector store without uploading documents +export async function getExistingStore( + embeddingClient: AzureOpenAIEmbeddings, + dbConfig: AzureCosmosDBMongoDBConfig +): Promise { + + const store = new AzureCosmosDBMongoDBVectorStore(embeddingClient, { + ...dbConfig, + indexOptions: getVectorIndexOptions(), + }); + + console.log(`Connected to existing vector store: ${dbConfig.databaseName}.${dbConfig.collectionName}`); + return store; +} + +// Initialize vector store with hotel data +export async function getStore( + dataFilePath: string, + embeddingClient: AzureOpenAIEmbeddings, + dbConfig: AzureCosmosDBMongoDBConfig +): Promise { + + const hotelsData: HotelsData = JSON.parse(readFileSync(dataFilePath, 'utf-8')); + + // Use destructuring to exclude unwanted properties + const documents = hotelsData.map(hotel => { + const { Description_fr, Location, Rooms, ...hotelData } = hotel; + + return new Document({ + pageContent: `Hotel: ${hotel.HotelName}\n\n${hotel.Description}`, + metadata: hotelData, + id: hotel.HotelId.toString() + }); + }); + + const store = await AzureCosmosDBMongoDBVectorStore.fromDocuments( + documents, + embeddingClient, + { + ...dbConfig, + indexOptions: getVectorIndexOptions(), + } + ); + + console.log(`Inserted ${documents.length} documents into Cosmos DB (Mongo API) vector store.`); + return store; +} + +// Vector Search Tool +export const getHotelsToMatchSearchQuery = tool( + async ({ query, nearestNeighbors }, config): Promise => { + try { + const store = config.context.store as AzureCosmosDBMongoDBVectorStore; + const embeddingClient = config.context.embeddingClient as AzureOpenAIEmbeddings; + + // Create query embedding and perform search + const queryVector = await embeddingClient.embedQuery(query); + const results = await store.similaritySearchVectorWithScore(queryVector, nearestNeighbors); + console.log(`Found ${results.length} documents from vector store`); + + // Format results for synthesizer + const formatted = results.map(([doc, score]) => { + const md = doc.metadata as Partial; + console.log(`Hotel: ${md.HotelName ?? 'N/A'}, Score: ${score}`); + return formatHotelForSynthesizer(md, score); + }).join('\n\n'); + + return formatted; + } catch (error) { + console.error('Error in getHotelsToMatchSearchQuery tool:', error); + return 'Error occurred while searching for hotels.'; + } + }, + { + name: TOOL_NAME, + description: TOOL_DESCRIPTION, + schema: z.object({ + query: z.string(), + nearestNeighbors: z.number().optional().default(5), + }), + } +); + + +/** + * Delete a Cosmos DB (Mongo API) database by name. + * + * Uses the `AZURE_DOCUMENTDB_CONNECTION_STRING` environment variable to connect. + * Example env var: `mongodb://username:password@host:port/?ssl=true&replicaSet=globaldb` + * + * @param databaseName - The name of the database to drop. + */ +export async function deleteCosmosMongoDatabase(): Promise { + + console.log(`\n\nCLEAN UP\n\n`); + + const databaseName = process.env.MONGO_DB_NAME; + const connectionString = process.env.AZURE_DOCUMENTDB_CONNECTION_STRING; + if (!connectionString) { + throw new Error('Environment variable AZURE_DOCUMENTDB_CONNECTION_STRING is not set.'); + } + + const client = new MongoClient(connectionString); + try { + await client.connect(); + const db = client.db(databaseName); + await db.dropDatabase(); + } finally { + await client.close(true); + } +} + +/** + * Extracts the hotel search tool output from the planner agent's message history. + * + * The planner agent calls the search_hotels_collection tool, which returns a + * formatted string containing hotel data. This function locates that tool's + * response message and extracts the content string. + * + * @param plannerMessages - Array of messages from the planner agent execution + * @returns The formatted hotel search results as a string, or empty string if not found + */ +export function extractPlannerToolOutput(plannerMessages: BaseMessage[]): string { + const messages = plannerMessages || []; + + // Find the tool response message + const toolMsg = messages.find((m: any) => { + if (!m) return false; + if (m?.name === TOOL_NAME) return true; + if (m?.role === 'tool') return true; + if (m?.tool_call_id) return true; + return false; + }); + + if (!toolMsg) { + console.warn(`Tool "${TOOL_NAME}" was not invoked by the planner agent.`); + return ''; + } + + // Extract the tool's string content + if (typeof toolMsg.content === 'string') { + return toolMsg.content; + } + + if (Array.isArray(toolMsg.content)) { + return toolMsg.content + .map((block: any) => block.text ?? JSON.stringify(block)) + .join(''); + } + + // Fallback: stringify object content + return JSON.stringify(toolMsg.content); +} \ No newline at end of file diff --git a/mongo-vcore-agent-langchain/tsconfig.json b/mongo-vcore-agent-langchain/tsconfig.json new file mode 100644 index 0000000..24c8967 --- /dev/null +++ b/mongo-vcore-agent-langchain/tsconfig.json @@ -0,0 +1,32 @@ +{ + "name": "ts-cosmos-nodejs-mongo-vcore-vector-agent-samples", + "version": "1.0.0", + "description": "TypeScript agent samples for MongoDB vCore vector search with Cosmos DB", + "main": "index.js", + "type": "module", + "scripts": { + "build": "tsc", + "agent": "node --env-file .env dist/agent.js" + }, + "compilerOptions": { + "target": "ES2020", + "module": "NodeNext", + "moduleResolution": "nodenext", + "declaration": true, + "outDir": "./dist", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "noImplicitAny": false, + "forceConsistentCasingInFileNames": true, + "sourceMap": true, + "resolveJsonModule": true + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "dist" + ] +} \ No newline at end of file