Files
RedditBot/src/services/conversation-learning-types.ts
T

130 lines
3.6 KiB
TypeScript

import { z } from 'zod';
import type { AttributionConfidence, ConversationEventKind } from '../database/conversation-event-repository.js';
import type { ConversationPolicyMode } from './conversation-state.js';
export const LearningStageSchema = z.enum([
'rapport',
'answer',
'flirt',
'disclosure',
'media-invitation',
'handoff',
'unknown',
]);
export type LearningStage = z.infer<typeof LearningStageSchema>;
export const LearningTurnSchema = z.strictObject({
eventId: z.string(),
timestamp: z.number(),
role: z.enum(['recipient', 'operator']),
actor: z.enum(['recipient', 'ai', 'human']),
kind: z.enum([
'inbound-text',
'opener',
'automation-reply',
'manual-text',
'library-reciprocal',
]),
text: z.string(),
});
export type LearningTurn = z.infer<typeof LearningTurnSchema>;
export const LearningCreditClasses = [
'human-created-success',
'human-improved-success',
'human-completed-ai-trajectory',
'autonomous-success',
'recipient-initiated-media',
'uncertain',
] as const;
export type LearningCreditClass = (typeof LearningCreditClasses)[number];
export type LearningTrajectoryStatus = 'provisional' | 'eligible' | 'excluded' | 'invalidated' | 'needs-review';
export type LearningDecisionStatus = 'eligible' | 'excluded' | 'invalidated' | 'needs-review';
export interface LearningTrajectoryRecord {
id: string;
accountId: string;
conversationId: string;
sentMessageId: number | null;
subreddit: string | null;
sourceType: 'image' | 'chat' | null;
policyMode: ConversationPolicyMode | null;
firstHumanEventId: string | null;
outcomeEventId: string;
outcomeType: 'image-received' | 'reciprocal-engagement';
creditClass: LearningCreditClass;
attributionConfidence: AttributionConfidence;
status: LearningTrajectoryStatus;
maturityAt: number;
invalidatedAt: number | null;
invalidationReason: string | null;
reviewedAt: number | null;
createdAt: number;
updatedAt: number;
}
export interface LearningDecisionRecord {
id: string;
trajectoryId: string;
accountId: string;
decisionEventId: string;
stage: LearningStage;
context: LearningTurn[];
chosenResponse: string;
turnsUntilOutcome: number;
isDirectMediaInvitation: boolean;
qualityStatus: LearningDecisionStatus;
createdAt: number;
updatedAt: number;
}
export interface LearningTrajectoryDetail extends LearningTrajectoryRecord {
decisions: LearningDecisionRecord[];
}
export interface RetrievedLearningExample {
decisionId: string;
trajectoryId: string;
creditClass: LearningCreditClass;
stage: LearningStage;
context: LearningTurn[];
chosenResponse: string;
turnsUntilOutcome: number;
subreddit: string | null;
policyMode: ConversationPolicyMode | null;
}
export interface ConversationLearningQuery {
accountId: string;
conversationId: string;
subreddit: string | null;
policyMode: ConversationPolicyMode;
stage: LearningStage;
purpose: 'reply' | 'media-ask';
incomingEventId: string;
}
export interface ConversationLearningProvider {
getExamples(query: ConversationLearningQuery): readonly RetrievedLearningExample[];
recordGeneration(input: {
query: ConversationLearningQuery;
mode: 'shadow' | 'injected';
policyVersion: string;
exampleIds: readonly string[];
promptSha256?: string | null;
generatedAt?: number;
}): void;
}
export interface AttributedConversationMessage {
eventId: string;
timestamp: number;
isSelf: boolean;
text: string;
mediaCount: number;
actor: 'recipient' | 'ai' | 'human' | 'unknown';
kind: ConversationEventKind;
confidence: AttributionConfidence;
}