前回、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
ブラウザで開くと以下のようになります。

過去の会話を考慮したりはしていないので、会話という感じではないですが、コマンドを打つより使い勝手良いはずです。