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:
Uday Sidagana
2025-11-14 04:06:18 +05:30
committed by GitHub
parent 4c5534b1d1
commit 104e1bc07d

View File

@ -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)