با LangGraph، CopilotKit، Tavily و Next.js یک کلون از Perplexity بسازید.

برنامه های کاربردی مبتنی بر هوش مصنوعی فراتر از عوامل مستقلی هستند که وظایف را انجام می دهند. یک رویکرد جدید شامل Human-in-the-Loop به کاربران این امکان را می دهد که بازخورد ارائه دهند، نتایج را بررسی کنند و در مورد مراحل بعدی هوش مصنوعی تصمیم بگیرند. این عوامل زمان اجرا به عنوان CoAgents شناخته می شوند.
TL; DR
در این آموزش، نحوه ساخت یک کلون Perplexity با استفاده از LangGraph، CopilotKit و Tavily را خواهید آموخت.
زمان شروع ساخت و ساز
خلبان عامل چیست؟
CopilotKit چگونه عوامل LangGraph را به برنامه شما می آورد.
CoAgents رویکرد CopilotKit برای ایجاد تجربیات نمایندگی است!
بهطور خلاصه، درخواستهای کاربر را با انجام چندین پرسوجوی جستجو انجام میدهد و جستجو را با وضعیت و نتایج در زمان واقعی برای مشتری ارسال میکند.
CopilotKit را بررسی کنید ⭐️
پیش نیازها
برای درک کامل این آموزش، باید درک اولیه ای از React یا Next.js داشته باشید.
ما همچنین از موارد زیر استفاده خواهیم کرد:
-
Python – یک زبان برنامه نویسی محبوب برای ساخت عوامل هوش مصنوعی با LangGraph. مطمئن شوید که روی کامپیوتر شما نصب شده است.
-
LangGraph – چارچوبی برای ایجاد و استقرار عوامل هوش مصنوعی. همچنین به تعریف جریان های کنترلی و اقداماتی که باید توسط عامل انجام شود کمک می کند.
-
کلید OpenAI API – به ما امکان می دهد تا با استفاده از مدل های GPT وظایف مختلفی را انجام دهیم. برای این آموزش، مطمئن شوید که به مدل GPT-4 دسترسی دارید.
-
Tavily AI – یک موتور جستجو که عوامل هوش مصنوعی را قادر می سازد تحقیقات انجام دهند و به دانش زمان واقعی در برنامه دسترسی پیدا کنند.
-
CopilotKit – یک چارچوب منبع باز کمکی برای ساخت چت ربات های سفارشی هوش مصنوعی، عوامل هوش مصنوعی درون برنامه ای و مناطق متنی.
-
Shad Cn UI – مجموعه ای از اجزای رابط کاربری قابل استفاده مجدد را در برنامه ارائه می دهد.
نحوه ایجاد عوامل هوش مصنوعی با LangGraph و CopilotKit
در این بخش، نحوه ایجاد یک عامل هوش مصنوعی با استفاده از LangGraph و CopilotKit را یاد خواهید گرفت.
ابتدا مخزن استارتر CopilotKit CoAgents را کلون کنید. این ui
دایرکتوری شامل frontend برای برنامه Next.js و agent
دایرکتوری CoAgent را برای برنامه نگه می دارد.
در داخل agent
دایرکتوری، وابستگی های پروژه را با استفاده از Poetry نصب کنید.
cd agent
poetry install
ایجاد یک .env
در پوشه agent فایل کنید و کلیدهای OpenAI و Tavily AI API خود را در فایل کپی کنید:
OPENAI_API_KEY=
TAVILY_API_KEY=
قطعه کد زیر را در قسمت کپی کنید 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)
کد بالا یک نقطه پایانی FastAPI ایجاد می کند که عامل LangGraph را میزبانی می کند و آن را به CopilotKit SDK متصل می کند.
می توانید کد باقیمانده برای ایجاد CoAgent را از مخزن GitHub کپی کنید. در بخشهای بعدی، نحوه ساخت رابط کاربری برای کلون Perplexity و رسیدگی به درخواستهای جستجو با استفاده از CopilotKit را خواهید آموخت.
ساخت رابط برنامه با Next.js
در این بخش، شما را از طریق فرآیند ساخت رابط کاربری برای برنامه راهنمایی خواهم کرد.
ابتدا با اجرای قطعه کد زیر یک پروژه Next.js Typescript ایجاد کنید:
# 👉🏻 Navigate into the ui folder
npx create-next-app ./
با اجرای قطعه کد زیر، کتابخانه ShadCn UI را در پروژه جدید ایجاد شده نصب کنید:
npx shadcn@latest init
بعد، a ایجاد کنید components
پوشه در ریشه پروژه Next.js، سپس آن را کپی کنید 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
جزء شامل عناصر اصلی برنامه است و رابط کاربری را مدیریت می کند.
ایجاد یک lib
پوشه حاوی الف research-provider.tsx
فایل را در ریشه پروژه Next.js قرار دهید و کد زیر را در فایل کپی کنید:
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
جزء به عنوان نمای پیش فرض و نمایش داده می شود ResultView
هنگامی که یک عبارت جستجو ارائه می شود. این useResearchContext
قلاب ما را قادر می سازد تا به 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
ایجاد کنید api
پوشه در Next.js app
پوشه در داخل api
پوشه، ایجاد یک copilotkit
دایرکتوری حاوی الف route.ts
فایل این یک نقطه پایانی API ایجاد می کند (/api/copilotkit
) که برنامه frontend را به 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);
};
قطعه کد بالا زمان اجرا CopilotKit را در /api/copilotkit
نقطه پایانی API، به CopilotKit اجازه می دهد تا درخواست های کاربر را از طریق عامل مشترک هوش مصنوعی پردازش کند.
در نهایت، به روز رسانی کنید app/page.tsx
با بسته بندی کل برنامه با مولفه CopilotKit که زمینه کمکی را برای همه اجزای برنامه فراهم می کند.
"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 کل برنامه را می پیچد و دو پروپوزال را می پذیرد – runtimeUrl
و agent
. این runtimeUrl
مسیر API باطنی است که میزبان عامل هوش مصنوعی و agent
نام عاملی است که عمل را انجام می دهد.
پذیرش درخواست ها و پاسخ های جریانی به فرانت اند
برای فعال کردن CopilotKit برای دسترسی و پردازش ورودی های کاربر، آن را فراهم می کند useCoAgent
hook، که امکان دسترسی به وضعیت عامل را از هر نقطه در برنامه فراهم می کند.
به عنوان مثال، قطعه کد زیر نحوه استفاده از آن را نشان می دهد useCoAgent
قلاب این state
متغیر اجازه دسترسی به وضعیت فعلی عامل را می دهد، setState
برای تغییر وضعیت استفاده می شود و از run
تابع دستورات را با استفاده از عامل اجرا می کند. این start
و stop
توابع اجرای عامل را آغاز و متوقف می کنند.
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
};
در مرحله بعد، میتوانید نتایج جستجو را به صفحه پخش کنید ResultsView
با دسترسی به متغیر state در داخل useCoAgent
قلاب قطعه کد زیر را در قسمت کپی کنید 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
قلاب نتایج جستجو در قالب علامت گذاری برگردانده می شوند و به قسمت ارسال می شوند AnswerMarkdown
کامپوننت، که محتوا را در صفحه نمایش می دهد.
در نهایت قطعه کد زیر را در قسمت کپی کنید AnswerMarkdown
جزء با استفاده از کتابخانه React Markdown، محتوای علامت گذاری شده به صورت متن فرمت شده ارائه می شود.
import Markdown from "react-markdown";
export function AnswerMarkdown({ markdown }: { markdown: string }) {
return (
<div className='markdown-wrapper'>
<Markdown>{markdown}</Markdown>
</div>
);
}
تبریک می گویم! شما پروژه این آموزش را تکمیل کرده اید. همچنین می توانید فیلم ضبط شده را در اینجا مشاهده کنید:
ضبط کامل وبینار
پیچیدن آن
هوش LLM زمانی مؤثرتر است که در کنار هوش انسانی کار کند و CopilotKit CoAgents به شما امکان می دهد عوامل هوش مصنوعی، خلبانان و انواع مختلف دستیارها را در برنامه های نرم افزاری خود تنها در چند دقیقه ادغام کنید.
اگر نیاز به ساخت یک محصول هوش مصنوعی یا ادغام عوامل هوش مصنوعی در برنامه خود دارید، باید CopilotKit را در نظر بگیرید.
کد منبع این آموزش در GitHub موجود است:
https://github.com/CopilotKit/CopilotKit/tree/main/examples/coagents-ai-researcher
با تشکر از شما برای خواندن!