diff --git a/studio/backend/core/data_recipe/jsonable.py b/studio/backend/core/data_recipe/jsonable.py index a6828b2274..b45a378028 100644 --- a/studio/backend/core/data_recipe/jsonable.py +++ b/studio/backend/core/data_recipe/jsonable.py @@ -2,9 +2,62 @@ from __future__ import annotations import base64 import io +from pathlib import Path from typing import Any +def _pil_to_preview_payload(image: Any) -> dict[str, Any]: + buffer = io.BytesIO() + image.convert("RGB").save(buffer, format="JPEG", quality=85) + return { + "type": "image", + "mime": "image/jpeg", + "width": image.width, + "height": image.height, + "data": base64.b64encode(buffer.getvalue()).decode("ascii"), + } + + +def _open_pil_image_from_bytes(raw_bytes: bytes): + from PIL import Image # type: ignore + + with Image.open(io.BytesIO(raw_bytes)) as image: + return image.copy() + + +def _to_pil_from_hf_image_dict(value: Any) -> Any | None: + if not isinstance(value, dict): + return None + + raw_bytes = value.get("bytes") + if isinstance(raw_bytes, (bytes, bytearray)) and len(raw_bytes) > 0: + try: + return _open_pil_image_from_bytes(bytes(raw_bytes)) + except (OSError, ValueError): + pass + if ( + isinstance(raw_bytes, list) + and len(raw_bytes) > 0 + and all(isinstance(item, int) and 0 <= item <= 255 for item in raw_bytes) + ): + try: + return _open_pil_image_from_bytes(bytes(raw_bytes)) + except (OSError, ValueError): + pass + + path_value = value.get("path") + if isinstance(path_value, str) and path_value.strip(): + try: + from PIL import Image # type: ignore + + with Image.open(Path(path_value)) as image: + return image.copy() + except (OSError, ValueError, TypeError): + return None + + return None + + def to_jsonable(value: Any) -> Any: """Convert numpy/pandas-ish values into plain JSON-safe values.""" try: @@ -39,17 +92,12 @@ def _to_preview_image_payload(value: Any) -> dict[str, Any] | None: return None if not isinstance(value, PILImage): - return None + hf_image = _to_pil_from_hf_image_dict(value) + if hf_image is None: + return None + value = hf_image - buffer = io.BytesIO() - value.convert("RGB").save(buffer, format="JPEG", quality=85) - return { - "type": "image", - "mime": "image/jpeg", - "width": value.width, - "height": value.height, - "data": base64.b64encode(buffer.getvalue()).decode("ascii"), - } + return _pil_to_preview_payload(value) def to_preview_jsonable(value: Any) -> Any: diff --git a/studio/frontend/src/features/recipe-studio/dialogs/llm/general-tab.tsx b/studio/frontend/src/features/recipe-studio/dialogs/llm/general-tab.tsx index 547e8745ce..c974944491 100644 --- a/studio/frontend/src/features/recipe-studio/dialogs/llm/general-tab.tsx +++ b/studio/frontend/src/features/recipe-studio/dialogs/llm/general-tab.tsx @@ -6,6 +6,11 @@ import { ComboboxItem, ComboboxList, } from "@/components/ui/combobox"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible"; import { Switch } from "@/components/ui/switch"; import { Select, @@ -15,7 +20,7 @@ import { SelectValue, } from "@/components/ui/select"; import { Textarea } from "@/components/ui/textarea"; -import { type ReactElement, type RefObject, useMemo } from "react"; +import { type ReactElement, type RefObject, useMemo, useState } from "react"; import { useRecipeStudioStore } from "../../stores/recipe-studio"; import { isLikelyImageValue } from "../../utils/image-preview"; import type { LlmConfig } from "../../types"; @@ -44,6 +49,15 @@ const CODE_LANG_OPTIONS = [ "sql:ansi", ]; +const TRACE_MODE_OPTIONS = ["none", "last_message", "all_messages"] as const; + +function normalizeTraceMode(value: string): LlmConfig["with_trace"] { + if (value === "last_message" || value === "all_messages") { + return value; + } + return "none"; +} + type LlmGeneralTabProps = { config: LlmConfig; modelConfigAliases: string[]; @@ -120,6 +134,9 @@ export function LlmGeneralTab({ }; const imageContextToggleId = `${config.id}-image-context-enabled`; const imageContextColumnId = `${config.id}-image-context-column`; + const traceModeId = `${config.id}-trace-mode`; + const reasoningToggleId = `${config.id}-reasoning-content`; + const [advancedOpen, setAdvancedOpen] = useState(false); return (