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

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

立即開始免費試讀!

人工智慧驅動的應用程式正在不斷發展,不僅僅是執行任務的自主代理。一種涉及人機互動的新方法允許用戶提供回饋、審查結果並決定人工智慧的後續步驟。這些執行時代理程式稱為 CoAgent。

長話短說

在本教程中,您將學習如何使用LangGraphCopilotKitTavily建立 Perplexity 克隆。

是時候開始建造了!

什麼是特工副駕駛?

Agentic copilots 是 CopilotKit 將 LangGraph 代理程式引入您的應用程式的方式。

CoAgents是 CopilotKit 建立代理體驗的方法!

簡而言之,它將透過執行多個搜尋查詢來處理使用者請求,並將搜尋狀態和結果即時傳回客戶端。

{% cta https://git.new/devtoarticle1 %} 查看 CopilotKit ⭐️ {% endcta %}


先決條件

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

我們還將利用以下內容:

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

  • LangGraph - 用於建立和部署人工智慧代理的框架。它還有助於定義代理要執行的控制流程和操作。

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

  • Tavily AI - 一個搜尋引擎,使人工智慧代理能夠在應用程式中進行研究並存取即時知識。

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

  • Shad Cn UI - 提供應用程式內可重複使用 UI 元件的集合。


如何使用 LangGraph 和 CopilotKit 建立 AI 代理

在本部分中,您將學習如何使用 LangGraph 和 CopilotKit 建立 AI 代理程式。

首先,複製CopilotKit CoAgents 入門儲存庫ui目錄包含 Next.js 應用程式的前端, agent目錄包含應用程式的 CoAgent。

agent目錄中,使用Poetry安裝專案相依性。

cd agent
poetry install

在代理資料夾中建立一個.env文件,並將OpenAITavily AI API 金鑰複製到該文件中:

OPENAI_API_KEY=
TAVILY_API_KEY=

取得 OpenAI API 金鑰

將以下程式碼片段複製到agent.py檔案中:

"""
This is the main entry point for the AI.
It defines the workflow graph and the entry point for the agent.
"""
# pylint: disable=line-too-long, unused-import
from langgraph.graph import StateGraph, END
from langgraph.checkpoint.memory import MemorySaver

from ai_researcher.state import AgentState
from ai_researcher.steps import steps_node
from ai_researcher.search import search_node
from ai_researcher.summarize import summarize_node
from ai_researcher.extract import extract_node

def route(state):
    """Route to research nodes."""
    if not state.get("steps", None):
        return END

    current_step = next((step for step in state["steps"] if step["status"] == "pending"), None)

    if not current_step:
        return "summarize_node"

    if current_step["type"] == "search":
        return "search_node"

    raise ValueError(f"Unknown step type: {current_step['type']}")

# Define a new graph
workflow = StateGraph(AgentState)
workflow.add_node("steps_node", steps_node)
workflow.add_node("search_node", search_node)
workflow.add_node("summarize_node", summarize_node)
workflow.add_node("extract_node", extract_node)
# Chatbot
workflow.set_entry_point("steps_node")

workflow.add_conditional_edges(
    "steps_node", 
    route,
    ["summarize_node", "search_node", END]
)

workflow.add_edge("search_node", "extract_node")

workflow.add_conditional_edges(
    "extract_node",
    route,
    ["summarize_node", "search_node"]
)

workflow.add_edge("summarize_node", END)

memory = MemorySaver()
graph = workflow.compile(checkpointer=memory)

上面的程式碼片段定義了 LangGraph 代理程式工作流程。它從steps_node開始,搜尋結果,總結結果,提取關鍵點。

代理商工作流程

接下來建立一個demo.py文件,其中包含以下程式碼片段:


"""Demo"""

import os
from dotenv import load_dotenv
load_dotenv()

from fastapi import FastAPI
import uvicorn
from copilotkit.integrations.fastapi import add_fastapi_endpoint
from copilotkit import CopilotKitSDK, LangGraphAgent
from ai_researcher.agent import graph

app = FastAPI()
sdk = CopilotKitSDK(
    agents=[
        LangGraphAgent(
            name="ai_researcher",
            description="Search agent.",
            graph=graph,
        )
    ],
)

add_fastapi_endpoint(app, sdk, "/copilotkit")

# add new route for health check
@app.get("/health")
def health():
    """Health check."""
    return {"status": "ok"}

def main():
    """Run the uvicorn server."""
    port = int(os.getenv("PORT", "8000"))
    uvicorn.run("ai_researcher.demo:app", host="0.0.0.0", port=port, reload=True)

上面的程式碼會建立一個託管 LangGraph 代理並將其連接到 CopilotKit SDK 的 FastAPI 端點。

您可以從GitHub 儲存庫複製用於建立 CoAgent 的其餘程式碼。在以下部分中,您將了解如何為 Perplexity 克隆建立使用者介面並使用 CopilotKit 處理搜尋請求。


使用 Next.js 建立應用程式介面

在本節中,我將引導您完成建立應用程式使用者介面的過程。

首先,透過執行以下程式碼片段來建立一個 Next.js Typescript 專案:

# 👉🏻 Navigate into the ui folder
npx create-next-app ./

Next.js 安裝

透過執行以下程式碼片段將ShadCn UI庫安裝到新建立的專案中:

npx shadcn@latest init

接下來,在 Next.js 專案的根目錄中建立components資料夾,然後將ui資料夾從此 GitHub 儲存庫複製到該資料夾中。 Shadcn 可讓您透過命令列安裝各種元件,輕鬆地將它們新增至您的應用程式。

除了 Shadcn 元件之外,您還需要建立一些代表應用程式介面不同部分的元件。在components資料夾中執行以下程式碼片段,將這些元件加入 Next.js 專案中:

touch ResearchWrapper.tsx ResultsView.tsx HomeView.tsx
touch AnswerMarkdown.tsx Progress.tsx SkeletonLoader.tsx

將下面的程式碼片段複製到app/page.tsx檔案中:

"use client";

import { ResearchWrapper } from "@/components/ResearchWrapper";
import { ModelSelectorProvider, useModelSelectorContext } from "@/lib/model-selector-provider";
import { ResearchProvider } from "@/lib/research-provider";
import { CopilotKit } from "@copilotkit/react-core";
import "@copilotkit/react-ui/styles.css";

export default function ModelSelectorWrapper() {

  return (
      <CopilotKit runtimeUrl={useLgc ? "/api/copilotkit-lgc" : "/api/copilotkit"} agent="ai_researcher">
        <ResearchProvider>
          <ResearchWrapper />
        </ResearchProvider>
      </CopilotKit>
  );
}

在上面的程式碼片段中, ResearchProvider是一個自訂的 React 上下文提供程序,它共享使用者的搜尋查詢和結果,使應用程式中的所有元件都可以存取它們。 ResearchWrapper元件包含核心應用程式元素並管理 UI。

在 Next.js 專案的根目錄下建立一個包含research-provider.tsx檔案的lib資料夾,並將下列程式碼複製到該檔案:

import { createContext, useContext, useState, ReactNode, useEffect } from "react";

type ResearchContextType = {
  researchQuery: string;
  setResearchQuery: (query: string) => void;
  researchInput: string;
  setResearchInput: (input: string) => void;
  isLoading: boolean;
  setIsLoading: (loading: boolean) => void;
  researchResult: ResearchResult | null;
  setResearchResult: (result: ResearchResult) => void;
};

type ResearchResult = {
  answer: string;
  sources: string[];
}

const ResearchContext = createContext<ResearchContextType | undefined>(undefined);

export const ResearchProvider = ({ children }: { children: ReactNode }) => {
  const [researchQuery, setResearchQuery] = useState<string>("");
  const [researchInput, setResearchInput] = useState<string>("");
  const [researchResult, setResearchResult] = useState<ResearchResult | null>(null);
  const [isLoading, setIsLoading] = useState<boolean>(false);

  useEffect(() => {
    if (!researchQuery) {
      setResearchResult(null);
      setResearchInput("");
    }
  }, [researchQuery, researchResult]);

  return (
    <ResearchContext.Provider
      value={{
        researchQuery,
        setResearchQuery,
        researchInput,
        setResearchInput,
        isLoading,
        setIsLoading,
        researchResult,
        setResearchResult,
      }}
    >
      {children}
    </ResearchContext.Provider>
  );
};

export const useResearchContext = () => {
  const context = useContext(ResearchContext);
  if (context === undefined) {
    throw new Error("useResearchContext must be used within a ResearchProvider");
  }
  return context;
};

狀態被聲明並保存到ResearchContext以確保它們在應用程式內的多個元件中得到正確的管理。

建立一個ResearchWrapper元件,如下所示:

import { HomeView } from "./HomeView";
import { ResultsView } from "./ResultsView";
import { AnimatePresence } from "framer-motion";
import { useResearchContext } from "@/lib/research-provider";

export function ResearchWrapper() {
  const { researchQuery, setResearchInput } = useResearchContext();

  return (
    <>
      <div className="flex flex-col items-center justify-center relative z-10">
        <div className="flex-1">
          {researchQuery ? (
            <AnimatePresence
              key="results"
              onExitComplete={() => {
                setResearchInput("");
              }}
              mode="wait"
            >
              <ResultsView key="results" />
            </AnimatePresence>
          ) : (
            <AnimatePresence key="home" mode="wait">
              <HomeView key="home" />
            </AnimatePresence>
          )}
        </div>
        <footer className="text-xs p-2">
          <a
            href="https://copilotkit.ai"
            target="_blank"
            rel="noopener noreferrer"
            className="text-slate-600 font-medium hover:underline"
          >
            Powered by CopilotKit 🪁
          </a>
        </footer>
      </div>
    </>
  );
}

ResearchWrapper元件將HomeView元件呈現為預設視圖,並在提供搜尋查詢時顯示ResultViewuseResearchContext掛鉤使我們能夠存取researchQuery狀態並相應地更新視圖。

最後,建立HomeView元件來渲染應用程式主頁介面。

"use client";

import { useEffect, useState } from "react";
import { Textarea } from "./ui/textarea";
import { cn } from "@/lib/utils";
import { Button } from "./ui/button";
import { CornerDownLeftIcon } from "lucide-react";
import { useResearchContext } from "@/lib/research-provider";
import { motion } from "framer-motion";
import { useCoAgent } from "@copilotkit/react-core";
import { TextMessage, MessageRole } from "@copilotkit/runtime-client-gql";
import type { AgentState } from "../lib/types";
import { useModelSelectorContext } from "@/lib/model-selector-provider";

const MAX_INPUT_LENGTH = 250;

export function HomeView() {
  const { setResearchQuery, researchInput, setResearchInput } =
    useResearchContext();
  const { model } = useModelSelectorContext();
  const [isInputFocused, setIsInputFocused] = useState(false);
  const {
    run: runResearchAgent,
  } = useCoAgent<AgentState>({
    name: "ai_researcher",
    initialState: {
      model,
    },
  });

  const handleResearch = (query: string) => {
    setResearchQuery(query);
    runResearchAgent(() => {
      return new TextMessage({
        role: MessageRole.User,
        content: query,
      });
    });
  };

  const suggestions = [
    { label: "Electric cars sold in 2024 vs 2023", icon: "🚙" },
    { label: "Top 10 richest people in the world", icon: "💰" },
    { label: "Population of the World", icon: "🌍 " },
    { label: "Weather in Seattle VS New York", icon: "⛅️" },
  ];

  return (
    <motion.div
      initial={{ opacity: 0, y: -50 }}
      animate={{ opacity: 1, y: 0 }}
      exit={{ opacity: 0 }}
      transition={{ duration: 0.4 }}
      className="h-screen w-full flex flex-col gap-y-2 justify-center items-center p-4 lg:p-0"
    >
      <h1 className="text-4xl font-extralight mb-6">
        What would you like to know?
      </h1>

      <div
        className={cn(
          "w-full bg-slate-100/50 border shadow-sm rounded-md transition-all",
          {
            "ring-1 ring-slate-300": isInputFocused,
          }
        )}
      >
        <Textarea
          placeholder="Ask anything..."
          className="bg-transparent p-4 resize-none focus-visible:ring-0 focus-visible:ring-offset-0 border-0 w-full"
          onFocus={() => setIsInputFocused(true)}
          onBlur={() => setIsInputFocused(false)}
          value={researchInput}
          onChange={(e) => setResearchInput(e.target.value)}
          onKeyDown={(e) => {
            if (e.key === "Enter" && !e.shiftKey) {
              e.preventDefault();
              handleResearch(researchInput);
            }
          }}
          maxLength={MAX_INPUT_LENGTH}
        />
        <div className="text-xs p-4 flex items-center justify-between">
          <div
            className={cn("transition-all duration-300 mt-4 text-slate-500", {
              "opacity-0": !researchInput,
              "opacity-100": researchInput,
            })}
          >
            {researchInput.length} / {MAX_INPUT_LENGTH}
          </div>
          <Button
            size="sm"
            className={cn("rounded-full transition-all duration-300", {
              "opacity-0 pointer-events-none": !researchInput,
              "opacity-100": researchInput,
            })}
            onClick={() => handleResearch(researchInput)}
          >
            Research
            <CornerDownLeftIcon className="w-4 h-4 ml-2" />
          </Button>
        </div>
      </div>
      <div className="grid grid-cols-2 w-full gap-2 text-sm">
        {suggestions.map((suggestion) => (
          <div
            key={suggestion.label}
            onClick={() => handleResearch(suggestion.label)}
            className="p-2 bg-slate-100/50 rounded-md border col-span-2 lg:col-span-1 flex cursor-pointer items-center space-x-2 hover:bg-slate-100 transition-all duration-300"
          >
            <span className="text-base">{suggestion.icon}</span>
            <span className="flex-1">{suggestion.label}</span>
          </div>
        ))}
      </div>
    </motion.div>
  );
}

申請首頁


如何將 CoAgent 連接到 Next.js 應用程式

在本部分中,您將了解如何將 CopilotKit CoAgent 連接到 Next.js 應用程式,以使用戶能夠在應用程式中執行搜尋操作。

安裝以下 CopilotKit 軟體包和OpenAI Node.js SDK 。 CopilotKit 套件允許協同代理與 React 狀態值互動並在應用程式內做出決策。

npm install @copilotkit/react-core @copilotkit/react-ui @copilotkit/runtime @copilotkit/runtime-client-gql openai

在 Next.js app資料夾中建立一個api資料夾。在api資料夾內,建立一個包含route.ts檔案的copilotkit目錄。這將建立一個 API 端點 ( /api/copilotkit ),將前端應用程式連接到 CopilotKit CoAgent。

cd app
mkdir api && cd api
mkdir copilotkit && cd copilotkit
touch route.ts

將下面的程式碼片段複製到api/copilotkit/route.ts檔案中:

import { NextRequest } from "next/server";
import {
  CopilotRuntime,
  OpenAIAdapter,
  copilotRuntimeNextJSAppRouterEndpoint,
} from "@copilotkit/runtime";
import OpenAI from "openai";

//👇🏻 initializes OpenAI as the adapter
const openai = new OpenAI();
const serviceAdapter = new OpenAIAdapter({ openai } as any);

//👇🏻 connects the CopilotKit runtime to the CoAgent
const runtime = new CopilotRuntime({
  remoteEndpoints: [
    {
      url: process.env.REMOTE_ACTION_URL || "http://localhost:8000/copilotkit",
    },
  ],
});

export const POST = async (req: NextRequest) => {
  const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
    runtime,
    serviceAdapter,
    endpoint: "/api/copilotkit",
  });

  return handleRequest(req);
};

上面的程式碼片段在/api/copilotkit API 端點設定 CopilotKit 執行時,允許 CopilotKit 透過 AI 協同代理處理使用者請求。

最後,透過使用CopilotKit 元件包裝整個應用程式來更新app/page.tsx ,該元件為所有應用程式元件提供 copilot 上下文。

"use client";

import { ModelSelector } from "@/components/ModelSelector";
import { ResearchWrapper } from "@/components/ResearchWrapper";
import { ModelSelectorProvider, useModelSelectorContext } from "@/lib/model-selector-provider";
import { ResearchProvider } from "@/lib/research-provider";
import { CopilotKit } from "@copilotkit/react-core";
import "@copilotkit/react-ui/styles.css";

export default function ModelSelectorWrapper() {
  return (
      <main className="flex flex-col items-center justify-between">
        <ModelSelectorProvider>
            <Home/>
          <ModelSelector />
        </ModelSelectorProvider>
      </main>
  );
}

function Home() {
  const { useLgc } = useModelSelectorContext();

  return (
      <CopilotKit runtimeUrl={useLgc ? "/api/copilotkit-lgc" : "/api/copilotkit"} agent="ai_researcher">
        <ResearchProvider>
          <ResearchWrapper />
        </ResearchProvider>
      </CopilotKit>
  );
}

CopilotKit 元件包裝了整個應用程式並接受兩個 props - runtimeUrlagentruntimeUrl是託管 AI 代理程式的後端 API 路由,而agent是執行操作的代理程式的名稱。

接受請求並將回應串流傳輸到前端

為了使 CopilotKit 能夠存取和處理使用者輸入,它提供了useCoAgent掛鉤,該掛鉤允許從應用程式內的任何位置存取代理程式的狀態。

例如,下面的程式碼片段示範如何使用useCoAgent掛鉤。 state變數允許存取代理程式的目前狀態, setState用於修改狀態, run函數使用代理執行指令。 startstop函數啟動和停止代理的執行。

const { state, setState, run, start, stop } = useCoAgent({
    name: "search_agent",
});

更新HomeView元件以在提供搜尋查詢時執行代理程式。

//👇🏻 import useCoAgent hook from CopilotKit
import { useCoAgent } from "@copilotkit/react-core";

const { run: runResearchAgent } = useCoAgent({
    name: "search_agent",
});

const handleResearch = (query: string) => {
    setResearchQuery(query);
    runResearchAgent(query); //👉🏻 starts the agent execution
};

接下來,您可以透過存取useCoAgent掛鉤中的狀態變數,將搜尋結果串流傳輸到ResultsView 。將下面的程式碼片段複製到ResultsView元件中。

"use client";

import { useResearchContext } from "@/lib/research-provider";
import { motion } from "framer-motion";
import { BookOpenIcon, LoaderCircleIcon, SparkleIcon } from "lucide-react";
import { SkeletonLoader } from "./SkeletonLoader";
import { useCoAgent } from "@copilotkit/react-core";
import { Progress } from "./Progress";
import { AnswerMarkdown } from "./AnswerMarkdown";

export function ResultsView() {
    const { researchQuery } = useResearchContext();
    //👇🏻 agent state
    const { state: agentState } = useCoAgent({
        name: "search_agent",
    });

    console.log("AGENT_STATE", agentState);

    //👇🏻 keeps track of the current agent processing state
    const steps =
        agentState?.steps?.map((step: any) => {
            return {
                description: step.description || "",
                status: step.status || "pending",
                updates: step.updates || [],
            };
        }) || [];

    const isLoading = !agentState?.answer?.markdown;

    return (
        <motion.div
            initial={{ opacity: 0, y: -50 }}
            animate={{ opacity: 1, y: 0 }}
            exit={{ opacity: 0, y: -50 }}
            transition={{ duration: 0.5, ease: "easeOut" }}
        >
            <div className='max-w-[1000px] p-8 lg:p-4 flex flex-col gap-y-8 mt-4 lg:mt-6 text-sm lg:text-base'>
                <div className='space-y-4'>
                    <h1 className='text-3xl lg:text-4xl font-extralight'>
                        {researchQuery}
                    </h1>
                </div>

                <Progress steps={steps} />

                <div className='grid grid-cols-12 gap-8'>
                    <div className='col-span-12 lg:col-span-8 flex flex-col'>
                        <h2 className='flex items-center gap-x-2'>
                            {isLoading ? (
                                <LoaderCircleIcon className='animate-spin w-4 h-4 text-slate-500' />
                            ) : (
                                <SparkleIcon className='w-4 h-4 text-slate-500' />
                            )}
                            Answer
                        </h2>

                        <div className='text-slate-500 font-light'>
                            {isLoading ? (
                                <SkeletonLoader />
                            ) : (
                                <AnswerMarkdown markdown={agentState?.answer?.markdown} /> //👈🏼 displays search results
                            )}
                        </div>
                    </div>

                    {agentState?.answer?.references?.length && (
                        <div className='flex col-span-12 lg:col-span-4 flex-col gap-y-4 w-[200px]'>
                            <h2 className='flex items-center gap-x-2'>
                                <BookOpenIcon className='w-4 h-4 text-slate-500' />
                                References
                            </h2>
                            <ul className='text-slate-900 font-light text-sm flex flex-col gap-y-2'>
                                {agentState?.answer?.references?.map(
                                    (ref: any, idx: number) => (
                                        <li key={idx}>
                                            <a
                                                href={ref.url}
                                                target='_blank'
                                                rel='noopener noreferrer'
                                            >
                                                {idx + 1}. {ref.title}
                                            </a>
                                        </li>
                                    )
                                )}
                            </ul>
                        </div>
                    )}
                </div>
            </div>
        </motion.div>
    );
}

上面的程式碼片段從代理的狀態檢索搜尋結果,並使用useCoAgent掛鉤將它們串流傳輸到前端。搜尋結果以 Markdown 格式傳回並傳遞到AnswerMarkdown元件,該元件在頁面上呈現內容。

最後,將下面的程式碼片段複製到AnswerMarkdown元件中。這將使用React Markdown 庫將 Markdown 內容呈現為格式化文字。

import Markdown from "react-markdown";

export function AnswerMarkdown({ markdown }: { markdown: string }) {
    return (
        <div className='markdown-wrapper'>
            <Markdown>{markdown}</Markdown>
        </div>
    );
}

結果視圖元件

恭喜!您已完成本教學的專案。您也可以在這裡觀看影片錄製:

完整的網路研討會錄音


把它包起來

當 LLM 智慧與人類智慧一起工作時,它是最有效的,而CopilotKit CoAgents允許您在短短幾分鐘內將 AI 代理、副駕駛和各種類型的助手整合到您的軟體應用程式中。

如果您需要建立 AI 產品或將 AI 代理整合到您的應用程式中,您應該考慮 CopilotKit。

本教學的源程式碼可在 GitHub 上取得:

https://github.com/CopilotKit/CopilotKit/tree/main/examples/coagents-ai-researcher

感謝您的閱讀!


原文出處:https://dev.to/copilotkit/build-a-clone-of-perplexity-with-langgraph-copilotkit-tavily-nextjs-23j2


共有 0 則留言


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

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

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

立即開始免費試讀!