feat(ai): add view management tools for AI chat (#16495)

## Summary

Adds a new **VIEW** tool category for the AI chat, enabling it to work
with views:

- **get-views**: List views in the workspace, optionally filtered by
object metadata ID
- **get-view-query-parameters**: Convert a view's filters and sorts into
GraphQL query parameters that can be passed to existing `find_*` data
tools
- **create-view**, **update-view**, **delete-view**: CRUD operations for
view management

### Key design decisions

1. **No pagination duplication**: Instead of creating a
`find-records-from-view` tool that would duplicate pagination logic,
`get-view-query-parameters` returns filter/sort parameters that the AI
can pass to existing record-fetching tools.

2. **Permission model**:
- Read tools (get-views, get-view-query-parameters) are available to all
users
   - Write tools require the `VIEW` permission
   - UNLISTED views can only be modified by their creator

3. **Leverages existing utilities**: Uses
`computeRecordGqlOperationFilter` from `twenty-shared` for filter
conversion.

### Files changed

- Added `ViewToolProvider`, `ViewToolsFactory`, and
`ViewQueryParamsService`
- Added `VIEW` to `ToolCategory` enum and tool registry
- Updated `chat-execution.service.ts` to include view tools in the
catalog and pass viewId in browsing context
- Extracted shared `formatValidationErrors` utility to reduce
duplication

## Test plan

- [x] Unit tests for `ViewToolsFactory` 
- [x] Unit tests for `ViewQueryParamsService`
- [x] Lint and typecheck pass
This commit is contained in:
Félix Malfait
2025-12-11 15:05:42 +01:00
committed by GitHub
parent 2943125410
commit 3cea19baf4
15 changed files with 1384 additions and 140 deletions
@@ -4,4 +4,5 @@ export enum ToolCategory {
WORKFLOW = 'WORKFLOW',
METADATA = 'METADATA',
NATIVE_MODEL = 'NATIVE_MODEL',
VIEW = 'VIEW',
}
@@ -0,0 +1,55 @@
import { Injectable } 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 { ToolCategory } from 'src/engine/core-modules/tool-provider/enums/tool-category.enum';
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
import { ViewToolsFactory } from 'src/engine/metadata-modules/view/tools/view-tools.factory';
@Injectable()
export class ViewToolProvider implements ToolProvider {
readonly category = ToolCategory.VIEW;
constructor(
private readonly viewToolsFactory: ViewToolsFactory,
private readonly permissionsService: PermissionsService,
) {}
async isAvailable(_context: ToolProviderContext): Promise<boolean> {
return true;
}
async generateTools(context: ToolProviderContext): Promise<ToolSet> {
const workspaceMemberId = context.actorContext?.workspaceMemberId;
const readTools = this.viewToolsFactory.generateReadTools(
context.workspaceId,
workspaceMemberId ?? undefined,
workspaceMemberId ?? undefined,
);
const hasViewPermission =
await this.permissionsService.checkRolesPermissions(
context.rolePermissionConfig,
context.workspaceId,
PermissionFlagType.VIEWS,
);
if (hasViewPermission) {
const writeTools = this.viewToolsFactory.generateWriteTools(
context.workspaceId,
workspaceMemberId ?? undefined,
);
return { ...readTools, ...writeTools };
}
return readTools;
}
}
@@ -15,14 +15,14 @@ import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-perm
export type ToolIndexEntry = {
name: string;
description: string;
category: 'database' | 'action' | 'workflow' | 'metadata';
category: 'database' | 'action' | 'workflow' | 'metadata' | 'view';
objectName?: string;
operation?: string;
};
export type ToolSearchOptions = {
limit?: number;
category?: 'database' | 'action' | 'workflow' | 'metadata';
category?: 'database' | 'action' | 'workflow' | 'metadata' | 'view';
};
export type ToolContext = {
@@ -173,6 +173,7 @@ export class ToolRegistryService {
WORKFLOW: 'workflow',
METADATA: 'metadata',
NATIVE_MODEL: 'action',
VIEW: 'view',
};
return Object.entries(tools).map(([name, tool]) => ({
@@ -6,6 +6,7 @@ import { TOOL_PROVIDERS } from 'src/engine/core-modules/tool-provider/constants/
import { ActionToolProvider } from 'src/engine/core-modules/tool-provider/providers/action-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';
import { WorkflowToolProvider } from 'src/engine/core-modules/tool-provider/providers/workflow-tool.provider';
import { ToolModule } from 'src/engine/core-modules/tool/tool.module';
import { AiAgentExecutionModule } from 'src/engine/metadata-modules/ai/ai-agent-execution/ai-agent-execution.module';
@@ -14,6 +15,7 @@ import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { ViewModule } from 'src/engine/metadata-modules/view/view.module';
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
import { ToolProviderService } from './services/tool-provider.service';
@@ -37,6 +39,7 @@ import { ToolRegistryService } from './services/tool-registry.service';
ObjectMetadataModule,
FieldMetadataModule,
PermissionsModule,
ViewModule,
WorkspaceCacheModule,
WorkspaceManyOrAllFlatEntityMapsCacheModule,
],
@@ -44,6 +47,7 @@ import { ToolRegistryService } from './services/tool-registry.service';
ActionToolProvider,
DatabaseToolProvider,
MetadataToolProvider,
ViewToolProvider,
WorkflowToolProvider,
{
provide: TOOL_PROVIDERS,
@@ -51,17 +55,20 @@ import { ToolRegistryService } from './services/tool-registry.service';
actionProvider: ActionToolProvider,
databaseProvider: DatabaseToolProvider,
metadataProvider: MetadataToolProvider,
viewProvider: ViewToolProvider,
workflowProvider: WorkflowToolProvider,
) => [
actionProvider,
databaseProvider,
metadataProvider,
viewProvider,
workflowProvider,
],
inject: [
ActionToolProvider,
DatabaseToolProvider,
MetadataToolProvider,
ViewToolProvider,
WorkflowToolProvider,
],
},
@@ -0,0 +1,28 @@
import type { WorkspaceMigrationBuilderExceptionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-builder-exception-v2';
export const formatValidationErrors = (
error: WorkspaceMigrationBuilderExceptionV2,
): string => {
const report = error.failedWorkspaceMigrationBuildResult.report;
const errorMessages: string[] = [];
for (const [entityType, failures] of Object.entries(report)) {
if (Array.isArray(failures) && failures.length > 0) {
for (const failure of failures) {
if (failure.errors && Array.isArray(failure.errors)) {
for (const validationError of failure.errors) {
const message = validationError.message || validationError.code;
errorMessages.push(`[${entityType}] ${message}`);
}
}
}
}
}
if (errorMessages.length === 0) {
return error.message;
}
return `Validation errors:\n${errorMessages.join('\n')}`;
};