218 lines
6.7 KiB
Python
218 lines
6.7 KiB
Python
"""
|
||
Context Manager - 管理对话历史和构建模型请求
|
||
|
||
Extracted from GuiAgent/core/context.py for modular use in DroidBot.
|
||
"""
|
||
import re
|
||
import logging
|
||
from typing import Optional, Dict, Any, List
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
# Constants
|
||
IMAGE_PLACEHOLDER = "<image>"
|
||
MAX_HISTORY_LENGTH = 20
|
||
|
||
|
||
class ContextManager:
|
||
"""
|
||
上下文管理器,负责:
|
||
1. 管理对话历史
|
||
2. 构建发送给模型的请求消息
|
||
3. 过滤和限制对话历史长度
|
||
"""
|
||
|
||
def __init__(self, system_prompt: str, max_history: int = MAX_HISTORY_LENGTH):
|
||
"""
|
||
初始化上下文管理器
|
||
|
||
Args:
|
||
system_prompt: 系统提示词
|
||
max_history: 最大对话历史长度
|
||
"""
|
||
self._system_prompt = system_prompt
|
||
self.max_history = max_history
|
||
|
||
# 对话历史:[{from: 'human'/'gpt', value: str, screenshot?: str}, ...]
|
||
self.conversations: List[Dict[str, Any]] = []
|
||
|
||
# 初始指令是否已添加
|
||
self._has_instruction: bool = False
|
||
|
||
@property
|
||
def system_prompt(self) -> str:
|
||
return self._system_prompt
|
||
|
||
@system_prompt.setter
|
||
def system_prompt(self, value: str) -> None:
|
||
self._system_prompt = value
|
||
|
||
def add_instruction(self, instruction: str) -> None:
|
||
"""添加用户初始指令(仅调用一次)"""
|
||
if self._has_instruction:
|
||
return
|
||
|
||
self.conversations.append({
|
||
'from': 'human',
|
||
'value': instruction
|
||
})
|
||
self._has_instruction = True
|
||
|
||
def add_user_message(self, message: str) -> None:
|
||
"""
|
||
添加用户消息到对话历史
|
||
|
||
Args:
|
||
message: 用户消息内容
|
||
"""
|
||
self.conversations.append({
|
||
'from': 'human',
|
||
'value': message
|
||
})
|
||
self._prune()
|
||
|
||
def add_screenshot(self, screenshot_base64: str, width: int, height: int) -> None:
|
||
"""
|
||
添加截图到对话历史
|
||
|
||
Args:
|
||
screenshot_base64: 截图的 Base64 编码(不含 data:image 前缀)
|
||
width: 图像宽度
|
||
height: 图像高度
|
||
"""
|
||
self.conversations.append({
|
||
'from': 'human',
|
||
'value': IMAGE_PLACEHOLDER,
|
||
'screenshot': screenshot_base64,
|
||
'size': (width, height)
|
||
})
|
||
self._prune()
|
||
|
||
def add_response(self, response: str) -> None:
|
||
"""
|
||
添加模型响应到对话历史
|
||
|
||
Args:
|
||
response: 模型响应文本
|
||
"""
|
||
# 提取摘要(移除 Reflection 部分)
|
||
summary = re.sub(r'Reflection:[\s\S]*?(?=Action:|$)', '', response).strip()
|
||
|
||
self.conversations.append({
|
||
'from': 'gpt',
|
||
'value': summary
|
||
})
|
||
self._prune()
|
||
|
||
def add_execution_feedback(self, success: bool, detail: str = "") -> None:
|
||
"""
|
||
添加动作执行结果反馈
|
||
|
||
Args:
|
||
success: 执行是否成功
|
||
detail: 详细信息(失败原因或额外说明)
|
||
"""
|
||
if success:
|
||
feedback = "✓ 上次操作已成功执行"
|
||
if detail:
|
||
feedback += f": {detail}"
|
||
else:
|
||
feedback = f"✗ 上次操作失败: {detail}。请分析失败原因并调整策略(如调整坐标、使用替代方案等)"
|
||
|
||
self.conversations.append({
|
||
'from': 'system',
|
||
'value': feedback
|
||
})
|
||
self._prune()
|
||
|
||
def _prune(self) -> None:
|
||
"""限制对话历史长度,保留初始指令 + 最近对话"""
|
||
if len(self.conversations) <= self.max_history:
|
||
return
|
||
|
||
# 保留第一条(初始指令)+ 最近的对话
|
||
first = self.conversations[0] if self.conversations else None
|
||
recent = self.conversations[-(self.max_history - 1):]
|
||
self.conversations = [first] + recent if first else recent
|
||
|
||
def _get_filtered(self) -> List[Dict[str, Any]]:
|
||
"""
|
||
获取过滤后的对话历史:
|
||
- 保留所有 AI 回复
|
||
- 保留所有系统反馈(执行结果)
|
||
- 保留初始指令 + 最近3轮的完整对话(包括截图)
|
||
"""
|
||
# 找出所有截图索引
|
||
screenshot_indices = [i for i, c in enumerate(self.conversations) if c.get('screenshot')]
|
||
|
||
if not screenshot_indices:
|
||
return self.conversations
|
||
|
||
# 保留最近3轮的截图及其后续对话
|
||
KEEP_RECENT_ROUNDS = 3
|
||
keep_from_idx = screenshot_indices[-KEEP_RECENT_ROUNDS] if len(screenshot_indices) >= KEEP_RECENT_ROUNDS else 0
|
||
|
||
filtered = []
|
||
for i, conv in enumerate(self.conversations):
|
||
# 始终保留初始指令
|
||
if i == 0:
|
||
filtered.append(conv)
|
||
# 保留最近N轮的所有对话
|
||
elif i >= keep_from_idx:
|
||
filtered.append(conv)
|
||
# 保留所有系统反馈(即使在旧轮次中)
|
||
elif conv['from'] == 'system':
|
||
filtered.append(conv)
|
||
|
||
return filtered
|
||
|
||
def build_messages(self) -> List[Dict[str, Any]]:
|
||
"""
|
||
构建发送给模型的消息列表(OpenAI 格式)
|
||
|
||
Returns:
|
||
消息列表,每条消息包含 role 和 content
|
||
"""
|
||
filtered = self._get_filtered()
|
||
messages = []
|
||
images = []
|
||
|
||
# 收集图像
|
||
for conv in filtered:
|
||
if conv.get('screenshot'):
|
||
images.append(conv['screenshot'])
|
||
|
||
image_idx = 0
|
||
for i, conv in enumerate(filtered):
|
||
if conv.get('screenshot'):
|
||
# 图像消息
|
||
messages.append({
|
||
'role': 'user',
|
||
'content': [{
|
||
'type': 'image_url',
|
||
'image_url': {'url': f"data:image/png;base64,{images[image_idx]}"}
|
||
}]
|
||
})
|
||
image_idx += 1
|
||
elif i == 0 and conv['from'] == 'human':
|
||
# 第一条消息:嵌入系统提示词
|
||
messages.append({
|
||
'role': 'system',
|
||
'content': f"{self._system_prompt}\n{conv['value']}"
|
||
})
|
||
else:
|
||
# 普通消息和系统反馈
|
||
role_map = {'human': 'user', 'gpt': 'assistant', 'system': 'user'}
|
||
role = role_map.get(conv['from'], 'user')
|
||
messages.append({
|
||
'role': role,
|
||
'content': conv['value']
|
||
})
|
||
|
||
return messages
|
||
|
||
def clear(self) -> None:
|
||
"""清空对话历史"""
|
||
self.conversations = []
|
||
self._has_instruction = False
|