fix(bot): localize topic processor responses and allow optional payment amounts

This commit is contained in:
2026-03-15 02:27:44 +04:00
parent 3c53ab9e1a
commit afbda43c0e
3 changed files with 36 additions and 14 deletions

View File

@@ -769,12 +769,21 @@ export function registerConfiguredPaymentTopicIngestion(
} }
// Create payment proposal using the parsed data from topic processor // Create payment proposal using the parsed data from topic processor
const amountMajor = Money.fromMinor( const amountMajor =
BigInt(processorResult.amountMinor), processorResult.amountMinor && processorResult.currency
processorResult.currency ? Money.fromMinor(
).toMajorString() BigInt(processorResult.amountMinor),
processorResult.currency
).toMajorString()
: null
const synthesizedText =
amountMajor && processorResult.currency
? `paid ${processorResult.kind} ${amountMajor} ${processorResult.currency}`
: `paid ${processorResult.kind}`
const proposal = await maybeCreatePaymentProposal({ const proposal = await maybeCreatePaymentProposal({
rawText: `paid ${processorResult.kind} ${amountMajor} ${processorResult.currency}`, rawText: synthesizedText,
householdId: record.householdId, householdId: record.householdId,
memberId: member.id, memberId: member.id,
financeService, financeService,

View File

@@ -15,7 +15,7 @@ import type {
} from '@household/ports' } from '@household/ports'
import { createDbClient, schema } from '@household/db' import { createDbClient, schema } from '@household/db'
import { getBotTranslations, type BotLocale } from './i18n' import { getBotTranslations, botLocaleFromContext, type BotLocale } from './i18n'
import type { AssistantConversationMemoryStore } from './assistant-state' import type { AssistantConversationMemoryStore } from './assistant-state'
import { buildConversationContext } from './conversation-orchestrator' import { buildConversationContext } from './conversation-orchestrator'
import type { import type {
@@ -2298,9 +2298,10 @@ export function registerPurchaseTopicIngestion(
rememberUserTurn(options.memoryStore, record) rememberUserTurn(options.memoryStore, record)
typingIndicator = typingIndicator =
options.interpreter && route.shouldStartTyping ? startTypingIndicator(ctx) : null options.interpreter && route.shouldStartTyping ? startTypingIndicator(ctx) : null
const locale = botLocaleFromContext(ctx)
const pendingReply = const pendingReply =
options.interpreter && shouldShowProcessingReply(ctx, record, route) options.interpreter && shouldShowProcessingReply(ctx, record, route)
? await sendPurchaseProcessingReply(ctx, getBotTranslations('en').purchase.processing) ? await sendPurchaseProcessingReply(ctx, getBotTranslations(locale).purchase.processing)
: null : null
const result = await repository.save(record, options.interpreter, 'GEL') const result = await repository.save(record, options.interpreter, 'GEL')

View File

@@ -1,6 +1,7 @@
import { extractOpenAiResponseText, parseJsonFromResponseText } from './openai-responses' import { extractOpenAiResponseText, parseJsonFromResponseText } from './openai-responses'
import type { TopicWorkflowState } from './topic-message-router' import type { TopicWorkflowState } from './topic-message-router'
import type { EngagementAssessment } from './conversation-orchestrator' import type { EngagementAssessment } from './conversation-orchestrator'
import { getBotTranslations } from './i18n'
export type TopicProcessorRoute = export type TopicProcessorRoute =
| 'silent' | 'silent'
@@ -27,8 +28,8 @@ export interface TopicProcessorPurchaseResult {
export interface TopicProcessorPaymentResult { export interface TopicProcessorPaymentResult {
route: 'payment' route: 'payment'
kind: 'rent' | 'utilities' kind: 'rent' | 'utilities'
amountMinor: string amountMinor: string | null
currency: 'GEL' | 'USD' currency: 'GEL' | 'USD' | null
confidence: number confidence: number
reason: string reason: string
} }
@@ -327,6 +328,10 @@ For bare summons ("бот?", "bot", "@kojori_bot"), use topic_helper to let the
For small talk or jokes directed at the bot, use chat_reply with a short playful response. For small talk or jokes directed at the bot, use chat_reply with a short playful response.
For questions that need household knowledge, use topic_helper. For questions that need household knowledge, use topic_helper.
=== LANGUAGE ===
- Always use the user's locale (locale=${input.locale}) for clarificationQuestion and replyText.
- If locale=ru, respond in Russian. If locale=en, respond in English.
=== WORKFLOWS === === WORKFLOWS ===
If there is an active clarification workflow and the user's message answers it, combine with context. If there is an active clarification workflow and the user's message answers it, combine with context.
If user dismisses ("не, забей", "cancel"), use dismiss_workflow.` If user dismisses ("не, забей", "cancel"), use dismiss_workflow.`
@@ -487,9 +492,10 @@ If user dismisses ("не, забей", "cancel"), use dismiss_workflow.`
}, },
'Topic processor missing purchase fields' 'Topic processor missing purchase fields'
) )
const t = getBotTranslations(input.locale).purchase
return { return {
route: 'purchase_clarification', route: 'purchase_clarification',
clarificationQuestion: 'Could you clarify the purchase details?', clarificationQuestion: t.clarificationLowConfidence,
reason: 'missing_required_fields' reason: 'missing_required_fields'
} }
} }
@@ -518,11 +524,16 @@ If user dismisses ("не, забей", "cancel"), use dismiss_workflow.`
case 'purchase_clarification': case 'purchase_clarification':
case 'payment_clarification': { case 'payment_clarification': {
const t = getBotTranslations(input.locale)
const defaultQuestion =
route === 'purchase_clarification'
? t.purchase.clarificationLowConfidence
: t.assistant.paymentClarification
const clarificationQuestion = const clarificationQuestion =
typeof parsed.clarificationQuestion === 'string' && typeof parsed.clarificationQuestion === 'string' &&
parsed.clarificationQuestion.trim().length > 0 parsed.clarificationQuestion.trim().length > 0
? parsed.clarificationQuestion.trim() ? parsed.clarificationQuestion.trim()
: 'Could you clarify?' : defaultQuestion
return { route, clarificationQuestion, reason } return { route, clarificationQuestion, reason }
} }
@@ -531,7 +542,7 @@ If user dismisses ("не, забей", "cancel"), use dismiss_workflow.`
const currency = normalizeCurrency(parsed.currency ?? null) const currency = normalizeCurrency(parsed.currency ?? null)
const kind = parsed.kind === 'rent' || parsed.kind === 'utilities' ? parsed.kind : null const kind = parsed.kind === 'rent' || parsed.kind === 'utilities' ? parsed.kind : null
if (!amountMinor || !currency || !kind) { if (!kind) {
logger?.warn( logger?.warn(
{ {
event: 'topic_processor.missing_payment_fields', event: 'topic_processor.missing_payment_fields',
@@ -541,9 +552,10 @@ If user dismisses ("не, забей", "cancel"), use dismiss_workflow.`
}, },
'Topic processor missing payment fields' 'Topic processor missing payment fields'
) )
const t = getBotTranslations(input.locale).assistant
return { return {
route: 'payment_clarification', route: 'payment_clarification',
clarificationQuestion: 'Could you clarify the payment details?', clarificationQuestion: t.paymentClarification,
reason: 'missing_required_fields' reason: 'missing_required_fields'
} }
} }
@@ -551,7 +563,7 @@ If user dismisses ("не, забей", "cancel"), use dismiss_workflow.`
return { return {
route, route,
kind, kind,
amountMinor: amountMinor.toString(), amountMinor: amountMinor?.toString() ?? null,
currency, currency,
confidence, confidence,
reason reason