feat(ai-moderation): use per-server nickname (displayName) in analysis payload
- resolveDisplayName(): member.displayName from captured metadata, falls back to global username - Applied to context lines, target message blocks, and media message blocks — LLM sees the name the channel actually sees (nickname can carry moderation signal itself)
This commit is contained in:
@@ -6,6 +6,7 @@ import {
|
|||||||
} from "../message-capture/messageMetadata.js";
|
} from "../message-capture/messageMetadata.js";
|
||||||
import type { MessageRecord } from "../message-capture/types.js";
|
import type { MessageRecord } from "../message-capture/types.js";
|
||||||
import { sanitizeDiscordTokens } from "./discordTokens.js";
|
import { sanitizeDiscordTokens } from "./discordTokens.js";
|
||||||
|
import { resolveDisplayName } from "./moderationBuilders.js";
|
||||||
|
|
||||||
const logger = createChildLogger("conversationContext");
|
const logger = createChildLogger("conversationContext");
|
||||||
|
|
||||||
@@ -130,7 +131,7 @@ export function formatMessageForPrompt(
|
|||||||
const mediaEvidence = formatMediaEvidenceForPrompt(msg.metadata);
|
const mediaEvidence = formatMediaEvidenceForPrompt(msg.metadata);
|
||||||
const mediaSuffix = mediaEvidence ? ` ${mediaEvidence}` : "";
|
const mediaSuffix = mediaEvidence ? ` ${mediaEvidence}` : "";
|
||||||
const refInfo = formatReferenceInfo(msg);
|
const refInfo = formatReferenceInfo(msg);
|
||||||
return `[${label}] id=${msg.id} time=${timestamp} user=${msg.username}: ${content}${mediaSuffix}${refInfo}`;
|
return `[${label}] id=${msg.id} time=${timestamp} user=${resolveDisplayName(msg)}: ${content}${mediaSuffix}${refInfo}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -36,6 +36,27 @@ export function getAnalysisContent(message: MessageRecord): string {
|
|||||||
).trim();
|
).trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Server nickname (member.displayName) when captured, else the author
|
||||||
|
* username. Discord shows the server nickname to other members, so the LLM
|
||||||
|
* should see the same name the channel sees — and a nickname can carry
|
||||||
|
* moderation signal itself (offensive nick + clean message → low warn).
|
||||||
|
*/
|
||||||
|
export function resolveDisplayName(msg: MessageRecord): string {
|
||||||
|
if (msg.metadata) {
|
||||||
|
try {
|
||||||
|
const meta = JSON.parse(msg.metadata) as {
|
||||||
|
member?: { displayName?: string | null } | null;
|
||||||
|
};
|
||||||
|
const dn = meta?.member?.displayName;
|
||||||
|
if (dn && dn.trim().length > 0) return dn;
|
||||||
|
} catch {
|
||||||
|
// malformed metadata — fall back to username
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return msg.username;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds a <reference> XML element for reply/forward/crosspost context.
|
* Builds a <reference> XML element for reply/forward/crosspost context.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
buildReferenceXml,
|
buildReferenceXml,
|
||||||
escapeXml,
|
escapeXml,
|
||||||
getAnalysisContent,
|
getAnalysisContent,
|
||||||
|
resolveDisplayName,
|
||||||
} from "./moderationBuilders.js";
|
} from "./moderationBuilders.js";
|
||||||
import {
|
import {
|
||||||
buildSystemPrompt as buildSystemPromptModular,
|
buildSystemPrompt as buildSystemPromptModular,
|
||||||
@@ -308,7 +309,7 @@ export async function runTextOnlyBatch(
|
|||||||
const userCtx = userContexts.get(msg.user_id) ?? "";
|
const userCtx = userContexts.get(msg.user_id) ?? "";
|
||||||
const userProfileCtx = userProfiles.get(msg.user_id) ?? "";
|
const userProfileCtx = userProfiles.get(msg.user_id) ?? "";
|
||||||
const refXml = await buildReferenceXml(msg);
|
const refXml = await buildReferenceXml(msg);
|
||||||
return `<message id="${msg.id}" user="${msg.username}">\n ${userCtx}${userProfileCtx ? `\n ${userProfileCtx}` : ""}${refXml ? `\n ${refXml}` : ""}\n <content>${escapeXml(content)}</content>${webContext}${mediaEvidenceCtx}\n</message>`;
|
return `<message id="${msg.id}" user="${resolveDisplayName(msg)}">\n ${userCtx}${userProfileCtx ? `\n ${userProfileCtx}` : ""}${refXml ? `\n ${refXml}` : ""}\n <content>${escapeXml(content)}</content>${webContext}${mediaEvidenceCtx}\n</message>`;
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
).join("\n");
|
).join("\n");
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import {
|
|||||||
buildReferenceXml,
|
buildReferenceXml,
|
||||||
escapeXml,
|
escapeXml,
|
||||||
getAnalysisContent,
|
getAnalysisContent,
|
||||||
|
resolveDisplayName,
|
||||||
} from "./moderationBuilders.js";
|
} from "./moderationBuilders.js";
|
||||||
import {
|
import {
|
||||||
buildCustomEmojiVisionPrompt,
|
buildCustomEmojiVisionPrompt,
|
||||||
@@ -367,6 +368,6 @@ export async function prepareMediaMessage(
|
|||||||
const profile = await getUserProfile(target.user_id);
|
const profile = await getUserProfile(target.user_id);
|
||||||
const refXml = await buildReferenceXml(target);
|
const refXml = await buildReferenceXml(target);
|
||||||
|
|
||||||
const messageBlock = `<message id="${escapeXml(target.id)}" user="${escapeXml(target.username)}">\n <user_reputation trust_score="${rep.trust_score}" />${profile ? `\n <user_profile>${sanitizeAiContent(profile.profile_summary)}</user_profile>` : ""}${refXml ? `\n ${refXml}` : ""}\n <content>${escapeXml(content)}</content>${mediaContext ? ` ${escapeXml(mediaContext)}` : ""}${webContext}${mediaAnalysisContext}${searxngXml}\n</message>`;
|
const messageBlock = `<message id="${escapeXml(target.id)}" user="${escapeXml(resolveDisplayName(target))}">\n <user_reputation trust_score="${rep.trust_score}" />${profile ? `\n <user_profile>${sanitizeAiContent(profile.profile_summary)}</user_profile>` : ""}${refXml ? `\n ${refXml}` : ""}\n <content>${escapeXml(content)}</content>${mediaContext ? ` ${escapeXml(mediaContext)}` : ""}${webContext}${mediaAnalysisContext}${searxngXml}\n</message>`;
|
||||||
return { targetId, messageBlock };
|
return { targetId, messageBlock };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { describe, expect, it } from "vitest";
|
|||||||
import {
|
import {
|
||||||
buildConversationContext,
|
buildConversationContext,
|
||||||
buildLocationContext,
|
buildLocationContext,
|
||||||
|
formatMessageForPrompt,
|
||||||
} from "../src/modules/ai-moderation/conversationContext.js";
|
} from "../src/modules/ai-moderation/conversationContext.js";
|
||||||
import { extractOgMeta } from "../src/modules/ai-moderation/urlFetcher.js";
|
import { extractOgMeta } from "../src/modules/ai-moderation/urlFetcher.js";
|
||||||
import type { MessageRecord } from "../src/modules/message-capture/types.js";
|
import type { MessageRecord } from "../src/modules/message-capture/types.js";
|
||||||
@@ -139,6 +140,30 @@ describe("buildConversationContext — recency gating", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("formatMessageForPrompt — server nickname (displayName)", () => {
|
||||||
|
it("renders member.displayName when captured (per-server nickname)", () => {
|
||||||
|
const m = msg("n1", NOW - MIN);
|
||||||
|
m.metadata = JSON.stringify({
|
||||||
|
member: {
|
||||||
|
displayName: "Si Goblok Server",
|
||||||
|
roles: [],
|
||||||
|
joinedTimestamp: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const line = formatMessageForPrompt(m, "context");
|
||||||
|
expect(line).toContain("user=Si Goblok Server");
|
||||||
|
expect(line).not.toContain("user_user_n1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to global username when displayName missing", () => {
|
||||||
|
const line = formatMessageForPrompt(
|
||||||
|
msg("n2", NOW - MIN, "halo"),
|
||||||
|
"context",
|
||||||
|
);
|
||||||
|
expect(line).toContain("user=user_n2");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("buildLocationContext — channel/thread/nsfw enrichment", () => {
|
describe("buildLocationContext — channel/thread/nsfw enrichment", () => {
|
||||||
it("renders channel name + thread name from captured metadata", () => {
|
it("renders channel name + thread name from captured metadata", () => {
|
||||||
const t = target();
|
const t = target();
|
||||||
|
|||||||
Reference in New Issue
Block a user