error highlighting and message improvements
This commit is contained in:
@@ -45,6 +45,13 @@ def _is_link(value: Any) -> bool:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class NodeExecutionError(Exception):
|
||||||
|
"""Wraps an error that occurred while executing a specific node."""
|
||||||
|
def __init__(self, node_id: str, original: Exception):
|
||||||
|
self.node_id = node_id
|
||||||
|
super().__init__(str(original))
|
||||||
|
|
||||||
|
|
||||||
class ExecutionEngine:
|
class ExecutionEngine:
|
||||||
"""Synchronous (blocking) graph executor. Run inside a thread pool from async code."""
|
"""Synchronous (blocking) graph executor. Run inside a thread pool from async code."""
|
||||||
|
|
||||||
@@ -99,12 +106,15 @@ class ExecutionEngine:
|
|||||||
class_name = node_def["class_type"]
|
class_name = node_def["class_type"]
|
||||||
|
|
||||||
if class_name not in NODE_CLASS_MAPPINGS:
|
if class_name not in NODE_CLASS_MAPPINGS:
|
||||||
raise ValueError(f"Unknown node type: '{class_name}'")
|
raise NodeExecutionError(node_id, ValueError(f"Unknown node type: '{class_name}'"))
|
||||||
|
|
||||||
cls = NODE_CLASS_MAPPINGS[class_name]
|
cls = NODE_CLASS_MAPPINGS[class_name]
|
||||||
raw_inputs = node_def.get("inputs", {})
|
raw_inputs = node_def.get("inputs", {})
|
||||||
input_types = cls.INPUT_TYPES()
|
input_types = cls.INPUT_TYPES()
|
||||||
inputs = self._resolve_inputs(raw_inputs, node_outputs, input_types)
|
try:
|
||||||
|
inputs = self._resolve_inputs(raw_inputs, node_outputs, input_types)
|
||||||
|
except Exception as exc:
|
||||||
|
raise NodeExecutionError(node_id, exc) from exc
|
||||||
input_signature = self._build_input_signature(class_name, raw_inputs, node_output_signatures)
|
input_signature = self._build_input_signature(class_name, raw_inputs, node_output_signatures)
|
||||||
|
|
||||||
cache_entry = self._get_cached_entry(node_id, class_name, input_signature)
|
cache_entry = self._get_cached_entry(node_id, class_name, input_signature)
|
||||||
@@ -118,8 +128,13 @@ class ExecutionEngine:
|
|||||||
instance = cls()
|
instance = cls()
|
||||||
func = getattr(instance, cls.FUNCTION)
|
func = getattr(instance, cls.FUNCTION)
|
||||||
start_time = perf_counter()
|
start_time = perf_counter()
|
||||||
with active_node(node_id):
|
try:
|
||||||
result = func(**inputs)
|
with active_node(node_id):
|
||||||
|
result = func(**inputs)
|
||||||
|
except NodeExecutionError:
|
||||||
|
raise
|
||||||
|
except Exception as exc:
|
||||||
|
raise NodeExecutionError(node_id, exc) from exc
|
||||||
elapsed_ms = (perf_counter() - start_time) * 1000.0
|
elapsed_ms = (perf_counter() - start_time) * 1000.0
|
||||||
|
|
||||||
# Nodes must return a tuple; coerce single values just in case
|
# Nodes must return a tuple; coerce single values just in case
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ def create_app(
|
|||||||
from backend.plugin_loader import load_plugins
|
from backend.plugin_loader import load_plugins
|
||||||
load_plugins(plugins_dir())
|
load_plugins(plugins_dir())
|
||||||
|
|
||||||
from backend.execution import ExecutionEngine, new_prompt_id
|
from backend.execution import ExecutionEngine, NodeExecutionError, new_prompt_id
|
||||||
from backend.node_registry import NODE_CLASS_MAPPINGS, get_all_node_info
|
from backend.node_registry import NODE_CLASS_MAPPINGS, get_all_node_info
|
||||||
|
|
||||||
ensure_runtime_dirs(with_plugins=_plugins_on)
|
ensure_runtime_dirs(with_plugins=_plugins_on)
|
||||||
@@ -522,6 +522,12 @@ def create_app(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
broadcast(session_id, {"type": "execution_complete", "data": {"prompt_id": prompt_id}})
|
broadcast(session_id, {"type": "execution_complete", "data": {"prompt_id": prompt_id}})
|
||||||
|
except NodeExecutionError as exc:
|
||||||
|
log.exception("Execution error on node %s", exc.node_id)
|
||||||
|
broadcast(session_id, {
|
||||||
|
"type": "execution_error",
|
||||||
|
"data": {"node_id": exc.node_id, "message": str(exc)},
|
||||||
|
})
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
log.exception("Execution error")
|
log.exception("Execution error")
|
||||||
broadcast(session_id, {
|
broadcast(session_id, {
|
||||||
|
|||||||
@@ -1300,7 +1300,7 @@ function Flow() {
|
|||||||
case 'execution_start':
|
case 'execution_start':
|
||||||
setNodes((ns) => ns.map((n) => ({
|
setNodes((ns) => ns.map((n) => ({
|
||||||
...n,
|
...n,
|
||||||
data: { ...n.data, processingTimeMs: null },
|
data: { ...n.data, processingTimeMs: null, error: null },
|
||||||
})));
|
})));
|
||||||
setExecutingNodeId(null);
|
setExecutingNodeId(null);
|
||||||
setStatus({ text: 'Running workflow…', level: 'info' });
|
setStatus({ text: 'Running workflow…', level: 'info' });
|
||||||
@@ -1315,7 +1315,12 @@ function Flow() {
|
|||||||
break;
|
break;
|
||||||
case 'execution_error':
|
case 'execution_error':
|
||||||
setExecutingNodeId(null);
|
setExecutingNodeId(null);
|
||||||
setStatus({ text: 'Error: ' + msg.data.message, level: 'error' });
|
if (msg.data.node_id) {
|
||||||
|
updateNodeData(msg.data.node_id, { error: msg.data.message });
|
||||||
|
}
|
||||||
|
if (!msg.data.node_id) {
|
||||||
|
setStatus({ text: 'Error: ' + msg.data.message, level: 'error' });
|
||||||
|
}
|
||||||
console.error('[tono] execution error', msg.data);
|
console.error('[tono] execution error', msg.data);
|
||||||
break;
|
break;
|
||||||
case 'preview':
|
case 'preview':
|
||||||
|
|||||||
@@ -1182,7 +1182,7 @@ function CustomNode({ id, data }) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{ctx?.executingNodeId === id && <div className="node-executing-glow" aria-hidden="true" />}
|
{ctx?.executingNodeId === id && <div className="node-executing-glow" aria-hidden="true" />}
|
||||||
<div className="custom-node">
|
<div className={`custom-node${data.error ? ' node-error' : ''}`}>
|
||||||
{/* Title */}
|
{/* Title */}
|
||||||
<div className="node-title drag-handle" style={{ background: catColor }}>
|
<div className="node-title drag-handle" style={{ background: catColor }}>
|
||||||
<div className="node-title-left">
|
<div className="node-title-left">
|
||||||
@@ -1289,6 +1289,11 @@ function CustomNode({ id, data }) {
|
|||||||
<div className="node-warning">{data.warning}</div>
|
<div className="node-warning">{data.warning}</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Error notification */}
|
||||||
|
{data.error && (
|
||||||
|
<div className="node-error-message">{data.error}</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{scalarDisplay && !standaloneWidgets.some((w) => w.opts?.text_input) && (
|
{scalarDisplay && !standaloneWidgets.some((w) => w.opts?.text_input) && (
|
||||||
<div className="node-value-display">
|
<div className="node-value-display">
|
||||||
<div className="node-value-box">
|
<div className="node-value-box">
|
||||||
|
|||||||
@@ -797,6 +797,21 @@ html, body, #root {
|
|||||||
border-bottom: 1px solid var(--warning-border);
|
border-bottom: 1px solid var(--warning-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.custom-node.node-error {
|
||||||
|
outline: 2px solid #ef4444;
|
||||||
|
outline-offset: -1px;
|
||||||
|
box-shadow: 0 0 8px rgba(239, 68, 68, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-error-message {
|
||||||
|
padding: 3px 10px;
|
||||||
|
font-size: 10px;
|
||||||
|
color: #fca5a5;
|
||||||
|
background: rgba(239, 68, 68, 0.12);
|
||||||
|
border-top: 1px solid rgba(239, 68, 68, 0.3);
|
||||||
|
border-bottom: 1px solid rgba(239, 68, 68, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
.node-value-display {
|
.node-value-display {
|
||||||
padding: 8px 10px 4px;
|
padding: 8px 10px 4px;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user