I have made a ai agent that can help me. but right now it can only call one tool or 2 at the time. but for example for a question like "age of bratt pid multiplied by 300" it needs to search for the age and after it did that use the multiply tool. 
import gradio as gr
from langchain_ollama import ChatOllama
from langchain.agents import AgentExecutor, create_openai_tools_agent
from langchain import hub
from langchain.tools import DuckDuckGoSearchRun, tool
from langchain.prompts import ChatPromptTemplate
import math
from typing import Dict, Any
from langsmith import traceable
import os

os.environ["LANGCHAIN_TRACING_V2"] = "true"
os.environ["LANGCHAIN_API_KEY"] = "<TRUFFLEHOG_REDACTED_LANGSMITH>"
os.environ["LANGCHAIN_PROJECT"] = "ollama tool call"

# Initialize the Ollama model
llm = ChatOllama(model="llama3.1", temperature=1)

# Create the DuckDuckGo search tool
search_tool = DuckDuckGoSearchRun()

# Create a math tool
@tool
def multiply(a: int, b: int) -> int:
    """Multiply a and b."""
    return a * b

# Create the tools list
tools = [search_tool, multiply]

# Get the OpenAI tools agent prompt
# prompt = hub.pull("hwchase17/openai-tools-agent")
prompt = ChatPromptTemplate.from_messages(
    [
        ("system", "You are a helpful assistant. You fulfill any request the user asks!"),
        ("placeholder", "{chat_history}"),
        ("human", "{input}"),
        ("placeholder", "{agent_scratchpad}"),
    ]
)
# prompt = "you are a helpful assistant"

# Create the agent
agent = create_openai_tools_agent(llm, tools, prompt)

# Create the agent executor
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)

# Function to run the agent
@traceable
def run_agent(input_text: str) -> Dict[str, Any]:
    try:
        response = agent_executor.invoke({"input": input_text})
        return {"output": response["output"]}
    except Exception as e:
        return {"output": f"An error occurred: {str(e)}"}

# Create the Gradio interface
iface = gr.Interface(
    fn=run_agent,
    inputs=gr.Textbox(lines=2, placeholder="Enter your question here..."),
    outputs=gr.Textbox(label="Agent Response"),
    title="Ollama Agent with Tool Calling",
    description="Ask a question, and the agent will use Ollama (llama3.1 model) with tool calling to answer.",
)

# Launch the interface
iface.launch()



But to have a work around that I want to implement a langgraph node workflow for the llm. For you to understand the key concepts of langgraph I have a example code right here:
def function_1(input_1):
    return input_1 + " Hi "

def function_2(input_2):
    return input_2 + "there"

from langgraph.graph import Graph

# Define a Langchain graph
workflow = Graph()

workflow.add_node("node_1", function_1)
workflow.add_node("node_2", function_2)

workflow.add_edge('node_1', 'node_2')

workflow.set_entry_point("node_1")
workflow.set_finish_point("node_2")

app = workflow.compile()

app.invoke("Hello")

'Hello Hi there'

input = 'Hello'
for output in app.stream(input):
    # stream() yields dictionaries with output keyed by node name
    for key, value in output.items():
        print(f"Output from node '{key}':")
        print("---")
        print(value)
    print("\n---\n")

Output from node 'node_1':
---
Hello Hi 

---

Output from node 'node_2':
---
Hello Hi there

---

Output from node '__end__':
---
Hello Hi there

---

As you can see, we can run the nodes as functions and return some values from them.
Adding LLM Call

Now, let's make the first node as an "Agent" that can call Open AI models. We can use langchain to make this call easy for us.

!pip install langchain langchain_openai

Requirement already satisfied: langchain in /usr/local/python/3.10.13/lib/python3.10/site-packages (0.1.7)
Requirement already satisfied: langchain_openai in /usr/local/python/3.10.13/lib/python3.10/site-packages (0.0.6)
Requirement already satisfied: PyYAML>=5.3 in /home/codespace/.local/lib/python3.10/site-packages (from langchain) (6.0.1)
Requirement already satisfied: SQLAlchemy<3,>=1.4 in /usr/local/python/3.10.13/lib/python3.10/site-packages (from langchain) (2.0.27)
Requirement already satisfied: aiohttp<4.0.0,>=3.8.3 in /usr/local/python/3.10.13/lib/python3.10/site-packages (from langchain) (3.9.3)
Requirement already satisfied: async-timeout<5.0.0,>=4.0.0 in /usr/local/python/3.10.13/lib/python3.10/site-packages (from langchain) (4.0.3)
Requirement already satisfied: dataclasses-json<0.7,>=0.5.7 in /usr/local/python/3.10.13/lib/python3.10/site-packages (from langchain) (0.6.4)
Requirement already satisfied: jsonpatch<2.0,>=1.33 in /usr/local/python/3.10.13/lib/python3.10/site-packages (from langchain) (1.33)
Requirement already satisfied: langchain-community<0.1,>=0.0.20 in /usr/local/python/3.10.13/lib/python3.10/site-packages (from langchain) (0.0.20)
Requirement already satisfied: langchain-core<0.2,>=0.1.22 in /usr/local/python/3.10.13/lib/python3.10/site-packages (from langchain) (0.1.23)
Requirement already satisfied: langsmith<0.1,>=0.0.83 in /usr/local/python/3.10.13/lib/python3.10/site-packages (from langchain) (0.0.87)
Requirement already satisfied: numpy<2,>=1 in /home/codespace/.local/lib/python3.10/site-packages (from langchain) (1.26.4)
Requirement already satisfied: pydantic<3,>=1 in /usr/local/python/3.10.13/lib/python3.10/site-packages (from langchain) (2.6.1)
Requirement already satisfied: requests<3,>=2 in /home/codespace/.local/lib/python3.10/site-packages (from langchain) (2.31.0)
Requirement already satisfied: tenacity<9.0.0,>=8.1.0 in /home/codespace/.local/lib/python3.10/site-packages (from langchain) (8.2.3)
Requirement already satisfied: openai<2.0.0,>=1.10.0 in /usr/local/python/3.10.13/lib/python3.10/site-packages (from langchain_openai) (1.12.0)
Requirement already satisfied: tiktoken<1,>=0.5.2 in /usr/local/python/3.10.13/lib/python3.10/site-packages (from langchain_openai) (0.6.0)
Requirement already satisfied: aiosignal>=1.1.2 in /usr/local/python/3.10.13/lib/python3.10/site-packages (from aiohttp<4.0.0,>=3.8.3->langchain) (1.3.1)
Requirement already satisfied: attrs>=17.3.0 in /home/codespace/.local/lib/python3.10/site-packages (from aiohttp<4.0.0,>=3.8.3->langchain) (23.2.0)
Requirement already satisfied: frozenlist>=1.1.1 in /usr/local/python/3.10.13/lib/python3.10/site-packages (from aiohttp<4.0.0,>=3.8.3->langchain) (1.4.1)
Requirement already satisfied: multidict<7.0,>=4.5 in /usr/local/python/3.10.13/lib/python3.10/site-packages (from aiohttp<4.0.0,>=3.8.3->langchain) (6.0.5)
Requirement already satisfied: yarl<2.0,>=1.0 in /usr/local/python/3.10.13/lib/python3.10/site-packages (from aiohttp<4.0.0,>=3.8.3->langchain) (1.9.4)
Requirement already satisfied: marshmallow<4.0.0,>=3.18.0 in /usr/local/python/3.10.13/lib/python3.10/site-packages (from dataclasses-json<0.7,>=0.5.7->langchain) (3.20.2)
Requirement already satisfied: typing-inspect<1,>=0.4.0 in /usr/local/python/3.10.13/lib/python3.10/site-packages (from dataclasses-json<0.7,>=0.5.7->langchain) (0.9.0)
Requirement already satisfied: jsonpointer>=1.9 in /home/codespace/.local/lib/python3.10/site-packages (from jsonpatch<2.0,>=1.33->langchain) (2.4)
Requirement already satisfied: anyio<5,>=3 in /home/codespace/.local/lib/python3.10/site-packages (from langchain-core<0.2,>=0.1.22->langchain) (4.2.0)
Requirement already satisfied: packaging<24.0,>=23.2 in /home/codespace/.local/lib/python3.10/site-packages (from langchain-core<0.2,>=0.1.22->langchain) (23.2)
Requirement already satisfied: distro<2,>=1.7.0 in /usr/local/python/3.10.13/lib/python3.10/site-packages (from openai<2.0.0,>=1.10.0->langchain_openai) (1.9.0)
Requirement already satisfied: httpx<1,>=0.23.0 in /home/codespace/.local/lib/python3.10/site-packages (from openai<2.0.0,>=1.10.0->langchain_openai) (0.26.0)
Requirement already satisfied: sniffio in /home/codespace/.local/lib/python3.10/site-packages (from openai<2.0.0,>=1.10.0->langchain_openai) (1.3.0)
Requirement already satisfied: tqdm>4 in /usr/local/python/3.10.13/lib/python3.10/site-packages (from openai<2.0.0,>=1.10.0->langchain_openai) (4.66.2)
Requirement already satisfied: typing-extensions<5,>=4.7 in /home/codespace/.local/lib/python3.10/site-packages (from openai<2.0.0,>=1.10.0->langchain_openai) (4.9.0)
Requirement already satisfied: annotated-types>=0.4.0 in /usr/local/python/3.10.13/lib/python3.10/site-packages (from pydantic<3,>=1->langchain) (0.6.0)
Requirement already satisfied: pydantic-core==2.16.2 in /usr/local/python/3.10.13/lib/python3.10/site-packages (from pydantic<3,>=1->langchain) (2.16.2)
Requirement already satisfied: charset-normalizer<4,>=2 in /home/codespace/.local/lib/python3.10/site-packages (from requests<3,>=2->langchain) (3.3.2)
Requirement already satisfied: idna<4,>=2.5 in /home/codespace/.local/lib/python3.10/site-packages (from requests<3,>=2->langchain) (3.6)
Requirement already satisfied: urllib3<3,>=1.21.1 in /usr/local/python/3.10.13/lib/python3.10/site-packages (from requests<3,>=2->langchain) (2.0.7)
Requirement already satisfied: certifi>=2017.4.17 in /home/codespace/.local/lib/python3.10/site-packages (from requests<3,>=2->langchain) (2024.2.2)
Requirement already satisfied: greenlet!=0.4.17 in /usr/local/python/3.10.13/lib/python3.10/site-packages (from SQLAlchemy<3,>=1.4->langchain) (3.0.3)
Requirement already satisfied: regex>=2022.1.18 in /usr/local/python/3.10.13/lib/python3.10/site-packages (from tiktoken<1,>=0.5.2->langchain_openai) (2023.12.25)
Requirement already satisfied: exceptiongroup>=1.0.2 in /home/codespace/.local/lib/python3.10/site-packages (from anyio<5,>=3->langchain-core<0.2,>=0.1.22->langchain) (1.2.0)
Requirement already satisfied: httpcore==1.* in /home/codespace/.local/lib/python3.10/site-packages (from httpx<1,>=0.23.0->openai<2.0.0,>=1.10.0->langchain_openai) (1.0.2)
Requirement already satisfied: h11<0.15,>=0.13 in /home/codespace/.local/lib/python3.10/site-packages (from httpcore==1.*->httpx<1,>=0.23.0->openai<2.0.0,>=1.10.0->langchain_openai) (0.14.0)
Requirement already satisfied: mypy-extensions>=0.3.0 in /usr/local/python/3.10.13/lib/python3.10/site-packages (from typing-inspect<1,>=0.4.0->dataclasses-json<0.7,>=0.5.7->langchain) (1.0.0)

A usual call to ChatOpenAI model in LangChain is done as below:

First set your API keys for OpenAI

!pip install python-dotenv

Requirement already satisfied: python-dotenv in /usr/local/python/3.10.13/lib/python3.10/site-packages (1.0.1)

from dotenv import load_dotenv
import os

# Load environment variables from .env file
load_dotenv()

# Now you can access your environment variables using os.environ
os.environ['OPENAI_API_KEY'] = os.environ.get("OPENAI_API_KEY")

from langchain_openai import ChatOpenAI

# Set the model as ChatOpenAI
model = ChatOpenAI(temperature=0) 

#Call the model with a user message
model.invoke('Hey there')

AIMessage(content='Hello! How can I assist you today?')

And if you just want to see the AI response, you can do the following:

model.invoke('Hey there').content

'Hello! How can I assist you today?'

Cool! Keeping that in mind, let's change the function 1 above so that we can send the user question to the model. Then we will send this response to function 2, which will add a short string and return to the user.

def function_1(input_1):
    response = model.invoke(input_1)
    return response.content

def function_2(input_2):
    return "Agent Says: " + input_2

# Define a Langchain graph
workflow = Graph()

#calling node 1 as agent
workflow.add_node("agent", function_1)
workflow.add_node("node_2", function_2)

workflow.add_edge('agent', 'node_2')

workflow.set_entry_point("agent")
workflow.set_finish_point("node_2")

app = workflow.compile()

app.invoke("Hey there")

'Agent Says: Hello! How can I assist you today?'

input = 'Hey there'
for output in app.stream(input):
    # stream() yields dictionaries with output keyed by node name
    for key, value in output.items():
        print(f"Output from node '{key}':")
        print("---")
        print(value)
    print("\n---\n")

Output from node 'agent':
---
Hello! How can I assist you today?

---

Output from node 'node_2':
---
Agent Says: Hello! How can I assist you today?

---

Output from node '__end__':
---
Agent Says: Hello! How can I assist you today?

---

First functional Agent App - City Temperature
Step 1: Parse the city mentioned

Let's extract the city that a user mentions in a query

def function_1(input_1):
    complete_query = "Your task is to provide only the city name based on the user query. \
        Nothing more, just the city name mentioned. Following is the user query: " + input_1
    response = model.invoke(complete_query)
    return response.content

def function_2(input_2):
    return "Agent Says: " + input_2

# Define a Langchain graph
workflow = Graph()

#calling node 1 as agent
workflow.add_node("agent", function_1)
workflow.add_node("node_2", function_2)

workflow.add_edge('agent', 'node_2')

workflow.set_entry_point("agent")
workflow.set_finish_point("node_2")

app = workflow.compile()

app.invoke("What's the temperature in Las Vegas")

'Agent Says: Las Vegas'

Step 2: Adding a weather API call

What if we want the function 2 to take the city name and give us the weather for that city.

Well we know that Open Weather Map is integrated into LangChain

We need to install pyown, create an API key on the website of Open Weather Map (which takes a few hours to activate) and then run the cells below to get weather of a given city.

!pip install pyowm

Requirement already satisfied: pyowm in /usr/local/python/3.10.13/lib/python3.10/site-packages (3.3.0)
Requirement already satisfied: requests<3,>=2.20.0 in /home/codespace/.local/lib/python3.10/site-packages (from pyowm) (2.31.0)
Requirement already satisfied: geojson<3,>=2.3.0 in /usr/local/python/3.10.13/lib/python3.10/site-packages (from pyowm) (2.5.0)
Requirement already satisfied: PySocks<2,>=1.7.1 in /usr/local/python/3.10.13/lib/python3.10/site-packages (from pyowm) (1.7.1)
Requirement already satisfied: charset-normalizer<4,>=2 in /home/codespace/.local/lib/python3.10/site-packages (from requests<3,>=2.20.0->pyowm) (3.3.2)
Requirement already satisfied: idna<4,>=2.5 in /home/codespace/.local/lib/python3.10/site-packages (from requests<3,>=2.20.0->pyowm) (3.6)
Requirement already satisfied: urllib3<3,>=1.21.1 in /usr/local/python/3.10.13/lib/python3.10/site-packages (from requests<3,>=2.20.0->pyowm) (2.0.7)
Requirement already satisfied: certifi>=2017.4.17 in /home/codespace/.local/lib/python3.10/site-packages (from requests<3,>=2.20.0->pyowm) (2024.2.2)

from langchain_community.utilities import OpenWeatherMapAPIWrapper
load_dotenv()
os.environ["OPENWEATHERMAP_API_KEY"] = os.environ.get("OPENWEATHERMAP_API_KEY")

weather = OpenWeatherMapAPIWrapper()

weather_data = weather.run("Las Vegas")
print(weather_data)

In Las Vegas, the current weather is as follows:
Detailed status: clear sky
Wind speed: 3.6 m/s, direction: 90°
Humidity: 33%
Temperature: 
  - Current: 18.71°C
  - High: 19.38°C
  - Low: 18.16°C
  - Feels like: 17.5°C
Rain: {}
Heat index: None
Cloud cover: 0%

Now, let's integrate this into function 2 and call the function two as a "tool" or "weather_agent" instead of "node_2" in our workflow.

def function_1(input_1):
    complete_query = "Your task is to provide only the city name based on the user query. \
        Nothing more, just the city name mentioned. Following is the user query: " + input_1
    response = model.invoke(complete_query)
    return response.content

def function_2(input_2):
    weather_data = weather.run(input_2)
    return weather_data

from langgraph.graph import Graph

workflow = Graph()

#calling node 1 as agent
workflow.add_node("agent", function_1)
workflow.add_node("tool", function_2)

workflow.add_edge('agent', 'tool')

workflow.set_entry_point("agent")
workflow.set_finish_point("tool")

app = workflow.compile()

app.invoke("What's the temperature in Las Vegas")

'In Las Vegas, the current weather is as follows:\nDetailed status: clear sky\nWind speed: 3.6 m/s, direction: 90°\nHumidity: 33%\nTemperature: \n  - Current: 18.71°C\n  - High: 19.38°C\n  - Low: 18.16°C\n  - Feels like: 17.5°C\nRain: {}\nHeat index: None\nCloud cover: 0%'

input = "What's the temperature in Las Vegas"
for output in app.stream(input):
    # stream() yields dictionaries with output keyed by node name
    for key, value in output.items():
        print(f"Output from node '{key}':")
        print("---")
        print(value)
    print("\n---\n")

Output from node 'agent':
---
Las Vegas

---

Output from node 'tool':
---
In Las Vegas, the current weather is as follows:
Detailed status: clear sky
Wind speed: 3.6 m/s, direction: 90°
Humidity: 33%
Temperature: 
  - Current: 18.65°C
  - High: 19.38°C
  - Low: 18.16°C
  - Feels like: 17.43°C
Rain: {}
Heat index: None
Cloud cover: 0%

---

Output from node '__end__':
---
In Las Vegas, the current weather is as follows:
Detailed status: clear sky
Wind speed: 3.6 m/s, direction: 90°
Humidity: 33%
Temperature: 
  - Current: 18.65°C
  - High: 19.38°C
  - Low: 18.16°C
  - Feels like: 17.43°C
Rain: {}
Heat index: None
Cloud cover: 0%

---

Step 3 Adding another LLM Call to filter results

What if we only want the temperature? But current setup gives us the full weather report.

Well we can make another LLM call to filter data

def function_3(input_3):
    complete_query = "Your task is to provide info concisely based on the user query. Following is the user query: " + "user input"
    response = model.invoke(complete_query)
    return response.content

But the issue is the user input is not available from node 2.

Can we pass user input all along from first node to the last?

Yes, we can use a dictionary and pass it between nodes (we could also use just a list, but dict makes it a bit easier)

# assign AgentState as an empty dict
AgentState = {}

# messages key will be assigned as an empty array. We will append new messages as we pass along nodes. 
AgentState["messages"] = []

AgentState

{'messages': []}

Our goal is to have this state filled as: {'messages': [HumanMessage, AIMessage, ...]]}

Also now we need to modify our functions to pass info along the new AgentState

def function_1(state):
    messages = state['messages']
    user_input = messages[-1]
    complete_query = "Your task is to provide only the city name based on the user query. \
                    Nothing more, just the city name mentioned. Following is the user query: " + user_input
    response = model.invoke(complete_query)
    state['messages'].append(response.content) # appending AIMessage response to the AgentState
    return state

def function_2(state):
    messages = state['messages']
    agent_response = messages[-1]
    weather = OpenWeatherMapAPIWrapper()
    weather_data = weather.run(agent_response)
    state['messages'].append(weather_data)
    return state

def function_3(state):
    messages = state['messages']
    user_input = messages[0]
    available_info = messages[-1]
    agent2_query = "Your task is to provide info concisely based on the user query and the available information from the internet. \
                        Following is the user query: " + user_input + " Available information: " + available_info
    response = model.invoke(agent2_query)
    return response.content

from langgraph.graph import Graph

workflow = Graph()


workflow.add_node("agent", function_1)
workflow.add_node("tool", function_2)
workflow.add_node("responder", function_3)

workflow.add_edge('agent', 'tool')
workflow.add_edge('tool', 'responder')

workflow.set_entry_point("agent")
workflow.set_finish_point("responder")

app = workflow.compile()

inputs = {"messages": ["what is the temperature in las vegas"]}
app.invoke(inputs)

'The current temperature in Las Vegas is 18.65°C.'

input = {"messages": ["what is the temperature in las vegas"]}
for output in app.stream(input):
    # stream() yields dictionaries with output keyed by node name
    for key, value in output.items():
        print(f"Output from node '{key}':")
        print("---")
        print(value)
    print("\n---\n")

Output from node 'agent':
---
{'messages': ['what is the temperature in las vegas', 'Las Vegas']}

---

Output from node 'tool':
---
{'messages': ['what is the temperature in las vegas', 'Las Vegas', 'In Las Vegas, the current weather is as follows:\nDetailed status: clear sky\nWind speed: 3.6 m/s, direction: 90°\nHumidity: 33%\nTemperature: \n  - Current: 18.71°C\n  - High: 19.38°C\n  - Low: 18.16°C\n  - Feels like: 17.5°C\nRain: {}\nHeat index: None\nCloud cover: 0%']}

---

Output from node 'responder':
---
The current temperature in Las Vegas is 18.71°C.

---

Output from node '__end__':
---
The current temperature in Las Vegas is 18.71°C.

---

As we notice that there is a lot of appending to the array going on, we can make it a bit easier with the following:

from typing import TypedDict, Annotated, Sequence
import operator
from langchain_core.messages import BaseMessage


class AgentState(TypedDict):
    messages: Annotated[Sequence[BaseMessage], operator.add]

It basically makes the state dictionary as saw previously, and also makes sure that any new message is appended to the messages array when we do the following:

{"messages": [new_array_element]}

We also realize that our app is not capable of answering simple questions like "how are you?"

inputs = {"messages": ["how are you?"]}
app.invoke(inputs)

---------------------------------------------------------------------------
NotFoundError                             Traceback (most recent call last)
Cell In[32], line 2
      1 inputs = {"messages": ["how are you?"]}
----> 2 app.invoke(inputs)

File ~/.python/current/lib/python3.10/site-packages/langgraph/pregel/__init__.py:579, in Pregel.invoke(self, input, config, output_keys, input_keys, **kwargs)
    569 def invoke(
    570     self,
    571     input: Union[dict[str, Any], Any],
   (...)
    576     **kwargs: Any,
    577 ) -> Union[dict[str, Any], Any]:
    578     latest: Union[dict[str, Any], Any] = None
--> 579     for chunk in self.stream(
    580         input,
    581         config,
    582         output_keys=output_keys if output_keys is not None else self.output,
    583         input_keys=input_keys,
    584         **kwargs,
    585     ):
    586         latest = chunk
    587     return latest

File ~/.python/current/lib/python3.10/site-packages/langgraph/pregel/__init__.py:615, in Pregel.transform(self, input, config, output_keys, input_keys, **kwargs)
    606 def transform(
    607     self,
    608     input: Iterator[Union[dict[str, Any], Any]],
   (...)
    613     **kwargs: Any,
    614 ) -> Iterator[Union[dict[str, Any], Any]]:
--> 615     for chunk in self._transform_stream_with_config(
    616         input,
    617         self._transform,
    618         config,
    619         output_keys=output_keys,
    620         input_keys=input_keys,
    621         **kwargs,
    622     ):
    623         yield chunk

File ~/.python/current/lib/python3.10/site-packages/langchain_core/runnables/base.py:1497, in Runnable._transform_stream_with_config(self, input, transformer, config, run_type, **kwargs)
   1495 try:
   1496     while True:
-> 1497         chunk: Output = context.run(next, iterator)  # type: ignore
   1498         yield chunk
   1499         if final_output_supported:

File ~/.python/current/lib/python3.10/site-packages/langgraph/pregel/__init__.py:355, in Pregel._transform(self, input, run_manager, config, input_keys, output_keys, interrupt)
    348 done, inflight = concurrent.futures.wait(
    349     futures,
    350     return_when=concurrent.futures.FIRST_EXCEPTION,
    351     timeout=self.step_timeout,
    352 )
    354 # interrupt on failure or timeout
--> 355 _interrupt_or_proceed(done, inflight, step)
    357 # apply writes to channels
    358 _apply_writes(
    359     checkpoint, channels, pending_writes, config, step + 1
    360 )

File ~/.python/current/lib/python3.10/site-packages/langgraph/pregel/__init__.py:698, in _interrupt_or_proceed(done, inflight, step)
    696             inflight.pop().cancel()
    697         # raise the exception
--> 698         raise exc
    699         # TODO this is where retry of an entire step would happen
    701 if inflight:
    702     # if we got here means we timed out

File ~/.python/current/lib/python3.10/concurrent/futures/thread.py:58, in _WorkItem.run(self)
     55     return
     57 try:
---> 58     result = self.fn(*self.args, **self.kwargs)
     59 except BaseException as exc:
     60     self.future.set_exception(exc)

File ~/.python/current/lib/python3.10/site-packages/langchain_core/runnables/base.py:4064, in RunnableBindingBase.invoke(self, input, config, **kwargs)
   4058 def invoke(
   4059     self,
   4060     input: Input,
   4061     config: Optional[RunnableConfig] = None,
   4062     **kwargs: Optional[Any],
   4063 ) -> Output:
-> 4064     return self.bound.invoke(
   4065         input,
   4066         self._merge_configs(config),
   4067         **{**self.kwargs, **kwargs},
   4068     )

File ~/.python/current/lib/python3.10/site-packages/langchain_core/runnables/base.py:2053, in RunnableSequence.invoke(self, input, config)
   2051 try:
   2052     for i, step in enumerate(self.steps):
-> 2053         input = step.invoke(
   2054             input,
   2055             # mark each step as a child run
   2056             patch_config(
   2057                 config, callbacks=run_manager.get_child(f"seq:step:{i+1}")
   2058             ),
   2059         )
   2060 # finish the root run
   2061 except BaseException as e:

File ~/.python/current/lib/python3.10/site-packages/langchain_core/runnables/base.py:3507, in RunnableLambda.invoke(self, input, config, **kwargs)
   3505 """Invoke this runnable synchronously."""
   3506 if hasattr(self, "func"):
-> 3507     return self._call_with_config(
   3508         self._invoke,
   3509         input,
   3510         self._config(config, self.func),
   3511         **kwargs,
   3512     )
   3513 else:
   3514     raise TypeError(
   3515         "Cannot invoke a coroutine function synchronously."
   3516         "Use `ainvoke` instead."
   3517     )

File ~/.python/current/lib/python3.10/site-packages/langchain_core/runnables/base.py:1246, in Runnable._call_with_config(self, func, input, config, run_type, **kwargs)
   1242     context = copy_context()
   1243     context.run(var_child_runnable_config.set, child_config)
   1244     output = cast(
   1245         Output,
-> 1246         context.run(
   1247             call_func_with_variable_args,
   1248             func,  # type: ignore[arg-type]
   1249             input,  # type: ignore[arg-type]
   1250             config,
   1251             run_manager,
   1252             **kwargs,
   1253         ),
   1254     )
   1255 except BaseException as e:
   1256     run_manager.on_chain_error(e)

File ~/.python/current/lib/python3.10/site-packages/langchain_core/runnables/config.py:326, in call_func_with_variable_args(func, input, config, run_manager, **kwargs)
    324 if run_manager is not None and accepts_run_manager(func):
    325     kwargs["run_manager"] = run_manager
--> 326 return func(input, **kwargs)

File ~/.python/current/lib/python3.10/site-packages/langchain_core/runnables/base.py:3383, in RunnableLambda._invoke(self, input, run_manager, config, **kwargs)
   3381                 output = chunk
   3382 else:
-> 3383     output = call_func_with_variable_args(
   3384         self.func, input, config, run_manager, **kwargs
   3385     )
   3386 # If the output is a runnable, invoke it
   3387 if isinstance(output, Runnable):

File ~/.python/current/lib/python3.10/site-packages/langchain_core/runnables/config.py:326, in call_func_with_variable_args(func, input, config, run_manager, **kwargs)
    324 if run_manager is not None and accepts_run_manager(func):
    325     kwargs["run_manager"] = run_manager
--> 326 return func(input, **kwargs)

Cell In[28], line 14, in function_2(state)
     12 agent_response = messages[-1]
     13 weather = OpenWeatherMapAPIWrapper()
---> 14 weather_data = weather.run(agent_response)
     15 state['messages'].append(weather_data)
     16 return state

File ~/.python/current/lib/python3.10/site-packages/langchain_community/utilities/openweathermap.py:73, in OpenWeatherMapAPIWrapper.run(self, location)
     71 """Get the current weather information for a specified location."""
     72 mgr = self.owm.weather_manager()
---> 73 observation = mgr.weather_at_place(location)
     74 w = observation.weather
     76 return self._format_weather_info(location, w)

File ~/.python/current/lib/python3.10/site-packages/pyowm/weatherapi25/weather_manager.py:53, in WeatherManager.weather_at_place(self, name)
     51 assert isinstance(name, str), "Value must be a string"
     52 params = {'q': name}
---> 53 _, json_data = self.http_client.get_json(OBSERVATION_URI, params=params)
     54 return observation.Observation.from_dict(json_data)

File ~/.python/current/lib/python3.10/site-packages/pyowm/commons/http_client.py:158, in HttpClient.get_json(self, path, params, headers)
    156 except requests.exceptions.Timeout:
    157     raise exceptions.TimeoutError('API call timeouted')
--> 158 HttpClient.check_status_code(resp.status_code, resp.text)
    159 try:
    160     return resp.status_code, resp.json()

File ~/.python/current/lib/python3.10/site-packages/pyowm/commons/http_client.py:315, in HttpClient.check_status_code(cls, status_code, payload)
    313     raise exceptions.UnauthorizedError('Invalid API Key provided')
    314 elif status_code == 404:
--> 315     raise exceptions.NotFoundError('Unable to find the resource')
    316 else:
    317     raise exceptions.BadGatewayError('Unable to contact the upstream server')

NotFoundError: Unable to find the resource

This is because we always want to parse a city and then find the weather.

We can make our agent smarter by saying only use the tool when needed, if not just respond back to the user.

The way we can do this LangGraph is:

    binding a tool to the agent
    adding a conditional edge to the agent with the option to either call the tool or not
    defining the criteria for the conditional edge as when to call the tool. We will define a function for this.

Let's start with the AgentState definition as mentioned a few cells above.

from typing import TypedDict, Annotated, Sequence
import operator
from langchain_core.messages import BaseMessage


class AgentState(TypedDict):
    messages: Annotated[Sequence[BaseMessage], operator.add]

Binding tool with agent (LLM Model) is made easy in langchain

from langchain_core.utils.function_calling import convert_to_openai_function
from langchain_community.tools.openweathermap import OpenWeatherMapQueryRun
from langchain_core.utils.function_calling import convert_to_openai_function

tools = [OpenWeatherMapQueryRun()]

model = ChatOpenAI(temperature=0, streaming=True)
functions = [convert_to_openai_function(t) for t in tools]
model = model.bind_functions(functions)

Our modified function_1 now becomes as below. The reason is, we are passing the human message as state and appending response to the state. Also, our agent now has a tool bound to it, that it can use.

def function_1(state):
    messages = state['messages']
    response = model.invoke(messages)
    return {"messages": [response]}

For function 2, we want it to setup a tool and call it. It's made easy to invoke a tool in LangChain by using ToolInvocation and executing it with ToolExecuter. Then we respond back as a FunctionMessage so that our agent (node 1) knows that the tool was used and a response from tool is available.

from langgraph.prebuilt import ToolInvocation
import json
from langchain_core.messages import FunctionMessage
from langgraph.prebuilt import ToolExecutor

tool_executor = ToolExecutor(tools)

def function_2(state):
    messages = state['messages']
    last_message = messages[-1] # this has the query we need to send to the tool provided by the agent

    parsed_tool_input = json.loads(last_message.additional_kwargs["function_call"]["arguments"])

    # We construct an ToolInvocation from the function_call and pass in the tool name and the expected str input for OpenWeatherMap tool
    action = ToolInvocation(
        tool=last_message.additional_kwargs["function_call"]["name"],
        tool_input=parsed_tool_input['__arg1'],
    )
    
    # We call the tool_executor and get back a response
    response = tool_executor.invoke(action)

    # We use the response to create a FunctionMessage
    function_message = FunctionMessage(content=str(response), name=action.tool)

    # We return a list, because this will get added to the existing list
    return {"messages": [function_message]}

Finally, we define a function for the conditional edge, to help us figure out which direction to go (tool or user response)

We can benefit from the agent (LLM) response in LangChain, which has additional_kwargs to make a function_call with the name of the tool.

So our logic is, if function_call available in the additional_kwargs, then call tool if not then end the discussion and respond back to the user

def where_to_go(state):
    messages = state['messages']
    last_message = messages[-1]
    
    if "function_call" in last_message.additional_kwargs:
        return "continue"
    else:
        return "end"

Now with all of the changes above, our LangGraph app is modified as below:

# from langgraph.graph import Graph, END

# workflow = Graph()

# Or you could import StateGraph and pass AgentState to it
from langgraph.graph import StateGraph, END
workflow = StateGraph(AgentState)

workflow.add_node("agent", function_1)
workflow.add_node("tool", function_2)

# The conditional edge requires the following info below.
# First, we define the start node. We use `agent`.
# This means these are the edges taken after the `agent` node is called.
# Next, we pass in the function that will determine which node is called next, in our case where_to_go().

workflow.add_conditional_edges("agent", where_to_go,{   # Based on the return from where_to_go
                                                        # If return is "continue" then we call the tool node.
                                                        "continue": "tool",
                                                        # Otherwise we finish. END is a special node marking that the graph should finish.
                                                        "end": END
                                                    }
)

# We now add a normal edge from `tools` to `agent`.
# This means that if `tool` is called, then it has to call the 'agent' next. 
workflow.add_edge('tool', 'agent')

# Basically, agent node has the option to call a tool node based on a condition, 
# whereas tool node must call the agent in all cases based on this setup.

workflow.set_entry_point("agent")


app = workflow.compile()

We also pass the first message using HumanMessage component available in langchain, makes it easy to differentiate from AIMessage, and FunctionMessage

from langchain_core.messages import HumanMessage

inputs = {"messages": [HumanMessage(content="what is the temperature in las vegas")]}
app.invoke(inputs)

{'messages': [HumanMessage(content='what is the temperature in las vegas'),
  AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\n  "__arg1": "Las Vegas"\n}', 'name': 'open_weather_map'}}),
  FunctionMessage(content='In Las Vegas, the current weather is as follows:\nDetailed status: clear sky\nWind speed: 3.6 m/s, direction: 90°\nHumidity: 34%\nTemperature: \n  - Current: 17.99°C\n  - High: 19.38°C\n  - Low: 16.73°C\n  - Feels like: 16.73°C\nRain: {}\nHeat index: None\nCloud cover: 0%', name='open_weather_map'),
  AIMessage(content='The current temperature in Las Vegas is 17.99°C.')]}

inputs = {"messages": [HumanMessage(content="what is the temperature in las vegas")]}
for output in app.stream(inputs):
    # stream() yields dictionaries with output keyed by node name
    for key, value in output.items():
        print(f"Output from node '{key}':")
        print("---")
        print(value)
    print("\n---\n")

Output from node 'agent':
---
{'messages': [AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\n  "__arg1": "Las Vegas"\n}', 'name': 'open_weather_map'}})]}

---

Output from node 'tool':
---
{'messages': [FunctionMessage(content='In Las Vegas, the current weather is as follows:\nDetailed status: clear sky\nWind speed: 3.6 m/s, direction: 90°\nHumidity: 34%\nTemperature: \n  - Current: 17.99°C\n  - High: 19.38°C\n  - Low: 16.73°C\n  - Feels like: 16.73°C\nRain: {}\nHeat index: None\nCloud cover: 0%', name='open_weather_map')]}

---

Output from node 'agent':
---
{'messages': [AIMessage(content='The current temperature in Las Vegas is 17.99°C.')]}

---

Output from node '__end__':
---
{'messages': [HumanMessage(content='what is the temperature in las vegas'), AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\n  "__arg1": "Las Vegas"\n}', 'name': 'open_weather_map'}}), FunctionMessage(content='In Las Vegas, the current weather is as follows:\nDetailed status: clear sky\nWind speed: 3.6 m/s, direction: 90°\nHumidity: 34%\nTemperature: \n  - Current: 17.99°C\n  - High: 19.38°C\n  - Low: 16.73°C\n  - Feels like: 16.73°C\nRain: {}\nHeat index: None\nCloud cover: 0%', name='open_weather_map'), AIMessage(content='The current temperature in Las Vegas is 17.99°C.')]}

---
	
	
and to solve my issue I want to make some kind of intelligent graph workflow with a planner a reviewer and a few other steps that utilze the tools. Reply with a full working implementation of my react ollama agent using my already made tool for duckduckgo serach and multiplying and make sure to keep the tracing stuff and the api keys from my code I gave you! Reply with full working code and explaination
Important is that I want to have a general react agent that utilzes his tool to get the best possible answer to the user. I dont want it to specifically focus on my example prompt with the age multiplication.