supratipb commited on
Commit
6d06e3c
Β·
verified Β·
1 Parent(s): f939e66

Update agent.py

Browse files
Files changed (1) hide show
  1. agent.py +350 -350
agent.py CHANGED
@@ -1,350 +1,350 @@
1
- """LangGraph Agent"""
2
-
3
- import os
4
- from dotenv import load_dotenv
5
-
6
- from langgraph.graph import START, StateGraph, MessagesState
7
- from langgraph.prebuilt import tools_condition
8
- from langgraph.prebuilt import ToolNode
9
-
10
- from langchain_google_genai import ChatGoogleGenerativeAI
11
- from langchain_groq import ChatGroq
12
- from langchain_huggingface import ChatHuggingFace, HuggingFaceEndpoint, HuggingFaceEmbeddings
13
- from langchain_tavily import TavilySearch
14
- from langchain_community.document_loaders import WikipediaLoader
15
- from langchain_community.document_loaders import ArxivLoader
16
- from langchain_community.vectorstores import SupabaseVectorStore
17
- from langchain_core.messages import SystemMessage, HumanMessage
18
- from langchain_core.tools import tool
19
- from langchain.tools.retriever import create_retriever_tool
20
- from langchain_openai import ChatOpenAI
21
- from langchain_anthropic import ChatAnthropic
22
- from supabase.client import Client, create_client
23
- import re
24
- from langchain_community.document_loaders import WikipediaLoader
25
- from youtube_transcript_api import YouTubeTranscriptApi, TranscriptsDisabled, NoTranscriptFound
26
- import sympy
27
- import wolframalpha
28
- import sys
29
- import requests
30
-
31
-
32
-
33
-
34
- load_dotenv()
35
-
36
- @tool
37
- def multiply(a: int, b: int) -> int:
38
- """Multiply two numbers.
39
-
40
- Args:
41
- a: first int
42
- b: second int
43
- """
44
- return a * b
45
-
46
- @tool
47
- def add(a: int, b: int) -> int:
48
- """Add two numbers.
49
-
50
- Args:
51
- a: first int
52
- b: second int
53
- """
54
- return a + b
55
-
56
- @tool
57
- def subtract(a: int, b: int) -> int:
58
- """Subtract two numbers.
59
-
60
- Args:
61
- a: first int
62
- b: second int
63
- """
64
- return a - b
65
-
66
- @tool
67
- def divide(a: int, b: int) -> int:
68
- """Divide two numbers.
69
-
70
- Args:
71
- a: first int
72
- b: second int
73
- """
74
- if b == 0:
75
- raise ValueError("Cannot divide by zero.")
76
- return a / b
77
-
78
- @tool
79
- def modulus(a: int, b: int) -> int:
80
- """Get the modulus of two numbers.
81
-
82
- Args:
83
- a: first int
84
- b: second int
85
- """
86
- return a % b
87
-
88
- @tool
89
- def wiki_search(query: str) -> str:
90
- """Search Wikipedia for a query and return maximum 2 results.
91
-
92
- Args:
93
- query: The search query."""
94
- search_docs = WikipediaLoader(query=query, load_max_docs=2).load()
95
- formatted_search_docs = "\n\n---\n\n".join(
96
- [
97
- f'<Document source="{doc.metadata["source"]}" page="{doc.metadata.get("page", "")}"/>\n{doc.page_content}\n</Document>'
98
- for doc in search_docs
99
- ])
100
- #return {"wiki_results": formatted_search_docs}
101
- return formatted_search_docs
102
-
103
-
104
-
105
- @tool
106
- def web_search(query: str) -> str:
107
- """Search Tavily for a query and return maximum 3 results.
108
-
109
- Args:
110
- query: The search query."""
111
- search_docs = TavilySearch(max_results=3).invoke(query=query)
112
- formatted_search_docs = "\n\n---\n\n".join(
113
- [
114
- f'<Document source="{doc.metadata["source"]}" page="{doc.metadata.get("page", "")}"/>\n{doc.page_content}\n</Document>'
115
- for doc in search_docs
116
- ])
117
- return {"web_results": formatted_search_docs}
118
-
119
-
120
- @tool
121
- def arvix_search(query: str) -> str:
122
- """Search Arxiv for a query and return maximum 3 result.
123
-
124
- Args:
125
- query: The search query."""
126
- search_docs = ArxivLoader(query=query, load_max_docs=3).load()
127
- formatted_search_docs = "\n\n---\n\n".join(
128
- [
129
- f'<Document source="{doc.metadata["source"]}" page="{doc.metadata.get("page", "")}"/>\n{doc.page_content[:1000]}\n</Document>'
130
- for doc in search_docs
131
- ])
132
- return {"arvix_results": formatted_search_docs}
133
-
134
-
135
- @tool
136
- def filtered_wiki_search(query: str, start_year: int = None, end_year: int = None) -> dict:
137
- """Search Wikipedia for a query and filter results by year if provided."""
138
- search_docs = WikipediaLoader(query=query, load_max_docs=5).load()
139
-
140
- def contains_year(text, start, end):
141
- years = re.findall(r'\b(19\d{2}|20\d{2})\b', text)
142
- for y in years:
143
- y_int = int(y)
144
- if start <= y_int <= end:
145
- return True
146
- return False
147
-
148
- filtered_docs = []
149
- for doc in search_docs:
150
- if start_year and end_year:
151
- if contains_year(doc.page_content, start_year, end_year):
152
- filtered_docs.append(doc)
153
- else:
154
- filtered_docs.append(doc)
155
-
156
- formatted_search_docs = "\n\n---\n\n".join(
157
- [
158
- f'<Document source="{doc.metadata["source"]}" page="{doc.metadata.get("page", "")}"/>\n{doc.page_content}\n</Document>'
159
- for doc in filtered_docs
160
- ])
161
- return {"wiki_results": formatted_search_docs}
162
-
163
-
164
-
165
- @tool
166
- def wolfram_alpha_query(query: str) -> str:
167
- """Query Wolfram Alpha with the given question and return the result."""
168
- client = wolframalpha.Client(os.environ['WOLFRAM_APP_ID'])
169
- res = client.query(query)
170
- try:
171
- return next(res.results).text
172
- except StopIteration:
173
- return "No result found."
174
-
175
-
176
-
177
-
178
- @tool
179
- def youtube_transcript(url: str) -> str:
180
- """Fetch YouTube transcript text from a video URL."""
181
- try:
182
- video_id = url.split("v=")[-1].split("&")[0]
183
- transcript_list = YouTubeTranscriptApi.get_transcript(video_id)
184
- transcript = " ".join([segment['text'] for segment in transcript_list])
185
- return transcript
186
- except (TranscriptsDisabled, NoTranscriptFound):
187
- return "Transcript not available for this video."
188
- except Exception as e:
189
- return f"Error fetching transcript: {str(e)}"
190
-
191
-
192
-
193
- @tool
194
- def solve_algebraic_expression(expression: str) -> str:
195
- """Solve or simplify the given algebraic expression."""
196
- try:
197
- expr = sympy.sympify(expression)
198
- simplified = sympy.simplify(expr)
199
- return str(simplified)
200
- except Exception as e:
201
- return f"Error solving expression: {str(e)}"
202
-
203
-
204
-
205
- @tool
206
- def run_python_code(code: str) -> str:
207
- """Execute python code and return the result of variable 'result' if defined."""
208
- try:
209
- local_vars = {}
210
- exec(code, {}, local_vars)
211
- if 'result' in local_vars:
212
- return str(local_vars['result'])
213
- else:
214
- return "Code executed successfully but no 'result' variable found."
215
- except Exception as e:
216
- return f"Error executing code: {str(e)}"
217
-
218
-
219
-
220
- @tool
221
- def wikidata_query(sparql_query: str) -> str:
222
- """Run a SPARQL query against Wikidata and return the JSON results."""
223
- endpoint = "https://query.wikidata.org/sparql"
224
- headers = {"Accept": "application/sparql-results+json"}
225
- try:
226
- response = requests.get(endpoint, params={"query": sparql_query}, headers=headers)
227
- response.raise_for_status()
228
- data = response.json()
229
- return str(data) # Or format as needed
230
- except Exception as e:
231
- return f"Error querying Wikidata: {str(e)}"
232
-
233
-
234
-
235
-
236
- # load the system prompt from the file
237
- with open("system_prompt.txt", "r", encoding="utf-8") as f:
238
- system_prompt = f.read()
239
-
240
- # System message
241
- sys_msg = SystemMessage(content=system_prompt)
242
-
243
- # build a retriever
244
-
245
- embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-mpnet-base-v2") # dim=768
246
- supabase: Client = create_client(
247
- os.environ.get("SUPABASE_URL"),
248
- os.environ.get("SUPABASE_SERVICE_KEY"))
249
- vector_store = SupabaseVectorStore(
250
- client=supabase,
251
- embedding= embeddings,
252
- table_name="documents",
253
- query_name="match_documents_langchain",
254
- )
255
- retriever_tool = create_retriever_tool(
256
- retriever=vector_store.as_retriever(),
257
- name="Question Search",
258
- description="A tool to retrieve similar questions from a vector store.",
259
- )
260
-
261
-
262
-
263
- tools = [
264
-
265
- multiply,
266
- add,
267
- subtract,
268
- divide,
269
- modulus,
270
- wiki_search,
271
- filtered_wiki_search,
272
- web_search,
273
- arvix_search,
274
- wolfram_alpha_query,
275
- retriever_tool,
276
- youtube_transcript,
277
- solve_algebraic_expression,
278
- run_python_code,
279
- wikidata_query
280
- ]
281
-
282
- # Build graph function
283
- def build_graph(provider: str = "groq"):
284
- """Build the graph"""
285
- # Load environment variables from .env file
286
- if provider == "openai":
287
- llm = ChatOpenAI(model_name="gpt-3.5-turbo", temperature=0)
288
- elif provider == "anthropic":
289
- llm = ChatAnthropic(model="claude-v1", temperature=0)
290
- elif provider == "google":
291
- llm = ChatGoogleGenerativeAI(model="gemini-2.0-flash", temperature=0)
292
- elif provider == "groq":
293
- llm = ChatGroq(model="qwen-qwq-32b", temperature=0) # optional : qwen-qwq-32b gemma2-9b-it
294
- elif provider == "huggingface":
295
- llm = ChatHuggingFace(
296
- llm = HuggingFaceEndpoint(
297
- endpoint_url="https://api-inference.huggingface.co/models/Meta-DeepLearning/llama-2-7b-chat-hf",
298
- temperature=0,
299
- ),
300
- )
301
- else:
302
- raise ValueError("Invalid provider. Choose 'google', 'groq' or 'huggingface'.")
303
- # Bind tools to LLM
304
- llm_with_tools = llm.bind_tools(tools)
305
-
306
- # Node
307
- def assistant(state: MessagesState):
308
- messages_with_sys = [sys_msg] + state["messages"]
309
- return {"messages": [llm_with_tools.invoke(messages_with_sys)]}
310
-
311
-
312
- def retriever(state: MessagesState):
313
- """Retriever node"""
314
- similar_question = vector_store.similarity_search(state["messages"][0].content)
315
- if not similar_question:
316
- # No similar documents found, fallback message
317
- example_msg = HumanMessage(
318
- content="Sorry, I could not find any similar questions in the vector store."
319
- )
320
- else:
321
- example_msg = HumanMessage(
322
- content=f"Here I provide a similar question and answer for reference: \n\n{similar_question[0].page_content}",
323
- )
324
- return {"messages": [sys_msg] + state["messages"] + [example_msg]}
325
-
326
- builder = StateGraph(MessagesState)
327
- builder.add_node("retriever", retriever)
328
- builder.add_node("assistant", assistant)
329
- builder.add_node("tools", ToolNode(tools))
330
- builder.add_edge(START, "retriever")
331
- builder.add_edge("retriever", "assistant")
332
- builder.add_conditional_edges(
333
- "assistant",
334
- tools_condition,
335
- )
336
- builder.add_edge("tools", "assistant")
337
-
338
- # Compile graph
339
- return builder.compile()
340
-
341
- # test
342
- if __name__ == "__main__":
343
- question = "When was a picture of St. Thomas Aquinas first added to the Wikipedia page on the Principle of double effect?"
344
- # Build the graph
345
- graph = build_graph(provider="groq")
346
- # Run the graph
347
- messages = [HumanMessage(content=question)]
348
- messages = graph.invoke({"messages": messages})
349
- for m in messages["messages"]:
350
- m.pretty_print()
 
1
+ """LangGraph Agent"""
2
+
3
+ import os
4
+ from dotenv import load_dotenv
5
+
6
+ from langgraph.graph import START, StateGraph, MessagesState
7
+ from langgraph.prebuilt import tools_condition
8
+ from langgraph.prebuilt import ToolNode
9
+
10
+ from langchain_google_genai import ChatGoogleGenerativeAI
11
+ from langchain_groq import ChatGroq
12
+ from langchain_huggingface import ChatHuggingFace, HuggingFaceEndpoint, HuggingFaceEmbeddings
13
+ from langchain_tavily import TavilySearch
14
+ from langchain_community.document_loaders import WikipediaLoader
15
+ from langchain_community.document_loaders import ArxivLoader
16
+ from langchain_community.vectorstores import SupabaseVectorStore
17
+ from langchain_core.messages import SystemMessage, HumanMessage
18
+ from langchain_core.tools import tool
19
+ from langchain.tools.retriever import create_retriever_tool
20
+ from langchain_openai import ChatOpenAI
21
+ from langchain_anthropic import ChatAnthropic
22
+ from supabase.client import Client, create_client
23
+ import re
24
+ from langchain_community.document_loaders import WikipediaLoader
25
+ from youtube_transcript_api import YouTubeTranscriptApi, TranscriptsDisabled, NoTranscriptFound
26
+ import sympy
27
+ import wolframalpha
28
+ import sys
29
+ import requests
30
+
31
+
32
+
33
+
34
+ load_dotenv()
35
+
36
+ @tool
37
+ def multiply(a: int, b: int) -> int:
38
+ """Multiply two numbers.
39
+
40
+ Args:
41
+ a: first int
42
+ b: second int
43
+ """
44
+ return a * b
45
+
46
+ @tool
47
+ def add(a: int, b: int) -> int:
48
+ """Add two numbers.
49
+
50
+ Args:
51
+ a: first int
52
+ b: second int
53
+ """
54
+ return a + b
55
+
56
+ @tool
57
+ def subtract(a: int, b: int) -> int:
58
+ """Subtract two numbers.
59
+
60
+ Args:
61
+ a: first int
62
+ b: second int
63
+ """
64
+ return a - b
65
+
66
+ @tool
67
+ def divide(a: int, b: int) -> int:
68
+ """Divide two numbers.
69
+
70
+ Args:
71
+ a: first int
72
+ b: second int
73
+ """
74
+ if b == 0:
75
+ raise ValueError("Cannot divide by zero.")
76
+ return a / b
77
+
78
+ @tool
79
+ def modulus(a: int, b: int) -> int:
80
+ """Get the modulus of two numbers.
81
+
82
+ Args:
83
+ a: first int
84
+ b: second int
85
+ """
86
+ return a % b
87
+
88
+ @tool
89
+ def wiki_search(query: str) -> str:
90
+ """Search Wikipedia for a query and return maximum 2 results.
91
+
92
+ Args:
93
+ query: The search query."""
94
+ search_docs = WikipediaLoader(query=query, load_max_docs=2).load()
95
+ formatted_search_docs = "\n\n---\n\n".join(
96
+ [
97
+ f'<Document source="{doc.metadata["source"]}" page="{doc.metadata.get("page", "")}"/>\n{doc.page_content}\n</Document>'
98
+ for doc in search_docs
99
+ ])
100
+ #return {"wiki_results": formatted_search_docs}
101
+ return formatted_search_docs
102
+
103
+
104
+
105
+ @tool
106
+ def web_search(query: str) -> str:
107
+ """Search Tavily for a query and return maximum 3 results.
108
+
109
+ Args:
110
+ query: The search query."""
111
+ search_docs = TavilySearch(max_results=3).invoke(query=query)
112
+ formatted_search_docs = "\n\n---\n\n".join(
113
+ [
114
+ f'<Document source="{doc.metadata["source"]}" page="{doc.metadata.get("page", "")}"/>\n{doc.page_content}\n</Document>'
115
+ for doc in search_docs
116
+ ])
117
+ return {"web_results": formatted_search_docs}
118
+
119
+
120
+ @tool
121
+ def arvix_search(query: str) -> str:
122
+ """Search Arxiv for a query and return maximum 3 result.
123
+
124
+ Args:
125
+ query: The search query."""
126
+ search_docs = ArxivLoader(query=query, load_max_docs=3).load()
127
+ formatted_search_docs = "\n\n---\n\n".join(
128
+ [
129
+ f'<Document source="{doc.metadata["source"]}" page="{doc.metadata.get("page", "")}"/>\n{doc.page_content[:1000]}\n</Document>'
130
+ for doc in search_docs
131
+ ])
132
+ return {"arvix_results": formatted_search_docs}
133
+
134
+
135
+ @tool
136
+ def filtered_wiki_search(query: str, start_year: int = None, end_year: int = None) -> dict:
137
+ """Search Wikipedia for a query and filter results by year if provided."""
138
+ search_docs = WikipediaLoader(query=query, load_max_docs=5).load()
139
+
140
+ def contains_year(text, start, end):
141
+ years = re.findall(r'\b(19\d{2}|20\d{2})\b', text)
142
+ for y in years:
143
+ y_int = int(y)
144
+ if start <= y_int <= end:
145
+ return True
146
+ return False
147
+
148
+ filtered_docs = []
149
+ for doc in search_docs:
150
+ if start_year and end_year:
151
+ if contains_year(doc.page_content, start_year, end_year):
152
+ filtered_docs.append(doc)
153
+ else:
154
+ filtered_docs.append(doc)
155
+
156
+ formatted_search_docs = "\n\n---\n\n".join(
157
+ [
158
+ f'<Document source="{doc.metadata["source"]}" page="{doc.metadata.get("page", "")}"/>\n{doc.page_content}\n</Document>'
159
+ for doc in filtered_docs
160
+ ])
161
+ return {"wiki_results": formatted_search_docs}
162
+
163
+
164
+
165
+ @tool
166
+ def wolfram_alpha_query(query: str) -> str:
167
+ """Query Wolfram Alpha with the given question and return the result."""
168
+ client = wolframalpha.Client(os.environ['WOLFRAM_APP_ID'])
169
+ res = client.query(query)
170
+ try:
171
+ return next(res.results).text
172
+ except StopIteration:
173
+ return "No result found."
174
+
175
+
176
+
177
+
178
+ @tool
179
+ def youtube_transcript(url: str) -> str:
180
+ """Fetch YouTube transcript text from a video URL."""
181
+ try:
182
+ video_id = url.split("v=")[-1].split("&")[0]
183
+ transcript_list = YouTubeTranscriptApi.get_transcript(video_id)
184
+ transcript = " ".join([segment['text'] for segment in transcript_list])
185
+ return transcript
186
+ except (TranscriptsDisabled, NoTranscriptFound):
187
+ return "Transcript not available for this video."
188
+ except Exception as e:
189
+ return f"Error fetching transcript: {str(e)}"
190
+
191
+
192
+
193
+ @tool
194
+ def solve_algebraic_expression(expression: str) -> str:
195
+ """Solve or simplify the given algebraic expression."""
196
+ try:
197
+ expr = sympy.sympify(expression)
198
+ simplified = sympy.simplify(expr)
199
+ return str(simplified)
200
+ except Exception as e:
201
+ return f"Error solving expression: {str(e)}"
202
+
203
+
204
+
205
+ @tool
206
+ def run_python_code(code: str) -> str:
207
+ """Execute python code and return the result of variable 'result' if defined."""
208
+ try:
209
+ local_vars = {}
210
+ exec(code, {}, local_vars)
211
+ if 'result' in local_vars:
212
+ return str(local_vars['result'])
213
+ else:
214
+ return "Code executed successfully but no 'result' variable found."
215
+ except Exception as e:
216
+ return f"Error executing code: {str(e)}"
217
+
218
+
219
+
220
+ @tool
221
+ def wikidata_query(sparql_query: str) -> str:
222
+ """Run a SPARQL query against Wikidata and return the JSON results."""
223
+ endpoint = "https://query.wikidata.org/sparql"
224
+ headers = {"Accept": "application/sparql-results+json"}
225
+ try:
226
+ response = requests.get(endpoint, params={"query": sparql_query}, headers=headers)
227
+ response.raise_for_status()
228
+ data = response.json()
229
+ return str(data) # Or format as needed
230
+ except Exception as e:
231
+ return f"Error querying Wikidata: {str(e)}"
232
+
233
+
234
+
235
+
236
+ # load the system prompt from the file
237
+ with open("system_prompt.txt", "r", encoding="utf-8") as f:
238
+ system_prompt = f.read()
239
+
240
+ # System message
241
+ sys_msg = SystemMessage(content=system_prompt)
242
+
243
+ # build a retriever
244
+
245
+ embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-mpnet-base-v2") # dim=768
246
+ supabase: Client = create_client(
247
+ os.environ.get("SUPABASE_URL"),
248
+ os.environ.get("SUPABASE_SERVICE_KEY"))
249
+ vector_store = SupabaseVectorStore(
250
+ client=supabase,
251
+ embedding= embeddings,
252
+ table_name="documents",
253
+ query_name="match_documents_langchain",
254
+ )
255
+ retriever_tool = create_retriever_tool(
256
+ retriever=vector_store.as_retriever(),
257
+ name="Question Search",
258
+ description="A tool to retrieve similar questions from a vector store.",
259
+ )
260
+
261
+
262
+
263
+ tools = [
264
+
265
+ multiply,
266
+ add,
267
+ subtract,
268
+ divide,
269
+ modulus,
270
+ wiki_search,
271
+ filtered_wiki_search,
272
+ web_search,
273
+ arvix_search,
274
+ wolfram_alpha_query,
275
+ retriever_tool,
276
+ youtube_transcript,
277
+ solve_algebraic_expression,
278
+ run_python_code,
279
+ wikidata_query
280
+ ]
281
+
282
+ # Build graph function
283
+ def build_graph(provider: str = "huggingface"):
284
+ """Build the graph"""
285
+ # Load environment variables from .env file
286
+ if provider == "openai":
287
+ llm = ChatOpenAI(model_name="gpt-3.5-turbo", temperature=0)
288
+ elif provider == "anthropic":
289
+ llm = ChatAnthropic(model="claude-v1", temperature=0)
290
+ elif provider == "google":
291
+ llm = ChatGoogleGenerativeAI(model="gemini-2.0-flash", temperature=0)
292
+ elif provider == "groq":
293
+ llm = ChatGroq(model="qwen-qwq-32b", temperature=0) # optional : qwen-qwq-32b gemma2-9b-it
294
+ elif provider == "huggingface":
295
+ llm = ChatHuggingFace(
296
+ llm = HuggingFaceEndpoint(
297
+ endpoint_url="https://api-inference.huggingface.co/models/Meta-DeepLearning/llama-2-7b-chat-hf",
298
+ temperature=0,
299
+ ),
300
+ )
301
+ else:
302
+ raise ValueError("Invalid provider. Choose 'google', 'groq' or 'huggingface'.")
303
+ # Bind tools to LLM
304
+ llm_with_tools = llm.bind_tools(tools)
305
+
306
+ # Node
307
+ def assistant(state: MessagesState):
308
+ messages_with_sys = [sys_msg] + state["messages"]
309
+ return {"messages": [llm_with_tools.invoke(messages_with_sys)]}
310
+
311
+
312
+ def retriever(state: MessagesState):
313
+ """Retriever node"""
314
+ similar_question = vector_store.similarity_search(state["messages"][0].content)
315
+ if not similar_question:
316
+ # No similar documents found, fallback message
317
+ example_msg = HumanMessage(
318
+ content="Sorry, I could not find any similar questions in the vector store."
319
+ )
320
+ else:
321
+ example_msg = HumanMessage(
322
+ content=f"Here I provide a similar question and answer for reference: \n\n{similar_question[0].page_content}",
323
+ )
324
+ return {"messages": [sys_msg] + state["messages"] + [example_msg]}
325
+
326
+ builder = StateGraph(MessagesState)
327
+ builder.add_node("retriever", retriever)
328
+ builder.add_node("assistant", assistant)
329
+ builder.add_node("tools", ToolNode(tools))
330
+ builder.add_edge(START, "retriever")
331
+ builder.add_edge("retriever", "assistant")
332
+ builder.add_conditional_edges(
333
+ "assistant",
334
+ tools_condition,
335
+ )
336
+ builder.add_edge("tools", "assistant")
337
+
338
+ # Compile graph
339
+ return builder.compile()
340
+
341
+ # test
342
+ if __name__ == "__main__":
343
+ question = "When was a picture of St. Thomas Aquinas first added to the Wikipedia page on the Principle of double effect?"
344
+ # Build the graph
345
+ graph = build_graph(provider="groq")
346
+ # Run the graph
347
+ messages = [HumanMessage(content=question)]
348
+ messages = graph.invoke({"messages": messages})
349
+ for m in messages["messages"]:
350
+ m.pretty_print()