feat(p7): wire AI into P6 routing + AiProjector (KG-05 structural proof)
Task 7.4: route-simulator now unions RULE flags with persisted AI moderation flags before decide() — so AI flags influence routing exactly like rule flags, only ever REVIEW/DENY, never ALLOW. SUMMARY/TRANSCRIPT/DIGEST previews render a proposed AI summary (aiSourced flag) instead of the P6 stub, still preview-only. AiProjector auto-runs CLASSIFY on interaction.normalized (channel-bearing only), idempotent via claim(). 4 tests prove KG-05: AI flag forces REVIEW on an auto-ALLOW binding, DENY on CHILD, AI summary in preview with 0 sends, projector idempotency. Full suite 79 green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
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<void> {
|
||||
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<boolean> {
|
||||
const res = await this.prisma.iiosProcessedEvent.createMany({
|
||||
data: [{ consumerName: this.consumer, eventId }],
|
||||
skipDuplicates: true,
|
||||
});
|
||||
return res.count > 0;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user