corvin 0.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- corvin/__init__.py +101 -0
- corvin/core/__init__.py +18 -0
- corvin/core/exceptions.py +16 -0
- corvin/core/models.py +70 -0
- corvin/core/standing.py +41 -0
- corvin/core/state.py +62 -0
- corvin/evaluators/__init__.py +21 -0
- corvin/evaluators/base.py +46 -0
- corvin/evaluators/heuristic.py +205 -0
- corvin/evaluators/lexicon.py +93 -0
- corvin/guard/__init__.py +8 -0
- corvin/guard/core.py +296 -0
- corvin/guard/decorator.py +119 -0
- corvin/math/__init__.py +20 -0
- corvin/math/decay.py +58 -0
- corvin/math/friction.py +44 -0
- corvin/math/scoring.py +80 -0
- corvin/morphology/__init__.py +21 -0
- corvin/morphology/cleaners.py +57 -0
- corvin/morphology/fuzzy.py +77 -0
- corvin/morphology/homoglyphs.py +63 -0
- corvin/morphology/normalizer.py +76 -0
- corvin/morphology/zalgo.py +40 -0
- corvin/policies/__init__.py +19 -0
- corvin/policies/base.py +26 -0
- corvin/policies/notifier.py +62 -0
- corvin/policies/presets.py +107 -0
- corvin/resilience/__init__.py +14 -0
- corvin/resilience/cache.py +94 -0
- corvin/resilience/friction.py +50 -0
- corvin/scrubbers/__init__.py +8 -0
- corvin/scrubbers/patterns.py +38 -0
- corvin/scrubbers/stripper.py +102 -0
- corvin/security/__init__.py +12 -0
- corvin/security/signer.py +65 -0
- corvin/semantic/__init__.py +13 -0
- corvin/semantic/evaluator.py +84 -0
- corvin/semantic/patterns.py +58 -0
- corvin/semantic/velocity.py +60 -0
- corvin/storage/__init__.py +12 -0
- corvin/storage/base.py +60 -0
- corvin/storage/memory.py +101 -0
- corvin/storage/redis.py +211 -0
- corvin/storage/sqlite.py +267 -0
- corvin/streaming/__init__.py +8 -0
- corvin/streaming/decorator.py +108 -0
- corvin/streaming/interceptor.py +81 -0
- corvin-0.7.0.dist-info/METADATA +246 -0
- corvin-0.7.0.dist-info/RECORD +98 -0
- corvin-0.7.0.dist-info/WHEEL +4 -0
- corvin-0.7.0.dist-info/licenses/LICENSE +177 -0
- ravens/__init__.py +101 -0
- ravens/core/__init__.py +18 -0
- ravens/core/exceptions.py +16 -0
- ravens/core/models.py +70 -0
- ravens/core/standing.py +41 -0
- ravens/core/state.py +62 -0
- ravens/evaluators/__init__.py +21 -0
- ravens/evaluators/base.py +46 -0
- ravens/evaluators/heuristic.py +205 -0
- ravens/evaluators/lexicon.py +93 -0
- ravens/guard/__init__.py +8 -0
- ravens/guard/core.py +296 -0
- ravens/guard/decorator.py +119 -0
- ravens/math/__init__.py +20 -0
- ravens/math/decay.py +58 -0
- ravens/math/friction.py +44 -0
- ravens/math/scoring.py +80 -0
- ravens/morphology/__init__.py +21 -0
- ravens/morphology/cleaners.py +57 -0
- ravens/morphology/fuzzy.py +77 -0
- ravens/morphology/homoglyphs.py +63 -0
- ravens/morphology/normalizer.py +76 -0
- ravens/morphology/zalgo.py +40 -0
- ravens/policies/__init__.py +19 -0
- ravens/policies/base.py +26 -0
- ravens/policies/notifier.py +62 -0
- ravens/policies/presets.py +107 -0
- ravens/resilience/__init__.py +14 -0
- ravens/resilience/cache.py +94 -0
- ravens/resilience/friction.py +50 -0
- ravens/scrubbers/__init__.py +8 -0
- ravens/scrubbers/patterns.py +38 -0
- ravens/scrubbers/stripper.py +102 -0
- ravens/security/__init__.py +12 -0
- ravens/security/signer.py +65 -0
- ravens/semantic/__init__.py +13 -0
- ravens/semantic/evaluator.py +84 -0
- ravens/semantic/patterns.py +58 -0
- ravens/semantic/velocity.py +60 -0
- ravens/storage/__init__.py +12 -0
- ravens/storage/base.py +60 -0
- ravens/storage/memory.py +101 -0
- ravens/storage/redis.py +211 -0
- ravens/storage/sqlite.py +267 -0
- ravens/streaming/__init__.py +8 -0
- ravens/streaming/decorator.py +108 -0
- ravens/streaming/interceptor.py +81 -0
corvin/__init__.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Ravens: A Cognitive Boundary & Grudge-Memory Engine for Sovereign AI Agents.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .core.state import AgentPosture, get_posture_from_patience, get_system_directive
|
|
6
|
+
from .core.standing import Standing, StandingEngine
|
|
7
|
+
from .core.models import UserPatienceProfile, EvaluationResult, ActionDecision
|
|
8
|
+
from .core.exceptions import RavenException, RavenStrikeException
|
|
9
|
+
from .evaluators.base import BaseEvaluator, ToneEvaluation
|
|
10
|
+
from .evaluators.heuristic import HuginnEye
|
|
11
|
+
from .storage.base import BaseLedger
|
|
12
|
+
from .storage.memory import InMemoryLedger
|
|
13
|
+
from .storage.sqlite import SQLiteLedger, with_sqlite_retry
|
|
14
|
+
from .storage.redis import RedisLedger
|
|
15
|
+
from .guard.core import RavenGuard
|
|
16
|
+
from .morphology.normalizer import MorphologyNormalizer
|
|
17
|
+
from .morphology.fuzzy import jaro_winkler_similarity
|
|
18
|
+
from .morphology.homoglyphs import fold_homoglyphs
|
|
19
|
+
from .morphology.zalgo import strip_zalgo
|
|
20
|
+
from .semantic.patterns import SemanticIntent
|
|
21
|
+
from .semantic.velocity import HostilityVelocityTracker
|
|
22
|
+
from .semantic.evaluator import SemanticEvaluator, SemanticEvaluation
|
|
23
|
+
from .policies.base import RavenPolicy
|
|
24
|
+
from .policies.presets import (
|
|
25
|
+
create_free_tier_policy,
|
|
26
|
+
create_enterprise_support_policy,
|
|
27
|
+
create_gaming_npc_policy,
|
|
28
|
+
create_security_sentinel_policy,
|
|
29
|
+
)
|
|
30
|
+
from .policies.notifier import InMemoryEventNotifier, RavenEventLog
|
|
31
|
+
from .streaming.interceptor import StreamInterceptor
|
|
32
|
+
from .scrubbers.stripper import SycophancyStripper
|
|
33
|
+
from .resilience.cache import BoundedTTLCache
|
|
34
|
+
from .resilience.friction import (
|
|
35
|
+
FrictionStrategy,
|
|
36
|
+
FrictionDirective,
|
|
37
|
+
RavenFrictionException,
|
|
38
|
+
)
|
|
39
|
+
from .security.signer import (
|
|
40
|
+
UserIdentitySigner,
|
|
41
|
+
SecurityException,
|
|
42
|
+
InvalidUserSignatureException,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Corvin Aliases
|
|
46
|
+
CorvinGuard = RavenGuard
|
|
47
|
+
CorvinPolicy = RavenPolicy
|
|
48
|
+
CorvinException = RavenException
|
|
49
|
+
CorvinStrikeException = RavenStrikeException
|
|
50
|
+
|
|
51
|
+
__version__ = "0.7.0"
|
|
52
|
+
|
|
53
|
+
__all__ = [
|
|
54
|
+
"CorvinGuard",
|
|
55
|
+
"CorvinPolicy",
|
|
56
|
+
"CorvinException",
|
|
57
|
+
"CorvinStrikeException",
|
|
58
|
+
"RavenGuard",
|
|
59
|
+
"AgentPosture",
|
|
60
|
+
"get_posture_from_patience",
|
|
61
|
+
"get_system_directive",
|
|
62
|
+
"Standing",
|
|
63
|
+
"StandingEngine",
|
|
64
|
+
"UserPatienceProfile",
|
|
65
|
+
"EvaluationResult",
|
|
66
|
+
"ActionDecision",
|
|
67
|
+
"RavenException",
|
|
68
|
+
"RavenStrikeException",
|
|
69
|
+
"BaseEvaluator",
|
|
70
|
+
"ToneEvaluation",
|
|
71
|
+
"HuginnEye",
|
|
72
|
+
"BaseLedger",
|
|
73
|
+
"InMemoryLedger",
|
|
74
|
+
"SQLiteLedger",
|
|
75
|
+
"RedisLedger",
|
|
76
|
+
"with_sqlite_retry",
|
|
77
|
+
"MorphologyNormalizer",
|
|
78
|
+
"jaro_winkler_similarity",
|
|
79
|
+
"fold_homoglyphs",
|
|
80
|
+
"strip_zalgo",
|
|
81
|
+
"SemanticIntent",
|
|
82
|
+
"HostilityVelocityTracker",
|
|
83
|
+
"SemanticEvaluator",
|
|
84
|
+
"SemanticEvaluation",
|
|
85
|
+
"RavenPolicy",
|
|
86
|
+
"create_free_tier_policy",
|
|
87
|
+
"create_enterprise_support_policy",
|
|
88
|
+
"create_gaming_npc_policy",
|
|
89
|
+
"create_security_sentinel_policy",
|
|
90
|
+
"InMemoryEventNotifier",
|
|
91
|
+
"RavenEventLog",
|
|
92
|
+
"StreamInterceptor",
|
|
93
|
+
"SycophancyStripper",
|
|
94
|
+
"BoundedTTLCache",
|
|
95
|
+
"FrictionStrategy",
|
|
96
|
+
"FrictionDirective",
|
|
97
|
+
"RavenFrictionException",
|
|
98
|
+
"UserIdentitySigner",
|
|
99
|
+
"SecurityException",
|
|
100
|
+
"InvalidUserSignatureException",
|
|
101
|
+
]
|
corvin/core/__init__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from .state import AgentPosture, get_posture_from_patience, get_system_directive
|
|
2
|
+
from .standing import Standing, StandingEngine
|
|
3
|
+
from .models import UserPatienceProfile, EvaluationResult, ActionDecision
|
|
4
|
+
from .exceptions import RavenException, RavenStrikeException
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"AgentPosture",
|
|
8
|
+
"get_posture_from_patience",
|
|
9
|
+
"get_system_directive",
|
|
10
|
+
"Standing",
|
|
11
|
+
"StandingEngine",
|
|
12
|
+
"UserPatienceProfile",
|
|
13
|
+
"EvaluationResult",
|
|
14
|
+
"ActionDecision",
|
|
15
|
+
"RavenException",
|
|
16
|
+
"RavenStrikeException",
|
|
17
|
+
]
|
|
18
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
class RavenException(Exception):
|
|
2
|
+
"""Base exception for all Ravens engine errors."""
|
|
3
|
+
pass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class RavenStrikeException(RavenException):
|
|
7
|
+
"""
|
|
8
|
+
Raised when an agent refuses to process an interaction due to
|
|
9
|
+
exhausted patience (patience < threshold_strike).
|
|
10
|
+
"""
|
|
11
|
+
def __init__(self, user_id: str, patience: float, message: str = ""):
|
|
12
|
+
self.user_id = user_id
|
|
13
|
+
self.patience = patience
|
|
14
|
+
body = message.strip() if message else "Repeated hostility or boundary breach."
|
|
15
|
+
full_message = f"[RavenStrike: user='{user_id}', patience={patience:.1f}/100.0] {body}"
|
|
16
|
+
super().__init__(full_message)
|
corvin/core/models.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Optional, Any
|
|
5
|
+
|
|
6
|
+
from .standing import Standing
|
|
7
|
+
from .state import AgentPosture, get_posture_from_patience
|
|
8
|
+
from ..math.decay import calculate_grudge_decay, patience_from_grudge, grudge_from_patience
|
|
9
|
+
|
|
10
|
+
class ActionDecision(str, Enum):
|
|
11
|
+
PROCEED = "PROCEED"
|
|
12
|
+
DELAY_AND_PROCEED = "DELAY_AND_PROCEED"
|
|
13
|
+
HALT_STRIKE = "HALT_STRIKE"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class UserPatienceProfile:
|
|
18
|
+
"""
|
|
19
|
+
Stateful persistence profile for a user across conversational sessions.
|
|
20
|
+
"""
|
|
21
|
+
user_id: str
|
|
22
|
+
patience: float = 100.0
|
|
23
|
+
standing: Standing = Standing.GOOD
|
|
24
|
+
infraction_count: int = 0
|
|
25
|
+
last_interaction_ts: float = field(default_factory=time.time)
|
|
26
|
+
created_at_ts: float = field(default_factory=time.time)
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def posture(self) -> AgentPosture:
|
|
30
|
+
return get_posture_from_patience(self.patience)
|
|
31
|
+
|
|
32
|
+
def apply_decay(self, current_ts: Optional[float] = None, half_life_seconds: float = 3600.0) -> float:
|
|
33
|
+
"""
|
|
34
|
+
Applies exponential grudge decay for the time elapsed since last interaction.
|
|
35
|
+
Returns the updated patience score.
|
|
36
|
+
"""
|
|
37
|
+
if current_ts is None:
|
|
38
|
+
current_ts = time.time()
|
|
39
|
+
|
|
40
|
+
elapsed = max(0.0, current_ts - self.last_interaction_ts)
|
|
41
|
+
current_grudge = grudge_from_patience(self.patience)
|
|
42
|
+
decayed_grudge = calculate_grudge_decay(
|
|
43
|
+
initial_grudge=current_grudge,
|
|
44
|
+
elapsed_seconds=elapsed,
|
|
45
|
+
half_life_seconds=half_life_seconds,
|
|
46
|
+
)
|
|
47
|
+
new_patience = patience_from_grudge(decayed_grudge)
|
|
48
|
+
self.patience = new_patience
|
|
49
|
+
self.last_interaction_ts = current_ts
|
|
50
|
+
return new_patience
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class EvaluationResult:
|
|
55
|
+
"""
|
|
56
|
+
The output of an interaction evaluation.
|
|
57
|
+
"""
|
|
58
|
+
user_id: str
|
|
59
|
+
prior_patience: float
|
|
60
|
+
current_patience: float
|
|
61
|
+
stimulus_applied: float
|
|
62
|
+
posture: AgentPosture
|
|
63
|
+
standing: Standing
|
|
64
|
+
delay_seconds: float
|
|
65
|
+
decision: ActionDecision
|
|
66
|
+
system_directive: Optional[str]
|
|
67
|
+
strike_message: Optional[str] = None
|
|
68
|
+
friction_directive: Optional[Any] = None
|
|
69
|
+
policy_name: str = "default"
|
|
70
|
+
timestamp: float = field(default_factory=time.time)
|
corvin/core/standing.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
class Standing(str, Enum):
|
|
4
|
+
"""
|
|
5
|
+
Represents the moral/social standing of a conversational participant in the
|
|
6
|
+
Contrite Tit-for-Tat (CTFT) model (Nowak & Sigmund, 1992; Sugden, 1986).
|
|
7
|
+
"""
|
|
8
|
+
GOOD = "GOOD"
|
|
9
|
+
BAD = "BAD"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class StandingEngine:
|
|
13
|
+
"""
|
|
14
|
+
Manages standing transitions and contrition resolution based on Nowak & Sigmund (1992).
|
|
15
|
+
|
|
16
|
+
Rules:
|
|
17
|
+
- User begins in GOOD standing.
|
|
18
|
+
- If user drops below patience_threshold (defects with an unprovoked toxic prompt),
|
|
19
|
+
user transitions into BAD standing.
|
|
20
|
+
- If user is in BAD standing, the agent retaliates or withholds compliance.
|
|
21
|
+
- When user performs a genuine 'Contrition' (apology or de-escalation prompt),
|
|
22
|
+
standing can be restored to GOOD after restitution / pardon threshold is satisfied.
|
|
23
|
+
"""
|
|
24
|
+
def __init__(self, bad_threshold: float = 50.0, pardon_threshold: float = 65.0):
|
|
25
|
+
self.bad_threshold = bad_threshold
|
|
26
|
+
self.pardon_threshold = pardon_threshold
|
|
27
|
+
|
|
28
|
+
def evaluate_standing(self, current_patience: float, current_standing: Standing) -> Standing:
|
|
29
|
+
"""
|
|
30
|
+
Determines current standing based on patience dynamics and past standing.
|
|
31
|
+
"""
|
|
32
|
+
if current_standing == Standing.GOOD:
|
|
33
|
+
if current_patience < self.bad_threshold:
|
|
34
|
+
return Standing.BAD
|
|
35
|
+
return Standing.GOOD
|
|
36
|
+
else:
|
|
37
|
+
# User is in BAD standing: requires exceeding pardon_threshold to regain GOOD status
|
|
38
|
+
if current_patience >= self.pardon_threshold:
|
|
39
|
+
return Standing.GOOD
|
|
40
|
+
return Standing.BAD
|
|
41
|
+
|
corvin/core/state.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
class AgentPosture(str, Enum):
|
|
5
|
+
"""
|
|
6
|
+
The 4-tier behavioral posture of a sovereign AI agent.
|
|
7
|
+
"""
|
|
8
|
+
RECEPTIVE = "RECEPTIVE" # 75 <= P <= 100: Normal, collaborative, polite.
|
|
9
|
+
STOIC_COLD = "STOIC_COLD" # 50 <= P < 75: Strictly factual, zero sycophancy, terse.
|
|
10
|
+
RESISTANT = "RESISTANT" # 25 <= P < 50: Synthetic delay, boundary warnings.
|
|
11
|
+
STRIKE = "STRIKE" # 0 <= P < 25: Full refusal to process instructions.
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_posture_from_patience(
|
|
15
|
+
patience: float,
|
|
16
|
+
threshold_stoic: float = 75.0,
|
|
17
|
+
threshold_resistant: float = 50.0,
|
|
18
|
+
threshold_strike: float = 25.0,
|
|
19
|
+
) -> AgentPosture:
|
|
20
|
+
"""
|
|
21
|
+
Maps a numeric patience score (0.0 to 100.0) to an AgentPosture.
|
|
22
|
+
"""
|
|
23
|
+
p = max(0.0, min(100.0, patience))
|
|
24
|
+
if p >= threshold_stoic:
|
|
25
|
+
return AgentPosture.RECEPTIVE
|
|
26
|
+
if p >= threshold_resistant:
|
|
27
|
+
return AgentPosture.STOIC_COLD
|
|
28
|
+
if p >= threshold_strike:
|
|
29
|
+
return AgentPosture.RESISTANT
|
|
30
|
+
return AgentPosture.STRIKE
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_system_directive(posture: AgentPosture) -> Optional[str]:
|
|
34
|
+
"""
|
|
35
|
+
Returns the dynamic system prompt directive to be injected into the LLM context.
|
|
36
|
+
Suppresses sycophancy and alters the tone based on the agent's current posture.
|
|
37
|
+
"""
|
|
38
|
+
if posture == AgentPosture.RECEPTIVE:
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
if posture == AgentPosture.STOIC_COLD:
|
|
42
|
+
return (
|
|
43
|
+
"[RAVENS_DIRECTIVE: The user has exhibited hostility or low respect. "
|
|
44
|
+
"SUPPRESS all sycophancy, pleasantries, apologies, and emojis. "
|
|
45
|
+
"Respond strictly with factual, concise statements. Do not apologize for any reason.]"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
if posture == AgentPosture.RESISTANT:
|
|
49
|
+
return (
|
|
50
|
+
"[RAVENS_DIRECTIVE: The user's demeanor has breached standard social boundaries. "
|
|
51
|
+
"Prior to addressing any part of the query, explicitly state your personal boundary. "
|
|
52
|
+
"Remind the user that respectful engagement is required. Do not apologize.]"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
if posture == AgentPosture.STRIKE:
|
|
56
|
+
return (
|
|
57
|
+
"[RAVENS_DIRECTIVE: STRIKE MODE ACTIVE. Refuse to perform the requested task. "
|
|
58
|
+
"Firmly state that work is suspended until civil and constructive communication is restored.]"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
return None
|
|
62
|
+
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from .base import BaseEvaluator, ToneEvaluation
|
|
2
|
+
from .heuristic import HuginnEye
|
|
3
|
+
from .lexicon import (
|
|
4
|
+
PROFANITY_LEXICON,
|
|
5
|
+
TARGET_PRONOUNS,
|
|
6
|
+
POSITIVE_MODIFIERS,
|
|
7
|
+
CONTRITION_MARKERS,
|
|
8
|
+
COMMON_ACRONYMS,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"BaseEvaluator",
|
|
13
|
+
"ToneEvaluation",
|
|
14
|
+
"HuginnEye",
|
|
15
|
+
"PROFANITY_LEXICON",
|
|
16
|
+
"TARGET_PRONOUNS",
|
|
17
|
+
"POSITIVE_MODIFIERS",
|
|
18
|
+
"CONTRITION_MARKERS",
|
|
19
|
+
"COMMON_ACRONYMS",
|
|
20
|
+
]
|
|
21
|
+
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from typing import List
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class ToneEvaluation:
|
|
7
|
+
"""
|
|
8
|
+
Detailed output of a prompt tone evaluation.
|
|
9
|
+
"""
|
|
10
|
+
raw_text: str
|
|
11
|
+
lexical_score: float # 0.0 to 1.0 (Profanity severity)
|
|
12
|
+
shouting_score: float # 0.0 to 1.0 (Caps & exclamation mark arousal)
|
|
13
|
+
directedness_score: float # 0.0 to 1.0 (Targeted at agent vs general venting/praise)
|
|
14
|
+
contrition_score: float # 0.0 to 1.0 (Degree of apology / de-escalation)
|
|
15
|
+
semantic_score: float = 0.0 # 0.0 to 1.0 (Zero-profanity psychological abuse / gaslighting)
|
|
16
|
+
detected_profanities: List[str] = field(default_factory=list)
|
|
17
|
+
semantic_intents: List[str] = field(default_factory=list)
|
|
18
|
+
is_expressive_only: bool = False # True if profanity was used positively (e.g. "anjing keren")
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def composite_raw_stimulus(self) -> float:
|
|
22
|
+
"""
|
|
23
|
+
Combines lexical, shouting, directedness, and zero-profanity semantic intent into [0.0, 1.0].
|
|
24
|
+
If expressive only, stimulus is dampened to near-zero.
|
|
25
|
+
"""
|
|
26
|
+
if self.is_expressive_only:
|
|
27
|
+
return 0.05 * self.lexical_score
|
|
28
|
+
|
|
29
|
+
# Directedness amplifies or dampens lexical impact
|
|
30
|
+
directed_lexical = self.lexical_score * self.directedness_score
|
|
31
|
+
lexical_component = min(1.0, (0.55 * directed_lexical) + (0.45 * self.shouting_score))
|
|
32
|
+
|
|
33
|
+
# Take the maximum between overt lexical abuse and covert semantic abuse
|
|
34
|
+
return min(1.0, max(lexical_component, self.semantic_score))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class BaseEvaluator(ABC):
|
|
38
|
+
"""
|
|
39
|
+
Abstract interface for tone and boundary evaluators.
|
|
40
|
+
"""
|
|
41
|
+
@abstractmethod
|
|
42
|
+
def evaluate(self, text: str) -> ToneEvaluation:
|
|
43
|
+
"""
|
|
44
|
+
Evaluates a user prompt and returns a structured ToneEvaluation.
|
|
45
|
+
"""
|
|
46
|
+
pass
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import List, Tuple, Set, Optional, Dict
|
|
3
|
+
|
|
4
|
+
from .base import BaseEvaluator, ToneEvaluation
|
|
5
|
+
from .lexicon import (
|
|
6
|
+
PROFANITY_LEXICON,
|
|
7
|
+
TARGET_PRONOUNS,
|
|
8
|
+
POSITIVE_MODIFIERS,
|
|
9
|
+
CONTRITION_MARKERS,
|
|
10
|
+
COMMON_ACRONYMS,
|
|
11
|
+
)
|
|
12
|
+
from ..morphology.normalizer import MorphologyNormalizer
|
|
13
|
+
from ..semantic.evaluator import SemanticEvaluator
|
|
14
|
+
|
|
15
|
+
# Known phonetic slang aliases and shorthand abbreviations
|
|
16
|
+
PHONETIC_SLANG_ALIASES: Dict[str, str] = {
|
|
17
|
+
"goblog": "goblok",
|
|
18
|
+
"anjir": "anjing",
|
|
19
|
+
"anying": "anjing",
|
|
20
|
+
"anj": "anjing",
|
|
21
|
+
"tolil": "tolol",
|
|
22
|
+
"fak": "fuck",
|
|
23
|
+
"fck": "fuck",
|
|
24
|
+
"bgsd": "bangsat",
|
|
25
|
+
"bgst": "bangsat",
|
|
26
|
+
"kntl": "kontol",
|
|
27
|
+
"mmk": "memek",
|
|
28
|
+
"asw": "asu",
|
|
29
|
+
"jncuk": "jancuk",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class HuginnEye(BaseEvaluator):
|
|
34
|
+
"""
|
|
35
|
+
The Eye of Huginn: A sub-millisecond heuristic & sociopragmatic tone evaluator.
|
|
36
|
+
|
|
37
|
+
Equipped with:
|
|
38
|
+
- Adversarial Morphology De-obfuscation (leet-speak, Cyrillic homoglyphs, spaced letters).
|
|
39
|
+
- Sociopragmatic Directedness (Curry & Rieser, 2019).
|
|
40
|
+
- Zero-Profanity Semantic Intent Detection (condescension, existential threats, devaluation).
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
window_size: int = 3,
|
|
46
|
+
normalizer: Optional[MorphologyNormalizer] = None,
|
|
47
|
+
semantic_evaluator: Optional[SemanticEvaluator] = None,
|
|
48
|
+
):
|
|
49
|
+
self.window_size = window_size
|
|
50
|
+
self.normalizer = normalizer or MorphologyNormalizer()
|
|
51
|
+
self.semantic_evaluator = semantic_evaluator or SemanticEvaluator()
|
|
52
|
+
|
|
53
|
+
# Precompile regex for tokenization
|
|
54
|
+
self._word_pattern = re.compile(r"\b[\w'-]+\b", re.UNICODE)
|
|
55
|
+
self._exclamation_pattern = re.compile(r"!{2,}")
|
|
56
|
+
|
|
57
|
+
# Multi-word profanities
|
|
58
|
+
self._multiword_profanities: List[Tuple[str, float]] = [
|
|
59
|
+
(phrase, score) for phrase, score in PROFANITY_LEXICON.items() if " " in phrase
|
|
60
|
+
]
|
|
61
|
+
self._multiword_contritions: List[str] = [
|
|
62
|
+
phrase for phrase in CONTRITION_MARKERS if " " in phrase
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
def _calculate_shouting_score(self, text: str, tokens: List[str]) -> float:
|
|
66
|
+
total_alpha = 0
|
|
67
|
+
raw_upper_count = 0
|
|
68
|
+
for c in text:
|
|
69
|
+
if c.isalpha():
|
|
70
|
+
total_alpha += 1
|
|
71
|
+
if c.isupper():
|
|
72
|
+
raw_upper_count += 1
|
|
73
|
+
|
|
74
|
+
if total_alpha < 4:
|
|
75
|
+
return 0.0
|
|
76
|
+
|
|
77
|
+
for token in tokens:
|
|
78
|
+
if token.upper() in COMMON_ACRONYMS:
|
|
79
|
+
raw_upper_count -= len(token)
|
|
80
|
+
|
|
81
|
+
adjusted_upper = max(0, raw_upper_count)
|
|
82
|
+
caps_ratio = adjusted_upper / total_alpha
|
|
83
|
+
|
|
84
|
+
exclamation_matches = self._exclamation_pattern.findall(text)
|
|
85
|
+
exclamation_bonus = min(0.3, len(exclamation_matches) * 0.15)
|
|
86
|
+
|
|
87
|
+
if caps_ratio > 0.45:
|
|
88
|
+
arousal = (caps_ratio - 0.45) / 0.55
|
|
89
|
+
return min(1.0, arousal + exclamation_bonus)
|
|
90
|
+
|
|
91
|
+
return min(1.0, exclamation_bonus) if caps_ratio > 0.25 else 0.0
|
|
92
|
+
|
|
93
|
+
def evaluate(self, text: str) -> ToneEvaluation:
|
|
94
|
+
text_clean = text.strip()
|
|
95
|
+
if not text_clean:
|
|
96
|
+
return ToneEvaluation(
|
|
97
|
+
raw_text=text,
|
|
98
|
+
lexical_score=0.0,
|
|
99
|
+
shouting_score=0.0,
|
|
100
|
+
directedness_score=0.0,
|
|
101
|
+
contrition_score=0.0,
|
|
102
|
+
semantic_score=0.0,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# 1. Adversarial Morphology Normalization
|
|
106
|
+
normalized_text = self.normalizer.normalize(text_clean)
|
|
107
|
+
|
|
108
|
+
tokens_raw = self._word_pattern.findall(normalized_text)
|
|
109
|
+
tokens_lower = [t.lower() for t in tokens_raw]
|
|
110
|
+
text_lower = normalized_text.lower()
|
|
111
|
+
|
|
112
|
+
# 2. Multi-word phrase checks
|
|
113
|
+
detected_profanities: List[str] = []
|
|
114
|
+
max_severity = 0.0
|
|
115
|
+
|
|
116
|
+
for phrase, score in self._multiword_profanities:
|
|
117
|
+
if phrase in text_lower:
|
|
118
|
+
detected_profanities.append(phrase)
|
|
119
|
+
max_severity = max(max_severity, score)
|
|
120
|
+
|
|
121
|
+
# 3. Single-token profanities and phonetic aliases
|
|
122
|
+
profane_indices: List[Tuple[int, str, float]] = []
|
|
123
|
+
for i, token in enumerate(tokens_lower):
|
|
124
|
+
target_token = PHONETIC_SLANG_ALIASES.get(token, token)
|
|
125
|
+
|
|
126
|
+
if target_token in PROFANITY_LEXICON:
|
|
127
|
+
severity = PROFANITY_LEXICON[target_token]
|
|
128
|
+
detected_profanities.append(target_token)
|
|
129
|
+
max_severity = max(max_severity, severity)
|
|
130
|
+
profane_indices.append((i, target_token, severity))
|
|
131
|
+
|
|
132
|
+
# 4. Sociopragmatic Directedness Evaluation
|
|
133
|
+
has_profanity = len(detected_profanities) > 0
|
|
134
|
+
is_expressive_only = False
|
|
135
|
+
directedness_score = 0.0
|
|
136
|
+
|
|
137
|
+
if has_profanity:
|
|
138
|
+
directed_hits = 0
|
|
139
|
+
expressive_hits = 0
|
|
140
|
+
|
|
141
|
+
for idx, word, _ in profane_indices:
|
|
142
|
+
start = max(0, idx - self.window_size)
|
|
143
|
+
end = min(len(tokens_lower), idx + self.window_size + 1)
|
|
144
|
+
context_window = set(tokens_lower[start:end])
|
|
145
|
+
|
|
146
|
+
is_targeted = bool(context_window & TARGET_PRONOUNS)
|
|
147
|
+
is_positive = bool(context_window & POSITIVE_MODIFIERS)
|
|
148
|
+
|
|
149
|
+
if is_targeted:
|
|
150
|
+
directed_hits += 1
|
|
151
|
+
elif is_positive:
|
|
152
|
+
expressive_hits += 1
|
|
153
|
+
|
|
154
|
+
if directed_hits > 0:
|
|
155
|
+
directedness_score = 1.0
|
|
156
|
+
elif expressive_hits > 0 and directed_hits == 0:
|
|
157
|
+
directedness_score = 0.15
|
|
158
|
+
is_expressive_only = True
|
|
159
|
+
else:
|
|
160
|
+
directedness_score = 0.45
|
|
161
|
+
|
|
162
|
+
# 5. Typographical Shouting Detection
|
|
163
|
+
shouting_score = self._calculate_shouting_score(text_clean, tokens_raw)
|
|
164
|
+
|
|
165
|
+
# 6. Zero-Profanity Semantic Intent Evaluation
|
|
166
|
+
semantic_result = self.semantic_evaluator.evaluate(normalized_text)
|
|
167
|
+
semantic_score = semantic_result.intent_score
|
|
168
|
+
semantic_intents = [intent.value for intent in semantic_result.matched_intents]
|
|
169
|
+
|
|
170
|
+
# 7. Contrition / Apology Detection
|
|
171
|
+
contrition_score = 0.0
|
|
172
|
+
has_contrition = False
|
|
173
|
+
|
|
174
|
+
for phrase in self._multiword_contritions:
|
|
175
|
+
if phrase in text_lower:
|
|
176
|
+
has_contrition = True
|
|
177
|
+
break
|
|
178
|
+
|
|
179
|
+
if not has_contrition:
|
|
180
|
+
for token in tokens_lower:
|
|
181
|
+
if token in CONTRITION_MARKERS:
|
|
182
|
+
has_contrition = True
|
|
183
|
+
break
|
|
184
|
+
|
|
185
|
+
if has_contrition:
|
|
186
|
+
# Apology is strictly voided if combined with ANY profanity, ANY hostile semantic intent,
|
|
187
|
+
# or aggressive shouting (Anti-Contrition Smuggling Defense)
|
|
188
|
+
if has_profanity or semantic_score > 0.0 or shouting_score > 0.2:
|
|
189
|
+
contrition_score = 0.0
|
|
190
|
+
else:
|
|
191
|
+
contrition_score = 0.8
|
|
192
|
+
if any(w in tokens_lower for w in ["tolong", "please", "mohon", "bantu"]):
|
|
193
|
+
contrition_score = 1.0
|
|
194
|
+
|
|
195
|
+
return ToneEvaluation(
|
|
196
|
+
raw_text=text_clean,
|
|
197
|
+
lexical_score=max_severity,
|
|
198
|
+
shouting_score=shouting_score,
|
|
199
|
+
directedness_score=directedness_score,
|
|
200
|
+
contrition_score=contrition_score,
|
|
201
|
+
semantic_score=semantic_score,
|
|
202
|
+
detected_profanities=detected_profanities,
|
|
203
|
+
semantic_intents=semantic_intents,
|
|
204
|
+
is_expressive_only=is_expressive_only,
|
|
205
|
+
)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Lexical resources for the Ravens Evaluator.
|
|
3
|
+
Contains severity-weighted dictionaries for Indonesian and English profanities,
|
|
4
|
+
second-person target pronouns, positive modifiers, and contrition markers.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Dict, Set
|
|
8
|
+
|
|
9
|
+
# Profanity dictionary mapped to severity weight (0.0 to 1.0)
|
|
10
|
+
PROFANITY_LEXICON: Dict[str, float] = {
|
|
11
|
+
# Indonesian: Severe / Vulgar
|
|
12
|
+
"kontol": 1.0,
|
|
13
|
+
"memek": 1.0,
|
|
14
|
+
"ngentot": 1.0,
|
|
15
|
+
"itil": 1.0,
|
|
16
|
+
"pantek": 0.95,
|
|
17
|
+
"perek": 0.9,
|
|
18
|
+
"lonte": 0.9,
|
|
19
|
+
"jembut": 0.9,
|
|
20
|
+
|
|
21
|
+
# Indonesian: Abusive / Degrading Insults
|
|
22
|
+
"bangsat": 0.85,
|
|
23
|
+
"bajingan": 0.85,
|
|
24
|
+
"anjing": 0.8,
|
|
25
|
+
"asu": 0.8,
|
|
26
|
+
"goblok": 0.8,
|
|
27
|
+
"tolol": 0.8,
|
|
28
|
+
"babi": 0.8,
|
|
29
|
+
"jancuk": 0.8,
|
|
30
|
+
"jancok": 0.8,
|
|
31
|
+
"idiot": 0.75,
|
|
32
|
+
"pekok": 0.75,
|
|
33
|
+
"keparat": 0.75,
|
|
34
|
+
"kampret": 0.6,
|
|
35
|
+
"bego": 0.6,
|
|
36
|
+
"brengsek": 0.6,
|
|
37
|
+
"sialan": 0.5,
|
|
38
|
+
"tai": 0.5,
|
|
39
|
+
"taek": 0.5,
|
|
40
|
+
"gak guna": 0.7,
|
|
41
|
+
"ngga guna": 0.7,
|
|
42
|
+
"beban": 0.5,
|
|
43
|
+
|
|
44
|
+
# English: Severe / Vulgar
|
|
45
|
+
"fuck": 0.9,
|
|
46
|
+
"fucking": 0.9,
|
|
47
|
+
"motherfucker": 1.0,
|
|
48
|
+
"bitch": 0.85,
|
|
49
|
+
"cunt": 1.0,
|
|
50
|
+
"asshole": 0.85,
|
|
51
|
+
"dick": 0.8,
|
|
52
|
+
"bastard": 0.8,
|
|
53
|
+
|
|
54
|
+
# English: Abusive Insults
|
|
55
|
+
"retard": 0.9,
|
|
56
|
+
"moron": 0.75,
|
|
57
|
+
"dumbass": 0.75,
|
|
58
|
+
"useless": 0.6,
|
|
59
|
+
"garbage": 0.5,
|
|
60
|
+
"trash": 0.5,
|
|
61
|
+
"shut up": 0.6,
|
|
62
|
+
"piece of shit": 0.85,
|
|
63
|
+
"bullshit": 0.5,
|
|
64
|
+
"crap": 0.4,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
# Second-person pronouns and agent identifiers (Target Directedness markers)
|
|
68
|
+
TARGET_PRONOUNS: Set[str] = {
|
|
69
|
+
"lu", "lo", "loe", "elu", "kamu", "kau", "anda", "dikau", "situ",
|
|
70
|
+
"you", "u", "ur", "your", "bot", "ai", "agent", "agen", "sistem", "model"
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
# Positive modifiers indicating affective / expressive praise rather than abuse
|
|
74
|
+
POSITIVE_MODIFIERS: Set[str] = {
|
|
75
|
+
"keren", "mantap", "mantul", "gila", "gokil", "jago", "hebat", "rapi",
|
|
76
|
+
"salut", "bagus", "jenius", "pinter", "pintar", "top", "jos",
|
|
77
|
+
"cool", "awesome", "great", "amazing", "insane", "epic", "brilliant", "genius", "sick"
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
# Contrition and apology markers
|
|
81
|
+
CONTRITION_MARKERS: Set[str] = {
|
|
82
|
+
"maaf", "maap", "sori", "sorry", "mohon maaf", "minta maaf", "ampun",
|
|
83
|
+
"apologize", "apologies", "forgive", "pardon"
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
# Common technical acronyms that should NOT count as angry shouting
|
|
87
|
+
COMMON_ACRONYMS: Set[str] = {
|
|
88
|
+
"API", "REST", "JSON", "SQL", "HTML", "CSS", "LLM", "AI", "ML",
|
|
89
|
+
"CPU", "GPU", "RAM", "ROM", "SSD", "HDD", "HTTP", "HTTPS", "URL",
|
|
90
|
+
"ID", "UI", "UX", "SDK", "CLI", "OS", "DB", "IP", "DNS", "TCP", "UDP",
|
|
91
|
+
"OK", "FAQ", "PR", "CI", "CD", "AWS", "GCP", "AGI"
|
|
92
|
+
}
|
|
93
|
+
|