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

@@ -512,6 +512,9 @@ describe('registerPurchaseTopicIngestion', () => {
async confirm() {
throw new Error('not used')
},
async saveWithInterpretation() {
throw new Error('not implemented')
},
async cancel() {
throw new Error('not used')
},
@@ -605,6 +608,9 @@ Confirm or cancel below.`,
async confirm() {
throw new Error('not used')
},
async saveWithInterpretation() {
throw new Error('not implemented')
},
async cancel() {
throw new Error('not used')
},
@@ -670,6 +676,9 @@ Confirm or cancel below.`,
async confirm() {
throw new Error('not used')
},
async saveWithInterpretation() {
throw new Error('not implemented')
},
async cancel() {
throw new Error('not used')
},
@@ -757,6 +766,9 @@ Confirm or cancel below.`,
async confirm() {
throw new Error('not used')
},
async saveWithInterpretation() {
throw new Error('not implemented')
},
async cancel() {
throw new Error('not used')
},
@@ -866,6 +878,9 @@ Confirm or cancel below.`,
async confirm() {
throw new Error('not used')
},
async saveWithInterpretation() {
throw new Error('not implemented')
},
async cancel() {
throw new Error('not used')
},
@@ -942,6 +957,9 @@ Confirm or cancel below.`,
async confirm() {
throw new Error('not used')
},
async saveWithInterpretation() {
throw new Error('not implemented')
},
async cancel() {
throw new Error('not used')
},
@@ -1037,6 +1055,9 @@ Confirm or cancel below.`
async confirm() {
throw new Error('not used')
},
async saveWithInterpretation() {
throw new Error('not implemented')
},
async cancel() {
throw new Error('not used')
},
@@ -1125,6 +1146,9 @@ Confirm or cancel below.`
async confirm() {
throw new Error('not used')
},
async saveWithInterpretation() {
throw new Error('not implemented')
},
async cancel() {
throw new Error('not used')
},
@@ -1182,6 +1206,9 @@ Confirm or cancel below.`
async confirm() {
throw new Error('not used')
},
async saveWithInterpretation() {
throw new Error('not implemented')
},
async cancel() {
throw new Error('not used')
},
@@ -1224,6 +1251,9 @@ Confirm or cancel below.`
async confirm() {
throw new Error('not used')
},
async saveWithInterpretation() {
throw new Error('not implemented')
},
async cancel() {
throw new Error('not used')
},
@@ -1271,6 +1301,9 @@ Confirm or cancel below.`
async confirm() {
throw new Error('not used')
},
async saveWithInterpretation() {
throw new Error('not implemented')
},
async cancel() {
throw new Error('not used')
},
@@ -1325,6 +1358,9 @@ Confirm or cancel below.`
async confirm() {
throw new Error('not used')
},
async saveWithInterpretation() {
throw new Error('not implemented')
},
async cancel() {
throw new Error('not used')
},
@@ -1391,6 +1427,9 @@ Confirm or cancel below.`
async confirm() {
throw new Error('not used')
},
async saveWithInterpretation() {
throw new Error('not implemented')
},
async cancel() {
throw new Error('not used')
},
@@ -1457,6 +1496,9 @@ Confirm or cancel below.`
async confirm() {
throw new Error('not used')
},
async saveWithInterpretation() {
throw new Error('not implemented')
},
async cancel() {
throw new Error('not used')
},
@@ -1516,6 +1558,9 @@ Confirm or cancel below.`
async confirm() {
throw new Error('not used')
},
async saveWithInterpretation() {
throw new Error('not implemented')
},
async cancel() {
throw new Error('not used')
},
@@ -1581,6 +1626,9 @@ Confirm or cancel below.`
async confirm() {
throw new Error('not used')
},
async saveWithInterpretation() {
throw new Error('not implemented')
},
async cancel() {
throw new Error('not used')
},
@@ -1655,6 +1703,9 @@ Confirm or cancel below.`
async confirm() {
throw new Error('not used')
},
async saveWithInterpretation() {
throw new Error('not implemented')
},
async cancel() {
throw new Error('not used')
},
@@ -1712,6 +1763,9 @@ Confirm or cancel below.`
async confirm() {
throw new Error('not used')
},
async saveWithInterpretation() {
throw new Error('not implemented')
},
async cancel() {
throw new Error('not used')
},
@@ -1814,6 +1868,9 @@ Confirm or cancel below.`,
async confirm() {
throw new Error('not used')
},
async saveWithInterpretation() {
throw new Error('not implemented')
},
async cancel() {
throw new Error('not used')
},
@@ -1865,6 +1922,9 @@ Confirm or cancel below.`,
async confirm() {
throw new Error('not used')
},
async saveWithInterpretation() {
throw new Error('not implemented')
},
async cancel() {
throw new Error('not used')
},
@@ -1942,6 +2002,9 @@ Confirm or cancel below.`,
async confirm() {
throw new Error('not used')
},
async saveWithInterpretation() {
throw new Error('not implemented')
},
async cancel() {
throw new Error('not used')
},
@@ -1992,6 +2055,9 @@ Confirm or cancel below.`,
async confirm() {
throw new Error('not used')
},
async saveWithInterpretation() {
throw new Error('not implemented')
},
async cancel() {
throw new Error('not used')
},
@@ -2051,17 +2117,9 @@ Confirm or cancel below.`,
repository,
{
historyRepository,
router: async (input) => {
topicProcessor: async (input) => {
if (input.messageText.includes('картошки')) {
return {
route: 'silent',
replyText: null,
helperKind: null,
shouldStartTyping: false,
shouldClearWorkflow: false,
confidence: 90,
reason: 'planning'
}
return { route: 'silent', reason: 'planning' }
}
recentTurnTexts = input.recentThreadMessages?.map((turn) => turn.text) ?? []
@@ -2069,10 +2127,6 @@ Confirm or cancel below.`,
return {
route: 'chat_reply',
replyText: 'No leaked context here.',
helperKind: 'assistant',
shouldStartTyping: false,
shouldClearWorkflow: false,
confidence: 91,
reason: 'thread_scoped'
}
}
@@ -2136,6 +2190,9 @@ Confirm or cancel below.`,
participants: participants()
}
},
async saveWithInterpretation() {
throw new Error('not implemented')
},
async cancel() {
throw new Error('not used')
},
@@ -2195,6 +2252,9 @@ Participants:
async confirm() {
throw new Error('not used')
},
async saveWithInterpretation() {
throw new Error('not implemented')
},
async cancel() {
throw new Error('not used')
},
@@ -2258,6 +2318,9 @@ Participants:
participants: participants()
}
},
async saveWithInterpretation() {
throw new Error('not implemented')
},
async cancel() {
throw new Error('not used')
},
@@ -2311,6 +2374,9 @@ Participants:
async confirm() {
throw new Error('not used')
},
async saveWithInterpretation() {
throw new Error('not implemented')
},
async cancel() {
return {
status: 'cancelled' as const,