前回、llama.cppを使ってLLMをローカルで動かすことができたので、サーバ機能を使ってAPIサーバを起動し、ブラウザから使ってみようと思います。
llama.cppのserverを使って、APIサーバを起動します。
# /work/llama.cpp/server -ngl 39 -m /work/model/ELYZA-japanese-Llama-2-7b-instruct-q4_K_M.gguf --host 0.0.0.0 --port 8080
curlでAPIにPOSTして動作確認します。
# curl --request POST\ --url http://localhost:8080/completion\ --header "Content-Type: application/json"\ --data '{"prompt": "[INST] <<SYS>>あなたは誠実で優秀な日本人のアシスタントです。<</SYS>>Pythonでmysqlか らselectするサンプルを実装して[/INST]","n_predict": 256}' {"content":" PythonでMySQLからのデータ選択を行うためのサンプルコードを作成します。\n\nまず、PythonでMySQLに接続するモジュールを求めます。ここではPyMySQLモジュールを使用します。\n```\nimport pymysql\n```\n次に、MySQLのサーバー情報、ユーザー 名、パスワード、データベース名を指定します。\n```\nserver = \"localhost\"\nuser = \"your_username\"\npassword = \"your_password\"\ndb = \"your_database\"\n```\n次に、PyMySQLのConnect関数を呼び出し、接続情報を渡してMySQLに接続します。\n```\nconn = pymysql.connect(host=server, user=user, passwd=password, db=db)\n```\n次 に、PyMySQLのCurserを開き、SQL文を指定します。\n```\ncursor = conn.cursor()\nquery = \"SELECT * FROM your_table\"\ncursor.execute(query)\n```\n次に、PyMySQLのfetchall()関数を呼び出し、データセレクトの結果をデータベースの行として取得します。\n```\nresult = cursor.fetchall()\n```\n次に、PyMySQLのcommit()関数を呼び出し、 データベースにコミット","id_slot":0,"stop":true,"model":"/work/model/ELYZA-japanese-Llama-2-13b-fast-instruct-q4_K_M.gguf","tokens_predicted":256,"tokens_evaluated":37,"generation_settings":{"n_ctx":512,"n_predict":-1,"model":"/work/model/ELYZA-japanese-Llama-2-13b-fast-instruct-q4_K_M.gguf","seed":4294967295,"temperature":0.800000011920929,"dynatemp_range":0.0,"dynatemp_exponent":1.0,"top_k":40,"top_p":0.949999988079071,"min_p":0.05000000074505806,"tfs_z":1.0,"typical_p":1.0,"repeat_last_n":64,"repeat_penalty":1.0,"presence_penalty":0.0,"frequency_penalty":0.0,"penalty_prompt_tokens":[],"use_penalty_prompt_tokens":false,"mirostat":0,"mirostat_tau":5.0,"mirostat_eta":0.10000000149011612,"penalize_nl":false,"stop":[],"n_keep":0,"n_discard":0,"ignore_eos":false,"stream":false,"logit_bias":[],"n_probs":0,"min_keep":0,"grammar":"","samplers":["top_k","tfs_z","typical_p","top_p","min_p","temperature"]},"prompt":"[INST] <<SYS>>あなたは誠実で優秀な日本 人のアシスタントです。<</SYS>>Pythonでmysqlからselectするサンプルを実装して[/INST]","truncated":false,"stopped_eos":false,"stopped_word":false,"stopped_limit":true,"stopping_word":"","tokens_cached":292,"timings":{"prompt_n":37,"prompt_ms":31625.747,"prompt_per_token_ms":854.7499189189189,"prompt_per_second":1.1699328398472295,"predicted_n":256,"predicted_ms":22570.624,"predicted_per_token_ms":88.1665,"predicted_per_second":11.342176450239036}}redicted_per_token_ms":83.9495546875,"predicted_per_second":11.911915479748208}}
WebAPIでも同様に応答が返ってきますね。
WebAPIとお話しするのにcrulを使っていてはコマンドライン実行と変わらないのでWebサーバを立てます。
結構いろんなUIがあるみたいですが、とりあえずシンプルに行きます。
# dnf install -y httpd
UIの部分です。/var/www/html/index.htmlとして保存します。
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <script src="./script.js" defer></script> <title>Chat with llama.cpp</title> <style> body { font-family: Arial, sans-serif; background-color: #f4f4f9; margin: 0; padding: 0; display: flex; justify-content: center; align-items: center; height: 100vh; } .chat-container { width: 80%; max-width: 1200px; background-color: #fff; margin: auto; padding: 20px; border: 1px solid #ddd; border-radius: 10px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); display: flex; flex-direction: column; height: calc(100vh - 40px); } .chat-box { flex: 1; width: 100%; border: 1px solid #ddd; padding: 10px; overflow-y: scroll; border-radius: 5px; background-color: #fafafa; margin-bottom: 10px; } .input-box-container { display: flex; } .input-box { flex: 1; padding: 10px; border: 1px solid #ddd; border-radius: 5px; margin-right: 10px; resize: none; height: 100px; } .send-button { padding: 10px 20px; border: none; background-color: #4CAF50; color: white; border-radius: 5px; cursor: pointer; transition: background-color 0.3s; } .send-button:disabled { background-color: #9E9E9E; cursor: not-allowed; } .send-button:hover:enabled { background-color: #45a049; } .message { margin: 5px 0; padding: 10px; border-radius: 5px; } .user { background-color: #d1e7dd; align-self: flex-end; text-align: right; } .bot { background-color: #f8d7da; align-self: flex-start; text-align: left; } </style> </head> <body> <div class="chat-container"> <h1>Chat with llama.cpp</h1> <div class="chat-box" id="chat-box"></div> <div class="input-box-container"> <textarea id="user-input" class="input-box" placeholder="Type a message..."></textarea> <button id="send-button" class="send-button" onclick="sendMessage()">Send</button> </div> </div> </body> </html>
APIとやり取りするscript.jsです。
let botThinking = false; let botToking = false; function formatCodeBlock(text) { const codeRegex = /```([\s\S]+?)```/g; return text.replace(codeRegex, (match, p1) => { const codeElement = document.createElement('pre'); const codeContent = document.createElement('code'); codeContent.textContent = p1.trim(); codeElement.appendChild(codeContent); return codeElement.outerHTML; }); } function formatMessageText(text) { return text.replace(/\n/g, '<br>'); } function updateSendButtonState() { const sendButton = document.getElementById('send-button'); sendButton.disabled = botThinking || botToking; } async function sendMessage() { const userInput = document.getElementById('user-input').value; if (!userInput) return; const chatBox = document.getElementById('chat-box'); appendMessage('You: ' + formatMessageText(userInput), 'user'); const prompt = `[INST] <<SYS>>あなたは誠実で優秀な日本人のアシスタントです。<</SYS> ${userInput}[/INST]`; const botMessage = appendMessage('Bot: ・・・', 'bot'); botThinking = true; updateSendButtonState(); try { const response = await fetch('http://127.0.0.1:8080/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer YOUR_API_KEY' }, body: JSON.stringify({ prompt: prompt, max_tokens: 256, stream: true }) }); const reader = response.body.pipeThrough(new TextDecoderStream()).getReader(); while (true) { const { value, done } = await reader.read(); if (done) break; const lines = value.split('\n').filter(line => line.trim() !== ''); for (const line of lines) { if (line.startsWith('data:')) { const json = line.substring(5).trim(); if (json !== '[DONE]') { try { const parsed = JSON.parse(json); if (parsed.content) { if (botThinking) { botMessage.innerHTML = 'Bot: '; botThinking = false; botToking = true; updateSendButtonState(); } botMessage.innerHTML += formatCodeBlock(formatMessageText(parsed.content)); chatBox.scrollTop = chatBox.scrollHeight; } if (parsed.stop || parsed.content == "") { reader.cancel(); botToking = false; updateSendButtonState(); break; } } catch (e) { console.error('Failed to parse JSON:', e); } } } } } } catch (error) { botMessage.innerHTML = 'Bot: エラーが発生しました。'; console.error('Error:', error); } document.getElementById('user-input').value = ''; chatBox.scrollTop = chatBox.scrollHeight; } function appendMessage(text, type) { const chatBox = document.getElementById('chat-box'); const message = document.createElement('div'); message.classList.add('message', type); message.innerHTML = text; chatBox.appendChild(message); chatBox.scrollTop = chatBox.scrollHeight; return message; } document.getElementById('user-input').addEventListener('keydown', function(event) { if (event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); sendMessage(); } });
ファイルを置いたらhttpdを起動します。
# systemctl enable --now httpd
ブラウザで開くと以下のようになります。
過去の会話を考慮したりはしていないので、会話という感じではないですが、コマンドを打つより使い勝手良いはずです。