hf_multiLLM / app_sourcecodereview.py
Kanekonkon's picture
Upload folder using huggingface_hub
ed9ec98 verified
import os
import gradio as gr
from pypdf import PdfReader
from sentence_transformers import SentenceTransformer
import chromadb
from chromadb.utils import embedding_functions
import ollama # Ollamaライブラリをインポート
from langchain_text_splitters import RecursiveCharacterTextSplitter
import uuid # For generating unique IDs for chunks
# --- Ollamaクライアントの初期化 ---
client_ollama = ollama.Client()
OLLAMA_MODEL_NAME = "llama3.2"
# OLLAMA_MODEL_NAME = "llama3:8b-instruct-q4_0"
# --- 埋め込みモデルの初期化 ---
# 重複定義を削除し、1回のみ初期化
embedding_model = SentenceTransformer('pkshatech/GLuCoSE-base-ja') # 日本語対応の埋め込みモデル
# --- ChromaDBのカスタム埋め込み関数 ---
# 重複定義を削除し、1回のみ定義
class SBERTEmbeddingFunction(embedding_functions.EmbeddingFunction):
def __init__(self, model):
self.model = model
def __call__(self, texts):
# sentence-transformersモデルはnumpy配列を返すため、tolist()でPythonリストに変換
return self.model.encode(texts).tolist()
sbert_ef = SBERTEmbeddingFunction(embedding_model)
# --- ChromaDBクライアントとコレクションの初期化 ---
# インメモリモードで動作させ、アプリケーション起動時にコレクションをリセットします。
# グローバル変数としてクライアントを保持
client = chromadb.Client()
collection_name = "pdf_documents_collection"
# アプリケーション起動時にコレクションが存在すれば削除し、新しく作成する
# (インメモリDBはセッションごとにリセットされるため、これは初回起動時のみ意味を持つ)
try:
client.delete_collection(name=collection_name)
print(f"既存のChromaDBコレクション '{collection_name}' を削除しました。")
except Exception as e:
# コレクションが存在しない場合はエラーになるので無視。デバッグ用にメッセージは出力。
print(f"ChromaDBコレクション '{collection_name}' の削除に失敗しました (存在しないか、その他のエラー): {e}")
pass
collection = client.get_or_create_collection(name=collection_name, embedding_function=sbert_ef)
print(f"ChromaDBコレクション '{collection_name}' を初期化しました。")
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000, # チャンクの最大文字数
chunk_overlap=150, # チャンク間のオーバーラップ文字数
length_function=len, # 文字数で長さを計算
separators=["\n\n", "\n", " ", ""] # 分割の優先順位
)
# --- ヘルパー関数 ---
def extract_text_from_pdf(pdf_file_path):
"""PDFファイルからテキストを抽出する"""
try:
reader = PdfReader(pdf_file_path)
text = ""
for page in reader.pages:
page_text = page.extract_text()
if page_text: # ページが空でなければ追加
text += page_text + "\n"
if not text.strip(): # 全ページからテキストが抽出できなかった場合
return "PDFからテキストを抽出できませんでした。画像ベースのPDFかもしれません。"
return text
except Exception as e:
return f"PDFの読み込み中にエラーが発生しました: {e}"
def get_ollama_response(query, context, source_code_to_check):
"""Ollamaモデルを使用して質問に回答する"""
messages = [
{"role": "system", "content": "あなたは提供されたコンテキスト(ソースコードチェックリスト)とレビュー対象のソースコードに基づいて、ソースコードをチェックし、その結果を返す有益なアシスタントです。チェックリストの項目ごとにソースコードを評価し、具体的な指摘と改善案を提示してください。コンテキストに情報がない場合は、「提供された情報からは回答できません。」と答えてください。"},
{"role": "user", "content": f"ソースコードチェックリスト:\n{context}\n\nレビュー対象のソースコード:\n```\n{source_code_to_check}\n```\n\n質問: {query}\n\nチェック結果:"}
]
try:
print(f"Ollamaモデル '{OLLAMA_MODEL_NAME}' にリクエストを送信中...")
response = client_ollama.chat(
model=OLLAMA_MODEL_NAME,
messages=messages,
options={
"temperature": 0.5,
"num_predict": 2000 # 回答の最大トークン数
}
)
print(f"Ollamaからの生応答: {response}") # デバッグ用にOllamaの生レスポンスを出力
if 'message' in response and 'content' in response['message']:
return response['message']['content'].strip()
else:
print(f"Ollamaからの応答形式が不正です: {response}")
return "Ollamaモデルからの応答形式が不正です。詳細をコンソールログで確認してください。"
except ollama.ResponseError as e: # Ollama固有のエラーを捕捉
print(f"Ollama APIエラーが発生しました: {e}")
return f"Ollamaモデルの呼び出し中にAPIエラーが発生しました: {e}\nOllamaサーバーが起動しているか、モデル '{OLLAMA_MODEL_NAME}' がインストールされているか確認してください。"
except Exception as e: # その他の予期せぬエラー
print(f"Ollamaモデルの呼び出し中に予期せぬエラーが発生しました: {e}")
return f"Ollamaモデルの呼び出し中に予期せぬエラーが発生しました: {e}"
def upload_pdf_and_process(pdf_files):
"""複数のPDFファイルをアップロードし、テキストを抽出し、ChromaDBに登録する"""
if not pdf_files:
return "PDFファイルがアップロードされていません。", gr.update(interactive=False), gr.update(interactive=False)
processed_files_count = 0
total_chunks_added = 0
all_status_messages = []
for pdf_file in pdf_files:
try:
pdf_path = pdf_file.name
file_name = os.path.basename(pdf_path)
all_status_messages.append(f"PDFファイル '{file_name}' を処理中...")
print(f"Processing PDF: {file_name}")
# 1. PDFからテキストを抽出
raw_text = extract_text_from_pdf(pdf_path)
if "エラー" in raw_text or "抽出できませんでした" in raw_text:
all_status_messages.append(raw_text)
print(f"Error extracting text from {file_name}: {raw_text}")
continue # 次のファイルへ
# 2. テキストをチャンクに分割
chunks = text_splitter.split_text(raw_text)
if not chunks:
all_status_messages.append(f"'{file_name}' から有効なテキストチャンクを抽出できませんでした。")
print(f"No valid chunks extracted from {file_name}.")
continue # 次のファイルへ
# 3. チャンクをChromaDBに登録
documents = chunks
metadatas = [{"source": file_name, "chunk_index": i} for i in range(len(chunks))]
ids = [str(uuid.uuid4()) for _ in range(len(chunks))]
collection.add(
documents=documents,
metadatas=metadatas,
ids=ids
)
processed_files_count += 1
total_chunks_added += len(chunks)
all_status_messages.append(f"PDFファイル '{file_name}' の処理が完了しました。{len(chunks)}個のチャンクがデータベースに登録されました。")
print(f"Finished processing {file_name}. Added {len(chunks)} chunks.")
except Exception as e:
all_status_messages.append(f"PDFファイル '{os.path.basename(pdf_file.name)}' 処理中に予期せぬエラーが発生しました: {e}")
print(f"Unexpected error during processing {os.path.basename(pdf_file.name)}: {e}")
continue # 次のファイルへ
final_status_message = f"{processed_files_count}個のPDFファイルの処理が完了しました。合計{total_chunks_added}個のチャンクがデータベースに登録されました。質問とソースコードを入力してください。\n\n" + "\n".join(all_status_messages)
return final_status_message, gr.update(interactive=True), gr.update(interactive=True)
def answer_question(question, source_code):
"""ChromaDBから関連情報を取得し、Ollamaモデルで質問に回答する"""
if not question and not source_code: # 質問またはソースコードのいずれかがあればOK
return "質問またはレビュー対象のソースコードを入力してください。", ""
if collection.count() == 0:
return "PDFがまだアップロードされていないか、処理されていません。まずPDFをアップロードしてください。", ""
try:
print(f"Searching ChromaDB for question: {question}") # デバッグ出力
results = collection.query(
query_texts=[question],
n_results=8 # 上位8つの関連チャンクを取得
)
context_chunks = results['documents'][0] if results['documents'] else []
if not context_chunks:
print("No relevant context chunks found in ChromaDB.")
return "関連する情報が見つかりませんでした。質問を明確にするか、別のPDFを試してください。", ""
context = "\n\n".join(context_chunks)
print(f"Retrieved context (first 500 chars):\n{context[:500]}...") # デバッグ用にコンテキストの一部を出力
answer = get_ollama_response(question, context, source_code) # 関数名を変更
return answer, context
except Exception as e:
print(f"質問応答中に予期せぬエラーが発生しました: {e}")
return f"質問応答中に予期せぬエラーが発生しました: {e}", ""
# --- Gradio UIの構築 ---
with gr.Blocks() as gradioUI:
gr.Markdown(
f"""
# PDF Q&A with Local LLM (Ollama: {OLLAMA_MODEL_NAME}) and Vector Database
PDFファイルとしてソースコードチェックリストをアップロードし、レビューしたいソースコードを入力してください。
**複数のPDFファイルを同時にアップロードできます。**
ローカルのOllama ({OLLAMA_MODEL_NAME}) を使用しています。
"""
)
with gr.Row():
with gr.Column():
pdf_input = gr.File(label="PDFドキュメントをアップロード", file_types=[".pdf"], file_count="multiple")
upload_status = gr.Textbox(label="ステータス", interactive=False, value="PDFをアップロードしてください。", lines=5)
with gr.Column():
source_code_input = gr.Code(
label="レビュー対象のソースコード (ここにソースコードを貼り付けてください)",
value="",
language="python",
interactive=True,
lines=15
)
question_input = gr.Textbox(label="レビュー指示(例: セキュリティの観点からレビュー)", placeholder="特定の観点からのレビュー指示を入力してください(任意)。", interactive=False)
review_button = gr.Button("レビュー開始")
answer_output = gr.Markdown(label="レビュー結果")
retrieved_context_output = gr.Textbox(label="取得されたチェックリスト項目", interactive=False, lines=10)
pdf_input.upload(
upload_pdf_and_process,
inputs=[pdf_input],
outputs=[upload_status, question_input, source_code_input]
)
review_button.click(
answer_question,
inputs=[question_input, source_code_input],
outputs=[answer_output, retrieved_context_output]
)
gradioUI.launch(server_name="0.0.0.0", server_port=7860)