RexTheSlasher commited on
Commit
a81e615
Β·
0 Parent(s):

Initial deployment

Browse files
Files changed (3) hide show
  1. Dockerfile +13 -0
  2. app.py +213 -0
  3. requirements.txt +10 -0
Dockerfile ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.12-slim
2
+
3
+ WORKDIR /code
4
+
5
+ COPY requirements.txt .
6
+
7
+ RUN pip install --no-cache-dir --timeout 300 -r requirements.txt
8
+
9
+ COPY app.py .
10
+
11
+ EXPOSE 7860
12
+
13
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
app.py ADDED
@@ -0,0 +1,213 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import warnings
4
+ warnings.filterwarnings("ignore")
5
+ os.environ["TF_ENABLE_ONEDNN_OPTS"] = "0"
6
+
7
+ from pathlib import Path
8
+ from fastapi import FastAPI, HTTPException
9
+ from fastapi.middleware.cors import CORSMiddleware
10
+ from pydantic import BaseModel
11
+ from contextlib import asynccontextmanager
12
+ from typing import Optional
13
+ import torch
14
+ import pickle
15
+ import re
16
+ import redis
17
+ from huggingface_hub import hf_hub_download
18
+ from transformers import AutoTokenizer, AutoModelForSequenceClassification
19
+
20
+ # ── Constants ────────────────────────────────────────────
21
+ CONFIDENCE_THRESHOLD = 10.0
22
+ OOS_INTENT = "oos"
23
+ OOS_RESPONSE = "I'm not sure I understand that. Could you rephrase or ask me something banking related?"
24
+ MAX_LEN = 64
25
+ DEVICE = torch.device("cpu")
26
+ MODEL_REPO = "RexTheSlasher/intent-classifier-bert"
27
+
28
+ # ── Redis Cloud ──────────────────────────────────────────
29
+ r = redis.Redis(
30
+ host=os.getenv('REDIS_HOST'),
31
+ port=int(os.getenv('REDIS_PORT', 6379)),
32
+ password=os.getenv('REDIS_PASSWORD'),
33
+ decode_responses=True
34
+ )
35
+
36
+ # ── Load model from Hugging Face Hub ────────────────────
37
+ print("Loading model from Hugging Face...")
38
+ tokenizer = AutoTokenizer.from_pretrained(MODEL_REPO)
39
+ model = AutoModelForSequenceClassification.from_pretrained(MODEL_REPO)
40
+ model = model.to(DEVICE)
41
+ model.eval()
42
+
43
+ # ── Load label encoder from Hugging Face Hub ────────────
44
+ encoder_path = hf_hub_download(repo_id=MODEL_REPO, filename="label_encoder.pkl")
45
+ with open(encoder_path, "rb") as f:
46
+ label_encoder = pickle.load(f)
47
+
48
+ print(f"Model loaded! Classes: {len(label_encoder.classes_)}")
49
+
50
+ # ── Text cleaning ────────────────────────────────────────
51
+ def clean_text(text: str) -> str:
52
+ text = text.lower()
53
+ text = re.sub(r"[^a-z0-9\s]", "", text)
54
+ text = re.sub(r"\s+", " ", text).strip()
55
+ return text
56
+
57
+ # ── Predict ──────────────────────────────────────────────
58
+ def predict(text: str) -> dict:
59
+ cleaned = clean_text(text)
60
+ encoding = tokenizer(
61
+ cleaned,
62
+ max_length=MAX_LEN,
63
+ padding="max_length",
64
+ truncation=True,
65
+ return_tensors="pt"
66
+ )
67
+ input_ids = encoding["input_ids"].to(DEVICE)
68
+ attention_mask = encoding["attention_mask"].to(DEVICE)
69
+
70
+ with torch.no_grad():
71
+ outputs = model(input_ids=input_ids, attention_mask=attention_mask)
72
+
73
+ probabilities = torch.softmax(outputs.logits, dim=1)
74
+ confidence, predicted_class = torch.max(probabilities, dim=1)
75
+ predicted_label = label_encoder.inverse_transform([predicted_class.item()])[0]
76
+ confidence_score = round(confidence.item() * 100, 2)
77
+
78
+ top3_probs, top3_indices = torch.topk(probabilities, 3, dim=1)
79
+ top3 = [
80
+ {
81
+ "intent": label_encoder.inverse_transform([idx.item()])[0],
82
+ "confidence": round(prob.item() * 100, 2)
83
+ }
84
+ for prob, idx in zip(top3_probs[0], top3_indices[0])
85
+ ]
86
+
87
+ is_low_confidence = confidence_score < CONFIDENCE_THRESHOLD
88
+ if predicted_label == OOS_INTENT or is_low_confidence:
89
+ predicted_label = "out_of_scope"
90
+ is_oos = True
91
+ else:
92
+ is_oos = False
93
+
94
+ return {
95
+ "input_text": text,
96
+ "cleaned_text": cleaned,
97
+ "intent": predicted_label,
98
+ "confidence": confidence_score,
99
+ "top3": top3,
100
+ "is_low_confidence": is_low_confidence,
101
+ "is_oos": is_oos,
102
+ "response": OOS_RESPONSE if is_oos else f"I detected your intent as {predicted_label}"
103
+ }
104
+
105
+ # ── FastAPI App ──────────────────────────────────────────
106
+ @asynccontextmanager
107
+ async def lifespan(app: FastAPI):
108
+ print("βœ… API started!")
109
+ yield
110
+
111
+ app = FastAPI(
112
+ title="Intent Classification Chatbot API",
113
+ version="2.0.0",
114
+ lifespan=lifespan
115
+ )
116
+
117
+ app.add_middleware(
118
+ CORSMiddleware,
119
+ allow_origins=["*"],
120
+ allow_methods=["*"],
121
+ allow_headers=["*"]
122
+ )
123
+
124
+ # ── Models ───────────────────────────────────────────────
125
+ class PredictRequest(BaseModel):
126
+ text: str
127
+ session_id: Optional[str] = None
128
+
129
+ class IntentResult(BaseModel):
130
+ intent: str
131
+ confidence: float
132
+
133
+ class PredictResponse(BaseModel):
134
+ session_id: str
135
+ input_text: str
136
+ cleaned_text: str
137
+ intent: str
138
+ confidence: float
139
+ top3: list[IntentResult]
140
+ last_intent: Optional[str] = None
141
+ is_low_confidence: bool = False
142
+ is_oos: bool = False
143
+ response: str = ""
144
+
145
+ # ── Routes ───────────────────────────────────────────────
146
+ @app.get("/")
147
+ def root():
148
+ return {"message": "Intent Chatbot API", "status": "running", "intents": len(label_encoder.classes_)}
149
+
150
+ @app.get("/health")
151
+ def health():
152
+ return {"status": "healthy", "model": "bert-base-uncased", "intents": len(label_encoder.classes_)}
153
+
154
+ @app.post("/predict", response_model=PredictResponse)
155
+ def predict_intent(request: PredictRequest):
156
+ text = request.text.strip()
157
+ if not text:
158
+ raise HTTPException(status_code=400, detail="Text cannot be empty")
159
+ if len(text) < 2:
160
+ raise HTTPException(status_code=400, detail="Text too short")
161
+ if len(text) > 200:
162
+ raise HTTPException(status_code=400, detail="Text too long")
163
+
164
+ import uuid, json
165
+ from datetime import datetime
166
+
167
+ session_id = request.session_id or str(uuid.uuid4())
168
+
169
+ # Get last intent
170
+ existing = r.get(f"session:{session_id}")
171
+ messages = json.loads(existing) if existing else []
172
+ last_intent = None
173
+ for msg in reversed(messages):
174
+ if msg.get("intent"):
175
+ last_intent = msg["intent"]
176
+ break
177
+
178
+ result = predict(text)
179
+
180
+ # Save to Redis
181
+ messages.append({"role": "user", "text": text, "timestamp": datetime.now().isoformat()})
182
+ messages.append({"role": "bot", "text": result["intent"], "intent": result["intent"], "confidence": result["confidence"], "timestamp": datetime.now().isoformat()})
183
+ r.setex(f"session:{session_id}", 3600, json.dumps(messages))
184
+
185
+ return PredictResponse(
186
+ session_id=session_id,
187
+ input_text=result["input_text"],
188
+ cleaned_text=result["cleaned_text"],
189
+ intent=result["intent"],
190
+ confidence=result["confidence"],
191
+ top3=[IntentResult(**r_) for r_ in result["top3"]],
192
+ last_intent=last_intent,
193
+ is_low_confidence=result["is_low_confidence"],
194
+ is_oos=result["is_oos"],
195
+ response=result["response"]
196
+ )
197
+
198
+ @app.get("/history/{session_id}")
199
+ def get_history(session_id: str):
200
+ existing = r.get(f"session:{session_id}")
201
+ if not existing:
202
+ raise HTTPException(status_code=404, detail="Session not found")
203
+ return {"session_id": session_id, "history": json.loads(existing)}
204
+
205
+ @app.delete("/history/{session_id}")
206
+ def delete_session(session_id: str):
207
+ r.delete(f"session:{session_id}")
208
+ return {"message": "Session cleared"}
209
+
210
+ @app.get("/intents")
211
+ def get_intents():
212
+ return {"total": len(label_encoder.classes_), "intents": list(label_encoder.classes_)}
213
+
requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ torch
2
+ transformers
3
+ fastapi
4
+ uvicorn
5
+ pydantic
6
+ scikit-learn
7
+ numpy
8
+ redis
9
+ huggingface_hub
10
+ python-dotenv