feat(sdk): add runAgent() to run app agents from logic functions (#21157)
<img width="948" height="593" alt="image" src="https://github.com/user-attachments/assets/d990fa98-3cfd-469d-ab7f-0b2d4ccf3afc" /> <img width="1361" height="802" alt="image" src="https://github.com/user-attachments/assets/1091f598-49f3-4c16-92ea-1e1c200181e2" /> ## Add `runAgent()` to the Logic Function SDK Lets an app's logic function run one of its own AI agents server-side and get the result back synchronously — reusing the existing agent executor instead of a new bespoke transport. ### Backend - New **`runAgent` GraphQL mutation** (metadata schema) in `ai-agent-execution`, wrapping the existing `AgentAsyncExecutorService.executeAgent`. Scopes the agent lookup to the calling application and runs it under an application auth context. - New `@AuthApplication()` param decorator (mirrors `@AuthWorkspace()`) — first GraphQL resolver authenticated by an **application access token**. - Guarded by `WorkspaceAuthGuard` + `SettingsPermissionGuard(PermissionFlagType.AI)`: the app's role must grant the `AI` permission flag. ### SDK - `runAgent({ agentUniversalIdentifier, prompt })` posts the mutation to `/metadata` with the app token via a new runtime GraphQL transport. Returns `{ result, hasNoMoreAvailableCredits }`. - Refactored the connections helpers onto a shared `postAppEndpoint` util (removes duplicated transport logic). ### Frontend - App install permission modal now shows an explicit consent line — _"Run AI agents and bill AI credits to your workspace"_ — when the app's role requests the `AI` flag. ### Docs - Documented `runAgent` and its `AI` permission-flag requirement in _Skills & Agents_. - Fixed outdated role-permission examples in _Roles & Permissions_ (`permissionFlags` → `permissionFlagUniversalIdentifiers`, `PermissionFlag` → `SystemPermissionFlag`). ### Test plan - [x] SDK unit tests (`run-agent.spec.ts`) — request shape, GraphQL/HTTP error handling, missing env vars - [x] `twenty-server`, `twenty-front`, `twenty-shared` typecheck + lint - [ ] Manual: install an app granting the `AI` flag, call `runAgent()` from a logic function, confirm the agent runs and credits are billed --------- Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
This commit is contained in:
@@ -2132,6 +2132,18 @@ type FieldConnection {
|
||||
edges: [FieldEdge!]!
|
||||
}
|
||||
|
||||
type AppConnection {
|
||||
id: ID!
|
||||
providerName: String!
|
||||
name: String!
|
||||
handle: String!
|
||||
visibility: String!
|
||||
userWorkspaceId: String!
|
||||
accessToken: String!
|
||||
scopes: [String!]!
|
||||
authFailedAt: String
|
||||
}
|
||||
|
||||
type ResendEmailVerificationToken {
|
||||
success: Boolean!
|
||||
}
|
||||
@@ -2529,6 +2541,12 @@ type AgentMessagePart {
|
||||
createdAt: DateTime!
|
||||
}
|
||||
|
||||
type RunAgentResult {
|
||||
result: JSON
|
||||
error: String
|
||||
success: Boolean!
|
||||
}
|
||||
|
||||
type ChannelSyncSuccess {
|
||||
success: Boolean!
|
||||
}
|
||||
@@ -3011,6 +3029,8 @@ type Query {
|
||||
myMessageChannels(connectedAccountId: UUID): [MessageChannel!]!
|
||||
myCalendarChannels(connectedAccountId: UUID): [CalendarChannel!]!
|
||||
minimalMetadata: MinimalMetadata!
|
||||
appConnections(filter: ListAppConnectionsInput): [AppConnection!]!
|
||||
appConnection(id: ID!): AppConnection!
|
||||
findWorkspaceAiStats: WorkspaceAiStats!
|
||||
chatThreads: [AgentChatThread!]!
|
||||
chatThread(id: UUID!): AgentChatThread!
|
||||
@@ -3065,6 +3085,12 @@ input AgentIdInput {
|
||||
id: UUID!
|
||||
}
|
||||
|
||||
input ListAppConnectionsInput {
|
||||
providerName: String
|
||||
userWorkspaceId: String
|
||||
visibility: String
|
||||
}
|
||||
|
||||
input EventLogQueryInput {
|
||||
table: EventLogTable!
|
||||
filters: EventLogFiltersInput
|
||||
@@ -3238,6 +3264,7 @@ type Mutation {
|
||||
upsertRowLevelPermissionPredicates(input: UpsertRowLevelPermissionPredicatesInput!): UpsertRowLevelPermissionPredicatesResult!
|
||||
assignRoleToAgent(agentId: UUID!, roleId: UUID!): Boolean!
|
||||
removeRoleFromAgent(agentId: UUID!): Boolean!
|
||||
runAgent(input: RunAgentInput!): RunAgentResult!
|
||||
createWebhook(input: CreateWebhookInput!): Webhook!
|
||||
updateWebhook(input: UpdateWebhookInput!): Webhook!
|
||||
deleteWebhook(id: UUID!): Webhook!
|
||||
@@ -4093,6 +4120,11 @@ input RowLevelPermissionPredicateGroupInput {
|
||||
positionInRowLevelPermissionPredicateGroup: Float
|
||||
}
|
||||
|
||||
input RunAgentInput {
|
||||
agentUniversalIdentifier: String!
|
||||
prompt: String!
|
||||
}
|
||||
|
||||
input CreateWebhookInput {
|
||||
id: UUID
|
||||
targetUrl: String!
|
||||
|
||||
@@ -1762,6 +1762,19 @@ export interface FieldConnection {
|
||||
__typename: 'FieldConnection'
|
||||
}
|
||||
|
||||
export interface AppConnection {
|
||||
id: Scalars['ID']
|
||||
providerName: Scalars['String']
|
||||
name: Scalars['String']
|
||||
handle: Scalars['String']
|
||||
visibility: Scalars['String']
|
||||
userWorkspaceId: Scalars['String']
|
||||
accessToken: Scalars['String']
|
||||
scopes: Scalars['String'][]
|
||||
authFailedAt?: Scalars['String']
|
||||
__typename: 'AppConnection'
|
||||
}
|
||||
|
||||
export interface ResendEmailVerificationToken {
|
||||
success: Scalars['Boolean']
|
||||
__typename: 'ResendEmailVerificationToken'
|
||||
@@ -2211,6 +2224,13 @@ export interface AgentMessagePart {
|
||||
__typename: 'AgentMessagePart'
|
||||
}
|
||||
|
||||
export interface RunAgentResult {
|
||||
result?: Scalars['JSON']
|
||||
error?: Scalars['String']
|
||||
success: Scalars['Boolean']
|
||||
__typename: 'RunAgentResult'
|
||||
}
|
||||
|
||||
export interface ChannelSyncSuccess {
|
||||
success: Scalars['Boolean']
|
||||
__typename: 'ChannelSyncSuccess'
|
||||
@@ -2607,6 +2627,8 @@ export interface Query {
|
||||
myMessageChannels: MessageChannel[]
|
||||
myCalendarChannels: CalendarChannel[]
|
||||
minimalMetadata: MinimalMetadata
|
||||
appConnections: AppConnection[]
|
||||
appConnection: AppConnection
|
||||
findWorkspaceAiStats: WorkspaceAiStats
|
||||
chatThreads: AgentChatThread[]
|
||||
chatThread: AgentChatThread
|
||||
@@ -2767,6 +2789,7 @@ export interface Mutation {
|
||||
upsertRowLevelPermissionPredicates: UpsertRowLevelPermissionPredicatesResult
|
||||
assignRoleToAgent: Scalars['Boolean']
|
||||
removeRoleFromAgent: Scalars['Boolean']
|
||||
runAgent: RunAgentResult
|
||||
createWebhook: Webhook
|
||||
updateWebhook: Webhook
|
||||
deleteWebhook: Webhook
|
||||
@@ -4722,6 +4745,20 @@ export interface FieldConnectionGenqlSelection{
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface AppConnectionGenqlSelection{
|
||||
id?: boolean | number
|
||||
providerName?: boolean | number
|
||||
name?: boolean | number
|
||||
handle?: boolean | number
|
||||
visibility?: boolean | number
|
||||
userWorkspaceId?: boolean | number
|
||||
accessToken?: boolean | number
|
||||
scopes?: boolean | number
|
||||
authFailedAt?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface ResendEmailVerificationTokenGenqlSelection{
|
||||
success?: boolean | number
|
||||
__typename?: boolean | number
|
||||
@@ -5226,6 +5263,14 @@ export interface AgentMessagePartGenqlSelection{
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface RunAgentResultGenqlSelection{
|
||||
result?: boolean | number
|
||||
error?: boolean | number
|
||||
success?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface ChannelSyncSuccessGenqlSelection{
|
||||
success?: boolean | number
|
||||
__typename?: boolean | number
|
||||
@@ -5646,6 +5691,8 @@ export interface QueryGenqlSelection{
|
||||
myMessageChannels?: (MessageChannelGenqlSelection & { __args?: {connectedAccountId?: (Scalars['UUID'] | null)} })
|
||||
myCalendarChannels?: (CalendarChannelGenqlSelection & { __args?: {connectedAccountId?: (Scalars['UUID'] | null)} })
|
||||
minimalMetadata?: MinimalMetadataGenqlSelection
|
||||
appConnections?: (AppConnectionGenqlSelection & { __args?: {filter?: (ListAppConnectionsInput | null)} })
|
||||
appConnection?: (AppConnectionGenqlSelection & { __args: {id: Scalars['ID']} })
|
||||
findWorkspaceAiStats?: WorkspaceAiStatsGenqlSelection
|
||||
chatThreads?: AgentChatThreadGenqlSelection
|
||||
chatThread?: (AgentChatThreadGenqlSelection & { __args: {id: Scalars['UUID']} })
|
||||
@@ -5698,6 +5745,8 @@ export interface AgentIdInput {
|
||||
/** The id of the agent. */
|
||||
id: Scalars['UUID']}
|
||||
|
||||
export interface ListAppConnectionsInput {providerName?: (Scalars['String'] | null),userWorkspaceId?: (Scalars['String'] | null),visibility?: (Scalars['String'] | null)}
|
||||
|
||||
export interface EventLogQueryInput {table: EventLogTable,filters?: (EventLogFiltersInput | null),first?: (Scalars['Int'] | null),after?: (Scalars['String'] | null)}
|
||||
|
||||
export interface EventLogFiltersInput {eventType?: (Scalars['String'] | null),userWorkspaceId?: (Scalars['String'] | null),dateRange?: (EventLogDateRangeInput | null),recordId?: (Scalars['String'] | null),objectMetadataId?: (Scalars['String'] | null)}
|
||||
@@ -5827,6 +5876,7 @@ export interface MutationGenqlSelection{
|
||||
upsertRowLevelPermissionPredicates?: (UpsertRowLevelPermissionPredicatesResultGenqlSelection & { __args: {input: UpsertRowLevelPermissionPredicatesInput} })
|
||||
assignRoleToAgent?: { __args: {agentId: Scalars['UUID'], roleId: Scalars['UUID']} }
|
||||
removeRoleFromAgent?: { __args: {agentId: Scalars['UUID']} }
|
||||
runAgent?: (RunAgentResultGenqlSelection & { __args: {input: RunAgentInput} })
|
||||
createWebhook?: (WebhookGenqlSelection & { __args: {input: CreateWebhookInput} })
|
||||
updateWebhook?: (WebhookGenqlSelection & { __args: {input: UpdateWebhookInput} })
|
||||
deleteWebhook?: (WebhookGenqlSelection & { __args: {id: Scalars['UUID']} })
|
||||
@@ -6184,6 +6234,8 @@ export interface RowLevelPermissionPredicateInput {id?: (Scalars['UUID'] | null)
|
||||
|
||||
export interface RowLevelPermissionPredicateGroupInput {id?: (Scalars['UUID'] | null),objectMetadataId: Scalars['UUID'],parentRowLevelPermissionPredicateGroupId?: (Scalars['UUID'] | null),logicalOperator: RowLevelPermissionPredicateGroupLogicalOperator,positionInRowLevelPermissionPredicateGroup?: (Scalars['Float'] | null)}
|
||||
|
||||
export interface RunAgentInput {agentUniversalIdentifier: Scalars['String'],prompt: Scalars['String']}
|
||||
|
||||
export interface CreateWebhookInput {id?: (Scalars['UUID'] | null),targetUrl: Scalars['String'],operations: Scalars['String'][],description?: (Scalars['String'] | null),secret?: (Scalars['String'] | null)}
|
||||
|
||||
export interface UpdateWebhookInput {
|
||||
@@ -7535,6 +7587,14 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
|
||||
|
||||
|
||||
|
||||
const AppConnection_possibleTypes: string[] = ['AppConnection']
|
||||
export const isAppConnection = (obj?: { __typename?: any } | null): obj is AppConnection => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isAppConnection"')
|
||||
return AppConnection_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const ResendEmailVerificationToken_possibleTypes: string[] = ['ResendEmailVerificationToken']
|
||||
export const isResendEmailVerificationToken = (obj?: { __typename?: any } | null): obj is ResendEmailVerificationToken => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isResendEmailVerificationToken"')
|
||||
@@ -7991,6 +8051,14 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
|
||||
|
||||
|
||||
|
||||
const RunAgentResult_possibleTypes: string[] = ['RunAgentResult']
|
||||
export const isRunAgentResult = (obj?: { __typename?: any } | null): obj is RunAgentResult => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isRunAgentResult"')
|
||||
return RunAgentResult_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const ChannelSyncSuccess_possibleTypes: string[] = ['ChannelSyncSuccess']
|
||||
export const isChannelSyncSuccess = (obj?: { __typename?: any } | null): obj is ChannelSyncSuccess => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isChannelSyncSuccess"')
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,8 +9,8 @@ A **role** is a permission set: which objects an app can read or write, which fi
|
||||
```ts src/roles/restricted-company-role.ts
|
||||
import {
|
||||
defineRole,
|
||||
PermissionFlag,
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
|
||||
SystemPermissionFlag,
|
||||
} from 'twenty-sdk/define';
|
||||
|
||||
export default defineRole({
|
||||
@@ -46,7 +46,7 @@ export default defineRole({
|
||||
canUpdateFieldValue: false,
|
||||
},
|
||||
],
|
||||
permissionFlags: [PermissionFlag.APPLICATIONS],
|
||||
permissionFlagUniversalIdentifiers: [SystemPermissionFlag.APPLICATIONS],
|
||||
});
|
||||
```
|
||||
|
||||
@@ -55,7 +55,7 @@ export default defineRole({
|
||||
When you scaffold a new app, the CLI creates a default role file declared with `defineApplicationRole()`:
|
||||
|
||||
```ts src/roles/default-role.ts
|
||||
import { defineApplicationRole, PermissionFlag } from 'twenty-sdk/define';
|
||||
import { defineApplicationRole } from 'twenty-sdk/define';
|
||||
|
||||
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
|
||||
'b648f87b-1d26-4961-b974-0908fd991061';
|
||||
@@ -74,7 +74,7 @@ export default defineApplicationRole({
|
||||
canBeAssignedToApiKeys: false,
|
||||
objectPermissions: [],
|
||||
fieldPermissions: [],
|
||||
permissionFlags: [],
|
||||
permissionFlagUniversalIdentifiers: [],
|
||||
});
|
||||
```
|
||||
|
||||
@@ -90,5 +90,5 @@ Notes:
|
||||
|
||||
- Start from the scaffolded role, then progressively restrict it — the default grants broad read access, which is rarely what you want in production.
|
||||
- Replace `objectPermissions` and `fieldPermissions` with the exact objects and fields your functions actually need.
|
||||
- `permissionFlags` control access to platform-level capabilities. Keep them minimal.
|
||||
- `permissionFlagUniversalIdentifiers` control access to platform-level capabilities. Keep them minimal.
|
||||
- See a working example: [`hello-world/src/roles/function-role.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/hello-world/src/roles/function-role.ts).
|
||||
|
||||
@@ -64,6 +64,86 @@ Key points:
|
||||
- `description` (optional) provides context about what the agent does.
|
||||
- `icon` (optional) sets the icon displayed in the UI.
|
||||
- `modelId` (optional) overrides the default AI model used by the agent.
|
||||
- `responseFormat` (optional) controls the shape of the agent's output. Defaults to `{ type: 'text' }` for free-form text. Use `{ type: 'json', schema }` to force structured JSON output.
|
||||
|
||||
By default an agent returns free-form text. To get structured output, set `responseFormat` to `{ type: 'json' }` and provide a `schema`:
|
||||
|
||||
```ts src/agents/structured-agent.ts
|
||||
import { defineAgent } from 'twenty-sdk/define';
|
||||
|
||||
export default defineAgent({
|
||||
universalIdentifier: 'c4d5e6f7-a8b9-0123-cdef-456789012345',
|
||||
name: 'lead-scorer',
|
||||
label: 'Lead Scorer',
|
||||
prompt: 'Score the lead and explain your reasoning.',
|
||||
responseFormat: {
|
||||
type: 'json',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
score: { type: 'number', description: 'Lead score from 0 to 100' },
|
||||
summary: { type: 'string', description: 'Short reasoning for the score' },
|
||||
},
|
||||
required: ['score', 'summary'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Schema notes:
|
||||
- The schema is a flat object: each property's `type` must be a primitive (`string`, `number`, or `boolean`). Nested objects and arrays are not supported.
|
||||
- `description` (optional) on each property guides the model on what to put there.
|
||||
- `required` (optional) lists the properties the model must always return.
|
||||
- `additionalProperties: false` (optional) forbids any property not declared in `properties`.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="runAgent" description="Run an agent from a logic function">
|
||||
|
||||
`runAgent()` lets a logic function run one of your app's agents (with its
|
||||
skills and tools). Identify the agent by the `universalIdentifier` you passed
|
||||
to `defineAgent()`:
|
||||
|
||||
```ts src/logic-functions/run-enricher.ts
|
||||
import { runAgent } from 'twenty-sdk/logic-function';
|
||||
|
||||
const { result, error, success } = await runAgent({
|
||||
agentUniversalIdentifier: 'b3c4d5e6-f7a8-9012-bcde-f34567890123',
|
||||
prompt: 'Enrich House Ad <recordId>: fill empty fields from its listing URL.',
|
||||
});
|
||||
```
|
||||
|
||||
Key points:
|
||||
- The agent runs **synchronously** and can read/update records itself via its
|
||||
own tools — `runAgent()` resolves once the run completes.
|
||||
- An app can only run its own agents.
|
||||
- The app's [default role](/developers/extend/apps/config/roles) must grant the
|
||||
`AI` permission flag — add `SystemPermissionFlag.AI` to its
|
||||
`permissionFlagUniversalIdentifiers` (or set `canAccessAllTools: true`).
|
||||
Without it, `runAgent()` fails with a permission error.
|
||||
- Set a generous `timeoutSeconds` on the logic function — agent runs can take
|
||||
several seconds.
|
||||
- `success` is `true` and `result` is non-null when the run completes; on
|
||||
failure `success` is `false`, `result` is `null`, and `error` holds the
|
||||
reason (for example, when the workspace ran out of AI credits mid-run).
|
||||
|
||||
```ts src/roles/default-role.ts
|
||||
import { defineApplicationRole, SystemPermissionFlag } from 'twenty-sdk/define';
|
||||
|
||||
export default defineApplicationRole({
|
||||
universalIdentifier: 'b648f87b-1d26-4961-b974-0908fd991061',
|
||||
label: 'Default function role',
|
||||
// runAgent() requires the AI permission flag on the app's default role.
|
||||
permissionFlagUniversalIdentifiers: [SystemPermissionFlag.AI],
|
||||
});
|
||||
```
|
||||
|
||||
<Warning>
|
||||
**Avoid loops:** if you call `runAgent()` from a `*.updated` database-event
|
||||
trigger and the agent updates the same record, scope the trigger with
|
||||
`updatedFields` to a field the agent never writes (e.g. the source URL), or
|
||||
guard on whether any target field is still empty before calling `runAgent()`.
|
||||
</Warning>
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
@@ -256,6 +256,19 @@ export type ApiKeyToken = {
|
||||
token: Scalars['String'];
|
||||
};
|
||||
|
||||
export type AppConnection = {
|
||||
__typename?: 'AppConnection';
|
||||
accessToken: Scalars['String'];
|
||||
authFailedAt?: Maybe<Scalars['String']>;
|
||||
handle: Scalars['String'];
|
||||
id: Scalars['ID'];
|
||||
name: Scalars['String'];
|
||||
providerName: Scalars['String'];
|
||||
scopes: Array<Scalars['String']>;
|
||||
userWorkspaceId: Scalars['String'];
|
||||
visibility: Scalars['String'];
|
||||
};
|
||||
|
||||
export type AppToken = {
|
||||
__typename?: 'AppToken';
|
||||
createdAt: Scalars['DateTime'];
|
||||
@@ -2116,6 +2129,12 @@ export type LineChartSeries = {
|
||||
label: Scalars['String'];
|
||||
};
|
||||
|
||||
export type ListAppConnectionsInput = {
|
||||
providerName?: InputMaybe<Scalars['String']>;
|
||||
userWorkspaceId?: InputMaybe<Scalars['String']>;
|
||||
visibility?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type Location = {
|
||||
__typename?: 'Location';
|
||||
lat?: Maybe<Scalars['Float']>;
|
||||
@@ -2495,6 +2514,7 @@ export type Mutation = {
|
||||
resetPageLayoutWidgetToDefault: PageLayoutWidget;
|
||||
revokeApiKey?: Maybe<ApiKey>;
|
||||
rotateApplicationRegistrationClientSecret: RotateClientSecret;
|
||||
runAgent: RunAgentResult;
|
||||
runEvaluationInput: AgentTurn;
|
||||
runWorkspaceMigration: Scalars['Boolean'];
|
||||
saveImapSmtpCaldavAccount: ImapSmtpCaldavConnectionSuccess;
|
||||
@@ -3194,6 +3214,11 @@ export type MutationRotateApplicationRegistrationClientSecretArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationRunAgentArgs = {
|
||||
input: RunAgentInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationRunEvaluationInputArgs = {
|
||||
agentId: Scalars['UUID'];
|
||||
input: Scalars['String'];
|
||||
@@ -4114,6 +4139,8 @@ export type Query = {
|
||||
agentTurns: Array<AgentTurn>;
|
||||
apiKey?: Maybe<ApiKey>;
|
||||
apiKeys: Array<ApiKey>;
|
||||
appConnection: AppConnection;
|
||||
appConnections: Array<AppConnection>;
|
||||
applicationConnectionProviders: Array<ApplicationConnectionProvider>;
|
||||
applicationRegistrationTarballUrl?: Maybe<Scalars['String']>;
|
||||
barChartData: BarChartData;
|
||||
@@ -4223,6 +4250,16 @@ export type QueryApiKeyArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type QueryAppConnectionArgs = {
|
||||
id: Scalars['ID'];
|
||||
};
|
||||
|
||||
|
||||
export type QueryAppConnectionsArgs = {
|
||||
filter?: InputMaybe<ListAppConnectionsInput>;
|
||||
};
|
||||
|
||||
|
||||
export type QueryApplicationConnectionProvidersArgs = {
|
||||
applicationId: Scalars['UUID'];
|
||||
};
|
||||
@@ -4735,6 +4772,18 @@ export enum RowLevelPermissionPredicateOperand {
|
||||
VECTOR_SEARCH = 'VECTOR_SEARCH'
|
||||
}
|
||||
|
||||
export type RunAgentInput = {
|
||||
agentUniversalIdentifier: Scalars['String'];
|
||||
prompt: Scalars['String'];
|
||||
};
|
||||
|
||||
export type RunAgentResult = {
|
||||
__typename?: 'RunAgentResult';
|
||||
error?: Maybe<Scalars['String']>;
|
||||
result?: Maybe<Scalars['JSON']>;
|
||||
success: Scalars['Boolean'];
|
||||
};
|
||||
|
||||
export type SsoConnection = {
|
||||
__typename?: 'SSOConnection';
|
||||
id: Scalars['UUID'];
|
||||
|
||||
+5
@@ -9,6 +9,7 @@ import {
|
||||
IconKey,
|
||||
IconSettings,
|
||||
IconSettingsAutomation,
|
||||
IconSparkles,
|
||||
IconTool,
|
||||
IconUsers,
|
||||
} from 'twenty-ui/display';
|
||||
@@ -114,6 +115,10 @@ export const buildPermissionSummaryFromRoleManifest = (
|
||||
label: 'Manage API keys and webhooks',
|
||||
Icon: IconCode,
|
||||
},
|
||||
[SystemPermissionFlag.AI]: {
|
||||
label: 'Run AI agents',
|
||||
Icon: IconSparkles,
|
||||
},
|
||||
};
|
||||
|
||||
for (const flag of otherFlags) {
|
||||
|
||||
@@ -21,6 +21,7 @@ export default defineAgent({
|
||||
label: '${name}',
|
||||
description: 'Add a description for your agent',
|
||||
prompt: 'Add the agent system prompt here',
|
||||
responseFormat: {type: 'text'},
|
||||
});
|
||||
`;
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ import { type AgentManifest } from 'twenty-shared/application';
|
||||
|
||||
export const defineAgent: DefineEntity<AgentManifest> = (config) => {
|
||||
const errors: string[] = [];
|
||||
const warnings: string[] = [];
|
||||
|
||||
if (!config.universalIdentifier) {
|
||||
errors.push('Agent must have a universalIdentifier');
|
||||
@@ -21,5 +22,11 @@ export const defineAgent: DefineEntity<AgentManifest> = (config) => {
|
||||
errors.push('Agent must have a prompt');
|
||||
}
|
||||
|
||||
return createValidationResult({ config, errors });
|
||||
if (!config.responseFormat) {
|
||||
warnings.push(
|
||||
`Agent '${config.name}' has no responseFormat, it will default to { type: 'text' }. Set it explicitly to control the agent's output shape.`,
|
||||
);
|
||||
}
|
||||
|
||||
return createValidationResult({ config, errors, warnings });
|
||||
};
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import {
|
||||
afterEach,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
vi,
|
||||
type MockInstance,
|
||||
} from 'vitest';
|
||||
|
||||
import { runAgent } from '@/sdk/logic-function/agents/run-agent';
|
||||
|
||||
describe('runAgent', () => {
|
||||
let fetchSpy: MockInstance<typeof fetch>;
|
||||
|
||||
beforeEach(() => {
|
||||
process.env.TWENTY_API_URL = 'https://api.test';
|
||||
process.env.TWENTY_APP_ACCESS_TOKEN = 'app-token';
|
||||
fetchSpy = vi.spyOn(globalThis, 'fetch');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
delete process.env.TWENTY_API_URL;
|
||||
delete process.env.TWENTY_APP_ACCESS_TOKEN;
|
||||
fetchSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('POSTs the runAgent mutation to /metadata and returns the result', async () => {
|
||||
const payload = {
|
||||
result: { response: 'done' },
|
||||
error: null,
|
||||
success: true,
|
||||
};
|
||||
|
||||
fetchSpy.mockResolvedValue(
|
||||
new Response(JSON.stringify({ data: { runAgent: payload } }), {
|
||||
status: 200,
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await runAgent({
|
||||
agentUniversalIdentifier: 'agent-uid',
|
||||
prompt: 'Enrich record 123',
|
||||
});
|
||||
|
||||
expect(result).toEqual(payload);
|
||||
|
||||
const [url, requestInit] = fetchSpy.mock.calls[0];
|
||||
|
||||
expect(url).toBe('https://api.test/metadata');
|
||||
expect(requestInit).toEqual(
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
headers: expect.objectContaining({
|
||||
Authorization: 'Bearer app-token',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
const sentBody = JSON.parse(requestInit?.body as string);
|
||||
|
||||
expect(sentBody.query).toContain('runAgent(input: $input)');
|
||||
expect(sentBody.variables).toEqual({
|
||||
input: {
|
||||
agentUniversalIdentifier: 'agent-uid',
|
||||
prompt: 'Enrich record 123',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('surfaces GraphQL errors as a regular Error', async () => {
|
||||
fetchSpy.mockResolvedValue(
|
||||
new Response(
|
||||
JSON.stringify({ errors: [{ message: 'Agent not found' }] }),
|
||||
{ status: 200 },
|
||||
),
|
||||
);
|
||||
|
||||
await expect(
|
||||
runAgent({ agentUniversalIdentifier: 'a', prompt: 'p' }),
|
||||
).rejects.toThrow(/Agent not found/);
|
||||
});
|
||||
|
||||
it('surfaces non-2xx HTTP responses as a regular Error', async () => {
|
||||
fetchSpy.mockResolvedValue(
|
||||
new Response('boom', { status: 500, statusText: 'Server Error' }),
|
||||
);
|
||||
|
||||
await expect(
|
||||
runAgent({ agentUniversalIdentifier: 'a', prompt: 'p' }),
|
||||
).rejects.toThrow(/HTTP 500/);
|
||||
});
|
||||
|
||||
it('throws when the runtime env vars are missing', async () => {
|
||||
delete process.env.TWENTY_API_URL;
|
||||
|
||||
await expect(
|
||||
runAgent({ agentUniversalIdentifier: 'a', prompt: 'p' }),
|
||||
).rejects.toThrow(/requires the app runtime env vars/);
|
||||
|
||||
expect(fetchSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
import {
|
||||
type RunAgentInput,
|
||||
type RunAgentResult,
|
||||
} from 'twenty-shared/application';
|
||||
|
||||
import { postGraphqlRequest } from '@/sdk/logic-function/utils/post-graphql-request.util';
|
||||
|
||||
const RUN_AGENT_MUTATION = `
|
||||
mutation RunAgent($input: RunAgentInput!) {
|
||||
runAgent(input: $input) {
|
||||
result
|
||||
error
|
||||
success
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const runAgent = async (
|
||||
input: RunAgentInput,
|
||||
): Promise<RunAgentResult> => {
|
||||
const { runAgent: result } = await postGraphqlRequest<
|
||||
{ input: RunAgentInput },
|
||||
{ runAgent: RunAgentResult }
|
||||
>({
|
||||
query: RUN_AGENT_MUTATION,
|
||||
variables: { input },
|
||||
caller: 'runAgent',
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
+22
-11
@@ -42,37 +42,48 @@ describe('getConnection', () => {
|
||||
fetchSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('POSTs the id to /apps/connections/get and returns the response', async () => {
|
||||
it('queries appConnection by id over GraphQL and returns the response', async () => {
|
||||
const connection = buildConnection({ id: 'persisted' });
|
||||
|
||||
fetchSpy.mockResolvedValue(
|
||||
new Response(JSON.stringify(connection), { status: 200 }),
|
||||
new Response(JSON.stringify({ data: { appConnection: connection } }), {
|
||||
status: 200,
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await getConnection('persisted');
|
||||
|
||||
expect(result).toEqual(connection);
|
||||
expect(fetchSpy).toHaveBeenCalledWith(
|
||||
'https://api.test/apps/connections/get',
|
||||
|
||||
const [url, requestInit] = fetchSpy.mock.calls[0];
|
||||
|
||||
expect(url).toBe('https://api.test/metadata');
|
||||
expect(requestInit).toEqual(
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ id: 'persisted' }),
|
||||
headers: expect.objectContaining({
|
||||
Authorization: 'Bearer app-token',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
const sentBody = JSON.parse(requestInit?.body as string);
|
||||
|
||||
expect(sentBody.query).toContain('appConnection(id: $id)');
|
||||
expect(sentBody.variables).toEqual({ id: 'persisted' });
|
||||
});
|
||||
|
||||
it('throws AppConnectionAuthFailedError when the connection needs reconnect', async () => {
|
||||
fetchSpy.mockResolvedValue(
|
||||
new Response(
|
||||
JSON.stringify(
|
||||
buildConnection({
|
||||
id: 'broken',
|
||||
authFailedAt: '2024-01-02T00:00:00.000Z',
|
||||
}),
|
||||
),
|
||||
JSON.stringify({
|
||||
data: {
|
||||
appConnection: buildConnection({
|
||||
id: 'broken',
|
||||
authFailedAt: '2024-01-02T00:00:00.000Z',
|
||||
}),
|
||||
},
|
||||
}),
|
||||
{ status: 200 },
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,20 +1,9 @@
|
||||
import { type AppConnection } from '@/sdk/logic-function/connections/types/app-connection.type';
|
||||
|
||||
// Resolution rule for an HTTP-route handler that wants "the request user's
|
||||
// connection, or fall back to a workspace-shared one." Pure function — no
|
||||
// network. Pass it the result of `listConnections` and the trigger event.
|
||||
//
|
||||
// The plan-of-record for picking a connection is documented in the v3 plan
|
||||
// notes; this utility encodes the most common case so handlers don't have
|
||||
// to repeat the same `find(...) ?? find(...)` chain.
|
||||
//
|
||||
// Returns `null` when no candidate exists — caller decides whether that's
|
||||
// a 4xx for the end user or a hard error.
|
||||
export const findConnectionForRequest = (
|
||||
connections: AppConnection[],
|
||||
event: { userWorkspaceId: string | null },
|
||||
): AppConnection | null => {
|
||||
// 1. Personal credential of the request user (highest specificity).
|
||||
if (event.userWorkspaceId !== null) {
|
||||
const personal = connections.find(
|
||||
(connection) =>
|
||||
@@ -27,7 +16,6 @@ export const findConnectionForRequest = (
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Any workspace-shared credential (team-managed service account).
|
||||
const workspaceShared = connections.find(
|
||||
(connection) => connection.visibility === 'workspace',
|
||||
);
|
||||
|
||||
@@ -1,25 +1,36 @@
|
||||
import { AppConnectionAuthFailedError } from '@/sdk/logic-function/connections/errors/app-connection-auth-failed.error';
|
||||
import { type AppConnection } from '@/sdk/logic-function/connections/types/app-connection.type';
|
||||
import { postConnectionsEndpoint } from '@/sdk/logic-function/connections/utils/post-connections-endpoint.util';
|
||||
import { postGraphqlRequest } from '@/sdk/logic-function/utils/post-graphql-request.util';
|
||||
|
||||
const GET_APP_CONNECTION_QUERY = `
|
||||
query GetAppConnection($id: ID!) {
|
||||
appConnection(id: $id) {
|
||||
id
|
||||
providerName
|
||||
name
|
||||
handle
|
||||
visibility
|
||||
userWorkspaceId
|
||||
accessToken
|
||||
scopes
|
||||
authFailedAt
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// Look up a single connection by id. The id is stable across reconnects
|
||||
// (the row keeps its id when the user clicks "Reconnect"), so apps can
|
||||
// safely persist it in their own data and call this helper on each
|
||||
// invocation to retrieve a fresh access token.
|
||||
//
|
||||
// Throws `AppConnectionAuthFailedError` if the credential is in a
|
||||
// permanent-failure state (the user must reconnect from the app's
|
||||
// settings tab). Throws a regular `Error` for any other failure
|
||||
// (network, not-found, transient refresh failure).
|
||||
export const getConnection = async (id: string): Promise<AppConnection> => {
|
||||
const connection = await postConnectionsEndpoint<
|
||||
const { appConnection } = await postGraphqlRequest<
|
||||
{ id: string },
|
||||
AppConnection
|
||||
>('get', { id });
|
||||
{ appConnection: AppConnection }
|
||||
>({
|
||||
query: GET_APP_CONNECTION_QUERY,
|
||||
variables: { id },
|
||||
caller: 'getConnection',
|
||||
});
|
||||
|
||||
if (connection.authFailedAt !== null) {
|
||||
throw new AppConnectionAuthFailedError(connection.id);
|
||||
if (appConnection.authFailedAt !== null) {
|
||||
throw new AppConnectionAuthFailedError(appConnection.id);
|
||||
}
|
||||
|
||||
return connection;
|
||||
return appConnection;
|
||||
};
|
||||
|
||||
@@ -1,26 +1,39 @@
|
||||
import { type AppConnection } from '@/sdk/logic-function/connections/types/app-connection.type';
|
||||
import { postConnectionsEndpoint } from '@/sdk/logic-function/connections/utils/post-connections-endpoint.util';
|
||||
import { postGraphqlRequest } from '@/sdk/logic-function/utils/post-graphql-request.util';
|
||||
|
||||
const LIST_APP_CONNECTIONS_QUERY = `
|
||||
query ListAppConnections($filter: ListAppConnectionsInput) {
|
||||
appConnections(filter: $filter) {
|
||||
id
|
||||
providerName
|
||||
name
|
||||
handle
|
||||
visibility
|
||||
userWorkspaceId
|
||||
accessToken
|
||||
scopes
|
||||
authFailedAt
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export type ListConnectionsFilter = {
|
||||
// Provider name as declared on `defineConnectionProvider({ name })`.
|
||||
providerName?: string;
|
||||
// Restrict to credentials owned by a specific user. Useful in cron
|
||||
// triggers when picking a service-account user via app config.
|
||||
userWorkspaceId?: string;
|
||||
// Restrict by row visibility — 'user' (private) or 'workspace' (shared).
|
||||
visibility?: 'user' | 'workspace';
|
||||
};
|
||||
|
||||
// Returns every connection owned by the running app, optionally filtered.
|
||||
// The server refreshes each access token on read, so the returned values
|
||||
// are usable immediately. When the running execution carries a user
|
||||
// context (HTTP-route trigger with `isAuthRequired`, tool calls, etc.),
|
||||
// `scope: 'user'` connections belonging to other users are filtered out
|
||||
// server-side. Cron and database-event triggers see all connections.
|
||||
export const listConnections = async (
|
||||
filter: ListConnectionsFilter = {},
|
||||
): Promise<AppConnection[]> =>
|
||||
postConnectionsEndpoint<ListConnectionsFilter, AppConnection[]>(
|
||||
'list',
|
||||
filter,
|
||||
);
|
||||
): Promise<AppConnection[]> => {
|
||||
const { appConnections } = await postGraphqlRequest<
|
||||
{ filter: ListConnectionsFilter },
|
||||
{ appConnections: AppConnection[] }
|
||||
>({
|
||||
query: LIST_APP_CONNECTIONS_QUERY,
|
||||
variables: { filter },
|
||||
caller: 'listConnections',
|
||||
});
|
||||
|
||||
return appConnections;
|
||||
};
|
||||
|
||||
-40
@@ -1,40 +0,0 @@
|
||||
import {
|
||||
DEFAULT_API_URL_NAME,
|
||||
DEFAULT_APP_ACCESS_TOKEN_NAME,
|
||||
} from 'twenty-shared/application';
|
||||
|
||||
// Shared transport for `/apps/connections/*` endpoints. Centralises the
|
||||
// env-var check, auth header, and HTTP error translation so each helper
|
||||
// stays focused on its own input/output shape.
|
||||
export const postConnectionsEndpoint = async <TBody, TResponse>(
|
||||
path: 'list' | 'get',
|
||||
body: TBody,
|
||||
): Promise<TResponse> => {
|
||||
const apiUrl = process.env[DEFAULT_API_URL_NAME];
|
||||
const accessToken = process.env[DEFAULT_APP_ACCESS_TOKEN_NAME];
|
||||
|
||||
if (!apiUrl || !accessToken) {
|
||||
throw new Error(
|
||||
`${path === 'list' ? 'listConnections' : 'getConnection'}() requires the app runtime env vars ` +
|
||||
`${DEFAULT_API_URL_NAME} and ${DEFAULT_APP_ACCESS_TOKEN_NAME}.`,
|
||||
);
|
||||
}
|
||||
|
||||
const response = await fetch(`${apiUrl}/apps/connections/${path}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`${path === 'list' ? 'listConnections' : 'getConnection'}() failed: ` +
|
||||
`HTTP ${response.status} ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
return (await response.json()) as TResponse;
|
||||
};
|
||||
@@ -44,5 +44,9 @@ export type { ListConnectionsFilter } from '@/sdk/logic-function/connections/lis
|
||||
export { findConnectionForRequest } from '@/sdk/logic-function/connections/find-connection-for-request';
|
||||
export { AppConnectionAuthFailedError } from '@/sdk/logic-function/connections/errors/app-connection-auth-failed.error';
|
||||
export type { AppConnection } from '@/sdk/logic-function/connections/types/app-connection.type';
|
||||
|
||||
export { runAgent } from '@/sdk/logic-function/agents/run-agent';
|
||||
export type { RunAgentInput, RunAgentResult } from 'twenty-shared/application';
|
||||
|
||||
export { Response } from '@/sdk/logic-function/response';
|
||||
export type { ResponseInit } from '@/sdk/logic-function/response';
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import {
|
||||
DEFAULT_API_URL_NAME,
|
||||
DEFAULT_APP_ACCESS_TOKEN_NAME,
|
||||
} from 'twenty-shared/application';
|
||||
|
||||
export const postGraphqlRequest = async <TVariables, TData>({
|
||||
query,
|
||||
variables,
|
||||
caller,
|
||||
}: {
|
||||
query: string;
|
||||
variables: TVariables;
|
||||
caller: string;
|
||||
}): Promise<TData> => {
|
||||
const apiUrl = process.env[DEFAULT_API_URL_NAME];
|
||||
const accessToken = process.env[DEFAULT_APP_ACCESS_TOKEN_NAME];
|
||||
|
||||
if (!apiUrl || !accessToken) {
|
||||
throw new Error(
|
||||
`${caller}() requires the app runtime env vars ` +
|
||||
`${DEFAULT_API_URL_NAME} and ${DEFAULT_APP_ACCESS_TOKEN_NAME}.`,
|
||||
);
|
||||
}
|
||||
|
||||
const response = await fetch(`${apiUrl}/metadata`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
body: JSON.stringify({ query, variables }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`${caller}() failed: HTTP ${response.status} ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
const body = (await response.json()) as {
|
||||
data?: TData;
|
||||
errors?: { message: string }[];
|
||||
};
|
||||
|
||||
if (body.errors && body.errors.length > 0) {
|
||||
throw new Error(
|
||||
`${caller}() failed: ${body.errors.map((error) => error.message).join(', ')}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!body.data) {
|
||||
throw new Error(`${caller}() failed: response contained no data.`);
|
||||
}
|
||||
|
||||
return body.data;
|
||||
};
|
||||
+4
-3
@@ -22,9 +22,10 @@ import { JwtAuthGuard } from 'src/engine/guards/jwt-auth.guard';
|
||||
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
|
||||
// On-demand connection lookup for app logic functions. Authenticated via the
|
||||
// application access token (already injected into the function runtime as
|
||||
// TWENTY_APP_ACCESS_TOKEN). Apps can only list their own connections.
|
||||
/** @deprecated Superseded by the `appConnections` / `appConnection` GraphQL
|
||||
* queries on the metadata schema (ApplicationConnectionsResolver). The SDK
|
||||
* helpers (`listConnections`, `getConnection`) now call GraphQL. Kept for
|
||||
* backward compatibility with already-deployed app runtimes. */
|
||||
@Controller('apps/connections')
|
||||
@UseGuards(JwtAuthGuard, WorkspaceAuthGuard, NoPermissionGuard)
|
||||
@UsePipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true }))
|
||||
|
||||
+5
-1
@@ -3,6 +3,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { ConnectionProviderEntity } from 'src/engine/core-modules/application/connection-provider/connection-provider.entity';
|
||||
import { ApplicationConnectionsController } from 'src/engine/core-modules/application/connection-provider/connections/application-connections.controller';
|
||||
import { ApplicationConnectionsResolver } from 'src/engine/core-modules/application/connection-provider/connections/application-connections.resolver';
|
||||
import { ApplicationConnectionsListService } from 'src/engine/core-modules/application/connection-provider/connections/services/application-connections-list.service';
|
||||
import { TokenModule } from 'src/engine/core-modules/auth/token/token.module';
|
||||
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
|
||||
@@ -25,7 +26,10 @@ import { RefreshTokensManagerModule } from 'src/modules/connected-account/refres
|
||||
RefreshTokensManagerModule,
|
||||
ConnectedAccountTokenEncryptionModule,
|
||||
],
|
||||
providers: [ApplicationConnectionsListService],
|
||||
providers: [
|
||||
ApplicationConnectionsListService,
|
||||
ApplicationConnectionsResolver,
|
||||
],
|
||||
controllers: [ApplicationConnectionsController],
|
||||
exports: [ApplicationConnectionsListService],
|
||||
})
|
||||
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
import { UseGuards } from '@nestjs/common';
|
||||
import { Args, ID, Query } from '@nestjs/graphql';
|
||||
|
||||
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
|
||||
import { AppConnectionObjectDto } from 'src/engine/core-modules/application/connection-provider/connections/dtos/app-connection.object';
|
||||
import { ListAppConnectionsInput } from 'src/engine/core-modules/application/connection-provider/connections/dtos/list-app-connections.input';
|
||||
import { ApplicationConnectionsListService } from 'src/engine/core-modules/application/connection-provider/connections/services/application-connections-list.service';
|
||||
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
|
||||
import { type FlatWorkspace } from 'src/engine/core-modules/workspace/types/flat-workspace.type';
|
||||
import { AuthApplication } from 'src/engine/decorators/auth/auth-application.decorator';
|
||||
import { AuthUserWorkspaceId } from 'src/engine/decorators/auth/auth-user-workspace-id.decorator';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
|
||||
@UseGuards(WorkspaceAuthGuard, NoPermissionGuard)
|
||||
@MetadataResolver()
|
||||
export class ApplicationConnectionsResolver {
|
||||
constructor(
|
||||
private readonly listService: ApplicationConnectionsListService,
|
||||
) {}
|
||||
|
||||
@Query(() => [AppConnectionObjectDto])
|
||||
async appConnections(
|
||||
@AuthApplication() application: FlatApplication,
|
||||
@AuthWorkspace() workspace: FlatWorkspace,
|
||||
@AuthUserWorkspaceId({ allowUndefined: true })
|
||||
userWorkspaceId: string | undefined,
|
||||
@Args('filter', { nullable: true }) filter?: ListAppConnectionsInput,
|
||||
): Promise<AppConnectionObjectDto[]> {
|
||||
return this.listService.list({
|
||||
applicationId: application.id,
|
||||
workspaceId: workspace.id,
|
||||
requestUserWorkspaceId: userWorkspaceId ?? null,
|
||||
filter: filter ?? {},
|
||||
});
|
||||
}
|
||||
|
||||
@Query(() => AppConnectionObjectDto)
|
||||
async appConnection(
|
||||
@AuthApplication() application: FlatApplication,
|
||||
@AuthWorkspace() workspace: FlatWorkspace,
|
||||
@AuthUserWorkspaceId({ allowUndefined: true })
|
||||
userWorkspaceId: string | undefined,
|
||||
@Args('id', { type: () => ID }) id: string,
|
||||
): Promise<AppConnectionObjectDto> {
|
||||
return this.listService.getOne({
|
||||
applicationId: application.id,
|
||||
workspaceId: workspace.id,
|
||||
requestUserWorkspaceId: userWorkspaceId ?? null,
|
||||
id,
|
||||
});
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
import { Field, ID, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { type AppConnection } from 'twenty-shared/application';
|
||||
|
||||
@ObjectType('AppConnection')
|
||||
export class AppConnectionObjectDto implements AppConnection {
|
||||
@Field(() => ID)
|
||||
id: string;
|
||||
|
||||
@Field()
|
||||
providerName: string;
|
||||
|
||||
@Field()
|
||||
name: string;
|
||||
|
||||
@Field()
|
||||
handle: string;
|
||||
|
||||
@Field(() => String)
|
||||
visibility: 'user' | 'workspace';
|
||||
|
||||
@Field()
|
||||
userWorkspaceId: string;
|
||||
|
||||
@Field()
|
||||
accessToken: string;
|
||||
|
||||
@Field(() => [String])
|
||||
scopes: string[];
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
authFailedAt: string | null;
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
|
||||
import { IsIn, IsOptional, IsString, IsUUID } from 'class-validator';
|
||||
|
||||
@InputType('ListAppConnectionsInput')
|
||||
export class ListAppConnectionsInput {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
providerName?: string;
|
||||
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
userWorkspaceId?: string;
|
||||
|
||||
@IsIn(['user', 'workspace'])
|
||||
@IsOptional()
|
||||
@Field(() => String, { nullable: true })
|
||||
visibility?: 'user' | 'workspace';
|
||||
}
|
||||
+1
-1
@@ -22,7 +22,7 @@ export const fromAgentManifestToUniversalFlatAgent = ({
|
||||
description: agentManifest.description ?? null,
|
||||
prompt: agentManifest.prompt,
|
||||
modelId: (agentManifest.modelId as ModelId) ?? AUTO_SELECT_SMART_MODEL_ID,
|
||||
responseFormat: { type: 'text' },
|
||||
responseFormat: agentManifest.responseFormat ?? { type: 'text' },
|
||||
modelConfiguration: null,
|
||||
evaluationInputs: [],
|
||||
isCustom: false,
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import {
|
||||
type ExecutionContext,
|
||||
ForbiddenException,
|
||||
createParamDecorator,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import { getRequest } from 'src/utils/extract-request';
|
||||
|
||||
interface DecoratorOptions {
|
||||
allowUndefined?: boolean;
|
||||
}
|
||||
|
||||
export const AuthApplication = createParamDecorator(
|
||||
(options: DecoratorOptions | undefined, ctx: ExecutionContext) => {
|
||||
const request = getRequest(ctx);
|
||||
|
||||
if (!options?.allowUndefined && !request.application) {
|
||||
throw new ForbiddenException(
|
||||
'This endpoint requires an APPLICATION_ACCESS token.',
|
||||
);
|
||||
}
|
||||
|
||||
return request.application;
|
||||
},
|
||||
);
|
||||
+7
@@ -1,6 +1,7 @@
|
||||
import { forwardRef, Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
|
||||
import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
|
||||
import { WorkspaceDomainsModule } from 'src/engine/core-modules/domain/workspace-domains/workspace-domains.module';
|
||||
import { FileUrlModule } from 'src/engine/core-modules/file/file-url/file-url.module';
|
||||
@@ -21,14 +22,17 @@ import { AgentMessagePartEntity } from './entities/agent-message-part.entity';
|
||||
import { AgentMessageEntity } from './entities/agent-message.entity';
|
||||
import { AgentTurnEntity } from './entities/agent-turn.entity';
|
||||
import { AgentMessagePartResolver } from './resolvers/agent-message-part.resolver';
|
||||
import { AgentRunResolver } from './resolvers/agent-run.resolver';
|
||||
import { AgentActorContextService } from './services/agent-actor-context.service';
|
||||
import { AgentAsyncExecutorService } from './services/agent-async-executor.service';
|
||||
import { AgentRunService } from './services/agent-run.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
AiBillingModule,
|
||||
AiModelsModule,
|
||||
AiAgentModule,
|
||||
ApplicationModule,
|
||||
BillingModule,
|
||||
FileUrlModule,
|
||||
WorkspaceDomainsModule,
|
||||
@@ -50,7 +54,10 @@ import { AgentAsyncExecutorService } from './services/agent-async-executor.servi
|
||||
AgentAsyncExecutorService,
|
||||
AgentActorContextService,
|
||||
AgentMessagePartResolver,
|
||||
AgentRunResolver,
|
||||
AgentRunService,
|
||||
provideWorkspaceScopedRepository(RoleTargetEntity),
|
||||
provideWorkspaceScopedRepository(AgentEntity),
|
||||
],
|
||||
exports: [
|
||||
AgentAsyncExecutorService,
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import GraphQLJSON from 'graphql-type-json';
|
||||
import { type RunAgentResult } from 'twenty-shared/application';
|
||||
|
||||
@ObjectType('RunAgentResult')
|
||||
export class RunAgentResultDTO implements RunAgentResult {
|
||||
@Field(() => GraphQLJSON, { nullable: true })
|
||||
result: object | null;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
error: string | null;
|
||||
|
||||
@Field()
|
||||
success: boolean;
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
|
||||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
import { type RunAgentInput } from 'twenty-shared/application';
|
||||
|
||||
@InputType('RunAgentInput')
|
||||
export class RunAgentInputDTO implements RunAgentInput {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
agentUniversalIdentifier: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
prompt: string;
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import { UseGuards } from '@nestjs/common';
|
||||
import { Args, Mutation } from '@nestjs/graphql';
|
||||
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
|
||||
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
|
||||
import { type FlatWorkspace } from 'src/engine/core-modules/workspace/types/flat-workspace.type';
|
||||
import { AuthUserWorkspaceId } from 'src/engine/decorators/auth/auth-user-workspace-id.decorator';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { RunAgentInputDTO } from 'src/engine/metadata-modules/ai/ai-agent-execution/dtos/run-agent.input';
|
||||
import { RunAgentResultDTO } from 'src/engine/metadata-modules/ai/ai-agent-execution/dtos/run-agent-result.dto';
|
||||
import { AgentRunService } from 'src/engine/metadata-modules/ai/ai-agent-execution/services/agent-run.service';
|
||||
|
||||
@UseGuards(WorkspaceAuthGuard, SettingsPermissionGuard(PermissionFlagType.AI))
|
||||
@MetadataResolver()
|
||||
export class AgentRunResolver {
|
||||
constructor(private readonly agentRunService: AgentRunService) {}
|
||||
|
||||
@Mutation(() => RunAgentResultDTO)
|
||||
async runAgent(
|
||||
@Args('input') input: RunAgentInputDTO,
|
||||
@AuthWorkspace() workspace: FlatWorkspace,
|
||||
@AuthUserWorkspaceId({ allowUndefined: true })
|
||||
userWorkspaceId: string | undefined,
|
||||
): Promise<RunAgentResultDTO> {
|
||||
return this.agentRunService.run({
|
||||
workspace,
|
||||
requestUserWorkspaceId: userWorkspaceId ?? null,
|
||||
input,
|
||||
});
|
||||
}
|
||||
}
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { type FlatWorkspace } from 'src/engine/core-modules/workspace/types/flat-workspace.type';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { AgentAsyncExecutorService } from 'src/engine/metadata-modules/ai/ai-agent-execution/services/agent-async-executor.service';
|
||||
import { AgentRunService } from 'src/engine/metadata-modules/ai/ai-agent-execution/services/agent-run.service';
|
||||
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
|
||||
import { getWorkspaceScopedRepositoryToken } from 'src/engine/twenty-orm/workspace-scoped-repository/get-workspace-scoped-repository-token.util';
|
||||
|
||||
describe('AgentRunService', () => {
|
||||
let service: AgentRunService;
|
||||
let agentRepository: { findOne: jest.Mock };
|
||||
let applicationService: { findById: jest.Mock };
|
||||
let agentAsyncExecutorService: { executeAgent: jest.Mock };
|
||||
|
||||
const workspace = { id: 'workspace-1' } as FlatWorkspace;
|
||||
|
||||
const agent = { id: 'agent-1', applicationId: 'app-1' } as AgentEntity;
|
||||
|
||||
const input = {
|
||||
agentUniversalIdentifier: 'agent-uid',
|
||||
prompt: 'Enrich record 123',
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
agentRepository = { findOne: jest.fn().mockResolvedValue(agent) };
|
||||
applicationService = {
|
||||
findById: jest.fn().mockResolvedValue({ id: 'app-1' }),
|
||||
};
|
||||
agentAsyncExecutorService = {
|
||||
executeAgent: jest.fn().mockResolvedValue({
|
||||
result: { response: 'done' },
|
||||
hasNoMoreAvailableCredits: false,
|
||||
}),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
AgentRunService,
|
||||
{
|
||||
provide: AgentAsyncExecutorService,
|
||||
useValue: agentAsyncExecutorService,
|
||||
},
|
||||
{
|
||||
provide: ApplicationService,
|
||||
useValue: applicationService,
|
||||
},
|
||||
{
|
||||
provide: getWorkspaceScopedRepositoryToken(AgentEntity),
|
||||
useValue: agentRepository,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get(AgentRunService);
|
||||
});
|
||||
|
||||
it('runs the agent found by its universal identifier and returns a success result', async () => {
|
||||
const result = await service.run({
|
||||
workspace,
|
||||
requestUserWorkspaceId: 'user-workspace-1',
|
||||
input,
|
||||
});
|
||||
|
||||
expect(agentRepository.findOne).toHaveBeenCalledWith(workspace.id, {
|
||||
where: {
|
||||
universalIdentifier: input.agentUniversalIdentifier,
|
||||
},
|
||||
});
|
||||
expect(applicationService.findById).toHaveBeenCalledWith(
|
||||
agent.applicationId,
|
||||
);
|
||||
expect(result).toEqual({
|
||||
result: { response: 'done' },
|
||||
error: null,
|
||||
success: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('builds the application auth context from the agent application', async () => {
|
||||
await service.run({
|
||||
workspace,
|
||||
requestUserWorkspaceId: 'user-workspace-1',
|
||||
input,
|
||||
});
|
||||
|
||||
expect(agentAsyncExecutorService.executeAgent).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
authContext: {
|
||||
type: 'application',
|
||||
workspace,
|
||||
application: { id: 'app-1' },
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('returns an error result when the workspace ran out of credits', async () => {
|
||||
agentAsyncExecutorService.executeAgent.mockResolvedValue({
|
||||
result: { response: 'partial' },
|
||||
hasNoMoreAvailableCredits: true,
|
||||
});
|
||||
|
||||
const result = await service.run({
|
||||
workspace,
|
||||
requestUserWorkspaceId: 'user-workspace-1',
|
||||
input,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
result: null,
|
||||
error: 'AI agent stopped: no more available credits.',
|
||||
success: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('throws when no agent matches the identifier', async () => {
|
||||
agentRepository.findOne.mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
service.run({
|
||||
workspace,
|
||||
requestUserWorkspaceId: null,
|
||||
input,
|
||||
}),
|
||||
).rejects.toThrow(/not found/);
|
||||
|
||||
expect(applicationService.findById).not.toHaveBeenCalled();
|
||||
expect(agentAsyncExecutorService.executeAgent).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("throws when the agent's application cannot be found", async () => {
|
||||
applicationService.findById.mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
service.run({
|
||||
workspace,
|
||||
requestUserWorkspaceId: null,
|
||||
input,
|
||||
}),
|
||||
).rejects.toThrow(/not found/);
|
||||
|
||||
expect(agentAsyncExecutorService.executeAgent).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
type RunAgentInput,
|
||||
type RunAgentResult,
|
||||
} from 'twenty-shared/application';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { type WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
|
||||
import { UsageOperationType } from 'src/engine/core-modules/usage/enums/usage-operation-type.enum';
|
||||
import { type FlatWorkspace } from 'src/engine/core-modules/workspace/types/flat-workspace.type';
|
||||
import { AgentAsyncExecutorService } from 'src/engine/metadata-modules/ai/ai-agent-execution/services/agent-async-executor.service';
|
||||
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
|
||||
import { InjectWorkspaceScopedRepository } from 'src/engine/twenty-orm/workspace-scoped-repository/inject-workspace-scoped-repository.decorator';
|
||||
import { WorkspaceScopedRepository } from 'src/engine/twenty-orm/workspace-scoped-repository/workspace-scoped-repository';
|
||||
|
||||
@Injectable()
|
||||
export class AgentRunService {
|
||||
constructor(
|
||||
private readonly agentAsyncExecutorService: AgentAsyncExecutorService,
|
||||
private readonly applicationService: ApplicationService,
|
||||
@InjectWorkspaceScopedRepository(AgentEntity)
|
||||
private readonly agentRepository: WorkspaceScopedRepository<AgentEntity>,
|
||||
) {}
|
||||
|
||||
async run({
|
||||
workspace,
|
||||
requestUserWorkspaceId,
|
||||
input,
|
||||
}: {
|
||||
workspace: FlatWorkspace;
|
||||
requestUserWorkspaceId: string | null;
|
||||
input: RunAgentInput;
|
||||
}): Promise<RunAgentResult> {
|
||||
const agent = await this.agentRepository.findOne(workspace.id, {
|
||||
where: {
|
||||
universalIdentifier: input.agentUniversalIdentifier,
|
||||
},
|
||||
});
|
||||
|
||||
if (!agent) {
|
||||
throw new NotFoundException(
|
||||
`Agent ${input.agentUniversalIdentifier} not found`,
|
||||
);
|
||||
}
|
||||
|
||||
const application = await this.applicationService.findById(
|
||||
agent.applicationId,
|
||||
);
|
||||
|
||||
if (!application) {
|
||||
throw new NotFoundException(
|
||||
`Application ${agent.applicationId} not found for agent ${input.agentUniversalIdentifier}`,
|
||||
);
|
||||
}
|
||||
|
||||
const authContext: WorkspaceAuthContext = {
|
||||
type: 'application',
|
||||
workspace,
|
||||
application,
|
||||
};
|
||||
|
||||
const { result, hasNoMoreAvailableCredits } =
|
||||
await this.agentAsyncExecutorService.executeAgent({
|
||||
agent,
|
||||
userPrompt: input.prompt,
|
||||
authContext,
|
||||
workspaceId: workspace.id,
|
||||
userWorkspaceId: requestUserWorkspaceId,
|
||||
operationType: UsageOperationType.AI_WORKFLOW_TOKEN,
|
||||
});
|
||||
|
||||
if (hasNoMoreAvailableCredits) {
|
||||
return {
|
||||
result: null,
|
||||
error: 'AI agent stopped: no more available credits.',
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
|
||||
return { result, error: null, success: true };
|
||||
}
|
||||
}
|
||||
+6
-12
@@ -1,12 +1,6 @@
|
||||
import { type AgentResponseSchema } from 'twenty-shared/ai';
|
||||
|
||||
export type AgentResponseFormatType = AgentResponseFormat['type'];
|
||||
|
||||
export type AgentTextResponseFormat = { type: 'text' };
|
||||
export type AgentJsonResponseFormat = {
|
||||
type: 'json';
|
||||
schema: AgentResponseSchema;
|
||||
};
|
||||
export type AgentResponseFormat =
|
||||
| AgentTextResponseFormat
|
||||
| AgentJsonResponseFormat;
|
||||
export type {
|
||||
AgentResponseFormat,
|
||||
AgentResponseFormatType,
|
||||
AgentTextResponseFormat,
|
||||
AgentJsonResponseFormat,
|
||||
} from 'twenty-shared/ai';
|
||||
|
||||
@@ -6,6 +6,7 @@ import { AiAgentModule } from 'src/engine/metadata-modules/ai/ai-agent/ai-agent.
|
||||
import { AiChatModule } from 'src/engine/metadata-modules/ai/ai-chat/ai-chat.module';
|
||||
import { AiGenerateTextModule } from 'src/engine/metadata-modules/ai/ai-generate-text/ai-generate-text.module';
|
||||
import { AiWorkspaceStatsModule } from 'src/engine/metadata-modules/ai/ai-workspace-stats/ai-workspace-stats.module';
|
||||
import { ApplicationConnectionsModule } from 'src/engine/core-modules/application/connection-provider/connections/application-connections.module';
|
||||
import { CalendarChannelMetadataModule } from 'src/engine/metadata-modules/calendar-channel/calendar-channel-metadata.module';
|
||||
import { ConnectedAccountMetadataModule } from 'src/engine/metadata-modules/connected-account/connected-account-metadata.module';
|
||||
import { CommandMenuItemModule } from 'src/engine/metadata-modules/command-menu-item/command-menu-item.module';
|
||||
@@ -45,6 +46,7 @@ import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/work
|
||||
AiChatModule,
|
||||
AiGenerateTextModule,
|
||||
AiWorkspaceStatsModule,
|
||||
ApplicationConnectionsModule,
|
||||
MinimalMetadataModule,
|
||||
ViewModule,
|
||||
WorkspaceMetadataVersionModule,
|
||||
|
||||
@@ -15,6 +15,12 @@ export { DATA_RESIDENCY_KEYS } from './constants/data-residency.const';
|
||||
export type { NativeAiSdkProviderId } from './constants/native-ai-sdk-provider-ids.const';
|
||||
export { NATIVE_AI_SDK_PROVIDER_IDS } from './constants/native-ai-sdk-provider-ids.const';
|
||||
export { ToolCategory } from './constants/tool-category.const';
|
||||
export type {
|
||||
AgentResponseFormatType,
|
||||
AgentTextResponseFormat,
|
||||
AgentJsonResponseFormat,
|
||||
AgentResponseFormat,
|
||||
} from './types/agent-response-format.type';
|
||||
export type {
|
||||
AgentResponseFieldType,
|
||||
AgentResponseSchema,
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { type AgentResponseSchema } from '@/ai/types/agent-response-schema.type';
|
||||
|
||||
export type AgentResponseFormatType = AgentResponseFormat['type'];
|
||||
|
||||
export type AgentTextResponseFormat = { type: 'text' };
|
||||
export type AgentJsonResponseFormat = {
|
||||
type: 'json';
|
||||
schema: AgentResponseSchema;
|
||||
};
|
||||
export type AgentResponseFormat =
|
||||
| AgentTextResponseFormat
|
||||
| AgentJsonResponseFormat;
|
||||
@@ -1,3 +1,4 @@
|
||||
import { type AgentResponseFormat } from '@/ai/types/agent-response-format.type';
|
||||
import { type SyncableEntityOptions } from '@/application/syncableEntityOptionsType';
|
||||
|
||||
export type AgentManifest = SyncableEntityOptions & {
|
||||
@@ -7,4 +8,5 @@ export type AgentManifest = SyncableEntityOptions & {
|
||||
description?: string;
|
||||
prompt: string;
|
||||
modelId?: string;
|
||||
responseFormat?: AgentResponseFormat;
|
||||
};
|
||||
|
||||
@@ -66,6 +66,7 @@ export type {
|
||||
FieldPermissionManifest,
|
||||
RoleManifest,
|
||||
} from './roleManifestType';
|
||||
export type { RunAgentInput, RunAgentResult } from './runAgentType';
|
||||
export type { ServerVariables } from './server-variables.type';
|
||||
export type { SkillManifest } from './skillManifestType';
|
||||
export type { StoredOAuthConnectionProviderConfig } from './storedOAuthConnectionProviderConfigType';
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
export type RunAgentInput = {
|
||||
agentUniversalIdentifier: string;
|
||||
prompt: string;
|
||||
};
|
||||
|
||||
export type RunAgentResult = {
|
||||
result: object | null;
|
||||
error: string | null;
|
||||
success: boolean;
|
||||
};
|
||||
Reference in New Issue
Block a user