| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168 |
- """
- Prompt Wrapper - 为 .prompt 文件提供 Prompt 实现
- 类似 Resonote 的 SimpleHPrompt,但增加了多模态支持
- """
- import base64
- from pathlib import Path
- from typing import List, Dict, Any, Union, Optional
- from agent.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)
|