oagi 0.1.0__py3-none-any.whl → 0.2.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 +41 -1
- oagi/exceptions.py +75 -0
- oagi/short_task.py +4 -82
- oagi/single_step.py +82 -0
- oagi/sync_client.py +140 -34
- oagi/task.py +109 -0
- oagi/types/image.py +2 -1
- {oagi-0.1.0.dist-info → oagi-0.2.1.dist-info}/METADATA +21 -1
- {oagi-0.1.0.dist-info → oagi-0.2.1.dist-info}/RECORD +11 -8
- {oagi-0.1.0.dist-info → oagi-0.2.1.dist-info}/WHEEL +0 -0
- {oagi-0.1.0.dist-info → oagi-0.2.1.dist-info}/licenses/LICENSE +0 -0
oagi/__init__.py
CHANGED
|
@@ -6,8 +6,48 @@
|
|
|
6
6
|
# Licensed under the MIT License.
|
|
7
7
|
# -----------------------------------------------------------------------------
|
|
8
8
|
|
|
9
|
+
from oagi.exceptions import (
|
|
10
|
+
APIError,
|
|
11
|
+
AuthenticationError,
|
|
12
|
+
ConfigurationError,
|
|
13
|
+
NetworkError,
|
|
14
|
+
NotFoundError,
|
|
15
|
+
OAGIError,
|
|
16
|
+
RateLimitError,
|
|
17
|
+
RequestTimeoutError,
|
|
18
|
+
ServerError,
|
|
19
|
+
ValidationError,
|
|
20
|
+
)
|
|
9
21
|
from oagi.pyautogui_action_handler import PyautoguiActionHandler
|
|
10
22
|
from oagi.screenshot_maker import ScreenshotMaker
|
|
11
23
|
from oagi.short_task import ShortTask
|
|
24
|
+
from oagi.single_step import single_step
|
|
25
|
+
from oagi.sync_client import ErrorDetail, ErrorResponse, LLMResponse, SyncClient
|
|
26
|
+
from oagi.task import Task
|
|
12
27
|
|
|
13
|
-
__all__ = [
|
|
28
|
+
__all__ = [
|
|
29
|
+
# Core classes
|
|
30
|
+
"Task",
|
|
31
|
+
"ShortTask",
|
|
32
|
+
"SyncClient",
|
|
33
|
+
# Functions
|
|
34
|
+
"single_step",
|
|
35
|
+
# Handler classes
|
|
36
|
+
"PyautoguiActionHandler",
|
|
37
|
+
"ScreenshotMaker",
|
|
38
|
+
# Response models
|
|
39
|
+
"LLMResponse",
|
|
40
|
+
"ErrorResponse",
|
|
41
|
+
"ErrorDetail",
|
|
42
|
+
# Exceptions
|
|
43
|
+
"OAGIError",
|
|
44
|
+
"APIError",
|
|
45
|
+
"AuthenticationError",
|
|
46
|
+
"ConfigurationError",
|
|
47
|
+
"NetworkError",
|
|
48
|
+
"NotFoundError",
|
|
49
|
+
"RateLimitError",
|
|
50
|
+
"ServerError",
|
|
51
|
+
"RequestTimeoutError",
|
|
52
|
+
"ValidationError",
|
|
53
|
+
]
|
oagi/exceptions.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
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 httpx
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class OAGIError(Exception):
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class APIError(OAGIError):
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
message: str,
|
|
20
|
+
code: str | None = None,
|
|
21
|
+
status_code: int | None = None,
|
|
22
|
+
response: httpx.Response | None = None,
|
|
23
|
+
):
|
|
24
|
+
"""Initialize APIError.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
message: Human-readable error message
|
|
28
|
+
code: API error code for programmatic handling
|
|
29
|
+
status_code: HTTP status code
|
|
30
|
+
response: Original HTTP response object
|
|
31
|
+
"""
|
|
32
|
+
super().__init__(message)
|
|
33
|
+
self.message = message
|
|
34
|
+
self.code = code
|
|
35
|
+
self.status_code = status_code
|
|
36
|
+
self.response = response
|
|
37
|
+
|
|
38
|
+
def __str__(self) -> str:
|
|
39
|
+
if self.code:
|
|
40
|
+
return f"API Error [{self.code}]: {self.message}"
|
|
41
|
+
return f"API Error: {self.message}"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class AuthenticationError(APIError):
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class RateLimitError(APIError):
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ValidationError(APIError):
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class NotFoundError(APIError):
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class ServerError(APIError):
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class NetworkError(OAGIError):
|
|
65
|
+
def __init__(self, message: str, original_error: Exception | None = None):
|
|
66
|
+
super().__init__(message)
|
|
67
|
+
self.original_error = original_error
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class RequestTimeoutError(NetworkError):
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class ConfigurationError(OAGIError):
|
|
75
|
+
pass
|
oagi/short_task.py
CHANGED
|
@@ -7,92 +7,14 @@
|
|
|
7
7
|
# -----------------------------------------------------------------------------
|
|
8
8
|
|
|
9
9
|
from .logging import get_logger
|
|
10
|
-
from .
|
|
11
|
-
from .types import ActionHandler,
|
|
10
|
+
from .task import Task
|
|
11
|
+
from .types import ActionHandler, ImageProvider
|
|
12
12
|
|
|
13
13
|
logger = get_logger("short_task")
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
class ShortTask:
|
|
17
|
-
|
|
18
|
-
self.client = SyncClient(base_url=base_url, api_key=api_key)
|
|
19
|
-
self.api_key = self.client.api_key
|
|
20
|
-
self.base_url = self.client.base_url
|
|
21
|
-
self.task_id: str | None = None
|
|
22
|
-
self.task_description: str | None = None
|
|
23
|
-
self.model = "vision-model-v1" # default model
|
|
24
|
-
|
|
25
|
-
def init_task(self, task_desc: str, max_steps: int = 5):
|
|
26
|
-
"""Initialize a new task with the given description."""
|
|
27
|
-
self.task_description = task_desc
|
|
28
|
-
response = self.client.create_message(
|
|
29
|
-
model=self.model,
|
|
30
|
-
screenshot="",
|
|
31
|
-
task_description=self.task_description,
|
|
32
|
-
task_id=None,
|
|
33
|
-
)
|
|
34
|
-
self.task_id = response.task_id # Reset task_id for new task
|
|
35
|
-
logger.info(f"Task initialized: '{task_desc}' (max_steps: {max_steps})")
|
|
36
|
-
|
|
37
|
-
def step(self, screenshot: Image) -> Step:
|
|
38
|
-
"""Send screenshot to the server and get the next actions."""
|
|
39
|
-
if not self.task_description:
|
|
40
|
-
raise ValueError("Task description must be set. Call init_task() first.")
|
|
41
|
-
|
|
42
|
-
logger.debug(f"Executing step for task: '{self.task_description}'")
|
|
43
|
-
|
|
44
|
-
try:
|
|
45
|
-
# Convert Image to bytes using the protocol
|
|
46
|
-
screenshot_bytes = screenshot.read()
|
|
47
|
-
screenshot_b64 = encode_screenshot_from_bytes(screenshot_bytes)
|
|
48
|
-
|
|
49
|
-
# Call API
|
|
50
|
-
response = self.client.create_message(
|
|
51
|
-
model=self.model,
|
|
52
|
-
screenshot=screenshot_b64,
|
|
53
|
-
task_description=self.task_description,
|
|
54
|
-
task_id=self.task_id,
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
-
# Update task_id from response
|
|
58
|
-
if self.task_id != response.task_id:
|
|
59
|
-
if self.task_id is None:
|
|
60
|
-
logger.debug(f"Task ID assigned: {response.task_id}")
|
|
61
|
-
else:
|
|
62
|
-
logger.debug(
|
|
63
|
-
f"Task ID changed: {self.task_id} -> {response.task_id}"
|
|
64
|
-
)
|
|
65
|
-
self.task_id = response.task_id
|
|
66
|
-
|
|
67
|
-
# Convert API response to Step
|
|
68
|
-
result = Step(
|
|
69
|
-
reason=response.reason,
|
|
70
|
-
actions=response.actions,
|
|
71
|
-
stop=response.is_complete,
|
|
72
|
-
)
|
|
73
|
-
|
|
74
|
-
if response.is_complete:
|
|
75
|
-
logger.info(f"Task completed after {response.current_step} steps")
|
|
76
|
-
else:
|
|
77
|
-
logger.debug(
|
|
78
|
-
f"Step {response.current_step} completed with {len(response.actions)} actions"
|
|
79
|
-
)
|
|
80
|
-
|
|
81
|
-
return result
|
|
82
|
-
|
|
83
|
-
except Exception as e:
|
|
84
|
-
logger.error(f"Error during step execution: {e}")
|
|
85
|
-
raise
|
|
86
|
-
|
|
87
|
-
def close(self):
|
|
88
|
-
"""Close the underlying HTTP client to free resources."""
|
|
89
|
-
self.client.close()
|
|
90
|
-
|
|
91
|
-
def __enter__(self):
|
|
92
|
-
return self
|
|
93
|
-
|
|
94
|
-
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
95
|
-
self.close()
|
|
16
|
+
class ShortTask(Task):
|
|
17
|
+
"""Task implementation with automatic mode for short-duration tasks."""
|
|
96
18
|
|
|
97
19
|
def auto_mode(
|
|
98
20
|
self,
|
oagi/single_step.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
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 pathlib import Path
|
|
10
|
+
|
|
11
|
+
from .task import Task
|
|
12
|
+
from .types import Image, Step
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def single_step(
|
|
16
|
+
task_description: str,
|
|
17
|
+
screenshot: str | bytes | Path | Image,
|
|
18
|
+
instruction: str | None = None,
|
|
19
|
+
api_key: str | None = None,
|
|
20
|
+
base_url: str | None = None,
|
|
21
|
+
) -> Step:
|
|
22
|
+
"""
|
|
23
|
+
Perform a single-step inference without maintaining task state.
|
|
24
|
+
|
|
25
|
+
This is useful for one-off analyses where you don't need to maintain
|
|
26
|
+
a conversation or task context across multiple steps.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
task_description: Description of the task to perform
|
|
30
|
+
screenshot: Screenshot as Image, bytes, or file path
|
|
31
|
+
instruction: Optional additional instruction for the task
|
|
32
|
+
api_key: OAGI API key (uses environment variable if not provided)
|
|
33
|
+
base_url: OAGI base URL (uses environment variable if not provided)
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Step: Object containing reasoning, actions, and completion status
|
|
37
|
+
|
|
38
|
+
Example:
|
|
39
|
+
>>> # Using with bytes
|
|
40
|
+
>>> with open("screenshot.png", "rb") as f:
|
|
41
|
+
... image_bytes = f.read()
|
|
42
|
+
>>> step = single_step(
|
|
43
|
+
... task_description="Click the submit button",
|
|
44
|
+
... screenshot=image_bytes
|
|
45
|
+
... )
|
|
46
|
+
|
|
47
|
+
>>> # Using with file path
|
|
48
|
+
>>> step = single_step(
|
|
49
|
+
... task_description="Fill in the form",
|
|
50
|
+
... screenshot=Path("screenshot.png"),
|
|
51
|
+
... instruction="Use test@example.com for email"
|
|
52
|
+
... )
|
|
53
|
+
|
|
54
|
+
>>> # Using with Image object
|
|
55
|
+
>>> from oagi.types import Image
|
|
56
|
+
>>> image = Image(...)
|
|
57
|
+
>>> step = single_step(
|
|
58
|
+
... task_description="Navigate to settings",
|
|
59
|
+
... screenshot=image
|
|
60
|
+
... )
|
|
61
|
+
"""
|
|
62
|
+
# Convert file paths to bytes
|
|
63
|
+
if isinstance(screenshot, (str, Path)):
|
|
64
|
+
path = Path(screenshot) if isinstance(screenshot, str) else screenshot
|
|
65
|
+
if path.exists():
|
|
66
|
+
with open(path, "rb") as f:
|
|
67
|
+
screenshot_bytes = f.read()
|
|
68
|
+
else:
|
|
69
|
+
raise FileNotFoundError(f"Screenshot file not found: {path}")
|
|
70
|
+
elif isinstance(screenshot, bytes):
|
|
71
|
+
screenshot_bytes = screenshot
|
|
72
|
+
elif isinstance(screenshot, Image):
|
|
73
|
+
screenshot_bytes = screenshot.read()
|
|
74
|
+
else:
|
|
75
|
+
raise ValueError(
|
|
76
|
+
f"screenshot must be Image, bytes, str, or Path, got {type(screenshot)}"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Use Task to perform single step
|
|
80
|
+
with Task(api_key=api_key, base_url=base_url) as task:
|
|
81
|
+
task.init_task(task_description)
|
|
82
|
+
return task.step(screenshot_bytes, instruction=instruction)
|
oagi/sync_client.py
CHANGED
|
@@ -8,11 +8,23 @@
|
|
|
8
8
|
|
|
9
9
|
import base64
|
|
10
10
|
import os
|
|
11
|
-
from
|
|
11
|
+
from functools import wraps
|
|
12
12
|
|
|
13
13
|
import httpx
|
|
14
|
+
from httpx import Response
|
|
14
15
|
from pydantic import BaseModel
|
|
15
16
|
|
|
17
|
+
from .exceptions import (
|
|
18
|
+
APIError,
|
|
19
|
+
AuthenticationError,
|
|
20
|
+
ConfigurationError,
|
|
21
|
+
NetworkError,
|
|
22
|
+
NotFoundError,
|
|
23
|
+
RateLimitError,
|
|
24
|
+
RequestTimeoutError,
|
|
25
|
+
ServerError,
|
|
26
|
+
ValidationError,
|
|
27
|
+
)
|
|
16
28
|
from .logging import get_logger
|
|
17
29
|
from .types import Action
|
|
18
30
|
|
|
@@ -25,6 +37,19 @@ class Usage(BaseModel):
|
|
|
25
37
|
total_tokens: int
|
|
26
38
|
|
|
27
39
|
|
|
40
|
+
class ErrorDetail(BaseModel):
|
|
41
|
+
"""Detailed error information."""
|
|
42
|
+
|
|
43
|
+
code: str
|
|
44
|
+
message: str
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class ErrorResponse(BaseModel):
|
|
48
|
+
"""Standard error response format."""
|
|
49
|
+
|
|
50
|
+
error: ErrorDetail | None
|
|
51
|
+
|
|
52
|
+
|
|
28
53
|
class LLMResponse(BaseModel):
|
|
29
54
|
id: str
|
|
30
55
|
task_id: str
|
|
@@ -37,29 +62,45 @@ class LLMResponse(BaseModel):
|
|
|
37
62
|
actions: list[Action]
|
|
38
63
|
reason: str | None = None
|
|
39
64
|
usage: Usage
|
|
65
|
+
error: ErrorDetail | None = None
|
|
40
66
|
|
|
41
67
|
|
|
42
|
-
|
|
43
|
-
error:
|
|
44
|
-
|
|
45
|
-
|
|
68
|
+
def _log_trace_id(response: Response):
|
|
69
|
+
logger.error(f"Request Id: {response.headers.get('x-request-id', '')}")
|
|
70
|
+
logger.error(f"Trace Id: {response.headers.get('x-trace-id', '')}")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def log_trace_on_failure(func):
|
|
74
|
+
"""Decorator that logs trace ID when a method fails."""
|
|
75
|
+
|
|
76
|
+
@wraps(func)
|
|
77
|
+
def wrapper(*args, **kwargs):
|
|
78
|
+
try:
|
|
79
|
+
return func(*args, **kwargs)
|
|
80
|
+
except Exception as e:
|
|
81
|
+
# Try to get response from the exception if it has one
|
|
82
|
+
if (response := getattr(e, "response", None)) is not None:
|
|
83
|
+
_log_trace_id(response)
|
|
84
|
+
raise
|
|
85
|
+
|
|
86
|
+
return wrapper
|
|
46
87
|
|
|
47
88
|
|
|
48
89
|
class SyncClient:
|
|
49
|
-
def __init__(self, base_url:
|
|
90
|
+
def __init__(self, base_url: str | None = None, api_key: str | None = None):
|
|
50
91
|
# Get from environment if not provided
|
|
51
92
|
self.base_url = base_url or os.getenv("OAGI_BASE_URL")
|
|
52
93
|
self.api_key = api_key or os.getenv("OAGI_API_KEY")
|
|
53
94
|
|
|
54
95
|
# Validate required configuration
|
|
55
96
|
if not self.base_url:
|
|
56
|
-
raise
|
|
97
|
+
raise ConfigurationError(
|
|
57
98
|
"OAGI base URL must be provided either as 'base_url' parameter or "
|
|
58
99
|
"OAGI_BASE_URL environment variable"
|
|
59
100
|
)
|
|
60
101
|
|
|
61
102
|
if not self.api_key:
|
|
62
|
-
raise
|
|
103
|
+
raise ConfigurationError(
|
|
63
104
|
"OAGI API key must be provided either as 'api_key' parameter or "
|
|
64
105
|
"OAGI_API_KEY environment variable"
|
|
65
106
|
)
|
|
@@ -80,14 +121,16 @@ class SyncClient:
|
|
|
80
121
|
"""Close the underlying httpx client"""
|
|
81
122
|
self.client.close()
|
|
82
123
|
|
|
124
|
+
@log_trace_on_failure
|
|
83
125
|
def create_message(
|
|
84
126
|
self,
|
|
85
127
|
model: str,
|
|
86
128
|
screenshot: str, # base64 encoded
|
|
87
|
-
task_description:
|
|
88
|
-
task_id:
|
|
89
|
-
|
|
90
|
-
|
|
129
|
+
task_description: str | None = None,
|
|
130
|
+
task_id: str | None = None,
|
|
131
|
+
instruction: str | None = None,
|
|
132
|
+
max_actions: int | None = 5,
|
|
133
|
+
api_version: str | None = None,
|
|
91
134
|
) -> LLMResponse:
|
|
92
135
|
"""
|
|
93
136
|
Call the /v1/message endpoint to analyze task and screenshot
|
|
@@ -97,6 +140,7 @@ class SyncClient:
|
|
|
97
140
|
screenshot: Base64-encoded screenshot image
|
|
98
141
|
task_description: Description of the task (required for new sessions)
|
|
99
142
|
task_id: Task ID for continuing existing task
|
|
143
|
+
instruction: Additional instruction when continuing a session (only works with task_id)
|
|
100
144
|
max_actions: Maximum number of actions to return (1-20)
|
|
101
145
|
api_version: API version header
|
|
102
146
|
|
|
@@ -118,6 +162,8 @@ class SyncClient:
|
|
|
118
162
|
payload["task_description"] = task_description
|
|
119
163
|
if task_id is not None:
|
|
120
164
|
payload["task_id"] = task_id
|
|
165
|
+
if instruction is not None:
|
|
166
|
+
payload["instruction"] = instruction
|
|
121
167
|
if max_actions is not None:
|
|
122
168
|
payload["max_actions"] = max_actions
|
|
123
169
|
|
|
@@ -126,32 +172,92 @@ class SyncClient:
|
|
|
126
172
|
f"Request includes task_description: {task_description is not None}, task_id: {task_id is not None}"
|
|
127
173
|
)
|
|
128
174
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
175
|
+
try:
|
|
176
|
+
response = self.client.post(
|
|
177
|
+
"/v1/message", json=payload, headers=headers, timeout=self.timeout
|
|
178
|
+
)
|
|
179
|
+
except httpx.TimeoutException as e:
|
|
180
|
+
logger.error(f"Request timed out after {self.timeout} seconds")
|
|
181
|
+
raise RequestTimeoutError(
|
|
182
|
+
f"Request timed out after {self.timeout} seconds", e
|
|
183
|
+
)
|
|
184
|
+
except httpx.NetworkError as e:
|
|
185
|
+
logger.error(f"Network error: {e}")
|
|
186
|
+
raise NetworkError(f"Network error: {e}", e)
|
|
132
187
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
188
|
+
try:
|
|
189
|
+
response_data = response.json()
|
|
190
|
+
except ValueError:
|
|
191
|
+
# If response is not JSON, raise API error
|
|
192
|
+
logger.error(f"Non-JSON API response: {response.status_code}")
|
|
193
|
+
raise APIError(
|
|
194
|
+
f"Invalid response format (status {response.status_code})",
|
|
195
|
+
status_code=response.status_code,
|
|
196
|
+
response=response,
|
|
137
197
|
)
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
logger.error(f"API Error {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
198
|
+
|
|
199
|
+
# Check if it's an error response (non-200 status or has error field)
|
|
200
|
+
if response.status_code != 200:
|
|
201
|
+
error_resp = ErrorResponse(**response_data)
|
|
202
|
+
if error_resp.error:
|
|
203
|
+
error_code = error_resp.error.code
|
|
204
|
+
error_msg = error_resp.error.message
|
|
205
|
+
logger.error(f"API Error [{error_code}]: {error_msg}")
|
|
206
|
+
|
|
207
|
+
# Map to specific exception types based on status code
|
|
208
|
+
exception_class = self._get_exception_class(response.status_code)
|
|
209
|
+
raise exception_class(
|
|
210
|
+
error_msg,
|
|
211
|
+
code=error_code,
|
|
212
|
+
status_code=response.status_code,
|
|
213
|
+
response=response,
|
|
214
|
+
)
|
|
215
|
+
else:
|
|
216
|
+
# Error response without error details
|
|
217
|
+
logger.error(
|
|
218
|
+
f"API error response without details: {response.status_code}"
|
|
219
|
+
)
|
|
220
|
+
exception_class = self._get_exception_class(response.status_code)
|
|
221
|
+
raise exception_class(
|
|
222
|
+
f"API error (status {response.status_code})",
|
|
223
|
+
status_code=response.status_code,
|
|
149
224
|
response=response,
|
|
150
225
|
)
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
226
|
+
|
|
227
|
+
# Parse successful response
|
|
228
|
+
result = LLMResponse(**response_data)
|
|
229
|
+
|
|
230
|
+
# Check if the response contains an error (even with 200 status)
|
|
231
|
+
if result.error:
|
|
232
|
+
logger.error(
|
|
233
|
+
f"API Error in response: [{result.error.code}]: {result.error.message}"
|
|
234
|
+
)
|
|
235
|
+
raise APIError(
|
|
236
|
+
result.error.message,
|
|
237
|
+
code=result.error.code,
|
|
238
|
+
status_code=200,
|
|
239
|
+
response=response,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
logger.info(
|
|
243
|
+
f"API request successful - task_id: {result.task_id}, step: {result.current_step}, complete: {result.is_complete}"
|
|
244
|
+
)
|
|
245
|
+
logger.debug(f"Response included {len(result.actions)} actions")
|
|
246
|
+
return result
|
|
247
|
+
|
|
248
|
+
def _get_exception_class(self, status_code: int) -> type[APIError]:
|
|
249
|
+
"""Get the appropriate exception class based on status code."""
|
|
250
|
+
status_map = {
|
|
251
|
+
401: AuthenticationError,
|
|
252
|
+
404: NotFoundError,
|
|
253
|
+
422: ValidationError,
|
|
254
|
+
429: RateLimitError,
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if status_code >= 500:
|
|
258
|
+
return ServerError
|
|
259
|
+
|
|
260
|
+
return status_map.get(status_code, APIError)
|
|
155
261
|
|
|
156
262
|
def health_check(self) -> dict:
|
|
157
263
|
"""
|
oagi/task.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
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 .logging import get_logger
|
|
10
|
+
from .sync_client import SyncClient, encode_screenshot_from_bytes
|
|
11
|
+
from .types import Image, Step
|
|
12
|
+
|
|
13
|
+
logger = get_logger("task")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Task:
|
|
17
|
+
"""Base class for task automation with the OAGI API."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, api_key: str | None = None, base_url: str | None = None):
|
|
20
|
+
self.client = SyncClient(base_url=base_url, api_key=api_key)
|
|
21
|
+
self.api_key = self.client.api_key
|
|
22
|
+
self.base_url = self.client.base_url
|
|
23
|
+
self.task_id: str | None = None
|
|
24
|
+
self.task_description: str | None = None
|
|
25
|
+
self.model = "vision-model-v1" # default model
|
|
26
|
+
|
|
27
|
+
def init_task(self, task_desc: str, max_steps: int = 5):
|
|
28
|
+
"""Initialize a new task with the given description."""
|
|
29
|
+
self.task_description = task_desc
|
|
30
|
+
response = self.client.create_message(
|
|
31
|
+
model=self.model,
|
|
32
|
+
screenshot="",
|
|
33
|
+
task_description=self.task_description,
|
|
34
|
+
task_id=None,
|
|
35
|
+
)
|
|
36
|
+
self.task_id = response.task_id # Reset task_id for new task
|
|
37
|
+
logger.info(f"Task initialized: '{task_desc}' (max_steps: {max_steps})")
|
|
38
|
+
|
|
39
|
+
def step(self, screenshot: Image | bytes, instruction: str | None = None) -> Step:
|
|
40
|
+
"""Send screenshot to the server and get the next actions.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
screenshot: Screenshot as Image object or raw bytes
|
|
44
|
+
instruction: Optional additional instruction for this step (only works with existing task_id)
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Step: The actions and reasoning for this step
|
|
48
|
+
"""
|
|
49
|
+
if not self.task_description:
|
|
50
|
+
raise ValueError("Task description must be set. Call init_task() first.")
|
|
51
|
+
|
|
52
|
+
logger.debug(f"Executing step for task: '{self.task_description}'")
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
# Convert Image to bytes using the protocol
|
|
56
|
+
if isinstance(screenshot, Image):
|
|
57
|
+
screenshot_bytes = screenshot.read()
|
|
58
|
+
else:
|
|
59
|
+
screenshot_bytes = screenshot
|
|
60
|
+
screenshot_b64 = encode_screenshot_from_bytes(screenshot_bytes)
|
|
61
|
+
|
|
62
|
+
# Call API
|
|
63
|
+
response = self.client.create_message(
|
|
64
|
+
model=self.model,
|
|
65
|
+
screenshot=screenshot_b64,
|
|
66
|
+
task_description=self.task_description,
|
|
67
|
+
task_id=self.task_id,
|
|
68
|
+
instruction=instruction,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Update task_id from response
|
|
72
|
+
if self.task_id != response.task_id:
|
|
73
|
+
if self.task_id is None:
|
|
74
|
+
logger.debug(f"Task ID assigned: {response.task_id}")
|
|
75
|
+
else:
|
|
76
|
+
logger.debug(
|
|
77
|
+
f"Task ID changed: {self.task_id} -> {response.task_id}"
|
|
78
|
+
)
|
|
79
|
+
self.task_id = response.task_id
|
|
80
|
+
|
|
81
|
+
# Convert API response to Step
|
|
82
|
+
result = Step(
|
|
83
|
+
reason=response.reason,
|
|
84
|
+
actions=response.actions,
|
|
85
|
+
stop=response.is_complete,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
if response.is_complete:
|
|
89
|
+
logger.info(f"Task completed after {response.current_step} steps")
|
|
90
|
+
else:
|
|
91
|
+
logger.debug(
|
|
92
|
+
f"Step {response.current_step} completed with {len(response.actions)} actions"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
return result
|
|
96
|
+
|
|
97
|
+
except Exception as e:
|
|
98
|
+
logger.error(f"Error during step execution: {e}")
|
|
99
|
+
raise
|
|
100
|
+
|
|
101
|
+
def close(self):
|
|
102
|
+
"""Close the underlying HTTP client to free resources."""
|
|
103
|
+
self.client.close()
|
|
104
|
+
|
|
105
|
+
def __enter__(self):
|
|
106
|
+
return self
|
|
107
|
+
|
|
108
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
109
|
+
self.close()
|
oagi/types/image.py
CHANGED
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
# Licensed under the MIT License.
|
|
7
7
|
# -----------------------------------------------------------------------------
|
|
8
8
|
|
|
9
|
-
from typing import Protocol
|
|
9
|
+
from typing import Protocol, runtime_checkable
|
|
10
10
|
|
|
11
11
|
|
|
12
|
+
@runtime_checkable
|
|
12
13
|
class Image(Protocol):
|
|
13
14
|
"""Protocol for image objects that can be read as bytes."""
|
|
14
15
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: oagi
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 0.2.1
|
|
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>
|
|
@@ -33,3 +33,23 @@ Requires-Dist: pydantic>=2.0.0
|
|
|
33
33
|
Description-Content-Type: text/markdown
|
|
34
34
|
|
|
35
35
|
# OAGI Python SDK
|
|
36
|
+
|
|
37
|
+
## Basic Usage
|
|
38
|
+
```bash
|
|
39
|
+
pip install oagi # python >= 3.10
|
|
40
|
+
```
|
|
41
|
+
```bash
|
|
42
|
+
export OAGI_BASE_URL=""
|
|
43
|
+
export OAGI_API_KEY="sk-xxxx"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from oagi import PyautoguiActionHandler, ScreenshotMaker, ShortTask
|
|
48
|
+
short_task = ShortTask()
|
|
49
|
+
is_completed = short_task.auto_mode(
|
|
50
|
+
"Search weather with Google",
|
|
51
|
+
max_steps=5,
|
|
52
|
+
executor=PyautoguiActionHandler(),
|
|
53
|
+
image_provider=(sm := ScreenshotMaker()),
|
|
54
|
+
)
|
|
55
|
+
```
|
|
@@ -1,17 +1,20 @@
|
|
|
1
|
-
oagi/__init__.py,sha256=
|
|
1
|
+
oagi/__init__.py,sha256=ms9ahLdHNrMWtiqX93q8Iv55ag__tO4Id0DQ3hA2TVM,1347
|
|
2
|
+
oagi/exceptions.py,sha256=VMwVS8ouE9nHhBpN3AZMYt5_U2kGcihWaTnBhoQLquo,1662
|
|
2
3
|
oagi/logging.py,sha256=CWe89mA5MKTipIvfrqSYkv2CAFNBSwHMDQMDkG_g64g,1350
|
|
3
4
|
oagi/pyautogui_action_handler.py,sha256=LBWmtqkXzZSJo07s3uOw-NWUE9rZZtbNAx0YI83pCbk,5482
|
|
4
5
|
oagi/screenshot_maker.py,sha256=lyJSMFagHeaqg59CQGMTqLvSzQN_pBbhbV2oIFG46vA,2077
|
|
5
|
-
oagi/short_task.py,sha256=
|
|
6
|
-
oagi/
|
|
6
|
+
oagi/short_task.py,sha256=ofcMi7vbu9W1MCSGOk_FNEHJcB02pfgNcx1-Y8UkpJY,1552
|
|
7
|
+
oagi/single_step.py,sha256=JEsF7ABa4wwW5Pi5AfjeKzyuKhC4kC4fcotnmnNye5o,2874
|
|
8
|
+
oagi/sync_client.py,sha256=E6EgFIe-H91rdsPhF1puwrBTpOnKaL6JA1WHR4R-CLY,9395
|
|
9
|
+
oagi/task.py,sha256=NmpNMu8CJll50zGsGtVie1kdpKeWnAAWudEa-aasBbU,3959
|
|
7
10
|
oagi/types/__init__.py,sha256=eh-1IEqMTY2hUrvQJeTg6vsvlE6F4Iz5C0_K86AnWn8,549
|
|
8
11
|
oagi/types/action_handler.py,sha256=NH8E-m5qpGqWcXzTSWfF7W0Xdp8SkzJsbhCmQ0B96cg,1075
|
|
9
|
-
oagi/types/image.py,sha256=
|
|
12
|
+
oagi/types/image.py,sha256=KgPCCTJ6D5vHIaGZdbTE7eQEa1WlT6G9tf59ZuUCV2U,537
|
|
10
13
|
oagi/types/image_provider.py,sha256=oYFdOYznrK_VOR9egzOjw5wFM5w8EY2sY01pH0ANAgU,1112
|
|
11
14
|
oagi/types/models/__init__.py,sha256=4qhKxWXsXEVzD6U_RM6PXR45os765qigtZs1BsS4WHg,414
|
|
12
15
|
oagi/types/models/action.py,sha256=8Xd3IcH32ENq7uXczo-mbQ736yUOGxO_TaZTfHVRY7w,935
|
|
13
16
|
oagi/types/models/step.py,sha256=RSI4H_2rrUBq_xyCoWKaq7JHdJWNobtQppaKC1l0aWU,471
|
|
14
|
-
oagi-0.1.
|
|
15
|
-
oagi-0.1.
|
|
16
|
-
oagi-0.1.
|
|
17
|
-
oagi-0.1.
|
|
17
|
+
oagi-0.2.1.dist-info/METADATA,sha256=5W_aB_J2LUEyKAvE6G_iOcySv7tf0PWiGikOQe_K7l4,2066
|
|
18
|
+
oagi-0.2.1.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
|
|
19
|
+
oagi-0.2.1.dist-info/licenses/LICENSE,sha256=sy5DLA2M29jFT4UfWsuBF9BAr3FnRkYtnAu6oDZiIf8,1075
|
|
20
|
+
oagi-0.2.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|