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:
2026-02-27 10:22:57 -05:00
parent a73d2505d9
commit ad1234ec99
3 changed files with 51 additions and 5 deletions

View File

@@ -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:

View File

@@ -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

View File

@@ -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: