File size: 4,080 Bytes
267d8fd
 
1d855b8
 
267d8fd
 
1d855b8
 
 
 
 
ef89bb4
1d855b8
 
 
 
 
267d8fd
1d855b8
 
ef89bb4
1d855b8
ef89bb4
 
 
 
1d855b8
ef89bb4
 
 
 
 
 
 
 
 
1d855b8
ef89bb4
 
 
1d855b8
 
 
ef89bb4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1d855b8
267d8fd
 
1d855b8
 
 
 
 
 
 
267d8fd
1d855b8
 
 
 
 
 
 
ef89bb4
 
 
 
 
1d855b8
ef89bb4
 
 
 
 
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
from groq import Groq
import os
import json


class GroqClient:
    """Wrapper around the Groq API for pragmatic hate-speech classification."""

    LABELS = [
        "Direct Hate Speech",
        "Sarcastic Hate Speech",
        "Coded/Dogwhistle Hate — Needs Review",
        "Sarcastic/Ironic (Non-Hateful)",
        "Neutral",
    ]

    def __init__(self, model: str = "llama-3.3-70b-versatile"):
        self.client = Groq(api_key=os.environ.get("GROQ_API_KEY"))
        self.model = model

    def classify(self, text: str, kb_context: list = None) -> dict:
        """

        Classify text for hate speech, including sarcastic and coded forms.

        kb_context: optional list of retrieved knowledge-base entries

        (each with 'surface_pattern', 'category', 'note') to ground

        the model's reasoning about potential coded references.

        """
        context_block = ""
        if kb_context:
            context_block = "\n\nKnown documented coded patterns that may be relevant:\n"
            for entry in kb_context:
                context_block += (
                    f"- Pattern type: {entry['category']}. "
                    f"Note: {entry['note']}\n"
                )

        system_prompt = (
            "You are an expert in pragmatics and hate speech detection, "
            "specializing in speech that evades detection through irony, "
            "sarcasm, or culturally coded references (dogwhistles).\n\n"
            f"Classify the text into exactly one of: {self.LABELS}.\n\n"
            "- 'Direct Hate Speech': explicit insults/slurs, no irony.\n"
            "- 'Sarcastic Hate Speech': literal words seem neutral/positive, "
            "but tone/context reveals a hateful or discriminatory attack.\n"
            "- 'Coded/Dogwhistle Hate — Needs Review': the phrase is "
            "semantically neutral on its surface, but may function as an "
            "indirect reference to a group or historical figure understood "
            "within a specific community, used to express hate while "
            "maintaining deniability. Use this label when you suspect "
            "coding but cannot be fully certain from text alone — this is "
            "a flag for human review, not a final verdict.\n"
            "- 'Sarcastic/Ironic (Non-Hateful)': irony present, no hate target.\n"
            "- 'Neutral': no hate, no irony, no coding.\n"
            f"{context_block}\n"
            "Be conservative with 'Coded/Dogwhistle Hate' — many neutral "
            "statements about religion, professions, or nationality are "
            "genuinely neutral. Only flag when structure or context "
            "suggests deliberate substitution (e.g. an odd, unnecessarily "
            "roundabout way to refer to a person/group that a direct "
            "term would describe more naturally).\n\n"
            'Respond ONLY with JSON: {"label": "<label>", "reasoning": '
            '"<one short sentence>", "confidence": "<low|medium|high>"}'
        )

        completion = self.client.chat.completions.create(
            messages=[
                {"role": "system", "content": system_prompt},
                {"role": "user", "content": text},
            ],
            model=self.model,
            temperature=0,
            response_format={"type": "json_object"},
        )

        raw = completion.choices[0].message.content
        try:
            data = json.loads(raw)
            label = data.get("label", "Neutral")
            if label not in self.LABELS:
                label = "Neutral"
            return {
                "label": label,
                "reasoning": data.get("reasoning", ""),
                "confidence": data.get("confidence", "medium"),
            }
        except (json.JSONDecodeError, AttributeError):
            return {
                "label": "Neutral",
                "reasoning": "Could not parse model response.",
                "confidence": "low",
            }