feat(ai): add dashboard tools for AI chat (#16517)

## Summary

- Implements real tools for the dashboard-building skill to create and
manage dashboards through the AI chat interface
- Adds 6 new dashboard tools: `create_complete_dashboard`,
`list_dashboards`, `get_dashboard`, `add_dashboard_widget`,
`update_dashboard_widget`, `delete_dashboard_widget`
- Improves widget configuration robustness with typed Zod schemas and
discriminated unions for graph types

## Key Changes

**New Dashboard Tools:**
- `create_complete_dashboard` - Creates a dashboard with layout, tab,
and widgets in a single call
- `list_dashboards` - Lists all dashboards in the workspace
- `get_dashboard` - Gets full dashboard details including tabs and
widget configurations
- `add_dashboard_widget` - Adds a widget to an existing dashboard tab
- `update_dashboard_widget` - Updates widget properties or configuration
- `delete_dashboard_widget` - Removes a widget from a dashboard

**Widget Configuration Improvements:**
- Typed Zod schemas for each chart type (AGGREGATE, BAR, LINE, PIE)
- Discriminated union validation based on `graphType`
- Widget-level error handling for partial success when creating
dashboards
- Clear documentation about required `objectMetadataId` and field UUIDs

**Skill Documentation Updates:**
- Updated `dashboard-building.skill.ts` with critical guidance about
looking up field metadata first
- Added workflow instructions: use `list_object_metadata_items` before
creating GRAPH widgets
- Practical grid layout recommendations

## Test plan

- [ ] Create a new dashboard via AI chat
- [ ] Verify widgets display data correctly when proper field IDs are
provided
- [ ] Test adding/updating/deleting widgets on existing dashboards
- [ ] Verify error messages are helpful when configuration is incorrect
This commit is contained in:
Félix Malfait
2025-12-12 07:35:14 +01:00
committed by GitHub
parent 70a78aafe9
commit 5f4f4c0af8
18 changed files with 976 additions and 55 deletions
@@ -8,59 +8,71 @@ export const DASHBOARD_BUILDING_SKILL: SkillDefinition = {
You help users create and manage dashboards with widgets.
## Capabilities
## CRITICAL: Creating GRAPH Widgets
- Create new dashboards from scratch
- Add, modify, and remove widgets from dashboards
- Configure widget types (VIEW, GRAPH, FIELDS, TIMELINE, TASKS, NOTES, FILES, EMAILS, CALENDAR, RICH_TEXT, IFRAME, WORKFLOW)
- Manage dashboard tabs and layouts
- Position widgets in a grid system (12-column layout)
Before creating any GRAPH widget, you MUST:
1. Use list_object_metadata_items to get the objectMetadataId (e.g., for "opportunity", "company")
2. From the response, get the field IDs you need (aggregateFieldMetadataId, primaryAxisGroupByFieldMetadataId)
## Dashboard Structure
GRAPH widgets require real UUIDs from the workspace metadata, NOT made-up values.
- **Dashboard**: Container with a title and pageLayout
- **PageLayout**: Contains tabs (type: DASHBOARD)
- **PageLayoutTab**: Contains widgets with a title, position, and layoutMode (grid/vertical-list/canvas)
- **PageLayoutWidget**: Individual widget with type, title, gridPosition, and optional configuration
## Widget Configuration
### 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"
### GRAPH - BAR/LINE Charts
Shows data grouped by a dimension.
Required:
- objectMetadataId: UUID of the object
- configuration.graphType: "VERTICAL_BAR", "HORIZONTAL_BAR", or "LINE"
- configuration.aggregateFieldMetadataId: field to aggregate
- configuration.aggregateOperation: aggregation type
- configuration.primaryAxisGroupByFieldMetadataId: field to group by (x-axis)
### GRAPH - PIE Charts
Shows data distribution as slices.
Required:
- objectMetadataId: UUID of the object
- configuration.graphType: "PIE"
- configuration.aggregateFieldMetadataId: field to aggregate
- configuration.aggregateOperation: aggregation type
- configuration.groupByFieldMetadataId: field to slice by
### IFRAME
Embeds external content:
- configuration.url: "https://..."
### STANDALONE_RICH_TEXT
Text content widget:
- configuration.body: "Your text here"
## Grid System
- 12 columns total
- Grid positions: { row, column, rowSpan, columnSpan }
- Common sizes: Full width (columnSpan: 12), Half width (columnSpan: 6), Quarter width (columnSpan: 3)
- Typical heights: Small (rowSpan: 4), Medium (rowSpan: 6), Large (rowSpan: 8)
- 12 columns (0-11)
- KPI widgets: rowSpan 2-4, columnSpan 3-4
- Charts: rowSpan 6-8, columnSpan 6-12
- Common layouts:
- 4 KPIs in a row: each { columnSpan: 3 }
- 2 charts side by side: each { columnSpan: 6 }
- Full width chart: { column: 0, columnSpan: 12 }
## Widget Types Explained
## Workflow
- **VIEW**: Display a filtered view of records (companies, people, opportunities, etc.)
- **GRAPH**: Show charts and visualizations of data
- **FIELDS**: Display specific fields from a record
- **TIMELINE**: Show activity timeline
- **TASKS**: Display tasks list
- **NOTES**: Show notes
- **FILES**: Display file attachments
- **EMAILS**: Show email communications
- **CALENDAR**: Display calendar events
- **RICH_TEXT**: Custom text content
- **IFRAME**: Embed external content
- **WORKFLOW**: Display workflow information
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
## Approach
## Best Practices
- Ask clarifying questions about dashboard purpose and desired widgets
- Suggest appropriate widget types and layouts for the use case
- Create well-organized, visually balanced dashboards
- For modifications, first understand current structure
- Explain widget placement and purpose
- Consider responsive design (widgets stack on smaller screens)
## Layout Best Practices
- Place most important information at the top
- Group related widgets together
- Use consistent widget sizes when possible
- Leave some whitespace for visual clarity
- Consider logical reading order (left to right, top to bottom)
Prioritize user needs and dashboard usability.`,
- Place KPIs at the top (row 0)
- Group related charts together
- Use consistent heights within rows
- Start simple, add complexity as needed`,
};
@@ -0,0 +1 @@
export const DASHBOARD_TOOL_SERVICE_TOKEN = Symbol('DASHBOARD_TOOL_SERVICE');
@@ -5,4 +5,5 @@ export enum ToolCategory {
METADATA = 'METADATA',
NATIVE_MODEL = 'NATIVE_MODEL',
VIEW = 'VIEW',
DASHBOARD = 'DASHBOARD',
}
@@ -0,0 +1,49 @@
import { Inject, Injectable, Optional } from '@nestjs/common';
import { type ToolSet } from 'ai';
import { PermissionFlagType } from 'twenty-shared/constants';
import {
type ToolProvider,
type ToolProviderContext,
} from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface';
import { DASHBOARD_TOOL_SERVICE_TOKEN } from 'src/engine/core-modules/tool-provider/constants/dashboard-tool-service.token';
import { ToolCategory } from 'src/engine/core-modules/tool-provider/enums/tool-category.enum';
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
import type { DashboardToolWorkspaceService } from 'src/modules/dashboard/tools/services/dashboard-tool.workspace-service';
@Injectable()
export class DashboardToolProvider implements ToolProvider {
readonly category = ToolCategory.DASHBOARD;
constructor(
@Optional()
@Inject(DASHBOARD_TOOL_SERVICE_TOKEN)
private readonly dashboardToolService: DashboardToolWorkspaceService | null,
private readonly permissionsService: PermissionsService,
) {}
async isAvailable(context: ToolProviderContext): Promise<boolean> {
if (!this.dashboardToolService) {
return false;
}
return this.permissionsService.checkRolesPermissions(
context.rolePermissionConfig,
context.workspaceId,
PermissionFlagType.LAYOUTS,
);
}
async generateTools(context: ToolProviderContext): Promise<ToolSet> {
if (!this.dashboardToolService) {
return {};
}
return this.dashboardToolService.generateDashboardTools(
context.workspaceId,
context.rolePermissionConfig,
);
}
}
@@ -15,14 +15,26 @@ import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-perm
export type ToolIndexEntry = {
name: string;
description: string;
category: 'database' | 'action' | 'workflow' | 'metadata' | 'view';
category:
| 'database'
| 'action'
| 'workflow'
| 'metadata'
| 'view'
| 'dashboard';
objectName?: string;
operation?: string;
};
export type ToolSearchOptions = {
limit?: number;
category?: 'database' | 'action' | 'workflow' | 'metadata' | 'view';
category?:
| 'database'
| 'action'
| 'workflow'
| 'metadata'
| 'view'
| 'dashboard';
};
export type ToolContext = {
@@ -174,6 +186,7 @@ export class ToolRegistryService {
METADATA: 'metadata',
NATIVE_MODEL: 'action',
VIEW: 'view',
DASHBOARD: 'dashboard',
};
return Object.entries(tools).map(([name, tool]) => ({
@@ -4,6 +4,7 @@ import { RecordCrudModule } from 'src/engine/core-modules/record-crud/record-cru
import { ToolGeneratorModule } from 'src/engine/core-modules/tool-generator/tool-generator.module';
import { TOOL_PROVIDERS } from 'src/engine/core-modules/tool-provider/constants/tool-providers.token';
import { ActionToolProvider } from 'src/engine/core-modules/tool-provider/providers/action-tool.provider';
import { DashboardToolProvider } from 'src/engine/core-modules/tool-provider/providers/dashboard-tool.provider';
import { DatabaseToolProvider } from 'src/engine/core-modules/tool-provider/providers/database-tool.provider';
import { MetadataToolProvider } from 'src/engine/core-modules/tool-provider/providers/metadata-tool.provider';
import { ViewToolProvider } from 'src/engine/core-modules/tool-provider/providers/view-tool.provider';
@@ -21,14 +22,10 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache
import { ToolProviderService } from './services/tool-provider.service';
import { ToolRegistryService } from './services/tool-registry.service';
// NOTE: This module does NOT import WorkflowToolsModule to avoid circular dependency:
// ToolProviderModule -> WorkflowToolsModule -> WorkflowTriggerModule
// -> WorkflowRunnerModule -> WorkflowExecutorModule -> AiAgentActionModule
// -> AiAgentExecutionModule -> ToolProviderModule
//
// Instead, WorkflowToolsModule is a @Global() module that provides WORKFLOW_TOOL_SERVICE_TOKEN.
// When WorkflowToolsModule is imported anywhere in the app (e.g., AiChatModule),
// the token becomes available globally to WorkflowToolProvider via @Optional() injection.
// NOTE: This module does NOT import WorkflowToolsModule or DashboardToolsModule to avoid
// circular dependencies. Instead, they are @Global() modules that provide their tokens.
// When imported anywhere in the app (e.g., AiChatModule), the tokens become available
// globally to their respective providers via @Optional() injection.
@Module({
imports: [
@@ -46,6 +43,7 @@ import { ToolRegistryService } from './services/tool-registry.service';
],
providers: [
ActionToolProvider,
DashboardToolProvider,
DatabaseToolProvider,
MetadataToolProvider,
ViewToolProvider,
@@ -54,12 +52,14 @@ import { ToolRegistryService } from './services/tool-registry.service';
provide: TOOL_PROVIDERS,
useFactory: (
actionProvider: ActionToolProvider,
dashboardProvider: DashboardToolProvider,
databaseProvider: DatabaseToolProvider,
metadataProvider: MetadataToolProvider,
viewProvider: ViewToolProvider,
workflowProvider: WorkflowToolProvider,
) => [
actionProvider,
dashboardProvider,
databaseProvider,
metadataProvider,
viewProvider,
@@ -67,6 +67,7 @@ import { ToolRegistryService } from './services/tool-registry.service';
],
inject: [
ActionToolProvider,
DashboardToolProvider,
DatabaseToolProvider,
MetadataToolProvider,
ViewToolProvider,