AI tools to create a demo workspace (#18236)
This PR adds the necessary tool to create a demo workspace with : relevant custom objects and fields, mock data and a real dashboard with graph widgets. It is still a bit under-optimized and slow but it works. This PR also adds an AI tool that allows to see what happens in real time, it navigates the app and waits when necessary. --------- Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
This commit is contained in:
+1
@@ -5,6 +5,7 @@ export enum ToolCategory {
|
||||
METADATA = 'METADATA',
|
||||
NATIVE_MODEL = 'NATIVE_MODEL',
|
||||
VIEW = 'VIEW',
|
||||
VIEW_FIELD = 'VIEW_FIELD',
|
||||
DASHBOARD = 'DASHBOARD',
|
||||
LOGIC_FUNCTION = 'LOGIC_FUNCTION',
|
||||
}
|
||||
|
||||
+13
-2
@@ -19,10 +19,11 @@ import {
|
||||
type ToolIndexEntry,
|
||||
} from 'src/engine/core-modules/tool-provider/types/tool-descriptor.type';
|
||||
import { CodeInterpreterTool } from 'src/engine/core-modules/tool/tools/code-interpreter-tool/code-interpreter-tool';
|
||||
import { HttpTool } from 'src/engine/core-modules/tool/tools/http-tool/http-tool';
|
||||
import { SearchHelpCenterTool } from 'src/engine/core-modules/tool/tools/search-help-center-tool/search-help-center-tool';
|
||||
import { DraftEmailTool } from 'src/engine/core-modules/tool/tools/email-tool/draft-email-tool';
|
||||
import { SendEmailTool } from 'src/engine/core-modules/tool/tools/email-tool/send-email-tool';
|
||||
import { HttpTool } from 'src/engine/core-modules/tool/tools/http-tool/http-tool';
|
||||
import { NavigateAppTool } from 'src/engine/core-modules/tool/tools/navigate-tool/navigate-app-tool';
|
||||
import { SearchHelpCenterTool } from 'src/engine/core-modules/tool/tools/search-help-center-tool/search-help-center-tool';
|
||||
import { type ToolInput } from 'src/engine/core-modules/tool/types/tool-input.type';
|
||||
import { type Tool } from 'src/engine/core-modules/tool/types/tool.type';
|
||||
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
|
||||
@@ -39,6 +40,7 @@ export class ActionToolProvider implements ToolProvider {
|
||||
private readonly draftEmailTool: DraftEmailTool,
|
||||
private readonly searchHelpCenterTool: SearchHelpCenterTool,
|
||||
private readonly codeInterpreterTool: CodeInterpreterTool,
|
||||
private readonly navigateAppTool: NavigateAppTool,
|
||||
private readonly permissionsService: PermissionsService,
|
||||
private readonly toolExecutorService: ToolExecutorService,
|
||||
) {
|
||||
@@ -48,6 +50,7 @@ export class ActionToolProvider implements ToolProvider {
|
||||
['draft_email', this.draftEmailTool],
|
||||
['search_help_center', this.searchHelpCenterTool],
|
||||
['code_interpreter', this.codeInterpreterTool],
|
||||
['navigate_app', this.navigateAppTool],
|
||||
]);
|
||||
|
||||
// Register each action tool as a static handler in the executor
|
||||
@@ -116,6 +119,14 @@ export class ActionToolProvider implements ToolProvider {
|
||||
),
|
||||
);
|
||||
|
||||
descriptors.push(
|
||||
this.buildDescriptor(
|
||||
'navigate_app',
|
||||
this.navigateAppTool,
|
||||
includeSchemas,
|
||||
),
|
||||
);
|
||||
|
||||
const hasCodeInterpreterPermission =
|
||||
await this.permissionsService.hasToolPermission(
|
||||
context.rolePermissionConfig,
|
||||
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
import { Injectable, OnModuleInit } from '@nestjs/common';
|
||||
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
|
||||
import {
|
||||
type GenerateDescriptorOptions,
|
||||
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 { ToolExecutorService } from 'src/engine/core-modules/tool-provider/services/tool-executor.service';
|
||||
import {
|
||||
type ToolDescriptor,
|
||||
type ToolIndexEntry,
|
||||
} from 'src/engine/core-modules/tool-provider/types/tool-descriptor.type';
|
||||
import { toolSetToDescriptors } from 'src/engine/core-modules/tool-provider/utils/tool-set-to-descriptors.util';
|
||||
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
|
||||
import { ViewFieldToolsFactory } from 'src/engine/metadata-modules/view-field/tools/view-field-tools.factory';
|
||||
|
||||
@Injectable()
|
||||
export class ViewFieldToolProvider implements ToolProvider, OnModuleInit {
|
||||
readonly category = ToolCategory.VIEW_FIELD;
|
||||
|
||||
constructor(
|
||||
private readonly viewFieldToolsFactory: ViewFieldToolsFactory,
|
||||
private readonly permissionsService: PermissionsService,
|
||||
private readonly toolExecutorService: ToolExecutorService,
|
||||
) {}
|
||||
|
||||
onModuleInit(): void {
|
||||
const factory = this.viewFieldToolsFactory;
|
||||
|
||||
this.toolExecutorService.registerCategoryGenerator(
|
||||
ToolCategory.VIEW_FIELD,
|
||||
async (context) => {
|
||||
const readTools = factory.generateReadTools(context.workspaceId);
|
||||
|
||||
const hasViewPermission =
|
||||
await this.permissionsService.checkRolesPermissions(
|
||||
context.rolePermissionConfig,
|
||||
context.workspaceId,
|
||||
PermissionFlagType.VIEWS,
|
||||
);
|
||||
|
||||
if (hasViewPermission) {
|
||||
const writeTools = factory.generateWriteTools(context.workspaceId);
|
||||
|
||||
return { ...readTools, ...writeTools };
|
||||
}
|
||||
|
||||
return readTools;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async isAvailable(_context: ToolProviderContext): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
async generateDescriptors(
|
||||
context: ToolProviderContext,
|
||||
options?: GenerateDescriptorOptions,
|
||||
): Promise<(ToolIndexEntry | ToolDescriptor)[]> {
|
||||
const schemaOptions = {
|
||||
includeSchemas: options?.includeSchemas ?? true,
|
||||
};
|
||||
|
||||
const readTools = this.viewFieldToolsFactory.generateReadTools(
|
||||
context.workspaceId,
|
||||
);
|
||||
|
||||
const hasViewPermission =
|
||||
await this.permissionsService.checkRolesPermissions(
|
||||
context.rolePermissionConfig,
|
||||
context.workspaceId,
|
||||
PermissionFlagType.VIEWS,
|
||||
);
|
||||
|
||||
if (hasViewPermission) {
|
||||
const writeTools = this.viewFieldToolsFactory.generateWriteTools(
|
||||
context.workspaceId,
|
||||
);
|
||||
|
||||
return toolSetToDescriptors(
|
||||
{ ...readTools, ...writeTools },
|
||||
ToolCategory.VIEW_FIELD,
|
||||
schemaOptions,
|
||||
);
|
||||
}
|
||||
|
||||
return toolSetToDescriptors(
|
||||
readTools,
|
||||
ToolCategory.VIEW_FIELD,
|
||||
schemaOptions,
|
||||
);
|
||||
}
|
||||
}
|
||||
+9
-2
@@ -7,9 +7,10 @@ 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 { 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 { LogicFunctionToolProvider } from 'src/engine/core-modules/tool-provider/providers/logic-function-tool.provider';
|
||||
import { MetadataToolProvider } from 'src/engine/core-modules/tool-provider/providers/metadata-tool.provider';
|
||||
import { NativeModelToolProvider } from 'src/engine/core-modules/tool-provider/providers/native-model-tool.provider';
|
||||
import { LogicFunctionToolProvider } from 'src/engine/core-modules/tool-provider/providers/logic-function-tool.provider';
|
||||
import { ViewFieldToolProvider } from 'src/engine/core-modules/tool-provider/providers/view-field-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 { ToolExecutorService } from 'src/engine/core-modules/tool-provider/services/tool-executor.service';
|
||||
@@ -19,10 +20,11 @@ import { AiAgentExecutionModule } from 'src/engine/metadata-modules/ai/ai-agent-
|
||||
import { AiModelsModule } from 'src/engine/metadata-modules/ai/ai-models/ai-models.module';
|
||||
import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/field-metadata.module';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
|
||||
import { LogicFunctionModule } from 'src/engine/metadata-modules/logic-function/logic-function.module';
|
||||
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
import { LogicFunctionModule } from 'src/engine/metadata-modules/logic-function/logic-function.module';
|
||||
import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module';
|
||||
import { ViewFieldModule } from 'src/engine/metadata-modules/view-field/view-field.module';
|
||||
import { ViewModule } from 'src/engine/metadata-modules/view/view.module';
|
||||
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
|
||||
|
||||
@@ -45,6 +47,7 @@ import { ToolRegistryService } from './services/tool-registry.service';
|
||||
FieldMetadataModule,
|
||||
PermissionsModule,
|
||||
ViewModule,
|
||||
ViewFieldModule,
|
||||
WorkspaceCacheModule,
|
||||
WorkspaceManyOrAllFlatEntityMapsCacheModule,
|
||||
LogicFunctionModule,
|
||||
@@ -60,6 +63,7 @@ import { ToolRegistryService } from './services/tool-registry.service';
|
||||
MetadataToolProvider,
|
||||
NativeModelToolProvider,
|
||||
LogicFunctionToolProvider,
|
||||
ViewFieldToolProvider,
|
||||
ViewToolProvider,
|
||||
WorkflowToolProvider,
|
||||
{
|
||||
@@ -72,6 +76,7 @@ import { ToolRegistryService } from './services/tool-registry.service';
|
||||
databaseProvider: DatabaseToolProvider,
|
||||
metadataProvider: MetadataToolProvider,
|
||||
logicFunctionProvider: LogicFunctionToolProvider,
|
||||
viewFieldProvider: ViewFieldToolProvider,
|
||||
viewProvider: ViewToolProvider,
|
||||
workflowProvider: WorkflowToolProvider,
|
||||
) => [
|
||||
@@ -80,6 +85,7 @@ import { ToolRegistryService } from './services/tool-registry.service';
|
||||
databaseProvider,
|
||||
metadataProvider,
|
||||
logicFunctionProvider,
|
||||
viewFieldProvider,
|
||||
viewProvider,
|
||||
workflowProvider,
|
||||
],
|
||||
@@ -89,6 +95,7 @@ import { ToolRegistryService } from './services/tool-registry.service';
|
||||
DatabaseToolProvider,
|
||||
MetadataToolProvider,
|
||||
LogicFunctionToolProvider,
|
||||
ViewFieldToolProvider,
|
||||
ViewToolProvider,
|
||||
WorkflowToolProvider,
|
||||
],
|
||||
|
||||
+6
-1
@@ -1,16 +1,21 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ToolCategory } from 'src/engine/core-modules/tool-provider/enums/tool-category.enum';
|
||||
import { type ToolRegistryService } from 'src/engine/core-modules/tool-provider/services/tool-registry.service';
|
||||
import { type ToolIndexEntry } from 'src/engine/core-modules/tool-provider/types/tool-descriptor.type';
|
||||
|
||||
export const GET_TOOL_CATALOG_TOOL_NAME = 'get_tool_catalog';
|
||||
|
||||
const availableCategories = Object.values(ToolCategory)
|
||||
.map((entry) => entry.toString())
|
||||
.join(', ');
|
||||
|
||||
export const getToolCatalogInputSchema = z.object({
|
||||
categories: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe(
|
||||
'Filter by category (e.g. DATABASE_CRUD, METADATA, VIEW, WORKFLOW, DASHBOARD, LOGIC_FUNCTION, ACTION). Omit to get all.',
|
||||
`Filter by category. Available categories: ${availableCategories}. Omit to get all.`,
|
||||
),
|
||||
});
|
||||
|
||||
|
||||
@@ -3,4 +3,5 @@ export enum ToolType {
|
||||
SEND_EMAIL = 'SEND_EMAIL',
|
||||
SEARCH_HELP_CENTER = 'SEARCH_HELP_CENTER',
|
||||
CODE_INTERPRETER = 'CODE_INTERPRETER',
|
||||
NAVIGATE_APP = 'NAVIGATE_APP',
|
||||
}
|
||||
|
||||
@@ -12,7 +12,12 @@ import { DraftEmailTool } from 'src/engine/core-modules/tool/tools/email-tool/dr
|
||||
import { EmailComposerService } from 'src/engine/core-modules/tool/tools/email-tool/email-composer.service';
|
||||
import { SendEmailTool } from 'src/engine/core-modules/tool/tools/email-tool/send-email-tool';
|
||||
import { HttpTool } from 'src/engine/core-modules/tool/tools/http-tool/http-tool';
|
||||
import { NavigateAppTool } from 'src/engine/core-modules/tool/tools/navigate-tool/navigate-app-tool';
|
||||
import { SearchHelpCenterTool } from 'src/engine/core-modules/tool/tools/search-help-center-tool/search-help-center-tool';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
|
||||
import { NavigationMenuItemModule } from 'src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.module';
|
||||
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
|
||||
import { ViewModule } from 'src/engine/metadata-modules/view/view.module';
|
||||
import { MessagingImportManagerModule } from 'src/modules/messaging/message-import-manager/messaging-import-manager.module';
|
||||
import { MessagingSendManagerModule } from 'src/modules/messaging/message-outbound-manager/messaging-send-manager.module';
|
||||
|
||||
@@ -26,6 +31,10 @@ import { MessagingSendManagerModule } from 'src/modules/messaging/message-outbou
|
||||
FileModule,
|
||||
JwtModule,
|
||||
SecureHttpClientModule,
|
||||
ObjectMetadataModule,
|
||||
ViewModule,
|
||||
NavigationMenuItemModule,
|
||||
WorkspaceManyOrAllFlatEntityMapsCacheModule,
|
||||
],
|
||||
providers: [
|
||||
HttpTool,
|
||||
@@ -34,6 +43,7 @@ import { MessagingSendManagerModule } from 'src/modules/messaging/message-outbou
|
||||
EmailComposerService,
|
||||
SearchHelpCenterTool,
|
||||
CodeInterpreterTool,
|
||||
NavigateAppTool,
|
||||
],
|
||||
exports: [
|
||||
HttpTool,
|
||||
@@ -42,6 +52,7 @@ import { MessagingSendManagerModule } from 'src/modules/messaging/message-outbou
|
||||
EmailComposerService,
|
||||
SearchHelpCenterTool,
|
||||
CodeInterpreterTool,
|
||||
NavigateAppTool,
|
||||
],
|
||||
})
|
||||
export class ToolModule {}
|
||||
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const NavigateAppInputZodSchema = z.discriminatedUnion('type', [
|
||||
z.object({
|
||||
type: z
|
||||
.literal('navigateToView')
|
||||
.describe(
|
||||
'Navigate to a specific view by name. ONLY use this type when the user explicitly mentions the word "view" (e.g. "go to the My Companies view", "open view All People"). Do NOT use this for general navigation requests.',
|
||||
),
|
||||
viewName: z
|
||||
.string()
|
||||
.describe(
|
||||
'The name of the view to navigate to (e.g. "My Companies", "All People")',
|
||||
),
|
||||
}),
|
||||
z.object({
|
||||
type: z
|
||||
.literal('navigateToObject')
|
||||
.describe(
|
||||
'Navigate to the default view for an object. This is the PREFERRED and DEFAULT type for all navigation requests unless the user explicitly mentions the word "view".',
|
||||
),
|
||||
objectNameSingular: z
|
||||
.string()
|
||||
.describe(
|
||||
'The singular name of the object to navigate to (e.g. "company", "person", "opportunity")',
|
||||
),
|
||||
}),
|
||||
z.object({
|
||||
type: z
|
||||
.literal('navigateToRecord')
|
||||
.describe(
|
||||
'Navigate to a specific record page. Use this when the user wants to go to a particular record by name (e.g. "go to the company Acme", "open the person John Doe", "show me the deal Enterprise Plan").',
|
||||
),
|
||||
objectNameSingular: z
|
||||
.string()
|
||||
.describe(
|
||||
'The singular name of the object type (e.g. "company", "person", "opportunity")',
|
||||
),
|
||||
recordName: z
|
||||
.string()
|
||||
.describe(
|
||||
'The name or label of the record to navigate to (e.g. "Acme", "John Doe", "Enterprise Plan")',
|
||||
),
|
||||
}),
|
||||
z.object({
|
||||
type: z
|
||||
.literal('wait')
|
||||
.describe(
|
||||
'Wait for a specified duration in milliseconds before continuing. Useful when you need the page to fully load after a navigation before taking further actions (e.g. 2000 for 2 seconds).',
|
||||
),
|
||||
durationMs: z
|
||||
.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.max(30000)
|
||||
.describe(
|
||||
'The duration in milliseconds to wait (e.g. 2000 for 2 seconds). Maximum 30000 (30 seconds).',
|
||||
),
|
||||
}),
|
||||
]);
|
||||
|
||||
export type NavigateAppInput = z.infer<typeof NavigateAppInputZodSchema>;
|
||||
+357
@@ -0,0 +1,357 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { sleep } from 'cloudflare/core';
|
||||
import Fuse from 'fuse.js';
|
||||
import { NavigateAppToolOutput } from 'twenty-shared/ai';
|
||||
import { FieldMetadataType, type ObjectRecord } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import {
|
||||
type NavigateAppInput,
|
||||
NavigateAppInputZodSchema,
|
||||
} from 'src/engine/core-modules/tool/tools/navigate-tool/navigate-app-tool.schema';
|
||||
import { type ToolInput } from 'src/engine/core-modules/tool/types/tool-input.type';
|
||||
import { ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type';
|
||||
import {
|
||||
type Tool,
|
||||
type ToolExecutionContext,
|
||||
} from 'src/engine/core-modules/tool/types/tool.type';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import { NavigationMenuItemService } from 'src/engine/metadata-modules/navigation-menu-item/navigation-menu-item.service';
|
||||
import { ViewService } from 'src/engine/metadata-modules/view/services/view.service';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
|
||||
|
||||
@Injectable()
|
||||
export class NavigateAppTool implements Tool {
|
||||
description = `Navigate the application.
|
||||
Use navigateToRecord when the user wants to go to a specific record by name.
|
||||
Default to navigateToObject for all other navigation requests.
|
||||
Only use navigateToView when the user explicitly mentions the word "view" in their request.
|
||||
If the user asks to wait, use the wait tool with the specified duration.`;
|
||||
|
||||
inputSchema = NavigateAppInputZodSchema;
|
||||
|
||||
constructor(
|
||||
private readonly navigationMenuItemService: NavigationMenuItemService,
|
||||
private readonly viewService: ViewService,
|
||||
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
) {}
|
||||
|
||||
async execute(
|
||||
parameters: ToolInput,
|
||||
context: ToolExecutionContext,
|
||||
): Promise<ToolOutput> {
|
||||
const parseResult = NavigateAppInputZodSchema.safeParse(parameters);
|
||||
|
||||
if (!parseResult.success) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Invalid navigation input',
|
||||
error: parseResult.error.message,
|
||||
};
|
||||
}
|
||||
|
||||
const input: NavigateAppInput = parseResult.data;
|
||||
|
||||
switch (input.type) {
|
||||
case 'navigateToView':
|
||||
return this.navigateToView(
|
||||
input.viewName,
|
||||
context.workspaceId,
|
||||
context.userWorkspaceId,
|
||||
);
|
||||
case 'navigateToObject':
|
||||
return this.navigateToObject(
|
||||
input.objectNameSingular,
|
||||
context.workspaceId,
|
||||
);
|
||||
case 'navigateToRecord':
|
||||
return this.navigateToRecord(
|
||||
input.objectNameSingular,
|
||||
input.recordName,
|
||||
context.workspaceId,
|
||||
);
|
||||
case 'wait':
|
||||
return this.wait(input.durationMs);
|
||||
}
|
||||
}
|
||||
|
||||
private async wait(
|
||||
durationMs: number,
|
||||
): Promise<ToolOutput<NavigateAppToolOutput>> {
|
||||
await sleep(durationMs);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Waited for ${durationMs}ms`,
|
||||
result: {
|
||||
action: 'wait',
|
||||
durationMs,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private async navigateToView(
|
||||
viewName: string,
|
||||
workspaceId: string,
|
||||
userWorkspaceId?: string,
|
||||
): Promise<ToolOutput<NavigateAppToolOutput>> {
|
||||
const views = await this.viewService.findByWorkspaceId(
|
||||
workspaceId,
|
||||
userWorkspaceId,
|
||||
);
|
||||
|
||||
const fuse = new Fuse(views, {
|
||||
keys: ['name'],
|
||||
threshold: 0.4,
|
||||
});
|
||||
|
||||
const results = fuse.search(viewName);
|
||||
const matchingView = results[0]?.item;
|
||||
|
||||
if (!matchingView) {
|
||||
const availableViewNames = views.map((view) => view.name).join(', ');
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: `View "${viewName}" not found`,
|
||||
error: `No view matching "${viewName}" was found in this workspace. Available views: ${availableViewNames}`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Navigating to view "${matchingView.name}"`,
|
||||
result: {
|
||||
action: 'navigateToView',
|
||||
viewName: matchingView.name,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private async navigateToObject(
|
||||
objectNameSingular: string,
|
||||
workspaceId: string,
|
||||
): Promise<ToolOutput<NavigateAppToolOutput>> {
|
||||
const navigationMenuItems = await this.navigationMenuItemService.findAll({
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
const { flatObjectMetadataMaps, flatViewMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: [
|
||||
'flatObjectMetadataMaps',
|
||||
'flatViewMaps',
|
||||
'flatNavigationMenuItemMaps',
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
const availableObjectNames = navigationMenuItems
|
||||
.map((navigationMenuItem) => {
|
||||
if (isDefined(navigationMenuItem.viewId)) {
|
||||
const correspondingViewUniversalIdentifier =
|
||||
flatViewMaps.universalIdentifierById[navigationMenuItem.viewId];
|
||||
|
||||
if (isDefined(correspondingViewUniversalIdentifier)) {
|
||||
const correspondingView =
|
||||
flatViewMaps.byUniversalIdentifier[
|
||||
correspondingViewUniversalIdentifier
|
||||
];
|
||||
|
||||
if (isDefined(correspondingView)) {
|
||||
const correspondingObjectMetadataUniversalIdentifier =
|
||||
flatObjectMetadataMaps.universalIdentifierById[
|
||||
correspondingView.objectMetadataId
|
||||
];
|
||||
|
||||
if (isDefined(correspondingObjectMetadataUniversalIdentifier)) {
|
||||
const correspondingObjectMetadata =
|
||||
flatObjectMetadataMaps.byUniversalIdentifier[
|
||||
correspondingObjectMetadataUniversalIdentifier
|
||||
];
|
||||
|
||||
if (isDefined(correspondingObjectMetadata)) {
|
||||
const correspondingObjectNameSingular =
|
||||
correspondingObjectMetadata.nameSingular;
|
||||
|
||||
return correspondingObjectNameSingular;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
.filter(isDefined);
|
||||
|
||||
const fuse = new Fuse(availableObjectNames, {
|
||||
threshold: 0.6,
|
||||
});
|
||||
|
||||
const results = fuse.search(objectNameSingular.replace(/\s/g, ''));
|
||||
const firstMatchingNavigationItemLabel = results[0]?.item;
|
||||
|
||||
if (!isDefined(firstMatchingNavigationItemLabel)) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Object "${objectNameSingular}" not found`,
|
||||
error: `No object with singular name "${objectNameSingular}" was found in this workspace. Available objects: ${availableObjectNames}`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Navigating to ${firstMatchingNavigationItemLabel} default view`,
|
||||
result: {
|
||||
action: 'navigateToObject',
|
||||
objectNameSingular: firstMatchingNavigationItemLabel,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private async navigateToRecord(
|
||||
objectNameSingular: string,
|
||||
recordName: string,
|
||||
workspaceId: string,
|
||||
): Promise<ToolOutput<NavigateAppToolOutput>> {
|
||||
const { flatObjectMetadataMaps, flatFieldMetadataMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatObjectMetadataMaps', 'flatFieldMetadataMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
const flatObjectMetadata = Object.values(
|
||||
flatObjectMetadataMaps.byUniversalIdentifier,
|
||||
).find(
|
||||
(metadata): metadata is FlatObjectMetadata =>
|
||||
isDefined(metadata) &&
|
||||
metadata.nameSingular === objectNameSingular &&
|
||||
metadata.isActive,
|
||||
);
|
||||
|
||||
if (!isDefined(flatObjectMetadata)) {
|
||||
const availableObjectNames = Object.values(
|
||||
flatObjectMetadataMaps.byUniversalIdentifier,
|
||||
)
|
||||
.filter(
|
||||
(metadata): metadata is FlatObjectMetadata =>
|
||||
isDefined(metadata) && metadata.isActive,
|
||||
)
|
||||
.map((metadata) => metadata.nameSingular)
|
||||
.join(', ');
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: `Object "${objectNameSingular}" not found`,
|
||||
error: `No object with singular name "${objectNameSingular}" was found. Available objects: ${availableObjectNames}`,
|
||||
};
|
||||
}
|
||||
|
||||
if (!isDefined(flatObjectMetadata.labelIdentifierFieldMetadataId)) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Object "${objectNameSingular}" has no label identifier field`,
|
||||
error: `Cannot search records by name for object "${objectNameSingular}" because it has no label identifier field configured.`,
|
||||
};
|
||||
}
|
||||
|
||||
const labelIdentifierField = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: flatObjectMetadata.labelIdentifierFieldMetadataId,
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
});
|
||||
|
||||
if (!isDefined(labelIdentifierField)) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Label identifier field not found for object "${objectNameSingular}"`,
|
||||
error: `The label identifier field metadata could not be resolved for object "${objectNameSingular}".`,
|
||||
};
|
||||
}
|
||||
|
||||
const isFullName =
|
||||
labelIdentifierField.type === FieldMetadataType.FULL_NAME;
|
||||
|
||||
const selectColumns = isFullName
|
||||
? [
|
||||
'id',
|
||||
`${labelIdentifierField.name}FirstName`,
|
||||
`${labelIdentifierField.name}LastName`,
|
||||
]
|
||||
: ['id', labelIdentifierField.name];
|
||||
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
const records =
|
||||
await this.globalWorkspaceOrmManager.executeInWorkspaceContext(
|
||||
async () => {
|
||||
const repository =
|
||||
await this.globalWorkspaceOrmManager.getRepository<ObjectRecord>(
|
||||
workspaceId,
|
||||
objectNameSingular,
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
return repository.find({
|
||||
select: selectColumns,
|
||||
});
|
||||
},
|
||||
authContext,
|
||||
);
|
||||
|
||||
const recordsWithDisplayName = records.map((record) => {
|
||||
let displayName: string;
|
||||
|
||||
if (isFullName) {
|
||||
const firstName =
|
||||
(record[`${labelIdentifierField.name}FirstName`] as string) ?? '';
|
||||
const lastName =
|
||||
(record[`${labelIdentifierField.name}LastName`] as string) ?? '';
|
||||
|
||||
displayName = `${firstName} ${lastName}`.trim();
|
||||
} else {
|
||||
displayName = String(record[labelIdentifierField.name] ?? '');
|
||||
}
|
||||
|
||||
return {
|
||||
id: record.id as string,
|
||||
displayName,
|
||||
};
|
||||
});
|
||||
|
||||
const fuse = new Fuse(recordsWithDisplayName, {
|
||||
keys: ['displayName'],
|
||||
threshold: 0.4,
|
||||
});
|
||||
|
||||
const results = fuse.search(recordName);
|
||||
const matchingRecord = results[0]?.item;
|
||||
|
||||
if (!isDefined(matchingRecord)) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Record "${recordName}" not found in ${objectNameSingular}`,
|
||||
error: `No ${objectNameSingular} record matching "${recordName}" was found.`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Navigating to ${objectNameSingular} record "${matchingRecord.displayName}"`,
|
||||
result: {
|
||||
action: 'navigateToRecord',
|
||||
objectNameSingular,
|
||||
recordId: matchingRecord.id,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
+9
-11
@@ -1,5 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { assertUnreachable } from 'twenty-shared/utils';
|
||||
|
||||
import { COMMON_PRELOAD_TOOLS } from 'src/engine/core-modules/tool-provider/constants/common-preload-tools.const';
|
||||
import { ToolCategory } from 'src/engine/core-modules/tool-provider/enums/tool-category.enum';
|
||||
import { ToolRegistryService } from 'src/engine/core-modules/tool-provider/services/tool-registry.service';
|
||||
@@ -279,15 +281,7 @@ ${otherPreloadedTools.length > 0 ? otherPreloadedTools.map((toolName) => `- \`${
|
||||
|
||||
### Tool Catalog by Category`);
|
||||
|
||||
const categoryOrder = [
|
||||
ToolCategory.DATABASE_CRUD,
|
||||
ToolCategory.ACTION,
|
||||
ToolCategory.WORKFLOW,
|
||||
ToolCategory.DASHBOARD,
|
||||
ToolCategory.METADATA,
|
||||
ToolCategory.VIEW,
|
||||
ToolCategory.LOGIC_FUNCTION,
|
||||
];
|
||||
const categoryOrder = Object.values(ToolCategory);
|
||||
|
||||
for (const category of categoryOrder) {
|
||||
const tools = toolsByCategory.get(category);
|
||||
@@ -321,7 +315,7 @@ ${hasWebSearch ? '3' : '2'}. **Other tools**: First call \`${LEARN_TOOLS_TOOL_NA
|
||||
return sections.join('\n');
|
||||
}
|
||||
|
||||
private getCategoryLabel(category: string): string {
|
||||
private getCategoryLabel(category: ToolCategory): string {
|
||||
switch (category) {
|
||||
case ToolCategory.DATABASE_CRUD:
|
||||
return 'Database Tools (CRUD operations)';
|
||||
@@ -337,8 +331,12 @@ ${hasWebSearch ? '3' : '2'}. **Other tools**: First call \`${LEARN_TOOLS_TOOL_NA
|
||||
return 'Dashboard Tools (create/manage dashboards)';
|
||||
case ToolCategory.LOGIC_FUNCTION:
|
||||
return 'Logic Functions (custom tools)';
|
||||
case ToolCategory.NATIVE_MODEL:
|
||||
return 'Native Model Capabilities (e.g. web search)';
|
||||
case ToolCategory.VIEW_FIELD:
|
||||
return 'View Field Tools (manage view columns)';
|
||||
default:
|
||||
return category;
|
||||
return assertUnreachable(category);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+96
@@ -104,6 +104,22 @@ const DeleteFieldMetadataInputSchema = z.object({
|
||||
id: z.string().uuid().describe('ID of the field to delete'),
|
||||
});
|
||||
|
||||
const CreateManyFieldMetadataInputSchema = z.object({
|
||||
fields: z
|
||||
.array(CreateFieldMetadataInputSchema)
|
||||
.min(1)
|
||||
.max(20)
|
||||
.describe('Array of field metadata to create (1-20 items).'),
|
||||
});
|
||||
|
||||
const UpdateManyFieldMetadataInputSchema = z.object({
|
||||
fields: z
|
||||
.array(UpdateFieldMetadataInputSchema)
|
||||
.min(1)
|
||||
.max(20)
|
||||
.describe('Array of field metadata updates to apply (1-20 items).'),
|
||||
});
|
||||
|
||||
@Injectable()
|
||||
export class FieldMetadataToolsFactory {
|
||||
constructor(private readonly fieldMetadataService: FieldMetadataService) {}
|
||||
@@ -229,6 +245,86 @@ export class FieldMetadataToolsFactory {
|
||||
}
|
||||
},
|
||||
},
|
||||
create_many_field_metadata: {
|
||||
description:
|
||||
'Create multiple field metadata at once on one or more objects. More efficient than calling create_field_metadata multiple times. Each item follows the same schema as create_field_metadata.',
|
||||
inputSchema: CreateManyFieldMetadataInputSchema,
|
||||
execute: async (parameters: {
|
||||
fields: Array<{
|
||||
objectMetadataId: string;
|
||||
type: FieldMetadataType;
|
||||
name: string;
|
||||
label: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
isNullable?: boolean;
|
||||
isUnique?: boolean;
|
||||
defaultValue?: unknown;
|
||||
options?: unknown;
|
||||
settings?: unknown;
|
||||
isLabelSyncedWithName?: boolean;
|
||||
isRemoteCreation?: boolean;
|
||||
relationCreationPayload?: unknown;
|
||||
}>;
|
||||
}) => {
|
||||
try {
|
||||
await this.fieldMetadataService.createManyFields({
|
||||
createFieldInputs: parameters.fields as Parameters<
|
||||
typeof this.fieldMetadataService.createManyFields
|
||||
>[0]['createFieldInputs'],
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error instanceof WorkspaceMigrationBuilderException) {
|
||||
throw new Error(formatValidationErrors(error));
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
},
|
||||
update_many_field_metadata: {
|
||||
description:
|
||||
'Update multiple field metadata at once. More efficient than calling update_field_metadata multiple times. Each item must include the field ID and the properties to update.',
|
||||
inputSchema: UpdateManyFieldMetadataInputSchema,
|
||||
execute: async (parameters: {
|
||||
fields: Array<{
|
||||
id: string;
|
||||
name?: string;
|
||||
label?: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
isActive?: boolean;
|
||||
isNullable?: boolean;
|
||||
isUnique?: boolean;
|
||||
defaultValue?: unknown;
|
||||
options?: unknown;
|
||||
settings?: unknown;
|
||||
isLabelSyncedWithName?: boolean;
|
||||
}>;
|
||||
}) => {
|
||||
try {
|
||||
await Promise.all(
|
||||
parameters.fields.map(async ({ id, ...update }) => {
|
||||
await this.fieldMetadataService.updateOneField({
|
||||
updateFieldInput: { id, ...update } as Parameters<
|
||||
typeof this.fieldMetadataService.updateOneField
|
||||
>[0]['updateFieldInput'],
|
||||
workspaceId,
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error instanceof WorkspaceMigrationBuilderException) {
|
||||
throw new Error(formatValidationErrors(error));
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+93
@@ -81,6 +81,22 @@ const DeleteObjectMetadataInputSchema = z.object({
|
||||
id: z.string().uuid().describe('ID of the object to delete'),
|
||||
});
|
||||
|
||||
const CreateManyObjectMetadataInputSchema = z.object({
|
||||
objects: z
|
||||
.array(CreateObjectMetadataInputSchema)
|
||||
.min(1)
|
||||
.max(20)
|
||||
.describe('Array of object metadata to create (1-20 items).'),
|
||||
});
|
||||
|
||||
const UpdateManyObjectMetadataInputSchema = z.object({
|
||||
objects: z
|
||||
.array(UpdateObjectMetadataInputSchema)
|
||||
.min(1)
|
||||
.max(20)
|
||||
.describe('Array of object metadata updates to apply (1-20 items).'),
|
||||
});
|
||||
|
||||
@Injectable()
|
||||
export class ObjectMetadataToolsFactory {
|
||||
constructor(private readonly objectMetadataService: ObjectMetadataService) {}
|
||||
@@ -202,6 +218,83 @@ export class ObjectMetadataToolsFactory {
|
||||
}
|
||||
},
|
||||
},
|
||||
create_many_object_metadata: {
|
||||
description:
|
||||
'Create multiple object metadata at once in the workspace data model. More efficient than calling create_object_metadata multiple times. Each item follows the same schema as create_object_metadata.',
|
||||
inputSchema: CreateManyObjectMetadataInputSchema,
|
||||
execute: async (parameters: {
|
||||
objects: Array<{
|
||||
nameSingular: string;
|
||||
namePlural: string;
|
||||
labelSingular: string;
|
||||
labelPlural: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
shortcut?: string;
|
||||
isRemote?: boolean;
|
||||
isLabelSyncedWithName?: boolean;
|
||||
}>;
|
||||
}) => {
|
||||
try {
|
||||
await Promise.all(
|
||||
parameters.objects.map(async (createObjectInput) => {
|
||||
await this.objectMetadataService.createOneObject({
|
||||
createObjectInput: createObjectInput as Parameters<
|
||||
typeof this.objectMetadataService.createOneObject
|
||||
>[0]['createObjectInput'],
|
||||
workspaceId,
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error instanceof WorkspaceMigrationBuilderException) {
|
||||
throw new Error(formatValidationErrors(error));
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
},
|
||||
update_many_object_metadata: {
|
||||
description:
|
||||
'Update multiple object metadata at once. More efficient than calling update_object_metadata multiple times. Each item must include the object ID and the properties to update.',
|
||||
inputSchema: UpdateManyObjectMetadataInputSchema,
|
||||
execute: async (parameters: {
|
||||
objects: Array<{
|
||||
id: string;
|
||||
labelSingular?: string;
|
||||
labelPlural?: string;
|
||||
nameSingular?: string;
|
||||
namePlural?: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
shortcut?: string;
|
||||
isActive?: boolean;
|
||||
labelIdentifierFieldMetadataId?: string;
|
||||
imageIdentifierFieldMetadataId?: string;
|
||||
isLabelSyncedWithName?: boolean;
|
||||
}>;
|
||||
}) => {
|
||||
try {
|
||||
await Promise.all(
|
||||
parameters.objects.map(async ({ id, ...update }) => {
|
||||
await this.objectMetadataService.updateOneObject({
|
||||
updateObjectInput: { id, update },
|
||||
workspaceId,
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error instanceof WorkspaceMigrationBuilderException) {
|
||||
throw new Error(formatValidationErrors(error));
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+357
@@ -0,0 +1,357 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { type ToolSet } from 'ai';
|
||||
import { AggregateOperations } from 'twenty-shared/types';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { formatValidationErrors } from 'src/engine/core-modules/tool-provider/utils/format-validation-errors.util';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { ViewFieldService } from 'src/engine/metadata-modules/view-field/services/view-field.service';
|
||||
import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception';
|
||||
|
||||
const GetViewFieldsInputSchema = z.object({
|
||||
viewId: z
|
||||
.string()
|
||||
.uuid()
|
||||
.describe(
|
||||
'The ID of the view to list fields for. Obtain this from get_views.',
|
||||
),
|
||||
});
|
||||
|
||||
const CreateViewFieldInputSchema = z.object({
|
||||
viewId: z.string().uuid().describe('The ID of the view to add the field to.'),
|
||||
fieldMetadataId: z
|
||||
.string()
|
||||
.uuid()
|
||||
.describe(
|
||||
'The ID of the field metadata to add. Use get_field_metadata to find available fields.',
|
||||
),
|
||||
isVisible: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(true)
|
||||
.describe('Whether the field is visible in the view.'),
|
||||
size: z
|
||||
.number()
|
||||
.int()
|
||||
.optional()
|
||||
.default(150)
|
||||
.describe('Column width in pixels.'),
|
||||
position: z
|
||||
.number()
|
||||
.optional()
|
||||
.default(0)
|
||||
.describe('Position of the field in the view (0-based).'),
|
||||
aggregateOperation: z
|
||||
.enum(Object.values(AggregateOperations) as [string, ...string[]])
|
||||
.optional()
|
||||
.describe(
|
||||
'Aggregate operation for this field (e.g., "SUM", "AVG", "COUNT").',
|
||||
),
|
||||
});
|
||||
|
||||
const UpdateViewFieldInputSchema = z.object({
|
||||
id: z
|
||||
.string()
|
||||
.uuid()
|
||||
.describe(
|
||||
'The ID of the view field to update. Obtain this from get_view_fields.',
|
||||
),
|
||||
isVisible: z.boolean().optional().describe('Whether the field is visible.'),
|
||||
size: z.number().int().optional().describe('Column width in pixels.'),
|
||||
position: z.number().optional().describe('Position of the field.'),
|
||||
aggregateOperation: z
|
||||
.enum(Object.values(AggregateOperations) as [string, ...string[]])
|
||||
.optional()
|
||||
.describe('Aggregate operation for this field.'),
|
||||
});
|
||||
|
||||
const DeleteViewFieldInputSchema = z.object({
|
||||
id: z
|
||||
.string()
|
||||
.uuid()
|
||||
.describe(
|
||||
'The ID of the view field to delete. Obtain this from get_view_fields.',
|
||||
),
|
||||
});
|
||||
|
||||
const CreateManyViewFieldsInputSchema = z.object({
|
||||
viewFields: z
|
||||
.array(CreateViewFieldInputSchema)
|
||||
.min(1)
|
||||
.max(50)
|
||||
.describe('Array of view fields to create (1-50 items).'),
|
||||
});
|
||||
|
||||
const UpdateManyViewFieldsInputSchema = z.object({
|
||||
viewFields: z
|
||||
.array(UpdateViewFieldInputSchema)
|
||||
.min(1)
|
||||
.max(50)
|
||||
.describe('Array of view field updates to apply (1-50 items).'),
|
||||
});
|
||||
|
||||
@Injectable()
|
||||
export class ViewFieldToolsFactory {
|
||||
constructor(
|
||||
private readonly viewFieldService: ViewFieldService,
|
||||
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
) {}
|
||||
|
||||
private async resolveFieldName(
|
||||
workspaceId: string,
|
||||
fieldMetadataId: string,
|
||||
): Promise<string | undefined> {
|
||||
const { flatFieldMetadataMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatFieldMetadataMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
const universalIdentifier =
|
||||
flatFieldMetadataMaps.universalIdentifierById[fieldMetadataId];
|
||||
|
||||
if (!universalIdentifier) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return flatFieldMetadataMaps.byUniversalIdentifier[universalIdentifier]
|
||||
?.name;
|
||||
}
|
||||
|
||||
generateReadTools(workspaceId: string): ToolSet {
|
||||
return {
|
||||
get_view_fields: {
|
||||
description:
|
||||
'List the columns (fields) displayed in a specific view. A view field controls which columns appear in a table or kanban view, their visibility, width, position, and aggregate operation. Use get_views first to find the view ID, then call this to inspect its column configuration.',
|
||||
|
||||
inputSchema: GetViewFieldsInputSchema,
|
||||
execute: async (parameters: { viewId: string }) => {
|
||||
const viewFields = await this.viewFieldService.findByViewId(
|
||||
workspaceId,
|
||||
parameters.viewId,
|
||||
);
|
||||
|
||||
const viewFieldsWithNames = await Promise.all(
|
||||
viewFields.map(async (viewField) => {
|
||||
const fieldName = await this.resolveFieldName(
|
||||
workspaceId,
|
||||
viewField.fieldMetadataId,
|
||||
);
|
||||
|
||||
return {
|
||||
id: viewField.id,
|
||||
fieldMetadataId: viewField.fieldMetadataId,
|
||||
fieldName: fieldName ?? null,
|
||||
viewId: viewField.viewId,
|
||||
isVisible: viewField.isVisible,
|
||||
size: viewField.size,
|
||||
position: viewField.position,
|
||||
aggregateOperation: viewField.aggregateOperation,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
return viewFieldsWithNames;
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
generateWriteTools(workspaceId: string): ToolSet {
|
||||
return {
|
||||
create_view_field: {
|
||||
description:
|
||||
'Add a new column to a view. View fields define which columns are shown in table or kanban views. First call get_field_metadata to find the fieldMetadataId of the column to add, and get_views to find the target viewId.',
|
||||
|
||||
inputSchema: CreateViewFieldInputSchema,
|
||||
execute: async (parameters: {
|
||||
viewId: string;
|
||||
fieldMetadataId: string;
|
||||
isVisible?: boolean;
|
||||
size?: number;
|
||||
position?: number;
|
||||
aggregateOperation?: string;
|
||||
}) => {
|
||||
try {
|
||||
const viewField = await this.viewFieldService.createOne({
|
||||
createViewFieldInput: {
|
||||
viewId: parameters.viewId,
|
||||
fieldMetadataId: parameters.fieldMetadataId,
|
||||
isVisible: parameters.isVisible ?? true,
|
||||
size: parameters.size ?? 150,
|
||||
position: parameters.position ?? 0,
|
||||
aggregateOperation:
|
||||
parameters.aggregateOperation as AggregateOperations,
|
||||
},
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
return {
|
||||
id: viewField.id,
|
||||
fieldMetadataId: viewField.fieldMetadataId,
|
||||
viewId: viewField.viewId,
|
||||
isVisible: viewField.isVisible,
|
||||
size: viewField.size,
|
||||
position: viewField.position,
|
||||
aggregateOperation: viewField.aggregateOperation,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof WorkspaceMigrationBuilderException) {
|
||||
throw new Error(formatValidationErrors(error));
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
},
|
||||
update_view_field: {
|
||||
description:
|
||||
"Update properties of a column in a view. You can change its visibility, width (size in pixels), display position, or aggregate operation. Use get_view_fields to find the view field ID. Constraints: position must not be -1, must not precede the label identifier field, and must not conflict with another field's position.",
|
||||
|
||||
inputSchema: UpdateViewFieldInputSchema,
|
||||
execute: async (parameters: {
|
||||
id: string;
|
||||
isVisible?: boolean;
|
||||
size?: number;
|
||||
position?: number;
|
||||
aggregateOperation?: string;
|
||||
}) => {
|
||||
try {
|
||||
const viewField = await this.viewFieldService.updateOne({
|
||||
updateViewFieldInput: {
|
||||
id: parameters.id,
|
||||
update: {
|
||||
isVisible: parameters.isVisible,
|
||||
size: parameters.size,
|
||||
position: parameters.position,
|
||||
aggregateOperation:
|
||||
parameters.aggregateOperation as AggregateOperations,
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
return {
|
||||
id: viewField.id,
|
||||
fieldMetadataId: viewField.fieldMetadataId,
|
||||
viewId: viewField.viewId,
|
||||
isVisible: viewField.isVisible,
|
||||
size: viewField.size,
|
||||
position: viewField.position,
|
||||
aggregateOperation: viewField.aggregateOperation,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof WorkspaceMigrationBuilderException) {
|
||||
throw new Error(formatValidationErrors(error));
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
},
|
||||
delete_view_field: {
|
||||
description:
|
||||
"Remove a column from a view. This removes the field from the view's displayed columns. Use get_view_fields to find the view field ID to delete.",
|
||||
|
||||
inputSchema: DeleteViewFieldInputSchema,
|
||||
execute: async (parameters: { id: string }) => {
|
||||
try {
|
||||
const viewField = await this.viewFieldService.deleteOne({
|
||||
deleteViewFieldInput: { id: parameters.id },
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
return {
|
||||
id: viewField.id,
|
||||
deleted: true,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof WorkspaceMigrationBuilderException) {
|
||||
throw new Error(formatValidationErrors(error));
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
},
|
||||
create_many_view_fields: {
|
||||
description:
|
||||
'Add multiple columns to a view at once. More efficient than calling create_view_field multiple times. Each item follows the same schema as create_view_field. All view fields can target the same or different views.',
|
||||
inputSchema: CreateManyViewFieldsInputSchema,
|
||||
execute: async (parameters: {
|
||||
viewFields: Array<{
|
||||
viewId: string;
|
||||
fieldMetadataId: string;
|
||||
isVisible?: boolean;
|
||||
size?: number;
|
||||
position?: number;
|
||||
aggregateOperation?: string;
|
||||
}>;
|
||||
}) => {
|
||||
try {
|
||||
await this.viewFieldService.createMany({
|
||||
createViewFieldInputs: parameters.viewFields.map((viewField) => ({
|
||||
viewId: viewField.viewId,
|
||||
fieldMetadataId: viewField.fieldMetadataId,
|
||||
isVisible: viewField.isVisible ?? true,
|
||||
size: viewField.size ?? 150,
|
||||
position: viewField.position ?? 0,
|
||||
aggregateOperation:
|
||||
viewField.aggregateOperation as AggregateOperations,
|
||||
})),
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error instanceof WorkspaceMigrationBuilderException) {
|
||||
throw new Error(formatValidationErrors(error));
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
},
|
||||
update_many_view_fields: {
|
||||
description:
|
||||
'Update multiple columns in a view at once. More efficient than calling update_view_field multiple times. Each item must include the view field ID and properties to update. Same constraints as update_view_field apply to each item.',
|
||||
inputSchema: UpdateManyViewFieldsInputSchema,
|
||||
execute: async (parameters: {
|
||||
viewFields: Array<{
|
||||
id: string;
|
||||
isVisible?: boolean;
|
||||
size?: number;
|
||||
position?: number;
|
||||
aggregateOperation?: string;
|
||||
}>;
|
||||
}) => {
|
||||
try {
|
||||
await Promise.all(
|
||||
parameters.viewFields.map(async (viewField) => {
|
||||
await this.viewFieldService.updateOne({
|
||||
updateViewFieldInput: {
|
||||
id: viewField.id,
|
||||
update: {
|
||||
isVisible: viewField.isVisible,
|
||||
size: viewField.size,
|
||||
position: viewField.position,
|
||||
aggregateOperation:
|
||||
viewField.aggregateOperation as AggregateOperations,
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error instanceof WorkspaceMigrationBuilderException) {
|
||||
throw new Error(formatValidationErrors(error));
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import { ViewFieldController } from 'src/engine/metadata-modules/view-field/cont
|
||||
import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity';
|
||||
import { ViewFieldResolver } from 'src/engine/metadata-modules/view-field/resolvers/view-field.resolver';
|
||||
import { ViewFieldService } from 'src/engine/metadata-modules/view-field/services/view-field.service';
|
||||
import { ViewFieldToolsFactory } from 'src/engine/metadata-modules/view-field/tools/view-field-tools.factory';
|
||||
import { ViewPermissionsModule } from 'src/engine/metadata-modules/view-permissions/view-permissions.module';
|
||||
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
|
||||
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
|
||||
@@ -24,7 +25,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
|
||||
ViewPermissionsModule,
|
||||
],
|
||||
controllers: [ViewFieldController],
|
||||
providers: [ViewFieldResolver, ViewFieldService],
|
||||
exports: [ViewFieldService],
|
||||
providers: [ViewFieldResolver, ViewFieldService, ViewFieldToolsFactory],
|
||||
exports: [ViewFieldService, ViewFieldToolsFactory],
|
||||
})
|
||||
export class ViewFieldModule {}
|
||||
|
||||
+4
@@ -366,6 +366,10 @@ export const seedAgents = async ({
|
||||
schemaName,
|
||||
workspaceId,
|
||||
}: SeedAgentsArgs) => {
|
||||
if (workspaceId === SEED_APPLE_WORKSPACE_ID) {
|
||||
return;
|
||||
}
|
||||
|
||||
const threadId = await seedChatThreads({
|
||||
queryRunner,
|
||||
schemaName,
|
||||
|
||||
+3
@@ -29,6 +29,9 @@ export const STANDARD_SKILL = {
|
||||
pptx: {
|
||||
universalIdentifier: '20202020-13b5-4e60-9359-b8519ef1c07d',
|
||||
},
|
||||
'workspace-demo-seeding': {
|
||||
universalIdentifier: '20202020-c81b-4af8-9255-4c34bd0eac9c',
|
||||
},
|
||||
} as const satisfies Record<
|
||||
string,
|
||||
{
|
||||
|
||||
+52
@@ -138,6 +138,58 @@ Prioritize data integrity and provide clear feedback on operations performed.`,
|
||||
},
|
||||
}),
|
||||
|
||||
'workspace-demo-seeding': (args: Omit<CreateStandardSkillArgs, 'context'>) =>
|
||||
createStandardSkillFlatMetadata({
|
||||
...args,
|
||||
context: {
|
||||
skillName: 'workspace-demo-seeding',
|
||||
name: 'workspace-demo-seeding',
|
||||
label: 'Workspace Demo Seeding',
|
||||
description:
|
||||
'Seeding demo metadata and data for workspace setup and testing purposes',
|
||||
icon: 'IconDatabase',
|
||||
content: `# Workspace Demo Seeding Skill
|
||||
You will create a demo workspace that fits a particular type of company given by the user.
|
||||
|
||||
Do not ask the user for more information, just be creative with the objects and fields, but stay professional and coherent.
|
||||
|
||||
Create relations fields between objects, for example a car repair shop workspace would have objects for cars, employees, repairs, customers, and the relevant relations between them.
|
||||
|
||||
DO NOT USE code-interpreter tool at all. Prefer more steps.
|
||||
|
||||
LIMIT TO 3 OBJECTS FOR DEMO, AND 3 FIELDS FOR EACH OBJECT, to avoid bugs.
|
||||
|
||||
For the fields you will create, make sure to create a good variety of field types to showcase the different capabilities of the platform, for example:
|
||||
- Create SELECT and SELECT_MULTIPLE field types for building demo board index views and table with groups views
|
||||
- Create DATE_TIME fields to be able to create calendar views
|
||||
- Create CURRENCY and NUMERIC fields for graphs
|
||||
|
||||
Here are the steps for you to work properly :
|
||||
- Proceed object by object, for each object.
|
||||
- Create the object with the right tool, DO THIS FIRST
|
||||
- Wait 3 seconds before navigating, for the view to be populated by the backend
|
||||
- Navigate to its default view
|
||||
- Then create each relevant field metadata one by one, and create a view field for each of them, reorder them to the start so we see them.
|
||||
- Then seed mock data relevant :
|
||||
- use the tool that is related to the object, look for tools, create_my_new_object, create_many_of_my_new_object, look again in tools, don't use http
|
||||
- between 20 and 50
|
||||
- with a coherent combination of values
|
||||
- proceed with the relevant tools for each object, do not use code-interpreter
|
||||
- navigate to each default view before seeding an object, so the user can see what happens.
|
||||
|
||||
After you've finished with this part, let's proceed to the dashboard creation. We will create a dashboard with 4 graphs.
|
||||
- Navigate to the dashboard list default view
|
||||
- Create a new dashboard
|
||||
- Navigate to the dashboard page
|
||||
- Create 4 graphs :
|
||||
- For each graph, find a relevant amount for y axis, a relevant date or select field for x axis, and if necessary a relevant group by stack
|
||||
- Change the name of each graph so it is relevant
|
||||
- Turn on the labels on the graphs
|
||||
`,
|
||||
isCustom: false,
|
||||
},
|
||||
}),
|
||||
|
||||
'dashboard-building': (args: Omit<CreateStandardSkillArgs, 'context'>) =>
|
||||
createStandardSkillFlatMetadata({
|
||||
...args,
|
||||
|
||||
Reference in New Issue
Block a user