Technology Sep 08, 2026 · 5 min read

Building an MVP AI Weather Agent with Node.js and OpenRouter 🌤️🤖

Have you ever wanted to build your own ChatGPT-like interface that can actually do things? Today, we're going to break down Mausam AI, a chatbot built with Node.js that can check real-time weather, temperature, and humidity for any city. ⚠️ Disclaimer: This project is an MVP (Minimum Viable Produc...

DE
DEV Community
by Ankit Halder
Building an MVP AI Weather Agent with Node.js and OpenRouter 🌤️🤖

Have you ever wanted to build your own ChatGPT-like interface that can actually do things? Today, we're going to break down Mausam AI, a chatbot built with Node.js that can check real-time weather, temperature, and humidity for any city.

⚠️ Disclaimer: This project is an MVP (Minimum Viable Product). It is not a production-ready application. Instead, it serves as a conceptual demonstration of a tool-using AI agent pipeline (often associated with Agentic/RAG architectures). It uses simple in-memory state and setInterval polling, which is great for learning but should be replaced with databases and WebSockets for production!

🔗 GitHub Repository: [https://github.com/OriginalAnkit/ai-rag-mausam-ai]

Screenshot of mausam ai

Let's dive into how the code works!

What is RAG and Why Do We Need It?

RAG stands for Retrieval-Augmented Generation. To understand why it's so important, we first need to understand a massive limitation of Large Language Models (LLMs):

Trained models don't have real-time data.
An LLM's knowledge is frozen in time based on when it was trained. If you ask a standard, isolated model, "What is the weather in Mumbai right now?", it will either hallucinate a random answer or apologize, stating that it cannot browse the live internet.

This is where RAG and Agentic workflows come in.
Instead of relying solely on the LLM's static internal memory, we augment its generation process by retrieving external data first. In a traditional RAG setup, this means fetching documents from a vector database.

In our Mausam AI MVP, we use an Agentic tool-use approach: we give the LLM a tool (get_mausam) that it can call to fetch the live, real-time weather report from an external API (wttr.in). We then inject that live data straight back into the conversation context so the LLM can generate an accurate, up-to-the-minute response!

1. The Brains: System Prompts and Agent Logic

The magic of this bot lives in helper.js. Instead of just asking the LLM to write text, we force the LLM to think in a structured loop: START ➡️ PLAN ➡️ TOOL ➡️ OUTPUT.

We achieve this using a strict system prompt and forcing the response format to JSON.

const MAIN_SYSTEM_PROMPT = `
You are an AI agent that reply only to queries related to weather, temperate and humidity.

STRICT RULES:
- output must a single valid json without any extra space, text.
- Run one step at a time. Do not run multiple steps in parallel. Stop after each step
- Strictly follow the Sequence of steps must be START then PLAN then TOOL then OUTPUT

OUTPUT FORMAT:
{
    "step": START|PLAN|TOOL|OUTPUT,
    "context": "string",
    "input": "string",
    "usefull": "boolean",
    "toolname": "string"
}

AVAILABLE TOOL:
- get_mausam -> return temperate, weather and humidity for a given location
`;

By enforcing this structure, our backend can read the JSON step by step. If the AI decides it needs to use a tool, it outputs {"step": "TOOL", "toolname": "get_mausam", "input": "Mumbai"}.

Executing the Loop

Our backend intercepts this and executes the tool on behalf of the AI:

const getConversation = async function (messages, context = []) { 
    while (true) {
        let completion = await callOpenRouterModel(messages);
        let output = safeParseJSON(completion?.choices[0].message.content);

        if (output?.step === "OUTPUT") {
            // The AI has the final answer
            context.push({ sender: "SYSTEM", message: output.context });
            return;
        } else if (output?.step === "TOOL" && output.toolname === "get_mausam") {
             // The AI requested a tool. We fetch the data and feed it back!
             let tool_resp = await getWeather(output.input);
             messages.push({ role: "system", content: `RESPONSE FROM get_mausam: ${tool_resp}` });
        } else {
             // Intermediate thinking steps (START, PLAN)
             context.push({ sender: "BOT", message: output.context + '...' });
             messages.push({ role: "system", content: JSON.stringify(output) });
        }
        await sleep(5000); // Respect API rate limits
    }
}

2. The Tool: Fetching Weather Data

When the AI calls get_mausam, it triggers a simple JavaScript fetch to wttr.in, an amazing console-oriented weather forecasting service.

async function getWeather(city) {
    // Custom formatting for wttr.in to return exactly what we need
    const url = \`https://wttr.in/\${encodeURIComponent(city)}?format=%c+%C+%t+%h+%T\`;

    const response = await fetch(url);
    if (!response.ok) throw new Error(\`Request failed\`);

    const data = await response.text(); 
    return data.trim(); // Returns e.g., "☀️ Clear +22°C 45%"
}

This data is then appended back into the messages array so the LLM can read it and formulate its final OUTPUT step.

3. Resilient API Calls

To keep our MVP robust against rate limits or API key exhaustion, we built a fallback mechanism when calling OpenRouter. If the primary key fails, it automatically tries a secondary key!

const callOpenRouterModel = async function (messages) {
    const requestPayload = {
        model: "minimax/minimax-m3:free",
        messages: messages,
        response_format: { type: "json_object" }
    };

    try {
        return await client.chat.completions.create(requestPayload);
    } catch (error) {
        console.error("Primary key failed, trying fallback key...", error.message);
        const fallbackClient = new OpenAI({
            baseURL: "https://openrouter.ai/api/v1",
            apiKey: process.env.OPEN_ROUTER_KEY_2,
        });
        return await fallbackClient.chat.completions.create(requestPayload);
    }
}

4. The Express Backend & Chat UI

We wrap this entire logic inside a simple Express.js server (app.js). We maintain a global messages array (again, MVP only—use a real database for production!) and expose a GET /messages endpoint.

On the frontend (index.ejs), we have a sleek dark-mode UI. To keep the chat updated while the AI loops through its START -> PLAN -> TOOL phases, the frontend uses simple polling:

// Poll every 10 seconds to fetch new messages/thoughts from the bot
setInterval(fetchMessages, 10000);

We even styled the intermediate "thinking" steps (the BOT sender) differently than the final answer (the SYSTEM sender) so users know the bot is working in the background!

Conclusion

Building Agentic pipelines doesn't require massive frameworks. By enforcing JSON schemas and writing a simple while loop, you can give LLMs access to the outside world.

Feel free to check out the GitHub repo and tinker with it yourself. Try adding new tools, like a news fetcher or a calculator!

Happy coding! 🚀

DE
Source

This article was originally published by DEV Community and written by Ankit Halder.

Read original article on DEV Community
Back to Discover

Reading List