Skip to content

Commit b7f91a9

Browse files
committed
优化 OpenManus 代码,1. 在异常处理系统中添加了OpenManusError基类和多个具体异常类型;2. 优化了配置管理,包括添加环境变量支持和配置验证功能;3. 重构了BaseAgent类,将状态管理和内存管理功能迁移到独立组件;4. 实现了ToolRegistry和ToolExecutor,支持工具的动态注册和异步执行;5. 优化了工具执行的并发控制和性能监控;6. 完善了提示词模板系统,支持多语言和版本控制。这些改进提高了代码的可维护性、可扩展性和性能。
1 parent 6716a47 commit b7f91a9

25 files changed

+1134
-148
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ coverage.xml
4848
*.cover
4949
*.py,cover
5050
.hypothesis/
51+
52+
# Configuration files
53+
config/config.toml
5154
.pytest_cache/
5255
cover/
5356

app/agent/base.py

+23-52
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
from abc import ABC, abstractmethod
2-
from contextlib import asynccontextmanager
32
from typing import List, Literal, Optional
43

54
from pydantic import BaseModel, Field, model_validator
65

76
from app.llm import LLM
87
from app.logger import logger
9-
from app.schema import AgentState, Memory, Message
8+
from app.schema import Message
9+
from .components.state_manager import StateManager, AgentState
10+
from .components.memory_manager import MemoryManager
1011

1112

1213
class BaseAgent(BaseModel, ABC):
@@ -30,16 +31,14 @@ class BaseAgent(BaseModel, ABC):
3031

3132
# Dependencies
3233
llm: LLM = Field(default_factory=LLM, description="Language model instance")
33-
memory: Memory = Field(default_factory=Memory, description="Agent's memory store")
34-
state: AgentState = Field(
35-
default=AgentState.IDLE, description="Current agent state"
36-
)
34+
memory: MemoryManager = Field(default_factory=MemoryManager, description="Agent's memory store")
35+
state_manager: StateManager = Field(default_factory=StateManager, description="Agent's state manager")
3736

3837
# Execution control
3938
max_steps: int = Field(default=10, description="Maximum steps before termination")
4039
current_step: int = Field(default=0, description="Current step in execution")
4140

42-
duplicate_threshold: int = 2
41+
4342

4443
class Config:
4544
arbitrary_types_allowed = True
@@ -50,35 +49,21 @@ def initialize_agent(self) -> "BaseAgent":
5049
"""Initialize agent with default settings if not provided."""
5150
if self.llm is None or not isinstance(self.llm, LLM):
5251
self.llm = LLM(config_name=self.name.lower())
53-
if not isinstance(self.memory, Memory):
54-
self.memory = Memory()
52+
if not isinstance(self.memory, MemoryManager):
53+
self.memory = MemoryManager()
54+
if not isinstance(self.state_manager, StateManager):
55+
self.state_manager = StateManager()
5556
return self
5657

57-
@asynccontextmanager
58-
async def state_context(self, new_state: AgentState):
59-
"""Context manager for safe agent state transitions.
60-
61-
Args:
62-
new_state: The state to transition to during the context.
63-
64-
Yields:
65-
None: Allows execution within the new state.
58+
@property
59+
def state(self) -> AgentState:
60+
"""Get the current agent state."""
61+
return self.state_manager.state
6662

67-
Raises:
68-
ValueError: If the new_state is invalid.
69-
"""
70-
if not isinstance(new_state, AgentState):
71-
raise ValueError(f"Invalid state: {new_state}")
72-
73-
previous_state = self.state
74-
self.state = new_state
75-
try:
76-
yield
77-
except Exception as e:
78-
self.state = AgentState.ERROR # Transition to ERROR on failure
79-
raise e
80-
finally:
81-
self.state = previous_state # Revert to previous state
63+
@state.setter
64+
def state(self, new_state: AgentState):
65+
"""Set the agent state."""
66+
self.state_manager.state = new_state
8267

8368
def update_memory(
8469
self,
@@ -129,7 +114,7 @@ async def run(self, request: Optional[str] = None) -> str:
129114
self.update_memory("user", request)
130115

131116
results: List[str] = []
132-
async with self.state_context(AgentState.RUNNING):
117+
async with self.state_manager.state_context(AgentState.RUNNING):
133118
while (
134119
self.current_step < self.max_steps and self.state != AgentState.FINISHED
135120
):
@@ -162,24 +147,6 @@ def handle_stuck_state(self):
162147
self.next_step_prompt = f"{stuck_prompt}\n{self.next_step_prompt}"
163148
logger.warning(f"Agent detected stuck state. Added prompt: {stuck_prompt}")
164149

165-
def is_stuck(self) -> bool:
166-
"""Check if the agent is stuck in a loop by detecting duplicate content"""
167-
if len(self.memory.messages) < 2:
168-
return False
169-
170-
last_message = self.memory.messages[-1]
171-
if not last_message.content:
172-
return False
173-
174-
# Count identical content occurrences
175-
duplicate_count = sum(
176-
1
177-
for msg in reversed(self.memory.messages[:-1])
178-
if msg.role == "assistant" and msg.content == last_message.content
179-
)
180-
181-
return duplicate_count >= self.duplicate_threshold
182-
183150
@property
184151
def messages(self) -> List[Message]:
185152
"""Retrieve a list of messages from the agent's memory."""
@@ -189,3 +156,7 @@ def messages(self) -> List[Message]:
189156
def messages(self, value: List[Message]):
190157
"""Set the list of messages in the agent's memory."""
191158
self.memory.messages = value
159+
160+
def is_stuck(self) -> bool:
161+
"""Check if the agent is stuck in a loop by detecting duplicate content."""
162+
return self.memory.is_stuck()

app/agent/components/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Components module for agent functionality
+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
from typing import List, Optional
2+
from pydantic import BaseModel
3+
from app.schema import Message
4+
from loguru import logger
5+
6+
class MemoryManager(BaseModel):
7+
"""Manages agent memory operations and message history.
8+
9+
Provides functionality for storing, retrieving, and managing messages
10+
in the agent's memory with built-in duplicate detection.
11+
"""
12+
13+
messages: List[Message] = []
14+
duplicate_threshold: int = 2
15+
16+
def add_message(self, message: Message) -> None:
17+
"""Add a message to memory with logging.
18+
19+
Args:
20+
message: The message to add to memory.
21+
"""
22+
self.messages.append(message)
23+
logger.debug(f"Added {message.role} message to memory")
24+
25+
def get_last_message(self) -> Optional[Message]:
26+
"""Retrieve the most recent message from memory.
27+
28+
Returns:
29+
The last message if memory is not empty, None otherwise.
30+
"""
31+
return self.messages[-1] if self.messages else None
32+
33+
def clear(self) -> None:
34+
"""Clear all messages from memory."""
35+
self.messages.clear()
36+
logger.debug("Cleared memory")
37+
38+
def is_stuck(self) -> bool:
39+
"""Check if the agent is stuck in a loop by detecting duplicate content.
40+
41+
Returns:
42+
bool: True if duplicate content threshold is reached, False otherwise.
43+
"""
44+
if len(self.messages) < 2:
45+
return False
46+
47+
last_message = self.get_last_message()
48+
if not last_message or not last_message.content:
49+
return False
50+
51+
# Count identical content occurrences
52+
duplicate_count = sum(
53+
1
54+
for msg in reversed(self.messages[:-1])
55+
if msg.role == "assistant" and msg.content == last_message.content
56+
)
57+
58+
return duplicate_count >= self.duplicate_threshold
59+
60+
def get_context_window(self, window_size: int = 5) -> List[Message]:
61+
"""Get the most recent messages within a context window.
62+
63+
Args:
64+
window_size: Number of recent messages to include.
65+
66+
Returns:
67+
List of most recent messages up to window_size.
68+
"""
69+
return self.messages[-window_size:] if len(self.messages) > window_size else self.messages.copy()
70+
71+
def search_messages(self, query: str) -> List[Message]:
72+
"""Search messages containing the query string.
73+
74+
Args:
75+
query: String to search for in message content.
76+
77+
Returns:
78+
List of messages containing the query string.
79+
"""
80+
return [msg for msg in self.messages if query.lower() in msg.content.lower()]
81+
82+
class Config:
83+
arbitrary_types_allowed = True

app/agent/components/state_manager.py

+90
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
from enum import Enum
2+
from contextlib import asynccontextmanager
3+
from typing import Optional
4+
from loguru import logger
5+
6+
class AgentState(Enum):
7+
"""Enumeration of possible agent states."""
8+
IDLE = "idle"
9+
RUNNING = "running"
10+
FINISHED = "finished"
11+
ERROR = "error"
12+
PAUSED = "paused"
13+
14+
class StateManager:
15+
"""Manages agent state transitions and state-related operations.
16+
17+
Provides a clean interface for state management with safety checks
18+
and logging of state transitions.
19+
"""
20+
21+
def __init__(self):
22+
self._state = AgentState.IDLE
23+
self._previous_state: Optional[AgentState] = None
24+
25+
@property
26+
def state(self) -> AgentState:
27+
"""Get the current agent state."""
28+
return self._state
29+
30+
@state.setter
31+
def state(self, new_state: AgentState):
32+
"""Set the agent state with validation and logging."""
33+
if not isinstance(new_state, AgentState):
34+
raise ValueError(f"Invalid state: {new_state}")
35+
36+
if new_state != self._state:
37+
logger.debug(f"State transition: {self._state.value} -> {new_state.value}")
38+
self._previous_state = self._state
39+
self._state = new_state
40+
41+
@asynccontextmanager
42+
async def state_context(self, new_state: AgentState):
43+
"""Context manager for temporary state transitions.
44+
45+
Args:
46+
new_state: The state to transition to during the context.
47+
48+
Yields:
49+
None: Allows execution within the new state.
50+
51+
Raises:
52+
ValueError: If the new_state is invalid.
53+
"""
54+
if not isinstance(new_state, AgentState):
55+
raise ValueError(f"Invalid state: {new_state}")
56+
57+
previous_state = self._state
58+
self.state = new_state
59+
try:
60+
yield
61+
except Exception as e:
62+
self.state = AgentState.ERROR
63+
raise e
64+
finally:
65+
self.state = previous_state
66+
67+
def can_transition_to(self, target_state: AgentState) -> bool:
68+
"""Check if transitioning to the target state is valid.
69+
70+
Args:
71+
target_state: The state to check transition possibility.
72+
73+
Returns:
74+
bool: True if the transition is valid, False otherwise.
75+
"""
76+
# Define valid state transitions
77+
valid_transitions = {
78+
AgentState.IDLE: [AgentState.RUNNING],
79+
AgentState.RUNNING: [AgentState.FINISHED, AgentState.PAUSED, AgentState.ERROR],
80+
AgentState.PAUSED: [AgentState.RUNNING, AgentState.FINISHED],
81+
AgentState.ERROR: [AgentState.IDLE],
82+
AgentState.FINISHED: [AgentState.IDLE]
83+
}
84+
85+
return target_state in valid_transitions.get(self._state, [])
86+
87+
def reset(self):
88+
"""Reset the state manager to initial state."""
89+
self._state = AgentState.IDLE
90+
self._previous_state = None

app/agent/manus.py

+10
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,13 @@ class Manus(ToolCallAgent):
3232
PythonExecute(), GoogleSearch(), BrowserUseTool(), FileSaver(), Terminate()
3333
)
3434
)
35+
36+
def __init__(self, **data):
37+
super().__init__(**data)
38+
self._initialize_state_context()
39+
40+
def _initialize_state_context(self):
41+
"""Initialize state context and related attributes."""
42+
if not hasattr(self, 'state_manager'):
43+
self.state_manager = StateManager()
44+
self.state_context = self.state_manager.state_context

app/agent/react.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55

66
from app.agent.base import BaseAgent
77
from app.llm import LLM
8-
from app.schema import AgentState, Memory
8+
from app.schema import Memory
9+
from app.agent.components.state_manager import AgentState
910

1011

1112
class ReActAgent(BaseAgent, ABC):
@@ -17,7 +18,7 @@ class ReActAgent(BaseAgent, ABC):
1718

1819
llm: Optional[LLM] = Field(default_factory=LLM)
1920
memory: Memory = Field(default_factory=Memory)
20-
state: AgentState = AgentState.IDLE
21+
# 移除直接定义的state属性,使用BaseAgent中的state_manager
2122

2223
max_steps: int = 10
2324
current_step: int = 0

app/agent/toolcall.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
from app.agent.react import ReActAgent
77
from app.logger import logger
88
from app.prompt.toolcall import NEXT_STEP_PROMPT, SYSTEM_PROMPT
9-
from app.schema import AgentState, Message, ToolCall
9+
from app.schema import Message, ToolCall
10+
from app.agent.components.state_manager import AgentState
1011
from app.tool import CreateChatCompletion, Terminate, ToolCollection
1112

1213

0 commit comments

Comments
 (0)