From 8908e0785f5d718aabddb46414b81681686f7d3c Mon Sep 17 00:00:00 2001 From: nitin <142569587+ehconitin@users.noreply.github.com> Date: Fri, 16 Jan 2026 02:25:47 +0530 Subject: [PATCH] [Dashboards] [ai] update dashboard tools schema and skill with correct config field names (#17180) CleanShot 2026-01-16 at 00 55 56 --- ...reate-standard-flat-skill-metadata.util.ts | 98 +++++++++++++++---- .../tools/create-complete-dashboard.tool.ts | 27 ++--- .../dashboard/tools/schemas/widget.schema.ts | 44 +++++---- .../tools/update-dashboard-widget.tool.ts | 15 ++- 4 files changed, 133 insertions(+), 51 deletions(-) diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/skill-metadata/create-standard-flat-skill-metadata.util.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/skill-metadata/create-standard-flat-skill-metadata.util.ts index d5ed649c7d..faa9556b8a 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/skill-metadata/create-standard-flat-skill-metadata.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/skill-metadata/create-standard-flat-skill-metadata.util.ts @@ -146,41 +146,101 @@ Before creating any GRAPH widget, you MUST: GRAPH widgets require real UUIDs from the workspace metadata, NOT made-up values. -## Widget Configuration +## Understanding User Language -### GRAPH - AGGREGATE (KPI numbers) -Shows a single aggregated value (count, sum, average). -Required: -- objectMetadataId: UUID of the object (e.g., opportunity) -- configuration.graphType: "AGGREGATE" -- configuration.aggregateFieldMetadataId: UUID of field to aggregate -- configuration.aggregateOperation: "COUNT", "SUM", "AVG", "MIN", "MAX" +Users describe charts using UI terminology. Here's how to translate: -### GRAPH - BAR/LINE Charts -Shows data grouped by a dimension. +### Bar/Line Chart Settings + +**Data section:** +- "Source" / "change the object": objectMetadataId +- "Filter" / "filter the data": configuration.filter + +**X axis section (primary grouping - the bars/categories):** +- "X axis" / "data on display" / "categories": primaryAxisGroupByFieldMetadataId +- "Date granularity" (on X axis): primaryAxisDateGranularity +- "Sort by" (on X axis): primaryAxisOrderBy + +**Y axis section (what's being measured + optional secondary grouping):** +- "Y axis" / "data on display" / "measure" / "metric": aggregateFieldMetadataId + aggregateOperation +- "Group by" / "stacking" / "colors" / "breakdown": secondaryAxisGroupByFieldMetadataId +- "Date granularity" (on Group by): secondaryAxisDateGranularity +- "Sort by" (on Group by): secondaryAxisOrderBy +- "Cumulative" / "running total": cumulative +- "Min range" / "Max range": rangeMin, rangeMax + +**Style section:** +- "Stacked" / "stacked bars": layout stays same, it's about secondaryAxisGroupByFieldMetadataId +- "Data labels" / "show values": displayDataLabel +- "Legend" / "show legend": displayLegend + +### CRITICAL: "Remove groupby" / "remove stacking" / "unstacked" +When users say this for bar/line charts, they mean remove the SECONDARY grouping (the colors/stacking). +- Set secondaryAxisGroupByFieldMetadataId to null +- Keep the chart type (BAR_CHART/LINE_CHART) +- Keep primaryAxisGroupByFieldMetadataId (the X axis categories) +- DO NOT convert to AGGREGATE_CHART unless user explicitly asks for "just a number" or "KPI" + +### Pie Chart Settings +- "Each slice represents" / "slices": groupByFieldMetadataId +- "Hide empty category": hideEmptyCategory +- "Show value in center": showValueInCenter + +### Aggregate Chart Settings (KPI numbers) +- "Prefix" (e.g., "$"): prefix +- "Suffix" (e.g., "%"): suffix + +## Widget Configuration Types + +### AGGREGATE_CHART (KPI number widget) +Shows a single aggregated value. Required: - objectMetadataId: UUID of the object -- configuration.graphType: "VERTICAL_BAR", "HORIZONTAL_BAR", or "LINE" +- configuration.configurationType: "AGGREGATE_CHART" +- configuration.aggregateFieldMetadataId: UUID of field to aggregate +- configuration.aggregateOperation: "COUNT", "SUM", "AVG", "MIN", "MAX" +Optional: prefix, suffix, displayDataLabel, filter + +### BAR_CHART +Shows data grouped by categories with optional secondary grouping. +Required: +- objectMetadataId: UUID of the object +- configuration.configurationType: "BAR_CHART" - configuration.aggregateFieldMetadataId: field to aggregate - configuration.aggregateOperation: aggregation type -- configuration.primaryAxisGroupByFieldMetadataId: field to group by (x-axis) +- configuration.primaryAxisGroupByFieldMetadataId: X axis categories +- configuration.layout: "VERTICAL" or "HORIZONTAL" +Optional: secondaryAxisGroupByFieldMetadataId (for stacking/colors), displayDataLabel, displayLegend, filter -### GRAPH - PIE Charts +### LINE_CHART +Shows trends over a dimension. +Required: +- objectMetadataId: UUID of the object +- configuration.configurationType: "LINE_CHART" +- configuration.aggregateFieldMetadataId: field to aggregate +- configuration.aggregateOperation: aggregation type +- configuration.primaryAxisGroupByFieldMetadataId: X axis (usually date) +Optional: secondaryAxisGroupByFieldMetadataId (for multiple lines), cumulative, displayDataLabel, filter + +### PIE_CHART Shows data distribution as slices. Required: - objectMetadataId: UUID of the object -- configuration.graphType: "PIE" +- configuration.configurationType: "PIE_CHART" - configuration.aggregateFieldMetadataId: field to aggregate - configuration.aggregateOperation: aggregation type -- configuration.groupByFieldMetadataId: field to slice by +- configuration.groupByFieldMetadataId: field to slice by (NOTE: different field name than bar/line!) +Optional: displayDataLabel, hideEmptyCategory, showValueInCenter, filter ### IFRAME Embeds external content: +- configuration.configurationType: "IFRAME" - configuration.url: "https://..." ### STANDALONE_RICH_TEXT Text content widget: -- configuration.body: "Your text here" +- configuration.configurationType: "STANDALONE_RICH_TEXT" +- configuration.body: rich text content ## Grid System @@ -197,14 +257,16 @@ Text content widget: 1. Ask user what data they want to visualize 2. Load list_object_metadata_items to discover available objects and fields 3. Create dashboard with appropriate widgets using real field IDs -4. Use get_dashboard to verify creation +4. Use get_dashboard to verify creation and see current configuration +5. When modifying, first understand current config before making changes ## Best Practices - Place KPIs at the top (row 0) - Group related charts together - Use consistent heights within rows -- Start simple, add complexity as needed`, +- Start simple, add complexity as needed +- When user asks to modify a chart, clarify if they want to change settings OR change chart type`, isCustom: false, }, }), diff --git a/packages/twenty-server/src/modules/dashboard/tools/create-complete-dashboard.tool.ts b/packages/twenty-server/src/modules/dashboard/tools/create-complete-dashboard.tool.ts index 8641c61db2..d8c645f651 100644 --- a/packages/twenty-server/src/modules/dashboard/tools/create-complete-dashboard.tool.ts +++ b/packages/twenty-server/src/modules/dashboard/tools/create-complete-dashboard.tool.ts @@ -60,24 +60,25 @@ GRID SYSTEM: WIDGET TYPES: -1. GRAPH with graphType "AGGREGATE" (KPI number): - - Requires: objectMetadataId, configuration.graphType, configuration.aggregateFieldMetadataId, configuration.aggregateOperation - - Example: { type: "GRAPH", objectMetadataId: "", configuration: { graphType: "AGGREGATE", aggregateFieldMetadataId: "", aggregateOperation: "SUM" } } +1. GRAPH with configurationType "AGGREGATE_CHART" (KPI number): + - Requires: objectMetadataId, configuration.configurationType, configuration.aggregateFieldMetadataId, configuration.aggregateOperation + - Example: { type: "GRAPH", objectMetadataId: "", configuration: { configurationType: "AGGREGATE_CHART", aggregateFieldMetadataId: "", aggregateOperation: "SUM" } } -2. GRAPH with graphType "VERTICAL_BAR" or "HORIZONTAL_BAR": +2. GRAPH with configurationType "BAR_CHART": + - Additional required: configuration.primaryAxisGroupByFieldMetadataId, configuration.layout ("VERTICAL" or "HORIZONTAL") + - Example: { type: "GRAPH", objectMetadataId: "", configuration: { configurationType: "BAR_CHART", aggregateFieldMetadataId: "", aggregateOperation: "COUNT", primaryAxisGroupByFieldMetadataId: "", layout: "VERTICAL" } } + +3. GRAPH with configurationType "LINE_CHART": - Additional required: configuration.primaryAxisGroupByFieldMetadataId - - Example: { graphType: "VERTICAL_BAR", aggregateFieldMetadataId: "", aggregateOperation: "COUNT", primaryAxisGroupByFieldMetadataId: "" } + - Example: { type: "GRAPH", objectMetadataId: "", configuration: { configurationType: "LINE_CHART", aggregateFieldMetadataId: "", aggregateOperation: "SUM", primaryAxisGroupByFieldMetadataId: "" } } -3. GRAPH with graphType "LINE": - - Same as bar charts, good for time series +4. GRAPH with configurationType "PIE_CHART": + - Additional required: configuration.groupByFieldMetadataId (note: different field name!) + - Example: { type: "GRAPH", objectMetadataId: "", configuration: { configurationType: "PIE_CHART", aggregateFieldMetadataId: "", aggregateOperation: "COUNT", groupByFieldMetadataId: "" } } -4. GRAPH with graphType "PIE": - - Requires: objectMetadataId, aggregateFieldMetadataId, aggregateOperation, groupByFieldMetadataId - - Example: { graphType: "PIE", aggregateFieldMetadataId: "", aggregateOperation: "COUNT", groupByFieldMetadataId: "" } +5. IFRAME: { type: "IFRAME", configuration: { configurationType: "IFRAME", url: "https://..." } } -5. IFRAME: { type: "IFRAME", configuration: { url: "https://..." } } - -6. STANDALONE_RICH_TEXT: { type: "STANDALONE_RICH_TEXT", configuration: { body: "..." } } +6. STANDALONE_RICH_TEXT: { type: "STANDALONE_RICH_TEXT", configuration: { configurationType: "STANDALONE_RICH_TEXT", body: { ... } } } AGGREGATION OPERATIONS: COUNT, SUM, AVG, MIN, MAX, COUNT_EMPTY, COUNT_NOT_EMPTY`, inputSchema: createCompleteDashboardSchema, diff --git a/packages/twenty-server/src/modules/dashboard/tools/schemas/widget.schema.ts b/packages/twenty-server/src/modules/dashboard/tools/schemas/widget.schema.ts index 3d3699a548..2245b732e9 100644 --- a/packages/twenty-server/src/modules/dashboard/tools/schemas/widget.schema.ts +++ b/packages/twenty-server/src/modules/dashboard/tools/schemas/widget.schema.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant'; -import { GraphType } from 'src/engine/metadata-modules/page-layout-widget/enums/graph-type.enum'; +import { WidgetConfigurationType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-configuration-type.type'; import { WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-type.enum'; export const gridPositionSchema = z.object({ @@ -28,7 +28,7 @@ export const widgetTypeSchema = z.enum([ // Graph configuration schema for AGGREGATE type (KPI numbers) const aggregateChartConfigSchema = z.object({ - graphType: z.literal(GraphType.AGGREGATE_CHART), + configurationType: z.literal(WidgetConfigurationType.AGGREGATE_CHART), aggregateFieldMetadataId: z .string() .uuid() @@ -48,7 +48,7 @@ const aggregateChartConfigSchema = z.object({ // Graph configuration schema for BAR charts const barChartConfigSchema = z .object({ - graphType: z.literal(GraphType.BAR_CHART), + configurationType: z.literal(WidgetConfigurationType.BAR_CHART), aggregateFieldMetadataId: z .string() .uuid() @@ -87,8 +87,6 @@ const barChartConfigSchema = z displayLegend: z.boolean().optional().default(true), layout: z .enum(['VERTICAL', 'HORIZONTAL']) - .optional() - .default('VERTICAL') .describe('Layout orientation for bar charts'), filter: z.record(z.string(), z.unknown()).optional(), }) @@ -118,7 +116,7 @@ const barChartConfigSchema = z // Graph configuration schema for LINE charts const lineChartConfigSchema = z .object({ - graphType: z.literal(GraphType.LINE_CHART), + configurationType: z.literal(WidgetConfigurationType.LINE_CHART), aggregateFieldMetadataId: z.string().uuid(), aggregateOperation: z.nativeEnum(AggregateOperations), primaryAxisGroupByFieldMetadataId: z.string().uuid(), @@ -176,7 +174,7 @@ const lineChartConfigSchema = z // Graph configuration schema for PIE charts const pieChartConfigSchema = z .object({ - graphType: z.literal(GraphType.PIE_CHART), + configurationType: z.literal(WidgetConfigurationType.PIE_CHART), aggregateFieldMetadataId: z.string().uuid(), aggregateOperation: z.nativeEnum(AggregateOperations), groupByFieldMetadataId: z @@ -211,25 +209,37 @@ const pieChartConfigSchema = z // Iframe configuration const iframeConfigSchema = z.object({ - url: z.string().url().describe('URL to embed'), + configurationType: z.literal(WidgetConfigurationType.IFRAME), + url: z.string().url().optional().describe('URL to embed'), }); // Rich text configuration const richTextConfigSchema = z.object({ - body: z.string().optional().describe('Rich text content'), + configurationType: z.literal(WidgetConfigurationType.STANDALONE_RICH_TEXT), + body: z.unknown().optional().describe('Rich text content (RichTextV2Body)'), }); -export const graphConfigurationSchema = z.discriminatedUnion('graphType', [ - aggregateChartConfigSchema, - barChartConfigSchema, - lineChartConfigSchema, - pieChartConfigSchema, -]); +export const graphConfigurationSchema = z.discriminatedUnion( + 'configurationType', + [ + aggregateChartConfigSchema, + barChartConfigSchema, + lineChartConfigSchema, + pieChartConfigSchema, + ], +); export const widgetConfigurationSchema = z - .union([graphConfigurationSchema, iframeConfigSchema, richTextConfigSchema]) + .discriminatedUnion('configurationType', [ + aggregateChartConfigSchema, + barChartConfigSchema, + lineChartConfigSchema, + pieChartConfigSchema, + iframeConfigSchema, + richTextConfigSchema, + ]) .optional() .describe('Widget configuration - structure depends on widget type'); // Export enums for documentation -export { AggregateOperations, GraphType as GraphType }; +export { AggregateOperations, WidgetConfigurationType }; diff --git a/packages/twenty-server/src/modules/dashboard/tools/update-dashboard-widget.tool.ts b/packages/twenty-server/src/modules/dashboard/tools/update-dashboard-widget.tool.ts index 57cf2d0435..315a48b7e7 100644 --- a/packages/twenty-server/src/modules/dashboard/tools/update-dashboard-widget.tool.ts +++ b/packages/twenty-server/src/modules/dashboard/tools/update-dashboard-widget.tool.ts @@ -1,4 +1,4 @@ -import { isDefined } from 'twenty-shared/utils'; +import { isDefined, isEmptyObject } from 'twenty-shared/utils'; import { z } from 'zod'; import { type WidgetType } from 'src/engine/metadata-modules/page-layout-widget/enums/widget-type.enum'; @@ -24,7 +24,7 @@ const updateDashboardWidgetSchema = z.object({ .uuid() .optional() .describe('New object metadata ID'), - configuration: widgetConfigurationSchema, + configuration: widgetConfigurationSchema.optional(), }); export const createUpdateDashboardWidgetTool = ( @@ -54,7 +54,16 @@ Only provide fields you want to change - others remain unchanged.`, try { const { widgetId, ...updates } = parameters; const updateData = Object.fromEntries( - Object.entries(updates).filter(([, value]) => isDefined(value)), + Object.entries(updates).filter(([key, value]) => { + if (!isDefined(value)) { + return false; + } + if (key === 'configuration' && isEmptyObject(value)) { + return false; + } + + return true; + }), ); const widget = await deps.pageLayoutWidgetService.update({