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.
Files changed (114) hide show
  1. deepset_mcp/__init__.py +0 -0
  2. deepset_mcp/agents/__init__.py +0 -0
  3. deepset_mcp/agents/debugging/__init__.py +0 -0
  4. deepset_mcp/agents/debugging/debugging_agent.py +37 -0
  5. deepset_mcp/agents/debugging/system_prompt.md +214 -0
  6. deepset_mcp/agents/generalist/__init__.py +0 -0
  7. deepset_mcp/agents/generalist/generalist_agent.py +38 -0
  8. deepset_mcp/agents/generalist/system_prompt.md +241 -0
  9. deepset_mcp/api/README.md +536 -0
  10. deepset_mcp/api/__init__.py +0 -0
  11. deepset_mcp/api/client.py +277 -0
  12. deepset_mcp/api/custom_components/__init__.py +0 -0
  13. deepset_mcp/api/custom_components/models.py +25 -0
  14. deepset_mcp/api/custom_components/protocols.py +17 -0
  15. deepset_mcp/api/custom_components/resource.py +56 -0
  16. deepset_mcp/api/exceptions.py +70 -0
  17. deepset_mcp/api/haystack_service/__init__.py +0 -0
  18. deepset_mcp/api/haystack_service/protocols.py +13 -0
  19. deepset_mcp/api/haystack_service/resource.py +55 -0
  20. deepset_mcp/api/indexes/__init__.py +0 -0
  21. deepset_mcp/api/indexes/models.py +63 -0
  22. deepset_mcp/api/indexes/protocols.py +53 -0
  23. deepset_mcp/api/indexes/resource.py +138 -0
  24. deepset_mcp/api/integrations/__init__.py +1 -0
  25. deepset_mcp/api/integrations/models.py +49 -0
  26. deepset_mcp/api/integrations/protocols.py +27 -0
  27. deepset_mcp/api/integrations/resource.py +57 -0
  28. deepset_mcp/api/pipeline/__init__.py +17 -0
  29. deepset_mcp/api/pipeline/log_level.py +9 -0
  30. deepset_mcp/api/pipeline/models.py +235 -0
  31. deepset_mcp/api/pipeline/protocols.py +83 -0
  32. deepset_mcp/api/pipeline/resource.py +378 -0
  33. deepset_mcp/api/pipeline_template/__init__.py +0 -0
  34. deepset_mcp/api/pipeline_template/models.py +56 -0
  35. deepset_mcp/api/pipeline_template/protocols.py +17 -0
  36. deepset_mcp/api/pipeline_template/resource.py +88 -0
  37. deepset_mcp/api/protocols.py +122 -0
  38. deepset_mcp/api/secrets/__init__.py +0 -0
  39. deepset_mcp/api/secrets/models.py +16 -0
  40. deepset_mcp/api/secrets/protocols.py +29 -0
  41. deepset_mcp/api/secrets/resource.py +112 -0
  42. deepset_mcp/api/shared_models.py +17 -0
  43. deepset_mcp/api/transport.py +336 -0
  44. deepset_mcp/api/user/__init__.py +0 -0
  45. deepset_mcp/api/user/protocols.py +11 -0
  46. deepset_mcp/api/user/resource.py +38 -0
  47. deepset_mcp/api/workspace/__init__.py +7 -0
  48. deepset_mcp/api/workspace/models.py +23 -0
  49. deepset_mcp/api/workspace/protocols.py +41 -0
  50. deepset_mcp/api/workspace/resource.py +94 -0
  51. deepset_mcp/benchmark/README.md +425 -0
  52. deepset_mcp/benchmark/__init__.py +1 -0
  53. deepset_mcp/benchmark/agent_configs/debugging_agent.yml +10 -0
  54. deepset_mcp/benchmark/agent_configs/generalist_agent.yml +6 -0
  55. deepset_mcp/benchmark/dp_validation_error_analysis/__init__.py +0 -0
  56. deepset_mcp/benchmark/dp_validation_error_analysis/eda.ipynb +757 -0
  57. deepset_mcp/benchmark/dp_validation_error_analysis/prepare_interaction_data.ipynb +167 -0
  58. deepset_mcp/benchmark/dp_validation_error_analysis/preprocessing_utils.py +213 -0
  59. deepset_mcp/benchmark/runner/__init__.py +0 -0
  60. deepset_mcp/benchmark/runner/agent_benchmark_runner.py +561 -0
  61. deepset_mcp/benchmark/runner/agent_loader.py +110 -0
  62. deepset_mcp/benchmark/runner/cli.py +39 -0
  63. deepset_mcp/benchmark/runner/cli_agent.py +373 -0
  64. deepset_mcp/benchmark/runner/cli_index.py +71 -0
  65. deepset_mcp/benchmark/runner/cli_pipeline.py +73 -0
  66. deepset_mcp/benchmark/runner/cli_tests.py +226 -0
  67. deepset_mcp/benchmark/runner/cli_utils.py +61 -0
  68. deepset_mcp/benchmark/runner/config.py +73 -0
  69. deepset_mcp/benchmark/runner/config_loader.py +64 -0
  70. deepset_mcp/benchmark/runner/interactive.py +140 -0
  71. deepset_mcp/benchmark/runner/models.py +203 -0
  72. deepset_mcp/benchmark/runner/repl.py +67 -0
  73. deepset_mcp/benchmark/runner/setup_actions.py +238 -0
  74. deepset_mcp/benchmark/runner/streaming.py +360 -0
  75. deepset_mcp/benchmark/runner/teardown_actions.py +196 -0
  76. deepset_mcp/benchmark/runner/tracing.py +21 -0
  77. deepset_mcp/benchmark/tasks/chat_rag_answers_wrong_format.yml +16 -0
  78. deepset_mcp/benchmark/tasks/documents_output_wrong.yml +13 -0
  79. deepset_mcp/benchmark/tasks/jinja_str_instead_of_complex_type.yml +11 -0
  80. deepset_mcp/benchmark/tasks/jinja_syntax_error.yml +11 -0
  81. deepset_mcp/benchmark/tasks/missing_output_mapping.yml +14 -0
  82. deepset_mcp/benchmark/tasks/no_query_input.yml +13 -0
  83. deepset_mcp/benchmark/tasks/pipelines/chat_agent_jinja_str.yml +141 -0
  84. deepset_mcp/benchmark/tasks/pipelines/chat_agent_jinja_syntax.yml +141 -0
  85. deepset_mcp/benchmark/tasks/pipelines/chat_rag_answers_wrong_format.yml +181 -0
  86. deepset_mcp/benchmark/tasks/pipelines/chat_rag_missing_output_mapping.yml +189 -0
  87. deepset_mcp/benchmark/tasks/pipelines/rag_documents_wrong_format.yml +193 -0
  88. deepset_mcp/benchmark/tasks/pipelines/rag_no_query_input.yml +191 -0
  89. deepset_mcp/benchmark/tasks/pipelines/standard_index.yml +167 -0
  90. deepset_mcp/initialize_embedding_model.py +12 -0
  91. deepset_mcp/main.py +133 -0
  92. deepset_mcp/prompts/deepset_copilot_prompt.md +271 -0
  93. deepset_mcp/prompts/deepset_debugging_agent.md +214 -0
  94. deepset_mcp/store.py +5 -0
  95. deepset_mcp/tool_factory.py +473 -0
  96. deepset_mcp/tools/__init__.py +0 -0
  97. deepset_mcp/tools/custom_components.py +52 -0
  98. deepset_mcp/tools/doc_search.py +83 -0
  99. deepset_mcp/tools/haystack_service.py +358 -0
  100. deepset_mcp/tools/haystack_service_models.py +97 -0
  101. deepset_mcp/tools/indexes.py +129 -0
  102. deepset_mcp/tools/model_protocol.py +16 -0
  103. deepset_mcp/tools/pipeline.py +335 -0
  104. deepset_mcp/tools/pipeline_template.py +116 -0
  105. deepset_mcp/tools/secrets.py +45 -0
  106. deepset_mcp/tools/tokonomics/__init__.py +73 -0
  107. deepset_mcp/tools/tokonomics/decorators.py +396 -0
  108. deepset_mcp/tools/tokonomics/explorer.py +347 -0
  109. deepset_mcp/tools/tokonomics/object_store.py +177 -0
  110. deepset_mcp/tools/workspace.py +61 -0
  111. deepset_mcp-0.0.2.dist-info/METADATA +288 -0
  112. deepset_mcp-0.0.2.dist-info/RECORD +114 -0
  113. deepset_mcp-0.0.2.dist-info/WHEEL +4 -0
  114. 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}"