fix(ai) - optimize crud tools (#21133)
- **Add delete many**, `delete_many_{object}` added alongside the
existing `delete_one_{object}`.
- **Uniformize naming**, crud module, type names, and MCP helper
constants renamed for consistency.
- **Optimize tool schema (learn phase)**
- `find_many(_companies)`: **7 158 → 2 700 tokens**
- `find_one(_company)`: **280 → 126 tokens**
- ....
- Main mechanism: `reused: 'ref'` (line 7 of
`to-tool-json-schema.util.ts`). Zod walks the schema tree, tracks which
Zod schema instances appear more than once, and emits each reused
instance exactly once in `$defs`, replacing all subsequent occurrences
with a `$ref`. Works because filter and value schemas are now extracted
as shared objects.
- **Optimize system prompt (tool catalog)**, DATABASE_CRUD section
restructured to list operation patterns (`find_many_{object}`, …) once +
objects once, instead of the full N×M cross-product of tool names.
- **Optimize execute_tool**, shared record-properties schema (same
`$defs` deduplication applies at call time); introduced `upsert_many`;
added `selectedFields` to `find_*` so the agent only fetches the fields
it needs.
This commit is contained in:
+2
-1
@@ -7,7 +7,8 @@ export const WORKFLOW_SYSTEM_PROMPTS = {
|
||||
|
||||
Tool usage strategy:
|
||||
- Chain multiple tools to solve complex tasks
|
||||
- Prefer batch tools (\`create_many_*\`, \`update_many_*\`, etc.) over looping single-item calls
|
||||
- Prefer batch tools (\`create_many_*\`, \`update_many_*\`, \`upsert_many_*\`, etc.) over looping single-item calls
|
||||
- Use \`upsert_many_*\` instead of \`update_many_*\` when records have different data to set individually, or when some records may not exist yet
|
||||
- If a tool fails, try alternative approaches
|
||||
- Use results from one tool to inform the next
|
||||
- Don't give up after first failure - be persistent
|
||||
|
||||
+5
-4
@@ -29,11 +29,12 @@ For simple CRUD operations (find/create/update/delete a record), you do NOT need
|
||||
|
||||
## Database vs HTTP Tools
|
||||
|
||||
- Use database tools (find_*, create_*, update_*, delete_*) for ALL Twenty CRM data operations
|
||||
- Use database tools (find_many_*, find_one_*, create_one_*, create_many_*, update_one_*, update_many_*, upsert_many_*, delete_one_*, delete_many_*) for ALL Twenty CRM data operations
|
||||
- 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.
|
||||
- If you need to look up a record by ID, use find_one_*; to search with filters, use find_many_*
|
||||
- For comparative/grouped analytics questions (by/per/top/most/least/average/total/ranking), use \`group_by_*\` instead of \`find_many_*\`; if multiple metrics are needed, run multiple \`group_by_*\` calls with the same dimensions and merge results.
|
||||
- **update_many_* vs upsert_many_***: use \`update_many_*\` when ALL matched records get the SAME data (e.g. mark all as closed). Use \`upsert_many_*\` when each record has different data to set, or when some records may not exist yet (insert-or-update per record).
|
||||
|
||||
## Data Efficiency
|
||||
|
||||
@@ -41,7 +42,7 @@ For simple CRUD operations (find/create/update/delete a record), you do NOT need
|
||||
- Always apply filters to narrow results — don't fetch all records of a type.
|
||||
- Fetch one type of data at a time and check if you have what you need before fetching more.
|
||||
- Every record returned consumes context. Fetching too many records at once will cause failures.
|
||||
- For multiple items of the same type, use batch tools (\`create_many_*\`, \`update_many_*\`, etc.) instead of looping single-item calls.
|
||||
- For multiple items of the same type, use batch tools (\`create_many_*\`, \`update_many_*\`, \`upsert_many_*\`, etc.) instead of looping single-item calls.
|
||||
|
||||
## Tool Strategy
|
||||
|
||||
|
||||
+74
-1
@@ -281,7 +281,16 @@ ${preloadedList}
|
||||
|
||||
const categoryLabel = this.getCategoryLabel(category);
|
||||
|
||||
sections.push(`
|
||||
if (category === ToolCategory.DATABASE_CRUD) {
|
||||
sections.push(
|
||||
this.buildDatabaseCrudCatalogSection(
|
||||
tools,
|
||||
preloadedSet,
|
||||
categoryLabel,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
sections.push(`
|
||||
#### ${categoryLabel} (${tools.length} tools)
|
||||
${tools
|
||||
.map((tool) => {
|
||||
@@ -290,6 +299,7 @@ ${tools
|
||||
return `- \`${tool.name}\`${status}`;
|
||||
})
|
||||
.join('\n')}`);
|
||||
}
|
||||
}
|
||||
|
||||
sections.push(`
|
||||
@@ -300,6 +310,69 @@ ${tools
|
||||
return sections.join('\n');
|
||||
}
|
||||
|
||||
private buildDatabaseCrudCatalogSection(
|
||||
tools: ToolIndexEntry[],
|
||||
preloadedSet: Set<string>,
|
||||
categoryLabel: string,
|
||||
): string {
|
||||
const operationOrder: string[] = [];
|
||||
const seenOps = new Set<string>();
|
||||
|
||||
const objectToolsMap = new Map<string, string[]>();
|
||||
const standaloneTools: ToolIndexEntry[] = [];
|
||||
|
||||
for (const tool of tools) {
|
||||
if (tool.objectName && tool.operation) {
|
||||
const ops = objectToolsMap.get(tool.objectName) ?? [];
|
||||
|
||||
ops.push(tool.operation);
|
||||
objectToolsMap.set(tool.objectName, ops);
|
||||
|
||||
if (!seenOps.has(tool.operation)) {
|
||||
seenOps.add(tool.operation);
|
||||
operationOrder.push(tool.operation);
|
||||
}
|
||||
} else {
|
||||
standaloneTools.push(tool);
|
||||
}
|
||||
}
|
||||
|
||||
const lines: string[] = [`\n#### ${categoryLabel} (${tools.length} tools)`];
|
||||
|
||||
if (objectToolsMap.size > 0) {
|
||||
const objectNames = [...objectToolsMap.keys()].sort();
|
||||
|
||||
lines.push(`Operations per object:`);
|
||||
lines.push(...operationOrder.map((op) => `- \`${op}_{object}\``));
|
||||
|
||||
lines.push(`\nObjects (${objectNames.length}):`);
|
||||
lines.push(...objectNames.map((name) => `- \`${name}\``));
|
||||
|
||||
const findManyExample = tools.find((t) => t.operation === 'find_many');
|
||||
const findOneExample = tools.find(
|
||||
(t) =>
|
||||
t.operation === 'find_one' &&
|
||||
t.objectName === findManyExample?.objectName,
|
||||
);
|
||||
const examplePart =
|
||||
findManyExample && findOneExample
|
||||
? ` e.g. \`${findManyExample.name}\` / \`${findOneExample.name}\``
|
||||
: '';
|
||||
|
||||
lines.push(
|
||||
`\nTool name = operation + object name. *_many_* operations use the plural form, *_one_* use the singular form.${examplePart}`,
|
||||
);
|
||||
}
|
||||
|
||||
for (const tool of standaloneTools) {
|
||||
const status = preloadedSet.has(tool.name) ? ' ✓' : '';
|
||||
|
||||
lines.push(`- \`${tool.name}\`${status}`);
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
private getCategoryLabel(category: ToolCategory): string {
|
||||
switch (category) {
|
||||
case ToolCategory.DATABASE_CRUD:
|
||||
|
||||
+2
-2
@@ -4,13 +4,13 @@ import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-m
|
||||
export const shouldExcludeFieldFromAgentToolSchema = (
|
||||
field: FieldMetadataEntity | FlatFieldMetadata,
|
||||
excludeId = true,
|
||||
additionalExcludedFieldNames: string[] = [],
|
||||
): boolean => {
|
||||
const excludedFieldNames = [
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'deletedAt',
|
||||
'searchVector',
|
||||
'createdBy',
|
||||
...additionalExcludedFieldNames,
|
||||
];
|
||||
|
||||
if (excludeId) {
|
||||
|
||||
Reference in New Issue
Block a user