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.

Files changed (58) hide show
  1. {oagi-0.4.3 → oagi-0.5.0}/PKG-INFO +1 -1
  2. oagi-0.5.0/examples/continued_session.py +53 -0
  3. {oagi-0.4.3 → oagi-0.5.0}/pyproject.toml +1 -1
  4. {oagi-0.4.3 → oagi-0.5.0}/src/oagi/async_client.py +8 -0
  5. {oagi-0.4.3 → oagi-0.5.0}/src/oagi/async_short_task.py +18 -2
  6. {oagi-0.4.3 → oagi-0.5.0}/src/oagi/async_task.py +25 -2
  7. {oagi-0.4.3 → oagi-0.5.0}/src/oagi/short_task.py +18 -2
  8. {oagi-0.4.3 → oagi-0.5.0}/src/oagi/sync_client.py +8 -0
  9. {oagi-0.4.3 → oagi-0.5.0}/src/oagi/task.py +25 -2
  10. {oagi-0.4.3 → oagi-0.5.0}/tests/test_sync_client.py +138 -0
  11. {oagi-0.4.3 → oagi-0.5.0}/tests/test_task.py +150 -0
  12. {oagi-0.4.3 → oagi-0.5.0}/uv.lock +1 -1
  13. {oagi-0.4.3 → oagi-0.5.0}/.github/workflows/ci.yml +0 -0
  14. {oagi-0.4.3 → oagi-0.5.0}/.github/workflows/release.yml +0 -0
  15. {oagi-0.4.3 → oagi-0.5.0}/.gitignore +0 -0
  16. {oagi-0.4.3 → oagi-0.5.0}/.python-version +0 -0
  17. {oagi-0.4.3 → oagi-0.5.0}/CONTRIBUTING.md +0 -0
  18. {oagi-0.4.3 → oagi-0.5.0}/LICENSE +0 -0
  19. {oagi-0.4.3 → oagi-0.5.0}/Makefile +0 -0
  20. {oagi-0.4.3 → oagi-0.5.0}/README.md +0 -0
  21. {oagi-0.4.3 → oagi-0.5.0}/examples/async_google_weather.py +0 -0
  22. {oagi-0.4.3 → oagi-0.5.0}/examples/execute_task_auto.py +0 -0
  23. {oagi-0.4.3 → oagi-0.5.0}/examples/execute_task_manual.py +0 -0
  24. {oagi-0.4.3 → oagi-0.5.0}/examples/google_weather.py +0 -0
  25. {oagi-0.4.3 → oagi-0.5.0}/examples/hotel_booking.py +0 -0
  26. {oagi-0.4.3 → oagi-0.5.0}/examples/screenshot_with_config.py +0 -0
  27. {oagi-0.4.3 → oagi-0.5.0}/examples/single_step.py +0 -0
  28. {oagi-0.4.3 → oagi-0.5.0}/src/oagi/__init__.py +0 -0
  29. {oagi-0.4.3 → oagi-0.5.0}/src/oagi/async_pyautogui_action_handler.py +0 -0
  30. {oagi-0.4.3 → oagi-0.5.0}/src/oagi/async_screenshot_maker.py +0 -0
  31. {oagi-0.4.3 → oagi-0.5.0}/src/oagi/async_single_step.py +0 -0
  32. {oagi-0.4.3 → oagi-0.5.0}/src/oagi/exceptions.py +0 -0
  33. {oagi-0.4.3 → oagi-0.5.0}/src/oagi/logging.py +0 -0
  34. {oagi-0.4.3 → oagi-0.5.0}/src/oagi/pil_image.py +0 -0
  35. {oagi-0.4.3 → oagi-0.5.0}/src/oagi/pyautogui_action_handler.py +0 -0
  36. {oagi-0.4.3 → oagi-0.5.0}/src/oagi/screenshot_maker.py +0 -0
  37. {oagi-0.4.3 → oagi-0.5.0}/src/oagi/single_step.py +0 -0
  38. {oagi-0.4.3 → oagi-0.5.0}/src/oagi/types/__init__.py +0 -0
  39. {oagi-0.4.3 → oagi-0.5.0}/src/oagi/types/action_handler.py +0 -0
  40. {oagi-0.4.3 → oagi-0.5.0}/src/oagi/types/async_action_handler.py +0 -0
  41. {oagi-0.4.3 → oagi-0.5.0}/src/oagi/types/async_image_provider.py +0 -0
  42. {oagi-0.4.3 → oagi-0.5.0}/src/oagi/types/image.py +0 -0
  43. {oagi-0.4.3 → oagi-0.5.0}/src/oagi/types/image_provider.py +0 -0
  44. {oagi-0.4.3 → oagi-0.5.0}/src/oagi/types/models/__init__.py +0 -0
  45. {oagi-0.4.3 → oagi-0.5.0}/src/oagi/types/models/action.py +0 -0
  46. {oagi-0.4.3 → oagi-0.5.0}/src/oagi/types/models/image_config.py +0 -0
  47. {oagi-0.4.3 → oagi-0.5.0}/src/oagi/types/models/step.py +0 -0
  48. {oagi-0.4.3 → oagi-0.5.0}/tests/__init__.py +0 -0
  49. {oagi-0.4.3 → oagi-0.5.0}/tests/conftest.py +0 -0
  50. {oagi-0.4.3 → oagi-0.5.0}/tests/test_async_client.py +0 -0
  51. {oagi-0.4.3 → oagi-0.5.0}/tests/test_async_handlers.py +0 -0
  52. {oagi-0.4.3 → oagi-0.5.0}/tests/test_async_task.py +0 -0
  53. {oagi-0.4.3 → oagi-0.5.0}/tests/test_logging.py +0 -0
  54. {oagi-0.4.3 → oagi-0.5.0}/tests/test_pil_image.py +0 -0
  55. {oagi-0.4.3 → oagi-0.5.0}/tests/test_pyautogui_action_handler.py +0 -0
  56. {oagi-0.4.3 → oagi-0.5.0}/tests/test_screenshot_maker.py +0 -0
  57. {oagi-0.4.3 → oagi-0.5.0}/tests/test_short_task.py +0 -0
  58. {oagi-0.4.3 → oagi-0.5.0}/tests/test_single_step.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: oagi
3
- Version: 0.4.3
3
+ Version: 0.5.0
4
4
  Summary: Official API of OpenAGI Foundation
5
5
  Project-URL: Homepage, https://github.com/agiopen-org/oagi
6
6
  Author-email: OpenAGI Foundation <contact@agiopen.org>
@@ -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()
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "oagi"
7
- version = "0.4.3"
7
+ version = "0.5.0"
8
8
  description = "Official API of OpenAGI Foundation"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -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(task_desc, max_steps=max_steps)
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(self, task_desc: str, max_steps: int = 5):
34
- """Initialize a new task with the given description."""
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(task_desc, max_steps=max_steps)
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(self, task_desc: str, max_steps: int = 5):
33
- """Initialize a new task with the given description."""
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
@@ -138,7 +138,7 @@ sdist = { url = "https://files.pythonhosted.org/packages/28/fa/b2ba8229b9381e8f6
138
138
 
139
139
  [[package]]
140
140
  name = "oagi"
141
- version = "0.4.3"
141
+ version = "0.5.0"
142
142
  source = { editable = "." }
143
143
  dependencies = [
144
144
  { name = "httpx" },
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