Skip to content

Commit

Permalink
machinery: add Azure OpenAI support
Browse files Browse the repository at this point in the history
  • Loading branch information
michael-smt committed Sep 17, 2024
1 parent dc9d575 commit c306d07
Show file tree
Hide file tree
Showing 6 changed files with 180 additions and 61 deletions.
26 changes: 26 additions & 0 deletions docs/admin/machine.rst
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,32 @@ You can also specify a custom category to use `custom translator <https://learn.
`"Authenticating with a Multi-service resource" <https://learn.microsoft.com/en-us/azure/ai-services/translator/reference/v3-0-reference#authenticating-with-a-multi-service-resource>`_
`"Authenticating with an access token" section <https://learn.microsoft.com/en-us/azure/ai-services/translator/reference/v3-0-reference#authenticating-with-an-access-token>`_

.. _mt-azure-openai:

Azure OpenAI
------------

.. versionadded:: 5.8

:Service ID: ``openai``
:Configuration: +------------------+---------------------+---------------------------------------------------------------------------------------------------------------------------+
| ``key`` | API key | |
+------------------+---------------------+---------------------------------------------------------------------------------------------------------------------------+
| ``azure_endpoint`` | Azure OpenAI Instance API URL | e.g. ``https://<my-instance>.openai.azure.com`` |
+------------------+---------------------+---------------------------------------------------------------------------------------------------------------------------+
| ``deployment`` | Deployment name | The model's unique deployment name. |
+------------------+---------------------+---------------------------------------------------------------------------------------------------------------------------+
| ``persona`` | Translator persona | Describe the persona of translator to improve the accuracy of the translation. For example: “You are a squirrel breeder.” |
+------------------+---------------------+---------------------------------------------------------------------------------------------------------------------------+
| ``style`` | Translator style | Describe the style of translation. For example: “Use informal language.” |
+------------------+---------------------+---------------------------------------------------------------------------------------------------------------------------+

Performs translation using `OpenAI`_ hosted on Azure.

.. seealso::

:ref:`mt-openai`

.. _mt-modernmt:

ModernMT
Expand Down
1 change: 1 addition & 0 deletions docs/changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Not yet released.

* :ref:`Searching` now supports filtering by object path.
* Merge requests credentials can now be passed in the repository URL, see :ref:`settings-credentials`.
* :ref:`mt-azure-openai` automatic suggestion service.

**Improvements**

Expand Down
70 changes: 47 additions & 23 deletions weblate/machinery/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,31 @@ class DeepLMachineryForm(KeyURLMachineryForm):
)


class OpenAIMachineryForm(KeyMachineryForm):
class BaseOpenAIMachineryForm(KeyMachineryForm):
persona = forms.CharField(
label=pgettext_lazy(
"Automatic suggestion service configuration",
"Translator persona",
),
widget=forms.Textarea,
help_text=gettext_lazy(
"Describe the persona of translator to improve the accuracy of the translation. For example: “You are a squirrel breeder.”"
),
required=False,
)
style = forms.CharField(
label=pgettext_lazy(
"Automatic suggestion service configuration",
"Translator style",
),
widget=forms.Textarea,
help_text=gettext_lazy(
"Describe the style of translation. For example: “Use informal language.”"
),
required=False,
)

class OpenAIMachineryForm(BaseOpenAIMachineryForm):
# Ordering choices here defines priority for automatic selection
MODEL_CHOICES = (
("auto", pgettext_lazy("OpenAI model selection", "Automatic selection")),
Expand Down Expand Up @@ -329,28 +353,6 @@ class OpenAIMachineryForm(KeyMachineryForm):
help_text=gettext_lazy("Only needed when model is set to 'Custom model'"),
required=False,
)
persona = forms.CharField(
label=pgettext_lazy(
"Automatic suggestion service configuration",
"Translator persona",
),
widget=forms.Textarea,
help_text=gettext_lazy(
"Describe the persona of translator to improve the accuracy of the translation. For example: “You are a squirrel breeder.”"
),
required=False,
)
style = forms.CharField(
label=pgettext_lazy(
"Automatic suggestion service configuration",
"Translator style",
),
widget=forms.Textarea,
help_text=gettext_lazy(
"Describe the style of translation. For example: “Use informal language.”"
),
required=False,
)

def clean(self) -> None:
has_custom_model = bool(self.cleaned_data.get("custom_model"))
Expand All @@ -364,3 +366,25 @@ def clean(self) -> None:
{"model": gettext("Choose custom model here to enable it.")}
)
super().clean()

class AzureOpenAIMachineryForm(BaseOpenAIMachineryForm):
azure_endpoint = WeblateServiceURLField(
label=pgettext_lazy(
"Automatic suggestion service configuration",
"Azure OpenAI Endpoint URL",
),
widget=forms.TextInput,
help_text=gettext_lazy(
"Endpoint URL of the Azure OpenAI API"
),
)
deployment = forms.CharField(
label=pgettext_lazy(
"Automatic suggestion service configuration",
"AzureOpenAI deployment",
),
widget=forms.TextInput,
help_text=gettext_lazy(
"Deployment name of the Azure OpenAI model"
),
)
1 change: 1 addition & 0 deletions weblate/machinery/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ class WeblateConf(AppConf):
"weblate.machinery.ibm.IBMTranslation",
"weblate.machinery.systran.SystranTranslation",
"weblate.machinery.openai.OpenAITranslation",
"weblate.machinery.openai.AzureOpenAITranslation",
"weblate.machinery.weblatetm.WeblateTranslation",
"weblate.memory.machine.WeblateMemory",
"weblate.machinery.cyrtranslit.CyrTranslitTranslation",
Expand Down
99 changes: 62 additions & 37 deletions weblate/machinery/openai.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
DownloadMultipleTranslations,
MachineTranslationError,
)
from .forms import OpenAIMachineryForm
from .forms import OpenAIMachineryForm, AzureOpenAIMachineryForm

if TYPE_CHECKING:
from weblate.trans.models import Unit
Expand Down Expand Up @@ -58,51 +58,16 @@
"""


class OpenAITranslation(BatchMachineTranslation):
name = "OpenAI"
class BaseOpenAITranslation(BatchMachineTranslation):
max_score = 90
request_timeout = 60

settings_form = OpenAIMachineryForm

def __init__(self, settings=None) -> None:
from openai import OpenAI

super().__init__(settings)
self.client = OpenAI(
api_key=self.settings["key"],
timeout=self.request_timeout,
base_url=self.settings.get("base_url") or None,
)
self._models: None | set[str] = None

def is_supported(self, source, language) -> bool:
return True

def get_model(self) -> str:
if self._models is None:
cache_key = self.get_cache_key("models")
models_cache = cache.get(cache_key)
if models_cache is not None:
# hiredis-py 3 makes list from set
self._models = set(models_cache)
else:
self._models = {model.id for model in self.client.models.list()}
cache.set(cache_key, self._models, 3600)

if self.settings["model"] in self._models:
return self.settings["model"]
if self.settings["model"] == "auto":
for model, _name in self.settings_form.MODEL_CHOICES:
if model == "auto":
continue
if model in self._models:
return model
if self.settings["model"] == "custom":
return self.settings["custom_model"]

raise MachineTranslationError(f"Unsupported model: {self.settings['model']}")

def format_prompt_part(self, name: Literal["style", "persona"]):
text = self.settings[name]
text = text.strip()
Expand Down Expand Up @@ -280,3 +245,63 @@ def _download(
"source": text,
}
)

class OpenAITranslation(BaseOpenAITranslation):
name = "OpenAI"

settings_form = OpenAIMachineryForm

def __init__(self, settings=None) -> None:
from openai import OpenAI

super().__init__(settings)
self.client = OpenAI(
api_key=self.settings["key"],
timeout=self.request_timeout,
base_url=self.settings.get("base_url") or None,
)
self._models: None | set[str] = None

def get_model(self) -> str:
if self._models is None:
cache_key = self.get_cache_key("models")
models_cache = cache.get(cache_key)
if models_cache is not None:
# hiredis-py 3 makes list from set
self._models = set(models_cache)
else:
self._models = {model.id for model in self.client.models.list()}
cache.set(cache_key, self._models, 3600)

if self.settings["model"] in self._models:
return self.settings["model"]
if self.settings["model"] == "auto":
for model, _name in self.settings_form.MODEL_CHOICES:
if model == "auto":
continue
if model in self._models:
return model
if self.settings["model"] == "custom":
return self.settings["custom_model"]

raise MachineTranslationError(f"Unsupported model: {self.settings['model']}")


class AzureOpenAITranslation(BaseOpenAITranslation):
name = "Azure OpenAI"
settings_form = AzureOpenAIMachineryForm

def __init__(self, settings=None) -> None:
from openai import AzureOpenAI

super().__init__(settings)
self.client = AzureOpenAI(
api_key=self.settings["key"],
api_version="2024-06-01",
timeout=self.request_timeout,
azure_endpoint=self.settings.get("azure_endpoint") or None,
azure_deployment=self.settings["deployment"]
)

def get_model(self) -> str:
return self.settings["deployment"]
44 changes: 43 additions & 1 deletion weblate/machinery/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
from weblate.machinery.modernmt import ModernMTTranslation
from weblate.machinery.mymemory import MyMemoryTranslation
from weblate.machinery.netease import NETEASE_API_ROOT, NeteaseSightTranslation
from weblate.machinery.openai import OpenAITranslation
from weblate.machinery.openai import OpenAITranslation, AzureOpenAITranslation
from weblate.machinery.saptranslationhub import SAPTranslationHub
from weblate.machinery.systran import SystranTranslation
from weblate.machinery.tmserver import TMServerTranslation
Expand Down Expand Up @@ -1683,6 +1683,48 @@ def test_clean_custom(self) -> None:
self.assertFalse(form.is_valid())


class AzureOpenAITranslationTest(OpenAITranslationTest):
MACHINE_CLS = AzureOpenAITranslation
CONFIGURATION = {
"key": "x",
"deployment": "my-deployment",
"persona": "",
"style": "",
"azure_endpoint": "https://my-instance.openai.azure.com",
}

def mock_response(self) -> None:
respx.post(
"https://my-instance.openai.azure.com/openai/deployments/my-deployment/chat/completions?api-version=2024-06-01",
).mock(
httpx.Response(
200,
json={
"id": "chatcmpl-123",
"object": "chat.completion",
"created": 1677652288,
"model": "my-deployment",
"system_fingerprint": "fp_44709d6fcb",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "Ahoj světe",
},
"finish_reason": "stop",
}
],
"usage": {
"prompt_tokens": 9,
"completion_tokens": 12,
"total_tokens": 21,
},
},
)
)


class WeblateTranslationTest(TransactionsTestMixin, FixtureTestCase):
def test_empty(self) -> None:
machine = WeblateTranslation({})
Expand Down

0 comments on commit c306d07

Please sign in to comment.