|
| 1 | +from abc import ABC, abstractmethod |
| 2 | +from dataclasses import dataclass, field |
| 3 | +from typing import Any |
| 4 | + |
| 5 | +from django.conf import settings |
| 6 | +from django.db import models |
| 7 | + |
| 8 | +from kitsune.wiki.models import Revision |
| 9 | + |
| 10 | + |
| 11 | +class TranslationMethod(models.TextChoices): |
| 12 | + """Available translation methods.""" |
| 13 | + AI = "ai", "AI Translation" |
| 14 | + VENDOR = "vendor", "Vendor Translation" |
| 15 | + HYBRID = "hybrid", "AI + Human Review" |
| 16 | + MANUAL = "manual", "Manual Translation" |
| 17 | + |
| 18 | + |
| 19 | +@dataclass |
| 20 | +class TranslationRequest: |
| 21 | + """Request object for translation operations.""" |
| 22 | + revision: Revision |
| 23 | + target_locale: str |
| 24 | + method: Any |
| 25 | + priority: str = "normal" |
| 26 | + metadata: dict[str, Any] = field(default_factory=dict) |
| 27 | + |
| 28 | + |
| 29 | +@dataclass |
| 30 | +class TranslationResult: |
| 31 | + """Result object for translation operations.""" |
| 32 | + success: bool |
| 33 | + translated_content: str | None = None |
| 34 | + method: Any = None |
| 35 | + cost: float = 0.0 |
| 36 | + quality_score: float = 0.0 |
| 37 | + error_message: str | None = None |
| 38 | + metadata: dict[str, Any] = field(default_factory=dict) |
| 39 | + |
| 40 | + |
| 41 | +class TranslationStrategy(ABC): |
| 42 | + """Abstract base class for translation strategies.""" |
| 43 | + |
| 44 | + @abstractmethod |
| 45 | + def translate(self, request: TranslationRequest) -> TranslationResult: |
| 46 | + """Translate content using this strategy.""" |
| 47 | + pass |
| 48 | + |
| 49 | + @abstractmethod |
| 50 | + def can_handle(self, request: TranslationRequest) -> bool: |
| 51 | + """Check if this strategy can handle the request.""" |
| 52 | + pass |
| 53 | + |
| 54 | + @abstractmethod |
| 55 | + def get_cost_estimate(self, request: TranslationRequest) -> float: |
| 56 | + """Get estimated cost for this translation.""" |
| 57 | + pass |
| 58 | + |
| 59 | + |
| 60 | +class AITranslationStrategy(TranslationStrategy): |
| 61 | + """AI/LLM-based translation strategy.""" |
| 62 | + |
| 63 | + def translate(self, request: TranslationRequest) -> TranslationResult: |
| 64 | + """Translate using AI/LLM.""" |
| 65 | + try: |
| 66 | + from kitsune.llm.l10n.translator import translate |
| 67 | + result = translate(request.revision.document, request.target_locale) |
| 68 | + |
| 69 | + # Ensure translated_content is a string |
| 70 | + translated_content = result.get("content", "") |
| 71 | + if not isinstance(translated_content, str): |
| 72 | + translated_content = str(translated_content) |
| 73 | + |
| 74 | + return TranslationResult( |
| 75 | + success=True, |
| 76 | + translated_content=translated_content, |
| 77 | + method=TranslationMethod.AI, |
| 78 | + cost=0.0, |
| 79 | + quality_score=0.85, |
| 80 | + metadata={"ai_model": "gpt-4", "result": result} |
| 81 | + ) |
| 82 | + except Exception as e: |
| 83 | + return TranslationResult( |
| 84 | + success=False, |
| 85 | + method=TranslationMethod.AI, |
| 86 | + error_message=str(e) |
| 87 | + ) |
| 88 | + |
| 89 | + def can_handle(self, request: TranslationRequest) -> bool: |
| 90 | + """AI can handle most content types.""" |
| 91 | + return True |
| 92 | + |
| 93 | + def get_cost_estimate(self, request: TranslationRequest) -> float: |
| 94 | + """AI translation is typically free.""" |
| 95 | + return 0.0 |
| 96 | + |
| 97 | + def _create_translated_revision(self, request: TranslationRequest, result: TranslationResult) -> Revision: |
| 98 | + """Create a translated revision.""" |
| 99 | + # TODO: Implement revision creation |
| 100 | + # Should create the revision object |
| 101 | + return Revision.objects.first() |
| 102 | + |
| 103 | + |
| 104 | +class HybridTranslationStrategy(TranslationStrategy): |
| 105 | + """AI + Human review translation strategy.""" |
| 106 | + |
| 107 | + def translate(self, request: TranslationRequest) -> TranslationResult: |
| 108 | + """Translate using AI + human review workflow.""" |
| 109 | + try: |
| 110 | + # Step 1: AI translation |
| 111 | + ai_strategy = AITranslationStrategy() |
| 112 | + ai_result = ai_strategy.translate(request) |
| 113 | + |
| 114 | + if not ai_result.success: |
| 115 | + return ai_result |
| 116 | + |
| 117 | + review_task = self._create_review_task(request, ai_result) |
| 118 | + |
| 119 | + return TranslationResult( |
| 120 | + success=True, |
| 121 | + translated_content=ai_result.translated_content, |
| 122 | + method=TranslationMethod.HYBRID, |
| 123 | + cost=10.0, |
| 124 | + quality_score=0.95, |
| 125 | + metadata={ |
| 126 | + "ai_result": ai_result.metadata, |
| 127 | + "review_task_id": review_task.get("id"), |
| 128 | + "status": "pending_review" |
| 129 | + } |
| 130 | + ) |
| 131 | + except Exception as e: |
| 132 | + return TranslationResult( |
| 133 | + success=False, |
| 134 | + method=TranslationMethod.HYBRID, |
| 135 | + error_message=str(e) |
| 136 | + ) |
| 137 | + |
| 138 | + def can_handle(self, request: TranslationRequest) -> bool: |
| 139 | + """Hybrid is the default strategy for all AI enabled locales.""" |
| 140 | + return True |
| 141 | + |
| 142 | + def get_cost_estimate(self, request: TranslationRequest) -> float: |
| 143 | + """Estimate cost for hybrid approach.""" |
| 144 | + # TODO: Implement cost estimation for hybrid approach |
| 145 | + return 0.0 |
| 146 | + |
| 147 | + def _create_review_task(self, request: TranslationRequest, ai_result: TranslationResult) -> dict[str, Any]: |
| 148 | + """Create a review task for human translator.""" |
| 149 | + # Placeholder for task creation |
| 150 | + # notify and create the revision? |
| 151 | + return {} |
| 152 | + |
| 153 | + |
| 154 | +class ManualTranslationStrategy(TranslationStrategy): |
| 155 | + """Manual human translation strategy.""" |
| 156 | + |
| 157 | + def translate(self, request: TranslationRequest) -> TranslationResult: |
| 158 | + """Create manual translation task.""" |
| 159 | + try: |
| 160 | + manual_task = self._create_manual_task(request) |
| 161 | + |
| 162 | + return TranslationResult( |
| 163 | + success=True, |
| 164 | + translated_content="", |
| 165 | + method=TranslationMethod.MANUAL, |
| 166 | + cost=50.0, |
| 167 | + quality_score=0.98, |
| 168 | + metadata={ |
| 169 | + "task_id": manual_task.get("id"), |
| 170 | + "status": "assigned_to_translator", |
| 171 | + "estimated_completion": manual_task.get("estimated_completion") |
| 172 | + } |
| 173 | + ) |
| 174 | + except Exception as e: |
| 175 | + return TranslationResult( |
| 176 | + success=False, |
| 177 | + method=TranslationMethod.MANUAL, |
| 178 | + error_message=str(e) |
| 179 | + ) |
| 180 | + |
| 181 | + def can_handle(self, request: TranslationRequest) -> bool: |
| 182 | + """Manual translation can handle any content.""" |
| 183 | + return True |
| 184 | + |
| 185 | + def get_cost_estimate(self, request: TranslationRequest) -> float: |
| 186 | + """Estimate cost for manual translation.""" |
| 187 | + return 0.0 |
| 188 | + |
| 189 | + def _create_manual_task(self, request: TranslationRequest) -> dict[str, Any]: |
| 190 | + """Create a manual translation task.""" |
| 191 | + # Placeholder for task creation |
| 192 | + return {} |
| 193 | + |
| 194 | + |
| 195 | +class TranslationStrategyFactory: |
| 196 | + """Factory for creating translation strategies.""" |
| 197 | + |
| 198 | + def __init__(self): |
| 199 | + self._strategies = { |
| 200 | + TranslationMethod.AI: AITranslationStrategy(), |
| 201 | + TranslationMethod.HYBRID: HybridTranslationStrategy(), |
| 202 | + TranslationMethod.MANUAL: ManualTranslationStrategy(), |
| 203 | + } |
| 204 | + |
| 205 | + def get_strategy(self, method: TranslationMethod) -> TranslationStrategy: |
| 206 | + """Get strategy for the specified method.""" |
| 207 | + if method not in self._strategies: |
| 208 | + raise ValueError(f"Unknown translation method: {method}") |
| 209 | + return self._strategies[method] |
| 210 | + |
| 211 | + def select_best_strategy(self, request: TranslationRequest) -> TranslationStrategy: |
| 212 | + """Select the best strategy based on business rules.""" |
| 213 | + if request.target_locale in settings.AI_ENABLED_LOCALES: |
| 214 | + return self._strategies[TranslationMethod.AI] |
| 215 | + return self._strategies[TranslationMethod.MANUAL] |
0 commit comments