Building a LangChain agent involves equipping it with custom tools that interact with external data sources or perform specific calculations. This approach simulates common production scenarios where an agent requires specialized capabilities beyond the LLM's inherent knowledge.Our goal is to create an agent that can answer questions about current weather and estimate driving times between locations. This requires giving the agent access to two distinct functionalities via custom tools.Scenario DefinitionImagine you need an assistant that can answer queries like:"What is the current temperature in Tokyo?""How long would it realistically take to drive from San Francisco to Los Angeles?""Give me the weather forecast for Paris and also estimate the driving time from Paris to Lyon."To handle these requests, the agent needs:A tool to fetch current weather information for a given city.A tool to estimate driving time between two cities.We will implement these as custom LangChain tools and integrate them into a ReAct agent.Step 1: Implementing the Weather ToolFirst, let's create a tool to get weather data. For a real application, you'd likely use a service like OpenWeatherMap, WeatherAPI, or a similar provider. This usually involves obtaining an API key. For simplicity in this practice section, we'll define a function that returns mock weather data. However, we'll structure it as if it were calling a real API.# prerequisites: Ensure you have langchain and langchain_openai installed # pip install langchain langchain_openai python-dotenv import os import random from dotenv import load_dotenv from langchain.tools import BaseTool, Tool from typing import Type, Optional from pydantic.v1 import BaseModel, Field # Use pydantic v1 for BaseTool compatibility # Load environment variables (optional, for API keys if using real APIs) load_dotenv() # --- Weather Tool Implementation --- class WeatherInput(BaseModel): """Input schema for the Weather Tool.""" location: str = Field(description="The city name for which to get the weather.") def get_current_weather(location: str) -> str: """ Simulates fetching current weather for a location. In a real application, this would call an external weather API. """ print(f"---> Calling Weather Tool for: {location}") # Simulate API call try: # Mock data generation temp_celsius = random.uniform(5.0, 35.0) conditions = random.choice(["Sunny", "Cloudy", "Rainy", "Windy", "Snowy (unlikely!)"]) humidity = random.randint(30, 90) return f"The current weather in {location} is {temp_celsius:.1f}°C, {conditions}, with {humidity}% humidity." except Exception as e: return f"Error fetching weather for {location}: {e}" # Option 1: Using the Tool decorator (simpler for basic functions) # weather_tool = Tool.from_function( # func=get_current_weather, # name="Weather Checker", # description="Useful for finding the current weather conditions in a specific city. Input should be a city name.", # args_schema=WeatherInput # ) # Option 2: Subclassing BaseTool (more control, better for complex logic/state) class WeatherTool(BaseTool): name: str = "Weather Checker" description: str = "Useful for finding the current weather conditions in a specific city. Input should be a city name." args_schema: Type[BaseModel] = WeatherInput def _run(self, location: str) -> str: """Use the tool.""" return get_current_weather(location) async def _arun(self, location: str) -> str: """Use the tool asynchronously.""" # For this simple mock function, async isn't strictly necessary, # but it demonstrates the pattern for real async API calls. # In a real scenario, you'd use an async HTTP client (e.g., aiohttp). return self._run(location) # Simulate async call weather_tool = WeatherTool() # Test the tool directly (optional) # print(weather_tool.run({"location": "London"})) # print(weather_tool.run("Paris")) # Also accepts direct string input if args_schema allowsImportant points about this tool:WeatherInput Schema: We define a Pydantic model WeatherInput to specify the expected input (location). This helps LangChain validate inputs and provides structure. Using Pydantic v1 (pydantic.v1) is often needed for compatibility with older BaseTool structures.get_current_weather Function: This is the core logic. It currently uses random data but mimics the structure of an API call handler, including basic error handling. The print statement helps trace tool execution.WeatherTool Class: We subclass BaseTool for more explicit control.name: A concise identifier for the tool.description: Important for the agent. The LLM uses this description to decide when to use the tool and what input to provide. Make it clear and informative.args_schema: Links to our Pydantic input model._run: The synchronous execution method._arun: The asynchronous execution method. Even if the underlying function is synchronous, implementing _arun is good practice for compatibility with asynchronous agent executors.Step 2: Implementing the Driving Time Tool"Next, we need a tool to estimate driving times. Again, implementations might use APIs like Google Maps Distance Matrix or OSRM. We'll simulate this with a simple calculation."# --- Driving Time Tool Implementation --- class DrivingTimeInput(BaseModel): """Input schema for the Driving Time Tool.""" origin: str = Field(description="The starting city or location.") destination: str = Field(description="The destination city or location.") def estimate_driving_time(origin: str, destination: str) -> str: """ Simulates estimating driving time between two locations. Assumes a fixed average speed for simplicity. """ print(f"---> Calling Driving Time Tool for: {origin} to {destination}") # Very simplified distance simulation based on city name lengths # (Replace with a real distance calculation or API call in production) simulated_distance_km = abs(len(origin) - len(destination)) * 50 + random.randint(50, 500) average_speed_kph = 80 if simulated_distance_km == 0: # Avoid division by zero for same origin/destination return f"Origin and destination ({origin}) are the same." time_hours = simulated_distance_km / average_speed_kph hours = int(time_hours) minutes = int((time_hours - hours) * 60) return f"The estimated driving time from {origin} to {destination} is approximately {hours} hours and {minutes} minutes ({simulated_distance_km} km)." class DrivingTimeTool(BaseTool): name: str = "Driving Time Estimator" description: str = ("Useful for estimating the driving time between two cities. " "Input should be the origin city and the destination city.") args_schema: Type[BaseModel] = DrivingTimeInput def _run(self, origin: str, destination: str) -> str: """Use the tool.""" return estimate_driving_time(origin, destination) async def _arun(self, origin: str, destination: str) -> str: """Use the tool asynchronously.""" # Simulate async call for demonstration return self._run(origin, destination) driving_tool = DrivingTimeTool() # Test the tool directly (optional) # print(driving_tool.run({"origin": "Paris", "destination": "Berlin"}))This tool follows the same pattern as the weather tool: an input schema (DrivingTimeInput), a core logic function (estimate_driving_time), and a BaseTool subclass (DrivingTimeTool) providing the name, description, and execution methods. The description clearly states its purpose and expected inputs.Step 3: Creating and Configuring the AgentNow that we have our custom tools, let's integrate them into an agent. We'll use a ReAct (Reasoning and Acting) agent, a common pattern where the LLM reasons about which action (tool) to take next based on the input and previous steps.# --- Agent Setup --- from langchain_openai import ChatOpenAI from langchain import hub from langchain.agents import create_react_agent, AgentExecutor # Ensure you have OPENAI_API_KEY set in your environment or .env file # os.environ["OPENAI_API_KEY"] = "your_api_key" if not os.getenv("OPENAI_API_KEY"): print("Warning: OPENAI_API_KEY not set. Agent execution will likely fail.") # 1. Initialize the LLM # Using a chat model is generally recommended for modern agents llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0) # 2. Define the list of tools tools = [weather_tool, driving_tool] # 3. Get the prompt template # Pulls a predefined ReAct prompt optimized for chat models # You can explore other prompts on the LangChain Hub: https://smith.langchain.com/hub prompt = hub.pull("hwchase17/react-chat") # Inspect the prompt template if curious: print(prompt.template) # 4. Create the ReAct Agent # This binds the LLM, tools, and prompt together agent = create_react_agent(llm, tools, prompt) # 5. Create the Agent Executor # This runs the agent loop (thought -> action -> observation -> ...) agent_executor = AgentExecutor( agent=agent, tools=tools, verbose=True, # Set to True to see the agent's reasoning steps handle_parsing_errors=True, # Add robustness for potential LLM output parsing issues max_iterations=5 # Prevent potential infinite loops ) print("Agent Executor created successfully.")Let's break down the agent creation:LLM Initialization: We instantiate ChatOpenAI. Temperature is set to 0 for more deterministic responses suitable for tool use. Remember to handle your API key securely (environment variables are common).Tools List: We gather our custom weather_tool and driving_tool instances into a list.Prompt Template: We fetch a proven ReAct prompt template from the LangChain Hub (hwchase17/react-chat). This template structures the interaction, guiding the LLM to think step-by-step and decide which tool to use (or if it can answer directly).create_react_agent: This function constructs the core agent logic, connecting the LLM's reasoning ability with the available tools and the prompt structure.AgentExecutor: This is the runtime environment for the agent. It takes the agent logic and the tools, then manages the execution cycle:Passes the input and conversation history to the agent.The agent (LLM + prompt) decides on an action (e.g., use "Weather Checker" with input "London").The executor calls the corresponding tool (weather_tool.run("London")).The tool's output (observation) is passed back to the agent.This loop continues until the agent determines it has the final answer.verbose=True is highly recommended during development to observe the agent's thought process.handle_parsing_errors=True makes the agent more resilient if the LLM generates output that doesn't perfectly match the expected format.max_iterations is a safety mechanism.Step 4: Running the Agent and Observing BehaviorWith the agent executor ready, let's test it with different queries.# --- Running the Agent --- print("\n--- Running Simple Weather Query ---") response1 = agent_executor.invoke({ "input": "What's the weather like right now in Toronto?", "chat_history": [] # Start with empty history for single-turn queries }) print("\nFinal Answer:", response1['output']) print("\n--- Running Simple Driving Time Query ---") response2 = agent_executor.invoke({ "input": "How long does it take to drive from Berlin to Munich?", "chat_history": [] }) print("\nFinal Answer:", response2['output']) print("\n--- Running Multi-Tool Query ---") response3 = agent_executor.invoke({ "input": "Can you tell me the current weather in Rome and also how long it might take to drive there from Naples?", "chat_history": [] }) print("\nFinal Answer:", response3['output']) # Example of a query the agent should answer directly (if possible) # print("\n--- Running Non-Tool Query ---") # response4 = agent_executor.invoke({ # "input": "What is the capital of France?", # "chat_history": [] # }) # print("\nFinal Answer:", response4['output'])Observe the output when verbose=True. You should see something like this pattern for each query involving tools:Thought: The LLM analyzes the input and decides it needs specific information (e.g., weather in Toronto). It recognizes that the "Weather Checker" tool description matches this need.Action: The LLM formats an action specifying the tool name (Weather Checker) and the required input ({"location": "Toronto"}).Observation: The AgentExecutor runs the weather_tool with "Toronto", captures its output (the simulated weather string), and feeds this back to the LLM.(Repeat if necessary): For the multi-tool query, after getting the weather, the agent's thought process will likely identify the need for driving time, select the "Driving Time Estimator" tool, execute it, get the observation, and then synthesize both pieces of information.Thought: The LLM now has the information it needed.Final Answer: The LLM formulates the final response to the user.Pay close attention to how the agent uses the exact names and input schemas defined in your BaseTool subclasses. The quality of the tool description is essential for the agent's ability to select the correct tool.Conclusion and Next StepsThis practical exercise demonstrated the fundamental workflow for creating a LangChain agent with custom capabilities:Define the Need: Identify specific tasks the agent must perform.Implement Tools: Create functions or classes for each capability, wrapping them using Tool.from_function or by subclassing BaseTool. Pay careful attention to the name, description, and args_schema.Configure Agent: Choose an appropriate agent type (like ReAct), select an LLM, gather the tools, and use a suitable prompt template.Instantiate Executor: Create the AgentExecutor to manage the runtime loop.Test and Observe: Run queries and use verbose=True to understand the agent's reasoning and tool usage.From here, you can explore:More Complex Tools: Integrate tools that interact with databases, proprietary APIs, or perform intricate calculations.Error Handling: Implement more sophisticated error handling within your tools and potentially within the agent executor itself (as discussed in the "Handling Tool Errors and Agent Recovery" section).Structured Tool Calling: For LLMs that support it (like newer OpenAI models), explore agents based on function/tool calling (create_openai_tools_agent, create_tool_calling_agent), which can offer more reliable input/output parsing compared to ReAct's text-based approach.Asynchronous Execution: Optimize performance by ensuring your tools and the agent executor effectively utilize asynchronous operations (_arun methods) for I/O-bound tasks like API calls.Building agents with custom tools is a core technique for creating powerful, specialized LLM applications that can interact and solve complex problems.