fix(ai) - fixes (#22227)

- ai chat author fix (before : "workflow", after : "user")
- https://discord.com/channels/1130383047699738754/1496872385687584768

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22227?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
Etienne
2026-06-26 19:53:29 +02:00
committed by GitHub
parent d58ec64e10
commit 3525187321
12 changed files with 165 additions and 22 deletions
@@ -1,7 +1,9 @@
import { styled } from '@linaria/react';
import { EditorContent } from '@tiptap/react';
import { useLingui } from '@lingui/react/macro';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { AiChatBanner } from '@/ai/components/AiChatBanner';
import { AiChatEmptyState } from '@/ai/components/AiChatEmptyState';
import { AIChatNoMoreBillingCreditsBanner } from '@/ai/components/AIChatNoMoreBillingCreditsBanner';
import { AiChatStandaloneError } from '@/ai/components/AiChatStandaloneError';
@@ -14,6 +16,7 @@ import { SendMessageButton } from '@/ai/components/internal/SendMessageButton';
import { useAgentChatModelId } from '@/ai/hooks/useAgentChatModelId';
import { useAiChatEditor } from '@/ai/hooks/useAiChatEditor';
import { useAiModelOptions } from '@/ai/hooks/useAiModelOptions';
import { useWorkspaceAiModelAvailability } from '@/ai/hooks/useWorkspaceAiModelAvailability';
import { agentChatUserSelectedModelState } from '@/ai/states/agentChatUserSelectedModelState';
import { Select } from '@/ui/input/components/Select';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
@@ -108,10 +111,13 @@ const StyledRightButtonsContainer = styled.div`
`;
export const AiChatEditorSection = () => {
const { t } = useLingui();
const isMobile = useIsMobile();
const hasReachedCurrentBillingPeriodCap = useAtomStateValue(
hasReachedCurrentBillingPeriodCapSelector,
);
const { enabledModels } = useWorkspaceAiModelAvailability();
const hasNoEnabledModels = enabledModels.length === 0;
const { options, pinnedOption } = useAiModelOptions({
variant: 'pinned-default',
});
@@ -140,6 +146,12 @@ export const AiChatEditorSection = () => {
<StyledInputArea isMobile={isMobile}>
<AgentChatContextPreview />
{hasNoEnabledModels && (
<AiChatBanner
message={t`No AI models are enabled in this workspace.`}
variant="warning"
/>
)}
{hasReachedCurrentBillingPeriodCap && (
<AIChatNoMoreBillingCreditsBanner />
)}
@@ -159,12 +171,16 @@ export const AiChatEditorSection = () => {
onChange={setAgentChatUserSelectedModel}
options={smartModelOptions}
pinnedOption={defaultPinnedOption}
disabled={hasNoEnabledModels}
selectSizeVariant="small"
showContextualTextInControl={false}
withSearchInput
dropdownOffset={{ x: 0, y: 8 }}
/>
<SendMessageButton onSend={handleSendAndClear} />
<SendMessageButton
onSend={handleSendAndClear}
isDisabled={hasNoEnabledModels}
/>
</StyledRightButtonsContainer>
</StyledButtonsContainer>
</StyledInputBox>
@@ -0,0 +1,39 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { ComponentDecorator } from 'twenty-ui/testing';
import { AiChatBanner } from '@/ai/components/AiChatBanner';
const meta: Meta<typeof AiChatBanner> = {
title: 'Modules/AI/AiChatBanner',
component: AiChatBanner,
decorators: [ComponentDecorator],
parameters: {
container: { width: 400 },
},
};
export default meta;
type Story = StoryObj<typeof AiChatBanner>;
export const NoEnabledModels: Story = {
args: {
message: 'No AI models are enabled in this workspace.',
variant: 'warning',
},
};
export const Default: Story = {
args: {
message: 'This is an informational message.',
variant: 'default',
},
};
export const WithButton: Story = {
args: {
message: "You've hit your usage limit.",
variant: 'warning',
buttonTitle: 'Upgrade',
buttonOnClick: () => {},
},
};
@@ -11,9 +11,13 @@ import { RoundedIconButton } from 'twenty-ui/input';
type SendMessageButtonProps = {
onSend: () => void;
isDisabled?: boolean;
};
export const SendMessageButton = ({ onSend }: SendMessageButtonProps) => {
export const SendMessageButton = ({
onSend,
isDisabled = false,
}: SendMessageButtonProps) => {
const agentChatInputIsEmpty = useAtomStateValue(
agentChatInputIsEmptySelector,
);
@@ -45,7 +49,7 @@ export const SendMessageButton = ({ onSend }: SendMessageButtonProps) => {
Icon={IconArrowUp}
size="medium"
onClick={onSend}
disabled={agentChatInputIsEmpty || agentChatIsLoading}
disabled={isDisabled || agentChatInputIsEmpty || agentChatIsLoading}
/>
);
};
@@ -1,5 +1,6 @@
import { CombinedGraphQLErrors } from '@apollo/client/errors';
import { useApolloClient } from '@apollo/client/react';
import { t } from '@lingui/core/macro';
import { useStore } from 'jotai';
import { useCallback } from 'react';
import { type ExtendedUIMessage } from 'twenty-shared/ai';
@@ -16,6 +17,7 @@ import { STOP_AGENT_CHAT_STREAM } from '@/ai/graphql/mutations/stopAgentChatStre
import { useAgentChatModelId } from '@/ai/hooks/useAgentChatModelId';
import { useGetBrowsingContext } from '@/ai/hooks/useBrowsingContext';
import { useOptimisticallyUnarchiveOnSend } from '@/ai/hooks/useOptimisticallyUnarchiveOnSend';
import { useWorkspaceAiModelAvailability } from '@/ai/hooks/useWorkspaceAiModelAvailability';
import {
AGENT_CHAT_NEW_THREAD_DRAFT_KEY,
agentChatDraftsByThreadIdState,
@@ -37,6 +39,7 @@ export const useAgentChat = (
ensureThreadIdForSend: () => Promise<string | null>,
) => {
const { modelIdForRequest } = useAgentChatModelId();
const { enabledModels } = useWorkspaceAiModelAvailability();
const { getBrowsingContext } = useGetBrowsingContext();
const { applyOptimisticUnarchive } = useOptimisticallyUnarchiveOnSend();
const apolloClient = useApolloClient();
@@ -70,6 +73,14 @@ export const useAgentChat = (
return;
}
if (enabledModels.length === 0) {
enqueueErrorSnackBar({
message: t`No AI models are enabled in this workspace.`,
});
return;
}
const agentChatSelectedFiles = store.get(agentChatSelectedFilesState.atom);
if (agentChatSelectedFiles.length > 0) {
@@ -225,6 +236,8 @@ export const useAgentChat = (
setAgentChatUploadedFiles,
setAgentChatDraftsByThreadId,
modelIdForRequest,
enabledModels,
enqueueErrorSnackBar,
setCurrentAiChatThread,
apolloClient,
applyOptimisticUnarchive,
@@ -155,7 +155,7 @@ export const SettingsAiModelsTab = () => {
<Section>
<H2Title
title={t`Available models`}
description={t`Models available in the chat model picker`}
description={t`Models available in the agent node and chat model pickers`}
/>
<Card rounded>
<SettingsOptionCardContentToggle
@@ -3,17 +3,20 @@ import {
FieldActorSource,
type FullNameMetadata,
} from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
type BuildCreatedByFromFullNameMetadataArgs = {
workspaceMemberId: string;
fullNameMetadata: FullNameMetadata;
source?: FieldActorSource;
};
export const buildCreatedByFromFullNameMetadata = ({
fullNameMetadata,
workspaceMemberId,
source = FieldActorSource.MANUAL,
}: BuildCreatedByFromFullNameMetadataArgs): ActorMetadata => ({
workspaceMemberId,
source: FieldActorSource.MANUAL,
source,
name: `${fullNameMetadata.firstName} ${fullNameMetadata.lastName}`,
context: {},
});
@@ -404,6 +404,7 @@ export class ToolRegistryService {
roleId: context.roleId,
rolePermissionConfig,
authContext: context.authContext,
actorContext: context.actorContext,
userId: context.userId,
userWorkspaceId: context.userWorkspaceId,
threadId: context.threadId,
@@ -90,13 +90,11 @@ export class AgentActorContextService {
);
}
const actorContext: ActorMetadata = {
...buildCreatedByFromFullNameMetadata({
fullNameMetadata: workspaceMember.name,
workspaceMemberId: workspaceMember.id,
}),
const actorContext = buildCreatedByFromFullNameMetadata({
fullNameMetadata: workspaceMember.name,
workspaceMemberId: workspaceMember.id,
source: FieldActorSource.AGENT,
};
});
const userContext: UserContext = {
firstName: workspaceMember.name?.firstName ?? '',
@@ -33,6 +33,7 @@ import {
isModelAllowedByWorkspace,
type WorkspaceModelAvailabilitySettings,
} from 'src/engine/metadata-modules/ai/ai-models/utils/is-model-allowed.util';
import { workspaceHasEnabledModels } from 'src/engine/metadata-modules/ai/ai-models/utils/workspace-has-enabled-models.util';
export interface RegisteredAiModel {
modelId: string;
@@ -293,7 +294,7 @@ export class AiModelRegistryService {
validateModelAvailability(
modelId: string,
workspace: WorkspaceModelAvailabilitySettings,
availabilitySettings: WorkspaceModelAvailabilitySettings,
): void {
if (!this.isModelAdminAllowed(modelId)) {
throw new AiException(
@@ -302,13 +303,17 @@ export class AiModelRegistryService {
);
}
if (
!isModelAllowedByWorkspace(
modelId,
workspace,
this.getRecommendedModelIds(),
)
) {
const recommendedModelIds = this.getRecommendedModelIds();
const isAvailable = isAutoSelectModelId(modelId)
? workspaceHasEnabledModels(availabilitySettings, recommendedModelIds)
: isModelAllowedByWorkspace(
modelId,
availabilitySettings,
recommendedModelIds,
);
if (!isAvailable) {
throw new AiException(
'The selected model is not available in this workspace.',
AiExceptionCode.AGENT_EXECUTION_FAILED,
@@ -0,0 +1,52 @@
import { workspaceHasEnabledModels } from 'src/engine/metadata-modules/ai/ai-models/utils/workspace-has-enabled-models.util';
describe('workspaceHasEnabledModels', () => {
describe('when the workspace uses recommended models', () => {
it('returns true when at least one model is recommended', () => {
expect(
workspaceHasEnabledModels(
{ useRecommendedModels: true, enabledAiModelIds: [] },
new Set(['openai/gpt-5.2']),
),
).toBe(true);
});
it('returns false when no model is recommended', () => {
expect(
workspaceHasEnabledModels(
{ useRecommendedModels: true, enabledAiModelIds: [] },
new Set(),
),
).toBe(false);
});
it('returns false when no recommended ids are provided', () => {
expect(
workspaceHasEnabledModels({
useRecommendedModels: true,
enabledAiModelIds: [],
}),
).toBe(false);
});
});
describe('when the workspace selects models manually', () => {
it('returns true when at least one model is enabled', () => {
expect(
workspaceHasEnabledModels({
useRecommendedModels: false,
enabledAiModelIds: ['openai/gpt-5.2'],
}),
).toBe(true);
});
it('returns false when every model is disabled', () => {
expect(
workspaceHasEnabledModels({
useRecommendedModels: false,
enabledAiModelIds: [],
}),
).toBe(false);
});
});
});
@@ -7,16 +7,16 @@ export type WorkspaceModelAvailabilitySettings = {
export const isModelAllowedByWorkspace = (
modelId: string,
workspace: WorkspaceModelAvailabilitySettings,
availabilitySettings: WorkspaceModelAvailabilitySettings,
recommendedModelIds?: Set<string>,
): boolean => {
if (isAutoSelectModelId(modelId)) {
return true;
}
if (workspace.useRecommendedModels) {
if (availabilitySettings.useRecommendedModels) {
return recommendedModelIds?.has(modelId) ?? false;
}
return workspace.enabledAiModelIds.includes(modelId);
return availabilitySettings.enabledAiModelIds.includes(modelId);
};
@@ -0,0 +1,12 @@
import { type WorkspaceModelAvailabilitySettings } from 'src/engine/metadata-modules/ai/ai-models/utils/is-model-allowed.util';
export const workspaceHasEnabledModels = (
availabilitySettings: WorkspaceModelAvailabilitySettings,
recommendedModelIds?: Set<string>,
): boolean => {
if (availabilitySettings.useRecommendedModels) {
return (recommendedModelIds?.size ?? 0) > 0;
}
return availabilitySettings.enabledAiModelIds.length > 0;
};