oagi 0.5.0__tar.gz → 0.6.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 (65) hide show
  1. {oagi-0.5.0 → oagi-0.6.0}/PKG-INFO +1 -1
  2. {oagi-0.5.0 → oagi-0.6.0}/examples/single_step.py +4 -6
  3. {oagi-0.5.0 → oagi-0.6.0}/pyproject.toml +1 -1
  4. {oagi-0.5.0 → oagi-0.6.0}/src/oagi/__init__.py +3 -6
  5. {oagi-0.5.0 → oagi-0.6.0}/src/oagi/async_single_step.py +4 -2
  6. {oagi-0.5.0/src/oagi/types/models → oagi-0.6.0/src/oagi/client}/__init__.py +3 -4
  7. oagi-0.6.0/src/oagi/client/async_.py +137 -0
  8. oagi-0.6.0/src/oagi/client/base.py +183 -0
  9. oagi-0.6.0/src/oagi/client/sync.py +142 -0
  10. {oagi-0.5.0 → oagi-0.6.0}/src/oagi/single_step.py +3 -1
  11. oagi-0.6.0/src/oagi/task/__init__.py +14 -0
  12. oagi-0.5.0/src/oagi/async_task.py → oagi-0.6.0/src/oagi/task/async_.py +23 -50
  13. oagi-0.5.0/src/oagi/async_short_task.py → oagi-0.6.0/src/oagi/task/async_short.py +18 -17
  14. oagi-0.6.0/src/oagi/task/base.py +130 -0
  15. oagi-0.5.0/src/oagi/short_task.py → oagi-0.6.0/src/oagi/task/short.py +18 -13
  16. oagi-0.5.0/src/oagi/task.py → oagi-0.6.0/src/oagi/task/sync.py +25 -49
  17. oagi-0.6.0/src/oagi/types/models/__init__.py +23 -0
  18. oagi-0.6.0/src/oagi/types/models/client.py +45 -0
  19. {oagi-0.5.0 → oagi-0.6.0}/tests/conftest.py +4 -4
  20. {oagi-0.5.0 → oagi-0.6.0}/tests/test_async_client.py +27 -1
  21. {oagi-0.5.0 → oagi-0.6.0}/tests/test_async_task.py +36 -2
  22. {oagi-0.5.0 → oagi-0.6.0}/tests/test_logging.py +1 -1
  23. {oagi-0.5.0 → oagi-0.6.0}/tests/test_short_task.py +6 -6
  24. {oagi-0.5.0 → oagi-0.6.0}/tests/test_single_step.py +18 -2
  25. {oagi-0.5.0 → oagi-0.6.0}/tests/test_sync_client.py +26 -9
  26. {oagi-0.5.0 → oagi-0.6.0}/tests/test_task.py +74 -11
  27. {oagi-0.5.0 → oagi-0.6.0}/uv.lock +1 -1
  28. oagi-0.5.0/src/oagi/async_client.py +0 -247
  29. oagi-0.5.0/src/oagi/sync_client.py +0 -297
  30. {oagi-0.5.0 → oagi-0.6.0}/.github/workflows/ci.yml +0 -0
  31. {oagi-0.5.0 → oagi-0.6.0}/.github/workflows/release.yml +0 -0
  32. {oagi-0.5.0 → oagi-0.6.0}/.gitignore +0 -0
  33. {oagi-0.5.0 → oagi-0.6.0}/.python-version +0 -0
  34. {oagi-0.5.0 → oagi-0.6.0}/CONTRIBUTING.md +0 -0
  35. {oagi-0.5.0 → oagi-0.6.0}/LICENSE +0 -0
  36. {oagi-0.5.0 → oagi-0.6.0}/Makefile +0 -0
  37. {oagi-0.5.0 → oagi-0.6.0}/README.md +0 -0
  38. {oagi-0.5.0 → oagi-0.6.0}/examples/async_google_weather.py +0 -0
  39. {oagi-0.5.0 → oagi-0.6.0}/examples/continued_session.py +0 -0
  40. {oagi-0.5.0 → oagi-0.6.0}/examples/execute_task_auto.py +0 -0
  41. {oagi-0.5.0 → oagi-0.6.0}/examples/execute_task_manual.py +0 -0
  42. {oagi-0.5.0 → oagi-0.6.0}/examples/google_weather.py +0 -0
  43. {oagi-0.5.0 → oagi-0.6.0}/examples/hotel_booking.py +0 -0
  44. {oagi-0.5.0 → oagi-0.6.0}/examples/screenshot_with_config.py +0 -0
  45. {oagi-0.5.0 → oagi-0.6.0}/src/oagi/async_pyautogui_action_handler.py +0 -0
  46. {oagi-0.5.0 → oagi-0.6.0}/src/oagi/async_screenshot_maker.py +0 -0
  47. {oagi-0.5.0 → oagi-0.6.0}/src/oagi/exceptions.py +0 -0
  48. {oagi-0.5.0 → oagi-0.6.0}/src/oagi/logging.py +0 -0
  49. {oagi-0.5.0 → oagi-0.6.0}/src/oagi/pil_image.py +0 -0
  50. {oagi-0.5.0 → oagi-0.6.0}/src/oagi/pyautogui_action_handler.py +0 -0
  51. {oagi-0.5.0 → oagi-0.6.0}/src/oagi/screenshot_maker.py +0 -0
  52. {oagi-0.5.0 → oagi-0.6.0}/src/oagi/types/__init__.py +0 -0
  53. {oagi-0.5.0 → oagi-0.6.0}/src/oagi/types/action_handler.py +0 -0
  54. {oagi-0.5.0 → oagi-0.6.0}/src/oagi/types/async_action_handler.py +0 -0
  55. {oagi-0.5.0 → oagi-0.6.0}/src/oagi/types/async_image_provider.py +0 -0
  56. {oagi-0.5.0 → oagi-0.6.0}/src/oagi/types/image.py +0 -0
  57. {oagi-0.5.0 → oagi-0.6.0}/src/oagi/types/image_provider.py +0 -0
  58. {oagi-0.5.0 → oagi-0.6.0}/src/oagi/types/models/action.py +0 -0
  59. {oagi-0.5.0 → oagi-0.6.0}/src/oagi/types/models/image_config.py +0 -0
  60. {oagi-0.5.0 → oagi-0.6.0}/src/oagi/types/models/step.py +0 -0
  61. {oagi-0.5.0 → oagi-0.6.0}/tests/__init__.py +0 -0
  62. {oagi-0.5.0 → oagi-0.6.0}/tests/test_async_handlers.py +0 -0
  63. {oagi-0.5.0 → oagi-0.6.0}/tests/test_pil_image.py +0 -0
  64. {oagi-0.5.0 → oagi-0.6.0}/tests/test_pyautogui_action_handler.py +0 -0
  65. {oagi-0.5.0 → oagi-0.6.0}/tests/test_screenshot_maker.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: oagi
3
- Version: 0.5.0
3
+ Version: 0.6.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>
@@ -6,16 +6,14 @@
6
6
  # Licensed under the MIT License.
7
7
  # -----------------------------------------------------------------------------
8
8
 
9
- from oagi import ScreenshotMaker, single_step
9
+ from oagi import single_step
10
10
 
11
- image_provider = ScreenshotMaker()
12
- image = image_provider()
13
11
  step = single_step(
14
12
  task_description="Search weather with Google",
15
- screenshot=image, # bytes or Path object or Image object
13
+ screenshot="some/path/to/local/image", # bytes or Path object or Image object
16
14
  instruction="The operating system is macos", # optional instruction
17
- api_key="sk-50DPDW87GnlNcH_0cAPRFZ4ntweCEdUrLEFIcQFaBhc",
18
- base_url="http://127.0.0.1:8000",
15
+ # api_key="your-api-key", if not set with OAGI_API_KEY env var
16
+ # base_url="https://api.example.com" if not set with OAGI_BASE_URL env var
19
17
  )
20
18
 
21
19
  print(step)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "oagi"
7
- version = "0.5.0"
7
+ version = "0.6.0"
8
8
  description = "Official API of OpenAGI Foundation"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -6,12 +6,10 @@
6
6
  # Licensed under the MIT License.
7
7
  # -----------------------------------------------------------------------------
8
8
 
9
- from oagi.async_client import AsyncClient
10
9
  from oagi.async_pyautogui_action_handler import AsyncPyautoguiActionHandler
11
10
  from oagi.async_screenshot_maker import AsyncScreenshotMaker
12
- from oagi.async_short_task import AsyncShortTask
13
11
  from oagi.async_single_step import async_single_step
14
- from oagi.async_task import AsyncTask
12
+ from oagi.client import AsyncClient, SyncClient
15
13
  from oagi.exceptions import (
16
14
  APIError,
17
15
  AuthenticationError,
@@ -27,15 +25,14 @@ from oagi.exceptions import (
27
25
  from oagi.pil_image import PILImage
28
26
  from oagi.pyautogui_action_handler import PyautoguiActionHandler, PyautoguiConfig
29
27
  from oagi.screenshot_maker import ScreenshotMaker
30
- from oagi.short_task import ShortTask
31
28
  from oagi.single_step import single_step
32
- from oagi.sync_client import ErrorDetail, ErrorResponse, LLMResponse, SyncClient
33
- from oagi.task import Task
29
+ from oagi.task import AsyncShortTask, AsyncTask, ShortTask, Task
34
30
  from oagi.types import (
35
31
  AsyncActionHandler,
36
32
  AsyncImageProvider,
37
33
  ImageConfig,
38
34
  )
35
+ from oagi.types.models import ErrorDetail, ErrorResponse, LLMResponse
39
36
 
40
37
  __all__ = [
41
38
  # Core sync classes
@@ -8,8 +8,8 @@
8
8
 
9
9
  from pathlib import Path
10
10
 
11
- from .async_task import AsyncTask
12
11
  from .pil_image import PILImage
12
+ from .task import AsyncTask
13
13
  from .types import Image, Step
14
14
 
15
15
 
@@ -19,6 +19,7 @@ async def async_single_step(
19
19
  instruction: str | None = None,
20
20
  api_key: str | None = None,
21
21
  base_url: str | None = None,
22
+ temperature: float | None = None,
22
23
  ) -> Step:
23
24
  """
24
25
  Perform a single-step inference asynchronously without maintaining task state.
@@ -32,6 +33,7 @@ async def async_single_step(
32
33
  instruction: Optional additional instruction for the task
33
34
  api_key: OAGI API key (uses environment variable if not provided)
34
35
  base_url: OAGI base URL (uses environment variable if not provided)
36
+ temperature: Sampling temperature (0.0-2.0) for LLM inference
35
37
 
36
38
  Returns:
37
39
  Step: Object containing reasoning, actions, and completion status
@@ -71,7 +73,7 @@ async def async_single_step(
71
73
  screenshot = PILImage.from_bytes(screenshot)
72
74
 
73
75
  # Create a temporary task instance
74
- task = AsyncTask(api_key=api_key, base_url=base_url)
76
+ task = AsyncTask(api_key=api_key, base_url=base_url, temperature=temperature)
75
77
 
76
78
  try:
77
79
  # Initialize task and perform single step
@@ -6,8 +6,7 @@
6
6
  # Licensed under the MIT License.
7
7
  # -----------------------------------------------------------------------------
8
8
 
9
- from .action import Action, ActionType
10
- from .image_config import ImageConfig
11
- from .step import Step
9
+ from .async_ import AsyncClient
10
+ from .sync import SyncClient
12
11
 
13
- __all__ = ["Action", "ActionType", "ImageConfig", "Step"]
12
+ __all__ = ["SyncClient", "AsyncClient"]
@@ -0,0 +1,137 @@
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 functools import wraps
10
+
11
+ import httpx
12
+
13
+ from ..exceptions import NetworkError, RequestTimeoutError
14
+ from ..logging import get_logger
15
+ from ..types.models import LLMResponse
16
+ from .base import BaseClient
17
+
18
+ logger = get_logger("async_client")
19
+
20
+
21
+ def async_log_trace_on_failure(func):
22
+ """Async decorator that logs trace ID when a method fails."""
23
+
24
+ @wraps(func)
25
+ async def wrapper(*args, **kwargs):
26
+ try:
27
+ return await func(*args, **kwargs)
28
+ except Exception as e:
29
+ # Try to get response from the exception if it has one
30
+ if (response := getattr(e, "response", None)) is not None:
31
+ logger.error(f"Request Id: {response.headers.get('x-request-id', '')}")
32
+ logger.error(f"Trace Id: {response.headers.get('x-trace-id', '')}")
33
+ raise
34
+
35
+ return wrapper
36
+
37
+
38
+ class AsyncClient(BaseClient[httpx.AsyncClient]):
39
+ """Asynchronous HTTP client for the OAGI API."""
40
+
41
+ def __init__(self, base_url: str | None = None, api_key: str | None = None):
42
+ super().__init__(base_url, api_key)
43
+ self.client = httpx.AsyncClient(base_url=self.base_url)
44
+ logger.info(f"AsyncClient initialized with base_url: {self.base_url}")
45
+
46
+ async def __aenter__(self):
47
+ return self
48
+
49
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
50
+ await self.client.aclose()
51
+
52
+ async def close(self):
53
+ """Close the underlying httpx async client."""
54
+ await self.client.aclose()
55
+
56
+ @async_log_trace_on_failure
57
+ async def create_message(
58
+ self,
59
+ model: str,
60
+ screenshot: str, # base64 encoded
61
+ task_description: str | None = None,
62
+ task_id: str | None = None,
63
+ instruction: str | None = None,
64
+ max_actions: int | None = 5,
65
+ last_task_id: str | None = None,
66
+ history_steps: int | None = None,
67
+ temperature: float | None = None,
68
+ api_version: str | None = None,
69
+ ) -> "LLMResponse":
70
+ """
71
+ Call the /v1/message endpoint to analyze task and screenshot
72
+
73
+ Args:
74
+ model: The model to use for task analysis
75
+ screenshot: Base64-encoded screenshot image
76
+ task_description: Description of the task (required for new sessions)
77
+ task_id: Task ID for continuing existing task
78
+ instruction: Additional instruction when continuing a session (only works with task_id)
79
+ max_actions: Maximum number of actions to return (1-20)
80
+ last_task_id: Previous task ID to retrieve history from (only works with task_id)
81
+ history_steps: Number of historical steps to include from last_task_id (default: 1, max: 10)
82
+ temperature: Sampling temperature (0.0-2.0) for LLM inference
83
+ api_version: API version header
84
+
85
+ Returns:
86
+ LLMResponse: The response from the API
87
+
88
+ Raises:
89
+ httpx.HTTPStatusError: For HTTP error responses
90
+ """
91
+ headers = self._build_headers(api_version)
92
+ payload = self._build_payload(
93
+ model=model,
94
+ screenshot=screenshot,
95
+ task_description=task_description,
96
+ task_id=task_id,
97
+ instruction=instruction,
98
+ max_actions=max_actions,
99
+ last_task_id=last_task_id,
100
+ history_steps=history_steps,
101
+ temperature=temperature,
102
+ )
103
+
104
+ self._log_request_info(model, task_description, task_id)
105
+
106
+ try:
107
+ response = await self.client.post(
108
+ "/v1/message", json=payload, headers=headers, timeout=self.timeout
109
+ )
110
+ except httpx.TimeoutException as e:
111
+ logger.error(f"Request timed out after {self.timeout} seconds")
112
+ raise RequestTimeoutError(
113
+ f"Request timed out after {self.timeout} seconds", e
114
+ )
115
+ except httpx.NetworkError as e:
116
+ logger.error(f"Network error: {e}")
117
+ raise NetworkError(f"Network error: {e}", e)
118
+
119
+ return self._process_response(response)
120
+
121
+ async def health_check(self) -> dict:
122
+ """
123
+ Call the /health endpoint for health check
124
+
125
+ Returns:
126
+ dict: Health check response
127
+ """
128
+ logger.debug("Making async health check request")
129
+ try:
130
+ response = await self.client.get("/health")
131
+ response.raise_for_status()
132
+ result = response.json()
133
+ logger.debug("Async health check successful")
134
+ return result
135
+ except httpx.HTTPStatusError as e:
136
+ logger.warning(f"Async health check failed: {e}")
137
+ raise
@@ -0,0 +1,183 @@
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
+ import os
10
+ from typing import Any, Generic, TypeVar
11
+
12
+ import httpx
13
+
14
+ from ..exceptions import (
15
+ APIError,
16
+ AuthenticationError,
17
+ ConfigurationError,
18
+ NotFoundError,
19
+ RateLimitError,
20
+ ServerError,
21
+ ValidationError,
22
+ )
23
+ from ..logging import get_logger
24
+ from ..types.models import ErrorResponse, LLMResponse
25
+
26
+ logger = get_logger("client.base")
27
+
28
+ # TypeVar for HTTP client type (httpx.Client or httpx.AsyncClient)
29
+ HttpClientT = TypeVar("HttpClientT")
30
+
31
+
32
+ class BaseClient(Generic[HttpClientT]):
33
+ """Base class with shared business logic for sync/async clients."""
34
+
35
+ def __init__(self, base_url: str | None = None, api_key: str | None = None):
36
+ # Get from environment if not provided
37
+ self.base_url = base_url or os.getenv("OAGI_BASE_URL")
38
+ self.api_key = api_key or os.getenv("OAGI_API_KEY")
39
+
40
+ # Validate required configuration
41
+ if not self.base_url:
42
+ raise ConfigurationError(
43
+ "OAGI base URL must be provided either as 'base_url' parameter or "
44
+ "OAGI_BASE_URL environment variable"
45
+ )
46
+
47
+ if not self.api_key:
48
+ raise ConfigurationError(
49
+ "OAGI API key must be provided either as 'api_key' parameter or "
50
+ "OAGI_API_KEY environment variable"
51
+ )
52
+
53
+ self.base_url = self.base_url.rstrip("/")
54
+ self.timeout = 60
55
+ self.client: HttpClientT # Will be set by subclasses
56
+
57
+ logger.info(f"Client initialized with base_url: {self.base_url}")
58
+
59
+ def _build_headers(self, api_version: str | None = None) -> dict[str, str]:
60
+ headers: dict[str, str] = {}
61
+ if api_version:
62
+ headers["x-api-version"] = api_version
63
+ if self.api_key:
64
+ headers["x-api-key"] = self.api_key
65
+ return headers
66
+
67
+ def _build_payload(
68
+ self,
69
+ model: str,
70
+ screenshot: str,
71
+ task_description: str | None = None,
72
+ task_id: str | None = None,
73
+ instruction: str | None = None,
74
+ max_actions: int | None = None,
75
+ last_task_id: str | None = None,
76
+ history_steps: int | None = None,
77
+ temperature: float | None = None,
78
+ ) -> dict[str, Any]:
79
+ payload: dict[str, Any] = {"model": model, "screenshot": screenshot}
80
+
81
+ if task_description is not None:
82
+ payload["task_description"] = task_description
83
+ if task_id is not None:
84
+ payload["task_id"] = task_id
85
+ if instruction is not None:
86
+ payload["instruction"] = instruction
87
+ if max_actions is not None:
88
+ payload["max_actions"] = max_actions
89
+ if last_task_id is not None:
90
+ payload["last_task_id"] = last_task_id
91
+ if history_steps is not None:
92
+ payload["history_steps"] = history_steps
93
+ if temperature is not None:
94
+ payload["sampling_params"] = {"temperature": temperature}
95
+
96
+ return payload
97
+
98
+ def _handle_response_error(
99
+ self, response: httpx.Response, response_data: dict
100
+ ) -> None:
101
+ error_resp = ErrorResponse(**response_data)
102
+ if error_resp.error:
103
+ error_code = error_resp.error.code
104
+ error_msg = error_resp.error.message
105
+ logger.error(f"API Error [{error_code}]: {error_msg}")
106
+
107
+ # Map to specific exception types based on status code
108
+ exception_class = self._get_exception_class(response.status_code)
109
+ raise exception_class(
110
+ error_msg,
111
+ code=error_code,
112
+ status_code=response.status_code,
113
+ response=response,
114
+ )
115
+ else:
116
+ # Error response without error details
117
+ logger.error(f"API error response without details: {response.status_code}")
118
+ exception_class = self._get_exception_class(response.status_code)
119
+ raise exception_class(
120
+ f"API error (status {response.status_code})",
121
+ status_code=response.status_code,
122
+ response=response,
123
+ )
124
+
125
+ def _get_exception_class(self, status_code: int) -> type[APIError]:
126
+ status_map = {
127
+ 401: AuthenticationError,
128
+ 404: NotFoundError,
129
+ 422: ValidationError,
130
+ 429: RateLimitError,
131
+ }
132
+
133
+ if status_code >= 500:
134
+ return ServerError
135
+
136
+ return status_map.get(status_code, APIError)
137
+
138
+ def _log_request_info(self, model: str, task_description: Any, task_id: Any):
139
+ logger.info(f"Making API request to /v1/message with model: {model}")
140
+ logger.debug(
141
+ f"Request includes task_description: {task_description is not None}, "
142
+ f"task_id: {task_id is not None}"
143
+ )
144
+
145
+ def _parse_response_json(self, response: httpx.Response) -> dict[str, Any]:
146
+ try:
147
+ return response.json()
148
+ except ValueError:
149
+ logger.error(f"Non-JSON API response: {response.status_code}")
150
+ raise APIError(
151
+ f"Invalid response format (status {response.status_code})",
152
+ status_code=response.status_code,
153
+ response=response,
154
+ )
155
+
156
+ def _process_response(self, response: httpx.Response) -> Any:
157
+ response_data = self._parse_response_json(response)
158
+
159
+ # Check if it's an error response (non-200 status)
160
+ if response.status_code != 200:
161
+ self._handle_response_error(response, response_data)
162
+
163
+ # Parse successful response
164
+ result = LLMResponse(**response_data)
165
+
166
+ # Check if the response contains an error (even with 200 status)
167
+ if result.error:
168
+ logger.error(
169
+ f"API Error in response: [{result.error.code}]: {result.error.message}"
170
+ )
171
+ raise APIError(
172
+ result.error.message,
173
+ code=result.error.code,
174
+ status_code=200,
175
+ response=response,
176
+ )
177
+
178
+ logger.info(
179
+ f"API request successful - task_id: {result.task_id}, "
180
+ f"step: {result.current_step}, complete: {result.is_complete}"
181
+ )
182
+ logger.debug(f"Response included {len(result.actions)} actions")
183
+ return result
@@ -0,0 +1,142 @@
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 functools import wraps
10
+
11
+ import httpx
12
+ from httpx import Response
13
+
14
+ from ..exceptions import NetworkError, RequestTimeoutError
15
+ from ..logging import get_logger
16
+ from ..types.models import LLMResponse
17
+ from .base import BaseClient
18
+
19
+ logger = get_logger("sync_client")
20
+
21
+
22
+ def _log_trace_id(response: Response):
23
+ logger.error(f"Request Id: {response.headers.get('x-request-id', '')}")
24
+ logger.error(f"Trace Id: {response.headers.get('x-trace-id', '')}")
25
+
26
+
27
+ def log_trace_on_failure(func):
28
+ """Decorator that logs trace ID when a method fails."""
29
+
30
+ @wraps(func)
31
+ def wrapper(*args, **kwargs):
32
+ try:
33
+ return func(*args, **kwargs)
34
+ except Exception as e:
35
+ # Try to get response from the exception if it has one
36
+ if (response := getattr(e, "response", None)) is not None:
37
+ _log_trace_id(response)
38
+ raise
39
+
40
+ return wrapper
41
+
42
+
43
+ class SyncClient(BaseClient[httpx.Client]):
44
+ """Synchronous HTTP client for the OAGI API."""
45
+
46
+ def __init__(self, base_url: str | None = None, api_key: str | None = None):
47
+ super().__init__(base_url, api_key)
48
+ self.client = httpx.Client(base_url=self.base_url)
49
+ logger.info(f"SyncClient initialized with base_url: {self.base_url}")
50
+
51
+ def __enter__(self):
52
+ return self
53
+
54
+ def __exit__(self, exc_type, exc_val, exc_tb):
55
+ self.client.close()
56
+
57
+ def close(self):
58
+ """Close the underlying httpx client."""
59
+ self.client.close()
60
+
61
+ @log_trace_on_failure
62
+ def create_message(
63
+ self,
64
+ model: str,
65
+ screenshot: str, # base64 encoded
66
+ task_description: str | None = None,
67
+ task_id: str | None = None,
68
+ instruction: str | None = None,
69
+ max_actions: int | None = 5,
70
+ last_task_id: str | None = None,
71
+ history_steps: int | None = None,
72
+ temperature: float | None = None,
73
+ api_version: str | None = None,
74
+ ) -> "LLMResponse":
75
+ """
76
+ Call the /v1/message endpoint to analyze task and screenshot
77
+
78
+ Args:
79
+ model: The model to use for task analysis
80
+ screenshot: Base64-encoded screenshot image
81
+ task_description: Description of the task (required for new sessions)
82
+ task_id: Task ID for continuing existing task
83
+ instruction: Additional instruction when continuing a session (only works with task_id)
84
+ max_actions: Maximum number of actions to return (1-20)
85
+ last_task_id: Previous task ID to retrieve history from (only works with task_id)
86
+ history_steps: Number of historical steps to include from last_task_id (default: 1, max: 10)
87
+ temperature: Sampling temperature (0.0-2.0) for LLM inference
88
+ api_version: API version header
89
+
90
+ Returns:
91
+ LLMResponse: The response from the API
92
+
93
+ Raises:
94
+ httpx.HTTPStatusError: For HTTP error responses
95
+ """
96
+ headers = self._build_headers(api_version)
97
+ payload = self._build_payload(
98
+ model=model,
99
+ screenshot=screenshot,
100
+ task_description=task_description,
101
+ task_id=task_id,
102
+ instruction=instruction,
103
+ max_actions=max_actions,
104
+ last_task_id=last_task_id,
105
+ history_steps=history_steps,
106
+ temperature=temperature,
107
+ )
108
+
109
+ self._log_request_info(model, task_description, task_id)
110
+
111
+ try:
112
+ response = self.client.post(
113
+ "/v1/message", json=payload, headers=headers, timeout=self.timeout
114
+ )
115
+ except httpx.TimeoutException as e:
116
+ logger.error(f"Request timed out after {self.timeout} seconds")
117
+ raise RequestTimeoutError(
118
+ f"Request timed out after {self.timeout} seconds", e
119
+ )
120
+ except httpx.NetworkError as e:
121
+ logger.error(f"Network error: {e}")
122
+ raise NetworkError(f"Network error: {e}", e)
123
+
124
+ return self._process_response(response)
125
+
126
+ def health_check(self) -> dict:
127
+ """
128
+ Call the /health endpoint for health check
129
+
130
+ Returns:
131
+ dict: Health check response
132
+ """
133
+ logger.debug("Making health check request")
134
+ try:
135
+ response = self.client.get("/health")
136
+ response.raise_for_status()
137
+ result = response.json()
138
+ logger.debug("Health check successful")
139
+ return result
140
+ except httpx.HTTPStatusError as e:
141
+ logger.warning(f"Health check failed: {e}")
142
+ raise
@@ -19,6 +19,7 @@ def single_step(
19
19
  instruction: str | None = None,
20
20
  api_key: str | None = None,
21
21
  base_url: str | None = None,
22
+ temperature: float | None = None,
22
23
  ) -> Step:
23
24
  """
24
25
  Perform a single-step inference without maintaining task state.
@@ -32,6 +33,7 @@ def single_step(
32
33
  instruction: Optional additional instruction for the task
33
34
  api_key: OAGI API key (uses environment variable if not provided)
34
35
  base_url: OAGI base URL (uses environment variable if not provided)
36
+ temperature: Sampling temperature (0.0-2.0) for LLM inference
35
37
 
36
38
  Returns:
37
39
  Step: Object containing reasoning, actions, and completion status
@@ -78,6 +80,6 @@ def single_step(
78
80
  )
79
81
 
80
82
  # Use Task to perform single step
81
- with Task(api_key=api_key, base_url=base_url) as task:
83
+ with Task(api_key=api_key, base_url=base_url, temperature=temperature) as task:
82
84
  task.init_task(task_description)
83
85
  return task.step(screenshot_bytes, instruction=instruction)
@@ -0,0 +1,14 @@
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 .async_ import AsyncTask
10
+ from .async_short import AsyncShortTask
11
+ from .short import ShortTask
12
+ from .sync import Task
13
+
14
+ __all__ = ["Task", "AsyncTask", "ShortTask", "AsyncShortTask"]