見出し画像

Workers AI+Cursorを使ってAIチャットを構築


はじめに

8歳の女の子がCloudflareのWorkers AI+Cursorを使って45分でAIチャットを作って色々カスタマイズしていたポストが騒がれていました

その再現をしてみたいと思ったのでやってみました
ですが、私のアプローチは8歳の子と全く同じ手順の再現ではなく手順が微妙に違うと思っていますが、やりたいこと(Workers AI+Cursorでチャット画面をカスタマイズ)は同じで達成もできます

※手順は確立したのでここでは15分を目指してやっていきたいと思います


事前に準備するもの

※Cloudflareでの課金は発生しません
※Cursorですが私はProプランで契約していますが、フリープランでも実施できるはずです

大まかな手順

  1. Workers AIでデプロイ

  2. ローカルに開発環境を準備

  3. Cursorでコーディング(画面をカスタマイズ)

  4. 後片付け(削除)

1.Workers AIでデプロイ


1-1) 
Cloudflareのコンソールを開きWorkers AIからWorkersテンプレートから作成する を選択

1-2

1-2) LLM Appを選択

画面が遷移されます
せっかくなのでコードも見ていきます


index.js

export default {
  async fetch(request, env) {
    const tasks = [];


    // prompt - simple completion style input
    let simple = {
      prompt: 'Tell me a joke about Cloudflare'
    };
    let response = await env.AI.run('@cf/meta/llama-3-8b-instruct', simple);
    tasks.push({ inputs: simple, response });


    // messages - chat style input
    let chat = {
      messages: [
        { role: 'system', content: 'You are a helpful assistant.' },
        { role: 'user', content: 'Who won the world series in 2020?' }
      ]
    };
    response = await env.AI.run('@cf/meta/llama-3-8b-instruct', chat);
    tasks.push({ inputs: chat, response });


    return Response.json(tasks);
  }
};
  • デフォルトでは「llama-3-8b-instruct」のLLMが指定されています

  • ユーザープロンプトで「Who won the world series in 2020」と質問を投げています
    →正しい返答なら「LA ドジャーズ」といった旨の回答が返るはずです


wrangler.toml

name = "WORKER-NAME"
main = "index.js"
compatibility_date = "2023-08-23"

[ai]
binding = "AI"

[ai]
binding = "AI"

上記の記述がAIの機能を有効化(バインディング)するようです
詳しくは以下の公式ドキュメントをご覧ください



1-3) それではデプロイしていくので展開を押します

1-4) これでグローバルにデプロイされたので表示されているURLを開きます

1-5) URLを開くと実行結果が返ってきます
ハイライトした部分が回答ですが、きちんとLAドジャースの回答が返ってきています


回答:
The Los Angeles Dodgers won the World Series in 2020, defeating the Tampa Bay Rays in the Fall Classic, 4 games to 2! It was their first World Series title since 1988

和訳:
ロサンゼルス・ドジャースは2020年、フォールクラシックでタンパベイ・レイズを4勝2敗で破り、ワールドシリーズを制覇した!1988年以来のワールドシリーズ制覇である。


2.ローカルに開発環境を準備

2-1) ローカル環境で開発をするために準備をしていきます
コードの編集を押します

2-2) CLIのアイコンをクリックし「npx」のコマンドをコピーします
(コード編集画面でもコードの編集は可能ですが、ローカルでコードの編集をやっていきます)

2-3) 任意の作業フォルダを作成し、作成したフォルダでCursorを開きます
※venvで仮想環境を作成したい人は以下のコマンドを

python3 -m venv name #"name"は任意の名前を指定
source name/bin/activate #MacやLinuxの場合
name\Scripts\activate #Windows OSの場合

2-4) venvで仮想環境モード(ここではwork6)になったことを確認

2-5) npxを実行します

npx create-cloudflare@latest XXXXX--existing-script XXXXX

2-6) Noを選択
※今回は特にGitは使わない

╰ Do you want to use git for version control?
  Yes / No

2-7) Workers AIのPJフォルダに移動します

cd XXXXX

2-8)ローカルで実行します

npx wrangler dev

2-9) ローカルでブラウザを開きます

2-10) 回答が返ってきていますが、質問を変えて再度実行してみます

2-11) 以下のような感じでCursorのチャットに指示します
※(チャット画面はCTRL+ALT(cmd)+B)で開く

「index.js の中のユーザープロンプトだけを「日本の首都は?日本語で答えて」に変えて」

右側からチャット入力→Apply→Ctrl+shift+Yで反映できます
(上記だけではなくCursorからコード修正を反映する手順は複数あります)

2-12) ブラウザを再読込すると「日本の首都は東京です」と正しい答えが返ってきたので正常に動いていることが確認できました

2-13) それでは、グローバルにデプロイをします

npx wrangler deploy

2-14) デプロイされたらURLを開きます

2-15) 正常に実行されていますね


3.Cursorでコーディング(画面をカスタマイズ)

ではいよいよ本題となる画面のカスタマイズを実施していきます
せっかくなのでCloudflareのWebフレームワーク「Hono」を使っていきます
※HonoはCloudflareの中の人のゆうすけさんが開発しているFWです
同じ日本人として誇らしいですね

3-1) Cursorのチャットに以下のような指示をしてCursorの提案に従ってindex.jsを修正し保存してください(保存はCtrl+S)

Cloudflareの Hono を使いたいです。index.jsをHonoを使えるように修正してください

3-2) さらに以下の提案が出てくるはずなのでHonoをインストールします

npm install hono@latest

3-3) 再びデプロイして正常に動くか確認してください
(エラーが出たらCursorでデバッグをしていく)

npx wrangler deploy

画面のカスタマイズ


3-4) それでは画面のカスタマイズです
今は味気ない黒い画面なので一般的なチャット画面に変えていきます

Cursorのチャットに以下の文章で指示をしCursorの提案通りApplyしていってください

トップページを 一般的な AIチャット画面に変更してください

3-5) その後にCtrl+sで保存デプロイ(npx wrangler deployをしてください


修正された画面です
英語ですが「東京に区はいくつある?」を「23区」と答えてくれています

日本語で答えるように変更

3-6) 英語ではなく日本語で必ず答えるように修正します
以下のようにCursorのチャットで指示をするかもしくは index.js を直接修正します

チャットで英語での回答になっているので、回答を必ず日本語だけで答えるようにしてください

index.js

const chat = {
messages: [
{ role: 'system', content: 'あなたは親切なアシスタントです。日本語で必ず回答してください。' },
{ role: 'user', content: message }
]
}

一部ローマ字ですが、日本語表記もしてくれているので良しとしましょう

画面をレトロに変更


3-7) では、次はUIの画面をガラっと見た目を変えたいと思います
Cursorのチャットに以下の指示

トップページの @index.js をノスタルジックな雰囲気のUIに変えてください

ガラッっと変わりましたね
こんな感じでCursorのチャットに指示してCursorの提案通りApplyをしていきましょう

3-8) あとは好きなように色々とカスタマイズしてみてください

LINE風の画面に変更


言語モデルを変えてみる

3-9) 「llama-3-8b-instruct」から「gemma-7b-it」に変えてみます

index.js

変更前:
const response = await c.env.AI.run('@cf/meta/llama-3-8b-instruct', chat)

変更後:
const response = await c.env.AI.run('@hf/google/gemma-7b-it', chat)

後片付け(削除)

4-1) Workersの画面から以下の通り操作し削除を行います
リソースを残していても課金は発生しないですが、使わないリソースが残っているのが嫌な場合は削除を行ってください


お疲れ様でした
以上となります

それでは、よいCloudflareとCursorライフを!

補足

最近はv0、Bolt、Replitなどで自然言語でのWebシステム開発を行えるようになってきました
AIチャットボットも同様にこれらを使えば短時間で構築できますが、Workers AIとの差は信頼の高いプラットフォーム上にデプロイでき、さらにCDN、キャッシュ、セキュリティ、無料枠、日本国内にデプロイできるという点です

最終的なコード


最終的なコードです
ストリーミングの実装もしています

src/index.js

import { Hono } from 'hono'
import { html } from 'hono/html'
import { streamSSE } from 'hono/streaming'

const app = new Hono()

app.get('/', (c) => {
  return c.html(
    html`
      <!DOCTYPE html>
      <html lang="ja">
        <head>
          <meta charset="UTF-8">
          <meta name="viewport" content="width=device-width, initial-scale=1.0">
          <title>AIチャット</title>
          <script src="https://unpkg.com/htmx.org@1.9.10"></script>
          <style>
            body {
              font-family: 'Helvetica Neue', Arial, sans-serif;
              background-color: #f0f0f0;
              margin: 0;
              padding: 0;
            }
            .chat-container {
              max-width: 600px;
              margin: 20px auto;
              background-color: #fff;
              border-radius: 10px;
              box-shadow: 0 2px 10px rgba(0,0,0,0.1);
              overflow: hidden;
            }
            .chat-header {
              background-color: #4CAF50;
              color: white;
              padding: 15px;
              text-align: center;
              font-size: 18px;
              font-weight: bold;
            }
            #chat-messages {
              height: 400px;
              overflow-y: auto;
              padding: 20px;
            }
            .message {
              margin-bottom: 15px;
              clear: both;
            }
            .message-content {
              padding: 10px 15px;
              border-radius: 20px;
              max-width: 70%;
              display: inline-block;
            }
            .ai-message .message-content {
              background-color: #e6e6e6;
              float: left;
            }
            .user-message .message-content {
              background-color: #4CAF50;
              color: white;
              float: right;
            }
            .chat-input {
              display: flex;
              padding: 10px;
              background-color: #f9f9f9;
              border-top: 1px solid #eee;
            }
            #user-input {
              flex-grow: 1;
              padding: 10px;
              border: 1px solid #ddd;
              border-radius: 20px;
              margin-right: 10px;
            }
            #send-button {
              background-color: #4CAF50;
              color: white;
              border: none;
              padding: 10px 20px;
              border-radius: 20px;
              cursor: pointer;
            }
          </style>
        </head>
        <body>
          <div class="chat-container">
            <div class="chat-header">AIチャット</div>
            <div id="chat-messages"></div>
            <form class="chat-input" hx-post="/chat" hx-target="#chat-messages" hx-swap="beforeend">
              <input type="text" id="user-input" name="message" placeholder="メッセージを入力" required>
              <button id="send-button" type="submit">送信</button>
            </form>
          </div>
          <script>
            document.body.addEventListener('htmx:afterRequest', function(event) {
              if (event.detail.elt.getAttribute('hx-post') === '/chat') {
                const chatMessages = document.getElementById('chat-messages');
                const aiMessage = document.createElement('div');
                aiMessage.className = 'message ai-message';
                aiMessage.innerHTML = '<div class="message-content"></div>';
                chatMessages.appendChild(aiMessage);
                
                const userMessage = event.detail.requestConfig.parameters.message;
                // ここを修正
                const eventSource = new EventSource('/stream?message=' + encodeURIComponent(userMessage));
                eventSource.onmessage = function(e) {
                  if (e.data === '[DONE]') {
                    eventSource.close();
                  } else {
                    aiMessage.querySelector('.message-content').textContent += e.data;
                  }
                  chatMessages.scrollTop = chatMessages.scrollHeight;
                };
              }
            });
          </script>
        </body>
      </html>
    `
  )
})

app.post('/chat', async (c) => {
  const { message } = await c.req.parseBody()
  
  return c.html(html`
    <div class="message user-message">
      <div class="message-content">${message}</div>
    </div>
  `)
})

app.get('/stream', async (c) => {
  return streamSSE(c, async (stream) => {
    const chat = {
      messages: [
        { role: 'system', content: 'あなたは親切で丁寧な日本語アシスタントです。常に日本語で、簡潔かつ正確に回答してください。不適切な言葉や文字化けは避けてください。' },
        { role: 'user', content: c.req.query('message') || '最後のユーザーメッセージ' }
      ]
    }
    
    const response = await c.env.AI.run('@hf/google/gemma-7b-it', chat)
    let text = response.response
    
    for (let i = 0; i < text.length; i++) {
      await stream.writeSSE({ data: text[i] })
      await new Promise(resolve => setTimeout(resolve, 50))
    }
    
    await stream.writeSSE({ data: '[DONE]' })
  })
})

export default app


src/package.json

{
  "name": "empty-sun-39eb",
  "version": "1.0.0",
  "license": "ISC",
  "scripts": {
    "dev": "wrangler dev src/index.js",
    "deploy": "wrangler deploy src/index.js"
  }
}


いいなと思ったら応援しよう!