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.
Files changed (60) hide show
  1. oagi/__init__.py +108 -0
  2. oagi/agent/__init__.py +31 -0
  3. oagi/agent/default.py +75 -0
  4. oagi/agent/factories.py +50 -0
  5. oagi/agent/protocol.py +55 -0
  6. oagi/agent/registry.py +155 -0
  7. oagi/agent/tasker/__init__.py +35 -0
  8. oagi/agent/tasker/memory.py +184 -0
  9. oagi/agent/tasker/models.py +83 -0
  10. oagi/agent/tasker/planner.py +385 -0
  11. oagi/agent/tasker/taskee_agent.py +395 -0
  12. oagi/agent/tasker/tasker_agent.py +323 -0
  13. oagi/async_pyautogui_action_handler.py +44 -0
  14. oagi/async_screenshot_maker.py +47 -0
  15. oagi/async_single_step.py +85 -0
  16. oagi/cli/__init__.py +11 -0
  17. oagi/cli/agent.py +125 -0
  18. oagi/cli/main.py +77 -0
  19. oagi/cli/server.py +94 -0
  20. oagi/cli/utils.py +82 -0
  21. oagi/client/__init__.py +12 -0
  22. oagi/client/async_.py +293 -0
  23. oagi/client/base.py +465 -0
  24. oagi/client/sync.py +296 -0
  25. oagi/exceptions.py +118 -0
  26. oagi/logging.py +47 -0
  27. oagi/pil_image.py +102 -0
  28. oagi/pyautogui_action_handler.py +268 -0
  29. oagi/screenshot_maker.py +41 -0
  30. oagi/server/__init__.py +13 -0
  31. oagi/server/agent_wrappers.py +98 -0
  32. oagi/server/config.py +46 -0
  33. oagi/server/main.py +157 -0
  34. oagi/server/models.py +98 -0
  35. oagi/server/session_store.py +116 -0
  36. oagi/server/socketio_server.py +405 -0
  37. oagi/single_step.py +87 -0
  38. oagi/task/__init__.py +14 -0
  39. oagi/task/async_.py +97 -0
  40. oagi/task/async_short.py +64 -0
  41. oagi/task/base.py +121 -0
  42. oagi/task/short.py +64 -0
  43. oagi/task/sync.py +97 -0
  44. oagi/types/__init__.py +28 -0
  45. oagi/types/action_handler.py +30 -0
  46. oagi/types/async_action_handler.py +30 -0
  47. oagi/types/async_image_provider.py +37 -0
  48. oagi/types/image.py +17 -0
  49. oagi/types/image_provider.py +34 -0
  50. oagi/types/models/__init__.py +32 -0
  51. oagi/types/models/action.py +33 -0
  52. oagi/types/models/client.py +64 -0
  53. oagi/types/models/image_config.py +47 -0
  54. oagi/types/models/step.py +17 -0
  55. oagi/types/url_image.py +47 -0
  56. oagi_core-0.9.0.dist-info/METADATA +257 -0
  57. oagi_core-0.9.0.dist-info/RECORD +60 -0
  58. oagi_core-0.9.0.dist-info/WHEEL +4 -0
  59. oagi_core-0.9.0.dist-info/entry_points.txt +2 -0
  60. 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
@@ -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
@@ -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
+ )