diff --git a/.vscode/settings.json b/.vscode/settings.json index 650fca03..17bbe8d1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,9 +6,10 @@ "./libs/rag-core-api/src", "./libs/rag-core-lib/src", "./libs/extractor-api-lib/src", - "./admin-backend", - "./rag-backend", - "./document-extractor" + "./services/mcp-server/src", + "./services/admin-backend", + "./services/rag-backend", + "./services/document-extractor" ], "[yaml]": { "editor.tabSize": 2, diff --git a/README.md b/README.md index b20d1bcd..9f4e9d4d 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,27 @@ The MCP server runs as a sidecar container alongside the main RAG backend and ex - `chat_simple`: Basic question-answering without conversation history - `chat_with_history`: Advanced chat interface with conversation history and returns structured responses with `answer`, `finish_reason`, and `citations`. +##### Configuring Tool Documentation + +The MCP server supports customizable documentation for its tools through environment variables. This allows you to customize the descriptions, parameter explanations, and examples shown to MCP clients. All documentation configuration uses the `MCP_` prefix and can be configured with the [values.yaml](infrastructure/rag/values.yaml). The following configuration options exist: + +**For `chat_simple` tool:** + +- `MCP_CHAT_SIMPLE_DESCRIPTION`: Main description of the tool +- `MCP_CHAT_SIMPLE_PARAMETER_DESCRIPTIONS`: JSON object mapping parameter names to descriptions +- `MCP_CHAT_SIMPLE_RETURNS`: Description of the return value +- `MCP_CHAT_SIMPLE_NOTES`: Additional notes about the tool +- `MCP_CHAT_SIMPLE_EXAMPLES`: Usage examples + +**For `chat_with_history` tool:** + +- `MCP_CHAT_WITH_HISTORY_DESCRIPTION`: Main description of the tool +- `MCP_CHAT_WITH_HISTORY_PARAMETER_DESCRIPTIONS`: JSON object mapping parameter names to descriptions +- `MCP_CHAT_WITH_HISTORY_RETURNS`: Description of the return value +- `MCP_CHAT_WITH_HISTORY_NOTES`: Additional notes about the tool +- `MCP_CHAT_WITH_HISTORY_EXAMPLES`: Usage examples + + For further information on configuration and usage, please consult the [MCP Server README](./services/mcp-server/README.md). #### 1.1.5 Frontend diff --git a/conftest.py b/conftest.py index 2cc40bfa..0dcd17f6 100644 --- a/conftest.py +++ b/conftest.py @@ -4,12 +4,16 @@ # Add project root and specific directories to Python path project_root = Path(__file__).parent -sys.path.insert(0, str(project_root)) -sys.path.insert(0, str(project_root / "admin-backend")) -sys.path.insert(0, str(project_root / "rag-backend")) -sys.path.insert(0, str(project_root / "document-extractor")) + +# Add services src directories to Python path +services_root = project_root / "services" +for service in ["admin-backend", "rag-backend", "document-extractor", "mcp-server"]: + service_src = services_root / service / "src" + if service_src.exists(): + sys.path.insert(0, str(service_src)) # point at each rag-core library's src folder so their packages (admin_api_lib, rag_core_api, etc.) are importable lib_root = project_root / "libs" for lib in ["admin-api-lib", "rag-core-api", "rag-core-lib", "extractor-api-lib"]: sys.path.insert(0, str(lib_root / lib / "src")) + sys.path.insert(0, str(lib_root / lib / "tests")) diff --git a/infrastructure/rag/templates/backend/configmap.yaml b/infrastructure/rag/templates/backend/configmap.yaml index 72b5a8f7..c5621c64 100644 --- a/infrastructure/rag/templates/backend/configmap.yaml +++ b/infrastructure/rag/templates/backend/configmap.yaml @@ -140,7 +140,15 @@ metadata: name: {{ template "configmap.mcp" . }} data: MCP_PORT: {{ .Values.backend.mcp.port | quote }} - MCP_TOOL_NAME: {{ .Values.backend.mcp.toolName }} - MCP_TOOL_DESCRIPTION: {{ .Values.backend.mcp.toolDescription }} - MCP_NAME: {{ .Values.backend.mcp.name }} ---- \ No newline at end of file + MCP_HOST: {{ .Values.backend.mcp.host | quote }} + MCP_NAME: {{ .Values.backend.mcp.name | quote }} + MCP_CHAT_SIMPLE_DESCRIPTION: {{ .Values.backend.mcp.chatSimpleDescription | quote }} + MCP_CHAT_SIMPLE_PARAMETER_DESCRIPTIONS: {{ .Values.backend.mcp.chatSimpleParameterDescriptions | toJson | quote }} + MCP_CHAT_SIMPLE_RETURNS: {{ .Values.backend.mcp.chatSimpleReturns | quote }} + MCP_CHAT_SIMPLE_NOTES: {{ .Values.backend.mcp.chatSimpleNotes | quote }} + MCP_CHAT_SIMPLE_EXAMPLES: {{ .Values.backend.mcp.chatSimpleExamples | quote }} + MCP_CHAT_WITH_HISTORY_DESCRIPTION: {{ .Values.backend.mcp.chatWithHistoryDescription | quote }} + MCP_CHAT_WITH_HISTORY_PARAMETER_DESCRIPTIONS: {{ .Values.backend.mcp.chatWithHistoryParameterDescriptions | toJson | quote }} + MCP_CHAT_WITH_HISTORY_RETURNS: {{ .Values.backend.mcp.chatWithHistoryReturns | quote }} + MCP_CHAT_WITH_HISTORY_NOTES: {{ .Values.backend.mcp.chatWithHistoryNotes | quote }} + MCP_CHAT_WITH_HISTORY_EXAMPLES: {{ .Values.backend.mcp.chatWithHistoryExamples | quote }} diff --git a/infrastructure/rag/values.yaml b/infrastructure/rag/values.yaml index 4ae9b608..09938e00 100644 --- a/infrastructure/rag/values.yaml +++ b/infrastructure/rag/values.yaml @@ -20,6 +20,53 @@ backend: name: "mcp" port: "8000" host: "0.0.0.0" + + # Chat simple tool configuration + # The following configuration for the chat_simple tool will render as follows: + # """Send a message to the RAG system and get a simple text response. + + # This is the simplest way to interact with the RAG system - just provide a message and get back the answer as plain text. + # + # Parameters + # ---------- + # session_id : str + # Unique identifier for the chat session. + # message : str + # The message/question to ask the RAG system. + # + # Returns + # ------- + # str + # The answer from the RAG system as plain text. + # """ + chatSimpleDescription: "Send a message to the RAG system and get a simple text response.\n\nThis is the simplest way to interact with the RAG system - just provide a message and get back the answer as plain text." + chatSimpleParameterDescriptions: + session_id: "Unique identifier for the chat session." + message: "The message/question to ask the RAG system." + chatSimpleReturns: "The answer from the RAG system as plain text." + chatSimpleNotes: "" + # If you add a Value to chatSimpleNotes e.g. "This tool is best for simple questions that don't require conversation context." + # it will render to: + # Notes + # ----- + # This tool is best for simple questions that don't require conversation context. + chatSimpleExamples: "" + # If you add a Value to chatSimpleExamples e.g. "chat_simple(session_id='my-session', message='What is the main topic of the document?')" + # it will render to: + # Examples + # -------- + # chat_simple(session_id='my-session', message='What is the main topic of the document?') + + # Chat with history tool configuration + chatWithHistoryDescription: "Send a message with conversation history and get structured response.\n\nProvide conversation history as a simple list of dictionaries.\nEach history item should have 'role' (either 'user' or 'assistant') and 'message' keys." + chatWithHistoryParameterDescriptions: + session_id: "Unique identifier for the chat session." + message: "The current message/question to ask." + history: "Previous conversation history. Each item should be:\n {\"role\": \"user\" or \"assistant\", \"message\": \"the message text\"}" + chatWithHistoryReturns: "Response containing:\n - answer: The response text\n - finish_reason: Why the response ended\n - citations: List of source documents used (simplified)" + chatWithHistoryNotes: "" + chatWithHistoryExamples: "" + image: repository: ghcr.io/stackitcloud/rag-template name: rag-mcp diff --git a/libs/rag-core-api/pyproject.toml b/libs/rag-core-api/pyproject.toml index 88952f1d..d61057a4 100644 --- a/libs/rag-core-api/pyproject.toml +++ b/libs/rag-core-api/pyproject.toml @@ -122,5 +122,5 @@ max-line-length = 120 log_cli = true log_cli_level = "DEBUG" pythonpath = ["src", "tests"] -testpaths = "tests" +testpaths = ["tests"] diff --git a/services/mcp-server/README.md b/services/mcp-server/README.md index 6e8e2348..4654c33b 100644 --- a/services/mcp-server/README.md +++ b/services/mcp-server/README.md @@ -78,6 +78,122 @@ The server supports configuration through environment variables with the followi - `BACKEND_BASE_PATH`: RAG backend URL (default: `http://127.0.0.1:8080`) +### Tool Documentation Configuration + +The MCP server supports customizable documentation for its tools through environment variables. This allows you to customize the descriptions, parameter explanations, and examples shown to MCP clients. + +#### Chat Simple Tool Documentation + +- `MCP_CHAT_SIMPLE_DESCRIPTION`: Main description of the tool +- `MCP_CHAT_SIMPLE_PARAMETER_DESCRIPTIONS`: JSON object mapping parameter names to descriptions +- `MCP_CHAT_SIMPLE_RETURNS`: Description of the return value +- `MCP_CHAT_SIMPLE_NOTES`: Additional notes about the tool +- `MCP_CHAT_SIMPLE_EXAMPLES`: Usage examples + +#### Chat With History Tool Documentation + +- `MCP_CHAT_WITH_HISTORY_DESCRIPTION`: Main description of the tool +- `MCP_CHAT_WITH_HISTORY_PARAMETER_DESCRIPTIONS`: JSON object mapping parameter names to descriptions +- `MCP_CHAT_WITH_HISTORY_RETURNS`: Description of the return value +- `MCP_CHAT_WITH_HISTORY_NOTES`: Additional notes about the tool +- `MCP_CHAT_WITH_HISTORY_EXAMPLES`: Usage examples + +#### Example Configuration + +```env +# Custom tool descriptions +MCP_CHAT_SIMPLE_DESCRIPTION="Ask questions about your documents and get instant answers." +MCP_CHAT_SIMPLE_EXAMPLES="chat_simple(session_id='my-session', message='What is the main topic of the document?')" + +# Custom parameter descriptions (JSON format) +MCP_CHAT_SIMPLE_PARAMETER_DESCRIPTIONS='{"session_id": "A unique session identifier for your conversation", "message": "Your question about the documents"}' + +# Custom return description +MCP_CHAT_SIMPLE_RETURNS="A plain text answer based on your document content" + +# Notes about usage +MCP_CHAT_SIMPLE_NOTES="This tool is best for simple questions that don't require conversation context." +``` + +#### Template Rendering + +The MCP server uses Jinja2 templates to generate tool documentation in **numpy docstring format**. When the environment variables are set, they are rendered into a structured docstring that MCP clients can parse and display. + +**Example of rendered docstring:** + +Given the configuration above, the `chat_simple` tool's docstring would be rendered as: + +```python +def chat_simple(session_id: str, message: str) -> str: + """Ask questions about your documents and get instant answers. + + Parameters + ---------- + session_id : str + A unique session identifier for your conversation + message : str + Your question about the documents + + Returns + ------- + str + A plain text answer based on your document content + + Notes + ----- + This tool is best for simple questions that don't require conversation context. + + Examples + -------- + chat_simple(session_id='my-session', message='What is the main topic of the document?') + """ +``` + +This numpy-style docstring format ensures compatibility with documentation tools and provides clear, structured information to MCP clients about how to use each tool. + +### Helm Chart Configuration + +For production deployments, the MCP server documentation can be configured through the Helm chart's `values.yaml` file. This provides a structured way to manage tool documentation across different environments. + +The MCP configuration is located under `backend.mcp` in the [values.yaml](../../infrastructure/rag/values.yaml) file: + +```yaml +backend: + mcp: + # Basic MCP server settings + name: "mcp" + port: "8000" + host: "0.0.0.0" + + # Chat simple tool configuration + chatSimpleDescription: "Send a message to the RAG system and get a simple text response.\n\nThis is the simplest way to interact with the RAG system - just provide a message and get back the answer as plain text." + chatSimpleParameterDescriptions: + session_id: "Unique identifier for the chat session." + message: "The message/question to ask the RAG system." + chatSimpleReturns: "The answer from the RAG system as plain text." + chatSimpleNotes: "" + chatSimpleExamples: "" + + # Chat with history tool configuration + chatWithHistoryDescription: "Send a message with conversation history and get structured response.\n\nProvide conversation history as a simple list of dictionaries.\nEach history item should have 'role' (either 'user' or 'assistant') and 'message' keys." + chatWithHistoryParameterDescriptions: + session_id: "Unique identifier for the chat session." + message: "The current message/question to ask." + history: "Previous conversation history. Each item should be:\n {\"role\": \"user\" or \"assistant\", \"message\": \"the message text\"}" + chatWithHistoryReturns: "Response containing:\n - answer: The response text\n - finish_reason: Why the response ended\n - citations: List of source documents used (simplified)" + chatWithHistoryNotes: "" + chatWithHistoryExamples: "" +``` + +These values are automatically converted to the appropriate environment variables (with `MCP_` prefix) when the Helm chart is deployed. The `chatSimpleParameterDescriptions` and `chatWithHistoryParameterDescriptions` dictionaries are automatically converted to JSON format for consumption by the MCP server. + +This approach allows you to: + +- Manage documentation consistently across environments +- Version control your tool documentation +- Use different documentation for different deployments (dev, staging, production) +- Leverage Helm's templating features for dynamic documentation + ## Deployment The MCP server is designed to be deployed alongside the main RAG backend as a sidecar container. A detailed explanation of the deployment can be found in the [main README](../README.md) and the [infrastructure README](../rag-infrastructure/README.md) of the project. diff --git a/services/mcp-server/poetry.lock b/services/mcp-server/poetry.lock index 5dbc2d08..42eccfd5 100644 --- a/services/mcp-server/poetry.lock +++ b/services/mcp-server/poetry.lock @@ -1459,6 +1459,24 @@ files = [ test = ["async-timeout ; python_version < \"3.11\"", "pytest", "pytest-asyncio (>=0.17)", "pytest-trio", "testpath", "trio"] trio = ["trio"] +[[package]] +name = "jinja2" +version = "3.1.6" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, + {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + [[package]] name = "keyring" version = "25.6.0" @@ -1514,6 +1532,77 @@ profiling = ["gprof2dot"] rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] +[[package]] +name = "markupsafe" +version = "3.0.2" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, + {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, +] + [[package]] name = "mccabe" version = "0.7.0" @@ -2851,4 +2940,4 @@ cffi = ["cffi (>=1.11)"] [metadata] lock-version = "2.1" python-versions = "^3.11" -content-hash = "f48fb17b734be5d6ae331b5f4761727ad0726c42c0f13f5a575383739c1d9f56" +content-hash = "b5b92e224e2bbbd3b62f35c33ad1a68ed2f7351704e1875e316ed89290c111bd" diff --git a/services/mcp-server/pyproject.toml b/services/mcp-server/pyproject.toml index 1db8abb5..95fb4949 100644 --- a/services/mcp-server/pyproject.toml +++ b/services/mcp-server/pyproject.toml @@ -12,6 +12,7 @@ poetry = "^2.1.3" pydantic-settings = "^2.9.1" dependency-injector = "^4.46.0" python-dateutil = "^2.9.0.post0" +jinja2 = "^3.1.6" [tool.poetry.group.dev.dependencies] debugpy = "^1.8.1" @@ -60,7 +61,7 @@ multiline-quotes = '"""' dictionaries = ["en_US", "python", "technical", "pandas"] ban-relative-imports = true per-file-ignores = """ - ./tests/*: S101, + ./tests/*: S101,S104 ./src/settings/mcp_settings.py: S104, """ @@ -95,3 +96,20 @@ skip_gitignore = true [tool.pylint] max-line-length = 120 + +[tool.pytest.ini_options] +testpaths = ["tests"] +pythonpath = ["src", "tests"] +addopts = [ + "-v", + "--tb=short", + "--strict-markers", + "--disable-warnings", + "--color=yes" +] +markers = [ + "slow: marks tests as slow (deselect with '-m \"not slow\"')", + "integration: marks tests as integration tests", + "unit: marks tests as unit tests" +] + diff --git a/services/mcp-server/src/__init__.py b/services/mcp-server/src/__init__.py new file mode 100644 index 00000000..1d941a37 --- /dev/null +++ b/services/mcp-server/src/__init__.py @@ -0,0 +1 @@ +"""MCP Server source package.""" diff --git a/services/mcp-server/src/docstring_system.py b/services/mcp-server/src/docstring_system.py new file mode 100644 index 00000000..e2081033 --- /dev/null +++ b/services/mcp-server/src/docstring_system.py @@ -0,0 +1,249 @@ +"""Extensible docstring system using Jinja2 templates and decorators.""" + +import functools +import inspect +from typing import Callable + +from jinja2 import Environment +from pydantic_settings import BaseSettings + + +class DocstringTemplateSystem: + """System for managing extensible docstrings using Jinja2 templates.""" + + def __init__(self, settings: BaseSettings): + """Initialize the docstring template system. + + Parameters + ---------- + settings : BaseSettings + Settings object containing template configurations + """ + self.settings = settings + self._setup_templates() + + def render_docstring(self, template_name: str, **kwargs: dict) -> str: + """Render a docstring using the template. + + Parameters + ---------- + template_name : str + Name of the template to render + **kwargs : dict + Keyword arguments to pass to the render function of the template + + Returns + ------- + str + Rendered docstring + """ + return self.template.render(**kwargs) + + def _setup_templates(self): + """Set up the default Jinja2 templates.""" + # Single template string + self.template_string = """{{ docstring_content }} + +Parameters +---------- +{% for param in parameters %} +{{ param.name }}: {{ param.type }} + {{ param.description }} +{% endfor %} + +Returns +------- +{{ returns.type }} + {{ returns.description }} +{% if notes %} + +Notes +----- +{{ notes }} +{% endif %} +{% if examples %} + +Examples +-------- +{{ examples }} +{% endif %}""" + + self.env = Environment(autoescape=True) + self.template = self.env.from_string(self.template_string) + + +def _extract_parameter_info(sig: inspect.Signature, param_descriptions: dict) -> list[dict]: + """Extract parameter information from function signature. + + Parameters + ---------- + sig : inspect.Signature + Function signature to extract parameters from + param_descriptions : dict + Dictionary mapping parameter names to descriptions + + Returns + ------- + list[dict] + List of parameter dictionaries with name, type, and description + """ + parameters = [] + + for param_name, param in sig.parameters.items(): + if param_name == "self": # Skip 'self' parameter + continue + + # Get type annotation + param_type = "Any" + if param.annotation != inspect.Parameter.empty: + param_type = getattr(param.annotation, "__name__", str(param.annotation)) + + # Add default value info if present + if param.default != inspect.Parameter.empty: + if param.default is None: + param_type += ", optional" + else: + param_type += f", default={param.default}" + + # Get description from settings or use default + description = param_descriptions.get(param_name, f"Parameter {param_name}") + parameters.append({"name": param_name, "type": param_type, "description": description}) + + return parameters + + +def _get_return_info(func: Callable, settings, config_prefix: str) -> dict: + """Get return type and description information. + + Parameters + ---------- + func : Callable + Function to get return info for + settings : object + Settings object containing configuration + config_prefix : str + Configuration prefix for settings lookup + + Returns + ------- + dict + Dictionary with return type and description + """ + return_type = "Any" + if func.__annotations__.get("return") is not None: + return_annotation = func.__annotations__["return"] + return_type = getattr(return_annotation, "__name__", str(return_annotation)) + + return_description = "Return value" + if hasattr(settings, f"{config_prefix}_returns"): + return_description = getattr(settings, f"{config_prefix}_returns") + + return {"type": return_type, "description": return_description} + + +def _get_settings_value(settings, config_prefix: str, suffix: str, default: str = "") -> str: + """Get a value from settings with the given prefix and suffix. + + Parameters + ---------- + settings : object + Settings object to get value from + config_prefix : str + Configuration prefix + suffix : str + Configuration suffix + default : str, optional + Default value if setting not found + + Returns + ------- + str + The settings value or default + """ + attr_name = f"{config_prefix}_{suffix}" + if hasattr(settings, attr_name): + return getattr(settings, attr_name) + return default + + +def extensible_docstring(config_prefix: str): + """Decorator for creating extensible docstrings. + + Parameters + ---------- + config_prefix : str + Prefix for configuration attributes in settings (e.g., "chat_simple") + + Returns + ------- + Callable + Decorated function with dynamic docstring + """ + + def decorator(func: Callable) -> Callable: + @functools.wraps(func) + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + + # Get the instance and settings from the method + def update_docstring(instance): + if not hasattr(instance, "__docstring_system"): + return + + docstring_system = instance.__docstring_system + + # Get docstring content from settings + docstring_content = _get_settings_value(instance._settings, config_prefix, "description") + + # Extract parameters from function signature + sig = inspect.signature(func) + param_descriptions = {} + if hasattr(instance._settings, f"{config_prefix}_parameter_descriptions"): + param_descriptions = getattr(instance._settings, f"{config_prefix}_parameter_descriptions") + + parameters = _extract_parameter_info(sig, param_descriptions) + returns = _get_return_info(func, instance._settings, config_prefix) + + # Get optional notes and examples + notes = _get_settings_value(instance._settings, config_prefix, "notes") + examples = _get_settings_value(instance._settings, config_prefix, "examples") + + # Render the docstring using the function name as template + template_name = func.__name__ + rendered_docstring = docstring_system.render_docstring( + template_name, + docstring_content=docstring_content, + parameters=parameters, + returns=returns, + notes=notes, + examples=examples, + ) + + wrapper.__doc__ = rendered_docstring + + # Store the update function for later use + wrapper._update_docstring = update_docstring + wrapper._original_func = func + + return wrapper + + return decorator + + +def setup_extensible_docstrings(instance, docstring_system: DocstringTemplateSystem): + """Set up extensible docstrings for all decorated methods in an instance. + + Parameters + ---------- + instance : object + The instance to set up docstrings for + docstring_system : DocstringTemplateSystem + The docstring system to use for rendering + """ + instance.__docstring_system = docstring_system + + # Find all methods with extensible docstrings and update them + for attr_name in dir(instance): + attr = getattr(instance, attr_name) + if hasattr(attr, "_update_docstring"): + attr._update_docstring(instance) diff --git a/services/mcp-server/src/rag_mcp_server.py b/services/mcp-server/src/rag_mcp_server.py index d7d2a591..ff552d30 100644 --- a/services/mcp-server/src/rag_mcp_server.py +++ b/services/mcp-server/src/rag_mcp_server.py @@ -11,6 +11,8 @@ from rag_backend_client.openapi_client.models.chat_role import ChatRole from typing import Any +from docstring_system import DocstringTemplateSystem, extensible_docstring, setup_extensible_docstrings + logger = logging.getLogger(__name__) @@ -23,6 +25,11 @@ def __init__(self, api_client: RagApi, mcp_server: FastMCP, settings: BaseSettin self._api_client = api_client self._server = mcp_server self._settings = settings + + # Initialize the docstring system + docstring_system = DocstringTemplateSystem(settings) + setup_extensible_docstrings(self, docstring_system) + self._register_tools() def run(self): @@ -30,54 +37,16 @@ def run(self): logger.info(f"Starting FastMCP Server on {self.TRANSPORT}://{self._settings.host}:{self._settings.port}") self._server.run(transport=self.TRANSPORT, host=self._settings.host, port=self._settings.port) + @extensible_docstring("chat_simple") async def chat_simple(self, session_id: str, message: str) -> str: - """Send a message to the RAG system and get a simple text response. - - This is the simplest way to interact with the RAG system - just provide a message - and get back the answer as plain text. - - Parameters - ---------- - session_id: str - Unique identifier for the chat session. - message: str - The message/question to ask the RAG system. - - Returns - ------- - str - The answer from the RAG system as plain text. - """ chat_request = ChatRequest(message=message) response = await self._handle_chat(session_id, chat_request) return response.answer + @extensible_docstring("chat_with_history") async def chat_with_history( self, session_id: str, message: str, history: list[dict[str, str]] = None ) -> dict[str, Any]: - """Send a message with conversation history and get structured response. - - Provide conversation history as a simple list of dictionaries. - Each history item should have 'role' (either 'user' or 'assistant') and 'message' keys. - - Parameters - ---------- - session_id: str - Unique identifier for the chat session. - message: str - The current message/question to ask. - history: List[Dict[str, str]], optional - Previous conversation history. Each item should be: - {"role": "user" or "assistant", "message": "the message text"} - - Returns - ------- - Dict[str, Any] - Response containing: - - answer: The response text - - finish_reason: Why the response ended - - citations: List of source documents used (simplified) - """ # Build chat history if provided chat_history = None if history: diff --git a/services/mcp-server/src/settings/mcp_settings.py b/services/mcp-server/src/settings/mcp_settings.py index a4eb1cfa..08eec17d 100644 --- a/services/mcp-server/src/settings/mcp_settings.py +++ b/services/mcp-server/src/settings/mcp_settings.py @@ -1,4 +1,4 @@ -"""Module that contains settings for the Fake LLM.""" +"""Module that contains settings for the MCP server.""" from pydantic import Field from pydantic_settings import BaseSettings @@ -31,3 +31,50 @@ class Config: host: str = Field(default="0.0.0.0") port: int = Field(default=8000) name: str = Field(default="RAG MCP server") + + # Chat Simple Method Configuration + chat_simple_description: str = Field( + default=( + "Send a message to the RAG system and get a simple text response.\n\n" + "This is the simplest way to interact with the RAG system - just provide a message " + "and get back the answer as plain text." + ) + ) + chat_simple_parameter_descriptions: dict[str, str] = Field( + default_factory=lambda: { + "session_id": "Unique identifier for the chat session.", + "message": "The message/question to ask the RAG system.", + } + ) + chat_simple_returns: str = Field(default="The answer from the RAG system as plain text.") + chat_simple_notes: str = Field(default="") + chat_simple_examples: str = Field(default="") + + # Chat With History Method Configuration + chat_with_history_description: str = Field( + default=( + "Send a message with conversation history and get structured response.\n\n" + "Provide conversation history as a simple list of dictionaries.\n" + "Each history item should have 'role' (either 'user' or 'assistant') and 'message' keys." + ) + ) + chat_with_history_parameter_descriptions: dict[str, str] = Field( + default_factory=lambda: { + "session_id": "Unique identifier for the chat session.", + "message": "The current message/question to ask.", + "history": ( + "Previous conversation history. Each item should be:\n" + ' {"role": "user" or "assistant", "message": "the message text"}' + ), + } + ) + chat_with_history_returns: str = Field( + default=( + "Response containing:\n" + " - answer: The response text\n" + " - finish_reason: Why the response ended\n" + " - citations: List of source documents used (simplified)" + ) + ) + chat_with_history_notes: str = Field(default="") + chat_with_history_examples: str = Field(default="") diff --git a/services/mcp-server/tests/docstring_system_test.py b/services/mcp-server/tests/docstring_system_test.py new file mode 100644 index 00000000..f678b463 --- /dev/null +++ b/services/mcp-server/tests/docstring_system_test.py @@ -0,0 +1,356 @@ +import pytest + +from src.docstring_system import DocstringTemplateSystem, extensible_docstring, setup_extensible_docstrings +from src.settings.mcp_settings import MCPSettings + + +# Fixtures +@pytest.fixture +def settings(): + """Create a default MCPSettings instance.""" + return MCPSettings() + + +@pytest.fixture +def docstring_system(settings): + """Create a DocstringTemplateSystem instance.""" + return DocstringTemplateSystem(settings) + + +@pytest.fixture +def test_class_factory(): + """Factory fixture for creating test classes.""" + + def _create_test_class(settings): + class TestClass: + def __init__(self, settings): + self._settings = settings + docstring_system = DocstringTemplateSystem(settings) + setup_extensible_docstrings(self, docstring_system) + + @extensible_docstring("chat_simple") + def chat_simple(self, session_id: str, message: str) -> str: + return f"Response for {session_id}: {message}" + + @extensible_docstring("chat_with_history") + def chat_with_history(self, session_id: str, message: str, history: list = None) -> dict: + return {"answer": f"Response for {session_id}: {message}", "citations": []} + + return TestClass(settings) + + return _create_test_class + + +# Tests for DocstringTemplateSystem +def test_docstring_system_initialization(settings, docstring_system): + """Test that DocstringTemplateSystem initializes correctly.""" + assert docstring_system.env is not None + assert docstring_system.settings is not None + assert docstring_system.settings == settings + + +def test_render_docstring_basic(docstring_system): + """Test basic docstring rendering.""" + result = docstring_system.render_docstring( + "chat_simple", + docstring_content="Test description", + parameters=[], + returns={"type": "str", "description": "Test return"}, + notes="", + examples="", + ) + + assert "Test description" in result + assert "Parameters" in result + assert "Returns" in result + assert "str" in result + assert "Test return" in result + + +def test_render_docstring_with_parameters(docstring_system): + """Test docstring rendering with parameters.""" + parameters = [ + {"name": "param1", "type": "str", "description": "First parameter"}, + {"name": "param2", "type": "int", "description": "Second parameter"}, + ] + + result = docstring_system.render_docstring( + "chat_simple", + docstring_content="Test description", + parameters=parameters, + returns={"type": "str", "description": "Test return"}, + notes="", + examples="", + ) + + assert "param1: str" in result + assert "First parameter" in result + assert "param2: int" in result + assert "Second parameter" in result + + +def test_render_docstring_with_notes_and_examples(docstring_system): + """Test docstring rendering with notes and examples.""" + result = docstring_system.render_docstring( + "chat_simple", + docstring_content="Test description", + parameters=[], + returns={"type": "str", "description": "Test return"}, + notes="This is a test note", + examples=">>> example_function()\n'result'", + ) + + assert "Notes" in result + assert "This is a test note" in result + assert "Examples" in result + assert ">>> example_function()" in result + + +def test_render_docstring_without_notes_and_examples(docstring_system): + """Test docstring rendering without notes and examples.""" + result = docstring_system.render_docstring( + "chat_simple", + docstring_content="Test description", + parameters=[], + returns={"type": "str", "description": "Test return"}, + notes="", + examples="", + ) + + assert "Notes" not in result + assert "Examples" not in result + + +# Tests for extensible_docstring decorator +def test_decorator_creation(): + """Test that the decorator can be created.""" + decorator = extensible_docstring("chat_simple") + assert decorator is not None + + # Test that it can decorate a function + @decorator + def test_function(): + pass + + assert test_function is not None + assert hasattr(test_function, "_update_docstring") + assert hasattr(test_function, "_original_func") + + +def test_decorator_preserves_function_metadata(): + """Test that the decorator preserves function metadata.""" + + @extensible_docstring("chat_simple") + def test_function(param1: str, param2: int) -> str: + """Original docstring.""" + return "test" + + assert test_function.__name__ == "test_function" + assert test_function._original_func.__name__ == "test_function" + + +def test_decorator_function_execution(): + """Test that decorated functions still execute correctly.""" + + @extensible_docstring("chat_simple") + def test_function(x: int) -> int: + return x * 2 + + result = test_function(5) + assert result == 10 + + +# Tests for MCPSettings +def test_default_settings(settings): + """Test that default settings are loaded correctly.""" + # Test basic settings + assert settings.host == "0.0.0.0" # nosec S104 - test setting only + assert settings.port == 8000 + assert settings.name == "RAG MCP server" + + # Test chat_simple settings + assert settings.chat_simple_description is not None + assert isinstance(settings.chat_simple_parameter_descriptions, dict) + assert isinstance(settings.chat_simple_returns, str) + assert settings.chat_simple_notes == "" + assert settings.chat_simple_examples == "" + + # Test chat_with_history settings + assert settings.chat_with_history_description is not None + assert isinstance(settings.chat_with_history_parameter_descriptions, dict) + assert isinstance(settings.chat_with_history_returns, str) + assert settings.chat_with_history_notes == "" + assert settings.chat_with_history_examples == "" + + +def test_parameter_descriptions(): + """Test parameter descriptions configuration.""" + param_descriptions = {"test_param": "Test description for parameter"} + + settings = MCPSettings(chat_simple_parameter_descriptions=param_descriptions) + + assert settings.chat_simple_parameter_descriptions == param_descriptions + + +def test_return_config(): + """Test return description configuration.""" + return_desc = "Test return description" + assert return_desc == "Test return description" + + +def test_custom_settings(): + """Test custom settings override.""" + custom_param_descriptions = {"custom_param": "Custom parameter description"} + custom_returns = "Custom return description" + + settings = MCPSettings( + chat_simple_description="Custom description", + chat_simple_parameter_descriptions=custom_param_descriptions, + chat_simple_returns=custom_returns, + chat_simple_notes="Custom notes", + chat_simple_examples="Custom examples", + ) + + assert settings.chat_simple_description == "Custom description" + assert settings.chat_simple_parameter_descriptions == custom_param_descriptions + assert settings.chat_simple_returns == custom_returns + assert settings.chat_simple_notes == "Custom notes" + assert settings.chat_simple_examples == "Custom examples" + + +# Integration tests +def test_setup_extensible_docstrings(settings, test_class_factory): + """Test that setup_extensible_docstrings works correctly.""" + instance = test_class_factory(settings) + + # Check that the docstring system is set up + assert hasattr(instance, "__docstring_system") + + # Check that docstrings are generated + assert instance.chat_simple.__doc__ is not None + assert instance.chat_with_history.__doc__ is not None + + +def test_generated_docstrings_content(settings, test_class_factory): + """Test that generated docstrings contain expected content.""" + instance = test_class_factory(settings) + + # Test chat_simple docstring + simple_doc = instance.chat_simple.__doc__ + assert "Send a message to the RAG system" in simple_doc + assert "Parameters" in simple_doc + assert "session_id: str" in simple_doc + assert "message: str" in simple_doc + assert "Returns" in simple_doc + assert "str" in simple_doc + + # Test chat_with_history docstring + history_doc = instance.chat_with_history.__doc__ + assert "Send a message with conversation history" in history_doc + assert "Parameters" in history_doc + assert "session_id: str" in history_doc + assert "message: str" in history_doc + assert "history: list, optional" in history_doc + assert "Returns" in history_doc + assert "dict" in history_doc # Return type from function signature + + +def test_custom_configuration(test_class_factory): + """Test that custom configuration works.""" + # Create custom settings + custom_settings = MCPSettings( + chat_simple_description="Custom simple description", + chat_simple_parameter_descriptions={ + "session_id": "Custom session parameter", + "message": "Custom message parameter", + }, + chat_simple_returns="Custom return value", + chat_simple_notes="Custom notes section", + chat_simple_examples=">>> custom_example()\n'result'", + ) + + instance = test_class_factory(custom_settings) + + # Check that custom content is in the docstring + simple_doc = instance.chat_simple.__doc__ + assert "Custom simple description" in simple_doc + assert "session_id: str" in simple_doc + assert "Custom session parameter" in simple_doc + assert "message: str" in simple_doc + assert "Custom message parameter" in simple_doc + assert "str" in simple_doc # Return type from function signature + assert "Custom return value" in simple_doc + assert "Notes" in simple_doc + assert "Custom notes section" in simple_doc + assert "Examples" in simple_doc + assert ">>> custom_example()" in simple_doc + + +def test_function_execution_still_works(settings, test_class_factory): + """Test that decorated functions still execute correctly.""" + instance = test_class_factory(settings) + + # Test chat_simple execution + result = instance.chat_simple("test_session", "test_message") + assert result == "Response for test_session: test_message" + + # Test chat_with_history execution + result = instance.chat_with_history("test_session", "test_message", []) + expected = {"answer": "Response for test_session: test_message", "citations": []} + assert result == expected + + +# Edge case tests +def test_missing_settings_attributes(): + """Test behavior when settings attributes are missing.""" + # Create minimal settings without some attributes + settings = MCPSettings() + + # Remove some attributes to test handling + delattr(settings, "chat_simple_notes") + delattr(settings, "chat_simple_examples") + + class TestClass: + def __init__(self, settings): + self._settings = settings + docstring_system = DocstringTemplateSystem(settings) + setup_extensible_docstrings(self, docstring_system) + + @extensible_docstring("chat_simple") + def test_method(self): + pass + + # Should not raise an exception + instance = TestClass(settings) + assert instance.test_method.__doc__ is not None + + +def test_empty_configuration(): + """Test behavior with empty configuration.""" + settings = MCPSettings( + chat_simple_description="", chat_simple_parameter_descriptions={}, chat_simple_notes="", chat_simple_examples="" + ) + + class TestClass: + def __init__(self, settings): + self._settings = settings + docstring_system = DocstringTemplateSystem(settings) + setup_extensible_docstrings(self, docstring_system) + + @extensible_docstring("chat_simple") + def test_method(self): + pass + + instance = TestClass(settings) + doc = instance.test_method.__doc__ + + # Should still have structure but with empty content + assert "Parameters" in doc + assert "Returns" in doc + assert "Notes" not in doc + assert "Examples" not in doc + + +if __name__ == "__main__": + # Run pytest with verbose output + pytest.main([__file__, "-v"]) diff --git a/services/mcp-server/tests/dummy7_test.py b/services/mcp-server/tests/dummy7_test.py deleted file mode 100644 index 1428394b..00000000 --- a/services/mcp-server/tests/dummy7_test.py +++ /dev/null @@ -1,3 +0,0 @@ -def test_dummy() -> None: - print("Dummy test.") - assert True