Spaces:
Configuration error
Configuration error
Commit
·
131e06d
1
Parent(s):
d3455e3
Create ReAct Agent with Langgraph nodes
Browse files- nodes_react_agent.py +101 -0
- 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 |
+
}
|