diff --git a/backend/app/api/evals_management.py b/backend/app/api/evals_management.py index e815d97b..90802753 100644 --- a/backend/app/api/evals_management.py +++ b/backend/app/api/evals_management.py @@ -1,8 +1,7 @@ from fastapi import APIRouter, HTTPException, BackgroundTasks, Depends from sqlalchemy.orm import Session from pathlib import Path -import yaml -from typing import List, Dict, Any, Optional +from typing import List, Dict, Any from datetime import datetime, timezone from ..database import get_db diff --git a/backend/app/api/openai_compatible_api.py b/backend/app/api/openai_compatible_api.py index dd60bf91..cd424ae5 100644 --- a/backend/app/api/openai_compatible_api.py +++ b/backend/app/api/openai_compatible_api.py @@ -1,12 +1,9 @@ -import json from datetime import datetime, timezone from typing import Dict, Any, List, Optional, Union from fastapi import APIRouter, HTTPException, Depends, BackgroundTasks from pydantic import BaseModel from sqlalchemy.orm import Session -from ..schemas.workflow_schemas import WorkflowDefinitionSchema -from ..execution.workflow_executor import WorkflowExecutor from ..models.workflow_model import WorkflowModel from ..database import get_db from .workflow_run import run_workflow_blocking diff --git a/backend/app/execution/workflow_executor.py b/backend/app/execution/workflow_executor.py index e8f8ceb2..358d9dff 100644 --- a/backend/app/execution/workflow_executor.py +++ b/backend/app/execution/workflow_executor.py @@ -5,7 +5,7 @@ from pydantic import ValidationError -from ..nodes.base import BaseNodeOutput +from ..nodes.base import BaseNode, BaseNodeOutput from ..nodes.factory import NodeFactory from ..schemas.workflow_schemas import ( @@ -46,6 +46,7 @@ def __init__( self.task_recorder = None self.context = context self._node_dict: Dict[str, WorkflowNodeSchema] = {} + self.node_instances: Dict[str, BaseNode] = {} self._dependencies: Dict[str, Set[str]] = {} self._node_tasks: Dict[str, asyncio.Task[Optional[BaseNodeOutput]]] = {} self._initial_inputs: Dict[str, Dict[str, Any]] = {} @@ -172,7 +173,7 @@ async def _execute_node(self, node_id: str) -> Optional[BaseNodeOutput]: for dep_id in dependency_ids ), ) - except Exception as e: + except Exception: raise UpstreamFailure( f"Node {node_id} skipped due to upstream failure" ) @@ -257,6 +258,7 @@ async def _execute_node(self, node_id: str) -> Optional[BaseNodeOutput]: node_instance = NodeFactory.create_node( node_name=node.title, node_type_name=node.node_type, config=node.config ) + self.node_instances[node_id] = node_instance # Update task recorder if self.task_recorder: self.task_recorder.update_task( @@ -300,7 +302,7 @@ async def _execute_node(self, node_id: str) -> Optional[BaseNodeOutput]: f"Node Type: {node.node_type}\n" f"Node Title: {node.title}\n" f"Inputs: {node_input}\n" - f"Error: {str(e)}" + f"Error: {traceback.format_exc()}" ) print(error_msg) self._failed_nodes.add(node_id) @@ -317,17 +319,23 @@ async def run( self, input: Dict[str, Any] = {}, node_ids: List[str] = [], - precomputed_outputs: Dict[str, Dict[str, Any]] = {}, + precomputed_outputs: Dict[str, Dict[str, Any] | List[Dict[str, Any]]] = {}, ) -> Dict[str, BaseNodeOutput]: # Handle precomputed outputs first if precomputed_outputs: for node_id, output in precomputed_outputs.items(): try: - self._outputs[node_id] = NodeFactory.create_node( - node_name=self._node_dict[node_id].title, - node_type_name=self._node_dict[node_id].node_type, - config=self._node_dict[node_id].config, - ).output_model.model_validate(output) + if isinstance(output, dict): + self._outputs[node_id] = NodeFactory.create_node( + node_name=self._node_dict[node_id].title, + node_type_name=self._node_dict[node_id].node_type, + config=self._node_dict[node_id].config, + ).output_model.model_validate(output) + else: + # If output is a list of dicts, do not validate the output + # these are outputs of loop nodes, their precomputed outputs are not supported yet + continue + except ValidationError as e: print( f"[WARNING]: Precomputed output validation failed for node {node_id}: {e}\n skipping precomputed output" @@ -387,7 +395,7 @@ async def __call__( self, input: Dict[str, Any] = {}, node_ids: List[str] = [], - precomputed_outputs: Dict[str, Dict[str, Any]] = {}, + precomputed_outputs: Dict[str, Dict[str, Any] | List[Dict[str, Any]]] = {}, ) -> Dict[str, BaseNodeOutput]: """ Execute the workflow with the given input data. diff --git a/backend/app/nodes/base.py b/backend/app/nodes/base.py index 4aa4ab27..9da9db11 100644 --- a/backend/app/nodes/base.py +++ b/backend/app/nodes/base.py @@ -121,8 +121,13 @@ def create_output_model_class( else (field_type, ...) # try as is ) for field_name, field_type in output_schema.items() - }, # type: ignore + }, __base__=BaseNodeOutput, + __config__=None, + __doc__=f"Output model for {self.name} node", + __module__=self.__module__, + __validators__=None, + __cls_kwargs__=None, ) def create_composite_model_instance( @@ -142,10 +147,15 @@ def create_composite_model_instance( return create_model( model_name, **{ - instance.__class__.__name__: (instance.__class__, ...) # type: ignore + instance.__class__.__name__: (instance.__class__, ...) for instance in instances }, __base__=BaseNodeInput, + __config__=None, + __doc__=f"Input model for {self.name} node", + __module__=self.__module__, + __validators__=None, + __cls_kwargs__=None, ) async def __call__( diff --git a/backend/app/nodes/llm/_utils.py b/backend/app/nodes/llm/_utils.py index 0571070d..e80e664d 100644 --- a/backend/app/nodes/llm/_utils.py +++ b/backend/app/nodes/llm/_utils.py @@ -5,8 +5,7 @@ import os import re from enum import Enum -from typing import Any, Callable, Dict, List, Optional, cast -from pathlib import Path +from typing import Any, Callable, Dict, List, Optional from docx2python import docx2python import litellm @@ -494,7 +493,7 @@ async def completion_with_backoff(**kwargs) -> str: return response.choices[0].message.content except Exception as e: - logging.error(f"=== LLM Request Error ===") + logging.error("=== LLM Request Error ===") # Create a save copy of kwargs without sensitive information save_config = kwargs.copy() save_config["api_key"] = "********" if "api_key" in save_config else None @@ -548,6 +547,8 @@ async def generate_text( output_json_schema = convert_output_schema_to_json_schema(output_schema) elif output_json_schema is not None and output_json_schema.strip() != "": output_json_schema = json.loads(output_json_schema) + else: + raise ValueError("Invalid output schema", output_schema, output_json_schema) output_json_schema["additionalProperties"] = False # check if the model supports response format @@ -783,7 +784,7 @@ def convert_docx_to_xml(file_path: str) -> str: try: with docx2python(file_path) as docx_content: # Convert the document content to XML format - xml_content = f"\n\n" + xml_content = "\n\n" # Add metadata xml_content += "\n" diff --git a/backend/app/nodes/llm/generative/best_of_n.py b/backend/app/nodes/llm/generative/best_of_n.py index e695b015..54d50fbe 100644 --- a/backend/app/nodes/llm/generative/best_of_n.py +++ b/backend/app/nodes/llm/generative/best_of_n.py @@ -36,7 +36,7 @@ class BestOfNNodeConfig(SingleLLMCallNodeConfig, BaseSubworkflowNodeConfig): description="System message for the generation LLM", ) user_message: str = Field(default="", description="User message template") - output_schema: Dict[str, str] = Field(default={"response": "str"}) + output_schema: Dict[str, str] = Field(default={"response": "string"}) class BestOfNNodeInput(BaseNodeInput): @@ -107,7 +107,7 @@ def setup_subworkflow(self) -> None: "llm_info": self.config.llm_info.model_dump(), "system_message": self.config.rating_prompt, "user_message": "", - "output_schema": {"rating": "float"}, + "output_schema": {"rating": "number"}, }, ) nodes.append(rate_node) @@ -171,7 +171,6 @@ def setup_subworkflow(self) -> None: id=output_node_id, node_type="OutputNode", config={ - "output_schema": output_schema, "output_map": { f"{k}": f"pick_one_node.{k}" for k in output_schema.keys() }, diff --git a/backend/app/nodes/logic/coalesce.py b/backend/app/nodes/logic/coalesce.py index af7024ce..71e5ead0 100644 --- a/backend/app/nodes/logic/coalesce.py +++ b/backend/app/nodes/logic/coalesce.py @@ -50,31 +50,39 @@ async def run(self, input: BaseModel) -> BaseModel: for key in self.config.preferences: # {{ edit_1 }} if key in data and data[key] is not None: # Return the first non-None value according to preferences - output_model = create_model( # type: ignore + output_model = create_model( f"{self.name}", **{ k: (type(v), ...) for k, v in data[key].items() - }, # Only include the first non-null key # type: ignore + }, # Only include the first non-null key __base__=CoalesceNodeOutput, + __config__=None, + __module__=self.__module__, + __doc__=f"Output model for {self.name} node", + __validators__=None, + __cls_kwargs__=None, ) self.output_model = output_model first_non_null_output = data[key] - return self.output_model(**first_non_null_output) # type: ignore + return self.output_model(**first_non_null_output) # If all preferred values are None, check the rest of the data for key, value in data.items(): if value is not None: # Return the first non-None value immediately - output_model = create_model( # type: ignore + output_model = create_model( f"{self.name}", - **{ - key: (type(value), ...) - }, # Only include the first non-null key # type: ignore + **{key: (type(value), ...)}, # Only include the first non-null key __base__=CoalesceNodeOutput, + __config__=None, + __module__=self.__module__, + __doc__=f"Output model for {self.name} node", + __validators__=None, + __cls_kwargs__=None, ) self.output_model = output_model first_non_null_output[key] = value - return self.output_model(**first_non_null_output) # type: ignore + return self.output_model(**first_non_null_output) # If all values are None, return an empty output return None # type: ignore diff --git a/backend/app/nodes/logic/router.py b/backend/app/nodes/logic/router.py index 9139b873..a359465d 100644 --- a/backend/app/nodes/logic/router.py +++ b/backend/app/nodes/logic/router.py @@ -130,20 +130,33 @@ async def run(self, input: BaseModel) -> BaseModel: Evaluates conditions for each route in order. The first route that matches gets the input data. If no routes match, the first route acts as a default. """ - output_model = create_model( # type: ignore + output_model = create_model( f"{self.name}", - **{field_name: (field_type, ...) for field_name, field_type in input.model_fields.items()}, # type: ignore + __config__=None, __base__=RouterNodeOutput, + __doc__=f"Output model for {self.name} node", + __module__=self.__module__, + __validators__=None, + __cls_kwargs__=None, + **{ + field_name: (field_type, None) + for field_name, field_type in input.model_fields.items() + }, ) # Create fields for each route with Optional[input type] - route_fields = { # type: ignore - route_name: (Optional[output_model], None) # type: ignore + route_fields = { + route_name: (Optional[output_model], None) for route_name in self.config.route_map.keys() } - new_output_model = create_model( # type: ignore + new_output_model = create_model( f"{self.name}CompositeOutput", __base__=RouterNodeOutput, - **route_fields, # type: ignore + __config__=None, + __doc__=f"Composite output model for {self.name} node", + __module__=self.__module__, + __validators__=None, + __cls_kwargs__=None, + **route_fields, ) self.output_model = new_output_model @@ -151,9 +164,9 @@ async def run(self, input: BaseModel) -> BaseModel: for route_name, route in self.config.route_map.items(): if self._evaluate_route_conditions(input, route): - output[route_name] = output_model(**input.model_dump()) # type: ignore + output[route_name] = output_model(**input.model_dump()) - return self.output_model(**output) # type: ignore + return self.output_model(**output) if __name__ == "__main__": diff --git a/backend/app/nodes/loops/base_loop_subworkflow_node.py b/backend/app/nodes/loops/base_loop_subworkflow_node.py index 9c0e1128..f5d32934 100644 --- a/backend/app/nodes/loops/base_loop_subworkflow_node.py +++ b/backend/app/nodes/loops/base_loop_subworkflow_node.py @@ -1,6 +1,8 @@ from abc import abstractmethod from typing import Any, Dict, List -from pydantic import BaseModel +from pydantic import BaseModel, create_model + +from ..primitives.output import OutputNode from ..base import BaseNodeInput, BaseNodeOutput from ...execution.workflow_executor import WorkflowExecutor @@ -62,9 +64,10 @@ async def run_iteration(self, input: Dict[str, Any]) -> Dict[str, Any]: iteration_input = {**input, "loop_history": self.loop_outputs} # Execute the subworkflow - workflow_executor = WorkflowExecutor( - workflow=self.subworkflow, context=self.context + self._executor = WorkflowExecutor( + workflow=self.config.subworkflow, context=self.context ) + workflow_executor = self._executor outputs = await workflow_executor.run(iteration_input) # Convert outputs to dict format @@ -83,16 +86,6 @@ async def run_iteration(self, input: Dict[str, Any]) -> Dict[str, Any]: async def run(self, input: BaseModel) -> BaseModel: """Execute the loop subworkflow until stopping condition is met""" - # Create output model dynamically based on the schema of the output node - output_node = next( - node - for node in self.config.subworkflow.nodes - if node.node_type == "OutputNode" - ) - self.output_model = self.create_output_model_class( - output_node.config.get("output_schema", {}) - ) - current_input = self._map_input(input) # Run iterations until stopping condition is met @@ -103,5 +96,25 @@ async def run(self, input: BaseModel) -> BaseModel: self.subworkflow_output = self.loop_outputs + # create output model for the loop from the subworkflow output node's output_model + output_node = next( + node + for _id, node in self._executor.node_instances.items() + if issubclass(node.__class__, OutputNode) + ) + self.output_model = create_model( + f"{self.name}", + **{ + name: (field, ...) + for name, field in output_node.output_model.model_fields.items() + }, + __base__=BaseLoopSubworkflowNodeOutput, + __config__=None, + __module__=self.__module__, + __cls_kwargs__={"arbitrary_types_allowed": True}, + __doc__=None, + __validators__=None, + ) + # Return final state as BaseModel return self.output_model.model_validate(current_input) # type: ignore diff --git a/backend/app/nodes/node_types.py b/backend/app/nodes/node_types.py index 41b15641..b13f6d8b 100644 --- a/backend/app/nodes/node_types.py +++ b/backend/app/nodes/node_types.py @@ -41,11 +41,6 @@ "module": ".nodes.llm.generative.best_of_n", "class_name": "BestOfNNode", }, - { - "node_type_name": "BranchSolveMergeNode", - "module": ".nodes.llm.generative.branch_solve_merge", - "class_name": "BranchSolveMergeNode", - }, ], "Code Execution": [ { @@ -61,11 +56,11 @@ "module": ".nodes.loops.for_loop_node", "class_name": "ForLoopNode", }, - { - "node_type_name": "RetrieverNode", - "module": ".nodes.llm.retriever", - "class_name": "RetrieverNode", - }, + # { + # "node_type_name": "RetrieverNode", + # "module": ".nodes.llm.retriever", + # "class_name": "RetrieverNode", + # }, ], "Integrations": [ { @@ -182,6 +177,11 @@ "module": ".nodes.subworkflow.subworkflow_node", "class_name": "SubworkflowNode", }, + { + "node_type_name": "BranchSolveMergeNode", + "module": ".nodes.llm.generative.branch_solve_merge", + "class_name": "BranchSolveMergeNode", + }, ] diff --git a/backend/app/nodes/primitives/input.py b/backend/app/nodes/primitives/input.py index 2282dc9c..c8bf1e20 100644 --- a/backend/app/nodes/primitives/input.py +++ b/backend/app/nodes/primitives/input.py @@ -64,16 +64,17 @@ async def run(self, input: BaseModel) -> BaseModel: if self.config.enforce_schema: return input else: - fields = { - key: (value.annotation, ...) - for key, value in input.model_fields.items() - if value.annotation is not None - } + fields = {key: (value, ...) for key, value in input.model_fields.items()} - new_output_model = create_model( # type: ignore + new_output_model = create_model( "InputNodeOutput", __base__=InputNodeOutput, - **fields, # type: ignore + __config__=None, + __module__=self.__module__, + __doc__=f"Output model for {self.name} node", + __validators__=None, + __cls_kwargs__=None, + **fields, ) self.output_model = new_output_model ret_value = self.output_model.model_validate(input.model_dump()) # type: ignore diff --git a/backend/app/nodes/primitives/output.py b/backend/app/nodes/primitives/output.py index f04a2c40..3937b5e2 100644 --- a/backend/app/nodes/primitives/output.py +++ b/backend/app/nodes/primitives/output.py @@ -1,51 +1,80 @@ from typing import Any, Dict -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, create_model from ..base import ( - BaseNodeInput, + BaseNode, + BaseNodeConfig, BaseNodeOutput, - VariableOutputBaseNode, - VariableOutputBaseNodeConfig, + BaseNodeInput, ) from ...utils.pydantic_utils import get_nested_field -class OutputNodeConfig(VariableOutputBaseNodeConfig): +class OutputNodeConfig(BaseNodeConfig): """ - Configuration for the OutputNode. + Configuration for the OutputNode, focusing on mapping from input fields + (possibly nested via dot notation) to output fields. """ output_map: Dict[str, str] = Field( - default=dict[str, str](), + default_factory=dict, title="Output Map", - description="A dictionary mapping input field names to output field names.", + description="A dictionary mapping input field names (dot-notation allowed) to output field names.", ) -class OutputNodeInput(BaseNodeInput): - pass - - -class OutputNodeOutput(BaseNodeOutput): - pass - - -class OutputNode(VariableOutputBaseNode): +class OutputNode(BaseNode): """ - Node for defining output schema and using the input from other nodes. + Node for defining a typed output schema automatically by inferring it + from the output_map. If output_map is empty, it will simply pass the + entire input through unmodified. """ name = "output_node" display_name = "Output" config_model = OutputNodeConfig - input_model = OutputNodeInput - output_model = OutputNodeOutput + input_model = BaseNodeInput + output_model = BaseNodeOutput async def run(self, input: BaseModel) -> BaseModel: - output: Dict[str, Any] = {} + """ + Maps the incoming input fields (possibly nested) to the node's output + fields according to self.config.output_map. If no output_map is set, + returns the entire input as output. + + Args: + input (BaseModel): The input model (from predecessor nodes). + + Returns: + BaseModel: The node's typed output model instance. + """ if self.config.output_map: + # If user provided mappings, create a new model with the mapped fields + model_fields: Dict[str, Any] = {} for output_key, input_key in self.config.output_map.items(): - # input_key is the field name with dot notation to access nested fields - output[output_key] = get_nested_field(input_key, input) + model_fields[output_key] = ( + type(get_nested_field(field_name_with_dots=input_key, model=input)), + ..., + ) + self.output_model = create_model( + f"{self.name}", + **model_fields, + __base__=BaseNodeOutput, + __config__=None, + __module__=self.__module__, + ) else: - output = input.model_dump() - return self.output_model(**output) + # If user provided no mappings, just return everything + model_fields = {k: (type(v), ...) for k, v in input.model_dump().items()} + self.output_model = create_model( + f"{self.name}", + **model_fields, + __base__=BaseNodeOutput, + __config__=None, + __module__=self.__module__, + ) + + output_dict: Dict[str, Any] = {} + for output_key, input_key in self.config.output_map.items(): + output_dict[output_key] = get_nested_field(input_key, input) + + return self.output_model(**output_dict) diff --git a/backend/app/nodes/subworkflow/base_subworkflow_node.py b/backend/app/nodes/subworkflow/base_subworkflow_node.py index 47ce503e..9d534cfa 100644 --- a/backend/app/nodes/subworkflow/base_subworkflow_node.py +++ b/backend/app/nodes/subworkflow/base_subworkflow_node.py @@ -34,8 +34,6 @@ def setup_subworkflow(self) -> None: self._subworkflow_output_node = next( (node for node in self.subworkflow.nodes if node.node_type == "OutputNode") ) - output_schema = self._subworkflow_output_node.config["output_schema"] - self.output_model = self.create_output_model_class(output_schema) def _build_dependencies(self) -> Dict[str, Set[str]]: assert self.subworkflow is not None diff --git a/backend/app/schemas/run_schemas.py b/backend/app/schemas/run_schemas.py index 64801bfa..7e1b6b1e 100644 --- a/backend/app/schemas/run_schemas.py +++ b/backend/app/schemas/run_schemas.py @@ -6,11 +6,13 @@ from ..models.run_model import RunStatus from .task_schemas import TaskResponseSchema, TaskStatus + class StartRunRequestSchema(BaseModel): initial_inputs: Optional[Dict[str, Dict[str, Any]]] = None parent_run_id: Optional[str] = None files: Optional[Dict[str, List[str]]] = None # Maps node_id to list of file paths + class RunResponseSchema(BaseModel): id: str workflow_id: str @@ -38,11 +40,13 @@ def percentage_complete(self): class Config: from_attributes = True + class PartialRunRequestSchema(BaseModel): node_id: str rerun_predecessors: bool = False initial_inputs: Optional[Dict[str, Dict[str, Any]]] = None - partial_outputs: Optional[Dict[str, Dict[str, Any]]] = None + partial_outputs: Optional[Dict[str, Dict[str, Any] | List[Dict[str, Any]]]] = None + class BatchRunRequestSchema(BaseModel): dataset_id: str diff --git a/frontend/src/components/CodeEditor.tsx b/frontend/src/components/CodeEditor.tsx index c31bebf6..e1425335 100644 --- a/frontend/src/components/CodeEditor.tsx +++ b/frontend/src/components/CodeEditor.tsx @@ -5,7 +5,7 @@ import { json } from '@codemirror/lang-json' import { oneDark } from '@codemirror/theme-one-dark' import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button, useDisclosure } from '@heroui/react' import { Icon } from '@iconify/react' -import { linter, Diagnostic } from "@codemirror/lint" +import { linter, Diagnostic } from '@codemirror/lint' import { EditorView } from '@codemirror/view' import { syntaxTree } from '@codemirror/language' @@ -13,18 +13,27 @@ import { syntaxTree } from '@codemirror/language' const jsonErrorTheme = EditorView.baseTheme({ '.cm-json-error': { backgroundColor: '#ff000020', - borderBottom: '2px wavy #ff0000' - } + borderBottom: '2px wavy #ff0000', + }, }) interface CodeEditorProps { code: string - mode?: string + label?: string onChange: (value: string) => void + disabled?: boolean + mode?: 'json' | 'python' | 'javascript' // Add mode prop to determine which language to use readOnly?: boolean } -const CodeEditor: React.FC = ({ code, mode = 'javascript', onChange, readOnly = false }) => { +const CodeEditor: React.FC = ({ + code, + onChange, + disabled, + mode = 'javascript', + label = 'Code Editor', + readOnly = false, +}) => { const [value, setValue] = useState('') const { isOpen, onOpen, onOpenChange } = useDisclosure() const [modalValue, setModalValue] = useState('') @@ -39,12 +48,14 @@ const CodeEditor: React.FC = ({ code, mode = 'javascript', onCh return [] } catch (e) { if (!(e instanceof SyntaxError)) { - return [{ - from: 0, - to: doc.length, - severity: 'error', - message: 'Invalid JSON' - }] + return [ + { + from: 0, + to: doc.length, + severity: 'error', + message: 'Invalid JSON', + }, + ] } const message = e.message @@ -60,22 +71,26 @@ const CodeEditor: React.FC = ({ code, mode = 'javascript', onCh // If we can't determine precise position, highlight the current line if (start === end) { const line = view.state.doc.lineAt(pos) - return [{ - from: line.from, - to: line.to, - severity: 'error', - message: message, - markClass: 'cm-json-error' - }] + return [ + { + from: line.from, + to: line.to, + severity: 'error', + message: message, + markClass: 'cm-json-error', + }, + ] } - return [{ - from: start, - to: end, - severity: 'error', - message: message, - markClass: 'cm-json-error' - }] + return [ + { + from: start, + to: end, + severity: 'error', + message: message, + markClass: 'cm-json-error', + }, + ] } }) @@ -121,11 +136,11 @@ const CodeEditor: React.FC = ({ code, mode = 'javascript', onCh return (
- +
diff --git a/frontend/src/components/nodes/loops/groupNodeUtils.ts b/frontend/src/components/nodes/loops/groupNodeUtils.ts index 9470cd8f..028f7de2 100644 --- a/frontend/src/components/nodes/loops/groupNodeUtils.ts +++ b/frontend/src/components/nodes/loops/groupNodeUtils.ts @@ -219,8 +219,8 @@ export const createDynamicGroupNodeWithChildren = ( 'InputNode', `${id}_input`, { - x: position.x + 50, - y: position.y + 300, // position.y + (height/2) + x: 0, + y: 300, // position.y + (height/2) }, loopNodeAndConfig.node.id ) @@ -231,8 +231,8 @@ export const createDynamicGroupNodeWithChildren = ( 'OutputNode', `${id}_output`, { - x: position.x + 950, // position.x + width - 250 - y: position.y + 300, // position.y + (height/2) + x: 950, // position.x + width - 250 + y: 300, // position.y + (height/2) }, loopNodeAndConfig.node.id ) diff --git a/frontend/src/components/nodes/nodeSidebar/NodeSidebar.tsx b/frontend/src/components/nodes/nodeSidebar/NodeSidebar.tsx index 8969f214..9612dec7 100644 --- a/frontend/src/components/nodes/nodeSidebar/NodeSidebar.tsx +++ b/frontend/src/components/nodes/nodeSidebar/NodeSidebar.tsx @@ -827,7 +827,10 @@ const NodeSidebar: React.FC = ({ nodeID }) => { if (key.endsWith('_prompt') || key.endsWith('_message') || key.endsWith('_template')) { const title = key.endsWith('_template') - ? key.slice(0, -9).replace(/_/g, ' ').replace(/\b\w/g, (char) => char.toUpperCase()) + ? key + .slice(0, -9) + .replace(/_/g, ' ') + .replace(/\b\w/g, (char) => char.toUpperCase()) : key.replace(/_/g, ' ').replace(/\b\w/g, (char) => char.toUpperCase()) return (
diff --git a/frontend/src/utils/JSPydanticModel.js b/frontend/src/utils/JSPydanticModel.js index f048a2e7..2200c222 100644 --- a/frontend/src/utils/JSPydanticModel.js +++ b/frontend/src/utils/JSPydanticModel.js @@ -1,5 +1,3 @@ -// frontend/src/utils/JSPydanticModel.js - import Ajv from 'ajv' import addFormats from 'ajv-formats' @@ -81,6 +79,15 @@ class JSPydanticModel { const validator = this.ajv.compile(node[key]) const obj = {} validator(obj) + + // For config, include all properties defined in the schema, even without defaults + if (key === 'config' && node[key].properties) { + Object.keys(node[key].properties).forEach(propKey => { + if (!(propKey in obj)) { + obj[propKey] = null; + } + }); + } // Merge the validated object with any existing fields for non-conditional nodes processedNode[key] = {