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