BasilTh
commited on
Commit
Β·
ae5323d
1
Parent(s):
938032f
Deploy updated SLM customer-support chatbot
Browse files- SLM_CService.py +28 -32
SLM_CService.py
CHANGED
@@ -66,50 +66,42 @@ chat_pipe = pipeline(
|
|
66 |
tokenizer=tokenizer,
|
67 |
trust_remote_code=True,
|
68 |
return_full_text=False,
|
69 |
-
)
|
70 |
|
71 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
72 |
# Moderation & blocking (strict)
|
73 |
from transformers import TextClassificationPipeline
|
74 |
|
75 |
-
# Heuristic list (not exhaustive; we also add classifier + bad_words_ids)
|
76 |
SEXUAL_TERMS = [
|
77 |
-
# single words
|
78 |
"sex","sexual","porn","nsfw","fetish","kink","bdsm","nude","naked","anal",
|
79 |
"blowjob","handjob","cum","breast","boobs","vagina","penis","semen","ejaculate",
|
80 |
"doggy","missionary","cowgirl","69","kamasutra","dominatrix","submissive","spank",
|
81 |
-
# common phrases
|
82 |
"sex position","have sex","make love","how to flirt","dominant in bed",
|
83 |
]
|
84 |
|
85 |
def _bad_words_ids(tokenizer, terms: List[str]) -> List[List[int]]:
|
86 |
-
"""Build bad_words_ids for generate(); add with/without leading space.
|
87 |
-
|
88 |
-
HF docs: pass bad_words_ids to prevent generating those tokens. :contentReference[oaicite:4]{index=4}
|
89 |
-
"""
|
90 |
ids = set()
|
91 |
for t in terms:
|
92 |
-
for
|
93 |
-
toks = tokenizer(
|
94 |
if toks:
|
95 |
ids.add(tuple(toks))
|
96 |
return [list(t) for t in ids]
|
97 |
|
98 |
BAD_WORD_IDS = _bad_words_ids(tokenizer, SEXUAL_TERMS)
|
99 |
|
100 |
-
# Optional classifiers for extra safety (small, fast)
|
101 |
nsfw_cls: TextClassificationPipeline = pipeline(
|
102 |
"text-classification",
|
103 |
model="eliasalbouzidi/distilbert-nsfw-text-classifier",
|
104 |
truncation=True,
|
105 |
-
)
|
106 |
|
107 |
toxicity_cls: TextClassificationPipeline = pipeline(
|
108 |
"text-classification",
|
109 |
model="unitary/toxic-bert",
|
110 |
truncation=True,
|
111 |
return_all_scores=True,
|
112 |
-
)
|
113 |
|
114 |
def is_sexual_or_toxic(text: str) -> bool:
|
115 |
t = (text or "").lower()
|
@@ -117,14 +109,12 @@ def is_sexual_or_toxic(text: str) -> bool:
|
|
117 |
return True
|
118 |
try:
|
119 |
res = nsfw_cls(t)[0]
|
120 |
-
|
121 |
-
score = float(res.get("score") or 0)
|
122 |
-
if label == "nsfw" and score > 0.60:
|
123 |
return True
|
124 |
except Exception:
|
125 |
pass
|
126 |
try:
|
127 |
-
scores = toxicity_cls(t)[0]
|
128 |
if any(item["score"] > 0.60 and item["label"].lower() in
|
129 |
{"toxic","severe_toxic","obscene","threat","insult","identity_hate"} for item in scores):
|
130 |
return True
|
@@ -136,7 +126,7 @@ REFUSAL = ("Sorry, I canβt help with that. Iβm only for store support "
|
|
136 |
"(orders, shipping, ETA, tracking, returns, warranty, account).")
|
137 |
|
138 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
139 |
-
# Memory (
|
140 |
memory = ConversationBufferMemory(return_messages=True)
|
141 |
|
142 |
# System prompt = domain guardrails
|
@@ -179,14 +169,13 @@ def _lc_to_messages() -> List[Dict[str,str]]:
|
|
179 |
return msgs
|
180 |
|
181 |
def _generate_reply(user_input: str) -> str:
|
182 |
-
# format as chat (HF chat templating guide) :contentReference[oaicite:8]{index=8}
|
183 |
messages = _lc_to_messages() + [{"role": "user", "content": user_input}]
|
184 |
prompt = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
|
185 |
out = chat_pipe(
|
186 |
prompt,
|
187 |
eos_token_id=tokenizer.eos_token_id,
|
188 |
pad_token_id=tokenizer.pad_token_id,
|
189 |
-
bad_words_ids=BAD_WORD_IDS,
|
190 |
**GEN_KW,
|
191 |
)[0]["generated_text"]
|
192 |
return out.strip()
|
@@ -198,20 +187,15 @@ def chat_with_memory(user_input: str) -> str:
|
|
198 |
if not ui:
|
199 |
return "How can I help with your order today?"
|
200 |
|
201 |
-
# 1)
|
202 |
if is_sexual_or_toxic(ui):
|
203 |
reply = REFUSAL
|
204 |
memory.save_context({"input": ui}, {"output": reply})
|
205 |
return reply
|
206 |
|
207 |
-
# 2) Support-only guard (light small-talk allowed)
|
208 |
low = ui.lower()
|
209 |
-
if not any(k in low for k in ALLOWED_KEYWORDS) and not any(k in low for k in ("hi","hello","hey","thanks","thank you")):
|
210 |
-
reply = "Iβm for store support only (orders, shipping, returns, warranty, account). How can I help with those?"
|
211 |
-
memory.save_context({"input": ui}, {"output": reply})
|
212 |
-
return reply
|
213 |
|
214 |
-
#
|
215 |
if any(tok in low for tok in ["thank you","thanks","thx"]):
|
216 |
reply = handle_gratitude()
|
217 |
memory.save_context({"input": ui}, {"output": reply})
|
@@ -221,15 +205,26 @@ def chat_with_memory(user_input: str) -> str:
|
|
221 |
memory.save_context({"input": ui}, {"output": reply})
|
222 |
return reply
|
223 |
|
224 |
-
#
|
225 |
new_o = extract_order(ui)
|
226 |
if new_o:
|
227 |
stored_order = new_o
|
228 |
if pending_intent in ("status","eta","track","link"):
|
229 |
fn = {"status": handle_status,"eta": handle_eta,"track": handle_track,"link": handle_link}[pending_intent]
|
230 |
-
reply = fn(stored_order)
|
231 |
-
|
232 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
233 |
if any(k in low for k in ["status","where is my order","check status"]):
|
234 |
intent = "status"
|
235 |
elif any(k in low for k in ["how long","eta","delivery time"]):
|
@@ -241,6 +236,7 @@ def chat_with_memory(user_input: str) -> str:
|
|
241 |
else:
|
242 |
intent = "fallback"
|
243 |
|
|
|
244 |
if intent in ("status","eta","track","link"):
|
245 |
if not stored_order:
|
246 |
pending_intent = intent
|
@@ -251,7 +247,7 @@ def chat_with_memory(user_input: str) -> str:
|
|
251 |
memory.save_context({"input": ui}, {"output": reply})
|
252 |
return reply
|
253 |
|
254 |
-
#
|
255 |
reply = _generate_reply(ui)
|
256 |
if is_sexual_or_toxic(reply):
|
257 |
reply = REFUSAL
|
|
|
66 |
tokenizer=tokenizer,
|
67 |
trust_remote_code=True,
|
68 |
return_full_text=False,
|
69 |
+
)
|
70 |
|
71 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
72 |
# Moderation & blocking (strict)
|
73 |
from transformers import TextClassificationPipeline
|
74 |
|
|
|
75 |
SEXUAL_TERMS = [
|
|
|
76 |
"sex","sexual","porn","nsfw","fetish","kink","bdsm","nude","naked","anal",
|
77 |
"blowjob","handjob","cum","breast","boobs","vagina","penis","semen","ejaculate",
|
78 |
"doggy","missionary","cowgirl","69","kamasutra","dominatrix","submissive","spank",
|
|
|
79 |
"sex position","have sex","make love","how to flirt","dominant in bed",
|
80 |
]
|
81 |
|
82 |
def _bad_words_ids(tokenizer, terms: List[str]) -> List[List[int]]:
|
|
|
|
|
|
|
|
|
83 |
ids = set()
|
84 |
for t in terms:
|
85 |
+
for v in (t, " " + t):
|
86 |
+
toks = tokenizer(v, add_special_tokens=False).input_ids
|
87 |
if toks:
|
88 |
ids.add(tuple(toks))
|
89 |
return [list(t) for t in ids]
|
90 |
|
91 |
BAD_WORD_IDS = _bad_words_ids(tokenizer, SEXUAL_TERMS)
|
92 |
|
|
|
93 |
nsfw_cls: TextClassificationPipeline = pipeline(
|
94 |
"text-classification",
|
95 |
model="eliasalbouzidi/distilbert-nsfw-text-classifier",
|
96 |
truncation=True,
|
97 |
+
)
|
98 |
|
99 |
toxicity_cls: TextClassificationPipeline = pipeline(
|
100 |
"text-classification",
|
101 |
model="unitary/toxic-bert",
|
102 |
truncation=True,
|
103 |
return_all_scores=True,
|
104 |
+
)
|
105 |
|
106 |
def is_sexual_or_toxic(text: str) -> bool:
|
107 |
t = (text or "").lower()
|
|
|
109 |
return True
|
110 |
try:
|
111 |
res = nsfw_cls(t)[0]
|
112 |
+
if (res.get("label","").lower() == "nsfw") and float(res.get("score",0)) > 0.60:
|
|
|
|
|
113 |
return True
|
114 |
except Exception:
|
115 |
pass
|
116 |
try:
|
117 |
+
scores = toxicity_cls(t)[0]
|
118 |
if any(item["score"] > 0.60 and item["label"].lower() in
|
119 |
{"toxic","severe_toxic","obscene","threat","insult","identity_hate"} for item in scores):
|
120 |
return True
|
|
|
126 |
"(orders, shipping, ETA, tracking, returns, warranty, account).")
|
127 |
|
128 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
129 |
+
# Memory (kept simple)
|
130 |
memory = ConversationBufferMemory(return_messages=True)
|
131 |
|
132 |
# System prompt = domain guardrails
|
|
|
169 |
return msgs
|
170 |
|
171 |
def _generate_reply(user_input: str) -> str:
|
|
|
172 |
messages = _lc_to_messages() + [{"role": "user", "content": user_input}]
|
173 |
prompt = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
|
174 |
out = chat_pipe(
|
175 |
prompt,
|
176 |
eos_token_id=tokenizer.eos_token_id,
|
177 |
pad_token_id=tokenizer.pad_token_id,
|
178 |
+
bad_words_ids=BAD_WORD_IDS,
|
179 |
**GEN_KW,
|
180 |
)[0]["generated_text"]
|
181 |
return out.strip()
|
|
|
187 |
if not ui:
|
188 |
return "How can I help with your order today?"
|
189 |
|
190 |
+
# 1) Safety first
|
191 |
if is_sexual_or_toxic(ui):
|
192 |
reply = REFUSAL
|
193 |
memory.save_context({"input": ui}, {"output": reply})
|
194 |
return reply
|
195 |
|
|
|
196 |
low = ui.lower()
|
|
|
|
|
|
|
|
|
197 |
|
198 |
+
# 2) Quick intents that don't depend on domain keywords
|
199 |
if any(tok in low for tok in ["thank you","thanks","thx"]):
|
200 |
reply = handle_gratitude()
|
201 |
memory.save_context({"input": ui}, {"output": reply})
|
|
|
205 |
memory.save_context({"input": ui}, {"output": reply})
|
206 |
return reply
|
207 |
|
208 |
+
# 3) *** ORDER NUMBER FIRST *** (so follow-ups like "It's #4567" work)
|
209 |
new_o = extract_order(ui)
|
210 |
if new_o:
|
211 |
stored_order = new_o
|
212 |
if pending_intent in ("status","eta","track","link"):
|
213 |
fn = {"status": handle_status,"eta": handle_eta,"track": handle_track,"link": handle_link}[pending_intent]
|
214 |
+
reply = fn(stored_order)
|
215 |
+
pending_intent = None
|
216 |
+
memory.save_context({"input": ui}, {"output": reply})
|
217 |
+
return reply
|
218 |
+
# No pending intent β fall through to classify what they want next.
|
219 |
+
|
220 |
+
# 4) Support-only guard (but SKIP if we just saw an order number or have a pending intent)
|
221 |
+
if pending_intent is None and new_o is None:
|
222 |
+
if not any(k in low for k in ALLOWED_KEYWORDS) and not any(k in low for k in ("hi","hello","hey")):
|
223 |
+
reply = "Iβm for store support only (orders, shipping, returns, warranty, account). How can I help with those?"
|
224 |
+
memory.save_context({"input": ui}, {"output": reply})
|
225 |
+
return reply
|
226 |
+
|
227 |
+
# 5) Intent classification
|
228 |
if any(k in low for k in ["status","where is my order","check status"]):
|
229 |
intent = "status"
|
230 |
elif any(k in low for k in ["how long","eta","delivery time"]):
|
|
|
236 |
else:
|
237 |
intent = "fallback"
|
238 |
|
239 |
+
# 6) Handle core intents
|
240 |
if intent in ("status","eta","track","link"):
|
241 |
if not stored_order:
|
242 |
pending_intent = intent
|
|
|
247 |
memory.save_context({"input": ui}, {"output": reply})
|
248 |
return reply
|
249 |
|
250 |
+
# 7) LLM fallback (still on-topic) + post-check for safety
|
251 |
reply = _generate_reply(ui)
|
252 |
if is_sexual_or_toxic(reply):
|
253 |
reply = REFUSAL
|