From 03b3d1f21ec48578e4c1f97b545f56283ee0e269 Mon Sep 17 00:00:00 2001 From: Kevin REMY Date: Thu, 18 Jun 2026 17:42:13 +0200 Subject: [PATCH 1/4] 14.7.0 Publish on PyPI (#360) * Sync sources with main repository * Release metadata --- HISTORY.txt | 5 + dataikuapi/dss/cobuild.py | 317 ++++++++++++++++++++++++++++++++++++++ dataikuapi/dss/ml.py | 1 + dataikuapi/dss/project.py | 49 +++++- setup.py | 2 +- 5 files changed, 372 insertions(+), 2 deletions(-) create mode 100644 dataikuapi/dss/cobuild.py diff --git a/HISTORY.txt b/HISTORY.txt index a07e9b2e..f999ff24 100644 --- a/HISTORY.txt +++ b/HISTORY.txt @@ -2,6 +2,11 @@ Changelog ========== +14.7.0 (2026-06-18) +------------------- + +* Initial release for DSS 14.7.0 + 14.6.2 (2026-06-15) ------------------- diff --git a/dataikuapi/dss/cobuild.py b/dataikuapi/dss/cobuild.py new file mode 100644 index 00000000..5c04550d --- /dev/null +++ b/dataikuapi/dss/cobuild.py @@ -0,0 +1,317 @@ +from dataikuapi.dss.dataset import DSSDataset +from dataikuapi.dss.evaluationstore import DSSEvaluationStore +from dataikuapi.dss.knowledgebank import DSSKnowledgeBank +from dataikuapi.dss.labeling_task import DSSLabelingTask +from dataikuapi.dss.managedfolder import DSSManagedFolder +from dataikuapi.dss.recipe import DSSRecipe +from dataikuapi.dss.savedmodel import DSSSavedModel +from dataikuapi.dss.streaming_endpoint import DSSStreamingEndpoint + + +class CobuildMessage(object): + def __init__(self, raw): + self._raw = raw + + @property + def message(self): + """ + The text content of the response. + + :rtype: str or None + """ + return self._raw.get("message") + + +class CobuildAssistantResponse(CobuildMessage): + """ + A response from the Cobuild AI assistant. + + .. important:: + Do not create this class directly, it is returned by :meth:`DSSCobuildConversation.send_message` + and :meth:`DSSCobuildConversation.answer_confirmation`. + """ + + @property + def role(self): + """ + Role of the message sender ("user" or "assistant") + + :rtype: str + """ + return "assistant" + + @property + def type(self): + """ + Type of the response: ``"assistant_message"``, ``"delete_confirmation_request"``, + or ``"error"``. + + - ``"assistant_message"``: the assistant has completed its turn; the conversation is idle. + - ``"delete_confirmation_request"``: the assistant is requesting confirmation before + deleting objects. Call :meth:`DSSCobuildConversation.answer_confirmation` + with a choice of ``"APPROVE"`` or ``"CANCEL"`` to continue. + - ``"error"``: an error occurred while processing the Cobuild turn. In the public API, + missing tool permissions are reported through this error response too. + + :rtype: str + """ + return self._raw.get("type") + + @property + def is_question(self): + """ + DEPRECATED ... DEPRECATED ... DEPRECATED ... DEPRECATED + Whether the assistant is asking the user a question. If ``True``, call + :meth:`DSSCobuildConversation.send_message` again with the answer to continue. + + :rtype: bool + """ + return False + + @property + def is_confirmation_request(self): + """ + Whether the assistant is requesting confirmation before deleting objects + or doing other operations requiring confirmation. + If ``True``, call :meth:`DSSCobuildConversation.answer_confirmation` with a choice of + ``"APPROVE"`` or ``"CANCEL"`` to continue. + If the confirmation is about a deletion, inspect :attr:`objects_to_delete` + and :attr:`deletion_impacts` for details. + + :rtype: bool + """ + return self.type == "delete_confirmation_request" + + @property + def is_error(self): + """ + Whether the response represents an error. + + :rtype: bool + """ + return self._raw.get("error", False) + + @property + def objects_to_delete(self): + """ + For ``"delete_confirmation_request"`` responses: the list of objects that the assistant + is requesting permission to delete (or unshare). Each entry is a dict with fields + ``projectKey``, ``type``, ``id``, and ``displayName``. + + ``None`` for other response types. + + :rtype: list of dict or None + """ + return self._raw.get("objectsToDelete") + + @property + def deletion_impacts(self): + """ + For ``"delete_confirmation_request"`` responses: a dict describing the cascading effects + of the requested deletion (recipes that would be deleted, datasets left unchanged, etc.). + + ``None`` for other response types. + + :rtype: dict or None + """ + return self._raw.get("deletionImpacts") + + def __repr__(self): + return "CobuildAssistantResponse(type=%r, message=%r)" % (self.type, self.message) + + +class CobuildUserMessage(CobuildMessage): + """ + A message sent to Cobuild AI assistant by the user. + + .. important:: + Do not create this class directly, it is automatically created when using :meth:`dataikuapi.dss.project.DSSProject.new_cobuild_conversation`, + :meth:`DSSCobuildConversation.send_message`, and + :meth:`DSSCobuildConversation.answer_confirmation`. + """ + + @property + def role(self): + """ + Role of the message sender ("user" or "assistant") + + :rtype: str + """ + return "user" + + @property + def type(self): + """ + Type of the message: ``"request"`` or ``"delete_confirmation_response"``. + + - ``"request"``: a normal message sent to cobuild. + - ``"delete_confirmation_response"``: user response when cobuild requests confirmation before deleting objects + + :rtype: str + """ + return self._raw.get("type", "request") + + @property + def is_confirmation_response(self): + """ + Whether this message is the answer for assistant requesting confirmation before deleting objects + + :rtype: bool + """ + return self.type == "delete_confirmation_response" + + def __repr__(self): + return "CobuildUserMessage(type=%r, message=%r)" % (self.type, self.message) + +def _DSS_objects_to_selected(project_key, objects): + return [_DSS_object_to_selected(project_key, object) for object in objects] + +def _DSS_object_to_selected(project_key, object): + prefix = "" if project_key == object.project_key else object.project_key + "." + + if isinstance(object, DSSDataset): + return { "type": "DATASET", "id": prefix + object.id} + if isinstance(object, DSSRecipe): + return { "type": "RECIPE", "id": prefix + object.id} + if isinstance(object, DSSManagedFolder): + return { "type": "MANAGED_FOLDER", "id": prefix + object.id} + if isinstance(object, DSSSavedModel): + return { "type": "SAVED_MODEL", "id": prefix + object.id} + if isinstance(object, DSSStreamingEndpoint): + return { "type": "STREAMING_ENDPOINT", "id": prefix + object.id} + if isinstance(object, DSSLabelingTask): + return { "type": "LABELING_TASK", "id": prefix + object.id} + if isinstance(object, DSSKnowledgeBank): + return { "type": "RETRIEVABLE_KNOWLEDGE", "id": prefix + object.id} + if isinstance(object, DSSEvaluationStore): + return { "type": "MODEL_EVALUATION_STORE", "id": prefix + object.id} + else: + raise ValueError("Unsupported object type") + + +class DSSCobuildConversation(object): + """ + A handle to a conversation with Cobuild AI assistant. + + .. important:: + Do not create this class directly, instead use + :meth:`dataikuapi.dss.project.DSSProject.new_cobuild_conversation`. + + The :attr:`messages` property accumulates all exchanges made through this handle (user inputs + and assistant responses). + + A typical interaction:: + + conv = project.new_cobuild_conversation() + response = conv.send_message("List the datasets in this project") + print(response.message) + + conv.send_message("Now filter the Orders dataset to keep only orders with an amount > 1000") + print(conv.messages[-1].message) + + When the assistant needs to delete objects, it first asks for confirmation:: + + response = conv.send_message("Delete the Orders dataset") + if response.is_confirmation_request: + print("Objects to delete:", response.objects_to_delete) + response = conv.answer_confirmation("APPROVE") + print(response.message) + + If a tool requires edit permission, pass ``allow_edit_project=True`` to + :meth:`send_message` to allow Cobuild to create and edit objects for that message. + """ + + def __init__(self, client, project_key, conversation_id, selected_objects=None): + self.client = client + self.project_key = project_key + self.conversation_id = conversation_id + self._messages = [] + self._pending_confirmation_id = None + self._selected_objects = selected_objects + + @property + def messages(self): + """ + All messages exchanged during this conversation, in order. + + User message entries are :class:`CobuildUserMessage`. + + Assistant message entries are :class:`CobuildAssistantResponse`. + + :rtype: list of :class:`CobuildAssistantResponse` or :class:`CobuildUserMessage` + """ + return list(self._messages) + + def send_message(self, message, selected_objects=None, allow_edit_project=False): + """ + Send a message to the assistant and wait for its response. + + The message and assistant response are appended to :attr:`messages`. + + :param str message: the message to send + :param selected_objects: object selection the assistant should focus on. It is reused for any subsequent message, unless overwritten + :type selected_objects: list[:class:`.DSSDataset`, :class:`.DSSRecipe`, :class:`.DSSLabelingTask`, :class:`.DSSManagedFolder`, :class:`.DSSSavedModel`, :class:`.DSSKnowledgeBank`, :class:`.DSSModelEvaluationStore` or :class:`.DSSStreamingEndpoint`] + :param bool allow_edit_project: whether to allow Cobuild to create and edit everything + needed in this project to follow this message. This permission applies only to this + message. + + :returns: the assistant's response + :rtype: :class:`CobuildAssistantResponse` + """ + if selected_objects is not None: + self._selected_objects = _DSS_objects_to_selected(self.project_key, selected_objects) + + self._messages.append(CobuildUserMessage({"type": "request", "message": message, "selected_objects": self._selected_objects})) + raw = self.client._perform_json( + "POST", + "/projects/%s/cobuild/conversations/%s/messages" % (self.project_key, self.conversation_id), + body={ + "message": message, + "selectedObjects": self._selected_objects or [], + "allowEditProject": allow_edit_project, + }, + ) + response = CobuildAssistantResponse(raw) + self._pending_confirmation_id = raw["confirmationId"] if response.is_confirmation_request else None + self._messages.append(response) + return response + + def answer_confirmation(self, choice, options=None): + """ + Answer a pending confirmation request and wait for the assistant's next response. + + Call this after receiving a response with :attr:`~CobuildAssistantResponse.is_confirmation_request` + set to ``True``. + + The choice and assistant response are appended to :attr:`messages`. + + :param str choice: ``"APPROVE"`` to proceed with the operation, or ``"CANCEL"`` to abort it + :param list options: optional list of per-object deletion options (advanced use, for delete + confirmations only). Each entry is a dict with ``projectKey``, ``type``, ``id``, and an + ``options`` dict containing ``dropData``, ``dropMetastoreTable``, and + ``deleteOrphanInsights`` booleans. When omitted, all options default to ``False`` + (data is not dropped, metastore tables are not dropped, orphan insights are not deleted). + + :returns: the assistant's response after the confirmation + :rtype: :class:`CobuildAssistantResponse` + """ + if choice not in ("APPROVE", "CANCEL"): + raise ValueError("choice must be 'APPROVE' or 'CANCEL', got %r" % choice) + if self._pending_confirmation_id is None: + raise ValueError("No pending confirmation request. Call send_message first and check is_confirmation_request.") + + confirmation_id = self._pending_confirmation_id + self._pending_confirmation_id = None + self._messages.append(CobuildUserMessage({"type": "delete_confirmation_response", "message": choice})) + body = {"choice": choice} + if options is not None: + body["options"] = options + raw = self.client._perform_json( + "POST", + "/projects/%s/cobuild/conversations/%s/confirmation/%s" % (self.project_key, self.conversation_id, confirmation_id), + body=body, + ) + response = CobuildAssistantResponse(raw) + self._pending_confirmation_id = raw["confirmationId"] if response.is_confirmation_request else None + self._messages.append(response) + return response diff --git a/dataikuapi/dss/ml.py b/dataikuapi/dss/ml.py index 1d5f60b2..ff6ba8be 100644 --- a/dataikuapi/dss/ml.py +++ b/dataikuapi/dss/ml.py @@ -3433,6 +3433,7 @@ def expected_max(self, expected_max): class DSSMLAssertionsMetrics(object): """ Object that represents the assertions metrics for all assertions on a trained model + .. important:: Do not create this object directly, use :meth:`DSSTrainedPredictionModelDetails.get_assertions_metrics` instead """ diff --git a/dataikuapi/dss/project.py b/dataikuapi/dss/project.py index fe826f69..f6081edf 100644 --- a/dataikuapi/dss/project.py +++ b/dataikuapi/dss/project.py @@ -49,6 +49,9 @@ from .webapp import DSSWebApp, DSSWebAppListItem from .wiki import DSSWiki from ..dss_plugin_mlflow import MLflowHandle +from .agent_review import DSSAgentReview, DSSAgentReviewListItem +from .cobuild import _DSS_objects_to_selected, CobuildUserMessage, DSSCobuildConversation, CobuildAssistantResponse + logger = logging.getLogger(__name__) @@ -3210,6 +3213,47 @@ def create_insight(self, creation_info): body = {"insightPrototype": creation_info})['id'] return DSSInsight(self.client, self.project_key, insight_id) + ######################################################## + # Cobuild + ######################################################## + + def new_cobuild_conversation(self): + """ + Start a new empty Cobuild conversation. + + Cobuild is an AI assistant that can inspect and build Flows, recipes, datasets, + dashboards, and other DSS objects. + + Example usage:: + + project = client.get_project("MY_PROJECT") + conv = project.new_cobuild_conversation() + + response = conv.send_message( + "Update wiki article 123 to add a one-line banner", + allow_edit_project=True, + ) + print(response.message) + + Without ``allow_edit_project=True``, Cobuild asks before using tools that create or edit + objects. In the public API, missing permissions are returned as an error response:: + + project = client.get_project("MY_PROJECT") + conv = project.new_cobuild_conversation() + + response = conv.send_message("Update wiki article 123 to add a one-line banner") + print(response.message) + + :returns: a handle to the newly created empty conversation + :rtype: :class:`dataikuapi.dss.cobuild.DSSCobuildConversation` + """ + raw = self.client._perform_json( + "POST", + "/projects/%s/cobuild/conversations" % self.project_key, + body={}, + ) + return DSSCobuildConversation(self.client, self.project_key, raw["conversationId"]) + ######################################################## # Git ######################################################## @@ -3618,11 +3662,14 @@ def get_status(self): """ Get the current state of the project's git repository. - :return: A dict containing the following keys: + :return: + A dict containing the following keys: + - **currentBranch** (*str*): The currently checked-out Git branch. - **remotes** (*list*): A list of configured remotes, each being a dict with: - **name** (*str*): The remote name (e.g. "origin"). - **url** (*str*): The remote repository URL. + - **trackingCount** (*dict*): The number of commits the local branch is ahead/behind its tracked remote branch. - **clean** (*bool*): Whether the working directory is clean (no changes). - **hasUncommittedChanges** (*bool*): Whether there are uncommitted changes. diff --git a/setup.py b/setup.py index 5a89359c..54b558b9 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup -VERSION = "14.6.2" +VERSION = "14.7.0" long_description = (open('README').read() + '\n\n' + open('HISTORY.txt').read()) From 6dff1c8510183ba4a29e452600fab4277f07f09c Mon Sep 17 00:00:00 2001 From: Kevin REMY Date: Mon, 13 Jul 2026 11:35:09 +0200 Subject: [PATCH 2/4] 14.7.1 - Publish on PyPI (#362) --- HISTORY.txt | 5 +++++ setup.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/HISTORY.txt b/HISTORY.txt index f999ff24..daf868bb 100644 --- a/HISTORY.txt +++ b/HISTORY.txt @@ -2,6 +2,11 @@ Changelog ========== +14.7.1 (2026-07-13) +------------------- + +* Initial release for DSS 14.7.1 + 14.7.0 (2026-06-18) ------------------- diff --git a/setup.py b/setup.py index 54b558b9..9f6863fe 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup -VERSION = "14.7.0" +VERSION = "14.7.1" long_description = (open('README').read() + '\n\n' + open('HISTORY.txt').read()) From 39418a3212d4e4a15726d43f6b545a9620e0d6d0 Mon Sep 17 00:00:00 2001 From: Kevin REMY Date: Mon, 13 Jul 2026 11:49:45 +0200 Subject: [PATCH 3/4] 14.7.2 publish on PyPI (#363) * Sync sources with main repository * Release metadata --- HISTORY.txt | 5 + dataikuapi/dss/agent.py | 45 ++++++ dataikuapi/dss/cobuild.py | 172 +++++++++++++++++++++- dataikuapi/dss/knowledgebank.py | 15 +- dataikuapi/dss/llm_utils/__init__.py | 3 + dataikuapi/dss/project.py | 17 +++ dataikuapi/dss/recipe.py | 70 +++++---- dataikuapi/dss/retrieval_augmented_llm.py | 45 ++++++ setup.py | 2 +- 9 files changed, 338 insertions(+), 36 deletions(-) diff --git a/HISTORY.txt b/HISTORY.txt index daf868bb..c86c43c2 100644 --- a/HISTORY.txt +++ b/HISTORY.txt @@ -2,6 +2,11 @@ Changelog ========== +14.7.2 (2026-07-13) +------------------- + +* Initial release for DSS 14.7.2 + 14.7.1 (2026-07-13) ------------------- diff --git a/dataikuapi/dss/agent.py b/dataikuapi/dss/agent.py index b97f5f49..6ccda42d 100644 --- a/dataikuapi/dss/agent.py +++ b/dataikuapi/dss/agent.py @@ -354,6 +354,51 @@ def wake_up(self, version_id=None): "versionId": version_id }) + def get_metrics_series(self, from_timestamp_ms=None, to_timestamp_ms=None, aggregation="MINUTE", timezone=None): + """ + Get the operational metrics series for this agent. + + The returned payload may include the ongoing interval for the requested granularity. + As a consequence, the latest datapoint is temporarily inconsistent and may evolve + as more raw events are flushed and aggregated at read time. + The requested time window is aligned to the bucket boundaries of the selected + aggregation before being read. + + :param int from_timestamp_ms: Beginning of the requested window, inclusive, as an + epoch timestamp in milliseconds. The effective lower bound is rounded down + to the start of its bucket. Optional, defaults to the oldest retained + timestamp available for the requested aggregation. + :param int to_timestamp_ms: End of the requested window, exclusive, as an epoch + timestamp in milliseconds. The effective upper bound is rounded up to the + next bucket boundary when it falls inside a bucket. + Optional, defaults to the current time when omitted. + :param str aggregation: Aggregation granularity. Supported values are ``MINUTE``, + ``FIVE_MINUTES``, ``HOUR``, ``DAY`` and ``MONTH``. + :param str timezone: Timezone used to align bucket boundaries. Optional, + defaults to ``UTC``. Can be a timezone name like ``Europe/Paris``. + :return: The list of datapoints. Each datapoint contains a ``timestampMs`` + expressed as the start timestamp of its bucket in epoch + milliseconds. For example, with ``HOUR`` aggregation, a datapoint + at ``18:00`` represents the interval ``[18:00, 19:00)``. + :rtype: list[dict] + """ + if from_timestamp_ms is not None and not isinstance(from_timestamp_ms, int): + raise TypeError("Expected int for from_timestamp_ms, got %s" % type(from_timestamp_ms).__name__) + if to_timestamp_ms is not None and not isinstance(to_timestamp_ms, int): + raise TypeError("Expected int for to_timestamp_ms, got %s" % type(to_timestamp_ms).__name__) + if timezone is not None and not isinstance(timezone, str): + raise TypeError("Expected str for timezone, got %s" % type(timezone).__name__) + + return self.client._perform_json( + "GET", "/projects/%s/agents/%s/operational-metrics/series" % (self.project_key, self.id), + params={ + "fromTimestampMs": from_timestamp_ms, + "toTimestampMs": to_timestamp_ms, + "aggregation": aggregation, + "timezone": timezone + } + ) + class DSSAgentSettings(DSSTaggableObjectSettings): """ diff --git a/dataikuapi/dss/cobuild.py b/dataikuapi/dss/cobuild.py index 5c04550d..ad5cb09a 100644 --- a/dataikuapi/dss/cobuild.py +++ b/dataikuapi/dss/cobuild.py @@ -28,7 +28,8 @@ class CobuildAssistantResponse(CobuildMessage): .. important:: Do not create this class directly, it is returned by :meth:`DSSCobuildConversation.send_message` - and :meth:`DSSCobuildConversation.answer_confirmation`. + :meth:`DSSCobuildConversation.answer_confirmation`, and + :meth:`DSSCobuildConversation.answer_question`. """ @property @@ -44,12 +45,14 @@ def role(self): def type(self): """ Type of the response: ``"assistant_message"``, ``"delete_confirmation_request"``, - or ``"error"``. + ``"ask_question_to_user_request"``, or ``"error"``. - ``"assistant_message"``: the assistant has completed its turn; the conversation is idle. - ``"delete_confirmation_request"``: the assistant is requesting confirmation before deleting objects. Call :meth:`DSSCobuildConversation.answer_confirmation` with a choice of ``"APPROVE"`` or ``"CANCEL"`` to continue. + - ``"ask_question_to_user_request"``: the assistant is requesting an explicit answer before it can + continue. Call :meth:`DSSCobuildConversation.answer_question` to continue. - ``"error"``: an error occurred while processing the Cobuild turn. In the public API, missing tool permissions are reported through this error response too. @@ -82,6 +85,16 @@ def is_confirmation_request(self): """ return self.type == "delete_confirmation_request" + @property + def is_question_request(self): + """ + Whether the assistant is requesting an explicit answer before continuing. + If ``True``, call :meth:`DSSCobuildConversation.answer_question`. + + :rtype: bool + """ + return self.type == "ask_question_to_user_request" + @property def is_error(self): """ @@ -116,6 +129,72 @@ def deletion_impacts(self): """ return self._raw.get("deletionImpacts") + @property + def question_id(self): + """ + For ``"ask_question_to_user_request"`` responses: the identifier of the pending question. + + ``None`` for other response types. + + :rtype: str or None + """ + return self._raw.get("questionId") + + @property + def title(self): + """ + For ``"ask_question_to_user_request"`` responses: the short question shown to the user. + + ``None`` for other response types. + + :rtype: str or None + """ + return self._raw.get("title") + + @property + def predefined_answers(self): + """ + For ``"ask_question_to_user_request"`` responses: the list of predefined answers proposed by Cobuild. + + ``None`` for other response types. + + :rtype: list[str] or None + """ + return self._raw.get("predefinedAnswers") + + @property + def allow_custom_answer(self): + """ + For ``"ask_question_to_user_request"`` responses: whether a custom free-text answer is allowed. + + ``None`` for other response types. + + :rtype: bool or None + """ + return self._raw.get("allowCustomAnswer") + + @property + def allow_multiple_answers(self): + """ + For ``"ask_question_to_user_request"`` responses: whether multiple answers can be selected. + + ``None`` for other response types. + + :rtype: bool or None + """ + return self._raw.get("allowMultipleAnswers") + + @property + def default_answer_set(self): + """ + For ``"ask_question_to_user_request"`` responses: whether Cobuild suggested a default answer. + + ``None`` for other response types. + + :rtype: bool or None + """ + return self._raw.get("selectFirstAnswerByDefault") + def __repr__(self): return "CobuildAssistantResponse(type=%r, message=%r)" % (self.type, self.message) @@ -127,7 +206,8 @@ class CobuildUserMessage(CobuildMessage): .. important:: Do not create this class directly, it is automatically created when using :meth:`dataikuapi.dss.project.DSSProject.new_cobuild_conversation`, :meth:`DSSCobuildConversation.send_message`, and - :meth:`DSSCobuildConversation.answer_confirmation`. + :meth:`DSSCobuildConversation.answer_confirmation`, and + :meth:`DSSCobuildConversation.answer_question`. """ @property @@ -142,10 +222,12 @@ def role(self): @property def type(self): """ - Type of the message: ``"request"`` or ``"delete_confirmation_response"``. + Type of the message: ``"request"``, ``"delete_confirmation_response"``, or + ``"ask_question_to_user_response"``. - ``"request"``: a normal message sent to cobuild. - ``"delete_confirmation_response"``: user response when cobuild requests confirmation before deleting objects + - ``"ask_question_to_user_response"``: user response when cobuild requests an explicit answer :rtype: str """ @@ -160,6 +242,15 @@ def is_confirmation_response(self): """ return self.type == "delete_confirmation_response" + @property + def is_question_response(self): + """ + Whether this message is the answer for assistant requesting an explicit answer. + + :rtype: bool + """ + return self.type == "ask_question_to_user_response" + def __repr__(self): return "CobuildUserMessage(type=%r, message=%r)" % (self.type, self.message) @@ -217,6 +308,31 @@ class DSSCobuildConversation(object): response = conv.answer_confirmation("APPROVE") print(response.message) + When the assistant needs an explicit answer, it asks a question:: + + response = conv.send_message("Use the best date column for sorting") + if response.is_question_request: + print("Question:", response.title) + print("Choices:", response.predefined_answers) + print("Allow custom answer:", response.allow_custom_answer) + response = conv.answer_question( + answers=["OrderDate"], + rejected=False, + used_custom_answer=False + ) + print(response.message) + + You can also decline answering a question:: + + response = conv.send_message("Pick the dataset to build") + if response.is_question_request: + response = conv.answer_question( + answers=[], + rejected=True, + used_custom_answer=False + ) + print(response.message) + If a tool requires edit permission, pass ``allow_edit_project=True`` to :meth:`send_message` to allow Cobuild to create and edit objects for that message. """ @@ -227,6 +343,7 @@ def __init__(self, client, project_key, conversation_id, selected_objects=None): self.conversation_id = conversation_id self._messages = [] self._pending_confirmation_id = None + self._pending_question_id = None self._selected_objects = selected_objects @property @@ -273,6 +390,7 @@ def send_message(self, message, selected_objects=None, allow_edit_project=False) ) response = CobuildAssistantResponse(raw) self._pending_confirmation_id = raw["confirmationId"] if response.is_confirmation_request else None + self._pending_question_id = raw["questionId"] if response.is_question_request else None self._messages.append(response) return response @@ -313,5 +431,51 @@ def answer_confirmation(self, choice, options=None): ) response = CobuildAssistantResponse(raw) self._pending_confirmation_id = raw["confirmationId"] if response.is_confirmation_request else None + self._pending_question_id = raw["questionId"] if response.is_question_request else None + self._messages.append(response) + return response + + def answer_question(self, answers=None, rejected=False, used_custom_answer=False): + """ + Answer a pending question request and wait for the assistant's next response. + + Call this after receiving a response with :attr:`~CobuildAssistantResponse.is_question_request` + set to ``True``. + + The answer and assistant response are appended to :attr:`messages`. + + :param list[str] answers: answers selected or entered by the user. Use an empty list when + ``rejected=True``. Defaults to ``[]``. + :param bool rejected: whether to decline answering the question + :param bool used_custom_answer: whether one of the answers came from the custom free-text input + + :returns: the assistant's response after the question answer + :rtype: :class:`CobuildAssistantResponse` + """ + if answers is None: + answers = [] + if not isinstance(answers, list): + raise ValueError("answers must be a list of strings") + if self._pending_question_id is None: + raise ValueError("No pending question request. Call send_message first and check is_question_request.") + + question_id = self._pending_question_id + self._pending_question_id = None + self._messages.append(CobuildUserMessage({ + "type": "ask_question_to_user_response", + "message": None if rejected else ", ".join(answers), + })) + raw = self.client._perform_json( + "POST", + "/projects/%s/cobuild/conversations/%s/question/%s" % (self.project_key, self.conversation_id, question_id), + body={ + "rejected": rejected, + "answers": answers, + "usedCustomAnswer": used_custom_answer, + }, + ) + response = CobuildAssistantResponse(raw) + self._pending_confirmation_id = raw["confirmationId"] if response.is_confirmation_request else None + self._pending_question_id = raw["questionId"] if response.is_question_request else None self._messages.append(response) return response diff --git a/dataikuapi/dss/knowledgebank.py b/dataikuapi/dss/knowledgebank.py index 79008bdd..e6af2b88 100644 --- a/dataikuapi/dss/knowledgebank.py +++ b/dataikuapi/dss/knowledgebank.py @@ -115,6 +115,16 @@ def delete(self): """ return self.client._perform_empty("DELETE", "/projects/%s/knowledge-banks/%s" % (self.project_key, self.id)) + def clear(self): + """ + Clear data in this knowledge bank. + + :returns: a dict containing the method call status. + :rtype: dict + """ + return self.client._perform_json( + "POST", "/projects/%s/knowledge-banks/%s/clear" % (self.project_key, self.id)) + def build(self, job_type="NON_RECURSIVE_FORCED_BUILD", wait=True): """ Start a new job to build this knowledge bank and wait for it to complete. @@ -449,18 +459,17 @@ def file_ref(self): folder_smart_id = source_file_info.get("folder_ref") folder_full_id = source_file_info.get("folder_full_id") # deprecated but to support existing KBs - folder_loc = None if folder_smart_id is not None: try: - folder_loc = AnyLoc.from_ref("donotmatter", folder_smart_id) # unused, just to check format + _ = AnyLoc.from_ref("donotmatter", folder_smart_id) # unused, just to check format return ManagedFolderDocumentRef(path, folder_smart_id) except ValueError as e: logger.error("Invalid folder_ref in DKU_DOCUMENT_INFO: {}, {}".format(e, document_info)) return None elif folder_full_id is not None: try: - folder_loc = AnyLoc.from_full(folder_full_id) # unused, just to check format + _ = AnyLoc.from_full(folder_full_id) # unused, just to check format return ManagedFolderDocumentRef(path, folder_full_id) except ValueError as e: logger.error("Invalid folder_full_id in DKU_DOCUMENT_INFO: {}, {}".format(e, document_info)) diff --git a/dataikuapi/dss/llm_utils/__init__.py b/dataikuapi/dss/llm_utils/__init__.py index cf434472..c4d907d6 100644 --- a/dataikuapi/dss/llm_utils/__init__.py +++ b/dataikuapi/dss/llm_utils/__init__.py @@ -8,7 +8,10 @@ _footer_attributes = [ # usage metadata "promptTokens", + "cacheReadInputTokens", + "cacheWriteInputTokens", "completionTokens", + "reasoningTokens", "totalTokens", "totalUsage", "tokenCountsAreEstimated", diff --git a/dataikuapi/dss/project.py b/dataikuapi/dss/project.py index f6081edf..7215f15e 100644 --- a/dataikuapi/dss/project.py +++ b/dataikuapi/dss/project.py @@ -3244,6 +3244,23 @@ def new_cobuild_conversation(self): response = conv.send_message("Update wiki article 123 to add a one-line banner") print(response.message) + Cobuild can also pause and request an explicit answer before continuing:: + + project = client.get_project("MY_PROJECT") + conv = project.new_cobuild_conversation() + + response = conv.send_message("Use the best date column for sorting") + if response.is_question_request: + print("Question:", response.title) + print("Choices:", response.predefined_answers) + print("Allow custom answer:", response.allow_custom_answer) + response = conv.answer_question( + answers=["OrderDate"], + rejected=False, + used_custom_answer=False + ) + print(response.message) + :returns: a handle to the newly created empty conversation :rtype: :class:`dataikuapi.dss.cobuild.DSSCobuildConversation` """ diff --git a/dataikuapi/dss/recipe.py b/dataikuapi/dss/recipe.py index d3233119..8dc899e5 100644 --- a/dataikuapi/dss/recipe.py +++ b/dataikuapi/dss/recipe.py @@ -98,6 +98,21 @@ def name(self): """ return self.recipe_name + @property + def type(self): + """ + Get the type of the recipe. + + :return: a recipe type, for example 'sync' or 'join' + :rtype: string + """ + return self._get_recipe_data()["recipe"]["type"] + + def _get_recipe_data(self): + return self.client._perform_json( + "GET", "/projects/%s/recipes/%s" % (self.project_key, self.recipe_name) + ) + def compute_schema_updates(self): """ Computes which updates are required to the outputs of this recipe. @@ -212,57 +227,56 @@ def get_settings(self): :rtype: :class:`DSSRecipeSettings` or a subclass """ - data = self.client._perform_json( - "GET", "/projects/%s/recipes/%s" % (self.project_key, self.recipe_name)) - type = data["recipe"]["type"] + data = self._get_recipe_data() + recipe_type = data["recipe"]["type"] - if type == "generate_features": + if recipe_type == "generate_features": return GenerateFeaturesRecipeSettings(self, data) - if type == "grouping": + if recipe_type == "grouping": return GroupingRecipeSettings(self, data) - if type == "upsert": + if recipe_type == "upsert": return UpsertRecipeSettings(self, data) - elif type == "window": + elif recipe_type == "window": return WindowRecipeSettings(self, data) - elif type == "sync": + elif recipe_type == "sync": return SyncRecipeSettings(self, data) - elif type == "pivot": + elif recipe_type == "pivot": return PivotRecipeSettings(self, data) - elif type == "sort": + elif recipe_type == "sort": return SortRecipeSettings(self, data) - elif type == "topn": + elif recipe_type == "topn": return TopNRecipeSettings(self, data) - elif type == "distinct": + elif recipe_type == "distinct": return DistinctRecipeSettings(self, data) - elif type == "join": + elif recipe_type == "join": return JoinRecipeSettings(self, data) - elif type == "vstack": + elif recipe_type == "vstack": return StackRecipeSettings(self, data) - elif type == "sampling": + elif recipe_type == "sampling": return SamplingRecipeSettings(self, data) - elif type == "split": + elif recipe_type == "split": return SplitRecipeSettings(self, data, self.client.get_project(self.project_key)) - elif type == "prepare" or type == "shaker": + elif recipe_type == "prepare" or recipe_type == "shaker": return PrepareRecipeSettings(self, data) - #elif type == "prediction_scoring": - #elif type == "clustering_scoring": - elif type == "download": + #elif recipe_type == "prediction_scoring": + #elif recipe_type == "clustering_scoring": + elif recipe_type == "download": return DownloadRecipeSettings(self, data) - elif type == 'export': + elif recipe_type == 'export': return ExportRecipeSettings(self, data) - #elif type == "sql_query": + #elif recipe_type == "sql_query": # return WindowRecipeSettings(self, data) - elif type in ["python", "r", "sql_script", "pyspark", "sparkr", "spark_scala", "shell", "spark_sql_query"]: + elif recipe_type in ["python", "r", "sql_script", "pyspark", "sparkr", "spark_scala", "shell", "spark_sql_query"]: return CodeRecipeSettings(self, data) - elif type == "nlp_llm_rag_embedding": + elif recipe_type == "nlp_llm_rag_embedding": return EmbedDatasetRecipeSettings(self, data) - elif type == "embed_documents": + elif recipe_type == "embed_documents": return EmbedDocumentsRecipeSettings(self, data) - elif type == "extract_content": + elif recipe_type == "extract_content": return ExtractContentRecipeSettings(self, data) - elif type == "extract_fields": + elif recipe_type == "extract_fields": return ExtractFieldsRecipeSettings(self, data) - elif type == "prompt": + elif recipe_type == "prompt": return PromptRecipeSettings(self, data) else: return DSSRecipeSettings(self, data) diff --git a/dataikuapi/dss/retrieval_augmented_llm.py b/dataikuapi/dss/retrieval_augmented_llm.py index 57c113ba..f669ff06 100644 --- a/dataikuapi/dss/retrieval_augmented_llm.py +++ b/dataikuapi/dss/retrieval_augmented_llm.py @@ -80,6 +80,51 @@ def delete(self): """ return self.client._perform_empty("DELETE", "/projects/%s/retrieval-augmented-llms/%s" % (self.project_key, self.id)) + def get_metrics_series(self, from_timestamp_ms=None, to_timestamp_ms=None, aggregation="MINUTE", timezone=None): + """ + Get the operational metrics series for this retrieval-augmented LLM. + + The returned payload may include the ongoing interval for the requested granularity. + As a consequence, the latest datapoint is temporarily inconsistent and may evolve + as more raw events are flushed and aggregated at read time. + The requested time window is aligned to the bucket boundaries of the selected + aggregation before being read. + + :param int from_timestamp_ms: Beginning of the requested window, inclusive, as an + epoch timestamp in milliseconds. The effective lower bound is rounded down + to the start of its bucket. Optional, defaults to the oldest retained + timestamp available for the requested aggregation. + :param int to_timestamp_ms: End of the requested window, exclusive, as an epoch + timestamp in milliseconds. The effective upper bound is rounded up to the + next bucket boundary when it falls inside a bucket. + Optional, defaults to the current time when omitted. + :param str aggregation: Aggregation granularity. Supported values are ``MINUTE``, + ``FIVE_MINUTES``, ``HOUR``, ``DAY`` and ``MONTH``. + :param str timezone: Timezone used to align bucket boundaries. Optional, + defaults to ``UTC``. Can be a timezone name like ``Europe/Paris``. + :return: The list of datapoints. Each datapoint contains a ``timestampMs`` + expressed as the start timestamp of its bucket in epoch + milliseconds. For example, with ``HOUR`` aggregation, a datapoint + at ``18:00`` represents the interval ``[18:00, 19:00)``. + :rtype: list[dict] + """ + if from_timestamp_ms is not None and not isinstance(from_timestamp_ms, int): + raise TypeError("Expected int for from_timestamp_ms, got %s" % type(from_timestamp_ms).__name__) + if to_timestamp_ms is not None and not isinstance(to_timestamp_ms, int): + raise TypeError("Expected int for to_timestamp_ms, got %s" % type(to_timestamp_ms).__name__) + if timezone is not None and not isinstance(timezone, str): + raise TypeError("Expected str for timezone, got %s" % type(timezone).__name__) + + return self.client._perform_json( + "GET", "/projects/%s/retrieval-augmented-llms/%s/operational-metrics/series" % (self.project_key, self.id), + params={ + "fromTimestampMs": from_timestamp_ms, + "toTimestampMs": to_timestamp_ms, + "aggregation": aggregation, + "timezone": timezone + } + ) + class DSSRetrievalAugmentedLLMSettings(DSSTaggableObjectSettings): """ Settings for a retrieval-augmented LLM diff --git a/setup.py b/setup.py index 9f6863fe..c9b2c635 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup -VERSION = "14.7.1" +VERSION = "14.7.2" long_description = (open('README').read() + '\n\n' + open('HISTORY.txt').read()) From 5c156142228e7eaa94edc4091b5aada046a53486 Mon Sep 17 00:00:00 2001 From: Kevin REMY Date: Mon, 3 Aug 2026 16:06:32 +0200 Subject: [PATCH 4/4] 14.7.3 Publish on PyPI (#364) * Sync sources with main repository * Release metadata --- HISTORY.txt | 5 + dataikuapi/dss/agent_tool.py | 7 +- dataikuapi/dss/langchain/embeddings.py | 13 +- dataikuapi/dss/llm.py | 158 +++++++++++++++-- dataikuapi/dss/mira.py | 226 +++++++++++++++++++++++++ dataikuapi/dssclient.py | 12 ++ setup.py | 2 +- 7 files changed, 406 insertions(+), 17 deletions(-) create mode 100644 dataikuapi/dss/mira.py diff --git a/HISTORY.txt b/HISTORY.txt index c86c43c2..5d5a8766 100644 --- a/HISTORY.txt +++ b/HISTORY.txt @@ -2,6 +2,11 @@ Changelog ========== +14.7.3 (2026-08-03) +------------------- + +* Initial release for DSS 14.7.3 + 14.7.2 (2026-07-13) ------------------- diff --git a/dataikuapi/dss/agent_tool.py b/dataikuapi/dss/agent_tool.py index 1c015532..a8af2ed4 100644 --- a/dataikuapi/dss/agent_tool.py +++ b/dataikuapi/dss/agent_tool.py @@ -64,7 +64,7 @@ def id(self): """ return self.tool_id - def get_descriptor(self): + def get_descriptor(self, context=None): """ Get the descriptor of the tool @@ -73,7 +73,10 @@ def get_descriptor(self): """ if self._descriptor is None: - self._descriptor = self.client._perform_json("GET", "/projects/%s/agents/tools/%s/descriptor" % (self.project_key, self.tool_id)) + if context is None: + self._descriptor = self.client._perform_json("GET", "/projects/%s/agents/tools/%s/descriptor" % (self.project_key, self.tool_id)) + else: + self._descriptor = self.client._perform_json("POST", "/projects/%s/agents/tools/%s/descriptor" % (self.project_key, self.tool_id), body={"context": context}) return self._descriptor def get_settings(self): diff --git a/dataikuapi/dss/langchain/embeddings.py b/dataikuapi/dss/langchain/embeddings.py index 095d5fff..3284c05c 100644 --- a/dataikuapi/dss/langchain/embeddings.py +++ b/dataikuapi/dss/langchain/embeddings.py @@ -2,16 +2,25 @@ import asyncio import concurrent import logging -import threading +import threading +import itertools from typing import Callable, List, Any, Union import pydantic + +_thread_pool_executor_counter = itertools.count().__next__ + +def next_thread_pool_executor_prefix(prefix): + return "{}-{}".format(prefix, _thread_pool_executor_counter()) + try: from langchain_core.embeddings.embeddings import Embeddings except ModuleNotFoundError: from langchain.embeddings.base import Embeddings from langchain_core.callbacks import BaseCallbackHandler, LLMManagerMixin + + from dataikuapi.dss.llm_tracing import new_trace, SpanBuilder from dataikuapi.dss.langchain.utils import must_use_deprecated_pydantic_config @@ -121,7 +130,7 @@ def embed_documents(self, texts: List[str]) -> List[List[float]]: async def aembed_documents(self, texts: List[str]) -> List[List[float]]: loop = asyncio.get_event_loop() - with concurrent.futures.ThreadPoolExecutor() as executor: + with concurrent.futures.ThreadPoolExecutor(thread_name_prefix=next_thread_pool_executor_prefix("DKUEmbeddingsAsyncExecutor")) as executor: result = await loop.run_in_executor(executor, self.embed_documents, texts) return result diff --git a/dataikuapi/dss/llm.py b/dataikuapi/dss/llm.py index d207ecd3..d8f5fd66 100644 --- a/dataikuapi/dss/llm.py +++ b/dataikuapi/dss/llm.py @@ -199,6 +199,8 @@ def add_image(self, image, text = None): def new_guardrail(self, type): """ Start adding a guardrail to the request. You need to configure the returned object, and call add() to actually add it + + :rtype: :class:`DSSLLMRequestGuardrailBuilder` """ return DSSLLMRequestGuardrailBuilder(self, type) @@ -420,15 +422,27 @@ def with_structured_output(self, model_type, strict=None, compatible=None): class DSSLLMRequestGuardrailBuilder(object): + """ + .. important:: + Do not create this class directly, use :meth:`dataikuapi.dss.llm.DSSLLMCompletionQuery.new_guardrail`, + :meth:`dataikuapi.dss.llm.DSSLLMCompletionsQuery.new_guardrail`, :meth:`dataikuapi.dss.llm.DSSLLMEmbeddingsQuery.new_guardrail` or + :meth:`dataikuapi.dss.llm.DSSLLMImageGenerationQuery.new_guardrail`. + """ + def __init__(self, request, type): self.request = request - self.guardrail = { "type" : type, "enabled": True, "params" : {}} + self.guardrail = {"type" : type, "enabled": True, "params" : {}} @property def params(self): + """ + :return: The parameters of this guardrail + :rtype: dict + """ return self.guardrail["params"] def add(self): + """Add this guardrail to the completion query""" if self.request._guardrails is None: self.request._guardrails = {"guardrails" : []} self.request._guardrails["guardrails"].append(self.guardrail) @@ -537,6 +551,8 @@ def settings(self): def new_guardrail(self, type): """ Start adding a guardrail to the request. You need to configure the returned object, and call add() to actually add it + + :rtype: :class:`DSSLLMRequestGuardrailBuilder` """ return DSSLLMRequestGuardrailBuilder(self, type) @@ -638,6 +654,8 @@ def new_completion(self): def new_guardrail(self, type): """ Start adding a guardrail to the request. You need to configure the returned object, and call add() to actually add it + + :rtype: :class:`DSSLLMRequestGuardrailBuilder` """ return DSSLLMRequestGuardrailBuilder(self, type) @@ -664,7 +682,7 @@ def execute(self): return DSSLLMCompletionsResponse(ret["responses"], response_parser=self._response_parser) -class DSSLLMCompletionQueryMultipartBuilder(object): +class _DSSLLMCompletionQueryMultipartBuilder(object): def __init__(self): self.parts = [] @@ -681,6 +699,8 @@ def _encode_image(image): def with_text(self, text): """ Add a text part to the multipart message + + :param str text: The text to add """ self.parts.append({"type": "TEXT", "text": text}) return self @@ -692,7 +712,7 @@ def with_inline_image(self, image, mime_type=None): :param Union[str, bytes] image: The image :param str mime_type: None for default """ - img_b64 = DSSLLMCompletionQueryMultipartMessage._encode_image(image) + img_b64 = _DSSLLMCompletionQueryMultipartBuilder._encode_image(image) part = { "type": "IMAGE_INLINE", @@ -713,7 +733,7 @@ def with_captioned_image_inline(self, caption, image, mime_type=None): :param Union[str, bytes] image: The image :param str mime_type: None for default """ - img_b64 = DSSLLMCompletionQueryMultipartMessage._encode_image(image) + img_b64 = _DSSLLMCompletionQueryMultipartBuilder._encode_image(image) image_part = { "type": "IMAGE_INLINE", @@ -736,14 +756,13 @@ def with_image_url(self, image): """ Add an image url part to the multipart message - :param image: str the image url + :param str image: the image url """ - self.parts.append({"type": "IMAGE_URI", "imageUrl": image}) return self -class DSSLLMCompletionQueryMultipartMessage(DSSLLMCompletionQueryMultipartBuilder): +class DSSLLMCompletionQueryMultipartMessage(_DSSLLMCompletionQueryMultipartBuilder): """ .. important:: Do not create this class directly, use :meth:`dataikuapi.dss.llm.DSSLLMCompletionQuery.new_multipart_message` or @@ -761,8 +780,43 @@ def add(self): self.q.cq["messages"].append(self.msg) return self.q + def with_text(self, text): + """ + Add a text part to the multipart message + + :param str text: The text to add + """ + return super().with_text(text) + + def with_inline_image(self, image, mime_type=None): + """ + Add an image part to the multipart message + + :param Union[str, bytes] image: The image + :param str mime_type: None for default + """ + return super().with_inline_image(image, mime_type) -class DSSLLMCompletionQueryMultipartToolOutput(DSSLLMCompletionQueryMultipartBuilder): + def with_captioned_image_inline(self, caption, image, mime_type=None): + """ + Add a captioned image part to the multipart message + + :param str caption: Image caption + :param Union[str, bytes] image: The image + :param str mime_type: None for default + """ + return super().with_captioned_image_inline(caption, image, mime_type) + + def with_image_url(self, image): + """ + Add an image url part to the multipart message + + :param str image: The image url + """ + return super().with_image_url(image) + + +class DSSLLMCompletionQueryMultipartToolOutput(_DSSLLMCompletionQueryMultipartBuilder): """ .. important:: Do not create this class directly, use :meth:`dataikuapi.dss.llm.DSSLLMCompletionQuery.new_multipart_tool_output` or @@ -787,24 +841,75 @@ def add(self): self.q.cq["messages"].append(self.msg) return self.q + def with_text(self, text): + """ + Add a text part to the multipart tool output + + :param str text: The text to add + """ + return super().with_text(text) + + def with_inline_image(self, image, mime_type=None): + """ + Add an image part to the multipart tool output + + :param Union[str, bytes] image: The image + :param str mime_type: None for default + """ + return super().with_inline_image(image, mime_type) + + def with_captioned_image_inline(self, caption, image, mime_type=None): + """ + Add a captioned image part to the multipart tool output + + :param str caption: Image caption + :param Union[str, bytes] image: The image + :param str mime_type: None for default + """ + return super().with_captioned_image_inline(caption, image, mime_type) + + def with_image_url(self, image): + """ + Add an image url part to the multipart tool output + + :param str image: The image url + """ + return super().with_image_url(image) + class DSSLLMStreamedCompletionChunk(object): + """ + A handle to interact with a streamed completion query chunk. + + .. important:: + Do not create this class directly, iterate over a :class:`dataikuapi.dss.llm.DSSLLMStreamedCompletionChunks` iterator instead to generate the chunks instead. + """ + def __init__(self, data): self.data = data @property def type(self): - """Type of this chunk, either "content" or "event" """ + """ + :return: Type of this chunk, either "content" or "event" + :rtype: Literal["content", "event"] + """ return self.data.get("type", "content") @property def text(self): - """If this chunk is content and has text, the (partial) text""" + """ + :return: If this chunk is content and has text, the (partial) text + :rtype: bool + """ return self.data.get("text", None) @property def event_kind(self): - """If this chunk is an event, its kind""" + """ + :return: If this chunk is an event, its kind + :rtype: str + """ return self.data.get("eventKind", None) def __repr__(self): @@ -812,16 +917,31 @@ def __repr__(self): class DSSLLMStreamedCompletionFooter(object): + """ + A handle to interact with a streamed completion query footer. + + .. important:: + Do not create this class directly, iterate over a :class:`dataikuapi.dss.llm.DSSLLMStreamedCompletionChunks` iterator instead to generate the chunks instead. + """ + def __init__(self, data): self.data = data # Compatibility for code that just checks for "type"" @property def type(self): + """ + :return: Type of this chunk, to distinguish it from :class:`dataikuapi.dss.llm.DSSLLMStreamedCompletionChunk` chunks. Can only be "footer" + :rtype: Literal["footer"] + """ return "footer" @property def trace(self): + """ + :return: The trace of the completion query if available, None otherwise. + :rtype: Union[dict, None] + """ return self.data.get("trace", None) @property @@ -890,7 +1010,11 @@ def iterevents(self): class DSSLLMCompletionResponse(object): """ - Response to a completion + A handle to interact with a completion query result. + + .. important:: + Do not create this class directly, use :meth:`dataikuapi.dss.llm.DSSLLMCompletionQuery.execute` or + :attr:`dataikuapi.dss.llm.DSSLLMCompletionsResponse.responses` or :attr:`dataikuapi.dss.llm.DSSLLMStreamedCompletionChunks.response` instead. """ def __init__(self, raw_resp=None, text=None, finish_reason=None, response_parser=None, trace=None, query=None): if raw_resp is not None: @@ -990,6 +1114,10 @@ def context_upsert(self): @property def trace(self): + """ + :return: The trace of the completion query if available, None otherwise. + :rtype: Union[dict, None] + """ return self._raw.get("trace", None) @property @@ -1157,6 +1285,8 @@ def with_mask(self, mode, image=None): def new_guardrail(self, type): """ Start adding a guardrail to the request. You need to configure the returned object, and call add() to actually add it + + :rtype: :class:`DSSLLMRequestGuardrailBuilder` """ return DSSLLMRequestGuardrailBuilder(self, type) @@ -1381,6 +1511,10 @@ def images(self): @property def trace(self): + """ + :return: The trace of the image generation query if available, None otherwise. + :rtype: Union[dict, None] + """ return self._raw.get("trace", None) @property diff --git a/dataikuapi/dss/mira.py b/dataikuapi/dss/mira.py new file mode 100644 index 00000000..06f0a82b --- /dev/null +++ b/dataikuapi/dss/mira.py @@ -0,0 +1,226 @@ +import sys +from ..utils import CallableStr + +if sys.version_info >= (3, 0): + import urllib.parse + dku_quote_fn = urllib.parse.quote +else: + import urllib + dku_quote_fn = urllib.quote + + +class DSSMIRA(object): + """ + Handle to interact with MIRA. + + Do not create this directly, use :meth:`dataikuapi.dss.DSSClient.get_mira` + """ + def __init__(self, client): + self.client = client + + def list_infras(self, as_objects=True): + """ + Lists MIRA infrastructures. + + :param boolean as_objects: if True, returns a list of :class:`DSSMIRAInfra`, else returns a list of dict. + :returns: a list - see as_objects for more information + :rtype: list + """ + response = self.client._perform_json("GET", "/mira/infras") + infras = response["infras"] + if as_objects: + return [DSSMIRAInfra(self.client, infra["name"]) for infra in infras] + else: + return infras + + def get_infra(self, infra_name): + """ + Returns a handle to interact with a MIRA infrastructure. + + :param str infra_name: MIRA infrastructure name + :rtype: :class:`DSSMIRAInfra` + """ + return DSSMIRAInfra(self.client, infra_name) + + def get_agent(self, infra_name, agent_id): + """ + Returns a handle to interact with a MIRA agent. + + :param str infra_name: MIRA infrastructure name + :param str agent_id: MIRA agent id + :rtype: :class:`DSSMIRAAgent` + """ + return DSSMIRAAgent(self.client, infra_name, agent_id) + + +class DSSMIRAInfra(object): + """ + Handle to interact with a MIRA infrastructure. + + Do not create this directly, use :meth:`DSSMIRA.get_infra`. + """ + def __init__(self, client, infra_name): + self.client = client + self.infra_name = infra_name + + @property + def name(self): + return CallableStr(self.infra_name) + + def __str__(self): + return CallableStr(self.infra_name) + + def get_info(self): + """ + Gets this MIRA infrastructure. + + :returns: infrastructure data as a dict + :rtype: dict + """ + return self.client._perform_json("GET", self._path()) + + def list_agents(self, as_objects=True): + """ + Lists agents of this MIRA infrastructure. + + :param boolean as_objects: if True, returns a list of :class:`DSSMIRAAgent`, else returns a list of dict. + :returns: a list - see as_objects for more information + :rtype: list + """ + response = self.client._perform_json("GET", self._path("agents")) + agents = response["agents"] + if as_objects: + return [DSSMIRAAgent(self.client, self.infra_name, agent["id"]) for agent in agents] + else: + return agents + + def get_agent(self, agent_id): + """ + Returns a handle to interact with a MIRA agent in this infrastructure. + + :param str agent_id: MIRA agent id + :rtype: :class:`DSSMIRAAgent` + """ + return DSSMIRAAgent(self.client, self.infra_name, agent_id) + + def get_uptime_metrics(self, bucket="HOUR", from_time=None, to_time=None, agent_ids=None): + """ + Get uptime metrics aggregated over MIRA agents in this infrastructure. + + :param str bucket: time bucket, "HOUR" or "DAY" + :param str from_time: optional inclusive ISO timestamp lower bound + :param str to_time: optional exclusive ISO timestamp upper bound + :param list agent_ids: optional list of agent ids to aggregate. Defaults to all allowed agents. + :returns: uptime metrics response as a dict + :rtype: dict + """ + params = {"bucket": bucket} + if agent_ids is not None: + params["agentIds"] = agent_ids + if from_time is not None: + params["from"] = from_time + if to_time is not None: + params["to"] = to_time + return self.client._perform_json("GET", self._path("uptime-metrics"), params=params) + + def _path(self, suffix=None): + path = "/mira/infras/%s" % dku_quote_fn(self.infra_name, safe="") + if suffix is not None: + path += "/" + suffix + return path + + +class DSSMIRAAgent(object): + """ + Handle to interact with a MIRA agent. + + Do not create this directly, use :meth:`DSSMIRA.get_agent` or :meth:`DSSMIRAInfra.get_agent`. + """ + def __init__(self, client, infra_name, agent_id): + self.client = client + self.infra_name = infra_name + self.agent_id = agent_id + + @property + def id(self): + return CallableStr(self.agent_id) + + def __str__(self): + return CallableStr(self.agent_id) + + def get_info(self): + """ + Gets this MIRA agent. + + :returns: agent data as a dict + :rtype: dict + """ + return self.client._perform_json("GET", self._path()) + + def get_uptime_metrics(self, bucket="HOUR", from_time=None, to_time=None): + """ + Get uptime metrics computed from MIRA uptime tests for this agent. + + :param str bucket: time bucket, "HOUR" or "DAY" + :param str from_time: optional inclusive ISO timestamp lower bound + :param str to_time: optional exclusive ISO timestamp upper bound + :returns: uptime metrics response as a dict + :rtype: dict + """ + params = {"bucket": bucket} + if from_time is not None: + params["from"] = from_time + if to_time is not None: + params["to"] = to_time + return self.client._perform_json("GET", self._path("uptime-metrics"), params=params) + + def list_uptime_tests(self, from_time=None, to_time=None): + """ + List raw MIRA uptime tests for this agent. + + :param str from_time: optional inclusive ISO timestamp lower bound + :param str to_time: optional exclusive ISO timestamp upper bound + :returns: uptime tests response as a dict + :rtype: dict + """ + params = {} + if from_time is not None: + params["from"] = from_time + if to_time is not None: + params["to"] = to_time + return self.client._perform_json("GET", self._path("uptime-tests"), params=params) + + def insert_uptime_tests(self, tests): + """ + Insert raw MIRA uptime test values for this agent. + + :param list tests: list of test dictionaries with timestamp, responseStatus and responseTimeMs + :returns: mutation response as a dict + :rtype: dict + """ + return self.client._perform_json("POST", self._path("uptime-tests"), body={"tests": tests}) + + def delete_uptime_tests(self, from_time=None, to_time=None): + """ + Delete raw MIRA uptime tests for this agent, optionally restricted to a time range. + + :param str from_time: optional inclusive ISO timestamp lower bound + :param str to_time: optional exclusive ISO timestamp upper bound + :returns: deletion response as a dict + :rtype: dict + """ + params = {} + if from_time is not None: + params["from"] = from_time + if to_time is not None: + params["to"] = to_time + return self.client._perform_json("DELETE", self._path("uptime-tests"), params=params) + + def _path(self, suffix=None): + path = "/mira/infras/%s/agents/%s" % ( + dku_quote_fn(self.infra_name, safe=""), + dku_quote_fn(self.agent_id, safe="") + ) + if suffix is not None: + path += "/" + suffix + return path diff --git a/dataikuapi/dssclient.py b/dataikuapi/dssclient.py index 0827b51a..007c4d71 100644 --- a/dataikuapi/dssclient.py +++ b/dataikuapi/dssclient.py @@ -30,6 +30,7 @@ from .dss.discussion import DSSObjectDiscussions from .dss.apideployer import DSSAPIDeployer from .dss.projectdeployer import DSSProjectDeployer +from .dss.mira import DSSMIRA from .dss.project_standards import DSSProjectStandards from .dss.unifiedmonitoring import DSSUnifiedMonitoring from .dss.utils import DSSInfoMessages, Enum @@ -1621,6 +1622,17 @@ def get_projectdeployer(self): """ return DSSProjectDeployer(self) + ######################################################## + # MIRA + ######################################################## + + def get_mira(self): + """Gets a handle to work with MIRA + + :rtype: :class:`~dataikuapi.dss.mira.DSSMIRA` + """ + return DSSMIRA(self) + ######################################################## # Unified Monitoring ######################################################## diff --git a/setup.py b/setup.py index c9b2c635..ef655ba8 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup -VERSION = "14.7.2" +VERSION = "14.7.3" long_description = (open('README').read() + '\n\n' + open('HISTORY.txt').read())