Skip to content

Commit 5174dd4

Browse files
committed
fix: use textual batch update for llm chunks
1 parent 490c958 commit 5174dd4

File tree

4 files changed

+64
-15
lines changed

4 files changed

+64
-15
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "uipath-dev"
3-
version = "0.0.9"
3+
version = "0.0.10"
44
description = "UiPath Developer Console"
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.11"

src/uipath/dev/ui/panels/chat_panel.py

Lines changed: 61 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212

1313
from uipath.dev.models import ChatMessage, ExecutionRun
1414

15+
# Tunables for streaming performance
16+
STREAM_MIN_INTERVAL = 0.08 # seconds between updates while streaming
17+
STREAM_MIN_DELTA_CHARS = 8 # min new chars before we bother updating
18+
1519

1620
class Prompt(Markdown):
1721
"""User prompt message bubble."""
@@ -36,12 +40,14 @@ class ChatPanel(Container):
3640

3741
_chat_widgets: dict[str, Markdown]
3842
_last_update_time: dict[str, float]
43+
_last_content: dict[str, str]
3944

4045
def __init__(self, **kwargs):
4146
"""Initialize the chat panel."""
4247
super().__init__(**kwargs)
4348
self._chat_widgets = {}
4449
self._last_update_time = {}
50+
self._last_content = {}
4551

4652
def compose(self) -> ComposeResult:
4753
"""Compose the UI layout."""
@@ -52,19 +58,21 @@ def compose(self) -> ComposeResult:
5258
id="chat-input",
5359
)
5460

55-
def update_messages(self, run: ExecutionRun) -> None:
61+
def refresh_messages(self, run: ExecutionRun) -> None:
5662
"""Update the chat panel with messages from the given execution run."""
57-
chat_view = self.query_one("#chat-view")
63+
chat_view = self.query_one("#chat-view", VerticalScroll)
5864
chat_view.remove_children()
5965
self._chat_widgets.clear()
6066
self._last_update_time.clear()
67+
self._last_content.clear()
6168

6269
for chat_msg in run.messages:
6370
self.add_chat_message(
6471
ChatMessage(message=chat_msg, event=None, run_id=run.id),
6572
auto_scroll=False,
6673
)
6774

75+
# For a fresh run, always show the latest messages
6876
chat_view.scroll_end(animate=False)
6977

7078
def add_chat_message(
@@ -73,10 +81,11 @@ def add_chat_message(
7381
auto_scroll: bool = True,
7482
) -> None:
7583
"""Add or update a chat message bubble."""
76-
chat_view = self.query_one("#chat-view")
84+
chat_view = self.query_one("#chat-view", VerticalScroll)
7785

78-
message = chat_msg.message
86+
should_autoscroll = auto_scroll and not chat_view.is_vertical_scrollbar_grabbed
7987

88+
message = chat_msg.message
8089
if message is None:
8190
return
8291

@@ -114,27 +123,67 @@ def add_chat_message(
114123

115124
content = "\n\n".join(content_lines)
116125

126+
prev_content = self._last_content.get(message.message_id)
127+
if prev_content is not None and content == prev_content:
128+
# We already rendered this exact content, no need to touch the UI.
129+
return
130+
117131
existing = self._chat_widgets.get(message.message_id)
118132
now = time.monotonic()
119133
last_update = self._last_update_time.get(message.message_id, 0.0)
134+
prev_content_len = len(prev_content) if prev_content is not None else 0
135+
delta_len = len(content) - prev_content_len
120136

121-
if existing:
137+
def should_update() -> bool:
122138
event = chat_msg.event
123-
should_update = (
139+
finished = (
124140
event
125141
and event.exchange
126142
and event.exchange.message
127143
and event.exchange.message.end is not None
128144
)
129-
if should_update or now - last_update > 0.15:
145+
146+
if finished:
147+
# Always paint the final state immediately.
148+
return True
149+
150+
# Throttle streaming: require both some time and a minimum delta size.
151+
if now - last_update < STREAM_MIN_INTERVAL:
152+
return False
153+
154+
if delta_len < STREAM_MIN_DELTA_CHARS:
155+
return False
156+
157+
return True
158+
159+
if existing:
160+
if not should_update():
161+
return
162+
163+
# Fast path: message is growing by appending new text.
164+
if (
165+
isinstance(existing, Markdown)
166+
and prev_content is not None
167+
and content.startswith(prev_content)
168+
):
169+
delta = content[len(prev_content) :]
170+
if delta:
171+
# Streaming update: only append the new portion.
172+
existing.append(delta)
173+
else:
174+
# Fallback for non-monotonic changes: full update.
130175
existing.update(content)
131-
self._last_update_time[message.message_id] = now
132-
if auto_scroll:
133-
chat_view.scroll_end(animate=False)
176+
177+
self._last_content[message.message_id] = content
178+
self._last_update_time[message.message_id] = now
134179
else:
180+
# First time we see this message: create a new widget.
135181
widget_instance = widget_cls(content)
136182
chat_view.mount(widget_instance)
137183
self._chat_widgets[message.message_id] = widget_instance
138184
self._last_update_time[message.message_id] = now
139-
if auto_scroll:
140-
chat_view.scroll_end(animate=False)
185+
self._last_content[message.message_id] = content
186+
187+
# Only auto-scroll if we were at the bottom *before* the update
188+
if should_autoscroll:
189+
chat_view.scroll_end(animate=False)

src/uipath/dev/ui/panels/run_details_panel.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,7 @@ def _show_run_details(self, run: ExecutionRun):
307307

308308
def _show_run_chat(self, run: ExecutionRun) -> None:
309309
chat_panel = self.query_one("#chat-panel", ChatPanel)
310-
chat_panel.update_messages(run)
310+
chat_panel.refresh_messages(run)
311311

312312
def _rebuild_spans_tree(self):
313313
"""Rebuild the spans tree from current run's traces."""

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)