From 9a6bfdffa000bdc3294c807662e963fecea55cfe Mon Sep 17 00:00:00 2001 From: Andrew Beal Date: Sat, 21 Mar 2026 17:20:31 +0000 Subject: [PATCH] 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. --- AIClasses/Claude/Claude.ts | 3 ++- AIClasses/Gemini/Gemini.ts | 3 ++- AIClasses/Mistral/Mistral.ts | 5 +++-- AIClasses/OpenAI/OpenAI.ts | 3 ++- AIPrompts/SystemPrompt.ts | 14 +++++++++++++- Enums/Copy.ts | 4 ++++ Helpers/DocumentHelper.ts | 4 ++-- esbuild.config.mjs | 26 +++++++++++++++++++++----- 8 files changed, 49 insertions(+), 13 deletions(-) diff --git a/AIClasses/Claude/Claude.ts b/AIClasses/Claude/Claude.ts index b8c2279..5bf69a0 100644 --- a/AIClasses/Claude/Claude.ts +++ b/AIClasses/Claude/Claude.ts @@ -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: { diff --git a/AIClasses/Gemini/Gemini.ts b/AIClasses/Gemini/Gemini.ts index e870fd5..837866a 100644 --- a/AIClasses/Gemini/Gemini.ts +++ b/AIClasses/Gemini/Gemini.ts @@ -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, diff --git a/AIClasses/Mistral/Mistral.ts b/AIClasses/Mistral/Mistral.ts index b92bfa5..7e6bb07 100644 --- a/AIClasses/Mistral/Mistral.ts +++ b/AIClasses/Mistral/Mistral.ts @@ -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 diff --git a/AIClasses/OpenAI/OpenAI.ts b/AIClasses/OpenAI/OpenAI.ts index 96c4e52..3995bda 100644 --- a/AIClasses/OpenAI/OpenAI.ts +++ b/AIClasses/OpenAI/OpenAI.ts @@ -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 diff --git a/AIPrompts/SystemPrompt.ts b/AIPrompts/SystemPrompt.ts index 1260e79..ec308b8 100644 --- a/AIPrompts/SystemPrompt.ts +++ b/AIPrompts/SystemPrompt.ts @@ -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 diff --git a/Enums/Copy.ts b/Enums/Copy.ts index aa30329..2db40ea 100644 --- a/Enums/Copy.ts +++ b/Enums/Copy.ts @@ -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 diff --git a/Helpers/DocumentHelper.ts b/Helpers/DocumentHelper.ts index 0f8bc66..72198ca 100644 --- a/Helpers/DocumentHelper.ts +++ b/Helpers/DocumentHelper.ts @@ -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 { @@ -34,7 +34,7 @@ export async function readPDF(arrayBuffer: ArrayBuffer): Promise { // Handles document formats: DOCX, PPTX, XLSX, ODT, ODP, ODS export async function readDocument(arrayBuffer: ArrayBuffer): Promise { try { - const ast = await OfficeParser.parseOffice(arrayBuffer, { + const ast = await parseOffice(arrayBuffer, { extractAttachments: true, ocr: true, ocrLanguage: "eng+esp" diff --git a/esbuild.config.mjs b/esbuild.config.mjs index d7a437e..b3467e7 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -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(),