oagi-core 0.9.0__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.
- oagi/__init__.py +108 -0
- oagi/agent/__init__.py +31 -0
- oagi/agent/default.py +75 -0
- oagi/agent/factories.py +50 -0
- oagi/agent/protocol.py +55 -0
- oagi/agent/registry.py +155 -0
- oagi/agent/tasker/__init__.py +35 -0
- oagi/agent/tasker/memory.py +184 -0
- oagi/agent/tasker/models.py +83 -0
- oagi/agent/tasker/planner.py +385 -0
- oagi/agent/tasker/taskee_agent.py +395 -0
- oagi/agent/tasker/tasker_agent.py +323 -0
- oagi/async_pyautogui_action_handler.py +44 -0
- oagi/async_screenshot_maker.py +47 -0
- oagi/async_single_step.py +85 -0
- oagi/cli/__init__.py +11 -0
- oagi/cli/agent.py +125 -0
- oagi/cli/main.py +77 -0
- oagi/cli/server.py +94 -0
- oagi/cli/utils.py +82 -0
- oagi/client/__init__.py +12 -0
- oagi/client/async_.py +293 -0
- oagi/client/base.py +465 -0
- oagi/client/sync.py +296 -0
- oagi/exceptions.py +118 -0
- oagi/logging.py +47 -0
- oagi/pil_image.py +102 -0
- oagi/pyautogui_action_handler.py +268 -0
- oagi/screenshot_maker.py +41 -0
- oagi/server/__init__.py +13 -0
- oagi/server/agent_wrappers.py +98 -0
- oagi/server/config.py +46 -0
- oagi/server/main.py +157 -0
- oagi/server/models.py +98 -0
- oagi/server/session_store.py +116 -0
- oagi/server/socketio_server.py +405 -0
- oagi/single_step.py +87 -0
- oagi/task/__init__.py +14 -0
- oagi/task/async_.py +97 -0
- oagi/task/async_short.py +64 -0
- oagi/task/base.py +121 -0
- oagi/task/short.py +64 -0
- oagi/task/sync.py +97 -0
- oagi/types/__init__.py +28 -0
- oagi/types/action_handler.py +30 -0
- oagi/types/async_action_handler.py +30 -0
- oagi/types/async_image_provider.py +37 -0
- oagi/types/image.py +17 -0
- oagi/types/image_provider.py +34 -0
- oagi/types/models/__init__.py +32 -0
- oagi/types/models/action.py +33 -0
- oagi/types/models/client.py +64 -0
- oagi/types/models/image_config.py +47 -0
- oagi/types/models/step.py +17 -0
- oagi/types/url_image.py +47 -0
- oagi_core-0.9.0.dist-info/METADATA +257 -0
- oagi_core-0.9.0.dist-info/RECORD +60 -0
- oagi_core-0.9.0.dist-info/WHEEL +4 -0
- oagi_core-0.9.0.dist-info/entry_points.txt +2 -0
- oagi_core-0.9.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,268 @@
|
|
|
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 re
|
|
10
|
+
import time
|
|
11
|
+
|
|
12
|
+
from pydantic import BaseModel, Field
|
|
13
|
+
|
|
14
|
+
from .exceptions import check_optional_dependency
|
|
15
|
+
from .types import Action, ActionType
|
|
16
|
+
|
|
17
|
+
check_optional_dependency("pyautogui", "PyautoguiActionHandler", "desktop")
|
|
18
|
+
import pyautogui # noqa: E402
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CapsLockManager:
|
|
22
|
+
"""Manages caps lock state for text transformation."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, mode: str = "session"):
|
|
25
|
+
"""Initialize caps lock manager.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
mode: Either "session" (internal state) or "system" (OS-level)
|
|
29
|
+
"""
|
|
30
|
+
self.mode = mode
|
|
31
|
+
self.caps_enabled = False
|
|
32
|
+
|
|
33
|
+
def toggle(self):
|
|
34
|
+
"""Toggle caps lock state in session mode."""
|
|
35
|
+
if self.mode == "session":
|
|
36
|
+
self.caps_enabled = not self.caps_enabled
|
|
37
|
+
|
|
38
|
+
def transform_text(self, text: str) -> str:
|
|
39
|
+
"""Transform text based on caps lock state.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
text: Input text to transform
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Transformed text (uppercase if caps enabled in session mode)
|
|
46
|
+
"""
|
|
47
|
+
if self.mode == "session" and self.caps_enabled:
|
|
48
|
+
# Transform letters to uppercase, preserve special characters
|
|
49
|
+
return "".join(c.upper() if c.isalpha() else c for c in text)
|
|
50
|
+
return text
|
|
51
|
+
|
|
52
|
+
def should_use_system_capslock(self) -> bool:
|
|
53
|
+
"""Check if system-level caps lock should be used."""
|
|
54
|
+
return self.mode == "system"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class PyautoguiConfig(BaseModel):
|
|
58
|
+
"""Configuration for PyautoguiActionHandler."""
|
|
59
|
+
|
|
60
|
+
drag_duration: float = Field(
|
|
61
|
+
default=0.5, description="Duration for drag operations in seconds"
|
|
62
|
+
)
|
|
63
|
+
scroll_amount: int = Field(
|
|
64
|
+
default=30, description="Amount to scroll (positive for up, negative for down)"
|
|
65
|
+
)
|
|
66
|
+
wait_duration: float = Field(
|
|
67
|
+
default=1.0, description="Duration for wait actions in seconds"
|
|
68
|
+
)
|
|
69
|
+
action_pause: float = Field(
|
|
70
|
+
default=0.1, description="Pause between PyAutoGUI actions in seconds"
|
|
71
|
+
)
|
|
72
|
+
hotkey_interval: float = Field(
|
|
73
|
+
default=0.1, description="Interval between key presses in hotkey combinations"
|
|
74
|
+
)
|
|
75
|
+
capslock_mode: str = Field(
|
|
76
|
+
default="session",
|
|
77
|
+
description="Caps lock handling mode: 'session' (internal state) or 'system' (OS-level)",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class PyautoguiActionHandler:
|
|
82
|
+
"""
|
|
83
|
+
Handles actions to be executed using PyAutoGUI.
|
|
84
|
+
|
|
85
|
+
This class provides functionality for handling and executing a sequence of
|
|
86
|
+
actions using the PyAutoGUI library. It processes a list of actions and executes
|
|
87
|
+
them as per the implementation.
|
|
88
|
+
|
|
89
|
+
Methods:
|
|
90
|
+
__call__: Executes the provided list of actions.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
actions (list[Action]): List of actions to be processed and executed.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
def __init__(self, config: PyautoguiConfig | None = None):
|
|
97
|
+
# Use default config if none provided
|
|
98
|
+
self.config = config or PyautoguiConfig()
|
|
99
|
+
# Get screen dimensions for coordinate denormalization
|
|
100
|
+
self.screen_width, self.screen_height = pyautogui.size()
|
|
101
|
+
# Set default delay between actions
|
|
102
|
+
pyautogui.PAUSE = self.config.action_pause
|
|
103
|
+
# Initialize caps lock manager
|
|
104
|
+
self.caps_manager = CapsLockManager(mode=self.config.capslock_mode)
|
|
105
|
+
|
|
106
|
+
def _denormalize_coords(self, x: float, y: float) -> tuple[int, int]:
|
|
107
|
+
"""Convert coordinates from 0-1000 range to actual screen coordinates.
|
|
108
|
+
|
|
109
|
+
Also handles corner coordinates to prevent PyAutoGUI fail-safe trigger.
|
|
110
|
+
Corner coordinates (0,0), (0,max), (max,0), (max,max) are offset by 1 pixel.
|
|
111
|
+
"""
|
|
112
|
+
screen_x = int(x * self.screen_width / 1000)
|
|
113
|
+
screen_y = int(y * self.screen_height / 1000)
|
|
114
|
+
|
|
115
|
+
# Prevent fail-safe by adjusting corner coordinates
|
|
116
|
+
# Check if coordinates are at screen corners (with small tolerance)
|
|
117
|
+
if screen_x < 1:
|
|
118
|
+
screen_x = 1
|
|
119
|
+
elif screen_x > self.screen_width - 1:
|
|
120
|
+
screen_x = self.screen_width - 1
|
|
121
|
+
|
|
122
|
+
if screen_y < 1:
|
|
123
|
+
screen_y = 1
|
|
124
|
+
elif screen_y > self.screen_height - 1:
|
|
125
|
+
screen_y = self.screen_height - 1
|
|
126
|
+
|
|
127
|
+
return screen_x, screen_y
|
|
128
|
+
|
|
129
|
+
def _parse_coords(self, args_str: str) -> tuple[int, int]:
|
|
130
|
+
"""Extract x, y coordinates from argument string."""
|
|
131
|
+
match = re.match(r"(\d+),\s*(\d+)", args_str)
|
|
132
|
+
if not match:
|
|
133
|
+
raise ValueError(f"Invalid coordinates format: {args_str}")
|
|
134
|
+
x, y = int(match.group(1)), int(match.group(2))
|
|
135
|
+
return self._denormalize_coords(x, y)
|
|
136
|
+
|
|
137
|
+
def _parse_drag_coords(self, args_str: str) -> tuple[int, int, int, int]:
|
|
138
|
+
"""Extract x1, y1, x2, y2 coordinates from drag argument string."""
|
|
139
|
+
match = re.match(r"(\d+),\s*(\d+),\s*(\d+),\s*(\d+)", args_str)
|
|
140
|
+
if not match:
|
|
141
|
+
raise ValueError(f"Invalid drag coordinates format: {args_str}")
|
|
142
|
+
x1, y1, x2, y2 = (
|
|
143
|
+
int(match.group(1)),
|
|
144
|
+
int(match.group(2)),
|
|
145
|
+
int(match.group(3)),
|
|
146
|
+
int(match.group(4)),
|
|
147
|
+
)
|
|
148
|
+
x1, y1 = self._denormalize_coords(x1, y1)
|
|
149
|
+
x2, y2 = self._denormalize_coords(x2, y2)
|
|
150
|
+
return x1, y1, x2, y2
|
|
151
|
+
|
|
152
|
+
def _parse_scroll(self, args_str: str) -> tuple[int, int, str]:
|
|
153
|
+
"""Extract x, y, direction from scroll argument string."""
|
|
154
|
+
match = re.match(r"(\d+),\s*(\d+),\s*(\w+)", args_str)
|
|
155
|
+
if not match:
|
|
156
|
+
raise ValueError(f"Invalid scroll format: {args_str}")
|
|
157
|
+
x, y = int(match.group(1)), int(match.group(2))
|
|
158
|
+
x, y = self._denormalize_coords(x, y)
|
|
159
|
+
direction = match.group(3).lower()
|
|
160
|
+
return x, y, direction
|
|
161
|
+
|
|
162
|
+
def _normalize_key(self, key: str) -> str:
|
|
163
|
+
"""Normalize key names for consistency."""
|
|
164
|
+
key = key.strip().lower()
|
|
165
|
+
# Normalize caps lock variations
|
|
166
|
+
if key in ["caps_lock", "caps", "capslock"]:
|
|
167
|
+
return "capslock"
|
|
168
|
+
return key
|
|
169
|
+
|
|
170
|
+
def _parse_hotkey(self, args_str: str) -> list[str]:
|
|
171
|
+
"""Parse hotkey string into list of keys."""
|
|
172
|
+
# Remove parentheses if present
|
|
173
|
+
args_str = args_str.strip("()")
|
|
174
|
+
# Split by '+' to get individual keys
|
|
175
|
+
keys = [self._normalize_key(key) for key in args_str.split("+")]
|
|
176
|
+
return keys
|
|
177
|
+
|
|
178
|
+
def _execute_single_action(self, action: Action) -> None:
|
|
179
|
+
"""Execute a single action once."""
|
|
180
|
+
arg = action.argument.strip("()") # Remove outer parentheses if present
|
|
181
|
+
|
|
182
|
+
match action.type:
|
|
183
|
+
case ActionType.CLICK:
|
|
184
|
+
x, y = self._parse_coords(arg)
|
|
185
|
+
pyautogui.click(x, y)
|
|
186
|
+
|
|
187
|
+
case ActionType.LEFT_DOUBLE:
|
|
188
|
+
x, y = self._parse_coords(arg)
|
|
189
|
+
pyautogui.doubleClick(x, y)
|
|
190
|
+
|
|
191
|
+
case ActionType.LEFT_TRIPLE:
|
|
192
|
+
x, y = self._parse_coords(arg)
|
|
193
|
+
pyautogui.tripleClick(x, y)
|
|
194
|
+
|
|
195
|
+
case ActionType.RIGHT_SINGLE:
|
|
196
|
+
x, y = self._parse_coords(arg)
|
|
197
|
+
pyautogui.rightClick(x, y)
|
|
198
|
+
|
|
199
|
+
case ActionType.DRAG:
|
|
200
|
+
x1, y1, x2, y2 = self._parse_drag_coords(arg)
|
|
201
|
+
pyautogui.moveTo(x1, y1)
|
|
202
|
+
pyautogui.dragTo(
|
|
203
|
+
x2, y2, duration=self.config.drag_duration, button="left"
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
case ActionType.HOTKEY:
|
|
207
|
+
keys = self._parse_hotkey(arg)
|
|
208
|
+
# Check if this is a caps lock key press
|
|
209
|
+
if len(keys) == 1 and keys[0] == "capslock":
|
|
210
|
+
if self.caps_manager.should_use_system_capslock():
|
|
211
|
+
# System mode: use OS-level caps lock
|
|
212
|
+
pyautogui.hotkey(
|
|
213
|
+
"capslock", interval=self.config.hotkey_interval
|
|
214
|
+
)
|
|
215
|
+
else:
|
|
216
|
+
# Session mode: toggle internal state
|
|
217
|
+
self.caps_manager.toggle()
|
|
218
|
+
else:
|
|
219
|
+
# Regular hotkey combination
|
|
220
|
+
pyautogui.hotkey(*keys, interval=self.config.hotkey_interval)
|
|
221
|
+
|
|
222
|
+
case ActionType.TYPE:
|
|
223
|
+
# Remove quotes if present
|
|
224
|
+
text = arg.strip("\"'")
|
|
225
|
+
# Apply caps lock transformation if needed
|
|
226
|
+
text = self.caps_manager.transform_text(text)
|
|
227
|
+
pyautogui.typewrite(text)
|
|
228
|
+
|
|
229
|
+
case ActionType.SCROLL:
|
|
230
|
+
x, y, direction = self._parse_scroll(arg)
|
|
231
|
+
pyautogui.moveTo(x, y)
|
|
232
|
+
scroll_amount = (
|
|
233
|
+
self.config.scroll_amount
|
|
234
|
+
if direction == "up"
|
|
235
|
+
else -self.config.scroll_amount
|
|
236
|
+
)
|
|
237
|
+
pyautogui.scroll(scroll_amount)
|
|
238
|
+
|
|
239
|
+
case ActionType.FINISH:
|
|
240
|
+
# Task completion - no action needed
|
|
241
|
+
pass
|
|
242
|
+
|
|
243
|
+
case ActionType.WAIT:
|
|
244
|
+
# Wait for a short period
|
|
245
|
+
time.sleep(self.config.wait_duration)
|
|
246
|
+
|
|
247
|
+
case ActionType.CALL_USER:
|
|
248
|
+
# Call user - implementation depends on requirements
|
|
249
|
+
print("User intervention requested")
|
|
250
|
+
|
|
251
|
+
case _:
|
|
252
|
+
print(f"Unknown action type: {action.type}")
|
|
253
|
+
|
|
254
|
+
def _execute_action(self, action: Action) -> None:
|
|
255
|
+
"""Execute an action, potentially multiple times."""
|
|
256
|
+
count = action.count or 1
|
|
257
|
+
|
|
258
|
+
for _ in range(count):
|
|
259
|
+
self._execute_single_action(action)
|
|
260
|
+
|
|
261
|
+
def __call__(self, actions: list[Action]) -> None:
|
|
262
|
+
"""Execute the provided list of actions."""
|
|
263
|
+
for action in actions:
|
|
264
|
+
try:
|
|
265
|
+
self._execute_action(action)
|
|
266
|
+
except Exception as e:
|
|
267
|
+
print(f"Error executing action {action.type}: {e}")
|
|
268
|
+
raise
|
oagi/screenshot_maker.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
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 typing import Optional
|
|
10
|
+
|
|
11
|
+
from .pil_image import PILImage
|
|
12
|
+
from .types import Image
|
|
13
|
+
from .types.models.image_config import ImageConfig
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ScreenshotMaker:
|
|
17
|
+
"""Takes screenshots using pyautogui."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, config: ImageConfig | None = None):
|
|
20
|
+
self.config = config or ImageConfig()
|
|
21
|
+
self._last_image: Optional[PILImage] = None
|
|
22
|
+
|
|
23
|
+
def __call__(self) -> Image:
|
|
24
|
+
"""Take and process a screenshot."""
|
|
25
|
+
# Create PILImage from screenshot
|
|
26
|
+
pil_image = PILImage.from_screenshot()
|
|
27
|
+
|
|
28
|
+
# Apply transformation if config is set
|
|
29
|
+
if self.config:
|
|
30
|
+
pil_image = pil_image.transform(self.config)
|
|
31
|
+
|
|
32
|
+
# Store as the last image
|
|
33
|
+
self._last_image = pil_image
|
|
34
|
+
|
|
35
|
+
return pil_image
|
|
36
|
+
|
|
37
|
+
def last_image(self) -> Image:
|
|
38
|
+
"""Return the last screenshot taken, or take a new one if none exists."""
|
|
39
|
+
if self._last_image is None:
|
|
40
|
+
return self()
|
|
41
|
+
return self._last_image
|
oagi/server/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
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 .config import ServerConfig
|
|
10
|
+
from .main import create_app
|
|
11
|
+
from .socketio_server import sio
|
|
12
|
+
|
|
13
|
+
__all__ = ["create_app", "sio", "ServerConfig"]
|
|
@@ -0,0 +1,98 @@
|
|
|
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 logging
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
from ..types import URLImage
|
|
13
|
+
from ..types.models.action import Action
|
|
14
|
+
from .models import ScreenshotRequestData, ScreenshotResponseData
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from .session_store import Session
|
|
18
|
+
from .socketio_server import SessionNamespace
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SocketIOActionHandler:
|
|
24
|
+
"""Wraps Socket.IO connection as an AsyncActionHandler.
|
|
25
|
+
|
|
26
|
+
This handler emits actions through the Socket.IO connection to the client.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, namespace: "SessionNamespace", session: "Session"):
|
|
30
|
+
self.namespace = namespace
|
|
31
|
+
self.session = session
|
|
32
|
+
|
|
33
|
+
async def __call__(self, actions: list[Action]) -> None:
|
|
34
|
+
if not actions:
|
|
35
|
+
logger.debug("No actions to execute")
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
logger.debug(f"Executing {len(actions)} actions via Socket.IO")
|
|
39
|
+
await self.namespace._emit_actions(self.session, actions)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class SocketIOImageProvider:
|
|
43
|
+
"""Wraps Socket.IO connection as an AsyncImageProvider.
|
|
44
|
+
|
|
45
|
+
This provider requests screenshots from the client through Socket.IO.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
namespace: "SessionNamespace",
|
|
51
|
+
session: "Session",
|
|
52
|
+
oagi_client,
|
|
53
|
+
):
|
|
54
|
+
self.namespace = namespace
|
|
55
|
+
self.session = session
|
|
56
|
+
self.oagi_client = oagi_client
|
|
57
|
+
self._last_url: str | None = None
|
|
58
|
+
|
|
59
|
+
async def __call__(self) -> URLImage:
|
|
60
|
+
logger.debug("Requesting screenshot via Socket.IO")
|
|
61
|
+
|
|
62
|
+
# Get S3 presigned URL from OAGI
|
|
63
|
+
upload_response = await self.oagi_client.get_s3_presigned_url()
|
|
64
|
+
|
|
65
|
+
# Request screenshot from client with the presigned URL
|
|
66
|
+
screenshot_data = await self.namespace.call(
|
|
67
|
+
"request_screenshot",
|
|
68
|
+
ScreenshotRequestData(
|
|
69
|
+
presigned_url=upload_response.url,
|
|
70
|
+
uuid=upload_response.uuid,
|
|
71
|
+
expires_at=str(upload_response.expires_at), # Convert int to string
|
|
72
|
+
).model_dump(),
|
|
73
|
+
to=self.session.socket_id,
|
|
74
|
+
timeout=self.namespace.config.socketio_timeout,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
if not screenshot_data:
|
|
78
|
+
raise Exception("No response from screenshot request")
|
|
79
|
+
|
|
80
|
+
# Validate response
|
|
81
|
+
ack = ScreenshotResponseData(**screenshot_data)
|
|
82
|
+
if not ack.success:
|
|
83
|
+
raise Exception(f"Screenshot upload failed: {ack.error}")
|
|
84
|
+
|
|
85
|
+
# Store the URL for last_image()
|
|
86
|
+
self._last_url = upload_response.download_url
|
|
87
|
+
self.session.current_screenshot_url = upload_response.download_url
|
|
88
|
+
|
|
89
|
+
logger.debug(f"Screenshot captured successfully: {upload_response.uuid}")
|
|
90
|
+
return URLImage(upload_response.download_url)
|
|
91
|
+
|
|
92
|
+
async def last_image(self) -> URLImage:
|
|
93
|
+
if self._last_url:
|
|
94
|
+
logger.debug("Returning last captured screenshot")
|
|
95
|
+
return URLImage(self._last_url)
|
|
96
|
+
|
|
97
|
+
logger.debug("No previous screenshot, capturing new one")
|
|
98
|
+
return await self()
|
oagi/server/config.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
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 pydantic import Field
|
|
10
|
+
|
|
11
|
+
from ..exceptions import check_optional_dependency
|
|
12
|
+
|
|
13
|
+
check_optional_dependency("pydantic_settings", "Server features", "server")
|
|
14
|
+
from pydantic_settings import BaseSettings # noqa: E402
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ServerConfig(BaseSettings):
|
|
18
|
+
# OAGI API settings
|
|
19
|
+
oagi_api_key: str = Field(..., alias="OAGI_API_KEY")
|
|
20
|
+
oagi_base_url: str = Field(default="https://api.agiopen.org", alias="OAGI_BASE_URL")
|
|
21
|
+
|
|
22
|
+
# Server settings
|
|
23
|
+
server_host: str = Field(default="0.0.0.0", alias="OAGI_SERVER_HOST")
|
|
24
|
+
server_port: int = Field(default=8000, alias="OAGI_SERVER_PORT")
|
|
25
|
+
cors_allowed_origins: str = Field(default="*", alias="OAGI_CORS_ORIGINS")
|
|
26
|
+
|
|
27
|
+
# Session settings
|
|
28
|
+
session_timeout_seconds: float = Field(default=10.0)
|
|
29
|
+
|
|
30
|
+
# Model settings
|
|
31
|
+
default_model: str = Field(default="lux-v1", alias="OAGI_DEFAULT_MODEL")
|
|
32
|
+
default_temperature: float = Field(default=0.0, ge=0.0, le=2.0)
|
|
33
|
+
|
|
34
|
+
# Agent settings
|
|
35
|
+
max_steps: int = Field(default=30, alias="OAGI_MAX_STEPS", ge=1, le=100)
|
|
36
|
+
|
|
37
|
+
# Socket.IO settings
|
|
38
|
+
socketio_path: str = Field(default="/socket.io")
|
|
39
|
+
socketio_timeout: float = Field(default=30.0)
|
|
40
|
+
|
|
41
|
+
model_config = {
|
|
42
|
+
"env_file": ".env",
|
|
43
|
+
"env_file_encoding": "utf-8",
|
|
44
|
+
"populate_by_name": True,
|
|
45
|
+
"extra": "ignore",
|
|
46
|
+
}
|
oagi/server/main.py
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
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 logging
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from ..exceptions import check_optional_dependency
|
|
14
|
+
from .config import ServerConfig
|
|
15
|
+
from .models import SessionStatusData
|
|
16
|
+
from .session_store import session_store
|
|
17
|
+
from .socketio_server import socket_app
|
|
18
|
+
|
|
19
|
+
check_optional_dependency("fastapi", "Server features", "server")
|
|
20
|
+
check_optional_dependency("uvicorn", "Server features", "server")
|
|
21
|
+
|
|
22
|
+
import uvicorn # noqa: E402
|
|
23
|
+
from fastapi import FastAPI, HTTPException # noqa: E402
|
|
24
|
+
from fastapi.middleware.cors import CORSMiddleware # noqa: E402
|
|
25
|
+
|
|
26
|
+
logging.basicConfig(
|
|
27
|
+
level=logging.INFO,
|
|
28
|
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def create_app(config: ServerConfig | None = None) -> FastAPI:
|
|
35
|
+
if config is None:
|
|
36
|
+
config = ServerConfig()
|
|
37
|
+
|
|
38
|
+
app = FastAPI(
|
|
39
|
+
title="OAGI Socket.IO Server",
|
|
40
|
+
description="Real-time task automation server for OAGI SDK",
|
|
41
|
+
version="0.1.0",
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
cors_origins = (
|
|
45
|
+
config.cors_allowed_origins.split(",")
|
|
46
|
+
if config.cors_allowed_origins != "*"
|
|
47
|
+
else ["*"]
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
app.add_middleware(
|
|
51
|
+
CORSMiddleware,
|
|
52
|
+
allow_origins=cors_origins,
|
|
53
|
+
allow_credentials=True,
|
|
54
|
+
allow_methods=["*"],
|
|
55
|
+
allow_headers=["*"],
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
@app.get("/")
|
|
59
|
+
async def root() -> dict[str, str]:
|
|
60
|
+
return {
|
|
61
|
+
"name": "OAGI Socket.IO Server",
|
|
62
|
+
"version": "0.1.0",
|
|
63
|
+
"status": "running",
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@app.get("/health")
|
|
67
|
+
async def health_check() -> dict[str, Any]:
|
|
68
|
+
return {
|
|
69
|
+
"status": "healthy",
|
|
70
|
+
"server": {
|
|
71
|
+
"name": "OAGI Socket.IO Server",
|
|
72
|
+
"version": "0.1.0",
|
|
73
|
+
},
|
|
74
|
+
"config": {
|
|
75
|
+
"base_url": config.oagi_base_url,
|
|
76
|
+
"default_model": config.default_model,
|
|
77
|
+
},
|
|
78
|
+
"sessions": {
|
|
79
|
+
"active": len(session_store.sessions),
|
|
80
|
+
"connected": sum(
|
|
81
|
+
1 for s in session_store.sessions.values() if s.socket_id
|
|
82
|
+
),
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@app.get("/sessions")
|
|
87
|
+
async def list_sessions() -> dict[str, Any]:
|
|
88
|
+
return {
|
|
89
|
+
"sessions": session_store.list_sessions(),
|
|
90
|
+
"total": len(session_store.sessions),
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
@app.get("/sessions/{session_id}")
|
|
94
|
+
async def get_session(session_id: str) -> SessionStatusData:
|
|
95
|
+
session = session_store.get_session(session_id)
|
|
96
|
+
if not session:
|
|
97
|
+
raise HTTPException(
|
|
98
|
+
status_code=404, detail=f"Session {session_id} not found"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
return SessionStatusData(
|
|
102
|
+
session_id=session.session_id,
|
|
103
|
+
status=session.status, # type: ignore
|
|
104
|
+
instruction=session.instruction,
|
|
105
|
+
created_at=session.created_at,
|
|
106
|
+
actions_executed=session.actions_executed,
|
|
107
|
+
last_activity=datetime.fromtimestamp(session.last_activity).isoformat(),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
@app.delete("/sessions/{session_id}")
|
|
111
|
+
async def delete_session(session_id: str) -> dict[str, str]:
|
|
112
|
+
session = session_store.get_session(session_id)
|
|
113
|
+
if not session:
|
|
114
|
+
raise HTTPException(
|
|
115
|
+
status_code=404, detail=f"Session {session_id} not found"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
if session.oagi_client:
|
|
119
|
+
try:
|
|
120
|
+
await session.oagi_client.close()
|
|
121
|
+
except Exception as e:
|
|
122
|
+
logger.warning(f"Error closing OAGI client: {e}")
|
|
123
|
+
|
|
124
|
+
deleted = session_store.delete_session(session_id)
|
|
125
|
+
if deleted:
|
|
126
|
+
return {"message": f"Session {session_id} deleted"}
|
|
127
|
+
else:
|
|
128
|
+
raise HTTPException(status_code=500, detail="Failed to delete session")
|
|
129
|
+
|
|
130
|
+
@app.post("/sessions/cleanup")
|
|
131
|
+
async def cleanup_sessions(timeout_hours: float = 1.0) -> dict[str, Any]:
|
|
132
|
+
timeout_seconds = timeout_hours * 3600
|
|
133
|
+
cleaned = session_store.cleanup_inactive_sessions(timeout_seconds)
|
|
134
|
+
return {
|
|
135
|
+
"cleaned": cleaned,
|
|
136
|
+
"remaining": len(session_store.sessions),
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
# Mount Socket.IO application
|
|
140
|
+
app.mount("/", socket_app)
|
|
141
|
+
|
|
142
|
+
logger.info(
|
|
143
|
+
f"Server created - will listen on {config.server_host}:{config.server_port}"
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
return app
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
if __name__ == "__main__":
|
|
150
|
+
config = ServerConfig()
|
|
151
|
+
app = create_app(config)
|
|
152
|
+
uvicorn.run(
|
|
153
|
+
app,
|
|
154
|
+
host=config.server_host,
|
|
155
|
+
port=config.server_port,
|
|
156
|
+
log_level="info",
|
|
157
|
+
)
|