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