Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
File renamed without changes.
18 changes: 17 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
{
"typescript.tsdk": "node_modules\\typescript\\lib"
"typescript.tsdk": "node_modules/typescript/lib",
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[javascript]": { "editor.defaultFormatter": "biomejs.biome" },
"[typescript]": { "editor.defaultFormatter": "biomejs.biome" },
"[javascriptreact]": { "editor.defaultFormatter": "biomejs.biome" },
"[typescriptreact]": { "editor.defaultFormatter": "biomejs.biome" },
"[json]": { "editor.defaultFormatter": "biomejs.biome" },
"[jsonc]": { "editor.defaultFormatter": "biomejs.biome" },
"[css]": { "editor.defaultFormatter": "biomejs.biome" },
"[graphql]": { "editor.defaultFormatter": "biomejs.biome" },
"editor.formatOnSave": true,
"editor.formatOnPaste": true,
"emmet.showExpandedAbbreviation": "never",
"editor.codeActionsOnSave": {
"source.fixAll.biome": "explicit",
"source.organizeImports.biome": "explicit"
}
}
1 change: 0 additions & 1 deletion apps/api/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
.env.local

# caches
.eslintcache
.cache
*.tsbuildinfo

Expand Down
37 changes: 0 additions & 37 deletions apps/api/biome.json

This file was deleted.

2 changes: 1 addition & 1 deletion apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,4 @@
"nats": "^2.29.3",
"openai": "^5.9.0"
}
}
}
119 changes: 60 additions & 59 deletions apps/api/src/agent/handlers/chart-handler.ts
Original file line number Diff line number Diff line change
@@ -1,73 +1,74 @@
import type { z } from 'zod';
import type { AIResponseJsonSchema } from '../prompts/agent';
import { validateSQL } from '../utils/sql-validator';
import { executeQuery } from '../utils/query-executor';
import { validateSQL } from '../utils/sql-validator';
import type { StreamingUpdate } from '../utils/stream-utils';

export interface ChartHandlerContext {
user: any;
website: any;
debugInfo: Record<string, unknown>;
startTime: number;
aiTime: number;
user: any;
website: any;
debugInfo: Record<string, unknown>;
startTime: number;
aiTime: number;
}

export async function* handleChartResponse(
parsedAiJson: z.infer<typeof AIResponseJsonSchema>,
context: ChartHandlerContext
parsedAiJson: z.infer<typeof AIResponseJsonSchema>,
context: ChartHandlerContext
): AsyncGenerator<StreamingUpdate> {
if (!parsedAiJson.sql) {
yield {
type: 'error',
content: "AI did not provide a query for the chart.",
debugInfo: context.user.role === 'ADMIN' ? context.debugInfo : undefined
};
return;
}
if (!parsedAiJson.sql) {
yield {
type: 'error',
content: 'AI did not provide a query for the chart.',
debugInfo: context.user.role === 'ADMIN' ? context.debugInfo : undefined,
};
return;
}

if (!validateSQL(parsedAiJson.sql)) {
yield {
type: 'error',
content: "Generated query failed security validation.",
debugInfo: context.user.role === 'ADMIN' ? context.debugInfo : undefined
};
return;
}
if (!validateSQL(parsedAiJson.sql)) {
yield {
type: 'error',
content: 'Generated query failed security validation.',
debugInfo: context.user.role === 'ADMIN' ? context.debugInfo : undefined,
};
return;
}

try {
const queryResult = await executeQuery(parsedAiJson.sql);
const totalTime = Date.now() - context.startTime;
try {
const queryResult = await executeQuery(parsedAiJson.sql);
const totalTime = Date.now() - context.startTime;

if (context.user.role === 'ADMIN') {
context.debugInfo.processing = {
aiTime: context.aiTime,
queryTime: Date.now() - context.startTime - context.aiTime,
totalTime
};
}

yield {
type: 'complete',
content: queryResult.data.length > 0
? `Found ${queryResult.data.length} data points. Displaying as a ${parsedAiJson.chart_type?.replace(/_/g, ' ') || 'chart'}.`
: "No data found for your query.",
data: {
hasVisualization: queryResult.data.length > 0,
chartType: parsedAiJson.chart_type,
data: queryResult.data,
responseType: 'chart'
},
debugInfo: context.user.role === 'ADMIN' ? context.debugInfo : undefined
};
} catch (queryError: unknown) {
console.error('❌ SQL execution error', {
error: queryError instanceof Error ? queryError.message : 'Unknown error',
sql: parsedAiJson.sql
});
yield {
type: 'error',
content: "Database query failed. The data might not be available.",
debugInfo: context.user.role === 'ADMIN' ? context.debugInfo : undefined
};
if (context.user.role === 'ADMIN') {
context.debugInfo.processing = {
aiTime: context.aiTime,
queryTime: Date.now() - context.startTime - context.aiTime,
totalTime,
};
}
}

yield {
type: 'complete',
content:
queryResult.data.length > 0
? `Found ${queryResult.data.length} data points. Displaying as a ${parsedAiJson.chart_type?.replace(/_/g, ' ') || 'chart'}.`
: 'No data found for your query.',
data: {
hasVisualization: queryResult.data.length > 0,
chartType: parsedAiJson.chart_type,
data: queryResult.data,
responseType: 'chart',
},
debugInfo: context.user.role === 'ADMIN' ? context.debugInfo : undefined,
};
} catch (queryError: unknown) {
console.error('❌ SQL execution error', {
error: queryError instanceof Error ? queryError.message : 'Unknown error',
sql: parsedAiJson.sql,
});
yield {
type: 'error',
content: 'Database query failed. The data might not be available.',
debugInfo: context.user.role === 'ADMIN' ? context.debugInfo : undefined,
};
}
}
121 changes: 68 additions & 53 deletions apps/api/src/agent/handlers/metric-handler.ts
Original file line number Diff line number Diff line change
@@ -1,74 +1,89 @@
import type { z } from 'zod';
import type { AIResponseJsonSchema } from '../prompts/agent';
import { validateSQL } from '../utils/sql-validator';
import { executeQuery } from '../utils/query-executor';
import { validateSQL } from '../utils/sql-validator';
import type { StreamingUpdate } from '../utils/stream-utils';

export interface MetricHandlerContext {
user: any;
website: any;
debugInfo: Record<string, unknown>;
user: any;
website: any;
debugInfo: Record<string, unknown>;
}

export async function* handleMetricResponse(
parsedAiJson: z.infer<typeof AIResponseJsonSchema>,
context: MetricHandlerContext
parsedAiJson: z.infer<typeof AIResponseJsonSchema>,
context: MetricHandlerContext
): AsyncGenerator<StreamingUpdate> {
if (parsedAiJson.sql) {
if (!validateSQL(parsedAiJson.sql)) {
yield {
type: 'error',
content: "Generated query failed security validation.",
debugInfo: context.user.role === 'ADMIN' ? context.debugInfo : undefined
};
return;
}
if (parsedAiJson.sql) {
if (!validateSQL(parsedAiJson.sql)) {
yield {
type: 'error',
content: 'Generated query failed security validation.',
debugInfo:
context.user.role === 'ADMIN' ? context.debugInfo : undefined,
};
return;
}

try {
const queryResult = await executeQuery(parsedAiJson.sql);
const metricValue = extractMetricValue(queryResult.data, parsedAiJson.metric_value);
yield* sendMetricResponse(parsedAiJson, metricValue, context);
} catch (queryError: unknown) {
console.error('❌ Metric SQL execution error', {
error: queryError instanceof Error ? queryError.message : 'Unknown error',
sql: parsedAiJson.sql
});
yield* sendMetricResponse(parsedAiJson, parsedAiJson.metric_value, context);
}
} else {
yield* sendMetricResponse(parsedAiJson, parsedAiJson.metric_value, context);
try {
const queryResult = await executeQuery(parsedAiJson.sql);
const metricValue = extractMetricValue(
queryResult.data,
parsedAiJson.metric_value
);
yield* sendMetricResponse(parsedAiJson, metricValue, context);
} catch (queryError: unknown) {
console.error('❌ Metric SQL execution error', {
error:
queryError instanceof Error ? queryError.message : 'Unknown error',
sql: parsedAiJson.sql,
});
yield* sendMetricResponse(
parsedAiJson,
parsedAiJson.metric_value,
context
);
}
} else {
yield* sendMetricResponse(parsedAiJson, parsedAiJson.metric_value, context);
}
}

function extractMetricValue(queryData: unknown[], defaultValue: unknown): unknown {
if (!queryData.length || !queryData[0]) return defaultValue;
function extractMetricValue(
queryData: unknown[],
defaultValue: unknown
): unknown {
if (!(queryData.length && queryData[0])) return defaultValue;

const firstRow = queryData[0] as Record<string, unknown>;
const valueKey = Object.keys(firstRow).find(key => typeof firstRow[key] === 'number') ||
Object.keys(firstRow)[0];
const firstRow = queryData[0] as Record<string, unknown>;
const valueKey =
Object.keys(firstRow).find((key) => typeof firstRow[key] === 'number') ||
Object.keys(firstRow)[0];

return valueKey ? firstRow[valueKey] : defaultValue;
return valueKey ? firstRow[valueKey] : defaultValue;
}

async function* sendMetricResponse(
parsedAiJson: z.infer<typeof AIResponseJsonSchema>,
metricValue: unknown,
context: MetricHandlerContext
parsedAiJson: z.infer<typeof AIResponseJsonSchema>,
metricValue: unknown,
context: MetricHandlerContext
): AsyncGenerator<StreamingUpdate> {
const formattedValue = typeof metricValue === 'number'
? metricValue.toLocaleString()
: metricValue;
const formattedValue =
typeof metricValue === 'number'
? metricValue.toLocaleString()
: metricValue;

yield {
type: 'complete',
content: parsedAiJson.text_response ||
`${parsedAiJson.metric_label || 'Result'}: ${formattedValue}`,
data: {
hasVisualization: false,
responseType: 'metric',
metricValue: metricValue,
metricLabel: parsedAiJson.metric_label
},
debugInfo: context.user.role === 'ADMIN' ? context.debugInfo : undefined
};
}
yield {
type: 'complete',
content:
parsedAiJson.text_response ||
`${parsedAiJson.metric_label || 'Result'}: ${formattedValue}`,
data: {
hasVisualization: false,
responseType: 'metric',
metricValue,
metricLabel: parsedAiJson.metric_label,
},
debugInfo: context.user.role === 'ADMIN' ? context.debugInfo : undefined,
};
}
30 changes: 17 additions & 13 deletions apps/api/src/agent/index.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
export { validateSQL } from './utils/sql-validator';
export { executeQuery } from './utils/query-executor';
export { getAICompletion } from './utils/ai-client';
export { parseAIResponse } from './utils/response-parser';
export { createStreamingResponse, generateThinkingSteps } from './utils/stream-utils';
export type { StreamingUpdate } from './utils/stream-utils';

export { handleMetricResponse } from './handlers/metric-handler';
export type { ChartHandlerContext } from './handlers/chart-handler';
export { handleChartResponse } from './handlers/chart-handler';
export type { MetricHandlerContext } from './handlers/metric-handler';
export type { ChartHandlerContext } from './handlers/chart-handler';

export { handleMetricResponse } from './handlers/metric-handler';
export type { AssistantContext, AssistantRequest } from './processor';
export { processAssistantRequest } from './processor';
export type { AssistantRequest, AssistantContext } from './processor';

export { comprehensiveUnifiedPrompt, AIResponseJsonSchema, AIPlanSchema } from './prompts/agent';
export {
AIPlanSchema,
AIResponseJsonSchema,
comprehensiveUnifiedPrompt,
} from './prompts/agent';
export { getAICompletion } from './utils/ai-client';
export { executeQuery } from './utils/query-executor';
export { parseAIResponse } from './utils/response-parser';
export { validateSQL } from './utils/sql-validator';
export type { StreamingUpdate } from './utils/stream-utils';
export {
createStreamingResponse,
generateThinkingSteps,
} from './utils/stream-utils';
Loading
Loading