autool/DroidBot/guiagent_core/context_manager.py
2026-06-17 19:44:18 +08:00

218 lines
6.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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