r00tb3 commited on
Commit
9365edb
·
verified ·
1 Parent(s): 9cf237d

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +158 -0
app.py ADDED
@@ -0,0 +1,158 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app.py — super-minimal ProdBot (Chat + Backlog only) for Hugging Face Spaces
2
+ import gradio as gr
3
+ import pandas as pd
4
+ from rapidfuzz import fuzz
5
+ import json, uuid, datetime as dt, re
6
+
7
+ STATE = {"features": [], "feedback": []}
8
+
9
+ def now_iso(): return dt.datetime.utcnow().isoformat() + "Z"
10
+ def mk_id(prefix="FB"): return f"{prefix}{uuid.uuid4().hex[:8]}"
11
+
12
+ EMAIL_RE = re.compile(r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}")
13
+ PHONE_RE = re.compile(r"\+?\d[\d\s\-()]{7,}\d")
14
+ IP_RE = re.compile(r"\b(?:\d{1,3}\.){3}\d{1,3}\b")
15
+ def redact(text: str) -> str:
16
+ text = EMAIL_RE.sub("[REDACTED_EMAIL]", text)
17
+ text = PHONE_RE.sub("[REDACTED_PHONE]", text)
18
+ text = IP_RE.sub("[REDACTED_IP]", text)
19
+ return text
20
+
21
+ POS_WORDS = {"love","great","awesome","amazing","nice","helpful","good"}
22
+ NEG_WORDS = {"slow","bad","hate","broken","fail","fails","error","crash","confusing","hard","bug"}
23
+ def tiny_sentiment(text: str):
24
+ t = text.lower()
25
+ pos = sum(w in t for w in POS_WORDS)
26
+ neg = sum(w in t for w in NEG_WORDS)
27
+ score = (pos - neg)
28
+ label = "positive" if score>0 else "negative" if score<0 else "neutral"
29
+ score = max(-1, min(1, score/3))
30
+ return label, round(score, 2)
31
+
32
+ def seed_demo():
33
+ if STATE["features"]: return
34
+ now = now_iso()
35
+ STATE["features"] = [
36
+ {"feature_id":"F101","title":"Invoice PDF export","description":"Export invoices as PDF",
37
+ "tags":["billing","export"],"status":"planned",
38
+ "demand_signals":{"feedback_count":14,"unique_users":11,"segments":{"pro":6,"enterprise":5}},
39
+ "priority_score":None,"subscribers":["finance@corp"],"updated_at":now},
40
+ {"feature_id":"F102","title":"Usage-based billing","description":"Metered billing by API calls",
41
+ "tags":["billing","pricing"],"status":"idea",
42
+ "demand_signals":{"feedback_count":8,"unique_users":8,"segments":{"enterprise":8}},
43
+ "priority_score":None,"subscribers":[],"updated_at":now},
44
+ {"feature_id":"F201","title":"Offline editing","description":"Work without internet, sync later",
45
+ "tags":["editor","offline","mobile"],"status":"planned",
46
+ "demand_signals":{"feedback_count":12,"unique_users":10,"segments":{"free":4,"pro":4,"enterprise":2}},
47
+ "priority_score":None,"subscribers":["pm_editor@corp"],"updated_at":now},
48
+ {"feature_id":"F202","title":"Comment threading","description":"Nested comments in editor",
49
+ "tags":["editor","collaboration","ux"],"status":"in_development",
50
+ "demand_signals":{"feedback_count":18,"unique_users":15,"segments":{"free":5,"pro":6,"enterprise":4}},
51
+ "priority_score":None,"subscribers":[],"updated_at":now},
52
+ {"feature_id":"F301","title":"Slack integration","description":"Notify Slack channels on changes",
53
+ "tags":["collaboration","integration"],"status":"beta",
54
+ "demand_signals":{"feedback_count":9,"unique_users":7,"segments":{"pro":5,"enterprise":2}},
55
+ "priority_score":None,"subscribers":["cs_team@corp"],"updated_at":now},
56
+ {"feature_id":"F302","title":"Role-based access control","description":"Viewer/Editor/Admin roles",
57
+ "tags":["collaboration","security"],"status":"planned",
58
+ "demand_signals":{"feedback_count":20,"unique_users":16,"segments":{"enterprise":16}},
59
+ "priority_score":None,"subscribers":["security@corp"],"updated_at":now}
60
+ ]
61
+ samples = [
62
+ ("u1","in_app","web",None,"Please add PDF invoice export."),
63
+ ("u2","email","web","1.2.0","Our finance team needs usage-based billing for API customers."),
64
+ ("u3","in_app","ios","3.0","I want to edit docs offline during flights."),
65
+ ("u4","survey","web",None,"Comment threads would make discussions much easier."),
66
+ ("u5","chat","web",None,"Slack integration is great but needs per-channel settings."),
67
+ ("u6","email","web","1.3.0","We require role-based access control for compliance."),
68
+ ("u7","in_app","android","2.5","Offline editing would be a lifesaver."),
69
+ ("u8","cs_transcript","web",None,"Customer asked about usage-based billing again."),
70
+ ("u9","in_app","web",None,"PDF invoices still missing; blocker for accounting."),
71
+ ("u10","in_app","desktop","2.2","Need nested comments inside editor like Google Docs.")
72
+ ]
73
+ for s in samples:
74
+ STATE["feedback"].append({
75
+ "id": mk_id("FB"), "user_id": s[0], "channel": s[1], "product_area": None,
76
+ "platform": s[2], "version": s[3], "text": redact(s[4]),
77
+ "sentiment_label": tiny_sentiment(s[4])[0],
78
+ "sentiment_score": tiny_sentiment(s[4])[1],
79
+ "language": None, "duplicates_of": None, "linked_feature_ids": [],
80
+ "created_at": now_iso()
81
+ })
82
+
83
+ def simple_classify(text: str):
84
+ t = text.lower()
85
+ if any(k in t for k in ["fail","error","crash","broken","bug"]): label="bug"
86
+ elif any(k in t for k in ["feature","add","please","need","would like","would love","request"]): label="feature_request"
87
+ elif any(k in t for k in ["how do i","help","docs","documentation","guide"]): label="support"
88
+ else: label="feedback"
89
+ return {"label": label, "confidence": 0.7}
90
+
91
+ def fuzzy_link(text: str):
92
+ scores=[]
93
+ for f in STATE["features"]:
94
+ blob = f"{f['title']} {f.get('description','')} {' '.join(f.get('tags',[]))}"
95
+ try:
96
+ from rapidfuzz import fuzz as _fuzz
97
+ sc = max(_fuzz.token_sort_ratio(text, blob), _fuzz.partial_ratio(text, blob))/100.0
98
+ except Exception:
99
+ sc = 0.0
100
+ scores.append((f["feature_id"], sc))
101
+ scores.sort(key=lambda x: x[1], reverse=True)
102
+ linked = [fid for fid, sc in scores if sc >= 0.60][:3]
103
+ confs = {fid: round(sc,2) for fid, sc in scores[:3]}
104
+ return linked, confs
105
+
106
+ def chat_fn(history, message):
107
+ message = message.strip()
108
+ if not message: return history, ""
109
+ red = redact(message)
110
+ cls = simple_classify(red)
111
+ s_label, s_score = tiny_sentiment(red)
112
+ fb = {"id": mk_id("FB"), "user_id":"anon", "channel":"chat","product_area":None,
113
+ "platform":"unknown","version":None,"text":red,"sentiment_label":s_label,
114
+ "sentiment_score":s_score,"language":None,"duplicates_of":None,
115
+ "linked_feature_ids":[],"created_at": now_iso()}
116
+ STATE["feedback"].append(fb)
117
+ linked, confs = fuzzy_link(red); fb["linked_feature_ids"] = linked
118
+ if linked:
119
+ titles = [next(f["title"] for f in STATE["features"] if f["feature_id"]==fid) for fid in linked]
120
+ reply = f"Thanks! I linked your message to: {', '.join(titles)}."
121
+ else:
122
+ reply = "Thanks! I couldn't confidently link this. File as a new idea?"
123
+ reply += f"\n\n[rationale] label={cls['label']} ({cls['confidence']}); sentiment={s_label}({s_score}); link_conf={confs}"
124
+ return (history or []) + [(message, reply)], ""
125
+
126
+ def tables(filter_text):
127
+ feats = STATE["features"]; fbs = STATE["feedback"]
128
+ if filter_text:
129
+ q = filter_text.lower()
130
+ feats = [f for f in feats if q in f["title"].lower() or any(q in t.lower() for t in f.get("tags",[]))]
131
+ fbs = [x for x in fbs if q in x["text"].lower()]
132
+ return pd.DataFrame(feats), pd.DataFrame(fbs)
133
+
134
+ with gr.Blocks(title="ProdBot (Minimal)") as demo:
135
+ gr.Markdown("# ProdBot (Minimal)\nCollect feedback ➜ auto-link to features.")
136
+ with gr.Tabs():
137
+ with gr.Tab("Chat"):
138
+ chat = gr.Chatbot(height=360) # tuple mode (avoids 'metadata is required')
139
+ inp = gr.Textbox(placeholder="Share feedback (e.g., 'Please add PDF invoice export')", label="Message")
140
+ def on_submit(h, m): return chat_fn(h or [], m)
141
+ inp.submit(on_submit, [chat, inp], [chat, inp])
142
+ with gr.Tab("Backlog"):
143
+ filt = gr.Textbox(placeholder="Filter by title/tag/text, e.g., 'billing'", label="Filter")
144
+ feat_df = gr.Dataframe(label="Features", interactive=False, wrap=True)
145
+ fb_df = gr.Dataframe(label="Feedback", interactive=False, wrap=True)
146
+ refresh = gr.Button("Refresh tables")
147
+ refresh.click(tables, [filt], [feat_df, fb_df])
148
+ filt.submit(tables, [filt], [feat_df, fb_df])
149
+
150
+ def _on_load():
151
+ seed_demo()
152
+ df1, df2 = tables("")
153
+ return df1, df2
154
+ demo.load(_on_load, None, [feat_df, fb_df])
155
+
156
+ if __name__ == "__main__":
157
+ seed_demo()
158
+ demo.launch()