Skip to content

Commit 997c427

Browse files
authored
Feat/align index update w pipeline update (#197)
* feat: align update index with pipeline * feat: add validate index
1 parent 8d8471c commit 997c427

8 files changed

Lines changed: 533 additions & 77 deletions

File tree

mkdocs.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ plugins:
2626
show_source: false
2727
show_signature_annotations: true
2828
show_root_heading: false
29-
show_root_full_path: false # Don't show full module paths
30-
show_root_members_full_path: false # Don't show full paths for members
29+
show_root_full_path: true # Don't show full module paths
30+
show_root_members_full_path: true # Don't show full paths for members
3131
show_root_toc_entry: false
3232
separate_signature: true # ← Separates signature from docstring
3333
signature_crossrefs: true

src/deepset_mcp/api/indexes/protocols.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,14 @@ async def deploy(self, index_name: str) -> PipelineValidationResult:
5050
"""
5151
...
5252

53+
async def validate(self, yaml_config: str) -> PipelineValidationResult:
54+
"""Validate an index's YAML configuration against the API.
55+
56+
:param yaml_config: The YAML configuration string to validate.
57+
:returns: PipelineValidationResult containing validation status and any errors.
58+
"""
59+
...
60+
5361
async def delete(self, index_name: str) -> None:
5462
"""Delete an index.
5563

src/deepset_mcp/api/indexes/resource.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,3 +173,34 @@ async def deploy(self, index_name: str) -> PipelineValidationResult:
173173
return PipelineValidationResult(valid=False, errors=errors)
174174

175175
raise UnexpectedAPIError(status_code=resp.status_code, message=resp.text, detail=resp.json)
176+
177+
async def validate(self, yaml_config: str) -> PipelineValidationResult:
178+
"""Validate an index's YAML configuration against the API.
179+
180+
:param yaml_config: The YAML configuration string to validate.
181+
:returns: PipelineValidationResult containing validation status and any errors.
182+
:raises ValueError: If the YAML is not valid (422 error) or contains syntax errors.
183+
"""
184+
data = {"indexing_yaml": yaml_config}
185+
186+
resp = await self._client.request(
187+
endpoint=f"v1/workspaces/{quote(self._workspace, safe='')}/pipeline_validations",
188+
method="POST",
189+
data=data,
190+
)
191+
192+
# If successful (status 200), the YAML is valid
193+
if resp.success:
194+
return PipelineValidationResult(valid=True)
195+
196+
if resp.status_code == 400 and resp.json is not None and isinstance(resp.json, dict) and "details" in resp.json:
197+
errors = [ValidationError(code=error["code"], message=error["message"]) for error in resp.json["details"]]
198+
199+
return PipelineValidationResult(valid=False, errors=errors)
200+
201+
if resp.status_code == 422:
202+
errors = [ValidationError(code="YAML_ERROR", message="Syntax error in YAML")]
203+
204+
return PipelineValidationResult(valid=False, errors=errors)
205+
206+
raise UnexpectedAPIError(status_code=resp.status_code, message=resp.text, detail=resp.json)

src/deepset_mcp/mcp/tool_registry.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
get_index as get_index_tool,
2828
list_indexes as list_indexes_tool,
2929
update_index as update_index_tool,
30+
validate_index as validate_index_tool,
3031
)
3132
from deepset_mcp.tools.object_store import create_get_from_object_store, create_get_slice_from_object_store
3233
from deepset_mcp.tools.pipeline import (
@@ -147,6 +148,10 @@ async def search_docs(query: str) -> str:
147148
deploy_index_tool,
148149
ToolConfig(needs_client=True, needs_workspace=True, memory_type=MemoryType.EXPLORABLE),
149150
),
151+
"validate_index": (
152+
validate_index_tool,
153+
ToolConfig(needs_client=True, needs_workspace=True, memory_type=MemoryType.EXPLORABLE_AND_REFERENCEABLE),
154+
),
150155
"list_templates": (
151156
list_pipeline_templates_tool,
152157
ToolConfig(

src/deepset_mcp/tools/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
run_component,
1212
search_component_definition,
1313
)
14-
from .indexes import create_index, deploy_index, get_index, list_indexes, update_index
14+
from .indexes import create_index, deploy_index, get_index, list_indexes, update_index, validate_index
1515
from .object_store import create_get_from_object_store, create_get_slice_from_object_store
1616
from .pipeline import (
1717
create_pipeline,
@@ -41,6 +41,7 @@
4141
"update_index",
4242
"create_index",
4343
"get_index",
44+
"validate_index",
4445
"create_get_from_object_store",
4546
"create_get_slice_from_object_store",
4647
"list_pipelines",

src/deepset_mcp/tools/indexes.py

Lines changed: 113 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,36 @@
22
#
33
# SPDX-License-Identifier: Apache-2.0
44

5+
import yaml
6+
from pydantic import BaseModel
7+
58
from deepset_mcp.api.exceptions import BadRequestError, ResourceNotFoundError, UnexpectedAPIError
69
from deepset_mcp.api.indexes.models import Index
710
from deepset_mcp.api.pipeline.models import PipelineValidationResult
811
from deepset_mcp.api.protocols import AsyncClientProtocol
912
from deepset_mcp.api.shared_models import PaginatedResponse
1013

1114

15+
class IndexValidationResultWithYaml(BaseModel):
16+
"""Model for index validation result that includes the original YAML."""
17+
18+
validation_result: PipelineValidationResult
19+
"Result of validating the index configuration"
20+
yaml_config: str
21+
"Original YAML configuration that was validated"
22+
23+
24+
class IndexOperationWithErrors(BaseModel):
25+
"""Model for index operations that complete with validation errors."""
26+
27+
message: str
28+
"Descriptive message about the index operation"
29+
validation_result: PipelineValidationResult
30+
"Validation errors encountered during the operation"
31+
index: Index
32+
"Index object after the operation completed"
33+
34+
1235
async def list_indexes(
1336
*, client: AsyncClientProtocol, workspace: str, after: str | None = None
1437
) -> PaginatedResponse[Index] | str:
@@ -43,6 +66,33 @@ async def get_index(*, client: AsyncClientProtocol, workspace: str, index_name:
4366
return response
4467

4568

69+
async def validate_index(
70+
*, client: AsyncClientProtocol, workspace: str, yaml_configuration: str
71+
) -> IndexValidationResultWithYaml | str:
72+
"""Validates the provided index YAML configuration against the deepset API.
73+
74+
:param client: The async client for API communication.
75+
:param workspace: The workspace name.
76+
:param yaml_configuration: The YAML configuration to validate.
77+
:returns: Validation result with original YAML or error message.
78+
"""
79+
if not yaml_configuration or not yaml_configuration.strip():
80+
return "You need to provide a YAML configuration to validate."
81+
82+
try:
83+
yaml.safe_load(yaml_configuration)
84+
except yaml.YAMLError as e:
85+
return f"Invalid YAML provided: {e}"
86+
87+
try:
88+
response = await client.indexes(workspace=workspace).validate(yaml_configuration)
89+
return IndexValidationResultWithYaml(validation_result=response, yaml_config=yaml_configuration)
90+
except ResourceNotFoundError:
91+
return f"There is no workspace named '{workspace}'. Did you mean to configure it?"
92+
except (BadRequestError, UnexpectedAPIError) as e:
93+
return f"Failed to validate index: {e}"
94+
95+
4696
async def create_index(
4797
*,
4898
client: AsyncClientProtocol,
@@ -78,35 +128,78 @@ async def update_index(
78128
client: AsyncClientProtocol,
79129
workspace: str,
80130
index_name: str,
81-
updated_index_name: str | None = None,
82-
yaml_configuration: str | None = None,
83-
) -> dict[str, str | Index] | str:
84-
"""Updates an existing index in your deepset platform workspace.
131+
original_config_snippet: str,
132+
replacement_config_snippet: str,
133+
skip_validation_errors: bool = True,
134+
) -> Index | IndexOperationWithErrors | str:
135+
"""
136+
Updates an index configuration in the specified workspace with a replacement configuration snippet.
85137
86-
This function can update either the name or the configuration of an existing index, or both.
87-
At least one of updated_index_name or yaml_configuration must be provided.
138+
This function validates the replacement configuration snippet before applying it to the index.
139+
If the validation fails and skip_validation_errors is False, it returns error messages.
140+
Otherwise, the replacement snippet is used to update the index's configuration.
88141
89-
:param client: Deepset API client to use.
90-
:param workspace: Workspace in which to update the index.
91-
:param index_name: Unique name of the index to update.
92-
:param updated_index_name: Updated name of the index.
93-
:param yaml_configuration: YAML configuration to update the index with.
142+
:param client: The async client for API communication.
143+
:param workspace: The workspace name.
144+
:param index_name: Name of the index to update.
145+
:param original_config_snippet: The configuration snippet to replace.
146+
:param replacement_config_snippet: The new configuration snippet.
147+
:param skip_validation_errors: If True (default), updates the index even if validation fails.
148+
If False, stops update when validation fails.
149+
:returns: Updated index or error message.
94150
"""
95-
if not updated_index_name and not yaml_configuration:
96-
return "You must provide either a new name or a new configuration to update the index."
97-
98151
try:
99-
result = await client.indexes(workspace=workspace).update(
100-
index_name=index_name, updated_index_name=updated_index_name, yaml_config=yaml_configuration
152+
original_index = await client.indexes(workspace=workspace).get(index_name=index_name)
153+
except ResourceNotFoundError:
154+
return f"There is no index named '{index_name}'. Did you mean to create it?"
155+
except (BadRequestError, UnexpectedAPIError) as e:
156+
return f"Failed to fetch index '{index_name}': {e}"
157+
158+
if original_index.yaml_config is None:
159+
return f"The index '{index_name}' does not have a YAML configuration."
160+
161+
occurrences = original_index.yaml_config.count(original_config_snippet)
162+
163+
if occurrences == 0:
164+
return f"No occurrences of the provided configuration snippet were found in the index '{index_name}'."
165+
166+
if occurrences > 1:
167+
return (
168+
f"Multiple occurrences ({occurrences}) of the provided configuration snippet were found in the index "
169+
f"'{index_name}'. Specify a more precise snippet to proceed with the update."
101170
)
171+
172+
updated_yaml_configuration = original_index.yaml_config.replace(
173+
original_config_snippet, replacement_config_snippet, 1
174+
)
175+
176+
try:
177+
validation_response = await client.indexes(workspace=workspace).validate(updated_yaml_configuration)
178+
179+
if not validation_response.valid and not skip_validation_errors:
180+
error_messages = [f"{error.code}: {error.message}" for error in validation_response.errors]
181+
return "Index validation failed:\n" + "\n".join(error_messages)
182+
183+
await client.indexes(workspace=workspace).update(index_name=index_name, yaml_config=updated_yaml_configuration)
184+
185+
# Get the full index after update
186+
index = await client.indexes(workspace=workspace).get(index_name)
187+
188+
# If validation failed but we proceeded anyway, return the special model
189+
if not validation_response.valid:
190+
return IndexOperationWithErrors(
191+
message="The operation completed with errors", validation_result=validation_response, index=index
192+
)
193+
194+
# Otherwise return just the index
195+
return index
196+
102197
except ResourceNotFoundError:
103198
return f"There is no index named '{index_name}'. Did you mean to create it?"
104199
except BadRequestError as e:
105-
return f"Failed to update index '{index_name}': {e}"
200+
return f"Failed to update the index '{index_name}': {e}"
106201
except UnexpectedAPIError as e:
107-
return f"Failed to update index '{index_name}': {e}"
108-
109-
return {"message": f"Index '{index_name}' updated successfully.", "index": result}
202+
return f"Failed to update the index '{index_name}': {e}"
110203

111204

112205
async def deploy_index(

test/integration/test_integration_index_resource.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,3 +405,62 @@ async def test_deploy_nonexistent_index(
405405
assert isinstance(result, PipelineValidationResult)
406406
assert result.valid is False
407407
assert len(result.errors) > 0
408+
409+
410+
@pytest.mark.asyncio
411+
async def test_validation_valid_yaml(index_resource: IndexResource, valid_index_config: str) -> None:
412+
"""Test validating a valid index YAML configuration."""
413+
config = json.loads(valid_index_config)
414+
result = await index_resource.validate(yaml_config=config["yaml_config"])
415+
416+
assert result.valid is True
417+
assert len(result.errors) == 0
418+
419+
420+
@pytest.mark.asyncio
421+
async def test_validation_invalid_yaml(
422+
index_resource: IndexResource,
423+
) -> None:
424+
"""Test validating an invalid index YAML configuration."""
425+
# Create an invalid YAML with missing required fields
426+
invalid_yaml = """
427+
components:
428+
document_embedder:
429+
# Missing 'type' field
430+
init_parameters:
431+
model: intfloat/e5-base-v2
432+
433+
inputs:
434+
files:
435+
- document_embedder.documents
436+
437+
max_runs_per_component: 100
438+
"""
439+
440+
result = await index_resource.validate(yaml_config=invalid_yaml)
441+
442+
# Check that validation failed with errors
443+
assert result.valid is False
444+
assert len(result.errors) > 0
445+
446+
# Should have a schema error (currently returns PIPELINE_SCHEMA_ERROR for indexes too)
447+
assert result.errors[0].code == "PIPELINE_SCHEMA_ERROR"
448+
449+
450+
@pytest.mark.asyncio
451+
async def test_validation_syntax_error(
452+
index_resource: IndexResource,
453+
) -> None:
454+
"""Test validating a YAML with syntax errors."""
455+
invalid_yaml_syntax = """
456+
components:
457+
document_embedder:
458+
type: haystack.components.embedders.sentence_transformers_document_embedder.SentenceTransformersDocumentEmbedder
459+
init_parameters
460+
model: intfloat/e5-base-v2
461+
"""
462+
463+
resp = await index_resource.validate(yaml_config=invalid_yaml_syntax)
464+
465+
assert resp.valid is False
466+
assert resp.errors[0].code == "YAML_ERROR"

0 commit comments

Comments
 (0)