oagi 0.5.0__py3-none-any.whl → 0.6.1__py3-none-any.whl
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/__init__.py +35 -20
- oagi/async_single_step.py +7 -5
- oagi/client/__init__.py +12 -0
- oagi/client/async_.py +137 -0
- oagi/client/base.py +183 -0
- oagi/client/sync.py +142 -0
- oagi/single_step.py +6 -2
- oagi/task/__init__.py +14 -0
- oagi/{async_task.py → task/async_.py} +23 -50
- oagi/{async_short_task.py → task/async_short.py} +18 -17
- oagi/task/base.py +130 -0
- oagi/{short_task.py → task/short.py} +18 -13
- oagi/{task.py → task/sync.py} +25 -49
- oagi/types/models/__init__.py +11 -1
- oagi/types/models/client.py +45 -0
- {oagi-0.5.0.dist-info → oagi-0.6.1.dist-info}/METADATA +1 -1
- oagi-0.6.1.dist-info/RECORD +35 -0
- oagi/async_client.py +0 -247
- oagi/sync_client.py +0 -297
- oagi-0.5.0.dist-info/RECORD +0 -30
- {oagi-0.5.0.dist-info → oagi-0.6.1.dist-info}/WHEEL +0 -0
- {oagi-0.5.0.dist-info → oagi-0.6.1.dist-info}/licenses/LICENSE +0 -0
oagi/__init__.py
CHANGED
|
@@ -5,13 +5,10 @@
|
|
|
5
5
|
# This file is part of the official API project.
|
|
6
6
|
# Licensed under the MIT License.
|
|
7
7
|
# -----------------------------------------------------------------------------
|
|
8
|
+
import importlib
|
|
8
9
|
|
|
9
|
-
from oagi.async_client import AsyncClient
|
|
10
|
-
from oagi.async_pyautogui_action_handler import AsyncPyautoguiActionHandler
|
|
11
|
-
from oagi.async_screenshot_maker import AsyncScreenshotMaker
|
|
12
|
-
from oagi.async_short_task import AsyncShortTask
|
|
13
10
|
from oagi.async_single_step import async_single_step
|
|
14
|
-
from oagi.
|
|
11
|
+
from oagi.client import AsyncClient, SyncClient
|
|
15
12
|
from oagi.exceptions import (
|
|
16
13
|
APIError,
|
|
17
14
|
AuthenticationError,
|
|
@@ -24,18 +21,35 @@ from oagi.exceptions import (
|
|
|
24
21
|
ServerError,
|
|
25
22
|
ValidationError,
|
|
26
23
|
)
|
|
27
|
-
from oagi.pil_image import PILImage
|
|
28
|
-
from oagi.pyautogui_action_handler import PyautoguiActionHandler, PyautoguiConfig
|
|
29
|
-
from oagi.screenshot_maker import ScreenshotMaker
|
|
30
|
-
from oagi.short_task import ShortTask
|
|
31
24
|
from oagi.single_step import single_step
|
|
32
|
-
from oagi.
|
|
33
|
-
from oagi.task import Task
|
|
25
|
+
from oagi.task import AsyncShortTask, AsyncTask, ShortTask, Task
|
|
34
26
|
from oagi.types import (
|
|
35
27
|
AsyncActionHandler,
|
|
36
28
|
AsyncImageProvider,
|
|
37
29
|
ImageConfig,
|
|
38
30
|
)
|
|
31
|
+
from oagi.types.models import ErrorDetail, ErrorResponse, LLMResponse
|
|
32
|
+
|
|
33
|
+
# Lazy imports for pyautogui-dependent modules
|
|
34
|
+
# These will only be imported when actually accessed
|
|
35
|
+
_LAZY_IMPORTS = {
|
|
36
|
+
"AsyncPyautoguiActionHandler": "oagi.async_pyautogui_action_handler",
|
|
37
|
+
"AsyncScreenshotMaker": "oagi.async_screenshot_maker",
|
|
38
|
+
"PILImage": "oagi.pil_image",
|
|
39
|
+
"PyautoguiActionHandler": "oagi.pyautogui_action_handler",
|
|
40
|
+
"PyautoguiConfig": "oagi.pyautogui_action_handler",
|
|
41
|
+
"ScreenshotMaker": "oagi.screenshot_maker",
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def __getattr__(name: str):
|
|
46
|
+
"""Lazy import for pyautogui-dependent modules."""
|
|
47
|
+
if name in _LAZY_IMPORTS:
|
|
48
|
+
module_name = _LAZY_IMPORTS[name]
|
|
49
|
+
module = importlib.import_module(module_name)
|
|
50
|
+
return getattr(module, name)
|
|
51
|
+
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
|
|
52
|
+
|
|
39
53
|
|
|
40
54
|
__all__ = [
|
|
41
55
|
# Core sync classes
|
|
@@ -49,15 +63,6 @@ __all__ = [
|
|
|
49
63
|
# Functions
|
|
50
64
|
"single_step",
|
|
51
65
|
"async_single_step",
|
|
52
|
-
# Image classes
|
|
53
|
-
"PILImage",
|
|
54
|
-
# Handler classes
|
|
55
|
-
"PyautoguiActionHandler",
|
|
56
|
-
"PyautoguiConfig",
|
|
57
|
-
"ScreenshotMaker",
|
|
58
|
-
# Async handler classes
|
|
59
|
-
"AsyncPyautoguiActionHandler",
|
|
60
|
-
"AsyncScreenshotMaker",
|
|
61
66
|
# Async protocols
|
|
62
67
|
"AsyncActionHandler",
|
|
63
68
|
"AsyncImageProvider",
|
|
@@ -78,4 +83,14 @@ __all__ = [
|
|
|
78
83
|
"ServerError",
|
|
79
84
|
"RequestTimeoutError",
|
|
80
85
|
"ValidationError",
|
|
86
|
+
# Lazy imports
|
|
87
|
+
# Image classes
|
|
88
|
+
"PILImage",
|
|
89
|
+
# Handler classes
|
|
90
|
+
"PyautoguiActionHandler",
|
|
91
|
+
"PyautoguiConfig",
|
|
92
|
+
"ScreenshotMaker",
|
|
93
|
+
# Async handler classes
|
|
94
|
+
"AsyncPyautoguiActionHandler",
|
|
95
|
+
"AsyncScreenshotMaker",
|
|
81
96
|
]
|
oagi/async_single_step.py
CHANGED
|
@@ -8,8 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
|
|
11
|
-
from .
|
|
12
|
-
from .pil_image import PILImage
|
|
11
|
+
from .task import AsyncTask
|
|
13
12
|
from .types import Image, Step
|
|
14
13
|
|
|
15
14
|
|
|
@@ -19,6 +18,7 @@ async def async_single_step(
|
|
|
19
18
|
instruction: str | None = None,
|
|
20
19
|
api_key: str | None = None,
|
|
21
20
|
base_url: str | None = None,
|
|
21
|
+
temperature: float | None = None,
|
|
22
22
|
) -> Step:
|
|
23
23
|
"""
|
|
24
24
|
Perform a single-step inference asynchronously without maintaining task state.
|
|
@@ -32,6 +32,7 @@ async def async_single_step(
|
|
|
32
32
|
instruction: Optional additional instruction for the task
|
|
33
33
|
api_key: OAGI API key (uses environment variable if not provided)
|
|
34
34
|
base_url: OAGI base URL (uses environment variable if not provided)
|
|
35
|
+
temperature: Sampling temperature (0.0-2.0) for LLM inference
|
|
35
36
|
|
|
36
37
|
Returns:
|
|
37
38
|
Step: Object containing reasoning, actions, and completion status
|
|
@@ -62,16 +63,17 @@ async def async_single_step(
|
|
|
62
63
|
... screenshot=image
|
|
63
64
|
... )
|
|
64
65
|
"""
|
|
66
|
+
# Lazy import PILImage only when needed
|
|
67
|
+
from .pil_image import PILImage # noqa: PLC0415
|
|
68
|
+
|
|
65
69
|
# Handle different screenshot input types
|
|
66
70
|
if isinstance(screenshot, (str, Path)):
|
|
67
|
-
# Convert file path to PILImage
|
|
68
71
|
screenshot = PILImage.from_file(str(screenshot))
|
|
69
72
|
elif isinstance(screenshot, bytes):
|
|
70
|
-
# Convert bytes to PILImage
|
|
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
|
oagi/client/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
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 AsyncClient
|
|
10
|
+
from .sync import SyncClient
|
|
11
|
+
|
|
12
|
+
__all__ = ["SyncClient", "AsyncClient"]
|
oagi/client/async_.py
ADDED
|
@@ -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
|
oagi/client/base.py
ADDED
|
@@ -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
|
oagi/client/sync.py
ADDED
|
@@ -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
|
oagi/single_step.py
CHANGED
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
|
|
11
|
-
from .pil_image import PILImage
|
|
12
11
|
from .task import Task
|
|
13
12
|
from .types import Image, Step
|
|
14
13
|
|
|
@@ -19,6 +18,7 @@ def single_step(
|
|
|
19
18
|
instruction: str | None = None,
|
|
20
19
|
api_key: str | None = None,
|
|
21
20
|
base_url: str | None = None,
|
|
21
|
+
temperature: float | None = None,
|
|
22
22
|
) -> Step:
|
|
23
23
|
"""
|
|
24
24
|
Perform a single-step inference without maintaining task state.
|
|
@@ -32,6 +32,7 @@ def single_step(
|
|
|
32
32
|
instruction: Optional additional instruction for the task
|
|
33
33
|
api_key: OAGI API key (uses environment variable if not provided)
|
|
34
34
|
base_url: OAGI base URL (uses environment variable if not provided)
|
|
35
|
+
temperature: Sampling temperature (0.0-2.0) for LLM inference
|
|
35
36
|
|
|
36
37
|
Returns:
|
|
37
38
|
Step: Object containing reasoning, actions, and completion status
|
|
@@ -60,6 +61,9 @@ def single_step(
|
|
|
60
61
|
... screenshot=image
|
|
61
62
|
... )
|
|
62
63
|
"""
|
|
64
|
+
# Lazy import PILImage only when needed
|
|
65
|
+
from .pil_image import PILImage # noqa: PLC0415
|
|
66
|
+
|
|
63
67
|
# Convert file paths to bytes using PILImage
|
|
64
68
|
if isinstance(screenshot, (str, Path)):
|
|
65
69
|
path = Path(screenshot) if isinstance(screenshot, str) else screenshot
|
|
@@ -78,6 +82,6 @@ def single_step(
|
|
|
78
82
|
)
|
|
79
83
|
|
|
80
84
|
# Use Task to perform single step
|
|
81
|
-
with Task(api_key=api_key, base_url=base_url) as task:
|
|
85
|
+
with Task(api_key=api_key, base_url=base_url, temperature=temperature) as task:
|
|
82
86
|
task.init_task(task_description)
|
|
83
87
|
return task.step(screenshot_bytes, instruction=instruction)
|
oagi/task/__init__.py
ADDED
|
@@ -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"]
|