99 lines
3.2 KiB
TypeScript
99 lines
3.2 KiB
TypeScript
import { openai } from "@ai-sdk/openai";
|
|
import { json, type ActionFunctionArgs } from "@remix-run/server-runtime";
|
|
import { tryCatch } from "@trigger.dev/core/utils";
|
|
import { z } from "zod";
|
|
import { prisma } from "~/db.server";
|
|
import { env } from "~/env.server";
|
|
import { findProjectBySlug } from "~/models/project.server";
|
|
import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server";
|
|
import { requireUserId } from "~/services/session.server";
|
|
import { EnvironmentParamSchema } from "~/utils/pathBuilder";
|
|
import { aiTitleRateLimiter } from "~/v3/services/aiTitleRateLimiter.server";
|
|
import { AIQueryTitleService } from "~/v3/services/aiQueryTitleService.server";
|
|
|
|
// `/resources/*` isn't covered by the global apiRateLimiter (`/api/*` only),
|
|
// so this endpoint needs its own per-route limiter and length cap.
|
|
const MAX_QUERY_LENGTH = 10_000;
|
|
|
|
const RequestSchema = z.object({
|
|
query: z.string().min(1, "Query is required").max(MAX_QUERY_LENGTH),
|
|
queryId: z.string().optional(),
|
|
});
|
|
|
|
export async function action({ request, params }: ActionFunctionArgs) {
|
|
const userId = await requireUserId(request);
|
|
const { organizationSlug, projectParam, envParam } = EnvironmentParamSchema.parse(params);
|
|
|
|
const limit = await aiTitleRateLimiter.limit(`user:${userId}`);
|
|
if (!limit.success) {
|
|
return json(
|
|
{
|
|
success: false as const,
|
|
error: "Too many requests — please wait a moment and try again.",
|
|
title: null,
|
|
},
|
|
{ status: 429 }
|
|
);
|
|
}
|
|
|
|
// Parse the request body
|
|
const [error, data] = await tryCatch(request.json());
|
|
if (error) {
|
|
return json({ success: false as const, error: error.message, title: null }, { status: 400 });
|
|
}
|
|
const submission = RequestSchema.safeParse(data);
|
|
|
|
if (!submission.success) {
|
|
return json(
|
|
{ success: false as const, error: "Invalid request data", title: null },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
const project = await findProjectBySlug(organizationSlug, projectParam, userId);
|
|
if (!project) {
|
|
return json(
|
|
{ success: false as const, error: "Project not found", title: null },
|
|
{ status: 404 }
|
|
);
|
|
}
|
|
|
|
const environment = await findEnvironmentBySlug(project.id, envParam, userId);
|
|
if (!environment) {
|
|
return json(
|
|
{ success: false as const, error: "Environment not found", title: null },
|
|
{ status: 404 }
|
|
);
|
|
}
|
|
|
|
if (!env.OPENAI_API_KEY) {
|
|
return json(
|
|
{ success: false as const, error: "OpenAI API key is not configured", title: null },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
const { query, queryId } = submission.data;
|
|
|
|
const service = new AIQueryTitleService(openai(env.AI_RUN_FILTER_MODEL ?? "gpt-4o-mini"));
|
|
|
|
const result = await service.generateTitle(query);
|
|
|
|
if (!result.success) {
|
|
return json({ success: false as const, error: result.error, title: null }, { status: 500 });
|
|
}
|
|
|
|
// Strip leading/trailing quotes that AI sometimes adds
|
|
const title = result.title.replace(/^["']|["']$/g, "");
|
|
|
|
// If a queryId was provided, update the CustomerQuery record with the title
|
|
if (queryId) {
|
|
await prisma.customerQuery.update({
|
|
where: { id: queryId, organizationId: project.organizationId },
|
|
data: { title },
|
|
});
|
|
}
|
|
|
|
return json({ success: true as const, title, error: null });
|
|
}
|