oagi 0.4.3__tar.gz → 0.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of oagi might be problematic. Click here for more details.
- {oagi-0.4.3 → oagi-0.5.0}/PKG-INFO +1 -1
- oagi-0.5.0/examples/continued_session.py +53 -0
- {oagi-0.4.3 → oagi-0.5.0}/pyproject.toml +1 -1
- {oagi-0.4.3 → oagi-0.5.0}/src/oagi/async_client.py +8 -0
- {oagi-0.4.3 → oagi-0.5.0}/src/oagi/async_short_task.py +18 -2
- {oagi-0.4.3 → oagi-0.5.0}/src/oagi/async_task.py +25 -2
- {oagi-0.4.3 → oagi-0.5.0}/src/oagi/short_task.py +18 -2
- {oagi-0.4.3 → oagi-0.5.0}/src/oagi/sync_client.py +8 -0
- {oagi-0.4.3 → oagi-0.5.0}/src/oagi/task.py +25 -2
- {oagi-0.4.3 → oagi-0.5.0}/tests/test_sync_client.py +138 -0
- {oagi-0.4.3 → oagi-0.5.0}/tests/test_task.py +150 -0
- {oagi-0.4.3 → oagi-0.5.0}/uv.lock +1 -1
- {oagi-0.4.3 → oagi-0.5.0}/.github/workflows/ci.yml +0 -0
- {oagi-0.4.3 → oagi-0.5.0}/.github/workflows/release.yml +0 -0
- {oagi-0.4.3 → oagi-0.5.0}/.gitignore +0 -0
- {oagi-0.4.3 → oagi-0.5.0}/.python-version +0 -0
- {oagi-0.4.3 → oagi-0.5.0}/CONTRIBUTING.md +0 -0
- {oagi-0.4.3 → oagi-0.5.0}/LICENSE +0 -0
- {oagi-0.4.3 → oagi-0.5.0}/Makefile +0 -0
- {oagi-0.4.3 → oagi-0.5.0}/README.md +0 -0
- {oagi-0.4.3 → oagi-0.5.0}/examples/async_google_weather.py +0 -0
- {oagi-0.4.3 → oagi-0.5.0}/examples/execute_task_auto.py +0 -0
- {oagi-0.4.3 → oagi-0.5.0}/examples/execute_task_manual.py +0 -0
- {oagi-0.4.3 → oagi-0.5.0}/examples/google_weather.py +0 -0
- {oagi-0.4.3 → oagi-0.5.0}/examples/hotel_booking.py +0 -0
- {oagi-0.4.3 → oagi-0.5.0}/examples/screenshot_with_config.py +0 -0
- {oagi-0.4.3 → oagi-0.5.0}/examples/single_step.py +0 -0
- {oagi-0.4.3 → oagi-0.5.0}/src/oagi/__init__.py +0 -0
- {oagi-0.4.3 → oagi-0.5.0}/src/oagi/async_pyautogui_action_handler.py +0 -0
- {oagi-0.4.3 → oagi-0.5.0}/src/oagi/async_screenshot_maker.py +0 -0
- {oagi-0.4.3 → oagi-0.5.0}/src/oagi/async_single_step.py +0 -0
- {oagi-0.4.3 → oagi-0.5.0}/src/oagi/exceptions.py +0 -0
- {oagi-0.4.3 → oagi-0.5.0}/src/oagi/logging.py +0 -0
- {oagi-0.4.3 → oagi-0.5.0}/src/oagi/pil_image.py +0 -0
- {oagi-0.4.3 → oagi-0.5.0}/src/oagi/pyautogui_action_handler.py +0 -0
- {oagi-0.4.3 → oagi-0.5.0}/src/oagi/screenshot_maker.py +0 -0
- {oagi-0.4.3 → oagi-0.5.0}/src/oagi/single_step.py +0 -0
- {oagi-0.4.3 → oagi-0.5.0}/src/oagi/types/__init__.py +0 -0
- {oagi-0.4.3 → oagi-0.5.0}/src/oagi/types/action_handler.py +0 -0
- {oagi-0.4.3 → oagi-0.5.0}/src/oagi/types/async_action_handler.py +0 -0
- {oagi-0.4.3 → oagi-0.5.0}/src/oagi/types/async_image_provider.py +0 -0
- {oagi-0.4.3 → oagi-0.5.0}/src/oagi/types/image.py +0 -0
- {oagi-0.4.3 → oagi-0.5.0}/src/oagi/types/image_provider.py +0 -0
- {oagi-0.4.3 → oagi-0.5.0}/src/oagi/types/models/__init__.py +0 -0
- {oagi-0.4.3 → oagi-0.5.0}/src/oagi/types/models/action.py +0 -0
- {oagi-0.4.3 → oagi-0.5.0}/src/oagi/types/models/image_config.py +0 -0
- {oagi-0.4.3 → oagi-0.5.0}/src/oagi/types/models/step.py +0 -0
- {oagi-0.4.3 → oagi-0.5.0}/tests/__init__.py +0 -0
- {oagi-0.4.3 → oagi-0.5.0}/tests/conftest.py +0 -0
- {oagi-0.4.3 → oagi-0.5.0}/tests/test_async_client.py +0 -0
- {oagi-0.4.3 → oagi-0.5.0}/tests/test_async_handlers.py +0 -0
- {oagi-0.4.3 → oagi-0.5.0}/tests/test_async_task.py +0 -0
- {oagi-0.4.3 → oagi-0.5.0}/tests/test_logging.py +0 -0
- {oagi-0.4.3 → oagi-0.5.0}/tests/test_pil_image.py +0 -0
- {oagi-0.4.3 → oagi-0.5.0}/tests/test_pyautogui_action_handler.py +0 -0
- {oagi-0.4.3 → oagi-0.5.0}/tests/test_screenshot_maker.py +0 -0
- {oagi-0.4.3 → oagi-0.5.0}/tests/test_short_task.py +0 -0
- {oagi-0.4.3 → oagi-0.5.0}/tests/test_single_step.py +0 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# -----------------------------------------------------------------------------
|
|
2
|
+
# Copyright (c) OpenAGI Foundation
|
|
3
|
+
# All rights reserved.
|
|
4
|
+
#
|
|
5
|
+
# This file is part of the official API project.
|
|
6
|
+
# Licensed under the MIT License.
|
|
7
|
+
# -----------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
from oagi import ScreenshotMaker, ShortTask
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def main():
|
|
13
|
+
# First session - start a new task
|
|
14
|
+
print("=== First Session ===")
|
|
15
|
+
task1 = ShortTask()
|
|
16
|
+
task1.init_task("Open calculator app")
|
|
17
|
+
|
|
18
|
+
image_provider = ScreenshotMaker()
|
|
19
|
+
|
|
20
|
+
# Execute a few steps in the first session
|
|
21
|
+
for i in range(1):
|
|
22
|
+
image = image_provider()
|
|
23
|
+
step = task1.step(image)
|
|
24
|
+
print(f"Session 1, Step {i + 1}: {step.reason}, Actions: {step.actions}")
|
|
25
|
+
|
|
26
|
+
if step.stop:
|
|
27
|
+
break
|
|
28
|
+
|
|
29
|
+
# Save the task_id from the first session
|
|
30
|
+
previous_task_id = task1.task_id
|
|
31
|
+
print(f"\nFirst session task_id: {previous_task_id}")
|
|
32
|
+
|
|
33
|
+
# Second session - continue with context from the first session
|
|
34
|
+
print("\n=== Second Session (with history) ===")
|
|
35
|
+
task2 = ShortTask()
|
|
36
|
+
task2.init_task("Calculate 25 * 4", last_task_id=previous_task_id, history_steps=1)
|
|
37
|
+
|
|
38
|
+
# Execute steps with history context
|
|
39
|
+
for i in range(3):
|
|
40
|
+
image = image_provider()
|
|
41
|
+
step = task2.step(image)
|
|
42
|
+
print(f"Session 2, Step {i + 1}: {step.reason}, Actions: {step.actions}")
|
|
43
|
+
|
|
44
|
+
if step.stop:
|
|
45
|
+
print("✓ Task completed!")
|
|
46
|
+
break
|
|
47
|
+
|
|
48
|
+
print(f"\nSecond session task_id: {task2.task_id}")
|
|
49
|
+
print(f"Used history from: {task2.last_task_id}")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
if __name__ == "__main__":
|
|
53
|
+
main()
|
|
@@ -91,6 +91,8 @@ class AsyncClient:
|
|
|
91
91
|
task_id: str | None = None,
|
|
92
92
|
instruction: str | None = None,
|
|
93
93
|
max_actions: int | None = 5,
|
|
94
|
+
last_task_id: str | None = None,
|
|
95
|
+
history_steps: int | None = None,
|
|
94
96
|
api_version: str | None = None,
|
|
95
97
|
) -> LLMResponse:
|
|
96
98
|
"""
|
|
@@ -103,6 +105,8 @@ class AsyncClient:
|
|
|
103
105
|
task_id: Task ID for continuing existing task
|
|
104
106
|
instruction: Additional instruction when continuing a session (only works with task_id)
|
|
105
107
|
max_actions: Maximum number of actions to return (1-20)
|
|
108
|
+
last_task_id: Previous task ID to retrieve history from (only works with task_id)
|
|
109
|
+
history_steps: Number of historical steps to include from last_task_id (default: 1, max: 10)
|
|
106
110
|
api_version: API version header
|
|
107
111
|
|
|
108
112
|
Returns:
|
|
@@ -127,6 +131,10 @@ class AsyncClient:
|
|
|
127
131
|
payload["instruction"] = instruction
|
|
128
132
|
if max_actions is not None:
|
|
129
133
|
payload["max_actions"] = max_actions
|
|
134
|
+
if last_task_id is not None:
|
|
135
|
+
payload["last_task_id"] = last_task_id
|
|
136
|
+
if history_steps is not None:
|
|
137
|
+
payload["history_steps"] = history_steps
|
|
130
138
|
|
|
131
139
|
logger.info(f"Making async API request to /v1/message with model: {model}")
|
|
132
140
|
logger.debug(
|
|
@@ -30,12 +30,28 @@ class AsyncShortTask(AsyncTask):
|
|
|
30
30
|
max_steps: int = 5,
|
|
31
31
|
executor: AsyncActionHandler = None,
|
|
32
32
|
image_provider: AsyncImageProvider = None,
|
|
33
|
+
last_task_id: str | None = None,
|
|
34
|
+
history_steps: int | None = None,
|
|
33
35
|
) -> bool:
|
|
34
|
-
"""Run the task in automatic mode with the provided executor and image provider.
|
|
36
|
+
"""Run the task in automatic mode with the provided executor and image provider.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
task_desc: Task description
|
|
40
|
+
max_steps: Maximum number of steps
|
|
41
|
+
executor: Async handler to execute actions
|
|
42
|
+
image_provider: Async provider for screenshots
|
|
43
|
+
last_task_id: Previous task ID to retrieve history from
|
|
44
|
+
history_steps: Number of historical steps to include
|
|
45
|
+
"""
|
|
35
46
|
logger.info(
|
|
36
47
|
f"Starting async auto mode for task: '{task_desc}' (max_steps: {max_steps})"
|
|
37
48
|
)
|
|
38
|
-
await self.init_task(
|
|
49
|
+
await self.init_task(
|
|
50
|
+
task_desc,
|
|
51
|
+
max_steps=max_steps,
|
|
52
|
+
last_task_id=last_task_id,
|
|
53
|
+
history_steps=history_steps,
|
|
54
|
+
)
|
|
39
55
|
|
|
40
56
|
for i in range(max_steps):
|
|
41
57
|
logger.debug(f"Async auto mode step {i + 1}/{max_steps}")
|
|
@@ -29,10 +29,27 @@ class AsyncTask:
|
|
|
29
29
|
self.task_id: str | None = None
|
|
30
30
|
self.task_description: str | None = None
|
|
31
31
|
self.model = model
|
|
32
|
+
self.last_task_id: str | None = None
|
|
33
|
+
self.history_steps: int | None = None
|
|
32
34
|
|
|
33
|
-
async def init_task(
|
|
34
|
-
|
|
35
|
+
async def init_task(
|
|
36
|
+
self,
|
|
37
|
+
task_desc: str,
|
|
38
|
+
max_steps: int = 5,
|
|
39
|
+
last_task_id: str | None = None,
|
|
40
|
+
history_steps: int | None = None,
|
|
41
|
+
):
|
|
42
|
+
"""Initialize a new task with the given description.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
task_desc: Task description
|
|
46
|
+
max_steps: Maximum number of steps (for logging)
|
|
47
|
+
last_task_id: Previous task ID to retrieve history from
|
|
48
|
+
history_steps: Number of historical steps to include (default: 1)
|
|
49
|
+
"""
|
|
35
50
|
self.task_description = task_desc
|
|
51
|
+
self.last_task_id = last_task_id
|
|
52
|
+
self.history_steps = history_steps
|
|
36
53
|
response = await self.client.create_message(
|
|
37
54
|
model=self.model,
|
|
38
55
|
screenshot="",
|
|
@@ -41,6 +58,10 @@ class AsyncTask:
|
|
|
41
58
|
)
|
|
42
59
|
self.task_id = response.task_id # Reset task_id for new task
|
|
43
60
|
logger.info(f"Async task initialized: '{task_desc}' (max_steps: {max_steps})")
|
|
61
|
+
if last_task_id:
|
|
62
|
+
logger.info(
|
|
63
|
+
f"Will include {history_steps or 1} steps from previous task: {last_task_id}"
|
|
64
|
+
)
|
|
44
65
|
|
|
45
66
|
async def step(
|
|
46
67
|
self, screenshot: Image | bytes, instruction: str | None = None
|
|
@@ -74,6 +95,8 @@ class AsyncTask:
|
|
|
74
95
|
task_description=self.task_description,
|
|
75
96
|
task_id=self.task_id,
|
|
76
97
|
instruction=instruction,
|
|
98
|
+
last_task_id=self.last_task_id if self.task_id else None,
|
|
99
|
+
history_steps=self.history_steps if self.task_id else None,
|
|
77
100
|
)
|
|
78
101
|
|
|
79
102
|
# Update task_id from response
|
|
@@ -30,12 +30,28 @@ class ShortTask(Task):
|
|
|
30
30
|
max_steps: int = 5,
|
|
31
31
|
executor: ActionHandler = None,
|
|
32
32
|
image_provider: ImageProvider = None,
|
|
33
|
+
last_task_id: str | None = None,
|
|
34
|
+
history_steps: int | None = None,
|
|
33
35
|
) -> bool:
|
|
34
|
-
"""Run the task in automatic mode with the provided executor and image provider.
|
|
36
|
+
"""Run the task in automatic mode with the provided executor and image provider.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
task_desc: Task description
|
|
40
|
+
max_steps: Maximum number of steps
|
|
41
|
+
executor: Handler to execute actions
|
|
42
|
+
image_provider: Provider for screenshots
|
|
43
|
+
last_task_id: Previous task ID to retrieve history from
|
|
44
|
+
history_steps: Number of historical steps to include
|
|
45
|
+
"""
|
|
35
46
|
logger.info(
|
|
36
47
|
f"Starting auto mode for task: '{task_desc}' (max_steps: {max_steps})"
|
|
37
48
|
)
|
|
38
|
-
self.init_task(
|
|
49
|
+
self.init_task(
|
|
50
|
+
task_desc,
|
|
51
|
+
max_steps=max_steps,
|
|
52
|
+
last_task_id=last_task_id,
|
|
53
|
+
history_steps=history_steps,
|
|
54
|
+
)
|
|
39
55
|
|
|
40
56
|
for i in range(max_steps):
|
|
41
57
|
logger.debug(f"Auto mode step {i + 1}/{max_steps}")
|
|
@@ -130,6 +130,8 @@ class SyncClient:
|
|
|
130
130
|
task_id: str | None = None,
|
|
131
131
|
instruction: str | None = None,
|
|
132
132
|
max_actions: int | None = 5,
|
|
133
|
+
last_task_id: str | None = None,
|
|
134
|
+
history_steps: int | None = None,
|
|
133
135
|
api_version: str | None = None,
|
|
134
136
|
) -> LLMResponse:
|
|
135
137
|
"""
|
|
@@ -142,6 +144,8 @@ class SyncClient:
|
|
|
142
144
|
task_id: Task ID for continuing existing task
|
|
143
145
|
instruction: Additional instruction when continuing a session (only works with task_id)
|
|
144
146
|
max_actions: Maximum number of actions to return (1-20)
|
|
147
|
+
last_task_id: Previous task ID to retrieve history from (only works with task_id)
|
|
148
|
+
history_steps: Number of historical steps to include from last_task_id (default: 1, max: 10)
|
|
145
149
|
api_version: API version header
|
|
146
150
|
|
|
147
151
|
Returns:
|
|
@@ -166,6 +170,10 @@ class SyncClient:
|
|
|
166
170
|
payload["instruction"] = instruction
|
|
167
171
|
if max_actions is not None:
|
|
168
172
|
payload["max_actions"] = max_actions
|
|
173
|
+
if last_task_id is not None:
|
|
174
|
+
payload["last_task_id"] = last_task_id
|
|
175
|
+
if history_steps is not None:
|
|
176
|
+
payload["history_steps"] = history_steps
|
|
169
177
|
|
|
170
178
|
logger.info(f"Making API request to /v1/message with model: {model}")
|
|
171
179
|
logger.debug(
|
|
@@ -28,10 +28,27 @@ class Task:
|
|
|
28
28
|
self.task_id: str | None = None
|
|
29
29
|
self.task_description: str | None = None
|
|
30
30
|
self.model = model
|
|
31
|
+
self.last_task_id: str | None = None
|
|
32
|
+
self.history_steps: int | None = None
|
|
31
33
|
|
|
32
|
-
def init_task(
|
|
33
|
-
|
|
34
|
+
def init_task(
|
|
35
|
+
self,
|
|
36
|
+
task_desc: str,
|
|
37
|
+
max_steps: int = 5,
|
|
38
|
+
last_task_id: str | None = None,
|
|
39
|
+
history_steps: int | None = None,
|
|
40
|
+
):
|
|
41
|
+
"""Initialize a new task with the given description.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
task_desc: Task description
|
|
45
|
+
max_steps: Maximum number of steps (for logging)
|
|
46
|
+
last_task_id: Previous task ID to retrieve history from
|
|
47
|
+
history_steps: Number of historical steps to include (default: 1)
|
|
48
|
+
"""
|
|
34
49
|
self.task_description = task_desc
|
|
50
|
+
self.last_task_id = last_task_id
|
|
51
|
+
self.history_steps = history_steps
|
|
35
52
|
response = self.client.create_message(
|
|
36
53
|
model=self.model,
|
|
37
54
|
screenshot="",
|
|
@@ -40,6 +57,10 @@ class Task:
|
|
|
40
57
|
)
|
|
41
58
|
self.task_id = response.task_id # Reset task_id for new task
|
|
42
59
|
logger.info(f"Task initialized: '{task_desc}' (max_steps: {max_steps})")
|
|
60
|
+
if last_task_id:
|
|
61
|
+
logger.info(
|
|
62
|
+
f"Will include {history_steps or 1} steps from previous task: {last_task_id}"
|
|
63
|
+
)
|
|
43
64
|
|
|
44
65
|
def step(self, screenshot: Image | bytes, instruction: str | None = None) -> Step:
|
|
45
66
|
"""Send screenshot to the server and get the next actions.
|
|
@@ -71,6 +92,8 @@ class Task:
|
|
|
71
92
|
task_description=self.task_description,
|
|
72
93
|
task_id=self.task_id,
|
|
73
94
|
instruction=instruction,
|
|
95
|
+
last_task_id=self.last_task_id if self.task_id else None,
|
|
96
|
+
history_steps=self.history_steps if self.task_id else None,
|
|
74
97
|
)
|
|
75
98
|
|
|
76
99
|
# Update task_id from response
|
|
@@ -19,6 +19,7 @@ from oagi.exceptions import (
|
|
|
19
19
|
AuthenticationError,
|
|
20
20
|
ConfigurationError,
|
|
21
21
|
RequestTimeoutError,
|
|
22
|
+
ValidationError,
|
|
22
23
|
)
|
|
23
24
|
from oagi.sync_client import (
|
|
24
25
|
ErrorDetail,
|
|
@@ -300,6 +301,143 @@ class TestHelperFunctions:
|
|
|
300
301
|
mock_open.assert_called_once_with("/path/to/image.png", "rb")
|
|
301
302
|
|
|
302
303
|
|
|
304
|
+
class TestSyncClientHistory:
|
|
305
|
+
"""Test the history functionality of SyncClient."""
|
|
306
|
+
|
|
307
|
+
@pytest.fixture
|
|
308
|
+
def base_client_setup(self):
|
|
309
|
+
"""Set up base client with successful response."""
|
|
310
|
+
client = SyncClient(base_url="http://test.com", api_key="test-key")
|
|
311
|
+
with patch.object(client.client, "post") as mock_post:
|
|
312
|
+
mock_response = Mock()
|
|
313
|
+
mock_response.status_code = 200
|
|
314
|
+
mock_response.json.return_value = {
|
|
315
|
+
"id": "resp-123",
|
|
316
|
+
"task_id": "task-456",
|
|
317
|
+
"object": "task.completion",
|
|
318
|
+
"created": 1677652288,
|
|
319
|
+
"model": "vision-model-v1",
|
|
320
|
+
"task_description": "Test task",
|
|
321
|
+
"current_step": 1,
|
|
322
|
+
"is_complete": False,
|
|
323
|
+
"actions": [],
|
|
324
|
+
"usage": {
|
|
325
|
+
"prompt_tokens": 100,
|
|
326
|
+
"completion_tokens": 50,
|
|
327
|
+
"total_tokens": 150,
|
|
328
|
+
},
|
|
329
|
+
}
|
|
330
|
+
mock_post.return_value = mock_response
|
|
331
|
+
yield client, mock_post
|
|
332
|
+
|
|
333
|
+
def test_create_message_with_history(self, base_client_setup):
|
|
334
|
+
"""Test create_message with history parameters."""
|
|
335
|
+
client, mock_post = base_client_setup
|
|
336
|
+
|
|
337
|
+
# Call with history parameters
|
|
338
|
+
client.create_message(
|
|
339
|
+
model="vision-model-v1",
|
|
340
|
+
screenshot="base64_image",
|
|
341
|
+
task_id="task-456",
|
|
342
|
+
last_task_id="previous-task",
|
|
343
|
+
history_steps=2,
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
# Verify request payload includes history params
|
|
347
|
+
mock_post.assert_called_once()
|
|
348
|
+
call_args = mock_post.call_args
|
|
349
|
+
payload = call_args[1]["json"]
|
|
350
|
+
assert payload["last_task_id"] == "previous-task"
|
|
351
|
+
assert payload["history_steps"] == 2
|
|
352
|
+
|
|
353
|
+
def test_create_message_without_history(self, base_client_setup):
|
|
354
|
+
"""Test create_message without history parameters."""
|
|
355
|
+
client, mock_post = base_client_setup
|
|
356
|
+
|
|
357
|
+
# Call without history parameters
|
|
358
|
+
client.create_message(
|
|
359
|
+
model="vision-model-v1",
|
|
360
|
+
screenshot="base64_image",
|
|
361
|
+
task_id="task-456",
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
# Verify request payload doesn't include history params
|
|
365
|
+
mock_post.assert_called_once()
|
|
366
|
+
call_args = mock_post.call_args
|
|
367
|
+
payload = call_args[1]["json"]
|
|
368
|
+
assert "last_task_id" not in payload
|
|
369
|
+
assert "history_steps" not in payload
|
|
370
|
+
|
|
371
|
+
def test_history_validation_error(self):
|
|
372
|
+
"""Test that server validation errors are handled properly."""
|
|
373
|
+
client = SyncClient(base_url="http://test.com", api_key="test-key")
|
|
374
|
+
|
|
375
|
+
with patch.object(client.client, "post") as mock_post:
|
|
376
|
+
# Mock validation error response
|
|
377
|
+
mock_response = Mock()
|
|
378
|
+
mock_response.status_code = 422
|
|
379
|
+
mock_response.json.return_value = {
|
|
380
|
+
"error": {
|
|
381
|
+
"code": "validation_error",
|
|
382
|
+
"message": "last_task_id can only be used when continuing a session with task_id",
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
mock_post.return_value = mock_response
|
|
386
|
+
|
|
387
|
+
# Try to use last_task_id without task_id
|
|
388
|
+
with pytest.raises(ValidationError) as exc_info:
|
|
389
|
+
client.create_message(
|
|
390
|
+
model="vision-model-v1",
|
|
391
|
+
screenshot="base64_image",
|
|
392
|
+
task_description="New task",
|
|
393
|
+
last_task_id="previous-task",
|
|
394
|
+
history_steps=1,
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
assert "last_task_id can only be used" in str(exc_info.value)
|
|
398
|
+
|
|
399
|
+
def test_history_with_instruction(self, base_client_setup):
|
|
400
|
+
"""Test combining history with instruction."""
|
|
401
|
+
client, mock_post = base_client_setup
|
|
402
|
+
|
|
403
|
+
# Call with both history and instruction
|
|
404
|
+
client.create_message(
|
|
405
|
+
model="vision-model-v1",
|
|
406
|
+
screenshot="base64_image",
|
|
407
|
+
task_id="task-456",
|
|
408
|
+
instruction="Click submit",
|
|
409
|
+
last_task_id="previous-task",
|
|
410
|
+
history_steps=1,
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
# Verify all parameters are included
|
|
414
|
+
mock_post.assert_called_once()
|
|
415
|
+
call_args = mock_post.call_args
|
|
416
|
+
payload = call_args[1]["json"]
|
|
417
|
+
assert payload["instruction"] == "Click submit"
|
|
418
|
+
assert payload["last_task_id"] == "previous-task"
|
|
419
|
+
assert payload["history_steps"] == 1
|
|
420
|
+
|
|
421
|
+
def test_history_default_steps(self, base_client_setup):
|
|
422
|
+
"""Test that omitting history_steps works."""
|
|
423
|
+
client, mock_post = base_client_setup
|
|
424
|
+
|
|
425
|
+
# Call with only last_task_id
|
|
426
|
+
client.create_message(
|
|
427
|
+
model="vision-model-v1",
|
|
428
|
+
screenshot="base64_image",
|
|
429
|
+
task_id="task-456",
|
|
430
|
+
last_task_id="previous-task",
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
# Verify request
|
|
434
|
+
mock_post.assert_called_once()
|
|
435
|
+
call_args = mock_post.call_args
|
|
436
|
+
payload = call_args[1]["json"]
|
|
437
|
+
assert payload["last_task_id"] == "previous-task"
|
|
438
|
+
assert "history_steps" not in payload # None values aren't sent
|
|
439
|
+
|
|
440
|
+
|
|
303
441
|
class TestTraceLogging:
|
|
304
442
|
@pytest.mark.parametrize(
|
|
305
443
|
"trace_headers,expected_logs",
|
|
@@ -108,6 +108,8 @@ class TestStep:
|
|
|
108
108
|
task_description="Test task",
|
|
109
109
|
task_id="existing-task",
|
|
110
110
|
instruction=None,
|
|
111
|
+
last_task_id=None,
|
|
112
|
+
history_steps=None,
|
|
111
113
|
)
|
|
112
114
|
|
|
113
115
|
# Verify returned Step
|
|
@@ -140,6 +142,8 @@ class TestStep:
|
|
|
140
142
|
task_description="Test task",
|
|
141
143
|
task_id=None,
|
|
142
144
|
instruction=None,
|
|
145
|
+
last_task_id=None,
|
|
146
|
+
history_steps=None,
|
|
143
147
|
)
|
|
144
148
|
|
|
145
149
|
# Verify task_id was updated
|
|
@@ -209,6 +213,8 @@ class TestStep:
|
|
|
209
213
|
task_description="Test task",
|
|
210
214
|
task_id="existing-task",
|
|
211
215
|
instruction="Click the submit button",
|
|
216
|
+
last_task_id=None,
|
|
217
|
+
history_steps=None,
|
|
212
218
|
)
|
|
213
219
|
|
|
214
220
|
assert isinstance(result, Step)
|
|
@@ -281,3 +287,147 @@ class TestIntegrationScenarios:
|
|
|
281
287
|
# Verify second call used the task_id
|
|
282
288
|
calls = task.client.create_message.call_args_list
|
|
283
289
|
assert calls[1][1]["task_id"] == "task-456"
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
class TestTaskHistory:
|
|
293
|
+
"""Test Task class history functionality."""
|
|
294
|
+
|
|
295
|
+
def test_init_task_with_history(self, task, sample_llm_response):
|
|
296
|
+
"""Test init_task with history parameters."""
|
|
297
|
+
task.client.create_message.return_value = sample_llm_response
|
|
298
|
+
|
|
299
|
+
# Initialize task with history
|
|
300
|
+
task.init_task(
|
|
301
|
+
"Test task",
|
|
302
|
+
max_steps=5,
|
|
303
|
+
last_task_id="previous-task",
|
|
304
|
+
history_steps=2,
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
# Verify task attributes
|
|
308
|
+
assert task.task_description == "Test task"
|
|
309
|
+
assert task.task_id == "task-456"
|
|
310
|
+
assert task.last_task_id == "previous-task"
|
|
311
|
+
assert task.history_steps == 2
|
|
312
|
+
|
|
313
|
+
# History is not sent during init, only stored
|
|
314
|
+
task.client.create_message.assert_called_once_with(
|
|
315
|
+
model="vision-model-v1",
|
|
316
|
+
screenshot="",
|
|
317
|
+
task_description="Test task",
|
|
318
|
+
task_id=None,
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
def test_init_task_without_history(self, task, sample_llm_response):
|
|
322
|
+
"""Test init_task without history parameters."""
|
|
323
|
+
task.client.create_message.return_value = sample_llm_response
|
|
324
|
+
|
|
325
|
+
# Initialize task without history
|
|
326
|
+
task.init_task("Test task", max_steps=5)
|
|
327
|
+
|
|
328
|
+
# Verify task attributes
|
|
329
|
+
assert task.task_description == "Test task"
|
|
330
|
+
assert task.task_id == "task-456"
|
|
331
|
+
assert task.last_task_id is None
|
|
332
|
+
assert task.history_steps is None
|
|
333
|
+
|
|
334
|
+
def test_step_with_history(self, task, sample_llm_response):
|
|
335
|
+
"""Test step method with history (continuing session)."""
|
|
336
|
+
task.task_description = "Test task"
|
|
337
|
+
task.task_id = "task-456" # Already have task_id
|
|
338
|
+
task.last_task_id = "previous-task"
|
|
339
|
+
task.history_steps = 2
|
|
340
|
+
task.client.create_message.return_value = sample_llm_response
|
|
341
|
+
|
|
342
|
+
with patch("oagi.task.encode_screenshot_from_bytes") as mock_encode:
|
|
343
|
+
mock_encode.return_value = "base64_encoded"
|
|
344
|
+
|
|
345
|
+
# Call step
|
|
346
|
+
task.step(b"screenshot_data", instruction="Click submit")
|
|
347
|
+
|
|
348
|
+
# Verify API call includes history params
|
|
349
|
+
task.client.create_message.assert_called_once_with(
|
|
350
|
+
model="vision-model-v1",
|
|
351
|
+
screenshot="base64_encoded",
|
|
352
|
+
task_description="Test task",
|
|
353
|
+
task_id="task-456",
|
|
354
|
+
instruction="Click submit",
|
|
355
|
+
last_task_id="previous-task",
|
|
356
|
+
history_steps=2,
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
def test_step_history_only_when_continuing(self, task, sample_llm_response):
|
|
360
|
+
"""Test that history is only sent when continuing (task_id exists)."""
|
|
361
|
+
task.task_description = "Test task"
|
|
362
|
+
task.last_task_id = "previous-task"
|
|
363
|
+
task.history_steps = 1
|
|
364
|
+
task.client.create_message.return_value = sample_llm_response
|
|
365
|
+
|
|
366
|
+
with patch("oagi.task.encode_screenshot_from_bytes") as mock_encode:
|
|
367
|
+
mock_encode.return_value = "base64_encoded"
|
|
368
|
+
|
|
369
|
+
# First step - no task_id yet
|
|
370
|
+
task.task_id = None
|
|
371
|
+
task.step(b"screenshot1")
|
|
372
|
+
|
|
373
|
+
# Verify first call - no history
|
|
374
|
+
first_call = task.client.create_message.call_args_list[0][1]
|
|
375
|
+
assert first_call["task_id"] is None
|
|
376
|
+
assert first_call["last_task_id"] is None
|
|
377
|
+
assert first_call["history_steps"] is None
|
|
378
|
+
|
|
379
|
+
# Task ID should be updated
|
|
380
|
+
assert task.task_id == "task-456"
|
|
381
|
+
|
|
382
|
+
# Second step - now have task_id
|
|
383
|
+
task.step(b"screenshot2")
|
|
384
|
+
|
|
385
|
+
# Verify second call - includes history
|
|
386
|
+
second_call = task.client.create_message.call_args_list[1][1]
|
|
387
|
+
assert second_call["task_id"] == "task-456"
|
|
388
|
+
assert second_call["last_task_id"] == "previous-task"
|
|
389
|
+
assert second_call["history_steps"] == 1
|
|
390
|
+
|
|
391
|
+
def test_init_task_with_default_history_steps(self, task, sample_llm_response):
|
|
392
|
+
"""Test that history_steps can be omitted."""
|
|
393
|
+
task.client.create_message.return_value = sample_llm_response
|
|
394
|
+
|
|
395
|
+
# Initialize task with last_task_id but no history_steps
|
|
396
|
+
task.init_task(
|
|
397
|
+
"Test task",
|
|
398
|
+
max_steps=5,
|
|
399
|
+
last_task_id="previous-task",
|
|
400
|
+
# history_steps not specified
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
# Verify task attributes
|
|
404
|
+
assert task.last_task_id == "previous-task"
|
|
405
|
+
assert task.history_steps is None # Not specified
|
|
406
|
+
|
|
407
|
+
def test_step_without_history(self, task, sample_llm_response):
|
|
408
|
+
"""Test step method without history (first step)."""
|
|
409
|
+
task.task_description = "Test task"
|
|
410
|
+
task.task_id = None # First step
|
|
411
|
+
task.client.create_message.return_value = sample_llm_response
|
|
412
|
+
|
|
413
|
+
with patch("oagi.task.encode_screenshot_from_bytes") as mock_encode:
|
|
414
|
+
mock_encode.return_value = "base64_encoded"
|
|
415
|
+
|
|
416
|
+
# Call step
|
|
417
|
+
result = task.step(b"screenshot_data")
|
|
418
|
+
|
|
419
|
+
# Verify API call - no history params on first step
|
|
420
|
+
task.client.create_message.assert_called_once_with(
|
|
421
|
+
model="vision-model-v1",
|
|
422
|
+
screenshot="base64_encoded",
|
|
423
|
+
task_description="Test task",
|
|
424
|
+
task_id=None,
|
|
425
|
+
instruction=None,
|
|
426
|
+
last_task_id=None,
|
|
427
|
+
history_steps=None,
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
# Verify result
|
|
431
|
+
assert isinstance(result, Step)
|
|
432
|
+
assert not result.stop
|
|
433
|
+
assert len(result.actions) == 2
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|