[AI] Add group_by_* database tools and centralize groupBy validation (#19406)

closes
https://discord.com/channels/1130383047699738754/1488990242873806868



https://github.com/user-attachments/assets/2b2bbfba-3fa6-4114-9a26-96a61599d748

<img width="729" height="1283" alt="CleanShot 2026-04-07 at 20 43 06"
src="https://github.com/user-attachments/assets/815efb97-81a0-44ea-8d79-b3ce7d5b00b6"
/>


<img width="708" height="1266" alt="CleanShot 2026-04-07 at 20 40 13"
src="https://github.com/user-attachments/assets/692366bc-b629-4d9f-b6b8-ab670d5ad046"
/>

<img width="665" height="3524" alt="CleanShot 2026-04-07 at 20 42 00"
src="https://github.com/user-attachments/assets/5e844e0f-7835-47a8-9d20-a5baddc0992d"
/>
This commit is contained in:
nitin
2026-04-10 14:15:18 +05:30
committed by GitHub
parent 43ce396152
commit f13e7e01fe
32 changed files with 1504 additions and 258 deletions
@@ -33,6 +33,7 @@ For simple CRUD operations (find/create/update/delete a record), you do NOT need
- NEVER guess or construct API URLs — always use the appropriate database tool
- The \`http_request\` tool is ONLY for external third-party APIs (not for Twenty's own data)
- If you need to look up a record, learn and execute the corresponding find_one_* or find_many_* tool
- For comparative/grouped analytics questions (by/per/top/most/least/average/total/ranking), use \`group_by_*\` instead of \`find_*\`; if multiple metrics are needed, run multiple \`group_by_*\` calls with the same dimensions and merge results.
## Data Efficiency
@@ -0,0 +1,140 @@
import { type CompositeProperty, FieldMetadataType } from 'twenty-shared/types';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { getGroupableSubFieldsForCompositeType } from 'src/engine/metadata-modules/field-metadata/utils/get-groupable-sub-fields-for-composite-type.util';
import { isCompositePropertySupportedInGroupBy } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-property-supported-in-group-by.util';
import { isFlatFieldMetadataSupportedInGroupBy } from 'src/engine/metadata-modules/field-metadata/utils/is-supported-in-group-by.util';
const buildFlatFieldMetadata = (
type: FieldMetadataType,
name = 'field',
isSystem = false,
): FlatFieldMetadata => ({ type, name, isSystem }) as FlatFieldMetadata;
const buildCompositeProperty = (
type: FieldMetadataType,
hidden: CompositeProperty['hidden'] = false,
): CompositeProperty => ({
name: 'subField',
type,
hidden,
isRequired: false,
});
describe('isFlatFieldMetadataSupportedInGroupBy', () => {
it('returns false for low-level field types', () => {
expect(
isFlatFieldMetadataSupportedInGroupBy(
buildFlatFieldMetadata(FieldMetadataType.TS_VECTOR),
),
).toBe(false);
expect(
isFlatFieldMetadataSupportedInGroupBy(
buildFlatFieldMetadata(FieldMetadataType.RAW_JSON),
),
).toBe(false);
expect(
isFlatFieldMetadataSupportedInGroupBy(
buildFlatFieldMetadata(FieldMetadataType.FILES),
),
).toBe(false);
expect(
isFlatFieldMetadataSupportedInGroupBy(
buildFlatFieldMetadata(FieldMetadataType.POSITION),
),
).toBe(false);
});
it('returns true for regular field types', () => {
expect(
isFlatFieldMetadataSupportedInGroupBy(
buildFlatFieldMetadata(FieldMetadataType.TEXT),
),
).toBe(true);
});
it('returns false for internal/system field names', () => {
expect(
isFlatFieldMetadataSupportedInGroupBy(
buildFlatFieldMetadata(FieldMetadataType.TEXT, 'id'),
),
).toBe(false);
expect(
isFlatFieldMetadataSupportedInGroupBy(
buildFlatFieldMetadata(FieldMetadataType.DATE_TIME, 'deletedAt'),
),
).toBe(false);
expect(
isFlatFieldMetadataSupportedInGroupBy(
buildFlatFieldMetadata(FieldMetadataType.TS_VECTOR, 'searchVector'),
),
).toBe(false);
expect(
isFlatFieldMetadataSupportedInGroupBy(
buildFlatFieldMetadata(FieldMetadataType.ACTOR, 'createdBy'),
),
).toBe(false);
});
it('returns true for createdAt and updatedAt date fields even if system', () => {
expect(
isFlatFieldMetadataSupportedInGroupBy(
buildFlatFieldMetadata(FieldMetadataType.DATE_TIME, 'createdAt', true),
),
).toBe(true);
expect(
isFlatFieldMetadataSupportedInGroupBy(
buildFlatFieldMetadata(FieldMetadataType.DATE_TIME, 'updatedAt', true),
),
).toBe(true);
});
it('returns false for other system fields', () => {
expect(
isFlatFieldMetadataSupportedInGroupBy(
buildFlatFieldMetadata(
FieldMetadataType.TEXT,
'customSystemField',
true,
),
),
).toBe(false);
});
});
describe('isCompositePropertySupportedInGroupBy', () => {
it('returns false for hidden or raw_json composite properties', () => {
expect(
isCompositePropertySupportedInGroupBy(
buildCompositeProperty(FieldMetadataType.TEXT, true),
),
).toBe(false);
expect(
isCompositePropertySupportedInGroupBy(
buildCompositeProperty(FieldMetadataType.RAW_JSON),
),
).toBe(false);
});
it('returns true for visible non-raw_json composite properties', () => {
expect(
isCompositePropertySupportedInGroupBy(
buildCompositeProperty(FieldMetadataType.TEXT),
),
).toBe(true);
});
});
describe('getGroupableSubFieldsForCompositeType', () => {
it('returns null for non-composite field types', () => {
expect(getGroupableSubFieldsForCompositeType(FieldMetadataType.TEXT)).toBe(
null,
);
});
it('returns supported subfields for composite field types', () => {
expect(
getGroupableSubFieldsForCompositeType(FieldMetadataType.CURRENCY),
).toEqual(expect.arrayContaining(['amountMicros', 'currencyCode']));
});
});
@@ -0,0 +1,20 @@
import {
compositeTypeDefinitions,
FieldMetadataType,
} from 'twenty-shared/types';
import { isCompositePropertySupportedInGroupBy } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-property-supported-in-group-by.util';
export const getGroupableSubFieldsForCompositeType = (
type: FieldMetadataType,
): string[] | null => {
const compositeTypeDefinition = compositeTypeDefinitions.get(type);
if (!compositeTypeDefinition) {
return null;
}
return compositeTypeDefinition.properties
.filter(isCompositePropertySupportedInGroupBy)
.map((property) => property.name);
};
@@ -0,0 +1,9 @@
import { type CompositeProperty, FieldMetadataType } from 'twenty-shared/types';
export const isCompositePropertySupportedInGroupBy = (
property: CompositeProperty,
): boolean => {
return (
property.hidden !== true && property.type !== FieldMetadataType.RAW_JSON
);
};
@@ -0,0 +1,34 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { isFieldMetadataDateKind } from 'twenty-shared/utils';
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { shouldExcludeFieldFromAgentToolSchema } from 'src/engine/metadata-modules/field-metadata/utils/should-exclude-field-from-agent-tool-schema.util';
const NON_GROUPABLE_FIELD_TYPES = new Set<FieldMetadataType>([
FieldMetadataType.TS_VECTOR,
FieldMetadataType.RAW_JSON,
FieldMetadataType.FILES,
FieldMetadataType.POSITION,
]);
const ALWAYS_GROUPABLE_SYSTEM_DATE_FIELD_NAMES = new Set([
'createdAt',
'updatedAt',
]);
export const isFlatFieldMetadataSupportedInGroupBy = (
fieldMetadata: FlatFieldMetadata,
): boolean => {
const isAlwaysGroupableSystemDateField =
ALWAYS_GROUPABLE_SYSTEM_DATE_FIELD_NAMES.has(fieldMetadata.name) &&
isFieldMetadataDateKind(fieldMetadata.type);
if (
!isAlwaysGroupableSystemDateField &&
shouldExcludeFieldFromAgentToolSchema(fieldMetadata)
) {
return false;
}
return !NON_GROUPABLE_FIELD_TYPES.has(fieldMetadata.type);
};