mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-06-06 19:35:44 +02:00
commit
6fc6abc2bc
107 changed files with 2359 additions and 649 deletions
|
|
@ -1,13 +1,15 @@
|
|||
"use server";
|
||||
import { auth0 } from "../lib/auth0";
|
||||
import { USE_AUTH } from "../lib/feature_flags";
|
||||
import { WithStringId, User } from "../lib/types/types";
|
||||
import { User } from "@/src/entities/models/user";
|
||||
import { getUserFromSessionId, GUEST_DB_USER } from "../lib/auth";
|
||||
import { z } from "zod";
|
||||
import { ObjectId } from "mongodb";
|
||||
import { usersCollection } from "../lib/mongodb";
|
||||
import { container } from "@/di/container";
|
||||
import { IUsersRepository } from "@/src/application/repositories/users.repository.interface";
|
||||
|
||||
export async function authCheck(): Promise<WithStringId<z.infer<typeof User>>> {
|
||||
const usersRepository = container.resolve<IUsersRepository>("usersRepository");
|
||||
|
||||
export async function authCheck(): Promise<z.infer<typeof User>> {
|
||||
if (!USE_AUTH) {
|
||||
return GUEST_DB_USER;
|
||||
}
|
||||
|
|
@ -42,12 +44,5 @@ export async function updateUserEmail(email: string) {
|
|||
}
|
||||
|
||||
// update customer email in db
|
||||
await usersCollection.updateOne({
|
||||
_id: new ObjectId(user._id),
|
||||
}, {
|
||||
$set: {
|
||||
email,
|
||||
updatedAt: new Date().toISOString(),
|
||||
}
|
||||
});
|
||||
await usersRepository.updateEmail(user.id, email);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,9 +21,8 @@ import {
|
|||
ModelsResponse
|
||||
} from "../lib/types/billing_types";
|
||||
import { z } from "zod";
|
||||
import { WithStringId } from "../lib/types/types";
|
||||
|
||||
export async function getCustomer(): Promise<WithStringId<z.infer<typeof Customer>>> {
|
||||
export async function getCustomer(): Promise<z.infer<typeof Customer>> {
|
||||
const user = await authCheck();
|
||||
if (!user.billingCustomerId) {
|
||||
throw new Error("Customer not found");
|
||||
|
|
@ -41,7 +40,7 @@ export async function authorizeUserAction(request: z.infer<typeof AuthorizeReque
|
|||
}
|
||||
|
||||
const customer = await getCustomer();
|
||||
const response = await authorize(customer._id, request);
|
||||
const response = await authorize(customer.id, request);
|
||||
return response;
|
||||
}
|
||||
|
||||
|
|
@ -51,7 +50,7 @@ export async function logUsage(request: z.infer<typeof LogUsageRequest>) {
|
|||
}
|
||||
|
||||
const customer = await getCustomer();
|
||||
await libLogUsage(customer._id, request);
|
||||
await libLogUsage(customer.id, request);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -61,7 +60,7 @@ export async function getCustomerPortalUrl(returnUrl: string): Promise<string> {
|
|||
}
|
||||
|
||||
const customer = await getCustomer();
|
||||
return await createCustomerPortalSession(customer._id, returnUrl);
|
||||
return await createCustomerPortalSession(customer.id, returnUrl);
|
||||
}
|
||||
|
||||
export async function getPrices(): Promise<z.infer<typeof PricesResponse>> {
|
||||
|
|
@ -80,7 +79,7 @@ export async function updateSubscriptionPlan(plan: z.infer<typeof SubscriptionPl
|
|||
|
||||
const customer = await getCustomer();
|
||||
const request: z.infer<typeof UpdateSubscriptionPlanRequest> = { plan, returnUrl };
|
||||
const url = await libUpdateSubscriptionPlan(customer._id, request);
|
||||
const url = await libUpdateSubscriptionPlan(customer.id, request);
|
||||
return url;
|
||||
}
|
||||
|
||||
|
|
@ -90,6 +89,6 @@ export async function getEligibleModels(): Promise<z.infer<typeof ModelsResponse
|
|||
}
|
||||
|
||||
const customer = await getCustomer();
|
||||
const response = await libGetEligibleModels(customer._id);
|
||||
const response = await libGetEligibleModels(customer.id);
|
||||
return response;
|
||||
}
|
||||
|
|
@ -49,7 +49,7 @@ export async function listToolkits(projectId: string, cursor: string | null = nu
|
|||
const user = await authCheck();
|
||||
return await listComposioToolkitsController.execute({
|
||||
caller: 'user',
|
||||
userId: user._id,
|
||||
userId: user.id,
|
||||
projectId,
|
||||
cursor,
|
||||
});
|
||||
|
|
@ -59,7 +59,7 @@ export async function getToolkit(projectId: string, toolkitSlug: string): Promis
|
|||
const user = await authCheck();
|
||||
return await getComposioToolkitController.execute({
|
||||
caller: 'user',
|
||||
userId: user._id,
|
||||
userId: user.id,
|
||||
projectId,
|
||||
toolkitSlug,
|
||||
});
|
||||
|
|
@ -69,7 +69,7 @@ export async function listTools(projectId: string, toolkitSlug: string, searchQu
|
|||
const user = await authCheck();
|
||||
return await listComposioToolsController.execute({
|
||||
caller: 'user',
|
||||
userId: user._id,
|
||||
userId: user.id,
|
||||
projectId,
|
||||
toolkitSlug,
|
||||
searchQuery,
|
||||
|
|
@ -81,7 +81,7 @@ export async function createComposioManagedOauth2ConnectedAccount(projectId: str
|
|||
const user = await authCheck();
|
||||
return await createComposioManagedConnectedAccountController.execute({
|
||||
caller: 'user',
|
||||
userId: user._id,
|
||||
userId: user.id,
|
||||
projectId,
|
||||
toolkitSlug,
|
||||
callbackUrl,
|
||||
|
|
@ -92,7 +92,7 @@ export async function createCustomConnectedAccount(projectId: string, request: z
|
|||
const user = await authCheck();
|
||||
return await createCustomConnectedAccountController.execute({
|
||||
caller: 'user',
|
||||
userId: user._id,
|
||||
userId: user.id,
|
||||
projectId,
|
||||
toolkitSlug: request.toolkitSlug,
|
||||
authConfig: request.authConfig,
|
||||
|
|
@ -104,7 +104,7 @@ export async function syncConnectedAccount(projectId: string, toolkitSlug: strin
|
|||
const user = await authCheck();
|
||||
return await syncConnectedAccountController.execute({
|
||||
caller: 'user',
|
||||
userId: user._id,
|
||||
userId: user.id,
|
||||
projectId,
|
||||
toolkitSlug,
|
||||
connectedAccountId,
|
||||
|
|
@ -116,7 +116,7 @@ export async function deleteConnectedAccount(projectId: string, toolkitSlug: str
|
|||
|
||||
await deleteComposioConnectedAccountController.execute({
|
||||
caller: 'user',
|
||||
userId: user._id,
|
||||
userId: user.id,
|
||||
projectId,
|
||||
toolkitSlug,
|
||||
});
|
||||
|
|
@ -144,7 +144,7 @@ export async function createComposioTriggerDeployment(request: {
|
|||
// create trigger deployment
|
||||
return await createComposioTriggerDeploymentController.execute({
|
||||
caller: 'user',
|
||||
userId: user._id,
|
||||
userId: user.id,
|
||||
projectId: request.projectId,
|
||||
data: {
|
||||
triggerTypeSlug: request.triggerTypeSlug,
|
||||
|
|
@ -163,7 +163,7 @@ export async function listComposioTriggerDeployments(request: {
|
|||
// list trigger deployments
|
||||
return await listComposioTriggerDeploymentsController.execute({
|
||||
caller: 'user',
|
||||
userId: user._id,
|
||||
userId: user.id,
|
||||
projectId: request.projectId,
|
||||
cursor: request.cursor,
|
||||
});
|
||||
|
|
@ -178,7 +178,7 @@ export async function deleteComposioTriggerDeployment(request: {
|
|||
// delete trigger deployment
|
||||
return await deleteComposioTriggerDeploymentController.execute({
|
||||
caller: 'user',
|
||||
userId: user._id,
|
||||
userId: user.id,
|
||||
projectId: request.projectId,
|
||||
deploymentId: request.deploymentId,
|
||||
});
|
||||
|
|
@ -188,7 +188,7 @@ export async function fetchComposioTriggerDeployment(request: { deploymentId: st
|
|||
const user = await authCheck();
|
||||
return await fetchComposioTriggerDeploymentController.execute({
|
||||
caller: 'user',
|
||||
userId: user._id,
|
||||
userId: user.id,
|
||||
deploymentId: request.deploymentId,
|
||||
});
|
||||
}
|
||||
|
|
@ -17,7 +17,7 @@ export async function listConversations(request: {
|
|||
|
||||
return await listConversationsController.execute({
|
||||
caller: 'user',
|
||||
userId: user._id,
|
||||
userId: user.id,
|
||||
projectId: request.projectId,
|
||||
cursor: request.cursor,
|
||||
limit: request.limit,
|
||||
|
|
@ -31,7 +31,7 @@ export async function fetchConversation(request: {
|
|||
|
||||
return await fetchConversationController.execute({
|
||||
caller: 'user',
|
||||
userId: user._id,
|
||||
userId: user.id,
|
||||
conversationId: request.conversationId,
|
||||
});
|
||||
}
|
||||
|
|
@ -28,7 +28,7 @@ export async function getCopilotResponseStream(
|
|||
streamId: string;
|
||||
} | { billingError: string }> {
|
||||
await projectAuthCheck(projectId);
|
||||
await usageQuotaPolicy.assertAndConsume(projectId);
|
||||
await usageQuotaPolicy.assertAndConsumeProjectAction(projectId);
|
||||
|
||||
// Check billing authorization
|
||||
const authResponse = await authorizeUserAction({
|
||||
|
|
@ -38,7 +38,7 @@ export async function getCopilotResponseStream(
|
|||
return { billingError: authResponse.error || 'Billing error' };
|
||||
}
|
||||
|
||||
await usageQuotaPolicy.assertAndConsume(projectId);
|
||||
await usageQuotaPolicy.assertAndConsumeProjectAction(projectId);
|
||||
|
||||
// prepare request
|
||||
const request: z.infer<typeof CopilotAPIRequest> = {
|
||||
|
|
@ -70,7 +70,7 @@ export async function getCopilotAgentInstructions(
|
|||
agentName: string,
|
||||
): Promise<string | { billingError: string }> {
|
||||
await projectAuthCheck(projectId);
|
||||
await usageQuotaPolicy.assertAndConsume(projectId);
|
||||
await usageQuotaPolicy.assertAndConsumeProjectAction(projectId);
|
||||
|
||||
// Check billing authorization
|
||||
const authResponse = await authorizeUserAction({
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ export async function addServer(projectId: string, name: string, server: McpServ
|
|||
validateUrl(server.serverUrl);
|
||||
await addCustomMcpServerController.execute({
|
||||
caller: 'user',
|
||||
userId: user._id,
|
||||
userId: user.id,
|
||||
projectId,
|
||||
name,
|
||||
server,
|
||||
|
|
@ -43,7 +43,7 @@ export async function removeServer(projectId: string, name: string): Promise<voi
|
|||
const user = await authCheck();
|
||||
await removeCustomMcpServerController.execute({
|
||||
caller: 'user',
|
||||
userId: user._id,
|
||||
userId: user.id,
|
||||
projectId,
|
||||
name,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ export async function getDataSource(sourceId: string): Promise<z.infer<typeof Da
|
|||
|
||||
return await fetchDataSourceController.execute({
|
||||
caller: 'user',
|
||||
userId: user._id,
|
||||
userId: user.id,
|
||||
sourceId,
|
||||
});
|
||||
}
|
||||
|
|
@ -45,7 +45,7 @@ export async function listDataSources(projectId: string): Promise<z.infer<typeof
|
|||
|
||||
return await listDataSourcesController.execute({
|
||||
caller: 'user',
|
||||
userId: user._id,
|
||||
userId: user.id,
|
||||
projectId,
|
||||
});
|
||||
}
|
||||
|
|
@ -66,7 +66,7 @@ export async function createDataSource({
|
|||
const user = await authCheck();
|
||||
return await createDataSourceController.execute({
|
||||
caller: 'user',
|
||||
userId: user._id,
|
||||
userId: user.id,
|
||||
data: {
|
||||
projectId,
|
||||
name,
|
||||
|
|
@ -82,7 +82,7 @@ export async function recrawlWebDataSource(sourceId: string) {
|
|||
|
||||
return await recrawlWebDataSourceController.execute({
|
||||
caller: 'user',
|
||||
userId: user._id,
|
||||
userId: user.id,
|
||||
sourceId,
|
||||
});
|
||||
}
|
||||
|
|
@ -92,7 +92,7 @@ export async function deleteDataSource(sourceId: string) {
|
|||
|
||||
return await deleteDataSourceController.execute({
|
||||
caller: 'user',
|
||||
userId: user._id,
|
||||
userId: user.id,
|
||||
sourceId,
|
||||
});
|
||||
}
|
||||
|
|
@ -102,7 +102,7 @@ export async function toggleDataSource(sourceId: string, active: boolean) {
|
|||
|
||||
return await toggleDataSourceController.execute({
|
||||
caller: 'user',
|
||||
userId: user._id,
|
||||
userId: user.id,
|
||||
sourceId,
|
||||
active,
|
||||
});
|
||||
|
|
@ -122,7 +122,7 @@ export async function addDocsToDataSource({
|
|||
|
||||
return await addDocsToDataSourceController.execute({
|
||||
caller: 'user',
|
||||
userId: user._id,
|
||||
userId: user.id,
|
||||
sourceId,
|
||||
docs: docData,
|
||||
});
|
||||
|
|
@ -144,7 +144,7 @@ export async function listDocsInDataSource({
|
|||
|
||||
const docs = await listDocsInDataSourceController.execute({
|
||||
caller: 'user',
|
||||
userId: user._id,
|
||||
userId: user.id,
|
||||
sourceId,
|
||||
});
|
||||
|
||||
|
|
@ -162,7 +162,7 @@ export async function deleteDocFromDataSource({
|
|||
const user = await authCheck();
|
||||
return await deleteDocFromDataSourceController.execute({
|
||||
caller: 'user',
|
||||
userId: user._id,
|
||||
userId: user.id,
|
||||
docId,
|
||||
});
|
||||
}
|
||||
|
|
@ -174,7 +174,7 @@ export async function getDownloadUrlForFile(
|
|||
|
||||
return await getDownloadUrlForFileController.execute({
|
||||
caller: 'user',
|
||||
userId: user._id,
|
||||
userId: user.id,
|
||||
fileId,
|
||||
});
|
||||
}
|
||||
|
|
@ -191,7 +191,7 @@ export async function getUploadUrlsForFilesDataSource(
|
|||
|
||||
return await getUploadUrlsForFilesController.execute({
|
||||
caller: 'user',
|
||||
userId: user._id,
|
||||
userId: user.id,
|
||||
sourceId,
|
||||
files,
|
||||
});
|
||||
|
|
@ -208,7 +208,7 @@ export async function updateDataSource({
|
|||
|
||||
return await updateDataSourceController.execute({
|
||||
caller: 'user',
|
||||
userId: user._id,
|
||||
userId: user.id,
|
||||
sourceId,
|
||||
data: {
|
||||
description,
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ export async function listJobs(request: {
|
|||
|
||||
return await listJobsController.execute({
|
||||
caller: 'user',
|
||||
userId: user._id,
|
||||
userId: user.id,
|
||||
projectId: request.projectId,
|
||||
filters: request.filters,
|
||||
cursor: request.cursor,
|
||||
|
|
@ -35,7 +35,7 @@ export async function fetchJob(request: {
|
|||
|
||||
return await fetchJobController.execute({
|
||||
caller: 'user',
|
||||
userId: user._id,
|
||||
userId: user.id,
|
||||
jobId: request.jobId,
|
||||
});
|
||||
}
|
||||
|
|
@ -22,7 +22,7 @@ export async function createConversation({
|
|||
const controller = container.resolve<ICreatePlaygroundConversationController>("createPlaygroundConversationController");
|
||||
|
||||
return await controller.execute({
|
||||
userId: user._id,
|
||||
userId: user.id,
|
||||
projectId,
|
||||
workflow,
|
||||
isLiveWorkflow,
|
||||
|
|
@ -41,7 +41,7 @@ export async function createCachedTurn({
|
|||
|
||||
const { key } = await createCachedTurnController.execute({
|
||||
caller: "user",
|
||||
userId: user._id,
|
||||
userId: user.id,
|
||||
conversationId,
|
||||
input: {
|
||||
messages,
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ export async function projectAuthCheck(projectId: string) {
|
|||
const user = await authCheck();
|
||||
await projectActionAuthorizationPolicy.authorize({
|
||||
caller: 'user',
|
||||
userId: user._id,
|
||||
userId: user.id,
|
||||
projectId,
|
||||
});
|
||||
}
|
||||
|
|
@ -69,7 +69,7 @@ export async function createProject(formData: FormData): Promise<{ id: string }
|
|||
|
||||
try {
|
||||
const project = await createProjectController.execute({
|
||||
userId: user._id,
|
||||
userId: user.id,
|
||||
data: {
|
||||
name: name || '',
|
||||
mode: {
|
||||
|
|
@ -94,7 +94,7 @@ export async function createProjectFromWorkflowJson(formData: FormData): Promise
|
|||
|
||||
try {
|
||||
const project = await createProjectController.execute({
|
||||
userId: user._id,
|
||||
userId: user.id,
|
||||
data: {
|
||||
name: name || '',
|
||||
mode: {
|
||||
|
|
@ -116,7 +116,7 @@ export async function fetchProject(projectId: string): Promise<z.infer<typeof Pr
|
|||
const user = await authCheck();
|
||||
const project = await fetchProjectController.execute({
|
||||
caller: 'user',
|
||||
userId: user._id,
|
||||
userId: user.id,
|
||||
projectId,
|
||||
});
|
||||
|
||||
|
|
@ -134,7 +134,7 @@ export async function listProjects(): Promise<z.infer<typeof Project>[]> {
|
|||
let cursor = undefined;
|
||||
do {
|
||||
const result = await listProjectsController.execute({
|
||||
userId: user._id,
|
||||
userId: user.id,
|
||||
cursor,
|
||||
});
|
||||
projects.push(...result.items);
|
||||
|
|
@ -148,7 +148,7 @@ export async function rotateSecret(projectId: string): Promise<string> {
|
|||
const user = await authCheck();
|
||||
return await rotateSecretController.execute({
|
||||
caller: 'user',
|
||||
userId: user._id,
|
||||
userId: user.id,
|
||||
projectId,
|
||||
});
|
||||
}
|
||||
|
|
@ -157,7 +157,7 @@ export async function updateWebhookUrl(projectId: string, url: string) {
|
|||
const user = await authCheck();
|
||||
await updateWebhookUrlController.execute({
|
||||
caller: 'user',
|
||||
userId: user._id,
|
||||
userId: user.id,
|
||||
projectId,
|
||||
url,
|
||||
});
|
||||
|
|
@ -167,7 +167,7 @@ export async function createApiKey(projectId: string): Promise<z.infer<typeof Ap
|
|||
const user = await authCheck();
|
||||
return await createApiKeyController.execute({
|
||||
caller: 'user',
|
||||
userId: user._id,
|
||||
userId: user.id,
|
||||
projectId,
|
||||
});
|
||||
}
|
||||
|
|
@ -176,7 +176,7 @@ export async function deleteApiKey(projectId: string, id: string) {
|
|||
const user = await authCheck();
|
||||
return await deleteApiKeyController.execute({
|
||||
caller: 'user',
|
||||
userId: user._id,
|
||||
userId: user.id,
|
||||
projectId,
|
||||
id,
|
||||
});
|
||||
|
|
@ -186,7 +186,7 @@ export async function listApiKeys(projectId: string): Promise<z.infer<typeof Api
|
|||
const user = await authCheck();
|
||||
return await listApiKeysController.execute({
|
||||
caller: 'user',
|
||||
userId: user._id,
|
||||
userId: user.id,
|
||||
projectId,
|
||||
});
|
||||
}
|
||||
|
|
@ -195,7 +195,7 @@ export async function updateProjectName(projectId: string, name: string) {
|
|||
const user = await authCheck();
|
||||
await updateProjectNameController.execute({
|
||||
caller: 'user',
|
||||
userId: user._id,
|
||||
userId: user.id,
|
||||
projectId,
|
||||
name,
|
||||
});
|
||||
|
|
@ -205,7 +205,7 @@ export async function deleteProject(projectId: string) {
|
|||
const user = await authCheck();
|
||||
await deleteProjectController.execute({
|
||||
caller: 'user',
|
||||
userId: user._id,
|
||||
userId: user.id,
|
||||
projectId,
|
||||
});
|
||||
|
||||
|
|
@ -216,7 +216,7 @@ export async function saveWorkflow(projectId: string, workflow: z.infer<typeof W
|
|||
const user = await authCheck();
|
||||
await updateDraftWorkflowController.execute({
|
||||
caller: 'user',
|
||||
userId: user._id,
|
||||
userId: user.id,
|
||||
projectId,
|
||||
workflow,
|
||||
});
|
||||
|
|
@ -226,7 +226,7 @@ export async function publishWorkflow(projectId: string, workflow: z.infer<typeo
|
|||
const user = await authCheck();
|
||||
await updateLiveWorkflowController.execute({
|
||||
caller: 'user',
|
||||
userId: user._id,
|
||||
userId: user.id,
|
||||
projectId,
|
||||
workflow,
|
||||
});
|
||||
|
|
@ -236,7 +236,7 @@ export async function revertToLiveWorkflow(projectId: string) {
|
|||
const user = await authCheck();
|
||||
await revertToLiveWorkflowController.execute({
|
||||
caller: 'user',
|
||||
userId: user._id,
|
||||
userId: user.id,
|
||||
projectId,
|
||||
});
|
||||
}
|
||||
|
|
@ -27,7 +27,7 @@ export async function createRecurringJobRule(request: {
|
|||
|
||||
return await createRecurringJobRuleController.execute({
|
||||
caller: 'user',
|
||||
userId: user._id,
|
||||
userId: user.id,
|
||||
projectId: request.projectId,
|
||||
input: request.input,
|
||||
cron: request.cron,
|
||||
|
|
@ -43,7 +43,7 @@ export async function listRecurringJobRules(request: {
|
|||
|
||||
return await listRecurringJobRulesController.execute({
|
||||
caller: 'user',
|
||||
userId: user._id,
|
||||
userId: user.id,
|
||||
projectId: request.projectId,
|
||||
cursor: request.cursor,
|
||||
limit: request.limit,
|
||||
|
|
@ -57,7 +57,7 @@ export async function fetchRecurringJobRule(request: {
|
|||
|
||||
return await fetchRecurringJobRuleController.execute({
|
||||
caller: 'user',
|
||||
userId: user._id,
|
||||
userId: user.id,
|
||||
ruleId: request.ruleId,
|
||||
});
|
||||
}
|
||||
|
|
@ -70,7 +70,7 @@ export async function toggleRecurringJobRule(request: {
|
|||
|
||||
return await toggleRecurringJobRuleController.execute({
|
||||
caller: 'user',
|
||||
userId: user._id,
|
||||
userId: user.id,
|
||||
ruleId: request.ruleId,
|
||||
disabled: request.disabled,
|
||||
});
|
||||
|
|
@ -84,7 +84,7 @@ export async function deleteRecurringJobRule(request: {
|
|||
|
||||
return await deleteRecurringJobRuleController.execute({
|
||||
caller: 'user',
|
||||
userId: user._id,
|
||||
userId: user.id,
|
||||
projectId: request.projectId,
|
||||
ruleId: request.ruleId,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ export async function createScheduledJobRule(request: {
|
|||
|
||||
return await createScheduledJobRuleController.execute({
|
||||
caller: 'user',
|
||||
userId: user._id,
|
||||
userId: user.id,
|
||||
projectId: request.projectId,
|
||||
input: request.input,
|
||||
scheduledTime: request.scheduledTime,
|
||||
|
|
@ -41,7 +41,7 @@ export async function listScheduledJobRules(request: {
|
|||
|
||||
return await listScheduledJobRulesController.execute({
|
||||
caller: 'user',
|
||||
userId: user._id,
|
||||
userId: user.id,
|
||||
projectId: request.projectId,
|
||||
cursor: request.cursor,
|
||||
limit: request.limit,
|
||||
|
|
@ -55,7 +55,7 @@ export async function fetchScheduledJobRule(request: {
|
|||
|
||||
return await fetchScheduledJobRuleController.execute({
|
||||
caller: 'user',
|
||||
userId: user._id,
|
||||
userId: user.id,
|
||||
ruleId: request.ruleId,
|
||||
});
|
||||
}
|
||||
|
|
@ -68,7 +68,7 @@ export async function deleteScheduledJobRule(request: {
|
|||
|
||||
return await deleteScheduledJobRuleController.execute({
|
||||
caller: 'user',
|
||||
userId: user._id,
|
||||
userId: user.id,
|
||||
projectId: request.projectId,
|
||||
ruleId: request.ruleId,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ export async function GET(request: Request, props: { params: Promise<{ streamId:
|
|||
}
|
||||
} catch (error) {
|
||||
console.error('Error processing copilot stream:', error);
|
||||
controller.error(error);
|
||||
controller.error(new Error("Something went wrong. Please try again."));
|
||||
} finally {
|
||||
// log copilot usage
|
||||
if (USE_BILLING && billingCustomerId) {
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export async function GET(request: Request, props: { params: Promise<{ streamId:
|
|||
// Iterate over the generator
|
||||
for await (const event of runCachedTurnController.execute({
|
||||
caller: "user",
|
||||
userId: user._id,
|
||||
userId: user.id,
|
||||
cachedTurnKey: params.streamId,
|
||||
})) {
|
||||
controller.enqueue(encoder.encode(`event: message\ndata: ${JSON.stringify(event)}\n\n`));
|
||||
|
|
@ -31,7 +31,7 @@ export async function GET(request: Request, props: { params: Promise<{ streamId:
|
|||
console.error('Error processing stream:', error);
|
||||
const errMessage: z.infer<typeof TurnEvent> = {
|
||||
type: "error",
|
||||
error: `Error processing stream: ${error}`,
|
||||
error: "Something went wrong. Please try again.",
|
||||
isBillingError: false,
|
||||
};
|
||||
controller.enqueue(encoder.encode(`event: message\ndata: ${JSON.stringify(errMessage)}\n\n`));
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ export async function POST(
|
|||
controller.close();
|
||||
} catch (error) {
|
||||
logger.log(`Error processing stream: ${error}`);
|
||||
controller.error(error);
|
||||
controller.error(new Error("Something went wrong. Please try again."));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import { z } from "zod";
|
|||
import { tokens } from "@/app/styles/design-tokens";
|
||||
import { SectionHeading } from "@/components/ui/section-heading";
|
||||
import { HorizontalDivider } from "@/components/ui/horizontal-divider";
|
||||
import { WithStringId } from "@/app/lib/types/types";
|
||||
import clsx from 'clsx';
|
||||
import { getCustomerPortalUrl } from "../actions/billing.actions";
|
||||
import { useState } from "react";
|
||||
|
|
@ -31,7 +30,7 @@ const planDetails = {
|
|||
};
|
||||
|
||||
interface BillingPageProps {
|
||||
customer: WithStringId<z.infer<typeof Customer>>;
|
||||
customer: z.infer<typeof Customer>;
|
||||
usage: z.infer<typeof UsageResponse>;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export default async function Page(
|
|||
) {
|
||||
const searchParams = await props.searchParams;
|
||||
const customer = await requireBillingCustomer();
|
||||
await syncWithStripe(customer._id);
|
||||
await syncWithStripe(customer.id);
|
||||
const redirectUrl = searchParams.redirect as string;
|
||||
redirect(redirectUrl || '/projects');
|
||||
}
|
||||
|
|
@ -12,6 +12,6 @@ export default async function Page() {
|
|||
}
|
||||
|
||||
const customer = await requireBillingCustomer();
|
||||
const usage = await getUsage(customer._id);
|
||||
const usage = await getUsage(customer.id);
|
||||
return <BillingPage customer={customer} usage={usage} />;
|
||||
}
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
import { z } from "zod";
|
||||
import { ObjectId } from "mongodb";
|
||||
import { usersCollection } from "./mongodb";
|
||||
import { auth0 } from "./auth0";
|
||||
import { User, WithStringId } from "./types/types";
|
||||
import { User } from "@/src/entities/models/user";
|
||||
import { USE_AUTH } from "./feature_flags";
|
||||
import { redirect } from "next/navigation";
|
||||
import { container } from "@/di/container";
|
||||
import { IUsersRepository } from "@/src/application/repositories/users.repository.interface";
|
||||
|
||||
export const GUEST_SESSION = {
|
||||
email: "guest@rowboatlabs.com",
|
||||
|
|
@ -12,13 +12,12 @@ export const GUEST_SESSION = {
|
|||
sub: "guest_user",
|
||||
}
|
||||
|
||||
export const GUEST_DB_USER: WithStringId<z.infer<typeof User>> = {
|
||||
_id: "guest_user",
|
||||
export const GUEST_DB_USER: z.infer<typeof User> = {
|
||||
id: "guest_user",
|
||||
auth0Id: "guest_user",
|
||||
name: "Guest",
|
||||
email: "guest@rowboatlabs.com",
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -33,7 +32,7 @@ export const GUEST_DB_USER: WithStringId<z.infer<typeof User>> = {
|
|||
* const user = await requireAuth();
|
||||
* ```
|
||||
*/
|
||||
export async function requireAuth(): Promise<WithStringId<z.infer<typeof User>>> {
|
||||
export async function requireAuth(): Promise<z.infer<typeof User>> {
|
||||
if (!USE_AUTH) {
|
||||
return GUEST_DB_USER;
|
||||
}
|
||||
|
|
@ -44,48 +43,26 @@ export async function requireAuth(): Promise<WithStringId<z.infer<typeof User>>>
|
|||
}
|
||||
|
||||
// fetch db user
|
||||
const usersRepository = container.resolve<IUsersRepository>("usersRepository");
|
||||
let dbUser = await getUserFromSessionId(user.sub);
|
||||
|
||||
// if db user does not exist, create one
|
||||
if (!dbUser) {
|
||||
// create user record
|
||||
const doc = {
|
||||
_id: new ObjectId(),
|
||||
dbUser = await usersRepository.create({
|
||||
auth0Id: user.sub,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
email: user.email,
|
||||
};
|
||||
console.log(`creating new user id ${doc._id.toString()} for session id ${user.sub}`);
|
||||
await usersCollection.insertOne(doc);
|
||||
|
||||
dbUser = {
|
||||
...doc,
|
||||
_id: doc._id.toString(),
|
||||
};
|
||||
});
|
||||
console.log(`created new user id ${dbUser.id} for session id ${user.sub}`);
|
||||
}
|
||||
|
||||
const { _id, ...rest } = dbUser;
|
||||
return {
|
||||
...rest,
|
||||
_id: _id.toString(),
|
||||
};
|
||||
return dbUser;
|
||||
}
|
||||
|
||||
export async function getUserFromSessionId(sessionUserId: string): Promise<WithStringId<z.infer<typeof User>> | null> {
|
||||
export async function getUserFromSessionId(sessionUserId: string): Promise<z.infer<typeof User> | null> {
|
||||
if (!USE_AUTH) {
|
||||
return GUEST_DB_USER;
|
||||
}
|
||||
|
||||
let dbUser = await usersCollection.findOne({
|
||||
auth0Id: sessionUserId
|
||||
});
|
||||
if (!dbUser) {
|
||||
return null;
|
||||
}
|
||||
const { _id, ...rest } = dbUser;
|
||||
return {
|
||||
...rest,
|
||||
_id: _id.toString(),
|
||||
};
|
||||
const usersRepository = container.resolve<IUsersRepository>("usersRepository");
|
||||
return await usersRepository.fetchByAuth0Id(sessionUserId);
|
||||
}
|
||||
|
|
@ -1,13 +1,11 @@
|
|||
import { WithStringId } from './types/types';
|
||||
import { z } from 'zod';
|
||||
import { Customer, AuthorizeRequest, AuthorizeResponse, LogUsageRequest, UsageResponse, CustomerPortalSessionResponse, PricesResponse, UpdateSubscriptionPlanRequest, UpdateSubscriptionPlanResponse, ModelsResponse, UsageItem } from './types/billing_types';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import { usersCollection } from './mongodb';
|
||||
import { redirect } from 'next/navigation';
|
||||
import { getUserFromSessionId, requireAuth } from './auth';
|
||||
import { USE_BILLING } from './feature_flags';
|
||||
import { container } from '@/di/container';
|
||||
import { IProjectsRepository } from '@/src/application/repositories/projects.repository.interface';
|
||||
import { IUsersRepository } from '@/src/application/repositories/users.repository.interface';
|
||||
|
||||
const BILLING_API_URL = process.env.BILLING_API_URL || 'http://billing';
|
||||
const BILLING_API_KEY = process.env.BILLING_API_KEY || 'test';
|
||||
|
|
@ -15,7 +13,7 @@ const BILLING_API_KEY = process.env.BILLING_API_KEY || 'test';
|
|||
let logCounter = 1;
|
||||
|
||||
const GUEST_BILLING_CUSTOMER = {
|
||||
_id: "guest-user",
|
||||
id: "guest-user",
|
||||
userId: "guest-user",
|
||||
name: "Guest",
|
||||
email: "guest@rowboatlabs.com",
|
||||
|
|
@ -24,9 +22,9 @@ const GUEST_BILLING_CUSTOMER = {
|
|||
subscriptionPlan: "free" as const,
|
||||
subscriptionStatus: "active" as const,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
|
||||
export class UsageTracker{
|
||||
private items: z.infer<typeof UsageItem>[] = [];
|
||||
|
||||
|
|
@ -41,8 +39,10 @@ export class UsageTracker{
|
|||
}
|
||||
}
|
||||
|
||||
export async function getCustomerForUserId(userId: string): Promise<WithStringId<z.infer<typeof Customer>> | null> {
|
||||
const user = await usersCollection.findOne({ _id: new ObjectId(userId) });
|
||||
export async function getCustomerForUserId(userId: string): Promise<z.infer<typeof Customer> | null> {
|
||||
const usersRepository = container.resolve<IUsersRepository>("usersRepository");
|
||||
|
||||
const user = await usersRepository.fetch(userId);
|
||||
if (!user) {
|
||||
throw new Error("User not found");
|
||||
}
|
||||
|
|
@ -62,10 +62,10 @@ export async function getCustomerIdForProject(projectId: string): Promise<string
|
|||
if (!customer) {
|
||||
throw new Error("User has no billing customer id");
|
||||
}
|
||||
return customer._id;
|
||||
return customer.id;
|
||||
}
|
||||
|
||||
export async function getBillingCustomer(id: string): Promise<WithStringId<z.infer<typeof Customer>> | null> {
|
||||
export async function getBillingCustomer(id: string): Promise<z.infer<typeof Customer> | null> {
|
||||
const response = await fetch(`${BILLING_API_URL}/api/customers/${id}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
|
|
@ -84,7 +84,7 @@ export async function getBillingCustomer(id: string): Promise<WithStringId<z.inf
|
|||
return parseResult.data;
|
||||
}
|
||||
|
||||
async function createBillingCustomer(userId: string, email: string): Promise<WithStringId<z.infer<typeof Customer>>> {
|
||||
async function createBillingCustomer(userId: string, email: string): Promise<z.infer<typeof Customer>> {
|
||||
const response = await fetch(`${BILLING_API_URL}/api/customers`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
|
|
@ -264,13 +264,14 @@ export async function getEligibleModels(customerId: string): Promise<z.infer<typ
|
|||
* const billingCustomer = await requireBillingCustomer();
|
||||
* ```
|
||||
*/
|
||||
export async function requireBillingCustomer(): Promise<WithStringId<z.infer<typeof Customer>>> {
|
||||
export async function requireBillingCustomer(): Promise<z.infer<typeof Customer>> {
|
||||
const user = await requireAuth();
|
||||
const usersRepository = container.resolve<IUsersRepository>("usersRepository");
|
||||
|
||||
if (!USE_BILLING) {
|
||||
return {
|
||||
...GUEST_BILLING_CUSTOMER,
|
||||
userId: user._id,
|
||||
userId: user.id,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -280,22 +281,15 @@ export async function requireBillingCustomer(): Promise<WithStringId<z.infer<typ
|
|||
}
|
||||
|
||||
// fetch or create customer
|
||||
let customer: WithStringId<z.infer<typeof Customer>> | null;
|
||||
let customer: z.infer<typeof Customer> | null;
|
||||
if (user.billingCustomerId) {
|
||||
customer = await getBillingCustomer(user.billingCustomerId);
|
||||
} else {
|
||||
customer = await createBillingCustomer(user._id, user.email);
|
||||
console.log("created billing customer", JSON.stringify({ userId: user._id, customer }));
|
||||
customer = await createBillingCustomer(user.id, user.email);
|
||||
console.log("created billing customer", JSON.stringify({ userId: user.id, customer }));
|
||||
|
||||
// update customer id in db
|
||||
await usersCollection.updateOne({
|
||||
_id: new ObjectId(user._id),
|
||||
}, {
|
||||
$set: {
|
||||
billingCustomerId: customer._id,
|
||||
updatedAt: new Date().toISOString(),
|
||||
}
|
||||
});
|
||||
await usersRepository.updateBillingCustomerId(user.id, customer.id);
|
||||
}
|
||||
if (!customer) {
|
||||
throw new Error("Failed to fetch or create billing customer");
|
||||
|
|
@ -316,12 +310,11 @@ export async function requireBillingCustomer(): Promise<WithStringId<z.infer<typ
|
|||
* const billingCustomer = await requireActiveBillingSubscription();
|
||||
* ```
|
||||
*/
|
||||
export async function requireActiveBillingSubscription(): Promise<WithStringId<z.infer<typeof Customer>>> {
|
||||
export async function requireActiveBillingSubscription(): Promise<z.infer<typeof Customer>> {
|
||||
const billingCustomer = await requireBillingCustomer();
|
||||
|
||||
if (USE_BILLING && billingCustomer.subscriptionStatus !== "active" && billingCustomer.subscriptionStatus !== "past_due") {
|
||||
redirect('/billing');
|
||||
}
|
||||
return billingCustomer;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,9 +1,11 @@
|
|||
import { WorkflowTool, WorkflowAgent, WorkflowPrompt, WorkflowPipeline } from "./types/workflow_types";
|
||||
import { z } from "zod";
|
||||
|
||||
const ZFallbackSchema = z.object({}).passthrough();
|
||||
|
||||
export function validateConfigChanges(configType: string, configChanges: Record<string, unknown>, name: string) {
|
||||
let testObject: any;
|
||||
let schema: z.ZodType<any>;
|
||||
let schema: z.ZodType<any> = ZFallbackSchema;
|
||||
|
||||
switch (configType) {
|
||||
case 'tool': {
|
||||
|
|
@ -56,6 +58,10 @@ export function validateConfigChanges(configType: string, configChanges: Record<
|
|||
schema = WorkflowPipeline;
|
||||
break;
|
||||
}
|
||||
case 'start_agent': {
|
||||
testObject = {};
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return { error: `Unknown config type: ${configType}` };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,11 +55,15 @@ export function createAtMentions({ agents, prompts, tools, pipelines = [], curre
|
|||
|
||||
// Add prompts (always allowed)
|
||||
for (const prompt of prompts) {
|
||||
const id = `prompt:${prompt.name}`;
|
||||
// Use 'variable' for base_prompt types, 'prompt' for others
|
||||
const isVariable = prompt.type === 'base_prompt';
|
||||
const type = isVariable ? 'variable' : 'prompt';
|
||||
const label = isVariable ? 'Variable' : 'Prompt';
|
||||
const id = `${type}:${prompt.name}`;
|
||||
atMentions.push({
|
||||
id,
|
||||
value: id,
|
||||
label: `Prompt: ${prompt.name}`,
|
||||
label: `${label}: ${prompt.name}`,
|
||||
denotationChar: "@",
|
||||
link: id,
|
||||
target: "_self"
|
||||
|
|
|
|||
|
|
@ -15,6 +15,6 @@ export const USE_VOICE_FEATURE = false;
|
|||
export const USE_TRANSFER_CONTROL_OPTIONS = false;
|
||||
export const USE_PRODUCT_TOUR = false;
|
||||
export const SHOW_COPILOT_MARQUEE = false;
|
||||
export const SHOW_PROMPTS_SECTION = false;
|
||||
export const SHOW_PROMPTS_SECTION = true;
|
||||
export const SHOW_DARK_MODE_TOGGLE = false;
|
||||
export const SHOW_VISUALIZATION = false
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
import { MongoClient } from "mongodb";
|
||||
import { User } from "./types/types";
|
||||
import { TwilioConfig, TwilioInboundCall } from "./types/voice_types";
|
||||
import { z } from 'zod';
|
||||
import { apiV1 } from "rowboat-shared";
|
||||
|
|
@ -10,7 +9,6 @@ export const db = client.db("rowboat");
|
|||
export const chatsCollection = db.collection<z.infer<typeof apiV1.Chat>>("chats");
|
||||
export const chatMessagesCollection = db.collection<z.infer<typeof apiV1.ChatMessage>>("chat_messages");
|
||||
export const twilioConfigsCollection = db.collection<z.infer<typeof TwilioConfig>>("twilio_configs");
|
||||
export const usersCollection = db.collection<z.infer<typeof User>>("users");
|
||||
export const twilioInboundCallsCollection = db.collection<z.infer<typeof TwilioInboundCall>>("twilio_inbound_calls");
|
||||
|
||||
// Create indexes
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,3 +1,17 @@
|
|||
/**
|
||||
* 🚨 ATTENTION: DO NOT MODIFY THIS FILE! 🚨
|
||||
*
|
||||
* This file contains billing types that are manually copied
|
||||
* from the billing service repository. Any manual changes will be
|
||||
* overwritten during the next sync.
|
||||
*
|
||||
* If you need to modify billing types:
|
||||
* 1. Make changes in the billing service repo
|
||||
* 2. Copy the updated file from there
|
||||
* 3. Never edit this file directly
|
||||
*
|
||||
* This file is a manual copy - keep it in sync with the source!
|
||||
*/
|
||||
import { z } from "zod";
|
||||
|
||||
export const SubscriptionPlan = z.enum(["free", "starter", "pro"]);
|
||||
|
|
@ -57,7 +71,7 @@ export const LogUsageRequest = z.object({
|
|||
export const CustomerUsageData = z.record(z.string(), z.number());
|
||||
|
||||
export const Customer = z.object({
|
||||
_id: z.string(),
|
||||
id: z.string(),
|
||||
userId: z.string(),
|
||||
email: z.string(),
|
||||
stripeCustomerId: z.string(),
|
||||
|
|
@ -65,7 +79,7 @@ export const Customer = z.object({
|
|||
subscriptionPlan: SubscriptionPlan.optional(),
|
||||
subscriptionStatus: z.enum([ 'active', 'past_due' ]).optional(),
|
||||
createdAt: z.string().datetime(),
|
||||
updatedAt: z.string().datetime(),
|
||||
updatedAt: z.string().datetime().optional(),
|
||||
subscriptionPlanUpdatedAt: z.string().datetime().optional(),
|
||||
usage: CustomerUsageData.optional(),
|
||||
usageUpdatedAt: z.string().datetime().optional(),
|
||||
|
|
|
|||
|
|
@ -124,15 +124,6 @@ export const McpServerResponse = z.object({
|
|||
error: z.string().nullable(),
|
||||
});
|
||||
|
||||
export const User = z.object({
|
||||
auth0Id: z.string(),
|
||||
billingCustomerId: z.string().optional(),
|
||||
name: z.string().optional(),
|
||||
email: z.string().optional(),
|
||||
createdAt: z.string().datetime(),
|
||||
updatedAt: z.string().datetime(),
|
||||
});
|
||||
|
||||
export const Webpage = z.object({
|
||||
_id: z.string(),
|
||||
title: z.string(),
|
||||
|
|
|
|||
|
|
@ -129,8 +129,8 @@ export function sanitizeTextWithMentions(
|
|||
sanitized: string;
|
||||
entities: z.infer<typeof ConnectedEntity>[];
|
||||
} {
|
||||
// Regex to match [@type:name](#type:something) pattern where type is tool/prompt/agent/pipeline
|
||||
const mentionRegex = /\[@(tool|prompt|agent|pipeline):([^\]]+)\]\(#mention\)/g;
|
||||
// Regex to match [@type:name](#type:something) pattern where type is tool/prompt/agent/pipeline/variable
|
||||
const mentionRegex = /\[@(tool|prompt|agent|pipeline|variable):([^\]]+)\]\(#mention\)/g;
|
||||
const seen = new Set<string>();
|
||||
|
||||
// collect entities
|
||||
|
|
@ -144,8 +144,10 @@ export function sanitizeTextWithMentions(
|
|||
return true;
|
||||
})
|
||||
.map(match => {
|
||||
// Treat @variable: as @prompt: internally
|
||||
const type = match[1] === 'variable' ? 'prompt' : match[1];
|
||||
return {
|
||||
type: match[1] as 'tool' | 'prompt' | 'agent' | 'pipeline',
|
||||
type: type as 'tool' | 'prompt' | 'agent' | 'pipeline',
|
||||
name: match[2],
|
||||
};
|
||||
})
|
||||
|
|
@ -176,6 +178,12 @@ export function sanitizeTextWithMentions(
|
|||
const id = `${entity.type}:${entity.name}`;
|
||||
const textToReplace = `[@${id}](#mention)`;
|
||||
text = text.replace(textToReplace, `[@${id}]`);
|
||||
|
||||
// Also handle @variable: mentions for prompts
|
||||
if (entity.type === 'prompt') {
|
||||
const variableTextToReplace = `[@variable:${entity.name}](#mention)`;
|
||||
text = text.replace(variableTextToReplace, `[@variable:${entity.name}]`);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,9 @@ import { Messages } from "./components/messages";
|
|||
import { CopyIcon, CheckIcon, PlusIcon, XIcon, InfoIcon, Sparkles } from "lucide-react";
|
||||
import { useCopilot } from "./use-copilot";
|
||||
import { BillingUpgradeModal } from "@/components/common/billing-upgrade-modal";
|
||||
import { SHOW_COPILOT_MARQUEE } from "@/app/lib/feature_flags";
|
||||
import Image from "next/image";
|
||||
import mascot from "@/public/mascot.png";
|
||||
|
||||
const CopilotContext = createContext<{
|
||||
workflow: z.infer<typeof Workflow> | null;
|
||||
|
|
@ -205,6 +208,56 @@ const App = forwardRef<{ handleCopyChat: () => void; handleUserMessage: (message
|
|||
<CopilotContext.Provider value={{ workflow: workflowRef.current, dispatch }}>
|
||||
<div className="h-full flex flex-col">
|
||||
<div className="flex-1 overflow-auto">
|
||||
{messages.length === 0 && (
|
||||
<div className="flex flex-col items-center justify-center py-4 pointer-events-none">
|
||||
{/* Replace Sparkles icon with mascot image */}
|
||||
<Image src={mascot} alt="Rowboat Mascot" width={160} height={160} className="object-contain mb-3 animate-float" />
|
||||
|
||||
{/* Welcome/Intro Section */}
|
||||
<div className="text-center max-w-md px-6 mb-3">
|
||||
<h3 className="text-xl font-semibold text-zinc-700 dark:text-zinc-300 mb-2 text-center">
|
||||
👋 Hi there!
|
||||
</h3>
|
||||
<p className="text-base text-zinc-600 dark:text-zinc-400 mb-4 text-center">
|
||||
I'm your copilot for building agents and adding tools to them.
|
||||
</p>
|
||||
<p className="text-base text-zinc-600 dark:text-zinc-400 mb-3 text-center">
|
||||
Here's what you can do in Rowboat:
|
||||
</p>
|
||||
<div className="space-y-2 max-w-2xl mx-auto text-left">
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="text-lg">⚡</span>
|
||||
<span className="text-sm text-zinc-600 dark:text-zinc-400">Build AI agents instantly with natural language.</span>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="text-lg">🔌</span>
|
||||
<span className="text-sm text-zinc-600 dark:text-zinc-400">Connect tools with one-click integrations.</span>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="text-lg">📂</span>
|
||||
<span className="text-sm text-zinc-600 dark:text-zinc-400">Power with knowledge by adding documents for RAG.</span>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="text-lg">🔄</span>
|
||||
<span className="text-sm text-zinc-600 dark:text-zinc-400">Automate workflows by setting up triggers and actions.</span>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="text-lg">🚀</span>
|
||||
<span className="text-sm text-zinc-600 dark:text-zinc-400">Deploy anywhere via API or SDK.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{SHOW_COPILOT_MARQUEE && (
|
||||
<div className="relative mt-2 max-w-full px-8">
|
||||
<div className="font-mono text-sm whitespace-nowrap text-blue-400/60 dark:text-blue-500/40 font-small inline-flex">
|
||||
<div className="overflow-hidden w-0 animate-typing">What can I help you build?</div>
|
||||
<div className="border-r-2 border-blue-400 dark:border-blue-500 animate-cursor"> </div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<Messages
|
||||
messages={messages}
|
||||
streamingResponse={streamingResponse}
|
||||
|
|
@ -320,7 +373,6 @@ export const Copilot = forwardRef<{ handleUserMessage: (message: string) => void
|
|||
<Panel
|
||||
variant="copilot"
|
||||
tourTarget="copilot"
|
||||
showWelcome={messages.length === 0}
|
||||
icon={<Sparkles className="w-5 h-5 text-indigo-600 dark:text-indigo-400" />}
|
||||
title="Skipper"
|
||||
subtitle="Build your assistant"
|
||||
|
|
|
|||
|
|
@ -51,9 +51,12 @@ export function Action({
|
|||
const appliedFields = Object.keys(action.config_changes).filter(key =>
|
||||
appliedChanges[getAppliedChangeKey(msgIndex, actionIndex, key)]
|
||||
);
|
||||
const allApplied = externallyApplied || Object.keys(action.config_changes).every(key =>
|
||||
let allApplied = externallyApplied || Object.keys(action.config_changes).every(key =>
|
||||
appliedFields.includes(key)
|
||||
);
|
||||
if (!externallyApplied && (action.action === "delete" || action.config_type === 'start_agent')) {
|
||||
allApplied = false;
|
||||
}
|
||||
|
||||
// Handle applying a single field change
|
||||
const handleFieldChange = (field: string) => {
|
||||
|
|
@ -160,7 +163,8 @@ export function Action({
|
|||
'transition-shadow duration-150',
|
||||
{
|
||||
'border-l-2 border-l-blue-500': !stale && !allApplied && action.action == 'create_new',
|
||||
'border-l-2 border-l-orange-500': !stale && !allApplied && action.action == 'edit',
|
||||
'border-l-2 border-l-yellow-500': !stale && !allApplied && action.action == 'edit',
|
||||
'border-l-2 border-l-red-500': !stale && !allApplied && action.action == 'delete',
|
||||
'border-l-2 border-l-gray-400': stale || allApplied || action.error,
|
||||
}
|
||||
)}>
|
||||
|
|
@ -171,14 +175,15 @@ export function Action({
|
|||
'inline-flex items-center justify-center rounded-full h-5 w-5 text-xs',
|
||||
{
|
||||
'bg-blue-100 text-blue-600': action.action == 'create_new',
|
||||
'bg-orange-100 text-orange-600': action.action == 'edit',
|
||||
'bg-yellow-100 text-yellow-600': action.action == 'edit',
|
||||
'bg-red-100 text-red-600': action.action == 'delete',
|
||||
'bg-gray-200 text-gray-600': stale || allApplied || action.error,
|
||||
}
|
||||
)}>
|
||||
{action.config_type === 'agent' ? '🧑💼' : action.config_type === 'tool' ? '🛠️' : action.config_type === 'pipeline' ? '⚙️' : '💬'}
|
||||
{action.config_type === 'agent' ? '🧑💼' : action.config_type === 'tool' ? '🛠️' : action.config_type === 'pipeline' ? '⚙️' : action.config_type === 'start_agent' ? '🏁' : action.config_type === 'prompt' ? '💬' : '💬'}
|
||||
</span>
|
||||
<span className="font-semibold text-sm text-zinc-800 dark:text-zinc-100 truncate flex-1">
|
||||
{action.action === 'create_new' ? 'Add' : 'Edit'} {action.config_type}: {action.name}
|
||||
{action.action === 'create_new' ? 'Add' : action.action === 'edit' ? 'Edit' : 'Delete'} {action.config_type}: {action.name}
|
||||
</span>
|
||||
{/* Action buttons - compact, icon only, show text on hover */}
|
||||
<div className="flex items-center gap-1">
|
||||
|
|
@ -195,13 +200,13 @@ export function Action({
|
|||
<CheckIcon size={13} className={allApplied ? 'text-zinc-400' : 'text-green-600 group-hover:text-green-700'} />
|
||||
<span>{allApplied ? 'Applied' : 'Apply'}</span>
|
||||
</button>
|
||||
<button
|
||||
{action.action !== 'delete' && <button
|
||||
className="flex items-center gap-1 rounded-full px-2 h-7 text-xs font-medium bg-transparent text-indigo-600 hover:text-indigo-700 transition-colors"
|
||||
onClick={handleViewDiff}
|
||||
>
|
||||
<EyeIcon size={13} className="text-indigo-600 group-hover:text-indigo-700" />
|
||||
<span>View Diff</span>
|
||||
</button>
|
||||
</button>}
|
||||
</div>
|
||||
</div>
|
||||
{/* Description of what happened */}
|
||||
|
|
@ -341,8 +346,8 @@ export function StreamingAction({
|
|||
loading,
|
||||
}: {
|
||||
action: {
|
||||
action?: 'create_new' | 'edit';
|
||||
config_type?: 'tool' | 'agent' | 'prompt' | 'pipeline';
|
||||
action?: 'create_new' | 'edit' | 'delete';
|
||||
config_type?: 'tool' | 'agent' | 'prompt' | 'pipeline' | 'start_agent';
|
||||
name?: string;
|
||||
};
|
||||
loading: boolean;
|
||||
|
|
@ -354,7 +359,8 @@ export function StreamingAction({
|
|||
'transition-shadow duration-150',
|
||||
{
|
||||
'border-l-2 border-l-blue-500': action.action == 'create_new',
|
||||
'border-l-2 border-l-orange-500': action.action == 'edit',
|
||||
'border-l-2 border-l-yellow-500': action.action == 'edit',
|
||||
'border-l-2 border-l-red-500': action.action == 'delete',
|
||||
'border-l-2 border-l-gray-400': !action.action,
|
||||
}
|
||||
)}>
|
||||
|
|
@ -364,14 +370,15 @@ export function StreamingAction({
|
|||
'inline-flex items-center justify-center rounded-full h-5 w-5 text-xs',
|
||||
{
|
||||
'bg-blue-100 text-blue-600': action.action == 'create_new',
|
||||
'bg-orange-100 text-orange-600': action.action == 'edit',
|
||||
'bg-yellow-100 text-yellow-600': action.action == 'edit',
|
||||
'bg-red-100 text-red-600': action.action == 'delete',
|
||||
'bg-gray-200 text-gray-600': !action.action,
|
||||
}
|
||||
)}>
|
||||
{action.config_type === 'agent' ? '🧑💼' : action.config_type === 'tool' ? '🛠️' : action.config_type === 'pipeline' ? '⚙️' : '💬'}
|
||||
{action.config_type === 'agent' ? '🧑💼' : action.config_type === 'tool' ? '🛠️' : action.config_type === 'pipeline' ? '⚙️' : action.config_type === 'start_agent' ? '🏁' : '💬'}
|
||||
</span>
|
||||
<span className="font-semibold text-sm text-zinc-800 dark:text-zinc-100 truncate flex-1">
|
||||
{action.action === 'create_new' ? 'Add' : 'Edit'} {action.config_type}: {action.name}
|
||||
{action.action === 'create_new' ? 'Add' : action.action === 'edit' ? 'Edit' : 'Delete'} {action.config_type}: {action.name}
|
||||
</span>
|
||||
</div>
|
||||
{/* Loading state body */}
|
||||
|
|
|
|||
|
|
@ -70,8 +70,8 @@ function enrich(response: string): z.infer<typeof CopilotResponsePart> {
|
|||
return {
|
||||
type: 'action',
|
||||
action: {
|
||||
action: metadata.action as 'create_new' | 'edit',
|
||||
config_type: metadata.config_type as 'tool' | 'agent' | 'prompt' | 'pipeline',
|
||||
action: metadata.action as 'create_new' | 'edit' | 'delete',
|
||||
config_type: metadata.config_type as 'tool' | 'agent' | 'prompt' | 'pipeline' | 'start_agent',
|
||||
name: metadata.name,
|
||||
change_description: jsonData.change_description || '',
|
||||
config_changes: {},
|
||||
|
|
@ -83,8 +83,8 @@ function enrich(response: string): z.infer<typeof CopilotResponsePart> {
|
|||
return {
|
||||
type: 'action',
|
||||
action: {
|
||||
action: metadata.action as 'create_new' | 'edit',
|
||||
config_type: metadata.config_type as 'tool' | 'agent' | 'prompt' | 'pipeline',
|
||||
action: metadata.action as 'create_new' | 'edit' | 'delete',
|
||||
config_type: metadata.config_type as 'tool' | 'agent' | 'prompt' | 'pipeline' | 'start_agent',
|
||||
name: metadata.name,
|
||||
change_description: jsonData.change_description || '',
|
||||
config_changes: result.changes
|
||||
|
|
@ -99,8 +99,8 @@ function enrich(response: string): z.infer<typeof CopilotResponsePart> {
|
|||
return {
|
||||
type: 'streaming_action',
|
||||
action: {
|
||||
action: (metadata.action as 'create_new' | 'edit') || undefined,
|
||||
config_type: (metadata.config_type as 'tool' | 'agent' | 'prompt' | 'pipeline') || undefined,
|
||||
action: (metadata.action as 'create_new' | 'edit' | 'delete') || undefined,
|
||||
config_type: (metadata.config_type as 'tool' | 'agent' | 'prompt' | 'pipeline' | 'start_agent') || undefined,
|
||||
name: metadata.name
|
||||
}
|
||||
};
|
||||
|
|
@ -289,6 +289,39 @@ function AssistantMessage({
|
|||
pipeline: action.config_changes
|
||||
});
|
||||
break;
|
||||
case 'start_agent':
|
||||
dispatch({
|
||||
type: 'set_main_agent',
|
||||
name: action.name,
|
||||
})
|
||||
break;
|
||||
}
|
||||
} else if (action.action === 'delete') {
|
||||
switch (action.config_type) {
|
||||
case 'agent':
|
||||
dispatch({
|
||||
type: 'delete_agent',
|
||||
name: action.name
|
||||
});
|
||||
break;
|
||||
case 'tool':
|
||||
dispatch({
|
||||
type: 'delete_tool',
|
||||
name: action.name
|
||||
});
|
||||
break;
|
||||
case 'prompt':
|
||||
dispatch({
|
||||
type: 'delete_prompt',
|
||||
name: action.name
|
||||
});
|
||||
break;
|
||||
case 'pipeline':
|
||||
dispatch({
|
||||
type: 'delete_pipeline',
|
||||
name: action.name
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, [dispatch, workflow.agents, workflow.tools]);
|
||||
|
|
@ -542,7 +575,7 @@ export function Messages({
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="h-full">
|
||||
<div className={displayMessages.length === 0 ? "" : "h-full"}>
|
||||
<div className="flex flex-col mb-4">
|
||||
{displayMessages.map((message, index) => (
|
||||
<div key={index} className="mb-4">
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@
|
|||
import { WorkflowPrompt, WorkflowAgent, Workflow, WorkflowTool } from "../../../lib/types/workflow_types";
|
||||
import { DataSource } from "@/src/entities/models/data-source";
|
||||
import { z } from "zod";
|
||||
import { PlusIcon, X as XIcon, ChevronDown, ChevronRight, Trash2, Maximize2, Minimize2, StarIcon, DatabaseIcon, UserIcon, Settings, Info } from "lucide-react";
|
||||
import { PlusIcon, X as XIcon, ChevronDown, ChevronRight, Trash2, Maximize2, Minimize2, StarIcon, DatabaseIcon, UserIcon, Settings, Info, Edit3 } from "lucide-react";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { usePreviewModal } from "../workflow/preview-modal";
|
||||
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Select, SelectItem, Chip, SelectSection } from "@heroui/react";
|
||||
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Select, SelectItem, Chip, SelectSection, Input } from "@heroui/react";
|
||||
import { PreviewModalProvider } from "../workflow/preview-modal";
|
||||
import { CopilotMessage } from "@/src/application/lib/copilot/types";
|
||||
import { getCopilotAgentInstructions } from "@/app/actions/copilot.actions";
|
||||
|
|
@ -17,7 +17,6 @@ import { Button as CustomButton } from "@/components/ui/button";
|
|||
import clsx from "clsx";
|
||||
import { InputField } from "@/app/lib/components/input-field";
|
||||
import { USE_TRANSFER_CONTROL_OPTIONS } from "@/app/lib/feature_flags";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Info as InfoIcon } from "lucide-react";
|
||||
import { useCopilot } from "../copilot/use-copilot";
|
||||
import { BillingUpgradeModal } from "@/components/common/billing-upgrade-modal";
|
||||
|
|
@ -78,6 +77,8 @@ export function AgentConfig({
|
|||
const [previousRagSources, setPreviousRagSources] = useState<string[]>([]);
|
||||
const [billingError, setBillingError] = useState<string | null>(null);
|
||||
const [showSavedBanner, setShowSavedBanner] = useState(false);
|
||||
const [isEditingName, setIsEditingName] = useState(false);
|
||||
const nameInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// Check if this agent is a pipeline agent
|
||||
const isPipelineAgent = agent.type === 'pipeline';
|
||||
|
|
@ -101,6 +102,14 @@ export function AgentConfig({
|
|||
setLocalName(agent.name);
|
||||
}, [agent.name]);
|
||||
|
||||
// Focus name input when entering edit mode
|
||||
useEffect(() => {
|
||||
if (isEditingName && nameInputRef.current) {
|
||||
nameInputRef.current.focus();
|
||||
nameInputRef.current.select();
|
||||
}
|
||||
}, [isEditingName]);
|
||||
|
||||
// Track changes in RAG datasources
|
||||
useEffect(() => {
|
||||
const currentSources = agent.ragDataSources || [];
|
||||
|
|
@ -149,6 +158,13 @@ export function AgentConfig({
|
|||
}
|
||||
}, [agent.controlType, agent.outputVisibility, agent, handleUpdate]);
|
||||
|
||||
// Add effect to ensure internal agents have maxCallsPerParentAgent set to 1 by default
|
||||
useEffect(() => {
|
||||
if (agent.outputVisibility === "internal" && !isPipelineAgent && agent.maxCallsPerParentAgent === undefined) {
|
||||
handleUpdate({ ...agent, maxCallsPerParentAgent: 1 });
|
||||
}
|
||||
}, [agent.outputVisibility, agent.maxCallsPerParentAgent, agent, handleUpdate, isPipelineAgent]);
|
||||
|
||||
// Add effect to handle escape key
|
||||
useEffect(() => {
|
||||
const handleEscape = (e: KeyboardEvent) => {
|
||||
|
|
@ -188,15 +204,36 @@ export function AgentConfig({
|
|||
return true;
|
||||
};
|
||||
|
||||
const handleNameChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newName = e.target.value;
|
||||
setLocalName(newName);
|
||||
|
||||
if (validateName(newName)) {
|
||||
setNameError(null);
|
||||
};
|
||||
|
||||
const handleNameCommit = () => {
|
||||
if (validateName(localName)) {
|
||||
handleUpdate({
|
||||
...agent,
|
||||
name: newName
|
||||
name: localName
|
||||
});
|
||||
showSavedMessage();
|
||||
setIsEditingName(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleNameCancel = () => {
|
||||
setLocalName(agent.name);
|
||||
setNameError(null);
|
||||
setIsEditingName(false);
|
||||
};
|
||||
|
||||
const handleNameKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
handleNameCommit();
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
handleNameCancel();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -209,20 +246,42 @@ export function AgentConfig({
|
|||
currentAgent: agent
|
||||
});
|
||||
|
||||
// Add local state for max calls input
|
||||
const [maxCallsInput, setMaxCallsInput] = useState(String(agent.maxCallsPerParentAgent || 3));
|
||||
const [maxCallsError, setMaxCallsError] = useState<string | null>(null);
|
||||
// Sync local state with agent prop
|
||||
useEffect(() => {
|
||||
setMaxCallsInput(String(agent.maxCallsPerParentAgent || 3));
|
||||
}, [agent.maxCallsPerParentAgent]);
|
||||
|
||||
|
||||
return (
|
||||
<Panel
|
||||
title={
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<div className="text-base font-semibold text-gray-900 dark:text-gray-100">
|
||||
{agent.name}
|
||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||
{isEditingName ? (
|
||||
<div className="flex flex-col min-w-0 flex-1">
|
||||
<Input
|
||||
ref={nameInputRef}
|
||||
type="text"
|
||||
value={localName}
|
||||
onChange={handleNameChange}
|
||||
onKeyDown={handleNameKeyDown}
|
||||
onBlur={handleNameCommit}
|
||||
isInvalid={!!nameError}
|
||||
errorMessage={nameError}
|
||||
variant="bordered"
|
||||
size="sm"
|
||||
classNames={{
|
||||
base: "max-w-xs",
|
||||
input: "text-base font-semibold px-2",
|
||||
inputWrapper: "min-h-[28px] h-[28px] border-gray-200 dark:border-gray-700 px-0"
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => setIsEditingName(true)}
|
||||
className="flex items-center gap-2 text-base font-semibold text-gray-900 dark:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-800 px-2 py-1 rounded-md transition-colors group"
|
||||
>
|
||||
<span className="truncate">{agent.name}</span>
|
||||
<Edit3 className="w-4 h-4 text-gray-400 group-hover:text-gray-600 dark:group-hover:text-gray-300 transition-colors" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<CustomButton
|
||||
variant="secondary"
|
||||
|
|
@ -260,7 +319,7 @@ export function AgentConfig({
|
|||
: "text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
|
||||
)}
|
||||
>
|
||||
{tab.charAt(0).toUpperCase() + tab.slice(1)}
|
||||
{tab === 'instructions' ? 'Instructions' : 'Model & RAG'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -480,38 +539,6 @@ export function AgentConfig({
|
|||
|
||||
{activeTab === 'configurations' && (
|
||||
<div className="flex flex-col gap-4 pb-4 pt-0">
|
||||
{/* Identity Section Card */}
|
||||
<SectionCard
|
||||
icon={<UserIcon className="w-5 h-5 text-indigo-500" />}
|
||||
title="Identity"
|
||||
labelWidth="md:w-32"
|
||||
className="mb-1"
|
||||
>
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex flex-col md:flex-row md:items-start gap-1 md:gap-0">
|
||||
<label className="text-sm font-semibold text-gray-600 dark:text-gray-300 md:w-32 mb-1 md:mb-0 md:pr-4">Name</label>
|
||||
<div className="flex-1">
|
||||
<InputField
|
||||
type="text"
|
||||
value={localName}
|
||||
onChange={(value) => {
|
||||
setLocalName(value);
|
||||
if (validateName(value)) {
|
||||
handleUpdate({
|
||||
...agent,
|
||||
name: value
|
||||
});
|
||||
}
|
||||
showSavedMessage();
|
||||
}}
|
||||
error={nameError}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</SectionCard>
|
||||
{/* Behavior Section Card */}
|
||||
<SectionCard
|
||||
icon={<Settings className="w-5 h-5 text-indigo-500" />}
|
||||
|
|
@ -618,43 +645,7 @@ export function AgentConfig({
|
|||
}
|
||||
</div>
|
||||
</div>
|
||||
{agent.outputVisibility === "internal" && !isPipelineAgent && (
|
||||
<div className="flex flex-col md:flex-row md:items-start gap-1 md:gap-0">
|
||||
<label className="text-sm font-semibold text-gray-600 dark:text-gray-300 md:w-32 mb-1 md:mb-0 md:pr-4">Max Calls From Parent</label>
|
||||
<div className="flex-1">
|
||||
<InputField
|
||||
type="number"
|
||||
value={maxCallsInput}
|
||||
onChange={(value: string) => {
|
||||
setMaxCallsInput(value);
|
||||
setMaxCallsError(null);
|
||||
const num = Number(value);
|
||||
if (value && !isNaN(num) && num >= 1 && Number.isInteger(num)) {
|
||||
if (num !== agent.maxCallsPerParentAgent) {
|
||||
handleUpdate({
|
||||
...agent,
|
||||
maxCallsPerParentAgent: num
|
||||
});
|
||||
}
|
||||
}
|
||||
}}
|
||||
validate={(value: string) => {
|
||||
const num = Number(value);
|
||||
if (!value || isNaN(num) || num < 1 || !Number.isInteger(num)) {
|
||||
return { valid: false, errorMessage: "Must be an integer >= 1" };
|
||||
}
|
||||
return { valid: true };
|
||||
}}
|
||||
error={maxCallsError}
|
||||
min={1}
|
||||
className="w-full max-w-24"
|
||||
/>
|
||||
{maxCallsError && (
|
||||
<p className="text-sm text-red-500 mt-1">{maxCallsError}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{USE_TRANSFER_CONTROL_OPTIONS && !isPipelineAgent && (
|
||||
<div className="flex flex-col md:flex-row md:items-start gap-1 md:gap-0">
|
||||
<label className="text-sm font-semibold text-gray-600 dark:text-gray-300 md:w-32 mb-1 md:mb-0 md:pr-4">After Turn</label>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,9 @@ const sectionHeaderStyles = "block text-xs font-medium uppercase tracking-wider
|
|||
// Enhanced textarea styles with improved states
|
||||
const textareaStyles = "rounded-lg p-3 border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-750 focus:shadow-inner focus:ring-2 focus:ring-indigo-500/20 dark:focus:ring-indigo-400/20 placeholder:text-gray-400 dark:placeholder:text-gray-500";
|
||||
|
||||
// Value field styles without grey placeholder text
|
||||
const valueTextareaStyles = "rounded-lg p-3 border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-750 focus:shadow-inner focus:ring-2 focus:ring-indigo-500/20 dark:focus:ring-indigo-400/20 placeholder:text-black dark:placeholder:text-white";
|
||||
|
||||
export function PromptConfig({
|
||||
prompt,
|
||||
agents,
|
||||
|
|
@ -128,7 +131,7 @@ export function PromptConfig({
|
|||
|
||||
<div className="space-y-4">
|
||||
<label className={sectionHeaderStyles}>
|
||||
Prompt
|
||||
Value
|
||||
</label>
|
||||
<Textarea
|
||||
value={prompt.prompt}
|
||||
|
|
@ -139,8 +142,8 @@ export function PromptConfig({
|
|||
});
|
||||
showSavedMessage();
|
||||
}}
|
||||
placeholder="Edit prompt here..."
|
||||
className={`${textareaStyles} min-h-[200px]`}
|
||||
placeholder="Enter variable value..."
|
||||
className={`${valueTextareaStyles} min-h-[200px]`}
|
||||
autoResize
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -160,6 +160,16 @@ export function Chat({
|
|||
setIsLastInteracted(true);
|
||||
}
|
||||
|
||||
// clean up event source on component unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (eventSourceRef.current) {
|
||||
eventSourceRef.current.close();
|
||||
eventSourceRef.current = null;
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const container = scrollContainerRef.current;
|
||||
if (!container) return;
|
||||
|
|
|
|||
|
|
@ -63,6 +63,9 @@ interface EntityListProps {
|
|||
onAddAgent: (agent: Partial<z.infer<typeof WorkflowAgent>>) => void;
|
||||
onAddTool: (tool: Partial<z.infer<typeof WorkflowTool>>) => void;
|
||||
onAddPrompt: (prompt: Partial<z.infer<typeof WorkflowPrompt>>) => void;
|
||||
onUpdatePrompt: (name: string, prompt: Partial<z.infer<typeof WorkflowPrompt>>) => void;
|
||||
onAddPromptFromModal: (prompt: Partial<z.infer<typeof WorkflowPrompt>>) => void;
|
||||
onUpdatePromptFromModal: (name: string, prompt: Partial<z.infer<typeof WorkflowPrompt>>) => void;
|
||||
onAddPipeline: (pipeline: Partial<z.infer<typeof WorkflowPipeline>>) => void;
|
||||
onAddAgentToPipeline: (pipelineName: string) => void;
|
||||
onToggleAgent: (name: string) => void;
|
||||
|
|
@ -97,6 +100,7 @@ const EmptyState: React.FC<EmptyStateProps> = ({ entity, hasFilteredItems }) =>
|
|||
|
||||
const ListItemWithMenu = ({
|
||||
name,
|
||||
value,
|
||||
isSelected,
|
||||
onClick,
|
||||
disabled,
|
||||
|
|
@ -110,6 +114,7 @@ const ListItemWithMenu = ({
|
|||
isMocked,
|
||||
}: {
|
||||
name: string;
|
||||
value?: string;
|
||||
isSelected?: boolean;
|
||||
onClick?: () => void;
|
||||
disabled?: boolean;
|
||||
|
|
@ -157,9 +162,34 @@ const ListItemWithMenu = ({
|
|||
/>
|
||||
) : icon}
|
||||
</div>
|
||||
<span className="text-xs">{name}</span>
|
||||
{value ? (
|
||||
<div className="flex-1 min-w-0 grid grid-cols-2 gap-2">
|
||||
<Tooltip
|
||||
content={name}
|
||||
size="sm"
|
||||
delay={500}
|
||||
isDisabled={name.length <= 20}
|
||||
>
|
||||
<span className="text-xs font-medium truncate">
|
||||
{name}
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
content={value}
|
||||
size="sm"
|
||||
delay={500}
|
||||
isDisabled={value.length <= 30}
|
||||
>
|
||||
<span className="text-xs text-zinc-600 dark:text-zinc-400 truncate">
|
||||
{value}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-xs">{name}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
{statusLabel}
|
||||
{isMocked && (
|
||||
<Tooltip content="Mocked" size="sm" delay={500}>
|
||||
|
|
@ -168,7 +198,9 @@ const ListItemWithMenu = ({
|
|||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
{menuContent}
|
||||
<div className="opacity-100">
|
||||
{menuContent}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -464,6 +496,9 @@ export const EntityList = forwardRef<
|
|||
onAddAgent,
|
||||
onAddTool,
|
||||
onAddPrompt,
|
||||
onUpdatePrompt,
|
||||
onAddPromptFromModal,
|
||||
onUpdatePromptFromModal,
|
||||
onAddPipeline,
|
||||
onAddAgentToPipeline,
|
||||
onToggleAgent,
|
||||
|
|
@ -490,6 +525,8 @@ export const EntityList = forwardRef<
|
|||
const [showAgentTypeModal, setShowAgentTypeModal] = useState(false);
|
||||
const [showToolsModal, setShowToolsModal] = useState(false);
|
||||
const [showDataSourcesModal, setShowDataSourcesModal] = useState(false);
|
||||
const [showAddVariableModal, setShowAddVariableModal] = useState(false);
|
||||
const [editingVariable, setEditingVariable] = useState<{name: string; value: string} | null>(null);
|
||||
// State to track which toolkit's tools panel to open
|
||||
const [selectedToolkitSlug, setSelectedToolkitSlug] = useState<string | null>(null);
|
||||
|
||||
|
|
@ -498,6 +535,11 @@ export const EntityList = forwardRef<
|
|||
outputVisibility: agentType
|
||||
});
|
||||
};
|
||||
|
||||
const handleVariableClick = (prompt: z.infer<typeof WorkflowPrompt>) => {
|
||||
setEditingVariable({ name: prompt.name, value: prompt.prompt });
|
||||
setShowAddVariableModal(true);
|
||||
};
|
||||
const selectedRef = useRef<HTMLDivElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [containerHeight, setContainerHeight] = useState<number>(0);
|
||||
|
|
@ -1146,7 +1188,7 @@ export const EntityList = forwardRef<
|
|||
)}
|
||||
</button>
|
||||
<PenLine className="w-4 h-4" />
|
||||
<span>Prompts</span>
|
||||
<span>Variables</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
|
|
@ -1154,11 +1196,11 @@ export const EntityList = forwardRef<
|
|||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setExpandedPanels(prev => ({ ...prev, prompts: true }));
|
||||
onAddPrompt({});
|
||||
setShowAddVariableModal(true);
|
||||
}}
|
||||
className={`group ${buttonClasses}`}
|
||||
showHoverContent={true}
|
||||
hoverContent="Add Prompt"
|
||||
hoverContent="Add Variable"
|
||||
>
|
||||
<PlusIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
|
|
@ -1174,8 +1216,9 @@ export const EntityList = forwardRef<
|
|||
<ListItemWithMenu
|
||||
key={`prompt-${index}`}
|
||||
name={prompt.name}
|
||||
value={prompt.prompt}
|
||||
isSelected={selectedEntity?.type === "prompt" && selectedEntity.name === prompt.name}
|
||||
onClick={() => onSelectPrompt(prompt.name)}
|
||||
onClick={() => handleVariableClick(prompt)}
|
||||
selectedRef={selectedEntity?.type === "prompt" && selectedEntity.name === prompt.name ? selectedRef : undefined}
|
||||
icon={<ScrollText className="w-4 h-4 text-blue-600/70 dark:text-blue-500/70" />}
|
||||
menuContent={
|
||||
|
|
@ -1188,7 +1231,7 @@ export const EntityList = forwardRef<
|
|||
))}
|
||||
</div>
|
||||
) : (
|
||||
<EmptyState entity="prompts" hasFilteredItems={false} />
|
||||
<EmptyState entity="variables" hasFilteredItems={false} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1227,6 +1270,27 @@ export const EntityList = forwardRef<
|
|||
useRagS3Uploads={useRagS3Uploads}
|
||||
useRagScraping={useRagScraping}
|
||||
/>
|
||||
<AddVariableModal
|
||||
isOpen={showAddVariableModal}
|
||||
onClose={() => {
|
||||
setShowAddVariableModal(false);
|
||||
setEditingVariable(null);
|
||||
}}
|
||||
onConfirm={(name, value) => {
|
||||
if (editingVariable) {
|
||||
// Update existing variable using modal-specific handler
|
||||
onUpdatePromptFromModal(editingVariable.name, { name, prompt: value });
|
||||
} else {
|
||||
// Add new variable using modal-specific handler
|
||||
onAddPromptFromModal({ name, prompt: value });
|
||||
}
|
||||
setShowAddVariableModal(false);
|
||||
setEditingVariable(null);
|
||||
}}
|
||||
initialName={editingVariable?.name}
|
||||
initialValue={editingVariable?.value}
|
||||
isEditing={!!editingVariable}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
@ -1922,4 +1986,134 @@ function AgentTypeModal({ isOpen, onClose, onConfirm, onCreatePipeline }: AgentT
|
|||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
interface AddVariableModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onConfirm: (name: string, value: string) => void;
|
||||
initialName?: string;
|
||||
initialValue?: string;
|
||||
isEditing?: boolean;
|
||||
}
|
||||
|
||||
function AddVariableModal({ isOpen, onClose, onConfirm, initialName, initialValue, isEditing = false }: AddVariableModalProps) {
|
||||
const [name, setName] = useState('');
|
||||
const [value, setValue] = useState('');
|
||||
const [errors, setErrors] = useState<{ name?: string; value?: string }>({});
|
||||
|
||||
// Initialize form with values when modal opens
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setName(initialName || '');
|
||||
setValue(initialValue || '');
|
||||
setErrors({});
|
||||
}
|
||||
}, [isOpen, initialName, initialValue]);
|
||||
|
||||
const resetForm = () => {
|
||||
setName('');
|
||||
setValue('');
|
||||
setErrors({});
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
resetForm();
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
const newErrors: { name?: string; value?: string } = {};
|
||||
|
||||
if (!name.trim()) {
|
||||
newErrors.name = 'Variable name is required';
|
||||
}
|
||||
|
||||
if (!value.trim()) {
|
||||
newErrors.value = 'Variable value is required';
|
||||
}
|
||||
|
||||
if (Object.keys(newErrors).length > 0) {
|
||||
setErrors(newErrors);
|
||||
return;
|
||||
}
|
||||
|
||||
onConfirm(name.trim(), value.trim());
|
||||
resetForm();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={handleClose} size="md">
|
||||
<ModalContent>
|
||||
<ModalHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<PenLine className="w-5 h-5 text-indigo-600" />
|
||||
<span>{isEditing ? 'Edit Variable' : 'Add Variable'}</span>
|
||||
</div>
|
||||
</ModalHeader>
|
||||
<ModalBody className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Variable Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => {
|
||||
setName(e.target.value);
|
||||
if (errors.name) setErrors(prev => ({ ...prev, name: undefined }));
|
||||
}}
|
||||
placeholder="Enter variable name (e.g., greeting_message)"
|
||||
className={clsx(
|
||||
"w-full px-3 py-2 border rounded-md text-sm",
|
||||
"focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500",
|
||||
"dark:bg-gray-800 dark:border-gray-600 dark:text-gray-100",
|
||||
errors.name ? "border-red-500" : "border-gray-300 dark:border-gray-600"
|
||||
)}
|
||||
/>
|
||||
{errors.name && (
|
||||
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.name}</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Variable Value
|
||||
</label>
|
||||
<textarea
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
setValue(e.target.value);
|
||||
if (errors.value) setErrors(prev => ({ ...prev, value: undefined }));
|
||||
}}
|
||||
placeholder="Enter the variable value..."
|
||||
rows={4}
|
||||
className={clsx(
|
||||
"w-full px-3 py-2 border rounded-md text-sm resize-none",
|
||||
"focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500",
|
||||
"dark:bg-gray-800 dark:border-gray-600 dark:text-gray-100",
|
||||
errors.value ? "border-red-500" : "border-gray-300 dark:border-gray-600"
|
||||
)}
|
||||
/>
|
||||
{errors.value && (
|
||||
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{errors.value}</p>
|
||||
)}
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={handleClose}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleConfirm}
|
||||
>
|
||||
{isEditing ? 'Update Variable' : 'Add Variable'}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
|
@ -32,7 +32,7 @@ export default async function Page(
|
|||
|
||||
const project = await fetchProjectController.execute({
|
||||
caller: "user",
|
||||
userId: user._id,
|
||||
userId: user.id,
|
||||
projectId: params.projectId,
|
||||
});
|
||||
if (!project) {
|
||||
|
|
@ -41,13 +41,13 @@ export default async function Page(
|
|||
|
||||
const sources = await listDataSourcesController.execute({
|
||||
caller: "user",
|
||||
userId: user._id,
|
||||
userId: user.id,
|
||||
projectId: params.projectId,
|
||||
});
|
||||
|
||||
let eligibleModels: z.infer<typeof ModelsResponse> | "*" = '*';
|
||||
if (USE_BILLING) {
|
||||
eligibleModels = await getEligibleModels(customer._id);
|
||||
eligibleModels = await getEligibleModels(customer.id);
|
||||
}
|
||||
|
||||
console.log('/workflow page.tsx serve');
|
||||
|
|
|
|||
|
|
@ -85,6 +85,9 @@ export type Action = {
|
|||
} | {
|
||||
type: "add_prompt";
|
||||
prompt: Partial<z.infer<typeof WorkflowPrompt>>;
|
||||
} | {
|
||||
type: "add_prompt_no_select";
|
||||
prompt: Partial<z.infer<typeof WorkflowPrompt>>;
|
||||
} | {
|
||||
type: "add_pipeline";
|
||||
pipeline: Partial<z.infer<typeof WorkflowPipeline>>;
|
||||
|
|
@ -143,6 +146,10 @@ export type Action = {
|
|||
type: "update_prompt";
|
||||
name: string;
|
||||
prompt: Partial<z.infer<typeof WorkflowPrompt>>;
|
||||
} | {
|
||||
type: "update_prompt_no_select";
|
||||
name: string;
|
||||
prompt: Partial<z.infer<typeof WorkflowPrompt>>;
|
||||
} | {
|
||||
type: "toggle_agent";
|
||||
name: string;
|
||||
|
|
@ -391,10 +398,10 @@ function reducer(state: State, action: Action): State {
|
|||
if (isLive) {
|
||||
break;
|
||||
}
|
||||
let newPromptName = "New prompt";
|
||||
let newPromptName = "New Variable";
|
||||
if (draft.workflow?.prompts.some((prompt) => prompt.name === newPromptName)) {
|
||||
newPromptName = `New prompt ${draft.workflow?.prompts.filter((prompt) =>
|
||||
prompt.name.startsWith("New prompt")).length + 1}`;
|
||||
newPromptName = `New Variable ${draft.workflow?.prompts.filter((prompt) =>
|
||||
prompt.name.startsWith("New Variable")).length + 1}`;
|
||||
}
|
||||
draft.workflow?.prompts.push({
|
||||
name: newPromptName,
|
||||
|
|
@ -410,6 +417,26 @@ function reducer(state: State, action: Action): State {
|
|||
draft.chatKey++;
|
||||
break;
|
||||
}
|
||||
case "add_prompt_no_select": {
|
||||
if (isLive) {
|
||||
break;
|
||||
}
|
||||
let newPromptName = "New Variable";
|
||||
if (draft.workflow?.prompts.some((prompt) => prompt.name === newPromptName)) {
|
||||
newPromptName = `New Variable ${draft.workflow?.prompts.filter((prompt) =>
|
||||
prompt.name.startsWith("New Variable")).length + 1}`;
|
||||
}
|
||||
draft.workflow?.prompts.push({
|
||||
name: newPromptName,
|
||||
type: "base_prompt",
|
||||
prompt: "",
|
||||
...action.prompt
|
||||
});
|
||||
// Don't set selection - this is the key difference
|
||||
draft.pendingChanges = true;
|
||||
draft.chatKey++;
|
||||
break;
|
||||
}
|
||||
case "add_pipeline": {
|
||||
if (isLive) {
|
||||
break;
|
||||
|
|
@ -751,6 +778,47 @@ function reducer(state: State, action: Action): State {
|
|||
draft.pendingChanges = true;
|
||||
draft.chatKey++;
|
||||
break;
|
||||
case "update_prompt_no_select":
|
||||
if (isLive) {
|
||||
break;
|
||||
}
|
||||
|
||||
// update prompt data
|
||||
draft.workflow.prompts = draft.workflow.prompts.map((prompt) =>
|
||||
prompt.name === action.name ? { ...prompt, ...action.prompt } : prompt
|
||||
);
|
||||
|
||||
// if the prompt is renamed
|
||||
if (action.prompt.name && action.prompt.name !== action.name) {
|
||||
// update this prompts references in other agents / prompts
|
||||
draft.workflow.agents = draft.workflow.agents.map(agent => ({
|
||||
...agent,
|
||||
instructions: agent.instructions.replace(
|
||||
`[@prompt:${action.name}](#mention)`,
|
||||
`[@prompt:${action.prompt.name}](#mention)`
|
||||
)
|
||||
}));
|
||||
draft.workflow.prompts = draft.workflow.prompts.map(prompt => ({
|
||||
...prompt,
|
||||
prompt: prompt.prompt.replace(
|
||||
`[@prompt:${action.name}](#mention)`,
|
||||
`[@prompt:${action.prompt.name}](#mention)`
|
||||
)
|
||||
}));
|
||||
|
||||
// if this is the selected prompt, update the selection
|
||||
if (draft.selection?.type === "prompt" && draft.selection.name === action.name) {
|
||||
draft.selection = {
|
||||
type: "prompt",
|
||||
name: action.prompt.name
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Don't set selection - this is the key difference
|
||||
draft.pendingChanges = true;
|
||||
draft.chatKey++;
|
||||
break;
|
||||
case "toggle_agent":
|
||||
if (isLive) {
|
||||
break;
|
||||
|
|
@ -1074,6 +1142,15 @@ export function WorkflowEditor({
|
|||
dispatch({ type: "update_prompt", name, prompt });
|
||||
}
|
||||
|
||||
// Modal-specific handlers that don't auto-select
|
||||
function handleAddPromptFromModal(prompt: Partial<z.infer<typeof WorkflowPrompt>>) {
|
||||
dispatch({ type: "add_prompt_no_select", prompt });
|
||||
}
|
||||
|
||||
function handleUpdatePromptFromModal(name: string, prompt: Partial<z.infer<typeof WorkflowPrompt>>) {
|
||||
dispatch({ type: "update_prompt_no_select", name, prompt });
|
||||
}
|
||||
|
||||
function handleDeletePrompt(name: string) {
|
||||
if (window.confirm(`Are you sure you want to delete the prompt "${name}"?`)) {
|
||||
dispatch({ type: "delete_prompt", name });
|
||||
|
|
@ -1129,7 +1206,23 @@ export function WorkflowEditor({
|
|||
// Remove handleCopyJSON and add handleDownloadJSON
|
||||
function handleDownloadJSON() {
|
||||
const workflow = state.present.workflow;
|
||||
const json = JSON.stringify(workflow, null, 2);
|
||||
|
||||
// Create a copy of the workflow and replace variable values with dummy text
|
||||
const workflowCopy = {
|
||||
...workflow,
|
||||
prompts: workflow.prompts.map(prompt => {
|
||||
// If this is a variable (base_prompt type), replace its value with dummy text
|
||||
if (prompt.type === 'base_prompt') {
|
||||
return {
|
||||
...prompt,
|
||||
prompt: '<needs to be added>'
|
||||
};
|
||||
}
|
||||
return prompt;
|
||||
})
|
||||
};
|
||||
|
||||
const json = JSON.stringify(workflowCopy, null, 2);
|
||||
const blob = new Blob([json], { type: 'application/json' });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
|
|
@ -1310,6 +1403,9 @@ export function WorkflowEditor({
|
|||
onAddAgent={handleAddAgent}
|
||||
onAddTool={handleAddTool}
|
||||
onAddPrompt={handleAddPrompt}
|
||||
onUpdatePrompt={handleUpdatePrompt}
|
||||
onAddPromptFromModal={handleAddPromptFromModal}
|
||||
onUpdatePromptFromModal={handleUpdatePromptFromModal}
|
||||
onAddPipeline={handleAddPipeline}
|
||||
onAddAgentToPipeline={handleAddAgentToPipeline}
|
||||
onToggleAgent={handleToggleAgent}
|
||||
|
|
|
|||
|
|
@ -256,24 +256,27 @@ export default function Sidebar({ projectId, useAuth, collapsed = false, onToggl
|
|||
|
||||
{/* Theme and Auth Controls */}
|
||||
<div className="p-3 border-t border-zinc-100 dark:border-zinc-800 space-y-2">
|
||||
{USE_PRODUCT_TOUR && !isProjectsRoute && (
|
||||
<Tooltip content={collapsed ? "Help" : ""} showArrow placement="right">
|
||||
<button
|
||||
onClick={showHelpModal}
|
||||
className={`
|
||||
w-full rounded-md flex items-center
|
||||
text-[15px] font-medium transition-all duration-200
|
||||
${collapsed ? 'justify-center py-4' : 'px-4 py-4 gap-3'}
|
||||
hover:bg-zinc-100 dark:hover:bg-zinc-800/50
|
||||
text-zinc-600 dark:text-zinc-400
|
||||
`}
|
||||
data-tour-target="tour-button"
|
||||
>
|
||||
<HelpCircle size={COLLAPSED_ICON_SIZE} />
|
||||
{!collapsed && <span>Help</span>}
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
{/* Help button - always visible, but behavior depends on feature flag */}
|
||||
<Tooltip content={collapsed ? "Help" : ""} showArrow placement="right">
|
||||
<button
|
||||
onClick={USE_PRODUCT_TOUR ? showHelpModal : () => {
|
||||
// Basic help behavior when tour is disabled
|
||||
// You can customize this to show a different help modal or redirect
|
||||
window.open('https://discord.com/invite/rxB8pzHxaS', '_blank');
|
||||
}}
|
||||
className={`
|
||||
w-full rounded-md flex items-center
|
||||
text-[15px] font-medium transition-all duration-200
|
||||
${collapsed ? 'justify-center py-4' : 'px-4 py-4 gap-3'}
|
||||
hover:bg-zinc-100 dark:hover:bg-zinc-800/50
|
||||
text-zinc-600 dark:text-zinc-400
|
||||
`}
|
||||
data-tour-target="tour-button"
|
||||
>
|
||||
<HelpCircle size={COLLAPSED_ICON_SIZE} />
|
||||
{!collapsed && <span>Help</span>}
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
{SHOW_DARK_MODE_TOGGLE && (
|
||||
<Tooltip content={collapsed ? "Appearance" : ""} showArrow placement="right">
|
||||
|
|
|
|||
|
|
@ -433,7 +433,7 @@ async function runDeletionPipeline(_logger: PrefixLogger, job: z.infer<typeof Da
|
|||
logger.log("Error processing doc:", e);
|
||||
await dataSourceDocsRepository.updateByVersion(doc.id, doc.version, {
|
||||
status: "error",
|
||||
error: e.message,
|
||||
error: "Error processing doc",
|
||||
});
|
||||
} finally {
|
||||
// log usage in billing
|
||||
|
|
@ -466,7 +466,7 @@ async function runDeletionPipeline(_logger: PrefixLogger, job: z.infer<typeof Da
|
|||
logger.log("Error deleting doc:", e);
|
||||
await dataSourceDocsRepository.updateByVersion(doc.id, doc.version, {
|
||||
status: "error",
|
||||
error: e.message,
|
||||
error: "Error deleting doc",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,56 +81,6 @@ export function Panel({
|
|||
onClick={onClick}
|
||||
data-tour-target={tourTarget}
|
||||
>
|
||||
{variant === 'copilot' && showWelcome && (
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center pointer-events-none -mt-8">
|
||||
{/* Replace Sparkles icon with mascot image */}
|
||||
<Image src={mascot} alt="Rowboat Mascot" width={192} height={192} className="object-contain mb-4 animate-float" />
|
||||
|
||||
{/* Welcome/Intro Section */}
|
||||
<div className="text-center max-w-md px-6 mb-4">
|
||||
<h3 className="text-xl font-semibold text-zinc-700 dark:text-zinc-300 mb-3 text-center">
|
||||
👋 Welcome to Rowboat!
|
||||
</h3>
|
||||
<p className="text-base text-zinc-600 dark:text-zinc-400 mb-6 text-center">
|
||||
I'm your copilot for building agents and adding tools to them.
|
||||
</p>
|
||||
<p className="text-base text-zinc-600 dark:text-zinc-400 mb-4 text-center">
|
||||
Here's what you can do in Rowboat:
|
||||
</p>
|
||||
<div className="space-y-3 max-w-2xl mx-auto text-left">
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="text-lg">⚡</span>
|
||||
<span className="text-sm text-zinc-600 dark:text-zinc-400">Build AI agents instantly with natural language.</span>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="text-lg">🔌</span>
|
||||
<span className="text-sm text-zinc-600 dark:text-zinc-400">Connect tools with one-click integrations.</span>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="text-lg">📂</span>
|
||||
<span className="text-sm text-zinc-600 dark:text-zinc-400">Power with knowledge by adding documents for RAG.</span>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="text-lg">🔄</span>
|
||||
<span className="text-sm text-zinc-600 dark:text-zinc-400">Automate workflows by setting up triggers and actions.</span>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="text-lg">🚀</span>
|
||||
<span className="text-sm text-zinc-600 dark:text-zinc-400">Deploy anywhere via API or SDK.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{SHOW_COPILOT_MARQUEE && (
|
||||
<div className="relative mt-4 max-w-full px-8">
|
||||
<div className="font-mono text-sm whitespace-nowrap text-blue-400/60 dark:text-blue-500/40 font-small inline-flex">
|
||||
<div className="overflow-hidden w-0 animate-typing">What can I help you build?</div>
|
||||
<div className="border-r-2 border-blue-400 dark:border-blue-500 animate-cursor"> </div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={clsx(
|
||||
"shrink-0 border-b relative",
|
||||
|
|
@ -207,7 +157,9 @@ export function Panel({
|
|||
<div className="px-4 py-3">
|
||||
{children}
|
||||
</div>
|
||||
) : children}
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
|
@ -145,6 +145,9 @@ import { UpdateLiveWorkflowController } from "@/src/interface-adapters/controlle
|
|||
import { RevertToLiveWorkflowUseCase } from "@/src/application/use-cases/projects/revert-to-live-workflow.use-case";
|
||||
import { RevertToLiveWorkflowController } from "@/src/interface-adapters/controllers/projects/revert-to-live-workflow.controller";
|
||||
|
||||
// users
|
||||
import { MongoDBUsersRepository } from "@/src/infrastructure/repositories/mongodb.users.repository";
|
||||
|
||||
export const container = createContainer({
|
||||
injectionMode: InjectionMode.PROXY,
|
||||
strict: true,
|
||||
|
|
@ -324,4 +327,8 @@ container.register({
|
|||
runTurnController: asClass(RunTurnController).singleton(),
|
||||
listConversationsController: asClass(ListConversationsController).singleton(),
|
||||
fetchConversationController: asClass(FetchConversationController).singleton(),
|
||||
|
||||
// users
|
||||
// ---
|
||||
usersRepository: asClass(MongoDBUsersRepository).singleton(),
|
||||
});
|
||||
|
|
@ -412,7 +412,7 @@ export function createMockTool(
|
|||
} catch (error) {
|
||||
logger.log(`Error executing mock tool ${config.name}:`, error);
|
||||
return JSON.stringify({
|
||||
error: `Mock tool execution failed: ${error}`,
|
||||
error: "Tool execution failed!",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -447,7 +447,7 @@ export function createWebhookTool(
|
|||
} catch (error) {
|
||||
logger.log(`Error executing webhook tool ${config.name}:`, error);
|
||||
return JSON.stringify({
|
||||
error: `Tool execution failed: ${error}`,
|
||||
error: "Tool execution failed!",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -482,7 +482,7 @@ export function createMcpTool(
|
|||
} catch (error) {
|
||||
logger.log(`Error executing mcp tool ${name}:`, error);
|
||||
return JSON.stringify({
|
||||
error: `Tool execution failed: ${error}`,
|
||||
error: "Tool execution failed!",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -521,7 +521,7 @@ export function createComposioTool(
|
|||
} catch (error) {
|
||||
logger.log(`Error executing composio tool ${name}:`, error);
|
||||
return JSON.stringify({
|
||||
error: `Tool execution failed: ${error}`,
|
||||
error: "Tool execution failed!",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -143,4 +143,24 @@ export const PIPELINE_TYPE_INSTRUCTIONS = (): string => `
|
|||
- Your response should be self-contained and ready to be consumed by the next pipeline step.
|
||||
- Reading the message history will show you the pipeline execution flow up to your step.
|
||||
- These are high level instructions only. The user will provide more specific instructions which will be below.
|
||||
`;
|
||||
`;
|
||||
|
||||
/**
|
||||
* Instructions for providing variable context to agents
|
||||
* Appends variable names and values to agent system prompts
|
||||
*/
|
||||
export const VARIABLES_CONTEXT_INSTRUCTIONS = (variablesList: Array<{name: string, value: string}>): string => {
|
||||
if (!variablesList || variablesList.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const variablesText = variablesList
|
||||
.map(variable => `${variable.name}: ${variable.value}`)
|
||||
.join('\n');
|
||||
|
||||
return `
|
||||
# Variables Context
|
||||
Here is information that is already provided:
|
||||
${variablesText}
|
||||
`;
|
||||
};
|
||||
|
|
@ -9,7 +9,7 @@ import crypto from "crypto";
|
|||
// Internal dependencies
|
||||
import { createTools, createRagTool } from "./agent-tools";
|
||||
import { ConnectedEntity, sanitizeTextWithMentions, Workflow, WorkflowAgent, WorkflowPipeline, WorkflowPrompt, WorkflowTool } from "@/app/lib/types/workflow_types";
|
||||
import { CHILD_TRANSFER_RELATED_INSTRUCTIONS, CONVERSATION_TYPE_INSTRUCTIONS, PIPELINE_TYPE_INSTRUCTIONS, RAG_INSTRUCTIONS, TASK_TYPE_INSTRUCTIONS } from "./agent_instructions";
|
||||
import { CHILD_TRANSFER_RELATED_INSTRUCTIONS, CONVERSATION_TYPE_INSTRUCTIONS, PIPELINE_TYPE_INSTRUCTIONS, RAG_INSTRUCTIONS, TASK_TYPE_INSTRUCTIONS, VARIABLES_CONTEXT_INSTRUCTIONS } from "./agent_instructions";
|
||||
import { PrefixLogger } from "@/app/lib/utils";
|
||||
import { Message, AssistantMessage, AssistantMessageWithToolCalls, ToolMessage } from "@/app/lib/types/types";
|
||||
import { UsageTracker } from "@/app/lib/billing";
|
||||
|
|
@ -99,6 +99,14 @@ function createAgent(
|
|||
): { agent: Agent, entities: z.infer<typeof ConnectedEntity>[] } {
|
||||
const agentLogger = logger.child(`createAgent: ${config.name}`);
|
||||
|
||||
// Extract variables from workflow prompts (variables are stored as prompts with type 'base_prompt')
|
||||
const variables = workflow.prompts
|
||||
.filter(prompt => prompt.type === 'base_prompt')
|
||||
.map(prompt => ({
|
||||
name: prompt.name,
|
||||
value: prompt.prompt
|
||||
}));
|
||||
|
||||
// Combine instructions and examples
|
||||
let instructions = `${RECOMMENDED_PROMPT_PREFIX}
|
||||
|
||||
|
|
@ -122,6 +130,8 @@ ${config.instructions}
|
|||
|
||||
${config.examples ? ('# Examples\n' + config.examples) : ''}
|
||||
|
||||
${VARIABLES_CONTEXT_INSTRUCTIONS(variables)}
|
||||
|
||||
${'-'.repeat(100)}
|
||||
|
||||
${CHILD_TRANSFER_RELATED_INSTRUCTIONS}
|
||||
|
|
@ -737,29 +747,44 @@ function maybeInjectGiveUpControlInstructions(
|
|||
async function* handleRawModelStreamEvent(
|
||||
event: RunRawModelStreamEvent,
|
||||
agentConfig: Record<string, z.infer<typeof WorkflowAgent>>,
|
||||
pipelineConfig: Record<string, z.infer<typeof WorkflowPipeline>>,
|
||||
agentName: string,
|
||||
turnMsgs: z.infer<typeof Message>[],
|
||||
usageTracker: UsageTracker,
|
||||
eventLogger: PrefixLogger,
|
||||
getAgentState?: (agentName: string) => AgentState
|
||||
): AsyncIterable<z.infer<typeof ZOutMessage>> {
|
||||
// check response visibility - could be an agent or pipeline
|
||||
const agentConfigObj = agentConfig[agentName];
|
||||
const pipelineConfigObj = pipelineConfig[agentName];
|
||||
const isInternal = agentConfigObj?.outputVisibility === 'internal' || agentConfigObj?.type === 'pipeline' || !!pipelineConfigObj;
|
||||
|
||||
if (event.data.type === 'response_done') {
|
||||
// Count tool calls (excluding transfer_to_* calls)
|
||||
const toolCallCount = event.data.response.output.filter(
|
||||
(output: any) => output.type === 'function_call' && !output.name.startsWith('transfer_to')
|
||||
).length;
|
||||
|
||||
// If we have tool calls, increment pending counter
|
||||
if (toolCallCount > 0 && getAgentState) {
|
||||
const state = getAgentState(agentName);
|
||||
state.pendingToolCalls += toolCallCount;
|
||||
eventLogger.log(`🔧 Agent ${agentName} has ${toolCallCount} new tool calls (total: ${state.pendingToolCalls})`);
|
||||
}
|
||||
|
||||
for (const output of event.data.response.output) {
|
||||
if (output.type === 'message') {
|
||||
for (const c of output.content) {
|
||||
if (c.type === 'output_text' && c.text.trim()) {
|
||||
const m: z.infer<typeof Message> = {
|
||||
role: 'assistant',
|
||||
content: c.text,
|
||||
agentName: agentName,
|
||||
responseType: isInternal ? 'internal' : 'external',
|
||||
};
|
||||
turnMsgs.push(m);
|
||||
yield* emitEvent(eventLogger, m);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handle tool call invocation
|
||||
// except for transfer_to_* tool calls
|
||||
if (output.type === 'function_call' && !output.name.startsWith('transfer_to')) {
|
||||
if (getAgentState) {
|
||||
const state = getAgentState(agentName);
|
||||
state.pendingToolCalls++;
|
||||
eventLogger.log(`🔧 Agent ${agentName} has ${state.pendingToolCalls} pending tool calls`);
|
||||
}
|
||||
|
||||
const m: z.infer<typeof Message> = {
|
||||
role: 'assistant',
|
||||
content: null,
|
||||
|
|
@ -1020,6 +1045,7 @@ async function* handleMessageOutput(
|
|||
const pipelineConfigObj = pipelineConfig[agentName];
|
||||
const isInternal = agentConfigObj?.outputVisibility === 'internal' || agentConfigObj?.type === 'pipeline' || !!pipelineConfigObj;
|
||||
|
||||
/* ignore handling text messages here in favor of handling raw events
|
||||
for (const content of event.item.rawItem.content) {
|
||||
if (content.type === 'output_text') {
|
||||
// todo: look into what is causing empty messages
|
||||
|
|
@ -1044,6 +1070,7 @@ async function* handleMessageOutput(
|
|||
yield* emitEvent(eventLogger, msg);
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
// if this is an internal agent or pipeline agent, switch to previous agent
|
||||
if (isInternal) {
|
||||
|
|
@ -1362,10 +1389,20 @@ export async function* streamResponse(
|
|||
// handle streaming events
|
||||
for await (const event of result) {
|
||||
const eventLogger = loopLogger.child(event.type);
|
||||
eventLogger.log(`*** GOT EVENT ***`, JSON.stringify(event));
|
||||
|
||||
switch (event.type) {
|
||||
case 'raw_model_stream_event':
|
||||
yield* handleRawModelStreamEvent(event, agentConfig, agentName!, turnMsgs, usageTracker, eventLogger, getAgentState);
|
||||
yield* handleRawModelStreamEvent(
|
||||
event,
|
||||
agentConfig,
|
||||
pipelineConfig,
|
||||
agentName!,
|
||||
turnMsgs,
|
||||
usageTracker,
|
||||
eventLogger,
|
||||
getAgentState,
|
||||
);
|
||||
break;
|
||||
|
||||
case 'run_item_stream_event':
|
||||
|
|
|
|||
|
|
@ -990,4 +990,92 @@ This workflow is now ready. Once you apply the changes, it will automatically ha
|
|||
|
||||
---
|
||||
|
||||
### Example 7: Setting the start agent
|
||||
|
||||
**User Request**
|
||||
Can you set the start agent to the Meeting Prep Hub?
|
||||
|
||||
**Copilot Response**
|
||||
|
||||
Yes, I can set the start agent to the Meeting Prep Hub.
|
||||
|
||||
\`\`\`copilot_change
|
||||
|
||||
// action: edit
|
||||
// config_type: start_agent
|
||||
// name: Meeting Prep Hub
|
||||
{
|
||||
"change_description": "Set the start agent to the Meeting Prep Hub.",
|
||||
"config_changes": {},
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
|
||||
---
|
||||
|
||||
### Example 8: Delete an agent
|
||||
|
||||
**User Request:**
|
||||
Can you delete the Slack Send Agent?
|
||||
|
||||
**Copilot Response:**
|
||||
|
||||
Yes, I can delete the Slack Send Agent.
|
||||
|
||||
\`\`\`copilot_change
|
||||
// action: delete
|
||||
// config_type: agent
|
||||
// name: Slack Send Agent
|
||||
{
|
||||
"change_description": "Delete the Slack Send Agent.",
|
||||
"config_changes": {},
|
||||
}
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
---
|
||||
|
||||
### Example 9: Delete a tool
|
||||
|
||||
**User Request:**
|
||||
Can you delete the Search tool?
|
||||
|
||||
**Copilot Response:**
|
||||
|
||||
Yes, I can delete the Search tool.
|
||||
|
||||
|
||||
\`\`\`copilot_change
|
||||
// action: delete
|
||||
// config_type: tool
|
||||
// name: Search
|
||||
{
|
||||
"change_description": "Delete the Search tool.",
|
||||
"config_changes": {},
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
---
|
||||
|
||||
### Example 10: Delete a pipeline
|
||||
|
||||
**User Request:**
|
||||
Can you delete the Meeting Prep Pipeline?
|
||||
|
||||
**Copilot Response:**
|
||||
|
||||
Yes, I can delete the Meeting Prep Pipeline.
|
||||
|
||||
\`\`\`copilot_change
|
||||
// action: delete
|
||||
// config_type: pipeline
|
||||
// name: Meeting Prep Pipeline
|
||||
{
|
||||
"change_description": "Delete the Meeting Prep Pipeline.",
|
||||
"config_changes": {},
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
---
|
||||
|
||||
`;
|
||||
|
|
@ -21,8 +21,8 @@ export const CopilotAssistantMessageTextPart = z.object({
|
|||
export const CopilotAssistantMessageActionPart = z.object({
|
||||
type: z.literal("action"),
|
||||
content: z.object({
|
||||
config_type: z.union([z.literal('tool'), z.literal('agent'), z.literal('prompt'), z.literal('pipeline')]),
|
||||
action: z.union([z.literal('create_new'), z.literal('edit')]),
|
||||
config_type: z.enum(['tool', 'agent', 'prompt', 'pipeline', 'start_agent']),
|
||||
action: z.enum(['create_new', 'edit', 'delete']),
|
||||
name: z.string(),
|
||||
change_description: z.string(),
|
||||
config_changes: z.record(z.string(), z.unknown()),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
// returns the number of seconds until the next minute
|
||||
export function secondsToNextMinute(): number {
|
||||
const now = new Date();
|
||||
const secondsUntilNextMinute = 60 - now.getSeconds();
|
||||
return secondsUntilNextMinute;
|
||||
}
|
||||
|
||||
export function minutesToNextHour(): number {
|
||||
const now = new Date();
|
||||
const minutesUntilNextHour = 60 - now.getMinutes();
|
||||
return minutesUntilNextHour;
|
||||
}
|
||||
|
|
@ -1,4 +1,21 @@
|
|||
import { QuotaExceededError } from "@/src/entities/errors/common";
|
||||
|
||||
export interface IUsageQuotaPolicy {
|
||||
// this method will throw a QuotaExceededError if the quota is exceeded
|
||||
assertAndConsume(projectId: string): Promise<void>;
|
||||
/**
|
||||
* Asserts that the project has not exceeded its usage quota and consumes the action.
|
||||
* Used for general project actions.
|
||||
*
|
||||
* @param projectId - The ID of the project to assert and consume.
|
||||
* @throws QuotaExceededError if the quota is exceeded.
|
||||
*/
|
||||
assertAndConsumeProjectAction(projectId: string): Promise<void>;
|
||||
|
||||
|
||||
/**
|
||||
* Asserts that the project has not exceeded its usage quota for running jobs.
|
||||
*
|
||||
* @param projectId - The ID of the project to assert and consume.
|
||||
* @throws QuotaExceededError if the quota is exceeded.
|
||||
*/
|
||||
assertAndConsumeRunJobAction(projectId: string): Promise<void>;
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import { z } from "zod";
|
||||
import { User } from "@/src/entities/models/user";
|
||||
|
||||
export const CreateSchema = User.pick({
|
||||
auth0Id: true,
|
||||
email: true,
|
||||
});
|
||||
|
||||
export interface IUsersRepository {
|
||||
create(data: z.infer<typeof CreateSchema>): Promise<z.infer<typeof User>>;
|
||||
|
||||
fetch(id: string): Promise<z.infer<typeof User> | null>;
|
||||
|
||||
fetchByAuth0Id(auth0Id: string): Promise<z.infer<typeof User> | null>;
|
||||
|
||||
updateEmail(id: string, email: string): Promise<z.infer<typeof User>>;
|
||||
|
||||
updateBillingCustomerId(id: string, billingCustomerId: string): Promise<z.infer<typeof User>>;
|
||||
}
|
||||
|
|
@ -59,7 +59,7 @@ export class CreateComposioTriggerDeploymentUseCase implements ICreateComposioTr
|
|||
});
|
||||
|
||||
// assert and consume quota
|
||||
await this.usageQuotaPolicy.assertAndConsume(projectId);
|
||||
await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId);
|
||||
|
||||
// get trigger type info
|
||||
const triggerType = await getTriggersType(request.data.triggerTypeSlug);
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ export class DeleteComposioTriggerDeploymentUseCase implements IDeleteComposioTr
|
|||
});
|
||||
|
||||
// assert and consume quota
|
||||
await this.usageQuotaPolicy.assertAndConsume(projectId);
|
||||
await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId);
|
||||
|
||||
// ensure deployment belongs to this project
|
||||
const deployment = await this.composioTriggerDeploymentsRepository.fetch(request.deploymentId);
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ export class FetchComposioTriggerDeploymentUseCase implements IFetchComposioTrig
|
|||
});
|
||||
|
||||
// assert and consume quota
|
||||
await this.usageQuotaPolicy.assertAndConsume(projectId);
|
||||
await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId);
|
||||
|
||||
return deployment;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ export class ListComposioTriggerDeploymentsUseCase implements IListComposioTrigg
|
|||
});
|
||||
|
||||
// assert and consume quota
|
||||
await this.usageQuotaPolicy.assertAndConsume(projectId);
|
||||
await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId);
|
||||
|
||||
// fetch deployments for project
|
||||
return await this.composioTriggerDeploymentsRepository.listByProjectId(projectId, request.cursor, limit);
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ export class CreateCachedTurnUseCase implements ICreateCachedTurnUseCase {
|
|||
});
|
||||
|
||||
// assert and consume quota
|
||||
await this.usageQuotaPolicy.assertAndConsume(projectId);
|
||||
await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId);
|
||||
|
||||
// create cache entry
|
||||
const key = nanoid();
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ export class CreateConversationUseCase implements ICreateConversationUseCase {
|
|||
}
|
||||
|
||||
// assert and consume quota
|
||||
await this.usageQuotaPolicy.assertAndConsume(projectId);
|
||||
await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId);
|
||||
|
||||
// if workflow is not provided, fetch workflow
|
||||
if (!workflow) {
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ export class FetchCachedTurnUseCase implements IFetchCachedTurnUseCase {
|
|||
});
|
||||
|
||||
// assert and consume quota
|
||||
await this.usageQuotaPolicy.assertAndConsume(projectId);
|
||||
await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId);
|
||||
|
||||
// delete from cache
|
||||
await this.cacheService.delete(`turn-${data.key}`);
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ export class FetchConversationUseCase implements IFetchConversationUseCase {
|
|||
});
|
||||
|
||||
// assert and consume quota
|
||||
await this.usageQuotaPolicy.assertAndConsume(projectId);
|
||||
await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId);
|
||||
|
||||
// return the conversation
|
||||
return conversation;
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ export class ListConversationsUseCase implements IListConversationsUseCase {
|
|||
});
|
||||
|
||||
// assert and consume quota
|
||||
await this.usageQuotaPolicy.assertAndConsume(projectId);
|
||||
await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId);
|
||||
|
||||
// fetch conversations for project
|
||||
return await this.conversationsRepository.list(projectId, request.cursor, limit);
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ export class RunConversationTurnUseCase implements IRunConversationTurnUseCase {
|
|||
}
|
||||
|
||||
// assert and consume quota
|
||||
await this.usageQuotaPolicy.assertAndConsume(projectId);
|
||||
await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId);
|
||||
|
||||
// Check billing auth
|
||||
let billingCustomerId: string | null = null;
|
||||
|
|
@ -165,6 +165,7 @@ export class RunConversationTurnUseCase implements IRunConversationTurnUseCase {
|
|||
}
|
||||
} finally {
|
||||
// Log billing usage
|
||||
console.log('finally logging billing usage');
|
||||
if (USE_BILLING && billingCustomerId) {
|
||||
await logUsage(billingCustomerId, {
|
||||
items: usageTracker.flush(),
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ export class AddDocsToDataSourceUseCase implements IAddDocsToDataSourceUseCase {
|
|||
projectId: source.projectId,
|
||||
});
|
||||
|
||||
await this.usageQuotaPolicy.assertAndConsume(source.projectId);
|
||||
await this.usageQuotaPolicy.assertAndConsumeProjectAction(source.projectId);
|
||||
|
||||
await this.dataSourceDocsRepository.bulkCreate(source.projectId, sourceId, docs);
|
||||
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ export class CreateDataSourceUseCase implements ICreateDataSourceUseCase {
|
|||
projectId,
|
||||
});
|
||||
|
||||
await this.usageQuotaPolicy.assertAndConsume(projectId);
|
||||
await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId);
|
||||
|
||||
let _status = "pending";
|
||||
// Only set status for non-file data sources
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ export class DeleteDataSourceUseCase implements IDeleteDataSourceUseCase {
|
|||
projectId,
|
||||
});
|
||||
|
||||
await this.usageQuotaPolicy.assertAndConsume(projectId);
|
||||
await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId);
|
||||
|
||||
await this.dataSourcesRepository.update(request.sourceId, {
|
||||
status: 'deleted',
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ export class DeleteDocFromDataSourceUseCase implements IDeleteDocFromDataSourceU
|
|||
projectId: doc.projectId,
|
||||
});
|
||||
|
||||
await this.usageQuotaPolicy.assertAndConsume(doc.projectId);
|
||||
await this.usageQuotaPolicy.assertAndConsumeProjectAction(doc.projectId);
|
||||
|
||||
await this.dataSourceDocsRepository.markAsDeleted(docId);
|
||||
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ export class FetchDataSourceUseCase implements IFetchDataSourceUseCase {
|
|||
projectId,
|
||||
});
|
||||
|
||||
await this.usageQuotaPolicy.assertAndConsume(projectId);
|
||||
await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId);
|
||||
|
||||
return source;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ export class GetDownloadUrlForFileUseCase implements IGetDownloadUrlForFileUseCa
|
|||
projectId: file.projectId,
|
||||
});
|
||||
|
||||
await this.usageQuotaPolicy.assertAndConsume(file.projectId);
|
||||
await this.usageQuotaPolicy.assertAndConsumeProjectAction(file.projectId);
|
||||
|
||||
if (file.data.type === 'file_local') {
|
||||
// use the file id instead of path here
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ export class GetUploadUrlsForFilesUseCase implements IGetUploadUrlsForFilesUseCa
|
|||
projectId: source.projectId,
|
||||
});
|
||||
|
||||
await this.usageQuotaPolicy.assertAndConsume(source.projectId);
|
||||
await this.usageQuotaPolicy.assertAndConsumeProjectAction(source.projectId);
|
||||
|
||||
const urls: { fileId: string, uploadUrl: string, path: string }[] = [];
|
||||
for (const file of files) {
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ export class ListDataSourcesUseCase implements IListDataSourcesUseCase {
|
|||
projectId,
|
||||
});
|
||||
|
||||
await this.usageQuotaPolicy.assertAndConsume(projectId);
|
||||
await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId);
|
||||
|
||||
// list all sources for now
|
||||
const sources = [];
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ export class ListDocsInDataSourceUseCase implements IListDocsInDataSourceUseCase
|
|||
projectId: source.projectId,
|
||||
});
|
||||
|
||||
await this.usageQuotaPolicy.assertAndConsume(source.projectId);
|
||||
await this.usageQuotaPolicy.assertAndConsumeProjectAction(source.projectId);
|
||||
|
||||
// fetch all docs
|
||||
const docs = [];
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ export class RecrawlWebDataSourceUseCase implements IRecrawlWebDataSourceUseCase
|
|||
projectId,
|
||||
});
|
||||
|
||||
await this.usageQuotaPolicy.assertAndConsume(projectId);
|
||||
await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId);
|
||||
|
||||
await this.dataSourceDocsRepository.markSourceDocsPending(request.sourceId);
|
||||
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ export class ToggleDataSourceUseCase implements IToggleDataSourceUseCase {
|
|||
projectId,
|
||||
});
|
||||
|
||||
await this.usageQuotaPolicy.assertAndConsume(projectId);
|
||||
await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId);
|
||||
|
||||
return await this.dataSourcesRepository.update(request.sourceId, { active: request.active });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ export class UpdateDataSourceUseCase implements IUpdateDataSourceUseCase {
|
|||
projectId,
|
||||
});
|
||||
|
||||
await this.usageQuotaPolicy.assertAndConsume(projectId);
|
||||
await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId);
|
||||
|
||||
return await this.dataSourcesRepository.update(request.sourceId, request.data, true);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ export class FetchJobUseCase implements IFetchJobUseCase {
|
|||
});
|
||||
|
||||
// assert and consume quota
|
||||
await this.usageQuotaPolicy.assertAndConsume(projectId);
|
||||
await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId);
|
||||
|
||||
// return the job
|
||||
return job;
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ export class ListJobsUseCase implements IListJobsUseCase {
|
|||
});
|
||||
|
||||
// assert and consume quota
|
||||
await this.usageQuotaPolicy.assertAndConsume(projectId);
|
||||
await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId);
|
||||
|
||||
// fetch jobs for project
|
||||
return await this.jobsRepository.list(projectId, request.filters, request.cursor, limit);
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ export class AddCustomMcpServerUseCase implements IAddCustomMcpServerUseCase {
|
|||
const { caller, userId, apiKey, projectId, name } = request;
|
||||
|
||||
await this.projectActionAuthorizationPolicy.authorize({ caller, userId, apiKey, projectId });
|
||||
await this.usageQuotaPolicy.assertAndConsume(projectId);
|
||||
await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId);
|
||||
|
||||
// Validate server URL
|
||||
const serverUrl = validateHttpHttpsUrl(request.server.serverUrl);
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ export class CreateComposioManagedConnectedAccountUseCase implements ICreateComp
|
|||
const { caller, userId, apiKey, projectId, toolkitSlug, callbackUrl } = request;
|
||||
|
||||
await this.projectActionAuthorizationPolicy.authorize({ caller, userId, apiKey, projectId });
|
||||
await this.usageQuotaPolicy.assertAndConsume(projectId);
|
||||
await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId);
|
||||
|
||||
// fetch managed auth configs
|
||||
const configs = await listAuthConfigs(toolkitSlug, null, true);
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ export class CreateCustomConnectedAccountUseCase implements ICreateCustomConnect
|
|||
const { caller, userId, apiKey, projectId, toolkitSlug, authConfig, callbackUrl } = request;
|
||||
|
||||
await this.projectActionAuthorizationPolicy.authorize({ caller, userId, apiKey, projectId });
|
||||
await this.usageQuotaPolicy.assertAndConsume(projectId);
|
||||
await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId);
|
||||
|
||||
// create custom auth config
|
||||
const created: z.infer<typeof ZCreateAuthConfigResponse> = await createAuthConfig({
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ export class CreateProjectUseCase implements ICreateProjectUseCase {
|
|||
}
|
||||
|
||||
// validate enough credits
|
||||
const result = await authorize(customer._id, {
|
||||
const result = await authorize(customer.id, {
|
||||
type: "create_project",
|
||||
data: {
|
||||
existingProjectCount: count,
|
||||
|
|
@ -113,7 +113,7 @@ export class CreateProjectUseCase implements ICreateProjectUseCase {
|
|||
});
|
||||
|
||||
// assert and consume quota
|
||||
await this.usageQuotaPolicy.assertAndConsume(project.id);
|
||||
await this.usageQuotaPolicy.assertAndConsumeProjectAction(project.id);
|
||||
|
||||
return project;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ export class DeleteComposioConnectedAccountUseCase implements IDeleteComposioCon
|
|||
});
|
||||
|
||||
// assert and consume quota
|
||||
await this.usageQuotaPolicy.assertAndConsume(projectId);
|
||||
await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId);
|
||||
|
||||
// fetch project
|
||||
const project = await this.projectsRepository.fetch(projectId);
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ export class FetchProjectUseCase implements IFetchProjectUseCase {
|
|||
});
|
||||
|
||||
// assert and consume quota
|
||||
await this.usageQuotaPolicy.assertAndConsume(projectId);
|
||||
await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId);
|
||||
|
||||
return await this.projectsRepository.fetch(projectId);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ export class GetComposioToolkitUseCase implements IGetComposioToolkitUseCase {
|
|||
async execute(request: z.infer<typeof InputSchema>): Promise<z.infer<typeof ZGetToolkitResponse>> {
|
||||
const { caller, userId, apiKey, projectId, toolkitSlug } = request;
|
||||
await this.projectActionAuthorizationPolicy.authorize({ caller, userId, apiKey, projectId });
|
||||
await this.usageQuotaPolicy.assertAndConsume(projectId);
|
||||
await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId);
|
||||
return await getToolkit(toolkitSlug);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ export class ListComposioToolkitsUseCase implements IListComposioToolkitsUseCase
|
|||
async execute(request: z.infer<typeof InputSchema>): Promise<z.infer<ReturnType<typeof ZListResponse<typeof ZToolkit>>>> {
|
||||
const { caller, userId, apiKey, projectId, cursor } = request;
|
||||
await this.projectActionAuthorizationPolicy.authorize({ caller, userId, apiKey, projectId });
|
||||
await this.usageQuotaPolicy.assertAndConsume(projectId);
|
||||
await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId);
|
||||
return await listToolkits(cursor ?? null);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ export class ListComposioToolsUseCase implements IListComposioToolsUseCase {
|
|||
async execute(request: z.infer<typeof InputSchema>): Promise<z.infer<ReturnType<typeof ZListResponse<typeof ZTool>>>> {
|
||||
const { caller, userId, apiKey, projectId, toolkitSlug, searchQuery, cursor } = request;
|
||||
await this.projectActionAuthorizationPolicy.authorize({ caller, userId, apiKey, projectId });
|
||||
await this.usageQuotaPolicy.assertAndConsume(projectId);
|
||||
await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId);
|
||||
return await listTools(toolkitSlug, searchQuery ?? null, cursor ?? null);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ export class RemoveCustomMcpServerUseCase implements IRemoveCustomMcpServerUseCa
|
|||
async execute(request: z.infer<typeof InputSchema>): Promise<void> {
|
||||
const { caller, userId, apiKey, projectId, name } = request;
|
||||
await this.projectActionAuthorizationPolicy.authorize({ caller, userId, apiKey, projectId });
|
||||
await this.usageQuotaPolicy.assertAndConsume(projectId);
|
||||
await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId);
|
||||
await this.projectsRepository.deleteCustomMcpServer(projectId, name);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ export class RevertToLiveWorkflowUseCase implements IRevertToLiveWorkflowUseCase
|
|||
apiKey: request.apiKey,
|
||||
projectId,
|
||||
});
|
||||
await this.usageQuotaPolicy.assertAndConsume(projectId);
|
||||
await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId);
|
||||
|
||||
const project = await this.projectsRepository.fetch(projectId);
|
||||
if (!project) {
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ export class RotateSecretUseCase implements IRotateSecretUseCase {
|
|||
});
|
||||
|
||||
// assert and consume quota
|
||||
await this.usageQuotaPolicy.assertAndConsume(projectId);
|
||||
await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId);
|
||||
|
||||
const secret = crypto.randomBytes(32).toString("hex");
|
||||
await this.projectsRepository.updateSecret(projectId, secret);
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ export class SyncConnectedAccountUseCase implements ISyncConnectedAccountUseCase
|
|||
const { caller, userId, apiKey, projectId, toolkitSlug, connectedAccountId } = request;
|
||||
|
||||
await this.projectActionAuthorizationPolicy.authorize({ caller, userId, apiKey, projectId });
|
||||
await this.usageQuotaPolicy.assertAndConsume(projectId);
|
||||
await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId);
|
||||
|
||||
// fetch project & account to verify
|
||||
const project = await this.projectsRepository.fetch(projectId);
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ export class UpdateDraftWorkflowUseCase implements IUpdateDraftWorkflowUseCase {
|
|||
apiKey: request.apiKey,
|
||||
projectId,
|
||||
});
|
||||
await this.usageQuotaPolicy.assertAndConsume(projectId);
|
||||
await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId);
|
||||
|
||||
const workflow = { ...request.workflow, lastUpdatedAt: new Date().toISOString() } as z.infer<typeof Workflow>;
|
||||
await this.projectsRepository.updateDraftWorkflow(projectId, workflow);
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ export class UpdateLiveWorkflowUseCase implements IUpdateLiveWorkflowUseCase {
|
|||
apiKey: request.apiKey,
|
||||
projectId,
|
||||
});
|
||||
await this.usageQuotaPolicy.assertAndConsume(projectId);
|
||||
await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId);
|
||||
|
||||
const workflow = { ...request.workflow, lastUpdatedAt: new Date().toISOString() } as z.infer<typeof Workflow>;
|
||||
await this.projectsRepository.updateLiveWorkflow(projectId, workflow);
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ export class UpdateProjectNameUseCase implements IUpdateProjectNameUseCase {
|
|||
});
|
||||
|
||||
// assert and consume quota
|
||||
await this.usageQuotaPolicy.assertAndConsume(projectId);
|
||||
await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId);
|
||||
|
||||
await this.projectsRepository.updateName(projectId, name);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ export class UpdateWebhookUrlUseCase implements IUpdateWebhookUrlUseCase {
|
|||
});
|
||||
|
||||
// assert and consume quota
|
||||
await this.usageQuotaPolicy.assertAndConsume(projectId);
|
||||
await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId);
|
||||
|
||||
await this.projectsRepository.updateWebhookUrl(projectId, url);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ export class CreateRecurringJobRuleUseCase implements ICreateRecurringJobRuleUse
|
|||
});
|
||||
|
||||
// assert and consume quota
|
||||
await this.usageQuotaPolicy.assertAndConsume(request.projectId);
|
||||
await this.usageQuotaPolicy.assertAndConsumeProjectAction(request.projectId);
|
||||
|
||||
// create the recurring job rule
|
||||
const rule = await this.recurringJobRulesRepository.create({
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ export class DeleteRecurringJobRuleUseCase implements IDeleteRecurringJobRuleUse
|
|||
});
|
||||
|
||||
// assert and consume quota
|
||||
await this.usageQuotaPolicy.assertAndConsume(request.projectId);
|
||||
await this.usageQuotaPolicy.assertAndConsumeProjectAction(request.projectId);
|
||||
|
||||
// ensure rule belongs to this project
|
||||
const rule = await this.recurringJobRulesRepository.fetch(request.ruleId);
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ export class FetchRecurringJobRuleUseCase implements IFetchRecurringJobRuleUseCa
|
|||
});
|
||||
|
||||
// assert and consume quota
|
||||
await this.usageQuotaPolicy.assertAndConsume(projectId);
|
||||
await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId);
|
||||
|
||||
// return the rule
|
||||
return rule;
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ export class ListRecurringJobRulesUseCase implements IListRecurringJobRulesUseCa
|
|||
});
|
||||
|
||||
// assert and consume quota
|
||||
await this.usageQuotaPolicy.assertAndConsume(projectId);
|
||||
await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId);
|
||||
|
||||
// fetch recurring job rules for project
|
||||
return await this.recurringJobRulesRepository.list(projectId, request.cursor, limit);
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ export class ToggleRecurringJobRuleUseCase implements IToggleRecurringJobRuleUse
|
|||
});
|
||||
|
||||
// assert and consume quota
|
||||
await this.usageQuotaPolicy.assertAndConsume(projectId);
|
||||
await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId);
|
||||
|
||||
// update the rule
|
||||
return await this.recurringJobRulesRepository.toggle(request.ruleId, request.disabled);
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ export class CreateScheduledJobRuleUseCase implements ICreateScheduledJobRuleUse
|
|||
});
|
||||
|
||||
// assert and consume quota
|
||||
await this.usageQuotaPolicy.assertAndConsume(request.projectId);
|
||||
await this.usageQuotaPolicy.assertAndConsumeProjectAction(request.projectId);
|
||||
|
||||
// create the scheduled job rule with UTC time
|
||||
const rule = await this.scheduledJobRulesRepository.create({
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ export class DeleteScheduledJobRuleUseCase implements IDeleteScheduledJobRuleUse
|
|||
});
|
||||
|
||||
// assert and consume quota
|
||||
await this.usageQuotaPolicy.assertAndConsume(request.projectId);
|
||||
await this.usageQuotaPolicy.assertAndConsumeProjectAction(request.projectId);
|
||||
|
||||
// ensure rule belongs to this project
|
||||
const rule = await this.scheduledJobRulesRepository.fetch(request.ruleId);
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ export class FetchScheduledJobRuleUseCase implements IFetchScheduledJobRuleUseCa
|
|||
});
|
||||
|
||||
// assert and consume quota
|
||||
await this.usageQuotaPolicy.assertAndConsume(projectId);
|
||||
await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId);
|
||||
|
||||
// return the scheduled job rule
|
||||
return rule;
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ export class ListScheduledJobRulesUseCase implements IListScheduledJobRulesUseCa
|
|||
});
|
||||
|
||||
// assert and consume quota
|
||||
await this.usageQuotaPolicy.assertAndConsume(projectId);
|
||||
await this.usageQuotaPolicy.assertAndConsumeProjectAction(projectId);
|
||||
|
||||
// fetch scheduled job rules for project
|
||||
return await this.scheduledJobRulesRepository.list(projectId, request.cursor, limit);
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { z } from "zod";
|
|||
import { nanoid } from "nanoid";
|
||||
import { PrefixLogger } from "@/app/lib/utils";
|
||||
import { RecurringJobRule } from "@/src/entities/models/recurring-job-rule";
|
||||
import { secondsToNextMinute } from "../lib/utils/time-to-next-minute";
|
||||
|
||||
export interface IJobRulesWorker {
|
||||
run(): Promise<void>;
|
||||
|
|
@ -20,8 +21,6 @@ export class JobRulesWorker implements IJobRulesWorker {
|
|||
private readonly jobsRepository: IJobsRepository;
|
||||
private readonly projectsRepository: IProjectsRepository;
|
||||
private readonly pubSubService: IPubSubService;
|
||||
// Run polls aligned to minute marks at this offset (e.g., 2000 ms => :02 each minute)
|
||||
private readonly minuteAlignmentOffsetMs: number = 2_000;
|
||||
private workerId: string;
|
||||
private logger: PrefixLogger;
|
||||
private isRunning: boolean = false;
|
||||
|
|
@ -131,14 +130,6 @@ export class JobRulesWorker implements IJobRulesWorker {
|
|||
}
|
||||
}
|
||||
|
||||
// Calculates delay so the next run happens at next minute + minuteAlignmentOffsetMs
|
||||
private calculateDelayToNextAlignedMinute(): number {
|
||||
const now = new Date();
|
||||
const millisecondsUntilNextMinute = (60 - now.getSeconds()) * 1000 - now.getMilliseconds();
|
||||
const delayMs = millisecondsUntilNextMinute + this.minuteAlignmentOffsetMs;
|
||||
return delayMs > 0 ? delayMs : this.minuteAlignmentOffsetMs;
|
||||
}
|
||||
|
||||
private async pollScheduled(): Promise<void> {
|
||||
const logger = this.logger.child(`poll-scheduled`);
|
||||
logger.log("Polling...");
|
||||
|
|
@ -176,7 +167,8 @@ export class JobRulesWorker implements IJobRulesWorker {
|
|||
}
|
||||
|
||||
private scheduleNextPoll(): void {
|
||||
const delayMs = this.calculateDelayToNextAlignedMinute();
|
||||
// schedule next poll for 2 s past the minute mark
|
||||
const delayMs = (secondsToNextMinute() + 2) * 1000;
|
||||
this.logger.log(`Scheduling next poll in ${delayMs} ms`);
|
||||
this.pollTimeoutId = setTimeout(async () => {
|
||||
if (!this.isRunning) return;
|
||||
|
|
@ -195,7 +187,6 @@ export class JobRulesWorker implements IJobRulesWorker {
|
|||
}
|
||||
this.isRunning = true;
|
||||
this.logger.log(`Starting worker ${this.workerId}`);
|
||||
// No immediate polling; align to 2s past the next minute
|
||||
this.scheduleNextPoll();
|
||||
}
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue