👀

爆速でAI Copliot機能付きアプリを作れるCopliotKitが面白い

2024/11/19に公開

この記事ではCopliotKitの公式チュートリアルを実施して、Next.jsでの導入から実際に動作させるところまでを実施します。

CopliotKitとは

自作のアプリケーションに、AI Coplit機能を組み込むことができるフレームワークです。
https://www.copilotkit.ai/
https://docs.copilotkit.ai/

公式サイトのKey featureを翻訳すると以下のとおりです。

  • In-App AI Chatbot: ヘッドレスUIを含む、簡単に組み込めるAIチャットボットコンポーネント。
  • Copilot Readable State: Copliotがアプリケーションの状態を読み取り、理解できる機能。
  • Copilot Actions: Copliotがアプリケーション内でアクションを実行可能。
  • Generative UI: Copliotのチャット欄内であらゆるコンポーネントをレンダリング。
  • Copilot Textarea: どのテキストエリアにも簡単に追加できる強力なAI自動補完機能。
  • AI Autosuggestions: 自動提案機能を備えたAIチャットインターフェース。
  • Copilot Tasks: アプリケーションの状態に基づいて、コパイロットが適切にアクションを実行可能。

今回はチャットボットに関係する機能を紹介します。

チャットボットからアプリケーションを操作できる

ただ単にAIチャットボットを組み込めるだけのフレームワークは数多くありますが、CopilotKitはその先を行っています。

useCopilotReadableによってStateを読み取ったり、useCopilotActionによって、チャットボットからアプリケーションに対して操作を実施したりできます。
(注:操作は事前定義しておく必要があります。)

以下の例では、残りのタスクの数の確認とタスクの削除を実施しました。

画像:公式チュートリアルのデモより

チュートリアル

以下の内容を日本語化&解説を入れながら実施していきます。

https://www.youtube.com/watch?v=n1lJyaLam7g
https://docs.copilotkit.ai/tutorials/ai-todo-app/overview

Step 1: レポジトリの取得

用意されているチュートリアル用のレポジトリをクローンして、まずは開発画面を立ち上げましょう。

git clone -b base-start-here https://github.com/CopilotKit/example-todos-app.git
cd example-todos-app
npm install
npm run dev

http://localhost:3000にアクセスします。

このような画面が立ち上がります。

Step 2: CopilotKitのセットアップ

まずCopilotKitのライブラリををインストールします。

npm install @copilotkit/react-core @copilotkit/react-ui

@copilotkit/react-core: CopilotKit provider とhookを含むコアライブラリ
@copilotkit/react-ui: sidebar, chat popup, textareaなどのCopilotKit UI components のUIライブラリ

エンドポイントの作成

app/api/copilotkit/route.tsにエンドポイントを作成します。

その際、リクエストとレスポンスの形を統一する際ためのLLM Adapterが用意されており、好きなLLMを利用できます。

  • OpenAI Adapter
  • OpenAI Assistant Adapter
  • LangChain Adapter
  • Groq Adapter
  • Google Generative AI Adapter
  • Anthropic Adapter

https://docs.copilotkit.ai/guides/bring-your-own-llm

OpenAIのAPIを使う場合(推奨)

これが一番簡単です。
当たり前ですがOpenAIに課金して事前にクレジットを入れておかないと動作しないためご注意ください。(一敗)

app/api/copilotkit/route.ts
import {
  CopilotRuntime,
  OpenAIAdapter,
  copilotRuntimeNextJSAppRouterEndpoint,
} from '@copilotkit/runtime';
import OpenAI from 'openai';
import { NextRequest } from 'next/server';
 
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const serviceAdapter = new OpenAIAdapter({ openai });
const runtime = new CopilotRuntime();
 
export const POST = async (req: NextRequest) => {
  const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
    runtime,
    serviceAdapter,
    endpoint: '/api/copilotkit',
  });
 
  return handleRequest(req);
};
LangChain経由でAzure OpenAI ServiceのAPIを使う場合(私の場合)

諸事情があり、ChatGPTのAPIキーはAzure OpenAI Serviceを使いたかったため、そのようにコードを作成します。

以下の例を参考に、動く状態まで持っていきました。

chainFnのところで型エラーを吐きますが動きます。
githubのissuesを見るに未解決のバグと思われます。
https://github.com/CopilotKit/CopilotKit/issues/534

また、公式の例のコードにはserviceAdapterのところにbindTools(tools)がありますが、これを含む場合上手く動作しなかったため、削除しました。この点はLangChainの理解を深めてから更新します。
https://docs.copilotkit.ai/reference/classes/llm-adapters/LangChainAdapter

app/api/copilotkit/route.ts
import {
  CopilotRuntime,
  LangChainAdapter,
  copilotRuntimeNextJSAppRouterEndpoint,
} from "@copilotkit/runtime";
import { AzureChatOpenAI } from "@langchain/openai";
import { NextRequest } from "next/server";

const runtime = new CopilotRuntime();

const model = new AzureChatOpenAI({
  openAIApiKey: process.env.OPENAI_API_KEY,
  openAIApiVersion: process.env.OPENAI_API_VERSION,
  openAIBasePath: process.env.OPENAI_BASE_PATH,
  deploymentName: process.env.DEPLOYMENT_NAME,
});

const serviceAdapter = new LangChainAdapter({
  chainFn: async ({ messages }) => {
    return model.stream(messages);
  },
});

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

  return handleRequest(req);
};

次にフロント側も書き換えます。
<CopilotKit runtimeUrl="/api/copilotkit">でapiの呼び出し先を設定します。<CopilotPopup />を使ってチャットボットを表示します。

app/page.tsx
"use client";
 
import { TasksList } from "@/components/TasksList";
import { TasksProvider } from "@/lib/hooks/use-tasks";
import { CopilotKit } from "@copilotkit/react-core";
import { CopilotPopup } from "@copilotkit/react-ui"; 
import "@copilotkit/react-ui/styles.css"; 
 
export default function Home() {
  return (
    <CopilotKit runtimeUrl="/api/copilotkit">
      <TasksProvider>
        <TasksList />
      </TasksProvider>
      <CopilotPopup />
    </CopilotKit>
  );
}

これでチャットボットをアプリケーションに埋め込むことができました。
しかしながらまだStateを読む機能を搭載していないため、ただのLLMになっています。

Step 3: Copilot Readable State

実際にアプリケーション内のStateを読み込めるようにしましょう。

lib/hooks/use-tasks.tsxを以下のように書き換えます。

lib/hooks/use-tasks.tsx
lib/hooks/use-tasks.tsx
import { createContext, useContext, useState, ReactNode } from "react";
import { defaultTasks } from "../default-tasks";
import { Task, TaskStatus } from "../tasks.types";

// 追加
import { useCopilotReadable } from "@copilotkit/react-core";

let nextId = defaultTasks.length + 1;

type TasksContextType = {
  tasks: Task[];
  addTask: (title: string) => void;
  setTaskStatus: (id: number, status: TaskStatus) => void;
  deleteTask: (id: number) => void;
};

// タスクの状態を管理するためのコンテキストを定義し、
// プロバイダーを通じて下位のコンポーネントで関数を利用できるようにする
const TasksContext = createContext<TasksContextType | undefined>(undefined);

export const TasksProvider = ({ children }: { children: ReactNode }) => {
  const [tasks, setTasks] = useState<Task[]>(defaultTasks);

  // 追加
  useCopilotReadable({
    description: "The state of the todo list",
    value: JSON.stringify(tasks),
  });

  const addTask = (title: string) => {
    setTasks([...tasks, { id: nextId++, title, status: TaskStatus.todo }]);
  };

  const setTaskStatus = (id: number, status: TaskStatus) => {
    setTasks(
      tasks.map((task) => (task.id === id ? { ...task, status } : task))
    );
  };

  const deleteTask = (id: number) => {
    setTasks(tasks.filter((task) => task.id !== id));
  };

  return (
    <TasksContext.Provider
      value={{ tasks, addTask, setTaskStatus, deleteTask }}>
      {children}
    </TasksContext.Provider>
  );
};

// useTasksフックは、TasksContextからタスクの状態を取得するためのカスタムフック
// const context = useContext(TasksContext);を定義するだけ
// 呼び出し側でエラーハンドリングを毎回書くのを防ぐために、事前にカスタムフックとしてまとめて書いておく
export const useTasks = () => {
  const context = useContext(TasksContext);
  if (context === undefined) {
    throw new Error("useTasks must be used within a TasksProvider");
  }
  return context;
};

今回追加した箇所はこちらです。
useCopilotReadable内でdescriptionとvalueを記載します。
descriptionはLLMがデータの内容を理解しやすくするための説明です。

lib/hooks/use-tasks.tsx
// ... the rest of the file
 
import { useCopilotReadable } from "@copilotkit/react-core"; //追加
 
export const TasksProvider = ({ children }: { children: ReactNode }) => {
  const [tasks, setTasks] = useState<Task[]>(defaultTasks);
 
  //追加
  useCopilotReadable({
    description: "The state of the todo list",
    value: JSON.stringify(tasks)
  });
 
  // ... the rest of the file
}

この時点で、残りのタスクについて尋ねると、値を読み取って正しく返答できるようになりました。

Step 4: Copilot Actions

次にチャットボットからアプリケーションの操作をできるようにしましょう。

lib/hooks/use-tasks.tsxを以下のように書き換えます。

lib/hooks/use-tasks.tsx
lib/hooks/use-tasks.tsx
import { createContext, useContext, useState, ReactNode } from "react";
import { defaultTasks } from "../default-tasks";
import { Task, TaskStatus } from "../tasks.types";

// 追加
import { useCopilotReadable, useCopilotAction } from "@copilotkit/react-core";

let nextId = defaultTasks.length + 1;

type TasksContextType = {
  tasks: Task[];
  addTask: (title: string) => void;
  setTaskStatus: (id: number, status: TaskStatus) => void;
  deleteTask: (id: number) => void;
};

// タスクの状態を管理するためのコンテキストを定義し、
// プロバイダーを通じて下位のコンポーネントで関数を利用できるようにする
const TasksContext = createContext<TasksContextType | undefined>(undefined);

export const TasksProvider = ({ children }: { children: ReactNode }) => {
  const [tasks, setTasks] = useState<Task[]>(defaultTasks);

  // 追加
  useCopilotReadable({
    description: "The state of the todo list",
    value: JSON.stringify(tasks),
  });

  // 追加
  useCopilotAction({
    name: "addTask",
    description: "Adds a task to the todo list",
    parameters: [
      {
        name: "title",
        type: "string",
        description: "The title of the task",
        required: true,
      },
    ],
    handler: ({ title }) => {
      addTask(title);
      console.log("Created");
    },
  });

  // 追加
  useCopilotAction({
    name: "deleteTask",
    description: "Deletes a task from the todo list",
    parameters: [
      {
        name: "id",
        type: "number",
        description: "The id of the task",
        required: true,
      },
    ],
    handler: ({ id }) => {
      deleteTask(id);
    },
  });

  // 追加
  useCopilotAction({
    name: "setTaskStatus",
    description: "Sets the status of a task",
    parameters: [
      {
        name: "id",
        type: "number",
        description: "The id of the task",
        required: true,
      },
      {
        name: "status",
        type: "string",
        description: "The status of the task",
        enum: Object.values(TaskStatus),
        required: true,
      },
    ],
    handler: ({ id, status }) => {
      setTaskStatus(id, status);
    },
  });

  const addTask = (title: string) => {
    setTasks([...tasks, { id: nextId++, title, status: TaskStatus.todo }]);
  };

  const setTaskStatus = (id: number, status: TaskStatus) => {
    setTasks(
      tasks.map((task) => (task.id === id ? { ...task, status } : task))
    );
  };

  const deleteTask = (id: number) => {
    setTasks(tasks.filter((task) => task.id !== id));
    console.log("削除が呼び出されました");
  };

  return (
    <TasksContext.Provider
      value={{ tasks, addTask, setTaskStatus, deleteTask }}>
      {children}
    </TasksContext.Provider>
  );
};

// useTasksフックは、TasksContextからタスクの状態を取得するためのカスタムフック
// const context = useContext(TasksContext);を定義するだけ
// 呼び出し側でエラーハンドリングを毎回書くのを防ぐために、事前にカスタムフックとしてまとめて書いておく
export const useTasks = () => {
  const context = useContext(TasksContext);
  if (context === undefined) {
    throw new Error("useTasks must be used within a TasksProvider");
  }
  return context;
};

それぞれの関数に対してuseCopilotActionを定義します。
引数として渡したいパラメータを定義した後、handlerの中で関数に渡します。

  useCopilotAction({
    name: "addTask",
    description: "Adds a task to the todo list",
    parameters: [
      {
        name: "title",
        type: "string",
        description: "The title of the task",
        required: true,
      },
    ],
    handler: ({ title }) => {
      addTask(title);
    },
  });
 
  useCopilotAction({
    name: "deleteTask",
    description: "Deletes a task from the todo list",
    parameters: [
      {
        name: "id",
        type: "number",
        description: "The id of the task",
        required: true,
      },
    ],
    handler: ({ id }) => {
      deleteTask(id);
    },
  });
 
  useCopilotAction({
    name: "setTaskStatus",
    description: "Sets the status of a task",
    parameters: [
      {
        name: "id",
        type: "number",
        description: "The id of the task",
        required: true,
      },
      {
        name: "status",
        type: "string",
        description: "The status of the task",
        enum: Object.values(TaskStatus),
        required: true,
      },
    ],
    handler: ({ id, status }) => {
      setTaskStatus(id, status);
    },
  });
 

例としてタスクの削除をしてみました。
必要なパラメータが抜けている場合は、再度質問をしてくれます。

最後に

アプリケーションとLLMとの深い連携を実現できそうでワクワクするツールでした。
私はまだ確認できていませんが、LangGraphとの連携ができるなど、まだまだ探求しがいがありそうです。
https://docs.copilotkit.ai/coagents

また、執筆時点でAzureでCopilot Actionsが動かせなかった点についても、解決でき次第追記します。

参考

https://blog.generative-agents.co.jp/entry/2024/11/12/162420

Discussion