mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage-server
synced 2026-04-11 18:08:16 +00:00
feat: agent state endpoint + search module
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@ import { GraphqlProxyModule } from './graphql-proxy/graphql-proxy.module';
|
||||
import { HealthModule } from './health/health.module';
|
||||
import { WorklistModule } from './worklist/worklist.module';
|
||||
import { CallAssistModule } from './call-assist/call-assist.module';
|
||||
import { SearchModule } from './search/search.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -28,6 +29,7 @@ import { CallAssistModule } from './call-assist/call-assist.module';
|
||||
HealthModule,
|
||||
WorklistModule,
|
||||
CallAssistModule,
|
||||
SearchModule,
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
||||
@@ -53,6 +53,29 @@ export class OzonetelAgentController {
|
||||
}
|
||||
}
|
||||
|
||||
@Post('agent-state')
|
||||
async agentState(
|
||||
@Body() body: { state: 'Ready' | 'Pause'; pauseReason?: string },
|
||||
) {
|
||||
if (!body.state) {
|
||||
throw new HttpException('state required', 400);
|
||||
}
|
||||
|
||||
this.logger.log(`Agent state change: ${this.defaultAgentId} → ${body.state} (${body.pauseReason ?? ''})`);
|
||||
|
||||
try {
|
||||
const result = await this.ozonetelAgent.changeAgentState({
|
||||
agentId: this.defaultAgentId,
|
||||
state: body.state,
|
||||
pauseReason: body.pauseReason,
|
||||
});
|
||||
return result;
|
||||
} catch (error: any) {
|
||||
const message = error.response?.data?.message ?? error.message ?? 'State change failed';
|
||||
return { status: 'error', message };
|
||||
}
|
||||
}
|
||||
|
||||
@Post('agent-ready')
|
||||
async agentReady() {
|
||||
this.logger.log(`Force ready: logging out and back in agent ${this.defaultAgentId}`);
|
||||
|
||||
94
src/search/search.controller.ts
Normal file
94
src/search/search.controller.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { Controller, Get, Query, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { PlatformGraphqlService } from '../platform/platform-graphql.service';
|
||||
|
||||
@Controller('api/search')
|
||||
export class SearchController {
|
||||
private readonly logger = new Logger(SearchController.name);
|
||||
private readonly platformApiKey: string;
|
||||
|
||||
constructor(
|
||||
private readonly platform: PlatformGraphqlService,
|
||||
private readonly config: ConfigService,
|
||||
) {
|
||||
this.platformApiKey = config.get<string>('platform.apiKey') ?? '';
|
||||
}
|
||||
|
||||
@Get()
|
||||
async search(@Query('q') query?: string) {
|
||||
if (!query || query.length < 2) {
|
||||
return { leads: [], patients: [], appointments: [] };
|
||||
}
|
||||
|
||||
const authHeader = this.platformApiKey ? `Bearer ${this.platformApiKey}` : '';
|
||||
if (!authHeader) {
|
||||
return { leads: [], patients: [], appointments: [] };
|
||||
}
|
||||
|
||||
this.logger.log(`Search: "${query}"`);
|
||||
|
||||
// Fetch all three in parallel, filter client-side for flexible matching
|
||||
try {
|
||||
const [leadsResult, patientsResult, appointmentsResult] = await Promise.all([
|
||||
this.platform.queryWithAuth<any>(
|
||||
`{ leads(first: 50) { edges { node {
|
||||
id name contactName { firstName lastName }
|
||||
contactPhone { primaryPhoneNumber }
|
||||
source status interestedService
|
||||
} } } }`,
|
||||
undefined, authHeader,
|
||||
).catch(() => ({ leads: { edges: [] } })),
|
||||
|
||||
this.platform.queryWithAuth<any>(
|
||||
`{ patients(first: 50) { edges { node {
|
||||
id name fullName { firstName lastName }
|
||||
phones { primaryPhoneNumber }
|
||||
gender dateOfBirth
|
||||
} } } }`,
|
||||
undefined, authHeader,
|
||||
).catch(() => ({ patients: { edges: [] } })),
|
||||
|
||||
this.platform.queryWithAuth<any>(
|
||||
`{ appointments(first: 50, orderBy: [{ scheduledAt: DescNullsLast }]) { edges { node {
|
||||
id scheduledAt doctorName department appointmentStatus patientId
|
||||
} } } }`,
|
||||
undefined, authHeader,
|
||||
).catch(() => ({ appointments: { edges: [] } })),
|
||||
]);
|
||||
|
||||
const q = query.toLowerCase();
|
||||
|
||||
const leads = (leadsResult.leads?.edges ?? [])
|
||||
.map((e: any) => e.node)
|
||||
.filter((l: any) => {
|
||||
const name = `${l.contactName?.firstName ?? ''} ${l.contactName?.lastName ?? ''}`.toLowerCase();
|
||||
const phone = l.contactPhone?.primaryPhoneNumber ?? '';
|
||||
return name.includes(q) || phone.includes(q) || (l.name ?? '').toLowerCase().includes(q);
|
||||
})
|
||||
.slice(0, 5);
|
||||
|
||||
const patients = (patientsResult.patients?.edges ?? [])
|
||||
.map((e: any) => e.node)
|
||||
.filter((p: any) => {
|
||||
const name = `${p.fullName?.firstName ?? ''} ${p.fullName?.lastName ?? ''}`.toLowerCase();
|
||||
const phone = p.phones?.primaryPhoneNumber ?? '';
|
||||
return name.includes(q) || phone.includes(q) || (p.name ?? '').toLowerCase().includes(q);
|
||||
})
|
||||
.slice(0, 5);
|
||||
|
||||
const appointments = (appointmentsResult.appointments?.edges ?? [])
|
||||
.map((e: any) => e.node)
|
||||
.filter((a: any) => {
|
||||
const doctor = (a.doctorName ?? '').toLowerCase();
|
||||
const dept = (a.department ?? '').toLowerCase();
|
||||
return doctor.includes(q) || dept.includes(q);
|
||||
})
|
||||
.slice(0, 5);
|
||||
|
||||
return { leads, patients, appointments };
|
||||
} catch (err: any) {
|
||||
this.logger.error(`Search failed: ${err.message}`);
|
||||
return { leads: [], patients: [], appointments: [] };
|
||||
}
|
||||
}
|
||||
}
|
||||
9
src/search/search.module.ts
Normal file
9
src/search/search.module.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { SearchController } from './search.controller';
|
||||
import { PlatformModule } from '../platform/platform.module';
|
||||
|
||||
@Module({
|
||||
imports: [PlatformModule],
|
||||
controllers: [SearchController],
|
||||
})
|
||||
export class SearchModule {}
|
||||
Reference in New Issue
Block a user