Skip to content

Commit 38a1ebf

Browse files
authored
feat: add cursor-based pagination to pipeline template resource (#177)
* feat: add cursor-based pagination to pipeline template resource * fix: do not expose after until fixed * fix: format
1 parent ca51be8 commit 38a1ebf

8 files changed

Lines changed: 361 additions & 152 deletions

File tree

src/deepset_mcp/api/indexes/models.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,3 @@ def __rich_repr__(self) -> Result:
5757
)
5858
yield "last_edited_at", self.last_edited_at.strftime("%m/%d/%Y %I:%M:%S %p") if self.last_edited_at else None
5959
yield "yaml_config", self.yaml_config if self.yaml_config is not None else "Get full index to see config."
60-
61-
62-
class IndexList(BaseModel):
63-
"""Response model for listing indexes."""
64-
65-
data: list[Index]
66-
has_more: bool
67-
total: int

src/deepset_mcp/api/pipeline_template/models.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -64,14 +64,6 @@ def populate_yaml_config(cls, values: Any) -> Any:
6464
return values
6565

6666

67-
class PipelineTemplateList(BaseModel):
68-
"""Response model for listing pipeline templates."""
69-
70-
data: list[PipelineTemplate]
71-
has_more: bool
72-
total: int
73-
74-
7567
class PipelineTemplateSearchResult(BaseModel):
7668
"""Model representing a search result for pipeline templates."""
7769

src/deepset_mcp/api/pipeline_template/protocols.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44

55
from typing import Protocol
66

7-
from deepset_mcp.api.pipeline_template.models import PipelineTemplate, PipelineTemplateList
7+
from deepset_mcp.api.pipeline_template.models import PipelineTemplate
8+
from deepset_mcp.api.shared_models import PaginatedResponse
89

910

1011
class PipelineTemplateResourceProtocol(Protocol):
@@ -14,8 +15,13 @@ async def get_template(self, template_name: str) -> PipelineTemplate:
1415
"""Fetch a single pipeline template by its name."""
1516
...
1617

17-
async def list_templates(
18-
self, limit: int = 100, field: str = "created_at", order: str = "DESC", filter: str | None = None
19-
) -> PipelineTemplateList:
20-
"""List pipeline templates in the configured workspace."""
18+
async def list(
19+
self,
20+
limit: int = 10,
21+
after: str | None = None,
22+
field: str = "created_at",
23+
order: str = "DESC",
24+
filter: str | None = None,
25+
) -> PaginatedResponse[PipelineTemplate]:
26+
"""Lists pipeline templates and returns the first page of results with pagination support."""
2127
...

src/deepset_mcp/api/pipeline_template/resource.py

Lines changed: 51 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55
from typing import Any
66

77
from deepset_mcp.api.exceptions import UnexpectedAPIError
8-
from deepset_mcp.api.pipeline_template.models import PipelineTemplate, PipelineTemplateList
8+
from deepset_mcp.api.pipeline_template.models import PipelineTemplate
99
from deepset_mcp.api.pipeline_template.protocols import PipelineTemplateResourceProtocol
1010
from deepset_mcp.api.protocols import AsyncClientProtocol
11+
from deepset_mcp.api.shared_models import PaginatedResponse
1112
from deepset_mcp.api.transport import raise_for_status
1213

1314

@@ -46,47 +47,58 @@ async def get_template(self, template_name: str) -> PipelineTemplate:
4647

4748
return PipelineTemplate.model_validate(data)
4849

49-
async def list_templates(
50-
self, limit: int = 100, field: str = "created_at", order: str = "DESC", filter: str | None = None
51-
) -> PipelineTemplateList:
52-
"""List pipeline templates in the configured workspace.
53-
54-
Parameters
55-
----------
56-
limit : int, optional (default=100)
57-
Maximum number of templates to return
58-
field : str, optional (default="created_at")
59-
Field to sort by
60-
order : str, optional (default="DESC")
61-
Sort order (ASC or DESC)
62-
filter : str | None, optional (default=None)
63-
OData filter expression for filtering templates
64-
65-
Returns
66-
-------
67-
PipelineTemplateList
68-
List of pipeline templates with metadata
50+
async def list(
51+
self,
52+
limit: int = 10,
53+
after: str | None = None,
54+
field: str = "created_at",
55+
order: str = "DESC",
56+
filter: str | None = None,
57+
) -> PaginatedResponse[PipelineTemplate]:
58+
"""Lists pipeline templates and returns the first page of results.
59+
60+
The returned object can be iterated over to fetch subsequent pages.
61+
62+
:param limit: The maximum number of pipeline templates to return per page.
63+
:param after: The cursor to fetch the next page of results.
64+
:param field: Field to sort by (default: "created_at").
65+
:param order: Sort order, either "ASC" or "DESC" (default: "DESC").
66+
:param filter: OData filter expression for filtering templates.
67+
:returns: A `PaginatedResponse` object containing the first page of pipeline templates.
6968
"""
70-
params = {"limit": limit, "page_number": 1, "field": field, "order": order}
71-
69+
# TODO: Remove when fixed
70+
if after is not None:
71+
raise ValueError("Pagination using 'after' parameter is currently not supported by the deepset platform.")
72+
73+
# 1. Prepare arguments for the initial API call
74+
# TODO: Pagination in the deepset API is currently implemented in an unintuitive way.
75+
# TODO: The cursor is always time based (created_at) and after signifies templates older than the current cursor
76+
# TODO: while 'before' signals templates younger than the current cursor.
77+
# TODO: This is applied irrespective of any sort (e.g. name) that would conflict with this approach.
78+
# TODO: Change this to 'after' once the behaviour is fixed on the deepset API
79+
request_params = {"limit": limit, "before": after, "field": field, "order": order}
7280
if filter is not None:
73-
params["filter"] = filter
81+
request_params["filter"] = filter
82+
request_params = {k: v for k, v in request_params.items() if v is not None}
7483

75-
response = await self._client.request(
76-
f"/v1/workspaces/{self._workspace}/pipeline_templates",
77-
method="GET",
78-
params=params,
79-
)
80-
81-
raise_for_status(response)
84+
# 2. Make the first API call using a private, stateless method
85+
page = await self._list_api_call(**request_params)
8286

83-
if response.json is None:
84-
raise UnexpectedAPIError(message="Unexpected API response, no templates returned.")
85-
86-
response_data: dict[str, Any] = response.json
87+
# 3. Inject the logic needed for subsequent fetches into the response object
88+
page._inject_paginator(
89+
fetch_func=self._list_api_call,
90+
# Base args for the *next* fetch don't include initial cursors
91+
base_args={"limit": limit, "field": field, "order": order, "filter": filter},
92+
)
93+
return page
8794

88-
return PipelineTemplateList(
89-
data=[PipelineTemplate.model_validate(template) for template in response_data["data"]],
90-
has_more=response_data.get("has_more", False),
91-
total=response_data.get("total", len(response_data["data"])),
95+
async def _list_api_call(self, **kwargs: Any) -> PaginatedResponse[PipelineTemplate]:
96+
"""A private, stateless method that performs the raw API call."""
97+
resp = await self._client.request(
98+
endpoint=f"v1/workspaces/{self._workspace}/pipeline_templates", method="GET", params=kwargs
9299
)
100+
raise_for_status(resp)
101+
if resp.json is None:
102+
raise UnexpectedAPIError(status_code=resp.status_code, message="Empty response", detail=None)
103+
104+
return PaginatedResponse[PipelineTemplate].create_with_cursor_field(resp.json, "name")

src/deepset_mcp/tools/pipeline_template.py

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@
77
from deepset_mcp.api.exceptions import ResourceNotFoundError, UnexpectedAPIError
88
from deepset_mcp.api.pipeline_template.models import (
99
PipelineTemplate,
10-
PipelineTemplateList,
1110
PipelineTemplateSearchResult,
1211
PipelineTemplateSearchResults,
1312
PipelineType,
1413
)
1514
from deepset_mcp.api.protocols import AsyncClientProtocol
15+
from deepset_mcp.api.shared_models import PaginatedResponse
1616
from deepset_mcp.tools.model_protocol import ModelProtocol
1717

1818

@@ -21,27 +21,22 @@ async def list_templates(
2121
client: AsyncClientProtocol,
2222
workspace: str,
2323
limit: int = 100,
24-
field: str = "created_at",
25-
order: str = "DESC",
2624
pipeline_type: PipelineType | str | None = None,
27-
) -> PipelineTemplateList | str:
25+
# after: str | None = None TODO
26+
) -> PaginatedResponse[PipelineTemplate] | str:
2827
"""Retrieves a list of all available pipeline and indexing templates.
2928
3029
:param client: The async client for API requests.
3130
:param workspace: The workspace to list templates from.
3231
:param limit: Maximum number of templates to return (default: 100).
33-
:param field: Field to sort by (default: "created_at").
34-
:param order: Sort order, either "ASC" or "DESC" (default: "DESC").
3532
:param pipeline_type: The type of pipeline to return.
3633
3734
:returns: List of pipeline templates or error message.
3835
"""
3936
try:
40-
return await client.pipeline_templates(workspace=workspace).list_templates(
37+
return await client.pipeline_templates(workspace=workspace).list(
4138
limit=limit,
42-
field=field,
43-
order=order,
44-
filter=f"pipeline_type eq '{pipeline_type}'" if pipeline_type else None,
39+
filter=f"pipeline_type eq '{pipeline_type}'" if pipeline_type else None, # TODO: after=after
4540
)
4641
except ResourceNotFoundError:
4742
return f"There is no workspace named '{workspace}'. Did you mean to configure it?"
@@ -87,18 +82,21 @@ async def search_templates(
8782
:returns: Search results with similarity scores or error message.
8883
"""
8984
try:
90-
response = await client.pipeline_templates(workspace=workspace).list_templates(
91-
filter=f"pipeline_type eq '{pipeline_type}'"
92-
)
85+
response = await client.pipeline_templates(workspace=workspace).list()
86+
templates = response.data
87+
88+
# Filter by pipeline_type if specified
89+
if pipeline_type:
90+
templates = [t for t in templates if t.pipeline_type == pipeline_type]
9391
except UnexpectedAPIError as e:
9492
return f"Failed to retrieve pipeline templates: {e}"
9593

96-
if not response.data:
94+
if not templates:
9795
return PipelineTemplateSearchResults(results=[], query=query, total_found=0)
9896

9997
# Extract text for embedding from all templates
10098
template_texts: list[tuple[str, str]] = [
101-
(template.template_name, f"{template.template_name} {template.description}") for template in response.data
99+
(template.template_name, f"{template.template_name} {template.description}") for template in templates
102100
]
103101
template_names: list[str] = [t[0] for t in template_texts]
104102

@@ -122,7 +120,7 @@ async def search_templates(
122120
search_results = []
123121
for template_name, sim in top_templates:
124122
# Find the template object by name
125-
template = next((t for t in response.data if t.template_name == template_name), None)
123+
template = next((t for t in templates if t.template_name == template_name), None)
126124
if template:
127125
search_results.append(PipelineTemplateSearchResult(template=template, similarity_score=float(sim)))
128126

0 commit comments

Comments
 (0)