1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

震災などの災害を想定したシンプルなローカルLLMのWEB-UIをPythonで書きました。

Last updated at Posted at 2025-08-03

これは何?

履歴・Ubuntuで実行しChromebookで家庭内LANで接続する・モデルの切り替えが出来る。
だけの、シンプルなWeb-UIです。

デモ

スクリーンショット

Screenshot 2025-08-03 14.37.03.png
Screenshot 2025-08-03 14.36.40.png

設計思想

日本は地震国です。AIに慣れて、被災時に使えないことは不便で不利益でしょう。

パソコンのCPU/GPUで動くから、外部と通信しないのでプライバシーやデータ主権の意味でも価値があります。

最低限、GUIで操作出来ると導入しやすいと考えました。

非常用に個人や組織が用意することで、被災時のインフラへの負担を減らせます。災害時に限られた通信帯域は、安否確認、救助要請、緊急連絡など、人命に関わる重要な情報のやり取りを優先したいと考えました。

防災ツールキットにローカルLLMも常備出来るようになれば、その仕組を海外に輸出すると利益が出るかもしれません。人は安心にお金払うので。

Ollamaバージョン

  • ollama version is 0.9.6

使用したモデル

  • qwen3:8b-q4_K_M
  • gemma3:12b-it-q4_K_M
  • deepseek-r1:7b-qwen-distill-q4_K_M

この3つの中では、「qwen3:8b-q4_K_M」が安定して日本語で回答しました。英語で回答されて方も利用されるなら、これが無難かもしれません。

ソースコード

試作品です。私の環境では、これで動きます。

#!/usr/bin/env python3
# ollama-webui.py
#
# 概要:
# 家庭内LAN上のOllamaと対話するための、単一ファイルで動作するWebアプリケーションです。
# FlaskをWebフレームワークとして使用し、SQLiteで会話履歴を管理します。
#
# 依存ライブラリのインストール:
# pip install Flask requests
#
# 実行方法:
# python ollama-webui.py
#
# ブラウザからのアクセス:
# http://<このスクリプトを実行しているマシンのIPアドレス>:8080

import sqlite3
import json
import requests
import uuid
from flask import Flask, request, Response, jsonify, render_template_string
from datetime import datetime

# --- 基本設定 (ご自身の環境に合わせて変更してください) ---
DB_FILE = "chat_history.db"
# Ollamaが動作しているURL。Docker内でこのスクリプトを動かす場合などは 'http://host.docker.internal:11434' などに変更。
OLLAMA_BASE_URL = "http://localhost:11434"
OLLAMA_API_URL = f"{OLLAMA_BASE_URL}/api/chat"
OLLAMA_TAGS_URL = f"{OLLAMA_BASE_URL}/api/tags"
HOST = "0.0.0.0"  # LAN内の他のデバイスからアクセス可能にする
PORT = 8080

# --- Flaskアプリケーションの初期化 ---
app = Flask(__name__)

# --- データベースのセットアップ ---
def get_db_connection():
    """データベース接続を取得します。"""
    conn = sqlite3.connect(DB_FILE)
    conn.row_factory = sqlite3.Row
    return conn

def init_database():
    """データベースを初期化し、必要なテーブルを作成します。"""
    conn = get_db_connection()
    cursor = conn.cursor()
    # 会話のメタ情報を管理するテーブル
    cursor.execute("""
    CREATE TABLE IF NOT EXISTS conversations (
        id TEXT PRIMARY KEY,
        title TEXT NOT NULL,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP
    )
    """)
    # 個々のメッセージを保存するテーブル
    cursor.execute("""
    CREATE TABLE IF NOT EXISTS messages (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        conversation_id TEXT NOT NULL,
        model TEXT NOT NULL,
        role TEXT NOT NULL, -- 'user' or 'assistant'
        content TEXT NOT NULL,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        FOREIGN KEY (conversation_id) REFERENCES conversations (id) ON DELETE CASCADE
    )
    """)
    conn.commit()
    conn.close()
    print(f"データベース '{DB_FILE}' が初期化されました。")


# --- HTML/CSS/JSテンプレート (単一ファイルに埋め込み) ---
HTML_TEMPLATE = """
<!DOCTYPE html>
<html lang="ja" class="dark">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Local LLM Chat</title>
    <!-- Tailwind CSS -->
    <script src="https://cdn.tailwindcss.com"></script>
    <!-- marked.js (Markdownパーサー) -->
    <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
    <!-- Font Awesome (アイコン) -->
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
    <style>
        /* カスタムスタイル */
        body { font-family: 'Inter', sans-serif; }
        @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap');
        
        /* スクロールバーのスタイル */
        ::-webkit-scrollbar { width: 8px; }
        ::-webkit-scrollbar-track { background: #1f2937; }
        ::-webkit-scrollbar-thumb { background: #4b5563; border-radius: 4px; }
        ::-webkit-scrollbar-thumb:hover { background: #6b7280; }

        /* コードブロックのスタイル */
        .prose pre {
            background-color: #1e293b;
            color: #e2e8f0;
            padding: 1rem;
            border-radius: 0.5rem;
            position: relative;
        }
        .prose pre code {
            font-family: 'Fira Code', monospace;
        }
        .copy-code-btn {
            position: absolute;
            top: 0.5rem;
            right: 0.5rem;
            background-color: #334155;
            color: #94a3b8;
            border: none;
            padding: 0.25rem 0.5rem;
            border-radius: 0.25rem;
            cursor: pointer;
            opacity: 0;
            transition: opacity 0.2s;
        }
        .prose pre:hover .copy-code-btn {
            opacity: 1;
        }
        .copy-code-btn:hover {
            background-color: #475569;
            color: #e2e8f0;
        }
        #chat-messages {
            scroll-behavior: smooth;
        }
    </style>
</head>
<body class="bg-gray-900 text-gray-200 flex h-screen">

    <!-- サイドバー: 会話履歴 -->
    <aside id="sidebar" class="bg-gray-800 w-64 p-4 flex flex-col h-full">
        <h1 class="text-xl font-bold mb-4">会話履歴</h1>
        <button id="new-chat-btn" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-lg mb-4 transition-colors">
            <i class="fas fa-plus mr-2"></i>新しいチャット
        </button>
        <div id="conversation-list" class="flex-grow overflow-y-auto">
            <!-- 会話履歴がここに動的に挿入されます -->
        </div>
        <div id="status-indicator" class="mt-auto text-sm text-gray-400">
            <span id="ollama-status-icon" class="mr-2"><i class="fas fa-circle text-yellow-500 animate-pulse"></i></span>
            <span id="ollama-status-text">Ollamaに接続中...</span>
        </div>
    </aside>

    <!-- メインコンテンツ: チャットウィンドウ -->
    <main class="flex-1 flex flex-col h-full">
        <!-- ヘッダー: モデル選択 -->
        <header class="bg-gray-800/50 backdrop-blur-sm p-4 border-b border-gray-700 flex items-center justify-between">
            <h2 id="chat-title" class="text-lg font-semibold">新しいチャット</h2>
            <div class="flex items-center gap-4">
                 <select id="model-select" class="bg-gray-700 border border-gray-600 rounded-lg px-3 py-2 text-white focus:outline-none focus:ring-2 focus:ring-blue-500">
                    <!-- モデルがここに動的に挿入されます -->
                </select>
                <button id="delete-chat-btn" class="text-gray-400 hover:text-red-500 transition-colors" title="このチャットを削除">
                    <i class="fas fa-trash-alt"></i>
                </button>
            </div>
        </header>

        <!-- チャットメッセージ表示エリア -->
        <div id="chat-messages" class="flex-1 overflow-y-auto p-6 space-y-6">
            <!-- メッセージがここに動的に挿入されます -->
        </div>

        <!-- 入力フォーム -->
        <div class="p-4 bg-gray-900/80 backdrop-blur-sm border-t border-gray-700">
            <form id="chat-form" class="flex items-center space-x-4">
                <textarea id="message-input" class="flex-1 bg-gray-700 rounded-lg p-3 resize-none focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="メッセージを入力... (Shift+Enterで改行)" rows="1"></textarea>
                <button type="submit" id="send-btn" class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 px-5 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
                    <i class="fas fa-paper-plane"></i>
                </button>
            </form>
        </div>
    </main>

<script>
    // --- DOM要素の取得 ---
    const modelSelect = document.getElementById('model-select');
    const conversationList = document.getElementById('conversation-list');
    const newChatBtn = document.getElementById('new-chat-btn');
    const deleteChatBtn = document.getElementById('delete-chat-btn');
    const chatMessages = document.getElementById('chat-messages');
    const chatForm = document.getElementById('chat-form');
    const messageInput = document.getElementById('message-input');
    const sendBtn = document.getElementById('send-btn');
    const chatTitle = document.getElementById('chat-title');
    const ollamaStatusIcon = document.getElementById('ollama-status-icon');
    const ollamaStatusText = document.getElementById('ollama-status-text');

    // --- 状態管理 ---
    let currentConversationId = null;
    let messages = [];
    let isGenerating = false;

    // --- 初期化処理 ---
    document.addEventListener('DOMContentLoaded', async () => {
        await checkOllamaStatus();
        await loadModels();
        await loadConversations();
        startNewChat();
        
        // テキストエリアの自動リサイズ
        messageInput.addEventListener('input', () => {
            messageInput.style.height = 'auto';
            messageInput.style.height = (messageInput.scrollHeight) + 'px';
        });

        // Shift+Enter以外でのEnterキーで送信
        messageInput.addEventListener('keydown', (e) => {
            if (e.key === 'Enter' && !e.shiftKey) {
                e.preventDefault();
                chatForm.dispatchEvent(new Event('submit'));
            }
        });
    });

    // --- API通信 ---
    /** Ollamaサーバーの状態を確認する */
    async function checkOllamaStatus() {
        try {
            const response = await fetch(OLLAMA_BASE_URL);
            if (response.ok) {
                ollamaStatusIcon.innerHTML = '<i class="fas fa-circle text-green-500"></i>';
                ollamaStatusText.textContent = 'Ollama 接続済み';
            } else {
                throw new Error('Ollama not running');
            }
        } catch (error) {
            ollamaStatusIcon.innerHTML = '<i class="fas fa-circle text-red-500"></i>';
            ollamaStatusText.textContent = 'Ollama 接続不可';
            console.error('Ollama status check failed:', error);
        }
    }

    /** 利用可能なモデルをロードしてドロップダウンに設定する */
    async function loadModels() {
        try {
            const response = await fetch('/api/models');
            if (!response.ok) throw new Error('モデルの取得に失敗しました。');
            const data = await response.json();
            modelSelect.innerHTML = data.models
                .map(model => `<option value="${model.name}">${model.name}</option>`)
                .join('');
        } catch (error) {
            console.error(error);
            modelSelect.innerHTML = '<option>モデル取得失敗</option>';
        }
    }

    /** 会話履歴をロードしてサイドバーに表示する */
    async function loadConversations() {
        try {
            const response = await fetch('/api/conversations');
            if (!response.ok) throw new Error('会話履歴の取得に失敗しました。');
            const conversations = await response.json();
            conversationList.innerHTML = conversations
                .map(conv => `
                    <div class="conversation-item p-2 rounded-lg cursor-pointer hover:bg-gray-700 ${conv.id === currentConversationId ? 'bg-blue-800/50' : ''}" data-id="${conv.id}">
                        <p class="font-semibold truncate">${conv.title}</p>
                        <p class="text-xs text-gray-400">${new Date(conv.created_at).toLocaleString()}</p>
                    </div>
                `).join('');
            
            // イベントリスナーを設定
            document.querySelectorAll('.conversation-item').forEach(item => {
                item.addEventListener('click', () => loadConversation(item.dataset.id));
            });
        } catch (error) {
            console.error(error);
            conversationList.innerHTML = '<p class="text-red-400">履歴の取得に失敗</p>';
        }
    }
    
    /** 特定の会話をロードする */
    async function loadConversation(convId) {
        if (isGenerating) return;
        try {
            const response = await fetch(`/api/conversations/${convId}`);
            if (!response.ok) throw new Error('会話の読み込みに失敗しました。');
            const data = await response.json();
            
            currentConversationId = convId;
            messages = data.messages;
            chatTitle.textContent = data.title;
            deleteChatBtn.style.display = 'block';

            renderMessages();
            updateConversationListHighlight();
        } catch (error) {
            console.error(error);
        }
    }

    // --- イベントハンドラ ---
    /** 新しいチャットボタンの処理 */
    newChatBtn.addEventListener('click', startNewChat);

    /** チャット削除ボタンの処理 */
    deleteChatBtn.addEventListener('click', async () => {
        if (!currentConversationId || !confirm('この会話を本当に削除しますか?')) return;
        
        try {
            const response = await fetch(`/api/conversations/delete/${currentConversationId}`, { method: 'POST' });
            if (!response.ok) throw new Error('会話の削除に失敗しました。');
            
            await loadConversations();
            startNewChat();
        } catch (error) {
            console.error(error);
            alert(error.message);
        }
    });

    /** チャットフォーム送信時の処理 */
    chatForm.addEventListener('submit', async (e) => {
        e.preventDefault();
        const userInput = messageInput.value.trim();
        if (!userInput || isGenerating) return;

        toggleGenerating(true);

        // ユーザーのメッセージを履歴に追加して表示
        const userMessage = { role: 'user', content: userInput };
        messages.push(userMessage);
        renderMessages();
        scrollToBottom();

        // 新規チャットの場合、最初に会話を作成する
        if (!currentConversationId) {
            try {
                const response = await fetch('/api/conversations/new', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ title: userInput.substring(0, 30) })
                });
                if (!response.ok) throw new Error('新規会話の作成に失敗しました。');
                const data = await response.json();
                currentConversationId = data.conversation_id;
                chatTitle.textContent = data.title;
                deleteChatBtn.style.display = 'block';
                await loadConversations();
                updateConversationListHighlight();
            } catch (error) {
                console.error(error);
                toggleGenerating(false);
                return;
            }
        }
        
        // アシスタントの応答をストリーミングで取得
        const assistantMessage = { role: 'assistant', content: '' };
        messages.push(assistantMessage);
        const assistantMessageDiv = renderMessage(assistantMessage, messages.length - 1);
        chatMessages.appendChild(assistantMessageDiv);
        const contentDiv = assistantMessageDiv.querySelector('.message-content');
        
        try {
            const response = await fetch('/api/chat', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({
                    conversation_id: currentConversationId,
                    model: modelSelect.value,
                    messages: messages.slice(0, -1) // 最後のアシスタントの空メッセージは含めない
                })
            });

            if (!response.ok) {
                 const errorData = await response.json();
                 throw new Error(errorData.error || 'APIリクエストに失敗しました。');
            }

            const reader = response.body.getReader();
            const decoder = new TextDecoder();
            
            while (true) {
                const { value, done } = await reader.read();
                if (done) break;
                
                const chunk = decoder.decode(value);
                const lines = chunk.split('\\n\\n');

                for (const line of lines) {
                    if (line.startsWith('data: ')) {
                        const jsonData = line.substring(6);
                        if (jsonData.trim()) {
                            try {
                                const parsedData = JSON.parse(jsonData);
                                if (parsedData.content) {
                                    assistantMessage.content += parsedData.content;
                                    renderMarkdown(contentDiv, assistantMessage.content + ' ▌');
                                    scrollToBottom();
                                }
                                if (parsedData.error) {
                                    throw new Error(parsedData.error);
                                }
                            } catch (e) {
                                console.warn("JSONのパースに失敗:", jsonData);
                            }
                        }
                    }
                }
            }
            
        } catch (error) {
            console.error('ストリーミングエラー:', error);
            assistantMessage.content += `\n\n**エラーが発生しました:** ${error.message}`;
        } finally {
            renderMarkdown(contentDiv, assistantMessage.content);
            addCopyButtons();
            toggleGenerating(false);
            // 会話が長くなった場合、タイトルを更新するかもしれない
            // await loadConversations();
        }
    });

    // --- UI描画・ヘルパー関数 ---
    /** 新しいチャットを開始する状態にする */
    function startNewChat() {
        currentConversationId = null;
        messages = [];
        chatTitle.textContent = '新しいチャット';
        chatMessages.innerHTML = `
            <div class="text-center text-gray-500">
                <p>下部の入力欄からチャットを開始してください。</p>
            </div>
        `;
        deleteChatBtn.style.display = 'none';
        updateConversationListHighlight();
    }

    /** メッセージリスト全体を描画する */
    function renderMessages() {
        chatMessages.innerHTML = '';
        messages.forEach((msg, index) => {
            const messageDiv = renderMessage(msg, index);
            chatMessages.appendChild(messageDiv);
        });
        addCopyButtons();
        scrollToBottom();
    }

    /** 個々のメッセージを描画するHTML要素を生成する */
    function renderMessage(message, index) {
        const isUser = message.role === 'user';
        const div = document.createElement('div');
        div.className = `flex items-start gap-4 ${isUser ? 'justify-end' : ''}`;
        
        const iconClass = isUser ? 'fa-user' : 'fa-robot';
        const bgColor = isUser ? 'bg-blue-600' : 'bg-gray-700';
        
        const iconHtml = `
            <div class="w-8 h-8 rounded-full ${bgColor} flex items-center justify-center flex-shrink-0">
                <i class="fas ${iconClass}"></i>
            </div>
        `;

        const contentHtml = `
            <div class="max-w-2xl prose prose-invert message-content rounded-lg p-4 ${isUser ? 'bg-blue-900/50' : 'bg-gray-800'}">
                ${message.content}
            </div>
        `;

        div.innerHTML = isUser ? contentHtml + iconHtml : iconHtml + contentHtml;
        
        // 初回レンダリング時にMarkdownを適用
        renderMarkdown(div.querySelector('.message-content'), message.content);
        
        return div;
    }
    
    /** MarkdownをHTMLに変換して表示する */
    function renderMarkdown(element, text) {
        element.innerHTML = marked.parse(text);
    }
    
    /** コードブロックにコピーボタンを追加する */
    function addCopyButtons() {
        document.querySelectorAll('.prose pre').forEach(pre => {
            // 既にボタンがあれば追加しない
            if (pre.querySelector('.copy-code-btn')) return;

            const code = pre.querySelector('code');
            const btn = document.createElement('button');
            btn.innerHTML = '<i class="fas fa-copy"></i>';
            btn.className = 'copy-code-btn';
            btn.title = 'Copy code';
            btn.addEventListener('click', () => {
                navigator.clipboard.writeText(code.innerText).then(() => {
                    btn.innerHTML = '<i class="fas fa-check"></i>';
                    setTimeout(() => { btn.innerHTML = '<i class="fas fa-copy"></i>'; }, 2000);
                });
            });
            pre.appendChild(btn);
        });
    }

    /** チャットウィンドウを一番下までスクロールする */
    function scrollToBottom() {
        chatMessages.scrollTop = chatMessages.scrollHeight;
    }

    /** 会話リストのハイライトを更新する */
    function updateConversationListHighlight() {
        document.querySelectorAll('.conversation-item').forEach(item => {
            if (item.dataset.id === currentConversationId) {
                item.classList.add('bg-blue-800/50');
            } else {
                item.classList.remove('bg-blue-800/50');
            }
        });
    }

    /** 生成中のUIの状態を切り替える */
    function toggleGenerating(generating) {
        isGenerating = generating;
        sendBtn.disabled = generating;
        messageInput.disabled = generating;
        messageInput.value = generating ? '' : messageInput.value;
        messageInput.style.height = 'auto';
    }

</script>
</body>
</html>
"""

# --- APIエンドポイント ---

@app.route("/")
def index():
    """メインのWebページを表示します。"""
    return render_template_string(HTML_TEMPLATE)

@app.route("/api/models")
def get_models():
    """Ollamaから利用可能なモデルの一覧を取得します。"""
    try:
        response = requests.get(OLLAMA_TAGS_URL, timeout=5)
        response.raise_for_status()
        return jsonify(response.json())
    except requests.exceptions.RequestException as e:
        print(f"Ollamaモデルの取得に失敗: {e}")
        return jsonify({"error": "Ollamaサーバーに接続できませんでした。"}), 500

@app.route("/api/conversations", methods=['GET'])
def get_conversations():
    """保存されているすべての会話のリストを取得します。"""
    conn = get_db_connection()
    conversations = conn.execute("SELECT id, title, created_at FROM conversations ORDER BY created_at DESC").fetchall()
    conn.close()
    return jsonify([dict(row) for row in conversations])

@app.route("/api/conversations/new", methods=['POST'])
def new_conversation():
    """新しい会話を作成します。"""
    data = request.json
    title = data.get('title', '無題の会話')
    new_id = str(uuid.uuid4())
    
    conn = get_db_connection()
    conn.execute("INSERT INTO conversations (id, title) VALUES (?, ?)", (new_id, title))
    conn.commit()
    conn.close()
    
    return jsonify({"conversation_id": new_id, "title": title})

@app.route("/api/conversations/<string:conv_id>", methods=['GET'])
def get_conversation_details(conv_id):
    """特定の会話のタイトルとメッセージ履歴を取得します。"""
    conn = get_db_connection()
    conv = conn.execute("SELECT title FROM conversations WHERE id = ?", (conv_id,)).fetchone()
    if not conv:
        return jsonify({"error": "Conversation not found"}), 404
        
    messages = conn.execute(
        "SELECT role, content FROM messages WHERE conversation_id = ? ORDER BY created_at ASC",
        (conv_id,)
    ).fetchall()
    conn.close()
    
    return jsonify({
        "title": conv['title'],
        "messages": [dict(row) for row in messages]
    })

@app.route("/api/conversations/delete/<string:conv_id>", methods=['POST'])
def delete_conversation(conv_id):
    """特定の会話を削除します。"""
    conn = get_db_connection()
    conn.execute("DELETE FROM conversations WHERE id = ?", (conv_id,))
    # messagesテーブルはFOREIGN KEYのON DELETE CASCADEで自動的に削除される
    conn.commit()
    conn.close()
    return jsonify({"success": True})


@app.route("/api/chat", methods=["POST"])
def chat():
    """Ollamaと通信し、結果をストリーミングで返します。"""
    data = request.json
    model = data.get("model")
    conversation_id = data.get("conversation_id")
    # フロントエンドから送られてくるメッセージ履歴には、ユーザーの最新メッセージが含まれている
    messages_history = data.get("messages", [])

    if not all([model, conversation_id, messages_history]):
        return jsonify({"error": "モデル、会話ID、メッセージは必須です。"}), 400

    # ユーザーの最後のメッセージをDBに保存
    user_message = messages_history[-1]
    conn = get_db_connection()
    conn.execute(
        "INSERT INTO messages (conversation_id, model, role, content) VALUES (?, ?, ?, ?)",
        (conversation_id, model, 'user', user_message['content'])
    )
    conn.commit()
    conn.close()

    def generate_responses():
        assistant_full_response = ""
        try:
            # Ollamaへのリクエストペイロードを作成
            payload = {
                "model": model,
                "messages": messages_history,
                "stream": True
            }
            
            # requestsを使ってOllamaにストリーミングリクエストを送信
            with requests.post(OLLAMA_API_URL, json=payload, stream=True, timeout=300) as r:
                r.raise_for_status() # HTTPエラーがあれば例外を発生させる
                
                for chunk in r.iter_lines():
                    if chunk:
                        try:
                            decoded_chunk = json.loads(chunk)
                            content = decoded_chunk.get("message", {}).get("content", "")
                            assistant_full_response += content
                            
                            # フロントエンドにイベントを送信
                            sse_data = {"content": content}
                            yield f"data: {json.dumps(sse_data)}\n\n"
                            
                            # ストリームの最後かチェック
                            if decoded_chunk.get("done"):
                                break
                        except json.JSONDecodeError:
                            print(f"JSONデコードエラー: {chunk}")
                            continue

        except requests.exceptions.RequestException as e:
            print(f"Ollamaへのリクエストエラー: {e}")
            error_message = f"Ollamaへの接続中にエラーが発生しました: {e}"
            yield f"data: {json.dumps({'error': error_message})}\n\n"
        except Exception as e:
            print(f"予期せぬエラー: {e}")
            error_message = f"予期せぬサーバーエラーが発生しました: {e}"
            yield f"data: {json.dumps({'error': error_message})}\n\n"
        finally:
            # アシスタントの完全な応答をデータベースに保存
            if assistant_full_response.strip():
                conn_final = get_db_connection()
                conn_final.execute(
                    "INSERT INTO messages (conversation_id, model, role, content) VALUES (?, ?, ?, ?)",
                    (conversation_id, model, 'assistant', assistant_full_response)
                )
                conn_final.commit()
                conn_final.close()

    # Server-Sent Events (SSE)としてレスポンスを返す
    return Response(generate_responses(), mimetype="text/event-stream")


# --- アプリケーションの実行 ---
if __name__ == "__main__":
    init_database()
    print(f"サーバーを http://{HOST}:{PORT} で起動します。")
    print("LAN内の他のデバイスからは http://<あなたのIPアドレス>:"+str(PORT)+" でアクセスしてください。")
    app.run(host=HOST, port=PORT, debug=False) # debug=Trueは開発時のみ
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?