deepset-mcp 0.0.2__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.2.dist-info/METADATA +288 -0
- deepset_mcp-0.0.2.dist-info/RECORD +114 -0
- deepset_mcp-0.0.2.dist-info/WHEEL +4 -0
- deepset_mcp-0.0.2.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Rich Explorer for Object Exploration and Rendering.
|
|
3
|
+
|
|
4
|
+
Presents Python objects in various Rich formats and supports basic
|
|
5
|
+
navigation, searching, and slicing.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from glom import GlomError, Path, T, glom
|
|
14
|
+
from rich.console import Console
|
|
15
|
+
from rich.pretty import Pretty
|
|
16
|
+
|
|
17
|
+
from .object_store import ObjectRef, ObjectStore
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class RichExplorer:
|
|
21
|
+
"""Presents Python objects in various Rich formats with navigation support.
|
|
22
|
+
|
|
23
|
+
:param store: Object store used for lookups.
|
|
24
|
+
:param max_items: Maximum number of items to show for lists and nested collections (default: 20).
|
|
25
|
+
Note: All keys are shown for top-level dicts.
|
|
26
|
+
:param max_string_length: Maximum string length before truncation (default: 300).
|
|
27
|
+
:param max_depth: Maximum depth for object representation (default: 3).
|
|
28
|
+
:param max_search_matches: Maximum number of search matches to display (default: 10).
|
|
29
|
+
:param search_context_length: Number of characters to show around search matches (default: 150).
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
store: ObjectStore,
|
|
35
|
+
max_items: int = 25,
|
|
36
|
+
max_string_length: int = 300,
|
|
37
|
+
max_depth: int = 4,
|
|
38
|
+
max_search_matches: int = 10,
|
|
39
|
+
search_context_length: int = 150,
|
|
40
|
+
) -> None:
|
|
41
|
+
"""Initialize the RichExplorer with storage and configuration options."""
|
|
42
|
+
self.store = store
|
|
43
|
+
self.console = Console(force_terminal=False, width=120)
|
|
44
|
+
|
|
45
|
+
# Display limits
|
|
46
|
+
self.max_items = max_items
|
|
47
|
+
self.max_string_length = max_string_length
|
|
48
|
+
self.max_depth = max_depth
|
|
49
|
+
|
|
50
|
+
# Search configuration
|
|
51
|
+
self.max_search_matches = max_search_matches
|
|
52
|
+
self.search_context_length = search_context_length
|
|
53
|
+
|
|
54
|
+
# Validation pattern for allowed attributes
|
|
55
|
+
self.allowed_attr_regex = re.compile(r"[A-Za-z][A-Za-z0-9_]*\Z")
|
|
56
|
+
|
|
57
|
+
def explore(self, obj_id: str, path: str = "") -> str:
|
|
58
|
+
"""Return a string preview of the requested object.
|
|
59
|
+
|
|
60
|
+
:param obj_id: Identifier obtained from the store.
|
|
61
|
+
:param path: Navigation path using ``.`` or ``[...]`` notation (e.g. ``@obj_001.path.to.attribute``).
|
|
62
|
+
:return: String representation of the object.
|
|
63
|
+
"""
|
|
64
|
+
obj = self._get_object_at_path(obj_id, path)
|
|
65
|
+
|
|
66
|
+
# Generate header and body
|
|
67
|
+
header = self._make_header(obj_id, path, obj)
|
|
68
|
+
|
|
69
|
+
# We want the full length str if the (nested) object is a string
|
|
70
|
+
if isinstance(obj, str):
|
|
71
|
+
body = obj
|
|
72
|
+
else:
|
|
73
|
+
body = self._get_pretty_repr(obj)
|
|
74
|
+
|
|
75
|
+
return f"{header}\n\n{body}"
|
|
76
|
+
|
|
77
|
+
def search(self, obj_id: str, pattern: str, path: str = "", case_sensitive: bool = False) -> str:
|
|
78
|
+
"""Search for a pattern within a string object.
|
|
79
|
+
|
|
80
|
+
:param obj_id: Identifier obtained from the store.
|
|
81
|
+
:param pattern: Regular expression pattern to search for.
|
|
82
|
+
:param path: Navigation path to search within (optional).
|
|
83
|
+
:param case_sensitive: Whether search should be case sensitive.
|
|
84
|
+
:return: Search results as formatted string.
|
|
85
|
+
"""
|
|
86
|
+
obj = self._get_object_at_path(obj_id, path)
|
|
87
|
+
|
|
88
|
+
# Generate header
|
|
89
|
+
header = self._make_header(obj_id, path, obj)
|
|
90
|
+
|
|
91
|
+
# Only allow search on strings
|
|
92
|
+
if not isinstance(obj, str):
|
|
93
|
+
return f"{header}\n\nSearch is only supported on string objects. Found {type(obj).__name__} at path."
|
|
94
|
+
|
|
95
|
+
# Search the string
|
|
96
|
+
flags = 0 if case_sensitive else re.IGNORECASE
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
matches = list(re.finditer(pattern, obj, flags))
|
|
100
|
+
except re.error as e:
|
|
101
|
+
return f"{header}\n\nInvalid regex pattern: {e}"
|
|
102
|
+
|
|
103
|
+
if not matches:
|
|
104
|
+
return f"{header}\n\nNo matches found for pattern '{pattern}'"
|
|
105
|
+
|
|
106
|
+
# Format results
|
|
107
|
+
result = [f"Found {len(matches)} matches for pattern '{pattern}':", ""]
|
|
108
|
+
|
|
109
|
+
# Show limited number of matches
|
|
110
|
+
for i, match in enumerate(matches[: self.max_search_matches]):
|
|
111
|
+
start, end = match.span()
|
|
112
|
+
context_start = max(0, start - self.search_context_length)
|
|
113
|
+
context_end = min(len(obj), end + self.search_context_length)
|
|
114
|
+
context = obj[context_start:context_end]
|
|
115
|
+
|
|
116
|
+
# Highlight the match
|
|
117
|
+
match_in_context = start - context_start
|
|
118
|
+
highlighted = (
|
|
119
|
+
context[:match_in_context]
|
|
120
|
+
+ f"[{context[match_in_context : match_in_context + (end - start)]}]"
|
|
121
|
+
+ context[match_in_context + (end - start) :]
|
|
122
|
+
)
|
|
123
|
+
result.append(f"Match {i + 1}: ...{highlighted}...")
|
|
124
|
+
|
|
125
|
+
if len(matches) > self.max_search_matches:
|
|
126
|
+
result.append(f"\n... and {len(matches) - self.max_search_matches} more matches")
|
|
127
|
+
|
|
128
|
+
return f"{header}\n\n" + "\n".join(result)
|
|
129
|
+
|
|
130
|
+
def slice(self, obj_id: str, start: int = 0, end: int | None = None, path: str = "") -> str:
|
|
131
|
+
"""Extract a slice from a string or list object.
|
|
132
|
+
|
|
133
|
+
:param obj_id: Identifier of the object.
|
|
134
|
+
:param start: Start index for slicing.
|
|
135
|
+
:param end: End index for slicing (None for end of sequence).
|
|
136
|
+
:param path: Navigation path to object to slice (optional).
|
|
137
|
+
:return: String representation of the slice.
|
|
138
|
+
"""
|
|
139
|
+
obj = self._get_object_at_path(obj_id, path)
|
|
140
|
+
|
|
141
|
+
# Generate header
|
|
142
|
+
header = self._make_header(obj_id, path, obj)
|
|
143
|
+
|
|
144
|
+
# Handle string slicing
|
|
145
|
+
if isinstance(obj, str):
|
|
146
|
+
sliced: str | list[Any] | tuple[Any] = obj[start:end]
|
|
147
|
+
actual_end = end if end is not None else len(obj)
|
|
148
|
+
body = f"String slice [{start}:{actual_end}] of length {len(sliced)}:\n\n{sliced}"
|
|
149
|
+
return f"{header}\n\n{body}"
|
|
150
|
+
|
|
151
|
+
# Handle list/tuple slicing
|
|
152
|
+
elif isinstance(obj, list | tuple):
|
|
153
|
+
sliced = obj[start:end]
|
|
154
|
+
actual_end = end if end is not None else len(obj)
|
|
155
|
+
|
|
156
|
+
# Use Pretty to render the sliced list with current settings
|
|
157
|
+
with self.console.capture() as cap:
|
|
158
|
+
self.console.print(
|
|
159
|
+
Pretty(
|
|
160
|
+
sliced,
|
|
161
|
+
max_depth=self.max_depth,
|
|
162
|
+
max_length=None, # Show all items in the slice
|
|
163
|
+
max_string=self.max_string_length,
|
|
164
|
+
overflow="ellipsis",
|
|
165
|
+
)
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
type_name = type(obj).__name__
|
|
169
|
+
body = (
|
|
170
|
+
f"{type_name.capitalize()} slice [{start}:{actual_end}] "
|
|
171
|
+
f"(showing {len(sliced)} of {len(obj)} items):\n\n"
|
|
172
|
+
f"{cap.get().rstrip()}"
|
|
173
|
+
)
|
|
174
|
+
return f"{header}\n\n{body}"
|
|
175
|
+
|
|
176
|
+
else:
|
|
177
|
+
return f"{header}\n\nObject of type {type(obj).__name__} does not support slicing"
|
|
178
|
+
|
|
179
|
+
def _get_object_at_path(self, obj_id: str, path: str) -> Any:
|
|
180
|
+
"""Get object from store and navigate to path if provided.
|
|
181
|
+
|
|
182
|
+
:param obj_id: Identifier obtained from the store.
|
|
183
|
+
:param path: Navigation path (optional).
|
|
184
|
+
:return: Object at path or error string.
|
|
185
|
+
"""
|
|
186
|
+
ref = ObjectRef.parse(obj_id)
|
|
187
|
+
# We accept @obj_001 as well as obj_001
|
|
188
|
+
if ref is None:
|
|
189
|
+
resolved_obj_id = obj_id
|
|
190
|
+
else:
|
|
191
|
+
resolved_obj_id = ref.obj_id
|
|
192
|
+
|
|
193
|
+
obj = self.store.get(resolved_obj_id)
|
|
194
|
+
if obj is None:
|
|
195
|
+
raise ValueError(f"Object {obj_id} not found or expired.")
|
|
196
|
+
|
|
197
|
+
if path:
|
|
198
|
+
self._validate_path(path)
|
|
199
|
+
try:
|
|
200
|
+
obj = glom(obj, self._parse_path(path))
|
|
201
|
+
except GlomError as e:
|
|
202
|
+
raise ValueError(f"Object '{obj_id}' does not have a value at path '{path}'.") from e
|
|
203
|
+
|
|
204
|
+
return obj
|
|
205
|
+
|
|
206
|
+
def _validate_path(self, path: str) -> None:
|
|
207
|
+
"""Ensure every attribute component matches the allow-list regex.
|
|
208
|
+
|
|
209
|
+
:param path: Path string to validate.
|
|
210
|
+
:raises ValueError: If path contains disallowed attributes.
|
|
211
|
+
"""
|
|
212
|
+
for part in re.split(r"[.\[\]]+", path):
|
|
213
|
+
if not part or part.isdigit():
|
|
214
|
+
continue
|
|
215
|
+
# Strip quotes for string keys
|
|
216
|
+
part = part.strip("\"'")
|
|
217
|
+
if not self.allowed_attr_regex.match(part):
|
|
218
|
+
raise ValueError(f"Access to attribute '{part}' is not permitted")
|
|
219
|
+
|
|
220
|
+
def _parse_path(self, path: str) -> Any:
|
|
221
|
+
"""Parse a path string into a glom spec.
|
|
222
|
+
|
|
223
|
+
:param path: Path string in dot/bracket notation.
|
|
224
|
+
:return: Glom spec for navigation.
|
|
225
|
+
"""
|
|
226
|
+
if not path:
|
|
227
|
+
return T
|
|
228
|
+
|
|
229
|
+
parts: list[Any] = []
|
|
230
|
+
current = ""
|
|
231
|
+
in_brackets = False
|
|
232
|
+
|
|
233
|
+
for char in path:
|
|
234
|
+
if char == "[":
|
|
235
|
+
if current:
|
|
236
|
+
parts.append(current)
|
|
237
|
+
current = ""
|
|
238
|
+
in_brackets = True
|
|
239
|
+
elif char == "]":
|
|
240
|
+
if current:
|
|
241
|
+
# Try to parse as int for list indices
|
|
242
|
+
try:
|
|
243
|
+
parts.append(int(current))
|
|
244
|
+
except ValueError:
|
|
245
|
+
# String key for dicts
|
|
246
|
+
parts.append(current.strip("\"'"))
|
|
247
|
+
current = ""
|
|
248
|
+
in_brackets = False
|
|
249
|
+
elif char == "." and not in_brackets:
|
|
250
|
+
if current:
|
|
251
|
+
parts.append(current)
|
|
252
|
+
current = ""
|
|
253
|
+
else:
|
|
254
|
+
current += char
|
|
255
|
+
|
|
256
|
+
if current:
|
|
257
|
+
parts.append(current)
|
|
258
|
+
|
|
259
|
+
return Path(*parts) if len(parts) > 1 else parts[0]
|
|
260
|
+
|
|
261
|
+
def _make_header(self, obj_id: str, path: str, obj: Any) -> str:
|
|
262
|
+
"""Create a header showing object info.
|
|
263
|
+
|
|
264
|
+
:param obj_id: Object identifier.
|
|
265
|
+
:param path: Navigation path.
|
|
266
|
+
:param obj: The object being displayed.
|
|
267
|
+
:return: Formatted header string.
|
|
268
|
+
"""
|
|
269
|
+
type_name = type(obj).__name__
|
|
270
|
+
if hasattr(obj, "__module__") and obj.__module__ not in ("builtins", "__main__"):
|
|
271
|
+
type_name = f"{obj.__module__}.{type_name}"
|
|
272
|
+
|
|
273
|
+
location = f"@{obj_id}" + (f".{path}" if path else "")
|
|
274
|
+
|
|
275
|
+
# Add size info for sized objects
|
|
276
|
+
size_info = ""
|
|
277
|
+
if hasattr(obj, "__len__"):
|
|
278
|
+
try:
|
|
279
|
+
size_info = f" (length: {len(obj)})"
|
|
280
|
+
except Exception:
|
|
281
|
+
pass
|
|
282
|
+
|
|
283
|
+
return f"{location} → {type_name}{size_info}"
|
|
284
|
+
|
|
285
|
+
def _get_pretty_repr(self, obj: Any) -> str:
|
|
286
|
+
"""Get Rich pretty representation of object.
|
|
287
|
+
|
|
288
|
+
:param obj: Object to represent.
|
|
289
|
+
:return: String representation using Rich Pretty.
|
|
290
|
+
"""
|
|
291
|
+
# Special handling for top-level dicts to show all keys
|
|
292
|
+
if isinstance(obj, dict):
|
|
293
|
+
if not obj:
|
|
294
|
+
return "{}"
|
|
295
|
+
|
|
296
|
+
# Pretty print each value separately with max_items applied
|
|
297
|
+
result_parts = ["{"]
|
|
298
|
+
|
|
299
|
+
for key, value in obj.items():
|
|
300
|
+
# Pretty print the key
|
|
301
|
+
with self.console.capture() as key_cap:
|
|
302
|
+
self.console.print(Pretty(key), end="")
|
|
303
|
+
key_str = key_cap.get().rstrip()
|
|
304
|
+
|
|
305
|
+
# Pretty print the value with limits applied
|
|
306
|
+
with self.console.capture() as val_cap:
|
|
307
|
+
self.console.print(
|
|
308
|
+
Pretty(
|
|
309
|
+
value,
|
|
310
|
+
max_depth=self.max_depth - 1, # Reduce depth since we're already one level in
|
|
311
|
+
max_length=self.max_items,
|
|
312
|
+
max_string=self.max_string_length,
|
|
313
|
+
expand_all=True,
|
|
314
|
+
overflow="ellipsis",
|
|
315
|
+
),
|
|
316
|
+
end="",
|
|
317
|
+
)
|
|
318
|
+
val_str = val_cap.get().rstrip()
|
|
319
|
+
|
|
320
|
+
# Handle multiline values
|
|
321
|
+
if "\n" in val_str:
|
|
322
|
+
# Indent continuation lines
|
|
323
|
+
lines = val_str.split("\n")
|
|
324
|
+
val_str = lines[0] + "\n" + "\n".join(" " + line for line in lines[1:])
|
|
325
|
+
|
|
326
|
+
result_parts.append(f" {key_str}: {val_str},")
|
|
327
|
+
|
|
328
|
+
# Remove trailing comma from last item
|
|
329
|
+
if result_parts[-1].endswith(","):
|
|
330
|
+
result_parts[-1] = result_parts[-1][:-1]
|
|
331
|
+
|
|
332
|
+
result_parts.append("}")
|
|
333
|
+
return "\n".join(result_parts)
|
|
334
|
+
|
|
335
|
+
# Regular pretty print for non-dict objects
|
|
336
|
+
with self.console.capture() as cap:
|
|
337
|
+
self.console.print(
|
|
338
|
+
Pretty(
|
|
339
|
+
obj,
|
|
340
|
+
max_depth=self.max_depth,
|
|
341
|
+
max_length=self.max_items,
|
|
342
|
+
max_string=self.max_string_length,
|
|
343
|
+
expand_all=True,
|
|
344
|
+
overflow="ellipsis",
|
|
345
|
+
)
|
|
346
|
+
)
|
|
347
|
+
return cap.get().rstrip()
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tokonomics: Explorable and Referenceable Tools.
|
|
3
|
+
|
|
4
|
+
===============================================
|
|
5
|
+
|
|
6
|
+
A library that equips LLM‑agents with:
|
|
7
|
+
|
|
8
|
+
* TTL‑based object storage
|
|
9
|
+
* Rich object exploration & reference passing
|
|
10
|
+
* Smart decorators for explorable outputs and referenceable inputs
|
|
11
|
+
* Strict typing / signature preservation (incl. *async* callables)
|
|
12
|
+
* ReST‑style docstring enhancement
|
|
13
|
+
* Configurable preview truncation (`max_bytes`) and a user‑supplied
|
|
14
|
+
`preview_callback` for custom renderings (e.g. pandas.DataFrame)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import re
|
|
20
|
+
import time
|
|
21
|
+
from typing import (
|
|
22
|
+
Any,
|
|
23
|
+
Generic,
|
|
24
|
+
TypeVar,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# =============================================================================
|
|
28
|
+
# 1 · Generic return‑value wrapper
|
|
29
|
+
# =============================================================================
|
|
30
|
+
|
|
31
|
+
_T = TypeVar("_T")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Explorable(Generic[_T]):
|
|
35
|
+
"""
|
|
36
|
+
Wrapper returned by ``@explorable`` decorated tools.
|
|
37
|
+
|
|
38
|
+
Attributes
|
|
39
|
+
----------
|
|
40
|
+
obj_id :
|
|
41
|
+
The internal identifier of the stored object.
|
|
42
|
+
value :
|
|
43
|
+
The original, *unmodified* object returned by the wrapped tool.
|
|
44
|
+
preview :
|
|
45
|
+
Rich‑rendered string generated by the explorer.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
__slots__ = ("obj_id", "value", "_preview")
|
|
49
|
+
|
|
50
|
+
def __init__(self, obj_id: str, value: _T, preview: str) -> None:
|
|
51
|
+
"""Initialize Explorable wrapper with object ID, value, and preview."""
|
|
52
|
+
self.obj_id = obj_id
|
|
53
|
+
self.value: _T = value
|
|
54
|
+
self._preview = preview
|
|
55
|
+
|
|
56
|
+
# ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
|
|
57
|
+
# Representations
|
|
58
|
+
# ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
|
|
59
|
+
|
|
60
|
+
def __str__(self) -> str:
|
|
61
|
+
"""Return the preview string representation."""
|
|
62
|
+
return self._preview
|
|
63
|
+
|
|
64
|
+
__repr__ = __str__
|
|
65
|
+
|
|
66
|
+
# ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
|
|
67
|
+
# Convenience for notebooks / REPLs
|
|
68
|
+
# ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
|
|
69
|
+
|
|
70
|
+
_ipython_display_ = __str__ # Jupyter friendly
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# =============================================================================
|
|
74
|
+
# 2 · Time‑to‑live based object store
|
|
75
|
+
# =============================================================================
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class ObjectStore:
|
|
79
|
+
"""
|
|
80
|
+
Very small in‑memory store with TTL‑based eviction.
|
|
81
|
+
|
|
82
|
+
Parameters
|
|
83
|
+
----------
|
|
84
|
+
ttl :
|
|
85
|
+
Default time‑to‑live in **seconds** for every stored entry. The TTL is
|
|
86
|
+
evaluated lazily on every ``get`` / ``put``. A value of ``0`` disables
|
|
87
|
+
expiry (mainly for tests).
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
def __init__(self, ttl: float = 3_600.0) -> None:
|
|
91
|
+
"""Initialize ObjectStore with TTL in seconds."""
|
|
92
|
+
self._ttl: float = float(ttl)
|
|
93
|
+
self._objects: dict[str, tuple[Any, float]] = {}
|
|
94
|
+
self._counter: int = 0
|
|
95
|
+
|
|
96
|
+
# ––––––––––––––––––––––––––––––––––––––––––––––––––––––
|
|
97
|
+
# Internal helpers
|
|
98
|
+
# ––––––––––––––––––––––––––––––––––––––––––––––––––––––
|
|
99
|
+
|
|
100
|
+
def _now(self) -> float:
|
|
101
|
+
return time.time()
|
|
102
|
+
|
|
103
|
+
def _evict_expired(self) -> None:
|
|
104
|
+
if self._ttl == 0:
|
|
105
|
+
return
|
|
106
|
+
expiry_threshold = self._now() - self._ttl
|
|
107
|
+
expired = [k for k, (_, ts) in self._objects.items() if ts < expiry_threshold]
|
|
108
|
+
for k in expired:
|
|
109
|
+
del self._objects[k]
|
|
110
|
+
|
|
111
|
+
# ––––––––––––––––––––––––––––––––––––––––––––––––––––––
|
|
112
|
+
# Public API
|
|
113
|
+
# ––––––––––––––––––––––––––––––––––––––––––––––––––––––
|
|
114
|
+
|
|
115
|
+
def put(self, obj: Any) -> str:
|
|
116
|
+
"""Store *obj* and return its new reference id (``obj_001`` …)."""
|
|
117
|
+
self._evict_expired()
|
|
118
|
+
self._counter += 1
|
|
119
|
+
obj_id = f"obj_{self._counter:03d}"
|
|
120
|
+
self._objects[obj_id] = (obj, self._now())
|
|
121
|
+
return obj_id
|
|
122
|
+
|
|
123
|
+
def get(self, obj_id: str) -> Any | None:
|
|
124
|
+
"""Retrieve *obj* by id.
|
|
125
|
+
|
|
126
|
+
Returns ``None`` if the object does not exist or has expired.
|
|
127
|
+
"""
|
|
128
|
+
self._evict_expired()
|
|
129
|
+
entry = self._objects.get(obj_id)
|
|
130
|
+
return None if entry is None else entry[0]
|
|
131
|
+
|
|
132
|
+
def delete(self, obj_id: str) -> bool:
|
|
133
|
+
"""Remove object from the store.
|
|
134
|
+
|
|
135
|
+
Returns ``True`` if the object was present.
|
|
136
|
+
"""
|
|
137
|
+
self._evict_expired()
|
|
138
|
+
return self._objects.pop(obj_id, None) is not None
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# =============================================================================
|
|
142
|
+
# 3 · Object references
|
|
143
|
+
# =============================================================================
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class ObjectRef:
|
|
147
|
+
"""Lightweight parser for reference strings of the form.
|
|
148
|
+
|
|
149
|
+
Examples::
|
|
150
|
+
|
|
151
|
+
@obj_042.settings.theme
|
|
152
|
+
@obj_123["settings"]["theme"]
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
_PATTERN = re.compile(r"^@(\w+)(.*)$")
|
|
156
|
+
|
|
157
|
+
def __init__(self, obj_id: str, path: str = "") -> None:
|
|
158
|
+
"""Initialize ObjectRef with object ID and optional path."""
|
|
159
|
+
self.obj_id = obj_id
|
|
160
|
+
self.path = path
|
|
161
|
+
|
|
162
|
+
# --------------------------------------------------------------------- #
|
|
163
|
+
# Factory
|
|
164
|
+
# --------------------------------------------------------------------- #
|
|
165
|
+
|
|
166
|
+
@classmethod
|
|
167
|
+
def parse(cls, ref: str | Any) -> ObjectRef | None:
|
|
168
|
+
"""Parse a reference string into an ObjectRef instance."""
|
|
169
|
+
if not isinstance(ref, str):
|
|
170
|
+
return None
|
|
171
|
+
m = cls._PATTERN.match(ref)
|
|
172
|
+
if m is None:
|
|
173
|
+
return None
|
|
174
|
+
obj_id, path = m.group(1), m.group(2) or ""
|
|
175
|
+
if path.startswith("."):
|
|
176
|
+
path = path[1:]
|
|
177
|
+
return cls(obj_id, path)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Tools for interacting with workspaces."""
|
|
2
|
+
|
|
3
|
+
from deepset_mcp.api.exceptions import BadRequestError, ResourceNotFoundError, UnexpectedAPIError
|
|
4
|
+
from deepset_mcp.api.protocols import AsyncClientProtocol
|
|
5
|
+
from deepset_mcp.api.shared_models import NoContentResponse
|
|
6
|
+
from deepset_mcp.api.workspace.models import Workspace, WorkspaceList
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
async def list_workspaces(*, client: AsyncClientProtocol) -> WorkspaceList | str:
|
|
10
|
+
"""Retrieves a list of all workspaces available to the user.
|
|
11
|
+
|
|
12
|
+
This tool provides an overview of all workspaces that the user has access to.
|
|
13
|
+
Each workspace contains information about its name, ID, supported languages,
|
|
14
|
+
and default idle timeout settings.
|
|
15
|
+
|
|
16
|
+
:param client: The async client for API communication.
|
|
17
|
+
:returns: List of workspaces or error message.
|
|
18
|
+
"""
|
|
19
|
+
try:
|
|
20
|
+
return await client.workspaces().list()
|
|
21
|
+
except (BadRequestError, UnexpectedAPIError) as e:
|
|
22
|
+
return f"Failed to list workspaces: {e}"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
async def get_workspace(*, client: AsyncClientProtocol, workspace_name: str) -> Workspace | str:
|
|
26
|
+
"""Fetches detailed information for a specific workspace by name.
|
|
27
|
+
|
|
28
|
+
This tool retrieves comprehensive details about a specific workspace, including
|
|
29
|
+
its unique ID, supported languages, and configuration settings. Use this when
|
|
30
|
+
you need detailed information about a particular workspace.
|
|
31
|
+
|
|
32
|
+
:param client: The async client for API communication.
|
|
33
|
+
:param workspace_name: The name of the workspace to fetch details for.
|
|
34
|
+
:returns: Workspace details or error message.
|
|
35
|
+
"""
|
|
36
|
+
try:
|
|
37
|
+
return await client.workspaces().get(workspace_name=workspace_name)
|
|
38
|
+
except ResourceNotFoundError:
|
|
39
|
+
return f"There is no workspace named '{workspace_name}'."
|
|
40
|
+
except (BadRequestError, UnexpectedAPIError) as e:
|
|
41
|
+
return f"Failed to fetch workspace '{workspace_name}': {e}"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
async def create_workspace(*, client: AsyncClientProtocol, name: str) -> NoContentResponse | str:
|
|
45
|
+
"""Creates a new workspace with the specified name.
|
|
46
|
+
|
|
47
|
+
This tool creates a new workspace that can be used to organize pipelines,
|
|
48
|
+
indexes, and other resources. The workspace name must be unique across
|
|
49
|
+
the platform. Once created, you can start deploying pipelines and other
|
|
50
|
+
resources within this workspace.
|
|
51
|
+
|
|
52
|
+
:param client: The async client for API communication.
|
|
53
|
+
:param name: The name for the new workspace. Must be unique.
|
|
54
|
+
:returns: Success confirmation or error message.
|
|
55
|
+
"""
|
|
56
|
+
try:
|
|
57
|
+
return await client.workspaces().create(name=name)
|
|
58
|
+
except BadRequestError as e:
|
|
59
|
+
return f"Failed to create workspace '{name}': {e}"
|
|
60
|
+
except UnexpectedAPIError as e:
|
|
61
|
+
return f"Failed to create workspace '{name}': {e}"
|