Skip to content

Commit 6998ab0

Browse files
Roopan-MicrosoftAjitPadhi-MicrosoftFr4nc3
authored
fix: Psl 6635 - BYOD workflow implementation (#1265)
Co-authored-by: Ajit Padhi (Persistent Systems Inc) <v-padhiajit@microsoft.com> Co-authored-by: Francia Riesco <Fr4nc3@users.noreply.github.com>
1 parent 53b899c commit 6998ab0

File tree

12 files changed

+341
-67
lines changed

12 files changed

+341
-67
lines changed

code/backend/Admin.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ def load_css(file_path):
5151
"""
5252
* If you want to ingest data (pdf, websites, etc.), then use the `Ingest Data` tab
5353
* If you want to explore how your data was chunked, check the `Explore Data` tab
54+
* If you want to delete your data, check the `Delete Data` tab
5455
* If you want to adapt the underlying prompts, logging settings and others, use the `Configuration` tab
5556
"""
5657
)

code/backend/batch/utilities/helpers/config/config_helper.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from ...orchestrator import OrchestrationSettings
1313
from ..env_helper import EnvHelper
1414
from .assistant_strategy import AssistantStrategy
15+
from .conversation_flow import ConversationFlow
1516

1617
CONFIG_CONTAINER_NAME = "config"
1718
CONFIG_FILE_NAME = "active.json"
@@ -90,6 +91,9 @@ def get_available_orchestration_strategies(self):
9091
def get_available_ai_assistant_types(self):
9192
return [c.value for c in AssistantStrategy]
9293

94+
def get_available_conversational_flows(self):
95+
return [c.value for c in ConversationFlow]
96+
9397

9498
# TODO: Change to AnsweringChain or something, Prompts is not a good name
9599
class Prompts:
@@ -102,6 +106,7 @@ def __init__(self, prompts: dict):
102106
self.enable_post_answering_prompt = prompts["enable_post_answering_prompt"]
103107
self.enable_content_safety = prompts["enable_content_safety"]
104108
self.ai_assistant_type = prompts["ai_assistant_type"]
109+
self.conversational_flow = prompts["conversational_flow"]
105110

106111

107112
class Example:
@@ -166,13 +171,20 @@ def _set_new_config_properties(config: dict, default_config: dict):
166171
config["example"] = default_config["example"]
167172

168173
if config["prompts"].get("ai_assistant_type") is None:
169-
config["prompts"]["ai_assistant_type"] = default_config["prompts"]["ai_assistant_type"]
174+
config["prompts"]["ai_assistant_type"] = default_config["prompts"][
175+
"ai_assistant_type"
176+
]
170177

171178
if config.get("integrated_vectorization_config") is None:
172179
config["integrated_vectorization_config"] = default_config[
173180
"integrated_vectorization_config"
174181
]
175182

183+
if config["prompts"].get("conversational_flow") is None:
184+
config["prompts"]["conversational_flow"] = default_config["prompts"][
185+
"conversational_flow"
186+
]
187+
176188
@staticmethod
177189
@functools.cache
178190
def get_active_config_or_default():
@@ -247,12 +259,14 @@ def get_default_config():
247259
@staticmethod
248260
@functools.cache
249261
def get_default_contract_assistant():
250-
contract_file_path = os.path.join(os.path.dirname(__file__), "default_contract_assistant_prompt.txt")
262+
contract_file_path = os.path.join(
263+
os.path.dirname(__file__), "default_contract_assistant_prompt.txt"
264+
)
251265
contract_assistant = ""
252266
with open(contract_file_path, encoding="utf-8") as f:
253267
contract_assistant = f.readlines()
254268

255-
return ''.join([str(elem) for elem in contract_assistant])
269+
return "".join([str(elem) for elem in contract_assistant])
256270

257271
@staticmethod
258272
def clear_config():

code/backend/batch/utilities/helpers/config/default.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
"use_on_your_data_format": true,
99
"enable_post_answering_prompt": false,
1010
"ai_assistant_type": "default",
11-
"enable_content_safety": true
11+
"enable_content_safety": true,
12+
"conversational_flow": "custom"
1213
},
1314
"example": {
1415
"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}",

code/backend/batch/utilities/helpers/env_helper.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
from dotenv import load_dotenv
55
from azure.identity import DefaultAzureCredential, get_bearer_token_provider
66
from azure.keyvault.secrets import SecretClient
7-
from .config.conversation_flow import ConversationFlow
87

98
logger = logging.getLogger(__name__)
109

@@ -69,9 +68,13 @@ def __load_config(self, **kwargs) -> None:
6968
self.AZURE_SEARCH_FIELDS_METADATA = os.getenv(
7069
"AZURE_SEARCH_FIELDS_METADATA", "metadata"
7170
)
72-
self.AZURE_SEARCH_SOURCE_COLUMN = os.getenv("AZURE_SEARCH_SOURCE_COLUMN", "source")
71+
self.AZURE_SEARCH_SOURCE_COLUMN = os.getenv(
72+
"AZURE_SEARCH_SOURCE_COLUMN", "source"
73+
)
7374
self.AZURE_SEARCH_CHUNK_COLUMN = os.getenv("AZURE_SEARCH_CHUNK_COLUMN", "chunk")
74-
self.AZURE_SEARCH_OFFSET_COLUMN = os.getenv("AZURE_SEARCH_OFFSET_COLUMN", "offset")
75+
self.AZURE_SEARCH_OFFSET_COLUMN = os.getenv(
76+
"AZURE_SEARCH_OFFSET_COLUMN", "offset"
77+
)
7578
self.AZURE_SEARCH_CONVERSATIONS_LOG_INDEX = os.getenv(
7679
"AZURE_SEARCH_CONVERSATIONS_LOG_INDEX", "conversations"
7780
)
@@ -211,10 +214,6 @@ def __load_config(self, **kwargs) -> None:
211214
self.ORCHESTRATION_STRATEGY = os.getenv(
212215
"ORCHESTRATION_STRATEGY", "openai_function"
213216
)
214-
# Conversation Type - which chooses between custom or byod
215-
self.CONVERSATION_FLOW = os.getenv(
216-
"CONVERSATION_FLOW", ConversationFlow.CUSTOM.value
217-
)
218217
# Speech Service
219218
self.AZURE_SPEECH_SERVICE_NAME = os.getenv("AZURE_SPEECH_SERVICE_NAME", "")
220219
self.AZURE_SPEECH_SERVICE_REGION = os.getenv("AZURE_SPEECH_SERVICE_REGION")

code/backend/pages/04_Configuration.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from batch.utilities.helpers.config.config_helper import ConfigHelper
88
from azure.core.exceptions import ResourceNotFoundError
99
from batch.utilities.helpers.config.assistant_strategy import AssistantStrategy
10+
from batch.utilities.helpers.config.conversation_flow import ConversationFlow
1011

1112
sys.path.append(os.path.join(os.path.dirname(__file__), ".."))
1213
env_helper: EnvHelper = EnvHelper()
@@ -65,6 +66,8 @@ def load_css(file_path):
6566
st.session_state["orchestrator_strategy"] = config.orchestrator.strategy.value
6667
if "ai_assistant_type" not in st.session_state:
6768
st.session_state["ai_assistant_type"] = config.prompts.ai_assistant_type
69+
if "conversational_flow" not in st.session_state:
70+
st.session_state["conversational_flow"] = config.prompts.conversational_flow
6871

6972
if env_helper.AZURE_SEARCH_USE_INTEGRATED_VECTORIZATION:
7073
if "max_page_length" not in st.session_state:
@@ -163,13 +166,30 @@ def validate_documents():
163166

164167

165168
try:
169+
conversational_flow_help = "Whether to use the custom conversational flow or byod conversational flow. Refer to the Conversational flow options README for more details."
170+
with st.expander("Conversational flow configuration", expanded=True):
171+
cols = st.columns([2, 4])
172+
with cols[0]:
173+
conv_flow = st.selectbox(
174+
"Conversational flow",
175+
key="conversational_flow",
176+
options=config.get_available_conversational_flows(),
177+
help=conversational_flow_help,
178+
)
179+
166180
with st.expander("Orchestrator configuration", expanded=True):
167181
cols = st.columns([2, 4])
168182
with cols[0]:
169183
st.selectbox(
170184
"Orchestrator strategy",
171185
key="orchestrator_strategy",
172186
options=config.get_available_orchestration_strategies(),
187+
disabled=(
188+
True
189+
if st.session_state["conversational_flow"]
190+
== ConversationFlow.BYOD.value
191+
else False
192+
),
173193
)
174194

175195
# # # condense_question_prompt_help = "This prompt is used to convert the user's input to a standalone question, using the context of the chat history."
@@ -377,6 +397,7 @@ def validate_documents():
377397
],
378398
"enable_content_safety": st.session_state["enable_content_safety"],
379399
"ai_assistant_type": st.session_state["ai_assistant_type"],
400+
"conversational_flow": st.session_state["conversational_flow"],
380401
},
381402
"messages": {
382403
"post_answering_filter": st.session_state[

code/create_app.py

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,67 @@
88
import mimetypes
99
from os import path
1010
import sys
11+
import re
1112
import requests
1213
from openai import AzureOpenAI, Stream, APIStatusError
1314
from openai.types.chat import ChatCompletionChunk
1415
from flask import Flask, Response, request, Request, jsonify
1516
from dotenv import load_dotenv
17+
from urllib.parse import quote
1618
from backend.batch.utilities.helpers.env_helper import EnvHelper
1719
from backend.batch.utilities.helpers.orchestrator_helper import Orchestrator
1820
from backend.batch.utilities.helpers.config.config_helper import ConfigHelper
1921
from backend.batch.utilities.helpers.config.conversation_flow import ConversationFlow
2022
from azure.mgmt.cognitiveservices import CognitiveServicesManagementClient
2123
from azure.identity import DefaultAzureCredential
24+
from backend.batch.utilities.helpers.azure_blob_storage_client import (
25+
AzureBlobStorageClient,
26+
)
2227

2328
ERROR_429_MESSAGE = "We're currently experiencing a high number of requests for the service you're trying to access. Please wait a moment and try again."
2429
ERROR_GENERIC_MESSAGE = "An error occurred. Please try again. If the problem persists, please contact the site administrator."
2530
logger = logging.getLogger(__name__)
2631

2732

33+
def get_markdown_url(source, title, container_sas):
34+
"""Get Markdown URL for a citation"""
35+
36+
url = quote(source, safe=":/")
37+
if "_SAS_TOKEN_PLACEHOLDER_" in url:
38+
url = url.replace("_SAS_TOKEN_PLACEHOLDER_", container_sas)
39+
return f"[{title}]({url})"
40+
41+
42+
def get_citations(citation_list):
43+
"""Returns Formated Citations"""
44+
blob_client = AzureBlobStorageClient()
45+
container_sas = blob_client.get_container_sas()
46+
citations_dict = {"citations": []}
47+
for citation in citation_list.get("citations"):
48+
metadata = (
49+
json.loads(citation["url"])
50+
if isinstance(citation["url"], str)
51+
else citation["url"]
52+
)
53+
title = citation["title"]
54+
url = get_markdown_url(metadata["source"], title, container_sas)
55+
citations_dict["citations"].append(
56+
{
57+
"content": url + "\n\n\n" + citation["content"],
58+
"id": metadata["id"],
59+
"chunk_id": (
60+
re.findall(r"\d+", metadata["chunk_id"])[-1]
61+
if metadata["chunk_id"] is not None
62+
else metadata["chunk"]
63+
),
64+
"title": title,
65+
"filepath": title.split("/")[-1],
66+
"url": url,
67+
}
68+
)
69+
return citations_dict
70+
71+
2872
def stream_with_data(response: Stream[ChatCompletionChunk]):
2973
"""This function streams the response from Azure OpenAI with data."""
3074
response_obj = {
@@ -67,8 +111,9 @@ def stream_with_data(response: Stream[ChatCompletionChunk]):
67111
role = delta.role
68112

69113
if role == "assistant":
114+
citations = get_citations(delta.model_extra["context"])
70115
response_obj["choices"][0]["messages"][0]["content"] = json.dumps(
71-
delta.model_extra["context"],
116+
citations,
72117
ensure_ascii=False,
73118
)
74119
else:
@@ -135,7 +180,8 @@ def conversation_with_data(conversation: Request, env_helper: EnvHelper):
135180
env_helper.AZURE_SEARCH_CONTENT_VECTOR_COLUMN
136181
],
137182
"title_field": env_helper.AZURE_SEARCH_TITLE_COLUMN or None,
138-
"url_field": env_helper.AZURE_SEARCH_URL_COLUMN or None,
183+
"url_field": env_helper.AZURE_SEARCH_FIELDS_METADATA
184+
or None,
139185
"filepath_field": (
140186
env_helper.AZURE_SEARCH_FILENAME_COLUMN or None
141187
),
@@ -166,6 +212,7 @@ def conversation_with_data(conversation: Request, env_helper: EnvHelper):
166212
)
167213

168214
if not env_helper.SHOULD_STREAM:
215+
citations = get_citations(response.choices[0].message.model_extra["context"])
169216
response_obj = {
170217
"id": response.id,
171218
"model": response.model,
@@ -176,7 +223,7 @@ def conversation_with_data(conversation: Request, env_helper: EnvHelper):
176223
"messages": [
177224
{
178225
"content": json.dumps(
179-
response.choices[0].message.model_extra["context"],
226+
citations,
180227
ensure_ascii=False,
181228
),
182229
"end_turn": False,
@@ -194,10 +241,7 @@ def conversation_with_data(conversation: Request, env_helper: EnvHelper):
194241

195242
return response_obj
196243

197-
return Response(
198-
stream_with_data(response),
199-
mimetype="application/json-lines",
200-
)
244+
return Response(stream_with_data(response), mimetype="application/json-lines")
201245

202246

203247
def stream_without_data(response: Stream[ChatCompletionChunk]):
@@ -405,7 +449,9 @@ async def conversation_custom():
405449

406450
@app.route("/api/conversation", methods=["POST"])
407451
async def conversation():
408-
conversation_flow = env_helper.CONVERSATION_FLOW
452+
ConfigHelper.get_active_config_or_default.cache_clear()
453+
result = ConfigHelper.get_active_config_or_default()
454+
conversation_flow = result.prompts.conversational_flow
409455
if conversation_flow == ConversationFlow.CUSTOM.value:
410456
return await conversation_custom()
411457
elif conversation_flow == ConversationFlow.BYOD.value:

code/frontend/src/components/Answer/AnswerParser.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ let filteredCitations = [] as Citation[];
1111

1212
// Define a function to check if a citation with the same Chunk_Id already exists in filteredCitations
1313
const isDuplicate = (citation: Citation,citationIndex:string) => {
14-
return filteredCitations.some((c) => c.chunk_id === citation.chunk_id) && !filteredCitations.find((c) => c.id === citationIndex) ;
14+
return filteredCitations.some((c) => c.chunk_id === citation.chunk_id) ;
1515
};
1616

1717
export function parseAnswer(answer: AskResponse): ParsedAnswer {

code/tests/functional/tests/backend_api/default/test_conversation.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import re
33
import pytest
44
from pytest_httpserver import HTTPServer
5+
from unittest.mock import patch
56
import requests
67

78
from tests.request_matching import (
@@ -176,7 +177,9 @@ def test_post_makes_correct_calls_to_openai_embeddings_to_embed_question_to_sear
176177

177178

178179
def test_post_makes_correct_calls_to_openai_embeddings_to_embed_question_to_store_in_conversation_log(
179-
app_url: str, app_config: AppConfig, httpserver: HTTPServer
180+
app_url: str,
181+
app_config: AppConfig,
182+
httpserver: HTTPServer,
180183
):
181184
# when
182185
requests.post(f"{app_url}{path}", json=body)
@@ -649,9 +652,15 @@ def test_post_makes_correct_call_to_store_conversation_in_search(
649652
)
650653

651654

655+
@patch(
656+
"backend.batch.utilities.helpers.config.config_helper.ConfigHelper.get_active_config_or_default"
657+
)
652658
def test_post_returns_error_when_downstream_fails(
653-
app_url: str, app_config: AppConfig, httpserver: HTTPServer
659+
get_active_config_or_default_mock, app_url: str, httpserver: HTTPServer
654660
):
661+
get_active_config_or_default_mock.return_value.prompts.conversational_flow = (
662+
"custom"
663+
)
655664
httpserver.expect_oneshot_request(
656665
re.compile(".*"),
657666
).respond_with_json({}, status=403)

0 commit comments

Comments
 (0)