whatsapp-bot/src/payload/TemplateResolver.ts
Martin Porwoll 8847358507 feat: initial WhatsApp Business Bot scaffold
Phase 1 implementation with all core modules:
- Fastify webhook server with Meta signature validation
- WhatsApp Cloud API client (send text/template/interactive, mark as read)
- LLM abstraction layer with Claude provider (Haiku for speed)
- BullMQ message processing pipeline (dedup, rate limiting)
- Bot routing (MessageRouter, ConversationManager, EscalationManager)
- Payload CMS integration (InteractionWriter via direct DB, RulesLoader, TemplateResolver)
- Healthcare-safe system prompt with medical keyword detection
- PM2 ecosystem config

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 10:58:51 +00:00

89 lines
2.3 KiB
TypeScript

import { getLogger } from '../lib/logger.js'
import type { PayloadClient } from './PayloadClient.js'
import { getConfig } from '../config.js'
import { DEFAULT_HEALTHCARE_PROMPT } from '../llm/HealthcarePrompt.js'
const log = getLogger('template-resolver')
interface CommunityTemplate {
id: number
name: string
category: string
content: string
tenant?: number | { id: number }
}
const TEMPLATE_CACHE_TTL = 600_000 // 10 minutes
export class TemplateResolver {
private systemPromptCache: string | null = null
private lastFetch = 0
constructor(private payloadClient: PayloadClient) {}
async getSystemPrompt(): Promise<string> {
const now = Date.now()
if (now - this.lastFetch < TEMPLATE_CACHE_TTL && this.systemPromptCache) {
return this.systemPromptCache
}
try {
const tenantId = getConfig().CCS_TENANT_ID
const result = await this.payloadClient.find<CommunityTemplate>(
'community-templates',
{
'where[category][equals]': 'whatsapp_system_prompt',
'where[tenant][equals]': tenantId,
limit: '1',
},
)
if (result.docs.length > 0) {
this.systemPromptCache = result.docs[0].content
this.lastFetch = now
log.info('System prompt loaded from CMS')
return this.systemPromptCache
}
} catch (err) {
log.error(
{ error: (err as Error).message },
'Failed to load system prompt from CMS',
)
}
// Fallback to hardcoded prompt
log.info('Using default healthcare prompt')
return DEFAULT_HEALTHCARE_PROMPT
}
async resolveTemplate(
category: string,
name?: string,
): Promise<string | null> {
try {
const tenantId = getConfig().CCS_TENANT_ID
const query: Record<string, unknown> = {
'where[category][equals]': category,
'where[tenant][equals]': tenantId,
limit: '1',
}
if (name) {
query['where[name][equals]'] = name
}
const result =
await this.payloadClient.find<CommunityTemplate>(
'community-templates',
query,
)
return result.docs[0]?.content ?? null
} catch (err) {
log.error(
{ category, name, error: (err as Error).message },
'Failed to resolve template',
)
return null
}
}
}