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:
+57
-45
@@ -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`,
|
||||
};
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
export const DASHBOARD_TOOL_SERVICE_TOKEN = Symbol('DASHBOARD_TOOL_SERVICE');
|
||||
+1
@@ -5,4 +5,5 @@ export enum ToolCategory {
|
||||
METADATA = 'METADATA',
|
||||
NATIVE_MODEL = 'NATIVE_MODEL',
|
||||
VIEW = 'VIEW',
|
||||
DASHBOARD = 'DASHBOARD',
|
||||
}
|
||||
|
||||
+49
@@ -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
-2
@@ -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]) => ({
|
||||
|
||||
+9
-8
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user