mirror of
https://github.com/whekin/household-bot.git
synced 2026-03-31 21:04:03 +00:00
fix(bot): support text cancellation of reminder drafts
This commit is contained in:
@@ -258,6 +258,24 @@ describe('registerAdHocNotifications', () => {
|
|||||||
},
|
},
|
||||||
async interpretDraftEdit() {
|
async interpretDraftEdit() {
|
||||||
draftEditCalls += 1
|
draftEditCalls += 1
|
||||||
|
if (draftEditCalls === 2) {
|
||||||
|
return {
|
||||||
|
decision: 'cancel',
|
||||||
|
notificationText: null,
|
||||||
|
assigneeChanged: false,
|
||||||
|
assigneeMemberId: null,
|
||||||
|
resolvedLocalDate: null,
|
||||||
|
resolvedHour: null,
|
||||||
|
resolvedMinute: null,
|
||||||
|
resolutionMode: null,
|
||||||
|
deliveryMode: null,
|
||||||
|
dmRecipientMemberIds: null,
|
||||||
|
clarificationQuestion: null,
|
||||||
|
confidence: 95,
|
||||||
|
parserMode: 'llm'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
decision: 'updated',
|
decision: 'updated',
|
||||||
notificationText: null,
|
notificationText: null,
|
||||||
@@ -352,8 +370,7 @@ describe('registerAdHocNotifications', () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const pending = await promptRepository.getPendingAction('-10012345', '10002')
|
const pending = await promptRepository.getPendingAction('-10012345', '10002')
|
||||||
const proposalId = (pending?.payload as { proposalId?: string } | null)?.proposalId
|
expect((pending?.payload as { proposalId?: string } | null)?.proposalId).toBeTruthy()
|
||||||
expect(proposalId).toBeTruthy()
|
|
||||||
|
|
||||||
await bot.handleUpdate(reminderMessageUpdate('Давай на 10 часов лучше') as never)
|
await bot.handleUpdate(reminderMessageUpdate('Давай на 10 часов лучше') as never)
|
||||||
|
|
||||||
@@ -362,12 +379,34 @@ describe('registerAdHocNotifications', () => {
|
|||||||
text: `Окей, ${updatedWhen} напомню.`
|
text: `Окей, ${updatedWhen} напомню.`
|
||||||
})
|
})
|
||||||
|
|
||||||
await bot.handleUpdate(reminderCallbackUpdate(`adhocnotif:confirm:${proposalId}`) as never)
|
await bot.handleUpdate(reminderMessageUpdate('А вообще, я не буду кушать') as never)
|
||||||
|
|
||||||
expect(calls[2]?.method).toBe('answerCallbackQuery')
|
expect(draftEditCalls).toBe(2)
|
||||||
expect(calls[3]?.method).toBe('editMessageText')
|
expect(calls[2]?.payload).toMatchObject({
|
||||||
expect(calls[3]?.payload).toMatchObject({
|
text: 'Окей, тогда не напоминаю.'
|
||||||
text: `Окей, ${updatedWhen} напомню.`
|
})
|
||||||
|
expect(await promptRepository.getPendingAction('-10012345', '10002')).toBeNull()
|
||||||
|
|
||||||
|
const replacementPending = await promptRepository.getPendingAction('-10012345', '10002')
|
||||||
|
expect(replacementPending).toBeNull()
|
||||||
|
|
||||||
|
await bot.handleUpdate(
|
||||||
|
reminderMessageUpdate('Железяка, напомни пошпынять Георгия завтра с утра') as never
|
||||||
|
)
|
||||||
|
|
||||||
|
const renewedPending = await promptRepository.getPendingAction('-10012345', '10002')
|
||||||
|
const renewedProposalId = (renewedPending?.payload as { proposalId?: string } | null)
|
||||||
|
?.proposalId
|
||||||
|
expect(renewedProposalId).toBeTruthy()
|
||||||
|
|
||||||
|
await bot.handleUpdate(
|
||||||
|
reminderCallbackUpdate(`adhocnotif:confirm:${renewedProposalId}`) as never
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(calls[4]?.method).toBe('answerCallbackQuery')
|
||||||
|
expect(calls[5]?.method).toBe('editMessageText')
|
||||||
|
expect(calls[5]?.payload).toMatchObject({
|
||||||
|
text: `Окей, ${initialWhen} напомню.`
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(scheduledRequests).toEqual([
|
expect(scheduledRequests).toEqual([
|
||||||
|
|||||||
@@ -77,6 +77,10 @@ function unavailableReply(locale: BotLocale): string {
|
|||||||
: 'I cannot create reminders right now because the AI module is temporarily unavailable.'
|
: 'I cannot create reminders right now because the AI module is temporarily unavailable.'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cancelledDraftReply(locale: BotLocale): string {
|
||||||
|
return locale === 'ru' ? 'Окей, тогда не напоминаю.' : 'Okay, I will drop this reminder.'
|
||||||
|
}
|
||||||
|
|
||||||
function localNowText(timezone: string, now = nowInstant()): string {
|
function localNowText(timezone: string, now = nowInstant()): string {
|
||||||
const local = now.toZonedDateTimeISO(timezone)
|
const local = now.toZonedDateTimeISO(timezone)
|
||||||
return [
|
return [
|
||||||
@@ -833,6 +837,15 @@ export function registerAdHocNotifications(options: {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (interpretedEdit.decision === 'cancel') {
|
||||||
|
await options.promptRepository.clearPendingAction(
|
||||||
|
ctx.chat!.id.toString(),
|
||||||
|
ctx.from!.id.toString()
|
||||||
|
)
|
||||||
|
await replyInTopic(ctx, cancelledDraftReply(reminderContext.locale))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const scheduleChanged =
|
const scheduleChanged =
|
||||||
interpretedEdit.resolvedLocalDate !== null ||
|
interpretedEdit.resolvedLocalDate !== null ||
|
||||||
interpretedEdit.resolvedHour !== null ||
|
interpretedEdit.resolvedHour !== null ||
|
||||||
|
|||||||
@@ -265,6 +265,55 @@ describe('createOpenAiAdHocNotificationInterpreter', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('interprets draft edit cancellation requests', async () => {
|
||||||
|
const interpreter = createOpenAiAdHocNotificationInterpreter({
|
||||||
|
apiKey: 'test-key',
|
||||||
|
parserModel: 'gpt-5-mini',
|
||||||
|
rendererModel: 'gpt-5-mini',
|
||||||
|
timeoutMs: 5000
|
||||||
|
})
|
||||||
|
expect(interpreter).toBeDefined()
|
||||||
|
|
||||||
|
const originalFetch = globalThis.fetch
|
||||||
|
globalThis.fetch = (async () =>
|
||||||
|
nestedJsonResponse({
|
||||||
|
decision: 'cancel',
|
||||||
|
notificationText: null,
|
||||||
|
assigneeChanged: false,
|
||||||
|
assigneeMemberId: null,
|
||||||
|
resolvedLocalDate: null,
|
||||||
|
resolvedHour: null,
|
||||||
|
resolvedMinute: null,
|
||||||
|
resolutionMode: null,
|
||||||
|
deliveryMode: null,
|
||||||
|
dmRecipientMemberIds: null,
|
||||||
|
confidence: 95,
|
||||||
|
clarificationQuestion: null
|
||||||
|
})) as unknown as typeof fetch
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await interpreter!.interpretDraftEdit({
|
||||||
|
locale: 'ru',
|
||||||
|
timezone: 'Asia/Tbilisi',
|
||||||
|
localNow: '2026-03-23 23:30',
|
||||||
|
text: 'А вообще, я не буду кушать',
|
||||||
|
members: [{ memberId: 'dima', displayName: 'Дима', status: 'active' }],
|
||||||
|
senderMemberId: 'dima',
|
||||||
|
currentNotificationText: 'покушать',
|
||||||
|
currentAssigneeMemberId: 'dima',
|
||||||
|
currentScheduledLocalDate: '2026-03-24',
|
||||||
|
currentScheduledHour: 11,
|
||||||
|
currentScheduledMinute: 0,
|
||||||
|
currentDeliveryMode: 'topic',
|
||||||
|
currentDmRecipientMemberIds: []
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result?.decision).toBe('cancel')
|
||||||
|
} finally {
|
||||||
|
globalThis.fetch = originalFetch
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
test('renders the final delivery text that should be persisted', async () => {
|
test('renders the final delivery text that should be persisted', async () => {
|
||||||
const interpreter = createOpenAiAdHocNotificationInterpreter({
|
const interpreter = createOpenAiAdHocNotificationInterpreter({
|
||||||
apiKey: 'test-key',
|
apiKey: 'test-key',
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export interface AdHocNotificationScheduleInterpretation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface AdHocNotificationDraftEditInterpretation {
|
export interface AdHocNotificationDraftEditInterpretation {
|
||||||
decision: 'updated' | 'clarification'
|
decision: 'updated' | 'clarification' | 'cancel'
|
||||||
notificationText: string | null
|
notificationText: string | null
|
||||||
assigneeChanged: boolean
|
assigneeChanged: boolean
|
||||||
assigneeMemberId: string | null
|
assigneeMemberId: string | null
|
||||||
@@ -77,7 +77,7 @@ interface ReminderDeliveryTextResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface ReminderDraftEditResult {
|
interface ReminderDraftEditResult {
|
||||||
decision: 'updated' | 'clarification'
|
decision: 'updated' | 'clarification' | 'cancel'
|
||||||
notificationText: string | null
|
notificationText: string | null
|
||||||
assigneeChanged: boolean
|
assigneeChanged: boolean
|
||||||
assigneeMemberId: string | null
|
assigneeMemberId: string | null
|
||||||
@@ -520,7 +520,7 @@ export function createOpenAiAdHocNotificationInterpreter(input: {
|
|||||||
properties: {
|
properties: {
|
||||||
decision: {
|
decision: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
enum: ['updated', 'clarification']
|
enum: ['updated', 'clarification', 'cancel']
|
||||||
},
|
},
|
||||||
notificationText: {
|
notificationText: {
|
||||||
anyOf: [{ type: 'string' }, { type: 'null' }]
|
anyOf: [{ type: 'string' }, { type: 'null' }]
|
||||||
@@ -596,6 +596,7 @@ export function createOpenAiAdHocNotificationInterpreter(input: {
|
|||||||
prompt: [
|
prompt: [
|
||||||
'You interpret edit messages for an already prepared household reminder draft.',
|
'You interpret edit messages for an already prepared household reminder draft.',
|
||||||
'Treat the latest message as a request to modify the existing draft, not as a brand new reminder.',
|
'Treat the latest message as a request to modify the existing draft, not as a brand new reminder.',
|
||||||
|
'If the user clearly says the reminder is no longer needed or should be dropped, return decision cancel.',
|
||||||
'Only return fields that should change; keep unchanged fields as null, except assigneeChanged must explicitly say whether the assignee should change.',
|
'Only return fields that should change; keep unchanged fields as null, except assigneeChanged must explicitly say whether the assignee should change.',
|
||||||
'Use notificationText only when the user changes what should be reminded.',
|
'Use notificationText only when the user changes what should be reminded.',
|
||||||
'Use deliveryMode when the user changes where the reminder should be sent.',
|
'Use deliveryMode when the user changes where the reminder should be sent.',
|
||||||
@@ -630,7 +631,12 @@ export function createOpenAiAdHocNotificationInterpreter(input: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
decision: parsed.decision === 'updated' ? 'updated' : 'clarification',
|
decision:
|
||||||
|
parsed.decision === 'updated' ||
|
||||||
|
parsed.decision === 'clarification' ||
|
||||||
|
parsed.decision === 'cancel'
|
||||||
|
? parsed.decision
|
||||||
|
: 'clarification',
|
||||||
notificationText: normalizeOptionalText(parsed.notificationText),
|
notificationText: normalizeOptionalText(parsed.notificationText),
|
||||||
assigneeChanged: parsed.assigneeChanged,
|
assigneeChanged: parsed.assigneeChanged,
|
||||||
assigneeMemberId: normalizeMemberId(parsed.assigneeMemberId, options.members),
|
assigneeMemberId: normalizeMemberId(parsed.assigneeMemberId, options.members),
|
||||||
|
|||||||
Reference in New Issue
Block a user