Skip to content

Commit 0b4cbc8

Browse files
committed
samples: workflow_streams: chat consumer header + cursor save/restore
Two display fixes for run_chat.py: 1. Print a header line right after start_workflow so the user sees immediate feedback ("[chat <id>] streaming response from gpt-5-mini, awaiting first token...") instead of a blank screen until the first delta arrives. 2. Replace the newline-counting ANSI clear with cursor save/restore (\033[s / \033[u\033[J). The previous version counted text newlines to decide how far up to move the cursor on retry, which undercounts when the terminal has wrapped long lines — the failed attempt's first wrapped lines stayed on screen above the retry marker. save/restore rewinds to a fixed position regardless of wrapping. Bumps the prompt to a 500-word distributed-systems comparison (Paxos vs Raft vs Viewstamped Replication) so there is enough output to comfortably kill the worker mid-stream and watch the retried attempt re-render from scratch.
1 parent e8620c6 commit 0b4cbc8

1 file changed

Lines changed: 40 additions & 33 deletions

File tree

workflow_streams/run_chat.py

Lines changed: 40 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -46,69 +46,76 @@
4646
)
4747
from workflow_streams.workflows.chat_workflow import ChatWorkflow
4848

49-
# A prompt long enough that you can comfortably kill the worker
50-
# mid-stream and watch the retry render. Adjust to taste.
49+
# Long enough that you can comfortably kill the worker mid-stream and
50+
# watch the retry render. Adjust to taste.
5151
DEFAULT_PROMPT = (
52-
"Write a 250-word friendly explainer for a new engineer about why "
53-
"durable execution matters in distributed systems. Use short "
54-
"paragraphs and a couple of concrete examples."
52+
"Write a 500-word comparison of Paxos, Raft, and Viewstamped "
53+
"Replication for a new distributed-systems engineer. Cover the "
54+
"core ideas, leader election, normal-case operation, "
55+
"reconfiguration, and the practical tradeoffs that show up when "
56+
"implementing each. Use short paragraphs."
5557
)
5658

5759

58-
def _ansi_clear(line_count: int) -> None:
59-
"""Move the cursor up `line_count` lines and clear to end of screen.
60-
61-
Used on RETRY to throw away the failed attempt's rendered output
62-
before the retried attempt starts. Counts logical newlines in the
63-
rendered text; a long line that wraps in the terminal won't be
64-
fully cleared by this — accept the rough edges, ``rich`` would do
65-
it cleanly but we are deliberately stdlib-only here.
66-
"""
67-
sys.stdout.write("\r")
68-
if line_count > 0:
69-
sys.stdout.write(f"\033[{line_count}A")
70-
sys.stdout.write("\033[J")
71-
sys.stdout.flush()
60+
# ANSI cursor save / restore. ``\033[s`` saves the current cursor
61+
# position, ``\033[u`` restores it, ``\033[J`` clears from the cursor
62+
# to the end of the screen. Save once before the first delta, and on
63+
# RETRY restore + clear-to-end so the failed attempt's rendered output
64+
# disappears regardless of how it was wrapped by the terminal. Save
65+
# again afterwards so a second retry can rewind to the same point.
66+
ANSI_SAVE = "\033[s"
67+
ANSI_RESTORE_AND_CLEAR = "\033[u\033[J"
7268

7369

7470
async def main() -> None:
7571
client = await Client.connect("localhost:7233")
7672
converter = client.data_converter.payload_converter
7773

7874
workflow_id = f"workflow-stream-chat-{uuid.uuid4().hex[:8]}"
75+
chat_input = ChatInput(prompt=DEFAULT_PROMPT)
7976
handle = await client.start_workflow(
8077
ChatWorkflow.run,
81-
ChatInput(prompt=DEFAULT_PROMPT),
78+
chat_input,
8279
id=workflow_id,
8380
task_queue=CHAT_TASK_QUEUE,
8481
)
8582

83+
# Print a header so the user sees something immediately. The
84+
# response will start streaming below it once the first delta
85+
# arrives — until then this is the only line on screen.
86+
print(
87+
f"[chat {workflow_id}] streaming response from {chat_input.model}, "
88+
f"awaiting first token..."
89+
)
90+
print()
91+
sys.stdout.write(ANSI_SAVE)
92+
sys.stdout.flush()
93+
8694
stream = WorkflowStreamClient.create(client, workflow_id)
8795

88-
# Subscribe to all three topics on a single iterator. result_type=
89-
# RawValue lets us dispatch on item.topic and decode against the
90-
# right dataclass per topic.
91-
accumulated: list[str] = []
96+
# Subscribe to all three topics on a single iterator.
97+
# result_type=RawValue lets us dispatch on item.topic and decode
98+
# against the right dataclass per topic.
9299
async for item in stream.subscribe(
93100
[TOPIC_DELTA, TOPIC_RETRY, TOPIC_COMPLETE],
94101
result_type=RawValue,
95102
):
96103
if item.topic == TOPIC_RETRY:
97104
evt = converter.from_payload(item.data.payload, RetryEvent)
98-
line_count = "".join(accumulated).count("\n")
99-
_ansi_clear(line_count)
100-
print(f"[retry attempt {evt.attempt}] resetting output\n")
101-
accumulated.clear()
105+
sys.stdout.write(ANSI_RESTORE_AND_CLEAR)
106+
sys.stdout.flush()
107+
print(f"[retry attempt {evt.attempt}] resetting output")
108+
print()
109+
sys.stdout.write(ANSI_SAVE)
110+
sys.stdout.flush()
102111
elif item.topic == TOPIC_DELTA:
103112
delta = converter.from_payload(item.data.payload, TextDelta)
104-
accumulated.append(delta.text)
105113
sys.stdout.write(delta.text)
106114
sys.stdout.flush()
107115
elif item.topic == TOPIC_COMPLETE:
108-
# Newline so the prompt isn't on the same line as the
109-
# last delta. The TextComplete payload is the full text
110-
# (also returned by the workflow), but the consumer has
111-
# already rendered it incrementally so we don't reprint.
116+
# The full text is also in the payload (and returned by
117+
# the workflow), but the consumer has already rendered it
118+
# incrementally. Just terminate the line.
112119
converter.from_payload(item.data.payload, TextComplete)
113120
print()
114121
break

0 commit comments

Comments
 (0)