Spaces:
Sleeping
Sleeping
| 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) | |