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
@@ -233,15 +233,17 @@ export class ChatExecutionService {
viewName: string;
filterDescriptions: string[];
}): string {
const { objectNameSingular, viewName, filterDescriptions } =
const { objectNameSingular, viewId, viewName, filterDescriptions } =
browsingContext;
let context = `The user is viewing a list of ${objectNameSingular} records in a view called "${viewName}".`;
let context = `The user is viewing a list of ${objectNameSingular} records in a view called "${viewName}" (viewId: ${viewId}).`;
if (filterDescriptions.length > 0) {
context += `\nFilters applied: ${filterDescriptions.join(', ')}`;
}
context += `\nUse get_view_query_parameters tool with this viewId to get the exact filter/sort parameters for querying records.`;
return context;
}
@@ -323,7 +325,13 @@ ${preloadedTools.length > 0 ? preloadedTools.map((t) => `- \`${t}\` ✓`).join('
### Tool Catalog by Category`);
const categoryOrder = ['database', 'action', 'workflow', 'metadata'];
const categoryOrder = [
'database',
'action',
'workflow',
'metadata',
'view',
];
for (const category of categoryOrder) {
const tools = toolsByCategory.get(category);
@@ -365,6 +373,8 @@ ${tools
return 'Workflow Tools (create/manage workflows)';
case 'metadata':
return 'Metadata Tools (schema management)';
case 'view':
return 'View Tools (query views)';
default:
return category;
}