Show tool execution messages in AI agent chat (#13117)

https://github.com/user-attachments/assets/c0a42726-50ac-496e-a993-9d6076a84a6a

---------

Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
Abdul Rahman
2025-07-10 11:15:05 +05:30
committed by GitHub
parent e6cdae5c27
commit 8310b4ff01
62 changed files with 1304 additions and 227 deletions

View File

@ -2,7 +2,10 @@ import {
ApolloClient,
ApolloClientOptions,
ApolloLink,
FetchResult,
fromPromise,
Observable,
Operation,
ServerError,
ServerParseError,
} from '@apollo/client';
@ -19,7 +22,12 @@ import { AuthTokenPair } from '~/generated/graphql';
import { logDebug } from '~/utils/logDebug';
import { i18n } from '@lingui/core';
import { GraphQLFormattedError } from 'graphql';
import {
DefinitionNode,
DirectiveNode,
GraphQLFormattedError,
SelectionNode,
} from 'graphql';
import isEmpty from 'lodash.isempty';
import { getGenericOperationName, isDefined } from 'twenty-shared/utils';
import { REACT_APP_SERVER_BASE_URL } from '~/config';
@ -115,45 +123,45 @@ export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
},
attempts: {
max: 2,
retryIf: (error) => !!error,
retryIf: (error) => {
if (this.isAuthenticationError(error)) {
return false;
}
return Boolean(error);
},
},
});
const handleTokenRenewal = (
operation: Operation,
forward: (operation: Operation) => Observable<FetchResult>,
) => {
return fromPromise(
renewToken(uri, getTokenPair())
.then((tokens) => {
if (isDefined(tokens)) {
onTokenPairChange?.(tokens);
cookieStorage.setItem('tokenPair', JSON.stringify(tokens));
}
})
.catch(() => {
onUnauthenticatedError?.();
}),
).flatMap(() => forward(operation));
};
const errorLink = onError(
({ graphQLErrors, networkError, forward, operation }) => {
if (isDefined(graphQLErrors)) {
onErrorCb?.(graphQLErrors);
for (const graphQLError of graphQLErrors) {
if (graphQLError.message === 'Unauthorized') {
return fromPromise(
renewToken(uri, getTokenPair())
.then((tokens) => {
if (isDefined(tokens)) {
onTokenPairChange?.(tokens);
}
})
.catch(() => {
onUnauthenticatedError?.();
}),
).flatMap(() => forward(operation));
return handleTokenRenewal(operation, forward);
}
switch (graphQLError?.extensions?.code) {
case 'UNAUTHENTICATED': {
return fromPromise(
renewToken(uri, getTokenPair())
.then((tokens) => {
if (isDefined(tokens)) {
onTokenPairChange?.(tokens);
cookieStorage.setItem(
'tokenPair',
JSON.stringify(tokens),
);
}
})
.catch(() => {
onUnauthenticatedError?.();
}),
).flatMap(() => forward(operation));
return handleTokenRenewal(operation, forward);
}
case 'FORBIDDEN': {
return;
@ -220,6 +228,13 @@ export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
}
if (isDefined(networkError)) {
if (
this.isRestOperation(operation) &&
this.isAuthenticationError(networkError as ServerError)
) {
return handleTokenRenewal(operation, forward);
}
if (isDebugMode === true) {
logDebug(`[Network error]: ${networkError}`);
}
@ -248,6 +263,26 @@ export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
});
}
private isRestOperation(operation: Operation): boolean {
return operation.query.definitions.some(
(def: DefinitionNode) =>
def.kind === 'OperationDefinition' &&
def.selectionSet?.selections.some(
(selection: SelectionNode) =>
selection.kind === 'Field' &&
selection.directives?.some(
(directive: DirectiveNode) =>
directive.name.value === 'rest' ||
directive.name.value === 'stream',
),
),
);
}
private isAuthenticationError(error: ServerError): boolean {
return error.statusCode === 401;
}
updateWorkspaceMember(workspaceMember: CurrentWorkspaceMember | null) {
this.currentWorkspaceMember = workspaceMember;
}

View File

@ -1,4 +1,9 @@
import { ApolloLink, Observable, Operation } from '@apollo/client/core';
import {
ApolloLink,
Observable,
Operation,
ServerError,
} from '@apollo/client/core';
import { FetchResult } from '@apollo/client/link/core';
import { ArgumentNode, DirectiveNode } from 'graphql';
import { isDefined } from 'twenty-shared/utils';
@ -57,7 +62,13 @@ export class StreamingRestLink extends ApolloLink {
fetch(url, requestConfig)
.then(async (response) => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
const networkError = new Error(
`HTTP error! status: ${response.status}`,
) as ServerError;
networkError.statusCode = response.status;
throw networkError;
}
if (!response.body) {
@ -66,7 +77,6 @@ export class StreamingRestLink extends ApolloLink {
const reader = response.body.getReader();
const decoder = new TextDecoder();
let accumulatedData = '';
let isStreaming = true;
while (isStreaming) {
@ -79,19 +89,9 @@ export class StreamingRestLink extends ApolloLink {
}
const decodedChunk = decoder.decode(value, { stream: true });
accumulatedData += decodedChunk;
if (isDefined(onChunk) && typeof onChunk === 'function') {
onChunk(accumulatedData);
}
try {
const parsedData = JSON.parse(decodedChunk);
observer.next({ data: parsedData });
} catch {
observer.next({
data: { streamingData: decodedChunk },
});
onChunk(decodedChunk);
}
}
})