mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-07-14 23:07:01 +10:00
chore: lint using react-doctor, update translations, dynamic imports
This commit is contained in:
@@ -355,9 +355,8 @@ describe("agentService.messages.send", () => {
|
||||
});
|
||||
aiProvidersServiceMock.markUsed.mockResolvedValue(undefined);
|
||||
|
||||
const { convertToModelMessages, ToolLoopAgent } = await import("ai");
|
||||
const { agentStreamLifecycle } = await import("./streams");
|
||||
const { streamToEventIterator } = await import("@orpc/server");
|
||||
const [{ convertToModelMessages, ToolLoopAgent }, { agentStreamLifecycle }, { streamToEventIterator }] =
|
||||
await Promise.all([import("ai"), import("./streams"), import("@orpc/server")]);
|
||||
vi.mocked(convertToModelMessages).mockResolvedValue([
|
||||
{ role: "user", content: [{ type: "text", text: "Use this file" }] },
|
||||
]);
|
||||
@@ -435,10 +434,12 @@ describe("agentService.messages.send", () => {
|
||||
});
|
||||
aiProvidersServiceMock.markUsed.mockResolvedValue(undefined);
|
||||
|
||||
const { convertToModelMessages, ToolLoopAgent } = await import("ai");
|
||||
const { agentStreamLifecycle } = await import("./streams");
|
||||
const { buildAgentTools } = await import("./tools");
|
||||
const { streamToEventIterator } = await import("@orpc/server");
|
||||
const [
|
||||
{ convertToModelMessages, ToolLoopAgent },
|
||||
{ agentStreamLifecycle },
|
||||
{ buildAgentTools },
|
||||
{ streamToEventIterator },
|
||||
] = await Promise.all([import("ai"), import("./streams"), import("./tools"), import("@orpc/server")]);
|
||||
vi.mocked(convertToModelMessages).mockResolvedValue([
|
||||
{ role: "user", content: [{ type: "text", text: "Add a custom field" }] },
|
||||
]);
|
||||
@@ -577,9 +578,8 @@ describe("agentService.messages.send", () => {
|
||||
aiProvidersServiceMock.markUsed.mockResolvedValue(undefined);
|
||||
storageServiceMock.read.mockResolvedValue({ data: new TextEncoder().encode("hello"), contentType: "text/plain" });
|
||||
|
||||
const { convertToModelMessages, ToolLoopAgent } = await import("ai");
|
||||
const { agentStreamLifecycle } = await import("./streams");
|
||||
const { streamToEventIterator } = await import("@orpc/server");
|
||||
const [{ convertToModelMessages, ToolLoopAgent }, { agentStreamLifecycle }, { streamToEventIterator }] =
|
||||
await Promise.all([import("ai"), import("./streams"), import("@orpc/server")]);
|
||||
const streamMock = vi.fn(async () => ({
|
||||
toUIMessageStream: vi.fn(() => new ReadableStream()),
|
||||
}));
|
||||
@@ -743,9 +743,8 @@ describe("agentService.messages.send", () => {
|
||||
});
|
||||
aiProvidersServiceMock.markUsed.mockResolvedValue(undefined);
|
||||
|
||||
const { convertToModelMessages, ToolLoopAgent } = await import("ai");
|
||||
const { agentStreamLifecycle } = await import("./streams");
|
||||
const { streamToEventIterator } = await import("@orpc/server");
|
||||
const [{ convertToModelMessages, ToolLoopAgent }, { agentStreamLifecycle }, { streamToEventIterator }] =
|
||||
await Promise.all([import("ai"), import("./streams"), import("@orpc/server")]);
|
||||
vi.mocked(convertToModelMessages).mockResolvedValue([
|
||||
{ role: "user", content: [{ type: "text", text: "Change the name" }] },
|
||||
{
|
||||
@@ -903,9 +902,8 @@ describe("agentService.messages.send", () => {
|
||||
});
|
||||
aiProvidersServiceMock.markUsed.mockResolvedValue(undefined);
|
||||
|
||||
const { convertToModelMessages, ToolLoopAgent } = await import("ai");
|
||||
const { agentStreamLifecycle } = await import("./streams");
|
||||
const { streamToEventIterator } = await import("@orpc/server");
|
||||
const [{ convertToModelMessages, ToolLoopAgent }, { agentStreamLifecycle }, { streamToEventIterator }] =
|
||||
await Promise.all([import("ai"), import("./streams"), import("@orpc/server")]);
|
||||
vi.mocked(convertToModelMessages).mockResolvedValue([{ role: "user", content: [{ type: "text", text: "Retry" }] }]);
|
||||
class MockToolLoopAgent {
|
||||
stream = vi.fn(async () => ({ toUIMessageStream: vi.fn(() => new ReadableStream()) }));
|
||||
|
||||
@@ -187,7 +187,11 @@ type AgentToolPart = UIMessage["parts"][number] & {
|
||||
toolCallId?: string;
|
||||
};
|
||||
|
||||
function isAnsweredAskUserQuestionPart(part: UIMessage["parts"][number]): part is AgentToolPart {
|
||||
type AnsweredAskUserQuestionPart = AgentToolPart & {
|
||||
toolCallId: string;
|
||||
};
|
||||
|
||||
function isAnsweredAskUserQuestionPart(part: UIMessage["parts"][number]): part is AnsweredAskUserQuestionPart {
|
||||
const toolPart = part as AgentToolPart;
|
||||
return (
|
||||
toolPart.type === "tool-ask_user_question" &&
|
||||
@@ -197,9 +201,11 @@ function isAnsweredAskUserQuestionPart(part: UIMessage["parts"][number]): part i
|
||||
}
|
||||
|
||||
function mergeAskUserQuestionOutputs(existingMessage: UIMessage, incomingMessage: UIMessage): UIMessage {
|
||||
const answeredParts = new Map(
|
||||
incomingMessage.parts.filter(isAnsweredAskUserQuestionPart).map((part) => [part.toolCallId, part] as const),
|
||||
);
|
||||
const answeredParts = new Map<string, AgentToolPart>();
|
||||
|
||||
for (const part of incomingMessage.parts) {
|
||||
if (isAnsweredAskUserQuestionPart(part)) answeredParts.set(part.toolCallId, part);
|
||||
}
|
||||
|
||||
let didMerge = false;
|
||||
const parts = existingMessage.parts.map((part) => {
|
||||
@@ -580,6 +586,7 @@ async function repairLegacyAskUserQuestionAnswers(
|
||||
input: { threadId: string; userId: string },
|
||||
) {
|
||||
const nextRows = [...rows];
|
||||
const updates: Promise<unknown>[] = [];
|
||||
|
||||
for (let index = 0; index < nextRows.length - 1; index++) {
|
||||
const assistantRow = nextRows[index];
|
||||
@@ -598,21 +605,25 @@ async function repairLegacyAskUserQuestionAnswers(
|
||||
uiMessage: mergedMessage as unknown as AgentMessageRecord["uiMessage"],
|
||||
};
|
||||
|
||||
await db
|
||||
.update(schema.agentMessage)
|
||||
.set({
|
||||
status: "completed",
|
||||
uiMessage: mergedMessage as unknown as Record<string, unknown>,
|
||||
})
|
||||
.where(
|
||||
and(
|
||||
eq(schema.agentMessage.id, assistantRow.id),
|
||||
eq(schema.agentMessage.threadId, input.threadId),
|
||||
eq(schema.agentMessage.userId, input.userId),
|
||||
updates.push(
|
||||
db
|
||||
.update(schema.agentMessage)
|
||||
.set({
|
||||
status: "completed",
|
||||
uiMessage: mergedMessage as unknown as Record<string, unknown>,
|
||||
})
|
||||
.where(
|
||||
and(
|
||||
eq(schema.agentMessage.id, assistantRow.id),
|
||||
eq(schema.agentMessage.threadId, input.threadId),
|
||||
eq(schema.agentMessage.userId, input.userId),
|
||||
),
|
||||
),
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.all(updates);
|
||||
|
||||
return nextRows;
|
||||
}
|
||||
|
||||
@@ -635,11 +646,13 @@ async function cleanupActiveRun(input: {
|
||||
}
|
||||
|
||||
function messageText(message: UIMessage) {
|
||||
return message.parts
|
||||
.filter((part) => part.type === "text")
|
||||
.map((part) => part.text)
|
||||
.join(" ")
|
||||
.trim();
|
||||
const textParts: string[] = [];
|
||||
|
||||
for (const part of message.parts) {
|
||||
if (part.type === "text") textParts.push(part.text);
|
||||
}
|
||||
|
||||
return textParts.join(" ").trim();
|
||||
}
|
||||
|
||||
function buildThreadTitle(message: UIMessage, fallback: string) {
|
||||
@@ -933,12 +946,14 @@ export const agentService = {
|
||||
|
||||
await getThread({ id: input.id, userId: input.userId });
|
||||
|
||||
await getStorageService().delete(`uploads/${input.userId}/agent/${input.id}`);
|
||||
await db.delete(schema.agentAttachment).where(eq(schema.agentAttachment.threadId, input.id));
|
||||
await db
|
||||
.update(schema.agentThread)
|
||||
.set({ status: "deleted", deletedAt: new Date() })
|
||||
.where(and(eq(schema.agentThread.id, input.id), eq(schema.agentThread.userId, input.userId)));
|
||||
await Promise.all([
|
||||
getStorageService().delete(`uploads/${input.userId}/agent/${input.id}`),
|
||||
db.delete(schema.agentAttachment).where(eq(schema.agentAttachment.threadId, input.id)),
|
||||
db
|
||||
.update(schema.agentThread)
|
||||
.set({ status: "deleted", deletedAt: new Date() })
|
||||
.where(and(eq(schema.agentThread.id, input.id), eq(schema.agentThread.userId, input.userId))),
|
||||
]);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -960,15 +975,17 @@ export const agentService = {
|
||||
throw new ORPCError("BAD_REQUEST", { message: "Agent messages must be user messages or tool results." });
|
||||
}
|
||||
|
||||
const runnableProvider = await aiProvidersService.getRunnableById({
|
||||
id: thread.aiProviderId,
|
||||
userId: input.userId,
|
||||
});
|
||||
const attachments = await getUnlinkedMessageAttachments({
|
||||
ids: input.attachmentIds ?? [],
|
||||
threadId: input.threadId,
|
||||
userId: input.userId,
|
||||
});
|
||||
const [runnableProvider, attachments] = await Promise.all([
|
||||
aiProvidersService.getRunnableById({
|
||||
id: thread.aiProviderId,
|
||||
userId: input.userId,
|
||||
}),
|
||||
getUnlinkedMessageAttachments({
|
||||
ids: input.attachmentIds ?? [],
|
||||
threadId: input.threadId,
|
||||
userId: input.userId,
|
||||
}),
|
||||
]);
|
||||
const runId = generateId();
|
||||
const streamId = generateId();
|
||||
const controller = new AbortController();
|
||||
@@ -1156,19 +1173,20 @@ export const agentService = {
|
||||
assertAgentEnvironment();
|
||||
await getThread({ id: input.threadId, userId: input.userId });
|
||||
|
||||
const [aggregate] = await db
|
||||
.select({ totalBytes: sql<number>`coalesce(sum(${schema.agentAttachment.size}), 0)` })
|
||||
.from(schema.agentAttachment)
|
||||
.where(
|
||||
and(eq(schema.agentAttachment.threadId, input.threadId), eq(schema.agentAttachment.userId, input.userId)),
|
||||
);
|
||||
|
||||
const [attachmentCount] = await db
|
||||
.select({ total: count() })
|
||||
.from(schema.agentAttachment)
|
||||
.where(
|
||||
and(eq(schema.agentAttachment.threadId, input.threadId), eq(schema.agentAttachment.userId, input.userId)),
|
||||
);
|
||||
const [[aggregate], [attachmentCount]] = await Promise.all([
|
||||
db
|
||||
.select({ totalBytes: sql<number>`coalesce(sum(${schema.agentAttachment.size}), 0)` })
|
||||
.from(schema.agentAttachment)
|
||||
.where(
|
||||
and(eq(schema.agentAttachment.threadId, input.threadId), eq(schema.agentAttachment.userId, input.userId)),
|
||||
),
|
||||
db
|
||||
.select({ total: count() })
|
||||
.from(schema.agentAttachment)
|
||||
.where(
|
||||
and(eq(schema.agentAttachment.threadId, input.threadId), eq(schema.agentAttachment.userId, input.userId)),
|
||||
),
|
||||
]);
|
||||
|
||||
if ((attachmentCount?.total ?? 0) >= MAX_ATTACHMENTS_PER_MESSAGE) throw new ORPCError("BAD_REQUEST");
|
||||
if (input.data.byteLength > MAX_ATTACHMENT_BYTES) throw new ORPCError("BAD_REQUEST");
|
||||
|
||||
@@ -17,17 +17,17 @@ vi.mock("drizzle-orm", () => ({
|
||||
isNull: (value: unknown) => ({ type: "isNull", value }),
|
||||
}));
|
||||
|
||||
async function readStream(stream: ReadableStream<string>) {
|
||||
const reader = stream.getReader();
|
||||
const chunks: string[] = [];
|
||||
async function readStreamChunks(reader: ReadableStreamDefaultReader<string>, chunks: string[]): Promise<string[]> {
|
||||
const { done, value } = await reader.read();
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
chunks.push(value);
|
||||
}
|
||||
if (done) return chunks;
|
||||
|
||||
return chunks;
|
||||
chunks.push(value);
|
||||
return readStreamChunks(reader, chunks);
|
||||
}
|
||||
|
||||
function readStream(stream: ReadableStream<string>) {
|
||||
return readStreamChunks(stream.getReader(), []);
|
||||
}
|
||||
|
||||
function createRunStateDb(returningRows: unknown[] = []) {
|
||||
|
||||
@@ -75,17 +75,28 @@ export async function* subscribeResumeUpdated({ resumeId, userId, signal }: Subs
|
||||
try {
|
||||
await client.query(`LISTEN ${RESUME_UPDATED_CHANNEL}`);
|
||||
|
||||
while (!done) {
|
||||
const waitForNextEvent = async (): Promise<ResumeUpdatedEvent | null> => {
|
||||
if (done) return null;
|
||||
|
||||
const event = queue.shift();
|
||||
if (event) {
|
||||
yield event;
|
||||
continue;
|
||||
}
|
||||
if (event) return event;
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
wake = resolve;
|
||||
});
|
||||
|
||||
return waitForNextEvent();
|
||||
};
|
||||
|
||||
async function* streamEvents(): AsyncGenerator<ResumeUpdatedEvent> {
|
||||
const event = await waitForNextEvent();
|
||||
if (!event) return;
|
||||
|
||||
yield event;
|
||||
yield* streamEvents();
|
||||
}
|
||||
|
||||
yield* streamEvents();
|
||||
} finally {
|
||||
signal?.removeEventListener("abort", onAbort);
|
||||
client.off("notification", onNotification);
|
||||
|
||||
@@ -101,13 +101,11 @@ const fetchGitHubStarsOnce = async (): Promise<number | null> => {
|
||||
}
|
||||
};
|
||||
|
||||
const getGitHubStars = async (): Promise<number | null> => {
|
||||
for (let attempt = 0; attempt < GITHUB_REQUEST_MAX_ATTEMPTS; attempt++) {
|
||||
const stars = await fetchGitHubStarsOnce();
|
||||
if (stars !== null) return stars;
|
||||
}
|
||||
const getGitHubStars = async (attempt = 1): Promise<number | null> => {
|
||||
if (attempt > GITHUB_REQUEST_MAX_ATTEMPTS) return null;
|
||||
|
||||
return null;
|
||||
const stars = await fetchGitHubStarsOnce();
|
||||
return stars ?? getGitHubStars(attempt + 1);
|
||||
};
|
||||
|
||||
export const statisticsService = {
|
||||
|
||||
@@ -300,9 +300,8 @@ class S3StorageService implements StorageService {
|
||||
}
|
||||
|
||||
async delete(keyOrPrefix: string): Promise<boolean> {
|
||||
const client = await this.getClient();
|
||||
// Use list to find all matching keys (handles both single file and folder/prefix)
|
||||
const keys = await this.list(keyOrPrefix);
|
||||
const [client, keys] = await Promise.all([this.getClient(), this.list(keyOrPrefix)]);
|
||||
|
||||
if (keys.length === 0) return false;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user