Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 31 additions & 5 deletions api/src/slices/bridle/domain/attachment.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,8 +237,11 @@ describe('BridleAttachmentService — text extraction', () => {

expect(out.attachments[0].kind).toBe(BridleAttachmentKinds.Binary);
expect(out.attachments[0].readableByAgent).toBe(false);
expect(out.text).toBe('read this');
expect(out.text).not.toContain('Attached file');
// Downgraded to binary: the model gets the unreadable-file notice, and
// none of the undecodable bytes reach the prompt as mojibake.
expect(out.text).toContain('read this');
expect(out.text).toContain('not readable');
expect(out.text).not.toContain('�');
});

it('downgrades a text-typed file containing NUL bytes', async () => {
Expand All @@ -253,7 +256,9 @@ describe('BridleAttachmentService — text extraction', () => {
const out = await service.expand(AGENT, 'read this', ['t1']);

expect(out.attachments[0].kind).toBe(BridleAttachmentKinds.Binary);
expect(out.text).toBe('read this');
expect(out.text).toContain('read this');
expect(out.text).toContain('not readable');
expect(out.text).not.toContain('ab');
});

it('preserves non-ASCII text intact', async () => {
Expand Down Expand Up @@ -302,7 +307,7 @@ describe('BridleAttachmentService — expansion to parts', () => {
expect(out.text).toBe('what is this?');
});

it('turns a binary into a file part and inlines nothing', async () => {
it('turns a binary into a file part and inlines an unreadable-file notice', async () => {
const { service, gw } = makeService();
gw.seed('b1', {
name: 'report.pdf',
Expand All @@ -318,7 +323,28 @@ describe('BridleAttachmentService — expansion to parts', () => {
name: 'report.pdf',
mimeType: 'application/pdf',
});
expect(out.text).toBe('read it');
// The runtime drops file parts before the model call, so without this
// notice the model would never learn the file exists at all.
expect(out.text).toContain('read it');
expect(out.text).toContain('report.pdf');
expect(out.text).toContain('not readable');
// Never the contents — only the reference.
expect(out.text).not.toContain('%PDF-1.7');
});

it('gives an attachment-only binary message a visible text body', async () => {
const { service, gw } = makeService();
gw.seed('b1', {
name: 'haha.xlsx',
mimeType:
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
size: 2,
body: Buffer.from('PK'),
});

const out = await service.expand(AGENT, '', ['b1']);

expect(out.text).toContain('haha.xlsx');
});

it('emits a file part AND inlined text for a text attachment', async () => {
Expand Down
20 changes: 20 additions & 0 deletions api/src/slices/bridle/domain/attachment.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,12 @@ export class BridleAttachmentService {
});
if (kind === BridleAttachmentKinds.Text) {
textBlocks.push(this.inlineTextBlock(stored));
} else {
// The runtime drops file parts before the model call, so without
// this line the model never learns the file exists — it would deny
// seeing an attachment the person is looking right at. The notice
// names the file and its limits; the contents stay unread.
textBlocks.push(BridleAttachmentService.binaryNoticeBlock(stored));
}
}

Expand Down Expand Up @@ -210,6 +216,20 @@ export class BridleAttachmentService {
return `[Attached file: ${stored.name}]\n${fence}\n${body}${notice}\n${fence}`;
}

/**
* What the model is told about a binary attachment: name, type, size —
* and that the contents are out of reach, so it answers honestly instead
* of denying the file exists.
*/
static binaryNoticeBlock(stored: IBridleStoredAttachment): string {
return (
`[Attached file: ${stored.name} ` +
`(${stored.mimeType}, ${stored.size.toLocaleString('en-US')} bytes). ` +
`Its contents are not readable in this chat — it is delivered as a ` +
`named reference only.]`
);
}

/** Path of the authenticated download route — never an S3 URL. */
static urlFor(agentId: string, attachmentId: string): string {
return `/api/agent/${encodeURIComponent(agentId)}/attachment/${attachmentId}`;
Expand Down
Loading