chuk-tool-processor 0.6.15__tar.gz → 0.6.18__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 chuk-tool-processor might be problematic. Click here for more details.

Files changed (65) hide show
  1. {chuk_tool_processor-0.6.15 → chuk_tool_processor-0.6.18}/PKG-INFO +2 -2
  2. {chuk_tool_processor-0.6.15 → chuk_tool_processor-0.6.18}/pyproject.toml +2 -2
  3. {chuk_tool_processor-0.6.15 → chuk_tool_processor-0.6.18}/src/chuk_tool_processor/core/processor.py +1 -1
  4. {chuk_tool_processor-0.6.15 → chuk_tool_processor-0.6.18}/src/chuk_tool_processor/execution/strategies/subprocess_strategy.py +2 -1
  5. {chuk_tool_processor-0.6.15 → chuk_tool_processor-0.6.18}/src/chuk_tool_processor/execution/wrappers/caching.py +5 -3
  6. {chuk_tool_processor-0.6.15 → chuk_tool_processor-0.6.18}/src/chuk_tool_processor/mcp/transport/http_streamable_transport.py +36 -24
  7. {chuk_tool_processor-0.6.15 → chuk_tool_processor-0.6.18}/src/chuk_tool_processor/mcp/transport/sse_transport.py +21 -2
  8. {chuk_tool_processor-0.6.15 → chuk_tool_processor-0.6.18}/src/chuk_tool_processor.egg-info/PKG-INFO +2 -2
  9. {chuk_tool_processor-0.6.15 → chuk_tool_processor-0.6.18}/src/chuk_tool_processor.egg-info/requires.txt +1 -1
  10. {chuk_tool_processor-0.6.15 → chuk_tool_processor-0.6.18}/README.md +0 -0
  11. {chuk_tool_processor-0.6.15 → chuk_tool_processor-0.6.18}/setup.cfg +0 -0
  12. {chuk_tool_processor-0.6.15 → chuk_tool_processor-0.6.18}/src/chuk_tool_processor/__init__.py +0 -0
  13. {chuk_tool_processor-0.6.15 → chuk_tool_processor-0.6.18}/src/chuk_tool_processor/core/__init__.py +0 -0
  14. {chuk_tool_processor-0.6.15 → chuk_tool_processor-0.6.18}/src/chuk_tool_processor/core/exceptions.py +0 -0
  15. {chuk_tool_processor-0.6.15 → chuk_tool_processor-0.6.18}/src/chuk_tool_processor/execution/__init__.py +0 -0
  16. {chuk_tool_processor-0.6.15 → chuk_tool_processor-0.6.18}/src/chuk_tool_processor/execution/strategies/__init__.py +0 -0
  17. {chuk_tool_processor-0.6.15 → chuk_tool_processor-0.6.18}/src/chuk_tool_processor/execution/strategies/inprocess_strategy.py +0 -0
  18. {chuk_tool_processor-0.6.15 → chuk_tool_processor-0.6.18}/src/chuk_tool_processor/execution/tool_executor.py +0 -0
  19. {chuk_tool_processor-0.6.15 → chuk_tool_processor-0.6.18}/src/chuk_tool_processor/execution/wrappers/__init__.py +0 -0
  20. {chuk_tool_processor-0.6.15 → chuk_tool_processor-0.6.18}/src/chuk_tool_processor/execution/wrappers/rate_limiting.py +0 -0
  21. {chuk_tool_processor-0.6.15 → chuk_tool_processor-0.6.18}/src/chuk_tool_processor/execution/wrappers/retry.py +0 -0
  22. {chuk_tool_processor-0.6.15 → chuk_tool_processor-0.6.18}/src/chuk_tool_processor/logging/__init__.py +0 -0
  23. {chuk_tool_processor-0.6.15 → chuk_tool_processor-0.6.18}/src/chuk_tool_processor/logging/context.py +0 -0
  24. {chuk_tool_processor-0.6.15 → chuk_tool_processor-0.6.18}/src/chuk_tool_processor/logging/formatter.py +0 -0
  25. {chuk_tool_processor-0.6.15 → chuk_tool_processor-0.6.18}/src/chuk_tool_processor/logging/helpers.py +0 -0
  26. {chuk_tool_processor-0.6.15 → chuk_tool_processor-0.6.18}/src/chuk_tool_processor/logging/metrics.py +0 -0
  27. {chuk_tool_processor-0.6.15 → chuk_tool_processor-0.6.18}/src/chuk_tool_processor/mcp/__init__.py +0 -0
  28. {chuk_tool_processor-0.6.15 → chuk_tool_processor-0.6.18}/src/chuk_tool_processor/mcp/mcp_tool.py +0 -0
  29. {chuk_tool_processor-0.6.15 → chuk_tool_processor-0.6.18}/src/chuk_tool_processor/mcp/register_mcp_tools.py +0 -0
  30. {chuk_tool_processor-0.6.15 → chuk_tool_processor-0.6.18}/src/chuk_tool_processor/mcp/setup_mcp_http_streamable.py +0 -0
  31. {chuk_tool_processor-0.6.15 → chuk_tool_processor-0.6.18}/src/chuk_tool_processor/mcp/setup_mcp_sse.py +0 -0
  32. {chuk_tool_processor-0.6.15 → chuk_tool_processor-0.6.18}/src/chuk_tool_processor/mcp/setup_mcp_stdio.py +0 -0
  33. {chuk_tool_processor-0.6.15 → chuk_tool_processor-0.6.18}/src/chuk_tool_processor/mcp/stream_manager.py +0 -0
  34. {chuk_tool_processor-0.6.15 → chuk_tool_processor-0.6.18}/src/chuk_tool_processor/mcp/transport/__init__.py +0 -0
  35. {chuk_tool_processor-0.6.15 → chuk_tool_processor-0.6.18}/src/chuk_tool_processor/mcp/transport/base_transport.py +0 -0
  36. {chuk_tool_processor-0.6.15 → chuk_tool_processor-0.6.18}/src/chuk_tool_processor/mcp/transport/stdio_transport.py +0 -0
  37. {chuk_tool_processor-0.6.15 → chuk_tool_processor-0.6.18}/src/chuk_tool_processor/models/__init__.py +0 -0
  38. {chuk_tool_processor-0.6.15 → chuk_tool_processor-0.6.18}/src/chuk_tool_processor/models/execution_strategy.py +0 -0
  39. {chuk_tool_processor-0.6.15 → chuk_tool_processor-0.6.18}/src/chuk_tool_processor/models/streaming_tool.py +0 -0
  40. {chuk_tool_processor-0.6.15 → chuk_tool_processor-0.6.18}/src/chuk_tool_processor/models/tool_call.py +0 -0
  41. {chuk_tool_processor-0.6.15 → chuk_tool_processor-0.6.18}/src/chuk_tool_processor/models/tool_export_mixin.py +0 -0
  42. {chuk_tool_processor-0.6.15 → chuk_tool_processor-0.6.18}/src/chuk_tool_processor/models/tool_result.py +0 -0
  43. {chuk_tool_processor-0.6.15 → chuk_tool_processor-0.6.18}/src/chuk_tool_processor/models/validated_tool.py +0 -0
  44. {chuk_tool_processor-0.6.15 → chuk_tool_processor-0.6.18}/src/chuk_tool_processor/plugins/__init__.py +0 -0
  45. {chuk_tool_processor-0.6.15 → chuk_tool_processor-0.6.18}/src/chuk_tool_processor/plugins/discovery.py +0 -0
  46. {chuk_tool_processor-0.6.15 → chuk_tool_processor-0.6.18}/src/chuk_tool_processor/plugins/parsers/__init__.py +0 -0
  47. {chuk_tool_processor-0.6.15 → chuk_tool_processor-0.6.18}/src/chuk_tool_processor/plugins/parsers/base.py +0 -0
  48. {chuk_tool_processor-0.6.15 → chuk_tool_processor-0.6.18}/src/chuk_tool_processor/plugins/parsers/function_call_tool.py +0 -0
  49. {chuk_tool_processor-0.6.15 → chuk_tool_processor-0.6.18}/src/chuk_tool_processor/plugins/parsers/json_tool.py +0 -0
  50. {chuk_tool_processor-0.6.15 → chuk_tool_processor-0.6.18}/src/chuk_tool_processor/plugins/parsers/openai_tool.py +0 -0
  51. {chuk_tool_processor-0.6.15 → chuk_tool_processor-0.6.18}/src/chuk_tool_processor/plugins/parsers/xml_tool.py +0 -0
  52. {chuk_tool_processor-0.6.15 → chuk_tool_processor-0.6.18}/src/chuk_tool_processor/registry/__init__.py +0 -0
  53. {chuk_tool_processor-0.6.15 → chuk_tool_processor-0.6.18}/src/chuk_tool_processor/registry/auto_register.py +0 -0
  54. {chuk_tool_processor-0.6.15 → chuk_tool_processor-0.6.18}/src/chuk_tool_processor/registry/decorators.py +0 -0
  55. {chuk_tool_processor-0.6.15 → chuk_tool_processor-0.6.18}/src/chuk_tool_processor/registry/interface.py +0 -0
  56. {chuk_tool_processor-0.6.15 → chuk_tool_processor-0.6.18}/src/chuk_tool_processor/registry/metadata.py +0 -0
  57. {chuk_tool_processor-0.6.15 → chuk_tool_processor-0.6.18}/src/chuk_tool_processor/registry/provider.py +0 -0
  58. {chuk_tool_processor-0.6.15 → chuk_tool_processor-0.6.18}/src/chuk_tool_processor/registry/providers/__init__.py +0 -0
  59. {chuk_tool_processor-0.6.15 → chuk_tool_processor-0.6.18}/src/chuk_tool_processor/registry/providers/memory.py +0 -0
  60. {chuk_tool_processor-0.6.15 → chuk_tool_processor-0.6.18}/src/chuk_tool_processor/registry/tool_export.py +0 -0
  61. {chuk_tool_processor-0.6.15 → chuk_tool_processor-0.6.18}/src/chuk_tool_processor/utils/__init__.py +0 -0
  62. {chuk_tool_processor-0.6.15 → chuk_tool_processor-0.6.18}/src/chuk_tool_processor/utils/validation.py +0 -0
  63. {chuk_tool_processor-0.6.15 → chuk_tool_processor-0.6.18}/src/chuk_tool_processor.egg-info/SOURCES.txt +0 -0
  64. {chuk_tool_processor-0.6.15 → chuk_tool_processor-0.6.18}/src/chuk_tool_processor.egg-info/dependency_links.txt +0 -0
  65. {chuk_tool_processor-0.6.15 → chuk_tool_processor-0.6.18}/src/chuk_tool_processor.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chuk-tool-processor
3
- Version: 0.6.15
3
+ Version: 0.6.18
4
4
  Summary: Async-native framework for registering, discovering, and executing tools referenced in LLM responses
5
5
  Author-email: CHUK Team <chrishayuk@somejunkmailbox.com>
6
6
  Maintainer-email: CHUK Team <chrishayuk@somejunkmailbox.com>
@@ -20,7 +20,7 @@ Classifier: Framework :: AsyncIO
20
20
  Classifier: Typing :: Typed
21
21
  Requires-Python: >=3.11
22
22
  Description-Content-Type: text/markdown
23
- Requires-Dist: chuk-mcp>=0.5.2
23
+ Requires-Dist: chuk-mcp>=0.5.4
24
24
  Requires-Dist: dotenv>=0.9.9
25
25
  Requires-Dist: psutil>=7.0.0
26
26
  Requires-Dist: pydantic>=2.11.3
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "chuk-tool-processor"
7
- version = "0.6.15"
7
+ version = "0.6.18"
8
8
  description = "Async-native framework for registering, discovering, and executing tools referenced in LLM responses"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -41,7 +41,7 @@ classifiers = [
41
41
  "Typing :: Typed",
42
42
  ]
43
43
  dependencies = [
44
- "chuk-mcp>=0.5.2",
44
+ "chuk-mcp>=0.5.4",
45
45
  "dotenv>=0.9.9",
46
46
  "psutil>=7.0.0",
47
47
  "pydantic>=2.11.3",
@@ -370,7 +370,7 @@ class ToolProcessor:
370
370
  def _args_digest(args: dict[str, Any]) -> str:
371
371
  """Return a stable hash for any JSON-serialisable payload."""
372
372
  blob = json.dumps(args, sort_keys=True, default=str)
373
- return hashlib.md5(blob.encode()).hexdigest()
373
+ return hashlib.md5(blob.encode(), usedforsecurity=False).hexdigest() # nosec B324
374
374
 
375
375
  unique_calls: dict[str, ToolCall] = {}
376
376
  for call in all_calls:
@@ -95,7 +95,8 @@ def _serialized_tool_worker(
95
95
 
96
96
  try:
97
97
  # Deserialize the complete tool
98
- tool = pickle.loads(serialized_tool_data)
98
+ # This is safe as the data comes from the parent process, not untrusted external sources
99
+ tool = pickle.loads(serialized_tool_data) # nosec B301
99
100
 
100
101
  # Multiple fallbacks to ensure tool_name is available
101
102
 
@@ -362,11 +362,11 @@ class CachingToolExecutor:
362
362
  """
363
363
  try:
364
364
  blob = json.dumps(arguments, sort_keys=True, default=str)
365
- return hashlib.md5(blob.encode()).hexdigest()
365
+ return hashlib.md5(blob.encode(), usedforsecurity=False).hexdigest() # nosec B324
366
366
  except Exception as e:
367
367
  logger.warning(f"Error hashing arguments: {e}")
368
368
  # Fallback to a string representation
369
- return hashlib.md5(str(arguments).encode()).hexdigest()
369
+ return hashlib.md5(str(arguments).encode(), usedforsecurity=False).hexdigest() # nosec B324
370
370
 
371
371
  def _is_cacheable(self, tool: str) -> bool:
372
372
  """
@@ -565,7 +565,9 @@ def invalidate_cache(tool: str, arguments: dict[str, Any] | None = None):
565
565
 
566
566
  async def _invalidate(cache: CacheInterface):
567
567
  if arguments is not None:
568
- h = hashlib.md5(json.dumps(arguments, sort_keys=True, default=str).encode()).hexdigest()
568
+ h = hashlib.md5(
569
+ json.dumps(arguments, sort_keys=True, default=str).encode(), usedforsecurity=False
570
+ ).hexdigest() # nosec B324
569
571
  await cache.invalidate(tool, h)
570
572
  logger.debug(f"Invalidated cache entry for {tool} with specific arguments")
571
573
  else:
@@ -16,10 +16,12 @@ from chuk_mcp.protocol.messages import ( # type: ignore[import-untyped]
16
16
  send_tools_call,
17
17
  send_tools_list,
18
18
  )
19
+ from chuk_mcp.transports.http.parameters import StreamableHTTPParameters # type: ignore[import-untyped]
19
20
 
20
21
  # Import chuk-mcp HTTP transport components
21
- from chuk_mcp.transports.http import http_client # type: ignore[import-untyped]
22
- from chuk_mcp.transports.http.parameters import StreamableHTTPParameters # type: ignore[import-untyped]
22
+ from chuk_mcp.transports.http.transport import (
23
+ StreamableHTTPTransport as ChukHTTPTransport, # type: ignore[import-untyped]
24
+ )
23
25
 
24
26
  from .base_transport import MCPBaseTransport
25
27
 
@@ -78,7 +80,7 @@ class HTTPStreamableTransport(MCPBaseTransport):
78
80
  logger.debug("Session ID configured: %s", self.session_id)
79
81
 
80
82
  # State tracking (enhanced like SSE)
81
- self._http_context = None
83
+ self._http_transport = None
82
84
  self._read_stream = None
83
85
  self._write_stream = None
84
86
  self._initialized = False
@@ -115,8 +117,9 @@ class HTTPStreamableTransport(MCPBaseTransport):
115
117
  if self.configured_headers:
116
118
  headers.update(self.configured_headers)
117
119
 
118
- # Add API key as Bearer token if provided
119
- if self.api_key:
120
+ # Add API key as Bearer token if provided and no Authorization header exists
121
+ # This prevents clobbering OAuth tokens from configured_headers
122
+ if self.api_key and "Authorization" not in headers:
120
123
  headers["Authorization"] = f"Bearer {self.api_key}"
121
124
 
122
125
  # Add session ID if provided
@@ -159,33 +162,38 @@ class HTTPStreamableTransport(MCPBaseTransport):
159
162
  headers = self._get_headers()
160
163
  logger.debug("Using headers: %s", list(headers.keys()))
161
164
 
162
- # Create StreamableHTTPParameters with proper configuration
165
+ # Create StreamableHTTPParameters with minimal configuration
166
+ # NOTE: Keep params minimal - extra params can break message routing
163
167
  http_params = StreamableHTTPParameters(
164
168
  url=self.url,
165
169
  timeout=self.default_timeout,
166
170
  headers=headers,
167
- bearer_token=None, # Don't duplicate auth - it's in headers
168
- session_id=self.session_id,
169
171
  enable_streaming=True,
170
- max_concurrent_requests=5,
171
- max_retries=2,
172
- retry_delay=1.0,
173
- user_agent="chuk-tool-processor/1.0.0",
174
172
  )
175
173
 
176
- # Create and enter the HTTP context
177
- self._http_context = http_client(http_params)
174
+ # Create and store transport (will be managed via async with in parent scope)
175
+ self._http_transport = ChukHTTPTransport(http_params)
178
176
 
177
+ # IMPORTANT: Must use async with for proper stream setup
179
178
  logger.debug("Establishing HTTP connection...")
180
- self._read_stream, self._write_stream = await asyncio.wait_for(
181
- self._http_context.__aenter__(), timeout=self.connection_timeout
179
+ self._http_context_entered = await asyncio.wait_for(
180
+ self._http_transport.__aenter__(), timeout=self.connection_timeout
182
181
  )
183
182
 
183
+ # Get streams after context entered
184
+ self._read_stream, self._write_stream = await self._http_transport.get_streams()
185
+
186
+ # Give the transport's message handler task time to start
187
+ await asyncio.sleep(0.1)
188
+
184
189
  # Enhanced MCP initialize sequence
185
190
  logger.debug("Sending MCP initialize request...")
186
191
  init_start = time.time()
187
192
 
188
- await asyncio.wait_for(send_initialize(self._read_stream, self._write_stream), timeout=self.default_timeout)
193
+ await asyncio.wait_for(
194
+ send_initialize(self._read_stream, self._write_stream, timeout=self.default_timeout),
195
+ timeout=self.default_timeout,
196
+ )
189
197
 
190
198
  init_time = time.time() - init_start
191
199
  logger.debug("MCP initialize completed in %.3fs", init_time)
@@ -193,9 +201,11 @@ class HTTPStreamableTransport(MCPBaseTransport):
193
201
  # Verify connection with ping (enhanced like SSE)
194
202
  logger.debug("Verifying connection with ping...")
195
203
  ping_start = time.time()
204
+ # Use longer timeout for initial ping - some servers (like Notion) are slow
205
+ ping_timeout = max(self.default_timeout, 15.0)
196
206
  ping_success = await asyncio.wait_for(
197
- send_ping(self._read_stream, self._write_stream),
198
- timeout=10.0, # Longer timeout for initial ping
207
+ send_ping(self._read_stream, self._write_stream, timeout=ping_timeout),
208
+ timeout=ping_timeout,
199
209
  )
200
210
  ping_time = time.time() - ping_start
201
211
 
@@ -273,8 +283,8 @@ class HTTPStreamableTransport(MCPBaseTransport):
273
283
  )
274
284
 
275
285
  try:
276
- if self._http_context is not None:
277
- await self._http_context.__aexit__(None, None, None)
286
+ if self._http_transport is not None:
287
+ await self._http_transport.__aexit__(None, None, None)
278
288
  logger.debug("HTTP Streamable context closed")
279
289
 
280
290
  except Exception as e:
@@ -284,7 +294,7 @@ class HTTPStreamableTransport(MCPBaseTransport):
284
294
 
285
295
  async def _cleanup(self) -> None:
286
296
  """Enhanced cleanup with state reset."""
287
- self._http_context = None
297
+ self._http_transport = None
288
298
  self._read_stream = None
289
299
  self._write_stream = None
290
300
  self._initialized = False
@@ -298,7 +308,8 @@ class HTTPStreamableTransport(MCPBaseTransport):
298
308
  start_time = time.time()
299
309
  try:
300
310
  result = await asyncio.wait_for(
301
- send_ping(self._read_stream, self._write_stream), timeout=self.default_timeout
311
+ send_ping(self._read_stream, self._write_stream, timeout=self.default_timeout),
312
+ timeout=self.default_timeout,
302
313
  )
303
314
 
304
315
  success = bool(result)
@@ -347,7 +358,8 @@ class HTTPStreamableTransport(MCPBaseTransport):
347
358
  start_time = time.time()
348
359
  try:
349
360
  tools_response = await asyncio.wait_for(
350
- send_tools_list(self._read_stream, self._write_stream), timeout=self.default_timeout
361
+ send_tools_list(self._read_stream, self._write_stream, timeout=self.default_timeout),
362
+ timeout=self.default_timeout,
351
363
  )
352
364
 
353
365
  # Normalize response
@@ -110,8 +110,9 @@ class SSETransport(MCPBaseTransport):
110
110
  if self.configured_headers:
111
111
  headers.update(self.configured_headers)
112
112
 
113
- # Add API key as Bearer token if provided
114
- if self.api_key:
113
+ # Add API key as Bearer token if provided and no Authorization header exists
114
+ # This prevents clobbering OAuth tokens from configured_headers
115
+ if self.api_key and "Authorization" not in headers:
115
116
  headers["Authorization"] = f"Bearer {self.api_key}"
116
117
 
117
118
  return headers
@@ -269,12 +270,30 @@ class SSETransport(MCPBaseTransport):
269
270
  # Extract session ID from URL if present
270
271
  if "session_id=" in data_part:
271
272
  self.session_id = data_part.split("session_id=")[1].split("&")[0]
273
+ elif "sessionId=" in data_part:
274
+ self.session_id = data_part.split("sessionId=")[1].split("&")[0]
272
275
  else:
273
276
  self.session_id = str(uuid.uuid4())
274
277
 
275
278
  logger.debug("Session endpoint discovered via event format: %s", self.message_url)
276
279
  continue
277
280
 
281
+ # RELATIVE PATH FORMAT: event: endpoint + data: /sse/message?sessionId=...
282
+ elif current_event == "endpoint" and data_part.startswith("/"):
283
+ endpoint_path = data_part
284
+ self.message_url = f"{self.url}{endpoint_path}"
285
+
286
+ # Extract session ID if present
287
+ if "session_id=" in endpoint_path:
288
+ self.session_id = endpoint_path.split("session_id=")[1].split("&")[0]
289
+ elif "sessionId=" in endpoint_path:
290
+ self.session_id = endpoint_path.split("sessionId=")[1].split("&")[0]
291
+ else:
292
+ self.session_id = str(uuid.uuid4())
293
+
294
+ logger.debug("Session endpoint discovered via relative path: %s", self.message_url)
295
+ continue
296
+
278
297
  # OLD FORMAT: data: /messages/... (backwards compatibility)
279
298
  elif "/messages/" in data_part:
280
299
  endpoint_path = data_part
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chuk-tool-processor
3
- Version: 0.6.15
3
+ Version: 0.6.18
4
4
  Summary: Async-native framework for registering, discovering, and executing tools referenced in LLM responses
5
5
  Author-email: CHUK Team <chrishayuk@somejunkmailbox.com>
6
6
  Maintainer-email: CHUK Team <chrishayuk@somejunkmailbox.com>
@@ -20,7 +20,7 @@ Classifier: Framework :: AsyncIO
20
20
  Classifier: Typing :: Typed
21
21
  Requires-Python: >=3.11
22
22
  Description-Content-Type: text/markdown
23
- Requires-Dist: chuk-mcp>=0.5.2
23
+ Requires-Dist: chuk-mcp>=0.5.4
24
24
  Requires-Dist: dotenv>=0.9.9
25
25
  Requires-Dist: psutil>=7.0.0
26
26
  Requires-Dist: pydantic>=2.11.3
@@ -1,4 +1,4 @@
1
- chuk-mcp>=0.5.2
1
+ chuk-mcp>=0.5.4
2
2
  dotenv>=0.9.9
3
3
  psutil>=7.0.0
4
4
  pydantic>=2.11.3