diff --git a/CHANGELOG.md b/CHANGELOG.md index 4536d66..a3a838e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,19 +1,59 @@ # Changelog All notable changes to this project's latest version. -## [0.1.23] - 2025-09-25 +## [0.1.24] - 2025-11-06 ### Bug Fixes -- Missing files form last version mess added +- Fixed examples on run cmd +- Init command fixing wip + +### Documentation + +- Sdk folder removed +- Added explanation folder +- Removed example folder +- Removed frameworks folder +- Removed get started folder +- Added how to folder +- Added resources folder +- Added new snippet +- Added tutorials +- Added reference +- Added new api reference +- Added cli overview +- Added new docs.json ### Features -- Runagent cloud support - partial implementation -- Release scripts updated +- Removed deploy-local command +- Deploy-local fully removed +- Updated same upload_agent function names +- Fingerprint feature added back to db +- Added git validation & efficient connectivity check +- Agent run cli working for remote+local +- Run-stream working locally, proper json format. +- Improved langgarph default template +- URL prefix working > rest+socket +- Debug option fixed +- Auth test fixed with updated endpoint +- Runagent deploy working +- Moved user_data json ro sqlite db approach +- Init coomand interactive +- Fixed & improved template downloading +- Split cli cmds into seperate files +- Express login [device-browser code flow] added +- Persistent agent id working +- Cleaned emojis form output +- Added whoami command +- Standardized local agent execution logging +- Auto rewload suported in local server +- Removed replacing agent id +- Muted db replace agent option +- Improved debug log ### Miscellaneous Tasks -- Bump version to v0.1.23 +- Bump version to v0.1.24 diff --git a/pyproject.toml b/pyproject.toml index 9c0dcf8..32bd242 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "hatchling.build" [project] name = "runagent" -version = "0.1.24" +version = "0.1.25" description = "A command-line tool and SDK for deploying, managing, and interacting with AI agents" readme = "README.md" requires-python = ">=3.9" @@ -103,7 +103,7 @@ line_length = 88 skip = ["docs"] [tool.mypy] -python_version = "0.1.24" +python_version = "0.1.25" warn_return_any = true warn_unused_configs = true disallow_untyped_defs = true @@ -159,7 +159,7 @@ fail_under = 80 [tool.ruff] line-length = 88 -target-version = "0.1.24" +target-version = "0.1.25" select = [ "E", # pycodestyle errors "W", # pycodestyle warnings diff --git a/runagent-go/runagent/version.go b/runagent-go/runagent/version.go index a7c836e..d9dd96b 100644 --- a/runagent-go/runagent/version.go +++ b/runagent-go/runagent/version.go @@ -1,4 +1,4 @@ package runagent // Version represents the current version of the RunAgent Go SDK -const Version = "0.1.24" +const Version = "0.1.25" diff --git a/runagent-rust/runagent/Cargo.toml b/runagent-rust/runagent/Cargo.toml index 1db89e2..215f818 100644 --- a/runagent-rust/runagent/Cargo.toml +++ b/runagent-rust/runagent/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "runagent" -version = "0.1.24" +version = "0.1.25" edition = "2021" description = "RunAgent SDK for Rust - Client SDK for interacting with deployed AI agents" license = "MIT" diff --git a/runagent-ts/package-lock.json b/runagent-ts/package-lock.json index 3f266ca..a7a9e5f 100644 --- a/runagent-ts/package-lock.json +++ b/runagent-ts/package-lock.json @@ -1,12 +1,12 @@ { "name": "runagent", - "version": "0.1.24", + "version": "0.1.25", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "runagent", - "version": "0.1.24", + "version": "0.1.25", "dependencies": { "better-sqlite3": "^12.2.0" }, diff --git a/runagent-ts/package.json b/runagent-ts/package.json index e7912b6..8f31ef2 100644 --- a/runagent-ts/package.json +++ b/runagent-ts/package.json @@ -1,6 +1,6 @@ { "name": "runagent", - "version": "0.1.24", + "version": "0.1.25", "type": "module", "files": [ "dist" diff --git a/runagent/__init__.py b/runagent/__init__.py index b224203..20aff99 100644 --- a/runagent/__init__.py +++ b/runagent/__init__.py @@ -5,7 +5,7 @@ built with frameworks like LangGraph, LangChain, and LlamaIndex. """ -__version__ = "0.1.24" +__version__ = "0.1.25" from .client import RunAgentClient diff --git a/runagent/__version__.py b/runagent/__version__.py index e8438af..43a0e4e 100644 --- a/runagent/__version__.py +++ b/runagent/__version__.py @@ -1 +1 @@ -__version__ = "0.1.24" +__version__ = "0.1.25" diff --git a/runagent/cli/commands/run.py b/runagent/cli/commands/run.py index a20a6c9..d1e91e8 100644 --- a/runagent/cli/commands/run.py +++ b/runagent/cli/commands/run.py @@ -199,7 +199,7 @@ def run(ctx, agent_id, host, port, input_file, local, tag, timeout): for key, value in extra_params.items(): # Try to parse value as JSON for complex types # TODO: Will add type inference later - console.print(f" --{key} = [green]{value}[/green]") + console.print(f" {key} = [green]{value}[/green]") input_params = extra_params else: diff --git a/runagent/cli/commands/run_stream.py b/runagent/cli/commands/run_stream.py index f8a40f7..3393526 100644 --- a/runagent/cli/commands/run_stream.py +++ b/runagent/cli/commands/run_stream.py @@ -166,7 +166,7 @@ def run_stream(ctx, agent_id, host, port, input_file, local, tag, timeout): elif extra_params: console.print(" Extra parameters:") for key, value in extra_params.items(): - console.print(f" --{key} = {value}") + console.print(f" {key} = [green]{value}[/green]") input_params = extra_params else: diff --git a/runagent/cli/commands/teardown.py b/runagent/cli/commands/teardown.py index 4edc891..951636a 100644 --- a/runagent/cli/commands/teardown.py +++ b/runagent/cli/commands/teardown.py @@ -46,7 +46,7 @@ def format_error_message(error_info): @click.command() -@click.option("--yes", is_flag=True, help="Skip confirmation") +@click.option("--yes", "-y", is_flag=True, help="Skip confirmation") def teardown(yes): """Complete teardown - Remove RunAgent configuration AND database""" try: diff --git a/runagent/client/client.py b/runagent/client/client.py index 2daed29..6cdf6e8 100644 --- a/runagent/client/client.py +++ b/runagent/client/client.py @@ -108,8 +108,9 @@ def run_stream(self, *input_args, **input_kwargs): self.agent_id, self.entrypoint_tag, input_args=input_args, input_kwargs=input_kwargs ) except Exception as e: - # Handle streaming errors with proper formatting - raise Exception(f"Streaming failed: {str(e)}") + # Error message is already cleaned by socket_client._clean_error_message + # Just re-raise with the cleaned message + raise Exception(str(e)) def _run_stream(self, *input_args, **input_kwargs): """Legacy method - use run_stream instead""" @@ -118,6 +119,18 @@ def _run_stream(self, *input_args, **input_kwargs): def _build_suggestion(self, code: str, message: str) -> str | None: message_lower = (message or "").lower() + # Check for permission/access errors first (403, permission denied, etc.) + if (code == "PERMISSION_ERROR" or code == "AUTHENTICATION_ERROR" or + "403" in message or "permission" in message_lower or + "access denied" in message_lower or "do not have permission" in message_lower): + dashboard_hint = f"https://app.run-agent.ai/dashboard/agents/{self.agent_id}" + return ( + "This agent doesn't belong to your account or your API key doesn't have permission to access it. " + f"Verify the agent ID is correct and that you have access to it. " + f"You can check your agents in the dashboard: {dashboard_hint}. " + f"If this is someone else's agent, you'll need to use their API key or have them share access." + ) + if "not found" in message_lower: tag_match = re.search(r"['\"](?P[A-Za-z0-9_\-]+)['\"]", message or "") if "entrypoint" in message_lower else None dashboard_hint = f"https://app.run-agent.ai/dashboard/agents/{self.agent_id}" diff --git a/runagent/sdk/rest_client.py b/runagent/sdk/rest_client.py index 0e556e8..19e25b9 100644 --- a/runagent/sdk/rest_client.py +++ b/runagent/sdk/rest_client.py @@ -1,6 +1,7 @@ import base64 import json import os +import re import tempfile import time import zipfile @@ -1446,6 +1447,29 @@ def _get_local_deployment_info(self, agent_id: str) -> Optional[Dict]: return None + def _clean_error_message(self, error_message: str) -> str: + """Clean up error messages by removing redundant prefixes""" + if not error_message: + return "Unknown error" + + # Remove common redundant prefixes + prefixes_to_remove = [ + "Server error: ", + "Database error: ", + "HTTP Error: ", + "Agent execution failed: ", + ] + + cleaned = error_message + for prefix in prefixes_to_remove: + if cleaned.startswith(prefix): + cleaned = cleaned[len(prefix):].strip() + + # Remove status codes that appear at the start (e.g., "403: ") + cleaned = re.sub(r'^\d{3}:\s*', '', cleaned) + + return cleaned.strip() if cleaned.strip() else error_message + def run_agent( self, agent_id: str, @@ -1491,14 +1515,61 @@ def run_agent( pass return result - except (ClientError, ServerError, ConnectionError) as e: + except AuthenticationError as e: + # Handle authentication/permission errors (401, 403) + error_message = self._clean_error_message(e.message) + error_code = "AUTHENTICATION_ERROR" + if "403" in error_message or "permission" in error_message.lower() or "access denied" in error_message.lower(): + error_code = "PERMISSION_ERROR" + # Create a clean, single error message for permission errors + error_message = "You do not have permission to access this agent" + return { + "success": False, + "data": None, + "message": None, + "error": { + "code": error_code, + "message": error_message, + "details": None, + "field": None + }, + "timestamp": None, + "request_id": None + } + except ServerError as e: + # Check if server error message contains permission/403 info (even if status is 500) + error_message = e.message + error_code = "SERVER_ERROR" + if ("403" in error_message or "permission" in error_message.lower() or + "access denied" in error_message.lower() or "do not have permission" in error_message.lower()): + error_code = "PERMISSION_ERROR" + # Create a clean, single error message for permission errors + error_message = "You do not have permission to access this agent" + else: + # Clean up server error messages to remove redundant prefixes + error_message = self._clean_error_message(error_message) + return { + "success": False, + "data": None, + "message": None, + "error": { + "code": error_code, + "message": error_message, + "details": None, + "field": None + }, + "timestamp": None, + "request_id": None + } + except (ClientError, ConnectionError) as e: + error_message = self._clean_error_message(e.message) return { "success": False, "data": None, "message": None, "error": { "code": "CONNECTION_ERROR", - "message": f"Agent execution failed: {e.message}", + "message": error_message, "details": None, "field": None }, diff --git a/runagent/sdk/socket_client.py b/runagent/sdk/socket_client.py index 24a2415..92a277f 100644 --- a/runagent/sdk/socket_client.py +++ b/runagent/sdk/socket_client.py @@ -1,6 +1,7 @@ import asyncio import json import os +import re import uuid from typing import Any, AsyncIterator, Iterator, Optional @@ -74,52 +75,59 @@ async def run_stream_async(self, agent_id: str, entrypoint_tag: str, *input_args if not self.is_local and self.api_key: extra_headers["Authorization"] = f"Bearer {self.api_key}" - async with websockets.connect( - uri, - extra_headers=extra_headers if extra_headers else None, - ping_interval=20, - ping_timeout=60, - close_timeout=10, - max_size=10 * 1024 * 1024 - ) as websocket: - # Send start stream request in the exact format required - request_data = { - "entrypoint_tag": entrypoint_tag, - "input_args": list(input_args), # Ensure JSON serializable - "input_kwargs": dict(input_kwargs), # Ensure JSON serializable - "timeout_seconds": 600, - "async_execution": False - } - - self._debug(f"Sending request: {request_data}") - - # Send the request as direct JSON - await websocket.send(json.dumps(request_data)) - - # Receive and yield chunks - async for raw_message in websocket: - try: - message = json.loads(raw_message) - except json.JSONDecodeError: - self._debug(f"[WARN] Invalid JSON message: {raw_message}") - continue + try: + async with websockets.connect( + uri, + extra_headers=extra_headers if extra_headers else None, + ping_interval=20, + ping_timeout=60, + close_timeout=10, + max_size=10 * 1024 * 1024 + ) as websocket: + # Send start stream request in the exact format required + request_data = { + "entrypoint_tag": entrypoint_tag, + "input_args": list(input_args), # Ensure JSON serializable + "input_kwargs": dict(input_kwargs), # Ensure JSON serializable + "timeout_seconds": 600, + "async_execution": False + } - message_type = message.get("type") + self._debug(f"Sending request: {request_data}") - if message_type == "error": - error_msg = message.get('error') or message.get('detail', 'Unknown error') - raise Exception(f"Stream error: {error_msg}") - elif message_type == "status": - status = message.get("status") - if status == "stream_completed": - self._debug("Stream completed") - break - elif status == "stream_started": - self._debug("Stream started") + # Send the request as direct JSON + await websocket.send(json.dumps(request_data)) + + # Receive and yield chunks + async for raw_message in websocket: + try: + message = json.loads(raw_message) + except json.JSONDecodeError: + self._debug(f"[WARN] Invalid JSON message: {raw_message}") continue - elif message_type == "data": - # Yield the actual chunk data - yield message.get("content") + + message_type = message.get("type") + + if message_type == "error": + error_msg = message.get('error') or message.get('detail', 'Unknown error') + cleaned_msg = self._clean_error_message(error_msg) + raise Exception(cleaned_msg) + elif message_type == "status": + status = message.get("status") + if status == "stream_completed": + self._debug("Stream completed") + break + elif status == "stream_started": + self._debug("Stream started") + continue + elif message_type == "data": + # Yield the actual chunk data + yield message.get("content") + except Exception as e: + # Clean up WebSocket connection errors + error_msg = str(e) + cleaned_msg = self._clean_error_message(error_msg) + raise Exception(cleaned_msg) def run_stream(self, agent_id: str, entrypoint_tag: str, input_args, input_kwargs) -> Iterator[Any]: """Stream agent execution results (sync version)""" @@ -140,54 +148,97 @@ def run_stream(self, agent_id: str, entrypoint_tag: str, input_args, input_kwarg extra_headers["Authorization"] = f"Bearer {self.api_key}" # Add proper timeout and keepalive settings - with connect( - uri, - additional_headers=extra_headers if extra_headers else None, - ping_interval=20, # Send ping every 20 seconds - ping_timeout=60, # Wait up to 60 seconds for pong - close_timeout=10, # Timeout for closing handshake - max_size=10 * 1024 * 1024, # 10MB max message size - open_timeout=30 # FIXED: Add connection timeout - ) as websocket: + try: + with connect( + uri, + additional_headers=extra_headers if extra_headers else None, + ping_interval=20, # Send ping every 20 seconds + ping_timeout=60, # Wait up to 60 seconds for pong + close_timeout=10, # Timeout for closing handshake + max_size=10 * 1024 * 1024, # 10MB max message size + open_timeout=30 # FIXED: Add connection timeout + ) as websocket: - # Send start stream request in the exact format required - request_data = { - "entrypoint_tag": entrypoint_tag, - "input_args": list(input_args) if input_args else [], - "input_kwargs": dict(input_kwargs) if input_kwargs else {}, - "timeout_seconds": 600, - "async_execution": False - } - - self._debug(f"Sending request: {request_data}") - - # Send the request as direct JSON - websocket.send(json.dumps(request_data)) - - # Receive and yield chunks - for raw_message in websocket: - try: - message = json.loads(raw_message) - except json.JSONDecodeError: - self._debug(f"[WARN] Invalid JSON message: {raw_message}") - continue + # Send start stream request in the exact format required + request_data = { + "entrypoint_tag": entrypoint_tag, + "input_args": list(input_args) if input_args else [], + "input_kwargs": dict(input_kwargs) if input_kwargs else {}, + "timeout_seconds": 600, + "async_execution": False + } + + self._debug(f"Sending request: {request_data}") - message_type = message.get("type") + # Send the request as direct JSON + websocket.send(json.dumps(request_data)) - if message_type == "error": - error_msg = message.get('error') or message.get('detail', 'Unknown error') - raise Exception(f"Stream error: {error_msg}") - elif message_type == "status": - status = message.get("status") - if status == "stream_completed": - self._debug("Stream completed") - break - elif status == "stream_started": - self._debug("Stream started") + # Receive and yield chunks + for raw_message in websocket: + try: + message = json.loads(raw_message) + except json.JSONDecodeError: + self._debug(f"[WARN] Invalid JSON message: {raw_message}") continue - elif message_type == "data": - # Yield the actual chunk data - yield message.get("content") + + message_type = message.get("type") + + if message_type == "error": + error_msg = message.get('error') or message.get('detail', 'Unknown error') + cleaned_msg = self._clean_error_message(error_msg) + raise Exception(cleaned_msg) + elif message_type == "status": + status = message.get("status") + if status == "stream_completed": + self._debug("Stream completed") + break + elif status == "stream_started": + self._debug("Stream started") + continue + elif message_type == "data": + # Yield the actual chunk data + yield message.get("content") + except Exception as e: + # Clean up WebSocket connection errors + error_msg = str(e) + cleaned_msg = self._clean_error_message(error_msg) + raise Exception(cleaned_msg) + + def _clean_error_message(self, error_message: str) -> str: + """Clean up error messages by removing redundant prefixes""" + if not error_message: + return "Unknown error" + + # Remove common redundant prefixes + prefixes_to_remove = [ + "received 1011 (internal error) ", + "Internal server error: ", + "Server error: ", + "Database error: ", + "HTTP Error: ", + "Stream error: ", + "Streaming failed: ", + ] + + cleaned = error_message + for prefix in prefixes_to_remove: + if cleaned.startswith(prefix): + cleaned = cleaned[len(prefix):].strip() + + # Remove status codes that appear at the start (e.g., "500: ", "403: ") + cleaned = re.sub(r'^\d{3}:\s*', '', cleaned) + + # Remove duplicate error messages (e.g., "error; then sent error") + if "; then sent" in cleaned.lower(): + # Extract just the first part before "; then sent" + cleaned = cleaned.split("; then sent")[0].strip() + + # Check if this is a permission error and provide a clean message + if ("403" in cleaned or "permission" in cleaned.lower() or + "access denied" in cleaned.lower() or "do not have permission" in cleaned.lower()): + return "You do not have permission to access this agent" + + return cleaned.strip() if cleaned.strip() else error_message def _debug(self, message: str) -> None: if os.getenv("RUNAGENT_DEBUG") or os.getenv("DISABLE_TRY_CATCH"): diff --git a/runagent/utils/serializer.py b/runagent/utils/serializer.py index 4271384..e7995bb 100644 --- a/runagent/utils/serializer.py +++ b/runagent/utils/serializer.py @@ -1,8 +1,10 @@ import os import json import logging +import math from typing import Any, Dict, Optional, Union from dataclasses import asdict +from datetime import datetime, date, time from runagent.utils.schema import SafeMessage class CoreSerializer: @@ -153,6 +155,247 @@ def deserialize_message(self, json_str: str) -> SafeMessage: self.logger.error(f"Message deserialization failed: {e}") raise + def serialize_object_to_structured(self, obj: Any) -> str: + """ + Serialize object to structured format with explicit type information. + + Returns a JSON string containing: + - type: OpenAPI-compatible type (string, integer, number, boolean, array, object, null) + - payload: JSON-stringified representation of the object + + Args: + obj: Any object to serialize + + Returns: + JSON string with 'type' and 'payload' keys + """ + try: + # Determine type before serialization + data_type = self._determine_type(obj) + + # Handle special conversions before JSON serialization + obj_to_serialize = self._prepare_for_serialization(obj) + + # Serialize to JSON string + payload = json.dumps( + obj_to_serialize, + ensure_ascii=False, + allow_nan=False, # Explicitly reject NaN/Inf + default=self._json_serializer_fallback + ) + + if not self.check_size_limit(payload): + self.logger.warning(f"Serialized payload exceeds size limit: {len(payload)} bytes") + + # Return the structured format as a JSON string + structured = { + "type": data_type, + "payload": payload + } + return json.dumps(structured, ensure_ascii=False) + + except (ValueError, TypeError) as e: + # Handle NaN/Inf or other JSON errors + if os.getenv('DISABLE_TRY_CATCH'): + raise + self.logger.error(f"Structured serialization failed: {e}") + error_payload = json.dumps({ + "error": f"Serialization Error: {str(e)}", + "original_type": str(type(obj)) + }) + error_structured = { + "type": "object", + "payload": error_payload + } + return json.dumps(error_structured, ensure_ascii=False) + except Exception as e: + if os.getenv('DISABLE_TRY_CATCH'): + raise + self.logger.error(f"Structured serialization failed: {e}") + error_payload = json.dumps({ + "error": f"Serialization Error: {str(e)}", + "original_type": str(type(obj)) + }) + error_structured = { + "type": "object", + "payload": error_payload + } + return json.dumps(error_structured, ensure_ascii=False) + + def deserialize_object_from_structured(self, structured_json: str) -> Any: + """ + Deserialize object from structured format JSON string. + + Args: + structured_json: JSON string with 'type' and 'payload' keys + + Returns: + Deserialized Python object + """ + try: + # Parse the JSON string + if not isinstance(structured_json, str): + raise ValueError(f"Expected JSON string input, got {type(structured_json)}") + + structured_data = json.loads(structured_json) + + if not isinstance(structured_data, dict): + raise ValueError(f"JSON must deserialize to a dict, got {type(structured_data)}") + + data_type = structured_data.get("type") + payload = structured_data.get("payload") + + if data_type is None or payload is None: + raise ValueError("Structured data must have 'type' and 'payload' keys") + + # Parse payload based on type + if data_type == "null": + return None + elif data_type == "string": + # Payload is JSON string, parse to get the actual string + return json.loads(payload) + elif data_type == "integer": + value = json.loads(payload) + return int(value) + elif data_type == "number": + value = json.loads(payload) + return float(value) + elif data_type == "boolean": + return json.loads(payload) + elif data_type in ("array", "object"): + # Parse JSON to get structured data + return json.loads(payload) + else: + # Unknown type, try to parse as JSON + self.logger.warning(f"Unknown type '{data_type}', attempting JSON parse") + return json.loads(payload) + + except json.JSONDecodeError as e: + if os.getenv('DISABLE_TRY_CATCH'): + raise + self.logger.error(f"Failed to deserialize structured data: {e}") + raise ValueError(f"Invalid structured data: {e}") + except Exception as e: + if os.getenv('DISABLE_TRY_CATCH'): + raise + self.logger.error(f"Deserialization failed: {e}") + raise + + def _determine_type(self, obj: Any) -> str: + """ + Determine OpenAPI-compatible type for an object. + + Returns: string, integer, number, boolean, array, object, null + """ + # Check None first + if obj is None: + return "null" + + # Check bool before int (bool is subclass of int in Python) + if isinstance(obj, bool): + return "boolean" + + # Check numeric types + if isinstance(obj, int): + return "integer" + + if isinstance(obj, float): + # Check for NaN or Infinity + if math.isnan(obj) or math.isinf(obj): + self.logger.warning(f"Float value is NaN or Inf: {obj}, treating as number") + return "number" + + # Check string + if isinstance(obj, str): + return "string" + + # Check array-like (list, tuple, set) + if isinstance(obj, (list, tuple, set)): + return "array" + + # Check dict + if isinstance(obj, dict): + return "object" + + # For complex objects, check if they can be converted to dict + if hasattr(obj, 'to_dict') or hasattr(obj, 'model_dump') or hasattr(obj, 'dict') or hasattr(obj, '__dict__'): + return "object" + + # Fallback: will be stringified + self.logger.warning(f"Unknown type {type(obj)}, will stringify as 'string'") + return "string" + + def _prepare_for_serialization(self, obj: Any) -> Any: + """ + Prepare object for JSON serialization by converting special types. + """ + # Handle None + if obj is None: + return None + + # Handle primitives (pass through) + if isinstance(obj, (bool, int, str)): + return obj + + # Handle floats with NaN/Inf check + if isinstance(obj, float): + if math.isnan(obj): + # Convert NaN to null + self.logger.warning("Converting NaN to null") + return None + elif math.isinf(obj): + # Convert Infinity to string representation + self.logger.warning(f"Converting Infinity to string: {obj}") + return str(obj) + return obj + + # Handle datetime objects - convert to ISO format string + if isinstance(obj, datetime): + return obj.isoformat() + if isinstance(obj, date): + return obj.isoformat() + if isinstance(obj, time): + return obj.isoformat() + + # Handle sets - convert to list + if isinstance(obj, set): + return list(obj) + + # Handle bytes - convert to base64 or hex string + if isinstance(obj, (bytes, bytearray)): + self.logger.warning("Converting bytes to hex string") + return obj.hex() + + # For pandas DataFrame, try to_dict + if hasattr(obj, 'to_dict') and callable(getattr(obj, 'to_dict')): + try: + return obj.to_dict() + except Exception as e: + self.logger.warning(f"Failed to call to_dict on {type(obj)}: {e}") + + # For Pydantic models + if hasattr(obj, 'model_dump'): + try: + return obj.model_dump() + except Exception as e: + self.logger.warning(f"Failed to call model_dump on {type(obj)}: {e}") + + if hasattr(obj, 'dict'): + try: + return obj.dict() + except Exception as e: + self.logger.warning(f"Failed to call dict on {type(obj)}: {e}") + + # Collections - recursively prepare elements + if isinstance(obj, (list, tuple)): + return [self._prepare_for_serialization(item) for item in obj] + + if isinstance(obj, dict): + return {k: self._prepare_for_serialization(v) for k, v in obj.items()} + + # Last resort: use the fallback which will be called by json.dumps + return obj + def _json_serializer_fallback(self, obj: Any) -> Any: """ Simplified fallback serializer for JSON.dumps when encountering non-serializable objects