Skip to content

Commit c55faf5

Browse files
authored
Merge pull request #485 from databuddy-analytics/staging
Land in-flight ai, db, and redis cleanup
2 parents 60be929 + ff9491a commit c55faf5

17 files changed

Lines changed: 1014 additions & 27 deletions

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
"db:deploy": "dotenv -- turbo run db:deploy",
5555
"db:seed": "dotenv -- sh -c 'cd packages/db && bun run db:seed \"$@\"' --",
5656
"email:dev": "dotenv -- sh -c 'cd packages/email && bun run dev'",
57+
"email:segments": "dotenv -- sh -c 'cd packages/db && bun run email:segments \"$@\"' --",
5758
"sdk:build": "turbo run build --filter @databuddy/sdk --filter @databuddy/cache",
5859
"dev:dashboard": "dotenv -- turbo run dev --filter @databuddy/dashboard --filter @databuddy/api --filter @databuddy/insights",
5960
"dev:insights": "dotenv -- turbo run dev --filter @databuddy/insights",

packages/ai/src/ai/mcp/agent-tools.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,8 @@ export function createMcpAgentTools(
147147
const data = await executeQuery(
148148
queryRequest,
149149
websiteDomain,
150-
queryRequest.timezone
150+
queryRequest.timezone,
151+
options.abortSignal
151152
);
152153
return { data, rowCount: data.length, type: args.type };
153154
},
@@ -182,6 +183,7 @@ Critical schema footguns: website id column is client_id (not website_id); times
182183
sql: args.sql,
183184
params: args.params,
184185
toolName: "MCP Agent SQL",
186+
abortSignal: options.abortSignal,
185187
});
186188
},
187189
}),
@@ -233,6 +235,7 @@ Critical schema footguns: website id column is client_id (not website_id); times
233235
const results = await executeBatch(requests, {
234236
websiteDomain,
235237
timezone: args.timezone ?? "UTC",
238+
abortSignal: options.abortSignal,
236239
});
237240
return {
238241
batch: true,

packages/ai/src/ai/mcp/investigate.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,12 @@ export async function runInvestigation(
302302
"",
303303
"Tool trace (what was actually queried and returned):",
304304
compactTrace(trace.toolCalls),
305+
...(trace.truncated
306+
? [
307+
"",
308+
"NOTE: This investigation was stopped by a time limit before the agent finished gathering data. Base your memo only on the partial tool trace above. State in the verdict reason that the run was time-limited, and set confidence to 'low' unless the partial evidence is unambiguous.",
309+
]
310+
: []),
305311
].join("\n"),
306312
});
307313

packages/ai/src/ai/mcp/run-agent.ts

Lines changed: 70 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
storeConversation,
66
} from "../../lib/supermemory";
77
import type { ApiKeyRow } from "@databuddy/api-keys/resolve";
8-
import type { LanguageModelUsage } from "ai";
8+
import type { LanguageModelUsage, StepResult, ToolSet } from "ai";
99
import { ToolLoopAgent } from "ai";
1010
import { DatabuddyAgentUserError } from "../../agent/errors";
1111
import { getAILogger } from "../../lib/ai-logger";
@@ -55,6 +55,7 @@ export interface RunMcpAgentTraceResult {
5555
answer: string;
5656
steps: number;
5757
toolCalls: McpAgentToolTrace[];
58+
truncated?: boolean;
5859
usage: LanguageModelUsage;
5960
}
6061

@@ -114,11 +115,73 @@ export async function runMcpAgentWithTrace(
114115
toolCalls: collectToolTrace(result.steps),
115116
usage: result.totalUsage,
116117
};
118+
} catch (err) {
119+
if (isInternalTimeoutAbort(err, options.abortSignal)) {
120+
return await buildTruncatedTrace(prepared);
121+
}
122+
throw err;
117123
} finally {
118124
abort.cleanup();
119125
}
120126
}
121127

128+
function isInternalTimeoutAbort(
129+
err: unknown,
130+
externalSignal: AbortSignal | undefined
131+
): boolean {
132+
return (
133+
err instanceof Error &&
134+
err.name === "AbortError" &&
135+
!externalSignal?.aborted
136+
);
137+
}
138+
139+
async function buildTruncatedTrace(
140+
prepared: Awaited<ReturnType<typeof prepareMcpAgentRun>>
141+
): Promise<RunMcpAgentTraceResult> {
142+
const steps = prepared.capturedSteps;
143+
const usage = aggregateStepUsage(steps);
144+
await trackPreparedUsage(prepared, usage);
145+
const stepCount = steps.length;
146+
return {
147+
answer: `The investigation reached its time budget after ${stepCount} step${stepCount === 1 ? "" : "s"} and was stopped before composing a final summary. The partial tool trace is the only evidence gathered.`,
148+
steps: stepCount,
149+
toolCalls: collectToolTrace(steps),
150+
truncated: true,
151+
usage,
152+
};
153+
}
154+
155+
function aggregateStepUsage(
156+
steps: ReadonlyArray<{ usage: LanguageModelUsage }>
157+
): LanguageModelUsage {
158+
let inputTokens = 0;
159+
let outputTokens = 0;
160+
let totalTokens = 0;
161+
let noCacheTokens = 0;
162+
let cacheReadTokens = 0;
163+
let cacheWriteTokens = 0;
164+
let textTokens = 0;
165+
let reasoningTokens = 0;
166+
for (const { usage } of steps) {
167+
inputTokens += usage.inputTokens ?? 0;
168+
outputTokens += usage.outputTokens ?? 0;
169+
totalTokens += usage.totalTokens ?? 0;
170+
noCacheTokens += usage.inputTokenDetails?.noCacheTokens ?? 0;
171+
cacheReadTokens += usage.inputTokenDetails?.cacheReadTokens ?? 0;
172+
cacheWriteTokens += usage.inputTokenDetails?.cacheWriteTokens ?? 0;
173+
textTokens += usage.outputTokenDetails?.textTokens ?? 0;
174+
reasoningTokens += usage.outputTokenDetails?.reasoningTokens ?? 0;
175+
}
176+
return {
177+
inputTokens,
178+
outputTokens,
179+
totalTokens,
180+
inputTokenDetails: { noCacheTokens, cacheReadTokens, cacheWriteTokens },
181+
outputTokenDetails: { textTokens, reasoningTokens },
182+
};
183+
}
184+
122185
export async function* streamMcpAgentText(
123186
options: RunMcpAgentOptions
124187
): AsyncGenerator<string> {
@@ -189,12 +252,7 @@ async function prepareMcpAgentRun(options: RunMcpAgentOptions) {
189252
const selectedModelId =
190253
options.modelOverride ?? getDefaultAgentModelId(source);
191254

192-
const apiKeyId =
193-
options.apiKey &&
194-
typeof options.apiKey === "object" &&
195-
"id" in options.apiKey
196-
? (options.apiKey as { id: string }).id
197-
: null;
255+
const apiKeyId = options.apiKey?.id ?? null;
198256

199257
const billingCustomerId =
200258
options.billingMode === "skip"
@@ -265,6 +323,7 @@ async function prepareMcpAgentRun(options: RunMcpAgentOptions) {
265323
mcpTelemetryMetadata["tcc.sessionId"] = sessionId;
266324

267325
const ai = getAILogger();
326+
const capturedSteps: StepResult<ToolSet>[] = [];
268327
const agent = new ToolLoopAgent({
269328
model: ai.wrap(config.model),
270329
instructions,
@@ -273,6 +332,9 @@ async function prepareMcpAgentRun(options: RunMcpAgentOptions) {
273332
stopWhen: config.stopWhen,
274333
temperature: config.temperature,
275334
experimental_context: config.experimental_context,
335+
onStepFinish: (step) => {
336+
capturedSteps.push(step);
337+
},
276338
experimental_telemetry: {
277339
isEnabled: true,
278340
functionId: `databuddy.${source}.ask`,
@@ -296,6 +358,7 @@ async function prepareMcpAgentRun(options: RunMcpAgentOptions) {
296358
agent,
297359
apiKeyId,
298360
billingCustomerId,
361+
capturedSteps,
299362
memoryUserId,
300363
mcpUserId,
301364
messages,

packages/ai/src/ai/tools/execute-sql-query.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,14 @@ export async function executeAgentSqlForWebsite({
3232
sql,
3333
params,
3434
toolName = "Execute SQL Tool",
35+
abortSignal,
3536
}: {
3637
websiteId: string;
3738
websiteDomain?: string;
3839
sql: string;
3940
params?: Record<string, unknown>;
4041
toolName?: string;
42+
abortSignal?: AbortSignal;
4143
}): Promise<QueryResult> {
4244
const validation = validateAgentSQL(sql);
4345
if (!validation.valid) {
@@ -66,7 +68,8 @@ export async function executeAgentSqlForWebsite({
6668
max_result_bytes: 50_000_000,
6769
result_overflow_mode: "break",
6870
use_query_cache: 0,
69-
}
71+
},
72+
abortSignal
7073
);
7174

7275
return result.data.length > MAX_MODEL_ROWS
@@ -104,6 +107,7 @@ export const executeSqlQueryTool = tool({
104107
websiteDomain: resolved.domain,
105108
sql,
106109
params,
110+
abortSignal: options.abortSignal,
107111
});
108112
},
109113
});

packages/ai/src/ai/tools/utils/query.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,13 +73,15 @@ export async function executeTimedQuery<T extends Record<string, unknown>>(
7373
sql: string,
7474
params: Record<string, unknown> = {},
7575
logContext?: Record<string, unknown>,
76-
clickhouseSettings?: Record<string, string | number>
76+
clickhouseSettings?: Record<string, string | number>,
77+
abortSignal?: AbortSignal
7778
): Promise<QueryResult<T>> {
7879
const logger = createToolLogger(toolName);
7980
const queryStart = Date.now();
8081

8182
try {
8283
const raw = await chQuery<T>(sql, params, {
84+
abort_signal: abortSignal,
8385
readonly: true,
8486
...(clickhouseSettings && { clickhouse_settings: clickhouseSettings }),
8587
});

packages/ai/src/query/batch-executor.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ interface BatchResult {
1515
type: string;
1616
}
1717
interface BatchOptions {
18+
abortSignal?: AbortSignal;
1819
timezone?: string;
1920
websiteDomain?: string | null;
2021
}
@@ -252,7 +253,7 @@ async function runSingle(
252253
{ ...req, timezone: opts?.timezone ?? req.timezone },
253254
opts?.websiteDomain
254255
);
255-
const data = await builder.execute();
256+
const data = await builder.execute(opts?.abortSignal);
256257

257258
mergeWideEvent({
258259
query_type: req.type,
@@ -422,6 +423,7 @@ export async function executeBatch(
422423
({ req }) => QueryBuilders[req.type]?.noCache
423424
);
424425
const rawRows = await chQuery(sql, params, {
426+
abort_signal: opts?.abortSignal,
425427
clickhouse_settings: getClickHouseQuerySettings(groupNoCache),
426428
});
427429

packages/ai/src/query/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,9 @@ function createBuilder(
8383
export const executeQuery = async (
8484
request: QueryRequest,
8585
websiteDomain?: string | null,
86-
timezone?: string
87-
) => createBuilder(request, websiteDomain, timezone).execute();
86+
timezone?: string,
87+
abortSignal?: AbortSignal
88+
) => createBuilder(request, websiteDomain, timezone).execute(abortSignal);
8889

8990
export const compileQuery = (
9091
request: QueryRequest,

packages/ai/src/query/simple-builder.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1104,9 +1104,10 @@ export class SimpleQueryBuilder {
11041104
return this.request.offset ? ` OFFSET ${this.request.offset}` : "";
11051105
}
11061106

1107-
async execute(): Promise<Record<string, unknown>[]> {
1107+
async execute(abortSignal?: AbortSignal): Promise<Record<string, unknown>[]> {
11081108
const { sql, params } = this.compile();
11091109
const rawData = await chQuery<Record<string, unknown>>(sql, params, {
1110+
abort_signal: abortSignal,
11101111
clickhouse_settings: getClickHouseQuerySettings(this.config.noCache),
11111112
});
11121113
return applyPlugins(rawData, this.config, this.websiteDomain);

packages/db/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"db:push": "dotenv -- drizzle-kit push",
1919
"db:studio": "dotenv -- drizzle-kit studio",
2020
"db:seed": "tsx src/seed.ts",
21+
"email:segments": "dotenv -- bun src/scripts/export-user-email-segments.ts",
2122
"clickhouse:init": "tsx src/clickhouse/setup.ts",
2223
"test": "bun test src",
2324
"e2e:db:create": "bun ./scripts/e2e-db-lifecycle.ts create",

0 commit comments

Comments
 (0)