// This program has been developed by students from the bachelor Computer Science at Utrecht // University within the Software Project course. // © Copyright Utrecht University (Department of Information and Computing Sciences) import { type Node } from "@xyflow/react" import { GoalReduce } from "../nodes/GoalNode" export type Plan = { name: string, id: string, steps: PlanElement[], } export type PlanElement = Goal | Action export type Goal = { id: string // we let the reducer figure out the rest dynamically type: "goal" } // Actions export type Action = SpeechAction | GestureAction | LLMAction export type SpeechAction = { id: string, text: string, type:"speech" } export type GestureAction = { id: string, gesture: string, isTag: boolean, type:"gesture" } export type LLMAction = { id: string, goal: string, type:"llm" } export type ActionTypes = "speech" | "gesture" | "llm"; // Extract the wanted information from a plan within the reducing of nodes export function PlanReduce(_nodes: Node[], plan?: Plan, ) { if (!plan) return "" return { id: plan.id, steps: plan.steps.map((x) => StepReduce(x, _nodes)) } } // Extract the wanted information from a plan element. function StepReduce(planElement: PlanElement, _nodes: Node[]) : Record { // We have different types of plan elements, requiring differnt types of output const nodes = _nodes const thisNode = _nodes.find((x) => x.id === planElement.id) switch (planElement.type) { case ("speech"): return { id: planElement.id, text: planElement.text, } case ("gesture"): return { id: planElement.id, gesture: { type: planElement.isTag ? "tag" : "single", name: planElement.gesture }, } case ("llm"): return { id: planElement.id, goal: planElement.goal, } case ("goal"): return thisNode ? GoalReduce(thisNode, nodes) : {} } } /** * Finds out whether the plan can iterate multiple times, or always stops after one action. * This comes down to checking if the plan only has speech/ gesture actions, or others as well. * @param plan: the plan to check * @returns: a boolean */ export function DoesPlanIterate( _nodes: Node[], plan?: Plan,) : boolean { // TODO: should recursively check plans that have goals (and thus more plans) in them. if (!plan) return false return plan.steps.filter((step) => step.type == "llm").length > 0 || ( // Find the goal node of this step plan.steps.filter((step) => step.type == "goal").map((goalStep) => { const goalId = goalStep.id; const goalNode = _nodes.find((x) => x.id === goalId); // In case we don't find any valid plan, this node doesn't iterate if (!goalNode || !goalNode.data.plan) return false; // Otherwise, check if this node can fail - if so, we should have the option to iterate return (goalNode && goalNode.data.plan && goalNode.data.can_fail) }) ).includes(true); } /** * Checks if any of the plan's goal steps has its can_fail value set to true. * @param plan: plan to check * @param _nodes: nodes in flow store. */ export function HasCheckingSubGoal(plan: Plan, _nodes: Node[]) { const goalSteps = plan.steps.filter((x) => x.type == "goal"); return goalSteps.map((goalStep) => { // Find the goal node and check its can_fail data boolean. const goalId = goalStep.id; const goalNode = _nodes.find((x) => x.id === goalId); return (goalNode && goalNode.data.can_fail) }).includes(true); } /** * Returns the value of the action. * Since typescript can't polymorphicly access the value field, * we need to switch over the types and return the correct field. * @param action: action to retrieve the value from * @returns string | undefined */ export function GetActionValue(action: Action) { let returnAction; switch (action.type) { case "gesture": returnAction = action as GestureAction return returnAction.gesture; case "speech": returnAction = action as SpeechAction return returnAction.text; case "llm": returnAction = action as LLMAction return returnAction.goal; default: } }