Skip to content

Commit 5f3358c

Browse files
committed
chore(app): static typing
1 parent 44243a1 commit 5f3358c

36 files changed

Lines changed: 458 additions & 345 deletions

app/agent/base.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from abc import ABC, abstractmethod
22
from contextlib import asynccontextmanager
3-
from typing import List, Optional
43

54
from pydantic import BaseModel, Field, model_validator
65

@@ -19,13 +18,13 @@ class BaseAgent(BaseModel, ABC):
1918

2019
# Core attributes
2120
name: str = Field(..., description="Unique name of the agent")
22-
description: Optional[str] = Field(None, description="Optional agent description")
21+
description: str | None = Field(None, description="Optional agent description")
2322

2423
# Prompts
25-
system_prompt: Optional[str] = Field(
24+
system_prompt: str | None = Field(
2625
None, description="System-level instruction prompt"
2726
)
28-
next_step_prompt: Optional[str] = Field(
27+
next_step_prompt: str | None = Field(
2928
None, description="Prompt for determining next action"
3029
)
3130

@@ -85,7 +84,7 @@ def update_memory(
8584
self,
8685
role: ROLE_TYPE, # type: ignore
8786
content: str,
88-
base64_image: Optional[str] = None,
87+
base64_image: str | None = None,
8988
**kwargs,
9089
) -> None:
9190
"""Add a message to the agent's memory.
@@ -113,7 +112,7 @@ def update_memory(
113112
kwargs = {"base64_image": base64_image, **(kwargs if role == "tool" else {})}
114113
self.memory.add_message(message_map[role](content, **kwargs))
115114

116-
async def run(self, request: Optional[str] = None) -> str:
115+
async def run(self, request: str | None = None) -> str:
117116
"""Execute the agent's main loop asynchronously.
118117
119118
Args:
@@ -131,7 +130,7 @@ async def run(self, request: Optional[str] = None) -> str:
131130
if request:
132131
self.update_memory("user", request)
133132

134-
results: List[str] = []
133+
results: list[str] = []
135134
async with self.state_context(AgentState.RUNNING):
136135
while (
137136
self.current_step < self.max_steps and self.state != AgentState.FINISHED
@@ -186,11 +185,11 @@ def is_stuck(self) -> bool:
186185
return duplicate_count >= self.duplicate_threshold
187186

188187
@property
189-
def messages(self) -> List[Message]:
188+
def messages(self) -> list[Message]:
190189
"""Retrieve a list of messages from the agent's memory."""
191190
return self.memory.messages
192191

193192
@messages.setter
194-
def messages(self, value: List[Message]):
193+
def messages(self, value: list[Message]):
195194
"""Set the list of messages in the agent's memory."""
196195
self.memory.messages = value

app/agent/browser.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import json
2-
from typing import Any, Optional
2+
from typing import Any
33

44
from pydantic import Field
55

@@ -18,14 +18,14 @@ class BrowserAgent(ToolCallAgent):
1818
extract content, and perform other browser-based actions to accomplish tasks.
1919
"""
2020

21-
name: str = "browser"
22-
description: str = "A browser agent that can control a browser to accomplish tasks"
21+
name = "browser"
22+
description = "A browser agent that can control a browser to accomplish tasks"
2323

24-
system_prompt: str = SYSTEM_PROMPT
25-
next_step_prompt: str = NEXT_STEP_PROMPT
24+
system_prompt = SYSTEM_PROMPT
25+
next_step_prompt = NEXT_STEP_PROMPT
2626

27-
max_observe: int = 10000
28-
max_steps: int = 20
27+
max_observe = 10000
28+
max_steps = 20
2929

3030
# Configure the available tools
3131
available_tools: ToolCollection = Field(
@@ -36,18 +36,18 @@ class BrowserAgent(ToolCallAgent):
3636
tool_choices: ToolChoice = ToolChoice.AUTO
3737
special_tool_names: list[str] = Field(default_factory=lambda: [Terminate().name])
3838

39-
_current_base64_image: Optional[str] = None
39+
_current_base64_image: str | None = None
4040

4141
async def _handle_special_tool(self, name: str, result: Any, **kwargs):
4242
if not self._is_special_tool(name):
4343
return
4444
else:
45-
await self.available_tools.get_tool(BrowserUseTool().name).cleanup()
45+
await self.available_tools.get_tool(BrowserUseTool).cleanup()
4646
await super()._handle_special_tool(name, result, **kwargs)
4747

48-
async def get_browser_state(self) -> Optional[dict]:
48+
async def get_browser_state(self) -> dict | None:
4949
"""Get the current browser state for context in next steps."""
50-
browser_tool = self.available_tools.get_tool(BrowserUseTool().name)
50+
browser_tool = self.available_tools.get_tool(BrowserUseTool)
5151
if not browser_tool:
5252
return None
5353

app/agent/planning.py

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import time
2-
from typing import Dict, List, Optional
2+
from typing import Sequence
33

44
from pydantic import Field, model_validator
55

6-
from app.agent.toolcall import ToolCallAgent
6+
from app.agent.toolcall import CompatibleToolCallObject, ToolCallAgent
77
from app.logger import logger
88
from app.prompt.planning import NEXT_STEP_PROMPT, PLANNING_SYSTEM_PROMPT
99
from app.schema import TOOL_CHOICE_TYPE, Message, ToolCall, ToolChoice
@@ -28,14 +28,16 @@ class PlanningAgent(ToolCallAgent):
2828
default_factory=lambda: ToolCollection(PlanningTool(), Terminate())
2929
)
3030
tool_choices: TOOL_CHOICE_TYPE = ToolChoice.AUTO # type: ignore
31-
special_tool_names: List[str] = Field(default_factory=lambda: [Terminate().name])
31+
special_tool_names: list[str] = Field(default_factory=lambda: [Terminate().name])
3232

33-
tool_calls: List[ToolCall] = Field(default_factory=list)
34-
active_plan_id: Optional[str] = Field(default=None)
33+
tool_calls: Sequence[ToolCall | CompatibleToolCallObject] = Field(
34+
default_factory=list
35+
)
36+
active_plan_id: str | None = Field(default=None)
3537

3638
# Add a dictionary to track the step status for each tool call
37-
step_execution_tracker: Dict[str, Dict] = Field(default_factory=dict)
38-
current_step_index: Optional[int] = None
39+
step_execution_tracker: dict[str, dict] = Field(default_factory=dict)
40+
current_step_index: int | None = None
3941

4042
max_steps: int = 20
4143

@@ -113,7 +115,7 @@ async def get_plan(self) -> str:
113115
)
114116
return result.output if hasattr(result, "output") else str(result)
115117

116-
async def run(self, request: Optional[str] = None) -> str:
118+
async def run(self, request: str | None = None) -> str:
117119
"""Run the agent with an optional initial request."""
118120
if request:
119121
await self.create_initial_plan(request)
@@ -155,7 +157,7 @@ async def update_plan_status(self, tool_call_id: str) -> None:
155157
except Exception as e:
156158
logger.warning(f"Failed to update plan status: {e}")
157159

158-
async def _get_current_step_index(self) -> Optional[int]:
160+
async def _get_current_step_index(self) -> int | None:
159161
"""
160162
Parse the current plan to identify the first non-completed step's index.
161163
Returns None if no active step is found.
@@ -209,19 +211,22 @@ async def create_initial_plan(self, request: str) -> None:
209211
]
210212
self.memory.add_messages(messages)
211213
response = await self.llm.ask_tool(
212-
messages=messages,
214+
messages=list(messages),
213215
system_msgs=[Message.system_message(self.system_prompt)],
214216
tools=self.available_tools.to_params(),
215217
tool_choice=ToolChoice.AUTO,
216218
)
219+
if not response:
220+
raise RuntimeError("Model refused to create plan")
221+
tool_calls = response.tool_calls or []
217222
assistant_msg = Message.from_tool_calls(
218-
content=response.content, tool_calls=response.tool_calls
223+
content=response.content or "", tool_calls=tool_calls
219224
)
220225

221226
self.memory.add_message(assistant_msg)
222227

223228
plan_created = False
224-
for tool_call in response.tool_calls:
229+
for tool_call in tool_calls:
225230
if tool_call.function.name == "planning":
226231
result = await self.execute_tool(tool_call)
227232
logger.info(

app/agent/react.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
from abc import ABC, abstractmethod
2-
from typing import Optional
32

43
from pydantic import Field
54

@@ -10,12 +9,12 @@
109

1110
class ReActAgent(BaseAgent, ABC):
1211
name: str
13-
description: Optional[str] = None
12+
description: str | None = None
1413

15-
system_prompt: Optional[str] = None
16-
next_step_prompt: Optional[str] = None
14+
system_prompt: str | None = None
15+
next_step_prompt: str | None = None
1716

18-
llm: Optional[LLM] = Field(default_factory=LLM)
17+
llm: LLM = Field(default_factory=LLM)
1918
memory: Memory = Field(default_factory=Memory)
2019
state: AgentState = AgentState.IDLE
2120

app/agent/swe.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
from typing import List
2-
31
from pydantic import Field
42

53
from app.agent.toolcall import ToolCallAgent
@@ -19,7 +17,7 @@ class SWEAgent(ToolCallAgent):
1917
available_tools: ToolCollection = ToolCollection(
2018
Bash(), StrReplaceEditor(), Terminate()
2119
)
22-
special_tool_names: List[str] = Field(default_factory=lambda: [Terminate().name])
20+
special_tool_names: list[str] = Field(default_factory=lambda: [Terminate().name])
2321

2422
max_steps: int = 30
2523

@@ -29,7 +27,7 @@ class SWEAgent(ToolCallAgent):
2927
async def think(self) -> bool:
3028
"""Process current state and decide next action"""
3129
# Update working directory
32-
self.working_dir = await self.bash.execute("pwd")
30+
self.working_dir = (await self.bash.execute("pwd")).output.strip()
3331
self.next_step_prompt = self.next_step_prompt.format(
3432
current_dir=self.working_dir
3533
)

app/agent/toolcall.py

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import json
2-
from typing import Any, List, Optional, Union
2+
from typing import Any, Sequence, TypeAlias
33

4+
from openai.types.chat.chat_completion_message_tool_call import (
5+
ChatCompletionMessageToolCall,
6+
)
47
from pydantic import Field
58

69
from app.agent.react import ReActAgent
@@ -11,29 +14,33 @@
1114
from app.tool import CreateChatCompletion, Terminate, ToolCollection
1215

1316

17+
CompatibleToolCallObject: TypeAlias = ChatCompletionMessageToolCall
18+
1419
TOOL_CALL_REQUIRED = "Tool calls required but none provided"
1520

1621

1722
class ToolCallAgent(ReActAgent):
1823
"""Base agent class for handling tool/function calls with enhanced abstraction"""
1924

20-
name: str = "toolcall"
21-
description: str = "an agent that can execute tool calls."
25+
name = "toolcall"
26+
description = "an agent that can execute tool calls."
2227

23-
system_prompt: str = SYSTEM_PROMPT
24-
next_step_prompt: str = NEXT_STEP_PROMPT
28+
system_prompt = SYSTEM_PROMPT
29+
next_step_prompt = NEXT_STEP_PROMPT
2530

2631
available_tools: ToolCollection = ToolCollection(
2732
CreateChatCompletion(), Terminate()
2833
)
2934
tool_choices: TOOL_CHOICE_TYPE = ToolChoice.AUTO # type: ignore
30-
special_tool_names: List[str] = Field(default_factory=lambda: [Terminate().name])
35+
special_tool_names: list[str] = Field(default_factory=lambda: [Terminate().name])
3136

32-
tool_calls: List[ToolCall] = Field(default_factory=list)
33-
_current_base64_image: Optional[str] = None
37+
tool_calls: Sequence[ToolCall | CompatibleToolCallObject] = Field(
38+
default_factory=list
39+
)
40+
_current_base64_image: str | None = None
3441

3542
max_steps: int = 30
36-
max_observe: Optional[Union[int, bool]] = None
43+
max_observe: int | None = None
3744

3845
async def think(self) -> bool:
3946
"""Process current state and decide next actions using tools"""
@@ -44,7 +51,7 @@ async def think(self) -> bool:
4451
try:
4552
# Get response with tool options
4653
response = await self.llm.ask_tool(
47-
messages=self.messages,
54+
messages=list(self.messages),
4855
system_msgs=(
4956
[Message.system_message(self.system_prompt)]
5057
if self.system_prompt
@@ -162,7 +169,7 @@ async def act(self) -> str:
162169

163170
return "\n\n".join(results)
164171

165-
async def execute_tool(self, command: ToolCall) -> str:
172+
async def execute_tool(self, command: ToolCall | CompatibleToolCallObject) -> str:
166173
"""Execute a single tool call with robust error handling"""
167174
if not command or not command.function or not command.function.name:
168175
return "Error: Invalid command format"

0 commit comments

Comments
 (0)