From cee5a35bd4de9dce151c66b699765594bccde3a1 Mon Sep 17 00:00:00 2001 From: JegernOUTT Date: Mon, 23 Mar 2026 14:59:34 +1030 Subject: [PATCH 1/6] feat(botwiz): add test run functionality for bots Implement complete bot test workflow: - Install bot dependencies and run install scripts - Support both script-based and manifest-based bots - Auto-create test persona in marketplace dev group - Frontend test run UI with progress tracking and persistence - "Talk to bot" and "Analyze by Bob" session navigation - Improved bot status detection and resource cleanup --- flexus_client_kit/ckit_ask_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flexus_client_kit/ckit_ask_model.py b/flexus_client_kit/ckit_ask_model.py index 1bf99c15..783befa0 100644 --- a/flexus_client_kit/ckit_ask_model.py +++ b/flexus_client_kit/ckit_ask_model.py @@ -188,7 +188,7 @@ async def bot_subchat_create_multiple( variable_values={ "who_is_asking": who_is_asking, "persona_id": persona_id, - "first_question": first_question, + "first_question": [json.dumps(q) for q in first_question], "first_calls": first_calls, "title": title, "fcall_id": fcall_id, From 62993d6996c9b21f6c6e413469baf575bee4032d Mon Sep 17 00:00:00 2001 From: JegernOUTT Date: Mon, 23 Mar 2026 19:34:41 +1030 Subject: [PATCH 2/6] feat(botwiz): add marketplace publishing and avatar generation tools Add comprehensive bot publishing workflow to marketplace with GitHub auth and Docker image building. Introduce avatar generation from style refs using xAI Grok Imagine. Key changes: - `publish_marketplace` tool with build/submit_to_review modes - `generate_avatar` tool with style bank seeding and idea-based generation - Backend GraphQL mutations: `botwiz_marketplace_action`, avatar RPCs - Frontend marketplace action menu and OAuth popup handling - Improved expert reuse by fexp_id and provenance tracking - Style bank manifest and default assets in flexus-client-kit --- .../avatar_from_idea_imagine.py | 168 +++++++++++++++++- .../bot_pictures/style_bank/manifest.json | 27 +++ setup.py | 1 + 3 files changed, 187 insertions(+), 9 deletions(-) create mode 100644 flexus_simple_bots/bot_pictures/style_bank/manifest.json diff --git a/flexus_simple_bots/avatar_from_idea_imagine.py b/flexus_simple_bots/avatar_from_idea_imagine.py index cd766ddf..7c281343 100644 --- a/flexus_simple_bots/avatar_from_idea_imagine.py +++ b/flexus_simple_bots/avatar_from_idea_imagine.py @@ -1,8 +1,158 @@ -import os, sys, asyncio, base64, io +import os, sys, asyncio, base64, io, json +from pathlib import Path from PIL import Image -import xai_sdk +try: + import xai_sdk +except ImportError: + xai_sdk = None -client = xai_sdk.Client() +_default_client = None + + +DEFAULT_MODEL = "grok-imagine-image" +DEFAULT_RESOLUTION = "1k" +_STYLE_BANK_MANIFEST = Path(__file__).parent / "bot_pictures" / "style_bank" / "manifest.json" + + +def create_xai_client(api_key: str | None = None): + if xai_sdk is None: + raise RuntimeError("xai-sdk package is required") + if api_key: + return xai_sdk.Client(api_key=api_key) + return xai_sdk.Client() + + +def _get_default_client(): + global _default_client + if _default_client is None: + _default_client = create_xai_client() + return _default_client + + +def _image_to_data_url(image_bytes: bytes, mime: str = "image/png") -> str: + return f"data:{mime};base64,{base64.b64encode(image_bytes).decode('utf-8')}" + + +def style_bank_manifest() -> list[dict]: + if not _STYLE_BANK_MANIFEST.exists(): + return [] + with open(_STYLE_BANK_MANIFEST, "r", encoding="utf-8") as f: + rows = json.load(f) + if not isinstance(rows, list): + raise ValueError(f"Bad style-bank manifest: {_STYLE_BANK_MANIFEST}") + return rows + + +def default_style_bank_files() -> dict[str, bytes]: + root = Path(__file__).parent + files = {} + for row in style_bank_manifest(): + rel = str(row.get("source_path", "")).strip() + target_name = str(row.get("target_name", "")).strip() + if not rel or not target_name: + continue + path = root / rel + if not path.exists(): + continue + files[target_name] = path.read_bytes() + return files + + +async def _sample_image( + xclient, + *, + prompt: str, + image_urls: list[str], + resolution: str = DEFAULT_RESOLUTION, +) -> bytes: + kwargs = { + "prompt": prompt, + "model": DEFAULT_MODEL, + "aspect_ratio": None, + "resolution": resolution, + "image_format": "base64", + } + image_urls = image_urls[:5] + if len(image_urls) == 1: + kwargs["image_url"] = image_urls[0] + else: + kwargs["image_urls"] = image_urls + + def _api_call(): + return xclient.image.sample(**kwargs) + + rsp = await asyncio.to_thread(_api_call) + return rsp.image + + +def _save_fullsize_webp_bytes(png_bytes: bytes, quality: int = 85) -> tuple[bytes, tuple[int, int]]: + out = io.BytesIO() + with Image.open(io.BytesIO(png_bytes)) as im: + im = make_transparent(im) + im.save(out, "WEBP", quality=quality, method=6) + size = im.size + return out.getvalue(), size + + +def _save_avatar_256_webp_bytes(avatar_png_bytes: bytes, quality: int = 85) -> tuple[bytes, tuple[int, int]]: + out = io.BytesIO() + with Image.open(io.BytesIO(avatar_png_bytes)) as im: + im = make_transparent(im) + s = min(im.size) + cx, cy = im.size[0] // 2, im.size[1] // 2 + im = im.crop((cx - s // 2, cy - s // 2, cx + s // 2, cy + s // 2)).resize((256, 256), Image.LANCZOS) + im.save(out, "WEBP", quality=quality, method=6) + size = im.size + return out.getvalue(), size + + +async def generate_avatar_assets_from_idea( + *, + input_image_bytes: bytes, + description: str, + style_reference_images: list[bytes], + api_key: str, + count: int = 5, +) -> list[dict]: + if not description or not description.strip(): + raise ValueError("description is required") + if count < 1 or count > 10: + raise ValueError("count must be in range [1, 10]") + + xclient = create_xai_client(api_key) + refs = [_image_to_data_url(input_image_bytes)] + refs += [_image_to_data_url(x) for x in style_reference_images] + refs = refs[:5] + + fullsize_prompt = ( + f"{description.strip()}. " + "Create a full-size variation of the character on pure solid bright green background (#00FF00)." + ) + avatar_prompt = ( + f"{description.strip()}. " + "Make avatar suitable for small pictures, face much bigger exactly in the center, " + "use a pure solid bright green background (#00FF00)." + ) + + async def _one(i: int): + fullsize_png = await _sample_image(xclient, prompt=fullsize_prompt, image_urls=refs) + fullsize_webp, fullsize_size = _save_fullsize_webp_bytes(fullsize_png) + + avatar_png = await _sample_image( + xclient, + prompt=avatar_prompt, + image_urls=[_image_to_data_url(fullsize_png)], + ) + avatar_webp_256, avatar_size_256 = _save_avatar_256_webp_bytes(avatar_png) + return { + "index": i, + "fullsize_webp": fullsize_webp, + "fullsize_size": fullsize_size, + "avatar_webp_256": avatar_webp_256, + "avatar_size_256": avatar_size_256, + } + + return await asyncio.gather(*[_one(i) for i in range(count)]) def make_transparent(im, fringe_fight: float = 0.30, defringe_radius: int = 4): @@ -64,12 +214,12 @@ async def make_fullsize_variations(input_path: str, base_name: str, out_dir: str async def generate_one(i): def api_call(): - return client.image.sample( + return _get_default_client().image.sample( prompt="Make variations of the charactor on solid bright green background (#00FF00).", - model="grok-imagine-image", + model=DEFAULT_MODEL, image_url=image_url, aspect_ratio=None, # does not work for image edit - resolution="1k", + resolution=DEFAULT_RESOLUTION, image_format="base64" ) rsp = await asyncio.to_thread(api_call) @@ -90,12 +240,12 @@ async def make_avatar(i: int, png_bytes: bytes, base_name: str, out_dir: str): image_url = f"data:image/png;base64,{base64.b64encode(png_bytes).decode('utf-8')}" def api_call(): - return client.image.sample( + return _get_default_client().image.sample( prompt="Make avatar suitable for small pictures, face much bigger exactly in the center, use a pure solid bright green background (#00FF00).", - model="grok-imagine-image", + model=DEFAULT_MODEL, image_url=image_url, aspect_ratio=None, # does not work for image edit - resolution="1k", + resolution=DEFAULT_RESOLUTION, image_format="base64" ) rsp = await asyncio.to_thread(api_call) diff --git a/flexus_simple_bots/bot_pictures/style_bank/manifest.json b/flexus_simple_bots/bot_pictures/style_bank/manifest.json new file mode 100644 index 00000000..775907e1 --- /dev/null +++ b/flexus_simple_bots/bot_pictures/style_bank/manifest.json @@ -0,0 +1,27 @@ +[ + { + "target_name": "frog.webp", + "source_path": "frog/frog-256x256.webp", + "label": "Cute mascot style" + }, + { + "target_name": "strategist.webp", + "source_path": "strategist/strategist-256x256.webp", + "label": "Professional portrait style" + }, + { + "target_name": "ad_monster.webp", + "source_path": "admonster/ad_monster-256x256.webp", + "label": "Playful monster style" + }, + { + "target_name": "karen.webp", + "source_path": "karen/karen-256x256.webp", + "label": "Clean assistant style" + }, + { + "target_name": "boss.webp", + "source_path": "boss/boss-256x256.webp", + "label": "Founder portrait style" + } +] diff --git a/setup.py b/setup.py index b18ba449..dde6751c 100644 --- a/setup.py +++ b/setup.py @@ -58,6 +58,7 @@ def run(self): "pandas", "playwright", "openai", + "xai-sdk", "mcp", "python-telegram-bot>=20.0", ], From d656a2d377d617f1dc470b9b56128c3791440776 Mon Sep 17 00:00:00 2001 From: JegernOUTT Date: Wed, 25 Mar 2026 22:28:25 +1030 Subject: [PATCH 3/6] feat(avatar): add rescaling to target size for fullsize WebP output Introduce _FULLSIZE_TARGET (1024x1536) and resize logic that: - Scales images proportionally to fit within target dimensions - Centers the scaled image on a transparent canvas - Applies to both WebP conversion and direct file saving Ensures consistent output dimensions while preserving aspect ratio. --- .../avatar_from_idea_imagine.py | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/flexus_simple_bots/avatar_from_idea_imagine.py b/flexus_simple_bots/avatar_from_idea_imagine.py index 7c281343..9d284155 100644 --- a/flexus_simple_bots/avatar_from_idea_imagine.py +++ b/flexus_simple_bots/avatar_from_idea_imagine.py @@ -85,10 +85,26 @@ def _api_call(): return rsp.image -def _save_fullsize_webp_bytes(png_bytes: bytes, quality: int = 85) -> tuple[bytes, tuple[int, int]]: - out = io.BytesIO() +_FULLSIZE_TARGET = (1024, 1536) + + +def _save_fullsize_webp_bytes( + png_bytes: bytes, + quality: int = 85, + target_size: tuple[int, int] = _FULLSIZE_TARGET, +) -> tuple[bytes, tuple[int, int]]: + tw, th = target_size with Image.open(io.BytesIO(png_bytes)) as im: im = make_transparent(im) + iw, ih = im.size + if (iw, ih) != (tw, th): + scale = min(tw / iw, th / ih) + new_w, new_h = int(iw * scale), int(ih * scale) + im = im.resize((new_w, new_h), Image.LANCZOS) + canvas = Image.new("RGBA", (tw, th), (0, 0, 0, 0)) + canvas.paste(im, ((tw - new_w) // 2, (th - new_h) // 2)) + im = canvas + out = io.BytesIO() im.save(out, "WEBP", quality=quality, method=6) size = im.size return out.getvalue(), size @@ -225,8 +241,17 @@ def api_call(): rsp = await asyncio.to_thread(api_call) png_bytes = rsp.image + tw, th = _FULLSIZE_TARGET with Image.open(io.BytesIO(png_bytes)) as im: im = make_transparent(im) + iw, ih = im.size + if (iw, ih) != (tw, th): + scale = min(tw / iw, th / ih) + new_w, new_h = int(iw * scale), int(ih * scale) + im = im.resize((new_w, new_h), Image.LANCZOS) + canvas = Image.new("RGBA", (tw, th), (0, 0, 0, 0)) + canvas.paste(im, ((tw - new_w) // 2, (th - new_h) // 2)) + im = canvas fn = os.path.join(out_dir, f"i{i:02d}-{base_name}-{im.size[0]}x{im.size[1]}.webp") im.save(fn, 'WEBP', quality=85, method=6) print(f"Saved {fn}") From 747d2f7641d9a7b78b34ef252de1b8cf8328e087 Mon Sep 17 00:00:00 2001 From: JegernOUTT Date: Wed, 25 Mar 2026 22:33:48 +1030 Subject: [PATCH 4/6] feat(avatar): add WebP encoding with automatic size optimization Introduce _encode_webp_within_limit() to progressively reduce quality from 100 down to 40 until image fits within 250KB limit, preventing upload failures due to oversized files. Replace direct WebP saves with this function in fullsize and 256px avatar generation. --- .../avatar_from_idea_imagine.py | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/flexus_simple_bots/avatar_from_idea_imagine.py b/flexus_simple_bots/avatar_from_idea_imagine.py index 9d284155..1d5c8072 100644 --- a/flexus_simple_bots/avatar_from_idea_imagine.py +++ b/flexus_simple_bots/avatar_from_idea_imagine.py @@ -86,11 +86,29 @@ def _api_call(): _FULLSIZE_TARGET = (1024, 1536) +_MAX_IMAGE_BYTES = 250_000 + + +def _encode_webp_within_limit( + im: Image.Image, + quality: int = 100, + max_bytes: int = _MAX_IMAGE_BYTES, + min_quality: int = 40, +) -> bytes: + for q in range(quality, min_quality - 1, -5): + out = io.BytesIO() + im.save(out, "WEBP", quality=q, method=6) + data = out.getvalue() + if len(data) <= max_bytes: + return data + out = io.BytesIO() + im.save(out, "WEBP", quality=min_quality, method=6) + return out.getvalue() def _save_fullsize_webp_bytes( png_bytes: bytes, - quality: int = 85, + quality: int = 100, target_size: tuple[int, int] = _FULLSIZE_TARGET, ) -> tuple[bytes, tuple[int, int]]: tw, th = target_size @@ -104,22 +122,20 @@ def _save_fullsize_webp_bytes( canvas = Image.new("RGBA", (tw, th), (0, 0, 0, 0)) canvas.paste(im, ((tw - new_w) // 2, (th - new_h) // 2)) im = canvas - out = io.BytesIO() - im.save(out, "WEBP", quality=quality, method=6) + data = _encode_webp_within_limit(im, quality) size = im.size - return out.getvalue(), size + return data, size -def _save_avatar_256_webp_bytes(avatar_png_bytes: bytes, quality: int = 85) -> tuple[bytes, tuple[int, int]]: - out = io.BytesIO() +def _save_avatar_256_webp_bytes(avatar_png_bytes: bytes, quality: int = 100) -> tuple[bytes, tuple[int, int]]: with Image.open(io.BytesIO(avatar_png_bytes)) as im: im = make_transparent(im) s = min(im.size) cx, cy = im.size[0] // 2, im.size[1] // 2 im = im.crop((cx - s // 2, cy - s // 2, cx + s // 2, cy + s // 2)).resize((256, 256), Image.LANCZOS) - im.save(out, "WEBP", quality=quality, method=6) + data = _encode_webp_within_limit(im, quality) size = im.size - return out.getvalue(), size + return data, size async def generate_avatar_assets_from_idea( From 62f1a0d81029a9b9a4cd97586aa1ad67f77db987 Mon Sep 17 00:00:00 2001 From: JegernOUTT Date: Fri, 27 Mar 2026 13:14:51 +1030 Subject: [PATCH 5/6] fix(xai): remove conditional xai_sdk import Make xai_sdk a hard dependency by removing the try/except import and related runtime check. Users must now install xai-sdk explicitly. --- flexus_simple_bots/avatar_from_idea_imagine.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/flexus_simple_bots/avatar_from_idea_imagine.py b/flexus_simple_bots/avatar_from_idea_imagine.py index 1d5c8072..8eb6897a 100644 --- a/flexus_simple_bots/avatar_from_idea_imagine.py +++ b/flexus_simple_bots/avatar_from_idea_imagine.py @@ -1,10 +1,7 @@ import os, sys, asyncio, base64, io, json from pathlib import Path from PIL import Image -try: - import xai_sdk -except ImportError: - xai_sdk = None +import xai_sdk _default_client = None @@ -15,8 +12,6 @@ def create_xai_client(api_key: str | None = None): - if xai_sdk is None: - raise RuntimeError("xai-sdk package is required") if api_key: return xai_sdk.Client(api_key=api_key) return xai_sdk.Client() From 7c2bd3bf5a7f4cb576bdf75aaeca9b1eb6a422ec Mon Sep 17 00:00:00 2001 From: JegernOUTT Date: Fri, 27 Mar 2026 17:42:28 +1030 Subject: [PATCH 6/6] feat(devenv): replace ping/pong with heartbeat mechanism - Add continuous heartbeat loop (every 10s) in service_devenv.py - Replace sys.pong with sys.heartbeat messages - Update frontend WebSocket to use heartbeat timeout (30s) instead of ping/pong - Remove ping/pong handling and auto-restart logic from botwiz.vue - Add heartbeat task lifecycle management and proper cleanup - Update TypeScript types for new heartbeat protocol --- flexus_simple_bots/bot_pictures/__init__.py | 1 + flexus_simple_bots/bot_pictures/style_bank/__init__.py | 1 + 2 files changed, 2 insertions(+) create mode 100644 flexus_simple_bots/bot_pictures/__init__.py create mode 100644 flexus_simple_bots/bot_pictures/style_bank/__init__.py diff --git a/flexus_simple_bots/bot_pictures/__init__.py b/flexus_simple_bots/bot_pictures/__init__.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/flexus_simple_bots/bot_pictures/__init__.py @@ -0,0 +1 @@ + diff --git a/flexus_simple_bots/bot_pictures/style_bank/__init__.py b/flexus_simple_bots/bot_pictures/style_bank/__init__.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/flexus_simple_bots/bot_pictures/style_bank/__init__.py @@ -0,0 +1 @@ +