diff --git a/apps/hash-ai-worker-ts/src/activities/flow-activities/generate-flow-run-name-activity.ts b/apps/hash-ai-worker-ts/src/activities/flow-activities/generate-flow-run-name-activity.ts index d29b6b5a319..01df5f7e3dd 100644 --- a/apps/hash-ai-worker-ts/src/activities/flow-activities/generate-flow-run-name-activity.ts +++ b/apps/hash-ai-worker-ts/src/activities/flow-activities/generate-flow-run-name-activity.ts @@ -1,8 +1,7 @@ import { - automaticBrowserInferenceFlow, - manualBrowserInferenceFlow, -} from "@local/hash-isomorphic-utils/flows/browser-plugin-flow-definitions"; -import { isGoalFlowDefinitionId } from "@local/hash-isomorphic-utils/flows/goal-flow-definitions"; + flowDefinitionIds, + isGoalFlowDefinitionId, +} from "@local/hash-isomorphic-utils/flows/flow-definition-ids"; import { getFlowContext } from "../shared/get-flow-context.js"; import { getLlmResponse } from "../shared/get-llm-response.js"; @@ -15,7 +14,7 @@ import type { AutomaticInferenceInputName, ManualInferenceInputName, } from "@local/hash-isomorphic-utils/flows/browser-plugin-flow-types"; -import type { GoalFlowInputName } from "@local/hash-isomorphic-utils/flows/goal-flow-definitions"; +import type { GoalFlowInputName } from "@local/hash-isomorphic-utils/flows/definitions/goal-flow-definitions"; import type { FlowActionDefinitionId, FlowDefinition, @@ -100,8 +99,8 @@ export const generateFlowRunName = async ( if ( [ - automaticBrowserInferenceFlow.flowDefinitionId, - manualBrowserInferenceFlow.flowDefinitionId, + flowDefinitionIds.automaticBrowserInference, + flowDefinitionIds.manualBrowserInference, ].includes(flowDefinitionId) ) { const webPage = flowInputs[ @@ -114,7 +113,7 @@ export const generateFlowRunName = async ( } return `${ - flowDefinitionId === automaticBrowserInferenceFlow.flowDefinitionId + flowDefinitionId === flowDefinitionIds.automaticBrowserInference ? "Auto-analyze" : "Analyze" } webpage: ${webPage.url}`; diff --git a/apps/hash-ai-worker-ts/src/activities/shared/get-flow-context.ts b/apps/hash-ai-worker-ts/src/activities/shared/get-flow-context.ts index ae34bd03310..71de562e8c7 100644 --- a/apps/hash-ai-worker-ts/src/activities/shared/get-flow-context.ts +++ b/apps/hash-ai-worker-ts/src/activities/shared/get-flow-context.ts @@ -18,7 +18,7 @@ import { graphApiClient } from "./graph-api-client.js"; import type { EntityId, UserId, WebId } from "@blockprotocol/type-system"; import type { ManualInferenceInputName } from "@local/hash-isomorphic-utils/flows/browser-plugin-flow-types"; -import type { GoalFlowInputName } from "@local/hash-isomorphic-utils/flows/goal-flow-definitions"; +import type { GoalFlowInputName } from "@local/hash-isomorphic-utils/flows/definitions/goal-flow-definitions"; import type { RunAiFlowWorkflowParams } from "@local/hash-isomorphic-utils/flows/temporal-types"; import type { FlowDataSources } from "@local/hash-isomorphic-utils/flows/types"; import type { File } from "@local/hash-isomorphic-utils/system-types/shared"; diff --git a/apps/hash-api/src/ai/infer-entities-websocket/handle-infer-entities-request.ts b/apps/hash-api/src/ai/infer-entities-websocket/handle-infer-entities-request.ts index 3f8b6c2e0b5..e556d53c9c1 100644 --- a/apps/hash-api/src/ai/infer-entities-websocket/handle-infer-entities-request.ts +++ b/apps/hash-api/src/ai/infer-entities-websocket/handle-infer-entities-request.ts @@ -1,8 +1,5 @@ import { getFlowRuns } from "@local/hash-backend-utils/flows"; -import { - automaticBrowserInferenceFlow, - manualBrowserInferenceFlow, -} from "@local/hash-isomorphic-utils/flows/browser-plugin-flow-definitions"; +import { flowDefinitionIds } from "@local/hash-isomorphic-utils/flows/flow-definition-ids"; import { FlowRunStatus } from "../../graphql/api-types.gen"; @@ -44,10 +41,10 @@ export const handleInferEntitiesRequest = async ({ payload: { webId, ...flowInputs }, } = message; - const { flowDefinitionId } = + const flowDefinitionId = message.type === "manual-inference-request" - ? manualBrowserInferenceFlow - : automaticBrowserInferenceFlow; + ? flowDefinitionIds.manualBrowserInference + : flowDefinitionIds.automaticBrowserInference; if (message.type === "automatic-inference-request") { const openFlowRuns = await getFlowRuns({ @@ -55,8 +52,8 @@ export const handleInferEntitiesRequest = async ({ filters: { executionStatus: FlowRunStatus.Running, flowDefinitionIds: [ - automaticBrowserInferenceFlow.flowDefinitionId, - manualBrowserInferenceFlow.flowDefinitionId, + flowDefinitionIds.automaticBrowserInference, + flowDefinitionIds.manualBrowserInference, ], }, graphApiClient, diff --git a/apps/hash-api/src/graph/ensure-system-graph-is-initialized/migrate-ontology-types/dev-flows.ts b/apps/hash-api/src/graph/ensure-system-graph-is-initialized/migrate-ontology-types/dev-flows.ts index d0dd21e713b..ced081dce7f 100644 --- a/apps/hash-api/src/graph/ensure-system-graph-is-initialized/migrate-ontology-types/dev-flows.ts +++ b/apps/hash-api/src/graph/ensure-system-graph-is-initialized/migrate-ontology-types/dev-flows.ts @@ -1,7 +1,7 @@ import { historicalFlightsFlow, scheduledFlightsFlow, -} from "@local/hash-isomorphic-utils/flows/integration-flow-definitions"; +} from "@local/hash-isomorphic-utils/flows/definitions/integration-flow-definitions"; import type { FlowDefinitionWithId } from "@local/hash-isomorphic-utils/flows/types"; diff --git a/apps/hash-api/src/graph/ensure-system-graph-is-initialized/migrate-ontology-types/seeded-flows.test.ts b/apps/hash-api/src/graph/ensure-system-graph-is-initialized/migrate-ontology-types/seeded-flows.test.ts index 37b2530d983..b230e171833 100644 --- a/apps/hash-api/src/graph/ensure-system-graph-is-initialized/migrate-ontology-types/seeded-flows.test.ts +++ b/apps/hash-api/src/graph/ensure-system-graph-is-initialized/migrate-ontology-types/seeded-flows.test.ts @@ -1,11 +1,11 @@ import { describe, expect, it } from "vitest"; -import * as browserPluginFlows from "@local/hash-isomorphic-utils/flows/browser-plugin-flow-definitions"; -import * as fileFlows from "@local/hash-isomorphic-utils/flows/file-flow-definitions"; -import * as frontendFlows from "@local/hash-isomorphic-utils/flows/frontend-flow-definitions"; -import * as ftseInvestorsFlows from "@local/hash-isomorphic-utils/flows/ftse-investors-flow-definition"; -import * as goalFlows from "@local/hash-isomorphic-utils/flows/goal-flow-definitions"; -import * as integrationFlows from "@local/hash-isomorphic-utils/flows/integration-flow-definitions"; +import * as browserPluginFlows from "@local/hash-isomorphic-utils/flows/definitions/browser-plugin-flow-definitions"; +import * as fileFlows from "@local/hash-isomorphic-utils/flows/definitions/file-flow-definitions"; +import * as frontendFlows from "@local/hash-isomorphic-utils/flows/definitions/frontend-flow-definitions"; +import * as ftseInvestorsFlows from "@local/hash-isomorphic-utils/flows/definitions/ftse-investors-flow-definition"; +import * as goalFlows from "@local/hash-isomorphic-utils/flows/definitions/goal-flow-definitions"; +import * as integrationFlows from "@local/hash-isomorphic-utils/flows/definitions/integration-flow-definitions"; import { validateFlowDefinition } from "@local/hash-isomorphic-utils/flows/validate-flow-definition"; import { devFlows } from "./dev-flows"; diff --git a/apps/hash-api/src/graph/ensure-system-graph-is-initialized/migrate-ontology-types/seeded-flows.ts b/apps/hash-api/src/graph/ensure-system-graph-is-initialized/migrate-ontology-types/seeded-flows.ts index 329575d26d8..c4dd2548107 100644 --- a/apps/hash-api/src/graph/ensure-system-graph-is-initialized/migrate-ontology-types/seeded-flows.ts +++ b/apps/hash-api/src/graph/ensure-system-graph-is-initialized/migrate-ontology-types/seeded-flows.ts @@ -1,13 +1,13 @@ import { automaticBrowserInferenceFlow, manualBrowserInferenceFlow, -} from "@local/hash-isomorphic-utils/flows/browser-plugin-flow-definitions"; -import { inferMetadataFromDocumentFlow } from "@local/hash-isomorphic-utils/flows/file-flow-definitions"; +} from "@local/hash-isomorphic-utils/flows/definitions/browser-plugin-flow-definitions"; +import { inferMetadataFromDocumentFlow } from "@local/hash-isomorphic-utils/flows/definitions/file-flow-definitions"; import { configureDashboardItemFlow, refineDashboardItemFlow, -} from "@local/hash-isomorphic-utils/flows/frontend-flow-definitions"; -import { goalFlow } from "@local/hash-isomorphic-utils/flows/goal-flow-definitions"; +} from "@local/hash-isomorphic-utils/flows/definitions/frontend-flow-definitions"; +import { goalFlow } from "@local/hash-isomorphic-utils/flows/definitions/goal-flow-definitions"; import type { FlowDefinitionWithId } from "@local/hash-isomorphic-utils/flows/types"; diff --git a/apps/hash-api/src/graphql/resolvers/knowledge/file/shared.ts b/apps/hash-api/src/graphql/resolvers/knowledge/file/shared.ts index c2d2eef5dd0..7a09a7a4f8b 100644 --- a/apps/hash-api/src/graphql/resolvers/knowledge/file/shared.ts +++ b/apps/hash-api/src/graphql/resolvers/knowledge/file/shared.ts @@ -1,4 +1,4 @@ -import { inferMetadataFromDocumentFlow } from "@local/hash-isomorphic-utils/flows/file-flow-definitions"; +import { flowDefinitionIds } from "@local/hash-isomorphic-utils/flows/flow-definition-ids"; import { generateUuid } from "@local/hash-isomorphic-utils/generate-uuid"; import type { Entity, UserId, WebId } from "@blockprotocol/type-system"; @@ -42,7 +42,7 @@ export const triggerPdfAnalysisWorkflow = async ({ }, }, }, - flowDefinitionId: inferMetadataFromDocumentFlow.flowDefinitionId, + flowDefinitionId: flowDefinitionIds.inferMetadataFromDocument, flowInputs: { fileEntityId: { kind: "EntityId", value: entityId }, }, @@ -58,7 +58,7 @@ export const triggerPdfAnalysisWorkflow = async ({ taskQueue: "ai", args: [params], memo: { - flowDefinitionId: inferMetadataFromDocumentFlow.flowDefinitionId, + flowDefinitionId: flowDefinitionIds.inferMetadataFromDocument, userAccountId, webId, }, diff --git a/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer.tsx b/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer.tsx index 7e313a1d427..667db745d89 100644 --- a/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer.tsx +++ b/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer.tsx @@ -10,12 +10,10 @@ import { IconButton, Skeleton } from "@hashintel/design-system"; import { deserializeSubgraph } from "@local/hash-graph-sdk/subgraph"; import { actionDefinitions } from "@local/hash-isomorphic-utils/flows/action-definitions"; import { - automaticBrowserInferenceFlow, - manualBrowserInferenceFlow, -} from "@local/hash-isomorphic-utils/flows/browser-plugin-flow-definitions"; -import { inferMetadataFromDocumentFlow } from "@local/hash-isomorphic-utils/flows/file-flow-definitions"; + flowDefinitionIds, + isGoalFlowDefinitionId, +} from "@local/hash-isomorphic-utils/flows/flow-definition-ids"; import { generateWorkerRunPath } from "@local/hash-isomorphic-utils/flows/frontend-paths"; -import { isGoalFlowDefinitionId } from "@local/hash-isomorphic-utils/flows/goal-flow-definitions"; import { almostFullOntologyResolveDepths, currentTimeInstantTemporalAxes, @@ -36,6 +34,7 @@ import { defaultBrowserPluginDomains } from "../../../goals/new.page/default-bro import { useFlowDefinitionsContext } from "../../../shared/flow-definitions-context"; import { useFlowRunsContext } from "../../../shared/flow-runs-context"; import { NotFound } from "../../../shared/not-found"; +import { useFlowDefinitionAtTime } from "../../../shared/use-flow-definitions"; import { useFlowSchedules } from "../../../shared/use-flow-schedules"; import { ActivityLog } from "./flow-visualizer/activity-log"; import { DAG } from "./flow-visualizer/dag"; @@ -64,7 +63,12 @@ import type { ProposedEntityOutput, } from "./flow-visualizer/shared/types"; import type { EntityRootType, Subgraph } from "@blockprotocol/graph"; -import type { EntityId, WebId } from "@blockprotocol/type-system"; +import type { + EntityId, + EntityUuid, + Timestamp, + WebId, +} from "@blockprotocol/type-system"; import type { Filter } from "@local/hash-graph-client"; import type { HashEntity } from "@local/hash-graph-sdk/entity"; import type { @@ -167,9 +171,9 @@ const containerHeight = `calc(100vh - ${HEADER_HEIGHT}px)`; /** Flows that only the browser plugin or a file upload starts, because their inputs come from there. */ const unrunnableDefinitionIds = [ - manualBrowserInferenceFlow.flowDefinitionId, - automaticBrowserInferenceFlow.flowDefinitionId, - inferMetadataFromDocumentFlow.flowDefinitionId, + flowDefinitionIds.manualBrowserInference, + flowDefinitionIds.automaticBrowserInference, + flowDefinitionIds.inferMetadataFromDocument, ]; export const FlowRunVisualizerSkeleton = () => ( @@ -207,6 +211,21 @@ export const FlowVisualizer = () => { const { selectedFlowRun } = useFlowRunsContext(); + /** + * A run is drawn with the edition of its definition it executed: the one current when it started. + */ + const { + flowDefinition: flowOfSelectedRun, + loading: flowOfSelectedRunLoading, + } = useFlowDefinitionAtTime({ + flowDefinitionId: selectedFlowRun + ? (selectedFlowRun.flowDefinitionId as EntityUuid) + : null, + timestamp: selectedFlowRun + ? (selectedFlowRun.startedAt as Timestamp) + : null, + }); + const { schedulesByEntityUuid } = useFlowSchedules(); /** @@ -224,12 +243,18 @@ export const FlowVisualizer = () => { const selectedFlow = useMemo( () => - flowDefinitions.find( - ({ flowDefinitionId }) => - flowDefinitionId === - (selectedFlowRun?.flowDefinitionId ?? selectedFlowDefinitionId), - ), - [flowDefinitions, selectedFlowDefinitionId, selectedFlowRun], + selectedFlowRun + ? flowOfSelectedRun + : flowDefinitions.find( + ({ flowDefinitionId }) => + flowDefinitionId === selectedFlowDefinitionId, + ), + [ + flowDefinitions, + flowOfSelectedRun, + selectedFlowDefinitionId, + selectedFlowRun, + ], ); const selectedFlowDefinition = selectedFlow?.flowDefinition; @@ -633,7 +658,7 @@ export const FlowVisualizer = () => { }, [runFlow, selectedFlowRun]); if (!selectedFlow || !selectedFlowDefinition) { - if (flowDefinitionsLoading) { + if (flowDefinitionsLoading || flowOfSelectedRunLoading) { return ; } diff --git a/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/dag-slide.tsx b/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/dag-slide.tsx index 8beef2d6d8f..ef393b6c4a3 100644 --- a/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/dag-slide.tsx +++ b/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/dag-slide.tsx @@ -1,7 +1,7 @@ import { Backdrop, Box, Slide, Stack } from "@mui/material"; import { IconButton, XMarkRegularIcon } from "@hashintel/design-system"; -import { isGoalFlowDefinitionId } from "@local/hash-isomorphic-utils/flows/goal-flow-definitions"; +import { isGoalFlowDefinitionId } from "@local/hash-isomorphic-utils/flows/flow-definition-ids"; import { DAG } from "./dag"; import { Topbar } from "./topbar"; diff --git a/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/flow-run-sidebar.tsx b/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/flow-run-sidebar.tsx index ef523fbe993..1ac052f4625 100644 --- a/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/flow-run-sidebar.tsx +++ b/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/flow-run-sidebar.tsx @@ -7,10 +7,7 @@ import { ClockRegularIcon, IconButton, } from "@hashintel/design-system"; -import { - isGoalFlowDefinitionId, - type GoalFlowInputName, -} from "@local/hash-isomorphic-utils/flows/goal-flow-definitions"; +import { isGoalFlowDefinitionId } from "@local/hash-isomorphic-utils/flows/flow-definition-ids"; import { Link } from "../../../../../shared/ui/link"; import { useFlowRunsContext } from "../../../../shared/flow-runs-context"; @@ -23,6 +20,7 @@ import { flowSectionBorderRadius } from "./shared/styles"; import type { FlowRun } from "../../../../../graphql/api-types.gen"; import type { EdgesAndNodes } from "./shared/types"; import type { EntityId, EntityUuid } from "@blockprotocol/type-system"; +import type { GoalFlowInputName } from "@local/hash-isomorphic-utils/flows/definitions/goal-flow-definitions"; import type { PropsWithChildren } from "react"; const SidebarSection = ({ children }: PropsWithChildren) => ( diff --git a/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/flow-run-sidebar/group-status.tsx b/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/flow-run-sidebar/group-status.tsx index 38d89496893..f8b77c3b555 100644 --- a/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/flow-run-sidebar/group-status.tsx +++ b/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/flow-run-sidebar/group-status.tsx @@ -17,7 +17,12 @@ import type { EdgesAndNodes } from "../shared/types"; export const GroupStatus = ({ graph }: { graph: EdgesAndNodes }) => { const groupStepsWithIds = useMemo( - () => graph.nodes.map((node) => ({ ...node, stepId: node.id })), + () => + graph.nodes.map((node) => ({ + ...node, + stepId: node.id, + parentStepId: node.parentNode, + })), [graph], ); diff --git a/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/outputs.tsx b/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/outputs.tsx index bdcea06b79c..40c3b409f03 100644 --- a/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/outputs.tsx +++ b/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/outputs.tsx @@ -9,7 +9,7 @@ import { } from "@blockprotocol/type-system"; import { CheckRegularIcon, IconButton } from "@hashintel/design-system"; import { HashEntity } from "@local/hash-graph-sdk/entity"; -import { isGoalFlowDefinitionId } from "@local/hash-isomorphic-utils/flows/goal-flow-definitions"; +import { isGoalFlowDefinitionId } from "@local/hash-isomorphic-utils/flows/flow-definition-ids"; import { currentTimeInstantTemporalAxes } from "@local/hash-isomorphic-utils/graph-queries"; import { getClosedMultiEntityTypesQuery } from "../../../../../graphql/queries/ontology/entity-type.queries"; diff --git a/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/swimlane.tsx b/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/swimlane.tsx index e441232ec64..84f8db3710e 100644 --- a/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/swimlane.tsx +++ b/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/swimlane.tsx @@ -139,7 +139,8 @@ export const Swimlane = ({ ); const stepsWithIds = useMemo( - () => nodes.map((node) => ({ stepId: node.id })), + () => + nodes.map((node) => ({ stepId: node.id, parentStepId: node.parentNode })), [nodes], ); diff --git a/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/swimlane/custom-node.tsx b/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/swimlane/custom-node.tsx index f0cb27c7a04..0758ab1f4d6 100644 --- a/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/swimlane/custom-node.tsx +++ b/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/swimlane/custom-node.tsx @@ -3,6 +3,7 @@ import { formatDistance } from "date-fns"; import { useEffect, useMemo, useState } from "react"; import { ArrowRightRegularIcon } from "@hashintel/design-system"; +import { parseStepRunId } from "@local/hash-isomorphic-utils/flows/step-run-id"; import { Button } from "../../../../../../shared/ui/button"; import { @@ -119,7 +120,8 @@ export const CustomNode = ({ data, id, selected }: NodeProps) => { */ let browserInputRequest: ExternalInputRequest | undefined = undefined; for (const request of selectedFlowRun?.inputRequests ?? []) { - if (request.resolvedAt ?? request.stepId !== id) { + /* A request is raised by a step run, which inside a for-each is one of several runs of this node's step. */ + if (request.resolvedAt ?? parseStepRunId(request.stepId).stepId !== id) { continue; } if (request.type === "human-input") { diff --git a/apps/hash-frontend/src/pages/dashboard/hooks/use-dashboard-item-config.ts b/apps/hash-frontend/src/pages/dashboard/hooks/use-dashboard-item-config.ts index 24c195176c3..bb77e7c4606 100644 --- a/apps/hash-frontend/src/pages/dashboard/hooks/use-dashboard-item-config.ts +++ b/apps/hash-frontend/src/pages/dashboard/hooks/use-dashboard-item-config.ts @@ -5,10 +5,7 @@ import { getPrimaryChartType, normalizeStructuralQuery, } from "@local/hash-isomorphic-utils/dashboard-types"; -import { - configureDashboardItemFlow, - refineDashboardItemFlow, -} from "@local/hash-isomorphic-utils/flows/frontend-flow-definitions"; +import { flowDefinitionIds } from "@local/hash-isomorphic-utils/flows/flow-definition-ids"; import { getFlowRunById } from "@local/hash-isomorphic-utils/graphql/queries/flow.queries"; import { systemPropertyTypes } from "@local/hash-isomorphic-utils/ontology-type-ids"; @@ -603,7 +600,7 @@ export const useDashboardItemConfig = ({ // Start the flow const { data } = await startFlow({ variables: { - flowDefinitionId: configureDashboardItemFlow.flowDefinitionId, + flowDefinitionId: flowDefinitionIds.configureDashboardItem, flowInputs: { userGoal: { kind: "Text", @@ -721,7 +718,7 @@ export const useDashboardItemConfig = ({ const { data } = await startFlow({ variables: { - flowDefinitionId: refineDashboardItemFlow.flowDefinitionId, + flowDefinitionId: flowDefinitionIds.refineDashboardItem, flowInputs: { userGoal: { kind: "Text", value: initialGoal }, refinementInstruction: { diff --git a/apps/hash-frontend/src/pages/flows.page.tsx b/apps/hash-frontend/src/pages/flows.page.tsx index db2c725ccf8..bc221aa18ce 100644 --- a/apps/hash-frontend/src/pages/flows.page.tsx +++ b/apps/hash-frontend/src/pages/flows.page.tsx @@ -3,8 +3,8 @@ import { formatDistanceToNowStrict } from "date-fns"; import { memo, useMemo, useState } from "react"; import { InfinityLightIcon } from "@hashintel/design-system"; +import { isGoalFlowDefinitionId } from "@local/hash-isomorphic-utils/flows/flow-definition-ids"; import { generateFlowDefinitionPath } from "@local/hash-isomorphic-utils/flows/frontend-paths"; -import { isGoalFlowDefinitionId } from "@local/hash-isomorphic-utils/flows/goal-flow-definitions"; import { getLayoutWithSidebar } from "../shared/layout"; import { Link } from "../shared/ui/link"; diff --git a/apps/hash-frontend/src/pages/goals.page/goals-list.tsx b/apps/hash-frontend/src/pages/goals.page/goals-list.tsx index 9ea6ca7c1a3..b502f4b1315 100644 --- a/apps/hash-frontend/src/pages/goals.page/goals-list.tsx +++ b/apps/hash-frontend/src/pages/goals.page/goals-list.tsx @@ -1,7 +1,7 @@ import { Box } from "@mui/material"; import { useMemo } from "react"; -import { isGoalFlowDefinitionId } from "@local/hash-isomorphic-utils/flows/goal-flow-definitions"; +import { isGoalFlowDefinitionId } from "@local/hash-isomorphic-utils/flows/flow-definition-ids"; import { useAuthenticatedUser } from "../shared/auth-info-context"; import { useFlowRunsContext } from "../shared/flow-runs-context"; diff --git a/apps/hash-frontend/src/pages/goals/new.page.tsx b/apps/hash-frontend/src/pages/goals/new.page.tsx index cc0c6dcb345..3f26caf3980 100644 --- a/apps/hash-frontend/src/pages/goals/new.page.tsx +++ b/apps/hash-frontend/src/pages/goals/new.page.tsx @@ -18,11 +18,8 @@ import { BullseyeLightIcon, TextField, } from "@hashintel/design-system"; +import { flowDefinitionIds } from "@local/hash-isomorphic-utils/flows/flow-definition-ids"; import { generateWorkerRunPath } from "@local/hash-isomorphic-utils/flows/frontend-paths"; -import { - goalFlow, - type GoalFlowInputName, -} from "@local/hash-isomorphic-utils/flows/goal-flow-definitions"; import { getFlowRunsQuery } from "@local/hash-isomorphic-utils/graphql/queries/flow.queries"; import { useGetOwnerForEntity } from "../../components/hooks/use-get-owner-for-entity"; @@ -48,6 +45,7 @@ import type { NextPageWithLayout } from "../../shared/layout"; import type { DeliverableSettingsState } from "./new.page/deliverable-settings"; import type { FileSettingsState } from "./new.page/file-settings"; import type { EntityTypeWithMetadata, WebId } from "@blockprotocol/type-system"; +import type { GoalFlowInputName } from "@local/hash-isomorphic-utils/flows/definitions/goal-flow-definitions"; import type { FlowDataSources, Payload, @@ -286,7 +284,7 @@ const NewGoalPageContent = () => { }, internetAccess: internetSettings, }, - flowDefinitionId: goalFlow.flowDefinitionId, + flowDefinitionId: flowDefinitionIds.goal, flowInputs, webId, }, diff --git a/apps/hash-frontend/src/pages/shared/flow-definitions-context-provider.tsx b/apps/hash-frontend/src/pages/shared/flow-definitions-context-provider.tsx index 9e8d8090721..b5ff0cae3c4 100644 --- a/apps/hash-frontend/src/pages/shared/flow-definitions-context-provider.tsx +++ b/apps/hash-frontend/src/pages/shared/flow-definitions-context-provider.tsx @@ -1,51 +1,25 @@ import { useMemo } from "react"; -import { - automaticBrowserInferenceFlow, - manualBrowserInferenceFlow, -} from "@local/hash-isomorphic-utils/flows/browser-plugin-flow-definitions"; -import { inferMetadataFromDocumentFlow } from "@local/hash-isomorphic-utils/flows/file-flow-definitions"; -import { - configureDashboardItemFlow, - refineDashboardItemFlow, -} from "@local/hash-isomorphic-utils/flows/frontend-flow-definitions"; -import { ftseInvestorsFlow } from "@local/hash-isomorphic-utils/flows/ftse-investors-flow-definition"; -import { goalFlow } from "@local/hash-isomorphic-utils/flows/goal-flow-definitions"; -import { - historicalFlightsFlow, - scheduledFlightsFlow, -} from "@local/hash-isomorphic-utils/flows/integration-flow-definitions"; - import { FlowDefinitionsContext } from "./flow-definitions-context"; +import { useFlowDefinitions } from "./use-flow-definitions"; import type { FlowDefinitionsContextType } from "./flow-definitions-context"; import type { EntityUuid } from "@blockprotocol/type-system"; -import type { FlowDefinitionWithId } from "@local/hash-isomorphic-utils/flows/types"; import type { PropsWithChildren } from "react"; -const flowsDefinedInCode: FlowDefinitionWithId[] = [ - goalFlow, - manualBrowserInferenceFlow, - automaticBrowserInferenceFlow, - inferMetadataFromDocumentFlow, - configureDashboardItemFlow, - refineDashboardItemFlow, - historicalFlightsFlow, - scheduledFlightsFlow, - ftseInvestorsFlow, -]; - export const FlowDefinitionsContextProvider = ({ children, selectedFlowDefinitionId, }: PropsWithChildren<{ selectedFlowDefinitionId: EntityUuid | null }>) => { + const { flowDefinitions, loading } = useFlowDefinitions(); + const context = useMemo( () => ({ - flowDefinitions: flowsDefinedInCode, - loading: false, + flowDefinitions, + loading, selectedFlowDefinitionId, }), - [selectedFlowDefinitionId], + [flowDefinitions, loading, selectedFlowDefinitionId], ); return ( diff --git a/apps/hash-frontend/src/pages/shared/flow-definitions-context.ts b/apps/hash-frontend/src/pages/shared/flow-definitions-context.ts index d10425d1625..a9b9ca439ff 100644 --- a/apps/hash-frontend/src/pages/shared/flow-definitions-context.ts +++ b/apps/hash-frontend/src/pages/shared/flow-definitions-context.ts @@ -4,7 +4,7 @@ import type { EntityUuid } from "@blockprotocol/type-system"; import type { FlowDefinitionWithId } from "@local/hash-isomorphic-utils/flows/types"; export type FlowDefinitionsContextType = { - /** Every flow defined in code. */ + /** The current edition of every flow definition the user can view, from the graph. */ flowDefinitions: FlowDefinitionWithId[]; loading: boolean; selectedFlowDefinitionId: EntityUuid | null; diff --git a/apps/hash-frontend/src/pages/shared/flow-runs-context.test.ts b/apps/hash-frontend/src/pages/shared/flow-runs-context.test.ts new file mode 100644 index 00000000000..a0fc788c24b --- /dev/null +++ b/apps/hash-frontend/src/pages/shared/flow-runs-context.test.ts @@ -0,0 +1,84 @@ +import { describe, expect, it } from "vitest"; + +import { generateStepRunId } from "@local/hash-isomorphic-utils/flows/step-run-id"; + +import { FlowRunStatus, FlowStepStatus } from "../../graphql/api-types.gen"; +import { getShownStepRun, getStatusForSteps } from "./flow-runs-context"; + +import type { StepRun } from "../../graphql/api-types.gen"; + +const stepRun = (stepId: string, status: FlowStepStatus): StepRun => ({ + attempt: 1, + logs: [], + scheduledAt: "2026-01-01T00:00:00.000Z", + status, + stepId, + stepType: "action", +}); + +/** A step before a `for-each`, and a step inside it, with a run per item. */ +const steps = [ + { stepId: "fetch" }, + { stepId: "forEach" }, + { stepId: "summarise", parentStepId: "forEach" }, +]; + +describe("getStatusForSteps", () => { + it("counts the runs of a step inside a for-each towards it and the for-each, until every item's run is complete", () => { + const running = getStatusForSteps( + { + closedAt: null, + status: FlowRunStatus.Running, + steps: [ + stepRun("fetch", FlowStepStatus.Completed), + stepRun(generateStepRunId("summarise", 0), FlowStepStatus.Completed), + stepRun(generateStepRunId("summarise", 1), FlowStepStatus.Started), + ], + }, + steps, + ); + + expect(running.overallStatus).toBe("In Progress"); + expect(running.statusByStep).toEqual({ + fetch: "Complete", + forEach: "In Progress", + summarise: "In Progress", + }); + + const complete = getStatusForSteps( + { + closedAt: null, + status: FlowRunStatus.Running, + steps: [ + stepRun("fetch", FlowStepStatus.Completed), + stepRun(generateStepRunId("summarise", 0), FlowStepStatus.Completed), + stepRun(generateStepRunId("summarise", 1), FlowStepStatus.Completed), + ], + }, + steps, + ); + + expect(complete.overallStatus).toBe("Complete"); + expect(complete.statusByStep.summarise).toBe("Complete"); + }); +}); + +describe("getShownStepRun", () => { + it("shows the run of a step inside a for-each whose status most needs attention", () => { + const failed = stepRun( + generateStepRunId("summarise", 1), + FlowStepStatus.Failed, + ); + + expect( + getShownStepRun( + [ + stepRun(generateStepRunId("summarise", 0), FlowStepStatus.Completed), + failed, + stepRun(generateStepRunId("summarise", 2), FlowStepStatus.Started), + ], + "summarise", + ), + ).toBe(failed); + }); +}); diff --git a/apps/hash-frontend/src/pages/shared/flow-runs-context.ts b/apps/hash-frontend/src/pages/shared/flow-runs-context.ts index dba4b9634ab..57b9ecd92d2 100644 --- a/apps/hash-frontend/src/pages/shared/flow-runs-context.ts +++ b/apps/hash-frontend/src/pages/shared/flow-runs-context.ts @@ -1,6 +1,8 @@ import { createContext, useContext, useMemo } from "react"; import { useNodeId } from "reactflow"; +import { parseStepRunId } from "@local/hash-isomorphic-utils/flows/step-run-id"; + import { FlowRunStatus, FlowStepStatus } from "../../graphql/api-types.gen"; import type { @@ -45,30 +47,6 @@ export type StepRunStatus = Pick< "inputs" | "outputs" | "status" | "closedAt" | "scheduledAt" | "logs" >; -export const useStatusForStep = ( - nodeId: string | null, -): StepRunStatus | null => { - const { selectedFlowRun } = useFlowRunsContext(); - - return useMemo(() => { - if (!selectedFlowRun || !nodeId) { - return null; - } - - if (nodeId === "trigger") { - return null; - } - - return selectedFlowRun.steps.find((step) => step.stepId === nodeId) ?? null; - }, [selectedFlowRun, nodeId]); -}; - -export const useStatusForCurrentStep = (): StepRunStatus | null => { - const nodeId = useNodeId(); - - return useStatusForStep(nodeId); -}; - export type SimpleStatus = | "Waiting" | "Information Required" @@ -109,105 +87,196 @@ export const statusToSimpleStatus = ( return simpleStatus; }; -export const useStatusForSteps = ( - steps: { stepId: string }[], -): { - closedAt?: string; - scheduledAt?: string; - overallStatus: SimpleStatus; - statusByStep: Record; -} | null => { +/** + * The id of the step in the flow definition that a step run is of. The engine runs the steps inside a `for-each` once + * per item, each with its own step run id. + */ +const getDefinitionStepId = (stepRunId: string) => + parseStepRunId(stepRunId).stepId; + +/** + * The status shown for a step that has several runs, as a step inside a `for-each` does: the first in this list that + * any of its runs has. + */ +const statusPrecedence: SimpleStatus[] = [ + "Errored", + "Information Required", + "In Progress", + "Waiting", + "Cancelled", + "Complete", + "Skipped", +]; + +const combineStatuses = ( + status: SimpleStatus | undefined, + otherStatus: SimpleStatus, +): SimpleStatus => + status !== undefined && + statusPrecedence.indexOf(status) <= statusPrecedence.indexOf(otherStatus) + ? status + : otherStatus; + +/** + * The run of a step, or for a step inside a `for-each`, the run of it whose status is shown (see `statusPrecedence`). + */ +export const getShownStepRun = ( + stepRuns: Run[], + stepId: string, +): Run | null => + stepRuns + .filter((stepRun) => getDefinitionStepId(stepRun.stepId) === stepId) + .reduce( + (shownStepRun, stepRun) => + !shownStepRun || + statusPrecedence.indexOf(statusToSimpleStatus(stepRun.status)) < + statusPrecedence.indexOf(statusToSimpleStatus(shownStepRun.status)) + ? stepRun + : shownStepRun, + null, + ); + +export const useStatusForStep = ( + nodeId: string | null, +): StepRunStatus | null => { const { selectedFlowRun } = useFlowRunsContext(); return useMemo(() => { - const statusByStep: Record = {}; + if (!selectedFlowRun || !nodeId) { + return null; + } - if (!selectedFlowRun) { + if (nodeId === "trigger") { return null; } - const stepRuns = selectedFlowRun.steps.filter((stepRun) => - steps.find((step) => step.stepId === stepRun.stepId), - ); + return getShownStepRun(selectedFlowRun.steps, nodeId); + }, [selectedFlowRun, nodeId]); +}; - const flowCompletedUnsuccessfully = - selectedFlowRun.closedAt && - selectedFlowRun.status !== FlowRunStatus.Completed; +export const useStatusForCurrentStep = (): StepRunStatus | null => { + const nodeId = useNodeId(); + + return useStatusForStep(nodeId); +}; + +type StepsStatus = { + closedAt?: string; + scheduledAt?: string; + overallStatus: SimpleStatus; + statusByStep: Record; +}; + +/** + * The status of each of `steps` in a flow run, and of them together. A step inside a `for-each` (`parentStepId`) has a + * run per item: those count towards it and towards the `for-each`, which has no run of its own. + */ +export const getStatusForSteps = ( + flowRun: Pick, + steps: { stepId: string; parentStepId?: string }[], +): StepsStatus => { + const statusByStep: Record = {}; + + const stepsById = new Map(steps.map((step) => [step.stepId, step])); + + const stepRuns = flowRun.steps.filter((stepRun) => + stepsById.has(getDefinitionStepId(stepRun.stepId)), + ); + + const flowCompletedUnsuccessfully = + flowRun.closedAt && flowRun.status !== FlowRunStatus.Completed; + + if (stepRuns.length === 0) { + return { + overallStatus: flowCompletedUnsuccessfully ? "Cancelled" : "Waiting", + statusByStep, + }; + } - if (stepRuns.length === 0) { - return { - overallStatus: flowCompletedUnsuccessfully ? "Cancelled" : "Waiting", - statusByStep, - }; + let scheduledAt: string | undefined; + let latestClosedAt: string | undefined; + let allStepsClosed = true; + let status: SimpleStatus = + stepRuns.at(-1)!.status === FlowStepStatus.Cancelled + ? "Cancelled" + : "Complete"; + + let hasError: boolean = false; + let hasWaiting: boolean = false; + let hasInProgress: boolean = false; + + for (const stepRun of stepRuns) { + if (!scheduledAt || stepRun.scheduledAt < scheduledAt) { + scheduledAt = stepRun.scheduledAt; } - let scheduledAt: string | undefined; - let latestClosedAt: string | undefined; - let allStepsClosed = true; - let status: SimpleStatus = - stepRuns.at(-1)!.status === FlowStepStatus.Cancelled - ? "Cancelled" - : "Complete"; - - let hasError: boolean = false; - let hasWaiting: boolean = false; - let hasInProgress: boolean = false; - - for (const stepRun of stepRuns) { - if (!scheduledAt || stepRun.scheduledAt < scheduledAt) { - scheduledAt = stepRun.scheduledAt; + if (stepRun.closedAt) { + if (!latestClosedAt || stepRun.closedAt > latestClosedAt) { + latestClosedAt = stepRun.closedAt; } + } else { + allStepsClosed = false; + } - if (stepRun.closedAt) { - if (!latestClosedAt || stepRun.closedAt > latestClosedAt) { - latestClosedAt = stepRun.closedAt; - } - } else { - allStepsClosed = false; - } + let simpleStatus = statusToSimpleStatus(stepRun.status); - let simpleStatus = statusToSimpleStatus(stepRun.status); + if ( + flowRun.closedAt && + ["In Progress", "Waiting", "Information Required"].includes(simpleStatus) + ) { + simpleStatus = "Cancelled"; + } - if ( - selectedFlowRun.closedAt && - ["In Progress", "Waiting", "Information Required"].includes( - simpleStatus, - ) - ) { - simpleStatus = "Cancelled"; - } + const step = stepsById.get(getDefinitionStepId(stepRun.stepId))!; - statusByStep[stepRun.stepId] = simpleStatus; - - if (simpleStatus === "Errored") { - hasError = true; - } else if ( - simpleStatus === "In Progress" || - simpleStatus === "Information Required" - ) { - hasInProgress = true; - } else if (simpleStatus === "Waiting") { - hasWaiting = true; + for (const stepId of [step.stepId, step.parentStepId]) { + if (stepId !== undefined) { + statusByStep[stepId] = combineStatuses( + statusByStep[stepId], + simpleStatus, + ); } } - if (hasError) { - status = "Errored"; - } else if (hasInProgress) { - status = "In Progress"; - } else if (hasWaiting) { - status = "Waiting"; + if (simpleStatus === "Errored") { + hasError = true; + } else if ( + simpleStatus === "In Progress" || + simpleStatus === "Information Required" + ) { + hasInProgress = true; + } else if (simpleStatus === "Waiting") { + hasWaiting = true; } + } - if (flowCompletedUnsuccessfully) { - status = "Cancelled"; - } + if (hasError) { + status = "Errored"; + } else if (hasInProgress) { + status = "In Progress"; + } else if (hasWaiting) { + status = "Waiting"; + } - return { - closedAt: allStepsClosed ? latestClosedAt : undefined, - scheduledAt, - overallStatus: status, - statusByStep, - }; - }, [selectedFlowRun, steps]); + if (flowCompletedUnsuccessfully) { + status = "Cancelled"; + } + + return { + closedAt: allStepsClosed ? latestClosedAt : undefined, + scheduledAt, + overallStatus: status, + statusByStep, + }; +}; + +export const useStatusForSteps = ( + steps: { stepId: string; parentStepId?: string }[], +): StepsStatus | null => { + const { selectedFlowRun } = useFlowRunsContext(); + + return useMemo( + () => (selectedFlowRun ? getStatusForSteps(selectedFlowRun, steps) : null), + [selectedFlowRun, steps], + ); }; diff --git a/apps/hash-frontend/src/pages/shared/use-flow-definitions.ts b/apps/hash-frontend/src/pages/shared/use-flow-definitions.ts new file mode 100644 index 00000000000..6f1a8aa9ca7 --- /dev/null +++ b/apps/hash-frontend/src/pages/shared/use-flow-definitions.ts @@ -0,0 +1,146 @@ +import { useQuery } from "@apollo/client"; +import { useMemo } from "react"; + +import { getRoots } from "@blockprotocol/graph/stdlib"; +import { extractEntityUuidFromEntityId } from "@blockprotocol/type-system"; +import { deserializeSubgraph } from "@local/hash-graph-sdk/subgraph"; +import { getFlowDefinitionFromEntity } from "@local/hash-isomorphic-utils/flows/mappings"; +import { + currentTimeInstantTemporalAxes, + generateTimeInstantTemporalAxes, + zeroedOntologyResolveDepths, +} from "@local/hash-isomorphic-utils/graph-queries"; +import { systemEntityTypes } from "@local/hash-isomorphic-utils/ontology-type-ids"; + +import { queryEntitySubgraphQuery } from "../../graphql/queries/knowledge/entity.queries"; + +import type { + QueryEntitySubgraphQuery, + QueryEntitySubgraphQueryVariables, +} from "../../graphql/api-types.gen"; +import type { EntityRootType } from "@blockprotocol/graph"; +import type { EntityUuid, Timestamp } from "@blockprotocol/type-system"; +import type { + Filter, + QueryTemporalAxesUnresolved, +} from "@local/hash-graph-client"; +import type { HashEntity } from "@local/hash-graph-sdk/entity"; +import type { FlowDefinitionWithId } from "@local/hash-isomorphic-utils/flows/types"; +import type { FlowDefinition as FlowDefinitionEntity } from "@local/hash-isomorphic-utils/system-types/flowdefinition"; + +const flowDefinitionsQueryVariables = ( + filter: Filter, + temporalAxes: QueryTemporalAxesUnresolved, +): QueryEntitySubgraphQueryVariables => ({ + request: { + filter: { + all: [ + filter, + { + equal: [ + { path: ["type", "baseUrl"] }, + { parameter: systemEntityTypes.flowDefinition.entityTypeBaseUrl }, + ], + }, + { equal: [{ path: ["archived"] }, { parameter: false }] }, + ], + }, + graphResolveDepths: zeroedOntologyResolveDepths, + traversalPaths: [], + temporalAxes, + includeDrafts: false, + includePermissions: false, + }, +}); + +/** + * The flow definitions the `Flow Definition` entities in a query's result hold. An entity whose definition is + * invalid is left out, with an error logged. + */ +const getFlowDefinitionsFromQueryResult = ( + data: QueryEntitySubgraphQuery | undefined, +): FlowDefinitionWithId[] => { + if (!data) { + return []; + } + + const subgraph = deserializeSubgraph< + EntityRootType> + >(data.queryEntitySubgraph.subgraph); + + return getRoots(subgraph).flatMap((entity) => { + const flowDefinitionId = extractEntityUuidFromEntityId( + entity.metadata.recordId.entityId, + ); + + try { + return { + flowDefinitionId, + flowDefinition: getFlowDefinitionFromEntity(entity), + }; + } catch (error) { + // eslint-disable-next-line no-console + console.error( + `Flow definition ${flowDefinitionId} is invalid, so isn't shown: ${(error as Error).message}`, + ); + return []; + } + }); +}; + +/** + * The current edition of every flow definition the user can view. + */ +export const useFlowDefinitions = (): { + flowDefinitions: FlowDefinitionWithId[]; + loading: boolean; +} => { + const { data, loading } = useQuery< + QueryEntitySubgraphQuery, + QueryEntitySubgraphQueryVariables + >(queryEntitySubgraphQuery, { + variables: flowDefinitionsQueryVariables( + { all: [] }, + currentTimeInstantTemporalAxes, + ), + fetchPolicy: "cache-and-network", + }); + + const flowDefinitions = useMemo( + () => getFlowDefinitionsFromQueryResult(data), + [data], + ); + + return { flowDefinitions, loading }; +}; + +/** + * A flow definition as it was at a moment, e.g. when a run started: the edition the run executed. + */ +export const useFlowDefinitionAtTime = (params: { + flowDefinitionId: EntityUuid | null; + timestamp: Timestamp | null; +}): { flowDefinition: FlowDefinitionWithId | null; loading: boolean } => { + const { flowDefinitionId, timestamp } = params; + + const { data, loading } = useQuery< + QueryEntitySubgraphQuery, + QueryEntitySubgraphQueryVariables + >(queryEntitySubgraphQuery, { + variables: flowDefinitionsQueryVariables( + { equal: [{ path: ["uuid"] }, { parameter: flowDefinitionId }] }, + timestamp + ? generateTimeInstantTemporalAxes(timestamp) + : currentTimeInstantTemporalAxes, + ), + skip: !flowDefinitionId || !timestamp, + fetchPolicy: "cache-first", + }); + + const flowDefinition = useMemo( + () => getFlowDefinitionsFromQueryResult(data)[0] ?? null, + [data], + ); + + return { flowDefinition, loading }; +}; diff --git a/apps/hash-frontend/src/pages/workers.page/flow-run-table.tsx b/apps/hash-frontend/src/pages/workers.page/flow-run-table.tsx index 00902cba969..189fadac2fc 100644 --- a/apps/hash-frontend/src/pages/workers.page/flow-run-table.tsx +++ b/apps/hash-frontend/src/pages/workers.page/flow-run-table.tsx @@ -14,8 +14,8 @@ import { InfinityLightIcon, Skeleton, } from "@hashintel/design-system"; +import { isGoalFlowDefinitionId } from "@local/hash-isomorphic-utils/flows/flow-definition-ids"; import { generateWorkerRunPath } from "@local/hash-isomorphic-utils/flows/frontend-paths"; -import { isGoalFlowDefinitionId } from "@local/hash-isomorphic-utils/flows/goal-flow-definitions"; import { Link } from "../../shared/ui/link"; import { useAuthenticatedUser } from "../shared/auth-info-context"; diff --git a/apps/plugin-browser/src/pages/popup/popup-contents/action-center/history/automatically-triggered.tsx b/apps/plugin-browser/src/pages/popup/popup-contents/action-center/history/automatically-triggered.tsx index 42e30c4334b..4ee5b937b23 100644 --- a/apps/plugin-browser/src/pages/popup/popup-contents/action-center/history/automatically-triggered.tsx +++ b/apps/plugin-browser/src/pages/popup/popup-contents/action-center/history/automatically-triggered.tsx @@ -1,7 +1,7 @@ import { TableCell, TableRow } from "@mui/material"; import { useMemo } from "react"; -import { automaticBrowserInferenceFlow } from "@local/hash-isomorphic-utils/flows/browser-plugin-flow-definitions"; +import { flowDefinitionIds } from "@local/hash-isomorphic-utils/flows/flow-definition-ids"; import { EventTable } from "./shared/event-table"; import { HistoryRow } from "./shared/history-row"; @@ -19,7 +19,7 @@ export const AutomaticallyTriggered = ({ unfilteredFlowRuns.filter( (flow) => flow.flowDefinitionId === - automaticBrowserInferenceFlow.flowDefinitionId || + flowDefinitionIds.automaticBrowserInference || flow.requestedPageUrl, ), [unfilteredFlowRuns], diff --git a/apps/plugin-browser/src/pages/popup/popup-contents/action-center/history/manually-triggered.tsx b/apps/plugin-browser/src/pages/popup/popup-contents/action-center/history/manually-triggered.tsx index e20594f0789..386212e2462 100644 --- a/apps/plugin-browser/src/pages/popup/popup-contents/action-center/history/manually-triggered.tsx +++ b/apps/plugin-browser/src/pages/popup/popup-contents/action-center/history/manually-triggered.tsx @@ -1,7 +1,7 @@ import { TableCell, TableRow } from "@mui/material"; import { useMemo } from "react"; -import { manualBrowserInferenceFlow } from "@local/hash-isomorphic-utils/flows/browser-plugin-flow-definitions"; +import { flowDefinitionIds } from "@local/hash-isomorphic-utils/flows/flow-definition-ids"; import { EventTable } from "./shared/event-table"; import { HistoryRow } from "./shared/history-row"; @@ -18,7 +18,7 @@ export const ManuallyTriggered = ({ () => unfilteredFlowRuns.filter( (flow) => - flow.flowDefinitionId === manualBrowserInferenceFlow.flowDefinitionId, + flow.flowDefinitionId === flowDefinitionIds.manualBrowserInference, ), [unfilteredFlowRuns], ); diff --git a/apps/plugin-browser/src/pages/popup/popup-contents/action-center/history/shared/table-label.tsx b/apps/plugin-browser/src/pages/popup/popup-contents/action-center/history/shared/table-label.tsx index ba0588dc2f3..32e93187322 100644 --- a/apps/plugin-browser/src/pages/popup/popup-contents/action-center/history/shared/table-label.tsx +++ b/apps/plugin-browser/src/pages/popup/popup-contents/action-center/history/shared/table-label.tsx @@ -1,10 +1,7 @@ import { Link, Stack, Typography } from "@mui/material"; import { ArrowUpRightRegularIcon } from "@hashintel/design-system"; -import { - automaticBrowserInferenceFlow, - manualBrowserInferenceFlow, -} from "@local/hash-isomorphic-utils/flows/browser-plugin-flow-definitions"; +import { flowDefinitionIds } from "@local/hash-isomorphic-utils/flows/flow-definition-ids"; import { generatedFilteredWorkersPath } from "@local/hash-isomorphic-utils/flows/frontend-paths"; export const TableLabel = ({ type }: { type: "manual" | "automatic" }) => { @@ -17,8 +14,8 @@ export const TableLabel = ({ type }: { type: "manual" | "automatic" }) => { href={`${FRONTEND_ORIGIN}${generatedFilteredWorkersPath({ flowDefinitionIds: type === "manual" - ? [manualBrowserInferenceFlow.flowDefinitionId] - : [automaticBrowserInferenceFlow.flowDefinitionId], + ? [flowDefinitionIds.manualBrowserInference] + : [flowDefinitionIds.automaticBrowserInference], })}`} sx={{ alignItems: "center", diff --git a/apps/plugin-browser/src/pages/popup/popup-contents/action-center/shared/use-flow-runs.ts b/apps/plugin-browser/src/pages/popup/popup-contents/action-center/shared/use-flow-runs.ts index a7fce63eeb2..1c324f86404 100644 --- a/apps/plugin-browser/src/pages/popup/popup-contents/action-center/shared/use-flow-runs.ts +++ b/apps/plugin-browser/src/pages/popup/popup-contents/action-center/shared/use-flow-runs.ts @@ -1,10 +1,7 @@ import { useEffect, useMemo, useState } from "react"; import { extractWebIdFromEntityId } from "@blockprotocol/type-system"; -import { - automaticBrowserInferenceFlow, - manualBrowserInferenceFlow, -} from "@local/hash-isomorphic-utils/flows/browser-plugin-flow-definitions"; +import { flowDefinitionIds } from "@local/hash-isomorphic-utils/flows/flow-definition-ids"; import { getMinimalFlowRunsQuery } from "../../../../../graphql/queries/flow.queries"; import { queryGraphQlApi } from "../../../../../shared/query-graphql-api"; @@ -93,9 +90,9 @@ const getFlowRuns = async ({ for (const flowRun of unfilteredFlowRuns) { if ( flowRun.flowDefinitionId === - manualBrowserInferenceFlow.flowDefinitionId || + flowDefinitionIds.manualBrowserInference || flowRun.flowDefinitionId === - automaticBrowserInferenceFlow.flowDefinitionId + flowDefinitionIds.automaticBrowserInference ) { flowRunsOfInterest.push(mapFlowRunToMinimalFlowRun(flowRun)); } diff --git a/apps/plugin-browser/src/scripts/background/infer-entities.ts b/apps/plugin-browser/src/scripts/background/infer-entities.ts index c5cac333e1b..32eb36515b3 100644 --- a/apps/plugin-browser/src/scripts/background/infer-entities.ts +++ b/apps/plugin-browser/src/scripts/background/infer-entities.ts @@ -2,10 +2,7 @@ import { v4 as uuid } from "uuid"; import browser from "webextension-polyfill"; import { entityIdFromComponents } from "@blockprotocol/type-system"; -import { - automaticBrowserInferenceFlow, - manualBrowserInferenceFlow, -} from "@local/hash-isomorphic-utils/flows/browser-plugin-flow-definitions"; +import { flowDefinitionIds } from "@local/hash-isomorphic-utils/flows/flow-definition-ids"; import { sleep } from "@local/hash-isomorphic-utils/sleep"; import { FlowRunStatus } from "../../graphql/api-types.gen"; @@ -371,10 +368,10 @@ export const inferEntities = async ( await sendInferEntitiesMessage(inferenceArgs); - const flowDefinition = + const flowDefinitionId = trigger === "automatic" - ? automaticBrowserInferenceFlow - : manualBrowserInferenceFlow; + ? flowDefinitionIds.automaticBrowserInference + : flowDefinitionIds.manualBrowserInference; /** * Optimistically add the run to local storage so that it appears in the history tab immediately. @@ -383,7 +380,7 @@ export const inferEntities = async ( await setLocalPendingRuns((existingValue) => [ ...(existingValue ?? []), { - flowDefinitionId: flowDefinition.flowDefinitionId, + flowDefinitionId, flowRunEntityId: entityIdFromComponents(webId, requestUuid), closedAt: null, executedAt: new Date().toISOString(), diff --git a/libs/@local/hash-backend-utils/src/flows/payload-storage.test.ts b/libs/@local/hash-backend-utils/src/flows/payload-storage.test.ts index 943cb6a0961..26eb3be60b5 100644 --- a/libs/@local/hash-backend-utils/src/flows/payload-storage.test.ts +++ b/libs/@local/hash-backend-utils/src/flows/payload-storage.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from "vitest"; +import { generateStepRunId } from "@local/hash-isomorphic-utils/flows/step-run-id"; import { appendCollectedValue, getArrayPayloadItems, @@ -127,9 +128,12 @@ describe("stored arrays", () => { const { storageProvider, getDownloadCount } = createMemoryStorageProvider(); const branches = await Promise.all([ - storeEntities(storageProvider, "3.2~0", [entity(0)]), - storeEntities(storageProvider, "3.2~1", [entity(1), entity(2)]), - storeEntities(storageProvider, "3.2~2", [entity(3)]), + storeEntities(storageProvider, generateStepRunId("3.2", 0), [entity(0)]), + storeEntities(storageProvider, generateStepRunId("3.2", 1), [ + entity(1), + entity(2), + ]), + storeEntities(storageProvider, generateStepRunId("3.2", 2), [entity(3)]), ]); const collected = branches.reduce< @@ -168,8 +172,11 @@ describe("stored arrays", () => { const { storageProvider, getDownloadCount } = createMemoryStorageProvider(); const branches = await Promise.all([ - storeEntities(storageProvider, "3.2~0", [entity(0)]), - storeEntities(storageProvider, "3.2~1", [entity(1), entity(2)]), + storeEntities(storageProvider, generateStepRunId("3.2", 0), [entity(0)]), + storeEntities(storageProvider, generateStepRunId("3.2", 1), [ + entity(1), + entity(2), + ]), ]); const collected = appendCollectedValue( @@ -198,7 +205,11 @@ describe("stored arrays", () => { it("refuses to mix stored and inline arrays when collecting", async () => { const { storageProvider } = createMemoryStorageProvider(); - const stored = await storeEntities(storageProvider, "3.2~0", [entity(0)]); + const stored = await storeEntities( + storageProvider, + generateStepRunId("3.2", 0), + [entity(0)], + ); expect(() => appendCollectedValue([entity(1)], stored)).toThrow(); expect(() => appendCollectedValue(stored, [entity(1)])).toThrow(); diff --git a/libs/@local/hash-backend-utils/src/flows/process-flow-workflow.ts b/libs/@local/hash-backend-utils/src/flows/process-flow-workflow.ts index 7dbf46204e0..727211411d6 100644 --- a/libs/@local/hash-backend-utils/src/flows/process-flow-workflow.ts +++ b/libs/@local/hash-backend-utils/src/flows/process-flow-workflow.ts @@ -7,6 +7,10 @@ import { import { actionDefinitions } from "@local/hash-isomorphic-utils/flows/action-definitions"; import { getFlowType } from "@local/hash-isomorphic-utils/flows/get-flow-type"; +import { + generateStepRunId, + parseStepRunId, +} from "@local/hash-isomorphic-utils/flows/step-run-id"; import { assertNoStoredPayloadRefsInFlowInputs, getArrayPayloadItems, @@ -37,7 +41,7 @@ import type { CreateFlowActivities, ProxyFlowActivity, } from "./action-types.js"; -import type { EntityUuid } from "@blockprotocol/type-system"; +import type { EntityUuid, Timestamp } from "@blockprotocol/type-system"; import type { FlowActionStepOutput } from "@local/hash-isomorphic-utils/flows/action-definitions"; import type { BaseRunFlowWorkflowParams, @@ -83,10 +87,10 @@ const getProducerRunStepId = ({ consumerStepId: string; producerStepId: string; }) => { - const [_, branchIndex] = consumerStepId.split("~"); + const { itemIndex } = parseStepRunId(consumerStepId); - if (branchIndex !== undefined) { - const producerInBranch = `${producerStepId}~${branchIndex}`; + if (itemIndex !== undefined) { + const producerInBranch = generateStepRunId(producerStepId, itemIndex); if ( getAllStepsInFlow(flow).some(({ stepId }) => stepId === producerInBranch) @@ -320,9 +324,11 @@ const processFlow = async < } = params; /* - * The run executes the definition's edition current when it starts. + * The run executes the definition's edition current when it starts: at the start of its execution chain, which a + * retry, replay or reset keeps. The frontend draws a run with the edition current then too. */ const flowDefinitionResult = await getFlowDefinitionActivity({ + atTime: workflowInfo().startTime.toISOString() as Timestamp, flowDefinitionId, userAuthentication, }); @@ -616,14 +622,14 @@ const processFlow = async < ? initializeActionStep({ flowInputs, stepDefinition, - overrideStepId: `${stepDefinition.stepId}~${index}`, + overrideStepId: generateStepRunId(stepDefinition.stepId, index), existingFlow: flow, item, }) : initializeForEachStep({ flowInputs, stepDefinition, - overrideStepId: `${stepDefinition.stepId}~${index}`, + overrideStepId: generateStepRunId(stepDefinition.stepId, index), existingFlow: flow, item, }), diff --git a/libs/@local/hash-backend-utils/src/flows/process-flow-workflow/common-activities/get-flow-definition-activity.ts b/libs/@local/hash-backend-utils/src/flows/process-flow-workflow/common-activities/get-flow-definition-activity.ts index 52015fa23b4..613110b82ed 100644 --- a/libs/@local/hash-backend-utils/src/flows/process-flow-workflow/common-activities/get-flow-definition-activity.ts +++ b/libs/@local/hash-backend-utils/src/flows/process-flow-workflow/common-activities/get-flow-definition-activity.ts @@ -6,18 +6,21 @@ import type { ActorEntityUuid, EntityId, EntityUuid, + Timestamp, } from "@blockprotocol/type-system"; import type { GraphApi } from "@local/hash-graph-client"; import type { FlowDefinition } from "@local/hash-isomorphic-utils/flows/types"; export type GetFlowDefinitionActivityParams = { + /** When the run started: it executes the edition of the definition current then. */ + atTime: Timestamp; flowDefinitionId: EntityUuid; userAuthentication: { actorId: ActorEntityUuid }; }; /** - * Loads the definition a run executes from its `Flow Definition` entity, and validates it. Returns the entity's id - * too, for the run to link to. + * Loads the definition a run executes from its `Flow Definition` entity, as it was at `atTime`, and validates it. + * Returns the entity's id too, for the run to link to. */ export const getFlowDefinitionActivity = async ( params: GetFlowDefinitionActivityParams & { graphApiClient: GraphApi }, @@ -29,9 +32,11 @@ export const getFlowDefinitionActivity = async ( } | { status: "not-found" | "invalid"; errorMessage: string } > => { - const { flowDefinitionId, graphApiClient, userAuthentication } = params; + const { atTime, flowDefinitionId, graphApiClient, userAuthentication } = + params; const entity = await getFlowDefinitionEntityById({ + atTime, flowDefinitionId, graphApiClient, userAuthentication, diff --git a/libs/@local/hash-backend-utils/src/flows/process-flow-workflow/get-step-definition-from-flow.ts b/libs/@local/hash-backend-utils/src/flows/process-flow-workflow/get-step-definition-from-flow.ts index 780b7ed6a71..e97faa298ef 100644 --- a/libs/@local/hash-backend-utils/src/flows/process-flow-workflow/get-step-definition-from-flow.ts +++ b/libs/@local/hash-backend-utils/src/flows/process-flow-workflow/get-step-definition-from-flow.ts @@ -1,3 +1,4 @@ +import { parseStepRunId } from "@local/hash-isomorphic-utils/flows/step-run-id"; import { getAllStepDefinitionsInFlowDefinition } from "@local/hash-isomorphic-utils/flows/util"; import type { @@ -20,7 +21,7 @@ export const getStepDefinitionFromFlowDefinition = < }): T extends ActionStep ? ActionStepDefinition : ForEachStepDefinition => { const { step, flowDefinition } = params; - const [stepIdWithoutIndex] = step.stepId.split("~"); + const { stepId: stepIdWithoutIndex } = parseStepRunId(step.stepId); const stepDefinition = getAllStepDefinitionsInFlowDefinition( flowDefinition, diff --git a/libs/@local/hash-backend-utils/src/flows/process-flow-workflow/pass-outputs-to-unprocessed-steps.ts b/libs/@local/hash-backend-utils/src/flows/process-flow-workflow/pass-outputs-to-unprocessed-steps.ts index 7d7c486fe5b..516daaf4aa0 100644 --- a/libs/@local/hash-backend-utils/src/flows/process-flow-workflow/pass-outputs-to-unprocessed-steps.ts +++ b/libs/@local/hash-backend-utils/src/flows/process-flow-workflow/pass-outputs-to-unprocessed-steps.ts @@ -1,3 +1,4 @@ +import { parseStepRunId } from "@local/hash-isomorphic-utils/flows/step-run-id"; import { createStepShapeResolver } from "@local/hash-isomorphic-utils/flows/step-shapes"; import { appendCollectedValue, @@ -33,7 +34,8 @@ export const passOutputsToUnprocessedSteps = (params: { }): Omit, "contents"> => { const { flow, flowDefinition, stepId, processedStepIds, outputs } = params; - const [currentStepIdWithoutIndex, currentStepIdIndex] = stepId.split("~"); + const { stepId: currentStepIdWithoutIndex, itemIndex: currentItemIndex } = + parseStepRunId(stepId); const unprocessedSteps = getAllStepsInFlow(flow).filter( (step) => @@ -43,9 +45,14 @@ export const passOutputsToUnprocessedSteps = (params: { ); for (const unprocessedStep of unprocessedSteps) { - const [_, unprocessedStepIdIndex] = unprocessedStep.stepId.split("~"); + const { itemIndex: unprocessedItemIndex } = parseStepRunId( + unprocessedStep.stepId, + ); - if (currentStepIdIndex && currentStepIdIndex !== unprocessedStepIdIndex) { + if ( + currentItemIndex !== undefined && + currentItemIndex !== unprocessedItemIndex + ) { /** * A step in a for-each branch only feeds steps in the same branch. */ @@ -144,7 +151,7 @@ export const passOutputsToUnprocessedSteps = (params: { processedStep.branchValues = { ...processedStep.branchValues, - [Number(currentStepIdIndex)]: collectedOutput.payload.value, + [Number(currentItemIndex)]: collectedOutput.payload.value, }; // eslint-disable-next-line @typescript-eslint/no-use-before-define -- the two pass each other's outputs on @@ -250,7 +257,7 @@ export const collectSkippedBranchStep = (params: { }): Omit, "contents"> => { const { flow, flowDefinition, stepId, processedStepIds } = params; - const [stepIdWithoutIndex, branchIndex] = stepId.split("~"); + const { stepId: stepIdWithoutIndex, itemIndex } = parseStepRunId(stepId); const forEachStep = getAllStepsInFlow(flow).find( (step): step is ForEachStep => @@ -268,7 +275,7 @@ export const collectSkippedBranchStep = (params: { forEachStep.branchValues = { ...forEachStep.branchValues, - [Number(branchIndex)]: null, + [Number(itemIndex)]: null, }; return completeForEachStep({ diff --git a/libs/@local/hash-backend-utils/src/flows/shared/get-flow-definition-entity-by-id.ts b/libs/@local/hash-backend-utils/src/flows/shared/get-flow-definition-entity-by-id.ts index 3c6209ebc67..c988061c631 100644 --- a/libs/@local/hash-backend-utils/src/flows/shared/get-flow-definition-entity-by-id.ts +++ b/libs/@local/hash-backend-utils/src/flows/shared/get-flow-definition-entity-by-id.ts @@ -1,21 +1,30 @@ import { type HashEntity, queryEntities } from "@local/hash-graph-sdk/entity"; -import { currentTimeInstantTemporalAxes } from "@local/hash-isomorphic-utils/graph-queries"; +import { + currentTimeInstantTemporalAxes, + generateTimeInstantTemporalAxes, +} from "@local/hash-isomorphic-utils/graph-queries"; import { systemEntityTypes } from "@local/hash-isomorphic-utils/ontology-type-ids"; -import type { ActorEntityUuid, EntityUuid } from "@blockprotocol/type-system"; +import type { + ActorEntityUuid, + EntityUuid, + Timestamp, +} from "@blockprotocol/type-system"; import type { GraphApi } from "@local/hash-graph-client"; import type { FlowDefinition as FlowDefinitionEntity } from "@local/hash-isomorphic-utils/system-types/flowdefinition"; /** * The `Flow Definition` entity of a flow definition, whose uuid is the flow definition's id: e.g. for a run or - * trigger to link to. + * trigger to link to. Its edition current at `atTime`, if given, and otherwise its current edition. */ export const getFlowDefinitionEntityById = async (params: { + atTime?: Timestamp; flowDefinitionId: EntityUuid; graphApiClient: GraphApi; userAuthentication: { actorId: ActorEntityUuid }; }): Promise | null> => { - const { flowDefinitionId, graphApiClient, userAuthentication } = params; + const { atTime, flowDefinitionId, graphApiClient, userAuthentication } = + params; const { entities: [flowDefinitionEntity], @@ -36,7 +45,9 @@ export const getFlowDefinitionEntityById = async (params: { }, ], }, - temporalAxes: currentTimeInstantTemporalAxes, + temporalAxes: atTime + ? generateTimeInstantTemporalAxes(atTime) + : currentTimeInstantTemporalAxes, includeDrafts: false, includePermissions: false, }, diff --git a/libs/@local/hash-isomorphic-utils/src/flows/browser-plugin-flow-definitions.ts b/libs/@local/hash-isomorphic-utils/src/flows/definitions/browser-plugin-flow-definitions.ts similarity index 91% rename from libs/@local/hash-isomorphic-utils/src/flows/browser-plugin-flow-definitions.ts rename to libs/@local/hash-isomorphic-utils/src/flows/definitions/browser-plugin-flow-definitions.ts index 551661e5e8d..faf8e5e76ab 100644 --- a/libs/@local/hash-isomorphic-utils/src/flows/browser-plugin-flow-definitions.ts +++ b/libs/@local/hash-isomorphic-utils/src/flows/definitions/browser-plugin-flow-definitions.ts @@ -1,5 +1,6 @@ -import { typedActionDefinitions as actions } from "./action-definitions.js"; -import { defineFlow, flowInput } from "./define-flow.js"; +import { typedActionDefinitions as actions } from "../action-definitions.js"; +import { defineFlow, flowInput } from "../define-flow.js"; +import { flowDefinitionIds } from "../flow-definition-ids.js"; const persistedEntitiesOutputDescription = "The entities created or updated by the flow run"; @@ -12,8 +13,7 @@ const failedEntityProposalsOutputDescription = */ export const manualBrowserInferenceFlow = defineFlow( { - /* manual-browser-inference */ - flowDefinitionId: "a7bc5e44-f1ce-5126-b5db-8eb4cf054116", + flowDefinitionId: flowDefinitionIds.manualBrowserInference, name: "Analyze webpage", description: "Find entities of the requested types in a web page", inputs: { @@ -70,8 +70,7 @@ export const manualBrowserInferenceFlow = defineFlow( */ export const automaticBrowserInferenceFlow = defineFlow( { - /* automatic-browser-inference */ - flowDefinitionId: "a4385fa8-a457-584e-b81e-f7e49f3d0b3c", + flowDefinitionId: flowDefinitionIds.automaticBrowserInference, name: "Auto-analyze webpage", description: "Find entities in a web page according to the user's passive analysis settings", diff --git a/libs/@local/hash-isomorphic-utils/src/flows/file-flow-definitions.ts b/libs/@local/hash-isomorphic-utils/src/flows/definitions/file-flow-definitions.ts similarity index 81% rename from libs/@local/hash-isomorphic-utils/src/flows/file-flow-definitions.ts rename to libs/@local/hash-isomorphic-utils/src/flows/definitions/file-flow-definitions.ts index 650c62ff27e..71563662849 100644 --- a/libs/@local/hash-isomorphic-utils/src/flows/file-flow-definitions.ts +++ b/libs/@local/hash-isomorphic-utils/src/flows/definitions/file-flow-definitions.ts @@ -1,5 +1,6 @@ -import { typedActionDefinitions as actions } from "./action-definitions.js"; -import { defineFlow, flowInput } from "./define-flow.js"; +import { typedActionDefinitions as actions } from "../action-definitions.js"; +import { defineFlow, flowInput } from "../define-flow.js"; +import { flowDefinitionIds } from "../flow-definition-ids.js"; /** * Started when a file is uploaded (see `graphql/resolvers/knowledge/file/shared.ts` in `hash-api`), with the @@ -7,8 +8,7 @@ import { defineFlow, flowInput } from "./define-flow.js"; */ export const inferMetadataFromDocumentFlow = defineFlow( { - /* infer-metadata-from-document */ - flowDefinitionId: "936f6a29-a964-533b-ae1a-03bed7da6471", + flowDefinitionId: flowDefinitionIds.inferMetadataFromDocument, name: "Infer metadata from document", description: "Infer metadata from a document, assign appropriate type to document, and create associated entities.", diff --git a/libs/@local/hash-isomorphic-utils/src/flows/frontend-flow-definitions.ts b/libs/@local/hash-isomorphic-utils/src/flows/definitions/frontend-flow-definitions.ts similarity index 91% rename from libs/@local/hash-isomorphic-utils/src/flows/frontend-flow-definitions.ts rename to libs/@local/hash-isomorphic-utils/src/flows/definitions/frontend-flow-definitions.ts index 531af2ad3d3..1fea517b591 100644 --- a/libs/@local/hash-isomorphic-utils/src/flows/frontend-flow-definitions.ts +++ b/libs/@local/hash-isomorphic-utils/src/flows/definitions/frontend-flow-definitions.ts @@ -1,7 +1,8 @@ -import { typedActionDefinitions as actions } from "./action-definitions.js"; -import { defineFlow, flowInput, type StepOutputs } from "./define-flow.js"; +import { typedActionDefinitions as actions } from "../action-definitions.js"; +import { defineFlow, flowInput, type StepOutputs } from "../define-flow.js"; +import { flowDefinitionIds } from "../flow-definition-ids.js"; -import type { TypedActionDefinitions } from "./action-definitions.js"; +import type { TypedActionDefinitions } from "../action-definitions.js"; /** * The outputs the frontend reads from both dashboard item flows. @@ -38,8 +39,7 @@ const dashboardItemOutputs = ({ */ export const configureDashboardItemFlow = defineFlow( { - /* configure-dashboard-item */ - flowDefinitionId: "bf7f36a2-221e-5e7d-9879-289c1f56c148", + flowDefinitionId: flowDefinitionIds.configureDashboardItem, name: "Configure Dashboard Item", description: "Generate query, analyze data, and create chart configuration for a dashboard item", @@ -84,8 +84,7 @@ export const configureDashboardItemFlow = defineFlow( export const refineDashboardItemFlow = defineFlow( { - /* refine-dashboard-item */ - flowDefinitionId: "6216ef71-5b7b-5468-bf12-9a34e30604b5", + flowDefinitionId: flowDefinitionIds.refineDashboardItem, name: "Refine Dashboard Item", description: "Plan and apply a scoped refinement to an existing dashboard item", diff --git a/libs/@local/hash-isomorphic-utils/src/flows/ftse-investors-flow-definition.ts b/libs/@local/hash-isomorphic-utils/src/flows/definitions/ftse-investors-flow-definition.ts similarity index 92% rename from libs/@local/hash-isomorphic-utils/src/flows/ftse-investors-flow-definition.ts rename to libs/@local/hash-isomorphic-utils/src/flows/definitions/ftse-investors-flow-definition.ts index 1e53daee740..833c5eb32c0 100644 --- a/libs/@local/hash-isomorphic-utils/src/flows/ftse-investors-flow-definition.ts +++ b/libs/@local/hash-isomorphic-utils/src/flows/definitions/ftse-investors-flow-definition.ts @@ -1,5 +1,6 @@ -import { typedActionDefinitions as actions } from "./action-definitions.js"; -import { constant, defineFlow, flowInput } from "./define-flow.js"; +import { typedActionDefinitions as actions } from "../action-definitions.js"; +import { constant, defineFlow, flowInput } from "../define-flow.js"; +import { flowDefinitionIds } from "../flow-definition-ids.js"; /** * Researches the FTSE350's constituents, then each constituent's investors in a for-each step. @@ -9,8 +10,7 @@ import { constant, defineFlow, flowInput } from "./define-flow.js"; */ export const ftseInvestorsFlow = defineFlow( { - /* ftse-350-investors */ - flowDefinitionId: "25f32cd2-6f6e-5641-92b7-d9a90e517e0f", + flowDefinitionId: flowDefinitionIds.ftseInvestors, name: "FTSE350 investors", description: "Research the FTSE350 index, its constituents, and the top investors in the index", diff --git a/libs/@local/hash-isomorphic-utils/src/flows/goal-flow-definitions.ts b/libs/@local/hash-isomorphic-utils/src/flows/definitions/goal-flow-definitions.ts similarity index 87% rename from libs/@local/hash-isomorphic-utils/src/flows/goal-flow-definitions.ts rename to libs/@local/hash-isomorphic-utils/src/flows/definitions/goal-flow-definitions.ts index 4a4bad6ff52..37b4f94d722 100644 --- a/libs/@local/hash-isomorphic-utils/src/flows/goal-flow-definitions.ts +++ b/libs/@local/hash-isomorphic-utils/src/flows/definitions/goal-flow-definitions.ts @@ -1,5 +1,6 @@ -import { typedActionDefinitions as actions } from "./action-definitions.js"; -import { constant, defineFlow, flowInput } from "./define-flow.js"; +import { typedActionDefinitions as actions } from "../action-definitions.js"; +import { constant, defineFlow, flowInput } from "../define-flow.js"; +import { flowDefinitionIds } from "../flow-definition-ids.js"; const goalFlowInputs = { researchGuidance: flowInput("Text", { label: "Research guidance" }), @@ -35,8 +36,7 @@ export type GoalFlowInputName = keyof typeof goalFlowInputs; */ export const goalFlow = defineFlow( { - /* research-goal */ - flowDefinitionId: "ea4ac27d-7fe2-5400-ac72-ab4b41de9f67", + flowDefinitionId: flowDefinitionIds.goal, name: "Research goal", description: "Discover entities according to a research brief and save them to HASH, optionally writing a report and saving the entities to a Google Sheet", @@ -87,9 +87,3 @@ export const goalFlow = defineFlow( }; }, ); - -/** - * Whether a run or definition is of the goal flow. - */ -export const isGoalFlowDefinitionId = (flowDefinitionId: string) => - flowDefinitionId === goalFlow.flowDefinitionId; diff --git a/libs/@local/hash-isomorphic-utils/src/flows/integration-flow-definitions.ts b/libs/@local/hash-isomorphic-utils/src/flows/definitions/integration-flow-definitions.ts similarity index 90% rename from libs/@local/hash-isomorphic-utils/src/flows/integration-flow-definitions.ts rename to libs/@local/hash-isomorphic-utils/src/flows/definitions/integration-flow-definitions.ts index e09bd930767..4967d41510d 100644 --- a/libs/@local/hash-isomorphic-utils/src/flows/integration-flow-definitions.ts +++ b/libs/@local/hash-isomorphic-utils/src/flows/definitions/integration-flow-definitions.ts @@ -1,13 +1,13 @@ -import { typedActionDefinitions as actions } from "./action-definitions.js"; -import { defineFlow, flowInput } from "./define-flow.js"; +import { typedActionDefinitions as actions } from "../action-definitions.js"; +import { defineFlow, flowInput } from "../define-flow.js"; +import { flowDefinitionIds } from "../flow-definition-ids.js"; /** * Flow for fetching historical flight arrivals for an airport over a date range and persisting them to the graph. */ export const historicalFlightsFlow = defineFlow( { - /* historical-flights */ - flowDefinitionId: "113ce2ec-6a12-56b6-b495-072475889b37", + flowDefinitionId: flowDefinitionIds.historicalFlights, name: "Get Historical Flights", description: "Fetch and save historical flight arrivals for an airport over a date range.", @@ -47,8 +47,7 @@ export const historicalFlightsFlow = defineFlow( */ export const scheduledFlightsFlow = defineFlow( { - /* scheduled-flights */ - flowDefinitionId: "96a80402-c41b-5978-a018-b30f7e09bfe0", + flowDefinitionId: flowDefinitionIds.scheduledFlights, name: "Get Scheduled Flights", description: "Fetch and save scheduled flight arrivals for an airport on a given date, with position updates for live flights.", diff --git a/libs/@local/hash-isomorphic-utils/src/flows/flow-definition-ids.ts b/libs/@local/hash-isomorphic-utils/src/flows/flow-definition-ids.ts new file mode 100644 index 00000000000..6a12288f7db --- /dev/null +++ b/libs/@local/hash-isomorphic-utils/src/flows/flow-definition-ids.ts @@ -0,0 +1,28 @@ +import type { EntityUuid } from "@blockprotocol/type-system"; + +/** + * The ids of the flows defined in code, which are also the entity uuids of their `Flow Definition` entities. + * + * Code that starts a run of one of these flows, or recognises its runs, uses its id from here. The definitions are + * in `definitions/`, which only seeding, the compiler and tests use: runs execute the definition stored in the + * graph, and the frontend draws it from there. + */ +export const flowDefinitionIds = { + goal: "ea4ac27d-7fe2-5400-ac72-ab4b41de9f67" as EntityUuid, + manualBrowserInference: "a7bc5e44-f1ce-5126-b5db-8eb4cf054116" as EntityUuid, + automaticBrowserInference: + "a4385fa8-a457-584e-b81e-f7e49f3d0b3c" as EntityUuid, + inferMetadataFromDocument: + "936f6a29-a964-533b-ae1a-03bed7da6471" as EntityUuid, + configureDashboardItem: "bf7f36a2-221e-5e7d-9879-289c1f56c148" as EntityUuid, + refineDashboardItem: "6216ef71-5b7b-5468-bf12-9a34e30604b5" as EntityUuid, + historicalFlights: "113ce2ec-6a12-56b6-b495-072475889b37" as EntityUuid, + scheduledFlights: "96a80402-c41b-5978-a018-b30f7e09bfe0" as EntityUuid, + ftseInvestors: "25f32cd2-6f6e-5641-92b7-d9a90e517e0f" as EntityUuid, +} as const; + +/** + * Whether a run or definition is of the goal flow. + */ +export const isGoalFlowDefinitionId = (flowDefinitionId: string) => + flowDefinitionId === flowDefinitionIds.goal; diff --git a/libs/@local/hash-isomorphic-utils/src/flows/flow-definition-schema.ts b/libs/@local/hash-isomorphic-utils/src/flows/flow-definition-schema.ts index 8b138bfe777..009ddf51322 100644 --- a/libs/@local/hash-isomorphic-utils/src/flows/flow-definition-schema.ts +++ b/libs/@local/hash-isomorphic-utils/src/flows/flow-definition-schema.ts @@ -1,5 +1,6 @@ import { z } from "zod"; +import { stepRunIdSeparator } from "./step-run-id.js"; import { allPayloadKinds } from "./types.js"; import type { @@ -23,8 +24,8 @@ const connectionOptionsShape = { const stepIdSchema = z .string() .min(1) - .refine((stepId) => !stepId.includes("~"), { - message: 'A step id cannot contain "~"', + .refine((stepId) => !stepId.includes(stepRunIdSeparator), { + message: `A step id cannot contain "${stepRunIdSeparator}"`, }); const stepInputSourceSchema = z.discriminatedUnion("kind", [ diff --git a/libs/@local/hash-isomorphic-utils/src/flows/step-run-id.ts b/libs/@local/hash-isomorphic-utils/src/flows/step-run-id.ts new file mode 100644 index 00000000000..0d315c6f514 --- /dev/null +++ b/libs/@local/hash-isomorphic-utils/src/flows/step-run-id.ts @@ -0,0 +1,34 @@ +/** + * Separates a step's id from the index of the `for-each` item a run of the step is for. Step ids can't contain it. + */ +export const stepRunIdSeparator = "~"; + +/** + * The id of the run of a step inside a `for-each` for one of its items: the step's id and the item's index. + */ +export type ForEachItemStepRunId = + `${string}${typeof stepRunIdSeparator}${number}`; + +export const generateStepRunId = ( + stepId: string, + itemIndex: number, +): ForEachItemStepRunId => `${stepId}${stepRunIdSeparator}${itemIndex}`; + +/** + * The step a run is of, and the index of the `for-each` item it's for, if any. A run's id is its step's own id, or for + * a step inside a `for-each`, a {@link ForEachItemStepRunId} per item. + */ +export const parseStepRunId = ( + stepRunId: string, +): { stepId: string; itemIndex?: number } => { + const separatorIndex = stepRunId.indexOf(stepRunIdSeparator); + + if (separatorIndex === -1) { + return { stepId: stepRunId }; + } + + return { + stepId: stepRunId.slice(0, separatorIndex), + itemIndex: Number(stepRunId.slice(separatorIndex + 1)), + }; +}; diff --git a/libs/@local/hash-isomorphic-utils/src/flows/validate-flow-definition.test.ts b/libs/@local/hash-isomorphic-utils/src/flows/validate-flow-definition.test.ts index cb4e98d0a54..568a7d5360a 100644 --- a/libs/@local/hash-isomorphic-utils/src/flows/validate-flow-definition.test.ts +++ b/libs/@local/hash-isomorphic-utils/src/flows/validate-flow-definition.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; -import { ftseInvestorsFlow } from "./ftse-investors-flow-definition.js"; -import { goalFlow } from "./goal-flow-definitions.js"; +import { ftseInvestorsFlow } from "./definitions/ftse-investors-flow-definition.js"; +import { goalFlow } from "./definitions/goal-flow-definitions.js"; import { type FlowDefinitionDiagnostic, validateFlowDefinition,