diff --git a/langfuse/_client/client.py b/langfuse/_client/client.py index a3f653ada..36c8021a8 100644 --- a/langfuse/_client/client.py +++ b/langfuse/_client/client.py @@ -80,6 +80,15 @@ from langfuse._utils.prompt_cache import PromptCache from langfuse.api.resources.commons.errors.error import Error from langfuse.api.resources.commons.errors.not_found_error import NotFoundError +from langfuse.api.resources.commons.types.dataset_run_with_items import ( + DatasetRunWithItems, +) +from langfuse.api.resources.datasets.types.delete_dataset_run_response import ( + DeleteDatasetRunResponse, +) +from langfuse.api.resources.datasets.types.paginated_dataset_runs import ( + PaginatedDatasetRuns, +) from langfuse.api.resources.ingestion.types.score_body import ScoreBody from langfuse.api.resources.prompts.types import ( CreatePromptRequest_Chat, @@ -2461,6 +2470,128 @@ def get_dataset( handle_fern_exception(e) raise e + def get_dataset_run( + self, + dataset_name: str, + run_name: str, + ) -> DatasetRunWithItems: + """Fetch a dataset run by dataset name and run name. + + This method properly URL-encodes the dataset and run names, supporting + folder-format names that contain slashes (e.g., "folder/subfolder/dataset"). + + Args: + dataset_name: The name of the dataset (can include slashes for folder structure). + run_name: The name of the run to fetch (can include slashes). + + Returns: + DatasetRunWithItems: The dataset run with its items. + + Raises: + NotFoundError: If the dataset or run is not found. + + Example: + ```python + # Fetch a run from a dataset in a folder + run = langfuse.get_dataset_run( + dataset_name="evaluation/qa-dataset", + run_name="experiment-1" + ) + print(f"Run has {len(run.dataset_run_items)} items") + ``` + """ + try: + langfuse_logger.debug(f"Getting dataset run {dataset_name}/{run_name}") + return self.api.datasets.get_run( + dataset_name=self._url_encode(dataset_name), + run_name=self._url_encode(run_name), + ) + except Error as e: + handle_fern_exception(e) + raise e + + def get_dataset_runs( + self, + dataset_name: str, + *, + page: Optional[int] = None, + limit: Optional[int] = None, + ) -> PaginatedDatasetRuns: + """Fetch all runs for a dataset. + + This method properly URL-encodes the dataset name, supporting + folder-format names that contain slashes (e.g., "folder/subfolder/dataset"). + + Args: + dataset_name: The name of the dataset (can include slashes for folder structure). + page: Optional page number for pagination (starts at 1). + limit: Optional limit of items per page. + + Returns: + PaginatedDatasetRuns: Paginated list of dataset runs. + + Raises: + NotFoundError: If the dataset is not found. + + Example: + ```python + # Get all runs for a dataset in a folder + runs = langfuse.get_dataset_runs("evaluation/qa-dataset") + for run in runs.data: + print(f"Run: {run.name}") + ``` + """ + try: + langfuse_logger.debug(f"Getting dataset runs for {dataset_name}") + return self.api.datasets.get_runs( + dataset_name=self._url_encode(dataset_name), + page=page, + limit=limit, + ) + except Error as e: + handle_fern_exception(e) + raise e + + def delete_dataset_run( + self, + dataset_name: str, + run_name: str, + ) -> DeleteDatasetRunResponse: + """Delete a dataset run and all its run items. + + This action is irreversible. This method properly URL-encodes the dataset + and run names, supporting folder-format names that contain slashes. + + Args: + dataset_name: The name of the dataset (can include slashes for folder structure). + run_name: The name of the run to delete (can include slashes). + + Returns: + DeleteDatasetRunResponse: Confirmation of the deletion. + + Raises: + NotFoundError: If the dataset or run is not found. + + Example: + ```python + # Delete a run from a dataset in a folder + result = langfuse.delete_dataset_run( + dataset_name="evaluation/qa-dataset", + run_name="old-experiment" + ) + print(result.message) # "Dataset run deleted successfully" + ``` + """ + try: + langfuse_logger.debug(f"Deleting dataset run {dataset_name}/{run_name}") + return self.api.datasets.delete_run( + dataset_name=self._url_encode(dataset_name), + run_name=self._url_encode(run_name), + ) + except Error as e: + handle_fern_exception(e) + raise e + def run_experiment( self, *, diff --git a/tests/test_datasets.py b/tests/test_datasets.py index c1b81868d..2c596b4f5 100644 --- a/tests/test_datasets.py +++ b/tests/test_datasets.py @@ -220,7 +220,7 @@ def test_get_dataset_runs(): langfuse.flush() time.sleep(1) # Give API time to process - runs = langfuse.api.datasets.get_runs(dataset_name) + runs = langfuse.get_dataset_runs(dataset_name) assert len(runs.data) == 2 assert runs.data[0].name == run_name_2 @@ -419,3 +419,117 @@ def execute_dataset_item(item, run_name): assert "args" in trace.input assert trace.input["args"][0] == expected_input assert trace.output == expected_input + + +def test_get_dataset_with_folder_name(): + """Test that get_dataset works with folder-format names containing slashes.""" + langfuse = Langfuse(debug=False) + + # Create a dataset with slashes in the name (folder format) + folder_name = f"folder/subfolder/dataset-{create_uuid()[:8]}" + langfuse.create_dataset(name=folder_name) + + # Fetch the dataset using the wrapper method + dataset = langfuse.get_dataset(folder_name) + + assert dataset.name == folder_name + assert "/" in dataset.name # Verify slashes are preserved + + +def test_get_dataset_runs_with_folder_name(): + """Test that get_dataset_runs works with folder-format dataset names.""" + langfuse = Langfuse(debug=False) + + # Create a dataset with slashes in the name + folder_name = f"folder/subfolder/dataset-{create_uuid()[:8]}" + langfuse.create_dataset(name=folder_name) + + # Create a dataset item + langfuse.create_dataset_item(dataset_name=folder_name, input={"test": "data"}) + + dataset = langfuse.get_dataset(folder_name) + assert len(dataset.items) == 1 + + # Create a run + run_name = f"run-{create_uuid()[:8]}" + for item in dataset.items: + with item.run(run_name=run_name): + pass + + langfuse.flush() + time.sleep(1) # Give API time to process + + # Fetch runs using the new wrapper method + runs = langfuse.get_dataset_runs(folder_name) + + assert len(runs.data) == 1 + assert runs.data[0].name == run_name + + +def test_get_dataset_run_with_folder_names(): + """Test that get_dataset_run works with folder-format dataset and run names.""" + langfuse = Langfuse(debug=False) + + # Create a dataset with slashes in the name + folder_name = f"folder/subfolder/dataset-{create_uuid()[:8]}" + langfuse.create_dataset(name=folder_name) + + # Create a dataset item + langfuse.create_dataset_item(dataset_name=folder_name, input={"test": "data"}) + + dataset = langfuse.get_dataset(folder_name) + assert len(dataset.items) == 1 + + # Create a run with slashes in the name + run_name = f"run/nested/{create_uuid()[:8]}" + for item in dataset.items: + with item.run(run_name=run_name, run_metadata={"key": "value"}): + pass + + langfuse.flush() + time.sleep(1) # Give API time to process + + # Fetch the specific run using the new wrapper method + run = langfuse.get_dataset_run(folder_name, run_name) + + assert run.name == run_name + assert run.metadata == {"key": "value"} + assert "/" in folder_name # Verify slashes are preserved + + +def test_delete_dataset_run_with_folder_names(): + """Test that delete_dataset_run works with folder-format dataset and run names.""" + langfuse = Langfuse(debug=False) + + # Create a dataset with slashes in the name + folder_name = f"folder/subfolder/dataset-{create_uuid()[:8]}" + langfuse.create_dataset(name=folder_name) + + # Create a dataset item + langfuse.create_dataset_item(dataset_name=folder_name, input={"test": "data"}) + + dataset = langfuse.get_dataset(folder_name) + + # Create a run with slashes in the name + run_name = f"run/to/delete/{create_uuid()[:8]}" + for item in dataset.items: + with item.run(run_name=run_name): + pass + + langfuse.flush() + time.sleep(1) # Give API time to process + + # Verify the run exists + runs_before = langfuse.get_dataset_runs(folder_name) + assert len(runs_before.data) == 1 + + # Delete the run using the new wrapper method + result = langfuse.delete_dataset_run(folder_name, run_name) + + assert result.message is not None + + time.sleep(1) # Give API time to process deletion + + # Verify the run is deleted + runs_after = langfuse.get_dataset_runs(folder_name) + assert len(runs_after.data) == 0