""" Prompt Wrapper - 为 .prompt 文件提供 Prompt 实现 类似 Resonote 的 SimpleHPrompt,但增加了多模态支持 """ import base64 from pathlib import Path from typing import List, Dict, Any, Union, Optional from agent.llm.prompts.loader import load_prompt, get_message class SimplePrompt: """ 通用的 Prompt 包装器 特性: - 加载 .prompt 文件(YAML frontmatter + sections) - 支持参数替换(%variable%) - 支持多模态消息(图片) 使用示例: # 纯文本 prompt = SimplePrompt(Path("task.prompt")) messages = prompt.build_messages(text="内容") # 多模态(文本 + 图片) messages = prompt.build_messages( text="分析这张图片", images="path/to/image.png" # 或 images=["img1.png", "img2.png"] ) """ def __init__(self, prompt_path: Union[Path, str]): """ Args: prompt_path: .prompt 文件路径 """ self.prompt_path = Path(prompt_path) if isinstance(prompt_path, str) else prompt_path # 加载 .prompt 文件 self.config, self._messages = load_prompt(self.prompt_path) def build_messages(self, **context) -> List[Dict[str, Any]]: """ 构造消息列表(支持多模态) Args: **context: 参数 - 普通参数:用于替换 %variable% - images: 图片资源(可选) - 单个图片:str 或 Path - 多个图片:List[str | Path] - 格式:文件路径或 base64 字符串 Returns: 消息列表,格式遵循 OpenAI API 规范 Example: >>> messages = prompt.build_messages( ... text="特征描述", ... images="input/image.png" ... ) [ {"role": "system", "content": "..."}, { "role": "user", "content": [ {"type": "text", "text": "..."}, {"type": "image_url", "image_url": {"url": "data:image/png;base64,..."}} ] } ] """ # 提取图片资源(从 context 中移除,避免传入 get_message) images = context.pop('images', None) # 构建文本内容(支持参数替换) system_content = get_message(self._messages, 'system', **context) user_content = get_message(self._messages, 'user', **context) messages = [] # 添加 system 消息 if system_content: messages.append({"role": "system", "content": system_content}) # 添加 user 消息(可能是多模态) if images: # 多模态消息 user_message = {"role": "user", "content": []} # 添加文本部分 if user_content: user_message["content"].append({ "type": "text", "text": user_content }) # 添加图片部分 if isinstance(images, (list, tuple)): for img in images: user_message["content"].append(self._build_image_content(img)) else: user_message["content"].append(self._build_image_content(images)) messages.append(user_message) else: # 纯文本消息 if user_content: messages.append({"role": "user", "content": user_content}) return messages def _build_image_content(self, image: Union[str, Path]) -> Dict[str, Any]: """ 构建图片内容部分(OpenAI 格式) Args: image: 图片路径或 base64 字符串 Returns: {"type": "image_url", "image_url": {"url": "data:..."}} """ # 如果已经是 base64 data URL,直接使用 if isinstance(image, str) and image.startswith("data:"): return { "type": "image_url", "image_url": {"url": image} } # 否则,读取文件并转为 base64 image_path = Path(image) if isinstance(image, str) else image # 推断 MIME type suffix = image_path.suffix.lower() mime_type_map = { '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.webp': 'image/webp' } mime_type = mime_type_map.get(suffix, 'image/png') # 读取并编码 with open(image_path, 'rb') as f: image_data = base64.b64encode(f.read()).decode('utf-8') data_url = f"data:{mime_type};base64,{image_data}" return { "type": "image_url", "image_url": {"url": data_url} } def create_prompt(prompt_path: Union[Path, str]) -> SimplePrompt: """ 工厂函数:创建 SimplePrompt 实例 Args: prompt_path: .prompt 文件路径 Returns: SimplePrompt 实例 """ return SimplePrompt(prompt_path)