import { Injectable, OnModuleInit } from '@nestjs/common'; import { CloudEvent, IIOS_EVENTS } from '@insignia/iios-contracts'; import { PrismaService } from '../prisma/prisma.service'; import { OutboxBus } from '../outbox/outbox.bus'; import { AiJobService } from './ai.service'; /** * Auto-proposal (P7). Reacts to `interaction.normalized` — channel-bearing messages * that arrived via an adapter — and runs a CLASSIFY job so inbound content gets an AI * moderation proposal automatically. Those flags only ever push routing toward * REVIEW/DENY (KG-05); nothing is sent or approved. Idempotent via `claim()` and the * job's own idempotency key. */ @Injectable() export class AiProjector implements OnModuleInit { private readonly consumer = 'ai-projector'; constructor( private readonly prisma: PrismaService, private readonly bus: OutboxBus, private readonly ai: AiJobService, ) {} onModuleInit(): void { this.bus.on(IIOS_EVENTS.interactionNormalized, (p) => void this.onNormalized(p as CloudEvent).catch(() => {})); } async onNormalized(event: CloudEvent): Promise { if (!(await this.claim(event.id))) return; const { interactionId } = event.data as { interactionId: string }; const interaction = await this.prisma.iiosInteraction.findUnique({ where: { id: interactionId }, include: { channel: true }, }); if (!interaction?.channel) return; // native DMs have no origin channel → not auto-classified await this.ai.runJob({ interactionId, jobType: 'CLASSIFY' }); } private async claim(eventId: string): Promise { const res = await this.prisma.iiosProcessedEvent.createMany({ data: [{ consumerName: this.consumer, eventId }], skipDuplicates: true, }); return res.count > 0; } }