Skip to content

Commit 677216a

Browse files
committed
copilot propositions
1 parent 7fe714c commit 677216a

4 files changed

Lines changed: 87 additions & 49 deletions

File tree

src/ai_agent/core/model_config.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
log = logging.getLogger("core.model_config")
99

10+
_available_models_cache: Optional[Dict[str, Dict[str, Optional[str]]]] = None
11+
1012

1113
def get_agent_model() -> Dict[str, Optional[str]]:
1214
"""
@@ -33,6 +35,10 @@ def get_available_models() -> Dict[str, Dict[str, Optional[str]]]:
3335
Dictionary mapping display_name -> model config
3436
Returns minimal default if config not available.
3537
"""
38+
global _available_models_cache
39+
if _available_models_cache is not None:
40+
return _available_models_cache
41+
3642
available_models = get_available_models_config()
3743

3844
model_configs = {}
@@ -59,6 +65,7 @@ def get_available_models() -> Dict[str, Dict[str, Optional[str]]]:
5965
}
6066

6167
log.info(f"Loaded {len(model_configs)} models from config: {list(model_configs.keys())}")
68+
_available_models_cache = model_configs
6269
return model_configs
6370

6471

src/ai_agent/ui/components.py

Lines changed: 44 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -299,12 +299,15 @@ def handle_chat(
299299
), gr.update(), gr.update(interactive=False)
300300

301301
# ------------------------------------------------------------------
302-
# Resize uploaded image files (max 500×500 px, aspect ratio kept).
303-
# The resized paths are used for BOTH the preview and the backend.
302+
# Resize uploaded image files for preview only (max 500×500 px).
303+
# Original paths are kept for backend format detection.
304304
# Non-image files (DICOM, NIfTI, CSV, …) are passed through as-is.
305305
# ------------------------------------------------------------------
306+
original_paths: List[str] = []
307+
resized_paths: List[str] = []
308+
resize_temps: List[str] = []
309+
306310
if files:
307-
resized_files: List[str] = []
308311
for f in files:
309312
if isinstance(f, str):
310313
raw_path = f
@@ -315,41 +318,45 @@ def handle_chat(
315318
else:
316319
raw_path = str(f)
317320
if raw_path:
318-
resized_files.append(resize_uploaded_image(raw_path))
319-
files = resized_files # replace with resized versions for all downstream use
321+
original_paths.append(raw_path)
322+
resized = resize_uploaded_image(raw_path)
323+
resized_paths.append(resized)
324+
if resized != raw_path:
325+
resize_temps.append(resized)
320326

321327
# If files were uploaded, build and show preview immediately
322-
if files:
323-
file_paths = []
324-
for f in files:
325-
if isinstance(f, str):
326-
file_paths.append(f)
327-
elif hasattr(f, "name"):
328-
file_paths.append(f.name)
329-
330-
if file_paths:
331-
# Build preview
332-
try:
333-
preview_path, meta_text = _build_preview_for_vlm(file_paths)
334-
if preview_path:
335-
# Show preview message
336-
preview_text = "📋 **Preview for analysis:**"
337-
if meta_text:
338-
preview_text += f"\n\n_{meta_text}_"
339-
history.append(
340-
{"role": "assistant", "content": preview_text}
341-
)
342-
history.append(
343-
{
344-
"role": "assistant",
345-
"content": {"path": preview_path},
346-
}
347-
)
348-
yield history, state_dict, gr.update(), gr.update(), gr.update(), gr.update(), None, gr.update(
349-
visible=False
350-
), gr.update(), gr.update()
351-
except Exception as e:
352-
log.warning("Preview generation failed: %r", e)
328+
if original_paths:
329+
try:
330+
preview_path, meta_text = _build_preview_for_vlm(
331+
resized_paths, metadata_paths=original_paths
332+
)
333+
if preview_path:
334+
# Show preview message
335+
preview_text = "📋 **Preview for analysis:**"
336+
if meta_text:
337+
preview_text += f"\n\n_{meta_text}_"
338+
history.append(
339+
{"role": "assistant", "content": preview_text}
340+
)
341+
history.append(
342+
{
343+
"role": "assistant",
344+
"content": {"path": preview_path},
345+
}
346+
)
347+
yield history, state_dict, gr.update(), gr.update(), gr.update(), gr.update(), None, gr.update(
348+
visible=False
349+
), gr.update(), gr.update()
350+
except Exception as e:
351+
log.warning("Preview generation failed: %r", e)
352+
finally:
353+
# Resized temp files were only needed for the preview.
354+
for tmp in resize_temps:
355+
try:
356+
os.unlink(tmp)
357+
except Exception:
358+
pass
359+
resize_temps.clear()
353360

354361
# Show "thinking" indicator for agent processing
355362
thinking_msg = {"role": "assistant", "content": "🤔 Finding tools..."}
@@ -362,7 +369,7 @@ def handle_chat(
362369
try:
363370
reply, new_state = respond(
364371
message=message or "",
365-
files=files or [],
372+
files=original_paths or [],
366373
state_dict=state_dict,
367374
doc_index=doc_index,
368375
model=model,

src/ai_agent/utils/previews.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,18 @@
2727
".png", ".jpg", ".jpeg", ".webp", ".bmp", ".gif", ".tif", ".tiff"
2828
}
2929

30+
# Mapping from extension to PIL format name (preserves original format on resize).
31+
_PIL_FORMAT_MAP: dict[str, str] = {
32+
".png": "PNG",
33+
".jpg": "JPEG",
34+
".jpeg": "JPEG",
35+
".webp": "WEBP",
36+
".bmp": "BMP",
37+
".gif": "GIF",
38+
".tif": "TIFF",
39+
".tiff": "TIFF",
40+
}
41+
3042

3143
def resize_uploaded_image(
3244
path: str,
@@ -72,7 +84,11 @@ def resize_uploaded_image(
7284
elif ext == ".webp":
7385
suffix, fmt, save_kw = ".webp", "WEBP", {"quality": 85}
7486
else:
75-
suffix, fmt, save_kw = ".png", "PNG", {}
87+
# Preserve original format and extension so downstream format
88+
# detection (e.g. detect_ext_token) is not misled by a .png suffix.
89+
fmt = _PIL_FORMAT_MAP.get(ext, "PNG")
90+
suffix = ext if fmt != "PNG" else ".png"
91+
save_kw = {}
7692

7793
fd, tmp_path = tempfile.mkstemp(suffix=suffix)
7894
os.close(fd)
@@ -322,6 +338,7 @@ def pad_to_square(img: np.ndarray, target_size: int) -> np.ndarray:
322338

323339
def _build_preview_for_vlm(
324340
image_paths: Optional[List[str]],
341+
metadata_paths: Optional[List[str]] = None,
325342
) -> Tuple[Optional[str], Optional[str]]:
326343
"""
327344
Build an enhanced preview image optimized for VLM analysis.
@@ -332,6 +349,13 @@ def _build_preview_for_vlm(
332349
- 4D data: Extract representative 3D volume, then multi-view
333350
- Medical images: Ensure proper intensity windowing
334351
352+
Args:
353+
image_paths: Paths used to load and render the preview image.
354+
metadata_paths: Paths used for metadata extraction only. When
355+
provided, metadata reflects these files (e.g. the originals
356+
before resizing) while the rendered preview uses
357+
``image_paths``. Defaults to ``image_paths``.
358+
335359
Returns:
336360
(preview_path, metadata_text)
337361
"""
@@ -346,7 +370,7 @@ def _build_preview_for_vlm(
346370

347371
meta_text = None
348372
try:
349-
meta_text = summarize_image_metadata(image_paths)
373+
meta_text = summarize_image_metadata(metadata_paths or image_paths)
350374
except Exception:
351375
log.exception(
352376
"Image metadata summarization failed; continuing without metadata."

tests/test_previews_cache.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -136,8 +136,8 @@ def test_resize_uploaded_image_large_landscape(tmp_path: Path):
136136
result = previews.resize_uploaded_image(str(src))
137137

138138
assert result != str(src), "Should write to a new temp file"
139-
out_img = Image.open(result)
140-
assert out_img.size == (500, 375)
139+
with Image.open(result) as out_img:
140+
assert out_img.size == (500, 375)
141141

142142

143143
def test_resize_uploaded_image_large_portrait(tmp_path: Path):
@@ -148,8 +148,8 @@ def test_resize_uploaded_image_large_portrait(tmp_path: Path):
148148
result = previews.resize_uploaded_image(str(src))
149149

150150
assert result != str(src)
151-
out_img = Image.open(result)
152-
assert out_img.size == (375, 500)
151+
with Image.open(result) as out_img:
152+
assert out_img.size == (375, 500)
153153

154154

155155
def test_resize_uploaded_image_already_small_unchanged(tmp_path: Path):
@@ -192,9 +192,9 @@ def test_resize_uploaded_image_jpeg_no_transparency(tmp_path: Path):
192192
result = previews.resize_uploaded_image(str(src))
193193

194194
assert result != str(src)
195-
out_img = Image.open(result)
196-
assert out_img.mode == "RGB"
197-
assert out_img.size == (500, 375)
195+
with Image.open(result) as out_img:
196+
assert out_img.mode == "RGB"
197+
assert out_img.size == (500, 375)
198198

199199

200200
def test_resize_uploaded_image_custom_bounds(tmp_path: Path):
@@ -204,6 +204,6 @@ def test_resize_uploaded_image_custom_bounds(tmp_path: Path):
204204

205205
result = previews.resize_uploaded_image(str(src), max_width=200, max_height=200)
206206

207-
out_img = Image.open(result)
208-
# 1000×500 scaled to fit 200×200: width limited → 200×100
209-
assert out_img.size == (200, 100)
207+
with Image.open(result) as out_img:
208+
# 1000×500 scaled to fit 200×200: width limited → 200×100
209+
assert out_img.size == (200, 100)

0 commit comments

Comments
 (0)