Kanekonkon commited on
Commit
3bdf4cd
·
verified ·
1 Parent(s): 41499f3

Upload folder using huggingface_hub

Browse files
Files changed (3) hide show
  1. app.py +192 -19
  2. app_multillm_OK.py +422 -0
  3. requirements.txt +5 -1
app.py CHANGED
@@ -10,6 +10,12 @@ import uuid # For generating unique IDs for chunks
10
  from dotenv import load_dotenv
11
  # from pdfminer.high_level import extract_text as pdfminer_extract_text
12
 
 
 
 
 
 
 
13
 
14
  # LLMクライアントのインポート
15
  from openai import OpenAI
@@ -18,7 +24,7 @@ import google.generativeai as genai
18
 
19
  import sys
20
 
21
- print(f"---thon executable: {sys.executable}")
22
  print(f"Python version: {sys.version}")
23
  print(f"Python version info: {sys.version_info}")
24
  print(f"--------------------------")
@@ -75,6 +81,15 @@ if GOOGLE_API_KEY:
75
  else:
76
  print("GOOGLE_API_KEYが設定されていません。Google Geminiモデルは利用できません。")
77
 
 
 
 
 
 
 
 
 
 
78
  # --- 埋め込みモデルの初期化 ---
79
  # 重複定義を削除し、1回のみ初期化
80
  embedding_model = SentenceTransformer('pkshatech/GLuCoSE-base-ja') # 日本語対応の埋め込みモデル
@@ -92,7 +107,8 @@ sbert_ef = SBERTEmbeddingFunction(embedding_model)
92
 
93
  # --- ChromaDBクライアントとコレクションの初期化 ---
94
  # インメモリモードで動作させ、アプリケーション起動時にコレクションをリセットします。
95
- # グローバル変数としてクライアントを保持
 
96
  client = chromadb.Client()
97
  collection_name = "pdf_documents_collection"
98
 
@@ -147,17 +163,6 @@ def extract_text_from_pdf(pdf_file_path):
147
  print(f" Error during PDF reading: {e}")
148
  return f"ERROR: PDFの読み込み中にエラーが発生しました: {e}" # プレフィックスを追加
149
 
150
- # def extract_text_from_pdf(pdf_file_path):
151
- # """PDFファイルからテキストを抽出する (pdfminer.sixを使用)"""
152
- # try:
153
- # # pdfminer.six の extract_text 関数を使用
154
- # text = pdfminer_extract_text(pdf_file_path)
155
- # if not text.strip():
156
- # return "PDFからテキストを抽出できませんでした。画像ベースのPDFかもしれません。"
157
- # return text
158
- # except Exception as e:
159
- # return f"PDFの読み込み中にエラーが発生しました: {e}"
160
-
161
  def get_llm_response(selected_llm, query, context, source_code_to_check):
162
  """選択されたLLMを使用して質問に回答する"""
163
  system_prompt = "あなたは提供されたコンテキスト(ソースコードチェックリスト)とレビュー対象のソースコードに基づいて、ソースコードをチェックし、その結果を返す有益なアシスタントです。チェックリストの項目ごとにソースコードを評価し、具体的な指摘と改善案を提示してください。コンテキストに情報がない場合は、「提供された情報からは回答できません。」と答えてください。"
@@ -276,7 +281,7 @@ def get_llm_response(selected_llm, query, context, source_code_to_check):
276
  return f"LLM ({selected_llm}) の呼び出し中にエラーが発生しました: {e}"
277
 
278
  def upload_pdf_and_process(pdf_files):
279
- """複数のPDFファイルをアップロードし、テキストを抽出し、ChromaDBに登録する"""
280
  if not pdf_files:
281
  print("No PDF files uploaded.")
282
  return "PDFファイルがアップロードされていません。", gr.update(interactive=False), gr.update(interactive=False)
@@ -297,10 +302,7 @@ def upload_pdf_and_process(pdf_files):
297
 
298
  # --- デバッグ用追加コード (前回のデバッグで追加したものは残しておくと良いでしょう) ---
299
  print(f"DEBUG: raw_text received from extract_text_from_pdf (length: {len(raw_text)})")
300
- # print(f"DEBUG: raw_text starts with: '{raw_text[:100].replace(newline_char, ' ')}'")
301
  print(f"DEBUG: 'エラー' in raw_text: {'エラー' in raw_text}")
302
- print(f"DEBUG: '抽出できませんでした' in raw_text: {'抽出できませんでした' in raw_text}")
303
- print(f"DEBUG: 'PDFにページが含まれていません' in raw_text: {'PDFにページが含まれていません' in raw_text}")
304
  # --- デバッグ情報ここまで ---
305
 
306
  # エラープレフィックスでチェックするように変更
@@ -343,6 +345,7 @@ def upload_pdf_and_process(pdf_files):
343
  final_status_message = f"{processed_files_count}個のPDFファイルの処理が完了しました。合計{total_chunks_added}個のチャンクがデータベースに登録されました。質問とソースコードを入力してください。\n\n" + "\n".join(all_status_messages)
344
  return final_status_message, gr.update(interactive=True), gr.update(interactive=True)
345
 
 
346
  def answer_question(question, source_code, selected_llm):
347
  """ChromaDBから関連情報を取得し、選択されたLLMで質問に回答する"""
348
  if not question and not source_code:
@@ -371,6 +374,156 @@ def answer_question(question, source_code, selected_llm):
371
  print(f"質問応答中に予期せぬエラーが発生しました: {e}")
372
  return f"質問応答中に予期せぬエラーが発生しました: {e}", ""
373
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
374
  # --- Gradio UIの構築 ---
375
  with gr.Blocks() as gradioUI:
376
  gr.Markdown(
@@ -429,5 +582,25 @@ with gr.Blocks() as gradioUI:
429
  outputs=[answer_output, retrieved_context_output]
430
  )
431
 
432
- # gradioUI.launch(server_name="localhost", server_port=7860)
433
- gradioUI.launch(server_name="0.0.0.0", server_port=7860)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  from dotenv import load_dotenv
11
  # from pdfminer.high_level import extract_text as pdfminer_extract_text
12
 
13
+ # FastAPI関連のインポート
14
+ from fastapi import FastAPI, UploadFile, File, HTTPException
15
+ from typing import List, Optional
16
+ from pydantic import BaseModel
17
+ import shutil
18
+ import tempfile
19
 
20
  # LLMクライアントのインポート
21
  from openai import OpenAI
 
24
 
25
  import sys
26
 
27
+ print(f"---Python executable: {sys.executable}")
28
  print(f"Python version: {sys.version}")
29
  print(f"Python version info: {sys.version_info}")
30
  print(f"--------------------------")
 
81
  else:
82
  print("GOOGLE_API_KEYが設定されていません。Google Geminiモデルは利用できません。")
83
 
84
+ # --- LLM選択肢のリスト (APIとGradioで共有) ---
85
+ llm_options = ["Ollama"]
86
+ if client_openai:
87
+ llm_options.append("GPT")
88
+ if client_anthropic:
89
+ llm_options.append("Anthropic")
90
+ if client_gemini:
91
+ llm_options.append("Google Gemini")
92
+
93
  # --- 埋め込みモデルの初期化 ---
94
  # 重複定義を削除し、1回のみ初期化
95
  embedding_model = SentenceTransformer('pkshatech/GLuCoSE-base-ja') # 日本語対応の埋め込みモデル
 
107
 
108
  # --- ChromaDBクライアントとコレクションの初期化 ---
109
  # インメモリモードで動作させ、アプリケーション起動時にコレクションをリセットします。
110
+ # APIとして利用する場合、永続化されたChromaDB (例: chromadb.PersistentClient(path="./chroma_db"))
111
+ # を使用する検討必要
112
  client = chromadb.Client()
113
  collection_name = "pdf_documents_collection"
114
 
 
163
  print(f" Error during PDF reading: {e}")
164
  return f"ERROR: PDFの読み込み中にエラーが発生しました: {e}" # プレフィックスを追加
165
 
 
 
 
 
 
 
 
 
 
 
 
166
  def get_llm_response(selected_llm, query, context, source_code_to_check):
167
  """選択されたLLMを使用して質問に回答する"""
168
  system_prompt = "あなたは提供されたコンテキスト(ソースコードチェックリスト)とレビュー対象のソースコードに基づいて、ソースコードをチェックし、その結果を返す有益なアシスタントです。チェックリストの項目ごとにソースコードを評価し、具体的な指摘と改善案を提示してください。コンテキストに情報がない場合は、「提供された情報からは回答できません。」と答えてください。"
 
281
  return f"LLM ({selected_llm}) の呼び出し中にエラーが発生しました: {e}"
282
 
283
  def upload_pdf_and_process(pdf_files):
284
+ """複数のPDFファイルをアップロードし、テキストを抽出し、ChromaDBに登録する (Gradio用)"""
285
  if not pdf_files:
286
  print("No PDF files uploaded.")
287
  return "PDFファイルがアップロードされていません。", gr.update(interactive=False), gr.update(interactive=False)
 
302
 
303
  # --- デバッグ用追加コード (前回のデバッグで追加したものは残しておくと良いでしょう) ---
304
  print(f"DEBUG: raw_text received from extract_text_from_pdf (length: {len(raw_text)})")
 
305
  print(f"DEBUG: 'エラー' in raw_text: {'エラー' in raw_text}")
 
 
306
  # --- デバッグ情報ここまで ---
307
 
308
  # エラープレフィックスでチェックするように変更
 
345
  final_status_message = f"{processed_files_count}個のPDFファイルの処理が完了しました。合計{total_chunks_added}個のチャンクがデータベースに登録されました。質問とソースコードを入力してください。\n\n" + "\n".join(all_status_messages)
346
  return final_status_message, gr.update(interactive=True), gr.update(interactive=True)
347
 
348
+ # --- Gradio UI用の質問応答関数 ---
349
  def answer_question(question, source_code, selected_llm):
350
  """ChromaDBから関連情報を取得し、選択されたLLMで質問に回答する"""
351
  if not question and not source_code:
 
374
  print(f"質問応答中に予期せぬエラーが発生しました: {e}")
375
  return f"質問応答中に予期せぬエラーが発生しました: {e}", ""
376
 
377
+
378
+ # --- FastAPI用のPydanticモデル ---
379
+ class PDFUploadResponse(BaseModel):
380
+ status: str
381
+ processed_files_count: int
382
+ total_chunks_added: int
383
+ details: List[str]
384
+
385
+ class CodeReviewRequest(BaseModel):
386
+ question: Optional[str] = ""
387
+ source_code: str
388
+ selected_llm: str
389
+
390
+ class CodeReviewResponse(BaseModel):
391
+ review_result: str
392
+ retrieved_context: str
393
+
394
+ # --- FastAPIアプリケーションの初期化 ---
395
+ app = FastAPI(
396
+ title="Code Review API with RAG",
397
+ description="Upload PDF checklists and get AI-powered code reviews using various LLMs.",
398
+ version="1.0.0"
399
+ )
400
+
401
+ # --- FastAPIエンドポイント ---
402
+
403
+ # --- FastAPIエンドポイント ---
404
+
405
+ @app.post("/api/upload_pdf", response_model=PDFUploadResponse, summary="Upload PDF documents for RAG context")
406
+ async def upload_pdf_for_api(pdf_files: List[UploadFile] = File(..., description="List of PDF files to upload")):
407
+ """
408
+ Uploads one or more PDF files. The text content will be extracted,
409
+ chunked, and stored in the vector database to be used as context
410
+ for code reviews.
411
+ """
412
+ if not pdf_files:
413
+ raise HTTPException(status_code=400, detail="No PDF files uploaded.")
414
+
415
+ processed_files_count = 0
416
+ total_chunks_added = 0
417
+ all_status_messages = []
418
+
419
+ for pdf_file in pdf_files:
420
+ # 一時ファイルを作成し、アップロードされたファイルを保存
421
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as tmp_file:
422
+ shutil.copyfileobj(pdf_file.file, tmp_file)
423
+ tmp_file_path = tmp_file.name
424
+
425
+ try:
426
+ file_name = pdf_file.filename if pdf_file.filename else "unknown_file.pdf"
427
+ all_status_messages.append(f"PDFファイル '{file_name}' を処理中...")
428
+ print(f"Processing PDF: {file_name} (Temporary Path: {tmp_file_path})")
429
+
430
+ raw_text = extract_text_from_pdf(tmp_file_path)
431
+
432
+ if raw_text.startswith("ERROR:"):
433
+ all_status_messages.append(raw_text)
434
+ print(f"Error during text extraction from {file_name}: {raw_text}")
435
+ continue
436
+
437
+ chunks = text_splitter.split_text(raw_text)
438
+ if not chunks:
439
+ all_status_messages.append(f"'{file_name}' から有効なテキストチャンクを抽出できませんでした。")
440
+ print(f"No valid chunks extracted from {file_name}.")
441
+ continue
442
+
443
+ documents = chunks
444
+ metadatas = [{"source": file_name, "chunk_index": i} for i in range(len(chunks))]
445
+ ids = [str(uuid.uuid4()) for _ in range(len(chunks))]
446
+ collection.add(
447
+ documents=documents,
448
+ metadatas=metadatas,
449
+ ids=ids
450
+ )
451
+ processed_files_count += 1
452
+ total_chunks_added += len(chunks)
453
+ all_status_messages.append(f"PDFファイル '{file_name}' の処理が完了しました。{len(chunks)}個のチャンクがデータベースに登録されました。")
454
+ print(f"Finished processing {file_name}. Added {len(chunks)} chunks.")
455
+
456
+ except Exception as e:
457
+ all_status_messages.append(f"PDFファイル '{file_name}' 処理中に予期せぬエラーが発生しました: {e}")
458
+ print(f"Unexpected error during processing {file_name}: {e}")
459
+ finally:
460
+ # 一時ファイルを削除
461
+ os.unlink(tmp_file_path)
462
+
463
+ final_status_message = f"{processed_files_count}個のPDFファイルの処理が完了しました。合計{total_chunks_added}個のチャンクがデータベースに登録されました。"
464
+ return PDFUploadResponse(
465
+ status=final_status_message,
466
+ processed_files_count=processed_files_count,
467
+ total_chunks_added=total_chunks_added,
468
+ details=all_status_messages
469
+ )
470
+
471
+ @app.post("/api/review_code", response_model=CodeReviewResponse, summary="Get an AI-powered code review")
472
+ async def review_code_for_api(request: CodeReviewRequest):
473
+ """
474
+ Performs an AI-powered code review based on the uploaded PDF checklists
475
+ and the provided source code and review instructions.
476
+ """
477
+ question = request.question if request.question else "一般��なコードレビューを実施してください。"
478
+ source_code = request.source_code
479
+ selected_llm = request.selected_llm
480
+
481
+ if not source_code:
482
+ raise HTTPException(status_code=400, detail="レビュー対象のソースコードを入力してください。")
483
+
484
+ if selected_llm not in llm_options:
485
+ raise HTTPException(status_code=400, detail=f"無効なLLMが選択されました: {selected_llm}。利用可能なLLM: {', '.join(llm_options)}")
486
+
487
+ if collection.count() == 0:
488
+ # PDFがアップロードされていない場合でも、LLMによっては一般的なレビューが可能だが、
489
+ # RAGの意図を考えるとエラーとするのが適切。
490
+ # ただし、ユーザーが「コンテキストなしでレビュー」を意図するなら、このエラーは不要。
491
+ # 今回はRAGが前提なのでエラーとする。
492
+ raise HTTPException(status_code=400, detail="PDFがまだアップロードされていないか、処理されていません。まずPDFをアップロードしてコンテキストを登録してください。")
493
+
494
+ try:
495
+ print(f"Searching ChromaDB for question: {question}")
496
+ results = collection.query(
497
+ query_texts=[question],
498
+ n_results=8
499
+ )
500
+ context_chunks = results['documents'][0] if results['documents'] else []
501
+ if not context_chunks:
502
+ print("No relevant context chunks found in ChromaDB.")
503
+ context = "提供されたコンテキストはありません。" # コンテキストが見つからなくても、LLMに渡す
504
+ else:
505
+ context = "\n\n".join(context_chunks)
506
+ print(f"Retrieved context (first 500 chars):\n{context[:500]}...")
507
+
508
+ answer = get_llm_response(selected_llm, question, context, source_code)
509
+
510
+ # get_llm_responseからのエラー文字列をHTTPExceptionに変換
511
+ if answer.startswith("LLM (") and "の呼び出し中にエラーが発生しました" in answer:
512
+ raise HTTPException(status_code=500, detail=answer)
513
+ if "APIキーが設定されていないため" in answer:
514
+ raise HTTPException(status_code=500, detail=answer)
515
+ if "からの応答形式が不正です" in answer:
516
+ raise HTTPException(status_code=500, detail=answer)
517
+ if "からの応答が安全ポリシーによりブロックされました" in answer:
518
+ raise HTTPException(status_code=403, detail=answer) # 403 Forbidden for safety issues
519
+
520
+ return CodeReviewResponse(review_result=answer, retrieved_context=context)
521
+ except HTTPException as e:
522
+ raise e # FastAPIのHTTPExceptionはそのまま再スロー
523
+ except Exception as e:
524
+ print(f"APIコードレビュー中に予期せぬエラーが発生しました: {e}")
525
+ raise HTTPException(stat
526
+
527
  # --- Gradio UIの構築 ---
528
  with gr.Blocks() as gradioUI:
529
  gr.Markdown(
 
582
  outputs=[answer_output, retrieved_context_output]
583
  )
584
 
585
+ # --- GradioアプリケーションをFastAPIにマウント ---
586
+ # Gradio UIは /gradio パスでアクセス可能になります。
587
+ app = gr.mount_gradio_app(app, gradioUI, path="/gradio")
588
+
589
+ # --- FastAPIのルートエンドポイント (Gradioへの誘導) ---
590
+ @app.get("/", summary="Root endpoint")
591
+ async def read_root():
592
+ return {
593
+ "message": "Welcome to the Code Review API! Access the Gradio UI at /gradio.",
594
+ "api_docs": "You can find the API documentation at /docs or /redoc.",
595
+ "api_endpoints": {
596
+ "upload_pdf": "/api/upload_pdf (POST)",
597
+ "review_code": "/api/review_code (POST)"
598
+ }
599
+ }
600
+
601
+ # --- アプリケーションの起動 ---
602
+ if __name__ == "__main__":
603
+ import uvicorn
604
+ # 開発中は reload=True を使うとコード変更時に自動で再起動します。
605
+ # uvicorn.run(app, host="0.0.0.0", port=7860, reload=True)
606
+ uvicorn.run(app, host="0.0.0.0", port=7860)
app_multillm_OK.py ADDED
@@ -0,0 +1,422 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import gradio as gr
3
+ from pypdf import PdfReader
4
+ from sentence_transformers import SentenceTransformer
5
+ import chromadb
6
+ from chromadb.utils import embedding_functions
7
+ import ollama # Ollamaライブラリをインポート
8
+ from langchain_text_splitters import RecursiveCharacterTextSplitter
9
+ import uuid # For generating unique IDs for chunks
10
+ from dotenv import load_dotenv
11
+ # from pdfminer.high_level import extract_text as pdfminer_extract_text
12
+
13
+
14
+ # LLMクライアントのインポート
15
+ from openai import OpenAI
16
+ import anthropic
17
+ import google.generativeai as genai
18
+
19
+ import sys
20
+
21
+ print(f"Python executable: {sys.executable}")
22
+ print(f"Python version: {sys.version}")
23
+ print(f"Python version info: {sys.version_info}")
24
+ print(f"--------------------------")
25
+
26
+ # .envファイルから環境変数を読み込む
27
+ load_dotenv()
28
+
29
+ # --- APIキーの取得 ---
30
+ OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
31
+ ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY")
32
+ GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
33
+
34
+ # --- Ollamaクライアントの初期化 ---
35
+ client_ollama = ollama.Client()
36
+
37
+ OLLAMA_MODEL_NAME = "llama3.2"
38
+ # OLLAMA_MODEL_NAME = "llama3:8b-instruct-q4_0"
39
+
40
+
41
+ client_openai = None
42
+ OPENAI_MODEL_NAME = "gpt-4o-mini"
43
+ if OPENAI_API_KEY:
44
+ try:
45
+ client_openai = OpenAI(api_key=OPENAI_API_KEY)
46
+ print(f"OpenAIクライアントを初期化しました (モデル: {OPENAI_MODEL_NAME})。")
47
+ except Exception as e:
48
+ print(f"OpenAIクライアントの初期化に失敗しました: {e}")
49
+ client_openai = None
50
+ else:
51
+ print("OPENAI_API_KEYが設定されていません。OpenAIモデルは利用できません。")
52
+
53
+ client_anthropic = None
54
+ ANTHROPIC_MODEL_NAME = "claude-3-haiku-20240307"
55
+ if ANTHROPIC_API_KEY:
56
+ try:
57
+ client_anthropic = anthropic.Anthropic(api_key=ANTHROPIC_API_KEY)
58
+ print(f"Anthropicクライアントを初期化しました (モデル: {ANTHROPIC_MODEL_NAME})。")
59
+ except Exception as e:
60
+ print(f"Anthropicクライアントの初期化に失敗しました: {e}")
61
+ client_anthropic = None
62
+ else:
63
+ print("ANTHROPIC_API_KEYが設定されていません。Anthropicモデルは利用できません。")
64
+
65
+ client_gemini = None
66
+ GOOGLE_MODEL_NAME = "gemini-2.5-flash"
67
+ if GOOGLE_API_KEY:
68
+ try:
69
+ genai.configure(api_key=GOOGLE_API_KEY)
70
+ client_gemini = genai.GenerativeModel(GOOGLE_MODEL_NAME)
71
+ print(f"Google Geminiクライアントを初期化しました (モデル: {GOOGLE_MODEL_NAME})。")
72
+ except Exception as e:
73
+ print(f"Google Geminiクライアントの初期化に失敗しました: {e}")
74
+ client_gemini = None
75
+ else:
76
+ print("GOOGLE_API_KEYが設定されていません。Google Geminiモデルは利用できません。")
77
+
78
+ # --- 埋め込みモデルの初期化 ---
79
+ # 重複定義を削除し、1回のみ初期化
80
+ embedding_model = SentenceTransformer('pkshatech/GLuCoSE-base-ja') # 日本語対応の埋め込みモデル
81
+
82
+ # --- ChromaDBのカスタム埋め込み関数 ---
83
+ # 重複定義を削除し、1回のみ定義
84
+ class SBERTEmbeddingFunction(embedding_functions.EmbeddingFunction):
85
+ def __init__(self, model):
86
+ self.model = model
87
+ def __call__(self, texts):
88
+ # sentence-transformersモデルはnumpy配列を返すため、tolist()でPythonリストに変換
89
+ return self.model.encode(texts).tolist()
90
+
91
+ sbert_ef = SBERTEmbeddingFunction(embedding_model)
92
+
93
+ # --- ChromaDBクライアントとコレクションの初期化 ---
94
+ # インメモリモードで動作させ、アプリケーション起動時にコレクションをリセットします。
95
+ # グローバル変数としてクライアントを保持
96
+ client = chromadb.Client()
97
+ collection_name = "pdf_documents_collection"
98
+
99
+ # アプリケーション起動時にコレクションが存在すれば削除し、新しく作成する
100
+ # (インメモリDBはセッションごとにリセットされるため、これは初回起動時のみ意味を持つ)
101
+ try:
102
+ client.delete_collection(name=collection_name)
103
+ print(f"既存のChromaDBコレクション '{collection_name}' を削除しました。")
104
+ except Exception as e:
105
+ # コレクションが存在しない場合はエラーになるので無視。デバッグ用にメッセージは出力。
106
+ print(f"ChromaDBコレクション '{collection_name}' の削除に失敗しました (存在しないか、その他のエラー): {e}")
107
+ pass
108
+
109
+ collection = client.get_or_create_collection(name=collection_name, embedding_function=sbert_ef)
110
+ print(f"ChromaDBコレクション '{collection_name}' を初期化しました。")
111
+
112
+ text_splitter = RecursiveCharacterTextSplitter(
113
+ chunk_size=1000, # チャンクの最大文字数
114
+ chunk_overlap=150, # チャンク間のオーバーラップ文字数
115
+ length_function=len, # 文字数で長さを計算
116
+ separators=["\n\n", "\n", " ", ""] # 分割の優先順位
117
+ )
118
+
119
+ # --- ヘルパー関数 ---
120
+ def extract_text_from_pdf(pdf_file_path):
121
+ """PDFファイルからテキストを抽出する"""
122
+ print(f"Attempting to extract text from: {pdf_file_path}")
123
+ try:
124
+ reader = PdfReader(pdf_file_path)
125
+ text = ""
126
+ if not reader.pages:
127
+ print(f" PDF '{os.path.basename(pdf_file_path)}' contains no pages.")
128
+ return "ERROR: PDFにページが含まれていません。" # プレフィックスを追加
129
+
130
+ for i, page in enumerate(reader.pages):
131
+ page_text = page.extract_text()
132
+ if page_text:
133
+ text += page_text + "\n"
134
+ # print(f" Page {i+1} extracted text (first 100 chars): {page_text[:100].replace('\n', ' ')}...")
135
+ cleaned_page_text = page_text[:100].replace('\n', ' ')
136
+ print(f" Page {i+1} extracted text (first 100 chars): {cleaned_page_text}...")
137
+ else:
138
+ print(f" Page {i+1} extracted no text.")
139
+
140
+ if not text.strip():
141
+ print(" No text extracted from any page.")
142
+ return "ERROR: PDFからテキストを抽出できませんでした。画像ベースのPDFかもしれません。" # プレフィックスを追加
143
+
144
+ print(f" Total text extracted (length: {len(text)}).")
145
+ return text
146
+ except Exception as e:
147
+ print(f" Error during PDF reading: {e}")
148
+ return f"ERROR: PDFの読み込み中にエラーが発生しました: {e}" # プレフィックスを追加
149
+
150
+ def get_llm_response(selected_llm, query, context, source_code_to_check):
151
+ """選択されたLLMを使用して質問に回答する"""
152
+ system_prompt = "あなたは提供されたコンテキスト(ソースコードチェックリスト)とレビュー対象のソースコードに基づいて、ソースコードをチェックし、その結果を返す有益なアシスタントです。チェックリストの項目ごとにソースコードを評価し、具体的な指摘と改善案を提示してください。コンテキストに情報がない場合は、「提供された情報からは回答できません。」と答えてください。"
153
+ user_content = f"ソースコードチェックリスト:\n{context}\n\nレビュー対象のソースコード:\n```\n{source_code_to_check}\n```\n\nレビュー指示: {query}\n\nチェック結果:"
154
+
155
+ try:
156
+ if selected_llm == "Ollama":
157
+ if not client_ollama:
158
+ return "Ollamaクライアントが初期化されていません。"
159
+ messages = [
160
+ {"role": "system", "content": system_prompt},
161
+ {"role": "user", "content": user_content}
162
+ ]
163
+ print(f"Ollamaモデル '{OLLAMA_MODEL_NAME}' にリクエストを送信中...")
164
+ response = client_ollama.chat(
165
+ model=OLLAMA_MODEL_NAME,
166
+ messages=messages,
167
+ options={
168
+ "temperature": 0.7,
169
+ "num_predict": 2000
170
+ }
171
+ )
172
+ if 'message' in response and 'content' in response['message']:
173
+ return response['message']['content'].strip()
174
+ else:
175
+ return f"Ollamaからの応答形式が不正です: {response}"
176
+
177
+ elif selected_llm == "GPT":
178
+ if not client_openai:
179
+ return "OpenAI APIキーが設定されていないため、GPTモデルは利用できません。"
180
+ messages = [
181
+ {"role": "system", "content": system_prompt},
182
+ {"role": "user", "content": user_content}
183
+ ]
184
+ print(f"GPTモデル '{OPENAI_MODEL_NAME}' にリクエストを送信中...")
185
+ response = client_openai.chat.completions.create(
186
+ model=OPENAI_MODEL_NAME,
187
+ messages=messages,
188
+ temperature=0.5,
189
+ max_tokens=2000
190
+ )
191
+ return response.choices[0].message.content.strip()
192
+
193
+ elif selected_llm == "Anthropic":
194
+ if not client_anthropic:
195
+ return "Anthropic APIキーが設定されていないため、Anthropicモデルは利用できません。"
196
+ messages = [
197
+ {"role": "user", "content": user_content}
198
+ ]
199
+ print(f"Anthropicモデル '{ANTHROPIC_MODEL_NAME}' にリクエストを送信中...")
200
+ response = client_anthropic.messages.create(
201
+ model=ANTHROPIC_MODEL_NAME,
202
+ max_tokens=2000,
203
+ temperature=0.5,
204
+ system=system_prompt, # Anthropicはsystemプロンプトを直接引数で渡す
205
+ messages=messages
206
+ )
207
+ return response.content[0].text.strip()
208
+
209
+ elif selected_llm == "Google Gemini":
210
+ if not client_gemini:
211
+ return "Google APIキーが設定されていないため、Geminiモデルは利用できません。"
212
+ # Geminiのsystem instructionはまだベ��タ版で、messagesと併用できない場合があるため、
213
+ # system_promptをuser_contentの先頭に結合する形式にする。
214
+ # --- システムプロンプトの調整 (後述の2.プロンプト調整も参照) ---
215
+ system_prompt = "あなたは提供されたコンテキスト(ソースコードチェックリスト)とレビュー対象のソースコードに基づいて、ソースコードのレビューを行うアシスタントです。チェックリストの項目ごとにソースコードを評価し、潜在的な問題点や改善の機会を提案してください。コンテキストに情報がない場合は、「提供された情報からは回答できません。」と答えてください。"
216
+ # --- ユーザープロンプトの調整 (後述の2.プロンプト調整も参照) ---
217
+ user_content = f"ソースコードチェックリスト:\n{context}\n\nレビュー対象のソースコード:\n```\n{source_code_to_check}\n```\n\nレビュー指示: {query}\n\nチェック結果:"
218
+
219
+ full_user_content = f"{system_prompt}\n\n{user_content}"
220
+ messages = [
221
+ {"role": "user", "parts": [full_user_content]}
222
+ ]
223
+ print(f"Google Geminiモデル '{GOOGLE_MODEL_NAME}' にリクエストを送信中...")
224
+ try:
225
+ response = client_gemini.generate_content(
226
+ messages,
227
+ generation_config=genai.types.GenerationConfig(
228
+ temperature=0.5, # まずは0.5で試す。必要なら0.7などに上げる
229
+ max_output_tokens=2000
230
+ )
231
+ )
232
+
233
+ # --- エラーハンドリングの強化 ---
234
+ # response.text を呼び出す前に、応答の候補と終了理由を確認
235
+ if response.candidates:
236
+ candidate = response.candidates[0]
237
+ # finish_reason が SAFETY (genai.types.HarmCategory.SAFETY) の場合、安全ポリシーによりブロックされた可能性が高い
238
+ if candidate.finish_reason == genai.types.HarmCategory.SAFETY:
239
+ safety_ratings = candidate.safety_ratings
240
+ safety_details = ", ".join([f"{sr.category.name}: {sr.probability.name}" for sr in safety_ratings])
241
+ print(f"Gemini response blocked due to safety policy. Details: {safety_details}")
242
+ return f"Google Geminiからの応答が安全ポリシーによりブロックされました。詳細: {safety_details}"
243
+ # 正常なコンテンツがあるか確認
244
+ elif candidate.content and candidate.content.parts:
245
+ return response.text.strip()
246
+ else:
247
+ # コンテンツがないが、finish_reasonがSAFETY以外の場合
248
+ print(f"Gemini response has no content parts. Finish reason: {candidate.finish_reason.name}")
249
+ return f"Google Geminiからの応答にコンテンツが含まれていません。終了理由: {candidate.finish_reason.name}"
250
+ else:
251
+ # 候補自体がない場合
252
+ print(f"Gemini response has no candidates. Raw response: {response}")
253
+ return f"Google Geminiからの応答に候補がありませんでした。生の応答: {response}"
254
+
255
+ except Exception as e:
256
+ # generate_content 自体でエラーが発生した場合
257
+ print(f"Google Gemini API呼び出し中にエラーが発生しました: {e}")
258
+ return f"Google Gemini API呼び出し中にエラーが発生しました: {e}"
259
+
260
+ else:
261
+ return "無効なLLMが選択されました。"
262
+
263
+ except Exception as e:
264
+ print(f"LLM ({selected_llm}) の呼び出し中にエラーが発生しました: {e}")
265
+ return f"LLM ({selected_llm}) の呼び出し中にエラーが発生しました: {e}"
266
+
267
+ def upload_pdf_and_process(pdf_files):
268
+ """複数のPDFファイルをアップロードし、テキストを抽出し、ChromaDBに登録する"""
269
+ if not pdf_files:
270
+ print("No PDF files uploaded.")
271
+ return "PDFファイルがアップロードされていません。", gr.update(interactive=False), gr.update(interactive=False)
272
+
273
+ processed_files_count = 0
274
+ total_chunks_added = 0
275
+ all_status_messages = []
276
+
277
+ for pdf_file in pdf_files:
278
+ try:
279
+ pdf_path = pdf_file.name
280
+ file_name = os.path.basename(pdf_path)
281
+ all_status_messages.append(f"PDFファイル '{file_name}' を処理中...")
282
+ print(f"Processing PDF: {file_name} (Temporary Path: {pdf_path})")
283
+
284
+ # 1. PDFからテキストを抽出
285
+ raw_text = extract_text_from_pdf(pdf_path)
286
+
287
+ # --- デバッグ用追加コード (前回��デバッグで追加したものは残しておくと良いでしょう) ---
288
+ print(f"DEBUG: raw_text received from extract_text_from_pdf (length: {len(raw_text)})")
289
+ # print(f"DEBUG: raw_text starts with: '{raw_text[:100].replace(newline_char, ' ')}'")
290
+ print(f"DEBUG: 'エラー' in raw_text: {'エラー' in raw_text}")
291
+ print(f"DEBUG: '抽出できませんでした' in raw_text: {'抽出できませんでした' in raw_text}")
292
+ print(f"DEBUG: 'PDFにページが含まれていません' in raw_text: {'PDFにページが含まれていません' in raw_text}")
293
+ # --- デバッグ情報ここまで ---
294
+
295
+ # エラープレフィックスでチェックするように変更
296
+ if raw_text.startswith("ERROR:"): # ここを変更
297
+ all_status_messages.append(raw_text)
298
+ print(f"Error during text extraction from {file_name}: {raw_text}") # ログメッセージも変更
299
+ continue # 次のファイルへ
300
+
301
+ # --- デバッグ用追加コード ---
302
+ print(f"\n--- Raw text extracted from {file_name} (length: {len(raw_text)}, first 500 chars) ---")
303
+ print(raw_text[:500])
304
+ print(f"--- End of raw text from {file_name} ---\n")
305
+
306
+ # 2. テキストをチャンクに分割
307
+ chunks = text_splitter.split_text(raw_text)
308
+ if not chunks:
309
+ all_status_messages.append(f"'{file_name}' から有効なテキストチャンクを抽出できませんでした。")
310
+ print(f"No valid chunks extracted from {file_name}.")
311
+ continue # 次のファイルへ
312
+
313
+ # 3. チャンクをChromaDBに登録
314
+ documents = chunks
315
+ metadatas = [{"source": file_name, "chunk_index": i} for i in range(len(chunks))]
316
+ ids = [str(uuid.uuid4()) for _ in range(len(chunks))]
317
+ collection.add(
318
+ documents=documents,
319
+ metadatas=metadatas,
320
+ ids=ids
321
+ )
322
+ processed_files_count += 1
323
+ total_chunks_added += len(chunks)
324
+ all_status_messages.append(f"PDFファイル '{file_name}' の処理が完了しました。{len(chunks)}個のチャンクがデータベースに登録されました。")
325
+ print(f"Finished processing {file_name}. Added {len(chunks)} chunks.")
326
+
327
+ except Exception as e:
328
+ all_status_messages.append(f"PDFファイル '{os.path.basename(pdf_file.name)}' 処理中に予期せぬエラーが発生しました: {e}")
329
+ print(f"Unexpected error during processing {os.path.basename(pdf_file.name)}: {e}")
330
+ continue # 次のファイルへ
331
+
332
+ final_status_message = f"{processed_files_count}個のPDFファイルの処理が完了しました。合計{total_chunks_added}個のチャンクがデータベースに登録されました。質問とソースコードを入力してください。\n\n" + "\n".join(all_status_messages)
333
+ return final_status_message, gr.update(interactive=True), gr.update(interactive=True)
334
+
335
+ def answer_question(question, source_code, selected_llm):
336
+ """ChromaDBから関連情報を取得し、選択されたLLMで質問に回答する"""
337
+ if not question and not source_code:
338
+ return "質問またはレビュー対象のソースコードを入力してください。", ""
339
+
340
+ if collection.count() == 0:
341
+ return "PDFがまだアップロードされていないか、処理されていません。まずPDFをアップロードしてください。", ""
342
+
343
+ try:
344
+ print(f"Searching ChromaDB for question: {question}")
345
+ results = collection.query(
346
+ query_texts=[question],
347
+ n_results=8
348
+ )
349
+ context_chunks = results['documents'][0] if results['documents'] else []
350
+ if not context_chunks:
351
+ print("No relevant context chunks found in ChromaDB.")
352
+ return "関連する情報が見つかりませんでした。質問を明確にするか、別のPDFを試してください。", ""
353
+
354
+ context = "\n\n".join(context_chunks)
355
+ print(f"Retrieved context (first 500 chars):\n{context[:500]}...")
356
+
357
+ answer = get_llm_response(selected_llm, question, context, source_code)
358
+ return answer, context
359
+ except Exception as e:
360
+ print(f"質問応答中に予期せぬエラーが発生しました: {e}")
361
+ return f"質問応答中に予期せぬエラーが発生しました: {e}", ""
362
+
363
+ # --- Gradio UIの構築 ---
364
+ with gr.Blocks() as gradioUI:
365
+ gr.Markdown(
366
+ f"""
367
+ # PDF Q&A with Local LLM (Ollama: {OLLAMA_MODEL_NAME}) and Vector Database
368
+ PDFファイルとしてソースコードチェックリストをアップロードし、レビューしたいソースコードを入力してください。
369
+ **複数のPDFファイルを同時にアップロードできます。**
370
+ 利用するLLMを選択し��ください。
371
+ """
372
+ )
373
+ with gr.Row():
374
+ with gr.Column():
375
+ pdf_input = gr.File(label="PDFドキュメントをアップロード", file_types=[".pdf"], file_count="multiple")
376
+ upload_status = gr.Textbox(label="ステータス", interactive=False, value="PDFをアップロードしてください。", lines=5)
377
+
378
+ with gr.Column():
379
+ # LLM選択コンポーネント
380
+ llm_options = ["Ollama"]
381
+ if client_openai:
382
+ llm_options.append("GPT")
383
+ if client_anthropic:
384
+ llm_options.append("Anthropic")
385
+ if client_gemini:
386
+ llm_options.append("Google Gemini")
387
+
388
+ llm_choice = gr.Radio(
389
+ llm_options,
390
+ label="使用するLLMを選択",
391
+ value=llm_options[0] if llm_options else None, # 利用可能な最初のLLMをデフォルトにする
392
+ interactive=True
393
+ )
394
+
395
+ source_code_input = gr.Code(
396
+ label="レビュー対象のソースコード (ここにソースコードを貼り付けてください)",
397
+ value="",
398
+ language="python",
399
+ interactive=False, # PDFアップロード後に有効化
400
+ lines=15
401
+ )
402
+ question_input = gr.Textbox(label="レビュー指示(例: セキュリティの観点からレビュー)", placeholder="特定の観点からのレビュー指示を入力してください(任意)。", interactive=False) # PDFアップロード後に有効化
403
+
404
+ review_button = gr.Button("レビュー開始")
405
+
406
+ answer_output = gr.Markdown(label="レビュー結果")
407
+ retrieved_context_output = gr.Textbox(label="取得されたチェックリスト項目", interactive=False, lines=10)
408
+
409
+ pdf_input.upload(
410
+ upload_pdf_and_process,
411
+ inputs=[pdf_input],
412
+ outputs=[upload_status, question_input, source_code_input]
413
+ )
414
+
415
+ review_button.click(
416
+ answer_question,
417
+ inputs=[question_input, source_code_input, llm_choice],
418
+ outputs=[answer_output, retrieved_context_output]
419
+ )
420
+
421
+ # gradioUI.launch(server_name="localhost", server_port=7860)
422
+ gradioUI.launch(server_name="0.0.0.0", server_port=7860)
requirements.txt CHANGED
@@ -9,4 +9,8 @@ sentencepiece
9
  tiktoken
10
  ollama
11
  anthropic
12
- google.generativeai
 
 
 
 
 
9
  tiktoken
10
  ollama
11
  anthropic
12
+ google.generativeai
13
+ fastapi
14
+ uvicorn
15
+ python-multipart
16
+ pydantic