This script integrates AWS services like DynamoDB and Bedrock LLM with LangChain to create an educational chatbot that can engage with students, ask questions, provide answers, and track student progress toward mastery of a topic. It also includes history-aware functionality, which uses chat history to provide relevant context during conversations.
- boto3: AWS SDK to interact with services like DynamoDB and manage resources.
- re: The re library in Python is used for working with regular expressions, which are sequences of characters that form search patterns.
- ChatBedrock: Interface for interacting with AWS Bedrock LLM.
- ChatPromptTemplate, MessagesPlaceholder: Templates for setting up prompts in LangChain with chat history awareness.
- create_stuff_documents_chain, create_retrieval_chain: LangChain utilities to combine document chains and retrieval chains for context-aware question-answering.
- RunnableWithMessageHistory: Allows the inclusion of chat history in the reasoning chain.
- DynamoDBChatMessageHistory: Stores chat history in DynamoDB.
- DynamoDB: Used to store and retrieve session history for conversations between the student and the chatbot.
- ChatBedrock: Used to interact with AWS Bedrock LLM for generating responses and engaging with the student.
- create_dynamodb_history_table: Creates a DynamoDB table to store chat session history if it doesn't already exist.
- get_bedrock_llm: Retrieves an instance of the Bedrock LLM based on a provided model ID.
- get_student_query: Formats a student's query into a structured template suitable for processing.
- get_initial_student_query: Generates an initial prompt for a student to greet the chatbot and request a question on a specific topic.
- get_response: Manages the interaction between the student query, the Bedrock LLM, and the history-aware retriever to generate responses.
- get_llm_output: Processes the output from the LLM and checks if the student's competency has been achieved.
- DynamoDB Table Creation: The
create_dynamodb_history_tablefunction ensures that a DynamoDB table is available to store session history. - Query Processing: The
get_student_queryandget_initial_student_queryfunctions format student queries for processing. - Response Generation: The
get_responsefunction uses the Bedrock LLM and chat history to generate responses to student queries and evaluates the student's progress toward mastering the topic. - Competency Evaluation: The
get_llm_outputfunction checks if the LLM response indicates that the student has mastered the topic.
def create_dynamodb_history_table(table_name: str) -> None:
# Get the service resource and client.
dynamodb_resource = boto3.resource("dynamodb")
dynamodb_client = boto3.client("dynamodb")
# Retrieve the list of tables that currently exist.
existing_tables = dynamodb_client.list_tables()['TableNames']
if table_name not in existing_tables: # Create a new table if it doesn't exist.
# Create the DynamoDB table.
table = dynamodb_resource.create_table(
TableName=table_name,
KeySchema=[{"AttributeName": "SessionId", "KeyType": "HASH"}],
AttributeDefinitions=[{"AttributeName": "SessionId", "AttributeType": "S"}],
BillingMode="PAY_PER_REQUEST",
)
# Wait until the table exists.
table.meta.client.get_waiter("table_exists").wait(TableName=table_name)Creates a DynamoDB table to store the chat session history if the table doesn't already exist.
- Check Existing Tables: Retrieves the list of existing DynamoDB tables.
- Table Creation: If the specified table does not exist, creates it with a
SessionIdkey schema and sets up pay-per-request billing mode. - Wait for Table: Waits for the table creation to complete before returning.
-
Inputs:
table_name: The name of the DynamoDB table to create.
-
Outputs:
- No return value. The function ensures that the specified table exists.
def get_bedrock_llm(
bedrock_llm_id: str,
temperature: float = 0
) -> ChatBedrock:
return ChatBedrock(
model_id=bedrock_llm_id,
model_kwargs=dict(temperature=temperature),
)Retrieves a Bedrock LLM instance based on the provided model ID, with optional control over response randomness through the temperature parameter.
- Create LLM Instance: Initializes a
ChatBedrockinstance with the specified model ID and temperature setting. - Return LLM: Returns the initialized LLM instance.
-
Inputs:
bedrock_llm_id: The model ID for the Bedrock LLM.temperature: Controls the randomness of the LLM's responses (default is 0 for deterministic outputs).
-
Outputs:
- Returns a
ChatBedrockinstance.
- Returns a
def get_student_query(raw_query: str) -> str:
student_query = f"""
user
{raw_query}
"""
return student_queryFormats a raw student query into a structured template suitable for further processing by the LLM.
- Format Query: Wraps the student's query with the
userlabel to structure it for the LLM. - Return Formatted Query: Returns the structured query.
-
Inputs:
raw_query: The raw query input from the student.
-
Outputs:
- Returns the formatted query string.
def get_initial_student_query(topic: str) -> str:
student_query = f"""
user
Greet me and then ask me a question related to the topic: {topic}.
"""
return student_queryGenerates an initial prompt asking the student to greet the system and pose a question related to a specific topic.
- Generate Initial Query: Constructs a query asking the student to greet the system and inquire about a specific topic.
- Return Query: Returns the generated query.
-
Inputs:
topic: The topic for which the initial question should be generated.
-
Outputs:
- Returns the formatted initial query string.
def get_response(
query: str,
topic: str,
llm: ChatBedrock,
history_aware_retriever,
table_name: str,
session_id: str
) -> dict:
# Create a system prompt for the question answering
system_prompt = (
""
"system"
"You are an instructor for a course. "
f"Your job is to help the student master the topic: {topic}. \n"
"Engage with the student by asking questions and conversing with them to identify any gaps in their understanding of the topic. If you identify gaps, address these gaps by providing explanations, answering the student's questions, and referring to the relevant context to help the student gain a comprehensive understanding of the topic. "
"Continue this process until you determine that the student has mastered the topic. \nOnce mastery is achieved, include COMPETENCY ACHIEVED in your response and do not ask any further questions about the topic. "
"Use the following pieces of retrieved context to answer "
"a question asked by the student. Use three sentences maximum and keep the "
"answer concise. End each answer with a question that tests the student's knowledge about the topic."
""
"documents"
"{context}"
""
"assistant"
)
qa_prompt = ChatPromptTemplate.from_messages(
[
("system", system_prompt),
MessagesPlaceholder("chat_history"),
("human", "{input}"),
]
)
question_answer_chain = create_stuff_documents_chain(llm, qa_prompt)
rag_chain = create_retrieval_chain(history_aware_retriever, question_answer_chain)
conversational_rag_chain = RunnableWithMessageHistory(
rag_chain,
lambda session_id: DynamoDBChatMessageHistory(
table_name=table_name,
session_id=session_id
),
input_messages_key="input",
history_messages_key="chat_history",
output_messages_key="answer",
)
# Generate the response
response = conversational_rag_chain.invoke(
{
"input": query
},
config={
"configurable": {"session_id": session_id}
}, # constructs a key "session_id" in `store`.
)["answer"]
return get_llm_output(response)Generates a response to the student's query using the LLM and a history-aware retriever, incorporating context from previous conversations stored in DynamoDB.
- Prompt Setup: Creates a system prompt instructing the LLM to help the student master a specific topic, engaging them in conversation and filling gaps in their understanding.
- Contextual Question Answering: Uses a retrieval chain to fetch relevant documents based on the student's query and chat history.
- Chat History Handling: The conversation history is managed using
DynamoDBChatMessageHistoryfor the specific session. - Generate Response: Generates the response using the LLM and returns the result.
-
Inputs:
query: The student's query string.topic: The topic the student is learning about.llm: The Bedrock LLM instance.history_aware_retriever: The retriever providing relevant documents for the query.table_name: DynamoDB table name used to store the chat history.session_id: Unique identifier for the chat session.
-
Outputs:
- Returns a dictionary containing the response and the source documents used for retrieval.
def split_into_sentences(paragraph: str) -> list[str]:
# Regular expression pattern
sentence_endings = r'(?<!\w\.\w.)(?<![A-Z][a-z]\.)(?<=\.|\?|\!)\s'
sentences = re.split(sentence_endings, paragraph)
return sentencesSplits a given paragraph into individual sentences using a regular expression to detect sentence boundaries while avoiding incorrect splits at abbreviations and edge cases.
- Regular Expression Pattern: The pattern
sentence_endingsis designed to identify sentence boundaries marked by periods (.), question marks (?), or exclamation marks (!) followed by a whitespace character. Negative lookbehind assertions are used to prevent splitting on common abbreviations (e.g., "Dr.", "U.S."). Here is a breakdown of the regular expression pattern:(?<!\w\.\w.): Negative lookbehind to avoid splitting within abbreviations like "e.g." or "U.S."(?<![A-Z][a-z]\.): Negative lookbehind to avoid splitting after titles like "Dr." or "Mrs."(?<=\.|\?|\!): Positive lookbehind to ensure the split occurs after a period (.), question mark (?), or exclamation mark (!).\s: Matches a whitespace character where the actual split will occur.
- Split a paragraph into sentences: The
re.split()function uses thesentence_endingspattern to split the input paragraph into a list of sentences. This results in a list where each element is a sentence extracted from the paragraph. - Return sentences list: The function returns the list of sentences for further processing.
-
Inputs:
paragraph(str): The input text paragraph to be split into sentences.
-
Outputs:
- Returns a
list[str]: A list where each element is a sentence from the input paragraph.
- Returns a
def get_llm_output(response: str) -> dict:
if "COMPETENCY ACHIEVED" not in response:
return dict(
llm_output=response,
llm_verdict=False
)
elif "COMPETENCY ACHIEVED" in response:
sentences = split_into_sentences(response)
for i in range(len(sentences)):
if "COMPETENCY ACHIEVED" in sentences[i]:
llm_response = ' '.join(sentences[0:i-1])
if sentences[i-1][-1] == '?':
return dict(
llm_output=llm_response,
llm_verdict=False
)
else:
return dict(
llm_output=llm_response,
llm_verdict=True
)
elif "compet" in response.lower() or "master" in response.lower():
return dict(
llm_output=response,
llm_verdict=True
)Processes the response from the LLM to determine if competency in the topic has been achieved by the student, and extracts the relevant output.
- Check for "COMPETENCY ACHIEVED" Absence: If "COMPETENCY ACHIEVED" is not in the response, return the original response with
llm_verdictset toFalse. - Check for "COMPETENCY ACHIEVED" Presence: If "COMPETENCY ACHIEVED" is in the response:
- Splits the response into sentences using
split_into_sentences(response). - Iterates through the sentences to find the one containing "COMPETENCY ACHIEVED".
- Extracts all sentences before "COMPETENCY ACHIEVED" and joins them into
llm_response. - Checks the punctuation of the sentence immediately before "COMPETENCY ACHIEVED":
- If the preceding sentence ends with a question mark (
?):- Sets
llm_verdicttoFalse(indicating competency not achieved).
- Sets
- Else:
- Sets
llm_verdicttoTrue(indicating competency achieved).
- Sets
- If the preceding sentence ends with a question mark (
- Returns
llm_responseandllm_verdict.
- Check for Keywords "compet" or "master": If the "compet" or "master" word stems are in the response, return the original response with
llm_verdictset toTrue.
-
Inputs:
response: The response generated by the LLM.
-
Outputs:
- Returns a dictionary with the LLM's output and a boolean indicating whether competency has been achieved.