Files
helix-engage-server/src/supervisor/supervisor.service.ts
saridsa2 695f119c2b feat: team module, multi-stage Dockerfile, doctor utils, AI config overhaul
- Team module: POST /api/team/members (in-place employee creation with
  temp password + Redis cache), PUT /api/team/members/:id, GET temp
  password endpoint. Uses signUpInWorkspace — no email invites.
- Dockerfile: rewritten as multi-stage build (builder + runtime) so
  native modules compile for target arch. Fixes darwin→linux crash.
- .dockerignore: exclude dist, node_modules, .env, .git, data/
- package-lock.json: regenerated against public npmjs.org (was
  pointing at localhost:4873 Verdaccio — broke docker builds)
- Doctor utils: shared DOCTOR_VISIT_SLOTS_FRAGMENT + normalizeDoctors
  helper for visit-slot-aware queries across 6 consumers
- AI config: full admin CRUD (GET/PUT/POST reset), workspace-scoped
  setup-state with workspace ID isolation, AI prompt defaults overhaul
- Agent config: camelCase field fix for SDK-synced workspaces
- Session service: workspace-scoped Redis key prefixing for setup state
- Recordings/supervisor/widget services: updated to use doctor-utils
  shared fragments instead of inline visitingHours queries

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 08:37:58 +05:30

140 lines
5.7 KiB
TypeScript

import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Subject } from 'rxjs';
import { PlatformGraphqlService } from '../platform/platform-graphql.service';
import { OzonetelAgentService } from '../ozonetel/ozonetel-agent.service';
type ActiveCall = {
ucid: string;
agentId: string;
callerNumber: string;
callType: string;
startTime: string;
status: 'active' | 'on-hold';
};
export type AgentOzonetelState = 'ready' | 'break' | 'training' | 'calling' | 'in-call' | 'acw' | 'offline';
type AgentStateEntry = {
state: AgentOzonetelState;
timestamp: string;
};
@Injectable()
export class SupervisorService implements OnModuleInit {
private readonly logger = new Logger(SupervisorService.name);
private readonly activeCalls = new Map<string, ActiveCall>();
private readonly agentStates = new Map<string, AgentStateEntry>();
readonly agentStateSubject = new Subject<{ agentId: string; state: AgentOzonetelState; timestamp: string }>();
constructor(
private platform: PlatformGraphqlService,
private ozonetel: OzonetelAgentService,
private config: ConfigService,
) {}
async onModuleInit() {
this.logger.log('Supervisor service initialized');
}
handleCallEvent(event: any) {
const action = event.action;
const ucid = event.ucid ?? event.monitorUCID;
const agentId = event.agent_id ?? event.agentID;
const callerNumber = event.caller_id ?? event.callerID;
const callType = event.call_type ?? event.Type;
const eventTime = event.event_time ?? event.eventTime ?? new Date().toISOString();
if (!ucid) return;
if (action === 'Answered' || action === 'Calling') {
this.activeCalls.set(ucid, {
ucid, agentId, callerNumber,
callType, startTime: eventTime, status: 'active',
});
this.logger.log(`Active call: ${agentId}${callerNumber} (${ucid})`);
} else if (action === 'Disconnect') {
this.activeCalls.delete(ucid);
this.logger.log(`Call ended: ${ucid}`);
}
}
handleAgentEvent(event: any) {
const agentId = event.agentId ?? event.agent_id ?? 'unknown';
const action = event.action ?? 'unknown';
const eventData = event.eventData ?? '';
const eventTime = event.event_time ?? event.eventTime ?? new Date().toISOString();
this.logger.log(`[AGENT-STATE] ${agentId}${action}${eventData ? ` (${eventData})` : ''} at ${eventTime}`);
const mapped = this.mapOzonetelAction(action, eventData);
if (mapped) {
this.agentStates.set(agentId, { state: mapped, timestamp: eventTime });
this.agentStateSubject.next({ agentId, state: mapped, timestamp: eventTime });
this.logger.log(`[AGENT-STATE] Emitted: ${agentId}${mapped}`);
}
}
private mapOzonetelAction(action: string, eventData: string): AgentOzonetelState | null {
switch (action) {
case 'release': return 'ready';
case 'IDLE': return 'ready'; // agent available after unanswered/canceled call
case 'calling': return 'calling';
case 'incall': return 'in-call';
case 'ACW': return 'acw';
case 'logout': return 'offline';
case 'AUX':
// "changeMode" is the brief AUX during login — not a real pause
if (eventData === 'changeMode') return null;
if (eventData?.toLowerCase().includes('training')) return 'training';
return 'break';
case 'login': return null; // wait for release
default: return null;
}
}
getAgentState(agentId: string): AgentStateEntry | null {
return this.agentStates.get(agentId) ?? null;
}
emitForceLogout(agentId: string) {
this.logger.log(`[AGENT-STATE] Emitting force-logout for ${agentId}`);
this.agentStates.set(agentId, { state: 'offline', timestamp: new Date().toISOString() });
// Use a special state so frontend can distinguish admin force-logout from normal Ozonetel logout
this.agentStateSubject.next({ agentId, state: 'force-logout' as any, timestamp: new Date().toISOString() });
}
getActiveCalls(): ActiveCall[] {
return Array.from(this.activeCalls.values());
}
async getTeamPerformance(date: string): Promise<any> {
// Get all agents from platform. Field names are label-derived
// camelCase on the current platform schema — see
// agent-config.service.ts for the canonical explanation of the
// legacy lowercase names that used to exist on staging.
const agentData = await this.platform.query<any>(
`{ agents(first: 20) { edges { node {
id name ozonetelAgentId npsScore
maxIdleMinutes minNpsThreshold minConversion
} } } }`,
);
const agents = agentData?.agents?.edges?.map((e: any) => e.node) ?? [];
// Fetch Ozonetel time summary per agent
const summaries = await Promise.all(
agents.map(async (agent: any) => {
if (!agent.ozonetelAgentId) return { ...agent, timeBreakdown: null };
try {
const summary = await this.ozonetel.getAgentSummary(agent.ozonetelAgentId, date);
return { ...agent, timeBreakdown: summary };
} catch (err) {
this.logger.warn(`Failed to get summary for ${agent.ozonetelAgentId}: ${err}`);
return { ...agent, timeBreakdown: null };
}
}),
);
return { date, agents: summaries };
}
}