oagi 0.0.0__py3-none-any.whl → 0.1.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.
Potentially problematic release.
This version of oagi might be problematic. Click here for more details.
- oagi/__init__.py +13 -1
- oagi/logging.py +45 -0
- oagi/pyautogui_action_handler.py +146 -0
- oagi/screenshot_maker.py +73 -0
- oagi/short_task.py +122 -0
- oagi/sync_client.py +183 -0
- oagi/types/__init__.py +14 -0
- oagi/types/action_handler.py +30 -0
- oagi/types/image.py +16 -0
- oagi/types/image_provider.py +34 -0
- oagi/types/models/__init__.py +12 -0
- oagi/types/models/action.py +32 -0
- oagi/types/models/step.py +17 -0
- {oagi-0.0.0.dist-info → oagi-0.1.0.dist-info}/METADATA +35 -31
- oagi-0.1.0.dist-info/RECORD +17 -0
- {oagi-0.0.0.dist-info → oagi-0.1.0.dist-info}/WHEEL +1 -2
- {oagi-0.0.0.dist-info → oagi-0.1.0.dist-info}/licenses/LICENSE +21 -21
- oagi/core.py +0 -2
- oagi-0.0.0.dist-info/RECORD +0 -7
- oagi-0.0.0.dist-info/top_level.txt +0 -1
oagi/__init__.py
CHANGED
|
@@ -1 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
# -----------------------------------------------------------------------------
|
|
2
|
+
# Copyright (c) OpenAGI Foundation
|
|
3
|
+
# All rights reserved.
|
|
4
|
+
#
|
|
5
|
+
# This file is part of the official API project.
|
|
6
|
+
# Licensed under the MIT License.
|
|
7
|
+
# -----------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
from oagi.pyautogui_action_handler import PyautoguiActionHandler
|
|
10
|
+
from oagi.screenshot_maker import ScreenshotMaker
|
|
11
|
+
from oagi.short_task import ShortTask
|
|
12
|
+
|
|
13
|
+
__all__ = ["ShortTask", "PyautoguiActionHandler", "ScreenshotMaker"]
|
oagi/logging.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
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
|
+
|
|
42
|
+
# Always update level in case environment variable changed
|
|
43
|
+
oagi_root.setLevel(level)
|
|
44
|
+
|
|
45
|
+
return logger
|
|
@@ -0,0 +1,146 @@
|
|
|
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
|
+
import pyautogui
|
|
13
|
+
|
|
14
|
+
from .types import Action, ActionType
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PyautoguiActionHandler:
|
|
18
|
+
"""
|
|
19
|
+
Handles actions to be executed using PyAutoGUI.
|
|
20
|
+
|
|
21
|
+
This class provides functionality for handling and executing a sequence of
|
|
22
|
+
actions using the PyAutoGUI library. It processes a list of actions and executes
|
|
23
|
+
them as per the implementation.
|
|
24
|
+
|
|
25
|
+
Methods:
|
|
26
|
+
__call__: Executes the provided list of actions.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
actions (list[Action]): List of actions to be processed and executed.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self):
|
|
33
|
+
# Get screen dimensions for coordinate denormalization
|
|
34
|
+
self.screen_width, self.screen_height = pyautogui.size()
|
|
35
|
+
# Set default delay between actions
|
|
36
|
+
pyautogui.PAUSE = 0.1
|
|
37
|
+
|
|
38
|
+
def _denormalize_coords(self, x: float, y: float) -> tuple[int, int]:
|
|
39
|
+
"""Convert coordinates from 0-1000 range to actual screen coordinates."""
|
|
40
|
+
screen_x = int(x * self.screen_width / 1000)
|
|
41
|
+
screen_y = int(y * self.screen_height / 1000)
|
|
42
|
+
return screen_x, screen_y
|
|
43
|
+
|
|
44
|
+
def _parse_coords(self, args_str: str) -> tuple[int, int]:
|
|
45
|
+
"""Extract x, y coordinates from argument string."""
|
|
46
|
+
match = re.match(r"(\d+),\s*(\d+)", args_str)
|
|
47
|
+
if not match:
|
|
48
|
+
raise ValueError(f"Invalid coordinates format: {args_str}")
|
|
49
|
+
x, y = int(match.group(1)), int(match.group(2))
|
|
50
|
+
return self._denormalize_coords(x, y)
|
|
51
|
+
|
|
52
|
+
def _parse_drag_coords(self, args_str: str) -> tuple[int, int, int, int]:
|
|
53
|
+
"""Extract x1, y1, x2, y2 coordinates from drag argument string."""
|
|
54
|
+
match = re.match(r"(\d+),\s*(\d+),\s*(\d+),\s*(\d+)", args_str)
|
|
55
|
+
if not match:
|
|
56
|
+
raise ValueError(f"Invalid drag coordinates format: {args_str}")
|
|
57
|
+
x1, y1, x2, y2 = (
|
|
58
|
+
int(match.group(1)),
|
|
59
|
+
int(match.group(2)),
|
|
60
|
+
int(match.group(3)),
|
|
61
|
+
int(match.group(4)),
|
|
62
|
+
)
|
|
63
|
+
x1, y1 = self._denormalize_coords(x1, y1)
|
|
64
|
+
x2, y2 = self._denormalize_coords(x2, y2)
|
|
65
|
+
return x1, y1, x2, y2
|
|
66
|
+
|
|
67
|
+
def _parse_scroll(self, args_str: str) -> tuple[int, int, str]:
|
|
68
|
+
"""Extract x, y, direction from scroll argument string."""
|
|
69
|
+
match = re.match(r"(\d+),\s*(\d+),\s*(\w+)", args_str)
|
|
70
|
+
if not match:
|
|
71
|
+
raise ValueError(f"Invalid scroll format: {args_str}")
|
|
72
|
+
x, y = int(match.group(1)), int(match.group(2))
|
|
73
|
+
x, y = self._denormalize_coords(x, y)
|
|
74
|
+
direction = match.group(3).lower()
|
|
75
|
+
return x, y, direction
|
|
76
|
+
|
|
77
|
+
def _parse_hotkey(self, args_str: str) -> list[str]:
|
|
78
|
+
"""Parse hotkey string into list of keys."""
|
|
79
|
+
# Remove parentheses if present
|
|
80
|
+
args_str = args_str.strip("()")
|
|
81
|
+
# Split by '+' to get individual keys
|
|
82
|
+
keys = [key.strip() for key in args_str.split("+")]
|
|
83
|
+
return keys
|
|
84
|
+
|
|
85
|
+
def _execute_action(self, action: Action) -> None:
|
|
86
|
+
"""Execute a single action."""
|
|
87
|
+
count = action.count or 1
|
|
88
|
+
arg = action.argument.strip("()") # Remove outer parentheses if present
|
|
89
|
+
|
|
90
|
+
for _ in range(count):
|
|
91
|
+
match action.type:
|
|
92
|
+
case ActionType.CLICK:
|
|
93
|
+
x, y = self._parse_coords(arg)
|
|
94
|
+
pyautogui.click(x, y)
|
|
95
|
+
|
|
96
|
+
case ActionType.LEFT_DOUBLE:
|
|
97
|
+
x, y = self._parse_coords(arg)
|
|
98
|
+
pyautogui.doubleClick(x, y)
|
|
99
|
+
|
|
100
|
+
case ActionType.RIGHT_SINGLE:
|
|
101
|
+
x, y = self._parse_coords(arg)
|
|
102
|
+
pyautogui.rightClick(x, y)
|
|
103
|
+
|
|
104
|
+
case ActionType.DRAG:
|
|
105
|
+
x1, y1, x2, y2 = self._parse_drag_coords(arg)
|
|
106
|
+
pyautogui.moveTo(x1, y1)
|
|
107
|
+
pyautogui.dragTo(x2, y2, duration=0.5, button="left")
|
|
108
|
+
|
|
109
|
+
case ActionType.HOTKEY:
|
|
110
|
+
keys = self._parse_hotkey(arg)
|
|
111
|
+
pyautogui.hotkey(*keys)
|
|
112
|
+
|
|
113
|
+
case ActionType.TYPE:
|
|
114
|
+
# Remove quotes if present
|
|
115
|
+
text = arg.strip("\"'")
|
|
116
|
+
pyautogui.typewrite(text)
|
|
117
|
+
|
|
118
|
+
case ActionType.SCROLL:
|
|
119
|
+
x, y, direction = self._parse_scroll(arg)
|
|
120
|
+
pyautogui.moveTo(x, y)
|
|
121
|
+
scroll_amount = 5 if direction == "up" else -5
|
|
122
|
+
pyautogui.scroll(scroll_amount)
|
|
123
|
+
|
|
124
|
+
case ActionType.FINISH:
|
|
125
|
+
# Task completion - no action needed
|
|
126
|
+
pass
|
|
127
|
+
|
|
128
|
+
case ActionType.WAIT:
|
|
129
|
+
# Wait for a short period
|
|
130
|
+
time.sleep(1)
|
|
131
|
+
|
|
132
|
+
case ActionType.CALL_USER:
|
|
133
|
+
# Call user - implementation depends on requirements
|
|
134
|
+
print("User intervention requested")
|
|
135
|
+
|
|
136
|
+
case _:
|
|
137
|
+
print(f"Unknown action type: {action.type}")
|
|
138
|
+
|
|
139
|
+
def __call__(self, actions: list[Action]) -> None:
|
|
140
|
+
"""Execute the provided list of actions."""
|
|
141
|
+
for action in actions:
|
|
142
|
+
try:
|
|
143
|
+
self._execute_action(action)
|
|
144
|
+
except Exception as e:
|
|
145
|
+
print(f"Error executing action {action.type}: {e}")
|
|
146
|
+
raise
|
oagi/screenshot_maker.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
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 io
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
import pyautogui
|
|
13
|
+
|
|
14
|
+
from .types import Image
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class FileImage:
|
|
18
|
+
def __init__(self, path: str):
|
|
19
|
+
self.path = path
|
|
20
|
+
with open(path, "rb") as f:
|
|
21
|
+
self.data = f.read()
|
|
22
|
+
|
|
23
|
+
def read(self) -> bytes:
|
|
24
|
+
return self.data
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class MockImage:
|
|
28
|
+
def read(self) -> bytes:
|
|
29
|
+
return b"mock screenshot data"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ScreenshotImage:
|
|
33
|
+
"""Image class that wraps a pyautogui screenshot."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, screenshot):
|
|
36
|
+
"""Initialize with a PIL Image from pyautogui."""
|
|
37
|
+
self.screenshot = screenshot
|
|
38
|
+
self._cached_bytes: Optional[bytes] = None
|
|
39
|
+
|
|
40
|
+
def read(self) -> bytes:
|
|
41
|
+
"""Convert the screenshot to bytes (PNG format)."""
|
|
42
|
+
if self._cached_bytes is None:
|
|
43
|
+
# Convert PIL Image to bytes
|
|
44
|
+
buffer = io.BytesIO()
|
|
45
|
+
self.screenshot.save(buffer, format="PNG")
|
|
46
|
+
self._cached_bytes = buffer.getvalue()
|
|
47
|
+
return self._cached_bytes
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ScreenshotMaker:
|
|
51
|
+
"""Takes screenshots using pyautogui."""
|
|
52
|
+
|
|
53
|
+
def __init__(self):
|
|
54
|
+
self._last_screenshot: Optional[ScreenshotImage] = None
|
|
55
|
+
|
|
56
|
+
def __call__(self) -> Image:
|
|
57
|
+
"""Take a screenshot and return it as an Image."""
|
|
58
|
+
# Take a screenshot using pyautogui
|
|
59
|
+
screenshot = pyautogui.screenshot()
|
|
60
|
+
|
|
61
|
+
# Wrap it in our ScreenshotImage class
|
|
62
|
+
screenshot_image = ScreenshotImage(screenshot)
|
|
63
|
+
|
|
64
|
+
# Store as the last screenshot
|
|
65
|
+
self._last_screenshot = screenshot_image
|
|
66
|
+
|
|
67
|
+
return screenshot_image
|
|
68
|
+
|
|
69
|
+
def last_image(self) -> Image:
|
|
70
|
+
"""Return the last screenshot taken, or take a new one if none exists."""
|
|
71
|
+
if self._last_screenshot is None:
|
|
72
|
+
return self()
|
|
73
|
+
return self._last_screenshot
|
oagi/short_task.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# -----------------------------------------------------------------------------
|
|
2
|
+
# Copyright (c) OpenAGI Foundation
|
|
3
|
+
# All rights reserved.
|
|
4
|
+
#
|
|
5
|
+
# This file is part of the official API project.
|
|
6
|
+
# Licensed under the MIT License.
|
|
7
|
+
# -----------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
from .logging import get_logger
|
|
10
|
+
from .sync_client import SyncClient, encode_screenshot_from_bytes
|
|
11
|
+
from .types import ActionHandler, Image, ImageProvider, Step
|
|
12
|
+
|
|
13
|
+
logger = get_logger("short_task")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ShortTask:
|
|
17
|
+
def __init__(self, api_key: str | None = None, base_url: str | None = None):
|
|
18
|
+
self.client = SyncClient(base_url=base_url, api_key=api_key)
|
|
19
|
+
self.api_key = self.client.api_key
|
|
20
|
+
self.base_url = self.client.base_url
|
|
21
|
+
self.task_id: str | None = None
|
|
22
|
+
self.task_description: str | None = None
|
|
23
|
+
self.model = "vision-model-v1" # default model
|
|
24
|
+
|
|
25
|
+
def init_task(self, task_desc: str, max_steps: int = 5):
|
|
26
|
+
"""Initialize a new task with the given description."""
|
|
27
|
+
self.task_description = task_desc
|
|
28
|
+
response = self.client.create_message(
|
|
29
|
+
model=self.model,
|
|
30
|
+
screenshot="",
|
|
31
|
+
task_description=self.task_description,
|
|
32
|
+
task_id=None,
|
|
33
|
+
)
|
|
34
|
+
self.task_id = response.task_id # Reset task_id for new task
|
|
35
|
+
logger.info(f"Task initialized: '{task_desc}' (max_steps: {max_steps})")
|
|
36
|
+
|
|
37
|
+
def step(self, screenshot: Image) -> Step:
|
|
38
|
+
"""Send screenshot to the server and get the next actions."""
|
|
39
|
+
if not self.task_description:
|
|
40
|
+
raise ValueError("Task description must be set. Call init_task() first.")
|
|
41
|
+
|
|
42
|
+
logger.debug(f"Executing step for task: '{self.task_description}'")
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
# Convert Image to bytes using the protocol
|
|
46
|
+
screenshot_bytes = screenshot.read()
|
|
47
|
+
screenshot_b64 = encode_screenshot_from_bytes(screenshot_bytes)
|
|
48
|
+
|
|
49
|
+
# Call API
|
|
50
|
+
response = self.client.create_message(
|
|
51
|
+
model=self.model,
|
|
52
|
+
screenshot=screenshot_b64,
|
|
53
|
+
task_description=self.task_description,
|
|
54
|
+
task_id=self.task_id,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Update task_id from response
|
|
58
|
+
if self.task_id != response.task_id:
|
|
59
|
+
if self.task_id is None:
|
|
60
|
+
logger.debug(f"Task ID assigned: {response.task_id}")
|
|
61
|
+
else:
|
|
62
|
+
logger.debug(
|
|
63
|
+
f"Task ID changed: {self.task_id} -> {response.task_id}"
|
|
64
|
+
)
|
|
65
|
+
self.task_id = response.task_id
|
|
66
|
+
|
|
67
|
+
# Convert API response to Step
|
|
68
|
+
result = Step(
|
|
69
|
+
reason=response.reason,
|
|
70
|
+
actions=response.actions,
|
|
71
|
+
stop=response.is_complete,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
if response.is_complete:
|
|
75
|
+
logger.info(f"Task completed after {response.current_step} steps")
|
|
76
|
+
else:
|
|
77
|
+
logger.debug(
|
|
78
|
+
f"Step {response.current_step} completed with {len(response.actions)} actions"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
return result
|
|
82
|
+
|
|
83
|
+
except Exception as e:
|
|
84
|
+
logger.error(f"Error during step execution: {e}")
|
|
85
|
+
raise
|
|
86
|
+
|
|
87
|
+
def close(self):
|
|
88
|
+
"""Close the underlying HTTP client to free resources."""
|
|
89
|
+
self.client.close()
|
|
90
|
+
|
|
91
|
+
def __enter__(self):
|
|
92
|
+
return self
|
|
93
|
+
|
|
94
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
95
|
+
self.close()
|
|
96
|
+
|
|
97
|
+
def auto_mode(
|
|
98
|
+
self,
|
|
99
|
+
task_desc: str,
|
|
100
|
+
max_steps: int = 5,
|
|
101
|
+
executor: ActionHandler = None,
|
|
102
|
+
image_provider: ImageProvider = None,
|
|
103
|
+
) -> bool:
|
|
104
|
+
"""Run the task in automatic mode with the provided executor and image provider."""
|
|
105
|
+
logger.info(
|
|
106
|
+
f"Starting auto mode for task: '{task_desc}' (max_steps: {max_steps})"
|
|
107
|
+
)
|
|
108
|
+
self.init_task(task_desc, max_steps=max_steps)
|
|
109
|
+
|
|
110
|
+
for i in range(max_steps):
|
|
111
|
+
logger.debug(f"Auto mode step {i + 1}/{max_steps}")
|
|
112
|
+
image = image_provider()
|
|
113
|
+
step = self.step(image)
|
|
114
|
+
if step.stop:
|
|
115
|
+
logger.info(f"Auto mode completed successfully after {i + 1} steps")
|
|
116
|
+
return True
|
|
117
|
+
if executor:
|
|
118
|
+
logger.debug(f"Executing {len(step.actions)} actions")
|
|
119
|
+
executor(step.actions)
|
|
120
|
+
|
|
121
|
+
logger.warning(f"Auto mode reached max steps ({max_steps}) without completion")
|
|
122
|
+
return False
|
oagi/sync_client.py
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
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 base64
|
|
10
|
+
import os
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
import httpx
|
|
14
|
+
from pydantic import BaseModel
|
|
15
|
+
|
|
16
|
+
from .logging import get_logger
|
|
17
|
+
from .types import Action
|
|
18
|
+
|
|
19
|
+
logger = get_logger("sync_client")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Usage(BaseModel):
|
|
23
|
+
prompt_tokens: int
|
|
24
|
+
completion_tokens: int
|
|
25
|
+
total_tokens: int
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class LLMResponse(BaseModel):
|
|
29
|
+
id: str
|
|
30
|
+
task_id: str
|
|
31
|
+
object: str = "task.completion"
|
|
32
|
+
created: int
|
|
33
|
+
model: str
|
|
34
|
+
task_description: str
|
|
35
|
+
current_step: int
|
|
36
|
+
is_complete: bool
|
|
37
|
+
actions: list[Action]
|
|
38
|
+
reason: str | None = None
|
|
39
|
+
usage: Usage
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ErrorResponse(BaseModel):
|
|
43
|
+
error: str
|
|
44
|
+
message: str
|
|
45
|
+
code: int
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class SyncClient:
|
|
49
|
+
def __init__(self, base_url: Optional[str] = None, api_key: Optional[str] = None):
|
|
50
|
+
# Get from environment if not provided
|
|
51
|
+
self.base_url = base_url or os.getenv("OAGI_BASE_URL")
|
|
52
|
+
self.api_key = api_key or os.getenv("OAGI_API_KEY")
|
|
53
|
+
|
|
54
|
+
# Validate required configuration
|
|
55
|
+
if not self.base_url:
|
|
56
|
+
raise ValueError(
|
|
57
|
+
"OAGI base URL must be provided either as 'base_url' parameter or "
|
|
58
|
+
"OAGI_BASE_URL environment variable"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if not self.api_key:
|
|
62
|
+
raise ValueError(
|
|
63
|
+
"OAGI API key must be provided either as 'api_key' parameter or "
|
|
64
|
+
"OAGI_API_KEY environment variable"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
self.base_url = self.base_url.rstrip("/")
|
|
68
|
+
self.client = httpx.Client(base_url=self.base_url)
|
|
69
|
+
self.timeout = 60
|
|
70
|
+
|
|
71
|
+
logger.info(f"SyncClient initialized with base_url: {self.base_url}")
|
|
72
|
+
|
|
73
|
+
def __enter__(self):
|
|
74
|
+
return self
|
|
75
|
+
|
|
76
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
77
|
+
self.client.close()
|
|
78
|
+
|
|
79
|
+
def close(self):
|
|
80
|
+
"""Close the underlying httpx client"""
|
|
81
|
+
self.client.close()
|
|
82
|
+
|
|
83
|
+
def create_message(
|
|
84
|
+
self,
|
|
85
|
+
model: str,
|
|
86
|
+
screenshot: str, # base64 encoded
|
|
87
|
+
task_description: Optional[str] = None,
|
|
88
|
+
task_id: Optional[str] = None,
|
|
89
|
+
max_actions: Optional[int] = 5,
|
|
90
|
+
api_version: Optional[str] = None,
|
|
91
|
+
) -> LLMResponse:
|
|
92
|
+
"""
|
|
93
|
+
Call the /v1/message endpoint to analyze task and screenshot
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
model: The model to use for task analysis
|
|
97
|
+
screenshot: Base64-encoded screenshot image
|
|
98
|
+
task_description: Description of the task (required for new sessions)
|
|
99
|
+
task_id: Task ID for continuing existing task
|
|
100
|
+
max_actions: Maximum number of actions to return (1-20)
|
|
101
|
+
api_version: API version header
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
LLMResponse: The response from the API
|
|
105
|
+
|
|
106
|
+
Raises:
|
|
107
|
+
httpx.HTTPStatusError: For HTTP error responses
|
|
108
|
+
"""
|
|
109
|
+
headers = {}
|
|
110
|
+
if api_version:
|
|
111
|
+
headers["x-api-version"] = api_version
|
|
112
|
+
if self.api_key:
|
|
113
|
+
headers["x-api-key"] = self.api_key
|
|
114
|
+
|
|
115
|
+
payload = {"model": model, "screenshot": screenshot}
|
|
116
|
+
|
|
117
|
+
if task_description is not None:
|
|
118
|
+
payload["task_description"] = task_description
|
|
119
|
+
if task_id is not None:
|
|
120
|
+
payload["task_id"] = task_id
|
|
121
|
+
if max_actions is not None:
|
|
122
|
+
payload["max_actions"] = max_actions
|
|
123
|
+
|
|
124
|
+
logger.info(f"Making API request to /v1/message with model: {model}")
|
|
125
|
+
logger.debug(
|
|
126
|
+
f"Request includes task_description: {task_description is not None}, task_id: {task_id is not None}"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
response = self.client.post(
|
|
130
|
+
"/v1/message", json=payload, headers=headers, timeout=self.timeout
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
if response.status_code == 200:
|
|
134
|
+
result = LLMResponse(**response.json())
|
|
135
|
+
logger.info(
|
|
136
|
+
f"API request successful - task_id: {result.task_id}, step: {result.current_step}, complete: {result.is_complete}"
|
|
137
|
+
)
|
|
138
|
+
logger.debug(f"Response included {len(result.actions)} actions")
|
|
139
|
+
return result
|
|
140
|
+
else:
|
|
141
|
+
# Handle error responses
|
|
142
|
+
try:
|
|
143
|
+
error_data = response.json()
|
|
144
|
+
error = ErrorResponse(**error_data)
|
|
145
|
+
logger.error(f"API Error {error.code}: {error.error} - {error.message}")
|
|
146
|
+
raise httpx.HTTPStatusError(
|
|
147
|
+
f"API Error {error.code}: {error.error} - {error.message}",
|
|
148
|
+
request=response.request,
|
|
149
|
+
response=response,
|
|
150
|
+
)
|
|
151
|
+
except ValueError:
|
|
152
|
+
# If response is not JSON, raise generic error
|
|
153
|
+
logger.error(f"Non-JSON API error response: {response.status_code}")
|
|
154
|
+
response.raise_for_status()
|
|
155
|
+
|
|
156
|
+
def health_check(self) -> dict:
|
|
157
|
+
"""
|
|
158
|
+
Call the /health endpoint for health check
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
dict: Health check response
|
|
162
|
+
"""
|
|
163
|
+
logger.debug("Making health check request")
|
|
164
|
+
try:
|
|
165
|
+
response = self.client.get("/health")
|
|
166
|
+
response.raise_for_status()
|
|
167
|
+
result = response.json()
|
|
168
|
+
logger.debug("Health check successful")
|
|
169
|
+
return result
|
|
170
|
+
except httpx.HTTPStatusError as e:
|
|
171
|
+
logger.warning(f"Health check failed: {e}")
|
|
172
|
+
raise
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def encode_screenshot_from_bytes(image_bytes: bytes) -> str:
|
|
176
|
+
"""Helper function to encode image bytes to base64 string"""
|
|
177
|
+
return base64.b64encode(image_bytes).decode("utf-8")
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def encode_screenshot_from_file(image_path: str) -> str:
|
|
181
|
+
"""Helper function to encode image file to base64 string"""
|
|
182
|
+
with open(image_path, "rb") as f:
|
|
183
|
+
return encode_screenshot_from_bytes(f.read())
|
oagi/types/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
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 .action_handler import ActionHandler
|
|
10
|
+
from .image import Image
|
|
11
|
+
from .image_provider import ImageProvider
|
|
12
|
+
from .models import Action, ActionType, Step
|
|
13
|
+
|
|
14
|
+
__all__ = ["Action", "ActionType", "Image", "Step", "ActionHandler", "ImageProvider"]
|
|
@@ -0,0 +1,30 @@
|
|
|
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 Protocol
|
|
10
|
+
|
|
11
|
+
from .models import Action
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ActionHandler(Protocol):
|
|
15
|
+
def __call__(self, actions: list[Action]) -> None:
|
|
16
|
+
"""
|
|
17
|
+
Executes a list of actions.
|
|
18
|
+
|
|
19
|
+
This method takes a list of `Action` objects and executes them. It is used
|
|
20
|
+
to perform operations represented by the `Action` instances. This method
|
|
21
|
+
does not return any value and modifies the system based on the input actions.
|
|
22
|
+
|
|
23
|
+
Parameters:
|
|
24
|
+
actions (list[Action]): A list of `Action` objects to be executed. Each
|
|
25
|
+
`Action` must encapsulate the logic that is intended to be applied
|
|
26
|
+
during the call.
|
|
27
|
+
|
|
28
|
+
Raises:
|
|
29
|
+
RuntimeError: If an error occurs during the execution of the actions.
|
|
30
|
+
"""
|
oagi/types/image.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
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 Protocol
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Image(Protocol):
|
|
13
|
+
"""Protocol for image objects that can be read as bytes."""
|
|
14
|
+
|
|
15
|
+
def read(self) -> bytes:
|
|
16
|
+
"""Read the image data as bytes."""
|
|
@@ -0,0 +1,34 @@
|
|
|
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 Protocol
|
|
10
|
+
|
|
11
|
+
from .image import Image
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ImageProvider(Protocol):
|
|
15
|
+
def __call__(self) -> Image:
|
|
16
|
+
"""
|
|
17
|
+
Represents the functionality to invoke the callable object and produce an Image
|
|
18
|
+
result. Typically used to process or generate images using the defined logic
|
|
19
|
+
within the __call__ method.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
Image: The resulting image output from the callable logic.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def last_image(self) -> Image:
|
|
26
|
+
"""
|
|
27
|
+
Returns the last captured image.
|
|
28
|
+
|
|
29
|
+
This method retrieves the most recent image that was captured and stored
|
|
30
|
+
in memory. If there are no images available, the method may return None.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Image: The last captured image, or None if no images are available.
|
|
34
|
+
"""
|
|
@@ -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 .action import Action, ActionType
|
|
10
|
+
from .step import Step
|
|
11
|
+
|
|
12
|
+
__all__ = ["Action", "ActionType", "Step"]
|
|
@@ -0,0 +1,32 @@
|
|
|
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 enum import Enum
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, Field
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ActionType(str, Enum):
|
|
15
|
+
CLICK = "click"
|
|
16
|
+
LEFT_DOUBLE = "left_double"
|
|
17
|
+
RIGHT_SINGLE = "right_single"
|
|
18
|
+
DRAG = "drag"
|
|
19
|
+
HOTKEY = "hotkey"
|
|
20
|
+
TYPE = "type"
|
|
21
|
+
SCROLL = "scroll"
|
|
22
|
+
FINISH = "finish"
|
|
23
|
+
WAIT = "wait"
|
|
24
|
+
CALL_USER = "call_user"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Action(BaseModel):
|
|
28
|
+
type: ActionType = Field(..., description="Type of action to perform")
|
|
29
|
+
argument: str = Field(..., description="Action argument in the specified format")
|
|
30
|
+
count: int | None = Field(
|
|
31
|
+
default=1, ge=1, description="Number of times to repeat the action"
|
|
32
|
+
)
|
|
@@ -0,0 +1,17 @@
|
|
|
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 BaseModel
|
|
10
|
+
|
|
11
|
+
from .action import Action
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Step(BaseModel):
|
|
15
|
+
reason: str | None = None
|
|
16
|
+
actions: list[Action]
|
|
17
|
+
stop: bool = False
|
|
@@ -1,31 +1,35 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
2
|
-
Name: oagi
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: Official API of OpenAGI Foundation
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
SOFTWARE
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: oagi
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official API of OpenAGI Foundation
|
|
5
|
+
Project-URL: Homepage, https://github.com/agiopen-org/oagi
|
|
6
|
+
Author-email: OpenAGI Foundation <contact@agiopen.org>
|
|
7
|
+
License: MIT License
|
|
8
|
+
|
|
9
|
+
Copyright (c) 2025 OpenAGI Foundation
|
|
10
|
+
|
|
11
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
12
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
13
|
+
in the Software without restriction, including without limitation the rights
|
|
14
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
15
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
16
|
+
furnished to do so, subject to the following conditions:
|
|
17
|
+
|
|
18
|
+
The above copyright notice and this permission notice shall be included in all
|
|
19
|
+
copies or substantial portions of the Software.
|
|
20
|
+
|
|
21
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
22
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
23
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
24
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
25
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
26
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
27
|
+
SOFTWARE.
|
|
28
|
+
Requires-Python: >=3.10
|
|
29
|
+
Requires-Dist: httpx>=0.28.0
|
|
30
|
+
Requires-Dist: pillow>=11.3.0
|
|
31
|
+
Requires-Dist: pyautogui>=0.9.54
|
|
32
|
+
Requires-Dist: pydantic>=2.0.0
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
# OAGI Python SDK
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
oagi/__init__.py,sha256=Qk4_6TmIBIGQhFSDPbt06h-WCpW_nqZ9PYBeXu-1XDA,531
|
|
2
|
+
oagi/logging.py,sha256=CWe89mA5MKTipIvfrqSYkv2CAFNBSwHMDQMDkG_g64g,1350
|
|
3
|
+
oagi/pyautogui_action_handler.py,sha256=LBWmtqkXzZSJo07s3uOw-NWUE9rZZtbNAx0YI83pCbk,5482
|
|
4
|
+
oagi/screenshot_maker.py,sha256=lyJSMFagHeaqg59CQGMTqLvSzQN_pBbhbV2oIFG46vA,2077
|
|
5
|
+
oagi/short_task.py,sha256=cDmMjQ-rv04FH6AVHLAasgCeVKWfeteaeuJp6LZNq2s,4479
|
|
6
|
+
oagi/sync_client.py,sha256=nH68my12ujxMusV4PkSl0gDiW-vFMvUKf_jSeC4Seks,5907
|
|
7
|
+
oagi/types/__init__.py,sha256=eh-1IEqMTY2hUrvQJeTg6vsvlE6F4Iz5C0_K86AnWn8,549
|
|
8
|
+
oagi/types/action_handler.py,sha256=NH8E-m5qpGqWcXzTSWfF7W0Xdp8SkzJsbhCmQ0B96cg,1075
|
|
9
|
+
oagi/types/image.py,sha256=1vmQaOgNJuNso3WtiPN-sAP2DGjMDHwogrB0Pgi9uQ8,499
|
|
10
|
+
oagi/types/image_provider.py,sha256=oYFdOYznrK_VOR9egzOjw5wFM5w8EY2sY01pH0ANAgU,1112
|
|
11
|
+
oagi/types/models/__init__.py,sha256=4qhKxWXsXEVzD6U_RM6PXR45os765qigtZs1BsS4WHg,414
|
|
12
|
+
oagi/types/models/action.py,sha256=8Xd3IcH32ENq7uXczo-mbQ736yUOGxO_TaZTfHVRY7w,935
|
|
13
|
+
oagi/types/models/step.py,sha256=RSI4H_2rrUBq_xyCoWKaq7JHdJWNobtQppaKC1l0aWU,471
|
|
14
|
+
oagi-0.1.0.dist-info/METADATA,sha256=vVl2-HqzJKJsScHkslj4zZ7ZAjKaPw3LtB9eakqssQ8,1656
|
|
15
|
+
oagi-0.1.0.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
|
|
16
|
+
oagi-0.1.0.dist-info/licenses/LICENSE,sha256=sy5DLA2M29jFT4UfWsuBF9BAr3FnRkYtnAu6oDZiIf8,1075
|
|
17
|
+
oagi-0.1.0.dist-info/RECORD,,
|
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2025 OpenAGI Foundation
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 OpenAGI Foundation
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
oagi/core.py
DELETED
oagi-0.0.0.dist-info/RECORD
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
oagi/__init__.py,sha256=K1GAo2fdj3I1a2lP7RIu-ViQ52DJ84qV5imkQNkF0JE,25
|
|
2
|
-
oagi/core.py,sha256=eU5aYaEmmgcUSmiPnRB9njRUI4Xrij2HcPunh67T86M,57
|
|
3
|
-
oagi-0.0.0.dist-info/licenses/LICENSE,sha256=xHvNtuFT_mr6qQ1vGCphFj9r4Jc6h4VJLXTVYkFzgWM,1096
|
|
4
|
-
oagi-0.0.0.dist-info/METADATA,sha256=h-xCKLcBqz9MiIIUELPUoK1Hz60Snvb8UXdMlCsGUDQ,1574
|
|
5
|
-
oagi-0.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
6
|
-
oagi-0.0.0.dist-info/top_level.txt,sha256=t4TE_HUmY4z48HHEpgpmRYWnHdmXKzSdjzLxsx7Gkd0,5
|
|
7
|
-
oagi-0.0.0.dist-info/RECORD,,
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
oagi
|