Skip to content

Commit 7a114fb

Browse files
committed
Merge branch 'staging'
2 parents 2aabb40 + b1fa260 commit 7a114fb

File tree

42 files changed

+3508
-550
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+3508
-550
lines changed

apps/api/src/agent/handlers/chart-handler.ts

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1+
import type { User } from '@databuddy/auth';
2+
import type { Website } from '@databuddy/shared';
13
import type { z } from 'zod';
24
import type { AIResponseJsonSchema } from '../prompts/agent';
35
import { executeQuery } from '../utils/query-executor';
46
import { validateSQL } from '../utils/sql-validator';
57
import type { StreamingUpdate } from '../utils/stream-utils';
68

7-
const getRandomMessage = (messages: string[]) =>
8-
messages[Math.floor(Math.random() * messages.length)];
9+
const getRandomMessage = (messages: string[]): string =>
10+
messages[Math.floor(Math.random() * messages.length)] ||
11+
'An error occurred while processing your request.';
912

1013
const queryFailedMessages = [
1114
'I ran into an issue getting that data. The information might not be available right now.',
@@ -20,8 +23,8 @@ const noDataMessages = [
2023
];
2124

2225
export interface ChartHandlerContext {
23-
user: any;
24-
website: any;
26+
user?: User | null;
27+
website: Website;
2528
debugInfo: Record<string, unknown>;
2629
startTime: number;
2730
aiTime: number;
@@ -35,7 +38,7 @@ export async function* handleChartResponse(
3538
yield {
3639
type: 'error',
3740
content: 'AI did not provide a query for the chart.',
38-
debugInfo: context.user.role === 'ADMIN' ? context.debugInfo : undefined,
41+
debugInfo: context.user?.role === 'ADMIN' ? context.debugInfo : undefined,
3942
};
4043
return;
4144
}
@@ -44,7 +47,7 @@ export async function* handleChartResponse(
4447
yield {
4548
type: 'error',
4649
content: 'Generated query failed security validation.',
47-
debugInfo: context.user.role === 'ADMIN' ? context.debugInfo : undefined,
50+
debugInfo: context.user?.role === 'ADMIN' ? context.debugInfo : undefined,
4851
};
4952
return;
5053
}
@@ -53,7 +56,7 @@ export async function* handleChartResponse(
5356
const queryResult = await executeQuery(parsedAiJson.sql);
5457
const totalTime = Date.now() - context.startTime;
5558

56-
if (context.user.role === 'ADMIN') {
59+
if (context.user?.role === 'ADMIN') {
5760
context.debugInfo.processing = {
5861
aiTime: context.aiTime,
5962
queryTime: Date.now() - context.startTime - context.aiTime,
@@ -65,15 +68,15 @@ export async function* handleChartResponse(
6568
type: 'complete',
6669
content:
6770
queryResult.data.length > 0
68-
? `Found ${queryResult.data.length} data points. Displaying as a ${parsedAiJson.chart_type?.replace(/_/g, ' ') || 'chart'}.`
71+
? `Found ${queryResult.data.length} data points. Displaying as a ${parsedAiJson.chart_type ? parsedAiJson.chart_type.replace(/_/g, ' ') : 'chart'}.`
6972
: getRandomMessage(noDataMessages),
7073
data: {
7174
hasVisualization: queryResult.data.length > 0,
7275
chartType: parsedAiJson.chart_type,
7376
data: queryResult.data,
7477
responseType: 'chart',
7578
},
76-
debugInfo: context.user.role === 'ADMIN' ? context.debugInfo : undefined,
79+
debugInfo: context.user?.role === 'ADMIN' ? context.debugInfo : undefined,
7780
};
7881
} catch (queryError: unknown) {
7982
console.error('❌ SQL execution error', {
@@ -83,7 +86,7 @@ export async function* handleChartResponse(
8386
yield {
8487
type: 'error',
8588
content: getRandomMessage(queryFailedMessages),
86-
debugInfo: context.user.role === 'ADMIN' ? context.debugInfo : undefined,
89+
debugInfo: context.user?.role === 'ADMIN' ? context.debugInfo : undefined,
8790
};
8891
}
8992
}

apps/api/src/agent/handlers/metric-handler.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1+
import type { User } from '@databuddy/auth';
2+
import type { Website } from '@databuddy/shared';
13
import type { z } from 'zod';
24
import type { AIResponseJsonSchema } from '../prompts/agent';
35
import { executeQuery } from '../utils/query-executor';
46
import { validateSQL } from '../utils/sql-validator';
57
import type { StreamingUpdate } from '../utils/stream-utils';
68

79
export interface MetricHandlerContext {
8-
user: any;
9-
website: any;
10+
user?: User | null;
11+
website: Website;
1012
debugInfo: Record<string, unknown>;
1113
}
1214

@@ -20,7 +22,7 @@ export async function* handleMetricResponse(
2022
type: 'error',
2123
content: 'Generated query failed security validation.',
2224
debugInfo:
23-
context.user.role === 'ADMIN' ? context.debugInfo : undefined,
25+
context.user?.role === 'ADMIN' ? context.debugInfo : undefined,
2426
};
2527
return;
2628
}
@@ -86,6 +88,6 @@ async function* sendMetricResponse(
8688
metricValue,
8789
metricLabel: parsedAiJson.metric_label,
8890
},
89-
debugInfo: context.user.role === 'ADMIN' ? context.debugInfo : undefined,
91+
debugInfo: context.user?.role === 'ADMIN' ? context.debugInfo : undefined,
9092
};
9193
}

apps/api/src/agent/processor.ts

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ import type { StreamingUpdate } from './utils/stream-utils';
99
import { generateThinkingSteps } from './utils/stream-utils';
1010

1111
// Simple message variation helpers
12-
const getRandomMessage = (messages: string[]) =>
13-
messages[Math.floor(Math.random() * messages.length)];
12+
const getRandomMessage = (messages: string[]): string =>
13+
messages[Math.floor(Math.random() * messages.length)] ||
14+
'An error occurred while processing your request.';
1415

1516
const parseErrorMessages = [
1617
"I'm having trouble understanding that request. Could you try asking in a different way?",
@@ -38,7 +39,7 @@ export interface AssistantRequest {
3839
}
3940

4041
export interface AssistantContext {
41-
user: User;
42+
user?: User | null;
4243
website: Website;
4344
debugInfo: Record<string, unknown>;
4445
}
@@ -56,7 +57,7 @@ export async function* processAssistantRequest(
5657
website_hostname: request.website_hostname,
5758
});
5859

59-
if (context.user.role === 'ADMIN') {
60+
if (context.user?.role === 'ADMIN') {
6061
context.debugInfo.validatedInput = {
6162
message: request.message,
6263
website_id: request.website_id,
@@ -70,7 +71,10 @@ export async function* processAssistantRequest(
7071
request.website_id,
7172
request.website_hostname,
7273
'execute_chat',
73-
request.context?.previousMessages,
74+
request.context?.previousMessages?.map((msg) => ({
75+
role: msg.role || 'user',
76+
content: msg.content,
77+
})),
7478
undefined,
7579
request.model
7680
);
@@ -90,7 +94,7 @@ export async function* processAssistantRequest(
9094
type: 'error',
9195
content: getRandomMessage(parseErrorMessages),
9296
debugInfo:
93-
context.user.role === 'ADMIN'
97+
context.user?.role === 'ADMIN'
9498
? {
9599
...context.debugInfo,
96100
parseError: parsedResponse.error,
@@ -107,7 +111,7 @@ export async function* processAssistantRequest(
107111
type: 'error',
108112
content: getRandomMessage(parseErrorMessages),
109113
debugInfo:
110-
context.user.role === 'ADMIN' ? context.debugInfo : undefined,
114+
context.user?.role === 'ADMIN' ? context.debugInfo : undefined,
111115
};
112116
return;
113117
}
@@ -131,7 +135,7 @@ export async function* processAssistantRequest(
131135
aiJson.text_response || "Here's the answer to your question.",
132136
data: { hasVisualization: false, responseType: 'text' },
133137
debugInfo:
134-
context.user.role === 'ADMIN' ? context.debugInfo : undefined,
138+
context.user?.role === 'ADMIN' ? context.debugInfo : undefined,
135139
};
136140
break;
137141

@@ -151,7 +155,7 @@ export async function* processAssistantRequest(
151155
type: 'error',
152156
content: 'Invalid chart configuration.',
153157
debugInfo:
154-
context.user.role === 'ADMIN' ? context.debugInfo : undefined,
158+
context.user?.role === 'ADMIN' ? context.debugInfo : undefined,
155159
};
156160
}
157161
break;
@@ -161,7 +165,7 @@ export async function* processAssistantRequest(
161165
type: 'error',
162166
content: 'Invalid response format from AI.',
163167
debugInfo:
164-
context.user.role === 'ADMIN' ? context.debugInfo : undefined,
168+
context.user?.role === 'ADMIN' ? context.debugInfo : undefined,
165169
};
166170
}
167171
} catch (error: unknown) {
@@ -175,7 +179,7 @@ export async function* processAssistantRequest(
175179
type: 'error',
176180
content: getRandomMessage(unexpectedErrorMessages),
177181
debugInfo:
178-
context.user.role === 'ADMIN' ? { error: errorMessage } : undefined,
182+
context.user?.role === 'ADMIN' ? { error: errorMessage } : undefined,
179183
};
180184
}
181185
}

apps/api/src/routes/assistant.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export const assistant = new Elysia({ prefix: '/v1/assistant' })
5353
};
5454

5555
const assistantContext: AssistantContext = {
56-
user: (user ?? null) as unknown as AssistantContext['user'],
56+
user: user ?? null,
5757
website,
5858
debugInfo: {},
5959
};

apps/api/src/routes/query.ts

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,13 @@ export const query = new Elysia({ prefix: '/v1/query' })
9999
try {
100100
if (Array.isArray(body)) {
101101
const uniqueWebsiteIds = [
102-
...new Set(body.flatMap((req) => req.parameters)),
102+
...new Set(
103+
body.flatMap((req) =>
104+
req.parameters.map((param) =>
105+
typeof param === 'string' ? param : param.name
106+
)
107+
)
108+
),
103109
];
104110
const domainCache = await getCachedWebsiteDomain(uniqueWebsiteIds);
105111

@@ -201,18 +207,48 @@ async function executeDynamicQuery(
201207
}
202208

203209
async function processParameter(
204-
parameter: string,
210+
parameterInput:
211+
| string
212+
| {
213+
name: string;
214+
start_date?: string;
215+
end_date?: string;
216+
granularity?: string;
217+
id?: string;
218+
},
205219
dynamicRequest: DynamicQueryRequestType,
206220
params: QueryParams,
207221
siteId: string | undefined,
208-
start: string | undefined,
209-
end: string | undefined,
222+
defaultStart: string | undefined,
223+
defaultEnd: string | undefined,
210224
domain: string | null
211225
) {
212-
const validation = validateParameterRequest(parameter, siteId, start, end);
226+
const isObject = typeof parameterInput === 'object';
227+
const parameterName = isObject ? parameterInput.name : parameterInput;
228+
const customId =
229+
isObject && parameterInput.id ? parameterInput.id : parameterName;
230+
const paramStart =
231+
isObject && parameterInput.start_date
232+
? parameterInput.start_date
233+
: defaultStart;
234+
const paramEnd =
235+
isObject && parameterInput.end_date
236+
? parameterInput.end_date
237+
: defaultEnd;
238+
const paramGranularity =
239+
isObject && parameterInput.granularity
240+
? parameterInput.granularity
241+
: dynamicRequest.granularity;
242+
243+
const validation = validateParameterRequest(
244+
parameterName,
245+
siteId,
246+
paramStart,
247+
paramEnd
248+
);
213249
if (!validation.success) {
214250
return {
215-
parameter,
251+
parameter: customId,
216252
success: false,
217253
error: validation.error,
218254
data: [],
@@ -222,10 +258,10 @@ async function executeDynamicQuery(
222258
try {
223259
const queryRequest = {
224260
projectId: validation.siteId,
225-
type: parameter,
261+
type: parameterName,
226262
from: validation.start,
227263
to: validation.end,
228-
timeUnit: getTimeUnit(dynamicRequest.granularity),
264+
timeUnit: getTimeUnit(paramGranularity),
229265
filters: dynamicRequest.filters || [],
230266
limit: dynamicRequest.limit || 100,
231267
offset: dynamicRequest.page
@@ -237,13 +273,13 @@ async function executeDynamicQuery(
237273
const data = await executeQuery(queryRequest, domain, params.timezone);
238274

239275
return {
240-
parameter,
276+
parameter: customId,
241277
success: true,
242278
data: data || [],
243279
};
244280
} catch (error) {
245281
return {
246-
parameter,
282+
parameter: customId,
247283
success: false,
248284
error: error instanceof Error ? error.message : 'Query failed',
249285
data: [],
@@ -252,7 +288,7 @@ async function executeDynamicQuery(
252288
}
253289

254290
const parameterResults = await Promise.all(
255-
request.parameters.map((param: string) => {
291+
request.parameters.map((param) => {
256292
return processParameter(
257293
param,
258294
request,

apps/api/src/schemas/query-schemas.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,24 @@ export const FilterSchema = t.Object({
2323
]),
2424
});
2525

26+
export const ParameterWithDatesSchema = t.Object({
27+
name: t.String(),
28+
start_date: t.Optional(t.String()),
29+
end_date: t.Optional(t.String()),
30+
granularity: t.Optional(
31+
t.Union([
32+
t.Literal('hourly'),
33+
t.Literal('daily'),
34+
t.Literal('hour'),
35+
t.Literal('day'),
36+
])
37+
),
38+
id: t.Optional(t.String()),
39+
});
40+
2641
export const DynamicQueryRequestSchema = t.Object({
2742
id: t.Optional(t.String()),
28-
parameters: t.Array(t.String()),
43+
parameters: t.Array(t.Union([t.String(), ParameterWithDatesSchema])),
2944
limit: t.Optional(t.Number()),
3045
page: t.Optional(t.Number()),
3146
filters: t.Optional(t.Array(FilterSchema)),
@@ -70,9 +85,17 @@ export type FilterType = {
7085
value: string | number | Array<string | number>;
7186
};
7287

88+
export type ParameterWithDatesType = {
89+
name: string;
90+
start_date?: string;
91+
end_date?: string;
92+
granularity?: 'hourly' | 'daily' | 'hour' | 'day';
93+
id?: string;
94+
};
95+
7396
export type DynamicQueryRequestType = {
7497
id?: string;
75-
parameters: string[];
98+
parameters: (string | ParameterWithDatesType)[];
7699
limit?: number;
77100
page?: number;
78101
filters?: FilterType[];

0 commit comments

Comments
 (0)