SahithMVS commited on
Commit
b5bc5c9
·
verified ·
1 Parent(s): 2cc443d

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +405 -0
app.py ADDED
@@ -0,0 +1,405 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app.py
2
+ # Inventory Management Assistant – HuggingFace / Gradio app
3
+
4
+ import os
5
+ from typing import Dict, List, Tuple
6
+
7
+ import pandas as pd
8
+ import gradio as gr
9
+
10
+ # ----------------------------- Data Load ----------------------------- #
11
+
12
+ DATA_FILES: Dict[str, List[str]] = {
13
+ # Re-use the same six datasets as your other two apps.
14
+ # Adjust candidate paths / filenames as needed.
15
+ "backlog": [
16
+ "data/Backlog w Customer Names.xlsx",
17
+ "data/Backlog_w_Customer_Names.xlsx",
18
+ "data/backlog.xlsx",
19
+ ],
20
+ "inventory": [
21
+ "data/INVENTORY.xlsx",
22
+ "data/Inventory.xlsx",
23
+ "data/inventory.xlsx",
24
+ ],
25
+ "forecast": [
26
+ "data/Forecast.xlsx",
27
+ "data/forecast.xlsx",
28
+ ],
29
+ "po": [
30
+ "data/Open_PO.xlsx",
31
+ "data/open_po.xlsx",
32
+ ],
33
+ "material_master": [
34
+ "data/Material_Master.xlsx",
35
+ "data/material_master.xlsx",
36
+ ],
37
+ "map": [
38
+ "data/Material_Plant_Map.xlsx",
39
+ "data/material_plant_map.xlsx",
40
+ ],
41
+ }
42
+
43
+
44
+ def _load_first_existing(path_candidates: List[str]) -> pd.DataFrame:
45
+ """Try all candidate paths and return the first one that exists."""
46
+ for p in path_candidates:
47
+ if os.path.exists(p):
48
+ if p.lower().endswith(".csv"):
49
+ return pd.read_csv(p)
50
+ else:
51
+ return pd.read_excel(p)
52
+ raise FileNotFoundError(f"None of these files were found: {path_candidates}")
53
+
54
+
55
+ def load_all_data() -> Dict[str, pd.DataFrame]:
56
+ data = {}
57
+ missing = []
58
+ for key, paths in DATA_FILES.items():
59
+ try:
60
+ df = _load_first_existing(paths)
61
+ data[key] = df
62
+ except FileNotFoundError:
63
+ missing.append(key)
64
+
65
+ if missing:
66
+ # Fail loudly so HF logs show which logical tables are missing
67
+ raise RuntimeError(f"Data load error – missing logical tables: {missing}")
68
+ return data
69
+
70
+
71
+ DATA = load_all_data()
72
+ INV = DATA["inventory"]
73
+ BACKLOG = DATA["backlog"]
74
+
75
+ # ----------------------------- Helper functions ----------------------------- #
76
+
77
+ def _pick(colnames: List[str], candidates: List[str]):
78
+ for c in candidates:
79
+ if c in colnames:
80
+ return c
81
+ return None
82
+
83
+
84
+ def _basic_inventory_view(df: pd.DataFrame, top_n: int = 20) -> pd.DataFrame:
85
+ """Return a light, generic view that won't break if columns differ."""
86
+ cols = df.columns.tolist()
87
+
88
+ mat_col = _pick(cols, ["SAP_MATERIAL_NO", "MATERIAL", "MATERIAL_NO"])
89
+ desc_col = _pick(cols, ["MATERIAL_DESCRIPTION", "MAT_DESC", "DESCRIPTION"])
90
+ plant_col = _pick(cols, ["PLANT", "LOCATION", "SITE"])
91
+ qty_col = _pick(cols, ["QOH", "QTY", "UNRESTRICTED_STOCK", "TOTAL_STOCK"])
92
+ age_col = _pick(cols, ["AGE_DAYS", "DAYS_ON_HAND", "DAYS_COVER"])
93
+
94
+ selected = [c for c in [mat_col, desc_col, plant_col, qty_col, age_col] if c]
95
+ if not selected:
96
+ return df.head(top_n)
97
+ return df[selected].head(top_n)
98
+
99
+
100
+ # ----------------------------- Business Logic ----------------------------- #
101
+
102
+ def get_fast_moving_materials(top_n: int = 25) -> pd.DataFrame:
103
+ """Very simple heuristic: lowest days cover / age, then highest demand/qty."""
104
+ df = INV.copy()
105
+ cols = df.columns.tolist()
106
+
107
+ days_cover_col = _pick(cols, ["DAYS_COVER", "AGE_DAYS", "DAYS_ON_HAND"])
108
+ demand_col = _pick(cols, ["AVG_DAILY_DEMAND", "DEMAND_PER_DAY", "ISSUES_PER_DAY"])
109
+ qty_col = _pick(cols, ["QOH", "QTY", "UNRESTRICTED_STOCK", "TOTAL_STOCK"])
110
+
111
+ if days_cover_col:
112
+ df = df.sort_values(by=days_cover_col, ascending=True)
113
+ elif demand_col:
114
+ df = df.sort_values(by=demand_col, ascending=False)
115
+ elif qty_col:
116
+ df = df.sort_values(by=qty_col, ascending=False)
117
+
118
+ return _basic_inventory_view(df, top_n=top_n)
119
+
120
+
121
+ def get_dead_stock(top_n: int = 25) -> pd.DataFrame:
122
+ """Heuristic: highest age / lowest movement."""
123
+ df = INV.copy()
124
+ cols = df.columns.tolist()
125
+
126
+ age_col = _pick(cols, ["AGE_DAYS", "DAYS_ON_HAND", "DAYS_SINCE_MOVEMENT"])
127
+ if age_col:
128
+ df = df.sort_values(by=age_col, ascending=False)
129
+ else:
130
+ # fallback: just low-qty materials
131
+ qty_col = _pick(cols, ["QOH", "QTY", "UNRESTRICTED_STOCK", "TOTAL_STOCK"])
132
+ if qty_col:
133
+ df = df.sort_values(by=qty_col, ascending=True)
134
+
135
+ return _basic_inventory_view(df, top_n=top_n)
136
+
137
+
138
+ def get_reallocation_opportunities(top_n: int = 25) -> pd.DataFrame:
139
+ """
140
+ Simple cross-plant reallocation view:
141
+ - Uses inventory + backlog.
142
+ - Marks surplus/shortage per material/plant.
143
+ """
144
+ inv = INV.copy()
145
+ bl = BACKLOG.copy()
146
+
147
+ inv_cols = inv.columns.tolist()
148
+ bl_cols = bl.columns.tolist()
149
+
150
+ mat_col_i = _pick(inv_cols, ["SAP_MATERIAL_NO", "MATERIAL", "MATERIAL_NO"])
151
+ plant_col_i = _pick(inv_cols, ["PLANT", "LOCATION", "SITE"])
152
+ qty_col_i = _pick(inv_cols, ["QOH", "QTY", "UNRESTRICTED_STOCK", "TOTAL_STOCK"])
153
+
154
+ mat_col_b = _pick(bl_cols, ["SAP_MATERIAL_NO", "MATERIAL", "MATERIAL_NO"])
155
+ plant_col_b = _pick(bl_cols, ["PLANT", "LOCATION", "SITE"])
156
+ demand_col_b = _pick(bl_cols, ["OPEN_QTY", "DEMAND_QTY", "BACKLOG_QTY"])
157
+
158
+ required = [mat_col_i, plant_col_i, qty_col_i, mat_col_b, plant_col_b, demand_col_b]
159
+ if any(c is None for c in required):
160
+ # If columns don't line up yet, just show generic message.
161
+ return pd.DataFrame(
162
+ {
163
+ "Message": [
164
+ "Reallocation logic needs aligned columns in inventory & backlog.",
165
+ f"Inventory columns: {inv_cols}",
166
+ f"Backlog columns: {bl_cols}",
167
+ ]
168
+ }
169
+ )
170
+
171
+ inv_agg = (
172
+ inv.groupby([mat_col_i, plant_col_i])[qty_col_i]
173
+ .sum()
174
+ .reset_index()
175
+ .rename(columns={qty_col_i: "QOH"})
176
+ )
177
+
178
+ bl_agg = (
179
+ bl.groupby([mat_col_b, plant_col_b])[demand_col_b]
180
+ .sum()
181
+ .reset_index()
182
+ .rename(columns={mat_col_b: mat_col_i, plant_col_b: plant_col_i, demand_col_b: "DEMAND"})
183
+ )
184
+
185
+ merged = inv_agg.merge(bl_agg, on=[mat_col_i, plant_col_i], how="outer").fillna(0)
186
+ merged["NET"] = merged["QOH"] - merged["DEMAND"]
187
+
188
+ # Mark surplus / shortage
189
+ merged["STATUS"] = merged["NET"].apply(
190
+ lambda x: "Surplus" if x > 0 else ("Shortage" if x < 0 else "Balanced")
191
+ )
192
+
193
+ # Keep only materials which have at least one surplus and one shortage plant
194
+ mat_status = (
195
+ merged.groupby(mat_col_i)["STATUS"]
196
+ .agg(lambda s: set(s))
197
+ .reset_index()
198
+ .rename(columns={"STATUS": "STATUS_SET"})
199
+ )
200
+ interesting_mats = mat_status[
201
+ mat_status["STATUS_SET"].apply(lambda s: {"Surplus", "Shortage"}.issubset(s))
202
+ ][mat_col_i]
203
+
204
+ out = merged[merged[mat_col_i].isin(interesting_mats)]
205
+ out = out.sort_values(by=[mat_col_i, "STATUS", "NET"])
206
+ return out.head(top_n * 4) # multiple rows per material
207
+
208
+
209
+ def get_risk_recommendations(top_n: int = 25) -> pd.DataFrame:
210
+ """
211
+ Very simple 'at-risk' view:
212
+ - Net = demand – stock; positive = shortage.
213
+ """
214
+ inv = INV.copy()
215
+ bl = BACKLOG.copy()
216
+
217
+ inv_cols = inv.columns.tolist()
218
+ bl_cols = bl.columns.tolist()
219
+
220
+ mat_col_i = _pick(inv_cols, ["SAP_MATERIAL_NO", "MATERIAL", "MATERIAL_NO"])
221
+ plant_col_i = _pick(inv_cols, ["PLANT", "LOCATION", "SITE"])
222
+ qty_col_i = _pick(inv_cols, ["QOH", "QTY", "UNRESTRICTED_STOCK", "TOTAL_STOCK"])
223
+
224
+ mat_col_b = _pick(bl_cols, ["SAP_MATERIAL_NO", "MATERIAL", "MATERIAL_NO"])
225
+ plant_col_b = _pick(bl_cols, ["PLANT", "LOCATION", "SITE"])
226
+ demand_col_b = _pick(bl_cols, ["OPEN_QTY", "DEMAND_QTY", "BACKLOG_QTY"])
227
+
228
+ required = [mat_col_i, plant_col_i, qty_col_i, mat_col_b, plant_col_b, demand_col_b]
229
+ if any(c is None for c in required):
230
+ return pd.DataFrame(
231
+ {
232
+ "Message": [
233
+ "Risk recommendations need aligned inventory & backlog columns.",
234
+ f"Inventory columns: {inv_cols}",
235
+ f"Backlog columns: {bl_cols}",
236
+ ]
237
+ }
238
+ )
239
+
240
+ inv_agg = (
241
+ inv.groupby([mat_col_i, plant_col_i])[qty_col_i]
242
+ .sum()
243
+ .reset_index()
244
+ .rename(columns={qty_col_i: "QOH"})
245
+ )
246
+ bl_agg = (
247
+ bl.groupby([mat_col_b, plant_col_b])[demand_col_b]
248
+ .sum()
249
+ .reset_index()
250
+ .rename(columns={mat_col_b: mat_col_i, plant_col_b: plant_col_i, demand_col_b: "DEMAND"})
251
+ )
252
+
253
+ merged = inv_agg.merge(bl_agg, on=[mat_col_i, plant_col_i], how="outer").fillna(0)
254
+ merged["SHORTAGE"] = merged["DEMAND"] - merged["QOH"]
255
+ merged = merged[merged["SHORTAGE"] > 0]
256
+
257
+ cols_out = [mat_col_i, plant_col_i, "QOH", "DEMAND", "SHORTAGE"]
258
+ return merged[cols_out].sort_values("SHORTAGE", ascending=False).head(top_n)
259
+
260
+
261
+ def search_inventory(query: str) -> pd.DataFrame:
262
+ """Very light search by material number / description / plant."""
263
+ if not query:
264
+ return _basic_inventory_view(INV, top_n=25)
265
+
266
+ df = INV.copy()
267
+ cols = df.columns.tolist()
268
+ mat_col = _pick(cols, ["SAP_MATERIAL_NO", "MATERIAL", "MATERIAL_NO"])
269
+ desc_col = _pick(cols, ["MATERIAL_DESCRIPTION", "MAT_DESC", "DESCRIPTION"])
270
+ plant_col = _pick(cols, ["PLANT", "LOCATION", "SITE"])
271
+
272
+ mask = pd.Series([False] * len(df))
273
+ if mat_col:
274
+ mask |= df[mat_col].astype(str).str.contains(query, case=False, na=False)
275
+ if desc_col:
276
+ mask |= df[desc_col].astype(str).str.contains(query, case=False, na=False)
277
+ if plant_col:
278
+ mask |= df[plant_col].astype(str).str.contains(query, case=False, na=False)
279
+
280
+ results = df[mask]
281
+ if results.empty:
282
+ return pd.DataFrame({"Message": [f"No inventory rows found for '{query}'"]})
283
+ return _basic_inventory_view(results, top_n=50)
284
+
285
+
286
+ # ----------------------------- Gradio Callbacks ----------------------------- #
287
+
288
+ def handle_tile(tile: str, history: List[Tuple[str, str]]):
289
+ if history is None:
290
+ history = []
291
+
292
+ if tile == "fast":
293
+ user_msg = "Show me fast moving materials."
294
+ df = get_fast_moving_materials()
295
+ assistant_msg = "Here are the current fast-moving materials based on days cover / age."
296
+ elif tile == "reallocate":
297
+ user_msg = "Show stock reallocation possibilities."
298
+ df = get_reallocation_opportunities()
299
+ assistant_msg = "These materials have surplus at some plants and shortages at others."
300
+ elif tile == "risk":
301
+ user_msg = "Show inventory risk recommendations."
302
+ df = get_risk_recommendations()
303
+ assistant_msg = "These materials have net shortages based on backlog vs available stock."
304
+ elif tile == "dead":
305
+ user_msg = "Show dead / slow-moving stock."
306
+ df = get_dead_stock()
307
+ assistant_msg = "These materials appear to be slow-moving or dead stock."
308
+ else:
309
+ user_msg = "Unknown action."
310
+ df = pd.DataFrame({"Message": ["Unknown tile clicked."]})
311
+ assistant_msg = "I couldn't identify that tile."
312
+
313
+ history = history + [(("user"), user_msg), (("assistant"), assistant_msg)]
314
+ return history, df
315
+
316
+
317
+ def handle_search(message: str, history: List[Tuple[str, str]]):
318
+ if history is None:
319
+ history = []
320
+
321
+ history = history + [("user", message)]
322
+ df = search_inventory(message)
323
+ assistant_msg = "Here is what I found in inventory for your search."
324
+ history = history + [("assistant", assistant_msg)]
325
+ return "", history, df
326
+
327
+
328
+ # ----------------------------- UI Layout ----------------------------- #
329
+
330
+ CUSTOM_CSS = """
331
+ .gradio-container {font-family: 'Segoe UI', system-ui, -apple-system, BlinkMacSystemFont, sans-serif;}
332
+ #header-bar {background-color: #002b5c; color: white; padding: 10px 16px; font-size: 20px; font-weight: 600;}
333
+ .tile-row button {height: 60px; font-size: 16px; font-weight: 600;}
334
+ #faq-bar {background-color: #003f87; color: white; padding: 8px 16px; margin-top: 8px;
335
+ border-radius: 8px; font-size: 15px; font-weight: 500;}
336
+ """
337
+
338
+ with gr.Blocks(css=CUSTOM_CSS, title="Inventory Management Assistant") as demo:
339
+ gr.HTML('<div id="header-bar">Inventory Assistant</div>')
340
+
341
+ with gr.Row(elem_id="tile-row"):
342
+ btn_fast = gr.Button("Fast Moving Materials")
343
+ btn_reallocate = gr.Button("Stock Reallocation")
344
+ btn_risk = gr.Button("Risk Recommendations")
345
+ btn_dead = gr.Button("Dead Stock Materials")
346
+
347
+ gr.HTML(
348
+ '<div id="faq-bar">💡 FAQ: Where are we at risk on inventory, and where can we reallocate stock?</div>'
349
+ )
350
+
351
+ chatbot = gr.Chatbot(label="Inventory Assistant", height=260)
352
+ results_table = gr.Dataframe(
353
+ headers=[],
354
+ datatype="auto",
355
+ label="Results",
356
+ interactive=False,
357
+ visible=True,
358
+ wrap=True,
359
+ height=260,
360
+ )
361
+
362
+ with gr.Row():
363
+ txt = gr.Textbox(
364
+ placeholder="Ask about materials, plants or inventory…",
365
+ show_label=False,
366
+ scale=5,
367
+ )
368
+ btn_search = gr.Button("Search", scale=1)
369
+
370
+ # Wire the tiles
371
+ btn_fast.click(
372
+ fn=lambda h: handle_tile("fast", h),
373
+ inputs=chatbot,
374
+ outputs=[chatbot, results_table],
375
+ )
376
+ btn_reallocate.click(
377
+ fn=lambda h: handle_tile("reallocate", h),
378
+ inputs=chatbot,
379
+ outputs=[chatbot, results_table],
380
+ )
381
+ btn_risk.click(
382
+ fn=lambda h: handle_tile("risk", h),
383
+ inputs=chatbot,
384
+ outputs=[chatbot, results_table],
385
+ )
386
+ btn_dead.click(
387
+ fn=lambda h: handle_tile("dead", h),
388
+ inputs=chatbot,
389
+ outputs=[chatbot, results_table],
390
+ )
391
+
392
+ # Wire the search bar
393
+ btn_search.click(
394
+ fn=handle_search,
395
+ inputs=[txt, chatbot],
396
+ outputs=[txt, chatbot, results_table],
397
+ )
398
+ txt.submit(
399
+ fn=handle_search,
400
+ inputs=[txt, chatbot],
401
+ outputs=[txt, chatbot, results_table],
402
+ )
403
+
404
+ if __name__ == "__main__":
405
+ demo.launch()