refactor(platform): rename 'competitor' to neutral wording + add defensive guards

- Rename CompetitorFormat → SourcePlatform across all backend/frontend code
- Rename convert_competitor_workflow → convert_workflow
- Rename import_competitor_workflow → import_workflow
- Change API path from /competitor-workflow to /workflow
- Rename frontend LibraryImportCompetitorDialog → LibraryImportWorkflowDialog
- Update all docstrings, comments, and UI text to neutral language
- Add defensive type guards in describers: isinstance checks for n8n nodes,
  Make.com modules, Zapier steps, and connection entries
- Add str() cast for operation/resource in _describe_n8n_action
- Add empty workflow rejection in describe_workflow router
- Regenerate API client for new endpoint path
This commit is contained in:
Zamil Majdy
2026-03-16 23:32:10 +07:00
parent ea84454657
commit 4c91d39f26
14 changed files with 148 additions and 121 deletions

View File

@@ -1,4 +1,4 @@
"""API endpoint for importing competitor workflows."""
"""API endpoint for importing external workflows."""
import logging
from typing import Annotated, Any
@@ -7,10 +7,10 @@ import pydantic
from autogpt_libs.auth import get_user_id, requires_user
from fastapi import APIRouter, HTTPException, Security
from backend.copilot.workflow_import.converter import convert_competitor_workflow
from backend.copilot.workflow_import.converter import convert_workflow
from backend.copilot.workflow_import.describers import describe_workflow
from backend.copilot.workflow_import.format_detector import (
CompetitorFormat,
SourcePlatform,
detect_format,
)
from backend.copilot.workflow_import.url_fetcher import fetch_n8n_template
@@ -21,7 +21,7 @@ router = APIRouter()
class ImportWorkflowRequest(pydantic.BaseModel):
"""Request body for importing a competitor workflow."""
"""Request body for importing an external workflow."""
workflow_json: dict[str, Any] | None = None
template_url: str | None = None
@@ -41,7 +41,7 @@ class ImportWorkflowRequest(pydantic.BaseModel):
class ImportWorkflowResponse(pydantic.BaseModel):
"""Response from importing a competitor workflow."""
"""Response from importing an external workflow."""
graph: dict[str, Any]
graph_id: str | None = None
@@ -52,16 +52,17 @@ class ImportWorkflowResponse(pydantic.BaseModel):
@router.post(
path="/competitor-workflow",
summary="Import a competitor workflow (n8n, Make.com, Zapier)",
path="/workflow",
summary="Import a workflow from another tool (n8n, Make.com, Zapier)",
tags=["import"],
dependencies=[Security(requires_user)],
)
async def import_competitor_workflow(
async def import_workflow(
request: ImportWorkflowRequest,
user_id: Annotated[str, Security(get_user_id)],
) -> ImportWorkflowResponse:
"""Import a workflow from a competitor platform and convert it to an AutoGPT agent.
"""Import a workflow from another automation platform and convert it to an
AutoGPT agent.
Accepts either raw workflow JSON or a template URL (n8n only for now).
The workflow is parsed, described, and then converted to an AutoGPT graph
@@ -81,7 +82,7 @@ async def import_competitor_workflow(
# Step 2: Detect format
fmt = detect_format(workflow_json)
if fmt == CompetitorFormat.UNKNOWN:
if fmt == SourcePlatform.UNKNOWN:
raise HTTPException(
status_code=400,
detail="Could not detect workflow format. Supported formats: "
@@ -94,7 +95,7 @@ async def import_competitor_workflow(
# Step 4: Convert to AutoGPT agent
try:
agent_json, conversion_notes = await convert_competitor_workflow(desc)
agent_json, conversion_notes = await convert_workflow(desc)
except ValueError as e:
raise HTTPException(
status_code=502,

View File

@@ -75,7 +75,7 @@ def mock_converter(mocker):
"links": [],
}
return mocker.patch(
"backend.api.features.workflow_import.convert_competitor_workflow",
"backend.api.features.workflow_import.convert_workflow",
new_callable=AsyncMock,
return_value=(agent_json, ["Applied 2 auto-fixes"]),
)
@@ -96,10 +96,10 @@ def mock_save(mocker):
)
class TestImportCompetitorWorkflow:
class TestImportWorkflow:
def test_import_n8n_workflow(self, mock_converter, mock_save):
response = client.post(
"/competitor-workflow",
"/workflow",
json={"workflow_json": N8N_WORKFLOW, "save": True},
)
assert response.status_code == 200
@@ -111,7 +111,7 @@ class TestImportCompetitorWorkflow:
def test_import_make_workflow(self, mock_converter, mock_save):
response = client.post(
"/competitor-workflow",
"/workflow",
json={"workflow_json": MAKE_WORKFLOW, "save": True},
)
assert response.status_code == 200
@@ -121,7 +121,7 @@ class TestImportCompetitorWorkflow:
def test_import_zapier_workflow(self, mock_converter, mock_save):
response = client.post(
"/competitor-workflow",
"/workflow",
json={"workflow_json": ZAPIER_WORKFLOW, "save": True},
)
assert response.status_code == 200
@@ -131,7 +131,7 @@ class TestImportCompetitorWorkflow:
def test_import_without_save(self, mock_converter, mock_save):
response = client.post(
"/competitor-workflow",
"/workflow",
json={"workflow_json": N8N_WORKFLOW, "save": False},
)
assert response.status_code == 200
@@ -142,14 +142,14 @@ class TestImportCompetitorWorkflow:
def test_no_source_provided(self):
response = client.post(
"/competitor-workflow",
"/workflow",
json={"save": True},
)
assert response.status_code == 422 # Pydantic validation error
def test_both_sources_provided(self):
response = client.post(
"/competitor-workflow",
"/workflow",
json={
"workflow_json": N8N_WORKFLOW,
"template_url": "https://n8n.io/workflows/123",
@@ -160,7 +160,7 @@ class TestImportCompetitorWorkflow:
def test_unknown_format_returns_400(self, mock_converter):
response = client.post(
"/competitor-workflow",
"/workflow",
json={"workflow_json": {"foo": "bar"}, "save": False},
)
assert response.status_code == 400
@@ -168,12 +168,12 @@ class TestImportCompetitorWorkflow:
def test_converter_failure_returns_502(self, mocker):
mocker.patch(
"backend.api.features.workflow_import.convert_competitor_workflow",
"backend.api.features.workflow_import.convert_workflow",
new_callable=AsyncMock,
side_effect=ValueError("LLM call failed"),
)
response = client.post(
"/competitor-workflow",
"/workflow",
json={"workflow_json": N8N_WORKFLOW, "save": False},
)
assert response.status_code == 502
@@ -186,7 +186,7 @@ class TestImportCompetitorWorkflow:
side_effect=RuntimeError("DB connection failed"),
)
response = client.post(
"/competitor-workflow",
"/workflow",
json={"workflow_json": N8N_WORKFLOW, "save": True},
)
assert response.status_code == 500
@@ -199,7 +199,7 @@ class TestImportCompetitorWorkflow:
side_effect=ValueError("Invalid URL format"),
)
response = client.post(
"/competitor-workflow",
"/workflow",
json={"template_url": "https://bad-url.com", "save": False},
)
assert response.status_code == 400
@@ -212,7 +212,7 @@ class TestImportCompetitorWorkflow:
side_effect=RuntimeError("n8n API returned 500"),
)
response = client.post(
"/competitor-workflow",
"/workflow",
json={"template_url": "https://n8n.io/workflows/123", "save": False},
)
assert response.status_code == 502
@@ -220,7 +220,7 @@ class TestImportCompetitorWorkflow:
def test_response_model_shape(self, mock_converter, mock_save):
response = client.post(
"/competitor-workflow",
"/workflow",
json={"workflow_json": N8N_WORKFLOW, "save": True},
)
data = response.json()

View File

@@ -1,15 +1,15 @@
"""Competitor workflow import module.
"""Workflow import module.
Converts workflows from n8n, Make.com, and Zapier into AutoGPT agent graphs.
"""
from .converter import convert_competitor_workflow
from .format_detector import CompetitorFormat, detect_format
from .converter import convert_workflow
from .format_detector import SourcePlatform, detect_format
from .models import WorkflowDescription
__all__ = [
"CompetitorFormat",
"SourcePlatform",
"WorkflowDescription",
"convert_competitor_workflow",
"convert_workflow",
"detect_format",
]

View File

@@ -1,4 +1,4 @@
"""LLM-powered conversion of competitor workflows to AutoGPT agent graphs.
"""LLM-powered conversion of external workflows to AutoGPT agent graphs.
Uses the CoPilot's LLM client to generate AutoGPT agent JSON from a structured
WorkflowDescription, then validates and fixes via the existing pipeline.
@@ -84,7 +84,7 @@ def _build_conversion_prompt(
system_msg = f"""You are an expert at converting automation workflows into AutoGPT agent graphs.
Your task: Convert the competitor workflow described below into a valid AutoGPT agent JSON.
Your task: Convert the workflow described below into a valid AutoGPT agent JSON.
## Agent Generation Guide
{agent_guide}
@@ -93,11 +93,11 @@ Your task: Convert the competitor workflow described below into a valid AutoGPT
{block_catalog}
## Instructions
1. Map each competitor workflow step to the most appropriate AutoGPT block(s)
1. Map each workflow step to the most appropriate AutoGPT block(s)
2. If no exact block match exists, use the closest alternative (e.g., HttpRequestBlock for generic API calls)
3. Every agent MUST have at least one AgentInputBlock and one AgentOutputBlock
4. Wire blocks together with links matching the original workflow's data flow
5. Set meaningful input_default values based on the competitor's parameters
5. Set meaningful input_default values based on the workflow's parameters
6. Position nodes with 800+ X-unit spacing
7. Return ONLY valid JSON — no markdown fences, no explanation"""
@@ -124,13 +124,13 @@ IMPORTANT: Your previous attempt had validation errors. Fix them:
]
async def convert_competitor_workflow(
async def convert_workflow(
desc: WorkflowDescription,
) -> tuple[dict[str, Any], list[str]]:
"""Convert a WorkflowDescription into an AutoGPT agent JSON.
Args:
desc: Structured description of the competitor workflow.
desc: Structured description of the source workflow.
Returns:
Tuple of (agent_json dict, conversion_notes list).

View File

@@ -1,28 +1,31 @@
"""Extract structured WorkflowDescription from competitor workflow JSONs.
"""Extract structured WorkflowDescription from external workflow JSONs.
Each describer is a pure function that deterministically parses the competitor
Each describer is a pure function that deterministically parses the source
format into a platform-agnostic WorkflowDescription. No LLM calls are made here.
"""
import re
from typing import Any
from .models import CompetitorFormat, StepDescription, WorkflowDescription
from .models import SourcePlatform, StepDescription, WorkflowDescription
def describe_workflow(
json_data: dict[str, Any], fmt: CompetitorFormat
json_data: dict[str, Any], fmt: SourcePlatform
) -> WorkflowDescription:
"""Route to the appropriate describer based on detected format."""
describers = {
CompetitorFormat.N8N: describe_n8n_workflow,
CompetitorFormat.MAKE: describe_make_workflow,
CompetitorFormat.ZAPIER: describe_zapier_workflow,
SourcePlatform.N8N: describe_n8n_workflow,
SourcePlatform.MAKE: describe_make_workflow,
SourcePlatform.ZAPIER: describe_zapier_workflow,
}
describer = describers.get(fmt)
if not describer:
raise ValueError(f"No describer available for format: {fmt}")
return describer(json_data)
result = describer(json_data)
if not result.steps:
raise ValueError(f"Workflow contains no steps (format: {fmt.value})")
return result
def describe_n8n_workflow(json_data: dict[str, Any]) -> WorkflowDescription:
@@ -35,8 +38,11 @@ def describe_n8n_workflow(json_data: dict[str, Any]) -> WorkflowDescription:
steps: list[StepDescription] = []
for i, node in enumerate(nodes):
if not isinstance(node, dict):
continue
node_name = node.get("name", f"Node {i}")
node_index[node_name] = i
node_index[node_name] = len(steps)
node_type = node.get("type", "unknown")
# Extract service name from type (e.g., "n8n-nodes-base.gmail" -> "Gmail")
@@ -44,6 +50,8 @@ def describe_n8n_workflow(json_data: dict[str, Any]) -> WorkflowDescription:
# Build action description from type and parameters
params = node.get("parameters", {})
if not isinstance(params, dict):
params = {}
action = _describe_n8n_action(node_type, node_name, params)
# Extract key parameters (skip large/internal ones)
@@ -51,7 +59,7 @@ def describe_n8n_workflow(json_data: dict[str, Any]) -> WorkflowDescription:
steps.append(
StepDescription(
order=i,
order=len(steps),
action=action,
service=service,
parameters=clean_params,
@@ -69,16 +77,22 @@ def describe_n8n_workflow(json_data: dict[str, Any]) -> WorkflowDescription:
if not isinstance(output_group, list):
continue
for conn in output_group:
if not isinstance(conn, dict):
continue
target_name = conn.get("node")
if not isinstance(target_name, str):
continue
target_idx = node_index.get(target_name)
if target_idx is not None:
steps[source_idx].connections_to.append(target_idx)
# Detect trigger type
trigger_type = None
if nodes:
if nodes and isinstance(nodes[0], dict):
first_type = nodes[0].get("type", "")
if "trigger" in first_type.lower() or "webhook" in first_type.lower():
if isinstance(first_type, str) and (
"trigger" in first_type.lower() or "webhook" in first_type.lower()
):
trigger_type = _extract_n8n_service(first_type)
return WorkflowDescription(
@@ -86,7 +100,7 @@ def describe_n8n_workflow(json_data: dict[str, Any]) -> WorkflowDescription:
description=_build_workflow_summary(steps),
steps=steps,
trigger_type=trigger_type,
source_format=CompetitorFormat.N8N,
source_format=SourcePlatform.N8N,
raw_json=json_data,
)
@@ -95,15 +109,21 @@ def describe_make_workflow(json_data: dict[str, Any]) -> WorkflowDescription:
"""Extract a structured description from a Make.com scenario blueprint."""
flow = json_data.get("flow", [])
steps: list[StepDescription] = []
valid_count = 0
for module in flow:
if not isinstance(module, dict):
continue
for i, module in enumerate(flow):
module_ref = module.get("module", "unknown:unknown")
if not isinstance(module_ref, str):
module_ref = "unknown:unknown"
parts = module_ref.split(":", 1)
service = parts[0].replace("-", " ").title() if parts else "Unknown"
action_verb = parts[1] if len(parts) > 1 else "process"
# Build human-readable action
action = f"{action_verb.replace(':', ' ').title()} via {service}"
action = f"{str(action_verb).replace(':', ' ').title()} via {service}"
params = module.get("mapper", module.get("parameters", {}))
clean_params = _clean_params(params) if isinstance(params, dict) else {}
@@ -116,23 +136,26 @@ def describe_make_workflow(json_data: dict[str, Any]) -> WorkflowDescription:
clean_params["_has_routes"] = len(routes)
else:
# Make.com flows are sequential by default; each step connects to next
connections_to = [i + 1] if i < len(flow) - 1 else []
connections_to = [valid_count + 1] if valid_count < len(flow) - 1 else []
steps.append(
StepDescription(
order=i,
order=valid_count,
action=action,
service=service,
parameters=clean_params,
connections_to=connections_to,
)
)
valid_count += 1
# Detect trigger
trigger_type = None
if flow:
if flow and isinstance(flow[0], dict):
first_module = flow[0].get("module", "")
if "watch" in first_module.lower() or "trigger" in first_module.lower():
if isinstance(first_module, str) and (
"watch" in first_module.lower() or "trigger" in first_module.lower()
):
trigger_type = first_module.split(":")[0].replace("-", " ").title()
return WorkflowDescription(
@@ -140,7 +163,7 @@ def describe_make_workflow(json_data: dict[str, Any]) -> WorkflowDescription:
description=_build_workflow_summary(steps),
steps=steps,
trigger_type=trigger_type,
source_format=CompetitorFormat.MAKE,
source_format=SourcePlatform.MAKE,
raw_json=json_data,
)
@@ -149,30 +172,35 @@ def describe_zapier_workflow(json_data: dict[str, Any]) -> WorkflowDescription:
"""Extract a structured description from a Zapier Zap JSON."""
zap_steps = json_data.get("steps", [])
steps: list[StepDescription] = []
valid_count = 0
for step in zap_steps:
if not isinstance(step, dict):
continue
for i, step in enumerate(zap_steps):
app = step.get("app", "Unknown")
action = step.get("action", "process")
action_desc = f"{action.replace('_', ' ').title()} via {app}"
action_desc = f"{str(action).replace('_', ' ').title()} via {app}"
params = step.get("params", step.get("inputFields", {}))
clean_params = _clean_params(params) if isinstance(params, dict) else {}
# Zapier zaps are linear: each step connects to next
connections_to = [i + 1] if i < len(zap_steps) - 1 else []
connections_to = [valid_count + 1] if valid_count < len(zap_steps) - 1 else []
steps.append(
StepDescription(
order=i,
order=valid_count,
action=action_desc,
service=app,
parameters=clean_params,
connections_to=connections_to,
)
)
valid_count += 1
trigger_type = None
if zap_steps:
if zap_steps and isinstance(zap_steps[0], dict):
trigger_type = zap_steps[0].get("app")
return WorkflowDescription(
@@ -180,7 +208,7 @@ def describe_zapier_workflow(json_data: dict[str, Any]) -> WorkflowDescription:
description=_build_workflow_summary(steps),
steps=steps,
trigger_type=trigger_type,
source_format=CompetitorFormat.ZAPIER,
source_format=SourcePlatform.ZAPIER,
raw_json=json_data,
)
@@ -213,8 +241,8 @@ def _extract_n8n_service(node_type: str) -> str:
def _describe_n8n_action(node_type: str, node_name: str, params: dict[str, Any]) -> str:
"""Build a human-readable action description for an n8n node."""
service = _extract_n8n_service(node_type)
resource = params.get("resource", "")
operation = params.get("operation", "")
resource = str(params.get("resource", ""))
operation = str(params.get("operation", ""))
if resource and operation:
return f"{operation.title()} {resource} via {service}"

View File

@@ -8,7 +8,7 @@ from .describers import (
describe_workflow,
describe_zapier_workflow,
)
from .models import CompetitorFormat
from .models import SourcePlatform
class TestDescribeN8nWorkflow:
@@ -35,7 +35,7 @@ class TestDescribeN8nWorkflow:
}
desc = describe_n8n_workflow(data)
assert desc.name == "Email on Webhook"
assert desc.source_format == CompetitorFormat.N8N
assert desc.source_format == SourcePlatform.N8N
assert len(desc.steps) == 2
assert desc.steps[0].connections_to == [1]
assert desc.steps[1].connections_to == []
@@ -83,7 +83,7 @@ class TestDescribeMakeWorkflow:
}
desc = describe_make_workflow(data)
assert desc.name == "Sheets to Calendar"
assert desc.source_format == CompetitorFormat.MAKE
assert desc.source_format == SourcePlatform.MAKE
assert len(desc.steps) == 2
# Sequential: step 0 connects to step 1
assert desc.steps[0].connections_to == [1]
@@ -113,7 +113,7 @@ class TestDescribeZapierWorkflow:
}
desc = describe_zapier_workflow(data)
assert desc.name == "Gmail to Slack"
assert desc.source_format == CompetitorFormat.ZAPIER
assert desc.source_format == SourcePlatform.ZAPIER
assert len(desc.steps) == 2
assert desc.steps[0].connections_to == [1]
assert desc.trigger_type == "Gmail"
@@ -127,9 +127,9 @@ class TestDescribeWorkflowRouter:
],
"connections": {},
}
desc = describe_workflow(data, CompetitorFormat.N8N)
assert desc.source_format == CompetitorFormat.N8N
desc = describe_workflow(data, SourcePlatform.N8N)
assert desc.source_format == SourcePlatform.N8N
def test_unknown_raises(self):
with pytest.raises(ValueError, match="No describer"):
describe_workflow({}, CompetitorFormat.UNKNOWN)
describe_workflow({}, SourcePlatform.UNKNOWN)

View File

@@ -1,29 +1,29 @@
"""Detect the source platform of a competitor workflow JSON."""
"""Detect the source platform of a workflow JSON."""
import re
from typing import Any
from .models import CompetitorFormat
from .models import SourcePlatform
_N8N_TYPE_RE = re.compile(r"^(n8n-nodes-base\.|@n8n/)")
def detect_format(json_data: dict[str, Any]) -> CompetitorFormat:
def detect_format(json_data: dict[str, Any]) -> SourcePlatform:
"""Inspect a workflow JSON and determine which platform it came from.
Args:
json_data: The parsed JSON data from a competitor workflow file.
json_data: The parsed JSON data from a workflow export file.
Returns:
The detected CompetitorFormat.
The detected SourcePlatform.
"""
if _is_n8n(json_data):
return CompetitorFormat.N8N
return SourcePlatform.N8N
if _is_make(json_data):
return CompetitorFormat.MAKE
return SourcePlatform.MAKE
if _is_zapier(json_data):
return CompetitorFormat.ZAPIER
return CompetitorFormat.UNKNOWN
return SourcePlatform.ZAPIER
return SourcePlatform.UNKNOWN
def _is_n8n(data: dict[str, Any]) -> bool:

View File

@@ -1,7 +1,7 @@
"""Tests for format_detector.py."""
from .format_detector import detect_format
from .models import CompetitorFormat
from .models import SourcePlatform
class TestDetectFormat:
@@ -26,7 +26,7 @@ class TestDetectFormat:
}
},
}
assert detect_format(data) == CompetitorFormat.N8N
assert detect_format(data) == SourcePlatform.N8N
def test_n8n_langchain_nodes(self):
data = {
@@ -39,7 +39,7 @@ class TestDetectFormat:
],
"connections": {},
}
assert detect_format(data) == CompetitorFormat.N8N
assert detect_format(data) == SourcePlatform.N8N
def test_make_scenario(self):
data = {
@@ -55,7 +55,7 @@ class TestDetectFormat:
},
],
}
assert detect_format(data) == CompetitorFormat.MAKE
assert detect_format(data) == SourcePlatform.MAKE
def test_zapier_zap(self):
data = {
@@ -69,14 +69,14 @@ class TestDetectFormat:
},
],
}
assert detect_format(data) == CompetitorFormat.ZAPIER
assert detect_format(data) == SourcePlatform.ZAPIER
def test_unknown_format(self):
data = {"foo": "bar", "nodes": []}
assert detect_format(data) == CompetitorFormat.UNKNOWN
assert detect_format(data) == SourcePlatform.UNKNOWN
def test_empty_dict(self):
assert detect_format({}) == CompetitorFormat.UNKNOWN
assert detect_format({}) == SourcePlatform.UNKNOWN
def test_autogpt_graph_not_detected_as_n8n(self):
"""AutoGPT graphs have nodes but not n8n-style types."""
@@ -86,16 +86,16 @@ class TestDetectFormat:
],
"connections": {},
}
assert detect_format(data) == CompetitorFormat.UNKNOWN
assert detect_format(data) == SourcePlatform.UNKNOWN
def test_make_without_colon_not_detected(self):
data = {
"flow": [{"module": "simplemodule", "mapper": {}}],
}
assert detect_format(data) == CompetitorFormat.UNKNOWN
assert detect_format(data) == SourcePlatform.UNKNOWN
def test_zapier_without_action_not_detected(self):
data = {
"steps": [{"app": "gmail"}],
}
assert detect_format(data) == CompetitorFormat.UNKNOWN
assert detect_format(data) == SourcePlatform.UNKNOWN

View File

@@ -1,4 +1,4 @@
"""Data models for competitor workflow import."""
"""Data models for external workflow import."""
from enum import Enum
from typing import Any
@@ -7,7 +7,7 @@ import pydantic
from pydantic import Field
class CompetitorFormat(str, Enum):
class SourcePlatform(str, Enum):
N8N = "n8n"
MAKE = "make"
ZAPIER = "zapier"
@@ -15,7 +15,7 @@ class CompetitorFormat(str, Enum):
class StepDescription(pydantic.BaseModel):
"""A single step/node extracted from a competitor workflow."""
"""A single step/node extracted from an external workflow."""
order: int
action: str
@@ -25,11 +25,11 @@ class StepDescription(pydantic.BaseModel):
class WorkflowDescription(pydantic.BaseModel):
"""Structured description of a competitor workflow."""
"""Structured description of an external workflow."""
name: str
description: str
steps: list[StepDescription]
trigger_type: str | None = None
source_format: CompetitorFormat
source_format: SourcePlatform
raw_json: dict[str, Any] = Field(default_factory=dict, exclude=True)

View File

@@ -1,4 +1,4 @@
"""Fetch competitor workflow templates by URL."""
"""Fetch workflow templates by URL."""
import logging
import re

View File

@@ -1,4 +1,4 @@
import LibraryImportCompetitorDialog from "../LibraryImportCompetitorDialog/LibraryImportCompetitorDialog";
import LibraryImportWorkflowDialog from "../LibraryImportWorkflowDialog/LibraryImportWorkflowDialog";
import { LibrarySearchBar } from "../LibrarySearchBar/LibrarySearchBar";
import LibraryUploadAgentDialog from "../LibraryUploadAgentDialog/LibraryUploadAgentDialog";
@@ -12,14 +12,14 @@ export function LibraryActionHeader({ setSearchTerm }: Props) {
<div className="mb-[32px] hidden items-center justify-center gap-4 md:flex">
<LibrarySearchBar setSearchTerm={setSearchTerm} />
<LibraryUploadAgentDialog />
<LibraryImportCompetitorDialog />
<LibraryImportWorkflowDialog />
</div>
{/* Mobile and tablet */}
<div className="flex flex-col gap-4 p-4 pt-[52px] md:hidden">
<div className="flex w-full justify-between gap-2">
<LibraryUploadAgentDialog />
<LibraryImportCompetitorDialog />
<LibraryImportWorkflowDialog />
</div>
<div className="flex items-center justify-center">

View File

@@ -13,14 +13,14 @@ import {
} from "@/components/molecules/Form/Form";
import { ArrowsClockwiseIcon } from "@phosphor-icons/react";
import { z } from "zod";
import { useLibraryImportCompetitorDialog } from "./useLibraryImportCompetitorDialog";
import { useLibraryImportWorkflowDialog } from "./useLibraryImportWorkflowDialog";
export const importCompetitorFormSchema = z.object({
export const importWorkflowFormSchema = z.object({
workflowFile: z.string(),
templateUrl: z.string(),
});
export default function LibraryImportCompetitorDialog() {
export default function LibraryImportWorkflowDialog() {
const {
onSubmit,
isConverting,
@@ -29,7 +29,7 @@ export default function LibraryImportCompetitorDialog() {
form,
importMode,
setImportMode,
} = useLibraryImportCompetitorDialog();
} = useLibraryImportWorkflowDialog();
const hasInput =
importMode === "url"
@@ -38,7 +38,7 @@ export default function LibraryImportCompetitorDialog() {
return (
<Dialog
title="Import Competitor Workflow"
title="Import Workflow"
styling={{ maxWidth: "32rem" }}
controlled={{
isOpen,
@@ -51,7 +51,7 @@ export default function LibraryImportCompetitorDialog() {
>
<Dialog.Trigger>
<Button
data-testid="import-competitor-button"
data-testid="import-workflow-button"
variant="primary"
className="h-[2.78rem] w-full md:w-[14rem]"
size="small"

View File

@@ -1,4 +1,4 @@
import { usePostV2ImportACompetitorWorkflowN8nMakeComZapier } from "@/app/api/__generated__/endpoints/import/import";
import { usePostV2ImportAWorkflowFromAnotherToolN8nMakeComZapier } from "@/app/api/__generated__/endpoints/import/import";
import type { ImportWorkflowRequest } from "@/app/api/__generated__/models/importWorkflowRequest";
import type { ImportWorkflowResponse } from "@/app/api/__generated__/models/importWorkflowResponse";
import { useToast } from "@/components/molecules/Toast/use-toast";
@@ -6,27 +6,25 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { importCompetitorFormSchema } from "./LibraryImportCompetitorDialog";
import { importWorkflowFormSchema } from "./LibraryImportWorkflowDialog";
export function useLibraryImportCompetitorDialog() {
export function useLibraryImportWorkflowDialog() {
const [isOpen, setIsOpen] = useState(false);
const { toast } = useToast();
const [importMode, setImportMode] = useState<"file" | "url">("file");
const { mutateAsync: importWorkflow, isPending: isConverting } =
usePostV2ImportACompetitorWorkflowN8nMakeComZapier();
usePostV2ImportAWorkflowFromAnotherToolN8nMakeComZapier();
const form = useForm<z.infer<typeof importCompetitorFormSchema>>({
resolver: zodResolver(importCompetitorFormSchema),
const form = useForm<z.infer<typeof importWorkflowFormSchema>>({
resolver: zodResolver(importWorkflowFormSchema),
defaultValues: {
workflowFile: "",
templateUrl: "",
},
});
const onSubmit = async (
values: z.infer<typeof importCompetitorFormSchema>,
) => {
const onSubmit = async (values: z.infer<typeof importWorkflowFormSchema>) => {
try {
let body: ImportWorkflowRequest;

View File

@@ -2920,12 +2920,12 @@
}
}
},
"/api/import/competitor-workflow": {
"/api/import/workflow": {
"post": {
"tags": ["v2", "import", "import"],
"summary": "Import a competitor workflow (n8n, Make.com, Zapier)",
"description": "Import a workflow from a competitor platform and convert it to an AutoGPT agent.\n\nAccepts either raw workflow JSON or a template URL (n8n only for now).\nThe workflow is parsed, described, and then converted to an AutoGPT graph\nusing LLM-powered block mapping.",
"operationId": "postV2Import a competitor workflow (n8n, make.com, zapier)",
"summary": "Import a workflow from another tool (n8n, Make.com, Zapier)",
"description": "Import a workflow from another automation platform and convert it to an\nAutoGPT agent.\n\nAccepts either raw workflow JSON or a template URL (n8n only for now).\nThe workflow is parsed, described, and then converted to an AutoGPT graph\nusing LLM-powered block mapping.",
"operationId": "postV2Import a workflow from another tool (n8n, make.com, zapier)",
"requestBody": {
"content": {
"application/json": {
@@ -10028,7 +10028,7 @@
},
"type": "object",
"title": "ImportWorkflowRequest",
"description": "Request body for importing a competitor workflow."
"description": "Request body for importing an external workflow."
},
"ImportWorkflowResponse": {
"properties": {
@@ -10057,7 +10057,7 @@
"type": "object",
"required": ["graph", "source_format", "source_name"],
"title": "ImportWorkflowResponse",
"description": "Response from importing a competitor workflow."
"description": "Response from importing an external workflow."
},
"InputValidationErrorResponse": {
"properties": {