Asmitha-28 commited on
Commit
4f7eaa5
·
verified ·
1 Parent(s): de4b110

Upload src\feature_extraction.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. src//feature_extraction.py +339 -0
src//feature_extraction.py ADDED
@@ -0,0 +1,339 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # src/feature_extraction.py
2
+ # Feature Extraction Module — Multi-signal ticket analysis
3
+ # SupportMind v1.0 — Asmitha
4
+
5
+ import re
6
+ import logging
7
+ from typing import Dict
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+ try:
12
+ from vaderSentiment.vaderSentiment import SentimentIntensityAnalyzer
13
+ HAS_VADER = True
14
+ except ImportError:
15
+ HAS_VADER = False
16
+
17
+ CRITICAL_URGENCY = [
18
+ 'crash', 'blocked', 'down', 'failing', 'cannot access', 'production issue',
19
+ 'outage', 'emergency', 'critical', 'urgent', 'immediately', 'blocking', 'locked out',
20
+ ]
21
+
22
+ GENERAL_URGENCY = [
23
+ 'asap', 'deadline', 'sla', 'escalate', 'priority', 'time-sensitive', 'showstopper', 'presentation',
24
+ ]
25
+
26
+ CONTEXTUAL_URGENCY_SIGNALS = [
27
+ (
28
+ 'business_impact',
29
+ 0.30,
30
+ [
31
+ r'\b(?:affecting|impacting|blocking)\s+(?:our\s+)?(?:customers|users|team|business|operations|sales|revenue|payroll|launch|production)\b',
32
+ r'\b(?:customers?|clients?)\s+(?:(?:are|is)\s+)?(?:waiting|blocked|affected|unable)\b',
33
+ r"\b(?:cannot|can't|unable to)\s+(?:process|ship|launch|serve|sell|invoice|onboard|work|access)\b",
34
+ ],
35
+ ),
36
+ (
37
+ 'deadline_pressure',
38
+ 0.25,
39
+ [
40
+ r'\b(?:in|within)\s+\d+\s*(?:min|mins|minutes|hour|hours|hrs|days?)\b',
41
+ r'\b(?:by|before)\s+(?:today|tomorrow|eod|end of day|tonight|monday|tuesday|wednesday|thursday|friday|saturday|sunday)\b',
42
+ r'\b(?:launch|demo|go-live|renewal|payroll|board meeting|presentation)\b',
43
+ ],
44
+ ),
45
+ (
46
+ 'production_outage',
47
+ 0.40,
48
+ [
49
+ r'\bproduction\s+(?:is\s+)?(?:down|blocked|broken|failing|impacted)\b',
50
+ r'\b(?:all|multiple|many)\s+(?:users|customers|accounts|teams)\s+(?:are\s+)?(?:affected|blocked|down|unable)\b',
51
+ r'\b(?:system|service|platform|dashboard|api)\s+(?:is\s+)?(?:down|unavailable|not responding)\b',
52
+ ],
53
+ ),
54
+ (
55
+ 'access_loss',
56
+ 0.25,
57
+ [
58
+ r"\b(?:locked out|cannot access|can't access|unable to access|access is blocked)\b",
59
+ r'\b(?:login|sso|authentication)\s+(?:is\s+)?(?:broken|failing|down|not working)\b',
60
+ ],
61
+ ),
62
+ (
63
+ 'repeat_issue',
64
+ 0.20,
65
+ [
66
+ r'\b(?:again|still|keeps?|repeated|recurring)\b',
67
+ r'\b(?:second|third|fourth)\s+time\b',
68
+ r'\b(?:raised|reported|opened)\s+(?:this\s+)?(?:before|multiple times|again)\b',
69
+ ],
70
+ ),
71
+ ]
72
+
73
+ DEESCALATION_PATTERNS = [
74
+ r'\bnot urgent\b',
75
+ r'\bno rush\b',
76
+ r'\bwhenever you can\b',
77
+ r'\bwhen you have time\b',
78
+ ]
79
+
80
+ NEGATIVE_SENTIMENT_SIGNALS = [
81
+ (
82
+ 'frustration',
83
+ -0.30,
84
+ [
85
+ r'\bfrustrat(?:ed|ing|ion)\b',
86
+ r'\bnot happy\b',
87
+ r'\bdisappoint(?:ed|ing|ment)\b',
88
+ r'\bthis is becoming difficult\b',
89
+ r'\bnot ideal\b',
90
+ r'\bunacceptable\b',
91
+ r'\bterrible\b',
92
+ r'\bawful\b',
93
+ ],
94
+ ),
95
+ (
96
+ 'trust_risk',
97
+ -0.25,
98
+ [
99
+ r'\b(?:losing|lost)\s+(?:trust|confidence)\b',
100
+ r'\b(?:considering|thinking about)\s+(?:switching|leaving|cancelling|canceling)\b',
101
+ ],
102
+ ),
103
+ (
104
+ 'polite_negative',
105
+ -0.22,
106
+ [
107
+ r'\b(?:this|it)\s+is\s+(?:affecting|impacting|blocking)\b',
108
+ r'\b(?:could you please|please)\b.*\b(?:fix|resolve|help)\b.*\b(?:blocking|affecting|stuck|broken|failing)\b',
109
+ r'\b(?:becoming|getting)\s+(?:difficult|hard|painful)\b',
110
+ ],
111
+ ),
112
+ ]
113
+
114
+ POSITIVE_SENTIMENT_SIGNALS = [
115
+ (
116
+ 'appreciation',
117
+ 0.08,
118
+ [
119
+ r'\bthanks?\b',
120
+ r'\bthank you\b',
121
+ r'\bappreciate\b',
122
+ ],
123
+ ),
124
+ ]
125
+
126
+ COMPLEXITY_KEYWORDS = [
127
+ 'integration', 'migration', 'sso', 'bulk', 'setup', 'configure', 'synchronization',
128
+ 'permissions', 'architecture', 'implementation', 'customization',
129
+ ]
130
+
131
+ MULTI_INTENT_KEYWORDS = ['also', 'and', 'additionally', 'moreover', 'furthermore', 'plus']
132
+
133
+ PRODUCT_KEYWORDS = {
134
+ 'dashboard': 'Dashboard',
135
+ 'api': 'API',
136
+ 'sso': 'SSO',
137
+ 'export': 'Export',
138
+ 'integration': 'Integration'
139
+ }
140
+
141
+
142
+ class FeatureExtractor:
143
+ """
144
+ Extracts multi-signal features from raw ticket text.
145
+
146
+ Features:
147
+ - Sentiment score (VADER or fallback)
148
+ - Urgency score (Operational danger)
149
+ - Complexity score (Implementation difficulty)
150
+ - Product/feature entity recognition
151
+ - Text complexity (Flesch-Kincaid approximation)
152
+ - Token count
153
+ - Named entities (basic regex-based NER)
154
+ """
155
+
156
+ def __init__(self):
157
+ self.sentiment_analyzer = SentimentIntensityAnalyzer() if HAS_VADER else None
158
+
159
+ def extract(self, text: str) -> Dict:
160
+ """Extract all features from ticket text."""
161
+ text_lower = text.lower()
162
+ words = text.split()
163
+ sentences = [s.strip() for s in re.split(r'[.!?]+', text) if s.strip()]
164
+
165
+ urgency = self._urgency_details(text_lower)
166
+ sentiment = self._sentiment_details(text)
167
+
168
+ return {
169
+ 'sentiment_score': sentiment['score'],
170
+ 'sentiment_label': sentiment['label'],
171
+ 'sentiment_evidence': sentiment['evidence'],
172
+ 'sentiment_raw_score': sentiment['raw_score'],
173
+ 'urgency_flags': urgency['flags'],
174
+ 'urgency_score': urgency['score'],
175
+ 'urgency_level': urgency['level'],
176
+ 'urgency_evidence': urgency['evidence'],
177
+ 'complexity_score': self._calculate_complexity(text_lower),
178
+ 'product_entities': self._product_entities(text_lower),
179
+ 'text_complexity_score': self._flesch_kincaid(words, sentences),
180
+ 'token_count': len(words),
181
+ 'sentence_count': len(sentences),
182
+ 'has_question': '?' in text,
183
+ 'has_error_code': bool(re.search(r'error\s*(?:code\s*)?[\d#:]+|err[-_]\d+|HTTP\s*\d{3}', text, re.I)),
184
+ 'has_multi_intent_signal': any(kw in text_lower for kw in MULTI_INTENT_KEYWORDS),
185
+ 'email_mentions': len(re.findall(r'[\w.+-]+@[\w-]+\.[\w.]+', text)),
186
+ 'url_mentions': len(re.findall(r'https?://\S+', text)),
187
+ 'mentioned_dates': bool(re.search(r'\b\d{1,2}[/-]\d{1,2}[/-]\d{2,4}\b|\blast\s+(?:week|month|tuesday|monday|wednesday|thursday|friday)\b', text_lower)),
188
+ }
189
+
190
+ def _sentiment(self, text: str) -> float:
191
+ return self._sentiment_details(text)['score']
192
+
193
+ def _sentiment_details(self, text: str) -> Dict:
194
+ tl = text.lower()
195
+ if self.sentiment_analyzer:
196
+ score = self.sentiment_analyzer.polarity_scores(text)['compound']
197
+ else:
198
+ neg = ['bad','terrible','broken','frustrated','angry','worst','hate','useless', 'invalid', 'locked out']
199
+ pos = ['good','great','love','excellent','amazing','helpful','thanks']
200
+ n = sum(1 for w in neg if w in tl)
201
+ p = sum(1 for w in pos if w in tl)
202
+ score = (p - n) / max(p + n, 1)
203
+
204
+ raw_score = score
205
+ adjustment, evidence = self._score_pattern_signals(tl, NEGATIVE_SENTIMENT_SIGNALS)
206
+ positive_adjustment, positive_evidence = self._score_pattern_signals(tl, POSITIVE_SENTIMENT_SIGNALS)
207
+
208
+ # Polite support messages often include "thanks" while still expressing risk.
209
+ if evidence:
210
+ positive_adjustment *= 0.35
211
+
212
+ if 'locked out' in tl:
213
+ adjustment -= 0.35
214
+ evidence.append('access_sentiment: locked out')
215
+ if 'invalid' in tl:
216
+ adjustment -= 0.20
217
+ evidence.append('error_sentiment: invalid')
218
+
219
+ score = max(min(score + adjustment + positive_adjustment, 1.0), -1.0)
220
+
221
+ if score <= -0.55 or any(e.startswith(('frustration', 'trust_risk')) for e in evidence):
222
+ label = 'frustrated'
223
+ elif score <= -0.20 or evidence:
224
+ label = 'concerned'
225
+ elif score >= 0.30:
226
+ label = 'positive'
227
+ else:
228
+ label = 'neutral'
229
+
230
+ return {
231
+ 'score': round(score, 4),
232
+ 'raw_score': round(raw_score, 4),
233
+ 'label': label,
234
+ 'evidence': evidence + positive_evidence,
235
+ }
236
+
237
+ def _urgency_flags(self, text_lower: str) -> list:
238
+ return self._urgency_details(text_lower)['flags']
239
+
240
+ def _calculate_urgency(self, text_lower: str) -> float:
241
+ """Operational danger score."""
242
+ return self._urgency_details(text_lower)['score']
243
+
244
+ def _urgency_details(self, text_lower: str) -> Dict:
245
+ critical_hits = [kw for kw in CRITICAL_URGENCY if kw in text_lower]
246
+ general_hits = [kw for kw in GENERAL_URGENCY if kw in text_lower]
247
+ contextual_score, contextual_evidence = self._score_pattern_signals(
248
+ text_lower,
249
+ CONTEXTUAL_URGENCY_SIGNALS,
250
+ )
251
+
252
+ evidence = []
253
+ evidence.extend([f'explicit_critical: {kw}' for kw in critical_hits])
254
+ evidence.extend([f'explicit_general: {kw}' for kw in general_hits])
255
+ evidence.extend(contextual_evidence)
256
+
257
+ score = (len(critical_hits) * 0.25) + (len(general_hits) * 0.12) + contextual_score
258
+ if any(re.search(p, text_lower) for p in DEESCALATION_PATTERNS):
259
+ score = min(score, 0.35)
260
+ evidence.append('deescalation: no immediate pressure')
261
+
262
+ score = round(min(max(score, 0.0), 1.0), 4)
263
+
264
+ if score >= 0.75:
265
+ level = 'critical'
266
+ elif score >= 0.50:
267
+ level = 'high'
268
+ elif score >= 0.25:
269
+ level = 'medium'
270
+ else:
271
+ level = 'low'
272
+
273
+ return {
274
+ 'score': score,
275
+ 'level': level,
276
+ 'flags': sorted(set(critical_hits + general_hits + [
277
+ e.split(':', 1)[0] for e in contextual_evidence
278
+ ])),
279
+ 'evidence': evidence,
280
+ }
281
+
282
+ def _score_pattern_signals(self, text_lower: str, signal_specs: list) -> tuple:
283
+ score = 0.0
284
+ evidence = []
285
+ for label, weight, patterns in signal_specs:
286
+ for pattern in patterns:
287
+ match = re.search(pattern, text_lower)
288
+ if match:
289
+ score += weight
290
+ evidence.append(f'{label}: {match.group(0)}')
291
+ break
292
+ return score, evidence
293
+
294
+ def _calculate_complexity(self, text_lower: str) -> float:
295
+ """Implementation difficulty score."""
296
+ comp_count = sum(1 for kw in COMPLEXITY_KEYWORDS if kw in text_lower)
297
+ score = comp_count * 0.25
298
+ return min(max(score, 0.0), 1.0)
299
+
300
+ def _product_entities(self, text_lower: str) -> list:
301
+ found = []
302
+ for kw, label in PRODUCT_KEYWORDS.items():
303
+ if kw in text_lower and label not in found:
304
+ found.append(label)
305
+ return found
306
+
307
+ def _flesch_kincaid(self, words: list, sentences: list) -> float:
308
+ if not words or not sentences:
309
+ return 0.0
310
+ avg_sentence_len = len(words) / len(sentences)
311
+ syllables = sum(self._count_syllables(w) for w in words)
312
+ avg_syllables = syllables / max(len(words), 1)
313
+ grade = 0.39 * avg_sentence_len + 11.8 * avg_syllables - 15.59
314
+ return round(max(0, grade), 2)
315
+
316
+ def _count_syllables(self, word: str) -> int:
317
+ word = word.lower().strip(".,!?;:'\"")
318
+ if len(word) <= 2:
319
+ return 1
320
+ vowels = 'aeiouy'
321
+ count = 0
322
+ prev_vowel = False
323
+ for ch in word:
324
+ is_vowel = ch in vowels
325
+ if is_vowel and not prev_vowel:
326
+ count += 1
327
+ prev_vowel = is_vowel
328
+ if word.endswith('e') and count > 1:
329
+ count -= 1
330
+ return max(count, 1)
331
+
332
+
333
+ if __name__ == '__main__':
334
+ ext = FeatureExtractor()
335
+ ticket = "Hey, we have been having issues with the export function since last Tuesday's update. Also our invoice from last month looks incorrect. Can someone help? We are considering upgrading but want this sorted first."
336
+ features = ext.extract(ticket)
337
+ for k, v in features.items():
338
+ print(f" {k}: {v}")
339
+