feat: LiveKit AI answering agent (Gemini 2.5 Flash native audio)

- Hospital receptionist agent "Helix" with Gemini realtime speech-to-speech
- Tools wired to platform: lookupDoctor, bookAppointment, collectLeadInfo, transferToAgent
- Loads hospital context (doctors, departments) from platform GraphQL on startup
- Connects to LiveKit Cloud, joins rooms when participants connect
- Silero VAD for voice activity detection
- @livekit/agents + @livekit/agents-plugin-google + @livekit/agents-plugin-silero

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-25 20:30:41 +05:30
parent fcc7c90e84
commit 3c06a01e7b
5 changed files with 3498 additions and 65 deletions

View File

@@ -116,55 +116,54 @@ export class AuthController {
this.logger.log(`User ${body.email} logged in with role: ${appRole} (platform roles: ${roleLabels.join(', ')})`);
// Multi-agent: resolve agent config + session lock for CC agents
// Check if user has an Agent entity with SIP config — applies to ALL roles
let agentConfigResponse: any = undefined;
const memberId = workspaceMember?.id;
if (appRole === 'cc-agent') {
const memberId = workspaceMember?.id;
if (!memberId) throw new HttpException('Workspace member not found', 400);
if (memberId) {
const agentConfig = await this.agentConfigService.getByMemberId(memberId);
if (!agentConfig) {
if (agentConfig) {
// Agent entity found — set up SIP + Ozonetel
const clientIp = (req.headers['x-forwarded-for'] as string)?.split(',')[0]?.trim() ?? req.ip ?? 'unknown';
const existingSession = await this.sessionService.getSession(agentConfig.ozonetelAgentId);
if (existingSession) {
this.logger.warn(`Duplicate login blocked for ${body.email} — session held by IP ${existingSession.ip} since ${existingSession.lockedAt}`);
throw new HttpException(`You are already logged in from another device (${existingSession.ip}). Please log out there first.`, 409);
}
await this.sessionService.lockSession(agentConfig.ozonetelAgentId, memberId, clientIp);
this.ozonetelAgent.refreshToken().catch(err => {
this.logger.warn(`Ozonetel token refresh on login failed: ${err.message}`);
});
const ozAgentPassword = process.env.OZONETEL_AGENT_PASSWORD ?? 'Test123$';
this.ozonetelAgent.loginAgent({
agentId: agentConfig.ozonetelAgentId,
password: ozAgentPassword,
phoneNumber: agentConfig.sipExtension,
mode: 'blended',
}).catch(err => {
this.logger.warn(`Ozonetel agent login failed (non-blocking): ${err.message}`);
});
agentConfigResponse = {
ozonetelAgentId: agentConfig.ozonetelAgentId,
sipExtension: agentConfig.sipExtension,
sipPassword: agentConfig.sipPassword,
sipUri: agentConfig.sipUri,
sipWsServer: agentConfig.sipWsServer,
campaignName: agentConfig.campaignName,
};
this.logger.log(`Agent ${body.email} → Ozonetel ${agentConfig.ozonetelAgentId} / SIP ${agentConfig.sipExtension}`);
} else if (appRole === 'cc-agent') {
// CC agent role but no Agent entity — block login
throw new HttpException('Agent account not configured. Contact administrator.', 403);
} else {
this.logger.log(`User ${body.email} has no Agent entity — SIP disabled`);
}
// Check for duplicate login — strict: one device only
const clientIp = (req.headers['x-forwarded-for'] as string)?.split(',')[0]?.trim() ?? req.ip ?? 'unknown';
const existingSession = await this.sessionService.getSession(agentConfig.ozonetelAgentId);
if (existingSession) {
this.logger.warn(`Duplicate login blocked for ${body.email} — session held by IP ${existingSession.ip} since ${existingSession.lockedAt}`);
throw new HttpException(`You are already logged in from another device (${existingSession.ip}). Please log out there first.`, 409);
}
// Lock session in Redis with IP
await this.sessionService.lockSession(agentConfig.ozonetelAgentId, memberId, clientIp);
// Force-refresh Ozonetel API token on login
this.ozonetelAgent.refreshToken().catch(err => {
this.logger.warn(`Ozonetel token refresh on login failed: ${err.message}`);
});
// Login to Ozonetel with agent-specific credentials
const ozAgentPassword = process.env.OZONETEL_AGENT_PASSWORD ?? 'Test123$';
this.ozonetelAgent.loginAgent({
agentId: agentConfig.ozonetelAgentId,
password: ozAgentPassword,
phoneNumber: agentConfig.sipExtension,
mode: 'blended',
}).catch(err => {
this.logger.warn(`Ozonetel agent login failed (non-blocking): ${err.message}`);
});
agentConfigResponse = {
ozonetelAgentId: agentConfig.ozonetelAgentId,
sipExtension: agentConfig.sipExtension,
sipPassword: agentConfig.sipPassword,
sipUri: agentConfig.sipUri,
sipWsServer: agentConfig.sipWsServer,
campaignName: agentConfig.campaignName,
};
this.logger.log(`CC agent ${body.email} → Ozonetel ${agentConfig.ozonetelAgentId} / SIP ${agentConfig.sipExtension}`);
}
return {