Skip to content

Commit 0be84e0

Browse files
committed
node pre and post processor code sync with UI
1 parent c5cb360 commit 0be84e0

File tree

1 file changed

+88
-15
lines changed

1 file changed

+88
-15
lines changed

studio/api.py

Lines changed: 88 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)