feat: add user alias mapping for jealousy detection context
Adds user_aliases config section mapping Discord IDs to known nicknames. Aliases are anonymized and injected into LLM analysis context so it can recognize when someone name-drops another member (even absent ones). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -253,6 +253,38 @@ class SentimentCog(commands.Cog):
|
|||||||
"""Replace display name keys with anonymous keys in user notes map."""
|
"""Replace display name keys with anonymous keys in user notes map."""
|
||||||
return {anon_map.get(name, name): notes for name, notes in user_notes_map.items()}
|
return {anon_map.get(name, name): notes for name, notes in user_notes_map.items()}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _build_alias_context(
|
||||||
|
messages: list[discord.Message],
|
||||||
|
anon_map: dict[str, str],
|
||||||
|
alias_config: dict,
|
||||||
|
) -> str:
|
||||||
|
"""Build anonymized alias context string for the LLM.
|
||||||
|
|
||||||
|
Maps user IDs from messages to their known nicknames using the
|
||||||
|
config, then replaces display names with anonymous keys.
|
||||||
|
"""
|
||||||
|
if not alias_config:
|
||||||
|
return ""
|
||||||
|
lines = []
|
||||||
|
seen_ids: set[int] = set()
|
||||||
|
for msg in messages:
|
||||||
|
uid = msg.author.id
|
||||||
|
if uid in seen_ids:
|
||||||
|
continue
|
||||||
|
seen_ids.add(uid)
|
||||||
|
aliases = alias_config.get(uid)
|
||||||
|
if aliases:
|
||||||
|
anon_key = anon_map.get(msg.author.display_name, msg.author.display_name)
|
||||||
|
lines.append(f" {anon_key} is also known as: {', '.join(aliases)}")
|
||||||
|
# Also include aliases for members NOT in the conversation (so the LLM
|
||||||
|
# can recognize name-drops of absent members)
|
||||||
|
for uid, aliases in alias_config.items():
|
||||||
|
uid = int(uid) if isinstance(uid, str) else uid
|
||||||
|
if uid not in seen_ids:
|
||||||
|
lines.append(f" (not in chat) also known as: {', '.join(aliases)}")
|
||||||
|
return "\n".join(lines) if lines else ""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _deanonymize_findings(result: dict, anon_map: dict[str, str]) -> None:
|
def _deanonymize_findings(result: dict, anon_map: dict[str, str]) -> None:
|
||||||
"""Replace anonymous keys back to display names in LLM findings (in-place)."""
|
"""Replace anonymous keys back to display names in LLM findings (in-place)."""
|
||||||
@@ -466,6 +498,9 @@ class SentimentCog(commands.Cog):
|
|||||||
anon_conversation = self._anonymize_conversation(conversation, anon_map)
|
anon_conversation = self._anonymize_conversation(conversation, anon_map)
|
||||||
anon_notes = self._anonymize_notes(user_notes_map, anon_map) if user_notes_map else user_notes_map
|
anon_notes = self._anonymize_notes(user_notes_map, anon_map) if user_notes_map else user_notes_map
|
||||||
|
|
||||||
|
alias_config = config.get("user_aliases", {})
|
||||||
|
alias_context = self._build_alias_context(all_messages, anon_map, alias_config)
|
||||||
|
|
||||||
channel_context = build_channel_context(ref_message, game_channels)
|
channel_context = build_channel_context(ref_message, game_channels)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -480,6 +515,7 @@ class SentimentCog(commands.Cog):
|
|||||||
channel_context=channel_context,
|
channel_context=channel_context,
|
||||||
user_notes_map=anon_notes,
|
user_notes_map=anon_notes,
|
||||||
new_message_start=new_message_start,
|
new_message_start=new_message_start,
|
||||||
|
user_aliases=alias_context,
|
||||||
)
|
)
|
||||||
|
|
||||||
if result is None:
|
if result is None:
|
||||||
@@ -499,6 +535,7 @@ class SentimentCog(commands.Cog):
|
|||||||
channel_context=channel_context,
|
channel_context=channel_context,
|
||||||
user_notes_map=anon_notes,
|
user_notes_map=anon_notes,
|
||||||
new_message_start=new_message_start,
|
new_message_start=new_message_start,
|
||||||
|
user_aliases=alias_context,
|
||||||
)
|
)
|
||||||
if heavy_result is not None:
|
if heavy_result is not None:
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -638,6 +675,9 @@ class SentimentCog(commands.Cog):
|
|||||||
anon_conversation = self._anonymize_conversation(conversation, anon_map)
|
anon_conversation = self._anonymize_conversation(conversation, anon_map)
|
||||||
anon_notes = self._anonymize_notes(user_notes_map, anon_map) if user_notes_map else user_notes_map
|
anon_notes = self._anonymize_notes(user_notes_map, anon_map) if user_notes_map else user_notes_map
|
||||||
|
|
||||||
|
alias_config = config.get("user_aliases", {})
|
||||||
|
alias_context = self._build_alias_context(raw_messages, anon_map, alias_config)
|
||||||
|
|
||||||
channel_context = build_channel_context(raw_messages[0], game_channels)
|
channel_context = build_channel_context(raw_messages[0], game_channels)
|
||||||
mention_context = (
|
mention_context = (
|
||||||
f"A user flagged this conversation and said: \"{mention_text}\"\n"
|
f"A user flagged this conversation and said: \"{mention_text}\"\n"
|
||||||
@@ -650,6 +690,7 @@ class SentimentCog(commands.Cog):
|
|||||||
mention_context=mention_context,
|
mention_context=mention_context,
|
||||||
channel_context=channel_context,
|
channel_context=channel_context,
|
||||||
user_notes_map=anon_notes,
|
user_notes_map=anon_notes,
|
||||||
|
user_aliases=alias_context,
|
||||||
)
|
)
|
||||||
|
|
||||||
if result is None:
|
if result is None:
|
||||||
|
|||||||
12
config.yaml
12
config.yaml
@@ -21,6 +21,13 @@ sentiment:
|
|||||||
escalation_threshold: 0.25 # Triage toxicity score that triggers re-analysis with heavy model
|
escalation_threshold: 0.25 # Triage toxicity score that triggers re-analysis with heavy model
|
||||||
escalation_boost: 0.04 # Per-message drama boost after warning (sustained toxicity ramps toward mute)
|
escalation_boost: 0.04 # Per-message drama boost after warning (sustained toxicity ramps toward mute)
|
||||||
|
|
||||||
|
# Nicknames/aliases for server members. Used by the LLM to recognize
|
||||||
|
# when someone references another member by name in chat.
|
||||||
|
user_aliases:
|
||||||
|
684222822272598058: ["Mark", "Limit"] # thelimitations
|
||||||
|
1113144994790903908: ["Glam", "G"] # Glamgirlxx
|
||||||
|
1195191381929508964: ["Bree"] # QueenBree10
|
||||||
|
|
||||||
game_channels:
|
game_channels:
|
||||||
gta-online: "GTA Online"
|
gta-online: "GTA Online"
|
||||||
battlefield: "Battlefield"
|
battlefield: "Battlefield"
|
||||||
@@ -135,11 +142,6 @@ polls:
|
|||||||
duration_hours: 4
|
duration_hours: 4
|
||||||
cooldown_minutes: 60 # Per-channel cooldown between auto-polls
|
cooldown_minutes: 60 # Per-channel cooldown between auto-polls
|
||||||
|
|
||||||
wordle:
|
|
||||||
enabled: true
|
|
||||||
bot_name: "Wordle" # Discord bot name to watch for
|
|
||||||
reply_chance: 0.75 # Chance to comment on result summaries (0.0-1.0)
|
|
||||||
playing_reply_chance: 0.0 # Chance to comment on "was playing" messages (0 = never)
|
|
||||||
|
|
||||||
coherence:
|
coherence:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|||||||
@@ -489,6 +489,7 @@ class LLMClient:
|
|||||||
channel_context: str = "",
|
channel_context: str = "",
|
||||||
user_notes_map: dict[str, str] | None = None,
|
user_notes_map: dict[str, str] | None = None,
|
||||||
new_message_start: int | None = None,
|
new_message_start: int | None = None,
|
||||||
|
user_aliases: str = "",
|
||||||
) -> dict | None:
|
) -> dict | None:
|
||||||
"""Analyze a conversation block in one call, returning per-user findings."""
|
"""Analyze a conversation block in one call, returning per-user findings."""
|
||||||
if not messages:
|
if not messages:
|
||||||
@@ -497,6 +498,8 @@ class LLMClient:
|
|||||||
convo_block = self._format_conversation_block(messages, new_message_start=new_message_start)
|
convo_block = self._format_conversation_block(messages, new_message_start=new_message_start)
|
||||||
|
|
||||||
user_content = f"=== CONVERSATION BLOCK ===\n{convo_block}\n\n"
|
user_content = f"=== CONVERSATION BLOCK ===\n{convo_block}\n\n"
|
||||||
|
if user_aliases:
|
||||||
|
user_content += f"=== KNOWN MEMBER ALIASES (names other members use to refer to each other) ===\n{user_aliases}\n\n"
|
||||||
if user_notes_map:
|
if user_notes_map:
|
||||||
notes_lines = [f" {u}: {n}" for u, n in user_notes_map.items() if n]
|
notes_lines = [f" {u}: {n}" for u, n in user_notes_map.items() if n]
|
||||||
if notes_lines:
|
if notes_lines:
|
||||||
|
|||||||
Reference in New Issue
Block a user