Here I show how to implement a simple AI agent with access to 2 tools using OOP principles: that is , classes
Weather agent — class-based version¶
This is the class-based (“industry style”) version of the weather agent.
Two classes:
WeatherService— plain Python, talks to the wttr.in API. Knows nothing about LangChain or agents.WeatherAgent— wraps the LLM, tools, system prompt, and optional memory into one reusable object with an.ask()method.
Setup before running¶
pip install langchain langchain-openai langgraph requests python-dotenv
export OPENAI_API_KEY="sk-..."
In [3]:
# Imports + class definitions
import os
import requests
from langchain_core.tools import tool
from langchain.agents import create_agent
from langchain_openai import ChatOpenAI
from langgraph.checkpoint.memory import InMemorySaver
# If you keep your key in a .env file, uncomment these two lines:
# from dotenv import load_dotenv
# load_dotenv()
# os.environ["OPENAI_API_KEY"] = "key_here"
assert os.environ.get("OPENAI_API_KEY"), "Set OPENAI_API_KEY before running this notebook."
class WeatherService:
"""
Plain Python class that knows how to talk to the wttr.in weather API.
This class has NO knowledge of LangChain, agents, or tools -- it is
just a normal service class, the kind you'd write regardless of
whether an LLM ever calls it. Keeping it separate from the agent
logic is the same "single responsibility" idea used in real
production code: business logic stays independent of whatever
framework happens to be calling it.
"""
BASE_URL = "https://wttr.in"
def _fetch(self, city: str) -> dict:
url = f"{self.BASE_URL}/{city}?format=j1"
response = requests.get(url, timeout=10)
response.raise_for_status()
return response.json()
def get_current_weather(self, city: str) -> str:
data = self._fetch(city)
current = data["current_condition"][0]
temp_c = current["temp_C"]
feels_like_c = current["FeelsLikeC"]
description = current["weatherDesc"][0]["value"]
return (
f"Current weather in {city}: {temp_c}°C, "
f"feels like {feels_like_c}°C, conditions: {description}."
)
def get_forecast(self, city: str, days_ahead: int = 1) -> str:
days_ahead = max(0, min(days_ahead, 2)) # wttr.in free tier: today + 2 days
data = self._fetch(city)
day = data["weather"][days_ahead]
date = day["date"]
max_temp = day["maxtempC"]
min_temp = day["mintempC"]
rain_chances = [int(hour["chanceofrain"]) for hour in day["hourly"]]
avg_rain_chance = sum(rain_chances) // len(rain_chances)
return (
f"Forecast for {city} on {date} ({days_ahead} day(s) ahead): "
f"high {max_temp}°C, low {min_temp}°C, "
f"average chance of rain: {avg_rain_chance}%."
)
class WeatherAgent:
"""
Wraps an LLM + a set of weather tools + a system prompt into a single
reusable object. This is the class-based version of the earlier script:
instead of loose global variables (llm, tools, agent, ask()), everything
lives on self, so you can create multiple independent agents (different
models, different memory settings) without them stepping on each other.
"""
SYSTEM_PROMPT = (
"You are a helpful weather assistant. Always check real weather data before "
"answering questions about clothing, umbrellas, or outdoor plans. Choose "
"between the current-weather tool and the forecast tool based on whether "
"the question is about right now or a future day. Clearly explain your "
"recommendation using the actual data you observed."
)
def __init__(self, model: str = "gpt-4o-mini", temperature: float = 0, use_memory: bool = False):
self.weather_service = WeatherService()
self.llm = ChatOpenAI(model=model, temperature=temperature)
self.tools = self._build_tools()
self.checkpointer = InMemorySaver() if use_memory else None
self.agent = create_agent(
model=self.llm,
tools=self.tools,
system_prompt=self.SYSTEM_PROMPT,
checkpointer=self.checkpointer,
)
def _build_tools(self):
"""
@tool needs a plain function, not a bound method -- so we define
small wrapper functions here that close over `self.weather_service`.
This closure pattern is the standard way to give a LangChain tool
access to instance state while keeping @tool happy.
"""
weather_service = self.weather_service
@tool
def get_weather(city: str) -> str:
"""
Get the CURRENT weather right now for a given city.
Use this for questions about what to wear or do right now / today.
Input should be a city name, e.g. 'New Delhi'.
"""
return weather_service.get_current_weather(city)
@tool
def get_forecast(city: str, days_ahead: int = 1) -> str:
"""
Get the weather FORECAST for a given city, for a future day.
Use this for questions about tomorrow, this weekend, or any future day --
NOT for questions about right now (use get_weather for that).
Args:
city: city name, e.g. 'New Delhi'.
days_ahead: how many days from today (0 = today, 1 = tomorrow, 2 = day after). Max 2.
"""
return weather_service.get_forecast(city, days_ahead)
return [get_weather, get_forecast]
def ask(self, question: str, thread_id: str = None, verbose: bool = True):
"""
Send one question to the agent. If thread_id is given AND this
agent was created with use_memory=True, the conversation persists
across calls that share the same thread_id.
"""
config = {"configurable": {"thread_id": thread_id}} if thread_id else {}
result = self.agent.invoke(
{"messages": [{"role": "user", "content": question}]},
config=config,
)
if verbose:
print(f"QUESTION: {question}\n")
for msg in result["messages"]:
if type(msg).__name__ == "AIMessage" and getattr(msg, "tool_calls", None):
for call in msg.tool_calls:
print(f" -> Agent chose tool: {call['name']} args={call['args']}")
elif type(msg).__name__ == "ToolMessage":
print(f" -> Observation: {msg.content}")
print(f"\nFINAL ANSWER:\n{result['messages'][-1].content}")
print("\n" + "=" * 70 + "\n")
return result
print("Classes ready: WeatherService, WeatherAgent")
Classes ready: WeatherService, WeatherAgent
In [9]:
# Run the agent WITHOUT memory (each .ask() call is independent)
weather_agent = WeatherAgent()
raw_answer = weather_agent.ask(
"I am living in New Delhi. I am planning to go outside right now. "
"Do I need cold clothes or warm clothes?"
)
print("RAW ANSWER:", raw_answer)
QUESTION: I am living in New Delhi. I am planning to go outside right now. Do I need cold clothes or warm clothes?
-> Agent chose tool: get_weather args={'city': 'New Delhi'}
-> Observation: Current weather in New Delhi: 27°C, feels like 32°C, conditions: Mist.
FINAL ANSWER:
The current temperature in New Delhi is 27°C, but it feels like 32°C due to the humidity and mist. Given this weather, I recommend wearing light and breathable clothing, as it is relatively warm. You might not need heavy or cold clothes right now. Enjoy your time outside!
======================================================================
RAW ANSWER: {'messages': [HumanMessage(content='I am living in New Delhi. I am planning to go outside right now. Do I need cold clothes or warm clothes?', additional_kwargs={}, response_metadata={}, id='79dd9e6a-cb6d-48a8-bcea-efb4bcabf8d6'), AIMessage(content='', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 15, 'prompt_tokens': 278, 'total_tokens': 293, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_provider': 'openai', 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_7b8553d7fb', 'id': 'chatcmpl-DzihlyT5Csti8r0xSq6rBArLSJUcl', 'service_tier': 'default', 'finish_reason': 'tool_calls', 'logprobs': None}, id='lc_run--019f46fa-1cde-73d3-851c-16c96b0d76b6-0', tool_calls=[{'name': 'get_weather', 'args': {'city': 'New Delhi'}, 'id': 'call_I37YUxlshJECKLNQAEVI8d3v', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 278, 'output_tokens': 15, 'total_tokens': 293, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}}), ToolMessage(content='Current weather in New Delhi: 27°C, feels like 32°C, conditions: Mist.', name='get_weather', id='ade6c3cc-6094-4839-86e0-02cfa3828a5c', tool_call_id='call_I37YUxlshJECKLNQAEVI8d3v'), AIMessage(content='The current temperature in New Delhi is 27°C, but it feels like 32°C due to the humidity and mist. Given this weather, I recommend wearing light and breathable clothing, as it is relatively warm. You might not need heavy or cold clothes right now. Enjoy your time outside!', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 60, 'prompt_tokens': 321, 'total_tokens': 381, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_provider': 'openai', 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_7b8553d7fb', 'id': 'chatcmpl-DzihowXjYrAR09mwWRJqtRB7s6Iky', 'service_tier': 'default', 'finish_reason': 'stop', 'logprobs': None}, id='lc_run--019f46fa-2891-7751-8cb2-71e50e998338-0', tool_calls=[], invalid_tool_calls=[], usage_metadata={'input_tokens': 321, 'output_tokens': 60, 'total_tokens': 381, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})]}
In [7]:
raw_answer = weather_agent.ask("Will it rain in Mumbai this weekend? Should I carry an umbrella?")
print("RAW ANSWER:", raw_answer)
QUESTION: Will it rain in Mumbai this weekend? Should I carry an umbrella?
-> Agent chose tool: get_forecast args={'city': 'Mumbai', 'days_ahead': 2}
-> Observation: Forecast for Mumbai on 2026-07-11 (2 day(s) ahead): high 29°C, low 28°C, average chance of rain: 27%.
FINAL ANSWER:
The forecast for Mumbai this weekend indicates a high of 29°C and a low of 28°C, with an average chance of rain at 27%. While there's a possibility of rain, it's not very high. However, it's always a good idea to be prepared for unexpected weather changes.
I recommend carrying an umbrella just in case!
======================================================================
RAW ANSWER: {'messages': [HumanMessage(content='Will it rain in Mumbai this weekend? Should I carry an umbrella?', additional_kwargs={}, response_metadata={}, id='805bde34-3957-4da1-b740-73eda05cffc3'), AIMessage(content='', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 21, 'prompt_tokens': 267, 'total_tokens': 288, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_provider': 'openai', 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_7b8553d7fb', 'id': 'chatcmpl-DzifWpq6ECyYzLGNKpbmsXDyEjecP', 'service_tier': 'default', 'finish_reason': 'tool_calls', 'logprobs': None}, id='lc_run--019f46f7-feb3-7960-8e5b-b524f7432a3d-0', tool_calls=[{'name': 'get_forecast', 'args': {'city': 'Mumbai', 'days_ahead': 2}, 'id': 'call_pCR4GnvVJ0vsnDOntGilLboI', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 267, 'output_tokens': 21, 'total_tokens': 288, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}}), ToolMessage(content='Forecast for Mumbai on 2026-07-11 (2 day(s) ahead): high 29°C, low 28°C, average chance of rain: 27%.', name='get_forecast', id='1093080f-61a8-4058-939c-2199e1ee5af5', tool_call_id='call_pCR4GnvVJ0vsnDOntGilLboI'), AIMessage(content="The forecast for Mumbai this weekend indicates a high of 29°C and a low of 28°C, with an average chance of rain at 27%. While there's a possibility of rain, it's not very high. However, it's always a good idea to be prepared for unexpected weather changes.\n\nI recommend carrying an umbrella just in case!", additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 68, 'prompt_tokens': 333, 'total_tokens': 401, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_provider': 'openai', 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_7b8553d7fb', 'id': 'chatcmpl-DzifeF0NfgLWcYVRSWzH6WG2CUOD0', 'service_tier': 'default', 'finish_reason': 'stop', 'logprobs': None}, id='lc_run--019f46f8-1fa2-70b3-800e-e6d52ad06aaf-0', tool_calls=[], invalid_tool_calls=[], usage_metadata={'input_tokens': 333, 'output_tokens': 68, 'total_tokens': 401, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})]}
In [ ]:
In [10]:
# Run the agent WITH memory (multi-turn conversation)
memory_agent = WeatherAgent(use_memory=True)
thread_id = "demo-conversation-1"
# Turn 1: mention the city explicitly
_ = memory_agent.ask(
"I live in New Delhi. Do I need warm or cold clothes right now?",
thread_id=thread_id,
)
# Turn 2: follow-up that does NOT repeat the city.
# Without memory the agent wouldn't know which city we mean.
# With the checkpointer, it recalls "New Delhi" from Turn 1 automatically.
_ = memory_agent.ask(
"What about tomorrow — should I carry an umbrella?",
thread_id=thread_id,
)
QUESTION: I live in New Delhi. Do I need warm or cold clothes right now?
-> Agent chose tool: get_weather args={'city': 'New Delhi'}
-> Observation: Current weather in New Delhi: 27°C, feels like 32°C, conditions: Mist.
FINAL ANSWER:
Right now in New Delhi, the temperature is 27°C, but it feels like 32°C due to mist. This indicates that the weather is warm and somewhat humid.
I recommend wearing light and breathable clothing to stay comfortable in this warm weather. No need for warm clothes at this time.
======================================================================
QUESTION: What about tomorrow — should I carry an umbrella?
-> Agent chose tool: get_weather args={'city': 'New Delhi'}
-> Observation: Current weather in New Delhi: 27°C, feels like 32°C, conditions: Mist.
-> Agent chose tool: get_forecast args={'city': 'New Delhi', 'days_ahead': 1}
-> Observation: Forecast for New Delhi on 2026-07-10 (1 day(s) ahead): high 33°C, low 26°C, average chance of rain: 41%.
FINAL ANSWER:
Tomorrow in New Delhi, the forecast indicates a high of 33°C and a low of 26°C, with an average chance of rain at 41%. Given this possibility of rain, it would be wise to carry an umbrella to stay prepared for any showers.
======================================================================
In [ ]:
