Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add server api for apps + Add external hooks for apps #101

Merged
merged 8 commits into from
Sep 9, 2024
Merged

Conversation

VictorS67
Copy link
Owner

@VictorS67 VictorS67 commented Jul 27, 2024

Add server api for apps

add new server api:

  • /app/run - Run the Encre workflow file (e.g. xxx.encre) and listening the workflow events through SSE (Server-Sent Events)

/app/run - Run Encre Workflow

Method

POST

Authentication required

No

Body

  • userInputs (GraphInputs) - Required. The inputs for running the Encre workflow, following the type schema GraphInputs. The schema is shown as follows:
{
  "<node-id>": {
    "<node-port-name>": {
      "type": "<node-port-value-type>",
      "value": <node-port-value>
    },
    ...
  },
  ...
}
  • appPath (string) - Required. The absolute path to the Encre workflow file.

  • context (Record<string, Data>) - Optional. The context for running the Encre workflow.

  • filter (ProcessStreamEventFilter) - Optional. A set of flags for indicating whether showing the workflow running result for the corresponding workflow event. These workflow events can be shown if those present in the filter:

    • nodeStart - whether be true, or an array of node id that indicates those are the nodes that require sending running results.
    • nodeFinish - whether be true, or an array of node id that indicates those are the nodes that require sending running results.
    • done - can be true.
    • error - can be true.

Note

For the userInputs attribute, please provide node value for the START nodes in the workflow, any nodes that are not from the START nodes will not overwrite their port values from userInputs.

If you are looking for how to add user inputs for nodes during the workflow running, please look at PR

Warning

For the filter attribute, if not provided, then no event result can be sent from the server.

Response

This API is a SSE (Server-Sent Events), so you should keep the connection open to receive the complete response.

  • Content-Type: text/event-stream
  • Transfer-Encoding: chunked
  • Cache-Control: no-cache
  • Connection: keep-alive

There is a detailed description of the event data format:

data: { "#event": "<event-name>", <event-data-fields> }

...

data: [DONE]

Event Types

Node Start

Emit event when a node processing begins.

  • Event Name: nodeStart
  • Event Data Fields:
    • nodeId (string): node ID.
    • nodeTitle (string): node title (default is node name).
    • inputs (ProcessInputMap): the value for the node input ports, following the schema:
{
  "<node-port-name>": {
    "type": "<node-port-value-type>",
    "value": <node-port-value>
  },
  ...
}
Node Finish

Emit event when a node processing is finished successfully.

  • Event Name: nodeFinish
  • Event Data Fields:
    • nodeId (string): node ID.
    • nodeTitle (string): node title (default is node name).
    • outputs (ProcessOutputMap): the value for the node output ports, sharing the same schema as ProcessInputMap.
Graph Processing Done

Emit event when the workflow finished processing (on whether success or not).

  • Event Name: done
  • Event Data Fields:
    • graphOutput (GraphOutputs): the final outputs from the graph processing, sharing the same schema as GraphInputs. If any error happens, no value should be sent from this event.
Graph Processing Error

Emit event when the workflow finished processing with errors.

  • Event Name: error
  • Event Data Fields:
    • error (string): the stacked error messages in some node processing error.

Note

By rules, the "Graph Processing Done" event is the last event that can be emitted from the workflow processing. However, it is recommanded to wait the data: [DONE] message before closing the SSE connection.

@VictorS67 VictorS67 changed the title Add server api for apps Add server api for apps + Add external hooks for apps Sep 4, 2024
@VictorS67
Copy link
Owner Author

VictorS67 commented Sep 6, 2024

Add external hooks for apps

new package: @encrejs/hooks

Description

  • Following structure of alibaba/ahooks, providing reliable react hooks for third-party apps to be converted into Encre Apps.

Install

yarn add @encrejs/hooks

Hooks

useAppHandler

useAppHandler provides event handlers such as onInput, onAction, and onOuput for handling emitted events from encre workflow processing.

Event handlers are used for

  • onInput: Loading data from React components to the input cache, preparing encre workflow inputs before processing;
  • onAction: Performing avaliable actions to interact with encre workflow, such as process, pause, resume, etcs;
  • onOutput: Listening to events emitted from encre app APIs, performing custom callbacks to handle those events. For example, parsing node processing results and passing to React components.

Note

Before using useAppHandler, please make sure the encre workflow handler configuration is loaded into the React APP.
Please checkout encre-webpack-plugin for loading Encre Workflow Handler Configuration file into web apps.

Usage

import { useAppHandler } from "@encrejs/hooks";

Encre Workflow Handler Configuration

To use useAppHandler in React, The Encre Workflow Handler Configuration file should be loaded into the React APP during webpack compilation.

The schema of configuration shows as follows:

{
  "workflow": "<absolute-path-encre-workflow-file>",
  "handlers": {
    "<input-handler-1>": {
      "handlerType": "input",
      "description": "<handler-description>",
      "event": "<handler-event-name>",
      ...
    },
    ...,
    "<action-handler-1>": {
      "handlerType": "action",
      "description": "<handler-description>",
      "event": "<handler-event-name>",
      ...
    },
    ...,
    "<output-handler-1>": {
      "handlerType": "output",
      "description": "<handler-description>",
      "event": "<handler-event-name>",
      ...
    },
    ...
  }
}

Note

Currently, only one workflow is supported in a single APP. If you are looking for multiple workflows support, please follow #108

Handlers

INPUT userInput

User Input Handler is used for listening data from React Components, such as input boxs, buttons, etcs. The onInput function will store the data into a cache, and provide these data as an appropriate format to the action handlers.

The handler configuration data schema shows as follows:

{
  "handlerType": "input",
  "description": "<handler-description>",
  "event": "userInput",
  "graphId": "<workflow-id>",
  "nodeId": "<node-id>",
  "portName": "<node-port-name>",
  "dataType": "<node-port-data-type>"
}
  • handlerType (string) - Required. Should be `'input'``;
  • description (string) - Required. A brief description of this handler;
  • event (string) - Required. Should be `'userInput'``;
  • graphId (string) - Required. This is the id of the workflow (i.e. graph id);
  • nodeId (string) - Required. This is the id of the node in the workflow that this input should be sent to;
  • portName (string) - Required. This is the name of the port in the node that this input should be sent to;
  • dataType (DataType) - Required. This is the data type of the port in the node that this input should be sent to.

Note

The nodeId should be the id from any start node in the workflow.
Adding input handlers to non-start node will store the data in the cache BUT will not send their cached value to the runGraph action handler.

ACTION runGraph

Run Graph Handler is used for start a connection to the server API /app/run and listening the incomming messages (e.g. the event data results such as nodeFinish, done, etcs).

The handler configuration data schema shows as follows:

{
  "handlerType": "action",
  "description": "<handler-description>",
  "event": "runGraph",
  "graphId": "<workflow-id>",
  "graphInputs": ["<node-id>", ...],
  "graphOutputs": ["<node-id>", ...]
}
  • handlerType (string) - Required. Should be `'action'``;
  • description (string) - Required. A brief description of this handler;
  • event (string) - Required. Should be `'runGraph'``;
  • graphId (string) - Required. This is the id of the workflow (i.e. graph id);
  • graphInputs (string[]) - Required. This is an array of start node ids in the workflow;
  • graphOutputs (string[]) - Required. This is an array of end node ids in the workflow.
OUTPUT graphOutput

Graph Output Handler is used for handling incoming data from done event in the server API /app/run.

The handler configuration data schema shows as follows:

{
  "handlerType": "output",
  "description": "<handler-description>",
  "event": "graphOutput",
  "graphId": "<workflow-id>",
  "nodeId": "<node-id>",
  "portName": "<node-port-name>",
  "dataType": "<node-port-data-type>"
}
  • handlerType (string) - Required. Should be `'output'``;
  • description (string) - Required. A brief description of this handler;
  • event (string) - Required. Should be `'graphOutput'``;
  • graphId (string) - Required. This is the id of the workflow (i.e. graph id);
  • nodeId (string) - Required. This is the id of the node in the workflow that this input should be sent to;
  • portName (string) - Required. This is the name of the port in the node that this input should be sent to;
  • dataType (DataType) - Required. This is the data type of the port in the node that this input should be sent to.

Note

The nodeId should be the id from any end node in the workflow.
Adding graph output handlers to non-end node will not get data result from the runGraph action handler. If you want to get data result from non-end node, please use node output handlers.

OUTPUT nodeOutput

Node Output Handler is used for handling incoming data from nodeFinish event in the server API /app/run.

The handler configuration data schema is the same as the data schema of Graph Output Handler.

Key Functions of useAppHandler

  • onInput: Manages the input of data from user interactions within React components into the Encre workflow. It preloads the input cache to prepare for workflow processing.
  • onAction: Executes available actions within the workflow, such as start, stop, pause, or resume processes.
  • onOutput: Captures and processes events emitted from the Encre Studio API, using custom callbacks to handle these events effectively, such as parsing node processing results and updating React components.
onInput(handler: string, value: unknown): Promise<void>

The onInput method is used to handle user inputs that trigger specific graph nodes within a workflow. Each input is associated with a handler, which correlates to an EncreAppInputHandler configuration. This method updates an in-memory cache with new input values which may later influence the workflow's execution.

  • Parameters:

    • handler: A string identifier for the input handler.
    • value: The value to be input, which can be of types specified in EncreAppInputHandler (e.g., string, number, boolean).
  • Usage Example:

    onInput("input-handler-name", "Example input");
  • Purpose: This method is typically called when a user action requires updating the state of a node within the workflow. It ensures that the input is stored and made available for when the graph is next run.

onAction(handler: string): Promise<void>

The onAction method executes predefined actions based on the specified handler. These actions typically involve running a portion or the entirety of a graph based on the provided inputs and expected outputs.

  • Parameters:

    • handler: A string identifier that corresponds to an EncreAppActionHandler.
  • Usage Example:

    onAction("action-handler-name");
  • Purpose: This method is used to initiate graph execution sequences, which are complex sets of node operations that may depend on prior inputs or result in outputs needed for other operations. It is essential for triggering backend processes that perform the core logic defined in the workflow.

onOutput(handler: string, stream: ReadableStream, callbacks: Object, signal?: AbortSignal): Promise<void>

The onOutput method is designed to handle the output of workflow processes. It listens to a stream of data which can include node outputs or final graph outputs. Outputs are processed and provided to the application via callback functions.

  • Parameters:

    • handler: A string identifier for the output handler.
    • stream: A ReadableStream from which output data is read.
    • callbacks: An object containing functions (onRead, onDone, onError) that handle different stages of the output processing.
    • signal: An optional AbortSignal to handle cancellation of the output processing.
  • Usage Example:

    onOutput("output-handler-name", stream, {
      onRead: (data) => console.log("Data received:", data),
      onDone: () => console.log("Processing complete"),
      onError: (error) => console.error("Error:", error),
    });
  • Purpose: This method is crucial for extracting processed data from nodes and making it available to the rest of the application, often influencing UI elements or subsequent processing stages.

Example

This is an example of a simple chat UI with Encre workflow.

encre.app.config.json:

{
  "workflow": "...",
  "handlers": {
    "input-box": {
      "handlerType": "input",
      "description": "When a user input box is entered",
      "event": "userInput",
      "graphId": "S4Ja0y6a7xHwJE0M0",
      "nodeId": "7BFZbm30GG8Sk_pcM",
      "portName": "content",
      "dataType": "string"
    },
    "click-send": {
      "handlerType": "action",
      "description": "When then send button is clicked",
      "event": "runGraph",
      "graphId": "S4Ja0y6a7xHwJE0M0",
      "graphInputs": ["j28rjy8BazLC@II17", "7BFZbm30GG8Sk_pcM"],
      "graphOutputs": ["ZSJ3g8tL6qsJU6R6c"]
    },
    "bot-message": {
      "handlerType": "output",
      "description": "The bot message is popped out when",
      "event": "nodeOutput",
      "graphId": "S4Ja0y6a7xHwJE0M0",
      "nodeId": "ZSJ3g8tL6qsJU6R6c",
      "portName": "output",
      "dataType": "string"
    }
  }
}

src/Components/ChatUI/index.tsx:

import React, { useCallback, useState, useReducer, useRef } from "react";
import { useAppHandler, MessageRole, Message } from "@encrejs/hooks";
import { debounce, DebouncedFunc } from "lodash";

const UserInput = (props: {
  loading: boolean;
  inputRef: React.RefObject<HTMLTextAreaElement>;
  onLoadInput: DebouncedFunc<(content: string | undefined) => Promise<void>>;
}) => {
  const { loading, inputRef, onLoadInput } = props;

  const handleInputChange = () => {
    const value = inputRef.current?.value;
    onLoadInput(value);
  };

  return (
    <textarea
      ref={inputRef!}
      className="input"
      disabled={loading}
      placeholder="Write something here..."
      autoComplete="off"
      rows={1}
      onInput={handleInputChange}
    />
  );
};

function SendButton(props: {
  loading: boolean;
  inputRef: React.RefObject<HTMLTextAreaElement>;
  onStop: () => void;
  onSend: (msg: Message) => void;
}) {
  const { loading, inputRef, onStop, onSend } = props;

  const handleSend = () => {
    const content = inputRef.current?.value;
    if (content) {
      inputRef.current!.value = "";
      inputRef.current!.style.height = "auto";
      onSend({ role: MessageRole.User, content });
    }
  };

  return (
    <>
      {loading && <button title="Stop" onClick={onStop} />}
      {!loading && <button title="Send" onClick={handleSend} />}
    </>
  );
}

const SendBar = (props: {
  currMsgContent: React.MutableRefObject<string>;
  onChangeMsgs: (value: React.SetStateAction<Message[]>) => void;
}) => {
  const { currMsgContent, onChangeMsgs } = props;

  const [loading, setLoading] = useState<boolean>(false);
  const [, forceUpdate] = useReducer((x) => !x, false);

  const inputRef = useRef<HTMLTextAreaElement>(null);
  const controller = useRef<AbortController | null>(null);

  const { onInput, onAction, onOutput } = useAppHandler();

  const archiveCurrentMessage = () => {
    const content = currMsgContent.current;
    currMsgContent.current = "";
    setLoading(false);
    if (content) {
      onChangeMsgs((msgs: Message[]) => {
        return [...msgs, { role: MessageRole.Assistant, content }];
      });
    }
  };

  const onLoadInput = useCallback(
    debounce(async (content: string) => {
      await onInput("input-box", content);
    }, 300),
    []
  ); // Debounce time 300ms

  const onSend = useCallback(
    async (message: Message) => {
      onChangeMsgs((msgs: Message[]) => {
        return [...msgs, message];
      });

      currMsgContent.current = "";
      controller.current = new AbortController();
      setLoading(true);

      const stream = (await onAction("click-send")) as ReadableStream;

      await onOutput(
        "bot-message",
        stream,
        {
          onRead(value) {
            if (value && typeof value === "string") {
              if (value !== "\n") {
                currMsgContent.current += value;
                forceUpdate();
              }
            }
          },
          onDone: archiveCurrentMessage,
          onError(error) {
            console.error(error);
            setLoading(false);
          },
        },
        controller.current?.signal
      );
    },
    [controller]
  );

  const onStop = useCallback(() => {
    if (controller.current) {
      controller.current.abort();
      archiveCurrentMessage();
    }
  }, [controller]);

  return (
    <div>
      <UserInput
        loading={loading}
        inputRef={inputRef}
        onLoadInput={onLoadInput}
      />
      <SendButton
        loading={loading}
        inputRef={inputRef}
        onSend={onSend}
        onStop={onStop}
      />
    </div>
  );
};

const MessageRow = (props: { role: MessageRole; content: string }) => {
  const { role, content } = props;

  return (
    <div>
      <span>{role}</span>
      <span>{content}</span>
    </div>
  );
};

const ChatUI = () => {
  const [messages, setMessages] = useState<Message[]>([]);
  const currMsgContent = useRef<string>("");

  return (
    <div>
      {messages.map((message, idx) => (
        <MessageRow
          key={idx}
          role={message.role}
          content={message.content as string}
        />
      ))}
      {currMsgContent.current && (
        <MessageRow
          role={MessageRole.Assistant}
          content={currMsgContent.current}
        />
      )}
      <SendBar currMsgContent={currMsgContent} onChangeMsgs={setMessages} />
    </div>
  );
};

export default ChatUI;


import { EncreData } from './types';

export interface EncreCache {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this interface for specifically, I understand that it is storing a graph and a node, but just want to make sure I understand the software, what part fo the process does EncreCache speeds up specifically?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good Question! EncreCache is used for storing incoming data from input handlers. You can image a scenario where there is a message box that connects to an input handler, so whenever user change the message, the input handler should store that changed value. But this changed value is not sent to the workflow immediately, since no action is triggered by the uset. So we need a cache to store this value.

} from '../types';
import { coerceEncreData } from '../utils';

const runGraphFetch = async (
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to make sure I understand this, is the purpose of this reads what's in the graph and populate important information in Record<> form?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and if the callbacks are unknown, what are the purposes of these callbacks?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you reference the code chunk? I need know what callbacks you are asking here.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For example, this:

nodeCallbacks: {
[key in string]: {
onNodeStart?: (
nodeId: string,
nodeTitle: string,
inputs: Record<string, EncreData>,
) => unknown;
onNodeFinish?: (
nodeId: string,
nodeTitle: string,
outputs: Record<string, EncreData>,
) => unknown;
};
},
graphCallbacks: {
onDone?: (
graphOutput: Record<string, Record<string, EncreData>>,
) => unknown;
onError?: (error: string) => unknown;
},

What are these use for?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These callbacks are coming from users. Basically, when user called onOutput function, they can write their own onRead, onDone, onError callbacks. Those callbacks will be formatted into the corresponding nodeCallbacks, graphCallbacks based on the hooks logic. So we won't know the output of those callbacks are. However, we will coerce those outputs so that we can ensure the data is formatted correctly to those output Components.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Hooks documentation may have more detailed description for those callbacks.

nodeId: json['nodeId'],
nodeTitle: json['nodeTitle'],
};

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is metadata encoded?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is for sending the data to the ReadableStream


// console.log(`encodedText: ${encodedText}`);

const queue = encoder.encode(encodedText);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the purpose of encoding an encodedText?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above. This is for sending the data to the ReadableStream

@VictorS67 VictorS67 merged commit 31a368d into master Sep 9, 2024
8 checks passed
@VictorS67 VictorS67 deleted the app-server branch September 9, 2024 04:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants