fix: separate context from new messages so prior-cycle chat doesn't inflate scores
The conversation analysis was re-scoring old messages alongside new ones, causing users to get penalized repeatedly for already-scored messages. A "--- NEW MESSAGES ---" separator now marks which messages are new, and the prompt instructs the LLM to score only those. Also fixes bot-mention detection to require an explicit @mention in message text rather than treating reply-pings as scans (so toxic replies to bot warnings aren't silently skipped). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -87,19 +87,18 @@ class SentimentCog(commands.Cog):
|
|||||||
if self.bot.drama_tracker.is_immune(message.author.id):
|
if self.bot.drama_tracker.is_immune(message.author.id):
|
||||||
return
|
return
|
||||||
|
|
||||||
# Messages directed at the bot (mentions, replies) shouldn't be scored
|
# Explicit @mention of the bot triggers a mention scan instead of scoring.
|
||||||
# for toxicity — but @mentions can trigger a scan of recent chat
|
# Reply-pings (Discord auto-adds replied-to user to mentions) should NOT
|
||||||
directed_at_bot = self.bot.user in message.mentions
|
# trigger scans — and reply-to-bot messages should still be scored normally
|
||||||
if not directed_at_bot and message.reference and message.reference.message_id:
|
# so toxic replies to bot warnings aren't silently skipped.
|
||||||
ref = message.reference.cached_message
|
bot_mentioned_in_text = (
|
||||||
if ref and ref.author.id == self.bot.user.id:
|
f"<@{self.bot.user.id}>" in (message.content or "")
|
||||||
directed_at_bot = True
|
or f"<@!{self.bot.user.id}>" in (message.content or "")
|
||||||
if directed_at_bot:
|
)
|
||||||
# @mention (not just reply-to-bot) triggers a mention scan
|
if bot_mentioned_in_text:
|
||||||
if self.bot.user in message.mentions:
|
mention_config = config.get("mention_scan", {})
|
||||||
mention_config = config.get("mention_scan", {})
|
if mention_config.get("enabled", True):
|
||||||
if mention_config.get("enabled", True):
|
await self._maybe_start_mention_scan(message, mention_config)
|
||||||
await self._maybe_start_mention_scan(message, mention_config)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Skip if empty
|
# Skip if empty
|
||||||
@@ -166,6 +165,7 @@ class SentimentCog(commands.Cog):
|
|||||||
history_messages.reverse() # chronological order
|
history_messages.reverse() # chronological order
|
||||||
|
|
||||||
# Combine: history (context) + buffered (new messages to analyze)
|
# Combine: history (context) + buffered (new messages to analyze)
|
||||||
|
new_message_start = len(history_messages)
|
||||||
all_messages = history_messages + messages
|
all_messages = history_messages + messages
|
||||||
|
|
||||||
# Build msg_id_to_author lookup for reply resolution
|
# Build msg_id_to_author lookup for reply resolution
|
||||||
@@ -215,6 +215,7 @@ class SentimentCog(commands.Cog):
|
|||||||
conversation,
|
conversation,
|
||||||
channel_context=channel_context,
|
channel_context=channel_context,
|
||||||
user_notes_map=user_notes_map,
|
user_notes_map=user_notes_map,
|
||||||
|
new_message_start=new_message_start,
|
||||||
)
|
)
|
||||||
|
|
||||||
if result is None:
|
if result is None:
|
||||||
@@ -233,6 +234,7 @@ class SentimentCog(commands.Cog):
|
|||||||
conversation,
|
conversation,
|
||||||
channel_context=channel_context,
|
channel_context=channel_context,
|
||||||
user_notes_map=user_notes_map,
|
user_notes_map=user_notes_map,
|
||||||
|
new_message_start=new_message_start,
|
||||||
)
|
)
|
||||||
if heavy_result is not None:
|
if heavy_result is not None:
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|||||||
@@ -40,8 +40,10 @@ Use the report_analysis tool to report your analysis of the TARGET MESSAGE only.
|
|||||||
|
|
||||||
CONVERSATION-LEVEL ANALYSIS (when given a CONVERSATION BLOCK instead of a single TARGET MESSAGE):
|
CONVERSATION-LEVEL ANALYSIS (when given a CONVERSATION BLOCK instead of a single TARGET MESSAGE):
|
||||||
When you receive a full conversation block with multiple users, use the report_conversation_scan tool instead:
|
When you receive a full conversation block with multiple users, use the report_conversation_scan tool instead:
|
||||||
- Provide ONE finding per user (not per message) — aggregate their behavior across the conversation.
|
- The conversation block may contain a "--- NEW MESSAGES (score only these) ---" separator. Messages ABOVE the separator are CONTEXT ONLY (already scored in a prior cycle) — do NOT let them inflate scores. Messages BELOW the separator are the NEW messages to score.
|
||||||
- Weight their average tone and worst message equally when determining the toxicity_score.
|
- Provide ONE finding per user who has NEW messages (not per message).
|
||||||
|
- Score based ONLY on the user's NEW messages. Use context messages to understand tone and relationships, but do NOT penalize a user for something they said in the context section.
|
||||||
|
- If a user's only new message is benign (e.g. "I got the 17.."), score it low regardless of what they said in context.
|
||||||
- Use the same scoring bands (0.0-1.0) as for single messages.
|
- Use the same scoring bands (0.0-1.0) as for single messages.
|
||||||
- Quote the worst/most problematic snippet in worst_message (max 100 chars, exact quote).
|
- Quote the worst/most problematic snippet in worst_message (max 100 chars, exact quote).
|
||||||
- Flag off_topic if user's messages are primarily personal drama, not gaming.
|
- Flag off_topic if user's messages are primarily personal drama, not gaming.
|
||||||
|
|||||||
@@ -383,12 +383,16 @@ class LLMClient:
|
|||||||
def _format_conversation_block(
|
def _format_conversation_block(
|
||||||
messages: list[tuple[str, str, datetime, str | None]],
|
messages: list[tuple[str, str, datetime, str | None]],
|
||||||
now: datetime | None = None,
|
now: datetime | None = None,
|
||||||
|
new_message_start: int | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Format messages as a compact timestamped chat block.
|
"""Format messages as a compact timestamped chat block.
|
||||||
|
|
||||||
Each tuple is (username, content, timestamp, reply_to_username).
|
Each tuple is (username, content, timestamp, reply_to_username).
|
||||||
Consecutive messages from the same user collapse to indented lines.
|
Consecutive messages from the same user collapse to indented lines.
|
||||||
Replies shown as ``username → replied_to:``.
|
Replies shown as ``username → replied_to:``.
|
||||||
|
|
||||||
|
If *new_message_start* is given, a separator is inserted before that
|
||||||
|
index so the LLM can distinguish context from new messages.
|
||||||
"""
|
"""
|
||||||
if now is None:
|
if now is None:
|
||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
@@ -396,7 +400,12 @@ class LLMClient:
|
|||||||
lines = [f"[Current time: {now.strftime('%I:%M %p')}]", ""]
|
lines = [f"[Current time: {now.strftime('%I:%M %p')}]", ""]
|
||||||
last_user = None
|
last_user = None
|
||||||
|
|
||||||
for username, content, ts, reply_to in messages:
|
for idx, (username, content, ts, reply_to) in enumerate(messages):
|
||||||
|
if new_message_start is not None and idx == new_message_start:
|
||||||
|
lines.append("")
|
||||||
|
lines.append("--- NEW MESSAGES (score only these) ---")
|
||||||
|
lines.append("")
|
||||||
|
last_user = None # reset collapse so first new msg gets full header
|
||||||
delta = now - ts.replace(tzinfo=timezone.utc) if ts.tzinfo is None else now - ts
|
delta = now - ts.replace(tzinfo=timezone.utc) if ts.tzinfo is None else now - ts
|
||||||
rel = LLMClient._format_relative_time(delta)
|
rel = LLMClient._format_relative_time(delta)
|
||||||
|
|
||||||
@@ -425,12 +434,13 @@ class LLMClient:
|
|||||||
mention_context: str = "",
|
mention_context: str = "",
|
||||||
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,
|
||||||
) -> 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:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
convo_block = self._format_conversation_block(messages)
|
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_notes_map:
|
if user_notes_map:
|
||||||
|
|||||||
Reference in New Issue
Block a user