feat(bot): unified topic processor replacing router+interpreter stack

Replace 3-layer architecture (gpt-5-nano router + gpt-4o-mini interpreter) with
single unified topic processor (gpt-4o-mini) for simplified message handling.

New components:
- HouseholdContextCache: TTL-based caching (5 min) for household config data
- TopicProcessor: Unified classification + parsing with structured JSON output

Key changes:
- Renamed ASSISTANT_ROUTER_MODEL → TOPIC_PROCESSOR_MODEL
- Added TOPIC_PROCESSOR_TIMEOUT_MS (default 10s)
- Refactored save() → saveWithInterpretation() for pre-parsed interpretations
- Removed deprecated createOpenAiTopicMessageRouter and ~300 lines legacy code
- Fixed typing indicator to only start when needed (purchase routes)
- Fixed amount formatting: convert minor units to major for rawText

Routes: silent, chat_reply, purchase, purchase_clarification, payment,
payment_clarification, topic_helper, dismiss_workflow

All 212 bot tests pass. Typecheck, lint, format, build clean.
This commit is contained in:
2026-03-14 13:33:57 +04:00
parent 9c3bb100e3
commit f38ee499ae
14 changed files with 1554 additions and 854 deletions

View File

@@ -0,0 +1,48 @@
import type { BotLocale } from './i18n'
export interface CachedHouseholdContext {
householdContext: string | null
assistantTone: string | null
defaultCurrency: 'GEL' | 'USD'
locale: BotLocale
cachedAt: number
}
interface CacheEntry {
context: CachedHouseholdContext
expiresAt: number
}
export class HouseholdContextCache {
private cache = new Map<string, CacheEntry>()
constructor(private ttlMs: number = 5 * 60_000) {}
async get(
householdId: string,
loader: () => Promise<CachedHouseholdContext>
): Promise<CachedHouseholdContext> {
const now = Date.now()
const entry = this.cache.get(householdId)
if (entry && entry.expiresAt > now) {
return entry.context
}
const context = await loader()
this.cache.set(householdId, {
context,
expiresAt: now + this.ttlMs
})
return context
}
invalidate(householdId: string): void {
this.cache.delete(householdId)
}
clear(): void {
this.cache.clear()
}
}