haystack-enterprise-sdk 0.1.0__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. {haystack_enterprise_sdk-0.1.0 → haystack_enterprise_sdk-0.2.0}/PKG-INFO +31 -10
  2. {haystack_enterprise_sdk-0.1.0 → haystack_enterprise_sdk-0.2.0}/README.md +23 -7
  3. {haystack_enterprise_sdk-0.1.0 → haystack_enterprise_sdk-0.2.0}/haystack_enterprise_sdk/README.md +1 -1
  4. {haystack_enterprise_sdk-0.1.0 → haystack_enterprise_sdk-0.2.0}/haystack_enterprise_sdk/_api/deployments.py +59 -1
  5. {haystack_enterprise_sdk-0.1.0 → haystack_enterprise_sdk-0.2.0}/haystack_enterprise_sdk/_api/haystack_enterprise_api.py +16 -4
  6. {haystack_enterprise_sdk-0.1.0 → haystack_enterprise_sdk-0.2.0}/haystack_enterprise_sdk/_service/deployment_service.py +55 -1
  7. {haystack_enterprise_sdk-0.1.0 → haystack_enterprise_sdk-0.2.0}/haystack_enterprise_sdk/_service/pipeline_extract.py +47 -2
  8. {haystack_enterprise_sdk-0.1.0 → haystack_enterprise_sdk-0.2.0}/haystack_enterprise_sdk/cli.py +76 -5
  9. haystack_enterprise_sdk-0.2.0/haystack_enterprise_sdk/py.typed +0 -0
  10. {haystack_enterprise_sdk-0.1.0 → haystack_enterprise_sdk-0.2.0}/haystack_enterprise_sdk/workflows/async_client/deployment_client.py +26 -4
  11. {haystack_enterprise_sdk-0.1.0 → haystack_enterprise_sdk-0.2.0}/haystack_enterprise_sdk/workflows/sync_client/deployment_client.py +13 -1
  12. {haystack_enterprise_sdk-0.1.0 → haystack_enterprise_sdk-0.2.0}/pyproject.toml +18 -5
  13. {haystack_enterprise_sdk-0.1.0 → haystack_enterprise_sdk-0.2.0}/pyproject.toml.orig +18 -5
  14. {haystack_enterprise_sdk-0.1.0 → haystack_enterprise_sdk-0.2.0}/LICENSE +0 -0
  15. {haystack_enterprise_sdk-0.1.0 → haystack_enterprise_sdk-0.2.0}/haystack_enterprise_sdk/__init__.py +0 -0
  16. {haystack_enterprise_sdk-0.1.0 → haystack_enterprise_sdk-0.2.0}/haystack_enterprise_sdk/_api/config.py +0 -0
  17. {haystack_enterprise_sdk-0.1.0 → haystack_enterprise_sdk-0.2.0}/haystack_enterprise_sdk/_api/files.py +0 -0
  18. {haystack_enterprise_sdk-0.1.0 → haystack_enterprise_sdk-0.2.0}/haystack_enterprise_sdk/_api/pipeline_run.py +0 -0
  19. {haystack_enterprise_sdk-0.1.0 → haystack_enterprise_sdk-0.2.0}/haystack_enterprise_sdk/_api/shared_prototypes.py +0 -0
  20. {haystack_enterprise_sdk-0.1.0 → haystack_enterprise_sdk-0.2.0}/haystack_enterprise_sdk/_api/upload_sessions.py +0 -0
  21. {haystack_enterprise_sdk-0.1.0 → haystack_enterprise_sdk-0.2.0}/haystack_enterprise_sdk/_console.py +0 -0
  22. {haystack_enterprise_sdk-0.1.0 → haystack_enterprise_sdk-0.2.0}/haystack_enterprise_sdk/_s3/__init__.py +0 -0
  23. {haystack_enterprise_sdk-0.1.0 → haystack_enterprise_sdk-0.2.0}/haystack_enterprise_sdk/_s3/upload.py +0 -0
  24. {haystack_enterprise_sdk-0.1.0 → haystack_enterprise_sdk-0.2.0}/haystack_enterprise_sdk/_service/files_service.py +0 -0
  25. {haystack_enterprise_sdk-0.1.0 → haystack_enterprise_sdk-0.2.0}/haystack_enterprise_sdk/_service/io_spec.py +0 -0
  26. {haystack_enterprise_sdk-0.1.0 → haystack_enterprise_sdk-0.2.0}/haystack_enterprise_sdk/_service/pipeline_service.py +0 -0
  27. {haystack_enterprise_sdk-0.1.0 → haystack_enterprise_sdk-0.2.0}/haystack_enterprise_sdk/_service/pipeline_transform.py +0 -0
  28. {haystack_enterprise_sdk-0.1.0 → haystack_enterprise_sdk-0.2.0}/haystack_enterprise_sdk/_utils/__init__.py +0 -0
  29. {haystack_enterprise_sdk-0.1.0 → haystack_enterprise_sdk-0.2.0}/haystack_enterprise_sdk/_utils/datetime.py +0 -0
  30. {haystack_enterprise_sdk-0.1.0 → haystack_enterprise_sdk-0.2.0}/haystack_enterprise_sdk/models.py +0 -0
  31. {haystack_enterprise_sdk-0.1.0 → haystack_enterprise_sdk-0.2.0}/haystack_enterprise_sdk/workflows/__init__.py +0 -0
  32. {haystack_enterprise_sdk-0.1.0 → haystack_enterprise_sdk-0.2.0}/haystack_enterprise_sdk/workflows/async_client/__init__.py +0 -0
  33. {haystack_enterprise_sdk-0.1.0 → haystack_enterprise_sdk-0.2.0}/haystack_enterprise_sdk/workflows/async_client/async_pipeline_client.py +0 -0
  34. {haystack_enterprise_sdk-0.1.0 → haystack_enterprise_sdk-0.2.0}/haystack_enterprise_sdk/workflows/async_client/files.py +0 -0
  35. {haystack_enterprise_sdk-0.1.0 → haystack_enterprise_sdk-0.2.0}/haystack_enterprise_sdk/workflows/sync_client/__init__.py +0 -0
  36. {haystack_enterprise_sdk-0.1.0 → haystack_enterprise_sdk-0.2.0}/haystack_enterprise_sdk/workflows/sync_client/files.py +0 -0
  37. {haystack_enterprise_sdk-0.1.0 → haystack_enterprise_sdk-0.2.0}/haystack_enterprise_sdk/workflows/sync_client/pipeline_client.py +0 -0
  38. {haystack_enterprise_sdk-0.1.0 → haystack_enterprise_sdk-0.2.0}/haystack_enterprise_sdk/workflows/sync_client/utils.py +0 -0
@@ -1,13 +1,16 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: haystack-enterprise-sdk
3
- Version: 0.1.0
4
- Summary: Haystack Enterprise SDK
5
- Keywords:
3
+ Version: 0.2.0
4
+ Summary: Python SDK and CLI for the Haystack Enterprise Platform
5
+ Keywords: agents,deepset,haystack,llm,pipelines,rag,sdk
6
6
  Author: deepset.ai
7
7
  Author-email: deepset.ai <malte.pietsch@deepset.ai>
8
8
  License-Expression: Apache-2.0
9
9
  License-File: LICENSE
10
10
  Classifier: Development Status :: 4 - Beta
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Operating System :: OS Independent
11
14
  Classifier: Programming Language :: Python
12
15
  Classifier: Programming Language :: Python :: 3
13
16
  Classifier: Programming Language :: Python :: 3.10
@@ -15,6 +18,8 @@ Classifier: Programming Language :: Python :: 3.11
15
18
  Classifier: Programming Language :: Python :: 3.12
16
19
  Classifier: Programming Language :: Python :: 3.13
17
20
  Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
22
+ Classifier: Typing :: Typed
18
23
  Requires-Dist: structlog>=24.0.0
19
24
  Requires-Dist: httpx>=0.27.2
20
25
  Requires-Dist: python-dotenv>=1.0.1
@@ -53,14 +58,30 @@ Python SDK and CLI for the Haystack Enterprise Platform.
53
58
 
54
59
  ## Installation
55
60
 
56
- Not published to a package registry yet — install directly from this repository with [uv](https://docs.astral.sh/uv/):
57
-
58
61
  ```bash
59
62
  # Install as a CLI tool
60
- uv tool install git+https://github.com/deepset-ai/haystack-enterprise-sdk.git
63
+ uv tool install haystack-enterprise-sdk
61
64
 
62
65
  # Or add it as a dependency of your project
63
- uv add git+https://github.com/deepset-ai/haystack-enterprise-sdk.git
66
+ uv add haystack-enterprise-sdk
67
+
68
+ # pip works too
69
+ pip install haystack-enterprise-sdk
70
+ ```
71
+
72
+ `deploy`, `validate`, and `run` load your pipeline in a subprocess using your project's own interpreter
73
+ (an auto-detected venv, or `--python`), so the CLI environment does not need Haystack. Install the
74
+ `deploy` extra only when the CLI environment doubles as the pipeline environment — that is, when there
75
+ is no separate project venv to detect:
76
+
77
+ ```bash
78
+ uv tool install "haystack-enterprise-sdk[deploy]"
79
+ ```
80
+
81
+ To install unreleased changes from `main`:
82
+
83
+ ```bash
84
+ uv tool install git+https://github.com/deepset-ai/haystack-enterprise-sdk.git
64
85
  ```
65
86
 
66
87
  ## Usage
@@ -124,14 +145,14 @@ A deployed service is served over an OpenAI-compatible chat-completions endpoint
124
145
  once the service is running:
125
146
 
126
147
  ```bash
127
- curl -N https://api.cloud.deepset.ai/api/v1/workspaces/<workspace>/deployments/<deployment-id>/chat/completions \
148
+ curl -N https://api.cloud.deepset.ai/api/v1/workspaces/<workspace>/deployments/v1/chat/completions \
128
149
  -H "Authorization: Bearer $API_KEY" \
129
150
  -H "Content-Type: application/json" \
130
- -d '{"model": "<workspace>/<service-name>", "messages": [{"role": "user", "content": "Hello"}]}'
151
+ -d '{"model": "<workspace>/<deployment-id>", "messages": [{"role": "user", "content": "Hello"}]}'
131
152
  ```
132
153
 
133
154
  The response is a server-sent-event stream of `chat.completion.chunk` objects. Any OpenAI client works —
134
- point its `base_url` at everything up to and including `/deployments/<deployment-id>`.
155
+ point its `base_url` at everything up to and including `/deployments/v1`.
135
156
 
136
157
  Pass `--verbose` to any command for INFO/DEBUG logs, and `<command> --help` for its arguments.
137
158
 
@@ -13,14 +13,30 @@ Python SDK and CLI for the Haystack Enterprise Platform.
13
13
 
14
14
  ## Installation
15
15
 
16
- Not published to a package registry yet — install directly from this repository with [uv](https://docs.astral.sh/uv/):
17
-
18
16
  ```bash
19
17
  # Install as a CLI tool
20
- uv tool install git+https://github.com/deepset-ai/haystack-enterprise-sdk.git
18
+ uv tool install haystack-enterprise-sdk
21
19
 
22
20
  # Or add it as a dependency of your project
23
- uv add git+https://github.com/deepset-ai/haystack-enterprise-sdk.git
21
+ uv add haystack-enterprise-sdk
22
+
23
+ # pip works too
24
+ pip install haystack-enterprise-sdk
25
+ ```
26
+
27
+ `deploy`, `validate`, and `run` load your pipeline in a subprocess using your project's own interpreter
28
+ (an auto-detected venv, or `--python`), so the CLI environment does not need Haystack. Install the
29
+ `deploy` extra only when the CLI environment doubles as the pipeline environment — that is, when there
30
+ is no separate project venv to detect:
31
+
32
+ ```bash
33
+ uv tool install "haystack-enterprise-sdk[deploy]"
34
+ ```
35
+
36
+ To install unreleased changes from `main`:
37
+
38
+ ```bash
39
+ uv tool install git+https://github.com/deepset-ai/haystack-enterprise-sdk.git
24
40
  ```
25
41
 
26
42
  ## Usage
@@ -84,14 +100,14 @@ A deployed service is served over an OpenAI-compatible chat-completions endpoint
84
100
  once the service is running:
85
101
 
86
102
  ```bash
87
- curl -N https://api.cloud.deepset.ai/api/v1/workspaces/<workspace>/deployments/<deployment-id>/chat/completions \
103
+ curl -N https://api.cloud.deepset.ai/api/v1/workspaces/<workspace>/deployments/v1/chat/completions \
88
104
  -H "Authorization: Bearer $API_KEY" \
89
105
  -H "Content-Type: application/json" \
90
- -d '{"model": "<workspace>/<service-name>", "messages": [{"role": "user", "content": "Hello"}]}'
106
+ -d '{"model": "<workspace>/<deployment-id>", "messages": [{"role": "user", "content": "Hello"}]}'
91
107
  ```
92
108
 
93
109
  The response is a server-sent-event stream of `chat.completion.chunk` objects. Any OpenAI client works —
94
- point its `base_url` at everything up to and including `/deployments/<deployment-id>`.
110
+ point its `base_url` at everything up to and including `/deployments/v1`.
95
111
 
96
112
  Pass `--verbose` to any command for INFO/DEBUG logs, and `<command> --help` for its arguments.
97
113
 
@@ -1,4 +1,4 @@
1
- # Software development kit for the deepset API
1
+ # Software design
2
2
 
3
3
  This package is split into multiple layers:
4
4
  - API layer
@@ -10,7 +10,7 @@ list endpoint and matching client-side.
10
10
 
11
11
  import asyncio
12
12
  import enum
13
- from dataclasses import dataclass
13
+ from dataclasses import dataclass, field
14
14
  from typing import Any, Dict, List, Optional, Type, TypeVar
15
15
  from uuid import UUID
16
16
 
@@ -97,6 +97,10 @@ class Deployment:
97
97
  # revision, so it stays None until a revision is activated. This is the platform's own answer to
98
98
  # "is this a chat pipeline?" -- the CLI reads it rather than guessing from components or sockets.
99
99
  output_type: Optional[PipelineOutputType] = None
100
+ # Free-form labels, capped server-side at MAX_SERVICE_TAGS (3). Not settable at creation --
101
+ # the platform has no `tags` field on its create-deployment request, only the dedicated
102
+ # add/remove-tag endpoints below, so this always reflects a create followed by N tag calls.
103
+ tags: List[str] = field(default_factory=list)
100
104
 
101
105
  @classmethod
102
106
  def from_response(cls, body: Dict[str, Any]) -> "Deployment":
@@ -120,6 +124,7 @@ class Deployment:
120
124
  # The platform enum has values this SDK does not model (e.g. "unknown"), and the field is
121
125
  # absent until a revision is active, so anything unrecognized degrades to None.
122
126
  output_type=_enum_or_none(PipelineOutputType, body.get("output_type")),
127
+ tags=list(body.get("tags") or []),
123
128
  )
124
129
 
125
130
 
@@ -244,6 +249,15 @@ class FailedToActivateRevisionError(Exception):
244
249
  """Raised when a deployment revision could not be activated."""
245
250
 
246
251
 
252
+ class FailedToTagDeploymentError(Exception):
253
+ """Raised when a tag could not be added to, or removed from, a deployment.
254
+
255
+ Covers the platform's own rejections too: a name over 50 chars or outside
256
+ ``[A-Za-z0-9 _-]``, a duplicate (case-insensitive), or a fourth tag past MAX_SERVICE_TAGS (3)
257
+ all come back as a 4xx from the tag endpoints, not just a network/auth failure.
258
+ """
259
+
260
+
247
261
  class FailedToValidatePipelineError(Exception):
248
262
  """Raised when the validation request itself failed (e.g. auth/5xx), not the pipeline config."""
249
263
 
@@ -453,6 +467,50 @@ class DeploymentsAPI:
453
467
  )
454
468
  return Deployment.from_response(response.json())
455
469
 
470
+ async def add_tag(self, workspace_name: str, deployment_id: UUID, tag_name: str) -> List[str]:
471
+ """Add a tag to a deployment.
472
+
473
+ :param workspace_name: Name of the workspace.
474
+ :param deployment_id: Deployment id.
475
+ :param tag_name: Tag name (1-50 chars; letters, digits, spaces, underscores, hyphens).
476
+ Rejected as a duplicate case-insensitively, and past the third tag on one deployment.
477
+ :raises FailedToTagDeploymentError: If the tag could not be added.
478
+ :return: The deployment's full tag list after the add.
479
+ """
480
+ response = await self._haystack_enterprise_api.post(
481
+ workspace_name=workspace_name,
482
+ endpoint=f"{self._ENDPOINT}/{deployment_id}/tags",
483
+ json={"name": tag_name},
484
+ )
485
+ raise_for_unexpected_status(
486
+ response,
487
+ (codes.OK,),
488
+ FailedToTagDeploymentError,
489
+ f"Failed to add tag {tag_name!r} to deployment '{deployment_id}'.",
490
+ )
491
+ return list(response.json())
492
+
493
+ async def remove_tag(self, workspace_name: str, deployment_id: UUID, tag_name: str) -> List[str]:
494
+ """Remove a tag from a deployment. Matching is case-insensitive.
495
+
496
+ :param workspace_name: Name of the workspace.
497
+ :param deployment_id: Deployment id.
498
+ :param tag_name: Tag name to remove.
499
+ :raises FailedToTagDeploymentError: If the tag could not be removed (including "not found").
500
+ :return: The deployment's full tag list after the removal.
501
+ """
502
+ response = await self._haystack_enterprise_api.delete(
503
+ workspace_name=workspace_name,
504
+ endpoint=f"{self._ENDPOINT}/{deployment_id}/tags/{tag_name}",
505
+ )
506
+ raise_for_unexpected_status(
507
+ response,
508
+ (codes.OK,),
509
+ FailedToTagDeploymentError,
510
+ f"Failed to remove tag {tag_name!r} from deployment '{deployment_id}'.",
511
+ )
512
+ return list(response.json())
513
+
456
514
  async def validate_pipeline(
457
515
  self,
458
516
  workspace_name: str,
@@ -148,11 +148,9 @@ def raise_for_unexpected_status(
148
148
 
149
149
 
150
150
  def deployment_base_url(api_url: str, workspace_name: str, deployment_id: Any) -> str:
151
- """Build the base URL of a deployment's OpenAI-compatible endpoint.
151
+ """Build the per-deployment base URL (used for per-deployment routes like ``/chat`` and ``/chat-stream``).
152
152
 
153
- The platform serves ``POST <this>/chat/completions`` for a deployment with an active revision. It is
154
- keyed on the deployment id, not the service name. Returned without the ``/chat/completions`` suffix
155
- because that is exactly the ``base_url`` an OpenAI client expects (it appends the path itself).
153
+ Returned without a trailing path segment because callers append their own route (e.g. ``/chat``).
156
154
 
157
155
  :param api_url: Base API URL, already normalized (see :func:`~haystack_enterprise_sdk._api.config.normalize_base_url`).
158
156
  :param workspace_name: Name of the workspace the deployment lives in.
@@ -162,6 +160,20 @@ def deployment_base_url(api_url: str, workspace_name: str, deployment_id: Any) -
162
160
  return f"{api_url}/{API_VERSION_PATH}/workspaces/{workspace_name}/deployments/{deployment_id}"
163
161
 
164
162
 
163
+ def chat_completions_base_url(api_url: str, workspace_name: str) -> str:
164
+ """Build the workspace-scoped OpenAI-compatible chat-completions base URL.
165
+
166
+ The platform's OpenAI-compatible gateway lives at ``POST <this>/chat/completions``.
167
+ The ``model`` field must be ``{workspace_name}/{deployment_id}`` (UUID, not service name).
168
+ Returned without the ``/chat/completions`` suffix so an OpenAI client can use it as ``base_url``.
169
+
170
+ :param api_url: Base API URL, already normalized (see :func:`~haystack_enterprise_sdk._api.config.normalize_base_url`).
171
+ :param workspace_name: Name of the workspace.
172
+ :return: e.g. ``https://api.cloud.deepset.ai/api/v1/workspaces/my-ws/deployments/v1``.
173
+ """
174
+ return f"{api_url}/{API_VERSION_PATH}/workspaces/{workspace_name}/deployments/v1"
175
+
176
+
165
177
  class HaystackEnterpriseAPI:
166
178
  """Haystack Enterprise Platform API client.
167
179
 
@@ -22,6 +22,7 @@ from haystack_enterprise_sdk._api.deployments import (
22
22
  DeploymentsAPI,
23
23
  DeploymentServiceLevel,
24
24
  DeploymentStatus,
25
+ FailedToTagDeploymentError,
25
26
  PipelineValidationError,
26
27
  PipelineValidationResult,
27
28
  )
@@ -147,6 +148,9 @@ class CreateOptions:
147
148
  cpu_limit: Optional[str] = None
148
149
  memory_limit: Optional[str] = None
149
150
  gpu_limit_gigabyte: Optional[int] = None
151
+ # Applied with one add_tag call each, right after creation -- the create-deployment endpoint
152
+ # itself has no `tags` field. Not a sizing field: it applies to serverless services too.
153
+ tags: Tuple[str, ...] = ()
150
154
 
151
155
  _SIZING_FIELDS = (
152
156
  "service_level",
@@ -493,6 +497,38 @@ class DeploymentService:
493
497
  )
494
498
  return await self._deployments.get_deployment(self._workspace_name, deployment.deployment_id)
495
499
 
500
+ async def add_tag(self, service_name: str, tag_name: str) -> List[str]:
501
+ """Add a tag to a service by name, independent of deploy/create.
502
+
503
+ :param service_name: Name of the service deployment.
504
+ :param tag_name: Tag name (1-50 chars; letters, digits, spaces, underscores, hyphens).
505
+ :raises ServiceNotFoundError: If no service with that name exists.
506
+ :raises FailedToTagDeploymentError: If the platform rejected it (limit, duplicate, bad name).
507
+ :return: The service's full tag list after the add.
508
+ """
509
+ deployment = await self._deployments.find_by_name(self._workspace_name, service_name)
510
+ if deployment is None:
511
+ raise ServiceNotFoundError(
512
+ f"No service deployment named '{service_name}' in workspace '{self._workspace_name}'."
513
+ )
514
+ return await self._deployments.add_tag(self._workspace_name, deployment.deployment_id, tag_name)
515
+
516
+ async def remove_tag(self, service_name: str, tag_name: str) -> List[str]:
517
+ """Remove a tag from a service by name. Matching is case-insensitive.
518
+
519
+ :param service_name: Name of the service deployment.
520
+ :param tag_name: Tag name to remove.
521
+ :raises ServiceNotFoundError: If no service with that name exists.
522
+ :raises FailedToTagDeploymentError: If the tag could not be removed (including "not found").
523
+ :return: The service's full tag list after the removal.
524
+ """
525
+ deployment = await self._deployments.find_by_name(self._workspace_name, service_name)
526
+ if deployment is None:
527
+ raise ServiceNotFoundError(
528
+ f"No service deployment named '{service_name}' in workspace '{self._workspace_name}'."
529
+ )
530
+ return await self._deployments.remove_tag(self._workspace_name, deployment.deployment_id, tag_name)
531
+
496
532
  async def create_shared_prototype(
497
533
  self, service_name: str, options: Optional[ShareOptions] = None
498
534
  ) -> SharedPrototype:
@@ -538,7 +574,7 @@ class DeploymentService:
538
574
  )
539
575
  options = create_options or CreateOptions()
540
576
  logger.info("Creating service deployment.", service=service_name, mode=options.deployment_mode.value)
541
- return await self._deployments.create_deployment(
577
+ deployment = await self._deployments.create_deployment(
542
578
  self._workspace_name,
543
579
  name=service_name,
544
580
  deployment_mode=options.deployment_mode,
@@ -550,6 +586,24 @@ class DeploymentService:
550
586
  memory_limit=options.memory_limit,
551
587
  gpu_limit_gigabyte=options.gpu_limit_gigabyte,
552
588
  )
589
+ for tag_name in options.tags:
590
+ # Sequential, not gathered: each call returns the full tag list so far, and the
591
+ # platform's own duplicate/limit checks are per-request -- gathering could race two
592
+ # adds past the 3-tag cap. There are at most 3 of these, so latency is not a concern.
593
+ #
594
+ # Caught here rather than left to propagate: the service was already created, so raising
595
+ # would abort a deploy that otherwise fully succeeded. `tag-add` can retry independently.
596
+ try:
597
+ deployment.tags = await self._deployments.add_tag(
598
+ self._workspace_name, deployment.deployment_id, tag_name
599
+ )
600
+ except FailedToTagDeploymentError:
601
+ logger.warning(
602
+ "Failed to add tag to newly created service; retry with 'tag-add'.",
603
+ service=service_name,
604
+ tag=tag_name,
605
+ )
606
+ return deployment
553
607
 
554
608
  async def _poll_until_settled(
555
609
  self,
@@ -33,6 +33,7 @@ import json
33
33
  import logging
34
34
  import os
35
35
  import sys
36
+ import sysconfig
36
37
  from pathlib import Path
37
38
  from typing import Any, Optional
38
39
 
@@ -53,6 +54,21 @@ _INDEX_MARKER_SUFFIXES = ("DocumentWriter",)
53
54
  # Path components that mark an installed package (even when the venv lives inside the project dir).
54
55
  _INSTALLED_PACKAGE_DIRS = {"site-packages", "dist-packages"}
55
56
 
57
+ # Namespaces the platform runtime provides itself. Never inline these, even when they resolve to a
58
+ # source checkout instead of site-packages (an editable ``haystack-ai`` install, for example).
59
+ _PLATFORM_NAMESPACES = ("haystack", "haystack_integrations", "deepset_cloud_custom_nodes")
60
+
61
+ # Directories owned by the interpreter running this extractor: its stdlib and its installed packages.
62
+ _INTERPRETER_DIRS = frozenset(
63
+ Path(p).resolve()
64
+ for p in (
65
+ sysconfig.get_paths()["stdlib"],
66
+ sysconfig.get_paths()["platstdlib"],
67
+ sys.prefix,
68
+ sys.base_prefix,
69
+ )
70
+ )
71
+
56
72
 
57
73
  class PipelineTransformError(Exception):
58
74
  """Raised when the pipeline cannot be loaded or transformed into deployable YAML."""
@@ -277,9 +293,20 @@ def _reject_index_pipeline(pipeline: Any) -> None:
277
293
  # Import classification
278
294
  # --------------------------------------------------------------------------- #
279
295
  def classify_module(module_name: str, project_root: Path) -> str:
280
- """Classify ``module_name`` as ``'local'``, ``'external'``, or ``'stdlib'``."""
296
+ """Classify ``module_name`` as ``'local'``, ``'external'``, or ``'stdlib'``.
297
+
298
+ Only ``'local'`` is acted on (it means "inline this into a platform Code component"); the other two
299
+ are equivalent "leave it alone" answers.
300
+ """
281
301
  if not module_name:
282
302
  return "external"
303
+ top = module_name.partition(".")[0]
304
+ # Decided by name, not by path: these must never be inlined even when they resolve to a source
305
+ # checkout (a vendored stdlib module, or an editable ``haystack-ai`` install).
306
+ if top in sys.stdlib_module_names:
307
+ return "stdlib"
308
+ if top in _PLATFORM_NAMESPACES:
309
+ return "external"
283
310
  try:
284
311
  spec = importlib.util.find_spec(module_name)
285
312
  except (ImportError, ValueError, ModuleNotFoundError, AttributeError):
@@ -299,12 +326,19 @@ def _classify_origin(origin_path: Path, project_root: Path) -> str:
299
326
  virtualenv often lives *inside* the project directory (e.g. ``<project>/.venv/.../site-packages``),
300
327
  so installed packages resolve under ``project_root`` yet must be treated as external dependencies,
301
328
  not inlined as local source.
329
+
330
+ Anything that is neither installed nor owned by the interpreter is the user's own source, wherever
331
+ it sits on disk. ``project_root`` (the pipeline file's directory) is only a fast positive hint:
332
+ requiring containment in it would miss the standard ``src``-layout repo, where the pipeline lives in
333
+ ``pipelines/`` and its components in ``src/``.
302
334
  """
303
335
  if _INSTALLED_PACKAGE_DIRS.intersection(origin_path.parts):
304
336
  return "external"
305
337
  if _is_relative_to(origin_path, project_root):
306
338
  return "local"
307
- return "stdlib"
339
+ if any(_is_relative_to(origin_path, d) for d in _INTERPRETER_DIRS):
340
+ return "stdlib"
341
+ return "local"
308
342
 
309
343
 
310
344
  def _is_relative_to(path: Path, root: Path) -> bool:
@@ -1599,6 +1633,17 @@ def extract_from_pipeline(pipeline: Any, project_root: Path) -> dict:
1599
1633
  comp_type = comp.get("type", "")
1600
1634
  module_name = comp_type.rpartition(".")[0]
1601
1635
  if classify_module(module_name, project_root) != "local":
1636
+ if not comp_type.startswith(_PLATFORM_NAMESPACES):
1637
+ # Can't be inlined and isn't something the platform ships. Most likely first-party code
1638
+ # deployed from an installed copy (``pip install ./src``), which we cannot tell apart
1639
+ # from a third-party integration - so say so rather than fail on the platform later.
1640
+ logger.warning(
1641
+ "Component '%s' keeps its original type '%s': it did not resolve to local source, so "
1642
+ "the platform must be able to import it. If it is your own code, deploy from the "
1643
+ "source tree rather than from an installed copy.",
1644
+ comp_name,
1645
+ comp_type,
1646
+ )
1602
1647
  continue
1603
1648
  logger.debug("Rewriting local component '%s' (%s) to Code", comp_name, comp_type)
1604
1649
  code = _build_code_block(comp_type, project_root)
@@ -27,6 +27,7 @@ from haystack_enterprise_sdk._api.config import (
27
27
  from haystack_enterprise_sdk._api.deployments import (
28
28
  DeploymentMode,
29
29
  DeploymentServiceLevel,
30
+ FailedToTagDeploymentError,
30
31
  PipelineValidationError,
31
32
  )
32
33
  from haystack_enterprise_sdk._api.haystack_enterprise_api import HaystackEnterpriseAPIError
@@ -449,6 +450,9 @@ def deploy( # pylint: disable=too-many-arguments,too-many-locals
449
450
  memory: Optional[str] = None,
450
451
  gpu: Optional[int] = None,
451
452
  idle_timeout: Optional[int] = None,
453
+ tag: Optional[List[str]] = typer.Option(
454
+ None, "--tag", help="Tag to add when creating the service (repeatable; up to 3)."
455
+ ),
452
456
  python: Optional[str] = None,
453
457
  dry_run: bool = False,
454
458
  output: Optional[Path] = None,
@@ -492,6 +496,11 @@ def deploy( # pylint: disable=too-many-arguments,too-many-locals
492
496
  :param memory: Memory limit, e.g. '2Gi' (with --managed).
493
497
  :param gpu: GPU memory limit in gigabytes (with --managed).
494
498
  :param idle_timeout: Idle timeout in seconds before scale-down (with --managed).
499
+ :param tag: Tag to add to the service when it is created (repeatable; the platform allows at
500
+ most 3 per service). Only applies when the service is created -- pass --create, or omit
501
+ --create against a service name that does not exist yet. Rejected if the service already
502
+ exists, same as --managed and the sizing flags -- use `tag-add`/`tag-remove` to change
503
+ tags on a service after creation.
495
504
  :param python: Path to the Python interpreter used to load your pipeline (defaults to an
496
505
  auto-detected virtualenv near the target file, else the current interpreter).
497
506
  :param dry_run: Transform the pipeline and print/write the resulting YAML without deploying. No
@@ -524,6 +533,9 @@ def deploy( # pylint: disable=too-many-arguments,too-many-locals
524
533
  Create a managed service with explicit sizing (activates and waits for the rollout):
525
534
  `haystack-enterprise deploy pipeline.py my-service --managed --service-level PRODUCTION --cpu 2`
526
535
 
536
+ Create a service tagged for a team and a hackathon, up to 3 tags:
537
+ `haystack-enterprise deploy pipeline.py my-service --create --tag team-success --tag hackathon`
538
+
527
539
  Describe the revision instead of using the auto-generated comment:
528
540
  `haystack-enterprise deploy pipeline.py my-service -m "Bump embedder model to bge-large"`
529
541
 
@@ -575,8 +587,8 @@ def deploy( # pylint: disable=too-many-arguments,too-many-locals
575
587
  if create is False and existing is None:
576
588
  typer.echo(f"No service named '{service_name}' in workspace '{workspace_name}'. Drop --no-create to create it.")
577
589
  raise typer.Exit(1)
578
- if existing is not None and (managed or given_sizing):
579
- creation_flags = (["--managed"] if managed else []) + given_sizing
590
+ if existing is not None and (managed or given_sizing or tag):
591
+ creation_flags = (["--managed"] if managed else []) + given_sizing + (["--tag"] if tag else [])
580
592
  applies = "only applies" if len(creation_flags) == 1 else "only apply"
581
593
  typer.echo(
582
594
  f"Service '{service_name}' already exists; {', '.join(creation_flags)} {applies} when a "
@@ -596,9 +608,10 @@ def deploy( # pylint: disable=too-many-arguments,too-many-locals
596
608
  cpu_limit=cpu,
597
609
  memory_limit=memory,
598
610
  gpu_limit_gigabyte=gpu,
611
+ tags=tuple(tag or ()),
599
612
  )
600
613
  else:
601
- create_options = CreateOptions(deployment_mode=DeploymentMode.SERVERLESS)
614
+ create_options = CreateOptions(deployment_mode=DeploymentMode.SERVERLESS, tags=tuple(tag or ()))
602
615
 
603
616
  activate = not skip_activation
604
617
 
@@ -1152,8 +1165,8 @@ def _echo_endpoint(client: DeploymentClient, service_name: str, result: DeployRe
1152
1165
  if not result.is_deployed:
1153
1166
  return
1154
1167
 
1155
- base_url = client.deployment_base_url(result.deployment.deployment_id)
1156
- model = f"{client.workspace_name}/{service_name}"
1168
+ base_url = client.chat_completions_base_url()
1169
+ model = f"{client.workspace_name}/{result.deployment.deployment_id}"
1157
1170
  typer.echo(f"\nChat completions endpoint (OpenAI-compatible):\n POST {base_url}/chat/completions")
1158
1171
  typer.echo(
1159
1172
  f"\n curl -N {base_url}/chat/completions \\\n"
@@ -1649,12 +1662,70 @@ def service_status(
1649
1662
  "pending_revision_id": (
1650
1663
  str(deployment.pending_revision_id) if deployment.pending_revision_id else None
1651
1664
  ),
1665
+ "tags": deployment.tags,
1652
1666
  },
1653
1667
  indent=4,
1654
1668
  )
1655
1669
  )
1656
1670
 
1657
1671
 
1672
+ @cli_app.command()
1673
+ def tag_add(
1674
+ service_name: str,
1675
+ tag_name: str,
1676
+ api_key: Optional[str] = None,
1677
+ api_url: Optional[str] = None,
1678
+ workspace_name: str = DEFAULT_WORKSPACE_NAME,
1679
+ ) -> None:
1680
+ """Add a tag to a service deployment, independent of deploy/create.
1681
+
1682
+ :param service_name: Name of the service deployment.
1683
+ :param tag_name: Tag name (1-50 chars; letters, digits, spaces, underscores, hyphens).
1684
+ Rejected as a duplicate case-insensitively, and past the third tag on one service.
1685
+ :param api_key: deepset API key to use for authentication.
1686
+ :param api_url: API URL to use for authentication.
1687
+ :param workspace_name: Workspace of the service. Uses the workspace from the .ENV file by default.
1688
+
1689
+ Example:
1690
+ `haystack-enterprise tag-add my-service hackathon`
1691
+ """
1692
+ client = DeploymentClient(api_key=api_key, api_url=api_url, workspace_name=workspace_name)
1693
+ try:
1694
+ tags = client.add_tag(service_name, tag_name)
1695
+ except (ServiceNotFoundError, FailedToTagDeploymentError) as err:
1696
+ typer.echo(str(err))
1697
+ raise typer.Exit(1) # noqa: B904
1698
+ typer.echo(f"Tags on '{service_name}': {', '.join(tags) or '(none)'}")
1699
+
1700
+
1701
+ @cli_app.command()
1702
+ def tag_remove(
1703
+ service_name: str,
1704
+ tag_name: str,
1705
+ api_key: Optional[str] = None,
1706
+ api_url: Optional[str] = None,
1707
+ workspace_name: str = DEFAULT_WORKSPACE_NAME,
1708
+ ) -> None:
1709
+ """Remove a tag from a service deployment. Matching is case-insensitive.
1710
+
1711
+ :param service_name: Name of the service deployment.
1712
+ :param tag_name: Tag name to remove.
1713
+ :param api_key: deepset API key to use for authentication.
1714
+ :param api_url: API URL to use for authentication.
1715
+ :param workspace_name: Workspace of the service. Uses the workspace from the .ENV file by default.
1716
+
1717
+ Example:
1718
+ `haystack-enterprise tag-remove my-service hackathon`
1719
+ """
1720
+ client = DeploymentClient(api_key=api_key, api_url=api_url, workspace_name=workspace_name)
1721
+ try:
1722
+ tags = client.remove_tag(service_name, tag_name)
1723
+ except (ServiceNotFoundError, FailedToTagDeploymentError) as err:
1724
+ typer.echo(str(err))
1725
+ raise typer.Exit(1) # noqa: B904
1726
+ typer.echo(f"Tags on '{service_name}': {', '.join(tags) or '(none)'}")
1727
+
1728
+
1658
1729
  def version_callback(value: bool) -> None:
1659
1730
  """Show the SDK version and exit.
1660
1731
 
@@ -19,6 +19,7 @@ from haystack_enterprise_sdk._api.deployments import (
19
19
  )
20
20
  from haystack_enterprise_sdk._api.haystack_enterprise_api import (
21
21
  HaystackEnterpriseAPI,
22
+ chat_completions_base_url,
22
23
  deployment_base_url,
23
24
  )
24
25
  from haystack_enterprise_sdk._api.pipeline_run import DEFAULT_RUN_RETRIES, OnRetry
@@ -76,16 +77,23 @@ class AsyncDeploymentClient:
76
77
  return self._workspace_name
77
78
 
78
79
  def deployment_base_url(self, deployment_id: Any) -> str:
79
- """The OpenAI-compatible base URL of a deployment in this client's workspace.
80
-
81
- Append ``/chat/completions`` to call it directly, or hand it to an OpenAI client as ``base_url``.
82
- Only usable once the deployment has an active revision.
80
+ """The per-deployment base URL (for per-deployment routes like ``/chat`` and ``/chat-stream``).
83
81
 
84
82
  :param deployment_id: Id of the deployment (e.g. ``DeployResult.deployment.deployment_id``).
85
83
  :return: The deployment's base URL.
86
84
  """
87
85
  return deployment_base_url(self._api_config.api_url, self._workspace_name, deployment_id)
88
86
 
87
+ def chat_completions_base_url(self) -> str:
88
+ """The workspace-scoped OpenAI-compatible base URL for chat completions.
89
+
90
+ Use ``POST <this>/chat/completions`` with ``model`` set to ``{workspace}/{deployment_id}`` (UUID).
91
+ Pass this as ``base_url`` to an OpenAI client.
92
+
93
+ :return: e.g. ``https://api.cloud.deepset.ai/api/v1/workspaces/my-ws/deployments/v1``.
94
+ """
95
+ return chat_completions_base_url(self._api_config.api_url, self._workspace_name)
96
+
89
97
  @asynccontextmanager
90
98
  async def _service(self) -> AsyncIterator[DeploymentService]:
91
99
  """Yield a :class:`DeploymentService` backed by a managed API client."""
@@ -210,6 +218,20 @@ class AsyncDeploymentClient:
210
218
  async with self._service() as service:
211
219
  return await service.get_service_status(service_name)
212
220
 
221
+ async def add_tag(self, service_name: str, tag_name: str) -> List[str]:
222
+ """Add a tag to a service by name, independent of deploy/create. See
223
+ :meth:`haystack_enterprise_sdk._service.deployment_service.DeploymentService.add_tag`.
224
+ """
225
+ async with self._service() as service:
226
+ return await service.add_tag(service_name, tag_name)
227
+
228
+ async def remove_tag(self, service_name: str, tag_name: str) -> List[str]:
229
+ """Remove a tag from a service by name. See
230
+ :meth:`haystack_enterprise_sdk._service.deployment_service.DeploymentService.remove_tag`.
231
+ """
232
+ async with self._service() as service:
233
+ return await service.remove_tag(service_name, tag_name)
234
+
213
235
  async def create_shared_prototype(
214
236
  self, service_name: str, options: Optional[ShareOptions] = None
215
237
  ) -> SharedPrototype:
@@ -183,6 +183,14 @@ class DeploymentClient: # pylint: disable=too-few-public-methods
183
183
  """Return the current deployment (with live runtime status) for ``service_name``."""
184
184
  return _run(self._async_client.get_service_status(service_name))
185
185
 
186
+ def add_tag(self, service_name: str, tag_name: str) -> List[str]:
187
+ """Add a tag to a service by name, independent of deploy/create, synchronously."""
188
+ return _run(self._async_client.add_tag(service_name, tag_name))
189
+
190
+ def remove_tag(self, service_name: str, tag_name: str) -> List[str]:
191
+ """Remove a tag from a service by name synchronously."""
192
+ return _run(self._async_client.remove_tag(service_name, tag_name))
193
+
186
194
  def create_shared_prototype(self, service_name: str, options: Optional[ShareOptions] = None) -> SharedPrototype:
187
195
  """Create a shared prototype (a shareable chat UI link) for a deployed service synchronously."""
188
196
  return _run(self._async_client.create_shared_prototype(service_name, options))
@@ -193,5 +201,9 @@ class DeploymentClient: # pylint: disable=too-few-public-methods
193
201
  return self._async_client.workspace_name
194
202
 
195
203
  def deployment_base_url(self, deployment_id: Any) -> str:
196
- """The OpenAI-compatible base URL of a deployment. See :meth:`AsyncDeploymentClient.deployment_base_url`."""
204
+ """The per-deployment base URL. See :meth:`AsyncDeploymentClient.deployment_base_url`."""
197
205
  return self._async_client.deployment_base_url(deployment_id)
206
+
207
+ def chat_completions_base_url(self) -> str:
208
+ """The workspace-scoped chat-completions base URL. See :meth:`AsyncDeploymentClient.chat_completions_base_url`."""
209
+ return self._async_client.chat_completions_base_url()
@@ -1,18 +1,29 @@
1
1
  [build-system]
2
- requires = ["uv_build>=0.9.13,<0.12.0"]
2
+ requires = ["uv_build>=0.9.13,<0.13.0"]
3
3
  build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "haystack-enterprise-sdk"
7
- version = "0.1.0"
8
- description = "Haystack Enterprise SDK"
7
+ version = "0.2.0"
8
+ description = "Python SDK and CLI for the Haystack Enterprise Platform"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
11
11
  license = "Apache-2.0"
12
12
  license-files = ["LICENSE"]
13
- keywords = []
13
+ keywords = [
14
+ "agents",
15
+ "deepset",
16
+ "haystack",
17
+ "llm",
18
+ "pipelines",
19
+ "rag",
20
+ "sdk",
21
+ ]
14
22
  classifiers = [
15
23
  "Development Status :: 4 - Beta",
24
+ "Environment :: Console",
25
+ "Intended Audience :: Developers",
26
+ "Operating System :: OS Independent",
16
27
  "Programming Language :: Python",
17
28
  "Programming Language :: Python :: 3",
18
29
  "Programming Language :: Python :: 3.10",
@@ -20,6 +31,8 @@ classifiers = [
20
31
  "Programming Language :: Python :: 3.12",
21
32
  "Programming Language :: Python :: 3.13",
22
33
  "Programming Language :: Python :: 3.14",
34
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
35
+ "Typing :: Typed",
23
36
  ]
24
37
  dependencies = [
25
38
  "structlog>=24.0.0",
@@ -66,7 +79,7 @@ code-quality = [
66
79
  "ruff==0.15.12",
67
80
  "mypy==1.19.1",
68
81
  "pre-commit==4.3.0",
69
- "types-aiofiles==25.1.0.20251011",
82
+ "types-aiofiles==25.1.0.20260518",
70
83
  "types-tabulate==0.9.0.20241207",
71
84
  ]
72
85
 
@@ -1,19 +1,30 @@
1
1
  [build-system]
2
- requires = ["uv_build>=0.9.13,<0.12.0"]
2
+ requires = ["uv_build>=0.9.13,<0.13.0"]
3
3
  build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "haystack-enterprise-sdk"
7
- version = "0.1.0"
8
- description = 'Haystack Enterprise SDK'
7
+ version = "0.2.0"
8
+ description = "Python SDK and CLI for the Haystack Enterprise Platform"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
11
11
  license = "Apache-2.0"
12
12
  license-files = ["LICENSE"]
13
- keywords = []
13
+ keywords = [
14
+ "agents",
15
+ "deepset",
16
+ "haystack",
17
+ "llm",
18
+ "pipelines",
19
+ "rag",
20
+ "sdk",
21
+ ]
14
22
  authors = [{ name = "deepset.ai", email = "malte.pietsch@deepset.ai" }]
15
23
  classifiers = [
16
24
  "Development Status :: 4 - Beta",
25
+ "Environment :: Console",
26
+ "Intended Audience :: Developers",
27
+ "Operating System :: OS Independent",
17
28
  "Programming Language :: Python",
18
29
  "Programming Language :: Python :: 3",
19
30
  "Programming Language :: Python :: 3.10",
@@ -21,6 +32,8 @@ classifiers = [
21
32
  "Programming Language :: Python :: 3.12",
22
33
  "Programming Language :: Python :: 3.13",
23
34
  "Programming Language :: Python :: 3.14",
35
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
36
+ "Typing :: Typed",
24
37
  ]
25
38
  dependencies = [
26
39
  "structlog>=24.0.0",
@@ -70,7 +83,7 @@ code-quality = [
70
83
  "ruff==0.15.12",
71
84
  "mypy==1.19.1",
72
85
  "pre-commit==4.3.0",
73
- "types-aiofiles==25.1.0.20251011",
86
+ "types-aiofiles==25.1.0.20260518",
74
87
  "types-tabulate==0.9.0.20241207",
75
88
  ]
76
89