kailash 0.1.1__py3-none-any.whl → 0.1.3__py3-none-any.whl

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.
Files changed (51) hide show
  1. kailash/api/__init__.py +7 -0
  2. kailash/api/workflow_api.py +383 -0
  3. kailash/nodes/__init__.py +2 -1
  4. kailash/nodes/ai/__init__.py +26 -0
  5. kailash/nodes/ai/ai_providers.py +1272 -0
  6. kailash/nodes/ai/embedding_generator.py +853 -0
  7. kailash/nodes/ai/llm_agent.py +1166 -0
  8. kailash/nodes/api/auth.py +3 -3
  9. kailash/nodes/api/graphql.py +2 -2
  10. kailash/nodes/api/http.py +391 -48
  11. kailash/nodes/api/rate_limiting.py +2 -2
  12. kailash/nodes/api/rest.py +465 -57
  13. kailash/nodes/base.py +71 -12
  14. kailash/nodes/code/python.py +2 -1
  15. kailash/nodes/data/__init__.py +7 -0
  16. kailash/nodes/data/readers.py +28 -26
  17. kailash/nodes/data/retrieval.py +178 -0
  18. kailash/nodes/data/sharepoint_graph.py +7 -7
  19. kailash/nodes/data/sources.py +65 -0
  20. kailash/nodes/data/sql.py +7 -5
  21. kailash/nodes/data/vector_db.py +2 -2
  22. kailash/nodes/data/writers.py +6 -3
  23. kailash/nodes/logic/__init__.py +2 -1
  24. kailash/nodes/logic/operations.py +2 -1
  25. kailash/nodes/logic/workflow.py +439 -0
  26. kailash/nodes/mcp/__init__.py +11 -0
  27. kailash/nodes/mcp/client.py +558 -0
  28. kailash/nodes/mcp/resource.py +682 -0
  29. kailash/nodes/mcp/server.py +577 -0
  30. kailash/nodes/transform/__init__.py +16 -1
  31. kailash/nodes/transform/chunkers.py +78 -0
  32. kailash/nodes/transform/formatters.py +96 -0
  33. kailash/nodes/transform/processors.py +5 -3
  34. kailash/runtime/docker.py +8 -6
  35. kailash/sdk_exceptions.py +24 -10
  36. kailash/tracking/metrics_collector.py +2 -1
  37. kailash/tracking/models.py +0 -20
  38. kailash/tracking/storage/database.py +4 -4
  39. kailash/tracking/storage/filesystem.py +0 -1
  40. kailash/utils/templates.py +6 -6
  41. kailash/visualization/performance.py +7 -7
  42. kailash/visualization/reports.py +1 -1
  43. kailash/workflow/graph.py +4 -4
  44. kailash/workflow/mock_registry.py +1 -1
  45. {kailash-0.1.1.dist-info → kailash-0.1.3.dist-info}/METADATA +441 -47
  46. kailash-0.1.3.dist-info/RECORD +83 -0
  47. kailash-0.1.1.dist-info/RECORD +0 -69
  48. {kailash-0.1.1.dist-info → kailash-0.1.3.dist-info}/WHEEL +0 -0
  49. {kailash-0.1.1.dist-info → kailash-0.1.3.dist-info}/entry_points.txt +0 -0
  50. {kailash-0.1.1.dist-info → kailash-0.1.3.dist-info}/licenses/LICENSE +0 -0
  51. {kailash-0.1.1.dist-info → kailash-0.1.3.dist-info}/top_level.txt +0 -0
kailash/nodes/api/auth.py CHANGED
@@ -21,7 +21,7 @@ from kailash.nodes.base import Node, NodeParameter, register_node
21
21
  from kailash.sdk_exceptions import NodeExecutionError, NodeValidationError
22
22
 
23
23
 
24
- @register_node(alias="BasicAuth")
24
+ @register_node()
25
25
  class BasicAuthNode(Node):
26
26
  """Node for adding Basic Authentication to API requests.
27
27
 
@@ -116,7 +116,7 @@ class BasicAuthNode(Node):
116
116
  return {"headers": headers, "auth_type": "basic"}
117
117
 
118
118
 
119
- @register_node(alias="OAuth2")
119
+ @register_node()
120
120
  class OAuth2Node(Node):
121
121
  """Node for handling OAuth 2.0 authentication flows.
122
122
 
@@ -421,7 +421,7 @@ class OAuth2Node(Node):
421
421
  }
422
422
 
423
423
 
424
- @register_node(alias="APIKey")
424
+ @register_node()
425
425
  class APIKeyNode(Node):
426
426
  """Node for API key authentication.
427
427
 
@@ -18,7 +18,7 @@ from kailash.nodes.base_async import AsyncNode
18
18
  from kailash.sdk_exceptions import NodeExecutionError, NodeValidationError
19
19
 
20
20
 
21
- @register_node(alias="GraphQLClient")
21
+ @register_node()
22
22
  class GraphQLClientNode(Node):
23
23
  """Node for interacting with GraphQL APIs.
24
24
 
@@ -331,7 +331,7 @@ class GraphQLClientNode(Node):
331
331
  }
332
332
 
333
333
 
334
- @register_node(alias="AsyncGraphQLClient")
334
+ @register_node()
335
335
  class AsyncGraphQLClientNode(AsyncNode):
336
336
  """Asynchronous node for interacting with GraphQL APIs.
337
337
 
kailash/nodes/api/http.py CHANGED
@@ -1,16 +1,12 @@
1
- """HTTP client nodes for making requests to external APIs.
1
+ """Enhanced HTTP client nodes with authentication and advanced features.
2
2
 
3
- This module provides nodes for making HTTP requests to external services.
4
- Both synchronous and asynchronous versions are provided to support different workflow
5
- execution modes.
6
-
7
- Key Components:
8
- - HTTPRequestNode: Synchronous HTTP client node
9
- - AsyncHTTPRequestNode: Asynchronous HTTP client node
10
- - Authentication helpers and utilities
3
+ This module provides an enhanced version of HTTPRequestNode that incorporates
4
+ the best features from both the original HTTPRequestNode and HTTPClientNode.
11
5
  """
12
6
 
13
7
  import asyncio
8
+ import base64
9
+ import time
14
10
  from enum import Enum
15
11
  from typing import Any, Dict, Optional
16
12
 
@@ -59,31 +55,35 @@ class HTTPResponse(BaseModel):
59
55
  url: str
60
56
 
61
57
 
62
- @register_node(alias="HTTPRequest")
58
+ @register_node()
63
59
  class HTTPRequestNode(Node):
64
- """Node for making HTTP requests to external APIs.
60
+ """Enhanced node for making HTTP requests to external APIs.
65
61
 
66
62
  This node provides a flexible interface for making HTTP requests with support for:
67
- - All common HTTP methods (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS)
68
- - JSON, form, and multipart request bodies
69
- - Custom headers and query parameters
70
- - Response parsing (JSON, text, binary)
71
- - Basic error handling and retries
63
+ * All common HTTP methods (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS)
64
+ * Multiple authentication methods (Bearer, Basic, API Key, OAuth2)
65
+ * JSON, form, and multipart request bodies
66
+ * Custom headers and query parameters
67
+ * Response parsing (JSON, text, binary)
68
+ * Error handling and retries with recovery suggestions
69
+ * Rate limiting support
70
+ * Request/response logging
72
71
 
73
72
  Design Purpose:
74
- - Enable workflow integration with external HTTP APIs
75
- - Provide a consistent interface for HTTP operations
76
- - Support common authentication patterns
77
- - Handle response parsing and error handling
73
+ * Enable workflow integration with external HTTP APIs
74
+ * Provide a consistent interface for HTTP operations
75
+ * Support common authentication patterns
76
+ * Handle response parsing and error handling
77
+ * Offer enterprise-grade features like rate limiting
78
78
 
79
79
  Upstream Usage:
80
- - Workflow: Creates and configures node for API integration
81
- - Specialized API nodes: May extend this node for specific APIs
80
+ * Workflow: Creates and configures node for API integration
81
+ * Specialized API nodes: May extend this node for specific APIs
82
82
 
83
83
  Downstream Consumers:
84
- - Data processing nodes: Consume API response data
85
- - Decision nodes: Route workflow based on API responses
86
- - Custom nodes: Process API-specific data formats
84
+ * Data processing nodes: Consume API response data
85
+ * Decision nodes: Route workflow based on API responses
86
+ * Custom nodes: Process API-specific data formats
87
87
  """
88
88
 
89
89
  def __init__(self, **kwargs):
@@ -101,6 +101,13 @@ class HTTPRequestNode(Node):
101
101
  verify_ssl (bool, optional): Whether to verify SSL certificates
102
102
  retry_count (int, optional): Number of times to retry failed requests
103
103
  retry_backoff (float, optional): Backoff factor for retries
104
+ auth_type (str, optional): Authentication type (bearer, basic, api_key, oauth2)
105
+ auth_token (str, optional): Authentication token/key
106
+ auth_username (str, optional): Username for basic auth
107
+ auth_password (str, optional): Password for basic auth
108
+ api_key_header (str, optional): Header name for API key auth
109
+ rate_limit_delay (float, optional): Delay between requests for rate limiting
110
+ log_requests (bool, optional): Whether to log request/response details
104
111
  **kwargs: Additional parameters passed to base Node
105
112
  """
106
113
  super().__init__(**kwargs)
@@ -116,7 +123,7 @@ class HTTPRequestNode(Node):
116
123
  "url": NodeParameter(
117
124
  name="url",
118
125
  type=str,
119
- required=True,
126
+ required=False,
120
127
  description="URL to send the request to",
121
128
  ),
122
129
  "method": NodeParameter(
@@ -189,6 +196,55 @@ class HTTPRequestNode(Node):
189
196
  default=0.5,
190
197
  description="Backoff factor for retries",
191
198
  ),
199
+ "auth_type": NodeParameter(
200
+ name="auth_type",
201
+ type=str,
202
+ required=False,
203
+ default=None,
204
+ description="Authentication type: bearer, basic, api_key, oauth2",
205
+ ),
206
+ "auth_token": NodeParameter(
207
+ name="auth_token",
208
+ type=str,
209
+ required=False,
210
+ default=None,
211
+ description="Authentication token/key for bearer, api_key, or oauth2",
212
+ ),
213
+ "auth_username": NodeParameter(
214
+ name="auth_username",
215
+ type=str,
216
+ required=False,
217
+ default=None,
218
+ description="Username for basic authentication",
219
+ ),
220
+ "auth_password": NodeParameter(
221
+ name="auth_password",
222
+ type=str,
223
+ required=False,
224
+ default=None,
225
+ description="Password for basic authentication",
226
+ ),
227
+ "api_key_header": NodeParameter(
228
+ name="api_key_header",
229
+ type=str,
230
+ required=False,
231
+ default="X-API-Key",
232
+ description="Header name for API key authentication",
233
+ ),
234
+ "rate_limit_delay": NodeParameter(
235
+ name="rate_limit_delay",
236
+ type=float,
237
+ required=False,
238
+ default=0,
239
+ description="Delay between requests to respect rate limits (seconds)",
240
+ ),
241
+ "log_requests": NodeParameter(
242
+ name="log_requests",
243
+ type=bool,
244
+ required=False,
245
+ default=False,
246
+ description="Log request and response details for debugging",
247
+ ),
192
248
  }
193
249
 
194
250
  def get_output_schema(self) -> Dict[str, NodeParameter]:
@@ -218,6 +274,49 @@ class HTTPRequestNode(Node):
218
274
  ),
219
275
  }
220
276
 
277
+ def _apply_authentication(
278
+ self,
279
+ headers: dict,
280
+ auth_type: Optional[str],
281
+ auth_token: Optional[str],
282
+ auth_username: Optional[str],
283
+ auth_password: Optional[str],
284
+ api_key_header: str,
285
+ ) -> dict:
286
+ """Apply authentication to request headers.
287
+
288
+ Args:
289
+ headers: Existing headers dictionary
290
+ auth_type: Type of authentication (bearer, basic, api_key, oauth2)
291
+ auth_token: Token for bearer/api_key/oauth2 authentication
292
+ auth_username: Username for basic authentication
293
+ auth_password: Password for basic authentication
294
+ api_key_header: Header name for API key authentication
295
+
296
+ Returns:
297
+ Updated headers dictionary with authentication
298
+ """
299
+ if not auth_type:
300
+ return headers
301
+
302
+ auth_headers = headers.copy()
303
+
304
+ if auth_type.lower() == "bearer" and auth_token:
305
+ auth_headers["Authorization"] = f"Bearer {auth_token}"
306
+
307
+ elif auth_type.lower() == "basic" and auth_username and auth_password:
308
+ credentials = f"{auth_username}:{auth_password}"
309
+ encoded = base64.b64encode(credentials.encode()).decode()
310
+ auth_headers["Authorization"] = f"Basic {encoded}"
311
+
312
+ elif auth_type.lower() == "api_key" and auth_token:
313
+ auth_headers[api_key_header] = auth_token
314
+
315
+ elif auth_type.lower() == "oauth2" and auth_token:
316
+ auth_headers["Authorization"] = f"Bearer {auth_token}"
317
+
318
+ return auth_headers
319
+
221
320
  def run(self, **kwargs) -> Dict[str, Any]:
222
321
  """Execute an HTTP request.
223
322
 
@@ -233,6 +332,13 @@ class HTTPRequestNode(Node):
233
332
  verify_ssl (bool, optional): Whether to verify SSL certificates
234
333
  retry_count (int, optional): Number of times to retry failed requests
235
334
  retry_backoff (float, optional): Backoff factor for retries
335
+ auth_type (str, optional): Authentication type
336
+ auth_token (str, optional): Authentication token
337
+ auth_username (str, optional): Username for basic auth
338
+ auth_password (str, optional): Password for basic auth
339
+ api_key_header (str, optional): Header name for API key
340
+ rate_limit_delay (float, optional): Rate limit delay
341
+ log_requests (bool, optional): Log request/response details
236
342
 
237
343
  Returns:
238
344
  Dictionary containing:
@@ -244,6 +350,8 @@ class HTTPRequestNode(Node):
244
350
  NodeExecutionError: If the request fails or returns an error status
245
351
  """
246
352
  url = kwargs.get("url")
353
+ if not url:
354
+ raise NodeValidationError("URL parameter is required")
247
355
  method = kwargs.get("method", "GET").upper()
248
356
  headers = kwargs.get("headers", {})
249
357
  params = kwargs.get("params", {})
@@ -254,6 +362,24 @@ class HTTPRequestNode(Node):
254
362
  verify_ssl = kwargs.get("verify_ssl", True)
255
363
  retry_count = kwargs.get("retry_count", 0)
256
364
  retry_backoff = kwargs.get("retry_backoff", 0.5)
365
+ auth_type = kwargs.get("auth_type")
366
+ auth_token = kwargs.get("auth_token")
367
+ auth_username = kwargs.get("auth_username")
368
+ auth_password = kwargs.get("auth_password")
369
+ api_key_header = kwargs.get("api_key_header", "X-API-Key")
370
+ rate_limit_delay = kwargs.get("rate_limit_delay", 0)
371
+ log_requests = kwargs.get("log_requests", False)
372
+
373
+ # Apply authentication to headers
374
+ if auth_type:
375
+ headers = self._apply_authentication(
376
+ headers,
377
+ auth_type,
378
+ auth_token,
379
+ auth_username,
380
+ auth_password,
381
+ api_key_header,
382
+ )
257
383
 
258
384
  # Validate method
259
385
  try:
@@ -273,6 +399,10 @@ class HTTPRequestNode(Node):
273
399
  f"Supported formats: {', '.join([f.value for f in ResponseFormat])}"
274
400
  )
275
401
 
402
+ # Apply rate limit delay if configured
403
+ if rate_limit_delay > 0:
404
+ time.sleep(rate_limit_delay)
405
+
276
406
  # Prepare request kwargs
277
407
  request_kwargs = {
278
408
  "url": url,
@@ -289,10 +419,15 @@ class HTTPRequestNode(Node):
289
419
  request_kwargs["data"] = data
290
420
 
291
421
  # Execute request with retries
292
- self.logger.info(f"Making {method} request to {url}")
422
+ if log_requests:
423
+ self.logger.info(f"Request: {method} {url}")
424
+ self.logger.info(f"Headers: {headers}")
425
+ if data or json_data:
426
+ self.logger.info(f"Body: {json_data or data}")
427
+ else:
428
+ self.logger.info(f"Making {method} request to {url}")
293
429
 
294
430
  response = None
295
- last_error = None
296
431
 
297
432
  for attempt in range(retry_count + 1):
298
433
  if attempt > 0:
@@ -300,29 +435,42 @@ class HTTPRequestNode(Node):
300
435
  self.logger.info(
301
436
  f"Retry attempt {attempt}/{retry_count} after {wait_time:.2f}s"
302
437
  )
303
- import time
304
-
305
438
  time.sleep(wait_time)
306
439
 
307
440
  try:
308
- import time
309
-
310
441
  start_time = time.time()
311
442
  response = self.session.request(method=method.value, **request_kwargs)
312
443
  response_time = (time.time() - start_time) * 1000 # Convert to ms
313
444
 
445
+ # Log response if enabled
446
+ if log_requests:
447
+ self.logger.info(f"Response: {response.status_code}")
448
+ self.logger.info(f"Headers: {dict(response.headers)}")
449
+ self.logger.info(f"Body: {response.text[:500]}...")
450
+
314
451
  # Success, break the retry loop
315
452
  break
316
453
 
317
454
  except requests.RequestException as e:
318
- last_error = e
319
455
  self.logger.warning(f"Request failed: {str(e)}")
320
456
 
321
457
  # Last attempt, no more retries
322
458
  if attempt == retry_count:
323
- raise NodeExecutionError(
324
- f"HTTP request failed after {retry_count + 1} attempts: {str(e)}"
325
- ) from e
459
+ # Enhanced error response with recovery suggestions
460
+ return {
461
+ "response": None,
462
+ "status_code": None,
463
+ "success": False,
464
+ "error": str(e),
465
+ "error_type": type(e).__name__,
466
+ "recovery_suggestions": [
467
+ "Check network connectivity",
468
+ "Verify URL is correct and accessible",
469
+ "Check authentication credentials",
470
+ "Increase timeout or retry settings",
471
+ "Check API rate limits",
472
+ ],
473
+ }
326
474
 
327
475
  # Parse response based on format
328
476
  content_type = response.headers.get("Content-Type", "")
@@ -363,16 +511,70 @@ class HTTPRequestNode(Node):
363
511
  # Return results
364
512
  success = 200 <= response.status_code < 300
365
513
 
366
- return {
514
+ # Add recovery suggestions for error responses
515
+ result = {
367
516
  "response": http_response,
368
517
  "status_code": response.status_code,
369
518
  "success": success,
370
519
  }
371
520
 
521
+ if not success:
522
+ result["recovery_suggestions"] = self._get_recovery_suggestions(
523
+ response.status_code
524
+ )
525
+
526
+ return result
527
+
528
+ def _get_recovery_suggestions(self, status_code: int) -> list:
529
+ """Get recovery suggestions based on status code.
372
530
 
373
- @register_node(alias="AsyncHTTPRequest")
531
+ Args:
532
+ status_code: HTTP status code
533
+
534
+ Returns:
535
+ List of recovery suggestions
536
+ """
537
+ if status_code == 401:
538
+ return [
539
+ "Check authentication credentials",
540
+ "Verify API key or token is valid",
541
+ "Ensure authentication method matches API requirements",
542
+ ]
543
+ elif status_code == 403:
544
+ return [
545
+ "Verify you have permission to access this resource",
546
+ "Check API key permissions/scopes",
547
+ "Ensure IP address is whitelisted if required",
548
+ ]
549
+ elif status_code == 404:
550
+ return [
551
+ "Verify the URL path is correct",
552
+ "Check if resource ID exists",
553
+ "Ensure API version in URL is correct",
554
+ ]
555
+ elif status_code == 429:
556
+ return [
557
+ "API rate limit exceeded - wait before retrying",
558
+ "Implement rate limiting in your requests",
559
+ "Check rate limit headers for reset time",
560
+ ]
561
+ elif status_code >= 500:
562
+ return [
563
+ "Server error - retry after a delay",
564
+ "Check API service status page",
565
+ "Contact API support if issue persists",
566
+ ]
567
+ else:
568
+ return [
569
+ "Check API documentation for this status code",
570
+ "Verify request format and parameters",
571
+ "Review response body for error details",
572
+ ]
573
+
574
+
575
+ @register_node()
374
576
  class AsyncHTTPRequestNode(AsyncNode):
375
- """Asynchronous node for making HTTP requests to external APIs.
577
+ """Asynchronous enhanced node for making HTTP requests to external APIs.
376
578
 
377
579
  This node provides the same functionality as HTTPRequestNode but uses
378
580
  asynchronous I/O for better performance, especially for concurrent requests.
@@ -418,6 +620,49 @@ class AsyncHTTPRequestNode(AsyncNode):
418
620
  # Same output schema as the synchronous version
419
621
  return HTTPRequestNode().get_output_schema()
420
622
 
623
+ def _apply_authentication(
624
+ self,
625
+ headers: dict,
626
+ auth_type: Optional[str],
627
+ auth_token: Optional[str],
628
+ auth_username: Optional[str],
629
+ auth_password: Optional[str],
630
+ api_key_header: str,
631
+ ) -> dict:
632
+ """Apply authentication to request headers.
633
+
634
+ Args:
635
+ headers: Existing headers dictionary
636
+ auth_type: Type of authentication (bearer, basic, api_key, oauth2)
637
+ auth_token: Token for bearer/api_key/oauth2 authentication
638
+ auth_username: Username for basic authentication
639
+ auth_password: Password for basic authentication
640
+ api_key_header: Header name for API key authentication
641
+
642
+ Returns:
643
+ Updated headers dictionary with authentication
644
+ """
645
+ if not auth_type:
646
+ return headers
647
+
648
+ auth_headers = headers.copy()
649
+
650
+ if auth_type.lower() == "bearer" and auth_token:
651
+ auth_headers["Authorization"] = f"Bearer {auth_token}"
652
+
653
+ elif auth_type.lower() == "basic" and auth_username and auth_password:
654
+ credentials = f"{auth_username}:{auth_password}"
655
+ encoded = base64.b64encode(credentials.encode()).decode()
656
+ auth_headers["Authorization"] = f"Basic {encoded}"
657
+
658
+ elif auth_type.lower() == "api_key" and auth_token:
659
+ auth_headers[api_key_header] = auth_token
660
+
661
+ elif auth_type.lower() == "oauth2" and auth_token:
662
+ auth_headers["Authorization"] = f"Bearer {auth_token}"
663
+
664
+ return auth_headers
665
+
421
666
  def run(self, **kwargs) -> Dict[str, Any]:
422
667
  """Synchronous version of the request, for compatibility.
423
668
 
@@ -450,6 +695,8 @@ class AsyncHTTPRequestNode(AsyncNode):
450
695
  NodeExecutionError: If the request fails or returns an error status
451
696
  """
452
697
  url = kwargs.get("url")
698
+ if not url:
699
+ raise NodeValidationError("URL parameter is required")
453
700
  method = kwargs.get("method", "GET").upper()
454
701
  headers = kwargs.get("headers", {})
455
702
  params = kwargs.get("params", {})
@@ -460,6 +707,24 @@ class AsyncHTTPRequestNode(AsyncNode):
460
707
  verify_ssl = kwargs.get("verify_ssl", True)
461
708
  retry_count = kwargs.get("retry_count", 0)
462
709
  retry_backoff = kwargs.get("retry_backoff", 0.5)
710
+ auth_type = kwargs.get("auth_type")
711
+ auth_token = kwargs.get("auth_token")
712
+ auth_username = kwargs.get("auth_username")
713
+ auth_password = kwargs.get("auth_password")
714
+ api_key_header = kwargs.get("api_key_header", "X-API-Key")
715
+ rate_limit_delay = kwargs.get("rate_limit_delay", 0)
716
+ log_requests = kwargs.get("log_requests", False)
717
+
718
+ # Apply authentication to headers
719
+ if auth_type:
720
+ headers = self._apply_authentication(
721
+ headers,
722
+ auth_type,
723
+ auth_token,
724
+ auth_username,
725
+ auth_password,
726
+ api_key_header,
727
+ )
463
728
 
464
729
  # Validate method
465
730
  try:
@@ -479,6 +744,10 @@ class AsyncHTTPRequestNode(AsyncNode):
479
744
  f"Supported formats: {', '.join([f.value for f in ResponseFormat])}"
480
745
  )
481
746
 
747
+ # Apply rate limit delay if configured
748
+ if rate_limit_delay > 0:
749
+ await asyncio.sleep(rate_limit_delay)
750
+
482
751
  # Create session if needed
483
752
  if self._session is None:
484
753
  self._session = aiohttp.ClientSession()
@@ -499,10 +768,15 @@ class AsyncHTTPRequestNode(AsyncNode):
499
768
  request_kwargs["data"] = data
500
769
 
501
770
  # Execute request with retries
502
- self.logger.info(f"Making async {method} request to {url}")
771
+ if log_requests:
772
+ self.logger.info(f"Request: {method} {url}")
773
+ self.logger.info(f"Headers: {headers}")
774
+ if data or json_data:
775
+ self.logger.info(f"Body: {json_data or data}")
776
+ else:
777
+ self.logger.info(f"Making async {method} request to {url}")
503
778
 
504
779
  response = None
505
- last_error = None
506
780
 
507
781
  for attempt in range(retry_count + 1):
508
782
  if attempt > 0:
@@ -513,8 +787,6 @@ class AsyncHTTPRequestNode(AsyncNode):
513
787
  await asyncio.sleep(wait_time)
514
788
 
515
789
  try:
516
- import time
517
-
518
790
  start_time = time.time()
519
791
 
520
792
  async with self._session.request(
@@ -525,6 +797,13 @@ class AsyncHTTPRequestNode(AsyncNode):
525
797
  # Get content type
526
798
  content_type = response.headers.get("Content-Type", "")
527
799
 
800
+ # Log response if enabled
801
+ if log_requests:
802
+ self.logger.info(f"Response: {response.status}")
803
+ self.logger.info(f"Headers: {dict(response.headers)}")
804
+ text_preview = await response.text()
805
+ self.logger.info(f"Body: {text_preview[:500]}...")
806
+
528
807
  # Determine response format
529
808
  actual_format = response_format
530
809
  if actual_format == ResponseFormat.AUTO:
@@ -564,27 +843,91 @@ class AsyncHTTPRequestNode(AsyncNode):
564
843
  # Return results
565
844
  success = 200 <= response.status < 300
566
845
 
567
- return {
846
+ result = {
568
847
  "response": http_response,
569
848
  "status_code": response.status,
570
849
  "success": success,
571
850
  }
572
851
 
852
+ if not success:
853
+ result["recovery_suggestions"] = self._get_recovery_suggestions(
854
+ response.status
855
+ )
856
+
857
+ return result
858
+
573
859
  except (aiohttp.ClientError, asyncio.TimeoutError) as e:
574
- last_error = e
575
860
  self.logger.warning(f"Async request failed: {str(e)}")
576
861
 
577
862
  # Last attempt, no more retries
578
863
  if attempt == retry_count:
579
- raise NodeExecutionError(
580
- f"Async HTTP request failed after {retry_count + 1} attempts: {str(e)}"
581
- ) from e
864
+ # Enhanced error response with recovery suggestions
865
+ return {
866
+ "response": None,
867
+ "status_code": None,
868
+ "success": False,
869
+ "error": str(e),
870
+ "error_type": type(e).__name__,
871
+ "recovery_suggestions": [
872
+ "Check network connectivity",
873
+ "Verify URL is correct and accessible",
874
+ "Check authentication credentials",
875
+ "Increase timeout or retry settings",
876
+ "Check API rate limits",
877
+ ],
878
+ }
582
879
 
583
880
  # Should not reach here, but just in case
584
881
  raise NodeExecutionError(
585
882
  f"Async HTTP request failed after {retry_count + 1} attempts."
586
883
  )
587
884
 
885
+ def _get_recovery_suggestions(self, status_code: int) -> list:
886
+ """Get recovery suggestions based on status code.
887
+
888
+ Args:
889
+ status_code: HTTP status code
890
+
891
+ Returns:
892
+ List of recovery suggestions
893
+ """
894
+ if status_code == 401:
895
+ return [
896
+ "Check authentication credentials",
897
+ "Verify API key or token is valid",
898
+ "Ensure authentication method matches API requirements",
899
+ ]
900
+ elif status_code == 403:
901
+ return [
902
+ "Verify you have permission to access this resource",
903
+ "Check API key permissions/scopes",
904
+ "Ensure IP address is whitelisted if required",
905
+ ]
906
+ elif status_code == 404:
907
+ return [
908
+ "Verify the URL path is correct",
909
+ "Check if resource ID exists",
910
+ "Ensure API version in URL is correct",
911
+ ]
912
+ elif status_code == 429:
913
+ return [
914
+ "API rate limit exceeded - wait before retrying",
915
+ "Implement rate limiting in your requests",
916
+ "Check rate limit headers for reset time",
917
+ ]
918
+ elif status_code >= 500:
919
+ return [
920
+ "Server error - retry after a delay",
921
+ "Check API service status page",
922
+ "Contact API support if issue persists",
923
+ ]
924
+ else:
925
+ return [
926
+ "Check API documentation for this status code",
927
+ "Verify request format and parameters",
928
+ "Review response body for error details",
929
+ ]
930
+
588
931
  async def __aenter__(self):
589
932
  """Context manager support for 'async with' statements."""
590
933
  if self._session is None: