mirror of
https://github.com/andy-stack/vaultkeeper-ai.git
synced 2026-07-22 06:42:03 +00:00
refactor: extract attachment processing logic to base class
Consolidate duplicate file upload and error handling code from Claude, Gemini, and OpenAI into shared `processAttachments` method in BaseAIClass. Remove unnecessary abort service wrapping from BaseAIFileService retry logic.
This commit is contained in:
parent
e24a5a02bf
commit
59db78d610
5 changed files with 65 additions and 89 deletions
|
|
@ -130,6 +130,32 @@ export abstract class BaseAIClass implements IAIClass {
|
|||
].filter(s => s).join("\n\n");
|
||||
}
|
||||
|
||||
protected async processAttachments<T>(
|
||||
attachments: Attachment[],
|
||||
formatBinaryFiles: (attachments: Attachment[]) => string
|
||||
): Promise<{ formattedParts: T[], errorMessage?: string }> {
|
||||
const failedUploads: string[] = [];
|
||||
|
||||
for (const attachment of attachments) {
|
||||
try {
|
||||
await this.aiFileService.uploadFile(attachment);
|
||||
} catch (error) {
|
||||
Exception.log(`Failed to upload ${attachment.fileName}: ${Exception.messageFrom(error)}`);
|
||||
failedUploads.push(attachment.fileName);
|
||||
}
|
||||
}
|
||||
|
||||
const formattedContent = formatBinaryFiles(attachments);
|
||||
const formattedParts = JSON.parse(formattedContent) as T[];
|
||||
|
||||
return {
|
||||
formattedParts,
|
||||
errorMessage: failedUploads.length > 0
|
||||
? `[Upload failed for: ${failedUploads.join(', ')}.]`
|
||||
: undefined
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a function call to legacy text format for cross-provider compatibility.
|
||||
* Used when a provider doesn't have the required ID field (e.g., Gemini → Claude/OpenAI).
|
||||
|
|
|
|||
|
|
@ -85,44 +85,28 @@ export abstract class BaseAIFileService implements IAIFileService {
|
|||
|
||||
// Retries operation on retryable errors (500, 502, 503, 504) with exponential backoff
|
||||
protected async withRetry<T>(operationName: string, operation: () => Promise<T>, defaultValue: T): Promise<T> {
|
||||
return await this.abortService.abortableOperation(async () => {
|
||||
for (let attempt = 1; attempt <= BaseAIFileService.MAX_RETRIES; attempt++) {
|
||||
try {
|
||||
return await operation();
|
||||
} catch (error) {
|
||||
|
||||
// Don't retry on abort errors - throw immediately
|
||||
if (AbortService.isAbortError(error)) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (ApiError.isApiError(error) && error.info.isRetryable) {
|
||||
if (attempt === BaseAIFileService.MAX_RETRIES) {
|
||||
Exception.log(`${operationName}: Max retries (${BaseAIFileService.MAX_RETRIES}) exhausted. Returning default value.`);
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
const delay = BaseAIFileService.RETRY_DELAYS[attempt];
|
||||
Exception.warn(`${operationName}: Attempt ${attempt}/${BaseAIFileService.MAX_RETRIES} failed with ${error.info.type} (status ${error.info.statusCode}). Retrying in ${delay}ms...`);
|
||||
|
||||
if (this.abortService.signal().aborted) {
|
||||
this.abortService.throw();
|
||||
}
|
||||
|
||||
await sleep(delay);
|
||||
|
||||
if (this.abortService.signal().aborted) {
|
||||
this.abortService.throw();
|
||||
}
|
||||
} else {
|
||||
// Non-retryable error - return default value
|
||||
Exception.log(`${operationName}: Non-retryable error. Returning default value.`);
|
||||
for (let attempt = 1; attempt <= BaseAIFileService.MAX_RETRIES; attempt++) {
|
||||
try {
|
||||
return await operation();
|
||||
} catch (error) {
|
||||
if (ApiError.isApiError(error) && error.info.isRetryable) {
|
||||
if (attempt === BaseAIFileService.MAX_RETRIES) {
|
||||
Exception.log(`${operationName}: Max retries (${BaseAIFileService.MAX_RETRIES}) exhausted. Returning default value.`);
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
const delay = BaseAIFileService.RETRY_DELAYS[attempt];
|
||||
Exception.warn(`${operationName}: Attempt ${attempt}/${BaseAIFileService.MAX_RETRIES} failed with ${error.info.type} (status ${error.info.statusCode}). Retrying in ${delay}ms...`);
|
||||
|
||||
await sleep(delay);
|
||||
} else {
|
||||
// Non-retryable error - return default value
|
||||
Exception.log(`${operationName}: Non-retryable error. Returning default value.`);
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
return defaultValue;
|
||||
});
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
protected createBoundary(): string {
|
||||
|
|
|
|||
|
|
@ -194,28 +194,17 @@ export class Claude extends BaseAIClass {
|
|||
|
||||
// Add binary file attachments if present
|
||||
if (content.attachments && content.attachments.length > 0) {
|
||||
// Upload all attachments and track failures
|
||||
const failedUploads: string[] = [];
|
||||
const { formattedParts, errorMessage } = await this.processAttachments<ContentBlockParam>(
|
||||
content.attachments,
|
||||
(attachments) => this.formatBinaryFiles(attachments)
|
||||
);
|
||||
|
||||
for (const attachment of content.attachments) {
|
||||
try {
|
||||
await this.aiFileService.uploadFile(attachment);
|
||||
} catch (error) {
|
||||
Exception.log(`Failed to upload ${attachment.fileName}: ${Exception.messageFrom(error)}`);
|
||||
failedUploads.push(attachment.fileName);
|
||||
}
|
||||
}
|
||||
contentBlocks.push(...formattedParts);
|
||||
|
||||
// Format successfully uploaded files
|
||||
const formattedContent = this.formatBinaryFiles(content.attachments);
|
||||
const rawContent = JSON.parse(formattedContent) as ContentBlockParam[];
|
||||
contentBlocks.push(...rawContent);
|
||||
|
||||
// Add error messages for failed uploads
|
||||
if (failedUploads.length > 0) {
|
||||
if (errorMessage) {
|
||||
contentBlocks.push({
|
||||
type: "text",
|
||||
text: `[Upload failed for: ${failedUploads.join(', ')}.]`
|
||||
text: errorMessage
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import type { IAIFunctionDefinition } from "AIClasses/FunctionDefinitions/IAIFun
|
|||
import type { ConversationContent } from "Conversations/ConversationContent";
|
||||
import type { Candidate, Part, FunctionDeclaration } from "@google/genai";
|
||||
import { FinishReason } from "@google/genai";
|
||||
import { Exception } from "Helpers/Exception";
|
||||
|
||||
export class Gemini extends BaseAIClass {
|
||||
|
||||
|
|
@ -200,27 +199,16 @@ export class Gemini extends BaseAIClass {
|
|||
|
||||
// Add binary file attachments if present
|
||||
if (content.attachments && content.attachments.length > 0) {
|
||||
// Upload all attachments and track failures
|
||||
const failedUploads: string[] = [];
|
||||
const { formattedParts, errorMessage } = await this.processAttachments<Part>(
|
||||
content.attachments,
|
||||
(attachments) => this.formatBinaryFiles(attachments)
|
||||
);
|
||||
|
||||
for (const attachment of content.attachments) {
|
||||
try {
|
||||
await this.aiFileService.uploadFile(attachment);
|
||||
} catch (error) {
|
||||
Exception.log(`Failed to upload ${attachment.fileName}: ${Exception.messageFrom(error)}`);
|
||||
failedUploads.push(attachment.fileName);
|
||||
}
|
||||
}
|
||||
parts.push(...formattedParts);
|
||||
|
||||
// Format successfully uploaded files
|
||||
const formattedContent = this.formatBinaryFiles(content.attachments);
|
||||
const rawContent = JSON.parse(formattedContent) as Part[];
|
||||
parts.push(...rawContent);
|
||||
|
||||
// Add error messages for failed uploads
|
||||
if (failedUploads.length > 0) {
|
||||
if (errorMessage) {
|
||||
parts.push({
|
||||
text: `[Upload failed for: ${failedUploads.join(', ')}.]`
|
||||
text: errorMessage
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -237,29 +237,18 @@ export class OpenAI extends BaseAIClass {
|
|||
|
||||
// Case 2: Binary file attachments
|
||||
if (content.attachments && content.attachments.length > 0) {
|
||||
// Upload all attachments and track failures
|
||||
const failedUploads: string[] = [];
|
||||
const { formattedParts, errorMessage } = await this.processAttachments<ResponsesAPIInput>(
|
||||
content.attachments,
|
||||
(attachments) => this.formatBinaryFiles(attachments)
|
||||
);
|
||||
|
||||
for (const attachment of content.attachments) {
|
||||
try {
|
||||
await this.aiFileService.uploadFile(attachment);
|
||||
} catch (error) {
|
||||
Exception.log(`Failed to upload ${attachment.fileName}: ${Exception.messageFrom(error)}`);
|
||||
failedUploads.push(attachment.fileName);
|
||||
}
|
||||
}
|
||||
results.push(...formattedParts);
|
||||
|
||||
// Format successfully uploaded files
|
||||
const formattedContent = this.formatBinaryFiles(content.attachments);
|
||||
const rawContent = JSON.parse(formattedContent) as ResponsesAPIInput[];
|
||||
results.push(...rawContent);
|
||||
|
||||
// Add error messages for failed uploads
|
||||
if (failedUploads.length > 0) {
|
||||
if (errorMessage) {
|
||||
// OpenAI formatBinaryFiles returns array with role wrapper, so add as separate message
|
||||
results.push({
|
||||
role: "user",
|
||||
content: `[Upload failed for: ${failedUploads.join(', ')}.]`
|
||||
content: errorMessage
|
||||
});
|
||||
}
|
||||
continue;
|
||||
|
|
|
|||
Loading…
Reference in a new issue