felixmortas commited on
Commit
131e06d
·
1 Parent(s): d3455e3

Create ReAct Agent with Langgraph nodes

Browse files
Files changed (2) hide show
  1. nodes_react_agent.py +101 -0
  2. nodes_react_agent_test.ipynb +312 -0
nodes_react_agent.py ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from dotenv import load_dotenv
3
+
4
+ from typing import TypedDict, Optional, Annotated
5
+
6
+ from langchain_google_genai import ChatGoogleGenerativeAI
7
+ from langchain_mistralai import ChatMistralAI
8
+ from langchain_groq import ChatGroq
9
+
10
+ from langgraph.graph import StateGraph, START, END
11
+ from langchain_core.messages import AnyMessage, HumanMessage
12
+ from langgraph.graph.message import add_messages
13
+ from langgraph.prebuilt import ToolNode, tools_condition
14
+
15
+ from custom_tools import custom_tools
16
+
17
+ class QuestionState(TypedDict):
18
+ input_file: Optional[str]
19
+ messages: Annotated[list[AnyMessage], add_messages]
20
+
21
+ class NodesReActAgent:
22
+ def __init__(self, provider: str="Google", model: str="gemini-2.5-pro"):
23
+ print('Initializing ReActAgent...')
24
+ load_dotenv()
25
+
26
+ # Set up the LLM based on provider
27
+ if provider == "Google":
28
+ os.environ["GOOGLE_API_KEY"] = os.getenv("GOOGLE")
29
+ llm = ChatGoogleGenerativeAI(model=model, temperature=0, max_retries=5)
30
+ elif provider == "Mistral":
31
+ os.environ["MISTRAL_API_KEY"] = os.getenv("MISTRAL")
32
+ llm = ChatMistralAI(model=model, temperature=0, max_retries=5)
33
+ elif provider == "Groq":
34
+ os.environ["GROQ_API_KEY"] = os.getenv("GROQ")
35
+ llm = ChatGroq(model=model, temperature=0, max_retries=5)
36
+ else:
37
+ raise ValueError(f"Unknown provider: {provider}")
38
+
39
+ self.llm_with_tools = llm.bind_tools(custom_tools)
40
+
41
+ def assistant(state: QuestionState):
42
+ input_file = state["input_file"]
43
+
44
+ sys_prompt = f"""
45
+ You are a general AI assistant. I will ask you a question. Report your thoughts, and finish your answer with the following template: FINAL ANSWER: [YOUR FINAL ANSWER].\n
46
+ \n
47
+ YOUR FINAL ANSWER should be a number OR as few words as possible OR a comma separated list of numbers and/or strings. If you are asked for a number, DON'T use comma to write your number NEITHER use units such as $ or percent sign unless specified otherwise. If you are asked for a string, DON'T use articles, NEITHER abbreviations (e.g. for cities) capitalize the first letter, and write the digits in plain text unless specified otherwise. If you are asked for a comma separated list, apply the above rules depending, unless the first letter capitalization, whether the element to be put in the list is a number or a string.\n
48
+ \n
49
+ EXAMPLES:\n
50
+ - What is US President Obama's first name? FINAL ANSWER: Barack\n
51
+ - What are the 3 mandatory ingredients for pancakes? FINAL ANSWER: eggs, flour, milk\n
52
+ - What is the final cost of an invoice comprising a $345.00 product and a $355.00 product? Provide the answer with two decimals. FINAL ANSWER: 700.00\n
53
+ - How many pairs of chromosomes does a human cell contain? FINAL ANSWER : 23\n
54
+ \n
55
+ \n
56
+ You will be provided with tools to help you answer questions.\n
57
+ If you are asked to make a calculation, absolutely use the tools provided to you. You should AVOID calculating by yourself and ABSOLUTELY use appropriate tools.\n
58
+ If you are asked to find something in a list of things or people, prefer using the wiki_search tool. Else, prefer to use the web_search tool. After using the web_search tool, look for the first URL provided with the url_search tool and ask yourself if the answer is in the tool response. If it is, answer the question. If not, search on other links.\n
59
+ \n
60
+ If needed, use one tool first, then use the output of that tool as an input to another thinking then to the use of another tool.\n
61
+ \n
62
+ \n You have access to some optional files. Currently the loaded file is: {input_file}"
63
+ """
64
+
65
+ return {
66
+ "messages": [self.llm_with_tools.invoke([sys_prompt] + state["messages"])],
67
+ "input_file": state["input_file"]
68
+ }
69
+
70
+ # The graph
71
+ builder = StateGraph(QuestionState)
72
+
73
+ # Define nodes: these do the work
74
+ builder.add_node("assistant", assistant)
75
+ builder.add_node("tools", ToolNode(custom_tools))
76
+
77
+ builder.add_edge(START, "assistant")
78
+ builder.add_conditional_edges(
79
+ "assistant",
80
+ # If the latest message requires a tool, route to "tools"
81
+ # Otherwise, route to "END" and provide a direct response
82
+ tools_condition,
83
+ )
84
+ builder.add_edge("tools", "assistant")
85
+ self.react_graph = builder.compile()
86
+
87
+ print(f"ReActAgent initialized with {provider} - {model}.")
88
+
89
+ def __call__(self, question: str, input_file: str) -> str:
90
+ input_msg = [HumanMessage(content=question)]
91
+ out = self.react_graph.invoke({"messages": input_msg, "input_file": input_file})
92
+
93
+ for o in out["messages"]:
94
+ o.pretty_print()
95
+ # The last message contains the agent's reply
96
+ reply = out["messages"][-1].content
97
+ # Optionally, strip out “Final Answer:” headers
98
+ if "FINAL ANSWER: " in reply:
99
+ reply = reply.split("FINAL ANSWER: ")[-1].strip()
100
+ return reply
101
+
nodes_react_agent_test.ipynb ADDED
@@ -0,0 +1,312 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "code",
5
+ "execution_count": null,
6
+ "id": "c446bcd1",
7
+ "metadata": {},
8
+ "outputs": [
9
+ {
10
+ "name": "stdout",
11
+ "output_type": "stream",
12
+ "text": [
13
+ "Initializing ReActAgent...\n",
14
+ "ReActAgent initialized with Google - gemini-2.5-pro.\n",
15
+ "================================\u001b[1m Human Message \u001b[0m=================================\n",
16
+ "\n",
17
+ "The attached Excel file contains the sales of menu items for a local fast-food chain. What were the total sales that the chain made from food (not including drinks)? Express your answer in USD with two decimal places.\n",
18
+ "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
19
+ "Tool Calls:\n",
20
+ " read_file_content (4da7a0ba-f9f5-471f-abd7-6993c9084eb5)\n",
21
+ " Call ID: 4da7a0ba-f9f5-471f-abd7-6993c9084eb5\n",
22
+ " Args:\n",
23
+ " file_name: 7bd855d8-463d-4ed5-93ca-5fe35145f733.xlsx\n",
24
+ "=================================\u001b[1m Tool Message \u001b[0m=================================\n",
25
+ "Name: read_file_content\n",
26
+ "\n",
27
+ " Location Burgers Hot Dogs Salads Fries Ice Cream Soda\n",
28
+ " Pinebrook 1594 1999 2002 2005 1977 1980\n",
29
+ " Wharvton 1983 2008 2014 2015 2017 2018\n",
30
+ " Sagrada 2019 2022 2022 2023 2021 2019\n",
31
+ " Algrimand 1958 1971 1982 1989 1998 2009\n",
32
+ " Marztep 2015 2016 2018 2019 2021 2022\n",
33
+ "San Cecelia 2011 2010 2012 2013 2015 2016\n",
34
+ " Pimento 2017 1999 2001 2003 1969 2967\n",
35
+ " Tinseles 1967 1969 1982 1994 2005 2006\n",
36
+ " Rosdale 2007 2009 2021 1989 2005 2011\n",
37
+ "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
38
+ "Tool Calls:\n",
39
+ " sum_excel_cols (222f4931-da00-47e3-9f2c-3a84247c1ca4)\n",
40
+ " Call ID: 222f4931-da00-47e3-9f2c-3a84247c1ca4\n",
41
+ " Args:\n",
42
+ " file_name: 7bd855d8-463d-4ed5-93ca-5fe35145f733.xlsx\n",
43
+ " column_names: ['Burgers', 'Hot Dogs', 'Salads', 'Fries', 'Ice Cream']\n",
44
+ "=================================\u001b[1m Tool Message \u001b[0m=================================\n",
45
+ "Name: sum_excel_cols\n",
46
+ "\n",
47
+ "89706\n",
48
+ "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
49
+ "\n",
50
+ "FINAL ANSWER: 89706.00\n"
51
+ ]
52
+ }
53
+ ],
54
+ "source": [
55
+ "from nodes_react_agent import NodesReActAgent\n",
56
+ "\n",
57
+ "agent = NodesReActAgent()\n",
58
+ "\n",
59
+ "response = agent(\"The attached Excel file contains the sales of menu items for a local fast-food chain. What were the total sales that the chain made from food (not including drinks)? Express your answer in USD with two decimal places.\", \"7bd855d8-463d-4ed5-93ca-5fe35145f733.xlsx\")"
60
+ ]
61
+ },
62
+ {
63
+ "cell_type": "code",
64
+ "execution_count": 9,
65
+ "id": "a8855a9f",
66
+ "metadata": {},
67
+ "outputs": [
68
+ {
69
+ "data": {
70
+ "text/plain": [
71
+ "'89706.00'"
72
+ ]
73
+ },
74
+ "execution_count": 9,
75
+ "metadata": {},
76
+ "output_type": "execute_result"
77
+ }
78
+ ],
79
+ "source": [
80
+ "response"
81
+ ]
82
+ },
83
+ {
84
+ "cell_type": "code",
85
+ "execution_count": null,
86
+ "id": "e02ba404",
87
+ "metadata": {},
88
+ "outputs": [],
89
+ "source": [
90
+ "import os\n",
91
+ "from dotenv import load_dotenv \n",
92
+ "from typing import TypedDict, Optional, Annotated\n",
93
+ "from langgraph.graph import StateGraph, START, END\n",
94
+ "from langchain_google_genai import ChatGoogleGenerativeAI\n",
95
+ "from langchain_core.messages import AnyMessage, HumanMessage\n",
96
+ "from langgraph.graph.message import add_messages\n",
97
+ "from langgraph.prebuilt import ToolNode, tools_condition\n",
98
+ "from IPython.display import Image, display\n",
99
+ "\n",
100
+ "load_dotenv() \n",
101
+ "os.environ[\"GOOGLE_API_KEY\"] = os.getenv(\"GOOGLE\")\n",
102
+ "model = ChatGoogleGenerativeAI(model=\"gemini-2.5-flash\", temperature=0)"
103
+ ]
104
+ },
105
+ {
106
+ "cell_type": "code",
107
+ "execution_count": 2,
108
+ "id": "7ec6fbfa",
109
+ "metadata": {},
110
+ "outputs": [],
111
+ "source": [
112
+ "class QuestionState(TypedDict):\n",
113
+ " input_file: Optional[str]\n",
114
+ " messages: Annotated[list[AnyMessage], add_messages]\n"
115
+ ]
116
+ },
117
+ {
118
+ "cell_type": "code",
119
+ "execution_count": 3,
120
+ "id": "34aa1a5c",
121
+ "metadata": {},
122
+ "outputs": [],
123
+ "source": [
124
+ "from custom_tools import custom_tools\n",
125
+ "\n",
126
+ "llm_with_tools = model.bind_tools(custom_tools)"
127
+ ]
128
+ },
129
+ {
130
+ "cell_type": "code",
131
+ "execution_count": 4,
132
+ "id": "c8e0d0ce",
133
+ "metadata": {},
134
+ "outputs": [],
135
+ "source": [
136
+ "def assistant(state: QuestionState):\n",
137
+ " input_file = state[\"input_file\"]\n",
138
+ "\n",
139
+ " sys_prompt = f\"\"\"\n",
140
+ " You are a general AI assistant. I will ask you a question. Report your thoughts, and finish your answer with the following template: FINAL ANSWER: [YOUR FINAL ANSWER].\\n\n",
141
+ " \\n\n",
142
+ " YOUR FINAL ANSWER should be a number OR as few words as possible OR a comma separated list of numbers and/or strings. If you are asked for a number, DON'T use comma to write your number NEITHER use units such as $ or percent sign unless specified otherwise. If you are asked for a string, DON'T use articles, NEITHER abbreviations (e.g. for cities) capitalize the first letter, and write the digits in plain text unless specified otherwise. If you are asked for a comma separated list, apply the above rules depending, unless the first letter capitalization, whether the element to be put in the list is a number or a string.\\n\n",
143
+ " \\n\n",
144
+ " EXAMPLES:\\n\n",
145
+ " - What is US President Obama's first name? FINAL ANSWER: Barack\\n\n",
146
+ " - What are the 3 mandatory ingredients for pancakes? FINAL ANSWER: eggs, flour, milk\\n\n",
147
+ " - What is the final cost of an invoice comprising a $345.00 product and a $355.00 product? Provide the answer with two decimals. FINAL ANSWER: 700.00\\n\n",
148
+ " - How many pairs of chromosomes does a human cell contain? FINAL ANSWER : 23\\n\n",
149
+ " \\n\n",
150
+ " \\n\n",
151
+ " You will be provided with tools to help you answer questions.\\n\n",
152
+ " If you are asked to make a calculation, absolutely use the tools provided to you. You should AVOID calculating by yourself and ABSOLUTELY use appropriate tools.\\n\n",
153
+ " If you are asked to find something in a list of things or people, prefer using the wiki_search tool. Else, prefer to use the web_search tool. After using the web_search tool, look for the first URL provided with the url_search tool and ask yourself if the answer is in the tool response. If it is, answer the question. If not, search on other links.\\n\n",
154
+ " \\n\n",
155
+ " If needed, use one tool first, then use the output of that tool as an input to another thinking then to the use of another tool.\\n\n",
156
+ " \\n\n",
157
+ " \\n You have access to some optional files. Currently the loaded file is: {input_file}\"\n",
158
+ " \"\"\"\n",
159
+ " \n",
160
+ " return {\n",
161
+ " \"messages\": [llm_with_tools.invoke([sys_prompt] + state[\"messages\"])],\n",
162
+ " \"input_file\": state[\"input_file\"]\n",
163
+ " }"
164
+ ]
165
+ },
166
+ {
167
+ "cell_type": "code",
168
+ "execution_count": 5,
169
+ "id": "5b569f8c",
170
+ "metadata": {},
171
+ "outputs": [
172
+ {
173
+ "data": {
174
+ "image/png": "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",
175
+ "text/plain": [
176
+ "<IPython.core.display.Image object>"
177
+ ]
178
+ },
179
+ "metadata": {},
180
+ "output_type": "display_data"
181
+ }
182
+ ],
183
+ "source": [
184
+ "# The graph\n",
185
+ "builder = StateGraph(QuestionState)\n",
186
+ "\n",
187
+ "# Define nodes: these do the work\n",
188
+ "builder.add_node(\"assistant\", assistant)\n",
189
+ "builder.add_node(\"tools\", ToolNode(custom_tools))\n",
190
+ "\n",
191
+ "# Define edges: these determine how the control flow moves\n",
192
+ "builder.add_edge(START, \"assistant\")\n",
193
+ "builder.add_conditional_edges(\n",
194
+ " \"assistant\",\n",
195
+ " # If the latest message requires a tool, route to \"tools\"\n",
196
+ " # Otherwise, route to \"END\" and provide a direct response\n",
197
+ " tools_condition,\n",
198
+ ")\n",
199
+ "builder.add_edge(\"tools\", \"assistant\")\n",
200
+ "react_graph = builder.compile()\n",
201
+ "\n",
202
+ "# Show the butler's thought process\n",
203
+ "display(Image(react_graph.get_graph(xray=True).draw_mermaid_png()))"
204
+ ]
205
+ },
206
+ {
207
+ "cell_type": "code",
208
+ "execution_count": 6,
209
+ "id": "5dcd88bf",
210
+ "metadata": {},
211
+ "outputs": [],
212
+ "source": [
213
+ "# Get keys for your project from the project settings page: https://cloud.langfuse.com\n",
214
+ "os.environ[\"LANGFUSE_PUBLIC_KEY\"] = os.getenv(\"LANGFUSE_PUBLIC\")\n",
215
+ "os.environ[\"LANGFUSE_SECRET_KEY\"] = os.getenv(\"LANGFUSE_SECRET\")\n",
216
+ "os.environ[\"LANGFUSE_HOST\"] = \"https://cloud.langfuse.com\" # 🇪🇺 EU region\n",
217
+ "\n",
218
+ "from langfuse.langchain import CallbackHandler\n",
219
+ "\n",
220
+ "# Initialize Langfuse CallbackHandler for LangGraph/Langchain (tracing)\n",
221
+ "langfuse_handler = CallbackHandler()"
222
+ ]
223
+ },
224
+ {
225
+ "cell_type": "code",
226
+ "execution_count": 7,
227
+ "id": "25209443",
228
+ "metadata": {},
229
+ "outputs": [
230
+ {
231
+ "name": "stdout",
232
+ "output_type": "stream",
233
+ "text": [
234
+ "================================\u001b[1m Human Message \u001b[0m=================================\n",
235
+ "\n",
236
+ "The attached Excel file contains the sales of menu items for a local fast-food chain. What were the total sales that the chain made from food (not including drinks)? Express your answer in USD with two decimal places.\n",
237
+ "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
238
+ "Tool Calls:\n",
239
+ " read_file_content (4ed0c3b2-eccc-465a-ab92-38d2ff4ae496)\n",
240
+ " Call ID: 4ed0c3b2-eccc-465a-ab92-38d2ff4ae496\n",
241
+ " Args:\n",
242
+ " file_name: 7bd855d8-463d-4ed5-93ca-5fe35145f733.xlsx\n",
243
+ "=================================\u001b[1m Tool Message \u001b[0m=================================\n",
244
+ "Name: read_file_content\n",
245
+ "\n",
246
+ " Location Burgers Hot Dogs Salads Fries Ice Cream Soda\n",
247
+ " Pinebrook 1594 1999 2002 2005 1977 1980\n",
248
+ " Wharvton 1983 2008 2014 2015 2017 2018\n",
249
+ " Sagrada 2019 2022 2022 2023 2021 2019\n",
250
+ " Algrimand 1958 1971 1982 1989 1998 2009\n",
251
+ " Marztep 2015 2016 2018 2019 2021 2022\n",
252
+ "San Cecelia 2011 2010 2012 2013 2015 2016\n",
253
+ " Pimento 2017 1999 2001 2003 1969 2967\n",
254
+ " Tinseles 1967 1969 1982 1994 2005 2006\n",
255
+ " Rosdale 2007 2009 2021 1989 2005 2011\n",
256
+ "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
257
+ "Tool Calls:\n",
258
+ " sum_excel_cols (74b46301-3530-4e91-88bd-347131e46394)\n",
259
+ " Call ID: 74b46301-3530-4e91-88bd-347131e46394\n",
260
+ " Args:\n",
261
+ " file_name: 7bd855d8-463d-4ed5-93ca-5fe35145f733.xlsx\n",
262
+ " column_names: ['Burgers', 'Hot Dogs', 'Salads', 'Fries', 'Ice Cream']\n",
263
+ "=================================\u001b[1m Tool Message \u001b[0m=================================\n",
264
+ "Name: sum_excel_cols\n",
265
+ "\n",
266
+ "89706\n",
267
+ "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
268
+ "\n",
269
+ "FINAL ANSWER: 89706.00\n"
270
+ ]
271
+ }
272
+ ],
273
+ "source": [
274
+ "messages = [HumanMessage(content=\"The attached Excel file contains the sales of menu items for a local fast-food chain. What were the total sales that the chain made from food (not including drinks)? Express your answer in USD with two decimal places.\")]\n",
275
+ "messages = react_graph.invoke({\"messages\": messages, \"input_file\": \"7bd855d8-463d-4ed5-93ca-5fe35145f733.xlsx\"},\n",
276
+ " config={\n",
277
+ " \"callbacks\": [langfuse_handler],\n",
278
+ " \"metadata\": {\n",
279
+ " \"langfuse_user_id\": \"felix\",\n",
280
+ " \"langfuse_session_id\": \"session_1\",\n",
281
+ " \"langfuse_tags\": [\"Node\", \"ReActAgent\"]\n",
282
+ " }\n",
283
+ " })\n",
284
+ "\n",
285
+ "# Show the messages\n",
286
+ "for m in messages['messages']:\n",
287
+ " m.pretty_print()"
288
+ ]
289
+ }
290
+ ],
291
+ "metadata": {
292
+ "kernelspec": {
293
+ "display_name": "Python 3",
294
+ "language": "python",
295
+ "name": "python3"
296
+ },
297
+ "language_info": {
298
+ "codemirror_mode": {
299
+ "name": "ipython",
300
+ "version": 3
301
+ },
302
+ "file_extension": ".py",
303
+ "mimetype": "text/x-python",
304
+ "name": "python",
305
+ "nbconvert_exporter": "python",
306
+ "pygments_lexer": "ipython3",
307
+ "version": "3.11.13"
308
+ }
309
+ },
310
+ "nbformat": 4,
311
+ "nbformat_minor": 5
312
+ }