mcp-haystack 0.0.2__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mcp_haystack-0.2.0/CHANGELOG.md +26 -0
- {mcp_haystack-0.0.2 → mcp_haystack-0.2.0}/PKG-INFO +4 -2
- mcp_haystack-0.2.0/examples/google_maps_agent.py +93 -0
- mcp_haystack-0.2.0/examples/mcp_filtered_tools.py +54 -0
- mcp_haystack-0.2.0/examples/mcp_sse_toolset.py +67 -0
- mcp_haystack-0.2.0/examples/mcp_stdio_toolset.py +60 -0
- mcp_haystack-0.2.0/examples/time_pipeline_toolset.py +61 -0
- {mcp_haystack-0.0.2 → mcp_haystack-0.2.0}/pydoc/config.yml +2 -1
- {mcp_haystack-0.0.2 → mcp_haystack-0.2.0}/pyproject.toml +15 -3
- {mcp_haystack-0.0.2 → mcp_haystack-0.2.0}/src/haystack_integrations/tools/mcp/__init__.py +6 -0
- {mcp_haystack-0.0.2 → mcp_haystack-0.2.0}/src/haystack_integrations/tools/mcp/mcp_tool.py +47 -16
- mcp_haystack-0.2.0/src/haystack_integrations/tools/mcp/mcp_toolset.py +257 -0
- {mcp_haystack-0.0.2 → mcp_haystack-0.2.0}/tests/test_mcp_tool.py +23 -3
- mcp_haystack-0.2.0/tests/test_mcp_toolset.py +296 -0
- mcp_haystack-0.0.2/CHANGELOG.md +0 -9
- mcp_haystack-0.0.2/examples/hayhooks/pipeline_wrapper.py +0 -82
- {mcp_haystack-0.0.2 → mcp_haystack-0.2.0}/.gitignore +0 -0
- {mcp_haystack-0.0.2 → mcp_haystack-0.2.0}/LICENSE.txt +0 -0
- {mcp_haystack-0.0.2 → mcp_haystack-0.2.0}/README.md +0 -0
- {mcp_haystack-0.0.2 → mcp_haystack-0.2.0}/examples/mcp_sse_client.py +0 -0
- {mcp_haystack-0.0.2 → mcp_haystack-0.2.0}/examples/mcp_sse_server.py +0 -0
- {mcp_haystack-0.0.2 → mcp_haystack-0.2.0}/examples/mcp_stdio_client.py +0 -0
- {mcp_haystack-0.0.2 → mcp_haystack-0.2.0}/examples/time_pipeline.py +0 -0
- {mcp_haystack-0.0.2 → mcp_haystack-0.2.0}/tests/__init__.py +0 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [integrations/mcp-v0.1.0] - 2025-04-22
|
|
4
|
+
|
|
5
|
+
### 🚀 Features
|
|
6
|
+
|
|
7
|
+
- Add MCPToolset (#1626)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
## [integrations/mcp-v0.0.2] - 2025-04-11
|
|
11
|
+
|
|
12
|
+
### 📚 Documentation
|
|
13
|
+
|
|
14
|
+
- Fix MCPTool import in code example (#1578)
|
|
15
|
+
|
|
16
|
+
### 🌀 Miscellaneous
|
|
17
|
+
|
|
18
|
+
- Feat: Add AsyncExecutor in MCPTool, ensure MCPTool works in hayhooks (#1643)
|
|
19
|
+
|
|
20
|
+
## [integrations/mcp-v0.0.1] - 2025-03-12
|
|
21
|
+
|
|
22
|
+
### 🚀 Features
|
|
23
|
+
|
|
24
|
+
- Add MCP tooling integration (#1487)
|
|
25
|
+
|
|
26
|
+
<!-- generated by git-cliff -->
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mcp-haystack
|
|
3
|
-
Version: 0.0
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Haystack integration for Model Context Protocol (MCP)
|
|
5
5
|
Project-URL: Documentation, https://github.com/deepset-ai/haystack-core-integrations/tree/main/integrations/mcp#readme
|
|
6
6
|
Project-URL: Issues, https://github.com/deepset-ai/haystack-core-integrations/issues
|
|
@@ -18,7 +18,9 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.13
|
|
19
19
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
20
20
|
Requires-Python: >=3.10
|
|
21
|
-
Requires-Dist:
|
|
21
|
+
Requires-Dist: exceptiongroup
|
|
22
|
+
Requires-Dist: haystack-ai>=2.13.0
|
|
23
|
+
Requires-Dist: httpx
|
|
22
24
|
Requires-Dist: mcp
|
|
23
25
|
Description-Content-Type: text/markdown
|
|
24
26
|
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
### Overview
|
|
6
|
+
# This script shows how to use the **Google Maps MCP Server** in combination with the **Haystack Agent** and **OpenAI's Chat Generator** to search for places via the Google Places API.
|
|
7
|
+
|
|
8
|
+
# ---
|
|
9
|
+
|
|
10
|
+
# ### 🔧 Step 1: Start the Google Maps MCP Server
|
|
11
|
+
|
|
12
|
+
# Make sure you have a valid **Google Maps API key**.
|
|
13
|
+
# See [Google Maps API Key](https://developers.google.com/maps/documentation/places/web-service/get-api-key) for more information.
|
|
14
|
+
|
|
15
|
+
# Run the following Docker command to start the MCP Server locally on port 8000:
|
|
16
|
+
|
|
17
|
+
# ```bash
|
|
18
|
+
# docker run -it --rm -p 8000:8000 \
|
|
19
|
+
# -e GOOGLE_MAPS_API_KEY=$GOOGLE_MAPS_API_KEY \
|
|
20
|
+
# supercorp/supergateway \
|
|
21
|
+
# --stdio "npx -y @modelcontextprotocol/server-google-maps" \
|
|
22
|
+
# --port 8000
|
|
23
|
+
# ```
|
|
24
|
+
|
|
25
|
+
# ---
|
|
26
|
+
|
|
27
|
+
# ### 🕵️ Step 2: Inspect Available Tools (Optional but useful)
|
|
28
|
+
|
|
29
|
+
# You can verify that the MCP server is running and see the available tools using:
|
|
30
|
+
|
|
31
|
+
# ```bash
|
|
32
|
+
# npx -y @modelcontextprotocol/inspector
|
|
33
|
+
# ```
|
|
34
|
+
|
|
35
|
+
# Connect MCP Inspector to the server at `http://localhost:8000/sse` and click on "List Tools" to display tools such as:
|
|
36
|
+
|
|
37
|
+
# - `maps_geocode`: Convert address → coordinates
|
|
38
|
+
# - `maps_reverse_geocode`: Convert coordinates → address
|
|
39
|
+
# - `maps_search_places`: Search places (e.g., restaurants)
|
|
40
|
+
# - `maps_place_details`: Get details of a specific place
|
|
41
|
+
# - `maps_distance_matrix`: Calculate travel distance/time
|
|
42
|
+
# - `maps_elevation`: Get elevation info
|
|
43
|
+
# - `maps_directions`: Get route directions
|
|
44
|
+
|
|
45
|
+
# ---
|
|
46
|
+
|
|
47
|
+
# ### ▶️ Step 3: Run the Python Script
|
|
48
|
+
|
|
49
|
+
# This script sets up:
|
|
50
|
+
# - A **Langfuse trace**
|
|
51
|
+
# - A **MCPToolset** with selected tools (`maps_geocode`, `maps_search_places`)
|
|
52
|
+
# - An **Agent** using `gpt-4.1` to process a natural language request
|
|
53
|
+
|
|
54
|
+
# To run the script:
|
|
55
|
+
|
|
56
|
+
# ```bash
|
|
57
|
+
# python examples/google_maps_agent.py
|
|
58
|
+
# ```
|
|
59
|
+
|
|
60
|
+
# The agent will respond to this example query:
|
|
61
|
+
|
|
62
|
+
# > "Find the five best Persian restaurants close to Zinnowitzer Str. 1, 10115 Berlin, Germany"
|
|
63
|
+
|
|
64
|
+
from haystack.components.agents import Agent
|
|
65
|
+
from haystack.components.generators.chat import OpenAIChatGenerator
|
|
66
|
+
from haystack.dataclasses import ChatMessage
|
|
67
|
+
|
|
68
|
+
# from haystack_integrations.components.connectors.langfuse.langfuse_connector import LangfuseConnector
|
|
69
|
+
from haystack_integrations.tools.mcp.mcp_tool import SSEServerInfo
|
|
70
|
+
from haystack_integrations.tools.mcp.mcp_toolset import MCPToolset
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def main():
|
|
74
|
+
# tracer = LangfuseConnector("Agent google maps search")
|
|
75
|
+
# Optionally, you can use Langfuse to trace the agent's activity but it needs
|
|
76
|
+
# additional configuration.
|
|
77
|
+
# See [Langfuse integration](https://github.com/deepset-ai/haystack-core-integrations/tree/main/integrations/langfuse) for more information.
|
|
78
|
+
toolset = MCPToolset(
|
|
79
|
+
SSEServerInfo(base_url="http://localhost:8000"), tool_names=["maps_geocode", "maps_search_places"]
|
|
80
|
+
)
|
|
81
|
+
agent = Agent(chat_generator=OpenAIChatGenerator(model="gpt-4.1"), tools=toolset)
|
|
82
|
+
result = agent.run(
|
|
83
|
+
messages=[
|
|
84
|
+
ChatMessage.from_user(
|
|
85
|
+
text="Find the five best persian restaurants close to Zinnowitzer Str. 1, 10115 Berlin, Germany"
|
|
86
|
+
)
|
|
87
|
+
]
|
|
88
|
+
)
|
|
89
|
+
print(result["messages"][-1].text)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
if __name__ == "__main__":
|
|
93
|
+
main()
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
from haystack_integrations.tools.mcp import MCPToolset, SSEServerInfo
|
|
6
|
+
|
|
7
|
+
# This example demonstrates using MCPToolset with SSE transport
|
|
8
|
+
# and filtering tools by name
|
|
9
|
+
# Run this client after running the server mcp_sse_server.py
|
|
10
|
+
# It shows how MCPToolset can selectively include only specific tools
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main():
|
|
14
|
+
"""Example of using MCPToolset with filtered tools."""
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
print("Creating toolset with all available tools:")
|
|
18
|
+
# Create a toolset with all available tools
|
|
19
|
+
full_toolset = MCPToolset(
|
|
20
|
+
server_info=SSEServerInfo(base_url="http://localhost:8000"),
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# Print all discovered tools
|
|
24
|
+
print(f"Discovered {len(full_toolset)} tools:")
|
|
25
|
+
for tool in full_toolset:
|
|
26
|
+
print(f" - {tool.name}: {tool.description}")
|
|
27
|
+
|
|
28
|
+
print("\nCreating toolset with filtered tools:")
|
|
29
|
+
# Create a toolset with only specific tools
|
|
30
|
+
# In this example, we're only including the 'add' tool
|
|
31
|
+
filtered_toolset = MCPToolset(
|
|
32
|
+
server_info=SSEServerInfo(base_url="http://localhost:8000"),
|
|
33
|
+
tool_names=["add"], # Only include the 'add' tool
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# Print filtered tools
|
|
37
|
+
print(f"Filtered toolset has {len(filtered_toolset)} tools:")
|
|
38
|
+
for tool in filtered_toolset:
|
|
39
|
+
print(f" - {tool.name}: {tool.description}")
|
|
40
|
+
|
|
41
|
+
# Use the filtered toolset
|
|
42
|
+
if len(filtered_toolset) > 0:
|
|
43
|
+
add_tool = filtered_toolset.tools[0] # The only tool should be 'add'
|
|
44
|
+
result = add_tool.invoke(a=10, b=5)
|
|
45
|
+
print(f"\nInvoking {add_tool.name}: 10 + 5 = {result.content[0].text}")
|
|
46
|
+
else:
|
|
47
|
+
print("No tools available in the filtered toolset")
|
|
48
|
+
|
|
49
|
+
except Exception as e:
|
|
50
|
+
print(f"Error in filtered toolset example: {e}")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
if __name__ == "__main__":
|
|
54
|
+
main()
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
from haystack_integrations.tools.mcp import MCPToolset, SSEServerInfo
|
|
6
|
+
|
|
7
|
+
# This example demonstrates using MCPToolset with SSE transport
|
|
8
|
+
# Run this client after running the server mcp_sse_server.py
|
|
9
|
+
# It shows how MCPToolset automatically discovers and creates tools from the MCP server
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def find_tool(toolset, tool_name):
|
|
13
|
+
"""
|
|
14
|
+
Find a tool by name in the toolset.
|
|
15
|
+
|
|
16
|
+
:param toolset: The toolset to search in
|
|
17
|
+
:param tool_name: The name of the tool to find
|
|
18
|
+
:returns: The tool if found, None otherwise
|
|
19
|
+
"""
|
|
20
|
+
for tool in toolset:
|
|
21
|
+
if tool.name == tool_name:
|
|
22
|
+
return tool
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def main():
|
|
27
|
+
"""Example of using MCPToolset with SSE transport."""
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
# Create the toolset - this automatically discovers all available tools
|
|
31
|
+
# from the MCP server and creates Tool instances for each one
|
|
32
|
+
sse_toolset = MCPToolset(
|
|
33
|
+
server_info=SSEServerInfo(base_url="http://localhost:8000"),
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# Print discovered tools
|
|
37
|
+
print(f"Discovered {len(sse_toolset)} tools:")
|
|
38
|
+
for tool in sse_toolset:
|
|
39
|
+
print(f" - {tool.name}: {tool.description}")
|
|
40
|
+
|
|
41
|
+
# Get tools by name from the toolset
|
|
42
|
+
add_tool = find_tool(sse_toolset, "add")
|
|
43
|
+
if not add_tool:
|
|
44
|
+
print("Add tool not found!")
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
subtract_tool = find_tool(sse_toolset, "subtract")
|
|
48
|
+
if not subtract_tool:
|
|
49
|
+
print("Subtract tool not found!")
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
# Use the tools
|
|
53
|
+
result = add_tool.invoke(a=7, b=3)
|
|
54
|
+
print(f"7 + 3 = {result.content[0].text}")
|
|
55
|
+
|
|
56
|
+
result = subtract_tool.invoke(a=5, b=3)
|
|
57
|
+
print(f"5 - 3 = {result.content[0].text}")
|
|
58
|
+
|
|
59
|
+
result = add_tool.invoke(a=10, b=20)
|
|
60
|
+
print(f"10 + 20 = {result.content[0].text}")
|
|
61
|
+
|
|
62
|
+
except Exception as e:
|
|
63
|
+
print(f"Error in SSE toolset example: {e}")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
if __name__ == "__main__":
|
|
67
|
+
main()
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
from haystack_integrations.tools.mcp import MCPToolset, StdioServerInfo
|
|
6
|
+
|
|
7
|
+
# This example shows how to use MCPToolset with stdio transport
|
|
8
|
+
# MCPToolset automatically discovers all available tools from the MCP server
|
|
9
|
+
# Here we use the mcp-server-time server
|
|
10
|
+
# See https://github.com/modelcontextprotocol/servers/tree/main/src/time for more details
|
|
11
|
+
# prior to running this script, pip install mcp-server-time
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def find_tool(toolset, tool_name):
|
|
15
|
+
"""
|
|
16
|
+
Find a tool by name in the toolset.
|
|
17
|
+
|
|
18
|
+
:param toolset: The toolset to search in
|
|
19
|
+
:param tool_name: The name of the tool to find
|
|
20
|
+
:returns: The tool if found, None otherwise
|
|
21
|
+
"""
|
|
22
|
+
for tool in toolset:
|
|
23
|
+
if tool.name == tool_name:
|
|
24
|
+
return tool
|
|
25
|
+
return None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def main():
|
|
29
|
+
"""Example of using the MCPToolset implementation with stdio transport."""
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
# Create server info for the time service
|
|
33
|
+
server_info = StdioServerInfo(command="uvx", args=["mcp-server-time", "--local-timezone=Europe/Berlin"])
|
|
34
|
+
|
|
35
|
+
# Create the toolset - this will automatically discover all available tools
|
|
36
|
+
stdio_toolset = MCPToolset(server_info=server_info)
|
|
37
|
+
|
|
38
|
+
# Print discovered tools
|
|
39
|
+
print(f"Discovered {len(stdio_toolset)} tools:")
|
|
40
|
+
for tool in stdio_toolset:
|
|
41
|
+
print(f" - {tool.name}: {tool.description}")
|
|
42
|
+
|
|
43
|
+
# Find tools by name using the helper function
|
|
44
|
+
time_tool = find_tool(stdio_toolset, "get_current_time")
|
|
45
|
+
if not time_tool:
|
|
46
|
+
print("Time tool not found!")
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
# Use the get_current_time tool
|
|
50
|
+
result = time_tool.invoke(timezone="America/New_York")
|
|
51
|
+
print(f"Current time in New York: {result.content[0].text}")
|
|
52
|
+
|
|
53
|
+
result = time_tool.invoke(timezone="America/Los_Angeles")
|
|
54
|
+
print(f"Current time in Los Angeles: {result.content[0].text}")
|
|
55
|
+
except Exception as e:
|
|
56
|
+
print(f"Error in stdio toolset example: {e}")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
if __name__ == "__main__":
|
|
60
|
+
main()
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
# Full example of a pipeline that uses MCPToolset to get the current time
|
|
6
|
+
# and then uses the time to answer a user question.
|
|
7
|
+
# Here we use the mcp-server-time mcp package
|
|
8
|
+
# See https://github.com/modelcontextprotocol/servers/tree/main/src/time for more details
|
|
9
|
+
# prior to running this script, pip install mcp-server-time
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
|
|
13
|
+
from haystack import Pipeline
|
|
14
|
+
from haystack.components.converters import OutputAdapter
|
|
15
|
+
from haystack.components.generators.chat import OpenAIChatGenerator
|
|
16
|
+
from haystack.components.tools import ToolInvoker
|
|
17
|
+
from haystack.dataclasses import ChatMessage
|
|
18
|
+
|
|
19
|
+
from haystack_integrations.tools.mcp import MCPToolset, StdioServerInfo
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def main():
|
|
23
|
+
# Create server info for the time service
|
|
24
|
+
server_info = StdioServerInfo(command="uvx", args=["mcp-server-time", "--local-timezone=Europe/Berlin"])
|
|
25
|
+
|
|
26
|
+
# Create the toolset - this will automatically discover all available tools
|
|
27
|
+
mcp_toolset = MCPToolset(server_info)
|
|
28
|
+
# Check if OpenAI API key is set
|
|
29
|
+
api_key = os.environ.get("OPENAI_API_KEY")
|
|
30
|
+
if not api_key:
|
|
31
|
+
print("OPENAI_API_KEY environment variable is not set.")
|
|
32
|
+
print("You need to set it to run the pipeline example.")
|
|
33
|
+
print("For now, demonstrating direct tool usage:")
|
|
34
|
+
|
|
35
|
+
pipeline = Pipeline()
|
|
36
|
+
pipeline.add_component("llm", OpenAIChatGenerator(model="gpt-4o-mini", tools=mcp_toolset))
|
|
37
|
+
pipeline.add_component("tool_invoker", ToolInvoker(tools=mcp_toolset))
|
|
38
|
+
pipeline.add_component(
|
|
39
|
+
"adapter",
|
|
40
|
+
OutputAdapter(
|
|
41
|
+
template="{{ initial_msg + initial_tool_messages + tool_messages }}",
|
|
42
|
+
output_type=list[ChatMessage],
|
|
43
|
+
unsafe=True,
|
|
44
|
+
),
|
|
45
|
+
)
|
|
46
|
+
pipeline.add_component("response_llm", OpenAIChatGenerator(model="gpt-4o-mini"))
|
|
47
|
+
pipeline.connect("llm.replies", "tool_invoker.messages")
|
|
48
|
+
pipeline.connect("llm.replies", "adapter.initial_tool_messages")
|
|
49
|
+
pipeline.connect("tool_invoker.tool_messages", "adapter.tool_messages")
|
|
50
|
+
pipeline.connect("adapter.output", "response_llm.messages")
|
|
51
|
+
|
|
52
|
+
user_input = "What is the time in New York? Be brief." # can be any city
|
|
53
|
+
user_input_msg = ChatMessage.from_user(text=user_input)
|
|
54
|
+
|
|
55
|
+
result = pipeline.run({"llm": {"messages": [user_input_msg]}, "adapter": {"initial_msg": [user_input_msg]}})
|
|
56
|
+
|
|
57
|
+
print(result["response_llm"]["replies"][0].text)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
if __name__ == "__main__":
|
|
61
|
+
main()
|
|
@@ -3,6 +3,7 @@ loaders:
|
|
|
3
3
|
search_path: [../src]
|
|
4
4
|
modules: [
|
|
5
5
|
"haystack_integrations.tools.mcp.mcp_tool",
|
|
6
|
+
"haystack_integrations.tools.mcp.mcp_toolset",
|
|
6
7
|
]
|
|
7
8
|
ignore_when_discovered: ["__init__"]
|
|
8
9
|
processors:
|
|
@@ -17,7 +18,7 @@ renderer:
|
|
|
17
18
|
type: haystack_pydoc_tools.renderers.ReadmeIntegrationRenderer
|
|
18
19
|
excerpt: MCP integration for Haystack
|
|
19
20
|
category_slug: integrations-api
|
|
20
|
-
title:
|
|
21
|
+
title: MCP
|
|
21
22
|
slug: integrations-mcp
|
|
22
23
|
order: 145
|
|
23
24
|
markdown:
|
|
@@ -27,7 +27,12 @@ classifiers = [
|
|
|
27
27
|
"Programming Language :: Python :: 3.13",
|
|
28
28
|
"Programming Language :: Python :: Implementation :: CPython",
|
|
29
29
|
]
|
|
30
|
-
dependencies = [
|
|
30
|
+
dependencies = [
|
|
31
|
+
"mcp",
|
|
32
|
+
"haystack-ai>=2.13.0",
|
|
33
|
+
"exceptiongroup", # Backport of ExceptionGroup for Python < 3.11
|
|
34
|
+
"httpx" # HTTP client library used for SSE connections
|
|
35
|
+
]
|
|
31
36
|
|
|
32
37
|
[project.urls]
|
|
33
38
|
Documentation = "https://github.com/deepset-ai/haystack-core-integrations/tree/main/integrations/mcp#readme"
|
|
@@ -75,7 +80,7 @@ dependencies = [
|
|
|
75
80
|
"pip",
|
|
76
81
|
"black>=23.1.0",
|
|
77
82
|
"mypy>=1.0.0",
|
|
78
|
-
"ruff>=0.0.243"
|
|
83
|
+
"ruff>=0.0.243"
|
|
79
84
|
]
|
|
80
85
|
|
|
81
86
|
[tool.hatch.envs.lint.scripts]
|
|
@@ -133,6 +138,10 @@ ignore = [
|
|
|
133
138
|
"PLR0912",
|
|
134
139
|
"PLR0913",
|
|
135
140
|
"PLR0915",
|
|
141
|
+
# Ignore the errors for process open we're seeing in tests
|
|
142
|
+
"RUF005",
|
|
143
|
+
"S603",
|
|
144
|
+
"S607",
|
|
136
145
|
]
|
|
137
146
|
unfixable = [
|
|
138
147
|
# Don't touch unused imports
|
|
@@ -165,9 +174,12 @@ module = [
|
|
|
165
174
|
"haystack.*",
|
|
166
175
|
"haystack_integrations.*",
|
|
167
176
|
"pytest.*",
|
|
177
|
+
"pytest_asyncio",
|
|
168
178
|
"anyio.*",
|
|
169
179
|
"mcp.*",
|
|
170
180
|
"mcp",
|
|
181
|
+
"httpx",
|
|
182
|
+
"exceptiongroup"
|
|
171
183
|
]
|
|
172
184
|
ignore_missing_imports = true
|
|
173
185
|
|
|
@@ -181,4 +193,4 @@ log_cli = true
|
|
|
181
193
|
asyncio_default_fixture_loop_scope = "function"
|
|
182
194
|
|
|
183
195
|
[tool.hatch.metadata]
|
|
184
|
-
allow-direct-references = true
|
|
196
|
+
allow-direct-references = true
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
1
5
|
from .mcp_tool import (
|
|
2
6
|
MCPClient,
|
|
3
7
|
MCPConnectionError,
|
|
@@ -11,6 +15,7 @@ from .mcp_tool import (
|
|
|
11
15
|
StdioClient,
|
|
12
16
|
StdioServerInfo,
|
|
13
17
|
)
|
|
18
|
+
from .mcp_toolset import MCPToolset
|
|
14
19
|
|
|
15
20
|
__all__ = [
|
|
16
21
|
"MCPClient",
|
|
@@ -20,6 +25,7 @@ __all__ = [
|
|
|
20
25
|
"MCPServerInfo",
|
|
21
26
|
"MCPTool",
|
|
22
27
|
"MCPToolNotFoundError",
|
|
28
|
+
"MCPToolset",
|
|
23
29
|
"SSEClient",
|
|
24
30
|
"SSEServerInfo",
|
|
25
31
|
"StdioClient",
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import asyncio
|
|
6
6
|
import concurrent.futures
|
|
7
7
|
import threading
|
|
8
|
+
import warnings
|
|
8
9
|
from abc import ABC, abstractmethod
|
|
9
10
|
from collections.abc import Coroutine
|
|
10
11
|
from contextlib import AsyncExitStack
|
|
@@ -16,6 +17,7 @@ from haystack import logging
|
|
|
16
17
|
from haystack.core.serialization import generate_qualified_class_name, import_class_by_name
|
|
17
18
|
from haystack.tools import Tool
|
|
18
19
|
from haystack.tools.errors import ToolInvocationError
|
|
20
|
+
from haystack.utils.url_validation import is_valid_http_url
|
|
19
21
|
|
|
20
22
|
from mcp import ClientSession, StdioServerParameters, types
|
|
21
23
|
from mcp.client.sse import sse_client
|
|
@@ -351,18 +353,19 @@ class SSEClient(MCPClient):
|
|
|
351
353
|
MCP client that connects to servers using SSE transport.
|
|
352
354
|
"""
|
|
353
355
|
|
|
354
|
-
def __init__(self,
|
|
356
|
+
def __init__(self, server_info: "SSEServerInfo") -> None:
|
|
355
357
|
"""
|
|
356
|
-
Initialize an SSE MCP client.
|
|
358
|
+
Initialize an SSE MCP client using server configuration.
|
|
357
359
|
|
|
358
|
-
:param
|
|
359
|
-
:param token: Authentication token for the server (optional)
|
|
360
|
-
:param timeout: Connection timeout in seconds
|
|
360
|
+
:param server_info: Configuration object containing URL, token, timeout, etc.
|
|
361
361
|
"""
|
|
362
362
|
super().__init__()
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
363
|
+
|
|
364
|
+
# in post_init we validate the url and set the url field so it is guaranteed to be valid
|
|
365
|
+
# safely ignore the mypy warning here
|
|
366
|
+
self.url: str = server_info.url # type: ignore[assignment]
|
|
367
|
+
self.token: str | None = server_info.token
|
|
368
|
+
self.timeout: int = server_info.timeout
|
|
366
369
|
|
|
367
370
|
async def connect(self) -> list[Tool]:
|
|
368
371
|
"""
|
|
@@ -371,12 +374,11 @@ class SSEClient(MCPClient):
|
|
|
371
374
|
:returns: List of available tools on the server
|
|
372
375
|
:raises MCPConnectionError: If connection to the server fails
|
|
373
376
|
"""
|
|
374
|
-
sse_url = f"{self.base_url}/sse"
|
|
375
377
|
headers = {"Authorization": f"Bearer {self.token}"} if self.token else None
|
|
376
378
|
sse_transport = await self.exit_stack.enter_async_context(
|
|
377
|
-
sse_client(
|
|
379
|
+
sse_client(self.url, headers=headers, timeout=self.timeout)
|
|
378
380
|
)
|
|
379
|
-
return await self._initialize_session_with_transport(sse_transport, f"HTTP server at {self.
|
|
381
|
+
return await self._initialize_session_with_transport(sse_transport, f"HTTP server at {self.url}")
|
|
380
382
|
|
|
381
383
|
|
|
382
384
|
@dataclass
|
|
@@ -432,22 +434,51 @@ class SSEServerInfo(MCPServerInfo):
|
|
|
432
434
|
"""
|
|
433
435
|
Data class that encapsulates SSE MCP server connection parameters.
|
|
434
436
|
|
|
435
|
-
:param
|
|
437
|
+
:param url: Full URL of the MCP server (including /sse endpoint)
|
|
438
|
+
:param base_url: Base URL of the MCP server (deprecated, use url instead)
|
|
436
439
|
:param token: Authentication token for the server (optional)
|
|
437
440
|
:param timeout: Connection timeout in seconds
|
|
438
441
|
"""
|
|
439
442
|
|
|
440
|
-
|
|
443
|
+
url: str | None = None
|
|
444
|
+
base_url: str | None = None # deprecated
|
|
441
445
|
token: str | None = None
|
|
442
446
|
timeout: int = 30
|
|
443
447
|
|
|
448
|
+
def __post_init__(self):
|
|
449
|
+
"""Validate that either url or base_url is provided."""
|
|
450
|
+
if not self.url and not self.base_url:
|
|
451
|
+
message = "Either url or base_url must be provided"
|
|
452
|
+
raise ValueError(message)
|
|
453
|
+
if self.url and self.base_url:
|
|
454
|
+
message = "Only one of url or base_url should be provided, if both are provided, base_url will be ignored"
|
|
455
|
+
warnings.warn(message, DeprecationWarning, stacklevel=2)
|
|
456
|
+
|
|
457
|
+
if self.base_url:
|
|
458
|
+
if not is_valid_http_url(self.base_url):
|
|
459
|
+
message = f"Invalid base_url: {self.base_url}"
|
|
460
|
+
raise ValueError(message)
|
|
461
|
+
|
|
462
|
+
warnings.warn(
|
|
463
|
+
"base_url is deprecated and will be removed in a future version. Use url instead.",
|
|
464
|
+
DeprecationWarning,
|
|
465
|
+
stacklevel=2,
|
|
466
|
+
)
|
|
467
|
+
# from now on only use url for the lifetime of the SSEServerInfo instance, never base_url
|
|
468
|
+
self.url = f"{self.base_url.rstrip('/')}/sse"
|
|
469
|
+
|
|
470
|
+
elif not is_valid_http_url(self.url):
|
|
471
|
+
message = f"Invalid url: {self.url}"
|
|
472
|
+
raise ValueError(message)
|
|
473
|
+
|
|
444
474
|
def create_client(self) -> MCPClient:
|
|
445
475
|
"""
|
|
446
476
|
Create an SSE MCP client.
|
|
447
477
|
|
|
448
|
-
:returns: Configured
|
|
478
|
+
:returns: Configured MCPClient instance
|
|
449
479
|
"""
|
|
450
|
-
|
|
480
|
+
# Pass the validated SSEServerInfo instance directly
|
|
481
|
+
return SSEClient(server_info=self)
|
|
451
482
|
|
|
452
483
|
|
|
453
484
|
@dataclass
|
|
@@ -491,7 +522,7 @@ class MCPTool(Tool):
|
|
|
491
522
|
# Create tool instance
|
|
492
523
|
tool = MCPTool(
|
|
493
524
|
name="add",
|
|
494
|
-
server_info=SSEServerInfo(
|
|
525
|
+
server_info=SSEServerInfo(url="http://localhost:8000/sse")
|
|
495
526
|
)
|
|
496
527
|
|
|
497
528
|
# Use the tool
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
from urllib.parse import urlparse
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
from exceptiongroup import ExceptionGroup
|
|
10
|
+
from haystack import logging
|
|
11
|
+
from haystack.core.serialization import generate_qualified_class_name, import_class_by_name
|
|
12
|
+
from haystack.tools import Tool, Toolset
|
|
13
|
+
|
|
14
|
+
from .mcp_tool import (
|
|
15
|
+
AsyncExecutor,
|
|
16
|
+
MCPConnectionError,
|
|
17
|
+
MCPServerInfo,
|
|
18
|
+
MCPToolNotFoundError,
|
|
19
|
+
SSEServerInfo,
|
|
20
|
+
StdioServerInfo,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class MCPToolset(Toolset):
|
|
27
|
+
"""
|
|
28
|
+
A Toolset that connects to an MCP (Model Context Protocol) server and provides
|
|
29
|
+
access to its tools.
|
|
30
|
+
|
|
31
|
+
MCPToolset dynamically discovers and loads all tools from any MCP-compliant server,
|
|
32
|
+
supporting both network-based SSE connections and local process-based stdio connections.
|
|
33
|
+
This dual connectivity allows for integrating with both remote and local MCP servers.
|
|
34
|
+
|
|
35
|
+
Example using MCPToolset in a Haystack Pipeline:
|
|
36
|
+
```python
|
|
37
|
+
# Prerequisites:
|
|
38
|
+
# 1. pip install uvx mcp-server-time # Install required MCP server and tools
|
|
39
|
+
# 2. export OPENAI_API_KEY="your-api-key" # Set up your OpenAI API key
|
|
40
|
+
|
|
41
|
+
import os
|
|
42
|
+
from haystack import Pipeline
|
|
43
|
+
from haystack.components.converters import OutputAdapter
|
|
44
|
+
from haystack.components.generators.chat import OpenAIChatGenerator
|
|
45
|
+
from haystack.components.tools import ToolInvoker
|
|
46
|
+
from haystack.dataclasses import ChatMessage
|
|
47
|
+
from haystack_integrations.tools.mcp import MCPToolset, StdioServerInfo
|
|
48
|
+
|
|
49
|
+
# Create server info for the time service (can also use SSEServerInfo for remote servers)
|
|
50
|
+
server_info = StdioServerInfo(command="uvx", args=["mcp-server-time", "--local-timezone=Europe/Berlin"])
|
|
51
|
+
|
|
52
|
+
# Create the toolset - this will automatically discover all available tools
|
|
53
|
+
# You can optionally specify which tools to include
|
|
54
|
+
mcp_toolset = MCPToolset(
|
|
55
|
+
server_info=server_info,
|
|
56
|
+
tool_names=["get_current_time"] # Only include the get_current_time tool
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Create a pipeline with the toolset
|
|
60
|
+
pipeline = Pipeline()
|
|
61
|
+
pipeline.add_component("llm", OpenAIChatGenerator(model="gpt-4o-mini", tools=mcp_toolset))
|
|
62
|
+
pipeline.add_component("tool_invoker", ToolInvoker(tools=mcp_toolset))
|
|
63
|
+
pipeline.add_component(
|
|
64
|
+
"adapter",
|
|
65
|
+
OutputAdapter(
|
|
66
|
+
template="{{ initial_msg + initial_tool_messages + tool_messages }}",
|
|
67
|
+
output_type=list[ChatMessage],
|
|
68
|
+
unsafe=True,
|
|
69
|
+
),
|
|
70
|
+
)
|
|
71
|
+
pipeline.add_component("response_llm", OpenAIChatGenerator(model="gpt-4o-mini"))
|
|
72
|
+
pipeline.connect("llm.replies", "tool_invoker.messages")
|
|
73
|
+
pipeline.connect("llm.replies", "adapter.initial_tool_messages")
|
|
74
|
+
pipeline.connect("tool_invoker.tool_messages", "adapter.tool_messages")
|
|
75
|
+
pipeline.connect("adapter.output", "response_llm.messages")
|
|
76
|
+
|
|
77
|
+
# Run the pipeline with a user question
|
|
78
|
+
user_input = "What is the time in New York? Be brief."
|
|
79
|
+
user_input_msg = ChatMessage.from_user(text=user_input)
|
|
80
|
+
|
|
81
|
+
result = pipeline.run({"llm": {"messages": [user_input_msg]}, "adapter": {"initial_msg": [user_input_msg]}})
|
|
82
|
+
print(result["response_llm"]["replies"][0].text)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
You can also use the toolset via MCP SSE to talk to remote servers:
|
|
86
|
+
```python
|
|
87
|
+
from haystack_integrations.tools.mcp import MCPToolset, SSEServerInfo
|
|
88
|
+
from haystack.components.tools import ToolInvoker
|
|
89
|
+
|
|
90
|
+
# Create the toolset with an SSE connection
|
|
91
|
+
sse_toolset = MCPToolset(
|
|
92
|
+
server_info=SSEServerInfo(url="http://some-remote-server.com:8000/sse"),
|
|
93
|
+
tool_names=["add", "subtract"] # Only include specific tools
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# Use the toolset as shown in the pipeline example above
|
|
97
|
+
```
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
def __init__(
|
|
101
|
+
self,
|
|
102
|
+
server_info: MCPServerInfo,
|
|
103
|
+
tool_names: list[str] | None = None,
|
|
104
|
+
connection_timeout: float = 30.0,
|
|
105
|
+
invocation_timeout: float = 30.0,
|
|
106
|
+
):
|
|
107
|
+
"""
|
|
108
|
+
Initialize the MCP toolset.
|
|
109
|
+
|
|
110
|
+
:param server_info: Connection information for the MCP server
|
|
111
|
+
:param tool_names: Optional list of tool names to include. If provided, only tools with
|
|
112
|
+
matching names will be added to the toolset.
|
|
113
|
+
:param connection_timeout: Timeout in seconds for server connection
|
|
114
|
+
:param invocation_timeout: Default timeout in seconds for tool invocations
|
|
115
|
+
:raises MCPToolNotFoundError: If any of the specified tool names are not found on the server
|
|
116
|
+
"""
|
|
117
|
+
# Store configuration
|
|
118
|
+
self.server_info = server_info
|
|
119
|
+
self.tool_names = tool_names
|
|
120
|
+
self.connection_timeout = connection_timeout
|
|
121
|
+
self.invocation_timeout = invocation_timeout
|
|
122
|
+
|
|
123
|
+
# Connect and load tools
|
|
124
|
+
try:
|
|
125
|
+
# Create the appropriate client using the factory method
|
|
126
|
+
client = self.server_info.create_client()
|
|
127
|
+
|
|
128
|
+
# Connect and get available tools using AsyncExecutor
|
|
129
|
+
tools = AsyncExecutor.get_instance().run(client.connect(), timeout=self.connection_timeout)
|
|
130
|
+
|
|
131
|
+
# If tool_names is provided, validate that all requested tools exist
|
|
132
|
+
if self.tool_names:
|
|
133
|
+
available_tools = {tool.name for tool in tools}
|
|
134
|
+
missing_tools = set(self.tool_names) - available_tools
|
|
135
|
+
if missing_tools:
|
|
136
|
+
message = (
|
|
137
|
+
f"The following tools were not found: {', '.join(missing_tools)}. "
|
|
138
|
+
f"Available tools: {', '.join(available_tools)}"
|
|
139
|
+
)
|
|
140
|
+
raise MCPToolNotFoundError(
|
|
141
|
+
message=message, tool_name=next(iter(missing_tools)), available_tools=list(available_tools)
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# This is a factory that creates the invocation function for the Tool
|
|
145
|
+
def create_invoke_tool(mcp_client, tool_name, tool_timeout):
|
|
146
|
+
def invoke_tool(**kwargs) -> Any:
|
|
147
|
+
"""Invoke a tool using the existing client and AsyncExecutor."""
|
|
148
|
+
result = AsyncExecutor.get_instance().run(
|
|
149
|
+
mcp_client.call_tool(tool_name, kwargs), timeout=tool_timeout
|
|
150
|
+
)
|
|
151
|
+
return result
|
|
152
|
+
|
|
153
|
+
return invoke_tool
|
|
154
|
+
|
|
155
|
+
# Create Tool instances not MCPTool for each available tool
|
|
156
|
+
haystack_tools = []
|
|
157
|
+
for tool_info in tools:
|
|
158
|
+
# Skip tools not in the tool_names list if tool_names is provided
|
|
159
|
+
if self.tool_names is not None and tool_info.name not in self.tool_names:
|
|
160
|
+
logger.debug(
|
|
161
|
+
"Skipping tool '{name}' as it's not in the requested tool_names list", name=tool_info.name
|
|
162
|
+
)
|
|
163
|
+
continue
|
|
164
|
+
|
|
165
|
+
# Use the helper function to create the invoke_tool function
|
|
166
|
+
tool = Tool(
|
|
167
|
+
name=tool_info.name,
|
|
168
|
+
description=tool_info.description,
|
|
169
|
+
parameters=tool_info.inputSchema,
|
|
170
|
+
function=create_invoke_tool(client, tool_info.name, self.invocation_timeout),
|
|
171
|
+
)
|
|
172
|
+
haystack_tools.append(tool)
|
|
173
|
+
|
|
174
|
+
# Initialize parent class with complete tools list
|
|
175
|
+
super().__init__(tools=haystack_tools)
|
|
176
|
+
|
|
177
|
+
except Exception as e:
|
|
178
|
+
if isinstance(self.server_info, SSEServerInfo):
|
|
179
|
+
base_message = f"Failed to connect to SSE server at {self.server_info.url}"
|
|
180
|
+
checks = ["1. The server is running"]
|
|
181
|
+
|
|
182
|
+
# Check for ConnectError in exception group or direct exception
|
|
183
|
+
has_connect_error = isinstance(e, httpx.ConnectError) or (
|
|
184
|
+
isinstance(e, ExceptionGroup) and any(isinstance(exc, httpx.ConnectError) for exc in e.exceptions)
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
if has_connect_error:
|
|
188
|
+
# Use urlparse to reliably get scheme, hostname, and port
|
|
189
|
+
parsed_url = urlparse(self.server_info.url)
|
|
190
|
+
port_str = ""
|
|
191
|
+
if parsed_url.port:
|
|
192
|
+
port_str = str(parsed_url.port)
|
|
193
|
+
elif parsed_url.scheme == "http":
|
|
194
|
+
port_str = "80 (default)"
|
|
195
|
+
elif parsed_url.scheme == "https":
|
|
196
|
+
port_str = "443 (default)"
|
|
197
|
+
else:
|
|
198
|
+
port_str = "unknown (scheme not http/https or missing)" # Or handle more schemes if needed
|
|
199
|
+
|
|
200
|
+
# Ensure hostname is handled correctly (it might be None)
|
|
201
|
+
hostname_str = str(parsed_url.hostname) if parsed_url.hostname else "<unknown>"
|
|
202
|
+
message = f"2. The address '{hostname_str}' and port '{port_str}' are correct"
|
|
203
|
+
checks.append(message)
|
|
204
|
+
checks.append("3. There are no firewall or network connectivity issues")
|
|
205
|
+
message = f"{base_message}. Please check if:\n" + "\\n".join(checks)
|
|
206
|
+
else:
|
|
207
|
+
message = f"{base_message}: {e}"
|
|
208
|
+
elif isinstance(self.server_info, StdioServerInfo): # stdio connection
|
|
209
|
+
base_message = "Failed to start MCP server process"
|
|
210
|
+
stdio_info = self.server_info
|
|
211
|
+
args_str = " ".join(stdio_info.args) if stdio_info.args else ""
|
|
212
|
+
cmd = f"{stdio_info.command}{' ' + args_str if args_str else ''}"
|
|
213
|
+
checks = [f"1. The command and arguments are correct (attempted: {cmd})"]
|
|
214
|
+
message = f"{base_message}. Please check if:\n" + "\n".join(checks)
|
|
215
|
+
else:
|
|
216
|
+
message = f"Unsupported server info type: {type(self.server_info)}"
|
|
217
|
+
|
|
218
|
+
raise MCPConnectionError(message=message, server_info=self.server_info, operation="initialize") from e
|
|
219
|
+
|
|
220
|
+
def to_dict(self) -> dict[str, Any]:
|
|
221
|
+
"""
|
|
222
|
+
Serialize the MCPToolset to a dictionary.
|
|
223
|
+
|
|
224
|
+
:returns: A dictionary representation of the MCPToolset
|
|
225
|
+
"""
|
|
226
|
+
return {
|
|
227
|
+
"type": generate_qualified_class_name(type(self)),
|
|
228
|
+
"data": {
|
|
229
|
+
"server_info": self.server_info.to_dict(),
|
|
230
|
+
"tool_names": self.tool_names,
|
|
231
|
+
"connection_timeout": self.connection_timeout,
|
|
232
|
+
"invocation_timeout": self.invocation_timeout,
|
|
233
|
+
},
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
@classmethod
|
|
237
|
+
def from_dict(cls, data: dict[str, Any]) -> "MCPToolset":
|
|
238
|
+
"""
|
|
239
|
+
Deserialize an MCPToolset from a dictionary.
|
|
240
|
+
|
|
241
|
+
:param data: Dictionary representation of the MCPToolset
|
|
242
|
+
:returns: A new MCPToolset instance
|
|
243
|
+
"""
|
|
244
|
+
inner_data = data["data"]
|
|
245
|
+
|
|
246
|
+
# Reconstruct the server_info object
|
|
247
|
+
server_info_dict = inner_data.get("server_info", {})
|
|
248
|
+
server_info_class = import_class_by_name(server_info_dict["type"])
|
|
249
|
+
server_info = server_info_class.from_dict(server_info_dict)
|
|
250
|
+
|
|
251
|
+
# Create a new MCPToolset instance
|
|
252
|
+
return cls(
|
|
253
|
+
server_info=server_info,
|
|
254
|
+
tool_names=inner_data.get("tool_names"),
|
|
255
|
+
connection_timeout=inner_data.get("connection_timeout", 30.0),
|
|
256
|
+
invocation_timeout=inner_data.get("invocation_timeout", 30.0),
|
|
257
|
+
)
|
|
@@ -132,6 +132,26 @@ class TestMCPServerInfo:
|
|
|
132
132
|
assert new_info.token == "test-token"
|
|
133
133
|
assert new_info.timeout == 45
|
|
134
134
|
|
|
135
|
+
def test_url_base_url_validation(self):
|
|
136
|
+
"""Test validation of url and base_url parameters."""
|
|
137
|
+
# Test with neither url nor base_url
|
|
138
|
+
with pytest.raises(ValueError, match="Either url or base_url must be provided"):
|
|
139
|
+
SSEServerInfo()
|
|
140
|
+
|
|
141
|
+
# Test with both url and base_url
|
|
142
|
+
with pytest.warns(DeprecationWarning, match="base_url is deprecated"):
|
|
143
|
+
SSEServerInfo(url="http://example.com/sse", base_url="http://example.com")
|
|
144
|
+
|
|
145
|
+
# Test with only url
|
|
146
|
+
server_info = SSEServerInfo(url="http://example.com/sse")
|
|
147
|
+
assert server_info.url == "http://example.com/sse"
|
|
148
|
+
assert server_info.base_url is None
|
|
149
|
+
|
|
150
|
+
# Test with only base_url (deprecated but supported)
|
|
151
|
+
with pytest.warns(DeprecationWarning, match="base_url is deprecated"):
|
|
152
|
+
server_info = SSEServerInfo(base_url="http://example.com")
|
|
153
|
+
assert server_info.base_url == "http://example.com" # Should preserve original base_url
|
|
154
|
+
|
|
135
155
|
def test_stdio_server_info_serde(self):
|
|
136
156
|
"""Test serialization/deserialization of StdioServerInfo."""
|
|
137
157
|
server_info = StdioServerInfo(command="python", args=["-m", "mcp_server_time"], env={"TEST_ENV": "value"})
|
|
@@ -157,7 +177,7 @@ class TestMCPServerInfo:
|
|
|
157
177
|
http_client = http_info.create_client()
|
|
158
178
|
stdio_client = stdio_info.create_client()
|
|
159
179
|
|
|
160
|
-
assert http_client.
|
|
180
|
+
assert http_client.url == "http://example.com/sse"
|
|
161
181
|
assert stdio_client.command == "python"
|
|
162
182
|
|
|
163
183
|
|
|
@@ -345,8 +365,8 @@ if __name__ == "__main__":
|
|
|
345
365
|
server_process = None
|
|
346
366
|
try:
|
|
347
367
|
# Start the server in a separate process
|
|
348
|
-
server_process = subprocess.Popen(
|
|
349
|
-
["python", server_script_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
|
|
368
|
+
server_process = subprocess.Popen(
|
|
369
|
+
["python", server_script_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
|
|
350
370
|
)
|
|
351
371
|
|
|
352
372
|
# Give the server a moment to start
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import time
|
|
4
|
+
from unittest.mock import AsyncMock, MagicMock, patch
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
import pytest_asyncio
|
|
8
|
+
from haystack.tools import Tool
|
|
9
|
+
|
|
10
|
+
from haystack_integrations.tools.mcp import MCPToolset, SSEServerInfo
|
|
11
|
+
from haystack_integrations.tools.mcp.mcp_tool import MCPConnectionError
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@pytest_asyncio.fixture
|
|
15
|
+
async def mock_mcp_toolset():
|
|
16
|
+
"""Fixture to create a pre-configured MCPToolset for testing without server connection."""
|
|
17
|
+
mock_tool1 = MagicMock(spec=Tool)
|
|
18
|
+
mock_tool1.name = "tool1"
|
|
19
|
+
mock_tool1.description = "Test tool 1"
|
|
20
|
+
mock_tool1.inputSchema = {"type": "object", "properties": {}}
|
|
21
|
+
|
|
22
|
+
mock_tool2 = MagicMock(spec=Tool)
|
|
23
|
+
mock_tool2.name = "tool2"
|
|
24
|
+
mock_tool2.description = "Test tool 2"
|
|
25
|
+
mock_tool2.inputSchema = {"type": "object", "properties": {}}
|
|
26
|
+
|
|
27
|
+
mock_client = AsyncMock()
|
|
28
|
+
mock_client.connect.return_value = [mock_tool1, mock_tool2]
|
|
29
|
+
mock_client.close = AsyncMock()
|
|
30
|
+
|
|
31
|
+
with (
|
|
32
|
+
patch("haystack_integrations.tools.mcp.mcp_toolset.AsyncExecutor.get_instance") as mock_executor,
|
|
33
|
+
patch("haystack_integrations.tools.mcp.mcp_tool.MCPServerInfo.create_client") as mock_create_client,
|
|
34
|
+
):
|
|
35
|
+
mock_create_client.return_value = mock_client
|
|
36
|
+
mock_executor.return_value.run.return_value = [mock_tool1, mock_tool2]
|
|
37
|
+
|
|
38
|
+
toolset = MCPToolset(
|
|
39
|
+
server_info=SSEServerInfo(base_url="http://example.com", token="test-token"),
|
|
40
|
+
connection_timeout=45,
|
|
41
|
+
invocation_timeout=60,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
yield toolset
|
|
45
|
+
|
|
46
|
+
# Cleanup
|
|
47
|
+
await mock_client.close()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@pytest_asyncio.fixture
|
|
51
|
+
async def mock_mcp_toolset_with_tool_names():
|
|
52
|
+
"""Fixture to create an MCPToolset with specific tool_names filtering."""
|
|
53
|
+
mock_tool2 = MagicMock(spec=Tool)
|
|
54
|
+
mock_tool2.name = "tool2"
|
|
55
|
+
mock_tool2.description = "Test tool 2"
|
|
56
|
+
mock_tool2.inputSchema = {"type": "object", "properties": {}}
|
|
57
|
+
|
|
58
|
+
mock_client = AsyncMock()
|
|
59
|
+
mock_client.connect.return_value = [mock_tool2]
|
|
60
|
+
mock_client.close = AsyncMock()
|
|
61
|
+
|
|
62
|
+
with (
|
|
63
|
+
patch("haystack_integrations.tools.mcp.mcp_toolset.AsyncExecutor.get_instance") as mock_executor,
|
|
64
|
+
patch("haystack_integrations.tools.mcp.mcp_tool.MCPServerInfo.create_client") as mock_create_client,
|
|
65
|
+
):
|
|
66
|
+
mock_create_client.return_value = mock_client
|
|
67
|
+
mock_executor.return_value.run.return_value = [mock_tool2]
|
|
68
|
+
|
|
69
|
+
toolset = MCPToolset(
|
|
70
|
+
server_info=SSEServerInfo(base_url="http://example.com", token="test-token"),
|
|
71
|
+
tool_names=["tool2"], # Only include tool2
|
|
72
|
+
connection_timeout=45,
|
|
73
|
+
invocation_timeout=60,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
yield toolset
|
|
77
|
+
|
|
78
|
+
# Cleanup
|
|
79
|
+
await mock_client.close()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@pytest.mark.asyncio
|
|
83
|
+
class TestMCPToolset:
|
|
84
|
+
"""Tests for the MCPToolset class."""
|
|
85
|
+
|
|
86
|
+
async def test_toolset_initialization(self, mock_mcp_toolset):
|
|
87
|
+
"""Test if the MCPToolset initializes correctly and loads tools."""
|
|
88
|
+
toolset = mock_mcp_toolset
|
|
89
|
+
|
|
90
|
+
assert isinstance(toolset.server_info, SSEServerInfo)
|
|
91
|
+
assert toolset.connection_timeout == 45
|
|
92
|
+
assert toolset.invocation_timeout == 60
|
|
93
|
+
assert len(toolset) == 2
|
|
94
|
+
|
|
95
|
+
tool_names = [tool.name for tool in toolset.tools]
|
|
96
|
+
assert "tool1" in tool_names
|
|
97
|
+
assert "tool2" in tool_names
|
|
98
|
+
|
|
99
|
+
tool1 = next(tool for tool in toolset.tools if tool.name == "tool1")
|
|
100
|
+
tool2 = next(tool for tool in toolset.tools if tool.name == "tool2")
|
|
101
|
+
|
|
102
|
+
assert tool1.name == "tool1"
|
|
103
|
+
assert tool2.name == "tool2"
|
|
104
|
+
assert tool1.description == "Test tool 1"
|
|
105
|
+
assert tool2.description == "Test tool 2"
|
|
106
|
+
|
|
107
|
+
async def test_toolset_with_filtered_tools(self, mock_mcp_toolset_with_tool_names):
|
|
108
|
+
"""Test if the MCPToolset correctly filters tools based on tool_names parameter."""
|
|
109
|
+
toolset = mock_mcp_toolset_with_tool_names
|
|
110
|
+
|
|
111
|
+
# Verify tool_names parameter was stored
|
|
112
|
+
assert toolset.tool_names == ["tool2"]
|
|
113
|
+
|
|
114
|
+
# Verify only the specified tool was added
|
|
115
|
+
assert len(toolset) == 1
|
|
116
|
+
|
|
117
|
+
tool_names = [tool.name for tool in toolset.tools]
|
|
118
|
+
assert "tool1" not in tool_names
|
|
119
|
+
assert "tool2" in tool_names
|
|
120
|
+
|
|
121
|
+
# Check the tool that was included
|
|
122
|
+
tool = toolset.tools[0]
|
|
123
|
+
assert tool.name == "tool2"
|
|
124
|
+
assert tool.description == "Test tool 2"
|
|
125
|
+
|
|
126
|
+
async def test_toolset_serde(self, mock_mcp_toolset):
|
|
127
|
+
"""Test serialization and deserialization of MCPToolset."""
|
|
128
|
+
toolset = mock_mcp_toolset
|
|
129
|
+
|
|
130
|
+
toolset_dict = toolset.to_dict()
|
|
131
|
+
assert toolset_dict["type"] == "haystack_integrations.tools.mcp.mcp_toolset.MCPToolset"
|
|
132
|
+
assert toolset_dict["data"]["connection_timeout"] == 45
|
|
133
|
+
assert toolset_dict["data"]["invocation_timeout"] == 60
|
|
134
|
+
assert toolset_dict["data"]["server_info"]["base_url"] == "http://example.com"
|
|
135
|
+
assert toolset_dict["data"]["tool_names"] is None
|
|
136
|
+
|
|
137
|
+
with patch("haystack_integrations.tools.mcp.mcp_toolset.MCPToolset.__init__", return_value=None) as mock_init:
|
|
138
|
+
MCPToolset.from_dict(toolset_dict)
|
|
139
|
+
|
|
140
|
+
mock_init.assert_called_once()
|
|
141
|
+
_, kwargs = mock_init.call_args
|
|
142
|
+
assert kwargs["connection_timeout"] == 45
|
|
143
|
+
assert kwargs["invocation_timeout"] == 60
|
|
144
|
+
assert kwargs["tool_names"] is None
|
|
145
|
+
assert isinstance(kwargs["server_info"], SSEServerInfo)
|
|
146
|
+
assert kwargs["server_info"].base_url == "http://example.com"
|
|
147
|
+
|
|
148
|
+
async def test_toolset_serde_with_tool_names(self, mock_mcp_toolset_with_tool_names):
|
|
149
|
+
"""Test serialization and deserialization of MCPToolset with tool_names parameter."""
|
|
150
|
+
toolset = mock_mcp_toolset_with_tool_names
|
|
151
|
+
|
|
152
|
+
toolset_dict = toolset.to_dict()
|
|
153
|
+
assert toolset_dict["type"] == "haystack_integrations.tools.mcp.mcp_toolset.MCPToolset"
|
|
154
|
+
assert toolset_dict["data"]["tool_names"] == ["tool2"]
|
|
155
|
+
|
|
156
|
+
with patch("haystack_integrations.tools.mcp.mcp_toolset.MCPToolset.__init__", return_value=None) as mock_init:
|
|
157
|
+
MCPToolset.from_dict(toolset_dict)
|
|
158
|
+
|
|
159
|
+
mock_init.assert_called_once()
|
|
160
|
+
_, kwargs = mock_init.call_args
|
|
161
|
+
assert kwargs["tool_names"] == ["tool2"]
|
|
162
|
+
|
|
163
|
+
async def test_toolset_combination(self, mock_mcp_toolset):
|
|
164
|
+
"""Test combining MCPToolset with other tools."""
|
|
165
|
+
toolset = mock_mcp_toolset
|
|
166
|
+
|
|
167
|
+
def add(a: int, b: int) -> int:
|
|
168
|
+
"""Add two numbers"""
|
|
169
|
+
return a + b
|
|
170
|
+
|
|
171
|
+
add_tool = Tool(
|
|
172
|
+
name="add",
|
|
173
|
+
description="Add two numbers",
|
|
174
|
+
function=add,
|
|
175
|
+
parameters={
|
|
176
|
+
"type": "object",
|
|
177
|
+
"properties": {"a": {"type": "integer"}, "b": {"type": "integer"}},
|
|
178
|
+
"required": ["a", "b"],
|
|
179
|
+
},
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
combined_tools = toolset + [add_tool]
|
|
183
|
+
|
|
184
|
+
assert len(combined_tools) == 3
|
|
185
|
+
|
|
186
|
+
tool_names = [tool.name for tool in combined_tools.tools]
|
|
187
|
+
assert "tool1" in tool_names
|
|
188
|
+
assert "tool2" in tool_names
|
|
189
|
+
assert "add" in tool_names
|
|
190
|
+
|
|
191
|
+
async def test_toolset_error_handling(self):
|
|
192
|
+
"""Test error handling during toolset initialization."""
|
|
193
|
+
with pytest.raises(MCPConnectionError):
|
|
194
|
+
MCPToolset(
|
|
195
|
+
server_info=SSEServerInfo(base_url="http://example.com"),
|
|
196
|
+
connection_timeout=30.0,
|
|
197
|
+
invocation_timeout=30.0,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@pytest.mark.integration
|
|
202
|
+
class TestMCPToolsetIntegration:
|
|
203
|
+
"""Integration tests for MCPToolset."""
|
|
204
|
+
|
|
205
|
+
@pytest.mark.skipif(sys.platform == "win32", reason="Windows fails for some reason")
|
|
206
|
+
def test_toolset_with_sse_connection(self):
|
|
207
|
+
"""Test MCPToolset with an SSE connection to a simple server."""
|
|
208
|
+
import socket
|
|
209
|
+
import subprocess
|
|
210
|
+
import tempfile
|
|
211
|
+
|
|
212
|
+
# Find an available port
|
|
213
|
+
def find_free_port():
|
|
214
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
215
|
+
s.bind(("", 0))
|
|
216
|
+
return s.getsockname()[1]
|
|
217
|
+
|
|
218
|
+
port = find_free_port()
|
|
219
|
+
|
|
220
|
+
# Create a temporary file for the server script
|
|
221
|
+
with tempfile.NamedTemporaryFile(suffix=".py", delete=False) as temp_file:
|
|
222
|
+
temp_file.write(
|
|
223
|
+
f"""
|
|
224
|
+
from mcp.server.fastmcp import FastMCP
|
|
225
|
+
mcp = FastMCP("MCP Calculator", host="127.0.0.1", port={port})
|
|
226
|
+
|
|
227
|
+
@mcp.tool()
|
|
228
|
+
def add(a: int, b: int) -> int:
|
|
229
|
+
\"\"\"Add two numbers\"\"\"
|
|
230
|
+
return a + b
|
|
231
|
+
|
|
232
|
+
@mcp.tool()
|
|
233
|
+
def subtract(a: int, b: int) -> int:
|
|
234
|
+
\"\"\"Subtract b from a\"\"\"
|
|
235
|
+
return a - b
|
|
236
|
+
|
|
237
|
+
if __name__ == "__main__":
|
|
238
|
+
try:
|
|
239
|
+
mcp.run(transport="sse")
|
|
240
|
+
except Exception as e:
|
|
241
|
+
sys.exit(1)
|
|
242
|
+
""".encode()
|
|
243
|
+
)
|
|
244
|
+
server_script_path = temp_file.name
|
|
245
|
+
|
|
246
|
+
server_process = None
|
|
247
|
+
try:
|
|
248
|
+
# Start the server in a separate process
|
|
249
|
+
server_process = subprocess.Popen(
|
|
250
|
+
["python", server_script_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
# Give the server a moment to start
|
|
254
|
+
time.sleep(2)
|
|
255
|
+
|
|
256
|
+
# Create the toolset
|
|
257
|
+
server_info = SSEServerInfo(base_url=f"http://127.0.0.1:{port}")
|
|
258
|
+
toolset = MCPToolset(server_info=server_info)
|
|
259
|
+
|
|
260
|
+
# Verify we got both tools
|
|
261
|
+
assert len(toolset) == 2
|
|
262
|
+
|
|
263
|
+
tool_names = [tool.name for tool in toolset.tools]
|
|
264
|
+
assert "add" in tool_names
|
|
265
|
+
assert "subtract" in tool_names
|
|
266
|
+
|
|
267
|
+
# Test the add tool
|
|
268
|
+
add_tool = next(tool for tool in toolset.tools if tool.name == "add")
|
|
269
|
+
result = add_tool.invoke(a=5, b=3)
|
|
270
|
+
assert result.content[0].text == "8"
|
|
271
|
+
|
|
272
|
+
# Test the subtract tool
|
|
273
|
+
subtract_tool = next(tool for tool in toolset.tools if tool.name == "subtract")
|
|
274
|
+
result = subtract_tool.invoke(a=10, b=4)
|
|
275
|
+
assert result.content[0].text == "6"
|
|
276
|
+
|
|
277
|
+
except Exception:
|
|
278
|
+
# Check server output for clues
|
|
279
|
+
if server_process and server_process.poll() is None:
|
|
280
|
+
server_process.terminate()
|
|
281
|
+
raise
|
|
282
|
+
|
|
283
|
+
finally:
|
|
284
|
+
# Clean up
|
|
285
|
+
if server_process:
|
|
286
|
+
if server_process.poll() is None: # Process is still running
|
|
287
|
+
server_process.terminate()
|
|
288
|
+
try:
|
|
289
|
+
server_process.wait(timeout=5)
|
|
290
|
+
except subprocess.TimeoutExpired:
|
|
291
|
+
server_process.kill()
|
|
292
|
+
server_process.wait(timeout=5)
|
|
293
|
+
|
|
294
|
+
# Remove the temporary file
|
|
295
|
+
if os.path.exists(server_script_path):
|
|
296
|
+
os.remove(server_script_path)
|
mcp_haystack-0.0.2/CHANGELOG.md
DELETED
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
|
|
2
|
-
#
|
|
3
|
-
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
-
|
|
5
|
-
"""
|
|
6
|
-
Example of a Hayhooks PipelineWrapper for deploying an MCP Tool-based time pipeline as a REST API.
|
|
7
|
-
|
|
8
|
-
To run this example:
|
|
9
|
-
|
|
10
|
-
1. Install Hayhooks and dependencies:
|
|
11
|
-
$ pip install hayhooks haystack-ai
|
|
12
|
-
|
|
13
|
-
2. Start the Hayhooks server:
|
|
14
|
-
$ hayhooks run
|
|
15
|
-
|
|
16
|
-
3. Deploy this pipeline wrapper:
|
|
17
|
-
$ hayhooks pipeline deploy-files -n time_pipeline {root_dir_for_mcp_haystack_integration}/examples/hayhooks/
|
|
18
|
-
|
|
19
|
-
4. Invoke via curl:
|
|
20
|
-
$ curl -X POST 'http://localhost:1416/time_pipeline/run' -H 'accept: application/json' -H 'Content-Type: application/json' -d '{"query":"What is the time in San Francisco? Be brief"}'
|
|
21
|
-
|
|
22
|
-
For more information, see: https://github.com/deepset-ai/hayhooks
|
|
23
|
-
"""
|
|
24
|
-
|
|
25
|
-
from hayhooks import BasePipelineWrapper
|
|
26
|
-
from haystack import Pipeline
|
|
27
|
-
from haystack.components.converters import OutputAdapter
|
|
28
|
-
from haystack.components.generators.chat import OpenAIChatGenerator
|
|
29
|
-
from haystack.components.tools import ToolInvoker
|
|
30
|
-
from haystack.dataclasses import ChatMessage
|
|
31
|
-
|
|
32
|
-
from haystack_integrations.tools.mcp.mcp_tool import MCPTool, StdioServerInfo
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
class PipelineWrapper(BasePipelineWrapper):
|
|
36
|
-
def setup(self) -> None:
|
|
37
|
-
"""
|
|
38
|
-
Setup the pipeline with MCP time tool.
|
|
39
|
-
|
|
40
|
-
This creates a pipeline that uses an MCP time tool to get the current time
|
|
41
|
-
and then uses the time to answer a user question.
|
|
42
|
-
"""
|
|
43
|
-
|
|
44
|
-
time_tool = MCPTool(
|
|
45
|
-
name="get_current_time",
|
|
46
|
-
server_info=StdioServerInfo(command="uvx", args=["mcp-server-time", "--local-timezone=Europe/Berlin"]),
|
|
47
|
-
)
|
|
48
|
-
|
|
49
|
-
self.pipeline = Pipeline()
|
|
50
|
-
self.pipeline.add_component("llm", OpenAIChatGenerator(model="gpt-4o-mini", tools=[time_tool]))
|
|
51
|
-
self.pipeline.add_component("tool_invoker", ToolInvoker(tools=[time_tool]))
|
|
52
|
-
self.pipeline.add_component(
|
|
53
|
-
"adapter",
|
|
54
|
-
OutputAdapter(
|
|
55
|
-
template="{{ initial_msg + initial_tool_messages + tool_messages }}",
|
|
56
|
-
output_type=list[ChatMessage],
|
|
57
|
-
unsafe=True,
|
|
58
|
-
),
|
|
59
|
-
)
|
|
60
|
-
self.pipeline.add_component("response_llm", OpenAIChatGenerator(model="gpt-4o-mini"))
|
|
61
|
-
self.pipeline.connect("llm.replies", "tool_invoker.messages")
|
|
62
|
-
self.pipeline.connect("llm.replies", "adapter.initial_tool_messages")
|
|
63
|
-
self.pipeline.connect("tool_invoker.tool_messages", "adapter.tool_messages")
|
|
64
|
-
self.pipeline.connect("adapter.output", "response_llm.messages")
|
|
65
|
-
|
|
66
|
-
def run_api(self, query: str) -> str:
|
|
67
|
-
"""
|
|
68
|
-
Run the pipeline with a user query.
|
|
69
|
-
|
|
70
|
-
:param query: The user query asking about time
|
|
71
|
-
:return: The response from the LLM
|
|
72
|
-
"""
|
|
73
|
-
# Create a user message from the query
|
|
74
|
-
user_input_msg = ChatMessage.from_user(text=query)
|
|
75
|
-
|
|
76
|
-
# Run the pipeline
|
|
77
|
-
result = self.pipeline.run(
|
|
78
|
-
{"llm": {"messages": [user_input_msg]}, "adapter": {"initial_msg": [user_input_msg]}}
|
|
79
|
-
)
|
|
80
|
-
|
|
81
|
-
# Return the text of the first reply
|
|
82
|
-
return result["response_llm"]["replies"][0].text
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|