第七色在线视频,2021少妇久久久久久久久久,亚洲欧洲精品成人久久av18,亚洲国产精品特色大片观看完整版,孙宇晨将参加特朗普的晚宴

為了賬號安全,請及時綁定郵箱和手機立即綁定

如何用CrewAI和CopilotKit打造全棧AI代理(?? 容易上手的AI助手搭建指南)

TL;DR(太长没看)

本文将教你如何构建一个结合了CrewAI、CopilotKit和Serper的人工智能代理的人机协作功能的全栈餐厅查找AI代理工具。

在我们开始之前,这里是我们要聊的内容:

  • CrewAI成员是什么?

  • 构建、运行以及部署CrewAI代理程序

  • 使用 Copilot Cloud 和 CopilotKit 为 CrewAI 添加前端 UI ,

(注:此处保持了源文本的列表项标识,增加了中文中的逗号以保持一致性)

这里是你将要构建的应用程序的预览。

你知道CrewAI代理是什么吗?

想象你正在做一个大团队项目,比如制作一个应用或游戏。你有一个团队成员,每个人都有他们自己的活儿。

一个人擅长后端开发,另一个做UI设计,还有人做测试,等等类似。

相反,人们将 CrewAI 代理人 想象成一群智能的自动化助手——各自承担不同的职责——一起解决问题或完成任务。

你可以在这里可以更多关于CrewAI智能体的信息 CrewAI官方文档

船员们

什么是 CopilotKit

CopilotKit (一个开源的全栈框架)用于构建用户交互式的代理和副驾驶。它使你的代理能够控制你的应用程序,说明它在做什么,并生成完全自定义的用户界面(UI)。

[CopilotKit插件]

快来访问 CopilotKit 的 GitHub ⭐️ 吧!链接

先决条件

要完全理解本教程,你需要对 React 或 Next.js 有一些基本的了解。

我们也将利用以下内容:

  • Python - 广受欢迎的编程语言,用于使用LangGraph构建AI代理程序;确保已在您的计算机上安装。

  • OpenAI API - 用于执行各种任务的GPT模型;在本教程中,请确保你能访问GPT-4模型。

  • CopilotKit - 一个开源的CopilotKit框架,用于构建自定义AI聊天机器人、应用程序内的AI助手和文本输入区域。

  • CrewAI - 一个基于 Python 的框架,让开发人员可以简单易用和精确的低层次控制创建自主 AI 代理。

  • SerperTool - 一个利用 serper.dev API 获取并展示与用户查询最相关的搜索结果的工具,
搭建、运行及部署一个CrewAI代理

在这一部分,你将学习如何使用CrewAI包构建和运行一个CrewAI代理程序。然后你将学习如何通过GitHub将AI代理程序部署到CrewAI企业平台。

咱们直接开始了。

第一步:创建 CrewAI 代理

首先,克隆餐厅查找器 Crew 仓库页面,该仓库包含了基于 Python 的 CrewAI Crew 代理代码。

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

全屏模式 退出全屏

该仓库包含一个CrewAI船员代理程序,具有以下结构:

    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研究工具中的一些关键文件:

  • agents.yaml - 定义你的AI代理及其职责

  • tasks.yaml - 定义代理程序任务及其工作流程

    配置文件(.env) - 放置 API 密钥和环境变量的地方

  • main.py - 项目主入口和执行逻辑

  • crew.py - 团队管理与协调

  • tools/ - 存放自定义代理工具的目录

接下来,在根目录里创建一个 .env 文件。然后将 OpenAI 和 Serper 的 API 密钥分别添加到环境设置里。

OPENAI_API_KEY=你的 OpenAI API 密钥
SERPER_API_KEY=你的 SERPER API 密钥

点击全屏 开启全屏模式 点击退出 结束全屏

使用CrewAI安装所有CrewAI Crew研究查找器相关的所有依赖项。如果没有安装CrewAI包,请按照CrewAI文档中的此安装指南进行操作。

运行crewai 安装

切换到全屏 退出全屏

步骤 2: 运行一个 CrewAI 船员代理

要运行CrewAI餐厅的代理,请在命令行中运行以下命令。

启动 crewai

进入全屏模式, 退出全屏

一旦开始操作后,它将使用Serper网络搜索工具来查找旧金山的餐厅。然后它将整理餐厅名单,并征求您的意见如下,请您看看并告诉我们您的想法。

图片 这是一张图片。

在终端回复“看起来不错”,然后按回车键。项目文件夹中应该有一份旧金山餐厅推荐列表的文件,如下图所示。

图片 查看点击图片
这是一张示例图片

第三步:部署 CrewAI 船员代理

要部署餐厅团队成员,请将餐厅查找器团队的代码推送到 GitHub 仓库中。具体操作如下。

这张图片 (点击图片可查看大图)

然后登录到CrewAI。在你的控制面板中,将你的GitHub账户与CrewAI关联起来,以便访问餐厅查找器代码库。

图片

接下来,选择餐厅查找器仓库。然后将OpenAI和Serper的API密钥添加到环境变量中,如下。最后,点击部署按钮。

图片 如下

餐厅搜索团队应立即开始部署,如下。需要注意的是,首次部署可能需要长达10分钟的时间。

图片 这是一张图片。

一旦成员部署完毕,打开它后,取得其网址和bearer token。网址和bearer token用于注册到Copilot Cloud

来看一看这张图片吧! 图片

现在我们已经学会了如何搭建、运行并部署一个CrewAI船员代理程序,接下来我们来看看如何为其添加一个前端UI以便能与它聊天。

使用 Copilot Cloud 和 CopilotKit 为 CrewAI 船员代理添加前端界面

在这个部分,你将学习如何使用Copilot Cloud和CopilotKit给CrewAI团队的代理添加一个前端用户界面。

咱们开始吧。

第一步:在 Copilot Cloud 中为 CrewAI 注册一个代理账号

要注册一个CrewAI团队代理,请访问Copilot Cloud,然后登录并点击开始按钮。

示例图片 如图所示。

然后将你的 OpenAI API 钥添加到“输入你的 OpenAI API 钥”部分,如下。

来看这张图片,下面是一张图片。
图片

接下来,滚动到远程端点部分,点击新建按钮。

看这张图片:图片

然后从弹出窗口中选择远程端点地址。接着,添加你的CrewAI代理端点URL、bearer token(携带令牌)、名称(Name)和描述,参照示例如下。然后点击保存。

图片 (点击可查看图片)

保存团队端点后,复制下面的Copilot Cloud公开API密钥。

来看看这张图片。(Come and take a look at this picture.)图片

第二步:构建CrewAI代理的前端UI

要构建一个CrewAI代理前端UI,首先复制或下载餐厅查找器UI仓库(repo),该代码库包含了一个Next.js项目的代码。

运行以下命令克隆仓库:
git clone https://github.com/TheGreatBonnie/restaurant-finder-ui.git

全屏模式 退出全屏

接下来,在根目录下创建一个 .env 文件,并将你的 CrewAI 名称和 Copilot Cloud 公共 API 钥设置进去。

    NEXT_PUBLIC_AGENT_NAME=restaurant_finder
    NEXT_PUBLIC_CPK_PUBLIC_API_KEY=你的copilot云API密钥

全屏模式 退出全屏

之后,使用 pnpm 安装前端依赖。

运行 pnpm install 命令。

全屏查看
退出全屏

接下来运行以下命令启动应用

运行开发模式的命令是 `pnpm run dev`。

全屏模式,退出全屏

打开浏览器并输入网址 http://localhost:3000/,你应该能看到正在运行的CrewAI餐厅查找前端页面。

图片

我们现在来看看如何用CopilotKit为CrewAI代理构建前端UI。

第三步:设置 CopilotKit 提供器

为了配置 CopilotKit 提供者,必须用 <CopilotKit> 组件包裹你应用中与 Copilot 相关的部分。对于大多数用例而言,将整个应用包裹在 <CopilotKit> 组件中是合适的,例如,在你的 layout.tsx 中,如下所示在 src/app/layout.tsx 文件里。

    // 导入 CopilotKit React UI 特定样式
    import "@copilotkit/react-ui/styles.css";

    // 导入 CopilotKit 组件以实现 AI 集成
    import { CopilotKit } from "@copilotkit/react-core";

    // 定义应用的元数据
    export const metadata: Metadata = {
      // 设置页面标题
      title: "CopilotKit Crew Demo",
      // 设置页面描述,用于 SEO 和预览
      description: "与你的团队交流",
    };

    export default function 根布局组件({
      children,
    }: Readonly<{
      children: React.ReactNode;
    }>) {
      return (
        <html lang="en" className="h-full">
          <body className={` antialiased h-full`}>
            {/* CopilotKit 包装器,用于 AI 功能 */}
            <CopilotKit
              // 在生产环境中隐藏开发控制台
              showDevConsole={false}
              // 从环境变量中设置代理名
              agent={process.env.NEXT_PUBLIC_AGENT_NAME}
              // 设置公共 API 密钥从环境变量
              publicApiKey={process.env.NEXT_PUBLIC_CPK_PUBLIC_API_KEY}>
              {children}
            </CopilotKit>
          </body>
        </html>
      );
    }

点击全屏,退出全屏

第 4 步:创建 Crew-Quickstart 组件

为了启动一个CrewAI代理程序,渲染船员的状态和进度,处理人类的反馈,并流式传输代理的响应内容,你需要创建一个CrewQuickstart组件,如在src/components/CrewQuickstart.tsx文件中所示。

    "use client"; 

    // 导入 CopilotKit 中用于团队和聊天功能的必要钩子和类型
    import {
      CrewsAgentState,
      useCoAgent,
      useCopilotChat,
      useCopilotAdditionalInstructions,
    } from "@copilotkit/react-core";
    // 导入 React 钩子用于状态管理和副作用
    import { useEffect, useState } from "react";
    // 导入用于聊天功能的消息类型
    import { MessageRole, TextMessage } from "@copilotkit/runtime-client-gql";
    // 导入用于可调整大小面板的 UI 组件
    import {
      ResizablePanelGroup,
      ResizablePanel,
      ResizableHandle,
    } from "./ui/resizable";
    // 导入用于检测窗口大小的自定义钩子
    import { useWindowSize } from "@/hooks/useWindowSize";

    // 定义组件的 props 接口
    interface CrewQuickstartProps {
      crewName: string; // 从用户收集的输入字段名称数组
      inputs: Array<string>; // 从用户收集的输入字段名称数组
    }

    // 导出带有类型化 props 的主要组件
    export const CrewQuickstart: React.FC<CrewQuickstartProps> = ({
      crewName,
      inputs,
    }: {
      crewName: string;
      inputs: Array<string>;
    }) => {
      // 状态以跟踪初始聊天消息是否已发送
      const [initialMessageSent, setInitialMessageSent] = useState(false);
      // 从自定义钩子获取移动设备检测
      const { isMobile } = useWindowSize();
      // 状态用于面板布局方向,桌面为水平,移动为垂直
      const [direction, setDirection] = useState<"horizontal" | "vertical">(
        "horizontal"
      );

      // 用于根据移动设备状态更新布局方向的副作用
      useEffect(() => {
        setDirection(isMobile ? "vertical" : "horizontal");
      }, [isMobile]);

      // 使用 CopilotKit 的 useCoAgent 钩子设置团队/代理,使用自定义状态
      const { state, setState, run } = useCoAgent<
        CrewsAgentState & {
          result: string; // 团队执行的最终结果
          inputs: Record<string, string>; // 存储用户输入的对象
        }
      >({
        name: crewName, // 团队名称
        initialState: {
          inputs: {}, // 初始为空的输入对象
          result: "团队结果将显示在这里...", // 默认结果消息
        },
      });

      // 通过移除非字母数字字符来清理 crewName 用于显示
      const agentName = crewName.replace(/[^a-zA-Z0-9]/g, " ");

      // 渲染组件 UI
      return (
        // 容器 div 占满全宽度和高度
        <div className="w-full h-full relative">
          {/* 可调整大小面板组用于布局 */}
          <ResizablePanelGroup direction={direction} className="w-full h-full">
            {/* 左/主面板用于聊天(本版本为空) */}
            <ResizablePanel defaultSize={60} minSize={30}>
              {/* 聊天组件的占位符 */}
            </ResizablePanel>

            {/* 为调整面板大小的控件 */}
            <ResizableHandle withHandle />

            {/* 右侧面板用于团队状态和结果 */}
            <ResizablePanel defaultSize={40} minSize={25}>
              {/* 带样式的可滚动容器 */}
              <div className="h-full overflow-y-auto bg-gray-50 dark:bg-gray-900 p-3">
                <div className="flex flex-col h-full">
                  {/* 含团队名称的标题 */}
                  <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>

                  {/* 内容区域 */}
                  <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">
                      {/* 结果内容的占位符 */}
                    </div>
                  </div>
                </div>
              </div>
            </ResizablePanel>
          </ResizablePanelGroup>
        </div>
      );
    };

进入全屏 退出全屏

创建了 CrewQuickstart 组件后,如下所示将其导入主页面,具体在 src/app/page.tsx 文件里。

    "use client";

    import React from "react";

    import { CrewQuickstart } from "@/components/CrewQuickstart";

    export default function Home() {
      return (
        <div className="w-full h-full relative">
          <CrewQuickstart
            crewName="餐厅查找器" // 船员/代理的名称
            inputs={["位置"]} // 所需用户输入数组
          />
        </div>
      );

请点击进入全屏,然后点击退出全屏。

步骤 5 挑选 Copilot UI 界面

要设置你的 Copilot UI,第一步是导入默认样式文件,通常在根组件如 layout.tsx 中进行。

导入 '@copilotkit/react-ui/styles.css'; // 这行代码是用来导入样式文件的。

进入全屏 退出全屏

Copilot UI 自带多种内置的 UI 模式;你可以选择你喜欢的任何一个,比如 CopilotPopupCopilotSidebarCopilotChat无头 UI

这张图片

在这种情形下,我们将用到定义于 src/components/Chat.tsx 文件中的 CopilotChat。

    // 声明此组件为Next.js中的客户端组件
    "use client";

    import React from "react";

    // 从CopilotKit的React UI包中导入特定类型和组件
    import { CopilotKitCSSProperties, CopilotChat } from "@copilotkit/react-ui";

    function Chat() {
      return (
        <div
          className="h-full relative overflow-y-auto"
          style={
            {
              // 定义CopilotKit主色调的自定义CSS变量
              "--copilot-kit-primary-color": "#4F4F4F",
            } as CopilotKitCSSProperties // CopilotKit特定CSS属性的类型断言
          }>
          {/* CopilotChat组件用于聊天界面的显示 */}
          <CopilotChat
            // Instructions属性为AI助手提供指导
            instructions={
              "根据您所掌握的信息,尽可能给出最好的回答。"
            }
            // 聊天界面的自定义标签
            labels={{
              // 显示在聊天标题中的标题
              title: "您的助手",
              // 聊天开始前显示的初始消息
              initial:
                "你好!👋 在我们开始前,请提供您想要找餐厅的位置。",
            }}
            // 使用Tailwind CSS为聊天组件设置样式
            className="h-full flex flex-col"
            // 自定义图标配置
            icons={{
              // 在加载过程中显示的自定义加载图标
              spinnerIcon: (
                // 带有动画闪烁点的span元素
                <span className="h-5 w-5 text-gray-500 animate-pulse">...</span>
              ),
            }}
          />
        </div>
      );
    }

    export default Chat;

按 Enter 进入全屏模式,再按一下退出

聊天组件随后被导入并在 src/components/CrewQuickstart.tsx 文件中使用。聊天组件并在前端 UI 上显示,如下图所示。

这是一张图片图片

第 6 步:开始 CrewAI 代理

要开始你的CrewAI代理,你需要发送一条初始的欢迎消息,定义一个收集用户输入的动作步骤,定义一个确认用户输入的效果步骤,并定义确保在代理运行前添加用户输入的指令,如下面的src/components/CrewQuickstart.tsx文件所示。

    "use client";

    // 导入 CopilotKit 模块的钩子和类型,用于管理队伍和聊天功能
    import {
      CrewsAgentState,
      useCoAgent, // 管理队伍状态和执行的钩子
      useCopilotChat, // 聊天功能的钩子
      useCopilotAdditionalInstructions, // 添加队伍指令的钩子
      useCopilotAction, // 定义队伍动作的钩子
    } from "@copilotkit/react-core";
    // 导入 React 的状态和副作用钩子
    import { useEffect, useState } from "react";
    // 导入聊天消息的类型
    import { MessageRole, TextMessage } from "@copilotkit/runtime-client-gql";

    // 定义组件的属性接口
    interface CrewQuickstartProps {
      crewName: string; // 队伍/代理的名称
      inputs: Array<string>; // 需要从用户获取的输入字段数组
    }

    // 导出 CrewQuickstart 组件,带类型属性
    export const CrewQuickstart: React.FC<CrewQuickstartProps> = ({
      crewName,
      inputs,
    }: {
      crewName: string;
      inputs: Array<string>;
    }) => {
      // 状态以跟踪初始欢迎消息是否已发送
      const [initialMessageSent, setInitialMessageSent] = useState(false);

      // 使用 useCoAgent 钩子设置队伍,使用自定义状态
      const { state, setState, run } = useCoAgent<
        CrewsAgentState & {
          result: string; // 存储最终队伍执行结果
          inputs: Record<string, string>; // 存储用户提供的输入,作为键值对
        }
      >({
        name: crewName, // 从属性设置队伍名称
        initialState: {
          inputs: {}, // 初始为空的输入对象
          result: "队伍结果将出现在这里...", // 默认结果占位符
        },
      });

      // 从 useCopilotChat 钩子获取聊天功能
      const { appendMessage, isLoading } = useCopilotChat();

      // 定义需要输入的指令,队伍执行前必须提供
      const instructions =
        "输入绝对必要。请在继续之前调用 getInputs。";

      // 效果以在组件挂载时发送初始欢迎消息
      useEffect(() => {
        if (initialMessageSent || isLoading) return; // 如果已发送或者正在加载,则跳过

        setTimeout(async () => {
          // 在聊天中添加欢迎消息
          await appendMessage(
            new TextMessage({
              content: "您好,请在开始之前提供您的输入。",
              role: MessageRole.Developer, // 归因于开发者角色
            })
          );
          setInitialMessageSent(true); // 标记为已发送
        }, 0);
        // eslint-disable-next-line react-hooks/exhaustive-deps
      }, []); // 空依赖数组:组件挂载时运行一次

      // 效果以在聊天中确认提供后的输入
      useEffect(() => {
        if (!initialMessageSent && Object.values(state?.inputs || {}).length > 0) {
          // 如果输入已存在且初始消息未发送,则在聊天中显示它们
          appendMessage(
            new TextMessage({
              role: MessageRole.Developer,
              content: "我的输入如下:" + JSON.stringify(state?.inputs),
            })
          ).then(() => {
            setInitialMessageSent(true); // 标记为已发送后追加
          });
        }
        // eslint-disable-next-line react-hooks/exhaustive-deps
      }, [initialMessageSent, state?.inputs]); // 依赖于消息状态和输入

      // 添加有条件可用的指令内容
      useCopilotAdditionalInstructions({
        instructions, // 上面定义的指令
        available:
          Object.values(state?.inputs || {}).length > 0 ? "enabled" : "disabled", // 输入提供后启用
      });

      // 定义获取用户输入的操作
      useCopilotAction({
        name: "getInputs", // 操作名称
        followUp: false, // 无后续操作
        description:
          "此操作允许队伍在开始之前从用户处获取所需的输入。",
        renderAndWaitForResponse({ status }) {
          // 渲染表单
          if (status === "inProgress" || status === "executing") {
            return (
              // 收集输入的表单
              <form
                className="flex flex-col gap-4" // 垂直布局的样式,带有间距
                onSubmit={async (e: React.FormEvent<HTMLFormElement>) => {
                  e.preventDefault(); // 阻止默认的表单提交
                  const form = e.currentTarget; // 获取表单元素
                  const input = form.elements.namedItem(
                    "input"
                  ) as HTMLTextAreaElement; // 获取输入元素
                  const inputValue = input.value; // 获取输入值
                  const inputKey = input.id; // 获取输入的ID(与属性中的输入名称匹配)

                  // 使用新输入更新队伍状态
                  setState({
                    ...state,
                    inputs: {
                      ...state.inputs,
                      [inputKey]: inputValue,
                    },
                  });
                  // 在状态更新后运行队伍
                  setTimeout(async () => {
                    console.log("正在运行队伍"); // 记录队伍执行开始
                    await run(); // 执行队伍
                    console.log("队伍运行完成"); // 记录完成
                  }, 0);
                }}>
                <div className="flex flex-col gap-4">
                  {/* 映射所需的输入以创建文本区域 */}
                  {inputs.map((input) => (
                    <div key={input} className="flex flex-col gap-2">
                      <textarea
                        id={input} // 与输入名称匹配的唯一ID
                        autoFocus // 自动聚焦第一个输入
                        name="input" // 表单元素名称
                        placeholder={`在此处输入 ${input}`} // 占位符文本
                        required // 输入是必需的
                        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" // 样式
                      />
                    </div>
                  ))}
                  {/* 提交按钮 */}
                  <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">
                    提交
                  </button>
                </div>
              </form>
            );
          }
          return <>提交成功提示</>; // 提交后显示的消息
        },
      });

      // 返回基本容器(此片段中的 UI 不完整)
      return (
        <div className="w-full h-full relative">
          {/* 附加 UI 占位 */}
        </div>
      );
    };

进入全屏 退出全屏

一个初始消息和一个用于收集用户输入的表单在前端界面中显示,如图所示。

来看看这张图片吧!图片

第七步:显示 CrewAI 角色的状态和进度

为了显示你的CrewAI代理的状态和进度,你需要定义一个这样的CrewStateRenderer组件,该组件实时显示代理步骤和任务的状态,如下所示,在src/components/CrewStateRenderer.tsx文件中。

    "use client";

    // 导入 CopilotKit 类型,用于管理小组状态
    import {
      CrewsAgentState, // 表示总体小组状态的类型
      CrewsResponseStatus, // 表示小组执行状态的类型
      CrewsTaskStateItem, // 表示任务项的类型
      CrewsToolStateItem, // 表示工具项的类型
    } from "@copilotkit/react-core";
    // 导入 React 钩子和工具
    import { useEffect, useMemo, useRef, useState } from "react";
    // 导入 ReactMarkdown 用于渲染 markdown 内容
    import ReactMarkdown from "react-markdown";

    /**

* 实时显示小组的步骤和任务。

* @param state - 小组的当前状态

* @param status - 小组的当前执行状态
     */
    function CrewStateRenderer({
      state,
      status,
    }: {
      state: CrewsAgentState; // 包含步骤和任务的小组状态
      status: CrewsResponseStatus; // 状态如 "inProgress" 或 "complete"
    }) {
      // 状态以跟踪渲染器是否折叠或展开
      const [isCollapsed, setIsCollapsed] = useState(true);
      // 引用以访问内容 div 进行滚动
      const contentRef = useRef<HTMLDivElement>(null);
      // 引用以跟踪之前的项目计数,检测是否有新项目
      const prevItemsLengthRef = useRef<number>(0);
      // 状态以跟踪需要高亮显示的项目(新添加的)
      const [highlightId, setHighlightId] = useState<string | null>(null);

      // 记忆化计算以组合并按时间戳排序步骤和任务
      const items = useMemo(() => {
        if (!state) return []; // 如果没有状态,返回空数组
        // 组合步骤和任务,按时间戳排序
        return [...(state.steps || []), ...(state.tasks || [])].sort(
          (a, b) =>
            new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime() // 按时间戳升序排序
        );
      }, [state]); // 状态更改时重新计算

      // 效果以高亮显示新项目并滚动到底部
      useEffect(() => {
        if (!state) return; // 如果没有状态,跳过
        if (items.length > prevItemsLengthRef.current) {
          // 检查是否有新项目添加
          const newestItem = items[items.length - 1]; // 获取最新项目
          setHighlightId(newestItem.id); // 高亮显示它
          setTimeout(() => setHighlightId(null), 1500); // 1.5 秒后清除高亮显示

          // 滚动到底部
          if (contentRef.current && !isCollapsed) {
            contentRef.current.scrollTop = contentRef.current.scrollHeight;
          }
        }
        prevItemsLengthRef.current = items.length; // 更新之前的长度
      }, [items, isCollapsed, state]); // 依赖于项目、折叠状态和小组状态

      // 如果没有状态,加载中
      if (!state) {
        return <div>加载中...</div>;
      }

      // 如果折叠且无内容且不在进行中,则不显示组件
      if (isCollapsed && items.length === 0 && status !== "inProgress") return null;

      // 渲染 UI
      return (
        <div className="mt-2 text-sm">
          {/* 切换标题 */}
          <div
            className="flex items-center cursor-pointer" // 弹性布局,指针光标
            onClick={() => setIsCollapsed(!isCollapsed)} // 切换折叠状态
          >
            <span className="mr-1">{isCollapsed ? "▶" : "▼"}</span>{" "} {/* 箭头指示符 */}
            <span className="text-gray-700">
              {status === "inProgress" ? "小组正在分析..." : "小组分析"} {/* 状态文本 */}
            </span>
          </div>

          {/* 内容区域,仅在展开时显示 */}
          {!isCollapsed && (
            <div
              ref={contentRef} // 引用以滚动
              className="max-h-[200px] overflow-auto border-l border-gray-200 pl-2 ml-1 mt-1">
              {items.length > 0 ? ( // 检查是否有项目要渲染
                items.map((item) => {
                  // 遍历排序后的项目
                  const isTool = (item as CrewsToolStateItem).tool !== undefined; // 检查项目是否是工具
                  const isHighlighted = item.id === highlightId; // 检查项目是否需要高亮
                  return (
                    <div
                      key={item.id} // 每个项目唯一的键
                      className={`mb-2 ${isHighlighted ? "animate-fadeIn" : ""}`}>
                      {/* 瑞典项目标题(工具名称或任务名称) */}
                      <div className="font-bold text-gray-800 dark:text-gray-200">
                        {isTool
                          ? (item as CrewsToolStateItem).tool // 显示工具名称
                          : (item as CrewsTaskStateItem).name} {/* 显示任务名称 */}
                      </div>
                      {/* 如果存在,想法部分 */}
                      {"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">想法:</span>{" "}
                          <ReactMarkdown>{item.thought}</ReactMarkdown>{" "} {/* 以 markdown 渲染想法 */}
                        </div>
                      )}
                      {/* 如果存在,结果部分 */}
                      {"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)} {/* 以格式化的 JSON 显示结果内容 */}
                        </pre>
                      )}
                      {/* 如果存在,描述部分 */}
                      {"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>{" "} {/* 以 markdown 渲染描述 */}
                        </div>
                      )}
                    </div>
                  );
                })
              ) : (
                <div className="opacity-70 text-gray-500">暂无动态...</div> {/* 空状态占位符 */}
              )}
            </div>
          )}

          {/* 针对淡入动画的内联样式 */}
          <style jsx>{`
            @keyframes fadeIn {
              0% {
                opacity: 0;
                transform: translateY(4px); // 从略微下方开始
              }
              100% {
                opacity: 1;
                transform: translateY(0); // 结束于正常位置
              }
            }
            .animate-fadeIn {
              animation: fadeIn 0.5s; // 应用 0.5 秒淡入动画
            }
          `}</style>
        </div>
      );
    }

    export default CrewStateRenderer;

全屏,退出全屏

CrewStateRenderer 组件随后被导入到 CrewQuickstart 组件中,并使用 CopilotKit 提供的 useCoAgentStateRender 钩子来动态渲染 CrewAI 代理的状态,如下。

// 导入 CopilotKit 中的 useCoAgentStateRender 钩子,用于渲染船员状态
import { useCoAgentStateRender } from "@copilotkit/react-core";

// 导入自定义的 CrewStateRenderer 组件,用于显示船员状态
import CrewStateRenderer from "./CrewStateRenderer";

export const CrewQuickstart: React.FC<CrewQuickstartProps> = ({
  crewName, // 船员的名称
  inputs, // 输入字段名称(当前版本未使用)
}: {
  crewName: string;
  inputs: Array<string>;
}) => {
  // 使用 CopilotKit 的钩子来动态渲染船员状态
  useCoAgentStateRender({
    name: crewName, // 传递船员名称,以标识要渲染哪个船员的状态
    render: (
      { state, status } // 定义如何渲染状态
    ) => (
      <CrewStateRenderer
        state={state} // 传递船员当前状态(步骤和任务)
        status={status} // 传递船员执行状态(例如 "进行中")
      />
    ),
  });

  return (
    <div className="w-full h-full relative">
      {/* 预留空间,用于添加 UI 元素 */}
    </div>
  );
};

进入全屏。退出全屏。

为了实时查看其状态,请在用户输入表单中输入纽约市,然后点击提交按钮。打开代理的状态信息后,你应该能看到代理的状态和进度,如下图所示。

来看看这张图片。图片

步骤 8:处理 CrewAI 代理中的反馈

为了处理用户反馈,你需要定义一个处理用户对CrewAI代理任务输出反馈的组件,如下面文件所示:src/components/CrewHumanFeedbackRenderer.tsx文件中所示。

    "use client";

    // 导入 CopilotKit 的类型,用于机组状态和状态项目
    import { CrewsResponseStatus, CrewsStateItem } from "@copilotkit/react-core";
    // 导入 React 钩子,用于状态管理
    import { useState } from "react";
    // 导入 ReactMarkdown,用于渲染 Markdown 格式的内容
    import ReactMarkdown from "react-markdown";

    // 定义一个接口,扩展 CrewsStateItem,为反馈数据添加特定字段
    interface CrewsFeedback extends CrewsStateItem {
      /**

* 任务执行的结果输出
       */
      task_output?: string; // 可选字段,用于任务的输出
    }

    /**

* 渲染一个简单的 UI,用于请求用户的反馈(批准 / 拒绝)。

* @param feedback - 机组提供的反馈数据,包括任务输出

* @param respond - 用于发送用户响应回机组的可选回调

* @param status - 当前反馈过程的状态
     */
    function CrewHumanFeedbackRenderer({
      feedback, // 从机组获取的反馈数据
      respond, // 发送响应的回调(可选)
      status, // 反馈状态,如 "inProgress"、"executing" 或 "complete"
    }: {
      feedback: CrewsFeedback;
      respond?: (input: string) => void;
      status: CrewsResponseStatus;
    }) {
      // 状态,用于切换任务输出的可见性
      const [isExpanded, setIsExpanded] = useState(true);
      // 状态,用于追踪用户的响应(批准/拒绝)
      const [userResponse, setUserResponse] = useState<string | null>(null);

      // 当反馈提交完成时渲染确认信息
      if (status === "complete") {
        return (
          <div style={{ marginTop: 8, textAlign: "right" }}>
            {userResponse || "反馈已提交。"} {/* 显示用户的响应或默认信息 */}
          </div>
        );
      }

      // 当反馈进行中或正在执行时渲染反馈 UI
      if (status === "inProgress" || status === "executing") {
        return (
          <div style={{ marginTop: 8 }}>
            {isExpanded && (
              <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"
                // 存在轻/暗主题支持的任务输出样式容器
              >
                <ReactMarkdown>{feedback.task_output || ""}</ReactMarkdown>
                {/* 要渲染的任务输出作为 Markdown 格式 */}
              </div>
            )}
            <div className="flex justify-end gap-2 mt-2">
              {/* 显示和隐藏任务输出的切换按钮 */}
              <button
                className="px-3 py-2 text-sm font-medium text-gray-700 hover:text-gray-900 transition-colors duration-200"
                onClick={() => setIsExpanded(!isExpanded)} // 切换展开状态
              >
                {isExpanded ? "隐藏" : "显示"}反馈 {/* 动态显示的按钮文本 */}
              </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"); // 点击按钮后,设置本地响应状态为“批准”,如果存在`respond`则发送“批准”给机组
                  respond?.("Approve"); // 如果存在 respond,则发送 "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("Rejected"); // 点击按钮后,设置本地响应状态为“拒绝”,如果存在`respond`则发送“拒绝”给机组
                  respond?.("Reject"); // 如果存在 respond,则发送 "Reject" 给机组
                }}
              >
                拒绝
              </button>
            </div>
          </div>
        );
      }

      // 对于其他状态(如请求反馈之前的状态),返回 null
      return null;
    }

    export { CrewHumanFeedbackRenderer };

全屏 查看全屏 退出全屏

The CrewHumanFeedbackRenderer 组件随后也被导入到 CrewQuickstart 组件中,并被 CopilotKit 的 useCopilotAction 使用来创建一个名为 crew_requesting_feedback 的请求反馈操作,该操作会渲染反馈界面并等待用户回应,如以下描述所示。

    // 导入 CopilotKit 类型和用于机组操作的钩子
    import {
      CrewsResponseStatus, // 用于机组执行状态的类型(例如:"inProgress","complete")
      CrewsStateItem, // 机组状态项目的基类型
      useCopilotAction, // 用于定义自定义机组操作的钩子
    } from "@copilotkit/react-core";

    // 导入自定义反馈渲染器组件
    import { CrewHumanFeedbackRenderer } from "./CrewHumanFeedbackRenderer";

    // 定义反馈接口,扩展 CrewsStateItem
    interface CrewsFeedback extends CrewsStateItem {
      /**

* 任务执行的结果
       */
      task_output?: string; // 任务结果的可选字段
    }

    // 定义 CrewQuickstart 组件的 props 接口
    interface CrewQuickstartProps {
      crewName: string; // 机组/代理的名称
      inputs: Array<string>; // 输入字段名称数组(在当前版本中未使用)
    }

    // 导出带有类型化的 props 的 CrewQuickstart 组件
    export const CrewQuickstart: React.FC<CrewQuickstartProps> = ({
      crewName, // 机组标识符
      inputs, // 必要的输入(在此处未使用)
    }: {
      crewName: string;
      inputs: Array<string>;
    }) => {
      // 定义一个 Copilot 操作来请求用户反馈
      useCopilotAction({
        name: "crew_requesting_feedback", // 操作名称
        description: "请求用户反馈", // 操作描述
        renderAndWaitForResponse(props) {
          // 用于渲染 UI 并等待响应的函数
          const { status, args, respond } = props; // 解构 props
          return (
            // 使用 CrewHumanFeedbackRenderer 渲染反馈 UI
            <CrewHumanFeedbackRenderer
              feedback={args as unknown as CrewsFeedback} // 将操作参数作为反馈传递(由于未知参数类型,进行类型转换)
              respond={respond} // 传递用于发送用户反馈的回调函数
              status={status as CrewsResponseStatus} // 传递当前状态
            />
          );
        },
      });

      return (
        <div className="w-full h-full relative">
          {/* 不使用的额外 UI 占位符 */}
        </div>
      );
    };

点击这里进入全屏,然后可以退出全屏

餐厅CrewAI代理使用CrewHumanFeedbackRenderer组件生成一份城市精选餐厅推荐列表。你可以通过点击下方的点赞或点踩按钮,如下图所示,发送反馈信息。

看看这张图片!这是一只可爱的小猫。图片

步骤九:实时流式传输 CrewAI 代理的响应

为了实时传输你的CrewAI代理响应,你需要在CrewQuickstart组件中以Markdown格式显示船员的响应,如下所示。

    "use client";

    // 导入 CopilotKit 类型和管理团队的钩子
    import { CrewsAgentState, useCoAgent } from "@copilotkit/react-core";

    import { useEffect, useState } from "react";
    // 导入 ReactMarkdown 用于渲染 Markdown 内容
    import ReactMarkdown from "react-markdown";
    // 导入自定义的可调整大小的面板 UI 组件
    import {
      ResizablePanelGroup,
      ResizablePanel,
      ResizableHandle,
    } from "./ui/resizable";
    // 导入用于检测窗口大小的自定义钩子
    import { useWindowSize } from "@/hooks/useWindowSize";

    // 定义组件的属性接口
    interface CrewQuickstartProps {
      crewName: string; // 团队/代理的名称
      inputs: Array<string>; // 必要输入(在本版本中未使用)
    }

    // 导出带有类型属性的 CrewQuickstart 组件
    export const CrewQuickstart: React.FC<CrewQuickstartProps> = ({
      crewName, // 团队标识符
      inputs, // 必要输入(在本版本中未使用)
    }: {
      crewName: string;
      inputs: Array<string>;
    }) => {
      // 从自定义钩子中获取是否为移动设备
      const { isMobile } = useWindowSize();
      // 状态用于设置面板布局的方向
      const [direction, setDirection] = useState<"horizontal" | "vertical">(
        "horizontal" // 默认为水平
      );

      // 效果用于根据移动设备状态更新布局方向
      useEffect(() => {
        setDirection(isMobile ? "vertical" : "horizontal"); // 移动设备时切换为垂直
      }, [isMobile]); // 当 isMobile 发生变化时重新运行

      // 使用 useCoAgent 钩子设置团队并使用自定义状态
      const { state, setState, run } = useCoAgent<
        CrewsAgentState & {
          result: string; // 团队执行结果
          inputs: Record<string, string>; // 用户提供的输入
        }
      >({
        name: crewName, // 从属性中设置团队名称
        initialState: {
          inputs: {}, // 默认为空的输入对象
          result: "团队结果将显示在这里...", // 默认结果占位符
        },
      });

      // 清洗团队名称以供显示,将非字母数字字符替换为空格
      const agentName = crewName.replace(/[^a-zA-Z0-9]/g, " ");

      // 渲染界面
      return (
        <div className="w-full h-full relative">
          {" "}
          {/* 完整尺寸容器 */}
          {/* 可调整大小的面板组以实现响应式布局 */}
          <ResizablePanelGroup direction={direction} className="w-full h-full">
            {/* 左侧/主面板(本版本中为空) */}
            <ResizablePanel defaultSize={60} minSize={30}>
              {/* 这里可以放置其他内容(例如聊天) */}
            </ResizablePanel>

            {/* 一个用于调整大小的面板处理器 */}
            <ResizableHandle withHandle />

            {/* 右侧面板用于显示团队结果 */}
            <ResizablePanel defaultSize={40} minSize={25}>
              <div className="h-full overflow-y-auto bg-gray-50 dark:bg-gray-900 p-3">
                {/* 带有浅色或深色主题的可滚动容器 */}
                <div className="flex flex-col h-full">
                  {/* 标题部分 */}
                  <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>

                  {/* 内容区域 */}
                  <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">
                      {/* 结果的样式容器 */}
                      <ReactMarkdown>{state?.result || ""}</ReactMarkdown>{" "}
                      {/* 将结果以 markdown 形式渲染 */}
                    </div>
                  </div>
                </div>
              </div>
            </ResizablePanel>
          </ResizablePanelGroup>
        </div>
      );
    };

全屏显示 退出全屏

你可以通过点击人工反馈区域的“批准”按钮来实时查看餐厅CrewAI代理的回复。接着,代理就会为你提供一个精选的餐厅名单,如下。

图片
点击图片查看大图

结论:

在这次教程里,我们讲了很多内容。希望你学会如何用CrewAI和Copilotkit构建、运行、部署并和全栈AI代理聊天。

您可以在这里查看完整的源代码:这里

关注 CopilotKit 的 Twitter,打个招呼,说声你好。如果你想搞点有意思的,可以加入 Discord 社区一起交流。

copilotkit 图

CopilotKit 订阅 https://dev.to/copilotkit

React UI + 用于 AI 副驾的优雅基础设施,包括应用内的 AI 代理、聊天机器人和 AI 智能文本框 🪁

點擊查看更多內(nèi)容
TA 點贊

若覺得本文不錯,就分享一下吧!

評論

作者其他優(yōu)質(zhì)文章

正在加載中
  • 推薦
  • 評論
  • 收藏
  • 共同學習,寫下你的評論
感謝您的支持,我會繼續(xù)努力的~
掃碼打賞,你說多少就多少
贊賞金額會直接到老師賬戶
支付方式
打開微信掃一掃,即可進行掃碼打賞哦
今天注冊有機會得

100積分直接送

付費專欄免費學

大額優(yōu)惠券免費領

立即參與 放棄機會
微信客服

購課補貼
聯(lián)系客服咨詢優(yōu)惠詳情

幫助反饋 APP下載

慕課網(wǎng)APP
您的移動學習伙伴

公眾號

掃描二維碼
關注慕課網(wǎng)微信公眾號

舉報

0/150
提交
取消