-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp.py
More file actions
199 lines (179 loc) · 10.1 KB
/
app.py
File metadata and controls
199 lines (179 loc) · 10.1 KB
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
# app.py — MyPlan Telco Recommender (V10 Final Hotfix)
import json
from pathlib import Path
import pandas as pd
import streamlit as st
from agents import Coordinator
# ---- Data path (robust) ----
_DEFAULT_NAME = "enhanced_multi_user_telecom_data.csv"
DEFAULT_DATA = (Path(__file__).parent / _DEFAULT_NAME).resolve()
def resolve_data_path() -> Path:
p = DEFAULT_DATA
if p.exists():
return p
alt = Path(__file__).parent / "data" / _DEFAULT_NAME
if alt.exists():
return alt.resolve()
return p # non-existent; caller handles error
# ---- CSS: force black text on light background ----
def inject_light_theme():
css = [
"<style>",
":root{--bg:#ffffff;--panel:#ffffff;--panel2:#fafafa;--panel3:#f4f6f8;--text:#111111;--muted:#444444;--accent:#1a73e8;}",
"html,body,.stApp,[data-testid='stAppViewContainer'],[data-testid='stHeader']"
"{background:var(--bg)!important;color:var(--text)!important;font-family:'Inter','Segoe UI',system-ui,-apple-system,sans-serif;}",
"section[data-testid='stSidebar']{background:var(--panel2)!important;color:var(--text)!important;border-right:1px solid #e6e6e6;}",
"*,.stMarkdown p,.stMarkdown span,.stMarkdown li,h1,h2,h3,h4,h5,h6,label{color:var(--text)!important;}",
"a{color:var(--accent)!important;}",
".stSelectbox [role='combobox']{background:#ffffff!important;color:#111111!important;border:1px solid #d0d5dd!important;border-radius:8px;}",
".stSelectbox [role='combobox'] *{color:#111111!important;}",
".stSelectbox input{color:#111111!important;background:#ffffff!important;}",
".stSelectbox svg{color:#111111!important;fill:#111111!important;}",
".stSelectbox [data-baseweb='popover'],.stSelectbox [data-baseweb='menu']{background:#ffffff!important;color:#111111!important;border:1px solid #d0d5dd!important;}",
".stSelectbox [data-baseweb='option']{background:#ffffff!important;color:#111111!important;}",
".stSelectbox [data-baseweb='option']:hover{background:#f2f4f7!important;color:#111111!important;}",
"div[data-baseweb='input'] input, textarea, .stNumberInput input{background:#ffffff!important;color:#111111!important;border:1px solid #d0d5dd!important;}",
".stButton>button, .stDownloadButton>button{background:#ffffff!important;color:#111111!important;border:1px solid #d0d5dd!important;border-radius:12px;padding:.6rem 1rem;font-weight:600;}",
"[role='tablist']{border-bottom:1px solid #e6e6e6;}",
"[role='tab']{background:#ffffff!important;color:#111111!important;border:1px solid #d0d5dd!important;border-bottom:none!important;margin-right:.25rem;border-top-left-radius:8px;border-top-right-radius:8px;}",
"[role='tab'][aria-selected='true']{background:#f9fafb!important;color:#111111!important;}",
".card,.card-big{background:#ffffff!important;border:1px solid #e6e6e6;border-radius:14px;padding:1rem;color:#111111!important;}",
".card-title{color:#333333!important;font-weight:600;font-size:.9rem;text-transform:uppercase;letter-spacing:.02em;}",
"code, pre{background:#f7f7f9!important;color:#111111!important;border:1px solid #e6e6e6!important;}",
"hr{border-color:#e6e6e6;}",
"</style>",
]
st.markdown("\n".join(css), unsafe_allow_html=True)
def md_to_html(md: str) -> str:
return md
st.set_page_config(page_title="MyPlan Telco Recommender — V10 Final", layout="wide")
inject_light_theme()
st.title("📶 MyPlan Telco Recommender — V10 Final")
# Sidebar controls
with st.sidebar:
st.subheader("Controls")
provider = st.selectbox("LLM Provider", ["Local Stub", "OpenAI", "Gemini", "Claude"], index=0, key="prov")
model = st.selectbox("Model / Variant", ["stub-v1", "gpt-4o-mini", "gemini-1.5", "claude-3-haiku"], index=0, key="model")
st.caption("Text is forced to BLACK to ensure visibility.")
# Inputs
c1, c2, c3, c4 = st.columns(4)
with c1:
voice = st.number_input("Monthly Voice Minutes", min_value=0, max_value=10000, value=700, step=10)
with c2:
data_gb = st.number_input("Monthly Data (GB)", min_value=0, max_value=1000, value=22, step=1)
with c3:
sms = st.number_input("Monthly SMS", min_value=0, max_value=5000, value=40, step=5)
with c4:
roam = st.number_input("Monthly Roaming Minutes", min_value=0, max_value=2000, value=0, step=5)
c5, c6, c7 = st.columns(3)
with c5:
intl = st.number_input("Monthly International Minutes", min_value=0, max_value=2000, value=0, step=5)
with c6:
bill_amt = st.number_input("Average Monthly Bill (₹)", min_value=0, max_value=100000, value=599, step=10)
with c7:
disc = st.number_input("Avg. Discount/Waiver (₹)", min_value=0, max_value=100000, value=0, step=10)
late = st.number_input("Avg. Late Fee (₹)", min_value=0, max_value=100000, value=0, step=10)
st.markdown("---")
st.subheader("Get Recommendation")
user_inputs = {
"voice_minutes": voice,
"data_gb": data_gb,
"sms_count": sms,
"roaming_minutes": roam,
"intl_minutes": intl,
"bill_amount": bill_amt,
"discount_amount": disc,
"late_fee": late,
}
@st.cache_resource(show_spinner=False)
def load_coordinator(prov: str, m: str):
data_path = resolve_data_path()
if not data_path.exists():
st.error(f"Data file not found: '{_DEFAULT_NAME}'. Place it beside app.py or in ./data/.")
st.stop()
df = pd.read_csv(str(data_path))
coord = Coordinator(provider=prov, model=m)
coord.fit(df)
return coord
if st.button("Recommend Plan", type="primary"):
try:
coord = load_coordinator(provider, model)
except Exception as e:
st.exception(e)
st.stop()
try:
rec = coord.recommend(user_inputs)
except Exception as e:
st.exception(e)
st.stop()
left, right = st.columns([1,1])
with left:
st.markdown("<div class='card'><div class='card-title'>Recommended Plan</div>", unsafe_allow_html=True)
label = getattr(rec, "plan_label", None) or getattr(rec, "label", None) or (rec.get("plan_label") if isinstance(rec, dict) else rec.get("label", ""))
st.metric("Plan", label if label is not None else "")
conf = getattr(rec, "confidence", None) or (rec.get("confidence") if isinstance(rec, dict) else None)
if conf is not None:
st.metric("Recommendation Strength", f"{float(conf)*100:.1f}%")
est = getattr(rec, "est_monthly_cost", None) or getattr(rec, "est_cost", None) or (rec.get("est_monthly_cost") if isinstance(rec, dict) else rec.get("est_cost", None))
fair = getattr(rec, "fair_cost_range", None) or getattr(rec, "fair_range", None) or (rec.get("fair_cost_range") if isinstance(rec, dict) else rec.get("fair_range", None))
if est is not None:
if isinstance(fair, (list, tuple)) and len(fair)==2:
st.write(f"Estimated Monthly Cost: **₹{est:.0f}** (fair range: ₹{fair[0]:.0f}–₹{fair[1]:.0f})")
else:
st.write(f"Estimated Monthly Cost: **₹{est:.0f}**")
st.markdown("</div>", unsafe_allow_html=True)
with right:
narrative = getattr(rec, "narrative", None) or (rec.get("narrative") if isinstance(rec, dict) else "")
st.markdown("<div class='card'><div class='card-title'>Persona & Why this plan</div>"
f"<div style='padding-top:.4rem'>{md_to_html(narrative)}</div></div>", unsafe_allow_html=True)
# Similar subscribers with monthly averages
try:
# Safe retrieval without boolean-evaluating a DataFrame
peers = None
if hasattr(rec, 'peers'):
peers = rec.peers
elif isinstance(rec, dict):
peers = rec.get('peers')
if isinstance(peers, pd.DataFrame) and not peers.empty:
st.markdown("#### Similar Subscribers (usage view · top 10)")
cols = ["user_id","current_plan","bill_amount","voice_minutes","data_gb","sms_count","roaming_minutes","intl_minutes"]
show_cols = [c for c in cols if c in peers.columns]
view = peers[show_cols].head(10).copy()
# enrich with dataset averages
try:
df = pd.read_csv(str(resolve_data_path()), parse_dates=["date"])
df["month"] = df["date"].dt.to_period("M")
monthly = df.groupby(["user_id","month"]).agg(
voice_minutes_used=("voice_minutes_used","sum"),
data_mb_used=("data_mb_used","sum"),
sms_count_used=("sms_count_used","sum"),
roaming_voice_minutes=("roaming_voice_minutes","sum"),
international_voice_minutes=("international_voice_minutes","sum"),
).reset_index()
avg = monthly.groupby("user_id").mean(numeric_only=True).reset_index()
if "data_mb_used" in avg.columns:
avg["data_mb_used"] = avg["data_mb_used"]/1024.0
avg = avg.rename(columns={
"voice_minutes_used":"avg_voice_min",
"data_mb_used":"avg_data_gb",
"sms_count_used":"avg_sms",
"roaming_voice_minutes":"avg_roaming_min",
"international_voice_minutes":"avg_intl_min",
})
view = view.merge(avg, on="user_id", how="left")
except Exception as e:
st.info(f"Averaged usage enrichment skipped: {e}")
st.dataframe(view, use_container_width=True)
csv_bytes = view.to_csv(index=False).encode("utf-8")
st.download_button("Download similar subscribers (CSV)", csv_bytes, file_name="similar_subscribers.csv", mime="text/csv")
except Exception as e:
st.info(f"Peers section skipped: {e}")
with st.expander("📊 Stats & Rationale", expanded=False):
numeric = getattr(rec, "numeric_rationale", None) or (rec.get("numeric_rationale") if isinstance(rec, dict) else {})
features = {}
if isinstance(numeric, dict):
features = numeric.get("features", {})
payload = {"features": features, "confidence": conf}
st.code(json.dumps(payload, indent=2), language="json")
else:
st.info("Adjust inputs and click **Recommend Plan** to see suggestions.")