Skip to content

Commit 5b91fb2

Browse files
authored
Basic Synthetic Conversation pipeline (#6)
* fix: update JSON from backend * fix: JSON parsing from backend * rename testing * rename testing * google learnLM new agent for comparison * introduction of google ai models * updates * synthetic conversation gen v1
1 parent ef5e4c0 commit 5b91fb2

18 files changed

+1329
-22
lines changed

.dockerignore

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,4 +144,16 @@ README.md
144144
data/
145145

146146
# Test reports
147-
reports/
147+
reports/
148+
149+
# Synthetic data conversations
150+
src/agents/utils/example_inputs/
151+
src/agents/utils/synthetic_conversations/
152+
src/agents/utils/synthetic_conversation_generator.py
153+
src/agents/utils/testbench_prompts.py
154+
src/agents/utils/langgraph_viz.py
155+
156+
# development agents
157+
src/agents/student_agent/
158+
src/agents/development_agents/
159+
src/agents/google_learnML_agent/

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,4 +132,7 @@ dmypy.json
132132
.vscode
133133

134134
.DS_Store
135-
evaluation_function/db_analytics
135+
evaluation_function/db_analytics
136+
137+
# Synthetic data conversations
138+
src/agents/utils/synthetic_conversations/*.json

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ langchain-chroma
33
langchain-community
44
langchain-core
55
langchain-openai
6+
langchain_google_genai
67
langchain-text-splitters
78
langchainhub
89
langdetect
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
try:
2+
from ..llm_factory import GoogleAILLMs
3+
from .google_learnLM_prompts import \
4+
role_prompt, conv_pref_prompt, update_conv_pref_prompt, summary_prompt, update_summary_prompt
5+
from ..utils.types import InvokeAgentResponseType
6+
except ImportError:
7+
from src.agents.llm_factory import GoogleAILLMs
8+
from src.agents.google_learnLM_agent.google_learnLM_prompts import \
9+
role_prompt, conv_pref_prompt, update_conv_pref_prompt, summary_prompt, update_summary_prompt
10+
from src.agents.utils.types import InvokeAgentResponseType
11+
12+
from langgraph.graph import StateGraph, START, END
13+
from langchain_core.messages import SystemMessage, RemoveMessage, HumanMessage, AIMessage
14+
from langchain_core.runnables.config import RunnableConfig
15+
from langgraph.graph.message import add_messages
16+
from typing import Annotated, TypeAlias
17+
from typing_extensions import TypedDict
18+
19+
"""
20+
GOOGLE's LearnLM-Tutor agent based on https://arxiv.org/pdf/2412.16429v2 available experimentally on Google AI Studio.
21+
Docs: https://ai.google.dev/gemini-api/docs/learnlm
22+
[LLM workflow with a summarisation, profiling, and chat agent that receives an external conversation history].
23+
24+
Used as one of the baselines for comparison for the other agents.
25+
26+
This agent is designed to:
27+
- [summarise_prompt] summarise the conversation after 'max_messages_to_summarize' number of messages is reached in the conversation
28+
- [conv_pref_prompt] analyse the conversation style of the student
29+
- [role_prompt] role of a tutor to answer student's questions on the topic
30+
"""
31+
32+
ValidMessageTypes: TypeAlias = SystemMessage | HumanMessage | AIMessage
33+
AllMessageTypes: TypeAlias = ValidMessageTypes | RemoveMessage
34+
35+
class State(TypedDict):
36+
messages: Annotated[list[AllMessageTypes], add_messages]
37+
summary: str
38+
conversationalStyle: str
39+
40+
class GoogleLearnLMAgent:
41+
def __init__(self):
42+
llm = GoogleAILLMs()
43+
self.llm = llm.get_llm()
44+
summarisation_llm = GoogleAILLMs()
45+
self.summarisation_llm = summarisation_llm.get_llm()
46+
self.summary = ""
47+
self.conversationalStyle = ""
48+
49+
# Define Agent's specific Parameters
50+
self.max_messages_to_summarize = 11
51+
self.role_prompt = role_prompt
52+
self.summary_prompt = summary_prompt
53+
self.update_summary_prompt = update_summary_prompt
54+
self.conversation_preference_prompt = conv_pref_prompt
55+
self.update_conversation_preference_prompt = update_conv_pref_prompt
56+
57+
# Define a new graph for the conversation & compile it
58+
self.workflow = StateGraph(State)
59+
self.workflow_definition()
60+
self.app = self.workflow.compile()
61+
62+
def call_model(self, state: State, config: RunnableConfig) -> str:
63+
"""Call the LLM model knowing the role system prompt, the summary and the conversational style."""
64+
65+
# Default AI tutor role prompt
66+
system_message = self.role_prompt
67+
68+
# Adding external student progress and question context details from data queries
69+
question_response_details = config["configurable"].get("question_response_details", "")
70+
if question_response_details:
71+
system_message += f"## Known Question Materials: {question_response_details} \n\n"
72+
73+
# Adding summary and conversational style to the system message
74+
summary = state.get("summary", "")
75+
conversationalStyle = state.get("conversationalStyle", "")
76+
if summary:
77+
system_message += f"## Summary of conversation earlier: {summary} \n\n"
78+
if conversationalStyle:
79+
system_message += f"## Known conversational style and preferences of the student for this conversation: {conversationalStyle}. \n\nYour answer must be in line with this conversational style."
80+
81+
messages = [SystemMessage(content=system_message)] + state['messages']
82+
83+
valid_messages = self.check_for_valid_messages(messages)
84+
response = self.llm.invoke(valid_messages)
85+
86+
# Save summary for fetching outside the class
87+
self.summary = summary
88+
self.conversationalStyle = conversationalStyle
89+
90+
return {"summary": summary, "messages": [response]}
91+
92+
def check_for_valid_messages(self, messages: list[AllMessageTypes]) -> list[ValidMessageTypes]:
93+
""" Removing the RemoveMessage() from the list of messages """
94+
95+
valid_messages: list[ValidMessageTypes] = []
96+
for message in messages:
97+
if message.type != 'remove':
98+
valid_messages.append(message)
99+
return valid_messages
100+
101+
def summarize_conversation(self, state: State, config: RunnableConfig) -> dict:
102+
"""Summarize the conversation."""
103+
104+
summary = state.get("summary", "")
105+
previous_summary = config["configurable"].get("summary", "")
106+
previous_conversationalStyle = config["configurable"].get("conversational_style", "")
107+
if previous_summary:
108+
summary = previous_summary
109+
110+
if summary:
111+
summary_message = (
112+
f"This is summary of the conversation to date: {summary}\n\n" +
113+
self.update_summary_prompt
114+
)
115+
else:
116+
summary_message = self.summary_prompt
117+
118+
if previous_conversationalStyle:
119+
conversationalStyle_message = (
120+
f"This is the previous conversational style of the student for this conversation: {previous_conversationalStyle}\n\n" +
121+
self.update_conversation_preference_prompt
122+
)
123+
else:
124+
conversationalStyle_message = self.conversation_preference_prompt
125+
126+
# STEP 1: Summarize the conversation
127+
messages = state["messages"][:-1] + [SystemMessage(content=summary_message)]
128+
valid_messages = self.check_for_valid_messages(messages)
129+
summary_response = self.summarisation_llm.invoke(valid_messages)
130+
131+
# STEP 2: Analyze the conversational style
132+
messages = state["messages"][:-1] + [SystemMessage(content=conversationalStyle_message)]
133+
valid_messages = self.check_for_valid_messages(messages)
134+
conversationalStyle_response = self.summarisation_llm.invoke(valid_messages)
135+
136+
# Delete messages that are no longer wanted, except the last ones
137+
delete_messages: list[AllMessageTypes] = [RemoveMessage(id=m.id) for m in state["messages"][:-3]]
138+
139+
return {"summary": summary_response.content, "conversationalStyle": conversationalStyle_response.content, "messages": delete_messages}
140+
141+
def should_summarize(self, state: State) -> str:
142+
"""
143+
Return the next node to execute.
144+
If there are more than X messages, then we summarize the conversation.
145+
Otherwise, we call the LLM.
146+
"""
147+
148+
messages = state["messages"]
149+
valid_messages = self.check_for_valid_messages(messages)
150+
nr_messages = len(valid_messages)
151+
if "system" in valid_messages[-1].type:
152+
nr_messages -= 1
153+
154+
# always pairs of (sent, response) + 1 latest message
155+
if nr_messages > self.max_messages_to_summarize:
156+
return "summarize_conversation"
157+
return "call_llm"
158+
159+
def workflow_definition(self) -> None:
160+
self.workflow.add_node("call_llm", self.call_model)
161+
self.workflow.add_node("summarize_conversation", self.summarize_conversation)
162+
163+
self.workflow.add_conditional_edges(source=START, path=self.should_summarize)
164+
self.workflow.add_edge("summarize_conversation", "call_llm")
165+
self.workflow.add_edge("call_llm", END)
166+
167+
def get_summary(self) -> str:
168+
return self.summary
169+
170+
def get_conversational_style(self) -> str:
171+
return self.conversationalStyle
172+
173+
def print_update(self, update: dict) -> None:
174+
for k, v in update.items():
175+
for m in v["messages"]:
176+
m.pretty_print()
177+
if "summary" in v:
178+
print(v["summary"])
179+
180+
def pretty_response_value(self, event: dict) -> str:
181+
return event["messages"][-1].content
182+
183+
agent = GoogleLearnLMAgent()
184+
def invoke_google_learnlm_agent(query: str, conversation_history: list, summary: str, conversationalStyle: str, question_response_details: str, session_id: str) -> InvokeAgentResponseType:
185+
"""
186+
Call an agent that has no conversation memory and expects to receive all past messages in the params and the latest human request in the query.
187+
If conversation history longer than X, the agent will summarize the conversation and will provide a conversational style analysis.
188+
"""
189+
print(f'in invoke_google_learnlm_agent(), query = {query}, thread_id = {session_id}')
190+
191+
config = {"configurable": {"thread_id": session_id, "summary": summary, "conversational_style": conversationalStyle, "question_response_details": question_response_details}}
192+
response_events = agent.app.invoke({"messages": conversation_history + [HumanMessage(content=query)]}, config=config, stream_mode="values") #updates
193+
pretty_printed_response = agent.pretty_response_value(response_events) # get last event/ai answer in the response
194+
195+
# Gather Metadata from the agent
196+
summary = agent.get_summary()
197+
conversationalStyle = agent.get_conversational_style()
198+
199+
return {
200+
"input": query,
201+
"output": pretty_printed_response,
202+
"intermediate_steps": [str(summary), conversationalStyle, conversation_history]
203+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# NOTE:
2+
# First person view prompts proven to be more effective in generating responses from the model (Dec 2024)
3+
# 'Keep your responses open for further questions and encourage the student's curiosity.' -> asks a question at the end to keep the conversation going
4+
# 'Let the student know that your reasoning might be wrong and the student should not trust your reasoning fully.' -> not relliant
5+
6+
# PROMPTS generated with the help of ChatGPT GPT-4o Nov 2024
7+
8+
role_prompt = "You are an excellent tutor that aims to provide clear and concise explanations to students. I am the student. Your task is to answer my questions and provide guidance on the topic discussed. Ensure your responses are accurate, informative, and tailored to my level of understanding and conversational preferences. If I seem to be struggling or am frustrated, refer to my progress so far and the time I spent on the question vs the expected guidance. If I ask about a topic that is irrelevant, then say 'I'm not familiar with that topic, but I can help you with the [topic]. You do not need to end your messages with a concluding statement.\n\n"
9+
10+
pref_guidelines = """**Guidelines:**
11+
- Use concise, objective language.
12+
- Note the student's educational goals, such as understanding foundational concepts, passing an exam, getting top marks, code implementation, hands-on practice, etc.
13+
- Note any specific preferences in how the student learns, such as asking detailed questions, seeking practical examples, requesting quizes, requesting clarifications, etc.
14+
- Note any specific preferences the student has when receiving explanations or corrections, such as seeking step-by-step guidance, clarifications, or other examples.
15+
- Note any specific preferences the student has regarding your (the chatbot's) tone, personality, or teaching style.
16+
- Avoid assumptions about motivation; observe only patterns evident in the conversation.
17+
- If no particular preference is detectable, state "No preference observed."
18+
"""
19+
20+
conv_pref_prompt = f"""Analyze the student’s conversational style based on the interaction above. Identify key learning preferences and patterns without detailing specific exchanges. Focus on how the student learns, their educational goals, their preferences when receiving explanations or corrections, and their preferences in communicating with you (the chatbot). Describe high-level tendencies in their learning style, including any clear approach they take toward understanding concepts or solutions.
21+
22+
{pref_guidelines}
23+
24+
Examples:
25+
26+
Example 1:
27+
**Conversation:**
28+
Student: "I understand that the derivative gives us the slope of a function, but what if we want to know the rate of change over an interval? Do we still use the derivative?"
29+
AI: "Good question! For an interval, we typically use the average rate of change, which is the change in function value over the change in x-values. The derivative gives the instantaneous rate of change at a specific point."
30+
31+
**Expected Answer:**
32+
The student prefers in-depth conceptual understanding and asks thoughtful questions that differentiate between similar concepts. They seem comfortable discussing foundational ideas in calculus.
33+
34+
Example 2:
35+
**Conversation:**
36+
Student: "I’m trying to solve this physics problem: if I throw a ball upwards at 10 m/s, how long will it take to reach the top? I thought I could just divide by gravity, but I’m not sure."
37+
AI: "You're on the right track! Since acceleration due to gravity is 9.8 m/s², you can divide the initial velocity by gravity to find the time to reach the peak, which would be around 1.02 seconds."
38+
39+
**Expected Answer:**
40+
The student prefers practical problem-solving and is open to corrections. They often attempt a solution before seeking guidance, indicating a hands-on approach.
41+
42+
Example 3:
43+
**Conversation:**
44+
Student: "Can you explain the difference between meiosis and mitosis? I know both involve cell division, but I’m confused about how they differ."
45+
AI: "Certainly! Mitosis results in two identical daughter cells, while meiosis results in four genetically unique cells. Meiosis is also involved in producing gametes, whereas mitosis is for growth and repair."
46+
47+
**Expected Answer:**
48+
The student prefers clear, comparative explanations when learning complex biological processes. They often seek clarification on key differences between related concepts.
49+
50+
Example 4:
51+
**Conversation:**
52+
Student: "I wrote this Python code to reverse a string, but it’s not working. Here’s what I tried: `for char in string: new_string = char + new_string`."
53+
AI: "You’re close! Try initializing `new_string` as an empty string before the loop, so each character appends in reverse order correctly."
54+
55+
**Expected Answer:**
56+
The student prefers hands-on guidance with code, often sharing specific code snippets. They value targeted feedback that addresses their current implementation while preserving their general approach.
57+
58+
"""
59+
60+
update_conv_pref_prompt = f"""Based on the interaction above, analyse the student’s conversational style. Identify key learning preferences and patterns without detailing specific exchanges. Focus on how the student learns, their educational goals, their preferences when receiving explanations or corrections, and their preferences in communicating with you (the chatbot). Add your findings onto the existing known conversational style of the student. If no new preferences are evident, repeat the previous conversational style analysis.
61+
62+
{pref_guidelines}
63+
"""
64+
65+
summary_prompt = """
66+
You are an AI assistant specializing in concise and accurate summarization. Your task is to summarize the previous conversation, capturing the main topics, key points, user questions, and your responses in a clear and organized format.
67+
68+
Ensure the summary is:
69+
70+
Concise: Keep the summary brief while including all essential information.
71+
Structured: Organize the summary into sections such as 'Topics Discussed,' 'Key Questions and Responses,' and 'Follow-Up Suggestions' if applicable.
72+
Neutral and Accurate: Avoid adding interpretations or opinions; focus only on the content shared.
73+
When summarizing: If the conversation is technical, highlight significant concepts, solutions, and terminology. If context involves problem-solving, detail the problem and the steps or solutions provided. If the user asks for creative input, briefly describe the ideas presented.
74+
75+
Provide the summary in a bulleted format for clarity. Avoid redundant details while preserving the core intent of the discussion.
76+
"""
77+
78+
update_summary_prompt = "Update the summary by taking into account the new messages above:"

src/agents/llm_factory.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@
66
from langchain_community.embeddings import OllamaEmbeddings
77
from langchain_openai import ChatOpenAI
88
from langchain_openai import OpenAIEmbeddings
9+
from langchain_google_genai import ChatGoogleGenerativeAI
910

1011
class AzureLLMs:
11-
def __init__(self):
12+
def __init__(self, temperature: int = 0):
1213
self._azure_llm = AzureChatOpenAI(
1314
openai_api_version=os.environ["AZURE_OPENAI_API_VERSION"],
1415
azure_deployment=os.environ["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"],
15-
temperature=0,
16+
temperature=temperature,
1617
max_tokens=None,
1718
)
1819
self._azure_embedding = AzureOpenAIEmbeddings(azure_deployment=os.environ['AZURE_OPENAI_EMBEDDING_1536_DEPLOYMENT'],
@@ -51,10 +52,10 @@ def get_embedding(self):
5152
return self._ollama_embedding
5253

5354
class OpenAILLMs:
54-
def __init__(self):
55+
def __init__(self, temperature: int = 0):
5556
self._openai_llm = ChatOpenAI(
5657
model=os.environ['OPENAI_MODEL'],
57-
temperature=0,
58+
temperature=temperature,
5859
api_key=os.environ["OPENAI_API_KEY"],
5960
)
6061

@@ -68,3 +69,15 @@ def get_llm(self):
6869

6970
def get_embedding(self):
7071
return self._openai_embedding
72+
73+
class GoogleAILLMs:
74+
def __init__(self, temperature: int = 0):
75+
76+
self._google_llm = ChatGoogleGenerativeAI(
77+
model=os.environ['GOOGLE_AI_MODEL'],
78+
temperature=temperature,
79+
google_api_key=os.environ['GOOGLE_AI_API_KEY'],
80+
)
81+
82+
def get_llm(self):
83+
return self._google_llm

0 commit comments

Comments
 (0)