Blog MNunes

Como Criar um Agente de Banco de Dados com Gemini e Node.js no Telegram

como-criar-um-agente-de-banco-de-dados-com-gemini-e-node-js-no-telegram

Recentemente, mostrei aqui no blog como criar um chatbot básico para Telegram. Hoje, vamos elevar o nível: vamos construir um Agente Inteligente.

Diferente de um bot comum que apenas responde comandos, um agente consegue tomar decisões, utilizar ferramentas (tools) e interagir com fontes de dados externas — no nosso caso, um banco de dados SQL — para responder perguntas complexas em linguagem natural.

A Arquitetura do Agente

Para este projeto, utilizamos uma estrutura modular onde o Gemini atua como o “cérebro” orquestrador, decidindo quando precisa consultar o banco de dados para obter uma informação que ele não possui nativamente.

  1. Telegram (Telegraf): Recebe a mensagem do usuário.
  2. Main.js: Valida a autorização e encaminha para o agente.
  3. Gemini (Orquestrador): Analisa a intenção. Se precisar de dados, chama a tool generate_sql.
  4. Tool (Generate SQL): Um prompt especializado gera a query baseada no schema.
  5. Database: A query é executada e o resultado volta para o Gemini sintetizar a resposta final.

1. Configuração do Ambiente (.env)

Antes de rodar o projeto, você precisa configurar suas credenciais. Crie um arquivo .env na raiz do projeto com as seguintes chaves:

TELEGRAM_TOKEN=seu_token_aqui
GOOGLE_API_KEY=sua_chave_gemini_aqui
CHAT_ID=seu_id_do_telegram
DB_HOST=localhost
DB_USER=root
DB_PASS=
DB_NAME=meu_db
  • TELEGRAM_TOKEN: Obtido via @BotFather.
  • GOOGLE_API_KEY: Obtida no Google AI Studio.
  • CHAT_ID: Fundamental para a segurança (veja a seção de Segurança abaixo), garantindo que apenas você controle o bot.

2. Preparando o Banco de Dados (Instalação Local)

Para que o agente funcione, ele precisa de um banco de dados para consultar. No repositório, incluímos o arquivo database_schema.txt. Este arquivo não serve apenas para instruir a IA, mas também como guia para você criar suas tabelas localmente.

Se estiver usando MySQL, você pode usar o conteúdo deste arquivo para rodar os comandos CREATE TABLE e inserir seus dados iniciais.

O arquivo db.js gerencia essa conexão e executa uma query de busca quando necessário:

import mysql from 'mysql2/promise';
import dotenv from 'dotenv';
dotenv.config();

async function query(sql) {
    const connection = await mysql.createConnection({
        host: process.env.DB_HOST,
        user: process.env.DB_USER,
        password: process.env.DB_PASS,
        port: process.env.DB_PORT || 3306,
        database: process.env.DB_NAME,
    });
    const [results] = await connection.execute(sql);

    await connection.end();

    return results;
}

export { query };

3. A Ferramenta de Geração de SQL (tools/generateSql.js)

O segredo de um bom agente de dados é o contexto. No arquivo generateSql.js, passamos o database_schema.txt como instrução da ferrament. Isso garante que o Gemini saiba exatamente quais tabelas e colunas existem e que a pergunta do usuário deve se referi a tabela que essa ferramenta conhece.

import fs from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const schemaFilePath = path.resolve(__dirname, './database_schema.txt');

export default {
    name: "generate_sql",
    description: `Recebe uma pergunta sobre a loja (ou suas colunas) e Gera SQL da tabela loja com base na pergunta ou comando do usuário.
    Considere comando ou pergunta como sendo a mesma coisa (pergunta).
    Schema a ser considerado:
    ${schemaFilePath} `,
    parameters: {
        type: "object",
        properties: {
            pergunta: { type: "string" },
        },
        required: ["pergunta"],
    },
};

4. O Agente e o Orquestrador (geminiAgent.js)

Aqui utilizamos o fluxo de Function Calling (Tools). O orquestrador (gemini-1.5-flash-lite) em sua response retorn um chamda de função, e verificamos se essa função se chama generate_sql. Por trás dos panos o modelo faz um “match” da mensagem com as ferramentas antes de responder. Se a função foi retornada, signfica que devemos fazer o que a ferramenta exige. Dai em diante, usamos o modelo pra traduzir a pergunta do usuário em uma query sql válida e com ela, consultamos no banco. Com o retorno da resposta, pedimos pro modelo criar uma resposta amigável (considerando a pergunta do usuáio e o retorno do banco de dados).

import { GoogleGenAI, Type } from '@google/genai';
import dotenv from 'dotenv';
dotenv.config();
import path from 'path';
import * as fs from 'fs/promises';
import generateSql from './tools/generateSql.js';
import { fileURLToPath } from 'url';
import { query } from './db.js';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const genAI = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });

async function runAgent(pergunta, history = [], ctx = null) {
    // Cria uma cópia do histórico sem incluir a última pergunta do usuário
    // já que ela será enviada diretamente como mensagem
    let chatHistory = [];

    let statusMessage = null;

    let thinkingMessage = null;

    if (ctx) {

        // Envia uma mensagem de "pensando" para o usuário
        thinkingMessage = await ctx.reply('🧠Pensando...');
    }

    if (history && history.length > 0) {
        // Se houver histórico e pelo menos 2 mensagens, pegamos tudo exceto a última (que é a pergunta atual)
        if (history.length > 1) {
            chatHistory = history.slice(0, -1);
        }
    }

    const chat = await genAI.chats.create({
        model: 'gemini-2.5-flash-lite-preview-06-17',
        thinkingConfig: {
            thinkingBudget: 256,
        },
        history: chatHistory,
        config: {
            systemInstruction: `
            Você é um assistente de gentente de uma loja que faz consulta aos dados da mesma através das ferramentas disponíveis.
            O usuário não precisa saber o nome da tabela, apenas o que ele quer saber.
            Em caso de pergunta genérica sobre uma tabela, você deve gerar uma SQL com todas as colunas da tabela em questão.
            Ex: Me liste todos os produtos = SELECT * FROM produtos;
            
            Caso a pergunta não se adeque a ferramenta, você deve responder com uma mensagem informando que não conseguiu entender a pergunta e como o usuáriode deve fazer a pergunta.`,
            tools: [{
                functionDeclarations: [generateSql],
            }],
        },
    });

    const response = await chat.sendMessage({
        message: pergunta,
    });

    await ctx.deleteMessage(thinkingMessage.message_id);

    if (response.functionCalls && response.functionCalls.length > 0) {

        if (ctx) {
            statusMessage = await ctx.reply('🔄 Consultando...');
        }

        const functionCall = response.functionCalls[0];
        const functionName = functionCall.name;

        let functionArgs;
        try {
            functionArgs = functionCall.args || {};
        } catch (error) {
            console.error("Erro ao processar argumentos da função:", error);
            functionArgs = {};
        }

        if (functionName === 'generate_sql') {

            const schemaFilePath = path.resolve(__dirname, './database_schema.txt');
            const schema = await fs.readFile(schemaFilePath, 'utf-8');
            const instructions = `
            Você deve receber uma pergunta/comando do usuário a ser traduzida em uma consulta SQL 
            sobre a tabela ${process.env.DB_NAME} que pode retornar o que o usuário precisa e quer saber.
            caso não seja informado um campo, pesquise nome e id.
            considere o histórico da conversa para entender o que o usuário quer, e não retorne nada além do JSON com o comando SQL.
            Schema do banco de dados: ${schema}
            Historico da conversa para contexto: ${JSON.stringify(chatHistory.slice(-4))}
            `;
            const sqlQuestion = `baseado no comando/pergunta: ${pergunta} Reponsa em JSON com um comando SQL `;

            const modelQuery = await chat.sendMessage({
                model: 'gemini-2.0-flash',
                message: sqlQuestion,
                config: {
                    systemInstruction: instructions,
                    responseMimeType: 'application/json',
                    responseSchema: {
                        type: Type.ARRAY,
                        items: {
                            type: Type.OBJECT,
                            properties: {
                                'query': {
                                    type: Type.STRING,
                                    description: 'Query SQL gerada',
                                    nullable: false,
                                },
                            },
                            required: ['query'],
                        },
                    },
                }
            });

            const sql = JSON.parse(modelQuery.text);

            let queryResult = null;

            try {
                const result = await query(sql[0].query);
                queryResult = `Resultado da consulta: ${JSON.stringify(result)}`;
            } catch (error) {
                console.error("Erro ao executar a consulta SQL:", error);
                return "Erro ao executar a consulta SQL.";
            }

            if (queryResult) {
                const finalResponse = await chat.sendMessage({
                    message: `
                    Reponendo a pergunta: ${pergunta}, retorne uma resposta em linguagem natural o resultado da query que foi a seguinte:
                    ${queryResult}
                    
                    retonre para responder de mandeira criativa com emojis (moderadamente) e para o telegram, use quebra de linha com '\n'`,
                });
                if (statusMessage) {
                    await ctx.deleteMessage(statusMessage.message_id);
                }
                return finalResponse.text;

            }
        }
    }

    return response.text;
}


export default runAgent;

5. Interface e Segurança (main.js)

Segurança é um ponto crítico ao expor um banco de dados a uma IA. No arquivo main.js, implementamos uma trava de CHAT_ID. Isso impede que qualquer pessoa que encontre seu bot no Telegram execute queries no seu banco.

import { Telegraf } from 'telegraf';
import { runAgent } from './geminiAgent.js';

const bot = new Telegraf(process.env.TELEGRAM_TOKEN);
const AUTHORIZED_USER = process.env.CHAT_ID;

bot.on('text', async (ctx) => {
    // Trava de Segurança: Verifica se o ID do usuário é o autorizado
    if (ctx.chat.id.toString() !== AUTHORIZED_USER) {
        console.warn(`Tentativa de acesso não autorizada do ID: ${ctx.chat.id}`);
        return ctx.reply("Acesso não autorizado. Este agente é privado.");
    }

    try {
        const response = await runAgent(ctx.message.text);
        await ctx.reply(response, { parse_mode: 'Markdown' });
    } catch (error) {
        console.error(error);
        await ctx.reply("Houve um erro ao processar sua consulta.");
    }
});

bot.launch();

Conclusão

A combinação de um orquestrador leve (1.5-flash-lite) com um gerador técnico preciso (2.0-flash) cria um agente robusto. Ao utilizar variáveis de ambiente e validação de CHAT_ID, garantimos que a potência da IA generativa seja utilizada de forma segura e controlada.
Agora imagine o que pode ser feito usando as chamdas de funções com ferramentas. Você pode construir agentes inteligentes com capacidade de interagir com o API’s, Base de Dados, Gerenciar aquivos e muito mais!

Links úteis

Repositório desse projeto: https://github.com/marcosnunesmbs/gemini_db_agent

Botfather: https://t.me/botfather

Documentação Gemini API: https://ai.google.dev/gemini-api/docs