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()