diff --git a/crowdin_api/api_resources/ai/enums.py b/crowdin_api/api_resources/ai/enums.py index fcd4d0b..920c9d0 100644 --- a/crowdin_api/api_resources/ai/enums.py +++ b/crowdin_api/api_resources/ai/enums.py @@ -87,3 +87,24 @@ class AiReportFormat(Enum): class ListSupportedAiModelsOrderBy(Enum): KNOWLEDGE_CUTOFF = "knowledgeCutoff" RELEASE_DATE = "releaseDate" + + +class AiRequestLogStatus(Enum): + PENDING = "pending" + SUCCESS = "success" + ERROR = "error" + TIMEOUT = "timeout" + + +class AiRequestLogSourceAction(Enum): + AI_PROXY = "ai_proxy" + AI_GATEWAY = "ai_gateway" + AI_TRANSLATE_STRINGS = "ai_translate_strings" + AI_FILE_TRANSLATE = "ai_file_translate" + AI_PROMPT_COMPLETION = "ai_prompt_completion" + PRE_TRANSLATE_MANUAL = "pre_translate:manual" + PRE_TRANSLATE_WORKFLOW = "pre_translate:workflow" + AI_ALIGNMENT = "ai_alignment" + QA_CHECK = "qa_check" + AI_SUGGESTION = "ai_suggestion" + ADVISOR = "advisor" diff --git a/crowdin_api/api_resources/ai/resource.py b/crowdin_api/api_resources/ai/resource.py index 3b06b8c..92ce2f3 100644 --- a/crowdin_api/api_resources/ai/resource.py +++ b/crowdin_api/api_resources/ai/resource.py @@ -1,9 +1,12 @@ +from datetime import datetime from typing import Dict, Iterable, Optional, Union from crowdin_api.api_resources.abstract.resources import BaseResource from crowdin_api.api_resources.ai.enums import ( AIPromptAction, AiPromptFineTuningJobStatus, + AiRequestLogSourceAction, + AiRequestLogStatus, AIProviderType, ) from crowdin_api.api_resources.ai.types import ( @@ -626,6 +629,60 @@ def download_ai_report( path=self.get_ai_reports_path(user_id, ai_report_id) + "/download", ) + def get_ai_request_logs_path(self, user_id: int): + return f"users/{user_id}/ai/request-logs" + + def list_ai_request_logs( + self, + user_id: int, + request_id: Optional[str] = None, + project_id: Optional[int] = None, + request_user_id: Optional[int] = None, + ai_provider_id: Optional[int] = None, + model: Optional[str] = None, + source_action: Optional[AiRequestLogSourceAction] = None, + prompt_action: Optional[str] = None, + statuses: Optional[Iterable[AiRequestLogStatus]] = None, + system_credentials: Optional[bool] = None, + is_auto_triggered: Optional[bool] = None, + token_name: Optional[str] = None, + oauth_client_id: Optional[str] = None, + created_after: Optional[datetime] = None, + created_before: Optional[datetime] = None, + limit: Optional[int] = None, + offset: Optional[int] = None, + ): + """ + List AI Request Logs + + Link to documentation: + https://developer.crowdin.com/api/v2/#operation/api.ai.requestLogs.getMany + """ + + params = { + "requestId": request_id, + "projectId": project_id, + "userId": request_user_id, + "aiProviderId": ai_provider_id, + "model": model, + "sourceAction": source_action, + "promptAction": prompt_action, + "statuses": convert_enum_collection_to_string_if_exists(statuses), + "systemCredentials": system_credentials, + "isAutoTriggered": is_auto_triggered, + "tokenName": token_name, + "oauthClientId": oauth_client_id, + "createdAfter": created_after, + "createdBefore": created_before, + } + params.update(self.get_page_params(limit=limit, offset=offset)) + + return self._get_entire_data( + method="get", + path=self.get_ai_request_logs_path(user_id), + params=params, + ) + def get_ai_settings_path(self, user_id: int): return f"users/{user_id}/ai/settings" @@ -1467,6 +1524,59 @@ def download_ai_report( path=self.get_ai_reports_path(ai_report_id) + "/download", ) + def get_ai_request_logs_path(self): + return "ai/request-logs" + + def list_ai_request_logs( + self, + request_id: Optional[str] = None, + project_id: Optional[int] = None, + request_user_id: Optional[int] = None, + ai_provider_id: Optional[int] = None, + model: Optional[str] = None, + source_action: Optional[AiRequestLogSourceAction] = None, + prompt_action: Optional[str] = None, + statuses: Optional[Iterable[AiRequestLogStatus]] = None, + system_credentials: Optional[bool] = None, + is_auto_triggered: Optional[bool] = None, + token_name: Optional[str] = None, + oauth_client_id: Optional[str] = None, + created_after: Optional[datetime] = None, + created_before: Optional[datetime] = None, + limit: Optional[int] = None, + offset: Optional[int] = None, + ): + """ + List AI Request Logs + + Link to documentation: + https://developer.crowdin.com/enterprise/api/v2/#operation/api.ai.requestLogs.getMany + """ + + params = { + "requestId": request_id, + "projectId": project_id, + "userId": request_user_id, + "aiProviderId": ai_provider_id, + "model": model, + "sourceAction": source_action, + "promptAction": prompt_action, + "statuses": convert_enum_collection_to_string_if_exists(statuses), + "systemCredentials": system_credentials, + "isAutoTriggered": is_auto_triggered, + "tokenName": token_name, + "oauthClientId": oauth_client_id, + "createdAfter": created_after, + "createdBefore": created_before, + } + params.update(self.get_page_params(limit=limit, offset=offset)) + + return self._get_entire_data( + method="get", + path=self.get_ai_request_logs_path(), + params=params, + ) + def get_ai_settings(self): """ Get AI Settings 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 b9fe7dd..5c467b8 100644 --- a/crowdin_api/api_resources/ai/tests/test_ai_resources.py +++ b/crowdin_api/api_resources/ai/tests/test_ai_resources.py @@ -5,6 +5,8 @@ from crowdin_api.api_resources.ai.enums import ( AIPromptAction, AiPromptFineTuningJobStatus, + AiRequestLogSourceAction, + AiRequestLogStatus, AIProviderType, AiReportFormat, AiToolType, @@ -1132,6 +1134,90 @@ def test_download_ai_report(self, m_request, base_absolut_url): path=f"users/{user_id}/ai/reports/{ai_report_id}/download", ) + def test_get_ai_request_logs_path(self, base_absolut_url): + resource = self.get_resource(base_absolut_url) + assert resource.get_ai_request_logs_path(1) == "users/1/ai/request-logs" + + @pytest.mark.parametrize( + "incoming_data, request_params", + ( + ( + {}, + { + "requestId": None, + "projectId": None, + "userId": None, + "aiProviderId": None, + "model": None, + "sourceAction": None, + "promptAction": None, + "statuses": None, + "systemCredentials": None, + "isAutoTriggered": None, + "tokenName": None, + "oauthClientId": None, + "createdAfter": None, + "createdBefore": None, + "limit": 25, + "offset": 0, + }, + ), + ( + { + "request_id": "9d3b1c4e-2f3a-4b5c-8d6e-7f8a9b0c1d2e", + "project_id": 8, + "request_user_id": 42, + "ai_provider_id": 3, + "model": "gpt-5.6-sol", + "source_action": AiRequestLogSourceAction.AI_GATEWAY, + "prompt_action": "pre_translate", + "statuses": [AiRequestLogStatus.PENDING, AiRequestLogStatus.SUCCESS], + "system_credentials": True, + "is_auto_triggered": False, + "token_name": "CI token", + "oauth_client_id": "gpbccUFxAKZDrLm5Nq8t", + "created_after": datetime(2026, 1, 1, tzinfo=timezone.utc), + "created_before": datetime(2026, 1, 2, tzinfo=timezone.utc), + "limit": 10, + "offset": 20, + }, + { + "requestId": "9d3b1c4e-2f3a-4b5c-8d6e-7f8a9b0c1d2e", + "projectId": 8, + "userId": 42, + "aiProviderId": 3, + "model": "gpt-5.6-sol", + "sourceAction": AiRequestLogSourceAction.AI_GATEWAY, + "promptAction": "pre_translate", + "statuses": "pending,success", + "systemCredentials": True, + "isAutoTriggered": False, + "tokenName": "CI token", + "oauthClientId": "gpbccUFxAKZDrLm5Nq8t", + "createdAfter": datetime(2026, 1, 1, tzinfo=timezone.utc), + "createdBefore": datetime(2026, 1, 2, tzinfo=timezone.utc), + "limit": 10, + "offset": 20, + }, + ), + ), + ) + @mock.patch("crowdin_api.requester.APIRequester.request") + def test_list_ai_request_logs( + self, m_request, incoming_data, request_params, base_absolut_url + ): + m_request.return_value = "response" + + user_id = 1 + resource = self.get_resource(base_absolut_url) + assert resource.list_ai_request_logs(user_id, **incoming_data) == "response" + + m_request.assert_called_once_with( + method="get", + path=resource.get_ai_request_logs_path(user_id), + params=request_params, + ) + @mock.patch("crowdin_api.requester.APIRequester.request") def test_get_ai_settings(self, m_request, base_absolut_url): m_request.return_value = "response" @@ -2594,6 +2680,89 @@ def test_download_ai_report(self, m_request, base_absolut_url): path=f"ai/reports/{ai_report_id}/download", ) + def test_get_ai_request_logs_path(self, base_absolut_url): + resource = self.get_resource(base_absolut_url) + assert resource.get_ai_request_logs_path() == "ai/request-logs" + + @pytest.mark.parametrize( + "incoming_data, request_params", + ( + ( + {}, + { + "requestId": None, + "projectId": None, + "userId": None, + "aiProviderId": None, + "model": None, + "sourceAction": None, + "promptAction": None, + "statuses": None, + "systemCredentials": None, + "isAutoTriggered": None, + "tokenName": None, + "oauthClientId": None, + "createdAfter": None, + "createdBefore": None, + "limit": 25, + "offset": 0, + }, + ), + ( + { + "request_id": "9d3b1c4e-2f3a-4b5c-8d6e-7f8a9b0c1d2e", + "project_id": 8, + "request_user_id": 42, + "ai_provider_id": 3, + "model": "gpt-5.6-sol", + "source_action": AiRequestLogSourceAction.AI_GATEWAY, + "prompt_action": "pre_translate", + "statuses": [AiRequestLogStatus.PENDING, AiRequestLogStatus.SUCCESS], + "system_credentials": True, + "is_auto_triggered": False, + "token_name": "CI token", + "oauth_client_id": "gpbccUFxAKZDrLm5Nq8t", + "created_after": datetime(2026, 1, 1, tzinfo=timezone.utc), + "created_before": datetime(2026, 1, 2, tzinfo=timezone.utc), + "limit": 10, + "offset": 20, + }, + { + "requestId": "9d3b1c4e-2f3a-4b5c-8d6e-7f8a9b0c1d2e", + "projectId": 8, + "userId": 42, + "aiProviderId": 3, + "model": "gpt-5.6-sol", + "sourceAction": AiRequestLogSourceAction.AI_GATEWAY, + "promptAction": "pre_translate", + "statuses": "pending,success", + "systemCredentials": True, + "isAutoTriggered": False, + "tokenName": "CI token", + "oauthClientId": "gpbccUFxAKZDrLm5Nq8t", + "createdAfter": datetime(2026, 1, 1, tzinfo=timezone.utc), + "createdBefore": datetime(2026, 1, 2, tzinfo=timezone.utc), + "limit": 10, + "offset": 20, + }, + ), + ), + ) + @mock.patch("crowdin_api.requester.APIRequester.request") + def test_list_ai_request_logs( + self, m_request, incoming_data, request_params, base_absolut_url + ): + m_request.return_value = "response" + + resource = self.get_resource(base_absolut_url) + assert resource.list_ai_request_logs(**incoming_data) == "response" + + m_request.assert_called_once_with( + method="get", + path=resource.get_ai_request_logs_path(), + params=request_params, + ) + @mock.patch("crowdin_api.requester.APIRequester.request") def test_get_ai_settings(self, m_request, base_absolut_url): m_request.return_value = "response"