diff --git a/crowdin_api/api_resources/ai/enums.py b/crowdin_api/api_resources/ai/enums.py index c4c16c9..d107581 100644 --- a/crowdin_api/api_resources/ai/enums.py +++ b/crowdin_api/api_resources/ai/enums.py @@ -37,3 +37,8 @@ class EditAIProviderPath(Enum): CONFIG = "/config" IS_ENABLED = "/isEnabled" USE_SYSTEM_CREDENTIALS = "/useSystemCredentials" + + +class DatasetPurpose(Enum): + TRAINING = "training" + VALIDATION = "validation" diff --git a/crowdin_api/api_resources/ai/resource.py b/crowdin_api/api_resources/ai/resource.py index 47c2ba2..4bd8fab 100644 --- a/crowdin_api/api_resources/ai/resource.py +++ b/crowdin_api/api_resources/ai/resource.py @@ -9,6 +9,8 @@ EditAIProviderRequestScheme, GoogleGeminiChatProxy, OtherChatProxy, + GenerateAIPromptFineTuningDatasetRequest, + CreateAIPromptFineTuningJobRequest, ) @@ -211,6 +213,118 @@ def create_ai_proxy_chat_completion( request_data=request_data, ) + def get_ai_prompt_fine_tuning_datasets_path( + self, + user_id: int, + ai_prompt_id: Optional[int] = None, + job_identifier: Optional[str] = None + ): + if job_identifier is not None: + return f"users/{user_id}/ai/prompts/{ai_prompt_id}/fine-tuning/datasets/{job_identifier}" + return f"users/{user_id}/ai/prompts/{ai_prompt_id}/fine-tuning/datasets" + + def get_ai_prompt_fine_tuning_jobs_path( + self, + user_id: int, + ai_prompt_id: Optional[int] = None, + job_identifier: Optional[str] = None + ): + if job_identifier is not None: + return f"users/{user_id}/ai/prompts/{ai_prompt_id}/fine-tuning/jobs/{job_identifier}" + return f"users/{user_id}/ai/prompts/{ai_prompt_id}/fine-tuning/jobs" + + def generate_ai_prompt_fine_tuning_dataset( + self, + user_id: int, + ai_prompt_id: int, + request_data: GenerateAIPromptFineTuningDatasetRequest, + ): + """ + Generate AI Prompt Fine-Tuning Dataset + + Link to documentation: + https://support.crowdin.com/developer/api/v2/#tag/AI/operation/api.ai.prompts.fine-tuning.datasets.post + """ + + return self.requester.request( + method="post", + path=self.get_ai_prompt_fine_tuning_datasets_path(user_id, ai_prompt_id), + request_data=request_data, + ) + + def get_ai_prompt_fine_tuning_dataset_generation_status( + self, + user_id: int, + ai_prompt_id: int, + job_identifier: str + ): + """ + Get AI Prompt Fine-Tuning Dataset Generation Status + + Link to documentation: + https://support.crowdin.com/developer/api/v2/#tag/AI/operation/api.users.ai.prompts.fine-tuning.datasets.get + """ + + return self.requester.request( + method="get", + path=self.get_ai_prompt_fine_tuning_datasets_path(user_id, ai_prompt_id, job_identifier), + ) + + def create_ai_prompt_fine_tuning_job( + self, + user_id: int, + ai_prompt_id: int, + request_data: CreateAIPromptFineTuningJobRequest + ): + """ + Create AI Prompt Fine-Tuning Job + + Link to documentation: + https://support.crowdin.com/developer/api/v2/#tag/AI/operation/api.ai.prompts.fine-tuning.jobs.post + """ + + return self.requester.request( + method="post", + path=self.get_ai_prompt_fine_tuning_jobs_path(user_id, ai_prompt_id), + request_data=request_data, + ) + + def get_ai_prompt_fine_tuning_job_status( + self, + user_id: int, + ai_prompt_id: int, + job_identifier: str + ): + """ + Get AI Prompt Fine-Tuning Job Status + + Link to documentation: + https://support.crowdin.com/developer/api/v2/#tag/AI/operation/api.users.ai.prompts.fine-tuning.jobs.get + """ + + return self.requester.request( + method="get", + path=self.get_ai_prompt_fine_tuning_jobs_path(user_id, ai_prompt_id, job_identifier), + ) + + def download_ai_prompt_fine_tuning_dataset( + self, + user_id: int, + ai_prompt_id: int, + job_identifier: str + ): + """ + Download AI Prompt Fine-Tuning Dataset + + Link to documentation: + https://support.crowdin.com/developer/api/v2/#tag/AI/operation/api.users.ai.prompts.fine-tuning.datasets.download.get + """ + + return self.requester.request( + method="get", + path=self.get_ai_prompt_fine_tuning_datasets_path(user_id, ai_prompt_id, job_identifier) + "/download", + ) + class EnterpriseAIResource(BaseResource): """ diff --git a/crowdin_api/api_resources/ai/tests/test_ai_resources.py b/crowdin_api/api_resources/ai/tests/test_ai_resources.py index f43e8c6..3f98197 100644 --- a/crowdin_api/api_resources/ai/tests/test_ai_resources.py +++ b/crowdin_api/api_resources/ai/tests/test_ai_resources.py @@ -1,9 +1,16 @@ +from datetime import datetime, timezone from unittest import mock import pytest -from crowdin_api.api_resources.ai.enums import AIPromptAction, AIProviderType +from crowdin_api.api_resources.ai.enums import AIPromptAction, AIProviderType, DatasetPurpose from crowdin_api.api_resources.ai.resource import AIResource, EnterpriseAIResource -from crowdin_api.api_resources.ai.types import AIPromptOperation, EditAIPromptPath +from crowdin_api.api_resources.ai.types import ( + AIPromptOperation, + EditAIPromptPath, + CreateAIPromptFineTuningJobRequest, + HyperParameters, + TrainingOptions, GenerateAIPromptFineTuningDatasetRequest +) from crowdin_api.requester import APIRequester @@ -405,6 +412,176 @@ def test_create_ai_proxy_chat_completion(self, m_request, base_absolut_url): request_data=request_data, ) + @pytest.mark.parametrize( + "incoming_data, request_data", + ( + ( + GenerateAIPromptFineTuningDatasetRequest( + projectIds=[1], + tmIds=[2, 3], + purpose=DatasetPurpose.TRAINING.value, + dateFrom=datetime(2019, 9, 23, 11, 26, 54, + tzinfo=timezone.utc).isoformat(), + dateTo=datetime(2019, 9, 23, 11, 26, 54, + tzinfo=timezone.utc).isoformat(), + maxFileSize=20, + minExamplesCount=2, + maxExamplesCount=10 + ), + { + "projectIds": [ + 1 + ], + "tmIds": [ + 2, 3 + ], + "purpose": "training", + "dateFrom": "2019-09-23T11:26:54+00:00", + "dateTo": "2019-09-23T11:26:54+00:00", + "maxFileSize": 20, + "minExamplesCount": 2, + "maxExamplesCount": 10 + } + ), + ), + ) + @mock.patch("crowdin_api.requester.APIRequester.request") + def test_generate_ai_prompt_fine_tuning_dataset(self, m_request, incoming_data, request_data, base_absolut_url): + m_request.return_value = "response" + + user_id = 1 + ai_prompt_id = 2 + + resource = self.get_resource(base_absolut_url) + assert ( + resource.generate_ai_prompt_fine_tuning_dataset(user_id, ai_prompt_id, request_data=incoming_data) + == "response" + ) + m_request.assert_called_once_with( + method="post", + path=f"users/{user_id}/ai/prompts/{ai_prompt_id}/fine-tuning/datasets", + request_data=request_data, + ) + + @mock.patch("crowdin_api.requester.APIRequester.request") + def test_get_ai_prompt_fine_tuning_dataset_generation_status(self, m_request, base_absolut_url): + m_request.return_value = "response" + + user_id = 1 + ai_prompt_id = 2 + job_identifier = "id" + + resource = self.get_resource(base_absolut_url) + assert ( + resource.get_ai_prompt_fine_tuning_dataset_generation_status(user_id, ai_prompt_id, job_identifier) + == "response" + ) + m_request.assert_called_once_with( + method="get", + path=f"users/{user_id}/ai/prompts/{ai_prompt_id}/fine-tuning/datasets/{job_identifier}", + ) + + @pytest.mark.parametrize( + "incoming_data, request_data", + ( + ( + CreateAIPromptFineTuningJobRequest( + dryRun=False, + hyperparameters=HyperParameters( + batchSize=1, + learningRateMultiplier=2.0, + nEpochs=100, + ), + trainingOptions=TrainingOptions( + projectIds=[1], + tmIds=[2], + dateFrom=datetime(2019, 9, 23, 11, 26, 54, + tzinfo=timezone.utc).isoformat(), + dateTo=datetime(2019, 9, 23, 11, 26, 54, + tzinfo=timezone.utc).isoformat(), + maxFileSize=10, + minExamplesCount=200, + maxExamplesCount=300 + ) + ), + { + "dryRun": False, + "hyperparameters": { + "batchSize": 1, + "learningRateMultiplier": 2.0, + "nEpochs": 100, + }, + "trainingOptions": { + "projectIds": [1], + "tmIds": [2], + "dateFrom": "2019-09-23T11:26:54+00:00", + "dateTo": "2019-09-23T11:26:54+00:00", + "maxFileSize": 10, + "minExamplesCount": 200, + "maxExamplesCount": 300 + } + } + ), + ), + ) + @mock.patch("crowdin_api.requester.APIRequester.request") + def test_create_ai_prompt_fine_tuning_job(self, m_request, incoming_data, request_data, base_absolut_url): + m_request.return_value = "response" + + user_id = 1 + ai_prompt_id = 2 + + resource = self.get_resource(base_absolut_url) + assert ( + resource.create_ai_prompt_fine_tuning_job(user_id, ai_prompt_id, request_data=incoming_data) + == "response" + ) + m_request.assert_called_once_with( + method="post", + path=f"users/{user_id}/ai/prompts/{ai_prompt_id}/fine-tuning/jobs", + request_data=request_data, + ) + + @mock.patch("crowdin_api.requester.APIRequester.request") + def test_get_ai_prompt_fine_tuning_job_status(self, m_request, base_absolut_url): + m_request.return_value = "response" + + user_id = 1 + ai_prompt_id = 2 + job_identifier = "id" + + resource = self.get_resource(base_absolut_url) + assert ( + resource.get_ai_prompt_fine_tuning_job_status(user_id, ai_prompt_id, job_identifier) + == "response" + ) + m_request.assert_called_once_with( + method="get", + path=f"users/{user_id}/ai/prompts/{ai_prompt_id}/fine-tuning/jobs/{job_identifier}", + ) + + @mock.patch("crowdin_api.requester.APIRequester.request") + def test_download_ai_prompt_fine_tuning_dataset( + self, + m_request, + base_absolut_url + ): + m_request.return_value = "response" + + user_id = 1 + ai_prompt_id = 2 + job_identifier = "id" + + resource = self.get_resource(base_absolut_url) + assert ( + resource.download_ai_prompt_fine_tuning_dataset(user_id, ai_prompt_id, job_identifier) + == "response" + ) + m_request.assert_called_once_with( + method="get", + path=f"users/{user_id}/ai/prompts/{ai_prompt_id}/fine-tuning/datasets/{job_identifier}/download", + ) + class TestEnterpriseAIResources: resource_class = EnterpriseAIResource diff --git a/crowdin_api/api_resources/ai/types.py b/crowdin_api/api_resources/ai/types.py index 5bcd532..ecdbe1c 100644 --- a/crowdin_api/api_resources/ai/types.py +++ b/crowdin_api/api_resources/ai/types.py @@ -148,3 +148,47 @@ class GoogleGeminiChatProxy(TypedDict): class OtherChatProxy(TypedDict): stream: Optional[bool] + + +class GenerateAIPromptFineTuningDatasetRequest(TypedDict): + projectIds: Optional[Iterable[int]] + tmIds: Optional[Iterable[int]] + purpose: Optional[str] + dateFrom: str + dateTo: str + maxFileSize: Optional[int] + minExamplesCount: Optional[int] + maxExamplesCount: Optional[int] + + +class HyperParameters(TypedDict): + batchSize: int + learningRateMultiplier: float + nEpochs: int + + +class TrainingOptions(TypedDict): + projectIds: Optional[Iterable[int]] + tmIds: Optional[Iterable[int]] + dateFrom: Optional[str] + dateTo: Optional[str] + maxFileSize: Optional[int] + minExamplesCount: Optional[int] + maxExamplesCount: Optional[int] + + +class ValidationOptions(TypedDict): + projectIds: Optional[Iterable[int]] + tmIds: Optional[Iterable[int]] + dateFrom: Optional[str] + dateTo: Optional[str] + maxFileSize: Optional[int] + minExamplesCount: Optional[int] + maxExamplesCount: Optional[int] + + +class CreateAIPromptFineTuningJobRequest(TypedDict): + dryRun: Optional[bool] + hyperparameters: Optional[HyperParameters] + trainingOptions: TrainingOptions + validationOptions: Optional[ValidationOptions]