DEV Community

Cover image for How To Build Full-Stack AI Agents 🚀 (CrewAI + CopilotKit)
Bonnie for CopilotKit

Posted on with Nathan Tarbert • Edited on • Originally published at copilotkit.ai

96 42 41 42 40

How To Build Full-Stack AI Agents 🚀 (CrewAI + CopilotKit)

TL;DR

In this article, you will learn how to build a full-stack restaurant finder AI agent that combines human-in-the-loop capabilities using CrewAI, CopilotKit, and Serper.

Before we jump in, here is what we will cover:

  • What are CrewAI agents?

  • Building, running, and deploying a CrewAI agent

  • Adding frontend UI to a CrewAI agent using Copilot Cloud and CopilotKit

Here’s a preview of the application we’ll be building.

What are CrewAI agents?

Imagine you’re working on a big group project, like building an app or a game. You’ve got a team, and each person has a specific job.

One is great at coding the backend, another designs the UI, someone else handles testing, and so on.

Instead, people picture CrewAI agents as a team of smart, automated helpers—each with its own role—working together to solve a problem or complete a task.

You can learn more about CrewAI agents here on CrewAI docs

Crew

What is CopilotKit

CopilotKit is an open-source, full-stack framework for building user-interactive agents and copilots. It enables your agents to take control of your application, communicate what it's doing, and generate a completely custom UI.

CopilotKit

Check out CopilotKit's GitHub ⭐️

Prerequisites

To fully understand this tutorial, you need to have a basic understanding of React or Next.js.

We'll also make use of the following:

  • Python - a popular programming language for building AI agents with LangGraph; make sure it is installed on your computer.

  • OpenAI API - to enable us to perform various tasks using the GPT models; for this tutorial, ensure you have access to the GPT-4 model.

  • CopilotKit - an open-source copilot framework for building custom AI chatbots, in-app AI agents, and text areas.

  • CrewAI - a Python framework that enables developers to create autonomous AI agents with high-level simplicity and precise low-level control.

  • SerperTool - a tool that utilizes the serper.dev API to fetch and display the most relevant search results based on the query provided by the user.

Building, Running, and Deploying a CrewAI Agent

In this section, you will learn how to build and run a CrewAI agent using the CrewAI package. Then you will learn how to deploy the AI agent to the CrewAI enterprise platform using GitHub.

Let’s jump in.

Step 1: Building a CrewAI Crew Agent

First, start by cloning the restaurant finder crew repository, which contains code for a Python-based CrewAI Crew agent:

git clone https://github.com/TheGreatBonnie/restaurant-finder.git
Enter fullscreen mode Exit fullscreen mode

The repository contains a CrewAI Crew agent with the following structure:

restaurant-finder/
├── .gitignore
├── pyproject.toml
├── README.md
├── .env
└── src/
       └── restaurant_finder_agent/
              ├── init.py
              ├── main.py
              ├── crew.py
              ├── tools/
                 ├── custom_tool.py
                 └── init.py
              └── config/
                     ├── agents.yaml
                     └── tasks.yaml
Enter fullscreen mode Exit fullscreen mode

Below are the essential files in the research finder CrewAI Crew:

  • agents.yaml - Define your AI agents and their roles

  • tasks.yaml - Set up agent tasks and workflows

  • .env - Store API keys and environment variables

  • main.py - Project entry point and execution flow

  • crew.py - Crew orchestration and coordination

  • tools/ - Directory for custom agent tools

Next, create a .env file in the root directory. Then add OpenAI and Serper API keys into the environment variables.

OPENAI_API_KEY=your-openai-api-key
SERPER_API_KEY=your-serper-api-key
Enter fullscreen mode Exit fullscreen mode

Then install all the research finder CrewAI Crew dependencies using CrewAI. If you don’t have the CrewAI package installed, follow this installation guide on CrewAI docs.

crewai install
Enter fullscreen mode Exit fullscreen mode

Step 2: Running a CrewAI Crew Agent

To run the restaurant CrewAI crew agent, execute the following command in the command line.

crewai run
Enter fullscreen mode Exit fullscreen mode

Once the crew has started, it will use the Serper web search tool to find restaurants in San Francisco. Then it will curate a list of restaurants and ask for your feedback, as shown below.

Image

Reply with a “Looks good” message in the terminal and press enter. A file with restaurant recommendations in San Francisco should be saved in the project folder, as shown below.

Image

Step 3: Deploying a CrewAI Crew Agent

To deploy the restaurant crew, push the restaurant finder crew code to a GitHub repository, as shown below.

Image

Then log in to CrewAI. On your dashboard, configure your GitHub account with CrewAI to access the restaurant finder repository.

Image

Next, select the restaurant finder repository. Then add OpenAI and Serper API keys to the environment variables, as shown below, and click the Deploy button.

Image

The restaurant finder crew should start deploying immediately, as shown below. Note that the first crew deployment can take as long as 10 minutes.

Image

Once the crew has been deployed, open it, then grab its URL and bearer token. The crew’s URL and bearer token will be used to register the crew with Copilot Cloud.

Image

Now that we have learned how to build, run, and deploy a CrewAI crew agent, let’s see how we can add a frontend UI to chat with it.

Adding frontend UI to a CrewAI crew agent using Copilot Cloud and CopilotKit

In this section, you will learn how to add a frontend UI to a CrewAI crew agent using Copilot Cloud and CopilotKit.

Let’s get started.

Step 1: Register a CrewAI agent with Copilot Cloud

To register a CrewAI crew agent, go to Copilot Cloud, sign in, and click the Get Started button.

Image

Then add your OpenAI API key to the "Provide OpenAI API Key" section, as shown below.

Image

Next, scroll down to the remote endpoints section and click the Add New button.

Image

Then select the remote endpoint from the modal that pops up. After that, add your CrewAI agent endpoint URL, bearer token, name, and description, as shown below. Then click the Save Endpoint button.

Image

Once the crew endpoint is saved, copy the Copilot Cloud Public API Key, as shown below.

Image

Step 2: Building a CrewAI agent frontend UI

To build a CrewAI agent frontend UI, start by cloning the restaurant finder UI repository, which contains code for a Next.js project:

git clone https://github.com/TheGreatBonnie/restaurant-finder-ui.git
Enter fullscreen mode Exit fullscreen mode

Next, create a .env file in the root directory. Then add your CrewAI agent name and Copilot Cloud Public API Key into the environment variables.

NEXT_PUBLIC_AGENT_NAME=restaurant_finder
NEXT_PUBLIC_CPK_PUBLIC_API_KEY=your-copilot-cloud-api-key
Enter fullscreen mode Exit fullscreen mode

After that, install the frontend dependencies using pnpm.

pnpm install
Enter fullscreen mode Exit fullscreen mode

Then start the app using the command below.

pnpm run dev
Enter fullscreen mode Exit fullscreen mode

Navigate to http://localhost:3000/ and you should see the restaurant finder CrewAI agent frontend up and running.

Image

Let’s now see how to build the frontend UI for the CrewAI agent using CopilotKit.

Step 3: Set up the CopilotKit Provider

To set up the CopilotKit Provider, the <CopilotKit> component must wrap the Copilot-aware parts of your application. For most use cases, it's appropriate to wrap the CopilotKit provider around the entire app, e.g., in your layout.tsx, as shown below in the src/app/layout.tsx file.

// Import CopilotKit React UI specific styles
import "@copilotkit/react-ui/styles.css";

// Import the CopilotKit component for AI integration
import { CopilotKit } from "@copilotkit/react-core";

// Define metadata for the application
export const metadata: Metadata = {
  // Set the page title
  title: "CopilotKit Crew Demo",
  // Set the page description for SEO and previews
  description: "Talk to your Crew",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en" className="h-full">
      <body className={` antialiased h-full`}>
        {/* CopilotKit wrapper for AI functionality */}
        <CopilotKit
          // Hide the development console in production
          showDevConsole={false}
          // Set the agent name from environment variables
          agent={process.env.NEXT_PUBLIC_AGENT_NAME}
          // Set the public API key from environment variables
          publicApiKey={process.env.NEXT_PUBLIC_CPK_PUBLIC_API_KEY}>
          {children}
        </CopilotKit>
      </body>
    </html>
  );
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Create a Crew-Quickstart Component

To start a CrewAI agent, render crew state and progress, handle human feedback, and stream the agent’s response, you need to create a CrewQuickstart component, as shown in the src/components/CrewQuickstart.tsx file.

"use client"; 

// Import necessary hooks and types from CopilotKit for crew and chat functionality
import {
  CrewsAgentState,
  useCoAgent,
  useCopilotChat,
  useCopilotAdditionalInstructions,
} from "@copilotkit/react-core";
// Import React hooks for state and side effects
import { useEffect, useState } from "react";
// Import message types for chat functionality
import { MessageRole, TextMessage } from "@copilotkit/runtime-client-gql";
// Import UI components for resizable panels
import {
  ResizablePanelGroup,
  ResizablePanel,
  ResizableHandle,
} from "./ui/resizable";
// Import custom hook for window size detection
import { useWindowSize } from "@/hooks/useWindowSize";

// Define props interface for the component
interface CrewQuickstartProps {
  crewName: string; // Name of the crew/agent
  inputs: Array<string>; // Array of input field names to collect from user
}

// Export the main component with typed props
export const CrewQuickstart: React.FC<CrewQuickstartProps> = ({
  crewName,
  inputs,
}: {
  crewName: string;
  inputs: Array<string>;
}) => {
  // State to track if initial chat message has been sent
  const [initialMessageSent, setInitialMessageSent] = useState(false);
  // Get mobile detection from custom hook
  const { isMobile } = useWindowSize();
  // State for panel layout direction (horizontal for desktop, vertical for mobile)
  const [direction, setDirection] = useState<"horizontal" | "vertical">(
    "horizontal"
  );

  // Effect to update layout direction based on mobile status
  useEffect(() => {
    setDirection(isMobile ? "vertical" : "horizontal");
  }, [isMobile]);

  // Setup crew/agent with custom state using CopilotKit's useCoAgent hook
  const { state, setState, run } = useCoAgent<
    CrewsAgentState & {
      result: string; // Final result of crew execution
      inputs: Record<string, string>; // Object storing user inputs
    }
  >({
    name: crewName, // Name of the crew
    initialState: {
      inputs: {}, // Initial empty inputs object
      result: "Crew result will appear here...", // Default result message
    },
  });

  // Clean crewName for display by removing non-alphanumeric characters
  const agentName = crewName.replace(/[^a-zA-Z0-9]/g, " ");

  // Render the component UI
  return (
    // Container div taking full width and height
    <div className="w-full h-full relative">
      {/* Resizable panel group for layout */}
      <ResizablePanelGroup direction={direction} className="w-full h-full">
        {/* Left/main panel for chat (empty in this version) */}
        <ResizablePanel defaultSize={60} minSize={30}>
          {/* Placeholder for chat component */}
        </ResizablePanel>

        {/* Handle for resizing panels */}
        <ResizableHandle withHandle />

        {/* Right panel for crew state/results */}
        <ResizablePanel defaultSize={40} minSize={25}>
          {/* Scrollable container with styling */}
          <div className="h-full overflow-y-auto bg-gray-50 dark:bg-gray-900 p-3">
            <div className="flex flex-col h-full">
              {/* Header with crew name */}
              <div className="flex items-center justify-between mb-2">
                <h1 className="text-lg font-medium text-gray-800 dark:text-gray-200">
                  {agentName}
                </h1>
              </div>

              {/* Content area */}
              <div className="h-full">
                {/* Styled container for results */}
                <div className="text-sm text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 rounded-md shadow-sm p-4 h-full overflow-y-auto prose dark:prose-invert max-w-none">
                  {/* Placeholder for result content */}
                </div>
              </div>
            </div>
          </div>
        </ResizablePanel>
      </ResizablePanelGroup>
    </div>
  );
};

Enter fullscreen mode Exit fullscreen mode

After creating the CrewQuickstart component, you need to import it to your main page as shown below in the src/app/page.tsx file.

"use client";

import React from "react";

// Import the custom CrewQuickstart component from components directory
import { CrewQuickstart } from "@/components/CrewQuickstart";

export default function Home() {
  return (
    <div className="w-full h-full relative">
      {/* Render the CrewQuickstart component with specific props */}
      <CrewQuickstart
        crewName="Restaurant Finder" // Name of the crew/agent
        inputs={["location"]} // Array specifying required user input
      />
    </div>
  );

Enter fullscreen mode Exit fullscreen mode

Step 5: Choose a Copilot UI

To set up your Copilot UI, first import the default styles in your root component (typically layout.tsx).

import "@copilotkit/react-ui/styles.css";
Enter fullscreen mode Exit fullscreen mode

Copilot UI ships with a number of built-in UI patterns; choose whichever one you like from CopilotPopup, CopilotSidebar, CopilotChat, or Headless UI.

Image

In this case, we will use CopilotChat defined in the src/components/Chat.tsx file.

// Declare this component as a client-side component in Next.js
"use client";

import React from "react";

// Import specific types and components from CopilotKit's React UI package
import { CopilotKitCSSProperties, CopilotChat } from "@copilotkit/react-ui";

function Chat() {
  return (
    <div
      className="h-full relative overflow-y-auto"
      style={
        {
          // Define custom CSS variable for CopilotKit's primary color
          "--copilot-kit-primary-color": "#4F4F4F",
        } as CopilotKitCSSProperties // Type assertion for CopilotKit-specific CSS properties
      }>
      {/* CopilotChat component for the chat interface */}
      <CopilotChat
        // Instructions prop provides guidance to the AI assistant
        instructions={
          "You are assisting the user as best as you can. Answer in the best way possible given the data you have."
        }
        // Custom labels for the chat interface
        labels={{
          // Title displayed in the chat header
          title: "Your Assistant",
          // Initial message shown when chat starts
          initial:
            "Hi! 👋 Please provide the location you want to find a restaurant before we get started.",
        }}
        // Styling classes for the chat component using Tailwind CSS
        className="h-full flex flex-col"
        // Custom icons configuration
        icons={{
          // Custom spinner icon shown during loading states
          spinnerIcon: (
            // Span element with animated pulsing dots
            <span className="h-5 w-5 text-gray-500 animate-pulse">...</span>
          ),
        }}
      />
    </div>
  );
}

export default Chat;
Enter fullscreen mode Exit fullscreen mode

The chat component is then imported and used in the src/components/CrewQuickstart.tsx file. The chat is then rendered on the frontend UI, as shown below.

Image

Step 6: Start the CrewAI agent

To start your CrewAI agent, you need to send an initial welcome message, define an action that collects user inputs, define an effect to confirm user inputs, and define instructions that ensure user inputs are added before the agent runs, as shown below in the src/components/CrewQuickstart.tsx file.

"use client";

// Import CopilotKit hooks and types for crew and chat functionality
import {
  CrewsAgentState,
  useCoAgent, // Hook for managing crew state and execution
  useCopilotChat, // Hook for chat functionality
  useCopilotAdditionalInstructions, // Hook for adding crew instructions
  useCopilotAction, // Hook for defining crew actions
} from "@copilotkit/react-core";
// Import React hooks for state and side effects
import { useEffect, useState } from "react";
// Import message types for chat
import { MessageRole, TextMessage } from "@copilotkit/runtime-client-gql";

// Define props interface for the component
interface CrewQuickstartProps {
  crewName: string; // Name of the crew/agent
  inputs: Array<string>; // Array of input field names required from user
}

// Export the CrewQuickstart component with typed props
export const CrewQuickstart: React.FC<CrewQuickstartProps> = ({
  crewName,
  inputs,
}: {
  crewName: string;
  inputs: Array<string>;
}) => {
  // State to track if initial welcome message has been sent
  const [initialMessageSent, setInitialMessageSent] = useState(false);

  // Setup crew with custom state using useCoAgent hook
  const { state, setState, run } = useCoAgent<
    CrewsAgentState & {
      result: string; // Stores the final crew execution result
      inputs: Record<string, string>; // Stores user-provided inputs as key-value pairs
    }
  >({
    name: crewName, // Set crew name from props
    initialState: {
      inputs: {}, // Initially empty inputs object
      result: "Crew result will appear here...", // Default result placeholder
    },
  });

  // Get chat functionality from useCopilotChat hook
  const { appendMessage, isLoading } = useCopilotChat();

  // Define instructions requiring inputs before crew execution
  const instructions =
    "INPUTS ARE ABSOLUTELY REQUIRED. Please call getInputs before proceeding with anything else.";

  // Effect to send initial welcome message when component mounts
  useEffect(() => {
    if (initialMessageSent || isLoading) return; // Skip if already sent or loading

    setTimeout(async () => {
      // Append welcome message to chat
      await appendMessage(
        new TextMessage({
          content: "Hi, Please provide your inputs before we get started.",
          role: MessageRole.Developer, // Attributed to developer role
        })
      );
      setInitialMessageSent(true); // Mark message as sent
    }, 0);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []); // Empty dependency array: runs once on mount

  // Effect to confirm inputs in chat once provided
  useEffect(() => {
    if (!initialMessageSent && Object.values(state?.inputs || {}).length > 0) {
      // If inputs exist and initial message not sent, show them in chat
      appendMessage(
        new TextMessage({
          role: MessageRole.Developer,
          content: "My inputs are: " + JSON.stringify(state?.inputs),
        })
      ).then(() => {
        setInitialMessageSent(true); // Mark as sent after appending
      });
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [initialMessageSent, state?.inputs]); // Depends on message state and inputs

  // Add instructions with conditional availability
  useCopilotAdditionalInstructions({
    instructions, // Instructions defined above
    available:
      Object.values(state?.inputs || {}).length > 0 ? "enabled" : "disabled", // Enable only when inputs are provided
  });

  // Define action to collect inputs from user
  useCopilotAction({
    name: "getInputs", // Action name
    followUp: false, // No follow-up action
    description:
      "This action allows Crew to get required inputs from the user before starting the Crew.",
    renderAndWaitForResponse({ status }) {
      // Render form and wait for submission
      if (status === "inProgress" || status === "executing") {
        return (
          // Form to collect inputs
          <form
            className="flex flex-col gap-4" // Styling for vertical layout with spacing
            onSubmit={async (e: React.FormEvent<HTMLFormElement>) => {
              e.preventDefault(); // Prevent default form submission
              const form = e.currentTarget; // Get form element
              const input = form.elements.namedItem(
                "input"
              ) as HTMLTextAreaElement; // Get input element
              const inputValue = input.value; // Get input value
              const inputKey = input.id; // Get input ID (matches input name from props)

              // Update crew state with new input
              setState({
                ...state,
                inputs: {
                  ...state.inputs,
                  [inputKey]: inputValue,
                },
              });
              // Run crew after state update
              setTimeout(async () => {
                console.log("running crew"); // Log start of crew execution
                await run(); // Execute the crew
                console.log("crew run complete"); // Log completion
              }, 0);
            }}>
            <div className="flex flex-col gap-4">
              {/* Map through required inputs to create textareas */}
              {inputs.map((input) => (
                <div key={input} className="flex flex-col gap-2">
                  <textarea
                    id={input} // Unique ID matching input name
                    autoFocus // Automatically focus first input
                    name="input" // Form element name
                    placeholder={`Enter ${input} here`} // Placeholder text
                    required // Input is mandatory
                    className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" // Styling
                  />
                </div>
              ))}
              {/* Submit button */}
              <button
                type="submit"
                className="w-full px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors duration-200">
                Submit
              </button>
            </div>
          </form>
        );
      }
      return <>Inputs submitted</>; // Message shown after submission
    },
  });

  // Return basic container (UI incomplete in this snippet)
  return (
    <div className="w-full h-full relative">
      {/* Placeholder for additional UI */}
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

An initial message and a form component for collecting user inputs are rendered in the frontend UI, as shown below.

Image

Step 7: Render CrewAI Agent State and Progress

To render your CrewAI agent state and progress, you need to define a CrewStateRenderer component that visualizes the real-time state of the agent’s steps and tasks, as shown below in the src/components/CrewStateRenderer.tsx file.

"use client";

// Import CopilotKit types for crew state management
import {
  CrewsAgentState, // Type for overall crew state
  CrewsResponseStatus, // Type for crew execution status
  CrewsTaskStateItem, // Type for task items
  CrewsToolStateItem, // Type for tool items
} from "@copilotkit/react-core";
// Import React hooks and utilities
import { useEffect, useMemo, useRef, useState } from "react";
// Import ReactMarkdown for rendering markdown content
import ReactMarkdown from "react-markdown";

/**
 * Renders your Crew's steps & tasks in real-time.
 * @param state - The current state of the crew
 * @param status - The current execution status of the crew
 */
function CrewStateRenderer({
  state,
  status,
}: {
  state: CrewsAgentState; // Crew state containing steps and tasks
  status: CrewsResponseStatus; // Status like "inProgress" or "complete"
}) {
  // State to track if the renderer is collapsed or expanded
  const [isCollapsed, setIsCollapsed] = useState(true);
  // Ref to access the content div for scrolling
  const contentRef = useRef<HTMLDivElement>(null);
  // Ref to track previous item count for detecting new items
  const prevItemsLengthRef = useRef<number>(0);
  // State to track which item to highlight (newly added)
  const [highlightId, setHighlightId] = useState<string | null>(null);

  // Memoized computation to combine and sort steps and tasks
  const items = useMemo(() => {
    if (!state) return []; // Return empty array if no state
    // Combine steps and tasks, sort by timestamp
    return [...(state.steps || []), ...(state.tasks || [])].sort(
      (a, b) =>
        new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime() // Sort ascending by timestamp
    );
  }, [state]); // Recompute when state changes

  // Effect to highlight new items and auto-scroll
  useEffect(() => {
    if (!state) return; // Skip if no state
    if (items.length > prevItemsLengthRef.current) {
      // Check if new item added
      const newestItem = items[items.length - 1]; // Get latest item
      setHighlightId(newestItem.id); // Highlight it
      setTimeout(() => setHighlightId(null), 1500); // Clear highlight after 1.5s

      // Auto-scroll to bottom if expanded
      if (contentRef.current && !isCollapsed) {
        contentRef.current.scrollTop = contentRef.current.scrollHeight;
      }
    }
    prevItemsLengthRef.current = items.length; // Update previous length
  }, [items, isCollapsed, state]); // Depends on items, collapse state, and crew state

  // Loading state if no state provided
  if (!state) {
    return <div>Loading crew state...</div>;
  }

  // Hide component if collapsed, empty, and not in progress
  if (isCollapsed && items.length === 0 && status !== "inProgress") return null;

  // Render the UI
  return (
    <div className="mt-2 text-sm">
      {/* Toggle header */}
      <div
        className="flex items-center cursor-pointer" // Flex layout with pointer cursor
        onClick={() => setIsCollapsed(!isCollapsed)} // Toggle collapse state
      >
        <span className="mr-1">{isCollapsed ? "" : ""}</span>{" "}
        {/* Arrow indicator */}
        <span className="text-gray-700">
          {status === "inProgress" ? "Crew is analyzing..." : "Crew analysis"}{" "}
          {/* Status text */}
        </span>
      </div>

      {/* Content area, shown only when expanded */}
      {!isCollapsed && (
        <div
          ref={contentRef} // Reference for scrolling
          className="max-h-[200px] overflow-auto border-l border-gray-200 pl-2 ml-1 mt-1">
          {items.length > 0 ? ( // Check if there are items to render
            items.map((item) => {
              // Map through sorted items
              const isTool = (item as CrewsToolStateItem).tool !== undefined; // Check if item is a tool
              const isHighlighted = item.id === highlightId; // Check if item should be highlighted
              return (
                <div
                  key={item.id} // Unique key for each item
                  className={`mb-2 ${isHighlighted ? "animate-fadeIn" : ""}`}>
                  {/* Item title (tool name or task name) */}
                  <div className="font-bold text-gray-800 dark:text-gray-200">
                    {isTool
                      ? (item as CrewsToolStateItem).tool // Display tool name
                      : (item as CrewsTaskStateItem).name}{" "}
                    {/*Display task name*/}
                  </div>
                  {/* Thought section, if present */}
                  {"thought" in item && item.thought && (
                    <div className="mt-1 opacity-80 text-gray-600 dark:text-gray-400 prose dark:prose-invert max-w-none">
                      <span className="font-medium">Thought:</span>{" "}
                      <ReactMarkdown>{item.thought}</ReactMarkdown>{" "}
                      {/* Render thought as markdown */}
                    </div>
                  )}
                  {/* Result section, if present */}
                  {"result" in item && item.result !== undefined && (
                    <pre className="mt-1 text-sm bg-gray-50 dark:bg-gray-800 p-2 rounded-md overflow-x-auto">
                      {JSON.stringify(item.result, null, 2)}{" "}
                      {/* Display result as formatted JSON */}
                    </pre>
                  )}
                  {/* Description section, if present */}
                  {"description" in item && item.description && (
                    <div className="mt-1 text-gray-700 dark:text-gray-300 prose dark:prose-invert max-w-none">
                      <ReactMarkdown>{item.description}</ReactMarkdown>{" "}
                      {/* Render description as markdown */}
                    </div>
                  )}
                </div>
              );
            })
          ) : (
            <div className="opacity-70 text-gray-500">No activity yet...</div> // Placeholder for empty state
          )}
        </div>
      )}

      {/* Inline styles for fade-in animation */}
      <style jsx>{`
        @keyframes fadeIn {
          0% {
            opacity: 0;
            transform: translateY(4px); // Start slightly below
          }
          100% {
            opacity: 1;
            transform: translateY(0); // End at normal position
          }
        }
        .animate-fadeIn {
          animation: fadeIn 0.5s; // Apply 0.5s fade-in animation
        }
      `}</style>
    </div>
  );
}

export default CrewStateRenderer;
Enter fullscreen mode Exit fullscreen mode

The CrewStateRenderer component is then imported to the CrewQuickstart component, where it is used by CopilotKit’s useCoAgentStateRender hook to render the CrewAI agent state dynamically, as shown below.

// Import the useCoAgentStateRender hook from CopilotKit for rendering crew state
import { useCoAgentStateRender } from "@copilotkit/react-core";

// Import the custom CrewStateRenderer component for visualizing crew state
import CrewStateRenderer from "./CrewStateRenderer";

export const CrewQuickstart: React.FC<CrewQuickstartProps> = ({
  crewName, // Name of the crew/agent
  inputs, // Array of input field names (unused in this version)
}: {
  crewName: string;
  inputs: Array<string>;
}) => {
  // Use CopilotKit's hook to render crew state dynamically
  useCoAgentStateRender({
    name: crewName, // Pass the crew name to identify which crew's state to render
    render: (
      { state, status } // Define how to render the state
    ) => (
      <CrewStateRenderer
        state={state} // Pass the crew's current state (steps, tasks, etc.)
        status={status} // Pass the crew's execution status (e.g., "inProgress")
      />
    ),
  });

  return (
    <div className="w-full h-full relative">
      {/* Placeholder for additional UI */}
    </div>
  );
};

Enter fullscreen mode Exit fullscreen mode

To see the restaurant CrewAI agent state being rendered in real-time, add New York City as an input in the user input form and click the submit button. If you open the agent’s state status text, you should see the agent’s state and progress, as shown below.

Image

Step 8: Handling Human Feedback in a CrewAI Agent

To handle human feedback, you need to define a CrewHumanFeedbackRenderer component that handles user feedback on CrewAI agent task output, as shown in the src/components/CrewHumanFeedbackRenderer.tsx file.

"use client";

// Import CopilotKit types for crew status and state items
import { CrewsResponseStatus, CrewsStateItem } from "@copilotkit/react-core";
// Import React hook for state management
import { useState } from "react";
// Import ReactMarkdown for rendering markdown content
import ReactMarkdown from "react-markdown";

// Define an interface extending CrewsStateItem for feedback-specific data
interface CrewsFeedback extends CrewsStateItem {
  /**
   * Output of the task execution
   */
  task_output?: string; // Optional field for the task's output
}

/**
 * Renders a simple UI for agent-requested user feedback (Approve / Reject).
 * @param feedback - The crew's feedback data including task output
 * @param respond - Optional callback to send user response back to the crew
 * @param status - Current status of the feedback process
 */
function CrewHumanFeedbackRenderer({
  feedback, // Feedback data from the crew
  respond, // Callback to send response (optional)
  status, // Status like "inProgress", "executing", or "complete"
}: {
  feedback: CrewsFeedback;
  respond?: (input: string) => void;
  status: CrewsResponseStatus;
}) {
  // State to toggle visibility of task output
  const [isExpanded, setIsExpanded] = useState(true);
  // State to track user's response (Approved/Rejected)
  const [userResponse, setUserResponse] = useState<string | null>(null);

  // Render feedback submission confirmation when complete
  if (status === "complete") {
    return (
      <div style={{ marginTop: 8, textAlign: "right" }}>
        {" "}
        {/* Right-aligned confirmation */}
        {userResponse || "Feedback submitted."}{" "}
        {/* Show user's response or default message */}
      </div>
    );
  }

  // Render feedback UI when in progress or executing
  if (status === "inProgress" || status === "executing") {
    return (
      <div style={{ marginTop: 8 }}>
        {isExpanded && ( // Show task output only when expanded
          <div
            className="border border-gray-200 rounded-lg p-4 mb-4 bg-white prose dark:prose-invert max-w-none dark:bg-gray-800 dark:border-gray-700 shadow-sm"
            // Styled container for task output with light/dark theme support
          >
            <ReactMarkdown>{feedback.task_output || ""}</ReactMarkdown>{" "}
            {/* Render task output as markdown */}
          </div>
        )}
        <div className="flex justify-end gap-2 mt-2">
          {/* Toggle button to show/hide task output */}
          <button
            className="px-3 py-2 text-sm font-medium text-gray-700 hover:text-gray-900 transition-colors duration-200"
            onClick={() => setIsExpanded(!isExpanded)} // Toggle expanded state
          >
            {isExpanded ? "Hide" : "Show"} Feedback {/* Dynamic button text */}
          </button>
          {/* Approve button */}
          <button
            className="px-4 py-2 text-sm font-medium text-white bg-gray-800 rounded-md hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 transition-colors duration-200"
            onClick={() => {
              setUserResponse("Approved"); // Set local response state
              respond?.("Approve"); // Send "Approve" to crew if respond exists
            }}>
            Approve
          </button>
          {/* Reject button */}
          <button
            className="px-4 py-2 text-sm font-medium text-white bg-gray-800 rounded-md hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 transition-colors duration-200"
            onClick={() => {
              setUserResponse("Rejected"); // Set local response state
              respond?.("Reject"); // Send "Reject" to crew if respond exists
            }}>
            Reject
          </button>
        </div>
      </div>
    );
  }

  // Return null for any other status (e.g., initial state before feedback request)
  return null;
}

export { CrewHumanFeedbackRenderer };

Enter fullscreen mode Exit fullscreen mode

The CrewHumanFeedbackRenderer component is then imported to the CrewQuickstart component, where it is used by CopilotKit’s useCopilotAction to create a crew_requesting_feedback action that renders a feedback UI and waits for user response, as shown below.

// Import CopilotKit types and hook for crew actions
import {
  CrewsResponseStatus, // Type for crew execution status (e.g., "inProgress", "complete")
  CrewsStateItem, // Base type for crew state items
  useCopilotAction, // Hook to define custom crew actions
} from "@copilotkit/react-core";

// Import custom feedback renderer component
import { CrewHumanFeedbackRenderer } from "./CrewHumanFeedbackRenderer";

// Define interface for feedback, extending CrewsStateItem
interface CrewsFeedback extends CrewsStateItem {
  /**
   * Output of the task execution
   */
  task_output?: string; // Optional field for task output
}

// Define props interface for the CrewQuickstart component
interface CrewQuickstartProps {
  crewName: string; // Name of the crew/agent
  inputs: Array<string>; // Array of input field names (unused in this version)
}

// Export the CrewQuickstart component with typed props
export const CrewQuickstart: React.FC<CrewQuickstartProps> = ({
  crewName, // Crew identifier
  inputs, // Required inputs (not utilized here)
}: {
  crewName: string;
  inputs: Array<string>;
}) => {
  // Define a Copilot action for requesting user feedback
  useCopilotAction({
    name: "crew_requesting_feedback", // Action name
    description: "Request feedback from the user", // Action description
    renderAndWaitForResponse(props) {
      // Function to render UI and wait for response
      const { status, args, respond } = props; // Destructure props
      return (
        // Render the feedback UI using CrewHumanFeedbackRenderer
        <CrewHumanFeedbackRenderer
          feedback={args as unknown as CrewsFeedback} // Pass action arguments as feedback (type cast due to unknown args type)
          respond={respond} // Pass the respond callback to send user feedback back
          status={status as CrewsResponseStatus} // Pass the current status
        />
      );
    },
  });

  return (
    <div className="w-full h-full relative">
      {/* Placeholder for additional UI */}
    </div>
  );
};

Enter fullscreen mode Exit fullscreen mode

The restaurant CrewAI agent uses the CrewHumanFeedbackRenderer component to create a curated list of restaurant recommendations in a city. You can send feedback to the agent by clicking the Approve or Reject button, as shown below.

Image

Step 9: Streaming CrewAI Agent Response

To stream your CrewAI agent response, you need to render the crew’s result in markdown format in the CrewQuickstart component, as shown below.

"use client";

// Import CopilotKit types and hook for crew management
import { CrewsAgentState, useCoAgent } from "@copilotkit/react-core";

import { useEffect, useState } from "react";
// Import ReactMarkdown for rendering markdown content
import ReactMarkdown from "react-markdown";
// Import custom UI components for resizable panels
import {
  ResizablePanelGroup,
  ResizablePanel,
  ResizableHandle,
} from "./ui/resizable";
// Import custom hook for window size detection
import { useWindowSize } from "@/hooks/useWindowSize";

// Define props interface for the component
interface CrewQuickstartProps {
  crewName: string; // Name of the crew/agent
  inputs: Array<string>; // Array of input field names (unused in this version)
}

// Export the CrewQuickstart component with typed props
export const CrewQuickstart: React.FC<CrewQuickstartProps> = ({
  crewName, // Crew identifier
  inputs, // Required inputs (not utilized here)
}: {
  crewName: string;
  inputs: Array<string>;
}) => {
  // Get mobile detection from custom hook
  const { isMobile } = useWindowSize();
  // State for panel layout direction (horizontal for desktop, vertical for mobile)
  const [direction, setDirection] = useState<"horizontal" | "vertical">(
    "horizontal" // Default to horizontal
  );

  // Effect to update layout direction based on mobile status
  useEffect(() => {
    setDirection(isMobile ? "vertical" : "horizontal"); // Switch to vertical on mobile
  }, [isMobile]); // Re-run when isMobile changes

  // Setup crew with custom state using useCoAgent hook
  const { state, setState, run } = useCoAgent<
    CrewsAgentState & {
      result: string; // Crew execution result
      inputs: Record<string, string>; // User-provided inputs
    }
  >({
    name: crewName, // Set crew name from props
    initialState: {
      inputs: {}, // Initially empty inputs object
      result: "Crew result will appear here...", // Default result placeholder
    },
  });

  // Clean crewName for display by replacing non-alphanumeric chars with spaces
  const agentName = crewName.replace(/[^a-zA-Z0-9]/g, " ");

  // Render the UI
  return (
    <div className="w-full h-full relative">
      {" "}
      {/* Full-size container */}
      {/* Resizable panel group for responsive layout */}
      <ResizablePanelGroup direction={direction} className="w-full h-full">
        {/* Left/main panel (empty in this version) */}
        <ResizablePanel defaultSize={60} minSize={30}>
          {/* Placeholder for additional content (e.g., chat) */}
        </ResizablePanel>

        {/* Handle for resizing panels */}
        <ResizableHandle withHandle />

        {/* Right panel for crew results */}
        <ResizablePanel defaultSize={40} minSize={25}>
          <div className="h-full overflow-y-auto bg-gray-50 dark:bg-gray-900 p-3">
            {/* Scrollable container with light/dark theme */}
            <div className="flex flex-col h-full">
              {/* Header section */}
              <div className="flex items-center justify-between mb-2">
                <h1 className="text-lg font-medium text-gray-800 dark:text-gray-200">
                  {agentName} {/* Display cleaned crew name */}
                </h1>
              </div>

              {/* Content area */}
              <div className="h-full">
                <div className="text-sm text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 rounded-md shadow-sm p-4 h-full overflow-y-auto prose dark:prose-invert max-w-none">
                  {/* Styled container for result */}
                  <ReactMarkdown>{state?.result || ""}</ReactMarkdown>{" "}
                  {/* Render result as markdown */}
                </div>
              </div>
            </div>
          </div>
        </ResizablePanel>
      </ResizablePanelGroup>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

You can stream the restaurant CrewAI agent response by clicking the Approve button in the human feedback section. Then the agent will render a finalized list of curated restaurant options, as shown below.

Image

Conclusion

We covered a lot in this tutorial. I hope you learned how to build, run, deploy, and chat with full-stack AI agents using CrewAI and Copilotkit.

Check out the full source code on GitHub here

Follow CopilotKit on Twitter and say hi, and if you'd like to build something cool, join the Discord community.

Hostinger image

Get n8n VPS hosting 3x cheaper than a cloud solution

Get fast, easy, secure n8n VPS hosting from $4.99/mo at Hostinger. Automate any workflow using a pre-installed n8n application and no-code customization.

Start now

Top comments (26)

Collapse
 
srijanbaniyal profile image
Srijan Baniyal

The screenshots are really nice {The Gradient Background thing . also it's quite subtle}. From where Did you create this ???

And this is very good tutorial on CrewAI-AI Agents + CopilotKit . This is amazing 🚀

Collapse
 
the_greatbonnie profile image
Bonnie

Thanks for the feedback, Srijan.

For the screenshots I used xnapper.com/app

Collapse
 
srijanbaniyal profile image
Srijan Baniyal

You're Welcome. And now I'll give xnapper.com a try.

Thread Thread
 
the_greatbonnie profile image
Bonnie

Awesome.

Collapse
 
anmolbaranwal profile image
Anmol Baranwal

This combo will make a huge impact! 🔥

Collapse
 
the_greatbonnie profile image
Bonnie

Totally agree.

Collapse
 
lonnie_mcrorey_c3108dcff6 profile image
Lonnie McRorey

Oh look, another “build your own AI agent in 10 minutes” demo. Great if you’re a startup with zero baggage and a repo cleaner than a nun’s browser history.

But for big orgs? Please. You’ve got legacy code older than TikTok interns. You’re not building agents—you’re still trying to find who the hell has access to that Jenkins server from 2013.

Startups can play with CrewAI and CopilotKit like it’s Lego for devs. But drop that into a corporate monolith and it’s like strapping a jet engine to a tricycle. The thing’ll shake, scream, and crash straight into a compliance meeting.

You don’t need AI agents—you need an exorcism, a rewrite, and a team of therapists for your devs.

Cool tech though. Just don’t pretend it’s plug-and-play when your repo’s a museum exhibit.

Collapse
 
the_greatbonnie profile image
Bonnie

Haha, fair point! You’re absolutely right—startups get to skip around in a sandbox while big orgs are still digging through the archaeological ruins of their codebase.

For the corporate monoliths, yeah, it’s less “build an AI agent in 10 minutes” and more “schedule a 6-month committee to discuss if we can think about AI agents.”

Still, I think the cool part isn’t the plug-and-play fantasy—it’s the nudge to start somewhere. Even if it’s just a tiny agent to auto-generate meeting excuses while the exorcism’s in progress.

Collapse
 
nathan_tarbert profile image
Nathan Tarbert

I've been excited to see the first CrewAI-AI Agent + CopilotKit tutorial.
This is fantastic 🚀

Collapse
 
the_greatbonnie profile image
Bonnie

I am happy to hear that, Nathan.

Collapse
 
harshit_rwt profile image
Harshit Rawat

I'm gonna try this for sure !!

Collapse
 
the_greatbonnie profile image
Bonnie

Let me know how you find it, Harshit.

Collapse
 
morgan-123 profile image
Morgan

Very impressive article!

Collapse
 
the_greatbonnie profile image
Bonnie

Thanks for your feedback, Morgan.

Collapse
 
james0123 profile image
James

Awesome!

Collapse
 
the_greatbonnie profile image
Bonnie

Thanks, James.

Collapse
 
trent0123 profile image
Trent

This is pretty great!

Collapse
 
the_greatbonnie profile image
Bonnie

I am happy to hear that, Trent.

Collapse
 
samcurran12 profile image
Sammy Scolling

Love this!

Collapse
 
the_greatbonnie profile image
Bonnie

I am happy to hear that, Sammy.

Collapse
 
sahil_clash profile image
Sahil Patil

Why does the ss become so blurred ...dev.to should improve it
By the nice post

Collapse
 
the_greatbonnie profile image
Bonnie

I will see how to improve the SS next time, Sahil.

Thanks for the feedback.

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

👋 Kindness is contagious

Explore a trove of insights in this engaging article, celebrated within our welcoming DEV Community. Developers from every background are invited to join and enhance our shared wisdom.

A genuine "thank you" can truly uplift someone’s day. Feel free to express your gratitude in the comments below!

On DEV, our collective exchange of knowledge lightens the road ahead and strengthens our community bonds. Found something valuable here? A small thank you to the author can make a big difference.

Okay