feat: add agent API and operational analytics

This commit is contained in:
jjbrunton committed 2026-08-08 22:08:14 +01:00
1 parent 720ea0652a
commit e8b730b592
65 files changed
+2650 -1214

No files matched your search

+30 -16
View File
@@ -1,10 +1,12 @@
import type { Logger } from '../../utils/logger.js';
import type {
ConversationOutcomesRepository,
MessagesRepository,
RedditUsersRepository,
SentMessageRecord,
SuccessfulMessagesRepository,
} from '../../database/repositories.js';
import { recordConversationReply } from '../conversation-outcome-recorder.js';
import type { MediaService } from '../media.js';
import { ChatCache } from './cache.js';
import {
@@ -151,6 +153,7 @@ export class ChatService {
private messagesRepo?: MessagesRepository,
private redditUsersRepo?: RedditUsersRepository,
private accountAuth?: AccountAuthGate,
private outcomesRepo?: ConversationOutcomesRepository,
) {
this.cache = cache ?? new ChatCache(logger);
if (this.redditUsersRepo) {
@@ -1007,6 +1010,7 @@ export class ChatService {
(message) => !message.isSelf && (message.media?.length ?? 0) > 0,
);
let performanceMessage: SentMessageRecord | undefined;
let firstAttributedReply = false;
if (firstSelf && chat.roomId && this.messagesRepo) {
try {
@@ -1017,7 +1021,7 @@ export class ChatService {
if (existing && hasPerformanceContext) {
performanceMessage = existing;
if (hasReply && !existing.gotResponse) {
this.messagesRepo.markGotResponse(existing.id);
firstAttributedReply = true;
}
if (hasReceivedImage && !existing.gotImage) {
this.messagesRepo.markGotImage(existing.id);
@@ -1028,19 +1032,26 @@ export class ChatService {
}
}
if (firstSelf && hasReply && performanceMessage && this.successfulMessagesRepo) {
if (!this.successfulMessagesRepo.existsByMessage(firstSelf.text)) {
const firstReply = chat.messages.find((m) => !m.isSelf);
try {
this.successfulMessagesRepo.insert({
sentMessageId: performanceMessage.id,
originalMessage: firstSelf.text,
imageAnalysis: performanceMessage.postImageAnalysis ?? null,
postTitle: performanceMessage.postTitle ?? null,
responseSnippet: firstReply?.text.slice(0, 200) || null,
subreddit: performanceMessage.subreddit ?? null,
});
this.logger.info(`[chat] stored successful chat example from ${chat.username}`);
if (firstSelf && hasReply && performanceMessage && this.successfulMessagesRepo && this.outcomesRepo && this.messagesRepo) {
const firstReply = chat.messages.find((m) => !m.isSelf);
const selfCount = chat.messages.filter((m) => m.isSelf).length;
const otherCount = chat.messages.length - selfCount;
try {
recordConversationReply(
performanceMessage,
firstReply?.text.slice(0, 200) ?? '',
{
messageCount: chat.messages.length,
reciprocal: selfCount >= 2 && otherCount >= 2,
},
{
messages: this.messagesRepo,
successful: this.successfulMessagesRepo,
outcomes: this.outcomesRepo,
logger: this.logger,
},
);
if (firstAttributedReply) {
try {
this.newReplyListener?.({
conversationId: chat.roomId,
@@ -1051,10 +1062,13 @@ export class ChatService {
} catch (err) {
this.logger.warn('[chat] newReplyListener threw:', err);
}
} catch (err) {
this.logger.warn('[chat] failed to store successful chat example:', err);
}
} catch (err) {
this.logger.warn('[chat] failed to record conversation outcome:', err);
}
} else if (hasReply && performanceMessage && this.messagesRepo && !performanceMessage.gotResponse) {
this.messagesRepo.markGotResponse(performanceMessage.id);
this.logger.warn('[chat] response recorded without canonical outcome repository; excluded from learning');
}
}