oagi 0.1.0__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of oagi might be problematic. Click here for more details.
- oagi-0.2.0/.github/workflows/release.yml +41 -0
- {oagi-0.1.0 → oagi-0.2.0}/PKG-INFO +21 -1
- oagi-0.2.0/README.md +21 -0
- {oagi-0.1.0 → oagi-0.2.0}/examples/execute_task_manual.py +11 -5
- oagi-0.2.0/examples/single_step.py +19 -0
- {oagi-0.1.0 → oagi-0.2.0}/pyproject.toml +2 -2
- oagi-0.2.0/src/oagi/__init__.py +53 -0
- oagi-0.2.0/src/oagi/exceptions.py +75 -0
- oagi-0.2.0/src/oagi/short_task.py +44 -0
- oagi-0.2.0/src/oagi/single_step.py +82 -0
- {oagi-0.1.0 → oagi-0.2.0}/src/oagi/sync_client.py +118 -36
- oagi-0.1.0/src/oagi/short_task.py → oagi-0.2.0/src/oagi/task.py +20 -33
- {oagi-0.1.0 → oagi-0.2.0}/src/oagi/types/image.py +2 -1
- {oagi-0.1.0 → oagi-0.2.0}/tests/conftest.py +50 -3
- {oagi-0.1.0 → oagi-0.2.0}/tests/test_logging.py +5 -8
- oagi-0.2.0/tests/test_short_task.py +148 -0
- oagi-0.2.0/tests/test_single_step.py +193 -0
- {oagi-0.1.0 → oagi-0.2.0}/tests/test_sync_client.py +33 -38
- oagi-0.2.0/tests/test_task.py +275 -0
- {oagi-0.1.0 → oagi-0.2.0}/uv.lock +1 -1
- oagi-0.1.0/.claude/settings.local.json +0 -19
- oagi-0.1.0/CLAUDE.md +0 -105
- oagi-0.1.0/README.md +0 -1
- oagi-0.1.0/examples/screenshot.png +0 -0
- oagi-0.1.0/examples/test.py +0 -20
- oagi-0.1.0/examples/test_screenshot.py +0 -41
- oagi-0.1.0/src/oagi/__init__.py +0 -13
- {oagi-0.1.0 → oagi-0.2.0}/.github/workflows/ci.yml +0 -0
- {oagi-0.1.0 → oagi-0.2.0}/.gitignore +0 -0
- {oagi-0.1.0 → oagi-0.2.0}/.python-version +0 -0
- {oagi-0.1.0 → oagi-0.2.0}/CONTRIBUTING.md +0 -0
- {oagi-0.1.0 → oagi-0.2.0}/LICENSE +0 -0
- {oagi-0.1.0 → oagi-0.2.0}/Makefile +0 -0
- {oagi-0.1.0 → oagi-0.2.0}/examples/execute_task_auto.py +0 -0
- {oagi-0.1.0 → oagi-0.2.0}/examples/google_weather.py +0 -0
- {oagi-0.1.0 → oagi-0.2.0}/examples/hotel_booking.py +0 -0
- {oagi-0.1.0 → oagi-0.2.0}/src/oagi/logging.py +0 -0
- {oagi-0.1.0 → oagi-0.2.0}/src/oagi/pyautogui_action_handler.py +0 -0
- {oagi-0.1.0 → oagi-0.2.0}/src/oagi/screenshot_maker.py +0 -0
- {oagi-0.1.0 → oagi-0.2.0}/src/oagi/types/__init__.py +0 -0
- {oagi-0.1.0 → oagi-0.2.0}/src/oagi/types/action_handler.py +0 -0
- {oagi-0.1.0 → oagi-0.2.0}/src/oagi/types/image_provider.py +0 -0
- {oagi-0.1.0 → oagi-0.2.0}/src/oagi/types/models/__init__.py +0 -0
- {oagi-0.1.0 → oagi-0.2.0}/src/oagi/types/models/action.py +0 -0
- {oagi-0.1.0 → oagi-0.2.0}/src/oagi/types/models/step.py +0 -0
- {oagi-0.1.0 → oagi-0.2.0}/tests/__init__.py +0 -0
- {oagi-0.1.0 → oagi-0.2.0}/tests/test_pyautogui_action_handler.py +0 -0
- {oagi-0.1.0 → oagi-0.2.0}/tests/test_screenshot_maker.py +0 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- 'v*'
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
release:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
permissions:
|
|
12
|
+
id-token: write # For trusted publishing
|
|
13
|
+
contents: write # For GitHub release
|
|
14
|
+
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- name: Install uv
|
|
19
|
+
uses: astral-sh/setup-uv@v3
|
|
20
|
+
with:
|
|
21
|
+
enable-cache: true
|
|
22
|
+
|
|
23
|
+
- name: Set up Python
|
|
24
|
+
run: uv python install 3.12
|
|
25
|
+
|
|
26
|
+
- name: Build package
|
|
27
|
+
run: uv build
|
|
28
|
+
|
|
29
|
+
- name: Publish to PyPI
|
|
30
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
31
|
+
with:
|
|
32
|
+
password: ${{ secrets.PYPI_API_TOKEN }}
|
|
33
|
+
# Another option: Use Trusted Publishing (recommended, no token needed)
|
|
34
|
+
# Configure at: https://pypi.org/manage/project/oagi/settings/publishing/
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
- name: Create GitHub Release
|
|
38
|
+
uses: softprops/action-gh-release@v1
|
|
39
|
+
with:
|
|
40
|
+
files: dist/*
|
|
41
|
+
generate_release_notes: true
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: oagi
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.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>
|
|
@@ -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
|
+
```
|
oagi-0.2.0/README.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# OAGI Python SDK
|
|
2
|
+
|
|
3
|
+
## Basic Usage
|
|
4
|
+
```bash
|
|
5
|
+
pip install oagi # python >= 3.10
|
|
6
|
+
```
|
|
7
|
+
```bash
|
|
8
|
+
export OAGI_BASE_URL=""
|
|
9
|
+
export OAGI_API_KEY="sk-xxxx"
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
```python
|
|
13
|
+
from oagi import PyautoguiActionHandler, ScreenshotMaker, ShortTask
|
|
14
|
+
short_task = ShortTask()
|
|
15
|
+
is_completed = short_task.auto_mode(
|
|
16
|
+
"Search weather with Google",
|
|
17
|
+
max_steps=5,
|
|
18
|
+
executor=PyautoguiActionHandler(),
|
|
19
|
+
image_provider=(sm := ScreenshotMaker()),
|
|
20
|
+
)
|
|
21
|
+
```
|
|
@@ -6,23 +6,29 @@
|
|
|
6
6
|
# Licensed under the MIT License.
|
|
7
7
|
# -----------------------------------------------------------------------------
|
|
8
8
|
|
|
9
|
-
from oagi import PyautoguiActionHandler, ScreenshotMaker,
|
|
9
|
+
from oagi import PyautoguiActionHandler, ScreenshotMaker, Task
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
def execute_task_manual(task_desc, max_steps=5):
|
|
13
13
|
# set OAGI_API_KEY and OAGI_BASE_URL
|
|
14
14
|
# or ShortTask(api_key="your_api_key", base_url="your_base_url")
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
task = Task()
|
|
16
|
+
task.init_task(task_desc, max_steps=max_steps)
|
|
17
17
|
executor = (
|
|
18
18
|
PyautoguiActionHandler()
|
|
19
19
|
) # executor = lambda actions: print(actions) for debugging
|
|
20
20
|
image_provider = ScreenshotMaker()
|
|
21
21
|
|
|
22
22
|
for i in range(max_steps):
|
|
23
|
+
# image can also be bytes
|
|
24
|
+
# with open("test_screenshot.png", "rb") as f:
|
|
25
|
+
# image = f.read()
|
|
23
26
|
image = image_provider()
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
|
|
28
|
+
# For additional instructions
|
|
29
|
+
# step = task.step(image, instruction="some instruction")
|
|
30
|
+
step = task.step(image)
|
|
31
|
+
|
|
26
32
|
# do something with step, maybe print to debug
|
|
27
33
|
print(f"Step {i}: {step.reason=}")
|
|
28
34
|
|
|
@@ -0,0 +1,19 @@
|
|
|
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 single_step
|
|
10
|
+
|
|
11
|
+
step = single_step(
|
|
12
|
+
task_description="Search weather with Google",
|
|
13
|
+
screenshot="some/path/to/local/image", # bytes or Path object or Image object
|
|
14
|
+
instruction="The operating system is macos", # optional instruction
|
|
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
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
print(step)
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "oagi"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "Official API of OpenAGI Foundation"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { file = "LICENSE" }
|
|
@@ -32,4 +32,4 @@ dev = [
|
|
|
32
32
|
]
|
|
33
33
|
|
|
34
34
|
[tool.ruff.lint]
|
|
35
|
-
extend-select = ["I"]
|
|
35
|
+
extend-select = ["I", "PLC0415"]
|
|
@@ -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.exceptions import (
|
|
10
|
+
APIError,
|
|
11
|
+
AuthenticationError,
|
|
12
|
+
ConfigurationError,
|
|
13
|
+
NetworkError,
|
|
14
|
+
NotFoundError,
|
|
15
|
+
OAGIError,
|
|
16
|
+
RateLimitError,
|
|
17
|
+
RequestTimeoutError,
|
|
18
|
+
ServerError,
|
|
19
|
+
ValidationError,
|
|
20
|
+
)
|
|
21
|
+
from oagi.pyautogui_action_handler import PyautoguiActionHandler
|
|
22
|
+
from oagi.screenshot_maker import ScreenshotMaker
|
|
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
|
|
27
|
+
|
|
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
|
+
]
|
|
@@ -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
|
|
@@ -0,0 +1,44 @@
|
|
|
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 .task import Task
|
|
11
|
+
from .types import ActionHandler, ImageProvider
|
|
12
|
+
|
|
13
|
+
logger = get_logger("short_task")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ShortTask(Task):
|
|
17
|
+
"""Task implementation with automatic mode for short-duration tasks."""
|
|
18
|
+
|
|
19
|
+
def auto_mode(
|
|
20
|
+
self,
|
|
21
|
+
task_desc: str,
|
|
22
|
+
max_steps: int = 5,
|
|
23
|
+
executor: ActionHandler = None,
|
|
24
|
+
image_provider: ImageProvider = None,
|
|
25
|
+
) -> bool:
|
|
26
|
+
"""Run the task in automatic mode with the provided executor and image provider."""
|
|
27
|
+
logger.info(
|
|
28
|
+
f"Starting auto mode for task: '{task_desc}' (max_steps: {max_steps})"
|
|
29
|
+
)
|
|
30
|
+
self.init_task(task_desc, max_steps=max_steps)
|
|
31
|
+
|
|
32
|
+
for i in range(max_steps):
|
|
33
|
+
logger.debug(f"Auto mode step {i + 1}/{max_steps}")
|
|
34
|
+
image = image_provider()
|
|
35
|
+
step = self.step(image)
|
|
36
|
+
if step.stop:
|
|
37
|
+
logger.info(f"Auto mode completed successfully after {i + 1} steps")
|
|
38
|
+
return True
|
|
39
|
+
if executor:
|
|
40
|
+
logger.debug(f"Executing {len(step.actions)} actions")
|
|
41
|
+
executor(step.actions)
|
|
42
|
+
|
|
43
|
+
logger.warning(f"Auto mode reached max steps ({max_steps}) without completion")
|
|
44
|
+
return False
|
|
@@ -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)
|
|
@@ -8,11 +8,21 @@
|
|
|
8
8
|
|
|
9
9
|
import base64
|
|
10
10
|
import os
|
|
11
|
-
from typing import Optional
|
|
12
11
|
|
|
13
12
|
import httpx
|
|
14
13
|
from pydantic import BaseModel
|
|
15
14
|
|
|
15
|
+
from .exceptions import (
|
|
16
|
+
APIError,
|
|
17
|
+
AuthenticationError,
|
|
18
|
+
ConfigurationError,
|
|
19
|
+
NetworkError,
|
|
20
|
+
NotFoundError,
|
|
21
|
+
RateLimitError,
|
|
22
|
+
RequestTimeoutError,
|
|
23
|
+
ServerError,
|
|
24
|
+
ValidationError,
|
|
25
|
+
)
|
|
16
26
|
from .logging import get_logger
|
|
17
27
|
from .types import Action
|
|
18
28
|
|
|
@@ -25,6 +35,19 @@ class Usage(BaseModel):
|
|
|
25
35
|
total_tokens: int
|
|
26
36
|
|
|
27
37
|
|
|
38
|
+
class ErrorDetail(BaseModel):
|
|
39
|
+
"""Detailed error information."""
|
|
40
|
+
|
|
41
|
+
code: str
|
|
42
|
+
message: str
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ErrorResponse(BaseModel):
|
|
46
|
+
"""Standard error response format."""
|
|
47
|
+
|
|
48
|
+
error: ErrorDetail | None
|
|
49
|
+
|
|
50
|
+
|
|
28
51
|
class LLMResponse(BaseModel):
|
|
29
52
|
id: str
|
|
30
53
|
task_id: str
|
|
@@ -37,29 +60,24 @@ class LLMResponse(BaseModel):
|
|
|
37
60
|
actions: list[Action]
|
|
38
61
|
reason: str | None = None
|
|
39
62
|
usage: Usage
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
class ErrorResponse(BaseModel):
|
|
43
|
-
error: str
|
|
44
|
-
message: str
|
|
45
|
-
code: int
|
|
63
|
+
error: ErrorDetail | None = None
|
|
46
64
|
|
|
47
65
|
|
|
48
66
|
class SyncClient:
|
|
49
|
-
def __init__(self, base_url:
|
|
67
|
+
def __init__(self, base_url: str | None = None, api_key: str | None = None):
|
|
50
68
|
# Get from environment if not provided
|
|
51
69
|
self.base_url = base_url or os.getenv("OAGI_BASE_URL")
|
|
52
70
|
self.api_key = api_key or os.getenv("OAGI_API_KEY")
|
|
53
71
|
|
|
54
72
|
# Validate required configuration
|
|
55
73
|
if not self.base_url:
|
|
56
|
-
raise
|
|
74
|
+
raise ConfigurationError(
|
|
57
75
|
"OAGI base URL must be provided either as 'base_url' parameter or "
|
|
58
76
|
"OAGI_BASE_URL environment variable"
|
|
59
77
|
)
|
|
60
78
|
|
|
61
79
|
if not self.api_key:
|
|
62
|
-
raise
|
|
80
|
+
raise ConfigurationError(
|
|
63
81
|
"OAGI API key must be provided either as 'api_key' parameter or "
|
|
64
82
|
"OAGI_API_KEY environment variable"
|
|
65
83
|
)
|
|
@@ -84,10 +102,11 @@ class SyncClient:
|
|
|
84
102
|
self,
|
|
85
103
|
model: str,
|
|
86
104
|
screenshot: str, # base64 encoded
|
|
87
|
-
task_description:
|
|
88
|
-
task_id:
|
|
89
|
-
|
|
90
|
-
|
|
105
|
+
task_description: str | None = None,
|
|
106
|
+
task_id: str | None = None,
|
|
107
|
+
instruction: str | None = None,
|
|
108
|
+
max_actions: int | None = 5,
|
|
109
|
+
api_version: str | None = None,
|
|
91
110
|
) -> LLMResponse:
|
|
92
111
|
"""
|
|
93
112
|
Call the /v1/message endpoint to analyze task and screenshot
|
|
@@ -97,6 +116,7 @@ class SyncClient:
|
|
|
97
116
|
screenshot: Base64-encoded screenshot image
|
|
98
117
|
task_description: Description of the task (required for new sessions)
|
|
99
118
|
task_id: Task ID for continuing existing task
|
|
119
|
+
instruction: Additional instruction when continuing a session (only works with task_id)
|
|
100
120
|
max_actions: Maximum number of actions to return (1-20)
|
|
101
121
|
api_version: API version header
|
|
102
122
|
|
|
@@ -118,6 +138,8 @@ class SyncClient:
|
|
|
118
138
|
payload["task_description"] = task_description
|
|
119
139
|
if task_id is not None:
|
|
120
140
|
payload["task_id"] = task_id
|
|
141
|
+
if instruction is not None:
|
|
142
|
+
payload["instruction"] = instruction
|
|
121
143
|
if max_actions is not None:
|
|
122
144
|
payload["max_actions"] = max_actions
|
|
123
145
|
|
|
@@ -126,32 +148,92 @@ class SyncClient:
|
|
|
126
148
|
f"Request includes task_description: {task_description is not None}, task_id: {task_id is not None}"
|
|
127
149
|
)
|
|
128
150
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
151
|
+
try:
|
|
152
|
+
response = self.client.post(
|
|
153
|
+
"/v1/message", json=payload, headers=headers, timeout=self.timeout
|
|
154
|
+
)
|
|
155
|
+
except httpx.TimeoutException as e:
|
|
156
|
+
logger.error(f"Request timed out after {self.timeout} seconds")
|
|
157
|
+
raise RequestTimeoutError(
|
|
158
|
+
f"Request timed out after {self.timeout} seconds", e
|
|
159
|
+
)
|
|
160
|
+
except httpx.NetworkError as e:
|
|
161
|
+
logger.error(f"Network error: {e}")
|
|
162
|
+
raise NetworkError(f"Network error: {e}", e)
|
|
132
163
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
164
|
+
try:
|
|
165
|
+
response_data = response.json()
|
|
166
|
+
except ValueError:
|
|
167
|
+
# If response is not JSON, raise API error
|
|
168
|
+
logger.error(f"Non-JSON API response: {response.status_code}")
|
|
169
|
+
raise APIError(
|
|
170
|
+
f"Invalid response format (status {response.status_code})",
|
|
171
|
+
status_code=response.status_code,
|
|
172
|
+
response=response,
|
|
137
173
|
)
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
logger.error(f"API Error {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
174
|
+
|
|
175
|
+
# Check if it's an error response (non-200 status or has error field)
|
|
176
|
+
if response.status_code != 200:
|
|
177
|
+
error_resp = ErrorResponse(**response_data)
|
|
178
|
+
if error_resp.error:
|
|
179
|
+
error_code = error_resp.error.code
|
|
180
|
+
error_msg = error_resp.error.message
|
|
181
|
+
logger.error(f"API Error [{error_code}]: {error_msg}")
|
|
182
|
+
|
|
183
|
+
# Map to specific exception types based on status code
|
|
184
|
+
exception_class = self._get_exception_class(response.status_code)
|
|
185
|
+
raise exception_class(
|
|
186
|
+
error_msg,
|
|
187
|
+
code=error_code,
|
|
188
|
+
status_code=response.status_code,
|
|
189
|
+
response=response,
|
|
190
|
+
)
|
|
191
|
+
else:
|
|
192
|
+
# Error response without error details
|
|
193
|
+
logger.error(
|
|
194
|
+
f"API error response without details: {response.status_code}"
|
|
195
|
+
)
|
|
196
|
+
exception_class = self._get_exception_class(response.status_code)
|
|
197
|
+
raise exception_class(
|
|
198
|
+
f"API error (status {response.status_code})",
|
|
199
|
+
status_code=response.status_code,
|
|
149
200
|
response=response,
|
|
150
201
|
)
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
202
|
+
|
|
203
|
+
# Parse successful response
|
|
204
|
+
result = LLMResponse(**response_data)
|
|
205
|
+
|
|
206
|
+
# Check if the response contains an error (even with 200 status)
|
|
207
|
+
if result.error:
|
|
208
|
+
logger.error(
|
|
209
|
+
f"API Error in response: [{result.error.code}]: {result.error.message}"
|
|
210
|
+
)
|
|
211
|
+
raise APIError(
|
|
212
|
+
result.error.message,
|
|
213
|
+
code=result.error.code,
|
|
214
|
+
status_code=200,
|
|
215
|
+
response=response,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
logger.info(
|
|
219
|
+
f"API request successful - task_id: {result.task_id}, step: {result.current_step}, complete: {result.is_complete}"
|
|
220
|
+
)
|
|
221
|
+
logger.debug(f"Response included {len(result.actions)} actions")
|
|
222
|
+
return result
|
|
223
|
+
|
|
224
|
+
def _get_exception_class(self, status_code: int) -> type[APIError]:
|
|
225
|
+
"""Get the appropriate exception class based on status code."""
|
|
226
|
+
status_map = {
|
|
227
|
+
401: AuthenticationError,
|
|
228
|
+
404: NotFoundError,
|
|
229
|
+
422: ValidationError,
|
|
230
|
+
429: RateLimitError,
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if status_code >= 500:
|
|
234
|
+
return ServerError
|
|
235
|
+
|
|
236
|
+
return status_map.get(status_code, APIError)
|
|
155
237
|
|
|
156
238
|
def health_check(self) -> dict:
|
|
157
239
|
"""
|