Spaces:
Running
Running
Commit ·
e028a83
1
Parent(s): 4647039
Fix character_id mismatch in savedata
Browse files- app.py +17 -2
- tests/test_app.py +67 -0
app.py
CHANGED
|
@@ -249,12 +249,18 @@ def save_conversation_fn(char_id: str, persona: dict, request: gr.Request):
|
|
| 249 |
try:
|
| 250 |
path = export_save(char_id, persona, session_id)
|
| 251 |
logger.info(f"[App] Save successful → {path}")
|
| 252 |
-
|
|
|
|
| 253 |
except Exception as e:
|
| 254 |
logger.error(f"[App] Save failed: {e}", exc_info=True)
|
| 255 |
return gr.update(visible=False), f"*❌ Save failed: {e}*"
|
| 256 |
|
| 257 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 258 |
def load_conversation_fn(file_path, current_char_id: str, request: gr.Request):
|
| 259 |
"""
|
| 260 |
Read an uploaded save file, restore memory, and refresh the UI.
|
|
@@ -588,6 +594,15 @@ with gr.Blocks(title=f"Chat with {char_name}") as demo:
|
|
| 588 |
outputs=[save_file, save_status],
|
| 589 |
)
|
| 590 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 591 |
load_event = load_upload.change(
|
| 592 |
fn=load_conversation_fn,
|
| 593 |
cancels=chatbot.generation_events,
|
|
@@ -622,4 +637,4 @@ with gr.Blocks(title=f"Chat with {char_name}") as demo:
|
|
| 622 |
|
| 623 |
|
| 624 |
if __name__ == "__main__":
|
| 625 |
-
demo.launch()
|
|
|
|
| 249 |
try:
|
| 250 |
path = export_save(char_id, persona, session_id)
|
| 251 |
logger.info(f"[App] Save successful → {path}")
|
| 252 |
+
name = load_character(char_id).get("name", char_id)
|
| 253 |
+
return gr.update(value=path, visible=True), f"*✓ Save file ready for {name} — click to download.*"
|
| 254 |
except Exception as e:
|
| 255 |
logger.error(f"[App] Save failed: {e}", exc_info=True)
|
| 256 |
return gr.update(visible=False), f"*❌ Save failed: {e}*"
|
| 257 |
|
| 258 |
|
| 259 |
+
def invalidate_save_fn():
|
| 260 |
+
"""A download is a snapshot; hide it once the conversation context changes."""
|
| 261 |
+
return gr.update(value=None, visible=False), ""
|
| 262 |
+
|
| 263 |
+
|
| 264 |
def load_conversation_fn(file_path, current_char_id: str, request: gr.Request):
|
| 265 |
"""
|
| 266 |
Read an uploaded save file, restore memory, and refresh the UI.
|
|
|
|
| 594 |
outputs=[save_file, save_status],
|
| 595 |
)
|
| 596 |
|
| 597 |
+
gr.on(
|
| 598 |
+
triggers=[char_selector.input, load_upload.change,
|
| 599 |
+
chatbot.chatbot_state.change, persona_state.change],
|
| 600 |
+
fn=invalidate_save_fn,
|
| 601 |
+
inputs=[],
|
| 602 |
+
outputs=[save_file, save_status],
|
| 603 |
+
queue=False,
|
| 604 |
+
)
|
| 605 |
+
|
| 606 |
load_event = load_upload.change(
|
| 607 |
fn=load_conversation_fn,
|
| 608 |
cancels=chatbot.generation_events,
|
|
|
|
| 637 |
|
| 638 |
|
| 639 |
if __name__ == "__main__":
|
| 640 |
+
demo.launch()
|
tests/test_app.py
CHANGED
|
@@ -12,6 +12,7 @@ from gradio.state_holder import SessionState
|
|
| 12 |
import app
|
| 13 |
from engine import memory
|
| 14 |
from engine.saveload import export_save
|
|
|
|
| 15 |
from test_conversations import Client, CHAR, OTHER
|
| 16 |
|
| 17 |
|
|
@@ -29,6 +30,72 @@ class AppTests(unittest.TestCase):
|
|
| 29 |
self.assertFalse(app.use_translator_state.value)
|
| 30 |
self.assertFalse(app.translator_toggle.value)
|
| 31 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
def test_character_switch_restores_visible_and_internal_history(self):
|
| 33 |
state = memory.load_conversation(self.session, CHAR)
|
| 34 |
state['recent_messages'].extend([{'role': 'user', 'content': 'Remember me'},
|
|
|
|
| 12 |
import app
|
| 13 |
from engine import memory
|
| 14 |
from engine.saveload import export_save
|
| 15 |
+
from engine.saveload import import_save
|
| 16 |
from test_conversations import Client, CHAR, OTHER
|
| 17 |
|
| 18 |
|
|
|
|
| 30 |
self.assertFalse(app.use_translator_state.value)
|
| 31 |
self.assertFalse(app.translator_toggle.value)
|
| 32 |
|
| 33 |
+
def test_session_states_are_registered_in_browser_config(self):
|
| 34 |
+
component_ids = {c['id'] for c in app.demo.get_config_file()['components']}
|
| 35 |
+
for state in (app.char_id_state, app.persona_state, app.language_state,
|
| 36 |
+
app.model_state, app.use_translator_state):
|
| 37 |
+
with self.subTest(state=state._id):
|
| 38 |
+
self.assertIn(state._id, component_ids)
|
| 39 |
+
|
| 40 |
+
def test_stale_save_download_is_cleared_when_context_changes(self):
|
| 41 |
+
fn = next(f for f in app.demo.fns.values() if f.name == 'invalidate_save_fn')
|
| 42 |
+
self.assertEqual([app.save_file, app.save_status], fn.outputs)
|
| 43 |
+
self.assertTrue({(app.char_selector._id, 'input'),
|
| 44 |
+
(app.load_upload._id, 'change'),
|
| 45 |
+
(app.chatbot.chatbot_state._id, 'change'),
|
| 46 |
+
(app.persona_state._id, 'change')}.issubset(set(fn.targets)))
|
| 47 |
+
update, status = app.invalidate_save_fn()
|
| 48 |
+
self.assertIsNone(update['value'])
|
| 49 |
+
self.assertFalse(update['visible'])
|
| 50 |
+
self.assertEqual('', status)
|
| 51 |
+
|
| 52 |
+
def test_haley_save_load_and_continue_uses_same_character(self):
|
| 53 |
+
async def dispatch(name, inputs, state):
|
| 54 |
+
fn_id = next(i for i, f in app.demo.fns.items() if f.name == name)
|
| 55 |
+
return await app.demo.process_api(fn_id, inputs, state=state,
|
| 56 |
+
request=self.request, session_hash=self.request.session_hash)
|
| 57 |
+
|
| 58 |
+
async def run():
|
| 59 |
+
haley = 'haley_dunphy'
|
| 60 |
+
state = SessionState(app.demo)
|
| 61 |
+
await dispatch('on_character_change', [haley], state)
|
| 62 |
+
history = memory.load_conversation(self.session, haley)['recent_messages']
|
| 63 |
+
history.extend([{'role': 'user', 'content': 'Remember our trip?'},
|
| 64 |
+
{'role': 'assistant', 'content': 'Of course!'}])
|
| 65 |
+
app.persist_visible_history(history, haley, self.request)
|
| 66 |
+
saved = await dispatch('save_conversation_fn', [None, None], state)
|
| 67 |
+
path = Path(saved['data'][0]['value']['path'])
|
| 68 |
+
self.assertEqual(haley, import_save(path)['character_id'])
|
| 69 |
+
self.assertIn('Haley Dunphy', saved['data'][1])
|
| 70 |
+
await dispatch('on_character_change', [CHAR], state)
|
| 71 |
+
with tempfile.TemporaryDirectory(dir=gr.utils.get_upload_folder()) as directory:
|
| 72 |
+
uploaded = Path(directory) / 'haley.json'
|
| 73 |
+
uploaded.write_bytes(path.read_bytes())
|
| 74 |
+
loaded = await dispatch('load_conversation_fn',
|
| 75 |
+
[{'path': str(uploaded), 'meta': {'_type': 'gradio.FileData'}}, None], state)
|
| 76 |
+
self.assertEqual(haley, state[app.char_id_state._id])
|
| 77 |
+
self.assertEqual(haley, loaded['data'][11]['value'])
|
| 78 |
+
self.assertEqual('## Haley Dunphy', loaded['data'][8])
|
| 79 |
+
self.assertEqual(history, memory.normalize_history(loaded['data'][7]['value']))
|
| 80 |
+
await dispatch('on_character_change', [CHAR], state)
|
| 81 |
+
await dispatch('on_character_change', [haley], state)
|
| 82 |
+
self.assertEqual(history, memory.normalize_history(state[app.chatbot.chatbot_state._id]))
|
| 83 |
+
client = Client()
|
| 84 |
+
with patch.object(app, 'HF_TOKEN', 'test'), patch.object(app, 'InferenceClient', return_value=client):
|
| 85 |
+
result = await dispatch('respond', ['What happened next?', None, 100, .7, .95,
|
| 86 |
+
None, None, None, None, None], state)
|
| 87 |
+
fn_id = next(i for i, f in app.demo.fns.items() if f.name == 'respond')
|
| 88 |
+
while result['is_generating']:
|
| 89 |
+
result = await app.demo.process_api(fn_id, [], state=state,
|
| 90 |
+
request=self.request, session_hash=self.request.session_hash,
|
| 91 |
+
iterator=result['iterator'])
|
| 92 |
+
self.assertIn('Haley', client.calls[0][0][0]['content'])
|
| 93 |
+
self.assertEqual(history, memory.load_conversation(self.session, haley)['recent_messages'][:-2])
|
| 94 |
+
self.assertEqual(1, len(memory.load_conversation(self.session, CHAR)['recent_messages']))
|
| 95 |
+
|
| 96 |
+
Path(gr.utils.get_upload_folder()).mkdir(parents=True, exist_ok=True)
|
| 97 |
+
asyncio.run(run())
|
| 98 |
+
|
| 99 |
def test_character_switch_restores_visible_and_internal_history(self):
|
| 100 |
state = memory.load_conversation(self.session, CHAR)
|
| 101 |
state['recent_messages'].extend([{'role': 'user', 'content': 'Remember me'},
|