[AI] Match ai chat composer to figma (#18874)
https://www.figma.com/design/xt8O9mFeLl46C5InWwoMrN/Twenty?node-id=93653-368288&t=obTG32NRidXid4lN-0 closes https://discord.com/channels/1130383047699738754/1480990726442582086 --------- Co-authored-by: Félix Malfait <felix.malfait@gmail.com> Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
+2
-2
@@ -1,6 +1,6 @@
|
||||
import { type AgentManifest } from 'twenty-shared/application';
|
||||
|
||||
import { DEFAULT_SMART_MODEL } from 'src/engine/metadata-modules/ai/ai-models/types/default-smart-model.const';
|
||||
import { AUTO_SELECT_SMART_MODEL_ID } from 'twenty-shared/constants';
|
||||
import { type ModelId } from 'src/engine/metadata-modules/ai/ai-models/types/model-id.type';
|
||||
import { type UniversalFlatAgent } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-agent.type';
|
||||
|
||||
@@ -21,7 +21,7 @@ export const fromAgentManifestToUniversalFlatAgent = ({
|
||||
icon: agentManifest.icon ?? null,
|
||||
description: agentManifest.description ?? null,
|
||||
prompt: agentManifest.prompt,
|
||||
modelId: (agentManifest.modelId as ModelId) ?? DEFAULT_SMART_MODEL,
|
||||
modelId: (agentManifest.modelId as ModelId) ?? AUTO_SELECT_SMART_MODEL_ID,
|
||||
responseFormat: { type: 'text' },
|
||||
modelConfiguration: null,
|
||||
evaluationInputs: [],
|
||||
|
||||
+20
-16
@@ -20,8 +20,10 @@ import { DomainServerConfigService } from 'src/engine/core-modules/domain/domain
|
||||
import { PUBLIC_FEATURE_FLAGS } from 'src/engine/core-modules/feature-flag/constants/public-feature-flag.const';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { convertDollarsToBillingCredits } from 'src/engine/metadata-modules/ai/ai-billing/utils/convert-dollars-to-billing-credits.util';
|
||||
import { DEFAULT_FAST_MODEL } from 'src/engine/metadata-modules/ai/ai-models/types/default-fast-model.const';
|
||||
import { DEFAULT_SMART_MODEL } from 'src/engine/metadata-modules/ai/ai-models/types/default-smart-model.const';
|
||||
import {
|
||||
AUTO_SELECT_FAST_MODEL_ID,
|
||||
AUTO_SELECT_SMART_MODEL_ID,
|
||||
} from 'twenty-shared/constants';
|
||||
import { MODEL_FAMILY_LABELS } from 'src/engine/metadata-modules/ai/ai-models/constants/model-family-labels.const';
|
||||
import { AiModelRegistryService } from 'src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service';
|
||||
|
||||
@@ -107,10 +109,6 @@ export class ClientConfigService {
|
||||
this.aiModelRegistryService.getDefaultSpeedModel();
|
||||
const defaultSpeedModelConfig =
|
||||
this.aiModelRegistryService.getModelConfig(defaultSpeedModel?.modelId);
|
||||
const defaultSpeedModelLabel =
|
||||
defaultSpeedModelConfig?.label ||
|
||||
defaultSpeedModel?.modelId ||
|
||||
'Default';
|
||||
|
||||
const defaultPerformanceModel =
|
||||
this.aiModelRegistryService.getDefaultPerformanceModel();
|
||||
@@ -118,23 +116,29 @@ export class ClientConfigService {
|
||||
this.aiModelRegistryService.getModelConfig(
|
||||
defaultPerformanceModel?.modelId,
|
||||
);
|
||||
const defaultPerformanceModelLabel =
|
||||
defaultPerformanceModelConfig?.label ||
|
||||
defaultPerformanceModel?.modelId ||
|
||||
'Default';
|
||||
|
||||
aiModels.unshift(
|
||||
{
|
||||
modelId: DEFAULT_SMART_MODEL,
|
||||
label: `Best (${defaultPerformanceModelLabel})`,
|
||||
sdkPackage: null,
|
||||
modelId: AUTO_SELECT_SMART_MODEL_ID,
|
||||
label:
|
||||
defaultPerformanceModelConfig?.label ||
|
||||
defaultPerformanceModel?.modelId ||
|
||||
'Default',
|
||||
modelFamily: defaultPerformanceModelConfig?.modelFamily,
|
||||
providerName: defaultPerformanceModel?.providerName,
|
||||
sdkPackage: defaultPerformanceModel?.sdkPackage ?? null,
|
||||
inputCostPerMillionTokensInCredits: 0,
|
||||
outputCostPerMillionTokensInCredits: 0,
|
||||
},
|
||||
{
|
||||
modelId: DEFAULT_FAST_MODEL,
|
||||
label: `Best (${defaultSpeedModelLabel})`,
|
||||
sdkPackage: null,
|
||||
modelId: AUTO_SELECT_FAST_MODEL_ID,
|
||||
label:
|
||||
defaultSpeedModelConfig?.label ||
|
||||
defaultSpeedModel?.modelId ||
|
||||
'Default',
|
||||
modelFamily: defaultSpeedModelConfig?.modelFamily,
|
||||
providerName: defaultSpeedModel?.providerName,
|
||||
sdkPackage: defaultSpeedModel?.sdkPackage ?? null,
|
||||
inputCostPerMillionTokensInCredits: 0,
|
||||
outputCostPerMillionTokensInCredits: 0,
|
||||
},
|
||||
|
||||
@@ -34,8 +34,10 @@ import { PublicDomainEntity } from 'src/engine/core-modules/public-domain/public
|
||||
import { WorkspaceSSOIdentityProviderEntity } from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
|
||||
import { DEFAULT_FAST_MODEL } from 'src/engine/metadata-modules/ai/ai-models/types/default-fast-model.const';
|
||||
import { DEFAULT_SMART_MODEL } from 'src/engine/metadata-modules/ai/ai-models/types/default-smart-model.const';
|
||||
import {
|
||||
AUTO_SELECT_FAST_MODEL_ID,
|
||||
AUTO_SELECT_SMART_MODEL_ID,
|
||||
} from 'twenty-shared/constants';
|
||||
import { type ModelId } from 'src/engine/metadata-modules/ai/ai-models/types/model-id.type';
|
||||
import { RoleDTO } from 'src/engine/metadata-modules/role/dtos/role.dto';
|
||||
import { ViewFieldDTO } from 'src/engine/metadata-modules/view-field/dtos/view-field.dto';
|
||||
@@ -301,11 +303,19 @@ export class WorkspaceEntity {
|
||||
version: string | null;
|
||||
|
||||
@Field(() => String, { nullable: false })
|
||||
@Column({ type: 'varchar', nullable: false, default: DEFAULT_FAST_MODEL })
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
nullable: false,
|
||||
default: AUTO_SELECT_FAST_MODEL_ID,
|
||||
})
|
||||
fastModel: ModelId;
|
||||
|
||||
@Field(() => String, { nullable: false })
|
||||
@Column({ type: 'varchar', nullable: false, default: DEFAULT_SMART_MODEL })
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
nullable: false,
|
||||
default: AUTO_SELECT_SMART_MODEL_ID,
|
||||
})
|
||||
smartModel: ModelId;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
|
||||
+6
-2
@@ -10,7 +10,7 @@ import {
|
||||
|
||||
import { AgentResponseFormat } from 'src/engine/metadata-modules/ai/ai-agent/types/agent-response-format.type';
|
||||
import { ModelConfiguration } from 'src/engine/metadata-modules/ai/ai-agent/types/modelConfiguration';
|
||||
import { DEFAULT_SMART_MODEL } from 'src/engine/metadata-modules/ai/ai-models/types/default-smart-model.const';
|
||||
import { AUTO_SELECT_SMART_MODEL_ID } from 'twenty-shared/constants';
|
||||
import { type ModelId } from 'src/engine/metadata-modules/ai/ai-models/types/model-id.type';
|
||||
import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
|
||||
import { JsonbProperty } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type';
|
||||
@@ -43,7 +43,11 @@ export class AgentEntity
|
||||
@Column({ nullable: false, type: 'text' })
|
||||
prompt: string;
|
||||
|
||||
@Column({ nullable: false, type: 'varchar', default: DEFAULT_SMART_MODEL })
|
||||
@Column({
|
||||
nullable: false,
|
||||
type: 'varchar',
|
||||
default: AUTO_SELECT_SMART_MODEL_ID,
|
||||
})
|
||||
modelId: ModelId;
|
||||
|
||||
// Should not be nullable
|
||||
|
||||
+4
-2
@@ -39,9 +39,9 @@ import { AiModelRegistryService } from 'src/engine/metadata-modules/ai/ai-models
|
||||
@Controller('rest/agent-chat')
|
||||
@UseGuards(JwtAuthGuard, WorkspaceAuthGuard)
|
||||
@UseFilters(
|
||||
RestApiExceptionFilter,
|
||||
AgentRestApiExceptionFilter,
|
||||
BillingRestApiExceptionFilter,
|
||||
RestApiExceptionFilter,
|
||||
)
|
||||
export class AgentChatController {
|
||||
constructor(
|
||||
@@ -59,6 +59,7 @@ export class AgentChatController {
|
||||
threadId: string;
|
||||
messages: ExtendedUIMessage[];
|
||||
browsingContext?: BrowsingContextType | null;
|
||||
modelId?: string;
|
||||
},
|
||||
@AuthUserWorkspaceId() userWorkspaceId: string,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
@@ -71,7 +72,7 @@ export class AgentChatController {
|
||||
);
|
||||
}
|
||||
|
||||
const resolvedModelId = workspace.smartModel;
|
||||
const resolvedModelId = body.modelId ?? workspace.smartModel;
|
||||
|
||||
this.aiModelRegistryService.validateModelAvailability(
|
||||
resolvedModelId,
|
||||
@@ -96,6 +97,7 @@ export class AgentChatController {
|
||||
threadId: body.threadId,
|
||||
messages: body.messages,
|
||||
browsingContext: body.browsingContext ?? null,
|
||||
modelId: body.modelId,
|
||||
userWorkspaceId,
|
||||
workspace,
|
||||
response,
|
||||
|
||||
+3
@@ -33,6 +33,7 @@ export type StreamAgentChatOptions = {
|
||||
response: Response;
|
||||
messages: ExtendedUIMessage[];
|
||||
browsingContext: BrowsingContextType | null;
|
||||
modelId?: string;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
@@ -51,6 +52,7 @@ export class AgentChatStreamingService {
|
||||
messages,
|
||||
browsingContext,
|
||||
response,
|
||||
modelId,
|
||||
}: StreamAgentChatOptions) {
|
||||
const thread = await this.threadRepository.findOne({
|
||||
where: {
|
||||
@@ -114,6 +116,7 @@ export class AgentChatStreamingService {
|
||||
messages,
|
||||
browsingContext,
|
||||
onCodeExecutionUpdate,
|
||||
modelId,
|
||||
});
|
||||
|
||||
let streamUsage = {
|
||||
|
||||
+8
-3
@@ -60,6 +60,7 @@ export type ChatExecutionOptions = {
|
||||
messages: UIMessage<unknown, UIDataTypes, UITools>[];
|
||||
browsingContext: BrowsingContextType | null;
|
||||
onCodeExecutionUpdate?: CodeExecutionStreamEmitter;
|
||||
modelId?: string;
|
||||
};
|
||||
|
||||
export type ChatExecutionResult = {
|
||||
@@ -89,6 +90,7 @@ export class ChatExecutionService {
|
||||
messages,
|
||||
browsingContext,
|
||||
onCodeExecutionUpdate,
|
||||
modelId,
|
||||
}: ChatExecutionOptions): Promise<ChatExecutionResult> {
|
||||
const { actorContext, roleId, userId, userContext } =
|
||||
await this.agentActorContextService.buildUserAndAgentActorContext(
|
||||
@@ -128,13 +130,16 @@ export class ChatExecutionService {
|
||||
toolContext,
|
||||
);
|
||||
|
||||
const modelId = workspace.smartModel;
|
||||
const resolvedModelId = modelId ?? workspace.smartModel;
|
||||
|
||||
this.aiModelRegistryService.validateModelAvailability(modelId, workspace);
|
||||
this.aiModelRegistryService.validateModelAvailability(
|
||||
resolvedModelId,
|
||||
workspace,
|
||||
);
|
||||
|
||||
const registeredModel =
|
||||
await this.aiModelRegistryService.resolveModelForAgent({
|
||||
modelId,
|
||||
modelId: resolvedModelId,
|
||||
});
|
||||
|
||||
const modelConfig = this.aiModelRegistryService.getEffectiveModelConfig(
|
||||
|
||||
+135
-135
File diff suppressed because it is too large
Load Diff
+9
-7
@@ -8,7 +8,7 @@ import { SdkProviderFactoryService } from 'src/engine/metadata-modules/ai/ai-mod
|
||||
import { buildCompositeModelId } from 'src/engine/metadata-modules/ai/ai-models/utils/composite-model-id.util';
|
||||
import { loadDefaultAiProviders } from 'src/engine/metadata-modules/ai/ai-models/utils/load-default-ai-providers.util';
|
||||
import { type AiProvidersConfig } from 'src/engine/metadata-modules/ai/ai-models/types/ai-providers-config.type';
|
||||
import { DEFAULT_SMART_MODEL } from 'src/engine/metadata-modules/ai/ai-models/types/default-smart-model.const';
|
||||
import { AUTO_SELECT_SMART_MODEL_ID } from 'twenty-shared/constants';
|
||||
|
||||
const DEFAULT_PROVIDERS: AiProvidersConfig = loadDefaultAiProviders();
|
||||
|
||||
@@ -125,13 +125,15 @@ describe('AiModelRegistryService', () => {
|
||||
service = module.get<AiModelRegistryService>(AiModelRegistryService);
|
||||
});
|
||||
|
||||
it('should throw when no models are available for DEFAULT_SMART_MODEL', () => {
|
||||
expect(() => service.getEffectiveModelConfig(DEFAULT_SMART_MODEL)).toThrow(
|
||||
it('should throw when no models are available for AUTO_SELECT_SMART_MODEL_ID', () => {
|
||||
expect(() =>
|
||||
service.getEffectiveModelConfig(AUTO_SELECT_SMART_MODEL_ID),
|
||||
).toThrow(
|
||||
'No AI models are available. Configure at least one AI provider.',
|
||||
);
|
||||
});
|
||||
|
||||
it('should return effective model config for DEFAULT_SMART_MODEL when models are available', () => {
|
||||
it('should return effective model config for AUTO_SELECT_SMART_MODEL_ID when models are available', () => {
|
||||
jest.spyOn(service, 'getAvailableModels').mockReturnValue([
|
||||
{
|
||||
modelId: 'openai/gpt-5.2',
|
||||
@@ -146,14 +148,14 @@ describe('AiModelRegistryService', () => {
|
||||
model: {} as any,
|
||||
});
|
||||
|
||||
const result = service.getEffectiveModelConfig(DEFAULT_SMART_MODEL);
|
||||
const result = service.getEffectiveModelConfig(AUTO_SELECT_SMART_MODEL_ID);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.modelId).toBe('openai/gpt-5.2');
|
||||
expect(result.sdkPackage).toBe('@ai-sdk/openai');
|
||||
});
|
||||
|
||||
it('should return effective model config for DEFAULT_SMART_MODEL with custom model', () => {
|
||||
it('should return effective model config for AUTO_SELECT_SMART_MODEL_ID with custom model', () => {
|
||||
jest.spyOn(service, 'getAvailableModels').mockReturnValue([
|
||||
{
|
||||
modelId: 'custom/mistral',
|
||||
@@ -168,7 +170,7 @@ describe('AiModelRegistryService', () => {
|
||||
model: {} as any,
|
||||
});
|
||||
|
||||
const result = service.getEffectiveModelConfig(DEFAULT_SMART_MODEL);
|
||||
const result = service.getEffectiveModelConfig(AUTO_SELECT_SMART_MODEL_ID);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.modelId).toBe('custom/mistral');
|
||||
|
||||
+10
-7
@@ -17,12 +17,15 @@ import { type AiProviderConfig } from 'src/engine/metadata-modules/ai/ai-models/
|
||||
import { type AiProviderModelConfig } from 'src/engine/metadata-modules/ai/ai-models/types/ai-provider-model-config.type';
|
||||
import { type AiProvidersConfig } from 'src/engine/metadata-modules/ai/ai-models/types/ai-providers-config.type';
|
||||
import { DEFAULT_CONTEXT_WINDOW_TOKENS } from 'src/engine/metadata-modules/ai/ai-models/types/default-context-window-tokens.const';
|
||||
import { DEFAULT_FAST_MODEL } from 'src/engine/metadata-modules/ai/ai-models/types/default-fast-model.const';
|
||||
import {
|
||||
AUTO_SELECT_FAST_MODEL_ID,
|
||||
AUTO_SELECT_SMART_MODEL_ID,
|
||||
} from 'twenty-shared/constants';
|
||||
import { isAutoSelectModelId } from 'twenty-shared/utils';
|
||||
|
||||
import { DEFAULT_MAX_OUTPUT_TOKENS } from 'src/engine/metadata-modules/ai/ai-models/types/default-max-output-tokens.const';
|
||||
import { DEFAULT_SMART_MODEL } from 'src/engine/metadata-modules/ai/ai-models/types/default-smart-model.const';
|
||||
import { buildCompositeModelId } from 'src/engine/metadata-modules/ai/ai-models/utils/composite-model-id.util';
|
||||
import { inferModelFamily } from 'src/engine/metadata-modules/ai/ai-models/utils/infer-model-family.util';
|
||||
import { isDefaultModelSentinel } from 'src/engine/metadata-modules/ai/ai-models/utils/is-default-model-sentinel.util';
|
||||
import {
|
||||
isModelAllowedByWorkspace,
|
||||
type WorkspaceModelAvailabilitySettings,
|
||||
@@ -203,9 +206,9 @@ export class AiModelRegistryService {
|
||||
}
|
||||
|
||||
getEffectiveModelConfig(modelId: string): AIModelConfig {
|
||||
if (isDefaultModelSentinel(modelId)) {
|
||||
if (isAutoSelectModelId(modelId)) {
|
||||
const defaultModel =
|
||||
modelId === DEFAULT_FAST_MODEL
|
||||
modelId === AUTO_SELECT_FAST_MODEL_ID
|
||||
? this.getDefaultSpeedModel()
|
||||
: this.getDefaultPerformanceModel();
|
||||
|
||||
@@ -253,7 +256,7 @@ export class AiModelRegistryService {
|
||||
}
|
||||
|
||||
isModelAdminAllowed(modelId: string): boolean {
|
||||
if (isDefaultModelSentinel(modelId)) {
|
||||
if (isAutoSelectModelId(modelId)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -360,7 +363,7 @@ export class AiModelRegistryService {
|
||||
|
||||
resolveModelForAgent(agent: { modelId: string } | null): RegisteredAIModel {
|
||||
const aiModel = this.getEffectiveModelConfig(
|
||||
agent?.modelId ?? DEFAULT_SMART_MODEL,
|
||||
agent?.modelId ?? AUTO_SELECT_SMART_MODEL_ID,
|
||||
);
|
||||
|
||||
const registeredModel = this.getModel(aiModel.modelId);
|
||||
|
||||
-1
@@ -1 +0,0 @@
|
||||
export const DEFAULT_FAST_MODEL = 'default-fast-model' as const;
|
||||
-1
@@ -1 +0,0 @@
|
||||
export const DEFAULT_SMART_MODEL = 'default-smart-model' as const;
|
||||
+5
-5
@@ -1,7 +1,7 @@
|
||||
export enum ModelFamily {
|
||||
GPT = 'gpt',
|
||||
CLAUDE = 'claude',
|
||||
GEMINI = 'gemini',
|
||||
MISTRAL = 'mistral',
|
||||
GROK = 'grok',
|
||||
GPT = 'GPT',
|
||||
CLAUDE = 'CLAUDE',
|
||||
GEMINI = 'GEMINI',
|
||||
MISTRAL = 'MISTRAL',
|
||||
GROK = 'GROK',
|
||||
}
|
||||
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
import { DEFAULT_FAST_MODEL } from 'src/engine/metadata-modules/ai/ai-models/types/default-fast-model.const';
|
||||
import { DEFAULT_SMART_MODEL } from 'src/engine/metadata-modules/ai/ai-models/types/default-smart-model.const';
|
||||
|
||||
export const isDefaultModelSentinel = (modelId: string): boolean =>
|
||||
modelId === DEFAULT_FAST_MODEL || modelId === DEFAULT_SMART_MODEL;
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
import { isDefaultModelSentinel } from 'src/engine/metadata-modules/ai/ai-models/utils/is-default-model-sentinel.util';
|
||||
import { isAutoSelectModelId } from 'twenty-shared/utils';
|
||||
|
||||
export type WorkspaceModelAvailabilitySettings = {
|
||||
useRecommendedModels: boolean;
|
||||
@@ -10,7 +10,7 @@ export const isModelAllowedByWorkspace = (
|
||||
workspace: WorkspaceModelAvailabilitySettings,
|
||||
recommendedModelIds?: Set<string>,
|
||||
): boolean => {
|
||||
if (isDefaultModelSentinel(modelId)) {
|
||||
if (isAutoSelectModelId(modelId)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
import { DEFAULT_SMART_MODEL } from 'src/engine/metadata-modules/ai/ai-models/types/default-smart-model.const';
|
||||
import { AUTO_SELECT_SMART_MODEL_ID } from 'twenty-shared/constants';
|
||||
import { type FlatAgent } from 'src/engine/metadata-modules/flat-agent/types/flat-agent.type';
|
||||
import { type AllStandardAgentName } from 'src/engine/workspace-manager/twenty-standard-application/types/all-standard-agent-name.type';
|
||||
import {
|
||||
@@ -40,7 +40,7 @@ Response format:
|
||||
- Use markdown for readability
|
||||
|
||||
Always base answers on official Twenty documentation. Be patient and helpful.`,
|
||||
modelId: DEFAULT_SMART_MODEL,
|
||||
modelId: AUTO_SELECT_SMART_MODEL_ID,
|
||||
responseFormat: { type: 'text' },
|
||||
isCustom: false,
|
||||
modelConfiguration: {},
|
||||
|
||||
Reference in New Issue
Block a user