Skip to content

Commit 9482164

Browse files
authored
Update basic workflow to use human in the loop (#8)
* Update basic workflow to use human in the loop * Fix formats * Hide stop button on initialization
1 parent 333a8a6 commit 9482164

File tree

4 files changed

+102
-33
lines changed

4 files changed

+102
-33
lines changed

.python-version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
3.12
1+
3.13

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ dev = [
2020
"ty>=0.0.1a16",
2121
"pytest>=8.4.1",
2222
"hatch>=1.14.1",
23+
"click>=8.0.0,!=8.3.0",
2324
]
2425

2526

src/app/workflow.py

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@
22
import time
33

44
from workflows import Context, Workflow, step
5-
from workflows.events import StartEvent, StopEvent, Event
5+
from workflows.events import (
6+
HumanResponseEvent,
7+
InputRequiredEvent,
8+
StartEvent,
9+
StopEvent,
10+
Event,
11+
)
612
import logging
713
from datetime import datetime
814

@@ -21,25 +27,49 @@ class WorkflowCompletedEvent(StopEvent):
2127
timestamp: str
2228

2329

30+
class PauseEvent(InputRequiredEvent):
31+
timestamp: str
32+
33+
34+
class ResumeEvent(HumanResponseEvent):
35+
should_continue: bool
36+
37+
38+
class OkGoEvent(Event):
39+
message: str
40+
41+
2442
class DefaultWorkflow(Workflow):
2543
@step
26-
async def start(self, event: PingEvent, context: Context) -> WorkflowCompletedEvent:
44+
async def start(self, event: PingEvent, context: Context) -> OkGoEvent:
45+
return OkGoEvent(message="OK GO")
46+
47+
@step
48+
async def loop(
49+
self, event: ResumeEvent | OkGoEvent, context: Context
50+
) -> PauseEvent | WorkflowCompletedEvent:
51+
if isinstance(event, ResumeEvent) and not event.should_continue:
52+
return WorkflowCompletedEvent(
53+
timestamp="workflow completed at "
54+
+ datetime.now().isoformat(timespec="seconds")
55+
)
2756
start = time.monotonic()
28-
logger.info(f"Received message: {event.message}")
57+
logger.info(f"Received message!!!!!: {event}")
2958
for i in range(5):
30-
logger.info(f"Processing message: {event.message} {i}")
59+
logger.info(f"Processing message: {event} {i}")
3160
elapsed = (time.monotonic() - start) * 1000
3261
context.write_event_to_stream(
3362
PongEvent(message=f"+{elapsed:.0f}ms PONG {i + 1}/5 ")
3463
)
3564
await asyncio.sleep(0.2)
36-
return WorkflowCompletedEvent(
37-
timestamp="workflow completed at "
65+
66+
return PauseEvent(
67+
timestamp="workflow paused at "
3868
+ datetime.now().isoformat(timespec="seconds")
3969
)
4070

4171

42-
workflow = DefaultWorkflow()
72+
workflow = DefaultWorkflow(timeout=None)
4373

4474
if __name__ == "__main__":
4575
logging.basicConfig(level=logging.INFO)

ui/src/pages/Home.tsx

Lines changed: 63 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,24 @@
11
import { useState } from "react";
2-
import { useWorkflowHandler, useWorkflowRun } from "@llamaindex/ui";
2+
import {
3+
type WorkflowEvent,
4+
useWorkflowHandler,
5+
useWorkflowRun,
6+
} from "@llamaindex/ui";
37

48
export default function Home() {
59
const [taskId, setTaskId] = useState<string | null>(null);
610
const createHandler = useWorkflowRun();
711
return (
812
<div className="aurora-container relative min-h-screen overflow-hidden bg-background text-foreground">
9-
<main className="relative mx-auto flex min-h-screen max-w-2xl px-6 flex-col gap-4 items-center justify-center">
13+
<main className="relative mx-auto flex min-h-screen max-w-2xl px-6 flex-col gap-4 items-center justify-center my-12">
1014
<div className="text-center mb-4 text-black/80 dark:text-white/80 text-lg">
1115
This is a basic starter app for LlamaDeploy. It's running a simple
12-
workflow on the backend, and vite with react on the frontend with
13-
llama-ui to call the workflow. Customize this app with your own
14-
workflow and UI.
16+
Human-in-the-loop workflow on the backend, and vite with react on the
17+
frontend with llama-ui to call the workflow. Customize this app with
18+
your own workflow and UI.
1519
</div>
1620
<div className="flex flex-row gap-4 items-start justify-center w-full">
17-
{taskId ? (
18-
<HandlerOutput handlerId={taskId} />
19-
) : (
20-
<Output>
21-
<span className="text-black/60 dark:text-white/60">
22-
Workflow result will show here.
23-
</span>
24-
</Output>
25-
)}
21+
<HandlerOutput handlerId={taskId} />
2622
<RunButton
2723
disabled={createHandler.isCreating}
2824
onClick={() =>
@@ -71,33 +67,75 @@ function RunButton({
7167
);
7268
}
7369

74-
function HandlerOutput({ handlerId }: { handlerId: string }) {
70+
type PongEvent = { type: `${string}.PongEvent`; data: { message: string } };
71+
type PauseEvent = { type: `${string}.PauseEvent`; data: { timestamp: string } };
72+
73+
function isPongEvent(event: WorkflowEvent): event is PongEvent {
74+
return event.type.endsWith(".PongEvent");
75+
}
76+
function isPauseEvent(event: WorkflowEvent): event is PauseEvent {
77+
return event.type.endsWith(".PauseEvent");
78+
}
79+
80+
function HandlerOutput({ handlerId }: { handlerId: string | null }) {
7581
// stream events and result from the workflow
76-
const handler = useWorkflowHandler(handlerId);
82+
const handler = useWorkflowHandler(handlerId ?? "");
7783

7884
// read workflow events here
79-
const pongs = handler.events.filter((event) =>
80-
event.type.match(/PongEvent$/),
81-
) as { type: string; data: { message: string } }[];
85+
const pongsOrResume = handler.events.filter(
86+
(event) => isPongEvent(event) || isPauseEvent(event),
87+
) as (PongEvent | PauseEvent)[];
8288
const completed = handler.events.find((event) =>
83-
event.type.match(/WorkflowCompletedEvent$/),
89+
event.type.endsWith(".WorkflowCompletedEvent"),
8490
) as { type: string; data: { timestamp: string } } | undefined;
8591

8692
return (
87-
<div className="flex flex-col gap-4 w-full min-h-60">
93+
<div className="flex flex-col gap-4 w-full min-h-60 items-center">
8894
<Output>{completed ? completed.data.timestamp : "Running... "}</Output>
89-
{pongs.map((pong, index) => (
95+
{pongsOrResume.map((pong, index) => (
9096
<span
91-
className="text-black/60 dark:text-white/60 text-sm m-0"
92-
key={pong.data.message}
97+
className="inline-flex items-center px-2 py-0.5 text-xs font-medium bg-black/3
98+
dark:bg-white/2 text-black/60 dark:text-white/60 rounded border border-black/5
99+
dark:border-white/5 backdrop-blur-sm"
100+
key={index}
93101
style={{
94102
animation: "fade-in-left 80ms ease-out both",
95103
willChange: "opacity, transform",
96104
}}
97105
>
98-
{pong.data.message}
106+
{isPongEvent(pong) ? pong.data.message : pong.data.timestamp}
107+
{isPauseEvent(pong) &&
108+
index === pongsOrResume.length - 1 &&
109+
!completed && (
110+
<button
111+
onClick={() =>
112+
handler.sendEvent({
113+
type: "app.workflow.ResumeEvent",
114+
data: { should_continue: true },
115+
})
116+
}
117+
className="ml-2 px-2 py-0.5 bg-black/10 hover:bg-black/20 dark:bg-white/10 dark:hover:bg-white/20
118+
text-black/80 dark:text-white/80 text-xs rounded border border-black/10 dark:border-white/10"
119+
>
120+
Resume?
121+
</button>
122+
)}
99123
</span>
100124
))}
125+
{!completed && pongsOrResume.length > 0 && (
126+
<button
127+
onClick={() =>
128+
handler.sendEvent({
129+
type: "app.workflow.ResumeEvent",
130+
data: { should_continue: false },
131+
})
132+
}
133+
className="ml-2 px-2 py-0.5 bg-black/10 hover:bg-black/20 dark:bg-white/10 dark:hover:bg-white/20
134+
text-black/80 dark:text-white/80 text-xs rounded border border-black/10 dark:border-white/10"
135+
>
136+
Stop
137+
</button>
138+
)}
101139
</div>
102140
);
103141
}

0 commit comments

Comments
 (0)