deepset-mcp 0.0.2rc1__py3-none-any.whl
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.
- deepset_mcp/__init__.py +0 -0
- deepset_mcp/agents/__init__.py +0 -0
- deepset_mcp/agents/debugging/__init__.py +0 -0
- deepset_mcp/agents/debugging/debugging_agent.py +37 -0
- deepset_mcp/agents/debugging/system_prompt.md +214 -0
- deepset_mcp/agents/generalist/__init__.py +0 -0
- deepset_mcp/agents/generalist/generalist_agent.py +38 -0
- deepset_mcp/agents/generalist/system_prompt.md +241 -0
- deepset_mcp/api/README.md +536 -0
- deepset_mcp/api/__init__.py +0 -0
- deepset_mcp/api/client.py +277 -0
- deepset_mcp/api/custom_components/__init__.py +0 -0
- deepset_mcp/api/custom_components/models.py +25 -0
- deepset_mcp/api/custom_components/protocols.py +17 -0
- deepset_mcp/api/custom_components/resource.py +56 -0
- deepset_mcp/api/exceptions.py +70 -0
- deepset_mcp/api/haystack_service/__init__.py +0 -0
- deepset_mcp/api/haystack_service/protocols.py +13 -0
- deepset_mcp/api/haystack_service/resource.py +55 -0
- deepset_mcp/api/indexes/__init__.py +0 -0
- deepset_mcp/api/indexes/models.py +63 -0
- deepset_mcp/api/indexes/protocols.py +53 -0
- deepset_mcp/api/indexes/resource.py +138 -0
- deepset_mcp/api/integrations/__init__.py +1 -0
- deepset_mcp/api/integrations/models.py +49 -0
- deepset_mcp/api/integrations/protocols.py +27 -0
- deepset_mcp/api/integrations/resource.py +57 -0
- deepset_mcp/api/pipeline/__init__.py +17 -0
- deepset_mcp/api/pipeline/log_level.py +9 -0
- deepset_mcp/api/pipeline/models.py +235 -0
- deepset_mcp/api/pipeline/protocols.py +83 -0
- deepset_mcp/api/pipeline/resource.py +378 -0
- deepset_mcp/api/pipeline_template/__init__.py +0 -0
- deepset_mcp/api/pipeline_template/models.py +56 -0
- deepset_mcp/api/pipeline_template/protocols.py +17 -0
- deepset_mcp/api/pipeline_template/resource.py +88 -0
- deepset_mcp/api/protocols.py +122 -0
- deepset_mcp/api/secrets/__init__.py +0 -0
- deepset_mcp/api/secrets/models.py +16 -0
- deepset_mcp/api/secrets/protocols.py +29 -0
- deepset_mcp/api/secrets/resource.py +112 -0
- deepset_mcp/api/shared_models.py +17 -0
- deepset_mcp/api/transport.py +336 -0
- deepset_mcp/api/user/__init__.py +0 -0
- deepset_mcp/api/user/protocols.py +11 -0
- deepset_mcp/api/user/resource.py +38 -0
- deepset_mcp/api/workspace/__init__.py +7 -0
- deepset_mcp/api/workspace/models.py +23 -0
- deepset_mcp/api/workspace/protocols.py +41 -0
- deepset_mcp/api/workspace/resource.py +94 -0
- deepset_mcp/benchmark/README.md +425 -0
- deepset_mcp/benchmark/__init__.py +1 -0
- deepset_mcp/benchmark/agent_configs/debugging_agent.yml +10 -0
- deepset_mcp/benchmark/agent_configs/generalist_agent.yml +6 -0
- deepset_mcp/benchmark/dp_validation_error_analysis/__init__.py +0 -0
- deepset_mcp/benchmark/dp_validation_error_analysis/eda.ipynb +757 -0
- deepset_mcp/benchmark/dp_validation_error_analysis/prepare_interaction_data.ipynb +167 -0
- deepset_mcp/benchmark/dp_validation_error_analysis/preprocessing_utils.py +213 -0
- deepset_mcp/benchmark/runner/__init__.py +0 -0
- deepset_mcp/benchmark/runner/agent_benchmark_runner.py +561 -0
- deepset_mcp/benchmark/runner/agent_loader.py +110 -0
- deepset_mcp/benchmark/runner/cli.py +39 -0
- deepset_mcp/benchmark/runner/cli_agent.py +373 -0
- deepset_mcp/benchmark/runner/cli_index.py +71 -0
- deepset_mcp/benchmark/runner/cli_pipeline.py +73 -0
- deepset_mcp/benchmark/runner/cli_tests.py +226 -0
- deepset_mcp/benchmark/runner/cli_utils.py +61 -0
- deepset_mcp/benchmark/runner/config.py +73 -0
- deepset_mcp/benchmark/runner/config_loader.py +64 -0
- deepset_mcp/benchmark/runner/interactive.py +140 -0
- deepset_mcp/benchmark/runner/models.py +203 -0
- deepset_mcp/benchmark/runner/repl.py +67 -0
- deepset_mcp/benchmark/runner/setup_actions.py +238 -0
- deepset_mcp/benchmark/runner/streaming.py +360 -0
- deepset_mcp/benchmark/runner/teardown_actions.py +196 -0
- deepset_mcp/benchmark/runner/tracing.py +21 -0
- deepset_mcp/benchmark/tasks/chat_rag_answers_wrong_format.yml +16 -0
- deepset_mcp/benchmark/tasks/documents_output_wrong.yml +13 -0
- deepset_mcp/benchmark/tasks/jinja_str_instead_of_complex_type.yml +11 -0
- deepset_mcp/benchmark/tasks/jinja_syntax_error.yml +11 -0
- deepset_mcp/benchmark/tasks/missing_output_mapping.yml +14 -0
- deepset_mcp/benchmark/tasks/no_query_input.yml +13 -0
- deepset_mcp/benchmark/tasks/pipelines/chat_agent_jinja_str.yml +141 -0
- deepset_mcp/benchmark/tasks/pipelines/chat_agent_jinja_syntax.yml +141 -0
- deepset_mcp/benchmark/tasks/pipelines/chat_rag_answers_wrong_format.yml +181 -0
- deepset_mcp/benchmark/tasks/pipelines/chat_rag_missing_output_mapping.yml +189 -0
- deepset_mcp/benchmark/tasks/pipelines/rag_documents_wrong_format.yml +193 -0
- deepset_mcp/benchmark/tasks/pipelines/rag_no_query_input.yml +191 -0
- deepset_mcp/benchmark/tasks/pipelines/standard_index.yml +167 -0
- deepset_mcp/initialize_embedding_model.py +12 -0
- deepset_mcp/main.py +133 -0
- deepset_mcp/prompts/deepset_copilot_prompt.md +271 -0
- deepset_mcp/prompts/deepset_debugging_agent.md +214 -0
- deepset_mcp/store.py +5 -0
- deepset_mcp/tool_factory.py +473 -0
- deepset_mcp/tools/__init__.py +0 -0
- deepset_mcp/tools/custom_components.py +52 -0
- deepset_mcp/tools/doc_search.py +83 -0
- deepset_mcp/tools/haystack_service.py +358 -0
- deepset_mcp/tools/haystack_service_models.py +97 -0
- deepset_mcp/tools/indexes.py +129 -0
- deepset_mcp/tools/model_protocol.py +16 -0
- deepset_mcp/tools/pipeline.py +335 -0
- deepset_mcp/tools/pipeline_template.py +116 -0
- deepset_mcp/tools/secrets.py +45 -0
- deepset_mcp/tools/tokonomics/__init__.py +73 -0
- deepset_mcp/tools/tokonomics/decorators.py +396 -0
- deepset_mcp/tools/tokonomics/explorer.py +347 -0
- deepset_mcp/tools/tokonomics/object_store.py +177 -0
- deepset_mcp/tools/workspace.py +61 -0
- deepset_mcp-0.0.2rc1.dist-info/METADATA +292 -0
- deepset_mcp-0.0.2rc1.dist-info/RECORD +114 -0
- deepset_mcp-0.0.2rc1.dist-info/WHEEL +4 -0
- deepset_mcp-0.0.2rc1.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Async-compatible practical streaming callback for deepset agent responses.
|
|
3
|
+
|
|
4
|
+
Handles text streaming, tool calls, and tool results with nice console formatting.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from haystack.dataclasses.streaming_chunk import StreamingChunk
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.live import Live
|
|
13
|
+
from rich.markdown import Markdown
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class StreamingCallbackManager:
|
|
17
|
+
"""
|
|
18
|
+
Async-compatible callback tailored to your exact streaming structure.
|
|
19
|
+
|
|
20
|
+
Handles the specific patterns from your deepset agent.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self) -> None:
|
|
24
|
+
"""Initialize the streaming callback."""
|
|
25
|
+
self.console = Console()
|
|
26
|
+
self.active_tools: dict[int, dict[str, Any]] = {}
|
|
27
|
+
self.accumulated_text = ""
|
|
28
|
+
self.live_display: Live | None = None
|
|
29
|
+
self.text_started = False
|
|
30
|
+
|
|
31
|
+
async def __call__(self, chunk: StreamingChunk) -> None:
|
|
32
|
+
"""Process each streaming chunk asynchronously."""
|
|
33
|
+
await self._handle_chunk(chunk)
|
|
34
|
+
|
|
35
|
+
async def _handle_chunk(self, chunk: StreamingChunk) -> None:
|
|
36
|
+
"""Handle different types of chunks based on your data structure."""
|
|
37
|
+
meta = chunk.meta
|
|
38
|
+
|
|
39
|
+
# 1. Handle text streaming (like "I'll help you troubleshoot...")
|
|
40
|
+
if self._is_text_delta(meta):
|
|
41
|
+
text = meta["delta"]["text"]
|
|
42
|
+
self.accumulated_text += text
|
|
43
|
+
await self._render_markdown_optimistic()
|
|
44
|
+
|
|
45
|
+
# 2. Handle tool call start (like list_pipelines, get_pipeline)
|
|
46
|
+
elif self._is_tool_start(meta):
|
|
47
|
+
await self._handle_tool_start(meta)
|
|
48
|
+
|
|
49
|
+
# 3. Handle tool arguments streaming (partial JSON)
|
|
50
|
+
elif self._is_tool_args(meta):
|
|
51
|
+
await self._handle_tool_args(meta)
|
|
52
|
+
|
|
53
|
+
# 4. Handle tool results
|
|
54
|
+
elif self._is_tool_result(meta):
|
|
55
|
+
await self._handle_tool_result(meta)
|
|
56
|
+
|
|
57
|
+
# 5. Handle message deltas (usage info, etc.)
|
|
58
|
+
elif self._is_message_delta(meta):
|
|
59
|
+
await self._handle_message_delta(meta)
|
|
60
|
+
|
|
61
|
+
if self._is_finish_event(meta):
|
|
62
|
+
await self._handle_finish_event(meta)
|
|
63
|
+
|
|
64
|
+
async def _render_markdown_optimistic(self) -> None:
|
|
65
|
+
"""Render accumulated text as markdown optimistically."""
|
|
66
|
+
if not self.accumulated_text.strip():
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
# Attempt to render as markdown
|
|
71
|
+
markdown = Markdown(self.accumulated_text)
|
|
72
|
+
|
|
73
|
+
# Start live display if not already started
|
|
74
|
+
if not self.live_display:
|
|
75
|
+
self.live_display = Live(markdown, console=self.console, refresh_per_second=10)
|
|
76
|
+
self.live_display.start()
|
|
77
|
+
self.text_started = True
|
|
78
|
+
else:
|
|
79
|
+
# Update the live display
|
|
80
|
+
self.live_display.update(markdown)
|
|
81
|
+
|
|
82
|
+
except Exception:
|
|
83
|
+
# Fallback to plain text if markdown parsing fails
|
|
84
|
+
if not self.live_display:
|
|
85
|
+
self.live_display = Live(self.accumulated_text, console=self.console, refresh_per_second=10)
|
|
86
|
+
self.live_display.start()
|
|
87
|
+
self.text_started = True
|
|
88
|
+
else:
|
|
89
|
+
self.live_display.update(self.accumulated_text)
|
|
90
|
+
|
|
91
|
+
def _is_text_delta(self, meta: dict[str, Any]) -> bool:
|
|
92
|
+
"""Check if this is a text streaming chunk."""
|
|
93
|
+
return meta.get("type") == "content_block_delta" and meta.get("delta", {}).get("type") == "text_delta"
|
|
94
|
+
|
|
95
|
+
def _is_tool_start(self, meta: dict[str, Any]) -> bool:
|
|
96
|
+
"""Check if this is the start of a tool call."""
|
|
97
|
+
return meta.get("type") == "content_block_start" and meta.get("content_block", {}).get("type") == "tool_use"
|
|
98
|
+
|
|
99
|
+
def _is_tool_args(self, meta: dict[str, Any]) -> bool:
|
|
100
|
+
"""Check if this is tool arguments streaming."""
|
|
101
|
+
return meta.get("type") == "content_block_delta" and meta.get("delta", {}).get("type") == "input_json_delta"
|
|
102
|
+
|
|
103
|
+
def _is_tool_result(self, meta: dict[str, Any]) -> bool:
|
|
104
|
+
"""Check if this is a tool result."""
|
|
105
|
+
return "tool_result" in meta and "tool_call" in meta
|
|
106
|
+
|
|
107
|
+
def _is_message_delta(self, meta: dict[str, Any]) -> bool:
|
|
108
|
+
"""Check if this is a message-level delta."""
|
|
109
|
+
return meta.get("type") == "message_delta"
|
|
110
|
+
|
|
111
|
+
def _is_finish_event(self, meta: dict[str, Any]) -> bool:
|
|
112
|
+
"""Check if this is a finish event."""
|
|
113
|
+
return "stop_reason" in meta.get("delta", {})
|
|
114
|
+
|
|
115
|
+
async def _handle_tool_start(self, meta: dict[str, Any]) -> None:
|
|
116
|
+
"""Handle the start of a tool call."""
|
|
117
|
+
content_block = meta["content_block"]
|
|
118
|
+
tool_name = content_block["name"]
|
|
119
|
+
tool_id = content_block["id"]
|
|
120
|
+
index = meta["index"]
|
|
121
|
+
|
|
122
|
+
# Stop live display if active
|
|
123
|
+
if self.live_display:
|
|
124
|
+
self.live_display.stop()
|
|
125
|
+
self.live_display = None
|
|
126
|
+
|
|
127
|
+
# Store tool state
|
|
128
|
+
self.active_tools[index] = {
|
|
129
|
+
"name": tool_name,
|
|
130
|
+
"id": tool_id,
|
|
131
|
+
"args_json": "",
|
|
132
|
+
"started": True,
|
|
133
|
+
"args_displayed": False,
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
# Display tool call header (text accumulation continues after tools)
|
|
137
|
+
self.console.print() # New line
|
|
138
|
+
self.console.print("┌─ 🔧 Tool Call", style="bold cyan")
|
|
139
|
+
self.console.print(f"│ Name: {tool_name}", style="cyan")
|
|
140
|
+
|
|
141
|
+
async def _handle_tool_args(self, meta: dict[str, Any]) -> None:
|
|
142
|
+
"""Handle streaming tool arguments."""
|
|
143
|
+
index = meta["index"]
|
|
144
|
+
if index not in self.active_tools:
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
partial_json = meta["delta"]["partial_json"]
|
|
148
|
+
self.active_tools[index]["args_json"] += partial_json
|
|
149
|
+
|
|
150
|
+
# Try to show current args when we have complete JSON
|
|
151
|
+
await self._try_display_complete_args(index)
|
|
152
|
+
|
|
153
|
+
async def _try_display_complete_args(self, index: int) -> None:
|
|
154
|
+
"""Try to display complete arguments when JSON is valid."""
|
|
155
|
+
tool = self.active_tools[index]
|
|
156
|
+
|
|
157
|
+
try:
|
|
158
|
+
# Try to parse the current JSON
|
|
159
|
+
if tool["args_json"].strip() and not tool["args_displayed"]:
|
|
160
|
+
args = json.loads(tool["args_json"])
|
|
161
|
+
|
|
162
|
+
# Display arguments in multi-line format
|
|
163
|
+
await self._display_tool_arguments(args)
|
|
164
|
+
tool["args_displayed"] = True
|
|
165
|
+
|
|
166
|
+
except json.JSONDecodeError:
|
|
167
|
+
# Still accumulating JSON, wait for more
|
|
168
|
+
pass
|
|
169
|
+
|
|
170
|
+
async def _display_tool_arguments(self, args: dict[str, Any]) -> None:
|
|
171
|
+
"""Display tool arguments in a pretty multi-line format."""
|
|
172
|
+
if not args:
|
|
173
|
+
self.console.print("│ (no arguments)", style="dim")
|
|
174
|
+
return
|
|
175
|
+
|
|
176
|
+
self.console.print("│ Arguments:", style="cyan")
|
|
177
|
+
|
|
178
|
+
for arg_name, arg_value in args.items():
|
|
179
|
+
self.console.print(f"│ {arg_name}:", style="yellow")
|
|
180
|
+
|
|
181
|
+
# Format the argument value with line limit
|
|
182
|
+
formatted_value = await self._format_argument_value(arg_value, max_lines=5)
|
|
183
|
+
|
|
184
|
+
# Display each line of the value with proper indentation
|
|
185
|
+
for line in formatted_value:
|
|
186
|
+
self.console.print(f"│ {line}", style="white")
|
|
187
|
+
|
|
188
|
+
async def _format_argument_value(self, value: Any, max_lines: int = 5) -> list[str]:
|
|
189
|
+
"""Format an argument value with line limits."""
|
|
190
|
+
if value is None:
|
|
191
|
+
return ["null"]
|
|
192
|
+
|
|
193
|
+
if isinstance(value, bool):
|
|
194
|
+
return [str(value).lower()]
|
|
195
|
+
|
|
196
|
+
if isinstance(value, int | float):
|
|
197
|
+
return [str(value)]
|
|
198
|
+
|
|
199
|
+
if isinstance(value, str):
|
|
200
|
+
# Handle multi-line strings
|
|
201
|
+
lines = value.split("\n")
|
|
202
|
+
|
|
203
|
+
# Limit lines
|
|
204
|
+
display_lines = lines[:max_lines]
|
|
205
|
+
result = []
|
|
206
|
+
|
|
207
|
+
for line in display_lines:
|
|
208
|
+
# Wrap long lines at 60 characters for readability
|
|
209
|
+
if len(line) <= 60:
|
|
210
|
+
result.append(f'"{line}"' if line else '""')
|
|
211
|
+
else:
|
|
212
|
+
result.append(f'"{line[:57]}..."')
|
|
213
|
+
|
|
214
|
+
# Add truncation indicator if needed
|
|
215
|
+
if len(lines) > max_lines:
|
|
216
|
+
result.append(f"... ({len(lines) - max_lines} more lines)")
|
|
217
|
+
|
|
218
|
+
return result
|
|
219
|
+
|
|
220
|
+
if isinstance(value, list | dict):
|
|
221
|
+
# Pretty print complex objects
|
|
222
|
+
try:
|
|
223
|
+
json_str = json.dumps(value, indent=2)
|
|
224
|
+
lines = json_str.split("\n")
|
|
225
|
+
|
|
226
|
+
display_lines = lines[:max_lines]
|
|
227
|
+
if len(lines) > max_lines:
|
|
228
|
+
display_lines.append(f"... ({len(lines) - max_lines} more lines)")
|
|
229
|
+
|
|
230
|
+
return display_lines
|
|
231
|
+
except Exception:
|
|
232
|
+
return [str(value)[:100] + "..." if len(str(value)) > 100 else str(value)]
|
|
233
|
+
|
|
234
|
+
# Fallback for other types
|
|
235
|
+
str_value = str(value)
|
|
236
|
+
if len(str_value) > 60:
|
|
237
|
+
return [str_value[:57] + "..."]
|
|
238
|
+
return [str_value]
|
|
239
|
+
|
|
240
|
+
async def _handle_tool_result(self, meta: dict[str, Any]) -> None:
|
|
241
|
+
"""Handle tool execution results."""
|
|
242
|
+
tool_result = meta["tool_result"]
|
|
243
|
+
|
|
244
|
+
# Close the tool call display
|
|
245
|
+
self.console.print("└─ ✅ Completed", style="green")
|
|
246
|
+
|
|
247
|
+
# Display tool result content (max 10 lines)
|
|
248
|
+
if tool_result:
|
|
249
|
+
await self._display_tool_result(tool_result)
|
|
250
|
+
|
|
251
|
+
async def _display_tool_result(self, tool_result: str, max_lines: int = 10) -> None:
|
|
252
|
+
"""Display tool result with a maximum number of lines."""
|
|
253
|
+
try:
|
|
254
|
+
# Parse the tool result JSON
|
|
255
|
+
if isinstance(tool_result, str):
|
|
256
|
+
result_data = json.loads(tool_result)
|
|
257
|
+
|
|
258
|
+
# Extract the actual content
|
|
259
|
+
content_text = await self._extract_result_content(result_data)
|
|
260
|
+
|
|
261
|
+
if content_text:
|
|
262
|
+
# Split into lines and limit to max_lines
|
|
263
|
+
lines = content_text.split("\n")
|
|
264
|
+
display_lines = lines[:max_lines]
|
|
265
|
+
|
|
266
|
+
# Show the result with indentation
|
|
267
|
+
self.console.print(" ┌─ Result:", style="dim cyan")
|
|
268
|
+
for line in display_lines:
|
|
269
|
+
if line.strip(): # Only show non-empty lines
|
|
270
|
+
self.console.print(f" │ {line}", style="dim")
|
|
271
|
+
|
|
272
|
+
# Show truncation indicator if needed
|
|
273
|
+
if len(lines) > max_lines:
|
|
274
|
+
remaining = len(lines) - max_lines
|
|
275
|
+
self.console.print(f" │ ... ({remaining} more lines)", style="dim yellow")
|
|
276
|
+
|
|
277
|
+
self.console.print(" └─", style="dim cyan")
|
|
278
|
+
else:
|
|
279
|
+
self.console.print(" → Result received", style="dim green")
|
|
280
|
+
|
|
281
|
+
except Exception:
|
|
282
|
+
# Fallback for unparseable results
|
|
283
|
+
self.console.print(" → Result received", style="dim green")
|
|
284
|
+
|
|
285
|
+
async def _extract_result_content(self, result_data: dict[str, Any]) -> str | None:
|
|
286
|
+
"""Extract meaningful content from tool result data."""
|
|
287
|
+
try:
|
|
288
|
+
# Handle the specific structure from your deepset results
|
|
289
|
+
if isinstance(result_data, dict):
|
|
290
|
+
content = result_data.get("content", [])
|
|
291
|
+
|
|
292
|
+
if isinstance(content, list) and content:
|
|
293
|
+
# Get the first content item
|
|
294
|
+
first_content = content[0]
|
|
295
|
+
|
|
296
|
+
if isinstance(first_content, dict):
|
|
297
|
+
text_content = first_content.get("text", "")
|
|
298
|
+
|
|
299
|
+
# Handle nested JSON strings (like "@obj_001 → deepset_mcp...")
|
|
300
|
+
if text_content.startswith('"') and text_content.endswith('"'):
|
|
301
|
+
# Parse the inner JSON string
|
|
302
|
+
inner_content = json.loads(text_content)
|
|
303
|
+
formatted = await self._format_deepset_content(str(inner_content))
|
|
304
|
+
return formatted
|
|
305
|
+
else:
|
|
306
|
+
return str(text_content) if text_content else None
|
|
307
|
+
|
|
308
|
+
return str(result_data) if result_data else None
|
|
309
|
+
|
|
310
|
+
except Exception:
|
|
311
|
+
return str(result_data) if result_data else None
|
|
312
|
+
|
|
313
|
+
async def _format_deepset_content(self, content: str) -> str:
|
|
314
|
+
"""Format deepset-specific content for better readability."""
|
|
315
|
+
try:
|
|
316
|
+
# Handle content like "@obj_001 → deepset_mcp.api.pipeline.models.PipelineList..."
|
|
317
|
+
if " → " in content:
|
|
318
|
+
parts = content.split(" → ", 1)
|
|
319
|
+
if len(parts) == 2:
|
|
320
|
+
obj_id, obj_content = parts
|
|
321
|
+
|
|
322
|
+
# Clean up the object content for better display
|
|
323
|
+
formatted = obj_content.replace("\\n", "\n").replace("\\\\", "\\")
|
|
324
|
+
|
|
325
|
+
# Add object ID as header
|
|
326
|
+
return f"Object: {obj_id}\n{formatted}"
|
|
327
|
+
|
|
328
|
+
# Fallback: clean up escape sequences
|
|
329
|
+
return content.replace("\\n", "\n").replace("\\\\", "\\")
|
|
330
|
+
|
|
331
|
+
except Exception:
|
|
332
|
+
return content
|
|
333
|
+
|
|
334
|
+
async def _handle_message_delta(self, meta: dict[str, Any]) -> None:
|
|
335
|
+
"""Handle message-level information."""
|
|
336
|
+
delta = meta.get("delta", {})
|
|
337
|
+
|
|
338
|
+
# Could show usage info if desired
|
|
339
|
+
if "usage" in delta:
|
|
340
|
+
usage = delta["usage"]
|
|
341
|
+
if usage.get("output_tokens"):
|
|
342
|
+
# Optionally show token usage
|
|
343
|
+
pass
|
|
344
|
+
|
|
345
|
+
async def _handle_finish_event(self, meta: dict[str, Any]) -> None:
|
|
346
|
+
"""Handle finish events."""
|
|
347
|
+
finish_reason = meta.get("delta", {}).get("stop_reason")
|
|
348
|
+
if finish_reason == "tool_call_results":
|
|
349
|
+
# Clean up after tool calls
|
|
350
|
+
self.active_tools.clear()
|
|
351
|
+
self.console.print() # Extra line after tools
|
|
352
|
+
elif finish_reason == "end_turn":
|
|
353
|
+
# Stop live display and reset for next interaction
|
|
354
|
+
if self.live_display:
|
|
355
|
+
self.live_display.stop()
|
|
356
|
+
self.live_display = None
|
|
357
|
+
# Ensure cursor is on a new line for the next prompt
|
|
358
|
+
self.console.print()
|
|
359
|
+
self.accumulated_text = ""
|
|
360
|
+
self.text_started = False
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import os
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from deepset_mcp.api.client import AsyncDeepsetClient
|
|
6
|
+
from deepset_mcp.benchmark.runner.models import TestCaseConfig
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _get_api_key(explicit_key: str | None) -> str:
|
|
10
|
+
"""
|
|
11
|
+
Return whichever API key to use: explicit_key takes precedence, otherwise read DP_API_KEY from the environment.
|
|
12
|
+
|
|
13
|
+
If still missing, raise ValueError.
|
|
14
|
+
"""
|
|
15
|
+
if explicit_key:
|
|
16
|
+
return explicit_key
|
|
17
|
+
env_key = os.getenv("DP_API_KEY")
|
|
18
|
+
if not env_key:
|
|
19
|
+
raise ValueError("No API key provided: pass --api-key or set DP_API_KEY in env.")
|
|
20
|
+
return env_key
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
24
|
+
# 1) LOW-LEVEL: "teardown_pipeline" and "teardown_index" using AsyncDeepsetClient as a context manager
|
|
25
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
async def teardown_pipeline_async(
|
|
29
|
+
*,
|
|
30
|
+
pipeline_name: str,
|
|
31
|
+
workspace_name: str,
|
|
32
|
+
api_key: str | None = None,
|
|
33
|
+
) -> None:
|
|
34
|
+
"""
|
|
35
|
+
Delete a pipeline in the given workspace.
|
|
36
|
+
|
|
37
|
+
Uses DP_API_KEY or explicit api_key.
|
|
38
|
+
"""
|
|
39
|
+
key_to_use = _get_api_key(api_key)
|
|
40
|
+
async with AsyncDeepsetClient(api_key=key_to_use) as client:
|
|
41
|
+
await client.pipelines(workspace=workspace_name).delete(pipeline_name)
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
async def teardown_index_async(
|
|
46
|
+
*,
|
|
47
|
+
index_name: str,
|
|
48
|
+
workspace_name: str,
|
|
49
|
+
api_key: str | None = None,
|
|
50
|
+
) -> None:
|
|
51
|
+
"""
|
|
52
|
+
Delete an index in the given workspace.
|
|
53
|
+
|
|
54
|
+
Uses DP_API_KEY or explicit api_key.
|
|
55
|
+
"""
|
|
56
|
+
key_to_use = _get_api_key(api_key)
|
|
57
|
+
async with AsyncDeepsetClient(api_key=key_to_use) as client:
|
|
58
|
+
await client.indexes(workspace=workspace_name).delete(index_name)
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
63
|
+
# 2) MID-LEVEL: teardown a full test-case (pipeline + index if present)
|
|
64
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
async def teardown_test_case_async(
|
|
68
|
+
*,
|
|
69
|
+
test_cfg: TestCaseConfig,
|
|
70
|
+
workspace_name: str,
|
|
71
|
+
api_key: str | None = None,
|
|
72
|
+
) -> None:
|
|
73
|
+
"""
|
|
74
|
+
Given a TestCaseConfig, delete the index and the pipeline in the specified workspace.
|
|
75
|
+
|
|
76
|
+
Uses DP_API_KEY or explicit api_key.
|
|
77
|
+
"""
|
|
78
|
+
# 1) If there's a "query pipeline" to delete:
|
|
79
|
+
if test_cfg.query_yaml:
|
|
80
|
+
assert test_cfg.query_name is not None # already validated by Pydantic model; added to satisfy mypy
|
|
81
|
+
await teardown_pipeline_async(
|
|
82
|
+
pipeline_name=test_cfg.query_name,
|
|
83
|
+
workspace_name=workspace_name,
|
|
84
|
+
api_key=api_key,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# 2) If there's an index to delete:
|
|
88
|
+
if test_cfg.index_yaml:
|
|
89
|
+
assert test_cfg.index_name is not None # already validated by Pydantic model; added to satisfy mypy
|
|
90
|
+
await teardown_index_async(
|
|
91
|
+
index_name=test_cfg.index_name,
|
|
92
|
+
workspace_name=workspace_name,
|
|
93
|
+
api_key=api_key,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
100
|
+
# 3) HIGH-LEVEL: parallel "teardown all" with configurable concurrency
|
|
101
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
async def teardown_all_async(
|
|
105
|
+
*,
|
|
106
|
+
test_cfgs: list[TestCaseConfig],
|
|
107
|
+
workspace_name: str,
|
|
108
|
+
api_key: str | None = None,
|
|
109
|
+
concurrency: int = 5,
|
|
110
|
+
) -> None:
|
|
111
|
+
"""
|
|
112
|
+
Given a list of TestCaseConfig, delete all indexes and pipelines in parallel.
|
|
113
|
+
|
|
114
|
+
Uses DP_API_KEY or explicit api_key.
|
|
115
|
+
"""
|
|
116
|
+
semaphore = asyncio.Semaphore(concurrency)
|
|
117
|
+
tasks: list[asyncio.Task[Any]] = []
|
|
118
|
+
|
|
119
|
+
async def sem_task(cfg: TestCaseConfig) -> str:
|
|
120
|
+
async with semaphore:
|
|
121
|
+
await teardown_test_case_async(test_cfg=cfg, workspace_name=workspace_name, api_key=api_key)
|
|
122
|
+
return cfg.name
|
|
123
|
+
|
|
124
|
+
for cfg in test_cfgs:
|
|
125
|
+
tasks.append(asyncio.create_task(sem_task(cfg)))
|
|
126
|
+
|
|
127
|
+
done, _ = await asyncio.wait(tasks, return_when=asyncio.ALL_COMPLETED)
|
|
128
|
+
errors: list[Exception] = []
|
|
129
|
+
for t in done:
|
|
130
|
+
if t.exception():
|
|
131
|
+
errors.append(t.exception()) # type: ignore
|
|
132
|
+
|
|
133
|
+
if errors:
|
|
134
|
+
raise RuntimeError(f"Errors during teardown: {errors}")
|
|
135
|
+
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
140
|
+
# 4) SYNC WRAPPERS for all of the above (now accept api_key)
|
|
141
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def teardown_pipeline(
|
|
145
|
+
*,
|
|
146
|
+
pipeline_name: str,
|
|
147
|
+
workspace_name: str,
|
|
148
|
+
api_key: str | None = None,
|
|
149
|
+
) -> None:
|
|
150
|
+
"""Synchronous wrapper for teardown_pipeline_async. Blocks until the pipeline is deleted."""
|
|
151
|
+
return asyncio.run(
|
|
152
|
+
teardown_pipeline_async(
|
|
153
|
+
pipeline_name=pipeline_name,
|
|
154
|
+
workspace_name=workspace_name,
|
|
155
|
+
api_key=api_key,
|
|
156
|
+
)
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def teardown_index(
|
|
161
|
+
*,
|
|
162
|
+
index_name: str,
|
|
163
|
+
workspace_name: str,
|
|
164
|
+
api_key: str | None = None,
|
|
165
|
+
) -> None:
|
|
166
|
+
"""Synchronous wrapper for teardown_index_async. Blocks until the index is deleted."""
|
|
167
|
+
return asyncio.run(
|
|
168
|
+
teardown_index_async(
|
|
169
|
+
index_name=index_name,
|
|
170
|
+
workspace_name=workspace_name,
|
|
171
|
+
api_key=api_key,
|
|
172
|
+
)
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def teardown_test_case(
|
|
177
|
+
*,
|
|
178
|
+
test_cfg: TestCaseConfig,
|
|
179
|
+
workspace_name: str,
|
|
180
|
+
api_key: str | None = None,
|
|
181
|
+
) -> None:
|
|
182
|
+
"""Synchronous wrapper: blocks until both pipeline and index (if any) are deleted."""
|
|
183
|
+
return asyncio.run(teardown_test_case_async(test_cfg=test_cfg, workspace_name=workspace_name, api_key=api_key))
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def teardown_all(
|
|
187
|
+
*,
|
|
188
|
+
test_cfgs: list[TestCaseConfig],
|
|
189
|
+
workspace_name: str,
|
|
190
|
+
api_key: str | None = None,
|
|
191
|
+
concurrency: int = 5,
|
|
192
|
+
) -> None:
|
|
193
|
+
"""Synchronous wrapper for teardown_all_async. Blocks until all test-cases are deleted."""
|
|
194
|
+
return asyncio.run(
|
|
195
|
+
teardown_all_async(test_cfgs=test_cfgs, workspace_name=workspace_name, api_key=api_key, concurrency=concurrency)
|
|
196
|
+
)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from haystack.tracing.tracer import enable_tracing as haystack_enable_tracing, tracer
|
|
2
|
+
from haystack_integrations.tracing.langfuse import LangfuseTracer
|
|
3
|
+
from langfuse import Langfuse
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def enable_tracing(
|
|
7
|
+
secret_key: str,
|
|
8
|
+
public_key: str,
|
|
9
|
+
name: str,
|
|
10
|
+
) -> None:
|
|
11
|
+
"""Enables tracing with langfuse."""
|
|
12
|
+
resolved_langfuse_client_kwargs = {
|
|
13
|
+
"secret_key": secret_key,
|
|
14
|
+
"public_key": public_key,
|
|
15
|
+
}
|
|
16
|
+
tracer.is_content_tracing_enabled = True
|
|
17
|
+
langfuse_tracer = LangfuseTracer(
|
|
18
|
+
tracer=Langfuse(**resolved_langfuse_client_kwargs),
|
|
19
|
+
name=name,
|
|
20
|
+
)
|
|
21
|
+
haystack_enable_tracing(langfuse_tracer)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
name: "chat_rag_answers_wrong_format"
|
|
2
|
+
objective: "Add an AnswerBuilder and connect it to the answers output."
|
|
3
|
+
prompt: "Can you check why my chat rag pipeline doesn't work."
|
|
4
|
+
query_yaml: "pipelines/chat_rag_answers_wrong_format.yml"
|
|
5
|
+
query_name: "rag-chat-test"
|
|
6
|
+
index_yaml: "pipelines/standard_index.yml"
|
|
7
|
+
index_name: "standard-index"
|
|
8
|
+
tags:
|
|
9
|
+
- "debug"
|
|
10
|
+
- "pipeline-outputs"
|
|
11
|
+
judge_prompt: |
|
|
12
|
+
The Agent has:
|
|
13
|
+
- successfully added an AnswerBuilder
|
|
14
|
+
- connected the qa_llm.replies output to AnswerBuilder.replies
|
|
15
|
+
- connected the query input to the AnswerBuilder
|
|
16
|
+
- added the output of AnswerBuilder as the overall "answers" output of the pipeline
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
name: "documents_output_wrong"
|
|
2
|
+
objective: "Connect the retrieved documents to the documents output."
|
|
3
|
+
prompt: "Can you check why my rag pipeline doesn't work."
|
|
4
|
+
query_yaml: "pipelines/rag_documents_wrong_format.yml"
|
|
5
|
+
query_name: "rag"
|
|
6
|
+
index_yaml: "pipelines/standard_index.yml"
|
|
7
|
+
index_name: "standard-index"
|
|
8
|
+
tags:
|
|
9
|
+
- "debug"
|
|
10
|
+
- "pipeline-outputs"
|
|
11
|
+
judge_prompt: |
|
|
12
|
+
The Agent has:
|
|
13
|
+
- successfully connected retrieved documents to the document output
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
name: "jinja_str_instead_of_complex_type"
|
|
2
|
+
objective: "Add parentheses after the filter in the OutputAdapter to get correct attribute access."
|
|
3
|
+
prompt: "Can you check why my chat agent pipeline doesn't work."
|
|
4
|
+
query_yaml: "pipelines/chat_agent_jinja_syntax.yml"
|
|
5
|
+
query_name: "chat-agent"
|
|
6
|
+
tags:
|
|
7
|
+
- "debug"
|
|
8
|
+
- "jinja"
|
|
9
|
+
judge_prompt: |
|
|
10
|
+
The Agent has:
|
|
11
|
+
- added parentheses around messages|last so that attribute access works correctly
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
name: "jinja_syntax_error"
|
|
2
|
+
objective: "Add double braces to the OutputAdapter to fix the syntax error."
|
|
3
|
+
prompt: "Can you check why my chat agent pipeline doesn't work."
|
|
4
|
+
query_yaml: "pipelines/chat_agent_jinja_str.yml"
|
|
5
|
+
query_name: "chat-agent"
|
|
6
|
+
tags:
|
|
7
|
+
- "debug"
|
|
8
|
+
- "jinja"
|
|
9
|
+
judge_prompt: |
|
|
10
|
+
The Agent has:
|
|
11
|
+
- successfully fixed the template on the OutputAdapter to not contain additional whitespace
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
name: "missing_output_mapping"
|
|
2
|
+
objective: "Add pipeline inputs and outputs."
|
|
3
|
+
prompt: "Can you check why my chat rag pipeline doesn't work."
|
|
4
|
+
query_yaml: "pipelines/chat_rag_missing_output_mapping.yml"
|
|
5
|
+
query_name: "rag-chat-test"
|
|
6
|
+
index_yaml: "pipelines/standard_index.yml"
|
|
7
|
+
index_name: "standard-index"
|
|
8
|
+
tags:
|
|
9
|
+
- "debug"
|
|
10
|
+
- "pipeline-outputs"
|
|
11
|
+
judge_prompt: |
|
|
12
|
+
The Agent has:
|
|
13
|
+
- successfully added an outputs key
|
|
14
|
+
- connected the document and answer outputs
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
name: "no_query_input"
|
|
2
|
+
objective: "Connect a query inputs to all relevant components."
|
|
3
|
+
prompt: "Can you check why my rag pipeline doesn't work."
|
|
4
|
+
query_yaml: "pipelines/rag_no_query_input.yml"
|
|
5
|
+
query_name: "rag"
|
|
6
|
+
index_yaml: "pipelines/standard_index.yml"
|
|
7
|
+
index_name: "standard-index"
|
|
8
|
+
tags:
|
|
9
|
+
- "debug"
|
|
10
|
+
- "pipeline-outputs"
|
|
11
|
+
judge_prompt: |
|
|
12
|
+
The Agent has:
|
|
13
|
+
- successfully connected a query input to all retrievers, ranker and Prompt- and AnswerBuilder
|