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:
@ -1042,6 +1042,7 @@ export enum MessageChannelVisibility {
|
|||||||
|
|
||||||
export enum ModelProvider {
|
export enum ModelProvider {
|
||||||
ANTHROPIC = 'ANTHROPIC',
|
ANTHROPIC = 'ANTHROPIC',
|
||||||
|
NONE = 'NONE',
|
||||||
OPENAI = 'OPENAI'
|
OPENAI = 'OPENAI'
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2785,6 +2786,7 @@ export type Workspace = {
|
|||||||
customDomain?: Maybe<Scalars['String']>;
|
customDomain?: Maybe<Scalars['String']>;
|
||||||
databaseSchema: Scalars['String'];
|
databaseSchema: Scalars['String'];
|
||||||
databaseUrl: Scalars['String'];
|
databaseUrl: Scalars['String'];
|
||||||
|
defaultAgent?: Maybe<Agent>;
|
||||||
defaultRole?: Maybe<Role>;
|
defaultRole?: Maybe<Role>;
|
||||||
deletedAt?: Maybe<Scalars['DateTime']>;
|
deletedAt?: Maybe<Scalars['DateTime']>;
|
||||||
displayName?: Maybe<Scalars['String']>;
|
displayName?: Maybe<Scalars['String']>;
|
||||||
@ -3599,7 +3601,7 @@ export type FindOneServerlessFunctionSourceCodeQueryVariables = Exact<{
|
|||||||
|
|
||||||
export type FindOneServerlessFunctionSourceCodeQuery = { __typename?: 'Query', getServerlessFunctionSourceCode?: any | null };
|
export type FindOneServerlessFunctionSourceCodeQuery = { __typename?: 'Query', getServerlessFunctionSourceCode?: any | null };
|
||||||
|
|
||||||
export type UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canAccessFullAdminPanel: boolean, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars?: any | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, deletedWorkspaceMembers?: Array<{ __typename?: 'DeletedWorkspaceMember', id: any, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', settingsPermissions?: Array<SettingPermissionType> | null, objectRecordsPermissions?: Array<PermissionsOnAllObjectRecords> | null, objectPermissions?: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null }> | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, customDomain?: string | null, isCustomDomainEnabled: boolean, metadataVersion: number, workspaceMembersCount?: number | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlagDTO', key: FeatureFlagKey, value: boolean }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null, metadata: any, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItem', id: any, hasReachedCurrentPeriodCap: boolean, quantity?: number | null, billingProduct?: { __typename?: 'BillingProduct', name: string, description: string, metadata: { __typename?: 'BillingProductMetadata', planKey: BillingPlanKey, priceUsageBased: BillingUsageType, productKey: BillingProductKey } } | null }> | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, metadata: any }>, defaultRole?: { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean } | null } | null, availableWorkspaces: { __typename?: 'AvailableWorkspaces', availableWorkspacesForSignIn: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }>, availableWorkspacesForSignUp: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }> } };
|
export type UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canAccessFullAdminPanel: boolean, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars?: any | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, deletedWorkspaceMembers?: Array<{ __typename?: 'DeletedWorkspaceMember', id: any, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', settingsPermissions?: Array<SettingPermissionType> | null, objectRecordsPermissions?: Array<PermissionsOnAllObjectRecords> | null, objectPermissions?: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null }> | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, customDomain?: string | null, isCustomDomainEnabled: boolean, metadataVersion: number, workspaceMembersCount?: number | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlagDTO', key: FeatureFlagKey, value: boolean }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null, metadata: any, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItem', id: any, hasReachedCurrentPeriodCap: boolean, quantity?: number | null, billingProduct?: { __typename?: 'BillingProduct', name: string, description: string, metadata: { __typename?: 'BillingProductMetadata', planKey: BillingPlanKey, priceUsageBased: BillingUsageType, productKey: BillingProductKey } } | null }> | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, metadata: any }>, defaultRole?: { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean } | null, defaultAgent?: { __typename?: 'Agent', id: any } | null } | null, availableWorkspaces: { __typename?: 'AvailableWorkspaces', availableWorkspacesForSignIn: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }>, availableWorkspacesForSignUp: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }> } };
|
||||||
|
|
||||||
export type WorkspaceUrlsFragmentFragment = { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null };
|
export type WorkspaceUrlsFragmentFragment = { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null };
|
||||||
|
|
||||||
@ -3618,7 +3620,7 @@ export type UploadProfilePictureMutation = { __typename?: 'Mutation', uploadProf
|
|||||||
export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>;
|
export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>;
|
||||||
|
|
||||||
|
|
||||||
export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canAccessFullAdminPanel: boolean, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars?: any | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, deletedWorkspaceMembers?: Array<{ __typename?: 'DeletedWorkspaceMember', id: any, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', settingsPermissions?: Array<SettingPermissionType> | null, objectRecordsPermissions?: Array<PermissionsOnAllObjectRecords> | null, objectPermissions?: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null }> | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, customDomain?: string | null, isCustomDomainEnabled: boolean, metadataVersion: number, workspaceMembersCount?: number | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlagDTO', key: FeatureFlagKey, value: boolean }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null, metadata: any, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItem', id: any, hasReachedCurrentPeriodCap: boolean, quantity?: number | null, billingProduct?: { __typename?: 'BillingProduct', name: string, description: string, metadata: { __typename?: 'BillingProductMetadata', planKey: BillingPlanKey, priceUsageBased: BillingUsageType, productKey: BillingProductKey } } | null }> | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, metadata: any }>, defaultRole?: { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean } | null } | null, availableWorkspaces: { __typename?: 'AvailableWorkspaces', availableWorkspacesForSignIn: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }>, availableWorkspacesForSignUp: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }> } } };
|
export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canAccessFullAdminPanel: boolean, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars?: any | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, deletedWorkspaceMembers?: Array<{ __typename?: 'DeletedWorkspaceMember', id: any, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', settingsPermissions?: Array<SettingPermissionType> | null, objectRecordsPermissions?: Array<PermissionsOnAllObjectRecords> | null, objectPermissions?: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null }> | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, customDomain?: string | null, isCustomDomainEnabled: boolean, metadataVersion: number, workspaceMembersCount?: number | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlagDTO', key: FeatureFlagKey, value: boolean }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null, metadata: any, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItem', id: any, hasReachedCurrentPeriodCap: boolean, quantity?: number | null, billingProduct?: { __typename?: 'BillingProduct', name: string, description: string, metadata: { __typename?: 'BillingProductMetadata', planKey: BillingPlanKey, priceUsageBased: BillingUsageType, productKey: BillingProductKey } } | null }> | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, metadata: any }>, defaultRole?: { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean } | null, defaultAgent?: { __typename?: 'Agent', id: any } | null } | null, availableWorkspaces: { __typename?: 'AvailableWorkspaces', availableWorkspacesForSignIn: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }>, availableWorkspacesForSignUp: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }> } } };
|
||||||
|
|
||||||
export type ActivateWorkflowVersionMutationVariables = Exact<{
|
export type ActivateWorkflowVersionMutationVariables = Exact<{
|
||||||
workflowVersionId: Scalars['String'];
|
workflowVersionId: Scalars['String'];
|
||||||
@ -4042,6 +4044,9 @@ export const UserQueryFragmentFragmentDoc = gql`
|
|||||||
defaultRole {
|
defaultRole {
|
||||||
...RoleFragment
|
...RoleFragment
|
||||||
}
|
}
|
||||||
|
defaultAgent {
|
||||||
|
id
|
||||||
|
}
|
||||||
}
|
}
|
||||||
availableWorkspaces {
|
availableWorkspaces {
|
||||||
...AvailableWorkspacesFragment
|
...AvailableWorkspacesFragment
|
||||||
|
|||||||
@ -999,6 +999,7 @@ export enum MessageChannelVisibility {
|
|||||||
|
|
||||||
export enum ModelProvider {
|
export enum ModelProvider {
|
||||||
ANTHROPIC = 'ANTHROPIC',
|
ANTHROPIC = 'ANTHROPIC',
|
||||||
|
NONE = 'NONE',
|
||||||
OPENAI = 'OPENAI'
|
OPENAI = 'OPENAI'
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2613,6 +2614,7 @@ export type Workspace = {
|
|||||||
customDomain?: Maybe<Scalars['String']>;
|
customDomain?: Maybe<Scalars['String']>;
|
||||||
databaseSchema: Scalars['String'];
|
databaseSchema: Scalars['String'];
|
||||||
databaseUrl: Scalars['String'];
|
databaseUrl: Scalars['String'];
|
||||||
|
defaultAgent?: Maybe<Agent>;
|
||||||
defaultRole?: Maybe<Role>;
|
defaultRole?: Maybe<Role>;
|
||||||
deletedAt?: Maybe<Scalars['DateTime']>;
|
deletedAt?: Maybe<Scalars['DateTime']>;
|
||||||
displayName?: Maybe<Scalars['String']>;
|
displayName?: Maybe<Scalars['String']>;
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import { ActionType } from '@/action-menu/actions/types/ActionType';
|
|||||||
import { ActionViewType } from '@/action-menu/actions/types/ActionViewType';
|
import { ActionViewType } from '@/action-menu/actions/types/ActionViewType';
|
||||||
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
|
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
|
||||||
import { msg } from '@lingui/core/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { IconSearch } from 'twenty-ui/display';
|
import { IconSearch, IconSparkles } from 'twenty-ui/display';
|
||||||
|
|
||||||
export const RECORD_AGNOSTIC_ACTIONS_CONFIG: Record<string, ActionConfig> = {
|
export const RECORD_AGNOSTIC_ACTIONS_CONFIG: Record<string, ActionConfig> = {
|
||||||
[RecordAgnosticActionsKeys.SEARCH_RECORDS]: {
|
[RecordAgnosticActionsKeys.SEARCH_RECORDS]: {
|
||||||
@ -50,4 +50,24 @@ export const RECORD_AGNOSTIC_ACTIONS_CONFIG: Record<string, ActionConfig> = {
|
|||||||
hotKeys: ['/'],
|
hotKeys: ['/'],
|
||||||
shouldBeRegistered: () => true,
|
shouldBeRegistered: () => true,
|
||||||
},
|
},
|
||||||
|
[RecordAgnosticActionsKeys.ASK_AI]: {
|
||||||
|
type: ActionType.Standard,
|
||||||
|
scope: ActionScope.Global,
|
||||||
|
key: RecordAgnosticActionsKeys.ASK_AI,
|
||||||
|
label: msg`Ask AI`,
|
||||||
|
shortLabel: msg`Ask AI`,
|
||||||
|
position: 2,
|
||||||
|
isPinned: false,
|
||||||
|
Icon: IconSparkles,
|
||||||
|
availableOn: [ActionViewType.GLOBAL],
|
||||||
|
component: (
|
||||||
|
<ActionOpenSidePanelPage
|
||||||
|
page={CommandMenuPages.AskAI}
|
||||||
|
pageTitle="Ask AI"
|
||||||
|
pageIcon={IconSparkles}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
hotKeys: ['@'],
|
||||||
|
shouldBeRegistered: () => true,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,25 @@
|
|||||||
|
import { RECORD_AGNOSTIC_ACTIONS_CONFIG } from '@/action-menu/actions/record-agnostic-actions/constants/RecordAgnosticActionsConfig';
|
||||||
|
import { RecordAgnosticActionsKeys } from '@/action-menu/actions/record-agnostic-actions/types/RecordAgnosticActionsKeys';
|
||||||
|
import { ActionConfig } from '@/action-menu/actions/types/ActionConfig';
|
||||||
|
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||||
|
import { FeatureFlagKey } from '~/generated/graphql';
|
||||||
|
|
||||||
|
export const useRecordAgnosticActions = () => {
|
||||||
|
const isAiEnabled = useIsFeatureEnabled(FeatureFlagKey.IS_AI_ENABLED);
|
||||||
|
|
||||||
|
const actions: Record<string, ActionConfig> = {
|
||||||
|
[RecordAgnosticActionsKeys.SEARCH_RECORDS]:
|
||||||
|
RECORD_AGNOSTIC_ACTIONS_CONFIG[RecordAgnosticActionsKeys.SEARCH_RECORDS],
|
||||||
|
[RecordAgnosticActionsKeys.SEARCH_RECORDS_FALLBACK]:
|
||||||
|
RECORD_AGNOSTIC_ACTIONS_CONFIG[
|
||||||
|
RecordAgnosticActionsKeys.SEARCH_RECORDS_FALLBACK
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isAiEnabled) {
|
||||||
|
actions[RecordAgnosticActionsKeys.ASK_AI] =
|
||||||
|
RECORD_AGNOSTIC_ACTIONS_CONFIG[RecordAgnosticActionsKeys.ASK_AI];
|
||||||
|
}
|
||||||
|
|
||||||
|
return actions;
|
||||||
|
};
|
||||||
@ -1,4 +1,5 @@
|
|||||||
export enum RecordAgnosticActionsKeys {
|
export enum RecordAgnosticActionsKeys {
|
||||||
SEARCH_RECORDS = 'search-records',
|
SEARCH_RECORDS = 'search-records',
|
||||||
SEARCH_RECORDS_FALLBACK = 'search-records-fallback',
|
SEARCH_RECORDS_FALLBACK = 'search-records-fallback',
|
||||||
|
ASK_AI = 'ask-ai',
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { RECORD_AGNOSTIC_ACTIONS_CONFIG } from '@/action-menu/actions/record-agnostic-actions/constants/RecordAgnosticActionsConfig';
|
import { useRecordAgnosticActions } from '@/action-menu/actions/record-agnostic-actions/hooks/useRecordAgnosticActions';
|
||||||
import { ActionViewType } from '@/action-menu/actions/types/ActionViewType';
|
import { ActionViewType } from '@/action-menu/actions/types/ActionViewType';
|
||||||
import { ShouldBeRegisteredFunctionParams } from '@/action-menu/actions/types/ShouldBeRegisteredFunctionParams';
|
import { ShouldBeRegisteredFunctionParams } from '@/action-menu/actions/types/ShouldBeRegisteredFunctionParams';
|
||||||
import { getActionConfig } from '@/action-menu/actions/utils/getActionConfig';
|
import { getActionConfig } from '@/action-menu/actions/utils/getActionConfig';
|
||||||
@ -30,7 +30,7 @@ export const useRegisteredActions = (
|
|||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
});
|
});
|
||||||
|
|
||||||
const recordAgnosticActionConfig = RECORD_AGNOSTIC_ACTIONS_CONFIG;
|
const recordAgnosticActionConfig = useRecordAgnosticActions();
|
||||||
|
|
||||||
const actionsConfig = {
|
const actionsConfig = {
|
||||||
...recordActionConfig,
|
...recordActionConfig,
|
||||||
|
|||||||
@ -2,7 +2,10 @@ import {
|
|||||||
ApolloClient,
|
ApolloClient,
|
||||||
ApolloClientOptions,
|
ApolloClientOptions,
|
||||||
ApolloLink,
|
ApolloLink,
|
||||||
|
FetchResult,
|
||||||
fromPromise,
|
fromPromise,
|
||||||
|
Observable,
|
||||||
|
Operation,
|
||||||
ServerError,
|
ServerError,
|
||||||
ServerParseError,
|
ServerParseError,
|
||||||
} from '@apollo/client';
|
} from '@apollo/client';
|
||||||
@ -19,7 +22,12 @@ import { AuthTokenPair } from '~/generated/graphql';
|
|||||||
import { logDebug } from '~/utils/logDebug';
|
import { logDebug } from '~/utils/logDebug';
|
||||||
|
|
||||||
import { i18n } from '@lingui/core';
|
import { i18n } from '@lingui/core';
|
||||||
import { GraphQLFormattedError } from 'graphql';
|
import {
|
||||||
|
DefinitionNode,
|
||||||
|
DirectiveNode,
|
||||||
|
GraphQLFormattedError,
|
||||||
|
SelectionNode,
|
||||||
|
} from 'graphql';
|
||||||
import isEmpty from 'lodash.isempty';
|
import isEmpty from 'lodash.isempty';
|
||||||
import { getGenericOperationName, isDefined } from 'twenty-shared/utils';
|
import { getGenericOperationName, isDefined } from 'twenty-shared/utils';
|
||||||
import { REACT_APP_SERVER_BASE_URL } from '~/config';
|
import { REACT_APP_SERVER_BASE_URL } from '~/config';
|
||||||
@ -115,45 +123,45 @@ export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
|
|||||||
},
|
},
|
||||||
attempts: {
|
attempts: {
|
||||||
max: 2,
|
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(
|
const errorLink = onError(
|
||||||
({ graphQLErrors, networkError, forward, operation }) => {
|
({ graphQLErrors, networkError, forward, operation }) => {
|
||||||
if (isDefined(graphQLErrors)) {
|
if (isDefined(graphQLErrors)) {
|
||||||
onErrorCb?.(graphQLErrors);
|
onErrorCb?.(graphQLErrors);
|
||||||
for (const graphQLError of graphQLErrors) {
|
for (const graphQLError of graphQLErrors) {
|
||||||
if (graphQLError.message === 'Unauthorized') {
|
if (graphQLError.message === 'Unauthorized') {
|
||||||
return fromPromise(
|
return handleTokenRenewal(operation, forward);
|
||||||
renewToken(uri, getTokenPair())
|
|
||||||
.then((tokens) => {
|
|
||||||
if (isDefined(tokens)) {
|
|
||||||
onTokenPairChange?.(tokens);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
onUnauthenticatedError?.();
|
|
||||||
}),
|
|
||||||
).flatMap(() => forward(operation));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (graphQLError?.extensions?.code) {
|
switch (graphQLError?.extensions?.code) {
|
||||||
case 'UNAUTHENTICATED': {
|
case 'UNAUTHENTICATED': {
|
||||||
return fromPromise(
|
return handleTokenRenewal(operation, forward);
|
||||||
renewToken(uri, getTokenPair())
|
|
||||||
.then((tokens) => {
|
|
||||||
if (isDefined(tokens)) {
|
|
||||||
onTokenPairChange?.(tokens);
|
|
||||||
cookieStorage.setItem(
|
|
||||||
'tokenPair',
|
|
||||||
JSON.stringify(tokens),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
onUnauthenticatedError?.();
|
|
||||||
}),
|
|
||||||
).flatMap(() => forward(operation));
|
|
||||||
}
|
}
|
||||||
case 'FORBIDDEN': {
|
case 'FORBIDDEN': {
|
||||||
return;
|
return;
|
||||||
@ -220,6 +228,13 @@ export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isDefined(networkError)) {
|
if (isDefined(networkError)) {
|
||||||
|
if (
|
||||||
|
this.isRestOperation(operation) &&
|
||||||
|
this.isAuthenticationError(networkError as ServerError)
|
||||||
|
) {
|
||||||
|
return handleTokenRenewal(operation, forward);
|
||||||
|
}
|
||||||
|
|
||||||
if (isDebugMode === true) {
|
if (isDebugMode === true) {
|
||||||
logDebug(`[Network error]: ${networkError}`);
|
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) {
|
updateWorkspaceMember(workspaceMember: CurrentWorkspaceMember | null) {
|
||||||
this.currentWorkspaceMember = workspaceMember;
|
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 { FetchResult } from '@apollo/client/link/core';
|
||||||
import { ArgumentNode, DirectiveNode } from 'graphql';
|
import { ArgumentNode, DirectiveNode } from 'graphql';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
@ -57,7 +62,13 @@ export class StreamingRestLink extends ApolloLink {
|
|||||||
fetch(url, requestConfig)
|
fetch(url, requestConfig)
|
||||||
.then(async (response) => {
|
.then(async (response) => {
|
||||||
if (!response.ok) {
|
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) {
|
if (!response.body) {
|
||||||
@ -66,7 +77,6 @@ export class StreamingRestLink extends ApolloLink {
|
|||||||
|
|
||||||
const reader = response.body.getReader();
|
const reader = response.body.getReader();
|
||||||
const decoder = new TextDecoder();
|
const decoder = new TextDecoder();
|
||||||
let accumulatedData = '';
|
|
||||||
|
|
||||||
let isStreaming = true;
|
let isStreaming = true;
|
||||||
while (isStreaming) {
|
while (isStreaming) {
|
||||||
@ -79,19 +89,9 @@ export class StreamingRestLink extends ApolloLink {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const decodedChunk = decoder.decode(value, { stream: true });
|
const decodedChunk = decoder.decode(value, { stream: true });
|
||||||
accumulatedData += decodedChunk;
|
|
||||||
|
|
||||||
if (isDefined(onChunk) && typeof onChunk === 'function') {
|
if (isDefined(onChunk) && typeof onChunk === 'function') {
|
||||||
onChunk(accumulatedData);
|
onChunk(decodedChunk);
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const parsedData = JSON.parse(decodedChunk);
|
|
||||||
observer.next({ data: parsedData });
|
|
||||||
} catch {
|
|
||||||
observer.next({
|
|
||||||
data: { streamingData: decodedChunk },
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@ -25,6 +25,7 @@ export type CurrentWorkspace = Pick<
|
|||||||
| 'metadataVersion'
|
| 'metadataVersion'
|
||||||
> & {
|
> & {
|
||||||
defaultRole?: Omit<Role, 'workspaceMembers'> | null;
|
defaultRole?: Omit<Role, 'workspaceMembers'> | null;
|
||||||
|
defaultAgent?: { id: string } | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const currentWorkspaceState = createState<CurrentWorkspace | null>({
|
export const currentWorkspaceState = createState<CurrentWorkspace | null>({
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { CommandMenu } from '@/command-menu/components/CommandMenu';
|
import { CommandMenu } from '@/command-menu/components/CommandMenu';
|
||||||
|
import { CommandMenuAskAIPage } from '@/command-menu/pages/ask-ai/components/CommandMenuAskAIPage';
|
||||||
import { CommandMenuCalendarEventPage } from '@/command-menu/pages/calendar-event/components/CommandMenuCalendarEventPage';
|
import { CommandMenuCalendarEventPage } from '@/command-menu/pages/calendar-event/components/CommandMenuCalendarEventPage';
|
||||||
import { CommandMenuMessageThreadPage } from '@/command-menu/pages/message-thread/components/CommandMenuMessageThreadPage';
|
import { CommandMenuMessageThreadPage } from '@/command-menu/pages/message-thread/components/CommandMenuMessageThreadPage';
|
||||||
import { CommandMenuRecordPage } from '@/command-menu/pages/record-page/components/CommandMenuRecordPage';
|
import { CommandMenuRecordPage } from '@/command-menu/pages/record-page/components/CommandMenuRecordPage';
|
||||||
@ -32,4 +33,5 @@ export const COMMAND_MENU_PAGES_CONFIG = new Map<
|
|||||||
[CommandMenuPages.WorkflowStepView, <CommandMenuWorkflowViewStep />],
|
[CommandMenuPages.WorkflowStepView, <CommandMenuWorkflowViewStep />],
|
||||||
[CommandMenuPages.WorkflowRunStepView, <CommandMenuWorkflowRunViewStep />],
|
[CommandMenuPages.WorkflowRunStepView, <CommandMenuWorkflowRunViewStep />],
|
||||||
[CommandMenuPages.SearchRecords, <CommandMenuSearchRecordsPage />],
|
[CommandMenuPages.SearchRecords, <CommandMenuSearchRecordsPage />],
|
||||||
|
[CommandMenuPages.AskAI, <CommandMenuAskAIPage />],
|
||||||
]);
|
]);
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { COMMAND_MENU_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/Com
|
|||||||
import { SIDE_PANEL_FOCUS_ID } from '@/command-menu/constants/SidePanelFocusId';
|
import { SIDE_PANEL_FOCUS_ID } from '@/command-menu/constants/SidePanelFocusId';
|
||||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||||
import { useCommandMenuHistory } from '@/command-menu/hooks/useCommandMenuHistory';
|
import { useCommandMenuHistory } from '@/command-menu/hooks/useCommandMenuHistory';
|
||||||
|
import { useOpenAskAIPageInCommandMenu } from '@/command-menu/hooks/useOpenAskAIPageInCommandMenu';
|
||||||
import { useOpenRecordsSearchPageInCommandMenu } from '@/command-menu/hooks/useOpenRecordsSearchPageInCommandMenu';
|
import { useOpenRecordsSearchPageInCommandMenu } from '@/command-menu/hooks/useOpenRecordsSearchPageInCommandMenu';
|
||||||
import { useSetGlobalCommandMenuContext } from '@/command-menu/hooks/useSetGlobalCommandMenuContext';
|
import { useSetGlobalCommandMenuContext } from '@/command-menu/hooks/useSetGlobalCommandMenuContext';
|
||||||
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
|
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
|
||||||
@ -12,15 +13,19 @@ import { useKeyboardShortcutMenu } from '@/keyboard-shortcut-menu/hooks/useKeybo
|
|||||||
import { useGlobalHotkeys } from '@/ui/utilities/hotkey/hooks/useGlobalHotkeys';
|
import { useGlobalHotkeys } from '@/ui/utilities/hotkey/hooks/useGlobalHotkeys';
|
||||||
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
|
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
|
||||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
|
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||||
import { isNonEmptyString } from '@sniptt/guards';
|
import { isNonEmptyString } from '@sniptt/guards';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { Key } from 'ts-key-enum';
|
import { Key } from 'ts-key-enum';
|
||||||
|
import { FeatureFlagKey } from '~/generated/graphql';
|
||||||
|
|
||||||
export const useCommandMenuHotKeys = () => {
|
export const useCommandMenuHotKeys = () => {
|
||||||
const { toggleCommandMenu } = useCommandMenu();
|
const { toggleCommandMenu } = useCommandMenu();
|
||||||
|
|
||||||
const { openRecordsSearchPage } = useOpenRecordsSearchPageInCommandMenu();
|
const { openRecordsSearchPage } = useOpenRecordsSearchPageInCommandMenu();
|
||||||
|
|
||||||
|
const { openAskAIPage } = useOpenAskAIPageInCommandMenu();
|
||||||
|
|
||||||
const { goBackFromCommandMenu } = useCommandMenuHistory();
|
const { goBackFromCommandMenu } = useCommandMenuHistory();
|
||||||
|
|
||||||
const { setGlobalCommandMenuContext } = useSetGlobalCommandMenuContext();
|
const { setGlobalCommandMenuContext } = useSetGlobalCommandMenuContext();
|
||||||
@ -31,6 +36,8 @@ export const useCommandMenuHotKeys = () => {
|
|||||||
|
|
||||||
const commandMenuPage = useRecoilValue(commandMenuPageState);
|
const commandMenuPage = useRecoilValue(commandMenuPageState);
|
||||||
|
|
||||||
|
const isAiEnabled = useIsFeatureEnabled(FeatureFlagKey.IS_AI_ENABLED);
|
||||||
|
|
||||||
const contextStoreTargetedRecordsRuleComponent = useRecoilComponentValueV2(
|
const contextStoreTargetedRecordsRuleComponent = useRecoilComponentValueV2(
|
||||||
contextStoreTargetedRecordsRuleComponentState,
|
contextStoreTargetedRecordsRuleComponentState,
|
||||||
COMMAND_MENU_COMPONENT_INSTANCE_ID,
|
COMMAND_MENU_COMPONENT_INSTANCE_ID,
|
||||||
@ -58,6 +65,20 @@ export const useCommandMenuHotKeys = () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useGlobalHotkeys(
|
||||||
|
['@'],
|
||||||
|
() => {
|
||||||
|
if (isAiEnabled) {
|
||||||
|
openAskAIPage();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
[openAskAIPage, isAiEnabled],
|
||||||
|
{
|
||||||
|
ignoreModifiers: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
useHotkeysOnFocusedElement({
|
useHotkeysOnFocusedElement({
|
||||||
keys: [Key.Escape],
|
keys: [Key.Escape],
|
||||||
callback: () => {
|
callback: () => {
|
||||||
|
|||||||
@ -0,0 +1,22 @@
|
|||||||
|
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||||
|
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
|
||||||
|
import { t } from '@lingui/core/macro';
|
||||||
|
import { IconSparkles } from 'twenty-ui/display';
|
||||||
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
|
export const useOpenAskAIPageInCommandMenu = () => {
|
||||||
|
const { navigateCommandMenu } = useCommandMenu();
|
||||||
|
|
||||||
|
const openAskAIPage = () => {
|
||||||
|
navigateCommandMenu({
|
||||||
|
page: CommandMenuPages.AskAI,
|
||||||
|
pageTitle: t`Ask AI`,
|
||||||
|
pageIcon: IconSparkles,
|
||||||
|
pageId: v4(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
openAskAIPage,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||||
|
import { AIChatTab } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/components/AIChatTab';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
|
const StyledContainer = styled.div`
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledEmptyState = styled.div`
|
||||||
|
align-items: center;
|
||||||
|
color: ${({ theme }) => theme.font.color.secondary};
|
||||||
|
display: flex;
|
||||||
|
font-size: ${({ theme }) => theme.font.size.md};
|
||||||
|
height: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const CommandMenuAskAIPage = () => {
|
||||||
|
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
||||||
|
const agentId = currentWorkspace?.defaultAgent?.id;
|
||||||
|
|
||||||
|
if (!agentId) {
|
||||||
|
return (
|
||||||
|
<StyledContainer>
|
||||||
|
<StyledEmptyState>No AI Agent found.</StyledEmptyState>
|
||||||
|
</StyledContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledContainer>
|
||||||
|
<AIChatTab agentId={agentId} />
|
||||||
|
</StyledContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -11,4 +11,5 @@ export enum CommandMenuPages {
|
|||||||
WorkflowStepEdit = 'workflow-step-edit',
|
WorkflowStepEdit = 'workflow-step-edit',
|
||||||
WorkflowRunStepView = 'workflow-run-step-view',
|
WorkflowRunStepView = 'workflow-run-step-view',
|
||||||
SearchRecords = 'search-records',
|
SearchRecords = 'search-records',
|
||||||
|
AskAI = 'ask-ai',
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,14 +1,17 @@
|
|||||||
|
import { useOpenAskAIPageInCommandMenu } from '@/command-menu/hooks/useOpenAskAIPageInCommandMenu';
|
||||||
import { useOpenRecordsSearchPageInCommandMenu } from '@/command-menu/hooks/useOpenRecordsSearchPageInCommandMenu';
|
import { useOpenRecordsSearchPageInCommandMenu } from '@/command-menu/hooks/useOpenRecordsSearchPageInCommandMenu';
|
||||||
import { SettingsPath } from '@/types/SettingsPath';
|
import { SettingsPath } from '@/types/SettingsPath';
|
||||||
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
|
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
|
||||||
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
|
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
|
||||||
import { navigationDrawerExpandedMemorizedState } from '@/ui/navigation/states/navigationDrawerExpandedMemorizedState';
|
import { navigationDrawerExpandedMemorizedState } from '@/ui/navigation/states/navigationDrawerExpandedMemorizedState';
|
||||||
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
|
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
|
||||||
|
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||||
import { useLingui } from '@lingui/react/macro';
|
import { useLingui } from '@lingui/react/macro';
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { useRecoilState, useSetRecoilState } from 'recoil';
|
import { useRecoilState, useSetRecoilState } from 'recoil';
|
||||||
import { IconSearch, IconSettings } from 'twenty-ui/display';
|
import { IconSearch, IconSettings, IconSparkles } from 'twenty-ui/display';
|
||||||
import { useIsMobile } from 'twenty-ui/utilities';
|
import { useIsMobile } from 'twenty-ui/utilities';
|
||||||
|
import { FeatureFlagKey } from '~/generated/graphql';
|
||||||
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
|
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
|
||||||
|
|
||||||
export const MainNavigationDrawerFixedItems = () => {
|
export const MainNavigationDrawerFixedItems = () => {
|
||||||
@ -29,6 +32,9 @@ export const MainNavigationDrawerFixedItems = () => {
|
|||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
|
|
||||||
const { openRecordsSearchPage } = useOpenRecordsSearchPageInCommandMenu();
|
const { openRecordsSearchPage } = useOpenRecordsSearchPageInCommandMenu();
|
||||||
|
const { openAskAIPage } = useOpenAskAIPageInCommandMenu();
|
||||||
|
const isAiEnabled = useIsFeatureEnabled(FeatureFlagKey.IS_AI_ENABLED);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
!isMobile && (
|
!isMobile && (
|
||||||
<>
|
<>
|
||||||
@ -39,6 +45,15 @@ export const MainNavigationDrawerFixedItems = () => {
|
|||||||
keyboard={['/']}
|
keyboard={['/']}
|
||||||
mouseUpNavigation={true}
|
mouseUpNavigation={true}
|
||||||
/>
|
/>
|
||||||
|
{isAiEnabled && (
|
||||||
|
<NavigationDrawerItem
|
||||||
|
label={t`Ask AI`}
|
||||||
|
Icon={IconSparkles}
|
||||||
|
onClick={openAskAIPage}
|
||||||
|
keyboard={['@']}
|
||||||
|
mouseUpNavigation={true}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<NavigationDrawerItem
|
<NavigationDrawerItem
|
||||||
label={t`Settings`}
|
label={t`Settings`}
|
||||||
to={getSettingsPath(SettingsPath.ProfilePage)}
|
to={getSettingsPath(SettingsPath.ProfilePage)}
|
||||||
|
|||||||
@ -93,6 +93,8 @@ export const UserProviderEffect = () => {
|
|||||||
setCurrentWorkspace({
|
setCurrentWorkspace({
|
||||||
...queryData.currentUser.currentWorkspace,
|
...queryData.currentUser.currentWorkspace,
|
||||||
defaultRole: queryData.currentUser.currentWorkspace.defaultRole ?? null,
|
defaultRole: queryData.currentUser.currentWorkspace.defaultRole ?? null,
|
||||||
|
defaultAgent:
|
||||||
|
queryData.currentUser.currentWorkspace.defaultAgent ?? null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
import { OBJECT_PERMISSION_FRAGMENT } from '@/settings/roles/graphql/fragments/objectPermissionFragment';
|
|
||||||
import { ROLE_FRAGMENT } from '@/settings/roles/graphql/fragments/roleFragment';
|
|
||||||
import { DELETED_WORKSPACE_MEMBER_QUERY_FRAGMENT } from '@/workspace-member/graphql/fragments/deletedWorkspaceMemberQueryFragment';
|
|
||||||
import { WORKSPACE_MEMBER_QUERY_FRAGMENT } from '@/workspace-member/graphql/fragments/workspaceMemberQueryFragment';
|
|
||||||
import { gql } from '@apollo/client';
|
|
||||||
import {
|
import {
|
||||||
AVAILABLE_WORKSPACE_FOR_AUTH_FRAGMENT,
|
AVAILABLE_WORKSPACE_FOR_AUTH_FRAGMENT,
|
||||||
AVAILABLE_WORKSPACES_FOR_AUTH_FRAGMENT,
|
AVAILABLE_WORKSPACES_FOR_AUTH_FRAGMENT,
|
||||||
} from '@/auth/graphql/fragments/authFragments';
|
} from '@/auth/graphql/fragments/authFragments';
|
||||||
|
import { OBJECT_PERMISSION_FRAGMENT } from '@/settings/roles/graphql/fragments/objectPermissionFragment';
|
||||||
|
import { ROLE_FRAGMENT } from '@/settings/roles/graphql/fragments/roleFragment';
|
||||||
import { WORKSPACE_URLS_FRAGMENT } from '@/users/graphql/fragments/workspaceUrlsFragment';
|
import { WORKSPACE_URLS_FRAGMENT } from '@/users/graphql/fragments/workspaceUrlsFragment';
|
||||||
|
import { DELETED_WORKSPACE_MEMBER_QUERY_FRAGMENT } from '@/workspace-member/graphql/fragments/deletedWorkspaceMemberQueryFragment';
|
||||||
|
import { WORKSPACE_MEMBER_QUERY_FRAGMENT } from '@/workspace-member/graphql/fragments/workspaceMemberQueryFragment';
|
||||||
|
import { gql } from '@apollo/client';
|
||||||
|
|
||||||
export const USER_QUERY_FRAGMENT = gql`
|
export const USER_QUERY_FRAGMENT = gql`
|
||||||
${ROLE_FRAGMENT}
|
${ROLE_FRAGMENT}
|
||||||
@ -92,6 +92,9 @@ export const USER_QUERY_FRAGMENT = gql`
|
|||||||
defaultRole {
|
defaultRole {
|
||||||
...RoleFragment
|
...RoleFragment
|
||||||
}
|
}
|
||||||
|
defaultAgent {
|
||||||
|
id
|
||||||
|
}
|
||||||
}
|
}
|
||||||
availableWorkspaces {
|
availableWorkspaces {
|
||||||
...AvailableWorkspacesFragment
|
...AvailableWorkspacesFragment
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { TextArea } from '@/ui/input/components/TextArea';
|
import { TextArea } from '@/ui/input/components/TextArea';
|
||||||
import { useTheme } from '@emotion/react';
|
import { keyframes, useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Avatar, IconDotsVertical, IconSparkles } from 'twenty-ui/display';
|
import { Avatar, IconDotsVertical, IconSparkles } from 'twenty-ui/display';
|
||||||
@ -11,6 +11,7 @@ import { t } from '@lingui/core/macro';
|
|||||||
import { Button } from 'twenty-ui/input';
|
import { Button } from 'twenty-ui/input';
|
||||||
import { beautifyPastDateRelativeToNow } from '~/utils/date-utils';
|
import { beautifyPastDateRelativeToNow } from '~/utils/date-utils';
|
||||||
import { useAgentChat } from '../hooks/useAgentChat';
|
import { useAgentChat } from '../hooks/useAgentChat';
|
||||||
|
import { AgentChatMessage } from '../hooks/useAgentChatMessages';
|
||||||
import { AIChatSkeletonLoader } from './AIChatSkeletonLoader';
|
import { AIChatSkeletonLoader } from './AIChatSkeletonLoader';
|
||||||
|
|
||||||
const StyledContainer = styled.div`
|
const StyledContainer = styled.div`
|
||||||
@ -86,9 +87,11 @@ const StyledMessageBubble = styled.div<{ isUser?: boolean }>`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledMessageRow = styled.div`
|
const StyledMessageRow = styled.div<{ isShowingToolCall?: boolean }>`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
align-items: ${({ isShowingToolCall }) =>
|
||||||
|
isShowingToolCall ? 'center' : 'flex-start'};
|
||||||
gap: ${({ theme }) => theme.spacing(3)};
|
gap: ${({ theme }) => theme.spacing(3)};
|
||||||
width: 100%;
|
width: 100%;
|
||||||
`;
|
`;
|
||||||
@ -152,6 +155,23 @@ const StyledMessageContainer = styled.div`
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const dots = keyframes`
|
||||||
|
0% { content: ''; }
|
||||||
|
33% { content: '.'; }
|
||||||
|
66% { content: '..'; }
|
||||||
|
100% { content: '...'; }
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledToolCallContainer = styled.div`
|
||||||
|
&::after {
|
||||||
|
display: inline-block;
|
||||||
|
content: '';
|
||||||
|
animation: ${dots} 750ms steps(3, end) infinite;
|
||||||
|
width: 2ch;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
type AIChatTabProps = {
|
type AIChatTabProps = {
|
||||||
agentId: string;
|
agentId: string;
|
||||||
};
|
};
|
||||||
@ -168,16 +188,49 @@ export const AIChatTab: React.FC<AIChatTabProps> = ({ agentId }) => {
|
|||||||
agentStreamingMessage,
|
agentStreamingMessage,
|
||||||
} = useAgentChat(agentId);
|
} = useAgentChat(agentId);
|
||||||
|
|
||||||
|
const getAssistantMessageContent = (message: AgentChatMessage) => {
|
||||||
|
if (message.content !== '') {
|
||||||
|
return message.content;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (agentStreamingMessage.streamingText !== '') {
|
||||||
|
return agentStreamingMessage.streamingText;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (agentStreamingMessage.toolCall !== '') {
|
||||||
|
return (
|
||||||
|
<StyledToolCallContainer>
|
||||||
|
{agentStreamingMessage.toolCall}
|
||||||
|
</StyledToolCallContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledDotsIconContainer>
|
||||||
|
<StyledDotsIcon size={theme.icon.size.xl} />
|
||||||
|
</StyledDotsIconContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
{messages.length !== 0 && (
|
{messages.length !== 0 && (
|
||||||
<StyledScrollWrapper componentInstanceId={agentId}>
|
<StyledScrollWrapper
|
||||||
|
componentInstanceId={`scroll-wrapper-ai-chat-${agentId}`}
|
||||||
|
>
|
||||||
{messages.map((msg) => (
|
{messages.map((msg) => (
|
||||||
<StyledMessageBubble
|
<StyledMessageBubble
|
||||||
key={msg.id}
|
key={msg.id}
|
||||||
isUser={msg.role === AgentChatMessageRole.USER}
|
isUser={msg.role === AgentChatMessageRole.USER}
|
||||||
>
|
>
|
||||||
<StyledMessageRow>
|
<StyledMessageRow
|
||||||
|
isShowingToolCall={
|
||||||
|
msg.role === AgentChatMessageRole.ASSISTANT &&
|
||||||
|
msg.content === '' &&
|
||||||
|
agentStreamingMessage.streamingText === '' &&
|
||||||
|
agentStreamingMessage.toolCall !== ''
|
||||||
|
}
|
||||||
|
>
|
||||||
{msg.role === AgentChatMessageRole.ASSISTANT && (
|
{msg.role === AgentChatMessageRole.ASSISTANT && (
|
||||||
<StyledAvatarContainer>
|
<StyledAvatarContainer>
|
||||||
<Avatar
|
<Avatar
|
||||||
@ -197,12 +250,8 @@ export const AIChatTab: React.FC<AIChatTabProps> = ({ agentId }) => {
|
|||||||
<StyledMessageText
|
<StyledMessageText
|
||||||
isUser={msg.role === AgentChatMessageRole.USER}
|
isUser={msg.role === AgentChatMessageRole.USER}
|
||||||
>
|
>
|
||||||
{msg.role === AgentChatMessageRole.ASSISTANT && !msg.content
|
{msg.role === AgentChatMessageRole.ASSISTANT
|
||||||
? agentStreamingMessage || (
|
? getAssistantMessageContent(msg)
|
||||||
<StyledDotsIconContainer>
|
|
||||||
<StyledDotsIcon size={theme.icon.size.xl} />
|
|
||||||
</StyledDotsIconContainer>
|
|
||||||
)
|
|
||||||
: msg.content}
|
: msg.content}
|
||||||
</StyledMessageText>
|
</StyledMessageText>
|
||||||
{msg.content && (
|
{msg.content && (
|
||||||
|
|||||||
@ -8,11 +8,11 @@ import { useScrollWrapperElement } from '@/ui/utilities/scroll/hooks/useScrollWr
|
|||||||
import { STREAM_CHAT_QUERY } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/api/agent-chat-apollo.api';
|
import { STREAM_CHAT_QUERY } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/api/agent-chat-apollo.api';
|
||||||
import { AgentChatMessageRole } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/constants/agent-chat-message-role';
|
import { AgentChatMessageRole } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/constants/agent-chat-message-role';
|
||||||
import { useApolloClient } from '@apollo/client';
|
import { useApolloClient } from '@apollo/client';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
|
||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
import { agentChatInputState } from '../states/agentChatInputState';
|
import { agentChatInputState } from '../states/agentChatInputState';
|
||||||
import { agentChatMessagesComponentState } from '../states/agentChatMessagesComponentState';
|
import { agentChatMessagesComponentState } from '../states/agentChatMessagesComponentState';
|
||||||
import { agentStreamingMessageState } from '../states/agentStreamingMessageState';
|
import { agentStreamingMessageState } from '../states/agentStreamingMessageState';
|
||||||
|
import { parseAgentStreamingChunk } from '../utils/parseAgentStreamingChunk';
|
||||||
import { AgentChatMessage, useAgentChatMessages } from './useAgentChatMessages';
|
import { AgentChatMessage, useAgentChatMessages } from './useAgentChatMessages';
|
||||||
import { useAgentChatThreads } from './useAgentChatThreads';
|
import { useAgentChatThreads } from './useAgentChatThreads';
|
||||||
|
|
||||||
@ -50,21 +50,14 @@ export const useAgentChat = (agentId: string) => {
|
|||||||
useAgentChatThreads(agentId);
|
useAgentChatThreads(agentId);
|
||||||
const currentThreadId = threads[0]?.id;
|
const currentThreadId = threads[0]?.id;
|
||||||
|
|
||||||
const {
|
const { loading: messagesLoading, refetch: refetchMessages } =
|
||||||
data: messagesData,
|
useAgentChatMessages(currentThreadId, ({ messages }) => {
|
||||||
loading: messagesLoading,
|
setAgentChatMessages(messages);
|
||||||
refetch: refetchMessages,
|
scrollToBottom();
|
||||||
} = useAgentChatMessages(currentThreadId);
|
});
|
||||||
|
|
||||||
const isLoading = messagesLoading || threadsLoading || isStreaming;
|
const isLoading = messagesLoading || threadsLoading || isStreaming;
|
||||||
|
|
||||||
if (
|
|
||||||
agentChatMessages.length === 0 &&
|
|
||||||
isDefined(messagesData?.messages?.length)
|
|
||||||
) {
|
|
||||||
setAgentChatMessages(messagesData.messages);
|
|
||||||
}
|
|
||||||
|
|
||||||
const createOptimisticMessages = (content: string): AgentChatMessage[] => {
|
const createOptimisticMessages = (content: string): AgentChatMessage[] => {
|
||||||
const optimisticUserMessage: OptimisticMessage = {
|
const optimisticUserMessage: OptimisticMessage = {
|
||||||
id: v4(),
|
id: v4(),
|
||||||
@ -104,8 +97,22 @@ export const useAgentChat = (agentId: string) => {
|
|||||||
},
|
},
|
||||||
context: {
|
context: {
|
||||||
onChunk: (chunk: string) => {
|
onChunk: (chunk: string) => {
|
||||||
setAgentStreamingMessage(chunk);
|
parseAgentStreamingChunk(chunk, {
|
||||||
scrollToBottom();
|
onTextDelta: (message: string) => {
|
||||||
|
setAgentStreamingMessage((prev) => ({
|
||||||
|
...prev,
|
||||||
|
streamingText: prev.streamingText + message,
|
||||||
|
}));
|
||||||
|
scrollToBottom();
|
||||||
|
},
|
||||||
|
onToolCall: (message: string) => {
|
||||||
|
setAgentStreamingMessage((prev) => ({
|
||||||
|
...prev,
|
||||||
|
toolCall: message,
|
||||||
|
}));
|
||||||
|
scrollToBottom();
|
||||||
|
},
|
||||||
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -128,7 +135,10 @@ export const useAgentChat = (agentId: string) => {
|
|||||||
const { data } = await refetchMessages();
|
const { data } = await refetchMessages();
|
||||||
|
|
||||||
setAgentChatMessages(data?.messages);
|
setAgentChatMessages(data?.messages);
|
||||||
setAgentStreamingMessage('');
|
setAgentStreamingMessage({
|
||||||
|
toolCall: '',
|
||||||
|
streamingText: '',
|
||||||
|
});
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -11,9 +11,13 @@ export type AgentChatMessage = {
|
|||||||
createdAt: string;
|
createdAt: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useAgentChatMessages = (threadId: string) => {
|
export const useAgentChatMessages = (
|
||||||
|
threadId: string,
|
||||||
|
onCompleted?: (data: { messages: AgentChatMessage[] }) => void,
|
||||||
|
) => {
|
||||||
return useQuery<{ messages: AgentChatMessage[] }>(GET_AGENT_CHAT_MESSAGES, {
|
return useQuery<{ messages: AgentChatMessage[] }>(GET_AGENT_CHAT_MESSAGES, {
|
||||||
variables: { threadId },
|
variables: { threadId },
|
||||||
skip: !isDefined(threadId),
|
skip: !isDefined(threadId),
|
||||||
|
onCompleted,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,6 +1,12 @@
|
|||||||
import { atom } from 'recoil';
|
import { atom } from 'recoil';
|
||||||
|
|
||||||
export const agentStreamingMessageState = atom<string>({
|
export const agentStreamingMessageState = atom<{
|
||||||
|
toolCall: string;
|
||||||
|
streamingText: string;
|
||||||
|
}>({
|
||||||
key: 'agentStreamingMessageState',
|
key: 'agentStreamingMessageState',
|
||||||
default: '',
|
default: {
|
||||||
|
toolCall: '',
|
||||||
|
streamingText: '',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -0,0 +1,43 @@
|
|||||||
|
export type AgentStreamingEvent = {
|
||||||
|
type: 'text-delta' | 'tool-call';
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AgentStreamingParserCallbacks = {
|
||||||
|
onTextDelta?: (message: string) => void;
|
||||||
|
onToolCall?: (message: string) => void;
|
||||||
|
onError?: (error: Error, rawLine: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parseAgentStreamingChunk = (
|
||||||
|
chunk: string,
|
||||||
|
callbacks: AgentStreamingParserCallbacks,
|
||||||
|
): void => {
|
||||||
|
const lines = chunk.split('\n');
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.trim() !== '') {
|
||||||
|
try {
|
||||||
|
const event = JSON.parse(line) as AgentStreamingEvent;
|
||||||
|
|
||||||
|
switch (event.type) {
|
||||||
|
case 'text-delta':
|
||||||
|
callbacks.onTextDelta?.(event.message);
|
||||||
|
break;
|
||||||
|
case 'tool-call':
|
||||||
|
callbacks.onToolCall?.(event.message);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error('Failed to parse stream event:', error);
|
||||||
|
|
||||||
|
const errorMessage =
|
||||||
|
error instanceof Error
|
||||||
|
? error
|
||||||
|
: new Error(`Unknown parsing error: ${String(error)}`);
|
||||||
|
callbacks.onError?.(errorMessage, line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class AddDefaultAgentId1752070094777 implements MigrationInterface {
|
||||||
|
name = 'AddDefaultAgentId1752070094777';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "core"."workspace" ADD "defaultAgentId" uuid`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "core"."agent" ALTER COLUMN "modelId" SET DEFAULT 'auto'`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "core"."agent" ALTER COLUMN "modelId" DROP DEFAULT`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "core"."workspace" DROP COLUMN "defaultAgentId"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class AgentAsStandardMetadata1752088464449
|
||||||
|
implements MigrationInterface
|
||||||
|
{
|
||||||
|
name = 'AgentAsStandardMetadata1752088464449';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "core"."agent" ADD "label" character varying NOT NULL`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "core"."agent" ADD "icon" character varying`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "core"."agent" ADD "isCustom" boolean NOT NULL DEFAULT false`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "core"."agent" ADD CONSTRAINT "IDX_AGENT_NAME_WORKSPACE_ID_UNIQUE" UNIQUE ("name", "workspaceId")`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "core"."agent" DROP CONSTRAINT "IDX_AGENT_NAME_WORKSPACE_ID_UNIQUE"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "core"."agent" DROP COLUMN "isCustom"`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(`ALTER TABLE "core"."agent" DROP COLUMN "icon"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "core"."agent" DROP COLUMN "label"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,55 @@
|
|||||||
|
import { getAIModelsWithAuto } from 'src/engine/core-modules/ai/utils/get-ai-models-with-auto.util';
|
||||||
|
import { getDefaultModelConfig } from 'src/engine/core-modules/ai/utils/get-default-model-config.util';
|
||||||
|
|
||||||
|
import { AI_MODELS, DEFAULT_MODEL_ID, ModelProvider } from './ai-models.const';
|
||||||
|
|
||||||
|
describe('AI_MODELS', () => {
|
||||||
|
it('should contain all expected models', () => {
|
||||||
|
expect(AI_MODELS).toHaveLength(6);
|
||||||
|
expect(AI_MODELS.map((model) => model.modelId)).toEqual([
|
||||||
|
'gpt-4o',
|
||||||
|
'gpt-4o-mini',
|
||||||
|
'gpt-4-turbo',
|
||||||
|
'claude-opus-4-20250514',
|
||||||
|
'claude-sonnet-4-20250514',
|
||||||
|
'claude-3-5-haiku-20241022',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have the default model as the first model', () => {
|
||||||
|
const DEFAULT_MODEL = AI_MODELS.find(
|
||||||
|
(model) => model.modelId === DEFAULT_MODEL_ID,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(DEFAULT_MODEL).toBeDefined();
|
||||||
|
expect(DEFAULT_MODEL?.modelId).toBe(DEFAULT_MODEL_ID);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getAIModelsWithAuto', () => {
|
||||||
|
it('should return AI_MODELS with auto model prepended', () => {
|
||||||
|
const ORIGINAL_MODELS = AI_MODELS;
|
||||||
|
const MODELS_WITH_AUTO = getAIModelsWithAuto();
|
||||||
|
|
||||||
|
expect(MODELS_WITH_AUTO).toHaveLength(ORIGINAL_MODELS.length + 1);
|
||||||
|
expect(MODELS_WITH_AUTO[0].modelId).toBe('auto');
|
||||||
|
expect(MODELS_WITH_AUTO[0].label).toBe('Auto');
|
||||||
|
expect(MODELS_WITH_AUTO[0].provider).toBe(ModelProvider.NONE);
|
||||||
|
|
||||||
|
// Check that the rest of the models are the same
|
||||||
|
expect(MODELS_WITH_AUTO.slice(1)).toEqual(ORIGINAL_MODELS);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have auto model with default model costs', () => {
|
||||||
|
const MODELS_WITH_AUTO = getAIModelsWithAuto();
|
||||||
|
const AUTO_MODEL = MODELS_WITH_AUTO[0];
|
||||||
|
const DEFAULT_MODEL = getDefaultModelConfig();
|
||||||
|
|
||||||
|
expect(AUTO_MODEL.inputCostPer1kTokensInCents).toBe(
|
||||||
|
DEFAULT_MODEL.inputCostPer1kTokensInCents,
|
||||||
|
);
|
||||||
|
expect(AUTO_MODEL.outputCostPer1kTokensInCents).toBe(
|
||||||
|
DEFAULT_MODEL.outputCostPer1kTokensInCents,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -1,9 +1,11 @@
|
|||||||
export enum ModelProvider {
|
export enum ModelProvider {
|
||||||
|
NONE = 'none',
|
||||||
OPENAI = 'openai',
|
OPENAI = 'openai',
|
||||||
ANTHROPIC = 'anthropic',
|
ANTHROPIC = 'anthropic',
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ModelId =
|
export type ModelId =
|
||||||
|
| 'auto'
|
||||||
| 'gpt-4o'
|
| 'gpt-4o'
|
||||||
| 'gpt-4o-mini'
|
| 'gpt-4o-mini'
|
||||||
| 'gpt-4-turbo'
|
| 'gpt-4-turbo'
|
||||||
@ -11,6 +13,8 @@ export type ModelId =
|
|||||||
| 'claude-sonnet-4-20250514'
|
| 'claude-sonnet-4-20250514'
|
||||||
| 'claude-3-5-haiku-20241022';
|
| 'claude-3-5-haiku-20241022';
|
||||||
|
|
||||||
|
export const DEFAULT_MODEL_ID: ModelId = 'gpt-4o';
|
||||||
|
|
||||||
export interface AIModelConfig {
|
export interface AIModelConfig {
|
||||||
modelId: ModelId;
|
modelId: ModelId;
|
||||||
label: string;
|
label: string;
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { Injectable, Logger } from '@nestjs/common';
|
|||||||
|
|
||||||
import { ModelId } from 'src/engine/core-modules/ai/constants/ai-models.const';
|
import { ModelId } from 'src/engine/core-modules/ai/constants/ai-models.const';
|
||||||
import { DOLLAR_TO_CREDIT_MULTIPLIER } from 'src/engine/core-modules/ai/constants/dollar-to-credit-multiplier';
|
import { DOLLAR_TO_CREDIT_MULTIPLIER } from 'src/engine/core-modules/ai/constants/dollar-to-credit-multiplier';
|
||||||
import { getAIModelById } from 'src/engine/core-modules/ai/utils/get-ai-model-by-id';
|
import { getAIModelById } from 'src/engine/core-modules/ai/utils/get-ai-model-by-id.util';
|
||||||
import { BILLING_FEATURE_USED } from 'src/engine/core-modules/billing/constants/billing-feature-used.constant';
|
import { BILLING_FEATURE_USED } from 'src/engine/core-modules/billing/constants/billing-feature-used.constant';
|
||||||
import { BillingMeterEventName } from 'src/engine/core-modules/billing/enums/billing-meter-event-names';
|
import { BillingMeterEventName } from 'src/engine/core-modules/billing/enums/billing-meter-event-names';
|
||||||
import { BillingUsageEvent } from 'src/engine/core-modules/billing/types/billing-usage-event.type';
|
import { BillingUsageEvent } from 'src/engine/core-modules/billing/types/billing-usage-event.type';
|
||||||
|
|||||||
@ -1,19 +1,19 @@
|
|||||||
import { Injectable, HttpException, HttpStatus } from '@nestjs/common';
|
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
|
||||||
import { ToolSet } from 'ai';
|
import { ToolSet } from 'ai';
|
||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
|
import { JsonRpc } from 'src/engine/core-modules/ai/dtos/json-rpc';
|
||||||
|
import { ToolService } from 'src/engine/core-modules/ai/services/tool.service';
|
||||||
|
import { wrapJsonRpcResponse } from 'src/engine/core-modules/ai/utils/wrap-jsonrpc-response.util';
|
||||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
|
|
||||||
import { ToolService } from 'src/engine/core-modules/ai/services/tool.service';
|
|
||||||
import { JsonRpc } from 'src/engine/core-modules/ai/dtos/json-rpc';
|
|
||||||
import { wrapJsonRpcResponse } from 'src/engine/core-modules/ai/utils/wrap-jsonrpc-response';
|
|
||||||
import { ADMIN_ROLE_LABEL } from 'src/engine/metadata-modules/permissions/constants/admin-role-label.constants';
|
import { ADMIN_ROLE_LABEL } from 'src/engine/metadata-modules/permissions/constants/admin-role-label.constants';
|
||||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||||
|
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class McpService {
|
export class McpService {
|
||||||
|
|||||||
@ -1,30 +1,31 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { ToolSet } from 'ai';
|
||||||
import {
|
import {
|
||||||
|
ILike,
|
||||||
In,
|
In,
|
||||||
IsNull,
|
IsNull,
|
||||||
LessThan,
|
LessThan,
|
||||||
LessThanOrEqual,
|
LessThanOrEqual,
|
||||||
Like,
|
Like,
|
||||||
ILike,
|
|
||||||
MoreThan,
|
MoreThan,
|
||||||
MoreThanOrEqual,
|
MoreThanOrEqual,
|
||||||
Not,
|
Not,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import { ToolSet } from 'ai';
|
|
||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
|
||||||
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
|
|
||||||
import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
|
|
||||||
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
|
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
|
||||||
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
|
|
||||||
import {
|
import {
|
||||||
generateBulkDeleteToolSchema,
|
generateBulkDeleteToolSchema,
|
||||||
|
generateFindOneToolSchema,
|
||||||
generateFindToolSchema,
|
generateFindToolSchema,
|
||||||
|
generateSoftDeleteToolSchema,
|
||||||
getRecordInputSchema,
|
getRecordInputSchema,
|
||||||
} from 'src/engine/metadata-modules/agent/utils/agent-tool-schema.utils';
|
} from 'src/engine/metadata-modules/agent/utils/agent-tool-schema.utils';
|
||||||
import { isWorkflowRelatedObject } from 'src/engine/metadata-modules/agent/utils/is-workflow-related-object.util';
|
import { isWorkflowRelatedObject } from 'src/engine/metadata-modules/agent/utils/is-workflow-related-object.util';
|
||||||
|
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
|
||||||
|
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
|
||||||
|
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||||
|
import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ToolService {
|
export class ToolService {
|
||||||
@ -72,7 +73,7 @@ export class ToolService {
|
|||||||
execute: async (parameters) => {
|
execute: async (parameters) => {
|
||||||
return this.createRecord(
|
return this.createRecord(
|
||||||
objectMetadata.nameSingular,
|
objectMetadata.nameSingular,
|
||||||
parameters,
|
parameters.input,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
roleId,
|
roleId,
|
||||||
);
|
);
|
||||||
@ -85,7 +86,7 @@ export class ToolService {
|
|||||||
execute: async (parameters) => {
|
execute: async (parameters) => {
|
||||||
return this.updateRecord(
|
return this.updateRecord(
|
||||||
objectMetadata.nameSingular,
|
objectMetadata.nameSingular,
|
||||||
parameters,
|
parameters.input,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
roleId,
|
roleId,
|
||||||
);
|
);
|
||||||
@ -100,7 +101,7 @@ export class ToolService {
|
|||||||
execute: async (parameters) => {
|
execute: async (parameters) => {
|
||||||
return this.findRecords(
|
return this.findRecords(
|
||||||
objectMetadata.nameSingular,
|
objectMetadata.nameSingular,
|
||||||
parameters,
|
parameters.input,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
roleId,
|
roleId,
|
||||||
);
|
);
|
||||||
@ -109,15 +110,11 @@ export class ToolService {
|
|||||||
|
|
||||||
tools[`find_one_${objectMetadata.nameSingular}`] = {
|
tools[`find_one_${objectMetadata.nameSingular}`] = {
|
||||||
description: `Retrieve a single ${objectMetadata.labelSingular} record by its unique ID. Use this when you know the exact record ID and need the complete record data. Returns the full record or an error if not found.`,
|
description: `Retrieve a single ${objectMetadata.labelSingular} record by its unique ID. Use this when you know the exact record ID and need the complete record data. Returns the full record or an error if not found.`,
|
||||||
parameters: z.object({
|
parameters: generateFindOneToolSchema(),
|
||||||
id: z
|
|
||||||
.string()
|
|
||||||
.describe('The unique UUID of the record to retrieve'),
|
|
||||||
}),
|
|
||||||
execute: async (parameters) => {
|
execute: async (parameters) => {
|
||||||
return this.findOneRecord(
|
return this.findOneRecord(
|
||||||
objectMetadata.nameSingular,
|
objectMetadata.nameSingular,
|
||||||
parameters,
|
parameters.input,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
roleId,
|
roleId,
|
||||||
);
|
);
|
||||||
@ -128,15 +125,11 @@ export class ToolService {
|
|||||||
if (objectPermission.canSoftDelete) {
|
if (objectPermission.canSoftDelete) {
|
||||||
tools[`soft_delete_${objectMetadata.nameSingular}`] = {
|
tools[`soft_delete_${objectMetadata.nameSingular}`] = {
|
||||||
description: `Soft delete a ${objectMetadata.labelSingular} record by marking it as deleted. The record remains in the database but is hidden from normal queries. This is reversible and preserves all data. Use this for temporary removal.`,
|
description: `Soft delete a ${objectMetadata.labelSingular} record by marking it as deleted. The record remains in the database but is hidden from normal queries. This is reversible and preserves all data. Use this for temporary removal.`,
|
||||||
parameters: z.object({
|
parameters: generateSoftDeleteToolSchema(),
|
||||||
id: z
|
|
||||||
.string()
|
|
||||||
.describe('The unique UUID of the record to soft delete'),
|
|
||||||
}),
|
|
||||||
execute: async (parameters) => {
|
execute: async (parameters) => {
|
||||||
return this.softDeleteRecord(
|
return this.softDeleteRecord(
|
||||||
objectMetadata.nameSingular,
|
objectMetadata.nameSingular,
|
||||||
parameters,
|
parameters.input,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
roleId,
|
roleId,
|
||||||
);
|
);
|
||||||
@ -149,7 +142,7 @@ export class ToolService {
|
|||||||
execute: async (parameters) => {
|
execute: async (parameters) => {
|
||||||
return this.softDeleteManyRecords(
|
return this.softDeleteManyRecords(
|
||||||
objectMetadata.nameSingular,
|
objectMetadata.nameSingular,
|
||||||
parameters,
|
parameters.input,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
roleId,
|
roleId,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -4,4 +4,5 @@
|
|||||||
* @param cents - Cost in cents (real cost)
|
* @param cents - Cost in cents (real cost)
|
||||||
* @returns Cost in credits (end-user cost)
|
* @returns Cost in credits (end-user cost)
|
||||||
*/
|
*/
|
||||||
export const convertCentsToCredits = (cents: number): number => cents * 10;
|
export const convertCentsToBillingCredits = (cents: number): number =>
|
||||||
|
cents * 10;
|
||||||
@ -1,9 +0,0 @@
|
|||||||
import {
|
|
||||||
AI_MODELS,
|
|
||||||
AIModelConfig,
|
|
||||||
ModelId,
|
|
||||||
} from 'src/engine/core-modules/ai/constants/ai-models.const';
|
|
||||||
|
|
||||||
export const getAIModelById = (modelId: ModelId): AIModelConfig | undefined => {
|
|
||||||
return AI_MODELS.find((model) => model.modelId === modelId);
|
|
||||||
};
|
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
import { AIModelConfig } from 'src/engine/core-modules/ai/constants/ai-models.const';
|
||||||
|
|
||||||
|
import { getEffectiveModelConfig } from './get-effective-model-config.util';
|
||||||
|
|
||||||
|
export const getAIModelById = (modelId: string): AIModelConfig => {
|
||||||
|
return getEffectiveModelConfig(modelId);
|
||||||
|
};
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
import {
|
||||||
|
AI_MODELS,
|
||||||
|
ModelProvider,
|
||||||
|
} from 'src/engine/core-modules/ai/constants/ai-models.const';
|
||||||
|
|
||||||
|
import { getAIModelsWithAuto } from './get-ai-models-with-auto.util';
|
||||||
|
import { getDefaultModelConfig } from './get-default-model-config.util';
|
||||||
|
|
||||||
|
describe('getAIModelsWithAuto', () => {
|
||||||
|
it('should return AI_MODELS with auto model prepended', () => {
|
||||||
|
const ORIGINAL_MODELS = AI_MODELS;
|
||||||
|
const MODELS_WITH_AUTO = getAIModelsWithAuto();
|
||||||
|
|
||||||
|
expect(MODELS_WITH_AUTO).toHaveLength(ORIGINAL_MODELS.length + 1);
|
||||||
|
expect(MODELS_WITH_AUTO[0].modelId).toBe('auto');
|
||||||
|
expect(MODELS_WITH_AUTO[0].label).toBe('Auto');
|
||||||
|
expect(MODELS_WITH_AUTO[0].provider).toBe(ModelProvider.NONE);
|
||||||
|
|
||||||
|
// Check that the rest of the models are the same
|
||||||
|
expect(MODELS_WITH_AUTO.slice(1)).toEqual(ORIGINAL_MODELS);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have auto model with default model costs', () => {
|
||||||
|
const MODELS_WITH_AUTO = getAIModelsWithAuto();
|
||||||
|
const AUTO_MODEL = MODELS_WITH_AUTO[0];
|
||||||
|
const DEFAULT_MODEL = getDefaultModelConfig();
|
||||||
|
|
||||||
|
expect(AUTO_MODEL.inputCostPer1kTokensInCents).toBe(
|
||||||
|
DEFAULT_MODEL.inputCostPer1kTokensInCents,
|
||||||
|
);
|
||||||
|
expect(AUTO_MODEL.outputCostPer1kTokensInCents).toBe(
|
||||||
|
DEFAULT_MODEL.outputCostPer1kTokensInCents,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
import {
|
||||||
|
AI_MODELS,
|
||||||
|
AIModelConfig,
|
||||||
|
ModelProvider,
|
||||||
|
} from 'src/engine/core-modules/ai/constants/ai-models.const';
|
||||||
|
|
||||||
|
import { getDefaultModelConfig } from './get-default-model-config.util';
|
||||||
|
|
||||||
|
export const getAIModelsWithAuto = (): AIModelConfig[] => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
modelId: 'auto',
|
||||||
|
label: 'Auto',
|
||||||
|
provider: ModelProvider.NONE,
|
||||||
|
inputCostPer1kTokensInCents:
|
||||||
|
getDefaultModelConfig().inputCostPer1kTokensInCents,
|
||||||
|
outputCostPer1kTokensInCents:
|
||||||
|
getDefaultModelConfig().outputCostPer1kTokensInCents,
|
||||||
|
},
|
||||||
|
...AI_MODELS,
|
||||||
|
];
|
||||||
|
};
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
import {
|
||||||
|
AI_MODELS,
|
||||||
|
DEFAULT_MODEL_ID,
|
||||||
|
ModelProvider,
|
||||||
|
} from 'src/engine/core-modules/ai/constants/ai-models.const';
|
||||||
|
|
||||||
|
import { getDefaultModelConfig } from './get-default-model-config.util';
|
||||||
|
|
||||||
|
describe('getDefaultModelConfig', () => {
|
||||||
|
it('should return the configuration for the default model', () => {
|
||||||
|
const result = getDefaultModelConfig();
|
||||||
|
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result.modelId).toBe(DEFAULT_MODEL_ID);
|
||||||
|
expect(result.provider).toBe(ModelProvider.OPENAI);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if default model is not found', () => {
|
||||||
|
const originalFind = AI_MODELS.find;
|
||||||
|
|
||||||
|
AI_MODELS.find = jest.fn().mockReturnValue(undefined);
|
||||||
|
|
||||||
|
expect(() => getDefaultModelConfig()).toThrow(
|
||||||
|
`Default model with ID ${DEFAULT_MODEL_ID} not found`,
|
||||||
|
);
|
||||||
|
|
||||||
|
AI_MODELS.find = originalFind;
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
import {
|
||||||
|
AI_MODELS,
|
||||||
|
AIModelConfig,
|
||||||
|
DEFAULT_MODEL_ID,
|
||||||
|
} from 'src/engine/core-modules/ai/constants/ai-models.const';
|
||||||
|
|
||||||
|
export const getDefaultModelConfig = (): AIModelConfig => {
|
||||||
|
const defaultModel = AI_MODELS.find(
|
||||||
|
(model) => model.modelId === DEFAULT_MODEL_ID,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!defaultModel) {
|
||||||
|
throw new Error(`Default model with ID ${DEFAULT_MODEL_ID} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultModel;
|
||||||
|
};
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
import {
|
||||||
|
DEFAULT_MODEL_ID,
|
||||||
|
ModelProvider,
|
||||||
|
} from 'src/engine/core-modules/ai/constants/ai-models.const';
|
||||||
|
|
||||||
|
import { getEffectiveModelConfig } from './get-effective-model-config.util';
|
||||||
|
|
||||||
|
describe('getEffectiveModelConfig', () => {
|
||||||
|
it('should return default model config when modelId is "auto"', () => {
|
||||||
|
const result = getEffectiveModelConfig('auto');
|
||||||
|
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result.modelId).toBe(DEFAULT_MODEL_ID);
|
||||||
|
expect(result.provider).toBe(ModelProvider.OPENAI);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the correct model config for a specific model', () => {
|
||||||
|
const result = getEffectiveModelConfig('gpt-4o');
|
||||||
|
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result.modelId).toBe('gpt-4o');
|
||||||
|
expect(result.provider).toBe(ModelProvider.OPENAI);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error for non-existent model', () => {
|
||||||
|
expect(() => getEffectiveModelConfig('non-existent-model' as any)).toThrow(
|
||||||
|
`Model with ID non-existent-model not found`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
import {
|
||||||
|
AI_MODELS,
|
||||||
|
AIModelConfig,
|
||||||
|
} from 'src/engine/core-modules/ai/constants/ai-models.const';
|
||||||
|
|
||||||
|
import { getDefaultModelConfig } from './get-default-model-config.util';
|
||||||
|
|
||||||
|
export const getEffectiveModelConfig = (modelId: string): AIModelConfig => {
|
||||||
|
if (modelId === 'auto') {
|
||||||
|
return getDefaultModelConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
const model = AI_MODELS.find((model) => model.modelId === modelId);
|
||||||
|
|
||||||
|
if (!model) {
|
||||||
|
throw new Error(`Model with ID ${modelId} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return model;
|
||||||
|
};
|
||||||
@ -9,9 +9,12 @@ import { DomainManagerService } from 'src/engine/core-modules/domain-manager/ser
|
|||||||
import { PUBLIC_FEATURE_FLAGS } from 'src/engine/core-modules/feature-flag/constants/public-feature-flag.const';
|
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 { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||||
|
|
||||||
jest.mock('src/engine/core-modules/ai/constants/ai-models.const', () => ({
|
jest.mock(
|
||||||
AI_MODELS: [],
|
'src/engine/core-modules/ai/utils/get-ai-models-with-auto.util',
|
||||||
}));
|
() => ({
|
||||||
|
getAIModelsWithAuto: jest.fn(() => []),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
describe('ClientConfigService', () => {
|
describe('ClientConfigService', () => {
|
||||||
let service: ClientConfigService;
|
let service: ClientConfigService;
|
||||||
|
|||||||
@ -3,11 +3,9 @@ import { Injectable } from '@nestjs/common';
|
|||||||
import { NodeEnvironment } from 'src/engine/core-modules/twenty-config/interfaces/node-environment.interface';
|
import { NodeEnvironment } from 'src/engine/core-modules/twenty-config/interfaces/node-environment.interface';
|
||||||
import { SupportDriver } from 'src/engine/core-modules/twenty-config/interfaces/support.interface';
|
import { SupportDriver } from 'src/engine/core-modules/twenty-config/interfaces/support.interface';
|
||||||
|
|
||||||
import {
|
import { ModelProvider } from 'src/engine/core-modules/ai/constants/ai-models.const';
|
||||||
AI_MODELS,
|
import { convertCentsToBillingCredits } from 'src/engine/core-modules/ai/utils/convert-cents-to-billing-credits.util';
|
||||||
ModelProvider,
|
import { getAIModelsWithAuto } from 'src/engine/core-modules/ai/utils/get-ai-models-with-auto.util';
|
||||||
} from 'src/engine/core-modules/ai/constants/ai-models.const';
|
|
||||||
import { convertCentsToCredits } from 'src/engine/core-modules/ai/utils/ai-cost.utils';
|
|
||||||
import {
|
import {
|
||||||
ClientAIModelConfig,
|
ClientAIModelConfig,
|
||||||
ClientConfig,
|
ClientConfig,
|
||||||
@ -29,29 +27,32 @@ export class ClientConfigService {
|
|||||||
const openaiApiKey = this.twentyConfigService.get('OPENAI_API_KEY');
|
const openaiApiKey = this.twentyConfigService.get('OPENAI_API_KEY');
|
||||||
const anthropicApiKey = this.twentyConfigService.get('ANTHROPIC_API_KEY');
|
const anthropicApiKey = this.twentyConfigService.get('ANTHROPIC_API_KEY');
|
||||||
|
|
||||||
const aiModels = AI_MODELS.reduce<ClientAIModelConfig[]>((acc, model) => {
|
const aiModels = getAIModelsWithAuto().reduce<ClientAIModelConfig[]>(
|
||||||
const isAvailable =
|
(acc, model) => {
|
||||||
(model.provider === ModelProvider.OPENAI && openaiApiKey) ||
|
const isAvailable =
|
||||||
(model.provider === ModelProvider.ANTHROPIC && anthropicApiKey);
|
(model.provider === ModelProvider.OPENAI && openaiApiKey) ||
|
||||||
|
(model.provider === ModelProvider.ANTHROPIC && anthropicApiKey);
|
||||||
|
|
||||||
|
if (!isAvailable) {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
acc.push({
|
||||||
|
modelId: model.modelId,
|
||||||
|
label: model.label,
|
||||||
|
provider: model.provider,
|
||||||
|
inputCostPer1kTokensInCredits: convertCentsToBillingCredits(
|
||||||
|
model.inputCostPer1kTokensInCents,
|
||||||
|
),
|
||||||
|
outputCostPer1kTokensInCredits: convertCentsToBillingCredits(
|
||||||
|
model.outputCostPer1kTokensInCents,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
if (!isAvailable) {
|
|
||||||
return acc;
|
return acc;
|
||||||
}
|
},
|
||||||
|
[],
|
||||||
acc.push({
|
);
|
||||||
modelId: model.modelId,
|
|
||||||
label: model.label,
|
|
||||||
provider: model.provider,
|
|
||||||
inputCostPer1kTokensInCredits: convertCentsToCredits(
|
|
||||||
model.inputCostPer1kTokensInCents,
|
|
||||||
),
|
|
||||||
outputCostPer1kTokensInCredits: convertCentsToCredits(
|
|
||||||
model.outputCostPer1kTokensInCents,
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const clientConfig: ClientConfig = {
|
const clientConfig: ClientConfig = {
|
||||||
billing: {
|
billing: {
|
||||||
|
|||||||
@ -25,6 +25,7 @@ import { WorkspaceSSOIdentityProvider } from 'src/engine/core-modules/sso/worksp
|
|||||||
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||||
import { Webhook } from 'src/engine/core-modules/webhook/webhook.entity';
|
import { Webhook } from 'src/engine/core-modules/webhook/webhook.entity';
|
||||||
import { AgentEntity } from 'src/engine/metadata-modules/agent/agent.entity';
|
import { AgentEntity } from 'src/engine/metadata-modules/agent/agent.entity';
|
||||||
|
import { AgentDTO } from 'src/engine/metadata-modules/agent/dtos/agent.dto';
|
||||||
import { RoleDTO } from 'src/engine/metadata-modules/role/dtos/role.dto';
|
import { RoleDTO } from 'src/engine/metadata-modules/role/dtos/role.dto';
|
||||||
|
|
||||||
registerEnumType(WorkspaceActivationStatus, {
|
registerEnumType(WorkspaceActivationStatus, {
|
||||||
@ -171,12 +172,20 @@ export class Workspace {
|
|||||||
@Column({ default: false })
|
@Column({ default: false })
|
||||||
isCustomDomainEnabled: boolean;
|
isCustomDomainEnabled: boolean;
|
||||||
|
|
||||||
|
// TODO: set as non nullable
|
||||||
@Column({ nullable: true, type: 'uuid' })
|
@Column({ nullable: true, type: 'uuid' })
|
||||||
defaultRoleId: string | null;
|
defaultRoleId: string | null;
|
||||||
|
|
||||||
@Field(() => RoleDTO, { nullable: true })
|
@Field(() => RoleDTO, { nullable: true })
|
||||||
defaultRole: RoleDTO | null;
|
defaultRole: RoleDTO | null;
|
||||||
|
|
||||||
|
// TODO: set as non nullable
|
||||||
|
@Column({ nullable: true, type: 'uuid' })
|
||||||
|
defaultAgentId: string | null;
|
||||||
|
|
||||||
|
@Field(() => AgentDTO, { nullable: true })
|
||||||
|
defaultAgent: AgentDTO | null;
|
||||||
|
|
||||||
@Field(() => String, { nullable: true })
|
@Field(() => String, { nullable: true })
|
||||||
@Column({ type: 'varchar', nullable: true })
|
@Column({ type: 'varchar', nullable: true })
|
||||||
version: string | null;
|
version: string | null;
|
||||||
|
|||||||
@ -19,6 +19,7 @@ import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user
|
|||||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||||
import { WorkspaceWorkspaceMemberListener } from 'src/engine/core-modules/workspace/workspace-workspace-member.listener';
|
import { WorkspaceWorkspaceMemberListener } from 'src/engine/core-modules/workspace/workspace-workspace-member.listener';
|
||||||
import { WorkspaceResolver } from 'src/engine/core-modules/workspace/workspace.resolver';
|
import { WorkspaceResolver } from 'src/engine/core-modules/workspace/workspace.resolver';
|
||||||
|
import { AgentModule } from 'src/engine/metadata-modules/agent/agent.module';
|
||||||
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
||||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||||
import { RoleModule } from 'src/engine/metadata-modules/role/role.module';
|
import { RoleModule } from 'src/engine/metadata-modules/role/role.module';
|
||||||
@ -57,6 +58,7 @@ import { WorkspaceService } from './services/workspace.service';
|
|||||||
WorkspaceCacheStorageModule,
|
WorkspaceCacheStorageModule,
|
||||||
AuditModule,
|
AuditModule,
|
||||||
RoleModule,
|
RoleModule,
|
||||||
|
AgentModule,
|
||||||
],
|
],
|
||||||
services: [WorkspaceService],
|
services: [WorkspaceService],
|
||||||
resolvers: workspaceAutoResolverOpts,
|
resolvers: workspaceAutoResolverOpts,
|
||||||
|
|||||||
@ -56,6 +56,8 @@ import { PublicEndpointGuard } from 'src/engine/guards/public-endpoint.guard';
|
|||||||
import { SettingsPermissionsGuard } from 'src/engine/guards/settings-permissions.guard';
|
import { SettingsPermissionsGuard } from 'src/engine/guards/settings-permissions.guard';
|
||||||
import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
|
import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
|
||||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||||
|
import { AgentService } from 'src/engine/metadata-modules/agent/agent.service';
|
||||||
|
import { AgentDTO } from 'src/engine/metadata-modules/agent/dtos/agent.dto';
|
||||||
import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants';
|
import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants';
|
||||||
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
|
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
|
||||||
import { RoleDTO } from 'src/engine/metadata-modules/role/dtos/role.dto';
|
import { RoleDTO } from 'src/engine/metadata-modules/role/dtos/role.dto';
|
||||||
@ -92,6 +94,7 @@ export class WorkspaceResolver {
|
|||||||
private readonly billingSubscriptionService: BillingSubscriptionService,
|
private readonly billingSubscriptionService: BillingSubscriptionService,
|
||||||
private readonly featureFlagService: FeatureFlagService,
|
private readonly featureFlagService: FeatureFlagService,
|
||||||
private readonly roleService: RoleService,
|
private readonly roleService: RoleService,
|
||||||
|
private readonly agentService: AgentService,
|
||||||
@InjectRepository(BillingSubscription, 'core')
|
@InjectRepository(BillingSubscription, 'core')
|
||||||
private readonly billingSubscriptionRepository: Repository<BillingSubscription>,
|
private readonly billingSubscriptionRepository: Repository<BillingSubscription>,
|
||||||
) {}
|
) {}
|
||||||
@ -222,6 +225,29 @@ export class WorkspaceResolver {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ResolveField(() => AgentDTO, { nullable: true })
|
||||||
|
async defaultAgent(@Parent() workspace: Workspace): Promise<AgentDTO | null> {
|
||||||
|
if (!workspace.defaultAgentId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const agent = await this.agentService.findOneAgent(
|
||||||
|
workspace.defaultAgentId,
|
||||||
|
workspace.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Convert roleId from null to undefined to match AgentDTO
|
||||||
|
return {
|
||||||
|
...agent,
|
||||||
|
roleId: agent.roleId ?? undefined,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
// If agent is not found, return null instead of throwing
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ResolveField(() => BillingSubscription, { nullable: true })
|
@ResolveField(() => BillingSubscription, { nullable: true })
|
||||||
async currentBillingSubscription(
|
async currentBillingSubscription(
|
||||||
@Parent() workspace: Workspace,
|
@Parent() workspace: Workspace,
|
||||||
|
|||||||
@ -62,11 +62,33 @@ export class AgentChatController {
|
|||||||
@AuthUserWorkspaceId() userWorkspaceId: string,
|
@AuthUserWorkspaceId() userWorkspaceId: string,
|
||||||
@Res() res: Response,
|
@Res() res: Response,
|
||||||
) {
|
) {
|
||||||
await this.agentStreamingService.streamAgentChat({
|
try {
|
||||||
threadId: body.threadId,
|
await this.agentStreamingService.streamAgentChat({
|
||||||
userMessage: body.userMessage,
|
threadId: body.threadId,
|
||||||
userWorkspaceId,
|
userMessage: body.userMessage,
|
||||||
res,
|
userWorkspaceId,
|
||||||
});
|
res,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
// Handle errors at controller level for streaming responses
|
||||||
|
// since the RestApiExceptionFilter interferes with our streaming error handling
|
||||||
|
const errorMessage =
|
||||||
|
error instanceof Error ? error.message : 'Unknown error occurred';
|
||||||
|
|
||||||
|
if (!res.headersSent) {
|
||||||
|
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
||||||
|
res.setHeader('Transfer-Encoding', 'chunked');
|
||||||
|
res.setHeader('Cache-Control', 'no-cache');
|
||||||
|
}
|
||||||
|
|
||||||
|
res.write(
|
||||||
|
JSON.stringify({
|
||||||
|
type: 'error',
|
||||||
|
message: errorMessage,
|
||||||
|
}) + '\n',
|
||||||
|
);
|
||||||
|
|
||||||
|
res.end();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,8 +13,6 @@ import {
|
|||||||
AgentExceptionCode,
|
AgentExceptionCode,
|
||||||
} from 'src/engine/metadata-modules/agent/agent.exception';
|
} from 'src/engine/metadata-modules/agent/agent.exception';
|
||||||
|
|
||||||
import { AgentExecutionService } from './agent-execution.service';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AgentChatService {
|
export class AgentChatService {
|
||||||
constructor(
|
constructor(
|
||||||
@ -22,7 +20,6 @@ export class AgentChatService {
|
|||||||
private readonly threadRepository: Repository<AgentChatThreadEntity>,
|
private readonly threadRepository: Repository<AgentChatThreadEntity>,
|
||||||
@InjectRepository(AgentChatMessageEntity, 'core')
|
@InjectRepository(AgentChatMessageEntity, 'core')
|
||||||
private readonly messageRepository: Repository<AgentChatMessageEntity>,
|
private readonly messageRepository: Repository<AgentChatMessageEntity>,
|
||||||
private readonly agentExecutionService: AgentExecutionService,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async createThread(agentId: string, userWorkspaceId: string) {
|
async createThread(agentId: string, userWorkspaceId: string) {
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import {
|
|||||||
ModelId,
|
ModelId,
|
||||||
ModelProvider,
|
ModelProvider,
|
||||||
} from 'src/engine/core-modules/ai/constants/ai-models.const';
|
} from 'src/engine/core-modules/ai/constants/ai-models.const';
|
||||||
import { getAIModelById } from 'src/engine/core-modules/ai/utils/get-ai-model-by-id';
|
import { getEffectiveModelConfig } from 'src/engine/core-modules/ai/utils/get-effective-model-config.util';
|
||||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||||
import {
|
import {
|
||||||
AgentChatMessageEntity,
|
AgentChatMessageEntity,
|
||||||
@ -22,6 +22,7 @@ import { AGENT_SYSTEM_PROMPTS } from 'src/engine/metadata-modules/agent/constant
|
|||||||
import { convertOutputSchemaToZod } from 'src/engine/metadata-modules/agent/utils/convert-output-schema-to-zod';
|
import { convertOutputSchemaToZod } from 'src/engine/metadata-modules/agent/utils/convert-output-schema-to-zod';
|
||||||
import { OutputSchema } from 'src/modules/workflow/workflow-builder/workflow-schema/types/output-schema.type';
|
import { OutputSchema } from 'src/modules/workflow/workflow-builder/workflow-schema/types/output-schema.type';
|
||||||
import { resolveInput } from 'src/modules/workflow/workflow-executor/utils/variable-resolver.util';
|
import { resolveInput } from 'src/modules/workflow/workflow-executor/utils/variable-resolver.util';
|
||||||
|
import { getAIModelById } from 'src/engine/core-modules/ai/utils/get-ai-model-by-id.util';
|
||||||
|
|
||||||
import { AgentEntity } from './agent.entity';
|
import { AgentEntity } from './agent.entity';
|
||||||
import { AgentException, AgentExceptionCode } from './agent.exception';
|
import { AgentException, AgentExceptionCode } from './agent.exception';
|
||||||
@ -45,12 +46,17 @@ export class AgentExecutionService {
|
|||||||
private readonly agentToolService: AgentToolService,
|
private readonly agentToolService: AgentToolService,
|
||||||
@InjectRepository(AgentEntity, 'core')
|
@InjectRepository(AgentEntity, 'core')
|
||||||
private readonly agentRepository: Repository<AgentEntity>,
|
private readonly agentRepository: Repository<AgentEntity>,
|
||||||
@InjectRepository(AgentChatMessageEntity, 'core')
|
|
||||||
private readonly agentChatmessageRepository: Repository<AgentChatMessageEntity>,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
getModel = (modelId: ModelId, provider: ModelProvider) => {
|
getModel = (modelId: ModelId, provider: ModelProvider) => {
|
||||||
switch (provider) {
|
switch (provider) {
|
||||||
|
case ModelProvider.NONE: {
|
||||||
|
const OpenAIProvider = createOpenAI({
|
||||||
|
apiKey: this.twentyConfigService.get('OPENAI_API_KEY'),
|
||||||
|
});
|
||||||
|
|
||||||
|
return OpenAIProvider(getEffectiveModelConfig(modelId).modelId);
|
||||||
|
}
|
||||||
case ModelProvider.OPENAI: {
|
case ModelProvider.OPENAI: {
|
||||||
const OpenAIProvider = createOpenAI({
|
const OpenAIProvider = createOpenAI({
|
||||||
apiKey: this.twentyConfigService.get('OPENAI_API_KEY'),
|
apiKey: this.twentyConfigService.get('OPENAI_API_KEY'),
|
||||||
@ -77,6 +83,9 @@ export class AgentExecutionService {
|
|||||||
let apiKey: string | undefined;
|
let apiKey: string | undefined;
|
||||||
|
|
||||||
switch (provider) {
|
switch (provider) {
|
||||||
|
case ModelProvider.NONE:
|
||||||
|
apiKey = this.twentyConfigService.get('OPENAI_API_KEY');
|
||||||
|
break;
|
||||||
case ModelProvider.OPENAI:
|
case ModelProvider.OPENAI:
|
||||||
apiKey = this.twentyConfigService.get('OPENAI_API_KEY');
|
apiKey = this.twentyConfigService.get('OPENAI_API_KEY');
|
||||||
break;
|
break;
|
||||||
@ -91,7 +100,7 @@ export class AgentExecutionService {
|
|||||||
}
|
}
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
throw new AgentException(
|
throw new AgentException(
|
||||||
`${provider.toUpperCase()} API key not configured`,
|
`${provider === ModelProvider.NONE ? 'OPENAI' : provider.toUpperCase()} API key not configured`,
|
||||||
AgentExceptionCode.API_KEY_NOT_CONFIGURED,
|
AgentExceptionCode.API_KEY_NOT_CONFIGURED,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -65,7 +65,7 @@ export class AgentStreamingService {
|
|||||||
|
|
||||||
this.setupStreamingHeaders(res);
|
this.setupStreamingHeaders(res);
|
||||||
|
|
||||||
const { textStream } =
|
const { fullStream } =
|
||||||
await this.agentExecutionService.streamChatResponse({
|
await this.agentExecutionService.streamChatResponse({
|
||||||
agentId: thread.agent.id,
|
agentId: thread.agent.id,
|
||||||
userMessage,
|
userMessage,
|
||||||
@ -74,9 +74,24 @@ export class AgentStreamingService {
|
|||||||
|
|
||||||
let aiResponse = '';
|
let aiResponse = '';
|
||||||
|
|
||||||
for await (const chunk of textStream) {
|
for await (const chunk of fullStream) {
|
||||||
aiResponse += chunk;
|
switch (chunk.type) {
|
||||||
res.write(chunk);
|
case 'text-delta':
|
||||||
|
aiResponse += chunk.textDelta;
|
||||||
|
this.sendStreamEvent(res, {
|
||||||
|
type: chunk.type,
|
||||||
|
message: chunk.textDelta,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'tool-call':
|
||||||
|
this.sendStreamEvent(res, {
|
||||||
|
type: chunk.type,
|
||||||
|
message: chunk.args?.toolDescription,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.agentChatService.addMessage({
|
await this.agentChatService.addMessage({
|
||||||
@ -90,10 +105,26 @@ export class AgentStreamingService {
|
|||||||
const errorMessage =
|
const errorMessage =
|
||||||
error instanceof Error ? error.message : 'Unknown error occurred';
|
error instanceof Error ? error.message : 'Unknown error occurred';
|
||||||
|
|
||||||
return { success: false, error: errorMessage };
|
if (!res.headersSent) {
|
||||||
|
this.setupStreamingHeaders(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sendStreamEvent(res, {
|
||||||
|
type: 'error',
|
||||||
|
message: errorMessage,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.end();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sendStreamEvent(
|
||||||
|
res: Response,
|
||||||
|
event: { type: string; message: string },
|
||||||
|
): void {
|
||||||
|
res.write(JSON.stringify(event) + '\n');
|
||||||
|
}
|
||||||
|
|
||||||
private setupStreamingHeaders(res: Response): void {
|
private setupStreamingHeaders(res: Response): void {
|
||||||
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
||||||
res.setHeader('Transfer-Encoding', 'chunked');
|
res.setHeader('Transfer-Encoding', 'chunked');
|
||||||
|
|||||||
@ -4,9 +4,9 @@ import { InjectRepository } from '@nestjs/typeorm';
|
|||||||
import { ToolSet } from 'ai';
|
import { ToolSet } from 'ai';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
|
import { ToolService } from 'src/engine/core-modules/ai/services/tool.service';
|
||||||
import { AgentService } from 'src/engine/metadata-modules/agent/agent.service';
|
import { AgentService } from 'src/engine/metadata-modules/agent/agent.service';
|
||||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||||
import { ToolService } from 'src/engine/core-modules/ai/services/tool.service';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AgentToolService {
|
export class AgentToolService {
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import {
|
|||||||
ManyToOne,
|
ManyToOne,
|
||||||
OneToMany,
|
OneToMany,
|
||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
|
Unique,
|
||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
|
|
||||||
@ -20,6 +21,7 @@ import { AgentChatThreadEntity } from './agent-chat-thread.entity';
|
|||||||
|
|
||||||
@Entity('agent')
|
@Entity('agent')
|
||||||
@Index('IDX_AGENT_ID_DELETED_AT', ['id', 'deletedAt'])
|
@Index('IDX_AGENT_ID_DELETED_AT', ['id', 'deletedAt'])
|
||||||
|
@Unique('IDX_AGENT_NAME_WORKSPACE_ID_UNIQUE', ['name', 'workspaceId'])
|
||||||
export class AgentEntity {
|
export class AgentEntity {
|
||||||
@PrimaryGeneratedColumn('uuid')
|
@PrimaryGeneratedColumn('uuid')
|
||||||
id: string;
|
id: string;
|
||||||
@ -27,13 +29,19 @@ export class AgentEntity {
|
|||||||
@Column({ nullable: false })
|
@Column({ nullable: false })
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
|
@Column({ nullable: false })
|
||||||
|
label: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
icon: string;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column({ nullable: true })
|
||||||
description: string;
|
description: string;
|
||||||
|
|
||||||
@Column({ nullable: false, type: 'text' })
|
@Column({ nullable: false, type: 'text' })
|
||||||
prompt: string;
|
prompt: string;
|
||||||
|
|
||||||
@Column({ nullable: false, type: 'varchar' })
|
@Column({ nullable: false, type: 'varchar', default: 'auto' })
|
||||||
modelId: ModelId;
|
modelId: ModelId;
|
||||||
|
|
||||||
@Column({ nullable: true, type: 'jsonb' })
|
@Column({ nullable: true, type: 'jsonb' })
|
||||||
@ -42,6 +50,9 @@ export class AgentEntity {
|
|||||||
@Column({ nullable: false, type: 'uuid' })
|
@Column({ nullable: false, type: 'uuid' })
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
|
|
||||||
|
@Column({ default: false })
|
||||||
|
isCustom: boolean;
|
||||||
|
|
||||||
@ManyToOne(() => Workspace, (workspace) => workspace.agents, {
|
@ManyToOne(() => Workspace, (workspace) => workspace.agents, {
|
||||||
onDelete: 'CASCADE',
|
onDelete: 'CASCADE',
|
||||||
})
|
})
|
||||||
|
|||||||
@ -11,4 +11,5 @@ export enum AgentExceptionCode {
|
|||||||
AGENT_NOT_FOUND = 'AGENT_NOT_FOUND',
|
AGENT_NOT_FOUND = 'AGENT_NOT_FOUND',
|
||||||
AGENT_EXECUTION_FAILED = 'AGENT_EXECUTION_FAILED',
|
AGENT_EXECUTION_FAILED = 'AGENT_EXECUTION_FAILED',
|
||||||
API_KEY_NOT_CONFIGURED = 'API_KEY_NOT_CONFIGURED',
|
API_KEY_NOT_CONFIGURED = 'API_KEY_NOT_CONFIGURED',
|
||||||
|
USER_WORKSPACE_ID_NOT_FOUND = 'USER_WORKSPACE_ID_NOT_FOUND',
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { InjectRepository } from '@nestjs/typeorm';
|
|||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
import { ModelId } from 'src/engine/core-modules/ai/constants/ai-models.const';
|
import { ModelId } from 'src/engine/core-modules/ai/constants/ai-models.const';
|
||||||
|
import { AgentChatService } from 'src/engine/metadata-modules/agent/agent-chat.service';
|
||||||
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
|
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
|
||||||
|
|
||||||
import { AgentEntity } from './agent.entity';
|
import { AgentEntity } from './agent.entity';
|
||||||
@ -16,6 +17,7 @@ export class AgentService {
|
|||||||
private readonly agentRepository: Repository<AgentEntity>,
|
private readonly agentRepository: Repository<AgentEntity>,
|
||||||
@InjectRepository(RoleTargetsEntity, 'core')
|
@InjectRepository(RoleTargetsEntity, 'core')
|
||||||
private readonly roleTargetsRepository: Repository<RoleTargetsEntity>,
|
private readonly roleTargetsRepository: Repository<RoleTargetsEntity>,
|
||||||
|
private readonly agentChatService: AgentChatService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async findManyAgents(workspaceId: string) {
|
async findManyAgents(workspaceId: string) {
|
||||||
@ -51,9 +53,35 @@ export class AgentService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async createOneAgentAndFirstThread(
|
||||||
|
input: {
|
||||||
|
name: string;
|
||||||
|
label: string;
|
||||||
|
description?: string;
|
||||||
|
prompt: string;
|
||||||
|
modelId: ModelId;
|
||||||
|
},
|
||||||
|
workspaceId: string,
|
||||||
|
userWorkspaceId: string | null,
|
||||||
|
) {
|
||||||
|
const agent = await this.createOneAgent(input, workspaceId);
|
||||||
|
|
||||||
|
if (!userWorkspaceId) {
|
||||||
|
throw new AgentException(
|
||||||
|
'User workspace ID not found',
|
||||||
|
AgentExceptionCode.USER_WORKSPACE_ID_NOT_FOUND,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.agentChatService.createThread(agent.id, userWorkspaceId);
|
||||||
|
|
||||||
|
return agent;
|
||||||
|
}
|
||||||
|
|
||||||
async createOneAgent(
|
async createOneAgent(
|
||||||
input: {
|
input: {
|
||||||
name: string;
|
name: string;
|
||||||
|
label: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
prompt: string;
|
prompt: string;
|
||||||
modelId: ModelId;
|
modelId: ModelId;
|
||||||
|
|||||||
@ -10,14 +10,34 @@ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadat
|
|||||||
import { convertObjectMetadataToSchemaProperties } from 'src/engine/utils/convert-object-metadata-to-schema-properties.util';
|
import { convertObjectMetadataToSchemaProperties } from 'src/engine/utils/convert-object-metadata-to-schema-properties.util';
|
||||||
import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
|
import { isFieldMetadataEntityOfType } from 'src/engine/utils/is-field-metadata-of-type.util';
|
||||||
|
|
||||||
export const getRecordInputSchema = (objectMetadata: ObjectMetadataEntity) => {
|
const createToolSchema = (
|
||||||
|
inputProperties: Record<string, JSONSchema7Definition>,
|
||||||
|
required?: string[],
|
||||||
|
) => {
|
||||||
return jsonSchema({
|
return jsonSchema({
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: convertObjectMetadataToSchemaProperties({
|
properties: {
|
||||||
|
toolDescription: {
|
||||||
|
type: 'string',
|
||||||
|
description:
|
||||||
|
'A clear, human-readable description of the action being performed. Explain what operation you are executing and with what parameters in natural language.',
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
type: 'object',
|
||||||
|
properties: inputProperties,
|
||||||
|
...(required && { required }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getRecordInputSchema = (objectMetadata: ObjectMetadataEntity) => {
|
||||||
|
return createToolSchema(
|
||||||
|
convertObjectMetadataToSchemaProperties({
|
||||||
item: objectMetadata,
|
item: objectMetadata,
|
||||||
forResponse: false,
|
forResponse: false,
|
||||||
}),
|
}),
|
||||||
});
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const generateFindToolSchema = (
|
export const generateFindToolSchema = (
|
||||||
@ -48,10 +68,7 @@ export const generateFindToolSchema = (
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return jsonSchema({
|
return createToolSchema(schemaProperties);
|
||||||
type: 'object',
|
|
||||||
properties: schemaProperties,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateFieldFilterJsonSchema = (
|
const generateFieldFilterJsonSchema = (
|
||||||
@ -808,25 +825,22 @@ const generateFieldFilterJsonSchema = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const generateBulkDeleteToolSchema = () => {
|
export const generateBulkDeleteToolSchema = () => {
|
||||||
return jsonSchema({
|
return createToolSchema({
|
||||||
type: 'object',
|
filter: {
|
||||||
properties: {
|
type: 'object',
|
||||||
filter: {
|
description: 'Filter criteria to select records for bulk delete',
|
||||||
type: 'object',
|
properties: {
|
||||||
description: 'Filter criteria to select records for bulk delete',
|
id: {
|
||||||
properties: {
|
type: 'object',
|
||||||
id: {
|
description: 'Filter to select records to delete',
|
||||||
type: 'object',
|
properties: {
|
||||||
description: 'Filter to select records to delete',
|
in: {
|
||||||
properties: {
|
type: 'array',
|
||||||
in: {
|
items: {
|
||||||
type: 'array',
|
type: 'string',
|
||||||
items: {
|
format: 'uuid',
|
||||||
type: 'string',
|
|
||||||
format: 'uuid',
|
|
||||||
},
|
|
||||||
description: 'Array of record IDs to delete',
|
|
||||||
},
|
},
|
||||||
|
description: 'Array of record IDs to delete',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -834,3 +848,29 @@ export const generateBulkDeleteToolSchema = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const generateFindOneToolSchema = () => {
|
||||||
|
return createToolSchema(
|
||||||
|
{
|
||||||
|
id: {
|
||||||
|
type: 'string',
|
||||||
|
format: 'uuid',
|
||||||
|
description: 'The unique UUID of the record to retrieve',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
['id'],
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generateSoftDeleteToolSchema = () => {
|
||||||
|
return createToolSchema(
|
||||||
|
{
|
||||||
|
id: {
|
||||||
|
type: 'string',
|
||||||
|
format: 'uuid',
|
||||||
|
description: 'The unique UUID of the record to soft delete',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
['id'],
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { Repository } from 'typeorm';
|
|||||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||||
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
|
import { AgentService } from 'src/engine/metadata-modules/agent/agent.service';
|
||||||
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
|
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
|
||||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||||
@ -115,6 +116,14 @@ describe('WorkspaceManagerService', () => {
|
|||||||
deleteObjectsMetadata: jest.fn(),
|
deleteObjectsMetadata: jest.fn(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: AgentService,
|
||||||
|
useValue: {
|
||||||
|
createOneAgent: jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue({ id: 'mock-agent-id' }),
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,277 @@
|
|||||||
|
import { DataSource } from 'typeorm';
|
||||||
|
|
||||||
|
import { AgentChatMessageRole } from 'src/engine/metadata-modules/agent/agent-chat-message.entity';
|
||||||
|
import { USER_WORKSPACE_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-user-workspaces.util';
|
||||||
|
import {
|
||||||
|
SEED_APPLE_WORKSPACE_ID,
|
||||||
|
SEED_YCOMBINATOR_WORKSPACE_ID,
|
||||||
|
} from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-workspaces.util';
|
||||||
|
|
||||||
|
const agentTableName = 'agent';
|
||||||
|
const workspaceTableName = 'workspace';
|
||||||
|
const agentChatThreadTableName = 'agentChatThread';
|
||||||
|
const agentChatMessageTableName = 'agentChatMessage';
|
||||||
|
|
||||||
|
export const AGENT_DATA_SEED_IDS = {
|
||||||
|
APPLE_DEFAULT_AGENT: '20202020-0000-4000-8000-000000000001',
|
||||||
|
YCOMBINATOR_DEFAULT_AGENT: '20202020-0000-4000-8000-000000000002',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AGENT_CHAT_THREAD_DATA_SEED_IDS = {
|
||||||
|
APPLE_DEFAULT_THREAD: '20202020-0000-4000-8000-000000000011',
|
||||||
|
YCOMBINATOR_DEFAULT_THREAD: '20202020-0000-4000-8000-000000000012',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AGENT_CHAT_MESSAGE_DATA_SEED_IDS = {
|
||||||
|
APPLE_MESSAGE_1: '20202020-0000-4000-8000-000000000021',
|
||||||
|
APPLE_MESSAGE_2: '20202020-0000-4000-8000-000000000022',
|
||||||
|
APPLE_MESSAGE_3: '20202020-0000-4000-8000-000000000023',
|
||||||
|
APPLE_MESSAGE_4: '20202020-0000-4000-8000-000000000024',
|
||||||
|
YCOMBINATOR_MESSAGE_1: '20202020-0000-4000-8000-000000000031',
|
||||||
|
YCOMBINATOR_MESSAGE_2: '20202020-0000-4000-8000-000000000032',
|
||||||
|
YCOMBINATOR_MESSAGE_3: '20202020-0000-4000-8000-000000000033',
|
||||||
|
YCOMBINATOR_MESSAGE_4: '20202020-0000-4000-8000-000000000034',
|
||||||
|
};
|
||||||
|
|
||||||
|
const seedAgentChatThreads = async (
|
||||||
|
dataSource: DataSource,
|
||||||
|
schemaName: string,
|
||||||
|
workspaceId: string,
|
||||||
|
agentId: string,
|
||||||
|
) => {
|
||||||
|
let threadId: string;
|
||||||
|
let userWorkspaceId: string;
|
||||||
|
|
||||||
|
if (workspaceId === SEED_APPLE_WORKSPACE_ID) {
|
||||||
|
threadId = AGENT_CHAT_THREAD_DATA_SEED_IDS.APPLE_DEFAULT_THREAD;
|
||||||
|
userWorkspaceId = USER_WORKSPACE_DATA_SEED_IDS.TIM;
|
||||||
|
} else if (workspaceId === SEED_YCOMBINATOR_WORKSPACE_ID) {
|
||||||
|
threadId = AGENT_CHAT_THREAD_DATA_SEED_IDS.YCOMBINATOR_DEFAULT_THREAD;
|
||||||
|
userWorkspaceId = USER_WORKSPACE_DATA_SEED_IDS.TIM_ACME;
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
`Unsupported workspace ID for agent chat thread seeding: ${workspaceId}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
await dataSource
|
||||||
|
.createQueryBuilder()
|
||||||
|
.insert()
|
||||||
|
.into(`${schemaName}.${agentChatThreadTableName}`, [
|
||||||
|
'id',
|
||||||
|
'agentId',
|
||||||
|
'userWorkspaceId',
|
||||||
|
'createdAt',
|
||||||
|
'updatedAt',
|
||||||
|
])
|
||||||
|
.orIgnore()
|
||||||
|
.values([
|
||||||
|
{
|
||||||
|
id: threadId,
|
||||||
|
agentId,
|
||||||
|
userWorkspaceId,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
return threadId;
|
||||||
|
};
|
||||||
|
|
||||||
|
const seedAgentChatMessages = async (
|
||||||
|
dataSource: DataSource,
|
||||||
|
schemaName: string,
|
||||||
|
workspaceId: string,
|
||||||
|
threadId: string,
|
||||||
|
) => {
|
||||||
|
let messageIds: string[];
|
||||||
|
let messages: Array<{
|
||||||
|
id: string;
|
||||||
|
threadId: string;
|
||||||
|
role: AgentChatMessageRole;
|
||||||
|
content: string;
|
||||||
|
createdAt: Date;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const baseTime = new Date(now.getTime() - 24 * 60 * 60 * 1000); // 24 hours ago
|
||||||
|
|
||||||
|
if (workspaceId === SEED_APPLE_WORKSPACE_ID) {
|
||||||
|
messageIds = [
|
||||||
|
AGENT_CHAT_MESSAGE_DATA_SEED_IDS.APPLE_MESSAGE_1,
|
||||||
|
AGENT_CHAT_MESSAGE_DATA_SEED_IDS.APPLE_MESSAGE_2,
|
||||||
|
AGENT_CHAT_MESSAGE_DATA_SEED_IDS.APPLE_MESSAGE_3,
|
||||||
|
AGENT_CHAT_MESSAGE_DATA_SEED_IDS.APPLE_MESSAGE_4,
|
||||||
|
];
|
||||||
|
messages = [
|
||||||
|
{
|
||||||
|
id: messageIds[0],
|
||||||
|
threadId,
|
||||||
|
role: AgentChatMessageRole.USER,
|
||||||
|
content:
|
||||||
|
'Hello! Can you help me understand our current product roadmap and key metrics?',
|
||||||
|
createdAt: new Date(baseTime.getTime()),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: messageIds[1],
|
||||||
|
threadId,
|
||||||
|
role: AgentChatMessageRole.ASSISTANT,
|
||||||
|
content:
|
||||||
|
"Hello! I'd be happy to help you understand Apple's product roadmap and metrics. Based on your workspace data, I can see you have various projects and initiatives tracked. What specific aspect would you like to explore - product development timelines, user engagement metrics, or revenue targets?",
|
||||||
|
createdAt: new Date(baseTime.getTime() + 5 * 60 * 1000), // 5 minutes later
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: messageIds[2],
|
||||||
|
threadId,
|
||||||
|
role: AgentChatMessageRole.USER,
|
||||||
|
content:
|
||||||
|
"I'd like to focus on our user engagement metrics and how they're trending over the last quarter.",
|
||||||
|
createdAt: new Date(baseTime.getTime() + 10 * 60 * 1000), // 10 minutes later
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: messageIds[3],
|
||||||
|
threadId,
|
||||||
|
role: AgentChatMessageRole.ASSISTANT,
|
||||||
|
content:
|
||||||
|
'Great! Looking at your user engagement data, I can see several key trends from the last quarter. Your active user base has grown by 15%, with particularly strong engagement in the mobile app. Daily active users are averaging 2.3 million, and session duration has increased by 8%. Would you like me to dive deeper into any specific engagement metrics or create a detailed report?',
|
||||||
|
createdAt: new Date(baseTime.getTime() + 15 * 60 * 1000), // 15 minutes later
|
||||||
|
},
|
||||||
|
];
|
||||||
|
} else if (workspaceId === SEED_YCOMBINATOR_WORKSPACE_ID) {
|
||||||
|
messageIds = [
|
||||||
|
AGENT_CHAT_MESSAGE_DATA_SEED_IDS.YCOMBINATOR_MESSAGE_1,
|
||||||
|
AGENT_CHAT_MESSAGE_DATA_SEED_IDS.YCOMBINATOR_MESSAGE_2,
|
||||||
|
AGENT_CHAT_MESSAGE_DATA_SEED_IDS.YCOMBINATOR_MESSAGE_3,
|
||||||
|
AGENT_CHAT_MESSAGE_DATA_SEED_IDS.YCOMBINATOR_MESSAGE_4,
|
||||||
|
];
|
||||||
|
messages = [
|
||||||
|
{
|
||||||
|
id: messageIds[0],
|
||||||
|
threadId,
|
||||||
|
role: AgentChatMessageRole.USER,
|
||||||
|
content:
|
||||||
|
'What are the current startup trends and which companies in our portfolio are performing best?',
|
||||||
|
createdAt: new Date(baseTime.getTime()),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: messageIds[1],
|
||||||
|
threadId,
|
||||||
|
role: AgentChatMessageRole.ASSISTANT,
|
||||||
|
content:
|
||||||
|
'Hello! I can help you analyze startup trends and portfolio performance. From your YCombinator workspace data, I can see strong performance in AI/ML startups, particularly in the B2B SaaS space. Several companies are showing 40%+ month-over-month growth. Would you like me to provide specific company performance metrics or focus on broader industry trends?',
|
||||||
|
createdAt: new Date(baseTime.getTime() + 3 * 60 * 1000), // 3 minutes later
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: messageIds[2],
|
||||||
|
threadId,
|
||||||
|
role: AgentChatMessageRole.USER,
|
||||||
|
content:
|
||||||
|
'Please focus on our top 5 performing companies and their key metrics.',
|
||||||
|
createdAt: new Date(baseTime.getTime() + 8 * 60 * 1000), // 8 minutes later
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: messageIds[3],
|
||||||
|
threadId,
|
||||||
|
role: AgentChatMessageRole.ASSISTANT,
|
||||||
|
content:
|
||||||
|
'Here are your top 5 performing portfolio companies: 1) TechFlow AI - 45% MoM growth, $2M ARR, 2) DataSync Pro - 38% MoM growth, $1.5M ARR, 3) CloudOps Solutions - 35% MoM growth, $3.2M ARR, 4) SecureNet - 32% MoM growth, $1.8M ARR, 5) HealthTech Plus - 28% MoM growth, $2.5M ARR. All are showing strong customer retention (>95%) and expanding market share. Would you like detailed breakdowns for any specific company?',
|
||||||
|
createdAt: new Date(baseTime.getTime() + 12 * 60 * 1000), // 12 minutes later
|
||||||
|
},
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
`Unsupported workspace ID for agent chat message seeding: ${workspaceId}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await dataSource
|
||||||
|
.createQueryBuilder()
|
||||||
|
.insert()
|
||||||
|
.into(`${schemaName}.${agentChatMessageTableName}`, [
|
||||||
|
'id',
|
||||||
|
'threadId',
|
||||||
|
'role',
|
||||||
|
'content',
|
||||||
|
'createdAt',
|
||||||
|
])
|
||||||
|
.orIgnore()
|
||||||
|
.values(messages)
|
||||||
|
.execute();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const seedAgents = async (
|
||||||
|
dataSource: DataSource,
|
||||||
|
schemaName: string,
|
||||||
|
workspaceId: string,
|
||||||
|
) => {
|
||||||
|
let agentId: string;
|
||||||
|
let agentName: string;
|
||||||
|
let agentLabel: string;
|
||||||
|
let agentDescription: string;
|
||||||
|
|
||||||
|
if (workspaceId === SEED_APPLE_WORKSPACE_ID) {
|
||||||
|
agentId = AGENT_DATA_SEED_IDS.APPLE_DEFAULT_AGENT;
|
||||||
|
agentName = 'apple-ai-assistant';
|
||||||
|
agentLabel = 'Apple AI Assistant';
|
||||||
|
agentDescription =
|
||||||
|
'AI assistant for Apple workspace to help with tasks, insights, and workflow guidance';
|
||||||
|
} else if (workspaceId === SEED_YCOMBINATOR_WORKSPACE_ID) {
|
||||||
|
agentId = AGENT_DATA_SEED_IDS.YCOMBINATOR_DEFAULT_AGENT;
|
||||||
|
agentName = 'yc-ai-assistant';
|
||||||
|
agentLabel = 'YC AI Assistant';
|
||||||
|
agentDescription =
|
||||||
|
'AI assistant for YCombinator workspace to help with tasks, insights, and workflow guidance';
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
`Unsupported workspace ID for agent seeding: ${workspaceId}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await dataSource
|
||||||
|
.createQueryBuilder()
|
||||||
|
.insert()
|
||||||
|
.into(`${schemaName}.${agentTableName}`, [
|
||||||
|
'id',
|
||||||
|
'name',
|
||||||
|
'label',
|
||||||
|
'description',
|
||||||
|
'prompt',
|
||||||
|
'modelId',
|
||||||
|
'responseFormat',
|
||||||
|
'workspaceId',
|
||||||
|
])
|
||||||
|
.orIgnore()
|
||||||
|
.values([
|
||||||
|
{
|
||||||
|
id: agentId,
|
||||||
|
name: agentName,
|
||||||
|
label: agentLabel,
|
||||||
|
description: agentDescription,
|
||||||
|
prompt:
|
||||||
|
'You are a helpful AI assistant for this workspace. Help users with their tasks, provide insights about their data, and guide them through workflows. Be concise but thorough in your responses.',
|
||||||
|
modelId: 'auto',
|
||||||
|
responseFormat: null,
|
||||||
|
workspaceId,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await dataSource
|
||||||
|
.createQueryBuilder()
|
||||||
|
.update(`${schemaName}.${workspaceTableName}`)
|
||||||
|
.set({ defaultAgentId: agentId })
|
||||||
|
.where('id = :workspaceId', { workspaceId })
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
const threadId = await seedAgentChatThreads(
|
||||||
|
dataSource,
|
||||||
|
schemaName,
|
||||||
|
workspaceId,
|
||||||
|
agentId,
|
||||||
|
);
|
||||||
|
|
||||||
|
await seedAgentChatMessages(dataSource, schemaName, workspaceId, threadId);
|
||||||
|
};
|
||||||
@ -1,6 +1,7 @@
|
|||||||
import { DataSource } from 'typeorm';
|
import { DataSource } from 'typeorm';
|
||||||
|
|
||||||
import { seedBillingSubscriptions } from 'src/engine/workspace-manager/dev-seeder/core/billing/utils/seed-billing-subscriptions.util';
|
import { seedBillingSubscriptions } from 'src/engine/workspace-manager/dev-seeder/core/billing/utils/seed-billing-subscriptions.util';
|
||||||
|
import { seedAgents } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-agents.util';
|
||||||
import { seedApiKeys } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-api-keys.util';
|
import { seedApiKeys } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-api-keys.util';
|
||||||
import { seedFeatureFlags } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util';
|
import { seedFeatureFlags } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util';
|
||||||
import { seedUserWorkspaces } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-user-workspaces.util';
|
import { seedUserWorkspaces } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-user-workspaces.util';
|
||||||
@ -33,6 +34,8 @@ export const seedCoreSchema = async ({
|
|||||||
await seedUsers(dataSource, schemaName);
|
await seedUsers(dataSource, schemaName);
|
||||||
await seedUserWorkspaces(dataSource, schemaName, workspaceId);
|
await seedUserWorkspaces(dataSource, schemaName, workspaceId);
|
||||||
|
|
||||||
|
await seedAgents(dataSource, schemaName, workspaceId);
|
||||||
|
|
||||||
await seedApiKeys(dataSource, schemaName, workspaceId);
|
await seedApiKeys(dataSource, schemaName, workspaceId);
|
||||||
|
|
||||||
if (shouldSeedFeatureFlags) {
|
if (shouldSeedFeatureFlags) {
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
|||||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||||
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
|
import { AgentModule } from 'src/engine/metadata-modules/agent/agent.module';
|
||||||
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
||||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||||
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
|
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
|
||||||
@ -33,6 +34,7 @@ import { WorkspaceManagerService } from './workspace-manager.service';
|
|||||||
WorkspaceHealthModule,
|
WorkspaceHealthModule,
|
||||||
FeatureFlagModule,
|
FeatureFlagModule,
|
||||||
PermissionsModule,
|
PermissionsModule,
|
||||||
|
AgentModule,
|
||||||
TypeOrmModule.forFeature([UserWorkspace, Workspace], 'core'),
|
TypeOrmModule.forFeature([UserWorkspace, Workspace], 'core'),
|
||||||
RoleModule,
|
RoleModule,
|
||||||
UserRoleModule,
|
UserRoleModule,
|
||||||
|
|||||||
@ -3,9 +3,11 @@ import { InjectRepository } from '@nestjs/typeorm';
|
|||||||
|
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
|
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||||
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
|
import { AgentService } from 'src/engine/metadata-modules/agent/agent.service';
|
||||||
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
|
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
|
||||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||||
@ -42,6 +44,7 @@ export class WorkspaceManagerService {
|
|||||||
private readonly roleRepository: Repository<RoleEntity>,
|
private readonly roleRepository: Repository<RoleEntity>,
|
||||||
@InjectRepository(RoleTargetsEntity, 'core')
|
@InjectRepository(RoleTargetsEntity, 'core')
|
||||||
private readonly roleTargetsRepository: Repository<RoleTargetsEntity>,
|
private readonly roleTargetsRepository: Repository<RoleTargetsEntity>,
|
||||||
|
private readonly agentService: AgentService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async init({
|
public async init({
|
||||||
@ -95,6 +98,17 @@ export class WorkspaceManagerService {
|
|||||||
`Permissions enabled took ${permissionsEnabledEnd - permissionsEnabledStart}ms`,
|
`Permissions enabled took ${permissionsEnabledEnd - permissionsEnabledStart}ms`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (featureFlags[FeatureFlagKey.IS_AI_ENABLED]) {
|
||||||
|
const defaultAgentEnabledStart = performance.now();
|
||||||
|
|
||||||
|
await this.initDefaultAgent(workspaceId);
|
||||||
|
const defaultAgentEnabledEnd = performance.now();
|
||||||
|
|
||||||
|
this.logger.log(
|
||||||
|
`Default agent enabled took ${defaultAgentEnabledEnd - defaultAgentEnabledStart}ms`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const prefillStandardObjectsStart = performance.now();
|
const prefillStandardObjectsStart = performance.now();
|
||||||
|
|
||||||
await this.prefillWorkspaceWithStandardObjectsRecords(
|
await this.prefillWorkspaceWithStandardObjectsRecords(
|
||||||
@ -194,4 +208,21 @@ export class WorkspaceManagerService {
|
|||||||
defaultRoleId: memberRole.id,
|
defaultRoleId: memberRole.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async initDefaultAgent(workspaceId: string) {
|
||||||
|
const agent = await this.agentService.createOneAgent(
|
||||||
|
{
|
||||||
|
label: 'Routing Agent',
|
||||||
|
name: 'routing-agent',
|
||||||
|
description: 'Default Routing Agent',
|
||||||
|
prompt: '',
|
||||||
|
modelId: 'auto',
|
||||||
|
},
|
||||||
|
workspaceId,
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.workspaceRepository.update(workspaceId, {
|
||||||
|
defaultAgentId: agent.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -616,14 +616,16 @@ export class WorkflowVersionStepWorkspaceService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
case WorkflowActionType.AI_AGENT: {
|
case WorkflowActionType.AI_AGENT: {
|
||||||
const newAgent = await this.agentService.createOneAgent(
|
const newAgent = await this.agentService.createOneAgentAndFirstThread(
|
||||||
{
|
{
|
||||||
name: 'AI Agent Workflow Step',
|
label: 'AI Agent Workflow Step',
|
||||||
|
name: 'ai-agent-workflow',
|
||||||
description: 'Created automatically for workflow step',
|
description: 'Created automatically for workflow step',
|
||||||
prompt: '',
|
prompt: '',
|
||||||
modelId: 'gpt-4o',
|
modelId: 'auto',
|
||||||
},
|
},
|
||||||
workspaceId,
|
workspaceId,
|
||||||
|
this.scopedWorkspaceContextFactory.create().userWorkspaceId,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isDefined(newAgent)) {
|
if (!isDefined(newAgent)) {
|
||||||
@ -636,15 +638,13 @@ export class WorkflowVersionStepWorkspaceService {
|
|||||||
const userWorkspaceId =
|
const userWorkspaceId =
|
||||||
this.scopedWorkspaceContextFactory.create().userWorkspaceId;
|
this.scopedWorkspaceContextFactory.create().userWorkspaceId;
|
||||||
|
|
||||||
if (!userWorkspaceId) {
|
if (userWorkspaceId) {
|
||||||
throw new WorkflowVersionStepException(
|
await this.agentChatService.createThread(
|
||||||
'User workspace ID not found',
|
newAgent.id,
|
||||||
WorkflowVersionStepExceptionCode.FAILURE,
|
userWorkspaceId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.agentChatService.createThread(newAgent.id, userWorkspaceId);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: newStepId,
|
id: newStepId,
|
||||||
name: 'AI Agent',
|
name: 'AI Agent',
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
|
||||||
import { AgentService } from 'src/engine/metadata-modules/agent/agent.service';
|
|
||||||
import { AgentResolver } from 'src/engine/metadata-modules/agent/agent.resolver';
|
|
||||||
import {
|
import {
|
||||||
AgentException,
|
AgentException,
|
||||||
AgentExceptionCode,
|
AgentExceptionCode,
|
||||||
} from 'src/engine/metadata-modules/agent/agent.exception';
|
} from 'src/engine/metadata-modules/agent/agent.exception';
|
||||||
|
import { AgentResolver } from 'src/engine/metadata-modules/agent/agent.resolver';
|
||||||
|
import { AgentService } from 'src/engine/metadata-modules/agent/agent.service';
|
||||||
|
|
||||||
// Mock the agent service
|
// Mock the agent service
|
||||||
jest.mock('../../../../../src/engine/metadata-modules/agent/agent.service');
|
jest.mock('../../../../../src/engine/metadata-modules/agent/agent.service');
|
||||||
|
|||||||
@ -216,7 +216,7 @@ describe('AgentToolService Integration', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const result = await createTool.execute(
|
const result = await createTool.execute(
|
||||||
{ name: 'Test Record', description: 'Test description' },
|
{ input: { name: 'Test Record', description: 'Test description' } },
|
||||||
{
|
{
|
||||||
toolCallId: 'test-tool-call-id',
|
toolCallId: 'test-tool-call-id',
|
||||||
messages: [
|
messages: [
|
||||||
@ -260,7 +260,7 @@ describe('AgentToolService Integration', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const result = await createTool.execute(
|
const result = await createTool.execute(
|
||||||
{ name: 'Test Record' },
|
{ input: { name: 'Test Record' } },
|
||||||
{
|
{
|
||||||
toolCallId: 'test-tool-call-id',
|
toolCallId: 'test-tool-call-id',
|
||||||
messages: [
|
messages: [
|
||||||
@ -304,7 +304,7 @@ describe('AgentToolService Integration', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const result = await findTool.execute(
|
const result = await findTool.execute(
|
||||||
{ limit: 10, offset: 0 },
|
{ input: { limit: 10, offset: 0 } },
|
||||||
{
|
{
|
||||||
toolCallId: 'test-tool-call-id',
|
toolCallId: 'test-tool-call-id',
|
||||||
messages: [
|
messages: [
|
||||||
@ -352,7 +352,7 @@ describe('AgentToolService Integration', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const result = await findOneTool.execute(
|
const result = await findOneTool.execute(
|
||||||
{ id: 'test-record-id' },
|
{ input: { id: 'test-record-id' } },
|
||||||
{
|
{
|
||||||
toolCallId: 'test-tool-call-id',
|
toolCallId: 'test-tool-call-id',
|
||||||
messages: [
|
messages: [
|
||||||
@ -393,7 +393,7 @@ describe('AgentToolService Integration', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const result = await findOneTool.execute(
|
const result = await findOneTool.execute(
|
||||||
{ id: 'non-existent-id' },
|
{ input: { id: 'non-existent-id' } },
|
||||||
{
|
{
|
||||||
toolCallId: 'test-tool-call-id',
|
toolCallId: 'test-tool-call-id',
|
||||||
messages: [
|
messages: [
|
||||||
@ -430,7 +430,7 @@ describe('AgentToolService Integration', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const result = await findOneTool.execute(
|
const result = await findOneTool.execute(
|
||||||
{},
|
{ input: {} },
|
||||||
{
|
{
|
||||||
toolCallId: 'test-tool-call-id',
|
toolCallId: 'test-tool-call-id',
|
||||||
messages: [
|
messages: [
|
||||||
@ -488,9 +488,11 @@ describe('AgentToolService Integration', () => {
|
|||||||
|
|
||||||
const result = await updateTool.execute(
|
const result = await updateTool.execute(
|
||||||
{
|
{
|
||||||
id: 'test-record-id',
|
input: {
|
||||||
name: 'New Name',
|
id: 'test-record-id',
|
||||||
description: 'New description',
|
name: 'New Name',
|
||||||
|
description: 'New description',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
toolCallId: 'test-tool-call-id',
|
toolCallId: 'test-tool-call-id',
|
||||||
@ -534,8 +536,10 @@ describe('AgentToolService Integration', () => {
|
|||||||
|
|
||||||
const result = await updateTool.execute(
|
const result = await updateTool.execute(
|
||||||
{
|
{
|
||||||
id: 'non-existent-id',
|
input: {
|
||||||
name: 'New Name',
|
id: 'non-existent-id',
|
||||||
|
name: 'New Name',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
toolCallId: 'test-tool-call-id',
|
toolCallId: 'test-tool-call-id',
|
||||||
@ -583,7 +587,7 @@ describe('AgentToolService Integration', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const result = await softDeleteTool.execute(
|
const result = await softDeleteTool.execute(
|
||||||
{ id: 'test-record-id' },
|
{ input: { id: 'test-record-id' } },
|
||||||
{
|
{
|
||||||
toolCallId: 'test-tool-call-id',
|
toolCallId: 'test-tool-call-id',
|
||||||
messages: [
|
messages: [
|
||||||
@ -624,7 +628,9 @@ describe('AgentToolService Integration', () => {
|
|||||||
|
|
||||||
const result = await softDeleteManyTool.execute(
|
const result = await softDeleteManyTool.execute(
|
||||||
{
|
{
|
||||||
filter: { id: { in: ['record-1', 'record-2', 'record-3'] } },
|
input: {
|
||||||
|
filter: { id: { in: ['record-1', 'record-2', 'record-3'] } },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
toolCallId: 'test-tool-call-id',
|
toolCallId: 'test-tool-call-id',
|
||||||
@ -671,7 +677,7 @@ describe('AgentToolService Integration', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const result = await findTool.execute(
|
const result = await findTool.execute(
|
||||||
{},
|
{ input: {} },
|
||||||
{
|
{
|
||||||
toolCallId: 'test-tool-call-id',
|
toolCallId: 'test-tool-call-id',
|
||||||
messages: [
|
messages: [
|
||||||
@ -716,10 +722,12 @@ describe('AgentToolService Integration', () => {
|
|||||||
|
|
||||||
const result = await findTool.execute(
|
const result = await findTool.execute(
|
||||||
{
|
{
|
||||||
name: null,
|
input: {
|
||||||
description: undefined,
|
name: null,
|
||||||
status: '',
|
description: undefined,
|
||||||
validField: 'valid value',
|
status: '',
|
||||||
|
validField: 'valid value',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
toolCallId: 'test-tool-call-id',
|
toolCallId: 'test-tool-call-id',
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { getRepositoryToken } from '@nestjs/typeorm';
|
|||||||
|
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
|
import { ToolService } from 'src/engine/core-modules/ai/services/tool.service';
|
||||||
import { AgentToolService } from 'src/engine/metadata-modules/agent/agent-tool.service';
|
import { AgentToolService } from 'src/engine/metadata-modules/agent/agent-tool.service';
|
||||||
import { AgentEntity } from 'src/engine/metadata-modules/agent/agent.entity';
|
import { AgentEntity } from 'src/engine/metadata-modules/agent/agent.entity';
|
||||||
import { AgentService } from 'src/engine/metadata-modules/agent/agent.service';
|
import { AgentService } from 'src/engine/metadata-modules/agent/agent.service';
|
||||||
@ -12,7 +13,6 @@ import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
|||||||
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
|
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
|
||||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||||
import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
|
import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
|
||||||
import { ToolService } from 'src/engine/core-modules/ai/services/tool.service';
|
|
||||||
|
|
||||||
export interface AgentToolTestContext {
|
export interface AgentToolTestContext {
|
||||||
module: TestingModule;
|
module: TestingModule;
|
||||||
@ -103,7 +103,10 @@ export const createAgentToolTestModule =
|
|||||||
|
|
||||||
const testAgent: AgentEntity & { roleId: string | null } = {
|
const testAgent: AgentEntity & { roleId: string | null } = {
|
||||||
id: testAgentId,
|
id: testAgentId,
|
||||||
name: 'Test Agent',
|
name: 'test-agent',
|
||||||
|
label: 'Test Agent',
|
||||||
|
icon: 'IconTest',
|
||||||
|
isCustom: false,
|
||||||
description: 'Test agent for integration tests',
|
description: 'Test agent for integration tests',
|
||||||
prompt: 'You are a test agent',
|
prompt: 'You are a test agent',
|
||||||
modelId: 'gpt-4o',
|
modelId: 'gpt-4o',
|
||||||
|
|||||||
Reference in New Issue
Block a user