构建一个由人工智能驱动的博客平台(Next.js、Langchain 和 CopilotKit)
CopilotKit:开源 Copilot 框架
TL;DR
在本文中,您将学习如何构建一个由人工智能驱动的博客平台,该平台可以搜索网络并研究博客文章的任何主题。
我们将涵盖:
- 用于应用程序框架的 Next.js 🖥️
- OpenAI 法学硕士项目
- LangChain 和 Tavily 打造的网页搜索 AI 代理 🤖
- 使用 CopilotKit 将 AI 集成到您的应用程序中
- Supabase 用于存储和检索博客平台文章数据。

CopilotKit:开源 Copilot 框架
CopilotKit 是开源的 AI Copilot 框架和平台。我们让您能够轻松地将强大的 AI 集成到您的 React 应用中。
建造:
-
ChatBots💬:具有上下文感知能力的应用内聊天机器人,可以在应用内采取行动
-
CopilotTextArea📝:具有上下文感知自动完成和插入功能的 AI 驱动文本字段
-
协同代理🤖:应用内 AI 代理,可与你的应用和用户互动。由 LangChain 提供支持。
现在回到文章。
先决条件
在开始构建应用程序之前,让我们先看看构建应用程序所需的依赖项或包
copilotkit/react-core
:CopilotKit 前端包带有反应钩子,用于向副驾驶提供应用程序状态和操作(AI 功能)copilotkit/react-ui:
聊天机器人侧边栏 UI 的 CopilotKit 前端包copilotkit/react-textarea:
CopilotKit 前端包,用于演示文稿演讲者笔记中的 AI 辅助文本编辑。LangChainJS:
由语言模型驱动的应用程序开发框架。Tavily Search API:
帮助将 LLM 和 AI 应用程序连接到可信实时知识的 API。
安装所有项目包和依赖项
在安装所有项目包和依赖项之前,让我们首先通过在终端上运行以下命令来创建一个 Nextjs 项目。
npx create-next-app@latest
然后,系统会提示您选择一些选项。请随意标记它们,如下所示。
之后,使用您选择的文本编辑器打开新创建的 Nextjs 项目。然后在命令行上运行以下命令来安装所有项目包和依赖项。
npm i @copilotkit/backend @copilotkit/shared @langchain/langgraph @copilotkit/react-core @copilotkit/react-ui @copilotkit/react-textarea @supabase/ssr @supabase/auth-helpers-nextjs
创建博客平台前端
在本节中,我将引导您完成使用静态内容创建博客平台前端的过程,以定义平台的用户界面。
首先,请转到 /[root]/src/app
并创建一个名为的文件夹 components
。在 components 文件夹中,创建一个名为 的文件 Article.tsx
。
之后,将以下代码添加到定义名为 的功能组件的文件中, Article
该组件将用于呈现文章创建表单。
"use client";
import { useRef, useState } from "react";
export function Article() {
// Define state variables for article outline, copilot text, and article title
const [articleOutline, setArticleOutline] = useState("");
const [copilotText, setCopilotText] = useState("");
const [articleTitle, setArticleTitle] = useState("");
return (
// Form element for article input
<form
action={""}
className="w-full h-full gap-10 flex flex-col items-center p-10">
{/* Input field for article title */}
<div className="flex w-full items-start gap-3">
<textarea
className="p-2 w-full h-12 rounded-lg flex-grow overflow-x-auto overflow-y-hidden whitespace-nowrap"
id="title"
name="title"
value={articleTitle}
placeholder="Article Title"
onChange={(event) => setArticleTitle(event.target.value)}
/>
</div>
{/* Textarea for article content */}
<textarea
className="p-4 w-full aspect-square font-bold text-xl bg-slate-800 text-white rounded-lg resize-none"
id="content"
name="content"
value={copilotText}
placeholder="Write your article content here"
onChange={(event) => setCopilotText(event.target.value)}
/>
{/* Publish button */}
<button
type="submit"
className="p-4 w-full !bg-slate-800 text-white rounded-lg">Publish</button>
</form>
);
}
接下来,在 components 文件夹中添加另一个文件,并将其命名为 Header.tsx
。然后将以下代码添加到定义名为 的功能组件的文件中, Header
该组件将呈现博客平台的导航栏。
import Link from "next/link";
export default function Header() {
return (
<>
<header className="flex flex-wrap sm:justify-start sm:flex-nowrap z-50 w-full bg-white border-b border-gray-200 text-sm py-3 sm:py-0 ">
<nav
className="relative max-w-7xl w-full mx-auto px-4 sm:flex sm:items-center sm:justify-between sm:px-6 lg:px-8"
aria-label="Global">
<div className="flex items-center justify-between">
<Link
className="flex-none text-xl font-semibold "
href="/"
aria-label="Brand">
AIBlogging
</Link>
</div>
<div id="navbar-collapse-with-animation" className="">
<div className="flex flex-col gap-y-4 gap-x-0 mt-5 sm:flex-row sm:items-center sm:justify-end sm:gap-y-0 sm:gap-x-7 sm:mt-0 sm:ps-7">
<Link
className="flex items-center font-medium text-gray-500 border-2 border-indigo-600 text-center p-2 rounded-md hover:text-blue-600 sm:border-s sm:my-6 "
href="/writearticle">
Create Post
</Link>
</div>
</div>
</nav>
</header>
</>
);
}
之后,转到 /[root]/src/app
并创建一个名为 的文件夹 writearticle
。在该writearticle
文件夹中,创建一个名为 file 的文件 。然后将以下代码添加到导入和组件的page.tsx
文件中。该代码随后定义了一个名为 的功能组件,它将渲染导航栏和文章创建表单。Article
Header
WriteArticle
import { Article } from "../components/Article";
import Header from "../components/Header";
export default function WriteArticle() {
return (
<>
<Header />
<Article />
</>
);
}
接下来,转到 /[root]/src/page.tsx
文件,并添加以下代码,该代码定义一个名为的功能组件,Home
该组件呈现博客平台主页,显示已发布文章的列表。
import Image from "next/image";
import Link from "next/link";
import Header from "./components/Header";
const Home = async () => {
return (
<>
<Header />
<div className="max-w-[85rem] h-full px-4 py-10 sm:px-6 lg:px-8 lg:py-14 mx-auto">
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-6">
<Link
key={""}
className="group flex flex-col h-full bg-white border border-gray-200 hover:border-transparent hover:shadow-lg transition-all duration-300 rounded-xl p-5 "
href={""}>
<div className="aspect-w-16 aspect-h-11">
<Image
className="object-cover h-48 w-96 rounded-xl"
src={`https://source.unsplash.com/featured/?${encodeURIComponent(
"hello world"
)}`}
width={500}
height={500}
alt="Image Description"
/>
</div>
<div className="my-6">
<h3 className="text-xl font-semibold text-gray-800 ">
Hello World
</h3>
</div>
</Link>
</div>
</div>
</>
);
};
export default Home;
之后,转到next.config.js
文件并添加以下代码,允许您使用来自 Unsplash 的图像作为已发布文章的封面图像。
module.exports = {
images: {
remotePatterns: [
{
protocol: "https",
hostname: "source.unsplash.com",
},
],
},
};
npm run dev
最后,在命令行上 运行该命令 ,然后导航到http://localhost:3000/。现在您应该可以在浏览器上查看博客平台前端,如下所示。
将博客平台与 CopilotKit 后端集成
在本节中,我将引导您完成博客平台与 CopilotKit 后端的集成过程。CopilotKit 后端负责处理来自前端的请求、提供函数调用以及各种 LLM 后端(例如 GPT)。此外,我们还将集成一个名为 Tavily 的 AI 代理,它可以在网络上搜索任何主题。
首先,在根目录中创建一个名为 的文件 。然后在保存您的搜索API 密钥.env.local
的文件中添加以下环境变量。ChatGPT
Tavily
OPENAI_API_KEY="Your ChatGPT API key"
TAVILY_API_KEY="Your Tavily Search API key"
要获取 ChatGPT API 密钥,请导航至 https://platform.openai.com/api-keys。
要获取 Tavily Search API 密钥,请导航至 https://app.tavily.com/home
之后,转到 /[root]/src/app
并创建一个名为 的文件夹 api
。在该 api
文件夹中,创建一个名为 的文件夹 copilotkit
。在该 copilotkit
文件夹中,创建一个名为 的文件 research.ts
。然后导航到此 research.ts gist 文件,复制代码并将其添加到 research.ts
文件中。
route.ts
接下来,在文件夹中 创建一个名为的文件 /[root]/src/app/api/copilotkit
。该文件将包含设置后端功能以处理 POST 请求的代码。它有条件地包含一个“研究”操作,该操作针对给定主题执行研究。
现在在文件顶部导入以下模块。
import { CopilotBackend, OpenAIAdapter } from "@copilotkit/backend"; // For backend functionality with CopilotKit.
import { researchWithLangGraph } from "./research"; // Import a custom function for conducting research.
import { AnnotatedFunction } from "@copilotkit/shared"; // For annotating functions with metadata.
在上面的代码下方,定义一个运行时环境变量和一个名为的函数researchAction
,使用下面的代码对某个主题进行研究。
// Define a runtime environment variable, indicating the environment where the code is expected to run.
export const runtime = "edge";
// Define an annotated function for research. This object includes metadata and an implementation for the function.
const researchAction: AnnotatedFunction<any> = {
name: "research", // Function name.
description: "Call this function to conduct research on a certain topic. Respect other notes about when to call this function", // Function description.
argumentAnnotations: [ // Annotations for arguments that the function accepts.
{
name: "topic", // Argument name.
type: "string", // Argument type.
description: "The topic to research. 5 characters or longer.", // Argument description.
required: true, // Indicates that the argument is required.
},
],
implementation: async (topic) => { // The actual function implementation.
console.log("Researching topic: ", topic); // Log the research topic.
return await researchWithLangGraph(topic); // Call the research function and return its result.
},
};
然后在上面的代码下添加下面的代码来定义一个处理POST请求的异步函数。
// Define an asynchronous function that handles POST requests.
export async function POST(req: Request): Promise<Response> {
const actions: AnnotatedFunction<any>[] = []; // Initialize an array to hold actions.
// Check if a specific environment variable is set, indicating access to certain functionality.
if (process.env["TAVILY_API_KEY"]) {
actions.push(researchAction); // Add the research action to the actions array if the condition is true.
}
// Instantiate CopilotBackend with the actions defined above.
const copilotKit = new CopilotBackend({
actions: actions,
});
// Use the CopilotBackend instance to generate a response for the incoming request using an OpenAIAdapter.
return copilotKit.response(req, new OpenAIAdapter());
}
将博客平台与 CopilotKit 前端集成
在本节中,我将引导您完成将博客平台与 CopilotKit 前端集成的过程,以方便进行博客文章研究和文章大纲生成。我们将使用一个聊天机器人侧边栏组件、一个 Copilot 文本区域组件、一个 useMakeCopilotReadable 钩子(用于向 Copilot 提供应用状态和其他信息)以及一个 useCopilotAction 钩子(用于提供 Copilot 可以调用的操作)。
首先, 在 文件 顶部 导入useMakeCopilotReadable
、、和钩子useCopilotAction
。CopilotTextarea
HTMLCopilotTextAreaElement
/[root]/src/app/components/Article.tsx
import {
useMakeCopilotReadable,
useCopilotAction,
} from "@copilotkit/react-core";
import {
CopilotTextarea,
HTMLCopilotTextAreaElement,
} from "@copilotkit/react-textarea";
在 Article 函数内部,在状态变量下方,添加以下代码,该代码使用 useMakeCopilotReadable
钩子添加文章大纲,该大纲将作为应用内聊天机器人的上下文生成。该钩子使副驾驶能够读取文章大纲。
useMakeCopilotReadable("Blog article outline: " + JSON.stringify(articleOutline));
在钩子下面useMakeCopilotReadable
,使用下面的代码创建copilotTextareaRef
对名为 的 textarea 元素的引用HTMLCopilotTextAreaElement
。
const copilotTextareaRef = useRef<HTMLCopilotTextAreaElement>(null);
在上述代码下方,添加以下代码,该代码使用 useCopilotAction
钩子设置一个名为 的操作, researchBlogArticleTopic
该操作将启用博客文章中指定主题的研究。该操作接受两个名为articleTitle
和articleOutline
的参数,用于生成文章标题和大纲。
该操作包含一个处理函数,该函数根据给定的主题生成文章标题和大纲。在处理函数内部,articleOutline
状态会使用新生成的大纲进行更新,同时articleTitle
状态也会使用新生成的标题进行更新,如下所示。
useCopilotAction(
{
name: "researchBlogArticleTopic",
description: "Research a given topic for a blog article.",
parameters: [
{
name: "articleTitle",
type: "string",
description: "Title for a blog article.",
required: true,
},
{
name: "articleOutline",
type: "string",
description:"Outline for a blog article that shows what the article covers.",
required: true,
},
],
handler: async ({ articleOutline, articleTitle }) => {
setArticleOutline(articleOutline);
setArticleTitle(articleTitle);
},
},
[]
);
在上面的代码下方,转到表单组件并添加以下CopilotTextarea
元素,使您能够向文章内容添加完成、插入和编辑。
<CopilotTextarea
value={copilotText}
ref={copilotTextareaRef}
placeholder="Write your article content here"
onChange={(event) => setCopilotText(event.target.value)}
className="p-4 w-full aspect-square font-bold text-xl bg-slate-800 text-white rounded-lg resize-none"
placeholderStyle={{
color: "white",
opacity: 0.5,
}}
autosuggestionsConfig={{
textareaPurpose: articleTitle,
chatApiConfigs: {
suggestionsApiConfig: {
forwardedParams: {
max_tokens: 5,
stop: ["\n", ".", ","],
},
},
insertionApiConfig: {},
},
debounceTime: 250,
}}
/>
然后将 Tailwindcss 隐藏类添加到用于保存文章内容的 Textarea 中,如下所示。该 Textarea 将保存文章内容,并在文章发布后将其插入数据库。
{/* Textarea for article content */}
<textarea
className="p-4 w-full aspect-square font-bold text-xl bg-slate-800 text-white rounded-lg resize-none hidden"
id="content"
name="content"
value={copilotText}
placeholder="Write your article content here"
onChange={(event) => setCopilotText(event.target.value)}
/>
之后,转到/[root]/src/app/writearticle/page.tsx
文件并使用以下代码在顶部导入 CopilotKit 前端包和样式。
import { CopilotKit } from "@copilotkit/react-core";
import { CopilotSidebar } from "@copilotkit/react-ui";
import "@copilotkit/react-ui/styles.css";
import "@copilotkit/react-textarea/styles.css";
然后使用 CopilotKit
和 CopilotSidebar
包装 Article 组件,如下所示。该CopilotKit
组件指定 CopilotKit 后端端点 ( ) 的 URL,/api/copilotkit/openai/
同时CopilotSidebar
渲染应用内聊天机器人,您可以提示用户研究文章中的任何主题。
export default function WriteArticle() {
return (
<>
<Header />
<CopilotKit url="/api/copilotkit">
<CopilotSidebar
instructions="Help the user research a blog article topic."
defaultOpen={true}
labels={{
title: "Blog Article Copilot",
initial:
"Hi you! 👋 I can help you research any topic for a blog article.",
}}
clickOutsideToClose={false}>
<Article />
</CopilotSidebar>
</CopilotKit>
</>
);
}
之后,运行开发服务器并导航至 http://localhost:3000/writearticle。您应该看到应用内聊天机器人已集成到博客平台中。
给右侧的聊天机器人一个提示,例如“研究一个关于生成式人工智能的博客文章主题,并给我文章大纲。”聊天机器人将开始研究该主题,然后生成博客标题。
当您开始在编辑器上写作时,您应该会看到内容自动建议,如下所示。
将博客平台与 Supabase 数据库集成
在本节中,我将引导您完成将博客平台与 Supabase 数据库集成以插入和获取博客文章数据的过程。
首先,导航至supabase.com并单击主页上的“开始您的项目”按钮。
然后创建一个名为AiBloggingPlatform的新项目,如下所示。
创建项目后,将您的 Supabase URL 和 API 密钥添加到 env.local 文件中的环境变量中,如下所示。
NEXT_PUBLIC_SUPABASE_URL=”Your Supabase URL”
NEXT_PUBLIC_SUPABASE_ANON_KEY=”Your Supabase API Key”
之后,转到 Supabase 上的项目仪表板并打开 SQL 编辑器部分。然后将以下 SQL 代码添加到编辑器中,并单击 CTRL + Enter 键以创建一个名为“articles”的表。articles 表包含 id、title 和 content 行。
create table if not exists
articles (
id bigint primary key generated always as identity,
title text,
content text
);
一旦表创建完成,您应该会收到一条成功消息,如下所示。
之后,转到/[root]/src/
文件夹并创建一个名为 的文件夹utils
。在该utils
文件夹内,创建一个名为 的文件supabase.ts
,并添加以下代码,用于创建并返回 Supabase 客户端。
// Importing necessary functions and types from the Supabase SSR package
import { createServerClient, type CookieOptions } from '@supabase/ssr'
// Define a function named 'supabase' that takes a 'CookieOptions' object as input
export const supabase = (cookies: CookieOptions) => {
// Retrieve cookies from the provided 'CookieOptions' object
const cookieStore = cookies()
// Create and return a Supabase client configured with environment variables and cookie handling
return createServerClient(
// Retrieve Supabase URL from environment variables
process.env.NEXT_PUBLIC_SUPABASE_URL!,
// Retrieve Supabase anonymous key from environment variables
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
// Define a custom 'get' function to retrieve cookies by name from the cookie store
get(name: string) {
return cookieStore.get(name)?.value
},
},
}
)
}
然后转到/[root]/src/app
文件夹并创建一个名为的文件夹serveractions
。在该serveractions
文件夹中,创建一个名为的文件AddArticle.ts
,并添加以下代码,将博客文章数据插入 Supabase 数据库。
// Importing necessary functions and modules for server-side operations
"use server";
import { createServerComponentClient } from "@supabase/auth-helpers-nextjs";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
// Define an asynchronous function named 'addArticle' that takes form data as input
export async function addArticle(formData: any) {
// Extract title and content from the provided form data
const title = formData.get("title");
const content = formData.get("content");
// Retrieve cookies from the HTTP headers
const cookieStore = cookies();
// Create a Supabase client configured with the provided cookies
const supabase = createServerComponentClient({ cookies: () => cookieStore });
// Insert the article data into the 'articles' table on Supabase
const { data, error } = await supabase.from("articles").insert([
{
title,
content,
},
]);
// Check for errors during the insertion process
if (error) {
console.error("Error inserting data", error);
return;
}
// Redirect the user to the home page after successfully adding the article
redirect("/");
// Return a success message
return { message: "Success" };
}
之后,转到/[root]/src/app/components/Article.tsx
文件并导入该addArticle
函数。
import { addArticle } from "../serveractions/AddArticle";
然后添加该addArticle
函数作为表单操作参数,如下所示。
// Form element for article input
<form
action={addArticle}
className="w-full h-full gap-10 flex flex-col items-center p-10">
</form>
之后,导航到 http://localhost:3000/writearticle,研究您选择的主题,添加文章内容,然后单击底部的发布按钮以发布文章。
然后前往 Supabase 上的项目仪表板,并导航到“表编辑器”部分。您应该会看到文章数据已插入 Supabase 数据库,如下所示。
接下来,转到/[root]/src/app/page.tsx
文件并在顶部导入 cookies 和 supabase 包。
import { cookies } from "next/headers";
import { supabase } from "@/utils/supabase";
然后在 Home 函数中,添加以下从 Supabase 数据库获取文章数据的代码。
const { data: articles, error } = await supabase(cookies).from('articles').select('*')
之后,更新元素代码如下所示,以在博客平台主页上呈现已发布的文章。
return (
<>
<Header />
<div className="max-w-[85rem] h-full px-4 py-10 sm:px-6 lg:px-8 lg:py-14 mx-auto">
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-6">
{articles?.map((post: any) => (
<Link
key={post.id}
className="group flex flex-col h-full bg-white border border-gray-200 hover:border-transparent hover:shadow-lg transition-all duration-300 rounded-xl p-5 "
href={`/posts/${post.id}`}>
<div className="aspect-w-16 aspect-h-11">
<Image
className="object-cover h-48 w-96 rounded-xl"
src={`https://source.unsplash.com/featured/?${encodeURIComponent(
post.title
)}`}
width={500}
height={500}
alt="Image Description"
/>
</div>
<div className="my-6">
<h3 className="text-xl font-semibold text-gray-800 ">
{post.title}
</h3>
</div>
</Link>
))}
</div>
</div>
</>
);
然后导航到 http://localhost:3000您应该会看到您发布的文章,如下所示。
之后,转到/[root]/src/app
文件夹并创建一个名为的文件夹[id].
在[id]
文件夹中,创建一个名为的文件page.tsx
并在顶部导入以下包和组件。
import { supabase } from '@/utils/supabase';
import { cookies } from "next/headers";
import Header from '@/app/components/Header';
在导入下方,定义一个名为“getArticles”的异步函数,该函数根据 id 参数从 supabase 数据库中检索文章数据,如下所示。
// Define an asynchronous function named 'getArticles' that retrieves article data based on the provided parameters
async function getArticles(params: any) {
// Extract the 'id' parameter from the provided 'params' object
const { id } = params
// Retrieve article data from Supabase database where the 'id' matches the provided value
const { data, error } = await supabase(cookies)
.from('articles')
.select('*')
.eq('id', id)
.single();
// Return the retrieved data
return data
}
在上面的代码下方,定义一个名为“Post”的函数,该函数以“params”作为属性,如下所示。
// Define a default asynchronous function named 'Post' that takes 'params' as props
export default async function Post({ params }: { params: any }) {
// Retrieve the post data asynchronously based on the provided 'params'
const post = await getArticles(params);
// Return JSX to render the post details
return (
<>
{/* Render the header component */}
<Header />
{/* Main content wrapper */}
<div className="max-w-3xl px-4 pt-6 lg:pt-10 pb-12 sm:px-6 lg:px-8 mx-auto">
<div className="max-w-2xl">
<div className="space-y-5 md:space-y-8">
<div className="space-y-3">
{/* Render the post title */}
<h2 className="text-2xl font-bold md:text-3xl dark:text-white">
{/* Render the post title only if 'post' is truthy */}
{post && post.title}
</h2>
{/* Render the post content */}
<p className="text-lg text-gray-800 dark:text-gray-200">
{/* Render the post content only if 'post' is truthy */}
{post && post.content}
</p>
</div>
</div>
</div>
</div>
</>
);
}
之后,导航到 http://localhost:3000并单击博客平台主页上显示的文章。
然后您将被重定向到文章的内容,如下所示。
结论
总而言之,您可以使用 CopilotKit 构建应用内 AI 聊天机器人,它可以查看当前应用状态并在应用内执行操作。该 AI 聊天机器人可以与您的应用前端、后端以及第三方服务进行通信。
完整源代码:https://github.com/TheGreatBonnie/aipoweredblog
文章来源:https://dev.to/copilotkit/how-to-build-an-ai-powered-blogging-platform-nextjs-langchain-supabase-1hdp