import streamlit as st
from openai import OpenAI
import chromadb
from docx import Document
import requests
# ChromaDBの設定
DB_DIR = "./chroma_db"
chroma_client = chromadb.PersistentClient(path=DB_DIR)
if "collection" not in st.session_state:
st.session_state.collection = chroma_client.get_or_create_collection(
name="local_docs"
)
# Ollamaからインストールしたモデルを使ったベクトル化関数
def ollama_embed(text):
r = requests.post(
"http://localhost:12000/api/embeddings",
json={"model": "nomic-embed-text", "prompt": text}
)
data = r.json()
return data["embedding"]
# Wordファイルを読み込む関数
def load_word_document(file):
return "\n".join(para.text for para in Document(file).paragraphs)
# テキスト分割関数
def split_text(text):
chunk_size = 200
overlap = 50
chunks = []
start = 0
while start < len(text):
end = start + chunk_size
chunks.append(text[start:end])
start += chunk_size - overlap
return chunks
## サイドバーの設定 ##
st.set_page_config(page_title="Local LLM Chat")
st.sidebar.title("設定")
model = st.sidebar.text_input("モデル名", value="llama3.1:8b")
temperature = st.sidebar.slider("temperature", 0.0, 2.0, 0.3, 0.1)
system_prompt = st.sidebar.text_area(
"System Prompt",
"あなたは有能なアシスタントです。日本語で回答してください",
)
# ワードファイルのアップロード
uploaded_files = st.sidebar.file_uploader(
"Wordファイルをアップロード(.docx)",
type=["docx"],
accept_multiple_files=True
)
if st.sidebar.button("インデックス作成"):
for file in uploaded_files:
text = load_word_document(file)
chunks = split_text(text)
for i, chunk in enumerate(chunks):
embed = ollama_embed(chunk)
st.session_state.collection.add(
documents=[chunk],
embeddings=,
ids=[f"{file.name}_{i}"]
)
st.sidebar.success("インデックス作成完了")
# タイトル
st.title("Local LLM Chat")
# 会話の履歴を保管
if "messages" not in st.session_state:
st.session_state.messages = []
# 会話の履歴をリセットするボタン
if st.sidebar.button("会話をリセット"):
st.session_state.messages = []
# 会話の履歴を表示
for m in st.session_state.messages:
with st.chat_message(m["role"]):
st.write(m["content"])
prompt = st.chat_input("メッセージを入力")
client = OpenAI(
api_key="ollama",
base_url="http://localhost:12000/v1"
)
if prompt:
# ユーザーのプロンプトを表示
with st.chat_message("user"):
st.write(prompt)
# RAG検索
query_embed = ollama_embed(prompt)
results = st.session_state.collection.query(
query_embeddings=[query_embed],
n_results=2
)
# {
# "ids":
# "documents": [["doc1", "doc2"]]
# "distances": [[XXX, XXX]]
# }
if results["documents"]:
context_text = "\n".join(results["documents"][0])
rag_prompt = f"""
以下は関連ドキュメントの抜粋です。
{context_text}
この情報を参考に以下の質問に答えてください。
{prompt}
"""
final_user_prompt = rag_prompt
else:
final_user_prompt = prompt
st.session_state.messages.append({"role": "user", "content": final_user_prompt})
if system_prompt.strip():
messages = [{"role": "system", "content": system_prompt}] + st.session_state.messages
else:
messages = st.session_state.messages
# LLMの返答を表示
with st.chat_message("assistant"):
placeholder = st.empty()
stream_response = ""
stream = client.chat.completions.create(
model=model,
messages=messages,
temperature=temperature,
stream=True
)
for chunk in stream:
stream_response += chunk.choices[0].delta.content
placeholder.write(stream_response)
# 会話の履歴を保存
st.session_state.messages.append({"role": "assistant", "content": stream_response})
のコードを
OpenAI のAPIではなく
Ollama のelyza:jp8bを使用するようにコードを変更
openai の依存を削除し、/api/chat(ストリーミング)を requests で直接呼び出し。
モデル既定値を elyza:jp8b に変更。
Ollama のベースURL をサイドバーで指定可能(既定: http://localhost:11434)。埋め込みも同じURLを使用。
温度などは options 経由で送信。
ストリーミングは1行ごとの JSON を読み、message.content を随時描画。
import streamlit as st
import chromadb
from docx import Document
import requests
import json
# -----------------------------
# 共通設定
# -----------------------------
st.set_page_config(page_title="Local LLM Chat")
st.sidebar.title("設定")
ollama_base_url = st.sidebar.text_input("Ollama ベースURL", value="http://localhost:11434")
model = st.sidebar.text_input("モデル名", value="elyza:jp8b") # 既定を elyza:jp8b に
temperature = st.sidebar.slider("temperature", 0.0, 2.0, 0.3, 0.1)
system_prompt = st.sidebar.text_area(
"System Prompt",
"あなたは有能なアシスタントです。日本語で回答してください",
)
# -----------------------------
# ChromaDB の設定
# -----------------------------
DB_DIR = "./chroma_db"
chroma_client = chromadb.PersistentClient(path=DB_DIR)
if "collection" not in st.session_state:
st.session_state.collection = chroma_client.get_or_create_collection(
name="local_docs"
)
# -----------------------------
# Ollama API ユーティリティ
# -----------------------------
def ollama_embed(text: str):
"""Ollama の /api/embeddings を使ってベクトルを取得"""
url = f"{ollama_base_url}/api/embeddings"
r = requests.post(
url,
json={"model": "nomic-embed-text", "prompt": text},
timeout=60,
)
r.raise_for_status()
data = r.json()
return data["embedding"]
def ollama_chat_stream(messages, model_name: str, temperature: float = 0.3):
"""
Ollama の /api/chat ストリームを逐次 yield するジェネレータ。
messages: [{"role":"system|user|assistant", "content":"..."}]
"""
url = f"{ollama_base_url}/api/chat"
payload = {
"model": model_name,
"messages": messages,
"stream": True,
"options": {
"temperature": float(temperature)
}
}
with requests.post(url, json=payload, stream=True) as r:
r.raise_for_status()
for line in r.iter_lines(decode_unicode=True):
if not line:
continue
# 各行は JSON。末尾で {"done":true} が来る
try:
obj = json.loads(line)
except json.JSONDecodeError:
continue
if obj.get("done"):
break
msg = obj.get("message", {})
delta = msg.get("content", "")
if delta:
yield delta
# -----------------------------
# Word 読み込み & チャンク
# -----------------------------
def load_word_document(file):
return "\n".join(para.text for para in Document(file).paragraphs)
def split_text(text):
chunk_size = 200
overlap = 50
chunks = []
start = 0
while start < len(text):
end = start + chunk_size
chunks.append(text[start:end])
start += chunk_size - overlap
return chunks
# -----------------------------
# UI: アップロード → インデックス
# -----------------------------
uploaded_files = st.sidebar.file_uploader(
"Wordファイルをアップロード(.docx)",
type=["docx"],
accept_multiple_files=True
)
if st.sidebar.button("インデックス作成"):
try:
for file in uploaded_files or []:
text = load_word_document(file)
chunks = split_text(text)
for i, chunk in enumerate(chunks):
embed = ollama_embed(chunk)
st.session_state.collection.add(
documents=[chunk],
embeddings=,
ids=[f"{file.name}_{i}"]
)
st.sidebar.success("インデックス作成完了")
except Exception as e:
st.sidebar.error(f"インデックス作成でエラー: {e}")
# -----------------------------
# 会話領域
# -----------------------------
st.title("Local LLM Chat")
if "messages" not in st.session_state:
st.session_state.messages = []
# リセットボタン
if st.sidebar.button("会話をリセット"):
st.session_state.messages = []
# 既存履歴の表示
for m in st.session_state.messages:
with st.chat_message(m["role"]):
st.write(m["content"])
# 入力
prompt = st.chat_input("メッセージを入力")
if prompt:
# ユーザーの入力を表示
with st.chat_message("user"):
st.write(prompt)
# --- RAG: 近傍検索 ---
try:
query_embed = ollama_embed(prompt)
results = st.session_state.collection.query(
query_embeddings=[query_embed],
n_results=2
)
except Exception as e:
results = {"documents": []}
st.warning(f"RAG検索でエラーが発生しました: {e}")
# ドキュメントの結合
if results.get("documents"):
context_text = "\n".join(results["documents"][0])
rag_prompt = f"""
以下は関連ドキュメントの抜粋です。
{context_text}
この情報を参考に以下の質問に答えてください。
{prompt}
"""
final_user_prompt = rag_prompt
else:
final_user_prompt = prompt
# セッション履歴にユーザー発話(RAG統合後)を追加
st.session_state.messages.append({"role": "user", "content": final_user_prompt})
# Ollama へ投げる message 配列を組み立て
chat_messages = []
if system_prompt.strip():
chat_messages.append({"role": "system", "content": system_prompt})
# 既存履歴をそのまま渡す(role は "user"/"assistant")
chat_messages.extend(st.session_state.messages)
# --- ストリーミング応答 ---
with st.chat_message("assistant"):
placeholder = st.empty()
stream_response = ""
try:
for delta in ollama_chat_stream(chat_messages, model, temperature):
stream_response += delta
placeholder.write(stream_response)
except Exception as e:
st.error(f"Ollama応答でエラーが発生しました: {e}")
# 履歴へ保存
st.session_state.messages.append({"role": "assistant", "content": stream_response})