mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage-server
synced 2026-04-12 02:18:18 +00:00
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:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user