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

@@ -469,6 +469,9 @@ function createPurchaseRepository(): PurchaseMessageIngestionRepository {
async hasClarificationContext(record) {
return clarificationKeys.has(key(record))
},
async saveWithInterpretation() {
throw new Error('not implemented')
},
async save(record) {
const threadKey = key(record)
@@ -1414,25 +1417,12 @@ Confirm or cancel below.`,
})
})
test('reuses the purchase-topic route instead of calling the shared router twice', async () => {
test('uses topic processor for classification and assistant for response', async () => {
const bot = createTestBot()
const calls: Array<{ method: string; payload: unknown }> = []
let assistantCalls = 0
let routerCalls = 0
let processorCalls = 0
const householdConfigurationRepository = createBoundHouseholdRepository('purchase')
const topicRouter = async () => {
routerCalls += 1
return {
route: 'topic_helper' as const,
replyText: null,
helperKind: 'assistant' as const,
shouldStartTyping: true,
shouldClearWorkflow: false,
confidence: 96,
reason: 'question'
}
}
bot.api.config.use(async (_prev, method, payload) => {
calls.push({ method, payload })
@@ -1463,7 +1453,10 @@ Confirm or cancel below.`,
householdConfigurationRepository,
createPurchaseRepository(),
{
router: topicRouter
topicProcessor: async () => {
processorCalls += 1
return { route: 'topic_helper', reason: 'test' }
}
}
)
@@ -1482,7 +1475,6 @@ Confirm or cancel below.`,
}
}
},
topicRouter,
purchaseRepository: createPurchaseRepository(),
purchaseInterpreter: async () => null,
householdConfigurationRepository,
@@ -1500,7 +1492,7 @@ Confirm or cancel below.`,
await bot.handleUpdate(topicMentionUpdate('@household_test_bot how is life?') as never)
expect(routerCalls).toBe(1)
expect(processorCalls).toBe(1)
expect(assistantCalls).toBe(1)
expect(calls).toEqual(
expect.arrayContaining([