Skip to content

Commit 97454ac

Browse files
committed
Harden background AI pipeline title updates
1 parent 0dd3673 commit 97454ac

19 files changed

Lines changed: 587 additions & 202 deletions

File tree

apps/api/src/ai-pipeline/ai-pipeline-workflow-spec.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ Real-time conversation handling for the active exchange:
1919
Delayed, non-public follow-up pipeline:
2020

2121
- scheduled after primary completion,
22-
- debounced per conversation with a 60s delay,
22+
- debounced per conversation with a 30s delay,
2323
- currently implemented as a shell/no-op pipeline (triage logic intentionally deferred).
2424

2525
## 2) Runtime Entry Points
@@ -64,7 +64,7 @@ Delayed, non-public follow-up pipeline:
6464
- `websiteId`
6565
- `organizationId`
6666
- `aiAgentId`
67-
- Delay: `60_000ms`
67+
- Delay: `30_000ms`
6868

6969
### 3.3 Redis Run Cursor (Primary)
7070

@@ -91,7 +91,7 @@ Updated after each successfully handled primary message (`completed` or `skipped
9191
- create delayed job (`status: "created"`).
9292
2. Existing `delayed` or `waiting` job:
9393
- remove existing,
94-
- add fresh delayed job (60s),
94+
- add fresh delayed job (30s),
9595
- return `status: "rescheduled"`.
9696
3. Existing `active` job:
9797
- do not interrupt,
@@ -109,7 +109,7 @@ Background scheduling is triggered from primary worker completion hook:
109109

110110
1. Primary run completes (not failed).
111111
2. Primary processed at least one message (`processedMessageCount > 0`).
112-
3. Worker schedules `ai-agent-background` with `delayMs = 60_000`.
112+
3. Worker schedules `ai-agent-background` with `delayMs = 30_000`.
113113

114114
Notes:
115115

@@ -129,7 +129,7 @@ Notes:
129129
4. Primary pipeline returns `completed|skipped|error` per message.
130130
5. Primary worker advances DB cursor for handled messages.
131131
6. Primary completion hook maintains primary cursor semantics (immediate follow-up if needed).
132-
7. Primary completion hook schedules background queue (60s) when processed message count > 0.
132+
7. Primary completion hook schedules background queue (30s) when processed message count > 0.
133133
8. Background worker executes background pipeline shell on delayed trigger.
134134

135135
## 7) Sequence Diagrams
@@ -146,20 +146,20 @@ sequenceDiagram
146146
Q1->>W1: process primary job
147147
W1->>P1: runPrimaryPipeline (window messages)
148148
P1-->>W1: completed/skipped
149-
W1->>Q2: enqueue background job (delay=60000)
149+
W1->>Q2: enqueue background job (delay=30000)
150150
```
151151

152-
### 7.2 Repeated primary completions within 60s
152+
### 7.2 Repeated primary completions within 30s
153153

154154
```mermaid
155155
sequenceDiagram
156156
participant W1 as "Primary Worker"
157157
participant Q2 as "Background Scheduler"
158158
159-
W1->>Q2: enqueue conv job (delay=60000)
159+
W1->>Q2: enqueue conv job (delay=30000)
160160
W1->>Q2: enqueue same conv again before delay
161161
Q2->>Q2: remove waiting/delayed prior job
162-
Q2->>Q2: add new delayed job (delay=60000)
162+
Q2->>Q2: add new delayed job (delay=30000)
163163
```
164164

165165
### 7.3 Background active + new primary completion

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

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ const loadConversationSeedMock = mock(async () => ({
5050
organizationId: "org-1",
5151
websiteId: "site-1",
5252
visitorId: "visitor-1",
53+
title: null as string | null,
54+
titleSource: null as "ai" | "user" | null,
55+
visitorTitle: null as string | null,
56+
visitorTitleLanguage: null as string | null,
5357
},
5458
triggerMetadata: {
5559
id: "msg-1",
@@ -58,6 +62,9 @@ const loadConversationSeedMock = mock(async () => ({
5862
},
5963
}));
6064
const loadIntakeContextMock = mock(async () => ({
65+
websiteDefaultLanguage: "en",
66+
visitorLanguage: "es",
67+
autoTranslateEnabled: true,
6168
conversationHistory: [],
6269
decisionMessages: [],
6370
generationEntries: [],
@@ -215,6 +222,10 @@ describe("runBackgroundPipeline", () => {
215222
organizationId: "org-1",
216223
websiteId: "site-1",
217224
visitorId: "visitor-1",
225+
title: null as string | null,
226+
titleSource: null as "ai" | "user" | null,
227+
visitorTitle: null as string | null,
228+
visitorTitleLanguage: null as string | null,
218229
},
219230
triggerMetadata: {
220231
id: "msg-1",
@@ -223,6 +234,9 @@ describe("runBackgroundPipeline", () => {
223234
},
224235
});
225236
loadIntakeContextMock.mockResolvedValue({
237+
websiteDefaultLanguage: "en",
238+
visitorLanguage: "es",
239+
autoTranslateEnabled: true,
226240
conversationHistory: [],
227241
decisionMessages: [],
228242
generationEntries: [],
@@ -342,6 +356,74 @@ describe("runBackgroundPipeline", () => {
342356
);
343357
});
344358

359+
it("passes multilingual title state into the background analysis runtime", async () => {
360+
loadConversationSeedMock.mockResolvedValueOnce({
361+
conversation: {
362+
id: "conv-1",
363+
organizationId: "org-1",
364+
websiteId: "site-1",
365+
visitorId: "visitor-1",
366+
title: "Billing question",
367+
titleSource: "ai",
368+
visitorTitle: "Pregunta de facturacion",
369+
visitorTitleLanguage: "es",
370+
},
371+
triggerMetadata: {
372+
id: "msg-1",
373+
createdAt: "2026-03-04T10:00:00.000Z",
374+
conversationId: "conv-1",
375+
},
376+
});
377+
loadIntakeContextMock.mockResolvedValueOnce({
378+
websiteDefaultLanguage: "en",
379+
visitorLanguage: "es",
380+
autoTranslateEnabled: true,
381+
conversationHistory: [],
382+
decisionMessages: [],
383+
generationEntries: [],
384+
visitorContext: null,
385+
conversationState: {
386+
hasHumanAssignee: false,
387+
assigneeIds: [],
388+
participantIds: [],
389+
isEscalated: false,
390+
escalationReason: null,
391+
},
392+
triggerMessage: {
393+
messageId: "msg-1",
394+
content: "Necesito ayuda con la facturacion",
395+
senderType: "visitor",
396+
senderId: null,
397+
senderName: null,
398+
timestamp: "2026-03-04T10:00:00.000Z",
399+
visibility: "public",
400+
},
401+
hasLaterHumanMessage: false,
402+
hasLaterAiMessage: false,
403+
});
404+
405+
const { runBackgroundPipeline } = await modulePromise;
406+
const result = await runBackgroundPipeline({
407+
db: {} as never,
408+
input: baseInput,
409+
});
410+
411+
expect(result.status).toBe("completed");
412+
expect(runGenerationRuntimeMock).toHaveBeenCalledWith(
413+
expect.objectContaining({
414+
websiteDefaultLanguage: "en",
415+
visitorLanguage: "es",
416+
autoTranslateEnabled: true,
417+
conversation: expect.objectContaining({
418+
title: "Billing question",
419+
titleSource: "ai",
420+
visitorTitle: "Pregunta de facturacion",
421+
visitorTitleLanguage: "es",
422+
}),
423+
})
424+
);
425+
});
426+
345427
it("returns skipped when the analysis run makes no metadata mutation", async () => {
346428
runGenerationRuntimeMock.mockResolvedValueOnce({
347429
status: "completed",

0 commit comments

Comments
 (0)