| |
| """ |
| 消息格式转换器。 |
| 主要负责将 OpenAI API 格式的消息列表转换为 Gemini API 兼容的 'contents' 列表格式。 |
| 支持处理文本内容、图像内容 (通过 Base64 编码的 Data URI),以及可选的系统指令提取。 |
| """ |
| import re |
| import logging |
| from typing import List, Dict, Any, Tuple, Union, Set |
|
|
| |
| from app.api.models import Message |
|
|
| logger = logging.getLogger('my_logger') |
|
|
| |
| SUPPORTED_IMAGE_MIME_TYPES: Set[str] = { |
| "image/jpeg", |
| "image/png", |
| "image/webp", |
| "image/heic", |
| "image/heif", |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| DATA_URI_REGEX = re.compile(r"^data:(" + "|".join(re.escape(m) for m in SUPPORTED_IMAGE_MIME_TYPES) + r");base64,(.+)$") |
|
|
| def _process_text_content( |
| content: str, |
| role: str, |
| gemini_history: List[Dict[str, Any]], |
| is_system_phase: bool, |
| system_instruction_text: str, |
| errors: List[str] |
| ) -> Tuple[bool, str]: |
| """ |
| (内部辅助函数) 处理纯文本类型的消息内容。 |
| - 如果处于系统指令处理阶段且角色为 'system',则累加文本到系统指令。 |
| - 否则,将文本内容添加到 Gemini contents 列表中,处理角色映射和消息合并。 |
| |
| Args: |
| content (str): 消息的文本内容。 |
| role (str): 消息的角色。 |
| gemini_history (List[Dict[str, Any]]): 当前已转换的 Gemini contents 列表。 |
| is_system_phase (bool): 是否处于系统指令处理阶段。 |
| system_instruction_text (str): 当前累积的系统指令文本。 |
| errors (List[str]): 用于记录错误的列表。 |
| |
| Returns: |
| Tuple[bool, str]: |
| - 第一个元素 (bool): 更新后的 is_system_phase 状态。 |
| - 第二个元素 (str): 更新后的 system_instruction_text。 |
| """ |
| if is_system_phase and role == 'system': |
| |
| if system_instruction_text: |
| system_instruction_text += "\n" + content |
| else: |
| system_instruction_text = content |
| return True, system_instruction_text |
| else: |
| |
| is_system_phase = False |
|
|
| |
| |
| if role in ['user', 'system']: |
| role_to_use = 'user' |
| elif role == 'assistant': |
| role_to_use = 'model' |
| else: |
| |
| errors.append(f"无效的角色 '{role}'") |
| |
| return False, system_instruction_text |
|
|
| |
| |
| |
| if gemini_history and gemini_history[-1]['role'] == role_to_use: |
| |
| |
| if isinstance(gemini_history[-1].get('parts'), list): |
| gemini_history[-1]['parts'].append({"text": content}) |
| else: |
| |
| gemini_history[-1]['parts'] = [{"text": content}] |
| logger.warning(f"发现非列表类型的 parts,已重新初始化。") |
| else: |
| |
| gemini_history.append( |
| {"role": role_to_use, "parts": [{"text": content}]} |
| ) |
|
|
| |
| return False, system_instruction_text |
|
|
| def _process_multi_part_content( |
| content: List[Dict[str, Any]], |
| role: str, |
| gemini_history: List[Dict[str, Any]], |
| errors: List[str], |
| message_index: int |
| ) -> bool: |
| """ |
| (内部辅助函数) 处理包含多部分内容(例如文本和图像)的 OpenAI 消息。 |
| 将 OpenAI 的多部分 content 列表转换为 Gemini 的 parts 列表。 |
| 支持 text 和 image_url (Data URI 格式) 类型。 |
| 处理角色映射和消息合并。 |
| |
| Args: |
| content (List[Dict[str, Any]]): OpenAI 格式的多部分内容列表。 |
| role (str): 消息的角色。 |
| gemini_history (List[Dict[str, Any]]): 当前已转换的 Gemini contents 列表。 |
| errors (List[str]): 用于记录错误的列表。 |
| message_index (int): 当前消息的索引,用于生成更清晰的错误信息。 |
| |
| Returns: |
| bool: 如果处理过程中发生错误,返回 True;否则返回 False。 |
| """ |
| parts = [] |
| has_error_in_item = False |
|
|
| |
| for item_index, item in enumerate(content): |
| item_type = item.get('type') |
|
|
| |
| |
| if item_type is None and isinstance(item, dict) and 'text' in item and isinstance(item.get('text'), str): |
| logger.debug(f"消息 {message_index} 项目 {item_index}: 检测到无类型的文本部分,直接处理。") |
| parts.append({"text": item['text']}) |
| continue |
| |
|
|
| if item_type == 'text': |
| |
| parts.append({"text": item.get('text', '')}) |
| elif item_type == 'image_url': |
| |
| image_url_dict = item.get('image_url', {}) |
| |
| if not isinstance(image_url_dict, dict): |
| errors.append(f"消息 {message_index} 项目 {item_index}: 'image_url' 必须是字典,但得到 {type(image_url_dict)}") |
| has_error_in_item = True |
| continue |
|
|
| image_data = image_url_dict.get('url', '') |
| |
| match = DATA_URI_REGEX.match(image_data) |
| if match: |
| mime_type = match.group(1) |
| base64_data = match.group(2) |
| |
| parts.append({ |
| "inline_data": { |
| "mime_type": mime_type, |
| "data": base64_data |
| } |
| }) |
| else: |
| |
| error_msg = f"消息 {message_index} 项目 {item_index}: 无效或不支持的图像 Data URI。" |
| if image_data.startswith('data:image/'): |
| error_msg += f" MIME 类型必须是 {SUPPORTED_IMAGE_MIME_TYPES} 之一且格式正确。" |
| else: |
| error_msg += f" 仅接受 Base64 编码的 Data URI,支持的 MIME 类型为: {', '.join(SUPPORTED_IMAGE_MIME_TYPES)}。" |
| errors.append(error_msg) |
| has_error_in_item = True |
| else: |
| |
| errors.append(f"消息 {message_index} 项目 {item_index}: 不支持的内容类型 '{item_type}'") |
| has_error_in_item = True |
|
|
| |
| |
| if parts and not has_error_in_item: |
| |
| |
| text_parts = [p['text'] for p in parts if 'text' in p and len(p) == 1] |
| non_text_parts = [p for p in parts if 'text' not in p or len(p) > 1] |
|
|
| if len(text_parts) > 1: |
| merged_text = "\n".join(text_parts) |
| logger.debug(f"消息 {message_index}: 检测到 {len(text_parts)} 个文本 parts,合并为一个。") |
| |
| merged_parts = [{"text": merged_text}] + non_text_parts |
| parts = merged_parts |
| |
|
|
| |
| if role in ['user', 'system']: |
| role_to_use = 'user' |
| elif role == 'assistant': |
| role_to_use = 'model' |
| else: |
| errors.append(f"消息 {message_index}: 无效的角色 '{role}'") |
| return True |
|
|
| |
| if gemini_history and gemini_history[-1]['role'] == role_to_use: |
| |
| if isinstance(gemini_history[-1].get('parts'), list): |
| gemini_history[-1]['parts'].extend(parts) |
| else: |
| gemini_history[-1]['parts'] = parts |
| logger.warning(f"消息 {message_index}: 发现非列表类型的 parts,已重新初始化。") |
| else: |
| |
| gemini_history.append( |
| {"role": role_to_use, "parts": parts} |
| ) |
| return False |
| elif not parts and not has_error_in_item: |
| logger.warning(f"消息 {message_index}: 内容列表为空或所有项目均无效,已跳过。") |
| return False |
| else: |
| |
| return True |
|
|
| def convert_messages(messages: List[Message], use_system_prompt=False) -> Union[Tuple[List[Dict[str, Any]], Dict[str, Any]], List[str]]: |
| """ |
| 将 OpenAI 格式的消息列表转换为 Gemini API 格式的 'contents' 列表和 system_instruction。 |
| |
| 处理逻辑: |
| 1. 遍历 OpenAI 消息列表。 |
| 2. 如果 `use_system_prompt` 为 True,将第一个 'system' 角色的消息内容提取为系统指令。 |
| 3. 处理后续消息: |
| - 映射角色 ('user'/'system' -> 'user', 'assistant' -> 'model')。 |
| - 处理文本内容。 |
| - 处理多部分内容(文本和图像 Data URI)。 |
| - 合并连续相同角色的消息。 |
| 4. 如果在转换过程中遇到任何错误,返回包含错误信息的列表。 |
| 5. 如果转换成功,返回包含 Gemini 'contents' 列表和 'system_instruction' 字典的元组。 |
| |
| Args: |
| messages (List[Message]): OpenAI 格式的消息列表 (Pydantic 模型对象列表)。 |
| use_system_prompt (bool): 是否启用系统指令提取功能。默认为 False。 |
| |
| Returns: |
| Union[Tuple[List[Dict[str, Any]], Dict[str, Any]], List[str]]: |
| - 成功时: 返回一个元组 `(gemini_history, system_instruction_dict)`。 |
| `gemini_history` 是转换后的 Gemini contents 列表。 |
| `system_instruction_dict` 是包含系统指令的字典 (如果提取到),否则为空字典。 |
| - 失败时: 返回一个包含描述性错误信息的字符串列表。 |
| """ |
| gemini_history: List[Dict[str, Any]] = [] |
| errors: List[str] = [] |
| system_instruction_text = "" |
| is_system_phase = use_system_prompt |
|
|
| |
| for i, message in enumerate(messages): |
| role = message.role |
| content = message.content |
|
|
| |
| logger.debug(f"正在处理消息 {i}: role={role}, content_type={type(content)}") |
|
|
| |
| if isinstance(content, str): |
| |
| is_system_phase, system_instruction_text = _process_text_content( |
| content, role, gemini_history, is_system_phase, system_instruction_text, errors |
| ) |
| elif isinstance(content, list): |
| |
| is_system_phase = False |
| |
| has_error = _process_multi_part_content( |
| content, role, gemini_history, errors, i |
| ) |
| |
| if has_error: |
| continue |
| else: |
| |
| errors.append(f"消息 {i}: 不支持的内容类型 '{type(content)}'") |
|
|
|
|
| |
| if errors: |
| logger.error(f"消息转换失败: {'; '.join(errors)}") |
| return errors |
| else: |
| |
| system_instruction_dict = {} |
| if system_instruction_text: |
| |
| system_instruction_dict = {"parts": [{"text": system_instruction_text}]} |
|
|
| |
| return gemini_history, system_instruction_dict |
|
|