BrianIsaac commited on
Commit
223daa0
Β·
1 Parent(s): 7c6a236

fix: resolve Decimal serialisation and UUID generation errors

Browse files

- Add orjson for high-performance JSON serialisation with Decimal support
- Convert Decimal to string (not float) to preserve financial precision
- Implement client-side UUID v5 generation to remove uuid-ossp dependency
- Configure Supabase service role client with proper authentication
- Improve error handling in portfolio save flow with return value checks

This fixes three critical issues:
1. Redis cache Decimal serialisation errors
2. PostgreSQL uuid_generate_v5 function missing errors
3. Supabase RLS failures due to improper authentication configuration

Files modified:
- backend/utils/serialisation.py (new): Decimal-safe JSON utilities
- backend/utils/uuid_generator.py (new): Client-side UUID generation
- backend/caching/redis_cache.py: Use orjson for cache serialisation
- backend/database.py: Python UUID generation + Decimal handling
- app.py: Improved error handling and validation
- pyproject.toml: Add orjson dependency

app.py CHANGED
@@ -812,15 +812,22 @@ async def run_analysis_with_ui_update(
812
  logger.error("No valid user_id available for portfolio creation")
813
  raise ValueError("User ID not available")
814
 
815
- await db.save_portfolio(
816
  portfolio_id=portfolio_id,
817
  user_id=user_id,
818
  name=f"Analysis {datetime.now().strftime('%Y-%m-%d %H:%M')}",
819
  risk_tolerance='moderate'
820
  )
821
- logger.info(f"Portfolio {portfolio_id} created for user {user_id}")
 
 
 
 
 
 
822
  except Exception as e:
823
- logger.warning(f"Failed to save portfolio: {e}")
 
824
  else:
825
  logger.info(f"Demo mode: Portfolio {portfolio_id} created (ephemeral, not saved to database)")
826
 
@@ -876,9 +883,12 @@ async def run_analysis_with_ui_update(
876
  if save_result:
877
  logger.info(f"βœ“ Successfully saved analysis for portfolio {portfolio_id}")
878
  else:
879
- logger.warning(f"βœ— Failed to save analysis for portfolio {portfolio_id} (returned False)")
 
 
880
  except Exception as e:
881
  logger.error(f"βœ— Exception saving analysis for portfolio {portfolio_id}: {e}", exc_info=True)
 
882
  else:
883
  logger.info(f"Demo mode: Analysis for portfolio {portfolio_id} not saved (ephemeral session)")
884
 
 
812
  logger.error("No valid user_id available for portfolio creation")
813
  raise ValueError("User ID not available")
814
 
815
+ portfolio_saved = await db.save_portfolio(
816
  portfolio_id=portfolio_id,
817
  user_id=user_id,
818
  name=f"Analysis {datetime.now().strftime('%Y-%m-%d %H:%M')}",
819
  risk_tolerance='moderate'
820
  )
821
+
822
+ if portfolio_saved:
823
+ logger.info(f"βœ“ Portfolio {portfolio_id} created for user {user_id}")
824
+ else:
825
+ logger.error(f"βœ— Failed to create portfolio {portfolio_id} in database")
826
+ raise ValueError(f"Portfolio creation failed for {portfolio_id}")
827
+
828
  except Exception as e:
829
+ logger.error(f"βœ— Failed to save portfolio: {e}")
830
+ raise ValueError(f"Database error: {e}") from e
831
  else:
832
  logger.info(f"Demo mode: Portfolio {portfolio_id} created (ephemeral, not saved to database)")
833
 
 
883
  if save_result:
884
  logger.info(f"βœ“ Successfully saved analysis for portfolio {portfolio_id}")
885
  else:
886
+ logger.error(f"βœ— Failed to save analysis for portfolio {portfolio_id} (returned False)")
887
+ # Note: We don't raise here because the analysis itself succeeded
888
+ # The user still gets their results even if database save fails
889
  except Exception as e:
890
  logger.error(f"βœ— Exception saving analysis for portfolio {portfolio_id}: {e}", exc_info=True)
891
+ # Note: We don't raise here because the analysis itself succeeded
892
  else:
893
  logger.info(f"Demo mode: Analysis for portfolio {portfolio_id} not saved (ephemeral session)")
894
 
backend/caching/redis_cache.py CHANGED
@@ -420,7 +420,14 @@ class HybridCache:
420
 
421
  try:
422
  if self.redis_client:
423
- serialised = serialiser(value) if serialiser else json.dumps(value)
 
 
 
 
 
 
 
424
  if ttl:
425
  self.redis_client.setex(key, ttl, serialised)
426
  else:
 
420
 
421
  try:
422
  if self.redis_client:
423
+ # Use custom serialiser if provided, otherwise use orjson with Decimal support
424
+ if serialiser:
425
+ serialised = serialiser(value)
426
+ else:
427
+ # Import here to avoid circular dependencies
428
+ from backend.utils.serialisation import dumps
429
+ serialised = dumps(value)
430
+
431
  if ttl:
432
  self.redis_client.setex(key, ttl, serialised)
433
  else:
backend/database.py CHANGED
@@ -4,42 +4,17 @@ Handles Supabase PostgreSQL connections and operations.
4
  """
5
 
6
  from typing import Optional, Dict, Any, List
7
- from decimal import Decimal
8
- from datetime import datetime, date
9
  from supabase import create_client, Client
10
  from backend.config import settings
 
 
11
  import logging
12
 
13
  logger = logging.getLogger(__name__)
14
 
15
 
16
- def decimal_to_float(obj: Any) -> Any:
17
- """Recursively convert non-JSON-serializable objects in nested structures.
18
-
19
- Converts:
20
- - Decimal objects to float (financial calculations)
21
- - datetime/date objects to ISO format strings (timestamps)
22
-
23
- This is needed because JSON serialization doesn't support these types,
24
- but financial data often contains them.
25
-
26
- Args:
27
- obj: Any Python object (dict, list, Decimal, datetime, etc.)
28
-
29
- Returns:
30
- Same structure with non-serializable objects converted
31
- """
32
- if isinstance(obj, Decimal):
33
- return float(obj)
34
- elif isinstance(obj, (datetime, date)):
35
- return obj.isoformat()
36
- elif isinstance(obj, dict):
37
- return {k: decimal_to_float(v) for k, v in obj.items()}
38
- elif isinstance(obj, list):
39
- return [decimal_to_float(item) for item in obj]
40
- elif isinstance(obj, tuple):
41
- return tuple(decimal_to_float(item) for item in obj)
42
- return obj
43
 
44
 
45
  class Database:
@@ -59,19 +34,32 @@ class Database:
59
  if settings.supabase_service_role_key:
60
  api_key = settings.supabase_service_role_key
61
  logger.info("Using Supabase service role key (bypasses RLS)")
 
 
 
 
 
 
 
 
 
62
  elif settings.supabase_key:
63
  api_key = settings.supabase_key
64
- logger.warning("Using anon key - history features may not work due to RLS. Add SUPABASE_SERVICE_ROLE_KEY for full functionality.")
 
 
 
 
 
 
 
 
 
 
 
65
  else:
66
  logger.error("No Supabase API key found (need either SUPABASE_SERVICE_ROLE_KEY or SUPABASE_KEY)")
67
  return
68
-
69
- # Simple client creation for backend service (no session management needed)
70
- self.client = create_client(
71
- settings.supabase_url,
72
- api_key
73
- )
74
- logger.info("Supabase client initialised successfully")
75
  except Exception as e:
76
  logger.error(f"Failed to initialise Supabase client: {e}")
77
  logger.info("Running in demo mode without database")
@@ -87,28 +75,24 @@ class Database:
87
  return self.client is not None
88
 
89
  def _string_to_uuid(self, string_id: str) -> str:
90
- """Convert string ID to deterministic UUID using uuid_generate_v5.
 
 
 
 
91
 
92
  Args:
93
- string_id: String identifier (e.g., 'demo_20251117_143022')
94
 
95
  Returns:
96
- UUID string
97
- """
98
- if not self.is_connected():
99
- return string_id
100
-
101
- try:
102
- result = self.client.rpc('string_to_portfolio_uuid', {
103
- 'input_string': string_id
104
- }).execute()
105
 
106
- if result.data:
107
- return str(result.data)
108
- except Exception as e:
109
- logger.warning(f"Failed to convert string to UUID: {e}")
110
-
111
- return string_id
112
 
113
  async def ensure_demo_user_exists(self) -> str:
114
  """Ensure demo user exists in database.
@@ -200,9 +184,9 @@ class Database:
200
  try:
201
  uuid_portfolio_id = self._string_to_uuid(portfolio_id)
202
 
203
- # Convert all Decimal objects to float for JSON serialization
204
- # Financial calculations often return Decimals which aren't JSON-serializable
205
- analysis_clean = decimal_to_float(analysis_results)
206
 
207
  data = {
208
  'portfolio_id': uuid_portfolio_id,
 
4
  """
5
 
6
  from typing import Optional, Dict, Any, List
 
 
7
  from supabase import create_client, Client
8
  from backend.config import settings
9
+ from backend.utils.serialisation import decimal_to_json_safe
10
+ from backend.utils.uuid_generator import string_to_uuid
11
  import logging
12
 
13
  logger = logging.getLogger(__name__)
14
 
15
 
16
+ # Re-export for backwards compatibility
17
+ __all__ = ['Database', 'decimal_to_json_safe']
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
 
19
 
20
  class Database:
 
34
  if settings.supabase_service_role_key:
35
  api_key = settings.supabase_service_role_key
36
  logger.info("Using Supabase service role key (bypasses RLS)")
37
+
38
+ # Simple client creation for backend service operations
39
+ # Service role automatically bypasses RLS, no special config needed
40
+ self.client = create_client(
41
+ settings.supabase_url,
42
+ api_key
43
+ )
44
+ logger.info("Supabase service role client initialised successfully")
45
+
46
  elif settings.supabase_key:
47
  api_key = settings.supabase_key
48
+ logger.warning(
49
+ "Using anon key - database operations may fail due to RLS policies. "
50
+ "Add SUPABASE_SERVICE_ROLE_KEY environment variable for full functionality."
51
+ )
52
+
53
+ # Anon key client (RLS enforced, requires user context)
54
+ self.client = create_client(
55
+ settings.supabase_url,
56
+ api_key
57
+ )
58
+ logger.info("Supabase anon client initialised (RLS enforced)")
59
+
60
  else:
61
  logger.error("No Supabase API key found (need either SUPABASE_SERVICE_ROLE_KEY or SUPABASE_KEY)")
62
  return
 
 
 
 
 
 
 
63
  except Exception as e:
64
  logger.error(f"Failed to initialise Supabase client: {e}")
65
  logger.info("Running in demo mode without database")
 
75
  return self.client is not None
76
 
77
  def _string_to_uuid(self, string_id: str) -> str:
78
+ """Convert string ID to deterministic UUID.
79
+
80
+ Uses Python UUID v5 generation for deterministic conversion.
81
+ This is faster than database-side generation and doesn't
82
+ require the uuid-ossp extension.
83
 
84
  Args:
85
+ string_id: Human-readable ID (e.g., 'demo_20241119_105323')
86
 
87
  Returns:
88
+ UUID string (deterministic, always same for same input)
 
 
 
 
 
 
 
 
89
 
90
+ Example:
91
+ >>> db = Database()
92
+ >>> db._string_to_uuid('demo_20241119_105323')
93
+ '...' # Same UUID every time for this input
94
+ """
95
+ return string_to_uuid(string_id)
96
 
97
  async def ensure_demo_user_exists(self) -> str:
98
  """Ensure demo user exists in database.
 
184
  try:
185
  uuid_portfolio_id = self._string_to_uuid(portfolio_id)
186
 
187
+ # Convert all Decimal objects to string for JSON serialisation
188
+ # This preserves precision - float conversion loses pennies in large numbers
189
+ analysis_clean = decimal_to_json_safe(analysis_results)
190
 
191
  data = {
192
  'portfolio_id': uuid_portfolio_id,
backend/utils/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Utility modules for the Portfolio Intelligence Platform."""
backend/utils/serialisation.py ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """JSON serialisation utilities with Decimal precision preservation.
2
+
3
+ This module provides safe JSON serialisation for financial data,
4
+ ensuring Decimal types are converted to strings (not floats) to
5
+ preserve full precision. Converting Decimal to float can cause
6
+ monetary errors in large values (e.g., Β£1,000,000,000.47 β†’ Β£1,000,000,000.50).
7
+ """
8
+
9
+ from typing import Any
10
+ from decimal import Decimal
11
+ from datetime import datetime, date
12
+ import orjson
13
+ import logging
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ def default_handler(obj: Any) -> Any:
19
+ """Custom default handler for orjson serialisation.
20
+
21
+ Handles types that aren't natively JSON-serialisable:
22
+ - Decimal β†’ string (preserves precision for financial data)
23
+ - datetime/date β†’ ISO format string
24
+
25
+ Args:
26
+ obj: Object to serialise
27
+
28
+ Returns:
29
+ JSON-serialisable representation
30
+
31
+ Raises:
32
+ TypeError: If object type is not handled
33
+
34
+ Example:
35
+ >>> import orjson
36
+ >>> from decimal import Decimal
37
+ >>> orjson.dumps({"price": Decimal("19.99")}, default=default_handler)
38
+ b'{"price":"19.99"}'
39
+ """
40
+ if isinstance(obj, Decimal):
41
+ # CRITICAL: Convert to string, NOT float
42
+ # Float loses precision in large numbers
43
+ return str(obj)
44
+ elif isinstance(obj, (datetime, date)):
45
+ return obj.isoformat()
46
+ raise TypeError(f"Type {type(obj)} not serialisable")
47
+
48
+
49
+ def dumps(obj: Any) -> bytes:
50
+ """Serialise object to JSON bytes with Decimal support.
51
+
52
+ Args:
53
+ obj: Object to serialise
54
+
55
+ Returns:
56
+ JSON bytes with Decimal values as strings
57
+
58
+ Example:
59
+ >>> from decimal import Decimal
60
+ >>> dumps({"price": Decimal("19.99"), "quantity": 5})
61
+ b'{"price":"19.99","quantity":5}'
62
+ """
63
+ return orjson.dumps(obj, default=default_handler)
64
+
65
+
66
+ def dumps_str(obj: Any) -> str:
67
+ """Serialise object to JSON string with Decimal support.
68
+
69
+ Args:
70
+ obj: Object to serialise
71
+
72
+ Returns:
73
+ JSON string with Decimal values as strings
74
+
75
+ Example:
76
+ >>> from decimal import Decimal
77
+ >>> dumps_str({"price": Decimal("19.99")})
78
+ '{"price":"19.99"}'
79
+ """
80
+ return dumps(obj).decode('utf-8')
81
+
82
+
83
+ def decimal_to_json_safe(obj: Any) -> Any:
84
+ """Recursively convert non-JSON-serialisable objects to safe types.
85
+
86
+ This function provides an alternative approach when you need
87
+ Python dict/list structures rather than JSON bytes.
88
+
89
+ Converts:
90
+ - Decimal objects β†’ string (preserves full precision)
91
+ - datetime/date objects β†’ ISO format strings
92
+
93
+ This preserves precision for financial calculations, which is critical
94
+ as converting Decimal to float can lose pence in large numbers.
95
+
96
+ Args:
97
+ obj: Any Python object (dict, list, Decimal, datetime, etc.)
98
+
99
+ Returns:
100
+ Same structure with non-serialisable objects converted
101
+
102
+ Example:
103
+ >>> from decimal import Decimal
104
+ >>> decimal_to_json_safe({"price": Decimal('19.99')})
105
+ {'price': '19.99'}
106
+ >>> decimal_to_json_safe([Decimal('1.23'), Decimal('4.56')])
107
+ ['1.23', '4.56']
108
+ """
109
+ if isinstance(obj, Decimal):
110
+ # CRITICAL: String preserves precision, float does not
111
+ return str(obj)
112
+ elif isinstance(obj, (datetime, date)):
113
+ return obj.isoformat()
114
+ elif isinstance(obj, dict):
115
+ return {k: decimal_to_json_safe(v) for k, v in obj.items()}
116
+ elif isinstance(obj, list):
117
+ return [decimal_to_json_safe(item) for item in obj]
118
+ elif isinstance(obj, tuple):
119
+ return tuple(decimal_to_json_safe(item) for item in obj)
120
+ return obj
backend/utils/uuid_generator.py ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """UUID generation utilities for deterministic ID conversion.
2
+
3
+ This module provides client-side UUID generation to convert human-readable
4
+ identifiers (like 'portfolio-demo', 'user-123') into deterministic UUIDs.
5
+ This approach is faster and more portable than database-side generation.
6
+ """
7
+
8
+ import uuid
9
+ from typing import Union
10
+ import logging
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ # Use null UUID as root namespace for application IDs
15
+ NULL_NAMESPACE = uuid.UUID('00000000-0000-0000-0000-000000000000')
16
+
17
+
18
+ class DeterministicUUIDGenerator:
19
+ """Generates deterministic UUIDs for consistent ID conversion.
20
+
21
+ This class provides a standardised way to convert human-readable
22
+ identifiers into deterministic UUIDs that remain consistent across
23
+ sessions using UUID v5 (SHA-1 based).
24
+
25
+ Args:
26
+ namespace: Base UUID for the namespace. Defaults to null UUID.
27
+
28
+ Example:
29
+ >>> generator = DeterministicUUIDGenerator()
30
+ >>> portfolio_id = generator.generate('portfolio-demo')
31
+ >>> same_id = generator.generate('portfolio-demo')
32
+ >>> portfolio_id == same_id
33
+ True
34
+ """
35
+
36
+ def __init__(self, namespace: Union[str, uuid.UUID, None] = None) -> None:
37
+ """Initialise the UUID generator with a namespace.
38
+
39
+ Args:
40
+ namespace: Base UUID for generation. If None, uses null UUID.
41
+ """
42
+ if namespace is None:
43
+ self.namespace = NULL_NAMESPACE
44
+ elif isinstance(namespace, str):
45
+ self.namespace = uuid.UUID(namespace)
46
+ else:
47
+ self.namespace = namespace
48
+
49
+ logger.debug(f"UUID generator initialised with namespace: {self.namespace}")
50
+
51
+ def generate(self, identifier: str) -> uuid.UUID:
52
+ """Generate deterministic UUID from identifier.
53
+
54
+ Uses UUID v5 (SHA-1 hashing) to create a deterministic UUID.
55
+ Same input always produces the same output.
56
+
57
+ Args:
58
+ identifier: Human-readable identifier string
59
+
60
+ Returns:
61
+ Deterministic UUID that will always be the same for this identifier
62
+
63
+ Example:
64
+ >>> gen = DeterministicUUIDGenerator()
65
+ >>> gen.generate('demo-portfolio')
66
+ UUID('...')
67
+ """
68
+ if not identifier:
69
+ raise ValueError("Identifier cannot be empty")
70
+
71
+ return uuid.uuid5(namespace=self.namespace, name=identifier)
72
+
73
+ def generate_str(self, identifier: str) -> str:
74
+ """Generate deterministic UUID as string.
75
+
76
+ Args:
77
+ identifier: Human-readable identifier string
78
+
79
+ Returns:
80
+ UUID as string with hyphens (e.g., '550e8400-e29b-41d4-a716-446655440000')
81
+ """
82
+ return str(self.generate(identifier))
83
+
84
+ def generate_hex(self, identifier: str) -> str:
85
+ """Generate deterministic UUID as hex string (no hyphens).
86
+
87
+ Args:
88
+ identifier: Human-readable identifier string
89
+
90
+ Returns:
91
+ UUID as hex string without hyphens (e.g., '550e8400e29b41d4a716446655440000')
92
+ """
93
+ return self.generate(identifier).hex
94
+
95
+
96
+ # Global instance for application use
97
+ _generator = DeterministicUUIDGenerator()
98
+
99
+
100
+ def string_to_uuid(identifier: str) -> str:
101
+ """Convert string identifier to deterministic UUID string.
102
+
103
+ Convenience function using the global UUID generator.
104
+
105
+ Args:
106
+ identifier: Human-readable identifier (e.g., 'user-123', 'demo-portfolio')
107
+
108
+ Returns:
109
+ UUID string representation
110
+
111
+ Example:
112
+ >>> string_to_uuid('portfolio-demo')
113
+ '...'
114
+ """
115
+ return _generator.generate_str(identifier)
116
+
117
+
118
+ def validate_or_generate_uuid(provided_uuid: Union[str, None], fallback_identifier: str) -> str:
119
+ """Validate provided UUID or generate from identifier.
120
+
121
+ Args:
122
+ provided_uuid: UUID string from client (if provided)
123
+ fallback_identifier: Identifier to use if validation fails
124
+
125
+ Returns:
126
+ Valid UUID string
127
+
128
+ Example:
129
+ >>> validate_or_generate_uuid(None, 'user-123')
130
+ '...'
131
+ >>> validate_or_generate_uuid('invalid', 'user-123')
132
+ '...'
133
+ """
134
+ if provided_uuid:
135
+ try:
136
+ # Validate it's a proper UUID
137
+ validated = uuid.UUID(provided_uuid)
138
+ # Verify it matches expected deterministic value
139
+ expected = _generator.generate(fallback_identifier)
140
+ if validated == expected:
141
+ return str(validated)
142
+ else:
143
+ logger.warning(
144
+ f"Provided UUID {provided_uuid} doesn't match expected "
145
+ f"deterministic UUID for {fallback_identifier}"
146
+ )
147
+ except (ValueError, AttributeError) as e:
148
+ logger.warning(f"Invalid UUID provided: {provided_uuid}, error: {e}")
149
+
150
+ # Generate server-side if validation fails or no UUID provided
151
+ return string_to_uuid(fallback_identifier)
pyproject.toml CHANGED
@@ -26,6 +26,7 @@ dependencies = [
26
  # Caching & Rate Limiting
27
  "redis>=5.0.0",
28
  "upstash-redis>=0.15.0",
 
29
  # Quantitative Finance (P0 - Deterministic Models)
30
  # Research validated: Package name is 'pyportfolioopt', import as 'pypfopt'
31
  "pyportfolioopt>=1.5.6",
 
26
  # Caching & Rate Limiting
27
  "redis>=5.0.0",
28
  "upstash-redis>=0.15.0",
29
+ "orjson>=3.10.0",
30
  # Quantitative Finance (P0 - Deterministic Models)
31
  # Research validated: Package name is 'pyportfolioopt', import as 'pypfopt'
32
  "pyportfolioopt>=1.5.6",
uv.lock CHANGED
@@ -3167,6 +3167,7 @@ dependencies = [
3167
  { name = "langgraph" },
3168
  { name = "mcp" },
3169
  { name = "numpy" },
 
3170
  { name = "pandas" },
3171
  { name = "psycopg2-binary" },
3172
  { name = "pydantic" },
@@ -3212,6 +3213,7 @@ requires-dist = [
3212
  { name = "langgraph", specifier = ">=0.1.0" },
3213
  { name = "mcp", specifier = ">=1.15.0" },
3214
  { name = "numpy", specifier = ">=2.0.0" },
 
3215
  { name = "pandas", specifier = ">=2.2.3" },
3216
  { name = "psycopg2-binary", specifier = ">=2.9.9" },
3217
  { name = "pydantic", specifier = ">=2.5.0" },
 
3167
  { name = "langgraph" },
3168
  { name = "mcp" },
3169
  { name = "numpy" },
3170
+ { name = "orjson" },
3171
  { name = "pandas" },
3172
  { name = "psycopg2-binary" },
3173
  { name = "pydantic" },
 
3213
  { name = "langgraph", specifier = ">=0.1.0" },
3214
  { name = "mcp", specifier = ">=1.15.0" },
3215
  { name = "numpy", specifier = ">=2.0.0" },
3216
+ { name = "orjson", specifier = ">=3.10.0" },
3217
  { name = "pandas", specifier = ">=2.2.3" },
3218
  { name = "psycopg2-binary", specifier = ">=2.9.9" },
3219
  { name = "pydantic", specifier = ">=2.5.0" },