From 0140d6e4e7ed69faeed9a490a5b8a07101d06813 Mon Sep 17 00:00:00 2001 From: vrushankportkey Date: Tue, 11 Nov 2025 13:41:31 +0530 Subject: [PATCH] fix json serializing for openai agents repo --- examples/notgiven_serialization_example.py | 174 +++++++++++++++++++++ portkey_ai/__init__.py | 11 ++ portkey_ai/_vendor/openai/_types.py | 12 ++ portkey_ai/utils/json_utils.py | 50 +++++- tests/test_notgiven_serialization.py | 121 ++++++++++++++ 5 files changed, 364 insertions(+), 4 deletions(-) create mode 100644 examples/notgiven_serialization_example.py create mode 100644 tests/test_notgiven_serialization.py diff --git a/examples/notgiven_serialization_example.py b/examples/notgiven_serialization_example.py new file mode 100644 index 00000000..cd22f29e --- /dev/null +++ b/examples/notgiven_serialization_example.py @@ -0,0 +1,174 @@ +""" +Example: Using Portkey with openai-agents (NotGiven Serialization) + +This example demonstrates that Portkey now automatically handles NotGiven +serialization out of the box. No manual setup required! + +The fix is transparent - just import and use Portkey normally. +""" + +import portkey_ai +from portkey_ai import AsyncPortkey, enable_notgiven_serialization, disable_notgiven_serialization +import json + + +def example_automatic_serialization(): + """Example showing automatic serialization works out of the box.""" + print("=== Automatic Serialization (No Setup Needed!) ===\n") + + from portkey_ai._vendor.openai._types import NOT_GIVEN + + # Data that contains NotGiven objects + data = { + "model": "gpt-4", + "temperature": 0.7, + "optional_param": NOT_GIVEN, # This works automatically now! + "another_param": None, + } + + # Use standard json.dumps - no special encoder needed! + try: + json_string = json.dumps(data) # Just works! ✨ + print(f"✅ Serialization works automatically: {json_string}\n") + except TypeError as e: + print(f"❌ Serialization failed: {e}\n") + + +def example_basic_usage(): + """Basic example of using PortkeyJSONEncoder for manual serialization.""" + print("=== Custom Encoder (Optional) ===\n") + + from portkey_ai import PortkeyJSONEncoder + from portkey_ai._vendor.openai._types import NOT_GIVEN + + # You can still use the custom encoder if you prefer + data = { + "model": "gpt-4", + "temperature": 0.7, + "optional_param": NOT_GIVEN, + "another_param": None, + } + + # Serialize using PortkeyJSONEncoder + try: + json_string = json.dumps(data, cls=PortkeyJSONEncoder) + print(f"✅ Successfully serialized with encoder: {json_string}\n") + except TypeError as e: + print(f"❌ Serialization failed: {e}\n") + + +def example_global_serialization(): + """Example showing disable/enable functionality.""" + print("=== Manual Enable/Disable Example ===\n") + + from portkey_ai._vendor.openai._types import NOT_GIVEN + + print("Note: Serialization is already enabled automatically!") + print("But you can disable and re-enable if needed:\n") + + # Disable temporarily + print("1. Disabling serialization...") + disable_notgiven_serialization() + + data = {"param": NOT_GIVEN} + + try: + json.dumps(data) + print(" ✅ Still works (unexpected)") + except TypeError: + print(" ✅ Correctly disabled - can't serialize NotGiven") + + # Re-enable + print("\n2. Re-enabling serialization...") + enable_notgiven_serialization() + + try: + json_string = json.dumps(data) + print(f" ✅ Works again: {json_string}\n") + except TypeError as e: + print(f" ❌ Failed: {e}\n") + + +def example_with_portkey_client(): + """Example showing how to use with a Portkey client.""" + print("=== Portkey Client Example ===\n") + + print("Serialization is enabled automatically - no setup needed!") + + # Create Portkey client - serialization works out of the box! + client = AsyncPortkey( + api_key="your-portkey-api-key", # or set PORTKEY_API_KEY env var + virtual_key="your-virtual-key", # optional + ) + + print("✅ AsyncPortkey client created successfully") + print("✅ The client can be serialized by external libraries like openai-agents") + + # Example: Simulate what openai-agents might do + try: + # Try to serialize the client's attributes + client_dict = { + "api_key": client.api_key, + "base_url": str(client.base_url), + "virtual_key": client.virtual_key, + } + serialized = json.dumps(client_dict) + print(f"✅ Client attributes serialized successfully: {serialized}\n") + except TypeError as e: + print(f"❌ Client serialization failed: {e}\n") + + +def example_before_and_after(): + """Demonstrate the problem and solution side by side.""" + print("=== Before and After Comparison ===\n") + + from portkey_ai._vendor.openai._types import NOT_GIVEN + + data = {"param": NOT_GIVEN} + + # BEFORE: This would fail + print("Before enabling global serialization:") + try: + json.dumps(data) + print("✅ Serialization succeeded (unexpected)") + except TypeError as e: + print(f"❌ Serialization failed as expected: {str(e)[:50]}...") + + # AFTER: This works + print("\nAfter enabling global serialization:") + enable_notgiven_serialization() + try: + result = json.dumps(data) + print(f"✅ Serialization succeeded: {result}") + except TypeError as e: + print(f"❌ Serialization failed (unexpected): {e}") + + from portkey_ai import disable_notgiven_serialization + disable_notgiven_serialization() + print() + + +def main(): + """Run all examples.""" + print("\n" + "="*60) + print("Portkey NotGiven Serialization Examples") + print("="*60 + "\n") + + print("✨ Good News: Serialization works automatically!") + print(" No manual setup required - just import and use.\n") + + example_automatic_serialization() + example_with_portkey_client() + example_basic_usage() + example_global_serialization() + example_before_and_after() + + print("="*60) + print("\n✨ All examples completed!") + print("\n📚 Key Takeaway: NotGiven serialization works out of the box!") + print(" For more information, see docs/notgiven-serialization.md") + print("="*60 + "\n") + + +if __name__ == "__main__": + main() diff --git a/portkey_ai/__init__.py b/portkey_ai/__init__.py index 10479a8b..3a257a83 100644 --- a/portkey_ai/__init__.py +++ b/portkey_ai/__init__.py @@ -164,6 +164,14 @@ PORTKEY_PROXY_ENV, PORTKEY_GATEWAY_URL, ) +from portkey_ai.utils.json_utils import ( + PortkeyJSONEncoder, + enable_notgiven_serialization, + disable_notgiven_serialization, +) + +# Automatically enable NotGiven serialization. Users can call disable_notgiven_serialization() if needed. +enable_notgiven_serialization() api_key = os.environ.get(PORTKEY_API_KEY_ENV) base_url = os.environ.get(PORTKEY_PROXY_ENV, PORTKEY_BASE_URL) @@ -175,6 +183,9 @@ "LLMOptions", "Modes", "PortkeyResponse", + "PortkeyJSONEncoder", + "enable_notgiven_serialization", + "disable_notgiven_serialization", "ModesLiteral", "ProviderTypes", "ProviderTypesLiteral", diff --git a/portkey_ai/_vendor/openai/_types.py b/portkey_ai/_vendor/openai/_types.py index 2387d7e0..28642055 100644 --- a/portkey_ai/_vendor/openai/_types.py +++ b/portkey_ai/_vendor/openai/_types.py @@ -143,6 +143,18 @@ def __bool__(self) -> Literal[False]: def __repr__(self) -> str: return "NOT_GIVEN" + def __reduce__(self) -> tuple[type[NotGiven], tuple[()]]: + """Support for pickling/serialization.""" + return (self.__class__, ()) + + def __copy__(self) -> NotGiven: + """Return self since NotGiven is a singleton-like sentinel.""" + return self + + def __deepcopy__(self, memo: dict[int, Any]) -> NotGiven: + """Return self since NotGiven is a singleton-like sentinel.""" + return self + not_given = NotGiven() # for backwards compatibility: diff --git a/portkey_ai/utils/json_utils.py b/portkey_ai/utils/json_utils.py index 6b016caf..26821ee1 100644 --- a/portkey_ai/utils/json_utils.py +++ b/portkey_ai/utils/json_utils.py @@ -1,11 +1,53 @@ import json +from portkey_ai._vendor.openai._types import NotGiven + + +class PortkeyJSONEncoder(json.JSONEncoder): + """Custom JSON encoder that handles Portkey-specific types like NotGiven.""" + + def default(self, obj): + if isinstance(obj, NotGiven): + # Return None for NotGiven instances during JSON serialization + return None + return super().default(obj) + + +_original_json_encoder = None + + +def enable_notgiven_serialization(): + """ + Enable global JSON serialization support for NotGiven types. + """ + global _original_json_encoder + if _original_json_encoder is None: + _original_json_encoder = json.JSONEncoder.default + + def patched_default(self, obj): + if isinstance(obj, NotGiven): + return None + return _original_json_encoder(self, obj) + + json.JSONEncoder.default = patched_default + + +def disable_notgiven_serialization(): + """ + Disable global JSON serialization support for NotGiven types. + + This restores the original JSONEncoder behavior. + """ + global _original_json_encoder + if _original_json_encoder is not None: + json.JSONEncoder.default = _original_json_encoder + _original_json_encoder = None def serialize_kwargs(**kwargs): # Function to check if a value is serializable def is_serializable(value): try: - json.dumps(value) + json.dumps(value, cls=PortkeyJSONEncoder) return True except (TypeError, ValueError): return False @@ -14,14 +56,14 @@ def is_serializable(value): serializable_kwargs = {k: v for k, v in kwargs.items() if is_serializable(v)} # Convert to string representation - return json.dumps(serializable_kwargs) + return json.dumps(serializable_kwargs, cls=PortkeyJSONEncoder) def serialize_args(*args): # Function to check if a value is serializable def is_serializable(value): try: - json.dumps(value) + json.dumps(value, cls=PortkeyJSONEncoder) return True except (TypeError, ValueError): return False @@ -30,4 +72,4 @@ def is_serializable(value): serializable_args = [arg for arg in args if is_serializable(arg)] # Convert to string representation - return json.dumps(serializable_args) + return json.dumps(serializable_args, cls=PortkeyJSONEncoder) diff --git a/tests/test_notgiven_serialization.py b/tests/test_notgiven_serialization.py new file mode 100644 index 00000000..06e1e68e --- /dev/null +++ b/tests/test_notgiven_serialization.py @@ -0,0 +1,121 @@ +""" +Test NotGiven serialization functionality. + +This test verifies that NotGiven sentinel objects can be properly serialized +to JSON, which is necessary for compatibility with external libraries like +openai-agents that may attempt to serialize client objects for logging/tracing. +""" + +import json +import pytest +from portkey_ai._vendor.openai._types import NOT_GIVEN, NotGiven +from portkey_ai.utils.json_utils import ( + PortkeyJSONEncoder, + enable_notgiven_serialization, + disable_notgiven_serialization, +) + + +def test_notgiven_with_custom_encoder(): + """Test that PortkeyJSONEncoder can serialize NotGiven objects.""" + test_obj = { + "key1": "value1", + "key2": NOT_GIVEN, + "key3": 123, + "nested": { + "key4": NOT_GIVEN, + "key5": "value5" + } + } + + # Should not raise TypeError + result = json.dumps(test_obj, cls=PortkeyJSONEncoder) + parsed = json.loads(result) + + # NOT_GIVEN should be serialized as None + assert parsed["key1"] == "value1" + assert parsed["key2"] is None + assert parsed["key3"] == 123 + assert parsed["nested"]["key4"] is None + assert parsed["nested"]["key5"] == "value5" + + +def test_notgiven_in_list(): + """Test that NotGiven objects in lists are properly serialized.""" + test_list = [1, "test", NOT_GIVEN, {"key": NOT_GIVEN}] + + result = json.dumps(test_list, cls=PortkeyJSONEncoder) + parsed = json.loads(result) + + assert parsed[0] == 1 + assert parsed[1] == "test" + assert parsed[2] is None + assert parsed[3]["key"] is None + + +def test_enable_notgiven_serialization(): + """Test that enable_notgiven_serialization allows standard json.dumps to work.""" + # Note: Serialization is now enabled by default when portkey_ai is imported + # This test verifies it works correctly + + # It should work automatically (already enabled by module import) + result = json.dumps({"key": NOT_GIVEN}) + parsed = json.loads(result) + assert parsed["key"] is None + + # Test with nested structures + complex_obj = { + "a": NOT_GIVEN, + "b": [1, NOT_GIVEN, 3], + "c": {"nested": NOT_GIVEN} + } + result = json.dumps(complex_obj) + parsed = json.loads(result) + assert parsed["a"] is None + assert parsed["b"] == [1, None, 3] + assert parsed["c"]["nested"] is None + + # Test that we can disable and re-enable + disable_notgiven_serialization() + with pytest.raises(TypeError, match="not JSON serializable"): + json.dumps({"key": NOT_GIVEN}) + + # Re-enable + enable_notgiven_serialization() + result = json.dumps({"key": NOT_GIVEN}) + parsed = json.loads(result) + assert parsed["key"] is None + + +def test_disable_notgiven_serialization(): + """Test that disable_notgiven_serialization restores original behavior.""" + enable_notgiven_serialization() + + # Should work with patch enabled + json.dumps({"key": NOT_GIVEN}) + + # Disable the patch + disable_notgiven_serialization() + + # Should fail again + with pytest.raises(TypeError, match="not JSON serializable"): + json.dumps({"key": NOT_GIVEN}) + + +def test_notgiven_instance_check(): + """Test that NotGiven instance checking works correctly.""" + assert isinstance(NOT_GIVEN, NotGiven) + assert not isinstance(None, NotGiven) + assert not isinstance("NOT_GIVEN", NotGiven) + + +def test_notgiven_boolean_behavior(): + """Test that NotGiven behaves correctly in boolean contexts.""" + # NotGiven should evaluate to False + assert not NOT_GIVEN + assert bool(NOT_GIVEN) is False + + +def test_notgiven_repr(): + """Test that NotGiven has proper string representation.""" + assert repr(NOT_GIVEN) == "NOT_GIVEN"