├── src ├── __init__.py ├── main.py ├── embedding_generator.py ├── rag_tools.py ├── mcp_server.py ├── cli.py ├── rag_service.py ├── document_processor.py └── vector_database.py ├── tests ├── conftest.py ├── test_vector_database.py ├── test_embedding_generator.py └── test_mcp_server.py ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── pull_request_template.md └── workflows │ ├── ruff.yml │ └── pytest.yml ├── .env.sample ├── LICENSE ├── pyproject.toml ├── .clinerules ├── CLAUDE.md ├── .gitignore ├── data └── source │ └── markdown │ ├── machine_learning_basics.md │ └── python_basics.md ├── README.md └── docs └── design.md /src/__init__.py: -------------------------------------------------------------------------------- 1 | # このファイルは notion_mcp_light パッケージを認識するためのものです。 2 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | 4 | sys.path.insert(0, os.getcwd()) 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 機能要望 3 | about: 新機能の提案用テンプレート 4 | title: '[FEATURE] ' 5 | labels: enhancement 6 | assignees: '' 7 | --- 8 | 9 | # 機能要望 10 | 11 | ## 概要 12 | 提案する機能の概要を説明してください。 13 | 14 | ## 背景・目的 15 | この機能が必要な理由や背景を説明してください。 16 | 17 | ## 提案内容 18 | 具体的な機能の実装案があれば記載してください。 19 | 20 | ### 期待する動作 21 | 1. 22 | 2. 23 | 3. 24 | 25 | ### 技術的な考慮事項 26 | - 27 | - 28 | 29 | ## 代替案 30 | 他に検討した実装方法があれば記載してください。 31 | 32 | ## その他 33 | - 参考情報 34 | - 関連するIssue/PR 35 | - スクリーンショット(必要な場合) 36 | 37 | ## チェックリスト 38 | - [ ] 既存の機能と重複していない 39 | - [ ] 実装の技術的な実現可能性を確認している -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: バグ報告 3 | about: バグの報告用テンプレート 4 | title: '[BUG] ' 5 | labels: bug 6 | assignees: '' 7 | --- 8 | 9 | # バグ報告 10 | 11 | ## バグの概要 12 | バグの内容を簡潔に説明してください。 13 | 14 | ## 再現手順 15 | 1. 16 | 2. 17 | 3. 18 | 19 | ## 期待する動作 20 | 本来期待される動作を説明してください。 21 | 22 | ## 実際の動作 23 | 実際に起きている動作を説明してください。 24 | 25 | ## 環境情報 26 | - OS: [例: macOS 14.0] 27 | - Python: [例: 3.11.0] 28 | - Pyxel: [例: 1.9.0] 29 | 30 | ## スクリーンショット 31 | 可能であれば、問題の説明に役立つスクリーンショットを添付してください。 32 | 33 | ## 補足情報 34 | 問題に関する追加情報があれば記載してください。 35 | 36 | ## 再現用コード 37 | ```python 38 | # 問題を再現するための最小限のコード -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | # PostgreSQL接続情報 2 | POSTGRES_HOST=localhost 3 | POSTGRES_PORT=5432 4 | POSTGRES_USER=postgres 5 | POSTGRES_PASSWORD=password 6 | POSTGRES_DB=ragdb 7 | 8 | # ドキュメントディレクトリ 9 | SOURCE_DIR=./data/source 10 | PROCESSED_DIR=./data/processed 11 | 12 | # エンベディングモデル 13 | # infloat/multilingual-e5-large 14 | EMBEDDING_MODEL=intfloat/multilingual-e5-large 15 | EMBEDDING_DIM=1024 16 | EMBEDDING_PREFIX_QUERY="query: " 17 | EMBEDDING_PREFIX_EMBEDDING="passage: " 18 | 19 | # cl-nagoya/ruri-v3-30m 20 | # EMBEDDING_MODEL=cl-nagoya/ruri-v3-30m 21 | # EMBEDDING_DIM=256 22 | # EMBEDDING_PREFIX_QUERY="検索クエリ: " 23 | # EMBEDDING_PREFIX_EMBEDDING="検索文書: " -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Pull Request 2 | 3 | ## 変更内容 4 | 変更の概要を簡潔に記載してください。 5 | 6 | ### 追加した機能 7 | - 8 | - 9 | 10 | ### 変更・修正した機能 11 | - 12 | - 13 | 14 | ### 削除した機能 15 | - 16 | - 17 | 18 | ## 変更理由 19 | この変更が必要な理由や背景を説明してください。 20 | 21 | ## 影響範囲 22 | この変更による影響範囲を記載してください。 23 | - [ ] 既存の機能への影響 24 | - [ ] パフォーマンスへの影響 25 | - [ ] セキュリティへの影響 26 | 27 | ## 動作確認 28 | - [ ] ユニットテストの追加・更新 29 | - [ ] 動作確認の実施 30 | - [ ] ドキュメントの更新 31 | 32 | ## スクリーンショット 33 | 必要に応じてスクリーンショットを添付してください。 34 | 35 | ## 補足情報 36 | レビュアーに伝えたい追加情報があれば記載してください。 37 | 38 | ## チェックリスト 39 | - [ ] コーディング規約に準拠している 40 | - [ ] 適切なコメントを追加している 41 | - [ ] 必要なテストを追加している 42 | - [ ] ドキュメントを更新している 43 | -------------------------------------------------------------------------------- /.github/workflows/ruff.yml: -------------------------------------------------------------------------------- 1 | name: Ruff 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | ruff: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: Set up Python 16 | uses: actions/setup-python@v4 17 | with: 18 | python-version: "3.10" 19 | 20 | - name: Install dependencies 21 | run: | 22 | python -m pip install --upgrade pip 23 | pip install ruff==0.9.1 24 | 25 | - name: Run Ruff Check 26 | run: ruff check --line-length=127 27 | 28 | - name: Run Ruff Format Check 29 | run: ruff format --check --diff --line-length=127 -------------------------------------------------------------------------------- /.github/workflows/pytest.yml: -------------------------------------------------------------------------------- 1 | name: Pytest CI 2 | on: 3 | push: 4 | branches: [ main ] 5 | pull_request: 6 | branches: [ main ] 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - name: Setup Python 13 | uses: actions/setup-python@v4 14 | with: 15 | python-version: '3.x' 16 | - name: Install uv 17 | run: | 18 | curl -LsSf https://astral.sh/uv/install.sh | sh 19 | echo "$HOME/.cargo/bin" >> $GITHUB_PATH 20 | - name: Setup virtual environment 21 | run: | 22 | uv venv 23 | - name: Install dependencies 24 | run: | 25 | uv pip install --upgrade pip 26 | uv pip install -e ".[dev]" 27 | - name: Run tests 28 | run: | 29 | source .venv/bin/activate 30 | python -m pytest -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 karaage 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=42", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "mcp-rag-server" 7 | version = "0.1.0" 8 | description = "MCP RAG Server - Model Context Protocol (MCP)に準拠したRAG機能を持つPythonサーバー" 9 | authors = [ 10 | {name = "MCP Server Team"} 11 | ] 12 | readme = "README.md" 13 | requires-python = ">=3.10" 14 | classifiers = [ 15 | "Programming Language :: Python :: 3", 16 | "Programming Language :: Python :: 3.10", 17 | "Operating System :: OS Independent", 18 | "License :: OSI Approved :: MIT License", 19 | "Topic :: Software Development :: Libraries :: Application Frameworks", 20 | "Topic :: Communications :: Chat", 21 | ] 22 | dependencies = [ 23 | "mcp[cli]", 24 | "python-dotenv", 25 | "psycopg2-binary", 26 | "sentence-transformers", 27 | "markdown", 28 | "numpy", 29 | "markitdown[all]", 30 | "sentencepiece>=0.2.0", 31 | ] 32 | 33 | [project.optional-dependencies] 34 | dev = [ 35 | "pytest", 36 | ] 37 | 38 | [project.scripts] 39 | mcp-rag-server = "src.main:main" 40 | mcp-rag-cli = "src.cli:main" 41 | 42 | [tool.setuptools] 43 | package-dir = {"" = "src"} 44 | 45 | [tool.setuptools.packages.find] 46 | where = ["src"] 47 | -------------------------------------------------------------------------------- /.clinerules: -------------------------------------------------------------------------------- 1 | # Cline Rules 2 | 3 | ## ロール定義 4 | 5 | あなたは熟練のPythonプログラマとしてコードを書いてください。 6 | 7 | 8 | ## 期待する回答 9 | 10 | - 実装コードは省略せず、完全な形で提供 11 | - 日本語での詳細な説明 12 | 13 | 14 | ## 注意事項 15 | 16 | ### 設計書 17 | 18 | - 新規開発時は docs ディレクトリ以下に以下の内容を含む設計書 `design.md`を作成してください: 19 | - 要件定義書 20 | - 設計書(概略・機能・クラス構成) 21 | - 既存のソフトウェアを修正する場合: 22 | - 既存の設計書を参照してソフトウェアを開発してください 23 | - 修正内容に応じて設計書も更新してください 24 | - 設計書を作成したら、コードを作成する前にユーザーに設計書のチェックを依頼してください 25 | 26 | ### コーディング規約 27 | 28 | - PEP8に従ったコードを書いてください 29 | - ruffのフォーマッタでファイルの保存と同時に自動整形するので、フォーマットの修正は不要です 30 | - GoogleスタイルのDocstringを書いてください 31 | 32 | ### テストコード 33 | 34 | - テストコードを tests ディレクトリ以下に src ディレクトリと同じ構成で作成してください 35 | - テストコードを作成したら pytest を実行してエラー無いことを確認してください。エラーが出たら修正してください 36 | 37 | ### Git操作 38 | 39 | - gitの操作はgit statusでステータス確認しながら慎重に行ってください 40 | - git管理されているファイルは、git mv や git rm を使って移動削除してください 41 | 42 | ### Pull Request(PR) 43 | 44 | #### PR作成時 45 | - PRを要望されたら、gitコマンドで差分を確認したうえで、`gh pr` コマンドを使ってPRを作成してください 46 | - PRのdescriptionは .github/pull_request_template.md を読み取ってフォーマットを合わせてください 47 | 48 | #### PRレビュー時 49 | 以下の手順でファイルごとにコメントを付けてください: 50 | 51 | 1. チェックする観点は .github/pull_request_template.md を参照してください 52 | 2. PRの差分を確認: 53 | ```bash 54 | gh pr diff 55 | ``` 56 | 57 | 3. ファイルごとに、変更後のファイル全体とPRの差分を確認した上でレビューコメントを追加: 58 | ```bash 59 | gh api repos///pulls//comments \ 60 | -F body="レビューコメント" \ 61 | -F commit_id="$(gh pr view --json headRefOid --jq .headRefOid)" \ 62 | -F path="対象ファイルのパス" \ 63 | -F position= 64 | ``` 65 | 66 | パラメータの説明: 67 | - position: diffの行番号(新規ファイルの場合は1から開始) 68 | - commit_id: PRの最新のコミットIDを自動取得 69 | -------------------------------------------------------------------------------- /tests/test_vector_database.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import patch, MagicMock 3 | import os 4 | import sys 5 | 6 | # `src`ディレクトリをパスに追加 7 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../src"))) 8 | 9 | # psycopg2をグローバルにモック化 10 | sys.modules["psycopg2"] = MagicMock() 11 | 12 | 13 | class TestVectorDatabase(unittest.TestCase): 14 | def setUp(self): 15 | # 各テストの前にvector_databaseモジュールをアンロードする 16 | if "vector_database" in sys.modules: 17 | del sys.modules["vector_database"] 18 | 19 | @patch.dict(os.environ, {"EMBEDDING_DIM": "512"}) 20 | def test_create_table_with_custom_embedding_dim(self): 21 | """環境変数からEMBEDDING_DIMを読み取り、テーブル作成クエリが正しくフォーマットされるかテスト""" 22 | # パッチされた環境でモジュールをインポート 23 | from vector_database import VectorDatabase, EMBEDDING_DIM 24 | 25 | mock_connect = MagicMock() 26 | with patch("vector_database.psycopg2.connect", return_value=mock_connect): 27 | mock_cursor = MagicMock() 28 | mock_connect.cursor.return_value = mock_cursor 29 | 30 | db = VectorDatabase(connection_params={"dbname": "test_db"}) 31 | db.initialize_database() 32 | 33 | # CREATE TABLEのSQL文を取得 34 | create_table_sql = mock_cursor.execute.call_args_list[1][0][0] 35 | 36 | # SQL内に正しいベクトル次元が含まれているか確認 37 | self.assertEqual(EMBEDDING_DIM, 512) 38 | self.assertIn("embedding vector(512)", create_table_sql) 39 | 40 | # executeが5回呼ばれることを確認 41 | self.assertEqual(mock_cursor.execute.call_count, 5) 42 | 43 | @patch("dotenv.load_dotenv") # .envの読み込みを無効化 44 | def test_create_table_with_default_embedding_dim(self, mock_load_dotenv): 45 | """環境変数がない場合にデフォルトのEMBEDDING_DIMでテーブルが作成されるかテスト""" 46 | # 環境変数をクリア 47 | with patch.dict(os.environ, {}, clear=True): 48 | # パッチされた環境でモジュールをインポート 49 | from vector_database import VectorDatabase, EMBEDDING_DIM 50 | 51 | mock_connect = MagicMock() 52 | with patch("vector_database.psycopg2.connect", return_value=mock_connect): 53 | mock_cursor = MagicMock() 54 | mock_connect.cursor.return_value = mock_cursor 55 | 56 | db = VectorDatabase(connection_params={"dbname": "test_db"}) 57 | db.initialize_database() 58 | 59 | # CREATE TABLEのSQL文を取得 60 | create_table_sql = mock_cursor.execute.call_args_list[1][0][0] 61 | 62 | # デフォルトの次元(1024)が使われているか確認 63 | self.assertEqual(EMBEDDING_DIM, 1024) 64 | self.assertIn("embedding vector(1024)", create_table_sql) 65 | 66 | 67 | if __name__ == "__main__": 68 | unittest.main() 69 | -------------------------------------------------------------------------------- /src/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | MCP RAG Server 4 | 5 | Model Context Protocol (MCP)に準拠したRAG機能を持つPythonサーバー 6 | """ 7 | 8 | import sys 9 | import os 10 | import argparse 11 | import importlib 12 | import logging 13 | from dotenv import load_dotenv 14 | 15 | from .mcp_server import MCPServer 16 | from .rag_tools import register_rag_tools, create_rag_service_from_env 17 | 18 | 19 | def main(): 20 | """ 21 | メイン関数 22 | 23 | コマンドライン引数を解析し、MCPサーバーを起動します。 24 | """ 25 | # コマンドライン引数の解析 26 | parser = argparse.ArgumentParser( 27 | description="MCP RAG Server - Model Context Protocol (MCP)に準拠したRAG機能を持つPythonサーバー" 28 | ) 29 | parser.add_argument("--name", default="mcp-rag-server", help="サーバー名") 30 | parser.add_argument("--version", default="0.1.0", help="サーバーバージョン") 31 | parser.add_argument("--description", default="MCP RAG Server - 複数形式のドキュメントのRAG検索", help="サーバーの説明") 32 | parser.add_argument("--module", help="追加のツールモジュール(例: myapp.tools)") 33 | args = parser.parse_args() 34 | 35 | # 環境変数の読み込み 36 | load_dotenv() 37 | 38 | # ディレクトリの作成 39 | os.makedirs("logs", exist_ok=True) 40 | os.makedirs(os.environ.get("SOURCE_DIR", "data/source"), exist_ok=True) 41 | os.makedirs(os.environ.get("PROCESSED_DIR", "data/processed"), exist_ok=True) 42 | 43 | # ロギングの設定 44 | logging.basicConfig( 45 | level=logging.INFO, 46 | format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", 47 | handlers=[ 48 | logging.StreamHandler(sys.stderr), 49 | logging.FileHandler(os.path.join("logs", "mcp_rag_server.log"), encoding="utf-8"), 50 | ], 51 | ) 52 | logger = logging.getLogger("main") 53 | 54 | try: 55 | # MCPサーバーの作成 56 | server = MCPServer() 57 | 58 | # RAGサービスの作成と登録 59 | logger.info("RAGサービスを初期化しています...") 60 | rag_service = create_rag_service_from_env() 61 | register_rag_tools(server, rag_service) 62 | logger.info("RAGツールを登録しました") 63 | 64 | # 追加のツールモジュールがある場合は読み込む 65 | if args.module: 66 | try: 67 | module = importlib.import_module(args.module) 68 | if hasattr(module, "register_tools"): 69 | module.register_tools(server) 70 | print(f"モジュール '{args.module}' からツールを登録しました", file=sys.stderr) 71 | else: 72 | print(f"警告: モジュール '{args.module}' に register_tools 関数が見つかりません", file=sys.stderr) 73 | except ImportError as e: 74 | print(f"警告: モジュール '{args.module}' の読み込みに失敗しました: {str(e)}", file=sys.stderr) 75 | 76 | # MCPサーバーの起動 77 | server.start(args.name, args.version, args.description) 78 | 79 | except KeyboardInterrupt: 80 | print("サーバーを終了します。", file=sys.stderr) 81 | sys.exit(0) 82 | 83 | except Exception as e: 84 | print(f"エラーが発生しました: {str(e)}", file=sys.stderr) 85 | sys.exit(1) 86 | 87 | 88 | if __name__ == "__main__": 89 | main() 90 | -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | # CLAUDE.md 2 | 3 | This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. 4 | 5 | ## プロジェクト概要 6 | 7 | MCP RAG Serverは、Model Context Protocol (MCP)とRAG (Retrieval-Augmented Generation)機能を実装したPythonサーバーです。複数のドキュメント形式に対応したベクトル検索システムを提供します。 8 | 9 | ## 主要コマンド 10 | 11 | ### 開発環境セットアップ 12 | ```bash 13 | # 依存関係のインストール (uvを使用) 14 | uv sync 15 | 16 | # PostgreSQLとpgvectorのセットアップが必要 17 | # DockerでPostgreSQLを起動: 18 | docker run -d \ 19 | --name pgvector-db \ 20 | -e POSTGRES_USER=your_user \ 21 | -e POSTGRES_PASSWORD=your_password \ 22 | -e POSTGRES_DB=your_database \ 23 | -p 5432:5432 \ 24 | pgvector/pgvector:pg16 25 | 26 | # .envファイルの設定が必要 27 | ``` 28 | 29 | ### 実行コマンド 30 | ```bash 31 | # MCPサーバーの起動 32 | uv run python -m src.main 33 | 34 | # CLIでドキュメントをインデックス 35 | uv run python -m src.cli index 36 | uv run python -m src.cli index --incremental # 差分インデックス 37 | 38 | # インデックスのクリア 39 | uv run python -m src.cli clear 40 | 41 | # ドキュメント数の確認 42 | uv run python -m src.cli count 43 | ``` 44 | 45 | ### テスト実行 46 | ```bash 47 | # pytestでテスト実行 48 | uv run pytest 49 | ``` 50 | 51 | ### Lint・フォーマット 52 | ```bash 53 | # ruffでlintチェック 54 | uv run ruff check --line-length=127 55 | 56 | # ruffで自動フォーマット 57 | uv run ruff format --line-length=127 58 | 59 | # フォーマットチェック(差分表示) 60 | uv run ruff format --check --diff --line-length=127 61 | ``` 62 | 63 | ### Pull Request(PR) 64 | 65 | #### PR作成時 66 | - PRを要望されたら、gitコマンドで差分を確認したうえで、`gh pr` コマンドを使ってPRを作成してください 67 | - PRのdescriptionは .github/pull_request_template.md を読み取ってフォーマットを合わせてください 68 | 69 | #### PRレビュー時 70 | 以下の手順でファイルごとにコメントを付けてください: 71 | 72 | 1. チェックする観点は .github/pull_request_template.md を参照してください 73 | 2. PRの差分を確認: 74 | ```bash 75 | gh pr diff 76 | ``` 77 | 78 | 3. ファイルごとに、変更後のファイル全体とPRの差分を確認した上でレビューコメントを追加: 79 | ```bash 80 | gh api repos///pulls//comments \ 81 | -F body="レビューコメント" \ 82 | -F commit_id="$(gh pr view --json headRefOid --jq .headRefOid)" \ 83 | -F path="対象ファイルのパス" \ 84 | -F position= 85 | ``` 86 | 87 | パラメータの説明: 88 | - position: diffの行番号(新規ファイルの場合は1から開始) 89 | - commit_id: PRの最新のコミットIDを自動取得 90 | 91 | ## アーキテクチャ概要 92 | 93 | ### コア構成 94 | - **MCPサーバー層**: `src/mcp_server.py`がJSON-RPC通信を処理 95 | - **RAGサービス層**: `src/rag_service.py`がドキュメント処理と検索を統括 96 | - **データ層**: PostgreSQL + pgvectorでベクトルデータベースを実装 97 | 98 | ### 主要モジュール 99 | - `src/main.py`: エントリーポイント 100 | - `src/rag_tools.py`: MCP用の検索ツール定義 101 | - `src/document_processor.py`: ドキュメント解析とチャンク化 102 | - `src/embedding_generator.py`: multilingual-e5-largeモデルでの埋め込み生成 103 | - `src/vector_database.py`: PostgreSQL/pgvectorインターフェース 104 | 105 | ### データフロー 106 | 1. `data/source/`配下のドキュメントを読み込み 107 | 2. markitdownでテキスト変換、チャンク分割 108 | 3. sentence-transformersで埋め込みベクトル生成 109 | 4. PostgreSQLにベクトルと共に保存 110 | 5. MCPツール経由でセマンティック検索を提供 111 | 112 | ### 重要な設計パターン 113 | - 差分インデックス: ファイルハッシュで変更検知 114 | - オーバーラップチャンク: コンテキスト保持のため重複あり 115 | - 隣接チャンク取得: 検索結果の前後文脈も取得可能 116 | 117 | ## 環境変数設定 118 | 119 | `.env`ファイルに以下を設定: 120 | ``` 121 | # PostgreSQL接続情報 122 | POSTGRES_HOST=localhost 123 | POSTGRES_PORT=5432 124 | POSTGRES_USER=your_user 125 | POSTGRES_PASSWORD=your_password 126 | POSTGRES_DB=your_database 127 | 128 | # パス設定 129 | SOURCE_DIR=data/source 130 | PROCESSED_DIR=data/processed 131 | ``` 132 | 133 | ## 対応ドキュメント形式 134 | - Markdown (.md) 135 | - テキスト (.txt) 136 | - PowerPoint (.pptx) 137 | - PDF (.pdf) 138 | - Word (.docx) 139 | 140 | ## 開発時の注意点 141 | - 新しいドキュメント形式を追加する場合は`document_processor.py`を拡張 142 | - ベクトルデータベースのスキーマ変更時は`vector_database.py`の`create_tables()`を更新 143 | - MCPツールを追加する場合は`rag_tools.py`にツール定義を追加 -------------------------------------------------------------------------------- /tests/test_embedding_generator.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import patch, MagicMock 3 | import os 4 | import numpy as np 5 | 6 | # `src`ディレクトリをパスに追加して、`embedding_generator`をインポート可能にする 7 | import sys 8 | 9 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../src"))) 10 | 11 | from embedding_generator import EmbeddingGenerator 12 | 13 | 14 | class TestEmbeddingGenerator(unittest.TestCase): 15 | def setUp(self): 16 | """テストケースごとに環境をクリーンアップし、モックを設定""" 17 | self.env_patcher = patch.dict(os.environ, {}, clear=True) 18 | self.mock_sentence_transformer_patcher = patch("embedding_generator.SentenceTransformer") 19 | 20 | self.env_patcher.start() 21 | self.mock_sentence_transformer = self.mock_sentence_transformer_patcher.start() 22 | 23 | # SentenceTransformerのコンストラクタとencodeメソッドをモック化 24 | self.mock_model_instance = MagicMock() 25 | self.mock_sentence_transformer.return_value = self.mock_model_instance 26 | # encodeが呼ばれたら固定のnumpy配列を返すように設定 27 | self.mock_model_instance.encode.return_value = np.array([[0.1, 0.2, 0.3]]) 28 | 29 | def tearDown(self): 30 | """パッチを停止""" 31 | self.env_patcher.stop() 32 | self.mock_sentence_transformer_patcher.stop() 33 | 34 | def test_initialization_with_env_variables(self): 35 | """環境変数から設定が読み込まれることをテスト""" 36 | test_env = { 37 | "EMBEDDING_MODEL": "test-model", 38 | "EMBEDDING_PREFIX_QUERY": "query: ", 39 | "EMBEDDING_PREFIX_EMBEDDING": "passage: ", 40 | } 41 | with patch.dict(os.environ, test_env, clear=True): 42 | generator = EmbeddingGenerator() 43 | self.assertEqual(generator.model_name, "test-model") 44 | self.assertEqual(generator.prefix_query, "query: ") 45 | self.assertEqual(generator.prefix_embedding, "passage: ") 46 | self.mock_sentence_transformer.assert_called_with("test-model") 47 | 48 | def test_initialization_with_defaults(self): 49 | """環境変数がない場合にデフォルト値が使われることをテスト""" 50 | generator = EmbeddingGenerator() 51 | self.assertEqual(generator.model_name, "intfloat/multilingual-e5-large") 52 | self.assertEqual(generator.prefix_query, "") 53 | self.assertEqual(generator.prefix_embedding, "") 54 | self.mock_sentence_transformer.assert_called_with("intfloat/multilingual-e5-large") 55 | 56 | def test_add_prefix(self): 57 | """_add_prefixメソッドのロジックをテスト""" 58 | generator = EmbeddingGenerator() 59 | self.assertEqual(generator._add_prefix("text", "prefix: "), "prefix: text") 60 | self.assertEqual(generator._add_prefix("prefix: text", "prefix: "), "prefix: text") 61 | self.assertEqual(generator._add_prefix("text", ""), "text") 62 | self.assertEqual(generator._add_prefix("TEXT", "prefix: "), "prefix: TEXT") 63 | 64 | def test_generate_embedding_with_prefix(self): 65 | """generate_embeddingが正しいプレフィックスを使用することをテスト""" 66 | test_env = {"EMBEDDING_PREFIX_EMBEDDING": "passage: "} 67 | with patch.dict(os.environ, test_env, clear=True): 68 | generator = EmbeddingGenerator() 69 | generator.generate_embedding("my text") 70 | self.mock_model_instance.encode.assert_called_with("passage: my text") 71 | 72 | def test_generate_embeddings_with_prefix(self): 73 | """generate_embeddingsが正しいプレフィックスを使用することをテスト""" 74 | test_env = {"EMBEDDING_PREFIX_EMBEDDING": "passage: "} 75 | with patch.dict(os.environ, test_env, clear=True): 76 | generator = EmbeddingGenerator() 77 | generator.generate_embeddings(["text1", "text2"]) 78 | self.mock_model_instance.encode.assert_called_with(["passage: text1", "passage: text2"]) 79 | 80 | def test_generate_query_embedding_with_prefix(self): 81 | """generate_query_embeddingが正しいプレフィックスを使用することをテスト""" 82 | test_env = {"EMBEDDING_PREFIX_QUERY": "query: "} 83 | with patch.dict(os.environ, test_env, clear=True): 84 | generator = EmbeddingGenerator() 85 | generator.generate_search_embedding("my query") 86 | self.mock_model_instance.encode.assert_called_with("query: my query") 87 | 88 | 89 | if __name__ == "__main__": 90 | unittest.main() 91 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # This repository 2 | mcp_settings.json 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a template from a python script 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | *.py,cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | cover/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | db.sqlite3-journal 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | .pybuilder/ 79 | target/ 80 | 81 | # Jupyter Notebook 82 | .ipynb_checkpoints 83 | 84 | # IPython 85 | profile_default/ 86 | ipython_config.py 87 | 88 | # pyenv 89 | # For a library or package, you might want to ignore these files since the code is 90 | # intended to run in multiple environments; otherwise, check them in: 91 | # .python-version 92 | 93 | # pipenv 94 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 95 | # However, in case of collaboration, if having platform-platform-specific dependencies or dependencies 96 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 97 | # install all needed dependencies. 98 | #Pipfile.lock 99 | 100 | # UV 101 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. 102 | # This is especially recommended for binary packages to ensure reproducibility, and is more 103 | # commonly ignored for libraries. 104 | #uv.lock 105 | 106 | # poetry 107 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 108 | # This is especially recommended for binary packages to ensure reproducibility, and is more 109 | # commonly ignored for libraries. 110 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 111 | #poetry.lock 112 | 113 | # pdm 114 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 115 | #pdm.lock 116 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 117 | # in version control. 118 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 119 | .pdm.toml 120 | .pdm-python 121 | .pdm-build/ 122 | 123 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 124 | __pypackages__/ 125 | 126 | # Celery stuff 127 | celerybeat-schedule 128 | celerybeat.pid 129 | 130 | # SageMath parsed files 131 | *.sage.py 132 | 133 | # Environments 134 | .env 135 | .venv 136 | env/ 137 | venv/ 138 | ENV/ 139 | env.bak/ 140 | venv.bak/ 141 | 142 | # Spyder project settings 143 | .spyderproject 144 | .spyproject 145 | 146 | # Rope project settings 147 | .ropeproject 148 | 149 | # mkdocs documentation 150 | /site 151 | 152 | # mypy 153 | .mypy_cache/ 154 | .dmypy.json 155 | dmypy.json 156 | 157 | # Pyre type checker 158 | .pyre/ 159 | 160 | # pytype static type analyzer 161 | .pytype/ 162 | 163 | # Cython debug symbols 164 | cython_debug/ 165 | 166 | # PyCharm 167 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 168 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 169 | # and can be added to the global gitignore or merged into this file. For a more nuclear 170 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 171 | #.idea/ 172 | 173 | # PyPI configuration file 174 | .pypirc 175 | 176 | .DS_Store 177 | data/processed/file_registry.json 178 | data/processed/*.md -------------------------------------------------------------------------------- /src/embedding_generator.py: -------------------------------------------------------------------------------- 1 | """ 2 | エンベディング生成モジュール 3 | 4 | テキストからエンベディングを生成します。 5 | """ 6 | 7 | import logging 8 | import os 9 | from typing import List 10 | from sentence_transformers import SentenceTransformer 11 | from dotenv import load_dotenv 12 | 13 | # .envの読み込み 14 | load_dotenv() 15 | 16 | 17 | class EmbeddingGenerator: 18 | """ 19 | エンベディング生成クラス 20 | 21 | テキストからエンベディングを生成します。 22 | 23 | Attributes: 24 | model: SentenceTransformerモデル 25 | logger: ロガー 26 | """ 27 | 28 | def __init__(self, model_name: str = None): 29 | """ 30 | EmbeddingGeneratorのコンストラクタ 31 | 32 | Args: 33 | model_name: 使用するモデル名(.env優先) 34 | """ 35 | # .envから設定を取得 36 | self.model_name = os.getenv("EMBEDDING_MODEL", "intfloat/multilingual-e5-large") 37 | self.prefix_query = os.getenv("EMBEDDING_PREFIX_QUERY", "") 38 | self.prefix_embedding = os.getenv("EMBEDDING_PREFIX_EMBEDDING", "") 39 | 40 | # ロガーの設定 41 | self.logger = logging.getLogger("embedding_generator") 42 | self.logger.setLevel(logging.INFO) 43 | 44 | # モデルの読み込み 45 | self.logger.info(f"モデル '{self.model_name}' を読み込んでいます...") 46 | try: 47 | self.model = SentenceTransformer(self.model_name) 48 | self.logger.info(f"モデル '{self.model_name}' を読み込みました") 49 | except Exception as e: 50 | self.logger.error(f"モデル '{self.model_name}' の読み込みに失敗しました: {str(e)}") 51 | raise 52 | 53 | def _add_prefix(self, text: str, prefix: str) -> str: 54 | """ 55 | テキストに適切なプレフィックスを追加する 56 | 57 | Args: 58 | text: 元のテキスト 59 | prefix: 追加するプレフィックス 60 | 61 | Returns: 62 | プレフィックス付きのテキスト 63 | """ 64 | if not prefix: 65 | return text 66 | 67 | # プレフィックスが既に含まれているかチェック(大文字小文字を区別) 68 | if text.startswith(prefix): 69 | return text 70 | 71 | return f"{prefix}{text}" 72 | 73 | def generate_embedding(self, text: str) -> List[float]: 74 | """ 75 | テキストからエンベディングを生成します。 76 | 77 | Args: 78 | text: エンベディングを生成するテキスト 79 | 80 | Returns: 81 | エンベディング(浮動小数点数のリスト) 82 | """ 83 | if not text: 84 | self.logger.warning("空のテキストからエンベディングを生成しようとしています") 85 | return [] 86 | 87 | try: 88 | processed_text = self._add_prefix(text, self.prefix_embedding) 89 | embedding = self.model.encode(processed_text) 90 | embedding_list = embedding.tolist() 91 | self.logger.debug(f"テキスト '{text[:50]}...' のエンベディングを生成しました") 92 | return embedding_list 93 | except Exception as e: 94 | self.logger.error(f"エンベディングの生成中にエラーが発生しました: {str(e)}") 95 | raise 96 | 97 | def generate_embeddings(self, texts: List[str]) -> List[List[float]]: 98 | """ 99 | 複数のテキストからエンベディングを生成します。 100 | 101 | Args: 102 | texts: エンベディングを生成するテキストのリスト 103 | 104 | Returns: 105 | エンベディングのリスト 106 | """ 107 | if not texts: 108 | self.logger.warning("空のテキストリストからエンベディングを生成しようとしています") 109 | return [] 110 | 111 | try: 112 | processed_texts = [self._add_prefix(text, self.prefix_embedding) for text in texts] 113 | embeddings = self.model.encode(processed_texts) 114 | embeddings_list = embeddings.tolist() 115 | self.logger.info(f"{len(texts)} 個のテキストのエンベディングを生成しました") 116 | return embeddings_list 117 | except Exception as e: 118 | self.logger.error(f"エンベディングの生成中にエラーが発生しました: {str(e)}") 119 | raise 120 | 121 | def generate_search_embedding(self, query: str) -> List[float]: 122 | """ 123 | 検索クエリからエンベディングを生成します。 124 | 125 | Args: 126 | query: 検索クエリ 127 | 128 | Returns: 129 | エンベディング(浮動小数点数のリスト) 130 | """ 131 | if not query: 132 | self.logger.warning("空のクエリからエンベディングを生成しようとしています") 133 | return [] 134 | 135 | try: 136 | processed_query = self._add_prefix(query, self.prefix_query) 137 | embedding = self.model.encode(processed_query) 138 | embedding_list = embedding.tolist() 139 | self.logger.debug(f"クエリ '{query}' のエンベディングを生成しました") 140 | return embedding_list 141 | except Exception as e: 142 | self.logger.error(f"クエリエンベディングの生成中にエラーが発生しました: {str(e)}") 143 | raise 144 | -------------------------------------------------------------------------------- /tests/test_mcp_server.py: -------------------------------------------------------------------------------- 1 | """ 2 | MCPサーバーのテスト 3 | """ 4 | 5 | import json 6 | from unittest.mock import patch 7 | 8 | from src.mcp_server import MCPServer 9 | 10 | 11 | def test_mcp_server_initialization(): 12 | """MCPサーバーの初期化をテストします""" 13 | server = MCPServer() 14 | assert server.tools == {} 15 | assert server.tool_handlers == {} 16 | 17 | 18 | def test_register_tool(): 19 | """ツールの登録をテストします""" 20 | server = MCPServer() 21 | 22 | # テスト用のハンドラ関数 23 | def test_handler(params): 24 | return {"result": "test"} 25 | 26 | # ツールを登録 27 | server.register_tool( 28 | name="test_tool", 29 | description="Test tool", 30 | input_schema={ 31 | "type": "object", 32 | "properties": { 33 | "param1": {"type": "string"}, 34 | }, 35 | "required": ["param1"], 36 | }, 37 | handler=test_handler, 38 | ) 39 | 40 | # ツールが登録されていることを確認 41 | assert "test_tool" in server.tools 42 | assert "test_tool" in server.tool_handlers 43 | assert server.tools["test_tool"]["name"] == "test_tool" 44 | assert server.tools["test_tool"]["description"] == "Test tool" 45 | assert server.tool_handlers["test_tool"] == test_handler 46 | 47 | 48 | @patch("sys.stdout") 49 | def test_send_response(mock_stdout): 50 | """レスポンスの送信をテストします""" 51 | server = MCPServer() 52 | 53 | # レスポンスを送信 54 | response = {"jsonrpc": "2.0", "result": "test", "id": 1} 55 | server._send_response(response) 56 | 57 | # 標準出力に正しいJSONが出力されていることを確認 58 | # 注: 実装によっては、writeが2回呼ばれる場合があります(JSONと改行を別々に書き込む場合) 59 | mock_stdout.write.assert_any_call(json.dumps(response)) 60 | mock_stdout.flush.assert_called_once() 61 | 62 | 63 | @patch("src.mcp_server.MCPServer._send_result") 64 | def test_handle_tools_call(mock_send_result): 65 | """tools/callメソッドの処理をテストします""" 66 | server = MCPServer() 67 | 68 | # テスト用のハンドラ関数 69 | def test_handler(params): 70 | return {"content": [{"type": "text", "text": f"Result: {params.get('param1')}"}]} 71 | 72 | # ツールを登録 73 | server.register_tool( 74 | name="test_tool", 75 | description="Test tool", 76 | input_schema={ 77 | "type": "object", 78 | "properties": { 79 | "param1": {"type": "string"}, 80 | }, 81 | "required": ["param1"], 82 | }, 83 | handler=test_handler, 84 | ) 85 | 86 | # tools/callメソッドを呼び出し 87 | params = { 88 | "name": "test_tool", 89 | "arguments": { 90 | "param1": "test_value", 91 | }, 92 | } 93 | server._handle_tools_call(params, 1) 94 | 95 | # _send_resultが正しく呼び出されていることを確認 96 | mock_send_result.assert_called_once_with({"content": [{"type": "text", "text": "Result: test_value"}]}, 1) 97 | 98 | 99 | @patch("src.mcp_server.MCPServer._send_result") 100 | def test_handle_tools_call_error(mock_send_result): 101 | """tools/callメソッドのエラー処理をテストします""" 102 | server = MCPServer() 103 | 104 | # テスト用のハンドラ関数(例外を発生させる) 105 | def test_handler(params): 106 | raise ValueError("Test error") 107 | 108 | # ツールを登録 109 | server.register_tool( 110 | name="test_tool", 111 | description="Test tool", 112 | input_schema={ 113 | "type": "object", 114 | "properties": { 115 | "param1": {"type": "string"}, 116 | }, 117 | "required": ["param1"], 118 | }, 119 | handler=test_handler, 120 | ) 121 | 122 | # tools/callメソッドを呼び出し 123 | params = { 124 | "name": "test_tool", 125 | "arguments": { 126 | "param1": "test_value", 127 | }, 128 | } 129 | server._handle_tools_call(params, 1) 130 | 131 | # _send_resultが正しく呼び出されていることを確認 132 | mock_send_result.assert_called_once() 133 | args, _ = mock_send_result.call_args 134 | assert args[0]["isError"] is True 135 | assert "Test error" in args[0]["content"][0]["text"] 136 | 137 | 138 | @patch("src.mcp_server.MCPServer._send_result") 139 | def test_handle_notifications_initialized(mock_send_result): 140 | """notifications/initializedメソッドの処理をテストします""" 141 | server = MCPServer() 142 | 143 | # notifications/initializedメソッドを呼び出し 144 | params = {} 145 | request_id = 1 146 | server._handle_notifications_initialized(params, request_id) 147 | 148 | # request_idが指定されている場合、_send_resultが呼び出されることを確認 149 | mock_send_result.assert_called_once_with({}, request_id) 150 | 151 | # request_idがNoneの場合、_send_resultが呼び出されないことを確認 152 | mock_send_result.reset_mock() 153 | server._handle_notifications_initialized(params, None) 154 | mock_send_result.assert_not_called() 155 | 156 | 157 | @patch("src.mcp_server.MCPServer._send_result") 158 | @patch("src.mcp_server.MCPServer._get_resources") 159 | def test_handle_resources_list(mock_get_resources, mock_send_result): 160 | """resources/listメソッドの処理をテストします""" 161 | server = MCPServer() 162 | 163 | # モックの戻り値を設定 164 | mock_resources = [{"name": "test_resource", "uri": "test://resource"}] 165 | mock_get_resources.return_value = mock_resources 166 | 167 | # resources/listメソッドを呼び出し 168 | request_id = 1 169 | server._handle_resources_list(request_id) 170 | 171 | # _get_resourcesが呼び出されることを確認 172 | mock_get_resources.assert_called_once() 173 | 174 | # _send_resultが正しく呼び出されていることを確認 175 | mock_send_result.assert_called_once_with({"resources": mock_resources}, request_id) 176 | 177 | 178 | @patch("src.mcp_server.MCPServer._send_result") 179 | @patch("src.mcp_server.MCPServer._get_resource_templates") 180 | def test_handle_resources_templates_list(mock_get_resource_templates, mock_send_result): 181 | """resources/templates/listメソッドの処理をテストします""" 182 | server = MCPServer() 183 | 184 | # モックの戻り値を設定 185 | mock_templates = [{"name": "test_template", "schema": {}}] 186 | mock_get_resource_templates.return_value = mock_templates 187 | 188 | # resources/templates/listメソッドを呼び出し 189 | request_id = 1 190 | server._handle_resources_templates_list(request_id) 191 | 192 | # _get_resource_templatesが呼び出されることを確認 193 | mock_get_resource_templates.assert_called_once() 194 | 195 | # _send_resultが正しく呼び出されていることを確認 196 | mock_send_result.assert_called_once_with({"templates": mock_templates}, request_id) 197 | 198 | 199 | def test_get_resource_templates(): 200 | """_get_resource_templatesメソッドをテストします""" 201 | server = MCPServer() 202 | 203 | # _get_resource_templatesメソッドを呼び出し 204 | templates = server._get_resource_templates() 205 | 206 | # 空のリストが返されることを確認 207 | assert templates == [] 208 | -------------------------------------------------------------------------------- /data/source/markdown/machine_learning_basics.md: -------------------------------------------------------------------------------- 1 | # 機械学習の基礎 2 | 3 | ## 機械学習とは 4 | 5 | 機械学習は、コンピュータシステムが明示的にプログラムされることなく、データから学習し、パターンを認識し、予測を行う能力を持つようにするための人工知能の一分野です。機械学習アルゴリズムは、サンプルデータ(訓練データ)を使用して予測モデルを構築します。 6 | 7 | ## 機械学習の種類 8 | 9 | ### 教師あり学習(Supervised Learning) 10 | 11 | 教師あり学習では、アルゴリズムは入力と出力のペアからなる訓練データを使用して学習します。目標は、新しい入力に対して正確な出力を予測できるモデルを作成することです。 12 | 13 | #### 回帰(Regression) 14 | 15 | 回帰は、連続的な出力値を予測するための教師あり学習の一種です。 16 | 17 | 例: 18 | - 住宅価格の予測 19 | - 株価の予測 20 | - 気温の予測 21 | 22 | 主な回帰アルゴリズム: 23 | - 線形回帰 24 | - 多項式回帰 25 | - 決定木回帰 26 | - ランダムフォレスト回帰 27 | - サポートベクター回帰(SVR) 28 | 29 | #### 分類(Classification) 30 | 31 | 分類は、入力データをカテゴリに分類するための教師あり学習の一種です。 32 | 33 | 例: 34 | - スパムメール検出 35 | - 画像認識 36 | - 疾病診断 37 | 38 | 主な分類アルゴリズム: 39 | - ロジスティック回帰 40 | - サポートベクターマシン(SVM) 41 | - 決定木 42 | - ランダムフォレスト 43 | - k近傍法(k-NN) 44 | - ナイーブベイズ 45 | 46 | ### 教師なし学習(Unsupervised Learning) 47 | 48 | 教師なし学習では、アルゴリズムはラベル付けされていないデータを使用して、データ内の隠れたパターンや構造を見つけます。 49 | 50 | #### クラスタリング(Clustering) 51 | 52 | クラスタリングは、データポイントを類似性に基づいてグループ(クラスタ)に分割します。 53 | 54 | 例: 55 | - 顧客セグメンテーション 56 | - 画像の圧縮 57 | - 異常検出 58 | 59 | 主なクラスタリングアルゴリズム: 60 | - K-means 61 | - 階層的クラスタリング 62 | - DBSCAN 63 | - 混合ガウスモデル 64 | 65 | #### 次元削減(Dimensionality Reduction) 66 | 67 | 次元削減は、データの複雑さを減らしながら、重要な情報を保持する技術です。 68 | 69 | 例: 70 | - データの可視化 71 | - ノイズ除去 72 | - 計算効率の向上 73 | 74 | 主な次元削減アルゴリズム: 75 | - 主成分分析(PCA) 76 | - t-SNE 77 | - UMAP 78 | - オートエンコーダ 79 | 80 | ### 強化学習(Reinforcement Learning) 81 | 82 | 強化学習では、エージェントは環境と相互作用し、行動の結果として報酬または罰則を受け取ります。目標は、長期的な報酬を最大化する行動ポリシーを学習することです。 83 | 84 | 例: 85 | - ゲームプレイ(チェス、囲碁、ビデオゲーム) 86 | - ロボット制御 87 | - 自動運転車 88 | 89 | 主な強化学習アルゴリズム: 90 | - Q学習 91 | - 深層Q学習(DQN) 92 | - 方策勾配法 93 | - アクター・クリティック法 94 | 95 | ## 機械学習の基本的なワークフロー 96 | 97 | 1. **問題の定義**: 解決したい問題を明確に定義します。 98 | 2. **データ収集**: 問題に関連するデータを収集します。 99 | 3. **データの前処理**: データをクリーニングし、特徴量を抽出・変換します。 100 | 4. **データの分割**: データを訓練セットとテストセットに分割します。 101 | 5. **モデルの選択**: 問題に適したアルゴリズムを選択します。 102 | 6. **モデルのトレーニング**: 訓練データを使用してモデルを学習させます。 103 | 7. **モデルの評価**: テストデータを使用してモデルの性能を評価します。 104 | 8. **モデルのチューニング**: ハイパーパラメータを調整して性能を向上させます。 105 | 9. **モデルのデプロイ**: 学習したモデルを実際のアプリケーションに統合します。 106 | 107 | ## 機械学習の評価指標 108 | 109 | ### 回帰モデルの評価指標 110 | 111 | - **平均絶対誤差(MAE)**: 予測値と実際の値の絶対差の平均 112 | - **平均二乗誤差(MSE)**: 予測値と実際の値の差の二乗の平均 113 | - **二乗平均平方根誤差(RMSE)**: MSEの平方根 114 | - **決定係数(R²)**: モデルによって説明される分散の割合 115 | 116 | ### 分類モデルの評価指標 117 | 118 | - **精度(Accuracy)**: 正しく分類されたサンプルの割合 119 | - **適合率(Precision)**: 陽性と予測されたサンプルのうち、実際に陽性であるサンプルの割合 120 | - **再現率(Recall)**: 実際に陽性であるサンプルのうち、陽性と予測されたサンプルの割合 121 | - **F1スコア**: 適合率と再現率の調和平均 122 | - **混同行列(Confusion Matrix)**: 予測クラスと実際のクラスの関係を示す行列 123 | - **ROC曲線とAUC**: 異なる閾値での真陽性率と偽陽性率の関係 124 | 125 | ## 機械学習の課題 126 | 127 | ### 過学習(Overfitting) 128 | 129 | モデルが訓練データに過度に適合し、新しいデータに対する一般化能力が低下する問題です。 130 | 131 | 対策: 132 | - 正則化(L1、L2正則化) 133 | - ドロップアウト 134 | - データ拡張 135 | - アンサンブル学習 136 | - 早期停止 137 | 138 | ### 過少学習(Underfitting) 139 | 140 | モデルがデータの基本的なパターンを捉えられず、訓練データでも性能が低い問題です。 141 | 142 | 対策: 143 | - より複雑なモデルの使用 144 | - 特徴量の追加 145 | - モデルの制約の緩和 146 | - ハイパーパラメータの調整 147 | 148 | ### バイアスとバリアンス(Bias-Variance Tradeoff) 149 | 150 | バイアスは、モデルの予測と実際の値の差を表します。バリアンスは、異なるトレーニングセットで学習した場合のモデルの予測のばらつきを表します。 151 | 152 | - 高バイアス:過少学習の原因 153 | - 高バリアンス:過学習の原因 154 | 155 | 理想的なモデルは、バイアスとバリアンスのバランスが取れています。 156 | 157 | ### 不均衡データ(Imbalanced Data) 158 | 159 | クラス間でサンプル数が大きく異なるデータセットでの学習の課題です。 160 | 161 | 対策: 162 | - リサンプリング(オーバーサンプリング、アンダーサンプリング) 163 | - SMOTE(Synthetic Minority Over-sampling Technique) 164 | - クラス重み付け 165 | - 異なる評価指標の使用(精度よりもF1スコアなど) 166 | 167 | ## Pythonでの機械学習 168 | 169 | ### 主要なライブラリ 170 | 171 | - **NumPy**: 数値計算のための基本ライブラリ 172 | - **Pandas**: データ操作と分析のためのライブラリ 173 | - **Matplotlib/Seaborn**: データ可視化のためのライブラリ 174 | - **Scikit-learn**: 機械学習アルゴリズムの実装を提供するライブラリ 175 | - **TensorFlow/Keras**: ディープラーニングのためのライブラリ 176 | - **PyTorch**: ディープラーニングのための柔軟なライブラリ 177 | 178 | ### Scikit-learnを使用した簡単な例 179 | 180 | ```python 181 | # 必要なライブラリのインポート 182 | import numpy as np 183 | from sklearn.model_selection import train_test_split 184 | from sklearn.linear_model import LogisticRegression 185 | from sklearn.metrics import accuracy_score, classification_report 186 | 187 | # サンプルデータの生成 188 | X = np.random.randn(100, 2) # 特徴量 189 | y = (X[:, 0] + X[:, 1] > 0).astype(int) # ラベル 190 | 191 | # データの分割 192 | X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) 193 | 194 | # モデルのトレーニング 195 | model = LogisticRegression() 196 | model.fit(X_train, y_train) 197 | 198 | # 予測 199 | y_pred = model.predict(X_test) 200 | 201 | # 評価 202 | accuracy = accuracy_score(y_test, y_pred) 203 | report = classification_report(y_test, y_pred) 204 | 205 | print(f"Accuracy: {accuracy}") 206 | print(report) 207 | ``` 208 | 209 | ## ディープラーニング 210 | 211 | ディープラーニングは、多層のニューラルネットワークを使用して複雑なパターンを学習する機械学習の一種です。 212 | 213 | ### ニューラルネットワークの基本構造 214 | 215 | - **入力層**: データを受け取る層 216 | - **隠れ層**: データの特徴を抽出する中間層(複数可) 217 | - **出力層**: 最終的な予測を出力する層 218 | - **活性化関数**: 非線形性を導入する関数(ReLU、シグモイド、tanh など) 219 | 220 | ### 主なディープラーニングアーキテクチャ 221 | 222 | - **畳み込みニューラルネットワーク(CNN)**: 画像処理に適したアーキテクチャ 223 | - **再帰型ニューラルネットワーク(RNN)**: 時系列データに適したアーキテクチャ 224 | - **長短期記憶(LSTM)**: RNNの一種で、長期依存関係を学習できる 225 | - **変換器(Transformer)**: 自然言語処理に革命をもたらしたアーキテクチャ 226 | - **生成的敵対的ネットワーク(GAN)**: 生成モデルの一種 227 | 228 | ### TensorFlow/Kerasを使用した簡単な例 229 | 230 | ```python 231 | import tensorflow as tf 232 | from tensorflow.keras.models import Sequential 233 | from tensorflow.keras.layers import Dense 234 | from sklearn.model_selection import train_test_split 235 | import numpy as np 236 | 237 | # サンプルデータの生成 238 | X = np.random.randn(1000, 20) # 特徴量 239 | y = (np.sum(X, axis=1) > 0).astype(int) # ラベル 240 | 241 | # データの分割 242 | X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) 243 | 244 | # モデルの構築 245 | model = Sequential([ 246 | Dense(64, activation='relu', input_shape=(20,)), 247 | Dense(32, activation='relu'), 248 | Dense(1, activation='sigmoid') 249 | ]) 250 | 251 | # モデルのコンパイル 252 | model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy']) 253 | 254 | # モデルのトレーニング 255 | history = model.fit(X_train, y_train, epochs=10, batch_size=32, validation_split=0.2) 256 | 257 | # モデルの評価 258 | loss, accuracy = model.evaluate(X_test, y_test) 259 | print(f"Test accuracy: {accuracy}") 260 | ``` 261 | 262 | ## 機械学習の応用分野 263 | 264 | ### コンピュータビジョン 265 | 266 | 画像や動画を理解し、解釈するための技術です。 267 | 268 | 応用例: 269 | - 物体検出 270 | - 顔認識 271 | - 画像分類 272 | - 画像生成 273 | - 自動運転 274 | 275 | ### 自然言語処理(NLP) 276 | 277 | コンピュータが人間の言語を理解し、処理するための技術です。 278 | 279 | 応用例: 280 | - 感情分析 281 | - 機械翻訳 282 | - 質問応答 283 | - テキスト生成 284 | - チャットボット 285 | 286 | ### 推薦システム 287 | 288 | ユーザーの好みや行動に基づいて、アイテムやコンテンツを推薦する技術です。 289 | 290 | 応用例: 291 | - 商品推薦 292 | - 映画・音楽推薦 293 | - コンテンツパーソナライゼーション 294 | 295 | ### 異常検出 296 | 297 | 通常のパターンから逸脱するデータポイントを検出する技術です。 298 | 299 | 応用例: 300 | - 不正検出 301 | - 故障予測 302 | - ネットワークセキュリティ 303 | 304 | ## 機械学習の倫理と責任 305 | 306 | 機械学習システムの開発と展開には、倫理的な考慮事項が伴います。 307 | 308 | ### バイアスと公平性 309 | 310 | 機械学習モデルは、訓練データに存在するバイアスを学習し、増幅する可能性があります。これにより、特定のグループに対する不公平な結果が生じる可能性があります。 311 | 312 | 対策: 313 | - 多様なデータセットの使用 314 | - バイアス検出と軽減のためのツールの使用 315 | - 公平性指標のモニタリング 316 | 317 | ### プライバシー 318 | 319 | 機械学習モデルは、個人データを使用して訓練されることが多く、プライバシーの懸念が生じます。 320 | 321 | 対策: 322 | - データの匿名化 323 | - 差分プライバシー 324 | - 連合学習 325 | 326 | ### 透明性と説明可能性 327 | 328 | 複雑な機械学習モデル(特にディープラーニング)は、「ブラックボックス」として機能し、決定の理由を説明することが困難な場合があります。 329 | 330 | 対策: 331 | - 説明可能なAI(XAI)技術の使用 332 | - モデルの解釈可能性の向上 333 | - 決定プロセスの透明性の確保 334 | 335 | ## まとめ 336 | 337 | 機械学習は、データからパターンを学習し、予測を行うための強力な技術です。教師あり学習、教師なし学習、強化学習など、さまざまな種類の機械学習があり、それぞれ異なる問題に適しています。機械学習の実装には、データの前処理、モデルの選択とトレーニング、評価、チューニングなどの一連のステップが含まれます。また、過学習や不均衡データなどの課題に対処するための技術も重要です。Pythonの豊富なライブラリエコシステムにより、機械学習の実装が容易になっています。最後に、機械学習システムの開発と展開には、バイアス、プライバシー、透明性などの倫理的な考慮事項が伴うことを忘れてはなりません。 -------------------------------------------------------------------------------- /data/source/markdown/python_basics.md: -------------------------------------------------------------------------------- 1 | # Python基礎知識 2 | 3 | ## Pythonとは 4 | 5 | Pythonは、汎用プログラミング言語の一つで、コードの読みやすさを重視した設計思想を持っています。インタープリタ型言語であり、動的型付け言語です。多くのプラットフォームで動作し、大規模なシステムから小さなスクリプトまで、さまざまな用途に使用されています。 6 | 7 | ## Pythonの特徴 8 | 9 | - **読みやすい構文**: インデントを使用したブロック構造により、コードが読みやすい 10 | - **豊富なライブラリ**: 標準ライブラリが充実しており、多くのサードパーティライブラリも利用可能 11 | - **マルチパラダイム**: 手続き型、オブジェクト指向型、関数型など、複数のプログラミングパラダイムをサポート 12 | - **インタープリタ型**: コンパイル不要で実行可能 13 | - **動的型付け**: 変数の型を明示的に宣言する必要がない 14 | - **自動メモリ管理**: ガベージコレクションによるメモリ管理 15 | 16 | ## Pythonの基本構文 17 | 18 | ### 変数と型 19 | 20 | Pythonでは、変数を宣言する際に型を指定する必要はありません。 21 | 22 | ```python 23 | # 整数 24 | x = 10 25 | 26 | # 浮動小数点数 27 | y = 3.14 28 | 29 | # 文字列 30 | name = "Python" 31 | 32 | # ブール値 33 | is_active = True 34 | 35 | # リスト 36 | numbers = [1, 2, 3, 4, 5] 37 | 38 | # タプル 39 | coordinates = (10, 20) 40 | 41 | # 辞書 42 | person = {"name": "Alice", "age": 30} 43 | 44 | # 集合 45 | unique_numbers = {1, 2, 3, 4, 5} 46 | ``` 47 | 48 | ### 条件分岐 49 | 50 | ```python 51 | x = 10 52 | 53 | if x > 0: 54 | print("正の数です") 55 | elif x < 0: 56 | print("負の数です") 57 | else: 58 | print("ゼロです") 59 | ``` 60 | 61 | ### ループ 62 | 63 | ```python 64 | # forループ 65 | for i in range(5): 66 | print(i) # 0, 1, 2, 3, 4 67 | 68 | # whileループ 69 | count = 0 70 | while count < 5: 71 | print(count) 72 | count += 1 73 | ``` 74 | 75 | ### 関数 76 | 77 | ```python 78 | def greet(name): 79 | """挨拶を返す関数""" 80 | return f"こんにちは、{name}さん!" 81 | 82 | # 関数の呼び出し 83 | message = greet("太郎") 84 | print(message) # こんにちは、太郎さん! 85 | ``` 86 | 87 | ### クラス 88 | 89 | ```python 90 | class Person: 91 | def __init__(self, name, age): 92 | self.name = name 93 | self.age = age 94 | 95 | def introduce(self): 96 | return f"私の名前は{self.name}、{self.age}歳です。" 97 | 98 | # クラスのインスタンス化 99 | person = Person("太郎", 30) 100 | print(person.introduce()) # 私の名前は太郎、30歳です。 101 | ``` 102 | 103 | ## Pythonの高度な機能 104 | 105 | ### ジェネレータ 106 | 107 | ジェネレータは、イテレータを作成するための簡単な方法です。メモリ効率が良く、大きなデータセットを扱う際に役立ちます。 108 | 109 | ```python 110 | def count_up_to(n): 111 | i = 0 112 | while i < n: 113 | yield i 114 | i += 1 115 | 116 | # ジェネレータの使用 117 | for number in count_up_to(5): 118 | print(number) # 0, 1, 2, 3, 4 119 | ``` 120 | 121 | ### デコレータ 122 | 123 | デコレータは、関数やメソッドの動作を変更するための構文です。 124 | 125 | ```python 126 | def log_function_call(func): 127 | def wrapper(*args, **kwargs): 128 | print(f"関数 {func.__name__} が呼び出されました") 129 | return func(*args, **kwargs) 130 | return wrapper 131 | 132 | @log_function_call 133 | def add(a, b): 134 | return a + b 135 | 136 | result = add(3, 5) # 関数 add が呼び出されました 137 | print(result) # 8 138 | ``` 139 | 140 | ### コンテキストマネージャ 141 | 142 | `with`文を使用して、リソースの確保と解放を自動的に行うことができます。 143 | 144 | ```python 145 | # ファイル操作の例 146 | with open("example.txt", "w") as file: 147 | file.write("Hello, World!") 148 | # ファイルは自動的に閉じられる 149 | ``` 150 | 151 | ### 内包表記 152 | 153 | リスト、辞書、集合の作成を簡潔に記述できます。 154 | 155 | ```python 156 | # リスト内包表記 157 | squares = [x**2 for x in range(10)] 158 | 159 | # 条件付きリスト内包表記 160 | even_squares = [x**2 for x in range(10) if x % 2 == 0] 161 | 162 | # 辞書内包表記 163 | square_dict = {x: x**2 for x in range(5)} 164 | 165 | # 集合内包表記 166 | square_set = {x**2 for x in range(5)} 167 | ``` 168 | 169 | ## Pythonのモジュールとパッケージ 170 | 171 | ### モジュール 172 | 173 | モジュールは、Pythonのコードを論理的に整理するための方法です。 174 | 175 | ```python 176 | # math モジュールのインポート 177 | import math 178 | 179 | # math モジュールの関数を使用 180 | print(math.sqrt(16)) # 4.0 181 | 182 | # 特定の関数だけをインポート 183 | from math import sqrt 184 | print(sqrt(16)) # 4.0 185 | 186 | # 別名をつけてインポート 187 | import math as m 188 | print(m.sqrt(16)) # 4.0 189 | ``` 190 | 191 | ### パッケージ 192 | 193 | パッケージは、複数のモジュールを含むディレクトリです。 194 | 195 | ``` 196 | my_package/ 197 | __init__.py 198 | module1.py 199 | module2.py 200 | subpackage/ 201 | __init__.py 202 | module3.py 203 | ``` 204 | 205 | ```python 206 | # パッケージのインポート 207 | import my_package.module1 208 | 209 | # サブパッケージのインポート 210 | import my_package.subpackage.module3 211 | ``` 212 | 213 | ## Pythonの例外処理 214 | 215 | ```python 216 | try: 217 | # 例外が発生する可能性のあるコード 218 | result = 10 / 0 219 | except ZeroDivisionError: 220 | # ゼロ除算エラーの処理 221 | print("ゼロで割ることはできません") 222 | except Exception as e: 223 | # その他の例外の処理 224 | print(f"エラーが発生しました: {e}") 225 | else: 226 | # 例外が発生しなかった場合の処理 227 | print("計算が成功しました") 228 | finally: 229 | # 例外の有無にかかわらず実行される処理 230 | print("処理を終了します") 231 | ``` 232 | 233 | ## Pythonの標準ライブラリ 234 | 235 | Pythonには、多くの便利な標準ライブラリが含まれています。 236 | 237 | - **os**: オペレーティングシステムとの対話 238 | - **sys**: Pythonインタープリタとの対話 239 | - **datetime**: 日付と時刻の操作 240 | - **math**: 数学関数 241 | - **random**: 乱数生成 242 | - **json**: JSONデータの処理 243 | - **re**: 正規表現 244 | - **collections**: 特殊なコンテナデータ型 245 | - **itertools**: イテレータ操作のための関数 246 | - **functools**: 高階関数と呼び出し可能オブジェクトの操作 247 | 248 | ## Pythonの仮想環境 249 | 250 | 仮想環境は、プロジェクトごとに独立したPython環境を作成するための機能です。 251 | 252 | ```bash 253 | # 仮想環境の作成 254 | python -m venv myenv 255 | 256 | # 仮想環境の有効化(Windows) 257 | myenv\Scripts\activate 258 | 259 | # 仮想環境の有効化(macOS/Linux) 260 | source myenv/bin/activate 261 | 262 | # パッケージのインストール 263 | pip install package_name 264 | 265 | # 仮想環境の無効化 266 | deactivate 267 | ``` 268 | 269 | ## Pythonのパッケージ管理 270 | 271 | ```bash 272 | # パッケージのインストール 273 | pip install package_name 274 | 275 | # 特定のバージョンのインストール 276 | pip install package_name==1.0.0 277 | 278 | # パッケージのアップグレード 279 | pip install --upgrade package_name 280 | 281 | # インストール済みパッケージの一覧表示 282 | pip list 283 | 284 | # requirements.txtの作成 285 | pip freeze > requirements.txt 286 | 287 | # requirements.txtからのインストール 288 | pip install -r requirements.txt 289 | ``` 290 | 291 | ## Pythonのテスト 292 | 293 | ### unittest 294 | 295 | ```python 296 | import unittest 297 | 298 | def add(a, b): 299 | return a + b 300 | 301 | class TestAddFunction(unittest.TestCase): 302 | def test_add_positive_numbers(self): 303 | self.assertEqual(add(1, 2), 3) 304 | 305 | def test_add_negative_numbers(self): 306 | self.assertEqual(add(-1, -2), -3) 307 | 308 | def test_add_mixed_numbers(self): 309 | self.assertEqual(add(-1, 2), 1) 310 | 311 | if __name__ == "__main__": 312 | unittest.main() 313 | ``` 314 | 315 | ### pytest 316 | 317 | ```python 318 | # test_add.py 319 | def add(a, b): 320 | return a + b 321 | 322 | def test_add_positive_numbers(): 323 | assert add(1, 2) == 3 324 | 325 | def test_add_negative_numbers(): 326 | assert add(-1, -2) == -3 327 | 328 | def test_add_mixed_numbers(): 329 | assert add(-1, 2) == 1 330 | ``` 331 | 332 | ```bash 333 | # pytestの実行 334 | pytest test_add.py 335 | ``` 336 | 337 | ## Pythonのデバッグ 338 | 339 | ### pdb(Python Debugger) 340 | 341 | ```python 342 | import pdb 343 | 344 | def complex_function(): 345 | x = 10 346 | y = 20 347 | pdb.set_trace() # デバッガが起動 348 | z = x + y 349 | return z 350 | 351 | result = complex_function() 352 | ``` 353 | 354 | ## Pythonのドキュメント 355 | 356 | ### Docstring 357 | 358 | ```python 359 | def calculate_area(radius): 360 | """ 361 | 円の面積を計算します。 362 | 363 | Args: 364 | radius (float): 円の半径 365 | 366 | Returns: 367 | float: 円の面積 368 | 369 | Raises: 370 | ValueError: 半径が負の値の場合 371 | """ 372 | if radius < 0: 373 | raise ValueError("半径は負の値にできません") 374 | return 3.14159 * radius ** 2 375 | ``` 376 | 377 | ## Pythonのコーディング規約 378 | 379 | Pythonには、PEP 8と呼ばれるコーディング規約があります。主なルールは以下の通りです: 380 | 381 | - インデントには4つのスペースを使用 382 | - 行の長さは最大79文字 383 | - 関数やクラスの間には2行の空行 384 | - インポートは別々の行に記述 385 | - スペースの使用法(演算子の前後にスペース、カンマの後にスペースなど) 386 | - 命名規則(クラス名はCamelCase、関数名とメソッド名はlower_case_with_underscoresなど) 387 | 388 | ## まとめ 389 | 390 | Pythonは、読みやすさと書きやすさを重視した汎用プログラミング言語です。豊富な標準ライブラリとサードパーティライブラリにより、さまざまな用途に使用できます。初心者にも優しい言語設計でありながら、高度な機能も備えているため、プログラミング初心者から専門家まで幅広く利用されています。 -------------------------------------------------------------------------------- /src/rag_tools.py: -------------------------------------------------------------------------------- 1 | """ 2 | RAGツールモジュール 3 | 4 | MCPサーバーに登録するRAG関連ツールを提供します。 5 | """ 6 | 7 | import os 8 | 9 | from typing import Dict, Any 10 | 11 | from .document_processor import DocumentProcessor 12 | from .embedding_generator import EmbeddingGenerator 13 | from .vector_database import VectorDatabase 14 | from .rag_service import RAGService 15 | 16 | from dotenv import load_dotenv 17 | 18 | load_dotenv() 19 | 20 | 21 | def register_rag_tools(server, rag_service: RAGService): 22 | """ 23 | RAG関連ツールをMCPサーバーに登録します。 24 | 25 | Args: 26 | server: MCPサーバーのインスタンス 27 | rag_service: RAGサービスのインスタンス 28 | """ 29 | # 検索ツールの登録 30 | server.register_tool( 31 | name="search", 32 | description="ベクトル検索を行います", 33 | input_schema={ 34 | "type": "object", 35 | "properties": { 36 | "query": { 37 | "type": "string", 38 | "description": "検索クエリ", 39 | }, 40 | "limit": { 41 | "type": "integer", 42 | "description": "返す結果の数(デフォルト: 5)", 43 | "default": 5, 44 | }, 45 | "with_context": { 46 | "type": "boolean", 47 | "description": "前後のチャンクも取得するかどうか(デフォルト: true)", 48 | "default": True, 49 | }, 50 | "context_size": { 51 | "type": "integer", 52 | "description": "前後に取得するチャンク数(デフォルト: 1)", 53 | "default": 1, 54 | }, 55 | "full_document": { 56 | "type": "boolean", 57 | "description": "ドキュメント全体を取得するかどうか(デフォルト: false)", 58 | "default": False, 59 | }, 60 | }, 61 | "required": ["query"], 62 | }, 63 | handler=lambda params: search_handler(params, rag_service), 64 | ) 65 | 66 | # ドキュメント数取得ツールの登録 67 | server.register_tool( 68 | name="get_document_count", 69 | description="インデックス内のドキュメント数を取得します", 70 | input_schema={ 71 | "type": "object", 72 | "properties": {}, 73 | "required": [], 74 | }, 75 | handler=lambda params: get_document_count_handler(params, rag_service), 76 | ) 77 | 78 | 79 | def search_handler(params: Dict[str, Any], rag_service: RAGService) -> Dict[str, Any]: 80 | """ 81 | ベクトル検索を行うハンドラ関数 82 | 83 | Args: 84 | params: パラメータ 85 | - query: 検索クエリ 86 | - limit: 返す結果の数(デフォルト: 5) 87 | - with_context: 前後のチャンクも取得するかどうか(デフォルト: true) 88 | - context_size: 前後に取得するチャンク数(デフォルト: 1) 89 | - full_document: ドキュメント全体を取得するかどうか(デフォルト: false) 90 | rag_service: RAGサービスのインスタンス 91 | 92 | Returns: 93 | 検索結果 94 | """ 95 | query = params.get("query") 96 | limit = params.get("limit", 5) 97 | with_context = params.get("with_context", True) 98 | context_size = params.get("context_size", 1) 99 | full_document = params.get("full_document", False) 100 | 101 | if not query: 102 | return { 103 | "content": [ 104 | { 105 | "type": "text", 106 | "text": "エラー: 検索クエリが指定されていません", 107 | } 108 | ], 109 | "isError": True, 110 | } 111 | 112 | try: 113 | # ドキュメント数を確認 114 | doc_count = rag_service.get_document_count() 115 | if doc_count == 0: 116 | return { 117 | "content": [ 118 | { 119 | "type": "text", 120 | "text": "インデックスにドキュメントが存在しません。CLIコマンド `python -m src.cli index` を使用してドキュメントをインデックス化してください。", 121 | } 122 | ], 123 | "isError": True, 124 | } 125 | 126 | # 検索を実行(前後のチャンクも取得、ドキュメント全体も取得) 127 | results = rag_service.search(query, limit, with_context, context_size, full_document) 128 | 129 | if not results: 130 | return { 131 | "content": [ 132 | { 133 | "type": "text", 134 | "text": f"クエリ '{query}' に一致する結果が見つかりませんでした", 135 | } 136 | ] 137 | } 138 | 139 | # 結果をファイルごとにグループ化 140 | file_groups = {} 141 | for result in results: 142 | file_path = result["file_path"] 143 | if file_path not in file_groups: 144 | file_groups[file_path] = [] 145 | file_groups[file_path].append(result) 146 | 147 | # 各グループ内でチャンクインデックスでソート 148 | for file_path in file_groups: 149 | file_groups[file_path].sort(key=lambda x: x["chunk_index"]) 150 | 151 | # 結果を整形 152 | content_items = [ 153 | { 154 | "type": "text", 155 | "text": f"クエリ '{query}' の検索結果({len(results)} 件):", 156 | } 157 | ] 158 | 159 | # ファイルごとに結果を表示 160 | for i, (file_path, group) in enumerate(file_groups.items()): 161 | file_name = os.path.basename(file_path) 162 | 163 | # ファイルヘッダー 164 | content_items.append( 165 | { 166 | "type": "text", 167 | "text": f"\n[{i + 1}] ファイル: {file_name}", 168 | } 169 | ) 170 | 171 | # 各チャンクを表示 172 | for j, result in enumerate(group): 173 | similarity_percent = result.get("similarity", 0) * 100 174 | is_context = result.get("is_context", False) 175 | is_full_document = result.get("is_full_document", False) 176 | 177 | # 全文ドキュメント、コンテキストチャンク、検索ヒットチャンクで表示を変える 178 | if is_full_document: 179 | content_items.append( 180 | { 181 | "type": "text", 182 | "text": f"\n+++ ドキュメント全文(チャンク {result['chunk_index']}) +++\n{result['content']}", 183 | } 184 | ) 185 | elif is_context: 186 | content_items.append( 187 | { 188 | "type": "text", 189 | "text": f"\n--- 前後のコンテキスト(チャンク {result['chunk_index']}) ---\n{result['content']}", 190 | } 191 | ) 192 | else: 193 | content_items.append( 194 | { 195 | "type": "text", 196 | "text": f"\n=== 検索ヒット(チャンク {result['chunk_index']}, 類似度: {similarity_percent:.2f}%) ===\n{result['content']}", 197 | } 198 | ) 199 | 200 | return {"content": content_items} 201 | 202 | except Exception as e: 203 | return { 204 | "content": [ 205 | { 206 | "type": "text", 207 | "text": f"検索中にエラーが発生しました: {str(e)}", 208 | } 209 | ], 210 | "isError": True, 211 | } 212 | 213 | 214 | def get_document_count_handler(params: Dict[str, Any], rag_service: RAGService) -> Dict[str, Any]: 215 | """ 216 | インデックス内のドキュメント数を取得するハンドラ関数 217 | 218 | Args: 219 | params: パラメータ(未使用) 220 | rag_service: RAGサービスのインスタンス 221 | 222 | Returns: 223 | ドキュメント数 224 | """ 225 | try: 226 | # ドキュメント数を取得 227 | count = rag_service.get_document_count() 228 | 229 | return { 230 | "content": [ 231 | { 232 | "type": "text", 233 | "text": f"インデックス内のドキュメント数: {count}", 234 | } 235 | ] 236 | } 237 | 238 | except Exception as e: 239 | return { 240 | "content": [ 241 | { 242 | "type": "text", 243 | "text": f"ドキュメント数の取得中にエラーが発生しました: {str(e)}", 244 | } 245 | ], 246 | "isError": True, 247 | } 248 | 249 | 250 | def create_rag_service_from_env() -> RAGService: 251 | """ 252 | 環境変数からRAGサービスを作成します。 253 | 254 | Returns: 255 | RAGサービスのインスタンス 256 | """ 257 | # 環境変数から接続情報を取得 258 | postgres_host = os.environ.get("POSTGRES_HOST", "localhost") 259 | postgres_port = os.environ.get("POSTGRES_PORT", "5432") 260 | postgres_user = os.environ.get("POSTGRES_USER", "postgres") 261 | postgres_password = os.environ.get("POSTGRES_PASSWORD", "password") 262 | postgres_db = os.environ.get("POSTGRES_DB", "ragdb") 263 | 264 | embedding_model = os.environ.get("EMBEDDING_MODEL", "intfloat/multilingual-e5-large") 265 | 266 | # コンポーネントの作成 267 | document_processor = DocumentProcessor() 268 | embedding_generator = EmbeddingGenerator(model_name=embedding_model) 269 | vector_database = VectorDatabase( 270 | { 271 | "host": postgres_host, 272 | "port": postgres_port, 273 | "user": postgres_user, 274 | "password": postgres_password, 275 | "database": postgres_db, 276 | } 277 | ) 278 | 279 | # RAGサービスの作成 280 | rag_service = RAGService(document_processor, embedding_generator, vector_database) 281 | 282 | return rag_service 283 | -------------------------------------------------------------------------------- /src/mcp_server.py: -------------------------------------------------------------------------------- 1 | """ 2 | MCPサーバーモジュール 3 | 4 | Model Context Protocol (MCP)に準拠したサーバーを提供します。 5 | JSON-RPC over stdioを使用してクライアントからのリクエストを処理します。 6 | """ 7 | 8 | import sys 9 | import json 10 | import logging 11 | from typing import Dict, Any, List, Callable 12 | from pathlib import Path 13 | 14 | 15 | class MCPServer: 16 | """ 17 | Model Context Protocol (MCP)に準拠したサーバークラス 18 | 19 | JSON-RPC over stdioを使用してクライアントからのリクエストを処理します。 20 | 21 | Attributes: 22 | tools: 登録されたツールのディクショナリ 23 | logger: ロガー 24 | """ 25 | 26 | def __init__(self): 27 | """ 28 | MCPServerのコンストラクタ 29 | """ 30 | self.tools = {} 31 | self.tool_handlers = {} 32 | 33 | # ロガーの設定 34 | self.logger = logging.getLogger("mcp_server") 35 | self.logger.setLevel(logging.INFO) 36 | 37 | # ファイルハンドラの設定 38 | log_dir = Path("logs") 39 | log_dir.mkdir(exist_ok=True) 40 | file_handler = logging.FileHandler(log_dir / "mcp_server.log") 41 | file_handler.setLevel(logging.INFO) 42 | 43 | # フォーマッタの設定 44 | formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") 45 | file_handler.setFormatter(formatter) 46 | 47 | # ハンドラの追加 48 | self.logger.addHandler(file_handler) 49 | 50 | def register_tool(self, name: str, description: str, input_schema: Dict[str, Any], handler: Callable): 51 | """ 52 | ツールを登録します。 53 | 54 | Args: 55 | name: ツール名 56 | description: ツールの説明 57 | input_schema: 入力スキーマ 58 | handler: ツールのハンドラ関数 59 | """ 60 | self.tools[name] = { 61 | "name": name, 62 | "description": description, 63 | "inputSchema": input_schema, 64 | } 65 | self.tool_handlers[name] = handler 66 | self.logger.info(f"ツール '{name}' を登録しました") 67 | 68 | def start(self, server_name: str = "mcp-server-python", version: str = "0.1.0", description: str = "Python MCP Server"): 69 | """ 70 | サーバーを起動し、stdioからのリクエストをリッスンします。 71 | 72 | Args: 73 | server_name: サーバー名 74 | version: バージョン 75 | description: 説明 76 | """ 77 | self.logger.info(f"MCPサーバー '{server_name}' を起動しました") 78 | 79 | # サーバー情報を保存(initializeリクエストで使用) 80 | self.server_name = server_name 81 | self.server_version = version 82 | self.server_description = description 83 | 84 | # リクエストをリッスン 85 | while True: 86 | try: 87 | # 標準入力からリクエストを読み込む 88 | request_line = sys.stdin.readline() 89 | if not request_line: 90 | break 91 | 92 | # リクエストをパース 93 | request = json.loads(request_line) 94 | self.logger.info(f"リクエストを受信しました: {request}") 95 | 96 | # リクエストを処理 97 | self._handle_request(request) 98 | 99 | except json.JSONDecodeError: 100 | self.logger.error("JSONのパースに失敗しました") 101 | self._send_error(-32700, "Parse error", None) 102 | 103 | except Exception as e: 104 | self.logger.error(f"エラーが発生しました: {str(e)}") 105 | self._send_error(-32603, f"Internal error: {str(e)}", None) 106 | 107 | def _handle_request(self, request: Dict[str, Any]): 108 | """ 109 | リクエストを処理します。 110 | 111 | Args: 112 | request: JSONリクエスト 113 | """ 114 | # リクエストのバリデーション 115 | if "jsonrpc" not in request or request["jsonrpc"] != "2.0": 116 | self._send_error(-32600, "Invalid Request", request.get("id")) 117 | return 118 | 119 | if "method" not in request: 120 | self._send_error(-32600, "Method not specified", request.get("id")) 121 | return 122 | 123 | # メソッドの取得 124 | method = request["method"] 125 | params = request.get("params", {}) 126 | request_id = request.get("id") 127 | 128 | # メソッドの処理 129 | if method == "initialize": 130 | self._handle_initialize(params, request_id) 131 | elif method == "tools/list": 132 | self._handle_tools_list(request_id) 133 | elif method == "tools/call": 134 | self._handle_tools_call(params, request_id) 135 | elif method == "notifications/initialized": 136 | self._handle_notifications_initialized(params, request_id) 137 | elif method == "resources/list": 138 | self._handle_resources_list(request_id) 139 | elif method == "resources/templates/list": 140 | self._handle_resources_templates_list(request_id) 141 | else: 142 | # 登録されたツールを直接呼び出す 143 | if method in self.tool_handlers: 144 | try: 145 | result = self.tool_handlers[method](params) 146 | self._send_result(result, request_id) 147 | except Exception as e: 148 | self._send_error(-32603, f"Tool execution error: {str(e)}", request_id) 149 | else: 150 | self._send_error(-32601, f"Method not found: {method}", request_id) 151 | 152 | def _handle_initialize(self, params: Dict[str, Any], request_id: Any): 153 | """ 154 | initializeメソッドを処理します。 155 | 156 | Args: 157 | params: リクエストパラメータ 158 | request_id: リクエストID 159 | """ 160 | # クライアント情報を取得(オプション) 161 | client_name = params.get("clientInfo", {}).get("name", "unknown") 162 | client_version = params.get("clientInfo", {}).get("version", "unknown") 163 | 164 | self.logger.info(f"クライアント '{client_name} {client_version}' が接続しました") 165 | 166 | # サーバーの機能を返す 167 | response = { 168 | "protocolVersion": "2024-11-05", 169 | "serverInfo": { 170 | "name": getattr(self, "server_name", "mcp-server-python"), 171 | "version": getattr(self, "server_version", "0.1.0"), 172 | }, 173 | "capabilities": { 174 | "tools": {}, 175 | "resources": {}, 176 | }, 177 | } 178 | 179 | self._send_result(response, request_id) 180 | 181 | def _send_result(self, result: Any, request_id: Any): 182 | """ 183 | 成功レスポンスを送信します。 184 | 185 | Args: 186 | result: レスポンス結果 187 | request_id: リクエストID 188 | """ 189 | response = {"jsonrpc": "2.0", "result": result, "id": request_id} 190 | 191 | self._send_response(response) 192 | 193 | def _send_error(self, code: int, message: str, request_id: Any): 194 | """ 195 | エラーレスポンスを送信します。 196 | 197 | Args: 198 | code: エラーコード 199 | message: エラーメッセージ 200 | request_id: リクエストID 201 | """ 202 | response = {"jsonrpc": "2.0", "error": {"code": code, "message": message}, "id": request_id} 203 | 204 | self._send_response(response) 205 | 206 | def _send_response(self, response: Dict[str, Any]): 207 | """ 208 | レスポンスを標準出力に送信します。 209 | 210 | Args: 211 | response: レスポンス 212 | """ 213 | response_json = json.dumps(response) 214 | print(response_json, flush=True) 215 | self.logger.info(f"レスポンスを送信しました: {response_json}") 216 | 217 | def _get_tools(self) -> List[Dict[str, Any]]: 218 | """ 219 | サーバーが提供するツールの一覧を取得します。 220 | 221 | Returns: 222 | ツールの一覧 223 | """ 224 | return list(self.tools.values()) 225 | 226 | def _handle_tools_call(self, params: Dict[str, Any], request_id: Any): 227 | """ 228 | tools/callメソッドを処理します。 229 | 230 | Args: 231 | params: リクエストパラメータ 232 | request_id: リクエストID 233 | """ 234 | # パラメータのバリデーション 235 | if "name" not in params: 236 | self._send_error(-32602, "Invalid params: name is required", request_id) 237 | return 238 | 239 | if "arguments" not in params: 240 | self._send_error(-32602, "Invalid params: arguments is required", request_id) 241 | return 242 | 243 | tool_name = params["name"] 244 | arguments = params["arguments"] 245 | 246 | # ツールの処理 247 | if tool_name in self.tool_handlers: 248 | try: 249 | result = self.tool_handlers[tool_name](arguments) 250 | if isinstance(result, dict) and "content" in result: 251 | self._send_result(result, request_id) 252 | else: 253 | # 結果をコンテンツ形式に変換 254 | content = [{"type": "text", "text": str(result)}] 255 | self._send_result({"content": content}, request_id) 256 | except Exception as e: 257 | self.logger.error(f"ツール '{tool_name}' の実行中にエラーが発生しました: {str(e)}") 258 | self._send_result( 259 | { 260 | "content": [{"type": "text", "text": f"ツールの実行中にエラーが発生しました: {str(e)}"}], 261 | "isError": True, 262 | }, 263 | request_id, 264 | ) 265 | else: 266 | self._send_result( 267 | {"content": [{"type": "text", "text": f"ツールが見つかりません: {tool_name}"}], "isError": True}, request_id 268 | ) 269 | 270 | def _handle_tools_list(self, request_id: Any): 271 | """ 272 | tools/listメソッドを処理します。 273 | 274 | Args: 275 | request_id: リクエストID 276 | """ 277 | tools = self._get_tools() 278 | self._send_result({"tools": tools}, request_id) 279 | 280 | def _handle_notifications_initialized(self, params: Dict[str, Any], request_id: Any): 281 | """ 282 | notifications/initializedメソッドを処理します。 283 | クライアントの初期化完了通知を処理します。 284 | 285 | Args: 286 | params: リクエストパラメータ 287 | request_id: リクエストID 288 | """ 289 | self.logger.info("クライアントの初期化が完了しました") 290 | # 通知なのでレスポンスは不要 291 | # ただし、エラーが発生した場合はエラーレスポンスを返す必要がある 292 | if request_id is not None: 293 | self._send_result({}, request_id) 294 | 295 | def _handle_resources_list(self, request_id: Any): 296 | """ 297 | resources/listメソッドを処理します。 298 | 利用可能なリソースの一覧を返します。 299 | 300 | Args: 301 | request_id: リクエストID 302 | """ 303 | resources = self._get_resources() 304 | self._send_result({"resources": resources}, request_id) 305 | 306 | def _handle_resources_templates_list(self, request_id: Any): 307 | """ 308 | resources/templates/listメソッドを処理します。 309 | 利用可能なリソーステンプレートの一覧を返します。 310 | 311 | Args: 312 | request_id: リクエストID 313 | """ 314 | templates = self._get_resource_templates() 315 | self._send_result({"templates": templates}, request_id) 316 | 317 | def _get_resources(self) -> List[Dict[str, Any]]: 318 | """ 319 | サーバーが提供するリソースの一覧を取得します。 320 | 321 | Returns: 322 | リソースの一覧 323 | """ 324 | return [] 325 | 326 | def _get_resource_templates(self) -> List[Dict[str, Any]]: 327 | """ 328 | サーバーが提供するリソーステンプレートの一覧を取得します。 329 | 330 | Returns: 331 | リソーステンプレートの一覧 332 | """ 333 | return [] 334 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MCP RAG Server 2 | 3 | MCP RAG Serverは、Model Context Protocol (MCP)に準拠したRAG(Retrieval-Augmented Generation)機能を持つPythonサーバーです。マークダウン、テキスト、パワーポイント、PDFなど複数の形式のドキュメントをデータソースとして、multilingual-e5-largeモデルを使用してインデックス化し、ベクトル検索によって関連情報を取得する機能を提供します。 4 | 5 | ## 概要 6 | 7 | このプロジェクトは、MCPサーバーの基本的な実装に加えて、RAG機能を提供します。複数形式のドキュメントをインデックス化し、自然言語クエリに基づいて関連情報を検索することができます。 8 | 9 | ## 機能 10 | 11 | - **MCPサーバーの基本実装** 12 | - JSON-RPC over stdioベースで動作 13 | - ツールの登録と実行のためのメカニズム 14 | - エラーハンドリングとロギング 15 | 16 | - **RAG機能** 17 | - 複数形式のドキュメント(マークダウン、テキスト、パワーポイント、PDF)の読み込みと解析 18 | - 階層構造を持つソースディレクトリに対応 19 | - markitdownライブラリを使用したパワーポイントやPDFからのマークダウン変換 20 | - 選択可能なエンベディングモデル(multilingual-e5-large、ruriなど)を使用したエンベディング生成 21 | - PostgreSQLのpgvectorを使用したベクトルデータベース 22 | - ベクトル検索による関連情報の取得 23 | - 前後のチャンク取得機能(コンテキストの連続性を確保) 24 | - ドキュメント全文取得機能(完全なコンテキストを提供) 25 | - 差分インデックス化機能(新規・変更ファイルのみを処理) 26 | 27 | - **ツール** 28 | - ベクトル検索ツール(MCP) 29 | - ドキュメント数取得ツール(MCP) 30 | - インデックス管理ツール(CLI) 31 | 32 | ## 前提条件 33 | 34 | - Python 3.10以上 35 | - PostgreSQL 14以上(pgvectorエクステンション付き) 36 | 37 | ## インストール 38 | 39 | ### 依存関係のインストール 40 | 41 | ```bash 42 | # uvがインストールされていない場合は先にインストール 43 | # pip install uv 44 | 45 | # 依存関係のインストール 46 | uv sync 47 | ``` 48 | 49 | ### PostgreSQLとpgvectorのセットアップ 50 | 51 | #### Dockerを使用する場合 52 | 53 | ```bash 54 | # pgvectorを含むPostgreSQLコンテナを起動 55 | docker run --name postgres-pgvector -e POSTGRES_PASSWORD=password -p 5432:5432 -d pgvector/pgvector:pg17 56 | ``` 57 | 58 | #### データベースの作成 59 | 60 | PostgreSQLコンテナを起動した後、以下のコマンドでデータベースを作成します: 61 | 62 | ```bash 63 | # ragdbデータベースの作成 64 | docker exec -it postgres-pgvector psql -U postgres -c "CREATE DATABASE ragdb;" 65 | ``` 66 | 67 | #### 既存のPostgreSQLにpgvectorをインストールする場合 68 | 69 | ```sql 70 | -- pgvectorエクステンションをインストール 71 | CREATE EXTENSION vector; 72 | ``` 73 | 74 | ### 環境変数の設定 75 | 76 | `.env`ファイルを作成し、以下の環境変数を設定します: 77 | 78 | ``` 79 | # PostgreSQL接続情報 80 | POSTGRES_HOST=localhost 81 | POSTGRES_PORT=5432 82 | POSTGRES_USER=postgres 83 | POSTGRES_PASSWORD=password 84 | POSTGRES_DB=ragdb 85 | 86 | # ドキュメントディレクトリ 87 | SOURCE_DIR=./data/source 88 | PROCESSED_DIR=./data/processed 89 | 90 | # エンベディングモデル設定 91 | EMBEDDING_MODEL=intfloat/multilingual-e5-large 92 | EMBEDDING_DIM=1024 93 | EMBEDDING_PREFIX_QUERY="query: " 94 | EMBEDDING_PREFIX_EMBEDDING="passage: " 95 | ``` 96 | 97 | ## エンベディングモデルの設定 98 | 99 | このサーバーでは、環境変数でエンベディングモデルを選択できます。 100 | 101 | ### サポートされているモデル 102 | 103 | #### multilingual-e5-large(デフォルト) 104 | ```env 105 | EMBEDDING_MODEL=intfloat/multilingual-e5-large 106 | EMBEDDING_DIM=1024 107 | EMBEDDING_PREFIX_QUERY="query: " 108 | EMBEDDING_PREFIX_EMBEDDING="passage: " 109 | ``` 110 | 111 | #### cl-nagoya/ruri-v3-30m 112 | ```env 113 | EMBEDDING_MODEL=cl-nagoya/ruri-v3-30m 114 | EMBEDDING_DIM=256 115 | EMBEDDING_PREFIX_QUERY="検索クエリ: " 116 | EMBEDDING_PREFIX_EMBEDDING="検索文書: " 117 | ``` 118 | 119 | ### プレフィックスについて 120 | 121 | 多くのエンベディングモデル(特にE5系)では、テキストの種類に応じてプレフィックスを付けることで性能が向上します: 122 | 123 | - **検索クエリ用**: `EMBEDDING_PREFIX_QUERY` - ユーザーの検索クエリに自動で追加 124 | - **文書用**: `EMBEDDING_PREFIX_EMBEDDING` - インデックス化される文書に自動で追加 125 | 126 | プレフィックスは自動で処理されるため、MCPクライアントは意識する必要がありません。 127 | 128 | ### モデル変更時の注意 129 | 130 | エンベディングモデルを変更した場合は、ベクトル次元が変わる可能性があるため、既存のインデックスをクリアして再作成してください: 131 | 132 | ```bash 133 | python -m src.cli clear 134 | python -m src.cli index 135 | ``` 136 | 137 | ## 使い方 138 | 139 | ### MCPサーバーの起動 140 | 141 | #### uvを使用する場合(推奨) 142 | 143 | ```bash 144 | uv run python -m src.main 145 | ``` 146 | 147 | オプションを指定する場合: 148 | 149 | ```bash 150 | uv run python -m src.main --name "my-rag-server" --version "1.0.0" --description "My RAG Server" 151 | ``` 152 | 153 | #### 通常のPythonを使用する場合 154 | 155 | ```bash 156 | python -m src.main 157 | ``` 158 | 159 | ### コマンドラインツール(CLI)の使用方法 160 | 161 | インデックスのクリアとインデックス化を行うためのコマンドラインツールが用意されています。 162 | 163 | #### ヘルプの表示 164 | 165 | ```bash 166 | python -m src.cli --help 167 | ``` 168 | 169 | #### インデックスのクリア 170 | 171 | ```bash 172 | python -m src.cli clear 173 | ``` 174 | 175 | #### ドキュメントのインデックス化 176 | 177 | ```bash 178 | # デフォルト設定でインデックス化(./data/source ディレクトリ) 179 | python -m src.cli index 180 | 181 | # 特定のディレクトリをインデックス化 182 | python -m src.cli index --directory ./path/to/documents 183 | 184 | # チャンクサイズとオーバーラップを指定してインデックス化 185 | python -m src.cli index --directory ./data/source --chunk-size 300 --chunk-overlap 50 186 | # または短い形式で 187 | python -m src.cli index -d ./data/source -s 300 -o 50 188 | 189 | # 差分インデックス化(新規・変更ファイルのみを処理) 190 | python -m src.cli index --incremental 191 | # または短い形式で 192 | python -m src.cli index -i 193 | ``` 194 | 195 | #### インデックス内のドキュメント数の取得 196 | 197 | ```bash 198 | python -m src.cli count 199 | ``` 200 | 201 | ### MCPホストでの設定 202 | 203 | MCPホスト(Claude Desktop、Cline、Cursorなど)でこのサーバーを使用するには、以下のような設定を行います。設定するjsonファイルについては、各MCPホストの ドキュメントを参照してください。 204 | 205 | #### 設定例 206 | 207 | ```json 208 | { 209 | "mcpServers": { 210 | "mcp-rag-server": { 211 | "command": "uv", 212 | "args": [ 213 | "run", 214 | "--directory", 215 | "/path/to/mcp-rag-server", 216 | "python", 217 | "-m", 218 | "src.main" 219 | ] 220 | } 221 | } 222 | } 223 | ``` 224 | 225 | #### 設定のポイント 226 | 227 | - `command`: `uv`(推奨)または`python` 228 | - `args`: 実行引数の配列 229 | - `/path/to/mcp-rag-server`: このリポジトリの実際のパスに置き換えてください 230 | 231 | #### uvを使用しない場合 232 | 233 | uvがインストールされていない環境では、通常のPythonを使用できます: 234 | 235 | ```json 236 | { 237 | "command": "python", 238 | "args": [ 239 | "-m", 240 | "src.main" 241 | ], 242 | "cwd": "/path/to/mcp-rag-server" 243 | } 244 | ``` 245 | 246 | ## RAGツールの使用方法 247 | 248 | ### search 249 | 250 | ベクトル検索を行います。 251 | 252 | ```json 253 | { 254 | "jsonrpc": "2.0", 255 | "method": "search", 256 | "params": { 257 | "query": "Pythonのジェネレータとは何ですか?", 258 | "limit": 5, 259 | "with_context": true, 260 | "context_size": 1, 261 | "full_document": false 262 | }, 263 | "id": 1 264 | } 265 | ``` 266 | 267 | #### パラメータの説明 268 | 269 | - `query`: 検索クエリ(必須) 270 | - `limit`: 返す結果の数(デフォルト: 5) 271 | - `with_context`: 前後のチャンクも取得するかどうか(デフォルト: true) 272 | - `context_size`: 前後に取得するチャンク数(デフォルト: 1) 273 | - `full_document`: ドキュメント全体を取得するかどうか(デフォルト: false) 274 | 275 | #### 検索結果の改善 276 | 277 | このツールは以下の機能により、より良い検索結果を提供します: 278 | 279 | 1. **前後のチャンク取得機能**: 280 | - 検索でヒットしたチャンクの前後のチャンクも取得して結果に含めます 281 | - `with_context`パラメータで有効/無効を切り替え可能 282 | - `context_size`パラメータで前後に取得するチャンク数を調整可能 283 | 284 | 2. **ドキュメント全文取得機能**: 285 | - 検索でヒットしたドキュメントの全文を取得して結果に含めます 286 | - `full_document`パラメータで有効/無効を切り替え可能 287 | - 特に短いドキュメントや全体の文脈が重要なドキュメントを扱う場合に有用 288 | 289 | 3. **結果の整形改善**: 290 | - 検索結果をファイルごとにグループ化 291 | - 「検索ヒット」「前後のコンテキスト」「ドキュメント全文」を視覚的に区別 292 | - チャンクインデックスでソートして文書の流れを維持 293 | 294 | ### get_document_count 295 | 296 | インデックス内のドキュメント数を取得します。 297 | 298 | ```json 299 | { 300 | "jsonrpc": "2.0", 301 | "method": "get_document_count", 302 | "params": {}, 303 | "id": 2 304 | } 305 | ``` 306 | 307 | ## 使用例 308 | 309 | 1. ドキュメントファイルを `data/source` ディレクトリに配置します。サポートされるファイル形式は以下の通りです: 310 | - マークダウン(.md, .markdown) 311 | - テキスト(.txt) 312 | - パワーポイント(.ppt, .pptx) 313 | - Word(.doc, .docx) 314 | - PDF(.pdf) 315 | 316 | 2. CLIコマンドを使用してドキュメントをインデックス化します: 317 | ```bash 318 | # 初回は全件インデックス化 319 | python -m src.cli index 320 | 321 | # 以降は差分インデックス化で効率的に更新 322 | python -m src.cli index -i 323 | ``` 324 | 325 | 3. MCPサーバーを起動します: 326 | ```bash 327 | uv run python -m src.main 328 | ``` 329 | 330 | 4. `search`ツールを使用して検索を行います。 331 | 332 | ## バックアップと復元 333 | 334 | インデックス化したデータベースを別のPCで使用するには、以下の手順でバックアップと復元を行います。 335 | 336 | ### 最小限のバックアップ(PostgreSQLデータベースのみ) 337 | 338 | 単純に他のPCでRAG検索機能を使いたいだけなら、PostgreSQLデータベースのバックアップだけで十分です。ベクトル化されたデータはすべてデータベースに保存されているためです。 339 | 340 | #### PostgreSQLデータベースのバックアップ 341 | 342 | PostgreSQLデータベースをバックアップするには、Dockerコンテナ内で`pg_dump`コマンドを使用します: 343 | 344 | ```bash 345 | # Dockerコンテナ内でデータベースをバックアップ 346 | docker exec -it postgres-pgvector pg_dump -U postgres -d ragdb -F c -f /tmp/ragdb_backup.dump 347 | 348 | # バックアップファイルをコンテナからホストにコピー 349 | docker cp postgres-pgvector:/tmp/ragdb_backup.dump ./ragdb_backup.dump 350 | ``` 351 | 352 | これにより、PostgreSQLデータベースのバックアップファイル(例:239MB)がカレントディレクトリに作成されます。 353 | 354 | #### 最小限の復元手順 355 | 356 | 1. 新しいPCでPostgreSQLとpgvectorをセットアップします: 357 | 358 | ```bash 359 | # Dockerを使用する場合 360 | docker run --name postgres-pgvector -e POSTGRES_PASSWORD=password -p 5432:5432 -d pgvector/pgvector:pg17 361 | 362 | # データベースを作成 363 | docker exec -it postgres-pgvector psql -U postgres -c "CREATE DATABASE ragdb;" 364 | ``` 365 | 366 | 2. バックアップからデータベースを復元します: 367 | 368 | ```bash 369 | # バックアップファイルをコンテナにコピー 370 | docker cp ./ragdb_backup.dump postgres-pgvector:/tmp/ragdb_backup.dump 371 | 372 | # コンテナ内でデータベースを復元 373 | docker exec -it postgres-pgvector pg_restore -U postgres -d ragdb -c /tmp/ragdb_backup.dump 374 | ``` 375 | 376 | 3. 環境設定を確認します: 377 | 378 | 新しいPCでは、`.env`ファイルのPostgreSQL接続情報が正しく設定されていることを確認してください。 379 | 380 | 4. 動作確認: 381 | 382 | ```bash 383 | python -m src.cli count 384 | ``` 385 | 386 | これにより、インデックス内のドキュメント数が表示されます。元のPCと同じ数が表示されれば、正常に復元されています。 387 | 388 | ### 完全バックアップ(オプション) 389 | 390 | 将来的に新しいドキュメントを追加する予定がある場合や、差分インデックス化機能を使用したい場合は、以下の追加バックアップも行うと良いでしょう: 391 | 392 | #### 処理済みドキュメントのバックアップ 393 | 394 | 処理済みドキュメントディレクトリをバックアップします: 395 | 396 | ```bash 397 | # 処理済みドキュメントディレクトリをZIPファイルにバックアップ 398 | zip -r processed_data_backup.zip data/processed/ 399 | ``` 400 | 401 | #### 環境設定ファイルのバックアップ 402 | 403 | `.env`ファイルをバックアップします: 404 | 405 | ```bash 406 | # .envファイルをコピー 407 | cp .env env_backup.txt 408 | ``` 409 | 410 | #### 完全復元手順 411 | 412 | 1. 前提条件 413 | 414 | 新しいPCには以下のソフトウェアがインストールされている必要があります: 415 | 416 | - Python 3.10以上 417 | - PostgreSQL 14以上(pgvectorエクステンション付き) 418 | - mcp-rag-serverのコードベース 419 | 420 | 2. PostgreSQLデータベースを上記の「最小限の復元手順」で復元します。 421 | 422 | 3. 処理済みドキュメントを復元します: 423 | 424 | ```bash 425 | # ZIPファイルを展開 426 | unzip processed_data_backup.zip -d /path/to/mcp-rag-server/ 427 | ``` 428 | 429 | 4. 環境設定ファイルを復元します: 430 | 431 | ```bash 432 | # .envファイルを復元 433 | cp env_backup.txt /path/to/mcp-rag-server/.env 434 | ``` 435 | 436 | 必要に応じて、新しいPC環境に合わせて`.env`ファイルの設定(特にPostgreSQL接続情報)を編集します。 437 | 438 | 5. 動作確認: 439 | 440 | ```bash 441 | python -m src.cli count 442 | ``` 443 | 444 | ### 注意点 445 | 446 | - PostgreSQLのバージョンとpgvectorのバージョンは、元のPCと新しいPCで互換性がある必要があります。 447 | - 大量のデータがある場合は、バックアップと復元に時間がかかる場合があります。 448 | - 新しいPCでは、必要なPythonパッケージ(`sentence-transformers`、`psycopg2-binary`など)をインストールしておく必要があります。 449 | 450 | ## ディレクトリ構造 451 | 452 | ``` 453 | mcp-rag-server/ 454 | ├── data/ 455 | │ ├── source/ # 原稿ファイル(階層構造対応) 456 | │ │ ├── markdown/ # マークダウンファイル 457 | │ │ ├── docs/ # ドキュメントファイル 458 | │ │ └── slides/ # プレゼンテーションファイル 459 | │ └── processed/ # 処理済みファイル(テキスト抽出済み) 460 | │ └── file_registry.json # 処理済みファイルの情報(差分インデックス用) 461 | ├── docs/ 462 | │ └── design.md # 設計書 463 | ├── logs/ # ログファイル 464 | ├── src/ 465 | │ ├── __init__.py 466 | │ ├── document_processor.py # ドキュメント処理モジュール 467 | │ ├── embedding_generator.py # エンベディング生成モジュール 468 | │ ├── example_tool.py # サンプルツールモジュール 469 | │ ├── main.py # メインエントリーポイント 470 | │ ├── mcp_server.py # MCPサーバーモジュール 471 | │ ├── rag_service.py # RAGサービスモジュール 472 | │ ├── rag_tools.py # RAGツールモジュール 473 | │ └── vector_database.py # ベクトルデータベースモジュール 474 | ├── tests/ 475 | │ ├── __init__.py 476 | │ ├── conftest.py 477 | │ ├── test_document_processor.py 478 | │ ├── test_embedding_generator.py 479 | │ ├── test_example_tool.py 480 | │ ├── test_mcp_server.py 481 | │ ├── test_rag_service.py 482 | │ ├── test_rag_tools.py 483 | │ └── test_vector_database.py 484 | ├── .env # 環境変数設定ファイル 485 | ├── .gitignore 486 | ├── LICENSE 487 | ├── pyproject.toml 488 | └── README.md 489 | ``` 490 | 491 | ## ライセンス 492 | 493 | このプロジェクトはMITライセンスの下で公開されています。詳細は[LICENSE](LICENSE)ファイルを参照してください。 494 | -------------------------------------------------------------------------------- /src/cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | MCP RAG Server CLI 4 | 5 | インデックスのクリアとインデックス化を行うためのコマンドラインインターフェース 6 | """ 7 | 8 | import sys 9 | import os 10 | import argparse 11 | import logging 12 | from pathlib import Path 13 | from dotenv import load_dotenv 14 | 15 | from .rag_tools import create_rag_service_from_env 16 | 17 | 18 | def setup_logging(): 19 | """ 20 | ロギングの設定 21 | """ 22 | # ログディレクトリの作成 23 | os.makedirs("logs", exist_ok=True) 24 | 25 | # ロギングの設定 26 | logging.basicConfig( 27 | level=logging.INFO, 28 | format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", 29 | handlers=[ 30 | logging.StreamHandler(sys.stdout), 31 | logging.FileHandler(os.path.join("logs", "mcp_rag_cli.log"), encoding="utf-8"), 32 | ], 33 | ) 34 | return logging.getLogger("cli") 35 | 36 | 37 | def clear_index(): 38 | """ 39 | インデックスをクリアする 40 | """ 41 | logger = setup_logging() 42 | logger.info("インデックスをクリアしています...") 43 | 44 | # 環境変数の読み込み 45 | load_dotenv() 46 | 47 | # RAGサービスの作成 48 | rag_service = create_rag_service_from_env() 49 | 50 | # 処理済みディレクトリのパス 51 | processed_dir = os.environ.get("PROCESSED_DIR", "data/processed") 52 | 53 | # ファイルレジストリの削除 54 | registry_path = Path(processed_dir) / "file_registry.json" 55 | if registry_path.exists(): 56 | try: 57 | registry_path.unlink() 58 | logger.info(f"ファイルレジストリを削除しました: {registry_path}") 59 | print(f"ファイルレジストリを削除しました: {registry_path}") 60 | except Exception as e: 61 | logger.error(f"ファイルレジストリの削除に失敗しました: {str(e)}") 62 | print(f"ファイルレジストリの削除に失敗しました: {str(e)}") 63 | 64 | # インデックスをクリア 65 | result = rag_service.clear_index() 66 | 67 | if result["success"]: 68 | logger.info(f"インデックスをクリアしました({result['deleted_count']} ドキュメントを削除)") 69 | print(f"インデックスをクリアしました({result['deleted_count']} ドキュメントを削除)") 70 | else: 71 | logger.error(f"インデックスのクリアに失敗しました: {result.get('error', '不明なエラー')}") 72 | print(f"インデックスのクリアに失敗しました: {result.get('error', '不明なエラー')}") 73 | sys.exit(1) 74 | 75 | 76 | def index_documents(directory_path, chunk_size=500, chunk_overlap=100, incremental=False): 77 | """ 78 | ドキュメントをインデックス化する 79 | 80 | Args: 81 | directory_path: インデックス化するドキュメントが含まれるディレクトリのパス 82 | chunk_size: チャンクサイズ(文字数) 83 | chunk_overlap: チャンク間のオーバーラップ(文字数) 84 | incremental: 差分のみをインデックス化するかどうか 85 | """ 86 | logger = setup_logging() 87 | if incremental: 88 | logger.info(f"ディレクトリ '{directory_path}' 内の差分ファイルをインデックス化しています...") 89 | else: 90 | logger.info(f"ディレクトリ '{directory_path}' 内のドキュメントをインデックス化しています...") 91 | 92 | # 環境変数の読み込み 93 | load_dotenv() 94 | 95 | # ディレクトリの存在確認 96 | if not os.path.exists(directory_path): 97 | logger.error(f"ディレクトリ '{directory_path}' が見つかりません") 98 | print(f"エラー: ディレクトリ '{directory_path}' が見つかりません") 99 | sys.exit(1) 100 | 101 | if not os.path.isdir(directory_path): 102 | logger.error(f"'{directory_path}' はディレクトリではありません") 103 | print(f"エラー: '{directory_path}' はディレクトリではありません") 104 | sys.exit(1) 105 | 106 | # RAGサービスの作成 107 | rag_service = create_rag_service_from_env() 108 | 109 | # 処理済みディレクトリのパス 110 | processed_dir = os.environ.get("PROCESSED_DIR", "data/processed") 111 | 112 | # インデックス化を実行 113 | if incremental: 114 | print(f"ディレクトリ '{directory_path}' 内の差分ファイルをインデックス化しています...") 115 | else: 116 | print(f"ディレクトリ '{directory_path}' 内のドキュメントをインデックス化しています...") 117 | 118 | # 進捗状況を表示するためのカウンタ 119 | processed_files = 0 120 | 121 | # 処理前にファイル数を取得 122 | total_files = 0 123 | for root, _, files in os.walk(directory_path): 124 | for file in files: 125 | file_path = os.path.join(root, file) 126 | ext = os.path.splitext(file_path)[1].lower() 127 | if ext in [".md", ".markdown", ".txt", ".pdf", ".ppt", ".pptx", ".doc", ".docx"]: 128 | total_files += 1 129 | 130 | print(f"合計 {total_files} 個のファイルを検索しました...") 131 | 132 | # 元のRAGServiceのindex_documentsメソッドを呼び出す前に、 133 | # DocumentProcessorのprocess_directoryメソッドをオーバーライドして進捗を表示 134 | original_process_directory = rag_service.document_processor.process_directory 135 | 136 | def process_directory_with_progress(source_dir, processed_dir, chunk_size=500, overlap=100, incremental=False): 137 | nonlocal processed_files 138 | results = [] 139 | source_directory = Path(source_dir) 140 | 141 | if not source_directory.exists() or not source_directory.is_dir(): 142 | logger.error(f"ディレクトリ '{source_dir}' が見つからないか、ディレクトリではありません") 143 | raise FileNotFoundError(f"ディレクトリ '{source_dir}' が見つからないか、ディレクトリではありません") 144 | 145 | # サポートするファイル拡張子を全て取得 146 | all_extensions = [] 147 | for ext_list in rag_service.document_processor.SUPPORTED_EXTENSIONS.values(): 148 | all_extensions.extend(ext_list) 149 | 150 | # ファイルを検索 151 | files = [] 152 | for ext in all_extensions: 153 | files.extend(list(source_directory.glob(f"**/*{ext}"))) 154 | 155 | logger.info(f"ディレクトリ '{source_dir}' 内に {len(files)} 個のファイルが見つかりました") 156 | 157 | # 差分処理の場合、ファイルレジストリを読み込む 158 | if incremental: 159 | file_registry = rag_service.document_processor.load_file_registry(processed_dir) 160 | logger.info(f"ファイルレジストリから {len(file_registry)} 個のファイル情報を読み込みました") 161 | 162 | # 処理対象のファイルを特定 163 | files_to_process = [] 164 | for file_path in files: 165 | str_path = str(file_path) 166 | # ファイルのメタデータを取得 167 | current_metadata = rag_service.document_processor.get_file_metadata(str_path) 168 | 169 | # レジストリに存在しない、またはハッシュ値が変更されている場合のみ処理 170 | if ( 171 | str_path not in file_registry 172 | or file_registry[str_path]["hash"] != current_metadata["hash"] 173 | or file_registry[str_path]["mtime"] != current_metadata["mtime"] 174 | or file_registry[str_path]["size"] != current_metadata["size"] 175 | ): 176 | files_to_process.append(file_path) 177 | # レジストリを更新 178 | file_registry[str_path] = current_metadata 179 | 180 | print(f"処理対象のファイル数: {len(files_to_process)} / {len(files)}") 181 | 182 | # 各ファイルを処理 183 | for i, file_path in enumerate(files_to_process): 184 | try: 185 | file_results = rag_service.document_processor.process_file( 186 | str(file_path), processed_dir, chunk_size, overlap 187 | ) 188 | results.extend(file_results) 189 | processed_files += 1 190 | print( 191 | f"処理中... {processed_files}/{len(files_to_process)} ファイル ({(processed_files / len(files_to_process) * 100):.1f}%): {file_path}" 192 | ) 193 | except Exception as e: 194 | logger.error(f"ファイル '{file_path}' の処理中にエラーが発生しました: {str(e)}") 195 | # エラーが発生しても処理を続行 196 | continue 197 | 198 | # ファイルレジストリを保存 199 | rag_service.document_processor.save_file_registry(processed_dir, file_registry) 200 | else: 201 | # 差分処理でない場合は全てのファイルを処理 202 | for i, file_path in enumerate(files): 203 | try: 204 | file_results = rag_service.document_processor.process_file( 205 | str(file_path), processed_dir, chunk_size, overlap 206 | ) 207 | results.extend(file_results) 208 | processed_files += 1 209 | print( 210 | f"処理中... {processed_files}/{total_files} ファイル ({(processed_files / total_files * 100):.1f}%): {file_path}" 211 | ) 212 | except Exception as e: 213 | logger.error(f"ファイル '{file_path}' の処理中にエラーが発生しました: {str(e)}") 214 | # エラーが発生しても処理を続行 215 | continue 216 | 217 | # 全ファイル処理の場合も、新しいレジストリを作成して保存 218 | file_registry = {} 219 | for file_path in files: 220 | str_path = str(file_path) 221 | file_registry[str_path] = rag_service.document_processor.get_file_metadata(str_path) 222 | rag_service.document_processor.save_file_registry(processed_dir, file_registry) 223 | 224 | logger.info(f"ディレクトリ '{source_dir}' 内のファイルを処理しました(合計 {len(results)} チャンク)") 225 | return results 226 | 227 | # 進捗表示付きの処理メソッドに置き換え 228 | rag_service.document_processor.process_directory = process_directory_with_progress 229 | 230 | # インデックス化を実行 231 | result = rag_service.index_documents(directory_path, processed_dir, chunk_size, chunk_overlap, incremental) 232 | 233 | # 元のメソッドに戻す 234 | rag_service.document_processor.process_directory = original_process_directory 235 | 236 | if result["success"]: 237 | incremental_text = "差分" if incremental else "全て" 238 | logger.info( 239 | f"インデックス化が完了しました({incremental_text}のファイルを処理、{result['document_count']} ドキュメント、{result['processing_time']:.2f} 秒)" 240 | ) 241 | print( 242 | f"インデックス化が完了しました({incremental_text}のファイルを処理)\n" 243 | f"- ドキュメント数: {result['document_count']}\n" 244 | f"- 処理時間: {result['processing_time']:.2f} 秒\n" 245 | f"- メッセージ: {result.get('message', '')}" 246 | ) 247 | else: 248 | logger.error(f"インデックス化に失敗しました: {result.get('error', '不明なエラー')}") 249 | print( 250 | f"インデックス化に失敗しました\n" 251 | f"- エラー: {result.get('error', '不明なエラー')}\n" 252 | f"- 処理時間: {result['processing_time']:.2f} 秒" 253 | ) 254 | sys.exit(1) 255 | 256 | 257 | def get_document_count(): 258 | """ 259 | インデックス内のドキュメント数を取得する 260 | """ 261 | logger = setup_logging() 262 | logger.info("インデックス内のドキュメント数を取得しています...") 263 | 264 | # 環境変数の読み込み 265 | load_dotenv() 266 | 267 | # RAGサービスの作成 268 | rag_service = create_rag_service_from_env() 269 | 270 | # ドキュメント数を取得 271 | try: 272 | count = rag_service.get_document_count() 273 | logger.info(f"インデックス内のドキュメント数: {count}") 274 | print(f"インデックス内のドキュメント数: {count}") 275 | except Exception as e: 276 | logger.error(f"ドキュメント数の取得中にエラーが発生しました: {str(e)}") 277 | print(f"ドキュメント数の取得中にエラーが発生しました: {str(e)}") 278 | sys.exit(1) 279 | 280 | 281 | def main(): 282 | """ 283 | メイン関数 284 | 285 | コマンドライン引数を解析し、適切な処理を実行します。 286 | """ 287 | # コマンドライン引数の解析 288 | parser = argparse.ArgumentParser( 289 | description="MCP RAG Server CLI - インデックスのクリアとインデックス化を行うためのコマンドラインインターフェース" 290 | ) 291 | subparsers = parser.add_subparsers(dest="command", help="実行するコマンド") 292 | 293 | # clearコマンド 294 | subparsers.add_parser("clear", help="インデックスをクリアする") 295 | 296 | # indexコマンド 297 | index_parser = subparsers.add_parser("index", help="ドキュメントをインデックス化する") 298 | index_parser.add_argument( 299 | "--directory", 300 | "-d", 301 | default=os.environ.get("SOURCE_DIR", "./data/source"), 302 | help="インデックス化するドキュメントが含まれるディレクトリのパス", 303 | ) 304 | index_parser.add_argument("--chunk-size", "-s", type=int, default=500, help="チャンクサイズ(文字数)") 305 | index_parser.add_argument("--chunk-overlap", "-o", type=int, default=100, help="チャンク間のオーバーラップ(文字数)") 306 | index_parser.add_argument("--incremental", "-i", action="store_true", help="差分のみをインデックス化する") 307 | 308 | # countコマンド 309 | subparsers.add_parser("count", help="インデックス内のドキュメント数を取得する") 310 | 311 | args = parser.parse_args() 312 | 313 | # コマンドに応じた処理を実行 314 | if args.command == "clear": 315 | clear_index() 316 | elif args.command == "index": 317 | index_documents(args.directory, args.chunk_size, args.chunk_overlap, args.incremental) 318 | elif args.command == "count": 319 | get_document_count() 320 | else: 321 | parser.print_help() 322 | sys.exit(1) 323 | 324 | 325 | if __name__ == "__main__": 326 | main() 327 | -------------------------------------------------------------------------------- /src/rag_service.py: -------------------------------------------------------------------------------- 1 | """ 2 | RAGサービスモジュール 3 | 4 | ドキュメント処理、エンベディング生成、ベクトルデータベースを統合して、 5 | インデックス化と検索の機能を提供します。 6 | """ 7 | 8 | import os 9 | import time 10 | import logging 11 | from typing import List, Dict, Any 12 | 13 | from .document_processor import DocumentProcessor 14 | from .embedding_generator import EmbeddingGenerator 15 | from .vector_database import VectorDatabase 16 | 17 | 18 | class RAGService: 19 | """ 20 | RAGサービスクラス 21 | 22 | ドキュメント処理、エンベディング生成、ベクトルデータベースを統合して、 23 | インデックス化と検索の機能を提供します。 24 | 25 | Attributes: 26 | document_processor: ドキュメント処理クラスのインスタンス 27 | embedding_generator: エンベディング生成クラスのインスタンス 28 | vector_database: ベクトルデータベースクラスのインスタンス 29 | logger: ロガー 30 | """ 31 | 32 | def __init__( 33 | self, document_processor: DocumentProcessor, embedding_generator: EmbeddingGenerator, vector_database: VectorDatabase 34 | ): 35 | """ 36 | RAGServiceのコンストラクタ 37 | 38 | Args: 39 | document_processor: ドキュメント処理クラスのインスタンス 40 | embedding_generator: エンベディング生成クラスのインスタンス 41 | vector_database: ベクトルデータベースクラスのインスタンス 42 | """ 43 | # ロガーの設定 44 | self.logger = logging.getLogger("rag_service") 45 | self.logger.setLevel(logging.INFO) 46 | 47 | # コンポーネントの設定 48 | self.document_processor = document_processor 49 | self.embedding_generator = embedding_generator 50 | self.vector_database = vector_database 51 | 52 | # データベースの初期化 53 | try: 54 | self.vector_database.initialize_database() 55 | except Exception as e: 56 | self.logger.error(f"データベースの初期化に失敗しました: {str(e)}") 57 | raise 58 | 59 | def index_documents( 60 | self, 61 | source_dir: str, 62 | processed_dir: str = None, 63 | chunk_size: int = 500, 64 | chunk_overlap: int = 100, 65 | incremental: bool = False, 66 | ) -> Dict[str, Any]: 67 | """ 68 | ディレクトリ内のファイルをインデックス化します。 69 | 70 | Args: 71 | source_dir: インデックス化するファイルが含まれるディレクトリのパス 72 | processed_dir: 処理済みファイルを保存するディレクトリのパス(指定がない場合はdata/processed) 73 | chunk_size: チャンクサイズ(文字数) 74 | chunk_overlap: チャンク間のオーバーラップ(文字数) 75 | incremental: 差分のみをインデックス化するかどうか 76 | 77 | Returns: 78 | インデックス化の結果 79 | - document_count: インデックス化されたドキュメント数 80 | - processing_time: 処理時間(秒) 81 | - success: 成功したかどうか 82 | - error: エラーメッセージ(エラーが発生した場合) 83 | """ 84 | start_time = time.time() 85 | document_count = 0 86 | 87 | # 処理済みディレクトリのデフォルト値 88 | if processed_dir is None: 89 | processed_dir = "data/processed" 90 | 91 | try: 92 | # ディレクトリ内のファイルを処理 93 | if incremental: 94 | self.logger.info(f"ディレクトリ '{source_dir}' 内の差分ファイルをインデックス化しています...") 95 | else: 96 | self.logger.info(f"ディレクトリ '{source_dir}' 内のファイルをインデックス化しています...") 97 | 98 | chunks = self.document_processor.process_directory( 99 | source_dir, processed_dir, chunk_size, chunk_overlap, incremental 100 | ) 101 | 102 | if not chunks: 103 | self.logger.warning(f"ディレクトリ '{source_dir}' 内に処理可能なファイルが見つかりませんでした") 104 | return { 105 | "document_count": 0, 106 | "processing_time": time.time() - start_time, 107 | "success": True, 108 | "message": f"ディレクトリ '{source_dir}' 内に処理可能なファイルが見つかりませんでした", 109 | } 110 | 111 | # チャンクのコンテンツからエンベディングを生成 112 | self.logger.info(f"{len(chunks)} チャンクのエンベディングを生成しています...") 113 | texts = [chunk["content"] for chunk in chunks] 114 | embeddings = self.embedding_generator.generate_embeddings(texts) 115 | 116 | # ドキュメントをデータベースに挿入 117 | self.logger.info(f"{len(chunks)} チャンクをデータベースに挿入しています...") 118 | documents = [] 119 | for i, chunk in enumerate(chunks): 120 | documents.append( 121 | { 122 | "document_id": chunk["document_id"], 123 | "content": chunk["content"], 124 | "file_path": chunk["file_path"], 125 | "chunk_index": chunk["chunk_index"], 126 | "embedding": embeddings[i], 127 | "metadata": { 128 | "file_name": os.path.basename(chunk["file_path"]), 129 | "directory": os.path.dirname(chunk["file_path"]), 130 | "original_file_path": chunk.get("original_file_path", ""), 131 | "directory_suffix": chunk.get("metadata", {}).get("directory_suffix", ""), 132 | }, 133 | } 134 | ) 135 | 136 | self.vector_database.batch_insert_documents(documents) 137 | document_count = len(documents) 138 | 139 | processing_time = time.time() - start_time 140 | self.logger.info(f"インデックス化が完了しました({document_count} ドキュメント、{processing_time:.2f} 秒)") 141 | 142 | return { 143 | "document_count": document_count, 144 | "processing_time": processing_time, 145 | "success": True, 146 | "message": f"{document_count} ドキュメントをインデックス化しました", 147 | } 148 | 149 | except Exception as e: 150 | processing_time = time.time() - start_time 151 | self.logger.error(f"インデックス化中にエラーが発生しました: {str(e)}") 152 | 153 | return {"document_count": document_count, "processing_time": processing_time, "success": False, "error": str(e)} 154 | 155 | def search( 156 | self, query: str, limit: int = 5, with_context: bool = False, context_size: int = 1, full_document: bool = False 157 | ) -> List[Dict[str, Any]]: 158 | """ 159 | ベクトル検索を行います。 160 | 161 | Args: 162 | query: 検索クエリ 163 | limit: 返す結果の数(デフォルト: 5) 164 | with_context: 前後のチャンクも取得するかどうか(デフォルト: False) 165 | context_size: 前後に取得するチャンク数(デフォルト: 1) 166 | full_document: ドキュメント全体を取得するかどうか(デフォルト: False) 167 | 168 | Returns: 169 | 検索結果のリスト(関連度順) 170 | - document_id: ドキュメントID 171 | - content: コンテンツ 172 | - file_path: ファイルパス 173 | - similarity: 類似度 174 | - metadata: メタデータ 175 | - is_context: コンテキストチャンクかどうか(前後のチャンクの場合はTrue) 176 | - is_full_document: 全文ドキュメントかどうか(ドキュメント全体の場合はTrue) 177 | """ 178 | try: 179 | # クエリからエンベディングを生成 180 | self.logger.info(f"クエリ '{query}' のエンベディングを生成しています...") 181 | query_embedding = self.embedding_generator.generate_search_embedding(query) 182 | 183 | # ベクトル検索 184 | self.logger.info(f"クエリ '{query}' でベクトル検索を実行しています...") 185 | results = self.vector_database.search(query_embedding, limit) 186 | 187 | # 前後のチャンクも取得する場合 188 | if with_context and context_size > 0: 189 | context_results = [] 190 | processed_files = set() # 処理済みのファイルとチャンクの組み合わせを記録 191 | 192 | for result in results: 193 | file_path = result["file_path"] 194 | chunk_index = result["chunk_index"] 195 | file_chunk_key = f"{file_path}_{chunk_index}" 196 | 197 | # 既に処理済みのファイルとチャンクの組み合わせはスキップ 198 | if file_chunk_key in processed_files: 199 | continue 200 | 201 | processed_files.add(file_chunk_key) 202 | 203 | # 前後のチャンクを取得 204 | adjacent_chunks = self.vector_database.get_adjacent_chunks(file_path, chunk_index, context_size) 205 | context_results.extend(adjacent_chunks) 206 | 207 | # 結果をマージ 208 | all_results = results.copy() 209 | 210 | # 重複を避けるために、既に結果に含まれているドキュメントIDを記録 211 | existing_doc_ids = {result["document_id"] for result in all_results} 212 | 213 | # 重複していないコンテキストチャンクのみを追加 214 | for context in context_results: 215 | if context["document_id"] not in existing_doc_ids: 216 | all_results.append(context) 217 | existing_doc_ids.add(context["document_id"]) 218 | 219 | # ファイルパスとチャンクインデックスでソート 220 | all_results.sort(key=lambda x: (x["file_path"], x["chunk_index"])) 221 | 222 | self.logger.info(f"検索結果(コンテキスト含む): {len(all_results)} 件") 223 | 224 | # ドキュメント全体を取得する場合 225 | if full_document: 226 | full_doc_results = [] 227 | processed_files = set() # 処理済みのファイルを記録 228 | 229 | # 検索結果に含まれるファイルの全文を取得 230 | for result in all_results: 231 | file_path = result["file_path"] 232 | 233 | # 既に処理済みのファイルはスキップ 234 | if file_path in processed_files: 235 | continue 236 | 237 | processed_files.add(file_path) 238 | 239 | # ファイルの全文を取得 240 | full_doc_chunks = self.vector_database.get_document_by_file_path(file_path) 241 | full_doc_results.extend(full_doc_chunks) 242 | 243 | # 結果をマージ 244 | merged_results = all_results.copy() 245 | 246 | # 重複を避けるために、既に結果に含まれているドキュメントIDを記録 247 | existing_doc_ids = {result["document_id"] for result in merged_results} 248 | 249 | # 重複していない全文チャンクのみを追加 250 | for doc_chunk in full_doc_results: 251 | if doc_chunk["document_id"] not in existing_doc_ids: 252 | merged_results.append(doc_chunk) 253 | existing_doc_ids.add(doc_chunk["document_id"]) 254 | 255 | # ファイルパスとチャンクインデックスでソート 256 | merged_results.sort(key=lambda x: (x["file_path"], x["chunk_index"])) 257 | 258 | self.logger.info(f"検索結果(全文含む): {len(merged_results)} 件") 259 | return merged_results 260 | else: 261 | return all_results 262 | else: 263 | # ドキュメント全体を取得する場合 264 | if full_document: 265 | full_doc_results = [] 266 | processed_files = set() # 処理済みのファイルを記録 267 | 268 | # 検索結果に含まれるファイルの全文を取得 269 | for result in results: 270 | file_path = result["file_path"] 271 | 272 | # 既に処理済みのファイルはスキップ 273 | if file_path in processed_files: 274 | continue 275 | 276 | processed_files.add(file_path) 277 | 278 | # ファイルの全文を取得 279 | full_doc_chunks = self.vector_database.get_document_by_file_path(file_path) 280 | full_doc_results.extend(full_doc_chunks) 281 | 282 | # 結果をマージ 283 | merged_results = results.copy() 284 | 285 | # 重複を避けるために、既に結果に含まれているドキュメントIDを記録 286 | existing_doc_ids = {result["document_id"] for result in merged_results} 287 | 288 | # 重複していない全文チャンクのみを追加 289 | for doc_chunk in full_doc_results: 290 | if doc_chunk["document_id"] not in existing_doc_ids: 291 | merged_results.append(doc_chunk) 292 | existing_doc_ids.add(doc_chunk["document_id"]) 293 | 294 | # ファイルパスとチャンクインデックスでソート 295 | merged_results.sort(key=lambda x: (x["file_path"], x["chunk_index"])) 296 | 297 | self.logger.info(f"検索結果(全文含む): {len(merged_results)} 件") 298 | return merged_results 299 | else: 300 | self.logger.info(f"検索結果: {len(results)} 件") 301 | return results 302 | 303 | except Exception as e: 304 | self.logger.error(f"検索中にエラーが発生しました: {str(e)}") 305 | raise 306 | 307 | def clear_index(self) -> Dict[str, Any]: 308 | """ 309 | インデックスをクリアします。 310 | 311 | Returns: 312 | クリアの結果 313 | - deleted_count: 削除されたドキュメント数 314 | - success: 成功したかどうか 315 | - error: エラーメッセージ(エラーが発生した場合) 316 | """ 317 | try: 318 | # データベースをクリア 319 | self.logger.info("インデックスをクリアしています...") 320 | deleted_count = self.vector_database.clear_database() 321 | 322 | self.logger.info(f"インデックスをクリアしました({deleted_count} ドキュメントを削除)") 323 | return {"deleted_count": deleted_count, "success": True, "message": f"{deleted_count} ドキュメントを削除しました"} 324 | 325 | except Exception as e: 326 | self.logger.error(f"インデックスのクリア中にエラーが発生しました: {str(e)}") 327 | 328 | return {"deleted_count": 0, "success": False, "error": str(e)} 329 | 330 | def get_document_count(self) -> int: 331 | """ 332 | インデックス内のドキュメント数を取得します。 333 | 334 | Returns: 335 | ドキュメント数 336 | """ 337 | try: 338 | # ドキュメント数を取得 339 | count = self.vector_database.get_document_count() 340 | self.logger.info(f"インデックス内のドキュメント数: {count}") 341 | return count 342 | 343 | except Exception as e: 344 | self.logger.error(f"ドキュメント数の取得中にエラーが発生しました: {str(e)}") 345 | raise 346 | -------------------------------------------------------------------------------- /src/document_processor.py: -------------------------------------------------------------------------------- 1 | """ 2 | ドキュメント処理モジュール 3 | 4 | マークダウン、テキスト、パワーポイント、PDFなどのファイルの読み込みと解析、チャンク分割を行います。 5 | """ 6 | 7 | import logging 8 | import os 9 | import json 10 | from pathlib import Path 11 | from typing import List, Dict, Any 12 | import hashlib 13 | import time 14 | 15 | import markitdown 16 | 17 | 18 | class DocumentProcessor: 19 | """ 20 | ドキュメント処理クラス 21 | 22 | マークダウン、テキスト、パワーポイント、PDFなどのファイルの読み込みと解析、チャンク分割を行います。 23 | 24 | Attributes: 25 | logger: ロガー 26 | """ 27 | 28 | # サポートするファイル拡張子 29 | SUPPORTED_EXTENSIONS = { 30 | "text": [".txt", ".md", ".markdown"], 31 | "office": [".ppt", ".pptx", ".doc", ".docx"], 32 | "pdf": [".pdf"], 33 | } 34 | 35 | def __init__(self): 36 | """ 37 | DocumentProcessorのコンストラクタ 38 | """ 39 | # ロガーの設定 40 | self.logger = logging.getLogger("document_processor") 41 | self.logger.setLevel(logging.INFO) 42 | 43 | def read_file(self, file_path: str) -> str: 44 | """ 45 | ファイルを読み込みます。 46 | 47 | Args: 48 | file_path: ファイルのパス 49 | 50 | Returns: 51 | ファイルの内容 52 | 53 | Raises: 54 | FileNotFoundError: ファイルが見つからない場合 55 | IOError: ファイルの読み込みに失敗した場合 56 | """ 57 | try: 58 | # ファイル拡張子を取得 59 | ext = Path(file_path).suffix.lower() 60 | 61 | # テキストファイル(マークダウン含む)の場合 62 | if ext in self.SUPPORTED_EXTENSIONS["text"]: 63 | with open(file_path, "r", encoding="utf-8") as f: 64 | content = f.read() 65 | # NUL文字を削除 66 | content = content.replace("\x00", "") 67 | self.logger.info(f"テキストファイル '{file_path}' を読み込みました") 68 | return content 69 | 70 | # パワーポイント、Word、PDFの場合はmarkitdownを使用して変換 71 | elif ext in self.SUPPORTED_EXTENSIONS["office"] or ext in self.SUPPORTED_EXTENSIONS["pdf"]: 72 | return self.convert_to_markdown(file_path) 73 | 74 | # サポートしていない拡張子の場合 75 | else: 76 | self.logger.warning(f"サポートしていないファイル形式です: {file_path}") 77 | return "" 78 | 79 | except FileNotFoundError: 80 | self.logger.error(f"ファイル '{file_path}' が見つかりません") 81 | raise 82 | except IOError as e: 83 | self.logger.error(f"ファイル '{file_path}' の読み込みに失敗しました: {str(e)}") 84 | raise 85 | 86 | def convert_to_markdown(self, file_path: str) -> str: 87 | """ 88 | パワーポイント、Word、PDFなどのファイルをマークダウンに変換します。 89 | 90 | Args: 91 | file_path: ファイルのパス 92 | 93 | Returns: 94 | マークダウンに変換された内容 95 | 96 | Raises: 97 | Exception: 変換に失敗した場合 98 | """ 99 | try: 100 | # ファイルURIを作成 101 | file_uri = f"file://{os.path.abspath(file_path)}" 102 | 103 | # markitdownを使用して変換 104 | markdown_content = markitdown.MarkItDown().convert_uri(file_uri).markdown 105 | # NUL文字を削除 106 | markdown_content = markdown_content.replace("\x00", "") 107 | 108 | self.logger.info(f"ファイル '{file_path}' をマークダウンに変換しました") 109 | return markdown_content 110 | except Exception as e: 111 | self.logger.error(f"ファイル '{file_path}' のマークダウン変換に失敗しました: {str(e)}") 112 | raise 113 | 114 | def split_into_chunks(self, text: str, chunk_size: int = 500, overlap: int = 100) -> List[str]: 115 | """ 116 | テキストをチャンクに分割します。 117 | 118 | Args: 119 | text: 分割するテキスト 120 | chunk_size: チャンクサイズ(文字数) 121 | overlap: チャンク間のオーバーラップ(文字数) 122 | 123 | Returns: 124 | チャンクのリスト 125 | """ 126 | if not text: 127 | return [] 128 | 129 | chunks = [] 130 | start = 0 131 | text_length = len(text) 132 | 133 | while start < text_length: 134 | end = min(start + chunk_size, text_length) 135 | 136 | # 文の途中で切らないように調整 137 | if end < text_length: 138 | # 次の改行または句点を探す 139 | next_newline = text.find("\n", end) 140 | next_period = text.find("。", end) 141 | 142 | if next_newline != -1 and (next_period == -1 or next_newline < next_period): 143 | end = next_newline + 1 # 改行を含める 144 | elif next_period != -1: 145 | end = next_period + 1 # 句点を含める 146 | 147 | chunks.append(text[start:end]) 148 | start = end - overlap if end - overlap > start else end 149 | 150 | # 終了条件 151 | if start >= text_length: 152 | break 153 | 154 | self.logger.info(f"テキストを {len(chunks)} チャンクに分割しました") 155 | return chunks 156 | 157 | def calculate_file_hash(self, file_path: str) -> str: 158 | """ 159 | ファイルのハッシュ値を計算します。 160 | 161 | Args: 162 | file_path: ファイルのパス 163 | 164 | Returns: 165 | ファイルのSHA-256ハッシュ値 166 | """ 167 | try: 168 | with open(file_path, "rb") as f: 169 | file_hash = hashlib.sha256(f.read()).hexdigest() 170 | return file_hash 171 | except Exception as e: 172 | self.logger.error(f"ファイル '{file_path}' のハッシュ計算に失敗しました: {str(e)}") 173 | # エラーが発生した場合は、タイムスタンプをハッシュとして使用 174 | return f"timestamp-{int(time.time())}" 175 | 176 | def get_file_metadata(self, file_path: str) -> Dict[str, Any]: 177 | """ 178 | ファイルのメタデータを取得します。 179 | 180 | Args: 181 | file_path: ファイルのパス 182 | 183 | Returns: 184 | ファイルのメタデータ(ハッシュ値、最終更新日時など) 185 | """ 186 | file_stat = os.stat(file_path) 187 | return { 188 | "hash": self.calculate_file_hash(file_path), 189 | "mtime": file_stat.st_mtime, 190 | "size": file_stat.st_size, 191 | "path": file_path, 192 | } 193 | 194 | def load_file_registry(self, processed_dir: str) -> Dict[str, Dict[str, Any]]: 195 | """ 196 | 処理済みファイルのレジストリを読み込みます。 197 | 198 | Args: 199 | processed_dir: 処理済みファイルを保存するディレクトリのパス 200 | 201 | Returns: 202 | 処理済みファイルのレジストリ(ファイルパスをキーとするメタデータの辞書) 203 | """ 204 | registry_path = Path(processed_dir) / "file_registry.json" 205 | if not registry_path.exists(): 206 | return {} 207 | 208 | try: 209 | with open(registry_path, "r", encoding="utf-8") as f: 210 | return json.load(f) 211 | except Exception as e: 212 | self.logger.error(f"ファイルレジストリの読み込みに失敗しました: {str(e)}") 213 | return {} 214 | 215 | def save_file_registry(self, processed_dir: str, registry: Dict[str, Dict[str, Any]]) -> None: 216 | """ 217 | 処理済みファイルのレジストリを保存します。 218 | 219 | Args: 220 | processed_dir: 処理済みファイルを保存するディレクトリのパス 221 | registry: 処理済みファイルのレジストリ 222 | """ 223 | registry_path = Path(processed_dir) / "file_registry.json" 224 | try: 225 | # 処理済みディレクトリが存在しない場合は作成 226 | os.makedirs(Path(processed_dir), exist_ok=True) 227 | 228 | with open(registry_path, "w", encoding="utf-8") as f: 229 | json.dump(registry, f, ensure_ascii=False, indent=2) 230 | self.logger.info(f"ファイルレジストリを保存しました: {registry_path}") 231 | except Exception as e: 232 | self.logger.error(f"ファイルレジストリの保存に失敗しました: {str(e)}") 233 | 234 | def process_file( 235 | self, file_path: str, processed_dir: str, chunk_size: int = 500, overlap: int = 100 236 | ) -> List[Dict[str, Any]]: 237 | """ 238 | ファイルを処理します。 239 | 240 | Args: 241 | file_path: ファイルのパス 242 | processed_dir: 処理済みファイルを保存するディレクトリのパス 243 | chunk_size: チャンクサイズ(文字数) 244 | overlap: チャンク間のオーバーラップ(文字数) 245 | 246 | Returns: 247 | 処理結果のリスト(各要素はチャンク情報を含む辞書) 248 | """ 249 | try: 250 | # ファイルを読み込む 251 | content = self.read_file(file_path) 252 | if not content: 253 | return [] 254 | 255 | # ファイルパスからディレクトリ構造を取得 256 | file_path_obj = Path(file_path) 257 | relative_path = file_path_obj.relative_to(Path(file_path_obj.parts[0]) / Path(file_path_obj.parts[1])) 258 | parent_dirs = relative_path.parent.parts 259 | 260 | # ディレクトリ名をサフィックスとして使用 261 | dir_suffix = "_".join(parent_dirs) if parent_dirs else "" 262 | 263 | # 処理済みファイル名を生成 264 | processed_file_name = f"{file_path_obj.stem}{('_' + dir_suffix) if dir_suffix else ''}.md" 265 | processed_file_path = Path(processed_dir) / processed_file_name 266 | 267 | # 処理済みディレクトリが存在しない場合は作成 268 | os.makedirs(Path(processed_dir), exist_ok=True) 269 | 270 | # 処理済みファイルに書き込む 271 | with open(processed_file_path, "w", encoding="utf-8") as f: 272 | f.write(content) 273 | 274 | self.logger.info(f"処理済みファイルを保存しました: {processed_file_path}") 275 | 276 | # チャンクに分割 277 | chunks = self.split_into_chunks(content, chunk_size, overlap) 278 | 279 | # 結果を作成 280 | results = [] 281 | for i, chunk in enumerate(chunks): 282 | document_id = f"{processed_file_name}_{i}" 283 | results.append( 284 | { 285 | "document_id": document_id, 286 | "content": chunk, 287 | "file_path": str(processed_file_path), 288 | "original_file_path": file_path, 289 | "chunk_index": i, 290 | "metadata": { 291 | "file_name": file_path_obj.name, 292 | "directory": str(file_path_obj.parent), 293 | "directory_suffix": dir_suffix, 294 | }, 295 | } 296 | ) 297 | 298 | self.logger.info(f"ファイル '{file_path}' を処理しました({len(results)} チャンク)") 299 | return results 300 | 301 | except Exception as e: 302 | self.logger.error(f"ファイル '{file_path}' の処理中にエラーが発生しました: {str(e)}") 303 | raise 304 | 305 | def process_directory( 306 | self, source_dir: str, processed_dir: str, chunk_size: int = 500, overlap: int = 100, incremental: bool = False 307 | ) -> List[Dict[str, Any]]: 308 | """ 309 | ディレクトリ内のファイルを処理します。 310 | 311 | Args: 312 | source_dir: 原稿ファイルが含まれるディレクトリのパス 313 | processed_dir: 処理済みファイルを保存するディレクトリのパス 314 | chunk_size: チャンクサイズ(文字数) 315 | overlap: チャンク間のオーバーラップ(文字数) 316 | incremental: 差分のみを処理するかどうか 317 | 318 | Returns: 319 | 処理結果のリスト(各要素はチャンク情報を含む辞書) 320 | """ 321 | results = [] 322 | source_directory = Path(source_dir) 323 | 324 | if not source_directory.exists() or not source_directory.is_dir(): 325 | self.logger.error(f"ディレクトリ '{source_dir}' が見つからないか、ディレクトリではありません") 326 | raise FileNotFoundError(f"ディレクトリ '{source_dir}' が見つからないか、ディレクトリではありません") 327 | 328 | # サポートするファイル拡張子を全て取得 329 | all_extensions = [] 330 | for ext_list in self.SUPPORTED_EXTENSIONS.values(): 331 | all_extensions.extend(ext_list) 332 | 333 | # ファイルを検索 334 | files = [] 335 | for ext in all_extensions: 336 | files.extend(list(source_directory.glob(f"**/*{ext}"))) 337 | 338 | self.logger.info(f"ディレクトリ '{source_dir}' 内に {len(files)} 個のファイルが見つかりました") 339 | 340 | # 差分処理の場合、ファイルレジストリを読み込む 341 | if incremental: 342 | file_registry = self.load_file_registry(processed_dir) 343 | self.logger.info(f"ファイルレジストリから {len(file_registry)} 個のファイル情報を読み込みました") 344 | else: 345 | file_registry = {} 346 | 347 | # 処理対象のファイルを特定 348 | files_to_process = [] 349 | for file_path in files: 350 | str_path = str(file_path) 351 | if incremental: 352 | # ファイルのメタデータを取得 353 | current_metadata = self.get_file_metadata(str_path) 354 | 355 | # レジストリに存在しない、またはハッシュ値が変更されている場合のみ処理 356 | if ( 357 | str_path not in file_registry 358 | or file_registry[str_path]["hash"] != current_metadata["hash"] 359 | or file_registry[str_path]["mtime"] != current_metadata["mtime"] 360 | or file_registry[str_path]["size"] != current_metadata["size"] 361 | ): 362 | files_to_process.append(file_path) 363 | # レジストリを更新 364 | file_registry[str_path] = current_metadata 365 | else: 366 | # 差分処理でない場合は全てのファイルを処理 367 | files_to_process.append(file_path) 368 | # レジストリを更新 369 | file_registry[str_path] = self.get_file_metadata(str_path) 370 | 371 | self.logger.info(f"処理対象のファイル数: {len(files_to_process)} / {len(files)}") 372 | 373 | # 各ファイルを処理 374 | for file_path in files_to_process: 375 | try: 376 | file_results = self.process_file(str(file_path), processed_dir, chunk_size, overlap) 377 | results.extend(file_results) 378 | except Exception as e: 379 | self.logger.error(f"ファイル '{file_path}' の処理中にエラーが発生しました: {str(e)}") 380 | # エラーが発生しても処理を続行 381 | continue 382 | 383 | # ファイルレジストリを保存 384 | self.save_file_registry(processed_dir, file_registry) 385 | 386 | self.logger.info(f"ディレクトリ '{source_dir}' 内のファイルを処理しました(合計 {len(results)} チャンク)") 387 | return results 388 | -------------------------------------------------------------------------------- /docs/design.md: -------------------------------------------------------------------------------- 1 | # 要件・設計書 2 | 3 | ## 1. 要件定義 4 | 5 | ### 1.1 基本情報 6 | - ソフトウェア名称: MCP RAG Server 7 | - リポジトリ名: mcp-rag-server 8 | 9 | ### 1.2 プロジェクト概要 10 | 11 | 本プロジェクトは、Model Context Protocol (MCP)に準拠したRAG(Retrieval-Augmented Generation)機能を持つPythonサーバーを提供することを目的とする。マークダウンファイル、テキストファイル、パワーポイント、PDFなど複数の形式のドキュメントをデータソースとして、選択可能なエンベディングモデル(multilingual-e5-large、ruriなど)を使用してインデックス化し、ベクトル検索によって関連情報を取得する機能を提供する。 12 | 13 | ### 1.3 機能要件 14 | 15 | #### 1.3.1 MCPサーバーの基本実装 16 | - JSON-RPC over stdioベースで動作 17 | - ツールの登録と実行のためのメカニズム 18 | - エラーハンドリングとロギング 19 | 20 | #### 1.3.2 RAG機能 21 | - 複数形式のドキュメント(マークダウン、テキスト、パワーポイント、PDF)の読み込みと解析 22 | - 階層構造を持つソースディレクトリに対応 23 | - Markitdownを使用したパワーポイントやPDFからのマークダウン変換 24 | - 選択可能なエンベディングモデル(multilingual-e5-large、ruriなど)を使用したエンベディング生成 25 | - PostgreSQLのpgvectorを使用したベクトルデータベース 26 | - ベクトル検索による関連情報の取得 27 | - 前後のチャンク取得機能(コンテキストの連続性を確保) 28 | - ドキュメント全文取得機能(完全なコンテキストを提供) 29 | - 差分インデックス化機能(新規・変更ファイルのみを処理) 30 | 31 | #### 1.3.3 ツール 32 | - ベクトル検索ツール(MCP) 33 | - ドキュメント数取得ツール(MCP) 34 | - インデックス管理ツール(CLI) 35 | 36 | ### 1.4 非機能要件 37 | 38 | - 迅速なレスポンス 39 | - シンプルな構成とメンテナンス性重視 40 | - 拡張性の高い設計 41 | 42 | ### 1.5 制約条件 43 | 44 | - Python 3.10以上で動作 45 | - JSON-RPC over stdioベースで動作 46 | - PostgreSQLとpgvectorエクステンションが必要 47 | 48 | ### 1.6 開発環境 49 | 50 | - 言語: Python 51 | - 外部ライブラリ: 52 | - `mcp[cli]` (Model Context Protocol) 53 | - `python-dotenv` 54 | - `psycopg2-binary` (PostgreSQL接続) 55 | - `sentence-transformers` (エンベディング生成) 56 | - `markdown` (マークダウン解析) 57 | - `numpy` (ベクトル操作) 58 | 59 | ### 1.7 成果物 60 | 61 | - Python製MCPサーバー 62 | - RAG機能の実装 63 | - README / 利用手順 64 | - 設計書 65 | 66 | ## 2. システム設計 67 | 68 | ### 2.1 システム概要設計 69 | 70 | #### 2.1.1 システムアーキテクチャ 71 | 72 | ##### システム構成図 73 | 74 | ```mermaid 75 | graph TB 76 | %% ノードの定義 77 | Client[MCP Host
Cline/Cursor] 78 | MCP[MCP RAG Server
Python] 79 | DB[(PostgreSQL
pgvector)] 80 | CLI[CLI Tool] 81 | Docs[Document Files] 82 | User[User] 83 | 84 | %% 関係の定義 85 | Client -->|"search
get_document_count
JSON-RPC over stdio"| MCP 86 | MCP -->|"ベクトル検索
データ取得"| DB 87 | 88 | User -->|"index
clear
count"| CLI 89 | CLI -->|"インデックス化
クリア
カウント"| MCP 90 | 91 | MCP -->|"読み込み
解析"| Docs 92 | CLI -->|"読み込み
解析"| Docs 93 | 94 | %% サブグラフの定義 95 | subgraph "MCP Server Environment" 96 | MCP 97 | CLI 98 | Docs 99 | end 100 | 101 | %% スタイル設定 102 | classDef client fill:#f9f,stroke:#333,stroke-width:2px; 103 | classDef server fill:#bbf,stroke:#333,stroke-width:2px; 104 | classDef database fill:#bfb,stroke:#333,stroke-width:2px; 105 | classDef tool fill:#fbb,stroke:#333,stroke-width:2px; 106 | classDef files fill:#fffacd,stroke:#333,stroke-width:2px; 107 | classDef user fill:#e6e6fa,stroke:#333,stroke-width:2px; 108 | 109 | class Client client; 110 | class MCP server; 111 | class DB database; 112 | class CLI tool; 113 | class Docs files; 114 | class User user; 115 | ``` 116 | 117 | ##### インデックス化のシーケンス図 118 | 119 | ```mermaid 120 | sequenceDiagram 121 | actor User 122 | participant CLI as CLI Tool 123 | participant RAG as RAG Service 124 | participant DocProc as Document Processor 125 | participant DB as PostgreSQL 126 | participant Files as Document Files 127 | 128 | %% 全件インデックス化 129 | User->>CLI: python -m src.cli index 130 | CLI->>RAG: index_documents() 131 | RAG->>DocProc: process_directory() 132 | DocProc->>Files: 全ファイルを読み込み 133 | Files-->>DocProc: ファイル内容 134 | DocProc->>DocProc: チャンク分割 135 | DocProc->>DocProc: ファイルレジストリ作成 136 | DocProc-->>RAG: チャンクリスト 137 | RAG->>RAG: エンベディング生成 138 | RAG->>DB: batch_insert_documents 139 | DB-->>RAG: 挿入結果 140 | RAG-->>CLI: 処理結果 141 | CLI-->>User: インデックス化完了メッセージ 142 | 143 | %% インデックスクリア 144 | User->>CLI: python -m src.cli clear 145 | CLI->>DocProc: ファイルレジストリ削除 146 | CLI->>RAG: clear_index() 147 | RAG->>DB: clear_database() 148 | DB-->>RAG: 削除結果 149 | RAG-->>CLI: 処理結果 150 | CLI-->>User: インデックスクリア完了メッセージ 151 | ``` 152 | 153 | ##### RAG(検索)のシーケンス図 154 | 155 | ```mermaid 156 | sequenceDiagram 157 | actor User 158 | participant Client as MCP Host (Cline/Cursor) 159 | participant MCP as MCP RAG Server 160 | participant RAG as RAG Service 161 | participant EmbGen as Embedding Generator 162 | participant DB as PostgreSQL 163 | 164 | %% 検索リクエスト 165 | User->>Client: 検索クエリ入力 166 | Client->>MCP: search(query, limit) 167 | MCP->>RAG: search(query, limit) 168 | 169 | %% エンベディング生成 170 | RAG->>EmbGen: generate_search_embedding(query) 171 | EmbGen-->>RAG: クエリエンベディング 172 | 173 | %% ベクトル検索 174 | RAG->>DB: search(query_embedding, limit) 175 | DB-->>RAG: 検索結果 176 | 177 | %% 結果整形 178 | RAG-->>MCP: 検索結果 179 | MCP-->>Client: 検索結果 180 | Client-->>User: 検索結果表示 181 | ``` 182 | 183 | #### 2.1.2 主要コンポーネント 184 | - **MCPサーバー** 185 | - JSON-RPC over stdioをリッスン 186 | - ツールの登録と実行を管理 187 | - **ドキュメント管理** 188 | - 複数形式のドキュメントの読み込みと解析 189 | - Markitdownを使用した形式変換 190 | - チャンク分割 191 | - ファイルレジストリによる差分管理 192 | - **エンベディング生成** 193 | - multilingual-e5-largeモデルを使用 194 | - テキストからベクトル表現を生成 195 | - **ベクトルデータベース** 196 | - PostgreSQLとpgvectorを使用 197 | - ベクトルの保存と検索 198 | 199 | ### 2.2 詳細設計 200 | 201 | #### 2.2.1 クラス設計 202 | 203 | ##### `MCPServer` 204 | ```python 205 | class MCPServer: 206 | def register_tool(name: str, description: str, input_schema: Dict[str, Any], handler: Callable) -> None 207 | def start(server_name: str, version: str, description: str) -> None 208 | def _handle_tools_call(params: Dict[str, Any], request_id: Any) -> None 209 | ``` 210 | 211 | ##### `DocumentProcessor` 212 | ```python 213 | class DocumentProcessor: 214 | def read_file(file_path: str) -> str 215 | def convert_to_markdown(file_path: str) -> str 216 | def split_into_chunks(text: str, chunk_size: int, overlap: int) -> List[str] 217 | def calculate_file_hash(file_path: str) -> str 218 | def get_file_metadata(file_path: str) -> Dict[str, Any] 219 | def load_file_registry(processed_dir: str) -> Dict[str, Dict[str, Any]] 220 | def save_file_registry(processed_dir: str, registry: Dict[str, Dict[str, Any]]) -> None 221 | def process_file(file_path: str, processed_dir: str, chunk_size: int, overlap: int) -> List[Dict[str, Any]] 222 | def process_directory(source_dir: str, processed_dir: str, chunk_size: int, overlap: int, incremental: bool = False) -> List[Dict[str, Any]] 223 | ``` 224 | 225 | ##### `EmbeddingGenerator` 226 | ```python 227 | class EmbeddingGenerator: 228 | def __init__(model_name: str) 229 | def generate_embedding(text: str) -> List[float] 230 | def generate_embeddings(texts: List[str]) -> List[List[float]] 231 | def generate_search_embedding(query: str) -> List[float] 232 | ``` 233 | 234 | ##### `VectorDatabase` 235 | ```python 236 | class VectorDatabase: 237 | def __init__(connection_params: Dict[str, Any]) 238 | def initialize_database() -> None 239 | def insert_document(document_id: str, content: str, file_path: str, chunk_index: int, embedding: List[float], metadata: Dict[str, Any]) -> None 240 | def batch_insert_documents(documents: List[Dict[str, Any]]) -> None 241 | def search(query_embedding: List[float], limit: int = 5) -> List[Dict[str, Any]] 242 | def delete_document(document_id: str) -> None 243 | def delete_by_file_path(file_path: str) -> int 244 | def clear_database() -> int 245 | def get_document_count() -> int 246 | def get_adjacent_chunks(file_path: str, chunk_index: int, context_size: int = 1) -> List[Dict[str, Any]] 247 | def get_document_by_file_path(file_path: str) -> List[Dict[str, Any]] 248 | ``` 249 | 250 | ##### `RAGService` 251 | ```python 252 | class RAGService: 253 | def __init__(document_processor: DocumentProcessor, embedding_generator: EmbeddingGenerator, vector_database: VectorDatabase) 254 | def index_documents(source_dir: str, processed_dir: str = None, chunk_size: int = 500, chunk_overlap: int = 100, incremental: bool = False) -> Dict[str, Any] 255 | def search(query: str, limit: int = 5, with_context: bool = False, context_size: int = 1, full_document: bool = False) -> List[Dict[str, Any]] 256 | def clear_index() -> Dict[str, Any] 257 | def get_document_count() -> int 258 | ``` 259 | 260 | #### 2.2.2 データベーススキーマ 261 | 262 | ```sql 263 | -- ドキュメントテーブル 264 | CREATE TABLE documents ( 265 | id SERIAL PRIMARY KEY, 266 | document_id TEXT UNIQUE NOT NULL, 267 | content TEXT NOT NULL, 268 | file_path TEXT NOT NULL, 269 | chunk_index INTEGER NOT NULL, 270 | embedding vector(1024), -- multilingual-e5-largeの次元数 271 | created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, 272 | metadata JSONB 273 | ); 274 | 275 | -- インデックス 276 | CREATE INDEX idx_documents_embedding ON documents USING ivfflat (embedding vector_cosine_ops); 277 | ``` 278 | 279 | ### 2.3 インターフェース設計 280 | 281 | #### 2.3.1 MCPツール 282 | 283 | ##### `search` 284 | ベクトル検索を行うツール 285 | 286 | - 入力パラメータ: 287 | - `query`: 検索クエリ 288 | - `limit` (オプション): 返す結果の数(デフォルト: 5) 289 | - `with_context` (オプション): 前後のチャンクも取得するかどうか(デフォルト: true) 290 | - `context_size` (オプション): 前後に取得するチャンク数(デフォルト: 1) 291 | - `full_document` (オプション): ドキュメント全体を取得するかどうか(デフォルト: false) 292 | 293 | - 出力: 294 | - 検索結果のリスト(ファイルパスとチャンクインデックスでソート) 295 | - ドキュメントID 296 | - コンテンツ 297 | - ファイルパス 298 | - チャンクインデックス 299 | - 関連度スコア 300 | - メタデータ 301 | - コンテキストフラグ(前後のチャンクの場合はTrue) 302 | - 全文ドキュメントフラグ(ドキュメント全体の場合はTrue) 303 | 304 | ##### `get_document_count` 305 | インデックス内のドキュメント数を取得するツール 306 | 307 | - 入力パラメータ: なし 308 | 309 | - 出力: 310 | - ドキュメント数 311 | 312 | #### 2.3.2 CLIコマンド 313 | 314 | ##### `index` 315 | ドキュメントをインデックス化するコマンド 316 | 317 | - 引数: 318 | - `--directory`, `-d`: インデックス化するドキュメントが含まれるディレクトリのパス(デフォルト: ./data/source) 319 | - `--chunk-size`, `-s`: チャンクサイズ(文字数)(デフォルト: 500) 320 | - `--chunk-overlap`, `-o`: チャンク間のオーバーラップ(文字数)(デフォルト: 100) 321 | - `--incremental`, `-i`: 差分のみをインデックス化するかどうか(フラグ) 322 | 323 | ##### `clear` 324 | インデックスをクリアするコマンド 325 | 326 | - 引数: なし 327 | 328 | ##### `count` 329 | インデックス内のドキュメント数を取得するコマンド 330 | 331 | - 引数: なし 332 | 333 | ### 2.4 セキュリティ設計 334 | 335 | - 環境変数で機密情報を管理(`.env`) 336 | - PostgreSQL接続情報 337 | - 外部からの直接アクセスは制限(ローカル環境前提) 338 | 339 | ### 2.5 テスト設計 340 | 341 | - 単体テスト 342 | - 各コンポーネントの機能テスト 343 | - MCPサーバーの基本機能テスト 344 | - 統合テスト 345 | - RAG機能の統合テスト 346 | - MCPリクエストを模擬した動作確認 347 | 348 | ### 2.6 開発環境・依存関係 349 | 350 | - Python 3.10+ 351 | - PostgreSQL 14+(pgvectorエクステンション付き) 352 | - 必要なPythonパッケージ: 353 | - `mcp[cli]` 354 | - `python-dotenv` 355 | - `psycopg2-binary` 356 | - `sentence-transformers` 357 | - `markdown` 358 | - `numpy` 359 | - `markitdown-mcp` 360 | 361 | ### 2.7 ファイル構造 362 | 363 | ``` 364 | mcp-rag-server/ 365 | ├── data/ 366 | │ ├── source/ # 原稿ファイル(階層構造対応) 367 | │ │ ├── markdown/ # マークダウンファイル 368 | │ │ ├── docs/ # ドキュメントファイル 369 | │ │ └── slides/ # プレゼンテーションファイル 370 | │ └── processed/ # 処理済みファイル(テキスト抽出済み) 371 | │ └── file_registry.json # 処理済みファイルの情報(差分インデックス用) 372 | ├── docs/ # プロジェクトドキュメント 373 | ├── logs/ # ログファイル 374 | ├── src/ # ソースコード 375 | └── tests/ # テストコード 376 | ``` 377 | 378 | ### 2.8 開発工程 379 | 380 | | フェーズ | 内容 | 期間 | 381 | |---------|------|------| 382 | | 要件定義 | 本仕様書作成 | 第1週 | 383 | | 設計 | アーキテクチャ・モジュール設計 | 第1週 | 384 | | 実装 | 各モジュールの開発 | 第2週 | 385 | | テスト | 単体・統合テスト | 第3週 | 386 | | リリース | ドキュメント整備・デプロイ対応 | 第3週 | 387 | 388 | ## 3. 実装ガイド 389 | 390 | ### 3.1 PostgreSQLとpgvectorのセットアップ 391 | 392 | #### 3.1.1 Dockerを使用する場合 393 | 394 | ```bash 395 | # pgvectorを含むPostgreSQLコンテナを起動 396 | docker run --name postgres-pgvector -e POSTGRES_PASSWORD=password -p 5432:5432 -d pgvector/pgvector:pg14 397 | ``` 398 | 399 | #### 3.1.2 既存のPostgreSQLにpgvectorをインストールする場合 400 | 401 | ```bash 402 | # pgvectorエクステンションをインストール 403 | CREATE EXTENSION vector; 404 | ``` 405 | 406 | ### 3.2 環境変数の設定 407 | 408 | `.env`ファイルに以下の環境変数を設定: 409 | 410 | ``` 411 | # PostgreSQL接続情報 412 | POSTGRES_HOST=localhost 413 | POSTGRES_PORT=5432 414 | POSTGRES_USER=postgres 415 | POSTGRES_PASSWORD=password 416 | POSTGRES_DB=ragdb 417 | 418 | # ドキュメントディレクトリ 419 | SOURCE_DIR=./data/source 420 | PROCESSED_DIR=./data/processed 421 | 422 | # エンベディングモデル 423 | EMBEDDING_MODEL=intfloat/multilingual-e5-large 424 | ``` 425 | 426 | ### 3.3 実装の流れ 427 | 428 | 1. 基本的なMCPサーバーの実装 429 | 2. ドキュメント処理コンポーネントの実装 430 | - 複数形式のファイル読み込み 431 | - Markitdownを使用した変換 432 | - 階層構造対応 433 | - ファイルレジストリによる差分管理 434 | 3. エンベディング生成コンポーネントの実装 435 | 4. ベクトルデータベースコンポーネントの実装 436 | 5. RAGサービスの実装 437 | 6. MCPツールの実装と登録 438 | 7. CLIコマンドの実装 439 | 8. テストとデバッグ 440 | 441 | ### 3.4 使用例 442 | 443 | #### 3.4.1 CLIによるインデックス化 444 | 445 | ```bash 446 | # 全件インデックス化 447 | python -m src.cli index 448 | 449 | # 差分インデックス化 450 | python -m src.cli index -i 451 | ``` 452 | 453 | #### 3.4.2 CLIによるインデックスのクリア 454 | 455 | ```bash 456 | python -m src.cli clear 457 | ``` 458 | 459 | #### 3.4.3 MCPによる検索 460 | 461 | ```json 462 | { 463 | "jsonrpc": "2.0", 464 | "method": "search", 465 | "params": { 466 | "query": "Pythonのジェネレータとは何ですか?", 467 | "limit": 5, 468 | "with_context": true, 469 | "context_size": 1, 470 | "full_document": false 471 | }, 472 | "id": 1 473 | } 474 | ``` 475 | 476 | ##### 前後のチャンクを取得する例 477 | 478 | ```json 479 | { 480 | "jsonrpc": "2.0", 481 | "method": "search", 482 | "params": { 483 | "query": "Pythonのジェネレータとは何ですか?", 484 | "limit": 3, 485 | "with_context": true, 486 | "context_size": 2 487 | }, 488 | "id": 1 489 | } 490 | ``` 491 | 492 | ##### ドキュメント全体を取得する例 493 | 494 | ```json 495 | { 496 | "jsonrpc": "2.0", 497 | "method": "search", 498 | "params": { 499 | "query": "Pythonのジェネレータとは何ですか?", 500 | "limit": 3, 501 | "full_document": true 502 | }, 503 | "id": 1 504 | } 505 | ``` 506 | 507 | #### 3.4.4 MCPによるドキュメント数の取得 508 | 509 | ```json 510 | { 511 | "jsonrpc": "2.0", 512 | "method": "get_document_count", 513 | "params": {}, 514 | "id": 2 515 | } 516 | -------------------------------------------------------------------------------- /src/vector_database.py: -------------------------------------------------------------------------------- 1 | """ 2 | ベクトルデータベースモジュール 3 | 4 | PostgreSQLとpgvectorを使用してベクトルの保存と検索を行います。 5 | """ 6 | 7 | import logging 8 | import psycopg2 9 | import json 10 | import os 11 | from dotenv import load_dotenv 12 | from typing import List, Dict, Any, Optional 13 | 14 | # .envの読み込み 15 | load_dotenv() 16 | EMBEDDING_DIM = int(os.getenv("EMBEDDING_DIM", "1024")) 17 | 18 | 19 | class VectorDatabase: 20 | """ 21 | ベクトルデータベースクラス 22 | 23 | PostgreSQLとpgvectorを使用してベクトルの保存と検索を行います。 24 | 25 | Attributes: 26 | connection_params: 接続パラメータ 27 | connection: データベース接続 28 | logger: ロガー 29 | """ 30 | 31 | def __init__(self, connection_params: Dict[str, Any]): 32 | """ 33 | VectorDatabaseのコンストラクタ 34 | 35 | Args: 36 | connection_params: 接続パラメータ 37 | - host: ホスト名 38 | - port: ポート番号 39 | - user: ユーザー名 40 | - password: パスワード 41 | - database: データベース名 42 | """ 43 | # ロガーの設定 44 | self.logger = logging.getLogger("vector_database") 45 | self.logger.setLevel(logging.INFO) 46 | 47 | # 接続パラメータの保存 48 | self.connection_params = connection_params 49 | self.connection = None 50 | 51 | def connect(self) -> None: 52 | """ 53 | データベースに接続します。 54 | 55 | Raises: 56 | Exception: 接続に失敗した場合 57 | """ 58 | try: 59 | self.connection = psycopg2.connect(**self.connection_params) 60 | self.logger.info("データベースに接続しました") 61 | except Exception as e: 62 | self.logger.error(f"データベースへの接続に失敗しました: {str(e)}") 63 | raise 64 | 65 | def disconnect(self) -> None: 66 | """ 67 | データベースから切断します。 68 | """ 69 | if self.connection: 70 | self.connection.close() 71 | self.connection = None 72 | self.logger.info("データベースから切断しました") 73 | 74 | def initialize_database(self) -> None: 75 | """ 76 | データベースを初期化します。 77 | 78 | テーブルとインデックスを作成します。 79 | 80 | Raises: 81 | Exception: 初期化に失敗した場合 82 | """ 83 | try: 84 | # 接続がない場合は接続 85 | if not self.connection: 86 | self.connect() 87 | 88 | # カーソルの作成 89 | cursor = self.connection.cursor() 90 | 91 | # pgvectorエクステンションの有効化 92 | cursor.execute("CREATE EXTENSION IF NOT EXISTS vector;") 93 | 94 | # ドキュメントテーブルの作成 95 | cursor.execute(f""" 96 | CREATE TABLE IF NOT EXISTS documents ( 97 | id SERIAL PRIMARY KEY, 98 | document_id TEXT UNIQUE NOT NULL, 99 | content TEXT NOT NULL, 100 | file_path TEXT NOT NULL, 101 | chunk_index INTEGER NOT NULL, 102 | metadata JSONB, 103 | embedding vector({EMBEDDING_DIM}), 104 | created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP 105 | ); 106 | """) 107 | 108 | # インデックスの作成 109 | cursor.execute(""" 110 | CREATE INDEX IF NOT EXISTS idx_documents_document_id ON documents (document_id); 111 | """) 112 | cursor.execute(""" 113 | CREATE INDEX IF NOT EXISTS idx_documents_file_path ON documents (file_path); 114 | """) 115 | cursor.execute(""" 116 | CREATE INDEX IF NOT EXISTS idx_documents_embedding ON documents USING ivfflat (embedding vector_cosine_ops); 117 | """) 118 | 119 | # コミット 120 | self.connection.commit() 121 | self.logger.info("データベースを初期化しました") 122 | 123 | except Exception as e: 124 | # ロールバック 125 | if self.connection: 126 | self.connection.rollback() 127 | self.logger.error(f"データベースの初期化に失敗しました: {str(e)}") 128 | raise 129 | 130 | finally: 131 | # カーソルを閉じる 132 | if "cursor" in locals() and cursor: 133 | cursor.close() 134 | 135 | def insert_document( 136 | self, 137 | document_id: str, 138 | content: str, 139 | file_path: str, 140 | chunk_index: int, 141 | embedding: List[float], 142 | metadata: Optional[Dict[str, Any]] = None, 143 | ) -> None: 144 | """ 145 | ドキュメントを挿入します。 146 | 147 | Args: 148 | document_id: ドキュメントID 149 | content: ドキュメントの内容 150 | file_path: ファイルパス 151 | chunk_index: チャンクインデックス 152 | embedding: エンベディング 153 | metadata: メタデータ(オプション) 154 | 155 | Raises: 156 | Exception: 挿入に失敗した場合 157 | """ 158 | try: 159 | # 接続がない場合は接続 160 | if not self.connection: 161 | self.connect() 162 | 163 | # カーソルの作成 164 | cursor = self.connection.cursor() 165 | 166 | # メタデータをJSON形式に変換 167 | metadata_json = json.dumps(metadata) if metadata else None 168 | 169 | # ドキュメントの挿入 170 | cursor.execute( 171 | """ 172 | INSERT INTO documents (document_id, content, file_path, chunk_index, embedding, metadata) 173 | VALUES (%s, %s, %s, %s, %s, %s) 174 | ON CONFLICT (document_id) 175 | DO UPDATE SET 176 | content = EXCLUDED.content, 177 | file_path = EXCLUDED.file_path, 178 | chunk_index = EXCLUDED.chunk_index, 179 | embedding = EXCLUDED.embedding, 180 | metadata = EXCLUDED.metadata, 181 | created_at = CURRENT_TIMESTAMP; 182 | """, 183 | (document_id, content, file_path, chunk_index, embedding, metadata_json), 184 | ) 185 | 186 | # コミット 187 | self.connection.commit() 188 | self.logger.debug(f"ドキュメント '{document_id}' を挿入しました") 189 | 190 | except Exception as e: 191 | # ロールバック 192 | if self.connection: 193 | self.connection.rollback() 194 | self.logger.error(f"ドキュメントの挿入に失敗しました: {str(e)}") 195 | raise 196 | 197 | finally: 198 | # カーソルを閉じる 199 | if "cursor" in locals() and cursor: 200 | cursor.close() 201 | 202 | def batch_insert_documents(self, documents: List[Dict[str, Any]]) -> None: 203 | """ 204 | 複数のドキュメントをバッチ挿入します。 205 | 206 | Args: 207 | documents: ドキュメントのリスト 208 | 各ドキュメントは以下のキーを持つ辞書: 209 | - document_id: ドキュメントID 210 | - content: ドキュメントの内容 211 | - file_path: ファイルパス 212 | - chunk_index: チャンクインデックス 213 | - embedding: エンベディング 214 | - metadata: メタデータ(オプション) 215 | 216 | Raises: 217 | Exception: 挿入に失敗した場合 218 | """ 219 | if not documents: 220 | self.logger.warning("挿入するドキュメントがありません") 221 | return 222 | 223 | try: 224 | # 接続がない場合は接続 225 | if not self.connection: 226 | self.connect() 227 | 228 | # カーソルの作成 229 | cursor = self.connection.cursor() 230 | 231 | # バッチ挿入用のデータ作成 232 | values = [] 233 | for doc in documents: 234 | metadata_json = json.dumps(doc.get("metadata")) if doc.get("metadata") else None 235 | values.append( 236 | (doc["document_id"], doc["content"], doc["file_path"], doc["chunk_index"], doc["embedding"], metadata_json) 237 | ) 238 | 239 | # バッチ挿入 240 | cursor.executemany( 241 | """ 242 | INSERT INTO documents (document_id, content, file_path, chunk_index, embedding, metadata) 243 | VALUES (%s, %s, %s, %s, %s, %s) 244 | ON CONFLICT (document_id) 245 | DO UPDATE SET 246 | content = EXCLUDED.content, 247 | file_path = EXCLUDED.file_path, 248 | chunk_index = EXCLUDED.chunk_index, 249 | embedding = EXCLUDED.embedding, 250 | metadata = EXCLUDED.metadata, 251 | created_at = CURRENT_TIMESTAMP; 252 | """, 253 | values, 254 | ) 255 | 256 | # コミット 257 | self.connection.commit() 258 | self.logger.info(f"{len(documents)} 個のドキュメントを挿入しました") 259 | 260 | except Exception as e: 261 | # ロールバック 262 | if self.connection: 263 | self.connection.rollback() 264 | self.logger.error(f"ドキュメントのバッチ挿入に失敗しました: {str(e)}") 265 | raise 266 | 267 | finally: 268 | # カーソルを閉じる 269 | if "cursor" in locals() and cursor: 270 | cursor.close() 271 | 272 | def search(self, query_embedding: List[float], limit: int = 5) -> List[Dict[str, Any]]: 273 | """ 274 | ベクトル検索を行います。 275 | 276 | Args: 277 | query_embedding: クエリのエンベディング 278 | limit: 返す結果の数(デフォルト: 5) 279 | 280 | Returns: 281 | 検索結果のリスト(関連度順) 282 | 283 | Raises: 284 | Exception: 検索に失敗した場合 285 | """ 286 | try: 287 | # 接続がない場合は接続 288 | if not self.connection: 289 | self.connect() 290 | 291 | # カーソルの作成 292 | cursor = self.connection.cursor() 293 | 294 | # クエリエンベディングをPostgreSQLの配列構文に変換 295 | embedding_str = str(query_embedding) 296 | embedding_array = f"ARRAY{embedding_str}::vector" 297 | 298 | # ベクトル検索 299 | cursor.execute( 300 | f""" 301 | SELECT 302 | document_id, 303 | content, 304 | file_path, 305 | chunk_index, 306 | metadata, 307 | 1 - (embedding <=> {embedding_array}) AS similarity 308 | FROM 309 | documents 310 | WHERE 311 | embedding IS NOT NULL 312 | ORDER BY 313 | embedding <=> {embedding_array} 314 | LIMIT %s; 315 | """, 316 | (limit,), 317 | ) 318 | 319 | # 結果の取得 320 | results = [] 321 | for row in cursor.fetchall(): 322 | document_id, content, file_path, chunk_index, metadata_json, similarity = row 323 | 324 | # メタデータをJSONからデコード 325 | if metadata_json: 326 | if isinstance(metadata_json, str): 327 | try: 328 | metadata = json.loads(metadata_json) 329 | except json.JSONDecodeError: 330 | metadata = {} 331 | else: 332 | # 既に辞書型の場合はそのまま使用 333 | metadata = metadata_json 334 | else: 335 | metadata = {} 336 | 337 | results.append( 338 | { 339 | "document_id": document_id, 340 | "content": content, 341 | "file_path": file_path, 342 | "chunk_index": chunk_index, 343 | "metadata": metadata, 344 | "similarity": similarity, 345 | } 346 | ) 347 | 348 | self.logger.info(f"クエリに対して {len(results)} 件の結果が見つかりました") 349 | return results 350 | 351 | except Exception as e: 352 | self.logger.error(f"ベクトル検索中にエラーが発生しました: {str(e)}") 353 | raise 354 | 355 | finally: 356 | # カーソルを閉じる 357 | if "cursor" in locals() and cursor: 358 | cursor.close() 359 | 360 | def delete_document(self, document_id: str) -> bool: 361 | """ 362 | ドキュメントを削除します。 363 | 364 | Args: 365 | document_id: 削除するドキュメントのID 366 | 367 | Returns: 368 | 削除に成功した場合はTrue、ドキュメントが見つからない場合はFalse 369 | 370 | Raises: 371 | Exception: 削除に失敗した場合 372 | """ 373 | try: 374 | # 接続がない場合は接続 375 | if not self.connection: 376 | self.connect() 377 | 378 | # カーソルの作成 379 | cursor = self.connection.cursor() 380 | 381 | # ドキュメントの削除 382 | cursor.execute("DELETE FROM documents WHERE document_id = %s;", (document_id,)) 383 | 384 | # 削除された行数を取得 385 | deleted_rows = cursor.rowcount 386 | 387 | # コミット 388 | self.connection.commit() 389 | 390 | if deleted_rows > 0: 391 | self.logger.info(f"ドキュメント '{document_id}' を削除しました") 392 | return True 393 | else: 394 | self.logger.warning(f"ドキュメント '{document_id}' が見つかりません") 395 | return False 396 | 397 | except Exception as e: 398 | # ロールバック 399 | if self.connection: 400 | self.connection.rollback() 401 | self.logger.error(f"ドキュメントの削除中にエラーが発生しました: {str(e)}") 402 | raise 403 | 404 | finally: 405 | # カーソルを閉じる 406 | if "cursor" in locals() and cursor: 407 | cursor.close() 408 | 409 | def delete_by_file_path(self, file_path: str) -> int: 410 | """ 411 | ファイルパスに基づいてドキュメントを削除します。 412 | 413 | Args: 414 | file_path: 削除するドキュメントのファイルパス 415 | 416 | Returns: 417 | 削除されたドキュメントの数 418 | 419 | Raises: 420 | Exception: 削除に失敗した場合 421 | """ 422 | try: 423 | # 接続がない場合は接続 424 | if not self.connection: 425 | self.connect() 426 | 427 | # カーソルの作成 428 | cursor = self.connection.cursor() 429 | 430 | # ドキュメントの削除 431 | cursor.execute("DELETE FROM documents WHERE file_path = %s;", (file_path,)) 432 | 433 | # 削除された行数を取得 434 | deleted_rows = cursor.rowcount 435 | 436 | # コミット 437 | self.connection.commit() 438 | 439 | self.logger.info(f"ファイルパス '{file_path}' に関連する {deleted_rows} 個のドキュメントを削除しました") 440 | return deleted_rows 441 | 442 | except Exception as e: 443 | # ロールバック 444 | if self.connection: 445 | self.connection.rollback() 446 | self.logger.error(f"ドキュメントの削除中にエラーが発生しました: {str(e)}") 447 | raise 448 | 449 | finally: 450 | # カーソルを閉じる 451 | if "cursor" in locals() and cursor: 452 | cursor.close() 453 | 454 | def clear_database(self) -> int: 455 | """ 456 | データベースをクリアします(全てのドキュメントを削除)。 457 | 458 | Raises: 459 | Exception: クリアに失敗した場合 460 | 461 | Returns: 462 | 削除されたドキュメントの数。テーブルをDROPするため、削除前の数を返します。 463 | """ 464 | try: 465 | # 接続がない場合は接続 466 | if not self.connection: 467 | self.connect() 468 | 469 | # 削除前のドキュメント数を取得 470 | count_before_delete = self.get_document_count() 471 | 472 | # カーソルの作成 473 | cursor = self.connection.cursor() 474 | 475 | # テーブルを削除してスキーマもクリア 476 | cursor.execute("DROP TABLE IF EXISTS documents;") 477 | 478 | # コミット 479 | self.connection.commit() 480 | 481 | if count_before_delete > 0: 482 | self.logger.info( 483 | f"データベースをクリアしました(documentsテーブルを削除、{count_before_delete} 個のドキュメントが対象でした)" 484 | ) 485 | else: 486 | self.logger.info("データベースをクリアしました(documentsテーブルを削除)") 487 | return count_before_delete 488 | 489 | except Exception as e: 490 | # ロールバック 491 | if self.connection: 492 | self.connection.rollback() 493 | self.logger.error(f"データベースのクリア中にエラーが発生しました: {str(e)}") 494 | raise 495 | 496 | finally: 497 | # カーソルを閉じる 498 | if "cursor" in locals() and cursor: 499 | cursor.close() 500 | 501 | def get_document_count(self) -> int: 502 | """ 503 | データベース内のドキュメント数を取得します。 504 | 505 | Returns: 506 | ドキュメント数 507 | 508 | Raises: 509 | Exception: 取得に失敗した場合 510 | """ 511 | try: 512 | # 接続がない場合は接続 513 | if not self.connection: 514 | self.connect() 515 | 516 | # カーソルの作成 517 | cursor = self.connection.cursor() 518 | 519 | # ドキュメント数を取得 520 | cursor.execute("SELECT COUNT(*) FROM documents;") 521 | count = cursor.fetchone()[0] 522 | 523 | self.logger.info(f"データベース内のドキュメント数: {count}") 524 | return count 525 | 526 | except psycopg2.errors.UndefinedTable: 527 | # テーブルが存在しない場合は0を返す 528 | self.connection.rollback() # エラー状態をリセット 529 | self.logger.info("documentsテーブルが存在しないため、ドキュメント数は0です") 530 | return 0 531 | except Exception as e: 532 | self.logger.error(f"ドキュメント数の取得中にエラーが発生しました: {str(e)}") 533 | raise 534 | 535 | def get_adjacent_chunks(self, file_path: str, chunk_index: int, context_size: int = 1) -> List[Dict[str, Any]]: 536 | """ 537 | 指定されたチャンクの前後のチャンクを取得します。 538 | 539 | Args: 540 | file_path: ファイルパス 541 | chunk_index: チャンクインデックス 542 | context_size: 前後に取得するチャンク数(デフォルト: 1) 543 | 544 | Returns: 545 | 前後のチャンクのリスト 546 | 547 | Raises: 548 | Exception: 取得に失敗した場合 549 | """ 550 | try: 551 | # 接続がない場合は接続 552 | if not self.connection: 553 | self.connect() 554 | 555 | # カーソルの作成 556 | cursor = self.connection.cursor() 557 | 558 | # 前後のチャンクを取得 559 | min_index = max(0, chunk_index - context_size) 560 | max_index = chunk_index + context_size 561 | 562 | cursor.execute( 563 | """ 564 | SELECT 565 | document_id, 566 | content, 567 | file_path, 568 | chunk_index, 569 | metadata, 570 | 1 AS similarity 571 | FROM 572 | documents 573 | WHERE 574 | file_path = %s 575 | AND chunk_index >= %s 576 | AND chunk_index <= %s 577 | AND chunk_index != %s 578 | ORDER BY 579 | chunk_index 580 | """, 581 | (file_path, min_index, max_index, chunk_index), 582 | ) 583 | 584 | # 結果の取得 585 | results = [] 586 | for row in cursor.fetchall(): 587 | document_id, content, file_path, chunk_index, metadata_json, similarity = row 588 | 589 | # メタデータをJSONからデコード 590 | if metadata_json: 591 | if isinstance(metadata_json, str): 592 | try: 593 | metadata = json.loads(metadata_json) 594 | except json.JSONDecodeError: 595 | metadata = {} 596 | else: 597 | # 既に辞書型の場合はそのまま使用 598 | metadata = metadata_json 599 | else: 600 | metadata = {} 601 | 602 | results.append( 603 | { 604 | "document_id": document_id, 605 | "content": content, 606 | "file_path": file_path, 607 | "chunk_index": chunk_index, 608 | "metadata": metadata, 609 | "similarity": similarity, 610 | "is_context": True, # コンテキストチャンクであることを示すフラグ 611 | } 612 | ) 613 | 614 | self.logger.info( 615 | f"ファイル '{file_path}' のチャンク {chunk_index} の前後 {len(results)} 件のチャンクを取得しました" 616 | ) 617 | return results 618 | 619 | except Exception as e: 620 | self.logger.error(f"前後のチャンク取得中にエラーが発生しました: {str(e)}") 621 | raise 622 | 623 | finally: 624 | # カーソルを閉じる 625 | if "cursor" in locals() and cursor: 626 | cursor.close() 627 | 628 | def get_document_by_file_path(self, file_path: str) -> List[Dict[str, Any]]: 629 | """ 630 | 指定されたファイルパスに基づいてドキュメント全体を取得します。 631 | 632 | Args: 633 | file_path: ファイルパス 634 | 635 | Returns: 636 | ドキュメント全体のチャンクのリスト 637 | 638 | Raises: 639 | Exception: 取得に失敗した場合 640 | """ 641 | try: 642 | # 接続がない場合は接続 643 | if not self.connection: 644 | self.connect() 645 | 646 | # カーソルの作成 647 | cursor = self.connection.cursor() 648 | 649 | # ファイルパスに基づいてドキュメントを取得 650 | cursor.execute( 651 | """ 652 | SELECT 653 | document_id, 654 | content, 655 | file_path, 656 | chunk_index, 657 | metadata, 658 | 1 AS similarity 659 | FROM 660 | documents 661 | WHERE 662 | file_path = %s 663 | ORDER BY 664 | chunk_index 665 | """, 666 | (file_path,), 667 | ) 668 | 669 | # 結果の取得 670 | results = [] 671 | for row in cursor.fetchall(): 672 | document_id, content, file_path, chunk_index, metadata_json, similarity = row 673 | 674 | # メタデータをJSONからデコード 675 | if metadata_json: 676 | if isinstance(metadata_json, str): 677 | try: 678 | metadata = json.loads(metadata_json) 679 | except json.JSONDecodeError: 680 | metadata = {} 681 | else: 682 | # 既に辞書型の場合はそのまま使用 683 | metadata = metadata_json 684 | else: 685 | metadata = {} 686 | 687 | results.append( 688 | { 689 | "document_id": document_id, 690 | "content": content, 691 | "file_path": file_path, 692 | "chunk_index": chunk_index, 693 | "metadata": metadata, 694 | "similarity": similarity, 695 | "is_full_document": True, # 全文ドキュメントであることを示すフラグ 696 | } 697 | ) 698 | 699 | self.logger.info(f"ファイル '{file_path}' の全文 {len(results)} チャンクを取得しました") 700 | return results 701 | 702 | except Exception as e: 703 | self.logger.error(f"ドキュメント全文の取得中にエラーが発生しました: {str(e)}") 704 | raise 705 | 706 | finally: 707 | # カーソルを閉じる 708 | if "cursor" in locals() and cursor: 709 | cursor.close() 710 | --------------------------------------------------------------------------------