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
oagi/cli/main.py ADDED
@@ -0,0 +1,77 @@
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 argparse
10
+ import sys
11
+
12
+ from oagi.cli.agent import add_agent_parser, handle_agent_command
13
+ from oagi.cli.server import add_server_parser, handle_server_command
14
+ from oagi.cli.utils import display_config, display_version, setup_logging
15
+
16
+
17
+ def create_parser() -> argparse.ArgumentParser:
18
+ parser = argparse.ArgumentParser(
19
+ prog="oagi", description="OAGI SDK Command Line Interface"
20
+ )
21
+
22
+ parser.add_argument(
23
+ "-v", "--verbose", action="store_true", help="Enable verbose (debug) logging"
24
+ )
25
+
26
+ parser.add_argument(
27
+ "--version",
28
+ action="version",
29
+ version="%(prog)s (use 'oagi version' for detailed info)",
30
+ )
31
+
32
+ # Create subparsers for commands
33
+ subparsers = parser.add_subparsers(dest="command", required=True)
34
+
35
+ add_server_parser(subparsers)
36
+ add_agent_parser(subparsers)
37
+
38
+ subparsers.add_parser("version", help="Show SDK version and environment info")
39
+
40
+ config_parser = subparsers.add_parser("config", help="Configuration management")
41
+ config_subparsers = config_parser.add_subparsers(
42
+ dest="config_command", required=True
43
+ )
44
+ config_subparsers.add_parser("show", help="Display current configuration")
45
+
46
+ return parser
47
+
48
+
49
+ def main() -> None:
50
+ parser = create_parser()
51
+ args = parser.parse_args()
52
+
53
+ setup_logging(args.verbose)
54
+
55
+ try:
56
+ if args.command == "server":
57
+ handle_server_command(args)
58
+ elif args.command == "agent":
59
+ handle_agent_command(args)
60
+ elif args.command == "version":
61
+ display_version()
62
+ elif args.command == "config":
63
+ if args.config_command == "show":
64
+ display_config()
65
+ else:
66
+ parser.print_help()
67
+ sys.exit(1)
68
+ except KeyboardInterrupt:
69
+ print("\nInterrupted by user.")
70
+ sys.exit(130)
71
+ except Exception as e:
72
+ print(f"Unexpected error: {e}", file=sys.stderr)
73
+ sys.exit(1)
74
+
75
+
76
+ if __name__ == "__main__":
77
+ main()
oagi/cli/server.py ADDED
@@ -0,0 +1,94 @@
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 argparse
10
+ import sys
11
+
12
+ from oagi.exceptions import check_optional_dependency
13
+
14
+
15
+ def add_server_parser(subparsers: argparse._SubParsersAction) -> None:
16
+ server_parser = subparsers.add_parser("server", help="Server management commands")
17
+ server_subparsers = server_parser.add_subparsers(
18
+ dest="server_command", required=True
19
+ )
20
+
21
+ # server start command
22
+ start_parser = server_subparsers.add_parser(
23
+ "start", help="Start the Socket.IO server"
24
+ )
25
+ start_parser.add_argument(
26
+ "--host",
27
+ type=str,
28
+ help="Server host (default: 0.0.0.0, or OAGI_SERVER_HOST env var)",
29
+ )
30
+ start_parser.add_argument(
31
+ "--port",
32
+ type=int,
33
+ help="Server port (default: 8000, or OAGI_SERVER_PORT env var)",
34
+ )
35
+ start_parser.add_argument(
36
+ "--oagi-api-key", type=str, help="OAGI API key (default: OAGI_API_KEY env var)"
37
+ )
38
+ start_parser.add_argument(
39
+ "--oagi-base-url",
40
+ type=str,
41
+ help="OAGI base URL (default: https://api.agiopen.org, or OAGI_BASE_URL env var)",
42
+ )
43
+
44
+
45
+ def handle_server_command(args: argparse.Namespace) -> None:
46
+ if args.server_command == "start":
47
+ start_server(args)
48
+
49
+
50
+ def start_server(args: argparse.Namespace) -> None:
51
+ # Check if server extras are installed
52
+ check_optional_dependency("fastapi", "Server", "server")
53
+ check_optional_dependency("uvicorn", "Server", "server")
54
+
55
+ import uvicorn # noqa: PLC0415
56
+
57
+ from oagi.server import create_app # noqa: PLC0415
58
+ from oagi.server.config import ServerConfig # noqa: PLC0415
59
+
60
+ # Create config with CLI overrides
61
+ config_kwargs = {}
62
+ if args.oagi_api_key:
63
+ config_kwargs["oagi_api_key"] = args.oagi_api_key
64
+ if args.oagi_base_url:
65
+ config_kwargs["oagi_base_url"] = args.oagi_base_url
66
+ if args.host:
67
+ config_kwargs["server_host"] = args.host
68
+ if args.port:
69
+ config_kwargs["server_port"] = args.port
70
+
71
+ try:
72
+ config = ServerConfig(**config_kwargs)
73
+ except Exception as e:
74
+ print(f"Error: Invalid configuration - {e}", file=sys.stderr)
75
+ sys.exit(1)
76
+
77
+ # Create and run app
78
+ print(
79
+ f"Starting OAGI Socket.IO server on {config.server_host}:{config.server_port}"
80
+ )
81
+ print(f"OAGI API: {config.oagi_base_url}")
82
+ print(f"Model: {config.default_model}")
83
+ print("\nPress Ctrl+C to stop the server")
84
+
85
+ try:
86
+ app = create_app(config)
87
+ uvicorn.run(
88
+ app, host=config.server_host, port=config.server_port, log_level="info"
89
+ )
90
+ except KeyboardInterrupt:
91
+ print("\nServer stopped.")
92
+ except Exception as e:
93
+ print(f"Error starting server: {e}", file=sys.stderr)
94
+ sys.exit(1)
oagi/cli/utils.py ADDED
@@ -0,0 +1,82 @@
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
+ import sys
12
+ from importlib.metadata import version as get_version
13
+
14
+ from oagi.exceptions import check_optional_dependency
15
+
16
+
17
+ def get_sdk_version() -> str:
18
+ try:
19
+ return get_version("oagi")
20
+ except Exception:
21
+ return "unknown"
22
+
23
+
24
+ def display_version() -> None:
25
+ sdk_version = get_sdk_version()
26
+ python_version = (
27
+ f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
28
+ )
29
+ platform = sys.platform
30
+
31
+ print(f"OAGI SDK version: {sdk_version}")
32
+ print(f"Python version: {python_version}")
33
+ print(f"Platform: {platform}")
34
+
35
+ # Check installed extras
36
+ extras = []
37
+ if check_optional_dependency("pyautogui", "Desktop", "desktop", raise_error=False):
38
+ extras.append("desktop")
39
+
40
+ if check_optional_dependency("fastapi", "Server", "server", raise_error=False):
41
+ extras.append("server")
42
+
43
+ if extras:
44
+ print(f"Installed extras: {', '.join(extras)}")
45
+ else:
46
+ print("Installed extras: none")
47
+
48
+
49
+ def display_config() -> None:
50
+ config_vars = {
51
+ "OAGI_API_KEY": os.getenv("OAGI_API_KEY", ""),
52
+ "OAGI_BASE_URL": os.getenv("OAGI_BASE_URL", "https://api.agiopen.org"),
53
+ "OAGI_LOG_LEVEL": os.getenv("OAGI_LOG_LEVEL", "INFO"),
54
+ "OAGI_SERVER_HOST": os.getenv("OAGI_SERVER_HOST", "0.0.0.0"),
55
+ "OAGI_SERVER_PORT": os.getenv("OAGI_SERVER_PORT", "8000"),
56
+ "OAGI_MAX_STEPS": os.getenv("OAGI_MAX_STEPS", "30"),
57
+ }
58
+
59
+ print("Current Configuration:")
60
+ print("-" * 50)
61
+ for key, value in config_vars.items():
62
+ if key == "OAGI_API_KEY" and value:
63
+ # Mask API key
64
+ masked = value[:8] + "..." if len(value) > 8 else "***"
65
+ print(f"{key}: {masked}")
66
+ else:
67
+ display_value = value if value else "(not set)"
68
+ print(f"{key}: {display_value}")
69
+
70
+
71
+ def setup_logging(verbose: bool) -> None:
72
+ if verbose:
73
+ os.environ["OAGI_LOG_LEVEL"] = "DEBUG"
74
+ logging.basicConfig(
75
+ level=logging.DEBUG,
76
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
77
+ )
78
+ else:
79
+ log_level = os.getenv("OAGI_LOG_LEVEL", "INFO")
80
+ logging.basicConfig(
81
+ level=getattr(logging, log_level), format="%(levelname)s: %(message)s"
82
+ )
@@ -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 .async_ import AsyncClient
10
+ from .sync import SyncClient
11
+
12
+ __all__ = ["SyncClient", "AsyncClient"]
oagi/client/async_.py ADDED
@@ -0,0 +1,293 @@
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 functools import wraps
10
+
11
+ import httpx
12
+
13
+ from ..logging import get_logger
14
+ from ..types import Image
15
+ from ..types.models import GenerateResponse, LLMResponse, UploadFileResponse
16
+ from .base import BaseClient
17
+
18
+ logger = get_logger("async_client")
19
+
20
+
21
+ def async_log_trace_on_failure(func):
22
+ """Async decorator that logs trace ID when a method fails."""
23
+
24
+ @wraps(func)
25
+ async def wrapper(*args, **kwargs):
26
+ try:
27
+ return await func(*args, **kwargs)
28
+ except Exception as e:
29
+ # Try to get response from the exception if it has one
30
+ if (response := getattr(e, "response", None)) is not None:
31
+ logger.error(f"Request Id: {response.headers.get('x-request-id', '')}")
32
+ logger.error(f"Trace Id: {response.headers.get('x-trace-id', '')}")
33
+ raise
34
+
35
+ return wrapper
36
+
37
+
38
+ class AsyncClient(BaseClient[httpx.AsyncClient]):
39
+ """Asynchronous HTTP client for the OAGI API."""
40
+
41
+ def __init__(self, base_url: str | None = None, api_key: str | None = None):
42
+ super().__init__(base_url, api_key)
43
+ self.client = httpx.AsyncClient(base_url=self.base_url)
44
+ self.upload_client = httpx.AsyncClient(timeout=60) # client for uploading image
45
+ logger.info(f"AsyncClient initialized with base_url: {self.base_url}")
46
+
47
+ async def __aenter__(self):
48
+ return self
49
+
50
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
51
+ await self.client.aclose()
52
+ await self.upload_client.aclose()
53
+
54
+ async def close(self):
55
+ """Close the underlying httpx async clients."""
56
+ await self.client.aclose()
57
+ await self.upload_client.aclose()
58
+
59
+ @async_log_trace_on_failure
60
+ async def create_message(
61
+ self,
62
+ model: str,
63
+ screenshot: bytes | None = None,
64
+ screenshot_url: str | None = None,
65
+ task_description: str | None = None,
66
+ task_id: str | None = None,
67
+ instruction: str | None = None,
68
+ messages_history: list | None = None,
69
+ temperature: float | None = None,
70
+ api_version: str | None = None,
71
+ ) -> "LLMResponse":
72
+ """
73
+ Call the /v2/message endpoint to analyze task and screenshot
74
+
75
+ Args:
76
+ model: The model to use for task analysis
77
+ screenshot: Screenshot image bytes (mutually exclusive with screenshot_url)
78
+ screenshot_url: Direct URL to screenshot (mutually exclusive with screenshot)
79
+ task_description: Description of the task (required for new sessions)
80
+ task_id: Task ID for continuing existing task
81
+ instruction: Additional instruction when continuing a session
82
+ messages_history: OpenAI-compatible chat message history
83
+ temperature: Sampling temperature (0.0-2.0) for LLM inference
84
+ api_version: API version header
85
+
86
+ Returns:
87
+ LLMResponse: The response from the API
88
+
89
+ Raises:
90
+ ValueError: If both or neither screenshot and screenshot_url are provided
91
+ httpx.HTTPStatusError: For HTTP error responses
92
+ """
93
+ # Validate that exactly one is provided
94
+ if (screenshot is None) == (screenshot_url is None):
95
+ raise ValueError(
96
+ "Exactly one of 'screenshot' or 'screenshot_url' must be provided"
97
+ )
98
+
99
+ self._log_request_info(model, task_description, task_id)
100
+
101
+ # Upload screenshot to S3 if bytes provided, otherwise use URL directly
102
+ upload_file_response = None
103
+ if screenshot is not None:
104
+ upload_file_response = await self.put_s3_presigned_url(
105
+ screenshot, api_version
106
+ )
107
+
108
+ # Prepare message payload
109
+ headers, payload = self._prepare_message_payload(
110
+ model=model,
111
+ upload_file_response=upload_file_response,
112
+ task_description=task_description,
113
+ task_id=task_id,
114
+ instruction=instruction,
115
+ messages_history=messages_history,
116
+ temperature=temperature,
117
+ api_version=api_version,
118
+ screenshot_url=screenshot_url,
119
+ )
120
+
121
+ # Make request
122
+ try:
123
+ response = await self.client.post(
124
+ "/v2/message", json=payload, headers=headers, timeout=self.timeout
125
+ )
126
+ return self._process_response(response)
127
+ except (httpx.TimeoutException, httpx.NetworkError) as e:
128
+ self._handle_upload_http_errors(e)
129
+
130
+ async def health_check(self) -> dict:
131
+ """
132
+ Call the /health endpoint for health check
133
+
134
+ Returns:
135
+ dict: Health check response
136
+ """
137
+ logger.debug("Making async health check request")
138
+ try:
139
+ response = await self.client.get("/health")
140
+ response.raise_for_status()
141
+ result = response.json()
142
+ logger.debug("Async health check successful")
143
+ return result
144
+ except httpx.HTTPStatusError as e:
145
+ logger.warning(f"Async health check failed: {e}")
146
+ raise
147
+
148
+ async def get_s3_presigned_url(
149
+ self,
150
+ api_version: str | None = None,
151
+ ) -> UploadFileResponse:
152
+ """
153
+ Call the /v1/file/upload endpoint to get a S3 presigned URL
154
+
155
+ Args:
156
+ api_version: API version header
157
+
158
+ Returns:
159
+ UploadFileResponse: The response from /v1/file/upload with uuid and presigned S3 URL
160
+ """
161
+ logger.debug("Making async API request to /v1/file/upload")
162
+
163
+ try:
164
+ headers = self._build_headers(api_version)
165
+ response = await self.client.get(
166
+ "/v1/file/upload", headers=headers, timeout=self.timeout
167
+ )
168
+ return self._process_upload_response(response)
169
+ except (httpx.TimeoutException, httpx.NetworkError, httpx.HTTPStatusError) as e:
170
+ self._handle_upload_http_errors(e, getattr(e, "response", None))
171
+
172
+ async def upload_to_s3(
173
+ self,
174
+ url: str,
175
+ content: bytes | Image,
176
+ ) -> None:
177
+ """
178
+ Upload image bytes to S3 using presigned URL
179
+
180
+ Args:
181
+ url: S3 presigned URL
182
+ content: Image bytes or Image object to upload
183
+
184
+ Raises:
185
+ APIError: If upload fails
186
+ """
187
+ logger.debug("Async uploading image to S3")
188
+
189
+ # Convert Image to bytes if needed
190
+ if isinstance(content, Image):
191
+ content = content.read()
192
+
193
+ response = None
194
+ try:
195
+ response = await self.upload_client.put(url=url, content=content)
196
+ response.raise_for_status()
197
+ except Exception as e:
198
+ self._handle_s3_upload_error(e, response)
199
+
200
+ async def put_s3_presigned_url(
201
+ self,
202
+ screenshot: bytes | Image,
203
+ api_version: str | None = None,
204
+ ) -> UploadFileResponse:
205
+ """
206
+ Get S3 presigned URL and upload image (convenience method)
207
+
208
+ Args:
209
+ screenshot: Screenshot image bytes or Image object
210
+ api_version: API version header
211
+
212
+ Returns:
213
+ UploadFileResponse: The response from /v1/file/upload with uuid and presigned S3 URL
214
+ """
215
+ upload_file_response = await self.get_s3_presigned_url(api_version)
216
+ await self.upload_to_s3(upload_file_response.url, screenshot)
217
+ return upload_file_response
218
+
219
+ @async_log_trace_on_failure
220
+ async def call_worker(
221
+ self,
222
+ worker_id: str,
223
+ overall_todo: str,
224
+ task_description: str,
225
+ todos: list[dict],
226
+ deliverables: list[dict],
227
+ history: list[dict] | None = None,
228
+ current_todo_index: int | None = None,
229
+ task_execution_summary: str | None = None,
230
+ current_screenshot: str | None = None,
231
+ current_subtask_instruction: str | None = None,
232
+ window_steps: list[dict] | None = None,
233
+ window_screenshots: list[str] | None = None,
234
+ result_screenshot: str | None = None,
235
+ prior_notes: str | None = None,
236
+ latest_todo_summary: str | None = None,
237
+ api_version: str | None = None,
238
+ ) -> GenerateResponse:
239
+ """Call the /v1/generate endpoint for OAGI worker processing.
240
+
241
+ Args:
242
+ worker_id: One of "oagi_first", "oagi_follow", "oagi_task_summary"
243
+ overall_todo: Current todo description
244
+ task_description: Overall task description
245
+ todos: List of todo dicts with index, description, status, execution_summary
246
+ deliverables: List of deliverable dicts with description, achieved
247
+ history: List of history dicts with todo_index, todo_description, action_count, summary, completed
248
+ current_todo_index: Index of current todo being executed
249
+ task_execution_summary: Summary of overall task execution
250
+ current_screenshot: Uploaded file UUID for screenshot (oagi_first)
251
+ current_subtask_instruction: Subtask instruction (oagi_follow)
252
+ window_steps: Action steps list (oagi_follow)
253
+ window_screenshots: Uploaded file UUIDs list (oagi_follow)
254
+ result_screenshot: Uploaded file UUID for result screenshot (oagi_follow)
255
+ prior_notes: Execution notes (oagi_follow)
256
+ latest_todo_summary: Latest summary (oagi_task_summary)
257
+ api_version: API version header
258
+
259
+ Returns:
260
+ GenerateResponse with LLM output and usage stats
261
+
262
+ Raises:
263
+ ValueError: If worker_id is invalid
264
+ APIError: If API returns error
265
+ """
266
+ # Prepare request (validation, payload, headers)
267
+ payload, headers = self._prepare_worker_request(
268
+ worker_id=worker_id,
269
+ overall_todo=overall_todo,
270
+ task_description=task_description,
271
+ todos=todos,
272
+ deliverables=deliverables,
273
+ history=history,
274
+ current_todo_index=current_todo_index,
275
+ task_execution_summary=task_execution_summary,
276
+ current_screenshot=current_screenshot,
277
+ current_subtask_instruction=current_subtask_instruction,
278
+ window_steps=window_steps,
279
+ window_screenshots=window_screenshots,
280
+ result_screenshot=result_screenshot,
281
+ prior_notes=prior_notes,
282
+ latest_todo_summary=latest_todo_summary,
283
+ api_version=api_version,
284
+ )
285
+
286
+ # Make request
287
+ try:
288
+ response = await self.client.post(
289
+ "/v1/generate", json=payload, headers=headers, timeout=self.timeout
290
+ )
291
+ return self._process_generate_response(response)
292
+ except (httpx.TimeoutException, httpx.NetworkError) as e:
293
+ self._handle_upload_http_errors(e)