Skip to content

Commit 71b5ef5

Browse files
authored
Merge pull request #53 from MaxLinCode/codex/turn-routing-refactor
Refactor turn routing: 3-layer pipeline (classify → extract → commit)
2 parents 95483d9 + d74a0c2 commit 71b5ef5

24 files changed

Lines changed: 2716 additions & 775 deletions

CLAUDE.md

Lines changed: 643 additions & 0 deletions
Large diffs are not rendered by default.

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

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const {
2828
sendTelegramMessageMock,
2929
sendTelegramChatActionMock,
3030
routeTurnWithResponsesMock,
31+
classifyTurnWithResponsesMock,
3132
summarizeConversationMemoryWithResponsesMock,
3233
respondToConversationTurnWithResponsesMock,
3334
recoverConfirmedMutationWithResponsesMock
@@ -36,6 +37,7 @@ const {
3637
sendTelegramMessageMock: vi.fn(),
3738
sendTelegramChatActionMock: vi.fn(),
3839
routeTurnWithResponsesMock: vi.fn(),
40+
classifyTurnWithResponsesMock: vi.fn(),
3941
summarizeConversationMemoryWithResponsesMock: vi.fn(),
4042
respondToConversationTurnWithResponsesMock: vi.fn(),
4143
recoverConfirmedMutationWithResponsesMock: vi.fn()
@@ -80,6 +82,7 @@ vi.mock("@atlas/integrations", async () => {
8082
editTelegramMessage: editTelegramMessageMock,
8183
respondToConversationTurnWithResponses: respondToConversationTurnWithResponsesMock,
8284
recoverConfirmedMutationWithResponses: recoverConfirmedMutationWithResponsesMock,
85+
classifyTurnWithResponses: classifyTurnWithResponsesMock,
8386
routeTurnWithResponses: routeTurnWithResponsesMock,
8487
sendTelegramChatAction: sendTelegramChatActionMock,
8588
sendTelegramMessage: sendTelegramMessageMock,
@@ -147,7 +150,8 @@ function buildRoutedTurn(input: {
147150
? { mutationInputSource: "recovered_proposal" as const }
148151
: input.action === "execute_mutation"
149152
? { mutationInputSource: "direct_user_turn" as const }
150-
: {})
153+
: {}),
154+
committedSlots: {}
151155
}
152156
};
153157
}
@@ -275,13 +279,19 @@ beforeEach(async () => {
275279
sendTelegramMessageMock.mockReset();
276280
sendTelegramChatActionMock.mockReset();
277281
routeTurnWithResponsesMock.mockReset();
282+
classifyTurnWithResponsesMock.mockReset();
278283
summarizeConversationMemoryWithResponsesMock.mockReset();
279284
respondToConversationTurnWithResponsesMock.mockReset();
280285
recoverConfirmedMutationWithResponsesMock.mockReset();
281286
routeTurnWithResponsesMock.mockResolvedValue({
282287
route: "mutation",
283288
reason: "Direct scheduling request."
284289
});
290+
classifyTurnWithResponsesMock.mockResolvedValue({
291+
turnType: "planning_request",
292+
confidence: 0.92,
293+
reasoning: "User wants to schedule or plan something."
294+
});
285295
summarizeConversationMemoryWithResponsesMock.mockResolvedValue({
286296
summary: "Recent conversation summary."
287297
});
@@ -522,6 +532,11 @@ describe("telegram webhook route", () => {
522532
const conversationResponder = vi.fn(async () => ({
523533
reply: "What time tomorrow should I schedule the dentist?"
524534
}));
535+
classifyTurnWithResponsesMock.mockResolvedValue({
536+
turnType: "planning_request",
537+
confidence: 0.55,
538+
reasoning: "Mixed-intent message with ambiguous scheduling request."
539+
});
525540

526541
const response = await handleTelegramWebhook(
527542
buildRequest({

apps/web/src/lib/server/conversation-state.test.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ describe("deriveConversationReplyState", () => {
3333
const result = deriveConversationReplyState({
3434
snapshot: buildSnapshot(),
3535
policy: {
36-
action: "ask_clarification"
36+
action: "ask_clarification",
37+
committedSlots: {}
3738
},
3839
interpretation: {
3940
turnType: "planning_request",
@@ -71,7 +72,8 @@ describe("deriveConversationReplyState", () => {
7172
snapshot: buildSnapshot(),
7273
policy: {
7374
action: "ask_clarification",
74-
clarificationSlots: ["proposal"]
75+
clarificationSlots: ["proposal"],
76+
committedSlots: {}
7577
},
7678
interpretation: {
7779
turnType: "confirmation",
@@ -94,7 +96,8 @@ describe("deriveConversationReplyState", () => {
9496
const result = deriveConversationReplyState({
9597
snapshot: buildSnapshot(),
9698
policy: {
97-
action: "ask_clarification"
99+
action: "ask_clarification",
100+
committedSlots: {}
98101
},
99102
interpretation: {
100103
turnType: "unknown",
@@ -154,7 +157,8 @@ describe("deriveConversationReplyState", () => {
154157
const result = deriveConversationReplyState({
155158
snapshot,
156159
policy: {
157-
action: "present_proposal"
160+
action: "present_proposal",
161+
committedSlots: {}
158162
},
159163
interpretation: {
160164
turnType: "clarification_answer",

apps/web/src/lib/server/conversation-state.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { type ProcessedInboxResult } from "@atlas/db";
1919

2020
type DeriveConversationReplyStateInput = {
2121
snapshot: ConversationStateSnapshot;
22-
policy: Pick<TurnPolicyDecision, "action" | "clarificationSlots" | "targetProposalId"> & {
22+
policy: Pick<TurnPolicyDecision, "action" | "clarificationSlots" | "targetProposalId" | "committedSlots"> & {
2323
action: Extract<TurnPolicyAction, "reply_only" | "ask_clarification" | "present_proposal">;
2424
};
2525
interpretation: TurnInterpretation;
@@ -134,7 +134,7 @@ export function deriveConversationReplyState(input: DeriveConversationReplyState
134134
});
135135
nextFocusEntityId ??= clarificationEntity.id;
136136
}
137-
const nextDiscourseState = updateDiscourseStateFromAssistantTurn(discourseState, {
137+
const updatedDiscourseState = updateDiscourseStateFromAssistantTurn(discourseState, {
138138
...(presentedItems.length > 0 ? { presentedItems } : {}),
139139
...(newClarifications.length > 0 ? { newClarifications } : {}),
140140
...(resolvedClarificationIds.length > 0 ? { resolvedClarificationIds } : {}),
@@ -143,6 +143,12 @@ export function deriveConversationReplyState(input: DeriveConversationReplyState
143143
validEntityIds: entityRegistry.map((entity) => entity.id)
144144
}).state;
145145

146+
const committedSlots = input.policy.committedSlots;
147+
const nextDiscourseState =
148+
committedSlots && Object.keys(committedSlots).length > 0
149+
? { ...updatedDiscourseState, resolved_slots: committedSlots }
150+
: updatedDiscourseState;
151+
146152
return {
147153
summaryText: input.summaryText,
148154
mode: getConversationModeForPolicy(input.policy.action),

0 commit comments

Comments
 (0)