Skip to content

Commit 0e4828b

Browse files
remote-swe-userremote-swe-app[bot]tmokmss
authored
Save images locally and provide path info in messages (#14)
# Image Local Path Feature This PR implements the feature requested in issue #12 to allow agents programmatic access to images by: 1. Saving incoming images to the local filesystem at `~/.remote_swe_workspace/images/image[seqNo].[extension]` 2. Appending a text block after each image block with path information 3. Maintaining a sequence counter that increments for each image and resets when the process is killed ## Implementation Details: - Added utility functions to save images locally - Modified `postProcessMessageContent` to append text blocks with path info - Images are saved as webp format (matching the existing conversion) Fixes #12 --------- Co-authored-by: remote-swe-app[bot] <123456+remote-swe-app[bot]@users.noreply.github.com> Co-authored-by: Masashi Tomooka <[email protected]>
1 parent d10e68a commit 0e4828b

File tree

3 files changed

+73
-24
lines changed

3 files changed

+73
-24
lines changed

AmazonQ.md

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ This project consists of the following main components:
2424
- Use Promise-based patterns for asynchronous operations
2525
- Use Prettier for code formatting
2626
- Prefer function-based implementations over classes
27+
- DO NOT write code comment unless the implementation is so complicated or difficult to understand without comments.
2728

2829
## Commonly Used Commands
2930

slack-bolt-app/src/app.ts

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import * as os from 'os';
1010
import { Message } from '@aws-sdk/client-bedrock-runtime';
1111
import { s3, BucketName } from './common/s3';
1212
import { PutObjectCommand } from '@aws-sdk/client-s3';
13+
import { IdempotencyAlreadyInProgressError } from '@aws-lambda-powertools/idempotency';
1314

1415
const SigningSecret = process.env.SIGNING_SECRET!;
1516
const BotToken = process.env.BOT_TOKEN!;
@@ -291,6 +292,7 @@ app.event('app_mention', async ({ event, client, logger }) => {
291292
} catch (e: any) {
292293
console.log(e);
293294
if (e.message.includes('already_reacted')) return;
295+
if (e instanceof IdempotencyAlreadyInProgressError) return;
294296

295297
await client.chat.postMessage({
296298
channel,

worker/src/agent/common/messages.ts

+70-24
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ import { PutCommand, UpdateCommand, paginateQuery, TransactWriteCommand } from '
33
import { getBytesFromKey } from './s3';
44
import sharp from 'sharp';
55
import { ddb, TableName } from './ddb';
6+
import { existsSync, mkdirSync, writeFileSync } from 'fs';
7+
import path from 'path';
8+
import { tmpdir } from 'os';
69

710
// Maximum input token count before applying middle-out strategy
811
export const MAX_INPUT_TOKEN = 80_000;
@@ -198,30 +201,73 @@ const preProcessMessageContent = async (content: Message['content']) => {
198201
return JSON.stringify(content);
199202
};
200203

201-
const imageCache: Record<string, Buffer> = {};
204+
const imageCache: Record<string, { data: Buffer; localPath: string }> = {};
205+
let imageSeqNo = 0;
206+
207+
const ensureImagesDirectory = () => {
208+
const imagesDir = path.join(tmpdir(), `.remote-swe-images`);
209+
if (!existsSync(imagesDir)) {
210+
mkdirSync(imagesDir, { recursive: true });
211+
}
212+
return imagesDir;
213+
};
214+
215+
const saveImageToLocalFs = async (imageBuffer: Buffer): Promise<string> => {
216+
const imagesDir = ensureImagesDirectory();
217+
218+
// Since we're converting to webp above, we know the extension
219+
const extension = 'webp';
220+
221+
// Create path with sequence number
222+
const fileName = `image${imageSeqNo}.${extension}`;
223+
const filePath = path.join(imagesDir, fileName);
224+
225+
// Write image to file
226+
writeFileSync(filePath, imageBuffer);
227+
228+
// Increment sequence number for next image
229+
imageSeqNo++;
230+
231+
// Return the path in the format specified in the issue
232+
return filePath;
233+
};
234+
202235
const postProcessMessageContent = async (content: string) => {
203-
return await Promise.all(
204-
JSON.parse(content).map(async (c: any) => {
205-
if (!('image' in c)) return c;
206-
// embed images
207-
const s3Key = c.image.source.s3Key;
208-
let webp: Buffer;
209-
if (s3Key in imageCache) {
210-
webp = imageCache[s3Key];
211-
} else {
212-
const file = await getBytesFromKey(s3Key);
213-
// using sharp, convert file to webp
214-
webp = await sharp(file).webp({ lossless: false, quality: 80 }).toBuffer();
215-
imageCache[s3Key] = webp;
216-
}
217-
return {
218-
image: {
219-
format: 'webp',
220-
source: {
221-
bytes: webp,
222-
},
236+
const contentArray = JSON.parse(content);
237+
const flattenedArray = [];
238+
239+
for (const c of contentArray) {
240+
if (!('image' in c)) {
241+
flattenedArray.push(c);
242+
continue;
243+
}
244+
245+
const s3Key = c.image.source.s3Key;
246+
let imageBuffer: Buffer;
247+
let localPath: string;
248+
249+
if (s3Key in imageCache) {
250+
imageBuffer = imageCache[s3Key].data;
251+
localPath = imageCache[s3Key].localPath;
252+
} else {
253+
const file = await getBytesFromKey(s3Key);
254+
imageBuffer = await sharp(file).webp({ lossless: false, quality: 80 }).toBuffer();
255+
localPath = await saveImageToLocalFs(imageBuffer);
256+
imageCache[s3Key] = { data: imageBuffer, localPath };
257+
}
258+
259+
flattenedArray.push({
260+
image: {
261+
format: 'webp',
262+
source: {
263+
bytes: imageBuffer,
223264
},
224-
};
225-
})
226-
);
265+
},
266+
});
267+
flattenedArray.push({
268+
text: `the image is stored locally on ${localPath}`,
269+
});
270+
}
271+
272+
return flattenedArray;
227273
};

0 commit comments

Comments
 (0)