From 3cf2b9992de8b49da1d01bd0eae30b9f7b89e6c3 Mon Sep 17 00:00:00 2001 From: Ramiro Medina <64783088+ramedina86@users.noreply.github.com> Date: Wed, 16 Oct 2024 12:43:08 +0000 Subject: [PATCH 01/30] feat: Function calling --- src/writer/workflows_blocks/setstate.py | 1 + src/writer/workflows_blocks/writerchat.py | 79 +++++++++++++++++++++-- 2 files changed, 73 insertions(+), 7 deletions(-) diff --git a/src/writer/workflows_blocks/setstate.py b/src/writer/workflows_blocks/setstate.py index f354ee4b..389c8a22 100644 --- a/src/writer/workflows_blocks/setstate.py +++ b/src/writer/workflows_blocks/setstate.py @@ -45,6 +45,7 @@ def run(self): element = self._get_field("element") value = self._get_field("value") self.evaluator.set_state(element, self.instance_path, value, base_context=self.execution_env) + self.result = value self.outcome = "success" except BaseException as e: self.outcome = "error" diff --git a/src/writer/workflows_blocks/writerchat.py b/src/writer/workflows_blocks/writerchat.py index 7e8c8ff9..21d33816 100644 --- a/src/writer/workflows_blocks/writerchat.py +++ b/src/writer/workflows_blocks/writerchat.py @@ -1,9 +1,33 @@ from writer.abstract import register_abstract_template from writer.ss_types import AbstractTemplate from writer.workflows_blocks.blocks import WorkflowBlock +import logging + +logging.basicConfig(level=logging.DEBUG) DEFAULT_MODEL = "palmyra-x-004" +function_tools_init = """{ + "estimate_customer_risk": { + "parameters": { + "time": {"type": "float", "description": "How many months they've been a customer for"}, + "transactions": {"type": "float", "description": "How many transactions they've performed"} + } + }, + "get_employee_info": { + "parameters": { + "id": {"type": "float", "description": "Id of the employee"}, + } + } + +}""" + +def get_latitude_and_longitude(city): + return "lat: 52.40692, lon: 16.92993" + +def get_weather(latitude, longitude): + return "37c" + class WriterChat(WorkflowBlock): @classmethod @@ -30,9 +54,18 @@ def register(cls, type: str): "name": "Temperature", "type": "Number", "default": "0.7" + }, + "functionTools": { + "name": "Function tools", + "type": "Object", + "default": "{}", + "init": function_tools_init } }, "outs": { + "$dynamic": { + "field": "functionTools" + }, "success": { "name": "Success", "description": "If the function doesn't raise an Exception.", @@ -47,6 +80,14 @@ def register(cls, type: str): } )) + def run_branch(self, outcome: str, **args): + print(f"Executing {outcome} with args {repr(args)}") + if outcome == "$dynamic_get_employee_info": + if args.get("employee_id") == 4: + return "The name of the employee is Jackson Koko and they're 35 years old they're manager of internal affairs" + else: + return "The name of the employee is Williams Bernard and they're 40 years old they're manager of public relations" + def run(self): try: import writer.ai @@ -55,6 +96,29 @@ def run(self): temperature = float(self._get_field("temperature", False, "0.7")) model_id = self._get_field("modelId", False, default_field_value=DEFAULT_MODEL) config = { "temperature": temperature, "model": model_id} + function_tools_raw = self._get_field("functionTools", True) + tools = [] + + for tool_name, tool_raw in function_tools_raw.items(): + # callable = None + # if tool_name == "get_weather": + # callable = get_weather + # elif tool_name == "get_latitude_and_longitude": + # callable = get_latitude_and_longitude + # else: + # raise ValueError("unrecognised") + tool = writer.ai.FunctionTool( + type="function", + name=tool_name, + # callable=callable, + callable=lambda **args: self.run_branch(f"$dynamic_{tool_name}", **args), + parameters=tool_raw.get("parameters") + ) + tr = repr(tool_raw.get("parameters")) + print(f"appended {tool_name} {tr}") + tools.append(tool) + + # print(repr(tools)) conversation = self.evaluator.evaluate_expression(conversation_state_element, self.instance_path, self.execution_env) @@ -62,15 +126,16 @@ def run(self): conversation = writer.ai.Conversation(config=config) self.evaluator.set_state(conversation_state_element, self.instance_path, conversation, base_context=self.execution_env) - # for chunk in conversation.stream_complete(): - # if chunk.get("content") is None: - # chunk["content"] = "" - # conversation += chunk + for chunk in conversation.stream_complete(tools=tools): + if chunk.get("content") is None: + chunk["content"] = "" + conversation += chunk + self.evaluator.set_state(conversation_state_element, self.instance_path, conversation, base_context=self.execution_env) - msg = conversation.complete() - conversation += msg + # msg = conversation.complete(tools=tools) + # conversation += msg self.evaluator.set_state(conversation_state_element, self.instance_path, conversation, base_context=self.execution_env) - self.result = msg + # self.result = msg self.outcome = "success" except BaseException as e: self.outcome = "error" From c98c121dd32fb8a28dc38ecf221f2c03d83abe2e Mon Sep 17 00:00:00 2001 From: mmikita95 Date: Wed, 16 Oct 2024 15:17:47 +0400 Subject: [PATCH 02/30] fix: switch to `create_function_call` function due to lack of default `type` --- docs/framework/ai-module.mdx | 10 +++++-- src/writer/ai.py | 53 ++++++++++++++++++++++++++++++------ 2 files changed, 52 insertions(+), 11 deletions(-) diff --git a/docs/framework/ai-module.mdx b/docs/framework/ai-module.mdx index 857d6147..4754f572 100644 --- a/docs/framework/ai-module.mdx +++ b/docs/framework/ai-module.mdx @@ -173,13 +173,13 @@ Framework allows you to register Python functions that can be called automatical Function tools are defined using either a Python class or a JSON configuration. ```python -from writer.ai import FunctionTool +from writer.ai import create_function_tool # Define a function tool with Python callable def calculate_interest(principal: float, rate: float, time: float): return principal * rate * time -tool = FunctionTool( +tool = create_function_tool( name="calculate_interest", callable=calculate_interest, parameters={ @@ -217,6 +217,12 @@ Function tools require the following properties: When a conversation involves a tool (either a graph or a function), Framework automatically handles the requests from LLM to use the tools during interactions. If the tool needs multiple steps (for example, querying data and processing it), Framework will handle those steps recursively, calling functions as needed until the final result is returned. +By default, to prevent endless recursion, Framework will only handle 3 consecutive tool calls. You can expand it in case it doesn't suit your case – both `complete()` and `stream_complete()` accept a `max_tool_depth` parameter, which configures the maximum allowed recursion depth: + +```python +response = conversation.complete(tools=tool, max_tool_depth=7) +``` + ### Providing a Tool or a List of Tools You can pass either a single tool or a list of tools to the `complete()` or `stream_complete()` methods. The tools can be a combination of FunctionTool, Graph, or JSON-defined tools. diff --git a/src/writer/ai.py b/src/writer/ai.py index f304fa67..f01cd67c 100644 --- a/src/writer/ai.py +++ b/src/writer/ai.py @@ -90,9 +90,26 @@ class GraphTool(Tool): class FunctionTool(Tool): callable: Callable name: str + description: Optional[str] parameters: Dict[str, Dict[str, str]] +def create_function_tool( + callable: Callable, + name: str, + parameters: Optional[Dict[str, Dict[str, str]]], + description: Optional[str] = None +) -> FunctionTool: + parameters = parameters or {} + return FunctionTool( + type="function", + callable=callable, + name=name, + description=description, + parameters=parameters + ) + + logger = logging.Logger(__name__, level=logging.DEBUG) @@ -1190,7 +1207,7 @@ def _execute_function_tool_call(self, index: int) -> dict: "role": "tool", "name": function_name, "tool_call_id": tool_call_id, - "content": f"{function_name}: {func_result}" + "content": f"{func_result}" } return follow_up_message @@ -1223,7 +1240,18 @@ def _process_tool_call(self, index, tool_call_id, tool_call_name, tool_call_argu # Accumulate arguments across chunks if tool_call_arguments is not None: - self._ongoing_tool_calls[index]["arguments"] += tool_call_arguments + if ( + tool_call_arguments.startswith("{") + and tool_call_arguments.endswith("}") + ): + # For cases when LLM "bugs" and returns + # the whole arguments string as a last chunk + fixed_chunk = tool_call_arguments.rsplit("{")[-1] + self._ongoing_tool_calls[index]["arguments"] = \ + "{" + fixed_chunk + else: + # Process normally + self._ongoing_tool_calls[index]["arguments"] += tool_call_arguments # Check if we have all necessary data to execute the function if ( @@ -1271,9 +1299,10 @@ def _process_response_data( passed_messages: List[WriterAIMessage], request_model: str, request_data: ChatOptions, - depth=1 + depth=1, + max_depth=3 ) -> 'Conversation.Message': - if depth > 3: + if depth > max_depth: raise RuntimeError("Reached maximum depth when processing response data tool calls.") for entry in response_data.choices: message = entry.message @@ -1322,9 +1351,10 @@ def _process_stream_response( request_model: str, request_data: ChatOptions, depth=1, + max_depth=3, flag_chunks=False ) -> Generator[dict, None, None]: - if depth > 3: + if depth > max_depth: raise RuntimeError("Reached maximum depth when processing response data tool calls.") # We avoid flagging first chunk # to trigger creating a message @@ -1361,6 +1391,7 @@ def _process_stream_response( request_model=request_model, request_data=request_data, depth=depth+1, + max_depth=max_depth, flag_chunks=True ) finally: @@ -1384,7 +1415,8 @@ def complete( FunctionTool, List[Union[Graph, GraphTool, FunctionTool]] ] # can be an instance of tool or a list of instances - ] = None + ] = None, + max_tool_depth: int = 3, ) -> 'Conversation.Message': """ Processes the conversation with the current messages and additional data to generate a response. @@ -1421,7 +1453,8 @@ def complete( response_data, passed_messages=passed_messages, request_model=request_model, - request_data=request_data + request_data=request_data, + max_depth=max_tool_depth ) def stream_complete( @@ -1434,7 +1467,8 @@ def stream_complete( FunctionTool, List[Union[Graph, GraphTool, FunctionTool]] ] # can be an instance of tool or a list of instances - ] = None + ] = None, + max_tool_depth: int = 3 ) -> Generator[dict, None, None]: """ Initiates a stream to receive chunks of the model's reply. @@ -1474,7 +1508,8 @@ def stream_complete( response=response, passed_messages=passed_messages, request_model=request_model, - request_data=request_data + request_data=request_data, + max_depth=max_tool_depth ) response.close() From 8113e175b3bf8cb3a58f72057ecd220530281e17 Mon Sep 17 00:00:00 2001 From: mmikita95 Date: Wed, 16 Oct 2024 16:30:35 +0400 Subject: [PATCH 03/30] fix: optimized clearing registry and tool calls pool --- src/writer/ai.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/writer/ai.py b/src/writer/ai.py index f01cd67c..eba5d976 100644 --- a/src/writer/ai.py +++ b/src/writer/ai.py @@ -896,6 +896,9 @@ def _clear_callable_registry(self): self._callable_registry = {} def _clear_ongoing_tool_calls(self): + """ + Clear ongoing tool calls after they've been processed + """ self._ongoing_tool_calls = {} def _clear_tool_calls_helpers(self): @@ -1328,11 +1331,10 @@ def _process_response_data( ) logger.debug(f"Received response – {follow_up_response}") - # Clear buffer and callable registry for the completed tool call - self._clear_tool_calls_helpers() - # Call the function recursively to either process a new tool call # or return the message if no tool calls are requested + + self._clear_ongoing_tool_calls() return self._process_response_data( follow_up_response, passed_messages=passed_messages, @@ -1382,9 +1384,8 @@ def _process_stream_response( ) ) - # Clear buffer and callable registry for the completed tool call try: - self._clear_tool_calls_helpers() + self._clear_ongoing_tool_calls() yield from self._process_stream_response( response=follow_up_response, passed_messages=passed_messages, @@ -1449,7 +1450,7 @@ def complete( ) ) - return self._process_response_data( + response = self._process_response_data( response_data, passed_messages=passed_messages, request_model=request_model, @@ -1457,6 +1458,11 @@ def complete( max_depth=max_tool_depth ) + # Clear buffer and callable registry for the completed tool call + self._clear_tool_calls_helpers() + + return response + def stream_complete( self, config: Optional['ChatOptions'] = None, @@ -1512,6 +1518,8 @@ def stream_complete( max_depth=max_tool_depth ) + # Clear buffer and callable registry for the completed tool call + self._clear_tool_calls_helpers() response.close() @property From 4a5013d4092af5c07c8bcd87d581dba5a66916ac Mon Sep 17 00:00:00 2001 From: Ramiro Medina <64783088+ramedina86@users.noreply.github.com> Date: Mon, 21 Oct 2024 12:40:49 +0000 Subject: [PATCH 04/30] feat: Function calling support --- src/writer/workflows.py | 31 +++++--- src/writer/workflows_blocks/blocks.py | 11 +++ src/writer/workflows_blocks/writerchat.py | 86 ++++++++++++----------- 3 files changed, 76 insertions(+), 52 deletions(-) diff --git a/src/writer/workflows.py b/src/writer/workflows.py index 51075267..5283c911 100644 --- a/src/writer/workflows.py +++ b/src/writer/workflows.py @@ -1,4 +1,3 @@ -import logging from typing import Dict, List, Literal, Tuple import writer.core @@ -22,9 +21,10 @@ def run_workflow_by_key(session, workflow_key: str, execution_env: Dict): def run_workflow(session, component_id: str, execution_env: Dict): execution: Dict[str, WorkflowBlock] = {} + nodes = _get_workflow_nodes(component_id) try: - for node in _get_terminal_nodes(component_id): - _run_node(node, execution, session, execution_env) + for node in get_terminal_nodes(nodes): + run_node(node, nodes, execution, session, execution_env) except BaseException as e: _generate_run_log(session, execution, "error") raise e @@ -43,16 +43,14 @@ def _generate_run_log(session: "writer.core.WriterSession", execution: Dict[str, state.add_log_entry(entry_type, "Workflow execution", msg, workflow_execution=exec_log) -def _get_terminal_nodes(component_id): - nodes = _get_workflow_nodes(component_id) +def get_terminal_nodes(nodes): return [node for node in nodes if not node.outs] -def _get_node_dependencies(target_node: "Component"): +def _get_node_dependencies(target_node: "Component", nodes: List["Component"]): dependencies:List[Tuple] = [] parent_id = target_node.parentId if not parent_id: - return dependencies - nodes = _get_workflow_nodes(parent_id) + return [] for node in nodes: if not node.outs: continue @@ -64,6 +62,16 @@ def _get_node_dependencies(target_node: "Component"): return dependencies +def get_branch_nodes(root_node_id: "Component"): + root_node = writer.core.base_component_tree.get_component(root_node_id) + branch_nodes = [root_node] + if not root_node.outs: + return branch_nodes + for out in root_node.outs: + branch_nodes += get_branch_nodes(out.get("toNodeId")) + return branch_nodes + + def _is_outcome_managed(target_node: "Component", target_out_id: str): if not target_node.outs: return False @@ -72,11 +80,12 @@ def _is_outcome_managed(target_node: "Component", target_out_id: str): return True return False -def _run_node(target_node: "Component", execution: Dict, session: "writer.core.WriterSession", execution_env: Dict): + +def run_node(target_node: "Component", nodes: List["Component"], execution: Dict, session: "writer.core.WriterSession", execution_env: Dict): tool_class = writer.workflows_blocks.blocks.block_map.get(target_node.type) if not tool_class: raise RuntimeError(f"Couldn't find tool for {target_node.type}.") - dependencies = _get_node_dependencies(target_node) + dependencies = _get_node_dependencies(target_node, nodes) tool = execution.get(target_node.id) if tool: @@ -85,7 +94,7 @@ def _run_node(target_node: "Component", execution: Dict, session: "writer.core.W result = None matched_dependencies = 0 for node, out_id in dependencies: - tool = _run_node(node, execution, session, execution_env) + tool = run_node(node, nodes, execution, session, execution_env) if not tool: continue if tool.outcome == out_id: diff --git a/src/writer/workflows_blocks/blocks.py b/src/writer/workflows_blocks/blocks.py index b844532c..767e67af 100644 --- a/src/writer/workflows_blocks/blocks.py +++ b/src/writer/workflows_blocks/blocks.py @@ -23,6 +23,17 @@ def __init__(self, component: "writer.core_ui.Component", execution: Dict, sessi self.evaluator = writer.core.Evaluator(session.session_state, session.session_component_tree) self.instance_path: InstancePath = [{"componentId": self.component.id, "instanceNumber": 0}] + def _get_nodes_at_outcome(self, target_outcome: str): + outs = self.component.outs + nodes = [] + if not outs: + return nodes + for out in outs: + if out.get("outId") == target_outcome: + component_id = out.get("toNodeId") + nodes.append(writer.core.base_component_tree.get_component(component_id)) + return nodes + def _get_field(self, field_key: str, as_json=False, default_field_value=None): if default_field_value is None: if as_json: diff --git a/src/writer/workflows_blocks/writerchat.py b/src/writer/workflows_blocks/writerchat.py index 21d33816..e531c1e2 100644 --- a/src/writer/workflows_blocks/writerchat.py +++ b/src/writer/workflows_blocks/writerchat.py @@ -1,19 +1,11 @@ +import writer.workflows from writer.abstract import register_abstract_template from writer.ss_types import AbstractTemplate from writer.workflows_blocks.blocks import WorkflowBlock -import logging - -logging.basicConfig(level=logging.DEBUG) DEFAULT_MODEL = "palmyra-x-004" function_tools_init = """{ - "estimate_customer_risk": { - "parameters": { - "time": {"type": "float", "description": "How many months they've been a customer for"}, - "transactions": {"type": "float", "description": "How many transactions they've performed"} - } - }, "get_employee_info": { "parameters": { "id": {"type": "float", "description": "Id of the employee"}, @@ -22,12 +14,6 @@ }""" -def get_latitude_and_longitude(city): - return "lat: 52.40692, lon: 16.92993" - -def get_weather(latitude, longitude): - return "37c" - class WriterChat(WorkflowBlock): @classmethod @@ -81,12 +67,24 @@ def register(cls, type: str): )) def run_branch(self, outcome: str, **args): - print(f"Executing {outcome} with args {repr(args)}") - if outcome == "$dynamic_get_employee_info": - if args.get("employee_id") == 4: - return "The name of the employee is Jackson Koko and they're 35 years old they're manager of internal affairs" - else: - return "The name of the employee is Williams Bernard and they're 40 years old they're manager of public relations" + branch_root_nodes = self._get_nodes_at_outcome(outcome) + result = None + for branch_root_node in branch_root_nodes: + branch_nodes = writer.workflows.get_branch_nodes(branch_root_node.id) + + terminal_nodes = writer.workflows.get_terminal_nodes(branch_nodes) + + for terminal_node in terminal_nodes: + tool = writer.workflows.run_node(terminal_node, branch_nodes, self.execution, self.session, self.execution_env | args) + if tool: + result = tool.result + + return repr(result) + + def _make_callable(self, tool_name: str): + def callable(**args): + return self.run_branch(f"$dynamic_{tool_name}", **args) + return callable def run(self): try: @@ -100,25 +98,19 @@ def run(self): tools = [] for tool_name, tool_raw in function_tools_raw.items(): - # callable = None - # if tool_name == "get_weather": - # callable = get_weather - # elif tool_name == "get_latitude_and_longitude": - # callable = get_latitude_and_longitude - # else: - # raise ValueError("unrecognised") tool = writer.ai.FunctionTool( type="function", name=tool_name, - # callable=callable, - callable=lambda **args: self.run_branch(f"$dynamic_{tool_name}", **args), + description=tool_raw.get("description"), + callable=self._make_callable(tool_name), parameters=tool_raw.get("parameters") ) - tr = repr(tool_raw.get("parameters")) - print(f"appended {tool_name} {tr}") tools.append(tool) - # print(repr(tools)) + # tools.append({ + # "type": "graph", + # "graph_ids": ["ddf83cb3-da4e-4bbd-b721-19a62c8f0ef8"] + # }) conversation = self.evaluator.evaluate_expression(conversation_state_element, self.instance_path, self.execution_env) @@ -126,16 +118,28 @@ def run(self): conversation = writer.ai.Conversation(config=config) self.evaluator.set_state(conversation_state_element, self.instance_path, conversation, base_context=self.execution_env) - for chunk in conversation.stream_complete(tools=tools): - if chunk.get("content") is None: - chunk["content"] = "" - conversation += chunk - self.evaluator.set_state(conversation_state_element, self.instance_path, conversation, base_context=self.execution_env) + # msg = "" + # for chunk in conversation.stream_complete(tools=tools): + # if chunk.get("content") is None: + # chunk["content"] = "" + # msg += chunk.get("content") + # conversation += chunk + # self.evaluator.set_state(conversation_state_element, self.instance_path, conversation, base_context=self.execution_env) + + msg = None + try: + msg = conversation.complete(tools=tools) + except BaseException as e: + msg = { + "role": "assistant", + "content": "Couldn't process the request." + } + raise e + finally: + conversation += msg - # msg = conversation.complete(tools=tools) - # conversation += msg self.evaluator.set_state(conversation_state_element, self.instance_path, conversation, base_context=self.execution_env) - # self.result = msg + self.result = msg self.outcome = "success" except BaseException as e: self.outcome = "error" From 9728f0ed42a06ed6a9e39b4a3cb4bba69b545604 Mon Sep 17 00:00:00 2001 From: Ramiro Medina <64783088+ramedina86@users.noreply.github.com> Date: Sat, 26 Oct 2024 09:35:35 +0000 Subject: [PATCH 05/30] feat: Function calling support --- src/writer/workflows.py | 16 +++++--- src/writer/workflows_blocks/__init__.py | 4 +- src/writer/workflows_blocks/blocks.py | 1 + src/writer/workflows_blocks/returnvalue.py | 47 ++++++++++++++++++++++ src/writer/workflows_blocks/writerchat.py | 8 ++-- 5 files changed, 65 insertions(+), 11 deletions(-) create mode 100644 src/writer/workflows_blocks/returnvalue.py diff --git a/src/writer/workflows.py b/src/writer/workflows.py index 5283c911..a11881c9 100644 --- a/src/writer/workflows.py +++ b/src/writer/workflows.py @@ -1,4 +1,4 @@ -from typing import Dict, List, Literal, Tuple +from typing import Any, Dict, List, Literal, Optional, Tuple import writer.core import writer.workflows_blocks @@ -22,23 +22,27 @@ def run_workflow_by_key(session, workflow_key: str, execution_env: Dict): def run_workflow(session, component_id: str, execution_env: Dict): execution: Dict[str, WorkflowBlock] = {} nodes = _get_workflow_nodes(component_id) + return_value = None try: for node in get_terminal_nodes(nodes): - run_node(node, nodes, execution, session, execution_env) + tool = run_node(node, nodes, execution, session, execution_env) + for component_id, tool in execution.items(): + if tool and tool.return_value: + return_value = tool.return_value except BaseException as e: _generate_run_log(session, execution, "error") raise e else: - _generate_run_log(session, execution, "info") + _generate_run_log(session, execution, "info", return_value) -def _generate_run_log(session: "writer.core.WriterSession", execution: Dict[str, WorkflowBlock], entry_type: Literal["info", "error"]): - msg = "" +def _generate_run_log(session: "writer.core.WriterSession", execution: Dict[str, WorkflowBlock], entry_type: Literal["info", "error"], return_value: Optional[Any] = None): exec_log = [] for component_id, tool in execution.items(): exec_log.append({ "componentId": component_id, - "outcome": tool.outcome + "outcome": tool.outcome + repr(tool.return_value) + repr(tool.result) }) + msg = f"Execution finished with value {repr(return_value)}" state = session.session_state state.add_log_entry(entry_type, "Workflow execution", msg, workflow_execution=exec_log) diff --git a/src/writer/workflows_blocks/__init__.py b/src/writer/workflows_blocks/__init__.py index 4a933d4b..84cd448f 100644 --- a/src/writer/workflows_blocks/__init__.py +++ b/src/writer/workflows_blocks/__init__.py @@ -11,6 +11,7 @@ from writer.workflows_blocks.writerclassification import WriterClassification from writer.workflows_blocks.writercompletion import WriterCompletion from writer.workflows_blocks.writernocodeapp import WriterNoCodeApp +from writer.workflows_blocks.returnvalue import ReturnValue SetState.register("workflows_setstate") WriterClassification.register("workflows_writerclassification") @@ -24,4 +25,5 @@ WriterAddChatMessage.register("workflows_writeraddchatmessage") ParseJSON.register("workflows_parsejson") CallEventHandler.register("workflows_calleventhandler") -AddToStateList.register("workflows_addtostatelist") \ No newline at end of file +AddToStateList.register("workflows_addtostatelist") +ReturnValue.register("workflows_returnvalue") \ No newline at end of file diff --git a/src/writer/workflows_blocks/blocks.py b/src/writer/workflows_blocks/blocks.py index 767e67af..87acb9dd 100644 --- a/src/writer/workflows_blocks/blocks.py +++ b/src/writer/workflows_blocks/blocks.py @@ -20,6 +20,7 @@ def __init__(self, component: "writer.core_ui.Component", execution: Dict, sessi self.session = session self.execution_env = execution_env self.result = None + self.return_value = None self.evaluator = writer.core.Evaluator(session.session_state, session.session_component_tree) self.instance_path: InstancePath = [{"componentId": self.component.id, "instanceNumber": 0}] diff --git a/src/writer/workflows_blocks/returnvalue.py b/src/writer/workflows_blocks/returnvalue.py new file mode 100644 index 00000000..8f8932e5 --- /dev/null +++ b/src/writer/workflows_blocks/returnvalue.py @@ -0,0 +1,47 @@ +import writer.workflows +from writer.abstract import register_abstract_template +from writer.ss_types import AbstractTemplate +from writer.workflows_blocks.blocks import WorkflowBlock + +class ReturnValue(WorkflowBlock): + + @classmethod + def register(cls, type: str): + super(ReturnValue, cls).register(type) + register_abstract_template(type, AbstractTemplate( + baseType="workflows_node", + writer={ + "name": "Return value", + "description": "Returns a value from a workflow or sub-workflow.", + "category": "Writer", + "fields": { + "value": { + "name": "Value", + "type": "Text", + "control": "Textarea" + }, + }, + "outs": { + "success": { + "name": "Success", + "description": "If the function doesn't raise an Exception.", + "style": "success", + }, + "error": { + "name": "Error", + "description": "If the function raises an Exception.", + "style": "error", + }, + }, + } + )) + + def run(self): + try: + value = self._get_field("value") + self.result = value + self.return_value = value + self.outcome = "success" + except BaseException as e: + self.outcome = "error" + raise e \ No newline at end of file diff --git a/src/writer/workflows_blocks/writerchat.py b/src/writer/workflows_blocks/writerchat.py index e531c1e2..730b9ffb 100644 --- a/src/writer/workflows_blocks/writerchat.py +++ b/src/writer/workflows_blocks/writerchat.py @@ -68,7 +68,7 @@ def register(cls, type: str): def run_branch(self, outcome: str, **args): branch_root_nodes = self._get_nodes_at_outcome(outcome) - result = None + return_value = None for branch_root_node in branch_root_nodes: branch_nodes = writer.workflows.get_branch_nodes(branch_root_node.id) @@ -76,10 +76,10 @@ def run_branch(self, outcome: str, **args): for terminal_node in terminal_nodes: tool = writer.workflows.run_node(terminal_node, branch_nodes, self.execution, self.session, self.execution_env | args) - if tool: - result = tool.result + if tool and tool.return_value: + return_value = tool.return_value - return repr(result) + return repr(return_value) def _make_callable(self, tool_name: str): def callable(**args): From 48759f2cb1e28e975dae403a2dba2c6a0377cd0a Mon Sep 17 00:00:00 2001 From: Ramiro Medina <64783088+ramedina86@users.noreply.github.com> Date: Sat, 26 Oct 2024 09:35:59 +0000 Subject: [PATCH 06/30] feat: Tools field --- src/ui/src/builder/BuilderFieldsTools.vue | 40 +++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 src/ui/src/builder/BuilderFieldsTools.vue diff --git a/src/ui/src/builder/BuilderFieldsTools.vue b/src/ui/src/builder/BuilderFieldsTools.vue new file mode 100644 index 00000000..8ce6e3a6 --- /dev/null +++ b/src/ui/src/builder/BuilderFieldsTools.vue @@ -0,0 +1,40 @@ + + + + + + + From 2ad1a757f5d084bc3b2588e7bc12f37c2d960d11 Mon Sep 17 00:00:00 2001 From: Ramiro Medina <64783088+ramedina86@users.noreply.github.com> Date: Sat, 26 Oct 2024 09:36:51 +0000 Subject: [PATCH 07/30] fix: Copy paste for workflows --- src/ui/src/builder/useComponentActions.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/ui/src/builder/useComponentActions.ts b/src/ui/src/builder/useComponentActions.ts index 3c3294f3..a7382bbd 100644 --- a/src/ui/src/builder/useComponentActions.ts +++ b/src/ui/src/builder/useComponentActions.ts @@ -489,6 +489,11 @@ export function useComponentActions(wf: Core, ssbm: BuilderManager) { if (nc.parentId == c.id) { nc.parentId = newId; } + nc.outs?.forEach((out) => { + if (out.toNodeId == c.id) { + out.toNodeId = newId; + } + }); }); c.id = newId; }); @@ -524,6 +529,15 @@ export function useComponentActions(wf: Core, ssbm: BuilderManager) { if (clipboard === null) return; const { operation, jsonSubtree } = clipboard; const subtree = JSON.parse(jsonSubtree); + + const rootComponent = subtree[0]; + if ( + typeof rootComponent.outs !== "undefined" && + rootComponent.parentId !== targetParentId + ) { + rootComponent.outs = []; + } + if (operation == ClipboardOperation.Cut) return pasteCutComponent(targetParentId, subtree); if (operation == ClipboardOperation.Copy) @@ -541,6 +555,7 @@ export function useComponentActions(wf: Core, ssbm: BuilderManager) { ssbm.setClipboard(null); const rootComponent = subtree[0]; + rootComponent.parentId = targetParentId; rootComponent.position = getNextInsertionPosition( targetParentId, From 8bd9ba137934b659bcdbded6f4e825447868ceed Mon Sep 17 00:00:00 2001 From: Ramiro Medina <64783088+ramedina86@users.noreply.github.com> Date: Sat, 26 Oct 2024 09:39:35 +0000 Subject: [PATCH 08/30] chore: Refactor, improved connection interaction --- .../workflows/WorkflowsWorkflow.vue | 287 ++++++++++++++---- .../workflows/abstract/WorkflowsNode.vue | 21 +- 2 files changed, 247 insertions(+), 61 deletions(-) diff --git a/src/ui/src/components/workflows/WorkflowsWorkflow.vue b/src/ui/src/components/workflows/WorkflowsWorkflow.vue index 5c28dbe7..450bf6f2 100644 --- a/src/ui/src/components/workflows/WorkflowsWorkflow.vue +++ b/src/ui/src/components/workflows/WorkflowsWorkflow.vue @@ -2,11 +2,13 @@
@@ -18,26 +20,36 @@ :is-engaged=" selectedArrow == arrowId || wfbm.getSelectedId() == arrow.fromNodeId || - wfbm.getSelectedId() == arrow.out.toNodeId + wfbm.getSelectedId() == arrow.toNodeId " @click="handleArrowClick($event, arrowId)" @delete="handleDeleteClick($event, arrow)" > + @@ -92,8 +104,9 @@ export type WorkflowArrowData = { y2: number; color: string; fromNodeId: Component["id"]; - out: Component["outs"][number]; - isEngaged: boolean; + fromOutId: Component["outs"][number]["outId"]; + toNodeId?: Component["id"]; + isEngaged?: boolean; }; + + From 2b94f3841d04381b6739ca2c2e4ce483c0640d36 Mon Sep 17 00:00:00 2001 From: Ramiro Medina <64783088+ramedina86@users.noreply.github.com> Date: Mon, 28 Oct 2024 10:59:53 +0000 Subject: [PATCH 14/30] chore: Tool field --- src/ui/src/builder/BuilderFieldsTools.vue | 260 +++++++++++++++++++++- 1 file changed, 251 insertions(+), 9 deletions(-) diff --git a/src/ui/src/builder/BuilderFieldsTools.vue b/src/ui/src/builder/BuilderFieldsTools.vue index 8ce6e3a6..67ab803e 100644 --- a/src/ui/src/builder/BuilderFieldsTools.vue +++ b/src/ui/src/builder/BuilderFieldsTools.vue @@ -1,19 +1,120 @@