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:
@@ -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
|
||||
|
||||
+4
-1
@@ -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: {},
|
||||
});
|
||||
|
||||
+1
@@ -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,
|
||||
|
||||
+4
-6
@@ -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 ?? '',
|
||||
|
||||
+13
-8
@@ -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,
|
||||
|
||||
+52
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
+3
-3
@@ -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);
|
||||
};
|
||||
|
||||
+12
@@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user