diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2861e8d..607c0d0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,4 +24,6 @@ jobs: - run: npm test + - run: python3 -m unittest skill/infographic-agent/test_portable_infographic.py + - run: npm run build diff --git a/.github/workflows/publish-skill.yml b/.github/workflows/publish-skill.yml index c5b6978..1142ff8 100644 --- a/.github/workflows/publish-skill.yml +++ b/.github/workflows/publish-skill.yml @@ -50,6 +50,7 @@ jobs: run: | node --check bin/infographic-agent.js python3 -m py_compile portable_infographic.py + python3 -m unittest test_portable_infographic.py npm pack --dry-run - name: Resolve version & publish state diff --git a/CHANGELOG.md b/CHANGELOG.md index f2e16da..26f2c37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [3.2.1] - 2026-07-15 + +### Fixed + +- **Portable Python SDK compatibility**: feature-detect `ImageConfig.image_size` and `ThinkingConfig.thinking_level` so generation works with `google-genai 1.47.0`, the latest release available to Python 3.9. Current SDKs still request the selected resolution and high thinking; 1.47 uses native image resolution and dynamic thinking instead of failing validation before the API call. The supported minimums are now explicit: Python 3.9 and `google-genai 1.47.0`. + ## [3.2.0] - 2026-07-15 ### Added diff --git a/skill/infographic-agent/README.md b/skill/infographic-agent/README.md index 2c1167c..f9e4ad9 100644 --- a/skill/infographic-agent/README.md +++ b/skill/infographic-agent/README.md @@ -24,7 +24,7 @@ npx infographic-agent "Top 5 programming languages in 2026" ## 📋 Prerequisites -- **Python 3.8+** +- **Python 3.9+** - **Gemini API Key**: Get a free key at [Google AI Studio](https://aistudio.google.com/apikey). Set it as: ```bash export GEMINI_API_KEY="your-key" diff --git a/skill/infographic-agent/SKILL.md b/skill/infographic-agent/SKILL.md index a6051fb..a3cf9ff 100644 --- a/skill/infographic-agent/SKILL.md +++ b/skill/infographic-agent/SKILL.md @@ -6,7 +6,7 @@ description: > then gemini-3.1-flash-lite-image renders it into a PNG. No browser, Playwright, or Chromium dependencies — the only requirement is Google's GenAI SDK. Fully portable to any agent CLI environment. metadata: - version: "3.2.0" + version: "3.2.1" author: "Infographic Agent contributors" --- diff --git a/skill/infographic-agent/bin/infographic-agent.js b/skill/infographic-agent/bin/infographic-agent.js index 95bf0ce..a43f0ac 100755 --- a/skill/infographic-agent/bin/infographic-agent.js +++ b/skill/infographic-agent/bin/infographic-agent.js @@ -51,7 +51,7 @@ function findPython() { if (match) { const major = parseInt(match[1], 10); const minor = parseInt(match[2], 10); - if (major >= 3 && minor >= 8) return candidate; + if (major >= 3 && minor >= 9) return candidate; } } } @@ -120,7 +120,7 @@ Examples: if (!python) { err( - "Python 3.8+ is required but was not found on your PATH.\n\n" + + "Python 3.9+ is required but was not found on your PATH.\n\n" + " Install Python from https://www.python.org/downloads/ and make sure\n" + " it is on your PATH, then re-run:\n\n" + " npx infographic-agent ..." @@ -132,7 +132,7 @@ if (!python) { if (doInstall) { print("Installing Python dependencies (google-genai, pillow)..."); - const result = run(python, ["-m", "pip", "install", "--upgrade", "google-genai", "pillow"]); + const result = run(python, ["-m", "pip", "install", "--upgrade", "google-genai>=1.47.0", "pillow"]); if (result.status !== 0) process.exit(result.status); print(` diff --git a/skill/infographic-agent/package.json b/skill/infographic-agent/package.json index 8a1fb50..3e5765e 100644 --- a/skill/infographic-agent/package.json +++ b/skill/infographic-agent/package.json @@ -1,6 +1,6 @@ { "name": "infographic-agent", - "version": "3.2.0", + "version": "3.2.1", "description": "Generate professional infographic PNGs from text with Gemini — a research agent (gemini-3.5-flash) prepares and validates the prompt, then gemini-3.1-flash-lite-image renders it. No browser/Playwright deps.", "license": "MIT", "author": "Ryan Baumann", diff --git a/skill/infographic-agent/portable_infographic.py b/skill/infographic-agent/portable_infographic.py index 85c1b3c..2b67230 100755 --- a/skill/infographic-agent/portable_infographic.py +++ b/skill/infographic-agent/portable_infographic.py @@ -637,11 +637,35 @@ def _normalize_to_png(data: bytes, mime: str): return data, m +def _config_fields(config_type): + """Return the declared fields across supported Pydantic SDK versions.""" + return getattr(config_type, "model_fields", None) or getattr(config_type, "__fields__", {}) + + +def _image_config(aspect: str, resolution: str): + """Request image size only when the installed SDK exposes that field.""" + fields = _config_fields(types.ImageConfig) + kwargs = {"aspect_ratio": aspect} + if "image_size" in fields: + kwargs["image_size"] = resolution + else: + warn("Installed google-genai does not expose image_size; using the model's native resolution.") + return types.ImageConfig(**kwargs) + + +def _thinking_config(): + """Use high thinking on current SDKs and dynamic thinking on older SDKs.""" + fields = _config_fields(types.ThinkingConfig) + if "thinking_level" in fields: + return types.ThinkingConfig(thinking_level="HIGH", include_thoughts=True) + return types.ThinkingConfig(thinking_budget=-1, include_thoughts=True) + + def generate_image(client, prompt: str, aspect: str, image_model: str, resolution: str): config = types.GenerateContentConfig( response_modalities=["TEXT", "IMAGE"], - image_config=types.ImageConfig(aspect_ratio=aspect, image_size=resolution), - thinking_config=types.ThinkingConfig(thinking_level="HIGH", include_thoughts=True), + image_config=_image_config(aspect, resolution), + thinking_config=_thinking_config(), http_options=types.HttpOptions(timeout=180_000), ) info(f"🎨 Generating the infographic ({image_model}) at {resolution}...") @@ -656,8 +680,8 @@ def generate_image(client, prompt: str, aspect: str, image_model: str, resolutio def refine_image(client, image_bytes: bytes, mime: str, instruction: str, aspect: str, image_model: str, resolution: str): config = types.GenerateContentConfig( response_modalities=["TEXT", "IMAGE"], - image_config=types.ImageConfig(aspect_ratio=aspect, image_size=resolution), - thinking_config=types.ThinkingConfig(thinking_level="HIGH", include_thoughts=True), + image_config=_image_config(aspect, resolution), + thinking_config=_thinking_config(), http_options=types.HttpOptions(timeout=180_000), ) contents = [ diff --git a/skill/infographic-agent/requirements.txt b/skill/infographic-agent/requirements.txt index bb94f99..704b837 100644 --- a/skill/infographic-agent/requirements.txt +++ b/skill/infographic-agent/requirements.txt @@ -1,4 +1,4 @@ # Runtime dependencies. Install with: pip install -r requirements.txt # (or: pip install google-genai pillow) -google-genai>=1.0.0 # Gemini SDK — the two-agent research + image pipeline +google-genai>=1.47.0 # Gemini SDK — includes the typed image/thinking config surface tested here pillow>=10.0.0 # transcodes the model's output to lossless PNG (crisp text) diff --git a/skill/infographic-agent/test_portable_infographic.py b/skill/infographic-agent/test_portable_infographic.py new file mode 100644 index 0000000..464ccb9 --- /dev/null +++ b/skill/infographic-agent/test_portable_infographic.py @@ -0,0 +1,62 @@ +import importlib.util +import unittest +from pathlib import Path +from types import SimpleNamespace +from unittest.mock import patch + + +SCRIPT_PATH = Path(__file__).with_name("portable_infographic.py") +SPEC = importlib.util.spec_from_file_location("portable_infographic", SCRIPT_PATH) +portable = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(portable) + + +def config_type(*field_names): + class Config: + model_fields = {name: object() for name in field_names} + + def __init__(self, **kwargs): + self.kwargs = kwargs + + return Config + + +class SDKConfigCompatibilityTest(unittest.TestCase): + def test_current_sdk_requests_resolution_and_high_thinking(self): + fake_types = SimpleNamespace( + ImageConfig=config_type("aspect_ratio", "image_size"), + ThinkingConfig=config_type("thinking_level", "include_thoughts"), + ) + + with patch.object(portable, "types", fake_types): + image = portable._image_config("16:9", "1K") + thinking = portable._thinking_config() + + self.assertEqual(image.kwargs, {"aspect_ratio": "16:9", "image_size": "1K"}) + self.assertEqual( + thinking.kwargs, + {"thinking_level": "HIGH", "include_thoughts": True}, + ) + + def test_sdk_1_47_falls_back_to_supported_fields(self): + fake_types = SimpleNamespace( + ImageConfig=config_type("aspect_ratio"), + ThinkingConfig=config_type("thinking_budget", "include_thoughts"), + ) + + with patch.object(portable, "types", fake_types), patch.object(portable, "warn") as warn: + image = portable._image_config("16:9", "1K") + thinking = portable._thinking_config() + + self.assertEqual(image.kwargs, {"aspect_ratio": "16:9"}) + self.assertEqual( + thinking.kwargs, + {"thinking_budget": -1, "include_thoughts": True}, + ) + warn.assert_called_once_with( + "Installed google-genai does not expose image_size; using the model's native resolution." + ) + + +if __name__ == "__main__": + unittest.main()