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 { HealthModule } from './health/health.module';
|
||||||
import { WorklistModule } from './worklist/worklist.module';
|
import { WorklistModule } from './worklist/worklist.module';
|
||||||
import { CallAssistModule } from './call-assist/call-assist.module';
|
import { CallAssistModule } from './call-assist/call-assist.module';
|
||||||
|
import { SearchModule } from './search/search.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -28,6 +29,7 @@ import { CallAssistModule } from './call-assist/call-assist.module';
|
|||||||
HealthModule,
|
HealthModule,
|
||||||
WorklistModule,
|
WorklistModule,
|
||||||
CallAssistModule,
|
CallAssistModule,
|
||||||
|
SearchModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
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')
|
@Post('agent-ready')
|
||||||
async agentReady() {
|
async agentReady() {
|
||||||
this.logger.log(`Force ready: logging out and back in agent ${this.defaultAgentId}`);
|
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