""" 邮件翻译模块 - 调用 LLM 自动识别语言并翻译为指定目标语言 支持: - 自动语言检测(通过 LLM 判断) - 翻译为英文或中文 - 保留邮件格式(换行、段落) """ import json import logging import asyncio from typing import Optional from openai import AsyncOpenAI, OpenAI from src import config as cfg logger = logging.getLogger(__name__) # ─── 系统提示词 ─── TRANSLATE_SYSTEM_PROMPT = """You are a professional email translator. Your task is to translate recruitment-related emails accurately. Rules: 1. Detect the source language automatically from the email content. 2. Translate the email body to the target language naturally and accurately. 3. Preserve formatting: line breaks, paragraphs, bullet points. 4. Keep company names, proper nouns, and technical terms intact (don't translate names). 5. Maintain the original tone (formal/informal) appropriate for recruitment context. 6. If the email is already in the target language, return it unchanged. You MUST respond with valid JSON only: { "source_language": "", "source_language_code": "", "translated_text": "", "is_already_target": } """ # ─── 数据类 ─── class TranslationResult: def __init__( self, source_language: str = "", source_language_code: str = "", translated_text: str = "", is_already_target: bool = False, error: str = "", ): self.source_language = source_language self.source_language_code = source_language_code self.translated_text = translated_text self.is_already_target = is_already_target self.error = error @property def is_valid(self) -> bool: return not self.error and bool(self.translated_text) # ─── 核心函数 ─── def _build_translate_prompt(text: str, target_language: str) -> str: return f"""Translate the following email to {target_language}. Email Body: {text} Return the translation in the required JSON format.""" def _parse_translate_response(content: str) -> TranslationResult: """解析 LLM 翻译响应""" content = content.strip() # 去除 markdown 代码块 if content.startswith("```"): lines = content.split("\n") content = "\n".join( lines[1:-1] if lines[-1].strip() == "```" else lines[1:] ) content = content.strip() try: data = json.loads(content) except json.JSONDecodeError as e: logger.error(f"翻译响应 JSON 解析失败: {e}\n原始内容: {content[:200]}") # 降级:把整个内容当译文 return TranslationResult( source_language="Unknown", source_language_code="unknown", translated_text=content[:2000], error=f"JSON 解析失败,已截断展示: {e}", ) return TranslationResult( source_language=data.get("source_language", "Unknown"), source_language_code=data.get("source_language_code", "unknown"), translated_text=data.get("translated_text", content[:2000]), is_already_target=data.get("is_already_target", False), ) async def translate_email_async( text: str, target_language: str = "English", ) -> TranslationResult: """ 异步翻译邮件正文 :param text: 邮件正文 :param target_language: 目标语言(如 "English" 或 "Chinese") """ if not text or not text.strip(): return TranslationResult(error="邮件正文为空,无需翻译") api_key = cfg.get_llm_api_key() if not api_key: return TranslationResult(error="未配置 LLM API Key,无法翻译") try: model = cfg.get_llm_model() base_url = cfg.get_llm_base_url() client_kwargs = {"api_key": api_key} if base_url: client_kwargs["base_url"] = base_url client = AsyncOpenAI(**client_kwargs) user_prompt = _build_translate_prompt(text[:4000], target_language) logger.info(f"正在翻译邮件至 {target_language}...") response = await client.chat.completions.create( model=model, messages=[ {"role": "system", "content": TRANSLATE_SYSTEM_PROMPT}, {"role": "user", "content": user_prompt}, ], temperature=0.1, max_tokens=2048, response_format={"type": "json_object"}, ) content = response.choices[0].message.content or "" result = _parse_translate_response(content) logger.info( f"翻译完成: {result.source_language} -> {target_language}" f" (已是目标语言: {result.is_already_target})" ) return result except Exception as e: logger.error(f"翻译失败: {e}", exc_info=True) return TranslationResult(error=f"翻译失败: {str(e)[:120]}") def translate_email_sync( text: str, target_language: str = "English", ) -> TranslationResult: """同步版本""" try: loop = asyncio.get_event_loop() if loop.is_running(): import concurrent.futures with concurrent.futures.ThreadPoolExecutor() as pool: future = pool.submit( asyncio.run, translate_email_async(text, target_language), ) return future.result(timeout=60) else: return loop.run_until_complete( translate_email_async(text, target_language) ) except RuntimeError: return asyncio.run(translate_email_async(text, target_language))