feat: label AI-generated conversation messages
This commit is contained in:
1 parent
3beb8aada2
commit
5ad7f1bf77
10 files changed
+69
-11
No files matched your search
@@ -7,6 +7,7 @@ import type { ChatService } from './chat/service.js';
|
||||
import {
|
||||
buildConversationState,
|
||||
chooseConversationIntent,
|
||||
conversationMessageFingerprint,
|
||||
MAX_AUTOMATED_CONVERSATION_TURNS,
|
||||
mergeConversationState,
|
||||
remainingAutomationTurns,
|
||||
@@ -101,6 +102,7 @@ export class ConversationAutomationService {
|
||||
state.askedForMedia = true;
|
||||
state.automation.turnsUsed += 1;
|
||||
state.automation.lastIncomingKey = incomingKey;
|
||||
state.automation.aiMessageFingerprints.push(conversationMessageFingerprint(reply));
|
||||
const exhausted = state.automation.turnsUsed >= MAX_AUTOMATED_CONVERSATION_TURNS;
|
||||
if (exhausted) {
|
||||
state.automation.status = 'human-required';
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
|
||||
export interface ConversationMessage {
|
||||
sender: string;
|
||||
text: string;
|
||||
@@ -45,6 +47,8 @@ export interface ConversationAutomationState {
|
||||
status: ConversationAutomationStatus;
|
||||
lastIncomingKey: string | null;
|
||||
handoffReason: string | null;
|
||||
/** SHA-256 fingerprints avoid storing a second copy of generated message text. */
|
||||
aiMessageFingerprints: string[];
|
||||
}
|
||||
|
||||
export function createConversationAutomationState(): ConversationAutomationState {
|
||||
@@ -53,9 +57,14 @@ export function createConversationAutomationState(): ConversationAutomationState
|
||||
status: 'active',
|
||||
lastIncomingKey: null,
|
||||
handoffReason: null,
|
||||
aiMessageFingerprints: [],
|
||||
};
|
||||
}
|
||||
|
||||
export function conversationMessageFingerprint(message: string): string {
|
||||
return createHash('sha256').update(message.trim()).digest('hex');
|
||||
}
|
||||
|
||||
export function remainingAutomationTurns(state: ConversationState): number {
|
||||
if (state.automation.status !== 'active') return 0;
|
||||
return Math.max(0, MAX_AUTOMATED_CONVERSATION_TURNS - state.automation.turnsUsed);
|
||||
@@ -222,6 +231,9 @@ function parseAutomationState(value: unknown): ConversationAutomationState {
|
||||
status,
|
||||
lastIncomingKey: typeof parsed.lastIncomingKey === 'string' ? parsed.lastIncomingKey : null,
|
||||
handoffReason: typeof parsed.handoffReason === 'string' ? parsed.handoffReason : null,
|
||||
aiMessageFingerprints: Array.isArray(parsed.aiMessageFingerprints)
|
||||
? parsed.aiMessageFingerprints.filter((item): item is string => typeof item === 'string')
|
||||
: [],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { WebContext } from '../context.js';
|
||||
import {
|
||||
buildConversationState,
|
||||
chooseConversationIntent,
|
||||
conversationMessageFingerprint,
|
||||
mergeConversationState,
|
||||
remainingAutomationTurns,
|
||||
} from '../../services/conversation-state.js';
|
||||
@@ -169,12 +170,14 @@ export function createConversationsRouter(ctx: WebContext): Router {
|
||||
imageUrl: string | null;
|
||||
sentAt: number;
|
||||
} | null = null;
|
||||
let automatedOpener: string | null = null;
|
||||
try {
|
||||
const messagesRepo = resolved.runtime?.resources.messagesRepo ?? ctx.repos.messages;
|
||||
const sent = conversation.roomId
|
||||
? messagesRepo.getByChatRoomId(conversation.roomId)
|
||||
?? messagesRepo.getLatestByRecipient(conversation.username)
|
||||
: messagesRepo.getLatestByRecipient(conversation.username);
|
||||
automatedOpener = sent?.message ?? null;
|
||||
if (sent && sent.postId) {
|
||||
const url = sent.postUrl ?? (sent.subreddit
|
||||
? `https://www.reddit.com/r/${sent.subreddit}/comments/${sent.postId}/`
|
||||
@@ -205,9 +208,25 @@ export function createConversationsRouter(ctx: WebContext): Router {
|
||||
intelligence.state.automation.handoffReason = intelligence.state.boundaries.join('; ');
|
||||
}
|
||||
if (conversation.roomId) stateRepo?.save(conversation.roomId, intelligence.state);
|
||||
const aiFingerprints = new Map<string, number>();
|
||||
if (automatedOpener) {
|
||||
aiFingerprints.set(conversationMessageFingerprint(automatedOpener), 1);
|
||||
}
|
||||
for (const fingerprint of intelligence.state.automation.aiMessageFingerprints) {
|
||||
aiFingerprints.set(fingerprint, (aiFingerprints.get(fingerprint) ?? 0) + 1);
|
||||
}
|
||||
const annotatedMessages = conversation.messages?.map((message) => {
|
||||
if (!message.isSelf) return message;
|
||||
const fingerprint = conversationMessageFingerprint(message.text);
|
||||
const remainingMatches = aiFingerprints.get(fingerprint) ?? 0;
|
||||
if (remainingMatches === 0) return message;
|
||||
aiFingerprints.set(fingerprint, remainingMatches - 1);
|
||||
return { ...message, isAi: true };
|
||||
});
|
||||
|
||||
res.json({
|
||||
...conversation,
|
||||
...(annotatedMessages ? { messages: annotatedMessages } : {}),
|
||||
owner: resolved.runtime
|
||||
? { accountId: resolved.runtime.session.accountId, redditUsername: resolved.runtime.session.redditUsername }
|
||||
: currentOwner(),
|
||||
|
||||
Reference in new issue
Block a user