File size: 14,278 Bytes
fa8f622
9594221
fa8f622
 
 
 
6afc342
fa8f622
72547e7
fa8f622
 
 
9594221
fa8f622
ab0a14d
fa8f622
ab0a14d
 
 
 
 
addec5c
c57e87b
 
 
 
ab0a14d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1d3bf6d
ab0a14d
c57e87b
 
ab0a14d
 
 
c57e87b
ab0a14d
 
 
 
addec5c
 
 
ab0a14d
 
 
 
 
 
76b9a7f
 
 
 
 
 
 
 
 
 
 
 
ab0a14d
 
 
 
 
 
 
 
76b9a7f
ab0a14d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
addec5c
 
ab0a14d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81375eb
ab0a14d
 
 
 
 
6afc342
addec5c
ab0a14d
 
 
 
 
 
 
 
 
 
6afc342
 
 
 
ab0a14d
 
 
 
 
 
 
 
 
addec5c
 
 
 
c57e87b
addec5c
 
 
 
 
6afc342
 
c57e87b
 
 
31404a1
 
 
 
 
 
6afc342
 
 
 
 
31404a1
 
6afc342
 
 
 
 
 
 
addec5c
 
 
 
c57e87b
addec5c
 
c57e87b
 
 
addec5c
c57e87b
 
addec5c
 
 
 
 
 
 
 
 
 
31404a1
 
 
 
 
 
c57e87b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6afc342
c57e87b
 
 
 
1d3bf6d
 
 
 
 
 
 
 
 
 
 
 
 
addec5c
ab0a14d
addec5c
 
 
ab0a14d
addec5c
 
 
c57e87b
 
ab0a14d
 
 
 
 
 
addec5c
 
ab0a14d
 
81375eb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
---
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": "<action>{content}</action>",
    "thought_format": "<thought>{content}</thought>",
    "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`.