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(),