twissamodi commited on
Commit
c1814a4
Β·
1 Parent(s): 108fd2f

add response

Browse files
Dockerfile CHANGED
@@ -4,10 +4,11 @@ WORKDIR /app
4
 
5
  COPY requirements.txt .
6
  RUN pip install --no-cache-dir -r requirements.txt
7
-
8
  COPY backend/ ./backend/
9
  COPY frontend/ ./frontend/
10
 
11
- EXPOSE 7860 7861
12
 
13
- CMD uvicorn backend.main:app --host 0.0.0.0 --port 7860 & python frontend/app.py
 
 
4
 
5
  COPY requirements.txt .
6
  RUN pip install --no-cache-dir -r requirements.txt
7
+
8
  COPY backend/ ./backend/
9
  COPY frontend/ ./frontend/
10
 
11
+ EXPOSE 7860 7862
12
 
13
+ CMD uvicorn backend.main:app --host 0.0.0.0 --port 7860 --app-dir /app & \
14
+ python frontend/app.py
backend/analytics.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from collections import defaultdict
2
+ from datetime import datetime
3
+
4
+ class AnalyticsTracker:
5
+ def __init__(self):
6
+ self.intent_counts = defaultdict(int)
7
+ self.total_queries = 0
8
+ self.recent_queries = [] # stores last 10 queries
9
+
10
+ def log(self, text: str, intent: str, confidence: float):
11
+ self.intent_counts[intent] += 1
12
+ self.total_queries += 1
13
+ self.recent_queries.append({
14
+ "text": text,
15
+ "intent": intent,
16
+ "confidence": confidence,
17
+ "timestamp": datetime.now().strftime("%H:%M:%S")
18
+ })
19
+ # keep only last 10
20
+ if len(self.recent_queries) > 10:
21
+ self.recent_queries.pop(0)
22
+
23
+ def get_top_intents(self, n=10):
24
+ sorted_intents = sorted(
25
+ self.intent_counts.items(),
26
+ key=lambda x: x[1],
27
+ reverse=True
28
+ )
29
+ return sorted_intents[:n]
30
+
31
+ def get_summary(self):
32
+ return {
33
+ "total_queries": self.total_queries,
34
+ "unique_intents_seen": len(self.intent_counts),
35
+ "top_intents": [
36
+ {"intent": k, "count": v}
37
+ for k, v in self.get_top_intents()
38
+ ],
39
+ "recent_queries": self.recent_queries
40
+ }
41
+
42
+ # Singleton
43
+ tracker = AnalyticsTracker()
backend/classifier.py CHANGED
@@ -32,7 +32,8 @@ LABEL_NAMES = [
32
  "unable_to_verify_identity", "verify_my_identity", "verify_source_of_funds",
33
  "verify_top_up", "virtual_card_not_working", "visa_or_mastercard",
34
  "why_verify_identity", "wrong_amount_of_cash_received",
35
- "wrong_exchange_rate_for_cash_withdrawal"
 
36
  ]
37
 
38
  class IntentClassifier:
@@ -45,7 +46,7 @@ class IntentClassifier:
45
 
46
  base_model = AutoModelForSequenceClassification.from_pretrained(
47
  MODEL_BASE,
48
- num_labels=77,
49
  torch_dtype=torch.float16,
50
  device_map="cpu"
51
  )
@@ -76,10 +77,25 @@ class IntentClassifier:
76
  "confidence": round(score.item() * 100, 2)
77
  })
78
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  return {
80
  "top_intent": results[0]["intent"],
81
- "confidence": results[0]["confidence"],
82
- "top3": results
 
 
83
  }
84
 
85
  classifier = IntentClassifier()
 
32
  "unable_to_verify_identity", "verify_my_identity", "verify_source_of_funds",
33
  "verify_top_up", "virtual_card_not_working", "visa_or_mastercard",
34
  "why_verify_identity", "wrong_amount_of_cash_received",
35
+ "wrong_exchange_rate_for_cash_withdrawal",
36
+ "unknown"
37
  ]
38
 
39
  class IntentClassifier:
 
46
 
47
  base_model = AutoModelForSequenceClassification.from_pretrained(
48
  MODEL_BASE,
49
+ num_labels=len(LABEL_NAMES),
50
  torch_dtype=torch.float16,
51
  device_map="cpu"
52
  )
 
77
  "confidence": round(score.item() * 100, 2)
78
  })
79
 
80
+ top_confidence = results[0]["confidence"]
81
+
82
+ # Confidence threshold β€” if model is uncertain, say so
83
+ THRESHOLD = 40.0
84
+ if results[0]["intent"] == "unknown" or results[0]["confidence"] < THRESHOLD:
85
+ return {
86
+ "top_intent": "unknown",
87
+ "confidence": results[0]["confidence"],
88
+ "top3": results,
89
+ "fallback": True,
90
+ "fallback_message": "I'm not sure I understand. Could you rephrase your banking query?"
91
+ }
92
+
93
  return {
94
  "top_intent": results[0]["intent"],
95
+ "confidence": top_confidence,
96
+ "top3": results,
97
+ "fallback": False,
98
+ "fallback_message": None
99
  }
100
 
101
  classifier = IntentClassifier()
backend/main.py CHANGED
@@ -1,6 +1,12 @@
 
 
 
 
1
  from fastapi import FastAPI, HTTPException
2
  from pydantic import BaseModel
3
  from classifier import classifier
 
 
4
 
5
  app = FastAPI(
6
  title="Banking Intent Classifier API",
@@ -20,6 +26,15 @@ class ClassifyResponse(BaseModel):
20
  top_intent: str
21
  confidence: float
22
  top3: list[IntentResult]
 
 
 
 
 
 
 
 
 
23
 
24
  # Routes
25
  @app.get("/")
@@ -36,4 +51,27 @@ def classify(request: ClassifyRequest):
36
  raise HTTPException(status_code=400, detail="Text cannot be empty")
37
 
38
  result = classifier.classify(request.text)
39
- return result
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import os
3
+ sys.path.append(os.path.dirname(os.path.abspath(__file__)))
4
+
5
  from fastapi import FastAPI, HTTPException
6
  from pydantic import BaseModel
7
  from classifier import classifier
8
+ from analytics import tracker
9
+ from responder import responder
10
 
11
  app = FastAPI(
12
  title="Banking Intent Classifier API",
 
26
  top_intent: str
27
  confidence: float
28
  top3: list[IntentResult]
29
+ fallback: bool
30
+ fallback_message: str | None
31
+
32
+ class RespondRequest(BaseModel):
33
+ text: str
34
+ intent: str
35
+
36
+ class RespondResponse(BaseModel):
37
+ response: str
38
 
39
  # Routes
40
  @app.get("/")
 
51
  raise HTTPException(status_code=400, detail="Text cannot be empty")
52
 
53
  result = classifier.classify(request.text)
54
+
55
+ # log to analytics
56
+ tracker.log(
57
+ text=request.text,
58
+ intent=result["top_intent"],
59
+ confidence=result["confidence"]
60
+ )
61
+
62
+ return result
63
+
64
+ @app.post("/respond", response_model=RespondResponse)
65
+ def respond(request: RespondRequest):
66
+ if request.intent == "unknown":
67
+ return {"response": "I'm not sure I understood your query. Could you please rephrase it or describe your banking issue in more detail?"}
68
+
69
+ response = responder.generate(
70
+ customer_message=request.text,
71
+ intent=request.intent
72
+ )
73
+ return {"response": response}
74
+
75
+ @app.get("/analytics")
76
+ def analytics():
77
+ return tracker.get_summary()
backend/responder.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from transformers import AutoTokenizer, AutoModelForCausalLM
2
+ import torch
3
+
4
+ INSTRUCT_MODEL = "Qwen/Qwen2.5-0.5B-Instruct"
5
+
6
+ SYSTEM_PROMPT = """You are a helpful, empathetic customer service agent for a digital banking app.
7
+ You help customers with payment issues, card problems, account management, and transfer queries.
8
+ Keep responses concise (2-3 sentences), friendly, and actionable.
9
+ If you need more information, ask one specific question.
10
+ Never make up specific account details or transaction information."""
11
+
12
+ class ResponseGenerator:
13
+ def __init__(self):
14
+ print("Loading response generator...")
15
+ self.device = "cuda" if torch.cuda.is_available() else "cpu"
16
+
17
+ self.tokenizer = AutoTokenizer.from_pretrained(INSTRUCT_MODEL)
18
+ self.model = AutoModelForCausalLM.from_pretrained(
19
+ INSTRUCT_MODEL,
20
+ torch_dtype=torch.float16 if self.device == "cuda" else torch.float32,
21
+ device_map="cpu"
22
+ )
23
+ self.model.eval()
24
+ print("Response generator loaded!")
25
+
26
+ def generate(self, customer_message: str, intent: str) -> str:
27
+ messages = [
28
+ {"role": "system", "content": SYSTEM_PROMPT},
29
+ {"role": "user", "content": f"""Customer message: "{customer_message}"
30
+ Detected intent: {intent.replace("_", " ")}
31
+
32
+ Please write a helpful response to this customer."""}
33
+ ]
34
+
35
+ text = self.tokenizer.apply_chat_template(
36
+ messages,
37
+ tokenize=False,
38
+ add_generation_prompt=True
39
+ )
40
+
41
+ inputs = self.tokenizer(
42
+ text,
43
+ return_tensors="pt"
44
+ ).to(self.device)
45
+
46
+ with torch.no_grad():
47
+ outputs = self.model.generate(
48
+ **inputs,
49
+ max_new_tokens=150,
50
+ temperature=0.7,
51
+ do_sample=True,
52
+ pad_token_id=self.tokenizer.eos_token_id
53
+ )
54
+
55
+ new_tokens = outputs[0][inputs['input_ids'].shape[1]:]
56
+ response = self.tokenizer.decode(new_tokens, skip_special_tokens=True)
57
+
58
+ return response.strip()
59
+
60
+ # Singleton
61
+ responder = ResponseGenerator()
frontend/app.py CHANGED
@@ -1,76 +1,179 @@
1
  import gradio as gr
2
  import requests
 
 
 
3
 
4
  BACKEND_URL = "http://localhost:7860"
5
 
6
  def classify_intent(text):
7
  if not text.strip():
8
- return "Please enter a message", ""
9
 
10
  try:
11
- response = requests.post(
 
12
  f"{BACKEND_URL}/classify",
13
  json={"text": text}
14
  )
15
- result = response.json()
16
 
17
- # Format top intent
18
- top_intent = result["top_intent"].replace("_", " ").title()
19
- confidence = result["confidence"]
20
-
21
- # Format top 3
22
  top3_text = ""
23
  for i, item in enumerate(result["top3"], 1):
24
  intent = item["intent"].replace("_", " ").title()
25
  conf = item["confidence"]
26
  top3_text += f"{i}. {intent} β€” {conf}%\n"
27
 
28
- return f"**{top_intent}** ({confidence}%)", top3_text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
 
30
  except Exception as e:
31
- return f"Error: {str(e)}", ""
 
 
32
 
33
- # Build UI
34
  with gr.Blocks(title="Banking Intent Classifier") as demo:
35
- gr.Markdown("""
36
- # 🏦 Banking Intent Classifier
37
- Powered by fine-tuned Qwen2.5 + LoRA | Trained on BANKING77
38
- """)
39
 
40
- with gr.Row():
41
- with gr.Column():
42
- text_input = gr.Textbox(
43
- label="Customer Message",
44
- placeholder="e.g. my card hasn't arrived yet...",
45
- lines=3
46
- )
47
- submit_btn = gr.Button("Classify", variant="primary")
48
-
49
- with gr.Column():
50
- intent_output = gr.Markdown(label="Detected Intent")
51
- top3_output = gr.Textbox(
52
- label="Top 3 Predictions",
 
 
 
 
 
 
 
 
 
53
  lines=4,
54
  interactive=False
55
  )
56
-
57
- # Example queries
58
- gr.Examples(
59
- examples=[
60
- ["My card hasn't arrived yet"],
61
- ["I can't remember my PIN"],
62
- ["My transfer failed"],
63
- ["I think my card was stolen"],
64
- ["Why is my balance wrong?"],
65
- ],
66
- inputs=text_input
67
- )
68
-
69
- submit_btn.click(
70
- fn=classify_intent,
71
- inputs=text_input,
72
- outputs=[intent_output, top3_output]
73
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
 
75
  if __name__ == "__main__":
76
- demo.launch(server_port=7861)
 
1
  import gradio as gr
2
  import requests
3
+ import matplotlib.pyplot as plt
4
+ import matplotlib
5
+ matplotlib.use('Agg') # non-interactive backend for servers
6
 
7
  BACKEND_URL = "http://localhost:7860"
8
 
9
  def classify_intent(text):
10
  if not text.strip():
11
+ return "Please enter a message", "", ""
12
 
13
  try:
14
+ # Step 1 - classify
15
+ classify_response = requests.post(
16
  f"{BACKEND_URL}/classify",
17
  json={"text": text}
18
  )
19
+ result = classify_response.json()
20
 
21
+ # Step 2 - format top 3
 
 
 
 
22
  top3_text = ""
23
  for i, item in enumerate(result["top3"], 1):
24
  intent = item["intent"].replace("_", " ").title()
25
  conf = item["confidence"]
26
  top3_text += f"{i}. {intent} β€” {conf}%\n"
27
 
28
+ # Step 3 - handle fallback
29
+ if result.get("fallback"):
30
+ return (
31
+ f"⚠️ **Unknown Query** ({result['confidence']}%)",
32
+ top3_text,
33
+ result["fallback_message"]
34
+ )
35
+
36
+ top_intent = result["top_intent"].replace("_", " ").title()
37
+ confidence = result["confidence"]
38
+
39
+ # Step 4 - generate response
40
+ respond_response = requests.post(
41
+ f"{BACKEND_URL}/respond",
42
+ json={
43
+ "text": text,
44
+ "intent": result["top_intent"]
45
+ }
46
+ )
47
+ generated = respond_response.json()["response"]
48
+
49
+ return (
50
+ f"**{top_intent}** ({confidence}%)",
51
+ top3_text,
52
+ generated
53
+ )
54
+
55
+ except Exception as e:
56
+ return f"Error: {str(e)}", "", ""
57
+
58
+ def get_analytics():
59
+ try:
60
+ response = requests.get(f"{BACKEND_URL}/analytics")
61
+ data = response.json()
62
+
63
+ total = data["total_queries"]
64
+ unique = data["unique_intents_seen"]
65
+ top_intents = data["top_intents"]
66
+ recent = data["recent_queries"]
67
+
68
+ # Build bar chart
69
+ if top_intents:
70
+ labels = [x["intent"].replace("_", " ").title() for x in top_intents]
71
+ counts = [x["count"] for x in top_intents]
72
+
73
+ fig, ax = plt.subplots(figsize=(10, 5))
74
+ bars = ax.barh(labels[::-1], counts[::-1], color="#4F86C6")
75
+ ax.set_xlabel("Number of Queries")
76
+ ax.set_title("Top Intents by Frequency")
77
+
78
+ # Add count labels on bars
79
+ for bar, count in zip(bars, counts[::-1]):
80
+ ax.text(
81
+ bar.get_width() + 0.1,
82
+ bar.get_y() + bar.get_height()/2,
83
+ str(count),
84
+ va='center'
85
+ )
86
+
87
+ plt.tight_layout()
88
+ else:
89
+ fig, ax = plt.subplots()
90
+ ax.text(0.5, 0.5, "No queries yet", ha='center', va='center')
91
+
92
+ # Build recent queries table
93
+ recent_text = ""
94
+ if recent:
95
+ recent_text = "Time | Intent | Confidence\n"
96
+ recent_text += "-" * 50 + "\n"
97
+ for q in reversed(recent):
98
+ intent = q["intent"].replace("_", " ").title()
99
+ recent_text += f"{q['timestamp']} | {intent} | {q['confidence']}%\n"
100
+ else:
101
+ recent_text = "No queries yet"
102
+
103
+ summary = f"Total Queries: {total} | Unique Intents Seen: {unique}"
104
+
105
+ return fig, recent_text, summary
106
 
107
  except Exception as e:
108
+ fig, ax = plt.subplots()
109
+ ax.text(0.5, 0.5, f"Error: {str(e)}", ha='center', va='center')
110
+ return fig, "", "Error fetching analytics"
111
 
 
112
  with gr.Blocks(title="Banking Intent Classifier") as demo:
113
+ gr.Markdown("# 🏦 Banking Intent Classifier")
114
+ gr.Markdown("Powered by fine-tuned Qwen2.5 + LoRA | Trained on BANKING77")
 
 
115
 
116
+ with gr.Tabs():
117
+ with gr.Tab("πŸ” Classify"):
118
+ with gr.Row():
119
+ with gr.Column():
120
+ text_input = gr.Textbox(
121
+ label="Customer Message",
122
+ placeholder="e.g. my card hasn't arrived yet...",
123
+ lines=3
124
+ )
125
+ submit_btn = gr.Button("Classify", variant="primary")
126
+
127
+ with gr.Column():
128
+ intent_output = gr.Markdown(label="Detected Intent")
129
+ top3_output = gr.Textbox(
130
+ label="Top 3 Predictions",
131
+ lines=4,
132
+ interactive=False
133
+ )
134
+
135
+ # Response below the two columns
136
+ response_output = gr.Textbox(
137
+ label="πŸ’¬ Suggested Customer Service Response",
138
  lines=4,
139
  interactive=False
140
  )
141
+
142
+ gr.Examples(
143
+ examples=[
144
+ ["My card hasn't arrived yet"],
145
+ ["I can't remember my PIN"],
146
+ ["My transfer failed"],
147
+ ["I think my card was stolen"],
148
+ ["Why is my balance wrong?"],
149
+ ["hi"], # ← tests unknown class
150
+ ["what is life"], # ← tests unknown class
151
+ ],
152
+ inputs=text_input
153
+ )
154
+
155
+ submit_btn.click(
156
+ fn=classify_intent,
157
+ inputs=text_input,
158
+ outputs=[intent_output, top3_output, response_output] # ← added response_output
159
+ )
160
+
161
+ with gr.Tab("πŸ“Š Analytics"):
162
+ refresh_btn = gr.Button("Refresh Dashboard", variant="primary")
163
+ summary_output = gr.Markdown()
164
+
165
+ with gr.Row():
166
+ chart_output = gr.Plot(label="Intent Frequency")
167
+ recent_output = gr.Textbox(
168
+ label="Recent Queries",
169
+ lines=12,
170
+ interactive=False
171
+ )
172
+
173
+ refresh_btn.click(
174
+ fn=get_analytics,
175
+ outputs=[chart_output, recent_output, summary_output]
176
+ )
177
 
178
  if __name__ == "__main__":
179
+ demo.launch(server_port=7862)
requirements.txt CHANGED
@@ -3,6 +3,7 @@ fastapi
3
  uvicorn
4
 
5
  # ML
 
6
  torch
7
  transformers
8
  peft
 
3
  uvicorn
4
 
5
  # ML
6
+ matplotlib
7
  torch
8
  transformers
9
  peft