From e518f03031bd0529084c3362e1ece28b04972d0b Mon Sep 17 00:00:00 2001 From: Abdul Rahman <81605929+abdulrahmancodes@users.noreply.github.com> Date: Thu, 6 Nov 2025 16:58:00 +0530 Subject: [PATCH] Fix: AI Agent tool errors and relation field handling (#15668) ### Problems Fixed 1. **Tool execution errors broke conversations** - Failed tool executions showed "Processing..." indefinitely instead of error messages - Tool errors with `input: null` caused subsequent messages to fail with `Missing required parameter: 'input[X].arguments'` 2. **Relation fields not saved in AI Agent** - AI Agent couldn't save relation fields (e.g., `companyId`) when creating/upserting records - Join column names weren't recognized during field validation ### Solutions **Tool Error Handling:** - Display error messages in UI with expandable error details - Ensure tool parts always have valid `input` field (`input: part.toolInput ?? {}`) - Refactored `ToolStepRenderer` to accept complete `toolPart` object **Relation Field Support:** - Updated field validation in `create-record.service.ts` and `upsert-record.service.ts` - Check both `fieldIdByName` and `fieldIdByJoinColumnName` mappings ### Changes - `packages/twenty-front/src/modules/ai/` - `ToolStepRenderer.tsx` - Error state handling - `AIChatAssistantMessageRenderer.tsx` - Pass complete toolPart - `mapDBPartToUIMessagePart.ts` - Prevent null tool input - `packages/twenty-server/src/engine/core-modules/record-crud/services/` - `create-record.service.ts` - Add join column validation - `upsert-record.service.ts` - Add join column validation --- .../AIChatAssistantMessageRenderer.tsx | 10 +- .../ai/components/ToolStepRenderer.tsx | 92 ++++++++++--------- .../ai/utils/mapDBPartToUIMessagePart.ts | 2 +- .../services/create-record.service.ts | 8 +- .../services/upsert-record.service.ts | 8 +- 5 files changed, 62 insertions(+), 58 deletions(-) diff --git a/packages/twenty-front/src/modules/ai/components/AIChatAssistantMessageRenderer.tsx b/packages/twenty-front/src/modules/ai/components/AIChatAssistantMessageRenderer.tsx index 5c5680924f..d7586bc0de 100644 --- a/packages/twenty-front/src/modules/ai/components/AIChatAssistantMessageRenderer.tsx +++ b/packages/twenty-front/src/modules/ai/components/AIChatAssistantMessageRenderer.tsx @@ -82,15 +82,7 @@ export const AIChatAssistantMessageRenderer = ({ default: { if (isToolUIPart(part)) { - const { output, input, type } = part; - return ( - - ); + return ; } } return null; diff --git a/packages/twenty-front/src/modules/ai/components/ToolStepRenderer.tsx b/packages/twenty-front/src/modules/ai/components/ToolStepRenderer.tsx index a954e7d07f..5892d5c250 100644 --- a/packages/twenty-front/src/modules/ai/components/ToolStepRenderer.tsx +++ b/packages/twenty-front/src/modules/ai/components/ToolStepRenderer.tsx @@ -3,11 +3,10 @@ import styled from '@emotion/styled'; import { useState } from 'react'; import { IconChevronDown, IconChevronUp } from 'twenty-ui/display'; -import { AnimatedExpandableContainer } from 'twenty-ui/layout'; import { JsonTree } from 'twenty-ui/json-visualizer'; +import { AnimatedExpandableContainer } from 'twenty-ui/layout'; import { ShimmeringText } from '@/ai/components/ShimmeringText'; -import { type ToolInput } from '@/ai/types/ToolInput'; import { getToolIcon } from '@/ai/utils/getToolIcon'; import { getToolDisplayMessage } from '@/ai/utils/getWebSearchToolDisplayMessage'; import { useLingui } from '@lingui/react/macro'; @@ -96,26 +95,22 @@ const StyledJsonContainer = styled.div` type TabType = 'output' | 'input'; -export const ToolStepRenderer = ({ - input, - output, - toolName, -}: { - input: ToolInput; - output: ToolUIPart['output']; - toolName: string; -}) => { +export const ToolStepRenderer = ({ toolPart }: { toolPart: ToolUIPart }) => { const { t } = useLingui(); const theme = useTheme(); const { copyToClipboard } = useCopyToClipboard(); const [isExpanded, setIsExpanded] = useState(false); const [activeTab, setActiveTab] = useState('output'); - const isExpandable = isDefined(output); + const { input, output, type, errorText } = toolPart; + const toolName = type.split('-')[1]; + + const hasError = isDefined(errorText); + const isExpandable = isDefined(output) || hasError; const isTwoFirstDepths = ({ depth }: { depth: number }) => depth < 2; - if (!output) { + if (!output && !hasError) { return ( @@ -129,11 +124,12 @@ export const ToolStepRenderer = ({ ); } - const displayMessage = - output && - typeof output === 'object' && - 'message' in output && - typeof output.message === 'string' + const displayMessage = hasError + ? 'Tool execution failed' + : output && + typeof output === 'object' && + 'message' in output && + typeof output.message === 'string' ? output.message : getToolDisplayMessage(input, toolName, true); @@ -165,33 +161,41 @@ export const ToolStepRenderer = ({ {isExpandable && ( - - setActiveTab('output')} - > - Output - - setActiveTab('input')} - > - Input - - + {hasError ? ( + {errorText} + ) : ( + <> + + setActiveTab('output')} + > + Output + + setActiveTab('input')} + > + Input + + - - - + + + + + )} )} diff --git a/packages/twenty-front/src/modules/ai/utils/mapDBPartToUIMessagePart.ts b/packages/twenty-front/src/modules/ai/utils/mapDBPartToUIMessagePart.ts index 72e702c5eb..f3414bebd0 100644 --- a/packages/twenty-front/src/modules/ai/utils/mapDBPartToUIMessagePart.ts +++ b/packages/twenty-front/src/modules/ai/utils/mapDBPartToUIMessagePart.ts @@ -59,7 +59,7 @@ export const mapDBPartToUIMessagePart = ( return { type: part.type as `tool-${string}`, toolCallId: part.toolCallId!, - input: part.toolInput, + input: part.toolInput ?? {}, output: part.toolOutput, errorText: part.errorMessage!, state: part.state, diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/services/create-record.service.ts b/packages/twenty-server/src/engine/core-modules/record-crud/services/create-record.service.ts index 12cdfd9315..bcbf9d9393 100644 --- a/packages/twenty-server/src/engine/core-modules/record-crud/services/create-record.service.ts +++ b/packages/twenty-server/src/engine/core-modules/record-crud/services/create-record.service.ts @@ -73,8 +73,12 @@ export class CreateRecordService { }); const validObjectRecord = Object.fromEntries( - Object.entries(objectRecord).filter(([key]) => - isDefined(objectMetadataItemWithFieldsMaps.fieldIdByName[key]), + Object.entries(objectRecord).filter( + ([key]) => + isDefined(objectMetadataItemWithFieldsMaps.fieldIdByName[key]) || + isDefined( + objectMetadataItemWithFieldsMaps.fieldIdByJoinColumnName[key], + ), ), ); diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/services/upsert-record.service.ts b/packages/twenty-server/src/engine/core-modules/record-crud/services/upsert-record.service.ts index 466b58b3d5..4b385cd534 100644 --- a/packages/twenty-server/src/engine/core-modules/record-crud/services/upsert-record.service.ts +++ b/packages/twenty-server/src/engine/core-modules/record-crud/services/upsert-record.service.ts @@ -92,9 +92,13 @@ export class UpsertRecordService { }); const uniqueFieldsToUpdate = fieldsToUpdateArray - .map((field) => objectMetadataItemWithFieldsMaps.fieldIdByName[field]) + .map( + (field) => + objectMetadataItemWithFieldsMaps.fieldIdByName[field] || + objectMetadataItemWithFieldsMaps.fieldIdByJoinColumnName[field], + ) .map((fieldId) => objectMetadataItemWithFieldsMaps.fieldsById[fieldId]) - .filter((field) => field.isUnique || field.name === 'id'); + .filter((field) => field && (field.isUnique || field.name === 'id')); const conflictPathsUniqueFieldsToUpdate = uniqueFieldsToUpdate.flatMap( (field) => {