File size: 13,584 Bytes
10b0bc1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
"""
Workplace-Pulse-Telemetry: Occupational Psychometric & NLP Telemetry Engine
Engineered by Fabio Torres (neurodeveloper11)

Bridges Organizational Psychology (Maslach MBI, Amy Edmondson, Karasek JDC, Res. 2764/2022)
with high-performance lexical-syntactic natural language telemetry.
"""

import re
from datetime import datetime
from typing import List, Tuple, Dict, Any
from src.schemas import SanitizedMessage, MessageTelemetry, KarasekQuadrant


class OccupationalTelemetryEngine:
    """
    Computes occupational health and organizational climate indicators from sanitized text:
    - OSBI: Occupational Stress & Burnout Index (0 - 100)
    - PSI: Psychological Safety Index (Amy Edmondson Model) (0 - 100)
    - IFCI: Interpersonal Friction & Climate Index (0 - 100)
    - Demand & Autonomy for Karasek Matrix (0 - 100)
    - Emotional Valence (-1.0 to +1.0)
    """

    def __init__(self):
        # 1. Burnout & Exhaustion Markers (Maslach MBI dimension: Emotional Exhaustion)
        self.exhaustion_terms = [
            r"\bagotad[oa]s?\b", r"\bno doy m[aá]s\b", r"\bquemad[oa]s?\b",
            r"\bcolaps[aoó]ndo?\b", r"\bdrenad[oa]s?\b", r"\bsin energ[ií]a\b",
            r"\bal l[ií]mite\b", r"\bsaturad[oa]s?\b", r"\bsobrecarga(?:da)?\b",
            r"\binsomnio\b", r"\bestr[eé]s\b", r"\bdesbordad[oa]s?\b",
            r"\bexhausted\b", r"\bburn(?:ed|ing)? out\b", r"\boverwhelmed\b",
            r"\bdrained\b", r"\bbreaking point\b", r"\bcan'?t take this\b"
        ]
        self.exhaustion_regex = re.compile("|".join(self.exhaustion_terms), re.IGNORECASE)

        # 2. Cynicism & Depersonalization Markers (MBI dimension: Depersonalization)
        self.cynicism_terms = [
            r"\bda igual\b", r"\bme da lo mismo\b", r"\bqu[eé] m[aá]s da\b",
            r"\bno me pagan lo suficiente\b", r"\bno es mi problema\b",
            r"\bhagan lo que quieran\b", r"\bpara qu[eé]\b", r"\bno vale la pena\b",
            r"\bwhatever\b", r"\bnot my problem\b", r"\bwho cares\b", r"\bdon'?t care\b"
        ]
        self.cynicism_regex = re.compile("|".join(self.cynicism_terms), re.IGNORECASE)

        # 3. Urgency & Panic Pacing Markers
        self.urgency_terms = [
            r"\burgente\b", r"\bya mismo\b", r"\bpara ayer\b", r"\bemergencia\b",
            r"\basap\b", r"\bapaga(?:ndo)? incendios\b", r"\binmediat[ao](?:mente)?\b",
            r"\bahora mismo\b", r"\bbloqueante\b", r"\bcr[ií]tic[ao]\b",
            r"\bse cay[oó]\b", r"\burgently?\b", r"\bemergency\b", r"\bright now\b"
        ]
        self.urgency_regex = re.compile("|".join(self.urgency_terms), re.IGNORECASE)

        # 4. Amy Edmondson - Psychological Safety Positive Indicators
        # A. Vulnerability & Error Admission
        self.vulnerability_terms = [
            r"\bme equivoqu[eé]\b", r"\bcomet[ií] un error\b", r"\bfue mi error\b",
            r"\bnecesito ayuda\b", r"\bno s[eé] c[oó]mo\b", r"\bpido disculpas\b",
            r"\bme hago cargo\b", r"\bi made a mistake\b", r"\bmy bad\b",
            r"\bi was wrong\b", r"\bneed help\b", r"\bapologies\b"
        ]
        self.vulnerability_regex = re.compile("|".join(self.vulnerability_terms), re.IGNORECASE)

        # B. Inquiry, Curiosity & Open Questions
        self.inquiry_terms = [
            r"\bqu[eé] opinan\b", r"\bc[oó]mo lo ven\b", r"\balguien tiene dudas\b",
            r"\bsugerencias\b", r"\bqu[eé] riesgos ven\b", r"\bfeedback\b",
            r"\bqu[eé] piensan\b", r"\bwhat do you think\b", r"\bany ideas\b",
            r"\bopen to feedback\b", r"\bany suggestions\b"
        ]
        self.inquiry_regex = re.compile("|".join(self.inquiry_terms), re.IGNORECASE)

        # C. Constructive Disagreement & Alternative Proposals
        self.constructive_terms = [
            r"\bpropongo\b", r"\botra alternativa\b", r"\botra perspectiva\b",
            r"\by si probamos\b", r"\bdesde mi punto de vista\b",
            r"\bwhat if we try\b", r"\banother perspective\b", r"\balternative approach\b"
        ]
        self.constructive_regex = re.compile("|".join(self.constructive_terms), re.IGNORECASE)

        # 5. Interpersonal Friction & Passive Aggression
        self.friction_terms = [
            r"\bcomo ya (?:te )?hab[ií]a dicho\b", r"\bcomo ya dije\b",
            r"\bfavor leer el hilo\b", r"\bcomo mencion[eé] antes\b",
            r"\botra vez con lo mismo\b", r"\bno vuelvo a repetir\b",
            r"\bno es mi trabajo\b", r"\beso te correspond[ií]a\b",
            r"\bas per my last (?:email|message)\b", r"\bas stated previously\b",
            r"\bread carefully\b", r"\bnot my job\b", r"\bi shouldn'?t have to repeat\b"
        ]
        self.friction_regex = re.compile("|".join(self.friction_terms), re.IGNORECASE)

        # 6. Prosocial & Collaboration Indicators
        self.prosocial_terms = [
            r"\bgracias\b", r"\bmuchas gracias\b", r"\bgran trabajo\b",
            r"\bfelicitaciones\b", r"\btrabajemos juntos\b", r"\bcon gusto\b",
            r"\ba la orden\b", r"\bte ayudo\b", r"\bte apoyo\b",
            r"\bthank you\b", r"\bgreat work\b", r"\bhappy to help\b",
            r"\bteam effort\b", r"\bwell done\b", r"\bkudos\b"
        ]
        self.prosocial_regex = re.compile("|".join(self.prosocial_terms), re.IGNORECASE)

        # 7. Autonomy & Decision Latitude (Karasek Control axis)
        self.autonomy_terms = [
            r"\bdecid[ií]\b", r"\bpodemos elegir\b", r"\btengo autonom[ií]a\b",
            r"\bresolv[ií]\b", r"\bflexibilidad\b", r"\bautogesti[oó]n\b",
            r"\bwe decided\b", r"\bi chose\b", r"\bautonomous\b", r"\bempowered\b"
        ]
        self.autonomy_regex = re.compile("|".join(self.autonomy_terms), re.IGNORECASE)

        # 8. Micromanagement & Rigid Command (Suppresses autonomy & psychological safety)
        self.command_terms = [
            r"\btienes que hacer\b", r"\bdebes enviar ya\b", r"\best[aá] prohibido\b",
            r"\bes obligatorio\b", r"\bhaz lo que digo\b", r"\bsin excepciones\b",
            r"\byou must\b", r"\bmandatory\b", r"\bdo as i said\b", r"\bno excuses\b"
        ]
        self.command_regex = re.compile("|".join(self.command_terms), re.IGNORECASE)

    def is_after_hours(self, dt: datetime) -> bool:
        """
        Determines whether a message was sent outside of normal occupational hours.
        Standard business window: Monday-Friday, 07:00 to 19:00.
        """
        # Weekday: 0=Monday, 6=Sunday. Saturday=5, Sunday=6
        if dt.weekday() in [5, 6]:
            return True
        if dt.hour < 7 or dt.hour >= 19:
            return True
        return False

    def compute_telemetry(self, msg: SanitizedMessage) -> MessageTelemetry:
        """
        Processes a sanitized message and calculates its full psychometric telemetry vector.
        """
        text = msg.sanitized_content
        after_hours = self.is_after_hours(msg.timestamp)
        markers_found: List[str] = []

        # Find matches
        exhaustion_matches = self.exhaustion_regex.findall(text)
        cynicism_matches = self.cynicism_regex.findall(text)
        urgency_matches = self.urgency_regex.findall(text)
        vulnerability_matches = self.vulnerability_regex.findall(text)
        inquiry_matches = self.inquiry_regex.findall(text)
        constructive_matches = self.constructive_regex.findall(text)
        friction_matches = self.friction_regex.findall(text)
        prosocial_matches = self.prosocial_regex.findall(text)
        autonomy_matches = self.autonomy_regex.findall(text)
        command_matches = self.command_regex.findall(text)

        # Register markers
        if exhaustion_matches:
            markers_found.append(f"Exhaustion({len(exhaustion_matches)})")
        if cynicism_matches:
            markers_found.append(f"Cynicism({len(cynicism_matches)})")
        if urgency_matches:
            markers_found.append(f"Urgency({len(urgency_matches)})")
        if vulnerability_matches:
            markers_found.append(f"Vulnerability({len(vulnerability_matches)})")
        if inquiry_matches:
            markers_found.append(f"Inquiry({len(inquiry_matches)})")
        if constructive_matches:
            markers_found.append(f"Constructive({len(constructive_matches)})")
        if friction_matches:
            markers_found.append(f"Friction({len(friction_matches)})")
        if prosocial_matches:
            markers_found.append(f"Prosocial({len(prosocial_matches)})")
        if after_hours:
            markers_found.append("AfterHoursTraffic")

        # Syntactic indicators: ALL-CAPS intensity & multiple exclamation marks
        words = text.split()
        caps_words = [w for w in words if w.isupper() and len(w) >= 4 and not w.startswith("USER_")]
        caps_penalty = min(25.0, len(caps_words) * 8.0)
        exclamation_count = len(re.findall(r"!{2,}", text))
        exclamation_penalty = min(15.0, exclamation_count * 7.5)

        # -------------------------------------------------------------
        # 1. OSBI: Occupational Stress & Burnout Index (0 - 100)
        # -------------------------------------------------------------
        # Baseline neutral stress: 15.0
        base_stress = 15.0
        stress_score = (
            base_stress
            + (len(exhaustion_matches) * 28.0)
            + (len(urgency_matches) * 18.0)
            + (len(cynicism_matches) * 22.0)
            + (18.0 if after_hours else 0.0)
            + caps_penalty
            + exclamation_penalty
            - (len(prosocial_matches) * 8.0)
        )
        stress_score = max(0.0, min(100.0, stress_score))

        # -------------------------------------------------------------
        # 2. PSI: Psychological Safety Index (Amy Edmondson Model) (0 - 100)
        # -------------------------------------------------------------
        # Baseline safety in typical professional interaction: 55.0
        base_psi = 55.0
        psi_score = (
            base_psi
            + (len(vulnerability_matches) * 22.0)
            + (len(inquiry_matches) * 16.0)
            + (len(constructive_matches) * 14.0)
            + (len(prosocial_matches) * 10.0)
            - (len(friction_matches) * 25.0)
            - (len(command_matches) * 15.0)
            - (caps_penalty * 0.5)
        )
        psi_score = max(0.0, min(100.0, psi_score))

        # -------------------------------------------------------------
        # 3. IFCI: Interpersonal Friction & Climate Index (0 - 100)
        # -------------------------------------------------------------
        # Baseline friction: 10.0
        base_friction = 10.0
        friction_score = (
            base_friction
            + (len(friction_matches) * 32.0)
            + (len(cynicism_matches) * 16.0)
            + (len(command_matches) * 14.0)
            + (caps_penalty * 0.6)
            - (len(prosocial_matches) * 15.0)
            - (len(vulnerability_matches) * 10.0)
        )
        friction_score = max(0.0, min(100.0, friction_score))

        # -------------------------------------------------------------
        # 4. Karasek Model Dimensions: Demand & Autonomy
        # -------------------------------------------------------------
        # Demand: combination of task urgency, after hours, and exhaustion load
        base_demand = 20.0
        demand_score = (
            base_demand
            + (len(urgency_matches) * 20.0)
            + (len(exhaustion_matches) * 18.0)
            + (25.0 if after_hours else 0.0)
            + caps_penalty
        )
        demand_score = max(0.0, min(100.0, demand_score))

        # Autonomy: combination of decision agency and inquiries minus rigid commands
        base_autonomy = 45.0
        autonomy_score = (
            base_autonomy
            + (len(autonomy_matches) * 25.0)
            + (len(constructive_matches) * 15.0)
            + (len(inquiry_matches) * 10.0)
            - (len(command_matches) * 22.0)
        )
        autonomy_score = max(0.0, min(100.0, autonomy_score))

        # -------------------------------------------------------------
        # 5. Emotional Valence (-1.0 to +1.0)
        # -------------------------------------------------------------
        pos_tokens = len(prosocial_matches) + len(vulnerability_matches) + len(constructive_matches)
        neg_tokens = len(exhaustion_matches) + len(cynicism_matches) + len(friction_matches) + len(urgency_matches)
        total_valence_tokens = pos_tokens + neg_tokens

        if total_valence_tokens == 0:
            emotional_valence = 0.0
        else:
            raw_valence = (pos_tokens - neg_tokens) / (total_valence_tokens + 1.5)
            emotional_valence = max(-1.0, min(1.0, round(raw_valence, 2)))

        return MessageTelemetry(
            message_id=msg.message_id,
            sender_pseudonym=msg.sender_pseudonym,
            department=msg.department,
            timestamp=msg.timestamp,
            stress_urgency_score=round(stress_score, 1),
            psychological_safety_score=round(psi_score, 1),
            friction_score=round(friction_score, 1),
            autonomy_score=round(autonomy_score, 1),
            demand_score=round(demand_score, 1),
            after_hours_flag=after_hours,
            emotional_valence=emotional_valence,
            detected_markers=markers_found
        )

    def compute_batch(self, messages: List[SanitizedMessage]) -> List[MessageTelemetry]:
        """Calculates telemetry for a batch of sanitized messages."""
        return [self.compute_telemetry(m) for m in messages]