Skip to content

Commit e48ed98

Browse files
authored
Merge pull request #26 from Azure-Samples/pmishra/few-shot-classifier
bug fixes and move to few-shot classifier
2 parents 8194735 + 16d9519 commit e48ed98

34 files changed

+347
-777
lines changed

End_to_end_Solutions/AOAISearchDemo/README.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ The repo includes sample data so it's ready to try end to end. In this sample ap
99

1010
The experience allows users to ask questions about the Surface Devices specifications, troubleshooting help, warranty as well as sales, availability and trend related questions.
1111

12+
There are two pre-recorded voiceovers that shows how enterprises can use this architecture for their different users/audiences. The demo uses two different personas:
13+
> 1. Emma is marketing lead [demo](./docs/Emma%20Miller_with%20voice.mp4)
14+
> 2. Dave is regional sales manager [demo](./docs/Dave%20Huang_with%20voice.mp4)
15+
1216
![RAG Architecture](docs/appcomponents.png)
1317

1418
## Features
@@ -22,6 +26,7 @@ The experience allows users to ask questions about the Surface Devices specifica
2226
* Handling failures gracefully and ability to retry failed queries against other data sources
2327
* Handling token limitations
2428
* Using fine-tuned model for classification in the orchestrator
29+
* > *Due to unavailability of fine-tuned models in certain regions, we have updated the code to use gpt-4 based few-shot classifer. Added a new section on how to test this classifier in [promptFlow](./docs/prompt_flow.md)*
2530
* Using instrumentation for debugging and also for driving certain usage reports from the logs
2631

2732
![Chat screen](docs/chatscreen.png)
@@ -42,8 +47,11 @@ The experience allows users to ask questions about the Surface Devices specifica
4247
* **Important**: Ensure you can run `python --version` from console. On Ubuntu, you might need to run `sudo apt install python-is-python3` to link `python` to `python3`.
4348
* [Node.js](https://nodejs.org/en/download/)
4449
* [Git](https://git-scm.com/downloads)
45-
* [Powershell 7+ (pwsh)](https://github.com/powershell/powershell) - For Windows users only.
50+
* [PowerShell 7+ (pwsh)](https://github.com/powershell/powershell)
4651
* **Important**: Ensure you can run `pwsh.exe` from a PowerShell command. If this fails, you likely need to upgrade PowerShell.
52+
* [The AzureAD PowerShell module version 2.0.2.180 or above](https://learn.microsoft.com/en-us/powershell/module/azuread/?view=azureadps-2.0)
53+
* [ODBC Driver for SQL Server v18](https://learn.microsoft.com/en-us/sql/connect/odbc/download-odbc-driver-for-sql-server)
54+
4755

4856
>NOTE: Your Azure Account must have `Microsoft.Authorization/roleAssignments/write` permissions, such as [User Access Administrator](https://learn.microsoft.com/azure/role-based-access-control/built-in-roles#user-access-administrator) or [Owner](https://learn.microsoft.com/azure/role-based-access-control/built-in-roles#owner).
4957
@@ -66,6 +74,7 @@ Due to high demand, Azure OpenAI resources can be difficult to spin up on the fl
6674
- `AZURE_OPENAI_CLASSIFIER_MODEL {Name of Azure OpenAI model to be used to do dialog classification}`.
6775
- `AZURE_OPENAI_CLASSIFIER_DEPLOYMENT {Name of existing Azure OpenAI model deployment to be used for dialog classification}`.
6876
* Ensure the model you specify for `AZURE_OPENAI_DEPLOYMENT` and `AZURE_OPENAI_MODEL` is a Chat GPT model, since the demo utilizes the ChatCompletions API when requesting completions from this model.
77+
* Ensure the model you specify for `AZURE_OPENAI_CLASSIFIER_DEPLOYMENT` and `AZURE_OPENAI_CLASSIFIER_MODEL` is compatible with the Completions API, since the demo utilizes the Completions API when requesting completions from this model.
6978
* You can also use existing Search and Storage Accounts. See `./infra/main.parameters.json` for list of environment variables to pass to `azd env set` to configure those existing resources.
7079
2. Go to `app/backend/bot_config.yaml`. This file contains the model configuration definitions for the Azure OpenAI models that will be used. It defines request parameters like temperature, max_tokens, etc., as well as the the deployment name (`engine`) and model name (`model_name`) of the deployed models to use from your Azure OpenAI resource. These are broken down by task, so the request parameters and model for doing question classification on a user utterance can differ from those used to turn natural language into SQL for example. You will want the deployment name (`engine`) for the `approach_classifier` to match the one set for `AZURE_OPENAI_CLASSIFIER_DEPLOYMENT`. For the rest, you wil want the deployment name (`engine`) and model name (`model_name`) to match `AZURE_OPENAI_DEPLOYMENT` and `AZURE_OPENAI_MODEL` respectively. For the models which specify a `total_max_tokens`, you will want to set this value to the maximum number of tokens your deployed GPT model allows for a completions request. This will allow the backend service to know when prompts need to be trimmed to avoid a token limit error.
7180
* Note that the config for `approach_classifier` doesn't contain a system prompt, this is because the demo expects this model to be a fine-tuned GPT model rather than one trained using few-shot training. You will need to provide a fine-tuned model trained on some sample data for the dialog classification to work well. For more information on how to do this, checkout the [fine-tuning section](README.md#fine-tuning).
@@ -203,6 +212,10 @@ You can find helpful resources on how to fine-tune a model on the Azure OpenAI w
203212

204213
***Answer***: Yes, as part of the development of the application, we included some basic logging to capture what is happening around a user conversation. Application Insights was used as the logging backend. The [log reports](docs/log_reports.md) document has some sample KQL queries and reports based on these logs
205214

215+
***Question***: Are there suggestions on how to develop and test prompts
216+
217+
***Answer***: Yes, we have included documentation on how you could leverage Prompt Flow for developing and testing prompts. An example of developing and performing bulk test on few-shot classifier is included in the [prompt flow](docs/prompt_flow.md) document.
218+
206219
### Troubleshooting
207220

208221
If you see this error while running `azd deploy`: `read /tmp/azd1992237260/backend_env/lib64: is a directory`, then delete the `./app/backend/backend_env folder` and re-run the `azd deploy` command. This issue is being tracked here: <https://github.com/Azure/azure-dev/issues/1237>

End_to_end_Solutions/AOAISearchDemo/app/backend/.env.template

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ AZURE-OPENAI-CLASSIFIER-API-KEY=""
1717
# Azure Cognitive Search
1818
AZURE-SEARCH-SERVICE=""
1919
AZURE-SEARCH-INDEX=""
20+
AZURE-SEARCH-KEY=""
2021
KB-FIELDS-CONTENT="content"
2122
KB-FIELDS-CATEGORY="category"
2223
KB-FIELDS-SOURCEPAGE="sourcepage"

End_to_end_Solutions/AOAISearchDemo/app/backend/app.py

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,26 @@
11
import datetime
22
import json
33
import mimetypes
4-
import openai
54
import time
5+
6+
import openai
67
import yaml
78
from azure.core.credentials import AzureKeyCredential
89
from azure.identity import DefaultAzureCredential
910
from azure.search.documents import SearchClient
1011
from azure.storage.blob import BlobServiceClient
1112
from backend.approaches.approach_classifier import ApproachClassifier
12-
from backend.approaches.chatunstructured import ChatUnstructuredApproach
1313
from backend.approaches.chatstructured import ChatStructuredApproach
14-
from common.contracts.chat_session import ChatSession, ParticipantType, DialogClassification
14+
from backend.approaches.chatunstructured import ChatUnstructuredApproach
1515
from backend.config import DefaultConfig
1616
from backend.contracts.chat_response import Answer, ApproachType, ChatResponse
17-
from backend.contracts.error import OutOfScopeException, UnauthorizedDBAccessException
17+
from backend.contracts.error import (OutOfScopeException,
18+
UnauthorizedDBAccessException)
1819
from backend.data_client.data_client import DataClient
1920
from backend.utilities.access_management import AccessManager
20-
from flask import Flask, request, jsonify
21+
from common.contracts.chat_session import (ChatSession, DialogClassification,
22+
ParticipantType)
23+
from flask import Flask, jsonify, request
2124

2225
# Use the current user identity to authenticate with Azure OpenAI, Cognitive Search and Blob Storage (no secrets needed,
2326
# just use 'az login' locally, and managed identity when deployed on Azure). If you need to use keys, use separate AzureKeyCredential instances with the
@@ -44,9 +47,9 @@
4447
logger = DefaultConfig.logger
4548

4649
chat_approaches = {
47-
ApproachType.unstructured.value: ChatUnstructuredApproach(search_client, DefaultConfig.KB_FIELDS_SOURCEPAGE,
50+
ApproachType.unstructured.name: ChatUnstructuredApproach(search_client, DefaultConfig.KB_FIELDS_SOURCEPAGE,
4851
DefaultConfig.KB_FIELDS_CONTENT, logger, search_threshold_percentage = DefaultConfig.SEARCH_THRESHOLD_PERCENTAGE),
49-
ApproachType.structured.value: ChatStructuredApproach(DefaultConfig.SQL_CONNECTION_STRING, logger)
52+
ApproachType.structured.name: ChatStructuredApproach(DefaultConfig.SQL_CONNECTION_STRING, logger)
5053
}
5154

5255

@@ -129,11 +132,11 @@ def chat():
129132
if classification_override:
130133
approach_type = ApproachType(classification_override)
131134
else:
132-
openai.api_base = f"https://{DefaultConfig.AZURE_OPENAI_CLASSIFIER_SERVICE}.openai.azure.com"
133-
openai.api_key = DefaultConfig.AZURE_OPENAI_CLASSIFIER_API_KEY
135+
openai.api_base = f"https://{DefaultConfig.AZURE_OPENAI_GPT4_SERVICE}.openai.azure.com"
136+
openai.api_key = DefaultConfig.AZURE_OPENAI_GPT4_API_KEY
134137
approach_type = approach_classifier.run(history, bot_config)
135138

136-
logger.info(f"question_type: {approach_type.value}", extra=properties)
139+
logger.info(f"question_type: {approach_type.name}", extra=properties)
137140

138141
if approach_type == ApproachType.chit_chat:
139142
chit_chat_canned_response = "I'm sorry, but the question you've asked is outside my area of expertise. I'd be happy to help with any questions related to Microsoft Surface PCs and Laptops. Please feel free to ask about those, and I'll do my best to assist you!"
@@ -162,7 +165,7 @@ def chat():
162165
simplified_history = [{"participant_type": dialog.participant_type.value, "utterance": dialog.utterance} for dialog in filtered_chat_session.conversation]
163166
simplified_history.append({"participant_type": ParticipantType.user.value, "utterance": user_message})
164167

165-
impl = chat_approaches.get(approach_type.value)
168+
impl = chat_approaches.get(approach_type.name)
166169

167170
if not impl:
168171
return jsonify({"error": "unknown approach"}), 400
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
from backend.contracts.chat_response import ChatResponse
21
from typing import List
32

3+
from backend.contracts.chat_response import ChatResponse
4+
5+
46
class Approach:
57
def run(self, history: List[dict], overrides: dict) -> ChatResponse:
68
raise NotImplementedError
Lines changed: 52 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,59 @@
1+
from textwrap import dedent
2+
from typing import List
3+
14
import openai
25
from backend.approaches.approach import Approach
3-
from backend.config import DefaultConfig
46
from backend.contracts.chat_response import ApproachType
7+
from common.contracts.chat_session import DialogClassification
58
from common.logging.log_helper import CustomLogger
6-
from typing import List
9+
710

811
class ApproachClassifier(Approach):
912
def __init__(self, logger: CustomLogger):
1013
self.logger = logger
11-
14+
1215
def run(self, history: List[str], bot_config) -> ApproachType:
13-
response = openai.Completion.create(
14-
prompt=history[-1]['utterance'] + ' ->',
15-
**bot_config["approach_classifier"]["openai_settings"]
16+
17+
message_list = [
18+
{
19+
"role": "system",
20+
"content": dedent(bot_config["approach_classifier"]["system_prompt"])
21+
}
22+
]
23+
24+
if bot_config["approach_classifier"]["history"]["include"]:
25+
for message in history[-((bot_config["approach_classifier"]["history"]["length"]*2) + 1):]:
26+
if message["participant_type"] == "user":
27+
message_list.append(
28+
{"role": "user", "content": message["utterance"]})
29+
else:
30+
classification = ''
31+
if message['question_type'] == DialogClassification.structured_query.name:
32+
classification = ApproachType.structured.value
33+
elif message['question_type'] == DialogClassification.unstructured_query.name:
34+
classification = ApproachType.unstructured.value
35+
elif message['question_type'] == DialogClassification.chit_chat.name:
36+
classification = ApproachType.chit_chat.value
37+
else:
38+
classification = ApproachType.unstructured.value
39+
message_list.append(
40+
{"role": "assistant", "content": classification})
41+
else:
42+
message_list.append(
43+
{"role": "user", "content": history[-1]["utterance"]})
44+
try:
45+
response = openai.ChatCompletion.create(
46+
messages=message_list,
47+
**bot_config["approach_classifier"]["openai_settings"]
1648
)
49+
except openai.error.InvalidRequestError as e:
50+
self.logger.error(
51+
f"OpenAI API Error: {e.message}", exc_info=True)
52+
raise e
1753

18-
q :str = response['choices'][0]['text'].strip()
19-
self.log_aoai_response_details(f'Classification Prompt:{history[-1]["utterance"]}', f'Response: {q}', response)
54+
q: str = response['choices'][0]['message']['content']
55+
self.log_aoai_response_details(
56+
f'Classification Prompt:{history[-1]["utterance"]}', f'Response: {q}', response)
2057

2158
if q == "1":
2259
return ApproachType.structured
@@ -28,14 +65,15 @@ def run(self, history: List[str], bot_config) -> ApproachType:
2865
# Continuation: Return last question type from history if it exists
2966
if len(history) > 1:
3067
last_question_type = history[-2]['question_type']
31-
if last_question_type == "structured_query":
68+
if last_question_type == DialogClassification.structured_query.value:
3269
return ApproachType.structured
33-
elif last_question_type == "unstructured_query":
70+
elif last_question_type == DialogClassification.unstructured_query.value:
3471
return ApproachType.unstructured
35-
elif last_question_type == "chit_chat":
72+
elif last_question_type == DialogClassification.chit_chat.value:
3673
return ApproachType.chit_chat
3774
else:
38-
raise Exception(f"Unknown question type: {last_question_type}")
75+
raise Exception(
76+
f"Unknown question type: {last_question_type}")
3977
else:
4078
return ApproachType.unstructured
4179
else:
@@ -49,4 +87,5 @@ def log_aoai_response_details(self, prompt, result, aoai_response):
4987
"aoai_response[MS]": aoai_response.response_ms
5088
}
5189
addl_properties = self.logger.get_updated_properties(addl_dimensions)
52-
self.logger.info(f"prompt: {prompt}, response: {result}", extra=addl_properties)
90+
self.logger.info(
91+
f"prompt: {prompt}, response: {result}", extra=addl_properties)

0 commit comments

Comments
 (0)