From 36982cddb1ddb3753c98cc9891585bed3f0bd6ea Mon Sep 17 00:00:00 2001 From: mathislucka Date: Mon, 19 May 2025 17:05:36 +0200 Subject: [PATCH 01/15] feat: add index formatting utilities --- .../tools/formatting_utils_index.py | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 src/deepset_mcp/tools/formatting_utils_index.py diff --git a/src/deepset_mcp/tools/formatting_utils_index.py b/src/deepset_mcp/tools/formatting_utils_index.py new file mode 100644 index 00000000..bd98a4f1 --- /dev/null +++ b/src/deepset_mcp/tools/formatting_utils_index.py @@ -0,0 +1,31 @@ +from deepset_mcp.api.indexes.models import Index, IndexList + + +def index_to_llm_readable_string(index: Index) -> str: + """Creates a string representation of an index that is readable by LLMs.""" + index_parts = [ + f' + +### Basic Information + +**Name:** {index.name} +**ID:** {index.id} +**Description:** {index.description if index.description else "No description provided"}\n' + ] + + if index.config_yaml is not None: + index_parts.append("\n### Index Configuration") + index_parts.append(f"\n```yaml\n{index.config_yaml}\n```") + + index_parts.append(f'\n') + + return "\n".join(index_parts) + + +def index_list_to_llm_readable_string(index_list: IndexList) -> str: + """Creates a string representation of a list of indexes that is readable by LLMs.""" + if not index_list.data: + return "No indexes found." + + index_strings = [index_to_llm_readable_string(index) for index in index_list.data] + return "\n\n".join(index_strings) \ No newline at end of file From 2a4a43f1f272872419846cc47fe7d995f5a48fc2 Mon Sep 17 00:00:00 2001 From: mathislucka Date: Mon, 19 May 2025 17:06:06 +0200 Subject: [PATCH 02/15] feat: add index tools implementation --- src/deepset_mcp/tools/indexes.py | 70 ++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 src/deepset_mcp/tools/indexes.py diff --git a/src/deepset_mcp/tools/indexes.py b/src/deepset_mcp/tools/indexes.py new file mode 100644 index 00000000..92fd3ed2 --- /dev/null +++ b/src/deepset_mcp/tools/indexes.py @@ -0,0 +1,70 @@ +from deepset_mcp.api.protocols import AsyncClientProtocol +from deepset_mcp.api.exceptions import BadRequestError, ResourceNotFoundError, UnexpectedAPIError +from deepset_mcp.tools.formatting_utils_index import index_to_llm_readable_string, index_list_to_llm_readable_string + + +async def list_indexes(client: AsyncClientProtocol, workspace: str) -> str: + """Retrieves a list of all indexes available within the currently configured deepset workspace.""" + response = await client.indexes(workspace=workspace).list() + return index_list_to_llm_readable_string(response) + + +async def get_index(client: AsyncClientProtocol, workspace: str, index_name: str) -> str: + """Fetches detailed configuration information for a specific index, identified by its unique `index_name`.""" + try: + response = await client.indexes(workspace=workspace).get(index_name) + except ResourceNotFoundError: + return f"There is no index named '{index_name}'. Did you mean to create it?" + + return index_to_llm_readable_string(response) + + +async def create_index( + client: AsyncClientProtocol, workspace: str, index_name: str, yaml_configuration: str, description: str | None = None +) -> str: + """Creates a new index within the currently configured deepset workspace.""" + try: + response = await client.indexes(workspace=workspace).create( + name=index_name, + yaml_config=yaml_configuration, + description=description + ) + except ResourceNotFoundError: + return f"There is no workspace named '{workspace}'. Did you mean to configure it?" + except BadRequestError as e: + return f"Failed to create index '{index_name}': {e}" + except UnexpectedAPIError as e: + return f"Failed to create index '{index_name}': {e}" + + return f"Index '{index_name}' created successfully." + + +async def update_index( + client: AsyncClientProtocol, + workspace: str, + index_name: str, + updated_index_name: str | None = None, + yaml_configuration: str | None = None +) -> str: + """Updates an existing index in the specified workspace. + + This function can update either the name or the configuration of an existing index, or both. + At least one of updated_index_name or yaml_configuration must be provided. + """ + if not updated_index_name and not yaml_configuration: + return "You must provide either a new name or a new configuration to update the index." + + try: + await client.indexes(workspace=workspace).update( + index_name=index_name, + updated_index_name=updated_index_name, + yaml_config=yaml_configuration + ) + except ResourceNotFoundError: + return f"There is no index named '{index_name}'. Did you mean to create it?" + except BadRequestError as e: + return f"Failed to update index '{index_name}': {e}" + except UnexpectedAPIError as e: + return f"Failed to update index '{index_name}': {e}" + + return f"Index '{index_name}' updated successfully." \ No newline at end of file From 864407a5946773a93232cc1a5fae7a4438cb083d Mon Sep 17 00:00:00 2001 From: mathislucka Date: Mon, 19 May 2025 17:06:49 +0200 Subject: [PATCH 03/15] test: add unit tests for index tools --- test/unit/tools/test_indexes.py | 210 ++++++++++++++++++++++++++++++++ 1 file changed, 210 insertions(+) create mode 100644 test/unit/tools/test_indexes.py diff --git a/test/unit/tools/test_indexes.py b/test/unit/tools/test_indexes.py new file mode 100644 index 00000000..14d539e3 --- /dev/null +++ b/test/unit/tools/test_indexes.py @@ -0,0 +1,210 @@ +from typing import AsyncGenerator, Callable + +import pytest +from pytest_mock import MockFixture + +from deepset_mcp.api.exceptions import BadRequestError, ResourceNotFoundError, UnexpectedAPIError +from deepset_mcp.api.indexes.models import Index, IndexList +from deepset_mcp.api.protocols import AsyncClientProtocol, IndexResourceProtocol +from deepset_mcp.tools.indexes import create_index, get_index, list_indexes, update_index +from test.unit.conftest import BaseFakeClient + + +class FakeIndexResource(IndexResourceProtocol): + def __init__(self, mocker: MockFixture): + self._mocker = mocker + self._list = self._mocker.AsyncMock(return_value=IndexList(data=[])) + self._get = self._mocker.AsyncMock( + return_value=Index( + name="test_index", + config_yaml="config", + id="123", + description="Test index", + ) + ) + self._create = self._mocker.AsyncMock( + return_value=Index( + name="test_index", + config_yaml="config", + id="123", + description="Test index", + ) + ) + self._update = self._mocker.AsyncMock() + + async def list(self, limit: int = 10, page_number: int = 1) -> IndexList: + return await self._list(limit=limit, page_number=page_number) + + async def get(self, index_name: str) -> Index: + return await self._get(index_name=index_name) + + async def create(self, name: str, yaml_config: str, description: str | None = None) -> Index: + return await self._create(name=name, yaml_config=yaml_config, description=description) + + async def update( + self, index_name: str, updated_index_name: str | None = None, yaml_config: str | None = None + ) -> None: + await self._update(index_name=index_name, updated_index_name=updated_index_name, yaml_config=yaml_config) + + +@pytest.fixture(name="client") +def client_fixture(mocker: MockFixture) -> AsyncGenerator[AsyncClientProtocol, None]: + class FakeClient(BaseFakeClient): + def indexes(self, workspace: str) -> IndexResourceProtocol: + return FakeIndexResource(mocker) + + c = FakeClient() + return c + + +PYTEST_TUPLE = tuple[AsyncClientProtocol, MockFixture] + + +@pytest.mark.asyncio +async def test_list_indexes_returns_formatted_string_when_no_indexes(client: AsyncClientProtocol) -> None: + result = await list_indexes(client=client, workspace="test") + + assert result == "No indexes found." + + +@pytest.mark.asyncio +async def test_list_indexes_returns_formatted_string_with_indexes( + client: AsyncClientProtocol, mocker: MockFixture +) -> None: + index = Index(name="test_index", config_yaml="config", id="123", description="Test index") + fake_resource: IndexResourceProtocol = client.indexes(workspace="test") + assert isinstance(fake_resource, FakeIndexResource) + fake_resource._list.return_value = IndexList(data=[index]) + + result = await list_indexes(client=client, workspace="test") + + assert "test_index" in result + assert "config" in result + assert "123" in result + assert "Test index" in result + + +@pytest.mark.asyncio +async def test_get_index_returns_formatted_string( + client: AsyncClientProtocol, mocker: MockFixture +) -> None: + result = await get_index(client=client, workspace="test", index_name="test_index") + + assert "test_index" in result + assert "config" in result + assert "123" in result + assert "Test index" in result + + +@pytest.mark.asyncio +async def test_get_index_returns_error_message_when_index_not_found( + client: AsyncClientProtocol, mocker: MockFixture +) -> None: + fake_resource: IndexResourceProtocol = client.indexes(workspace="test") + assert isinstance(fake_resource, FakeIndexResource) + fake_resource._get.side_effect = ResourceNotFoundError("Not found") + + result = await get_index(client=client, workspace="test", index_name="test_index") + + assert "There is no index named 'test_index'" in result + + +@pytest.mark.asyncio +async def test_create_index_returns_success_message( + client: AsyncClientProtocol, mocker: MockFixture +) -> None: + result = await create_index( + client=client, + workspace="test", + index_name="test_index", + yaml_configuration="config", + description="Test index", + ) + + assert "Index 'test_index' created successfully." == result + + +@pytest.mark.parametrize( + "error_class,expected_message", + [ + (ResourceNotFoundError, "There is no workspace named 'test'"), + (BadRequestError, "Failed to create index 'test_index'"), + (UnexpectedAPIError, "Failed to create index 'test_index'"), + ], +) +async def test_create_index_returns_error_message( + client: AsyncClientProtocol, + mocker: MockFixture, + error_class: type[Exception], + expected_message: str, +) -> None: + fake_resource: IndexResourceProtocol = client.indexes(workspace="test") + assert isinstance(fake_resource, FakeIndexResource) + fake_resource._create.side_effect = error_class("Error") + + result = await create_index( + client=client, + workspace="test", + index_name="test_index", + yaml_configuration="config", + description="Test index", + ) + + assert expected_message in result + + +@pytest.mark.asyncio +async def test_update_index_returns_success_message( + client: AsyncClientProtocol, mocker: MockFixture +) -> None: + result = await update_index( + client=client, + workspace="test", + index_name="test_index", + updated_index_name="new_test_index", + yaml_configuration="new_config", + ) + + assert "Index 'test_index' updated successfully." == result + + +@pytest.mark.asyncio +async def test_update_index_returns_error_message_when_no_changes_provided( + client: AsyncClientProtocol, mocker: MockFixture +) -> None: + result = await update_index( + client=client, + workspace="test", + index_name="test_index", + ) + + assert "You must provide either a new name or a new configuration to update the index." == result + + +@pytest.mark.parametrize( + "error_class,expected_message", + [ + (ResourceNotFoundError, "There is no index named 'test_index'"), + (BadRequestError, "Failed to update index 'test_index'"), + (UnexpectedAPIError, "Failed to update index 'test_index'"), + ], +) +async def test_update_index_returns_error_message( + client: AsyncClientProtocol, + mocker: MockFixture, + error_class: type[Exception], + expected_message: str, +) -> None: + fake_resource: IndexResourceProtocol = client.indexes(workspace="test") + assert isinstance(fake_resource, FakeIndexResource) + fake_resource._update.side_effect = error_class("Error") + + result = await update_index( + client=client, + workspace="test", + index_name="test_index", + updated_index_name="new_test_index", + yaml_configuration="new_config", + ) + + assert expected_message in result \ No newline at end of file From 6f375f13b36d2fd2de7426f0a7a4489f6847659f Mon Sep 17 00:00:00 2001 From: mathislucka Date: Mon, 19 May 2025 17:07:08 +0200 Subject: [PATCH 04/15] feat: add index tools to MCP server --- src/deepset_mcp/main.py | 77 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/src/deepset_mcp/main.py b/src/deepset_mcp/main.py index 3aa97e84..d57dd6d4 100644 --- a/src/deepset_mcp/main.py +++ b/src/deepset_mcp/main.py @@ -196,6 +196,83 @@ async def search_component_definitions(query: str) -> str: return response +@mcp.tool() +async def list_indexes() -> str: + """Retrieves a list of all indexes available in the deepset workspace. + + Use this to get an overview of existing indexes and their configurations. + The response includes basic information for each index. + """ + workspace = get_workspace() + async with AsyncDeepsetClient() as client: + response = await list_indexes_tool(client=client, workspace=workspace) + return response + + +@mcp.tool() +async def get_index(index_name: str) -> str: + """Fetches detailed configuration information for a specific index. + + Use this to get the full configuration and details of a single index. + + :param index_name: The name of the index to fetch. + """ + workspace = get_workspace() + async with AsyncDeepsetClient() as client: + response = await get_index_tool(client=client, workspace=workspace, index_name=index_name) + return response + + +@mcp.tool() +async def create_index(index_name: str, yaml_configuration: str, description: str | None = None) -> str: + """Creates a new index in the deepset workspace. + + Use this to create a new index with the given configuration. + Make sure the YAML configuration is valid before creating the index. + + :param index_name: The name for the new index. + :param yaml_configuration: YAML configuration for the index. + :param description: Optional description for the index. + """ + workspace = get_workspace() + async with AsyncDeepsetClient() as client: + response = await create_index_tool( + client=client, + workspace=workspace, + index_name=index_name, + yaml_configuration=yaml_configuration, + description=description + ) + return response + + +@mcp.tool() +async def update_index( + index_name: str, + updated_index_name: str | None = None, + yaml_configuration: str | None = None +) -> str: + """Updates an existing index in the deepset workspace. + + Use this to update the name or configuration of an existing index. + You must provide at least one of updated_index_name or yaml_configuration. + + :param index_name: The name of the index to update. + :param updated_index_name: Optional new name for the index. + :param yaml_configuration: Optional new YAML configuration. + """ + workspace = get_workspace() + async with AsyncDeepsetClient() as client: + response = await update_index_tool( + client=client, + workspace=workspace, + index_name=index_name, + updated_index_name=updated_index_name, + yaml_configuration=yaml_configuration + ) + return response + + # # # @mcp.tool() From 5d1e0271c5830343c1b747a5c5b30d0dad7ad039 Mon Sep 17 00:00:00 2001 From: mathislucka Date: Mon, 19 May 2025 17:07:16 +0200 Subject: [PATCH 05/15] feat: import index tools --- src/deepset_mcp/main.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/deepset_mcp/main.py b/src/deepset_mcp/main.py index d57dd6d4..3122cf3a 100644 --- a/src/deepset_mcp/main.py +++ b/src/deepset_mcp/main.py @@ -18,6 +18,12 @@ update_pipeline as update_pipeline_tool, validate_pipeline as validate_pipeline_tool, ) +from deepset_mcp.tools.indexes import ( + create_index as create_index_tool, + get_index as get_index_tool, + list_indexes as list_indexes_tool, + update_index as update_index_tool, +) from deepset_mcp.tools.pipeline_template import ( get_pipeline_template as get_pipeline_template_tool, list_pipeline_templates as list_pipeline_templates_tool, From d6afa9b59a0cdb3ab505d56b0bed0700be69a83c Mon Sep 17 00:00:00 2001 From: mathislucka Date: Tue, 20 May 2025 10:06:36 +0200 Subject: [PATCH 06/15] refactor: simplify client fixture to remove mock dependency --- test/unit/tools/test_indexes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/unit/tools/test_indexes.py b/test/unit/tools/test_indexes.py index 14d539e3..846e0546 100644 --- a/test/unit/tools/test_indexes.py +++ b/test/unit/tools/test_indexes.py @@ -48,10 +48,10 @@ async def update( @pytest.fixture(name="client") -def client_fixture(mocker: MockFixture) -> AsyncGenerator[AsyncClientProtocol, None]: +def client_fixture() -> AsyncGenerator[AsyncClientProtocol, None]: class FakeClient(BaseFakeClient): def indexes(self, workspace: str) -> IndexResourceProtocol: - return FakeIndexResource(mocker) + return FakeIndexResource() c = FakeClient() return c From f36d417859e50fa223e5e5e473f00f7358aef8ff Mon Sep 17 00:00:00 2001 From: mathislucka Date: Tue, 20 May 2025 10:06:54 +0200 Subject: [PATCH 07/15] refactor: update list indexes test to use new fake implementation --- test/unit/tools/test_indexes.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/test/unit/tools/test_indexes.py b/test/unit/tools/test_indexes.py index 846e0546..980958e3 100644 --- a/test/unit/tools/test_indexes.py +++ b/test/unit/tools/test_indexes.py @@ -69,18 +69,17 @@ async def test_list_indexes_returns_formatted_string_when_no_indexes(client: Asy @pytest.mark.asyncio async def test_list_indexes_returns_formatted_string_with_indexes( - client: AsyncClientProtocol, mocker: MockFixture + client: AsyncClientProtocol ) -> None: index = Index(name="test_index", config_yaml="config", id="123", description="Test index") - fake_resource: IndexResourceProtocol = client.indexes(workspace="test") - assert isinstance(fake_resource, FakeIndexResource) - fake_resource._list.return_value = IndexList(data=[index]) - - result = await list_indexes(client=client, workspace="test") + c = FakeClient() + c._indexes = FakeIndexResource(list_response=IndexList(data=[index])) + + result = await list_indexes(client=c, workspace="test") assert "test_index" in result assert "config" in result - assert "123" in result + assert "123" in result assert "Test index" in result From 8ee720b3c7b8a0d60e2ba7c50eb79b269aabebf4 Mon Sep 17 00:00:00 2001 From: mathislucka Date: Tue, 20 May 2025 10:07:20 +0200 Subject: [PATCH 08/15] refactor: improve index resource test fixture --- test/unit/tools/test_indexes.py | 58 ++++++++++++++++++++------------- 1 file changed, 35 insertions(+), 23 deletions(-) diff --git a/test/unit/tools/test_indexes.py b/test/unit/tools/test_indexes.py index 980958e3..a44a232a 100644 --- a/test/unit/tools/test_indexes.py +++ b/test/unit/tools/test_indexes.py @@ -11,40 +11,52 @@ class FakeIndexResource(IndexResourceProtocol): - def __init__(self, mocker: MockFixture): - self._mocker = mocker - self._list = self._mocker.AsyncMock(return_value=IndexList(data=[])) - self._get = self._mocker.AsyncMock( - return_value=Index( - name="test_index", - config_yaml="config", - id="123", - description="Test index", - ) - ) - self._create = self._mocker.AsyncMock( - return_value=Index( - name="test_index", - config_yaml="config", - id="123", - description="Test index", - ) + def __init__( + self, + list_response: IndexList | None = None, + get_response: Index | None = None, + create_response: Index | None = None, + get_exception: Exception | None = None, + create_exception: Exception | None = None, + update_exception: Exception | None = None, + ) -> None: + self._list_response = list_response or IndexList(data=[]) + self._get_response = get_response or Index( + name="test_index", + config_yaml="config", + id="123", + description="Test index", ) - self._update = self._mocker.AsyncMock() + self._create_response = create_response + self._get_exception = get_exception + self._create_exception = create_exception + self._update_exception = update_exception async def list(self, limit: int = 10, page_number: int = 1) -> IndexList: - return await self._list(limit=limit, page_number=page_number) + return self._list_response async def get(self, index_name: str) -> Index: - return await self._get(index_name=index_name) + if self._get_exception: + raise self._get_exception + return self._get_response async def create(self, name: str, yaml_config: str, description: str | None = None) -> Index: - return await self._create(name=name, yaml_config=yaml_config, description=description) + if self._create_exception: + raise self._create_exception + if self._create_response is not None: + return self._create_response + return Index( + name=name, + config_yaml=yaml_config, + id="123", + description=description, + ) async def update( self, index_name: str, updated_index_name: str | None = None, yaml_config: str | None = None ) -> None: - await self._update(index_name=index_name, updated_index_name=updated_index_name, yaml_config=yaml_config) + if self._update_exception: + raise self._update_exception @pytest.fixture(name="client") From fa16ff33ab379507c367108e7081f56b1693e390 Mon Sep 17 00:00:00 2001 From: mathislucka Date: Tue, 20 May 2025 10:07:29 +0200 Subject: [PATCH 09/15] refactor: update create index test to use new fake implementation --- test/unit/tools/test_indexes.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/test/unit/tools/test_indexes.py b/test/unit/tools/test_indexes.py index a44a232a..b2583999 100644 --- a/test/unit/tools/test_indexes.py +++ b/test/unit/tools/test_indexes.py @@ -145,16 +145,14 @@ async def test_create_index_returns_success_message( ) async def test_create_index_returns_error_message( client: AsyncClientProtocol, - mocker: MockFixture, error_class: type[Exception], expected_message: str, ) -> None: - fake_resource: IndexResourceProtocol = client.indexes(workspace="test") - assert isinstance(fake_resource, FakeIndexResource) - fake_resource._create.side_effect = error_class("Error") + c = FakeClient() + c._indexes = FakeIndexResource(create_exception=error_class("Error")) result = await create_index( - client=client, + client=c, workspace="test", index_name="test_index", yaml_configuration="config", From 31cefbf51e630b33c1646db521267be28cc02bca Mon Sep 17 00:00:00 2001 From: mathislucka Date: Tue, 20 May 2025 10:07:42 +0200 Subject: [PATCH 10/15] refactor: update update index test to use new fake implementation --- test/unit/tools/test_indexes.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/test/unit/tools/test_indexes.py b/test/unit/tools/test_indexes.py index b2583999..350a43aa 100644 --- a/test/unit/tools/test_indexes.py +++ b/test/unit/tools/test_indexes.py @@ -200,16 +200,14 @@ async def test_update_index_returns_error_message_when_no_changes_provided( ) async def test_update_index_returns_error_message( client: AsyncClientProtocol, - mocker: MockFixture, error_class: type[Exception], expected_message: str, ) -> None: - fake_resource: IndexResourceProtocol = client.indexes(workspace="test") - assert isinstance(fake_resource, FakeIndexResource) - fake_resource._update.side_effect = error_class("Error") + c = FakeClient() + c._indexes = FakeIndexResource(update_exception=error_class("Error")) result = await update_index( - client=client, + client=c, workspace="test", index_name="test_index", updated_index_name="new_test_index", From 96b6c1242ac2d78bf2e8b9b1bc2de5f1f0eea45d Mon Sep 17 00:00:00 2001 From: mathislucka Date: Tue, 20 May 2025 10:07:46 +0200 Subject: [PATCH 11/15] refactor: remove unused mocker parameter from test --- test/unit/tools/test_indexes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/tools/test_indexes.py b/test/unit/tools/test_indexes.py index 350a43aa..c517a913 100644 --- a/test/unit/tools/test_indexes.py +++ b/test/unit/tools/test_indexes.py @@ -97,7 +97,7 @@ async def test_list_indexes_returns_formatted_string_with_indexes( @pytest.mark.asyncio async def test_get_index_returns_formatted_string( - client: AsyncClientProtocol, mocker: MockFixture + client: AsyncClientProtocol ) -> None: result = await get_index(client=client, workspace="test", index_name="test_index") From 3fea41123f68bbf19c821f72783f7b398b089cf0 Mon Sep 17 00:00:00 2001 From: mathislucka Date: Tue, 20 May 2025 10:07:55 +0200 Subject: [PATCH 12/15] refactor: remove unused mocker parameter from test --- test/unit/tools/test_indexes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/tools/test_indexes.py b/test/unit/tools/test_indexes.py index c517a913..3dc6f8b3 100644 --- a/test/unit/tools/test_indexes.py +++ b/test/unit/tools/test_indexes.py @@ -122,7 +122,7 @@ async def test_get_index_returns_error_message_when_index_not_found( @pytest.mark.asyncio async def test_create_index_returns_success_message( - client: AsyncClientProtocol, mocker: MockFixture + client: AsyncClientProtocol ) -> None: result = await create_index( client=client, From 92fb3b6447f61700daa1cbe7515e02814056baf5 Mon Sep 17 00:00:00 2001 From: mathislucka Date: Tue, 20 May 2025 10:08:18 +0200 Subject: [PATCH 13/15] refactor: remove unused mocker parameter from test --- test/unit/tools/test_indexes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/tools/test_indexes.py b/test/unit/tools/test_indexes.py index 3dc6f8b3..638bab0b 100644 --- a/test/unit/tools/test_indexes.py +++ b/test/unit/tools/test_indexes.py @@ -164,7 +164,7 @@ async def test_create_index_returns_error_message( @pytest.mark.asyncio async def test_update_index_returns_success_message( - client: AsyncClientProtocol, mocker: MockFixture + client: AsyncClientProtocol ) -> None: result = await update_index( client=client, From d8d904e3e9779ed1bc868859771b713e1d8d2731 Mon Sep 17 00:00:00 2001 From: mathislucka Date: Tue, 20 May 2025 10:08:24 +0200 Subject: [PATCH 14/15] refactor: remove unused mocker parameter from test --- test/unit/tools/test_indexes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/tools/test_indexes.py b/test/unit/tools/test_indexes.py index 638bab0b..bb2ac6d0 100644 --- a/test/unit/tools/test_indexes.py +++ b/test/unit/tools/test_indexes.py @@ -179,7 +179,7 @@ async def test_update_index_returns_success_message( @pytest.mark.asyncio async def test_update_index_returns_error_message_when_no_changes_provided( - client: AsyncClientProtocol, mocker: MockFixture + client: AsyncClientProtocol ) -> None: result = await update_index( client=client, From 25b9610e8ab93e769ced308e1909f8d331b55aa7 Mon Sep 17 00:00:00 2001 From: mathislucka Date: Fri, 23 May 2025 11:36:49 +0200 Subject: [PATCH 15/15] fix: tests --- src/deepset_mcp/main.py | 20 +- .../tools/formatting_utils_index.py | 9 +- src/deepset_mcp/tools/indexes.py | 26 +- test/unit/tools/test_indexes.py | 265 ++++++++++++------ 4 files changed, 209 insertions(+), 111 deletions(-) diff --git a/src/deepset_mcp/main.py b/src/deepset_mcp/main.py index 3122cf3a..bc65d223 100644 --- a/src/deepset_mcp/main.py +++ b/src/deepset_mcp/main.py @@ -11,6 +11,12 @@ list_component_families as list_component_families_tool, search_component_definition as search_component_definition_tool, ) +from deepset_mcp.tools.indexes import ( + create_index as create_index_tool, + get_index as get_index_tool, + list_indexes as list_indexes_tool, + update_index as update_index_tool, +) from deepset_mcp.tools.pipeline import ( create_pipeline as create_pipeline_tool, get_pipeline as get_pipeline_tool, @@ -18,12 +24,6 @@ update_pipeline as update_pipeline_tool, validate_pipeline as validate_pipeline_tool, ) -from deepset_mcp.tools.indexes import ( - create_index as create_index_tool, - get_index as get_index_tool, - list_indexes as list_indexes_tool, - update_index as update_index_tool, -) from deepset_mcp.tools.pipeline_template import ( get_pipeline_template as get_pipeline_template_tool, list_pipeline_templates as list_pipeline_templates_tool, @@ -247,16 +247,14 @@ async def create_index(index_name: str, yaml_configuration: str, description: st workspace=workspace, index_name=index_name, yaml_configuration=yaml_configuration, - description=description + description=description, ) return response @mcp.tool() async def update_index( - index_name: str, - updated_index_name: str | None = None, - yaml_configuration: str | None = None + index_name: str, updated_index_name: str | None = None, yaml_configuration: str | None = None ) -> str: """Updates an existing index in the deepset workspace. @@ -274,7 +272,7 @@ async def update_index( workspace=workspace, index_name=index_name, updated_index_name=updated_index_name, - yaml_configuration=yaml_configuration + yaml_configuration=yaml_configuration, ) return response diff --git a/src/deepset_mcp/tools/formatting_utils_index.py b/src/deepset_mcp/tools/formatting_utils_index.py index bd98a4f1..9e17ee95 100644 --- a/src/deepset_mcp/tools/formatting_utils_index.py +++ b/src/deepset_mcp/tools/formatting_utils_index.py @@ -4,20 +4,21 @@ def index_to_llm_readable_string(index: Index) -> str: """Creates a string representation of an index that is readable by LLMs.""" index_parts = [ - f' + f""" ### Basic Information **Name:** {index.name} -**ID:** {index.id} +**ID:** {index.pipeline_index_id} **Description:** {index.description if index.description else "No description provided"}\n' +""" ] if index.config_yaml is not None: index_parts.append("\n### Index Configuration") index_parts.append(f"\n```yaml\n{index.config_yaml}\n```") - index_parts.append(f'\n') + index_parts.append(f'\n') return "\n".join(index_parts) @@ -28,4 +29,4 @@ def index_list_to_llm_readable_string(index_list: IndexList) -> str: return "No indexes found." index_strings = [index_to_llm_readable_string(index) for index in index_list.data] - return "\n\n".join(index_strings) \ No newline at end of file + return "\n\n".join(index_strings) diff --git a/src/deepset_mcp/tools/indexes.py b/src/deepset_mcp/tools/indexes.py index 92fd3ed2..5785ab03 100644 --- a/src/deepset_mcp/tools/indexes.py +++ b/src/deepset_mcp/tools/indexes.py @@ -1,6 +1,6 @@ -from deepset_mcp.api.protocols import AsyncClientProtocol from deepset_mcp.api.exceptions import BadRequestError, ResourceNotFoundError, UnexpectedAPIError -from deepset_mcp.tools.formatting_utils_index import index_to_llm_readable_string, index_list_to_llm_readable_string +from deepset_mcp.api.protocols import AsyncClientProtocol +from deepset_mcp.tools.formatting_utils_index import index_list_to_llm_readable_string, index_to_llm_readable_string async def list_indexes(client: AsyncClientProtocol, workspace: str) -> str: @@ -15,19 +15,21 @@ async def get_index(client: AsyncClientProtocol, workspace: str, index_name: str response = await client.indexes(workspace=workspace).get(index_name) except ResourceNotFoundError: return f"There is no index named '{index_name}'. Did you mean to create it?" - + return index_to_llm_readable_string(response) async def create_index( - client: AsyncClientProtocol, workspace: str, index_name: str, yaml_configuration: str, description: str | None = None + client: AsyncClientProtocol, + workspace: str, + index_name: str, + yaml_configuration: str, + description: str | None = None, ) -> str: """Creates a new index within the currently configured deepset workspace.""" try: - response = await client.indexes(workspace=workspace).create( - name=index_name, - yaml_config=yaml_configuration, - description=description + await client.indexes(workspace=workspace).create( + name=index_name, yaml_config=yaml_configuration, description=description ) except ResourceNotFoundError: return f"There is no workspace named '{workspace}'. Did you mean to configure it?" @@ -44,7 +46,7 @@ async def update_index( workspace: str, index_name: str, updated_index_name: str | None = None, - yaml_configuration: str | None = None + yaml_configuration: str | None = None, ) -> str: """Updates an existing index in the specified workspace. @@ -56,9 +58,7 @@ async def update_index( try: await client.indexes(workspace=workspace).update( - index_name=index_name, - updated_index_name=updated_index_name, - yaml_config=yaml_configuration + index_name=index_name, updated_index_name=updated_index_name, yaml_config=yaml_configuration ) except ResourceNotFoundError: return f"There is no index named '{index_name}'. Did you mean to create it?" @@ -67,4 +67,4 @@ async def update_index( except UnexpectedAPIError as e: return f"Failed to update index '{index_name}': {e}" - return f"Index '{index_name}' updated successfully." \ No newline at end of file + return f"Index '{index_name}' updated successfully." diff --git a/test/unit/tools/test_indexes.py b/test/unit/tools/test_indexes.py index bb2ac6d0..5bf900ee 100644 --- a/test/unit/tools/test_indexes.py +++ b/test/unit/tools/test_indexes.py @@ -1,11 +1,11 @@ -from typing import AsyncGenerator, Callable +from datetime import datetime import pytest -from pytest_mock import MockFixture from deepset_mcp.api.exceptions import BadRequestError, ResourceNotFoundError, UnexpectedAPIError -from deepset_mcp.api.indexes.models import Index, IndexList -from deepset_mcp.api.protocols import AsyncClientProtocol, IndexResourceProtocol +from deepset_mcp.api.indexes.models import Index, IndexList, IndexStatus +from deepset_mcp.api.protocols import IndexResourceProtocol +from deepset_mcp.api.shared_models import DeepsetUser from deepset_mcp.tools.indexes import create_index, get_index, list_indexes, update_index from test.unit.conftest import BaseFakeClient @@ -16,123 +16,158 @@ def __init__( list_response: IndexList | None = None, get_response: Index | None = None, create_response: Index | None = None, + update_response: Index | None = None, get_exception: Exception | None = None, create_exception: Exception | None = None, update_exception: Exception | None = None, ) -> None: - self._list_response = list_response or IndexList(data=[]) - self._get_response = get_response or Index( - name="test_index", - config_yaml="config", - id="123", - description="Test index", - ) + self._list_response = list_response + self._get_response = get_response self._create_response = create_response + self._update_response = update_response self._get_exception = get_exception self._create_exception = create_exception self._update_exception = update_exception async def list(self, limit: int = 10, page_number: int = 1) -> IndexList: - return self._list_response + if self._list_response is not None: + return self._list_response + return IndexList(data=[], has_more=False, total=0) async def get(self, index_name: str) -> Index: if self._get_exception: raise self._get_exception - return self._get_response + if self._get_response is not None: + return self._get_response + raise NotImplementedError async def create(self, name: str, yaml_config: str, description: str | None = None) -> Index: if self._create_exception: raise self._create_exception if self._create_response is not None: return self._create_response - return Index( - name=name, - config_yaml=yaml_config, - id="123", - description=description, - ) + raise NotImplementedError async def update( self, index_name: str, updated_index_name: str | None = None, yaml_config: str | None = None - ) -> None: + ) -> Index: if self._update_exception: raise self._update_exception + if self._update_response is not None: + return self._update_response + + raise NotImplementedError + + +class FakeClient(BaseFakeClient): + def __init__(self, resource: FakeIndexResource) -> None: + self._resource = resource + super().__init__() + + def indexes(self, workspace: str) -> FakeIndexResource: + return self._resource + + +def create_test_index( + name: str = "test_index", + description: str | None = "Test index description", + config_yaml: str = "config: value", +) -> Index: + """Helper function to create a complete Index object for testing.""" + user = DeepsetUser(user_id="u1", given_name="Test", family_name="User") + status = IndexStatus( + pending_file_count=0, + failed_file_count=0, + indexed_no_documents_file_count=0, + indexed_file_count=10, + total_file_count=10, + ) - -@pytest.fixture(name="client") -def client_fixture() -> AsyncGenerator[AsyncClientProtocol, None]: - class FakeClient(BaseFakeClient): - def indexes(self, workspace: str) -> IndexResourceProtocol: - return FakeIndexResource() - - c = FakeClient() - return c - - -PYTEST_TUPLE = tuple[AsyncClientProtocol, MockFixture] + return Index( + pipeline_index_id="idx_123", + name=name, + description=description, + config_yaml=config_yaml, + workspace_id="ws_123", + settings={"key": "value"}, + desired_status="DEPLOYED", + deployed_at=datetime(2023, 1, 1, 12, 0), + last_edited_at=datetime(2023, 1, 2, 14, 30), + max_index_replica_count=3, + created_at=datetime(2023, 1, 1, 10, 0), + updated_at=datetime(2023, 1, 2, 14, 30), + created_by=user, + last_edited_by=user, + status=status, + ) @pytest.mark.asyncio -async def test_list_indexes_returns_formatted_string_when_no_indexes(client: AsyncClientProtocol) -> None: +async def test_list_indexes_returns_formatted_string_when_no_indexes() -> None: + resource = FakeIndexResource(list_response=IndexList(data=[], has_more=False, total=0)) + client = FakeClient(resource) + result = await list_indexes(client=client, workspace="test") assert result == "No indexes found." @pytest.mark.asyncio -async def test_list_indexes_returns_formatted_string_with_indexes( - client: AsyncClientProtocol -) -> None: - index = Index(name="test_index", config_yaml="config", id="123", description="Test index") - c = FakeClient() - c._indexes = FakeIndexResource(list_response=IndexList(data=[index])) - - result = await list_indexes(client=c, workspace="test") +async def test_list_indexes_returns_formatted_string_with_indexes() -> None: + index1 = create_test_index(name="index1", description="First index") + index2 = create_test_index(name="index2", description="Second index") - assert "test_index" in result - assert "config" in result - assert "123" in result - assert "Test index" in result + resource = FakeIndexResource(list_response=IndexList(data=[index1, index2], has_more=False, total=2)) + client = FakeClient(resource) + + result = await list_indexes(client=client, workspace="test") + + assert "index1" in result + assert "index2" in result + assert "First index" in result + assert "Second index" in result + assert "idx_123" in result @pytest.mark.asyncio -async def test_get_index_returns_formatted_string( - client: AsyncClientProtocol -) -> None: - result = await get_index(client=client, workspace="test", index_name="test_index") +async def test_get_index_returns_formatted_string() -> None: + index = create_test_index(name="my_index", description="My special index") + resource = FakeIndexResource(get_response=index) + client = FakeClient(resource) + + result = await get_index(client=client, workspace="test", index_name="my_index") - assert "test_index" in result - assert "config" in result - assert "123" in result - assert "Test index" in result + assert "my_index" in result + assert "config: value" in result + assert "idx_123" in result + assert "My special index" in result @pytest.mark.asyncio -async def test_get_index_returns_error_message_when_index_not_found( - client: AsyncClientProtocol, mocker: MockFixture -) -> None: - fake_resource: IndexResourceProtocol = client.indexes(workspace="test") - assert isinstance(fake_resource, FakeIndexResource) - fake_resource._get.side_effect = ResourceNotFoundError("Not found") +async def test_get_index_returns_error_message_when_index_not_found() -> None: + resource = FakeIndexResource(get_exception=ResourceNotFoundError()) + client = FakeClient(resource) - result = await get_index(client=client, workspace="test", index_name="test_index") + result = await get_index(client=client, workspace="test", index_name="nonexistent") - assert "There is no index named 'test_index'" in result + assert "There is no index named 'nonexistent'" in result @pytest.mark.asyncio -async def test_create_index_returns_success_message( - client: AsyncClientProtocol -) -> None: +async def test_create_index_returns_success_message() -> None: + created_index = create_test_index(name="new_index") + resource = FakeIndexResource(create_response=created_index) + client = FakeClient(resource) + result = await create_index( client=client, workspace="test", - index_name="test_index", - yaml_configuration="config", - description="Test index", + index_name="new_index", + yaml_configuration="config: new", + description="New index description", ) - assert "Index 'test_index' created successfully." == result + assert "Index 'new_index' created successfully." == result @pytest.mark.parametrize( @@ -143,16 +178,16 @@ async def test_create_index_returns_success_message( (UnexpectedAPIError, "Failed to create index 'test_index'"), ], ) +@pytest.mark.asyncio async def test_create_index_returns_error_message( - client: AsyncClientProtocol, error_class: type[Exception], expected_message: str, ) -> None: - c = FakeClient() - c._indexes = FakeIndexResource(create_exception=error_class("Error")) + resource = FakeIndexResource(create_exception=error_class("Error message")) + client = FakeClient(resource) result = await create_index( - client=c, + client=client, workspace="test", index_name="test_index", yaml_configuration="config", @@ -163,9 +198,10 @@ async def test_create_index_returns_error_message( @pytest.mark.asyncio -async def test_update_index_returns_success_message( - client: AsyncClientProtocol -) -> None: +async def test_update_index_returns_success_message() -> None: + resource = FakeIndexResource(update_response=create_test_index(name="new_test_index")) + client = FakeClient(resource) + result = await update_index( client=client, workspace="test", @@ -178,9 +214,10 @@ async def test_update_index_returns_success_message( @pytest.mark.asyncio -async def test_update_index_returns_error_message_when_no_changes_provided( - client: AsyncClientProtocol -) -> None: +async def test_update_index_returns_error_message_when_no_changes_provided() -> None: + resource = FakeIndexResource() + client = FakeClient(resource) + result = await update_index( client=client, workspace="test", @@ -198,20 +235,82 @@ async def test_update_index_returns_error_message_when_no_changes_provided( (UnexpectedAPIError, "Failed to update index 'test_index'"), ], ) +@pytest.mark.asyncio async def test_update_index_returns_error_message( - client: AsyncClientProtocol, error_class: type[Exception], expected_message: str, ) -> None: - c = FakeClient() - c._indexes = FakeIndexResource(update_exception=error_class("Error")) + resource = FakeIndexResource(update_exception=error_class("Error details")) + client = FakeClient(resource) result = await update_index( - client=c, + client=client, workspace="test", index_name="test_index", updated_index_name="new_test_index", yaml_configuration="new_config", ) - assert expected_message in result \ No newline at end of file + assert expected_message in result + + +@pytest.mark.asyncio +async def test_get_index_raises_unexpected_api_error() -> None: + resource = FakeIndexResource(get_exception=UnexpectedAPIError(status_code=500, message="Server error")) + client = FakeClient(resource) + + with pytest.raises(UnexpectedAPIError): + await get_index(client=client, workspace="test", index_name="test_index") + + +@pytest.mark.asyncio +async def test_create_index_with_detailed_error_messages() -> None: + # Test BadRequestError with detailed message + resource_bad = FakeIndexResource(create_exception=BadRequestError(message="Invalid YAML configuration")) + client_bad = FakeClient(resource_bad) + + result_bad = await create_index( + client=client_bad, + workspace="test", + index_name="bad_index", + yaml_configuration="invalid", + ) + + assert "Failed to create index 'bad_index'" in result_bad + assert "Invalid YAML configuration" in result_bad + assert "400" in result_bad + + # Test UnexpectedAPIError with status code + resource_unexpected = FakeIndexResource( + create_exception=UnexpectedAPIError(status_code=503, message="Service unavailable") + ) + client_unexpected = FakeClient(resource_unexpected) + + result_unexpected = await create_index( + client=client_unexpected, + workspace="test", + index_name="unavailable_index", + yaml_configuration="config", + ) + + assert "Failed to create index 'unavailable_index'" in result_unexpected + assert "Service unavailable" in result_unexpected + assert "503" in result_unexpected + + +@pytest.mark.asyncio +async def test_update_index_with_detailed_error_messages() -> None: + # Test with detailed BadRequestError + resource = FakeIndexResource(update_exception=BadRequestError(message="Name already exists")) + client = FakeClient(resource) + + result = await update_index( + client=client, + workspace="test", + index_name="existing_index", + updated_index_name="duplicate_name", + ) + + assert "Failed to update index 'existing_index'" in result + assert "Name already exists" in result + assert "400" in result