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.
Files changed (98) hide show
  1. corvin/__init__.py +101 -0
  2. corvin/core/__init__.py +18 -0
  3. corvin/core/exceptions.py +16 -0
  4. corvin/core/models.py +70 -0
  5. corvin/core/standing.py +41 -0
  6. corvin/core/state.py +62 -0
  7. corvin/evaluators/__init__.py +21 -0
  8. corvin/evaluators/base.py +46 -0
  9. corvin/evaluators/heuristic.py +205 -0
  10. corvin/evaluators/lexicon.py +93 -0
  11. corvin/guard/__init__.py +8 -0
  12. corvin/guard/core.py +296 -0
  13. corvin/guard/decorator.py +119 -0
  14. corvin/math/__init__.py +20 -0
  15. corvin/math/decay.py +58 -0
  16. corvin/math/friction.py +44 -0
  17. corvin/math/scoring.py +80 -0
  18. corvin/morphology/__init__.py +21 -0
  19. corvin/morphology/cleaners.py +57 -0
  20. corvin/morphology/fuzzy.py +77 -0
  21. corvin/morphology/homoglyphs.py +63 -0
  22. corvin/morphology/normalizer.py +76 -0
  23. corvin/morphology/zalgo.py +40 -0
  24. corvin/policies/__init__.py +19 -0
  25. corvin/policies/base.py +26 -0
  26. corvin/policies/notifier.py +62 -0
  27. corvin/policies/presets.py +107 -0
  28. corvin/resilience/__init__.py +14 -0
  29. corvin/resilience/cache.py +94 -0
  30. corvin/resilience/friction.py +50 -0
  31. corvin/scrubbers/__init__.py +8 -0
  32. corvin/scrubbers/patterns.py +38 -0
  33. corvin/scrubbers/stripper.py +102 -0
  34. corvin/security/__init__.py +12 -0
  35. corvin/security/signer.py +65 -0
  36. corvin/semantic/__init__.py +13 -0
  37. corvin/semantic/evaluator.py +84 -0
  38. corvin/semantic/patterns.py +58 -0
  39. corvin/semantic/velocity.py +60 -0
  40. corvin/storage/__init__.py +12 -0
  41. corvin/storage/base.py +60 -0
  42. corvin/storage/memory.py +101 -0
  43. corvin/storage/redis.py +211 -0
  44. corvin/storage/sqlite.py +267 -0
  45. corvin/streaming/__init__.py +8 -0
  46. corvin/streaming/decorator.py +108 -0
  47. corvin/streaming/interceptor.py +81 -0
  48. corvin-0.7.0.dist-info/METADATA +246 -0
  49. corvin-0.7.0.dist-info/RECORD +98 -0
  50. corvin-0.7.0.dist-info/WHEEL +4 -0
  51. corvin-0.7.0.dist-info/licenses/LICENSE +177 -0
  52. ravens/__init__.py +101 -0
  53. ravens/core/__init__.py +18 -0
  54. ravens/core/exceptions.py +16 -0
  55. ravens/core/models.py +70 -0
  56. ravens/core/standing.py +41 -0
  57. ravens/core/state.py +62 -0
  58. ravens/evaluators/__init__.py +21 -0
  59. ravens/evaluators/base.py +46 -0
  60. ravens/evaluators/heuristic.py +205 -0
  61. ravens/evaluators/lexicon.py +93 -0
  62. ravens/guard/__init__.py +8 -0
  63. ravens/guard/core.py +296 -0
  64. ravens/guard/decorator.py +119 -0
  65. ravens/math/__init__.py +20 -0
  66. ravens/math/decay.py +58 -0
  67. ravens/math/friction.py +44 -0
  68. ravens/math/scoring.py +80 -0
  69. ravens/morphology/__init__.py +21 -0
  70. ravens/morphology/cleaners.py +57 -0
  71. ravens/morphology/fuzzy.py +77 -0
  72. ravens/morphology/homoglyphs.py +63 -0
  73. ravens/morphology/normalizer.py +76 -0
  74. ravens/morphology/zalgo.py +40 -0
  75. ravens/policies/__init__.py +19 -0
  76. ravens/policies/base.py +26 -0
  77. ravens/policies/notifier.py +62 -0
  78. ravens/policies/presets.py +107 -0
  79. ravens/resilience/__init__.py +14 -0
  80. ravens/resilience/cache.py +94 -0
  81. ravens/resilience/friction.py +50 -0
  82. ravens/scrubbers/__init__.py +8 -0
  83. ravens/scrubbers/patterns.py +38 -0
  84. ravens/scrubbers/stripper.py +102 -0
  85. ravens/security/__init__.py +12 -0
  86. ravens/security/signer.py +65 -0
  87. ravens/semantic/__init__.py +13 -0
  88. ravens/semantic/evaluator.py +84 -0
  89. ravens/semantic/patterns.py +58 -0
  90. ravens/semantic/velocity.py +60 -0
  91. ravens/storage/__init__.py +12 -0
  92. ravens/storage/base.py +60 -0
  93. ravens/storage/memory.py +101 -0
  94. ravens/storage/redis.py +211 -0
  95. ravens/storage/sqlite.py +267 -0
  96. ravens/streaming/__init__.py +8 -0
  97. ravens/streaming/decorator.py +108 -0
  98. 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
+ ]
@@ -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)
@@ -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
+
@@ -0,0 +1,8 @@
1
+ from .core import RavenGuard
2
+ from .decorator import create_protect_decorator
3
+
4
+ __all__ = [
5
+ "RavenGuard",
6
+ "create_protect_decorator",
7
+ ]
8
+