Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor/move-prompts-to-jinja-templates #2164

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 19 additions & 29 deletions app/backend/approaches/chatapproach.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
<<Are there exclusions for prescriptions?>>
<<Which pharmacies can be ordered from?>>
<<What is the limit for over-the-counter medication?>>
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
Expand All @@ -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)
Expand Down
14 changes: 6 additions & 8 deletions app/backend/approaches/chatreadretrieveread.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand Down Expand Up @@ -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
Expand Down
18 changes: 5 additions & 13 deletions app/backend/approaches/chatreadretrievereadvision.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:<file_name>
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,
Expand Down
10 changes: 10 additions & 0 deletions app/backend/approaches/prompts/ask/few_shots.json
Original file line number Diff line number Diff line change
@@ -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]."
}
5 changes: 5 additions & 0 deletions app/backend/approaches/prompts/ask/system_message.jinja
Original file line number Diff line number Diff line change
@@ -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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you install pre-commit, it should fix the new lines. See CONTRIBUTING.md for installation instructions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not entirely sure what you mean here. Could you clarify?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I notice that several files don't have newlines at the ends of the file, which usually means that the pre-commit hasn't run, as the pre-commit hooks fix that issue (and others). There are instructions for installing pre-commit hooks here:
https://github.com/Azure-Samples/azure-search-openai-demo/blob/main/CONTRIBUTING.md#setting-up-the-development-environment

Original file line number Diff line number Diff line change
@@ -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:<file_name>.
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.
7 changes: 7 additions & 0 deletions app/backend/approaches/prompts/chat/follow_up_questions.jinja
Original file line number Diff line number Diff line change
@@ -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:
<<Are there exclusions for prescriptions?>>
<<Which pharmacies can be ordered from?>>
<<What is the limit for over-the-counter medication?>>
Do not repeat questions that have already been asked.
Make sure the last question ends with ">>".
18 changes: 18 additions & 0 deletions app/backend/approaches/prompts/chat/query_few_shots.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
8 changes: 8 additions & 0 deletions app/backend/approaches/prompts/chat/query_template.jinja
Original file line number Diff line number Diff line change
@@ -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.
6 changes: 6 additions & 0 deletions app/backend/approaches/prompts/chat/system_message.jinja
Original file line number Diff line number Diff line change
@@ -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 }}
11 changes: 11 additions & 0 deletions app/backend/approaches/prompts/chat/system_message_vision.jinja
Original file line number Diff line number Diff line change
@@ -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:<file_name>
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}
45 changes: 24 additions & 21 deletions app/backend/approaches/retrievethenread.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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,
*,
Expand Down Expand Up @@ -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],
Expand Down Expand Up @@ -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,
Expand Down
15 changes: 5 additions & 10 deletions app/backend/approaches/retrievethenreadvision.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:<file_name> "
+ "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,
*,
Expand Down Expand Up @@ -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],
Expand Down