Spaces:
Running
on
Zero
Running
on
Zero
Update app.py
Browse files
app.py
CHANGED
@@ -604,14 +604,14 @@ def analyze_single_message(text, thresholds):
|
|
604 |
return abuse_score, threshold_labels, top_patterns, {"label": sentiment}, stage, darvo_score, tone_tag
|
605 |
|
606 |
def analyze_composite(msg1, date1, msg2, date2, msg3, date3, *answers_and_none):
|
|
|
|
|
607 |
none_selected_checked = answers_and_none[-1]
|
608 |
responses_checked = any(answers_and_none[:-1])
|
609 |
none_selected = not responses_checked and none_selected_checked
|
610 |
|
611 |
-
|
612 |
-
|
613 |
-
risk_level = "unknown"
|
614 |
-
else:
|
615 |
escalation_score = sum(w for (_, w), a in zip(ESCALATION_QUESTIONS, answers_and_none[:-1]) if a)
|
616 |
|
617 |
messages = [msg1, msg2, msg3]
|
@@ -620,22 +620,18 @@ def analyze_composite(msg1, date1, msg2, date2, msg3, date3, *answers_and_none):
|
|
620 |
if not active:
|
621 |
return "Please enter at least one message."
|
622 |
|
623 |
-
# Run model on messages
|
624 |
results = [(analyze_single_message(m, THRESHOLDS.copy()), d) for m, d in active]
|
625 |
-
# --- Combined Abuse Escalation Scoring ---
|
626 |
|
627 |
-
|
628 |
-
|
|
|
|
|
|
|
629 |
|
630 |
-
|
631 |
high = {'control'}
|
632 |
-
moderate = {
|
633 |
-
'gaslighting', 'dismissiveness', 'obscure language',
|
634 |
-
'insults', 'contradictory statements', 'guilt tripping'
|
635 |
-
}
|
636 |
low = {'blame shifting', 'projection', 'recovery phase'}
|
637 |
-
|
638 |
-
# Count severity types
|
639 |
counts = {'high': 0, 'moderate': 0, 'low': 0}
|
640 |
for label in predicted_labels:
|
641 |
if label in high:
|
@@ -645,126 +641,21 @@ def analyze_composite(msg1, date1, msg2, date2, msg3, date3, *answers_and_none):
|
|
645 |
elif label in low:
|
646 |
counts['low'] += 1
|
647 |
|
648 |
-
#
|
|
|
649 |
if counts['high'] >= 2 and counts['moderate'] >= 2:
|
650 |
-
|
651 |
elif (counts['high'] >= 2 and counts['moderate'] >= 1) or (counts['moderate'] >= 3) or (counts['high'] >= 1 and counts['moderate'] >= 2):
|
652 |
-
|
653 |
elif (counts['moderate'] == 2) or (counts['high'] == 1 and counts['moderate'] == 1) or (counts['moderate'] == 1 and counts['low'] >= 2) or (counts['high'] == 1 and sum(counts.values()) == 1):
|
654 |
-
|
655 |
-
else:
|
656 |
-
abuse_risk = 'Low'
|
657 |
-
|
658 |
-
# Combine abuse_risk and checklist score into final risk_level
|
659 |
-
if escalation_score is not None:
|
660 |
-
if escalation_score >= 8 or abuse_risk == 'Critical':
|
661 |
-
risk_level = 'Critical'
|
662 |
-
elif escalation_score >= 5 or abuse_risk == 'High':
|
663 |
-
risk_level = 'High'
|
664 |
-
elif escalation_score >= 2 or abuse_risk == 'Moderate':
|
665 |
-
risk_level = 'Moderate'
|
666 |
-
else:
|
667 |
-
risk_level = 'Low'
|
668 |
-
abuse_scores = [r[0][0] for r in results]
|
669 |
-
top_labels = [r[0][1][0] if r[0][1] else r[0][2][0][0] for r in results]
|
670 |
-
top_scores = [r[0][2][0][1] for r in results]
|
671 |
-
sentiments = [r[0][3]['label'] for r in results]
|
672 |
-
stages = [r[0][4] for r in results]
|
673 |
-
darvo_scores = [r[0][5] for r in results]
|
674 |
-
tone_tags= [r[0][6] for r in results]
|
675 |
-
dates_used = [r[1] or "Undated" for r in results] # Store dates for future mapping
|
676 |
-
# Calculate escalation bump *after* model results exist
|
677 |
-
escalation_bump = 0
|
678 |
-
for result, _ in results:
|
679 |
-
abuse_score, threshold_labels, top_patterns, sentiment, stage, darvo_score, tone_tag = result
|
680 |
-
if darvo_score > 0.65:
|
681 |
-
escalation_bump += 3
|
682 |
-
if tone_tag in ["forced accountability flip", "emotional threat"]:
|
683 |
-
escalation_bump += 2
|
684 |
-
if abuse_score > 80:
|
685 |
-
escalation_bump += 2
|
686 |
-
if stage == 2:
|
687 |
-
escalation_bump += 3
|
688 |
|
689 |
-
|
690 |
-
|
691 |
-
|
692 |
-
"High" if hybrid_score >= 16 else
|
693 |
-
"Moderate" if hybrid_score >= 8 else
|
694 |
"Low"
|
695 |
)
|
696 |
|
697 |
-
# Now compute scores and allow override
|
698 |
-
abuse_scores = [r[0][0] for r in results]
|
699 |
-
stages = [r[0][4] for r in results]
|
700 |
-
|
701 |
-
# Post-check override (e.g. stage 2 or high abuse score forces Moderate risk)
|
702 |
-
if any(score > 70 for score in abuse_scores) or any(stage == 2 for stage in stages):
|
703 |
-
if risk_level == "Low":
|
704 |
-
risk_level = "Moderate"
|
705 |
-
|
706 |
-
for result, date in results:
|
707 |
-
assert len(result) == 7, "Unexpected output from analyze_single_message"
|
708 |
-
from collections import Counter
|
709 |
-
|
710 |
-
# --- Step 1: Pattern-based escalation scoring ---
|
711 |
-
flat_patterns = [label for r in results for (label, _) in r[0][2]]
|
712 |
-
|
713 |
-
PATTERN_RISKS = {
|
714 |
-
"blame shifting": "low",
|
715 |
-
"contradictory statements": "moderate",
|
716 |
-
"control": "high",
|
717 |
-
"dismissiveness": "moderate",
|
718 |
-
"gaslighting": "moderate",
|
719 |
-
"guilt tripping": "moderate",
|
720 |
-
"insults": "moderate",
|
721 |
-
"obscure language": "low",
|
722 |
-
"projection": "low",
|
723 |
-
" recovery phase": "low"
|
724 |
-
}
|
725 |
-
|
726 |
-
risk_counts = Counter(PATTERN_RISKS.get(p, "unknown") for p in flat_patterns)
|
727 |
-
num_critical = 0 # no "critical" tags defined in your label set
|
728 |
-
num_high = risk_counts["high"]
|
729 |
-
num_moderate = risk_counts["moderate"]
|
730 |
-
num_low = risk_counts["low"]
|
731 |
-
|
732 |
-
# Determine pattern-based escalation risk
|
733 |
-
pattern_escalation_risk = "Low"
|
734 |
-
if num_high >= 2 and num_moderate >= 2:
|
735 |
-
pattern_escalation_risk = "Critical"
|
736 |
-
elif num_high >= 2 and num_moderate >= 1:
|
737 |
-
pattern_escalation_risk = "High"
|
738 |
-
elif num_moderate >= 3:
|
739 |
-
pattern_escalation_risk = "High"
|
740 |
-
elif num_high == 1 and num_moderate >= 1:
|
741 |
-
pattern_escalation_risk = "Moderate"
|
742 |
-
elif num_moderate == 2:
|
743 |
-
pattern_escalation_risk = "Moderate"
|
744 |
-
elif num_moderate == 1 and num_low >= 2:
|
745 |
-
pattern_escalation_risk = "Moderate"
|
746 |
-
elif num_high == 1 and (num_high + num_moderate + num_low) == 1:
|
747 |
-
pattern_escalation_risk = "Moderate"
|
748 |
-
|
749 |
-
# --- Step 2: Checklist escalation logic ---
|
750 |
-
if escalation_score is None:
|
751 |
-
escalation_text = "π« **Escalation Potential: Unknown** (Checklist not completed)\n"
|
752 |
-
escalation_text += "β οΈ This section was not completed. Escalation potential cannot be calculated.\n"
|
753 |
-
hybrid_score = 0
|
754 |
-
else:
|
755 |
-
hybrid_score = escalation_score + escalation_bump
|
756 |
-
risk_level = (
|
757 |
-
"π΄ High" if hybrid_score >= 16 else
|
758 |
-
"π Moderate" if hybrid_score >= 8 else
|
759 |
-
"π’ Low"
|
760 |
-
)
|
761 |
-
|
762 |
-
escalation_text = f"π **Escalation Potential: {risk_level} ({hybrid_score}/29)**\n"
|
763 |
-
escalation_text += "π This score comes from your checklist *and* detected high-risk patterns.\n"
|
764 |
-
escalation_text += "π§ It reflects both situational risk and behavioral red flags."
|
765 |
-
|
766 |
-
|
767 |
-
# --- Step 3: Escalation bump from DARVO, tone, abuse score, etc.
|
768 |
escalation_bump = 0
|
769 |
for result, _ in results:
|
770 |
abuse_score, _, _, sentiment, stage, darvo_score, tone_tag = result
|
@@ -777,31 +668,37 @@ def analyze_composite(msg1, date1, msg2, date2, msg3, date3, *answers_and_none):
|
|
777 |
if stage == 2:
|
778 |
escalation_bump += 3
|
779 |
|
780 |
-
|
781 |
-
|
782 |
-
return ["Low", "Moderate", "High", "Critical"].index(risk_label) if risk_label else 0
|
783 |
|
784 |
combined_score = rank(pattern_escalation_risk) + rank(checklist_escalation_risk) + escalation_bump
|
785 |
-
|
786 |
escalation_risk = (
|
787 |
"Critical" if combined_score >= 6 else
|
788 |
"High" if combined_score >= 4 else
|
789 |
"Moderate" if combined_score >= 2 else
|
790 |
"Low"
|
791 |
)
|
792 |
-
# --- Composite Abuse Score using compute_abuse_score ---
|
793 |
-
composite_abuse_scores = []
|
794 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
795 |
for result, _ in results:
|
796 |
_, _, top_patterns, sentiment, _, _, _ = result
|
797 |
matched_scores = [(label, score, PATTERN_WEIGHTS.get(label, 1.0)) for label, score in top_patterns]
|
798 |
final_score = compute_abuse_score(matched_scores, sentiment["label"])
|
799 |
composite_abuse_scores.append(final_score)
|
800 |
-
|
801 |
composite_abuse = int(round(sum(composite_abuse_scores) / len(composite_abuse_scores)))
|
802 |
|
803 |
-
|
804 |
-
|
805 |
most_common_stage = max(set(stages), key=stages.count)
|
806 |
stage_text = RISK_STAGE_LABELS[most_common_stage]
|
807 |
|
@@ -813,39 +710,17 @@ def analyze_composite(msg1, date1, msg2, date2, msg3, date3, *answers_and_none):
|
|
813 |
|
814 |
out = f"Abuse Intensity: {composite_abuse}%\n"
|
815 |
out += "π This reflects the strength and severity of detected abuse patterns in the message(s).\n\n"
|
816 |
-
|
817 |
-
# Save this line for later use at the
|
818 |
-
if escalation_score is None:
|
819 |
-
escalation_text = "π Escalation Potential: Unknown (Checklist not completed)\n"
|
820 |
-
escalation_text += "β οΈ *This section was not completed. Escalation potential is unknown.*\n"
|
821 |
-
hybrid_score = 0 # β
fallback so it's defined for generate_risk_snippet
|
822 |
-
else:
|
823 |
-
escalation_text = f"𧨠**Escalation Potential: {risk_level} ({escalation_score}/{sum(w for _, w in ESCALATION_QUESTIONS)})**\n"
|
824 |
-
escalation_text += "This score comes directly from the safety checklist and functions as a standalone escalation risk score.\n"
|
825 |
-
escalation_text += "It indicates how many serious risk factors are present based on your answers to the safety checklist.\n"
|
826 |
-
# Derive top_label from the strongest top_patterns across all messages
|
827 |
-
top_label = None
|
828 |
-
if results:
|
829 |
-
sorted_patterns = sorted(
|
830 |
-
[(label, score) for r in results for label, score in r[0][2]],
|
831 |
-
key=lambda x: x[1],
|
832 |
-
reverse=True
|
833 |
-
)
|
834 |
-
if sorted_patterns:
|
835 |
-
top_label = f"{sorted_patterns[0][0]} β {int(round(sorted_patterns[0][1] * 100))}%"
|
836 |
-
if top_label is None:
|
837 |
-
top_label = "Unknown β 0%"
|
838 |
-
out += generate_risk_snippet(composite_abuse, top_label, hybrid_score if escalation_score is not None else 0, most_common_stage)
|
839 |
out += f"\n\n{stage_text}"
|
840 |
out += darvo_blurb
|
841 |
out += "\n\nπ **Emotional Tones Detected:**\n"
|
842 |
for i, tone in enumerate(tone_tags):
|
843 |
-
|
844 |
-
|
845 |
-
|
846 |
-
pattern_labels = [r[0][2][0][0] for r in results] # top label for each message
|
847 |
timeline_image = generate_abuse_score_chart(dates_used, abuse_scores, pattern_labels)
|
848 |
out += "\n\n" + escalation_text
|
|
|
849 |
return out, timeline_image
|
850 |
|
851 |
message_date_pairs = [
|
|
|
604 |
return abuse_score, threshold_labels, top_patterns, {"label": sentiment}, stage, darvo_score, tone_tag
|
605 |
|
606 |
def analyze_composite(msg1, date1, msg2, date2, msg3, date3, *answers_and_none):
|
607 |
+
from collections import Counter
|
608 |
+
|
609 |
none_selected_checked = answers_and_none[-1]
|
610 |
responses_checked = any(answers_and_none[:-1])
|
611 |
none_selected = not responses_checked and none_selected_checked
|
612 |
|
613 |
+
escalation_score = None
|
614 |
+
if not none_selected:
|
|
|
|
|
615 |
escalation_score = sum(w for (_, w), a in zip(ESCALATION_QUESTIONS, answers_and_none[:-1]) if a)
|
616 |
|
617 |
messages = [msg1, msg2, msg3]
|
|
|
620 |
if not active:
|
621 |
return "Please enter at least one message."
|
622 |
|
|
|
623 |
results = [(analyze_single_message(m, THRESHOLDS.copy()), d) for m, d in active]
|
|
|
624 |
|
625 |
+
abuse_scores = [r[0][0] for r in results]
|
626 |
+
stages = [r[0][4] for r in results]
|
627 |
+
darvo_scores = [r[0][5] for r in results]
|
628 |
+
tone_tags = [r[0][6] for r in results]
|
629 |
+
dates_used = [r[1] or "Undated" for r in results]
|
630 |
|
631 |
+
predicted_labels = [label for r in results for label, _ in r[0][2]]
|
632 |
high = {'control'}
|
633 |
+
moderate = {'gaslighting', 'dismissiveness', 'obscure language', 'insults', 'contradictory statements', 'guilt tripping'}
|
|
|
|
|
|
|
634 |
low = {'blame shifting', 'projection', 'recovery phase'}
|
|
|
|
|
635 |
counts = {'high': 0, 'moderate': 0, 'low': 0}
|
636 |
for label in predicted_labels:
|
637 |
if label in high:
|
|
|
641 |
elif label in low:
|
642 |
counts['low'] += 1
|
643 |
|
644 |
+
# Pattern escalation logic
|
645 |
+
pattern_escalation_risk = "Low"
|
646 |
if counts['high'] >= 2 and counts['moderate'] >= 2:
|
647 |
+
pattern_escalation_risk = "Critical"
|
648 |
elif (counts['high'] >= 2 and counts['moderate'] >= 1) or (counts['moderate'] >= 3) or (counts['high'] >= 1 and counts['moderate'] >= 2):
|
649 |
+
pattern_escalation_risk = "High"
|
650 |
elif (counts['moderate'] == 2) or (counts['high'] == 1 and counts['moderate'] == 1) or (counts['moderate'] == 1 and counts['low'] >= 2) or (counts['high'] == 1 and sum(counts.values()) == 1):
|
651 |
+
pattern_escalation_risk = "Moderate"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
652 |
|
653 |
+
checklist_escalation_risk = "Unknown" if escalation_score is None else (
|
654 |
+
"Critical" if escalation_score >= 20 else
|
655 |
+
"Moderate" if escalation_score >= 10 else
|
|
|
|
|
656 |
"Low"
|
657 |
)
|
658 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
659 |
escalation_bump = 0
|
660 |
for result, _ in results:
|
661 |
abuse_score, _, _, sentiment, stage, darvo_score, tone_tag = result
|
|
|
668 |
if stage == 2:
|
669 |
escalation_bump += 3
|
670 |
|
671 |
+
def rank(label):
|
672 |
+
return {"Low": 0, "Moderate": 1, "High": 2, "Critical": 3, "Unknown": 0}.get(label, 0)
|
|
|
673 |
|
674 |
combined_score = rank(pattern_escalation_risk) + rank(checklist_escalation_risk) + escalation_bump
|
|
|
675 |
escalation_risk = (
|
676 |
"Critical" if combined_score >= 6 else
|
677 |
"High" if combined_score >= 4 else
|
678 |
"Moderate" if combined_score >= 2 else
|
679 |
"Low"
|
680 |
)
|
|
|
|
|
681 |
|
682 |
+
if escalation_score is None:
|
683 |
+
escalation_text = "π« **Escalation Potential: Unknown** (Checklist not completed)\nβ οΈ This section was not completed. Escalation potential is estimated using message data only.\n"
|
684 |
+
hybrid_score = 0
|
685 |
+
else:
|
686 |
+
hybrid_score = escalation_score + escalation_bump
|
687 |
+
escalation_text = f"π **Escalation Potential: {escalation_risk} ({hybrid_score}/29)**\n"
|
688 |
+
escalation_text += "π This score combines your safety checklist answers *and* detected high-risk behavior.\n"
|
689 |
+
escalation_text += f"β’ Pattern Risk: {pattern_escalation_risk}\n"
|
690 |
+
escalation_text += f"β’ Checklist Risk: {checklist_escalation_risk}\n"
|
691 |
+
escalation_text += f"β’ Escalation Bump: +{escalation_bump} (from DARVO, tone, intensity, etc.)"
|
692 |
+
|
693 |
+
# Composite Abuse Score
|
694 |
+
composite_abuse_scores = []
|
695 |
for result, _ in results:
|
696 |
_, _, top_patterns, sentiment, _, _, _ = result
|
697 |
matched_scores = [(label, score, PATTERN_WEIGHTS.get(label, 1.0)) for label, score in top_patterns]
|
698 |
final_score = compute_abuse_score(matched_scores, sentiment["label"])
|
699 |
composite_abuse_scores.append(final_score)
|
|
|
700 |
composite_abuse = int(round(sum(composite_abuse_scores) / len(composite_abuse_scores)))
|
701 |
|
|
|
|
|
702 |
most_common_stage = max(set(stages), key=stages.count)
|
703 |
stage_text = RISK_STAGE_LABELS[most_common_stage]
|
704 |
|
|
|
710 |
|
711 |
out = f"Abuse Intensity: {composite_abuse}%\n"
|
712 |
out += "π This reflects the strength and severity of detected abuse patterns in the message(s).\n\n"
|
713 |
+
out += generate_risk_snippet(composite_abuse, top_labels[0], hybrid_score, most_common_stage)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
714 |
out += f"\n\n{stage_text}"
|
715 |
out += darvo_blurb
|
716 |
out += "\n\nπ **Emotional Tones Detected:**\n"
|
717 |
for i, tone in enumerate(tone_tags):
|
718 |
+
out += f"β’ Message {i+1}: *{tone or 'none'}*\n"
|
719 |
+
|
720 |
+
pattern_labels = [r[0][2][0][0] for r in results]
|
|
|
721 |
timeline_image = generate_abuse_score_chart(dates_used, abuse_scores, pattern_labels)
|
722 |
out += "\n\n" + escalation_text
|
723 |
+
|
724 |
return out, timeline_image
|
725 |
|
726 |
message_date_pairs = [
|