阿川私房教材:
學 JavaScript 前端,帶作品集去面試!

63 個專案實戰,寫出作品集,讓面試官眼前一亮!

立即開始免費試讀!

總結

在本文中,您將學習如何使用 CrewAI、CopilotKit 和 Serper 建立結合人機互動功能的全端餐廳查找器 AI 代理程式。

在開始之前,我們將介紹以下內容:

  • 什麼是 CrewAI 代理?

  • 建置、執行和部署 CrewAI 代理

  • 使用 Copilot Cloud 和 CopilotKit 為 CrewAI 代理程式新增前端 UI

這是我們將要建立的應用程式的預覽。

{% 嵌入 https://youtu.be/YsyELMUebSA %}

什麼是 CrewAI 代理?

想像一下你正在從事一個大型的團隊專案,例如開發一個應用程式或一個遊戲。你有一個團隊,每個人都有特定的工作。

一個人擅長寫後端程式碼,另一個人設計 UI,還有人負責測試,等等。

相反,人們將CrewAI 代理想像成一個智慧自動化助理團隊——每個代理商都有自己的角色——共同解決問題或完成任務。

您可以在CrewAI 文件中了解有關 CrewAI 代理的更多訊息

全體人員

什麼是 CopilotKit

CopilotKit是一個用於建立使用者互動代理和副駕駛的開源全端框架。它使您的代理能夠控制您的應用程式、傳達其正在做的事情並產生完全自訂的 UI。

副駕駛套件

{% cta https://go.copilotkit.ai/copilotkit %}查看 CopilotKit 的 GitHub ⭐️ {% endcta %}

先決條件

為了完全理解本教程,您需要對 React 或 Next.js 有基本的了解。

我們還將利用以下內容:

  • Python—一種使用 LangGraph 建立 AI 代理程式的流行程式語言;確保它已安裝在您的電腦上。

  • OpenAI API—使我們能夠使用 GPT 模型執行各種任務;對於本教程,請確保您可以存取 GPT-4 模型。

  • CopilotKit - 一個開源副駕駛框架,用於建立自訂 AI 聊天機器人、應用程式內 AI 代理程式和文字區域。

  • CrewAI——一個Python框架,使開發人員能夠建立具有高級簡單性和精確低階控制的自主 AI 代理程式。

  • SerperTool - 一種利用serper.dev API 根據使用者提供的查詢取得並顯示最相關的搜尋結果的工具。

建置、執行和部署 CrewAI 代理

在本節中,您將學習如何使用 CrewAI 套件建置和執行 CrewAI 代理程式。然後,您將學習如何使用 GitHub 將 AI 代理程式部署到 CrewAI 企業平台。

讓我們開始吧。

步驟 1:建構 CrewAI 船員代理

首先,從克隆餐廳查找器工作人員儲存庫開始,其中包含基於 Python 的 CrewAI Crew 代理程式的程式碼:

git clone https://github.com/TheGreatBonnie/restaurant-finder.git

該存儲庫包含一個 CrewAI Crew 代理,其結構如下:

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

以下是研究查找器 CrewAI Crew 中的基本檔案:

  • agent.yaml - 定義您的 AI 代理程式及其角色

  • task.yaml - 設定代理任務和工作流程

  • .env - 儲存 API 金鑰和環境變數

  • main.py——專案入口點和執行流程

  • crew.py-機組人員編排和協調

  • tools/-自訂代理程式工具的目錄

接下來,在根目錄中建立一個.env檔。然後將OpenAISerper API 金鑰加入到環境變數中。

OPENAI_API_KEY=your-openai-api-key
SERPER_API_KEY=your-serper-api-key

然後使用 CrewAI 安裝所有研究查找器 CrewAI Crew 相依性。如果您尚未安裝 CrewAI 包,請按照CrewAI 文件中的安裝指南進行操作。

crewai install

步驟 2:執行 CrewAI 船員代理

若要執行餐廳 CrewAI 船員代理,請在命令列中執行以下命令。

crewai run

一旦機組啟動,它將使用 Serper 網路搜尋工具找到舊金山的餐廳。然後它會整理出一份餐廳清單並徵求您的回饋,如下所示。

影像

在終端機中回覆「看起來不錯」訊息並按下回車鍵。應在專案資料夾中保存一個包含舊金山餐廳推薦的文件,如下所示。

影像

步驟 3:部署 CrewAI 船員代理

要部署餐廳團隊,請將餐廳查找器團隊程式碼推送到 GitHub 儲存庫,如下所示。

影像

然後登入CrewAI 。在您的儀表板上,使用 CrewAI 配置您的 GitHub 帳戶以存取餐廳查找器儲存庫。

影像

接下來,選擇餐廳查找器儲存庫。然後將 OpenAI 和 Serper API 金鑰新增到環境變數中,如下所示,然後按一下部署按鈕。

影像

餐廳查找器團隊應立即開始部門,如下所示。請注意,首次機組人員部署可能需要長達 10 分鐘的時間。

影像

一旦部署了工作人員,請打開它,然後取得其 URL 和承載令牌。機組人員的 URL 和承載令牌將用於將機組人員註冊到Copilot Cloud

影像

現在我們已經了解如何建置、執行和部署 CrewAI 機組人員代理,讓我們看看如何新增前端 UI 與其聊天。

使用 Copilot Cloud 和 CopilotKit 為 CrewAI 機組人員代理程式新增前端 UI

在本節中,您將學習如何使用 Copilot Cloud 和 CopilotKit 為 CrewAI 機組代理程式新增前端 UI。

讓我們開始吧。

步驟 1:使用 Copilot Cloud 註冊 CrewAI 代理

若要註冊 CrewAI 機組代理,請前往Copilot Cloud ,登錄,然後按一下「開始」按鈕。

影像

然後將您的 OpenAI API 金鑰新增至「提供 OpenAI API 金鑰」部分,如下所示。

影像

接下來,向下捲動到遠端端點部分並點擊新增按鈕。

影像

然後從彈出的模式中選擇遠端端點。之後,新增您的 CrewAI 代理端點 URL、承載令牌、名稱和描述,如下所示。然後點選儲存端點按鈕。

影像

儲存機組端點後,複製 Copilot Cloud 公用 API 金鑰,如下所示。

影像

第 2 步:建立 CrewAI 代理前端 UI

要建立 CrewAI 代理前端 UI,首先複製餐廳查找器 UI 儲存庫,其中包含 Next.js 專案的程式碼:

git clone https://github.com/TheGreatBonnie/restaurant-finder-ui.git

接下來,在根目錄中建立一個.env檔。然後將您的 CrewAI 代理名稱和 Copilot Cloud Public API Key 新增到環境變數中。

NEXT_PUBLIC_AGENT_NAME=restaurant_finder
NEXT_PUBLIC_CPK_PUBLIC_API_KEY=your-copilot-cloud-api-key

之後,使用 pnpm 安裝前端相依性。

pnpm install

然後使用以下命令啟動應用程式。

pnpm run dev

導航至http://localhost:3000/ ,您應該會看到餐廳查找器 CrewAI 代理前端已啟動並正在執行。

影像

現在讓我們看看如何使用 CopilotKit 為 CrewAI 代理程式建立前端 UI。

步驟 3:設定 CopilotKit 提供者

要設定 CopilotKit 提供程序, <CopilotKit>元件必須包裝應用程式中支援 Copilot 的部分。對於大多數用例來說,將 CopilotKit 提供者包裝在整個應用程式中是合適的,例如在您的layout.tsx中,如下面的src/app/layout.tsx檔案中所示。

// 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>
  );
}

步驟 4:建立 Crew-Quickstart 元件

要啟動 CrewAI 代理程式、呈現工作人員狀態和進度、處理人工回饋以及串流代理程式的回應,您需要建立 CrewQuickstart 元件,如src/components/CrewQuickstart.tsx檔案所示。

"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>
  );
};

建立 CrewQuickstart 元件後,您需要將其匯入主頁面,如下面的src/app/page.tsx檔案所示。

"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>
  );

步驟 5:選擇 Copilot UI

要設定您的 Copilot UI,請先在根元件中匯入預設樣式(通常是layout.tsx )。

import "@copilotkit/react-ui/styles.css";

Copilot UI 隨附許多內建的 UI 模式;從CopilotPopupCopilotSidebarCopilotChatHeadless UI中選擇您喜歡的哪一個。

影像

在本例中,我們將使用src/components/Chat.tsx檔案中定義的 CopilotChat。

// 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;

然後匯入聊天元件並在src/components/CrewQuickstart.tsx檔案中使用。然後聊天內容將顯示在前端 UI 上,如下所示。

影像

步驟 6:啟動 CrewAI 代理

要啟動您的 CrewAI 代理,您需要發送初始歡迎訊息,定義收集使用者輸入的操作,定義確認使用者輸入的效果,並定義確保在代理執行之前新增使用者輸入的指令,如下面的src/components/CrewQuickstart.tsx檔案中所示。

"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>
  );
};

初始訊息和用於收集使用者輸入的表單元件呈現在前端 UI 中,如下所示。

影像

步驟 7:渲染 CrewAI 代理狀態與進度

要呈現您的 CrewAI 代理程式狀態和進度,您需要定義一個 CrewStateRenderer 元件來視覺化代理程式的步驟和任務的即時狀態,如下面的src/components/CrewStateRenderer.tsx檔案所示。

"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;

然後將 CrewStateRenderer 元件匯入到 CrewQuickstart 元件,CopilotKit 的useCoAgentStateRender鉤子使用它來動態渲染 CrewAI 代理程式狀態,如下所示。

// 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>
  );
};

若要查看餐廳 CrewAI 代理狀態的即時呈現,請在使用者輸入表單中新增紐約市作為輸入,然後按一下提交按鈕。如果您開啟代理的狀態文本,您應該會看到代理的狀態和進度,如下所示。

影像

步驟 8:在 CrewAI 代理程式中處理人工回饋

為了處理手動回饋,您需要定義一個 CrewHumanFeedbackRenderer 元件來處理使用者對 CrewAI 代理任務輸出的回饋,如src/components/CrewHumanFeedbackRenderer.tsx檔案所示。


"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 n

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。

阿川私房教材:
學 JavaScript 前端,帶作品集去面試!

63 個專案實戰,寫出作品集,讓面試官眼前一亮!

立即開始免費試讀!