Skip to content

Commit cd6a516

Browse files
committed
feat: better background ai pipeline
1 parent 1c5f0b5 commit cd6a516

21 files changed

Lines changed: 884 additions & 84 deletions

File tree

apps/api/src/ai-pipeline/background-pipeline/index.test.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,6 @@ describe("runBackgroundPipeline", () => {
325325
hasLaterHumanMessage: false,
326326
hasLaterAiMessage: false,
327327
toolAllowlist: [
328-
"requestKnowledgeClarification",
329328
"updateConversationTitle",
330329
"updateSentiment",
331330
"setPriority",
@@ -406,11 +405,7 @@ describe("runBackgroundPipeline", () => {
406405
});
407406
expect(runGenerationRuntimeMock).toHaveBeenCalledWith(
408407
expect.objectContaining({
409-
toolAllowlist: [
410-
"requestKnowledgeClarification",
411-
"categorizeConversation",
412-
"skip",
413-
],
408+
toolAllowlist: ["categorizeConversation", "skip"],
414409
availableViews: [
415410
{
416411
id: "view-1",

apps/api/src/ai-pipeline/background-pipeline/index.ts

Lines changed: 10 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ type BackgroundPipelineContext = {
4848
};
4949

5050
const BACKGROUND_TOOL_IDS: AiAgentToolId[] = [
51-
"requestKnowledgeClarification",
5251
"updateConversationTitle",
5352
"updateSentiment",
5453
"setPriority",
@@ -66,10 +65,6 @@ function getBackgroundToolAllowlist(
6665
const settings = getBehaviorSettings(aiAgent);
6766
const allowlist: AiAgentToolId[] = [];
6867

69-
if (settings.canRequestKnowledgeClarification) {
70-
allowlist.push("requestKnowledgeClarification");
71-
}
72-
7368
if (settings.autoGenerateTitle) {
7469
allowlist.push("updateConversationTitle");
7570
}
@@ -378,22 +373,16 @@ export async function runBackgroundPipeline(
378373
};
379374
}
380375

381-
const knowledgeGapReviewResult =
382-
(generationResult.toolCallsByName.requestKnowledgeClarification ?? 0) > 0
383-
? ({
384-
status: "skipped",
385-
reason: "active_clarification_exists",
386-
} as const)
387-
: await runBackgroundKnowledgeGapReview({
388-
db: ctx.db,
389-
input: ctx.input,
390-
intake: {
391-
aiAgent: intakeResult.aiAgent,
392-
conversation: intakeResult.conversation,
393-
conversationHistory: intakeResult.conversationHistory,
394-
triggerMessage: intakeResult.triggerMessage,
395-
},
396-
});
376+
const knowledgeGapReviewResult = await runBackgroundKnowledgeGapReview({
377+
db: ctx.db,
378+
input: ctx.input,
379+
intake: {
380+
aiAgent: intakeResult.aiAgent,
381+
conversation: intakeResult.conversation,
382+
conversationHistory: intakeResult.conversationHistory,
383+
triggerMessage: intakeResult.triggerMessage,
384+
},
385+
});
397386

398387
if (knowledgeGapReviewResult.status === "created") {
399388
logAiPipeline({

apps/api/src/ai-pipeline/primary-pipeline/steps/intake/history.test.ts

Lines changed: 82 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,32 +65,37 @@ function createTool(params: {
6565
createdAt: string;
6666
query?: string;
6767
totalFound?: number;
68+
text?: string;
69+
input?: Record<string, unknown>;
70+
output?: Record<string, unknown>;
6871
}) {
6972
return {
7073
id: params.id,
7174
conversationId: "conv-1",
7275
organizationId: "org-1",
7376
type: "tool",
7477
text:
75-
params.toolName === "searchKnowledgeBase"
78+
params.text ??
79+
(params.toolName === "searchKnowledgeBase"
7680
? `Found ${params.totalFound ?? 0} relevant sources`
77-
: `Completed ${params.toolName}`,
81+
: `Completed ${params.toolName}`),
7882
parts: [
7983
{
8084
type: `tool-${params.toolName}`,
8185
toolCallId: `${params.id}-call`,
8286
toolName: params.toolName,
8387
state: "result",
84-
input: params.query ? { query: params.query } : {},
88+
input: params.input ?? (params.query ? { query: params.query } : {}),
8589
output:
86-
params.toolName === "searchKnowledgeBase"
90+
params.output ??
91+
(params.toolName === "searchKnowledgeBase"
8792
? {
8893
data: {
8994
totalFound: params.totalFound ?? 0,
9095
articles: [],
9196
},
9297
}
93-
: {},
98+
: {}),
9499
},
95100
],
96101
userId: null,
@@ -170,6 +175,78 @@ describe("buildConversationTranscript", () => {
170175
expect(toolEntries[0]?.content).toContain('query="refund policy"');
171176
expect(toolEntries[0]?.content).toContain("results=3");
172177
});
178+
179+
it("does not append metadata detail for unchanged background tool results", async () => {
180+
getConversationTimelineItemsMock.mockResolvedValueOnce({
181+
items: [
182+
createMessage(1, {
183+
senderType: "visitor",
184+
text: "Can you help with billing?",
185+
}),
186+
createTool({
187+
id: "tool-title-unchanged",
188+
toolName: "updateConversationTitle",
189+
createdAt: "2026-03-08T10:01:00.000Z",
190+
text: "Conversation title unchanged",
191+
input: { title: "Help with billing" },
192+
output: {
193+
data: {
194+
changed: false,
195+
reason: "unchanged",
196+
title: "Help with billing",
197+
},
198+
},
199+
}) as never,
200+
createTool({
201+
id: "tool-sentiment-unchanged",
202+
toolName: "updateSentiment",
203+
createdAt: "2026-03-08T10:02:00.000Z",
204+
text: "Sentiment unchanged",
205+
input: { sentiment: "positive" },
206+
output: {
207+
data: {
208+
changed: false,
209+
reason: "unchanged",
210+
sentiment: "positive",
211+
},
212+
},
213+
}) as never,
214+
createTool({
215+
id: "tool-priority-unchanged",
216+
toolName: "setPriority",
217+
createdAt: "2026-03-08T10:03:00.000Z",
218+
text: "Priority unchanged",
219+
input: { priority: "high" },
220+
output: {
221+
data: {
222+
changed: false,
223+
reason: "unchanged",
224+
priority: "high",
225+
},
226+
},
227+
}) as never,
228+
],
229+
hasNextPage: false,
230+
nextCursor: undefined,
231+
});
232+
233+
const { buildConversationTranscript } = await modulePromise;
234+
const transcript = await buildConversationTranscript({} as never, {
235+
conversationId: "conv-1",
236+
organizationId: "org-1",
237+
websiteId: "site-1",
238+
});
239+
240+
const toolEntries = transcript.filter(isConversationToolAction);
241+
242+
expect(toolEntries).toHaveLength(3);
243+
expect(toolEntries[0]?.content).toContain("Conversation title unchanged");
244+
expect(toolEntries[0]?.content).not.toContain('title="Help with billing"');
245+
expect(toolEntries[1]?.content).toContain("Sentiment unchanged");
246+
expect(toolEntries[1]?.content).not.toContain("sentiment=positive");
247+
expect(toolEntries[2]?.content).toContain("Priority unchanged");
248+
expect(toolEntries[2]?.content).not.toContain("priority=high");
249+
});
173250
});
174251

175252
describe("buildTriggerCenteredTimelineContext", () => {

apps/api/src/ai-pipeline/primary-pipeline/steps/intake/history.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,14 @@ function getNumberField(
125125
return typeof value === "number" && Number.isFinite(value) ? value : null;
126126
}
127127

128+
function getBooleanField(
129+
record: Record<string, unknown>,
130+
key: string
131+
): boolean | null {
132+
const value = record[key];
133+
return typeof value === "boolean" ? value : null;
134+
}
135+
128136
function mapTimelineMessage(
129137
item: TimelineItem
130138
): ConversationTranscriptEntry | null {
@@ -214,6 +222,12 @@ function summarizeIdentifyVisitor(toolPart: ToolPart): string | null {
214222
function summarizeUpdateConversationTitle(toolPart: ToolPart): string | null {
215223
const output = isRecord(toolPart.output) ? toolPart.output : null;
216224
const data = output && isRecord(output.data) ? output.data : null;
225+
const changed =
226+
(data && getBooleanField(data, "changed")) ??
227+
(output && getBooleanField(output, "changed"));
228+
if (changed === false) {
229+
return null;
230+
}
217231
const title =
218232
(data && getStringField(data, "title")) ??
219233
getStringField(toolPart.input, "title");
@@ -224,6 +238,12 @@ function summarizeUpdateConversationTitle(toolPart: ToolPart): string | null {
224238
function summarizeUpdateSentiment(toolPart: ToolPart): string | null {
225239
const output = isRecord(toolPart.output) ? toolPart.output : null;
226240
const data = output && isRecord(output.data) ? output.data : null;
241+
const changed =
242+
(data && getBooleanField(data, "changed")) ??
243+
(output && getBooleanField(output, "changed"));
244+
if (changed === false) {
245+
return null;
246+
}
227247
const sentiment =
228248
(data && getStringField(data, "sentiment")) ??
229249
getStringField(toolPart.input, "sentiment");
@@ -234,6 +254,12 @@ function summarizeUpdateSentiment(toolPart: ToolPart): string | null {
234254
function summarizeSetPriority(toolPart: ToolPart): string | null {
235255
const output = isRecord(toolPart.output) ? toolPart.output : null;
236256
const data = output && isRecord(output.data) ? output.data : null;
257+
const changed =
258+
(data && getBooleanField(data, "changed")) ??
259+
(output && getBooleanField(output, "changed"));
260+
if (changed === false) {
261+
return null;
262+
}
237263
const priority =
238264
(data && getStringField(data, "priority")) ??
239265
getStringField(toolPart.input, "priority");
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import type { Database } from "@api/db";
2+
import { conversation } from "@api/db/schema/conversation";
3+
import { eq } from "drizzle-orm";
4+
5+
export async function loadCurrentConversation(
6+
db: Database,
7+
conversationId: string
8+
) {
9+
const [currentConversation] = await db
10+
.select()
11+
.from(conversation)
12+
.where(eq(conversation.id, conversationId))
13+
.limit(1);
14+
15+
return currentConversation ?? null;
16+
}

0 commit comments

Comments
 (0)