kailash 0.1.1__py3-none-any.whl → 0.1.2__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 (37) hide show
  1. kailash/nodes/__init__.py +2 -1
  2. kailash/nodes/ai/__init__.py +26 -0
  3. kailash/nodes/ai/ai_providers.py +1272 -0
  4. kailash/nodes/ai/embedding_generator.py +853 -0
  5. kailash/nodes/ai/llm_agent.py +1166 -0
  6. kailash/nodes/api/auth.py +3 -3
  7. kailash/nodes/api/graphql.py +2 -2
  8. kailash/nodes/api/http.py +391 -44
  9. kailash/nodes/api/rate_limiting.py +2 -2
  10. kailash/nodes/api/rest.py +464 -56
  11. kailash/nodes/base.py +71 -12
  12. kailash/nodes/code/python.py +2 -1
  13. kailash/nodes/data/__init__.py +7 -0
  14. kailash/nodes/data/readers.py +28 -26
  15. kailash/nodes/data/retrieval.py +178 -0
  16. kailash/nodes/data/sharepoint_graph.py +7 -7
  17. kailash/nodes/data/sources.py +65 -0
  18. kailash/nodes/data/sql.py +4 -2
  19. kailash/nodes/data/writers.py +6 -3
  20. kailash/nodes/logic/operations.py +2 -1
  21. kailash/nodes/mcp/__init__.py +11 -0
  22. kailash/nodes/mcp/client.py +558 -0
  23. kailash/nodes/mcp/resource.py +682 -0
  24. kailash/nodes/mcp/server.py +571 -0
  25. kailash/nodes/transform/__init__.py +16 -1
  26. kailash/nodes/transform/chunkers.py +78 -0
  27. kailash/nodes/transform/formatters.py +96 -0
  28. kailash/runtime/docker.py +6 -6
  29. kailash/sdk_exceptions.py +24 -10
  30. kailash/tracking/metrics_collector.py +2 -1
  31. kailash/utils/templates.py +6 -6
  32. {kailash-0.1.1.dist-info → kailash-0.1.2.dist-info}/METADATA +344 -46
  33. {kailash-0.1.1.dist-info → kailash-0.1.2.dist-info}/RECORD +37 -26
  34. {kailash-0.1.1.dist-info → kailash-0.1.2.dist-info}/WHEEL +0 -0
  35. {kailash-0.1.1.dist-info → kailash-0.1.2.dist-info}/entry_points.txt +0 -0
  36. {kailash-0.1.1.dist-info → kailash-0.1.2.dist-info}/licenses/LICENSE +0 -0
  37. {kailash-0.1.1.dist-info → kailash-0.1.2.dist-info}/top_level.txt +0 -0
kailash/nodes/api/rest.py CHANGED
@@ -5,9 +5,9 @@ synchronous and asynchronous modes. These nodes build on the base HTTP nodes
5
5
  to provide a more convenient interface for working with REST APIs.
6
6
 
7
7
  Key Components:
8
- - RESTClientNode: Synchronous REST API client
9
- - AsyncRESTClientNode: Asynchronous REST API client
10
- - Resource path builders and response handlers
8
+ * RESTClientNode: Synchronous REST API client
9
+ * AsyncRESTClientNode: Asynchronous REST API client
10
+ * Resource path builders and response handlers
11
11
  """
12
12
 
13
13
  from typing import Any, Dict, List, Optional
@@ -18,31 +18,31 @@ from kailash.nodes.base_async import AsyncNode
18
18
  from kailash.sdk_exceptions import NodeExecutionError, NodeValidationError
19
19
 
20
20
 
21
- @register_node(alias="RESTClient")
21
+ @register_node()
22
22
  class RESTClientNode(Node):
23
23
  """Node for interacting with REST APIs.
24
24
 
25
25
  This node provides a higher-level interface for interacting with REST APIs,
26
26
  with built-in support for:
27
- - Resource-based operations (e.g., GET /users/{id})
28
- - Common REST patterns (list, get, create, update, delete)
29
- - Pagination handling
30
- - Response schema validation
31
- - Error response handling
27
+ * Resource-based operations (e.g., GET /users/{id})
28
+ * Common REST patterns (list, get, create, update, delete)
29
+ * Pagination handling
30
+ * Response schema validation
31
+ * Error response handling
32
32
 
33
33
  Design Purpose:
34
- - Simplify REST API integration in workflows
35
- - Provide consistent interfaces for common REST operations
36
- - Support standard REST conventions and patterns
37
- - Handle common REST-specific error cases
34
+ * Simplify REST API integration in workflows
35
+ * Provide consistent interfaces for common REST operations
36
+ * Support standard REST conventions and patterns
37
+ * Handle common REST-specific error cases
38
38
 
39
39
  Upstream Usage:
40
- - Workflow: Creates and configures for specific REST APIs
41
- - API integration workflows: Uses for external service integration
40
+ * Workflow: Creates and configures for specific REST APIs
41
+ * API integration workflows: Uses for external service integration
42
42
 
43
43
  Downstream Consumers:
44
- - Data processing nodes: Consume API response data
45
- - Custom nodes: Process API-specific data formats
44
+ * Data processing nodes: Consume API response data
45
+ * Custom nodes: Process API-specific data formats
46
46
  """
47
47
 
48
48
  def __init__(self, **kwargs):
@@ -60,7 +60,7 @@ class RESTClientNode(Node):
60
60
  **kwargs: Additional parameters passed to base Node
61
61
  """
62
62
  super().__init__(**kwargs)
63
- self.http_node = HTTPRequestNode(**kwargs)
63
+ self.http_node = HTTPRequestNode(url="")
64
64
 
65
65
  def get_parameters(self) -> Dict[str, NodeParameter]:
66
66
  """Define the parameters this node accepts.
@@ -72,13 +72,13 @@ class RESTClientNode(Node):
72
72
  "base_url": NodeParameter(
73
73
  name="base_url",
74
74
  type=str,
75
- required=True,
75
+ required=False,
76
76
  description="Base URL for the REST API (e.g., https://api.example.com)",
77
77
  ),
78
78
  "resource": NodeParameter(
79
79
  name="resource",
80
80
  type=str,
81
- required=True,
81
+ required=False,
82
82
  description="API resource path (e.g., 'users' or 'products/{id}')",
83
83
  ),
84
84
  "method": NodeParameter(
@@ -165,6 +165,41 @@ class RESTClientNode(Node):
165
165
  default=0.5,
166
166
  description="Backoff factor for retries",
167
167
  ),
168
+ "auth_type": NodeParameter(
169
+ name="auth_type",
170
+ type=str,
171
+ required=False,
172
+ default=None,
173
+ description="Authentication type: bearer, basic, api_key, oauth2",
174
+ ),
175
+ "auth_token": NodeParameter(
176
+ name="auth_token",
177
+ type=str,
178
+ required=False,
179
+ default=None,
180
+ description="Authentication token/key for bearer, api_key, or oauth2",
181
+ ),
182
+ "auth_username": NodeParameter(
183
+ name="auth_username",
184
+ type=str,
185
+ required=False,
186
+ default=None,
187
+ description="Username for basic authentication",
188
+ ),
189
+ "auth_password": NodeParameter(
190
+ name="auth_password",
191
+ type=str,
192
+ required=False,
193
+ default=None,
194
+ description="Password for basic authentication",
195
+ ),
196
+ "api_key_header": NodeParameter(
197
+ name="api_key_header",
198
+ type=str,
199
+ required=False,
200
+ default="X-API-Key",
201
+ description="Header name for API key authentication",
202
+ ),
168
203
  }
169
204
 
170
205
  def get_output_schema(self) -> Dict[str, NodeParameter]:
@@ -271,9 +306,9 @@ class RESTClientNode(Node):
271
306
  """Handle pagination for REST API responses.
272
307
 
273
308
  This method supports common pagination patterns:
274
- - Page-based: ?page=1&per_page=100
275
- - Offset-based: ?offset=0&limit=100
276
- - Cursor-based: ?cursor=abc123
309
+ * Page-based: ?page=1&per_page=100
310
+ * Offset-based: ?offset=0&limit=100
311
+ * Cursor-based: ?cursor=abc123
277
312
 
278
313
  Args:
279
314
  initial_response: Response from the first API call
@@ -383,6 +418,11 @@ class RESTClientNode(Node):
383
418
  pagination_params (dict, optional): Pagination configuration
384
419
  retry_count (int, optional): Number of times to retry failed requests
385
420
  retry_backoff (float, optional): Backoff factor for retries
421
+ auth_type (str, optional): Authentication type (bearer, basic, api_key, oauth2)
422
+ auth_token (str, optional): Authentication token/key
423
+ auth_username (str, optional): Username for basic auth
424
+ auth_password (str, optional): Password for basic auth
425
+ api_key_header (str, optional): Header name for API key auth
386
426
 
387
427
  Returns:
388
428
  Dictionary containing:
@@ -409,6 +449,12 @@ class RESTClientNode(Node):
409
449
  pagination_params = kwargs.get("pagination_params")
410
450
  retry_count = kwargs.get("retry_count", 0)
411
451
  retry_backoff = kwargs.get("retry_backoff", 0.5)
452
+ # Authentication parameters
453
+ auth_type = kwargs.get("auth_type")
454
+ auth_token = kwargs.get("auth_token")
455
+ auth_username = kwargs.get("auth_username")
456
+ auth_password = kwargs.get("auth_password")
457
+ api_key_header = kwargs.get("api_key_header", "X-API-Key")
412
458
 
413
459
  # Build full URL with path parameters
414
460
  url = self._build_url(base_url, resource, path_params, version)
@@ -438,6 +484,11 @@ class RESTClientNode(Node):
438
484
  "verify_ssl": verify_ssl,
439
485
  "retry_count": retry_count,
440
486
  "retry_backoff": retry_backoff,
487
+ "auth_type": auth_type,
488
+ "auth_token": auth_token,
489
+ "auth_username": auth_username,
490
+ "auth_password": auth_password,
491
+ "api_key_header": api_key_header,
441
492
  }
442
493
 
443
494
  # Execute the HTTP request
@@ -445,30 +496,58 @@ class RESTClientNode(Node):
445
496
  result = self.http_node.run(**http_params)
446
497
 
447
498
  # Extract response data
448
- response = result["response"]
449
- status_code = result["status_code"]
450
- success = result["success"]
499
+ response = result.get("response")
500
+ status_code = result.get("status_code")
501
+ success = result.get("success", False)
451
502
 
452
503
  # Handle potential error responses
453
504
  if not success:
454
- error_message = "Unknown error"
455
- if isinstance(response["content"], dict):
505
+ error_message = result.get("error", "Unknown error")
506
+
507
+ # If we have a response object, try to extract error details
508
+ if response and isinstance(response.get("content"), dict):
456
509
  # Try to extract error message from common formats
457
- error_message = (
458
- response["content"].get("error", {}).get("message")
459
- or response["content"].get("message")
460
- or response["content"].get("error")
461
- or f"API returned error status: {status_code}"
462
- )
510
+ content = response["content"]
511
+ # Handle case where error is a string or dict
512
+ error_value = content.get("error")
513
+ if isinstance(error_value, dict):
514
+ error_message = error_value.get("message") or error_message
515
+ elif isinstance(error_value, str):
516
+ error_message = error_value
517
+ # Check for message at root level
518
+ if not error_message or error_message == result.get(
519
+ "error", "Unknown error"
520
+ ):
521
+ error_message = content.get("message") or error_message
522
+
523
+ # If we have a status code, include it
524
+ if status_code:
525
+ error_message = f"{error_message} (status: {status_code})"
463
526
 
464
527
  self.logger.error(f"REST API error: {error_message}")
465
528
 
529
+ # Return error response with recovery suggestions if available
530
+ error_result = {
531
+ "data": None,
532
+ "status_code": status_code,
533
+ "success": False,
534
+ "error": error_message,
535
+ "error_type": result.get("error_type", "APIError"),
536
+ "metadata": {},
537
+ }
538
+
539
+ # Include recovery suggestions if available
540
+ if "recovery_suggestions" in result:
541
+ error_result["recovery_suggestions"] = result["recovery_suggestions"]
542
+
543
+ return error_result
544
+
466
545
  # Note: We don't raise an exception here, as the caller might want
467
546
  # to handle error responses normally. Instead, we set success=False
468
547
  # and include error details in the response.
469
548
 
470
549
  # Handle pagination if requested
471
- data = response["content"]
550
+ data = response["content"] if response else None
472
551
  if paginate and method == "GET" and success:
473
552
  try:
474
553
  data = self._handle_pagination(data, query_params, pagination_params)
@@ -479,10 +558,15 @@ class RESTClientNode(Node):
479
558
  metadata = {
480
559
  "url": url,
481
560
  "method": method,
482
- "response_time_ms": response["response_time_ms"],
483
- "headers": response["headers"],
484
561
  }
485
562
 
563
+ # Add response metadata if available
564
+ if response:
565
+ metadata["response_time_ms"] = response.get("response_time_ms", 0)
566
+ metadata["headers"] = response.get("headers", {})
567
+ # Extract additional metadata
568
+ metadata.update(self._extract_metadata(response))
569
+
486
570
  return {
487
571
  "data": data,
488
572
  "status_code": status_code,
@@ -490,8 +574,293 @@ class RESTClientNode(Node):
490
574
  "metadata": metadata,
491
575
  }
492
576
 
577
+ # Convenience methods for CRUD operations
578
+ def get(
579
+ self, base_url: str, resource: str, resource_id: Optional[str] = None, **kwargs
580
+ ) -> Dict[str, Any]:
581
+ """GET a resource or list of resources.
582
+
583
+ Args:
584
+ base_url: Base API URL
585
+ resource: Resource name (e.g., 'users', 'posts')
586
+ resource_id: Optional resource ID for single resource retrieval
587
+ **kwargs: Additional parameters (query_params, headers, etc.)
588
+
589
+ Returns:
590
+ API response dictionary
591
+ """
592
+ if resource_id:
593
+ # Single resource retrieval
594
+ path_params = kwargs.pop("path_params", {})
595
+ path_params["id"] = resource_id
596
+ resource_path = f"{resource}/{{id}}"
597
+ else:
598
+ # List resources
599
+ resource_path = resource
600
+ path_params = kwargs.pop("path_params", {})
601
+
602
+ return self.run(
603
+ base_url=base_url,
604
+ resource=resource_path,
605
+ method="GET",
606
+ path_params=path_params,
607
+ **kwargs,
608
+ )
609
+
610
+ def create(
611
+ self, base_url: str, resource: str, data: Dict[str, Any], **kwargs
612
+ ) -> Dict[str, Any]:
613
+ """CREATE (POST) a new resource.
614
+
615
+ Args:
616
+ base_url: Base API URL
617
+ resource: Resource name (e.g., 'users', 'posts')
618
+ data: Resource data to create
619
+ **kwargs: Additional parameters (headers, etc.)
620
+
621
+ Returns:
622
+ API response dictionary
623
+ """
624
+ return self.run(
625
+ base_url=base_url, resource=resource, method="POST", data=data, **kwargs
626
+ )
627
+
628
+ def update(
629
+ self,
630
+ base_url: str,
631
+ resource: str,
632
+ resource_id: str,
633
+ data: Dict[str, Any],
634
+ partial: bool = False,
635
+ **kwargs,
636
+ ) -> Dict[str, Any]:
637
+ """UPDATE (PUT/PATCH) an existing resource.
638
+
639
+ Args:
640
+ base_url: Base API URL
641
+ resource: Resource name (e.g., 'users', 'posts')
642
+ resource_id: Resource ID to update
643
+ data: Updated resource data
644
+ partial: If True, use PATCH for partial update; if False, use PUT
645
+ **kwargs: Additional parameters (headers, etc.)
646
+
647
+ Returns:
648
+ API response dictionary
649
+ """
650
+ path_params = kwargs.pop("path_params", {})
651
+ path_params["id"] = resource_id
652
+
653
+ return self.run(
654
+ base_url=base_url,
655
+ resource=f"{resource}/{{id}}",
656
+ method="PATCH" if partial else "PUT",
657
+ path_params=path_params,
658
+ data=data,
659
+ **kwargs,
660
+ )
661
+
662
+ def delete(
663
+ self, base_url: str, resource: str, resource_id: str, **kwargs
664
+ ) -> Dict[str, Any]:
665
+ """DELETE a resource.
666
+
667
+ Args:
668
+ base_url: Base API URL
669
+ resource: Resource name (e.g., 'users', 'posts')
670
+ resource_id: Resource ID to delete
671
+ **kwargs: Additional parameters (headers, etc.)
672
+
673
+ Returns:
674
+ API response dictionary
675
+ """
676
+ path_params = kwargs.pop("path_params", {})
677
+ path_params["id"] = resource_id
678
+
679
+ return self.run(
680
+ base_url=base_url,
681
+ resource=f"{resource}/{{id}}",
682
+ method="DELETE",
683
+ path_params=path_params,
684
+ **kwargs,
685
+ )
686
+
687
+ def _extract_metadata(self, response: Dict[str, Any]) -> Dict[str, Any]:
688
+ """Extract additional metadata from response.
689
+
690
+ Args:
691
+ response: HTTP response dictionary
692
+
693
+ Returns:
694
+ Dictionary with extracted metadata
695
+ """
696
+ metadata = {}
697
+ headers = response.get("headers", {})
698
+
699
+ # Extract rate limit information
700
+ rate_limit = self._extract_rate_limit_metadata(headers)
701
+ if rate_limit:
702
+ metadata["rate_limit"] = rate_limit
703
+
704
+ # Extract pagination metadata
705
+ pagination = self._extract_pagination_metadata(
706
+ headers, response.get("content", {})
707
+ )
708
+ if pagination:
709
+ metadata["pagination"] = pagination
710
+
711
+ # Extract HATEOAS links
712
+ links = self._extract_links(response.get("content", {}))
713
+ if links:
714
+ metadata["links"] = links
715
+
716
+ return metadata
717
+
718
+ def _extract_rate_limit_metadata(
719
+ self, headers: Dict[str, str]
720
+ ) -> Optional[Dict[str, Any]]:
721
+ """Extract rate limiting information from response headers.
722
+
723
+ Args:
724
+ headers: Response headers dictionary
725
+
726
+ Returns:
727
+ Rate limit metadata or None if not found
728
+ """
729
+ rate_limit = {}
730
+
731
+ # Common rate limit headers
732
+ rate_limit_headers = {
733
+ "X-RateLimit-Limit": "limit",
734
+ "X-RateLimit-Remaining": "remaining",
735
+ "X-RateLimit-Reset": "reset",
736
+ "X-Rate-Limit-Limit": "limit",
737
+ "X-Rate-Limit-Remaining": "remaining",
738
+ "X-Rate-Limit-Reset": "reset",
739
+ "RateLimit-Limit": "limit",
740
+ "RateLimit-Remaining": "remaining",
741
+ "RateLimit-Reset": "reset",
742
+ }
743
+
744
+ for header, key in rate_limit_headers.items():
745
+ value = headers.get(header) or headers.get(header.lower())
746
+ if value:
747
+ try:
748
+ rate_limit[key] = int(value)
749
+ except ValueError:
750
+ rate_limit[key] = value
751
+
752
+ return rate_limit if rate_limit else None
493
753
 
494
- @register_node(alias="AsyncRESTClient")
754
+ def _extract_pagination_metadata(
755
+ self, headers: Dict[str, str], content: Any
756
+ ) -> Optional[Dict[str, Any]]:
757
+ """Extract pagination information from headers and response body.
758
+
759
+ Args:
760
+ headers: Response headers dictionary
761
+ content: Response body content
762
+
763
+ Returns:
764
+ Pagination metadata or None if not found
765
+ """
766
+ pagination = {}
767
+
768
+ # Extract from headers (Link header parsing)
769
+ link_header = headers.get("Link") or headers.get("link")
770
+ if link_header:
771
+ links = self._parse_link_header(link_header)
772
+ pagination.update(links)
773
+
774
+ # Extract from response body (common patterns)
775
+ if isinstance(content, dict):
776
+ # Look for common pagination fields
777
+ pagination_fields = {
778
+ "page": ["page", "current_page", "pageNumber"],
779
+ "per_page": ["per_page", "page_size", "pageSize", "limit"],
780
+ "total": ["total", "totalCount", "total_count", "totalRecords"],
781
+ "total_pages": ["total_pages", "totalPages", "pageCount"],
782
+ "has_next": ["has_next", "hasNext", "has_more", "hasMore"],
783
+ "has_prev": ["has_prev", "hasPrev", "has_previous", "hasPrevious"],
784
+ }
785
+
786
+ for key, fields in pagination_fields.items():
787
+ for field in fields:
788
+ # Check in root
789
+ if field in content:
790
+ pagination[key] = content[field]
791
+ break
792
+ # Check in meta/metadata
793
+ meta = content.get("meta") or content.get("metadata", {})
794
+ if isinstance(meta, dict) and field in meta:
795
+ pagination[key] = meta[field]
796
+ break
797
+
798
+ return pagination if pagination else None
799
+
800
+ def _parse_link_header(self, link_header: str) -> Dict[str, str]:
801
+ """Parse Link header for pagination URLs.
802
+
803
+ Args:
804
+ link_header: Link header value
805
+
806
+ Returns:
807
+ Dictionary of rel -> URL mappings
808
+ """
809
+ links = {}
810
+
811
+ # Parse Link header format: <url>; rel="next", <url>; rel="prev"
812
+ for link in link_header.split(","):
813
+ link = link.strip()
814
+ if ";" in link:
815
+ url_part, rel_part = link.split(";", 1)
816
+ url = url_part.strip("<>")
817
+ rel_match = rel_part.split("=", 1)
818
+ if len(rel_match) == 2:
819
+ rel = rel_match[1].strip("\"'")
820
+ links[rel] = url
821
+
822
+ return links
823
+
824
+ def _extract_links(self, content: Any) -> Optional[Dict[str, Any]]:
825
+ """Extract HATEOAS links from response content.
826
+
827
+ Args:
828
+ content: Response body content
829
+
830
+ Returns:
831
+ Links dictionary or None if not found
832
+ """
833
+ if not isinstance(content, dict):
834
+ return None
835
+
836
+ links = {}
837
+
838
+ # Check common link locations
839
+ link_fields = ["links", "_links", "link", "href"]
840
+
841
+ for field in link_fields:
842
+ if field in content:
843
+ link_data = content[field]
844
+ if isinstance(link_data, dict):
845
+ # HAL format: {"self": {"href": "..."}, "next": {"href": "..."}}
846
+ for rel, link_obj in link_data.items():
847
+ if isinstance(link_obj, dict) and "href" in link_obj:
848
+ links[rel] = link_obj["href"]
849
+ elif isinstance(link_obj, str):
850
+ links[rel] = link_obj
851
+ elif isinstance(link_data, list):
852
+ # Array of link objects
853
+ for link_obj in link_data:
854
+ if isinstance(link_obj, dict):
855
+ rel = link_obj.get("rel", "related")
856
+ href = link_obj.get("href") or link_obj.get("url")
857
+ if href:
858
+ links[rel] = href
859
+
860
+ return links if links else None
861
+
862
+
863
+ @register_node()
495
864
  class AsyncRESTClientNode(AsyncNode):
496
865
  """Asynchronous node for interacting with REST APIs.
497
866
 
@@ -499,17 +868,17 @@ class AsyncRESTClientNode(AsyncNode):
499
868
  asynchronous I/O for better performance, especially for concurrent requests.
500
869
 
501
870
  Design Purpose:
502
- - Enable efficient, non-blocking REST API operations in workflows
503
- - Provide the same interface as RESTClientNode but with async execution
504
- - Support high-throughput API integrations with minimal overhead
871
+ * Enable efficient, non-blocking REST API operations in workflows
872
+ * Provide the same interface as RESTClientNode but with async execution
873
+ * Support high-throughput API integrations with minimal overhead
505
874
 
506
875
  Upstream Usage:
507
- - AsyncLocalRuntime: Executes workflow with async support
508
- - Specialized async API nodes: May extend this node
876
+ * AsyncLocalRuntime: Executes workflow with async support
877
+ * Specialized async API nodes: May extend this node
509
878
 
510
879
  Downstream Consumers:
511
- - Data processing nodes: Consume API response data
512
- - Decision nodes: Route workflow based on API responses
880
+ * Data processing nodes: Consume API response data
881
+ * Decision nodes: Route workflow based on API responses
513
882
  """
514
883
 
515
884
  def __init__(self, **kwargs):
@@ -585,6 +954,12 @@ class AsyncRESTClientNode(AsyncNode):
585
954
  pagination_params = kwargs.get("pagination_params")
586
955
  retry_count = kwargs.get("retry_count", 0)
587
956
  retry_backoff = kwargs.get("retry_backoff", 0.5)
957
+ # Authentication parameters
958
+ auth_type = kwargs.get("auth_type")
959
+ auth_token = kwargs.get("auth_token")
960
+ auth_username = kwargs.get("auth_username")
961
+ auth_password = kwargs.get("auth_password")
962
+ api_key_header = kwargs.get("api_key_header", "X-API-Key")
588
963
 
589
964
  # Build full URL with path parameters (reuse from synchronous version)
590
965
  url = self.rest_node._build_url(base_url, resource, path_params, version)
@@ -614,6 +989,11 @@ class AsyncRESTClientNode(AsyncNode):
614
989
  "verify_ssl": verify_ssl,
615
990
  "retry_count": retry_count,
616
991
  "retry_backoff": retry_backoff,
992
+ "auth_type": auth_type,
993
+ "auth_token": auth_token,
994
+ "auth_username": auth_username,
995
+ "auth_password": auth_password,
996
+ "api_key_header": api_key_header,
617
997
  }
618
998
 
619
999
  # Execute the HTTP request asynchronously
@@ -621,24 +1001,52 @@ class AsyncRESTClientNode(AsyncNode):
621
1001
  result = await self.http_node.async_run(**http_params)
622
1002
 
623
1003
  # Extract response data
624
- response = result["response"]
625
- status_code = result["status_code"]
626
- success = result["success"]
1004
+ response = result.get("response")
1005
+ status_code = result.get("status_code")
1006
+ success = result.get("success", False)
627
1007
 
628
1008
  # Handle potential error responses
629
1009
  if not success:
630
- error_message = "Unknown error"
631
- if isinstance(response["content"], dict):
1010
+ error_message = result.get("error", "Unknown error")
1011
+
1012
+ # If we have a response object, try to extract error details
1013
+ if response and isinstance(response.get("content"), dict):
632
1014
  # Try to extract error message from common formats
633
- error_message = (
634
- response["content"].get("error", {}).get("message")
635
- or response["content"].get("message")
636
- or response["content"].get("error")
637
- or f"API returned error status: {status_code}"
638
- )
1015
+ content = response["content"]
1016
+ # Handle case where error is a string or dict
1017
+ error_value = content.get("error")
1018
+ if isinstance(error_value, dict):
1019
+ error_message = error_value.get("message") or error_message
1020
+ elif isinstance(error_value, str):
1021
+ error_message = error_value
1022
+ # Check for message at root level
1023
+ if not error_message or error_message == result.get(
1024
+ "error", "Unknown error"
1025
+ ):
1026
+ error_message = content.get("message") or error_message
1027
+
1028
+ # If we have a status code, include it
1029
+ if status_code:
1030
+ error_message = f"{error_message} (status: {status_code})"
639
1031
 
640
1032
  self.logger.error(f"REST API error: {error_message}")
641
1033
 
1034
+ # Return error response with recovery suggestions if available
1035
+ error_result = {
1036
+ "data": None,
1037
+ "status_code": status_code,
1038
+ "success": False,
1039
+ "error": error_message,
1040
+ "error_type": result.get("error_type", "APIError"),
1041
+ "metadata": {},
1042
+ }
1043
+
1044
+ # Include recovery suggestions if available
1045
+ if "recovery_suggestions" in result:
1046
+ error_result["recovery_suggestions"] = result["recovery_suggestions"]
1047
+
1048
+ return error_result
1049
+
642
1050
  # Handle pagination if requested (simplified for now)
643
1051
  data = response["content"]
644
1052
  if paginate and method == "GET" and success: