feat: event bus with Redpanda + AI insight consumer

- EventBusService: Kafka/Redpanda pub/sub with kafkajs, graceful fallback when broker unavailable
- Topics: call.completed, call.missed, agent.state
- AiInsightConsumer: on call.completed, fetches lead activity → OpenAI generates summary + suggested action → updates Lead entity on platform
- Disposition endpoint emits call.completed event after Ozonetel dispose
- EventsModule registered as @Global for cross-module injection
- Redpanda container added to VPS docker-compose
- Docker image rebuilt for linux/amd64 with kafkajs dependency

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-26 09:38:02 +05:30
parent 3c06a01e7b
commit 3e2e7372cc
8 changed files with 311 additions and 0 deletions

View File

@@ -3,6 +3,8 @@ import { ConfigService } from '@nestjs/config';
import { OzonetelAgentService } from './ozonetel-agent.service';
import { MissedQueueService } from '../worklist/missed-queue.service';
import { PlatformGraphqlService } from '../platform/platform-graphql.service';
import { EventBusService } from '../events/event-bus.service';
import { Topics } from '../events/event-types';
@Controller('api/ozonetel')
export class OzonetelAgentController {
@@ -17,6 +19,7 @@ export class OzonetelAgentController {
private readonly config: ConfigService,
private readonly missedQueue: MissedQueueService,
private readonly platform: PlatformGraphqlService,
private readonly eventBus: EventBusService,
) {
this.defaultAgentId = config.get<string>('OZONETEL_AGENT_ID') ?? 'agent3';
this.defaultAgentPassword = config.get<string>('OZONETEL_AGENT_PASSWORD') ?? '';
@@ -161,6 +164,20 @@ export class OzonetelAgentController {
this.logger.warn(`Auto-assignment after dispose failed: ${err}`);
}
// Emit event for downstream processing (AI insights, metrics, etc.)
this.eventBus.emit(Topics.CALL_COMPLETED, {
callId: null,
ucid: body.ucid,
agentId: this.defaultAgentId,
callerPhone: body.callerPhone ?? '',
direction: body.direction ?? 'INBOUND',
durationSec: body.durationSec ?? 0,
disposition: body.disposition,
leadId: body.leadId ?? null,
notes: body.notes ?? null,
timestamp: new Date().toISOString(),
}).catch(() => {});
return { status: 'ok' };
}