oagi-core 0.10.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.
- oagi/__init__.py +148 -0
- oagi/agent/__init__.py +33 -0
- oagi/agent/default.py +124 -0
- oagi/agent/factories.py +74 -0
- oagi/agent/observer/__init__.py +38 -0
- oagi/agent/observer/agent_observer.py +99 -0
- oagi/agent/observer/events.py +28 -0
- oagi/agent/observer/exporters.py +445 -0
- oagi/agent/observer/protocol.py +12 -0
- oagi/agent/protocol.py +55 -0
- oagi/agent/registry.py +155 -0
- oagi/agent/tasker/__init__.py +33 -0
- oagi/agent/tasker/memory.py +160 -0
- oagi/agent/tasker/models.py +77 -0
- oagi/agent/tasker/planner.py +408 -0
- oagi/agent/tasker/taskee_agent.py +512 -0
- oagi/agent/tasker/tasker_agent.py +324 -0
- oagi/cli/__init__.py +11 -0
- oagi/cli/agent.py +281 -0
- oagi/cli/display.py +56 -0
- oagi/cli/main.py +77 -0
- oagi/cli/server.py +94 -0
- oagi/cli/tracking.py +55 -0
- oagi/cli/utils.py +89 -0
- oagi/client/__init__.py +12 -0
- oagi/client/async_.py +290 -0
- oagi/client/base.py +457 -0
- oagi/client/sync.py +293 -0
- oagi/exceptions.py +118 -0
- oagi/handler/__init__.py +24 -0
- oagi/handler/_macos.py +55 -0
- oagi/handler/async_pyautogui_action_handler.py +44 -0
- oagi/handler/async_screenshot_maker.py +47 -0
- oagi/handler/pil_image.py +102 -0
- oagi/handler/pyautogui_action_handler.py +291 -0
- oagi/handler/screenshot_maker.py +41 -0
- oagi/logging.py +55 -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/task/__init__.py +21 -0
- oagi/task/async_.py +101 -0
- oagi/task/async_short.py +76 -0
- oagi/task/base.py +157 -0
- oagi/task/short.py +76 -0
- oagi/task/sync.py +99 -0
- oagi/types/__init__.py +50 -0
- oagi/types/action_handler.py +30 -0
- oagi/types/async_action_handler.py +30 -0
- oagi/types/async_image_provider.py +38 -0
- oagi/types/image.py +17 -0
- oagi/types/image_provider.py +35 -0
- oagi/types/models/__init__.py +32 -0
- oagi/types/models/action.py +33 -0
- oagi/types/models/client.py +68 -0
- oagi/types/models/image_config.py +47 -0
- oagi/types/models/step.py +17 -0
- oagi/types/step_observer.py +93 -0
- oagi/types/url.py +3 -0
- oagi_core-0.10.1.dist-info/METADATA +245 -0
- oagi_core-0.10.1.dist-info/RECORD +68 -0
- oagi_core-0.10.1.dist-info/WHEEL +4 -0
- oagi_core-0.10.1.dist-info/entry_points.txt +2 -0
- oagi_core-0.10.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,291 @@
|
|
|
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 sys
|
|
11
|
+
import time
|
|
12
|
+
|
|
13
|
+
from pydantic import BaseModel, Field
|
|
14
|
+
|
|
15
|
+
from ..exceptions import check_optional_dependency
|
|
16
|
+
from ..types import Action, ActionType
|
|
17
|
+
|
|
18
|
+
check_optional_dependency("pyautogui", "PyautoguiActionHandler", "desktop")
|
|
19
|
+
import pyautogui # noqa: E402
|
|
20
|
+
|
|
21
|
+
if sys.platform == "darwin":
|
|
22
|
+
from . import _macos
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class CapsLockManager:
|
|
26
|
+
"""Manages caps lock state for text transformation."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, mode: str = "session"):
|
|
29
|
+
"""Initialize caps lock manager.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
mode: Either "session" (internal state) or "system" (OS-level)
|
|
33
|
+
"""
|
|
34
|
+
self.mode = mode
|
|
35
|
+
self.caps_enabled = False
|
|
36
|
+
|
|
37
|
+
def toggle(self):
|
|
38
|
+
"""Toggle caps lock state in session mode."""
|
|
39
|
+
if self.mode == "session":
|
|
40
|
+
self.caps_enabled = not self.caps_enabled
|
|
41
|
+
|
|
42
|
+
def transform_text(self, text: str) -> str:
|
|
43
|
+
"""Transform text based on caps lock state.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
text: Input text to transform
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Transformed text (uppercase if caps enabled in session mode)
|
|
50
|
+
"""
|
|
51
|
+
if self.mode == "session" and self.caps_enabled:
|
|
52
|
+
# Transform letters to uppercase, preserve special characters
|
|
53
|
+
return "".join(c.upper() if c.isalpha() else c for c in text)
|
|
54
|
+
return text
|
|
55
|
+
|
|
56
|
+
def should_use_system_capslock(self) -> bool:
|
|
57
|
+
"""Check if system-level caps lock should be used."""
|
|
58
|
+
return self.mode == "system"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class PyautoguiConfig(BaseModel):
|
|
62
|
+
"""Configuration for PyautoguiActionHandler."""
|
|
63
|
+
|
|
64
|
+
drag_duration: float = Field(
|
|
65
|
+
default=0.5, description="Duration for drag operations in seconds"
|
|
66
|
+
)
|
|
67
|
+
scroll_amount: int = Field(
|
|
68
|
+
default=30, description="Amount to scroll (positive for up, negative for down)"
|
|
69
|
+
)
|
|
70
|
+
wait_duration: float = Field(
|
|
71
|
+
default=1.0, description="Duration for wait actions in seconds"
|
|
72
|
+
)
|
|
73
|
+
action_pause: float = Field(
|
|
74
|
+
default=0.1, description="Pause between PyAutoGUI actions in seconds"
|
|
75
|
+
)
|
|
76
|
+
hotkey_interval: float = Field(
|
|
77
|
+
default=0.1, description="Interval between key presses in hotkey combinations"
|
|
78
|
+
)
|
|
79
|
+
capslock_mode: str = Field(
|
|
80
|
+
default="session",
|
|
81
|
+
description="Caps lock handling mode: 'session' (internal state) or 'system' (OS-level)",
|
|
82
|
+
)
|
|
83
|
+
macos_ctrl_to_cmd: bool = Field(
|
|
84
|
+
default=True,
|
|
85
|
+
description="Replace 'ctrl' with 'command' in hotkey combinations on macOS",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class PyautoguiActionHandler:
|
|
90
|
+
"""
|
|
91
|
+
Handles actions to be executed using PyAutoGUI.
|
|
92
|
+
|
|
93
|
+
This class provides functionality for handling and executing a sequence of
|
|
94
|
+
actions using the PyAutoGUI library. It processes a list of actions and executes
|
|
95
|
+
them as per the implementation.
|
|
96
|
+
|
|
97
|
+
Methods:
|
|
98
|
+
__call__: Executes the provided list of actions.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
actions (list[Action]): List of actions to be processed and executed.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
def __init__(self, config: PyautoguiConfig | None = None):
|
|
105
|
+
# Use default config if none provided
|
|
106
|
+
self.config = config or PyautoguiConfig()
|
|
107
|
+
# Get screen dimensions for coordinate denormalization
|
|
108
|
+
self.screen_width, self.screen_height = pyautogui.size()
|
|
109
|
+
# Set default delay between actions
|
|
110
|
+
pyautogui.PAUSE = self.config.action_pause
|
|
111
|
+
# Initialize caps lock manager
|
|
112
|
+
self.caps_manager = CapsLockManager(mode=self.config.capslock_mode)
|
|
113
|
+
|
|
114
|
+
def _denormalize_coords(self, x: float, y: float) -> tuple[int, int]:
|
|
115
|
+
"""Convert coordinates from 0-1000 range to actual screen coordinates.
|
|
116
|
+
|
|
117
|
+
Also handles corner coordinates to prevent PyAutoGUI fail-safe trigger.
|
|
118
|
+
Corner coordinates (0,0), (0,max), (max,0), (max,max) are offset by 1 pixel.
|
|
119
|
+
"""
|
|
120
|
+
screen_x = int(x * self.screen_width / 1000)
|
|
121
|
+
screen_y = int(y * self.screen_height / 1000)
|
|
122
|
+
|
|
123
|
+
# Prevent fail-safe by adjusting corner coordinates
|
|
124
|
+
# Check if coordinates are at screen corners (with small tolerance)
|
|
125
|
+
if screen_x < 1:
|
|
126
|
+
screen_x = 1
|
|
127
|
+
elif screen_x > self.screen_width - 1:
|
|
128
|
+
screen_x = self.screen_width - 1
|
|
129
|
+
|
|
130
|
+
if screen_y < 1:
|
|
131
|
+
screen_y = 1
|
|
132
|
+
elif screen_y > self.screen_height - 1:
|
|
133
|
+
screen_y = self.screen_height - 1
|
|
134
|
+
|
|
135
|
+
return screen_x, screen_y
|
|
136
|
+
|
|
137
|
+
def _parse_coords(self, args_str: str) -> tuple[int, int]:
|
|
138
|
+
"""Extract x, y coordinates from argument string."""
|
|
139
|
+
match = re.match(r"(\d+),\s*(\d+)", args_str)
|
|
140
|
+
if not match:
|
|
141
|
+
raise ValueError(f"Invalid coordinates format: {args_str}")
|
|
142
|
+
x, y = int(match.group(1)), int(match.group(2))
|
|
143
|
+
return self._denormalize_coords(x, y)
|
|
144
|
+
|
|
145
|
+
def _parse_drag_coords(self, args_str: str) -> tuple[int, int, int, int]:
|
|
146
|
+
"""Extract x1, y1, x2, y2 coordinates from drag argument string."""
|
|
147
|
+
match = re.match(r"(\d+),\s*(\d+),\s*(\d+),\s*(\d+)", args_str)
|
|
148
|
+
if not match:
|
|
149
|
+
raise ValueError(f"Invalid drag coordinates format: {args_str}")
|
|
150
|
+
x1, y1, x2, y2 = (
|
|
151
|
+
int(match.group(1)),
|
|
152
|
+
int(match.group(2)),
|
|
153
|
+
int(match.group(3)),
|
|
154
|
+
int(match.group(4)),
|
|
155
|
+
)
|
|
156
|
+
x1, y1 = self._denormalize_coords(x1, y1)
|
|
157
|
+
x2, y2 = self._denormalize_coords(x2, y2)
|
|
158
|
+
return x1, y1, x2, y2
|
|
159
|
+
|
|
160
|
+
def _parse_scroll(self, args_str: str) -> tuple[int, int, str]:
|
|
161
|
+
"""Extract x, y, direction from scroll argument string."""
|
|
162
|
+
match = re.match(r"(\d+),\s*(\d+),\s*(\w+)", args_str)
|
|
163
|
+
if not match:
|
|
164
|
+
raise ValueError(f"Invalid scroll format: {args_str}")
|
|
165
|
+
x, y = int(match.group(1)), int(match.group(2))
|
|
166
|
+
x, y = self._denormalize_coords(x, y)
|
|
167
|
+
direction = match.group(3).lower()
|
|
168
|
+
return x, y, direction
|
|
169
|
+
|
|
170
|
+
def _normalize_key(self, key: str) -> str:
|
|
171
|
+
"""Normalize key names for consistency."""
|
|
172
|
+
key = key.strip().lower()
|
|
173
|
+
# Normalize caps lock variations
|
|
174
|
+
hotkey_variations_mapping = {
|
|
175
|
+
"capslock": ["caps_lock", "caps", "capslock"],
|
|
176
|
+
"pgup": ["page_up", "pageup"],
|
|
177
|
+
"pgdn": ["page_down", "pagedown"],
|
|
178
|
+
}
|
|
179
|
+
for normalized, variations in hotkey_variations_mapping.items():
|
|
180
|
+
if key in variations:
|
|
181
|
+
return normalized
|
|
182
|
+
# Remap ctrl to command on macOS if enabled
|
|
183
|
+
if self.config.macos_ctrl_to_cmd and sys.platform == "darwin" and key == "ctrl":
|
|
184
|
+
return "command"
|
|
185
|
+
return key
|
|
186
|
+
|
|
187
|
+
def _parse_hotkey(self, args_str: str) -> list[str]:
|
|
188
|
+
"""Parse hotkey string into list of keys."""
|
|
189
|
+
# Remove parentheses if present
|
|
190
|
+
args_str = args_str.strip("()")
|
|
191
|
+
# Split by '+' to get individual keys
|
|
192
|
+
keys = [self._normalize_key(key) for key in args_str.split("+")]
|
|
193
|
+
return keys
|
|
194
|
+
|
|
195
|
+
def _execute_single_action(self, action: Action) -> None:
|
|
196
|
+
"""Execute a single action once."""
|
|
197
|
+
arg = action.argument.strip("()") # Remove outer parentheses if present
|
|
198
|
+
|
|
199
|
+
match action.type:
|
|
200
|
+
case ActionType.CLICK:
|
|
201
|
+
x, y = self._parse_coords(arg)
|
|
202
|
+
pyautogui.click(x, y)
|
|
203
|
+
|
|
204
|
+
case ActionType.LEFT_DOUBLE:
|
|
205
|
+
x, y = self._parse_coords(arg)
|
|
206
|
+
if sys.platform == "darwin":
|
|
207
|
+
_macos.macos_click(x, y, clicks=2)
|
|
208
|
+
else:
|
|
209
|
+
pyautogui.doubleClick(x, y)
|
|
210
|
+
|
|
211
|
+
case ActionType.LEFT_TRIPLE:
|
|
212
|
+
x, y = self._parse_coords(arg)
|
|
213
|
+
if sys.platform == "darwin":
|
|
214
|
+
_macos.macos_click(x, y, clicks=3)
|
|
215
|
+
else:
|
|
216
|
+
pyautogui.tripleClick(x, y)
|
|
217
|
+
|
|
218
|
+
case ActionType.RIGHT_SINGLE:
|
|
219
|
+
x, y = self._parse_coords(arg)
|
|
220
|
+
pyautogui.rightClick(x, y)
|
|
221
|
+
|
|
222
|
+
case ActionType.DRAG:
|
|
223
|
+
x1, y1, x2, y2 = self._parse_drag_coords(arg)
|
|
224
|
+
pyautogui.moveTo(x1, y1)
|
|
225
|
+
pyautogui.dragTo(
|
|
226
|
+
x2, y2, duration=self.config.drag_duration, button="left"
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
case ActionType.HOTKEY:
|
|
230
|
+
keys = self._parse_hotkey(arg)
|
|
231
|
+
# Check if this is a caps lock key press
|
|
232
|
+
if len(keys) == 1 and keys[0] == "capslock":
|
|
233
|
+
if self.caps_manager.should_use_system_capslock():
|
|
234
|
+
# System mode: use OS-level caps lock
|
|
235
|
+
pyautogui.hotkey(
|
|
236
|
+
"capslock", interval=self.config.hotkey_interval
|
|
237
|
+
)
|
|
238
|
+
else:
|
|
239
|
+
# Session mode: toggle internal state
|
|
240
|
+
self.caps_manager.toggle()
|
|
241
|
+
else:
|
|
242
|
+
# Regular hotkey combination
|
|
243
|
+
pyautogui.hotkey(*keys, interval=self.config.hotkey_interval)
|
|
244
|
+
|
|
245
|
+
case ActionType.TYPE:
|
|
246
|
+
# Remove quotes if present
|
|
247
|
+
text = arg.strip("\"'")
|
|
248
|
+
# Apply caps lock transformation if needed
|
|
249
|
+
text = self.caps_manager.transform_text(text)
|
|
250
|
+
pyautogui.typewrite(text)
|
|
251
|
+
|
|
252
|
+
case ActionType.SCROLL:
|
|
253
|
+
x, y, direction = self._parse_scroll(arg)
|
|
254
|
+
pyautogui.moveTo(x, y)
|
|
255
|
+
scroll_amount = (
|
|
256
|
+
self.config.scroll_amount
|
|
257
|
+
if direction == "up"
|
|
258
|
+
else -self.config.scroll_amount
|
|
259
|
+
)
|
|
260
|
+
pyautogui.scroll(scroll_amount)
|
|
261
|
+
|
|
262
|
+
case ActionType.FINISH:
|
|
263
|
+
# Task completion - no action needed
|
|
264
|
+
pass
|
|
265
|
+
|
|
266
|
+
case ActionType.WAIT:
|
|
267
|
+
# Wait for a short period
|
|
268
|
+
time.sleep(self.config.wait_duration)
|
|
269
|
+
|
|
270
|
+
case ActionType.CALL_USER:
|
|
271
|
+
# Call user - implementation depends on requirements
|
|
272
|
+
print("User intervention requested")
|
|
273
|
+
|
|
274
|
+
case _:
|
|
275
|
+
print(f"Unknown action type: {action.type}")
|
|
276
|
+
|
|
277
|
+
def _execute_action(self, action: Action) -> None:
|
|
278
|
+
"""Execute an action, potentially multiple times."""
|
|
279
|
+
count = action.count or 1
|
|
280
|
+
|
|
281
|
+
for _ in range(count):
|
|
282
|
+
self._execute_single_action(action)
|
|
283
|
+
|
|
284
|
+
def __call__(self, actions: list[Action]) -> None:
|
|
285
|
+
"""Execute the provided list of actions."""
|
|
286
|
+
for action in actions:
|
|
287
|
+
try:
|
|
288
|
+
self._execute_action(action)
|
|
289
|
+
except Exception as e:
|
|
290
|
+
print(f"Error executing action {action.type}: {e}")
|
|
291
|
+
raise
|
|
@@ -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 ..types import Image
|
|
12
|
+
from ..types.models.image_config import ImageConfig
|
|
13
|
+
from .pil_image import PILImage
|
|
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/logging.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
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
|
+
import os
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_logger(name: str) -> logging.Logger:
|
|
14
|
+
"""
|
|
15
|
+
Get a logger with the specified name under the 'oagi' namespace.
|
|
16
|
+
|
|
17
|
+
Log level is controlled by OAGI_LOG environment variable.
|
|
18
|
+
Valid values: DEBUG, INFO, WARNING, ERROR, CRITICAL
|
|
19
|
+
Default: INFO
|
|
20
|
+
"""
|
|
21
|
+
logger = logging.getLogger(f"oagi.{name}")
|
|
22
|
+
oagi_root = logging.getLogger("oagi")
|
|
23
|
+
|
|
24
|
+
# Get log level from environment
|
|
25
|
+
log_level = os.getenv("OAGI_LOG", "INFO").upper()
|
|
26
|
+
|
|
27
|
+
# Convert string to logging level
|
|
28
|
+
try:
|
|
29
|
+
level = getattr(logging, log_level)
|
|
30
|
+
except AttributeError:
|
|
31
|
+
level = logging.INFO
|
|
32
|
+
|
|
33
|
+
# Configure root oagi logger once
|
|
34
|
+
if not oagi_root.handlers:
|
|
35
|
+
handler = logging.StreamHandler()
|
|
36
|
+
formatter = logging.Formatter(
|
|
37
|
+
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
38
|
+
)
|
|
39
|
+
handler.setFormatter(formatter)
|
|
40
|
+
oagi_root.addHandler(handler)
|
|
41
|
+
# Prevent propagation to root logger to avoid duplicate logs
|
|
42
|
+
oagi_root.propagate = False
|
|
43
|
+
|
|
44
|
+
# Always update level in case environment variable changed
|
|
45
|
+
oagi_root.setLevel(level)
|
|
46
|
+
|
|
47
|
+
# Suppress verbose httpx logs unless DEBUG level is enabled
|
|
48
|
+
# httpx logs every HTTP request at INFO level by default
|
|
49
|
+
httpx_logger = logging.getLogger("httpx")
|
|
50
|
+
if level == logging.DEBUG:
|
|
51
|
+
httpx_logger.setLevel(logging.DEBUG)
|
|
52
|
+
else:
|
|
53
|
+
httpx_logger.setLevel(logging.WARNING)
|
|
54
|
+
|
|
55
|
+
return logger
|
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 URL
|
|
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) -> URL:
|
|
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 URL(upload_response.download_url)
|
|
91
|
+
|
|
92
|
+
async def last_image(self) -> URL:
|
|
93
|
+
if self._last_url:
|
|
94
|
+
logger.debug("Returning last captured screenshot")
|
|
95
|
+
return URL(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-actor-1", alias="OAGI_DEFAULT_MODEL")
|
|
32
|
+
default_temperature: float = Field(default=0.5, ge=0.0, le=2.0)
|
|
33
|
+
|
|
34
|
+
# Agent settings
|
|
35
|
+
max_steps: int = Field(default=20, 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
|
+
}
|