oagi-core 0.10.3__py3-none-any.whl → 0.12.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 (44) hide show
  1. oagi/__init__.py +1 -3
  2. oagi/actor/__init__.py +21 -0
  3. oagi/{task → actor}/async_.py +23 -7
  4. oagi/{task → actor}/async_short.py +1 -1
  5. oagi/actor/base.py +222 -0
  6. oagi/{task → actor}/short.py +1 -1
  7. oagi/{task → actor}/sync.py +21 -5
  8. oagi/agent/default.py +5 -0
  9. oagi/agent/factories.py +75 -3
  10. oagi/agent/observer/exporters.py +6 -0
  11. oagi/agent/observer/report_template.html +19 -0
  12. oagi/agent/tasker/planner.py +31 -19
  13. oagi/agent/tasker/taskee_agent.py +26 -7
  14. oagi/agent/tasker/tasker_agent.py +4 -0
  15. oagi/cli/agent.py +54 -30
  16. oagi/client/async_.py +54 -96
  17. oagi/client/base.py +81 -133
  18. oagi/client/sync.py +52 -99
  19. oagi/constants.py +7 -2
  20. oagi/handler/__init__.py +16 -0
  21. oagi/handler/_macos.py +137 -0
  22. oagi/handler/_windows.py +101 -0
  23. oagi/handler/async_pyautogui_action_handler.py +8 -0
  24. oagi/handler/capslock_manager.py +55 -0
  25. oagi/handler/pyautogui_action_handler.py +21 -39
  26. oagi/server/session_store.py +3 -3
  27. oagi/server/socketio_server.py +4 -4
  28. oagi/task/__init__.py +22 -8
  29. oagi/types/__init__.py +2 -1
  30. oagi/types/models/__init__.py +0 -2
  31. oagi/types/models/action.py +4 -1
  32. oagi/types/models/client.py +1 -17
  33. oagi/types/step_observer.py +2 -0
  34. oagi/types/url.py +25 -0
  35. oagi/utils/__init__.py +12 -0
  36. oagi/utils/output_parser.py +166 -0
  37. oagi/utils/prompt_builder.py +44 -0
  38. {oagi_core-0.10.3.dist-info → oagi_core-0.12.0.dist-info}/METADATA +90 -10
  39. oagi_core-0.12.0.dist-info/RECORD +76 -0
  40. oagi/task/base.py +0 -158
  41. oagi_core-0.10.3.dist-info/RECORD +0 -70
  42. {oagi_core-0.10.3.dist-info → oagi_core-0.12.0.dist-info}/WHEEL +0 -0
  43. {oagi_core-0.10.3.dist-info → oagi_core-0.12.0.dist-info}/entry_points.txt +0 -0
  44. {oagi_core-0.10.3.dist-info → oagi_core-0.12.0.dist-info}/licenses/LICENSE +0 -0
oagi/handler/_macos.py CHANGED
@@ -6,6 +6,15 @@
6
6
  # Licensed under the MIT License.
7
7
  # -----------------------------------------------------------------------------
8
8
 
9
+ """macOS-specific keyboard and mouse input handling.
10
+
11
+ This module provides:
12
+ - macos_click(): Fix for PyAutoGUI multi-click bug on macOS
13
+ - typewrite_exact(): Type text exactly, ignoring system capslock state
14
+ """
15
+
16
+ import time
17
+
9
18
  import pyautogui
10
19
 
11
20
  from ..exceptions import check_optional_dependency
@@ -13,6 +22,134 @@ from ..exceptions import check_optional_dependency
13
22
  check_optional_dependency("Quartz", "macOS multiple clicks", "desktop")
14
23
  import Quartz # noqa: E402
15
24
 
25
+ # macOS virtual key codes for typeable characters
26
+ KEYCODE_MAP = {
27
+ "a": 0x00,
28
+ "b": 0x0B,
29
+ "c": 0x08,
30
+ "d": 0x02,
31
+ "e": 0x0E,
32
+ "f": 0x03,
33
+ "g": 0x05,
34
+ "h": 0x04,
35
+ "i": 0x22,
36
+ "j": 0x26,
37
+ "k": 0x28,
38
+ "l": 0x25,
39
+ "m": 0x2E,
40
+ "n": 0x2D,
41
+ "o": 0x1F,
42
+ "p": 0x23,
43
+ "q": 0x0C,
44
+ "r": 0x0F,
45
+ "s": 0x01,
46
+ "t": 0x11,
47
+ "u": 0x20,
48
+ "v": 0x09,
49
+ "w": 0x0D,
50
+ "x": 0x07,
51
+ "y": 0x10,
52
+ "z": 0x06,
53
+ "1": 0x12,
54
+ "2": 0x13,
55
+ "3": 0x14,
56
+ "4": 0x15,
57
+ "5": 0x17,
58
+ "6": 0x16,
59
+ "7": 0x1A,
60
+ "8": 0x1C,
61
+ "9": 0x19,
62
+ "0": 0x1D,
63
+ " ": 0x31, # space
64
+ "-": 0x1B,
65
+ "=": 0x18,
66
+ "[": 0x21,
67
+ "]": 0x1E,
68
+ "\\": 0x2A,
69
+ ";": 0x29,
70
+ "'": 0x27,
71
+ "`": 0x32,
72
+ ",": 0x2B,
73
+ ".": 0x2F,
74
+ "/": 0x2C,
75
+ "\t": 0x30, # tab
76
+ "\n": 0x24, # return
77
+ }
78
+
79
+ # Characters that require shift key (on US keyboard layout)
80
+ SHIFT_CHARS = set('~!@#$%^&*()_+{}|:"<>?ABCDEFGHIJKLMNOPQRSTUVWXYZ')
81
+
82
+ # Mapping of shifted characters to their base key
83
+ SHIFT_KEY_MAP = {
84
+ "~": "`",
85
+ "!": "1",
86
+ "@": "2",
87
+ "#": "3",
88
+ "$": "4",
89
+ "%": "5",
90
+ "^": "6",
91
+ "&": "7",
92
+ "*": "8",
93
+ "(": "9",
94
+ ")": "0",
95
+ "_": "-",
96
+ "+": "=",
97
+ "{": "[",
98
+ "}": "]",
99
+ "|": "\\",
100
+ ":": ";",
101
+ '"': "'",
102
+ "<": ",",
103
+ ">": ".",
104
+ "?": "/",
105
+ }
106
+
107
+
108
+ def typewrite_exact(text: str, interval: float = 0.01) -> None:
109
+ """Type text exactly as specified, ignoring system capslock state.
110
+
111
+ This function uses Quartz CGEventCreateKeyboardEvent with explicit
112
+ flag control via CGEventSetFlags() to type each character with the
113
+ correct case, regardless of the system's capslock state.
114
+
115
+ Args:
116
+ text: The text to type exactly as specified
117
+ interval: Time in seconds between each character (default: 0.01)
118
+ """
119
+ for char in text:
120
+ # Determine if this character needs shift
121
+ needs_shift = char in SHIFT_CHARS
122
+
123
+ # Get the base key (for shifted chars, look up the unshifted version)
124
+ if char.isupper():
125
+ base_char = char.lower()
126
+ elif char in SHIFT_KEY_MAP:
127
+ base_char = SHIFT_KEY_MAP[char]
128
+ else:
129
+ base_char = char
130
+
131
+ # Get keycode for the base character
132
+ keycode = KEYCODE_MAP.get(base_char)
133
+ if keycode is None:
134
+ # Character not in our keycode map, skip it
135
+ continue
136
+
137
+ # Set flags: shift if needed, otherwise clear all flags
138
+ flags = Quartz.kCGEventFlagMaskShift if needs_shift else 0
139
+
140
+ # Key down
141
+ event_down = Quartz.CGEventCreateKeyboardEvent(None, keycode, True)
142
+ Quartz.CGEventSetFlags(event_down, flags)
143
+ Quartz.CGEventPost(Quartz.kCGHIDEventTap, event_down)
144
+
145
+ # Key up
146
+ event_up = Quartz.CGEventCreateKeyboardEvent(None, keycode, False)
147
+ Quartz.CGEventSetFlags(event_up, flags)
148
+ Quartz.CGEventPost(Quartz.kCGHIDEventTap, event_up)
149
+
150
+ if interval > 0:
151
+ time.sleep(interval)
152
+
16
153
 
17
154
  def macos_click(x: int, y: int, clicks: int = 1) -> None:
18
155
  """
@@ -0,0 +1,101 @@
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
+ """Windows-specific keyboard input handling.
10
+
11
+ This module provides typewrite_exact() which types text exactly as specified,
12
+ ignoring the system's capslock state by using SendInput with KEYEVENTF_UNICODE.
13
+ """
14
+
15
+ import ctypes
16
+ import time
17
+ from ctypes import wintypes
18
+
19
+ INPUT_KEYBOARD = 1
20
+ KEYEVENTF_UNICODE = 0x0004
21
+ KEYEVENTF_KEYUP = 0x0002
22
+
23
+
24
+ class KEYBDINPUT(ctypes.Structure):
25
+ _fields_ = [
26
+ ("wVk", wintypes.WORD),
27
+ ("wScan", wintypes.WORD),
28
+ ("dwFlags", wintypes.DWORD),
29
+ ("time", wintypes.DWORD),
30
+ ("dwExtraInfo", ctypes.POINTER(ctypes.c_ulong)),
31
+ ]
32
+
33
+
34
+ class MOUSEINPUT(ctypes.Structure):
35
+ _fields_ = [
36
+ ("dx", ctypes.c_long),
37
+ ("dy", ctypes.c_long),
38
+ ("mouseData", wintypes.DWORD),
39
+ ("dwFlags", wintypes.DWORD),
40
+ ("time", wintypes.DWORD),
41
+ ("dwExtraInfo", ctypes.POINTER(ctypes.c_ulong)),
42
+ ]
43
+
44
+
45
+ class HARDWAREINPUT(ctypes.Structure):
46
+ _fields_ = [
47
+ ("uMsg", wintypes.DWORD),
48
+ ("wParamL", wintypes.WORD),
49
+ ("wParamH", wintypes.WORD),
50
+ ]
51
+
52
+
53
+ class INPUT(ctypes.Structure):
54
+ class _I(ctypes.Union):
55
+ _fields_ = [
56
+ ("ki", KEYBDINPUT),
57
+ ("mi", MOUSEINPUT),
58
+ ("hi", HARDWAREINPUT),
59
+ ]
60
+
61
+ _anonymous_ = ("i",)
62
+ _fields_ = [
63
+ ("type", wintypes.DWORD),
64
+ ("i", _I),
65
+ ]
66
+
67
+
68
+ # Configure SendInput with proper argtypes for 64-bit compatibility
69
+ SendInput = ctypes.windll.user32.SendInput
70
+ SendInput.argtypes = [wintypes.UINT, ctypes.POINTER(INPUT), ctypes.c_int]
71
+ SendInput.restype = wintypes.UINT
72
+
73
+
74
+ def typewrite_exact(text: str, interval: float = 0.01) -> None:
75
+ """Type text exactly using Unicode input - ignores capslock, keyboard layout, etc.
76
+
77
+ This function uses SendInput with KEYEVENTF_UNICODE to send characters
78
+ directly by their Unicode codepoint, completely bypassing keyboard state
79
+ (capslock, layout, etc.).
80
+
81
+ Args:
82
+ text: The text to type exactly as specified
83
+ interval: Time in seconds between each character (default: 0.01)
84
+ """
85
+ for char in text:
86
+ inputs = (INPUT * 2)()
87
+
88
+ # Key down
89
+ inputs[0].type = INPUT_KEYBOARD
90
+ inputs[0].ki.wScan = ord(char)
91
+ inputs[0].ki.dwFlags = KEYEVENTF_UNICODE
92
+
93
+ # Key up
94
+ inputs[1].type = INPUT_KEYBOARD
95
+ inputs[1].ki.wScan = ord(char)
96
+ inputs[1].ki.dwFlags = KEYEVENTF_UNICODE | KEYEVENTF_KEYUP
97
+
98
+ SendInput(2, inputs, ctypes.sizeof(INPUT))
99
+
100
+ if interval > 0:
101
+ time.sleep(interval)
@@ -29,6 +29,14 @@ class AsyncPyautoguiActionHandler:
29
29
  self.sync_handler = PyautoguiActionHandler(config=config)
30
30
  self.config = config or PyautoguiConfig()
31
31
 
32
+ def reset(self):
33
+ """Reset handler state.
34
+
35
+ Delegates to the underlying synchronous handler's reset method.
36
+ Called at automation start/end and when FINISH action is received.
37
+ """
38
+ self.sync_handler.reset()
39
+
32
40
  async def __call__(self, actions: list[Action]) -> None:
33
41
  """
34
42
  Execute actions asynchronously using a thread pool executor.
@@ -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
+
10
+ class CapsLockManager:
11
+ """Manages caps lock state for text transformation.
12
+
13
+ This class maintains an internal caps lock state that can be toggled
14
+ independently of the system's caps lock state. This allows for consistent
15
+ text case handling during automation regardless of the system state.
16
+ """
17
+
18
+ def __init__(self, mode: str = "session"):
19
+ """Initialize caps lock manager.
20
+
21
+ Args:
22
+ mode: Either "session" (internal state) or "system" (OS-level)
23
+ """
24
+ self.mode = mode
25
+ self.caps_enabled = False
26
+
27
+ def reset(self):
28
+ """Reset caps lock state to default (off).
29
+
30
+ Called at automation start/end and when FINISH action is received.
31
+ """
32
+ self.caps_enabled = False
33
+
34
+ def toggle(self):
35
+ """Toggle caps lock state in session mode."""
36
+ if self.mode == "session":
37
+ self.caps_enabled = not self.caps_enabled
38
+
39
+ def transform_text(self, text: str) -> str:
40
+ """Transform text based on caps lock state.
41
+
42
+ Args:
43
+ text: Input text to transform
44
+
45
+ Returns:
46
+ Transformed text (uppercase alphabets if caps enabled in session mode)
47
+ """
48
+ if self.mode == "session" and self.caps_enabled:
49
+ # Transform letters to uppercase, preserve special characters
50
+ return "".join(c.upper() if c.isalpha() else c for c in text)
51
+ return text
52
+
53
+ def should_use_system_capslock(self) -> bool:
54
+ """Check if system-level caps lock should be used."""
55
+ return self.mode == "system"
@@ -13,48 +13,15 @@ from pydantic import BaseModel, Field
13
13
 
14
14
  from ..exceptions import check_optional_dependency
15
15
  from ..types import Action, ActionType, parse_coords, parse_drag_coords, parse_scroll
16
+ from .capslock_manager import CapsLockManager
16
17
 
17
18
  check_optional_dependency("pyautogui", "PyautoguiActionHandler", "desktop")
18
19
  import pyautogui # noqa: E402
19
20
 
20
21
  if sys.platform == "darwin":
21
22
  from . import _macos
22
-
23
-
24
- class CapsLockManager:
25
- """Manages caps lock state for text transformation."""
26
-
27
- def __init__(self, mode: str = "session"):
28
- """Initialize caps lock manager.
29
-
30
- Args:
31
- mode: Either "session" (internal state) or "system" (OS-level)
32
- """
33
- self.mode = mode
34
- self.caps_enabled = False
35
-
36
- def toggle(self):
37
- """Toggle caps lock state in session mode."""
38
- if self.mode == "session":
39
- self.caps_enabled = not self.caps_enabled
40
-
41
- def transform_text(self, text: str) -> str:
42
- """Transform text based on caps lock state.
43
-
44
- Args:
45
- text: Input text to transform
46
-
47
- Returns:
48
- Transformed text (uppercase if caps enabled in session mode)
49
- """
50
- if self.mode == "session" and self.caps_enabled:
51
- # Transform letters to uppercase, preserve special characters
52
- return "".join(c.upper() if c.isalpha() else c for c in text)
53
- return text
54
-
55
- def should_use_system_capslock(self) -> bool:
56
- """Check if system-level caps lock should be used."""
57
- return self.mode == "system"
23
+ elif sys.platform == "win32":
24
+ from . import _windows
58
25
 
59
26
 
60
27
  class PyautoguiConfig(BaseModel):
@@ -111,6 +78,14 @@ class PyautoguiActionHandler:
111
78
  # Initialize caps lock manager
112
79
  self.caps_manager = CapsLockManager(mode=self.config.capslock_mode)
113
80
 
81
+ def reset(self):
82
+ """Reset handler state.
83
+
84
+ Called at automation start/end and when FINISH action is received.
85
+ Resets the internal capslock state.
86
+ """
87
+ self.caps_manager.reset()
88
+
114
89
  def _denormalize_coords(self, x: float, y: float) -> tuple[int, int]:
115
90
  """Convert coordinates from 0-1000 range to actual screen coordinates.
116
91
 
@@ -238,7 +213,14 @@ class PyautoguiActionHandler:
238
213
  text = arg.strip("\"'")
239
214
  # Apply caps lock transformation if needed
240
215
  text = self.caps_manager.transform_text(text)
241
- pyautogui.typewrite(text)
216
+ # Use platform-specific typing that ignores system capslock
217
+ if sys.platform == "darwin":
218
+ _macos.typewrite_exact(text)
219
+ elif sys.platform == "win32":
220
+ _windows.typewrite_exact(text)
221
+ else:
222
+ # Fallback for other platforms
223
+ pyautogui.typewrite(text)
242
224
 
243
225
  case ActionType.SCROLL:
244
226
  x, y, direction = self._parse_scroll(arg)
@@ -251,8 +233,8 @@ class PyautoguiActionHandler:
251
233
  pyautogui.scroll(scroll_amount)
252
234
 
253
235
  case ActionType.FINISH:
254
- # Task completion - no action needed
255
- pass
236
+ # Task completion - reset handler state
237
+ self.reset()
256
238
 
257
239
  case ActionType.WAIT:
258
240
  # Wait for a short period
@@ -11,7 +11,7 @@ from datetime import datetime
11
11
  from typing import Any
12
12
  from uuid import uuid4
13
13
 
14
- from ..constants import MODE_ACTOR, MODEL_ACTOR
14
+ from ..constants import DEFAULT_TEMPERATURE_LOW, MODE_ACTOR, MODEL_ACTOR
15
15
 
16
16
 
17
17
  class Session:
@@ -21,7 +21,7 @@ class Session:
21
21
  instruction: str,
22
22
  mode: str = MODE_ACTOR,
23
23
  model: str = MODEL_ACTOR,
24
- temperature: float = 0.0,
24
+ temperature: float = DEFAULT_TEMPERATURE_LOW,
25
25
  ):
26
26
  self.session_id: str = session_id
27
27
  self.instruction: str = instruction
@@ -57,7 +57,7 @@ class SessionStore:
57
57
  instruction: str,
58
58
  mode: str = MODE_ACTOR,
59
59
  model: str = MODEL_ACTOR,
60
- temperature: float = 0.0,
60
+ temperature: float = DEFAULT_TEMPERATURE_LOW,
61
61
  session_id: str | None = None,
62
62
  ) -> str:
63
63
  if session_id is None:
@@ -158,8 +158,8 @@ class SessionNamespace(socketio.AsyncNamespace):
158
158
  session_store.update_activity(session_id)
159
159
 
160
160
  logger.info(
161
- f"Session {session_id} initialized with: {event_data.instruction} "
162
- f"(mode={event_data.mode}, model={event_data.model})"
161
+ f"Session {session_id} initialized with: {session.instruction} "
162
+ f"(mode={session.mode}, model={session.model})"
163
163
  )
164
164
 
165
165
  # Create agent and wrappers
@@ -168,8 +168,8 @@ class SessionNamespace(socketio.AsyncNamespace):
168
168
  api_key=self.config.oagi_api_key,
169
169
  base_url=self.config.oagi_base_url,
170
170
  max_steps=self.config.max_steps,
171
- model=event_data.model,
172
- temperature=event_data.temperature,
171
+ model=session.model,
172
+ temperature=session.temperature,
173
173
  )
174
174
 
175
175
  action_handler = SocketIOActionHandler(self, session)
oagi/task/__init__.py CHANGED
@@ -6,16 +6,30 @@
6
6
  # Licensed under the MIT License.
7
7
  # -----------------------------------------------------------------------------
8
8
 
9
- from .async_ import AsyncActor, AsyncTask
10
- from .async_short import AsyncShortTask
11
- from .short import ShortTask
12
- from .sync import Actor, Task
9
+ """Deprecated: Use oagi.actor instead. This module will be removed in a future version."""
10
+
11
+ import warnings
12
+
13
+ from oagi.actor import (
14
+ Actor,
15
+ AsyncActor,
16
+ AsyncShortTask,
17
+ AsyncTask,
18
+ ShortTask,
19
+ Task,
20
+ )
21
+
22
+ warnings.warn(
23
+ "oagi.task is deprecated, use oagi.actor instead",
24
+ DeprecationWarning,
25
+ stacklevel=2,
26
+ )
13
27
 
14
28
  __all__ = [
15
29
  "Actor",
16
30
  "AsyncActor",
17
- "Task", # Deprecated: Use Actor instead
18
- "AsyncTask", # Deprecated: Use AsyncActor instead
19
- "ShortTask", # Deprecated
20
- "AsyncShortTask", # Deprecated
31
+ "Task",
32
+ "AsyncTask",
33
+ "ShortTask",
34
+ "AsyncShortTask",
21
35
  ]
oagi/types/__init__.py CHANGED
@@ -32,7 +32,7 @@ from .step_observer import (
32
32
  SplitEvent,
33
33
  StepEvent,
34
34
  )
35
- from .url import URL
35
+ from .url import URL, extract_uuid_from_url
36
36
 
37
37
  __all__ = [
38
38
  "Action",
@@ -55,6 +55,7 @@ __all__ = [
55
55
  "ImageProvider",
56
56
  "AsyncImageProvider",
57
57
  "URL",
58
+ "extract_uuid_from_url",
58
59
  "parse_coords",
59
60
  "parse_drag_coords",
60
61
  "parse_scroll",
@@ -17,7 +17,6 @@ from .client import (
17
17
  ErrorDetail,
18
18
  ErrorResponse,
19
19
  GenerateResponse,
20
- LLMResponse,
21
20
  UploadFileResponse,
22
21
  Usage,
23
22
  )
@@ -31,7 +30,6 @@ __all__ = [
31
30
  "ErrorResponse",
32
31
  "GenerateResponse",
33
32
  "ImageConfig",
34
- "LLMResponse",
35
33
  "Step",
36
34
  "UploadFileResponse",
37
35
  "Usage",
@@ -81,4 +81,7 @@ def parse_scroll(args_str: str) -> tuple[int, int, str] | None:
81
81
  match = re.match(r"(\d+),\s*(\d+),\s*(\w+)", args_str)
82
82
  if not match:
83
83
  return None
84
- return int(match.group(1)), int(match.group(2)), match.group(3).lower()
84
+ direction = match.group(3).lower()
85
+ if direction not in ("up", "down"):
86
+ return None
87
+ return int(match.group(1)), int(match.group(2)), direction
@@ -8,8 +8,6 @@
8
8
 
9
9
  from pydantic import BaseModel, Field
10
10
 
11
- from .action import Action
12
-
13
11
 
14
12
  class Usage(BaseModel):
15
13
  prompt_tokens: int
@@ -30,21 +28,6 @@ class ErrorResponse(BaseModel):
30
28
  error: ErrorDetail | None
31
29
 
32
30
 
33
- class LLMResponse(BaseModel):
34
- id: str
35
- task_id: str
36
- object: str = "task.completion"
37
- created: int
38
- model: str
39
- task_description: str
40
- is_complete: bool
41
- actions: list[Action]
42
- reason: str | None = None
43
- usage: Usage
44
- error: ErrorDetail | None = None
45
- raw_output: str | None = None
46
-
47
-
48
31
  class UploadFileResponse(BaseModel):
49
32
  """Response from S3 presigned URL upload."""
50
33
 
@@ -66,3 +49,4 @@ class GenerateResponse(BaseModel):
66
49
  deprecated=True,
67
50
  description="This field is deprecated",
68
51
  )
52
+ request_id: str | None = None
@@ -35,6 +35,7 @@ class StepEvent(BaseEvent):
35
35
  step_num: int
36
36
  image: bytes | str
37
37
  step: Step
38
+ task_id: str | None = None
38
39
 
39
40
 
40
41
  class ActionEvent(BaseEvent):
@@ -68,6 +69,7 @@ class PlanEvent(BaseEvent):
68
69
  image: bytes | str | None = None
69
70
  reasoning: str
70
71
  result: str | None = None
72
+ request_id: str | None = None
71
73
 
72
74
 
73
75
  ObserverEvent = ImageEvent | StepEvent | ActionEvent | LogEvent | SplitEvent | PlanEvent
oagi/types/url.py CHANGED
@@ -1,3 +1,28 @@
1
+ import re
1
2
  from typing import NewType
2
3
 
3
4
  URL = NewType("URL", str)
5
+
6
+ # Pattern to extract UUID from S3 URL
7
+ # Formats:
8
+ # - https://bucket.s3.amazonaws.com/{user_id}/{uuid}.{ext} (download URL)
9
+ # - https://bucket.s3.amazonaws.com/{user_id}/{uuid}?... (presigned URL)
10
+ _UUID_PATTERN = re.compile(
11
+ r"/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?:\.[a-z]+)?(?:\?|$)",
12
+ re.IGNORECASE,
13
+ )
14
+
15
+
16
+ def extract_uuid_from_url(url: str) -> str | None:
17
+ """Extract UUID from S3 URL.
18
+
19
+ Args:
20
+ url: S3 URL in one of these formats:
21
+ - https://bucket.s3.amazonaws.com/{user_id}/{uuid}.jpg (download URL)
22
+ - https://bucket.s3.amazonaws.com/{user_id}/{uuid}?... (presigned URL)
23
+
24
+ Returns:
25
+ UUID string if found, None otherwise
26
+ """
27
+ match = _UUID_PATTERN.search(url)
28
+ return match.group(1) if match else None
oagi/utils/__init__.py ADDED
@@ -0,0 +1,12 @@
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 .output_parser import parse_raw_output
10
+ from .prompt_builder import build_prompt
11
+
12
+ __all__ = ["build_prompt", "parse_raw_output"]