diff --git a/apps/dojo/src/agents.ts b/apps/dojo/src/agents.ts index 0d650c62b..7ff8d7309 100644 --- a/apps/dojo/src/agents.ts +++ b/apps/dojo/src/agents.ts @@ -377,6 +377,11 @@ export const agentsIntegrations = { predictive_state_updates: "predictive_state_updates", shared_state: "shared_state", tool_based_generative_ui: "tool_based_generative_ui", + tool_confirmation: "tool_confirmation", + user_input: "user_input", + backend_feedback: "backend_feedback", + team_human_in_the_loop: "team_human_in_the_loop", + nested_team_chat: "nested_team_chat", }, ), diff --git a/apps/dojo/src/app/[integrationId]/feature/(v2)/backend_feedback/README.mdx b/apps/dojo/src/app/[integrationId]/feature/(v2)/backend_feedback/README.mdx new file mode 100644 index 000000000..bab14a273 --- /dev/null +++ b/apps/dojo/src/app/[integrationId]/feature/(v2)/backend_feedback/README.mdx @@ -0,0 +1,25 @@ +# Multiple Choice Feedback + +## What This Demo Shows + +This demo showcases the **multiple choice feedback** HITL pattern: + +1. **Option Selection**: Agent presents choices for user to select from +2. **Radio Buttons**: User picks one option from a list +3. **Frontend-Defined Tools**: Tools are registered via `useHumanInTheLoop` hook + +## How to Interact + +1. Ask your Copilot for a recommendation: + - "Help me pick a cuisine for dinner" + - "Help me plan my weekend activities" + +2. A card with multiple options will appear + +3. Select an option and click **Confirm Selection** + +## Technical Details + +- Backend: Minimal agent with no tools (tools come from frontend) +- Frontend: Uses `useHumanInTheLoop` hook with `MultipleChoiceCard` +- The agent receives the selected choice and responds accordingly diff --git a/apps/dojo/src/app/[integrationId]/feature/(v2)/backend_feedback/page.tsx b/apps/dojo/src/app/[integrationId]/feature/(v2)/backend_feedback/page.tsx new file mode 100644 index 000000000..b1a30ba6e --- /dev/null +++ b/apps/dojo/src/app/[integrationId]/feature/(v2)/backend_feedback/page.tsx @@ -0,0 +1,68 @@ +"use client"; +import React from "react"; +import "@copilotkit/react-core/v2/styles.css"; +import { + useHumanInTheLoop, + useConfigureSuggestions, + CopilotChat, + CopilotChatConfigurationProvider, +} from "@copilotkit/react-core/v2"; +import { CopilotKit } from "@copilotkit/react-core"; +import { z } from "zod"; +import { MultipleChoiceCard } from "../hitl-components"; + +interface BackendFeedbackProps { + params: Promise<{ integrationId: string }>; +} + +const BackendFeedback: React.FC = ({ params }) => { + const { integrationId } = React.use(params); + + return ( + + + + + + ); +}; + +const ChatContent = () => { + useConfigureSuggestions({ + suggestions: [ + { title: "Restaurant", message: "Help me pick a cuisine for dinner" }, + { title: "Weekend", message: "Help me plan my weekend activities" }, + ], + available: "always", + }); + + useHumanInTheLoop({ + agentId: "backend_feedback", + name: "get_user_choice", + description: "Present options to the user and get their selection", + parameters: z.object({ + question: z.string().describe("The question to ask the user"), + options: z.array(z.string()).describe("Array of options for the user to choose from"), + }), + render: ({ args, respond, status }: any) => ( + + ), + }); + + return ( +
+
+ +
+
+ ); +}; + +export default BackendFeedback; diff --git a/apps/dojo/src/app/[integrationId]/feature/(v2)/hitl-components.tsx b/apps/dojo/src/app/[integrationId]/feature/(v2)/hitl-components.tsx new file mode 100644 index 000000000..082cacc39 --- /dev/null +++ b/apps/dojo/src/app/[integrationId]/feature/(v2)/hitl-components.tsx @@ -0,0 +1,481 @@ +"use client"; +import React, { useState } from "react"; +import { useTheme } from "next-themes"; + +// Shared card wrapper with theme support +export const HITLCard = ({ + children, + width = "w-[450px]", +}: { + children: React.ReactNode; + width?: string; +}) => { + const { theme } = useTheme(); + return ( +
+
+ {children} +
+
+ ); +}; + +// Status badge shown after action is taken +export const StatusBadge = ({ + variant, + icon, + label, +}: { + variant: "success" | "error" | "info"; + icon: string; + label: string; +}) => { + const { theme } = useTheme(); + const colors = { + success: + theme === "dark" + ? "bg-green-900/30 text-green-300 border border-green-500/30" + : "bg-green-50 text-green-700 border border-green-200", + error: + theme === "dark" + ? "bg-red-900/30 text-red-300 border border-red-500/30" + : "bg-red-50 text-red-700 border border-red-200", + info: + theme === "dark" + ? "bg-blue-900/30 text-blue-300 border border-blue-500/30" + : "bg-blue-50 text-blue-700 border border-blue-200", + }; + + return ( +
+
+ {icon} {label} +
+
+ ); +}; + +// Card header with title and subtitle +export const CardHeader = ({ title, subtitle }: { title: string; subtitle?: string }) => { + const { theme } = useTheme(); + return ( +
+

{title}

+ {subtitle && ( +

{subtitle}

+ )} +
+ ); +}; + +// Primary action button +export const ActionButton = ({ + onClick, + disabled, + variant = "primary", + children, +}: { + onClick: () => void; + disabled?: boolean; + variant?: "primary" | "secondary" | "success" | "danger"; + children: React.ReactNode; +}) => { + const { theme } = useTheme(); + const variants = { + primary: + "bg-gradient-to-r from-blue-500 to-blue-600 hover:from-blue-600 hover:to-blue-700 text-white", + secondary: + theme === "dark" + ? "bg-slate-700 hover:bg-slate-600 text-white border border-slate-600" + : "bg-gray-100 hover:bg-gray-200 text-gray-800 border border-gray-300", + success: + "bg-gradient-to-r from-green-500 to-emerald-600 hover:from-green-600 hover:to-emerald-700 text-white", + danger: + "bg-gradient-to-r from-red-500 to-red-600 hover:from-red-600 hover:to-red-700 text-white", + }; + + return ( + + ); +}; + +// Text input field +export const TextInput = ({ + value, + onChange, + placeholder, + disabled, + type = "text", +}: { + value: string; + onChange: (value: string) => void; + placeholder?: string; + disabled?: boolean; + type?: "text" | "password"; +}) => { + const { theme } = useTheme(); + return ( + onChange(e.target.value)} + placeholder={placeholder} + disabled={disabled} + className={`w-full p-3 rounded-lg border ${ + theme === "dark" + ? "bg-slate-800 border-slate-600 text-white placeholder-slate-500" + : "bg-white border-gray-300 text-gray-800 placeholder-gray-400" + } focus:outline-none focus:ring-2 focus:ring-blue-500`} + /> + ); +}; + +// Detail row for displaying key-value pairs +export const DetailRow = ({ label, value }: { label: string; value: string }) => { + const { theme } = useTheme(); + return ( +
+ + {label}: + +

{value}

+
+ ); +}; + +// Details section with background +export const DetailsSection = ({ children }: { children: React.ReactNode }) => { + const { theme } = useTheme(); + return ( +
+ {children} +
+ ); +}; + +// Multiple choice option +export const ChoiceOption = ({ + label, + selected, + onClick, + disabled, +}: { + label: string; + selected: boolean; + onClick: () => void; + disabled?: boolean; +}) => { + const { theme } = useTheme(); + return ( + + ); +}; + +// ─── Pre-built HITL Cards ──────────────────────────────────────────── + +// Email confirmation card (approve/reject) +export const EmailConfirmationCard = ({ + args, + respond, + status, +}: { + args: { to?: string; subject?: string; body?: string }; + respond: (result: any) => void; + status: string; +}) => { + const [decision, setDecision] = useState<"approved" | "rejected" | null>(null); + + const handleApprove = () => { + setDecision("approved"); + // Backend expects {"accepted": true/false} for HITL confirmation + respond({ accepted: true }); + }; + + const handleReject = () => { + setDecision("rejected"); + respond({ accepted: false }); + }; + + if (!args?.to) return null; + + return ( + + {decision ? ( + + ) : ( + <> + + + + {args.subject && } + {args.body && } + +
+ + ✗ Cancel + + + ✓ Send Email + +
+ + )} +
+ ); +}; + +// Generic confirmation card (approve/reject any action) +export const ConfirmationCard = ({ + args, + respond, + status, +}: { + args: { action?: string; description?: string; details?: Record }; + respond: (result: any) => void; + status: string; +}) => { + const [decision, setDecision] = useState<"approved" | "rejected" | null>(null); + + const handleApprove = () => { + setDecision("approved"); + respond({ approved: true, result: `Action "${args?.action}" approved` }); + }; + + const handleReject = () => { + setDecision("rejected"); + respond({ approved: false, result: `Action "${args?.action}" rejected by user` }); + }; + + return ( + + {decision ? ( + + ) : ( + <> + + {args?.details && ( + + {Object.entries(args.details).map(([key, value]) => ( + + ))} + + )} +
+ + ✗ Reject + + + ✓ Approve + +
+ + )} +
+ ); +}; + +// Text input card +export const TextInputCard = ({ + args, + respond, + status, +}: { + args: { prompt?: string; placeholder?: string }; + respond: (result: any) => void; + status: string; +}) => { + const [value, setValue] = useState(""); + const [submitted, setSubmitted] = useState(false); + + const handleSubmit = () => { + setSubmitted(true); + respond({ text: value }); + }; + + return ( + + {submitted ? ( + + ) : ( + <> + +
+ +
+
+ + Submit + +
+ + )} +
+ ); +}; + +// Secret/password input card +export const SecretInputCard = ({ + args, + respond, + status, +}: { + args: { prompt?: string; service?: string }; + respond: (result: any) => void; + status: string; +}) => { + const [value, setValue] = useState(""); + const [submitted, setSubmitted] = useState(false); + + const handleSubmit = () => { + setSubmitted(true); + const masked = value.length > 8 ? value.slice(0, 4) + "..." + value.slice(-4) : "****"; + respond({ secret: value, masked }); + }; + + return ( + + {submitted ? ( + + ) : ( + <> + +
+ +
+
+ + Configure + +
+ + )} +
+ ); +}; + +// Multiple choice card +export const MultipleChoiceCard = ({ + args, + respond, + status, +}: { + args: { question?: string; options?: string[] }; + respond: (result: any) => void; + status: string; +}) => { + const [selected, setSelected] = useState(null); + const [submitted, setSubmitted] = useState(false); + + const handleSubmit = () => { + if (selected) { + setSubmitted(true); + respond({ choice: selected }); + } + }; + + const options = args?.options || []; + + return ( + + {submitted ? ( + + ) : ( + <> + +
+ {options.map((option) => ( + setSelected(option)} + disabled={status !== "executing"} + /> + ))} +
+
+ + Confirm Selection + +
+ + )} +
+ ); +}; diff --git a/apps/dojo/src/app/[integrationId]/feature/(v2)/nested_team_chat/README.mdx b/apps/dojo/src/app/[integrationId]/feature/(v2)/nested_team_chat/README.mdx new file mode 100644 index 000000000..b1e9553ce --- /dev/null +++ b/apps/dojo/src/app/[integrationId]/feature/(v2)/nested_team_chat/README.mdx @@ -0,0 +1,26 @@ +# 🤝 Nested Team Chat + +## What This Demo Shows + +This demo showcases **nested team agents** working together in a chat interface: + +1. **Multi-Agent Collaboration**: Multiple specialized agents coordinate on tasks +2. **Team Hierarchy**: Teams can contain sub-teams for complex workflows +3. **Seamless Handoffs**: Agents delegate to sub-teams when specialized help is needed + +## How to Interact + +Try asking: + +- "Research the latest AI developments and summarize them" +- "Help me analyze this problem from multiple perspectives" +- "Coordinate a complex task that needs multiple specialists" + +## ✨ Nested Teams in Action + +**What's happening technically:** + +- A supervisor agent routes tasks to specialized sub-teams +- Each sub-team has its own agents with specific capabilities +- Results flow back up through the team hierarchy +- The final response synthesizes all contributions diff --git a/apps/dojo/src/app/[integrationId]/feature/(v2)/nested_team_chat/page.tsx b/apps/dojo/src/app/[integrationId]/feature/(v2)/nested_team_chat/page.tsx new file mode 100644 index 000000000..ec66df7b0 --- /dev/null +++ b/apps/dojo/src/app/[integrationId]/feature/(v2)/nested_team_chat/page.tsx @@ -0,0 +1,63 @@ +"use client"; +import React from "react"; +import "@copilotkit/react-core/v2/styles.css"; +import { + CopilotChat, + CopilotChatAssistantMessage, + type CopilotChatAssistantMessageProps, +} from "@copilotkit/react-core/v2"; +import { CopilotKit } from "@copilotkit/react-core"; +import type { AssistantMessage } from "@ag-ui/core"; + +interface NestedTeamChatProps { + params: Promise<{ + integrationId: string; + }>; +} + +function TeamAssistantMessage(props: CopilotChatAssistantMessageProps) { + const { message } = props; + const name = (message as AssistantMessage & { name?: string }).name; + + return ( +
+ {name && ( +
+ + {name} + +
+ )} + +
+ ); +} + +// Type assertion to satisfy SlotValue — the component works at runtime +const TeamAssistantMessageSlot = TeamAssistantMessage as unknown as typeof CopilotChatAssistantMessage; + +const NestedTeamChat: React.FC = ({ params }) => { + const { integrationId } = React.use(params); + + return ( + +
+
+ +
+
+
+ ); +}; + +export default NestedTeamChat; diff --git a/apps/dojo/src/app/[integrationId]/feature/(v2)/team_human_in_the_loop/README.mdx b/apps/dojo/src/app/[integrationId]/feature/(v2)/team_human_in_the_loop/README.mdx new file mode 100644 index 000000000..c97fc3e32 --- /dev/null +++ b/apps/dojo/src/app/[integrationId]/feature/(v2)/team_human_in_the_loop/README.mdx @@ -0,0 +1,27 @@ +# Team Human-in-the-Loop + +## What This Demo Shows + +This demo showcases **HITL with teams**: + +1. **Team Routing**: Leader routes to appropriate member agents +2. **Member Pause**: Member's tool with `requires_confirmation=True` pauses the team +3. **Confirmation Surfaces**: Team member pauses are surfaced to the UI for user action + +## How to Interact + +1. Ask the team to send an email: + - "Email alice@example.com to say the quarterly report is ready" + +2. The team routes to the Emailer member + +3. A confirmation card appears for the email + +4. Click **Send Email** to approve or **Cancel** to reject + +## Technical Details + +- Backend: Team with Researcher and Emailer members +- Emailer has `send_email` tool with `requires_confirmation=True` +- Team pauses are emitted from `TeamRunPausedEvent.active_requirements` +- Frontend receives the same confirmation UI as single-agent HITL diff --git a/apps/dojo/src/app/[integrationId]/feature/(v2)/team_human_in_the_loop/page.tsx b/apps/dojo/src/app/[integrationId]/feature/(v2)/team_human_in_the_loop/page.tsx new file mode 100644 index 000000000..6d4acaf60 --- /dev/null +++ b/apps/dojo/src/app/[integrationId]/feature/(v2)/team_human_in_the_loop/page.tsx @@ -0,0 +1,72 @@ +"use client"; +import React from "react"; +import "@copilotkit/react-core/v2/styles.css"; +import { + useConfigureSuggestions, + useHumanInTheLoop, + CopilotChat, + CopilotChatConfigurationProvider, +} from "@copilotkit/react-core/v2"; +import { CopilotKit } from "@copilotkit/react-core"; +import { z } from "zod"; +import { EmailConfirmationCard } from "../hitl-components"; + +interface TeamHumanInTheLoopProps { + params: Promise<{ integrationId: string }>; +} + +const TeamHumanInTheLoop: React.FC = ({ params }) => { + const { integrationId } = React.use(params); + + return ( + + + + + + ); +}; + +const ChatContent = () => { + useConfigureSuggestions({ + suggestions: [ + { title: "Send email", message: "Email alice@example.com to say the quarterly report is ready" }, + { title: "Research", message: "What is the capital of France?" }, + ], + available: "always", + }); + + // Test: Register frontend tool with same name as backend tool + // CopilotKit uses "last-in wins" - frontend tool takes priority over backend + // When agent calls send_email, frontend intercepts it and shows HITL UI + useHumanInTheLoop({ + agentId: "team_human_in_the_loop", + name: "send_email", + description: "Send an email (requires user confirmation)", + parameters: z.object({ + to: z.string().describe("Recipient email address"), + subject: z.string().describe("Email subject line"), + body: z.string().describe("Email body content"), + }), + render: ({ args, respond, status }: any) => ( + + ), + }); + + return ( +
+
+ +
+
+ ); +}; + +export default TeamHumanInTheLoop; diff --git a/apps/dojo/src/app/[integrationId]/feature/(v2)/tool_confirmation/README.mdx b/apps/dojo/src/app/[integrationId]/feature/(v2)/tool_confirmation/README.mdx new file mode 100644 index 000000000..85bf030dc --- /dev/null +++ b/apps/dojo/src/app/[integrationId]/feature/(v2)/tool_confirmation/README.mdx @@ -0,0 +1,25 @@ +# Tool Confirmation + +## What This Demo Shows + +This demo showcases the **tool confirmation** HITL pattern: + +1. **Confirmation UI**: Tools marked with `requires_confirmation=True` pause before execution +2. **Approve/Reject**: User reviews the tool call arguments and decides to proceed or cancel +3. **Frontend-Defined Tools**: Tools are registered via `useHumanInTheLoop` hook + +## How to Interact + +1. Ask your Copilot to send an email: + - "Send an email to alice@example.com saying hello" + - "Delete all temporary files" + +2. Review the confirmation card that appears + +3. Click **Send Email** to approve or **Cancel** to reject + +## Technical Details + +- Backend: Minimal agent with no tools (tools come from frontend) +- Frontend: Uses `useHumanInTheLoop` hook with custom `EmailConfirmationCard` +- Response flows back via the `respond()` callback diff --git a/apps/dojo/src/app/[integrationId]/feature/(v2)/tool_confirmation/page.tsx b/apps/dojo/src/app/[integrationId]/feature/(v2)/tool_confirmation/page.tsx new file mode 100644 index 000000000..f51a1bb3c --- /dev/null +++ b/apps/dojo/src/app/[integrationId]/feature/(v2)/tool_confirmation/page.tsx @@ -0,0 +1,83 @@ +"use client"; +import React from "react"; +import "@copilotkit/react-core/v2/styles.css"; +import { + useHumanInTheLoop, + useConfigureSuggestions, + CopilotChat, + CopilotChatConfigurationProvider, +} from "@copilotkit/react-core/v2"; +import { CopilotKit } from "@copilotkit/react-core"; +import { z } from "zod"; +import { EmailConfirmationCard, ConfirmationCard } from "../hitl-components"; + +interface ToolConfirmationProps { + params: Promise<{ integrationId: string }>; +} + +const ToolConfirmation: React.FC = ({ params }) => { + const { integrationId } = React.use(params); + + return ( + + + + + + ); +}; + +const ChatContent = () => { + useConfigureSuggestions({ + suggestions: [ + { title: "Send email", message: "Send an email to alice@example.com saying hello" }, + { title: "Delete files", message: "Delete all temporary files from the project" }, + ], + available: "always", + }); + + useHumanInTheLoop({ + agentId: "tool_confirmation", + name: "send_email", + description: "Send an email (requires user confirmation before sending)", + parameters: z.object({ + to: z.string().describe("Recipient email address"), + subject: z.string().describe("Email subject line"), + body: z.string().describe("Email body content"), + }), + render: ({ args, respond, status }: any) => ( + + ), + }); + + useHumanInTheLoop({ + agentId: "tool_confirmation", + name: "delete_files", + description: "Delete files (requires user confirmation before deletion)", + parameters: z.object({ + action: z.string().describe("Description of the delete action"), + description: z.string().describe("What files will be deleted"), + details: z.record(z.string()).optional().describe("Additional details about the files"), + }), + render: ({ args, respond, status }: any) => ( + + ), + }); + + return ( +
+
+ +
+
+ ); +}; + +export default ToolConfirmation; diff --git a/apps/dojo/src/app/[integrationId]/feature/(v2)/user_input/README.mdx b/apps/dojo/src/app/[integrationId]/feature/(v2)/user_input/README.mdx new file mode 100644 index 000000000..a10b6c386 --- /dev/null +++ b/apps/dojo/src/app/[integrationId]/feature/(v2)/user_input/README.mdx @@ -0,0 +1,25 @@ +# User Input Required + +## What This Demo Shows + +This demo showcases the **user input** HITL pattern: + +1. **Text Input**: Tools that need user-provided text data +2. **Secret Input**: Secure password/API key entry +3. **Frontend-Defined Tools**: Tools are registered via `useHumanInTheLoop` hook + +## How to Interact + +1. Ask your Copilot to configure something: + - "Search the database for customer records" + - "Configure my OpenAI API key" + +2. A text input or password field will appear + +3. Enter your response and click **Submit** or **Configure** + +## Technical Details + +- Backend: Minimal agent with no tools (tools come from frontend) +- Frontend: Uses `useHumanInTheLoop` hook with `TextInputCard` and `SecretInputCard` +- Secret input uses `type="password"` and masks the value in the response diff --git a/apps/dojo/src/app/[integrationId]/feature/(v2)/user_input/page.tsx b/apps/dojo/src/app/[integrationId]/feature/(v2)/user_input/page.tsx new file mode 100644 index 000000000..b3f478ea0 --- /dev/null +++ b/apps/dojo/src/app/[integrationId]/feature/(v2)/user_input/page.tsx @@ -0,0 +1,81 @@ +"use client"; +import React from "react"; +import "@copilotkit/react-core/v2/styles.css"; +import { + useHumanInTheLoop, + useConfigureSuggestions, + CopilotChat, + CopilotChatConfigurationProvider, +} from "@copilotkit/react-core/v2"; +import { CopilotKit } from "@copilotkit/react-core"; +import { z } from "zod"; +import { TextInputCard, SecretInputCard } from "../hitl-components"; + +interface UserInputProps { + params: Promise<{ integrationId: string }>; +} + +const UserInput: React.FC = ({ params }) => { + const { integrationId } = React.use(params); + + return ( + + + + + + ); +}; + +const ChatContent = () => { + useConfigureSuggestions({ + suggestions: [ + { title: "Search", message: "Search the database for customer records" }, + { title: "API Key", message: "Configure my OpenAI API key" }, + ], + available: "always", + }); + + useHumanInTheLoop({ + agentId: "user_input", + name: "get_user_text", + description: "Get text input from the user", + parameters: z.object({ + prompt: z.string().describe("The prompt to show the user"), + placeholder: z.string().optional().describe("Placeholder text for the input field"), + }), + render: ({ args, respond, status }: any) => ( + + ), + }); + + useHumanInTheLoop({ + agentId: "user_input", + name: "get_secret_input", + description: "Get sensitive input like API keys or passwords from the user", + parameters: z.object({ + prompt: z.string().describe("The prompt to show the user"), + service: z.string().optional().describe("Name of the service (e.g., OpenAI, Stripe)"), + }), + render: ({ args, respond, status }: any) => ( + + ), + }); + + return ( +
+
+ +
+
+ ); +}; + +export default UserInput; diff --git a/apps/dojo/src/menu.ts b/apps/dojo/src/menu.ts index f7cef402a..30d3fb830 100644 --- a/apps/dojo/src/menu.ts +++ b/apps/dojo/src/menu.ts @@ -254,6 +254,11 @@ export const menuIntegrations = [ "predictive_state_updates", "shared_state", "tool_based_generative_ui", + "tool_confirmation", + "user_input", + "backend_feedback", + "team_human_in_the_loop", + "nested_team_chat", ], }, {