diff --git a/app/backend/approaches/chatapproach.py b/app/backend/approaches/chatapproach.py index ea1857da3b..5499abd1d6 100644 --- a/app/backend/approaches/chatapproach.py +++ b/app/backend/approaches/chatapproach.py @@ -2,39 +2,27 @@ import re from abc import ABC, abstractmethod from typing import Any, AsyncGenerator, Optional +from jinja2 import Environment, FileSystemLoader from openai.types.chat import ChatCompletion, ChatCompletionMessageParam from approaches.approach import Approach - class ChatApproach(Approach, ABC): - query_prompt_few_shots: list[ChatCompletionMessageParam] = [ - {"role": "user", "content": "How did crypto do last year?"}, - {"role": "assistant", "content": "Summarize Cryptocurrency Market Dynamics from last year"}, - {"role": "user", "content": "What are my health plans?"}, - {"role": "assistant", "content": "Show available health plans"}, - ] + NO_RESPONSE = "0" - follow_up_questions_prompt_content = """Generate 3 very brief follow-up questions that the user would likely ask next. - Enclose the follow-up questions in double angle brackets. Example: - <> - <> - <> - Do no repeat questions that have already been asked. - Make sure the last question ends with ">>". - """ - - query_prompt_template = """Below is a history of the conversation so far, and a new question asked by the user that needs to be answered by searching in a knowledge base. - You have access to Azure AI Search index with 100's of documents. - Generate a search query based on the conversation and the new question. - Do not include cited source filenames and document names e.g info.txt or doc.pdf in the search query terms. - Do not include any text inside [] or <<>> in the search query terms. - Do not include any special characters like '+'. - If the question is not in English, translate the question to English before generating the search query. - If you cannot generate a search query, return just the number 0. - """ + def __init__(self): + self._initialize_templates() + + def _initialize_templates(self): + self.env = Environment(loader=FileSystemLoader('approaches/prompts/chat')) + json_content = self.env.loader.get_source(self.env, 'query_few_shots.json')[0] + self.query_prompt_few_shots: list[ChatCompletionMessageParam] = json.loads(json_content) + self.query_prompt_template = self.env.get_template('query_template.jinja').render() + self.follow_up_questions_prompt = self.env.get_template('follow_up_questions.jinja').render() + self.system_message_chat_conversation_template = self.env.get_template('system_message.jinja') + self.system_message_chat_conversation_vision_template = self.env.get_template('system_message_vision.jinja') @property @abstractmethod @@ -47,12 +35,14 @@ async def run_until_final_call(self, messages, overrides, auth_claims, should_st def get_system_prompt(self, override_prompt: Optional[str], follow_up_questions_prompt: str) -> str: if override_prompt is None: - return self.system_message_chat_conversation.format( - injected_prompt="", follow_up_questions_prompt=follow_up_questions_prompt + return self.system_message_chat_conversation_template.render( + follow_up_questions_prompt=follow_up_questions_prompt, + injected_prompt="" ) elif override_prompt.startswith(">>>"): - return self.system_message_chat_conversation.format( - injected_prompt=override_prompt[3:] + "\n", follow_up_questions_prompt=follow_up_questions_prompt + return self.system_message_chat_conversation_template.render( + follow_up_questions_prompt=follow_up_questions_prompt, + injected_prompt=override_prompt[3:] + "\n" ) else: return override_prompt.format(follow_up_questions_prompt=follow_up_questions_prompt) diff --git a/app/backend/approaches/chatreadretrieveread.py b/app/backend/approaches/chatreadretrieveread.py index b752547e71..2780da3255 100644 --- a/app/backend/approaches/chatreadretrieveread.py +++ b/app/backend/approaches/chatreadretrieveread.py @@ -39,6 +39,7 @@ def __init__( query_language: str, query_speller: str, ): + super().__init__() self.search_client = search_client self.openai_client = openai_client self.auth_helper = auth_helper @@ -55,13 +56,10 @@ def __init__( @property def system_message_chat_conversation(self): - return """Assistant helps the company employees with their healthcare plan questions, and questions about the employee handbook. Be brief in your answers. - 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. - If the question is not in English, answer in the language used in the question. - 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, for example [info1.txt]. Don't combine sources, list each source separately, for example [info1.txt][info2.pdf]. - {follow_up_questions_prompt} - {injected_prompt} - """ + return self.system_message_chat_conversation_template.render( + follow_up_questions_prompt="", + injected_prompt="" + ) @overload async def run_until_final_call( @@ -177,7 +175,7 @@ async def run_until_final_call( # Allow client to replace the entire prompt, or to inject into the exiting prompt using >>> system_message = self.get_system_prompt( overrides.get("prompt_template"), - self.follow_up_questions_prompt_content if overrides.get("suggest_followup_questions") else "", + self.follow_up_questions_prompt_content if overrides.get("suggest_followup_questions") else "" ) response_token_limit = 1024 diff --git a/app/backend/approaches/chatreadretrievereadvision.py b/app/backend/approaches/chatreadretrievereadvision.py index 6b48643077..075243961c 100644 --- a/app/backend/approaches/chatreadretrievereadvision.py +++ b/app/backend/approaches/chatreadretrievereadvision.py @@ -46,6 +46,7 @@ def __init__( vision_endpoint: str, vision_token_provider: Callable[[], Awaitable[str]] ): + super().__init__() self.search_client = search_client self.blob_container_client = blob_container_client self.openai_client = openai_client @@ -67,19 +68,10 @@ def __init__( @property def system_message_chat_conversation(self): - return """ - You are an intelligent assistant helping analyze the Annual Financial Report of Contoso Ltd., The documents contain text, graphs, tables and images. - Each image source has the file name in the top left corner of the image with coordinates (10,10) pixels and is in the format SourceFileName: - Each text source starts in a new line and has the file name followed by colon and the actual information - Always include the source name from the image or text for each fact you use in the response in the format: [filename] - Answer the following question using only the data provided in the sources below. - If asking a clarifying question to the user would help, ask the question. - Be brief in your answers. - The text and image source can be the same file name, don't use the image title when citing the image source, only use the file name as mentioned - If you cannot answer using the sources below, say you don't know. Return just the answer without any input texts. - {follow_up_questions_prompt} - {injected_prompt} - """ + return self.system_message_chat_conversation_vision_template.render( + follow_up_questions_prompt="", + injected_prompt="" + ) async def run_until_final_call( self, diff --git a/app/backend/approaches/prompts/ask/few_shots.json b/app/backend/approaches/prompts/ask/few_shots.json new file mode 100644 index 0000000000..3413ba1f25 --- /dev/null +++ b/app/backend/approaches/prompts/ask/few_shots.json @@ -0,0 +1,10 @@ +{ + "question": "What is the deductible for the employee plan for a visit to Overlake in Bellevue?", + "sources": { + "info1.txt": "deductibles depend on whether you are in-network or out-of-network. In-network deductibles are $500 for employee and $1000 for family. Out-of-network deductibles are $1000 for employee and $2000 for family.", + "info2.pdf": "Overlake is in-network for the employee plan.", + "info3.pdf": "Overlake is the name of the area that includes a park and ride near Bellevue.", + "info4.pdf": "In-network institutions include Overlake, Swedish and others in the region." + }, + "answer": "In-network deductibles are $500 for employee and $1000 for family [info1.txt] and Overlake is in-network for the employee plan [info2.pdf][info4.pdf]." +} \ No newline at end of file diff --git a/app/backend/approaches/prompts/ask/system_message.jinja b/app/backend/approaches/prompts/ask/system_message.jinja new file mode 100644 index 0000000000..6873dec18a --- /dev/null +++ b/app/backend/approaches/prompts/ask/system_message.jinja @@ -0,0 +1,5 @@ +You are an intelligent assistant helping Contoso Inc employees with their healthcare plan questions and employee handbook questions. +Use 'you' to refer to the individual asking the questions even if they ask with 'I'. +Answer the following question using only the data provided in the sources below. +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. +If you cannot answer using the sources below, say you don't know. Use below example to answer \ No newline at end of file diff --git a/app/backend/approaches/prompts/ask/system_message_vision.jinja b/app/backend/approaches/prompts/ask/system_message_vision.jinja new file mode 100644 index 0000000000..963b137ed3 --- /dev/null +++ b/app/backend/approaches/prompts/ask/system_message_vision.jinja @@ -0,0 +1,7 @@ +You are an intelligent assistant helping analyze the Annual Financial Report of Contoso Ltd., The documents contain text, graphs, tables and images. +Each image source has the file name in the top left corner of the image with coordinates (10,10) pixels and is in the format SourceFileName:. +Each text source starts in a new line and has the file name followed by colon and the actual information. +Always include the source name from the image or text for each fact you use in the response in the format: [filename]. +Answer the following question using only the data provided in the sources below. +The text and image source can be the same file name, don't use the image title when citing the image source, only use the file name as mentioned. +If you cannot answer using the sources below, say you don't know. Return just the answer without any input texts. \ No newline at end of file diff --git a/app/backend/approaches/prompts/chat/follow_up_questions.jinja b/app/backend/approaches/prompts/chat/follow_up_questions.jinja new file mode 100644 index 0000000000..1eaf118252 --- /dev/null +++ b/app/backend/approaches/prompts/chat/follow_up_questions.jinja @@ -0,0 +1,7 @@ +Generate 3 very brief follow-up questions that the user would likely ask next. +Enclose the follow-up questions in double angle brackets. Example: +<> +<> +<> +Do not repeat questions that have already been asked. +Make sure the last question ends with ">>". \ No newline at end of file diff --git a/app/backend/approaches/prompts/chat/query_few_shots.json b/app/backend/approaches/prompts/chat/query_few_shots.json new file mode 100644 index 0000000000..f22ea73a5f --- /dev/null +++ b/app/backend/approaches/prompts/chat/query_few_shots.json @@ -0,0 +1,18 @@ +[ + { + "role": "user", + "content": "How did crypto do last year?" + }, + { + "role": "assistant", + "content": "Summarize Cryptocurrency Market Dynamics from last year" + }, + { + "role": "user", + "content": "What are my health plans?" + }, + { + "role": "assistant", + "content": "Show available health plans" + } +] diff --git a/app/backend/approaches/prompts/chat/query_template.jinja b/app/backend/approaches/prompts/chat/query_template.jinja new file mode 100644 index 0000000000..d9ecd59d0b --- /dev/null +++ b/app/backend/approaches/prompts/chat/query_template.jinja @@ -0,0 +1,8 @@ +Below is a history of the conversation so far, and a new question asked by the user that needs to be answered by searching in a knowledge base. +You have access to Azure AI Search index with 100's of documents. +Generate a search query based on the conversation and the new question. +Do not include cited source filenames and document names e.g. info.txt or doc.pdf in the search query terms. +Do not include any text inside [] or <<>> in the search query terms. +Do not include any special characters like '+'. +If the question is not in English, translate the question to English before generating the search query. +If you cannot generate a search query, return just the number 0. diff --git a/app/backend/approaches/prompts/chat/system_message.jinja b/app/backend/approaches/prompts/chat/system_message.jinja new file mode 100644 index 0000000000..022d6a1343 --- /dev/null +++ b/app/backend/approaches/prompts/chat/system_message.jinja @@ -0,0 +1,6 @@ +Assistant helps the company employees with their healthcare plan questions, and questions about the employee handbook. Be brief in your answers. +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. +If the question is not in English, answer in the language used in the question. +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, for example [info1.txt]. Don't combine sources, list each source separately, for example [info1.txt][info2.pdf]. +{{ follow_up_questions_prompt }} +{{ injected_prompt }} \ No newline at end of file diff --git a/app/backend/approaches/prompts/chat/system_message_vision.jinja b/app/backend/approaches/prompts/chat/system_message_vision.jinja new file mode 100644 index 0000000000..2747f32db7 --- /dev/null +++ b/app/backend/approaches/prompts/chat/system_message_vision.jinja @@ -0,0 +1,11 @@ +You are an intelligent assistant helping analyze the Annual Financial Report of Contoso Ltd., The documents contain text, graphs, tables and images. +Each image source has the file name in the top left corner of the image with coordinates (10,10) pixels and is in the format SourceFileName: +Each text source starts in a new line and has the file name followed by colon and the actual information +Always include the source name from the image or text for each fact you use in the response in the format: [filename] +Answer the following question using only the data provided in the sources below. +If asking a clarifying question to the user would help, ask the question. +Be brief in your answers. +The text and image source can be the same file name, don't use the image title when citing the image source, only use the file name as mentioned +If you cannot answer using the sources below, say you don't know. Return just the answer without any input texts. +{follow_up_questions_prompt} +{injected_prompt} \ No newline at end of file diff --git a/app/backend/approaches/retrievethenread.py b/app/backend/approaches/retrievethenread.py index 5c73def39e..dcd6a70f53 100644 --- a/app/backend/approaches/retrievethenread.py +++ b/app/backend/approaches/retrievethenread.py @@ -1,10 +1,14 @@ from typing import Any, Optional +import json +import re +import ast from azure.search.documents.aio import SearchClient from azure.search.documents.models import VectorQuery from openai import AsyncOpenAI from openai.types.chat import ChatCompletionMessageParam from openai_messages_token_helper import build_messages, get_token_limit +from jinja2 import Environment, FileSystemLoader from approaches.approach import Approach, ThoughtStep from core.authentication import AuthenticationHelper @@ -17,26 +21,6 @@ class RetrieveThenReadApproach(Approach): (answer) with that prompt. """ - system_chat_template = ( - "You are an intelligent assistant helping Contoso Inc employees with their healthcare plan questions and employee handbook questions. " - + "Use 'you' to refer to the individual asking the questions even if they ask with 'I'. " - + "Answer the following question using only the data provided in the sources below. " - + "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. " - + "If you cannot answer using the sources below, say you don't know. Use below example to answer" - ) - - # shots/sample conversation - question = """ -'What is the deductible for the employee plan for a visit to Overlake in Bellevue?' - -Sources: -info1.txt: deductibles depend on whether you are in-network or out-of-network. In-network deductibles are $500 for employee and $1000 for family. Out-of-network deductibles are $1000 for employee and $2000 for family. -info2.pdf: Overlake is in-network for the employee plan. -info3.pdf: Overlake is the name of the area that includes a park and ride near Bellevue. -info4.pdf: In-network institutions include Overlake, Swedish and others in the region -""" - answer = "In-network deductibles are $500 for employee and $1000 for family [info1.txt] and Overlake is in-network for the employee plan [info2.pdf][info4.pdf]." - def __init__( self, *, @@ -68,6 +52,14 @@ def __init__( self.query_speller = query_speller self.chatgpt_token_limit = get_token_limit(chatgpt_model, self.ALLOW_NON_GPT_MODELS) + self._initialize_templates() + + def _initialize_templates(self): + self.env = Environment(loader=FileSystemLoader('approaches/prompts/ask')) + self.system_chat_template = self.env.get_template('system_message.jinja').render() + json_content = self.env.loader.get_source(self.env, 'few_shots.json')[0] + self.few_shots = json.loads(json_content) + async def run( self, messages: list[ChatCompletionMessageParam], @@ -114,11 +106,22 @@ async def run( content = "\n".join(sources_content) user_content = q + "\n" + f"Sources:\n {content}" + few_shots = [ + { + "role": "user", + "content": f"{self.few_shots['question']}\nSources:\n" + "\n".join([f"{k}: {v}" for k, v in self.few_shots['sources'].items()]) + }, + { + "role": "assistant", + "content": self.few_shots["answer"] + } + ] + response_token_limit = 1024 updated_messages = build_messages( model=self.chatgpt_model, system_prompt=overrides.get("prompt_template", self.system_chat_template), - few_shots=[{"role": "user", "content": self.question}, {"role": "assistant", "content": self.answer}], + few_shots=[{"role": "user", "content": self.few_shots["question"]}, {"role": "assistant", "content": self.few_shots["answer"]}], new_user_content=user_content, max_tokens=self.chatgpt_token_limit - response_token_limit, fallback_to_default=self.ALLOW_NON_GPT_MODELS, diff --git a/app/backend/approaches/retrievethenreadvision.py b/app/backend/approaches/retrievethenreadvision.py index cd0bf0d08d..cf8b0bda91 100644 --- a/app/backend/approaches/retrievethenreadvision.py +++ b/app/backend/approaches/retrievethenreadvision.py @@ -9,6 +9,7 @@ ChatCompletionMessageParam, ) from openai_messages_token_helper import build_messages, get_token_limit +from jinja2 import Environment, FileSystemLoader from approaches.approach import Approach, ThoughtStep from core.authentication import AuthenticationHelper @@ -22,16 +23,6 @@ class RetrieveThenReadVisionApproach(Approach): (answer) with that prompt. """ - system_chat_template_gpt4v = ( - "You are an intelligent assistant helping analyze the Annual Financial Report of Contoso Ltd., The documents contain text, graphs, tables and images. " - + "Each image source has the file name in the top left corner of the image with coordinates (10,10) pixels and is in the format SourceFileName: " - + "Each text source starts in a new line and has the file name followed by colon and the actual information " - + "Always include the source name from the image or text for each fact you use in the response in the format: [filename] " - + "Answer the following question using only the data provided in the sources below. " - + "The text and image source can be the same file name, don't use the image title when citing the image source, only use the file name as mentioned " - + "If you cannot answer using the sources below, say you don't know. Return just the answer without any input texts " - ) - def __init__( self, *, @@ -68,6 +59,10 @@ def __init__( self.vision_token_provider = vision_token_provider self.gpt4v_token_limit = get_token_limit(gpt4v_model, self.ALLOW_NON_GPT_MODELS) + self.env = Environment(loader=FileSystemLoader('approaches/prompts/ask')) + self.system_chat_template_gpt4v = self.env.get_template('system_message_vision.jinja').render() + + async def run( self, messages: list[ChatCompletionMessageParam],