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:
agent.py:
from typing import TypedDict, Literal

from langgraph.graph import StateGraph, END
from my_agent.utils.nodes import call_model, should_continue, tool_node
from my_agent.utils.state import AgentState


# Define the config
class GraphConfig(TypedDict):
    model_name: Literal["anthropic", "openai"]


# Define a new graph
workflow = StateGraph(AgentState, config_schema=GraphConfig)

# Define the two nodes we will cycle between
workflow.add_node("agent", call_model)
workflow.add_node("action", tool_node)

# Set the entrypoint as `agent`
# This means that this node is the first one called
workflow.set_entry_point("agent")

# We now add a conditional edge
workflow.add_conditional_edges(
    # First, we define the start node. We use `agent`.
    # This means these are the edges taken after the `agent` node is called.
    "agent",
    # Next, we pass in the function that will determine which node is called next.
    should_continue,
    # Finally we pass in a mapping.
    # The keys are strings, and the values are other nodes.
    # END is a special node marking that the graph should finish.
    # What will happen is we will call `should_continue`, and then the output of that
    # will be matched against the keys in this mapping.
    # Based on which one it matches, that node will then be called.
    {
        # If `tools`, then we call the tool node.
        "continue": "action",
        # Otherwise we finish.
        "end": END,
    },
)

# We now add a normal edge from `tools` to `agent`.
# This means that after `tools` is called, `agent` node is called next.
workflow.add_edge("action", "agent")

# Finally, we compile it!
# This compiles it into a LangChain Runnable,
# meaning you can use it as you would any other runnable
graph = workflow.compile()


utils/nodes.py:
from functools import lru_cache
from langchain_anthropic import ChatAnthropic
from langchain_openai import ChatOpenAI
from my_agent.utils.tools import tools
from langgraph.prebuilt import ToolNode


@lru_cache(maxsize=4)
def _get_model(model_name: str):
    if model_name == "openai":
        model = ChatOpenAI(temperature=0, model_name="gpt-4o")
    elif model_name == "anthropic":
        model =  ChatAnthropic(temperature=0, model_name="claude-3-sonnet-20240229")
    else:
        raise ValueError(f"Unsupported model type: {model_name}")

    model = model.bind_tools(tools)
    return model

# Define the function that determines whether to continue or not
def should_continue(state):
    messages = state["messages"]
    last_message = messages[-1]
    # If there are no tool calls, then we finish
    if not last_message.tool_calls:
        return "end"
    # Otherwise if there is, we continue
    else:
        return "continue"


system_prompt = """Be a helpful assistant"""

# Define the function that calls the model
def call_model(state, config):
    messages = state["messages"]
    messages = [{"role": "system", "content": system_prompt}] + messages
    model_name = config.get('configurable', {}).get("model_name", "anthropic")
    model = _get_model(model_name)
    response = model.invoke(messages)
    # We return a list, because this will get added to the existing list
    return {"messages": [response]}

# Define the function to execute tools
tool_node = ToolNode(tools)


utils/state.py:
from langgraph.graph import add_messages
from langchain_core.messages import BaseMessage
from typing import TypedDict, Annotated, Sequence

class AgentState(TypedDict):
    messages: Annotated[Sequence[BaseMessage], add_messages]
	
	
utils/tools.py:
from langchain_community.tools.tavily_search import TavilySearchResults

tools = [TavilySearchResults(max_results=1)]




	
and to solve my issue I want to make some kind of intelligent graph workflow that utilzes 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.