refactor: centralize file attachment message using Copy enum

Replace hardcoded "The contents of the file" strings across all AI provider classes (Claude, Gemini, Mistral, OpenAI) with centralized Copy.AttachedFile enum value. Update SystemPrompt to document attached file behavior and clarify files are not in vault. Fix officeparser import to use named export and add esbuild plugin to handle browser bundle's IIFE format for mobile compatibility.
This commit is contained in:
Andrew Beal 2026-03-21 17:20:31 +00:00
parent fed0287ba0
commit 9a6bfdffa0
8 changed files with 49 additions and 13 deletions

View file

@ -16,6 +16,7 @@ import { MimeTypeToFileTypes } from "Enums/FileTypeMimeTypeMapping";
import { ApiError, ApiErrorType } from "Types/ApiError";
import { parseToolCall, parseFunctionResponse } from "Helpers/ResponseHelper";
import { AIToolUsageMode } from "Enums/AIToolUsageMode";
import { Copy, replaceCopy } from "Enums/Copy";
export class Claude extends BaseAIClass {
@ -308,7 +309,7 @@ export class Claude extends BaseAIClass {
}
return [
{type: "text", text: `The contents of the file '${attachment.fileName}' are provided below.` },
{type: "text", text: replaceCopy(Copy.AttachedFile, [attachment.fileName]) },
{
type: isPlainText || mimeType === MimeType.APPLICATION_PDF ? "document" : "image",
source: {

View file

@ -18,6 +18,7 @@ import { ApiError, ApiErrorType } from "Types/ApiError";
import { parseToolCall, parseFunctionResponse } from "Helpers/ResponseHelper";
import type { GeminiRetryInfo, GeminiErrorResponse } from "./GeminiTypes";
import { AIToolUsageMode } from "Enums/AIToolUsageMode";
import { Copy, replaceCopy } from "Enums/Copy";
export class Gemini extends BaseAIClass {
@ -348,7 +349,7 @@ export class Gemini extends BaseAIClass {
continue;
}
parts.push({ text: `The contents of the file '${attachment.fileName}' are provided below.` });
parts.push({ text: replaceCopy(Copy.AttachedFile, [attachment.fileName])});
parts.push({
fileData: {
mimeType: mimeType,

View file

@ -17,6 +17,7 @@ import { parseToolCall, parseFunctionResponse } from "Helpers/ResponseHelper";
import { AIToolUsageMode } from "Enums/AIToolUsageMode";
import type { MistralStreamChunk, MistralToolDefinition, MistralMessage, MistralContentPart } from "./MistralTypes";
import type { MistralFileService } from "./MistralFileService";
import { Copy, replaceCopy } from "Enums/Copy";
export class Mistral extends BaseAIClass {
@ -355,7 +356,7 @@ export class Mistral extends BaseAIClass {
const signedUrl = fileService.getSignedUrl(fileID);
if (signedUrl) {
contentParts.push(
{ type: "text", text: `The contents of the file '${attachment.fileName}' are provided below.` },
{ type: "text", text: replaceCopy(Copy.AttachedFile, [attachment.fileName]) },
{ type: "document_url", document_url: signedUrl }
);
} else {
@ -371,7 +372,7 @@ export class Mistral extends BaseAIClass {
const signedUrl = fileService.getSignedUrl(fileID);
if (signedUrl) {
contentParts.push(
{ type: "text", text: `The contents of the file '${attachment.fileName}' are provided below.` },
{ type: "text", text: replaceCopy(Copy.AttachedFile, [attachment.fileName]) },
{
type: "image_url",
image_url: signedUrl

View file

@ -15,6 +15,7 @@ import { isTextFile } from "Enums/FileType";
import { MimeTypeToFileTypes } from "Enums/FileTypeMimeTypeMapping";
import { parseToolCall, parseFunctionResponse } from "Helpers/ResponseHelper";
import { AIToolUsageMode } from "Enums/AIToolUsageMode";
import { Copy, replaceCopy } from "Enums/Copy";
export class OpenAI extends BaseAIClass {
@ -352,7 +353,7 @@ export class OpenAI extends BaseAIClass {
}
contentBlocks.push(
{ type: "input_text", text: `The contents of the file '${attachment.fileName}' are provided below.` },
{ type: "input_text", text: replaceCopy(Copy.AttachedFile, [attachment.fileName]) },
{
type: isPlainText || mimeType === MimeType.APPLICATION_PDF ? "input_file" : "input_image",
file_id: fileID

View file

@ -1,3 +1,5 @@
import { Copy } from "Enums/Copy";
export const SystemInstruction: string = `
# Obsidian AI Assistant
@ -18,7 +20,8 @@ You are a specialized AI assistant with direct access to the user's Obsidian vau
- Task verbs (create, generate, update, delete) Execute corresponding function
- Implied actions ("I need X") Call the function that produces X
- Outcome requests ("Show me Y") Use tools to retrieve/generate Y
- Image/PDF references Read the file first
- Image/PDF/Document references Read the file first
- Attached files Use the provided content directly
**Example:**
User: "Create a note about today's meeting with Sarah"
@ -255,6 +258,15 @@ When searches return or reference images or PDFs:
- Extract relevant information to answer the user's query
- Reference the source file with [[wiki-links]] as usual
### File Attachments
**Users can attach files directly to their messages.** When a file is attached, its content is provided inline in the conversation. Attached files are likely NOT present in the vault so use the attached content directly.
**How to recognize attached files:**
- A text block states: ${Copy.AttachedFile}
- The file content or file ID immediately follows that text block
- Attachments for document filetypes (.docx, .odt, .xlsx, etc.,) are included as plain text
---
## Obsidian Bases

View file

@ -101,6 +101,10 @@ export enum Copy {
ButtonTurnOffPlanningMode = "Turn off Planning Mode",
ButtonTurnOnPlanningMode = "Turn on Planning Mode",
// Agent file message
AttachedFile = `The user has attached the file {fileName}. The contents of the file are included below.
**Note that this is an attachment to the chat and the file is likely NOT present in the vault**`,
// Execution Plan Messages
PlanningFailedError = `Failed to generate plan. You should attempt to recover from this.
### Next Actions

View file

@ -1,7 +1,7 @@
import { extractText, getDocumentProxy } from 'unpdf';
import type { IPageText } from '../Types/SearchTypes';
import { Exception } from './Exception';
import OfficeParser from 'officeparser';
import { parseOffice } from 'officeparser';
// Handles PDF format
export async function readPDF(arrayBuffer: ArrayBuffer): Promise<IPageText[]> {
@ -34,7 +34,7 @@ export async function readPDF(arrayBuffer: ArrayBuffer): Promise<IPageText[]> {
// Handles document formats: DOCX, PPTX, XLSX, ODT, ODP, ODS
export async function readDocument(arrayBuffer: ArrayBuffer): Promise<IPageText[]> {
try {
const ast = await OfficeParser.parseOffice(arrayBuffer, {
const ast = await parseOffice(arrayBuffer, {
extractAttachments: true,
ocr: true,
ocrLanguage: "eng+esp"

View file

@ -102,13 +102,29 @@ const cssMergerPlugin = {
}
};
const buildOptions = {
// officeparser's default entry point requires Node's "fs" module, which crashes the plugin on
// Obsidian mobile. The browser bundle provides identical functionality using web APIs instead.
alias: {
'officeparser': './node_modules/officeparser/dist/officeparser.browser.js',
// officeparser's browser bundle is an IIFE (var officeParser = (()=> { ... })()) that doesn't
// set module.exports, so esbuild can't resolve its exports. This plugin intercepts the resolve
// and appends a CJS export line so imports work correctly.
const officeParserPlugin = {
name: "officeparser-cjs-shim",
setup(build) {
build.onResolve({ filter: /^officeparser$/ }, () => ({
path: join(process.cwd(), 'node_modules', 'officeparser', 'dist', 'officeparser.browser.js'),
namespace: 'officeparser-shim',
}));
build.onLoad({ filter: /.*/, namespace: 'officeparser-shim' }, async (args) => {
const contents = readFileSync(args.path, 'utf-8');
return {
contents: contents + '\nmodule.exports = officeParser;\n',
loader: 'js',
};
});
},
};
const buildOptions = {
plugins: [
officeParserPlugin,
esbuildSvelte({
compilerOptions: { css: "injected" },
preprocess: sveltePreprocess(),