Skip to content

Commit

Permalink
fix: Post-Deployment Script for Managing Bicep Outputs in .env File a…
Browse files Browse the repository at this point in the history
…nd Update Conversation flow based on template selection (#1567)

Co-authored-by: Pavan Kumar <v-kupavan.microsoft.com>
  • Loading branch information
Pavan-Microsoft authored Dec 19, 2024
1 parent 93b84ed commit 0875b92
Show file tree
Hide file tree
Showing 21 changed files with 1,176 additions and 544 deletions.
22 changes: 14 additions & 8 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ AZURE_SEARCH_DATASOURCE_NAME=
# Azure OpenAI for generating the answer and computing the embedding of the documents
AZURE_OPENAI_RESOURCE=
AZURE_OPENAI_API_KEY=
AZURE_OPENAI_MODEL_INFO="{\"model\":\"gpt-35-turbo-16k\",\"modelName\":\"gpt-35-turbo-16k\",\"modelVersion\":\"0613\"}"
AZURE_OPENAI_EMBEDDING_MODEL_INFO="{\"model\":\"text-embedding-ada-002\",\"modelName\":\"text-embedding-ada-002\",\"modelVersion\":\"2\"}"
AZURE_OPENAI_MODEL=gpt-35-turbo
AZURE_OPENAI_MODEL_NAME=gpt-35-turbo
AZURE_OPENAI_EMBEDDING_MODEL=text-embedding-ada-002
AZURE_OPENAI_TEMPERATURE=0
AZURE_OPENAI_TOP_P=1.0
AZURE_OPENAI_MAX_TOKENS=1000
Expand All @@ -35,10 +36,12 @@ AZURE_OPENAI_STREAM=True
AzureWebJobsStorage=
BACKEND_URL=http://localhost:7071
DOCUMENT_PROCESSING_QUEUE_NAME=
# Azure Blob Storage for storing the original documents to be processed
AZURE_BLOB_STORAGE_INFO="{\"containerName\":\"documents\",\"accountName\":\"\",\"accountKey\":\"\"}"
AZURE_BLOB_ACCOUNT_NAME=
AZURE_BLOB_ACCOUNT_KEY=
AZURE_BLOB_CONTAINER_NAME=
# Azure Form Recognizer for extracting the text from the documents
AZURE_FORM_RECOGNIZER_INFO="{\"endpoint\":\"\",\"key\":\"\"}"
AZURE_FORM_RECOGNIZER_ENDPOINT=
AZURE_FORM_RECOGNIZER_KEY=
# Azure AI Content Safety for filtering out the inappropriate questions or answers
AZURE_CONTENT_SAFETY_ENDPOINT=
AZURE_CONTENT_SAFETY_KEY=
Expand All @@ -60,8 +63,11 @@ AZURE_KEY_VAULT_ENDPOINT=
# Chat conversation type to decide between custom or byod (bring your own data) conversation type
CONVERSATION_FLOW=
# Chat History CosmosDB Integration Settings
AZURE_COSMOSDB_INFO="{\"accountName\":\"cosmos-abc123\",\"databaseName\":\"db_conversation_history\",\"containerName\":\"conversations\"}"
AZURE_COSMOSDB_ACCOUNT_KEY=
AZURE_COSMOSDB_ACCOUNT_NAME=
AZURE_COSMOSDB_DATABASE_NAME=
AZURE_COSMOSDB_CONVERSATIONS_CONTAINER_NAME=
AZURE_COSMOSDB_ENABLE_FEEDBACK=
AZURE_POSTGRESQL_INFO="{\"user\":\"\",\"dbname\":\"postgres\",\"host\":\"\"}"
AZURE_POSTGRESQL_HOST_NAME=
AZURE_POSTGRESQL_DATABASE_NAME=
AZURE_POSTGRESQL_USER=
DATABASE_TYPE="CosmosDB"
6 changes: 6 additions & 0 deletions azure.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ metadata:
hooks:
postprovision:
run: ./infra/prompt-flow/create-prompt-flow.sh
posix:
shell: sh
run: ./scripts/parse_env.sh
windows:
shell: pwsh
run: ./scripts/parse_env.ps1
services:
web:
project: ./code
Expand Down
10 changes: 5 additions & 5 deletions code/backend/batch/utilities/helpers/config/config_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ def __init__(self, config: dict):
)
self.enable_chat_history = config["enable_chat_history"]
self.database_type = config.get("database_type", self.env_helper.DATABASE_TYPE)
self.conversational_flow = config.get(
"conversational_flow", self.env_helper.CONVERSATION_FLOW
)

def get_available_document_types(self) -> list[str]:
document_types = {
Expand Down Expand Up @@ -247,11 +250,7 @@ def get_default_config():
logger.info("Loading default config from %s", config_file_path)
ConfigHelper._default_config = json.loads(
Template(f.read()).substitute(
ORCHESTRATION_STRATEGY=(
OrchestrationStrategy.SEMANTIC_KERNEL.value
if env_helper.DATABASE_TYPE == DatabaseType.POSTGRESQL.value
else env_helper.ORCHESTRATION_STRATEGY
),
ORCHESTRATION_STRATEGY=env_helper.ORCHESTRATION_STRATEGY,
LOG_USER_INTERACTIONS=(
False
if env_helper.DATABASE_TYPE == DatabaseType.POSTGRESQL.value
Expand All @@ -262,6 +261,7 @@ def get_default_config():
if env_helper.DATABASE_TYPE == DatabaseType.POSTGRESQL.value
else True
),
CONVERSATION_FLOW=env_helper.CONVERSATION_FLOW,
DATABASE_TYPE=env_helper.DATABASE_TYPE,
)
)
Expand Down
2 changes: 1 addition & 1 deletion code/backend/batch/utilities/helpers/config/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"enable_post_answering_prompt": false,
"ai_assistant_type": "default",
"enable_content_safety": true,
"conversational_flow": "custom"
"conversational_flow": "${CONVERSATION_FLOW}"
},
"example": {
"documents": "{\n \"retrieved_documents\": [\n {\n \"[doc1]\": {\n \"content\": \"Dual Transformer Encoder (DTE) DTE (https://dev.azure.com/TScience/TSciencePublic/_wiki/wikis/TSciencePublic.wiki/82/Dual-Transformer-Encoder) DTE is a general pair-oriented sentence representation learning framework based on transformers. It provides training, inference and evaluation for sentence similarity models. Model Details DTE can be used to train a model for sentence similarity with the following features: - Build upon existing transformer-based text representations (e.g.TNLR, BERT, RoBERTa, BAG-NLR) - Apply smoothness inducing technology to improve the representation robustness - SMART (https://arxiv.org/abs/1911.03437) SMART - Apply NCE (Noise Contrastive Estimation) based similarity learning to speed up training of 100M pairs We use pretrained DTE model\"\n }\n },\n {\n \"[doc2]\": {\n \"content\": \"trained on internal data. You can find more details here - Models.md (https://dev.azure.com/TScience/_git/TSciencePublic?path=%2FDualTransformerEncoder%2FMODELS.md&version=GBmaster&_a=preview) Models.md DTE-pretrained for In-context Learning Research suggests that finetuned transformers can be used to retrieve semantically similar exemplars for e.g. KATE (https://arxiv.org/pdf/2101.06804.pdf) KATE . They show that finetuned models esp. tuned on related tasks give the maximum boost to GPT-3 in-context performance. DTE have lot of pretrained models that are trained on intent classification tasks. We can use these model embedding to find natural language utterances which are similar to our test utterances at test time. The steps are: 1. Embed\"\n }\n },\n {\n \"[doc3]\": {\n \"content\": \"train and test utterances using DTE model 2. For each test embedding, find K-nearest neighbors. 3. Prefix the prompt with nearest embeddings. The following diagram from the above paper (https://arxiv.org/pdf/2101.06804.pdf) the above paper visualizes this process: DTE-Finetuned This is an extension of DTE-pretrained method where we further finetune the embedding models for prompt crafting task. In summary, we sample random prompts from our training data and use them for GPT-3 inference for the another part of training data. Some prompts work better and lead to right results whereas other prompts lead\"\n }\n },\n {\n \"[doc4]\": {\n \"content\": \"to wrong completions. We finetune the model on the downstream task of whether a prompt is good or not based on whether it leads to right or wrong completion. This approach is similar to this paper: Learning To Retrieve Prompts for In-Context Learning (https://arxiv.org/pdf/2112.08633.pdf) this paper: Learning To Retrieve Prompts for In-Context Learning . This method is very general but it may require a lot of data to actually finetune a model to learn how to retrieve examples suitable for the downstream inference model like GPT-3.\"\n }\n }\n ]\n}",
Expand Down
52 changes: 40 additions & 12 deletions code/backend/batch/utilities/helpers/env_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
from dotenv import load_dotenv
from azure.identity import DefaultAzureCredential, get_bearer_token_provider
from azure.keyvault.secrets import SecretClient

from backend.batch.utilities.orchestrator.orchestration_strategy import (
OrchestrationStrategy,
)
from backend.batch.utilities.helpers.config.conversation_flow import ConversationFlow
from ..helpers.config.database_type import DatabaseType

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -97,11 +102,24 @@ def __load_config(self, **kwargs) -> None:
# Cosmos DB configuration
if self.DATABASE_TYPE == DatabaseType.COSMOSDB.value:
azure_cosmosdb_info = self.get_info_from_env("AZURE_COSMOSDB_INFO", "")
self.AZURE_COSMOSDB_DATABASE = azure_cosmosdb_info.get("databaseName", "")
self.AZURE_COSMOSDB_ACCOUNT = azure_cosmosdb_info.get("accountName", "")
self.AZURE_COSMOSDB_CONVERSATIONS_CONTAINER = azure_cosmosdb_info.get(
"containerName", ""
)
if azure_cosmosdb_info:
self.AZURE_COSMOSDB_DATABASE = azure_cosmosdb_info.get(
"databaseName", ""
)
self.AZURE_COSMOSDB_ACCOUNT = azure_cosmosdb_info.get("accountName", "")
self.AZURE_COSMOSDB_CONVERSATIONS_CONTAINER = azure_cosmosdb_info.get(
"containerName", ""
)
else:
self.AZURE_COSMOSDB_DATABASE = os.getenv(
"AZURE_COSMOSDB_DATABASE_NAME", ""
)
self.AZURE_COSMOSDB_ACCOUNT = os.getenv(
"AZURE_COSMOSDB_ACCOUNT_NAME", ""
)
self.AZURE_COSMOSDB_CONVERSATIONS_CONTAINER = os.getenv(
"AZURE_COSMOSDB_CONVERSATIONS_CONTAINER_NAME", ""
)
self.AZURE_COSMOSDB_ACCOUNT_KEY = self.secretHelper.get_secret(
"AZURE_COSMOSDB_ACCOUNT_KEY"
)
Expand All @@ -114,18 +132,32 @@ def __load_config(self, **kwargs) -> None:
self.USE_ADVANCED_IMAGE_PROCESSING = self.get_env_var_bool(
"USE_ADVANCED_IMAGE_PROCESSING", "False"
)
self.CONVERSATION_FLOW = os.getenv("CONVERSATION_FLOW", "custom")
# Orchestration Settings
self.ORCHESTRATION_STRATEGY = os.getenv(
"ORCHESTRATION_STRATEGY", "openai_function"
)
# PostgreSQL configuration
elif self.DATABASE_TYPE == DatabaseType.POSTGRESQL.value:
self.AZURE_POSTGRES_SEARCH_TOP_K = self.get_env_var_int(
"AZURE_POSTGRES_SEARCH_TOP_K", 5
)
azure_postgresql_info = self.get_info_from_env("AZURE_POSTGRESQL_INFO", "")
self.POSTGRESQL_USER = azure_postgresql_info.get("user", "")
self.POSTGRESQL_DATABASE = azure_postgresql_info.get("dbname", "")
self.POSTGRESQL_HOST = azure_postgresql_info.get("host", "")
if azure_postgresql_info:
self.POSTGRESQL_USER = azure_postgresql_info.get("user", "")
self.POSTGRESQL_DATABASE = azure_postgresql_info.get("dbname", "")
self.POSTGRESQL_HOST = azure_postgresql_info.get("host", "")
else:
self.POSTGRESQL_USER = os.getenv("AZURE_POSTGRESQL_USER", "")
self.POSTGRESQL_DATABASE = os.getenv(
"AZURE_POSTGRESQL_DATABASE_NAME", ""
)
self.POSTGRESQL_HOST = os.getenv("AZURE_POSTGRESQL_HOST_NAME", "")
# Ensure integrated vectorization is disabled for PostgreSQL
self.AZURE_SEARCH_USE_INTEGRATED_VECTORIZATION = False
self.USE_ADVANCED_IMAGE_PROCESSING = False
self.CONVERSATION_FLOW = ConversationFlow.CUSTOM.value
self.ORCHESTRATION_STRATEGY = OrchestrationStrategy.SEMANTIC_KERNEL.value
else:
raise ValueError(
"Unsupported DATABASE_TYPE. Please set DATABASE_TYPE to 'CosmosDB' or 'PostgreSQL'."
Expand Down Expand Up @@ -305,10 +337,6 @@ def __load_config(self, **kwargs) -> None:
self.AZURE_CONTENT_SAFETY_KEY = self.secretHelper.get_secret(
"AZURE_CONTENT_SAFETY_KEY"
)
# Orchestration Settings
self.ORCHESTRATION_STRATEGY = os.getenv(
"ORCHESTRATION_STRATEGY", "openai_function"
)
# Speech Service
self.AZURE_SPEECH_SERVICE_NAME = os.getenv("AZURE_SPEECH_SERVICE_NAME", "")
self.AZURE_SPEECH_SERVICE_REGION = os.getenv("AZURE_SPEECH_SERVICE_REGION")
Expand Down
181 changes: 181 additions & 0 deletions code/tests/chat_history/test_cosmosdb.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import pytest
from unittest.mock import AsyncMock, patch
from azure.cosmos import exceptions
from backend.batch.utilities.chat_history.cosmosdb import CosmosConversationClient


@pytest.fixture
def mock_cosmos_client():
mock_client = AsyncMock()
mock_database_client = AsyncMock()
mock_container_client = AsyncMock()

mock_client.get_database_client.return_value = mock_database_client
mock_database_client.get_container_client.return_value = mock_container_client

return mock_client, mock_database_client, mock_container_client


@pytest.fixture
def cosmos_client(mock_cosmos_client):
cosmosdb_client, database_client, container_client = mock_cosmos_client
with patch("azure.cosmos.aio.CosmosClient", return_value=cosmosdb_client):
client = CosmosConversationClient(
cosmosdb_endpoint="https://test-cosmosdb.com",
credential="test-credential",
database_name="test-database",
container_name="test-container",
)
client.cosmosdb_client = cosmosdb_client
client.database_client = database_client
client.container_client = container_client
return client


@pytest.mark.asyncio
async def test_initialize_client_success(cosmos_client):
client = cosmos_client

assert client.cosmosdb_endpoint == "https://test-cosmosdb.com"
assert client.credential == "test-credential"
assert client.database_name == "test-database"
assert client.container_name == "test-container"


@pytest.mark.asyncio
async def test_ensure_client_initialized_success(cosmos_client):
client = cosmos_client
client.database_client.read = AsyncMock()
client.container_client.read = AsyncMock()

result, message = await client.ensure()

assert result is True
assert message == "CosmosDB client initialized successfully"
client.database_client.read.assert_called_once()
client.container_client.read.assert_called_once()


@pytest.mark.asyncio
async def test_ensure_client_not_initialized(cosmos_client):
client = cosmos_client
client.database_client.read = AsyncMock(
side_effect=exceptions.CosmosHttpResponseError
)
client.container_client.read = AsyncMock()

result, message = await client.ensure()

assert result is False
assert "not found" in message.lower()
client.database_client.read.assert_called_once()


@pytest.mark.asyncio
async def test_create_conversation_success(cosmos_client):
client = cosmos_client
client.container_client.upsert_item = AsyncMock(
return_value={"id": "500e77bd-26b9-441a-8fe3-cd0e02993671"}
)

response = await client.create_conversation(
"user-123", "500e77bd-26b9-441a-8fe3-cd0e02993671", "Test Conversation"
)

assert response["id"] == "500e77bd-26b9-441a-8fe3-cd0e02993671"


@pytest.mark.asyncio
async def test_create_conversation_failure(cosmos_client):
client = cosmos_client
client.container_client.upsert_item = AsyncMock(return_value=None)

response = await client.create_conversation(
"user-123", "500e77bd-26b9-441a-8fe3-cd0e02993671", "Test Conversation"
)

assert response is False


@pytest.mark.asyncio
async def test_upsert_conversation_success(cosmos_client):
client = cosmos_client
client.container_client.upsert_item = AsyncMock(
return_value={"id": "500e77bd-26b9-441a-8fe3-cd0e02993671"}
)

conversation = {
"id": "500e77bd-26b9-441a-8fe3-cd0e02993671",
"type": "conversation",
"userId": "user-123",
"title": "Updated Conversation",
}
response = await client.upsert_conversation(conversation)

assert response["id"] == "500e77bd-26b9-441a-8fe3-cd0e02993671"


@pytest.mark.asyncio
async def test_delete_conversation_success(cosmos_client):
client = cosmos_client
client.container_client.read_item = AsyncMock(
return_value={"id": "500e77bd-26b9-441a-8fe3-cd0e02993671"}
)
client.container_client.delete_item = AsyncMock(return_value={"status": "deleted"})

response = await client.delete_conversation(
"user-123", "500e77bd-26b9-441a-8fe3-cd0e02993671"
)

assert response["status"] == "deleted"
client.container_client.delete_item.assert_called_once_with(
item="500e77bd-26b9-441a-8fe3-cd0e02993671", partition_key="user-123"
)


@pytest.mark.asyncio
async def test_delete_messages_success(cosmos_client):
client = cosmos_client
client.get_messages = AsyncMock(
return_value=[
{"id": "39c395da-e2f7-49c9-bca5-c9511d3c5172"},
{"id": "39c395da-e2f7-49c9-bca5-c9511d3c5174"},
]
)
client.container_client.delete_item = AsyncMock()

response = await client.delete_messages(
"500e77bd-26b9-441a-8fe3-cd0e02993671", "user-123"
)

assert len(response) == 2
client.get_messages.assert_called_once_with(
"user-123", "500e77bd-26b9-441a-8fe3-cd0e02993671"
)
client.container_client.delete_item.assert_any_call(
item="39c395da-e2f7-49c9-bca5-c9511d3c5172", partition_key="user-123"
)
client.container_client.delete_item.assert_any_call(
item="39c395da-e2f7-49c9-bca5-c9511d3c5174", partition_key="user-123"
)


@pytest.mark.asyncio
async def test_update_message_feedback_success(cosmos_client):
client = cosmos_client
client.container_client.read_item = AsyncMock(
return_value={"id": "39c395da-e2f7-49c9-bca5-c9511d3c5172", "feedback": ""}
)
client.container_client.upsert_item = AsyncMock(
return_value={
"id": "39c395da-e2f7-49c9-bca5-c9511d3c5172",
"feedback": "positive",
}
)

response = await client.update_message_feedback(
"user-123", "39c395da-e2f7-49c9-bca5-c9511d3c5172", "positive"
)

assert response["feedback"] == "positive"
client.container_client.upsert_item.assert_called_once()
Loading

0 comments on commit 0875b92

Please sign in to comment.