Asmitha-28 commited on
Commit
92e130f
ยท
verified ยท
1 Parent(s): 377dd22

Upload src\ticket_validator.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. src//ticket_validator.py +253 -0
src//ticket_validator.py ADDED
@@ -0,0 +1,253 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # src/ticket_validator.py
2
+ # Ticket Input Validator โ€” Edge Case Handler
3
+ # SupportMind v1.0 โ€” Asmitha
4
+
5
+ import re
6
+ import logging
7
+ from typing import Dict, Tuple
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+ # โ”€โ”€ Constants โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
12
+ MIN_WORDS = 4
13
+ MAX_WORDS = 500
14
+ MIN_CHARS = 10
15
+ MAX_CHARS = 3000
16
+
17
+ # Non-English character detection
18
+ # Covers: Arabic, Hindi/Devanagari, Tamil, Chinese,
19
+ # Japanese, Korean, Thai, Russian
20
+ NON_LATIN_PATTERN = re.compile(
21
+ r'[\u0600-\u06FF' # Arabic
22
+ r'\u0900-\u097F' # Devanagari (Hindi)
23
+ r'\u0B80-\u0BFF' # Tamil
24
+ r'\u4E00-\u9FFF' # Chinese
25
+ r'\u3040-\u30FF' # Japanese
26
+ r'\uAC00-\uD7AF' # Korean
27
+ r'\u0E00-\u0E7F' # Thai
28
+ r'\u0400-\u04FF]' # Russian/Cyrillic
29
+ )
30
+
31
+ # Gibberish detection
32
+ # No vowels in long sequences = likely gibberish
33
+ GIBBERISH_PATTERN = re.compile(r'\b[^aeiou\s]{6,}\b', re.IGNORECASE)
34
+
35
+
36
+ # Already resolved patterns
37
+ RESOLVED_PATTERNS = [
38
+ r'never ?mind',
39
+ r'problem (?:is )?(?:solved|fixed|resolved)',
40
+ r'(?:sorted|fixed) (?:it )?(?:out)?',
41
+ r'no longer (?:need|require)',
42
+ r'cancel (?:this )?(?:ticket|request)',
43
+ r'disregard',
44
+ r'ignore (?:this|my)',
45
+ r'thanks?,? (?:got it|all good|figured)',
46
+ ]
47
+
48
+ # Greeting/test patterns
49
+ GREETING_PATTERNS = [
50
+ r'^hi+\s*[.!?]*$',
51
+ r'^hello+\s*[.!?]*$',
52
+ r'^hey+\s*[.!?]*$',
53
+ r'^test\s*[.!?]*$',
54
+ r'^testing\s*[.!?]*$',
55
+ r'^help\s*[.!?]*$',
56
+ r'^\?\s*$',
57
+ r'^\.+$',
58
+ ]
59
+
60
+ # Abuse/spam patterns (basic)
61
+ SPAM_PATTERNS = [
62
+ r'(.)\1{9,}', # Same char repeated 10+ times
63
+ r'(\b\w+\b)(\s+\1){4,}', # Same word repeated 5+ times
64
+ ]
65
+
66
+
67
+ class TicketValidator:
68
+ """
69
+ Validates and pre-processes ticket text before ML inference.
70
+
71
+ Catches edge cases early so the ML pipeline never receives
72
+ bad input. Returns structured validation result with
73
+ specific response for each edge case.
74
+ """
75
+
76
+ def validate(self, text: str) -> Dict:
77
+ """
78
+ Validate ticket text and return result.
79
+
80
+ Returns:
81
+ {
82
+ 'valid': bool,
83
+ 'cleaned_text': str, # cleaned version if valid
84
+ 'error_type': str | None, # type of error if invalid
85
+ 'response': str, # what to show user
86
+ 'should_route': bool, # proceed to ML?
87
+ }
88
+ """
89
+
90
+ # โ”€โ”€ Check 1: Empty or None โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
91
+ if not text or not text.strip():
92
+ return self._invalid(
93
+ error_type='empty',
94
+ response="It looks like your message is empty. "
95
+ "Please describe your issue and we'll help you right away."
96
+ )
97
+
98
+ # Clean whitespace
99
+ cleaned = ' '.join(text.strip().split())
100
+
101
+ # โ”€โ”€ Check 2: Too short โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
102
+ words = cleaned.split()
103
+ if len(words) < MIN_WORDS or len(cleaned) < MIN_CHARS:
104
+ # Check if it's a greeting specifically
105
+ if any(re.match(p, cleaned.lower()) for p in GREETING_PATTERNS):
106
+ return self._invalid(
107
+ error_type='greeting',
108
+ response="Hi there! ๐Ÿ‘‹ Could you describe the issue "
109
+ "you're experiencing? We're here to help."
110
+ )
111
+ return self._invalid(
112
+ error_type='too_short',
113
+ response="Could you share a bit more detail about your issue? "
114
+ "The more context you provide, the faster we can help."
115
+ )
116
+
117
+ # โ”€โ”€ Check 3: Too long โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
118
+ if len(words) > MAX_WORDS or len(cleaned) > MAX_CHARS:
119
+ # Truncate intelligently โ€” keep first 500 words
120
+ truncated_words = words[:MAX_WORDS]
121
+ cleaned = ' '.join(truncated_words)
122
+ logger.info(f"Ticket truncated from {len(words)} to {MAX_WORDS} words")
123
+ # Still valid โ€” just truncated
124
+ return self._valid(
125
+ cleaned_text=cleaned,
126
+ warning="Your message was very long โ€” "
127
+ "we've focused on the first part to route you correctly."
128
+ )
129
+
130
+ # โ”€โ”€ Check 4: Non-English โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
131
+ non_latin_chars = len(NON_LATIN_PATTERN.findall(cleaned))
132
+ total_chars = len(re.sub(r'\s', '', cleaned))
133
+ non_latin_ratio = non_latin_chars / max(total_chars, 1)
134
+
135
+ if non_latin_ratio > 0.3:
136
+ language = self._detect_language_hint(cleaned)
137
+ return self._invalid(
138
+ error_type='non_english',
139
+ response=f"We noticed your message may be in another language. "
140
+ f"Our routing system currently works best in English. "
141
+ f"Could you resend your message in English? "
142
+ f"We want to make sure you reach the right team quickly."
143
+ )
144
+
145
+ # โ”€โ”€ Check 5: Already resolved โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
146
+ if any(re.search(p, cleaned.lower()) for p in RESOLVED_PATTERNS):
147
+ return self._invalid(
148
+ error_type='resolved',
149
+ response="Glad to hear it's sorted! ๐Ÿ˜Š "
150
+ "If you need anything else, don't hesitate to reach out.",
151
+ should_route=False
152
+ )
153
+
154
+ # โ”€โ”€ Check 6: Gibberish โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
155
+ gibberish_matches = GIBBERISH_PATTERN.findall(cleaned)
156
+ total_words = len(words)
157
+ gibberish_ratio = len(gibberish_matches) / max(total_words, 1)
158
+
159
+ if gibberish_ratio >= 0.4:
160
+ return self._invalid(
161
+ error_type='gibberish',
162
+ response="We couldn't quite understand your message. "
163
+ "Could you describe your issue in plain language?"
164
+ )
165
+
166
+ # โ”€โ”€ Check 7: Spam โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
167
+ for pattern in SPAM_PATTERNS:
168
+ if re.search(pattern, cleaned):
169
+ return self._invalid(
170
+ error_type='spam',
171
+ response="We weren't able to process your message. "
172
+ "Please describe your issue clearly."
173
+ )
174
+
175
+ # โ”€โ”€ Check 8: Only numbers/symbols โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
176
+ alpha_chars = len(re.findall(r'[a-zA-Z]', cleaned))
177
+ if alpha_chars < 5:
178
+ return self._invalid(
179
+ error_type='no_text',
180
+ response="Could you describe your issue in words? "
181
+ "We want to make sure you reach the right team."
182
+ )
183
+
184
+ # โ”€โ”€ All checks passed โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
185
+ return self._valid(cleaned_text=cleaned)
186
+
187
+ def _valid(self, cleaned_text: str, warning: str = None) -> Dict:
188
+ return {
189
+ 'valid': True,
190
+ 'cleaned_text': cleaned_text,
191
+ 'error_type': None,
192
+ 'response': warning,
193
+ 'should_route': True,
194
+ 'warning': warning is not None,
195
+ }
196
+
197
+ def _invalid(self,
198
+ error_type: str,
199
+ response: str,
200
+ should_route: bool = False) -> Dict:
201
+ return {
202
+ 'valid': False,
203
+ 'cleaned_text': None,
204
+ 'error_type': error_type,
205
+ 'response': response,
206
+ 'should_route': should_route,
207
+ 'warning': False,
208
+ }
209
+
210
+ def _detect_language_hint(self, text: str) -> str:
211
+ """Basic language hint for logging."""
212
+ if re.search(r'[\u0B80-\u0BFF]', text):
213
+ return 'Tamil'
214
+ if re.search(r'[\u0900-\u097F]', text):
215
+ return 'Hindi'
216
+ if re.search(r'[\u0600-\u06FF]', text):
217
+ return 'Arabic'
218
+ if re.search(r'[\u4E00-\u9FFF]', text):
219
+ return 'Chinese'
220
+ if re.search(r'[\uAC00-\uD7AF]', text):
221
+ return 'Korean'
222
+ return 'Unknown'
223
+
224
+
225
+ # โ”€โ”€ Quick test โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
226
+ if __name__ == '__main__':
227
+ validator = TicketValidator()
228
+
229
+ test_cases = [
230
+ ("hi", "greeting"),
231
+ ("", "empty"),
232
+ (" ", "empty"),
233
+ ("asdfghjkl qwerty zxcvbnm poiuytrewq", "gibberish"),
234
+ ("เฎŽเฎฉเฎคเฏ เฎ•เฎฃเฎ•เฏเฎ•เฎฟเฎฒเฏ เฎšเฎฟเฎ•เฏเฎ•เฎฒเฏ เฎ‰เฎณเฏเฎณเฎคเฏ", "tamil"),
235
+ ("My invoice is wrong please help me fix this billing issue", "valid"),
236
+ ("never mind got it sorted thanks", "resolved"),
237
+ ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "spam"),
238
+ ("500 404 200 301 302", "no_text"),
239
+ ("The API endpoint returns 500 error " * 200, "too_long"),
240
+ ]
241
+
242
+ print("=" * 60)
243
+ print("TICKET VALIDATOR โ€” EDGE CASE TESTS")
244
+ print("=" * 60)
245
+
246
+ for text, expected in test_cases:
247
+ result = validator.validate(text)
248
+ status = "[OK]" if not result['valid'] or result['valid'] else "[ERROR]"
249
+ preview = text[:40] + "..." if len(text) > 40 else text
250
+ print(f"\nInput: '{preview}'")
251
+ print(f"Expected: {expected}")
252
+ print(f"Got: {result['error_type'] or 'valid'}")
253
+ print(f"Response: {result['response']}")