@@ -2003,18 +2003,19 @@ async def get_workflow_code(workflow_id: str):
20032003 @app .get ("/api/workflows/{workflow_id}/node/{node_id}/code/{code_type}" )
20042004 async def get_node_code (workflow_id : str , node_id : str , code_type : str ):
20052005 """
2006- Get the code for a specific node from task_executor.py.
2006+ Get the code for a specific node from task_executor.py or from the class path specified in YAML .
20072007
20082008 Uses AST-based detection to find code blocks by checking base class inheritance.
2009- This is the single source of truth - no markers or metadata copies.
2009+ When a class path is specified in the node config (e.g., pre_process or post_process),
2010+ it extracts the class name and searches for that specific class.
20102011
20112012 Args:
20122013 workflow_id: The workflow ID
20132014 node_id: The node ID
20142015 code_type: Type of code ('pre_process', 'post_process', 'lambda', 'branch_condition', 'output_generator', 'data_transform')
20152016
20162017 Returns:
2017- { "code": "...", "found": true/false }
2018+ { "code": "...", "found": true/false, "class_path": "..." }
20182019 """
20192020 import ast
20202021
@@ -2026,28 +2027,82 @@ async def get_node_code(workflow_id: str, node_id: str, code_type: str):
20262027
20272028 workflow = _workflows [workflow_id ]
20282029 workflow_dir = Path (workflow .source_path ).parent
2029- task_executor_path = workflow_dir / "task_executor.py"
20302030
20312031 valid_types = {'pre_process' , 'post_process' , 'lambda' , 'branch_condition' , 'output_generator' , 'data_transform' }
20322032 if code_type not in valid_types :
20332033 raise HTTPException (status_code = 400 , detail = f"Invalid code_type: { code_type } " )
20342034
2035- if not task_executor_path .exists ():
2036- return {"code" : "" , "found" : False , "path" : None }
2035+ # Get the class path from node config if available
2036+ class_path = None
2037+ target_class_name = None
2038+ target_file_path = None
2039+
2040+ # Find the node in the workflow to get its config
2041+ node_config = None
2042+ for n in workflow .nodes :
2043+ if n .id == node_id :
2044+ node_config = n
2045+ break
2046+
2047+ if node_config :
2048+ # Check for class path in node metadata (original_config from YAML)
2049+ original_config = node_config .metadata .get ("original_config" , {}) if node_config .metadata else {}
2050+
2051+ if code_type == 'pre_process' :
2052+ class_path = original_config .get ("pre_process" ) or getattr (node_config , "pre_process" , None )
2053+ elif code_type == 'post_process' :
2054+ class_path = original_config .get ("post_process" ) or getattr (node_config , "post_process" , None )
2055+ elif code_type == 'lambda' :
2056+ class_path = original_config .get ("lambda" ) or original_config .get ("function" ) or getattr (node_config , "function_path" , None )
2057+
2058+ # If we have a class path, extract the class name and determine the file
2059+ if class_path :
2060+ # Extract class name from path (e.g., "tasks.examples.foo.task_executor.MyClass" -> "MyClass")
2061+ parts = class_path .split ("." )
2062+ target_class_name = parts [- 1 ] if parts else None
2063+
2064+ # Determine the file path from the module path
2065+ # e.g., "tasks.examples.foo.task_executor.MyClass" -> "tasks/examples/foo/task_executor.py"
2066+ if len (parts ) > 1 :
2067+ module_parts = parts [:- 1 ] # Everything except the class name
2068+ relative_path = "/" .join (module_parts ) + ".py"
2069+
2070+ # Try to find the file relative to the project root
2071+ # First try: relative to workflow directory's parent (tasks folder level)
2072+ potential_paths = [
2073+ workflow_dir / relative_path ,
2074+ workflow_dir .parent / relative_path ,
2075+ workflow_dir .parent .parent / relative_path ,
2076+ Path .cwd () / relative_path ,
2077+ ]
2078+
2079+ for potential_path in potential_paths :
2080+ if potential_path .exists ():
2081+ target_file_path = potential_path
2082+ break
2083+
2084+ # Default to task_executor.py in workflow directory
2085+ if not target_file_path :
2086+ target_file_path = workflow_dir / "task_executor.py"
2087+
2088+ if not target_file_path .exists ():
2089+ return {"code" : "" , "found" : False , "path" : None , "class_path" : class_path }
20372090
20382091 try :
2039- with open (task_executor_path , 'r' ) as f :
2092+ with open (target_file_path , 'r' ) as f :
20402093 content = f .read ()
20412094 except Exception as e :
2042- return {"code" : "" , "found" : False , "error" : str (e )}
2095+ return {"code" : "" , "found" : False , "error" : str (e ), "class_path" : class_path }
20432096
20442097 # Find the code block using AST
2045- code = _get_node_code_from_file (content , node_id , code_type )
2098+ # If we have a specific class name from the config, search for it directly
2099+ code = _get_node_code_from_file (content , node_id , code_type , target_class_name )
20462100
20472101 return {
20482102 "code" : code if code else "" ,
20492103 "found" : code is not None ,
2050- "path" : str (task_executor_path .resolve ())
2104+ "path" : str (target_file_path .resolve ()),
2105+ "class_path" : class_path
20512106 }
20522107
20532108 @app .put ("/api/workflows/{workflow_id}/yaml" )
@@ -6196,10 +6251,16 @@ def _remove_code_block_from_file(content: str, node_id: str, code_type: str) ->
61966251 return result
61976252
61986253
6199- def _get_node_code_from_file (content : str , node_id : str , code_type : str ) -> Optional [str ]:
6254+ def _get_node_code_from_file (content : str , node_id : str , code_type : str , target_class_name : Optional [ str ] = None ) -> Optional [str ]:
62006255 """
62016256 Extract the code for a specific node from file content using AST.
62026257
6258+ Args:
6259+ content: The file content to parse
6260+ node_id: The node ID (used for fallback matching)
6261+ code_type: Type of code ('pre_process', 'post_process', etc.)
6262+ target_class_name: Optional specific class name to search for (from YAML config)
6263+
62036264 Returns the code string if found, None otherwise.
62046265 """
62056266 import ast
@@ -6225,7 +6286,7 @@ def _get_node_code_from_file(content: str, node_id: str, code_type: str) -> Opti
62256286 'branch_condition' : 'Condition' ,
62266287 }
62276288
6228- # Normalize node_id for comparison
6289+ # Normalize node_id for comparison (fallback matching)
62296290 safe_node_id = re .sub (r'[^a-zA-Z0-9_]' , '' , node_id .replace ('-' , '_' ).replace (' ' , '_' ))
62306291 expected_suffix = SUFFIX_MAP .get (code_type , '' )
62316292 expected_name = f"{ safe_node_id } { expected_suffix } "
@@ -6255,10 +6316,22 @@ def _get_node_code_from_file(content: str, node_id: str, code_type: str) -> Opti
62556316 detected_type = BASE_CLASS_TO_TYPE [base_name ]
62566317 break
62576318
6258- # Match if type and node_id match
6319+ # Match logic:
6320+ # 1. If target_class_name is provided, match exact class name with correct base class
6321+ # 2. Otherwise, fall back to node_id-based matching
62596322 if detected_type == code_type :
6260- normalized_class_id = re .sub (r'[^a-zA-Z0-9_]' , '' , class_safe_id .replace ('-' , '_' ))
6261- if normalized_class_id == safe_node_id or class_name == expected_name :
6323+ match_found = False
6324+
6325+ # Priority 1: Match by specific class name from YAML config
6326+ if target_class_name and class_name == target_class_name :
6327+ match_found = True
6328+ # Priority 2: Fall back to node_id-based matching
6329+ elif not target_class_name :
6330+ normalized_class_id = re .sub (r'[^a-zA-Z0-9_]' , '' , class_safe_id .replace ('-' , '_' ))
6331+ if normalized_class_id == safe_node_id or class_name == expected_name :
6332+ match_found = True
6333+
6334+ if match_found :
62626335 start_line = node .lineno - 1 # 0-indexed
62636336 end_line = node .end_lineno if hasattr (node , 'end_lineno' ) else start_line + 1
62646337
0 commit comments