Spaces:
Sleeping
Sleeping
File size: 9,972 Bytes
52b35ff d77f3f1 4f62127 d77f3f1 52b35ff d77f3f1 4f62127 d77f3f1 4f62127 d77f3f1 4f62127 d77f3f1 4f62127 d77f3f1 4f62127 d77f3f1 52b35ff 4f62127 d77f3f1 52b35ff d77f3f1 52b35ff | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 | """
AI Data Chatbot β Ask questions about your dataset, get tables and charts.
Usage: streamlit run app.py
"""
import os
import streamlit as st
import pandas as pd
from dotenv import load_dotenv
from utils.data_handler import (
load_dataframe,
get_schema_info,
get_sample_rows,
execute_pandas_code,
)
from utils.viz_handler import execute_viz_code, make_fallback_chart
from utils.llm_handler import LLMHandler
load_dotenv()
# Allow API key to be passed via query param or env (useful for HF Spaces)
_env_key = os.getenv("GEMINI_API_KEY", "")
# ββ Page config βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
st.set_page_config(
page_title="AI Data Chatbot",
page_icon="π",
layout="wide",
initial_sidebar_state="expanded",
)
st.markdown("""
<style>
.chat-header { font-size: 1.6rem; font-weight: 700; margin-bottom: 0.2rem; }
.stChatMessage { border-radius: 12px; }
div[data-testid="stSidebar"] { background-color: #f8f9fa; }
</style>
""", unsafe_allow_html=True)
# ββ Session state βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if "messages" not in st.session_state:
st.session_state.messages = [] # {role, content, result_df?, fig?, error?}
if "df" not in st.session_state:
st.session_state.df = None
if "llm" not in st.session_state:
st.session_state.llm = LLMHandler()
if "schema_info" not in st.session_state:
st.session_state.schema_info = ""
if "sample_rows" not in st.session_state:
st.session_state.sample_rows = ""
if "data_source" not in st.session_state:
st.session_state.data_source = ""
# ββ Sidebar βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
with st.sidebar:
st.markdown("## π AI Data Chatbot")
st.markdown("Ask questions about your data in plain English.")
st.divider()
# ββ API Key input ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
st.markdown("### π Gemini API Key")
if _env_key:
st.success("API key loaded from environment.", icon="β
")
api_key = _env_key
else:
api_key = st.text_input(
"Enter your Gemini API key",
type="password",
placeholder="AIza...",
help="Get a free key at aistudio.google.com/apikey",
)
if not api_key:
st.info("Enter your Gemini API key above to get started. Free at aistudio.google.com/apikey")
if api_key:
os.environ["GEMINI_API_KEY"] = api_key
st.divider()
# Data source selection
st.markdown("### Data Source")
data_option = st.radio(
"Choose data:",
["Use sample dataset (employees)", "Upload your own file"],
key="data_option",
)
if data_option == "Upload your own file":
uploaded = st.file_uploader(
"Upload CSV or Excel",
type=["csv", "xlsx", "xls"],
help="Max 200MB. Columns with year/date enable time comparisons.",
)
if uploaded:
df, err = load_dataframe(uploaded)
if err:
st.error(err)
elif df is not None:
st.session_state.df = df
st.session_state.schema_info = get_schema_info(df)
st.session_state.sample_rows = get_sample_rows(df)
st.session_state.data_source = uploaded.name
st.session_state.messages = []
st.session_state.llm.reset_conversation()
st.success(f"Loaded {len(df):,} rows Γ {len(df.columns)} columns")
else:
sample_path = "sample_data/employees.csv"
if os.path.exists(sample_path):
df = pd.read_csv(sample_path)
if st.session_state.data_source != "employees.csv":
st.session_state.df = df
st.session_state.schema_info = get_schema_info(df)
st.session_state.sample_rows = get_sample_rows(df)
st.session_state.data_source = "employees.csv"
st.session_state.messages = []
st.session_state.llm.reset_conversation()
st.success(f"Loaded {len(df):,} rows Γ {len(df.columns)} columns")
else:
st.error("Sample data not found. Run `python generate_sample_data.py` first.")
# Dataset preview
if st.session_state.df is not None:
st.divider()
st.markdown("### Dataset Preview")
with st.expander("Show schema"):
st.code(st.session_state.schema_info, language="text")
with st.expander("Show first 5 rows"):
st.dataframe(st.session_state.df.head(), use_container_width=True)
# Example queries
st.divider()
st.markdown("### Example Queries")
example_queries = [
"Compare 2022 vs 2023 highest paid employees by job title β show as chart and table",
"What is the average salary by department for each year?",
"Show the top 10 highest paid employees in 2023",
"Which department has the highest salary growth from 2021 to 2024?",
"Show salary distribution by location in 2023",
"How many employees are in each department?",
"Compare average salaries across job titles in Engineering in 2023",
]
for q in example_queries:
if st.button(q, key=f"ex_{q[:20]}", use_container_width=True):
st.session_state.pending_query = q
# Reset
st.divider()
if st.button("Clear conversation", use_container_width=True):
st.session_state.messages = []
st.session_state.llm.reset_conversation()
st.rerun()
# ββ Main chat area βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
st.markdown('<div class="chat-header">π AI Data Chatbot</div>', unsafe_allow_html=True)
st.markdown("Ask questions about your data β get tables **and** charts automatically.")
if not os.getenv("GEMINI_API_KEY"):
st.warning("β¬
οΈ Enter your Gemini API key in the sidebar to get started. Free at aistudio.google.com/apikey")
st.stop()
if st.session_state.df is None:
st.info("β¬
οΈ Load a dataset from the sidebar to get started.")
st.stop()
# Render existing conversation
for msg in st.session_state.messages:
with st.chat_message(msg["role"]):
st.markdown(msg["content"])
if msg["role"] == "assistant":
if msg.get("error"):
st.error(msg["error"])
if msg.get("result_df") is not None:
st.markdown("**Results Table**")
st.dataframe(msg["result_df"], use_container_width=True)
if msg.get("fig") is not None:
st.plotly_chart(msg["fig"], use_container_width=True)
def run_query(user_input: str):
"""Process a user query: call LLM, execute code, display results."""
if not user_input.strip():
return
# Add user message to history and display it
st.session_state.messages.append({"role": "user", "content": user_input})
with st.chat_message("user"):
st.markdown(user_input)
# Generate analysis via Claude
with st.chat_message("assistant"):
with st.spinner("Analyzing your data..."):
analysis = st.session_state.llm.generate_analysis(
question=user_input,
schema_info=st.session_state.schema_info,
sample_rows=st.session_state.sample_rows,
)
explanation = analysis.get("explanation", "")
pandas_code = analysis.get("pandas_code", "")
viz_code = analysis.get("viz_code")
viz_type = analysis.get("viz_type", "table_only")
# Show explanation
st.markdown(explanation)
result_df = None
fig = None
error_msg = None
# Execute pandas code
if pandas_code:
result_df, exec_error = execute_pandas_code(pandas_code, st.session_state.df)
if exec_error:
error_msg = f"Data processing error: {exec_error}"
st.error(error_msg)
elif result_df is not None:
st.markdown("**Results Table**")
st.dataframe(result_df, use_container_width=True)
# Execute viz code
if viz_code and viz_type not in ("table_only", "null", None):
fig, viz_error = execute_viz_code(viz_code, result_df)
if viz_error:
# Try fallback chart
fig = make_fallback_chart(result_df, title=user_input[:60])
if fig is None:
st.warning(f"Could not render chart: {viz_error}")
if fig is not None:
st.plotly_chart(fig, use_container_width=True)
# Store message in history
assistant_entry = {
"role": "assistant",
"content": explanation,
"result_df": result_df,
"fig": fig,
"error": error_msg,
}
st.session_state.messages.append(assistant_entry)
# Handle example query button clicks
if "pending_query" in st.session_state:
pending = st.session_state.pop("pending_query")
run_query(pending)
st.rerun()
# Chat input
if user_input := st.chat_input("Ask a question about your data..."):
run_query(user_input)
st.rerun()
|