mirror of
https://github.com/langflow-ai/langflow.git
synced 2026-07-24 07:34:10 +08:00
feat: replaced initiate method with link method. (#10580)
feat: replaced .initiate() with .link() Co-authored-by: Edwin Jose <edwin.jose@datastax.com>
This commit is contained in:
@ -135,7 +135,7 @@ class ComposioBaseComponent(Component):
|
||||
SecretStrInput(
|
||||
name="generic_api_key",
|
||||
display_name="API Key",
|
||||
info="",
|
||||
info="Enter API key on Composio page",
|
||||
show=False,
|
||||
value="",
|
||||
required=False,
|
||||
@ -1048,24 +1048,14 @@ class ComposioBaseComponent(Component):
|
||||
return auth_config.id
|
||||
|
||||
def _initiate_connection(self, app_name: str) -> tuple[str, str]:
|
||||
"""Initiate OAuth connection and return (redirect_url, connection_id)."""
|
||||
"""Initiate connection using link method and return (redirect_url, connection_id)."""
|
||||
try:
|
||||
composio = self._build_wrapper()
|
||||
|
||||
auth_configs = composio.auth_configs.list(toolkit_slug=app_name)
|
||||
if len(auth_configs.items) == 0:
|
||||
auth_config_id = self.create_new_auth_config(app_name)
|
||||
else:
|
||||
auth_config_id = None
|
||||
for auth_config in auth_configs.items:
|
||||
if auth_config.auth_scheme == "OAUTH2":
|
||||
auth_config_id = auth_config.id
|
||||
# Always create a new auth config (previous behavior)
|
||||
auth_config_id = self.create_new_auth_config(app_name)
|
||||
|
||||
auth_config_id = auth_configs.items[0].id
|
||||
|
||||
connection_request = composio.connected_accounts.initiate(
|
||||
user_id=self.entity_id, auth_config_id=auth_config_id
|
||||
)
|
||||
connection_request = composio.connected_accounts.link(user_id=self.entity_id, auth_config_id=auth_config_id)
|
||||
|
||||
redirect_url = getattr(connection_request, "redirect_url", None)
|
||||
connection_id = getattr(connection_request, "id", None)
|
||||
@ -1078,12 +1068,12 @@ class ComposioBaseComponent(Component):
|
||||
msg = "No connection ID received from Composio"
|
||||
raise ValueError(msg)
|
||||
|
||||
logger.info(f"OAuth connection initiated for {app_name}: {redirect_url} (ID: {connection_id})")
|
||||
logger.info(f"Connection initiated for {app_name}: {redirect_url} (ID: {connection_id})")
|
||||
return redirect_url, connection_id # noqa: TRY300
|
||||
|
||||
except (ValueError, ConnectionError, TypeError, AttributeError) as e:
|
||||
logger.error(f"Error initiating connection for {app_name}: {e}")
|
||||
msg = f"Failed to initiate OAuth connection: {e}"
|
||||
msg = f"Failed to initiate connection: {e}"
|
||||
raise ValueError(msg) from e
|
||||
|
||||
def _check_connection_status_by_id(self, connection_id: str) -> str | None:
|
||||
@ -1400,20 +1390,14 @@ class ComposioBaseComponent(Component):
|
||||
desc = field.get("description")
|
||||
self._add_text_field(build_config, name, disp, desc, required=required, default_value=default_val)
|
||||
|
||||
# a) AuthConfigCreation fields (for custom OAuth2, etc.)
|
||||
# Only process AuthConfigCreation fields (for custom OAuth2, etc.)
|
||||
# Connection initiation fields are now handled on Composio page via link method
|
||||
creation = fields.get("auth_config_creation") or fields.get("authConfigCreation") or {}
|
||||
# Process required fields
|
||||
process_fields(creation.get("required", []), required=True)
|
||||
# Process optional fields (excluding those with defaults and bearer_token)
|
||||
process_fields(creation.get("optional", []), required=False)
|
||||
|
||||
# b) ConnectedAccountInitiation fields (for API_KEY, etc.)
|
||||
initiation = fields.get("connected_account_initiation") or fields.get("connectedAccountInitiation") or {}
|
||||
# Process required fields
|
||||
process_fields(initiation.get("required", []), required=True)
|
||||
# Process optional fields (excluding those with defaults)
|
||||
process_fields(initiation.get("optional", []), required=False)
|
||||
|
||||
def _collect_all_auth_field_names(self, schema: dict[str, Any] | None) -> set[str]:
|
||||
names: set[str] = set()
|
||||
if not schema:
|
||||
@ -1523,10 +1507,15 @@ class ComposioBaseComponent(Component):
|
||||
selected_mode = (build_config.get("auth_mode") or {}).get("value")
|
||||
managed = (schema or {}).get("composio_managed_auth_schemes") or []
|
||||
# Don't render custom fields if "Composio_Managed" is selected
|
||||
if selected_mode and selected_mode != "Composio_Managed":
|
||||
# For API_KEY and other token modes, no fields are needed as they use link method
|
||||
token_modes = ["API_KEY", "BEARER_TOKEN", "BASIC"]
|
||||
if selected_mode and selected_mode not in ["Composio_Managed", *token_modes]:
|
||||
self._clear_auth_dynamic_fields(build_config)
|
||||
self._render_custom_auth_fields(build_config, schema or {}, selected_mode)
|
||||
# Already reordered in _render_custom_auth_fields
|
||||
elif selected_mode in token_modes:
|
||||
# Clear any existing auth fields for token-based modes
|
||||
self._clear_auth_dynamic_fields(build_config)
|
||||
except (TypeError, ValueError, AttributeError):
|
||||
pass
|
||||
|
||||
@ -1673,6 +1662,9 @@ class ComposioBaseComponent(Component):
|
||||
if mode == "Composio_Managed":
|
||||
# Composio_Managed → no extra fields needed
|
||||
pass
|
||||
elif mode in ["API_KEY", "BEARER_TOKEN", "BASIC"]:
|
||||
# Token-based modes → no fields needed, user enters on Composio page via link
|
||||
pass
|
||||
elif isinstance(managed, list) and mode in managed:
|
||||
# This is a specific managed auth scheme (e.g., OAUTH2) but user can still choose custom
|
||||
# So we should render custom fields for this mode
|
||||
@ -1737,6 +1729,13 @@ class ComposioBaseComponent(Component):
|
||||
|
||||
# Create new connection ONLY if we truly have no usable connection yet
|
||||
if existing_active is None:
|
||||
# Check if we already have a redirect URL in progress
|
||||
current_auth_link_value = build_config.get("auth_link", {}).get("value", "")
|
||||
if current_auth_link_value and current_auth_link_value.startswith(("http://", "https://")):
|
||||
# We already have a redirect URL, don't create a new one
|
||||
logger.info(f"Redirect URL already exists for {toolkit_slug}, skipping new creation")
|
||||
return self.update_input_types(build_config)
|
||||
|
||||
try:
|
||||
# Determine auth mode
|
||||
schema = self._get_toolkit_schema()
|
||||
@ -1759,7 +1758,7 @@ class ComposioBaseComponent(Component):
|
||||
build_config["auth_link"]["auth_tooltip"] = "Select Auth Mode"
|
||||
return self.update_input_types(build_config)
|
||||
# Custom modes: create auth config and/or initiate with config
|
||||
# Validate required fields before creating any auth config
|
||||
# Only validate auth_config_creation fields for OAUTH2
|
||||
required_missing = []
|
||||
if mode == "OAUTH2":
|
||||
req_names_pre = self._get_schema_field_names(
|
||||
@ -1773,30 +1772,6 @@ class ComposioBaseComponent(Component):
|
||||
val = build_config[fname].get("value")
|
||||
if val in (None, ""):
|
||||
required_missing.append(fname)
|
||||
elif mode == "API_KEY":
|
||||
req_names_pre = self._get_schema_field_names(
|
||||
schema,
|
||||
"API_KEY",
|
||||
"connected_account_initiation",
|
||||
"required",
|
||||
)
|
||||
for fname in req_names_pre:
|
||||
if fname in build_config:
|
||||
val = build_config[fname].get("value")
|
||||
if val in (None, ""):
|
||||
required_missing.append(fname)
|
||||
else:
|
||||
req_names_pre = self._get_schema_field_names(
|
||||
schema,
|
||||
mode,
|
||||
"connected_account_initiation",
|
||||
"required",
|
||||
)
|
||||
for fname in req_names_pre:
|
||||
if fname in build_config:
|
||||
val = build_config[fname].get("value")
|
||||
if val in (None, ""):
|
||||
required_missing.append(fname)
|
||||
if required_missing:
|
||||
# Surface errors on each missing field
|
||||
for fname in required_missing:
|
||||
@ -1820,24 +1795,18 @@ class ComposioBaseComponent(Component):
|
||||
# If an auth_config was already created via the button, use it and include initiation fields
|
||||
stored_ac_id = (build_config.get("auth_link") or {}).get("auth_config_id")
|
||||
if stored_ac_id:
|
||||
# Build val from schema-declared connected_account_initiation required + rendered fields
|
||||
val_payload = {}
|
||||
init_req = self._get_schema_field_names(
|
||||
schema,
|
||||
"OAUTH2",
|
||||
"connected_account_initiation",
|
||||
"required",
|
||||
)
|
||||
candidate_names = set(self._auth_dynamic_fields) | init_req
|
||||
for fname in candidate_names:
|
||||
if fname in build_config:
|
||||
v = build_config[fname].get("value")
|
||||
if v not in (None, ""):
|
||||
val_payload[fname] = v
|
||||
redirect = composio.connected_accounts.initiate(
|
||||
# Check if we already have a redirect URL to prevent duplicates
|
||||
current_link_value = build_config.get("auth_link", {}).get("value", "")
|
||||
if current_link_value and current_link_value.startswith(("http://", "https://")):
|
||||
logger.info(
|
||||
f"Redirect URL already exists for {toolkit_slug} OAUTH2, skipping new creation"
|
||||
)
|
||||
return self.update_input_types(build_config)
|
||||
|
||||
# Use link method - no need to collect connection initiation fields
|
||||
redirect = composio.connected_accounts.link(
|
||||
user_id=self.entity_id,
|
||||
auth_config_id=stored_ac_id,
|
||||
config={"auth_scheme": "OAUTH2", "val": val_payload} if val_payload else None,
|
||||
)
|
||||
redirect_url = getattr(redirect, "redirect_url", None)
|
||||
connection_id = getattr(redirect, "id", None)
|
||||
@ -1848,6 +1817,9 @@ class ComposioBaseComponent(Component):
|
||||
# Clear action blocker text on successful initiation
|
||||
build_config["action_button"]["helper_text"] = ""
|
||||
build_config["action_button"]["helper_text_metadata"] = {}
|
||||
# Clear any auth fields
|
||||
schema = self._get_toolkit_schema()
|
||||
self._clear_auth_fields_from_schema(build_config, schema)
|
||||
return self.update_input_types(build_config)
|
||||
# Otherwise, create custom OAuth2 auth config using schema-declared required fields
|
||||
credentials = {}
|
||||
@ -1868,6 +1840,14 @@ class ComposioBaseComponent(Component):
|
||||
else:
|
||||
missing.append(fname)
|
||||
# proceed even if missing optional; backend will validate
|
||||
# Check if we already have a redirect URL to prevent duplicates
|
||||
current_link_value = build_config.get("auth_link", {}).get("value", "")
|
||||
if current_link_value and current_link_value.startswith(("http://", "https://")):
|
||||
logger.info(
|
||||
f"Redirect URL already exists for {toolkit_slug} OAUTH2, skipping new creation"
|
||||
)
|
||||
return self.update_input_types(build_config)
|
||||
|
||||
ac = composio.auth_configs.create(
|
||||
toolkit=toolkit_slug,
|
||||
options={
|
||||
@ -1877,30 +1857,8 @@ class ComposioBaseComponent(Component):
|
||||
},
|
||||
)
|
||||
auth_config_id = getattr(ac, "id", None)
|
||||
# If the schema declares initiation required fields, render them and defer initiation
|
||||
init_req = self._get_schema_field_names(
|
||||
schema,
|
||||
"OAUTH2",
|
||||
"connected_account_initiation",
|
||||
"required",
|
||||
)
|
||||
if init_req:
|
||||
self._clear_auth_dynamic_fields(build_config)
|
||||
for name in init_req:
|
||||
self._add_text_field(
|
||||
build_config,
|
||||
name=name,
|
||||
display_name=name.replace("_", " ").title(),
|
||||
info="Provide connection parameter",
|
||||
required=True,
|
||||
)
|
||||
build_config.setdefault("auth_link", {})
|
||||
build_config["auth_link"]["auth_config_id"] = auth_config_id
|
||||
build_config["auth_link"]["value"] = "connect"
|
||||
build_config["auth_link"]["auth_tooltip"] = "Connect"
|
||||
return self.update_input_types(build_config)
|
||||
# Otherwise initiate immediately
|
||||
redirect = composio.connected_accounts.initiate(
|
||||
# Use link method directly - no need to check for connection initiation fields
|
||||
redirect = composio.connected_accounts.link(
|
||||
user_id=self.entity_id,
|
||||
auth_config_id=auth_config_id,
|
||||
)
|
||||
@ -1917,141 +1875,64 @@ class ComposioBaseComponent(Component):
|
||||
build_config["action_button"]["helper_text_metadata"] = {}
|
||||
return self.update_input_types(build_config)
|
||||
if mode == "API_KEY":
|
||||
# Check if we already have a redirect URL to prevent duplicates
|
||||
current_link_value = build_config.get("auth_link", {}).get("value", "")
|
||||
if current_link_value and current_link_value.startswith(("http://", "https://")):
|
||||
logger.info(
|
||||
f"Redirect URL already exists for {toolkit_slug} API_KEY, skipping new creation"
|
||||
)
|
||||
return self.update_input_types(build_config)
|
||||
|
||||
ac = composio.auth_configs.create(
|
||||
toolkit=toolkit_slug,
|
||||
options={"type": "use_custom_auth", "auth_scheme": "API_KEY", "credentials": {}},
|
||||
)
|
||||
auth_config_id = getattr(ac, "id", None)
|
||||
# Build initiation config.val from schema-declared required names and dynamic fields
|
||||
val_payload = {}
|
||||
missing = []
|
||||
# Collect required names from schema
|
||||
req_names = self._get_schema_field_names(
|
||||
schema,
|
||||
"API_KEY",
|
||||
"connected_account_initiation",
|
||||
"required",
|
||||
)
|
||||
# Merge rendered dynamic fields and schema-required names
|
||||
candidate_names = set(self._auth_dynamic_fields) | req_names
|
||||
for fname in candidate_names:
|
||||
if fname in build_config:
|
||||
val = build_config[fname].get("value")
|
||||
if val not in (None, ""):
|
||||
val_payload[fname] = val
|
||||
else:
|
||||
missing.append(fname)
|
||||
initiation = composio.connected_accounts.initiate(
|
||||
# Use link method - user will enter API key on Composio page
|
||||
initiation = composio.connected_accounts.link(
|
||||
user_id=self.entity_id,
|
||||
auth_config_id=auth_config_id,
|
||||
config={"auth_scheme": "API_KEY", "val": val_payload},
|
||||
)
|
||||
connection_id = getattr(initiation, "id", None)
|
||||
redirect_url = getattr(initiation, "redirect_url", None)
|
||||
# Do not store connection_id on initiation; only when ACTIVE
|
||||
# API_KEY now also returns redirect URL with new link method
|
||||
if redirect_url:
|
||||
build_config["auth_link"]["value"] = redirect_url
|
||||
build_config["auth_link"]["auth_tooltip"] = "Disconnect"
|
||||
else:
|
||||
# No redirect for API_KEY; mark as connected
|
||||
build_config["auth_link"]["value"] = "validated"
|
||||
build_config["auth_link"]["auth_tooltip"] = "Disconnect"
|
||||
# In both cases, hide auth fields immediately after successful initiation
|
||||
# Hide auth fields immediately after successful initiation
|
||||
schema = self._get_toolkit_schema()
|
||||
self._clear_auth_fields_from_schema(build_config, schema)
|
||||
build_config["action_button"]["helper_text"] = ""
|
||||
build_config["action_button"]["helper_text_metadata"] = {}
|
||||
|
||||
# Convert auth_mode to pill for connected state
|
||||
if not redirect_url and mode: # API_KEY or similar direct connection
|
||||
build_config["auth_link"]["connection_id"] = connection_id
|
||||
build_config.setdefault("auth_mode", {})
|
||||
build_config["auth_mode"]["value"] = mode
|
||||
build_config["auth_mode"]["options"] = [mode]
|
||||
build_config["auth_mode"]["show"] = False
|
||||
try:
|
||||
pill = TabInput(
|
||||
name="auth_mode",
|
||||
display_name="Auth Mode",
|
||||
options=[mode],
|
||||
value=mode,
|
||||
).to_dict()
|
||||
pill["show"] = True
|
||||
build_config["auth_mode"] = pill
|
||||
except (TypeError, ValueError, AttributeError):
|
||||
build_config["auth_mode"] = {
|
||||
"name": "auth_mode",
|
||||
"display_name": "Auth Mode",
|
||||
"type": "tab",
|
||||
"options": [mode],
|
||||
"value": mode,
|
||||
"show": True,
|
||||
}
|
||||
|
||||
return self.update_input_types(build_config)
|
||||
# Generic custom auth flow for any other mode (treat like API_KEY)
|
||||
# Check if we already have a redirect URL to prevent duplicates
|
||||
current_link_value = build_config.get("auth_link", {}).get("value", "")
|
||||
if current_link_value and current_link_value.startswith(("http://", "https://")):
|
||||
logger.info(f"Redirect URL already exists for {toolkit_slug} {mode}, skipping new creation")
|
||||
return self.update_input_types(build_config)
|
||||
|
||||
ac = composio.auth_configs.create(
|
||||
toolkit=toolkit_slug,
|
||||
options={"type": "use_custom_auth", "auth_scheme": mode, "credentials": {}},
|
||||
)
|
||||
auth_config_id = getattr(ac, "id", None)
|
||||
val_payload = {}
|
||||
req_names = self._get_schema_field_names(
|
||||
schema,
|
||||
mode,
|
||||
"connected_account_initiation",
|
||||
"required",
|
||||
)
|
||||
candidate_names = set(self._auth_dynamic_fields) | req_names
|
||||
for fname in candidate_names:
|
||||
if fname in build_config:
|
||||
val = build_config[fname].get("value")
|
||||
if val not in (None, ""):
|
||||
val_payload[fname] = val
|
||||
initiation = composio.connected_accounts.initiate(
|
||||
# Use link method - user will enter required fields on Composio page
|
||||
initiation = composio.connected_accounts.link(
|
||||
user_id=self.entity_id,
|
||||
auth_config_id=auth_config_id,
|
||||
config={"auth_scheme": mode, "val": val_payload},
|
||||
)
|
||||
connection_id = getattr(initiation, "id", None)
|
||||
redirect_url = getattr(initiation, "redirect_url", None)
|
||||
# Do not store connection_id on initiation; only when ACTIVE
|
||||
if redirect_url:
|
||||
build_config["auth_link"]["value"] = redirect_url
|
||||
build_config["auth_link"]["auth_tooltip"] = "Disconnect"
|
||||
else:
|
||||
build_config["auth_link"]["value"] = "validated"
|
||||
build_config["auth_link"]["auth_tooltip"] = "Disconnect"
|
||||
build_config["auth_link"]["connection_id"] = connection_id
|
||||
|
||||
# Clear auth fields when connected
|
||||
schema = self._get_toolkit_schema()
|
||||
self._clear_auth_fields_from_schema(build_config, schema)
|
||||
|
||||
# Convert auth_mode to pill for connected state
|
||||
if mode:
|
||||
build_config.setdefault("auth_mode", {})
|
||||
build_config["auth_mode"]["value"] = mode
|
||||
build_config["auth_mode"]["options"] = [mode]
|
||||
build_config["auth_mode"]["show"] = False
|
||||
try:
|
||||
pill = TabInput(
|
||||
name="auth_mode",
|
||||
display_name="Auth Mode",
|
||||
options=[mode],
|
||||
value=mode,
|
||||
).to_dict()
|
||||
pill["show"] = True
|
||||
build_config["auth_mode"] = pill
|
||||
except (TypeError, ValueError, AttributeError):
|
||||
build_config["auth_mode"] = {
|
||||
"name": "auth_mode",
|
||||
"display_name": "Auth Mode",
|
||||
"type": "tab",
|
||||
"options": [mode],
|
||||
"value": mode,
|
||||
"show": True,
|
||||
}
|
||||
# Clear auth fields
|
||||
schema = self._get_toolkit_schema()
|
||||
self._clear_auth_fields_from_schema(build_config, schema)
|
||||
build_config["action_button"]["helper_text"] = ""
|
||||
build_config["action_button"]["helper_text_metadata"] = {}
|
||||
return self.update_input_types(build_config)
|
||||
except (ValueError, ConnectionError, TypeError) as e:
|
||||
logger.error(f"Error creating connection: {e}")
|
||||
@ -2185,7 +2066,12 @@ class ComposioBaseComponent(Component):
|
||||
schema = self._get_toolkit_schema()
|
||||
mode = (build_config.get("auth_mode") or {}).get("value")
|
||||
managed = (schema or {}).get("composio_managed_auth_schemes") or []
|
||||
if mode and mode != "Composio_Managed" and not getattr(self, "_auth_dynamic_fields", set()):
|
||||
token_modes = ["API_KEY", "BEARER_TOKEN", "BASIC"]
|
||||
if (
|
||||
mode
|
||||
and mode not in ["Composio_Managed", *token_modes]
|
||||
and not getattr(self, "_auth_dynamic_fields", set())
|
||||
):
|
||||
self._render_custom_auth_fields(build_config, schema or {}, mode)
|
||||
# Already reordered in _render_custom_auth_fields
|
||||
except (TypeError, ValueError, AttributeError):
|
||||
@ -2230,6 +2116,12 @@ class ComposioBaseComponent(Component):
|
||||
# Handle auth config button click
|
||||
if field_name == "create_auth_config" and field_value == "create":
|
||||
try:
|
||||
# Check if we already have a redirect URL to prevent duplicates
|
||||
current_link_value = build_config.get("auth_link", {}).get("value", "")
|
||||
if current_link_value and current_link_value.startswith(("http://", "https://")):
|
||||
logger.info("Redirect URL already exists, skipping new auth config creation")
|
||||
return self.update_input_types(build_config)
|
||||
|
||||
composio = self._build_wrapper()
|
||||
toolkit_slug = self.app_name.lower()
|
||||
schema = self._get_toolkit_schema() or {}
|
||||
@ -2250,29 +2142,8 @@ class ComposioBaseComponent(Component):
|
||||
auth_config_id = getattr(ac, "id", None)
|
||||
build_config.setdefault("auth_link", {})
|
||||
if auth_config_id:
|
||||
# Check if there are connection initiation required fields
|
||||
initiation_required = self._get_schema_field_names(
|
||||
schema, "OAUTH2", "connected_account_initiation", "required"
|
||||
)
|
||||
if initiation_required:
|
||||
# Populate those fields dynamically for the user to fill
|
||||
self._clear_auth_dynamic_fields(build_config)
|
||||
for name in initiation_required:
|
||||
# Render as text inputs to collect connection fields
|
||||
self._add_text_field(
|
||||
build_config,
|
||||
name=name,
|
||||
display_name=name.replace("_", " ").title(),
|
||||
info="Provide connection parameter",
|
||||
required=True,
|
||||
)
|
||||
# Store the new auth_config_id so pressing Connect will use it
|
||||
build_config["auth_link"]["auth_config_id"] = auth_config_id
|
||||
build_config["auth_link"]["value"] = "connect"
|
||||
build_config["auth_link"]["auth_tooltip"] = "Connect"
|
||||
return self.update_input_types(build_config)
|
||||
# If no initiation fields required, initiate immediately
|
||||
connection_request = composio.connected_accounts.initiate(
|
||||
# Use link method directly - no need to check for connection initiation fields
|
||||
connection_request = composio.connected_accounts.link(
|
||||
user_id=self.entity_id, auth_config_id=auth_config_id
|
||||
)
|
||||
redirect_url = getattr(connection_request, "redirect_url", None)
|
||||
|
||||
Reference in New Issue
Block a user