fix: load supported document attachments (#205)
This commit is contained in:
@@ -2,6 +2,7 @@ import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import { Attachment } from 'discord.js';
|
||||
import { isModelDocumentPath } from 'ejclaw-runners-shared';
|
||||
|
||||
import { DATA_DIR } from '../config.js';
|
||||
import { logger } from '../logger.js';
|
||||
@@ -63,6 +64,15 @@ function isTextAttachment(att: Attachment, contentType: string): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
function isStructuredDocumentAttachment(
|
||||
att: Attachment,
|
||||
contentType: string,
|
||||
): boolean {
|
||||
if (contentType === 'application/pdf') return true;
|
||||
if (contentType.startsWith('text/')) return true;
|
||||
return isModelDocumentPath(att.name || '');
|
||||
}
|
||||
|
||||
function formatAttachmentReference(
|
||||
label: 'Audio' | 'File' | 'Image' | 'Video',
|
||||
att: Attachment,
|
||||
@@ -121,6 +131,9 @@ export async function describeDownloadedAttachment(
|
||||
attachmentDefaultExtension(contentType),
|
||||
);
|
||||
const reference = formatAttachmentReference(label, att, filePath);
|
||||
if (label === 'File' && isStructuredDocumentAttachment(att, contentType)) {
|
||||
if (!isTextAttachment(att, contentType)) return reference;
|
||||
}
|
||||
if (!isTextAttachment(att, contentType)) {
|
||||
if (label === 'Image') return reference;
|
||||
return formatUnavailableAttachmentReference(
|
||||
|
||||
@@ -681,7 +681,7 @@ describe('attachments', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('stores file attachment with a visible unsupported marker', async () => {
|
||||
it('stores PDF file attachment as a structured document reference', async () => {
|
||||
const opts = createTestOpts();
|
||||
const channel = new DiscordChannel('test-token', opts);
|
||||
await channel.connect();
|
||||
@@ -707,9 +707,7 @@ describe('attachments', () => {
|
||||
expect(opts.onMessage).toHaveBeenCalledWith(
|
||||
'dc:1234567890123456',
|
||||
expect.objectContaining({
|
||||
content: expect.stringMatching(
|
||||
/^\[File unavailable: report\.pdf → .+\.pdf — application\/pdf is not loaded as structured model input\]$/,
|
||||
),
|
||||
content: expect.stringMatching(/^\[File: report\.pdf → .+\.pdf\]$/),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -170,7 +170,7 @@ describe('message-runtime-prompts output-only context', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('makes non-image turn attachments visibly unavailable instead of implying inspection', () => {
|
||||
it('carries supported document turn attachments into reviewer prompts as file inputs', () => {
|
||||
const prompt = buildReviewerPendingPrompt({
|
||||
chatJid: 'group@test',
|
||||
timezone: 'UTC',
|
||||
@@ -192,9 +192,7 @@ describe('message-runtime-prompts output-only context', () => {
|
||||
taskCreatedAt: '2026-04-20T01:00:00.000Z',
|
||||
});
|
||||
|
||||
expect(prompt).toContain(
|
||||
'[Attachment unavailable: report.pdf → /tmp/report.pdf — application/pdf is not loaded as structured model input]',
|
||||
);
|
||||
expect(prompt).toContain('[File: report.pdf → /tmp/report.pdf]');
|
||||
});
|
||||
|
||||
it('includes current task user scope in reviewer pending prompts without pulling older human messages', () => {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { isModelDocumentPath } from 'ejclaw-runners-shared';
|
||||
|
||||
import { buildArbiterContextPrompt } from './arbiter-context.js';
|
||||
import { TASK_USER_CONTEXT_START_SKEW_MS } from './message-runtime-task-context.js';
|
||||
import { formatMessages } from './router.js';
|
||||
@@ -21,6 +23,12 @@ function isImageAttachment(attachment: OutboundAttachment): boolean {
|
||||
return /\.(?:png|jpe?g|gif|webp|bmp)$/i.test(attachment.path);
|
||||
}
|
||||
|
||||
function isDocumentAttachment(attachment: OutboundAttachment): boolean {
|
||||
const mime = attachment.mime?.toLowerCase();
|
||||
if (mime === 'application/pdf' || mime?.startsWith('text/')) return true;
|
||||
return isModelDocumentPath(attachment.path);
|
||||
}
|
||||
|
||||
function attachmentLabel(attachment: OutboundAttachment): string {
|
||||
return attachment.name?.trim() || attachment.path.split('/').at(-1) || 'file';
|
||||
}
|
||||
@@ -39,11 +47,15 @@ function formatTurnOutputAttachmentContext(
|
||||
if (!attachments?.length) return '';
|
||||
const lines = attachments.map((attachment) => {
|
||||
const label = attachmentLabel(attachment);
|
||||
return isImageAttachment(attachment)
|
||||
? `[Image: ${label} → ${attachment.path}]`
|
||||
: `[Attachment unavailable: ${label} → ${attachment.path} — ${nonImageAttachmentReason(
|
||||
attachment,
|
||||
)}]`;
|
||||
if (isImageAttachment(attachment)) {
|
||||
return `[Image: ${label} → ${attachment.path}]`;
|
||||
}
|
||||
if (isDocumentAttachment(attachment)) {
|
||||
return `[File: ${label} → ${attachment.path}]`;
|
||||
}
|
||||
return `[Attachment unavailable: ${label} → ${attachment.path} — ${nonImageAttachmentReason(
|
||||
attachment,
|
||||
)}]`;
|
||||
});
|
||||
return `\n\nAttached evidence from this turn:\n${lines.join('\n')}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user