Skip to content

Commit b03b260

Browse files
authored
fix: IO types might be lists not strings (#173)
1 parent f50746b commit b03b260

2 files changed

Lines changed: 220 additions & 1 deletion

File tree

src/deepset_mcp/tools/haystack_service.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,18 @@ def extract_component_texts(*, component_def: dict[str, Any]) -> tuple[str, str]
3939
return component_type, f"{name} {description}"
4040

4141

42+
def _format_type(type_: str | list[str]) -> str:
43+
"""Formats the component type as a single string.
44+
45+
:param type_: The component type
46+
:return: The component type formatted as a single string
47+
"""
48+
if isinstance(type_, str):
49+
return type_
50+
51+
return " | ".join(type_)
52+
53+
4254
async def _build_component_definition(
4355
*, component_def: dict[str, Any], component_type: str, haystack_service: Any, schema: dict[str, Any] | None = None
4456
) -> ComponentDefinition | str:
@@ -79,7 +91,7 @@ async def _build_component_definition(
7991
name=prop_name,
8092
annotation=prop_info.get("_annotation", prop_info.get("type", "Unknown")),
8193
description=prop_info.get("description", "No description available."),
82-
type=prop_info.get("type", "Unknown"),
94+
type=_format_type(prop_info.get("type", "Unknown")),
8395
required=prop_name in input_required,
8496
)
8597
for prop_name, prop_info in input_props.items()
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
# SPDX-FileCopyrightText: 2025-present deepset GmbH <info@deepset.ai>
2+
#
3+
# SPDX-License-Identifier: Apache-2.0
4+
5+
import pytest
6+
7+
from deepset_mcp.api.client import AsyncDeepsetClient
8+
from deepset_mcp.initialize_embedding_model import get_initialized_model
9+
from deepset_mcp.tools.haystack_service import (
10+
get_component_definition,
11+
get_custom_components,
12+
list_component_families,
13+
run_component,
14+
search_component_definition,
15+
)
16+
from deepset_mcp.tools.haystack_service_models import (
17+
ComponentDefinition,
18+
ComponentDefinitionList,
19+
ComponentFamilyList,
20+
ComponentSearchResults,
21+
)
22+
from deepset_mcp.tools.model_protocol import ModelProtocol
23+
24+
pytestmark = pytest.mark.integration
25+
26+
27+
@pytest.fixture
28+
def embedding_model() -> ModelProtocol:
29+
"""Create an embedding model for testing search functionality."""
30+
return get_initialized_model()
31+
32+
33+
@pytest.mark.asyncio
34+
async def test_get_component_definition(client: AsyncDeepsetClient) -> None:
35+
"""Test getting a specific component definition."""
36+
result = await get_component_definition(
37+
client=client, component_type="haystack.components.builders.prompt_builder.PromptBuilder"
38+
)
39+
40+
assert isinstance(result, ComponentDefinition)
41+
assert result.component_type == "haystack.components.builders.prompt_builder.PromptBuilder"
42+
assert result.title is not None
43+
assert result.description is not None
44+
assert result.family is not None
45+
assert len(result.init_parameters) > 0
46+
47+
# Check that input/output schemas are included
48+
assert result.input_schema is not None
49+
assert result.output_schema is not None
50+
51+
52+
@pytest.mark.asyncio
53+
async def test_get_component_definition_nonexistent(client: AsyncDeepsetClient) -> None:
54+
"""Test getting a non-existent component definition."""
55+
result = await get_component_definition(client=client, component_type="nonexistent.component.Type")
56+
57+
assert isinstance(result, str)
58+
assert "Component not found" in result
59+
60+
61+
@pytest.mark.asyncio
62+
async def test_search_component_definition(client: AsyncDeepsetClient, embedding_model: ModelProtocol) -> None:
63+
"""Test searching for components by query."""
64+
result = await search_component_definition(client=client, query="prompt builder", model=embedding_model, top_k=3)
65+
66+
assert isinstance(result, ComponentSearchResults)
67+
assert result.query == "prompt builder"
68+
assert result.total_found >= 0
69+
assert len(result.results) <= 3
70+
71+
if result.results:
72+
for search_result in result.results:
73+
assert isinstance(search_result.component, ComponentDefinition)
74+
assert isinstance(search_result.similarity_score, float)
75+
assert 0.0 <= search_result.similarity_score <= 1.0
76+
77+
78+
@pytest.mark.asyncio
79+
async def test_search_component_definition_generator_query(
80+
client: AsyncDeepsetClient, embedding_model: ModelProtocol
81+
) -> None:
82+
"""Test searching for generator components."""
83+
result = await search_component_definition(
84+
client=client, query="generate text openai", model=embedding_model, top_k=5
85+
)
86+
87+
assert isinstance(result, ComponentSearchResults)
88+
assert result.query == "generate text openai"
89+
assert result.total_found > 0
90+
91+
# Should find OpenAI generator or similar components
92+
found_generator = False
93+
for search_result in result.results:
94+
if "generator" in search_result.component.component_type.lower():
95+
found_generator = True
96+
break
97+
98+
assert found_generator, "Should find at least one generator component"
99+
100+
101+
@pytest.mark.asyncio
102+
async def test_list_component_families(client: AsyncDeepsetClient) -> None:
103+
"""Test listing all component families."""
104+
result = await list_component_families(client=client)
105+
106+
assert isinstance(result, ComponentFamilyList)
107+
assert result.total_count > 0
108+
assert len(result.families) > 0
109+
assert len(result.families) == result.total_count
110+
111+
# Check for expected families
112+
family_names = [family.name for family in result.families]
113+
expected_families = ["builders", "generators", "retrievers"]
114+
115+
for expected_family in expected_families:
116+
assert any(expected_family in family_name for family_name in family_names), (
117+
f"Expected to find family containing '{expected_family}'"
118+
)
119+
120+
for family in result.families:
121+
assert family.name is not None
122+
assert family.description is not None
123+
124+
125+
@pytest.mark.asyncio
126+
async def test_get_custom_components(client: AsyncDeepsetClient) -> None:
127+
"""Test getting custom components."""
128+
result = await get_custom_components(client=client)
129+
130+
# This could be either a ComponentDefinitionList or a string message if no custom components exist
131+
if isinstance(result, ComponentDefinitionList):
132+
assert result.total_count >= 0
133+
assert len(result.components) == result.total_count
134+
135+
for component in result.components:
136+
assert isinstance(component, ComponentDefinition)
137+
assert component.is_custom is True
138+
assert component.package_version is not None
139+
else:
140+
# Should be a string message about no custom components
141+
assert isinstance(result, str)
142+
assert "custom components" in result.lower()
143+
144+
145+
@pytest.mark.asyncio
146+
async def test_run_component_prompt_builder(client: AsyncDeepsetClient) -> None:
147+
"""Test running a PromptBuilder component."""
148+
result = await run_component(
149+
client=client,
150+
component_type="haystack.components.builders.prompt_builder.PromptBuilder",
151+
init_params={"template": "Hello, {{name}}! How are you?"},
152+
input_data={"name": "World"},
153+
input_types={"name": "str"},
154+
)
155+
156+
assert isinstance(result, dict)
157+
assert "prompt" in result
158+
assert result["prompt"] == "Hello, World! How are you?"
159+
160+
161+
@pytest.mark.asyncio
162+
async def test_run_component_prompt_builder_no_variables(client: AsyncDeepsetClient) -> None:
163+
"""Test running a PromptBuilder component with no template variables."""
164+
result = await run_component(
165+
client=client,
166+
component_type="haystack.components.builders.prompt_builder.PromptBuilder",
167+
init_params={"template": "Static message"},
168+
input_data={},
169+
input_types={},
170+
)
171+
172+
assert isinstance(result, dict)
173+
assert "prompt" in result
174+
assert result["prompt"] == "Static message"
175+
176+
177+
@pytest.mark.asyncio
178+
async def test_run_component_answer_builder(client: AsyncDeepsetClient) -> None:
179+
"""Test running an AnswerBuilder component."""
180+
result = await run_component(
181+
client=client,
182+
component_type="haystack.components.builders.answer_builder.AnswerBuilder",
183+
init_params={},
184+
input_data={"query": "What is AI?", "replies": ["AI is artificial intelligence."]},
185+
input_types={"query": "str", "replies": "list[str]"},
186+
)
187+
188+
assert isinstance(result, dict)
189+
assert "answers" in result
190+
assert isinstance(result["answers"], list)
191+
assert len(result["answers"]) > 0
192+
193+
194+
@pytest.mark.asyncio
195+
async def test_run_component_with_invalid_params(client: AsyncDeepsetClient) -> None:
196+
"""Test running a component with invalid parameters."""
197+
result = await run_component(
198+
client=client,
199+
component_type="haystack.components.builders.prompt_builder.PromptBuilder",
200+
init_params={"invalid_param": "value"},
201+
input_data={"name": "World"},
202+
input_types={"name": "str"},
203+
)
204+
205+
# Should return an error string
206+
assert isinstance(result, str)
207+
assert "Failed to run component" in result

0 commit comments

Comments
 (0)