diff --git a/README.md b/README.md index def4e9b23..b68add48f 100644 --- a/README.md +++ b/README.md @@ -162,7 +162,7 @@ const client = contentful.createClient( // This is the access token for this space. Normally you get the token in the Contentful web app accessToken: 'YOUR_ACCESS_TOKEN', }, - { type: 'plain' } + { type: 'plain' }, ) //.... ``` @@ -177,7 +177,7 @@ const plainClient = contentful.createClient( { accessToken: 'YOUR_ACCESS_TOKEN', }, - { type: 'plain' } + { type: 'plain' }, ) const environment = await plainClient.environment.get({ @@ -205,7 +205,7 @@ const scopedPlainClient = contentful.createClient( spaceId: '', environmentId: '', }, - } + }, ) // entries from '' & '' @@ -232,18 +232,19 @@ The benefits of using the "plain" version of the client, over the legacy version Cursor-based pagination is supported on collection endpoints for content types, entries, and assets. To use cursor-based pagination, use the related entity methods `getAssetsWithCursor`, `getContentTypesWithCursor`, and `getEntriesWithCursor` ```js -const response = await environment.getEntriesWithCursor({ limit: 10 }); -console.log(response.items); // Array of items -console.log(response.pages?.next); // Cursor for next page +const response = await environment.getEntriesWithCursor({ limit: 10 }) +console.log(response.items) // Array of items +console.log(response.pages?.next) // Cursor for next page ``` + Use the value from `response.pages.next` to fetch the next page. ```js const secondPage = await environment.getEntriesWithCursor({ limit: 2, pageNext: response.pages?.next, -}); -console.log(secondPage.items); // Array of items +}) +console.log(secondPage.items) // Array of items ``` ## Legacy Client Interface @@ -298,7 +299,7 @@ contentfulApp.init((sdk) => { environmentId: sdk.ids.environmentAlias ?? sdk.ids.environment, spaceId: sdk.ids.space, }, - } + }, ) // ...rest of initialization code @@ -444,9 +445,11 @@ To download a build that has features that are not yet released, you can use the npm install contentful-management@canary ``` +In addition, there may be some experimental features in the main build of this SDK that are subject to breaking changes without notice, which are listed below: + ### Current experimental features -Currently there are no features in experimental status. +- **AI Agents**: The Agent and Agent Run APIs (`getAgent`, `getAgents`, `getAgentRun`, `getAgentRuns`, `generateWithAgent`) are experimental and subject to breaking changes without notice. ## Reach out to us diff --git a/lib/adapters/REST/endpoints/agent-run.ts b/lib/adapters/REST/endpoints/agent-run.ts new file mode 100644 index 000000000..1adfcd087 --- /dev/null +++ b/lib/adapters/REST/endpoints/agent-run.ts @@ -0,0 +1,45 @@ +import type { RawAxiosRequestHeaders } from 'axios' +import type { AxiosInstance } from 'contentful-sdk-core' +import type { CollectionProp, GetSpaceEnvironmentParams } from '../../../common-types' +import type { AgentRunProps, AgentRunQueryOptions } from '../../../entities/agent-run' +import type { RestEndpoint } from '../types' +import * as raw from './raw' + +const AgentRunAlphaHeaders = { + 'x-contentful-enable-alpha-feature': 'agents', +} + +export const get: RestEndpoint<'AgentRun', 'get'> = ( + http: AxiosInstance, + params: GetSpaceEnvironmentParams & { runId: string }, + headers?: RawAxiosRequestHeaders, +) => { + return raw.get( + http, + `/spaces/${params.spaceId}/environments/${params.environmentId}/ai_agents/runs/${params.runId}`, + { + headers: { + ...AgentRunAlphaHeaders, + ...headers, + }, + }, + ) +} + +export const getMany: RestEndpoint<'AgentRun', 'getMany'> = ( + http: AxiosInstance, + params: GetSpaceEnvironmentParams & { query?: AgentRunQueryOptions }, + headers?: RawAxiosRequestHeaders, +) => { + return raw.get>( + http, + `/spaces/${params.spaceId}/environments/${params.environmentId}/ai_agents/runs`, + { + params: params.query, + headers: { + ...AgentRunAlphaHeaders, + ...headers, + }, + }, + ) +} diff --git a/lib/adapters/REST/endpoints/agent.ts b/lib/adapters/REST/endpoints/agent.ts new file mode 100644 index 000000000..4bfaad94a --- /dev/null +++ b/lib/adapters/REST/endpoints/agent.ts @@ -0,0 +1,64 @@ +import type { RawAxiosRequestHeaders } from 'axios' +import type { AxiosInstance } from 'contentful-sdk-core' +import type { CollectionProp, GetSpaceEnvironmentParams, QueryParams } from '../../../common-types' +import type { AgentGeneratePayload, AgentProps } from '../../../entities/agent' +import type { AgentRunProps } from '../../../entities/agent-run' +import type { RestEndpoint } from '../types' +import * as raw from './raw' + +const AgentAlphaHeaders = { + 'x-contentful-enable-alpha-feature': 'agents', +} + +export const get: RestEndpoint<'Agent', 'get'> = ( + http: AxiosInstance, + params: GetSpaceEnvironmentParams & { agentId: string }, + headers?: RawAxiosRequestHeaders, +) => { + return raw.get( + http, + `/spaces/${params.spaceId}/environments/${params.environmentId}/ai_agents/agents/${params.agentId}`, + { + headers: { + ...AgentAlphaHeaders, + ...headers, + }, + }, + ) +} + +export const getMany: RestEndpoint<'Agent', 'getMany'> = ( + http: AxiosInstance, + params: GetSpaceEnvironmentParams, + headers?: RawAxiosRequestHeaders, +) => { + return raw.get>( + http, + `/spaces/${params.spaceId}/environments/${params.environmentId}/ai_agents/agents`, + { + headers: { + ...AgentAlphaHeaders, + ...headers, + }, + }, + ) +} + +export const generate: RestEndpoint<'Agent', 'generate'> = ( + http: AxiosInstance, + params: GetSpaceEnvironmentParams & { agentId: string }, + data: AgentGeneratePayload, + headers?: RawAxiosRequestHeaders, +) => { + return raw.post( + http, + `/spaces/${params.spaceId}/environments/${params.environmentId}/ai_agents/agents/${params.agentId}/generate`, + data, + { + headers: { + ...AgentAlphaHeaders, + ...headers, + }, + }, + ) +} diff --git a/lib/adapters/REST/endpoints/index.ts b/lib/adapters/REST/endpoints/index.ts index 21ef04c34..f6c18e194 100644 --- a/lib/adapters/REST/endpoints/index.ts +++ b/lib/adapters/REST/endpoints/index.ts @@ -1,5 +1,7 @@ import * as AiAction from './ai-action' import * as AiActionInvocation from './ai-action-invocation' +import * as Agent from './agent' +import * as AgentRun from './agent-run' import * as AccessToken from './access-token' import * as ApiKey from './api-key' import * as AppAccessToken from './app-access-token' @@ -75,6 +77,8 @@ import * as WorkflowsChangelog from './workflows-changelog' export default { AiAction, AiActionInvocation, + Agent, + AgentRun, ApiKey, AppAction, AppActionCall, diff --git a/lib/common-types.ts b/lib/common-types.ts index da615a938..430fb9c45 100644 --- a/lib/common-types.ts +++ b/lib/common-types.ts @@ -191,6 +191,8 @@ import type { AiActionInvocationProps, AiActionInvocationType, } from './entities/ai-action-invocation' +import type { AgentGeneratePayload, AgentProps } from './entities/agent' +import type { AgentRunProps, AgentRunQueryOptions } from './entities/agent-run' import type { UpdateVectorizationStatusProps, VectorizationStatusProps, @@ -458,6 +460,13 @@ type MRInternal = { (opts: MROpts<'AiActionInvocation', 'get', UA>): MRReturn<'AiActionInvocation', 'get'> + (opts: MROpts<'Agent', 'get', UA>): MRReturn<'Agent', 'get'> + (opts: MROpts<'Agent', 'getMany', UA>): MRReturn<'Agent', 'getMany'> + (opts: MROpts<'Agent', 'generate', UA>): MRReturn<'Agent', 'generate'> + + (opts: MROpts<'AgentRun', 'get', UA>): MRReturn<'AgentRun', 'get'> + (opts: MROpts<'AgentRun', 'getMany', UA>): MRReturn<'AgentRun', 'getMany'> + (opts: MROpts<'AppAction', 'get', UA>): MRReturn<'AppAction', 'get'> (opts: MROpts<'AppAction', 'getMany', UA>): MRReturn<'AppAction', 'getMany'> (opts: MROpts<'AppAction', 'delete', UA>): MRReturn<'AppAction', 'delete'> @@ -1033,6 +1042,36 @@ export type MRActions = { return: AiActionInvocationProps } } + Agent: { + get: { + params: GetSpaceEnvironmentParams & { agentId: string } + headers?: RawAxiosRequestHeaders + return: AgentProps + } + getMany: { + params: GetSpaceEnvironmentParams + headers?: RawAxiosRequestHeaders + return: CollectionProp + } + generate: { + params: GetSpaceEnvironmentParams & { agentId: string } + payload: AgentGeneratePayload + headers?: RawAxiosRequestHeaders + return: AgentRunProps + } + } + AgentRun: { + get: { + params: GetSpaceEnvironmentParams & { runId: string } + headers?: RawAxiosRequestHeaders + return: AgentRunProps + } + getMany: { + params: GetSpaceEnvironmentParams & { query?: AgentRunQueryOptions } + headers?: RawAxiosRequestHeaders + return: CollectionProp + } + } AppAction: { get: { params: GetAppActionParams; return: AppActionProps } getMany: { diff --git a/lib/create-environment-api.ts b/lib/create-environment-api.ts index 06b5b406a..bf2143bef 100644 --- a/lib/create-environment-api.ts +++ b/lib/create-environment-api.ts @@ -65,6 +65,8 @@ import type { CreateAppAccessTokenProps } from './entities/app-access-token' import type { ResourceQueryOptions } from './entities/resource' import type { AiActionInvocationType } from './entities/ai-action-invocation' import { wrapAiActionInvocation } from './entities/ai-action-invocation' +import type { AgentGeneratePayload } from './entities/agent' +import type { AgentRunQueryOptions } from './entities/agent-run' import type { GetSemanticDuplicatesProps } from './entities/semantic-duplicates' import type { GetSemanticRecommendationsProps } from './entities/semantic-recommendations' import type { GetSemanticReferenceSuggestionsProps } from './entities/semantic-reference-suggestions' @@ -98,6 +100,8 @@ export default function createEnvironmentApi(makeRequest: MakeRequest) { const { wrapAppActionCall } = entities.appActionCall const { wrapBulkAction } = entities.bulkAction const { wrapAppAccessToken } = entities.appAccessToken + const { wrapAgent, wrapAgentCollection } = entities.agent + const { wrapAgentRun, wrapAgentRunCollection } = entities.agentRun const { wrapResourceTypesForEnvironmentCollection } = entities.resourceType const { wrapResourceCollection } = entities.resource const { wrapSemanticDuplicates } = entities.semanticDuplicates @@ -2918,5 +2922,175 @@ export default function createEnvironmentApi(makeRequest: MakeRequest) { payload, }).then((data) => wrapSemanticSearch(makeRequest, data)) }, + + /** + * Gets an AI Agent + * @param agentId - AI Agent ID + * @return Promise for an AI Agent + * @example ```javascript + * const contentful = require('contentful-management') + * + * const client = contentful.createClient({ + * accessToken: '' + * }) + * + * client.getSpace('') + * .then((space) => space.getEnvironment('')) + * .then((environment) => environment.getAgent('')) + * .then((agent) => console.log(agent)) + * .catch(console.error) + * ``` + */ + getAgent(agentId: string) { + const raw = this.toPlainObject() as EnvironmentProps + return makeRequest({ + entityType: 'Agent', + action: 'get', + params: { + spaceId: raw.sys.space.sys.id, + environmentId: raw.sys.id, + agentId, + }, + }).then((data) => wrapAgent(makeRequest, data)) + }, + + /** + * Gets a collection of AI Agents + * @return Promise for a collection of AI Agents + * @example ```javascript + * const contentful = require('contentful-management') + * + * const client = contentful.createClient({ + * accessToken: '' + * }) + * + * client.getSpace('') + * .then((space) => space.getEnvironment('')) + * .then((environment) => environment.getAgents()) + * .then((response) => console.log(response.items)) + * .catch(console.error) + * ``` + */ + getAgents() { + const raw = this.toPlainObject() as EnvironmentProps + return makeRequest({ + entityType: 'Agent', + action: 'getMany', + params: { + spaceId: raw.sys.space.sys.id, + environmentId: raw.sys.id, + }, + }).then((data) => wrapAgentCollection(makeRequest, data)) + }, + + /** + * Generates content using an AI Agent + * @param agentId - AI Agent ID + * @param payload - Generation payload + * @return Promise for the generation response + * @example ```javascript + * const contentful = require('contentful-management') + * + * const client = contentful.createClient({ + * accessToken: '' + * }) + * + * client.getSpace('') + * .then((space) => space.getEnvironment('')) + * .then((environment) => environment.generateWithAgent('', { + * messages: [ + * { + * parts: [ + * { + * type: 'text', + * text: 'Write a short poem about Contentful' + * } + * ], + * role: 'user' + * } + * ] + * })) + * .then((result) => console.log(result)) + * .catch(console.error) + * ``` + */ + generateWithAgent(agentId: string, payload: AgentGeneratePayload) { + const raw = this.toPlainObject() as EnvironmentProps + return makeRequest({ + entityType: 'Agent', + action: 'generate', + params: { + spaceId: raw.sys.space.sys.id, + environmentId: raw.sys.id, + agentId, + }, + payload, + }).then((data) => wrapAgentRun(makeRequest, data)) + }, + + /** + * Gets an AI Agent Run + * @param runId - AI Agent Run ID + * @return Promise for an AI Agent Run + * @example ```javascript + * const contentful = require('contentful-management') + * + * const client = contentful.createClient({ + * accessToken: '' + * }) + * + * client.getSpace('') + * .then((space) => space.getEnvironment('')) + * .then((environment) => environment.getAgentRun('')) + * .then((run) => console.log(run)) + * .catch(console.error) + * ``` + */ + getAgentRun(runId: string) { + const raw = this.toPlainObject() as EnvironmentProps + return makeRequest({ + entityType: 'AgentRun', + action: 'get', + params: { + spaceId: raw.sys.space.sys.id, + environmentId: raw.sys.id, + runId, + }, + }).then((data) => wrapAgentRun(makeRequest, data)) + }, + + /** + * Gets a collection of AI Agent Runs with optional filtering + * @param query - Object with search parameters (agentIn, statusIn) + * @return Promise for a collection of AI Agent Runs + * @example ```javascript + * const contentful = require('contentful-management') + * + * const client = contentful.createClient({ + * accessToken: '' + * }) + * + * client.getSpace('') + * .then((space) => space.getEnvironment('')) + * .then((environment) => environment.getAgentRuns({ + * agentIn: ['agent1', 'agent2'], + * statusIn: ['COMPLETED', 'IN_PROGRESS'] + * })) + * .then((response) => console.log(response.items)) + * .catch(console.error) + * ``` + */ + getAgentRuns(query: AgentRunQueryOptions = {}) { + const raw = this.toPlainObject() as EnvironmentProps + return makeRequest({ + entityType: 'AgentRun', + action: 'getMany', + params: { + spaceId: raw.sys.space.sys.id, + environmentId: raw.sys.id, + query, + }, + }).then((data) => wrapAgentRunCollection(makeRequest, data)) + }, } } diff --git a/lib/entities/agent-run.ts b/lib/entities/agent-run.ts new file mode 100644 index 000000000..775f29c6d --- /dev/null +++ b/lib/entities/agent-run.ts @@ -0,0 +1,63 @@ +import { freezeSys, toPlainObject } from 'contentful-sdk-core' +import copy from 'fast-copy' +import type { DefaultElements, Link, MakeRequest, MetaSysProps } from '../common-types' +import { wrapCollection } from '../common-utils' + +export type AgentRunStatus = 'IN_PROGRESS' | 'FAILED' | 'COMPLETED' | 'PENDING_REVIEW' | 'DRAFT' + +export type AgentRunMessageRole = 'system' | 'user' | 'assistant' | 'tool' + +export type AgentRunMessageTextPart = { + type: 'text' + text: string +} + +export type AgentRunMessageToolCallPart = { + type: 'tool-call' + toolCallId: string + toolName: string + args: unknown +} + +export type AgentRunMessagePart = AgentRunMessageTextPart | AgentRunMessageToolCallPart + +export type AgentRunMessage = { + id: string + createdAt: string + role: AgentRunMessageRole + content: { + parts: Array + } +} + +export type AgentRunProps = { + sys: MetaSysProps & { + type: 'AgentRun' + createdAt: string + updatedAt?: string + status: AgentRunStatus + id: string + } + agent: { + sys: Link<'Agent'> + } + space: { + sys: Link<'Space'> + } + title: string + messages: Array +} + +export type AgentRunQueryOptions = { + agentIn?: string[] + statusIn?: AgentRunStatus[] +} + +export interface AgentRun extends AgentRunProps, DefaultElements {} + +export function wrapAgentRun(_makeRequest: MakeRequest, data: AgentRunProps): AgentRun { + const agentRun = toPlainObject(copy(data)) + return freezeSys(agentRun) +} + +export const wrapAgentRunCollection = wrapCollection(wrapAgentRun) diff --git a/lib/entities/agent.ts b/lib/entities/agent.ts new file mode 100644 index 000000000..3690a9bc0 --- /dev/null +++ b/lib/entities/agent.ts @@ -0,0 +1,75 @@ +import { freezeSys, toPlainObject } from 'contentful-sdk-core' +import copy from 'fast-copy' +import type { DefaultElements, MakeRequest, MetaSysProps } from '../common-types' +import { wrapCollection } from '../common-utils' +import enhanceWithMethods from '../enhance-with-methods' +import { wrapAgentRun, type AgentRun } from './agent-run' + +export type AgentToolLink = { + sys: { + type: 'Link' + linkType: 'AgentTool' + id: string + } +} + +export type AgentProps = { + sys: MetaSysProps & { + type: 'Agent' + space: { sys: { id: string } } + environment: { sys: { id: string } } + createdAt: string + id: string + } + name: string + description: string + tools?: Array + provider?: string + modelId?: string +} + +type AgentMessageRole = 'system' | 'user' | 'assistant' | 'tool' + +export type AgentGeneratePayload = { + messages: Array<{ + parts: Array<{ + type: 'text' + text: string + }> + id?: string + role: AgentMessageRole + }> + threadId?: string +} + +export interface Agent extends AgentProps, DefaultElements { + generate(payload: AgentGeneratePayload): Promise +} + +function createAgentApi(makeRequest: MakeRequest) { + const getParams = (data: AgentProps) => ({ + spaceId: data.sys.space.sys.id, + environmentId: data.sys.environment.sys.id, + agentId: data.sys.id, + }) + + return { + generate: function generate(payload: AgentGeneratePayload) { + const self = this as AgentProps + return makeRequest({ + entityType: 'Agent', + action: 'generate', + params: getParams(self), + payload, + }).then((data) => wrapAgentRun(makeRequest, data)) + }, + } +} + +export function wrapAgent(makeRequest: MakeRequest, data: AgentProps): Agent { + const agent = toPlainObject(copy(data)) + const agentWithMethods = enhanceWithMethods(agent, createAgentApi(makeRequest)) + return freezeSys(agentWithMethods) +} + +export const wrapAgentCollection = wrapCollection(wrapAgent) diff --git a/lib/entities/index.ts b/lib/entities/index.ts index fd71b7f33..39fc78a86 100644 --- a/lib/entities/index.ts +++ b/lib/entities/index.ts @@ -1,5 +1,7 @@ import * as aiAction from './ai-action' import * as aiActionInvocation from './ai-action-invocation' +import * as agent from './agent' +import * as agentRun from './agent-run' import * as apiKey from './api-key' import * as appAction from './app-action' import * as appActionCall from './app-action-call' @@ -69,6 +71,8 @@ import * as resource from './resource' export default { aiAction, aiActionInvocation, + agent, + agentRun, accessToken, appAction, appActionCall, diff --git a/lib/export-types.ts b/lib/export-types.ts index ffee6038b..0340cfba2 100644 --- a/lib/export-types.ts +++ b/lib/export-types.ts @@ -12,6 +12,18 @@ export type { } from './entities/app-access-token' export type { AiAction, AiActionProps, CreateAiActionProps } from './entities/ai-action' export type { AiActionInvocation, AiActionInvocationProps } from './entities/ai-action-invocation' +export type { Agent, AgentGeneratePayload, AgentProps, AgentToolLink } from './entities/agent' +export type { + AgentRun, + AgentRunMessage, + AgentRunMessagePart, + AgentRunMessageRole, + AgentRunMessageTextPart, + AgentRunMessageToolCallPart, + AgentRunProps, + AgentRunQueryOptions, + AgentRunStatus, +} from './entities/agent-run' export type { AppAction, AppActionCategoryProps, diff --git a/lib/plain/common-types.ts b/lib/plain/common-types.ts index a79c71d33..2222c4c41 100644 --- a/lib/plain/common-types.ts +++ b/lib/plain/common-types.ts @@ -140,6 +140,8 @@ import type { OAuthApplicationPlainClientAPI } from './entities/oauth-applicatio import type { FunctionLogPlainClientAPI } from './entities/function-log' import type { AiActionPlainClientAPI } from './entities/ai-action' import type { AiActionInvocationPlainClientAPI } from './entities/ai-action-invocation' +import type { AgentPlainClientAPI } from './entities/agent' +import type { AgentRunPlainClientAPI } from './entities/agent-run' import type { VectorizationStatusPlainClientAPI } from './entities/vectorization-status' import type { SemanticDuplicatesPlainClientAPI } from './entities/semantic-duplicates' import type { SemanticRecommendationsPlainClientAPI } from './entities/semantic-recommendations' @@ -158,6 +160,8 @@ export type PlainClientAPI = { } aiAction: AiActionPlainClientAPI aiActionInvocation: AiActionInvocationPlainClientAPI + agent: AgentPlainClientAPI + agentRun: AgentRunPlainClientAPI appAction: AppActionPlainClientAPI appActionCall: AppActionCallPlainClientAPI appBundle: AppBundlePlainClientAPI diff --git a/lib/plain/entities/agent-run.ts b/lib/plain/entities/agent-run.ts new file mode 100644 index 000000000..666ce160c --- /dev/null +++ b/lib/plain/entities/agent-run.ts @@ -0,0 +1,30 @@ +import type { RawAxiosRequestHeaders } from 'axios' +import type { CollectionProp, GetSpaceEnvironmentParams } from '../../common-types' +import type { AgentRunProps, AgentRunQueryOptions } from '../../entities/agent-run' +import type { OptionalDefaults } from '../wrappers/wrap' + +export type AgentRunPlainClientAPI = { + /** + * Fetches an AI Agent Run. + * @param params Entity IDs to identify the AI Agent Run. + * Must include spaceId, environmentId, and runId. + * @param headers Optional headers for the request. + * @returns A promise resolving with the AI Agent Run. + * @throws if the request fails or the AI Agent Run is not found. + */ + get( + params: OptionalDefaults, + headers?: Partial, + ): Promise + /** + * Fetches all AI Agent Runs for the given space and environment. + * @param params Entity IDs and query options. + * @param headers Optional headers for the request. + * @returns A promise resolving with a collection of AI Agent Runs. + * @throws if the request fails or the entities are not found. + */ + getMany( + params: OptionalDefaults, + headers?: Partial, + ): Promise> +} diff --git a/lib/plain/entities/agent.ts b/lib/plain/entities/agent.ts new file mode 100644 index 000000000..c345e907a --- /dev/null +++ b/lib/plain/entities/agent.ts @@ -0,0 +1,39 @@ +import type { RawAxiosRequestHeaders } from 'axios' +import type { CollectionProp, GetSpaceEnvironmentParams, QueryParams } from '../../common-types' +import type { AgentGeneratePayload, AgentProps } from '../../entities/agent' +import type { AgentRunProps } from '../../entities/agent-run' +import type { OptionalDefaults } from '../wrappers/wrap' + +export type AgentPlainClientAPI = { + /** + * Fetches the AI Agent. + * @param params Entity IDs to identify the AI Agent. + * @returns The AI Agent. + * @throws if the request fails or the AI Agent is not found. + */ + get( + params: OptionalDefaults, + ): Promise + /** + * Fetches all AI Agents for the given space and environment. + * @param params Entity IDs and query options. + * @returns A collection containing an array of AI Agents. + * @throws if the request fails or the entities are not found. + */ + getMany( + params: OptionalDefaults, + ): Promise> + /** + * Generates a response from an AI Agent. + * @param params Entity IDs to identify the AI Agent. + * @param payload The generation payload. + * @param headers Optional headers for the request. + * @returns A promise resolving with the AI Agent generation response. + * @throws if the request fails or the payload is malformed. + */ + generate( + params: OptionalDefaults, + payload: AgentGeneratePayload, + headers?: Partial, + ): Promise +} diff --git a/lib/plain/plain-client.ts b/lib/plain/plain-client.ts index dcdf6eca0..6330641ae 100644 --- a/lib/plain/plain-client.ts +++ b/lib/plain/plain-client.ts @@ -71,6 +71,15 @@ export const createPlainClient = ( aiActionInvocation: { get: wrap(wrapParams, 'AiActionInvocation', 'get'), }, + agent: { + get: wrap(wrapParams, 'Agent', 'get'), + getMany: wrap(wrapParams, 'Agent', 'getMany'), + generate: wrap(wrapParams, 'Agent', 'generate'), + }, + agentRun: { + get: wrap(wrapParams, 'AgentRun', 'get'), + getMany: wrap(wrapParams, 'AgentRun', 'getMany'), + }, appAction: { get: wrap(wrapParams, 'AppAction', 'get'), getMany: wrap(wrapParams, 'AppAction', 'getMany'), diff --git a/test/integration/agent-integration.test.ts b/test/integration/agent-integration.test.ts new file mode 100644 index 000000000..28a3851d4 --- /dev/null +++ b/test/integration/agent-integration.test.ts @@ -0,0 +1,77 @@ +import { afterAll, beforeAll, describe, expect, test } from 'vitest' +import type { Environment, Space } from '../../lib/export-types' +import { createTestSpace, defaultClient, timeoutToCalmRateLimiting } from '../helpers' + +// Skip in CI since the endpoints are feature flagged and test spaces are not persistent +// Run locally when API_INTEGRATION_TESTS is set to point to local dev environment where access can be controlled +describe.skipIf(!process.env.API_INTEGRATION_TESTS)('Agent api', { sequential: true }, () => { + let space: Space + let environment: Environment + + beforeAll(async () => { + space = await createTestSpace(defaultClient, 'Agent') + environment = await space.getEnvironment('master') + }) + + afterAll(async () => { + if (space) { + return space.delete() + } + + await timeoutToCalmRateLimiting() + }) + + test('Gets ai agents', async () => { + return environment.getAgents().then((response) => { + expect(response.sys, 'sys').to.be.ok + expect(response.items, 'items').to.be.ok + }) + }) + + test('Gets ai agent runs', async () => { + return environment.getAgentRuns().then((response) => { + expect(response.sys, 'sys').to.be.ok + expect(response.items, 'items').to.be.ok + }) + }) + + test('Get specific ai agent', async () => { + const agentId = 'translation-agent' + return environment.getAgent(agentId).then((agent) => { + expect(agent.sys.id).equals(agentId) + }) + }) + + test('Generate with ai agent', async () => { + const agentId = 'translation-agent' + const result = await environment.generateWithAgent(agentId, { + messages: [ + { + parts: [ + { + type: 'text', + text: 'test', + }, + ], + id: 'YlOfVycwiPhTMX1G', + role: 'user', + }, + ], + }) + expect(result.result).to.be.ok + }) + + test('Get specific ai agent run', async () => { + const runs = await environment.getAgentRuns() + if (runs.items.length > 0) { + const runId = runs.items[0].sys.id + return environment.getAgentRun(runId).then((run) => { + expect(run.sys.id).equals(runId) + expect(run.sys.type).equals('AgentRun', 'type is AgentRun') + expect(run.title).to.be.ok + expect(run.agent).to.be.ok + expect(run.space).to.be.ok + }) + } + }) +}) diff --git a/test/unit/create-environment-api.test.ts b/test/unit/create-environment-api.test.ts index 76b813742..abad17f42 100644 --- a/test/unit/create-environment-api.test.ts +++ b/test/unit/create-environment-api.test.ts @@ -904,6 +904,91 @@ describe('A createEnvironmentApi', () => { }) }) + // AI Agents + + test('API call getAgent', async () => { + const agent = cloneMock('agent') + const { api, entitiesMock } = setup(Promise.resolve(agent)) + entitiesMock.agent.wrapAgent.mockReturnValue(agent) + + return api.getAgent('agent-id').then((r) => { + expect(r).to.eql(agent) + }) + }) + + test('API call getAgent fails', async () => { + return makeEntityMethodFailingTest(setup, { + methodToTest: 'getAgent', + }) + }) + + test('API call getAgents', async () => { + const agentCollection = mockCollection(cloneMock('agent')) + const { api, entitiesMock } = setup(Promise.resolve(agentCollection)) + entitiesMock.agent.wrapAgentCollection.mockReturnValue(agentCollection) + + return api.getAgents().then((r) => { + expect(r).to.eql(agentCollection) + }) + }) + + test('API call getAgents fails', async () => { + return makeEntityMethodFailingTest(setup, { + methodToTest: 'getAgents', + }) + }) + + test('API call generateWithAgent', async () => { + const generateResponse = { result: 'generated content' } + const { api } = setup(Promise.resolve(generateResponse)) + + return api + .generateWithAgent('agent-id', { + messages: [{ parts: [{ type: 'text', text: 'Hello' }], role: 'user' }], + }) + .then((r) => { + expect(r).to.eql(generateResponse) + }) + }) + + test('API call generateWithAgent fails', async () => { + return makeEntityMethodFailingTest(setup, { + methodToTest: 'generateWithAgent', + }) + }) + + test('API call getAgentRun', async () => { + const agentRun = cloneMock('agentRun') + const { api, entitiesMock } = setup(Promise.resolve(agentRun)) + entitiesMock.agentRun.wrapAgentRun.mockReturnValue(agentRun) + + return api.getAgentRun('run-id').then((r) => { + expect(r).to.eql(agentRun) + }) + }) + + test('API call getAgentRun fails', async () => { + return makeEntityMethodFailingTest(setup, { + methodToTest: 'getAgentRun', + }) + }) + + test('API call getAgentRuns', async () => { + const agentRunCollection = mockCollection(cloneMock('agentRun')) + const { api, entitiesMock } = setup(Promise.resolve(agentRunCollection)) + entitiesMock.agentRun.wrapAgentRunCollection.mockReturnValue(agentRunCollection) + + return api.getAgentRuns().then((r) => { + expect(r).to.eql(agentRunCollection) + }) + }) + + test('API call getAgentRuns fails', async () => { + return makeEntityMethodFailingTest(setup, { + methodToTest: 'getAgentRuns', + }) + }) + // Embargoed Assets test('API call createAssetKey', async () => { diff --git a/test/unit/entities/agent-run.test.ts b/test/unit/entities/agent-run.test.ts new file mode 100644 index 000000000..232bba860 --- /dev/null +++ b/test/unit/entities/agent-run.test.ts @@ -0,0 +1,29 @@ +import { describe, test } from 'vitest' +import { wrapAgentRun, wrapAgentRunCollection } from '../../../lib/entities/agent-run' +import { cloneMock } from '../mocks/entities' +import setupMakeRequest from '../mocks/makeRequest' +import { + entityCollectionWrappedTest, + entityWrappedTest, +} from '../test-creators/instance-entity-methods' + +function setup(promise: any) { + return { + makeRequest: setupMakeRequest(promise), + entityMock: cloneMock('agentRun'), + } +} + +describe('Entity AgentRun', () => { + test('AgentRun is wrapped', async () => { + return entityWrappedTest(setup, { + wrapperMethod: wrapAgentRun, + }) + }) + + test('AgentRun collection is wrapped', async () => { + return entityCollectionWrappedTest(setup, { + wrapperMethod: wrapAgentRunCollection, + }) + }) +}) diff --git a/test/unit/entities/agent.test.ts b/test/unit/entities/agent.test.ts new file mode 100644 index 000000000..6ccca1553 --- /dev/null +++ b/test/unit/entities/agent.test.ts @@ -0,0 +1,49 @@ +import { describe, test } from 'vitest' +import { wrapAgent, wrapAgentCollection } from '../../../lib/entities/agent' +import { cloneMock } from '../mocks/entities' +import setupMakeRequest from '../mocks/makeRequest' +import { + entityActionTest, + entityCollectionWrappedTest, + entityWrappedTest, + failingActionTest, +} from '../test-creators/instance-entity-methods' + +function setup(promise: any) { + return { + makeRequest: setupMakeRequest(promise), + entityMock: cloneMock('agent'), + } +} + +describe('Entity Agent', () => { + test('Agent is wrapped', async () => { + return entityWrappedTest(setup, { + wrapperMethod: wrapAgent, + }) + }) + + test('Agent collection is wrapped', async () => { + return entityCollectionWrappedTest(setup, { + wrapperMethod: wrapAgentCollection, + }) + }) + + test('Agent generate', async () => { + return entityActionTest( + setup, + { + wrapperMethod: wrapAgent, + actionMethod: 'generate', + }, + false, + ) + }) + + test('Agent generate fails', async () => { + return failingActionTest(setup, { + wrapperMethod: wrapAgent, + actionMethod: 'generate', + }) + }) +}) diff --git a/test/unit/mocks/entities.ts b/test/unit/mocks/entities.ts index 191111649..79ab486aa 100644 --- a/test/unit/mocks/entities.ts +++ b/test/unit/mocks/entities.ts @@ -90,6 +90,8 @@ import { AiActionInvocationProps, AiActionInvocationType, } from '../../../lib/entities/ai-action-invocation' +import { AgentProps } from '../../../lib/entities/agent' +import { AgentRunProps } from '../../../lib/entities/agent-run' import { EmbeddingSetStatus, VectorizationStatusProps, @@ -943,6 +945,83 @@ const aiActionInvocationMock: AiActionInvocationProps = { }, } +const agentMock: AgentProps = { + sys: Object.assign(cloneDeep(sysMock), { + type: 'Agent' as const, + id: 'mocked-agent-id', + space: { sys: { id: 'mocked-space-id' } }, + environment: { sys: { id: 'mocked-environment-id' } }, + createdAt: '2025-12-15T10:00:00.000Z', + }), + name: 'Mocked AI Agent', + description: 'This is a mocked AI Agent for testing purposes.', + provider: 'openai', + modelId: 'gpt-4', + tools: [ + { + sys: { + type: 'Link' as const, + linkType: 'AgentTool' as const, + id: 'tool-1', + }, + }, + ], +} + +const agentRunMock: AgentRunProps = { + sys: { + ...cloneDeep(sysMock), + type: 'AgentRun' as const, + id: 'mocked-agent-run-id', + createdAt: '2025-12-15T10:00:00.000Z', + updatedAt: '2025-12-15T10:05:00.000Z', + status: 'COMPLETED' as const, + }, + agent: { + sys: { + type: 'Link' as const, + linkType: 'Agent' as const, + id: 'mocked-agent-id', + }, + }, + space: { + sys: { + type: 'Link' as const, + linkType: 'Space' as const, + id: 'mocked-space-id', + }, + }, + title: 'Mocked Agent Run', + messages: [ + { + id: 'msg-1', + createdAt: '2025-12-15T10:00:00.000Z', + role: 'user' as const, + content: { + parts: [ + { + type: 'text' as const, + text: 'Hello, agent!', + }, + ], + }, + }, + { + id: 'msg-2', + createdAt: '2025-12-15T10:00:05.000Z', + role: 'assistant' as const, + content: { + parts: [ + { + type: 'text' as const, + text: 'Hello! How can I help you?', + }, + ], + }, + }, + ], +} + const apiKeyMock: ApiKeyProps = { sys: Object.assign(cloneDeep(sysMock), { type: 'ApiKey', @@ -1482,6 +1561,8 @@ const mocks = { aiAction: aiActionMock, aiActionInvocation: aiActionInvocationMock, aiActionInvocationPayload: aiActionInvocationPayloadMock, + agent: agentMock, + agentRun: agentRunMock, apiKey: apiKeyMock, appAction: appActionMock, appActionCall: appActionCallMock, @@ -1605,6 +1686,14 @@ function setupEntitiesMock() { aiActionInvocation: { wrapAiActionInvocation: vi.fn(), }, + agent: { + wrapAgent: vi.fn(), + wrapAgentCollection: vi.fn(), + }, + agentRun: { + wrapAgentRun: vi.fn(), + wrapAgentRunCollection: vi.fn(), + }, appAction: { wrapAppAction: vi.fn(), wrapAppActionCollection: vi.fn(), diff --git a/test/unit/plain/agent-run.test.ts b/test/unit/plain/agent-run.test.ts new file mode 100644 index 000000000..cf9e18b8d --- /dev/null +++ b/test/unit/plain/agent-run.test.ts @@ -0,0 +1,125 @@ +import { describe, expect, test } from 'vitest' +import { createClient } from '../../../lib/contentful-management' +import setupRestAdapter from '../adapters/REST/helpers/setupRestAdapter' + +describe('AgentRun', () => { + const spaceId = 'space-id' + const environmentId = 'env-id' + const runId = 'run-id' + + const mockAgentRun = { + sys: { + type: 'AgentRun', + id: runId, + version: 1, + createdAt: '2025-12-15T10:00:00.000Z', + updatedAt: '2025-12-15T10:05:00.000Z', + status: 'COMPLETED', + }, + agent: { + sys: { + type: 'Link', + linkType: 'Agent', + id: 'agent-id', + }, + }, + space: { + sys: { + type: 'Link', + linkType: 'Space', + id: spaceId, + }, + }, + title: 'Test Agent Run', + messages: [ + { + id: 'msg-1', + createdAt: '2025-12-15T10:00:00.000Z', + role: 'user', + content: { + parts: [ + { + type: 'text', + text: 'Hello', + }, + ], + }, + }, + ], + } + + test('get', async () => { + const { httpMock, adapterMock } = setupRestAdapter(Promise.resolve({ data: mockAgentRun })) + const plainClient = createClient({ apiAdapter: adapterMock }, { type: 'plain' }) + const response = await plainClient.agentRun.get({ spaceId, environmentId, runId }) + + expect(response).toBeInstanceOf(Object) + expect(response.sys.id).toBe(runId) + expect(response.title).toBe('Test Agent Run') + expect(response.sys.status).toBe('COMPLETED') + + expect(httpMock.get).toHaveBeenCalledWith( + `/spaces/${spaceId}/environments/${environmentId}/ai_agents/runs/${runId}`, + expect.objectContaining({ + baseURL: 'https://api.contentful.com', + headers: expect.objectContaining({ + 'x-contentful-enable-alpha-feature': 'agents', + }), + }), + ) + }) + + test('getMany', async () => { + const { httpMock, adapterMock } = setupRestAdapter( + Promise.resolve({ data: { items: [mockAgentRun], total: 1 } }), + ) + const plainClient = createClient({ apiAdapter: adapterMock }, { type: 'plain' }) + const response = await plainClient.agentRun.getMany({ + spaceId, + environmentId, + query: { statusIn: ['COMPLETED'] }, + }) + + expect(response).toBeInstanceOf(Object) + expect(response.items).toBeInstanceOf(Array) + expect(response.items[0].sys.id).toBe(runId) + expect(response.items[0].sys.status).toBe('COMPLETED') + + expect(httpMock.get).toHaveBeenCalledWith( + `/spaces/${spaceId}/environments/${environmentId}/ai_agents/runs`, + expect.objectContaining({ + baseURL: 'https://api.contentful.com', + params: { statusIn: ['COMPLETED'] }, + headers: expect.objectContaining({ + 'x-contentful-enable-alpha-feature': 'agents', + }), + }), + ) + }) + + test('getMany with agentIn filter', async () => { + const { httpMock, adapterMock } = setupRestAdapter( + Promise.resolve({ data: { items: [mockAgentRun], total: 1 } }), + ) + const plainClient = createClient({ apiAdapter: adapterMock }, { type: 'plain' }) + const response = await plainClient.agentRun.getMany({ + spaceId, + environmentId, + query: { agentIn: ['agent-1', 'agent-2'], statusIn: ['COMPLETED', 'IN_PROGRESS'] }, + }) + + expect(response).toBeInstanceOf(Object) + expect(response.items).toBeInstanceOf(Array) + + expect(httpMock.get).toHaveBeenCalledWith( + `/spaces/${spaceId}/environments/${environmentId}/ai_agents/runs`, + expect.objectContaining({ + baseURL: 'https://api.contentful.com', + params: { agentIn: ['agent-1', 'agent-2'], statusIn: ['COMPLETED', 'IN_PROGRESS'] }, + headers: expect.objectContaining({ + 'x-contentful-enable-alpha-feature': 'agents', + }), + }), + ) + }) +}) diff --git a/test/unit/plain/agent.test.ts b/test/unit/plain/agent.test.ts new file mode 100644 index 000000000..1b178ae0f --- /dev/null +++ b/test/unit/plain/agent.test.ts @@ -0,0 +1,98 @@ +import { describe, expect, test } from 'vitest' +import { createClient } from '../../../lib/contentful-management' +import setupRestAdapter from '../adapters/REST/helpers/setupRestAdapter' + +describe('Agent', () => { + const spaceId = 'space-id' + const environmentId = 'env-id' + const agentId = 'agent-id' + + const mockAgent = { + sys: { + type: 'Agent', + id: agentId, + version: 1, + space: { sys: { id: spaceId } }, + environment: { sys: { id: environmentId } }, + createdAt: '2025-12-15T10:00:00.000Z', + updatedAt: '2025-12-15T10:00:00.000Z', + }, + name: 'Test AI Agent', + description: 'Test description', + provider: 'openai', + modelId: 'gpt-4', + } + + test('get', async () => { + const { httpMock, adapterMock } = setupRestAdapter(Promise.resolve({ data: mockAgent })) + const plainClient = createClient({ apiAdapter: adapterMock }, { type: 'plain' }) + const response = await plainClient.agent.get({ spaceId, environmentId, agentId }) + + expect(response).toBeInstanceOf(Object) + expect(response.sys.id).toBe(agentId) + expect(response.name).toBe('Test AI Agent') + + expect(httpMock.get).toHaveBeenCalledWith( + `/spaces/${spaceId}/environments/${environmentId}/ai_agents/agents/${agentId}`, + expect.objectContaining({ + baseURL: 'https://api.contentful.com', + headers: expect.objectContaining({ + 'x-contentful-enable-alpha-feature': 'agents', + }), + }), + ) + }) + + test('getMany', async () => { + const { httpMock, adapterMock } = setupRestAdapter( + Promise.resolve({ data: { items: [mockAgent], total: 1 } }), + ) + const plainClient = createClient({ apiAdapter: adapterMock }, { type: 'plain' }) + const response = await plainClient.agent.getMany({ + spaceId, + environmentId, + }) + + expect(response).toBeInstanceOf(Object) + expect(response.items).toBeInstanceOf(Array) + expect(response.items[0].sys.id).toBe(agentId) + + expect(httpMock.get).toHaveBeenCalledWith( + `/spaces/${spaceId}/environments/${environmentId}/ai_agents/agents`, + expect.objectContaining({ + baseURL: 'https://api.contentful.com', + headers: expect.objectContaining({ + 'x-contentful-enable-alpha-feature': 'agents', + }), + }), + ) + }) + + test('generate', async () => { + const mockResponse = { + result: 'Generated response', + } + const { httpMock, adapterMock } = setupRestAdapter(Promise.resolve({ data: mockResponse })) + const plainClient = createClient({ apiAdapter: adapterMock }, { type: 'plain' }) + + const payload = { + messages: [{ parts: [{ type: 'text' as const, text: 'Hello' }], role: 'user' as const }], + } + + const response = await plainClient.agent.generate({ spaceId, environmentId, agentId }, payload) + + expect(response).toBeInstanceOf(Object) + expect(response.result).toBe('Generated response') + + expect(httpMock.post).toHaveBeenCalledWith( + `/spaces/${spaceId}/environments/${environmentId}/ai_agents/agents/${agentId}/generate`, + payload, + expect.objectContaining({ + baseURL: 'https://api.contentful.com', + headers: expect.objectContaining({ + 'x-contentful-enable-alpha-feature': 'agents', + }), + }), + ) + }) +})