Add OpenAI support for 4.16.0 (#726)

* Add OpenAI support for 4.16.0

* Add support for background polling and use that in OpenAI integration to power assistants

* Much improved OpenAI docs

* Added backgroundPoll docs

* Implements waitForEvent and added docs for more built in tasks

* Add sendEvent API referenc

* Write the task libray

* Add changeset and warning for waitForEvent
This commit is contained in:
Eric Allam
2023-11-09 16:59:58 +00:00
committed by GitHub
parent d02173442c
commit cb1825bfaf
73 changed files with 4691 additions and 874 deletions
+8
View File
@@ -0,0 +1,8 @@
---
"@trigger.dev/integration-kit": patch
"@trigger.dev/sdk": patch
"@trigger.dev/openai": patch
"@trigger.dev/core": patch
---
OpenAI support for 4.16.0
+7
View File
@@ -0,0 +1,7 @@
---
"@trigger.dev/sdk": patch
"@trigger.dev/openai": patch
"@trigger.dev/core": patch
---
Add support for background polling and use that in OpenAI integration to power assistants
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/sdk": patch
---
Adding support for waitForEvent
+7 -1
View File
@@ -11,7 +11,10 @@ const EnvironmentSchema = z.object({
SESSION_SECRET: z.string(),
MAGIC_LINK_SECRET: z.string(),
ENCRYPTION_KEY: z.string(),
WHITELISTED_EMAILS: z.string().refine(isValidRegex, "WHITELISTED_EMAILS must be a valid regex.").optional(),
WHITELISTED_EMAILS: z
.string()
.refine(isValidRegex, "WHITELISTED_EMAILS must be a valid regex.")
.optional(),
REMIX_APP_PORT: z.string().optional(),
LOGIN_ORIGIN: z.string().default("http://localhost:3030"),
APP_ORIGIN: z.string().default("http://localhost:3030"),
@@ -42,6 +45,9 @@ const EnvironmentSchema = z.object({
EXECUTION_WORKER_POLL_INTERVAL: z.coerce.number().int().default(1000),
WORKER_ENABLED: z.string().default("true"),
EXECUTION_WORKER_ENABLED: z.string().default("true"),
TASK_OPERATION_WORKER_ENABLED: z.string().default("true"),
TASK_OPERATION_WORKER_CONCURRENCY: z.coerce.number().int().default(10),
TASK_OPERATION_WORKER_POLL_INTERVAL: z.coerce.number().int().default(1000),
GRACEFUL_SHUTDOWN_TIMEOUT: z.coerce.number().int().default(60000),
});
@@ -0,0 +1,22 @@
import { z } from "zod";
export const JobVersionDispatchableSchema = z.object({
type: z.literal("JOB_VERSION"),
id: z.string(),
});
export const DynamicTriggerDispatchableSchema = z.object({
type: z.literal("DYNAMIC_TRIGGER"),
id: z.string(),
});
export const EphemeralDispatchableSchema = z.object({
type: z.literal("EPHEMERAL"),
url: z.string(),
});
export const DispatchableSchema = z.discriminatedUnion("type", [
JobVersionDispatchableSchema,
DynamicTriggerDispatchableSchema,
EphemeralDispatchableSchema,
]);
+1
View File
@@ -16,6 +16,7 @@ export function taskWithAttemptsToServerTask(task: TaskWithAttempts): ServerTask
description: task.description,
params: task.params as any,
output: task.output as any,
context: task.context as any,
properties: task.properties as any,
style: task.style as any,
error: task.error,
@@ -0,0 +1,59 @@
import type { ActionFunctionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import {
EphemeralEventDispatcherRequestBodySchema,
InvokeJobRequestBodySchema,
} from "@trigger.dev/core";
import { z } from "zod";
import { PrismaErrorSchema } from "~/db.server";
import { authenticateApiRequest } from "~/services/apiAuth.server";
import { CreateEphemeralEventDispatcherService } from "~/services/dispatchers/createEphemeralEventDispatcher.server";
import { InvokeJobService } from "~/services/jobs/invokeJob.server";
import { logger } from "~/services/logger.server";
export async function action({ request, params }: ActionFunctionArgs) {
// Ensure this is a POST request
if (request.method.toUpperCase() !== "POST") {
return { status: 405, body: "Method Not Allowed" };
}
// Authenticate the request
const authenticationResult = await authenticateApiRequest(request);
if (!authenticationResult) {
return json({ error: "Invalid or Missing API Key" }, { status: 401 });
}
// Now parse the request body
const anyBody = await request.json();
logger.debug("CreateEphemeralEventDispatcherService.call() request body", {
body: anyBody,
});
const body = EphemeralEventDispatcherRequestBodySchema.safeParse(anyBody);
if (!body.success) {
return json({ error: "Invalid request body" }, { status: 400 });
}
const service = new CreateEphemeralEventDispatcherService();
try {
const dispatcher = await service.call(authenticationResult.environment, body.data);
if (!dispatcher) {
return json({ error: "Could not create Event Dispatcher" }, { status: 500 });
}
return json({ id: dispatcher.id });
} catch (error) {
const prismaError = PrismaErrorSchema.safeParse(error);
// Record not found in the database
if (prismaError.success && prismaError.data.code === "P2005") {
return json({ error: "Dispatcher not found" }, { status: 404 });
} else {
return json({ error: "Internal Server Error" }, { status: 500 });
}
}
}
@@ -1,6 +1,5 @@
import type { ActionFunctionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import { TaskStatus } from "@trigger.dev/database";
import {
API_VERSIONS,
RunTaskBodyOutput,
@@ -8,15 +7,16 @@ import {
RunTaskResponseWithCachedTasksBody,
ServerTask,
} from "@trigger.dev/core";
import { TaskStatus } from "@trigger.dev/database";
import { z } from "zod";
import { $transaction, PrismaClient, prisma } from "~/db.server";
import { env } from "~/env.server";
import { prepareTasksForCaching, taskWithAttemptsToServerTask } from "~/models/task.server";
import { authenticateApiRequest } from "~/services/apiAuth.server";
import { logger } from "~/services/logger.server";
import { ulid } from "~/services/ulid.server";
import { workerQueue } from "~/services/worker.server";
import { generateSecret } from "~/services/sources/utils.server";
import { env } from "~/env.server";
import { ulid } from "~/services/ulid.server";
import { taskOperationWorker, workerQueue } from "~/services/worker.server";
const ParamsSchema = z.object({
runId: z.string(),
@@ -302,7 +302,7 @@ export class RunTaskService {
if (task.status === "RUNNING" && typeof taskBody.operation === "string") {
// We need to schedule the operation
await workerQueue.enqueue(
await taskOperationWorker.enqueue(
"performTaskOperation",
{
id: task.id,
@@ -0,0 +1,68 @@
import { EphemeralEventDispatcherRequestBody } from "@trigger.dev/core";
import { $transaction, PrismaClient, prisma } from "~/db.server";
import { AuthenticatedEnvironment } from "../apiAuth.server";
import { ExpireDispatcherService } from "./expireDispatcher.server";
export class CreateEphemeralEventDispatcherService {
#prismaClient: PrismaClient;
constructor(prismaClient: PrismaClient = prisma) {
this.#prismaClient = prismaClient;
}
public async call(
environment: AuthenticatedEnvironment,
data: EphemeralEventDispatcherRequestBody
) {
return await $transaction(this.#prismaClient, async (tx) => {
const existingDispatcher = await tx.eventDispatcher.findUnique({
where: {
dispatchableId_environmentId: {
dispatchableId: data.url,
environmentId: environment.id,
},
},
});
if (existingDispatcher) {
return existingDispatcher;
}
const externalAccount = data.accountId
? await this.#prismaClient.externalAccount.upsert({
where: {
environmentId_identifier: {
environmentId: environment.id,
identifier: data.accountId,
},
},
create: {
environmentId: environment.id,
organizationId: environment.organizationId,
identifier: data.accountId,
},
update: {},
})
: undefined;
const dispatcher = await tx.eventDispatcher.create({
data: {
dispatchableId: data.url,
environmentId: environment.id,
source: data.source ?? "trigger.dev",
payloadFilter: data.filter,
contextFilter: data.contextFilter,
dispatchable: { url: data.url, type: "EPHEMERAL" },
enabled: true,
event: typeof data.name === "string" ? [data.name] : data.name,
manual: false,
externalAccountId: externalAccount?.id,
},
});
await ExpireDispatcherService.enqueue(dispatcher.id, data.timeoutInSeconds, tx);
return dispatcher;
});
}
}
@@ -0,0 +1,36 @@
import { PrismaClient, PrismaClientOrTransaction, prisma } from "~/db.server";
import { workerQueue } from "../worker.server";
export class ExpireDispatcherService {
#prismaClient: PrismaClient;
constructor(prismaClient: PrismaClient = prisma) {
this.#prismaClient = prismaClient;
}
public async call(id: string) {
await this.#prismaClient.eventDispatcher.delete({
where: {
id,
},
});
}
static async dequeue(id: string, tx?: PrismaClientOrTransaction) {
await workerQueue.dequeue(`expire:${id}`, { tx });
}
static async enqueue(id: string, timeoutInSeconds: number, tx?: PrismaClientOrTransaction) {
await workerQueue.enqueue(
"expireDispatcher",
{
id,
},
{
tx,
runAt: new Date(Date.now() + 1000 * timeoutInSeconds),
jobKey: `expire:${id}`,
}
);
}
}
@@ -0,0 +1,108 @@
import { PrismaClient, PrismaClientOrTransaction, prisma } from "~/db.server";
import { taskOperationWorker } from "../worker.server";
import { EphemeralDispatchableSchema } from "~/models/eventDispatcher.server";
import { fetch } from "@whatwg-node/fetch";
import { ExpireDispatcherService } from "./expireDispatcher.server";
export class InvokeEphemeralDispatcherService {
#prismaClient: PrismaClient;
constructor(prismaClient: PrismaClient = prisma) {
this.#prismaClient = prismaClient;
}
public async call(id: string, eventRecordId: string) {
const eventDispatcher = await this.#prismaClient.eventDispatcher.findUnique({
where: {
id,
},
});
if (!eventDispatcher) {
return;
}
if (!eventDispatcher.enabled) {
return;
}
const eventRecord = await this.#prismaClient.eventRecord.findUnique({
where: {
id: eventRecordId,
},
include: {
externalAccount: true,
},
});
if (!eventRecord) {
return;
}
if (eventRecord.cancelledAt) {
return;
}
const dispatchable = EphemeralDispatchableSchema.safeParse(eventDispatcher.dispatchable);
if (!dispatchable.success) {
return;
}
const url = dispatchable.data.url;
const body = {
id: eventRecord.eventId,
source: eventRecord.source,
name: eventRecord.name,
payload: eventRecord.payload,
context: eventRecord.context,
timestamp: eventRecord.timestamp,
accountId: eventRecord.externalAccount ? eventRecord.externalAccount.identifier : undefined,
};
const abortController = new AbortController();
const timeoutId = setTimeout(() => {
abortController.abort();
}, 5000);
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json; charset=utf-8",
},
body: JSON.stringify(body),
signal: abortController.signal,
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(
`Failed to invoke ephemeral dispatcher: ${response.statusText} [${response.status}]`
);
}
// Run the expire dispatcher service
await ExpireDispatcherService.enqueue(id, 0);
}
static async dequeue(id: string, tx?: PrismaClientOrTransaction) {
await taskOperationWorker.dequeue(`invoke:ephemeral:${id}`, { tx });
}
static async enqueue(id: string, eventRecordId: string, tx?: PrismaClientOrTransaction) {
await taskOperationWorker.enqueue(
"invokeEphemeralDispatcher",
{
id,
eventRecordId,
},
{
tx,
jobKey: `invoke:ephemeral:${id}`,
}
);
}
}
@@ -96,6 +96,13 @@ export class DeliverEventService {
return true;
}
if (
dispatcher.externalAccountId &&
dispatcher.externalAccountId !== eventRecord.externalAccountId
) {
return false;
}
const payloadFilter = EventFilterSchema.safeParse(dispatcher.payloadFilter ?? {});
const contextFilter = EventFilterSchema.safeParse(dispatcher.contextFilter ?? {});
@@ -3,21 +3,8 @@ import type { PrismaClientOrTransaction } from "~/db.server";
import { prisma } from "~/db.server";
import { logger } from "~/services/logger.server";
import { CreateRunService } from "~/services/runs/createRun.server";
const JobVersionDispatchableSchema = z.object({
type: z.literal("JOB_VERSION"),
id: z.string(),
});
const DynamicTriggerDispatchableSchema = z.object({
type: z.literal("DYNAMIC_TRIGGER"),
id: z.string(),
});
const DispatchableSchema = z.discriminatedUnion("type", [
JobVersionDispatchableSchema,
DynamicTriggerDispatchableSchema,
]);
import { InvokeEphemeralDispatcherService } from "../dispatchers/invokeEphemeralEventDispatcher.server";
import { DispatchableSchema } from "~/models/eventDispatcher.server";
export class InvokeDispatcherService {
#prismaClient: PrismaClientOrTransaction;
@@ -142,6 +129,11 @@ export class InvokeDispatcherService {
});
}
break;
}
case "EPHEMERAL": {
await InvokeEphemeralDispatcherService.enqueue(eventDispatcher.id, eventRecord.id);
break;
}
}
@@ -1,20 +1,26 @@
import {
FetchOperationSchema,
FetchPollOperationSchema,
FetchRequestInit,
FetchRetryOptions,
FetchRetryStrategy,
RedactString,
RetryOptions,
calculateResetAt,
calculateRetryAt,
eventFilterMatches,
responseFilterMatches,
} from "@trigger.dev/core";
import { type Task } from "@trigger.dev/database";
import { $transaction, PrismaClient, PrismaClientOrTransaction, prisma } from "~/db.server";
import { formatUnknownError } from "~/utils/formatErrors.server";
import { safeJsonFromResponse } from "~/utils/json";
import { logger } from "../logger.server";
import { workerQueue } from "../worker.server";
import { taskOperationWorker, workerQueue } from "../worker.server";
import { ResumeTaskService } from "./resumeTask.server";
import { fetch } from "@whatwg-node/fetch";
import { fromZodError } from "zod-validation-error";
import { ulid } from "../ulid.server";
type FoundTask = Awaited<ReturnType<typeof findTask>>;
@@ -32,16 +38,166 @@ export class PerformTaskOperationService {
return;
}
if (task.status === "CANCELED") {
return;
}
if (task.status === "COMPLETED" || task.status === "ERRORED") {
return await this.#resumeRunExecution(task, this.#prismaClient);
}
if (!task.operation) {
return await this.#resumeTask(task, null, 0);
return await this.#resumeTask(task, null, null, 200, "fetch", 0);
}
switch (task.operation) {
case "fetch": {
case "fetch-poll": {
const pollOperation = FetchPollOperationSchema.safeParse(task.params);
if (!pollOperation.success) {
return await this.#resumeTaskWithError(
task,
fromZodError(pollOperation.error, {
prefix: "Invalid fetch poll params",
}).message
);
}
const { url, requestInit, timeout, interval, responseFilter, requestTimeout } =
pollOperation.data;
// check if we need to fail the task because it's timed out
const startedAt = task.startedAt;
if (!startedAt) {
return await this.#resumeTaskWithError(task, {
message: "Task has not been started",
});
}
if (Date.now() - startedAt.getTime() > timeout * 1000) {
return await this.#resumeTaskWithError(task, {
message: `Task timed out after ${timeout} seconds`,
});
}
const startTimeInMs = performance.now();
const abortController = new AbortController();
// calculate the actual timeout. If timeoutInMs is undefined, we use the default of 5s
// Also make sure the timeout is at least 1s, but not bigger than 5s
const actualTimeoutInMs = Math.min(
Math.max(requestTimeout?.durationInMs ?? 5000, 1000),
5000
);
const timeoutId = setTimeout(() => {
abortController.abort();
}, actualTimeoutInMs);
try {
logger.debug("PerformTaskOperationService.call poll request", {
task,
actualTimeoutInMs,
url,
responseFilter,
});
const startedAt = new Date();
const method = requestInit?.method ?? "GET";
const response = await fetch(url, {
method,
headers: normalizeHeaders(requestInit?.headers ?? {}),
body: requestInit?.body,
signal: abortController.signal,
});
clearTimeout(timeoutId);
const durationInMs = Math.floor(performance.now() - startTimeInMs);
const headers = Object.fromEntries(response.headers.entries());
logger.debug("PerformTaskOperationService.call poll response", {
url,
requestInit,
statusCode: response.status,
headers: Object.fromEntries(response.headers.entries()),
durationInMs,
});
const matchResult = await responseFilterMatches(response, responseFilter);
await this.#prismaClient.task.create({
data: {
id: ulid(),
idempotencyKey: ulid(),
runId: task.runId,
parentId: task.id,
name: "poll attempt",
icon: "activity",
status: "COMPLETED",
noop: true,
style: { style: "minimal", variant: "info" },
description: `${method} ${url} ${response.status}`,
params: {
status: response.status,
headers,
body: matchResult.body as any,
},
startedAt,
completedAt: new Date(),
},
});
if (matchResult.match) {
logger.debug("PerformTaskOperationService.call poll response matched", {
url,
matchResult,
});
return await this.#resumeTask(
task,
matchResult.body,
Object.fromEntries(response.headers.entries()),
response.status,
"fetch",
durationInMs
);
} else {
const retryAt = new Date(Date.now() + interval * 1000);
return await this.#retryTask(task, retryAt);
}
} catch (error) {
if (error instanceof Error && error.name === "AbortError") {
const durationInMs = Math.floor(performance.now() - startTimeInMs);
logger.debug("PerformTaskOperationService.call poll timed out", {
url,
durationInMs,
error,
});
const retryAt = this.#calculateRetryForTimeout(task, requestTimeout?.retry);
if (retryAt) {
return await this.#retryTask(task, retryAt);
}
return await this.#resumeTaskWithError(task, {
message: `Fetch timed out after ${actualTimeoutInMs.toFixed(0)}ms`,
});
}
throw error;
}
}
case "fetch":
case "fetch-response": {
const fetchOperation = FetchOperationSchema.safeParse(task.params);
if (!fetchOperation.success) {
@@ -97,7 +253,7 @@ export class PerformTaskOperationService {
});
if (!response.ok) {
const retryAt = this.#calculateRetryForResponse(task, retry, response);
const retryAt = this.#calculateRetryForResponse(task, retry, response, jsonBody);
if (retryAt) {
return await this.#retryTaskWithError(
@@ -117,7 +273,14 @@ export class PerformTaskOperationService {
}
}
return await this.#resumeTask(task, jsonBody, durationInMs);
return await this.#resumeTask(
task,
jsonBody,
Object.fromEntries(response.headers.entries()),
response.status,
task.operation,
durationInMs
);
} catch (error) {
if (error instanceof Error && error.name === "AbortError") {
const durationInMs = Math.floor(performance.now() - startTimeInMs);
@@ -157,13 +320,14 @@ export class PerformTaskOperationService {
#calculateRetryForResponse(
task: NonNullable<FoundTask>,
retry: FetchRetryOptions | undefined,
response: Response
response: Response,
body: any
): Date | undefined {
if (!retry) {
return;
}
const strategy = this.#getRetryStrategyForStatusCode(response.status, retry);
const strategy = this.#getRetryStrategyForResponse(response, body, retry);
if (!strategy) {
return;
@@ -180,11 +344,10 @@ export class PerformTaskOperationService {
return calculateRetryAt(strategy, task.attempts.length - 1);
}
case "headers": {
const remaining = response.headers.get(strategy.remainingHeader);
const resetAt = response.headers.get(strategy.resetHeader);
if (typeof remaining === "string" && typeof resetAt === "string" && remaining === "0") {
return new Date(Number(resetAt) * 1000 + addJitterInMs());
if (typeof resetAt === "string") {
return calculateResetAt(resetAt, strategy.resetFormat);
}
}
}
@@ -201,8 +364,9 @@ export class PerformTaskOperationService {
return calculateRetryAt(retry, task.attempts.length - 1);
}
#getRetryStrategyForStatusCode(
statusCode: number,
#getRetryStrategyForResponse(
response: Response,
body: any,
retry: FetchRetryOptions
): FetchRetryStrategy | undefined {
const statusCodes = Object.keys(retry);
@@ -211,7 +375,19 @@ export class PerformTaskOperationService {
const statusRange = statusCodes[i];
const strategy = retry[statusRange];
if (isStatusCodeInRange(statusCode, statusRange)) {
if (isStatusCodeInRange(response.status, statusRange)) {
if (strategy.bodyFilter) {
if (!body) {
continue;
}
if (eventFilterMatches(body, strategy.bodyFilter)) {
return strategy;
} else {
continue;
}
}
return strategy;
}
}
@@ -248,16 +424,26 @@ export class PerformTaskOperationService {
},
});
await workerQueue.enqueue(
await taskOperationWorker.enqueue(
"performTaskOperation",
{
id: task.id,
},
{ tx, runAt: retryAt }
{ tx, runAt: retryAt, jobKey: `operation:${task.id}` }
);
});
}
async #retryTask(task: Task, retryAt: Date) {
await taskOperationWorker.enqueue(
"performTaskOperation",
{
id: task.id,
},
{ runAt: retryAt, jobKey: `operation:${task.id}` }
);
}
async #resumeTaskWithError(task: NonNullable<FoundTask>, output: any) {
await $transaction(this.#prismaClient, async (tx) => {
await tx.task.update({
@@ -284,7 +470,14 @@ export class PerformTaskOperationService {
});
}
async #resumeTask(task: NonNullable<FoundTask>, output: any, durationInMs: number) {
async #resumeTask(
task: NonNullable<FoundTask>,
output: any,
context: any,
status: number,
operation: "fetch" | "fetch-response",
durationInMs: number
) {
await $transaction(this.#prismaClient, async (tx) => {
await tx.taskAttempt.updateMany({
where: {
@@ -296,12 +489,22 @@ export class PerformTaskOperationService {
},
});
const taskOutput =
operation === "fetch"
? output
: {
data: output,
headers: context,
status,
};
await tx.task.update({
where: { id: task.id },
data: {
status: "COMPLETED",
completedAt: new Date(),
output: output ? output : undefined,
output: taskOutput,
context: context ? context : undefined,
run: {
update: {
executionDuration: {
+75 -13
View File
@@ -24,6 +24,8 @@ import { ProbeEndpointService } from "./endpoints/probeEndpoint.server";
import { DeliverRunSubscriptionService } from "./runs/deliverRunSubscription.server";
import { DeliverRunSubscriptionsService } from "./runs/deliverRunSubscriptions.server";
import { ResumeTaskService } from "./tasks/resumeTask.server";
import { ExpireDispatcherService } from "./dispatchers/expireDispatcher.server";
import { InvokeEphemeralDispatcherService } from "./dispatchers/invokeEphemeralEventDispatcher.server";
const workerCatalog = {
indexEndpoint: z.object({
@@ -40,9 +42,6 @@ const workerCatalog = {
processCallbackTimeout: z.object({
id: z.string(),
}),
performTaskOperation: z.object({
id: z.string(),
}),
deliverHttpSourceRequest: z.object({ id: z.string() }),
refreshOAuthToken: z.object({
organizationId: z.string(),
@@ -93,6 +92,9 @@ const workerCatalog = {
resumeTask: z.object({
id: z.string(),
}),
expireDispatcher: z.object({
id: z.string(),
}),
};
const executionWorkerCatalog = {
@@ -108,12 +110,24 @@ const executionWorkerCatalog = {
}),
};
const taskOperationWorkerCatalog = {
performTaskOperation: z.object({
id: z.string(),
}),
invokeEphemeralDispatcher: z.object({
id: z.string(),
eventRecordId: z.string(),
}),
};
let workerQueue: ZodWorker<typeof workerCatalog>;
let executionWorker: ZodWorker<typeof executionWorkerCatalog>;
let taskOperationWorker: ZodWorker<typeof taskOperationWorkerCatalog>;
declare global {
var __worker__: ZodWorker<typeof workerCatalog>;
var __executionWorker__: ZodWorker<typeof executionWorkerCatalog>;
var __taskOperationWorker__: ZodWorker<typeof taskOperationWorkerCatalog>;
}
// this is needed because in development we don't want to restart
@@ -123,6 +137,7 @@ declare global {
if (env.NODE_ENV === "production") {
workerQueue = getWorkerQueue();
executionWorker = getExecutionWorkerQueue();
taskOperationWorker = getTaskOperationWorkerQueue();
} else {
if (!global.__worker__) {
global.__worker__ = getWorkerQueue();
@@ -134,6 +149,12 @@ if (env.NODE_ENV === "production") {
}
executionWorker = global.__executionWorker__;
if (!global.__taskOperationWorker__) {
global.__taskOperationWorker__ = getTaskOperationWorkerQueue();
}
taskOperationWorker = global.__taskOperationWorker__;
}
export async function init() {
@@ -148,6 +169,10 @@ export async function init() {
if (env.EXECUTION_WORKER_ENABLED === "true") {
await executionWorker.initialize();
}
if (env.TASK_OPERATION_WORKER_ENABLED === "true") {
await taskOperationWorker.initialize();
}
}
function getWorkerQueue() {
@@ -286,15 +311,6 @@ function getWorkerQueue() {
await service.call(payload.id);
},
},
performTaskOperation: {
priority: 0, // smaller number = higher priority
maxAttempts: 3,
handler: async (payload, job) => {
const service = new PerformTaskOperationService();
await service.call(payload.id);
},
},
scheduleEmail: {
priority: 100,
maxAttempts: 3,
@@ -375,6 +391,15 @@ function getWorkerQueue() {
handler: async (payload, job) => {
const service = new ResumeTaskService();
return await service.call(payload.id);
},
},
expireDispatcher: {
priority: 10,
maxAttempts: 3,
handler: async (payload) => {
const service = new ExpireDispatcherService();
return await service.call(payload.id);
},
},
@@ -428,4 +453,41 @@ function getExecutionWorkerQueue() {
});
}
export { executionWorker, workerQueue };
function getTaskOperationWorkerQueue() {
return new ZodWorker({
name: "taskOperationWorker",
prisma,
runnerOptions: {
connectionString: env.DATABASE_URL,
concurrency: env.TASK_OPERATION_WORKER_CONCURRENCY,
pollInterval: env.TASK_OPERATION_WORKER_POLL_INTERVAL,
noPreparedStatements: env.DATABASE_URL !== env.DIRECT_URL,
schema: env.WORKER_SCHEMA,
maxPoolSize: env.TASK_OPERATION_WORKER_CONCURRENCY,
},
shutdownTimeoutInMs: env.GRACEFUL_SHUTDOWN_TIMEOUT,
schema: taskOperationWorkerCatalog,
tasks: {
performTaskOperation: {
priority: 0, // smaller number = higher priority
maxAttempts: 3,
handler: async (payload, job) => {
const service = new PerformTaskOperationService();
await service.call(payload.id);
},
},
invokeEphemeralDispatcher: {
priority: 0, // smaller number = higher priority
maxAttempts: 10,
handler: async (payload, job) => {
const service = new InvokeEphemeralDispatcherService();
await service.call(payload.id, payload.eventRecordId);
},
},
},
});
}
export { executionWorker, workerQueue, taskOperationWorker };
+255
View File
@@ -0,0 +1,255 @@
---
title: "Task Library"
description: "These are the built-in tasks that are available to use in your Jobs."
---
Welcome to the Trigger.dev Task Library 📚. You may be wondering, what is a Task and why are there a library of them? Well you see, Trigger.dev works by divvying up a long-running job execution into a bunch of little tasks, each one taking less time then a single serverless function execution. 💫
You can define and run your own tasks easily using [io.runTask()](/sdk/io/runtask), or you can use one of our [Integrations](/integrations/introduction) which are tasks for specific APIs, like OpenAI or Stripe.
<Note>Read more about how Tasks work [here](/documentation/concepts/tasks).</Note>
We also have a growing library of built-in tasks that you can use in your Jobs through the `io` object. These tasks are designed to be generic and reusable, and are a great way to get started with Trigger.dev.
<Info>
You may notice that I'm using emojis for all the cache keys below, which is totally 💯% fine as
long as they are unique inside a run. Read more about how cache keys work
[here](/documentation/concepts/tasks#task-cache-keys)
</Info>
## `wait`
This task allows you to resume executing your job after a certain amount of time has passed:
```ts
await io.wait("⏰", 60); // wait 60 seconds
```
Internally this task is considered a "noop", and noop tasks have no output.
[reference docs](/sdk/io/wait)
## `waitForRequest`
You supply this task with a callback to receive a URL. When a POST request is made to that URL, the JSON body of the request becomes the task output.
The example below uses `waitForRequest` to capture a Screenshot of a website using [ScreenshotOne.com](https://screenshotone.com/) and passes the callback URL to the webhook URL to get notified when the screenshot is finished:
```ts
const result = await io.waitForRequest<ScreenshotResponse>(
"📸",
async (url) => {
await fetch(`https://api.screenshotone.com/take`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
webhook_url: url, // this is the URL that will be called when the screenshot is ready
access_key: "my-access-key",
url: "https://trigger.dev",
store: "true",
storage_path: "my-screeshots",
response_type: "json",
async: "true",
storage_return_location: "true",
}),
});
},
{
timeoutInSeconds: 300, // wait up to 5 minutes for the screenshot to be ready
}
);
```
We actually originally built this task for our [Replicate integration](/integrations/apis/replicate), which accepts a callback URL to notify you when a prediction is ready. So this allows you to write very succinct code to create a prediction and wait for it's results:
```ts
const sdPrediction = await io.replicate.predictions.createAndAwait("🧑‍🎨", {
version: "ac732df83cea7fff18b8472768c88ad041fa750ff7682a21affe81863cbe77e4",
input: {
prompt: "What is the meaning of life?",
},
});
```
[reference docs](/sdk/io/wait-for-request)
## `waitForEvent`
This task allows you to wait for an event to be sent. To read about how events work, check out the [Events](/documentation/concepts/triggers/events) documentation.
```ts
const event = await io.waitForEvent(
"🥂",
{
name: "user.created",
schema: z.object({
id: z.string(),
createdAt: z.coerce.date(),
isAdmin: z.boolean(),
}),
filter: {
isAdmin: [true], // Only wait for events where isAdmin is true
},
},
{
timeoutInSeconds: 60 * 60, // Wait for up to an hour
}
);
```
The event object returned from this task is the full event object that was sent, including `id`, `name`, `payload`, `context`, and more.
[reference docs](/sdk/io/wait-for-event)
## `backgroundFetch`
This task allows you to perform a `fetch` request in the background, and then resume the execution of your job after the request has completed.
```ts
const body = io.backgroundFetch<MyResponseData>("🕸️", "https://example.com/api/endpoint", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: redactString`Bearer ${auth.apiKey}`,
},
body: JSON.stringify({ foo: "bar" }),
});
```
This is useful for when an API is slow to respond and might not finish before your serverless function times out. We created this task to power our [OpenAI integration](/integrations/apis/openai), which can sometimes take more than a minute to respond:
```ts
// This uses backgroundFetch under the hood
await io.openai.chat.completions.backgroundCreate("💬", {
model: "gpt-3.5-turbo",
messages: [
{
role: "user",
content: "Create a good programming joke about background jobs",
},
],
});
```
[reference docs](/sdk/io/backgroundfetch)
## `backgroundPoll`
This task is similar to `backgroundFetch`, but instead of waiting for a single request to complete, it will poll a URL until it returns a certain value.
```ts
const result = await io.backgroundPoll<{ foo: string }>("🔃", {
url: "https://example.com/api/endpoint",
interval: 10, // every 10 seconds
timeout: 300, // stop polling after 5 minutes
responseFilter: {
// stop polling once this filter matches
status: [200],
body: {
status: ["SUCCESS"],
},
},
});
```
## `logger`
The logger object allows you to log messages to the Trigger.dev console. This is useful for debugging your jobs, or just to see what's going on inside your job.
```ts
await io.logger.info("This is an info message");
```
You can optionally pass a `context` object to the logger, which will be displayed in the console:
```ts
await io.logger.info("This is an info message", {
foo: "bar",
});
```
We support the following log levels:
- `io.logger.debug()`
- `io.logger.info()`
- `io.logger.warn()`
- `io.logger.error()`
<Note>
You may notice these tasks don't include cache keys. We automatically create a cache key for you
based on the message and the log-level
</Note>
[reference docs](/sdk/io/logger)
## `random`
Use this task to generate a random number that stays stable during run retries/resumes:
```ts
const randomNumber = await io.random("🎲", {
min: 1,
max: 100,
});
```
[reference docs](/sdk/io/random)
## `sendEvent`
This task allows you to send an event from inside your job run.
If you want to send an event from outside a run (e.g. just from your backend) you should use [client.sendEvent()](/sdk/triggerclient/instancemethods/sendevent) instead.
```ts
await io.sendEvent("🚚", {
id: "e_1234567890",
name: "new.user",
payload: {
userId: "u_1234567890",
},
});
```
[reference docs](/sdk/io/sendevent)
## `getEvent`
This task allows you to get an event by ID from inside your job run.
If you want to get an event from outside a run (e.g. just from your backend) you should use [client.getEvent()](/sdk/triggerclient/instancemethods/getevent) instead.
```ts
const event = await io.getEvent("📥", "e_1234567890");
```
[reference docs](/sdk/io/getevent)
## `cancelEvent`
If you send an event that has a delivery date in the future, you can use this task to cancel it.
```ts
await io.sendEvent(
"🚚",
{
id: "e_1234567890",
name: "new.user",
payload: {
userId: "u_1234567890",
},
},
{
deliverAt: new Date(Date.now() + 1000 * 60 * 60 * 24), // deliver in 24 hours
}
);
// Later on, if you want to cancel the event:
await io.cancelEvent("🚫", "e_1234567890");
```
## `createStatus`
Coming soon
-643
View File
@@ -1,643 +0,0 @@
---
title: OpenAI tasks
sidebarTitle: Tasks
---
Tasks are executed after the job is triggered and are the main building blocks of a job. You can string together as many tasks as you want.
---
## All tasks
### `createCompletion`
Generates text completions as per given prompt. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/chat)
```ts example.ts
run: async (payload, io, ctx) => {
// This code demonstrates using OpenAI's text completion with the "davinci" model.
// It generates text based on the given prompt.
await io.openai.createCompletion("completion", {
model: "davinci",
prompt: "Once upon a time",
});
},
```
### `backgroundCreateCompletion`
Generates text completions in the background. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/chat)
```ts example.ts
run: async (payload, io, ctx) => {
// This code showcases background text completion using the "gpt-3.5-turbo" model.
// It generates text based on the provided programming task and logs the result.
const programmingTask = `Create a function that checks if a string is a palindrome.`;
const response = await io.openai.backgroundCreateCompletion("background-completion", {
model: "gpt-3.5-turbo",
prompt: `Coding task: ${programmingTask}\n\n`,
});
await io.logger.info("codeSnippet", response.choices[0]?.text);
},
```
You can also pass an optional third parameter to `backgroundCreateCompletion` to specify OpenAI request options:
```ts requestOptions.ts
run: async (payload, io, ctx) => {
// This code showcases background text completion using the "gpt-3.5-turbo" model.
// It generates text based on the provided programming task and logs the result.
const programmingTask = `Create a function that checks if a string is a palindrome.`;
const response = await io.openai.backgroundCreateCompletion("background-completion", {
model: "gpt-3.5-turbo",
prompt: `Coding task: ${programmingTask}\n\n`,
}, {
headers: {
"User-Agent": "my-user-agent"
}
});
await io.logger.info("codeSnippet", response.choices[0]?.text);
},
```
This task is implemented using [io.backgroundFetch()](/sdk/io/backgroundfetch) and so you can also pass a 4th parameter customizing the retry and timeout options:
```ts fetchOptions.ts
run: async (payload, io, ctx) => {
// This code showcases background text completion using the "gpt-3.5-turbo" model.
// It generates text based on the provided programming task and logs the result.
const programmingTask = `Create a function that checks if a string is a palindrome.`;
const response = await io.openai.backgroundCreateCompletion("background-completion", {
model: "gpt-3.5-turbo",
prompt: `Coding task: ${programmingTask}\n\n`,
}, {
headers: {
"User-Agent": "my-user-agent"
}
}, {
timeout: {
durationInMs: 10000,
retry: {
limit: 3,
minTimeoutInMs: 1000,
factor: 2,
}
}
});
await io.logger.info("codeSnippet", response.choices[0]?.text);
},
```
### `createChatCompletion`
Generates text completions in a conversational context. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/chat/create)
```ts example.ts
run: async (payload, io, ctx) => {
// This code demonstrates chat completion with the "gpt-3.5-turbo" model.
// It simulates a conversation by providing messages and receiving a chat response.
await io.openai.createChatCompletion("chat-completion", {
model: "gpt-3.5-turbo",
messages: [
{
role: "user",
content: "Create a good programming joke about background jobs",
},
],
});
},
```
### `backgroundCreateChatCompletion`
Generates text completions in a conversational context in the background. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/chat/object)
```ts example.ts
run: async (payload, io, ctx) => {
// This code showcases background chat completion using the "gpt-3.5-turbo" model.
// It simulates a conversation with a user message and logs the response choices.
const response = await io.openai.backgroundCreateChatCompletion("background-chat-completion", {
model: "gpt-3.5-turbo",
messages: [
{
role: "user",
content: "Create a good programming joke about background jobs",
},
],
});
await io.logger.info("choices", response.choices);
},
```
You can also use the more "fluent" pattern used by the OpenAI SDK:
```ts fluent.ts
run: async (payload, io, ctx) => {
// This code showcases background chat completion using the "gpt-3.5-turbo" model.
// It simulates a conversation with a user message and logs the response choices.
const response = await io.openai.chat.completions.backgroundCreate("background-chat-completion", {
model: "gpt-3.5-turbo",
messages: [
{
role: "user",
content: "Create a good programming joke about background jobs",
},
],
});
await io.logger.info("choices", response.choices);
},
```
Additionally, you can pass an optional third parameter to specify OpenAI request options:
```ts requestOptions.ts
run: async (payload, io, ctx) => {
// This code showcases background text completion using the "gpt-3.5-turbo" model.
// It generates text based on the provided programming task and logs the result.
const programmingTask = `Create a function that checks if a string is a palindrome.`;
const response = await io.openai.chat.completions.backgroundCreate("background-completion", {
model: "gpt-3.5-turbo",
messages: [
{
role: "user",
content: "Create a good programming joke about background jobs",
},
],
}, {
headers: {
"User-Agent": "my-user-agent"
}
});
await io.logger.info("choices", response.choices);
},
```
This task is implemented using [io.backgroundFetch()](/sdk/io/backgroundfetch) and so you can also pass a 4th parameter customizing the retry and timeout options:
```ts fetchOptions.ts
run: async (payload, io, ctx) => {
// This code showcases background text completion using the "gpt-3.5-turbo" model.
// It generates text based on the provided programming task and logs the result.
const programmingTask = `Create a function that checks if a string is a palindrome.`;
const response = await io.openai.chat.completions.backgroundCreate("background-completion", {
model: "gpt-3.5-turbo",
messages: [
{
role: "user",
content: "Create a good programming joke about background jobs",
},
],
}, {
headers: {
"User-Agent": "my-user-agent"
}
}, {
timeout: {
durationInMs: 10000,
retry: {
limit: 3,
minTimeoutInMs: 1000,
factor: 2,
}
}
});
await io.logger.info("choices", response.choices);
},
```
### `retrieveModel`
Retrieves a specific model by ID. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/models/retrieve)
```ts example.ts
run: async (payload, io, ctx) => {
// In this code snippet, we retrieve detailed information about a specific OpenAI model.
// Specify the ID of the model you want to retrieve. Replace 'your_model_id' with the actual model ID.
const modelIdToRetrieve = "your_model_id";
try {
// Retrieve the model information using the OpenAI API
const retrievedModel = await io.openai.retrieveModel("get-model", {
model: modelIdToRetrieve,
});
// Log the detailed model information
await io.logger.info("retrievedModel", retrievedModel);
} catch (error) {
// Handle errors, such as if the model with the provided ID does not exist.
await io.logger.error("Error retrieving model:", error.message);
}
},
```
### `listModels`
Lists the available models. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/models/list)
```ts example.ts
run: async (payload, io, ctx) => {
// This code lists available models without retrieving detailed information.
const models = await io.openai.listModels("list-models");
},
```
### `createEdit`
Edits a given text prompt. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/edits/create)
```ts example.ts
run: async (payload, io, ctx) => {
// This code snippet demonstrates using the OpenAI API to create an edit task.
// Specify the task parameters:
const editTaskParams = {
model: "text-davinci-edit-001", // Replace with the desired model
input: "Thsi is ridddled with erors", // Replace with the input text
instruction: "Fix the spelling errors", // Replace with the editing instruction
};
try {
// Create an edit task using the OpenAI API
const editResponse = await io.openai.createEdit("edit", editTaskParams);
// Log the response
await io.logger.info("editResponse", editResponse);
} catch (error) {
// Handle any potential errors that may occur during the API request.
await io.logger.error("Error creating edit task:", error.message);
}
},
```
### `createImage`
Generates images from textual descriptions. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/images/create)
```ts example.ts
run: async (payload, io, ctx) => {
const imageResults = await io.openai.createImage("image", {
prompt: "A hedgehog wearing a party hat",
n: 2,
size: "256x256",
response_format: "url",
});
```
### `createImageEdit`
Creates an edited or extended image given an original image and a prompt. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/images/createEdit)
```ts example.ts
run: async (payload, io, ctx) => {
// Specify the parameters for the image edit
const imageEditParams = {
style: "data:image/png;base64,base64_encoded_style_image",
content: "data:image/png;base64,base64_encoded_content_image",
};
// Create the image edit using the OpenAI API
const imageEditResponse = await io.openai.createImageEdit(imageEditParams);
// Log the response
await io.logger.info("imageEditResponse", imageEditResponse);
},
```
### `createImageVariation`
Creates a variation of a given image. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/images/createVariation)
```ts example.ts
run: async (payload, io, ctx) => {
// Specify the parameters for creating an image variation
const imageVariationParams = {
image: "data:image/png;base64,base64_encoded_image",
variation: "brightness(1.2) contrast(0.8) rotate(45deg)",
};
// Create the image variation using the OpenAI API
const imageVariationResponse = await io.openai.createImageVariation(imageVariationParams);
// Log the response
await io.logger.info("imageVariationResponse", imageVariationResponse);
},
```
### `createEmbedding`
Generates embeddings for a given text. [Official OpenAI Docs](hhttps://platform.openai.com/docs/api-reference/embeddings/object)
```ts example.ts
run: async (payload, io, ctx) => {
// This code snippet demonstrates using the OpenAI API to create a text embedding.
// Specify the task parameters:
const embeddingTaskParams = {
model: "text-embedding-ada-002", // Replace with the desired model
input: "The food was delicious and the waiter...", // Replace with the input text
};
try {
// Create a text embedding using the OpenAI API
const embeddingResponse = await io.openai.createEmbedding("embedding", embeddingTaskParams);
// Log the response
await io.logger.info("embeddingResponse", embeddingResponse);
} catch (error) {
// Handle any potential errors that may occur during the API request.
await io.logger.error("Error creating text embedding:", error.message);
}
},
```
### `createFile`
Uploads a file to the OpenAI API. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/files/object)
```ts example.ts
run: async (payload, io, ctx) => {
// Specify the parameters for creating a file
const fileParams = {
name: "example.txt",
content: "This is the content of the file.",
};
// Create the file using the OpenAI API
const fileResponse = await io.openai.createFile(fileParams);
// Log the response
await io.logger.info("fileResponse", fileResponse);
},
```
### `listFiles`
Lists the uploaded files. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/files/list)
```ts example.ts
run: async (payload, io, ctx) => {
// List the files available in your OpenAI account
const fileListResponse = await io.openai.listFiles();
// Log the list of files
await io.logger.info("fileListResponse", fileListResponse);
},
```
### `createFineTuneFile`
Uploads a file for fine-tuning a model. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning)
```ts example.ts
run: async (payload, io, ctx) => {
// Specify the parameters for creating a fine-tune file
const fineTuneFileParams = {
model: "text-davinci-002",
prompt: "Translate English to French: 'Hello, world.'",
language: "en",
description: "Fine-tune file for translation task",
};
// Create the fine-tune file using the OpenAI API
const fineTuneFileResponse = await io.openai.createFineTuneFile(fineTuneFileParams);
// Log the response
await io.logger.info("fineTuneFileResponse", fineTuneFileResponse);
},
```
### `createFineTune`
Fine-tunes a model on a given task. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning/create)
```ts example.ts
run: async (payload, io, ctx) => {
// Specify the parameters for creating a fine-tune task
const fineTuneParams = {
model: "text-davinci-002",
dataset: "your_dataset_id",
description: "Fine-tune task for custom dataset",
};
// Create the fine-tune task using the OpenAI API
const fineTuneResponse = await io.openai.createFineTune(fineTuneParams);
// Log the response
await io.logger.info("fineTuneResponse", fineTuneResponse);
},
```
### `listFineTunes`
Lists the available fine-tunes. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning/list)
```ts example.ts
run: async (payload, io, ctx) => {
// List the fine-tunes available in your OpenAI account
const fineTunesListResponse = await io.openai.listFineTunes();
// Log the list of fine-tunes
await io.logger.info("fineTunesListResponse", fineTunesListResponse);
},
```
### `retrieveFineTune`
Retrieves a specific fine-tune by ID. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning/retrieve)
```ts example.ts
run: async (payload, io, ctx) => {
// Specify the ID of the fine-tune you want to retrieve
const fineTuneId = "your_fine_tune_id"; // Replace with the actual fine-tune ID
// Retrieve the fine-tune using the OpenAI API
const retrievedFineTune = await io.openai.retrieveFineTune(fineTuneId);
// Log the retrieved fine-tune
await io.logger.info("retrievedFineTune", retrievedFineTune);
},
```
### `cancelFineTune`
Cancels a specific fine-tune by ID. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning/cancel)
```ts example.ts
run: async (payload, io, ctx) => {
// Specify the ID of the fine-tune you want to cancel
const fineTuneIdToCancel = "your_fine_tune_id"; // Replace with the actual fine-tune ID
// Cancel the specified fine-tune using the OpenAI API
const cancellationResponse = await io.openai.cancelFineTune(fineTuneIdToCancel);
// Log the cancellation response
await io.logger.info("cancellationResponse", cancellationResponse);
},
```
### `createFineTuningJob`
Creates a job that fine-tunes a specified model from a given dataset. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning/create)
```ts example.ts
run: async (payload, io, ctx) => {
// Specify the parameters for creating a fine-tuning job
const fineTuningJobParams = {
fineTuneId: "your_fine_tune_id", // Replace with the actual fine-tune ID
datasetId: "your_dataset_id", // Replace with the ID of your dataset
model: "text-davinci-002", // Replace with the model for fine-tuning
n_examples: 100, // Replace with the number of examples
};
// Create the fine-tuning job using the OpenAI API
const fineTuningJobResponse = await io.openai.createFineTuningJob(fineTuningJobParams);
// Log the response
await io.logger.info("fineTuningJobResponse", fineTuningJobResponse);
},
```
### `retrieveFineTuningJob`
Get info about a fine-tuning job. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning/retrieve)
```ts example.ts
run: async (payload, io, ctx) => {
// Specify the ID of the fine-tuning job you want to retrieve
const fineTuningJobId = "your_fine_tuning_job_id"; // Replace with the actual job ID
// Retrieve the fine-tuning job using the OpenAI API
const retrievedJob = await io.openai.retrieveFineTuningJob(fineTuningJobId);
// Log the retrieved job
await io.logger.info("retrievedJob", retrievedJob);
},
```
### `cancelFineTuningJob`
Cancel a fine-tuning job. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning/cancel)
```ts example.ts
run: async (payload, io, ctx) => {
// Specify the ID of the fine-tuning job you want to cancel
const fineTuningJobIdToCancel = "your_fine_tuning_job_id"; // Replace with the actual job ID
// Cancel the specified fine-tuning job using the OpenAI API
const cancellationResponse = await io.openai.cancelFineTuningJob(fineTuningJobIdToCancel);
// Log the cancellation response
await io.logger.info("cancellationResponse", cancellationResponse);
},
```
### `listFineTuningJobEvents`
List events for a fine-tuning job. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning/list-events)
```ts example.ts
run: async (payload, io, ctx) => {
// Specify the ID of the fine-tuning job for which you want to list events
const fineTuningJobId = "your_fine_tuning_job_id"; // Replace with the actual job ID
// List events for the specified fine-tuning job using the OpenAI API
const eventsListResponse = await io.openai.listFineTuningJobEvents(fineTuningJobId);
// Log the list of events
await io.logger.info("eventsListResponse", eventsListResponse);
},
```
### `listFineTuningJobs`
List fine tuning jobs. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning/list)
```ts example.ts
run: async (payload, io, ctx) => {
// List the fine-tuning jobs available in your OpenAI account
const jobsListResponse = await io.openai.listFineTuningJobs();
// Log the list of fine-tuning jobs
await io.logger.info("jobsListResponse", jobsListResponse);
},
```
## Example usage
In this example we'll create a task that generates a random joke using OpenAI GPT 3.5 .
```ts example.ts
import { TriggerClient, eventTrigger } from "@trigger.dev/sdk";
import { OpenAI } from "@trigger.dev/openai";
import { z } from "zod";
// Initialize a TriggerClient with the ID "jobs-showcase"
const client = new TriggerClient({ id: "jobs-showcase" });
// Create an instance of the OpenAI client and provide the OpenAI API key from environment variables
const openai = new OpenAI({
id: "openai",
apiKey: process.env.OPENAI_API_KEY!, // Replace with your actual OpenAI API key
});
// Define a job that uses OpenAI GPT-3.5 Turbo to tell jokes
client.defineJob({
id: "openai-tell-me-a-joke",
name: "OpenAI: tell me a joke",
version: "1.0.0",
trigger: eventTrigger({
name: "openai.tasks", // Define the trigger event name
schema: z.object({
jokePrompt: z.string(), // Expect a joke prompt as input
}),
}),
integrations: {
openai, // Use the OpenAI integration for this job
},
run: async (payload, io, ctx) => {
// Retrieve information about the GPT-3.5 Turbo model
await io.openai.retrieveModel("get-model", {
model: "gpt-3.5-turbo",
});
// List available models (optional, for reference)
const models = await io.openai.listModels("list-models");
// Generate a joke in the background using the chat conversation format
const jokeResult = await io.openai.backgroundCreateChatCompletion(
"background-chat-completion",
{
model: "gpt-3.5-turbo",
messages: [
{
role: "user",
content: payload.jokePrompt, // User-provided joke prompt
},
],
}
);
// Return the generated joke as the result
return {
joke: jokeResult.choices[0]?.message?.content,
};
},
});
// These lines are specific to the Express framework and can be removed if not needed
import { createExpressServer } from "@trigger.dev/express";
createExpressServer(client);
```
+63 -3
View File
@@ -49,11 +49,71 @@ const openai = new OpenAI({
## Tasks
Once you have set up a OpenAI client, you can use it to create tasks.
Once you have set up a OpenAI client, you can add it to your job and start using the provided tasks:
```ts
client.defineJob({
id: "openai-job",
name: "OpenAI Job",
version: "1.0.0",
trigger: invokeTrigger(),
integrations: {
openai, // Add the OpenAI client as an integration
},
run: async (payload, io, ctx) => {
// Now you can access it through the io object
const completion = await io.openai.chat.completions.create("completion", {
model: "gpt-3.5-turbo",
messages: [
{
role: "user",
content: "Create a good programming joke about background jobs",
},
],
});
},
});
```
As you can see above, we've replicated the API of the [OpenAI TypeScript SDK](https://github.com/openai/openai-node), with a crucial difference of adding the [Task Cache Key](https://trigger.dev/docs/documentation/concepts/tasks#task-cache-keys) as the first parameter.
We've also added a few convenience methods to make it easier to work with the OpenAI API, especially in a serverless environment. For example, you can run a Chat Completion task in the background with [backgroundCreate()](/integrations/apis/openai/chat#completions-backgroundcreate):
```ts
const completion = await io.openai.chat.completions.backgroundCreate("completion", {
model: "gpt-3.5-turbo",
messages: [
{
role: "user",
content: "Create a good programming joke about background jobs",
},
],
});
```
See our full task reference below:
<CardGroup>
<Card title="Tasks" icon="sparkles" href="/integrations/apis/openai-tasks">
Perform different AI-powered tasks using OpenAI.
<Card title="Chat Completions" icon="sparkles" href="/integrations/apis/openai/chat">
Given a list of messages comprising a conversation, the model will return a response
</Card>
<Card title="Assistants (Beta)" icon="arrows-spin" href="/integrations/apis/openai/assistants">
Build assistants that can call models and use tools to perform tasks
</Card>
<Card title="Files" icon="file" href="/integrations/apis/openai/files">
Upload files to use with assistants and fine-tuning
</Card>
<Card title="Images" icon="image" href="/integrations/apis/openai/images">
Given a prompt and/or an input image, the model will generate a new image
</Card>
<Card title="Fine Tuning Jobs" icon="vial" href="/integrations/apis/openai/fine-tunes">
Manage fine-tuning jobs to tailor a model to your specific training data
</Card>
<Card title="Models" icon="server" href="/integrations/apis/openai/models">
List and describe the various models available in the API
</Card>
<Card title="Completions (Legacy)" icon="scroll" href="/integrations/apis/openai/completions">
Given a prompt, the model will return one or more predicted completions.
</Card>
</CardGroup>
@@ -0,0 +1,287 @@
---
title: Assistant Tasks
sidebarTitle: Assitants (Beta)
---
<Note>
This feature is currently marked as a "Beta" by OpenAI. Make sure to check our their [How
Assistants Work](https://platform.openai.com/docs/assistants/how-it-works) and [Assistants
Overview](https://platform.openai.com/docs/assistants/overview) guides.
</Note>
## Assistants
Build assistants that can call models and use tools to perform tasks. [Official OpenAI docs](https://platform.openai.com/docs/api-reference/assistants)
### `create()`
Create an assistant with a model and instructions. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/assistants/createAssistant)
```ts example.ts
const file = await io.openai.files.createAndWaitForProcessing("upload-file", {
purpose: "assistants",
file: fs.createReadStream("./fixtures/mydata.csv"),
});
const assistant = await io.openai.beta.assistants.create("create-assistant", {
name: "Data visualizer",
description:
"You are great at creating beautiful data visualizations. You analyze data present in .csv files, understand trends, and come up with data visualizations relevant to those trends. You also share a brief text summary of the trends observed.",
model: "gpt-4-1106-preview",
tools: [{ type: "code_interpreter" }],
file_ids: [file.id],
});
```
## Threads
Create threads that assistants can interact with. [Official OpenAI docs](https://platform.openai.com/docs/api-reference/threads/createThread)
### `create()`
Create a thread. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/assistants/createAssistant)
```ts example.ts
const thread = await io.openai.beta.threads.create("create-thread", {
messages: [
{
role: "user",
content: "Create 3 data visualizations based on the trends in this file.",
file_ids: [fileId],
},
],
});
```
### `createAndRun()`
Create a thread and run it in one task.
```ts example.ts
const run = await io.openai.beta.threads.createAndRun("create-and-run-thread", {
assistant_id: "asst_abc123",
thread: {
messages: [
{
role: "user",
content: "Create 3 data visualizations based on the trends in this file.",
file_ids: [fileId],
},
],
},
});
```
### `createAndRunUntilCompletion()`
Create a thread and runs it in one task, and only returns when the run is completed by polling in the background using [io.backgroundPoll()](/sdk/io/background-poll).
```ts example.ts
const run = await io.openai.beta.threads.createAndRunUntilCompletion("create-thread", {
assistant_id: "asst_abc123",
thread: {
messages: [
{
role: "user",
content: "Create 3 data visualizations based on the trends in this file.",
file_ids: [fileId],
},
],
},
});
if (run.status !== "completed") {
throw new Error(`Run finished with status ${run.status}: ${JSON.stringify(run.last_error)}`);
}
// List all messages in the thread
const messages = await io.openai.beta.threads.messages.list("list-messages", run.thread_id);
```
### `retrieve()`
Retrieves a thread. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/threads/getThread)
```ts example.ts
const thread = await io.openai.beta.threads.retrieve("get-thread", "thread_abc123");
```
### `update()`
Modifies a thread. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/threads/modifyThread)
```ts example.ts
await io.openai.beta.threads.update("update-thread", "thread_abc123", {
metadata: {
foo: "bar",
},
});
```
### `del()`
Deletes a thread. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/threads/deleteThread)
```ts example.ts
const deletedThread = await io.openai.beta.threads.del("update-thread", "thread_abc123");
```
## Messages
Create messages within threads. [Official OpenAI docs](https://platform.openai.com/docs/api-reference/messages)
### `list()`
List all messages in a thread.
```ts example.ts
const messages = await io.openai.beta.threads.messages.list("list-messages", "thread_abc123");
```
### `create()`
Create a message. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/messages/createMessage)
```ts example.ts
const thread = await io.openai.beta.threads.create("get-thread");
const message = await io.openai.beta.threads.messages.create("create-message", thread.id, {
role: "user",
content: "Create 3 data visualizations based on the trends in this file.",
file_ids: [fileId],
});
```
### `retrieve()`
Retrieve a message. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/messages/getMessage)
```ts example.ts
const message = await io.openai.beta.threads.messages.retrieve(
"get-message",
"thread_abc123",
"message_abc123"
);
```
### `update()`
Update a message. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/messages/modifyMessage)
```ts example.ts
await io.openai.beta.threads.messages.update("update-message", thread.id, message.id, {
metadata: {
foo: "bar",
},
});
```
## Runs
Represents an execution run on a thread. [Official OpenAI docs](https://platform.openai.com/docs/api-reference/runs)
### `list()`
List all runs belonging to a thread.
```ts example.ts
const runs = await io.openai.beta.threads.runs.list("list-runs", "thread_abc123");
```
### `create()`
Create a run. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/runs/createRun)
```ts example.ts
const run = await io.openai.beta.threads.runs.create("create-run", "thread_abc123", {
assistant_id: payload.id,
});
```
### `createAndWaitForCompletion()`
Create a run and only return when the run is completed by polling in the background using [io.backgroundPoll()](/sdk/io/background-poll).
```ts example.ts
const run = await io.openai.beta.threads.runs.createAndWaitForCompletion(
"create-run",
"thread_abc123",
{
assistant_id: payload.id,
}
);
if (run.status !== "completed") {
throw new Error(`Run finished with status ${run.status}: ${JSON.stringify(run.last_error)}`);
}
const messages = await io.openai.beta.threads.messages.list("list-messages", run.thread_id);
```
### `waitForCompletion()`
Wait for a run to complete by polling in the background using [io.backgroundPoll()](/sdk/io/background-poll).
```ts example.ts
const run = await io.openai.beta.threads.runs.create("create-run", "thread_abc123", {
assistant_id: payload.id,
});
const completedRun = await io.openai.beta.threads.runs.waitForCompletion(
"wait-for-completion",
"thread_abc123",
run.id
);
if (completedRun.status !== "completed") {
throw new Error(
`Run finished with status ${completedRun.status}: ${JSON.stringify(completedRun.last_error)}`
);
}
const messages = await io.openai.beta.threads.messages.list("list-messages", run.thread_id);
```
### `retrieve()`
Retrieve a run. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/runs/getRun)
```ts example.ts
const run = await io.openai.beta.threads.runs.retrieve("get-run", "thread_abc123", "run_abc123");
```
### `cancel()`
Cancels a run that is `in_progress`. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/runs/cancelRun)
```ts example.ts
const run = await io.openai.beta.threads.runs.cancel("cancel-run", "thread_abc123", "run_abc123");
```
### `submitToolOutputs()`
When a run has the `status: "requires_action"` and `required_action.type` is `submit_tool_outputs`, this endpoint can be used to submit the outputs from the tool calls once they're all completed. All outputs must be submitted in a single request. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/runs/submitToolOutputs)
```ts example.ts
const run = await io.openai.beta.threads.runs.submitToolOutputs(
"submit-tool-outputs",
"thread_abc123",
"run_abc123",
{
tool_outputs: [
{
tool_call_id: "tool_run_abc123",
output: "This is the output of the tool call.",
},
],
}
);
```
### `list()`
Returns all runs belonging to a thread. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/runs/listRuns)
```ts example.ts
const runs = await io.openai.beta.threads.runs.list("list-runs", "thread_abc123");
```
+38
View File
@@ -0,0 +1,38 @@
---
title: Chat Completion Tasks
sidebarTitle: Chat Completions
---
Given a list of messages comprising a conversation, the model will return a response. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/chat)
### `completions.create()`
Creates a model response for the given chat conversation. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/chat/create)
```ts example.ts
await io.openai.chat.completions.create("chat-completion", {
model: "gpt-3.5-turbo",
messages: [
{
role: "user",
content: "Create a good programming joke about background jobs",
},
],
});
```
### `completions.backgroundCreate()`
Creates a model response for the given chat conversation, but runs the request in the background using [io.backgroundFetch()](/sdk/io/backgroundfetch)
```ts example.ts
await io.openai.chat.completions.create("chat-completion", {
model: "gpt-3.5-turbo",
messages: [
{
role: "user",
content: "Create a good programming joke about background jobs",
},
],
});
```
@@ -0,0 +1,19 @@
---
title: Completion Tasks
sidebarTitle: Completions (Legacy)
---
Given a prompt, the model will return one or more predicted completions, and can also return the probabilities of alternative tokens at each position. We recommend most users use the Chat Completions API. [Learn more](https://platform.openai.com/docs/deprecations/2023-07-06-gpt-and-embeddings)
### `create()`
<Warning>This is a legacy API</Warning>
Creates a completion for the provided prompt and parameters. [Official OpenAI docs](https://platform.openai.com/docs/api-reference/completions/create)
```ts example.ts
const completion = await io.openai.completions.create("completion", {
model: "text-davinci-003",
prompt: "Create a good programming joke about Tasks",
});
```
+62
View File
@@ -0,0 +1,62 @@
---
title: File Tasks
sidebarTitle: Files
---
Files are used to upload documents that can be used with features like [Assistants](https://platform.openai.com/docs/api-reference/assistants) and [Fine-tuning](https://platform.openai.com/docs/api-reference/fine-tuning). [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/files)
### `list()`
Returns a list of files that belong to the user's organization. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/files/list)
```ts example.ts
await io.openai.files.list("list-files");
await io.openai.files.list("list-files", { purpose: "assistants" }); // gets only assistant files
```
### `create()`
Upload a file that can be used across various endpoints/features. The size of all the files uploaded by one organization can be up to 100 GB.
The size of individual files for can be a maximum of `512MB`. See the [Assistants Tools guide](https://platform.openai.com/docs/assistants/tools) to learn more about the types of files supported. The Fine-tuning API only supports `.jsonl` files.
[Official OpenAI Docs](https://platform.openai.com/docs/api-reference/files/create)
```ts example.ts
const file = await io.openai.files.create("upload-file", {
purpose: "assistants",
file: fs.createReadStream("./fixtures/mydata.csv"),
});
```
### `createAndWaitForProcessing()`
Upload a file and will return when the file is processed by polling in the background using [io.backgroundPoll()](/sdk/io/background-poll).
```ts example.ts
const file = await io.openai.files.createAndWaitForProcessing("upload-file", {
purpose: "assistants",
file: fs.createReadStream("./fixtures/mydata.csv"),
});
```
### `waitForProcessing()`
Will return when the file is processed by polling in the background using [io.backgroundPoll()](/sdk/io/background-poll).
```ts example.ts
const file = await io.openai.files.create("upload-file", {
purpose: "assistants",
file: fs.createReadStream("./fixtures/mydata.csv"),
});
const processedFile = await io.openai.files.waitForProcessing("wait", file.id);
```
### `retrieve()`
Returns information about a specific file. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/files/retrieve)
```ts example.ts
await io.openai.files.retrieve("retrieve-file", "file-id");
```
@@ -0,0 +1,59 @@
---
title: Fine Tuning Tasks
sidebarTitle: Fine Tunes
---
Manage fine-tuning jobs to tailor a model to your specific training data. See the related guide [Fine Tuning models](https://platform.openai.com/docs/guides/fine-tuning) and view the [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning).
### `jobs.create()`
Creates a job that fine-tunes a specified model from a given dataset.
Response includes details of the enqueued job including job status and the name of the fine-tuned models once complete.
You must first upload a dataset to the API before creating a fine-tuning job. See our [OpenAI File Tasks](/integrations/apis/openai/files#createandwaitforprocessing) for more information.
```ts example.ts
const file = await io.openai.files.create("upload-file", {
purpose: "fine-tune",
file: fs.createReadStream("./mydata.jsonl"),
});
const fineTuning = await io.openai.fineTuning.jobs.create("fine-tuning", {
training_file: file.id,
model: "gpt-3.5-turbo",
suffix: "my-model",
});
```
### `jobs.list()`
List your organization's fine-tuning jobs. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning/list)
```ts example.ts
const fts = await io.openai.fineTuning.jobs.list("list");
```
### `jobs.retrieve()`
Get info about a fine-tuning job. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning/retrieve)
```ts example.ts
const fineTuning = await io.openai.fineTuning.jobs.retrieve("fine-tuning", "ft_1234");
```
### `jobs.cancel()`
Immediately cancel a fine-tune job. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning/cancel)
```ts example.ts
const fineTuning = await io.openai.fineTuning.jobs.cancel("fine-tuning", "ft_1234");
```
### `jobs.listEvents()`
Get status updates for a fine-tuning job. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/fine-tuning/list-events)
```ts example.ts
const events = await io.openai.fineTuning.jobs.listEvents("fine-tuning", { id: "ft_1234" });
```
+55
View File
@@ -0,0 +1,55 @@
---
title: Image Tasks
sidebarTitle: Images
---
Given a prompt and/or an input image, the model will generate a new image. See the [Image generation guide](https://platform.openai.com/docs/guides/images) and the [Official OpenAI docs](https://platform.openai.com/docs/api-reference/images).
### `create()`
Creates an image given a prompt. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/images/create)
```ts example.ts
await io.openai.images.create("dalle-3", {
model: "dall-e-3",
prompt:
"I would like to generate an image of an american giraffe riding a bycicle in a suburban neighborhood, into the sunset.",
});
```
### `backgroundCreate()`
Creates a an image given a prompt, but runs the request in the background using [io.backgroundFetch()](/sdk/io/backgroundfetch)
```ts example.ts
await io.openai.images.backgroundCreate("dalle-3", {
model: "dall-e-3",
prompt:
"I would like to generate an image of an american giraffe riding a bycicle in a suburban neighborhood, into the sunset.",
});
```
### `edit()`
Creates an edited or extended image given an original image and a prompt. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/images/createEdit)
```ts example.ts
await io.openai.images.edit("dalle-2", {
model: "dall-e-2",
image: fs.createReadStream("./giraffe.jpg"),
prompt: "A painting of a giraffe in a suburban neighborhood",
response_format: "url",
});
```
### `createVariation()`
Creates a variation of a given image. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/images/createVariation)
```ts example.ts
await io.openai.images.createVariation("dalle-3", {
model: "dall-e-2",
image: fs.createReadStream("./giraffe.jpg"),
response_format: "url",
});
```
+14
View File
@@ -0,0 +1,14 @@
---
title: Model Tasks
sidebarTitle: Models
---
List and describe the various models available in the API. You can refer to the Models documentation to understand what models are available and the differences between them. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/models)
### `list`
Lists the available models. [Official OpenAI Docs](https://platform.openai.com/docs/api-reference/models/list)
```ts example.ts
const models = await io.openai.models.list("list-models");
```
+49 -10
View File
@@ -1,6 +1,9 @@
{
"$schema": "https://mintlify.com/schema.json",
"name": "Trigger.dev",
"openapi": [
"/openapi.yml"
],
"logo": {
"dark": "/logo/dark.png",
"light": "/logo/light.png",
@@ -95,6 +98,7 @@
]
},
"documentation/guides/writing-jobs-step-by-step",
"documentation/guides/task-library",
"documentation/guides/video-walkthrough"
]
},
@@ -251,7 +255,10 @@
"pages": [
{
"group": "Airtable",
"pages": ["integrations/apis/airtable", "integrations/apis/airtable-tasks"]
"pages": [
"integrations/apis/airtable",
"integrations/apis/airtable-tasks"
]
},
{
"group": "GitHub",
@@ -264,18 +271,33 @@
"integrations/apis/linear",
{
"group": "OpenAI",
"pages": ["integrations/apis/openai", "integrations/apis/openai-tasks"]
"pages": [
"integrations/apis/openai",
"integrations/apis/openai/chat",
"integrations/apis/openai/assistants",
"integrations/apis/openai/files",
"integrations/apis/openai/images",
"integrations/apis/openai/fine-tunes",
"integrations/apis/openai/models",
"integrations/apis/openai/completions"
]
},
{
"group": "Plain",
"pages": ["integrations/apis/plain", "integrations/apis/plain-tasks"]
"pages": [
"integrations/apis/plain",
"integrations/apis/plain-tasks"
]
},
"integrations/apis/replicate",
"integrations/apis/resend",
"integrations/apis/sendgrid",
{
"group": "Slack",
"pages": ["integrations/apis/slack", "integrations/apis/slack-tasks"]
"pages": [
"integrations/apis/slack",
"integrations/apis/slack-tasks"
]
},
"integrations/apis/stripe",
{
@@ -323,12 +345,15 @@
"pages": [
"sdk/io/overview",
"sdk/io/runtask",
"sdk/io/wait",
"sdk/io/logger",
"sdk/io/sendevent",
"sdk/io/sendevents",
"sdk/io/wait",
"sdk/io/wait-for-event",
"sdk/io/wait-for-request",
"sdk/io/backgroundfetch",
"sdk/io/background-poll",
"sdk/io/random",
"sdk/io/logger",
"sdk/io/try",
"sdk/io/registerinterval",
"sdk/io/unregisterinterval",
@@ -352,7 +377,10 @@
"sdk/dynamictrigger/constructor",
{
"group": "Instance methods",
"pages": ["sdk/dynamictrigger/register", "sdk/dynamictrigger/unregister"]
"pages": [
"sdk/dynamictrigger/register",
"sdk/dynamictrigger/unregister"
]
}
]
},
@@ -363,7 +391,10 @@
"sdk/dynamicschedule/constructor",
{
"group": "Instance methods",
"pages": ["sdk/dynamicschedule/register", "sdk/dynamicschedule/unregister"]
"pages": [
"sdk/dynamicschedule/register",
"sdk/dynamicschedule/unregister"
]
}
]
},
@@ -373,6 +404,12 @@
"sdk/verify-request-signature"
]
},
{
"group": "HTTP Reference",
"pages": [
"sdk/api-reference/events/create-an-event"
]
},
{
"group": "React SDK",
"pages": [
@@ -385,7 +422,9 @@
},
{
"group": "Overview",
"pages": ["examples/introduction"]
"pages": [
"examples/introduction"
]
}
],
"footerSocials": {
@@ -398,4 +437,4 @@
"apiKey": "phc_hwYmedO564b3Ik8nhA4Csrb5SueY0EwFJWCbseGwWW"
}
}
}
}
+137
View File
@@ -0,0 +1,137 @@
openapi: 3.0.0
info:
title: Trigger.dev API
description: API for triggering events in Trigger.dev
version: 1.0.0
servers:
- url: https://api.trigger.dev
description: Trigger.dev API server
security:
- BearerAuth: []
paths:
/api/v1/events:
post:
operationId: sendEvent
externalDocs:
description: Find more info here
url: "https://trigger.dev/docs/api/events/send-event"
tags:
- Events
summary: Create an event
description: Send an event to Trigger.dev to trigger job runs through eventTrigger()
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/EventRequest"
responses:
"200":
description: Event successfully sent
content:
application/json:
schema:
$ref: "#/components/schemas/EventResponse"
"400":
description: Invalid request
"401":
description: Unauthorized - API key is missing or invalid
"422":
description: Invalid request body
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
components:
schemas:
Error:
type: object
properties:
message:
type: string
EventRequest:
type: object
properties:
event:
type: object
required:
- name
properties:
name:
type: string
description: The name of the event
payload:
type: object
additionalProperties: true
description: The payload of the event
context:
type: object
additionalProperties: true
description: An optional context object
id:
type: string
description: Unique identifier for the event. Auto-generated if not provided. If you provide an ID that already exists, the event will not be redelivered.
timestamp:
type: string
format: date-time
description: Event timestamp. Defaults to current timestamp if not provided.
source:
type: string
description: Event source, default is 'trigger.dev'.
options:
type: object
properties:
deliverAt:
type: string
format: date-time
description: Optional Date to deliver the event.
deliverAfter:
type: integer
description: Optional delay in seconds before delivering the event.
accountId:
type: string
description: Optional account ID to associate with the event.
EventResponse:
type: object
properties:
id:
type: string
description: The ID of the event that was sent.
name:
type: string
description: The name of the event that was sent.
payload:
$ref: "#/components/schemas/DeserializedJson"
context:
$ref: "#/components/schemas/DeserializedJson"
nullable: true
description: The context of the event that was sent. Null if no context was set.
timestamp:
type: string
format: date-time
description: The timestamp of the event that was sent.
deliverAt:
type: string
format: date-time
nullable: true
description: The timestamp when the event will be delivered. Null if not applicable.
deliveredAt:
type: string
format: date-time
nullable: true
description: The timestamp when the event was delivered. Null if not applicable.
cancelledAt:
type: string
format: date-time
nullable: true
description: The timestamp when the event was cancelled. Null if the event wasn't cancelled.
DeserializedJson:
type: object
additionalProperties: true
description: A JSON object that represents the deserialized payload or context.
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
@@ -0,0 +1,3 @@
---
openapi: post /api/v1/events
---
+137
View File
@@ -0,0 +1,137 @@
---
title: "io.backgroundPoll()"
sidebarTitle: "backgroundPoll()"
description: "`io.backgroundPoll()` allows you to fetch data from a URL on an interval."
---
## Parameters
<Snippet file="stable-key-param.mdx" />
<ResponseField name="url" type="string" required>
The url to fetch.
</ResponseField>
<ResponseField name="interval" type="number" required>
The interval in seconds to wait between requests. Minimum interval is 10 seconds and maximum is 5
minutes.
</ResponseField>
<ResponseField name="timeout" type="number" required>
The timeout in seconds before aborting the polling. Minimum timeout is 30 seconds and maximum is 1
hour.
</ResponseField>
<ResponseField name="requestInit" type="RequestInit">
Options for the fetch request
<Expandable title="options" defaultOpen>
<ResponseField name="method" type="string">
The HTTP method to use for the request.
</ResponseField>
<ResponseField name="headers" type="object">
Any headers to send with the request. Note that you can use [redactString](sdk/redactString) to
prevent sensitive information from being stored (e.g. in the logs), like API keys and tokens.
</ResponseField>
<ResponseField name="body" type="string | ArrayBuffer">
The body of the request.
</ResponseField>
</Expandable>
</ResponseField>
<ResponseField name="responseFilter" type="ResponseFilter">
Allows you to filter the response to determine when to stop polling.
<Expandable title="options" defaultOpen>
<ResponseField name="status" type="string[]">
An array of status codes to match against.
</ResponseField>
<ResponseField name="headers" type="EventFilter">
An object of header key/values to match. This uses the [EventFilter matching syntax](/documentation/guides/event-filter)
```ts example.ts
filter: {
header: {
"content-type": [{ $startsWith: "application/json" }],
},
},
```
</ResponseField>
<ResponseField name="body" type="EventFilter">
An [EventFilter](/documentation/guides/event-filter) object to match against the response body. This will only be applied if the response body is JSON.
</ResponseField>
</Expandable>
</ResponseField>
<ResponseField name="requestTimeout" type="object">
An optional object to specify a timeout for each individual request.
<Expandable title="options" defaultOpen>
<ResponseField name="durationInMs" type="number" required>
The timeout in milliseconds before aborting the request.
</ResponseField>
<ResponseField name="retry" type="RetryOptions">
<Expandable title="options">
{" "}
<ResponseField name="limit" type="number">
The maximum number of times to retry the request.
</ResponseField>
<ResponseField name="minTimeoutInMs" type="number">
The minimum amount of time to wait before retrying the request.
</ResponseField>
<ResponseField name="maxTimeoutInMs" type="number">
The maximum amount of time to wait before retrying the request.
</ResponseField>
<ResponseField name="factor" type="number">
The exponential factor to use when calculating the next retry time.
</ResponseField>
<ResponseField name="randomize" type="boolean">
Whether to randomize the retry time.
</ResponseField>
</Expandable>
</ResponseField>
</Expandable>
</ResponseField>
## Returns
A `Promise` that resolves with the JSON response body of the matching background fetch request. You can specify the type of the response body as a generic parameter.
<RequestExample>
```ts polling
client.defineJob({
id: "background-poll-job",
name: "Background Poll Job",
version: "0.0.1",
trigger: invokeTrigger({
schema: z.object({ url: z.string().url() }),
}),
run: async (payload, io, ctx) => {
const result = await io.backgroundPoll<{ foo: string }>("poll", {
url: payload.url,
interval: 10, // every 10 seconds
timeout: 300, // stop polling after 5 minutes
responseFilter: {
// stop polling once this filter matches
status: [200],
body: {
status: ["SUCCESS"],
},
},
});
},
});
```
</RequestExample>
+2 -2
View File
@@ -4,7 +4,7 @@ sidebarTitle: "backgroundFetch()"
description: "`io.backgroundFetch()` allows you to fetch data from a URL that can take longer that the serverless timeout. The actual `fetch` request is performed on the Trigger.dev platform, and the response is sent back to you. An example use case is fetching data from a slow API, like some AI endpoints."
---
This is used inside the OpenAI Integration for Tasks like `backgroundCreateChatCompletion` and `backgroundCreateCompletion`.
This is used inside the OpenAI Integration for Tasks like [Chat Completions Background Create](/integrations/apis/openai/chat#completions-backgroundcreate)
## Parameters
@@ -140,7 +140,7 @@ An individual retrying strategy can be one of two types:
## Returns
A `Promise` that resolves after the specified amount of time.
A `Promise` that resolves with the JSON response body of the background fetch request. You can specify the type of the response body as a generic parameter.
<RequestExample>
+15 -3
View File
@@ -26,15 +26,23 @@ Used to send log messages to the [Run log](/documentation/guides/viewing-runs).
`io.runTask()` allows you to run a [Task](/documentation/concepts/tasks) from inside a Job run. A Task is a resumable unit of a Run that can be retried, resumed and is logged. [Integrations](/integrations) use Tasks internally to perform their actions.
### [sendEvent()](/sdk/io/sendevent)
`io.sendEvent()` allows you to send an event from inside a Job run. The sent even will trigger any Jobs that are listening for that event (based on the name).
If you want to send an event from outside a run (e.g. just from your backend) you can use [client.sendEvent()](/sdk/triggerclient/instancemethods/sendevent).
### [wait()](/sdk/io/wait)
Waits for a certain amount of time before continuing the Job. Delays works even if you're on a serverless platform with timeouts, or if your server goes down. They utilize [resumability](/documentation/concepts/resumability) to ensure that the Run can be resumed after the delay.
### [sendEvent()](/sdk/io/sendevent)
### [waitForEvent()](/sdk/io/wait-for-event)
`io.sendEvent()` allows you to send an event from inside a Job run. The sent event will trigger any Jobs that are listening for that event (based on the name).
`io.waitForEvent()` allows you to pause the execution of a run until an event is received, and receive the event data.
If you want to send an event from outside a run (e.g. just from your backend) you can use [client.sendEvent()](/sdk/triggerclient/instancemethods/sendevent).
### [waitForRequest()](/sdk/io/wait-for-request)
`io.waitForRequest()` allows you to pause the execution of a run until the provided URL is requested, and receive the request data.
### [sendEvents()](/sdk/io/sendevents)
@@ -46,6 +54,10 @@ If you want to send multiple events from outside a run (e.g. just from your back
`io.backgroundFetch()` allows you to fetch data from a URL that can take longer that the serverless timeout. The actual `fetch` request is performed on the Trigger.dev platform, and the response is sent back to you. An example use case is fetching data from a slow API, like some AI endpoints.
### [backgroundPoll()](/sdk/io/background-poll)
`io.backgroundPoll()` allows you to fetch data from a URL on an interval, in the background.
### [random()](/sdk/io/random)
`io.random()` is identical to `Math.random()` when called without options but ensures your random numbers are not regenerated on resume or retry. It will return a pseudo-random floating-point number between optional `min` (default: 0, inclusive) and `max` (default: 1, exclusive). Can optionally `round` to the nearest integer.
+105
View File
@@ -0,0 +1,105 @@
---
title: "io.waitForEvent()"
sidebarTitle: "waitForEvent()"
description: "`io.waitForEvent()` waits for the next event to be emitted, and returns the event data"
---
<Warning>This feature is in beta and has not yet been deployed to the Trigger.dev cloud</Warning>
## Parameters
<Snippet file="stable-key-param.mdx" />
<ResponseField name="event" type="object" required>
Specify the options for the event to wait for.
{" "}
<Expandable title="fields" defaultOpen>
<ResponseField name="name" type="string | string[]" required>
The name(s) of the event to wait for.
</ResponseField>
<ResponseField name="schema" type="ZodTypeAny">
An optional Zod schema to validate the event payload against. If omitted the `event.payload`
will be typed as `any`
</ResponseField>
<ResponseField name="filter" type="EventFilter">
An [EventFilter](/documentation/guides/event-filter) to match against the event payload.
</ResponseField>
<ResponseField name="source" type="string">
The source of the event to wait for. If omitted, the event can come from any source.
</ResponseField>
<ResponseField name="contextFilter" type="EventFilter">
An [EventFilter](/documentation/guides/event-filter) to match against the event context.
</ResponseField>
<ResponseField name="accountId" type="string">
The account ID of the event to wait for. If omitted, the event can come from any account.
</ResponseField>
</Expandable>
</ResponseField>
<ResponseField name="options" type="object">
Specify the options for the event to wait for.
{" "}
<Expandable title="options" defaultOpen>
<ResponseField name="timeoutInSeconds" type="number">
The amount of time to wait for the event to be emitted before timing out. The default timeout is
1 hour and the maximum timeout is 1 year. If the timeout is reached, the task will fail with an
error message and the run will be exited.
</ResponseField>
</Expandable>
</ResponseField>
## Returns
<ResponseField name="id" type="string" required>
The ID of the event that was emitted.
</ResponseField>
<ResponseField name="name" type="string" required>
The name of the event that was emitted.
</ResponseField>
<ResponseField name="payload" type="any" required>
The payload of the event that was emitted.
</ResponseField>
<ResponseField name="context" type="any">
The context of the event that was emitted.
</ResponseField>
<ResponseField name="timestamp" type="Date" required>
The timestamp of the event that was emitted.
</ResponseField>
<ResponseField name="accountId" type="string">
The account ID of the event that was emitted.
</ResponseField>
<RequestExample>
```ts example.ts
const event = await io.waitForEvent(
"wait",
{
name: "my.event",
schema: z.object({
id: z.string(),
createdAt: z.coerce.date(),
isAdmin: z.boolean(),
}),
filter: {
isAdmin: [true], // Only wait for events where isAdmin is true
},
},
{
timeoutInSeconds: 60 * 60, // Wait for up to an hour
}
);
```
</RequestExample>
+75
View File
@@ -0,0 +1,75 @@
---
title: "io.waitForRequest()"
sidebarTitle: "waitForRequest()"
description: "`io.waitForRequest()` waits for a request to be made to the provided URL."
---
## Parameters
<Snippet file="stable-key-param.mdx" />
<ResponseField name="callback" type="function" required>
A callback function that is called with a single `url` parameter. When the URL is POSTed to, the
task will be completed and the POST request body will be returned.
</ResponseField>
<ResponseField name="options" type="object">
<Expandable title="fields" defaultOpen>
<ResponseField name="timeoutInSeconds" type="number">
The amount of time to wait for the request to be made before timing out. Defaults to 1 hour.
</ResponseField>
</Expandable>
</ResponseField>
## Returns
Returns a `Promise` that resolves to the request body when the request is made.
<RequestExample>
```ts example.ts
type ScreenshotResponse = {
store: {
location: string;
}
}
client.defineJob({
id: "screenshot-one-example",
name: "Screenshot One Example",
version: "1.0.0",
trigger: invokeTrigger({
schema: z.object({
url: z.string().url().default("https://trigger.dev"),
}),
}),
run: async (payload, io, ctx) => {
const result = await io.waitForRequest<ScreenshotResponse>(
"screenshot-one",
async (url) => {
await fetch(`https://api.screenshotone.com/take`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
access_key: process.env.SCREENSHOT_ONE_API_KEY,
url: payload.url,
store: "true",
storage_path: "my-screeshots",
response_type: "json",
async: "true",
webhook_url: url, // this is the URL that will be called when the screenshot is ready
storage_return_location: "true",
}),
});
},
{
timeoutInSeconds: 300,
}
);
},
});
```
</RequestExample>
+8
View File
@@ -0,0 +1,8 @@
module.exports = {
moduleFileExtensions: ["ts", "tsx", "js"],
transform: {
"^.+\\.(ts|tsx)$": "ts-jest",
},
testMatch: ["<rootDir>/test/**/*.ts?(x)", "<rootDir>/test/**/?(*.)+(spec|test).ts?(x)"],
testEnvironment: "node",
};
+7 -3
View File
@@ -17,16 +17,20 @@
"@types/node": "18",
"rimraf": "^3.0.2",
"tsup": "^6.5.0",
"typescript": "^4.9.4"
"typescript": "^4.9.4",
"@types/jest": "^29.5.3",
"jest": "^29.6.2",
"ts-jest": "^29.1.1"
},
"scripts": {
"clean": "rimraf dist",
"build": "npm run clean && npm run build:tsup",
"build:tsup": "tsup",
"typecheck": "tsc --noEmit"
"typecheck": "tsc --noEmit",
"test": "jest"
},
"dependencies": {
"openai": "^4.13.0",
"openai": "^4.16.1",
"@trigger.dev/sdk": "workspace:^2.2.5",
"@trigger.dev/integration-kit": "workspace:^2.2.5"
},
+57
View File
@@ -0,0 +1,57 @@
import { IntegrationTaskKey, Prettify } from "@trigger.dev/sdk";
import { OpenAIRunTask } from "./index";
import { OpenAIIntegrationOptions, OpenAIRequestOptions } from "./types";
import OpenAI from "openai";
import { createTaskOutputProperties, handleOpenAIError } from "./taskUtils";
export class Assistants {
constructor(
private runTask: OpenAIRunTask,
private options: OpenAIIntegrationOptions
) {}
async create(
key: IntegrationTaskKey,
params: Prettify<OpenAI.Beta.AssistantCreateParams>,
options: OpenAIRequestOptions = {}
): Promise<OpenAI.Beta.Assistant> {
return this.runTask(
key,
async (client, task) => {
const { data, response } = await client.beta.assistants
.create(params, {
idempotencyKey: task.idempotencyKey,
...options,
})
.withResponse();
const outputProperties = createTaskOutputProperties(undefined, response.headers);
task.outputProperties = [
...(outputProperties ?? []),
{
label: "assistantId",
text: data.id,
},
];
return data;
},
{
name: "Create Assistant",
params,
properties: [
{
label: "model",
text: params.model,
},
...(params.name ? [{ label: "name", text: params.name }] : []),
...(params.file_ids && params.file_ids.length > 0
? [{ label: "files", text: params.file_ids.join(", ") }]
: []),
],
},
handleOpenAIError
);
}
}
+19
View File
@@ -0,0 +1,19 @@
import { Assistants } from "./assistants";
import { OpenAIRunTask } from "./index";
import { Threads } from "./threads";
import { OpenAIIntegrationOptions } from "./types";
export class Beta {
constructor(
private runTask: OpenAIRunTask,
private options: OpenAIIntegrationOptions
) {}
get assistants() {
return new Assistants(this.runTask.bind(this), this.options);
}
get threads() {
return new Threads(this.runTask.bind(this), this.options);
}
}
+24 -13
View File
@@ -5,7 +5,8 @@ import {
backgroundTaskRetries,
createBackgroundFetchHeaders,
createBackgroundFetchUrl,
createTaskUsageProperties,
createTaskOutputProperties,
handleOpenAIError,
} from "./taskUtils";
import { OpenAIIntegrationOptions, OpenAIRequestOptions } from "./types";
import { FetchRetryOptions, FetchTimeoutOptions } from "@trigger.dev/integration-kit";
@@ -25,12 +26,16 @@ export class Chat {
return this.runTask(
key,
async (client, task) => {
const response = await client.chat.completions.create(params, {
idempotencyKey: task.idempotencyKey,
...options,
});
task.outputProperties = createTaskUsageProperties(response.usage);
return response;
const { data, response } = await client.chat.completions
.create(params, {
idempotencyKey: task.idempotencyKey,
...options,
})
.withResponse();
task.outputProperties = createTaskOutputProperties(data.usage, response.headers);
return data;
},
{
name: "Chat Completion",
@@ -41,7 +46,8 @@ export class Chat {
text: params.model,
},
],
}
},
handleOpenAIError
);
},
@@ -61,7 +67,7 @@ export class Chat {
options
);
const response = await io.backgroundFetch<OpenAI.Chat.ChatCompletion>(
const response = await io.backgroundFetchResponse<OpenAI.Chat.ChatCompletion>(
"background",
url,
{
@@ -74,13 +80,18 @@ export class Chat {
),
body: JSON.stringify(params),
},
fetchOptions.retries ?? backgroundTaskRetries,
fetchOptions.timeout
{
retry: fetchOptions?.retries ?? backgroundTaskRetries,
timeout: fetchOptions?.timeout,
}
);
task.outputProperties = createTaskUsageProperties(response.usage);
task.outputProperties = createTaskOutputProperties(
response.data.usage,
new Headers(response.headers)
);
return response;
return response.data;
},
{
name: "Background Chat Completion",
+24 -13
View File
@@ -5,7 +5,8 @@ import {
backgroundTaskRetries,
createBackgroundFetchHeaders,
createBackgroundFetchUrl,
createTaskUsageProperties,
createTaskOutputProperties,
handleOpenAIError,
} from "./taskUtils";
import { OpenAIIntegrationOptions, OpenAIRequestOptions } from "./types";
import { FetchRetryOptions, FetchTimeoutOptions } from "@trigger.dev/integration-kit";
@@ -24,12 +25,16 @@ export class Completions {
return this.runTask(
key,
async (client, task) => {
const response = await client.completions.create(params, {
idempotencyKey: task.idempotencyKey,
...options,
});
task.outputProperties = createTaskUsageProperties(response.usage);
return response;
const { data, response } = await client.completions
.create(params, {
idempotencyKey: task.idempotencyKey,
...options,
})
.withResponse();
task.outputProperties = createTaskOutputProperties(data.usage, response.headers);
return data;
},
{
name: "Completion",
@@ -40,7 +45,8 @@ export class Completions {
text: params.model,
},
],
}
},
handleOpenAIError
);
}
@@ -60,7 +66,7 @@ export class Completions {
options
);
const response = await io.backgroundFetch<OpenAI.Completion>(
const response = await io.backgroundFetchResponse<OpenAI.Completion>(
"background",
url,
{
@@ -73,13 +79,18 @@ export class Completions {
),
body: JSON.stringify(params),
},
fetchOptions.retries ?? backgroundTaskRetries,
fetchOptions.timeout
{
retry: fetchOptions?.retries ?? backgroundTaskRetries,
timeout: fetchOptions?.timeout,
}
);
task.outputProperties = createTaskUsageProperties(response.usage);
task.outputProperties = createTaskOutputProperties(
response.data.usage,
new Headers(response.headers)
);
return response;
return response.data;
},
{
name: "Background Completion",
+11 -8
View File
@@ -2,7 +2,7 @@ import { truncate } from "@trigger.dev/integration-kit";
import { IntegrationTaskKey, Prettify } from "@trigger.dev/sdk";
import OpenAI from "openai";
import { OpenAIRunTask } from "./index";
import { createTaskUsageProperties } from "./taskUtils";
import { createTaskOutputProperties, handleOpenAIError } from "./taskUtils";
import { OpenAIRequestOptions } from "./types";
export class Edits {
@@ -42,18 +42,21 @@ export class Edits {
return this.runTask(
key,
async (client, task) => {
const response = await client.edits.create(params, {
idempotencyKey: task.idempotencyKey,
...options,
});
task.outputProperties = createTaskUsageProperties(response.usage);
return response;
const { data, response } = await client.edits
.create(params, {
idempotencyKey: task.idempotencyKey,
...options,
})
.withResponse();
task.outputProperties = createTaskOutputProperties(data.usage, response.headers);
return data;
},
{
name: "Create edit",
params,
properties,
}
},
handleOpenAIError
);
}
}
+11 -8
View File
@@ -1,7 +1,7 @@
import { IntegrationTaskKey } from "@trigger.dev/sdk";
import OpenAI from "openai";
import { OpenAIRunTask } from "./index";
import { createTaskUsageProperties } from "./taskUtils";
import { createTaskOutputProperties, handleOpenAIError } from "./taskUtils";
import { OpenAIRequestOptions } from "./types";
export class Embeddings {
@@ -19,12 +19,14 @@ export class Embeddings {
return this.runTask(
key,
async (client, task) => {
const response = await client.embeddings.create(params, {
idempotencyKey: task.idempotencyKey,
...options,
});
task.outputProperties = createTaskUsageProperties(response.usage);
return response;
const { data, response } = await client.embeddings
.create(params, {
idempotencyKey: task.idempotencyKey,
...options,
})
.withResponse();
task.outputProperties = createTaskOutputProperties(data.usage, response.headers);
return data;
},
{
name: "Create embedding",
@@ -35,7 +37,8 @@ export class Embeddings {
text: params.model,
},
],
}
},
handleOpenAIError
);
}
}
+168 -13
View File
@@ -2,12 +2,19 @@ import { fileFromString } from "@trigger.dev/integration-kit";
import { IntegrationTaskKey } from "@trigger.dev/sdk";
import OpenAI from "openai";
import { OpenAIRunTask } from "./index";
import { OpenAIRequestOptions } from "./types";
import { OpenAIIntegrationOptions, OpenAIRequestOptions } from "./types";
import {
createBackgroundFetchHeaders,
createBackgroundFetchUrl,
createTaskOutputProperties,
handleOpenAIError,
} from "./taskUtils";
import { Uploadable } from "openai/uploads";
type CreateFileRequest = {
file: string | File;
file: string | File | Uploadable;
fileName?: string;
purpose: string;
purpose: "fine-tune" | "assistants";
};
type CreateFineTuneFileRequest = {
@@ -19,11 +26,10 @@ type CreateFineTuneFileRequest = {
};
export class Files {
runTask: OpenAIRunTask;
constructor(runTask: OpenAIRunTask) {
this.runTask = runTask;
}
constructor(
private runTask: OpenAIRunTask,
private options: OpenAIIntegrationOptions
) {}
create(
key: IntegrationTaskKey,
@@ -33,7 +39,7 @@ export class Files {
return this.runTask(
key,
async (client, task) => {
let file: File;
let file: Uploadable;
if (typeof params.file === "string") {
file = await fileFromString(params.file, params.fileName ?? "file.txt");
@@ -59,24 +65,172 @@ export class Files {
text: typeof params.file === "string" ? "string" : "File",
},
],
}
},
handleOpenAIError
);
}
async createAndWaitForProcessing(
key: IntegrationTaskKey,
params: CreateFileRequest,
options: OpenAIRequestOptions = {}
): Promise<OpenAI.Files.FileObject> {
return this.runTask(
key,
async (client, task, io) => {
let file: Uploadable;
if (typeof params.file === "string") {
file = await fileFromString(params.file, params.fileName ?? "file.txt");
} else {
file = params.file;
}
const { data, response } = await client.files
.create(
{ file, purpose: params.purpose },
{ idempotencyKey: task.idempotencyKey, ...options }
)
.withResponse();
task.outputProperties = createTaskOutputProperties(undefined, response.headers);
if (["processed", "error", "deleted"].includes(data.status)) {
return data;
}
const url = createBackgroundFetchUrl(
client,
`/files/${data.id}`,
this.options.defaultQuery,
options
);
const headers = this.options.defaultHeaders ?? {};
const processedFile = await io.backgroundPoll<OpenAI.Files.FileObject>("poll", {
url,
requestInit: {
headers: createBackgroundFetchHeaders(client, task.idempotencyKey, headers, options),
},
interval: 10,
timeout: 600,
responseFilter: {
status: [200],
body: {
status: ["processed", "error", "deleted"],
},
},
});
return processedFile;
},
{
name: "Create file and wait for processing",
params,
properties: [
{
label: "Purpose",
text: params.purpose,
},
{
label: "Input type",
text: typeof params.file === "string" ? "string" : "File",
},
],
},
handleOpenAIError
);
}
async waitForProcessing(
key: IntegrationTaskKey,
id: string,
options: OpenAIRequestOptions = {}
): Promise<OpenAI.Files.FileObject> {
return this.runTask(
key,
async (client, task, io) => {
const url = createBackgroundFetchUrl(
client,
`/files/${id}`,
this.options.defaultQuery,
options
);
const headers = this.options.defaultHeaders ?? {};
const processedFile = await io.backgroundPoll<OpenAI.Files.FileObject>("poll", {
url,
requestInit: {
headers: createBackgroundFetchHeaders(client, task.idempotencyKey, headers, options),
},
interval: 10,
timeout: 600,
responseFilter: {
status: [200],
body: {
status: ["processed", "error", "deleted"],
},
},
});
return processedFile;
},
{
name: "Wait for processing",
properties: [
{
label: "fileId",
text: id,
},
],
},
handleOpenAIError
);
}
retrieve(
key: IntegrationTaskKey,
id: string,
options: OpenAIRequestOptions = {}
): Promise<OpenAI.Files.FileObject> {
return this.runTask(
key,
async (client, task) => {
const response = await client.files.retrieve(id, options);
return response;
},
{
name: "Retrieve file",
properties: [
{
label: "fileId",
text: id,
},
],
},
handleOpenAIError
);
}
list(
key: IntegrationTaskKey,
query?: OpenAI.Files.FileListParams,
options: OpenAIRequestOptions = {}
): Promise<OpenAI.Files.FileObject[]> {
return this.runTask(
key,
async (client, task) => {
const response = await client.files.list(options);
const response = await client.files.list(query, options);
return response.data;
},
{
name: "List files",
properties: [],
}
},
handleOpenAIError
);
}
@@ -107,7 +261,8 @@ export class Files {
text: params.examples.length.toString(),
},
],
}
},
handleOpenAIError
);
}
}
+21 -10
View File
@@ -2,6 +2,7 @@ import { IntegrationTaskKey } from "@trigger.dev/sdk";
import OpenAI from "openai";
import { OpenAIRunTask } from "./index";
import { OpenAIRequestOptions } from "./types";
import { handleOpenAIError } from "./taskUtils";
type SpecificFineTuneRequest = {
fineTuneId: string;
@@ -49,7 +50,8 @@ export class FineTunes {
name: "Create fine tune",
params,
properties,
}
},
handleOpenAIError
);
}
@@ -66,7 +68,8 @@ export class FineTunes {
{
name: "List fine tunes",
properties: [],
}
},
handleOpenAIError
);
}
@@ -89,7 +92,8 @@ export class FineTunes {
text: params.fineTuneId,
},
],
}
},
handleOpenAIError
);
}
@@ -115,7 +119,8 @@ export class FineTunes {
text: params.fineTuneId,
},
],
}
},
handleOpenAIError
);
}
@@ -138,7 +143,8 @@ export class FineTunes {
text: params.fineTuneId,
},
],
}
},
handleOpenAIError
);
}
@@ -189,7 +195,8 @@ export class FineTunes {
name: "Create Fine Tuning Job",
params,
properties,
}
},
handleOpenAIError
);
},
@@ -212,7 +219,8 @@ export class FineTunes {
text: params.id,
},
],
}
},
handleOpenAIError
);
},
@@ -238,7 +246,8 @@ export class FineTunes {
text: params.id,
},
],
}
},
handleOpenAIError
);
},
@@ -262,7 +271,8 @@ export class FineTunes {
text: params.id,
},
],
}
},
handleOpenAIError
);
},
@@ -281,7 +291,8 @@ export class FineTunes {
{
name: "List Fine Tuning Jobs",
params,
}
},
handleOpenAIError
);
},
};
+144 -36
View File
@@ -1,33 +1,43 @@
import { fileFromUrl } from "@trigger.dev/integration-kit";
import { FetchRetryOptions, FetchTimeoutOptions, fileFromUrl } from "@trigger.dev/integration-kit";
import { IntegrationTaskKey, Prettify } from "@trigger.dev/sdk";
import OpenAI from "openai";
import { OpenAIRunTask } from "./index";
import { OpenAIRequestOptions } from "./types";
import { OpenAIIntegrationOptions, OpenAIRequestOptions } from "./types";
import {
backgroundTaskRetries,
createBackgroundFetchHeaders,
createBackgroundFetchUrl,
createImageTaskOutputProperties,
createTaskOutputProperties,
handleOpenAIError,
} from "./taskUtils";
import { Uploadable } from "openai/uploads";
export type CreateImageEditRequest = {
image: string | File;
image: string | File | Uploadable;
prompt: string;
mask?: string | File;
mask?: string | File | Uploadable;
n?: number;
size?: "256x256" | "512x512" | "1024x1024";
response_format?: "url" | "b64_json";
user?: string;
model?: (string & {}) | "dall-e-2" | null;
};
export type CreateImageVariationRequest = {
image: string | File;
image: string | File | Uploadable;
n?: number;
size?: "256x256" | "512x512" | "1024x1024";
response_format?: "url" | "b64_json";
user?: string;
model?: (string & {}) | "dall-e-2" | null;
};
export class Images {
runTask: OpenAIRunTask;
constructor(runTask: OpenAIRunTask) {
this.runTask = runTask;
}
constructor(
private runTask: OpenAIRunTask,
private options: OpenAIIntegrationOptions
) {}
generate(
key: IntegrationTaskKey,
@@ -65,16 +75,89 @@ export class Images {
return this.runTask(
key,
async (client, task) => {
return client.images.generate(params, { idempotencyKey: task.idempotencyKey, ...options });
const { data, response } = await client.images
.generate(params, { idempotencyKey: task.idempotencyKey, ...options })
.withResponse();
task.outputProperties = createImageTaskOutputProperties(data, response.headers);
return data;
},
{
name: "Create image",
params,
properties,
},
handleOpenAIError
);
}
backgroundGenerate(
key: IntegrationTaskKey,
params: Prettify<OpenAI.Images.ImageGenerateParams>,
options: OpenAIRequestOptions = {},
fetchOptions: { retries?: FetchRetryOptions; timeout?: FetchTimeoutOptions } = {}
): Promise<OpenAI.Images.ImagesResponse> {
return this.runTask(
key,
async (client, task, io) => {
const url = createBackgroundFetchUrl(
client,
"/images/generations",
this.options.defaultQuery,
options
);
const response = await io.backgroundFetchResponse<OpenAI.Images.ImagesResponse>(
"background",
url,
{
method: options.method ?? "POST",
headers: createBackgroundFetchHeaders(
client,
task.idempotencyKey,
this.options.defaultHeaders,
options
),
body: JSON.stringify(params),
},
{
retry: fetchOptions?.retries ?? backgroundTaskRetries,
timeout: fetchOptions?.timeout,
}
);
task.outputProperties = createImageTaskOutputProperties(
response.data,
new Headers(response.headers)
);
return response.data;
},
{
name: "Background Image Generate",
params,
properties: [
{
label: "model",
text: params.model ?? "unknown",
},
],
retry: {
limit: 0,
},
}
);
}
create(...args: Parameters<Images["generate"]>) {
return this.generate(...args);
}
backgroundCreate(...args: Parameters<Images["backgroundGenerate"]>) {
return this.backgroundGenerate(...args);
}
edit(
key: IntegrationTaskKey,
params: CreateImageEditRequest,
@@ -87,6 +170,13 @@ export class Images {
text: params.prompt,
});
if (typeof params.model === "string") {
properties.push({
label: "model",
text: params.model,
});
}
if (params.n) {
properties.push({
label: "Number of images",
@@ -123,26 +213,32 @@ export class Images {
typeof params.image === "string" ? await fileFromUrl(params.image) : params.image;
const mask = typeof params.mask === "string" ? await fileFromUrl(params.mask) : params.mask;
const response = await client.images.edit(
{
image: file,
prompt: params.prompt,
mask: mask,
n: params.n,
size: params.size,
response_format: params.response_format,
user: params.user,
},
{ idempotencyKey: task.idempotencyKey, ...options }
);
const { data, response } = await client.images
.edit(
{
image: file,
prompt: params.prompt,
mask: mask,
n: params.n,
size: params.size,
response_format: params.response_format,
user: params.user,
model: params.model,
},
{ idempotencyKey: task.idempotencyKey, ...options }
)
.withResponse();
return response;
task.outputProperties = createTaskOutputProperties(undefined, response.headers);
return data;
},
{
name: "Create image edit",
params,
properties,
}
},
handleOpenAIError
);
}
@@ -153,6 +249,13 @@ export class Images {
): Promise<OpenAI.Images.ImagesResponse> {
let properties = [];
if (typeof params.model === "string") {
properties.push({
label: "model",
text: params.model,
});
}
if (params.n) {
properties.push({
label: "Number of images",
@@ -188,18 +291,23 @@ export class Images {
const file =
typeof params.image === "string" ? await fileFromUrl(params.image) : params.image;
const response = await client.images.createVariation(
{
image: file,
n: params.n,
size: params.size,
response_format: params.response_format,
user: params.user,
},
{ idempotencyKey: task.idempotencyKey, ...options }
);
const { data, response } = await client.images
.createVariation(
{
image: file,
n: params.n,
size: params.size,
response_format: params.response_format,
user: params.user,
model: params.model,
},
{ idempotencyKey: task.idempotencyKey, ...options }
)
.withResponse();
return response;
task.outputProperties = createTaskOutputProperties(undefined, response.headers);
return data;
},
{
name: "Create image variation",
+124 -8
View File
@@ -19,6 +19,7 @@ import { FineTunes } from "./fineTunes";
import { Images } from "./images";
import { Models } from "./models";
import { OpenAIIntegrationOptions } from "./types";
import { Beta } from "./beta";
export type OpenAIRunTask = InstanceType<typeof OpenAI>["runTask"];
@@ -103,7 +104,10 @@ export class OpenAI implements TriggerIntegration {
options?: RunTaskOptions,
errorCallback?: RunTaskErrorCallback
): Promise<TResult> {
if (!this._io) throw new Error("No IO");
if (!this._io)
throw new Error(
"Issue with running task: IO not found. It's possible that you forgot to prefix openai with io. inside a run"
);
if (!this._connectionKey) throw new Error("No connection key");
return this._io.runTask(
key,
@@ -113,7 +117,7 @@ export class OpenAI implements TriggerIntegration {
},
{
icon: this._options.icon ?? "openai",
retry: retry.standardBackoff,
retry: retry.exponentialBackoff,
...(options ?? {}),
connectionKey: this._connectionKey,
},
@@ -129,6 +133,10 @@ export class OpenAI implements TriggerIntegration {
return new Completions(this.runTask.bind(this), this._options);
}
get beta() {
return new Beta(this.runTask.bind(this), this._options);
}
get chat() {
return new Chat(this.runTask.bind(this), this._options);
}
@@ -138,7 +146,7 @@ export class OpenAI implements TriggerIntegration {
}
get images() {
return new Images(this.runTask.bind(this));
return new Images(this.runTask.bind(this), this._options);
}
get embeddings() {
@@ -146,18 +154,45 @@ export class OpenAI implements TriggerIntegration {
}
get files() {
return new Files(this.runTask.bind(this));
return new Files(this.runTask.bind(this), this._options);
}
get fineTunes() {
return this.fineTuning;
}
get fineTuning() {
return new FineTunes(this.runTask.bind(this));
}
/**
* @deprecated Please use openai.models.retrieve instead
*/
retrieveModel = this.models.retrieve;
/**
* @deprecated Please use openai.models.list instead
*/
listModels = this.models.list;
/**
* @deprecated Please use openai.models.delete instead
*/
deleteModel = this.models.delete;
/**
* @deprecated Please use openai.models.delete instead
*/
deleteFineTune = this.models.delete;
/**
* @deprecated Please use openai.completions.create instead
*/
createCompletion = this.completions.create;
/**
* @deprecated Please use openai.chat.completions.create instead
*/
createChatCompletion = this.chat.completions.create;
/**
@@ -176,19 +211,82 @@ export class OpenAI implements TriggerIntegration {
return this.chat.completions.backgroundCreate(...args);
}
/**
* @deprecated Please use openai.edits.create instead
*/
createEdit = this.edits.create;
generateImage = this.images.generate;
createImage = this.images.generate;
createImageEdit = this.images.edit;
createImageVariation = this.images.createVariation;
/**
* @deprecated Please use openai.images.generate instead
*/
async generateImage(...args: Parameters<typeof this.images.generate>) {
return this.images.generate(...args);
}
/**
* @deprecated Please use openai.images.create instead
*/
async createImage(...args: Parameters<typeof this.images.generate>) {
return this.images.generate(...args);
}
/**
* @deprecated Please use openai.images.edit instead
*/
async createImageEdit(...args: Parameters<typeof this.images.edit>) {
return this.images.edit(...args);
}
/**
* @deprecated Please use openai.images.createVariation instead
*/
async createImageVariation(...args: Parameters<typeof this.images.createVariation>) {
return this.images.createVariation(...args);
}
/**
* @deprecated Please use openai.embeddings.create instead
*/
createEmbedding = this.embeddings.create;
/**
* @deprecated Please use openai.files.create instead
*/
createFile = this.files.create;
/**
* @deprecated Please use openai.files.list instead
*/
listFiles = this.files.list;
/**
* @deprecated Please use openai.files.create instead
*/
createFineTuneFile = this.files.createFineTune;
/**
* @deprecated Please use openai.fineTuning.create instead
*/
createFineTune = this.fineTunes.create;
/**
* @deprecated Please use openai.fineTuning.list instead
*/
listFineTunes = this.fineTunes.list;
/**
* @deprecated Please use openai.fineTuning.retrieve instead
*/
retrieveFineTune = this.fineTunes.retrieve;
/**
* @deprecated Please use openai.fineTuning.cancel instead
*/
cancelFineTune = this.fineTunes.cancel;
/**
* @deprecated Please use openai.fineTuning.listEvents instead
*/
listFineTuneEvents = this.fineTunes.listEvents;
/**
@@ -198,10 +296,28 @@ export class OpenAI implements TriggerIntegration {
* of the fine-tuned models once complete.
*
* [Learn more about fine-tuning](https://platform.openai.com/docs/guides/fine-tuning)
*
* @deprecated Please use openai.fineTuning.jobs.create instead
*/
createFineTuningJob = this.fineTunes.jobs.create;
/**
* @deprecated Please use openai.fineTuning.jobs.retrieve instead
*/
retrieveFineTuningJob = this.fineTunes.jobs.retrieve;
/**
* @deprecated Please use openai.fineTuning.jobs.cancel instead
*/
cancelFineTuningJob = this.fineTunes.jobs.cancel;
/**
* @deprecated Please use openai.fineTuning.jobs.listEvents instead
*/
listFineTuningJobEvents = this.fineTunes.jobs.listEvents;
/**
* @deprecated Please use openai.fineTuning.jobs.list instead
*/
listFineTuningJobs = this.fineTunes.jobs.list;
}
+7 -3
View File
@@ -3,6 +3,7 @@ import { Model } from "openai/resources";
import { OpenAIRunTask } from "./index";
import OpenAI from "openai";
import { OpenAIRequestOptions } from "./types";
import { handleOpenAIError } from "./taskUtils";
type DeleteFineTunedModelRequest = {
fineTunedModelId: string;
@@ -33,7 +34,8 @@ export class Models {
text: params.model,
},
],
}
},
handleOpenAIError
);
}
@@ -47,7 +49,8 @@ export class Models {
{
name: "List models",
properties: [],
}
},
handleOpenAIError
);
}
@@ -70,7 +73,8 @@ export class Models {
text: params.fineTunedModelId,
},
],
}
},
handleOpenAIError
);
}
}
+191 -18
View File
@@ -1,12 +1,56 @@
import OpenAI from "openai";
import OpenAI, { APIError } from "openai";
import { OpenAIRequestOptions } from "./types";
import { redactString } from "@trigger.dev/sdk";
import { calculateResetAtUtil } from "@trigger.dev/integration-kit";
import { FetchRetryOptions } from "@trigger.dev/integration-kit";
export function createTaskUsageProperties(
export function createImageTaskOutputProperties(
response: OpenAI.ImagesResponse | undefined,
headers?: Headers | undefined
) {
if (!response && !headers) {
return;
}
return [...createTaskImageProperties(response), ...createTaskRateLimitProperties(headers)];
}
function createTaskImageProperties(response: OpenAI.ImagesResponse | undefined) {
if (!response) {
return [];
}
const imageUrls = response.data.map((image) => image.url).filter(Boolean) as string[];
if (imageUrls.length === 0) {
return [];
}
return [
{
label: "Images",
text: imageUrls[0],
imageUrl: imageUrls,
},
];
}
export function createTaskOutputProperties(
usage: OpenAI.Completions.CompletionUsage | OpenAI.CreateEmbeddingResponse.Usage | undefined,
headers?: Headers | undefined
) {
if (!usage && !headers) {
return;
}
return [...createTaskUsageProperties(usage), ...createTaskRateLimitProperties(headers)];
}
function createTaskUsageProperties(
usage: OpenAI.Completions.CompletionUsage | OpenAI.CreateEmbeddingResponse.Usage | undefined
) {
if (!usage) {
return;
return [];
}
return [
@@ -22,15 +66,117 @@ export function createTaskUsageProperties(
},
]
: []),
{
label: "Total Usage",
text: String(usage.total_tokens),
},
];
}
export function onTaskError(error: unknown) {
return;
function createTaskRateLimitProperties(headers: Headers | undefined) {
if (!headers) {
return [];
}
const remainingRequests = headers.get("x-ratelimit-remaining-requests");
const remainingTokens = headers.get("x-ratelimit-remaining-tokens");
const resetRequests = headers.get("x-ratelimit-reset-requests");
const resetTokens = headers.get("x-ratelimit-reset-tokens");
return [
...(remainingRequests
? [
{
label: "Remaining Requests",
text: remainingRequests ?? "Unknown",
},
]
: []),
...(resetRequests
? [
{
label: "Reset Requests",
text: resetRequests ?? "Unknown",
},
]
: []),
...(remainingTokens
? [
{
label: "Remaining Tokens",
text: remainingTokens ?? "Unknown",
},
]
: []),
...(resetTokens
? [
{
label: "Reset Tokens",
text: resetTokens ?? "Unknown",
},
]
: []),
];
}
export function handleOpenAIError(error: unknown) {
if (error instanceof APIError) {
const isErrorRetryable = () => {
if (typeof error.status !== "number") {
return false;
}
if (error.status === 429 && error.type === "insufficient_quota") {
return false;
}
return (
error.status === 429 ||
error.status === 408 ||
error.status === 409 ||
(error.status >= 500 && error.status <= 599)
);
};
const calculateRetryAt = () => {
if (error.status !== 429) {
return;
}
if (!error.headers) {
return;
}
const remainingRequests = error.headers["x-ratelimit-remaining-requests"];
const requestResets = error.headers["x-ratelimit-reset-requests"];
if (typeof remainingRequests === "string" && Number(remainingRequests) === 0) {
return calculateResetAt(requestResets);
}
const remainingTokens = error.headers["x-ratelimit-remaining-tokens"];
const tokensResets = error.headers["x-ratelimit-reset-tokens"];
if (typeof remainingTokens === "string" && Number(remainingTokens) === 0) {
return calculateResetAt(tokensResets);
}
};
return {
error,
skipRetrying: !isErrorRetryable(),
retryAt: calculateRetryAt(),
};
}
return error as Error;
}
// This takes a string in the format of 1s or 6m59s, 1h6m18s and calculates the date
// If the string is invalid, it returns undefined
// If the string is null or undefined, it returns undefined
export function calculateResetAt(
resets: string | null | undefined,
now: Date = new Date()
): Date | undefined {
return calculateResetAtUtil(resets, "iso_8601_duration_openai_variant", now);
}
export function createBackgroundFetchUrl(
@@ -83,29 +229,56 @@ export function createBackgroundFetchHeaders(
};
}
export const backgroundTaskRetries = {
export const backgroundTaskRetries: FetchRetryOptions = {
"500-599": {
strategy: "backoff",
limit: 5,
minTimeoutInMs: 1000,
maxTimeoutInMs: 30000,
factor: 1.8,
factor: 2,
randomize: true,
},
"429": {
strategy: "backoff",
limit: 10,
minTimeoutInMs: 1000,
maxTimeoutInMs: 60000,
factor: 2,
randomize: true,
limit: 0,
bodyFilter: {
error: {
code: ["insufficient_quota"],
},
},
},
"429,429": {
strategy: "headers",
limitHeader: "x-ratelimit-limit-requests",
remainingHeader: "x-ratelimit-remaining-requests",
resetHeader: "x-ratelimit-reset-requests",
resetFormat: "iso_8601_duration_openai_variant",
bodyFilter: {
error: {
code: ["rate_limit_exceeded"],
type: ["requests"],
},
},
},
"429,429,429": {
strategy: "headers",
limitHeader: "x-ratelimit-limit-tokens",
remainingHeader: "x-ratelimit-remaining-tokens",
resetHeader: "x-ratelimit-reset-tokens",
resetFormat: "iso_8601_duration_openai_variant",
bodyFilter: {
error: {
code: ["rate_limit_exceeded"],
type: ["tokens"],
},
},
},
"408-409": {
strategy: "backoff",
limit: 3,
limit: 5,
minTimeoutInMs: 1000,
maxTimeoutInMs: 60000,
factor: 2,
randomize: true,
},
} as const;
};
+691
View File
@@ -0,0 +1,691 @@
import { IntegrationTaskKey, Prettify } from "@trigger.dev/sdk";
import { OpenAIRunTask } from "./index";
import { OpenAIIntegrationOptions, OpenAIRequestOptions } from "./types";
import OpenAI from "openai";
import {
createBackgroundFetchHeaders,
createBackgroundFetchUrl,
createTaskOutputProperties,
handleOpenAIError,
} from "./taskUtils";
import { RunSubmitToolOutputsParams } from "openai/resources/beta/threads/runs/runs";
import { ThreadUpdateParams } from "openai/resources/beta/threads/threads";
export class Threads {
constructor(
private runTask: OpenAIRunTask,
private options: OpenAIIntegrationOptions
) {}
/**
* Create a thread and run it in one task.
*/
async createAndRun(
key: IntegrationTaskKey,
params: Prettify<OpenAI.Beta.ThreadCreateAndRunParams>,
options: OpenAIRequestOptions = {}
): Promise<OpenAI.Beta.Threads.Run> {
return this.runTask(
key,
async (client, task) => {
const { data, response } = await client.beta.threads
.createAndRun(params, {
idempotencyKey: task.idempotencyKey,
...options,
})
.withResponse();
const outputProperties = [
...(createTaskOutputProperties(undefined, response.headers) ?? []),
{
label: "threadId",
text: data.thread_id,
},
{
label: "runId",
text: data.id,
},
];
task.outputProperties = outputProperties;
return data;
},
{
name: "Create Thread and Run",
params,
},
handleOpenAIError
);
}
/**
* Create a thread and runs it in one task, and only returns when the run is completed by polling in the background.
*/
async createAndRunUntilCompletion(
key: IntegrationTaskKey,
params: Prettify<OpenAI.Beta.ThreadCreateAndRunParams>,
options: OpenAIRequestOptions = {}
): Promise<OpenAI.Beta.Threads.Run> {
return this.runTask(
key,
async (client, task, io) => {
const { data, response } = await client.beta.threads
.createAndRun(params, {
idempotencyKey: task.idempotencyKey,
...options,
})
.withResponse();
const outputProperties = [
...(createTaskOutputProperties(undefined, response.headers) ?? []),
{
label: "threadId",
text: data.thread_id,
},
{
label: "runId",
text: data.id,
},
];
task.outputProperties = outputProperties;
const url = createBackgroundFetchUrl(
client,
`/threads/${data.thread_id}/runs/${data.id}`,
this.options.defaultQuery,
options
);
const headers = this.options.defaultHeaders ?? {};
headers["OpenAI-Beta"] = "assistants=v1";
const completedRun = await io.backgroundPoll<OpenAI.Beta.Threads.Run>("poll", {
url,
requestInit: {
headers: createBackgroundFetchHeaders(client, task.idempotencyKey, headers, options),
},
interval: 10,
timeout: 600,
responseFilter: {
status: [200],
body: {
status: ["completed", "expired", "cancelled", "failed"],
},
},
});
return completedRun;
},
{
name: "Run Created Thread and Wait for Completion",
params,
},
handleOpenAIError
);
}
/**
* Create a thread.
*/
async create(
key: IntegrationTaskKey,
params: Prettify<OpenAI.Beta.ThreadCreateParams> = {},
options: OpenAIRequestOptions = {}
): Promise<OpenAI.Beta.Thread> {
return this.runTask(
key,
async (client, task) => {
const { data, response } = await client.beta.threads
.create(params, {
idempotencyKey: task.idempotencyKey,
...options,
})
.withResponse();
task.outputProperties = createTaskOutputProperties(undefined, response.headers);
return data;
},
{
name: "Create Thread",
params,
},
handleOpenAIError
);
}
/**
* Retrieves a thread.
*/
async retrieve(
key: IntegrationTaskKey,
threadId: string,
options: OpenAIRequestOptions = {}
): Promise<OpenAI.Beta.Thread> {
return this.runTask(
key,
async (client, task) => {
const { data, response } = await client.beta.threads
.retrieve(threadId, {
idempotencyKey: task.idempotencyKey,
...options,
})
.withResponse();
task.outputProperties = createTaskOutputProperties(undefined, response.headers);
return data;
},
{
name: "Retrieve Thread",
properties: [{ label: "threadId", text: threadId }],
},
handleOpenAIError
);
}
/**
* Modifies a thread.
*/
async update(
key: IntegrationTaskKey,
threadId: string,
body: ThreadUpdateParams,
options: OpenAIRequestOptions = {}
): Promise<OpenAI.Beta.Thread> {
return this.runTask(
key,
async (client, task) => {
const { data, response } = await client.beta.threads
.update(threadId, body, {
idempotencyKey: task.idempotencyKey,
...options,
})
.withResponse();
task.outputProperties = createTaskOutputProperties(undefined, response.headers);
return data;
},
{
name: "Update Thread",
properties: [{ label: "threadId", text: threadId }],
},
handleOpenAIError
);
}
/**
* Delete a thread.
*/
async del(
key: IntegrationTaskKey,
threadId: string,
options: OpenAIRequestOptions = {}
): Promise<OpenAI.Beta.ThreadDeleted> {
return this.runTask(
key,
async (client, task) => {
const { data, response } = await client.beta.threads
.del(threadId, {
idempotencyKey: task.idempotencyKey,
...options,
})
.withResponse();
task.outputProperties = createTaskOutputProperties(undefined, response.headers);
return data;
},
{
name: "Delete Thread",
properties: [{ label: "threadId", text: threadId }],
},
handleOpenAIError
);
}
get runs() {
return new Runs(this.runTask.bind(this), this.options);
}
get messages() {
return new Messages(this.runTask.bind(this), this.options);
}
}
class Runs {
constructor(
private runTask: OpenAIRunTask,
private options: OpenAIIntegrationOptions
) {}
/**
* Creates a run and waits for it to complete by polling in the background.
*/
async createAndWaitForCompletion(
key: IntegrationTaskKey,
threadId: string,
params: Prettify<OpenAI.Beta.Threads.RunCreateParams>,
options: OpenAIRequestOptions = {}
): Promise<OpenAI.Beta.Threads.Run> {
return this.runTask(
key,
async (client, task, io) => {
const { data, response } = await client.beta.threads.runs
.create(threadId, params, {
idempotencyKey: task.idempotencyKey,
...options,
})
.withResponse();
task.outputProperties = createTaskOutputProperties(undefined, response.headers);
const url = createBackgroundFetchUrl(
client,
`/threads/${threadId}/runs/${data.id}`,
this.options.defaultQuery,
options
);
const headers = this.options.defaultHeaders ?? {};
headers["OpenAI-Beta"] = "assistants=v1";
const completedRun = await io.backgroundPoll<OpenAI.Beta.Threads.Run>("poll", {
url,
requestInit: {
headers: createBackgroundFetchHeaders(client, task.idempotencyKey, headers, options),
},
interval: 10,
timeout: 600,
responseFilter: {
status: [200],
body: {
status: ["completed", "expired", "cancelled", "failed"],
},
},
});
return completedRun;
},
{
name: "Run Thread and Wait for Completion",
params,
properties: [{ label: "threadId", text: threadId }],
},
handleOpenAIError
);
}
/**
* Waits for a run to complete by polling in the background.
*/
async waitForCompletion(
key: IntegrationTaskKey,
threadId: string,
runId: string,
options: OpenAIRequestOptions = {}
): Promise<OpenAI.Beta.Threads.Run> {
return this.runTask(
key,
async (client, task, io) => {
const url = createBackgroundFetchUrl(
client,
`/threads/${threadId}/runs/${runId}`,
this.options.defaultQuery,
options
);
const headers = this.options.defaultHeaders ?? {};
headers["OpenAI-Beta"] = "assistants=v1";
const completedRun = await io.backgroundPoll<OpenAI.Beta.Threads.Run>("poll", {
url,
requestInit: {
headers: createBackgroundFetchHeaders(client, task.idempotencyKey, headers, options),
},
interval: 10,
timeout: 600,
responseFilter: {
status: [200],
body: {
status: ["completed", "expired", "cancelled", "failed"],
},
},
});
return completedRun;
},
{
name: "Wait for Run Completion",
properties: [
{ label: "threadId", text: threadId },
{ label: "runId", text: runId },
],
},
handleOpenAIError
);
}
/**
* Creates a run.
*/
async create(
key: IntegrationTaskKey,
threadId: string,
params: Prettify<OpenAI.Beta.Threads.RunCreateParams>,
options: OpenAIRequestOptions = {}
): Promise<OpenAI.Beta.Threads.Run> {
return this.runTask(
key,
async (client, task, io) => {
const { data, response } = await client.beta.threads.runs
.create(threadId, params, {
idempotencyKey: task.idempotencyKey,
...options,
})
.withResponse();
task.outputProperties = createTaskOutputProperties(undefined, response.headers);
return data;
},
{
name: "Run Thread",
params,
properties: [{ label: "threadId", text: threadId }],
},
handleOpenAIError
);
}
/**
* Retrieves a run.
*/
async retrieve(
key: IntegrationTaskKey,
threadId: string,
runId: string,
options: OpenAIRequestOptions = {}
): Promise<OpenAI.Beta.Threads.Run> {
return this.runTask(
key,
async (client, task, io) => {
const { data, response } = await client.beta.threads.runs
.retrieve(threadId, runId, {
idempotencyKey: task.idempotencyKey,
...options,
})
.withResponse();
task.outputProperties = createTaskOutputProperties(undefined, response.headers);
return data;
},
{
name: "Retrieve Run",
properties: [
{ label: "threadId", text: threadId },
{ label: "runId", text: runId },
],
},
handleOpenAIError
);
}
/**
* Cancels a run that is `in_progress`.
*/
async cancel(
key: IntegrationTaskKey,
threadId: string,
runId: string,
options: OpenAIRequestOptions = {}
): Promise<OpenAI.Beta.Threads.Run> {
return this.runTask(
key,
async (client, task, io) => {
const { data, response } = await client.beta.threads.runs
.cancel(threadId, runId, {
idempotencyKey: task.idempotencyKey,
...options,
})
.withResponse();
task.outputProperties = createTaskOutputProperties(undefined, response.headers);
return data;
},
{
name: "Cancel Run",
properties: [
{ label: "threadId", text: threadId },
{ label: "runId", text: runId },
],
},
handleOpenAIError
);
}
/**
* When a run has the `status: "requires_action"` and `required_action.type` is
* `submit_tool_outputs`, this endpoint can be used to submit the outputs from the
* tool calls once they're all completed. All outputs must be submitted in a single
* request.
*/
async submitToolOutputs(
key: IntegrationTaskKey,
threadId: string,
runId: string,
body: RunSubmitToolOutputsParams,
options: OpenAIRequestOptions = {}
): Promise<OpenAI.Beta.Threads.Run> {
return this.runTask(
key,
async (client, task, io) => {
const { data, response } = await client.beta.threads.runs
.submitToolOutputs(threadId, runId, body, {
idempotencyKey: task.idempotencyKey,
...options,
})
.withResponse();
task.outputProperties = createTaskOutputProperties(undefined, response.headers);
return data;
},
{
name: "Submit Tool Outputs",
properties: [
{ label: "threadId", text: threadId },
{ label: "runId", text: runId },
],
},
handleOpenAIError
);
}
/**
* Returns all runs belonging to a thread.
*/
async list(
key: IntegrationTaskKey,
threadId: string,
options: OpenAIRequestOptions = {}
): Promise<OpenAI.Beta.Threads.Run[]> {
return this.runTask(
key,
async (client, task, io) => {
const { data: page, response } = await client.beta.threads.runs
.list(threadId, {
idempotencyKey: task.idempotencyKey,
...options,
})
.withResponse();
const allRuns = [];
for await (const fineTuningJob of page) {
allRuns.push(fineTuningJob);
}
task.outputProperties = createTaskOutputProperties(undefined, response.headers);
return allRuns;
},
{
name: "List Runs",
properties: [{ label: "threadId", text: threadId }],
},
handleOpenAIError
);
}
}
class Messages {
constructor(
private runTask: OpenAIRunTask,
private options: OpenAIIntegrationOptions
) {}
/**
* Returns all messages for a given thread.
*/
async list(
key: IntegrationTaskKey,
threadId: string,
options: OpenAIRequestOptions = {}
): Promise<OpenAI.Beta.Threads.ThreadMessage[]> {
return this.runTask(
key,
async (client, task, io) => {
const { data: page, response } = await client.beta.threads.messages
.list(threadId, {
idempotencyKey: task.idempotencyKey,
...options,
})
.withResponse();
const allMessages = [];
for await (const fineTuningJob of page) {
allMessages.push(fineTuningJob);
}
task.outputProperties = createTaskOutputProperties(undefined, response.headers);
return allMessages;
},
{
name: "List Messages",
properties: [{ label: "threadId", text: threadId }],
},
handleOpenAIError
);
}
/**
* Create a message.
*/
async create(
key: IntegrationTaskKey,
threadId: string,
body: Prettify<OpenAI.Beta.Threads.MessageCreateParams>,
options: OpenAIRequestOptions = {}
): Promise<OpenAI.Beta.Threads.ThreadMessage> {
return this.runTask(
key,
async (client, task, io) => {
const { data, response } = await client.beta.threads.messages
.create(threadId, body, {
idempotencyKey: task.idempotencyKey,
...options,
})
.withResponse();
task.outputProperties = createTaskOutputProperties(undefined, response.headers);
return data;
},
{
name: "Create Message",
properties: [{ label: "threadId", text: threadId }],
},
handleOpenAIError
);
}
/**
* Retrieve a message.
*/
async retrieve(
key: IntegrationTaskKey,
threadId: string,
messageId: string,
options: OpenAIRequestOptions = {}
): Promise<OpenAI.Beta.Threads.ThreadMessage> {
return this.runTask(
key,
async (client, task, io) => {
const { data, response } = await client.beta.threads.messages
.retrieve(threadId, messageId, {
idempotencyKey: task.idempotencyKey,
...options,
})
.withResponse();
task.outputProperties = createTaskOutputProperties(undefined, response.headers);
return data;
},
{
name: "Retrieve Message",
properties: [
{ label: "threadId", text: threadId },
{ label: "messageId", text: messageId },
],
},
handleOpenAIError
);
}
/**
* Modifies a message.
*/
async update(
key: IntegrationTaskKey,
threadId: string,
messageId: string,
body: Prettify<OpenAI.Beta.Threads.MessageUpdateParams>,
options: OpenAIRequestOptions = {}
): Promise<OpenAI.Beta.Threads.ThreadMessage> {
return this.runTask(
key,
async (client, task, io) => {
const { data, response } = await client.beta.threads.messages
.update(threadId, messageId, body, {
idempotencyKey: task.idempotencyKey,
...options,
})
.withResponse();
task.outputProperties = createTaskOutputProperties(undefined, response.headers);
return data;
},
{
name: "Update Message",
properties: [
{ label: "threadId", text: threadId },
{ label: "messageId", text: messageId },
],
},
handleOpenAIError
);
}
}
@@ -0,0 +1,18 @@
import { calculateResetAt } from "../src/taskUtils";
describe("calculateResetAt", () => {
it("Should be able to correctly calculate based on a variety of formats", () => {
const now = new Date("2023-01-01T00:00:00.000Z");
expect(calculateResetAt("1s", now)).toEqual(new Date("2023-01-01T00:00:01.000Z"));
expect(calculateResetAt("6m59s", now)).toEqual(new Date("2023-01-01T00:06:59.000Z"));
expect(calculateResetAt("5m48s", now)).toEqual(new Date("2023-01-01T00:05:48.000Z"));
expect(calculateResetAt("1h44m5s", now)).toEqual(new Date("2023-01-01T01:44:05.000Z"));
expect(calculateResetAt("1h2s", now)).toEqual(new Date("2023-01-01T01:00:02.000Z"));
expect(calculateResetAt("45m", now)).toEqual(new Date("2023-01-01T00:45:00.000Z"));
expect(calculateResetAt("23h59m0s", now)).toEqual(new Date("2023-01-01T23:59:00.000Z"));
expect(calculateResetAt("1d22h8m1s", now)).toEqual(new Date("2023-01-02T22:08:01.000Z"));
expect(calculateResetAt("3h36m7.312s", now)).toEqual(new Date("2023-01-01T03:36:07.312Z"));
expect(calculateResetAt("72ms", now)).toEqual(new Date("2023-01-01T00:00:00.072Z"));
});
});
+18
View File
@@ -0,0 +1,18 @@
{
"extends": "@trigger.dev/tsconfig/node18.json",
"include": ["./src/**/*.ts", "tsup.config.ts"],
"compilerOptions": {
"lib": ["DOM", "DOM.Iterable", "ES2019"],
"paths": {
"@trigger.dev/sdk/*": ["../../packages/trigger-sdk/src/*"],
"@trigger.dev/sdk": ["../../packages/trigger-sdk/src/index"],
"@trigger.dev/integration-kit/*": ["../../packages/integration-kit/src/*"],
"@trigger.dev/integration-kit": ["../../packages/integration-kit/src/index"]
},
"declaration": false,
"declarationMap": false,
"baseUrl": ".",
"stripInternal": true
},
"exclude": ["node_modules"]
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"extends": "@trigger.dev/tsconfig/node18.json",
"include": ["./src/**/*.ts", "tsup.config.ts"],
"include": ["./src/**/*.ts", "tsup.config.ts", "src/globals.d.ts", "./test/**/*.ts"],
"compilerOptions": {
"lib": ["DOM", "DOM.Iterable", "ES2019"],
"paths": {
+30 -1
View File
@@ -1,5 +1,5 @@
import { eventFilterMatches } from "./eventFilterMatches";
import { HttpMethod, RequestFilter, StringMatch } from "./schemas/requestFilter";
import { HttpMethod, RequestFilter, ResponseFilter, StringMatch } from "./schemas/requestFilter";
export async function requestFilterMatches(
request: Request,
@@ -38,6 +38,35 @@ export async function requestFilterMatches(
return true;
}
export type ResponseFilterMatchResult = {
match: boolean;
body?: unknown;
};
export async function responseFilterMatches(
response: Response,
filter: ResponseFilter
): Promise<ResponseFilterMatchResult> {
if (filter.headers && !eventFilterMatches(response.headers, filter.headers)) {
return { match: false };
}
try {
const json = await response.json();
if (filter.body && !eventFilterMatches(json, filter.body)) {
return { match: false, body: json };
} else {
return { match: true, body: json };
}
} catch (e) {
if (filter.body) {
return { match: false, body: undefined };
}
}
return { match: true, body: undefined };
}
function requestMethodMatches(method: HttpMethod, filter: RequestFilter["method"]): boolean {
if (!filter) {
return true;
+105
View File
@@ -32,3 +32,108 @@ export function calculateRetryAt(retryOptions: RetryOptions, attempts: number):
return new Date(Date.now() + timeoutInMs);
}
export function calculateResetAt(
resets: string | undefined | null,
format:
| "unix_timestamp"
| "iso_8601"
| "iso_8601_duration_openai_variant"
| "unix_timestamp_in_ms",
now: Date = new Date()
): Date | undefined {
if (!resets) return;
switch (format) {
case "iso_8601_duration_openai_variant": {
return calculateISO8601DurationOpenAIVariantResetAt(resets, now);
}
case "iso_8601": {
return calculateISO8601ResetAt(resets, now);
}
case "unix_timestamp": {
return calculateUnixTimestampResetAt(resets, now);
}
case "unix_timestamp_in_ms": {
return calculateUnixTimestampInMsResetAt(resets, now);
}
}
}
function calculateUnixTimestampResetAt(resets: string, now: Date = new Date()): Date | undefined {
// Check if the input is null or undefined
if (!resets) return undefined;
// Convert the string to a number
const resetAt = parseInt(resets, 10);
// If the string doesn't match the expected format, return undefined
if (isNaN(resetAt)) return undefined;
// Return the date
return new Date(resetAt * 1000);
}
function calculateUnixTimestampInMsResetAt(
resets: string,
now: Date = new Date()
): Date | undefined {
// Check if the input is null or undefined
if (!resets) return undefined;
// Convert the string to a number
const resetAt = parseInt(resets, 10);
// If the string doesn't match the expected format, return undefined
if (isNaN(resetAt)) return undefined;
// Return the date
return new Date(resetAt);
}
function calculateISO8601ResetAt(resets: string, now: Date = new Date()): Date | undefined {
// Check if the input is null or undefined
if (!resets) return undefined;
// Parse the date
const resetAt = new Date(resets);
// If the string doesn't match the expected format, return undefined
if (isNaN(resetAt.getTime())) return undefined;
return resetAt;
}
function calculateISO8601DurationOpenAIVariantResetAt(
resets: string,
now: Date = new Date()
): Date | undefined {
// Check if the input is null or undefined
if (!resets) return undefined;
// Regular expression to match the duration string pattern
const pattern = /^(?:(\d+)d)?(?:(\d+)h)?(?:(\d+)m)?(?:(\d+(?:\.\d+)?)s)?(?:(\d+)ms)?$/;
const match = resets.match(pattern);
// If the string doesn't match the expected format, return undefined
if (!match) return undefined;
// Extract days, hours, minutes, seconds, and milliseconds from the string
const days = parseInt(match[1], 10) || 0;
const hours = parseInt(match[2], 10) || 0;
const minutes = parseInt(match[3], 10) || 0;
const seconds = parseFloat(match[4]) || 0;
const milliseconds = parseInt(match[5], 10) || 0;
// Calculate the future date based on the current date plus the extracted time
const resetAt = new Date(now);
resetAt.setDate(resetAt.getDate() + days);
resetAt.setHours(resetAt.getHours() + hours);
resetAt.setMinutes(resetAt.getMinutes() + minutes);
resetAt.setSeconds(resetAt.getSeconds() + Math.floor(seconds));
resetAt.setMilliseconds(
resetAt.getMilliseconds() + (seconds - Math.floor(seconds)) * 1000 + milliseconds
);
return resetAt;
}
+31 -3
View File
@@ -3,7 +3,7 @@ import { z } from "zod";
import { Prettify } from "../types";
import { addMissingVersionField } from "./addMissingVersionField";
import { ErrorWithStackSchema, SchemaErrorSchema } from "./errors";
import { EventRuleSchema } from "./eventFilter";
import { EventFilterSchema, EventRuleSchema } from "./eventFilter";
import { ConnectionAuthSchema, IntegrationConfigSchema } from "./integrations";
import { DeserializedJsonSchema, SerializableJsonSchema } from "./json";
import { DisplayPropertySchema, StyleSchema } from "./properties";
@@ -787,8 +787,8 @@ export const RunTaskOptionsSchema = z.object({
.optional(),
/** Allows you to link the Integration connection in the logs. This is handled automatically in integrations. */
connectionKey: z.string().optional(),
/** An operation you want to perform on the Trigger.dev platform, current only "fetch" is supported. If you wish to `fetch` use [`io.backgroundFetch()`](https://trigger.dev/docs/sdk/io/backgroundfetch) instead. */
operation: z.enum(["fetch"]).optional(),
/** An operation you want to perform on the Trigger.dev platform, current only "fetch", "fetch-response", and "fetch-poll" is supported. If you wish to `fetch` use [`io.backgroundFetch()`](https://trigger.dev/docs/sdk/io/backgroundfetch) instead. */
operation: z.enum(["fetch", "fetch-response", "fetch-poll"]).optional(),
/** A No Operation means that the code won't be executed. This is used internally to implement features like [io.wait()](https://trigger.dev/docs/sdk/io/wait). */
noop: z.boolean().default(false),
redact: RedactSchema.optional(),
@@ -989,3 +989,31 @@ export const InvokeOptionsSchema = z.object({
});
export type InvokeOptions = z.infer<typeof InvokeOptionsSchema>;
export const EphemeralEventDispatcherRequestBodySchema = z.object({
url: z.string(),
name: z.string().or(z.array(z.string())),
source: z.string().optional(),
filter: EventFilterSchema.optional(),
contextFilter: EventFilterSchema.optional(),
accountId: z.string().optional(),
timeoutInSeconds: z
.number()
.int()
.positive()
.min(10)
.max(60 * 60 * 24 * 365)
.default(3600),
});
export type EphemeralEventDispatcherRequestBody = z.infer<
typeof EphemeralEventDispatcherRequestBodySchema
>;
export const EphemeralEventDispatcherResponseBodySchema = z.object({
id: z.string(),
});
export type EphemeralEventDispatcherResponseBody = z.infer<
typeof EphemeralEventDispatcherResponseBodySchema
>;
+28
View File
@@ -1,5 +1,8 @@
import { z } from "zod";
import { RedactStringSchema, RetryOptionsSchema } from "./api";
import { EventFilterSchema } from "./eventFilter";
import { ResponseFilterSchema } from "./requestFilter";
import { Prettify } from "../types";
export const FetchRetryHeadersStrategySchema = z.object({
/** The `headers` strategy retries the request using info from the response headers. */
@@ -10,6 +13,18 @@ export const FetchRetryHeadersStrategySchema = z.object({
remainingHeader: z.string(),
/** The header to use to determine the time when the number of remaining retries will be reset. */
resetHeader: z.string(),
/** The event filter to use to determine if the request should be retried. */
bodyFilter: EventFilterSchema.optional(),
/** The format of the `resetHeader` value. */
resetFormat: z
.enum([
"unix_timestamp",
"unix_timestamp_in_ms",
"iso_8601",
"iso_8601_duration_openai_variant",
])
.default("unix_timestamp"),
});
export type FetchRetryHeadersStrategy = z.infer<typeof FetchRetryHeadersStrategySchema>;
@@ -18,6 +33,8 @@ export type FetchRetryHeadersStrategy = z.infer<typeof FetchRetryHeadersStrategy
export const FetchRetryBackoffStrategySchema = RetryOptionsSchema.extend({
/** The `backoff` strategy retries the request with an exponential backoff. */
strategy: z.literal("backoff"),
/** The event filter to use to determine if the request should be retried. */
bodyFilter: EventFilterSchema.optional(),
});
/** The `backoff` strategy retries the request with an exponential backoff. */
@@ -67,3 +84,14 @@ export const FetchOperationSchema = z.object({
});
export type FetchOperation = z.infer<typeof FetchOperationSchema>;
export const FetchPollOperationSchema = z.object({
url: z.string(),
interval: z.number().int().positive().min(10).max(600).default(10), // defaults to 10 seconds
timeout: z.number().int().positive().min(30).max(3600).default(600), // defaults to 10 minutes
responseFilter: ResponseFilterSchema,
requestInit: FetchRequestInitSchema.optional(),
requestTimeout: FetchTimeoutOptionsSchema.optional(),
});
export type FetchPollOperation = Prettify<z.infer<typeof FetchPollOperationSchema>>;
+2
View File
@@ -8,6 +8,8 @@ export const DisplayPropertySchema = z.object({
text: z.string(),
/** The URL to link to when the property is clicked */
url: z.string().optional(),
/** The URL to a list of images to display next to the property */
imageUrl: z.array(z.string()).optional(),
});
export const DisplayPropertiesSchema = z.array(DisplayPropertySchema);
@@ -1,5 +1,6 @@
import { z } from "zod";
import { EventFilterSchema, stringPatternMatchers } from "./eventFilter";
import { Prettify } from "../types";
const StringMatchSchema = z.union([
/** Match against a string */
@@ -58,3 +59,11 @@ export const RequestFilterSchema = z.object({
});
export type RequestFilter = z.infer<typeof RequestFilterSchema>;
/** Only Requests that match this filter will cause the `handler` function to run.
* For example, you can use this to only respond to `GET` Requests. */
export const ResponseFilterSchema = RequestFilterSchema.omit({ method: true, query: true }).extend({
status: z.array(z.number()).optional(),
});
export type ResponseFilter = Prettify<z.infer<typeof ResponseFilterSchema>>;
+1
View File
@@ -27,6 +27,7 @@ export const TaskSchema = z.object({
outputProperties: z.array(DisplayPropertySchema).optional().nullable(),
params: DeserializedJsonSchema.optional().nullable(),
output: DeserializedJsonSchema.optional().nullable(),
context: DeserializedJsonSchema.optional().nullable(),
error: z.string().optional().nullable(),
parentId: z.string().optional().nullable(),
style: StyleSchema.optional().nullable(),
@@ -0,0 +1,56 @@
import { calculateResetAt } from "../src";
describe("calculateResetAt", () => {
it("Should be able to correctly calculate iso_8601_duration_openai_variant reset values", () => {
const now = new Date("2023-01-01T00:00:00.000Z");
expect(calculateResetAt("1s", "iso_8601_duration_openai_variant", now)).toEqual(
new Date("2023-01-01T00:00:01.000Z")
);
expect(calculateResetAt("6m59s", "iso_8601_duration_openai_variant", now)).toEqual(
new Date("2023-01-01T00:06:59.000Z")
);
expect(calculateResetAt("5m48s", "iso_8601_duration_openai_variant", now)).toEqual(
new Date("2023-01-01T00:05:48.000Z")
);
expect(calculateResetAt("1h44m5s", "iso_8601_duration_openai_variant", now)).toEqual(
new Date("2023-01-01T01:44:05.000Z")
);
expect(calculateResetAt("1h2s", "iso_8601_duration_openai_variant", now)).toEqual(
new Date("2023-01-01T01:00:02.000Z")
);
expect(calculateResetAt("45m", "iso_8601_duration_openai_variant", now)).toEqual(
new Date("2023-01-01T00:45:00.000Z")
);
expect(calculateResetAt("23h59m0s", "iso_8601_duration_openai_variant", now)).toEqual(
new Date("2023-01-01T23:59:00.000Z")
);
expect(calculateResetAt("1d22h8m1s", "iso_8601_duration_openai_variant", now)).toEqual(
new Date("2023-01-02T22:08:01.000Z")
);
expect(calculateResetAt("3h36m7.312s", "iso_8601_duration_openai_variant", now)).toEqual(
new Date("2023-01-01T03:36:07.312Z")
);
expect(calculateResetAt("72ms", "iso_8601_duration_openai_variant", now)).toEqual(
new Date("2023-01-01T00:00:00.072Z")
);
});
it("Should be able to correctly calculate unix_timestamp reset values", () => {
expect(calculateResetAt("1699369436", "unix_timestamp")).toEqual(
new Date("2023-11-07T15:03:56.000Z")
);
});
it("Should be able to correctly calculate unix_timestamp_in_ms reset values", () => {
expect(calculateResetAt("1699369436000", "unix_timestamp_in_ms")).toEqual(
new Date("2023-11-07T15:03:56.000Z")
);
});
it("Should be able to correctly calculate iso_8601 reset values", () => {
expect(calculateResetAt("2023-11-07T15:03:56.000Z", "iso_8601")).toEqual(
new Date("2023-11-07T15:03:56.000Z")
);
});
});
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Task" ADD COLUMN "context" JSONB;
@@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE "EventDispatcher" ADD COLUMN "externalAccountId" TEXT;
-- AddForeignKey
ALTER TABLE "EventDispatcher" ADD CONSTRAINT "EventDispatcher_externalAccountId_fkey" FOREIGN KEY ("externalAccountId") REFERENCES "ExternalAccount"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+5
View File
@@ -104,6 +104,7 @@ model ExternalAccount {
schedules ScheduleSource[]
triggerSources TriggerSource[]
missingConnections MissingConnection[]
EventDispatcher EventDispatcher[]
@@unique([environmentId, identifier])
}
@@ -644,6 +645,9 @@ model EventDispatcher {
registrations DynamicTriggerRegistration[]
scheduleSources ScheduleSource[]
externalAccount ExternalAccount? @relation(fields: [externalAccountId], references: [id], onDelete: Cascade, onUpdate: Cascade)
externalAccountId String?
@@unique([dispatchableId, environmentId])
}
@@ -883,6 +887,7 @@ model Task {
outputProperties Json?
params Json?
output Json?
context Json?
error String?
redact Json?
style Json?
+1
View File
@@ -5,3 +5,4 @@ export * from "./properties";
export * from "./file";
export * from "./prettify";
export * from "./types";
export * from "./utils";
+3
View File
@@ -0,0 +1,3 @@
import { calculateResetAt } from "@trigger.dev/core";
export const calculateResetAtUtil = calculateResetAt;
+25
View File
@@ -34,6 +34,8 @@ import {
InvokeOptions,
InvokeJobRequestBody,
CompleteTaskBodyV2Input,
EphemeralEventDispatcherRequestBody,
EphemeralEventDispatcherResponseBodySchema,
} from "@trigger.dev/core";
import { z } from "zod";
@@ -525,6 +527,29 @@ export class ApiClient {
});
}
async createEphemeralEventDispatcher(payload: EphemeralEventDispatcherRequestBody) {
const apiKey = await this.#apiKey();
this.#logger.debug("Creating ephemeral event dispatcher", {
payload,
});
const response = await zodfetch(
EphemeralEventDispatcherResponseBodySchema,
`${this.#apiUrl}/api/v1/event-dispatchers/ephemeral`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify(payload),
}
);
return response;
}
async #apiKey() {
const apiKey = getApiKey(this.#options.apiKey);
+232 -10
View File
@@ -4,6 +4,8 @@ import {
ConnectionAuth,
CronOptions,
ErrorWithStackSchema,
EventFilter,
FetchPollOperation,
FetchRequestInit,
FetchRetryOptions,
FetchTimeoutOptions,
@@ -41,7 +43,14 @@ import { TriggerClient } from "./triggerClient";
import { DynamicTrigger } from "./triggers/dynamic";
import { ExternalSource, ExternalSourceParams } from "./triggers/externalSource";
import { DynamicSchedule } from "./triggers/scheduled";
import { EventSpecification, TaskLogger, TriggerContext } from "./types";
import {
EventSpecification,
TaskLogger,
TriggerContext,
WaitForEventResult,
waitForEventSchema,
} from "./types";
import { z } from "zod";
export type IOTask = ServerTask;
@@ -103,6 +112,12 @@ export class JSONOutputSerializer implements OutputSerializer {
}
}
export type BackgroundFetchResponse<T> = {
status: number;
data: T;
headers: Record<string, string>;
};
export class IO {
private _id: string;
private _apiClient: ApiClient;
@@ -331,6 +346,72 @@ export class IO {
});
}
async waitForEvent<T extends z.ZodTypeAny = z.ZodTypeAny>(
cacheKey: string | any[],
event: {
name: string;
schema?: T;
filter?: EventFilter;
source?: string;
contextFilter?: EventFilter;
accountId?: string;
},
options?: { timeoutInSeconds?: number }
): Promise<WaitForEventResult<z.output<T>>> {
const timeoutInSeconds = options?.timeoutInSeconds ?? 60 * 60;
return (await this.runTask(
cacheKey,
async (task, io) => {
if (!task.callbackUrl) {
throw new Error("No callbackUrl found on task");
}
await this.triggerClient.createEphemeralEventDispatcher({
url: task.callbackUrl,
name: event.name,
filter: event.filter,
contextFilter: event.contextFilter,
source: event.source,
accountId: event.accountId,
timeoutInSeconds,
});
return {} as Promise<{}>;
},
{
name: "Wait for Event",
icon: "custom-event",
params: {
name: event.name,
source: event.source,
filter: event.filter,
contextFilter: event.contextFilter,
accountId: event.accountId,
},
callback: {
enabled: true,
timeoutInSeconds,
},
properties: [
{
label: "Event",
text: event.name,
},
{
label: "Timeout",
text: `${timeoutInSeconds}s`,
},
...(event.source ? [{ label: "Source", text: event.source }] : []),
...(event.accountId ? [{ label: "Account ID", text: event.accountId }] : []),
],
parseOutput: (output) => {
return waitForEventSchema(event.schema ?? z.any()).parse(output);
},
}
)) as WaitForEventResult<z.output<T>>;
}
/** `io.waitForRequest()` allows you to pause the execution of a run until the url provided in the callback is POSTed to.
* This is useful for integrating with external services that require a callback URL to be provided, or if you want to be able to wait until an action is performed somewhere else in your system.
* @param cacheKey Should be a stable and unique key inside the `run()`. See [resumability](https://trigger.dev/docs/documentation/concepts/resumability) for more information.
@@ -447,19 +528,23 @@ export class IO {
cacheKey: string | any[],
url: string,
requestInit?: FetchRequestInit,
retry?: FetchRetryOptions,
timeout?: FetchTimeoutOptions
options?: {
retry?: FetchRetryOptions;
timeout?: FetchTimeoutOptions;
}
): Promise<TResponseData> {
const urlObject = new URL(url);
return (await this.runTask(
cacheKey,
async (task) => {
console.log("task context", task.context);
return task.output;
},
{
name: `fetch ${urlObject.hostname}${urlObject.pathname}`,
params: { url, requestInit, retry, timeout },
params: { url, requestInit, retry: options?.retry, timeout: options?.timeout },
operation: "fetch",
icon: "background",
noop: false,
@@ -477,7 +562,9 @@ export class IO {
label: "background",
text: "true",
},
...(timeout ? [{ label: "timeout", text: `${timeout.durationInMs}ms` }] : []),
...(options?.timeout
? [{ label: "timeout", text: `${options.timeout.durationInMs}ms` }]
: []),
],
retry: {
limit: 0,
@@ -486,6 +573,135 @@ export class IO {
)) as TResponseData;
}
/** `io.backgroundPoll()` will fetch data from a URL on an interval. The actual `fetch` requests are performed on the Trigger.dev server, so you don't have to worry about serverless function timeouts.
* @param cacheKey Should be a stable and unique key inside the `run()`. See [resumability](https://trigger.dev/docs/documentation/concepts/resumability) for more information.
* @param params The options for the background poll
* @param params.url The URL to fetch from.
* @param params.requestInit The options for the request, like headers and method
* @param params.responseFilter An [EventFilter](https://trigger.dev/docs/documentation/guides/event-filter) that allows you to specify when to stop polling.
* @param params.interval The interval in seconds to poll the URL in seconds. Defaults to 10 seconds which is the minimum.
* @param params.timeout The timeout in seconds for each request in seconds. Defaults to 10 minutes. Minimum is 60 seconds and max is 1 hour
* @param params.requestTimeout An optional object that allows you to timeout individual fetch requests
* @param params.requestTimeout An optional object that allows you to timeout individual fetch requests
* @param params.requestTimeout.durationInMs The duration in milliseconds to timeout the request
*
* @example
* ```ts
* const result = await io.backgroundPoll<{ id: string; status: string; }>("poll", {
url: `http://localhost:3030/api/v1/runs/${run.id}`,
requestInit: {
headers: {
Accept: "application/json",
Authorization: redactString`Bearer ${process.env["TRIGGER_API_KEY"]!}`,
},
},
interval: 10,
timeout: 600,
responseFilter: {
status: [200],
body: {
status: ["SUCCESS"],
},
},
});
* ```
*/
async backgroundPoll<TResponseData>(
cacheKey: string | any[],
params: FetchPollOperation
): Promise<TResponseData> {
const urlObject = new URL(params.url);
return (await this.runTask(
cacheKey,
async (task) => {
return task.output;
},
{
name: `poll ${urlObject.hostname}${urlObject.pathname}`,
params,
operation: "fetch-poll",
icon: "clock-bolt",
noop: false,
properties: [
{
label: "url",
text: params.url,
},
{
label: "interval",
text: `${params.interval}s`,
},
{
label: "timeout",
text: `${params.timeout}s`,
},
],
retry: {
limit: 0,
},
}
)) as TResponseData;
}
/** `io.backgroundFetchResponse()` fetches data from a URL that can take longer that the serverless timeout. The actual `fetch` request is performed on the Trigger.dev platform, and the response is sent back to you.
* @param cacheKey Should be a stable and unique key inside the `run()`. See [resumability](https://trigger.dev/docs/documentation/concepts/resumability) for more information.
* @param url The URL to fetch from.
* @param requestInit The options for the request
* @param retry The options for retrying the request if it fails
* An object where the key is a status code pattern and the value is a retrying strategy.
* Supported patterns are:
* - Specific status codes: 429
* - Ranges: 500-599
* - Wildcards: 2xx, 3xx, 4xx, 5xx
*/
async backgroundFetchResponse<TResponseData>(
cacheKey: string | any[],
url: string,
requestInit?: FetchRequestInit,
options?: {
retry?: FetchRetryOptions;
timeout?: FetchTimeoutOptions;
}
): Promise<BackgroundFetchResponse<TResponseData>> {
const urlObject = new URL(url);
return (await this.runTask(
cacheKey,
async (task) => {
return task.output;
},
{
name: `fetch response ${urlObject.hostname}${urlObject.pathname}`,
params: { url, requestInit, retry: options?.retry, timeout: options?.timeout },
operation: "fetch-response",
icon: "background",
noop: false,
properties: [
{
label: "url",
text: url,
url,
},
{
label: "method",
text: requestInit?.method ?? "GET",
},
{
label: "background",
text: "true",
},
...(options?.timeout
? [{ label: "timeout", text: `${options.timeout.durationInMs}ms` }]
: []),
],
retry: {
limit: 0,
},
}
)) as BackgroundFetchResponse<TResponseData>;
}
/** `io.sendEvent()` allows you to send an event from inside a Job run. The sent event will trigger any Jobs that are listening for that event (based on the name).
* @param cacheKey Should be a stable and unique key inside the `run()`. See [resumability](https://trigger.dev/docs/documentation/concepts/resumability) for more information.
* @param event The event to send. The event name must match the name of the event that your Jobs are listening for.
@@ -835,7 +1051,7 @@ export class IO {
async runTask<T extends Json<T> | void>(
cacheKey: string | any[],
callback: (task: ServerTask, io: IO) => Promise<T>,
options?: RunTaskOptions,
options?: RunTaskOptions & { parseOutput?: (output: unknown) => T },
onError?: RunTaskErrorCallback
): Promise<T> {
this.#detectAutoYield("start_task", 500);
@@ -879,7 +1095,9 @@ export class IO {
this._stats.cachedTaskHits++;
return cachedTask.output as T;
return options?.parseOutput
? options.parseOutput(cachedTask.output)
: (cachedTask.output as T);
}
if (options?.noop && this._noopTasksBloomFilter) {
@@ -894,13 +1112,15 @@ export class IO {
}
}
const runOptions = { ...(options ?? {}), parseOutput: undefined };
const response = await this._apiClient.runTask(
this._id,
{
idempotencyKey,
displayKey: typeof cacheKey === "string" ? cacheKey : undefined,
noop: false,
...(options ?? {}),
...(runOptions ?? {}),
parentId,
},
{
@@ -962,7 +1182,7 @@ export class IO {
this.#addToCachedTasks(task);
}
return task.output as T;
return options?.parseOutput ? options.parseOutput(task.output) : (task.output as T);
}
if (task.status === "ERRORED") {
@@ -1021,7 +1241,9 @@ export class IO {
this.#detectAutoYield("after_complete_task", 500);
return this._outputSerializer.deserialize<T>(output);
const deserializedOutput = this._outputSerializer.deserialize<T>(output);
return options?.parseOutput ? options.parseOutput(deserializedOutput) : deserializedOutput;
} catch (error) {
if (isTriggerError(error)) {
throw error;
+7
View File
@@ -12,4 +12,11 @@ export const retry = {
maxTimeoutInMs: 30000,
randomize: true,
},
exponentialBackoff: {
limit: 8,
factor: 2,
minTimeoutInMs: 1000,
maxTimeoutInMs: 30000,
randomize: true,
},
} as const satisfies Record<string, RetryOptions>;
@@ -2,6 +2,7 @@ import {
API_VERSIONS,
ConnectionAuth,
DeserializedJson,
EphemeralEventDispatcherRequestBody,
ErrorWithStackSchema,
GetRunOptionsWithTaskDetails,
GetRunsOptions,
@@ -795,6 +796,10 @@ export class TriggerClient {
return this.#client.invokeJob(jobId, payload, options);
}
async createEphemeralEventDispatcher(payload: EphemeralEventDispatcherRequestBody) {
return this.#client.createEphemeralEventDispatcher(payload);
}
authorized(
apiKey?: string | null
): "authorized" | "unauthorized" | "missing-client" | "missing-header" {
+22
View File
@@ -143,3 +143,25 @@ export type SchemaParserResult<T> =
export type SchemaParser<T extends unknown = unknown> = {
safeParse: (a: unknown) => SchemaParserResult<T>;
};
export type WaitForEventResult<TEvent> = {
id: string;
name: string;
source: string;
payload: TEvent;
timestamp: Date;
context?: any;
accountId?: string;
};
export function waitForEventSchema(schema: z.ZodTypeAny) {
return z.object({
id: z.string(),
name: z.string(),
source: z.string(),
payload: schema,
timestamp: z.coerce.date(),
context: z.any().optional(),
accountId: z.string().optional(),
});
}
+103 -12
View File
@@ -466,19 +466,25 @@ importers:
'@trigger.dev/integration-kit': workspace:^2.2.5
'@trigger.dev/sdk': workspace:^2.2.5
'@trigger.dev/tsconfig': workspace:*
'@types/jest': ^29.5.3
'@types/node': '18'
openai: ^4.13.0
jest: ^29.6.2
openai: ^4.16.1
rimraf: ^3.0.2
ts-jest: ^29.1.1
tsup: ^6.5.0
typescript: ^4.9.4
dependencies:
'@trigger.dev/integration-kit': link:../../packages/integration-kit
'@trigger.dev/sdk': link:../../packages/trigger-sdk
openai: 4.13.0
openai: 4.16.1
devDependencies:
'@trigger.dev/tsconfig': link:../../config-packages/tsconfig
'@types/jest': 29.5.3
'@types/node': 18.15.13
jest: 29.6.2_@types+node@18.15.13
rimraf: 3.0.2
ts-jest: 29.1.1_xlkreayjyan5lfqjb5gzdqf3my
tsup: 6.6.3_typescript@4.9.5
typescript: 4.9.5
@@ -2272,11 +2278,6 @@ packages:
resolution: {integrity: sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==}
engines: {node: '>=6.9.0'}
/@babel/helper-validator-identifier/7.19.1:
resolution: {integrity: sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==}
engines: {node: '>=6.9.0'}
dev: true
/@babel/helper-validator-identifier/7.22.15:
resolution: {integrity: sha512-4E/F9IIEi8WR94324mbDUMo074YTheJmd7eZF5vITTeYchqAi6sYXRLHUVsmkdmY4QjfKTcB2jB7dVP3NaBElQ==}
engines: {node: '>=6.9.0'}
@@ -5202,7 +5203,7 @@ packages:
engines: {node: '>=6.9.0'}
dependencies:
'@babel/helper-string-parser': 7.21.5
'@babel/helper-validator-identifier': 7.19.1
'@babel/helper-validator-identifier': 7.22.15
to-fast-properties: 2.0.0
dev: true
@@ -20592,7 +20593,7 @@ packages:
chalk: 4.1.2
chokidar: 3.5.3
cosmiconfig: 7.1.0
deepmerge: 4.2.2
deepmerge: 4.3.1
fs-extra: 10.1.0
memfs: 3.5.3
minimatch: 3.1.2
@@ -22651,6 +22652,35 @@ packages:
- ts-node
dev: true
/jest-cli/29.6.2_@types+node@18.15.13:
resolution: {integrity: sha512-TT6O247v6dCEX2UGHGyflMpxhnrL0DNqP2fRTKYm3nJJpCTfXX3GCMQPGFjXDoj0i5/Blp3jriKXFgdfmbYB6Q==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
hasBin: true
peerDependencies:
node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0
peerDependenciesMeta:
node-notifier:
optional: true
dependencies:
'@jest/core': 29.6.2
'@jest/test-result': 29.6.2
'@jest/types': 29.6.1
chalk: 4.1.2
exit: 0.1.2
graceful-fs: 4.2.10
import-local: 3.1.0
jest-config: 29.6.2_@types+node@18.15.13
jest-util: 29.6.2
jest-validate: 29.6.2
prompts: 2.4.2
yargs: 17.7.2
transitivePeerDependencies:
- '@types/node'
- babel-plugin-macros
- supports-color
- ts-node
dev: true
/jest-cli/29.6.2_@types+node@18.17.1:
resolution: {integrity: sha512-TT6O247v6dCEX2UGHGyflMpxhnrL0DNqP2fRTKYm3nJJpCTfXX3GCMQPGFjXDoj0i5/Blp3jriKXFgdfmbYB6Q==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@@ -22720,6 +22750,46 @@ packages:
- supports-color
dev: true
/jest-config/29.6.2_@types+node@18.15.13:
resolution: {integrity: sha512-VxwFOC8gkiJbuodG9CPtMRjBUNZEHxwfQXmIudSTzFWxaci3Qub1ddTRbFNQlD/zUeaifLndh/eDccFX4wCMQw==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
peerDependencies:
'@types/node': '*'
ts-node: '>=9.0.0'
peerDependenciesMeta:
'@types/node':
optional: true
ts-node:
optional: true
dependencies:
'@babel/core': 7.22.17
'@jest/test-sequencer': 29.6.2
'@jest/types': 29.6.1
'@types/node': 18.15.13
babel-jest: 29.6.2_@babel+core@7.22.17
chalk: 4.1.2
ci-info: 3.8.0
deepmerge: 4.3.1
glob: 7.2.3
graceful-fs: 4.2.10
jest-circus: 29.6.2
jest-environment-node: 29.6.2
jest-get-type: 29.4.3
jest-regex-util: 29.4.3
jest-resolve: 29.6.2
jest-runner: 29.6.2
jest-util: 29.6.2
jest-validate: 29.6.2
micromatch: 4.0.5
parse-json: 5.2.0
pretty-format: 29.6.2
slash: 3.0.0
strip-json-comments: 3.1.1
transitivePeerDependencies:
- babel-plugin-macros
- supports-color
dev: true
/jest-config/29.6.2_@types+node@18.17.1:
resolution: {integrity: sha512-VxwFOC8gkiJbuodG9CPtMRjBUNZEHxwfQXmIudSTzFWxaci3Qub1ddTRbFNQlD/zUeaifLndh/eDccFX4wCMQw==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@@ -23140,6 +23210,27 @@ packages:
- ts-node
dev: true
/jest/29.6.2_@types+node@18.15.13:
resolution: {integrity: sha512-8eQg2mqFbaP7CwfsTpCxQ+sHzw1WuNWL5UUvjnWP4hx2riGz9fPSzYOaU5q8/GqWn1TfgZIVTqYJygbGbWAANg==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
hasBin: true
peerDependencies:
node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0
peerDependenciesMeta:
node-notifier:
optional: true
dependencies:
'@jest/core': 29.6.2
'@jest/types': 29.6.1
import-local: 3.1.0
jest-cli: 29.6.2_@types+node@18.15.13
transitivePeerDependencies:
- '@types/node'
- babel-plugin-macros
- supports-color
- ts-node
dev: true
/jest/29.6.2_@types+node@18.17.1:
resolution: {integrity: sha512-8eQg2mqFbaP7CwfsTpCxQ+sHzw1WuNWL5UUvjnWP4hx2riGz9fPSzYOaU5q8/GqWn1TfgZIVTqYJygbGbWAANg==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@@ -25740,8 +25831,8 @@ packages:
is-docker: 2.2.1
is-wsl: 2.2.0
/openai/4.13.0:
resolution: {integrity: sha512-EPqHcB0got9cXDZmQae1KytgA4YWtTnUc7tV8hlahZtcO70DMa4kiaXoxnutj9lwmeKQO7ntG+6pmXtrCMejuQ==}
/openai/4.16.1:
resolution: {integrity: sha512-Gr+uqUN1ICSk6VhrX64E+zL7skjI1TgPr/XUN+ZQuNLLOvx15+XZulx/lSW4wFEAQzgjBDlMBbBeikguGIjiMg==}
hasBin: true
dependencies:
'@types/node': 18.17.1
@@ -30445,7 +30536,7 @@ packages:
dependencies:
bs-logger: 0.2.6
fast-json-stable-stringify: 2.1.0
jest: 29.6.2_@types+node@16.18.11
jest: 29.6.2_@types+node@18.15.13
jest-util: 29.6.2
json5: 2.2.3
lodash.memoize: 4.1.2
@@ -0,0 +1,52 @@
Date,Amount,CustomerID,SKU,Fullfilled
2023-01-01,10.5,abc,123,true
2023-01-02,15.2,def,456,false
2023-01-03,8.9,ghi,789,true
2023-01-04,12.3,jkl,234,false
2023-01-05,9.7,mno,567,true
2023-01-06,14.5,pqr,890,false
2023-01-07,11.0,stu,345,true
2023-01-08,16.3,vwx,678,false
2023-01-09,13.2,yz,901,true
2023-01-10,10.8,abc,234,false
2023-01-11,12.1,def,567,true
2023-01-12,14.9,ghi,890,false
2023-01-13,9.6,jkl,123,true
2023-01-14,11.4,mno,456,false
2023-01-15,13.7,pqr,789,true
2023-01-16,15.1,stu,234,false
2023-01-17,8.2,vwx,567,true
2023-01-18,9.5,yz,890,false
2023-01-19,11.8,abc,123,true
2023-01-20,16.5,def,456,false
2023-01-21,10.9,ghi,789,true
2023-01-22,12.6,jkl,234,false
2023-01-23,14.3,mno,567,true
2023-01-24,11.6,pqr,890,false
2023-01-25,9.3,stu,345,true
2023-01-26,16.2,vwx,678,false
2023-01-27,13.5,yz,901,true
2023-01-28,10.2,abc,234,false
2023-01-29,12.9,def,567,true
2023-01-30,15.4,ghi,890,false
2023-01-31,11.7,jkl,123,true
2023-02-01,13.9,mno,456,false
2023-02-02,9.8,pqr,789,true
2023-02-03,12.7,stu,234,false
2023-02-04,16.1,vwx,567,true
2023-02-05,10.3,yz,890,false
2023-02-06,11.5,abc,123,true
2023-02-07,14.2,def,456,false
2023-02-08,13.8,ghi,789,true
2023-02-09,9.9,jkl,234,false
2023-02-10,12.4,mno,567,true
2023-02-11,15.6,pqr,890,false
2023-02-12,11.3,stu,345,true
2023-02-13,13.4,vwx,678,false
2023-02-14,10.1,yz,901,true
2023-02-15,12.8,abc,234,false
2023-02-16,14.6,def,567,true
2023-02-17,9.2,ghi,890,false
2023-02-18,11.9,jkl,123,true
2023-02-19,13.1,mno,456,false
2023-02-20,15.9,pqr,789,true
1 Date Amount CustomerID SKU Fullfilled
2 2023-01-01 10.5 abc 123 true
3 2023-01-02 15.2 def 456 false
4 2023-01-03 8.9 ghi 789 true
5 2023-01-04 12.3 jkl 234 false
6 2023-01-05 9.7 mno 567 true
7 2023-01-06 14.5 pqr 890 false
8 2023-01-07 11.0 stu 345 true
9 2023-01-08 16.3 vwx 678 false
10 2023-01-09 13.2 yz 901 true
11 2023-01-10 10.8 abc 234 false
12 2023-01-11 12.1 def 567 true
13 2023-01-12 14.9 ghi 890 false
14 2023-01-13 9.6 jkl 123 true
15 2023-01-14 11.4 mno 456 false
16 2023-01-15 13.7 pqr 789 true
17 2023-01-16 15.1 stu 234 false
18 2023-01-17 8.2 vwx 567 true
19 2023-01-18 9.5 yz 890 false
20 2023-01-19 11.8 abc 123 true
21 2023-01-20 16.5 def 456 false
22 2023-01-21 10.9 ghi 789 true
23 2023-01-22 12.6 jkl 234 false
24 2023-01-23 14.3 mno 567 true
25 2023-01-24 11.6 pqr 890 false
26 2023-01-25 9.3 stu 345 true
27 2023-01-26 16.2 vwx 678 false
28 2023-01-27 13.5 yz 901 true
29 2023-01-28 10.2 abc 234 false
30 2023-01-29 12.9 def 567 true
31 2023-01-30 15.4 ghi 890 false
32 2023-01-31 11.7 jkl 123 true
33 2023-02-01 13.9 mno 456 false
34 2023-02-02 9.8 pqr 789 true
35 2023-02-03 12.7 stu 234 false
36 2023-02-04 16.1 vwx 567 true
37 2023-02-05 10.3 yz 890 false
38 2023-02-06 11.5 abc 123 true
39 2023-02-07 14.2 def 456 false
40 2023-02-08 13.8 ghi 789 true
41 2023-02-09 9.9 jkl 234 false
42 2023-02-10 12.4 mno 567 true
43 2023-02-11 15.6 pqr 890 false
44 2023-02-12 11.3 stu 345 true
45 2023-02-13 13.4 vwx 678 false
46 2023-02-14 10.1 yz 901 true
47 2023-02-15 12.8 abc 234 false
48 2023-02-16 14.6 def 567 true
49 2023-02-17 9.2 ghi 890 false
50 2023-02-18 11.9 jkl 123 true
51 2023-02-19 13.1 mno 456 false
52 2023-02-20 15.9 pqr 789 true
+145 -1
View File
@@ -1,5 +1,6 @@
import { createExpressServer } from "@trigger.dev/express";
import { TriggerClient, eventTrigger, invokeTrigger } from "@trigger.dev/sdk";
import { TriggerClient, eventTrigger, invokeTrigger, redactString } from "@trigger.dev/sdk";
import { z } from "zod";
export const client = new TriggerClient({
id: "job-catalog",
@@ -100,6 +101,111 @@ client.defineJob({
},
});
client.defineJob({
id: "screenshot-one-example",
name: "Screenshot One Example",
version: "1.0.0",
trigger: invokeTrigger({
schema: z.object({
url: z.string().url().default("https://trigger.dev"),
}),
}),
run: async (payload, io, ctx) => {
const result = await io.waitForRequest(
"screenshot-one",
async (url) => {
await fetch(`https://api.screenshotone.com/take`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
access_key: process.env["SCREENSHOT_ONE_API_KEY"]!,
url: payload.url,
store: "true",
storage_path: "my-screeshots",
response_type: "json",
async: "true",
webhook_url: url, // this is the URL that will be called when the screenshot is ready
storage_return_location: "true",
}),
});
},
{
timeoutInSeconds: 300,
}
);
},
});
const pollingRunJob = client.defineJob({
id: "polling-run",
name: "Background Poll Run",
version: "1.0.0",
trigger: invokeTrigger(),
run: async (payload, io, ctx) => {
await io.wait("wait", 30);
return {
foo: "bar",
};
},
});
client.defineJob({
id: "background-poll",
name: "Background Poll Example",
version: "1.0.0",
trigger: invokeTrigger(),
run: async (payload, io, ctx) => {
const run = await pollingRunJob.invoke("invoke");
// TODO invoke a run and then use the run ID to poll for the result
const result = await io.backgroundPoll<{ message: string }>("poll", {
url: `http://localhost:3030/api/v1/runs/${run.id}`,
requestInit: {
headers: {
Accept: "application/json",
Authorization: redactString`Bearer ${process.env["TRIGGER_API_KEY"]!}`,
},
},
interval: 10,
timeout: 300,
responseFilter: {
status: [200],
body: {
status: ["SUCCESS"],
},
},
});
},
});
const sendWaitForEventJob = client.defineJob({
id: "send-wait-for-event",
name: "Send Wait for Event Example",
version: "1.0.0",
trigger: invokeTrigger(),
run: async (payload, io, ctx) => {
await io.wait("wait", 1);
await io.sendEvent("send-event", {
name: "wait.for.event",
payload: {
jobId: "wait-for-event",
foo: "bar",
ts: new Date(),
},
context: ctx,
});
await io.sendEvent("send-event-1", {
name: "wait.for.event",
payload,
context: ctx,
});
},
});
client.defineJob({
id: "send-event-example",
name: "Send Event Example",
@@ -114,6 +220,44 @@ client.defineJob({
},
});
client.defineJob({
id: "wait-for-event",
name: "Wait for Event Example",
version: "1.0.0",
trigger: invokeTrigger(),
run: async (payload, io, ctx) => {
await sendWaitForEventJob.invoke("invoke", {
jobId: "send-wait-for-event",
foo: "bar",
ts: new Date(),
});
const event = await io.waitForEvent(
"wait",
{
name: "wait.for.event",
schema: z.object({
jobId: z.string(),
foo: z.string(),
ts: z.coerce.date(),
}),
filter: {
jobId: ["send-wait-for-event"], // only wait for events from this job
},
},
{
timeoutInSeconds: 60,
}
);
await io.logger.info("Event received", {
event,
tsType: typeof event.payload.ts,
timestampType: typeof event.timestamp,
});
},
});
client.defineJob({
id: "send-events-example",
name: "Send Multiple Events Example",
+268 -1
View File
@@ -1,6 +1,9 @@
import { createExpressServer } from "@trigger.dev/express";
import { TriggerClient, eventTrigger } from "@trigger.dev/sdk";
import { TriggerClient, eventTrigger, invokeTrigger } from "@trigger.dev/sdk";
import { OpenAI } from "@trigger.dev/openai";
import fs from "node:fs";
import fsPromises from "node:fs/promises";
import { z } from "zod";
export const client = new TriggerClient({
id: "job-catalog",
@@ -78,6 +81,270 @@ client.defineJob({
},
});
client.defineJob({
id: "openai-gpt-4-turbo",
name: "OpenAI GPT 4 Turbo",
version: "0.0.1",
trigger: invokeTrigger(),
integrations: {
openai,
},
run: async (payload, io, ctx) => {
await io.openai.chat.completions.create("background-chat-completion", {
model: "gpt-4-1106-preview",
messages: [
{
role: "user",
content:
"Create a good programming joke about background jobs, including something about Trigger.dev",
},
],
});
},
});
client.defineJob({
id: "openai-dalle-3",
name: "OpenAI Dalle 3",
version: "0.0.1",
trigger: invokeTrigger(),
integrations: {
openai,
},
run: async (payload, io, ctx) => {
await io.openai.images.create("dalle-3", {
model: "dall-e-3",
prompt:
"I would like to generate an image of an american boy riding a bycicle in a suburban neighborhood, into the sunset.",
});
await io.openai.images.backgroundCreate("dalle-3-background", {
model: "dall-e-3",
prompt:
"Create a comic strip featuring miles morales and spiderpunk fighting off the sinister six",
});
},
});
client.defineJob({
id: "openai-background-completion",
name: "OpenAI Background Completion",
version: "0.0.1",
trigger: invokeTrigger(),
integrations: {
openai,
},
run: async (payload, io, ctx) => {
await io.openai.chat.completions.backgroundCreate("completion 1", {
model: "gpt-4",
messages: [
{
role: "user",
content: "What is the difference between green threads and native threads in Python?",
},
],
});
},
});
client.defineJob({
id: "openai-rate-limit-handling",
name: "OpenAI GPT Rate Limits",
version: "0.0.1",
trigger: invokeTrigger(),
integrations: {
openai,
},
run: async (payload, io, ctx) => {
await io.openai.chat.completions.backgroundCreate("completion 1", {
model: "gpt-4-1106-preview",
messages: [
{
role: "user",
content:
'I want you to act as a debater. I will provide you with some topics related to current events and your task is to research both sides of the debates, present valid arguments for each side, refute opposing points of view, and draw persuasive conclusions based on evidence. Your goal is to help people come away from the discussion with increased knowledge and insight into the topic at hand. My first request is "I want an opinion piece about Deno."',
},
],
});
await io.openai.chat.completions.backgroundCreate("completion 2", {
model: "gpt-4-1106-preview",
messages: [
{
role: "user",
content:
'I want you to act as a movie critic. You will develop an engaging and creative movie review. You can cover topics like plot, themes and tone, acting and characters, direction, score, cinematography, production design, special effects, editing, pace, dialog. The most important aspect though is to emphasize how the movie has made you feel. What has really resonated with you. You can also be critical about the movie. Please avoid spoilers. My first request is "I need to write a movie review for the movie Interstellar"',
},
],
});
await io.openai.chat.completions.backgroundCreate(
"completion 3",
{
model: "gpt-4-1106-preview",
messages: [
{
role: "user",
content: ` want you to act as a motivational speaker. Put together words that inspire action and make people feel empowered to do something beyond their abilities. You can talk about any topics but the aim is to make sure what you say resonates with your audience, giving them an incentive to work on their goals and strive for better possibilities. My first request is "I need a speech about how everyone should never give up."`,
},
],
},
{},
{ timeout: { durationInMs: 30000, retry: { limit: 1 } } }
);
},
});
client.defineJob({
id: "openai-create-assistant",
name: "OpenAI GPT Create Assistant",
version: "0.0.1",
trigger: invokeTrigger({
schema: z.object({
model: z.string().default("gpt-4-1106-preview"),
}),
}),
integrations: {
openai,
},
run: async (payload, io, ctx) => {
const file = await io.openai.files.createAndWaitForProcessing("upload-file", {
purpose: "assistants",
file: fs.createReadStream("./fixtures/mydata.csv"),
});
const assistant = await io.openai.beta.assistants.create("create-assistant", {
name: "Data visualizer",
description:
"You are great at creating beautiful data visualizations. You analyze data present in .csv files, understand trends, and come up with data visualizations relevant to those trends. You also share a brief text summary of the trends observed.",
model: payload.model,
tools: [{ type: "code_interpreter" }],
file_ids: [file.id],
});
// Really we would want to save the assistant id somewhere
return { assistant, file };
},
});
client.defineJob({
id: "openai-use-assistant",
name: "OpenAI GPT Use Assistant",
version: "0.0.1",
trigger: invokeTrigger({
schema: z.object({
id: z.string(),
fileId: z.string(),
}),
}),
integrations: {
openai,
},
run: async (payload, io, ctx) => {
const run = await io.openai.beta.threads.createAndRunUntilCompletion("create-thread", {
assistant_id: payload.id,
thread: {
messages: [
{
role: "user",
content: "Create 3 data visualizations based on the trends in this file.",
file_ids: [payload.fileId],
},
],
},
});
if (run.status !== "completed") {
throw new Error(`Run finished with status ${run.status}: ${JSON.stringify(run.last_error)}`);
}
const messages = await io.openai.beta.threads.messages.list("list-messages", run.thread_id);
const reversedMessages = [...messages].reverse();
await io.runTask(
"log-messages",
async (task) => {
for (const message of reversedMessages) {
switch (message.role) {
case "user": {
for (const content of message.content) {
switch (content.type) {
case "text": {
await io.logger.info(`Assistant: ${content.text.value}`);
break;
}
case "image_file": {
const file = await io.openai.files.retrieve(
["file", content.image_file.file_id],
content.image_file.file_id
);
const fileContent = await io.openai.native.files.retrieveContent(
content.image_file.file_id
);
const filePath = `tmp/${file.filename}`;
// Use fsPromises to write the file to disk at tmp/file.fileName
await fsPromises.writeFile(filePath, fileContent);
await io.logger.info(
`Assistant: retrieved file ${content.image_file.file_id} at ${filePath}`
);
}
}
}
break;
}
case "assistant": {
for (const content of message.content) {
switch (content.type) {
case "text": {
await io.logger.info(`Assistant: ${content.text.value}`);
break;
}
case "image_file": {
const file = await io.openai.files.retrieve(
["file", content.image_file.file_id],
content.image_file.file_id
);
const fileContent = await io.openai.native.files.retrieveContent(
content.image_file.file_id
);
const filePath = `tmp/${file.filename}`;
// Use fsPromises to write the file to disk at tmp/file.fileName
await fsPromises.writeFile(filePath, fileContent);
await io.logger.info(
`Assistant: retrieved file ${content.image_file.file_id} at ${filePath}`
);
}
}
}
break;
}
}
}
},
{
name: "Log messages",
icon: "openai",
}
);
return run;
},
});
const perplexity = new OpenAI({
id: "perplexity",
apiKey: process.env["PERPLEXITY_API_KEY"]!,