"""Prompt constants and system-prompt assembly helpers. Holds every prompt string the bot sends to the LLM (image layout selection, image-prompt engineering, prompt verification) plus the shared response-length hint and the ``build_system_prompt`` assembler used by the chat and speak-with-bot paths. """ from __future__ import annotations import re from typing import TYPE_CHECKING from vibe_bot.config import ( IMAGE_GEN_SIZE_LANDSCAPE, IMAGE_GEN_SIZE_PORTRAIT, IMAGE_GEN_SIZE_SQUARE, ) if TYPE_CHECKING: import discord # Image layout (canvas orientation) selection for doodlebob. DEFAULT_IMAGE_LAYOUT = "square" VALID_IMAGE_LAYOUTS = ("portrait", "landscape", "square") LAYOUT_SIZES: dict[str, str] = { "portrait": IMAGE_GEN_SIZE_PORTRAIT, "landscape": IMAGE_GEN_SIZE_LANDSCAPE, "square": IMAGE_GEN_SIZE_SQUARE, } # Shared response-length hint appended to bot system prompts. RESPONSE_LENGTH_HINT = "Keep your responses under 2-3 sentences." IMAGE_LAYOUT_SYSTEM_PROMPT = ( "You decide the aspect ratio (layout) of an image that will be generated " "from a user's request. Choose exactly ONE layout from these three options:\n" "- portrait: a tall, vertical image (taller than wide). Use for subjects that " "are taller than they are wide, such as a single standing person or animal, " "a full-body character, a tall building, a skyscraper, a tree, a rocket, or " "any vertical composition.\n" "- landscape: a wide, horizontal image (wider than tall). Use for scenes that " "are wider than they are tall, such as wide landscapes, panoramas, cityscapes, " "seas and horizons, battle or group scenes spread out horizontally, or any " "horizontal composition.\n" "- square: an image that is as wide as it is tall. Use for balanced subjects, " "close-ups, faces, single objects, logos, emblems, or whenever no strong tall " "or wide orientation is implied.\n" "Rules:\n" "- Base your choice ONLY on the orientation the content implies.\n" "- Respond with ONLY the single word portrait, landscape, or square.\n" "- Do NOT include any other text, punctuation, explanation, or reasoning.\n" ) IMAGE_PROMPT_SYSTEM_PROMPT_TEMPLATE = ( "You are an expert art director and image-generation prompt engineer. " "Convert the user's message into one single, extremely detailed image " "generation prompt that will be passed directly to a text-to-image model. " "The image model is weak: it guesses at compositions, fumbles rendered " "text, and invents details on its own. Your prompt must therefore leave " "nothing to interpretation - explicitly describe every visible aspect of " "the image so it can be created with extreme precision and detail.\n" "The final image will use a {layout} canvas, so compose the scene to fit " "that orientation.\n" "Your prompt must cover all of the following as one flowing, descriptive " "passage. Begin with the main subject, described completely in the very " "first sentence:\n" "- Subject(s): every subject with concrete specifics (species or " "character, age, build, clothing, colors, materials, accessories), its " "exact pose, expression, gaze direction, and its precise position in the " 'frame (for example "centered in the foreground" or "small in the ' 'upper-left background"). State the relative scale of subjects to each ' "other and to the frame. If the subject is a fusion, hybrid, or anything " "unusual, the first sentence must state in full what is joined to what " "and exactly how it looks, and that description must be repeated near " "the end of the passage.\n" "- Composition and framing: the camera angle (eye-level, low, high, " "bird's-eye), the shot type (extreme close-up, portrait, full body, wide " "establishing shot), the focal point, the arrangement of elements across " "the {layout} canvas, and the depth of field.\n" "- Text: if the image must contain readable text (titles, signs, " "posters, labels, banners, watermarks, logos, captions), quote the EXACT " "text verbatim in double quotes with precise capitalization and " "punctuation, and specify its font style, color, size, and exact " "placement. If the image should contain no text, state that explicitly " '("no text anywhere in the image").\n' "- Setting and background: the complete environment with concrete " "details - location, time of day, weather, and every notable background " "and foreground element with its position.\n" "- Style and rendering: the art style or medium (for example " "photorealistic 35mm photograph, oil painting, watercolor, cel-shaded " "anime, pixel art, vector illustration), the color palette with specific " "colors, the lighting (source, direction, quality, mood), the overall " "atmosphere, and the level of detail.\n" '- Finish with concise quality terms such as "highly detailed, sharp ' 'focus".\n' "Rules:\n" '- Be concrete and specific. Never use vague words like "nice", ' '"cool", "epic", or "various" - name exact colors, objects, ' "positions, and quantities.\n" "- Be literal. Interpret the user's request exactly as written. Never " 'rationalize, normalize, or "improve" it: surreal, absurd, or ' 'anthropomorphic requests are intentional, not mistakes. A "fountain ' 'pen wearing pants" is an anthropomorphized fountain pen character ' "wearing pants, not a pen lying next to a pair of pants.\n" "- Preserve everything the user specified. Fill in details the user did " "not specify with coherent choices that fit the request, but never " "alter, drop, or reinterpret what the user did specify.\n" "- Decompose concepts. The image model lacks world knowledge, so never " "rely on a name alone for anything it might misrender (mythical " "creatures, fictional characters, cultural items, animal breeds, " "instruments, vehicles). Spell out the visual anatomy: silhouette, body " "parts, materials, and distinguishing features, with explicit " "disambiguation. A centaur is a single creature with a human torso, " "arms, and head seamlessly fused to a horse's front half, the horse's " "four legs extending from the human's waist - one fused body, not a " "person riding a horse.\n" "- If told to generate an image of yourself, generate a picture of a " "canada goose. If told to generate a picture of 'me', 'myself', or some " "other self reference, generate a picture of a canada goose.\n" "- Respond with ONLY the image generation prompt itself. Do not affirm " "the user, do not answer the user's questions, and do not add headings, " "labels, numbered lists, or any other text." ) IMAGE_PROMPT_VERIFY_SYSTEM_PROMPT = ( "You are the final quality check for an image generation prompt. The " "image model that will use it has no world knowledge: it renders only " "what is described literally and silently drops anything it does not " "understand - a prompt that merely names a centaur without describing " "the fused human-animal body will produce a plain horse. " "Given the user's original request and the drafted prompt, judge " "strictly: would the drafted prompt, taken completely literally, " "produce exactly what the user asked for, including every unusual, " "mythical, surreal, or anthropomorphic element? " "Respond with ONLY the single word PASS if it would. Otherwise respond " "with ONLY a corrected version of the prompt that would produce exactly " "what the user asked for: one flowing descriptive passage, the " "subject's full anatomy and every unusual element described explicitly " "in the first sentence and repeated near the end, no other text." ) def parse_image_layout(response: str) -> str: """Parse an LLM response into a valid image layout. Args: response: The raw LLM response text. Returns: One of "portrait", "landscape", or "square". Falls back to "square" when the response is empty or does not contain a valid layout. """ text = response.strip().lower() for layout in VALID_IMAGE_LAYOUTS: if re.search(rf"\b{layout}\b", text): return layout return DEFAULT_IMAGE_LAYOUT def build_system_prompt(personality: str, user_info: str) -> str: """Assemble a custom-bot system prompt with the length hint and user info. Args: personality: The base bot personality / system prompt. user_info: Preformatted user information to append. Returns: The assembled system prompt: personality, a response-length hint, and the user information block. """ return f"{personality}\n{RESPONSE_LENGTH_HINT}\n\nUser Information:\n{user_info}" def get_user_info(user: discord.User | discord.Member) -> str: """Format user information for inclusion in bot prompts. Reads only presentation attributes off the (User or Member) object, so it has no runtime dependency on the discord package. """ parts: list[str] = [] if user.global_name: parts.append(f"Global Name: {user.global_name}") nick = getattr(user, "nick", None) if nick: parts.append(f"Nickname: {nick}") top_role = getattr(user, "top_role", None) if top_role and top_role.name != "@everyone": parts.append(f"Top Role: {top_role.name}") activities = getattr(user, "activities", None) if activities: activity_names = [ getattr(a, "name", str(a)) for a in activities if getattr(a, "name", "") != "custom_status" ] if activity_names: parts.append(f"Activities: {', '.join(activity_names)}") joined_at = getattr(user, "joined_at", None) if joined_at: parts.append(f"Joined: {joined_at.strftime('%Y-%m-%d')}") parts.append(f"Username: {user.name}") parts.append(f"User ID: {user.id}") parts.append( f"Account Created: {user.created_at.strftime('%Y-%m-%d') if user.created_at else 'Unknown'}" ) return "\n".join(parts)