Spaces:
Running
Running
Download engine/prompt_builder.py from cereeenn120/CharacterChatBot: direct link, hf CLI and curl.
- Browser
- Download file 11.7 kB
-
https://huggingface.co/spaces/cereeenn120/CharacterChatBot/resolve/main/engine/prompt_builder.py
- Command line
-
hf download hf://spaces/cereeenn120/CharacterChatBot/engine/prompt_builder.py
-
curl -L -o prompt_builder.py https://huggingface.co/spaces/cereeenn120/CharacterChatBot/resolve/main/engine/prompt_builder.py
11.7 kB
| """ | |
| Prompt builder β assembles the full system prompt from all parts. | |
| Build order (each section separated by a blank line): | |
| 1. Global rules (format rules, roleplay rules) | |
| 2. Character definition (identity, personality, speech, world, examples) | |
| 3. Character-specific instructions (system_prompt field in JSON) | |
| 4. User persona (optional) | |
| 5. Memory (conversation summary + long-term memories) | |
| Then passed as the system message, followed by: | |
| 6. Recent conversation history | |
| 7. Current user message | |
| """ | |
| import glob | |
| import logging | |
| import os | |
| import re | |
| from pathlib import Path | |
| from typing import Any | |
| from engine.static_files import load_static_json, load_static_text | |
| logger = logging.getLogger(__name__) | |
| ROOT = Path(__file__).resolve().parent.parent | |
| CHARACTERS_DIR = ROOT / "characters" | |
| PROMPTS_DIR = ROOT / "prompts" | |
| ASSETS_DIR = ROOT / "assets" | |
| # ββ Character loading ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def load_character(character_id: str) -> dict[str, Any]: | |
| """Load and return a character dict from characters/{character_id}.json.""" | |
| if not isinstance(character_id, str) or not re.fullmatch(r"[a-zA-Z0-9_-]{1,100}", character_id): | |
| raise ValueError("Invalid character ID.") | |
| path = os.path.join(CHARACTERS_DIR, f"{character_id}.json") | |
| if not os.path.exists(path): | |
| raise FileNotFoundError(f"Character file not found: {path}") | |
| return load_static_json(path) | |
| def list_characters() -> list[dict[str, str]]: | |
| """ | |
| Discover all characters from the characters/ directory. | |
| Returns a list of dicts with id, name, and description. | |
| Skips any files that fail to parse (logs a warning instead of raising). | |
| """ | |
| characters = [] | |
| for path in sorted(glob.glob(os.path.join(CHARACTERS_DIR, "*.json"))): | |
| try: | |
| data = load_static_json(path) | |
| characters.append({ | |
| "id": data.get("id", os.path.splitext(os.path.basename(path))[0]), | |
| "name": data.get("name", "Unknown"), | |
| "description": data.get("description", ""), | |
| "categories": data.get("categories", []), | |
| }) | |
| except Exception as e: | |
| logger.warning(f"[Characters] Skipping {path}: {e}") | |
| return characters | |
| def get_avatar_path(character_id: str) -> str | None: | |
| """ | |
| Return the first avatar image path found for a character. | |
| Logs a warning and returns None if no image exists β does not raise. | |
| """ | |
| avatar_dir = os.path.join(ASSETS_DIR, "characters", character_id) | |
| for pattern in ("*.png", "*.jpg", "*.jpeg", "*.webp"): | |
| matches = glob.glob(os.path.join(avatar_dir, pattern)) | |
| if matches: | |
| return sorted(matches)[0] | |
| logger.warning( | |
| f"[Avatar] No image found for '{character_id}' in '{avatar_dir}'. " | |
| "A placeholder will be shown in the UI." | |
| ) | |
| return None | |
| # ββ Prompt sections ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def build_global_prompt() -> str: | |
| """Load the global system prompt from prompts/system_prompt.txt.""" | |
| path = os.path.join(PROMPTS_DIR, "system_prompt.txt") | |
| return load_static_text(path).strip() | |
| def build_character_prompt(character: dict[str, Any]) -> str: | |
| """ | |
| Build the character definition block from a character dict. | |
| Covers: identity, personality, speech style, behavior, world, roleplay rules, | |
| and a capped set of example dialogues. | |
| """ | |
| name = character.get("name", "Unknown") | |
| sections = [f"You are {name}."] | |
| # Identity | |
| identity = character.get("identity", {}) | |
| if identity: | |
| parts = [] | |
| if identity.get("role"): | |
| parts.append(f"Role: {identity['role']}") | |
| if identity.get("background"): | |
| parts.append(f"Background: {identity['background']}") | |
| if parts: | |
| sections.append("IDENTITY\n" + "\n".join(parts)) | |
| # Personality | |
| personality = character.get("personality", []) | |
| if personality: | |
| sections.append("PERSONALITY\n" + ", ".join(personality)) | |
| # Speech style | |
| speech = character.get("speech_style", {}) | |
| if speech: | |
| parts = [] | |
| if speech.get("tone"): | |
| parts.append(f"Tone: {', '.join(speech['tone'])}") | |
| if speech.get("formality"): | |
| parts.append(f"Formality: {speech['formality']}") | |
| if speech.get("sentence_style"): | |
| parts.append(f"Sentence style: {speech['sentence_style']}") | |
| flags = [] | |
| if speech.get("uses_humor"): | |
| flags.append("humor") | |
| if speech.get("uses_rhetorical_questions"): | |
| flags.append("rhetorical questions") | |
| if speech.get("avoids_excessive_enthusiasm"): | |
| flags.append("avoids excessive enthusiasm") | |
| if flags: | |
| parts.append("Uses: " + ", ".join(flags)) | |
| if parts: | |
| sections.append("SPEECH STYLE\n" + "\n".join(parts)) | |
| # Behavior | |
| behavior = character.get("behavior", {}) | |
| if behavior: | |
| lines = [ | |
| f"- {k.replace('_', ' ')}: {v}" | |
| for k, v in behavior.items() | |
| ] | |
| sections.append("BEHAVIOR\n" + "\n".join(lines)) | |
| # World | |
| world = character.get("world", {}) | |
| if world: | |
| parts = [] | |
| if world.get("universe"): | |
| parts.append(f"Universe: {world['universe']}") | |
| if world.get("setting"): | |
| parts.append(f"Setting: {world['setting']}") | |
| if world.get("description"): | |
| parts.append(world["description"]) | |
| sections.append("WORLD\n" + "\n".join(parts)) | |
| # Roleplay rules | |
| roleplay = character.get("roleplay", {}) | |
| if roleplay: | |
| parts = [] | |
| if roleplay.get("action_format"): | |
| parts.append(f"Action format: {roleplay['action_format']}") | |
| if roleplay.get("thought_format"): | |
| parts.append(f"Thought format: {roleplay['thought_format']}") | |
| if roleplay.get("break_character") is False: | |
| parts.append("Never break character.") | |
| if parts: | |
| sections.append("ROLEPLAY RULES\n" + "\n".join(parts)) | |
| # Example dialogues (capped at 4 to manage token budget) | |
| examples = character.get("example_dialogues", []) | |
| if examples: | |
| lines = ["EXAMPLE DIALOGUES"] | |
| for ex in examples[:4]: | |
| lines.append(f'User: {ex["user"]}') | |
| lines.append(f'{name}: {ex["character"]}') | |
| lines.append("") | |
| sections.append("\n".join(lines).strip()) | |
| return "\n\n".join(sections) | |
| def build_character_specific_prompt(character: dict[str, Any]) -> str: | |
| """ | |
| Return the character's system_prompt field. | |
| This is character-specific fine-tuning on top of the global rules β | |
| it does not replace them. | |
| """ | |
| sp = character.get("system_prompt", "").strip() | |
| if sp: | |
| return f"ADDITIONAL CHARACTER NOTES\n{sp}" | |
| return "" | |
| def build_persona_prompt(persona: dict[str, str] | None) -> str: | |
| """ | |
| Build the user persona section. | |
| Returns an empty string if persona is absent or all fields are blank. | |
| """ | |
| if not persona or not any(persona.values()): | |
| return "" | |
| lines = ["THE USER IS PLAYING THE ROLE OF:"] | |
| for key in ("name", "age", "gender", "personality", "background"): | |
| val = persona.get(key, "").strip() | |
| if val: | |
| lines.append(f"{key.capitalize()}: {val}") | |
| return "\n".join(lines) | |
| def build_memory_prompt(summary: str, long_term_memory: list[dict]) -> str: | |
| """Build the memory section from conversation summary and long-term memories.""" | |
| parts = [] | |
| if summary: | |
| parts.append(f"CONVERSATION SUMMARY\n{summary}") | |
| if long_term_memory: | |
| lines = ["LONG-TERM MEMORY"] | |
| for mem in long_term_memory: | |
| importance = mem.get("importance", "medium").upper() | |
| content = mem.get("content", "") | |
| lines.append(f"[{importance}] {content}") | |
| parts.append("\n".join(lines)) | |
| return "\n\n".join(parts) | |
| # ββ Language instruction βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Maps lowercase language name β display label (native name in parentheses) | |
| _LANG_LABELS: dict[str, str] = { | |
| "turkish": "Turkish (TΓΌrkΓ§e)", | |
| "french": "French (FranΓ§ais)", | |
| "german": "German (Deutsch)", | |
| "italian": "Italian (Italiano)", | |
| "russian": "Russian (Π ΡΡΡΠΊΠΈΠΉ)", | |
| "chinese": "Chinese (δΈζ)", | |
| } | |
| def build_language_prompt(language: str) -> str: | |
| """ | |
| Return a language-enforcement instruction for the system prompt. | |
| Returns an empty string for English (the default behaviour of most models). | |
| """ | |
| key = language.strip().lower() | |
| if not key or key == "english": | |
| return "" | |
| label = _LANG_LABELS.get(key, language) | |
| return ( | |
| f"LANGUAGE\n" | |
| f"You must always respond in {label}. " | |
| f"This applies to all dialogue, actions, and internal thoughts β " | |
| f"regardless of what language the user writes in." | |
| ) | |
| # ββ Final assembly βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def build_messages( | |
| character: dict[str, Any], | |
| persona: dict[str, str] | None, | |
| summary: str, | |
| long_term_memory: list[dict], | |
| recent_messages: list[dict], | |
| current_message: str, | |
| language: str = "English", | |
| ) -> list[dict[str, str]]: | |
| """ | |
| Assemble the full messages array to send to the LLM. | |
| System prompt build order: | |
| 1. Global rules (format rules, roleplay rules) | |
| 2. Character definition (identity, personality, speech, world, examples) | |
| 3. Character-specific instructions (system_prompt field in JSON) | |
| 4. User persona (optional) | |
| 5. Memory (summary + long-term) | |
| 6. Language instruction (omitted for English) | |
| Then: | |
| 7. Recent conversation history | |
| 8. Current user message | |
| """ | |
| system_sections: list[str] = [] | |
| global_prompt = build_global_prompt() | |
| if global_prompt: | |
| system_sections.append(global_prompt) | |
| char_prompt = build_character_prompt(character) | |
| if char_prompt: | |
| system_sections.append(char_prompt) | |
| char_specific = build_character_specific_prompt(character) | |
| if char_specific: | |
| system_sections.append(char_specific) | |
| persona_prompt = build_persona_prompt(persona) | |
| if persona_prompt: | |
| system_sections.append(persona_prompt) | |
| memory_prompt = build_memory_prompt(summary, long_term_memory) | |
| if memory_prompt: | |
| system_sections.append(memory_prompt) | |
| # Language instruction goes last β recency helps models follow it reliably | |
| lang_prompt = build_language_prompt(language) | |
| if lang_prompt: | |
| system_sections.append(lang_prompt) | |
| system_content = "\n\n".join(system_sections) | |
| messages: list[dict[str, str]] = [{"role": "system", "content": system_content}] | |
| messages.extend(recent_messages) | |
| messages.append({"role": "user", "content": current_message}) | |
| logger.debug( | |
| f"[PromptBuilder] Built: {len(system_content)} chars system | " | |
| f"{len(system_sections)} sections | " | |
| f"{len(recent_messages)} history turns | " | |
| f"language='{language}'" | |
| ) | |
| return messages | |