Blame view

translation/backends/llm.py 6.57 KB
5e4dc8e4   tangwang   翻译架构按“一个翻译服务 +
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
  """LLM-based translation backend."""
  
  from __future__ import annotations
  
  import logging
  import os
  import time
  from typing import List, Optional, Sequence, Union
  
  from openai import OpenAI
  
  from config.env_config import DASHSCOPE_API_KEY
  from config.services_config import get_translation_config
  from config.translate_prompts import TRANSLATION_PROMPTS
  from config.tenant_config_loader import SOURCE_LANG_CODE_MAP
  
  logger = logging.getLogger(__name__)
  
  DEFAULT_QWEN_BASE_URL = "https://dashscope-us.aliyuncs.com/compatible-mode/v1"
  DEFAULT_LLM_MODEL = "qwen-flash"
  
  
  def _build_prompt(
      text: str,
      *,
      source_lang: Optional[str],
      target_lang: str,
      scene: Optional[str],
  ) -> str:
      tgt = (target_lang or "").lower() or "en"
      src = (source_lang or "auto").lower()
      normalized_scene = (scene or "").strip() or "general"
      if normalized_scene in {"query", "ecommerce_search", "ecommerce_search_query"}:
          group_key = "ecommerce_search_query"
      elif normalized_scene in {"product_title", "sku_name"}:
          group_key = "sku_name"
      else:
          group_key = normalized_scene
      group = TRANSLATION_PROMPTS.get(group_key) or TRANSLATION_PROMPTS["general"]
      template = group.get(tgt) or group.get("en")
      if not template:
          template = (
              "You are a professional {source_lang} ({src_lang_code}) to "
              "{target_lang} ({tgt_lang_code}) translator, output only the translation: {text}"
          )
  
      source_lang_label = SOURCE_LANG_CODE_MAP.get(src, src)
      target_lang_label = SOURCE_LANG_CODE_MAP.get(tgt, tgt)
  
      return template.format(
          source_lang=source_lang_label,
          src_lang_code=src,
          target_lang=target_lang_label,
          tgt_lang_code=tgt,
          text=text,
      )
  
  
  class LLMTranslationBackend:
      def __init__(
          self,
          *,
          model: Optional[str] = None,
          timeout_sec: float = 30.0,
          base_url: Optional[str] = None,
      ) -> None:
          cfg = get_translation_config()
          llm_cfg = cfg.get_capability_cfg("llm")
          self.model = model or llm_cfg.get("model") or DEFAULT_LLM_MODEL
          self.timeout_sec = float(llm_cfg.get("timeout_sec") or timeout_sec or 30.0)
          self.base_url = (
              (base_url or "").strip()
              or (llm_cfg.get("base_url") or "").strip()
              or os.getenv("DASHSCOPE_BASE_URL")
              or DEFAULT_QWEN_BASE_URL
          )
          self.client = self._create_client()
  
      @property
      def supports_batch(self) -> bool:
          return True
  
      def _create_client(self) -> Optional[OpenAI]:
          api_key = DASHSCOPE_API_KEY or os.getenv("DASHSCOPE_API_KEY")
          if not api_key:
              logger.warning("DASHSCOPE_API_KEY not set; llm translation unavailable")
              return None
          try:
              return OpenAI(api_key=api_key, base_url=self.base_url)
          except Exception as exc:
              logger.error("Failed to initialize llm translation client: %s", exc, exc_info=True)
              return None
  
      def _translate_single(
          self,
          text: str,
          target_lang: str,
          source_lang: Optional[str] = None,
          context: Optional[str] = None,
          prompt: Optional[str] = None,
      ) -> Optional[str]:
          if not text or not str(text).strip():
              return text
          if not self.client:
              return None
  
          tgt = (target_lang or "").lower() or "en"
          src = (source_lang or "auto").lower()
          scene = context or "default"
          user_prompt = prompt or _build_prompt(
              text=text,
              source_lang=src,
              target_lang=tgt,
              scene=scene,
          )
          start = time.time()
          try:
              logger.info(
                  "[llm] Request | src=%s tgt=%s model=%s prompt=%s",
                  src,
                  tgt,
                  self.model,
                  user_prompt,
              )
              completion = self.client.chat.completions.create(
                  model=self.model,
                  messages=[{"role": "user", "content": user_prompt}],
                  timeout=self.timeout_sec,
              )
              content = (completion.choices[0].message.content or "").strip()
              latency_ms = (time.time() - start) * 1000
              if not content:
                  logger.warning("[llm] Empty result | src=%s tgt=%s latency=%.1fms", src, tgt, latency_ms)
                  return None
              logger.info(
                  "[llm] Success | src=%s tgt=%s src_text=%s response=%s latency=%.1fms",
                  src,
                  tgt,
                  text,
                  content,
                  latency_ms,
              )
              return content
          except Exception as exc:
              latency_ms = (time.time() - start) * 1000
              logger.warning(
                  "[llm] Failed | src=%s tgt=%s latency=%.1fms error=%s",
                  src,
                  tgt,
                  latency_ms,
                  exc,
                  exc_info=True,
              )
              return None
  
      def translate(
          self,
          text: Union[str, Sequence[str]],
          target_lang: str,
          source_lang: Optional[str] = None,
          context: Optional[str] = None,
          prompt: Optional[str] = None,
      ) -> Union[Optional[str], List[Optional[str]]]:
          if isinstance(text, (list, tuple)):
              results: List[Optional[str]] = []
              for item in text:
                  if item is None:
                      results.append(None)
                      continue
                  results.append(
                      self._translate_single(
                          text=str(item),
                          target_lang=target_lang,
                          source_lang=source_lang,
                          context=context,
                          prompt=prompt,
                      )
                  )
              return results
  
          return self._translate_single(
              text=str(text),
              target_lang=target_lang,
              source_lang=source_lang,
              context=context,
              prompt=prompt,
          )
  
  
  LLMTranslatorProvider = LLMTranslationBackend
  
  
  def llm_translate(
      text: Union[str, Sequence[str]],
      target_lang: str,
      *,
      source_lang: Optional[str] = None,
      source_lang_label: Optional[str] = None,
      target_lang_label: Optional[str] = None,
      timeout_sec: Optional[float] = None,
  ) -> Union[Optional[str], List[Optional[str]]]:
      del source_lang_label, target_lang_label
      provider = LLMTranslationBackend(timeout_sec=timeout_sec or 30.0)
      return provider.translate(
          text=text,
          target_lang=target_lang,
          source_lang=source_lang,
          context=None,
      )