本文将教你如何构建一个结合了CrewAI、CopilotKit和Serper的人工智能代理的人机协作功能的全栈餐厅查找AI代理工具。
在我们开始之前,这里是我们要聊的内容:
-
CrewAI成员是什么?
-
构建、运行以及部署CrewAI代理程序
- 使用 Copilot Cloud 和 CopilotKit 为 CrewAI 添加前端 UI ,
(注:此处保持了源文本的列表项标识,增加了中文中的逗号以保持一致性)
这里是你将要构建的应用程序的预览。
你知道CrewAI代理是什么吗?想象你正在做一个大团队项目,比如制作一个应用或游戏。你有一个团队成员,每个人都有他们自己的活儿。
一个人擅长后端开发,另一个做UI设计,还有人做测试,等等类似。
相反,人们将 CrewAI 代理人 想象成一群智能的自动化助手——各自承担不同的职责——一起解决问题或完成任务。
你可以在这里可以更多关于CrewAI智能体的信息 CrewAI官方文档
CopilotKit (一个开源的全栈框架)用于构建用户交互式的代理和副驾驶。它使你的代理能够控制你的应用程序,说明它在做什么,并生成完全自定义的用户界面(UI)。
[]
快来访问 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代理程序。然后你将学习如何通过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 模式;你可以选择你喜欢的任何一个,比如 CopilotPopup、CopilotSidebar、CopilotChat 或 无头 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 订阅 https://dev.to/copilotkit共同學習,寫下你的評論
評論加載中...
作者其他優(yōu)質(zhì)文章