complete restructure
This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user