mirror of
https://github.com/openai/codex.git
synced 2026-05-05 13:51:29 +03:00
Add FS abstraction and use in view_image (#14960)
Adds an environment crate and environment + file system abstraction. Environment is a combination of attributes and services specific to environment the agent is connected to: File system, process management, OS, default shell. The goal is to move most of agent logic that assumes environment to work through the environment abstraction.
This commit is contained in:
@@ -53,18 +53,13 @@ struct ImageCacheKey {
|
||||
static IMAGE_CACHE: LazyLock<BlockingLruCache<ImageCacheKey, EncodedImage>> =
|
||||
LazyLock::new(|| BlockingLruCache::new(NonZeroUsize::new(32).unwrap_or(NonZeroUsize::MIN)));
|
||||
|
||||
pub fn load_and_resize_to_fit(path: &Path) -> Result<EncodedImage, ImageProcessingError> {
|
||||
load_for_prompt(path, PromptImageMode::ResizeToFit)
|
||||
}
|
||||
|
||||
pub fn load_for_prompt(
|
||||
pub fn load_for_prompt_bytes(
|
||||
path: &Path,
|
||||
file_bytes: Vec<u8>,
|
||||
mode: PromptImageMode,
|
||||
) -> Result<EncodedImage, ImageProcessingError> {
|
||||
let path_buf = path.to_path_buf();
|
||||
|
||||
let file_bytes = read_file_bytes(path, &path_buf)?;
|
||||
|
||||
let key = ImageCacheKey {
|
||||
digest: sha1_digest(&file_bytes),
|
||||
mode,
|
||||
@@ -136,24 +131,6 @@ fn can_preserve_source_bytes(format: ImageFormat) -> bool {
|
||||
)
|
||||
}
|
||||
|
||||
fn read_file_bytes(path: &Path, path_for_error: &Path) -> Result<Vec<u8>, ImageProcessingError> {
|
||||
match tokio::runtime::Handle::try_current() {
|
||||
// If we're inside a Tokio runtime, avoid block_on (it panics on worker threads).
|
||||
// Use block_in_place and do a standard blocking read safely.
|
||||
Ok(_) => tokio::task::block_in_place(|| std::fs::read(path)).map_err(|source| {
|
||||
ImageProcessingError::Read {
|
||||
path: path_for_error.to_path_buf(),
|
||||
source,
|
||||
}
|
||||
}),
|
||||
// Outside a runtime, just read synchronously.
|
||||
Err(_) => std::fs::read(path).map_err(|source| ImageProcessingError::Read {
|
||||
path: path_for_error.to_path_buf(),
|
||||
source,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn encode_image(
|
||||
image: &DynamicImage,
|
||||
preferred_format: ImageFormat,
|
||||
@@ -223,11 +200,20 @@ fn format_to_mime(format: ImageFormat) -> String {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::io::Cursor;
|
||||
|
||||
use super::*;
|
||||
use image::GenericImageView;
|
||||
use image::ImageBuffer;
|
||||
use image::Rgba;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
fn image_bytes(image: &ImageBuffer<Rgba<u8>, Vec<u8>>, format: ImageFormat) -> Vec<u8> {
|
||||
let mut encoded = Cursor::new(Vec::new());
|
||||
DynamicImage::ImageRgba8(image.clone())
|
||||
.write_to(&mut encoded, format)
|
||||
.expect("encode image to bytes");
|
||||
encoded.into_inner()
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn returns_original_image_when_within_bounds() {
|
||||
@@ -235,14 +221,15 @@ mod tests {
|
||||
(ImageFormat::Png, "image/png"),
|
||||
(ImageFormat::WebP, "image/webp"),
|
||||
] {
|
||||
let temp_file = NamedTempFile::new().expect("temp file");
|
||||
let image = ImageBuffer::from_pixel(64, 32, Rgba([10u8, 20, 30, 255]));
|
||||
image
|
||||
.save_with_format(temp_file.path(), format)
|
||||
.expect("write image to temp file");
|
||||
let original_bytes = image_bytes(&image, format);
|
||||
|
||||
let original_bytes = std::fs::read(temp_file.path()).expect("read written image");
|
||||
let encoded = load_and_resize_to_fit(temp_file.path()).expect("process image");
|
||||
let encoded = load_for_prompt_bytes(
|
||||
Path::new("in-memory-image"),
|
||||
original_bytes.clone(),
|
||||
PromptImageMode::ResizeToFit,
|
||||
)
|
||||
.expect("process image");
|
||||
|
||||
assert_eq!(encoded.width, 64);
|
||||
assert_eq!(encoded.height, 32);
|
||||
@@ -257,13 +244,15 @@ mod tests {
|
||||
(ImageFormat::Png, "image/png"),
|
||||
(ImageFormat::WebP, "image/webp"),
|
||||
] {
|
||||
let temp_file = NamedTempFile::new().expect("temp file");
|
||||
let image = ImageBuffer::from_pixel(4096, 2048, Rgba([200u8, 10, 10, 255]));
|
||||
image
|
||||
.save_with_format(temp_file.path(), format)
|
||||
.expect("write image to temp file");
|
||||
let original_bytes = image_bytes(&image, format);
|
||||
|
||||
let processed = load_and_resize_to_fit(temp_file.path()).expect("process image");
|
||||
let processed = load_for_prompt_bytes(
|
||||
Path::new("in-memory-image"),
|
||||
original_bytes,
|
||||
PromptImageMode::ResizeToFit,
|
||||
)
|
||||
.expect("process image");
|
||||
|
||||
assert!(processed.width <= MAX_WIDTH);
|
||||
assert!(processed.height <= MAX_HEIGHT);
|
||||
@@ -281,15 +270,15 @@ mod tests {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn preserves_large_image_in_original_mode() {
|
||||
let temp_file = NamedTempFile::new().expect("temp file");
|
||||
let image = ImageBuffer::from_pixel(4096, 2048, Rgba([180u8, 30, 30, 255]));
|
||||
image
|
||||
.save_with_format(temp_file.path(), ImageFormat::Png)
|
||||
.expect("write png to temp file");
|
||||
let original_bytes = image_bytes(&image, ImageFormat::Png);
|
||||
|
||||
let original_bytes = std::fs::read(temp_file.path()).expect("read written image");
|
||||
let processed =
|
||||
load_for_prompt(temp_file.path(), PromptImageMode::Original).expect("process image");
|
||||
let processed = load_for_prompt_bytes(
|
||||
Path::new("in-memory-image"),
|
||||
original_bytes.clone(),
|
||||
PromptImageMode::Original,
|
||||
)
|
||||
.expect("process image");
|
||||
|
||||
assert_eq!(processed.width, 4096);
|
||||
assert_eq!(processed.height, 2048);
|
||||
@@ -299,10 +288,12 @@ mod tests {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn fails_cleanly_for_invalid_images() {
|
||||
let temp_file = NamedTempFile::new().expect("temp file");
|
||||
std::fs::write(temp_file.path(), b"not an image").expect("write bytes");
|
||||
|
||||
let err = load_and_resize_to_fit(temp_file.path()).expect_err("invalid image should fail");
|
||||
let err = load_for_prompt_bytes(
|
||||
Path::new("in-memory-image"),
|
||||
b"not an image".to_vec(),
|
||||
PromptImageMode::ResizeToFit,
|
||||
)
|
||||
.expect_err("invalid image should fail");
|
||||
match err {
|
||||
ImageProcessingError::Decode { .. } => {}
|
||||
_ => panic!("unexpected error variant"),
|
||||
@@ -315,20 +306,25 @@ mod tests {
|
||||
IMAGE_CACHE.clear();
|
||||
}
|
||||
|
||||
let temp_file = NamedTempFile::new().expect("temp file");
|
||||
let first_image = ImageBuffer::from_pixel(32, 16, Rgba([20u8, 120, 220, 255]));
|
||||
first_image
|
||||
.save_with_format(temp_file.path(), ImageFormat::Png)
|
||||
.expect("write initial image");
|
||||
let first_bytes = image_bytes(&first_image, ImageFormat::Png);
|
||||
|
||||
let first = load_and_resize_to_fit(temp_file.path()).expect("process first image");
|
||||
let first = load_for_prompt_bytes(
|
||||
Path::new("in-memory-image"),
|
||||
first_bytes,
|
||||
PromptImageMode::ResizeToFit,
|
||||
)
|
||||
.expect("process first image");
|
||||
|
||||
let second_image = ImageBuffer::from_pixel(96, 48, Rgba([50u8, 60, 70, 255]));
|
||||
second_image
|
||||
.save_with_format(temp_file.path(), ImageFormat::Png)
|
||||
.expect("write updated image");
|
||||
let second_bytes = image_bytes(&second_image, ImageFormat::Png);
|
||||
|
||||
let second = load_and_resize_to_fit(temp_file.path()).expect("process updated image");
|
||||
let second = load_for_prompt_bytes(
|
||||
Path::new("in-memory-image"),
|
||||
second_bytes,
|
||||
PromptImageMode::ResizeToFit,
|
||||
)
|
||||
.expect("process updated image");
|
||||
|
||||
assert_eq!(first.width, 32);
|
||||
assert_eq!(first.height, 16);
|
||||
|
||||
Reference in New Issue
Block a user