Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Langchain Pandas DataFrame Agent #83

Merged
merged 14 commits into from
Oct 23, 2023
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -171,4 +171,5 @@ docs/components.md
docs/features.md
docs/langchain.md
docs/mistral.md
docs/openai.md
docs/openai.md
docs/examples/langchain/penguins.csv
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/videos/langchain_chat_pandas_df.mp4
Binary file not shown.
209 changes: 209 additions & 0 deletions docs/examples/langchain/langchain_chat_pandas_df.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
"""
Demonstrates how to use the `ChatInterface` and `PanelCallbackHandler` to create a
chatbot to talk to your Pandas DataFrame. This is heavily inspired by the
[LangChain `chat_pandas_df` Reference Example](https://github.com/langchain-ai/streamlit-agent/blob/main/streamlit_agent/chat_pandas_df.py).
"""
from __future__ import annotations

MarcSkovMadsen marked this conversation as resolved.
Show resolved Hide resolved
from pathlib import Path
from textwrap import dedent

import pandas as pd
import panel as pn
import param
import requests
from langchain.agents import AgentType, create_pandas_dataframe_agent
from langchain.chat_models import ChatOpenAI

from panel_chat_examples import EnvironmentWidgetBase

pn.extension("perspective", design="material")

PENGUINS_URL = (
"https://raw.githubusercontent.com/mwaskom/seaborn-data/master/penguins.csv"
)
PENGUINS_PATH = Path(__file__).parent / "penguins.csv"
if not PENGUINS_PATH.exists():
response = requests.get(PENGUINS_URL)
PENGUINS_PATH.write_text(response.text)

FILE_DOWNLOAD_STYLE = """
.bk-btn a {
padding: 0px;
}
.bk-btn-group > button, .bk-input-group > button {
font-size: small;
}
"""


class Environment(EnvironmentWidgetBase):
"""Will be asking the user for the API Key if not set as environment variable"""

OPENAI_API_KEY = param.String()


class AgentConfig(param.Parameterized):
"""Configuration used for the Pandas Agent"""

user = param.String("Pandas Agent")
avatar = param.String("🐼")

show_chain_of_thought = param.Boolean(default=False)

def _get_agent_message(self, message: str) -> pn.chat.ChatMessage:
return pn.chat.ChatMessage(message, user=self.user, avatar=self.avatar)


class AppState(param.Parameterized):
data = param.DataFrame()

llm = param.Parameter(constant=True)
pandas_df_agent = param.Parameter(constant=True)

config: AgentConfig = param.ClassSelector(class_=AgentConfig)
environ: Environment = param.ClassSelector(class_=Environment, constant=True)

def __init__(
self, config: AgentConfig | None = None, environ: Environment | None = None
):
if not config:
config = AgentConfig()
if not environ:
environ = Environment()
super().__init__(config=config, environ=environ)

@param.depends("environ.OPENAI_API_KEY", on_init=True, watch=True)
def _reset_llm(self):
with param.edit_constant(self):
if self.environ.OPENAI_API_KEY:
self.llm = ChatOpenAI(
temperature=0,
model="gpt-3.5-turbo-0613",
api_key=self.environ.OPENAI_API_KEY,
streaming=True,
)
else:
self.llm = None

@param.depends("llm", "data", on_init=True, watch=True)
def _reset_pandas_df_agent(self):
with param.edit_constant(self):
if not self.error_message:
self.pandas_df_agent = create_pandas_dataframe_agent(
self.llm,
self.data,
verbose=True,
agent_type=AgentType.OPENAI_FUNCTIONS,
handle_parsing_errors=True,
)
else:
self.pandas_df_agent = None

@property
def error_message(self):
if not self.llm and self.data is None:
return dedent(
"""\
Please provide your `OPENAI_API_KEY`, **upload a `.csv` file**
and click the **send** button."""
)
if not self.llm:
return "Please provide your `OPENAI_API_KEY`."
if self.data is None:
return "Please **upload a `.csv` file** and click the **send** button."
return ""

@property
def welcome_message(self):
return dedent(
f"""
I'm your <a href="\
https://python.langchain.com/docs/integrations/toolkits/pandas" \
target="_blank">LangChain Pandas DataFrame Agent</a>.

I execute LLM generated Python code under the hood - this can be bad if
the `llm` generated Python code is harmful. Use cautiously!

{self.error_message}"""
).strip()

async def callback(self, contents, user, instance):
if isinstance(contents, pd.DataFrame):
self.data = contents
instance.active = 1
message = self.config._get_agent_message(
dedent(
"""You can ask me anything about the data. For example 'how many
species are there?'"""
)
)
# We `send` instead of just `return` due to the bug
# https://github.com/holoviz/panel/issues/5708
instance.send(message, respond=False)
return # message
ahuang11 marked this conversation as resolved.
Show resolved Hide resolved

if self.error_message:
message = self.config._get_agent_message(self.error_message)
instance.send(message, respond=False)
return # message
ahuang11 marked this conversation as resolved.
Show resolved Hide resolved

if self.config.show_chain_of_thought:
langchain_callbacks = [
pn.chat.langchain.PanelCallbackHandler(instance=instance)
]
else:
langchain_callbacks = []
response = await state.pandas_df_agent.arun(
contents, callbacks=langchain_callbacks
)
message = self.config._get_agent_message(response)
instance.send(message, respond=False)
return


state = AppState()

chat_interface = pn.chat.ChatInterface(
widgets=[
pn.widgets.FileInput(name="Upload"),
pn.widgets.TextInput(name="Message", placeholder="Send a message"),
],
renderers=pn.pane.Perspective,
callback=state.callback,
callback_exception="verbose",
show_rerun=False,
show_undo=False,
show_clear=False,
min_height=400,
)
chat_interface.send(
state.welcome_message,
user=state.config.user,
avatar=state.config.avatar,
respond=False,
)

download_button = pn.widgets.FileDownload(
PENGUINS_PATH,
button_type="primary",
button_style="outline",
height=30,
width=335,
stylesheets=[FILE_DOWNLOAD_STYLE],
)

layout = pn.template.MaterialTemplate(
title="🦜 LangChain - Chat with Pandas DataFrame",
main=[chat_interface],
sidebar=[
download_button,
"#### Agent Settings",
state.config.param.show_chain_of_thought,
],
)

if state.environ.variables_not_set:
layout.sidebar.append(state.environ)

layout.servable()
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ dependencies = [
"openai",
"panel @ git+https://github.com/holoviz/panel.git@main",
"pypdf",
"tabulate",
MarcSkovMadsen marked this conversation as resolved.
Show resolved Hide resolved
"tiktoken",
]

Expand Down
Loading
Loading