Skip to content

Commit a482353

Browse files
authored
Merge pull request #75 from MaxLinCode/claude/phase3-pipeline
Phase 3: Move entity resolution out of classifier
2 parents 47a6017 + c7c32a0 commit a482353

8 files changed

Lines changed: 230 additions & 81 deletions

File tree

apps/web/src/lib/server/decide-turn-policy.test.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,19 +24,27 @@ const emptyCommit: CommitPolicyOutput = {
2424
};
2525

2626
function input(
27-
classification: Partial<TurnClassifierOutput>,
27+
classification: Partial<TurnClassifierOutput> & {
28+
resolvedEntityIds?: string[];
29+
resolvedProposalId?: string;
30+
},
2831
commitResult: Partial<CommitPolicyOutput>,
2932
routingContext: DecideTurnPolicyInput["routingContext"],
3033
): DecideTurnPolicyInput {
34+
const { resolvedEntityIds, resolvedProposalId, ...classificationRest } =
35+
classification;
3136
return {
3237
classification: {
3338
turnType: "unknown",
3439
confidence: 0.5,
35-
resolvedEntityIds: [],
36-
...classification,
40+
...classificationRest,
3741
},
3842
commitResult: { ...emptyCommit, ...commitResult },
3943
routingContext,
44+
...(resolvedEntityIds?.[0]
45+
? { targetEntityId: resolvedEntityIds[0] }
46+
: {}),
47+
...(resolvedProposalId ? { resolvedProposalId } : {}),
4048
};
4149
}
4250

apps/web/src/lib/server/decide-turn-policy.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ export type DecideTurnPolicyInput = {
1414
classification: TurnClassifierOutput;
1515
commitResult: CommitPolicyOutput;
1616
routingContext: TurnRoutingInput;
17+
targetEntityId?: string;
18+
resolvedProposalId?: string;
1719
};
1820

1921
type StructuredWriteReadiness =
@@ -36,7 +38,7 @@ export function decideTurnPolicy(
3638
input: DecideTurnPolicyInput,
3739
): TurnPolicyDecision {
3840
const { classification, commitResult } = input;
39-
const targetEntityId = classification.resolvedEntityIds[0];
41+
const targetEntityId = input.targetEntityId;
4042
const ambiguity = deriveAmbiguity({
4143
classifierConfidence: classification.confidence,
4244
missingFields: commitResult.missingFields,
@@ -65,7 +67,7 @@ export function decideTurnPolicy(
6567
};
6668
case "confirmation": {
6769
const proposalId =
68-
classification.resolvedProposalId ??
70+
input.resolvedProposalId ??
6971
resolveSingleActiveProposalId(
7072
input.routingContext.entityRegistry ?? [],
7173
);
@@ -167,7 +169,7 @@ function deriveStructuredWriteReadiness(
167169
const alreadyConfirmed = entityRegistry.some(
168170
(e) =>
169171
e.kind === "proposal_option" &&
170-
e.id === classification.resolvedProposalId &&
172+
e.id === input.resolvedProposalId &&
171173
e.status === "confirmed",
172174
);
173175

@@ -181,9 +183,13 @@ function deriveStructuredWriteReadiness(
181183
}
182184

183185
const consentRequirement = deriveConsentRequirement({
184-
classification,
186+
resolvedEntityIds: input.targetEntityId ? [input.targetEntityId] : [],
187+
...(input.resolvedProposalId
188+
? { resolvedProposalId: input.resolvedProposalId }
189+
: {}),
185190
entityRegistry: input.routingContext.entityRegistry ?? [],
186191
resolvedFields: commitResult.resolvedFields,
192+
turnType: classification.turnType,
187193
});
188194

189195
if (consentRequirement.required) {

apps/web/src/lib/server/llm-classifier.test.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,6 @@ describe("classifyTurn", () => {
4848
expect(result).toMatchObject({
4949
turnType: "confirmation",
5050
confidence: 0.97,
51-
resolvedEntityIds: ["task-1"],
52-
resolvedProposalId: "proposal-1",
5351
});
5452
});
5553

@@ -78,7 +76,7 @@ describe("classifyTurn", () => {
7876

7977
expect(result).toMatchObject({
8078
turnType: "confirmation",
81-
resolvedProposalId: "proposal-1",
79+
confidence: 0.97,
8280
});
8381
});
8482

@@ -242,7 +240,6 @@ describe("classifyTurn", () => {
242240
expect(result).toMatchObject({
243241
turnType: "edit_request",
244242
confidence: 0.88,
245-
resolvedEntityIds: ["task-1"],
246243
});
247244
});
248245

@@ -284,7 +281,7 @@ describe("classifyTurn", () => {
284281
});
285282
});
286283

287-
it("attaches resolvedProposalId when single proposal exists", async () => {
284+
it("does not attach resolvedProposalId (entity resolution moved to router)", async () => {
288285
const client = mockClient({
289286
turnType: "clarification_answer",
290287
confidence: 0.88,
@@ -316,7 +313,11 @@ describe("classifyTurn", () => {
316313
client,
317314
);
318315

319-
expect(result.resolvedProposalId).toBe("proposal-1");
316+
expect(result).toMatchObject({
317+
turnType: "clarification_answer",
318+
confidence: 0.88,
319+
});
320+
expect((result as Record<string, unknown>).resolvedProposalId).toBeUndefined();
320321
});
321322

322323
it("clamps confidence to 0-1 range", async () => {
Lines changed: 0 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import {
22
type ConversationEntity,
3-
getActivePendingClarifications,
43
type TurnClassifierInput,
54
type TurnClassifierOutput,
65
} from "@atlas/core";
@@ -14,7 +13,6 @@ export async function classifyTurn(
1413
client?: OpenAIResponsesClient,
1514
): Promise<TurnClassifierOutput> {
1615
const entityRegistry = input.entityRegistry ?? [];
17-
const discourseState = input.discourseState ?? null;
1816
const normalizedText = input.normalizedText.trim();
1917
const lower = normalizedText.toLowerCase();
2018

@@ -26,11 +24,6 @@ export async function classifyTurn(
2624
(entity.status === "active" || entity.status === "presented"),
2725
);
2826

29-
const resolvedEntityIds = compactResolvedEntityIds([
30-
discourseState?.currently_editable_entity_id ?? null,
31-
discourseState?.focus_entity_id ?? null,
32-
]);
33-
3427
const singleProposal =
3528
activeProposals.length === 1 ? activeProposals[0] : null;
3629

@@ -39,43 +32,22 @@ export async function classifyTurn(
3932
return {
4033
turnType: "confirmation",
4134
confidence: 0.97,
42-
resolvedEntityIds: singleProposal.data.targetEntityId
43-
? [singleProposal.data.targetEntityId]
44-
: resolvedEntityIds,
45-
resolvedProposalId: singleProposal.id,
4635
};
4736
}
4837

49-
// TEMP: disabled informational fast-exit until routing stabilizes
50-
// Fast-exit informational: question lead + no write verbs + no active clarifications
51-
// const activeClarifications = discourseState
52-
// ? getActivePendingClarifications(discourseState)
53-
// : [];
54-
55-
// if (isInformationalTurn(lower) && activeClarifications.length === 0 && !containsWriteVerb(lower)) {
56-
// return {
57-
// turnType: "informational",
58-
// confidence: 0.93,
59-
// resolvedEntityIds
60-
// };
61-
// }
62-
6338
// Everything else → LLM
6439
try {
6540
const llmResponse = await classifyTurnWithResponses(input, client);
6641

6742
return {
6843
turnType: llmResponse.turnType,
6944
confidence: Math.max(0, Math.min(1, llmResponse.confidence)),
70-
resolvedEntityIds,
71-
...(singleProposal ? { resolvedProposalId: singleProposal.id } : {}),
7245
};
7346
} catch {
7447
// Degrade gracefully: return unknown with low confidence
7548
return {
7649
turnType: "unknown",
7750
confidence: 0.3,
78-
resolvedEntityIds,
7951
};
8052
}
8153
}
@@ -86,20 +58,3 @@ function isPureConfirmationTurn(lower: string) {
8658
);
8759
}
8860

89-
function isInformationalTurn(lower: string) {
90-
return /^(what|when|where|why|how|who|which|can you explain|tell me)\b/.test(
91-
lower,
92-
);
93-
}
94-
95-
function containsWriteVerb(lower: string) {
96-
return /\b(schedule|plan|move|reschedule|shift|create|add|book|put|mark|complete|archive|cancel|delete|change|update)\b/.test(
97-
lower,
98-
);
99-
}
100-
101-
function compactResolvedEntityIds(entityIds: Array<string | null>) {
102-
return Array.from(
103-
new Set(entityIds.filter((id): id is string => Boolean(id))),
104-
);
105-
}

0 commit comments

Comments
 (0)