Azure Static Web Apps(Node.js) で、ストリームなChatGPTを構築する

# はじめに
ChatGPTなシステムを作るときに、Azure Static Web Appsは最善の方法の１つですが、これまではストリームができないという課題がありました。(.NETならできるんですが）

と、つい最近以下のアナウンスが出ました。
[Azure Functions: Support for HTTP Streams in Node.js is now in Preview](https://techcommunity.microsoft.com/t5/apps-on-azure-blog/azure-functions-support-for-http-streams-in-node-js-is-now-in/ba-p/4066575)
これで、Node.JSのFunctionでStreamが扱えるようになったので、その動作をざっくり検証してみます。（Pythonも早くできるようになるといいですよねぇ)

上記ブログにもありますが、以下の前提条件が必要です。古い場合は適宜更新してください。
> - Version 4 of the Node.js programming model. Learn more about the differences between v3 and v4 in the migration guide.
> - Version 4.3.0 or higher of the @azure/functions npm package.
> - If running in Azure, version 4.28 of the Azure Functions runtime.
> - If running locally, version 4.0.5530 of Azure Functions Core Tools.

# サンプルアプリ

Azure Static Web Appsのプロジェクト構成を準備していきます。初期構成の準備は「[SvelteKit + Azure StaticWebApps で SSGなサイトを構築する](https://qiita.com/shyamagu/items/b41eeeca5ba65559b017)」の手順通りにやっていきます。なので、Svelte + Node.JS構成になります。

その上で、以下のようにストリームを返すFunctions(Node.js)を準備します。func newで作成した関数に対して、まずは今回の目玉、ストリームをONにします。
```javascript
app.setup({ enableHttpStream: true });
```
基本はこれだけです。

次に、ChatGPTからのレスポンスをストリームで返す用意をします。必要に応じてもう少し上段から返すなどは変えてください。（とりあえず回答文字だけストリームします。）
```javascript
const chatgpt_streamer = (response) => {
    return {
        [Symbol.asyncIterator]: async function* () {
            for await (const chunk of response) {
                const content = chunk?.choices[0]?.delta?.content;
                if (content) yield content;
            }
        }
    };
};
```
例外処理などは省いて、全体像は以下のようなコードになります。このコードでは、keyやシステムプロンプトなどは全部画面から入力されている前提です。

```javascript:yourfunc.js
const { app } = require('@azure/functions');
app.setup({ enableHttpStream: true });

const { OpenAIClient, AzureKeyCredential } = require("@azure/openai");

const chatgpt_streamer = (response) => {
    return {
        [Symbol.asyncIterator]: async function* () {
            for await (const chunk of response) {
                const content = chunk?.choices[0]?.delta?.content;
                if (content) yield content;
            }
        }
    };
};


app.http('yourfunc', {
    methods: ['POST'],
    authLevel: 'anonymous',
    handler: async (request, context) => {

        data = await request.json()

        const client = new OpenAIClient(data.endpoint, new AzureKeyCredential(data.api_key));

        const messages = [
            {role: "system", content: data.system_prompt},
            {role: "user", content: data.user_prompt}
        ]

        const params = {
            temperature:0,
        }

        const event = await client.streamChatCompletions(data.model_name, messages, params);

        return { 
            body: chatgpt_streamer(event),
            headers: {
                "Content-Type": "text/event-stream"
            }
        };
    }
});

```

あとは受け取り側です。今回フロントはSvelteなので、以下のように受け取ります。変数`message`にストリーム回答が入ってきますので、お好きに表示すればOkです。(`<div>{message}</div>`でもなんでも)

```javascript:+page.svelte

    let message =""
    
    const response = await fetch("/api/yourfunc", {
    method: "post",
    headers: {
        "Content-Type": "application/json"
    },
    body: JSON.stringify(sendMessage)
    });

    message = "";

    if(response.body){

        const decoder = new TextDecoder();
        const reader = response.body.getReader();

        while(true){
        const {done, value} = await reader.read();
        if(done){
            break;
        }
        const text = decoder.decode(value);
        message += text;
        }

    }
```

# おわりに
Azure Function(Node.js) でストリームが使えるようになったので、それを使ったChatGPTなアプリケーションについて検証してみました。
特につまるところもなく無事に動きましたので、あとはGAを待つのみですね。それとPythonも早く…