oagi 0.2.0__tar.gz → 0.2.1__tar.gz

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.

Files changed (42) hide show
  1. {oagi-0.2.0 → oagi-0.2.1}/PKG-INFO +1 -1
  2. {oagi-0.2.0 → oagi-0.2.1}/pyproject.toml +1 -1
  3. {oagi-0.2.0 → oagi-0.2.1}/src/oagi/sync_client.py +24 -0
  4. {oagi-0.2.0 → oagi-0.2.1}/tests/conftest.py +2 -0
  5. {oagi-0.2.0 → oagi-0.2.1}/tests/test_logging.py +146 -130
  6. oagi-0.2.1/tests/test_sync_client.py +410 -0
  7. {oagi-0.2.0 → oagi-0.2.1}/uv.lock +1 -1
  8. oagi-0.2.0/tests/test_sync_client.py +0 -326
  9. {oagi-0.2.0 → oagi-0.2.1}/.github/workflows/ci.yml +0 -0
  10. {oagi-0.2.0 → oagi-0.2.1}/.github/workflows/release.yml +0 -0
  11. {oagi-0.2.0 → oagi-0.2.1}/.gitignore +0 -0
  12. {oagi-0.2.0 → oagi-0.2.1}/.python-version +0 -0
  13. {oagi-0.2.0 → oagi-0.2.1}/CONTRIBUTING.md +0 -0
  14. {oagi-0.2.0 → oagi-0.2.1}/LICENSE +0 -0
  15. {oagi-0.2.0 → oagi-0.2.1}/Makefile +0 -0
  16. {oagi-0.2.0 → oagi-0.2.1}/README.md +0 -0
  17. {oagi-0.2.0 → oagi-0.2.1}/examples/execute_task_auto.py +0 -0
  18. {oagi-0.2.0 → oagi-0.2.1}/examples/execute_task_manual.py +0 -0
  19. {oagi-0.2.0 → oagi-0.2.1}/examples/google_weather.py +0 -0
  20. {oagi-0.2.0 → oagi-0.2.1}/examples/hotel_booking.py +0 -0
  21. {oagi-0.2.0 → oagi-0.2.1}/examples/single_step.py +0 -0
  22. {oagi-0.2.0 → oagi-0.2.1}/src/oagi/__init__.py +0 -0
  23. {oagi-0.2.0 → oagi-0.2.1}/src/oagi/exceptions.py +0 -0
  24. {oagi-0.2.0 → oagi-0.2.1}/src/oagi/logging.py +0 -0
  25. {oagi-0.2.0 → oagi-0.2.1}/src/oagi/pyautogui_action_handler.py +0 -0
  26. {oagi-0.2.0 → oagi-0.2.1}/src/oagi/screenshot_maker.py +0 -0
  27. {oagi-0.2.0 → oagi-0.2.1}/src/oagi/short_task.py +0 -0
  28. {oagi-0.2.0 → oagi-0.2.1}/src/oagi/single_step.py +0 -0
  29. {oagi-0.2.0 → oagi-0.2.1}/src/oagi/task.py +0 -0
  30. {oagi-0.2.0 → oagi-0.2.1}/src/oagi/types/__init__.py +0 -0
  31. {oagi-0.2.0 → oagi-0.2.1}/src/oagi/types/action_handler.py +0 -0
  32. {oagi-0.2.0 → oagi-0.2.1}/src/oagi/types/image.py +0 -0
  33. {oagi-0.2.0 → oagi-0.2.1}/src/oagi/types/image_provider.py +0 -0
  34. {oagi-0.2.0 → oagi-0.2.1}/src/oagi/types/models/__init__.py +0 -0
  35. {oagi-0.2.0 → oagi-0.2.1}/src/oagi/types/models/action.py +0 -0
  36. {oagi-0.2.0 → oagi-0.2.1}/src/oagi/types/models/step.py +0 -0
  37. {oagi-0.2.0 → oagi-0.2.1}/tests/__init__.py +0 -0
  38. {oagi-0.2.0 → oagi-0.2.1}/tests/test_pyautogui_action_handler.py +0 -0
  39. {oagi-0.2.0 → oagi-0.2.1}/tests/test_screenshot_maker.py +0 -0
  40. {oagi-0.2.0 → oagi-0.2.1}/tests/test_short_task.py +0 -0
  41. {oagi-0.2.0 → oagi-0.2.1}/tests/test_single_step.py +0 -0
  42. {oagi-0.2.0 → oagi-0.2.1}/tests/test_task.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: oagi
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Official API of OpenAGI Foundation
5
5
  Project-URL: Homepage, https://github.com/agiopen-org/oagi
6
6
  Author-email: OpenAGI Foundation <contact@agiopen.org>
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "oagi"
7
- version = "0.2.0"
7
+ version = "0.2.1"
8
8
  description = "Official API of OpenAGI Foundation"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -8,8 +8,10 @@
8
8
 
9
9
  import base64
10
10
  import os
11
+ from functools import wraps
11
12
 
12
13
  import httpx
14
+ from httpx import Response
13
15
  from pydantic import BaseModel
14
16
 
15
17
  from .exceptions import (
@@ -63,6 +65,27 @@ class LLMResponse(BaseModel):
63
65
  error: ErrorDetail | None = None
64
66
 
65
67
 
68
+ def _log_trace_id(response: Response):
69
+ logger.error(f"Request Id: {response.headers.get('x-request-id', '')}")
70
+ logger.error(f"Trace Id: {response.headers.get('x-trace-id', '')}")
71
+
72
+
73
+ def log_trace_on_failure(func):
74
+ """Decorator that logs trace ID when a method fails."""
75
+
76
+ @wraps(func)
77
+ def wrapper(*args, **kwargs):
78
+ try:
79
+ return func(*args, **kwargs)
80
+ except Exception as e:
81
+ # Try to get response from the exception if it has one
82
+ if (response := getattr(e, "response", None)) is not None:
83
+ _log_trace_id(response)
84
+ raise
85
+
86
+ return wrapper
87
+
88
+
66
89
  class SyncClient:
67
90
  def __init__(self, base_url: str | None = None, api_key: str | None = None):
68
91
  # Get from environment if not provided
@@ -98,6 +121,7 @@ class SyncClient:
98
121
  """Close the underlying httpx client"""
99
122
  self.client.close()
100
123
 
124
+ @log_trace_on_failure
101
125
  def create_message(
102
126
  self,
103
127
  model: str,
@@ -26,6 +26,8 @@ def clean_env():
26
26
  for var in env_vars:
27
27
  if var in os.environ:
28
28
  original_values[var] = os.environ[var]
29
+ # Clear the environment variable for test isolation
30
+ del os.environ[var]
29
31
 
30
32
  yield
31
33
 
@@ -20,30 +20,54 @@ from oagi.screenshot_maker import MockImage
20
20
  from oagi.sync_client import SyncClient
21
21
 
22
22
 
23
- class TestLogging:
24
- def setup_method(self):
25
- # Clear environment variables and reset logging state
26
- if "OAGI_LOG" in os.environ:
27
- del os.environ["OAGI_LOG"]
23
+ @pytest.fixture
24
+ def clean_logging_state():
25
+ """Clean and reset OAGI logging state before and after test."""
28
26
 
29
- # Clear any existing oagi loggers
27
+ def _clean_loggers():
30
28
  oagi_logger = logging.getLogger("oagi")
31
29
  oagi_logger.handlers.clear()
32
30
  oagi_logger.setLevel(logging.NOTSET)
33
31
 
34
- # Clear any child loggers
32
+ # Clear child loggers
35
33
  for name in list(logging.Logger.manager.loggerDict.keys()):
36
34
  if name.startswith("oagi."):
37
35
  logger = logging.getLogger(name)
38
36
  logger.handlers.clear()
39
37
  logger.setLevel(logging.NOTSET)
40
38
 
41
- def test_default_log_level(self):
42
- logger = get_logger("test")
43
- oagi_root = logging.getLogger("oagi")
39
+ _clean_loggers()
40
+ yield
41
+ _clean_loggers()
42
+
43
+
44
+ @pytest.fixture
45
+ def set_log_level():
46
+ """Helper to set OAGI_LOG environment variable for tests."""
47
+
48
+ def _set_level(level: str):
49
+ os.environ["OAGI_LOG"] = level
50
+
51
+ return _set_level
52
+
53
+
54
+ @pytest.fixture
55
+ def oagi_root_logger():
56
+ """Get the root OAGI logger."""
57
+ return logging.getLogger("oagi")
58
+
44
59
 
45
- assert oagi_root.level == logging.INFO
46
- assert logger.name == "oagi.test"
60
+ @pytest.fixture
61
+ def test_logger():
62
+ """Create a test logger using get_logger."""
63
+ return get_logger("test")
64
+
65
+
66
+ class TestLogging:
67
+ @pytest.mark.usefixtures("clean_logging_state")
68
+ def test_default_log_level(self, test_logger, oagi_root_logger):
69
+ assert oagi_root_logger.level == logging.INFO
70
+ assert test_logger.name == "oagi.test"
47
71
 
48
72
  @pytest.mark.parametrize(
49
73
  "env_value,expected_level",
@@ -53,63 +77,53 @@ class TestLogging:
53
77
  ("WARNING", logging.WARNING),
54
78
  ("ERROR", logging.ERROR),
55
79
  ("CRITICAL", logging.CRITICAL),
56
- ("debug", logging.DEBUG), # Case insensitive
57
- ("info", logging.INFO), # Case insensitive
80
+ ("debug", logging.DEBUG),
81
+ ("info", logging.INFO),
58
82
  ],
59
83
  )
60
- def test_log_level_configuration(self, env_value, expected_level):
61
- """Test that log level is correctly set from environment variable."""
62
- os.environ["OAGI_LOG"] = env_value
63
- get_logger("test")
64
- oagi_root = logging.getLogger("oagi")
65
-
66
- assert oagi_root.level == expected_level
67
-
68
- def test_invalid_log_level_defaults_to_info(self):
69
- os.environ["OAGI_LOG"] = "INVALID_LEVEL"
84
+ @pytest.mark.usefixtures("clean_logging_state")
85
+ def test_log_level_configuration(
86
+ self, env_value, expected_level, set_log_level, oagi_root_logger
87
+ ):
88
+ set_log_level(env_value)
70
89
  get_logger("test")
71
- oagi_root = logging.getLogger("oagi")
72
-
73
- assert oagi_root.level == logging.INFO
90
+ assert oagi_root_logger.level == expected_level
74
91
 
75
- def test_handler_configuration(self):
92
+ @pytest.mark.usefixtures("clean_logging_state")
93
+ def test_invalid_log_level_defaults_to_info(self, set_log_level, oagi_root_logger):
94
+ set_log_level("INVALID_LEVEL")
76
95
  get_logger("test")
77
- oagi_root = logging.getLogger("oagi")
96
+ assert oagi_root_logger.level == logging.INFO
78
97
 
79
- assert len(oagi_root.handlers) == 1
80
- handler = oagi_root.handlers[0]
98
+ @pytest.mark.usefixtures("clean_logging_state")
99
+ def test_handler_configuration(self, test_logger, oagi_root_logger):
100
+ assert len(oagi_root_logger.handlers) == 1
101
+ handler = oagi_root_logger.handlers[0]
81
102
  assert isinstance(handler, logging.StreamHandler)
82
103
 
83
- # Check formatter
84
104
  formatter = handler.formatter
85
105
  assert "%(asctime)s - %(name)s - %(levelname)s - %(message)s" in formatter._fmt
86
106
 
87
- def test_multiple_loggers_share_configuration(self):
107
+ @pytest.mark.usefixtures("clean_logging_state")
108
+ def test_multiple_loggers_share_configuration(self, oagi_root_logger):
88
109
  logger1 = get_logger("module1")
89
110
  logger2 = get_logger("module2")
90
111
 
91
- oagi_root = logging.getLogger("oagi")
92
-
93
- # Should only have one handler
94
- assert len(oagi_root.handlers) == 1
95
-
96
- # Both loggers should be under the same root
112
+ assert len(oagi_root_logger.handlers) == 1
97
113
  assert logger1.name == "oagi.module1"
98
114
  assert logger2.name == "oagi.module2"
99
115
 
100
- def test_log_level_change_after_initialization(self):
101
- # First initialization with INFO
102
- os.environ["OAGI_LOG"] = "INFO"
116
+ @pytest.mark.usefixtures("clean_logging_state")
117
+ def test_log_level_change_after_initialization(
118
+ self, set_log_level, oagi_root_logger
119
+ ):
120
+ set_log_level("INFO")
103
121
  get_logger("test1")
104
- oagi_root = logging.getLogger("oagi")
105
- assert oagi_root.level == logging.INFO
122
+ assert oagi_root_logger.level == logging.INFO
106
123
 
107
- # Change environment and create new logger
108
- os.environ["OAGI_LOG"] = "DEBUG"
124
+ set_log_level("DEBUG")
109
125
  get_logger("test2")
110
-
111
- # Level should be updated
112
- assert oagi_root.level == logging.DEBUG
126
+ assert oagi_root_logger.level == logging.DEBUG
113
127
 
114
128
  @pytest.mark.parametrize(
115
129
  "log_level,should_appear,should_not_appear",
@@ -136,63 +150,58 @@ class TestLogging:
136
150
  ),
137
151
  ],
138
152
  )
153
+ @pytest.mark.usefixtures("clean_logging_state")
139
154
  @patch("sys.stderr", new_callable=StringIO)
140
155
  def test_log_filtering_by_level(
141
- self, mock_stderr, log_level, should_appear, should_not_appear
156
+ self, mock_stderr, log_level, should_appear, should_not_appear, set_log_level
142
157
  ):
143
- """Test that log messages are correctly filtered based on log level."""
144
- os.environ["OAGI_LOG"] = log_level
158
+ set_log_level(log_level)
145
159
  logger = get_logger("test_module")
146
160
 
147
- # Test different log levels
148
- logger.debug("Debug message")
149
- logger.info("Info message")
150
- logger.warning("Warning message")
151
- logger.error("Error message")
152
-
161
+ self._log_all_levels(logger)
153
162
  output = mock_stderr.getvalue()
154
163
 
155
- # Check messages that should appear
156
- for message in should_appear:
157
- assert message in output, f"{message} should appear at {log_level} level"
158
-
159
- # Check messages that should not appear
160
- for message in should_not_appear:
161
- assert message not in output, (
162
- f"{message} should not appear at {log_level} level"
163
- )
164
+ self._assert_messages_in_output(
165
+ output, should_appear, log_level, should_appear=True
166
+ )
167
+ self._assert_messages_in_output(
168
+ output, should_not_appear, log_level, should_appear=False
169
+ )
164
170
 
165
- # Check logger name in output if any messages appear
166
171
  if should_appear:
167
172
  assert "oagi.test_module" in output
168
173
 
174
+ def _log_all_levels(self, logger):
175
+ """Helper to log messages at all levels."""
176
+ logger.debug("Debug message")
177
+ logger.info("Info message")
178
+ logger.warning("Warning message")
179
+ logger.error("Error message")
169
180
 
170
- class TestLoggingIntegration:
171
- def setup_method(self):
172
- # Clear any existing oagi loggers
173
- oagi_logger = logging.getLogger("oagi")
174
- oagi_logger.handlers.clear()
175
- oagi_logger.setLevel(logging.NOTSET)
181
+ def _assert_messages_in_output(self, output, messages, log_level, should_appear):
182
+ """Helper to assert messages appear or don't appear in output."""
183
+ for message in messages:
184
+ if should_appear:
185
+ assert message in output, (
186
+ f"{message} should appear at {log_level} level"
187
+ )
188
+ else:
189
+ assert message not in output, (
190
+ f"{message} should not appear at {log_level} level"
191
+ )
176
192
 
177
- # Clear any child loggers
178
- for name in list(logging.Logger.manager.loggerDict.keys()):
179
- if name.startswith("oagi."):
180
- logger = logging.getLogger(name)
181
- logger.handlers.clear()
182
- logger.setLevel(logging.NOTSET)
183
193
 
184
- def test_sync_client_logging(self, api_env, caplog):
185
- """Test that SyncClient logs initialization correctly."""
186
- os.environ["OAGI_LOG"] = "INFO"
194
+ class TestLoggingIntegration:
195
+ @pytest.mark.usefixtures("clean_logging_state")
196
+ def test_sync_client_logging(self, api_env, caplog, set_log_level):
197
+ set_log_level("INFO")
187
198
 
188
199
  with caplog.at_level(logging.INFO, logger="oagi"):
189
200
  client = SyncClient()
190
201
  client.close()
191
202
 
192
- assert (
193
- f"SyncClient initialized with base_url: {api_env['base_url']}"
194
- in caplog.text
195
- )
203
+ expected_msg = f"SyncClient initialized with base_url: {api_env['base_url']}"
204
+ assert expected_msg in caplog.text
196
205
  assert any("oagi.sync_client" in record.name for record in caplog.records)
197
206
 
198
207
  @pytest.mark.parametrize(
@@ -219,12 +228,13 @@ class TestLoggingIntegration:
219
228
  (
220
229
  "ERROR",
221
230
  "Error test",
222
- "error", # Special case - will trigger error
231
+ "error",
223
232
  ["Error during step execution"],
224
233
  ["Task initialized", "SyncClient initialized"],
225
234
  ),
226
235
  ],
227
236
  )
237
+ @pytest.mark.usefixtures("clean_logging_state")
228
238
  def test_task_logging_levels(
229
239
  self,
230
240
  mock_httpx_client_class,
@@ -238,81 +248,87 @@ class TestLoggingIntegration:
238
248
  should_have_step,
239
249
  expected_messages,
240
250
  unexpected_messages,
251
+ set_log_level,
241
252
  ):
242
- """Test ShortTask logging at different levels."""
243
- os.environ["OAGI_LOG"] = log_level
253
+ set_log_level(log_level)
254
+
255
+ mock_response = self._create_mock_response(api_response_init_task, task_desc)
256
+ self._setup_mock_client_behavior(
257
+ mock_httpx_client, mock_response, should_have_step, http_status_error
258
+ )
244
259
 
245
- # Setup mock response
260
+ with caplog.at_level(getattr(logging, log_level), logger="oagi"):
261
+ self._execute_task_scenario(task_desc, log_level, should_have_step)
262
+
263
+ self._assert_log_messages(caplog.text, expected_messages, unexpected_messages)
264
+
265
+ def _create_mock_response(self, api_response_init_task, task_desc):
266
+ """Helper to create mock HTTP response."""
246
267
  mock_response = Mock()
247
268
  mock_response.status_code = 200
248
269
  response_data = api_response_init_task.copy()
249
270
  response_data["task_description"] = task_desc
250
271
  mock_response.json.return_value = response_data
272
+ return mock_response
251
273
 
274
+ def _setup_mock_client_behavior(
275
+ self, mock_httpx_client, mock_response, should_have_step, http_status_error
276
+ ):
277
+ """Helper to setup mock client behavior based on test scenario."""
252
278
  if should_have_step == "error":
253
- # First call succeeds, second fails
254
279
  mock_httpx_client.post.side_effect = [mock_response, http_status_error]
255
280
  else:
256
281
  mock_httpx_client.post.return_value = mock_response
257
282
 
258
- with caplog.at_level(getattr(logging, log_level), logger="oagi"):
259
- task = ShortTask()
283
+ def _execute_task_scenario(self, task_desc, log_level, should_have_step):
284
+ """Helper to execute the task scenario."""
285
+ task = ShortTask()
260
286
 
261
- if log_level == "INFO":
262
- task.init_task(task_desc, max_steps=3)
263
- else:
264
- task.init_task(task_desc)
265
-
266
- if should_have_step == "error":
267
- try:
268
- task.step(MockImage())
269
- except Exception:
270
- pass # Expected to fail
271
- elif should_have_step:
287
+ if log_level == "INFO":
288
+ task.init_task(task_desc, max_steps=3)
289
+ else:
290
+ task.init_task(task_desc)
291
+
292
+ if should_have_step == "error":
293
+ try:
272
294
  task.step(MockImage())
295
+ except Exception:
296
+ pass # Expected to fail
297
+ elif should_have_step:
298
+ task.step(MockImage())
273
299
 
274
- task.close()
300
+ task.close()
275
301
 
276
- # Check expected messages
302
+ def _assert_log_messages(self, log_text, expected_messages, unexpected_messages):
303
+ """Helper to assert expected and unexpected messages in logs."""
277
304
  for msg in expected_messages:
278
- assert msg in caplog.text, f"Expected '{msg}' in logs"
305
+ assert msg in log_text, f"Expected '{msg}' in logs"
279
306
 
280
- # Check unexpected messages
281
307
  for msg in unexpected_messages:
282
- assert msg not in caplog.text, f"Did not expect '{msg}' in logs"
308
+ assert msg not in log_text, f"Did not expect '{msg}' in logs"
283
309
 
284
- def test_no_logging_with_invalid_config(self, caplog):
285
- # Don't set OAGI_BASE_URL or OAGI_API_KEY to trigger errors
286
- os.environ["OAGI_LOG"] = "INFO"
310
+ @pytest.mark.usefixtures("clean_logging_state")
311
+ def test_no_logging_with_invalid_config(self, caplog, set_log_level):
312
+ os.environ.pop("OAGI_BASE_URL", None)
313
+ os.environ.pop("OAGI_API_KEY", None)
314
+ set_log_level("INFO")
287
315
 
288
316
  with caplog.at_level(logging.INFO, logger="oagi"):
289
- try:
317
+ with pytest.raises(ConfigurationError):
290
318
  SyncClient()
291
- except ConfigurationError:
292
- pass # Expected to fail
293
319
 
294
- # Should not have any successful initialization logs
295
320
  assert "SyncClient initialized" not in caplog.text
296
321
 
297
- def test_logger_namespace_isolation(self):
298
- """Test that OAGI loggers don't interfere with other loggers"""
299
- os.environ["OAGI_LOG"] = "DEBUG"
300
-
301
- # Create an OAGI logger
322
+ @pytest.mark.usefixtures("clean_logging_state")
323
+ def test_logger_namespace_isolation(self, set_log_level, oagi_root_logger):
324
+ set_log_level("DEBUG")
302
325
  get_logger("test")
303
326
 
304
- # Create a regular logger
305
327
  other_logger = logging.getLogger("other.module")
306
328
  other_logger.setLevel(logging.WARNING)
307
329
 
308
- oagi_root = logging.getLogger("oagi")
309
-
310
- # OAGI should be at DEBUG level
311
- assert oagi_root.level == logging.DEBUG
312
-
313
- # Other logger should remain unaffected
330
+ assert oagi_root_logger.level == logging.DEBUG
314
331
  assert other_logger.level == logging.WARNING
315
332
 
316
- # Root logger should remain unaffected
317
333
  root_logger = logging.getLogger()
318
334
  assert root_logger.level != logging.DEBUG