LlamaIndex.tsをVercelで使う

## はじめに

LlamaIndexはLLMとデータを扱うのに便利なオープンソースのデータフレームワークで、pythonで書かれています。これをJavaScript/TypeScript環境で使えるようにしたのがLlamaIndex.tsです。Node.jsやDeno、Vercel Edge Runtimeなど複数のJavaScriptランタイムで動作します​。サーバサイドでの利用に焦点を当てており、ブラウザからの直接利用は一部APIの不足により制限があります。

Typescript版はPython版の完全な移植というわけではなく、一部使えない機能があります。また、Vercelにデプロイする場合にはサーバーレス環境特有の課題に注意する必要があるなど、導入に難しいところもありますが、レイテンシが低く軽量に動作するのは大きな利点です。また、VercelはAI SDKを提供しており、LlamaIndexといっしょに使うことが出来ます。

この記事では、Vercelで使う上での基本的な手順と注意点のメモをご紹介します。私自身初心者なので、色々間違いがあるかもしれません。何かありましたらコメントいただければ幸いです。

## 基礎資料

LlamaIndex.tsを使うなら以下の資料は押さえておきたいです。

- [LlamaIndex.ts 公式 Github](https://github.com/run-llama/LlamaIndexTS)
    - 特に[サンプル](https://github.com/run-llama/LlamaIndexTS/tree/main/examples)は見ておきたいです。
- [LlamaIndex.ts 公式ドキュメント](https://ts.llamaindex.ai/docs/llamaindex)
    - [Next.js向けにセットアップする際の注意](https://ts.llamaindex.ai/docs/llamaindex/getting_started/setup/next)
    - [Vercelとの統合](https://ts.llamaindex.ai/docs/llamaindex/integration/vercel)
- [Vercel AI SDK/Adapter/LlamaIndex](https://sdk.vercel.ai/providers/adapters/llamaindex)

## 基本的なセットアップ
まずはnext.js, Vercelなどを準備します。ルートになるディレクトリにて以下のコマンドを入力します

```
#Next.jsの準備
npx create-next-app@latest .

#Vercelプロジェクトの準備
vercel

# Node.js 22 のインストールと使用
nvm install 22
nvm use 22

# 必要なパッケージのインストール
npm install llamaindex
```

上の手順のあとなら既に設定済みと思いますが、tsconfig.jsonの以下のパラメーターを確認して下さい。
```tsconfig.json
{
  compilerOptions: {
    ...
    moduleResolution: "bundler"
    ...
  },
}
```
これは TypeScript が 最新のパッケージ配布方法（ESM + CJS + サーバーレス環境向けの conditional exports）に対応するため に必要な設定とのことです([参照](https://www.npmjs.com/package/llamaindex/v/0.6.22))。

次に、[公式の指示に従い](https://ts.llamaindex.ai/docs/llamaindex/getting_started/setup/next)、next.config.tsは削除して、代わりにnext.config.mjsを作ります。
```next.config.mjs
import withLlamaIndex from "llamaindex/next";
 
/** @type {import('next').NextConfig} */
const nextConfig = {};
 
export default withLlamaIndex(nextConfig);
```
Next.js の ESM (ECMAScript Modules) 化に適応し、最新の依存関係の問題を避けるために有用とのことです([資料](https://ts.llamaindex.ai/docs/llamaindex/getting_started/setup/next))。


## OpenAIのChatモデルを使ってみる

[公式サンプル](https://github.com/run-llama/LlamaIndexTS/blob/main/examples/openai.ts)を更に簡略した例を使って、OpenAIのgpt-4o-miniと質疑応答します。

まず、OpenAI API呼び出しの抽象化と設定の管理を行うヘルパーモジュールと作ります。これは、本質的には公式サンプルと同じです。

```lib/openai.ts
import { OpenAI } from "@llamaindex/openai";

export async function getChatResponse(message: string) {
  const llm = new OpenAI({ model: "gpt-4o-mini", temperature: 0.1 });
  const response = await llm.chat({
    messages: [{ content: message, role: "user" }],
  });
  return response.message.content;
}
```
次に、バックエンドAPIエンドポイントを作ります。これがクライアントからのリクエスト処理、エラーハンドリング、openai.tsでのOpenAI関連の処理の橋渡しをします

```app/api/openai/route.ts
import { NextResponse } from 'next/server';
import { getChatResponse } from '@/lib/openai';

export async function POST(request: Request) {
  try {
    const { content } = await request.json();
    const response = await getChatResponse(content);
    return NextResponse.json({ response });
  } catch (error) {
    console.error('API Error:', error);
    return NextResponse.json({ error: 'Error occurred' }, { status: 500 });
  }
} 
```
最後に、フロントエンドのUIコンポーネントを作ります。
```app/page.tsx
'use client';

import { useState } from 'react';

export default function Home() {
  const [input, setInput] = useState('');
  const [response, setResponse] = useState('');

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    try {
      const res = await fetch('/api/openai', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ type: 'chat', content: input }),
      });
      const data = await res.json();
      setResponse(data.response);
    } catch (error) {
      console.error('Client Error:', error);
      setResponse('エラーが発生しました。');
    }
  };

  return (
    <div className="p-8">
      <form onSubmit={handleSubmit}>
        <textarea
          value={input}
          onChange={(e) => setInput(e.target.value)}
          className="w-full p-2 border rounded"
        />
        <button type="submit" className="mt-2 px-4 py-2 bg-blue-500 text-white rounded">
          送信
        </button>
      </form>

      {response && (
        <div className="mt-4 p-4 bg-gray-100 rounded">
          {response}
        </div>
      )}
    </div>
  );
}
```
ローカルのテスト用に、.env.localにAPIキーを登録してください。

```.env.local
OPENAI_API_KEY=sk-xQoeXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXQY0u
```

ついでに、vercelのサイトのsettingsのenvironment variableのところでも同じように設定しておいてください。こちらはデプロイしたときに使います。


準備ができたら
```
npm run dev
```
を実行し、http://localhost:3000 で動作確認してください。


上手く行ったら、Vercelプロジェクトにデプロイしてみましょう。
```
vercel deploy
```

## Vercel AI SDKを使ってみる。

次は、Vercelが提供している[AI SDK](https://sdk.vercel.ai/)を使う例を紹介します。最初に、パッケージをインストールします。

```
npm install ai
```

次に、[公式サンプルのモジュール](https://sdk.vercel.ai/providers/adapters/llamaindex)を、ほぼコピーしてきます(console.logは区別のために追加)。

```app/api/aisdk/route.ts
import { OpenAI, SimpleChatEngine } from 'llamaindex';
import { LlamaIndexAdapter } from 'ai';

export const maxDuration = 60;

export async function POST(req: Request) {

  console.log('ai sdk route');
    
  const { prompt } = await req.json();

  const llm = new OpenAI({ model: 'gpt-4o-mini' });
  const chatEngine = new SimpleChatEngine({ llm });

  const stream = await chatEngine.chat({
    message: prompt,
    stream: true,
  });

  return LlamaIndexAdapter.toDataStreamResponse(stream);
}
```

page.tsxも、公式に従って修正します。useCompletionを使うことで、大幅に簡略になることがわかります。また、テキストはストリームされます。

```app/page.tsx
'use client';

import { useCompletion } from 'ai/react';

export default function Home() {
  const { completion, input, handleInputChange, handleSubmit } = useCompletion({
    api: '/api/aisdk',
  });

  return (
    <div className="p-8">
      <form onSubmit={handleSubmit}>
        <textarea
          value={input}
          onChange={handleInputChange}
          className="w-full p-2 border rounded"
        />
        <button type="submit" className="mt-2 px-4 py-2 bg-blue-500 text-white rounded">
          送信
        </button>
      </form>

      {completion && (
        <div className="mt-4 p-4 bg-gray-100 rounded">
          {completion}
        </div>
      )}
    </div>
  );
}
```

## 最後に
LlamaIndex.tsは、Python版に比べると情報が少なくて勉強しづらいですが、ビルド時間は短く動作もサクサクで快適なので、積極的に使っていきたいですね。