cereeenn120 commited on
Commit
e028a83
·
1 Parent(s): 4647039

Fix character_id mismatch in savedata

Browse files
Files changed (2) hide show
  1. app.py +17 -2
  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
- return gr.update(value=path, visible=True), "*✓ Save file ready — click to download.*"
 
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'},