Azure Functions と Next.js でAssistants API のFile Search機能を実装してみた

## はじめに
Azure Functions と Next.js でAssistants API のFile Search機能を実装してみました。

## 開発環境
- **OS**: Windows 11
- **言語**: TypeScript
- **フレームワーク**: 
  - バックエンド: Azure Functions
  - フロントエンド: Next.js
- **ライブラリ**: 
  - バックエンド: openai: ^4.52.0

## 実装
### 1. Azure Functions プロジェクトの作成

まず、新規で「assistants-api-file-search」というフォルダを作成し、Azure Functions のプロジェクトを作成します。（TypeScript、Model V4、HTTP Trigger、関数名はsearchFiles）


次に、openaiライブラリをインストールします。

```bash
npm i openai
```
　　
### 2. Azure Functions の設定

`local.settings.json` ファイルを以下のように設定します。

```json
{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "",
    "FUNCTIONS_WORKER_RUNTIME": "node",
    "AzureWebJobsFeatureFlags": "EnableWorkerIndexing",
    "AZURE_OPENAI_ENDPOINT": "your Azure OpenAI endpoint",
    "AZURE_OPENAI_API_KEY": "your Azure OpenAI key"
  },
  "Host": {
    "CORS": "http://localhost:3000",
    "CORSCredentials": true
  }
}
```
　　
### 3. Azure Functions の実装
`src/functions/searchFiles.ts` ファイルを以下のように実装します。

```typescript
import { app, HttpRequest, HttpResponseInit, InvocationContext } from "@azure/functions";
const { AzureOpenAI, toFile } = require('openai');

const endpoint = process.env.AZURE_OPENAI_ENDPOINT;
const azureApiKey = process.env.AZURE_OPENAI_API_KEY;

const client = new AzureOpenAI({ endpoint, apiKey: azureApiKey, apiVersion: '2024-05-01-preview' });

export async function searchFiles(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> {
    const requestData = await request.json();
    const message = (requestData as { message?: string }).message;
    const file = (requestData as { file?: { name: string; content: string } }).file;
    const assistantId = (requestData as { assistantId?: string })?.assistantId || (await createAssistant()).id;
    const threadId = (requestData as { threadId?: string })?.threadId || (await createThread()).id;

    console.log("assistantId", assistantId);
    console.log("threadId", threadId);

    if (file && file.name && file.content) {
        const fileContent = Buffer.from(file.content, "base64");
        const upload_file = await client.files.create({
            file: await toFile(fileContent, file.name),
            purpose: "assistants",
        });
        await addMessage(threadId, message, upload_file.id);
    } else {
        await addMessage(threadId, message, null);
    }
    
    const run = await runThread(assistantId, threadId);
    return {
        status: 200,
        headers: { "Content-Type": "text/event-stream" },
        body: assistant_streamer(run)
    };
};

const createAssistant = async () => {
    const assistant = await client.beta.assistants.create({
        name: "File Search Assistant",
        instructions: "You are a helpful assistant.",
        model: "gpt-4o",
        tools: [{ type: "file_search" }],
    });
    return assistant;
};

const createThread = async () => {
    return await client.beta.threads.create();
};

const addMessage = async (threadId: string, message: string, fileId: string | null) => {
    const messagePayload = {
        role: "user",
        content: message,
        ...(fileId ? {
            attachments: [
                {
                    file_id: fileId,
                    tools: [{ type: "file_search" }]
                }
            ]
        } : {})
    };

    const response = await client.beta.threads.messages.create(
        threadId,
        messagePayload
    );

    return response;
};

const assistant_streamer = (stream) => {
    return {
        [Symbol.asyncIterator]: async function* () {
            for await (const event of stream) {
                if (event.event === 'thread.message.delta') {
                    const chunk = event.data.delta.content?.[0];
                    if (chunk && 'text' in chunk && chunk.text.value) {
                        yield chunk.text.value;
                    }
                }
            }
        }
    };
};

const runThread = async (assistantId: string, threadId: string) => {
    return client.beta.threads.runs.stream(threadId, {
        assistant_id: assistantId
    });
};

app.http('searchFiles', {
    methods: ['POST'],
    authLevel: 'function',
    handler: searchFiles
});
```
　　
### 4. Next.js プロジェクトの作成
新しいディレクトリで Next.js プロジェクトを作成します。

```bash
npx create-next-app@latest azure-function-integration
cd azure-function-integration
```

Next.js プロジェクトの作成時に表示される設定オプションは以下のようになります：

![Next.js プロジェクト作成時の設定オプション](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124507/12577bc4-8596-85b8-6857-977cec9607fc.png)
　　
### 5. Next.js フロントエンドの実装

`app/page.tsx` ファイルを以下のように実装します。

```typescript
"use client";
import React, { useState } from 'react';

const SearchFiles: React.FC = () => {
  const [message, setMessage] = useState<string>('');
  const [file, setFile] = useState<File | null>(null);
  const [response, setResponse] = useState<string>('');
  const [isLoading, setIsLoading] = useState<boolean>(false);

  const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    if (e.target.files && e.target.files.length > 0) {
      setFile(e.target.files[0]);
    }
  };

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();

    if (!message || !file) {
      alert('Please provide a message and a file.');
      return;
    }

    setIsLoading(true);
    setResponse('');

    const fileContent = await file.arrayBuffer();
    const base64FileContent = Buffer.from(fileContent).toString('base64');

    const requestData = {
      message,
      file: {
        name: file.name,
        content: base64FileContent,
      },
      assistantId: 'your assistantId',
      threadId: '',
    };

    try {
      const azureFunctionUrl = 'http://localhost:7071/api/searchFiles';
      const res = await fetch(azureFunctionUrl, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(requestData),
      });

      if (res.ok) {
        const reader = res.body?.getReader();
        if (reader) {
          const decoder = new TextDecoder();
          let responseData = '';

          while (true) {
            const { value, done } = await reader.read();
            if (done) break;

            const chunk = decoder.decode(value);
            responseData += chunk;

            setResponse((prevResponse) => prevResponse + chunk);
          }
        }
      } else {
        console.error('Error fetching data:', res.status);
      }
    } catch (err) {
      console.error('Error fetching data:', err);
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <div className="flex flex-col items-center justify-center min-h-screen bg-gray-100">
      <div className="max-w-md w-full bg-white rounded-lg shadow-lg p-6">
        <h1 className="text-2xl font-bold mb-4">ファイル検索</h1>
        <form onSubmit={handleSubmit}>
          <div className="mb-4">
            <input
              type="text"
              placeholder="メッセージを入力してください"
              value={message}
              onChange={(e) => setMessage(e.target.value)}
              className="border border-gray-300 rounded-lg py-2 px-4 w-full"
            />
          </div>
          <div className="mb-4">
            <input
              type="file"
              onChange={handleFileChange}
              className="border border-gray-300 rounded-lg py-2 px-4 w-full"
            />
          </div>
          <button
            type="submit"
            className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded mb-4"
            disabled={isLoading}
          >
            {isLoading ? 'Loading...' : 'リクエストを送信'}
          </button>
        </form>
        <pre className="whitespace-pre-wrap">{response}</pre>
      </div>
    </div>
  );
};

export default SearchFiles;
```
　　
### 6. アプリケーションの実行

Azure Functions と Next.js アプリケーションを別々のターミナルで実行します。

```bash
# Azure Functions
func start

# Next.js
npm run dev
```

実行後、ブラウザで http://localhost:3000 にアクセスし、ファイルとメッセージを送信すると以下のような画面が表示されます。

![ファイル検索アプリケーションのインターフェース](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3124507/a4e47941-3cd8-d5b8-ab50-10c005f51fbf.png)
　　
上記の画面で使用しているsample.txtは以下になります。
```sample.txt
The Evolution of Technology and Its Impact

The evolution of technology over the past few decades has been nothing short of revolutionary. From the advent of the internet to the proliferation of smartphones, the rapid advancement of technology has profoundly transformed various aspects of human life, reshaping how we communicate, work, and entertain ourselves.

One of the most significant technological breakthroughs was the creation of the internet. Initially developed for military purposes and academic research, the internet has grown into a global network connecting billions of people. It has democratized access to information, enabling individuals to learn, share, and collaborate in ways that were previously unimaginable. The rise of social media platforms has further amplified this connectivity, allowing people to stay in touch with friends and family across the globe, fostering new relationships, and even sparking social and political movements.

The workplace has also undergone a dramatic transformation due to technological advancements. Automation and artificial intelligence (AI) are changing the nature of work, leading to increased efficiency and productivity. Routine and repetitive tasks are increasingly being handled by machines, freeing up human workers to focus on more complex and creative endeavors. However, this shift also brings challenges, including the displacement of certain job roles and the need for workers to continuously adapt to new technologies. Lifelong learning and upskilling have become essential in this rapidly changing landscape.

In addition to its impact on communication and work, technology has revolutionized entertainment. The emergence of streaming services like Netflix and Spotify has changed how we consume media, offering on-demand access to vast libraries of content. Virtual and augmented reality technologies are creating immersive experiences that were once the stuff of science fiction. Video games have evolved into sophisticated forms of storytelling and social interaction, attracting a diverse audience and becoming a major cultural force.

Moreover, the integration of technology into everyday life has led to the development of smart homes and the Internet of Things (IoT). From smart thermostats that learn your schedule to voice-activated assistants that manage your daily tasks, these innovations are designed to enhance convenience and improve quality of life. However, they also raise concerns about privacy and security, as more personal data is collected and stored by connected devices.

Healthcare is another domain where technology has had a profound impact. Advances in medical technology, such as telemedicine, wearable health monitors, and precision medicine, are improving patient outcomes and making healthcare more accessible. Remote consultations and monitoring allow for better management of chronic conditions, while personalized treatments based on genetic information hold the promise of more effective therapies.

Despite the many benefits, the rapid pace of technological change also presents significant challenges. Ethical considerations surrounding AI, data privacy issues, and the digital divide are pressing concerns that need to be addressed. Ensuring that technology serves the greater good requires thoughtful regulation and a commitment to inclusivity and ethical standards.

In conclusion, the evolution of technology has brought about remarkable changes that have enhanced many aspects of our lives. As we continue to innovate and integrate new technologies, it is crucial to navigate the accompanying challenges with foresight and responsibility. By doing so, we can harness the power of technology to create a better, more connected, and equitable world for all.
```
　　
以上です！
最後まで読んでいただき、ありがとうございました！

## 参考資料
https://platform.openai.com/docs/assistants/tools/file-search/quickstart