Designing a Langchain chatbot with RAG
Project
Table of contents
- Overview
- Environment Setup & Run/Stop
- How the Code Is Organized
- What Happens Step‑by‑Step
- How Each Service Works (with key code)
- API Request Libraries: Python and JavaScript
- Quick Tips:
- Resources
- Project repository
Overview
This project demonstrates a LangChain-powered chatbot with conversational memory and live-data services. It combines a FastAPI backend, a lightweight frontend, and service routing for:
- Local date/time
- Weather (Open‑Meteo)
- News (GNews)
- Web search RAG (Tavily)
Environment Setup & Run/Stop
- Create a .env file in the project root:
- OPENAI_API_KEY=your_key_here
- GNEWS_API_KEY=your_gnews_key_here
- TAVILY_API_KEY=your_tavily_key_here
- Install dependencies from requirement.txt or use Docker.
- Start the backend in backend/web_app.py.
- Open http://localhost:8000.
How the Code Is Organized
- Backend
- backend/chatbot_app.py: Core chain, routing, and service handlers
- backend/web_app.py: FastAPI endpoints and static UI serving
- Frontend
- frontend/index.html: Chat UI
- Root
- Dockerfile, requirement.txt, and .env
What Happens Step‑by‑Step
- The browser posts a message to /chat.
- FastAPI passes the message and session id to
chat_once(). chat_once()checks service detectors (date/time, weather, news, web).- If a service matches, it returns that service response.
- Otherwise, the LLM chain is invoked with history for a normal reply.
How Each Service Works (with key code)
Implemented services in this project:
- General Q&A (LLM default path)
- Summarization (LLM default path)
- Date/time (local system clock)
- Weather (Open‑Meteo)
- News (GNews)
- Web search RAG (Tavily)
Conversation Memory
The chatbot stores per-session conversation history so the LLM can respond with context.
1
2
3
4
5
6
7
8
9
10
11
12
store: Dict[str, InMemoryChatMessageHistory] = {}
def get_session_history(session_id: str) -> InMemoryChatMessageHistory:
if session_id not in store:
store[session_id] = InMemoryChatMessageHistory()
return store[session_id]
return RunnableWithMessageHistory(
get_session_history,
input_messages_key="input",
history_messages_key="history",
)
General Q&A
If no service matches, the LLM chain answers the question with history.
1
return chatbot.invoke({"input": user_input}, config={"configurable": {"session_id": session_id}})
Summarization
Summaries are handled by the same LLM chain when the user asks to summarize.
Date/Time (Local System Clock)
Uses the local system clock to respond immediately.
1
2
3
def get_current_datetime_response() -> str:
now = datetime.now()
return f"Today's date is {now.strftime('%B %d, %Y')}. The current time is {now.strftime('%I:%M %p')}"
Weather (Open‑Meteo)
Geocodes the location, then fetches current weather from Open‑Meteo.
1
2
3
4
5
6
def fetch_open_meteo_weather(location: str) -> str:
geo_resp = requests.get("https://geocoding-api.open-meteo.com/v1/search", params={"name": location, "count": 1})
place = geo_resp.json().get("results")[0]
weather_resp = requests.get("https://api.open-meteo.com/v1/forecast", params={"latitude": place["latitude"], "longitude": place["longitude"], "current_weather": True})
weather = weather_resp.json().get("current_weather", {})
return f"Weather in {place['name']}: {weather['temperature']}°C"
News (GNews)
Uses GNews for either top headlines or query search.
1
2
3
4
5
6
def fetch_gnews_headlines(query: str, max_results: int = 5) -> str:
is_top = query.lower() in {"top headlines", "top", "headlines"}
endpoint = "https://gnews.io/api/v4/top-headlines" if is_top else "https://gnews.io/api/v4/search"
response = requests.get(endpoint, params={"q": query, "lang": "en", "max": max_results, "token": os.getenv("GNEWS_API_KEY")})
data = response.json()
return "\n".join([a.get("title", "") for a in data.get("articles", [])])
Presentation: organize each news item
To keep the UI clean, the backend formats each article with clear separators and labeled fields. The frontend then parses this structure to render separate “cards” with a bold title, a short description, and a link.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
def format_news_results(articles: list[dict]) -> str:
if not articles:
return "No recent news found."
lines = ["NEWS_RESULTS"]
for article in articles:
title = (article.get("title") or "").strip()
description = (article.get("description") or "").strip()
url = (article.get("url") or "").strip()
lines.append(f"Title: {title}")
lines.append(f"Description: {description}")
lines.append(f"Link: {url}")
lines.append("---")
return "\n".join(lines)
Web Search RAG (Tavily)
Searches the web and injects sources into the LLM prompt.
1
2
3
4
5
def answer_with_web_rag(chatbot, query: str, session_id: str) -> str:
results, _ = tavily_search(query)
sources = [f"{r['title']} - {r['url']}\n{r.get('content','')}" for r in results]
rag_prompt = f"Sources:\n" + "\n\n".join(sources) + f"\n\nQuestion: {query}"
return chatbot.invoke({"input": rag_prompt}, config={"configurable": {"session_id": session_id}})
API Request Libraries: Python and JavaScript
This project uses Python’s requests in the backend to call external APIs. An alternative in the Python standard library is urllib.request, which is lower-level and more verbose. On the frontend (browser), JavaScript uses fetch (or libraries like Axios) to call your backend or public APIs. When to use each:
- Backend (Python): use
requestsfor clarity and fewer lines of code. - Backend (Python, no extra deps): use
urllib.requestif you must avoid third‑party libraries. - Frontend (JavaScript): use
fetch(built‑in) or Axios for convenience.
Python (requests)
1
2
3
4
5
6
7
8
9
10
import requests
response = requests.get(
"https://api.open-meteo.com/v1/forecast",
params={"latitude": 43.65, "longitude": -79.38, "current_weather": True},
timeout=10,
)
response.raise_for_status()
data = response.json()
print(data.get("current_weather", {}))
Python (urllib.request)
1
2
3
4
5
6
7
8
9
10
11
12
13
import json
import urllib.request
apikey = "API_KEY"
category = "general"
url = (
"https://gnews.io/api/v4/top-headlines"
f"?category={category}&lang=en&country=us&max=10&apikey={apikey}"
)
with urllib.request.urlopen(url, timeout=10) as response:
data = json.loads(response.read().decode("utf-8"))
print(data.get("articles", []))
JavaScript (fetch)
1
2
3
4
5
6
7
8
fetch("/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message: "Weather in Toronto", session_id: "demo" })
})
.then((res) => res.json())
.then((data) => console.log(data))
.catch((err) => console.error(err));
Quick Tips:
-
Uvicorn (ASGI Server) Uvicorn is the ASGI server that runs the FastAPI application and handles concurrent requests. In this project it launches
web_app:appand serves the UI and /chat endpoint on port 8000. - API Basics (GET vs curl/Postman)
- A GET endpoint is a server route that responds to HTTP GET requests.
- curl/Postman are client tools that can send any HTTP method (GET, POST, etc.) to any endpoint.
- Libraries Used
- dotenv for environment variables
- langchain_openai and langchain_core for LLM chains and memory
- requests for API calls
- fastapi for the web API
- uvicorn for the ASGI server
Resources
Project repository
GitHub Code: Langchain chatbot with RAG