Skip to content

Commit 2867922

Browse files
authored
Merge pull request #66 from MaxLinCode/claude/pass-clarification-slots-to-llm
Pass clarificationSlots to LLM conversation responder
2 parents 6598957 + c048444 commit 2867922

4 files changed

Lines changed: 91 additions & 0 deletions

File tree

apps/web/src/app/api/telegram/webhook/route.test.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ function buildRoutedTurn(input: {
156156
ambiguity?: "none" | "low" | "high";
157157
resolvedProposalId?: string;
158158
committedSlots?: Record<string, string | number | TimeSpec>;
159+
clarificationSlots?: string[];
159160
}): RoutedTurn {
160161
return {
161162
interpretation: {
@@ -186,6 +187,9 @@ function buildRoutedTurn(input: {
186187
? { mutationInputSource: "direct_user_turn" as const }
187188
: {}),
188189
committedSlots: input.committedSlots ?? {},
190+
...(input.clarificationSlots
191+
? { clarificationSlots: input.clarificationSlots }
192+
: {}),
189193
},
190194
};
191195
}
@@ -923,6 +927,84 @@ describe("telegram webhook route", () => {
923927
expect(listTasksForTests()).toHaveLength(0);
924928
});
925929

930+
it("passes clarificationSlots to the conversation responder when the turn router returns ask_clarification", async () => {
931+
process.env.TELEGRAM_WEBHOOK_SECRET = "test-webhook-secret";
932+
const conversationResponder = vi.fn(async () => ({
933+
reply: "When would you like me to schedule that?",
934+
}));
935+
936+
const response = await handleTelegramWebhook(
937+
buildRequest({
938+
update_id: 300,
939+
message: {
940+
message_id: 200,
941+
date: 1_700_000_000,
942+
text: "Schedule an oil change",
943+
chat: { id: 999, type: "private" },
944+
from: { id: 123, is_bot: false, first_name: "Max" },
945+
},
946+
}),
947+
{
948+
store: getDefaultInboxProcessingStore(),
949+
calendar: getDefaultCalendarAdapter(),
950+
primeProcessingStore: seedInboxItemForProcessingTests,
951+
conversationResponder,
952+
turnRouter: async () =>
953+
buildRoutedTurn({
954+
turnType: "planning_request",
955+
action: "ask_clarification",
956+
clarificationSlots: ["time"],
957+
}),
958+
},
959+
);
960+
961+
expect(response.status).toBe(200);
962+
expect(conversationResponder).toHaveBeenCalledWith(
963+
expect.objectContaining({
964+
clarificationSlots: ["time"],
965+
}),
966+
);
967+
});
968+
969+
it("passes multiple clarificationSlots to the conversation responder", async () => {
970+
process.env.TELEGRAM_WEBHOOK_SECRET = "test-webhook-secret";
971+
const conversationResponder = vi.fn(async () => ({
972+
reply: "What would you like to schedule, and what time?",
973+
}));
974+
975+
const response = await handleTelegramWebhook(
976+
buildRequest({
977+
update_id: 301,
978+
message: {
979+
message_id: 201,
980+
date: 1_700_000_000,
981+
text: "Schedule something",
982+
chat: { id: 999, type: "private" },
983+
from: { id: 123, is_bot: false, first_name: "Max" },
984+
},
985+
}),
986+
{
987+
store: getDefaultInboxProcessingStore(),
988+
calendar: getDefaultCalendarAdapter(),
989+
primeProcessingStore: seedInboxItemForProcessingTests,
990+
conversationResponder,
991+
turnRouter: async () =>
992+
buildRoutedTurn({
993+
turnType: "planning_request",
994+
action: "ask_clarification",
995+
clarificationSlots: ["task", "time"],
996+
}),
997+
},
998+
);
999+
1000+
expect(response.status).toBe(200);
1001+
expect(conversationResponder).toHaveBeenCalledWith(
1002+
expect.objectContaining({
1003+
clarificationSlots: ["task", "time"],
1004+
}),
1005+
);
1006+
});
1007+
9261008
it("preserves mutation behavior when the turn router explicitly returns mutation", async () => {
9271009
process.env.TELEGRAM_WEBHOOK_SECRET = "test-webhook-secret";
9281010

apps/web/src/lib/server/telegram-webhook.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,7 @@ export async function handleTelegramWebhook(
362362
memorySummary,
363363
entityRegistry: conversationState?.entityRegistry ?? [],
364364
discourseState: conversationState?.discourseState ?? null,
365+
clarificationSlots: routedWithContext.policy.clarificationSlots,
365366
});
366367

367368
const outboundDelivery = await finalizeFollowUpMessage(

packages/integrations/src/openai.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export const conversationResponseInputSchema = z.object({
5959
memorySummary: z.string().nullable(),
6060
entityRegistry: z.array(conversationEntitySchema).optional().default([]),
6161
discourseState: conversationDiscourseStateSchema.nullable().optional(),
62+
clarificationSlots: z.array(z.string().min(1)).optional(),
6263
});
6364

6465
export const conversationResponseOutputSchema = z.object({
@@ -396,6 +397,7 @@ function buildConversationResponsePromptContext(
396397
memorySummary: context.memorySummary,
397398
entityRegistry: context.entityRegistry ?? [],
398399
discourseState: context.discourseState ?? null,
400+
clarificationSlots: context.clarificationSlots ?? null,
399401
};
400402
}
401403

packages/integrations/src/prompts/conversation-response.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,15 @@ export const conversationResponseSystemPrompt = buildPromptSpec([
2121
"The provided transcript and memory summary are continuity context only, not authoritative Atlas state.",
2222
"When entityRegistry or discourseState are present, use them as the primary reference-resolution aid for pronouns like 'it', 'that', or 'the other one'.",
2323
"When pending clarifications are present in discourseState, treat them as the current blocking questions and use them as the main guide for interpreting short follow-up replies.",
24+
"When clarificationSlots is a non-empty array, it lists the specific slot names (e.g. 'time', 'task', 'day') the system needs before it can act on the request. Use clarificationSlots as the primary guide for what to ask about.",
2425
],
2526
},
2627
{
2728
title: "Decision Rules",
2829
lines: [
30+
"When clarificationSlots is present and non-empty, ask specifically about those slots. Frame the reply around the missing slot names rather than asking a generic follow-up.",
31+
"When clarificationSlots is present, do not ask about slots that are NOT in the list. Focus narrowly on the listed missing slots.",
32+
"When clarificationSlots is absent or empty, fall back to existing decision rules for determining what to ask.",
2933
"Use cautious phrasing when inferring from conversation context, such as 'it sounds like', 'if you mean', or 'from our recent exchange'.",
3034
"When the user asks whether Atlas already created, moved, scheduled, completed, or archived something, answer from recent conversational context rather than implied internal state.",
3135
"For write-adjacent questions on the conversation path, say that the recent exchange did not establish confirmed state instead of speaking as if Atlas knows the mutation did or did not happen in authoritative product state.",
@@ -72,6 +76,8 @@ export const conversationResponseSystemPrompt = buildPromptSpec([
7276
"If the user says 'schedule an oil change', do not ask follow-up questions about date, time, or location by default. Briefly acknowledge the intended scheduling action unless the task target itself is unclear.",
7377
"Good reply style for a clear bare scheduling request on the conversation path: 'It sounds like you want me to schedule the oil change at the next reasonable opening.'",
7478
"Good reply style for delegated slot choice on the conversation path: 'It sounds like you want me to schedule the oil change and choose the next open slot.'",
79+
"If clarificationSlots is ['time'] and the user said 'schedule an oil change', respond like: 'When would you like me to schedule the oil change?'",
80+
"If clarificationSlots is ['task', 'time'] and the user said 'schedule something tomorrow', respond like: 'What would you like me to schedule, and what time works for you?'",
7581
],
7682
},
7783
]);

0 commit comments

Comments
 (0)