lfx-nightly 0.1.12.dev4__py3-none-any.whl → 0.1.12.dev6__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.
Potentially problematic release.
This version of lfx-nightly might be problematic. Click here for more details.
- lfx/base/composio/composio_base.py +994 -128
- {lfx_nightly-0.1.12.dev4.dist-info → lfx_nightly-0.1.12.dev6.dist-info}/METADATA +1 -1
- {lfx_nightly-0.1.12.dev4.dist-info → lfx_nightly-0.1.12.dev6.dist-info}/RECORD +5 -5
- {lfx_nightly-0.1.12.dev4.dist-info → lfx_nightly-0.1.12.dev6.dist-info}/WHEEL +0 -0
- {lfx_nightly-0.1.12.dev4.dist-info → lfx_nightly-0.1.12.dev6.dist-info}/entry_points.txt +0 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import copy
|
|
2
|
+
import json
|
|
2
3
|
import re
|
|
3
4
|
from typing import Any
|
|
4
5
|
|
|
@@ -6,53 +7,28 @@ from composio import Composio
|
|
|
6
7
|
from composio_langchain import LangchainProvider
|
|
7
8
|
from langchain_core.tools import Tool
|
|
8
9
|
|
|
10
|
+
from lfx.base.mcp.util import create_input_schema_from_json_schema
|
|
9
11
|
from lfx.custom.custom_component.component import Component
|
|
10
|
-
from lfx.inputs.inputs import
|
|
12
|
+
from lfx.inputs.inputs import (
|
|
13
|
+
AuthInput,
|
|
14
|
+
DropdownInput,
|
|
15
|
+
FileInput,
|
|
16
|
+
InputTypes,
|
|
17
|
+
MessageTextInput,
|
|
18
|
+
MultilineInput,
|
|
19
|
+
SecretStrInput,
|
|
20
|
+
SortableListInput,
|
|
21
|
+
StrInput,
|
|
22
|
+
TabInput,
|
|
23
|
+
)
|
|
11
24
|
from lfx.io import Output
|
|
12
25
|
from lfx.io.schema import flatten_schema, schema_to_langflow_inputs
|
|
13
26
|
from lfx.log.logger import logger
|
|
14
27
|
from lfx.schema.data import Data
|
|
15
28
|
from lfx.schema.dataframe import DataFrame
|
|
16
|
-
from lfx.schema.json_schema import create_input_schema_from_json_schema
|
|
17
29
|
from lfx.schema.message import Message
|
|
18
30
|
|
|
19
31
|
|
|
20
|
-
def _patch_graph_clean_null_input_types() -> None:
|
|
21
|
-
"""Monkey-patch Graph._create_vertex to clean legacy templates."""
|
|
22
|
-
try:
|
|
23
|
-
from lfx.graph.graph.base import Graph
|
|
24
|
-
|
|
25
|
-
if getattr(Graph, "_composio_patch_applied", False):
|
|
26
|
-
return
|
|
27
|
-
|
|
28
|
-
original_create_vertex = Graph._create_vertex
|
|
29
|
-
|
|
30
|
-
def _create_vertex_with_cleanup(self, frontend_data):
|
|
31
|
-
try:
|
|
32
|
-
node_id: str | None = frontend_data.get("id") if isinstance(frontend_data, dict) else None
|
|
33
|
-
if node_id and "Composio" in node_id:
|
|
34
|
-
template = frontend_data.get("data", {}).get("node", {}).get("template", {})
|
|
35
|
-
if isinstance(template, dict):
|
|
36
|
-
for field_cfg in template.values():
|
|
37
|
-
if isinstance(field_cfg, dict) and field_cfg.get("input_types") is None:
|
|
38
|
-
field_cfg["input_types"] = []
|
|
39
|
-
except (AttributeError, TypeError, KeyError) as e:
|
|
40
|
-
logger.debug(f"Composio template cleanup encountered error: {e}")
|
|
41
|
-
|
|
42
|
-
return original_create_vertex(self, frontend_data)
|
|
43
|
-
|
|
44
|
-
Graph._create_vertex = _create_vertex_with_cleanup # type: ignore[method-assign]
|
|
45
|
-
Graph._composio_patch_applied = True # type: ignore[attr-defined]
|
|
46
|
-
logger.debug("Applied Composio template cleanup patch to Graph._create_vertex")
|
|
47
|
-
|
|
48
|
-
except (AttributeError, TypeError) as e:
|
|
49
|
-
logger.debug(f"Failed to apply Composio Graph patch: {e}")
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
# Apply the patch at import time
|
|
53
|
-
_patch_graph_clean_null_input_types()
|
|
54
|
-
|
|
55
|
-
|
|
56
32
|
class ComposioBaseComponent(Component):
|
|
57
33
|
"""Base class for Composio components with common functionality."""
|
|
58
34
|
|
|
@@ -73,6 +49,17 @@ class ComposioBaseComponent(Component):
|
|
|
73
49
|
real_time_refresh=True,
|
|
74
50
|
value="COMPOSIO_API_KEY",
|
|
75
51
|
),
|
|
52
|
+
DropdownInput(
|
|
53
|
+
name="auth_mode",
|
|
54
|
+
display_name="Auth Mode",
|
|
55
|
+
options=[],
|
|
56
|
+
placeholder="Select auth mode",
|
|
57
|
+
toggle=True,
|
|
58
|
+
toggle_disable=True,
|
|
59
|
+
show=False,
|
|
60
|
+
real_time_refresh=True,
|
|
61
|
+
helper_text="Choose how to authenticate with the toolkit.",
|
|
62
|
+
),
|
|
76
63
|
AuthInput(
|
|
77
64
|
name="auth_link",
|
|
78
65
|
value="",
|
|
@@ -117,6 +104,10 @@ class ComposioBaseComponent(Component):
|
|
|
117
104
|
self._key_to_display_map: dict[str, str] = {}
|
|
118
105
|
self._sanitized_names: dict[str, str] = {}
|
|
119
106
|
self._action_schemas: dict[str, Any] = {}
|
|
107
|
+
# Toolkit schema cache per instance
|
|
108
|
+
self._toolkit_schema: dict[str, Any] | None = None
|
|
109
|
+
# Track generated custom auth inputs to hide/show/reset
|
|
110
|
+
self._auth_dynamic_fields: set[str] = set()
|
|
120
111
|
|
|
121
112
|
def as_message(self) -> Message:
|
|
122
113
|
result = self.execute_action()
|
|
@@ -130,14 +121,11 @@ class ComposioBaseComponent(Component):
|
|
|
130
121
|
if isinstance(result, dict):
|
|
131
122
|
result = [result]
|
|
132
123
|
# Build DataFrame and avoid exposing a 'data' attribute via column access,
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
except (AttributeError, TypeError, ValueError, KeyError) as e:
|
|
139
|
-
logger.debug(f"Failed to rename 'data' column: {e}")
|
|
140
|
-
return result_dataframe
|
|
124
|
+
# which interferes with logging utilities that probe for '.data'.
|
|
125
|
+
df = DataFrame(result)
|
|
126
|
+
if "data" in df.columns:
|
|
127
|
+
df = df.rename(columns={"data": "_data"})
|
|
128
|
+
return df
|
|
141
129
|
|
|
142
130
|
def as_data(self) -> Data:
|
|
143
131
|
result = self.execute_action()
|
|
@@ -358,6 +346,13 @@ class ComposioBaseComponent(Component):
|
|
|
358
346
|
|
|
359
347
|
# Check original schema properties for file_uploadable fields
|
|
360
348
|
original_props = parameters_schema.get("properties", {})
|
|
349
|
+
|
|
350
|
+
# Determine top-level fields that should be treated as single JSON inputs
|
|
351
|
+
json_parent_fields = set()
|
|
352
|
+
for top_name, top_schema in original_props.items():
|
|
353
|
+
if isinstance(top_schema, dict) and top_schema.get("type") in {"object", "array"}:
|
|
354
|
+
json_parent_fields.add(top_name)
|
|
355
|
+
|
|
361
356
|
for field_name, field_schema in original_props.items():
|
|
362
357
|
if isinstance(field_schema, dict):
|
|
363
358
|
clean_field_name = field_name.replace("[0]", "")
|
|
@@ -373,6 +368,10 @@ class ComposioBaseComponent(Component):
|
|
|
373
368
|
|
|
374
369
|
for field in raw_action_fields:
|
|
375
370
|
clean_field = field.replace("[0]", "")
|
|
371
|
+
# Skip subfields of JSON parents; we will expose the parent as a single field
|
|
372
|
+
top_prefix = clean_field.split(".")[0].split("[")[0]
|
|
373
|
+
if top_prefix in json_parent_fields and "." in clean_field:
|
|
374
|
+
continue
|
|
376
375
|
# Check if this field is attachment-related
|
|
377
376
|
if clean_field.lower().startswith("attachment."):
|
|
378
377
|
attachment_related_found = True
|
|
@@ -381,8 +380,11 @@ class ComposioBaseComponent(Component):
|
|
|
381
380
|
# Handle conflicting field names - rename user_id to avoid conflicts with entity_id
|
|
382
381
|
if clean_field == "user_id":
|
|
383
382
|
clean_field = f"{self.app_name}_user_id"
|
|
384
|
-
|
|
385
|
-
|
|
383
|
+
|
|
384
|
+
# Handle reserved attribute name conflicts (e.g., 'status') by prefixing with app name
|
|
385
|
+
# This prevents clashes with component attributes like self.status
|
|
386
|
+
if clean_field in {"status"}:
|
|
387
|
+
clean_field = f"{self.app_name}_{clean_field}"
|
|
386
388
|
|
|
387
389
|
action_fields.append(clean_field)
|
|
388
390
|
|
|
@@ -391,6 +393,11 @@ class ComposioBaseComponent(Component):
|
|
|
391
393
|
action_fields.append("attachment")
|
|
392
394
|
file_upload_fields.add("attachment") # Attachment fields are also file upload fields
|
|
393
395
|
|
|
396
|
+
# Ensure parents for object/array are present as fields (single JSON field)
|
|
397
|
+
for parent in json_parent_fields:
|
|
398
|
+
if parent not in action_fields:
|
|
399
|
+
action_fields.append(parent)
|
|
400
|
+
|
|
394
401
|
# Track boolean parameters so we can coerce them later
|
|
395
402
|
properties = flat_schema.get("properties", {})
|
|
396
403
|
if properties:
|
|
@@ -530,17 +537,17 @@ class ComposioBaseComponent(Component):
|
|
|
530
537
|
# Handle conflicting field names - rename user_id to avoid conflicts with entity_id
|
|
531
538
|
if clean_field_name == "user_id":
|
|
532
539
|
clean_field_name = f"{self.app_name}_user_id"
|
|
533
|
-
# Update
|
|
540
|
+
# Update the field schema description to reflect the name change
|
|
534
541
|
field_schema_copy = field_schema.copy()
|
|
535
542
|
field_schema_copy["description"] = (
|
|
536
543
|
f"User ID for {self.app_name.title()}: " + field_schema["description"]
|
|
537
544
|
)
|
|
538
545
|
elif clean_field_name == "status":
|
|
539
546
|
clean_field_name = f"{self.app_name}_status"
|
|
540
|
-
# Update
|
|
547
|
+
# Update the field schema description to reflect the name change
|
|
541
548
|
field_schema_copy = field_schema.copy()
|
|
542
|
-
field_schema_copy["description"] = (
|
|
543
|
-
|
|
549
|
+
field_schema_copy["description"] = f"Status for {self.app_name.title()}: " + field_schema.get(
|
|
550
|
+
"description", ""
|
|
544
551
|
)
|
|
545
552
|
else:
|
|
546
553
|
# Use the original field schema for all other fields
|
|
@@ -593,8 +600,20 @@ class ComposioBaseComponent(Component):
|
|
|
593
600
|
if attachment_related_fields: # If we consolidated attachment fields
|
|
594
601
|
file_upload_fields = file_upload_fields | {"attachment"}
|
|
595
602
|
|
|
603
|
+
# Identify top-level JSON parents (object/array) to render as single CodeInput
|
|
604
|
+
top_props_for_json = set()
|
|
605
|
+
props_dict = parameters_schema.get("properties", {}) if isinstance(parameters_schema, dict) else {}
|
|
606
|
+
for top_name, top_schema in props_dict.items():
|
|
607
|
+
if isinstance(top_schema, dict) and top_schema.get("type") in {"object", "array"}:
|
|
608
|
+
top_props_for_json.add(top_name)
|
|
609
|
+
|
|
596
610
|
for inp in result:
|
|
597
611
|
if hasattr(inp, "name") and inp.name is not None:
|
|
612
|
+
# Skip flattened subfields of JSON parents; handle array prefixes (e.g., parent[0].x)
|
|
613
|
+
raw_prefix = inp.name.split(".")[0]
|
|
614
|
+
base_prefix = raw_prefix.replace("[0]", "")
|
|
615
|
+
if base_prefix in top_props_for_json and ("." in inp.name or "[" in inp.name):
|
|
616
|
+
continue
|
|
598
617
|
# Check if this specific field is a file upload field
|
|
599
618
|
if inp.name.lower() in file_upload_fields or inp.name.lower() == "attachment":
|
|
600
619
|
# Replace with FileInput for file upload fields
|
|
@@ -655,6 +674,24 @@ class ComposioBaseComponent(Component):
|
|
|
655
674
|
else:
|
|
656
675
|
processed_inputs.append(inp)
|
|
657
676
|
|
|
677
|
+
# Add single CodeInput for each JSON parent field
|
|
678
|
+
props_dict = parameters_schema.get("properties", {}) if isinstance(parameters_schema, dict) else {}
|
|
679
|
+
for top_name in top_props_for_json:
|
|
680
|
+
# Avoid duplicates if already present
|
|
681
|
+
if any(getattr(i, "name", None) == top_name for i in processed_inputs):
|
|
682
|
+
continue
|
|
683
|
+
top_schema = props_dict.get(top_name, {})
|
|
684
|
+
processed_inputs.append(
|
|
685
|
+
MultilineInput(
|
|
686
|
+
name=top_name,
|
|
687
|
+
display_name=top_schema.get("title") or top_name.replace("_", " ").title(),
|
|
688
|
+
info=(
|
|
689
|
+
top_schema.get("description") or "Provide JSON for this parameter (object or array)."
|
|
690
|
+
),
|
|
691
|
+
required=top_name in required_fields_set,
|
|
692
|
+
)
|
|
693
|
+
)
|
|
694
|
+
|
|
658
695
|
return processed_inputs
|
|
659
696
|
return result # noqa: TRY300
|
|
660
697
|
except ValueError as e:
|
|
@@ -707,9 +744,7 @@ class ComposioBaseComponent(Component):
|
|
|
707
744
|
if inp.name is not None:
|
|
708
745
|
inp_dict = inp.to_dict() if hasattr(inp, "to_dict") else inp.__dict__.copy()
|
|
709
746
|
|
|
710
|
-
#
|
|
711
|
-
if not isinstance(inp_dict.get("input_types"), list):
|
|
712
|
-
inp_dict["input_types"] = []
|
|
747
|
+
# Do not mutate input_types here; keep original configuration
|
|
713
748
|
|
|
714
749
|
inp_dict.setdefault("show", True) # visible once action selected
|
|
715
750
|
# Preserve previously entered value if user already filled something
|
|
@@ -773,7 +808,7 @@ class ComposioBaseComponent(Component):
|
|
|
773
808
|
logger.info(f"OAuth connection initiated for {app_name}: {redirect_url} (ID: {connection_id})")
|
|
774
809
|
return redirect_url, connection_id # noqa: TRY300
|
|
775
810
|
|
|
776
|
-
except
|
|
811
|
+
except (ValueError, ConnectionError, TypeError, AttributeError) as e:
|
|
777
812
|
logger.error(f"Error initiating connection for {app_name}: {e}")
|
|
778
813
|
msg = f"Failed to initiate OAuth connection: {e}"
|
|
779
814
|
raise ValueError(msg) from e
|
|
@@ -813,6 +848,22 @@ class ComposioBaseComponent(Component):
|
|
|
813
848
|
else:
|
|
814
849
|
return None
|
|
815
850
|
|
|
851
|
+
def _get_connection_auth_info(self, connection_id: str) -> tuple[str | None, bool | None]:
|
|
852
|
+
"""Return (auth_scheme, is_composio_managed) for a given connection id, if available."""
|
|
853
|
+
try:
|
|
854
|
+
composio = self._build_wrapper()
|
|
855
|
+
connection = composio.connected_accounts.get(nanoid=connection_id)
|
|
856
|
+
auth_config = getattr(connection, "auth_config", None)
|
|
857
|
+
if auth_config is None and hasattr(connection, "__dict__"):
|
|
858
|
+
auth_config = getattr(connection.__dict__, "auth_config", None)
|
|
859
|
+
scheme = getattr(auth_config, "auth_scheme", None) if auth_config else None
|
|
860
|
+
is_managed = getattr(auth_config, "is_composio_managed", None) if auth_config else None
|
|
861
|
+
except (AttributeError, ValueError, ConnectionError, TypeError) as e:
|
|
862
|
+
logger.debug(f"Could not retrieve auth info for connection {connection_id}: {e}")
|
|
863
|
+
return None, None
|
|
864
|
+
else:
|
|
865
|
+
return scheme, is_managed
|
|
866
|
+
|
|
816
867
|
def _disconnect_specific_connection(self, connection_id: str) -> None:
|
|
817
868
|
"""Disconnect a specific Composio connection by ID."""
|
|
818
869
|
try:
|
|
@@ -825,12 +876,227 @@ class ComposioBaseComponent(Component):
|
|
|
825
876
|
msg = f"Failed to disconnect connection {connection_id}: {e}"
|
|
826
877
|
raise ValueError(msg) from e
|
|
827
878
|
|
|
879
|
+
def _to_plain_dict(self, obj: Any) -> Any:
|
|
880
|
+
"""Recursively convert SDK models/lists to plain Python dicts/lists for safe .get access."""
|
|
881
|
+
try:
|
|
882
|
+
if isinstance(obj, dict):
|
|
883
|
+
return {k: self._to_plain_dict(v) for k, v in obj.items()}
|
|
884
|
+
if isinstance(obj, (list, tuple, set)):
|
|
885
|
+
return [self._to_plain_dict(v) for v in obj]
|
|
886
|
+
if hasattr(obj, "model_dump"):
|
|
887
|
+
try:
|
|
888
|
+
return self._to_plain_dict(obj.model_dump())
|
|
889
|
+
except (TypeError, AttributeError, ValueError):
|
|
890
|
+
pass
|
|
891
|
+
if hasattr(obj, "__dict__") and not isinstance(obj, (str, bytes)):
|
|
892
|
+
try:
|
|
893
|
+
return self._to_plain_dict({k: v for k, v in obj.__dict__.items() if not k.startswith("_")})
|
|
894
|
+
except (TypeError, AttributeError, ValueError):
|
|
895
|
+
pass
|
|
896
|
+
except (TypeError, ValueError, AttributeError, RecursionError):
|
|
897
|
+
return obj
|
|
898
|
+
else:
|
|
899
|
+
return obj
|
|
900
|
+
|
|
901
|
+
def _get_toolkit_schema(self) -> dict[str, Any] | None:
|
|
902
|
+
"""Fetch and cache toolkit schema for auth details (modes and fields)."""
|
|
903
|
+
if self._toolkit_schema is not None:
|
|
904
|
+
return self._toolkit_schema
|
|
905
|
+
try:
|
|
906
|
+
composio = self._build_wrapper()
|
|
907
|
+
# The SDK typically offers a retrieve by slug; if not present, try a few fallbacks
|
|
908
|
+
app_slug = getattr(self, "app_name", "").lower()
|
|
909
|
+
if not app_slug:
|
|
910
|
+
return None
|
|
911
|
+
try:
|
|
912
|
+
schema = composio.toolkits.retrieve(slug=app_slug)
|
|
913
|
+
except (AttributeError, ValueError, ConnectionError, TypeError):
|
|
914
|
+
schema = None
|
|
915
|
+
for method_name, kwargs in (
|
|
916
|
+
("retrieve", {"toolkit_slug": app_slug}),
|
|
917
|
+
("get", {"slug": app_slug}),
|
|
918
|
+
("get", {"toolkit_slug": app_slug}),
|
|
919
|
+
):
|
|
920
|
+
try:
|
|
921
|
+
method = getattr(composio.toolkits, method_name)
|
|
922
|
+
schema = method(**kwargs)
|
|
923
|
+
if schema:
|
|
924
|
+
break
|
|
925
|
+
except (AttributeError, ValueError, ConnectionError, TypeError):
|
|
926
|
+
continue
|
|
927
|
+
self._toolkit_schema = self._to_plain_dict(schema)
|
|
928
|
+
except (AttributeError, ValueError, ConnectionError, TypeError) as e:
|
|
929
|
+
logger.debug(f"Could not retrieve toolkit schema for {getattr(self, 'app_name', '')}: {e}")
|
|
930
|
+
return None
|
|
931
|
+
else:
|
|
932
|
+
return self._toolkit_schema
|
|
933
|
+
|
|
934
|
+
def _extract_auth_modes_from_schema(self, schema: dict[str, Any] | None) -> list[str]:
|
|
935
|
+
"""Return available auth modes (e.g., OAUTH2, API_KEY) from toolkit schema."""
|
|
936
|
+
if not schema:
|
|
937
|
+
return []
|
|
938
|
+
modes: list[str] = []
|
|
939
|
+
# composio_managed_auth_schemes: list[str]
|
|
940
|
+
managed = schema.get("composio_managed_auth_schemes") or schema.get("composioManagedAuthSchemes") or []
|
|
941
|
+
if isinstance(managed, list):
|
|
942
|
+
modes.extend([m for m in managed if isinstance(m, str)])
|
|
943
|
+
# auth_config_details: list with entries containing mode
|
|
944
|
+
details = schema.get("auth_config_details") or schema.get("authConfigDetails") or []
|
|
945
|
+
for item in details:
|
|
946
|
+
mode = item.get("mode") or item.get("auth_method")
|
|
947
|
+
if isinstance(mode, str) and mode not in modes:
|
|
948
|
+
modes.append(mode)
|
|
949
|
+
return modes
|
|
950
|
+
|
|
951
|
+
def _render_auth_mode_dropdown(self, build_config: dict, modes: list[str]) -> None:
|
|
952
|
+
"""Populate and show the auth_mode control; if only one mode, show as selected chip-style list."""
|
|
953
|
+
try:
|
|
954
|
+
build_config.setdefault("auth_mode", {})
|
|
955
|
+
auth_mode_cfg = build_config["auth_mode"]
|
|
956
|
+
# Prefer the connected scheme if known; otherwise use schema-provided modes as-is
|
|
957
|
+
stored_scheme = (build_config.get("auth_link") or {}).get("auth_scheme")
|
|
958
|
+
if isinstance(stored_scheme, str) and stored_scheme:
|
|
959
|
+
modes = [stored_scheme]
|
|
960
|
+
|
|
961
|
+
if len(modes) <= 1:
|
|
962
|
+
# Single mode → show a pill in the auth_mode slot (right after API Key)
|
|
963
|
+
selected = modes[0] if modes else ""
|
|
964
|
+
try:
|
|
965
|
+
pill = TabInput(
|
|
966
|
+
name="auth_mode",
|
|
967
|
+
display_name="Auth Mode",
|
|
968
|
+
options=[selected] if selected else [],
|
|
969
|
+
value=selected,
|
|
970
|
+
).to_dict()
|
|
971
|
+
pill["show"] = True
|
|
972
|
+
build_config["auth_mode"] = pill
|
|
973
|
+
except (TypeError, ValueError, AttributeError):
|
|
974
|
+
build_config["auth_mode"] = {
|
|
975
|
+
"name": "auth_mode",
|
|
976
|
+
"display_name": "Auth Mode",
|
|
977
|
+
"type": "tab",
|
|
978
|
+
"options": [selected],
|
|
979
|
+
"value": selected,
|
|
980
|
+
"show": True,
|
|
981
|
+
}
|
|
982
|
+
else:
|
|
983
|
+
# Multiple modes → normal dropdown, hide the display chip if present
|
|
984
|
+
auth_mode_cfg["options"] = modes
|
|
985
|
+
auth_mode_cfg["show"] = True
|
|
986
|
+
if not auth_mode_cfg.get("value") and modes:
|
|
987
|
+
auth_mode_cfg["value"] = modes[0]
|
|
988
|
+
if "auth_mode_display" in build_config:
|
|
989
|
+
build_config["auth_mode_display"]["show"] = False
|
|
990
|
+
auth_mode_cfg["helper_text"] = "Choose how to authenticate with the toolkit."
|
|
991
|
+
except (TypeError, ValueError, AttributeError) as e:
|
|
992
|
+
logger.debug(f"Failed to render auth_mode dropdown: {e}")
|
|
993
|
+
|
|
994
|
+
def _clear_auth_dynamic_fields(self, build_config: dict) -> None:
|
|
995
|
+
for fname in list(self._auth_dynamic_fields):
|
|
996
|
+
if fname in build_config:
|
|
997
|
+
build_config.pop(fname, None)
|
|
998
|
+
self._auth_dynamic_fields.clear()
|
|
999
|
+
|
|
1000
|
+
def _add_text_field(
|
|
1001
|
+
self,
|
|
1002
|
+
build_config: dict,
|
|
1003
|
+
name: str,
|
|
1004
|
+
display_name: str,
|
|
1005
|
+
info: str | None,
|
|
1006
|
+
*,
|
|
1007
|
+
required: bool,
|
|
1008
|
+
default_value: str | None = None,
|
|
1009
|
+
) -> None:
|
|
1010
|
+
"""Add a simple text input (StrInput) for custom auth forms, prefilled with schema defaults when available."""
|
|
1011
|
+
field = StrInput(
|
|
1012
|
+
name=name,
|
|
1013
|
+
display_name=display_name or name.replace("_", " ").title(),
|
|
1014
|
+
info=info or "",
|
|
1015
|
+
required=required,
|
|
1016
|
+
real_time_refresh=True,
|
|
1017
|
+
show=True,
|
|
1018
|
+
).to_dict()
|
|
1019
|
+
if default_value is not None and default_value != "":
|
|
1020
|
+
field["value"] = default_value
|
|
1021
|
+
build_config[name] = field
|
|
1022
|
+
self._auth_dynamic_fields.add(name)
|
|
1023
|
+
|
|
1024
|
+
def _render_custom_auth_fields(self, build_config: dict, schema: dict[str, Any], mode: str) -> None:
|
|
1025
|
+
"""Render fields for custom auth based on schema auth_config_details sections."""
|
|
1026
|
+
details = schema.get("auth_config_details") or schema.get("authConfigDetails") or []
|
|
1027
|
+
selected = None
|
|
1028
|
+
for item in details:
|
|
1029
|
+
if (item.get("mode") or item.get("auth_method")) == mode:
|
|
1030
|
+
selected = item
|
|
1031
|
+
break
|
|
1032
|
+
if not selected:
|
|
1033
|
+
return
|
|
1034
|
+
fields = selected.get("fields") or {}
|
|
1035
|
+
# a) AuthConfigCreation required fields for OAUTH2 custom
|
|
1036
|
+
creation = fields.get("auth_config_creation") or fields.get("authConfigCreation") or {}
|
|
1037
|
+
for req in creation.get("required", []):
|
|
1038
|
+
name = req.get("name")
|
|
1039
|
+
if not name:
|
|
1040
|
+
continue
|
|
1041
|
+
disp = req.get("display_name") or req.get("displayName") or name
|
|
1042
|
+
desc = req.get("description")
|
|
1043
|
+
default_val = req.get("default")
|
|
1044
|
+
self._add_text_field(build_config, name, disp, desc, required=True, default_value=default_val)
|
|
1045
|
+
# Optional auth_config_creation fields intentionally not rendered
|
|
1046
|
+
# b) ConnectedAccountInitiation fields for API_KEY mode
|
|
1047
|
+
initiation = fields.get("connected_account_initiation") or fields.get("connectedAccountInitiation") or {}
|
|
1048
|
+
for req in initiation.get("required", []):
|
|
1049
|
+
name = req.get("name")
|
|
1050
|
+
if not name:
|
|
1051
|
+
continue
|
|
1052
|
+
disp = req.get("display_name") or req.get("displayName") or name
|
|
1053
|
+
desc = req.get("description")
|
|
1054
|
+
default_val = req.get("default")
|
|
1055
|
+
self._add_text_field(build_config, name, disp, desc, required=True, default_value=default_val)
|
|
1056
|
+
for opt in initiation.get("optional", []):
|
|
1057
|
+
name = opt.get("name")
|
|
1058
|
+
if not name:
|
|
1059
|
+
continue
|
|
1060
|
+
disp = opt.get("display_name") or opt.get("displayName") or name
|
|
1061
|
+
desc = opt.get("description")
|
|
1062
|
+
default_val = opt.get("default")
|
|
1063
|
+
self._add_text_field(build_config, name, disp, desc, required=False, default_value=default_val)
|
|
1064
|
+
|
|
1065
|
+
def _collect_all_auth_field_names(self, schema: dict[str, Any] | None) -> set[str]:
|
|
1066
|
+
names: set[str] = set()
|
|
1067
|
+
if not schema:
|
|
1068
|
+
return names
|
|
1069
|
+
details = schema.get("auth_config_details") or schema.get("authConfigDetails") or []
|
|
1070
|
+
for item in details:
|
|
1071
|
+
fields = (item.get("fields") or {}) if isinstance(item, dict) else {}
|
|
1072
|
+
for section_key in (
|
|
1073
|
+
"auth_config_creation",
|
|
1074
|
+
"authConfigCreation",
|
|
1075
|
+
"connected_account_initiation",
|
|
1076
|
+
"connectedAccountInitiation",
|
|
1077
|
+
):
|
|
1078
|
+
section = fields.get(section_key) or {}
|
|
1079
|
+
for bucket in ("required", "optional"):
|
|
1080
|
+
for entry in section.get(bucket, []) or []:
|
|
1081
|
+
name = entry.get("name") if isinstance(entry, dict) else None
|
|
1082
|
+
if name:
|
|
1083
|
+
names.add(name)
|
|
1084
|
+
# Only use names discovered from the toolkit schema; do not add aliases
|
|
1085
|
+
return names
|
|
1086
|
+
|
|
1087
|
+
def _clear_auth_fields_from_schema(self, build_config: dict, schema: dict[str, Any] | None) -> None:
|
|
1088
|
+
all_names = self._collect_all_auth_field_names(schema)
|
|
1089
|
+
for name in list(all_names):
|
|
1090
|
+
if name in build_config and isinstance(build_config[name], dict):
|
|
1091
|
+
# Hide and reset instead of removing to ensure UI updates immediately
|
|
1092
|
+
build_config[name]["show"] = False
|
|
1093
|
+
build_config[name]["value"] = ""
|
|
1094
|
+
# Also clear any tracked dynamic fields
|
|
1095
|
+
self._clear_auth_dynamic_fields(build_config)
|
|
1096
|
+
|
|
828
1097
|
def update_build_config(self, build_config: dict, field_value: Any, field_name: str | None = None) -> dict:
|
|
829
1098
|
"""Update build config for auth and action selection."""
|
|
830
|
-
#
|
|
831
|
-
for _fconfig in build_config.values():
|
|
832
|
-
if isinstance(_fconfig, dict) and _fconfig.get("input_types") is None:
|
|
833
|
-
_fconfig["input_types"] = []
|
|
1099
|
+
# Avoid normalizing legacy input_types here; rely on upstream fixes
|
|
834
1100
|
|
|
835
1101
|
# BULLETPROOF tool_mode checking - check all possible places where tool_mode could be stored
|
|
836
1102
|
instance_tool_mode = getattr(self, "tool_mode", False) if hasattr(self, "tool_mode") else False
|
|
@@ -880,6 +1146,19 @@ class ComposioBaseComponent(Component):
|
|
|
880
1146
|
logger.info(f"Populating actions data for {getattr(self, 'app_name', 'unknown')}...")
|
|
881
1147
|
self._populate_actions_data()
|
|
882
1148
|
logger.info(f"Actions populated: {len(self._actions_data)} actions found")
|
|
1149
|
+
# Also fetch toolkit schema to drive auth UI
|
|
1150
|
+
schema = self._get_toolkit_schema()
|
|
1151
|
+
modes = self._extract_auth_modes_from_schema(schema)
|
|
1152
|
+
self._render_auth_mode_dropdown(build_config, modes)
|
|
1153
|
+
# If a mode is selected (including auto-default), render custom fields when not managed
|
|
1154
|
+
try:
|
|
1155
|
+
selected_mode = (build_config.get("auth_mode") or {}).get("value")
|
|
1156
|
+
managed = (schema or {}).get("composio_managed_auth_schemes") or []
|
|
1157
|
+
if selected_mode and not (isinstance(managed, list) and selected_mode in managed):
|
|
1158
|
+
self._clear_auth_dynamic_fields(build_config)
|
|
1159
|
+
self._render_custom_auth_fields(build_config, schema or {}, selected_mode)
|
|
1160
|
+
except (TypeError, ValueError, AttributeError):
|
|
1161
|
+
pass
|
|
883
1162
|
|
|
884
1163
|
# CRITICAL: Set action options if we have actions (either from fresh population or cache)
|
|
885
1164
|
if self._actions_data:
|
|
@@ -888,6 +1167,10 @@ class ComposioBaseComponent(Component):
|
|
|
888
1167
|
{"name": self.sanitize_action_name(action), "metadata": action} for action in self._actions_data
|
|
889
1168
|
]
|
|
890
1169
|
logger.info(f"Action options set in build_config: {len(build_config['action_button']['options'])} options")
|
|
1170
|
+
# Always (re)populate auth_mode as well when actions are available
|
|
1171
|
+
schema = self._get_toolkit_schema()
|
|
1172
|
+
modes = self._extract_auth_modes_from_schema(schema)
|
|
1173
|
+
self._render_auth_mode_dropdown(build_config, modes)
|
|
891
1174
|
else:
|
|
892
1175
|
build_config["action_button"]["options"] = []
|
|
893
1176
|
logger.warning("No actions found, setting empty options")
|
|
@@ -902,31 +1185,127 @@ class ComposioBaseComponent(Component):
|
|
|
902
1185
|
logger.info(f"Cleared stored connection_id '{stored_connection_before}' due to API key change")
|
|
903
1186
|
else:
|
|
904
1187
|
logger.info("DEBUG: EARLY No stored connection_id to clear on API key change")
|
|
1188
|
+
# Also clear any stored scheme and reset auth mode UI when API key changes
|
|
1189
|
+
build_config.setdefault("auth_link", {})
|
|
1190
|
+
build_config["auth_link"].pop("auth_scheme", None)
|
|
1191
|
+
build_config.setdefault("auth_mode", {})
|
|
1192
|
+
build_config["auth_mode"].pop("value", None)
|
|
1193
|
+
build_config["auth_mode"]["show"] = True
|
|
1194
|
+
# If auth_mode is currently a TabInput pill, convert it back to dropdown
|
|
1195
|
+
if isinstance(build_config.get("auth_mode"), dict) and build_config["auth_mode"].get("type") == "tab":
|
|
1196
|
+
build_config["auth_mode"].pop("type", None)
|
|
1197
|
+
# Re-render dropdown options for the new API key context
|
|
1198
|
+
try:
|
|
1199
|
+
schema = self._get_toolkit_schema()
|
|
1200
|
+
modes = self._extract_auth_modes_from_schema(schema)
|
|
1201
|
+
# Rebuild as DropdownInput to ensure proper rendering
|
|
1202
|
+
dd = DropdownInput(
|
|
1203
|
+
name="auth_mode",
|
|
1204
|
+
display_name="Auth Mode",
|
|
1205
|
+
options=modes,
|
|
1206
|
+
placeholder="Select auth mode",
|
|
1207
|
+
toggle=True,
|
|
1208
|
+
toggle_disable=True,
|
|
1209
|
+
show=True,
|
|
1210
|
+
real_time_refresh=True,
|
|
1211
|
+
helper_text="Choose how to authenticate with the toolkit.",
|
|
1212
|
+
).to_dict()
|
|
1213
|
+
build_config["auth_mode"] = dd
|
|
1214
|
+
except (TypeError, ValueError, AttributeError):
|
|
1215
|
+
pass
|
|
1216
|
+
# NEW: Clear any selected action and hide generated fields when API key is re-entered
|
|
1217
|
+
try:
|
|
1218
|
+
if "action_button" in build_config and isinstance(build_config["action_button"], dict):
|
|
1219
|
+
build_config["action_button"]["value"] = "disabled"
|
|
1220
|
+
self._hide_all_action_fields(build_config)
|
|
1221
|
+
except (TypeError, ValueError, AttributeError):
|
|
1222
|
+
pass
|
|
905
1223
|
|
|
906
1224
|
# Handle disconnect operations when tool mode is enabled
|
|
907
1225
|
if field_name == "auth_link" and field_value == "disconnect":
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
1226
|
+
# Soft disconnect: do not delete remote account; only clear local state
|
|
1227
|
+
stored_connection_id = build_config.get("auth_link", {}).get("connection_id")
|
|
1228
|
+
if not stored_connection_id:
|
|
1229
|
+
logger.warning("No connection ID found to disconnect (soft)")
|
|
1230
|
+
build_config.setdefault("auth_link", {})
|
|
1231
|
+
build_config["auth_link"]["value"] = "connect"
|
|
1232
|
+
build_config["auth_link"]["auth_tooltip"] = "Connect"
|
|
1233
|
+
build_config["auth_link"].pop("connection_id", None)
|
|
1234
|
+
build_config["action_button"]["helper_text"] = "Please connect before selecting actions."
|
|
1235
|
+
build_config["action_button"]["helper_text_metadata"] = {"variant": "destructive"}
|
|
1236
|
+
return build_config
|
|
1237
|
+
|
|
1238
|
+
# Handle auth mode change -> render appropriate fields based on schema
|
|
1239
|
+
if field_name == "auth_mode":
|
|
1240
|
+
schema = self._get_toolkit_schema() or {}
|
|
1241
|
+
# Clear any previously rendered auth fields when switching modes
|
|
1242
|
+
self._clear_auth_fields_from_schema(build_config, schema)
|
|
1243
|
+
mode = field_value if isinstance(field_value, str) else (build_config.get("auth_mode", {}).get("value"))
|
|
1244
|
+
if not mode and isinstance(build_config.get("auth_mode"), dict):
|
|
1245
|
+
mode = build_config["auth_mode"].get("value")
|
|
1246
|
+
# Always show auth_link for any mode
|
|
1247
|
+
build_config.setdefault("auth_link", {})
|
|
1248
|
+
build_config["auth_link"]["show"] = False
|
|
1249
|
+
# Reset connection state when switching modes
|
|
1250
|
+
build_config["auth_link"].pop("connection_id", None)
|
|
1251
|
+
build_config["auth_link"].pop("auth_config_id", None)
|
|
1252
|
+
build_config["auth_link"]["value"] = "connect"
|
|
1253
|
+
build_config["auth_link"]["auth_tooltip"] = "Connect"
|
|
1254
|
+
# If an ACTIVE connection already exists, don't render any auth fields
|
|
1255
|
+
existing_active = self._find_active_connection_for_app(self.app_name)
|
|
1256
|
+
if existing_active:
|
|
1257
|
+
connection_id, _ = existing_active
|
|
1258
|
+
self._clear_auth_fields_from_schema(build_config, schema)
|
|
1259
|
+
build_config.setdefault("create_auth_config", {})
|
|
1260
|
+
build_config["create_auth_config"]["show"] = False
|
|
1261
|
+
build_config["auth_link"]["value"] = "validated"
|
|
1262
|
+
build_config["auth_link"]["auth_tooltip"] = "Disconnect"
|
|
1263
|
+
build_config["auth_link"]["connection_id"] = connection_id
|
|
1264
|
+
# Reflect the connected auth scheme in the UI
|
|
1265
|
+
scheme, is_managed = self._get_connection_auth_info(connection_id)
|
|
1266
|
+
if scheme:
|
|
1267
|
+
build_config.setdefault("auth_link", {})
|
|
1268
|
+
build_config["auth_link"]["auth_scheme"] = scheme
|
|
1269
|
+
build_config.setdefault("auth_mode", {})
|
|
1270
|
+
build_config["auth_mode"]["value"] = scheme
|
|
1271
|
+
build_config["auth_mode"]["options"] = [scheme]
|
|
1272
|
+
build_config["auth_mode"]["show"] = False
|
|
1273
|
+
try:
|
|
1274
|
+
pill = TabInput(
|
|
1275
|
+
name="auth_mode",
|
|
1276
|
+
display_name="Auth Mode",
|
|
1277
|
+
options=[scheme],
|
|
1278
|
+
value=scheme,
|
|
1279
|
+
).to_dict()
|
|
1280
|
+
pill["show"] = True
|
|
1281
|
+
build_config["auth_mode"] = pill
|
|
1282
|
+
except (TypeError, ValueError, AttributeError):
|
|
1283
|
+
build_config["auth_mode"] = {
|
|
1284
|
+
"name": "auth_mode",
|
|
1285
|
+
"display_name": "Auth Mode",
|
|
1286
|
+
"type": "tab",
|
|
1287
|
+
"options": [scheme],
|
|
1288
|
+
"value": scheme,
|
|
1289
|
+
"show": True,
|
|
1290
|
+
}
|
|
1291
|
+
build_config["action_button"]["helper_text"] = ""
|
|
1292
|
+
build_config["action_button"]["helper_text_metadata"] = {}
|
|
918
1293
|
return build_config
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
build_config
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
build_config["
|
|
926
|
-
build_config["
|
|
927
|
-
build_config["
|
|
928
|
-
|
|
929
|
-
|
|
1294
|
+
if mode:
|
|
1295
|
+
managed = schema.get("composio_managed_auth_schemes") or []
|
|
1296
|
+
# Always hide the Create Auth Config control (used internally only)
|
|
1297
|
+
build_config.setdefault("create_auth_config", {})
|
|
1298
|
+
build_config["create_auth_config"]["show"] = False
|
|
1299
|
+
build_config["create_auth_config"]["display_name"] = ""
|
|
1300
|
+
build_config["create_auth_config"]["value"] = ""
|
|
1301
|
+
build_config["create_auth_config"]["helper_text"] = ""
|
|
1302
|
+
build_config["create_auth_config"]["options"] = ["create"]
|
|
1303
|
+
if isinstance(managed, list) and mode in managed:
|
|
1304
|
+
# Managed → no extra fields
|
|
1305
|
+
pass
|
|
1306
|
+
else:
|
|
1307
|
+
# Custom → render only required fields based on the toolkit schema
|
|
1308
|
+
self._render_custom_auth_fields(build_config, schema, mode)
|
|
930
1309
|
return build_config
|
|
931
1310
|
|
|
932
1311
|
# Handle connection initiation when tool mode is enabled
|
|
@@ -946,36 +1325,268 @@ class ComposioBaseComponent(Component):
|
|
|
946
1325
|
logger.info(f"Using existing ACTIVE connection {connection_id} for {toolkit_slug}")
|
|
947
1326
|
return build_config
|
|
948
1327
|
|
|
949
|
-
#
|
|
950
|
-
stored_connection_id =
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
#
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
1328
|
+
# Only reuse ACTIVE connections; otherwise create a new connection
|
|
1329
|
+
stored_connection_id = None
|
|
1330
|
+
|
|
1331
|
+
# Create new connection ONLY if we truly have no usable connection yet
|
|
1332
|
+
if existing_active is None:
|
|
1333
|
+
try:
|
|
1334
|
+
# Determine auth mode
|
|
1335
|
+
schema = self._get_toolkit_schema()
|
|
1336
|
+
mode = None
|
|
1337
|
+
if isinstance(build_config.get("auth_mode"), dict):
|
|
1338
|
+
mode = build_config["auth_mode"].get("value")
|
|
1339
|
+
# If no managed default exists (400 Default auth config), require mode selection
|
|
1340
|
+
managed = (schema or {}).get("composio_managed_auth_schemes") or []
|
|
1341
|
+
if isinstance(managed, list) and "OAUTH2" in managed and (mode is None or mode == "OAUTH2"):
|
|
1342
|
+
redirect_url, connection_id = self._initiate_connection(toolkit_slug)
|
|
1343
|
+
build_config["auth_link"]["value"] = redirect_url
|
|
1344
|
+
logger.info(f"New OAuth URL created for {toolkit_slug}: {redirect_url}")
|
|
1345
|
+
return build_config
|
|
1346
|
+
if not mode:
|
|
1347
|
+
build_config["auth_link"]["value"] = "connect"
|
|
1348
|
+
build_config["auth_link"]["auth_tooltip"] = "Select Auth Mode"
|
|
1349
|
+
return build_config
|
|
1350
|
+
# Custom modes: create auth config and/or initiate with config
|
|
1351
|
+
# Validate required fields before creating any auth config
|
|
1352
|
+
required_missing = []
|
|
1353
|
+
if mode == "OAUTH2":
|
|
1354
|
+
req_names_pre = self._get_schema_field_names(
|
|
1355
|
+
schema,
|
|
1356
|
+
"OAUTH2",
|
|
1357
|
+
"auth_config_creation",
|
|
1358
|
+
"required",
|
|
1359
|
+
)
|
|
1360
|
+
for fname in req_names_pre:
|
|
1361
|
+
if fname in build_config:
|
|
1362
|
+
val = build_config[fname].get("value")
|
|
1363
|
+
if val in (None, ""):
|
|
1364
|
+
required_missing.append(fname)
|
|
1365
|
+
elif mode == "API_KEY":
|
|
1366
|
+
req_names_pre = self._get_schema_field_names(
|
|
1367
|
+
schema,
|
|
1368
|
+
"API_KEY",
|
|
1369
|
+
"connected_account_initiation",
|
|
1370
|
+
"required",
|
|
1371
|
+
)
|
|
1372
|
+
for fname in req_names_pre:
|
|
1373
|
+
if fname in build_config:
|
|
1374
|
+
val = build_config[fname].get("value")
|
|
1375
|
+
if val in (None, ""):
|
|
1376
|
+
required_missing.append(fname)
|
|
1377
|
+
else:
|
|
1378
|
+
req_names_pre = self._get_schema_field_names(
|
|
1379
|
+
schema,
|
|
1380
|
+
mode,
|
|
1381
|
+
"connected_account_initiation",
|
|
1382
|
+
"required",
|
|
1383
|
+
)
|
|
1384
|
+
for fname in req_names_pre:
|
|
1385
|
+
if fname in build_config:
|
|
1386
|
+
val = build_config[fname].get("value")
|
|
1387
|
+
if val in (None, ""):
|
|
1388
|
+
required_missing.append(fname)
|
|
1389
|
+
if required_missing:
|
|
1390
|
+
# Surface errors on each missing field
|
|
1391
|
+
for fname in required_missing:
|
|
1392
|
+
if fname in build_config and isinstance(build_config[fname], dict):
|
|
1393
|
+
build_config[fname]["helper_text"] = "This field is required"
|
|
1394
|
+
build_config[fname]["helper_text_metadata"] = {"variant": "destructive"}
|
|
1395
|
+
# Also reflect in info for guaranteed visibility
|
|
1396
|
+
existing_info = build_config[fname].get("info") or ""
|
|
1397
|
+
build_config[fname]["info"] = f"Required: {existing_info}".strip()
|
|
1398
|
+
build_config[fname]["show"] = True
|
|
1399
|
+
# Add a visible top-level hint near Auth Mode as well
|
|
1400
|
+
build_config.setdefault("auth_mode", {})
|
|
1401
|
+
missing_joined = ", ".join(required_missing)
|
|
1402
|
+
build_config["auth_mode"]["helper_text"] = f"Missing required: {missing_joined}"
|
|
1403
|
+
build_config["auth_mode"]["helper_text_metadata"] = {"variant": "destructive"}
|
|
1404
|
+
build_config["auth_link"]["value"] = "connect"
|
|
1405
|
+
build_config["auth_link"]["auth_tooltip"] = f"Missing: {missing_joined}"
|
|
1406
|
+
return build_config
|
|
1407
|
+
composio = self._build_wrapper()
|
|
1408
|
+
if mode == "OAUTH2":
|
|
1409
|
+
# If an auth_config was already created via the button, use it and include initiation fields
|
|
1410
|
+
stored_ac_id = (build_config.get("auth_link") or {}).get("auth_config_id")
|
|
1411
|
+
if stored_ac_id:
|
|
1412
|
+
# Build val from schema-declared connected_account_initiation required + rendered fields
|
|
1413
|
+
val_payload = {}
|
|
1414
|
+
init_req = self._get_schema_field_names(
|
|
1415
|
+
schema,
|
|
1416
|
+
"OAUTH2",
|
|
1417
|
+
"connected_account_initiation",
|
|
1418
|
+
"required",
|
|
1419
|
+
)
|
|
1420
|
+
candidate_names = set(self._auth_dynamic_fields) | init_req
|
|
1421
|
+
for fname in candidate_names:
|
|
1422
|
+
if fname in build_config:
|
|
1423
|
+
v = build_config[fname].get("value")
|
|
1424
|
+
if v not in (None, ""):
|
|
1425
|
+
val_payload[fname] = v
|
|
1426
|
+
redirect = composio.connected_accounts.initiate(
|
|
1427
|
+
user_id=self.entity_id,
|
|
1428
|
+
auth_config_id=stored_ac_id,
|
|
1429
|
+
config={"auth_scheme": "OAUTH2", "val": val_payload} if val_payload else None,
|
|
1430
|
+
)
|
|
1431
|
+
redirect_url = getattr(redirect, "redirect_url", None)
|
|
1432
|
+
connection_id = getattr(redirect, "id", None)
|
|
962
1433
|
if redirect_url:
|
|
963
1434
|
build_config["auth_link"]["value"] = redirect_url
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1435
|
+
if connection_id:
|
|
1436
|
+
build_config["auth_link"]["connection_id"] = connection_id
|
|
1437
|
+
# Clear action blocker text on successful initiation
|
|
1438
|
+
build_config["action_button"]["helper_text"] = ""
|
|
1439
|
+
build_config["action_button"]["helper_text_metadata"] = {}
|
|
1440
|
+
return build_config
|
|
1441
|
+
# Otherwise, create custom OAuth2 auth config using schema-declared required fields
|
|
1442
|
+
credentials = {}
|
|
1443
|
+
missing = []
|
|
1444
|
+
# Collect required names from schema
|
|
1445
|
+
req_names = self._get_schema_field_names(
|
|
1446
|
+
schema,
|
|
1447
|
+
"OAUTH2",
|
|
1448
|
+
"auth_config_creation",
|
|
1449
|
+
"required",
|
|
1450
|
+
)
|
|
1451
|
+
candidate_names = set(self._auth_dynamic_fields) | req_names
|
|
1452
|
+
for fname in candidate_names:
|
|
1453
|
+
if fname in build_config:
|
|
1454
|
+
val = build_config[fname].get("value")
|
|
1455
|
+
if val not in (None, ""):
|
|
1456
|
+
credentials[fname] = val
|
|
1457
|
+
else:
|
|
1458
|
+
missing.append(fname)
|
|
1459
|
+
# proceed even if missing optional; backend will validate
|
|
1460
|
+
ac = composio.auth_configs.create(
|
|
1461
|
+
toolkit=toolkit_slug,
|
|
1462
|
+
options={
|
|
1463
|
+
"type": "use_custom_auth",
|
|
1464
|
+
"auth_scheme": "OAUTH2",
|
|
1465
|
+
"credentials": credentials,
|
|
1466
|
+
},
|
|
1467
|
+
)
|
|
1468
|
+
auth_config_id = getattr(ac, "id", None)
|
|
1469
|
+
# If the schema declares initiation required fields, render them and defer initiation
|
|
1470
|
+
init_req = self._get_schema_field_names(
|
|
1471
|
+
schema,
|
|
1472
|
+
"OAUTH2",
|
|
1473
|
+
"connected_account_initiation",
|
|
1474
|
+
"required",
|
|
1475
|
+
)
|
|
1476
|
+
if init_req:
|
|
1477
|
+
self._clear_auth_dynamic_fields(build_config)
|
|
1478
|
+
for name in init_req:
|
|
1479
|
+
self._add_text_field(
|
|
1480
|
+
build_config,
|
|
1481
|
+
name=name,
|
|
1482
|
+
display_name=name.replace("_", " ").title(),
|
|
1483
|
+
info="Provide connection parameter",
|
|
1484
|
+
required=True,
|
|
1485
|
+
)
|
|
1486
|
+
build_config.setdefault("auth_link", {})
|
|
1487
|
+
build_config["auth_link"]["auth_config_id"] = auth_config_id
|
|
1488
|
+
build_config["auth_link"]["value"] = "connect"
|
|
1489
|
+
build_config["auth_link"]["auth_tooltip"] = "Connect"
|
|
1490
|
+
return build_config
|
|
1491
|
+
# Otherwise initiate immediately
|
|
1492
|
+
redirect = composio.connected_accounts.initiate(
|
|
1493
|
+
user_id=self.entity_id,
|
|
1494
|
+
auth_config_id=auth_config_id,
|
|
1495
|
+
)
|
|
1496
|
+
redirect_url = getattr(redirect, "redirect_url", None)
|
|
1497
|
+
connection_id = getattr(redirect, "id", None)
|
|
1498
|
+
if redirect_url:
|
|
1499
|
+
build_config["auth_link"]["value"] = redirect_url
|
|
1500
|
+
if connection_id:
|
|
1501
|
+
build_config["auth_link"]["connection_id"] = connection_id
|
|
1502
|
+
# Hide auth fields immediately after successful initiation
|
|
1503
|
+
schema = self._get_toolkit_schema()
|
|
1504
|
+
self._clear_auth_fields_from_schema(build_config, schema)
|
|
1505
|
+
build_config["action_button"]["helper_text"] = ""
|
|
1506
|
+
build_config["action_button"]["helper_text_metadata"] = {}
|
|
1507
|
+
return build_config
|
|
1508
|
+
if mode == "API_KEY":
|
|
1509
|
+
ac = composio.auth_configs.create(
|
|
1510
|
+
toolkit=toolkit_slug,
|
|
1511
|
+
options={"type": "use_custom_auth", "auth_scheme": "API_KEY", "credentials": {}},
|
|
1512
|
+
)
|
|
1513
|
+
auth_config_id = getattr(ac, "id", None)
|
|
1514
|
+
# Build initiation config.val from schema-declared required names and dynamic fields
|
|
1515
|
+
val_payload = {}
|
|
1516
|
+
missing = []
|
|
1517
|
+
# Collect required names from schema
|
|
1518
|
+
req_names = self._get_schema_field_names(
|
|
1519
|
+
schema,
|
|
1520
|
+
"API_KEY",
|
|
1521
|
+
"connected_account_initiation",
|
|
1522
|
+
"required",
|
|
1523
|
+
)
|
|
1524
|
+
# Merge rendered dynamic fields and schema-required names
|
|
1525
|
+
candidate_names = set(self._auth_dynamic_fields) | req_names
|
|
1526
|
+
for fname in candidate_names:
|
|
1527
|
+
if fname in build_config:
|
|
1528
|
+
val = build_config[fname].get("value")
|
|
1529
|
+
if val not in (None, ""):
|
|
1530
|
+
val_payload[fname] = val
|
|
1531
|
+
else:
|
|
1532
|
+
missing.append(fname)
|
|
1533
|
+
initiation = composio.connected_accounts.initiate(
|
|
1534
|
+
user_id=self.entity_id,
|
|
1535
|
+
auth_config_id=auth_config_id,
|
|
1536
|
+
config={"auth_scheme": "API_KEY", "val": val_payload},
|
|
1537
|
+
)
|
|
1538
|
+
connection_id = getattr(initiation, "id", None)
|
|
1539
|
+
redirect_url = getattr(initiation, "redirect_url", None)
|
|
1540
|
+
# Do not store connection_id on initiation; only when ACTIVE
|
|
1541
|
+
if redirect_url:
|
|
1542
|
+
build_config["auth_link"]["value"] = redirect_url
|
|
1543
|
+
build_config["auth_link"]["auth_tooltip"] = "Disconnect"
|
|
1544
|
+
else:
|
|
1545
|
+
# No redirect for API_KEY; mark as connected
|
|
1546
|
+
build_config["auth_link"]["value"] = "validated"
|
|
1547
|
+
build_config["auth_link"]["auth_tooltip"] = "Disconnect"
|
|
1548
|
+
# In both cases, hide auth fields immediately after successful initiation
|
|
1549
|
+
schema = self._get_toolkit_schema()
|
|
1550
|
+
self._clear_auth_fields_from_schema(build_config, schema)
|
|
1551
|
+
build_config["action_button"]["helper_text"] = ""
|
|
1552
|
+
build_config["action_button"]["helper_text_metadata"] = {}
|
|
1553
|
+
return build_config
|
|
1554
|
+
# Generic custom auth flow for any other mode (treat like API_KEY)
|
|
1555
|
+
ac = composio.auth_configs.create(
|
|
1556
|
+
toolkit=toolkit_slug,
|
|
1557
|
+
options={"type": "use_custom_auth", "auth_scheme": mode, "credentials": {}},
|
|
1558
|
+
)
|
|
1559
|
+
auth_config_id = getattr(ac, "id", None)
|
|
1560
|
+
val_payload = {}
|
|
1561
|
+
req_names = self._get_schema_field_names(
|
|
1562
|
+
schema,
|
|
1563
|
+
mode,
|
|
1564
|
+
"connected_account_initiation",
|
|
1565
|
+
"required",
|
|
1566
|
+
)
|
|
1567
|
+
candidate_names = set(self._auth_dynamic_fields) | req_names
|
|
1568
|
+
for fname in candidate_names:
|
|
1569
|
+
if fname in build_config:
|
|
1570
|
+
val = build_config[fname].get("value")
|
|
1571
|
+
if val not in (None, ""):
|
|
1572
|
+
val_payload[fname] = val
|
|
1573
|
+
initiation = composio.connected_accounts.initiate(
|
|
1574
|
+
user_id=self.entity_id,
|
|
1575
|
+
auth_config_id=auth_config_id,
|
|
1576
|
+
config={"auth_scheme": mode, "val": val_payload},
|
|
1577
|
+
)
|
|
1578
|
+
connection_id = getattr(initiation, "id", None)
|
|
1579
|
+
redirect_url = getattr(initiation, "redirect_url", None)
|
|
1580
|
+
# Do not store connection_id on initiation; only when ACTIVE
|
|
1581
|
+
if redirect_url:
|
|
1582
|
+
build_config["auth_link"]["value"] = redirect_url
|
|
1583
|
+
build_config["auth_link"]["auth_tooltip"] = "Disconnect"
|
|
1584
|
+
else:
|
|
1585
|
+
build_config["auth_link"]["value"] = "validated"
|
|
1586
|
+
build_config["auth_link"]["auth_tooltip"] = "Disconnect"
|
|
1587
|
+
return build_config
|
|
1588
|
+
except (ValueError, ConnectionError, TypeError) as e:
|
|
1589
|
+
logger.error(f"Error creating connection: {e}")
|
|
979
1590
|
build_config["auth_link"]["value"] = "connect"
|
|
980
1591
|
build_config["auth_link"]["auth_tooltip"] = f"Error: {e!s}"
|
|
981
1592
|
else:
|
|
@@ -1017,6 +1628,41 @@ class ComposioBaseComponent(Component):
|
|
|
1017
1628
|
# Show validated connection status
|
|
1018
1629
|
build_config["auth_link"]["value"] = "validated"
|
|
1019
1630
|
build_config["auth_link"]["auth_tooltip"] = "Disconnect"
|
|
1631
|
+
build_config["auth_link"]["show"] = False
|
|
1632
|
+
# Update auth mode UI to reflect connected scheme
|
|
1633
|
+
scheme, is_managed = self._get_connection_auth_info(active_connection_id)
|
|
1634
|
+
if scheme:
|
|
1635
|
+
build_config.setdefault("auth_link", {})
|
|
1636
|
+
build_config["auth_link"]["auth_scheme"] = scheme
|
|
1637
|
+
build_config.setdefault("auth_mode", {})
|
|
1638
|
+
build_config["auth_mode"]["value"] = scheme
|
|
1639
|
+
build_config["auth_mode"]["options"] = [scheme]
|
|
1640
|
+
build_config["auth_mode"]["show"] = False
|
|
1641
|
+
try:
|
|
1642
|
+
pill = TabInput(
|
|
1643
|
+
name="auth_mode",
|
|
1644
|
+
display_name="Auth Mode",
|
|
1645
|
+
options=[scheme],
|
|
1646
|
+
value=scheme,
|
|
1647
|
+
).to_dict()
|
|
1648
|
+
pill["show"] = True
|
|
1649
|
+
build_config["auth_mode"] = pill
|
|
1650
|
+
except (TypeError, ValueError, AttributeError):
|
|
1651
|
+
build_config["auth_mode"] = {
|
|
1652
|
+
"name": "auth_mode",
|
|
1653
|
+
"display_name": "Auth Mode",
|
|
1654
|
+
"type": "tab",
|
|
1655
|
+
"options": [scheme],
|
|
1656
|
+
"value": scheme,
|
|
1657
|
+
"show": True,
|
|
1658
|
+
}
|
|
1659
|
+
build_config["action_button"]["helper_text"] = ""
|
|
1660
|
+
build_config["action_button"]["helper_text_metadata"] = {}
|
|
1661
|
+
# Clear any auth fields since we are already connected
|
|
1662
|
+
schema = self._get_toolkit_schema()
|
|
1663
|
+
self._clear_auth_fields_from_schema(build_config, schema)
|
|
1664
|
+
build_config.setdefault("create_auth_config", {})
|
|
1665
|
+
build_config["create_auth_config"]["show"] = False
|
|
1020
1666
|
build_config["action_button"]["helper_text"] = ""
|
|
1021
1667
|
build_config["action_button"]["helper_text_metadata"] = {}
|
|
1022
1668
|
else:
|
|
@@ -1025,20 +1671,30 @@ class ComposioBaseComponent(Component):
|
|
|
1025
1671
|
build_config["action_button"]["helper_text"] = "Please connect before selecting actions."
|
|
1026
1672
|
build_config["action_button"]["helper_text_metadata"] = {"variant": "destructive"}
|
|
1027
1673
|
|
|
1028
|
-
# CRITICAL: If tool_mode is enabled from ANY source,
|
|
1674
|
+
# CRITICAL: If tool_mode is enabled from ANY source, hide action UI but keep auth flow available
|
|
1029
1675
|
if current_tool_mode:
|
|
1030
1676
|
build_config["action_button"]["show"] = False
|
|
1031
1677
|
|
|
1032
|
-
#
|
|
1678
|
+
# Hide ALL action parameter fields when tool mode is enabled
|
|
1033
1679
|
for field in self._all_fields:
|
|
1034
1680
|
if field in build_config:
|
|
1035
1681
|
build_config[field]["show"] = False
|
|
1036
1682
|
|
|
1037
1683
|
# Also hide any other action-related fields that might be in build_config
|
|
1038
1684
|
for field_name_in_config in build_config: # noqa: PLC0206
|
|
1039
|
-
# Skip base fields like api_key, tool_mode, action, etc
|
|
1685
|
+
# Skip base fields like api_key, tool_mode, action, etc., and dynamic auth fields
|
|
1040
1686
|
if (
|
|
1041
|
-
field_name_in_config
|
|
1687
|
+
field_name_in_config
|
|
1688
|
+
not in [
|
|
1689
|
+
"api_key",
|
|
1690
|
+
"tool_mode",
|
|
1691
|
+
"action_button",
|
|
1692
|
+
"auth_link",
|
|
1693
|
+
"entity_id",
|
|
1694
|
+
"auth_mode",
|
|
1695
|
+
"auth_mode_pill",
|
|
1696
|
+
]
|
|
1697
|
+
and field_name_in_config not in getattr(self, "_auth_dynamic_fields", set())
|
|
1042
1698
|
and isinstance(build_config[field_name_in_config], dict)
|
|
1043
1699
|
and "show" in build_config[field_name_in_config]
|
|
1044
1700
|
):
|
|
@@ -1049,8 +1705,23 @@ class ComposioBaseComponent(Component):
|
|
|
1049
1705
|
build_config["tool_mode"] = {"value": True}
|
|
1050
1706
|
elif isinstance(build_config["tool_mode"], dict):
|
|
1051
1707
|
build_config["tool_mode"]["value"] = True
|
|
1052
|
-
#
|
|
1053
|
-
|
|
1708
|
+
# Keep auth UI available and render fields if needed
|
|
1709
|
+
build_config.setdefault("auth_link", {})
|
|
1710
|
+
build_config["auth_link"]["show"] = False
|
|
1711
|
+
build_config["auth_link"]["display_name"] = ""
|
|
1712
|
+
try:
|
|
1713
|
+
schema = self._get_toolkit_schema()
|
|
1714
|
+
mode = (build_config.get("auth_mode") or {}).get("value")
|
|
1715
|
+
managed = (schema or {}).get("composio_managed_auth_schemes") or []
|
|
1716
|
+
if (
|
|
1717
|
+
mode
|
|
1718
|
+
and not (isinstance(managed, list) and mode in managed)
|
|
1719
|
+
and not getattr(self, "_auth_dynamic_fields", set())
|
|
1720
|
+
):
|
|
1721
|
+
self._render_custom_auth_fields(build_config, schema or {}, mode)
|
|
1722
|
+
except (TypeError, ValueError, AttributeError):
|
|
1723
|
+
pass
|
|
1724
|
+
# Do NOT return here; allow auth flow to run in Tool Mode
|
|
1054
1725
|
|
|
1055
1726
|
if field_name == "tool_mode":
|
|
1056
1727
|
if field_value is True:
|
|
@@ -1064,11 +1735,97 @@ class ComposioBaseComponent(Component):
|
|
|
1064
1735
|
return build_config
|
|
1065
1736
|
|
|
1066
1737
|
if field_name == "action_button":
|
|
1738
|
+
# If selection is cancelled/cleared, remove generated fields
|
|
1739
|
+
def _is_cleared(val: Any) -> bool:
|
|
1740
|
+
return (
|
|
1741
|
+
not val
|
|
1742
|
+
or (
|
|
1743
|
+
isinstance(val, list)
|
|
1744
|
+
and (len(val) == 0 or (len(val) > 0 and isinstance(val[0], dict) and not val[0].get("name")))
|
|
1745
|
+
)
|
|
1746
|
+
or (isinstance(val, str) and val in ("", "disabled", "placeholder"))
|
|
1747
|
+
)
|
|
1748
|
+
|
|
1749
|
+
if _is_cleared(field_value):
|
|
1750
|
+
self._hide_all_action_fields(build_config)
|
|
1751
|
+
return build_config
|
|
1752
|
+
|
|
1067
1753
|
self._update_action_config(build_config, field_value)
|
|
1068
1754
|
# Keep the existing show/hide behaviour
|
|
1069
1755
|
self.show_hide_fields(build_config, field_value)
|
|
1070
1756
|
return build_config
|
|
1071
1757
|
|
|
1758
|
+
# Handle auth config button click
|
|
1759
|
+
if field_name == "create_auth_config" and field_value == "create":
|
|
1760
|
+
try:
|
|
1761
|
+
composio = self._build_wrapper()
|
|
1762
|
+
toolkit_slug = self.app_name.lower()
|
|
1763
|
+
schema = self._get_toolkit_schema() or {}
|
|
1764
|
+
# Collect required fields from the current build_config
|
|
1765
|
+
credentials = {}
|
|
1766
|
+
req_names = self._get_schema_field_names(schema, "OAUTH2", "auth_config_creation", "required")
|
|
1767
|
+
candidate_names = set(self._auth_dynamic_fields) | req_names
|
|
1768
|
+
for fname in candidate_names:
|
|
1769
|
+
if fname in build_config:
|
|
1770
|
+
val = build_config[fname].get("value")
|
|
1771
|
+
if val not in (None, ""):
|
|
1772
|
+
credentials[fname] = val
|
|
1773
|
+
# Create a new auth config using the collected credentials
|
|
1774
|
+
ac = composio.auth_configs.create(
|
|
1775
|
+
toolkit=toolkit_slug,
|
|
1776
|
+
options={"type": "use_custom_auth", "auth_scheme": "OAUTH2", "credentials": credentials},
|
|
1777
|
+
)
|
|
1778
|
+
auth_config_id = getattr(ac, "id", None)
|
|
1779
|
+
build_config.setdefault("auth_link", {})
|
|
1780
|
+
if auth_config_id:
|
|
1781
|
+
# Check if there are connection initiation required fields
|
|
1782
|
+
initiation_required = self._get_schema_field_names(
|
|
1783
|
+
schema, "OAUTH2", "connected_account_initiation", "required"
|
|
1784
|
+
)
|
|
1785
|
+
if initiation_required:
|
|
1786
|
+
# Populate those fields dynamically for the user to fill
|
|
1787
|
+
self._clear_auth_dynamic_fields(build_config)
|
|
1788
|
+
for name in initiation_required:
|
|
1789
|
+
# Render as text inputs to collect connection fields
|
|
1790
|
+
self._add_text_field(
|
|
1791
|
+
build_config,
|
|
1792
|
+
name=name,
|
|
1793
|
+
display_name=name.replace("_", " ").title(),
|
|
1794
|
+
info="Provide connection parameter",
|
|
1795
|
+
required=True,
|
|
1796
|
+
)
|
|
1797
|
+
# Store the new auth_config_id so pressing Connect will use it
|
|
1798
|
+
build_config["auth_link"]["auth_config_id"] = auth_config_id
|
|
1799
|
+
build_config["auth_link"]["value"] = "connect"
|
|
1800
|
+
build_config["auth_link"]["auth_tooltip"] = "Connect"
|
|
1801
|
+
return build_config
|
|
1802
|
+
# If no initiation fields required, initiate immediately
|
|
1803
|
+
connection_request = composio.connected_accounts.initiate(
|
|
1804
|
+
user_id=self.entity_id, auth_config_id=auth_config_id
|
|
1805
|
+
)
|
|
1806
|
+
redirect_url = getattr(connection_request, "redirect_url", None)
|
|
1807
|
+
connection_id = getattr(connection_request, "id", None)
|
|
1808
|
+
if redirect_url and redirect_url.startswith(("http://", "https://")):
|
|
1809
|
+
build_config["auth_link"]["value"] = redirect_url
|
|
1810
|
+
build_config["auth_link"]["auth_tooltip"] = "Disconnect"
|
|
1811
|
+
build_config["auth_link"]["connection_id"] = connection_id
|
|
1812
|
+
build_config["action_button"]["helper_text"] = ""
|
|
1813
|
+
build_config["action_button"]["helper_text_metadata"] = {}
|
|
1814
|
+
logger.info(f"New OAuth URL created for {toolkit_slug}: {redirect_url}")
|
|
1815
|
+
else:
|
|
1816
|
+
logger.error(f"Failed to initiate connection with new auth config: {redirect_url}")
|
|
1817
|
+
build_config["auth_link"]["value"] = "error"
|
|
1818
|
+
build_config["auth_link"]["auth_tooltip"] = f"Error: {redirect_url}"
|
|
1819
|
+
else:
|
|
1820
|
+
logger.error(f"Failed to create new auth config for {toolkit_slug}")
|
|
1821
|
+
build_config["auth_link"]["value"] = "error"
|
|
1822
|
+
build_config["auth_link"]["auth_tooltip"] = "Create Auth Config failed"
|
|
1823
|
+
except (ValueError, ConnectionError, TypeError) as e:
|
|
1824
|
+
logger.error(f"Error creating new auth config: {e}")
|
|
1825
|
+
build_config["auth_link"]["value"] = "error"
|
|
1826
|
+
build_config["auth_link"]["auth_tooltip"] = f"Error: {e!s}"
|
|
1827
|
+
return build_config
|
|
1828
|
+
|
|
1072
1829
|
# Handle API key removal
|
|
1073
1830
|
if field_name == "api_key" and len(field_value) == 0:
|
|
1074
1831
|
build_config["auth_link"]["value"] = ""
|
|
@@ -1076,7 +1833,34 @@ class ComposioBaseComponent(Component):
|
|
|
1076
1833
|
build_config["action_button"]["options"] = []
|
|
1077
1834
|
build_config["action_button"]["helper_text"] = "Please connect before selecting actions."
|
|
1078
1835
|
build_config["action_button"]["helper_text_metadata"] = {"variant": "destructive"}
|
|
1836
|
+
build_config.setdefault("auth_link", {})
|
|
1079
1837
|
build_config["auth_link"].pop("connection_id", None)
|
|
1838
|
+
build_config["auth_link"].pop("auth_scheme", None)
|
|
1839
|
+
# Restore auth_mode dropdown and hide pill
|
|
1840
|
+
try:
|
|
1841
|
+
dd = DropdownInput(
|
|
1842
|
+
name="auth_mode",
|
|
1843
|
+
display_name="Auth Mode",
|
|
1844
|
+
options=[],
|
|
1845
|
+
placeholder="Select auth mode",
|
|
1846
|
+
toggle=True,
|
|
1847
|
+
toggle_disable=True,
|
|
1848
|
+
show=True,
|
|
1849
|
+
real_time_refresh=True,
|
|
1850
|
+
helper_text="Choose how to authenticate with the toolkit.",
|
|
1851
|
+
).to_dict()
|
|
1852
|
+
build_config["auth_mode"] = dd
|
|
1853
|
+
except (TypeError, ValueError, AttributeError):
|
|
1854
|
+
build_config.setdefault("auth_mode", {})
|
|
1855
|
+
build_config["auth_mode"]["show"] = True
|
|
1856
|
+
build_config["auth_mode"].pop("value", None)
|
|
1857
|
+
# NEW: Clear any selected action and hide generated fields when API key is cleared
|
|
1858
|
+
try:
|
|
1859
|
+
if "action_button" in build_config and isinstance(build_config["action_button"], dict):
|
|
1860
|
+
build_config["action_button"]["value"] = "disabled"
|
|
1861
|
+
self._hide_all_action_fields(build_config)
|
|
1862
|
+
except (TypeError, ValueError, AttributeError):
|
|
1863
|
+
pass
|
|
1080
1864
|
return build_config
|
|
1081
1865
|
|
|
1082
1866
|
# Only proceed with connection logic if we have an API key
|
|
@@ -1166,7 +1950,7 @@ class ComposioBaseComponent(Component):
|
|
|
1166
1950
|
configured_tools.append(tool)
|
|
1167
1951
|
return configured_tools
|
|
1168
1952
|
|
|
1169
|
-
|
|
1953
|
+
def _get_tools(self) -> list[Tool]:
|
|
1170
1954
|
"""Get tools with cached results and optimized name sanitization."""
|
|
1171
1955
|
composio = self._build_wrapper()
|
|
1172
1956
|
self.set_default_tools()
|
|
@@ -1228,22 +2012,26 @@ class ComposioBaseComponent(Component):
|
|
|
1228
2012
|
if value is None or value == "" or (isinstance(value, list) and len(value) == 0):
|
|
1229
2013
|
continue
|
|
1230
2014
|
|
|
2015
|
+
# Determine schema for this field
|
|
2016
|
+
prop_schema = schema_properties.get(field, {})
|
|
2017
|
+
|
|
2018
|
+
# Parse JSON for object/array string inputs (applies to required and optional)
|
|
2019
|
+
if isinstance(value, str) and prop_schema.get("type") in {"array", "object"}:
|
|
2020
|
+
try:
|
|
2021
|
+
value = json.loads(value)
|
|
2022
|
+
except json.JSONDecodeError:
|
|
2023
|
+
# Fallback for simple arrays of primitives
|
|
2024
|
+
if prop_schema.get("type") == "array":
|
|
2025
|
+
value = [item.strip() for item in value.split(",") if item.strip() != ""]
|
|
2026
|
+
|
|
1231
2027
|
# For optional fields, be more strict about including them
|
|
1232
2028
|
# Only include if the user has explicitly provided a meaningful value
|
|
1233
2029
|
if field not in required_fields:
|
|
1234
|
-
#
|
|
1235
|
-
|
|
1236
|
-
schema_default = field_schema.get("default")
|
|
1237
|
-
|
|
1238
|
-
# Skip if the current value matches the schema default
|
|
2030
|
+
# Compare against schema default after normalization
|
|
2031
|
+
schema_default = prop_schema.get("default")
|
|
1239
2032
|
if value == schema_default:
|
|
1240
2033
|
continue
|
|
1241
2034
|
|
|
1242
|
-
# Convert comma-separated to list for array parameters (heuristic)
|
|
1243
|
-
prop_schema = schema_properties.get(field, {})
|
|
1244
|
-
if prop_schema.get("type") == "array" and isinstance(value, str):
|
|
1245
|
-
value = [item.strip() for item in value.split(",")]
|
|
1246
|
-
|
|
1247
2035
|
if field in self._bool_variables:
|
|
1248
2036
|
value = bool(value)
|
|
1249
2037
|
|
|
@@ -1251,8 +2039,6 @@ class ComposioBaseComponent(Component):
|
|
|
1251
2039
|
final_field_name = field
|
|
1252
2040
|
if field.endswith("_user_id") and field.startswith(self.app_name):
|
|
1253
2041
|
final_field_name = "user_id"
|
|
1254
|
-
elif field.endswith("_status") and field.startswith(self.app_name):
|
|
1255
|
-
final_field_name = "status"
|
|
1256
2042
|
|
|
1257
2043
|
arguments[final_field_name] = value
|
|
1258
2044
|
|
|
@@ -1289,3 +2075,83 @@ class ComposioBaseComponent(Component):
|
|
|
1289
2075
|
|
|
1290
2076
|
def set_default_tools(self):
|
|
1291
2077
|
"""Set the default tools."""
|
|
2078
|
+
|
|
2079
|
+
def _get_schema_field_names(
|
|
2080
|
+
self,
|
|
2081
|
+
schema: dict[str, Any] | None,
|
|
2082
|
+
mode: str,
|
|
2083
|
+
section_kind: str,
|
|
2084
|
+
bucket: str,
|
|
2085
|
+
) -> set[str]:
|
|
2086
|
+
names: set[str] = set()
|
|
2087
|
+
if not schema:
|
|
2088
|
+
return names
|
|
2089
|
+
details = schema.get("auth_config_details") or schema.get("authConfigDetails") or []
|
|
2090
|
+
for item in details:
|
|
2091
|
+
if (item.get("mode") or item.get("auth_method")) != mode:
|
|
2092
|
+
continue
|
|
2093
|
+
fields = item.get("fields") or {}
|
|
2094
|
+
section = (
|
|
2095
|
+
fields.get(section_kind)
|
|
2096
|
+
or fields.get(
|
|
2097
|
+
"authConfigCreation" if section_kind == "auth_config_creation" else "connectedAccountInitiation"
|
|
2098
|
+
)
|
|
2099
|
+
or {}
|
|
2100
|
+
)
|
|
2101
|
+
for entry in section.get(bucket, []) or []:
|
|
2102
|
+
name = entry.get("name") if isinstance(entry, dict) else None
|
|
2103
|
+
if name:
|
|
2104
|
+
names.add(name)
|
|
2105
|
+
return names
|
|
2106
|
+
|
|
2107
|
+
def _get_schema_required_entries(
|
|
2108
|
+
self,
|
|
2109
|
+
schema: dict[str, Any] | None,
|
|
2110
|
+
mode: str,
|
|
2111
|
+
section_kind: str,
|
|
2112
|
+
) -> list[dict[str, Any]]:
|
|
2113
|
+
if not schema:
|
|
2114
|
+
return []
|
|
2115
|
+
details = schema.get("auth_config_details") or schema.get("authConfigDetails") or []
|
|
2116
|
+
for item in details:
|
|
2117
|
+
if (item.get("mode") or item.get("auth_method")) != mode:
|
|
2118
|
+
continue
|
|
2119
|
+
fields = item.get("fields") or {}
|
|
2120
|
+
section = (
|
|
2121
|
+
fields.get(section_kind)
|
|
2122
|
+
or fields.get(
|
|
2123
|
+
"authConfigCreation" if section_kind == "auth_config_creation" else "connectedAccountInitiation"
|
|
2124
|
+
)
|
|
2125
|
+
or {}
|
|
2126
|
+
)
|
|
2127
|
+
req = section.get("required", []) or []
|
|
2128
|
+
# Normalize dict-like entries
|
|
2129
|
+
return [entry for entry in req if isinstance(entry, dict)]
|
|
2130
|
+
return []
|
|
2131
|
+
|
|
2132
|
+
def _hide_all_action_fields(self, build_config: dict) -> None:
|
|
2133
|
+
"""Hide and reset all action parameter inputs, regardless of trace flags."""
|
|
2134
|
+
# Hide known action fields
|
|
2135
|
+
for fname in list(self._all_fields):
|
|
2136
|
+
if fname in build_config and isinstance(build_config[fname], dict):
|
|
2137
|
+
build_config[fname]["show"] = False
|
|
2138
|
+
build_config[fname]["value"] = "" if fname not in self._bool_variables else False
|
|
2139
|
+
# Hide any other visible, non-protected fields that look like parameters
|
|
2140
|
+
protected = {
|
|
2141
|
+
"code",
|
|
2142
|
+
"entity_id",
|
|
2143
|
+
"api_key",
|
|
2144
|
+
"auth_link",
|
|
2145
|
+
"action_button",
|
|
2146
|
+
"tool_mode",
|
|
2147
|
+
"auth_mode",
|
|
2148
|
+
"auth_mode_pill",
|
|
2149
|
+
"create_auth_config",
|
|
2150
|
+
}
|
|
2151
|
+
for key, cfg in list(build_config.items()):
|
|
2152
|
+
if key in protected:
|
|
2153
|
+
continue
|
|
2154
|
+
if isinstance(cfg, dict) and "show" in cfg:
|
|
2155
|
+
cfg["show"] = False
|
|
2156
|
+
if "value" in cfg:
|
|
2157
|
+
cfg["value"] = ""
|