File size: 12,754 Bytes
a90e58e
c5cc948
 
a90e58e
 
c5cc948
 
 
 
 
 
a90e58e
 
c5cc948
a90e58e
 
 
 
 
 
 
 
 
 
 
c5cc948
a90e58e
c5cc948
a90e58e
 
 
c5cc948
a90e58e
 
c5cc948
 
a90e58e
 
 
 
 
c5cc948
a90e58e
 
 
 
 
c5cc948
a90e58e
 
c5cc948
a90e58e
 
 
 
 
 
 
c5cc948
a90e58e
c5cc948
 
 
 
a90e58e
 
 
 
 
 
 
 
 
 
c5cc948
 
a90e58e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c5cc948
a90e58e
 
 
c5cc948
a90e58e
 
 
 
 
 
 
 
 
 
 
 
c5cc948
a90e58e
 
 
c5cc948
a90e58e
 
c5cc948
a90e58e
 
 
 
 
 
 
 
 
 
 
 
 
 
c5cc948
a90e58e
 
 
 
 
 
c5cc948
a90e58e
c5cc948
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a90e58e
c5cc948
 
 
 
 
 
 
 
a90e58e
c5cc948
 
 
 
 
 
 
 
 
a90e58e
c5cc948
 
 
 
 
 
 
 
 
 
 
 
 
a90e58e
c5cc948
 
 
 
 
 
 
 
 
 
a90e58e
c5cc948
 
 
a90e58e
 
c5cc948
 
 
 
 
 
 
 
 
 
 
 
 
a90e58e
c5cc948
 
 
 
 
 
 
 
 
a90e58e
 
c5cc948
a90e58e
 
c5cc948
 
 
 
 
 
 
a90e58e
c5cc948
 
a90e58e
c5cc948
 
 
 
 
 
a90e58e
c5cc948
 
 
 
 
a90e58e
c5cc948
 
 
 
a90e58e
 
c5cc948
 
 
 
a90e58e
 
 
c5cc948
a90e58e
c5cc948
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a90e58e
c5cc948
 
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
import os
from collections import Counter
from typing import List, Dict, Tuple

import gradio as gr
import pandas as pd

try:
    from huggingface_hub import InferenceClient
except ImportError:
    InferenceClient = None

# -----------------------------------------------------
# DATA LOADING (same logic as before, no UI changes)
# -----------------------------------------------------
DATA_CANDIDATES = [
    "Backlog_updated.xlsx",
    "Backlog_updated.xls",
    "data/Backlog_updated.xlsx",
    "data/Backlog_updated.xls",
]

SO_COL = "SALES_ORDER_NO"
CUSTOMER_COL = "CUSTOMER NAME"
WORTH_COL = "EXTENDED_RESALE"
DATE_COL = "ATP_DATE_RAW"

BLOCK_COLUMNS = ["CREDIT BLOCK", "DELIVERY BLOCK", "PRICING BLOCK", "SHIP DEBIT BLOCK"]
BLOCK_MAP = {
    "CREDIT BLOCK": "Credit",
    "DELIVERY BLOCK": "Delivery",
    "PRICING BLOCK": "Pricing",
    "SHIP DEBIT BLOCK": "Ship Debit",
}
BLOCKS_ORDER = ["Pricing", "Credit", "Delivery", "Ship Debit"]


def find_data_file() -> str:
    for p in DATA_CANDIDATES:
        if os.path.exists(p):
            return p
    raise FileNotFoundError(f"Could not find backlog file in {DATA_CANDIDATES}")


def load_raw_df(path: str) -> pd.DataFrame:
    df = pd.read_excel(path)
    df.columns = df.columns.str.strip().str.upper()
    needed = [SO_COL, CUSTOMER_COL, WORTH_COL, DATE_COL, *BLOCK_COLUMNS]
    missing = [c for c in needed if c not in df.columns]
    if missing:
        raise KeyError(f"Missing required columns in Excel: {missing}")
    return df


DATA_PATH = find_data_file()
RAW_DF = load_raw_df(DATA_PATH)

# -----------------------------------------------------
# ANALYTICS HELPERS (same computations as Flask app)
# -----------------------------------------------------
def fmt_money(x: float) -> str:
    return "${:,.0f}".format(float(x or 0))


def block_summary_df() -> pd.DataFrame:
    tmp = RAW_DF.copy()
    tmp["BLOCK_TYPE"] = tmp[BLOCK_COLUMNS].apply(
        lambda row: " + ".join(
            [BLOCK_MAP[c] for c in BLOCK_COLUMNS if str(row[c]).upper() == "X"]
        )
        or "No Block",
        axis=1,
    )
    tmp = tmp[tmp["BLOCK_TYPE"] != "No Block"]
    if tmp.empty:
        return pd.DataFrame(columns=["BLOCK_TYPE", WORTH_COL])
    agg = (
        tmp.groupby("BLOCK_TYPE", as_index=False)[WORTH_COL]
        .sum()
        .sort_values(WORTH_COL, ascending=False)
    )
    return agg


def actionable_focus_df():
    bs = block_summary_df()
    if bs.empty:
        return pd.DataFrame(), "No actionable focus area data."
    total_val = bs[WORTH_COL].sum()
    bs["SHARE_%"] = (bs[WORTH_COL] / total_val) * 100
    top2 = bs.head(2)
    blocks = ", ".join(top2["BLOCK_TYPE"])
    insight = f"{blocks} contribute {top2['SHARE_%'].sum():.1f}% of total blocked value."
    return top2, insight


def multiple_block_df() -> pd.DataFrame:
    tmp = RAW_DF.copy()
    tmp["BLOCK_COUNT"] = tmp[BLOCK_COLUMNS].apply(
        lambda row: sum(str(x).upper() == "X" for x in row), axis=1
    )
    multiblocks = tmp[tmp["BLOCK_COUNT"] > 1][
        [SO_COL, CUSTOMER_COL, WORTH_COL, "BLOCK_COUNT"]
    ].copy()
    return multiblocks


# Fiscal year helper: FY runs Apr–Mar, FY = year it ENDS (like we discussed)
def _cb_fiscal_year_quarter(dt: pd.Timestamp, start_month: int = 4) -> Tuple[int, int]:
    if pd.isna(dt):
        return 0, 0
    m, y = dt.month, dt.year
    shifted = ((m - start_month) % 12) + 1
    fq = ((shifted - 1) // 3) + 1
    fy = y if m < start_month else y + 1
    return fy, fq


def quarter_trends_df() -> pd.DataFrame:
    temp = RAW_DF.copy()
    temp[DATE_COL] = pd.to_datetime(temp[DATE_COL], errors="coerce")

    recs = []
    for b in BLOCK_COLUMNS:
        subset = temp[temp[b].astype(str).str.upper().eq("X")].dropna(subset=[DATE_COL])
        if subset.empty:
            continue
        fy_fq = subset[DATE_COL].apply(
            lambda d: _cb_fiscal_year_quarter(d, start_month=4)
        )
        subset = subset.assign(FY=[t[0] for t in fy_fq], FQ=[t[1] for t in fy_fq])

        grouped = subset.groupby(["FY", "FQ"], as_index=False)[WORTH_COL].sum()
        grouped["BLOCK_TYPE"] = BLOCK_MAP[b]
        grouped["QUARTER"] = (
            grouped["FY"].astype(int).astype(str)
            + "Q"
            + grouped["FQ"].astype(int).astype(str)
        )
        recs.append(grouped[["QUARTER", "BLOCK_TYPE", WORTH_COL, "FY", "FQ"]])

    if not recs:
        return pd.DataFrame(columns=["QUARTER", "BLOCK_TYPE", WORTH_COL])
    qdf = pd.concat(recs, ignore_index=True)
    qdf = qdf.sort_values(["FY", "FQ"]).drop(columns=["FY", "FQ"])
    return qdf


# -----------------------------------------------------
# CHAT LOGIC (same rules as earlier Flask chatbot)
# -----------------------------------------------------
def analytics_reply(user_q: str) -> str:
    q = user_q.lower().strip()
    reply = "I can help with blocked orders, customers, and quarterly trends."

    if "customer" in q:
        cust_summary = (
            RAW_DF.groupby(CUSTOMER_COL)[WORTH_COL]
            .sum()
            .sort_values(ascending=False)
            .reset_index()
        )
        if cust_summary.empty:
            return "I couldn't find any customers with backlog in the file."
        top = cust_summary.iloc[0]
        cust_name, value = top[CUSTOMER_COL], top[WORTH_COL]
        block_counts = (
            RAW_DF[RAW_DF[CUSTOMER_COL] == cust_name][BLOCK_COLUMNS]
            .apply(
                lambda row: [
                    BLOCK_MAP[c]
                    for c in BLOCK_COLUMNS
                    if str(row[c]).upper() == "X"
                ],
                axis=1,
            )
            .explode()
            .value_counts()
        )
        main_block = block_counts.index[0] if not block_counts.empty else "general"
        reply = (
            f"{cust_name} currently holds the highest backlog value ({fmt_money(value)}), "
            f"with **{main_block}** as the main issue. Coordinate with that team to resolve it."
        )

    elif "multiple" in q or "two blocks" in q or "more than one" in q:
        mb = multiple_block_df()
        if mb.empty:
            reply = "No orders have multiple active blocks right now."
        else:
            reply = (
                f"{mb[SO_COL].nunique()} sales orders have **multiple active blocks**. "
                f"Prioritize clearing these to restore smoother flow in the backlog."
            )

    elif "quarter" in q or "trend" in q:
        qdf = quarter_trends_df()
        if qdf.empty:
            reply = "No quarterly trend data available in the file."
        else:
            # Pick current FY quarter if present, else last available
            today = pd.Timestamp.today()
            fy_end, fq = _cb_fiscal_year_quarter(today, start_month=4)
            current_key = f"{fy_end}Q{fq}"

            available = set(qdf["QUARTER"])
            if current_key in available:
                q_sel = current_key
            else:
                q_sel = sorted(
                    qdf["QUARTER"].unique(),
                    key=lambda x: (int(x[:4]), int(x[-1])),
                )[-1]

            q_now_df = qdf[qdf["QUARTER"] == q_sel]
            if not q_now_df.empty:
                top_row = q_now_df.sort_values(WORTH_COL, ascending=False).iloc[0]
                fy_end = int(q_sel[:4])
                fq_now = int(q_sel[-1])
                reply = (
                    f"In **Q{fq_now} FY{fy_end}**, the **{top_row['BLOCK_TYPE']}** block shows the "
                    f"highest exposure at **{fmt_money(top_row[WORTH_COL])}**. "
                    "Prepare mitigation plans with the responsible owners."
                )
            else:
                reply = f"No data found for fiscal quarter {current_key}."

    elif "block" in q:
        bs = block_summary_df()
        if bs.empty:
            reply = "I couldn't find any blocked orders in the file."
        else:
            top = bs.iloc[0]
            reply = (
                f"**{top['BLOCK_TYPE']}** block represents the largest backlog exposure "
                f"at **{fmt_money(top[WORTH_COL])}**. "
                "Engage the respective team to address these issues first."
            )

    else:
        # Generic fallback
        reply = (
            "I can help you with:\n"
            "- Which block has the highest exposure\n"
            "- Which customers face backlog\n"
            "- Orders with multiple blocks\n"
            "- Quarterly trend of blocked exposure\n\n"
            "Try asking: **'Quarterly trend?'** or **'Which block has highest exposure?'**"
        )

    return reply


def maybe_rewrite_with_llm(text: str) -> str:
    """
    Optional LLM rewriter. If HF_TOKEN and HF_MODEL_ID are set and
    huggingface_hub is installed, we rewrite the analytics insight.
    """
    hf_token = os.getenv("HF_TOKEN")
    model_id = os.getenv("HF_MODEL_ID", "").strip()

    if not hf_token or not model_id or InferenceClient is None:
        return text  # analytics-only mode

    try:
        client = InferenceClient(model=model_id, token=hf_token)
        prompt = (
            "You are an operations assistant. Rewrite the following insight for a VP of Operations. "
            "Use at most 3 short bullet points. Be concise and actionable.\n\n"
            f"Insight:\n{text}\n\nBullets:\n-"
        )
        resp = client.text_generation(
            prompt,
            max_new_tokens=180,
            temperature=0.3,
            do_sample=True,
        )
        return resp.strip()
    except Exception:
        # If anything fails, fall back to the original
        return text


def chat_fn(message: str, history: List[Tuple[str, str]]) -> str:
    base = analytics_reply(message)
    final = maybe_rewrite_with_llm(base)
    return final


# -----------------------------------------------------
# GRADIO UI – Tiles + Chat (no Flask)
# -----------------------------------------------------
CUSTOM_CSS = """
.chatbot {max-height: 480px;}
.gradio-container {font-family: 'Segoe UI', system-ui, sans-serif;}
.tiles-row button {
    border-radius: 10px !important;
    border: 1px solid #e5e7eb !important;
    background: #ffffff !important;
    font-weight: 600 !important;
    color: #0A2048 !important;
}
.tiles-row button:hover {
    background: #f1f5f9 !important;
}
"""

with gr.Blocks(css=CUSTOM_CSS, title="Blocks AI Assistant") as demo:
    gr.Markdown(
        "## Blocks AI Assistant  \n"
        "Hybrid **AI + backlog analytics** over your blocked orders dataset."
    )

    with gr.Row(elem_classes="tiles-row"):
        btn_summary = gr.Button("Overall Block Summary")
        btn_focus = gr.Button("Actionable Focus Area")
    with gr.Row(elem_classes="tiles-row"):
        btn_multi = gr.Button("Multiple Block Orders")
        btn_quarter = gr.Button("Quarterly Trend")
        btn_faq = gr.Button("FAQ: What can you do?")

    chatbot = gr.Chatbot(
        label="Conversation",
        type="messages",
        height=420,
    )
    msg = gr.Textbox(
        placeholder="Ask about blocks, customers, or quarterly trends…",
        show_label=False,
    )
    clear_btn = gr.Button("Clear Chat")

    def handle_user_message(user_message, history):
        reply = chat_fn(user_message, history or [])
        (history or []).append({"role": "user", "content": user_message})
        history.append({"role": "assistant", "content": reply})
        return "", history

    msg.submit(handle_user_message, [msg, chatbot], [msg, chatbot])
    clear_btn.click(lambda: ([], ""), outputs=[chatbot, msg])

    # Tile → pre-defined questions wired into same logic
    def ask_predefined(prompt, history):
        reply = chat_fn(prompt, history or [])
        (history or []).append({"role": "user", "content": prompt})
        history.append({"role": "assistant", "content": reply})
        return history

    btn_summary.click(
        ask_predefined,
        inputs=[chatbot],
        outputs=[chatbot],
        _js=None,
        kwargs={"prompt": "Which block has highest exposure?"},
    )
    btn_focus.click(
        ask_predefined,
        inputs=[chatbot],
        outputs=[chatbot],
        kwargs={"prompt": "Which block should we focus on?"},
    )
    btn_multi.click(
        ask_predefined,
        inputs=[chatbot],
        outputs=[chatbot],
        kwargs={"prompt": "Orders with multiple blocks?"},
    )
    btn_quarter.click(
        ask_predefined,
        inputs=[chatbot],
        outputs=[chatbot],
        kwargs={"prompt": "Quarterly trend?"},
    )
    btn_faq.click(
        ask_predefined,
        inputs=[chatbot],
        outputs=[chatbot],
        kwargs={"prompt": "What can you do?"},
    )

if __name__ == "__main__":
    demo.launch()