Three concepts that make LangChain apps production-ready
What This Module Covers
Output Parsers → get structured data from LLM instead of raw text
Memory → give your chatbot the ability to remember conversations
Tools → let your LLM call functions and external APIs
These three things separate a basic LLM wrapper from a real application.
Part 1 — Output Parsers
The Problem
LLMs return text. But your application needs structured data.
// What LLM returns (raw text): // "The product has pros: fast, reliable, cheap. // Cons: no mobile app, limited support."
// What your app needs (structured data): { pros: ["fast", "reliable", "cheap"], cons: ["no mobile app", "limited support"] }
You could parse the text manually — split strings, use regex — but that's fragile and breaks constantly.
Output Parsers tell the LLM exactly what format to use AND automatically parse the response for you.
Create src/04_output_parsers.js:
import { ChatOpenAI } from "@langchain/openai"; import { ChatPromptTemplate } from "@langchain/core/prompts"; import { StringOutputParser, JsonOutputParser, CommaSeparatedListOutputParser, StructuredOutputParser } from "@langchain/core/output_parsers"; // StringOutputParser = AIMessage → plain string // JsonOutputParser = AIMessage → JS object // CommaSeparatedListOutputParser = AIMessage → array of strings
// StructuredOutputParser = most powerful — enforces exact schema
import { z } from "zod"; // zod = schema validation library // used to define exact shape of output we expect
import * as dotenv from "dotenv"; dotenv.config();
const llm = new ChatOpenAI({ model: "gpt-4o", temperature: 0 }); // temperature 0 = consistent, deterministic output // important for structured data — we want the same format every time
// ───────────────────────────────────────── // PARSER 1 — StringOutputParser // Simplest — just extracts text from AIMessage // ─────────────────────────────────────────
async function parser1() { console.log("─".repeat(50)); console.log("PARSER 1: StringOutputParser"); console.log("─".repeat(50));
const chain = ChatPromptTemplate.fromMessages([ ["human", "What is {topic}?"], ]).pipe(llm).pipe(new StringOutputParser());
const result = await chain.invoke({ topic: "LangChain" });
console.log("Type:", typeof result); // "string"
console.log("Result:", result); // "LangChain is a framework for building AI applications..." console.log(); }
// ───────────────────────────────────────── // PARSER 2 — CommaSeparatedListOutputParser // Returns an array of strings // ─────────────────────────────────────────
async function parser2() { console.log("─".repeat(50)); console.log("PARSER 2: CommaSeparatedListOutputParser"); console.log("─".repeat(50));
const parser = new CommaSeparatedListOutputParser(); // tells LLM to respond with comma-separated values // automatically splits into array
const formatInstructions = parser.getFormatInstructions(); // getFormatInstructions() = returns text instructions for the LLM // tells LLM exactly how to format its response // example: "Your response should be a list of comma separated values..."
console.log("Format instructions sent to LLM:"); console.log(formatInstructions); console.log();
const prompt = ChatPromptTemplate.fromMessages([ ["human", `List 5 popular vector databases. {format_instructions}`], // include format instructions in the prompt // tells LLM how to structure its response ]);
const chain = prompt.pipe(llm).pipe(parser);
const result = await chain.invoke({ format_instructions: formatInstructions, // fills {format_instructions} placeholder });
console.log("Type:", typeof result); // "object" (array)
console.log("Result:", result); // ["Pinecone", "Chroma", "Weaviate", "Qdrant", "Milvus"]
console.log("Length:", result.length); // 5
console.log("First item:", result[0]); // "Pinecone" console.log(); }
// ───────────────────────────────────────── // PARSER 3 — StructuredOutputParser with Zod // Most powerful — enforces exact data shape // Best for production applications // ─────────────────────────────────────────
async function parser3() { console.log("─".repeat(50)); console.log("PARSER 3: StructuredOutputParser with Zod schema"); console.log("─".repeat(50));
// Define the exact shape you want using Zod const schema = z.object({ name: z.string().describe("the name of the technology"), // z.string() = must be a string // .describe() = tells LLM what this field means
category: z.string().describe("category like 'database', 'framework', 'library'"),
yearCreated: z.number().describe("year it was created"), // z.number() = must be a number
keyFeatures: z.array(z.string()).describe("list of 3 key features"), // z.array(z.string()) = array of strings
difficulty: z.enum(["beginner", "intermediate", "advanced"]) .describe("learning difficulty level"), // z.enum() = must be one of these exact values
isOpenSource: z.boolean().describe("whether it is open source"), // z.boolean() = must be true or false
summary: z.string().describe("one sentence description"), });
const parser = StructuredOutputParser.fromZodSchema(schema); // creates parser from Zod schema // automatically generates format instructions for LLM // automatically validates and parses LLM response
const formatInstructions = parser.getFormatInstructions(); // generates detailed instructions telling LLM exactly how to respond // includes the JSON structure, field types, descriptions
const prompt = ChatPromptTemplate.fromMessages([ ["system", "You extract structured information about technologies."], ["human", `Extract information about: {technology}
{format_instructions}`], ]);
const chain = prompt.pipe(llm).pipe(parser);
const result = await chain.invoke({ technology: "Pinecone", format_instructions: formatInstructions, });
console.log("Type:", typeof result); // "object"
console.log("Parsed result:"); console.log(JSON.stringify(result, null, 2)); // { // "name": "Pinecone", // "category": "database", // "yearCreated": 2019, // "keyFeatures": ["vector search", "scalable", "managed service"], // "difficulty": "beginner", // "isOpenSource": false, // "summary": "Pinecone is a managed vector database..." // }
// Access fields directly console.log("\nDirect field access:"); console.log("Name:", result.name); console.log("Open source:", result.isOpenSource); console.log("Features:", result.keyFeatures); console.log(); }
// ───────────────────────────────────────── // PARSER 4 — Real world example // Customer support ticket classifier // ─────────────────────────────────────────
async function parser4() { console.log("─".repeat(50)); console.log("PARSER 4: Real world — ticket classifier"); console.log("─".repeat(50));
const ticketSchema = z.object({ category: z.enum(["billing", "technical", "general", "complaint", "feature_request"]) .describe("ticket category"),
priority: z.enum(["low", "medium", "high", "urgent"]) .describe("ticket priority based on content"),
sentiment: z.enum(["positive", "neutral", "negative", "very_negative"]) .describe("customer sentiment"),
summary: z.string().describe("one sentence summary of the issue"),
requiresHuman: z.boolean() .describe("whether this needs a human agent or can be auto-resolved"),
suggestedResponse: z.string() .describe("suggested first response to send to customer"), });
const parser = StructuredOutputParser.fromZodSchema(ticketSchema);
const prompt = ChatPromptTemplate.fromMessages([ ["system", `You are a customer support AI that classifies tickets. Analyze the ticket and extract structured information. {format_instructions}`], ["human", "Ticket: {ticket}"], ]);
const chain = prompt.pipe(llm).pipe(parser);
const tickets = [ "I've been charged twice for my subscription this month. Please fix this immediately!", "How do I export my data to CSV?", "Your app keeps crashing whenever I try to upload files. This is unacceptable.", ];
for (const ticket of tickets) { const result = await chain.invoke({ ticket, format_instructions: parser.getFormatInstructions(), });
console.log(`\nTicket: "${ticket.substring(0, 50)}..."`); console.log(`Category: ${result.category}`); console.log(`Priority: ${result.priority}`); console.log(`Sentiment: ${result.sentiment}`); console.log(`Needs human: ${result.requiresHuman}`); console.log(`Suggested response: ${result.suggestedResponse.substring(0, 80)}...`); } console.log(); }
async function main() { console.log("\n🔧 OUTPUT PARSERS DEMO\n"); await parser1(); await parser2(); await parser3(); await parser4(); console.log("✅ All parser examples complete!"); }
main().catch(console.error);
Run:
node src/04_output_parsers.js
Part 2 — Memory
The Problem
By default LLMs have no memory. Every call is independent.
// Without memory: await chain.invoke({ question: "My name is Sofia" }); // Model: "Nice to meet you Sofia!"
await chain.invoke({ question: "What is my name?" }); // Model: "I don't know your name." ← forgot immediately
Memory fixes this by storing conversation history and injecting it into every new call.
Create src/05_memory.js:
// ───────────────────────────────────────── // IMPORTS // ─────────────────────────────────────────
import { ChatOpenAI } from "@langchain/openai"; // ChatOpenAI = LangChain's wrapper for OpenAI's chat models // Internally it calls: POST https://api.openai.com/v1/chat/completions // You don't write the fetch() call manually — LangChain does it for you
import { ChatPromptTemplate, MessagesPlaceholder } from "@langchain/core/prompts"; // ChatPromptTemplate = a reusable template for building chat prompts // MessagesPlaceholder = a special slot in the template // → at runtime, this slot gets filled with actual conversation history // → think of it like a {variable} but specifically for a list of messages
import { StringOutputParser } from "@langchain/core/output_parsers"; // StringOutputParser = converts LangChain AIMessage object → plain string // Without this: response is an AIMessage { content: "...", role: "assistant" } // With this: response is just "..." (the text directly)
import { RunnableWithMessageHistory } from "@langchain/core/runnables"; // RunnableWithMessageHistory = a wrapper that adds memory to ANY chain // Before each chain call: // → it loads the conversation history from storage // → injects it into the prompt // After each chain call: // → it saves the new human message + AI response to storage // Without this wrapper: chain has zero memory of past messages
import { InMemoryChatMessageHistory } from "@langchain/core/chat_history"; // InMemoryChatMessageHistory = stores messages in a JavaScript array in RAM // When your Node.js process stops → all history is lost (it's in-memory) // For production apps you'd replace this with: // → RedisChatMessageHistory (stores in Redis database) // → MongoDBChatMessageHistory (stores in MongoDB) // → PostgresChatMessageHistory (stores in PostgreSQL) // But for learning — in-memory is perfect
import { HumanMessage, AIMessage } from "@langchain/core/messages"; // HumanMessage = represents a message from the user // Internal structure: { role: "user", content: "what user typed" } // AIMessage = represents a response from the AI model // Internal structure: { role: "assistant", content: "what AI replied" } // These are used when manually adding messages to history
import * as dotenv from "dotenv"; // dotenv = reads your .env file and loads variables into process.env dotenv.config(); // After this line: process.env.OPENAI_API_KEY = "sk-proj-..." // LangChain automatically reads this key when making API calls
// ───────────────────────────────────────── // CREATE THE LLM // ─────────────────────────────────────────
const llm = new ChatOpenAI({ model: "gpt-4o", // which OpenAI model to use // "gpt-4o" = GPT-4 Omni — fast and capable
temperature: 0.7, // controls randomness in responses // 0.0 = always picks the highest probability token → same answer every time // 0.7 = some randomness → natural, varied responses // 1.0 = very random → creative but less predictable // For a friendly chat assistant, 0.7 is a good balance }); // Internal state of llm object: // { // model: "gpt-4o", // temperature: 0.7, // apiKey: "sk-proj-..." (read from process.env automatically) // }
const parser = new StringOutputParser(); // parser = an object with one job: extract .content from AIMessage // When LLM responds, LangChain returns: // AIMessage { content: "Hello! How can I help?", role: "assistant", ... } // After parser: // "Hello! How can I help?" ← just the string
// ───────────────────────────────────────── // BUILD THE PROMPT TEMPLATE // ─────────────────────────────────────────
const prompt = ChatPromptTemplate.fromMessages([ // fromMessages() takes an array and builds a reusable template // Each item becomes one "message" in the conversation
["system", `You are a helpful AI assistant. You remember everything said in this conversation. Be friendly and reference previous messages when relevant.`], // ["system", "..."] = creates a SystemMessage // This is the hidden instruction that shapes the AI's behavior // The user never sees this — it's like a briefing before the conversation // Internal: SystemMessage { role: "system", content: "You are a helpful..." }
new MessagesPlaceholder("history"), // This is NOT a regular message — it's a placeholder // At runtime, "history" gets replaced with the actual array of past messages // Example: if 3 messages happened before: // HumanMessage { content: "Hi, I'm Sofia" } // AIMessage { content: "Hello Sofia!" } // HumanMessage { content: "I'm learning AI" } // → these 3 messages get inserted RIGHT HERE in the prompt // "history" = the key name — must match historyMessagesKey below
["human", "{question}"], // {question} = placeholder for the CURRENT user message // Gets filled with whatever the user just typed // Internal: HumanMessage { role: "user", content: "actual question" } ]);
// What the full prompt looks like at runtime (simplified): // [ // SystemMessage: "You are a helpful AI assistant..." // HumanMessage: "Hi, I'm Sofia" ← from history // AIMessage: "Hello Sofia!" ← from history // HumanMessage: "What is my name?" ← current question // ] // This entire array gets sent to OpenAI in one API call
// ───────────────────────────────────────── // BUILD THE BASE CHAIN (no memory yet) // ─────────────────────────────────────────
const baseChain = prompt.pipe(llm).pipe(parser); // .pipe() = connect components — output of left becomes input of right // This creates a pipeline:
// Step 1: prompt // Input: { question: "What is my name?", history: [array of messages] } // Output: Array of formatted messages (SystemMessage + history + HumanMessage)
// Step 2: llm // Input: Array of formatted messages // Internally: calls OpenAI API with all those messages // Output: AIMessage { content: "Your name is Sofia!", role: "assistant" }
// Step 3: parser // Input: AIMessage { content: "Your name is Sofia!" } // Output: "Your name is Sofia!" ← plain string
// baseChain by itself has NO memory // Every .invoke() call is completely independent // It doesn't know anything about previous calls
// ───────────────────────────────────────── // MEMORY STORAGE // ─────────────────────────────────────────
const messageHistories = {}; // messageHistories = plain JavaScript object (acts like a dictionary/HashMap) // Key = session ID string (who is talking) // Value = InMemoryChatMessageHistory object (their message history) // // Current state: {} (empty — no conversations yet) // // After a few conversations it will look like: // { // "demo_session_1": InMemoryChatMessageHistory { // messages: [ // HumanMessage { content: "Hi, I'm Sofia" }, // AIMessage { content: "Hello Sofia!" }, // HumanMessage { content: "I'm learning AI" }, // AIMessage { content: "That's great!" } // ] // }, // "user_alice": InMemoryChatMessageHistory { // messages: [ // HumanMessage { content: "My favorite color is blue" }, // AIMessage { content: "Blue is a great color!" } // ] // } // }
function getSessionHistory(sessionId) { // This function is called AUTOMATICALLY by RunnableWithMessageHistory // before every chain invocation // sessionId = the ID passed in the config object // example: "demo_session_1", "user_alice", "user_bob"
if (!messageHistories[sessionId]) { // First time we see this sessionId → create empty history for them messageHistories[sessionId] = new InMemoryChatMessageHistory(); // InMemoryChatMessageHistory starts with: // { messages: [] } ← empty array }
return messageHistories[sessionId]; // Returns the history object for this specific session // RunnableWithMessageHistory uses this to: // 1. LOAD history → inject into prompt BEFORE calling the chain // 2. SAVE new messages → add to history AFTER chain responds }
// ───────────────────────────────────────── // WRAP BASE CHAIN WITH MEMORY // ─────────────────────────────────────────
const chainWithMemory = new RunnableWithMessageHistory({ runnable: baseChain, // the chain we want to add memory to // This is the prompt | llm | parser pipeline we built above
getMessageHistory: getSessionHistory, // function to call to get/create history for a session // RunnableWithMessageHistory calls this automatically
inputMessagesKey: "question", // tells the wrapper: "the user's current message is in the 'question' key" // must match {question} in the prompt template // when you call .invoke({ question: "..." }) → this is the current message
historyMessagesKey: "history", // tells the wrapper: "inject the history into the 'history' key" // must match new MessagesPlaceholder("history") in the prompt template // the wrapper automatically fills this before calling baseChain });
// chainWithMemory is now a STATEFUL chain // Every .invoke() call automatically: // BEFORE: loads history → adds to prompt // DURING: runs baseChain (prompt → llm → parser) // AFTER: saves [HumanMessage, AIMessage] to history for next time
// ───────────────────────────────────────── // EXAMPLE 1 — Conversation with memory // ─────────────────────────────────────────
async function example1() { console.log("─".repeat(50)); console.log("EXAMPLE 1: Conversation with memory"); console.log("─".repeat(50));
const sessionId = "demo_session_1"; // Unique identifier for this conversation // Think of it like a chat room ID or user ID // All messages with this ID share the same history // Different IDs = different conversations = different memories
const config = { configurable: { sessionId } }; // config object that gets passed to .invoke() // RunnableWithMessageHistory reads sessionId from here // to know which history to load and save to // configurable is LangChain's standard way to pass runtime config
const questions = [ "Hi! My name is Sofia and I'm a MERN developer.", "I'm learning AI engineering right now.", "What is my name?", "What am I learning?", "Give me a learning tip based on what you know about me.", ]; // 5 questions that build on each other // Questions 3, 4, 5 test if the AI remembers questions 1 and 2
for (const question of questions) { console.log(`\nYou: ${question}`);
const response = await chainWithMemory.invoke( { question }, // { question: "Hi! My name is Sofia..." } // This is the CURRENT user message // "question" key matches inputMessagesKey in RunnableWithMessageHistory
config, // { configurable: { sessionId: "demo_session_1" } } // Tells the wrapper which session history to use );
console.log(`AI: ${response}`); // response = plain string from StringOutputParser // example: "Nice to meet you Sofia! How can I help you today?" }
// What happens internally for each .invoke() call:
// ── TURN 1: "Hi! My name is Sofia and I'm a MERN developer." ── // // 1. RunnableWithMessageHistory calls getSessionHistory("demo_session_1") // → History is empty: { messages: [] } // // 2. Builds prompt with empty history: // [SystemMessage: "You are a helpful...", // HumanMessage: "Hi! My name is Sofia..."] // // 3. Sends to OpenAI API → gets response: // "Nice to meet you Sofia! As a MERN developer, you must be..." // // 4. Saves to history: // messages: [ // HumanMessage { content: "Hi! My name is Sofia..." }, // AIMessage { content: "Nice to meet you Sofia!..." } // ] // // 5. Returns: "Nice to meet you Sofia!..."
// ── TURN 2: "I'm learning AI engineering right now." ── // // 1. getSessionHistory("demo_session_1") → history now has 2 messages // // 2. Builds prompt WITH history: // [SystemMessage: "You are a helpful...", // HumanMessage: "Hi! My name is Sofia..." ← from history // AIMessage: "Nice to meet you Sofia!..." ← from history // HumanMessage: "I'm learning AI engineering right now."] ← current // // 3. OpenAI sees the FULL conversation → responds with context // "That's exciting! AI engineering is a fascinating field..." // // 4. Saves to history: now 4 messages total // // 5. Returns: "That's exciting!..."
// ── TURN 3: "What is my name?" ── // // 1. getSessionHistory → history has 4 messages // // 2. Full prompt sent to OpenAI: // [System, Human("Hi I'm Sofia"), AI("Nice to meet you"), // Human("I'm learning AI"), AI("That's exciting"), // Human("What is my name?")] // // 3. OpenAI has seen "Sofia" mentioned in turn 1 // Response: "Your name is Sofia!" // // Without memory, OpenAI would say "I don't know your name" // With memory, it can see the entire conversation → "Sofia"
console.log(); }
// ───────────────────────────────────────── // EXAMPLE 2 — Multiple separate sessions // Different users have different memories // ─────────────────────────────────────────
async function example2() { console.log("─".repeat(50)); console.log("EXAMPLE 2: Multiple separate sessions"); console.log("─".repeat(50));
// User 1 conversation await chainWithMemory.invoke( { question: "My favorite color is blue and I love pizza." }, { configurable: { sessionId: "user_alice" } } // session "user_alice" );
// User 2 conversation await chainWithMemory.invoke( { question: "I'm a doctor and I specialize in cardiology." }, { configurable: { sessionId: "user_bob" } } // session "user_bob" — completely separate memory );
// Now ask both users about themselves const aliceAnswer = await chainWithMemory.invoke( { question: "What do you know about me?" }, { configurable: { sessionId: "user_alice" } } // uses alice's history — knows about blue and pizza );
const bobAnswer = await chainWithMemory.invoke( { question: "What do you know about me?" }, { configurable: { sessionId: "user_bob" } } // uses bob's history — knows about cardiology );
console.log("Alice session response:", aliceAnswer); console.log("\nBob session response:", bobAnswer); // Different answers — different memories — same model console.log(); }
// ───────────────────────────────────────── // EXAMPLE 3 — Inspect stored memory // See what's actually stored // ─────────────────────────────────────────
async function example3() { console.log("─".repeat(50)); console.log("EXAMPLE 3: Inspecting stored memory"); console.log("─".repeat(50));
// Run a quick conversation const sessionId = "inspect_demo"; const config = { configurable: { sessionId } };
await chainWithMemory.invoke( { question: "My name is Arjun" }, config );
await chainWithMemory.invoke( { question: "I work at a startup building AI products" }, config );
// Now inspect what's stored const history = getSessionHistory(sessionId); const messages = await history.getMessages(); // getMessages() = returns all stored messages
console.log(`\nStored messages for session "${sessionId}":`); messages.forEach((msg, index) => { const role = msg.constructor.name; // "HumanMessage" or "AIMessage" const preview = msg.content.substring(0, 60); console.log(` ${index + 1}. [${role}]: ${preview}...`); });
console.log(`\nTotal messages stored: ${messages.length}`); // 4 messages: 2 human + 2 AI responses console.log(); }
// ───────────────────────────────────────── // EXAMPLE 4 — Memory with context window limit // Real apps need to limit memory size // ─────────────────────────────────────────
async function example4() { console.log("─".repeat(50)); console.log("EXAMPLE 4: Memory with window limit"); console.log("─".repeat(50)); console.log("(Showing concept — keeps last N messages only)\n");
// In production you'd use a windowed memory // that only keeps the last N messages // to prevent context window overflow
// Simple manual implementation: const MAX_HISTORY = 6; // keep only last 6 messages (3 exchanges)
const sessionId = "windowed_demo";
// Simulate a long conversation const turns = [ "Message 1: I like cats", "Message 2: My favorite food is sushi", "Message 3: I work as a developer", "Message 4: I live in India", "Message 5: I enjoy hiking on weekends", ];
for (const message of turns) { await chainWithMemory.invoke( { question: message }, { configurable: { sessionId } } );
// After each turn — check and trim history if needed const history = getSessionHistory(sessionId); const messages = await history.getMessages();
if (messages.length > MAX_HISTORY) { // Too many messages — trim to last MAX_HISTORY const trimmed = messages.slice(-MAX_HISTORY); // slice(-6) = take last 6 messages
// Clear and refill with trimmed history await history.clear(); for (const msg of trimmed) { await history.addMessage(msg); } console.log(` Trimmed to last ${MAX_HISTORY} messages`); } }
// Final check const finalHistory = getSessionHistory(sessionId); const finalMessages = await finalHistory.getMessages(); console.log(`\nFinal message count: ${finalMessages.length} (max: ${MAX_HISTORY})`); console.log("Memory windowing working correctly ✅"); console.log(); }
async function main() { console.log("\n🧠 MEMORY DEMO\n"); // await example1(); // await example2(); // await example3(); await example4(); console.log("✅ All memory examples complete!"); }
main().catch(console.error);
Run:
node src/05_memory.js
Part 3 — Tools
The Problem
LLMs can only generate text. But real applications need to DO things:
Calculate the current date
Search the web
Call an API
Run a database query
Send an email
Tools give the LLM the ability to call functions — and use the results in its response.
Create src/06_tools.js:
// ───────────────────────────────────────── // IMPORTS // ─────────────────────────────────────────
// ChatOpenAI = wrapper class that lets us talk to OpenAI's GPT models import { ChatOpenAI } from "@langchain/openai";
// tool() = helper function that converts a normal JS function into a // "tool" that the LLM (the AI model) can call on its own when needed import { tool } from "@langchain/core/tools";
// createAgent = the modern way (2026) to build an agent in LangChain. // It replaces the old AgentExecutor + createToolCallingAgent combo. // Internally it runs on LangGraph, which manages the think -> call tool -> think -> answer loop. import { createAgent } from "langchain";
// zod = a library used to define "schemas" (rules for what data should look like). // The LLM uses these schemas to know exactly what inputs a tool expects. import { z } from "zod";
// dotenv = loads variables (like API keys) from a .env file into process.env import * as dotenv from "dotenv"; dotenv.config(); // Actually reads the .env file and loads it into memory
// ───────────────────────────────────────── // CREATE THE LLM (the "brain" of the agent) // ─────────────────────────────────────────
const llm = new ChatOpenAI({ model: "gpt-4o", // which OpenAI model to use temperature: 0, // 0 = very consistent/predictable answers, no randomness });
// ───────────────────────────────────────── // TOOL 1 — CALCULATOR // Purpose: Do math instead of letting the LLM "guess" numbers in its head // ─────────────────────────────────────────
const calculatorTool = tool( // First argument: the actual function that runs when this tool is called. // Whatever the LLM decides to pass in (operation, a, b) lands here automatically. async ({ operation, a, b }) => { switch (operation) { case "add": return `${a} + ${b} = ${a + b}`; case "subtract": return `${a} - ${b} = ${a - b}`; case "multiply": return `${a} × ${b} = ${a * b}`; case "divide": if (b === 0) return "Error: Cannot divide by zero"; // safety check return `${a} ÷ ${b} = ${a / b}`; default: return "Unknown operation"; // fallback if something unexpected is passed } },
// Second argument: metadata that tells the LLM how/when to use this tool { name: "calculator", // this exact name is how the LLM "calls" the tool internally
description: "Performs basic math operations. Use this for any calculations.", // The LLM reads this description to decide WHEN this tool is relevant. // Better description = better decision-making by the LLM.
schema: z.object({ operation: z.enum(["add", "subtract", "multiply", "divide"]) .describe("the math operation to perform"), a: z.number().describe("first number"), b: z.number().describe("second number"), }), // schema = strict contract. The LLM MUST send exactly these fields // (operation, a, b) with these exact types, or the call will fail validation. } );
// ───────────────────────────────────────── // TOOL 2 — WEATHER (simulated / fake data) // Purpose: Demonstrate how a tool can return "external" data // In a real app, this function would call a real weather API (like OpenWeatherMap) // ─────────────────────────────────────────
const weatherTool = tool( async ({ city }) => { // Hardcoded fake weather database (stand-in for a real API call) const weatherData = { "Delhi": { temp: 38, condition: "Hot and sunny", humidity: 45 }, "Mumbai": { temp: 29, condition: "Humid and cloudy", humidity: 85 }, "Bangalore": { temp: 24, condition: "Pleasant and breezy", humidity: 65 }, "London": { temp: 15, condition: "Cloudy with light rain", humidity: 78 }, };
const data = weatherData[city]; // look up the city in our fake database if (!data) return `Weather data not available for ${city}`; // handle unknown city
// Build a readable sentence using the found data return `Weather in ${city}: ${data.condition}, ${data.temp}°C, Humidity: ${data.humidity}%`; }, { name: "get_weather", description: "Gets current weather for a city. Use when asked about weather.", schema: z.object({ city: z.string().describe("the city name to get weather for"), }), } );
// ───────────────────────────────────────── // TOOL 3 — PRODUCT SEARCH (simulated store database) // Purpose: Show how a tool can filter/search data based on multiple inputs // ─────────────────────────────────────────
const productSearchTool = tool( async ({ query, maxPrice }) => { // Fake product catalog (stand-in for a real database query) const products = [ { name: "Laptop Pro X1", price: 85000, category: "laptop", rating: 4.5 }, { name: "Budget Laptop Z", price: 35000, category: "laptop", rating: 4.0 }, { name: "Wireless Mouse", price: 1500, category: "mouse", rating: 4.3 }, { name: "Mechanical Keyboard", price: 5000, category: "keyboard", rating: 4.7 }, { name: "4K Monitor", price: 45000, category: "monitor", rating: 4.6 }, { name: "Budget Monitor", price: 15000, category: "monitor", rating: 3.9 }, ];
// Filter the products array based on the search query and optional max price const results = products.filter(p => { const matchesQuery = p.name.toLowerCase().includes(query.toLowerCase()) || // match by name p.category.toLowerCase().includes(query.toLowerCase()); // OR match by category const withinPrice = maxPrice ? p.price <= maxPrice : true; // price filter (optional) return matchesQuery && withinPrice; // must satisfy BOTH conditions });
if (results.length === 0) return "No products found matching your criteria";
// Turn the filtered array into a readable multi-line string return results.map(p => `${p.name} — ₹${p.price.toLocaleString()} (Rating: ${p.rating}/5)` ).join("\n"); // join each product on a new line }, { name: "search_products", description: "Search for products in our store. Use when customer asks about products or prices.", schema: z.object({ query: z.string().describe("product name or category to search for"), maxPrice: z.number().optional().describe("maximum price in rupees (optional)"), // .optional() = the LLM does NOT have to provide this field }), } );
// ───────────────────────────────────────── // TOOL 4 — DATE AND TIME // Purpose: Give the LLM access to the real current date/time // (LLMs don't inherently "know" the current real-world time on their own) // ─────────────────────────────────────────
const dateTimeTool = tool( async ({ timezone }) => { const now = new Date(); // grabs the real current date/time from the system
const options = { timeZone: timezone || "Asia/Kolkata", // default to India time if none given dateStyle: "full", // e.g. "Wednesday, 15 July 2026" timeStyle: "long", // e.g. "5:45:12 pm GMT+5:30" };
// Format the date/time nicely in Indian English style return `Current date/time: ${now.toLocaleString("en-IN", options)}`; }, { name: "get_datetime", description: "Gets the current date and time. Use when asked about today's date or current time.", schema: z.object({ timezone: z.string().optional() .describe("timezone like 'Asia/Kolkata' or 'America/New_York' (optional)"), }), } );
// ───────────────────────────────────────── // COLLECT ALL TOOLS INTO ONE ARRAY // The agent will pick whichever tool fits the question, on its own // ─────────────────────────────────────────
const tools = [calculatorTool, weatherTool, productSearchTool, dateTimeTool];
// ───────────────────────────────────────── // SYSTEM PROMPT // This is like a "job description" given to the LLM before any conversation starts. // It sets rules/behavior the LLM should always follow. // ─────────────────────────────────────────
const systemPrompt = `You are a helpful assistant with access to tools. Use tools whenever they would help answer the question accurately. Always use the calculator tool for any math — don't calculate in your head. Always use get_datetime for current date/time questions.`;
// ───────────────────────────────────────── // CREATE THE AGENT // createAgent() bundles together: the LLM (model) + the tools + the system prompt // and internally builds a LangGraph "graph" that manages the full reasoning loop: // user asks -> LLM thinks -> LLM may call a tool -> tool runs -> LLM sees result -> LLM answers // ─────────────────────────────────────────
const agent = createAgent({ model: llm, // the brain that decides what to do tools, // the list of actions it's allowed to take systemPrompt, // the behavior rules it must follow });
// ───────────────────────────────────────── // FUNCTION TO RUN THE AGENT FOR A SINGLE QUESTION // ─────────────────────────────────────────
async function runAgent(question) { console.log("─".repeat(50)); // prints a line of 50 dashes, just for visual separation console.log(`Question: ${question}`); console.log("─".repeat(50));
// agent.invoke() actually RUNS the agent on this input and waits for the final result. // The input MUST be in { messages: [...] } format because createAgent (LangGraph-based) // works with a "conversation state" made of messages, not a plain string. const result = await agent.invoke({ messages: [ { role: "user", content: question }, // this is the user's question, as a chat message ], });
// result.messages = the FULL conversation history including: // the user's question, any tool calls, any tool results, and the final AI reply. // We only care about the LAST message, which is the agent's final answer. const finalMessage = result.messages[result.messages.length - 1];
console.log("\nFinal Answer:", finalMessage.content); console.log(); }
// ───────────────────────────────────────── // MAIN FUNCTION — entry point of the script // ─────────────────────────────────────────
async function main() { console.log("\n🔨 TOOLS AND AGENTS DEMO\n");
// This will trigger the agent to internally call the calculatorTool // because the system prompt told it: "Always use the calculator tool for any math" await runAgent("What is 1847 multiplied by 23?");
// Will trigger weatherTool since the system prompt covers weather questions await runAgent("What is the weather like in Bangalore right now?");
// Will trigger productSearchTool with query="laptops" and maxPrice=50000 await runAgent("Show me laptops under ₹50,000");
// Will trigger dateTimeTool await runAgent("What is today's date and time?");
// This will trigger MULTIPLE tools in one go: weatherTool AND productSearchTool await runAgent( "What's the weather in Delhi? And what laptops can I buy for under ₹40,000?" );
console.log("✅ All tool examples complete!"); }
// Actually starts everything. .catch(console.error) makes sure that if // anything crashes/fails, the error gets printed instead of silently failing. main().catch(console.error);
Run:
node src/06_tools.js
How Tools Work Under the Hood
This is important to understand — it's not magic:
User: "What is 1847 × 23?"
↓
LLM thinks: "I need to calculate this.
I have a calculator tool.
I should use it."
↓
LLM outputs a tool call (not text):
{ "tool": "calculator", "inputs": { "operation": "multiply", "a": 1847, "b": 23 } }
↓
AgentExecutor intercepts this
Runs the actual calculator function
Gets result: "1847 × 23 = 42481"
↓
Result fed back to LLM:
"Tool result: 1847 × 23 = 42481"
↓
LLM generates final response:
"1847 multiplied by 23 equals 42,481."
The LLM doesn't calculate — it decides to use a tool, the tool calculates, the result comes back to the LLM, and the LLM formats the final answer.
The Agent Loop
User question
↓
LLM thinks: do I need a tool?
↓
YES → pick the right tool
↓
Call tool with correct inputs
↓
Get tool result
↓
LLM thinks: do I have enough info now?
↓
YES → generate final answer
NO → call another tool
↓
Final answer to user
This loop runs until the LLM decides it has enough information.
3-Line Summary
- Output Parsers tell the LLM exactly what format to return and automatically convert the response —
StructuredOutputParserwith Zod gives you typed JavaScript objects with validated fields instead of raw strings. - Memory stores conversation history per session using a session ID —
RunnableWithMessageHistorywraps any chain to automatically inject past messages into every new call so the LLM can reference what was said before. - Tools are JavaScript functions wrapped with a name, description, and input schema — the LLM reads the descriptions to decide which tool to call, passes the right inputs, gets the result back, and uses it to form its final answer.
Module 6.3 — Complete ✅
Coming up — Module 6.4 — Document Loaders, Text Splitters & Vector Stores in LangChain
We rebuild the PDF chatbot from Phase 5 — but this time using LangChain components. You'll see exactly how much simpler the code becomes, and understand the LangChain versions of every piece you built manually.
No comments:
Post a Comment