File size: 19,236 Bytes
278ad21
 
 
 
 
 
 
 
3b695d1
 
bbbf3e0
801ee8d
 
 
 
 
 
 
 
 
3871fc1
801ee8d
3871fc1
801ee8d
3871fc1
 
801ee8d
3871fc1
801ee8d
3871fc1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
801ee8d
278ad21
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56ddc54
278ad21
ad9661b
84caa9d
 
 
 
 
 
 
ad9661b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
278ad21
 
 
 
 
7ccc3ef
278ad21
12ec701
538dbed
a9ac8a0
538dbed
a9ac8a0
12ec701
538dbed
 
 
12ec701
 
538dbed
12ec701
 
 
87fe0b2
 
 
 
12ec701
 
 
7ccc3ef
12ec701
 
 
7ccc3ef
12ec701
 
 
 
 
7ccc3ef
12ec701
 
 
 
 
 
accafcf
 
 
12ec701
7ccc3ef
 
 
 
 
 
 
 
 
 
 
 
 
12ec701
7ccc3ef
 
12ec701
7ccc3ef
 
 
 
 
 
12ec701
 
e991c1e
 
 
 
 
 
 
 
 
 
 
 
 
12ec701
 
e991c1e
12ec701
e991c1e
12ec701
 
7ccc3ef
12ec701
 
 
 
 
7ccc3ef
 
12ec701
 
 
 
 
 
 
 
 
 
7ccc3ef
5a0f100
 
 
 
 
12ec701
 
5a0f100
12ec701
 
5a0f100
12ec701
5a0f100
 
 
 
12ec701
 
 
 
233eddf
 
7ccc3ef
 
 
233eddf
 
 
12ec701
 
 
 
233eddf
 
12ec701
233eddf
12ec701
233eddf
 
12ec701
 
 
 
 
 
233eddf
e991c1e
12ec701
e991c1e
12ec701
 
 
 
 
3cb357d
538dbed
3cb357d
278ad21
0ba95bc
801ee8d
7ccc3ef
278ad21
 
 
7ccc3ef
56ddc54
801ee8d
 
5fbb944
e991c1e
801ee8d
 
 
e991c1e
 
 
 
 
5fbb944
e991c1e
 
 
 
 
 
 
 
 
 
5fbb944
 
801ee8d
 
e991c1e
 
 
 
 
 
5fbb944
 
e991c1e
 
 
 
5fbb944
 
 
 
 
801ee8d
7ccc3ef
278ad21
7ccc3ef
278ad21
7ccc3ef
 
 
 
 
 
 
278ad21
9466e2d
278ad21
 
7ccc3ef
278ad21
 
 
 
 
 
7ccc3ef
278ad21
 
 
 
801ee8d
 
278ad21
 
 
7ccc3ef
278ad21
 
 
7ccc3ef
278ad21
 
 
7ccc3ef
 
 
 
 
 
 
 
 
278ad21
9466e2d
278ad21
7ccc3ef
278ad21
 
 
 
 
801ee8d
278ad21
 
 
 
 
 
 
 
 
 
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
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
import gradio as gr
import pandas as pd
import matplotlib.pyplot as plt
import re
from io import BytesIO
from PIL import Image
from transformers import pipeline
import time
from sklearn.linear_model import LinearRegression
import numpy as np
import os

# === Vector Memory Enhancements ===
from sentence_transformers import SentenceTransformer

embedding_model = SentenceTransformer("all-MiniLM-L6-v2")
prompt_history = []  # Stores {"company": str, "question": str, "embedding": np.array}

last_uploaded_company = None

def get_company_name(df, file=None):
    try:
        # Try name column first
        possible_names = df["name"].dropna().unique()
        if len(possible_names) > 0:
            return possible_names[0]
    except Exception:
        pass

    # Try fallback to unique CIK
    try:
        if "cik" in df.columns:
            cik = str(df["cik"].unique()[0])
            cik_map = {
                "320193": "Apple",
                "1018724": "Amazon",
                "1652044": "Google"
            }
            return cik_map.get(cik.zfill(7), f"CIK_{cik}")
    except Exception:
        pass

    # Fallback to filename
    if file:
        return os.path.splitext(os.path.basename(file.name))[0]

    return "Unknown"

# Load TAPAS pipeline
table_qa = pipeline("table-question-answering", model="google/tapas-large-finetuned-wtq")

global_df = None

keywords = ["Revenue", "Sales", "NetIncome", "NetIncomeLoss", "Cash", "Assets", "Liabilities"]

def generate_chart(df):
    try:
        df["ddate"] = pd.to_datetime(df["ddate"], format="%Y%m%d", errors="coerce")
        mask = df["tag"].astype(str).str.contains('|'.join(keywords), case=False, na=False) |                df["tlabel"].astype(str).str.contains('|'.join(keywords), case=False, na=False)
        core = df[mask].copy()
        core = core[core["value"].notnull()]
        core["metric"] = core["tlabel"].fillna(core["tag"]).astype(str).str.extract(
            r"(Revenue|Sales|Net Income|Cash.*?|Assets|Liabilities)",
            expand=False,
            flags=re.IGNORECASE
        ).str.strip().fillna("Other")
        summary = core.pivot_table(index="ddate", columns="metric", values="value", aggfunc="sum").sort_index()
        plt.figure(figsize=(12, 6))
        summary.plot(marker='o')
        plt.title("Financial Metrics Over Time")
        plt.ylabel("USD ($)")
        plt.grid(True)
        plt.tight_layout()
        buf = BytesIO()
        plt.savefig(buf, format="png")
        plt.close()
        buf.seek(0)
        return Image.open(buf)
    except Exception as e:
        print("Chart generation error:", e)
        return None

def plot_assets_by_quarter_last_two_years():
    try:
        df = global_df.copy()
        df["ddate"] = pd.to_datetime(df["ddate"], format="%Y%m%d", errors="coerce")
        df = df[df["ddate"].notnull()]
        latest_date = df["ddate"].max()
        cutoff_date = latest_date - pd.DateOffset(years=2)
        recent_df = df[df["ddate"] >= cutoff_date]
        asset_mask = recent_df["tlabel"].astype(str).str.contains("asset", case=False, na=False)
        asset_data = recent_df[asset_mask].copy()
        asset_data = asset_data[asset_data["value"].notnull()]
        asset_data["quarter"] = asset_data["ddate"].dt.to_period("Q")
        summary = asset_data.groupby("quarter")["value"].sum().sort_index()
        summary.index = summary.index.astype(str).str.replace("Q", " Q")
        plt.figure(figsize=(10, 5))
        summary.plot(kind="bar")
        plt.title("Total Assets by Quarter (Last 2 Years)")
        plt.ylabel("USD ($)")
        plt.xlabel("Quarter")
        plt.grid(True)
        plt.tight_layout()
        buf = BytesIO()
        plt.savefig(buf, format="png")
        plt.close()
        buf.seek(0)
        return Image.open(buf)
    except Exception as e:
        print("Custom assets plot error:", e)
        return None

def upload_csv(file, history):
    global global_df, uploaded_filename, last_uploaded_company
    try:
        uploaded_filename = os.path.splitext(os.path.basename(file.name))[0]
        if "apple" in uploaded_filename:
            last_uploaded_company = "apple"
        elif "amazon" in uploaded_filename:
            last_uploaded_company = "amazon"
        else:
            last_uploaded_company = uploaded_filename
            
        df = pd.read_csv(file.name)

        # Validate necessary columns
        required_columns = {"ddate", "value", "tlabel", "tag"}
        missing = required_columns - set(df.columns)
        if missing:
            history.append((None, f"Missing required columns: {', '.join(missing)}"))
            return history

        df["value"] = (
            df["value"]
            .astype(str)
            .str.replace(r"[^\d.\-]", "", regex=True)
            .replace("", pd.NA)
        )
        df = df.dropna(subset=["value"])
        df["value"] = df["value"].astype(float)

        df["ddate"] = pd.to_datetime(df["ddate"], format="%Y%m%d", errors="coerce")

        global_df = df
        history.append((None, f"File uploaded successfully for {uploaded_filename}. Ask me anything about it!"))
        return history
    except Exception as e:
        history.append((None, f"Failed to load CSV: {str(e)}"))
        return history


def handle_custom_question(question):
    global global_df

    if global_df is None:
        return "No data loaded yet.", None

    df = global_df.copy()

    df["ddate"] = pd.to_datetime(df["ddate"], errors='coerce')
    df = df.dropna(subset=["ddate"])
    df["quarter"] = df["ddate"].dt.to_period("Q")
    df["year"] = df["ddate"].dt.year

    q = question.lower()

    def to_img(fig):
        buf = BytesIO()
        plt.savefig(buf, format="png")
        plt.close()
        buf.seek(0)
        return Image.open(buf)

    if "average revenue" in q and "2025" in q:
        avg = df[(df["year"] == 2025) & df["tlabel"].astype(str).str.contains("revenue", case=False, na=False)]["value"].mean()
        return f"The average revenue for 2025 so far is ${avg:,.2f}.", None

    if "net income change" in q and "2020" in q and "2025" in q:
        mask = df["tlabel"].astype(str).str.contains("net income", case=False, na=False)
        net_income_by_year = df[mask].groupby("year")["value"].sum()
        change = net_income_by_year.get(2025, 0) - net_income_by_year.get(2020, 0)
        return f"Net income changed by ${change:,.2f} from 2020 to 2025.", None

    if "least amount of liabilities" in q:
        mask = df["tlabel"].astype(str).str.contains("liabilit", case=False, na=False)
        liabilities_by_year = df[mask].groupby("year")["value"].sum()
        if liabilities_by_year.empty:
            return "No liability data found in the dataset.", None
        lowest_year = liabilities_by_year.idxmin()
        lowest_value = liabilities_by_year.min()
        return f"The year with the least amount of liabilities was {lowest_year} with ${lowest_value:,.2f}.", None
    if "asset" in q and ("graph" in q or "plot" in q or "quarter" in q):
        image = plot_assets_by_quarter_last_two_years()
        return "Here is the total assets by quarter for the last 2 years.", image

    # === Free Cash Flow trend (last 8 quarters) ===
    if "free cash flow" in question.lower() or "fcf" in question.lower():
        fcf = df[df["tlabel"].astype(str).str.contains("free cash flow", case=False, na=False)]
        if fcf.empty:
            op = df[df["tlabel"].str.contains("net cash.*operating", case=False, na=False)]
            capex = df[df["tlabel"].str.contains("acquire.*property|capital expenditure", case=False, na=False)]
            op_group = op.groupby(df["ddate"].dt.to_period("Q"))["value"].sum()
            capex_group = capex.groupby(df["ddate"].dt.to_period("Q"))["value"].sum()
            fcf = (op_group - capex_group).dropna()
        else:
            fcf = fcf.groupby(df["ddate"].dt.to_period("Q"))["value"].sum()

        fcf = fcf.sort_index().iloc[-8:]  # last 8 quarters
        fig, ax = plt.subplots()
        fcf.plot(kind="line", marker='o', ax=ax)
        ax.set_title("Free Cash Flow Trend (Last 8 Quarters)")
        ax.set_ylabel("USD ($)")
        ax.grid(True)
        buf = BytesIO()
        plt.savefig(buf, format="png")
        plt.close()
        buf.seek(0)
        return "Free cash flow trend over the past 8 quarters", Image.open(buf)

    if "r&d" in q or "research and development" in q:
        # Look for both label AND tag variants (Amazon often uses Technology and Content)
        rnd_mask = (
            df["tlabel"].astype(str).str.contains("research and development|r&d|technology and content", case=False, na=False)
            | df["tag"].astype(str).str.contains("Research.*Development|TechnologyAndContent", case=False, na=False)
        )
        r_and_d = df[rnd_mask]
        if r_and_d.empty:
            return "No R&D-like lines found in this dataset (looked for Research and Development / R&D / Technology and Content).", None
    
        summary = r_and_d.groupby("quarter")["value"].sum().sort_index()
        if summary.empty or summary.dropna().empty:
            return "R&D series is empty after grouping—nothing to plot for this file.", None
        
        fig, ax = plt.subplots()
        summary.plot(kind="bar", ax=ax)
        ax.set_title("R&D (or Technology & Content) Spending per Quarter")
        ax.set_ylabel("USD ($)")
        return "R&D (or Technology & Content) spending trend", to_img(fig)

    if "share buyback" in q or "repurchase" in q:
        mask = df["tlabel"].astype(str).str.contains("repurchase of common stock", case=False, na=False)
        summary = df[mask].groupby("year")["value"].sum()
        response = "\n".join([f"{yr}: ${val:,.2f}" for yr, val in summary.items()])
        return f"Apple's share buyback per year:\n{response}", None

    if "gross margin" in q:
        rev = df[df["tlabel"].astype(str).str.contains("revenue", case=False, na=False)]
        cogs = df[df["tlabel"].astype(str).str.contains("cost of goods", case=False, na=False)]
        rev_group = rev.groupby("quarter")["value"].sum()
        cogs_group = cogs.groupby("quarter")["value"].sum()
        gross_margin = ((rev_group - cogs_group) / rev_group * 100).dropna()
        fig, ax = plt.subplots()
        gross_margin.plot(kind="bar", ax=ax)
        ax.set_title("Gross Margin % by Quarter")
        ax.set_ylabel("Percentage (%)")
        return "Gross margin percentage by quarter", to_img(fig)

    if "forecast" in q and "revenue" in q:
        rev = df[df["tlabel"].astype(str).str.contains("revenue", case=False, na=False)]
        rev_group = rev.groupby("quarter")["value"].sum().sort_index()
        if rev_group.shape[0] < 4:
            return "Not enough data to forecast revenue.", None
        X = np.arange(len(rev_group)).reshape(-1, 1)
        y = rev_group.values
        model = LinearRegression().fit(X, y)
        X_future = np.arange(len(rev_group), len(rev_group) + 2).reshape(-1, 1)
        y_future = model.predict(X_future)
        forecast_index = pd.period_range(start=rev_group.index[-1] + 1, periods=2, freq="Q")
        forecast_series = pd.Series(y_future, index=forecast_index)
        full_series = pd.concat([rev_group, forecast_series])
        fig, ax = plt.subplots()
        full_series.plot(marker='o', ax=ax)
        ax.axvline(x=rev_group.index[-1], color='gray', linestyle='--')
        ax.set_title("Actual & Forecasted Revenue (Next 2 Quarters)")
        ax.set_ylabel("USD ($)")
        return "Here is the forecasted revenue for the next 2 quarters.", to_img(fig)

    if "financial health" in q:
        latest_year = df["year"].max()
    
        # Filter relevant rows
        revenue = df[df["tlabel"].astype(str).str.contains("revenue", case=False, na=False)]
        net_income = df[df["tag"].astype(str).isin(["NetIncomeLoss", "NetIncome"])]
        assets = df[df["tag"].astype(str).str.contains("Assets", case=False, na=False)]
        equity = df[df["tag"].astype(str).str.contains("Equity", case=False, na=False) & ~df["tag"].astype(str).str.contains("Liabilities", case=False, na=False)]
    
        # Get latest values
        rev_latest = revenue[revenue["year"] == latest_year]["value"].sum()
        net_income_latest = net_income[net_income["year"] == latest_year]["value"].sum()
        assets_latest = assets[assets["year"] == latest_year]["value"].sum()
        equity_latest = equity[equity["year"] == latest_year]["value"].sum()
    
        # Calculate metrics
        profit_margin = (net_income_latest / rev_latest * 100) if rev_latest > 0 else 0
        debt_to_equity = ((assets_latest - equity_latest) / equity_latest) if equity_latest > 0 else 0
        roa = (net_income_latest / assets_latest * 100) if assets_latest > 0 else 0
    
        # Classification
        if profit_margin > 15 and debt_to_equity < 1 and roa > 5:
            health = "Healthy"
        elif profit_margin > 5 and debt_to_equity < 2:
            health = "Watchlist"
        else:
            health = "Risky"
    
        company_label = (last_uploaded_company or "This company").title()
        return (
            f"For {latest_year}, {company_label} is classified as **{health}** based on:\n"
            f"- Profit Margin: {profit_margin:.2f}%\n"
            f"- Debt-to-Equity: {debt_to_equity:.2f}\n"
            f"- Return on Assets (ROA): {roa:.2f}%",
            None
        )

    return "No custom handler available for this question.", None


def ask_question(history, question):
    global global_df, last_uploaded_company, prompt_history

    if global_df is None:
        return history + [(question, "Please upload a CSV file first.")], gr.update()

    # === Check for reuse pattern (this must come BEFORE appending current question to history) ===
    if "similar analysis" in question.lower() and (last_uploaded_company or "").lower() != "apple":
        similar_prompts = [item for item in prompt_history if item["company"].lower() == "apple"]
        if not similar_prompts:
            return history + [(question, "No Apple prompts stored to reference.")], gr.update(value=None)
    
        query_embedding = embedding_model.encode(question)
        scores = [np.dot(query_embedding, item["embedding"]) for item in similar_prompts]
        top_idxs = np.argsort(scores)[::-1][:3]
    
        def rewrite_company(text: str) -> str:
            company = (last_uploaded_company or "this company").title()
            return text.replace("Apple's", f"{company}'s").replace("Apple", company)
    
        df = global_df
        def dataset_supports(prompt: str) -> bool:
            q = prompt.lower()
            if "research and development" in q or "r&d" in q:
                has_rnd = (
                    df["tlabel"].astype(str).str.contains("research and development|r&d|technology and content", case=False, na=False).any()
                    or df["tag"].astype(str).str.contains("Research.*Development|TechnologyAndContent", case=False, na=False).any()
                )
                return bool(has_rnd)
            return True
    
        image_to_show = None  # <— NEW
    
        for i in top_idxs:
            past_q = similar_prompts[i]["question"]
            if not dataset_supports(past_q):
                history.append((f"Skipped: {past_q}", f"This file doesn’t have the needed rows to run that analysis on {last_uploaded_company.title()}."))            
                continue
            try:
                resp, img = handle_custom_question(rewrite_company(past_q))
                history.append((f"Reused: {rewrite_company(past_q)}", resp))
                if img is not None:
                    image_to_show = img  # <— capture latest non-None image
            except Exception as e:
                history.append((f"Skipped (error): {past_q}", f"Couldn’t run this on {last_uploaded_company.title()}: {e}"))
                continue
    
        # Return the captured image if any; otherwise clear the image pane
        if image_to_show is not None:
            return history, image_to_show
        else:
            return history, gr.update(value=None)

    # === Handle current question ===
    custom_response, custom_image = handle_custom_question(question)

    if custom_response is not None:
        if embedding_model:
            embedding = embedding_model.encode(question)
            prompt_history.append({
                "company": last_uploaded_company or "Unknown",
                "question": question,
                "embedding": embedding
            })
        time.sleep(2)
        print("Current prompt history:", prompt_history)
        return history + [(question, custom_response)], custom_image if custom_image else gr.update()

    # === Fallback to generic plot if question mentions plot/graph/chart ===
    if "plot" in question.lower() or "graph" in question.lower() or "chart" in question.lower():
        image = generate_chart(global_df)
        time.sleep(2)
        response = "Here is the generated chart based on your data."
        return history + [(question, response)], image

    # === Try table-qa as final fallback ===
    try:
        lower_q = question.lower()
        matched_keywords = [k.lower() for k in keywords if k.lower() in lower_q]
        if matched_keywords:
            mask = global_df["tag"].astype(str).str.contains('|'.join(keywords), case=False, na=False) | \
                   global_df["tlabel"].astype(str).str.contains('|'.join(keywords), case=False, na=False)
            df_filtered = global_df[mask].head(50).copy()
        else:
            df_filtered = global_df.head(50).copy()

        for col in df_filtered.columns:
            if df_filtered[col].dtype == "object":
                df_filtered[col] = df_filtered[col].astype(str)

        string_table = df_filtered.to_dict(orient="records")
        result = table_qa(table=string_table, query=question)
        answer = result["answer"]

        if embedding_model:
            embedding = embedding_model.encode(question)
            prompt_history.append({
                "company": last_uploaded_company or "Unknown",
                "question": question,
                "embedding": embedding
            })

        time.sleep(2)
        print("Current prompt history:", prompt_history)
        return history + [(question, answer)], gr.update()

    except Exception as e:
        time.sleep(2)
        return history + [(question, f"Error answering: {str(e)}")], gr.update()

with gr.Blocks() as demo:
    gr.Markdown("Financial Document Chatbot with TAPAS + Plotting")
    with gr.Row():
        file_input = gr.File(label="Upload CSV")
        upload_btn = gr.Button("Upload")
        chatbot = gr.Chatbot(label="Chatbot Conversation", value=[(None, "Hi! How can I help you today?")])
    question_input = gr.Textbox(label="Ask a question")
    ask_btn = gr.Button("Ask")
    chart_output = gr.Image(label="Generated Chart", type="pil")
    upload_btn.click(upload_csv, inputs=[file_input, chatbot], outputs=chatbot)
    ask_btn.click(ask_question, inputs=[chatbot, question_input], outputs=[chatbot, chart_output])
demo.launch(server_name="0.0.0.0", server_port=7860)