Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Budget for Summ AI #3065

Draft
wants to merge 1 commit into
base: develop
Choose a base branch
from
Draft

Conversation

PeterNerlich
Copy link
Collaborator

Short description

This PR adds separate budget accounting for Summ AI. Until now the usage of Summ AI was not metered at all by us.

Proposed changes

  • Add summ_ai_budget_used to the Region model, as well as various auxiliary properties and budget related changes analogous to machine translations
  • Update summ_ai_budget_used of the region accordingly after a batch of translations was performed
    This required a change to how the asynchronous translation flow works:
  • Instead of putting all tasks into the PatientTaskQueue from the start, more are added every time the queue gets low enough
    • This is because individual tasks don't represent entire content translation objects, but individual snippets (e.g. a single paragraph), since Summ AI doesn't handle HTML content (well enough for our purposes)
    • We want to avoid sending Summ AI requests that we get billed for and then finding out that the region didn't have the translation budget for it
  • Introduce the BudgetEstimate class to keep track of "allocated" budget, via an instance shared with each TranslationHelper – the helper class representing a single content translation object responsible for splitting it up into single translatable units and splicing it back together after translation
  • Pass TranslationHelper instead of TextField instances around to batch translation tasks per content translation
  • Only add all translation tasks necessary for a content translation if it still fits into the budget according to BudgetEstimate
  • This on-demand expansion of the queue is facilitated by means of a management task, during its run any workers trying to fetch a task from an empty queue won't quit because it is likely that more tasks will be available a moment later, and the worker pool shouldn't shrink from such race conditions
  • The PatientTaskQueue is initialized empty and worker() is run directly to fill it before the workers are initialized

Side effects

  • Machine translations with DeepL or Google Cloud Translate are using up a regions budget whether they are successful or not. Translations with Summ AI are metered in the same way, but whether there were retries because of API rate limiting as well as whether translations were not attempted at all because the budget was already used up is not taken into account. However, I think this should not pose too much of a problem.
  • This is a particularly confusing section of our code base, and I play not a small role in this. I hope I added enough and helpful enough comments, please tell me if you think this could be improved!
  • This PR has become so large that we didn't include tests. The existing tests still succeed and guarantee the translation functionality, but the budget metering is not covered.

Resolved issues

This fulfills the first half of #2173


Pull Request Review Guidelines

Copy link

codeclimate bot commented Sep 18, 2024

Code Climate has analyzed commit 292c552 and detected 0 issues on this pull request.

The test coverage on the diff in this pull request is 89.9% (50% is the threshold).

This pull request will bring the total coverage in the repository to 82.8% (0.0% change).

View more on Code Climate.

@JoeyStk JoeyStk added the prio: high Needs to be resolved ASAP. label Oct 2, 2024
@charludo charludo self-requested a review October 8, 2024 06:18
@charludo charludo self-assigned this Oct 8, 2024
Copy link
Contributor

@charludo charludo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am sorry, but this needs a major architectural overhaul. Apart from the suggestions below, the main reason I am saying this is the following:

  • when SummAiApiClient.translate_queryset is called,
  • a lambda function returning a closure calling SummAiApiClient.check_usage (which overrides MachineTranslationApiClient.check_usage) is used in the creation of a BudgetHelper object,
  • which is used for the instantiation of a TranslationHelper object,
  • which has TranslationHelper.check_usage method,
  • which calls its embedded BudgetHelper.check_usage method,
  • which calls BudgetHelper._check_usage,
  • which is the aforementioned closure.

Now back up the chain:

  • BudgetHelper.check_usage is only called by BudgetHelper.allocate,
  • which is only called by TranslationHelper.allocate_budget,
  • which is only called by SummAiApiClient.translate_text_fields,
  • which is only called by SummAiApiClient.translate_queryset.

Frankly, I question the need for the BudgetEstimate class, as well as the helper methods on TranslationHelper.

However, it is difficult to give more directed feedback, simply because of the amount of obfuscation going on due to these enormous call chains.

I have not yet had a chance to examine the async functions.

Comment on lines +16 to +27
if isinstance(translation, AbstractContentTranslation):
attributes = [
getattr(translation, attr, None)
for attr in ["title", "content", "meta_description"]
]

content_to_translate = [
unescape(strip_tags(attr)) for attr in attributes if attr
]
content_to_translate_str = " ".join(content_to_translate)
else:
content_to_translate_str = translation
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This errs on nitpick territory, but: I really dislike the isinstance pattern. What do you think about refactoring this so both this function and check_usage always get a string as the input?

This would require adding a translateable_content (or similar) property to the AbstractContentTranslation model. For cases where the input is string, nothing needs to be changed; whenever a ACT is passed, pass ACT.translateable_content()` instead?

This would also reduce maintanence overhead if we decide to make more fields trasnslatable in the future, since only this new method would need to be tweaked, not multiple functions for multiple translation providers.

This is not 100% necessary, just thought I would put it out there!


#: cms/models/regions/region.py
msgid "Credits renewal date for simplified language translation"
msgstr "Credits Zurücksetzungsdatum für Übersetzung in Einfache Sprache"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
msgstr "Credits Zurücksetzungsdatum für Übersetzung in Einfache Sprache"
msgstr "Zurücksetzungsdatum der Credits für Übersetzung in Einfache Sprache"

I know, this is copied from the existing translation, I just noticed that this is probably a more understandable translation. Also applies to the other instance of this.

@@ -7841,6 +7865,14 @@ msgstr "Bereits verbraucht"
msgid "Remaining words"
msgstr "Verbleibende Wörter"

#: cms/templates/regions/region_form.html
msgid "Currently HIX is globally deactivated"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
msgid "Currently HIX is globally deactivated"
msgid "HIX is currently deactivated globally"

Has nothing to do with this PR, really, just noticed it in the diff. THis is a very German ordering of the words, the suggestion is the far more "natural" sentence in English 😄

Comment on lines +78 to +102
def check_usage(
self,
region: Region,
source_translation: str | AbstractContentTranslation,
allocated_budget: int = 0,
) -> tuple[bool, int]:
"""
This function checks if the attempted translation would exceed the region's word limit

:param region: region for which to check usage
:param source_translation: single content object
:param allocated_budget: how many additional words should be considered already spent
:return: translation would exceed limit, word count of attempted translation
"""
words = word_count(source_translation)

region.refresh_from_db()
# Allow up to SUMM_AI_SOFT_MARGIN more words than the actual limit
word_count_leeway = max(
1, words + allocated_budget - settings.SUMM_AI_SOFT_MARGIN
)
translation_exceeds_limit = region.summ_ai_budget_remaining < word_count_leeway

return (translation_exceeds_limit, words)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is almost 1:1 from the MachineTranslationApiClient. Suggestion to avoid this code duplication:

  • add margin_field and budget_field as class variables of MachineTranslationApiClient
  • use these fields in the check_usage method of that class. Then you just need to overwrite the fields in SummAiApiClient.

Copy link
Contributor

@charludo charludo Oct 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you either open an issue to refactor this file, or split it into multiple files in this PR? 🙈 660 lines is way too much for a utils.py 😆

@property
def valid(self) -> bool:
"""
Wether or not the translation was successful
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Wether or not the translation was successful
Whether or not the translation was successful

@property
def word_count(self) -> int:
"""
How many words need to be translated..
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
How many words need to be translated..
How many words need to be translated.

@JoeyStk JoeyStk marked this pull request as draft December 7, 2024 11:54
@JoeyStk
Copy link
Contributor

JoeyStk commented Dec 7, 2024

As this needs some major refactoring and time is sparse at the moment, it'll take some time until we will get back to this

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
prio: high Needs to be resolved ASAP.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants