Skip to content

Commit ee73178

Browse files
Merge pull request #234 from askui/feat/add-time-tools
feat(tools): add time and wait tools to universal tool store
2 parents 42d5657 + 8bb72f9 commit ee73178

6 files changed

Lines changed: 431 additions & 0 deletions

File tree

src/askui/tools/store/universal/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,24 @@
44
AndroidAgent, or any other agent type.
55
"""
66

7+
from .get_current_time import GetCurrentTimeTool
78
from .list_files_tool import ListFilesTool
89
from .load_image_tool import LoadImageTool
910
from .print_to_console import PrintToConsoleTool
1011
from .read_from_file_tool import ReadFromFileTool
12+
from .wait_tool import WaitTool
13+
from .wait_until_condition_tool import WaitUntilConditionTool
14+
from .wait_with_progress_tool import WaitWithProgressTool
1115
from .write_to_file_tool import WriteToFileTool
1216

1317
__all__ = [
18+
"GetCurrentTimeTool",
1419
"ListFilesTool",
1520
"PrintToConsoleTool",
1621
"ReadFromFileTool",
22+
"WaitTool",
23+
"WaitUntilConditionTool",
24+
"WaitWithProgressTool",
1725
"WriteToFileTool",
1826
"LoadImageTool",
1927
]
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
"""Tool that returns the current date and time in the local timezone."""
2+
3+
from datetime import datetime
4+
5+
from askui.models.shared.tools import Tool
6+
7+
8+
class GetCurrentTimeTool(Tool):
9+
"""
10+
Tool for returning the current date and time in the local timezone.
11+
12+
This tool allows the agent to know the current time when scheduling,
13+
logging, or time-dependent decisions. The time is formatted in a
14+
human-readable way including the timezone.
15+
16+
Example:
17+
```python
18+
from askui import VisionAgent
19+
from askui.tools.store.universal import GetCurrentTimeTool
20+
21+
with VisionAgent() as agent:
22+
agent.act(
23+
"What time is it? Plan the next step based on current time.",
24+
tools=[GetCurrentTimeTool()]
25+
)
26+
```
27+
"""
28+
29+
def __init__(self) -> None:
30+
super().__init__(
31+
name="get_current_time_tool",
32+
description=(
33+
"Returns the current date and time in the local timezone. "
34+
"Use this tool when you need to know the current time for scheduling, "
35+
"logging, or time-dependent decisions."
36+
),
37+
input_schema={
38+
"type": "object",
39+
"properties": {},
40+
"required": [],
41+
},
42+
)
43+
44+
def __call__(self) -> str:
45+
"""
46+
Return the current date and time in the local timezone.
47+
48+
Returns:
49+
str: Human-readable string with today's date, current time, and
50+
timezone (e.g. "Today is February 18, 2025 and currently
51+
it is 14:30:00 CET").
52+
"""
53+
now = datetime.now().astimezone()
54+
date_str = now.strftime("%B %d, %Y")
55+
time_str = now.strftime("%H:%M:%S")
56+
tz_str = now.strftime("%Z") or now.strftime("%z")
57+
return f"Today is {date_str} and currently it is {time_str} {tz_str}"
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
"""Tool that waits for a specified duration without console output."""
2+
3+
import time
4+
5+
from askui.models.shared.tools import Tool
6+
7+
8+
class WaitTool(Tool):
9+
"""
10+
Tool for waiting a specified number of seconds without any console output.
11+
12+
Use when a short, silent pause is needed between actions (e.g. after
13+
clicking before taking a screenshot). For longer waits or when the user
14+
should see progress, prefer `WaitWithProgressTool`.
15+
16+
Args:
17+
max_wait_time (int, optional): Maximum allowed wait duration in seconds.
18+
Defaults to 3600 (1 hour).
19+
20+
Example:
21+
```python
22+
from askui import VisionAgent
23+
from askui.tools.store.universal import WaitTool
24+
25+
with VisionAgent() as agent:
26+
agent.act(
27+
"Click the button then wait 2 seconds and take a screenshot",
28+
tools=[WaitTool(max_wait_time=60)]
29+
)
30+
```
31+
"""
32+
33+
def __init__(self, max_wait_time: int = 10 * 60) -> None:
34+
if max_wait_time < 1:
35+
msg = "Max wait time must be at least 1 second"
36+
raise ValueError(msg)
37+
super().__init__(
38+
name="wait_tool",
39+
description=(
40+
"Waits for a specified number of seconds without any console output. "
41+
"Use for short, silent waits (e.g. brief pause between actions). "
42+
"For longer waits or visible progress, use wait_with_progress_tool."
43+
),
44+
input_schema={
45+
"type": "object",
46+
"properties": {
47+
"wait_duration": {
48+
"type": "integer",
49+
"description": (
50+
"Duration of the wait in seconds "
51+
"(must be an integer, e.g. 5 for 5 seconds)."
52+
),
53+
"minimum": 1,
54+
"maximum": max_wait_time,
55+
},
56+
},
57+
"required": ["wait_duration"],
58+
},
59+
)
60+
self._max_wait_time = max_wait_time
61+
62+
def __call__(self, wait_duration: int) -> str:
63+
"""
64+
Wait for the specified number of seconds.
65+
66+
Args:
67+
wait_duration (int): Duration to wait in seconds (at least 1, at
68+
most the `max_wait_time` set at construction).
69+
70+
Returns:
71+
str: Confirmation message after the wait completes.
72+
73+
Raises:
74+
ValueError: If `wait_duration` is less than 1 or exceeds
75+
`max_wait_time`.
76+
"""
77+
if wait_duration < 1:
78+
msg = "Wait duration must be at least 1 second"
79+
raise ValueError(msg)
80+
if wait_duration > self._max_wait_time:
81+
msg = f"Wait duration must not exceed {self._max_wait_time} seconds"
82+
raise ValueError(msg)
83+
time.sleep(wait_duration)
84+
return f"Finished waiting for {wait_duration} seconds."
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
"""Tool that waits until a condition is met or a maximum wait time is reached."""
2+
3+
import time
4+
from typing import Callable
5+
6+
from askui.models.shared.tools import Tool
7+
from askui.tools.utils import wait_with_progress
8+
9+
10+
class WaitUntilConditionTool(Tool):
11+
"""
12+
Tool for waiting until a condition is met or a timeout is reached.
13+
14+
Polls a callable at a fixed interval. Returns as soon as the condition
15+
returns `True`, or when the maximum wait time is reached. During each
16+
interval between checks, a progress bar is shown in the console.
17+
18+
Args:
19+
condition_check (Callable[[], bool]): Callable with no arguments
20+
invoked at each poll; return `True` when the condition is met,
21+
`False` otherwise.
22+
description (str): Short description of what the condition checks for,
23+
used in the tool description for the agent.
24+
max_wait_time (float, optional): Maximum time to wait in seconds.
25+
Defaults to 3600 (1 hour).
26+
27+
Example:
28+
```python
29+
from pathlib import Path
30+
from askui import VisionAgent
31+
from askui.tools.store.universal import WaitUntilConditionTool
32+
33+
def file_ready() -> bool:
34+
return Path("output/result.json").exists()
35+
36+
with VisionAgent() as agent:
37+
agent.act(
38+
"Wait until the result file appears",
39+
tools=[WaitUntilConditionTool(
40+
condition_check=file_ready,
41+
description="result file exists",
42+
max_wait_time=300
43+
)]
44+
)
45+
```
46+
"""
47+
48+
def __init__(
49+
self,
50+
condition_check: Callable[[], bool],
51+
description: str,
52+
max_wait_time: int = 60 * 60,
53+
) -> None:
54+
if max_wait_time < 1:
55+
msg = "Max wait time must be at least 1 second"
56+
raise ValueError(msg)
57+
super().__init__(
58+
name="wait_until_condition_tool",
59+
description=(
60+
f"Waits for: {description}. "
61+
"Polls a condition at a given interval up to a maximum time; "
62+
"returns early if the condition is met, otherwise after timeout."
63+
),
64+
input_schema={
65+
"type": "object",
66+
"properties": {
67+
"max_wait_time": {
68+
"type": "integer",
69+
"description": (
70+
"Maximum time to wait in seconds before giving up."
71+
),
72+
"minimum": 1,
73+
"maximum": int(max_wait_time),
74+
},
75+
"check_interval": {
76+
"type": "integer",
77+
"description": (
78+
"Interval in seconds between condition checks "
79+
"(e.g. 5 for every 5 seconds). Must be at least 1."
80+
),
81+
"minimum": 1,
82+
"maximum": int(max_wait_time),
83+
},
84+
},
85+
"required": ["max_wait_time"],
86+
},
87+
)
88+
self._condition_check = condition_check
89+
self._max_wait_time = max_wait_time
90+
91+
def __call__(self, max_wait_time: int, check_interval: int = 1) -> str:
92+
"""
93+
Wait until the condition is met or the given timeout is reached.
94+
95+
Args:
96+
max_wait_time (int): Maximum time to wait in seconds (must not
97+
exceed the limit set at construction).
98+
check_interval (int, optional): Seconds between condition checks.
99+
Defaults to 1. Must be at least 1 and not greater than
100+
`max_wait_time`.
101+
102+
Returns:
103+
str: Message indicating either that the condition was met (with
104+
elapsed time) or that the timeout was reached.
105+
106+
Raises:
107+
ValueError: If `max_wait_time` or `check_interval` are out of
108+
valid range.
109+
"""
110+
if max_wait_time > self._max_wait_time:
111+
msg = f"max_wait_time must not exceed {self._max_wait_time} seconds"
112+
raise ValueError(msg)
113+
if check_interval < 1:
114+
msg = "check_interval must be at least 1 second"
115+
raise ValueError(msg)
116+
if check_interval > max_wait_time:
117+
msg = "check_interval must not exceed max_wait_time"
118+
raise ValueError(msg)
119+
120+
start = time.monotonic()
121+
num_checks = 0
122+
while True:
123+
num_checks += 1
124+
if self._condition_check():
125+
elapsed = time.monotonic() - start
126+
return (
127+
f"Condition met after {elapsed:.1f} seconds ({num_checks} checks)."
128+
)
129+
elapsed = time.monotonic() - start
130+
if elapsed >= max_wait_time:
131+
return (
132+
f"Timeout after {max_wait_time} seconds "
133+
f"(condition not met after {num_checks} checks)."
134+
)
135+
sleep_for = min(check_interval, max_wait_time - elapsed)
136+
if sleep_for > 0:
137+
wait_with_progress(
138+
sleep_for,
139+
f"Waiting for condition (check {num_checks})",
140+
)

0 commit comments

Comments
 (0)