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.
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.2rc1.dist-info/METADATA +292 -0
  112. deepset_mcp-0.0.2rc1.dist-info/RECORD +114 -0
  113. deepset_mcp-0.0.2rc1.dist-info/WHEEL +4 -0
  114. deepset_mcp-0.0.2rc1.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,203 @@
1
+ from pathlib import Path
2
+ from typing import Any, Self
3
+
4
+ import yaml
5
+ from pydantic import BaseModel, Field, PrivateAttr, ValidationError, model_validator
6
+
7
+
8
+ class TestCaseConfig(BaseModel):
9
+ """
10
+ Pydantic v2 model for a single “benchmark/tasks/<test>.yml” file.
11
+
12
+ Provides a `from_file()` constructor that:
13
+ - Reads the YAML from disk.
14
+ - Resolves `query_yaml`, `index_yaml`, `expected_query` as paths relative to the YAML file’s directory.
15
+ - Validates that at least one of query/index is present, that paired fields (query_name/index_name) exist,
16
+ and that each referenced file actually exists on disk.
17
+ """
18
+
19
+ name: str = Field(
20
+ ...,
21
+ description="Unique identifier for this test case (snake_case, no spaces).",
22
+ pattern=r"^[a-z0-9_]+$",
23
+ )
24
+ objective: str = Field(..., description="A short description of what this test is about.")
25
+ prompt: str = Field(..., description="The prompt text to send to the Agent.")
26
+ query_yaml: str | None = Field(None, description="Relative or absolute path to a query pipeline YAML.")
27
+ query_name: str | None = Field(
28
+ None, description="Name to assign to the ‘query’ pipeline if `query_yaml` is present."
29
+ )
30
+ index_yaml: str | None = Field(None, description="Relative or absolute path to an indexing pipeline YAML.")
31
+ index_name: str | None = Field(None, description="Name to assign to the Index if `index_yaml` is present.")
32
+ expected_query: str | None = Field(
33
+ None, description="(Optional) Relative or absolute path to a 'gold' query pipeline YAML."
34
+ )
35
+ expected_index: str | None = Field(None, description="(Optional) Relative or absolute path to a 'gold' index YAML.")
36
+ tags: list[str] = Field(default_factory=list, description="Tags (e.g. [api-outputs, debug]).")
37
+ judge_prompt: str | None = Field(
38
+ None,
39
+ description="(Optional) Prompt to use for a judge LLM to verify correctness.",
40
+ )
41
+
42
+ # These PrivateAttrs will hold the raw YAML‐as‐text after reading from disk:
43
+ _query_yaml_text: str | None = PrivateAttr(default=None)
44
+ _index_yaml_text: str | None = PrivateAttr(default=None)
45
+ _expected_query_text: str | None = PrivateAttr(default=None)
46
+ _expected_index_text: str | None = PrivateAttr(default=None)
47
+
48
+ @model_validator(mode="before")
49
+ def _check_at_least_one(cls, values: dict[str, str]) -> dict[str, str]:
50
+ """Before any field‐level validation, ensure at least one of `query_yaml` or `index_yaml` is provided."""
51
+ if not values.get("query_yaml") and not values.get("index_yaml"):
52
+ raise ValueError("At least one of `query_yaml` or `index_yaml` must be provided.")
53
+ return values
54
+
55
+ @model_validator(mode="after")
56
+ def _load_yaml_files(self) -> Self:
57
+ """
58
+ Hook to load YAML contents from disk.
59
+
60
+ After all standard field validation has passed, this hook:
61
+
62
+ 1) If `query_yaml` is set:
63
+ - Ensures `query_name` is also set.
64
+ - Reads the file from disk into `_query_yaml_text`.
65
+ 2) If `index_yaml` is set:
66
+ - Ensures `index_name` is also set.
67
+ - Reads the file from disk into `_index_yaml_text`.
68
+ 3) If `expected_query` is set:
69
+ - Reads that file into `_expected_query_text`.
70
+ Any missing paired field or missing file will raise an error.
71
+ """
72
+ # 1) Query pipeline YAML → text
73
+ if self.query_yaml:
74
+ if not self.query_name:
75
+ raise ValueError("`query_name` must be provided if `query_yaml` is set.")
76
+ path = Path(self.query_yaml)
77
+ if not path.is_file():
78
+ raise FileNotFoundError(f"query_yaml file not found: {self.query_yaml}")
79
+ self._query_yaml_text = path.read_text(encoding="utf-8")
80
+
81
+ # 2) Index YAML → text
82
+ if self.index_yaml:
83
+ if not self.index_name:
84
+ raise ValueError("`index_name` must be provided if `index_yaml` is set.")
85
+ path = Path(self.index_yaml)
86
+ if not path.is_file():
87
+ raise FileNotFoundError(f"index_yaml file not found: {self.index_yaml}")
88
+ self._index_yaml_text = path.read_text(encoding="utf-8")
89
+
90
+ # 3) Expected “gold” pipeline YAML → text
91
+ if self.expected_query:
92
+ path = Path(self.expected_query)
93
+ if not path.is_file():
94
+ raise FileNotFoundError(f"expected_query file not found: {self.expected_query}")
95
+ self._expected_query_text = path.read_text(encoding="utf-8")
96
+
97
+ if self.expected_index:
98
+ path = Path(self.expected_index)
99
+ if not path.is_file():
100
+ raise FileNotFoundError(f"expected_index file not found: {self.expected_index}")
101
+ self._expected_index_text = path.read_text(encoding="utf-8")
102
+
103
+ return self
104
+
105
+ def get_query_yaml_text(self) -> str | None:
106
+ """Return the raw text of the query‐pipeline YAML (or None if not set)."""
107
+ return self._query_yaml_text
108
+
109
+ def get_index_yaml_text(self) -> str | None:
110
+ """Return the raw text of the index YAML (or None if not set)."""
111
+ return self._index_yaml_text
112
+
113
+ def get_expected_query_text(self) -> str | None:
114
+ """Return the raw text of the expected “gold” pipeline YAML (or None)."""
115
+ return self._expected_query_text
116
+
117
+ def get_expected_index_text(self) -> str | None:
118
+ """Return the raw text of the expected 'gold' index YAML (or None)."""
119
+ return self._expected_index_text
120
+
121
+ @classmethod
122
+ def from_file(cls, cfg_path: Path) -> Self:
123
+ """
124
+ Read a test-case YAML from `cfg_path`, then initialize and return a TestCaseConfig instance.
125
+
126
+ Raises:
127
+ - FileNotFoundError if cfg_path doesn’t exist.
128
+ - ValidationError if any field is invalid or any referenced file is missing.
129
+ """
130
+ if not cfg_path.is_file():
131
+ raise FileNotFoundError(f"Test-case config not found: {cfg_path}")
132
+
133
+ raw = yaml.safe_load(cfg_path.read_text(encoding="utf-8"))
134
+ if not isinstance(raw, dict):
135
+ raise ValidationError(f"Invalid test-case YAML at {cfg_path}; expected a mapping.")
136
+
137
+ base_dir = cfg_path.parent
138
+
139
+ # For each of query_yaml, index_yaml, expected_query: if present and relative, make it absolute.
140
+ for field_name in ("query_yaml", "index_yaml", "expected_query"):
141
+ p = raw.get(field_name)
142
+ if p:
143
+ # Only rewrite if it’s not already absolute
144
+ candidate = Path(p)
145
+ if not candidate.is_absolute():
146
+ raw[field_name] = str((base_dir / candidate).resolve())
147
+
148
+ return cls(**raw)
149
+
150
+
151
+ class AgentConfig(BaseModel):
152
+ """Agent configuration with flexible loading patterns."""
153
+
154
+ agent_json: str | None = Field(None, description="Relative or absolute path to an agent JSON file.")
155
+
156
+ agent_factory_function: str | None = Field(None, description="Qualified name of Agent factory function.")
157
+
158
+ display_name: str = Field(..., description="Display name for the agent.")
159
+
160
+ interactive: bool = Field(False, description="Whether to run the agent in interactive mode.")
161
+
162
+ required_env_vars: list[str] = Field(
163
+ default_factory=list, description="Required environment variables to run the agent."
164
+ )
165
+
166
+ @model_validator(mode="before")
167
+ @classmethod
168
+ def check_mutually_exclusive(cls, values: dict[str, Any]) -> dict[str, Any]:
169
+ """Ensure exactly one loading method is specified."""
170
+ methods = [values.get("agent_json"), values.get("agent_factory_function")]
171
+
172
+ if sum(bool(m) for m in methods) != 1:
173
+ raise ValueError("Exactly one of agent_json or agent_factory_function must be provided")
174
+ return values
175
+
176
+ @model_validator(mode="after")
177
+ def validate_files_exist(self) -> Self:
178
+ """Validate that referenced files exist."""
179
+ if self.agent_json:
180
+ json_path = Path(self.agent_json)
181
+ if not json_path.is_file():
182
+ raise FileNotFoundError(f"Agent JSON file not found: {self.agent_json}")
183
+ return self
184
+
185
+ @classmethod
186
+ def from_file(cls, cfg_path: Path) -> Self:
187
+ """Read agent config from YAML file."""
188
+ if not cfg_path.is_file():
189
+ raise FileNotFoundError(f"Agent config not found: {cfg_path}")
190
+
191
+ raw = yaml.safe_load(cfg_path.read_text(encoding="utf-8"))
192
+ if not isinstance(raw, dict):
193
+ raise ValueError(f"Invalid agent config YAML at {cfg_path}; expected a mapping.")
194
+
195
+ base_dir = cfg_path.parent
196
+
197
+ # Resolve relative paths for agent_json
198
+ if "agent_json" in raw and raw["agent_json"]:
199
+ json_path = Path(raw["agent_json"])
200
+ if not json_path.is_absolute():
201
+ raw["agent_json"] = str((base_dir / json_path).resolve())
202
+
203
+ return cls(**raw)
@@ -0,0 +1,67 @@
1
+ import asyncio
2
+ from typing import Any
3
+
4
+ import typer
5
+ from haystack.dataclasses.chat_message import ChatMessage
6
+ from haystack.dataclasses.streaming_chunk import StreamingChunk
7
+
8
+ from deepset_mcp.benchmark.runner.agent_loader import load_agent
9
+ from deepset_mcp.benchmark.runner.config import BenchmarkConfig
10
+ from deepset_mcp.benchmark.runner.models import AgentConfig
11
+ from deepset_mcp.benchmark.runner.streaming import StreamingCallbackManager
12
+
13
+
14
+ async def run_repl_session_async(agent_config: AgentConfig, benchmark_config: BenchmarkConfig) -> None:
15
+ """Starts an interactive REPL session with the specified agent."""
16
+ agent, _ = load_agent(
17
+ config=agent_config,
18
+ benchmark_config=benchmark_config,
19
+ interactive=agent_config.interactive,
20
+ )
21
+ history: list[ChatMessage] = []
22
+
23
+ typer.secho(f"Starting interactive session with '{agent_config.display_name}'.", fg=typer.colors.CYAN)
24
+ typer.secho("Type 'exit' or 'quit' to end the session.", fg=typer.colors.CYAN)
25
+
26
+ while True:
27
+ try:
28
+ user_input = typer.prompt("\n👤 You")
29
+ if user_input.lower() in ["exit", "quit"]:
30
+ typer.secho("Ending session. Goodbye!", fg=typer.colors.MAGENTA)
31
+ break
32
+
33
+ # Add user message to history
34
+ history.append(ChatMessage.from_user(user_input))
35
+
36
+ # Setup streaming
37
+ streaming_callback_manager = StreamingCallbackManager()
38
+
39
+ async def streaming_callback(
40
+ chunk: StreamingChunk,
41
+ manager: StreamingCallbackManager = streaming_callback_manager,
42
+ ) -> Any:
43
+ return await manager(chunk)
44
+
45
+ # Run the agent
46
+ typer.secho("\n🤖 Agent\n\n", fg=typer.colors.BLUE, nl=False)
47
+ agent_output = await agent.run_async(messages=history, streaming_callback=streaming_callback)
48
+
49
+ # The streaming callback handles printing the final text output.
50
+ # We replace our local history with the full history from the agent
51
+ # to preserve the tool calls and results.
52
+ if agent_output and "messages" in agent_output:
53
+ history = agent_output["messages"]
54
+
55
+ except (KeyboardInterrupt, EOFError):
56
+ typer.secho("\nEnding session. Goodbye!", fg=typer.colors.MAGENTA)
57
+ break
58
+ except Exception as e:
59
+ typer.secho(f"\nAn error occurred: {e}", fg=typer.colors.RED)
60
+ # Remove the user message that caused the error to prevent it from being re-processed
61
+ if history and history[-1].is_from("user"):
62
+ history.pop()
63
+
64
+
65
+ def run_repl_session(agent_config: AgentConfig, benchmark_config: BenchmarkConfig) -> None:
66
+ """Synchronous wrapper for the REPL session."""
67
+ asyncio.run(run_repl_session_async(agent_config, benchmark_config))
@@ -0,0 +1,238 @@
1
+ import asyncio
2
+ import os
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ from deepset_mcp.api.client import AsyncDeepsetClient
7
+ from deepset_mcp.benchmark.runner.models import TestCaseConfig
8
+
9
+
10
+ def _get_api_key(explicit_key: str | None) -> str:
11
+ """
12
+ Return whichever API key to use: explicit_key takes precedence, otherwise read DP_API_KEY from the environment.
13
+
14
+ If still missing, raise ValueError.
15
+ """
16
+ if explicit_key:
17
+ return explicit_key
18
+ env_key = os.getenv("DP_API_KEY")
19
+ if not env_key:
20
+ raise ValueError("No API key provided: pass --api-key or set DP_API_KEY in env.")
21
+ return env_key
22
+
23
+
24
+ # ─────────────────────────────────────────────────────────────────────────────
25
+ # 1) LOW-LEVEL: “setup_pipeline” and “setup_index” using AsyncDeepsetClient as a context manager
26
+ # ─────────────────────────────────────────────────────────────────────────────
27
+
28
+
29
+ async def setup_pipeline_async(
30
+ *,
31
+ yaml_path: str | None = None,
32
+ yaml_content: str | None = None,
33
+ pipeline_name: str,
34
+ workspace_name: str,
35
+ api_key: str | None = None,
36
+ ) -> None:
37
+ """
38
+ Create a new pipeline in the given workspace. Exactly one of (yaml_path, yaml_content) must be provided.
39
+
40
+ Uses DP_API_KEY or explicit api_key.
41
+ """
42
+ if (yaml_path and yaml_content) or (not yaml_path and not yaml_content):
43
+ raise ValueError("Exactly one of `yaml_path` or `yaml_content` must be specified.")
44
+
45
+ if yaml_path is not None:
46
+ yaml_str = Path(yaml_path).read_text(encoding="utf-8")
47
+ else:
48
+ yaml_str = yaml_content # type: ignore
49
+
50
+ key_to_use = _get_api_key(api_key)
51
+ async with AsyncDeepsetClient(api_key=key_to_use) as client:
52
+ await client.pipelines(workspace=workspace_name).create(
53
+ name=pipeline_name,
54
+ yaml_config=yaml_str,
55
+ )
56
+ return None
57
+
58
+
59
+ async def setup_index_async(
60
+ *,
61
+ yaml_path: str | None = None,
62
+ yaml_content: str | None = None,
63
+ index_name: str,
64
+ workspace_name: str,
65
+ api_key: str | None = None,
66
+ description: str | None = None,
67
+ ) -> None:
68
+ """
69
+ Create a new index in the given workspace. Exactly one of (yaml_path, yaml_content) must be provided.
70
+
71
+ Uses DP_API_KEY or explicit api_key.
72
+ """
73
+ if (yaml_path and yaml_content) or (not yaml_path and not yaml_content):
74
+ raise ValueError("Exactly one of `yaml_path` or `yaml_content` must be specified.")
75
+
76
+ if yaml_path is not None:
77
+ yaml_str = Path(yaml_path).read_text(encoding="utf-8")
78
+ else:
79
+ yaml_str = yaml_content # type: ignore
80
+
81
+ key_to_use = _get_api_key(api_key)
82
+ async with AsyncDeepsetClient(api_key=key_to_use) as client:
83
+ await client.indexes(workspace=workspace_name).create(
84
+ name=index_name,
85
+ yaml_config=yaml_str,
86
+ description=description or "",
87
+ )
88
+ return None
89
+
90
+
91
+ # ─────────────────────────────────────────────────────────────────────────────
92
+ # 2) MID-LEVEL: setup a full test-case (pipeline + index if present)
93
+ # ─────────────────────────────────────────────────────────────────────────────
94
+
95
+
96
+ async def setup_test_case_async(
97
+ *,
98
+ test_cfg: TestCaseConfig,
99
+ workspace_name: str,
100
+ api_key: str | None = None,
101
+ ) -> None:
102
+ """
103
+ Given a TestCaseConfig, create the index and the pipeline in the specified workspace.
104
+
105
+ Uses DP_API_KEY or explicit api_key.
106
+ """
107
+ # 1) If there’s an index to create:
108
+ if test_cfg.index_yaml:
109
+ assert test_cfg.index_name is not None # already validated by Pydantic model; added to satisfy mypy
110
+ await setup_index_async(
111
+ yaml_content=test_cfg.get_index_yaml_text(),
112
+ index_name=test_cfg.index_name,
113
+ workspace_name=workspace_name,
114
+ api_key=api_key,
115
+ description=f"Index for test {test_cfg.name}",
116
+ )
117
+
118
+ # 2) If there’s a “query pipeline” to create:
119
+ if test_cfg.query_yaml:
120
+ assert test_cfg.query_name is not None # already validated by Pydantic model; added to satisfy mypy
121
+ await setup_pipeline_async(
122
+ yaml_content=test_cfg.get_query_yaml_text(),
123
+ pipeline_name=test_cfg.query_name,
124
+ workspace_name=workspace_name,
125
+ api_key=api_key,
126
+ )
127
+
128
+ return None
129
+
130
+
131
+ # ─────────────────────────────────────────────────────────────────────────────
132
+ # 3) HIGH-LEVEL: parallel “setup all” with configurable concurrency
133
+ # ─────────────────────────────────────────────────────────────────────────────
134
+
135
+
136
+ async def setup_all_async(
137
+ *,
138
+ test_cfgs: list[TestCaseConfig],
139
+ workspace_name: str,
140
+ api_key: str | None = None,
141
+ concurrency: int = 5,
142
+ ) -> None:
143
+ """
144
+ Given a list of TestCaseConfig, create all indexes and pipelines in parallel.
145
+
146
+ Uses DP_API_KEY or explicit api_key.
147
+ """
148
+ semaphore = asyncio.Semaphore(concurrency)
149
+ tasks: list[asyncio.Task[Any]] = []
150
+
151
+ async def sem_task(cfg: TestCaseConfig) -> str:
152
+ async with semaphore:
153
+ await setup_test_case_async(test_cfg=cfg, workspace_name=workspace_name, api_key=api_key)
154
+ return cfg.name
155
+
156
+ for cfg in test_cfgs:
157
+ tasks.append(asyncio.create_task(sem_task(cfg)))
158
+
159
+ done, _ = await asyncio.wait(tasks, return_when=asyncio.ALL_COMPLETED)
160
+ errors: list[Exception] = []
161
+ for t in done:
162
+ if t.exception():
163
+ errors.append(t.exception()) # type: ignore
164
+
165
+ if errors:
166
+ raise RuntimeError(f"Errors during setup: {errors}")
167
+
168
+ return None
169
+
170
+
171
+ # ─────────────────────────────────────────────────────────────────────────────
172
+ # 4) SYNC WRAPPERS for all of the above (now accept api_key)
173
+ # ─────────────────────────────────────────────────────────────────────────────
174
+
175
+
176
+ def setup_pipeline(
177
+ *,
178
+ yaml_path: str | None = None,
179
+ yaml_content: str | None = None,
180
+ pipeline_name: str,
181
+ workspace_name: str,
182
+ api_key: str | None = None,
183
+ ) -> None:
184
+ """Synchronous wrapper for setup_pipeline_async. Blocks until the pipeline is created."""
185
+ return asyncio.run(
186
+ setup_pipeline_async(
187
+ yaml_path=yaml_path,
188
+ yaml_content=yaml_content,
189
+ pipeline_name=pipeline_name,
190
+ workspace_name=workspace_name,
191
+ api_key=api_key,
192
+ )
193
+ )
194
+
195
+
196
+ def setup_index(
197
+ *,
198
+ yaml_path: str | None = None,
199
+ yaml_content: str | None = None,
200
+ index_name: str,
201
+ workspace_name: str,
202
+ api_key: str | None = None,
203
+ description: str | None = None,
204
+ ) -> None:
205
+ """Synchronous wrapper for setup_index_async. Blocks until the index is created."""
206
+ return asyncio.run(
207
+ setup_index_async(
208
+ yaml_path=yaml_path,
209
+ yaml_content=yaml_content,
210
+ index_name=index_name,
211
+ workspace_name=workspace_name,
212
+ api_key=api_key,
213
+ description=description,
214
+ )
215
+ )
216
+
217
+
218
+ def setup_test_case(
219
+ *,
220
+ test_cfg: TestCaseConfig,
221
+ workspace_name: str,
222
+ api_key: str | None = None,
223
+ ) -> None:
224
+ """Synchronous wrapper: blocks until both pipeline and index (if any) are created."""
225
+ return asyncio.run(setup_test_case_async(test_cfg=test_cfg, workspace_name=workspace_name, api_key=api_key))
226
+
227
+
228
+ def setup_all(
229
+ *,
230
+ test_cfgs: list[TestCaseConfig],
231
+ workspace_name: str,
232
+ api_key: str | None = None,
233
+ concurrency: int = 5,
234
+ ) -> None:
235
+ """Synchronous wrapper for setup_all_async. Blocks until all test-cases are created."""
236
+ return asyncio.run(
237
+ setup_all_async(test_cfgs=test_cfgs, workspace_name=workspace_name, api_key=api_key, concurrency=concurrency)
238
+ )