Skip to content

Commit 0ed0e08

Browse files
committed
feat: adds component run method to api sdk
1 parent 4995084 commit 0ed0e08

4 files changed

Lines changed: 276 additions & 0 deletions

File tree

src/deepset_mcp/api/haystack_service/protocols.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,24 @@ async def get_component_schemas(self) -> dict[str, Any]:
1515
async def get_component_input_output(self, component_name: str) -> dict[str, Any]:
1616
"""Fetch input and output schema for a component from the API."""
1717
...
18+
19+
async def run_component(
20+
self,
21+
component_type: str,
22+
init_params: dict[str, Any] | None = None,
23+
input_data: dict[str, Any] | None = None,
24+
input_types: dict[str, str] | None = None,
25+
workspace: str | None = None,
26+
) -> dict[str, Any]:
27+
"""Run a Haystack component with the given parameters.
28+
29+
:param component_type: The type of component to run
30+
(e.g., "haystack.components.builders.prompt_builder.PromptBuilder")
31+
:param init_params: Initialization parameters for the component
32+
:param input_data: Input data for the component
33+
:param input_types: Optional type information for inputs (inferred if not provided)
34+
:param workspace: Optional workspace name to run the component in
35+
36+
:returns: Dictionary containing the component's output sockets
37+
"""
38+
...

src/deepset_mcp/api/haystack_service/resource.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,49 @@ async def get_component_input_output(self, component_name: str) -> dict[str, Any
5757
raise ResourceNotFoundError(f"Component '{component_name}' not found.")
5858

5959
return resp.json[0] if resp.json is not None else {}
60+
61+
async def run_component(
62+
self,
63+
component_type: str,
64+
init_params: dict[str, Any] | None = None,
65+
input_data: dict[str, Any] | None = None,
66+
input_types: dict[str, str] | None = None,
67+
workspace: str | None = None,
68+
) -> dict[str, Any]:
69+
"""Run a Haystack component with the given parameters.
70+
71+
:param component_type: The type of component to run (e.g., "haystack.components.builders.PromptBuilder")
72+
:param init_params: Initialization parameters for the component
73+
:param input_data: Input data for the component
74+
:param input_types: Optional type information for inputs (inferred if not provided)
75+
:param workspace: Optional workspace name to run the component in
76+
77+
:returns: Dictionary containing the component's output sockets
78+
"""
79+
payload: dict[str, Any] = {
80+
"component_type": component_type,
81+
"init_params": init_params or {},
82+
"input": input_data or {},
83+
}
84+
85+
if input_types is not None:
86+
payload["input_types"] = input_types
87+
88+
endpoint = "v1/haystack/components/run"
89+
if workspace is not None:
90+
endpoint = f"v1/workspaces/{workspace}/haystack/components/run"
91+
92+
resp = await self._client.request(
93+
endpoint=endpoint,
94+
method="POST",
95+
headers={
96+
"accept": "application/json",
97+
"content-type": "application/json",
98+
},
99+
data=payload,
100+
response_type=dict[str, Any],
101+
)
102+
103+
raise_for_status(resp)
104+
105+
return resp.json if resp.json is not None else {}

test/integration/test_integration_haystack_service_resource.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,38 @@ async def test_get_component_input_output(
4141
assert response["name"] == "Agent"
4242
assert "input" in response
4343
assert "output" in response
44+
45+
46+
@pytest.mark.asyncio
47+
async def test_run_component(
48+
haystack_service_resource: HaystackServiceResource,
49+
) -> None:
50+
"""Test for running a Haystack component."""
51+
response = await haystack_service_resource.run_component(
52+
component_type="haystack.components.builders.prompt_builder.PromptBuilder",
53+
init_params={"template": "Hello, {{name}}!"},
54+
input_data={"name": "deepset"},
55+
input_types={"name": "str"},
56+
)
57+
58+
assert isinstance(response, dict)
59+
assert "prompt" in response
60+
assert response["prompt"] == "Hello, deepset!"
61+
62+
63+
@pytest.mark.asyncio
64+
async def test_run_component_with_workspace(
65+
haystack_service_resource: HaystackServiceResource,
66+
) -> None:
67+
"""Test for running a Haystack component in a specific workspace."""
68+
response = await haystack_service_resource.run_component(
69+
component_type="haystack.components.builders.prompt_builder.PromptBuilder",
70+
init_params={"template": "Hello, {{name}}!"},
71+
input_data={"name": "deepset"},
72+
input_types={"name": "str"},
73+
workspace="default",
74+
)
75+
76+
assert isinstance(response, dict)
77+
assert "prompt" in response
78+
assert response["prompt"] == "Hello, deepset!"

test/unit/api/haystack_service/test_haystack_service_resource.py

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,40 @@ def mock_io_error_response(mock_client: BaseFakeClient) -> None:
6060
)
6161

6262

63+
def make_component_run_response() -> dict[str, Any]:
64+
return {"prompt": "Generated prompt text", "metadata": {"some_key": "some_value"}}
65+
66+
67+
@pytest.fixture
68+
def mock_successful_run_response(mock_client: BaseFakeClient) -> None:
69+
"""Configure the mock client to return a successful component run response."""
70+
mock_client.responses["v1/haystack/components/run"] = TransportResponse(
71+
status_code=200,
72+
json=make_component_run_response(),
73+
text=json.dumps(make_component_run_response()),
74+
)
75+
76+
77+
@pytest.fixture
78+
def mock_run_error_response(mock_client: BaseFakeClient) -> None:
79+
"""Configure the mock client to return an error response for component run."""
80+
mock_client.responses["v1/haystack/components/run"] = TransportResponse(
81+
status_code=500,
82+
json={"message": "Internal server error"},
83+
text="Internal server error",
84+
)
85+
86+
87+
@pytest.fixture
88+
def mock_successful_workspace_run_response(mock_client: BaseFakeClient) -> None:
89+
"""Configure the mock client to return a successful workspace component run response."""
90+
mock_client.responses["v1/workspaces/test-workspace/haystack/components/run"] = TransportResponse(
91+
status_code=200,
92+
json=make_component_run_response(),
93+
text=json.dumps(make_component_run_response()),
94+
)
95+
96+
6397
def test_initialization(mock_client: BaseFakeClient) -> None:
6498
"""Test HaystackServiceResource initialization."""
6599
resource = HaystackServiceResource(client=mock_client)
@@ -115,3 +149,143 @@ async def test_get_component_input_output_error(
115149
resource = HaystackServiceResource(client=mock_client)
116150
with pytest.raises(UnexpectedAPIError):
117151
await resource.get_component_input_output("Agent")
152+
153+
154+
@pytest.mark.asyncio
155+
async def test_run_component_success(
156+
mock_client: BaseFakeClient,
157+
mock_successful_run_response: None,
158+
) -> None:
159+
"""Test successful component run."""
160+
resource = HaystackServiceResource(client=mock_client)
161+
result = await resource.run_component(
162+
component_type="haystack.components.builders.PromptBuilder",
163+
init_params={"some_parameter": "some_value"},
164+
input_data={"question": "What does it do"},
165+
input_types={"question": "str"},
166+
)
167+
168+
assert result == make_component_run_response()
169+
request = mock_client.requests[-1]
170+
assert request["method"] == "POST"
171+
assert request["endpoint"] == "v1/haystack/components/run"
172+
assert request["headers"] == {
173+
"accept": "application/json",
174+
"content-type": "application/json",
175+
}
176+
assert request["data"] == {
177+
"component_type": "haystack.components.builders.PromptBuilder",
178+
"init_params": {"some_parameter": "some_value"},
179+
"input": {"question": "What does it do"},
180+
"input_types": {"question": "str"},
181+
}
182+
183+
184+
@pytest.mark.asyncio
185+
async def test_run_component_minimal_params(
186+
mock_client: BaseFakeClient,
187+
mock_successful_run_response: None,
188+
) -> None:
189+
"""Test component run with minimal parameters."""
190+
resource = HaystackServiceResource(client=mock_client)
191+
result = await resource.run_component(component_type="haystack.components.builders.PromptBuilder")
192+
193+
assert result == make_component_run_response()
194+
request = mock_client.requests[-1]
195+
assert request["method"] == "POST"
196+
assert request["endpoint"] == "v1/haystack/components/run"
197+
assert request["headers"] == {
198+
"accept": "application/json",
199+
"content-type": "application/json",
200+
}
201+
assert request["data"] == {
202+
"component_type": "haystack.components.builders.PromptBuilder",
203+
"init_params": {},
204+
"input": {},
205+
}
206+
207+
208+
@pytest.mark.asyncio
209+
async def test_run_component_without_input_types(
210+
mock_client: BaseFakeClient,
211+
mock_successful_run_response: None,
212+
) -> None:
213+
"""Test component run without input_types (should be inferred)."""
214+
resource = HaystackServiceResource(client=mock_client)
215+
result = await resource.run_component(
216+
component_type="haystack.components.builders.PromptBuilder",
217+
init_params={"template": "Hello {{name}}"},
218+
input_data={"name": "World"},
219+
)
220+
221+
assert result == make_component_run_response()
222+
request = mock_client.requests[-1]
223+
assert "input_types" not in request["data"]
224+
225+
226+
@pytest.mark.asyncio
227+
async def test_run_component_error(
228+
mock_client: BaseFakeClient,
229+
mock_run_error_response: None,
230+
) -> None:
231+
"""Test error handling in component run."""
232+
resource = HaystackServiceResource(client=mock_client)
233+
234+
with pytest.raises(UnexpectedAPIError):
235+
await resource.run_component(component_type="haystack.components.builders.PromptBuilder")
236+
237+
238+
@pytest.mark.asyncio
239+
async def test_run_component_with_workspace(
240+
mock_client: BaseFakeClient,
241+
mock_successful_workspace_run_response: None,
242+
) -> None:
243+
"""Test component run with workspace parameter."""
244+
resource = HaystackServiceResource(client=mock_client)
245+
result = await resource.run_component(
246+
component_type="haystack.components.builders.PromptBuilder",
247+
init_params={"template": "Hello {{name}}"},
248+
input_data={"name": "World"},
249+
workspace="test-workspace",
250+
)
251+
252+
assert result == make_component_run_response()
253+
request = mock_client.requests[-1]
254+
assert request["method"] == "POST"
255+
assert request["endpoint"] == "v1/workspaces/test-workspace/haystack/components/run"
256+
assert request["headers"] == {
257+
"accept": "application/json",
258+
"content-type": "application/json",
259+
}
260+
assert request["data"] == {
261+
"component_type": "haystack.components.builders.PromptBuilder",
262+
"init_params": {"template": "Hello {{name}}"},
263+
"input": {"name": "World"},
264+
}
265+
266+
267+
@pytest.mark.asyncio
268+
async def test_run_component_with_workspace_and_input_types(
269+
mock_client: BaseFakeClient,
270+
mock_successful_workspace_run_response: None,
271+
) -> None:
272+
"""Test component run with workspace and input_types parameters."""
273+
resource = HaystackServiceResource(client=mock_client)
274+
result = await resource.run_component(
275+
component_type="haystack.components.builders.PromptBuilder",
276+
init_params={"template": "Hello {{name}}"},
277+
input_data={"name": "World"},
278+
input_types={"name": "str"},
279+
workspace="test-workspace",
280+
)
281+
282+
assert result == make_component_run_response()
283+
request = mock_client.requests[-1]
284+
assert request["method"] == "POST"
285+
assert request["endpoint"] == "v1/workspaces/test-workspace/haystack/components/run"
286+
assert request["data"] == {
287+
"component_type": "haystack.components.builders.PromptBuilder",
288+
"init_params": {"template": "Hello {{name}}"},
289+
"input": {"name": "World"},
290+
"input_types": {"name": "str"},
291+
}

0 commit comments

Comments
 (0)