David-stout commited on
Commit
af1e438
·
verified ·
1 Parent(s): 216d436

Fix multi-turn chat: lock composer during generation, always stream into a fresh end-of-history bubble

Browse files
Files changed (1) hide show
  1. app.py +51 -43
app.py CHANGED
@@ -724,6 +724,11 @@ def _parse_think(raw: str) -> tuple[str, str, bool]:
724
  close_idx = rest.find(THINK_CLOSE)
725
  before = raw[:open_idx]
726
  if close_idx == -1:
 
 
 
 
 
727
  return rest.strip(), before.strip(), True
728
  think = rest[:close_idx].strip()
729
  answer = (before + rest[close_idx + len(THINK_CLOSE) :]).strip()
@@ -853,20 +858,27 @@ def generate_reply(history, max_new_tokens, temperature, top_p, enable_thinking)
853
  return
854
  conversation = _history_for_model(history)
855
 
 
 
856
  think_msg = None
857
- for msg in reversed(history):
858
- if msg.get("role") == "assistant" and isinstance(msg.get("metadata"), dict):
859
- think_msg = msg
860
- break
861
- if think_msg is None:
862
- think_msg = {
863
- "role": "assistant",
864
- "content": "",
865
- "metadata": {"title": "Logicizing…", "status": "pending"},
866
- }
 
 
867
  history.append(think_msg)
868
  else:
869
- think_msg["metadata"] = {"title": "Logicizing…", "status": "pending"}
 
 
 
870
  yield history
871
 
872
  answer_msg = {"role": "assistant", "content": ""}
@@ -958,39 +970,21 @@ with gr.Blocks(fill_height=True, fill_width=True, elem_id="app-root") as demo:
958
  gr.Button("Analyze it", size="sm", elem_classes=["twil-card-btn"])
959
  )
960
 
961
- send.click(
962
- queue_message,
963
- [prompt, chatbot],
964
- [prompt, chatbot, chips_row],
965
- show_progress="hidden",
966
- ).then(
967
- generate_reply,
968
- [chatbot, max_new_tokens, temperature, top_p, enable_thinking],
969
- chatbot,
970
- concurrency_limit=1,
971
- show_progress="hidden",
972
- )
973
- prompt.submit(
974
- queue_message,
975
- [prompt, chatbot],
976
- [prompt, chatbot, chips_row],
977
- show_progress="hidden",
978
- ).then(
979
- generate_reply,
980
- [chatbot, max_new_tokens, temperature, top_p, enable_thinking],
981
- chatbot,
982
- concurrency_limit=1,
983
- show_progress="hidden",
984
- )
985
- for btn, (_, template) in zip(chip_btns, CHIPS):
986
- btn.click(lambda t=template: t, outputs=prompt)
987
 
988
- for btn, case in zip(card_btns, SHOWCASE):
989
- case_prompt = SHOWCASE_TEMPLATE.format(argument=case["quote"])
990
- btn.click(
991
- lambda p=case_prompt: (p, gr.Tabs(selected="chat")),
992
- outputs=[prompt, tabs],
993
- ).then(
 
 
 
 
 
994
  queue_message,
995
  [prompt, chatbot],
996
  [prompt, chatbot, chips_row],
@@ -1002,6 +996,20 @@ with gr.Blocks(fill_height=True, fill_width=True, elem_id="app-root") as demo:
1002
  concurrency_limit=1,
1003
  show_progress="hidden",
1004
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1005
 
1006
  if __name__ == "__main__":
1007
  demo.launch(
 
724
  close_idx = rest.find(THINK_CLOSE)
725
  before = raw[:open_idx]
726
  if close_idx == -1:
727
+ # Hide a partially streamed closing tag at the end of the think block.
728
+ for i in range(len(THINK_CLOSE) - 1, 0, -1):
729
+ if rest.endswith(THINK_CLOSE[:i]):
730
+ rest = rest[:-i]
731
+ break
732
  return rest.strip(), before.strip(), True
733
  think = rest[:close_idx].strip()
734
  answer = (before + rest[close_idx + len(THINK_CLOSE) :]).strip()
 
858
  return
859
  conversation = _history_for_model(history)
860
 
861
+ # Stream only into a bubble at the END of the history. Reaching for any
862
+ # older assistant message would overwrite a previous turn in place.
863
  think_msg = None
864
+ last = history[-1] if history else None
865
+ if (
866
+ last is not None
867
+ and last.get("role") == "assistant"
868
+ and isinstance(last.get("metadata"), dict)
869
+ and last["metadata"].get("status") == "pending"
870
+ ):
871
+ think_msg = last
872
+ think_msg["content"] = ""
873
+ elif last is not None and last.get("role") == "user":
874
+ # Gradio strips the empty pending bubble in preprocessing; recreate it.
875
+ think_msg = {"role": "assistant", "content": ""}
876
  history.append(think_msg)
877
  else:
878
+ # No new user turn (e.g. an empty submit); don't regenerate old answers.
879
+ yield history
880
+ return
881
+ think_msg["metadata"] = {"title": "Logicizing…", "status": "pending"}
882
  yield history
883
 
884
  answer_msg = {"role": "assistant", "content": ""}
 
970
  gr.Button("Analyze it", size="sm", elem_classes=["twil-card-btn"])
971
  )
972
 
973
+ # Lock the composer while a turn is running: overlapping events make the
974
+ # finishing stream overwrite the chat with its own stale history.
975
+ lockable = [prompt, send, *card_btns]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
976
 
977
+ def _lock():
978
+ return [gr.update(interactive=False)] * len(lockable)
979
+
980
+ def _unlock():
981
+ return [gr.update(interactive=True)] * len(lockable)
982
+
983
+ def _wire(event, prep_fn=None, prep_outputs=None):
984
+ chain = event(_lock, None, lockable, show_progress="hidden")
985
+ if prep_fn is not None:
986
+ chain = chain.then(prep_fn, outputs=prep_outputs, show_progress="hidden")
987
+ chain = chain.then(
988
  queue_message,
989
  [prompt, chatbot],
990
  [prompt, chatbot, chips_row],
 
996
  concurrency_limit=1,
997
  show_progress="hidden",
998
  )
999
+ chain.then(_unlock, None, lockable, show_progress="hidden")
1000
+
1001
+ _wire(send.click)
1002
+ _wire(prompt.submit)
1003
+ for btn, (_, template) in zip(chip_btns, CHIPS):
1004
+ btn.click(lambda t=template: t, outputs=prompt)
1005
+
1006
+ for btn, case in zip(card_btns, SHOWCASE):
1007
+ case_prompt = SHOWCASE_TEMPLATE.format(argument=case["quote"])
1008
+ _wire(
1009
+ btn.click,
1010
+ prep_fn=lambda p=case_prompt: (p, gr.Tabs(selected="chat")),
1011
+ prep_outputs=[prompt, tabs],
1012
+ )
1013
 
1014
  if __name__ == "__main__":
1015
  demo.launch(