Utilizando agentes de IA para acessar base de dados relacional

Marcelo Nunes Alves
6 min readJun 9, 2024

--

Após o lançamento do ChatGPT para o mercado geral, as IAs ganharam bastante espaço nas notícias, discussões e dentro do escopo das empresas que se perguntam como incorporar essa nova tecnologia ao seu negócio. O ChatGPT trouxe a aplicação deste modelo de IA como um chatbot, que recebe e responde perguntas de usuários de forma automática. Há vários tutoriais no YouTube sobre como personalizá-lo para o seu próprio objetivo, desde os mais aprofundados tecnicamente, que ensinam como criar seu próprio chatbot com o ChatGPT, ou IAs análogas, até os tutoriais mais simples, focados em como escrever prompts e automatizar tarefas de geração de conteúdo.

No entanto, é possível que você ou sua empresa não estejam interessados em um serviço de chatbot, ou em como gerar conteúdo para suas tarefas. É claro que um chatbot, que auxilia em tarefas do dia a dia como a geração ou documentação de códigos, pode ser extremamente eficiente, mas e se tentássemos ir um pouco além do modelo de chat? Nesse caso, que outras aplicações esses modelos conseguiriam fornecer? Quais outras rotinas ou tarefas poderiam se beneficiar do uso destes modelos de Inteligência Artificial?

Trabalhando em projetos de grande volume de dados estruturados, uma das minhas principais preocupações é como permitir que o usuário possa pesquisar qualquer coisa no DW de forma fácil e rápida, sem a necessidade de criar um dashboard ou ter conhecimento de comando SQL para responder a sua pergunta.

Neste post, irei demonstrar como fazer isso utilizando os recursos da Generative AI para executar consulta em banco de dados relacional, e devolver os dados de acordo com a solicitação do usuário.

E para isso será utilizado o Llama 3 para o processamento de linguagem natural, que é o modelo de IA desenvolvido pelo Meta. Por que o Llama 3? Bom, a resposta é bem simples:

em primeiro lugar, muitos dos exemplos que existe na internet é utilizando os modelos da OpenAI, e o segundo, porque ele é um modelo Open Source, gratuito, não há necessidade de pagar por seu uso e o modelo pode rodar localmente, sem nenhuma preocupação com quantidade de requisições ou onde seus dados de requisição estão sendo armazenados. Vamos explorar como o Llama 3 pode ser integrado com bancos de dados para extrair informações valiosas a partir de textos.

Para isso irei utilizar os dados de consumo de energia elétrica no brasil por estado, esses dados estão disponíveis na plataforma basedosdados.org e pode ser baixado aqui.

Ambiente

Após baixar o arquivo CSV com os dados, agora chegou o momento de subir um banco de dados MariaBD para que o agente possa carregar os dados e responder a solicitação dos usuários.

docker run --name banco_de_dados -p 3306:3306 -e MYSQL_ROOT_PASSWORD=art_llama3 -d mariadb:lts

O comando abaixo cria o banco de dados:

docker exec -it banco_de_dados mariadb -uroot -part_llama3 -e "CREATE DATABASE IF NOT EXISTS mme;"

Segue abaixo o comando de carregamento dos dados de estados, que serão utilizados para enriquecer o retorno.

docker exec -it banco_de_dados mariadb -uroot -part_llama3 mme -e "CREATE TABLE IF NOT EXISTS uf (sigla VARCHAR(2), nome_do_estado VARCHAR(255),PRIMARY KEY (sigla)); INSERT INTO uf (sigla, nome_do_estado) VALUES ('AC', 'Acre'), ('AL', 'Alagoas'), ('AP', 'Amapá'), ('AM', 'Amazonas'), ('BA', 'Bahia'), ('CE', 'Ceará'), ('DF', 'Distrito Federal'), ('ES', 'Espírito Santo'), ('GO', 'Goiás'), ('MA', 'Maranhão'), ('MT', 'Mato Grosso'), ('MS', 'Mato Grosso do Sul'), ('MG', 'Minas Gerais'), ('PA', 'Pará'), ('PB', 'Paraíba'), ('PR', 'Paraná'), ('PE', 'Pernambuco'), ('PI', 'Piauí'), ('RJ', 'Rio de Janeiro'), ('RN', 'Rio Grande do Norte'), ('RS', 'Rio Grande do Sul'), ('RO', 'Rondônia'), ('RR', 'Roraima'), ('SC', 'Santa Catarina'), ('SP', 'São Paulo'), ('SE', 'Sergipe'), ('TO', 'Tocantins');"

O comando abaixo copia o arquivo CSV para o contêiner.

docker cp uf.csv banco_de_dados:/uf.csv

Para carregar os dados para o MariaDB no docker, execute o comando abaixo:

docker exec -it banco_de_dados mariadb -uroot -part_llama3 mme -e "CREATE TABLE IF NOT EXISTS consumo_energia_eletrica (ano INT, mes INT, sigla_uf VARCHAR(255), tipo_consumo VARCHAR(255), numero_consumidores INT, consumo_MWh FLOAT, CONSTRAINT sigla_uf_FK FOREIGN KEY (sigla_uf) REFERENCES uf(sigla)); LOAD DATA INFILE '/uf.csv' INTO TABLE consumo_energia_eletrica FIELDS TERMINATED BY ',' ENCLOSED BY '\"' LINES TERMINATED BY '\n' IGNORE 1 ROWS (ano, mes, sigla_uf, tipo_consumo, @numero_consumidores, consumo_MWh) SET numero_consumidores = NULLIF(@numero_consumidores, '');"

Fiz a remoção de alguns dados que poderiam gerar um problema de alucinação, pois existem dados de totalizações juntos que podem deixar o retorno confuso para o agente.

docker exec -it banco_de_dados mariadb -uroot -part_llama3 mme -e "delete from consumo_energia_eletrica where tipo_consumo in ('Total', 'Cativo');"

Notebook

Neste bloco, inicializarei todas as bibliotecas que serão utilizadas no projeto, incluindo o Langchain e o CrewAI.

from crewai import Agent, Task, Crew, Process
from langchain_community.utilities import SQLDatabase
from langchain_community.llms import Ollama
from langchain_groq import ChatGroq
from crewai_tools import tool
import ast

A seguir, apresento a conexão com o banco de dados, que será utilizada nas consultas da ferramenta personalizada do agente.

maria_uri = 'mysql+mysqlconnector://root:art_llama3@localhost:3306/mme'
db = SQLDatabase.from_uri(maria_uri)

Este método retorna o esquema do banco de dados que será utilizado na construção da consulta.

def get_schema(_):
schema = db.get_table_info()
return schema

Neste trabalho, utilizei o serviço Groq para instanciar o modelo LLM que será utilizado pelo agente. O Groq oferece uma ampla gama de modelos de forma gratuita, o que me permitiu escolher o modelo mais adequado para a tarefa em questão.

Observação: Devido à indisponibilidade de recursos computacionais suficientes, não foi possível executar o modelo Llama3:70B localmente. Além disso, o modelo Llama3:8B não obteve resultados satisfatórios para o prompt utilizado.

llm = ChatGroq(temperature=0, groq_api_key="[Conteudo do token da API do Groq]", model_name="llama3-70b-8192")

A seguir, apresento o método que executa a consulta no banco de dados com base na consulta elaborada pelo modelo LLM utilizado no agente.

@tool("Execute query DB tool")
def run_query(query: str):
"""Execute a query in the database and return the data."""
print(query)
return "This is the return data from the database: " + db.run(query, fetch = "all", include_columns = True)

O CrewAI opera com base no conceito de múltiplos agentes, assemelhando-se a uma tripulação com diversos papéis e responsabilidades, todos trabalhando em conjunto para alcançar um objetivo comum. Neste ponto, um agente específico é criado para assumir a responsabilidade de compreender a solicitação do cliente, gerar a consulta correspondente para execução no banco de dados e retornar ao usuário a resposta em linguagem natural da execução dessa consulta.

sql_developer_agent = Agent(
role='Senior SQL developer',
goal="Return data from the database by running the Execute query DB tool.",
backstory="""You are familiar with MariaDB syntax. You know the following table schema. Use the Execute query DB tool to execute the query in the database. You must respond to what is returned from the database.""",
tools=[run_query],
allow_delegation=False,
verbose=True,
llm=llm
)

Todos os agentes podem ter uma ou mais tarefas a serem executadas. Cada tarefa descreve o que deve ser realizado e qual o objetivo a ser entregue.

sql_developer_task = Task(
description="""Build a SQL query to answer the question: {question}. Follow the following schema: {schema}. This query is executed on the database. Your final answer MUST be exactly text in portuguese(pt-BR) based the data returned.""",
expected_output="""Return data from the database.""",
agent=sql_developer_agent,
tools=[run_query]
)

Abaixo é criado o time que irá realizar o trabalho.

crew = Crew(
agents=[sql_developer_agent],
tasks=[sql_developer_task],
process=Process.sequential,
manager_llm=llm
)

A formação da equipe permite o início das interações. A primeira pergunta a ser explorada é: “Qual foi o estado e o ano que teve o maior consumo em MWh?”. Com base nessa pergunta, o agente, que atua como desenvolvedor de consultas SQL, irá compreender a solicitação, formular a consulta SQL correspondente e executá-la. A partir do resultado obtido, o agente gerará um texto explicativo.

result = crew.kickoff(inputs={'question': 'Qual foi o estado e o ano que teve o maior consumo em MWh?', 'schema': get_schema(None)})

print(result)

Apresentando o seguinte resultado:

A segunda pergunta a ser explorada é: “Qual foi o estado que teve o menor consumo?”

result = news_crew.kickoff(inputs={'question': 'Qual foi o estado que teve o menor consumo?', 'schema': get_schema(None)})

print(result)

Apresentando o seguinte resultado:

O código desse notebook pode ser baixado aqui.

Conclusão

O processo de agente conseguiu criar a query com o banco de dados corretamente e retornar a resposta adequada. No entanto, o modelo de 8 bilhões de parâmetros não obteve tanto sucesso quanto um prompt simples, que conseguiu realizar a tarefa eficientemente. Acredito que a separação do agente em duas partes, diferenciando melhor os papéis, pode aumentar a precisão e a eficácia da solução, permitindo uma abordagem mais direcionada e eficiente para cada agente.

--

--