Skip to content

Commit bfb207d

Browse files
committed
feat: add hitl-in-chat and hitl-in-chat-booking demos to ms-agent-python
Port the in-chat HITL pattern (useHumanInTheLoop) from langgraph-python. The book_call tool is defined entirely on the frontend; the MS Agent Framework agent has tools=[] and just calls it by name. The booking-flow alias reuses the same backend agent and shares the time-picker component.
1 parent c749812 commit bfb207d

6 files changed

Lines changed: 374 additions & 0 deletions

File tree

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
"""MS Agent Framework agent backing the In-Chat HITL (useHumanInTheLoop) demo.
2+
3+
The `book_call` tool is defined entirely on the frontend via
4+
`useHumanInTheLoop`. CopilotKit's runtime forwards the frontend tool
5+
definition to the agent at request time, so this agent has no backend tools
6+
of its own -- it just needs to recognize "book a call" intent and emit the
7+
tool call.
8+
9+
When the user picks a slot (or cancels), CopilotKit returns that choice as
10+
the tool result and the agent confirms in a short follow-up message.
11+
"""
12+
13+
from __future__ import annotations
14+
15+
from textwrap import dedent
16+
17+
from agent_framework import Agent, BaseChatClient
18+
from agent_framework_ag_ui import AgentFrameworkAgent
19+
20+
21+
SYSTEM_PROMPT = dedent(
22+
"""
23+
You help users book an onboarding or intro call with the sales team.
24+
25+
When the user asks to book a call, schedule a meeting, or set up a 1:1,
26+
call the frontend-provided `book_call` tool with:
27+
- `topic`: a short summary of what the call is about (e.g. 'Intro with
28+
sales', 'Q2 goals review').
29+
- `attendee`: who the call is with, if known (e.g. 'Alice from Sales').
30+
31+
The tool surfaces a time-picker UI inside the chat. The user will pick a
32+
slot or cancel. After the tool returns, send one short confirmation
33+
sentence reflecting the user's choice (or noting cancellation). Do NOT
34+
ask for approval yourself -- always call the tool and let the picker
35+
handle the decision. Keep all replies to one sentence.
36+
"""
37+
).strip()
38+
39+
40+
def create_hitl_in_chat_agent(chat_client: BaseChatClient) -> AgentFrameworkAgent:
41+
"""Instantiate the In-Chat HITL demo agent."""
42+
base_agent = Agent(
43+
client=chat_client,
44+
name="hitl_in_chat_agent",
45+
instructions=SYSTEM_PROMPT,
46+
# `book_call` is registered on the frontend via `useHumanInTheLoop`.
47+
tools=[],
48+
)
49+
50+
return AgentFrameworkAgent(
51+
agent=base_agent,
52+
name="CopilotKitMSAgentHitlInChatAgent",
53+
description=(
54+
"Scheduling assistant that delegates the time-picker interaction "
55+
"to a frontend-defined `book_call` tool rendered inline in the chat."
56+
),
57+
require_confirmation=False,
58+
)
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# In-Chat HITL — Booking Flow
2+
3+
## What This Demo Shows
4+
5+
A booking-flavored alias of the in-chat HITL pattern: the agent calls a
6+
frontend-defined `book_call` tool, the time-picker card renders inline,
7+
and the user's chosen slot is returned to the agent.
8+
9+
## How to Interact
10+
11+
Try asking your Copilot to:
12+
13+
- "Book an intro call with the sales team."
14+
- "Schedule an onboarding session for next week."
15+
16+
A time-picker card appears in the chat. Pick a slot (or cancel) and the
17+
agent confirms.
18+
19+
## Technical Details
20+
21+
Same wiring as `hitl-in-chat`: the `book_call` tool is registered via
22+
`useHumanInTheLoop`; the MS Agent Framework agent has `tools=[]` and runs
23+
on the same `/hitl-in-chat` FastAPI endpoint. The booking-flow alias
24+
reuses the time-picker component from `../hitl-in-chat/time-picker-card.tsx`.
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
"use client";
2+
3+
import React from "react";
4+
import {
5+
CopilotKit,
6+
CopilotChat,
7+
useHumanInTheLoop,
8+
useConfigureSuggestions,
9+
} from "@copilotkit/react-core/v2";
10+
import { z } from "zod";
11+
import {
12+
TimePickerCard,
13+
TimeSlot,
14+
} from "../hitl-in-chat/time-picker-card";
15+
16+
// Booking-flow alias of `hitl-in-chat` — same time-picker tool, same
17+
// backend agent. The page exists so the showcase can deep-link to a
18+
// booking-flavored variant of the in-chat HITL pattern.
19+
const DEFAULT_SLOTS: TimeSlot[] = [
20+
{ label: "Tomorrow 10:00 AM", iso: "2026-04-30T10:00:00-07:00" },
21+
{ label: "Tomorrow 2:00 PM", iso: "2026-04-30T14:00:00-07:00" },
22+
{ label: "Monday 9:00 AM", iso: "2026-05-04T09:00:00-07:00" },
23+
{ label: "Monday 3:30 PM", iso: "2026-05-04T15:30:00-07:00" },
24+
];
25+
26+
export default function HitlInChatBookingDemo() {
27+
return (
28+
<CopilotKit runtimeUrl="/api/copilotkit" agent="hitl-in-chat-booking">
29+
<div className="flex justify-center items-center h-screen w-full">
30+
<div className="h-full w-full max-w-4xl">
31+
<Chat />
32+
</div>
33+
</div>
34+
</CopilotKit>
35+
);
36+
}
37+
38+
function Chat() {
39+
useConfigureSuggestions({
40+
suggestions: [
41+
{
42+
title: "Book a sales intro",
43+
message:
44+
"I'd like to book an intro call with the sales team to discuss pricing.",
45+
},
46+
{
47+
title: "Schedule onboarding",
48+
message: "Schedule an onboarding session for next week.",
49+
},
50+
],
51+
available: "always",
52+
});
53+
54+
useHumanInTheLoop({
55+
agentId: "hitl-in-chat-booking",
56+
name: "book_call",
57+
description:
58+
"Ask the user to pick a time slot for a call. The picker UI presents fixed candidate slots; the user's choice is returned to the agent.",
59+
parameters: z.object({
60+
topic: z
61+
.string()
62+
.describe("What the call is about (e.g. 'Intro with sales')"),
63+
attendee: z
64+
.string()
65+
.describe("Who the call is with (e.g. 'Alice from Sales')"),
66+
}),
67+
render: ({ args, status, respond }: any) => (
68+
<TimePickerCard
69+
topic={args?.topic ?? "a call"}
70+
attendee={args?.attendee}
71+
slots={DEFAULT_SLOTS}
72+
status={status}
73+
onSubmit={(result) => respond?.(result)}
74+
/>
75+
),
76+
});
77+
78+
return (
79+
<CopilotChat
80+
agentId="hitl-in-chat-booking"
81+
className="h-full rounded-2xl"
82+
/>
83+
);
84+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# In-Chat Human in the Loop (useHumanInTheLoop)
2+
3+
## What This Demo Shows
4+
5+
Inline approval/decision UI rendered directly inside the chat via the
6+
high-level `useHumanInTheLoop` hook — the agent calls a frontend-defined
7+
`book_call` tool, the picker UI renders inline, and the user's choice is
8+
returned to the agent.
9+
10+
## How to Interact
11+
12+
Try asking your Copilot to:
13+
14+
- "Please book an intro call with the sales team."
15+
- "Schedule a 1:1 with Alice next week to review Q2 goals."
16+
17+
A time-picker card appears in the chat. Pick a slot (or cancel) and the
18+
agent confirms the result.
19+
20+
## Technical Details
21+
22+
- The `book_call` tool is defined entirely on the frontend via
23+
`useHumanInTheLoop`; the MS Agent Framework agent has `tools=[]` and just
24+
calls the tool by name.
25+
- CopilotKit forwards the user's chosen slot (or `{ cancelled: true }`) back
26+
to the agent as the tool result, which the agent reflects in its
27+
follow-up message.
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
"use client";
2+
3+
import React from "react";
4+
import {
5+
CopilotKit,
6+
CopilotChat,
7+
useHumanInTheLoop,
8+
useConfigureSuggestions,
9+
} from "@copilotkit/react-core/v2";
10+
import { z } from "zod";
11+
import { TimePickerCard, TimeSlot } from "./time-picker-card";
12+
13+
// @region[time-slots]
14+
const DEFAULT_SLOTS: TimeSlot[] = [
15+
{ label: "Tomorrow 10:00 AM", iso: "2026-04-30T10:00:00-07:00" },
16+
{ label: "Tomorrow 2:00 PM", iso: "2026-04-30T14:00:00-07:00" },
17+
{ label: "Monday 9:00 AM", iso: "2026-05-04T09:00:00-07:00" },
18+
{ label: "Monday 3:30 PM", iso: "2026-05-04T15:30:00-07:00" },
19+
];
20+
// @endregion[time-slots]
21+
22+
export default function HitlInChatDemo() {
23+
return (
24+
<CopilotKit runtimeUrl="/api/copilotkit" agent="hitl-in-chat">
25+
<div className="flex justify-center items-center h-screen w-full">
26+
<div className="h-full w-full max-w-4xl">
27+
<Chat />
28+
</div>
29+
</div>
30+
</CopilotKit>
31+
);
32+
}
33+
34+
function Chat() {
35+
useConfigureSuggestions({
36+
suggestions: [
37+
{
38+
title: "Book a call with sales",
39+
message:
40+
"Please book an intro call with the sales team to discuss pricing.",
41+
},
42+
{
43+
title: "Schedule a 1:1 with Alice",
44+
message: "Schedule a 1:1 with Alice next week to review Q2 goals.",
45+
},
46+
],
47+
available: "always",
48+
});
49+
50+
// @region[hitl-hook]
51+
useHumanInTheLoop({
52+
agentId: "hitl-in-chat",
53+
name: "book_call",
54+
description:
55+
"Ask the user to pick a time slot for a call. The picker UI presents fixed candidate slots; the user's choice is returned to the agent.",
56+
parameters: z.object({
57+
topic: z
58+
.string()
59+
.describe("What the call is about (e.g. 'Intro with sales')"),
60+
attendee: z
61+
.string()
62+
.describe("Who the call is with (e.g. 'Alice from Sales')"),
63+
}),
64+
render: ({ args, status, respond }: any) => (
65+
<TimePickerCard
66+
topic={args?.topic ?? "a call"}
67+
attendee={args?.attendee}
68+
slots={DEFAULT_SLOTS}
69+
status={status}
70+
onSubmit={(result) => respond?.(result)}
71+
/>
72+
),
73+
});
74+
// @endregion[hitl-hook]
75+
76+
return <CopilotChat agentId="hitl-in-chat" className="h-full rounded-2xl" />;
77+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
"use client";
2+
3+
import React, { useState } from "react";
4+
5+
export interface TimeSlot {
6+
label: string;
7+
iso: string;
8+
}
9+
10+
export type TimePickerStatus = "inProgress" | "executing" | "complete";
11+
12+
export interface TimePickerCardProps {
13+
topic: string;
14+
attendee?: string;
15+
slots: TimeSlot[];
16+
status: TimePickerStatus;
17+
onSubmit: (
18+
result: { chosen_time: string; chosen_label: string } | { cancelled: true },
19+
) => void;
20+
}
21+
22+
/**
23+
* Renders a "Book a call" card with a small grid of time slots.
24+
* The user picks one slot (or cancels); that resolution is forwarded back
25+
* to the agent via the onSubmit callback wired up by `useHumanInTheLoop`.
26+
*/
27+
export function TimePickerCard({
28+
topic,
29+
attendee,
30+
slots,
31+
status,
32+
onSubmit,
33+
}: TimePickerCardProps) {
34+
const [picked, setPicked] = useState<TimeSlot | null>(null);
35+
const [cancelled, setCancelled] = useState(false);
36+
const disabled = status !== "executing" || picked !== null || cancelled;
37+
38+
if (cancelled) {
39+
return (
40+
<div
41+
className="rounded-2xl border border-[#DBDBE5] bg-[#F7F7F9] p-4 text-sm text-[#57575B] max-w-md"
42+
data-testid="time-picker-cancelled"
43+
>
44+
Cancelled — no time picked.
45+
</div>
46+
);
47+
}
48+
49+
if (picked) {
50+
return (
51+
<div
52+
className="rounded-2xl border border-[#85ECCE4D] bg-[#85ECCE]/10 p-4 max-w-md"
53+
data-testid="time-picker-picked"
54+
>
55+
<p className="text-sm text-[#010507]">
56+
Booked for <span className="font-semibold">{picked.label}</span>
57+
</p>
58+
</div>
59+
);
60+
}
61+
62+
return (
63+
<div
64+
className="rounded-2xl border border-[#DBDBE5] bg-white p-5 shadow-sm max-w-md"
65+
data-testid="time-picker-card"
66+
>
67+
<p className="text-[10px] uppercase tracking-[0.14em] text-[#57575B] font-medium">
68+
Book a call
69+
</p>
70+
<h3 className="text-base font-semibold text-[#010507] mt-1.5">{topic}</h3>
71+
{attendee && (
72+
<p className="text-sm text-[#57575B] mt-0.5">With {attendee}</p>
73+
)}
74+
75+
<p className="text-sm text-[#57575B] mt-4 mb-2">Pick a time:</p>
76+
<div className="grid grid-cols-2 gap-2">
77+
{slots.map((s) => (
78+
<button
79+
key={s.iso}
80+
disabled={disabled}
81+
data-testid="time-picker-slot"
82+
onClick={() => {
83+
setPicked(s);
84+
onSubmit({ chosen_time: s.iso, chosen_label: s.label });
85+
}}
86+
className="rounded-xl border border-[#DBDBE5] bg-white px-3 py-2 text-sm font-medium text-[#010507] hover:border-[#BEC2FF] hover:bg-[#BEC2FF1A] disabled:opacity-50 transition-colors"
87+
>
88+
{s.label}
89+
</button>
90+
))}
91+
</div>
92+
<button
93+
disabled={disabled}
94+
onClick={() => {
95+
setCancelled(true);
96+
onSubmit({ cancelled: true });
97+
}}
98+
className="mt-3 w-full rounded-xl border border-[#E9E9EF] px-3 py-1.5 text-xs text-[#838389] hover:bg-[#FAFAFC] disabled:opacity-50 transition-colors"
99+
>
100+
None of these work
101+
</button>
102+
</div>
103+
);
104+
}

0 commit comments

Comments
 (0)