diff --git a/.cursor/mcp.json b/.cursor/mcp.json index 0bbcb11..31347c5 100644 --- a/.cursor/mcp.json +++ b/.cursor/mcp.json @@ -15,13 +15,10 @@ "disabled": false, "autoApprove": [] }, - "apidog": { + "API specification": { "command": "npx", - "args": ["-y", "apidog-mcp-server@latest", "--project-id=${env:APIDOG_PROJECT_ID}"], - "env": { - "APIDOG_PROJECT_ID": "${env:APIDOG_PROJECT_ID}", - "APIDOG_ACCESS_TOKEN": "${env:APIDOG_ACCESS_TOKEN}" - } + "args": ["-y", "apidog-mcp-server@latest", "--project=1134437"], + "envFile": "${workspaceFolder}/.env" } } } diff --git a/.env.example b/.env.example index 489728c..1f5678e 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,5 @@ # MCP -APIDOG_PROJECT_ID=1058162 +APIDOG_ACCESS_TOKEN= # DB POSTGRES_USER=chatbot diff --git a/backend/app/main.py b/backend/app/main.py index f558df9..d37f064 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -23,7 +23,7 @@ validation_exception_handler, ) from app.presentation.middleware.request_id import RequestIDMiddleware -from app.presentation.routers import chat, health +from app.presentation.routers import chat, health, sse # ログ設定 configure_logging(log_level=settings.LOG_LEVEL, json_logs=settings.JSON_LOGS) @@ -85,6 +85,7 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]: # ルーターの登録 app.include_router(health.router, prefix="/api/health", tags=["health"]) app.include_router(chat.router, prefix="/api/chat", tags=["chat"]) +app.include_router(sse.router, prefix="/api/sse", tags=["sse"]) # MCPサーバーのマウント(有効な場合のみ) # Claude Desktop、VS Code等のMCPクライアントから /mcp エンドポイントで接続可能 diff --git a/backend/app/presentation/routers/sse.py b/backend/app/presentation/routers/sse.py new file mode 100644 index 0000000..9ae50b1 --- /dev/null +++ b/backend/app/presentation/routers/sse.py @@ -0,0 +1,74 @@ +""" +SSE(Server-Sent Events)エンドポイント +Apidogでのテスト用シンプル実装 +""" + +import asyncio +from collections.abc import AsyncGenerator + +from fastapi import APIRouter +from fastapi.responses import StreamingResponse + +router = APIRouter() + + +async def event_generator() -> AsyncGenerator[str, None]: + """ + SSEイベントを生成するジェネレーター + 5回のイベントを1秒間隔で送信 + """ + for i in range(1, 6): + # SSEフォーマット: "data: メッセージ\n\n" + yield f"data: Message {i}: Hello from SSE!\n\n" + await asyncio.sleep(1) + + # 完了イベント + yield "data: [DONE]\n\n" + + +@router.get("/stream") +async def sse_stream() -> StreamingResponse: + """ + シンプルなSSEストリーミングエンドポイント + 1秒間隔で5つのメッセージを送信し、最後に[DONE]を送信 + """ + return StreamingResponse( + event_generator(), + media_type="text/event-stream", + headers={ + "Cache-Control": "no-cache", + "Connection": "keep-alive", + "X-Accel-Buffering": "no", # Nginxのバッファリング無効化 + }, + ) + + +async def countdown_generator(count: int) -> AsyncGenerator[str, None]: + """ + カウントダウンイベントを生成するジェネレーター + """ + for i in range(count, 0, -1): + yield f"event: countdown\ndata: {i}\n\n" + await asyncio.sleep(1) + + yield "event: complete\ndata: Countdown finished!\n\n" + + +@router.get("/countdown/{count}") +async def sse_countdown(count: int = 10) -> StreamingResponse: + """ + カウントダウンSSEエンドポイント + 指定した数からカウントダウン(デフォルト: 10) + """ + # 最大60秒に制限 + count = min(count, 60) + + return StreamingResponse( + countdown_generator(count), + media_type="text/event-stream", + headers={ + "Cache-Control": "no-cache", + "Connection": "keep-alive", + "X-Accel-Buffering": "no", + }, + ) diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 0d47a47..9156efe 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -24,7 +24,7 @@ dependencies = [ "structlog>=25.5.0", # LangChain "langchain>=1.0.5", - "langchain-core>=1.0.7", + "langchain-core>=1.2.5", "langchain-community>=0.4.1", "langchain-google-genai>=3.0.2", # LangGraph @@ -34,6 +34,10 @@ dependencies = [ "fastmcp>=2.14.1", # Security: CVE-2025-66418, CVE-2025-66471 対応 "urllib3>=2.6.0", + # Security: CVE-2025-68480 対応 + "marshmallow>=3.26.2", + # Security: GHSA-w853-jp5j-5j7f 対応 + "filelock>=3.20.1", ] [project.optional-dependencies] diff --git a/backend/uv.lock b/backend/uv.lock index 6bc6f78..d6cd56a 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -215,6 +215,7 @@ dependencies = [ { name = "boto3" }, { name = "fastapi" }, { name = "fastmcp" }, + { name = "filelock" }, { name = "greenlet" }, { name = "hiredis" }, { name = "langchain" }, @@ -223,6 +224,7 @@ dependencies = [ { name = "langchain-google-genai" }, { name = "langfuse" }, { name = "langgraph" }, + { name = "marshmallow" }, { name = "psycopg2-binary" }, { name = "pydantic" }, { name = "pydantic-settings" }, @@ -253,14 +255,16 @@ requires-dist = [ { name = "boto3", specifier = "==1.40.71" }, { name = "fastapi", specifier = "==0.121.1" }, { name = "fastmcp", specifier = ">=2.14.1" }, + { name = "filelock", specifier = ">=3.20.1" }, { name = "greenlet", specifier = "==3.2.4" }, { name = "hiredis", specifier = "==3.3.0" }, { name = "langchain", specifier = ">=1.0.5" }, { name = "langchain-community", specifier = ">=0.4.1" }, - { name = "langchain-core", specifier = ">=1.0.7" }, + { name = "langchain-core", specifier = ">=1.2.5" }, { name = "langchain-google-genai", specifier = ">=3.0.2" }, { name = "langfuse", specifier = ">=3.10.1" }, { name = "langgraph", specifier = ">=1.0.3" }, + { name = "marshmallow", specifier = ">=3.26.2" }, { name = "mypy", marker = "extra == 'dev'", specifier = "==1.18.2" }, { name = "pip-audit", marker = "extra == 'dev'", specifier = ">=2.7.0" }, { name = "psycopg2-binary", specifier = "==2.9.11" }, @@ -756,11 +760,11 @@ wheels = [ [[package]] name = "filelock" -version = "3.20.0" +version = "3.20.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/58/46/0028a82567109b5ef6e4d2a1f04a583fb513e6cf9527fcdd09afd817deeb/filelock-3.20.0.tar.gz", hash = "sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4", size = 18922, upload-time = "2025-10-08T18:03:50.056Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/23/ce7a1126827cedeb958fc043d61745754464eb56c5937c35bbf2b8e26f34/filelock-3.20.1.tar.gz", hash = "sha256:b8360948b351b80f420878d8516519a2204b07aefcdcfd24912a5d33127f188c", size = 19476, upload-time = "2025-12-15T23:54:28.027Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054, upload-time = "2025-10-08T18:03:48.35Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7f/a1a97644e39e7316d850784c642093c99df1290a460df4ede27659056834/filelock-3.20.1-py3-none-any.whl", hash = "sha256:15d9e9a67306188a44baa72f569d2bfd803076269365fdea0934385da4dc361a", size = 16666, upload-time = "2025-12-15T23:54:26.874Z" }, ] [[package]] @@ -1440,7 +1444,7 @@ wheels = [ [[package]] name = "langchain-core" -version = "1.1.0" +version = "1.2.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jsonpatch" }, @@ -1450,10 +1454,11 @@ dependencies = [ { name = "pyyaml" }, { name = "tenacity" }, { name = "typing-extensions" }, + { name = "uuid-utils" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1e/17/67c1cc2ace919e2b02dd9d783154d7fb3f1495a4ef835d9cd163b7855ac2/langchain_core-1.1.0.tar.gz", hash = "sha256:2b76a82d427922c8bc51c08404af4fc2a29e9f161dfe2297cb05091e810201e7", size = 781995, upload-time = "2025-11-21T21:01:26.958Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c8/86/bd678d69341ae4178bc8dfa04024d63636e5d580ff03d4502c8bc2262917/langchain_core-1.2.5.tar.gz", hash = "sha256:d674f6df42f07e846859b9d3afe547cad333d6bf9763e92c88eb4f8aaedcd3cc", size = 820445, upload-time = "2025-12-22T23:45:32.041Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/71/1e/e129fc471a2d2a7b3804480a937b5ab9319cab9f4142624fcb115f925501/langchain_core-1.1.0-py3-none-any.whl", hash = "sha256:2c9f27dadc6d21ed4aa46506a37a56e6a7e2d2f9141922dc5c251ba921822ee6", size = 473752, upload-time = "2025-11-21T21:01:25.841Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/9df897cbc98290bf71140104ee5b9777cf5291afb80333aa7da5a497339b/langchain_core-1.2.5-py3-none-any.whl", hash = "sha256:3255944ef4e21b2551facb319bfc426057a40247c0a05de5bd6f2fc021fbfa34", size = 484851, upload-time = "2025-12-22T23:45:30.525Z" }, ] [[package]] @@ -1731,14 +1736,14 @@ wheels = [ [[package]] name = "marshmallow" -version = "3.26.1" +version = "3.26.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ab/5e/5e53d26b42ab75491cda89b871dab9e97c840bf12c63ec58a1919710cd06/marshmallow-3.26.1.tar.gz", hash = "sha256:e6d8affb6cb61d39d26402096dc0aee12d5a26d490a121f118d2e81dc0719dc6", size = 221825, upload-time = "2025-02-03T15:32:25.093Z" } +sdist = { url = "https://files.pythonhosted.org/packages/55/79/de6c16cc902f4fc372236926b0ce2ab7845268dcc30fb2fbb7f71b418631/marshmallow-3.26.2.tar.gz", hash = "sha256:bbe2adb5a03e6e3571b573f42527c6fe926e17467833660bebd11593ab8dfd57", size = 222095, upload-time = "2025-12-22T06:53:53.309Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/34/75/51952c7b2d3873b44a0028b1bd26a25078c18f92f256608e8d1dc61b39fd/marshmallow-3.26.1-py3-none-any.whl", hash = "sha256:3350409f20a70a7e4e11a27661187b77cdcaeb20abca41c1454fe33636bea09c", size = 50878, upload-time = "2025-02-03T15:32:22.295Z" }, + { url = "https://files.pythonhosted.org/packages/be/2f/5108cb3ee4ba6501748c4908b908e55f42a5b66245b4cfe0c99326e1ef6e/marshmallow-3.26.2-py3-none-any.whl", hash = "sha256:013fa8a3c4c276c24d26d84ce934dc964e2aa794345a0f8c7e5a7191482c8a73", size = 50964, upload-time = "2025-12-22T06:53:51.801Z" }, ] [[package]] @@ -3388,6 +3393,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl", hash = "sha256:ec21cddfe7724fc7cb4ba4bea7aa8e2ef36f607a4bab81aa6ce42a13dc3f03dd", size = 131182, upload-time = "2025-12-11T15:56:38.584Z" }, ] +[[package]] +name = "uuid-utils" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/0e/512fb221e4970c2f75ca9dae412d320b7d9ddc9f2b15e04ea8e44710396c/uuid_utils-0.12.0.tar.gz", hash = "sha256:252bd3d311b5d6b7f5dfce7a5857e27bb4458f222586bb439463231e5a9cbd64", size = 20889, upload-time = "2025-12-01T17:29:55.494Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/43/de5cd49a57b6293b911b6a9a62fc03e55db9f964da7d5882d9edbee1e9d2/uuid_utils-0.12.0-cp39-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:3b9b30707659292f207b98f294b0e081f6d77e1fbc760ba5b41331a39045f514", size = 603197, upload-time = "2025-12-01T17:29:30.104Z" }, + { url = "https://files.pythonhosted.org/packages/02/fa/5fd1d8c9234e44f0c223910808cde0de43bb69f7df1349e49b1afa7f2baa/uuid_utils-0.12.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:add3d820c7ec14ed37317375bea30249699c5d08ff4ae4dbee9fc9bce3bfbf65", size = 305168, upload-time = "2025-12-01T17:29:31.384Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c6/8633ac9942bf9dc97a897b5154e5dcffa58816ec4dd780b3b12b559ff05c/uuid_utils-0.12.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b8fce83ecb3b16af29c7809669056c4b6e7cc912cab8c6d07361645de12dd79", size = 340580, upload-time = "2025-12-01T17:29:32.362Z" }, + { url = "https://files.pythonhosted.org/packages/f3/88/8a61307b04b4da1c576373003e6d857a04dade52ab035151d62cb84d5cb5/uuid_utils-0.12.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ec921769afcb905035d785582b0791d02304a7850fbd6ce924c1a8976380dfc6", size = 346771, upload-time = "2025-12-01T17:29:33.708Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fb/aab2dcf94b991e62aa167457c7825b9b01055b884b888af926562864398c/uuid_utils-0.12.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f3b060330f5899a92d5c723547dc6a95adef42433e9748f14c66859a7396664", size = 474781, upload-time = "2025-12-01T17:29:35.237Z" }, + { url = "https://files.pythonhosted.org/packages/5a/7a/dbd5e49c91d6c86dba57158bbfa0e559e1ddf377bb46dcfd58aea4f0d567/uuid_utils-0.12.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:908dfef7f0bfcf98d406e5dc570c25d2f2473e49b376de41792b6e96c1d5d291", size = 343685, upload-time = "2025-12-01T17:29:36.677Z" }, + { url = "https://files.pythonhosted.org/packages/1a/19/8c4b1d9f450159733b8be421a4e1fb03533709b80ed3546800102d085572/uuid_utils-0.12.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c6a24148926bd0ca63e8a2dabf4cc9dc329a62325b3ad6578ecd60fbf926506", size = 366482, upload-time = "2025-12-01T17:29:37.979Z" }, + { url = "https://files.pythonhosted.org/packages/82/43/c79a6e45687647f80a159c8ba34346f287b065452cc419d07d2212d38420/uuid_utils-0.12.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:64a91e632669f059ef605f1771d28490b1d310c26198e46f754e8846dddf12f4", size = 523132, upload-time = "2025-12-01T17:29:39.293Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a2/b2d75a621260a40c438aa88593827dfea596d18316520a99e839f7a5fb9d/uuid_utils-0.12.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:93c082212470bb4603ca3975916c205a9d7ef1443c0acde8fbd1e0f5b36673c7", size = 614218, upload-time = "2025-12-01T17:29:40.315Z" }, + { url = "https://files.pythonhosted.org/packages/13/6b/ba071101626edd5a6dabf8525c9a1537ff3d885dbc210540574a03901fef/uuid_utils-0.12.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:431b1fb7283ba974811b22abd365f2726f8f821ab33f0f715be389640e18d039", size = 546241, upload-time = "2025-12-01T17:29:41.656Z" }, + { url = "https://files.pythonhosted.org/packages/01/12/9a942b81c0923268e6d85bf98d8f0a61fcbcd5e432fef94fdf4ce2ef8748/uuid_utils-0.12.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2ffd7838c40149100299fa37cbd8bab5ee382372e8e65a148002a37d380df7c8", size = 511842, upload-time = "2025-12-01T17:29:43.107Z" }, + { url = "https://files.pythonhosted.org/packages/a9/a7/c326f5163dd48b79368b87d8a05f5da4668dd228a3f5ca9d79d5fee2fc40/uuid_utils-0.12.0-cp39-abi3-win32.whl", hash = "sha256:487f17c0fee6cbc1d8b90fe811874174a9b1b5683bf2251549e302906a50fed3", size = 179088, upload-time = "2025-12-01T17:29:44.492Z" }, + { url = "https://files.pythonhosted.org/packages/38/92/41c8734dd97213ee1d5ae435cf4499705dc4f2751e3b957fd12376f61784/uuid_utils-0.12.0-cp39-abi3-win_amd64.whl", hash = "sha256:9598e7c9da40357ae8fffc5d6938b1a7017f09a1acbcc95e14af8c65d48c655a", size = 183003, upload-time = "2025-12-01T17:29:45.47Z" }, + { url = "https://files.pythonhosted.org/packages/c9/f9/52ab0359618987331a1f739af837d26168a4b16281c9c3ab46519940c628/uuid_utils-0.12.0-cp39-abi3-win_arm64.whl", hash = "sha256:c9bea7c5b2aa6f57937ebebeee4d4ef2baad10f86f1b97b58a3f6f34c14b4e84", size = 182975, upload-time = "2025-12-01T17:29:46.444Z" }, +] + [[package]] name = "uvicorn" version = "0.38.0"