hf_multiLLM / app_multillm_OK.py
Kanekonkon's picture
Upload folder using huggingface_hub
3bdf4cd 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
from dotenv import load_dotenv
# from pdfminer.high_level import extract_text as pdfminer_extract_text
# LLMクライアントのインポート
from openai import OpenAI
import anthropic
import google.generativeai as genai
import sys
print(f"Python executable: {sys.executable}")
print(f"Python version: {sys.version}")
print(f"Python version info: {sys.version_info}")
print(f"--------------------------")
# .envファイルから環境変数を読み込む
load_dotenv()
# --- APIキーの取得 ---
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY")
GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
# --- Ollamaクライアントの初期化 ---
client_ollama = ollama.Client()
OLLAMA_MODEL_NAME = "llama3.2"
# OLLAMA_MODEL_NAME = "llama3:8b-instruct-q4_0"
client_openai = None
OPENAI_MODEL_NAME = "gpt-4o-mini"
if OPENAI_API_KEY:
try:
client_openai = OpenAI(api_key=OPENAI_API_KEY)
print(f"OpenAIクライアントを初期化しました (モデル: {OPENAI_MODEL_NAME})。")
except Exception as e:
print(f"OpenAIクライアントの初期化に失敗しました: {e}")
client_openai = None
else:
print("OPENAI_API_KEYが設定されていません。OpenAIモデルは利用できません。")
client_anthropic = None
ANTHROPIC_MODEL_NAME = "claude-3-haiku-20240307"
if ANTHROPIC_API_KEY:
try:
client_anthropic = anthropic.Anthropic(api_key=ANTHROPIC_API_KEY)
print(f"Anthropicクライアントを初期化しました (モデル: {ANTHROPIC_MODEL_NAME})。")
except Exception as e:
print(f"Anthropicクライアントの初期化に失敗しました: {e}")
client_anthropic = None
else:
print("ANTHROPIC_API_KEYが設定されていません。Anthropicモデルは利用できません。")
client_gemini = None
GOOGLE_MODEL_NAME = "gemini-2.5-flash"
if GOOGLE_API_KEY:
try:
genai.configure(api_key=GOOGLE_API_KEY)
client_gemini = genai.GenerativeModel(GOOGLE_MODEL_NAME)
print(f"Google Geminiクライアントを初期化しました (モデル: {GOOGLE_MODEL_NAME})。")
except Exception as e:
print(f"Google Geminiクライアントの初期化に失敗しました: {e}")
client_gemini = None
else:
print("GOOGLE_API_KEYが設定されていません。Google Geminiモデルは利用できません。")
# --- 埋め込みモデルの初期化 ---
# 重複定義を削除し、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ファイルからテキストを抽出する"""
print(f"Attempting to extract text from: {pdf_file_path}")
try:
reader = PdfReader(pdf_file_path)
text = ""
if not reader.pages:
print(f" PDF '{os.path.basename(pdf_file_path)}' contains no pages.")
return "ERROR: PDFにページが含まれていません。" # プレフィックスを追加
for i, page in enumerate(reader.pages):
page_text = page.extract_text()
if page_text:
text += page_text + "\n"
# print(f" Page {i+1} extracted text (first 100 chars): {page_text[:100].replace('\n', ' ')}...")
cleaned_page_text = page_text[:100].replace('\n', ' ')
print(f" Page {i+1} extracted text (first 100 chars): {cleaned_page_text}...")
else:
print(f" Page {i+1} extracted no text.")
if not text.strip():
print(" No text extracted from any page.")
return "ERROR: PDFからテキストを抽出できませんでした。画像ベースのPDFかもしれません。" # プレフィックスを追加
print(f" Total text extracted (length: {len(text)}).")
return text
except Exception as e:
print(f" Error during PDF reading: {e}")
return f"ERROR: PDFの読み込み中にエラーが発生しました: {e}" # プレフィックスを追加
def get_llm_response(selected_llm, query, context, source_code_to_check):
"""選択されたLLMを使用して質問に回答する"""
system_prompt = "あなたは提供されたコンテキスト(ソースコードチェックリスト)とレビュー対象のソースコードに基づいて、ソースコードをチェックし、その結果を返す有益なアシスタントです。チェックリストの項目ごとにソースコードを評価し、具体的な指摘と改善案を提示してください。コンテキストに情報がない場合は、「提供された情報からは回答できません。」と答えてください。"
user_content = f"ソースコードチェックリスト:\n{context}\n\nレビュー対象のソースコード:\n```\n{source_code_to_check}\n```\n\nレビュー指示: {query}\n\nチェック結果:"
try:
if selected_llm == "Ollama":
if not client_ollama:
return "Ollamaクライアントが初期化されていません。"
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_content}
]
print(f"Ollamaモデル '{OLLAMA_MODEL_NAME}' にリクエストを送信中...")
response = client_ollama.chat(
model=OLLAMA_MODEL_NAME,
messages=messages,
options={
"temperature": 0.7,
"num_predict": 2000
}
)
if 'message' in response and 'content' in response['message']:
return response['message']['content'].strip()
else:
return f"Ollamaからの応答形式が不正です: {response}"
elif selected_llm == "GPT":
if not client_openai:
return "OpenAI APIキーが設定されていないため、GPTモデルは利用できません。"
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_content}
]
print(f"GPTモデル '{OPENAI_MODEL_NAME}' にリクエストを送信中...")
response = client_openai.chat.completions.create(
model=OPENAI_MODEL_NAME,
messages=messages,
temperature=0.5,
max_tokens=2000
)
return response.choices[0].message.content.strip()
elif selected_llm == "Anthropic":
if not client_anthropic:
return "Anthropic APIキーが設定されていないため、Anthropicモデルは利用できません。"
messages = [
{"role": "user", "content": user_content}
]
print(f"Anthropicモデル '{ANTHROPIC_MODEL_NAME}' にリクエストを送信中...")
response = client_anthropic.messages.create(
model=ANTHROPIC_MODEL_NAME,
max_tokens=2000,
temperature=0.5,
system=system_prompt, # Anthropicはsystemプロンプトを直接引数で渡す
messages=messages
)
return response.content[0].text.strip()
elif selected_llm == "Google Gemini":
if not client_gemini:
return "Google APIキーが設定されていないため、Geminiモデルは利用できません。"
# Geminiのsystem instructionはまだベータ版で、messagesと併用できない場合があるため、
# system_promptをuser_contentの先頭に結合する形式にする。
# --- システムプロンプトの調整 (後述の2.プロンプト調整も参照) ---
system_prompt = "あなたは提供されたコンテキスト(ソースコードチェックリスト)とレビュー対象のソースコードに基づいて、ソースコードのレビューを行うアシスタントです。チェックリストの項目ごとにソースコードを評価し、潜在的な問題点や改善の機会を提案してください。コンテキストに情報がない場合は、「提供された情報からは回答できません。」と答えてください。"
# --- ユーザープロンプトの調整 (後述の2.プロンプト調整も参照) ---
user_content = f"ソースコードチェックリスト:\n{context}\n\nレビュー対象のソースコード:\n```\n{source_code_to_check}\n```\n\nレビュー指示: {query}\n\nチェック結果:"
full_user_content = f"{system_prompt}\n\n{user_content}"
messages = [
{"role": "user", "parts": [full_user_content]}
]
print(f"Google Geminiモデル '{GOOGLE_MODEL_NAME}' にリクエストを送信中...")
try:
response = client_gemini.generate_content(
messages,
generation_config=genai.types.GenerationConfig(
temperature=0.5, # まずは0.5で試す。必要なら0.7などに上げる
max_output_tokens=2000
)
)
# --- エラーハンドリングの強化 ---
# response.text を呼び出す前に、応答の候補と終了理由を確認
if response.candidates:
candidate = response.candidates[0]
# finish_reason が SAFETY (genai.types.HarmCategory.SAFETY) の場合、安全ポリシーによりブロックされた可能性が高い
if candidate.finish_reason == genai.types.HarmCategory.SAFETY:
safety_ratings = candidate.safety_ratings
safety_details = ", ".join([f"{sr.category.name}: {sr.probability.name}" for sr in safety_ratings])
print(f"Gemini response blocked due to safety policy. Details: {safety_details}")
return f"Google Geminiからの応答が安全ポリシーによりブロックされました。詳細: {safety_details}"
# 正常なコンテンツがあるか確認
elif candidate.content and candidate.content.parts:
return response.text.strip()
else:
# コンテンツがないが、finish_reasonがSAFETY以外の場合
print(f"Gemini response has no content parts. Finish reason: {candidate.finish_reason.name}")
return f"Google Geminiからの応答にコンテンツが含まれていません。終了理由: {candidate.finish_reason.name}"
else:
# 候補自体がない場合
print(f"Gemini response has no candidates. Raw response: {response}")
return f"Google Geminiからの応答に候補がありませんでした。生の応答: {response}"
except Exception as e:
# generate_content 自体でエラーが発生した場合
print(f"Google Gemini API呼び出し中にエラーが発生しました: {e}")
return f"Google Gemini API呼び出し中にエラーが発生しました: {e}"
else:
return "無効なLLMが選択されました。"
except Exception as e:
print(f"LLM ({selected_llm}) の呼び出し中にエラーが発生しました: {e}")
return f"LLM ({selected_llm}) の呼び出し中にエラーが発生しました: {e}"
def upload_pdf_and_process(pdf_files):
"""複数のPDFファイルをアップロードし、テキストを抽出し、ChromaDBに登録する"""
if not pdf_files:
print("No PDF files uploaded.")
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} (Temporary Path: {pdf_path})")
# 1. PDFからテキストを抽出
raw_text = extract_text_from_pdf(pdf_path)
# --- デバッグ用追加コード (前回のデバッグで追加したものは残しておくと良いでしょう) ---
print(f"DEBUG: raw_text received from extract_text_from_pdf (length: {len(raw_text)})")
# print(f"DEBUG: raw_text starts with: '{raw_text[:100].replace(newline_char, ' ')}'")
print(f"DEBUG: 'エラー' in raw_text: {'エラー' in raw_text}")
print(f"DEBUG: '抽出できませんでした' in raw_text: {'抽出できませんでした' in raw_text}")
print(f"DEBUG: 'PDFにページが含まれていません' in raw_text: {'PDFにページが含まれていません' in raw_text}")
# --- デバッグ情報ここまで ---
# エラープレフィックスでチェックするように変更
if raw_text.startswith("ERROR:"): # ここを変更
all_status_messages.append(raw_text)
print(f"Error during text extraction from {file_name}: {raw_text}") # ログメッセージも変更
continue # 次のファイルへ
# --- デバッグ用追加コード ---
print(f"\n--- Raw text extracted from {file_name} (length: {len(raw_text)}, first 500 chars) ---")
print(raw_text[:500])
print(f"--- End of raw text from {file_name} ---\n")
# 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, selected_llm):
"""ChromaDBから関連情報を取得し、選択されたLLMで質問に回答する"""
if not question and not source_code:
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
)
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_llm_response(selected_llm, 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ファイルを同時にアップロードできます。**
利用するLLMを選択してください。
"""
)
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():
# LLM選択コンポーネント
llm_options = ["Ollama"]
if client_openai:
llm_options.append("GPT")
if client_anthropic:
llm_options.append("Anthropic")
if client_gemini:
llm_options.append("Google Gemini")
llm_choice = gr.Radio(
llm_options,
label="使用するLLMを選択",
value=llm_options[0] if llm_options else None, # 利用可能な最初のLLMをデフォルトにする
interactive=True
)
source_code_input = gr.Code(
label="レビュー対象のソースコード (ここにソースコードを貼り付けてください)",
value="",
language="python",
interactive=False, # PDFアップロード後に有効化
lines=15
)
question_input = gr.Textbox(label="レビュー指示(例: セキュリティの観点からレビュー)", placeholder="特定の観点からのレビュー指示を入力してください(任意)。", interactive=False) # PDFアップロード後に有効化
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, llm_choice],
outputs=[answer_output, retrieved_context_output]
)
# gradioUI.launch(server_name="localhost", server_port=7860)
gradioUI.launch(server_name="0.0.0.0", server_port=7860)