--- title: CharacterChatBot emoji: πŸ’¬ colorFrom: yellow colorTo: purple sdk: gradio sdk_version: 6.12.0 app_file: app.py pinned: true hf_oauth: true hf_oauth_scopes: - inference-api short_description: A Character-Based Chatbot --- # CharacterChatBot A Gradio-based roleplay chatbot for immersive AI character conversations, built on HuggingFace Spaces. ## Features - **Character system** β€” Personalities, speech styles, and behaviors defined in JSON - **Three-layer memory** β€” Session-isolated history, rolling summary, and long-term facts per character - **Memory controls** β€” Remember, edit, and forget facts from the sidebar - **Relationship state** β€” Four independent axes shown as short phrases; romance and attraction are optional - **Edit last message** β€” Revise your last turn and send it again - **Reply suggestions** β€” 4 contextual suggestions on demand, using the selected language and persona - **User persona** β€” Define who you're playing in the roleplay - **Action & thought formatting** β€” `(actions)` and `*thoughts*` rendered distinctly in chat - **Multi-character support** β€” Switch characters via sidebar dropdown ## Project structure ``` CharacterChatBot/ β”œβ”€β”€ app.py # Gradio UI β€” layout, events, respond() β”œβ”€β”€ requirements.txt β”œβ”€β”€ characters/ # One JSON file per character β”‚ └── jessica_jones.json β”œβ”€β”€ engine/ # Core logic (no UI dependencies) β”‚ β”œβ”€β”€ chat.py # LLM streaming generator β”‚ β”œβ”€β”€ prompt_builder.py # System prompt assembly β”‚ β”œβ”€β”€ static_files.py # Bounded static JSON/text cache β”‚ β”œβ”€β”€ memory.py # Three-layer memory system β”‚ β”œβ”€β”€ memory_controls.py # Manual facts and source exclusions β”‚ β”œβ”€β”€ memory_jobs.py # Bounded background extraction β”‚ β”œβ”€β”€ parser.py # Action/thought response parser β”‚ β”œβ”€β”€ suggestions.py # Reply suggestion generator β”‚ β”œβ”€β”€ persona.py # User persona management β”‚ └── relationship.py # Independent relationship axes β”œβ”€β”€ prompts/ # Prompt templates (plain text) β”‚ β”œβ”€β”€ system_prompt.txt # Global roleplay + format rules β”‚ β”œβ”€β”€ reply_suggestion.txt # Suggestion generation prompt β”‚ └── memory_prompt.txt # Memory extraction prompt β”œβ”€β”€ memory/sessions/ # Auto-created; isolated by browser session β”‚ └── {session_id}/ β”‚ └── jessica_jones.json # Atomic snapshot of all memory layers └── assets/ └── characters/ └── jessica_jones/ └── avatar.png ``` ## Character categories The sidebar first selects a category, then lists characters assigned to it. Categories are configured in each character JSON file using a `categories` array. A character can belong to more than one category (for example, Luke Cage is listed under both Marvel and Marvel Defenders). To add a category to an existing character, edit `characters/{character_id}.json` and add or update the field at the top level: ```json "categories": ["Marvel", "Marvel Defenders"] ``` Category names are shown as written and grouped automatically. Use the same spelling and capitalization for characters that belong to the same category. To add a new character, create its JSON file as described below and include a `categories` array. The category appears in the UI automatically; no UI code change is needed. ## Adding a new character 1. Create `characters/{character_id}.json`: ```json { "id": "my_character", "name": "My Character", "categories": ["My Category"], "description": "One-line description shown in the selector.", "identity": { "role": "Their role or profession", "background": "Brief backstory" }, "personality": ["trait1", "trait2", "trait3"], "speech_style": { "tone": ["calm", "precise"], "formality": "formal", "sentence_style": "short_to_medium", "uses_humor": false, "uses_rhetorical_questions": false, "avoids_excessive_enthusiasm": true }, "behavior": { "shows_emotions": true, "deflects_uncomfortable_topics_with_humor": false }, "world": { "universe": "Original", "setting": "Where they live", "description": "World context for the LLM." }, "roleplay": { "action_format": "{content}", "thought_format": "{content}", "break_character": false }, "system_prompt": "Character-specific fine-tuning. This adds to global rules, not replaces them.", "greeting": "The character's opening line when the chat starts.", "example_dialogues": [ { "situation": "Optional context", "user": "Example user message", "character": "Example character response" } ], "visuals": { "avatar": "assets/characters/my_character/avatar.png" } } ``` 2. Add an avatar image under `assets/characters/{character_id}/`, for example `{character_id}.jpg` or `{character_id}_00.png`. PNG, JPG, JPEG, and WebP are supported. The first available image is used (PNG first, then JPG, JPEG, WebP; filenames sorted within each format). If no image is available, the portrait is hidden. Character images are stored directly in Git so they work in checkouts without Git LFS. 3. Restart the Space. The character will appear in the sidebar dropdown automatically. ## Local setup Requires Python 3.10+; Gradio is pinned to the Space SDK version (6.12.0). ```bash git clone https://huggingface.co/spaces/your-username/CharacterChatBot cd CharacterChatBot pip install -r requirements.txt export HF_TOKEN=hf_your_token_here python app.py ``` The app opens at `http://localhost:7860`. Keep the Gradio pin in `requirements.txt` and this README’s `sdk_version` aligned. Version 6.12.0 is browser-tested for replacing chatbot history on character changes and imports; 6.5.1 retained stale message views in these tests. ## HuggingFace Space deployment 1. Push your changes to the Space repository. 2. Go to **Settings β†’ Variables and secrets β†’ New secret**. 3. Name: `HF_TOKEN`, Value: your HuggingFace API token. 4. The Space restarts automatically and picks up the secret. ## Memory system Each browser session has its own per-character snapshot under `memory/sessions/{session_id}/{character_id}.json`. The session key is derived from Gradio's request context, never accepted from an uploaded save file. Switching characters restores that session's complete visible conversation. Clearing the chat clears its transcript, summary, facts, exclusions, and relationship state. Retry and undo update the same history the model uses; changing already-summarized content invalidates its derived memory. The complete transcript is retained for display and export. Once the unsummarized history exceeds 20 messages (not 20 user/assistant pairs), a batch of about 10 older messages is summarized **after the answer is displayed**. When extraction falls behind, a batch can grow to 40 messages (plus one to keep a reply with its user turn). This periodic model call also extracts long-term facts and relationship developments. Subsequent prompts contain the cumulative summary, up to 40 automatic facts plus 60 manually pinned facts, relationship context, and the remaining recent messages. Extraction runs in a background pool with at most two jobs across the app and one per conversation. The chat request completes without waiting for extraction. There is no growing job queue: if the pool is busy, messages remain intact and scheduling is tried after a later response. The same retry policy applies to extraction errors. Before committing, a worker checks a separate memory revision, its original transcript prefix, and the summary it started from. Appending completed turns does not invalidate extraction; only derived fields are merged into the newer conversation. Edits, resets, imports, and memory/relationship setting changes still invalidate pending work. Memory-only updates do not advance the transcript revision. A response being streamed can therefore append to the current snapshot without losing a summary that finished in the meantime. Recent model context has a fallback limit of 40 messages and 24,000 characters so a slow or failed summary cannot make it grow without bound. Older unsummarized turns can temporarily be omitted, and an oversized final message is clipped with an explicit marker. These limits apply to recent history, not the system prompt or current user input. The full transcript remains available for display, later summarization, and export. Invalid relationship observations are discarded without discarding an otherwise valid summary. Storage is isolated by browser session, not an authenticated user account. Refreshing the page or opening a new browser session starts fresh; use **Save / Load** to resume later. Legacy shared `memory/{character_id}` files are never loaded automatically because their owner is unknown. Existing version 1.0 and 2.0 save files can still be imported explicitly into the current session. Save files use version 3.0 and include the full visible transcript, persona, summary, facts and their sources, summary position, memory exclusions, and relationship options and events. Imports validate the entire file before writing: known character ID, supported version, text-only user/assistant messages, memory importance and provenance, relationship events, field lengths, and a 4 MB file limit. Exports have unique filenames. Each snapshot is replaced atomically; in-process revision checks protect concurrent writes in the standard single-process Gradio app. Deployments with multiple app processes would require a shared transactional store instead of this file backend. Translation is off by default in both the UI and backend. The main model streams in the selected language. If translation is enabled explicitly, the displayed translation is also the persisted conversation, so reload and subsequent context match what the user saw. Reply suggestions remain opt-in through the **Suggest replies** button. Their single model call receives the selected language and the applied user persona. On failure, four neutral fallback suggestions are returned in the selected UI language without another model call. Changing language, persona, character, or conversation clears the old suggestion buttons. ## Memory controls and editing Open **Memories** to add a fact with **Remember**, or select a saved fact to **Update selected** or **Forget selected**. Manual facts are pinned and automatic extraction cannot overwrite them. Editing or forgetting invalidates pending extraction, clears the summary for rebuilding, and excludes the fact's recorded source messages and verbatim echoes from future model context. The visible transcript remains unchanged. Other automatic facts and relationship events sharing those sources are removed too. Forgetting is source-based, not guaranteed semantic erasure: paraphrases elsewhere or separately pinned facts can still contain the information. Exact normalized re-extraction is blocked; explicitly remembering the same text removes that block. **Edit last message** removes the last user turn and its reply, puts that user's text back in the input, and rolls back derived memory from the removed portion. Edit the text and send it to generate a new answer. The existing retry action regenerates an answer without changing your message. Previous replies are not saved as alternate branches. ## Relationship state Trust, familiarity, romantic closeness, and sexual attraction develop independently. Romance and attraction are individually opt-in for each character in the session; disabling an axis clears its recorded developments. Low trust or familiarity does not prevent romance or attraction, and no fixed relationship route is enforced. The state describes the character's attitude toward the user persona and does not assume reciprocal feelings or override the character's personality and motivations. The sidebar shows short phrases rather than scores (Turkish and English labels; other languages currently use English). Changes are extracted with the periodic summary call, so they appear after batches of older messages are processed, not after every turn. The panel checks local state every ten seconds without making model calls. Source-linked events allow edits and retries to roll back affected developments and prevent counting the same extraction twice. Save / Load preserves this state; older saves begin with optional axes disabled. ## Static file cache Character JSON and the system, reply-suggestion, and memory-extraction prompt files are cached in process RAM (up to 64 entries, least recently used eviction). Each access checks file metadata; edits and atomic replacements refresh the entry. Missing or invalid files raise errors instead of silently reusing old content. Character data is deep-copied for each caller to avoid shared mutations. The cache is protected by a lock for chat and background worker access. Conversation history, personas, summaries, and user memories are not cached here. This saves local file reads and JSON parsing only; model calls and token usage remain unchanged. Restarting the process clears the cache. ## Tests ```bash python -m unittest discover -s tests -v ``` Tests use temporary storage and fake inference clients; they make no model calls. They cover session isolation, import validation, full-history roundtrips, summary extraction and failures, stale writes, translation, and Gradio event processing for character switching and streaming, manual memory controls, independent relationship axes, source rollback, and last-message editing. ## Configuration In `app.py`, two constants control the active character and model: ```python DEFAULT_CHARACTER = "hermione_granger" # must match a file in characters/ DEFAULT_MODEL = MODELS[4][1] # selected initial model ``` Supported models include anything available on the HuggingFace Serverless Inference API β€” for example `openai/gpt-4o-mini`, `deepseek-ai/DeepSeek-V3`, or `mistralai/Mistral-7B-Instruct-v0.3`.