SahithMVS's picture
Update app.py
2f6f992 verified
Raw
History Blame Contribute Delete
13.6 kB
# app.py
# Inventory Management Assistant – HuggingFace / Gradio app
import os
from typing import Dict, List, Tuple
import pandas as pd
import gradio as gr
# ----------------------------- Data Load ----------------------------- #
DATA_FILES: Dict[str, List[str]] = {
# --- exact files you have in /data/ ---
"backlog": [
"data/Backlog_updated.xlsx",
],
"inventory": [
"data/Inventory_updated.xlsx",
],
"billing": [
"data/Billing_updated.xlsx",
],
"lead_time": [
"data/Lead time_updated.xlsx", # note the space
],
"purchase_orders": [
"data/Purchase Orders_updated.xlsx", # note the space
],
"safety_stock": [
"data/safety_stock_updated.xlsx",
],
}
def _load_first_existing(path_candidates: List[str]) -> pd.DataFrame:
"""Try all candidate paths and return the first one that exists."""
for p in path_candidates:
if os.path.exists(p):
if p.lower().endswith(".csv"):
return pd.read_csv(p)
else:
return pd.read_excel(p)
raise FileNotFoundError(f"None of these files were found: {path_candidates}")
def load_all_data() -> Dict[str, pd.DataFrame]:
data = {}
missing = []
for key, paths in DATA_FILES.items():
try:
df = _load_first_existing(paths)
data[key] = df
except FileNotFoundError:
missing.append(key)
if missing:
# Fail loudly so HF logs show which logical tables are missing
raise RuntimeError(f"Data load error – missing logical tables: {missing}")
return data
DATA = load_all_data()
INV = DATA["inventory"]
BACKLOG = DATA["backlog"]
# ----------------------------- Helper functions ----------------------------- #
def _pick(colnames: List[str], candidates: List[str]):
for c in candidates:
if c in colnames:
return c
return None
def _basic_inventory_view(df: pd.DataFrame, top_n: int = 20) -> pd.DataFrame:
"""Return a light, generic view that won't break if columns differ."""
cols = df.columns.tolist()
mat_col = _pick(cols, ["SAP_MATERIAL_NO", "MATERIAL", "MATERIAL_NO"])
desc_col = _pick(cols, ["MATERIAL_DESCRIPTION", "MAT_DESC", "DESCRIPTION"])
plant_col = _pick(cols, ["PLANT", "LOCATION", "SITE"])
qty_col = _pick(cols, ["QOH", "QTY", "UNRESTRICTED_STOCK", "TOTAL_STOCK"])
age_col = _pick(cols, ["AGE_DAYS", "DAYS_ON_HAND", "DAYS_COVER"])
selected = [c for c in [mat_col, desc_col, plant_col, qty_col, age_col] if c]
if not selected:
return df.head(top_n)
return df[selected].head(top_n)
# ----------------------------- Business Logic ----------------------------- #
def get_fast_moving_materials(top_n: int = 25) -> pd.DataFrame:
"""Very simple heuristic: lowest days cover / age, then highest demand/qty."""
df = INV.copy()
cols = df.columns.tolist()
days_cover_col = _pick(cols, ["DAYS_COVER", "AGE_DAYS", "DAYS_ON_HAND"])
demand_col = _pick(cols, ["AVG_DAILY_DEMAND", "DEMAND_PER_DAY", "ISSUES_PER_DAY"])
qty_col = _pick(cols, ["QOH", "QTY", "UNRESTRICTED_STOCK", "TOTAL_STOCK"])
if days_cover_col:
df = df.sort_values(by=days_cover_col, ascending=True)
elif demand_col:
df = df.sort_values(by=demand_col, ascending=False)
elif qty_col:
df = df.sort_values(by=qty_col, ascending=False)
return _basic_inventory_view(df, top_n=top_n)
def get_dead_stock(top_n: int = 25) -> pd.DataFrame:
"""Heuristic: highest age / lowest movement."""
df = INV.copy()
cols = df.columns.tolist()
age_col = _pick(cols, ["AGE_DAYS", "DAYS_ON_HAND", "DAYS_SINCE_MOVEMENT"])
if age_col:
df = df.sort_values(by=age_col, ascending=False)
else:
# fallback: just low-qty materials
qty_col = _pick(cols, ["QOH", "QTY", "UNRESTRICTED_STOCK", "TOTAL_STOCK"])
if qty_col:
df = df.sort_values(by=qty_col, ascending=True)
return _basic_inventory_view(df, top_n=top_n)
def get_reallocation_opportunities(top_n: int = 25) -> pd.DataFrame:
"""
Simple cross-plant reallocation view:
- Uses inventory + backlog.
- Marks surplus/shortage per material/plant.
"""
inv = INV.copy()
bl = BACKLOG.copy()
inv_cols = inv.columns.tolist()
bl_cols = bl.columns.tolist()
mat_col_i = _pick(inv_cols, ["SAP_MATERIAL_NO", "MATERIAL", "MATERIAL_NO"])
plant_col_i = _pick(inv_cols, ["PLANT", "LOCATION", "SITE"])
qty_col_i = _pick(inv_cols, ["QOH", "QTY", "UNRESTRICTED_STOCK", "TOTAL_STOCK"])
mat_col_b = _pick(bl_cols, ["SAP_MATERIAL_NO", "MATERIAL", "MATERIAL_NO"])
plant_col_b = _pick(bl_cols, ["PLANT", "LOCATION", "SITE"])
demand_col_b = _pick(bl_cols, ["OPEN_QTY", "DEMAND_QTY", "BACKLOG_QTY"])
required = [mat_col_i, plant_col_i, qty_col_i, mat_col_b, plant_col_b, demand_col_b]
if any(c is None for c in required):
# If columns don't line up yet, just show generic message.
return pd.DataFrame(
{
"Message": [
"Reallocation logic needs aligned columns in inventory & backlog.",
f"Inventory columns: {inv_cols}",
f"Backlog columns: {bl_cols}",
]
}
)
inv_agg = (
inv.groupby([mat_col_i, plant_col_i])[qty_col_i]
.sum()
.reset_index()
.rename(columns={qty_col_i: "QOH"})
)
bl_agg = (
bl.groupby([mat_col_b, plant_col_b])[demand_col_b]
.sum()
.reset_index()
.rename(columns={mat_col_b: mat_col_i, plant_col_b: plant_col_i, demand_col_b: "DEMAND"})
)
merged = inv_agg.merge(bl_agg, on=[mat_col_i, plant_col_i], how="outer").fillna(0)
merged["NET"] = merged["QOH"] - merged["DEMAND"]
# Mark surplus / shortage
merged["STATUS"] = merged["NET"].apply(
lambda x: "Surplus" if x > 0 else ("Shortage" if x < 0 else "Balanced")
)
# Keep only materials which have at least one surplus and one shortage plant
mat_status = (
merged.groupby(mat_col_i)["STATUS"]
.agg(lambda s: set(s))
.reset_index()
.rename(columns={"STATUS": "STATUS_SET"})
)
interesting_mats = mat_status[
mat_status["STATUS_SET"].apply(lambda s: {"Surplus", "Shortage"}.issubset(s))
][mat_col_i]
out = merged[merged[mat_col_i].isin(interesting_mats)]
out = out.sort_values(by=[mat_col_i, "STATUS", "NET"])
return out.head(top_n * 4) # multiple rows per material
def get_risk_recommendations(top_n: int = 25) -> pd.DataFrame:
"""
Very simple 'at-risk' view:
- Net = demand – stock; positive = shortage.
"""
inv = INV.copy()
bl = BACKLOG.copy()
inv_cols = inv.columns.tolist()
bl_cols = bl.columns.tolist()
mat_col_i = _pick(inv_cols, ["SAP_MATERIAL_NO", "MATERIAL", "MATERIAL_NO"])
plant_col_i = _pick(inv_cols, ["PLANT", "LOCATION", "SITE"])
qty_col_i = _pick(inv_cols, ["QOH", "QTY", "UNRESTRICTED_STOCK", "TOTAL_STOCK"])
mat_col_b = _pick(bl_cols, ["SAP_MATERIAL_NO", "MATERIAL", "MATERIAL_NO"])
plant_col_b = _pick(bl_cols, ["PLANT", "LOCATION", "SITE"])
demand_col_b = _pick(bl_cols, ["OPEN_QTY", "DEMAND_QTY", "BACKLOG_QTY"])
required = [mat_col_i, plant_col_i, qty_col_i, mat_col_b, plant_col_b, demand_col_b]
if any(c is None for c in required):
return pd.DataFrame(
{
"Message": [
"Risk recommendations need aligned inventory & backlog columns.",
f"Inventory columns: {inv_cols}",
f"Backlog columns: {bl_cols}",
]
}
)
inv_agg = (
inv.groupby([mat_col_i, plant_col_i])[qty_col_i]
.sum()
.reset_index()
.rename(columns={qty_col_i: "QOH"})
)
bl_agg = (
bl.groupby([mat_col_b, plant_col_b])[demand_col_b]
.sum()
.reset_index()
.rename(columns={mat_col_b: mat_col_i, plant_col_b: plant_col_i, demand_col_b: "DEMAND"})
)
merged = inv_agg.merge(bl_agg, on=[mat_col_i, plant_col_i], how="outer").fillna(0)
merged["SHORTAGE"] = merged["DEMAND"] - merged["QOH"]
merged = merged[merged["SHORTAGE"] > 0]
cols_out = [mat_col_i, plant_col_i, "QOH", "DEMAND", "SHORTAGE"]
return merged[cols_out].sort_values("SHORTAGE", ascending=False).head(top_n)
def search_inventory(query: str) -> pd.DataFrame:
"""Very light search by material number / description / plant."""
if not query:
return _basic_inventory_view(INV, top_n=25)
df = INV.copy()
cols = df.columns.tolist()
mat_col = _pick(cols, ["SAP_MATERIAL_NO", "MATERIAL", "MATERIAL_NO"])
desc_col = _pick(cols, ["MATERIAL_DESCRIPTION", "MAT_DESC", "DESCRIPTION"])
plant_col = _pick(cols, ["PLANT", "LOCATION", "SITE"])
mask = pd.Series([False] * len(df))
if mat_col:
mask |= df[mat_col].astype(str).str.contains(query, case=False, na=False)
if desc_col:
mask |= df[desc_col].astype(str).str.contains(query, case=False, na=False)
if plant_col:
mask |= df[plant_col].astype(str).str.contains(query, case=False, na=False)
results = df[mask]
if results.empty:
return pd.DataFrame({"Message": [f"No inventory rows found for '{query}'"]})
return _basic_inventory_view(results, top_n=50)
# ----------------------------- Gradio Callbacks ----------------------------- #
def handle_tile(tile: str, history: List[Tuple[str, str]]):
if history is None:
history = []
if tile == "fast":
user_msg = "Show me fast moving materials."
df = get_fast_moving_materials()
assistant_msg = "Here are the current fast-moving materials based on days cover / age."
elif tile == "reallocate":
user_msg = "Show stock reallocation possibilities."
df = get_reallocation_opportunities()
assistant_msg = "These materials have surplus at some plants and shortages at others."
elif tile == "risk":
user_msg = "Show inventory risk recommendations."
df = get_risk_recommendations()
assistant_msg = "These materials have net shortages based on backlog vs available stock."
elif tile == "dead":
user_msg = "Show dead / slow-moving stock."
df = get_dead_stock()
assistant_msg = "These materials appear to be slow-moving or dead stock."
else:
user_msg = "Unknown action."
df = pd.DataFrame({"Message": ["Unknown tile clicked."]})
assistant_msg = "I couldn't identify that tile."
history = history + [(("user"), user_msg), (("assistant"), assistant_msg)]
return history, df
def handle_search(message: str, history: List[Tuple[str, str]]):
if history is None:
history = []
history = history + [("user", message)]
df = search_inventory(message)
assistant_msg = "Here is what I found in inventory for your search."
history = history + [("assistant", assistant_msg)]
return "", history, df
# ----------------------------- UI Layout ----------------------------- #
CUSTOM_CSS = """
.gradio-container {font-family: 'Segoe UI', system-ui, -apple-system, BlinkMacSystemFont, sans-serif;}
#header-bar {background-color: #002b5c; color: white; padding: 10px 16px; font-size: 20px; font-weight: 600;}
.tile-row button {height: 60px; font-size: 16px; font-weight: 600;}
#faq-bar {background-color: #003f87; color: white; padding: 8px 16px; margin-top: 8px;
border-radius: 8px; font-size: 15px; font-weight: 500;}
"""
with gr.Blocks(css=CUSTOM_CSS, title="Inventory Management Assistant") as demo:
gr.HTML('<div id="header-bar">Inventory Assistant</div>')
with gr.Row(elem_id="tile-row"):
btn_fast = gr.Button("Fast Moving Materials")
btn_reallocate = gr.Button("Stock Reallocation")
btn_risk = gr.Button("Risk Recommendations")
btn_dead = gr.Button("Dead Stock Materials")
gr.HTML(
'<div id="faq-bar">💡 FAQ: Where are we at risk on inventory, and where can we reallocate stock?</div>'
)
chatbot = gr.Chatbot(label="Inventory Assistant", height=260)
results_table = gr.Dataframe(
headers=[],
datatype="auto",
label="Results",
interactive=False,
visible=True,
wrap=True,
height=260,
)
with gr.Row():
txt = gr.Textbox(
placeholder="Ask about materials, plants or inventory…",
show_label=False,
scale=5,
)
btn_search = gr.Button("Search", scale=1)
# Wire the tiles
btn_fast.click(
fn=lambda h: handle_tile("fast", h),
inputs=chatbot,
outputs=[chatbot, results_table],
)
btn_reallocate.click(
fn=lambda h: handle_tile("reallocate", h),
inputs=chatbot,
outputs=[chatbot, results_table],
)
btn_risk.click(
fn=lambda h: handle_tile("risk", h),
inputs=chatbot,
outputs=[chatbot, results_table],
)
btn_dead.click(
fn=lambda h: handle_tile("dead", h),
inputs=chatbot,
outputs=[chatbot, results_table],
)
# Wire the search bar
btn_search.click(
fn=handle_search,
inputs=[txt, chatbot],
outputs=[txt, chatbot, results_table],
)
txt.submit(
fn=handle_search,
inputs=[txt, chatbot],
outputs=[txt, chatbot, results_table],
)
if __name__ == "__main__":
demo.launch()