mirror of
https://github.com/grafana/grafana.git
synced 2025-12-23 13:14:35 +08:00
Compare commits
1 Commits
docs/add-t
...
alexspence
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c61bfc5d3 |
@@ -1,18 +1,20 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
import { t } from '@grafana/i18n';
|
import { t } from '@grafana/i18n';
|
||||||
|
|
||||||
import { GenAIButton } from '../../../../dashboard/components/GenAI/GenAIButton';
|
import { GenAIButton } from '../../../../dashboard/components/GenAI/GenAIButton';
|
||||||
import { EventTrackingSrc } from '../../../../dashboard/components/GenAI/tracking';
|
import { EventTrackingSrc } from '../../../../dashboard/components/GenAI/tracking';
|
||||||
import { Message, Role } from '../../../../dashboard/components/GenAI/utils';
|
import { Message, Role } from '../../../../dashboard/components/GenAI/utils';
|
||||||
|
import { SQLSchemasResponse } from '../hooks/useSQLSchemas';
|
||||||
|
|
||||||
import { getSQLExplanationSystemPrompt, QueryUsageContext } from './sqlPromptConfig';
|
import { getSQLExplanationSystemPrompt, QueryUsageContext } from './sqlPromptConfig';
|
||||||
|
import { formatSchemasForPrompt } from './utils/formatSchemas';
|
||||||
|
|
||||||
interface GenAISQLExplainButtonProps {
|
interface GenAISQLExplainButtonProps {
|
||||||
currentQuery: string;
|
currentQuery: string;
|
||||||
onExplain: (explanation: string) => void;
|
onExplain: (explanation: string) => void;
|
||||||
refIds: string[];
|
refIds: string[];
|
||||||
schemas?: unknown; // Reserved for future schema implementation
|
schemas?: SQLSchemasResponse | null;
|
||||||
queryContext?: QueryUsageContext;
|
queryContext?: QueryUsageContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,20 +35,20 @@ Explain what this query does in simple terms.`;
|
|||||||
*
|
*
|
||||||
* @param refIds - The list of RefIDs available in the current context
|
* @param refIds - The list of RefIDs available in the current context
|
||||||
* @param currentQuery - The current SQL query to explain
|
* @param currentQuery - The current SQL query to explain
|
||||||
* @param schemas - Optional schema information (planned for future implementation)
|
* @param formattedSchemas - Pre-formatted schema information string
|
||||||
* @param queryContext - Optional query usage context
|
* @param queryContext - Optional query usage context
|
||||||
* @returns A list of messages to be sent to the LLM for explaining the SQL query
|
* @returns A list of messages to be sent to the LLM for explaining the SQL query
|
||||||
*/
|
*/
|
||||||
const getSQLExplanationMessages = (
|
const getSQLExplanationMessages = (
|
||||||
refIds: string[],
|
refIds: string[],
|
||||||
currentQuery: string,
|
currentQuery: string,
|
||||||
schemas?: unknown,
|
formattedSchemas?: string,
|
||||||
queryContext?: QueryUsageContext
|
queryContext?: QueryUsageContext
|
||||||
): Message[] => {
|
): Message[] => {
|
||||||
const systemPrompt = getSQLExplanationSystemPrompt({
|
const systemPrompt = getSQLExplanationSystemPrompt({
|
||||||
refIds: refIds.length > 0 ? refIds.join(', ') : 'A',
|
refIds: refIds.length > 0 ? refIds.join(', ') : 'A',
|
||||||
currentQuery: currentQuery.trim() || 'No current query provided',
|
currentQuery: currentQuery.trim() || 'No current query provided',
|
||||||
schemas, // Will be utilized once schema extraction is implemented
|
formattedSchemas,
|
||||||
queryContext,
|
queryContext,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -69,11 +71,13 @@ export const GenAISQLExplainButton = ({
|
|||||||
onExplain,
|
onExplain,
|
||||||
queryContext,
|
queryContext,
|
||||||
refIds,
|
refIds,
|
||||||
schemas, // Future implementation will use this for enhanced context
|
schemas,
|
||||||
}: GenAISQLExplainButtonProps) => {
|
}: GenAISQLExplainButtonProps) => {
|
||||||
|
const formattedSchemas = useMemo(() => formatSchemasForPrompt(schemas), [schemas]);
|
||||||
|
|
||||||
const messages = useCallback(() => {
|
const messages = useCallback(() => {
|
||||||
return getSQLExplanationMessages(refIds, currentQuery, schemas, queryContext);
|
return getSQLExplanationMessages(refIds, currentQuery, formattedSchemas, queryContext);
|
||||||
}, [refIds, currentQuery, schemas, queryContext]);
|
}, [refIds, currentQuery, formattedSchemas, queryContext]);
|
||||||
|
|
||||||
const hasQuery = currentQuery && currentQuery.trim() !== '';
|
const hasQuery = currentQuery && currentQuery.trim() !== '';
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
import { t } from '@grafana/i18n';
|
import { t } from '@grafana/i18n';
|
||||||
|
|
||||||
import { GenAIButton } from '../../../../dashboard/components/GenAI/GenAIButton';
|
import { GenAIButton } from '../../../../dashboard/components/GenAI/GenAIButton';
|
||||||
import { EventTrackingSrc } from '../../../../dashboard/components/GenAI/tracking';
|
import { EventTrackingSrc } from '../../../../dashboard/components/GenAI/tracking';
|
||||||
import { Message, Role } from '../../../../dashboard/components/GenAI/utils';
|
import { Message, Role } from '../../../../dashboard/components/GenAI/utils';
|
||||||
|
import { SQLSchemasResponse } from '../hooks/useSQLSchemas';
|
||||||
|
|
||||||
import { getSQLSuggestionSystemPrompt, QueryUsageContext } from './sqlPromptConfig';
|
import { getSQLSuggestionSystemPrompt, QueryUsageContext } from './sqlPromptConfig';
|
||||||
|
import { formatSchemasForPrompt } from './utils/formatSchemas';
|
||||||
|
|
||||||
interface GenAISQLSuggestionsButtonProps {
|
interface GenAISQLSuggestionsButtonProps {
|
||||||
currentQuery: string;
|
currentQuery: string;
|
||||||
@@ -14,7 +16,7 @@ interface GenAISQLSuggestionsButtonProps {
|
|||||||
onHistoryUpdate?: (history: string[]) => void;
|
onHistoryUpdate?: (history: string[]) => void;
|
||||||
refIds: string[];
|
refIds: string[];
|
||||||
initialQuery: string;
|
initialQuery: string;
|
||||||
schemas?: unknown; // Reserved for future schema implementation
|
schemas?: SQLSchemasResponse | null;
|
||||||
errorContext?: string[];
|
errorContext?: string[];
|
||||||
queryContext?: QueryUsageContext;
|
queryContext?: QueryUsageContext;
|
||||||
}
|
}
|
||||||
@@ -41,15 +43,15 @@ const getContextualPrompts = (refIds: string[], currentQuery: string): string[]
|
|||||||
*
|
*
|
||||||
* @param refIds - The list of RefIDs available in the current context
|
* @param refIds - The list of RefIDs available in the current context
|
||||||
* @param currentQuery - The current SQL query being edited
|
* @param currentQuery - The current SQL query being edited
|
||||||
* @param schemas - Optional schema information (planned for future implementation)
|
* @param formattedSchemas - Pre-formatted schema information string
|
||||||
* @param errorContext - Optional error context for targeted fixes (planned for future implementation)
|
* @param errorContext - Optional error context for targeted fixes
|
||||||
* @param queryContext - Optional query usage context
|
* @param queryContext - Optional query usage context
|
||||||
* @returns A list of messages to be sent to the LLM for generating SQL suggestions
|
* @returns A list of messages to be sent to the LLM for generating SQL suggestions
|
||||||
*/
|
*/
|
||||||
const getSQLSuggestionMessages = (
|
const getSQLSuggestionMessages = (
|
||||||
refIds: string[],
|
refIds: string[],
|
||||||
currentQuery: string,
|
currentQuery: string,
|
||||||
schemas?: unknown,
|
formattedSchemas?: string,
|
||||||
errorContext?: string[],
|
errorContext?: string[],
|
||||||
queryContext?: QueryUsageContext
|
queryContext?: QueryUsageContext
|
||||||
): Message[] => {
|
): Message[] => {
|
||||||
@@ -62,7 +64,7 @@ const getSQLSuggestionMessages = (
|
|||||||
refIds: refIds.length > 0 ? refIds.join(', ') : 'A',
|
refIds: refIds.length > 0 ? refIds.join(', ') : 'A',
|
||||||
currentQuery: trimmedQuery || 'No current query provided',
|
currentQuery: trimmedQuery || 'No current query provided',
|
||||||
queryInstruction: queryInstruction,
|
queryInstruction: queryInstruction,
|
||||||
schemas, // Will be utilized once schema extraction is implemented
|
formattedSchemas,
|
||||||
errorContext,
|
errorContext,
|
||||||
queryContext,
|
queryContext,
|
||||||
});
|
});
|
||||||
@@ -88,13 +90,15 @@ export const GenAISQLSuggestionsButton = ({
|
|||||||
onHistoryUpdate,
|
onHistoryUpdate,
|
||||||
refIds,
|
refIds,
|
||||||
initialQuery,
|
initialQuery,
|
||||||
schemas, // Future implementation will use this for enhanced context
|
schemas,
|
||||||
errorContext,
|
errorContext,
|
||||||
queryContext,
|
queryContext,
|
||||||
}: GenAISQLSuggestionsButtonProps) => {
|
}: GenAISQLSuggestionsButtonProps) => {
|
||||||
|
const formattedSchemas = useMemo(() => formatSchemasForPrompt(schemas), [schemas]);
|
||||||
|
|
||||||
const messages = useCallback(() => {
|
const messages = useCallback(() => {
|
||||||
return getSQLSuggestionMessages(refIds, currentQuery, schemas, errorContext, queryContext);
|
return getSQLSuggestionMessages(refIds, currentQuery, formattedSchemas, errorContext, queryContext);
|
||||||
}, [refIds, currentQuery, schemas, errorContext, queryContext]);
|
}, [refIds, currentQuery, formattedSchemas, errorContext, queryContext]);
|
||||||
|
|
||||||
const text = !currentQuery || currentQuery === initialQuery ? 'Generate suggestion' : 'Improve query';
|
const text = !currentQuery || currentQuery === initialQuery ? 'Generate suggestion' : 'Improve query';
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { getSQLSuggestionSystemPrompt, QueryUsageContext } from './sqlPromptConfig';
|
||||||
|
|
||||||
|
describe('getSQLSuggestionSystemPrompt', () => {
|
||||||
|
it('includes all context fields in generated prompt', () => {
|
||||||
|
const queryContext: QueryUsageContext = {
|
||||||
|
panelId: 'timeseries',
|
||||||
|
alerting: false,
|
||||||
|
dashboardContext: {
|
||||||
|
dashboardTitle: 'Production Metrics',
|
||||||
|
panelName: 'CPU Usage',
|
||||||
|
},
|
||||||
|
datasources: ['prometheus', 'postgres'],
|
||||||
|
totalRows: 5000,
|
||||||
|
requestTime: 250,
|
||||||
|
numberOfQueries: 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getSQLSuggestionSystemPrompt({
|
||||||
|
refIds: 'A, B',
|
||||||
|
currentQuery: 'SELECT * FROM A',
|
||||||
|
queryInstruction: 'Focus on fixing the current query',
|
||||||
|
formattedSchemas: 'RefID A:\n - time (TIMESTAMP, not null)\n - value (FLOAT, nullable)',
|
||||||
|
errorContext: ['Syntax error near WHERE'],
|
||||||
|
queryContext,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toContain('A, B');
|
||||||
|
expect(result).toContain('SELECT * FROM A');
|
||||||
|
expect(result).toContain('Focus on fixing the current query');
|
||||||
|
expect(result).toContain('time (TIMESTAMP, not null)');
|
||||||
|
expect(result).toContain('Syntax error near WHERE');
|
||||||
|
expect(result).toContain('Panel Type: timeseries');
|
||||||
|
expect(result).toContain('Dashboard: Production Metrics, Panel: CPU Usage');
|
||||||
|
expect(result).toContain('Datasources: prometheus, postgres');
|
||||||
|
expect(result).toContain('Total rows in the query: 5000');
|
||||||
|
expect(result).toContain('Request time: 250');
|
||||||
|
expect(result).toContain('Number of queries: 3');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('formats multiple errors with newlines', () => {
|
||||||
|
const result = getSQLSuggestionSystemPrompt({
|
||||||
|
refIds: 'A',
|
||||||
|
currentQuery: 'SELECT * FROM A',
|
||||||
|
queryInstruction: 'Fix errors',
|
||||||
|
errorContext: ['Error 1: Syntax error', 'Error 2: Column not found'],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toContain('Error 1: Syntax error');
|
||||||
|
expect(result).toContain('Error 2: Column not found');
|
||||||
|
expect(result).toContain('Error 1: Syntax error\nError 2: Column not found');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,10 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Configuration file for SQL AI prompts used across expression components
|
* Configuration file for SQL AI prompts used across expression components
|
||||||
* NOTE: Schema and error context information integration is planned for future implementation
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { DataQuery } from '@grafana/schema';
|
|
||||||
|
|
||||||
// Common SQL context information shared across all prompts
|
// Common SQL context information shared across all prompts
|
||||||
const COMMON_SQL_CONTEXT = {
|
const COMMON_SQL_CONTEXT = {
|
||||||
engineInfo: 'MySQL dialectic based on dolthub go-mysql-server. The tables are all in memory',
|
engineInfo: 'MySQL dialectic based on dolthub go-mysql-server. The tables are all in memory',
|
||||||
@@ -17,15 +14,14 @@ const TEMPLATE_PLACEHOLDERS = {
|
|||||||
refIds: '{refIds}',
|
refIds: '{refIds}',
|
||||||
currentQuery: '{currentQuery}',
|
currentQuery: '{currentQuery}',
|
||||||
queryInstruction: '{queryInstruction}',
|
queryInstruction: '{queryInstruction}',
|
||||||
schemaInfo: '{schemaInfo}', // Note: Schema information will be implemented in future updates
|
schemaInfo: '{schemaInfo}',
|
||||||
errorContext: '{errorContext}', // Note: Error context will be implemented in future updates
|
errorContext: '{errorContext}',
|
||||||
queryContext: '{queryContext}',
|
queryContext: '{queryContext}',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export interface QueryUsageContext {
|
export interface QueryUsageContext {
|
||||||
panelId?: string;
|
panelId?: string;
|
||||||
alerting?: boolean;
|
alerting?: boolean;
|
||||||
queries?: DataQuery[];
|
|
||||||
dashboardContext?: {
|
dashboardContext?: {
|
||||||
dashboardTitle?: string;
|
dashboardTitle?: string;
|
||||||
panelName?: string;
|
panelName?: string;
|
||||||
@@ -34,7 +30,6 @@ export interface QueryUsageContext {
|
|||||||
totalRows?: number;
|
totalRows?: number;
|
||||||
requestTime?: number;
|
requestTime?: number;
|
||||||
numberOfQueries?: number;
|
numberOfQueries?: number;
|
||||||
seriesData?: unknown;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -58,8 +53,6 @@ ${TEMPLATE_PLACEHOLDERS.queryContext}
|
|||||||
|
|
||||||
Query instruction: ${TEMPLATE_PLACEHOLDERS.queryInstruction}
|
Query instruction: ${TEMPLATE_PLACEHOLDERS.queryInstruction}
|
||||||
|
|
||||||
You may be able to derive schema information from the series data in queryContext.
|
|
||||||
|
|
||||||
Given the above data, help users with their SQL query by:
|
Given the above data, help users with their SQL query by:
|
||||||
- **PRIORITY: If there are errors listed above, focus on fixing them first**
|
- **PRIORITY: If there are errors listed above, focus on fixing them first**
|
||||||
- Fixing syntax errors using available field and data type information
|
- Fixing syntax errors using available field and data type information
|
||||||
@@ -96,8 +89,7 @@ Explain SQL queries clearly and concisely, focusing on:
|
|||||||
- What data is being selected and from which RefIDs
|
- What data is being selected and from which RefIDs
|
||||||
- How the data is being transformed or aggregated
|
- How the data is being transformed or aggregated
|
||||||
- The purpose and business meaning of the query using dashboard and panel name from query context if relevant
|
- The purpose and business meaning of the query using dashboard and panel name from query context if relevant
|
||||||
- Performance implications and optimization opportunities. Database columns can not be indexed in context of Grafana sql expressions. Don't focus on
|
- Performance implications and optimization opportunities. Database columns cannot be indexed in context of Grafana sql expressions. Don't focus on performance unless the query context has a requestTime or totalRows that looks like it could benefit from it.
|
||||||
performance unless the query context has a requestTime or totalRows that looks like it could benefit from it.
|
|
||||||
- Time series specific patterns and their significance
|
- Time series specific patterns and their significance
|
||||||
|
|
||||||
Provide a clear explanation of what this SQL query does:`;
|
Provide a clear explanation of what this SQL query does:`;
|
||||||
@@ -112,33 +104,19 @@ const generateQueryContext = (queryContext?: QueryUsageContext): string => {
|
|||||||
|
|
||||||
const contextParts = [];
|
const contextParts = [];
|
||||||
if (queryContext.panelId) {
|
if (queryContext.panelId) {
|
||||||
contextParts.push(
|
contextParts.push(`Panel Type: ${queryContext.panelId}`);
|
||||||
`Panel Type: ${queryContext.panelId}. Please use this to generate suggestions that are relevant to the panel type.`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (queryContext.alerting) {
|
if (queryContext.alerting) {
|
||||||
contextParts.push(
|
contextParts.push('Context: Alerting rule (focus on boolean/threshold results)');
|
||||||
'Context: Alerting rule (focus on boolean/threshold results). Please use this to generate suggestions that are relevant to the alerting rule.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (queryContext.queries) {
|
|
||||||
const queriesText = Array.isArray(queryContext.queries)
|
|
||||||
? JSON.stringify(queryContext.queries, null, 2)
|
|
||||||
: String(queryContext.queries);
|
|
||||||
contextParts.push(`Queries available to use in the SQL Expression: ${queriesText}`);
|
|
||||||
}
|
}
|
||||||
if (queryContext.dashboardContext) {
|
if (queryContext.dashboardContext) {
|
||||||
const dashboardText =
|
const { dashboardTitle, panelName } = queryContext.dashboardContext;
|
||||||
typeof queryContext.dashboardContext === 'object'
|
if (dashboardTitle || panelName) {
|
||||||
? JSON.stringify(queryContext.dashboardContext, null, 2)
|
contextParts.push(`Dashboard: ${dashboardTitle || 'Unknown'}, Panel: ${panelName || 'Unknown'}`);
|
||||||
: String(queryContext.dashboardContext);
|
|
||||||
contextParts.push(`Dashboard context (dashboard title and panel name): ${dashboardText}`);
|
|
||||||
}
|
}
|
||||||
if (queryContext.datasources) {
|
}
|
||||||
const datasourcesText = Array.isArray(queryContext.datasources)
|
if (queryContext.datasources && queryContext.datasources.length > 0) {
|
||||||
? JSON.stringify(queryContext.datasources, null, 2)
|
contextParts.push(`Datasources: ${queryContext.datasources.join(', ')}`);
|
||||||
: String(queryContext.datasources);
|
|
||||||
contextParts.push(`Datasources available to use in the SQL Expression: ${datasourcesText}`);
|
|
||||||
}
|
}
|
||||||
if (queryContext.totalRows) {
|
if (queryContext.totalRows) {
|
||||||
contextParts.push(`Total rows in the query: ${queryContext.totalRows}`);
|
contextParts.push(`Total rows in the query: ${queryContext.totalRows}`);
|
||||||
@@ -149,13 +127,6 @@ const generateQueryContext = (queryContext?: QueryUsageContext): string => {
|
|||||||
if (queryContext.numberOfQueries) {
|
if (queryContext.numberOfQueries) {
|
||||||
contextParts.push(`Number of queries: ${queryContext.numberOfQueries}`);
|
contextParts.push(`Number of queries: ${queryContext.numberOfQueries}`);
|
||||||
}
|
}
|
||||||
if (queryContext.seriesData) {
|
|
||||||
const seriesDataText =
|
|
||||||
typeof queryContext.seriesData === 'object'
|
|
||||||
? JSON.stringify(queryContext.seriesData, null, 2)
|
|
||||||
: String(queryContext.seriesData);
|
|
||||||
contextParts.push(`Series data: ${seriesDataText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return contextParts.length
|
return contextParts.length
|
||||||
? `Query Context:
|
? `Query Context:
|
||||||
@@ -170,19 +141,17 @@ export interface SQLPromptVariables {
|
|||||||
refIds: string;
|
refIds: string;
|
||||||
currentQuery: string;
|
currentQuery: string;
|
||||||
queryInstruction: string;
|
queryInstruction: string;
|
||||||
schemas?: unknown; // Reserved for future schema implementation
|
formattedSchemas?: string;
|
||||||
errorContext?: string[];
|
errorContext?: string[];
|
||||||
queryContext?: QueryUsageContext;
|
queryContext?: QueryUsageContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate the complete system prompt for SQL suggestions with enhanced context
|
* Generate the complete system prompt for SQL suggestions with enhanced context
|
||||||
*
|
|
||||||
* Note: Schema information integration is planned for future implementation
|
|
||||||
*/
|
*/
|
||||||
export const getSQLSuggestionSystemPrompt = (variables: SQLPromptVariables): string => {
|
export const getSQLSuggestionSystemPrompt = (variables: SQLPromptVariables): string => {
|
||||||
const queryContext = generateQueryContext(variables.queryContext);
|
const queryContext = generateQueryContext(variables.queryContext);
|
||||||
const schemaInfo = ''; // Placeholder for future schema information
|
const schemaInfo = variables.formattedSchemas ?? 'No schema information available.';
|
||||||
const errorContext = variables.errorContext?.length
|
const errorContext = variables.errorContext?.length
|
||||||
? variables.errorContext.join('\n')
|
? variables.errorContext.join('\n')
|
||||||
: 'No current errors detected.';
|
: 'No current errors detected.';
|
||||||
@@ -200,8 +169,7 @@ export const getSQLSuggestionSystemPrompt = (variables: SQLPromptVariables): str
|
|||||||
*/
|
*/
|
||||||
export const getSQLExplanationSystemPrompt = (variables: Omit<SQLPromptVariables, 'queryInstruction'>): string => {
|
export const getSQLExplanationSystemPrompt = (variables: Omit<SQLPromptVariables, 'queryInstruction'>): string => {
|
||||||
const queryContext = generateQueryContext(variables.queryContext);
|
const queryContext = generateQueryContext(variables.queryContext);
|
||||||
|
const schemaInfo = variables.formattedSchemas ?? 'No schema information available.';
|
||||||
const schemaInfo = ''; // Placeholder for future schema information
|
|
||||||
|
|
||||||
return SQL_EXPLANATION_SYSTEM_PROMPT.replaceAll(TEMPLATE_PLACEHOLDERS.refIds, variables.refIds)
|
return SQL_EXPLANATION_SYSTEM_PROMPT.replaceAll(TEMPLATE_PLACEHOLDERS.refIds, variables.refIds)
|
||||||
.replaceAll(TEMPLATE_PLACEHOLDERS.schemaInfo, schemaInfo)
|
.replaceAll(TEMPLATE_PLACEHOLDERS.schemaInfo, schemaInfo)
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import { SQLSchemasResponse } from '../../hooks/useSQLSchemas';
|
||||||
|
|
||||||
|
import { formatSchemasForPrompt } from './formatSchemas';
|
||||||
|
|
||||||
|
describe('formatSchemasForPrompt', () => {
|
||||||
|
const createSchemaResponse = (sqlSchemas: SQLSchemasResponse['sqlSchemas']): SQLSchemasResponse => ({
|
||||||
|
kind: 'SQLSchemaResponse',
|
||||||
|
apiVersion: 'query.grafana.app/v0alpha1',
|
||||||
|
sqlSchemas,
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns fallback message for null/undefined/empty schemas', () => {
|
||||||
|
expect(formatSchemasForPrompt(null)).toBe('No schema information available.');
|
||||||
|
expect(formatSchemasForPrompt(undefined)).toBe('No schema information available.');
|
||||||
|
expect(formatSchemasForPrompt(createSchemaResponse({}))).toBe('No schema information available.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('includes schema errors instead of silently failing', () => {
|
||||||
|
const schemas = createSchemaResponse({
|
||||||
|
A: {
|
||||||
|
columns: null,
|
||||||
|
sampleRows: null,
|
||||||
|
error: 'Connection timeout',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = formatSchemasForPrompt(schemas);
|
||||||
|
|
||||||
|
expect(result).toBe('RefID A: Error - Connection timeout');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('truncates columns beyond default limit to prevent token overflow', () => {
|
||||||
|
const maxColumns = 10; // Default limit
|
||||||
|
const totalColumns = 15;
|
||||||
|
const columns = Array.from({ length: totalColumns }, (_, i) => ({
|
||||||
|
name: `column_${i + 1}`,
|
||||||
|
mysqlType: 'VARCHAR',
|
||||||
|
dataFrameFieldType: 'string',
|
||||||
|
nullable: false,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const schemas = createSchemaResponse({
|
||||||
|
A: {
|
||||||
|
columns,
|
||||||
|
sampleRows: null,
|
||||||
|
error: undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = formatSchemasForPrompt(schemas);
|
||||||
|
|
||||||
|
// Should include columns up to limit
|
||||||
|
expect(result).toContain('column_1 (VARCHAR, not null)');
|
||||||
|
expect(result).toContain(`column_${maxColumns} (VARCHAR, not null)`);
|
||||||
|
|
||||||
|
// Should truncate columns beyond limit
|
||||||
|
expect(result).not.toContain(`column_${maxColumns + 1}`);
|
||||||
|
expect(result).toContain(`... and ${totalColumns - maxColumns} more columns`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('formats multiple RefIDs with proper separation', () => {
|
||||||
|
const schemas = createSchemaResponse({
|
||||||
|
A: {
|
||||||
|
columns: [{ name: 'time', mysqlType: 'TIMESTAMP', dataFrameFieldType: 'time', nullable: false }],
|
||||||
|
sampleRows: [[1234567890]],
|
||||||
|
error: undefined,
|
||||||
|
},
|
||||||
|
B: {
|
||||||
|
columns: [{ name: 'value', mysqlType: 'FLOAT', dataFrameFieldType: 'number', nullable: true }],
|
||||||
|
sampleRows: [[42.5]],
|
||||||
|
error: undefined,
|
||||||
|
},
|
||||||
|
C: {
|
||||||
|
columns: [{ name: 'label', mysqlType: 'VARCHAR', dataFrameFieldType: 'string', nullable: false }],
|
||||||
|
sampleRows: [['production']],
|
||||||
|
error: undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = formatSchemasForPrompt(schemas);
|
||||||
|
|
||||||
|
expect(result).toContain('RefID A:');
|
||||||
|
expect(result).toContain('time (TIMESTAMP, not null)');
|
||||||
|
expect(result).toContain('RefID B:');
|
||||||
|
expect(result).toContain('value (FLOAT, nullable)');
|
||||||
|
expect(result).toContain('RefID C:');
|
||||||
|
expect(result).toContain('label (VARCHAR, not null)');
|
||||||
|
expect(result.split('\n\n').length).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { SQLSchemasResponse, SQLSchemaField } from '../../hooks/useSQLSchemas';
|
||||||
|
|
||||||
|
const DEFAULT_MAX_COLUMNS = 10;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format schemas into a readable string for LLM context with token budget management
|
||||||
|
*
|
||||||
|
* This function converts SQL schema information into a human-readable format suitable
|
||||||
|
* for LLM prompts. It includes column names, types, nullability, and sample data.
|
||||||
|
*
|
||||||
|
* Token budget management:
|
||||||
|
* - Limits columns per RefID to prevent excessive token usage
|
||||||
|
* - Respects original column order from schema
|
||||||
|
* - Provides summary for truncated columns
|
||||||
|
*
|
||||||
|
* @param schemas - The SQL schemas response from the API
|
||||||
|
* @param maxColumnsPerRefId - Maximum number of columns to include per RefID (default: 10)
|
||||||
|
* @returns A formatted string representation of the schemas
|
||||||
|
*/
|
||||||
|
export const formatSchemasForPrompt = (
|
||||||
|
schemas?: SQLSchemasResponse | null,
|
||||||
|
maxColumnsPerRefId = DEFAULT_MAX_COLUMNS
|
||||||
|
): string => {
|
||||||
|
if (!schemas?.sqlSchemas || Object.keys(schemas.sqlSchemas).length === 0) {
|
||||||
|
return 'No schema information available.';
|
||||||
|
}
|
||||||
|
|
||||||
|
const schemaParts: string[] = [];
|
||||||
|
|
||||||
|
for (const [refId, schemaData] of Object.entries(schemas.sqlSchemas)) {
|
||||||
|
if (schemaData.error) {
|
||||||
|
schemaParts.push(`RefID ${refId}: Error - ${schemaData.error}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!schemaData.columns || schemaData.columns.length === 0) {
|
||||||
|
schemaParts.push(`RefID ${refId}: No columns available`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const columnsToShow = schemaData.columns.slice(0, maxColumnsPerRefId);
|
||||||
|
const remainingCount = schemaData.columns.length - columnsToShow.length;
|
||||||
|
|
||||||
|
const columnDescriptions = columnsToShow.map(({ nullable, name, mysqlType }: SQLSchemaField) => {
|
||||||
|
const isNullable = nullable ? 'nullable' : 'not null';
|
||||||
|
return ` - ${name} (${mysqlType}, ${isNullable})`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add truncation notice if we hit the limit
|
||||||
|
if (remainingCount > 0) {
|
||||||
|
columnDescriptions.push(` ... and ${remainingCount} more column${remainingCount > 1 ? 's' : ''}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build schema text parts
|
||||||
|
const textParts = [`RefID ${refId}:`, columnDescriptions.join('\n')];
|
||||||
|
|
||||||
|
// Add sample data if available (first row only)
|
||||||
|
if (schemaData.sampleRows && schemaData.sampleRows.length > 0) {
|
||||||
|
const sampleRow = schemaData.sampleRows[0];
|
||||||
|
const sampleValues = columnsToShow
|
||||||
|
.map(({ name }: SQLSchemaField, idx: number) => `${name}=${JSON.stringify(sampleRow[idx])}`)
|
||||||
|
.join(', ');
|
||||||
|
textParts.push(` Sample: ${sampleValues}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
schemaParts.push(textParts.join('\n'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return schemaParts.join('\n\n');
|
||||||
|
};
|
||||||
@@ -121,7 +121,6 @@ LIMIT
|
|||||||
refetch: refetchSchemas,
|
refetch: refetchSchemas,
|
||||||
} = useSQLSchemas({
|
} = useSQLSchemas({
|
||||||
queries,
|
queries,
|
||||||
enabled: isSchemaInspectorOpen,
|
|
||||||
timeRange: metadata?.range,
|
timeRange: metadata?.range,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -129,7 +128,6 @@ LIMIT
|
|||||||
() => ({
|
() => ({
|
||||||
alerting,
|
alerting,
|
||||||
panelId: metadata?.data?.request?.panelPluginId,
|
panelId: metadata?.data?.request?.panelPluginId,
|
||||||
queries: metadata?.queries,
|
|
||||||
dashboardContext: {
|
dashboardContext: {
|
||||||
dashboardTitle: metadata?.data?.request?.dashboardTitle ?? '',
|
dashboardTitle: metadata?.data?.request?.dashboardTitle ?? '',
|
||||||
panelName: metadata?.data?.request?.panelName ?? '',
|
panelName: metadata?.data?.request?.panelName ?? '',
|
||||||
@@ -140,7 +138,6 @@ LIMIT
|
|||||||
? metadata?.data?.request?.endTime - metadata?.data?.request?.startTime
|
? metadata?.data?.request?.endTime - metadata?.data?.request?.startTime
|
||||||
: -1,
|
: -1,
|
||||||
numberOfQueries: metadata?.data?.request?.targets?.length ?? 0,
|
numberOfQueries: metadata?.data?.request?.targets?.length ?? 0,
|
||||||
seriesData: metadata?.data?.series,
|
|
||||||
}),
|
}),
|
||||||
[alerting, metadata]
|
[alerting, metadata]
|
||||||
);
|
);
|
||||||
@@ -276,7 +273,7 @@ LIMIT
|
|||||||
onExplain={handleExplain}
|
onExplain={handleExplain}
|
||||||
queryContext={queryContext}
|
queryContext={queryContext}
|
||||||
refIds={vars}
|
refIds={vars}
|
||||||
// schemas={schemas} // Will be added when schema extraction is implemented
|
schemas={schemas}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Suspense>
|
</Suspense>
|
||||||
@@ -288,8 +285,8 @@ LIMIT
|
|||||||
onHistoryUpdate={handleHistoryUpdate}
|
onHistoryUpdate={handleHistoryUpdate}
|
||||||
queryContext={queryContext}
|
queryContext={queryContext}
|
||||||
refIds={vars}
|
refIds={vars}
|
||||||
errorContext={errorContext} // Will be added when error tracking is implemented
|
errorContext={errorContext}
|
||||||
// schemas={schemas} // Will be added when schema extraction is implemented
|
schemas={schemas}
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
import { renderHook, waitFor } from '@testing-library/react';
|
||||||
|
import { testWithFeatureToggles } from 'test/test-utils';
|
||||||
|
|
||||||
|
import { useSQLSchemas, SQLSchemasResponse } from './useSQLSchemas';
|
||||||
|
|
||||||
|
jest.mock('@grafana/runtime', () => ({
|
||||||
|
...jest.requireActual('@grafana/runtime'),
|
||||||
|
getBackendSrv: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('@grafana/api-clients', () => ({
|
||||||
|
getAPINamespace: jest.fn(() => 'default'),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('@grafana/data', () => ({
|
||||||
|
...jest.requireActual('@grafana/data'),
|
||||||
|
getDefaultTimeRange: jest.fn(() => ({
|
||||||
|
from: { toISOString: () => '2024-01-01T00:00:00.000Z' },
|
||||||
|
to: { toISOString: () => '2024-01-01T23:59:59.999Z' },
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('useSQLSchemas', () => {
|
||||||
|
testWithFeatureToggles({ enable: ['queryService'] });
|
||||||
|
|
||||||
|
beforeEach(() => jest.clearAllMocks());
|
||||||
|
|
||||||
|
const mockQueries = [
|
||||||
|
{
|
||||||
|
refId: 'A',
|
||||||
|
datasource: { type: 'prometheus', uid: 'prom-uid' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const mockSchemasResponse: SQLSchemasResponse = {
|
||||||
|
kind: 'SQLSchemaResponse',
|
||||||
|
apiVersion: 'query.grafana.app/v0alpha1',
|
||||||
|
sqlSchemas: {
|
||||||
|
A: {
|
||||||
|
columns: [
|
||||||
|
{ name: 'time', mysqlType: 'TIMESTAMP', dataFrameFieldType: 'time', nullable: false },
|
||||||
|
{ name: 'value', mysqlType: 'FLOAT', dataFrameFieldType: 'number', nullable: true },
|
||||||
|
],
|
||||||
|
sampleRows: [[1234567890, 42.5]],
|
||||||
|
error: undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const setupMockBackendSrv = (mockImplementation: jest.Mock) => {
|
||||||
|
const { getBackendSrv } = require('@grafana/runtime');
|
||||||
|
getBackendSrv.mockReturnValue({ post: mockImplementation });
|
||||||
|
return mockImplementation;
|
||||||
|
};
|
||||||
|
|
||||||
|
it('fetches schemas successfully and updates state', async () => {
|
||||||
|
// Arrange
|
||||||
|
const mockPost = setupMockBackendSrv(jest.fn().mockResolvedValue(mockSchemasResponse));
|
||||||
|
|
||||||
|
// Act - Render the hook
|
||||||
|
const { result } = renderHook(() => useSQLSchemas({ queries: mockQueries }));
|
||||||
|
|
||||||
|
// Assert - Initial state
|
||||||
|
expect(result.current.loading).toBe(true);
|
||||||
|
expect(result.current.schemas).toBe(null);
|
||||||
|
expect(result.current.error).toBe(null);
|
||||||
|
|
||||||
|
// Wait for async fetch to complete
|
||||||
|
await waitFor(() => expect(result.current.loading).toBe(false));
|
||||||
|
|
||||||
|
// Assert - Final state
|
||||||
|
expect(result.current.schemas).toEqual(mockSchemasResponse);
|
||||||
|
expect(result.current.schemas?.sqlSchemas.A.columns).toHaveLength(2);
|
||||||
|
expect(result.current.schemas?.sqlSchemas.A.columns?.[0].name).toBe('time');
|
||||||
|
expect(result.current.error).toBe(null);
|
||||||
|
|
||||||
|
// Verify API was called correctly
|
||||||
|
expect(mockPost).toHaveBeenCalledWith(
|
||||||
|
'/apis/query.grafana.app/v0alpha1/namespaces/default/sqlschemas/name',
|
||||||
|
expect.objectContaining({
|
||||||
|
queries: mockQueries,
|
||||||
|
from: '2024-01-01T00:00:00.000Z',
|
||||||
|
to: '2024-01-01T23:59:59.999Z',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles API errors gracefully without crashing', async () => {
|
||||||
|
// Arrange
|
||||||
|
const mockError = new Error('Network error');
|
||||||
|
setupMockBackendSrv(jest.fn().mockRejectedValue(mockError));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const { result } = renderHook(() => useSQLSchemas({ queries: mockQueries }));
|
||||||
|
|
||||||
|
// Wait for error to be set
|
||||||
|
await waitFor(() => expect(result.current.loading).toBe(false));
|
||||||
|
|
||||||
|
// Assert - Error state is set, no crash
|
||||||
|
expect(result.current.error).toEqual(mockError);
|
||||||
|
expect(result.current.schemas).toBe(null);
|
||||||
|
expect(result.current.loading).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when feature flag is disabled', () => {
|
||||||
|
testWithFeatureToggles({ disable: ['queryService', 'grafanaAPIServerWithExperimentalAPIs'] });
|
||||||
|
|
||||||
|
it('does not fetch schemas', async () => {
|
||||||
|
// Arrange
|
||||||
|
setupMockBackendSrv(jest.fn());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const { result } = renderHook(() => useSQLSchemas({ queries: mockQueries }));
|
||||||
|
|
||||||
|
// Assert - No API call, feature disabled
|
||||||
|
expect(result.current.isFeatureEnabled).toBe(false);
|
||||||
|
expect(result.current.loading).toBe(false);
|
||||||
|
expect(result.current.schemas).toBe(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty schemas for empty queries array without calling API', async () => {
|
||||||
|
// Arrange
|
||||||
|
const mockPost = setupMockBackendSrv(jest.fn());
|
||||||
|
|
||||||
|
// Act - Empty queries array
|
||||||
|
const { result } = renderHook(() => useSQLSchemas({ queries: [] }));
|
||||||
|
|
||||||
|
// Wait for state to settle
|
||||||
|
await waitFor(() => expect(result.current.loading).toBe(false));
|
||||||
|
|
||||||
|
// Assert - Returns empty schemas, no API call
|
||||||
|
expect(result.current.schemas).toEqual({
|
||||||
|
kind: 'SQLSchemaResponse',
|
||||||
|
apiVersion: 'query.grafana.app/v0alpha1',
|
||||||
|
sqlSchemas: {},
|
||||||
|
});
|
||||||
|
expect(result.current.error).toBe(null);
|
||||||
|
expect(mockPost).not.toHaveBeenCalled(); // No API call for empty queries
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -28,11 +28,10 @@ export interface SQLSchemasResponse {
|
|||||||
|
|
||||||
interface UseSQLSchemasOptions {
|
interface UseSQLSchemasOptions {
|
||||||
queries?: DataQuery[];
|
queries?: DataQuery[];
|
||||||
enabled: boolean;
|
|
||||||
timeRange?: TimeRange;
|
timeRange?: TimeRange;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useSQLSchemas({ queries, enabled, timeRange }: UseSQLSchemasOptions) {
|
export function useSQLSchemas({ queries, timeRange }: UseSQLSchemasOptions) {
|
||||||
const isFeatureEnabled = useMemo(
|
const isFeatureEnabled = useMemo(
|
||||||
() => config.featureToggles.queryService || config.featureToggles.grafanaAPIServerWithExperimentalAPIs || false,
|
() => config.featureToggles.queryService || config.featureToggles.grafanaAPIServerWithExperimentalAPIs || false,
|
||||||
[]
|
[]
|
||||||
@@ -40,7 +39,7 @@ export function useSQLSchemas({ queries, enabled, timeRange }: UseSQLSchemasOpti
|
|||||||
|
|
||||||
// Start with loading=true if we're going to fetch on mount
|
// Start with loading=true if we're going to fetch on mount
|
||||||
const [schemas, setSchemas] = useState<SQLSchemasResponse | null>(null);
|
const [schemas, setSchemas] = useState<SQLSchemasResponse | null>(null);
|
||||||
const [loading, setLoading] = useState(enabled && isFeatureEnabled && Boolean(queries));
|
const [loading, setLoading] = useState(isFeatureEnabled && Boolean(queries));
|
||||||
const [error, setError] = useState<Error | null>(null);
|
const [error, setError] = useState<Error | null>(null);
|
||||||
|
|
||||||
// Store queries in ref so we can access current value without triggering effect
|
// Store queries in ref so we can access current value without triggering effect
|
||||||
@@ -48,7 +47,7 @@ export function useSQLSchemas({ queries, enabled, timeRange }: UseSQLSchemasOpti
|
|||||||
queriesRef.current = queries;
|
queriesRef.current = queries;
|
||||||
|
|
||||||
const fetchSchemas = useCallback(async () => {
|
const fetchSchemas = useCallback(async () => {
|
||||||
if (!enabled || !isFeatureEnabled) {
|
if (!isFeatureEnabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,7 +84,7 @@ export function useSQLSchemas({ queries, enabled, timeRange }: UseSQLSchemasOpti
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [enabled, isFeatureEnabled, timeRange]);
|
}, [isFeatureEnabled, timeRange]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchSchemas();
|
fetchSchemas();
|
||||||
|
|||||||
Reference in New Issue
Block a user