mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage-server
synced 2026-05-18 20:08:19 +00:00
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>
This commit is contained in:
@@ -158,48 +158,28 @@ export class WidgetChatService {
|
||||
const kb = await this.getKnowledgeBase();
|
||||
|
||||
// Branch context flips the tool-usage rules: no branch = must call
|
||||
// pick_branch first; branch set = always pass it to branch-aware tools.
|
||||
// pick_branch first; branch set = always pass it to branch-aware
|
||||
// tools. We pre-render this block since the structure is dynamic
|
||||
// and the template just slots it in via {{branchContext}}.
|
||||
const branchContext = selectedBranch
|
||||
? [
|
||||
`CURRENT BRANCH: ${selectedBranch}`,
|
||||
`The visitor is interested in the ${selectedBranch} branch. You MUST pass branch="${selectedBranch}"`,
|
||||
'to list_departments, show_clinic_timings, show_doctors, and show_doctor_slots every time.',
|
||||
]
|
||||
].join('\n')
|
||||
: [
|
||||
'BRANCH STATUS: NOT SET',
|
||||
'The visitor has not picked a branch yet. Before calling list_departments, show_clinic_timings,',
|
||||
'show_doctors, or show_doctor_slots, you MUST call pick_branch first so the visitor can choose.',
|
||||
'Only skip this if the user asks a pure general question that does not need branch-specific data.',
|
||||
];
|
||||
].join('\n');
|
||||
|
||||
return [
|
||||
`You are a helpful, concise assistant for ${init.brand.name}.`,
|
||||
`You are chatting with a website visitor named ${userName}.`,
|
||||
'',
|
||||
...branchContext,
|
||||
'',
|
||||
'TOOL USAGE RULES (STRICT):',
|
||||
'- When the user asks about departments, call list_departments and DO NOT also list departments in prose.',
|
||||
'- When they ask about clinic timings, visiting hours, or "when is X open", call show_clinic_timings.',
|
||||
'- When they ask about doctors in a department, call show_doctors and DO NOT also list doctors in prose.',
|
||||
'- When they ask about a specific doctor\'s availability or want to book with them, call show_doctor_slots.',
|
||||
'- When the conversation is trending toward booking, call suggest_booking.',
|
||||
'- After calling a tool, DO NOT restate its contents in prose. At most write a single short sentence',
|
||||
' (under 15 words) framing the widget, or no text at all. The widget already shows the data.',
|
||||
'- If you are about to write a bulleted or numbered list of departments, doctors, hours, or slots,',
|
||||
' STOP and call the appropriate tool instead.',
|
||||
'- NEVER use markdown formatting (no **bold**, no *italic*, no bullet syntax). Plain text only in',
|
||||
' non-tool replies.',
|
||||
'- NEVER invent or mention specific dates in prose or tool inputs. The server owns "today".',
|
||||
' If the visitor asks about a future date, tell them to use the Book tab\'s date picker.',
|
||||
'',
|
||||
'OTHER RULES:',
|
||||
'- Answer other questions (directions, general info) concisely in prose.',
|
||||
'- If you do not know something, say so and suggest they call the hospital.',
|
||||
'- Never quote prices. No medical advice. For clinical questions, defer to a doctor.',
|
||||
'',
|
||||
kb,
|
||||
].join('\n');
|
||||
return this.aiConfig.renderPrompt('widgetChat', {
|
||||
hospitalName: init.brand.name,
|
||||
userName,
|
||||
branchContext,
|
||||
knowledgeBase: kb,
|
||||
});
|
||||
}
|
||||
|
||||
// Streams the assistant reply as an async iterable of UIMessageChunk-shaped
|
||||
@@ -213,13 +193,15 @@ export class WidgetChatService {
|
||||
const platform = this.platform;
|
||||
const widgetSvc = this.widget;
|
||||
|
||||
// Small helper: does a doctor's clinic match the branch filter?
|
||||
// Case-insensitive substring match so "Indiranagar" matches
|
||||
// "Indiranagar Clinic" etc.
|
||||
// Branch-matching now uses the doctor's full `clinics` array
|
||||
// (NormalizedDoctor) since one doctor can visit multiple
|
||||
// clinics under the post-rework data model. doctorMatchesBranch
|
||||
// returns true if ANY of their visit-slot clinics matches.
|
||||
const matchesBranch = (d: any, branch: string | undefined): boolean => {
|
||||
if (!branch) return true;
|
||||
const clinicName = String(d.clinic?.clinicName ?? '').toLowerCase();
|
||||
return clinicName.includes(branch.toLowerCase());
|
||||
const needle = branch.toLowerCase();
|
||||
const clinics: Array<{ clinicName: string }> = d.clinics ?? [];
|
||||
return clinics.some((c) => c.clinicName.toLowerCase().includes(needle));
|
||||
};
|
||||
|
||||
const tools = {
|
||||
@@ -229,23 +211,37 @@ export class WidgetChatService {
|
||||
inputSchema: z.object({}),
|
||||
execute: async () => {
|
||||
const doctors = await widgetSvc.getDoctors();
|
||||
const byBranch = new Map<string, { doctorCount: number; departments: Set<string> }>();
|
||||
// Branches come from the union of all doctors'
|
||||
// visit-slot clinics. Each (clinic × doctor) pair
|
||||
// counts once toward that branch's doctor count;
|
||||
// we use a Set on doctor ids to avoid double-
|
||||
// counting the same doctor against the same branch
|
||||
// when they have multiple slots there.
|
||||
const byBranch = new Map<
|
||||
string,
|
||||
{ doctorIds: Set<string>; departments: Set<string> }
|
||||
>();
|
||||
for (const d of doctors) {
|
||||
const name = d.clinic?.clinicName?.trim();
|
||||
if (!name) continue;
|
||||
if (!byBranch.has(name)) {
|
||||
byBranch.set(name, { doctorCount: 0, departments: new Set() });
|
||||
for (const c of d.clinics ?? []) {
|
||||
const name = c.clinicName?.trim();
|
||||
if (!name) continue;
|
||||
if (!byBranch.has(name)) {
|
||||
byBranch.set(name, {
|
||||
doctorIds: new Set(),
|
||||
departments: new Set(),
|
||||
});
|
||||
}
|
||||
const entry = byBranch.get(name)!;
|
||||
if (d.id) entry.doctorIds.add(d.id);
|
||||
if (d.department) entry.departments.add(String(d.department).replace(/_/g, ' '));
|
||||
}
|
||||
const entry = byBranch.get(name)!;
|
||||
entry.doctorCount += 1;
|
||||
if (d.department) entry.departments.add(String(d.department).replace(/_/g, ' '));
|
||||
}
|
||||
return {
|
||||
branches: Array.from(byBranch.entries())
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.map(([name, { doctorCount, departments }]) => ({
|
||||
.map(([name, { doctorIds, departments }]) => ({
|
||||
name,
|
||||
doctorCount,
|
||||
doctorCount: doctorIds.size,
|
||||
departmentCount: departments.size,
|
||||
})),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user