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:
@ -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;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user