見出し画像

簡単AI対話ミニアプリ ソースコード公開

前回のLine風アプリを改良して、二人のキャラクタの定義付け、選択会話、そして二人のAIキャラ同士の対話とそこにユーザーが割り込める機能を追加してミニアプリ完成です。詳細はここでは記述しません。ソースコードのみ公開です。。

FastAPI

from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.responses import HTMLResponse
from openai import AsyncOpenAI
import json

app = FastAPI()

client = AsyncOpenAI(
    base_url='http://192.168.5.71:8080/v1',
    api_key="YOUR_OPENAI_API_KEY",  # このままでOK
)


@app.get("/", response_class=HTMLResponse)
async def get():
    with open('static/index4B.html', 'r') as f:
        return f.read()

@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
    await websocket.accept()
    while True:
            data = await websocket.receive_text()
            data_dict = json.loads(data)  # 受信したJSONデータをPython辞書に変換
            message = data_dict.get("message")
            role = data_dict.get("role")
            print(f"Received message: {message} with role: {role}")
            stream = await client.chat.completions.create(
                model="gpt-4",
                messages=[{"role": role, "content": message}],
                stream=True
            )
            response_buffer = []
            async for chunk in stream:
                if chunk.choices[0].delta.content:
                    response_message=chunk.choices[0].delta.content
                    #response_buffer.append(chunk.choices[0].delta.content)
                    response_buffer.append(response_message)
                    # チャンクをリアルタイムで送信
                    #await  websocket.send_text(chunk.choices[0].delta.content)
                    await  websocket.send_text(response_message)
            response_sum = "".join(response_buffer)
            print("chunk sum  ==>", response_sum)
            print("+++++++++++++++++++++++++++++++++++++")


if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="127.0.0.1", port=8004)

index4B.html

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Chat with llama.cpp</title>
    </head>
    <body>
        <div id="mainArea">
            <div class="left" id="responses"></div>
            <div class="right">
                <div class="right-content">
                    <div id="inputArea">
                        <h4>メッセージ・質問・会話・指示</h6>
                        <div>
                            <label><input type="radio" name="character" value="char1"> キャラ1</label>
                            <label><input type="radio" name="character" value="char2"> キャラ2</label>
                            <label><input type="radio" name="character" value="char12"> みんなで雑談</label>
                            <button onclick="conversationTerminate()">雑談終わり</button>
                        </div>
                        <textarea class="user-input"  type="text" id="inputText" placeholder="メッセージを書く"></textarea>
                        <div id="nameArea">
                            <h4 style="margin-top:15px;margin-left: 5px;">キャラ1</h6>
                            <input type="text1" id="nameInput1" placeholder="名前">
                            <h4 style="margin-top:15px;margin-left: 5px;">キャラ2</h6>
                            <input type="text1" id="nameInput2" placeholder="名前">
                            <h4 style="margin-top:15px;margin-left: 5px;">ユーザ</h6>
                                <input type="text1" id="nameUser" placeholder="User">
                            <h4 style="margin-top:15px;margin-left: 5px;">会話記憶数</h6>
                            <input type="number" id="numberInput" min="0" max="20" placeholder="0-20">
                        </div>
                    </div>
                    <div id="roleArea">
                        <h4>キャラ1へのRole</h6>
                        <textarea class="role-input" type="text" id="roleText" placeholder="LLMのロール"></textarea>
                    </div>
                    <div id="roleArea">
                        <h4>キャラ2へのRole</h6>
                        <textarea class="role-input" type="text" id="roleText2" placeholder="LLMのロール"></textarea>
                    </div>
                </div>
            </div>
        </div>
    </body>
</html>

<script>
    const ws = new WebSocket("ws://127.0.0.1:8004/ws");
    let isFirstResponse = true;  // フラグを初期化
    const messagesContainer = document.getElementById('responses');  // コンテナを取得
    let savedRoleMessage = "";  // roleMessageを保存するためのグローバル変数
    let LastChank = "";
    let lastMessage = "";
    let conversationLogs = [];  // 会話ログを保存する配列
    const defaultMaxLogs = 5;   // デフォルトの最大ログ数
    let singleTurn = "";
    let currentUserName = "";  // ユーザー名を保存するためのグローバル変数
    let AutoConversationFlag = false;
    let togle=false;

    document.querySelectorAll('textarea').forEach(textarea => {
        textarea.addEventListener('input', function() {
            this.style.height = 'auto';
            this.style.height = (this.scrollHeight) + 'px';
        }, false);
    });

    // Enterキーでメッセージを送信
    document.getElementById('inputText').addEventListener('keydown', function(event) {
        if (event.key === 'Enter' && !event.shiftKey) {  // Shift + Enterで改行
            const selected = document.querySelector('input[name="character"]:checked');
            AutoConversationFlag=false;
            if (selected) {
                const character = selected.value;
                console.log("+++++character=", character);}
            event.preventDefault();  // ページリロード防止
            const input=document.getElementById('inputText');
            const nameU =  document.getElementById('nameUser');
            if (selected.value === "char1") {
                const role = document.getElementById('roleText');
                const name =  document.getElementById('nameInput1');
                sendMessage(input,role,name,nameU);}  // メッセージ送信
            else if (selected.value === "char2") {
                const role = document.getElementById('roleText2');
                const name =  document.getElementById('nameInput2');
                sendMessage(input,role,name,nameU);}  // メッセージ送信
            else if (selected.value === "char12") {
                const role = document.getElementById('roleText');   
                const name =  document.getElementById('nameInput1');
                sendMessage(input,role,name,nameU);  // メッセージ送信
                AutoConversationFlag=true;
                AutoConversation(input);}  // みんなで雑談開始
            else {
                alert("Please select a character.");
                return;}
        }
    });

    ws.onmessage = function(event) {
        if (isFirstResponse) {
            const initialDiv = document.createElement('div');
            initialDiv.className = 'response-message assistant';  // Assistant message on the left
            if (AutoConversationFlag !== true){
                initialDiv.innerHTML = `<strong style="color: red;">${currentUserName}:</strong> `;}
            else{
                if (togle){
                    initialDiv.innerHTML = `<strong style="color: red;">${currentUserName}:</strong> `;
                    togle=false;}
                else{
                    initialDiv.innerHTML = `<strong style="color: green;">${currentUserName}:</strong> `;
                    togle=true;}
                }
            messagesContainer.appendChild(initialDiv);
            isFirstResponse = false;}
        const lastMessageDiv = messagesContainer.lastElementChild;
        if (lastMessageDiv && lastMessageDiv.classList.contains('response-message')) {
            const span = document.createElement('span');
            span.innerHTML = event.data;
            lastMessageDiv.appendChild(span);
            LastChank += event.data;} 
        else {
            const newMessageDiv = document.createElement('div');
            newMessageDiv.className = 'response-message assistant';  // Assistant message on the left
            newMessageDiv.innerHTML = event.data;
            messagesContainer.appendChild(newMessageDiv);
            LastChank += event.data;}
        messagesContainer.scrollTop = messagesContainer.scrollHeight;
    };

    function conversationTerminate(){
        console.log("+++++conversationTerminate");
        AutoConversationFlag=false;
        console.log("+++++conversationTerminate", AutoConversationFlag);
    };

    async function AutoConversation(input){
        let inputa=input;
        while (AutoConversationFlag === true){
            const role = document.getElementById('roleText');
            const name =  document.getElementById('nameInput1');
            const role2 = document.getElementById('roleText2');
            const name2 =  document.getElementById('nameInput2');
            await sendMessage(inputa,role,name,name2);  // メッセージ送信
            await new Promise(resolve => setTimeout(resolve, 10000));
            let inputb=lastMessage
            await sendMessage(inputb,role2,name2,name);  // メッセージ送信  2回目
            await new Promise(resolve => setTimeout(resolve, 10000));
            inputa=lastMessage;
        }
    }; 

    function sendMessage(input,role,name_selected,name_user) {
        let message;
        if (typeof input === 'string') {   // input がテキスト文字列の場合
            message = input;               // input が文字列ならそのまま使用
        } else {                           // input がDOM要素の場合
            const inputElement = input;
            message = inputElement.value;  // inputElement の .value を取得
        }
        console.log("+++++INPUTmessage=", message);
        const inputElement = input;
        const roleElement = role;
        const numberElement = document.getElementById('numberInput');
        const nameElement = name_selected;
        const name = nameElement.value.trim();
        const name_uElement=name_user;
        const name_u = name_uElement.value.trim();       
        let roleMessage = roleElement.value.trim();
    
        if (!message.trim() && !name.trim()) {
            alert("Please enter llm name and a message.");
            return;}
        if (roleMessage === "") {
            roleMessage = savedRoleMessage;} 
        else {
            savedRoleMessage = roleMessage;}
        currentUserName = name;
        console.log("+++++currentUserName=", currentUserName);  
        if (message.trim() === "" && roleMessage === "") return;
        if (lastMessage !== "") {
            singleTurn = 'user:' + lastMessage + 'response:' + LastChank;};
        lastMessage = LastChank;
        LastChank = "";

        console.log("+++++RESPONCERmessage=", lastMessage);
        const conv_log = addLog(singleTurn);
        const NewPrompt = conv_log + 'user:' + message;
        if (AutoConversationFlag !== true){
            const userDiv = document.createElement('div');
            userDiv.className = 'user-message';  // User message on the right
            userDiv.innerHTML = `<strong style="color: blue;">${name_u}:</strong> ${message}`;
            messagesContainer.appendChild(userDiv);
            }
        ws.send(JSON.stringify({ message: NewPrompt, role: roleMessage }));
        inputElement.value = '';
        isFirstResponse = true;
        messagesContainer.scrollTop = messagesContainer.scrollHeight;
    }

    function addLog(message) {
        const maxLogCount = document.getElementById('numberInput').value || defaultMaxLogs;
        const logEntry = { message };
        conversationLogs.push(logEntry);
        if (conversationLogs.length > maxLogCount) {
            conversationLogs.shift();}
        return conversationLogs.map(log => log.message).join('\n');
    }
</script>

<style>
    body {
      display: flex;
      flex-direction: row; /* 横並び */
      justify-content: space-between; /* 要素間のスペースを調整 */
      margin: 0;
      padding: 20px;
      height: 100vh; /* ビューポートの高さに合わせる */
    }
    .left {
      padding: 20px;
      background-color: #f0f0f0;
      margin: 10px;
    }
    .left {
      flex-grow: 2; /* 左の要素が大きくなるように設定 */
      flex-basis: 60%; /* 基本的に左側の幅を70%に設定 */
    }
    .right {
      flex-grow: 1; /* 右の要素が小さくなるように設定 */
      flex-basis: 40%; /* 基本的に右側の幅を30%に設定 */
    }
    .right-content {
        padding-right: 20px;
        padding-top: 10px;
        background-color: lightblue; /* 右側の要素に合わせた背景色 */
        }

    #mainArea {
        display: flex;
        width: 100%;}
    #inputArea, #roleArea {
        width: 100%;
        padding: 10px;
        padding-top:0px;
        margin-top:-20px;
        padding-right:10px; }
    #roleArea {
        width: 100%;
        padding: 10px;
        padding-top:0px;
        margin-top:-20px;
        padding-right:10px; }
    #nameArea {
        display: flex;
        width: 100%;}
    .user-input{
        height: 50px;
        }
    .role-input{
        height: 150px;
    }
    textarea, input[type="text"], input[type="number"] {
        width: 100%;
        padding: 10px;
        box-sizing: border-box;
        margin-bottom: 5px;
        font-size: 18px;}

    input, input[type="text1"], input[type="number"] {
        width: 13%;
        padding-top: 10px;
        margin-top: 5px;
        margin-bottom: 5px;
        box-sizing: border-box;
          font-size: 18px;}

    h4 {margin-bottom: 5px;}
    #responses {
        width: 65%;
        height: 800px;
        overflow-y: auto;
        border: 1px solid #ccc;
        padding: 0px;
        margin-top:0px;
        background-color: #f9f9f9;
        white-space: pre-wrap;}
    .user-message, .response-message {
        margin: 5px 0;
        padding: 10px;
        border-radius: 10px;}
    .user-message {
        background-color: #d1ecf1;
        text-align: right;
        margin-left: auto;
        width: fit-content;
        max-width: 80%;}
    .response-message {
        background-color: #f8d7da;
        text-align: left;
        margin-right: auto;
        width: fit-content;
        max-width: 80%;}

</style>