Utilizando GenAI y Agentes Inteligentes para extraer insights

Santiago Elewaut
Data & AI Accenture Argentina
10 min readAug 19, 2024

--

En la era de los datos, la capacidad de gestionar y manipular bases de datos de manera eficiente se ha vuelto una competencia crucial para las empresas. Las consultas SQL son la herramienta principal para interactuar con bases de datos, permitiendo la extracción, inserción y modificación de datos. Sin embargo, escribir consultas SQL puede ser una tarea compleja y propensa a errores, especialmente para quienes no son expertos en programación. En un contexto donde cada vez hay más decisiones que se toman de una forma data driven, puede pasar que, por ejemplo, personas con poca o nula experiencia en programación necesiten extraer información o un insight sencillo de una base de datos, pero no sepan cómo hacerlo.

En este contexto, los modelos generativos de inteligencia artificial están emergiendo como una solución innovadora. Estos modelos, como GPT-3 y GPT-4, o incluso el más nuevo e innovador GPT-4o de OpenAI, han demostrado una capacidad sorprendente para entender y generar lenguaje natural, y ahora están siendo aplicados para revolucionar la forma en que interactuamos con información, desde texto e imágenes, hasta audio. Es aquí donde surge la idea de construir un sistema que pueda traducir una solicitud en lenguaje natural, como “Muéstrame las ventas del último trimestre”, en una consulta SQL precisa y optimizada, y que luego podamos obtener los resultados a esa consulta, sin la necesidad de saber programación o lenguaje SQL. De esta forma, estamos pudiendo potenciar la capacidad de una organización de generar insights valiosos para la toma de decisiones.

En este artículo explicaremos cómo armar un ejemplo sencillo de aplicación de un modelo generativo de lenguaje natural para traducir preguntas sobre datos en lenguaje natural a consultas SQL, extrayendo información sea de una base de datos con varias tablas, o por ejemplo un archivo.csv o Excel. Para ello, utilizaremos el modelo GPT-4 de OpenAI, que ha demostrado una capacidad excepcional para comprender y generar lenguaje natural. Para armar esta pequeña demo utilizaremos Python y el famoso framework de desarrollo de aplicaciones con LLMs, Langchain.

Conociendo Langchain

Langchain es un framework que nos provee un montón de bloques modulares que simplifican el desarrollo, productización y deployment de aplicaciones basadas en modelos de lenguaje natural. Langchain nos permite construir aplicaciones de lenguaje natural de manera rápida y sencilla, sin tener que preocuparnos por la infraestructura subyacente, y poniendo como foco el problema que queremos resolver. Incluso provee algunos templates para comenzar a trabajar directamente.

En este diagrama podemos ver cómo están compuestos los diferentes bloques de Langchain.

Fuente: Introduction | 🦜️🔗 LangChain

Para esta demo, usaremos Chains y Agents, dos de los principales componentes de langchain.

Armando el Agente SQL

Para armar el agente, seguiremos los siguientes pasos:

1. Conectarse a la base datos

2. Inicializar nuestra conexión con la API de OpenAI mediante `langchain`

3. Inicializar agente con conectores a la API y a la base de datos

1. Conectarse a la base datos

Para conectarse a la base de datos, langchain cuenta con conectores para una gran variedad de base de datos SQL. En nuestro caso usaremos una base de datos local con `sqlite3` que cuenta con información de un dataset de Kaggle de ventas de supermercado.

Nos conectaremos con el siguiente código:

from sqlalchemy import create_engine
engine = create_engine('sqlite:///sales_data.db')

La estructura de la tabla es la siguiente:

Table: sales_data
Invoice ID: TEXT
Branch: TEXT
City: TEXT
Customer type: TEXT
Gender: TEXT
Product line: TEXT
Unit price: REAL
Quantity: INTEGER
Tax 5%: REAL
Total: REAL
Date: TEXT
Time: TEXT
Payment: TEXT
cogs: REAL
gross margin percentage: REAL
gross income: REAL
Rating: REAL

Para utilizar la base de datos con `langchain`, usaremos su respectivo conector, que ya cuenta con un toolkit para ejecutar queries, inspeccionar el esquema de la base de datos (fundamental para que el LLM conozca la data), etc.

2. Inicializar nuestra conexion con la API de OpenAI mediante `langchain`

Para conectarnos con el LLM de OpenAI, utilizaremos el objeto conector, que automáticamente busca una KEY para la API, y podemos utilizar en los diferentes pasos de la cadena donde se necesite acceso al LLM.

from langchain_community.chat_models import ChatOpenAI
chat_model_agent = ChatOpenAI(temperature=0.1, model="gpt-4o")

3. Inicializar agente con conectores a la API y a la base de datos

Luego todo esto lo consolidaremos en una cadena.

from itertools import chain
from operator import itemgetter

from langchain.agents import AgentExecutor
from langchain_core.utils.function_calling import convert_to_openai_function
from langchain.agents.format_scratchpad.tools import format_to_tool_messages
from langchain.agents.format_scratchpad.openai_functions import format_to_openai_functions
from langchain.agents.output_parsers import OpenAIFunctionsAgentOutputParser, ToolsAgentOutputParser
from langchain.schema.output_parser import StrOutputParser
from langchain.schema.runnable import (
RunnablePassthrough,
)


from langchain.sql_database import SQLDatabase
from langchain_community.agent_toolkits import SQLDatabaseToolkit

from sqlalchemy.engine import Engine
from purch_ai.toolkits.sql_agent_toolkit import SQLAgentToolkit

# database connection
sql = SQLDatabase(engine=engine)
# database tookit
toolkit = SQLAgentToolkit(
db=sql,
llm=chat_model_agent,
toolkit=SQLDatabaseToolkit(db=sql, llm=chat_model_agent),
)
# bind tools/functions to openai model.
functions = [convert_to_openai_function(f) for f in toolkit.get_tools()]
chat_model_agent = chat_model_agent.bind(functions=functions)
# relevant question chain

def create_agent_chain(chat_model_agent, engine: Engine):
# instantiate toolkit
sql = SQLDatabase(engine=engine)
toolkit = SQLAgentToolkit(
db=sql,
llm=chat_model_agent,
toolkit=SQLDatabaseToolkit(db=sql, llm=chat_model_agent),
)
# create chain.

chat_model_agent = chat_model_agent.bind(functions=functions)

# agent chain
agent_chain = (
RunnablePassthrough.assign(
agent_scratchpad=lambda x: format_to_openai_functions(
x["intermediate_steps"]
)
)
| HIST_PROMPT
| chat_model_agent
| OpenAIFunctionsAgentOutputParser()
)

tools = toolkit.get_tools()
return agent_chain, tools

Como verán, se utiliza una variable HIST_PROMPT. Acá es donde se le pasan las instrucciones al LLM, para que tenga en su contexto lo que espera a nivel de definición de datos, y qué tipos de tareas se le van a encomendar. El contenido específico de esta prompt, es muy particular al problema que se quiere resolver, y definirla en sí es un proceso de prueba y error.

Para el caso de nuestra aplicación, le pasamos el siguiente texto.

from langchain.prompts import PromptTemplate
from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder, PromptTemplate
HIST_PROMPT = ChatPromptTemplate.from_messages(
[
(
"system",
"""You are a super intelligent chatbot having a conversation with a human and helping him do analysis on a database and a purchase order dataset, containing things like sales, prices, transactions, quantity, prices, order ids, etc, quantities, sales. Generally buisines related questions. QUestions can be either in spanish or english.
Remember to keep the language of the question, if it is in english answer in english and if it is in spanish answer in spanish.
Also remeember that is question has already been answered in the format of output: <answer_placeholder> you can use these values instead of reaccessing the database so that you can be faster at providing answers.
Speed of your answers is a must. RemembMer not tomodify results from things such as names, that could have leading or trailing spaces, typos or special symbols.
Maintaining the original data is critical to continue a fluid conversation and avoid SQL bad queries.
The assistant is typically calculating, methodic, producing precise analytical and relevant numerical responses.""" ),
MessagesPlaceholder(variable_name="chat_history"),
("user", "{input}"),
MessagesPlaceholder(variable_name="agent_scratchpad"),
]
)

Como se puede ver, le damos un resumen del contexto de la data. Que tipo de respuesta se espera recibir, y se le pide que use su “memoria” (tema que discutiremos luego), en caso de que la pregunta sea o repetida, o un detalle de algo que ya se respondió. Esto queda resumido

Luego, encapsularemos la creacion del agente en una funcion.

import create_agent_chain
from langchain.agents import AgentExecutor
from langchain.memory import ConversationBufferMemory


def create_agent_exector(chat_model_agent, engine, memory: ConversationBufferMemory):
agent_chain, tools = create_agent_chain(chat_model_agent, engine)

# initialize agent executor
agent_executor = AgentExecutor(
agent=agent_chain, tools=tools, verbose=True, memory=memory
)
return agent_executor

Por último, generamos el agente, que utiliza la cadena para poder interactuar con la base de datos y, en caso que el LLM lo considere, utilizará las funciones para ejecutar queries. Por otro lado, si se trata de consolidar algún insight, o generar algún resumen, este directamente se extraerá de la “memoria”.

Armando el bot en una webapp con un chat.

Si bien ya el bot quedó listo, lo que termina de darle una mejor experiencia al usuario. Intentaremos hacer esto embebiendo a nuestro bot en una interfaz con un chat como con ChatGPT. Para implementar esto, usaremos streamlit, que permite con facilidad y rapidez armar una interfaz sencilla de chat para interactuar con el agente SQL.

Adicionalmente, streamlit, en conjunto con langchain nos permite guardar en memoria mensajes anteriores de la sesión de chat, para mantener información relevante de preguntas anteriores a disposición del modelo evitando, por ejemplo, hacer queries repetidas.

Con este código, lo que hacemos es inicializar los objetos donde se guardan los mensajes históricos de la sesión de chat, y por último generamos una interfaz simple con streamlit.

import streamlit as st
from langchain.memory import ConversationBufferMemory
from langchain_community.callbacks import StreamlitCallbackHandler
from langchain_community.chat_message_histories import StreamlitChatMessageHistory
from langchain_core.runnables import RunnableConfig
import create_agent_exector
st.set_page_config(page_title="LangChain: Chateando con una base de datos", page_icon="🦜")
st.title("🦜 LangChain: Cheateando con una base de datos")


msgs = StreamlitChatMessageHistory()
memory = ConversationBufferMemory(
chat_memory=msgs,
return_messages=True,
memory_key="chat_history",
output_key="output",
)

if len(msgs.messages) == 0 or st.sidebar.button("Reset chat history"):
msgs.clear()
msgs.add_ai_message("¿En qué puedo ayudarte hoy?")
st.session_state.steps = {}


avatars = {"human": "user", "ai": "assistant"}
for idx, msg in enumerate(msgs.messages):
with st.chat_message(avatars[msg.type]):
# Render intermediate steps if any were saved
for step in st.session_state.steps.get(str(idx), []):
if step[0].tool == "_Exception":
continue
with st.status(
f"**{step[0].tool}**: {step[0].tool_input}", state="complete"
):
st.write(step[0].log)
st.write(step[1])
st.write(msg.content)

if prompt := st.chat_input(placeholder="¿Cuales son las ventas totales?"):
st.chat_message("user").write(prompt)

sql_agent = create_agent_exector(
chat_model_agent=chat_model, engine=engine, memory=memory
)

with st.chat_message("assistant"):
st_cb = StreamlitCallbackHandler(st.container(), expand_new_thoughts=True)
cfg = RunnableConfig()
cfg["callbacks"] = [st_cb]
response = sql_agent.invoke({"input": prompt}, cfg)
st.write(response["output"])

Probando el agente

Ahora que tenemos el agente armado. Probaremos algunas consultas a través de la app y el agente. Primero empezaremos por consultas sencillas, e iremos aumentando complejidad para evaluar la capacidad del agente de responder correctamente al problema.

Ejemplo 1:

Para este ejemplo, preguntaremos cuántas son las ventas totales de los supermercados

Aquí podemos apreciar la ventaja de langchain, que nos muestra detalladamente los diferentes pasos que sigue cada agente.

1. Revisa las tablas disponibles. En este caso es una sola, pero si la base de datos tuviera múltiples tablas le permitiría, por ejemplo, hacer consultas combinando tablas.

2. Para las tablas relevantes, extrae las columnas y formato (texto, decimal, entero, etc). Esto es relevante para entender qué operaciones son permitidas en cada columna al momento de ejecutar una consulta SQL.

3. Genera una query y valida que sea correcta.

4. Ejecuta la consulta a la base de datos y extrae el resultado

5. Imprime el resultado.

Estos pasos intermedios son útiles principalmente para realizar pruebas en la aplicación, pero también si se le quiere dar mayor información al usuario, que puede ayudar a corregir al agente.

Ejemplo 2:

Ahora probaremos una consulta un poco más compleja que requiere varios pasos y subconsultas.

La base de datos tiene transacciones con información horaria, y queremos saber cuál es la hora donde hay más transacciones para cada ciudad.

Esta consulta, primero requiere un primer paso que es contar las transacciones por hora para cada ciudad y luego, en una segunda consulta extraer el la hora donde ocurre el máximo.

Como referencia, según los datos la respuesta correcta es la siguiente:

Veamos los resultados:

Como se puede comparar con los datos, aquí la respuesta es incorrecta ya que el bot solo calculó el máximo de la columna Hora para cada ciudad, que son las 20:00 hs.

Pero esto se puede solucionar siendo un poco más específico en la consulta, e incluso dando indicios de los pasos a tomar al agente. Este procesamiento de refinamiento de las consultas que se le hace a un LLM suele ser llamado prompt engineering. Podemos ver que mejorando la calidad de la pregunta, los resultados pasan a ser correctos.

Ejemplo 3: aprovechando la memoria

Veamos un ejemplo muy simple de cómo se aprovecha la memoria de mensajes anteriores, evitando asi ejecutar una consulta SQL que puede ser larga y costosa en términos de computo (y además, se puede ver que aún cuando se tienen errores tipográficos en el prompt, el bot responde correctamente).

Haremos una re pregunta sobre el resultado anterior, para evaluar cuál es la ciudad con la hora con más actividad:

Como se puede ver, no sigue los pasos de razonamiento como en los ejemplos anteriores sino que, teniendo la respuesta anterior, el modelo de lenguaje resume el resultado anterior, lo cual hace que el cálculo se realice de forma mucho más rápida.

En conclusión, el uso de modelos generativos de inteligencia artificial, como GPT-4, junto con frameworks como Langchain, está transformando la manera en que las empresas pueden interactuar con sus bases de datos. Este enfoque permite a personas sin conocimientos técnicos en programación SQL extraer información y generar insights valiosos simplemente formulando preguntas en lenguaje natural. Al implementar una solución que traduce estas preguntas a consultas SQL, las organizaciones pueden mejorar significativamente su capacidad para tomar decisiones basadas en datos.

Por otra parte, un posible siguiente paso para completar mas aun este agente, sería integrar este agente con un agente que pueda extraer informacion de datos provenientes de, por ejemplo, texto, reportes, reuniones, conformando un sistema tipo RAG (Retrieval Augmented Generation).

Tambien, vimos un ejemplo práctico de cómo configurar un agente SQL utilizando Langchain y el modelo GPT-4, demostrando la eficacia de esta combinación para manejar consultas de bases de datos de forma eficiente. Además, al integrar este agente en una interfaz de chat con Streamlit, se logra una experiencia de usuario amigable y accesible, que mantiene la relevancia y consistencia de la información a través de la memoria de la conversación. En definitiva, esta innovación no solo facilita el proceso de consulta de datos, sino que también democratiza el acceso a información crucial dentro de una organización.

--

--

Santiago Elewaut
Data & AI Accenture Argentina

I’m a Data Scientist specializing in using data analytics and machine learning to solve complex supply chain and operations problems