""" n8n Workflow Generator - Gradio Web Interface Deploy this to Hugging Face Spaces """ import gradio as gr from transformers import AutoModelForCausalLM, AutoTokenizer from peft import PeftModel import torch import json import re # ============================================================================== # CONFIGURATION # ============================================================================== MODEL_REPO = "Nishan30/n8n-workflow-generator" # Update with your HF repo BASE_MODEL = "Qwen/Qwen2.5-Coder-1.5B-Instruct" SYSTEM_PROMPT = """You are an expert n8n workflow generator. Given a user's request, you generate clean, functional TypeScript code using the @n8n-generator/core DSL. Your output should: - Only contain the code, no explanations - Use the Workflow class from @n8n-generator/core - Use workflow.add() to create nodes - Use .to() or workflow.connect() for connections - Be ready to compile directly to n8n JSON Example: User: "Create a webhook that sends data to Slack" Assistant: ```typescript const workflow = new Workflow('Webhook to Slack'); const webhook = workflow.add('n8n-nodes-base.webhook', { path: 'data' }); const slack = workflow.add('n8n-nodes-base.slack', { channel: '#general' }); webhook.to(slack); ```""" # ============================================================================== # MODEL LOADING # ============================================================================== @gr.cache def load_model(): """Load model once and cache it""" print("Loading model...") # Load base model base_model = AutoModelForCausalLM.from_pretrained( BASE_MODEL, torch_dtype=torch.float16, device_map="auto", trust_remote_code=True ) # Load LoRA adapter model = PeftModel.from_pretrained(base_model, MODEL_REPO) tokenizer = AutoTokenizer.from_pretrained(MODEL_REPO) print("Model loaded successfully!") return model, tokenizer # Load model at startup model, tokenizer = load_model() # ============================================================================== # CODE GENERATION # ============================================================================== def generate_workflow(prompt, temperature=0.3, max_tokens=512): """Generate n8n workflow code from prompt""" if not prompt.strip(): return "Please enter a workflow description.", None, None # Format messages messages = [ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": prompt} ] # Apply chat template text = tokenizer.apply_chat_template( messages, tokenize=False, add_generation_prompt=True ) # Tokenize inputs = tokenizer(text, return_tensors="pt").to(model.device) # Generate with torch.no_grad(): outputs = model.generate( **inputs, max_new_tokens=max_tokens, temperature=temperature, do_sample=True if temperature > 0 else False, top_p=0.9, repetition_penalty=1.1 ) # Decode generated_text = tokenizer.decode(outputs[0], skip_special_tokens=True) # Extract code from response code = extract_code(generated_text) # Convert to n8n JSON n8n_json = convert_to_n8n_json(code) # Create visualization visualization = create_visualization(n8n_json) return code, json.dumps(n8n_json, indent=2), visualization def extract_code(text): """Extract TypeScript code from generated text""" # Try to find code block code_match = re.search(r'```(?:typescript|ts)?\n(.*?)```', text, re.DOTALL) if code_match: return code_match.group(1).strip() # If no code block, look for code after assistant response if "assistant" in text.lower(): parts = text.split("assistant", 1) if len(parts) > 1: # Remove any markdown code blocks code = parts[1].strip() code = re.sub(r'```(?:typescript|ts)?\n', '', code) code = re.sub(r'```', '', code) return code.strip() return text.strip() # ============================================================================== # N8N JSON CONVERSION # ============================================================================== def convert_to_n8n_json(typescript_code): """Convert TypeScript DSL to n8n JSON format""" nodes = [] connections = {} workflow_name = "Generated Workflow" # Extract workflow name name_match = re.search(r"new Workflow\(['\"](.*?)['\"]\)", typescript_code) if name_match: workflow_name = name_match.group(1) # Extract node definitions node_pattern = r'const\s+(\w+)\s*=\s*workflow\.add\([\'"]([^\'\"]+)[\'"](?:,\s*({[^}]+}))?\)' node_matches = re.finditer(node_pattern, typescript_code) node_map = {} # variable name -> node id position_y = 250 position_x = 300 for i, match in enumerate(node_matches): var_name = match.group(1) node_type = match.group(2) params_str = match.group(3) if match.group(3) else "{}" # Parse parameters (basic JSON parsing) try: parameters = json.loads(params_str) except: parameters = {} node_id = str(i) node_map[var_name] = node_id nodes.append({ "id": node_id, "name": var_name, "type": node_type, "typeVersion": 1, "position": [position_x, position_y], "parameters": parameters }) position_x += 300 # Extract connections connection_pattern = r'(\w+)\.to\((\w+)\)' connection_matches = re.finditer(connection_pattern, typescript_code) for match in connection_matches: source_var = match.group(1) target_var = match.group(2) if source_var in node_map and target_var in node_map: source_id = node_map[source_var] target_id = node_map[target_var] # Find source node name source_node = next((n for n in nodes if n["id"] == source_id), None) if source_node: source_name = source_node["name"] if source_name not in connections: connections[source_name] = {"main": [[]] } connections[source_name]["main"][0].append({ "node": target_var, "type": "main", "index": 0 }) return { "name": workflow_name, "nodes": nodes, "connections": connections, "active": False, "settings": {} } # ============================================================================== # VISUALIZATION # ============================================================================== def create_visualization(n8n_json): """Create HTML visualization of the workflow""" nodes = n8n_json.get("nodes", []) connections = n8n_json.get("connections", {}) if not nodes: return "
{node_type}
{value_str}