Skip to content

Commit 4063ad6

Browse files
authored
Merge pull request #385 from Azure-Samples/srbalakr/chatCompletion5.7
Upgrade sample to use ChatCompletion API
2 parents 889f77b + 34221ef commit 4063ad6

File tree

3 files changed

+88
-24
lines changed

3 files changed

+88
-24
lines changed

app/backend/app.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
AZURE_OPENAI_SERVICE = os.environ.get("AZURE_OPENAI_SERVICE") or "myopenai"
2222
AZURE_OPENAI_GPT_DEPLOYMENT = os.environ.get("AZURE_OPENAI_GPT_DEPLOYMENT") or "davinci"
2323
AZURE_OPENAI_CHATGPT_DEPLOYMENT = os.environ.get("AZURE_OPENAI_CHATGPT_DEPLOYMENT") or "chat"
24+
AZURE_OPENAI_CHATGPT_MODEL = os.environ.get("AZURE_OPENAI_CHATGPT_MODEL") or "gpt-35-turbo"
2425

2526
KB_FIELDS_CONTENT = os.environ.get("KB_FIELDS_CONTENT") or "content"
2627
KB_FIELDS_CATEGORY = os.environ.get("KB_FIELDS_CATEGORY") or "category"
@@ -35,7 +36,7 @@
3536
# Used by the OpenAI SDK
3637
openai.api_type = "azure"
3738
openai.api_base = f"https://{AZURE_OPENAI_SERVICE}.openai.azure.com"
38-
openai.api_version = "2022-12-01"
39+
openai.api_version = "2023-05-15"
3940

4041
# Comment these two lines out if using keys, set your API key in the OPENAI_API_KEY environment variable instead
4142
openai.api_type = "azure_ad"
@@ -61,7 +62,7 @@
6162
}
6263

6364
chat_approaches = {
64-
"rrr": ChatReadRetrieveReadApproach(search_client, AZURE_OPENAI_CHATGPT_DEPLOYMENT, AZURE_OPENAI_GPT_DEPLOYMENT, KB_FIELDS_SOURCEPAGE, KB_FIELDS_CONTENT)
65+
"rrr": ChatReadRetrieveReadApproach(search_client, AZURE_OPENAI_CHATGPT_DEPLOYMENT, AZURE_OPENAI_CHATGPT_MODEL, AZURE_OPENAI_GPT_DEPLOYMENT, KB_FIELDS_SOURCEPAGE, KB_FIELDS_CONTENT)
6566
}
6667

6768
app = Flask(__name__)

app/backend/approaches/chatreadretrieveread.py

Lines changed: 83 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,30 @@
11
from typing import Any, Sequence
22

33
import openai
4+
import tiktoken
45
from azure.search.documents import SearchClient
56
from azure.search.documents.models import QueryType
67
from approaches.approach import Approach
78
from text import nonewlines
89

910
class ChatReadRetrieveReadApproach(Approach):
11+
# Chat roles
12+
SYSTEM = "system"
13+
USER = "user"
14+
ASSISTANT = "assistant"
15+
1016
"""
1117
Simple retrieve-then-read implementation, using the Cognitive Search and OpenAI APIs directly. It first retrieves
1218
top documents from search, then constructs a prompt with them, and then uses OpenAI to generate an completion
1319
(answer) with that prompt.
1420
"""
15-
16-
prompt_prefix = """<|im_start|>system
17-
Assistant helps the company employees with their healthcare plan questions, and questions about the employee handbook. Be brief in your answers.
21+
system_message_chat_conversation = """Assistant helps the company employees with their healthcare plan questions, and questions about the employee handbook. Be brief in your answers.
1822
Answer ONLY with the facts listed in the list of sources below. If there isn't enough information below, say you don't know. Do not generate answers that don't use the sources below. If asking a clarifying question to the user would help, ask the question.
1923
For tabular information return it as an html table. Do not return markdown format.
2024
Each source has a name followed by colon and the actual information, always include the source name for each fact you use in the response. Use square brackets to reference the source, e.g. [info1.txt]. Don't combine sources, list each source separately, e.g. [info1.txt][info2.pdf].
2125
{follow_up_questions_prompt}
2226
{injected_prompt}
23-
Sources:
24-
{sources}
25-
<|im_end|>
26-
{chat_history}
2727
"""
28-
2928
follow_up_questions_prompt_content = """Generate three very brief follow-up questions that the user would likely ask next about their healthcare plan and employee handbook.
3029
Use double angle brackets to reference the questions, e.g. <<Are there exclusions for prescriptions?>>.
3130
Try not to repeat questions that have already been asked.
@@ -46,9 +45,10 @@ class ChatReadRetrieveReadApproach(Approach):
4645
Search query:
4746
"""
4847

49-
def __init__(self, search_client: SearchClient, chatgpt_deployment: str, gpt_deployment: str, sourcepage_field: str, content_field: str):
48+
def __init__(self, search_client: SearchClient, chatgpt_deployment: str, chatgpt_model: str, gpt_deployment: str, sourcepage_field: str, content_field: str):
5049
self.search_client = search_client
5150
self.chatgpt_deployment = chatgpt_deployment
51+
self.chatgpt_model = chatgpt_model
5252
self.gpt_deployment = gpt_deployment
5353
self.sourcepage_field = sourcepage_field
5454
self.content_field = content_field
@@ -92,23 +92,20 @@ def run(self, history: Sequence[dict[str, str]], overrides: dict[str, Any]) -> A
9292

9393
# Allow client to replace the entire prompt, or to inject into the exiting prompt using >>>
9494
prompt_override = overrides.get("prompt_template")
95-
if prompt_override is None:
96-
prompt = self.prompt_prefix.format(injected_prompt="", sources=content, chat_history=self.get_chat_history_as_text(history), follow_up_questions_prompt=follow_up_questions_prompt)
97-
elif prompt_override.startswith(">>>"):
98-
prompt = self.prompt_prefix.format(injected_prompt=prompt_override[3:] + "\n", sources=content, chat_history=self.get_chat_history_as_text(history), follow_up_questions_prompt=follow_up_questions_prompt)
99-
else:
100-
prompt = prompt_override.format(sources=content, chat_history=self.get_chat_history_as_text(history), follow_up_questions_prompt=follow_up_questions_prompt)
95+
messages = self.get_messages_from_history(prompt_override=prompt_override, follow_up_questions_prompt=follow_up_questions_prompt,history=history, sources=content)
10196

10297
# STEP 3: Generate a contextual and content specific answer using the search results and chat history
103-
completion = openai.Completion.create(
104-
engine=self.chatgpt_deployment,
105-
prompt=prompt,
98+
chat_completion = openai.ChatCompletion.create(
99+
deployment_id=self.chatgpt_deployment,
100+
model=self.chatgpt_model,
101+
messages=messages,
106102
temperature=overrides.get("temperature") or 0.7,
107103
max_tokens=1024,
108-
n=1,
109-
stop=["<|im_end|>", "<|im_start|>"])
104+
n=1)
105+
106+
chat_content = chat_completion.choices[0].message.content
110107

111-
return {"data_points": results, "answer": completion.choices[0].text, "thoughts": f"Searched for:<br>{q}<br><br>Prompt:<br>" + prompt.replace('\n', '<br>')}
108+
return {"data_points": results, "answer": chat_content, "thoughts": f"Searched for:<br>{q}<br><br>Prompt:<br>" + prompt.replace('\n', '<br>')}
112109

113110
def get_chat_history_as_text(self, history: Sequence[dict[str, str]], include_last_turn: bool=True, approx_max_tokens: int=1000) -> str:
114111
history_text = ""
@@ -117,3 +114,68 @@ def get_chat_history_as_text(self, history: Sequence[dict[str, str]], include_la
117114
if len(history_text) > approx_max_tokens*4:
118115
break
119116
return history_text
117+
118+
def get_messages_from_history(self, prompt_override, follow_up_questions_prompt, history: Sequence[dict[str, str]], sources: str, approx_max_tokens: int = 1000) -> []:
119+
'''
120+
Generate messages needed for chat Completion api
121+
'''
122+
messages = []
123+
token_count = 0
124+
if prompt_override is None:
125+
system_message = self.system_message_chat_conversation.format(injected_prompt="", follow_up_questions_prompt=follow_up_questions_prompt)
126+
elif prompt_override.startswith(">>>"):
127+
system_message = self.system_message_chat_conversation.format(injected_prompt=prompt_override[3:] + "\n", follow_up_questions_prompt=follow_up_questions_prompt)
128+
else:
129+
system_message = prompt_override.format(follow_up_questions_prompt=follow_up_questions_prompt)
130+
131+
messages.append({"role":self.SYSTEM, "content": system_message})
132+
token_count += self.num_tokens_from_messages(messages[-1], self.chatgpt_model)
133+
134+
# latest conversation
135+
user_content = history[-1]["user"] + " \nSources:" + sources
136+
messages.append({"role": self.USER, "content": user_content})
137+
token_count += token_count + self.num_tokens_from_messages(messages[-1], self.chatgpt_model)
138+
139+
'''
140+
Enqueue in reverse order
141+
if limit exceeds truncate old messages
142+
leaving system message behind
143+
Keep track of token count for each conversation
144+
If token count exceeds limit, break
145+
'''
146+
for h in reversed(history[:-1]):
147+
if h.get("bot"):
148+
messages.insert(1, {"role": self.ASSISTANT, "content" : h.get("bot")})
149+
token_count += self.num_tokens_from_messages(messages[1], self.chatgpt_model)
150+
messages.insert(1, {"role": self.USER, "content" : h.get("user")})
151+
token_count += self.num_tokens_from_messages(messages[1], self.chatgpt_model)
152+
if token_count > approx_max_tokens*4:
153+
break
154+
return messages
155+
156+
def num_tokens_from_messages(self, message: dict[str,str], model: str) -> int:
157+
"""
158+
Calculate the number of tokens required to encode a message.
159+
Args:
160+
message (dict): The message to encode, represented as a dictionary.
161+
model (str): The name of the model to use for encoding.
162+
Returns:
163+
int: The total number of tokens required to encode the message.
164+
Example:
165+
message = {'role': 'user', 'content': 'Hello, how are you?'}
166+
model = 'gpt-3.5-turbo'
167+
num_tokens_from_messages(message, model)
168+
output: 11
169+
"""
170+
encoding = tiktoken.encoding_for_model(self.get_oai_chatmodel_tiktok(model))
171+
num_tokens = 0
172+
num_tokens += 2 # For "role" and "content" keys
173+
for key, value in message.items():
174+
num_tokens += len(encoding.encode(value))
175+
return num_tokens
176+
177+
def get_oai_chatmodel_tiktok(self, aoaimodel: str):
178+
if aoaimodel == "" or aoaimodel is None:
179+
raise Exception("Expected AOAI chatGPT model name")
180+
181+
return "gpt-3.5-turbo" if aoaimodel == "gpt-35-turbo" else aoaimodel

app/backend/requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
azure-identity==1.13.0
22
Flask==2.2.5
33
langchain==0.0.187
4-
openai==0.26.4
4+
openai==0.27.8
5+
tiktoken==0.3.0
56
azure-search-documents==11.4.0b3
67
azure-storage-blob==12.14.1

0 commit comments

Comments
 (0)