diff --git a/014_ai_exploratory_copilot_app/app.py b/014_ai_exploratory_copilot_app/app.py
new file mode 100644
index 0000000..c55a97b
--- /dev/null
+++ b/014_ai_exploratory_copilot_app/app.py
@@ -0,0 +1,436 @@
+# BUSINESS SCIENCE
+# Exploratory Data Analysis (EDA) Copilot App
+# -----------------------
+
+# This app helps you search for data and produces exploratory analysis reports.
+
+# Imports
+# !pip install git+https://github.com/business-science/ai-data-science-team.git --upgrade
+
+from openai import OpenAI
+import streamlit as st
+import streamlit.components.v1 as components
+import pandas as pd
+from pathlib import Path
+import html
+
+from langchain_community.chat_message_histories import StreamlitChatMessageHistory
+from langchain_openai import ChatOpenAI
+
+from ai_data_science_team.ds_agents import EDAToolsAgent
+from ai_data_science_team.utils.matplotlib import matplotlib_from_base64
+from ai_data_science_team.utils.plotly import plotly_from_dict
+
+# =============================================================================
+# STREAMLIT APP SETUP (including data upload, API key, etc.)
+# =============================================================================
+
+MODEL_LIST = ['gpt-4o-mini', 'gpt-4o']
+TITLE = "Your Exploratory Data Analysis (EDA) Copilot"
+st.set_page_config(page_title=TITLE, page_icon="📊")
+st.title("📊 " + TITLE)
+
+st.markdown("""
+Welcome to the EDA Copilot. This AI agent is designed to help you find and load data
+and return exploratory analysis reports that can be used to understand the data
+prior to other analysis (e.g. modeling, feature engineering, etc).
+""")
+
+with st.expander("Example Questions", expanded=False):
+ st.write(
+ """
+ - What tools do you have access to? Return a table.
+ - Give me information on the correlation funnel tool.
+ - Explain the dataset.
+ - What do the first 5 rows contain?
+ - Describe the dataset.
+ - Analyze missing data in the dataset.
+ - Generate a correlation funnel. Use the Churn feature as the target.
+ - Generate a Sweetviz report for the dataset. Use the Churn feature as the target.
+ """
+ )
+
+# Sidebar for file upload / demo data
+st.sidebar.header("EDA Copilot: Data Upload/Selection", divider=True)
+st.sidebar.header("Upload Data (CSV or Excel)")
+use_demo_data = st.sidebar.checkbox("Use demo data", value=False)
+
+if "DATA_RAW" not in st.session_state:
+ st.session_state["DATA_RAW"] = None
+
+if use_demo_data:
+ demo_file_path = Path("data/churn_data.csv")
+ if demo_file_path.exists():
+ df = pd.read_csv(demo_file_path)
+ file_name = "churn_data"
+ st.session_state["DATA_RAW"] = df.copy()
+ st.write(f"## Preview of {file_name} data:")
+ st.dataframe(st.session_state["DATA_RAW"])
+ else:
+ st.error(f"Demo data file not found at {demo_file_path}. Please ensure it exists.")
+else:
+ uploaded_file = st.sidebar.file_uploader("Upload CSV or Excel file", type=["csv", "xlsx"])
+ if uploaded_file:
+ if uploaded_file.name.endswith('.csv'):
+ df = pd.read_csv(uploaded_file)
+ elif uploaded_file.name.endswith('.xlsx'):
+ df = pd.read_excel(uploaded_file)
+ st.session_state["DATA_RAW"] = df.copy()
+ file_name = Path(uploaded_file.name).stem
+ st.write(f"## Preview of {file_name} data:")
+ st.dataframe(st.session_state["DATA_RAW"])
+ else:
+ st.info("Please upload a CSV or Excel file or Use Demo Data to proceed.")
+
+# Sidebar: OpenAI API Key and Model Selection
+st.sidebar.header("Enter your OpenAI API Key")
+st.session_state["OPENAI_API_KEY"] = st.sidebar.text_input(
+ "API Key",
+ type="password",
+ help="Your OpenAI API key is required for the app to function."
+)
+
+if st.session_state["OPENAI_API_KEY"]:
+ client = OpenAI(api_key=st.session_state["OPENAI_API_KEY"])
+ try:
+ models = client.models.list()
+ st.success("API Key is valid!")
+ except Exception as e:
+ st.error(f"Invalid API Key: {e}")
+else:
+ st.info("Please enter your OpenAI API Key to proceed.")
+ st.stop()
+
+model_option = st.sidebar.selectbox("Choose OpenAI model", MODEL_LIST, index=0)
+OPENAI_LLM = ChatOpenAI(
+ model=model_option,
+ api_key=st.session_state["OPENAI_API_KEY"]
+)
+llm = OPENAI_LLM
+
+# =============================================================================
+# CHAT MESSAGE HISTORY AND ARTIFACT STORAGE
+# =============================================================================
+
+msgs = StreamlitChatMessageHistory(key="langchain_messages")
+if len(msgs.messages) == 0:
+ msgs.add_ai_message("How can I help you?")
+
+if "chat_artifacts" not in st.session_state:
+ st.session_state["chat_artifacts"] = {}
+
+def display_chat_history():
+ """
+ Renders the entire chat history along with any artifacts attached to messages.
+ Artifacts (e.g., plots, dataframes, Sweetviz reports) are rendered inside expanders.
+ """
+ for i, msg in enumerate(msgs.messages):
+ with st.chat_message(msg.type):
+ st.write(msg.content)
+ if "chat_artifacts" in st.session_state and i in st.session_state["chat_artifacts"]:
+ for artifact in st.session_state["chat_artifacts"][i]:
+ with st.expander(artifact["title"], expanded=True):
+ if artifact["render_type"] == "dataframe":
+ st.dataframe(artifact["data"])
+ elif artifact["render_type"] == "matplotlib":
+ st.pyplot(artifact["data"])
+ elif artifact["render_type"] == "plotly":
+ st.plotly_chart(artifact["data"])
+ elif artifact["render_type"] == "sweetviz":
+ report_file = artifact["data"].get("report_file")
+ try:
+ with open(report_file, "r", encoding="utf-8") as f:
+ report_html = f.read()
+ except Exception as e:
+ st.error(f"Could not open report file: {e}")
+ report_html = "