diff --git a/apps/hash-ai-worker-ts/src/activities/flow-activities.ts b/apps/hash-ai-worker-ts/src/activities/flow-activities.ts index 398b9d7f7c5..1c7fc4b63da 100644 --- a/apps/hash-ai-worker-ts/src/activities/flow-activities.ts +++ b/apps/hash-ai-worker-ts/src/activities/flow-activities.ts @@ -1,5 +1,6 @@ +import type { CreateFlowActivities } from "@local/hash-backend-utils/flows"; import type { VaultClient } from "@local/hash-backend-utils/vault"; -import type { ActionDefinitionId } from "@local/hash-isomorphic-utils/flows/action-definitions"; +import type { AiFlowActionDefinitionId } from "@local/hash-isomorphic-utils/flows/action-definitions"; import { answerQuestionAction } from "./flow-activities/answer-question-action.js"; import { generateFlowRunName } from "./flow-activities/generate-flow-run-name-activity.js"; @@ -11,19 +12,14 @@ import { inferEntitiesFromContentAction } from "./flow-activities/infer-entities import { inferMetadataFromDocumentAction } from "./flow-activities/infer-metadata-from-document-action.js"; import { persistEntitiesAction } from "./flow-activities/persist-entities-action.js"; import { persistEntityAction } from "./flow-activities/persist-entity-action.js"; -import { persistFlowActivity } from "./flow-activities/persist-flow-activity.js"; import { processAutomaticBrowsingSettingsAction } from "./flow-activities/process-automatic-browsing-settings-action.js"; import { researchEntitiesAction } from "./flow-activities/research-entities-action.js"; -import type { FlowActionActivity } from "./flow-activities/types.js"; -import { userHasPermissionToRunFlowInWebActivity } from "./flow-activities/user-has-permission-to-run-flow-in-web-activity.js"; import { webSearchAction } from "./flow-activities/web-search-action.js"; import { writeGoogleSheetAction } from "./flow-activities/write-google-sheet-action.js"; -export const createFlowActionActivities = ({ - vaultClient, -}: { - vaultClient: VaultClient; -}): Record<`${ActionDefinitionId}Action`, FlowActionActivity> => ({ +export const createFlowActionActivities: CreateFlowActivities< + AiFlowActionDefinitionId +> = ({ vaultClient }: { vaultClient: VaultClient }) => ({ generateWebQueriesAction, webSearchAction, getWebPageByUrlAction, @@ -50,6 +46,4 @@ export const createFlowActivities = ({ }) => ({ ...createFlowActionActivities({ vaultClient }), generateFlowRunName, - persistFlowActivity, - userHasPermissionToRunFlowInWebActivity, }); diff --git a/apps/hash-ai-worker-ts/src/activities/flow-activities/answer-question-action.ts b/apps/hash-ai-worker-ts/src/activities/flow-activities/answer-question-action.ts index df0128a4089..718806236f0 100644 --- a/apps/hash-ai-worker-ts/src/activities/flow-activities/answer-question-action.ts +++ b/apps/hash-ai-worker-ts/src/activities/flow-activities/answer-question-action.ts @@ -1,7 +1,8 @@ import { extractEntityUuidFromEntityId } from "@blockprotocol/type-system"; +import type { FlowActionActivity } from "@local/hash-backend-utils/flows"; import { getSimpleGraph } from "@local/hash-backend-utils/simplified-graph"; import { queryEntitySubgraph } from "@local/hash-graph-sdk/entity"; -import { getSimplifiedActionInputs } from "@local/hash-isomorphic-utils/flows/action-definitions"; +import { getSimplifiedAiFlowActionInputs } from "@local/hash-isomorphic-utils/flows/action-definitions"; import type { FormattedText, StepOutput, @@ -32,7 +33,6 @@ import { mapActionInputEntitiesToEntities } from "../shared/map-action-input-ent import { openAiSeed } from "../shared/open-ai-seed.js"; import type { PermittedOpenAiModel } from "../shared/openai-client.js"; import { stringify } from "../shared/stringify.js"; -import type { FlowActionActivity } from "./types.js"; const answerTools: LlmToolDefinition[] = [ { @@ -403,7 +403,7 @@ export const answerQuestionAction: FlowActionActivity = async ({ inputs }) => { context, entities: inputEntities, question, - } = getSimplifiedActionInputs({ + } = getSimplifiedAiFlowActionInputs({ inputs, actionType: "answerQuestion", }); 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 243376e6c3b..6fad03a4f63 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 @@ -9,6 +9,7 @@ import type { import type { GoalFlowTriggerInput } from "@local/hash-isomorphic-utils/flows/goal-flow-definitions"; import { goalFlowDefinitionIds } from "@local/hash-isomorphic-utils/flows/goal-flow-definitions"; import type { + FlowActionDefinitionId, FlowDefinition, FlowTrigger, PayloadKind, @@ -21,8 +22,8 @@ import { getLlmResponse } from "../shared/get-llm-response.js"; import { getTextContentFromLlmMessage } from "../shared/get-llm-response/llm-message.js"; import { graphApiClient } from "../shared/graph-api-client.js"; -type PersistFlowActivityParams = { - flowDefinition: FlowDefinition; +type GenerateFlowRunNameActivityParams = { + flowDefinition: FlowDefinition; flowTrigger: FlowTrigger; }; @@ -90,7 +91,7 @@ const outputKindsToIgnore: PayloadKind[] = [ ]; export const generateFlowRunName = async ( - params: PersistFlowActivityParams, + params: GenerateFlowRunNameActivityParams, ) => { const { flowDefinition, flowTrigger } = params; diff --git a/apps/hash-ai-worker-ts/src/activities/flow-activities/generate-web-queries-action.ts b/apps/hash-ai-worker-ts/src/activities/flow-activities/generate-web-queries-action.ts index 29042c49d2f..a99515add54 100644 --- a/apps/hash-ai-worker-ts/src/activities/flow-activities/generate-web-queries-action.ts +++ b/apps/hash-ai-worker-ts/src/activities/flow-activities/generate-web-queries-action.ts @@ -1,5 +1,6 @@ +import type { FlowActionActivity } from "@local/hash-backend-utils/flows"; import { isInferenceModelName } from "@local/hash-isomorphic-utils/ai-inference-types"; -import { getSimplifiedActionInputs } from "@local/hash-isomorphic-utils/flows/action-definitions"; +import { getSimplifiedAiFlowActionInputs } from "@local/hash-isomorphic-utils/flows/action-definitions"; import { StatusCode } from "@local/status"; import dedent from "dedent"; @@ -9,7 +10,6 @@ import { getToolCallsFromLlmAssistantMessage } from "../shared/get-llm-response/ import type { LlmToolDefinition } from "../shared/get-llm-response/types.js"; import { graphApiClient } from "../shared/graph-api-client.js"; import { inferenceModelAliasToSpecificModel } from "../shared/inference-model-alias-to-llm-model.js"; -import type { FlowActionActivity } from "./types.js"; const webQueriesSystemPrompt = dedent(` You are a Web Search Assistant. @@ -43,7 +43,7 @@ type ProposeQueryFunctionCallArguments = { export const generateWebQueriesAction: FlowActionActivity = async ({ inputs, }) => { - const { prompt, model } = getSimplifiedActionInputs({ + const { prompt, model } = getSimplifiedAiFlowActionInputs({ inputs, actionType: "generateWebQueries", }); diff --git a/apps/hash-ai-worker-ts/src/activities/flow-activities/get-file-from-url-action.ts b/apps/hash-ai-worker-ts/src/activities/flow-activities/get-file-from-url-action.ts index 3c69fd417d6..08b4215f0fb 100644 --- a/apps/hash-ai-worker-ts/src/activities/flow-activities/get-file-from-url-action.ts +++ b/apps/hash-ai-worker-ts/src/activities/flow-activities/get-file-from-url-action.ts @@ -1,20 +1,20 @@ +import type { FlowActionActivity } from "@local/hash-backend-utils/flows"; import { - getSimplifiedActionInputs, - type OutputNameForAction, + getSimplifiedAiFlowActionInputs, + type OutputNameForAiFlowAction, } from "@local/hash-isomorphic-utils/flows/action-definitions"; import { StatusCode } from "@local/status"; import { Context } from "@temporalio/activity"; import { logProgress } from "../shared/log-progress.js"; import { createFileEntityFromUrl } from "./shared/create-file-entity-from-url.js"; -import type { FlowActionActivity } from "./types.js"; export const getFileFromUrlAction: FlowActionActivity = async ({ inputs }) => { const { description, displayName, url: originalUrl, - } = getSimplifiedActionInputs({ + } = getSimplifiedAiFlowActionInputs({ inputs, actionType: "getFileFromUrl", }); @@ -58,7 +58,7 @@ export const getFileFromUrlAction: FlowActionActivity = async ({ inputs }) => { outputs: [ { outputName: - "fileEntity" satisfies OutputNameForAction<"getFileFromUrl">, + "fileEntity" satisfies OutputNameForAiFlowAction<"getFileFromUrl">, payload: { kind: "Entity", value: fileEntity, diff --git a/apps/hash-ai-worker-ts/src/activities/flow-activities/get-web-page-by-url-action.ts b/apps/hash-ai-worker-ts/src/activities/flow-activities/get-web-page-by-url-action.ts index 51a8d002b5e..4c46d401578 100644 --- a/apps/hash-ai-worker-ts/src/activities/flow-activities/get-web-page-by-url-action.ts +++ b/apps/hash-ai-worker-ts/src/activities/flow-activities/get-web-page-by-url-action.ts @@ -1,15 +1,15 @@ import type { Url } from "@blockprotocol/type-system"; +import type { FlowActionActivity } from "@local/hash-backend-utils/flows"; import { - getSimplifiedActionInputs, - type OutputNameForAction, + getSimplifiedAiFlowActionInputs, + type OutputNameForAiFlowAction, } from "@local/hash-isomorphic-utils/flows/action-definitions"; import { StatusCode } from "@local/status"; import { getWebPageActivity } from "../get-web-page-activity.js"; -import type { FlowActionActivity } from "./types.js"; export const getWebPageByUrlAction: FlowActionActivity = async ({ inputs }) => { - const { url } = getSimplifiedActionInputs({ + const { url } = getSimplifiedAiFlowActionInputs({ inputs, actionType: "getWebPageByUrl", }); @@ -35,7 +35,7 @@ export const getWebPageByUrlAction: FlowActionActivity = async ({ inputs }) => { outputs: [ { outputName: - "webPage" satisfies OutputNameForAction<"getWebPageByUrl">, + "webPage" satisfies OutputNameForAiFlowAction<"getWebPageByUrl">, payload: { kind: "WebPage", value: webPage, diff --git a/apps/hash-ai-worker-ts/src/activities/flow-activities/get-web-page-summary-action.ai.test.ts b/apps/hash-ai-worker-ts/src/activities/flow-activities/get-web-page-summary-action.ai.test.ts index cc4a5d51283..c6ee9547372 100644 --- a/apps/hash-ai-worker-ts/src/activities/flow-activities/get-web-page-summary-action.ai.test.ts +++ b/apps/hash-ai-worker-ts/src/activities/flow-activities/get-web-page-summary-action.ai.test.ts @@ -1,6 +1,6 @@ import "../../shared/testing-utilities/mock-get-flow-context.js"; -import type { InputNameForAction } from "@local/hash-isomorphic-utils/flows/action-definitions"; +import type { InputNameForAiFlowAction } from "@local/hash-isomorphic-utils/flows/action-definitions"; import { actionDefinitions } from "@local/hash-isomorphic-utils/flows/action-definitions"; import type { StepInput } from "@local/hash-isomorphic-utils/flows/types"; import { expect, test } from "vitest"; @@ -15,7 +15,8 @@ test( const status = await getWebPageSummaryAction({ inputs: [ { - inputName: "url" satisfies InputNameForAction<"getWebPageSummary">, + inputName: + "url" satisfies InputNameForAiFlowAction<"getWebPageSummary">, payload: { kind: "Text", value: url }, }, ...actionDefinitions.getWebPageSummary.inputs.flatMap( diff --git a/apps/hash-ai-worker-ts/src/activities/flow-activities/get-web-page-summary-action.ts b/apps/hash-ai-worker-ts/src/activities/flow-activities/get-web-page-summary-action.ts index 5c1d72e5b1f..91befe58d21 100644 --- a/apps/hash-ai-worker-ts/src/activities/flow-activities/get-web-page-summary-action.ts +++ b/apps/hash-ai-worker-ts/src/activities/flow-activities/get-web-page-summary-action.ts @@ -1,8 +1,9 @@ import type { Url } from "@blockprotocol/type-system"; +import type { FlowActionActivity } from "@local/hash-backend-utils/flows"; import { isInferenceModelName } from "@local/hash-isomorphic-utils/ai-inference-types"; import { - getSimplifiedActionInputs, - type OutputNameForAction, + getSimplifiedAiFlowActionInputs, + type OutputNameForAiFlowAction, } from "@local/hash-isomorphic-utils/flows/action-definitions"; import { StatusCode } from "@local/status"; import dedent from "dedent"; @@ -13,7 +14,6 @@ import { getLlmResponse } from "../shared/get-llm-response.js"; import { getTextContentFromLlmMessage } from "../shared/get-llm-response/llm-message.js"; import { graphApiClient } from "../shared/graph-api-client.js"; import { inferenceModelAliasToSpecificModel } from "../shared/inference-model-alias-to-llm-model.js"; -import type { FlowActionActivity } from "./types.js"; const generateSummarizeWebPageSystemPrompt = (params: { numberOfSentences: number; @@ -34,7 +34,7 @@ const generateSummarizeWebPageSystemPrompt = (params: { export const getWebPageSummaryAction: FlowActionActivity = async ({ inputs, }) => { - const { url, model, numberOfSentences } = getSimplifiedActionInputs({ + const { url, model, numberOfSentences } = getSimplifiedAiFlowActionInputs({ inputs, actionType: "getWebPageSummary", }); @@ -124,7 +124,7 @@ export const getWebPageSummaryAction: FlowActionActivity = async ({ outputs: [ { outputName: - "summary" satisfies OutputNameForAction<"getWebPageSummary">, + "summary" satisfies OutputNameForAiFlowAction<"getWebPageSummary">, payload: { kind: "Text", value: summary, @@ -132,7 +132,7 @@ export const getWebPageSummaryAction: FlowActionActivity = async ({ }, { outputName: - "title" satisfies OutputNameForAction<"getWebPageSummary">, + "title" satisfies OutputNameForAiFlowAction<"getWebPageSummary">, payload: { kind: "Text", value: webPage.title, diff --git a/apps/hash-ai-worker-ts/src/activities/flow-activities/infer-entities-from-content-action.ts b/apps/hash-ai-worker-ts/src/activities/flow-activities/infer-entities-from-content-action.ts index 12727ea3c6f..84dcb6fd2ff 100644 --- a/apps/hash-ai-worker-ts/src/activities/flow-activities/infer-entities-from-content-action.ts +++ b/apps/hash-ai-worker-ts/src/activities/flow-activities/infer-entities-from-content-action.ts @@ -11,10 +11,11 @@ import { entityIdFromComponents, } from "@blockprotocol/type-system"; import { typedKeys } from "@local/advanced-types/typed-entries"; +import type { FlowActionActivity } from "@local/hash-backend-utils/flows"; import { isInferenceModelName } from "@local/hash-isomorphic-utils/ai-inference-types"; import { - getSimplifiedActionInputs, - type OutputNameForAction, + getSimplifiedAiFlowActionInputs, + type OutputNameForAiFlowAction, } from "@local/hash-isomorphic-utils/flows/action-definitions"; import type { ProposedEntity } from "@local/hash-isomorphic-utils/flows/types"; import { generateUuid } from "@local/hash-isomorphic-utils/generate-uuid"; @@ -31,7 +32,6 @@ import { getFlowContext } from "../shared/get-flow-context.js"; import { graphApiClient } from "../shared/graph-api-client.js"; import { inferenceModelAliasToSpecificModel } from "../shared/inference-model-alias-to-llm-model.js"; import { isPermittedOpenAiModel } from "../shared/openai-client.js"; -import type { FlowActionActivity } from "./types.js"; export const inferEntitiesFromContentAction: FlowActionActivity = async ({ inputs, @@ -41,7 +41,7 @@ export const inferEntitiesFromContentAction: FlowActionActivity = async ({ entityTypeIds, model: modelAlias, relevantEntitiesPrompt, - } = getSimplifiedActionInputs({ + } = getSimplifiedAiFlowActionInputs({ inputs, actionType: "inferEntitiesFromContent", }); @@ -218,7 +218,7 @@ export const inferEntitiesFromContentAction: FlowActionActivity = async ({ outputs: [ { outputName: - "proposedEntities" satisfies OutputNameForAction<"inferEntitiesFromContent">, + "proposedEntities" satisfies OutputNameForAiFlowAction<"inferEntitiesFromContent">, payload: { kind: "ProposedEntity", value: proposedEntities, diff --git a/apps/hash-ai-worker-ts/src/activities/flow-activities/infer-metadata-from-document-action.ts b/apps/hash-ai-worker-ts/src/activities/flow-activities/infer-metadata-from-document-action.ts index d6da22eb857..deda5110041 100644 --- a/apps/hash-ai-worker-ts/src/activities/flow-activities/infer-metadata-from-document-action.ts +++ b/apps/hash-ai-worker-ts/src/activities/flow-activities/infer-metadata-from-document-action.ts @@ -6,10 +6,11 @@ import type { Url, } from "@blockprotocol/type-system"; import { extractEntityUuidFromEntityId } from "@blockprotocol/type-system"; +import type { FlowActionActivity } from "@local/hash-backend-utils/flows"; import type { HashEntity } from "@local/hash-graph-sdk/entity"; import { - getSimplifiedActionInputs, - type OutputNameForAction, + getSimplifiedAiFlowActionInputs, + type OutputNameForAiFlowAction, } from "@local/hash-isomorphic-utils/flows/action-definitions"; import type { PersistedEntity } from "@local/hash-isomorphic-utils/flows/types"; import { @@ -32,7 +33,6 @@ import { useFileSystemPathFromEntity } from "../shared/use-file-system-file-from import { generateDocumentPropertyPatches } from "./infer-metadata-from-document-action/generate-property-patches.js"; import { generateDocumentProposedEntitiesAndCreateClaims } from "./infer-metadata-from-document-action/generate-proposed-entities-and-claims.js"; import { getLlmAnalysisOfDoc } from "./infer-metadata-from-document-action/get-llm-analysis-of-doc.js"; -import type { FlowActionActivity } from "./types.js"; const isFileEntity = (entity: HashEntity): entity is HashEntity => systemPropertyTypes.fileStorageKey.propertyTypeBaseUrl in entity.properties && @@ -48,7 +48,7 @@ export const inferMetadataFromDocumentAction: FlowActionActivity = async ({ webId, } = await getFlowContext(); - const { documentEntityId } = getSimplifiedActionInputs({ + const { documentEntityId } = getSimplifiedAiFlowActionInputs({ inputs, actionType: "inferMetadataFromDocument", }); @@ -247,7 +247,7 @@ export const inferMetadataFromDocumentAction: FlowActionActivity = async ({ outputs: [ { outputName: - "proposedEntities" satisfies OutputNameForAction<"inferMetadataFromDocument">, + "proposedEntities" satisfies OutputNameForAiFlowAction<"inferMetadataFromDocument">, payload: { kind: "ProposedEntity", value: proposedEntities, @@ -255,7 +255,7 @@ export const inferMetadataFromDocumentAction: FlowActionActivity = async ({ }, { outputName: - "updatedDocumentEntity" satisfies OutputNameForAction<"inferMetadataFromDocument">, + "updatedDocumentEntity" satisfies OutputNameForAiFlowAction<"inferMetadataFromDocument">, payload: { kind: "PersistedEntity", value: persistedDocumentEntity, diff --git a/apps/hash-ai-worker-ts/src/activities/flow-activities/persist-entities-action.ts b/apps/hash-ai-worker-ts/src/activities/flow-activities/persist-entities-action.ts index ccb4740e493..e38fb2a5a23 100644 --- a/apps/hash-ai-worker-ts/src/activities/flow-activities/persist-entities-action.ts +++ b/apps/hash-ai-worker-ts/src/activities/flow-activities/persist-entities-action.ts @@ -1,9 +1,10 @@ import type { EntityId } from "@blockprotocol/type-system"; +import type { FlowActionActivity } from "@local/hash-backend-utils/flows"; import { flattenPropertyMetadata, HashEntity, } from "@local/hash-graph-sdk/entity"; -import { getSimplifiedActionInputs } from "@local/hash-isomorphic-utils/flows/action-definitions"; +import { getSimplifiedAiFlowActionInputs } from "@local/hash-isomorphic-utils/flows/action-definitions"; import type { FailedEntityProposal, PersistedEntities, @@ -16,10 +17,9 @@ import { fileEntityTypeIds, persistEntityAction, } from "./persist-entity-action.js"; -import type { FlowActionActivity } from "./types.js"; export const persistEntitiesAction: FlowActionActivity = async ({ inputs }) => { - const { draft, proposedEntities } = getSimplifiedActionInputs({ + const { draft, proposedEntities } = getSimplifiedAiFlowActionInputs({ inputs, actionType: "persistEntities", }); diff --git a/apps/hash-ai-worker-ts/src/activities/flow-activities/persist-entity-action.ts b/apps/hash-ai-worker-ts/src/activities/flow-activities/persist-entity-action.ts index d27b45fab85..b1ca09d0dfd 100644 --- a/apps/hash-ai-worker-ts/src/activities/flow-activities/persist-entity-action.ts +++ b/apps/hash-ai-worker-ts/src/activities/flow-activities/persist-entity-action.ts @@ -1,5 +1,6 @@ import type { EntityId, VersionedUrl } from "@blockprotocol/type-system"; import { extractEntityUuidFromEntityId } from "@blockprotocol/type-system"; +import type { FlowActionActivity } from "@local/hash-backend-utils/flows"; import { getWebMachineId } from "@local/hash-backend-utils/machine-actors"; import type { CreateEntityParameters } from "@local/hash-graph-sdk/entity"; import { @@ -8,8 +9,8 @@ import { mergePropertyObjectAndMetadata, } from "@local/hash-graph-sdk/entity"; import { - getSimplifiedActionInputs, - type OutputNameForAction, + getSimplifiedAiFlowActionInputs, + type OutputNameForAiFlowAction, } from "@local/hash-isomorphic-utils/flows/action-definitions"; import type { PersistedEntity } from "@local/hash-isomorphic-utils/flows/types"; import { systemEntityTypes } from "@local/hash-isomorphic-utils/ontology-type-ids"; @@ -38,7 +39,6 @@ import { getEntityUpdate, getLatestEntityById, } from "./shared/graph-requests.js"; -import type { FlowActionActivity } from "./types.js"; export const fileEntityTypeIds: VersionedUrl[] = [ systemEntityTypes.file.entityTypeId, @@ -58,10 +58,11 @@ export const persistEntityAction: FlowActionActivity = async ({ inputs }) => { webId, } = await getFlowContext(); - const { draft, proposedEntityWithResolvedLinks } = getSimplifiedActionInputs({ - inputs, - actionType: "persistEntity", - }); + const { draft, proposedEntityWithResolvedLinks } = + getSimplifiedAiFlowActionInputs({ + inputs, + actionType: "persistEntity", + }); const createEditionAsDraft = draft ?? false; @@ -327,7 +328,7 @@ export const persistEntityAction: FlowActionActivity = async ({ inputs }) => { outputs: [ { outputName: - "persistedEntity" as OutputNameForAction<"persistEntity">, + "persistedEntity" as OutputNameForAiFlowAction<"persistEntity">, payload: { kind: "PersistedEntity", value: persistedEntity, diff --git a/apps/hash-ai-worker-ts/src/activities/flow-activities/process-automatic-browsing-settings-action.ts b/apps/hash-ai-worker-ts/src/activities/flow-activities/process-automatic-browsing-settings-action.ts index 4b67c5753b4..becac787bd3 100644 --- a/apps/hash-ai-worker-ts/src/activities/flow-activities/process-automatic-browsing-settings-action.ts +++ b/apps/hash-ai-worker-ts/src/activities/flow-activities/process-automatic-browsing-settings-action.ts @@ -1,7 +1,8 @@ +import type { FlowActionActivity } from "@local/hash-backend-utils/flows"; import type { HashEntity } from "@local/hash-graph-sdk/entity"; import { - getSimplifiedActionInputs, - type OutputNameForAction, + getSimplifiedAiFlowActionInputs, + type OutputNameForAiFlowAction, } from "@local/hash-isomorphic-utils/flows/action-definitions"; import type { AutomaticInferenceSettings } from "@local/hash-isomorphic-utils/flows/browser-plugin-flow-types"; import { generateVersionedUrlMatchingFilter } from "@local/hash-isomorphic-utils/graph-queries"; @@ -12,11 +13,10 @@ import { StatusCode } from "@local/status"; import { getEntityByFilter } from "../shared/get-entity-by-filter.js"; import { getFlowContext } from "../shared/get-flow-context.js"; import { graphApiClient } from "../shared/graph-api-client.js"; -import type { FlowActionActivity } from "./types.js"; export const processAutomaticBrowsingSettingsAction: FlowActionActivity = async ({ inputs }) => { - const { webPage } = getSimplifiedActionInputs({ + const { webPage } = getSimplifiedAiFlowActionInputs({ inputs, actionType: "processAutomaticBrowsingSettings", }); @@ -108,7 +108,7 @@ export const processAutomaticBrowsingSettingsAction: FlowActionActivity = outputs: [ { outputName: - "draft" satisfies OutputNameForAction<"processAutomaticBrowsingSettings">, + "draft" satisfies OutputNameForAiFlowAction<"processAutomaticBrowsingSettings">, payload: { kind: "Boolean", value: createAs === "draft", @@ -116,7 +116,7 @@ export const processAutomaticBrowsingSettingsAction: FlowActionActivity = }, { outputName: - "entityTypeIds" satisfies OutputNameForAction<"processAutomaticBrowsingSettings">, + "entityTypeIds" satisfies OutputNameForAiFlowAction<"processAutomaticBrowsingSettings">, payload: { kind: "VersionedUrl", value: entityTypeIdsToInfer, @@ -124,7 +124,7 @@ export const processAutomaticBrowsingSettingsAction: FlowActionActivity = }, { outputName: - "model" satisfies OutputNameForAction<"processAutomaticBrowsingSettings">, + "model" satisfies OutputNameForAiFlowAction<"processAutomaticBrowsingSettings">, payload: { kind: "Text", value: model, diff --git a/apps/hash-ai-worker-ts/src/activities/flow-activities/research-entities-action.ts b/apps/hash-ai-worker-ts/src/activities/flow-activities/research-entities-action.ts index 82a6c7234f4..7d4d1807130 100644 --- a/apps/hash-ai-worker-ts/src/activities/flow-activities/research-entities-action.ts +++ b/apps/hash-ai-worker-ts/src/activities/flow-activities/research-entities-action.ts @@ -2,6 +2,7 @@ import type { OriginProvenance, ProvidedEntityEditionProvenance, } from "@blockprotocol/type-system"; +import type { FlowActionActivity } from "@local/hash-backend-utils/flows"; import { queryEntities } from "@local/hash-graph-sdk/entity"; import { generateUuid } from "@local/hash-isomorphic-utils/generate-uuid"; import { @@ -18,7 +19,6 @@ import { } from "./research-entities-action/checkpoints.js"; import { runCoordinatingAgent } from "./research-entities-action/coordinating-agent.js"; import type { CoordinatingAgentState } from "./research-entities-action/shared/coordinators.js"; -import type { FlowActionActivity } from "./types.js"; /** * An action to research entities of requested types according to the user's research goal. diff --git a/apps/hash-ai-worker-ts/src/activities/flow-activities/research-entities-action/coordinating-agent.ts b/apps/hash-ai-worker-ts/src/activities/flow-activities/research-entities-action/coordinating-agent.ts index 304bf1e9ff7..b40fd5f4a13 100644 --- a/apps/hash-ai-worker-ts/src/activities/flow-activities/research-entities-action/coordinating-agent.ts +++ b/apps/hash-ai-worker-ts/src/activities/flow-activities/research-entities-action/coordinating-agent.ts @@ -4,10 +4,11 @@ import type { Url, } from "@blockprotocol/type-system"; import { entityIdFromComponents } from "@blockprotocol/type-system"; +import type { FlowActionActivity } from "@local/hash-backend-utils/flows"; import { flattenPropertyMetadata } from "@local/hash-graph-sdk/entity"; import { - getSimplifiedActionInputs, - type OutputNameForAction, + getSimplifiedAiFlowActionInputs, + type OutputNameForAiFlowAction, } from "@local/hash-isomorphic-utils/flows/action-definitions"; import type { ProposedEntity, @@ -31,7 +32,6 @@ import { graphApiClient } from "../../shared/graph-api-client.js"; import { logProgress } from "../../shared/log-progress.js"; import { mapActionInputEntitiesToEntities } from "../../shared/map-action-input-entities-to-entities.js"; import { stringify } from "../../shared/stringify.js"; -import type { FlowActionActivity } from "../types.js"; import { createCheckpoint } from "./checkpoints.js"; import { createInitialPlan } from "./coordinating-agent/create-initial-plan.js"; import { processCompleteToolCall } from "./coordinating-agent/process-complete-tool-call.js"; @@ -73,7 +73,7 @@ const parseAndResolveCoordinatorInputs = async (params: { entityTypeIds, existingEntities: inputExistingEntities, reportSpecification, - } = getSimplifiedActionInputs({ + } = getSimplifiedAiFlowActionInputs({ inputs: stepInputs, actionType: "researchEntities", }); @@ -586,7 +586,7 @@ export const runCoordinatingAgent: FlowActionActivity<{ outputs: [ { outputName: - "proposedEntities" satisfies OutputNameForAction<"researchEntities">, + "proposedEntities" satisfies OutputNameForAiFlowAction<"researchEntities">, payload: { kind: "ProposedEntity", value: [...allProposedEntities, ...fileEntityProposals], @@ -594,7 +594,7 @@ export const runCoordinatingAgent: FlowActionActivity<{ }, { outputName: - "highlightedEntities" satisfies OutputNameForAction<"researchEntities">, + "highlightedEntities" satisfies OutputNameForAiFlowAction<"researchEntities">, payload: { kind: "EntityId", value: submittedEntities.map( diff --git a/apps/hash-ai-worker-ts/src/activities/flow-activities/research-entities-action/shared/handle-web-search-tool-call.ts b/apps/hash-ai-worker-ts/src/activities/flow-activities/research-entities-action/shared/handle-web-search-tool-call.ts index 429373c7dc9..973262d7643 100644 --- a/apps/hash-ai-worker-ts/src/activities/flow-activities/research-entities-action/shared/handle-web-search-tool-call.ts +++ b/apps/hash-ai-worker-ts/src/activities/flow-activities/research-entities-action/shared/handle-web-search-tool-call.ts @@ -1,7 +1,7 @@ import type { Url } from "@blockprotocol/type-system"; import type { - InputNameForAction, - OutputNameForAction, + InputNameForAiFlowAction, + OutputNameForAiFlowAction, } from "@local/hash-isomorphic-utils/flows/action-definitions"; import { actionDefinitions } from "@local/hash-isomorphic-utils/flows/action-definitions"; import type { @@ -35,12 +35,12 @@ export const handleWebSearchToolCall = async (params: { const response = await webSearchAction({ inputs: [ { - inputName: "query" satisfies InputNameForAction<"webSearch">, + inputName: "query" satisfies InputNameForAiFlowAction<"webSearch">, payload: { kind: "Text", value: query }, }, { inputName: - "numberOfSearchResults" satisfies InputNameForAction<"webSearch">, + "numberOfSearchResults" satisfies InputNameForAiFlowAction<"webSearch">, payload: { kind: "Number", value: 5 }, }, ], @@ -68,7 +68,7 @@ export const handleWebSearchToolCall = async (params: { const webPageUrlsOutput = webSearchOutputs.find( ({ outputName }) => outputName === - ("webSearchResult" satisfies OutputNameForAction<"webSearch">), + ("webSearchResult" satisfies OutputNameForAiFlowAction<"webSearch">), ); if (!webPageUrlsOutput) { @@ -88,7 +88,7 @@ export const handleWebSearchToolCall = async (params: { inputs: [ { inputName: - "url" satisfies InputNameForAction<"getWebPageSummary">, + "url" satisfies InputNameForAiFlowAction<"getWebPageSummary">, payload: { kind: "Text", value: url }, }, ...actionDefinitions.getWebPageSummary.inputs.flatMap( @@ -113,7 +113,7 @@ export const handleWebSearchToolCall = async (params: { const summaryOutput = webPageSummaryOutputs?.find( ({ outputName }) => outputName === - ("summary" satisfies OutputNameForAction<"getWebPageSummary">), + ("summary" satisfies OutputNameForAiFlowAction<"getWebPageSummary">), ); if (!summaryOutput) { diff --git a/apps/hash-ai-worker-ts/src/activities/flow-activities/types.ts b/apps/hash-ai-worker-ts/src/activities/flow-activities/types.ts deleted file mode 100644 index 31cb8514673..00000000000 --- a/apps/hash-ai-worker-ts/src/activities/flow-activities/types.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { - StepInput, - StepOutput, -} from "@local/hash-isomorphic-utils/flows/types"; -import type { Status } from "@local/status"; - -export type FlowActionActivity = ( - params: { - inputs: StepInput[]; - } & AdditionalParams, -) => Promise< - Status<{ - outputs: StepOutput[]; - }> ->; diff --git a/apps/hash-ai-worker-ts/src/activities/flow-activities/web-search-action.ts b/apps/hash-ai-worker-ts/src/activities/flow-activities/web-search-action.ts index 25fd31614aa..863821a5e1d 100644 --- a/apps/hash-ai-worker-ts/src/activities/flow-activities/web-search-action.ts +++ b/apps/hash-ai-worker-ts/src/activities/flow-activities/web-search-action.ts @@ -1,8 +1,9 @@ import type { Url } from "@blockprotocol/type-system"; +import type { FlowActionActivity } from "@local/hash-backend-utils/flows"; import { internalApiClient } from "@local/hash-backend-utils/internal-api-client"; import { - getSimplifiedActionInputs, - type OutputNameForAction, + getSimplifiedAiFlowActionInputs, + type OutputNameForAiFlowAction, } from "@local/hash-isomorphic-utils/flows/action-definitions"; import type { StepOutput } from "@local/hash-isomorphic-utils/flows/types"; import type { GetWebSearchResults200ResponseWebSearchResultsInner } from "@local/internal-api-client"; @@ -10,8 +11,6 @@ import type { Status } from "@local/status"; import { StatusCode } from "@local/status"; import { backOff } from "exponential-backoff"; -import type { FlowActionActivity } from "./types.js"; - export type GetWebSearchResultsResponse = Omit< GetWebSearchResults200ResponseWebSearchResultsInner, "url" @@ -27,7 +26,7 @@ const mapWebSearchResults = ( ); export const webSearchAction: FlowActionActivity = async ({ inputs }) => { - const { query, numberOfSearchResults } = getSimplifiedActionInputs({ + const { query, numberOfSearchResults } = getSimplifiedAiFlowActionInputs({ inputs, actionType: "webSearch", }); @@ -49,7 +48,7 @@ export const webSearchAction: FlowActionActivity = async ({ inputs }) => { outputs: [ { outputName: - "webSearchResult" satisfies OutputNameForAction<"webSearch">, + "webSearchResult" satisfies OutputNameForAiFlowAction<"webSearch">, payload: { kind: "WebSearchResult", value: mapWebSearchResults(webPages), diff --git a/apps/hash-ai-worker-ts/src/activities/flow-activities/write-google-sheet-action.ts b/apps/hash-ai-worker-ts/src/activities/flow-activities/write-google-sheet-action.ts index bf683715146..9bd43522d1a 100644 --- a/apps/hash-ai-worker-ts/src/activities/flow-activities/write-google-sheet-action.ts +++ b/apps/hash-ai-worker-ts/src/activities/flow-activities/write-google-sheet-action.ts @@ -2,6 +2,7 @@ import type { OriginProvenance, ProvidedEntityEditionProvenance, } from "@blockprotocol/type-system"; +import type { FlowActionActivity } from "@local/hash-backend-utils/flows"; import { createGoogleOAuth2Client, getGoogleAccountById, @@ -10,7 +11,7 @@ import { import { getWebMachineId } from "@local/hash-backend-utils/machine-actors"; import type { VaultClient } from "@local/hash-backend-utils/vault"; import { HashEntity } from "@local/hash-graph-sdk/entity"; -import { getSimplifiedActionInputs } from "@local/hash-isomorphic-utils/flows/action-definitions"; +import { getSimplifiedAiFlowActionInputs } from "@local/hash-isomorphic-utils/flows/action-definitions"; import { generateEntityIdFilter } from "@local/hash-isomorphic-utils/graph-queries"; import { googleEntityTypes, @@ -31,7 +32,6 @@ import { getEntityByFilter } from "../shared/get-entity-by-filter.js"; import { getFlowContext } from "../shared/get-flow-context.js"; import { graphApiClient } from "../shared/graph-api-client.js"; import { getEntityUpdate } from "./shared/graph-requests.js"; -import type { FlowActionActivity } from "./types.js"; import { convertCsvToSheetRequests } from "./write-google-sheet-action/convert-csv-to-sheet-requests.js"; import { convertSubgraphToSheetRequests } from "./write-google-sheet-action/convert-subgraph-to-sheet-requests.js"; import { getFilterFromBlockProtocolQueryEntity } from "./write-google-sheet-action/get-filter-from-bp-query-entity.js"; @@ -78,7 +78,7 @@ export const writeGoogleSheetAction: FlowActionActivity<{ await getFlowContext(); const { audience, dataToWrite, googleAccountId, googleSheet } = - getSimplifiedActionInputs({ + getSimplifiedAiFlowActionInputs({ inputs, actionType: "writeGoogleSheet", }); diff --git a/apps/hash-ai-worker-ts/src/activities/infer-entities/inference-types.ts b/apps/hash-ai-worker-ts/src/activities/infer-entities/inference-types.ts index 47f2b741131..9e02e99f9bb 100644 --- a/apps/hash-ai-worker-ts/src/activities/infer-entities/inference-types.ts +++ b/apps/hash-ai-worker-ts/src/activities/infer-entities/inference-types.ts @@ -1,8 +1,8 @@ import type { VersionedUrl } from "@blockprotocol/type-system"; import type { SerializedEntity } from "@local/hash-graph-sdk/entity"; import type { + DeprecatedProposedEntity, InferredEntityChangeResult, - ProposedEntity, } from "@local/hash-isomorphic-utils/ai-inference-types"; import type OpenAI from "openai"; @@ -34,7 +34,7 @@ export type ProposedEntitySummary = { export type UpdateCandidate = { entity: SerializedEntity; - proposedEntity: ProposedEntity; + proposedEntity: DeprecatedProposedEntity; status: "update-candidate"; }; @@ -46,7 +46,10 @@ export type InferenceState = { /** A list of entities that can be inferred from the input, in summary form (no properties) */ proposedEntitySummaries: ProposedEntitySummary[]; /** A map of entity type IDs to a set of proposed entities, in entity form (with properties) */ - proposedEntityCreationsByType: Record; + proposedEntityCreationsByType: Record< + VersionedUrl, + DeprecatedProposedEntity[] + >; /** The results of attempting to persist entities inferred from the input */ resultsByTemporaryId: Record< number, diff --git a/apps/hash-ai-worker-ts/src/activities/infer-entities/propose-entities.ts b/apps/hash-ai-worker-ts/src/activities/infer-entities/propose-entities.ts index acf60f33b0b..6684bf1b137 100644 --- a/apps/hash-ai-worker-ts/src/activities/infer-entities/propose-entities.ts +++ b/apps/hash-ai-worker-ts/src/activities/infer-entities/propose-entities.ts @@ -2,7 +2,7 @@ import type { EntityUuid, VersionedUrl } from "@blockprotocol/type-system"; import { entityIdFromComponents } from "@blockprotocol/type-system"; import { typedEntries } from "@local/advanced-types/typed-entries"; import { mergePropertyObjectAndMetadata } from "@local/hash-graph-sdk/entity"; -import type { ProposedEntity } from "@local/hash-isomorphic-utils/ai-inference-types"; +import type { DeprecatedProposedEntity } from "@local/hash-isomorphic-utils/ai-inference-types"; import type { Status } from "@local/status"; import { StatusCode } from "@local/status"; import { Context } from "@temporalio/activity"; @@ -480,7 +480,7 @@ export const proposeEntities = async (params: { const validProposedEntitiesByType = Object.fromEntries( typedEntries(proposedEntitiesByType).map< - [VersionedUrl, ProposedEntity[]] + [VersionedUrl, DeprecatedProposedEntity[]] >(([simplifiedEntityTypeId, entities]) => { const entityTypeId = simplifiedEntityTypeIdMappings[simplifiedEntityTypeId]; diff --git a/apps/hash-ai-worker-ts/src/activities/infer-entities/shared/generate-propose-entities-tools.ts b/apps/hash-ai-worker-ts/src/activities/infer-entities/shared/generate-propose-entities-tools.ts index 4d00a5500ab..ac220fdd735 100644 --- a/apps/hash-ai-worker-ts/src/activities/infer-entities/shared/generate-propose-entities-tools.ts +++ b/apps/hash-ai-worker-ts/src/activities/infer-entities/shared/generate-propose-entities-tools.ts @@ -1,7 +1,7 @@ import type { VersionedUrl } from "@blockprotocol/type-system"; import type { DistributiveOmit } from "@local/advanced-types/distribute"; import type { - ProposedEntity, + DeprecatedProposedEntity, ProposedEntitySchemaOrData, } from "@local/hash-isomorphic-utils/ai-inference-types"; import type { JSONSchema } from "openai/lib/jsonschema"; @@ -15,7 +15,7 @@ import { stripIdsFromDereferencedProperties } from "./strip-ids-from-dereference export type ProposeEntitiesToolName = "abandon_entities" | "create_entities"; type ProposedEntityWithSimplifiedProperties = DistributiveOmit< - ProposedEntity, + DeprecatedProposedEntity, "properties" > & { properties?: Record; diff --git a/apps/hash-ai-worker-ts/src/activities/shared/find-existing-entity.ts b/apps/hash-ai-worker-ts/src/activities/shared/find-existing-entity.ts index d2b727aa9c4..6ef4f2ab115 100644 --- a/apps/hash-ai-worker-ts/src/activities/shared/find-existing-entity.ts +++ b/apps/hash-ai-worker-ts/src/activities/shared/find-existing-entity.ts @@ -422,7 +422,7 @@ export const findExistingLinkEntity = async ({ } /** - * If we've reached here, the input has some properties + * If we've reached here, the new input has some properties */ const potentialMatchesWithProperties = linksWithOverlappingTypes.filter( (entity) => Object.keys(entity.properties).length > 0, 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 b923b7656af..8c88093be4d 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 @@ -14,7 +14,7 @@ import { parseHistoryItemPayload } from "@local/hash-backend-utils/temporal/pars import { type HashEntity, queryEntities } from "@local/hash-graph-sdk/entity"; import type { ManualInferenceTriggerInputName } from "@local/hash-isomorphic-utils/flows/browser-plugin-flow-types"; import type { GoalFlowTriggerInput } from "@local/hash-isomorphic-utils/flows/goal-flow-definitions"; -import type { RunFlowWorkflowParams } from "@local/hash-isomorphic-utils/flows/temporal-types"; +import type { RunAiFlowWorkflowParams } from "@local/hash-isomorphic-utils/flows/temporal-types"; import type { FlowDataSources } from "@local/hash-isomorphic-utils/flows/types"; import { currentTimeInstantTemporalAxes } from "@local/hash-isomorphic-utils/graph-queries"; import { normalizeWhitespace } from "@local/hash-isomorphic-utils/normalize"; @@ -31,7 +31,7 @@ let _temporalClient: TemporalClient | undefined; let _runFlowWorkflowParamsCache: MemoryCache | undefined; type PartialRunFlowWorkflowParams = Pick< - RunFlowWorkflowParams, + RunAiFlowWorkflowParams, "dataSources" | "webId" | "userAuthentication" > & { createEntitiesAsDraft: boolean }; @@ -95,7 +95,7 @@ const getPartialRunFlowWorkflowParams = async (params: { ); } - const [runFlowWorkflowParams] = inputs as RunFlowWorkflowParams[]; + const [runFlowWorkflowParams] = inputs as RunAiFlowWorkflowParams[]; if (!runFlowWorkflowParams) { throw new Error( diff --git a/apps/hash-ai-worker-ts/src/main.ts b/apps/hash-ai-worker-ts/src/main.ts index db2135fe098..c873dd34ffa 100644 --- a/apps/hash-ai-worker-ts/src/main.ts +++ b/apps/hash-ai-worker-ts/src/main.ts @@ -15,6 +15,7 @@ import { fileURLToPath } from "node:url"; import { createGraphClient } from "@local/hash-backend-utils/create-graph-client"; import { getRequiredEnv } from "@local/hash-backend-utils/environment"; +import { createCommonFlowActivities } from "@local/hash-backend-utils/flows"; import { SentryActivityInboundInterceptor } from "@local/hash-backend-utils/temporal/interceptors/activities/sentry"; import { sentrySinks } from "@local/hash-backend-utils/temporal/sinks/sentry"; import { createVaultClient } from "@local/hash-backend-utils/vault"; @@ -128,6 +129,7 @@ async function run() { graphApiClient, }), ...createFlowActivities({ vaultClient }), + ...createCommonFlowActivities({ graphApiClient }), }, connection, /** diff --git a/apps/hash-ai-worker-ts/src/shared/testing-utilities/mock-get-flow-context.ts b/apps/hash-ai-worker-ts/src/shared/testing-utilities/mock-get-flow-context.ts index 478cc6b306e..3d7919d916b 100644 --- a/apps/hash-ai-worker-ts/src/shared/testing-utilities/mock-get-flow-context.ts +++ b/apps/hash-ai-worker-ts/src/shared/testing-utilities/mock-get-flow-context.ts @@ -5,8 +5,9 @@ import type { } from "@blockprotocol/type-system"; import { extractEntityUuidFromEntityId } from "@blockprotocol/type-system"; import { HashEntity } from "@local/hash-graph-sdk/entity"; +import type { AiFlowActionDefinitionId } from "@local/hash-isomorphic-utils/flows/action-definitions"; import { mapFlowRunToEntityProperties } from "@local/hash-isomorphic-utils/flows/mappings"; -import type { RunFlowWorkflowParams } from "@local/hash-isomorphic-utils/flows/temporal-types"; +import type { RunAiFlowWorkflowParams } from "@local/hash-isomorphic-utils/flows/temporal-types"; import type { FlowDefinition } from "@local/hash-isomorphic-utils/flows/types"; import { generateUuid } from "@local/hash-isomorphic-utils/generate-uuid"; import { systemEntityTypes } from "@local/hash-isomorphic-utils/ontology-type-ids"; @@ -120,7 +121,7 @@ vi.mock("@local/hash-backend-utils/temporal", async (importOriginal) => { signal: async () => {}, // eslint-disable-next-line @typescript-eslint/require-await fetchHistory: async () => { - const mockedFlorWorkflowParams: RunFlowWorkflowParams = { + const mockedFlorWorkflowParams: RunAiFlowWorkflowParams = { dataSources: { files: { fileEntityIds: [] }, internetAccess: { @@ -131,7 +132,7 @@ vi.mock("@local/hash-backend-utils/temporal", async (importOriginal) => { }, }, }, - flowDefinition: {} as FlowDefinition, + flowDefinition: {} as FlowDefinition, flowTrigger: { triggerDefinitionId: "userTrigger", }, diff --git a/apps/hash-ai-worker-ts/src/workflows/run-flow-workflow.ts b/apps/hash-ai-worker-ts/src/workflows/run-flow-workflow.ts index 1bd4d547c34..ae6b91cdfe7 100644 --- a/apps/hash-ai-worker-ts/src/workflows/run-flow-workflow.ts +++ b/apps/hash-ai-worker-ts/src/workflows/run-flow-workflow.ts @@ -1,140 +1,23 @@ -import type { EntityUuid } from "@blockprotocol/type-system"; -import { sleep } from "@local/hash-backend-utils/utils"; -import { actionDefinitions } from "@local/hash-isomorphic-utils/flows/action-definitions"; +import type { ProxyFlowActivity } from "@local/hash-backend-utils/flows"; +import { processFlowWorkflow } from "@local/hash-backend-utils/flows/process-flow-workflow"; +import { type AiFlowActionDefinitionId } from "@local/hash-isomorphic-utils/flows/action-definitions"; import type { - RunFlowWorkflowParams, + RunAiFlowWorkflowParams, RunFlowWorkflowResponse, } from "@local/hash-isomorphic-utils/flows/temporal-types"; import type { FlowDefinition, - FlowStep, - Payload, - StepOutput, + FlowTrigger, } from "@local/hash-isomorphic-utils/flows/types"; -import { validateFlowDefinition } from "@local/hash-isomorphic-utils/flows/util"; -import { stringifyError } from "@local/hash-isomorphic-utils/stringify-error"; -import type { Status } from "@local/status"; -import { StatusCode } from "@local/status"; import { ActivityCancellationType, - ApplicationFailure, proxyActivities, - workflowInfo, } from "@temporalio/workflow"; import type { createFlowActivities } from "../activities/flow-activities.js"; import { heartbeatTimeoutSeconds } from "../shared/heartbeats.js"; -import { getAllStepsInFlow } from "./run-flow-workflow/get-all-steps-in-flow.js"; -import { getStepDefinitionFromFlowDefinition } from "./run-flow-workflow/get-step-definition-from-flow.js"; -import { - initializeActionStep, - initializeFlow, - initializeParallelGroup, -} from "./run-flow-workflow/initialize-flow.js"; -import { passOutputsToUnprocessedSteps } from "./run-flow-workflow/pass-outputs-to-unprocessed-steps.js"; import { setQueryAndSignalHandlers } from "./run-flow-workflow/set-query-and-signal-handlers.js"; -const log = (message: string) => { - // eslint-disable-next-line no-console - console.log(message); -}; - -const doesFlowStepHaveSatisfiedDependencies = (params: { - step: FlowStep; - flowDefinition: FlowDefinition; - processedStepIds: string[]; -}) => { - const { step, flowDefinition, processedStepIds } = params; - - if (step.kind === "action") { - /** - * An action step has satisfied dependencies if all of its inputs have - * been provided, based on the input sources defined in the step's - * definition. - * - * We don't need to check if all required inputs have been provided, - * as this will have been enforced when the flow was validated. - */ - - const { inputSources } = getStepDefinitionFromFlowDefinition({ - step, - flowDefinition, - }); - - const actionDefinition = actionDefinitions[step.actionDefinitionId]; - - return inputSources.every((inputSource) => { - const inputDefinition = actionDefinition.inputs.find( - ({ name }) => name === inputSource.inputName, - ); - - if (!inputDefinition) { - const errorMessage = `Definition for inputName '${inputSource.inputName}' in step ${step.stepId} not found in action definition ${step.actionDefinitionId}`; - - throw ApplicationFailure.create({ - message: errorMessage, - details: [ - { - code: StatusCode.FailedPrecondition, - message: errorMessage, - }, - ], - }); - } - - if ( - step.inputs?.some((input) => input.inputName === inputSource.inputName) - ) { - /** - * If the input has been provided, the input has been satisfied. - */ - return true; - } else if (inputDefinition.required) { - /** - * If the input is required, and it hasn't been provided the step - * has not satisfied its dependencies. - */ - return false; - } else if ( - inputSource.kind === "step-output" && - inputSource.sourceStepId !== "trigger" - ) { - /** - * If the input is optional, but depends on a runnable step (i.e. not - * the trigger), the step only has satisfied its dependencies if the - * step it depends on has been processed. - * - * This ensures that the step is processed when all possible inputs - * are provided in the flow. - */ - return processedStepIds.includes(inputSource.sourceStepId); - } else if (inputSource.kind === "parallel-group-input") { - /** - * If the input is optional, but has a parallel group input as it's source - * the step should only be processed once this input has been provided. - * - * Otherwise the parallel group won't run, and produce any outputs. - */ - return false; - } else { - /** - * Otherwise, we consider the input satisfied because it is optional. - */ - return true; - } - }); - } else { - /** - * A parallel group step has satisfied dependencies if the input it - * parallelizes over has been provided. - */ - - const { inputToParallelizeOn } = step; - - return !!inputToParallelizeOn; - } -}; - type FlowActivityId = keyof ReturnType; /** @@ -148,21 +31,20 @@ const activitiesHandlingCancellation: FlowActivityId[] = [ "researchEntitiesAction", ]; -const proxyFlowActivity = (params: { - actionId: ActionId; - maximumAttempts: number; - activityId: string; -}): ReturnType[ActionId] => { - const { actionId, maximumAttempts, activityId } = params; +const proxyFlowActivity: ProxyFlowActivity< + AiFlowActionDefinitionId, + typeof createFlowActivities +> = (params) => { + const { actionName, maximumAttempts, activityId } = params; - const { [actionId]: action } = proxyActivities< + const { [actionName]: action } = proxyActivities< ReturnType >({ - cancellationType: activitiesHandlingCancellation.includes(actionId) + cancellationType: activitiesHandlingCancellation.includes(actionName) ? ActivityCancellationType.WAIT_CANCELLATION_COMPLETED : ActivityCancellationType.ABANDON, - startToCloseTimeout: activitiesHandlingCancellation.includes(actionId) + startToCloseTimeout: activitiesHandlingCancellation.includes(actionName) ? /** * @todo H-3129 – research tasks can take a long time, and waiting for user input takes an indefinite amount of time. * - we need to be able to sleep at the workflow level and have activities that take a bounded, shorter amount of time. @@ -182,7 +64,7 @@ const proxyFlowActivity = (params: { * - heartbeats are throttled by default to 80% of the heartbeatTimeout, so sending a heartbeat does not mean it will be processed then * - maxHeartbeatThrottleInterval can be set in WorkerOptions, and otherwise defaults to 60s */ - heartbeatTimeout: activitiesHandlingCancellation.includes(actionId) + heartbeatTimeout: activitiesHandlingCancellation.includes(actionName) ? `${heartbeatTimeoutSeconds} second` : undefined, retry: { maximumAttempts }, @@ -192,430 +74,31 @@ const proxyFlowActivity = (params: { return action; }; -export const runFlowWorkflow = async ( - params: RunFlowWorkflowParams, -): Promise => { - const { flowDefinition, flowTrigger, userAuthentication, webId } = params; - - try { - validateFlowDefinition(flowDefinition); - } catch (error) { - throw ApplicationFailure.create({ - message: (error as Error).message, - details: [ - { - code: StatusCode.InvalidArgument, - message: (error as Error).message, - contents: [], - }, - ], - }); - } - - const userHasPermissionActivity = proxyFlowActivity({ - actionId: "userHasPermissionToRunFlowInWebActivity", - maximumAttempts: 1, - activityId: "check-user-permission", - }); - - const persistFlowActivity = proxyFlowActivity({ - actionId: "persistFlowActivity", - maximumAttempts: 1, - activityId: "persist-flow", - }); - - // Ensure the user has permission to create entities in specified web - const userHasPermissionToRunFlowInWeb = await userHasPermissionActivity(); - - if (userHasPermissionToRunFlowInWeb.status !== "ok") { - const errorMessage = `User does not have permission to run flow in web ${webId}: ${userHasPermissionToRunFlowInWeb.errorMessage}`; - throw ApplicationFailure.create({ - message: errorMessage, - details: [ - { - code: StatusCode.PermissionDenied, - message: errorMessage, - contents: [], - }, - ], - }); - } - - log(`Initializing ${flowDefinition.name} Flow`); - - const { workflowId } = workflowInfo(); - - const flow = initializeFlow({ - flowDefinition, - flowTrigger, - flowRunId: workflowId as EntityUuid, - /** use the flow definition's name as a placeholder – we need the Flow persisted to link the generating name usage to it */ - name: flowDefinition.name, - }); - - await persistFlowActivity({ flow, userAuthentication, webId }); - - const generateFlowRunNameActivity = proxyFlowActivity({ - actionId: "generateFlowRunName", - maximumAttempts: 1, - activityId: "generate-flow-run-name", - }); +const generateFlowRunName = (params: { + flowDefinition: FlowDefinition; + flowTrigger: FlowTrigger; +}) => { + const { flowDefinition, flowTrigger } = params; - const generatedName = await generateFlowRunNameActivity({ - flowDefinition, - flowTrigger, + const { generateFlowRunName: generateFlowRunNameActivity } = proxyActivities< + ReturnType + >({ + startToCloseTimeout: "60 second", + retry: { maximumAttempts: 1 }, }); - flow.name = generatedName; - await persistFlowActivity({ flow, userAuthentication, webId }); + return generateFlowRunNameActivity({ flowDefinition, flowTrigger }); +}; +export const runFlowWorkflow = async ( + params: RunAiFlowWorkflowParams, +): Promise => { setQueryAndSignalHandlers(); - const processedStepIds: string[] = []; - const processStepErrors: Record, "contents">> = {}; - - // Function to process a single step - const processStep = async (currentStepId: string) => { - log(`Step ${currentStepId}: processing step`); - - const currentStep = getAllStepsInFlow(flow).find( - (step) => step.stepId === currentStepId, - ); - - if (!currentStep) { - processStepErrors[currentStepId] = { - code: StatusCode.NotFound, - message: `No step found with id ${currentStepId}`, - }; - - return; - } - - if (currentStep.kind === "action") { - const actionStepDefinition = getStepDefinitionFromFlowDefinition({ - step: currentStep, - flowDefinition, - }); - - const actionActivity = proxyFlowActivity({ - actionId: `${currentStep.actionDefinitionId}Action`, - maximumAttempts: actionStepDefinition.retryCount ?? 3, - activityId: currentStep.stepId, - }); - - log( - `Step ${currentStepId}: executing "${ - currentStep.actionDefinitionId - }" action with ${(currentStep.inputs ?? []).length} inputs`, - ); - - let actionResponse: Status<{ - outputs: StepOutput[]; - }>; - - try { - actionResponse = await actionActivity({ - inputs: currentStep.inputs ?? [], - }); - } catch (error) { - log( - `Step ${currentStepId}: encountered runtime error executing "${ - currentStep.actionDefinitionId - }" action: ${stringifyError(error)}`, - ); - - actionResponse = { - contents: [], - code: StatusCode.Internal, - message: `Error executing action ${ - currentStep.actionDefinitionId - }: ${stringifyError(error)}`, - }; - - processStepErrors[currentStepId] = actionResponse; - } - - /** - * Consider the step processed, even if the action failed to prevent - * an infinite loop of retries. - */ - processedStepIds.push(currentStep.stepId); - - if (actionResponse.code !== StatusCode.Ok) { - log( - `Step ${currentStepId}: error executing "${currentStep.actionDefinitionId}" action`, - ); - - processStepErrors[currentStepId] = { - code: StatusCode.Internal, - message: `Action ${currentStep.actionDefinitionId} failed with status code ${actionResponse.code}: ${actionResponse.message}`, - }; - - return; - } - - const { outputs } = actionResponse.contents[0]!; - - log( - `Step ${currentStepId}: obtained ${outputs.length} outputs from "${currentStep.actionDefinitionId}" action`, - ); - - currentStep.outputs = outputs; - - const status = passOutputsToUnprocessedSteps({ - flow, - flowDefinition, - outputs, - processedStepIds, - stepId: currentStepId, - outputDefinitions: - actionDefinitions[currentStep.actionDefinitionId].outputs, - }); - - if (status.code !== StatusCode.Ok) { - processStepErrors[currentStepId] = { - code: status.code, - message: status.message, - }; - - // eslint-disable-next-line no-useless-return - return; - } - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - } else if (currentStep.kind === "parallel-group") { - const parallelGroupStepDefinition = getStepDefinitionFromFlowDefinition({ - step: currentStep, - flowDefinition, - }); - - const { inputToParallelizeOn } = currentStep; - - if (!inputToParallelizeOn) { - processStepErrors[currentStepId] = { - code: StatusCode.Internal, - message: `No input provided to parallelize on for step ${currentStepId}`, - }; - - return; - } - - const { steps: parallelGroupStepDefinitions } = - parallelGroupStepDefinition; - - const arrayToParallelizeOn = inputToParallelizeOn.payload.value; - - const newSteps = arrayToParallelizeOn.flatMap( - (parallelizedValue, index) => - parallelGroupStepDefinitions.map((stepDefinition) => { - if (stepDefinition.kind === "action") { - const parallelGroupInputPayload: Payload = { - kind: inputToParallelizeOn.payload.kind, - value: parallelizedValue, - /** @todo: figure out why this isn't assignable */ - } as Payload; - - return initializeActionStep({ - flowTrigger, - stepDefinition, - overrideStepId: `${stepDefinition.stepId}~${index}`, - parallelGroupInputPayload, - }); - } else { - return initializeParallelGroup({ flowTrigger, stepDefinition }); - } - }), - ); - - /** - * Add the new steps to the child steps of the parallel group step. - */ - currentStep.steps = [...(currentStep.steps ?? []), ...newSteps]; - - /** - * We consider the parallel group step "processed", even though its child - * steps may not have finished executing, so that the step is not re-evaluated - * in a subsequent iteration of `processSteps`. - */ - processedStepIds.push(currentStep.stepId); - } - }; - - const stepWithSatisfiedDependencies = getAllStepsInFlow(flow).filter((step) => - doesFlowStepHaveSatisfiedDependencies({ - step, - flowDefinition, - processedStepIds, - }), - ); - - if (stepWithSatisfiedDependencies.length === 0) { - const errorMessage = - "No steps have satisfied dependencies when initializing the flow."; - throw ApplicationFailure.create({ - message: errorMessage, - details: [ - { - code: StatusCode.FailedPrecondition, - message: errorMessage, - contents: [{ flow }], - }, - ], - }); - } - - // Recursively process steps which have satisfied dependencies - const processSteps = async () => { - const stepsToProcess = getAllStepsInFlow(flow).filter( - (step) => - doesFlowStepHaveSatisfiedDependencies({ - step, - flowDefinition, - processedStepIds, - }) && - !processedStepIds.some( - (processedStepId) => processedStepId === step.stepId, - ), - ); - - // There are no more steps which can be processed, so we exit the recursive loop - if (stepsToProcess.length === 0) { - return; - } - - await Promise.all(stepsToProcess.map((step) => processStep(step.stepId))); - - await persistFlowActivity({ flow, userAuthentication, webId }); - - // Recursively call processSteps until all steps are processed - await processSteps(); - }; - - await processSteps(); - - log("All processable steps have completed processing"); - - /** - * Wait to flush logs - * @todo flush logs by calling the debounced function's flush, flushLogs – need to deal with it importing code that - * the workflow can't - */ - await sleep(3_000); - - const stepErrors = Object.entries(processStepErrors).map( - ([stepId, status]) => ({ ...status, contents: [{ stepId }] }), - ); - - /** @todo this is not necessarily an error once there are branches */ - if (processedStepIds.length !== getAllStepsInFlow(flow).length) { - const errorMessage = "Not all steps in the flows were processed."; - throw ApplicationFailure.create({ - message: errorMessage, - details: [ - { - code: StatusCode.Unknown, - message: errorMessage, - contents: [{ flow, stepErrors }], - }, - ], - }); - } - - for (const outputDefinition of flowDefinition.outputs) { - const step = getAllStepsInFlow(flow).find( - (flowStep) => flowStep.stepId === outputDefinition.stepId, - ); - - const errorPrefix = `Error processing output definition '${outputDefinition.name}', `; - - if (!step) { - if (!outputDefinition.required) { - continue; - } - - const errorMessage = `${errorPrefix}required step with id '${outputDefinition.stepId}' not found in outputs.`; - throw ApplicationFailure.create({ - message: errorMessage, - details: [ - { - code: StatusCode.NotFound, - message: errorMessage, - contents: [{ flow, stepErrors }], - }, - ], - }); - } - - if (step.kind === "action") { - const output = step.outputs?.find( - ({ outputName }) => outputName === outputDefinition.stepOutputName, - ); - - if (!output) { - if (!outputDefinition.required) { - continue; - } - - const errorMessage = `${errorPrefix}there is no output with name '${outputDefinition.stepOutputName}' in step ${step.stepId}`; - - throw ApplicationFailure.create({ - message: errorMessage, - details: [ - { - code: StatusCode.NotFound, - message: errorMessage, - contents: [{ stepErrors }], - }, - ], - }); - } - - flow.outputs = [ - ...(flow.outputs ?? []), - { - outputName: outputDefinition.name, - payload: output.payload, - }, - ]; - } else { - const output = step.aggregateOutput; - - if (!output) { - const errorMessage = `${errorPrefix}no aggregate output found in step ${step.stepId}`; - throw ApplicationFailure.create({ - message: errorMessage, - details: [ - { - code: StatusCode.NotFound, - message: errorMessage, - contents: [{ stepErrors }], - }, - ], - }); - } - - flow.outputs = [ - ...(flow.outputs ?? []), - { - outputName: outputDefinition.name, - payload: output.payload, - }, - ]; - } - } - - await persistFlowActivity({ flow, userAuthentication, webId }); - - const outputs = flow.outputs ?? []; - - return { - /** - * Steps may error and be retried, or the whole workflow retried, while still producing the required outputs - * – start with an initial status of OK if the outputs are present, to be adjusted if necessary. - */ - code: - outputs.length === flowDefinition.outputs.length - ? StatusCode.Ok - : StatusCode.Internal, - contents: [{ outputs, stepErrors }], - }; + return await processFlowWorkflow({ + ...params, + flowType: "ai", + proxyFlowActivity, + generateFlowRunName, + }); }; diff --git a/apps/hash-api/src/graph/ensure-system-graph-is-initialized/migrate-ontology-types/migrations/025-add-aviation-types.dev.migration.ts b/apps/hash-api/src/graph/ensure-system-graph-is-initialized/migrate-ontology-types/migrations/025-add-aviation-types.dev.migration.ts new file mode 100644 index 00000000000..4cb73021291 --- /dev/null +++ b/apps/hash-api/src/graph/ensure-system-graph-is-initialized/migrate-ontology-types/migrations/025-add-aviation-types.dev.migration.ts @@ -0,0 +1,1021 @@ +import { + blockProtocolDataTypes, + blockProtocolEntityTypes, + blockProtocolPropertyTypes, +} from "@local/hash-isomorphic-utils/ontology-type-ids"; + +import type { MigrationFunction } from "../types"; +import { + createSystemDataTypeIfNotExists, + createSystemEntityTypeIfNotExists, + createSystemPropertyTypeIfNotExists, + getCurrentHashDataTypeId, +} from "../util"; + +const migrate: MigrationFunction = async ({ + context, + authentication, + migrationState, +}) => { + /** + * Step 1: Create data types + */ + + /** + * Angle data type hierarchy: Angle → Degree → Latitude/Longitude + */ + + const angleDataType = await createSystemDataTypeIfNotExists( + context, + authentication, + { + dataTypeDefinition: { + allOf: [{ $ref: blockProtocolDataTypes.number.dataTypeId }], + abstract: true, + title: "Angle", + description: + "A measure of rotation or the space between two intersecting lines.", + type: "number", + }, + conversions: {}, + migrationState, + webShortname: "h", + }, + ); + + const degreeDataType = await createSystemDataTypeIfNotExists( + context, + authentication, + { + dataTypeDefinition: { + allOf: [{ $ref: angleDataType.schema.$id }], + title: "Degree", + description: + "A unit of angular measure equal to 1/360 of a full rotation.", + label: { + right: "°", + }, + type: "number", + }, + conversions: {}, + migrationState, + webShortname: "h", + }, + ); + + const latitudeDataType = await createSystemDataTypeIfNotExists( + context, + authentication, + { + dataTypeDefinition: { + allOf: [{ $ref: degreeDataType.schema.$id }], + title: "Latitude", + description: + "The angular distance of a position north or south of the equator, ranging from -90° (South Pole) to +90° (North Pole).", + minimum: -90, + maximum: 90, + type: "number", + }, + conversions: {}, + migrationState, + webShortname: "h", + }, + ); + + const longitudeDataType = await createSystemDataTypeIfNotExists( + context, + authentication, + { + dataTypeDefinition: { + allOf: [{ $ref: degreeDataType.schema.$id }], + title: "Longitude", + description: + "The angular distance of a position east or west of the prime meridian, ranging from -180° to +180°.", + minimum: -180, + maximum: 180, + type: "number", + }, + conversions: {}, + migrationState, + webShortname: "h", + }, + ); + + /** + * Speed data type hierarchy: Speed → Meters per Second (canonical) → km/h, knots, ft/min + */ + + const speedDataType = await createSystemDataTypeIfNotExists( + context, + authentication, + { + dataTypeDefinition: { + allOf: [{ $ref: blockProtocolDataTypes.number.dataTypeId }], + abstract: true, + title: "Speed", + description: + "A measure of the rate of movement or change in position over time.", + type: "number", + }, + conversions: {}, + migrationState, + webShortname: "h", + }, + ); + + const metersPerSecondDataType = await createSystemDataTypeIfNotExists( + context, + authentication, + { + dataTypeDefinition: { + allOf: [{ $ref: speedDataType.schema.$id }], + title: "Meters per Second", + description: + "The SI unit of speed, expressing the number of meters traveled in one second.", + label: { + right: "m/s", + }, + type: "number", + }, + conversions: {}, + migrationState, + webShortname: "h", + }, + ); + + const _kilometersPerHourDataType = await createSystemDataTypeIfNotExists( + context, + authentication, + { + dataTypeDefinition: { + allOf: [{ $ref: speedDataType.schema.$id }], + title: "Kilometers per Hour", + description: + "A unit of speed expressing the number of kilometers traveled in one hour.", + label: { + right: "km/h", + }, + type: "number", + }, + conversions: { + [metersPerSecondDataType.metadata.recordId.baseUrl]: { + // 1 km/h = 1000m / 3600s = 5/18 m/s + // m/s → km/h: self * 18 / 5 + from: { + expression: [ + "/", + ["*", "self", { const: 18, type: "number" }], + { const: 5, type: "number" }, + ], + }, + // km/h → m/s: self * 5 / 18 + to: { + expression: [ + "/", + ["*", "self", { const: 5, type: "number" }], + { const: 18, type: "number" }, + ], + }, + }, + }, + migrationState, + webShortname: "h", + }, + ); + + const knotsDataType = await createSystemDataTypeIfNotExists( + context, + authentication, + { + dataTypeDefinition: { + allOf: [{ $ref: speedDataType.schema.$id }], + title: "Knots", + description: + "A unit of speed equal to one nautical mile per hour, commonly used in aviation and maritime contexts.", + label: { + right: "kn", + }, + type: "number", + }, + conversions: { + [metersPerSecondDataType.metadata.recordId.baseUrl]: { + // 1 knot = 1852m / 3600s (1 nautical mile = 1852m exactly) + // m/s → knots: self * 3600 / 1852 + from: { + expression: [ + "/", + ["*", "self", { const: 3600, type: "number" }], + { const: 1852, type: "number" }, + ], + }, + // knots → m/s: self * 1852 / 3600 + to: { + expression: [ + "/", + ["*", "self", { const: 1852, type: "number" }], + { const: 3600, type: "number" }, + ], + }, + }, + }, + migrationState, + webShortname: "h", + }, + ); + + const feetPerMinuteDataType = await createSystemDataTypeIfNotExists( + context, + authentication, + { + dataTypeDefinition: { + allOf: [{ $ref: speedDataType.schema.$id }], + title: "Feet per Minute", + description: + "A unit of vertical speed commonly used in aviation to measure rate of climb or descent.", + label: { + right: "ft/min", + }, + type: "number", + }, + conversions: { + [metersPerSecondDataType.metadata.recordId.baseUrl]: { + // 1 ft/min = 0.3048m / 60s (1 foot = 0.3048m exactly) + // m/s → ft/min: self * 60 / 0.3048 + from: { + expression: [ + "/", + ["*", "self", { const: 60, type: "number" }], + { const: 0.3048, type: "number" }, + ], + }, + // ft/min → m/s: self * 0.3048 / 60 + to: { + expression: [ + "/", + ["*", "self", { const: 0.3048, type: "number" }], + { const: 60, type: "number" }, + ], + }, + }, + }, + migrationState, + webShortname: "h", + }, + ); + + /** + * Step 2: Create property types + */ + + const iataCodePropertyType = await createSystemPropertyTypeIfNotExists( + context, + authentication, + { + propertyTypeDefinition: { + title: "IATA Code", + description: + "A code assigned by the International Air Transport Association (IATA) to identify airports, airlines, or aircraft types.", + possibleValues: [{ primitiveDataType: "text" }], + }, + migrationState, + webShortname: "h", + }, + ); + + const icaoCodePropertyType = await createSystemPropertyTypeIfNotExists( + context, + authentication, + { + propertyTypeDefinition: { + title: "ICAO Code", + description: + "A code assigned by the International Civil Aviation Organization (ICAO) to identify airports, airlines, or aircraft types.", + possibleValues: [{ primitiveDataType: "text" }], + }, + migrationState, + webShortname: "h", + }, + ); + + const gatePropertyType = await createSystemPropertyTypeIfNotExists( + context, + authentication, + { + propertyTypeDefinition: { + title: "Gate", + description: + "The gate number or identifier at an airport terminal where passengers board or disembark.", + possibleValues: [{ primitiveDataType: "text" }], + }, + migrationState, + webShortname: "h", + }, + ); + + const terminalPropertyType = await createSystemPropertyTypeIfNotExists( + context, + authentication, + { + propertyTypeDefinition: { + title: "Terminal", + description: + "The terminal building or area at an airport where passengers check in, wait, and board flights.", + possibleValues: [{ primitiveDataType: "text" }], + }, + migrationState, + webShortname: "h", + }, + ); + + const baggageClaimPropertyType = await createSystemPropertyTypeIfNotExists( + context, + authentication, + { + propertyTypeDefinition: { + title: "Baggage Claim", + description: + "The area or carousel number where passengers collect their checked luggage after a flight.", + possibleValues: [{ primitiveDataType: "text" }], + }, + migrationState, + webShortname: "h", + }, + ); + + const integerDataTypeId = getCurrentHashDataTypeId({ + dataTypeKey: "integer", + migrationState, + }); + + const datetimeDataTypeId = getCurrentHashDataTypeId({ + dataTypeKey: "datetime", + migrationState, + }); + + const dateDataTypeId = getCurrentHashDataTypeId({ + dataTypeKey: "date", + migrationState, + }); + + const delayInSecondsPropertyType = await createSystemPropertyTypeIfNotExists( + context, + authentication, + { + propertyTypeDefinition: { + title: "Delay In Seconds", + description: + "The amount of delay in seconds for a scheduled event such as a flight departure or arrival.", + possibleValues: [{ dataTypeId: integerDataTypeId }], + }, + migrationState, + webShortname: "h", + }, + ); + + /** + * Gate times (pushback/arrival at gate) + */ + const scheduledGateTimePropertyType = + await createSystemPropertyTypeIfNotExists(context, authentication, { + propertyTypeDefinition: { + title: "Scheduled Gate Time", + description: + "The originally planned date and time for gate departure (pushback) or arrival.", + possibleValues: [{ dataTypeId: datetimeDataTypeId }], + }, + migrationState, + webShortname: "h", + }); + + const estimatedGateTimePropertyType = + await createSystemPropertyTypeIfNotExists(context, authentication, { + propertyTypeDefinition: { + title: "Estimated Gate Time", + description: + "The predicted date and time for gate departure (pushback) or arrival.", + possibleValues: [{ dataTypeId: datetimeDataTypeId }], + }, + migrationState, + webShortname: "h", + }); + + const actualGateTimePropertyType = await createSystemPropertyTypeIfNotExists( + context, + authentication, + { + propertyTypeDefinition: { + title: "Actual Gate Time", + description: + "The actual date and time of gate departure (pushback) or arrival.", + possibleValues: [{ dataTypeId: datetimeDataTypeId }], + }, + migrationState, + webShortname: "h", + }, + ); + + /** + * Runway times (takeoff/touchdown) + */ + const scheduledRunwayTimePropertyType = + await createSystemPropertyTypeIfNotExists(context, authentication, { + propertyTypeDefinition: { + title: "Scheduled Runway Time", + description: + "The originally planned date and time for runway departure (takeoff) or arrival (touchdown).", + possibleValues: [{ dataTypeId: datetimeDataTypeId }], + }, + migrationState, + webShortname: "h", + }); + + const estimatedRunwayTimePropertyType = + await createSystemPropertyTypeIfNotExists(context, authentication, { + propertyTypeDefinition: { + title: "Estimated Runway Time", + description: + "The predicted date and time for runway departure (takeoff) or arrival (touchdown).", + possibleValues: [{ dataTypeId: datetimeDataTypeId }], + }, + migrationState, + webShortname: "h", + }); + + const actualRunwayTimePropertyType = + await createSystemPropertyTypeIfNotExists(context, authentication, { + propertyTypeDefinition: { + title: "Actual Runway Time", + description: + "The actual date and time of runway departure (takeoff) or arrival (touchdown).", + possibleValues: [{ dataTypeId: datetimeDataTypeId }], + }, + migrationState, + webShortname: "h", + }); + + const flightNumberPropertyType = await createSystemPropertyTypeIfNotExists( + context, + authentication, + { + propertyTypeDefinition: { + title: "Flight Number", + description: + "A numeric or alphanumeric code identifying a specific scheduled airline service.", + possibleValues: [{ primitiveDataType: "text" }], + }, + migrationState, + webShortname: "h", + }, + ); + + const flightStatusDataType = await createSystemDataTypeIfNotExists( + context, + authentication, + { + dataTypeDefinition: { + allOf: [{ $ref: blockProtocolDataTypes.text.dataTypeId }], + title: "Flight Status", + description: + "The current operational status of a flight, indicating whether it is scheduled, in progress, completed, or has encountered issues.", + enum: [ + "Scheduled", + "Active", + "Landed", + "Cancelled", + "Incident", + "Diverted", + ], + type: "string", + }, + conversions: {}, + migrationState, + webShortname: "h", + }, + ); + + const flightStatusPropertyType = await createSystemPropertyTypeIfNotExists( + context, + authentication, + { + propertyTypeDefinition: { + title: "Flight Status", + description: "The current operational status of a flight.", + possibleValues: [{ dataTypeId: flightStatusDataType.schema.$id }], + }, + migrationState, + webShortname: "h", + }, + ); + + const flightDatePropertyType = await createSystemPropertyTypeIfNotExists( + context, + authentication, + { + propertyTypeDefinition: { + title: "Flight Date", + description: + "The calendar date on which a flight is scheduled to operate.", + possibleValues: [{ dataTypeId: dateDataTypeId }], + }, + migrationState, + webShortname: "h", + }, + ); + + const timezonePropertyType = await createSystemPropertyTypeIfNotExists( + context, + authentication, + { + propertyTypeDefinition: { + title: "Timezone", + description: + "A time zone identifier (e.g. 'America/Los_Angeles', 'Europe/London').", + possibleValues: [{ primitiveDataType: "text" }], + }, + migrationState, + webShortname: "h", + }, + ); + + const cityPropertyType = await createSystemPropertyTypeIfNotExists( + context, + authentication, + { + propertyTypeDefinition: { + title: "City", + description: "The city where something is located, occurred, etc.", + possibleValues: [{ primitiveDataType: "text" }], + }, + migrationState, + webShortname: "h", + }, + ); + + const runwayPropertyType = await createSystemPropertyTypeIfNotExists( + context, + authentication, + { + propertyTypeDefinition: { + title: "Runway", + description: "The runway identifier used for takeoff or landing.", + possibleValues: [{ primitiveDataType: "text" }], + }, + migrationState, + webShortname: "h", + }, + ); + + const flightTypePropertyType = await createSystemPropertyTypeIfNotExists( + context, + authentication, + { + propertyTypeDefinition: { + title: "Flight Type", + description: "The category of flight operation.", + possibleValues: [{ primitiveDataType: "text" }], + }, + migrationState, + webShortname: "h", + }, + ); + + const codesharePropertyType = await createSystemPropertyTypeIfNotExists( + context, + authentication, + { + propertyTypeDefinition: { + title: "Codeshare", + description: + "A codeshare flight number, where multiple airlines sell seats on the same flight under their own flight numbers.", + possibleValues: [ + { + propertyTypeObjectProperties: { + [iataCodePropertyType.metadata.recordId.baseUrl]: { + $ref: iataCodePropertyType.schema.$id, + }, + [icaoCodePropertyType.metadata.recordId.baseUrl]: { + $ref: icaoCodePropertyType.schema.$id, + }, + }, + propertyTypeObjectRequiredProperties: [], + }, + ], + }, + migrationState, + webShortname: "h", + }, + ); + + const registrationNumberPropertyType = + await createSystemPropertyTypeIfNotExists(context, authentication, { + propertyTypeDefinition: { + title: "Registration Number", + description: + "A unique alphanumeric code assigned to an aircraft, also known as a tail number (e.g. 'N123AB').", + possibleValues: [{ primitiveDataType: "text" }], + }, + migrationState, + webShortname: "h", + }); + + const metersDataTypeId = getCurrentHashDataTypeId({ + dataTypeKey: "meters", + migrationState, + }); + + const latitudePropertyType = await createSystemPropertyTypeIfNotExists( + context, + authentication, + { + propertyTypeDefinition: { + title: "Latitude", + description: + "The angular distance of a position north or south of the equator.", + possibleValues: [{ dataTypeId: latitudeDataType.schema.$id }], + }, + migrationState, + webShortname: "h", + }, + ); + + const longitudePropertyType = await createSystemPropertyTypeIfNotExists( + context, + authentication, + { + propertyTypeDefinition: { + title: "Longitude", + description: + "The angular distance of a position east or west of the prime meridian.", + possibleValues: [{ dataTypeId: longitudeDataType.schema.$id }], + }, + migrationState, + webShortname: "h", + }, + ); + + const altitudePropertyType = await createSystemPropertyTypeIfNotExists( + context, + authentication, + { + propertyTypeDefinition: { + title: "Altitude", + description: + "The height of an object above a reference point, such as sea level or the ground.", + possibleValues: [{ dataTypeId: metersDataTypeId }], + }, + migrationState, + webShortname: "h", + }, + ); + + const directionPropertyType = await createSystemPropertyTypeIfNotExists( + context, + authentication, + { + propertyTypeDefinition: { + title: "Direction", + description: + "The heading or bearing of something, measured in degrees from true north.", + possibleValues: [{ dataTypeId: degreeDataType.schema.$id }], + }, + migrationState, + webShortname: "h", + }, + ); + + const groundSpeedPropertyType = await createSystemPropertyTypeIfNotExists( + context, + authentication, + { + propertyTypeDefinition: { + title: "Ground Speed", + description: + "The horizontal speed of an aircraft relative to the ground.", + possibleValues: [{ dataTypeId: knotsDataType.schema.$id }], + }, + migrationState, + webShortname: "h", + }, + ); + + const verticalSpeedPropertyType = await createSystemPropertyTypeIfNotExists( + context, + authentication, + { + propertyTypeDefinition: { + title: "Vertical Speed", + description: "The rate of vertical movement (climb or descent).", + possibleValues: [{ dataTypeId: feetPerMinuteDataType.schema.$id }], + }, + migrationState, + webShortname: "h", + }, + ); + + const isOnGroundPropertyType = await createSystemPropertyTypeIfNotExists( + context, + authentication, + { + propertyTypeDefinition: { + title: "Is On Ground", + description: "Whether something is currently on the ground.", + possibleValues: [{ primitiveDataType: "boolean" }], + }, + migrationState, + webShortname: "h", + }, + ); + + /** + * Step 2: Create entity types + */ + + const airportEntityType = await createSystemEntityTypeIfNotExists( + context, + authentication, + { + entityTypeDefinition: { + title: "Airport", + titlePlural: "Airports", + icon: "🏢", + description: + "A facility where aircraft take off and land, with infrastructure for passenger and cargo services.", + labelProperty: blockProtocolPropertyTypes.name.propertyTypeBaseUrl, + properties: [ + { + propertyType: blockProtocolPropertyTypes.name.propertyTypeId, + required: true, + }, + { + propertyType: iataCodePropertyType, + }, + { + propertyType: icaoCodePropertyType, + }, + { + propertyType: timezonePropertyType, + }, + { + propertyType: cityPropertyType, + }, + ], + }, + migrationState, + webShortname: "h", + }, + ); + + const airlineEntityType = await createSystemEntityTypeIfNotExists( + context, + authentication, + { + entityTypeDefinition: { + title: "Airline", + titlePlural: "Airlines", + icon: "🏦", + description: + "A company that provides air transport services for passengers and/or cargo.", + labelProperty: blockProtocolPropertyTypes.name.propertyTypeBaseUrl, + properties: [ + { + propertyType: blockProtocolPropertyTypes.name.propertyTypeId, + required: true, + }, + { + propertyType: iataCodePropertyType, + }, + { + propertyType: icaoCodePropertyType, + }, + ], + }, + migrationState, + webShortname: "h", + }, + ); + + const aircraftEntityType = await createSystemEntityTypeIfNotExists( + context, + authentication, + { + entityTypeDefinition: { + title: "Aircraft", + titlePlural: "Aircraft", + icon: "🛩️", + description: + "A vehicle designed for air travel, such as an airplane or helicopter.", + labelProperty: registrationNumberPropertyType.metadata.recordId.baseUrl, + properties: [ + { + propertyType: registrationNumberPropertyType, + required: true, + }, + { + propertyType: icaoCodePropertyType, + }, + ], + }, + migrationState, + webShortname: "h", + }, + ); + + /** + * Step 3: Create link entity types + */ + + const departsFromLinkEntityType = await createSystemEntityTypeIfNotExists( + context, + authentication, + { + entityTypeDefinition: { + allOf: [blockProtocolEntityTypes.link.entityTypeId], + title: "Departs From", + icon: "🛫", + inverse: { + title: "Departure For", + }, + description: + "Indicates the airport from which a flight departs, including departure-specific details.", + properties: [ + { propertyType: gatePropertyType }, + { propertyType: terminalPropertyType }, + { propertyType: runwayPropertyType }, + { propertyType: delayInSecondsPropertyType }, + { propertyType: scheduledGateTimePropertyType }, + { propertyType: estimatedGateTimePropertyType }, + { propertyType: actualGateTimePropertyType }, + { propertyType: scheduledRunwayTimePropertyType }, + { propertyType: estimatedRunwayTimePropertyType }, + { propertyType: actualRunwayTimePropertyType }, + ], + }, + migrationState, + webShortname: "h", + }, + ); + + const arrivesAtLinkEntityType = await createSystemEntityTypeIfNotExists( + context, + authentication, + { + entityTypeDefinition: { + allOf: [blockProtocolEntityTypes.link.entityTypeId], + title: "Arrives At", + icon: "🛬", + inverse: { + title: "Arrival For", + }, + description: + "Indicates the airport at which a flight arrives, including arrival-specific details.", + properties: [ + { propertyType: gatePropertyType }, + { propertyType: terminalPropertyType }, + { propertyType: runwayPropertyType }, + { propertyType: baggageClaimPropertyType }, + { propertyType: delayInSecondsPropertyType }, + { propertyType: scheduledGateTimePropertyType }, + { propertyType: estimatedGateTimePropertyType }, + { propertyType: actualGateTimePropertyType }, + { propertyType: scheduledRunwayTimePropertyType }, + { propertyType: estimatedRunwayTimePropertyType }, + { propertyType: actualRunwayTimePropertyType }, + ], + }, + migrationState, + webShortname: "h", + }, + ); + + const operatedByLinkEntityType = await createSystemEntityTypeIfNotExists( + context, + authentication, + { + entityTypeDefinition: { + allOf: [blockProtocolEntityTypes.link.entityTypeId], + title: "Operated By", + icon: "👨‍✈️", + inverse: { + title: "Operates", + }, + description: "Indicates the airline that operates a flight.", + properties: [], + }, + migrationState, + webShortname: "h", + }, + ); + + const usesAircraftLinkEntityType = await createSystemEntityTypeIfNotExists( + context, + authentication, + { + entityTypeDefinition: { + allOf: [blockProtocolEntityTypes.link.entityTypeId], + title: "Uses Aircraft", + icon: "🛩️", + inverse: { + title: "Used For Flight", + }, + description: "Indicates the aircraft used to operate a flight.", + properties: [], + }, + migrationState, + webShortname: "h", + }, + ); + + /** + * Step 4: Create the Flight entity type with links + */ + + const _flightEntityType = await createSystemEntityTypeIfNotExists( + context, + authentication, + { + entityTypeDefinition: { + title: "Flight", + titlePlural: "Flights", + icon: "✈️", + description: "A scheduled air transport service between two airports.", + labelProperty: flightNumberPropertyType.metadata.recordId.baseUrl, + properties: [ + { + propertyType: flightNumberPropertyType, + required: true, + }, + { + propertyType: iataCodePropertyType, + }, + { + propertyType: icaoCodePropertyType, + }, + { + propertyType: flightTypePropertyType, + }, + { + propertyType: flightStatusPropertyType, + }, + { + propertyType: flightDatePropertyType, + }, + { + propertyType: codesharePropertyType, + array: true, + }, + { + propertyType: latitudePropertyType, + }, + { + propertyType: longitudePropertyType, + }, + { + propertyType: altitudePropertyType, + }, + { + propertyType: directionPropertyType, + }, + { + propertyType: groundSpeedPropertyType, + }, + { + propertyType: verticalSpeedPropertyType, + }, + { + propertyType: isOnGroundPropertyType, + }, + ], + outgoingLinks: [ + { + linkEntityType: departsFromLinkEntityType, + destinationEntityTypes: [airportEntityType.schema.$id], + minItems: 1, + maxItems: 1, + }, + { + linkEntityType: arrivesAtLinkEntityType, + destinationEntityTypes: [airportEntityType.schema.$id], + minItems: 1, + maxItems: 1, + }, + { + linkEntityType: operatedByLinkEntityType, + destinationEntityTypes: [airlineEntityType.schema.$id], + maxItems: 1, + }, + { + linkEntityType: usesAircraftLinkEntityType, + destinationEntityTypes: [aircraftEntityType.schema.$id], + maxItems: 1, + }, + ], + }, + migrationState, + webShortname: "h", + }, + ); + + return migrationState; +}; + +export default migrate; diff --git a/apps/hash-api/src/graphql/resolvers/flows/start-flow.ts b/apps/hash-api/src/graphql/resolvers/flows/start-flow.ts index fff7dee429f..ffa2f7a44ae 100644 --- a/apps/hash-api/src/graphql/resolvers/flows/start-flow.ts +++ b/apps/hash-api/src/graphql/resolvers/flows/start-flow.ts @@ -6,7 +6,11 @@ import type { import { validateFlowDefinition } from "@local/hash-isomorphic-utils/flows/util"; import { generateUuid } from "@local/hash-isomorphic-utils/generate-uuid"; -import type { MutationStartFlowArgs, ResolverFn } from "../../api-types.gen"; +import { + FlowType, + type MutationStartFlowArgs, + type ResolverFn, +} from "../../api-types.gen"; import type { LoggedInGraphQLContext } from "../../context"; import * as Error from "../../error"; @@ -17,32 +21,36 @@ export const startFlow: ResolverFn< MutationStartFlowArgs > = async ( _, - { dataSources, flowTrigger, flowDefinition, webId }, + { dataSources, flowTrigger, flowDefinition, flowType, webId }, graphQLContext, ) => { const { temporal, user } = graphQLContext; - if (!user.enabledFeatureFlags.includes("ai")) { - throw Error.forbidden("Flows are not enabled for this user"); + if (flowType === FlowType.Ai && !user.enabledFeatureFlags.includes("ai")) { + throw Error.forbidden("AI flows are not enabled for this user"); } - validateFlowDefinition(flowDefinition); + validateFlowDefinition(flowDefinition, flowType); const workflowId = generateUuid(); + if (flowType === FlowType.Ai && !dataSources) { + throw Error.badRequest("Data sources are required for AI flows"); + } + + const params: RunFlowWorkflowParams = { + ...(flowType === FlowType.Ai ? { dataSources } : {}), + flowTrigger, + flowDefinition, + userAuthentication: { actorId: user.accountId }, + webId, + }; + await temporal.workflow.start< (params: RunFlowWorkflowParams) => Promise >("runFlow", { - taskQueue: "ai", - args: [ - { - dataSources, - flowTrigger, - flowDefinition, - userAuthentication: { actorId: user.accountId }, - webId, - }, - ], + taskQueue: flowType, + args: [params], memo: { flowDefinitionId: flowDefinition.flowDefinitionId, userAccountId: user.accountId, diff --git a/apps/hash-frontend/src/graphql/queries/knowledge/flow.queries.ts b/apps/hash-frontend/src/graphql/queries/knowledge/flow.queries.ts index 18fe5228ea3..1bb9f913190 100644 --- a/apps/hash-frontend/src/graphql/queries/knowledge/flow.queries.ts +++ b/apps/hash-frontend/src/graphql/queries/knowledge/flow.queries.ts @@ -2,15 +2,17 @@ import { gql } from "@apollo/client"; export const startFlowMutation = gql` mutation startFlow( - $dataSources: FlowDataSources! - $flowTrigger: FlowTrigger! + $dataSources: FlowDataSources $flowDefinition: FlowDefinition! + $flowTrigger: FlowTrigger! + $flowType: FlowType! $webId: WebId! ) { startFlow( dataSources: $dataSources - flowTrigger: $flowTrigger flowDefinition: $flowDefinition + flowTrigger: $flowTrigger + flowType: $flowType webId: $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 c1392aeebb5..f3b1b719a34 100644 --- a/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer.tsx +++ b/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer.tsx @@ -3,12 +3,13 @@ import "reactflow/dist/style.css"; import { useApolloClient, useMutation } from "@apollo/client"; import type { EntityId, WebId } from "@blockprotocol/type-system"; import { IconButton, Skeleton } from "@hashintel/design-system"; -import type { OutputNameForAction } from "@local/hash-isomorphic-utils/flows/action-definitions"; +import type { OutputNameForAiFlowAction } from "@local/hash-isomorphic-utils/flows/action-definitions"; import { actionDefinitions } from "@local/hash-isomorphic-utils/flows/action-definitions"; import { manualBrowserInferenceFlowDefinition } from "@local/hash-isomorphic-utils/flows/browser-plugin-flow-definitions"; import { generateWorkerRunPath } from "@local/hash-isomorphic-utils/flows/frontend-paths"; import { goalFlowDefinitionIds } from "@local/hash-isomorphic-utils/flows/goal-flow-definitions"; import type { + FlowActionDefinitionId, FlowDefinition as FlowDefinitionType, FlowInputs, FlowTrigger, @@ -19,9 +20,10 @@ import { useRouter } from "next/router"; import { useCallback, useMemo, useState } from "react"; import { useGetOwnerForEntity } from "../../../../components/hooks/use-get-owner-for-entity"; -import type { - StartFlowMutation, - StartFlowMutationVariables, +import { + FlowType, + type StartFlowMutation, + type StartFlowMutationVariables, } from "../../../../graphql/api-types.gen"; import { startFlowMutation } from "../../../../graphql/queries/knowledge/flow.queries"; import { ArrowRightToLineIcon } from "../../../../shared/icons/arrow-right-to-line-icon"; @@ -56,7 +58,7 @@ import { import { Topbar, topbarHeight } from "./flow-visualizer/topbar"; const getGraphFromFlowDefinition = ( - flowDefinition: FlowDefinitionType, + flowDefinition: FlowDefinitionType, showAllDependencies = false, ) => { /** @@ -220,6 +222,8 @@ export const FlowVisualizer = () => { const [logDisplay, setLogDisplay] = useState("grouped"); + const [startFlowPending, setStartFlowPending] = useState(false); + const apolloClient = useApolloClient(); const { push } = useRouter(); @@ -496,7 +500,7 @@ export const FlowVisualizer = () => { case "EntityId": if ( output.outputName === - ("highlightedEntities" satisfies OutputNameForAction<"researchEntities">) + ("highlightedEntities" satisfies OutputNameForAiFlowAction<"researchEntities">) ) { if (Array.isArray(output.payload.value)) { highlightedEntityIds.push(...output.payload.value); @@ -548,7 +552,13 @@ export const FlowVisualizer = () => { } const { inputs } = selectedFlowRun; - flowInputs = inputs[0]; + flowInputs = { + ...inputs[0], + flowType: + selectedFlowDefinition.type === "ai" + ? FlowType.Ai + : FlowType.Integration, + }; } else { const { webId, outputs } = args; flowInputs = { @@ -563,6 +573,10 @@ export const FlowVisualizer = () => { }, }, flowDefinition: selectedFlowDefinition, + flowType: + selectedFlowDefinition.type === "ai" + ? FlowType.Ai + : FlowType.Integration, flowTrigger: { outputs, triggerDefinitionId: "userTrigger", @@ -571,24 +585,30 @@ export const FlowVisualizer = () => { }; } - const { data } = await startFlow({ - variables: flowInputs, - }); + setStartFlowPending(true); - const flowRunId = data?.startFlow; - if (!flowRunId) { - throw new Error("Failed to start flow"); - } + try { + const { data } = await startFlow({ + variables: flowInputs, + }); - await apolloClient.refetchQueries({ - include: ["getFlowRuns"], - }); + const flowRunId = data?.startFlow; + if (!flowRunId) { + throw new Error("Failed to start flow"); + } - setShowRunModal(false); + await apolloClient.refetchQueries({ + include: ["getFlowRuns"], + }); - const { shortname } = getOwner({ webId: flowInputs.webId }); + setShowRunModal(false); - void push(generateWorkerRunPath({ shortname, flowRunId })); + const { shortname } = getOwner({ webId: flowInputs.webId }); + + void push(generateWorkerRunPath({ shortname, flowRunId })); + } finally { + setStartFlowPending(false); + } }, [ apolloClient, @@ -657,6 +677,7 @@ export const FlowVisualizer = () => { palette.gray[5] }}> 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 728461ad4c0..8ff81c09b28 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,6 +1,9 @@ import { IconButton, XMarkRegularIcon } from "@hashintel/design-system"; import { goalFlowDefinitionIds } from "@local/hash-isomorphic-utils/flows/goal-flow-definitions"; -import type { FlowDefinition } from "@local/hash-isomorphic-utils/flows/types"; +import type { + FlowActionDefinitionId, + FlowDefinition, +} from "@local/hash-isomorphic-utils/flows/types"; import { Backdrop, Box, Slide, Stack } from "@mui/material"; import { DAG } from "./dag"; @@ -14,7 +17,7 @@ type DagSlideProps = { groups: [UngroupedEdgesAndNodes] | GroupWithEdgesAndNodes[]; open: boolean; onClose: () => void; - selectedFlowDefinition: FlowDefinition; + selectedFlowDefinition: FlowDefinition; }; export const DagSlide = ({ @@ -58,6 +61,7 @@ export const DagSlide = ({ null} showRunButton={false} + startFlowPending={false} readonly workerType={isGoal ? "goal" : "flow"} /> diff --git a/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/dag.tsx b/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/dag.tsx index fde826919e2..a8d40237755 100644 --- a/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/dag.tsx +++ b/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/dag.tsx @@ -1,4 +1,7 @@ -import type { FlowDefinition } from "@local/hash-isomorphic-utils/flows/types"; +import type { + FlowActionDefinitionId, + FlowDefinition, +} from "@local/hash-isomorphic-utils/flows/types"; import { Stack, Typography } from "@mui/material"; import { format } from "date-fns"; import { ReactFlowProvider } from "reactflow"; @@ -16,7 +19,7 @@ export const DAG = ({ selectedFlowDefinition, }: { groups: [UngroupedEdgesAndNodes] | GroupWithEdgesAndNodes[]; - selectedFlowDefinition: FlowDefinition; + selectedFlowDefinition: FlowDefinition; }) => { const { selectedFlowRun } = useFlowRunsContext(); 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 c9616ed7b05..a1bf568941d 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 @@ -8,7 +8,10 @@ import { goalFlowDefinitionIds, type GoalFlowTriggerInput, } from "@local/hash-isomorphic-utils/flows/goal-flow-definitions"; -import type { FlowDefinition } from "@local/hash-isomorphic-utils/flows/types"; +import type { + FlowActionDefinitionId, + FlowDefinition, +} from "@local/hash-isomorphic-utils/flows/types"; import { Box, Collapse, Stack, Typography } from "@mui/material"; import type { PropsWithChildren } from "react"; import { useMemo, useState } from "react"; @@ -38,7 +41,7 @@ const SidebarSection = ({ children }: PropsWithChildren) => ( ); type FlowRunSidebarProps = { - flowDefinition: FlowDefinition; + flowDefinition: FlowDefinition; flowRunId: EntityUuid; groups: FlowMaybeGrouped["groups"]; name: FlowRun["name"]; diff --git a/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/outputs/entity-result-table.tsx b/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/outputs/entity-result-table.tsx index c06ab242118..ae78799d8a3 100644 --- a/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/outputs/entity-result-table.tsx +++ b/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/outputs/entity-result-table.tsx @@ -673,16 +673,21 @@ export const EntityResultTable = memo( closedTypesByKey[typeKey] ??= closedMultiEntityType; - const entityLabel = generateEntityLabel(closedMultiEntityType, { - properties: entity.properties, - metadata: { - entityTypeIds, - recordId: { - entityId, - editionId: "irrelevant-here" as EntityEditionId, + let entityLabel: string; + try { + entityLabel = generateEntityLabel(closedMultiEntityType, { + properties: entity.properties, + metadata: { + entityTypeIds, + recordId: { + entityId, + editionId: "irrelevant-here" as EntityEditionId, + }, }, - }, - }); + }); + } catch { + entityLabel = ""; + } entitiesByEntityId[entityId] = { closedMultiEntityType, diff --git a/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/run-flow-modal.tsx b/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/run-flow-modal.tsx index 6bc94d14b10..4709a9f8211 100644 --- a/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/run-flow-modal.tsx +++ b/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/run-flow-modal.tsx @@ -1,12 +1,14 @@ import type { WebId } from "@blockprotocol/type-system"; import { typedValues } from "@local/advanced-types/typed-entries"; import type { + FlowActionDefinitionId, FlowDefinition, FlowTrigger, OutputDefinition, StepOutput, } from "@local/hash-isomorphic-utils/flows/types"; import { Box, Typography } from "@mui/material"; +import { format } from "date-fns"; import type { PropsWithChildren } from "react"; import { useState } from "react"; @@ -53,6 +55,8 @@ const generateInitialFormState = (outputDefinitions: OutputDefinition[]) => defaultValue = false; } else if (outputDefinition.payloadKind === "Entity") { defaultValue = undefined; + } else if (outputDefinition.payloadKind === "Date") { + defaultValue = format(new Date(), "yyyy-MM-dd"); } acc[outputDefinition.name] = { @@ -67,9 +71,9 @@ const generateInitialFormState = (outputDefinitions: OutputDefinition[]) => }, {}); type RunFlowModalProps = { - flowDefinition: FlowDefinition; - open: boolean; + flowDefinition: FlowDefinition; onClose: () => void; + open: boolean; runFlow: (outputs: FlowTrigger["outputs"], webId: WebId) => Promise; }; diff --git a/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/run-flow-modal/manual-trigger-input.tsx b/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/run-flow-modal/manual-trigger-input.tsx index b5820d74b27..1622f802bf2 100644 --- a/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/run-flow-modal/manual-trigger-input.tsx +++ b/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/run-flow-modal/manual-trigger-input.tsx @@ -76,6 +76,18 @@ export const ManualTriggerInput = ({ onChange={(event) => setValue(event.target.checked)} /> ); + case "Date": + if (array || Array.isArray(payload.value)) { + throw new Error("Selecting multiple dates is not supported"); + } + return ( + setValue(event.target.value)} + sx={textFieldSx} + type="date" + value={payload.value} + /> + ); case "VersionedUrl": { return ( , { ActorType: ActorTypeDataType; + Date: string; Entity: HashEntity; FormattedText: FormattedText; GoogleAccountId: string; diff --git a/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/shared/types.ts b/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/shared/types.ts index e52b392d617..f17ba3f8b80 100644 --- a/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/shared/types.ts +++ b/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/shared/types.ts @@ -2,6 +2,7 @@ import type { ActionDefinition, ActionStepDefinition, ActionStepWithParallelInput, + FlowActionDefinitionId, ParallelGroupStepDefinition, ProgressLogBase, ProposedEntity, @@ -16,7 +17,7 @@ import type { SimpleStatus } from "../../../../../shared/flow-runs-context"; export type NodeData = { kind: StepDefinition["kind"]; groupId?: number; - actionDefinition?: ActionDefinition | null; + actionDefinition?: ActionDefinition | null; label: string; inputSources: | ActionStepDefinition["inputSources"] 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 7d07c5f4728..4a395413e21 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 @@ -1,6 +1,8 @@ import { ArrowRightRegularIcon } from "@hashintel/design-system"; -import type { ActionDefinitionId } from "@local/hash-isomorphic-utils/flows/action-definitions"; -import type { ExternalInputRequest } from "@local/hash-isomorphic-utils/flows/types"; +import type { + ExternalInputRequest, + FlowActionDefinitionId, +} from "@local/hash-isomorphic-utils/flows/types"; import type { SxProps, Theme } from "@mui/material"; import { Box, Stack, Typography } from "@mui/material"; import { formatDistance } from "date-fns"; @@ -33,7 +35,7 @@ const useStatusText = ({ simpleStatusName, statusData, }: { - actionDefinitionId?: ActionDefinitionId; + actionDefinitionId?: FlowActionDefinitionId; simpleStatusName: SimpleStatus; statusData?: StepRunStatus | null; }) => { diff --git a/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/topbar.tsx b/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/topbar.tsx index ef52f27f8fe..a3300c4672b 100644 --- a/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/topbar.tsx +++ b/apps/hash-frontend/src/pages/@/[shortname]/shared/flow-visualizer/topbar.tsx @@ -152,17 +152,18 @@ export const Topbar = ({ handleRunFlowClicked, readonly, showRunButton, + startFlowPending, workerType, }: { handleRunFlowClicked: () => void; readonly?: boolean; showRunButton: boolean; + startFlowPending: boolean; workerType: "goal" | "flow"; }) => { const { push } = useRouter(); const [cancelling, setCancelling] = useState(false); - const [waitingToRun, setWaitingToRun] = useState(false); const { flowDefinitions, selectedFlowDefinitionId } = useFlowDefinitionsContext(); @@ -190,15 +191,7 @@ export const Topbar = ({ }, [cancelFlow, selectedFlowRunId]); const onRunFlowClicked = useCallback(() => { - setWaitingToRun(true); - try { - handleRunFlowClicked(); - } catch { - /** - * We don't need to worry about the success case because the user will be sent to the new flow run's page - */ - setWaitingToRun(false); - } + handleRunFlowClicked(); }, [handleRunFlowClicked]); const getOwner = useGetOwnerForEntity(); @@ -356,7 +349,7 @@ export const Topbar = ({ background="blue" Icon={PlaySolidIcon} onClick={onRunFlowClicked} - pending={waitingToRun} + pending={startFlowPending} text={selectedFlowRun ? "Re-run" : "Run"} /> ) : null} diff --git a/apps/hash-frontend/src/pages/goals/new.page.tsx b/apps/hash-frontend/src/pages/goals/new.page.tsx index 36cb9df0360..71c9d3f9590 100644 --- a/apps/hash-frontend/src/pages/goals/new.page.tsx +++ b/apps/hash-frontend/src/pages/goals/new.page.tsx @@ -5,6 +5,7 @@ import { BullseyeLightIcon, TextField, } from "@hashintel/design-system"; +import type { AiFlowActionDefinitionId } from "@local/hash-isomorphic-utils/flows/action-definitions"; import { generateWorkerRunPath } from "@local/hash-isomorphic-utils/flows/frontend-paths"; import { goalFlowDefinition, @@ -38,9 +39,10 @@ import type { FormEvent, FunctionComponent, PropsWithChildren } from "react"; import { useMemo, useState } from "react"; import { useGetOwnerForEntity } from "../../components/hooks/use-get-owner-for-entity"; -import type { - StartFlowMutation, - StartFlowMutationVariables, +import { + FlowType, + type StartFlowMutation, + type StartFlowMutationVariables, } from "../../graphql/api-types.gen"; import { startFlowMutation } from "../../graphql/queries/knowledge/flow.queries"; import { FilesRegularIcon } from "../../shared/icons/files-regular-icon"; @@ -265,7 +267,8 @@ const NewGoalPageContent = () => { }, ]; - let flowDefinition: FlowDefinition = goalFlowDefinition; + let flowDefinition: FlowDefinition = + goalFlowDefinition; if (deliverablesSettings.document && deliverablesSettings.spreadsheet) { if ( !deliverablesSettings.document.brief || @@ -347,6 +350,7 @@ const NewGoalPageContent = () => { internetAccess: internetSettings, }, flowDefinition, + flowType: FlowType.Ai, flowTrigger: { outputs: triggerOutputs, triggerDefinitionId: "userTrigger", diff --git a/apps/hash-frontend/src/pages/shared/entity/entity-editor/history-section.tsx b/apps/hash-frontend/src/pages/shared/entity/entity-editor/history-section.tsx index 93a5c340731..bd493d7ef4f 100644 --- a/apps/hash-frontend/src/pages/shared/entity/entity-editor/history-section.tsx +++ b/apps/hash-frontend/src/pages/shared/entity/entity-editor/history-section.tsx @@ -10,6 +10,7 @@ import { } from "@local/hash-isomorphic-utils/graph-queries"; import { useMemo } from "react"; +import { useUserOrOrgShortnameByWebId } from "../../../../components/hooks/use-user-or-org-shortname-by-owned-by-id"; import type { GetEntityDiffsQuery, GetEntityDiffsQueryVariables, @@ -28,6 +29,10 @@ import type { HistoryEvent } from "./history-section/shared/types"; export const HistorySection = ({ entityId }: { entityId: EntityId }) => { const [webId, entityUuid, _draftUuid] = splitEntityId(entityId); + const { shortname } = useUserOrOrgShortnameByWebId({ + webId, + }); + const { data: editionsData, loading: editionsLoading } = useQuery< QueryEntitySubgraphQuery, QueryEntitySubgraphQueryVariables @@ -145,7 +150,11 @@ export const HistorySection = ({ entityId }: { entityId: EntityId }) => { {loading || !subgraph ? ( ) : ( - + )} diff --git a/apps/hash-frontend/src/pages/shared/entity/entity-editor/history-section/history-table.tsx b/apps/hash-frontend/src/pages/shared/entity/entity-editor/history-section/history-table.tsx index 5ff86a3b02e..a15756df2c6 100644 --- a/apps/hash-frontend/src/pages/shared/entity/entity-editor/history-section/history-table.tsx +++ b/apps/hash-frontend/src/pages/shared/entity/entity-editor/history-section/history-table.tsx @@ -94,6 +94,7 @@ const TableRow = memo( isLastRow, numberColumnWidth, scrollContainerRef, + shortname, subgraph, }: HistoryRowData) => { const { number, timestamp } = event; @@ -240,7 +241,11 @@ const TableRow = memo( - + @@ -254,6 +259,7 @@ type HistoryRowData = { isLastRow: boolean; numberColumnWidth: number; scrollContainerRef: React.RefObject; + shortname: string; subgraph: Subgraph; }; @@ -267,15 +273,18 @@ const createRowContent: CreateVirtualizedRowContentFn = ( isFirstRow={row.data.isFirstRow} isLastRow={row.data.isLastRow} scrollContainerRef={row.data.scrollContainerRef} + shortname={row.data.shortname} subgraph={row.data.subgraph} /> ); export const HistoryTable = ({ events, + shortname, subgraph, }: { events: HistoryEvent[]; + shortname: string; subgraph: Subgraph; }) => { const [sort, setSort] = useState>({ @@ -314,10 +323,11 @@ export const HistoryTable = ({ isLastRow: index === events.length - 1, numberColumnWidth, scrollContainerRef, + shortname, subgraph, }, })); - }, [events, sort, subgraph, scrollContainerRef]); + }, [events, sort, subgraph, scrollContainerRef, shortname]); const columns = useMemo(() => createColumns(rows.length), [rows]); diff --git a/apps/hash-frontend/src/pages/shared/entity/entity-editor/history-section/history-table/provenance.tsx b/apps/hash-frontend/src/pages/shared/entity/entity-editor/history-section/history-table/provenance.tsx index 5b9dcef3e19..645ba6dbe47 100644 --- a/apps/hash-frontend/src/pages/shared/entity/entity-editor/history-section/history-table/provenance.tsx +++ b/apps/hash-frontend/src/pages/shared/entity/entity-editor/history-section/history-table/provenance.tsx @@ -11,7 +11,6 @@ import { import { generateWorkerRunPath } from "@local/hash-isomorphic-utils/flows/frontend-paths"; import type { SvgIconProps, SxProps, Theme } from "@mui/material"; import { Box, Stack, Typography } from "@mui/material"; -import { useRouter } from "next/router"; import type { FunctionComponent, PropsWithChildren } from "react"; import { useState } from "react"; @@ -58,9 +57,11 @@ const ProvenanceRow = ({ children }: PropsWithChildren) => ( export const Provenance = ({ event, + shortname, subgraph, }: { event: HistoryEvent; + shortname: string; subgraph: Subgraph; }) => { const { @@ -73,9 +74,6 @@ export const Provenance = ({ const [showSourcesSlideover, setShowSourcesSlideover] = useState(false); - const { query } = useRouter(); - const shortname = query.shortname as string; - if (loading) { return ( diff --git a/apps/hash-frontend/src/pages/shared/flow-definitions-context.tsx b/apps/hash-frontend/src/pages/shared/flow-definitions-context.tsx index 37b942cad58..563f25b5179 100644 --- a/apps/hash-frontend/src/pages/shared/flow-definitions-context.tsx +++ b/apps/hash-frontend/src/pages/shared/flow-definitions-context.tsx @@ -18,19 +18,23 @@ import { goalFlowDefinitionWithReportDeliverable, goalFlowDefinitionWithSpreadsheetDeliverable, } from "@local/hash-isomorphic-utils/flows/goal-flow-definitions"; -import type { FlowDefinition } from "@local/hash-isomorphic-utils/flows/types"; +import { scheduledFlightsFlowDefinition } from "@local/hash-isomorphic-utils/flows/integration-flow-definitions"; +import type { + FlowActionDefinitionId, + FlowDefinition, +} from "@local/hash-isomorphic-utils/flows/types"; import type { PropsWithChildren } from "react"; import { createContext, useContext, useMemo, useState } from "react"; export type FlowDefinitionsContextType = { - flowDefinitions: FlowDefinition[]; + flowDefinitions: FlowDefinition[]; selectedFlowDefinitionId: EntityUuid | null; }; export const FlowDefinitionsContext = createContext(null); -const exampleFlows: FlowDefinition[] = [ +const exampleFlows: FlowDefinition[] = [ researchTaskFlowDefinition, researchEntitiesFlowDefinition, ftseInvestorsFlowDefinition, @@ -44,6 +48,7 @@ const exampleFlows: FlowDefinition[] = [ goalFlowDefinitionWithReportDeliverable, goalFlowDefinitionWithSpreadsheetDeliverable, goalFlowDefinitionWithReportAndSpreadsheetDeliverable, + scheduledFlightsFlowDefinition, ]; export const FlowDefinitionsContextProvider = ({ @@ -51,7 +56,7 @@ export const FlowDefinitionsContextProvider = ({ selectedFlowDefinitionId, }: PropsWithChildren<{ selectedFlowDefinitionId: EntityUuid | null }>) => { const [flowDefinitions, setFlowDefinitions] = - useState(exampleFlows); + useState[]>(exampleFlows); const context = useMemo( () => ({ diff --git a/apps/hash-frontend/src/pages/shared/sources-popover.tsx b/apps/hash-frontend/src/pages/shared/sources-popover.tsx index 0c4d9a46cfa..1cf71874c2b 100644 --- a/apps/hash-frontend/src/pages/shared/sources-popover.tsx +++ b/apps/hash-frontend/src/pages/shared/sources-popover.tsx @@ -39,7 +39,7 @@ export const SourcesList = ({ sources }: { sources: SourceProvenance[] }) => { p: 1.5, })} > - {sourceUrl ? ( + {source.type !== "integration" && sourceUrl ? ( = ({ + graphApiClient, +}: { + graphApiClient: GraphApi; +}) => ({ + ...createAviationActivities(), + ...createIntegrationActivities({ graphApiClient }), +}); diff --git a/apps/hash-integration-worker/src/activities/flow-activities/aviation-activities.ts b/apps/hash-integration-worker/src/activities/flow-activities/aviation-activities.ts new file mode 100644 index 00000000000..300c023e466 --- /dev/null +++ b/apps/hash-integration-worker/src/activities/flow-activities/aviation-activities.ts @@ -0,0 +1,14 @@ +import { getScheduledFlightsAction } from "./aviation-activities/get-scheduled-flights-action.js"; + +export { getScheduledFlightsAction } from "./aviation-activities/get-scheduled-flights-action.js"; +export { createPersistIntegrationEntitiesAction as createPersistFlightEntitiesAction } from "./integration-activities/persist-integration-entities-action.js"; + +/** + * Creates the aviation flow action activities. + */ +export const createAviationActivities = () => ({ + /** + * Fetches scheduled flights from AeroAPI and returns them as ProposedEntity objects. + */ + getScheduledFlightsAction, +}); diff --git a/apps/hash-integration-worker/src/activities/flow-activities/aviation-activities/get-scheduled-flights-action.ts b/apps/hash-integration-worker/src/activities/flow-activities/aviation-activities/get-scheduled-flights-action.ts new file mode 100644 index 00000000000..9eeebd9933a --- /dev/null +++ b/apps/hash-integration-worker/src/activities/flow-activities/aviation-activities/get-scheduled-flights-action.ts @@ -0,0 +1,133 @@ +import { + type EntityId, + extractBaseUrl, + type OriginProvenance, + type ProvidedEntityEditionProvenance, +} from "@blockprotocol/type-system"; +import type { FlowActionActivity } from "@local/hash-backend-utils/flows"; +import type { AviationProposedEntity } from "@local/hash-backend-utils/integrations/aviation"; +import { getScheduledArrivalEntities } from "@local/hash-backend-utils/integrations/aviation"; +import { getSimplifiedIntegrationFlowActionInputs } from "@local/hash-isomorphic-utils/flows/action-definitions"; +import type { ProposedEntity } from "@local/hash-isomorphic-utils/flows/types"; +import { systemEntityTypes } from "@local/hash-isomorphic-utils/ontology-type-ids"; +import { StatusCode } from "@local/status"; + +import { splitPropertiesAndMetadata } from "../shared/split-properties-and-metadata.js"; + +/** + * Converts an {@link AviationProposedEntity} to a flow {@link ProposedEntity} format. + * + * This handles splitting propertiesWithMetadata into properties and propertyMetadata, + * and converts link entities by adding sourceEntityId and targetEntityId. + * + * @todo we should just pass the propertiesWithMetadata around everywhere, but the existing PersistedEntity format has them split + * – they then get merged again anyway to persist to the Graph. Worth refactoring at some point. + */ +export const aviationProposedEntityToFlowProposedEntity = ( + entity: AviationProposedEntity, + provenance: ProvidedEntityEditionProvenance, +): ProposedEntity => { + const { properties, propertyMetadata } = splitPropertiesAndMetadata( + entity.properties, + ); + + const flowEntity: ProposedEntity = { + claims: { + isSubjectOf: [], + isObjectOf: [], + }, + sourceEntityId: entity.sourceEntityLocalId + ? { + kind: "proposed-entity", + localId: entity.sourceEntityLocalId as EntityId, + } + : undefined, + targetEntityId: entity.targetEntityLocalId + ? { + kind: "proposed-entity", + localId: entity.targetEntityLocalId as EntityId, + } + : undefined, + provenance, + propertyMetadata, + localEntityId: entity.localEntityId as EntityId, + entityTypeIds: entity.entityTypeIds, + properties, + }; + + return flowEntity; +}; + +/** + * Fetches scheduled flights from AeroAPI for a given airport and date and returns them as ProposedEntity objects. + */ +export const getScheduledFlightsAction: FlowActionActivity = async ({ + inputs, +}) => { + try { + const { airportIcao, date } = getSimplifiedIntegrationFlowActionInputs({ + inputs, + actionType: "getScheduledFlights", + }); + + const { entities, provenance } = await getScheduledArrivalEntities( + airportIcao, + date, + ); + + const fullProvenance = { + ...provenance, + actorType: "machine" as const, + origin: { + type: "flow", + id: "aviation-integration", + } satisfies OriginProvenance, + }; + + const proposedEntities: ProposedEntity[] = []; + + let flightCount = 0; + for (const entity of entities.values()) { + if ( + entity.entityTypeIds.some( + (entityTypeId) => + extractBaseUrl(entityTypeId) === + systemEntityTypes.flight.entityTypeBaseUrl, + ) + ) { + flightCount++; + } + + proposedEntities.push( + aviationProposedEntityToFlowProposedEntity(entity, fullProvenance), + ); + } + + return { + code: StatusCode.Ok, + message: `Generated ${flightCount} flights and ${entities.size - flightCount} related entities for ${airportIcao} on ${date}`, + contents: [ + { + outputs: [ + { + outputName: "proposedEntities", + payload: { + kind: "ProposedEntity", + value: proposedEntities, + }, + }, + ], + }, + ], + }; + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : "Unknown error occurred"; + + return { + code: StatusCode.Internal, + message: `Failed to fetch scheduled flights: ${errorMessage}`, + contents: [], + }; + } +}; diff --git a/apps/hash-integration-worker/src/activities/flow-activities/integration-activities.ts b/apps/hash-integration-worker/src/activities/flow-activities/integration-activities.ts new file mode 100644 index 00000000000..6f625aad949 --- /dev/null +++ b/apps/hash-integration-worker/src/activities/flow-activities/integration-activities.ts @@ -0,0 +1,13 @@ +import type { GraphApi } from "@local/hash-graph-client"; + +import { createPersistIntegrationEntitiesAction } from "./integration-activities/persist-integration-entities-action.js"; + +export const createIntegrationActivities = ({ + graphApiClient, +}: { + graphApiClient: GraphApi; +}) => ({ + persistIntegrationEntitiesAction: createPersistIntegrationEntitiesAction({ + graphApiClient, + }), +}); diff --git a/apps/hash-integration-worker/src/activities/flow-activities/integration-activities/persist-integration-entities-action.ts b/apps/hash-integration-worker/src/activities/flow-activities/integration-activities/persist-integration-entities-action.ts new file mode 100644 index 00000000000..65d2e775cab --- /dev/null +++ b/apps/hash-integration-worker/src/activities/flow-activities/integration-activities/persist-integration-entities-action.ts @@ -0,0 +1,498 @@ +import { + type ActorEntityUuid, + type EntityId, + extractBaseUrl, + type OriginProvenance, + type ProvidedEntityEditionProvenance, + type VersionedUrl, + type WebId, +} from "@blockprotocol/type-system"; +import type { FlowActionActivity } from "@local/hash-backend-utils/flows"; +import { + generateEntityMatcher, + generateLinkMatcher, +} from "@local/hash-backend-utils/integrations/aviation"; +import type { GraphApi } from "@local/hash-graph-client"; +import { + HashEntity, + HashLinkEntity, + mergePropertyObjectAndMetadata, + patchesFromPropertyObjects, + queryEntities, +} from "@local/hash-graph-sdk/entity"; +import { getSimplifiedIntegrationFlowActionInputs } from "@local/hash-isomorphic-utils/flows/action-definitions"; +import type { + FailedEntityProposal, + PersistedEntities, + PersistedEntity, + ProposedEntity, +} from "@local/hash-isomorphic-utils/flows/types"; +import { + currentTimeInstantTemporalAxes, + generateVersionedUrlMatchingFilter, +} from "@local/hash-isomorphic-utils/graph-queries"; +import { StatusCode } from "@local/status"; + +import { getFlowContext } from "../shared/get-integration-flow-context.js"; + +const findExistingEntity = async (params: { + authentication: { actorId: ActorEntityUuid }; + graphApiClient: GraphApi; + proposedEntity: ProposedEntity; + webId: WebId; +}): Promise => { + const { graphApiClient, authentication, proposedEntity, webId } = params; + + const [entityTypeId] = proposedEntity.entityTypeIds; + const entityTypeBaseUrl = extractBaseUrl(entityTypeId); + + const entityMatcher = generateEntityMatcher[entityTypeBaseUrl]; + + if (!entityMatcher) { + // No matcher defined for this entity type, skip matching + return null; + } + + const propertyFilter = entityMatcher(proposedEntity); + + const { entities } = await queryEntities( + { graphApi: graphApiClient }, + authentication, + { + filter: { + all: [ + generateVersionedUrlMatchingFilter(entityTypeId, { + ignoreParents: true, + }), + { equal: [{ path: ["webId"] }, { parameter: webId }] }, + { equal: [{ path: ["archived"] }, { parameter: false }] }, + propertyFilter, + ], + }, + temporalAxes: currentTimeInstantTemporalAxes, + includeDrafts: false, + includePermissions: false, + }, + ); + + const [entity] = entities; + return entity ?? null; +}; + +const findExistingLink = async (params: { + authentication: { actorId: ActorEntityUuid }; + graphApiClient: GraphApi; + leftEntityId: EntityId; + linkEntityTypeId: VersionedUrl; + rightEntityId: EntityId; + webId: WebId; +}): Promise => { + const { + graphApiClient, + authentication, + linkEntityTypeId, + leftEntityId, + rightEntityId, + webId, + } = params; + + const linkTypeBaseUrl = extractBaseUrl(linkEntityTypeId); + + const linkMatcher = generateLinkMatcher[linkTypeBaseUrl]; + + if (!linkMatcher) { + // No matcher defined for this link type, skip matching + return null; + } + + const linkFilter = linkMatcher({ leftEntityId, rightEntityId }); + + const { entities } = await queryEntities( + { graphApi: graphApiClient }, + authentication, + { + filter: { + all: [ + generateVersionedUrlMatchingFilter(linkEntityTypeId, { + ignoreParents: true, + }), + { equal: [{ path: ["archived"] }, { parameter: false }] }, + { equal: [{ path: ["webId"] }, { parameter: webId }] }, + linkFilter, + ], + }, + temporalAxes: currentTimeInstantTemporalAxes, + includeDrafts: false, + includePermissions: false, + }, + ); + + const [entity] = entities; + return entity ? new HashLinkEntity(entity) : null; +}; + +/** + * Persists proposed entities to the graph, creating new entities as needed. + * Returns the mapping of local entity IDs to persisted entity IDs. + */ +const persistEntities = async (params: { + authentication: { actorId: ActorEntityUuid }; + graphApiClient: GraphApi; + proposedEntities: ProposedEntity[]; + provenance: ProvidedEntityEditionProvenance; + webId: WebId; +}): Promise<{ + persistedEntities: PersistedEntity[]; + failedEntityProposals: FailedEntityProposal[]; + entityIdsByLocalId: Map; +}> => { + const { + authentication, + graphApiClient, + proposedEntities, + provenance, + webId, + } = params; + + const persistedEntities: PersistedEntity[] = []; + const failedEntityProposals: FailedEntityProposal[] = []; + const entityIdsByLocalId = new Map(); + + const nonLinkEntities = proposedEntities.filter( + (entity) => !entity.sourceEntityId && !entity.targetEntityId, + ); + + for (const proposedEntity of nonLinkEntities) { + try { + const existingEntity = await findExistingEntity({ + graphApiClient, + authentication, + proposedEntity, + webId, + }); + + if (existingEntity) { + const newProperties = mergePropertyObjectAndMetadata( + proposedEntity.properties, + proposedEntity.propertyMetadata, + ); + + const propertyPatches = patchesFromPropertyObjects({ + oldProperties: existingEntity.properties, + newProperties, + }); + + const updatedEntity = + propertyPatches.length > 0 + ? await existingEntity.patch(graphApiClient, authentication, { + propertyPatches, + provenance: { + ...provenance, + sources: proposedEntity.provenance.sources, + }, + }) + : existingEntity; + + entityIdsByLocalId.set( + proposedEntity.localEntityId, + updatedEntity.metadata.recordId.entityId, + ); + persistedEntities.push({ + entity: updatedEntity.toJSON(), + existingEntity: existingEntity.toJSON(), + operation: + propertyPatches.length > 0 + ? "update" + : "already-exists-as-proposed", + }); + } else { + const newEntity = await HashEntity.create( + graphApiClient, + authentication, + { + webId, + draft: false, + properties: mergePropertyObjectAndMetadata( + proposedEntity.properties, + proposedEntity.propertyMetadata, + ), + provenance: { + ...provenance, + sources: proposedEntity.provenance.sources, + }, + entityTypeIds: proposedEntity.entityTypeIds, + }, + ); + + entityIdsByLocalId.set( + proposedEntity.localEntityId, + newEntity.metadata.recordId.entityId, + ); + + persistedEntities.push({ + entity: newEntity.toJSON(), + operation: "create", + }); + } + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; + failedEntityProposals.push({ + proposedEntity, + message: `Failed to persist entity: ${errorMessage}`, + }); + } + } + + return { persistedEntities, failedEntityProposals, entityIdsByLocalId }; +}; + +/** + * Persists proposed links to the graph, creating new links where they don't exist. + */ +const persistLinks = async (params: { + authentication: { actorId: ActorEntityUuid }; + entityIdsByLocalId: Map; + graphApiClient: GraphApi; + proposedEntities: ProposedEntity[]; + provenance: ProvidedEntityEditionProvenance; + webId: WebId; +}): Promise<{ + persistedEntities: PersistedEntity[]; + failedEntityProposals: FailedEntityProposal[]; +}> => { + const { + authentication, + entityIdsByLocalId, + graphApiClient, + proposedEntities, + provenance, + webId, + } = params; + + const persistedEntities: PersistedEntity[] = []; + const failedEntityProposals: FailedEntityProposal[] = []; + + const linkEntities = proposedEntities.filter( + (entity) => entity.sourceEntityId && entity.targetEntityId, + ); + + for (const proposedLink of linkEntities) { + const { sourceEntityId, targetEntityId } = proposedLink; + + if (!sourceEntityId || !targetEntityId) { + failedEntityProposals.push({ + proposedEntity: proposedLink, + message: "Link entity missing sourceEntityId or targetEntityId", + }); + continue; + } + + const leftEntityId = + sourceEntityId.kind === "proposed-entity" + ? entityIdsByLocalId.get(sourceEntityId.localId) + : sourceEntityId.entityId; + + const rightEntityId = + targetEntityId.kind === "proposed-entity" + ? entityIdsByLocalId.get(targetEntityId.localId) + : targetEntityId.entityId; + + if (!leftEntityId || !rightEntityId) { + failedEntityProposals.push({ + proposedEntity: proposedLink, + message: `Could not resolve entity IDs for link: left=${leftEntityId}, right=${rightEntityId}`, + }); + continue; + } + + const [linkEntityTypeId] = proposedLink.entityTypeIds; + + try { + const existingLink = await findExistingLink({ + graphApiClient, + authentication, + linkEntityTypeId, + leftEntityId, + rightEntityId, + webId, + }); + + if (existingLink) { + const newProperties = mergePropertyObjectAndMetadata( + proposedLink.properties, + proposedLink.propertyMetadata, + ); + + const propertyPatches = patchesFromPropertyObjects({ + oldProperties: existingLink.properties, + newProperties, + }); + + const updatedLink = + propertyPatches.length > 0 + ? await existingLink.patch(graphApiClient, authentication, { + propertyPatches, + provenance: { + ...provenance, + sources: proposedLink.provenance.sources, + }, + }) + : existingLink; + + persistedEntities.push({ + entity: updatedLink.toJSON(), + existingEntity: existingLink.toJSON(), + operation: + propertyPatches.length > 0 + ? "update" + : "already-exists-as-proposed", + }); + } else { + const newLink = await HashLinkEntity.create( + graphApiClient, + authentication, + { + webId, + draft: false, + linkData: { + leftEntityId, + rightEntityId, + }, + properties: mergePropertyObjectAndMetadata( + proposedLink.properties, + proposedLink.propertyMetadata, + ), + provenance: { + ...provenance, + sources: proposedLink.provenance.sources, + }, + entityTypeIds: proposedLink.entityTypeIds, + }, + ); + + persistedEntities.push({ + entity: newLink.toJSON(), + operation: "create", + }); + } + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; + failedEntityProposals.push({ + proposedEntity: proposedLink, + message: `Failed to persist link: ${errorMessage}`, + }); + } + } + + return { persistedEntities, failedEntityProposals }; +}; + +/** + * Creates the persist integration entities action that can be bound to a GraphApi client. + */ +export const createPersistIntegrationEntitiesAction = ({ + graphApiClient, +}: { + graphApiClient: GraphApi; +}): FlowActionActivity => { + return async ({ inputs }) => { + try { + const { flowEntityId, stepId, userAuthentication, webId } = + await getFlowContext(); + + const { proposedEntities } = getSimplifiedIntegrationFlowActionInputs({ + inputs, + actionType: "persistIntegrationEntities", + }); + + const provenance: ProvidedEntityEditionProvenance = { + actorType: "machine", + origin: { + type: "flow", + id: flowEntityId, + stepIds: [stepId], + } satisfies OriginProvenance, + }; + const { + persistedEntities: persistedNonLinkEntities, + failedEntityProposals: failedNonLinkProposals, + entityIdsByLocalId, + } = await persistEntities({ + authentication: userAuthentication, + graphApiClient, + proposedEntities, + provenance, + webId, + }); + + const { + persistedEntities: persistedLinkEntities, + failedEntityProposals: failedLinkProposals, + } = await persistLinks({ + authentication: userAuthentication, + entityIdsByLocalId, + graphApiClient, + proposedEntities, + provenance, + webId, + }); + + const allPersistedEntities = [ + ...persistedNonLinkEntities, + ...persistedLinkEntities, + ]; + const allFailedProposals = [ + ...failedNonLinkProposals, + ...failedLinkProposals, + ]; + + const result: PersistedEntities = { + persistedEntities: allPersistedEntities, + failedEntityProposals: allFailedProposals, + }; + + const code = + allPersistedEntities.length > 0 + ? StatusCode.Ok + : proposedEntities.length > 0 + ? StatusCode.Internal + : StatusCode.Ok; + + const message = + allPersistedEntities.length > 0 + ? `Persisted ${allPersistedEntities.length} entities${allFailedProposals.length > 0 ? `, ${allFailedProposals.length} failed` : ""}` + : proposedEntities.length > 0 + ? `Failed to persist ${allFailedProposals.length} entities` + : "No entities to persist"; + + return { + code, + message, + contents: [ + { + outputs: [ + { + outputName: "persistedEntities", + payload: { + kind: "PersistedEntities", + value: result, + }, + }, + ], + }, + ], + }; + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : "Unknown error occurred"; + + return { + code: StatusCode.Internal, + message: `Failed to persist entities: ${errorMessage}`, + contents: [], + }; + } + }; +}; diff --git a/apps/hash-integration-worker/src/activities/flow-activities/shared/get-integration-flow-context.ts b/apps/hash-integration-worker/src/activities/flow-activities/shared/get-integration-flow-context.ts new file mode 100644 index 00000000000..57d3ed5e0d7 --- /dev/null +++ b/apps/hash-integration-worker/src/activities/flow-activities/shared/get-integration-flow-context.ts @@ -0,0 +1,150 @@ +import type { + EntityId, + EntityUuid, + UserId, + WebId, +} from "@blockprotocol/type-system"; +import { entityIdFromComponents } from "@blockprotocol/type-system"; +import { createTemporalClient } from "@local/hash-backend-utils/temporal"; +import { parseHistoryItemPayload } from "@local/hash-backend-utils/temporal/parse-history-item-payload"; +import type { + BaseRunFlowWorkflowParams, + RunAiFlowWorkflowParams, +} from "@local/hash-isomorphic-utils/flows/temporal-types"; +import { Context } from "@temporalio/activity"; +import type { Client as TemporalClient } from "@temporalio/client"; +import type { MemoryCache } from "cache-manager"; +import { caching } from "cache-manager"; + +let _temporalClient: TemporalClient | undefined; + +let _runFlowWorkflowParamsCache: MemoryCache | undefined; + +type PartialRunFlowWorkflowParams = Pick< + BaseRunFlowWorkflowParams, + "webId" | "userAuthentication" +>; + +const getCache = async () => { + _runFlowWorkflowParamsCache = + _runFlowWorkflowParamsCache ?? + (await caching("memory", { + max: 100, // 100 items + ttl: 10 * 60 * 1000, // 10 minutes + })); + return _runFlowWorkflowParamsCache; +}; + +export const getTemporalClient = async () => { + _temporalClient = _temporalClient ?? (await createTemporalClient()); + return _temporalClient; +}; + +const getPartialRunFlowWorkflowParams = async (params: { + workflowId: string; +}): Promise => { + const { workflowId } = params; + + const runFlowWorkflowParamsCache = await getCache(); + + const cachedPartialRunFlowWorkflowParams = + await runFlowWorkflowParamsCache.get( + workflowId, + ); + + if (cachedPartialRunFlowWorkflowParams) { + return cachedPartialRunFlowWorkflowParams; + } + + const temporalClient = await getTemporalClient(); + const handle = temporalClient.workflow.getHandle(workflowId); + + const { events } = await handle.fetchHistory(); + + if (!events) { + throw new Error(`No events found for workflowId ${workflowId}`); + } + + const workflowExecutionStartedEventAttributes = + events.find((event) => event.workflowExecutionStartedEventAttributes) + ?.workflowExecutionStartedEventAttributes ?? undefined; + + if (!workflowExecutionStartedEventAttributes) { + throw new Error( + `No workflow execution started event attributes found for workflowId ${workflowId}`, + ); + } + + const inputs = parseHistoryItemPayload( + workflowExecutionStartedEventAttributes.input, + ); + + if (!inputs) { + throw new Error( + `No inputs found for workflowId ${workflowId} in the workflow execution started event`, + ); + } + + const [runFlowWorkflowParams] = inputs as RunAiFlowWorkflowParams[]; + + if (!runFlowWorkflowParams) { + throw new Error( + `No parameters of the "runFlow" workflow found for workflowId ${workflowId}`, + ); + } + + /** + * Avoid caching the entire `RunFlowWorkflowParams` object to reduce memory usage + * of the cache. + */ + const partialRunFlowWorkflowParams: PartialRunFlowWorkflowParams = { + userAuthentication: runFlowWorkflowParams.userAuthentication, + webId: runFlowWorkflowParams.webId, + }; + + await runFlowWorkflowParamsCache.set( + workflowId, + partialRunFlowWorkflowParams, + ); + + return partialRunFlowWorkflowParams; +}; + +type FlowContext = { + flowEntityId: EntityId; + stepId: string; + userAuthentication: { actorId: UserId }; + webId: WebId; +}; + +/** + * Get the context of the flow that is currently being executed + * from a temporal activity. + * + * This method must be called from a temporal activity that is + * called within the `runFlow` temporal workflow. + */ +export const getFlowContext = async (): Promise => { + const activityContext = Context.current(); + + const { workflowId } = activityContext.info.workflowExecution; + + const { userAuthentication, webId } = await getPartialRunFlowWorkflowParams({ + workflowId, + }); + + const flowEntityId = entityIdFromComponents( + webId, + // Assumes the flow entity UUID is the same as the workflow ID + workflowId as EntityUuid, + ); + + const { activityId: stepId } = Context.current().info; + + return { + userAuthentication, + flowEntityId, + webId, + stepId, + }; +}; diff --git a/apps/hash-integration-worker/src/activities/flow-activities/shared/split-properties-and-metadata.ts b/apps/hash-integration-worker/src/activities/flow-activities/shared/split-properties-and-metadata.ts new file mode 100644 index 00000000000..4b8df634841 --- /dev/null +++ b/apps/hash-integration-worker/src/activities/flow-activities/shared/split-properties-and-metadata.ts @@ -0,0 +1,171 @@ +import type { + ArrayMetadata, + BaseUrl, + ObjectMetadata, + PropertyArrayMetadata, + PropertyMetadata, + PropertyObject, + PropertyObjectMetadata, + PropertyValue, + PropertyValueMetadata, + PropertyWithMetadata, +} from "@blockprotocol/type-system"; +import { isBaseUrl } from "@blockprotocol/type-system"; + +/** + * Recursively extracts the raw property value from a PropertyWithMetadata. + */ +const extractPropertyValue = ( + propertyWithMetadata: PropertyWithMetadata, +): PropertyValue => { + // Check if it's a value with metadata (has metadata.dataTypeId at the value level) + if ( + "metadata" in propertyWithMetadata && + propertyWithMetadata.metadata && + typeof propertyWithMetadata.metadata === "object" && + "dataTypeId" in propertyWithMetadata.metadata + ) { + // This is a PropertyValueWithMetadata + return propertyWithMetadata.value as PropertyValue; + } + + // Check if it's an array + if (Array.isArray(propertyWithMetadata.value)) { + return propertyWithMetadata.value.map((element) => + extractPropertyValue(element as PropertyWithMetadata), + ) as PropertyValue; + } + + // Check if it's an object (PropertyObjectWithMetadata) + if ( + typeof propertyWithMetadata.value === "object" && + propertyWithMetadata.value !== null + ) { + const entries = Object.entries(propertyWithMetadata.value); + if (entries.length > 0 && entries.every(([key]) => isBaseUrl(key))) { + // It's an object with BaseUrl keys, recurse into it + return Object.fromEntries( + entries.map(([key, value]) => [ + key, + extractPropertyValue(value as PropertyWithMetadata), + ]), + ) as PropertyValue; + } + } + + // Fallback: treat as a raw value + return propertyWithMetadata.value as PropertyValue; +}; + +/** + * Recursively extracts the PropertyMetadata from a PropertyWithMetadata. + */ +const extractPropertyMetadata = ( + propertyWithMetadata: PropertyWithMetadata, +): PropertyMetadata => { + // Check if it's a value with metadata (has metadata.dataTypeId at the value level) + if ( + "metadata" in propertyWithMetadata && + propertyWithMetadata.metadata && + typeof propertyWithMetadata.metadata === "object" && + "dataTypeId" in propertyWithMetadata.metadata + ) { + // This is a PropertyValueWithMetadata + return { + metadata: propertyWithMetadata.metadata, + } as PropertyValueMetadata; + } + + // Check if it's an array + if (Array.isArray(propertyWithMetadata.value)) { + const result: PropertyArrayMetadata = { + value: propertyWithMetadata.value.map((element) => + extractPropertyMetadata(element as PropertyWithMetadata), + ), + }; + + // Add array-level metadata if present + const arrayMetadata = (propertyWithMetadata as { metadata?: ArrayMetadata }) + .metadata; + if (arrayMetadata) { + result.metadata = arrayMetadata; + } + + return result; + } + + // Check if it's an object (PropertyObjectWithMetadata) + if ( + typeof propertyWithMetadata.value === "object" && + propertyWithMetadata.value !== null + ) { + const entries = Object.entries(propertyWithMetadata.value); + if (entries.length > 0 && entries.every(([key]) => isBaseUrl(key))) { + // It's an object with BaseUrl keys + const result: PropertyObjectMetadata = { + value: Object.fromEntries( + entries.map(([key, value]) => [ + key as BaseUrl, + extractPropertyMetadata(value as PropertyWithMetadata), + ]), + ), + }; + + // Add object-level metadata if present + const objectMetadata = ( + propertyWithMetadata as { metadata?: ObjectMetadata } + ).metadata; + if (objectMetadata) { + result.metadata = objectMetadata; + } + + return result; + } + } + + // Fallback: return value metadata with null dataTypeId + return { + metadata: { dataTypeId: null }, + } as PropertyValueMetadata; +}; + +/** + * Splits a PropertyObjectWithMetadata into separate properties and propertyMetadata objects. + * + * This is the inverse of mergePropertyObjectAndMetadata. + * + * @param propertiesWithMetadata - The combined properties with metadata object + * @returns An object containing separate properties and propertyMetadata + */ +export const splitPropertiesAndMetadata = (propertiesWithMetadata: { + value: Record; + metadata?: ObjectMetadata; +}): { + properties: PropertyObject; + propertyMetadata: PropertyObjectMetadata; +} => { + const properties: PropertyObject = {}; + const propertyMetadataValue: Record = {}; + + for (const [key, propertyWithMetadata] of Object.entries( + propertiesWithMetadata.value, + )) { + const baseUrl = key as BaseUrl; + properties[baseUrl] = extractPropertyValue(propertyWithMetadata); + propertyMetadataValue[baseUrl] = + extractPropertyMetadata(propertyWithMetadata); + } + + const result: PropertyObjectMetadata = { + value: propertyMetadataValue, + }; + + if (propertiesWithMetadata.metadata) { + result.metadata = propertiesWithMetadata.metadata; + } + + return { + properties, + propertyMetadata: result, + }; +}; diff --git a/apps/hash-integration-worker/src/linear-activities.ts b/apps/hash-integration-worker/src/activities/linear-activities.ts similarity index 99% rename from apps/hash-integration-worker/src/linear-activities.ts rename to apps/hash-integration-worker/src/activities/linear-activities.ts index e4aefa445c1..a42d00c1b33 100644 --- a/apps/hash-integration-worker/src/linear-activities.ts +++ b/apps/hash-integration-worker/src/activities/linear-activities.ts @@ -26,16 +26,16 @@ import { import { linearPropertyTypes } from "@local/hash-isomorphic-utils/ontology-type-ids"; import { v4 as uuidv4 } from "uuid"; +import { logger } from "../main.js"; +import { + getEntitiesByLinearId, + getEntityOutgoingLinks, +} from "../shared/graph-requests.js"; import { mapHashEntityToLinearUpdateInput, mapLinearDataToEntity, mapLinearDataToEntityWithOutgoingLinks, -} from "./linear-activities/mappings"; -import { logger } from "./main"; -import { - getEntitiesByLinearId, - getEntityOutgoingLinks, -} from "./shared/graph-requests"; +} from "./linear-activities/mappings.js"; const provenance: ProvidedEntityEditionProvenance = { actorType: "machine", diff --git a/apps/hash-integration-worker/src/linear-activities/mappings.ts b/apps/hash-integration-worker/src/activities/linear-activities/mappings.ts similarity index 99% rename from apps/hash-integration-worker/src/linear-activities/mappings.ts rename to apps/hash-integration-worker/src/activities/linear-activities/mappings.ts index a84028d07c5..0d1db44d416 100644 --- a/apps/hash-integration-worker/src/linear-activities/mappings.ts +++ b/apps/hash-integration-worker/src/activities/linear-activities/mappings.ts @@ -20,7 +20,7 @@ import { // getEntitiesByLinearId, getEntityOutgoingLinks, getLatestEntityById, -} from "../shared/graph-requests"; +} from "../../shared/graph-requests.js"; export const mapLinearDataToEntity = < T extends SupportedLinearTypeNames, diff --git a/apps/hash-integration-worker/src/main.ts b/apps/hash-integration-worker/src/main.ts index 84bdd2294fc..c61e55e3ca5 100644 --- a/apps/hash-integration-worker/src/main.ts +++ b/apps/hash-integration-worker/src/main.ts @@ -15,6 +15,7 @@ import { fileURLToPath } from "node:url"; import { createGraphClient } from "@local/hash-backend-utils/create-graph-client"; import { getRequiredEnv } from "@local/hash-backend-utils/environment"; +import { createCommonFlowActivities } from "@local/hash-backend-utils/flows"; import { Logger } from "@local/hash-backend-utils/logger"; import { SentryActivityInboundInterceptor } from "@local/hash-backend-utils/temporal/interceptors/activities/sentry"; import { sentrySinks } from "@local/hash-backend-utils/temporal/sinks/sentry"; @@ -22,8 +23,9 @@ import type { WorkflowTypeMap } from "@local/hash-backend-utils/temporal-integra import { defaultSinks, NativeConnection, Worker } from "@temporalio/worker"; import { config } from "dotenv-flow"; -import * as linearActivities from "./linear-activities"; -import * as workflows from "./workflows"; +import { createFlowActivities } from "./activities/flow-activities.js"; +import * as linearActivities from "./activities/linear-activities.js"; +import * as workflows from "./workflows.js"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -82,6 +84,7 @@ const workflowOption = () => async function run() { // eslint-disable-next-line no-console console.info("Starting integration worker..."); + const graphApiClient = createGraphClient(logger, { host: getRequiredEnv("HASH_GRAPH_HTTP_HOST"), port: parseInt(getRequiredEnv("HASH_GRAPH_HTTP_PORT"), 10), @@ -93,6 +96,10 @@ async function run() { ...linearActivities.createLinearIntegrationActivities({ graphApiClient, }), + ...createFlowActivities({ + graphApiClient, + }), + ...createCommonFlowActivities({ graphApiClient }), }, connection: await NativeConnection.connect({ address: `${TEMPORAL_HOST}:${TEMPORAL_PORT}`, diff --git a/apps/hash-integration-worker/src/workflows.ts b/apps/hash-integration-worker/src/workflows.ts index 105d94f59d6..263dd026890 100644 --- a/apps/hash-integration-worker/src/workflows.ts +++ b/apps/hash-integration-worker/src/workflows.ts @@ -8,7 +8,8 @@ import type { import type { ActivityOptions } from "@temporalio/workflow"; import { proxyActivities } from "@temporalio/workflow"; -import type { createLinearIntegrationActivities } from "./linear-activities"; +import type { createLinearIntegrationActivities } from "./activities/linear-activities.js"; +import { runFlowWorkflow } from "./workflows/run-flow-workflow.js"; const commonConfig: ActivityOptions = { startToCloseTimeout: "360 second", @@ -72,3 +73,5 @@ export const readLinearTeams: ReadLinearTeamsWorkflow = async ({ apiKey }) => export const updateLinearData: UpdateLinearDataWorkflow = async (params) => linearActivities.updateLinearData(params); + +export const runFlow = runFlowWorkflow; diff --git a/apps/hash-integration-worker/src/workflows/run-flow-workflow.ts b/apps/hash-integration-worker/src/workflows/run-flow-workflow.ts new file mode 100644 index 00000000000..f7d7136af55 --- /dev/null +++ b/apps/hash-integration-worker/src/workflows/run-flow-workflow.ts @@ -0,0 +1,43 @@ +import { type ProxyFlowActivity } from "@local/hash-backend-utils/flows"; +import { processFlowWorkflow } from "@local/hash-backend-utils/flows/process-flow-workflow"; +import type { IntegrationFlowActionDefinitionId } from "@local/hash-isomorphic-utils/flows/action-definitions"; +import type { + BaseRunFlowWorkflowParams, + RunFlowWorkflowResponse, +} from "@local/hash-isomorphic-utils/flows/temporal-types"; +import { + ActivityCancellationType, + proxyActivities, +} from "@temporalio/workflow"; + +import type { createFlowActivities } from "../activities/flow-activities.js"; + +const proxyFlowActivity: ProxyFlowActivity< + IntegrationFlowActionDefinitionId, + typeof createFlowActivities +> = (params) => { + const { actionName, maximumAttempts, activityId } = params; + + const { [actionName]: action } = proxyActivities< + ReturnType + >({ + cancellationType: ActivityCancellationType.ABANDON, + + startToCloseTimeout: "300 second", + + retry: { maximumAttempts }, + activityId, + }); + + return action; +}; + +export const runFlowWorkflow = async ( + params: BaseRunFlowWorkflowParams, +): Promise => { + return await processFlowWorkflow({ + ...params, + flowType: "integration", + proxyFlowActivity, + }); +}; diff --git a/apps/hash-integration-worker/tsconfig.json b/apps/hash-integration-worker/tsconfig.json index 8f69ca92e8c..e3d9688aab4 100644 --- a/apps/hash-integration-worker/tsconfig.json +++ b/apps/hash-integration-worker/tsconfig.json @@ -2,9 +2,7 @@ "extends": "@local/tsconfig/legacy-base-tsconfig-to-refactor.json", "include": ["./src/", "./scripts/"], "compilerOptions": { - "outDir": "dist", - "sourceMap": true, - "inlineSources": true, - "sourceRoot": "/" + "module": "NodeNext", + "moduleResolution": "NodeNext" } } diff --git a/apps/plugin-browser/src/scripts/background/infer-entities.ts b/apps/plugin-browser/src/scripts/background/infer-entities.ts index 0311899f1bb..581d4f11a69 100644 --- a/apps/plugin-browser/src/scripts/background/infer-entities.ts +++ b/apps/plugin-browser/src/scripts/background/infer-entities.ts @@ -19,6 +19,7 @@ import { sleep } from "@local/hash-isomorphic-utils/sleep"; import { v4 as uuid } from "uuid"; import browser from "webextension-polyfill"; +import type { FlowType } from "../../graphql/api-types.gen"; import { FlowRunStatus } from "../../graphql/api-types.gen"; import type { InferEntitiesRequest } from "../../shared/messages"; import { @@ -368,6 +369,7 @@ export const inferEntities = async ( }, }, flowDefinition, + flowType: "ai" as FlowType, flowTrigger: { triggerDefinitionId: flowDefinition.trigger.triggerDefinitionId, outputs: [], diff --git a/infra/terraform/hash/main.tf b/infra/terraform/hash/main.tf index 99324097afb..0dbd11ab98a 100644 --- a/infra/terraform/hash/main.tf +++ b/infra/terraform/hash/main.tf @@ -503,6 +503,10 @@ module "application" { name = "HASH_TEMPORAL_WORKER_INTEGRATION_SENTRY_DSN", secret = true, value = sensitive(data.vault_kv_secret_v2.secrets.data["hash_temporal_worker_integration_sentry_dsn"]) }, + { + name = "AERO_API_KEY", secret = true, + value = sensitive(data.vault_kv_secret_v2.secrets.data["aero_api_key"]) + } ] temporal_host = module.temporal.host temporal_port = module.temporal.port diff --git a/libs/@blockprotocol/graph/src/codegen/compile/compile-schemas-to-typescript.ts b/libs/@blockprotocol/graph/src/codegen/compile/compile-schemas-to-typescript.ts index 654a762f736..51c9b04df38 100644 --- a/libs/@blockprotocol/graph/src/codegen/compile/compile-schemas-to-typescript.ts +++ b/libs/@blockprotocol/graph/src/codegen/compile/compile-schemas-to-typescript.ts @@ -35,6 +35,7 @@ const compileIndividualSchemaToTypescript = async ( }, resolve: { http: { + safeUrlResolver: false, read({ url }) { if (validateVersionedUrl(url).type === "Err") { throw new Error( diff --git a/libs/@local/hash-backend-utils/src/flows.ts b/libs/@local/hash-backend-utils/src/flows.ts index 7e5265ae7c9..81eb1b44dcc 100644 --- a/libs/@local/hash-backend-utils/src/flows.ts +++ b/libs/@local/hash-backend-utils/src/flows.ts @@ -10,7 +10,7 @@ import { } from "@blockprotocol/type-system"; import { typedKeys } from "@local/advanced-types/typed-entries"; import type { GraphApi } from "@local/hash-graph-client"; -import { type HashEntity, queryEntities } from "@local/hash-graph-sdk/entity"; +import { queryEntities } from "@local/hash-graph-sdk/entity"; import type { SparseFlowRun } from "@local/hash-isomorphic-utils/flows/types"; import { currentTimeInstantTemporalAxes, @@ -30,40 +30,18 @@ import { getFlowRunFromWorkflowId, getSparseFlowRunFromWorkflowId, } from "./flows/get-flow-run-details.js"; +import { getFlowRunEntityById } from "./flows/shared/get-flow-run-entity-by-id.js"; import type { TemporalClient } from "./temporal.js"; -export const getFlowRunEntityById = async (params: { - flowRunId: EntityUuid; - graphApiClient: GraphApi; - userAuthentication: { actorId: ActorEntityUuid }; -}): Promise | null> => { - const { flowRunId, graphApiClient, userAuthentication } = params; +export { getFlowRunEntityById }; - const { - entities: [existingFlowEntity], - } = await queryEntities( - { graphApi: graphApiClient }, - userAuthentication, - { - filter: { - all: [ - { - equal: [{ path: ["uuid"] }, { parameter: flowRunId }], - }, - generateVersionedUrlMatchingFilter( - systemEntityTypes.flowRun.entityTypeId, - { ignoreParents: true }, - ), - ], - }, - temporalAxes: currentTimeInstantTemporalAxes, - includeDrafts: false, - includePermissions: false, - }, - ); - - return existingFlowEntity ?? null; -}; +export type { + ActionName, + CreateFlowActivities, + FlowActionActivity, + ProxyFlowActivity, +} from "./flows/action-types.js"; +export { createCommonFlowActivities } from "./flows/process-flow-workflow/common-activities.js"; type GetFlowRunByIdFnArgs = { flowRunId: EntityUuid; diff --git a/libs/@local/hash-backend-utils/src/flows/action-types.ts b/libs/@local/hash-backend-utils/src/flows/action-types.ts new file mode 100644 index 00000000000..0cf96bab674 --- /dev/null +++ b/libs/@local/hash-backend-utils/src/flows/action-types.ts @@ -0,0 +1,32 @@ +import type { + StepInput, + StepOutput, +} from "@local/hash-isomorphic-utils/flows/types"; +import type { Status } from "@local/status"; + +export type FlowActionActivity = ( + params: { + inputs: StepInput[]; + } & AdditionalParams, +) => Promise< + Status<{ + outputs: StepOutput[]; + }> +>; + +export type ActionName = + `${ActionDefinitionId}Action`; + +export type CreateFlowActivities = ( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + params: any, +) => Record, FlowActionActivity>; + +export type ProxyFlowActivity< + ActionId extends string, + CreateActivitiesFn extends CreateFlowActivities, +> = (params: { + actionName: ActionName; + maximumAttempts: number; + activityId: string; +}) => ReturnType[ActionName]; 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 new file mode 100644 index 00000000000..8a75a2ccaf6 --- /dev/null +++ b/libs/@local/hash-backend-utils/src/flows/process-flow-workflow.ts @@ -0,0 +1,622 @@ +import { + entityIdFromComponents, + type EntityUuid, +} from "@blockprotocol/type-system"; +import { actionDefinitions } from "@local/hash-isomorphic-utils/flows/action-definitions"; +import type { + BaseRunFlowWorkflowParams, + RunFlowWorkflowResponse, +} from "@local/hash-isomorphic-utils/flows/temporal-types"; +import type { + FlowActionDefinitionId, + FlowDefinition, + FlowStep, + FlowTrigger, + Payload, + StepOutput, +} from "@local/hash-isomorphic-utils/flows/types"; +import { validateFlowDefinition } from "@local/hash-isomorphic-utils/flows/util"; +import { stringifyError } from "@local/hash-isomorphic-utils/stringify-error"; +import type { Status } from "@local/status"; +import { StatusCode } from "@local/status"; +import { + ApplicationFailure, + proxyActivities, + workflowInfo, +} from "@temporalio/workflow"; + +import { sleep } from "../utils.js"; +import type { + ActionName, + CreateFlowActivities, + ProxyFlowActivity, +} from "./action-types.js"; +import { createCommonFlowActivities } from "./process-flow-workflow/common-activities.js"; +import { getAllStepsInFlow } from "./process-flow-workflow/get-all-steps-in-flow.js"; +import { getStepDefinitionFromFlowDefinition } from "./process-flow-workflow/get-step-definition-from-flow.js"; +import { + initializeActionStep, + initializeFlow, + initializeParallelGroup, +} from "./process-flow-workflow/initialize-flow.js"; +import { passOutputsToUnprocessedSteps } from "./process-flow-workflow/pass-outputs-to-unprocessed-steps.js"; + +export { createCommonFlowActivities }; + +const log = (message: string) => { + // eslint-disable-next-line no-console + console.log(message); +}; + +const doesFlowStepHaveSatisfiedDependencies = (params: { + step: FlowStep; + flowDefinition: FlowDefinition; + processedStepIds: string[]; +}) => { + const { step, flowDefinition, processedStepIds } = params; + + if (step.kind === "action") { + /** + * An action step has satisfied dependencies if all of its inputs have + * been provided, based on the input sources defined in the step's + * definition. + * + * We don't need to check if all required inputs have been provided, + * as this will have been enforced when the flow was validated. + */ + + const { inputSources } = getStepDefinitionFromFlowDefinition({ + step, + flowDefinition, + }); + + const actionDefinition = actionDefinitions[step.actionDefinitionId]; + + return inputSources.every((inputSource) => { + const inputDefinition = actionDefinition.inputs.find( + ({ name }) => name === inputSource.inputName, + ); + + if (!inputDefinition) { + const errorMessage = `Definition for inputName '${inputSource.inputName}' in step ${step.stepId} not found in action definition ${step.actionDefinitionId}`; + + throw ApplicationFailure.create({ + message: errorMessage, + details: [ + { + code: StatusCode.FailedPrecondition, + message: errorMessage, + }, + ], + }); + } + + if ( + step.inputs?.some((input) => input.inputName === inputSource.inputName) + ) { + /** + * If the input has been provided, the input has been satisfied. + */ + return true; + } else if (inputDefinition.required) { + /** + * If the input is required, and it hasn't been provided the step + * has not satisfied its dependencies. + */ + return false; + } else if ( + inputSource.kind === "step-output" && + inputSource.sourceStepId !== "trigger" + ) { + /** + * If the input is optional, but depends on a runnable step (i.e. not + * the trigger), the step only has satisfied its dependencies if the + * step it depends on has been processed. + * + * This ensures that the step is processed when all possible inputs + * are provided in the flow. + */ + return processedStepIds.includes(inputSource.sourceStepId); + } else if (inputSource.kind === "parallel-group-input") { + /** + * If the input is optional, but has a parallel group input as it's source + * the step should only be processed once this input has been provided. + * + * Otherwise the parallel group won't run, and produce any outputs. + */ + return false; + } else { + /** + * Otherwise, we consider the input satisfied because it is optional. + */ + return true; + } + }); + } else { + /** + * A parallel group step has satisfied dependencies if the input it + * parallelizes over has been provided. + */ + + const { inputToParallelizeOn } = step; + + return !!inputToParallelizeOn; + } +}; + +const { persistFlowActivity, userHasPermissionToRunFlowInWebActivity } = + proxyActivities>({ + startToCloseTimeout: "60 second", + retry: { + maximumAttempts: 1, + }, + }); + +export const processFlowWorkflow = async < + ValidActionDefinitionId extends FlowActionDefinitionId, + CreateActivitiesFn extends CreateFlowActivities, +>( + params: BaseRunFlowWorkflowParams & { + flowType: "ai" | "integration"; + generateFlowRunName?: ({ + flowDefinition, + flowTrigger, + }: { + flowDefinition: FlowDefinition; + flowTrigger: FlowTrigger; + }) => Promise; + proxyFlowActivity: ProxyFlowActivity< + ValidActionDefinitionId, + CreateActivitiesFn + >; + }, +): Promise => { + const { + flowDefinition, + flowType, + flowTrigger, + proxyFlowActivity, + userAuthentication, + webId, + generateFlowRunName, + } = params; + + try { + validateFlowDefinition(flowDefinition, flowType); + } catch (error) { + throw ApplicationFailure.create({ + message: (error as Error).message, + details: [ + { + code: StatusCode.InvalidArgument, + message: (error as Error).message, + contents: [], + }, + ], + }); + } + + // Ensure the user has permission to create entities in specified web + const userHasPermissionToRunFlowInWeb = + await userHasPermissionToRunFlowInWebActivity({ + userAuthentication, + webId, + }); + + if (userHasPermissionToRunFlowInWeb.status !== "ok") { + const errorMessage = `User does not have permission to run flow in web ${webId}: ${userHasPermissionToRunFlowInWeb.errorMessage}`; + throw ApplicationFailure.create({ + message: errorMessage, + details: [ + { + code: StatusCode.PermissionDenied, + message: errorMessage, + contents: [], + }, + ], + }); + } + + log(`Initializing ${flowDefinition.name} Flow`); + + const { workflowId } = workflowInfo(); + + const flow = initializeFlow({ + flowDefinition, + flowTrigger, + flowRunId: workflowId as EntityUuid, + /** use the flow definition's name as a placeholder – we need the Flow persisted to link the generating name usage to it */ + name: flowDefinition.name, + }); + + const flowEntityId = entityIdFromComponents(webId, workflowId as EntityUuid); + + await persistFlowActivity({ + flow, + flowEntityId, + stepIds: ["initialize-flow"], + userAuthentication, + webId, + }); + + if (generateFlowRunName) { + const generatedName = await generateFlowRunName({ + flowDefinition, + flowTrigger, + }); + flow.name = generatedName; + await persistFlowActivity({ + flow, + flowEntityId, + stepIds: ["generate-flow-run-name"], + userAuthentication, + webId, + }); + } + + const processedStepIds: string[] = []; + const processStepErrors: Record, "contents">> = {}; + + // Function to process a single step + const processStep = async (currentStepId: string) => { + log(`Step ${currentStepId}: processing step`); + + const currentStep = getAllStepsInFlow(flow).find( + (step) => step.stepId === currentStepId, + ); + + if (!currentStep) { + processStepErrors[currentStepId] = { + code: StatusCode.NotFound, + message: `No step found with id ${currentStepId}`, + }; + + return; + } + + if (currentStep.kind === "action") { + const actionStepDefinition = getStepDefinitionFromFlowDefinition({ + step: currentStep, + flowDefinition, + }); + + const actionName = + `${currentStep.actionDefinitionId}Action` satisfies ActionName; + + const actionActivity = proxyFlowActivity({ + // @ts-expect-error - not sure what's going on with inference of valid names here + actionName, + maximumAttempts: actionStepDefinition.retryCount ?? 3, + activityId: currentStep.stepId, + }); + + log( + `Step ${currentStepId}: executing "${ + currentStep.actionDefinitionId + }" action with ${(currentStep.inputs ?? []).length} inputs`, + ); + + let actionResponse: Status<{ + outputs: StepOutput[]; + }>; + + try { + actionResponse = await actionActivity({ + inputs: currentStep.inputs ?? [], + }); + } catch (error) { + log( + `Step ${currentStepId}: encountered runtime error executing "${ + currentStep.actionDefinitionId + }" action: ${stringifyError(error)}`, + ); + + actionResponse = { + contents: [], + code: StatusCode.Internal, + message: `Error executing action ${ + currentStep.actionDefinitionId + }: ${stringifyError(error)}`, + }; + + processStepErrors[currentStepId] = actionResponse; + } + + /** + * Consider the step processed, even if the action failed to prevent + * an infinite loop of retries. + */ + processedStepIds.push(currentStep.stepId); + + if (actionResponse.code !== StatusCode.Ok) { + log( + `Step ${currentStepId}: error executing "${currentStep.actionDefinitionId}" action`, + ); + + processStepErrors[currentStepId] = { + code: StatusCode.Internal, + message: `Action ${currentStep.actionDefinitionId} failed with status code ${actionResponse.code}: ${actionResponse.message}`, + }; + + return; + } + + const { outputs } = actionResponse.contents[0]!; + + log( + `Step ${currentStepId}: obtained ${outputs.length} outputs from "${currentStep.actionDefinitionId}" action`, + ); + + currentStep.outputs = outputs; + + const status = passOutputsToUnprocessedSteps({ + flow, + flowDefinition, + outputs, + processedStepIds, + stepId: currentStepId, + outputDefinitions: + actionDefinitions[currentStep.actionDefinitionId].outputs, + }); + + if (status.code !== StatusCode.Ok) { + processStepErrors[currentStepId] = { + code: status.code, + message: status.message, + }; + + // eslint-disable-next-line no-useless-return + return; + } + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + } else if (currentStep.kind === "parallel-group") { + const parallelGroupStepDefinition = getStepDefinitionFromFlowDefinition({ + step: currentStep, + flowDefinition, + }); + + const { inputToParallelizeOn } = currentStep; + + if (!inputToParallelizeOn) { + processStepErrors[currentStepId] = { + code: StatusCode.Internal, + message: `No input provided to parallelize on for step ${currentStepId}`, + }; + + return; + } + + const { steps: parallelGroupStepDefinitions } = + parallelGroupStepDefinition; + + const arrayToParallelizeOn = inputToParallelizeOn.payload.value; + + const newSteps = arrayToParallelizeOn.flatMap( + (parallelizedValue, index) => + parallelGroupStepDefinitions.map((stepDefinition) => { + if (stepDefinition.kind === "action") { + const parallelGroupInputPayload: Payload = { + kind: inputToParallelizeOn.payload.kind, + value: parallelizedValue, + /** @todo: figure out why this isn't assignable */ + } as Payload; + + return initializeActionStep({ + flowTrigger, + stepDefinition, + overrideStepId: `${stepDefinition.stepId}~${index}`, + parallelGroupInputPayload, + }); + } else { + return initializeParallelGroup({ flowTrigger, stepDefinition }); + } + }), + ); + + /** + * Add the new steps to the child steps of the parallel group step. + */ + currentStep.steps = [...(currentStep.steps ?? []), ...newSteps]; + + /** + * We consider the parallel group step "processed", even though its child + * steps may not have finished executing, so that the step is not re-evaluated + * in a subsequent iteration of `processSteps`. + */ + processedStepIds.push(currentStep.stepId); + } + }; + + const stepWithSatisfiedDependencies = getAllStepsInFlow(flow).filter((step) => + doesFlowStepHaveSatisfiedDependencies({ + step, + flowDefinition, + processedStepIds, + }), + ); + + if (stepWithSatisfiedDependencies.length === 0) { + const errorMessage = + "No steps have satisfied dependencies when initializing the flow."; + throw ApplicationFailure.create({ + message: errorMessage, + details: [ + { + code: StatusCode.FailedPrecondition, + message: errorMessage, + contents: [{ flow }], + }, + ], + }); + } + + // Recursively process steps which have satisfied dependencies + const processSteps = async () => { + const stepsToProcess = getAllStepsInFlow(flow).filter( + (step) => + doesFlowStepHaveSatisfiedDependencies({ + step, + flowDefinition, + processedStepIds, + }) && + !processedStepIds.some( + (processedStepId) => processedStepId === step.stepId, + ), + ); + + // There are no more steps which can be processed, so we exit the recursive loop + if (stepsToProcess.length === 0) { + return; + } + + await Promise.all(stepsToProcess.map((step) => processStep(step.stepId))); + + const lastStepIds = stepsToProcess.map((step) => step.stepId); + + await persistFlowActivity({ + flow, + flowEntityId, + stepIds: lastStepIds, + userAuthentication, + webId, + }); + + // Recursively call processSteps until all steps are processed + await processSteps(); + }; + + await processSteps(); + + log("All processable steps have completed processing"); + + /** + * Wait to flush logs + * @todo flush logs by calling the debounced function's flush, flushLogs – need to deal with it importing code that + * the workflow can't + */ + await sleep(3_000); + + const stepErrors = Object.entries(processStepErrors).map( + ([stepId, status]) => ({ ...status, contents: [{ stepId }] }), + ); + + /** @todo this is not necessarily an error once there are branches */ + if (processedStepIds.length !== getAllStepsInFlow(flow).length) { + const errorMessage = "Not all steps in the flows were processed."; + throw ApplicationFailure.create({ + message: errorMessage, + details: [ + { + code: StatusCode.Unknown, + message: errorMessage, + contents: [{ flow, stepErrors }], + }, + ], + }); + } + + for (const outputDefinition of flowDefinition.outputs) { + const step = getAllStepsInFlow(flow).find( + (flowStep) => flowStep.stepId === outputDefinition.stepId, + ); + + const errorPrefix = `Error processing output definition '${outputDefinition.name}', `; + + if (!step) { + if (!outputDefinition.required) { + continue; + } + + const errorMessage = `${errorPrefix}required step with id '${outputDefinition.stepId}' not found in outputs.`; + throw ApplicationFailure.create({ + message: errorMessage, + details: [ + { + code: StatusCode.NotFound, + message: errorMessage, + contents: [{ flow, stepErrors }], + }, + ], + }); + } + + if (step.kind === "action") { + const output = step.outputs?.find( + ({ outputName }) => outputName === outputDefinition.stepOutputName, + ); + + if (!output) { + if (!outputDefinition.required) { + continue; + } + + const errorMessage = `${errorPrefix}there is no output with name '${outputDefinition.stepOutputName}' in step ${step.stepId}`; + + throw ApplicationFailure.create({ + message: errorMessage, + details: [ + { + code: StatusCode.NotFound, + message: errorMessage, + contents: [{ stepErrors }], + }, + ], + }); + } + + flow.outputs = [ + ...(flow.outputs ?? []), + { + outputName: outputDefinition.name, + payload: output.payload, + }, + ]; + } else { + const output = step.aggregateOutput; + + if (!output) { + const errorMessage = `${errorPrefix}no aggregate output found in step ${step.stepId}`; + throw ApplicationFailure.create({ + message: errorMessage, + details: [ + { + code: StatusCode.NotFound, + message: errorMessage, + contents: [{ stepErrors }], + }, + ], + }); + } + + flow.outputs = [ + ...(flow.outputs ?? []), + { + outputName: outputDefinition.name, + payload: output.payload, + }, + ]; + } + } + + await persistFlowActivity({ + flow, + flowEntityId, + stepIds: ["complete-flow"], + userAuthentication, + webId, + }); + + const outputs = flow.outputs ?? []; + + return { + /** + * Steps may error and be retried, or the whole workflow retried, while still producing the required outputs + * – start with an initial status of OK if the outputs are present, to be adjusted if necessary. + */ + code: + outputs.length === flowDefinition.outputs.length + ? StatusCode.Ok + : StatusCode.Internal, + contents: [{ outputs, stepErrors }], + }; +}; diff --git a/libs/@local/hash-backend-utils/src/flows/process-flow-workflow/common-activities.ts b/libs/@local/hash-backend-utils/src/flows/process-flow-workflow/common-activities.ts new file mode 100644 index 00000000000..2ce51f4e64d --- /dev/null +++ b/libs/@local/hash-backend-utils/src/flows/process-flow-workflow/common-activities.ts @@ -0,0 +1,31 @@ +import type { GraphApi } from "@local/hash-graph-client"; + +import { + persistFlowActivity, + type PersistFlowActivityParams, +} from "./common-activities/persist-flow-activity.js"; +import { + userHasPermissionToRunFlowInWebActivity, + type UserHasPermissionToRunFlowInWebActivityParams, +} from "./common-activities/user-has-permission-to-run-flow-in-web-activity.js"; + +export const createCommonFlowActivities = ({ + graphApiClient, +}: { + graphApiClient: GraphApi; +}) => ({ + userHasPermissionToRunFlowInWebActivity( + params: UserHasPermissionToRunFlowInWebActivityParams, + ) { + return userHasPermissionToRunFlowInWebActivity({ + graphApiClient, + ...params, + }); + }, + persistFlowActivity(params: PersistFlowActivityParams) { + return persistFlowActivity({ + graphApiClient, + ...params, + }); + }, +}); diff --git a/apps/hash-ai-worker-ts/src/activities/flow-activities/persist-flow-activity.ts b/libs/@local/hash-backend-utils/src/flows/process-flow-workflow/common-activities/persist-flow-activity.ts similarity index 78% rename from apps/hash-ai-worker-ts/src/activities/flow-activities/persist-flow-activity.ts rename to libs/@local/hash-backend-utils/src/flows/process-flow-workflow/common-activities/persist-flow-activity.ts index 7403c9c82e9..5449a1662b2 100644 --- a/apps/hash-ai-worker-ts/src/activities/flow-activities/persist-flow-activity.ts +++ b/libs/@local/hash-backend-utils/src/flows/process-flow-workflow/common-activities/persist-flow-activity.ts @@ -1,34 +1,41 @@ import type { ActorEntityUuid, + EntityId, OriginProvenance, ProvidedEntityEditionProvenance, WebId, } from "@blockprotocol/type-system"; -import { getFlowRunEntityById } from "@local/hash-backend-utils/flows"; +import type { GraphApi } from "@local/hash-graph-client"; import { HashEntity } from "@local/hash-graph-sdk/entity"; import { mapFlowRunToEntityProperties } from "@local/hash-isomorphic-utils/flows/mappings"; import type { LocalFlowRun } from "@local/hash-isomorphic-utils/flows/types"; import { systemEntityTypes } from "@local/hash-isomorphic-utils/ontology-type-ids"; import type { FlowRun } from "@local/hash-isomorphic-utils/system-types/shared"; -import { getFlowContext } from "../shared/get-flow-context.js"; -import { graphApiClient } from "../shared/graph-api-client.js"; +import { getFlowRunEntityById } from "../../shared/get-flow-run-entity-by-id.js"; -type PersistFlowActivityParams = { +export type PersistFlowActivityParams = { flow: LocalFlowRun; + flowEntityId: EntityId; + stepIds: string[]; userAuthentication: { actorId: ActorEntityUuid }; webId: WebId; }; export const persistFlowActivity = async ( - params: PersistFlowActivityParams, + params: PersistFlowActivityParams & { graphApiClient: GraphApi }, ) => { - const { flow, userAuthentication, webId } = params; + const { + flow, + flowEntityId, + graphApiClient, + stepIds, + userAuthentication, + webId, + } = params; const { flowRunId } = flow; - const { flowEntityId, stepId } = await getFlowContext(); - const flowRunProperties = mapFlowRunToEntityProperties(flow); const existingFlowEntity = await getFlowRunEntityById({ @@ -42,7 +49,7 @@ export const persistFlowActivity = async ( origin: { type: "flow", id: flowEntityId, - stepIds: [stepId], + stepIds, } satisfies OriginProvenance, }; diff --git a/libs/@local/hash-backend-utils/src/flows/process-flow-workflow/common-activities/user-has-permission-to-run-flow-in-web-activity.ts b/libs/@local/hash-backend-utils/src/flows/process-flow-workflow/common-activities/user-has-permission-to-run-flow-in-web-activity.ts new file mode 100644 index 00000000000..e24db2f9b43 --- /dev/null +++ b/libs/@local/hash-backend-utils/src/flows/process-flow-workflow/common-activities/user-has-permission-to-run-flow-in-web-activity.ts @@ -0,0 +1,43 @@ +import type { ActorEntityUuid, WebId } from "@blockprotocol/type-system"; +import type { GraphApi } from "@local/hash-graph-client"; +import { getActorGroupRole } from "@local/hash-graph-sdk/principal/actor-group"; + +export type UserHasPermissionToRunFlowInWebActivityParams = { + userAuthentication: { actorId: ActorEntityUuid }; + webId: WebId; +}; + +/** + * Check whether a user has permission to run a flow in a web, which + * requires the user to have permission to: + * - create entities in the web + */ +export const userHasPermissionToRunFlowInWebActivity = async ( + params: UserHasPermissionToRunFlowInWebActivityParams & { + graphApiClient: GraphApi; + }, +): Promise< + | { + status: "ok"; + } + | { + status: "not-role-in-web"; + errorMessage: string; + } +> => { + const { graphApiClient, userAuthentication, webId } = params; + + const webRole = await getActorGroupRole(graphApiClient, userAuthentication, { + actorId: userAuthentication.actorId, + actorGroupId: webId, + }); + + if (!webRole) { + return { + status: "not-role-in-web", + errorMessage: "User is not assigned to any role in the web", + }; + } + + return { status: "ok" }; +}; diff --git a/apps/hash-ai-worker-ts/src/workflows/run-flow-workflow/get-all-steps-in-flow.ts b/libs/@local/hash-backend-utils/src/flows/process-flow-workflow/get-all-steps-in-flow.ts similarity index 100% rename from apps/hash-ai-worker-ts/src/workflows/run-flow-workflow/get-all-steps-in-flow.ts rename to libs/@local/hash-backend-utils/src/flows/process-flow-workflow/get-all-steps-in-flow.ts diff --git a/apps/hash-ai-worker-ts/src/workflows/run-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 similarity index 80% rename from apps/hash-ai-worker-ts/src/workflows/run-flow-workflow/get-step-definition-from-flow.ts rename to libs/@local/hash-backend-utils/src/flows/process-flow-workflow/get-step-definition-from-flow.ts index e6d5d2b5e3f..73f748445e3 100644 --- a/apps/hash-ai-worker-ts/src/workflows/run-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,6 +1,7 @@ import type { ActionStep, - ActionStepDefinition, + ActionStepWithParallelInput, + FlowActionDefinitionId, FlowDefinition, FlowStep, ParallelGroupStepDefinition, @@ -15,12 +16,9 @@ export const getStepDefinitionFromFlowDefinition = < T extends FlowStep, >(params: { step: T; - flowDefinition: FlowDefinition; + flowDefinition: FlowDefinition; }): T extends ActionStep - ? ActionStepDefinition<{ - inputName: string; - kind: "parallel-group-input"; - }> + ? ActionStepWithParallelInput : ParallelGroupStepDefinition => { const { step, flowDefinition } = params; @@ -40,9 +38,6 @@ export const getStepDefinitionFromFlowDefinition = < } return flowDefinitionNode as T extends ActionStep - ? ActionStepDefinition<{ - inputName: string; - kind: "parallel-group-input"; - }> + ? ActionStepWithParallelInput : ParallelGroupStepDefinition; }; diff --git a/apps/hash-ai-worker-ts/src/workflows/run-flow-workflow/initialize-flow.ts b/libs/@local/hash-backend-utils/src/flows/process-flow-workflow/initialize-flow.ts similarity index 97% rename from apps/hash-ai-worker-ts/src/workflows/run-flow-workflow/initialize-flow.ts rename to libs/@local/hash-backend-utils/src/flows/process-flow-workflow/initialize-flow.ts index a95d5aa9278..ac101184c8e 100644 --- a/apps/hash-ai-worker-ts/src/workflows/run-flow-workflow/initialize-flow.ts +++ b/libs/@local/hash-backend-utils/src/flows/process-flow-workflow/initialize-flow.ts @@ -3,7 +3,9 @@ import { actionDefinitions } from "@local/hash-isomorphic-utils/flows/action-def import type { ActionStep, ActionStepDefinition, + ActionStepWithParallelInput, ArrayPayload, + FlowActionDefinitionId, FlowDefinition, FlowStep, FlowTrigger, @@ -18,12 +20,7 @@ import { getAllStepsInFlow } from "./get-all-steps-in-flow.js"; export const initializeActionStep = (params: { flowTrigger: FlowTrigger; - stepDefinition: - | ActionStepDefinition - | ActionStepDefinition<{ - inputName: string; - kind: "parallel-group-input"; - }>; + stepDefinition: ActionStepDefinition | ActionStepWithParallelInput; overrideStepId?: string; existingFlow?: LocalFlowRun; parallelGroupInputPayload?: Payload; @@ -173,7 +170,7 @@ export const initializeParallelGroup = (params: { export const initializeFlow = (params: { flowRunId: EntityUuid; - flowDefinition: FlowDefinition; + flowDefinition: FlowDefinition; flowTrigger: FlowTrigger; name: string; }): LocalFlowRun => { diff --git a/apps/hash-ai-worker-ts/src/workflows/run-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 similarity index 98% rename from apps/hash-ai-worker-ts/src/workflows/run-flow-workflow/pass-outputs-to-unprocessed-steps.ts rename to libs/@local/hash-backend-utils/src/flows/process-flow-workflow/pass-outputs-to-unprocessed-steps.ts index a1e8fbbada7..ccfcc48d250 100644 --- a/apps/hash-ai-worker-ts/src/workflows/run-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 @@ -2,6 +2,7 @@ import { actionDefinitions } from "@local/hash-isomorphic-utils/flows/action-def import type { ArrayPayload, DeepReadOnly, + FlowActionDefinitionId, FlowDefinition, LocalFlowRun, OutputDefinition, @@ -21,7 +22,7 @@ import { getStepDefinitionFromFlowDefinition } from "./get-step-definition-from- */ export const passOutputsToUnprocessedSteps = (params: { flow: LocalFlowRun; - flowDefinition: FlowDefinition; + flowDefinition: FlowDefinition; stepId: string; outputDefinitions: DeepReadOnly; outputs: StepOutput[]; diff --git a/libs/@local/hash-backend-utils/src/flows/shared/get-flow-run-entity-by-id.ts b/libs/@local/hash-backend-utils/src/flows/shared/get-flow-run-entity-by-id.ts new file mode 100644 index 00000000000..93609ee82a6 --- /dev/null +++ b/libs/@local/hash-backend-utils/src/flows/shared/get-flow-run-entity-by-id.ts @@ -0,0 +1,42 @@ +import type { ActorEntityUuid, EntityUuid } from "@blockprotocol/type-system"; +import type { GraphApi } from "@local/hash-graph-client"; +import { type HashEntity, queryEntities } from "@local/hash-graph-sdk/entity"; +import { + currentTimeInstantTemporalAxes, + generateVersionedUrlMatchingFilter, +} from "@local/hash-isomorphic-utils/graph-queries"; +import { systemEntityTypes } from "@local/hash-isomorphic-utils/ontology-type-ids"; +import type { FlowRun as FlowRunEntity } from "@local/hash-isomorphic-utils/system-types/shared"; + +export const getFlowRunEntityById = async (params: { + flowRunId: EntityUuid; + graphApiClient: GraphApi; + userAuthentication: { actorId: ActorEntityUuid }; +}): Promise | null> => { + const { flowRunId, graphApiClient, userAuthentication } = params; + + const { + entities: [existingFlowEntity], + } = await queryEntities( + { graphApi: graphApiClient }, + userAuthentication, + { + filter: { + all: [ + { + equal: [{ path: ["uuid"] }, { parameter: flowRunId }], + }, + generateVersionedUrlMatchingFilter( + systemEntityTypes.flowRun.entityTypeId, + { ignoreParents: true }, + ), + ], + }, + temporalAxes: currentTimeInstantTemporalAxes, + includeDrafts: false, + includePermissions: false, + }, + ); + + return existingFlowEntity ?? null; +}; diff --git a/libs/@local/hash-backend-utils/src/integrations/aviation.ts b/libs/@local/hash-backend-utils/src/integrations/aviation.ts new file mode 100644 index 00000000000..4d222643120 --- /dev/null +++ b/libs/@local/hash-backend-utils/src/integrations/aviation.ts @@ -0,0 +1,9 @@ +export { + type AviationProposedEntity, + getScheduledArrivalEntities, +} from "./aviation/aero-api/client.js"; +export { getFlightPositionProperties } from "./aviation/flightradar24/client.js"; +export { + generateEntityMatcher, + generateLinkMatcher, +} from "./aviation/shared/primary-keys.js"; diff --git a/libs/@local/hash-backend-utils/src/integrations/aviation/aero-api/client.ts b/libs/@local/hash-backend-utils/src/integrations/aviation/aero-api/client.ts new file mode 100644 index 00000000000..5dbe4c01f33 --- /dev/null +++ b/libs/@local/hash-backend-utils/src/integrations/aviation/aero-api/client.ts @@ -0,0 +1,136 @@ +import type { ProvidedEntityEditionProvenance } from "@blockprotocol/type-system"; + +import { + type BatchFlightGraphResult, + buildFlightGraphBatch, +} from "./client/build-graph.js"; +import { generateAeroApiProvenance } from "./client/provenance.js"; +import type { + AeroApiScheduledArrivalsResponse, + AeroApiScheduledFlight, + ScheduledArrivalsRequestParams, +} from "./client/types.js"; + +export type { AviationProposedEntity } from "./client/build-graph.js"; +export type { + AeroApiAirport, + AeroApiPaginationLinks, + AeroApiScheduledArrivalsResponse, + AeroApiScheduledFlight, +} from "./client/types.js"; + +const baseUrl = "https://aeroapi.flightaware.com/aeroapi"; + +const generateUrl = ( + path: string, + params?: Record, +) => { + const url = new URL(`${baseUrl}${path}`); + + if (params) { + for (const [key, value] of Object.entries(params)) { + if (value !== undefined) { + url.searchParams.set(key, String(value)); + } + } + } + + return url.toString(); +}; + +const makeRequest = async (url: string): Promise => { + const apiKey = process.env.AERO_API_KEY; + + if (!apiKey) { + throw new Error("AERO_API_KEY environment variable is not set"); + } + + const response = await fetch(url, { + headers: { + Accept: "application/json", + "x-apikey": apiKey, + }, + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error( + `AeroAPI error (${response.status}): ${errorText || response.statusText}`, + ); + } + + return (await response.json()) as T; +}; + +/** + * Retrieve a single page of scheduled arrivals for an airport. + * + * @see https://www.flightaware.com/aeroapi/portal/documentation#get-/airports/-id-/flights/scheduled_arrivals + */ +const getScheduledArrivals = async ( + params: ScheduledArrivalsRequestParams, +): Promise => { + const { airportIcao, ...queryParams } = params; + const url = generateUrl( + `/airports/${airportIcao}/flights/scheduled_arrivals`, + queryParams, + ); + return makeRequest(url); +}; + +/** + * Retrieve all scheduled arrivals for an airport, handling pagination automatically. + */ +const getAllScheduledArrivals = async ( + params: Omit, +): Promise => { + const allFlights: AeroApiScheduledFlight[] = []; + + let response = await getScheduledArrivals(params); + allFlights.push(...response.scheduled_arrivals); + + while (response.links?.next) { + response = await makeRequest( + `${baseUrl}${response.links.next}`, + ); + allFlights.push(...response.scheduled_arrivals); + } + + return allFlights; +}; + +/** + * Fetch scheduled arrivals for an airport on a given date and map them to HASH entities. + * + * @param airportIcao - ICAO airport code (e.g., "EGLL" for London Heathrow) + * @param date - Date string in YYYY-MM-DD format + * @returns Deduplicated entities and links ready for database insertion + */ +export const getScheduledArrivalEntities = async ( + airportIcao: string, + date: string, +): Promise< + BatchFlightGraphResult & { + provenance: Pick; + } +> => { + const start = `${date}T04:00:00Z`; + + const dateObj = new Date(date); + dateObj.setUTCDate(dateObj.getUTCDate() + 1); + const tomorrow = dateObj.toISOString().slice(0, 10); + const end = `${tomorrow}T03:59:59Z`; + + const flights = await getAllScheduledArrivals({ + airportIcao, + start, + end, + }); + + const provenance = generateAeroApiProvenance(); + + return { + ...buildFlightGraphBatch(flights, provenance), + provenance, + }; +}; diff --git a/libs/@local/hash-backend-utils/src/integrations/aviation/aero-api/client/build-graph.ts b/libs/@local/hash-backend-utils/src/integrations/aviation/aero-api/client/build-graph.ts new file mode 100644 index 00000000000..682b190a01b --- /dev/null +++ b/libs/@local/hash-backend-utils/src/integrations/aviation/aero-api/client/build-graph.ts @@ -0,0 +1,291 @@ +import type { + ProvidedEntityEditionProvenance, + TypeIdsAndPropertiesForEntity, + VersionedUrl, +} from "@blockprotocol/type-system"; +import type { CreateEntityParameters } from "@local/hash-graph-sdk/entity"; +import type { + ArrivesAt as HashArrivesAt, + DepartsFrom as HashDepartsFrom, + Flight as HashFlight, + OperatedBy as HashOperatedBy, +} from "@local/hash-isomorphic-utils/system-types/flight"; +import type { + Airline as HashAirline, + Airport as HashAirport, +} from "@local/hash-isomorphic-utils/system-types/shared"; + +import { mapAirline } from "./build-graph/airline.js"; +import { mapAirport } from "./build-graph/airport.js"; +import { mapArrivesAt } from "./build-graph/arrives-at.js"; +import { mapDepartsFrom } from "./build-graph/departs-from.js"; +import { mapFlight } from "./build-graph/flight.js"; +import type { AeroApiScheduledFlight } from "./types.js"; + +/** + * A proposed entity or link to be created or matched against existing entities. + * + * If `sourceEntityLocalId` and `targetEntityLocalId` are present, this is a link entity. + * Otherwise, it's a regular entity. + */ +export type AviationProposedEntity< + T extends TypeIdsAndPropertiesForEntity = TypeIdsAndPropertiesForEntity, +> = { + /** + * The primary key string for in-memory deduplication. + */ + localEntityId: string; + /** + * The entity type IDs for this entity. + */ + entityTypeIds: [VersionedUrl, ...VersionedUrl[]]; + /** + * The properties for this entity. + */ + properties: CreateEntityParameters["properties"]; +} & ( + | { + /** + * For link entities, this must be defined. + */ + sourceEntityLocalId: string; + targetEntityLocalId: string; + } + | { + sourceEntityLocalId?: never; + targetEntityLocalId?: never; + } +); + +/** + * Type guard to check if a ProposedEntity is a link entity. + */ +export const isLinkEntity = ( + entity: AviationProposedEntity, +): entity is AviationProposedEntity & { + sourceEntityLocalId: string; + targetEntityLocalId: string; +} => { + return entity.sourceEntityLocalId !== undefined; +}; + +/** + * The result of building a flight graph, with typed entities. + */ +export type FlightGraphResult = { + entities: { + flight: AviationProposedEntity; + originAirport?: AviationProposedEntity; + destinationAirport?: AviationProposedEntity; + airline?: AviationProposedEntity; + departsFrom?: AviationProposedEntity; + arrivesAt?: AviationProposedEntity; + operatedBy?: AviationProposedEntity; + }; +}; + +/** + * Builds a graph of proposed entities from a single AeroAPI flight. + * Returns `null` if the flight cannot be mapped (missing required data). + * + * This extracts: + * - The Flight entity + * - Origin Airport entity (if origin data exists and has ICAO code) + * - Destination Airport entity (if destination data exists and has ICAO code) + * - Airline entity (if operator ICAO code exists) + * - DepartsFrom link (Flight → Origin Airport) + * - ArrivesAt link (Flight → Destination Airport) + * - OperatedBy link (Flight → Airline) + */ +export const buildSingleFlightGraph = ( + flight: AeroApiScheduledFlight, + provenance: Pick, +): FlightGraphResult | null => { + const flightMapping = mapFlight(flight, provenance); + + if (!flightMapping) { + return null; + } + + const flightEntity: AviationProposedEntity = { + localEntityId: flightMapping.primaryKey, + entityTypeIds: flightMapping.typeIdsAndProperties.entityTypeIds, + properties: flightMapping.typeIdsAndProperties.properties, + }; + + const result: FlightGraphResult = { + entities: { + flight: flightEntity, + }, + }; + + if (flight.origin) { + const airportMapping = mapAirport(flight.origin, provenance); + if (airportMapping) { + const originAirport: AviationProposedEntity = { + localEntityId: airportMapping.primaryKey, + entityTypeIds: airportMapping.typeIdsAndProperties.entityTypeIds, + properties: airportMapping.typeIdsAndProperties.properties, + }; + result.entities.originAirport = originAirport; + + const departsFromMapping = mapDepartsFrom(flight, provenance); + const departsFromLink: AviationProposedEntity = { + localEntityId: `departsFrom:${flightEntity.localEntityId}:${originAirport.localEntityId}`, + sourceEntityLocalId: flightEntity.localEntityId, + targetEntityLocalId: originAirport.localEntityId, + entityTypeIds: departsFromMapping.typeIdsAndProperties.entityTypeIds, + properties: departsFromMapping.typeIdsAndProperties.properties, + }; + result.entities.departsFrom = departsFromLink; + } + } + + if (flight.destination) { + const airportMapping = mapAirport(flight.destination, provenance); + if (airportMapping) { + const destinationAirport: AviationProposedEntity = { + localEntityId: airportMapping.primaryKey, + entityTypeIds: airportMapping.typeIdsAndProperties.entityTypeIds, + properties: airportMapping.typeIdsAndProperties.properties, + }; + result.entities.destinationAirport = destinationAirport; + + const arrivesAtMapping = mapArrivesAt(flight, provenance); + const arrivesAtLink: AviationProposedEntity = { + localEntityId: `arrivesAt:${flightEntity.localEntityId}:${destinationAirport.localEntityId}`, + sourceEntityLocalId: flightEntity.localEntityId, + targetEntityLocalId: destinationAirport.localEntityId, + entityTypeIds: arrivesAtMapping.typeIdsAndProperties.entityTypeIds, + properties: arrivesAtMapping.typeIdsAndProperties.properties, + }; + result.entities.arrivesAt = arrivesAtLink; + } + } + + const airlineMapping = mapAirline(flight, provenance); + if (airlineMapping) { + const airline: AviationProposedEntity = { + localEntityId: airlineMapping.primaryKey, + entityTypeIds: airlineMapping.typeIdsAndProperties.entityTypeIds, + properties: airlineMapping.typeIdsAndProperties.properties, + }; + result.entities.airline = airline; + + const operatedByLink: AviationProposedEntity = { + localEntityId: `operatedBy:${flightEntity.localEntityId}:${airline.localEntityId}`, + sourceEntityLocalId: flightEntity.localEntityId, + targetEntityLocalId: airline.localEntityId, + entityTypeIds: ["https://hash.ai/@h/types/entity-type/operated-by/v/1"], + properties: { value: {} }, + }; + result.entities.operatedBy = operatedByLink; + } + + return result; +}; + +/** + * The result of building graphs for multiple flights, with deduplication. + */ +export type BatchFlightGraphResult = { + /** + * Deduplicated entities and links by primary key string. + * The key is the localEntityId string, the value is the proposed entity. + */ + entities: Map; +}; + +/** + * Builds graphs for multiple flights and deduplicates shared entities. + * + * Entities like airports and airlines that appear in multiple flights + * are deduplicated by their primary key. The first occurrence's properties + * are kept (they should be identical for the same entity). + * + * Flights that cannot be mapped (missing required data) are skipped. + */ +export const buildFlightGraphBatch = ( + flights: AeroApiScheduledFlight[], + provenance: Pick, +): BatchFlightGraphResult => { + const entities = new Map(); + + for (const flight of flights) { + const graph = buildSingleFlightGraph(flight, provenance); + + if (!graph) { + continue; + } + + const addEntity = (entity: AviationProposedEntity) => { + if (!entities.has(entity.localEntityId)) { + entities.set(entity.localEntityId, entity); + } + }; + + // Add all entities (including links) + addEntity(graph.entities.flight); + if (graph.entities.originAirport) { + addEntity(graph.entities.originAirport); + } + if (graph.entities.destinationAirport) { + addEntity(graph.entities.destinationAirport); + } + if (graph.entities.airline) { + addEntity(graph.entities.airline); + } + if (graph.entities.departsFrom) { + addEntity(graph.entities.departsFrom); + } + if (graph.entities.arrivesAt) { + addEntity(graph.entities.arrivesAt); + } + if (graph.entities.operatedBy) { + addEntity(graph.entities.operatedBy); + } + } + + return { entities }; +}; + +/** + * Converts a FlightGraphResult to a flat array of ProposedEntities. + * Useful when you need to iterate over all elements uniformly. + */ +export const flattenFlightGraph = ( + result: FlightGraphResult, +): AviationProposedEntity[] => { + const elements: AviationProposedEntity[] = []; + + elements.push(result.entities.flight); + if (result.entities.originAirport) { + elements.push(result.entities.originAirport); + } + if (result.entities.destinationAirport) { + elements.push(result.entities.destinationAirport); + } + if (result.entities.airline) { + elements.push(result.entities.airline); + } + if (result.entities.departsFrom) { + elements.push(result.entities.departsFrom); + } + if (result.entities.arrivesAt) { + elements.push(result.entities.arrivesAt); + } + if (result.entities.operatedBy) { + elements.push(result.entities.operatedBy); + } + + return elements; +}; + +/** + * Converts a BatchFlightGraphResult to a flat array of ProposedEntities. + */ +export const flattenBatchFlightGraph = ( + result: BatchFlightGraphResult, +): AviationProposedEntity[] => { + return [...result.entities.values()]; +}; diff --git a/libs/@local/hash-backend-utils/src/integrations/aviation/aero-api/client/build-graph/aircraft.ts b/libs/@local/hash-backend-utils/src/integrations/aviation/aero-api/client/build-graph/aircraft.ts new file mode 100644 index 00000000000..5138cdee470 --- /dev/null +++ b/libs/@local/hash-backend-utils/src/integrations/aviation/aero-api/client/build-graph/aircraft.ts @@ -0,0 +1,71 @@ +import type { ProvidedEntityEditionProvenance } from "@blockprotocol/type-system"; +import type { Aircraft as HashAircraft } from "@local/hash-isomorphic-utils/system-types/shared"; + +import { generatePrimaryKey } from "../../../shared/primary-keys.js"; +import type { AeroApiScheduledFlight } from "../types.js"; +import type { MappingFunction } from "./mapping-types.js"; + +/** + * Input type for aircraft mapping from AeroAPI data. + */ +export type AeroApiAircraftInput = Pick< + AeroApiScheduledFlight, + "registration" | "aircraft_type" +>; + +/** + * Maps AeroAPI aircraft data to a HASH Aircraft entity. + * Returns `null` if registration number is missing. + * + * AeroAPI provides: + * - registration: Aircraft registration number (e.g., "G-XLEA") – but sometimes missing + * - aircraft_type: ICAO aircraft type code (e.g., "A388" for Airbus A380-800) - not yet encountered missing + */ +export const mapAircraft: MappingFunction< + AeroApiAircraftInput, + HashAircraft +> = ( + input: AeroApiAircraftInput, + provenance: Pick, +) => { + const registration = input.registration?.trim(); + + const primaryKey = generatePrimaryKey.aircraft({ + registrationNumber: registration, + }); + + if (!primaryKey || !registration) { + return null; + } + + const properties: HashAircraft["propertiesWithMetadata"] = { + value: { + "https://hash.ai/@h/types/property-type/registration-number/": { + value: registration, + metadata: { + dataTypeId: + "https://blockprotocol.org/@blockprotocol/types/data-type/text/v/1", + provenance, + }, + }, + ...(input.aircraft_type && { + "https://hash.ai/@h/types/property-type/icao-code/": { + value: input.aircraft_type, + metadata: { + dataTypeId: + "https://blockprotocol.org/@blockprotocol/types/data-type/text/v/1", + provenance, + }, + }, + }), + }, + }; + + return { + primaryKey, + typeIdsAndProperties: { + entityTypeIds: ["https://hash.ai/@h/types/entity-type/aircraft/v/1"], + properties, + }, + }; +}; diff --git a/libs/@local/hash-backend-utils/src/integrations/aviation/aero-api/client/build-graph/airline.ts b/libs/@local/hash-backend-utils/src/integrations/aviation/aero-api/client/build-graph/airline.ts new file mode 100644 index 00000000000..d77b04a0722 --- /dev/null +++ b/libs/@local/hash-backend-utils/src/integrations/aviation/aero-api/client/build-graph/airline.ts @@ -0,0 +1,67 @@ +import type { ProvidedEntityEditionProvenance } from "@blockprotocol/type-system"; +import type { Airline as HashAirline } from "@local/hash-isomorphic-utils/system-types/shared"; + +import { generatePrimaryKey } from "../../../shared/primary-keys.js"; +import type { AeroApiScheduledFlight } from "../types.js"; +import type { MappingFunction } from "./mapping-types.js"; + +export type AeroApiAirlineInput = Pick< + AeroApiScheduledFlight, + "operator" | "operator_icao" | "operator_iata" +>; + +/** + * Maps AeroAPI operator data to a HASH Airline entity. + */ +export const mapAirline: MappingFunction = ( + input: AeroApiAirlineInput, + provenance: Pick, +) => { + const primaryKey = generatePrimaryKey.airline({ + icaoCode: input.operator_icao, + }); + + if (!primaryKey) { + return null; + } + + const properties: HashAirline["propertiesWithMetadata"] = { + value: { + "https://blockprotocol.org/@blockprotocol/types/property-type/name/": { + value: + input.operator ?? input.operator_icao ?? input.operator_iata ?? "", + metadata: { + dataTypeId: + "https://blockprotocol.org/@blockprotocol/types/data-type/text/v/1", + provenance, + }, + }, + "https://hash.ai/@h/types/property-type/icao-code/": { + value: input.operator_icao!, + metadata: { + dataTypeId: + "https://blockprotocol.org/@blockprotocol/types/data-type/text/v/1", + provenance, + }, + }, + ...(input.operator_iata && { + "https://hash.ai/@h/types/property-type/iata-code/": { + value: input.operator_iata, + metadata: { + dataTypeId: + "https://blockprotocol.org/@blockprotocol/types/data-type/text/v/1", + provenance, + }, + }, + }), + }, + }; + + return { + primaryKey, + typeIdsAndProperties: { + entityTypeIds: ["https://hash.ai/@h/types/entity-type/airline/v/1"], + properties, + }, + }; +}; diff --git a/libs/@local/hash-backend-utils/src/integrations/aviation/aero-api/client/build-graph/airport.ts b/libs/@local/hash-backend-utils/src/integrations/aviation/aero-api/client/build-graph/airport.ts new file mode 100644 index 00000000000..09cf6179953 --- /dev/null +++ b/libs/@local/hash-backend-utils/src/integrations/aviation/aero-api/client/build-graph/airport.ts @@ -0,0 +1,83 @@ +import type { ProvidedEntityEditionProvenance } from "@blockprotocol/type-system"; +import type { Airport as HashAirport } from "@local/hash-isomorphic-utils/system-types/shared"; + +import { generatePrimaryKey } from "../../../shared/primary-keys.js"; +import type { AeroApiAirport } from "../types.js"; +import type { MappingFunction } from "./mapping-types.js"; + +export type AeroApiAirportInput = AeroApiAirport; + +/** + * Maps AeroAPI airport data to a HASH Airport entity. + */ +export const mapAirport: MappingFunction = ( + input: AeroApiAirportInput, + provenance: Pick, +) => { + const primaryKey = generatePrimaryKey.airport({ + icaoCode: input.code_icao, + }); + + if (!primaryKey) { + return null; + } + + const properties: HashAirport["propertiesWithMetadata"] = { + value: { + "https://blockprotocol.org/@blockprotocol/types/property-type/name/": { + value: input.name, + metadata: { + dataTypeId: + "https://blockprotocol.org/@blockprotocol/types/data-type/text/v/1", + provenance, + }, + }, + "https://hash.ai/@h/types/property-type/icao-code/": { + value: input.code_icao!, + metadata: { + dataTypeId: + "https://blockprotocol.org/@blockprotocol/types/data-type/text/v/1", + provenance, + }, + }, + ...(input.code_iata && { + "https://hash.ai/@h/types/property-type/iata-code/": { + value: input.code_iata, + metadata: { + dataTypeId: + "https://blockprotocol.org/@blockprotocol/types/data-type/text/v/1", + provenance, + }, + }, + }), + ...(input.timezone && { + "https://hash.ai/@h/types/property-type/timezone/": { + value: input.timezone, + metadata: { + dataTypeId: + "https://blockprotocol.org/@blockprotocol/types/data-type/text/v/1", + provenance, + }, + }, + }), + ...(input.city && { + "https://hash.ai/@h/types/property-type/city/": { + value: input.city, + metadata: { + dataTypeId: + "https://blockprotocol.org/@blockprotocol/types/data-type/text/v/1", + provenance, + }, + }, + }), + }, + }; + + return { + primaryKey, + typeIdsAndProperties: { + entityTypeIds: ["https://hash.ai/@h/types/entity-type/airport/v/1"], + properties, + }, + }; +}; diff --git a/libs/@local/hash-backend-utils/src/integrations/aviation/aero-api/client/build-graph/arrives-at.ts b/libs/@local/hash-backend-utils/src/integrations/aviation/aero-api/client/build-graph/arrives-at.ts new file mode 100644 index 00000000000..3f883ed4314 --- /dev/null +++ b/libs/@local/hash-backend-utils/src/integrations/aviation/aero-api/client/build-graph/arrives-at.ts @@ -0,0 +1,153 @@ +import type { ProvidedEntityEditionProvenance } from "@blockprotocol/type-system"; +import type { ArrivesAt as HashArrivesAt } from "@local/hash-isomorphic-utils/system-types/flight"; + +import type { AeroApiScheduledFlight } from "../types.js"; +import type { MappingFunction } from "./mapping-types.js"; + +/** + * Input type for arrival link mapping from AeroAPI data. + */ +export type AeroApiArrivalInput = Pick< + AeroApiScheduledFlight, + | "gate_destination" + | "terminal_destination" + | "actual_runway_on" + | "baggage_claim" + | "arrival_delay" + | "scheduled_in" + | "estimated_in" + | "actual_in" + | "scheduled_on" + | "estimated_on" + | "actual_on" +>; + +/** + * Maps AeroAPI arrival data to a HASH "Arrives At" link entity. + */ +export const mapArrivesAt: MappingFunction< + AeroApiArrivalInput, + HashArrivesAt, + true +> = ( + input: AeroApiArrivalInput, + provenance: Pick, +) => { + const properties: HashArrivesAt["propertiesWithMetadata"] = { + value: { + ...(input.gate_destination && { + "https://hash.ai/@h/types/property-type/gate/": { + value: input.gate_destination, + metadata: { + dataTypeId: + "https://blockprotocol.org/@blockprotocol/types/data-type/text/v/1", + provenance, + }, + }, + }), + ...(input.terminal_destination && { + "https://hash.ai/@h/types/property-type/terminal/": { + value: input.terminal_destination, + metadata: { + dataTypeId: + "https://blockprotocol.org/@blockprotocol/types/data-type/text/v/1", + provenance, + }, + }, + }), + ...(input.actual_runway_on && { + "https://hash.ai/@h/types/property-type/runway/": { + value: input.actual_runway_on, + metadata: { + dataTypeId: + "https://blockprotocol.org/@blockprotocol/types/data-type/text/v/1", + provenance, + }, + }, + }), + ...(input.baggage_claim && { + "https://hash.ai/@h/types/property-type/baggage-claim/": { + value: input.baggage_claim, + metadata: { + dataTypeId: + "https://blockprotocol.org/@blockprotocol/types/data-type/text/v/1", + provenance, + }, + }, + }), + ...(input.arrival_delay !== null && { + "https://hash.ai/@h/types/property-type/delay-in-seconds/": { + value: input.arrival_delay, + metadata: { + dataTypeId: "https://hash.ai/@h/types/data-type/integer/v/1", + provenance, + }, + }, + }), + // Gate times + ...(input.scheduled_in && { + "https://hash.ai/@h/types/property-type/scheduled-gate-time/": { + value: input.scheduled_in, + metadata: { + dataTypeId: "https://hash.ai/@h/types/data-type/datetime/v/1", + provenance, + }, + }, + }), + ...(input.estimated_in && { + "https://hash.ai/@h/types/property-type/estimated-gate-time/": { + value: input.estimated_in, + metadata: { + dataTypeId: "https://hash.ai/@h/types/data-type/datetime/v/1", + provenance, + }, + }, + }), + ...(input.actual_in && { + "https://hash.ai/@h/types/property-type/actual-gate-time/": { + value: input.actual_in, + metadata: { + dataTypeId: "https://hash.ai/@h/types/data-type/datetime/v/1", + provenance, + }, + }, + }), + // Runway times + ...(input.scheduled_on && { + "https://hash.ai/@h/types/property-type/scheduled-runway-time/": { + value: input.scheduled_on, + metadata: { + dataTypeId: "https://hash.ai/@h/types/data-type/datetime/v/1", + provenance, + }, + }, + }), + ...(input.estimated_on && { + "https://hash.ai/@h/types/property-type/estimated-runway-time/": { + value: input.estimated_on, + metadata: { + dataTypeId: "https://hash.ai/@h/types/data-type/datetime/v/1", + provenance, + }, + }, + }), + ...(input.actual_on && { + "https://hash.ai/@h/types/property-type/actual-runway-time/": { + value: input.actual_on, + metadata: { + dataTypeId: "https://hash.ai/@h/types/data-type/datetime/v/1", + provenance, + }, + }, + }), + }, + }; + + return { + primaryKey: null, // Links don't have primary keys + typeIdsAndProperties: { + entityTypeIds: ["https://hash.ai/@h/types/entity-type/arrives-at/v/1"], + properties, + }, + }; +}; diff --git a/libs/@local/hash-backend-utils/src/integrations/aviation/aero-api/client/build-graph/departs-from.ts b/libs/@local/hash-backend-utils/src/integrations/aviation/aero-api/client/build-graph/departs-from.ts new file mode 100644 index 00000000000..e3afe86f48a --- /dev/null +++ b/libs/@local/hash-backend-utils/src/integrations/aviation/aero-api/client/build-graph/departs-from.ts @@ -0,0 +1,142 @@ +import type { ProvidedEntityEditionProvenance } from "@blockprotocol/type-system"; +import type { DepartsFrom as HashDepartsFrom } from "@local/hash-isomorphic-utils/system-types/flight"; + +import type { AeroApiScheduledFlight } from "../types.js"; +import type { MappingFunction } from "./mapping-types.js"; + +/** + * Input type for departure link mapping from AeroAPI data. + */ +export type AeroApiDepartureInput = Pick< + AeroApiScheduledFlight, + | "gate_origin" + | "terminal_origin" + | "actual_runway_off" + | "departure_delay" + | "scheduled_out" + | "estimated_out" + | "actual_out" + | "scheduled_off" + | "estimated_off" + | "actual_off" +>; + +/** + * Maps AeroAPI departure data to a HASH "Departs From" link entity. + */ +export const mapDepartsFrom: MappingFunction< + AeroApiDepartureInput, + HashDepartsFrom, + true +> = ( + input: AeroApiDepartureInput, + provenance: Pick, +) => { + const properties: HashDepartsFrom["propertiesWithMetadata"] = { + value: { + ...(input.gate_origin && { + "https://hash.ai/@h/types/property-type/gate/": { + value: input.gate_origin, + metadata: { + dataTypeId: + "https://blockprotocol.org/@blockprotocol/types/data-type/text/v/1", + provenance, + }, + }, + }), + ...(input.terminal_origin && { + "https://hash.ai/@h/types/property-type/terminal/": { + value: input.terminal_origin, + metadata: { + dataTypeId: + "https://blockprotocol.org/@blockprotocol/types/data-type/text/v/1", + provenance, + }, + }, + }), + ...(input.actual_runway_off && { + "https://hash.ai/@h/types/property-type/runway/": { + value: input.actual_runway_off, + metadata: { + dataTypeId: + "https://blockprotocol.org/@blockprotocol/types/data-type/text/v/1", + provenance, + }, + }, + }), + ...(input.departure_delay !== null && { + "https://hash.ai/@h/types/property-type/delay-in-seconds/": { + value: input.departure_delay, + metadata: { + dataTypeId: "https://hash.ai/@h/types/data-type/integer/v/1", + provenance, + }, + }, + }), + // Gate times + ...(input.scheduled_out && { + "https://hash.ai/@h/types/property-type/scheduled-gate-time/": { + value: input.scheduled_out, + metadata: { + dataTypeId: "https://hash.ai/@h/types/data-type/datetime/v/1", + provenance, + }, + }, + }), + ...(input.estimated_out && { + "https://hash.ai/@h/types/property-type/estimated-gate-time/": { + value: input.estimated_out, + metadata: { + dataTypeId: "https://hash.ai/@h/types/data-type/datetime/v/1", + provenance, + }, + }, + }), + ...(input.actual_out && { + "https://hash.ai/@h/types/property-type/actual-gate-time/": { + value: input.actual_out, + metadata: { + dataTypeId: "https://hash.ai/@h/types/data-type/datetime/v/1", + provenance, + }, + }, + }), + // Runway times + ...(input.scheduled_off && { + "https://hash.ai/@h/types/property-type/scheduled-runway-time/": { + value: input.scheduled_off, + metadata: { + dataTypeId: "https://hash.ai/@h/types/data-type/datetime/v/1", + provenance, + }, + }, + }), + ...(input.estimated_off && { + "https://hash.ai/@h/types/property-type/estimated-runway-time/": { + value: input.estimated_off, + metadata: { + dataTypeId: "https://hash.ai/@h/types/data-type/datetime/v/1", + provenance, + }, + }, + }), + ...(input.actual_off && { + "https://hash.ai/@h/types/property-type/actual-runway-time/": { + value: input.actual_off, + metadata: { + dataTypeId: "https://hash.ai/@h/types/data-type/datetime/v/1", + provenance, + }, + }, + }), + }, + }; + + return { + primaryKey: null, // Links don't have primary keys + typeIdsAndProperties: { + entityTypeIds: ["https://hash.ai/@h/types/entity-type/departs-from/v/1"], + properties, + }, + }; +}; diff --git a/libs/@local/hash-backend-utils/src/integrations/aviation/aero-api/client/build-graph/flight.ts b/libs/@local/hash-backend-utils/src/integrations/aviation/aero-api/client/build-graph/flight.ts new file mode 100644 index 00000000000..68057351fb7 --- /dev/null +++ b/libs/@local/hash-backend-utils/src/integrations/aviation/aero-api/client/build-graph/flight.ts @@ -0,0 +1,210 @@ +import type { ProvidedEntityEditionProvenance } from "@blockprotocol/type-system"; +import type { Flight as HashFlight } from "@local/hash-isomorphic-utils/system-types/flight"; + +import { generatePrimaryKey } from "../../../shared/primary-keys.js"; +import type { AeroApiScheduledFlight } from "../types.js"; +import type { MappingFunction } from "./mapping-types.js"; + +export type AeroApiFlightInput = AeroApiScheduledFlight; + +/** + * Derives a HASH flight status from AeroAPI flight data. + */ +const deriveFlightStatus = ( + flight: AeroApiScheduledFlight, +): "Scheduled" | "Active" | "Landed" | "Cancelled" | "Diverted" | undefined => { + if (flight.cancelled) { + return "Cancelled"; + } + if (flight.diverted) { + return "Diverted"; + } + if (flight.actual_on) { + return "Landed"; + } + if (flight.actual_off || flight.actual_out) { + return "Active"; + } + if (flight.scheduled_out || flight.scheduled_off) { + return "Scheduled"; + } + return undefined; +}; + +/** + * Extracts flight date from the earliest available timestamp. + */ +const extractFlightDate = (flight: AeroApiScheduledFlight): string | null => { + const timestamp = + flight.scheduled_out ?? + flight.scheduled_off ?? + flight.estimated_out ?? + flight.estimated_off ?? + flight.actual_out ?? + flight.actual_off; + + if (!timestamp) { + return null; + } + + return timestamp.split("T")[0] ?? null; +}; + +/** + * Maps AeroAPI scheduled flight data to a HASH Flight entity. + * Returns `null` if flight number or date is missing. + * + * Departure and arrival details (times, gates, runways) are presented on the ArrivesAt and DepartsFrom link entities. + */ +export const mapFlight: MappingFunction = ( + input: AeroApiFlightInput, + provenance: Pick, +) => { + const flightDate = extractFlightDate(input); + + /** + * The 2 letter IATA code of the airline and the flight number. + * This is the flight number presented to consumers, and also that used by the flightradar24 API. + * Standardising on this allows us to use the same primary key and reliably identify the same flight across sources. + */ + const flightNumber = + input.operator_iata && input.flight_number + ? `${input.operator_iata}${input.flight_number}` + : null; + + const primaryKey = generatePrimaryKey.flight({ + flightNumber, + flightDate, + }); + + if (!primaryKey || !flightNumber) { + return null; + } + + const flightStatus = deriveFlightStatus(input); + + // Build codeshares array from both ICAO and IATA codes + const codeshares: Array<{ + "https://hash.ai/@h/types/property-type/icao-code/"?: { + value: string; + metadata: { + dataTypeId: "https://blockprotocol.org/@blockprotocol/types/data-type/text/v/1"; + provenance: Pick; + }; + }; + "https://hash.ai/@h/types/property-type/iata-code/"?: { + value: string; + metadata: { + dataTypeId: "https://blockprotocol.org/@blockprotocol/types/data-type/text/v/1"; + provenance: Pick; + }; + }; + }> = []; + + // Pair ICAO and IATA codeshares (they should be in matching order) + const maxCodeshares = Math.max( + input.codeshares.length, + input.codeshares_iata.length, + ); + for (let i = 0; i < maxCodeshares; i++) { + const icaoCode = input.codeshares[i]; + const iataCode = input.codeshares_iata[i]; + + if (icaoCode || iataCode) { + codeshares.push({ + ...(icaoCode && { + "https://hash.ai/@h/types/property-type/icao-code/": { + value: icaoCode, + metadata: { + dataTypeId: + "https://blockprotocol.org/@blockprotocol/types/data-type/text/v/1", + provenance, + }, + }, + }), + ...(iataCode && { + "https://hash.ai/@h/types/property-type/iata-code/": { + value: iataCode, + metadata: { + dataTypeId: + "https://blockprotocol.org/@blockprotocol/types/data-type/text/v/1", + provenance, + }, + }, + }), + }); + } + } + + const properties: HashFlight["propertiesWithMetadata"] = { + value: { + "https://hash.ai/@h/types/property-type/flight-number/": { + value: flightNumber, + metadata: { + dataTypeId: + "https://blockprotocol.org/@blockprotocol/types/data-type/text/v/1", + provenance, + }, + }, + ...(input.ident_icao && { + "https://hash.ai/@h/types/property-type/icao-code/": { + value: input.ident_icao, + metadata: { + dataTypeId: + "https://blockprotocol.org/@blockprotocol/types/data-type/text/v/1", + provenance, + }, + }, + }), + ...(input.ident_iata && { + "https://hash.ai/@h/types/property-type/iata-code/": { + value: input.ident_iata, + metadata: { + dataTypeId: + "https://blockprotocol.org/@blockprotocol/types/data-type/text/v/1", + provenance, + }, + }, + }), + ...(input.type && { + "https://hash.ai/@h/types/property-type/flight-type/": { + value: input.type, + metadata: { + dataTypeId: + "https://blockprotocol.org/@blockprotocol/types/data-type/text/v/1", + provenance, + }, + }, + }), + ...(flightStatus && { + "https://hash.ai/@h/types/property-type/flight-status/": { + value: flightStatus, + metadata: { + dataTypeId: "https://hash.ai/@h/types/data-type/flight-status/v/1", + provenance, + }, + }, + }), + "https://hash.ai/@h/types/property-type/flight-date/": { + value: flightDate!, + metadata: { + dataTypeId: "https://hash.ai/@h/types/data-type/date/v/1", + provenance, + }, + }, + ...(codeshares.length > 0 && { + "https://hash.ai/@h/types/property-type/codeshare/": { + value: codeshares.map((codeshare) => ({ value: codeshare })), + }, + }), + }, + }; + + return { + primaryKey, + typeIdsAndProperties: { + entityTypeIds: ["https://hash.ai/@h/types/entity-type/flight/v/1"], + properties, + }, + }; +}; diff --git a/libs/@local/hash-backend-utils/src/integrations/aviation/aero-api/client/build-graph/mapping-types.ts b/libs/@local/hash-backend-utils/src/integrations/aviation/aero-api/client/build-graph/mapping-types.ts new file mode 100644 index 00000000000..2c6aa1e0d14 --- /dev/null +++ b/libs/@local/hash-backend-utils/src/integrations/aviation/aero-api/client/build-graph/mapping-types.ts @@ -0,0 +1,38 @@ +import type { + ProvidedEntityEditionProvenance, + TypeIdsAndPropertiesForEntity, +} from "@blockprotocol/type-system"; +import type { CreateEntityParameters } from "@local/hash-graph-sdk/entity"; + +export type MappingResult = { + primaryKey: string; + typeIdsAndProperties: Pick< + CreateEntityParameters, + "entityTypeIds" | "properties" + >; +}; + +export type LinkMappingResult< + OutputType extends TypeIdsAndPropertiesForEntity, +> = { + primaryKey: null; + typeIdsAndProperties: Pick< + CreateEntityParameters, + "entityTypeIds" | "properties" + >; +}; + +/** + * A function that maps external API data to a HASH entity. + * Returns `null` if the entity cannot be created (e.g., missing required data for primary key). + */ +export type MappingFunction< + InputType, + OutputType extends TypeIdsAndPropertiesForEntity, + IsLinkType extends boolean = false, +> = ( + input: InputType, + provenance: Pick, +) => IsLinkType extends true + ? LinkMappingResult + : MappingResult | null; diff --git a/libs/@local/hash-backend-utils/src/integrations/aviation/aero-api/client/provenance.ts b/libs/@local/hash-backend-utils/src/integrations/aviation/aero-api/client/provenance.ts new file mode 100644 index 00000000000..978bc7e9a79 --- /dev/null +++ b/libs/@local/hash-backend-utils/src/integrations/aviation/aero-api/client/provenance.ts @@ -0,0 +1,21 @@ +import { + currentTimestamp, + type ProvidedEntityEditionProvenance, + type Url, +} from "@blockprotocol/type-system"; + +export const generateAeroApiProvenance = (): Pick< + ProvidedEntityEditionProvenance, + "sources" +> => ({ + sources: [ + { + type: "integration", + location: { + name: "FlightAware AeroAPI", + uri: "https://aeroapi.flightaware.com/aeroapi/" as Url, + }, + loadedAt: currentTimestamp(), + }, + ], +}); diff --git a/libs/@local/hash-backend-utils/src/integrations/aviation/aero-api/client/types.ts b/libs/@local/hash-backend-utils/src/integrations/aviation/aero-api/client/types.ts new file mode 100644 index 00000000000..47500dd0087 --- /dev/null +++ b/libs/@local/hash-backend-utils/src/integrations/aviation/aero-api/client/types.ts @@ -0,0 +1,171 @@ +/** + * Request parameters for the scheduled arrivals endpoint. + */ +export type ScheduledArrivalsRequestParams = { + /** ICAO airport code */ + airportIcao: string; + /** Filter by airline (ICAO code) */ + airline?: string; + /** Filter by flight type (e.g., "Airline", "General_Aviation") */ + type?: string; + /** Start of time range (ISO 8601) */ + start?: string; + /** End of time range (ISO 8601) */ + end?: string; + /** Maximum number of results per page (default: 15, max: 200) */ + max_pages?: number; + /** Cursor for pagination */ + cursor?: string; +}; + +/** + * Airport information in AeroAPI responses. + */ +export type AeroApiAirport = { + /** Primary airport code (ICAO if available, otherwise IATA or LID) */ + code: string; + /** ICAO airport code */ + code_icao: string | null; + /** IATA airport code */ + code_iata: string | null; + /** FAA Location Identifier */ + code_lid: string | null; + /** Timezone identifier (e.g., "Europe/Berlin") */ + timezone: string; + /** Airport name */ + name: string; + /** City where the airport is located */ + city: string; + /** Relative URL for airport info */ + airport_info_url: string; +}; + +/** + * Scheduled flight information from AeroAPI. + */ +export type AeroApiScheduledFlight = { + /** Flight identifier (usually callsign) */ + ident: string; + /** ICAO flight identifier */ + ident_icao: string | null; + /** IATA flight identifier */ + ident_iata: string | null; + /** Actual runway used for takeoff */ + actual_runway_off: string | null; + /** Actual runway used for landing */ + actual_runway_on: string | null; + /** FlightAware unique flight identifier */ + fa_flight_id: string; + /** Operating airline code */ + operator: string | null; + /** Operating airline ICAO code */ + operator_icao: string | null; + /** Operating airline IATA code */ + operator_iata: string | null; + /** Flight number (numeric portion only) */ + flight_number: string | null; + /** Aircraft registration number */ + registration: string | null; + /** ATC identifier/callsign */ + atc_ident: string | null; + /** FlightAware ID of the inbound flight */ + inbound_fa_flight_id: string | null; + /** Codeshare flight numbers (ICAO format) */ + codeshares: string[]; + /** Codeshare flight numbers (IATA format) */ + codeshares_iata: string[]; + /** Whether flight information is blocked */ + blocked: boolean; + /** Whether the flight has been diverted */ + diverted: boolean; + /** Whether the flight has been cancelled */ + cancelled: boolean; + /** Whether this is position-only data (no flight plan) */ + position_only: boolean; + /** Origin airport information */ + origin: AeroApiAirport | null; + /** Destination airport information */ + destination: AeroApiAirport | null; + /** Departure delay in seconds */ + departure_delay: number | null; + /** Arrival delay in seconds */ + arrival_delay: number | null; + /** Filed estimated time enroute in seconds */ + filed_ete: number | null; + /** Scheduled gate departure time (ISO 8601) */ + scheduled_out: string | null; + /** Estimated gate departure time (ISO 8601) */ + estimated_out: string | null; + /** Actual gate departure time (ISO 8601) */ + actual_out: string | null; + /** Scheduled runway departure time (ISO 8601) */ + scheduled_off: string | null; + /** Estimated runway departure time (ISO 8601) */ + estimated_off: string | null; + /** Actual runway departure time (ISO 8601) */ + actual_off: string | null; + /** Scheduled runway arrival time (ISO 8601) */ + scheduled_on: string | null; + /** Estimated runway arrival time (ISO 8601) */ + estimated_on: string | null; + /** Actual runway arrival time (ISO 8601) */ + actual_on: string | null; + /** Scheduled gate arrival time (ISO 8601) */ + scheduled_in: string | null; + /** Estimated gate arrival time (ISO 8601) */ + estimated_in: string | null; + /** Actual gate arrival time (ISO 8601) */ + actual_in: string | null; + /** Flight progress percentage (0-100) */ + progress_percent: number | null; + /** Human-readable flight status */ + status: string | null; + /** ICAO aircraft type code */ + aircraft_type: string | null; + /** Route distance in statute miles */ + route_distance: number | null; + /** Filed airspeed in knots */ + filed_airspeed: number | null; + /** Filed altitude in feet (hundreds) */ + filed_altitude: number | null; + /** Filed route string */ + route: string | null; + /** Baggage claim area */ + baggage_claim: string | null; + /** Number of business class seats */ + seats_cabin_business: number | null; + /** Number of coach/economy seats */ + seats_cabin_coach: number | null; + /** Number of first class seats */ + seats_cabin_first: number | null; + /** Departure gate */ + gate_origin: string | null; + /** Arrival gate */ + gate_destination: string | null; + /** Departure terminal */ + terminal_origin: string | null; + /** Arrival terminal */ + terminal_destination: string | null; + /** Flight type (e.g., "Airline", "General_Aviation") */ + type: string; +}; + +/** + * Pagination links in AeroAPI responses. + */ +export type AeroApiPaginationLinks = { + /** URL for the next page of results */ + next: string | null; +}; + +/** + * Response from the scheduled arrivals endpoint. + */ +export type AeroApiScheduledArrivalsResponse = { + /** Pagination links */ + links: AeroApiPaginationLinks | null; + /** Number of pages available */ + num_pages: number; + /** Array of scheduled flights */ + scheduled_arrivals: AeroApiScheduledFlight[]; +}; diff --git a/libs/@local/hash-backend-utils/src/integrations/aviation/flightradar24/client.ts b/libs/@local/hash-backend-utils/src/integrations/aviation/flightradar24/client.ts new file mode 100644 index 00000000000..774eaece161 --- /dev/null +++ b/libs/@local/hash-backend-utils/src/integrations/aviation/flightradar24/client.ts @@ -0,0 +1,103 @@ +import type { ProvidedEntityEditionProvenance } from "@blockprotocol/type-system"; +import type { Flight as HashFlight } from "@local/hash-isomorphic-utils/system-types/flight"; + +import { mapFlight } from "./client/flight.js"; +import { generateFlightradar24Provenance } from "./client/provenance.js"; +import type { + ErrorResponse, + FlightPositionsLightRequestParams, + FlightPositionsLightResponse, +} from "./client/types.js"; + +const baseUrl = "https://fr24api.flightradar24.com/api/"; + +const generateUrl = (path: string, params?: Record) => { + const url = new URL(`${baseUrl}${path}`); + + if (params) { + for (const [key, value] of Object.entries(params)) { + if (value === undefined) { + continue; + } + + if (Array.isArray(value)) { + url.searchParams.set(key, value.join(",")); + } else if (typeof value === "string" || typeof value === "number") { + url.searchParams.set(key, String(value)); + } + } + } + + return url.toString(); +}; + +const makeRequest = async (url: string): Promise => { + const apiToken = process.env.FLIGHTRADAR24_API_TOKEN; + + if (!apiToken) { + throw new Error("FLIGHTRADAR24_API_TOKEN environment variable is not set"); + } + + const response = await fetch(url, { + headers: { + Accept: "application/json", + Authorization: `Bearer ${apiToken}`, + }, + }); + + if (!response.ok) { + const errorData = (await response.json()) as ErrorResponse; + throw new Error( + `Flightradar24 API error: ${errorData.error.message} (code: ${errorData.error.code})`, + ); + } + + const data = (await response.json()) as T | ErrorResponse; + + if ("error" in data) { + throw new Error( + `Flightradar24 API error: ${data.error.message} (code: ${data.error.code})`, + ); + } + + return data; +}; + +/** + * Retrieve live flight position data from Flightradar24's flight-positions/light endpoint. + * + * @see https://fr24api.flightradar24.com/docs/endpoints/overview#live-flight-positions-light + */ +export const getFlightPositionsLight = async ( + params: FlightPositionsLightRequestParams, +): Promise => { + const url = generateUrl("live/flight-positions/light", params); + return makeRequest(url); +}; + +/** + * Fetch live flight position by callsign and map to a HASH Flight entity. + * + * @param flightNumber - Flight number (e.g., "BAW123") + * @returns The flight entity if found, or null if no matching flight or missing data + */ +export const getFlightPositionProperties = async ( + flightNumber: string, +): Promise<{ + provenance: Pick; + properties: Partial; +} | null> => { + const response = await getFlightPositionsLight({ flights: flightNumber }); + + const flight = response.data[0]; + if (!flight) { + return null; + } + + const provenance = generateFlightradar24Provenance(); + + return { + ...mapFlight(flight, provenance), + provenance, + }; +}; diff --git a/libs/@local/hash-backend-utils/src/integrations/aviation/flightradar24/client/flight.ts b/libs/@local/hash-backend-utils/src/integrations/aviation/flightradar24/client/flight.ts new file mode 100644 index 00000000000..f9d52408463 --- /dev/null +++ b/libs/@local/hash-backend-utils/src/integrations/aviation/flightradar24/client/flight.ts @@ -0,0 +1,71 @@ +import type { ProvidedEntityEditionProvenance } from "@blockprotocol/type-system"; +import type { Flight as HashFlight } from "@local/hash-isomorphic-utils/system-types/flight"; + +import type { FlightPositionLight } from "./types.js"; + +const feetToMeters = (feet: number) => feet * 0.3048; + +/** + * Maps Flightradar24 flight position data to a HASH Flight entity. + * + * Maps position-related properties: lat, lon, altitude, direction, speeds. + */ +export const mapFlight = ( + input: FlightPositionLight, + provenance: Pick, +): { properties: Partial } => { + const properties: Partial = { + "https://hash.ai/@h/types/property-type/latitude/": { + value: input.lat, + metadata: { + dataTypeId: "https://hash.ai/@h/types/data-type/latitude/v/1", + provenance, + }, + }, + "https://hash.ai/@h/types/property-type/longitude/": { + value: input.lon, + metadata: { + dataTypeId: "https://hash.ai/@h/types/data-type/longitude/v/1", + provenance, + }, + }, + "https://hash.ai/@h/types/property-type/altitude/": { + value: feetToMeters(input.alt), + metadata: { + dataTypeId: "https://hash.ai/@h/types/data-type/meters/v/1", + provenance, + }, + }, + "https://hash.ai/@h/types/property-type/direction/": { + value: input.track, + metadata: { + dataTypeId: "https://hash.ai/@h/types/data-type/degree/v/1", + provenance, + }, + }, + "https://hash.ai/@h/types/property-type/ground-speed/": { + value: input.gspeed, + metadata: { + dataTypeId: "https://hash.ai/@h/types/data-type/knots/v/1", + provenance, + }, + }, + "https://hash.ai/@h/types/property-type/vertical-speed/": { + value: input.vspeed, + metadata: { + dataTypeId: "https://hash.ai/@h/types/data-type/feet-per-minute/v/1", + provenance, + }, + }, + "https://hash.ai/@h/types/property-type/is-on-ground/": { + value: input.alt === 0, + metadata: { + dataTypeId: + "https://blockprotocol.org/@blockprotocol/types/data-type/boolean/v/1", + provenance, + }, + }, + }; + + return { properties }; +}; diff --git a/libs/@local/hash-backend-utils/src/integrations/aviation/flightradar24/client/provenance.ts b/libs/@local/hash-backend-utils/src/integrations/aviation/flightradar24/client/provenance.ts new file mode 100644 index 00000000000..5561635dea1 --- /dev/null +++ b/libs/@local/hash-backend-utils/src/integrations/aviation/flightradar24/client/provenance.ts @@ -0,0 +1,21 @@ +import { + currentTimestamp, + type ProvidedEntityEditionProvenance, + type Url, +} from "@blockprotocol/type-system"; + +export const generateFlightradar24Provenance = (): Pick< + ProvidedEntityEditionProvenance, + "sources" +> => ({ + sources: [ + { + type: "integration", + location: { + name: "Flightradar24 API", + uri: "https://fr24api.flightradar24.com/api/" as Url, + }, + loadedAt: currentTimestamp(), + }, + ], +}); diff --git a/libs/@local/hash-backend-utils/src/integrations/aviation/flightradar24/client/types.ts b/libs/@local/hash-backend-utils/src/integrations/aviation/flightradar24/client/types.ts new file mode 100644 index 00000000000..cfe0ebb2c96 --- /dev/null +++ b/libs/@local/hash-backend-utils/src/integrations/aviation/flightradar24/client/types.ts @@ -0,0 +1,108 @@ +export type ErrorResponse = { + /** Error details from the Flightradar24 API */ + error: { + /** A unique error code identifying the type of error */ + code: string; + /** A human-readable description of the error */ + message: string; + }; +}; + +/** + * Flight categories used by Flightradar24 + * - P: Passenger + * - C: Cargo + * - M: Military + * - J: Business Jet + * - T: General Aviation (Turboprop) + * - H: Helicopter + * - B: Balloon + * - G: Glider + * - D: Drone + * - V: Ground Vehicle + * - O: Other + * - N: Non-categorized + */ +export type FlightCategory = + | "P" + | "C" + | "M" + | "J" + | "T" + | "H" + | "B" + | "G" + | "D" + | "V" + | "O" + | "N"; + +/** + * Request parameters for the flight-positions/light endpoint. + * + * At least one of the filter parameters is required. + */ +export type FlightPositionsLightRequestParams = { + /** Geographic bounds: north,south,west,east (comma-separated float values) */ + bounds?: string; + /** Comma-separated list of flight numbers (max 15) */ + flights?: string; + /** Comma-separated list of flight callsigns (max 15) */ + callsigns?: string; + /** Comma-separated list of aircraft registration numbers (max 15) */ + registrations?: string; + /** + * Airports using IATA, ICAO, or ISO 3166-1 alpha-2 codes. + * Optional direction prefix: "inbound:JFK,outbound:LAX" + */ + airports?: string; + /** Flight routes between airports or countries (e.g., "JFK-LAX,LHR-CDG") */ + routes?: string; + /** Comma-separated list of aircraft ICAO type codes (max 15) */ + aircraft?: string; + /** Altitude ranges in feet (e.g., "0-3000,30000-40000") */ + altitude_ranges?: string; + /** Comma-separated list of flight categories */ + categories?: FlightCategory[]; + /** Maximum number of results to return (max 30000) */ + limit?: number; +}; + +/** + * A flight position record from the light endpoint. + */ +export type FlightPositionLight = { + /** Unique identifier assigned by Flightradar24 to each flight */ + fr24_id: string; + /** + * The ICAO 24-bit transponder address in hexadecimal + */ + hex: string; + /** + * Up to 8 characters as sent from the aircraft's transponder + */ + callsign: string; + /** Latitude of the aircraft's position in decimal degrees */ + lat: number; + /** Longitude of the aircraft's position in decimal degrees */ + lon: number; + /** Track angle of the aircraft in degrees (0-360) */ + track: number; + /** Altitude of the aircraft in feet */ + alt: number; + /** Ground speed of the aircraft in knots */ + gspeed: number; + /** Vertical speed of the aircraft in feet per minute */ + vspeed: number; + /** 4-digit squawk code of the aircraft */ + squawk: number; + /** Timestamp of the position data (ISO 8601 format) */ + timestamp: string; + /** Source of the position data, e.g. "ADSB" */ + source: string; +}; + +export type FlightPositionsLightResponse = { + /** Array of flight position records matching the request parameters */ + data: FlightPositionLight[]; +}; diff --git a/libs/@local/hash-backend-utils/src/integrations/aviation/shared/primary-keys.ts b/libs/@local/hash-backend-utils/src/integrations/aviation/shared/primary-keys.ts new file mode 100644 index 00000000000..26e7ecc2d10 --- /dev/null +++ b/libs/@local/hash-backend-utils/src/integrations/aviation/shared/primary-keys.ts @@ -0,0 +1,268 @@ +import { + type BaseUrl, + type EntityId, + extractEntityUuidFromEntityId, +} from "@blockprotocol/type-system"; +import type { Filter } from "@local/hash-graph-client"; +import type { ProposedEntity } from "@local/hash-isomorphic-utils/flows/types"; +import { + systemEntityTypes, + systemLinkEntityTypes, + systemPropertyTypes, +} from "@local/hash-isomorphic-utils/ontology-type-ids"; + +/** + * Generates string primary keys for aviation entities, used for in-memory deduplication. + * + * Returns `null` if any required field(s) are missing. + */ +export const generatePrimaryKey = { + flight: (input: { + flightNumber: string | null | undefined; + flightDate: string | null | undefined; + }): string | null => { + if (!input.flightNumber || !input.flightDate) { + return null; + } + + return `flight-${input.flightNumber}-${input.flightDate}`; + }, + aircraft: (input: { + registrationNumber: string | null | undefined; + }): string | null => { + if (!input.registrationNumber) { + return null; + } + + return `aircraft-${input.registrationNumber}`; + }, + airport: (input: { icaoCode: string | null | undefined }): string | null => { + if (!input.icaoCode) { + return null; + } + + return `airport-${input.icaoCode}`; + }, + airline: (input: { icaoCode: string | null | undefined }): string | null => { + if (!input.icaoCode) { + return null; + } + + return `airline-${input.icaoCode}`; + }, +}; + +/** + * Generates Graph API filters to find existing entities matching a proposed entity. + */ +export const generateEntityMatcher = { + [systemEntityTypes.flight.entityTypeBaseUrl]: (input: ProposedEntity) => { + return { + all: [ + { + equal: [ + { + path: [ + "properties", + systemPropertyTypes.flightNumber.propertyTypeBaseUrl, + ], + }, + { + parameter: + input.properties[ + systemPropertyTypes.flightNumber.propertyTypeBaseUrl + ], + }, + ], + }, + { + equal: [ + { + path: [ + "properties", + systemPropertyTypes.flightDate.propertyTypeBaseUrl, + ], + }, + { + parameter: + input.properties[ + systemPropertyTypes.flightDate.propertyTypeBaseUrl + ], + }, + ], + }, + ], + }; + }, + [systemEntityTypes.aircraft.entityTypeBaseUrl]: (input: ProposedEntity) => { + return { + all: [ + { + equal: [ + { + path: [ + "properties", + systemPropertyTypes.registrationNumber.propertyTypeBaseUrl, + ], + }, + { + parameter: + input.properties[ + systemPropertyTypes.registrationNumber.propertyTypeBaseUrl + ], + }, + ], + }, + ], + }; + }, + [systemEntityTypes.airport.entityTypeBaseUrl]: (input: ProposedEntity) => { + return { + all: [ + { + equal: [ + { + path: [ + "properties", + systemPropertyTypes.icaoCode.propertyTypeBaseUrl, + ], + }, + { + parameter: + input.properties[ + systemPropertyTypes.icaoCode.propertyTypeBaseUrl + ], + }, + ], + }, + ], + }; + }, + [systemEntityTypes.airline.entityTypeBaseUrl]: (input: ProposedEntity) => { + return { + all: [ + { + equal: [ + { + path: [ + "properties", + systemPropertyTypes.icaoCode.propertyTypeBaseUrl, + ], + }, + { + parameter: + input.properties[ + systemPropertyTypes.icaoCode.propertyTypeBaseUrl + ], + }, + ], + }, + ], + }; + }, +} as const satisfies Record Filter>; + +/** + * Generates Graph API filters to find existing link entities. + */ +export const generateLinkMatcher = { + [systemLinkEntityTypes.arrivesAt.linkEntityTypeBaseUrl]: (input: { + leftEntityId: EntityId; + rightEntityId: EntityId; + }) => { + const leftEntityUuid = extractEntityUuidFromEntityId(input.leftEntityId); + const rightEntityUuid = extractEntityUuidFromEntityId(input.rightEntityId); + + return { + all: [ + { + equal: [ + { path: ["leftEntity", "uuid"] }, + { parameter: leftEntityUuid }, + ], + }, + { + equal: [ + { path: ["rightEntity", "uuid"] }, + { parameter: rightEntityUuid }, + ], + }, + ], + }; + }, + [systemLinkEntityTypes.departsFrom.linkEntityTypeBaseUrl]: (input: { + leftEntityId: EntityId; + rightEntityId: EntityId; + }) => { + const leftEntityUuid = extractEntityUuidFromEntityId(input.leftEntityId); + const rightEntityUuid = extractEntityUuidFromEntityId(input.rightEntityId); + + return { + all: [ + { + equal: [ + { path: ["leftEntity", "uuid"] }, + { parameter: leftEntityUuid }, + ], + }, + { + equal: [ + { path: ["rightEntity", "uuid"] }, + { parameter: rightEntityUuid }, + ], + }, + ], + }; + }, + [systemLinkEntityTypes.operatedBy.linkEntityTypeBaseUrl]: (input: { + leftEntityId: EntityId; + rightEntityId: EntityId; + }) => { + const leftEntityUuid = extractEntityUuidFromEntityId(input.leftEntityId); + const rightEntityUuid = extractEntityUuidFromEntityId(input.rightEntityId); + + return { + all: [ + { + equal: [ + { path: ["leftEntity", "uuid"] }, + { parameter: leftEntityUuid }, + ], + }, + { + equal: [ + { path: ["rightEntity", "uuid"] }, + { parameter: rightEntityUuid }, + ], + }, + ], + }; + }, + [systemLinkEntityTypes.usesAircraft.linkEntityTypeBaseUrl]: (input: { + leftEntityId: EntityId; + rightEntityId: EntityId; + }) => { + const leftEntityUuid = extractEntityUuidFromEntityId(input.leftEntityId); + const rightEntityUuid = extractEntityUuidFromEntityId(input.rightEntityId); + + return { + all: [ + { + equal: [ + { path: ["leftEntity", "uuid"] }, + { parameter: leftEntityUuid }, + ], + }, + { + equal: [ + { path: ["rightEntity", "uuid"] }, + { parameter: rightEntityUuid }, + ], + }, + ], + }; + }, +} as const satisfies Record< + (typeof systemLinkEntityTypes)[keyof typeof systemLinkEntityTypes]["linkEntityTypeBaseUrl"], + (input: { leftEntityId: EntityId; rightEntityId: EntityId }) => Filter +>; diff --git a/libs/@local/hash-backend-utils/src/temporal-integration-workflow-types.ts b/libs/@local/hash-backend-utils/src/temporal-integration-workflow-types.ts index 6d3730e03e1..e0c9109eb37 100644 --- a/libs/@local/hash-backend-utils/src/temporal-integration-workflow-types.ts +++ b/libs/@local/hash-backend-utils/src/temporal-integration-workflow-types.ts @@ -7,6 +7,11 @@ import type { } from "@blockprotocol/type-system"; import type { Team } from "@linear/sdk"; import type { SerializedEntity } from "@local/hash-graph-sdk/entity"; +import type { IntegrationFlowActionDefinitionId } from "@local/hash-isomorphic-utils/flows/action-definitions"; +import type { + BaseRunFlowWorkflowParams, + RunFlowWorkflowResponse, +} from "@local/hash-isomorphic-utils/flows/temporal-types"; export type PartialEntity = { properties: Entity["properties"]; @@ -61,6 +66,10 @@ export type SyncQueryToGoogleSheetWorkflow = (params: { userAccountId: MachineId; }) => Promise; +export type RunIntegrationFlowWorkflow = ( + params: BaseRunFlowWorkflowParams, +) => Promise; + export type WorkflowTypeMap = { syncLinearToWeb: SyncWebWorkflow; readLinearTeams: ReadLinearTeamsWorkflow; @@ -70,4 +79,6 @@ export type WorkflowTypeMap = { updateLinearData: UpdateLinearDataWorkflow; /** @todo: add `createLinearData` */ + + runFlow: RunIntegrationFlowWorkflow; }; diff --git a/libs/@local/hash-isomorphic-utils/src/ai-inference-types.ts b/libs/@local/hash-isomorphic-utils/src/ai-inference-types.ts index 7b3770b33e8..a1381029bd7 100644 --- a/libs/@local/hash-isomorphic-utils/src/ai-inference-types.ts +++ b/libs/@local/hash-isomorphic-utils/src/ai-inference-types.ts @@ -56,7 +56,10 @@ export type ProposedEntityLinkFields = { targetEntityId: number; }; -export type ProposedEntity = +/** + * @deprecated only used in an old inference flow – use ProposedEntity from the flows/types.ts file instead + */ +export type DeprecatedProposedEntity = | BaseProposedEntity | (BaseProposedEntity & ProposedEntityLinkFields); @@ -70,7 +73,7 @@ type InferredEntityResultBase = { entity?: SerializedEntity | null; entityTypeIds: VersionedUrl[]; operation: "create" | "update" | "already-exists-as-proposed"; - proposedEntity: ProposedEntity; + proposedEntity: DeprecatedProposedEntity; status: "success" | "failure"; }; diff --git a/libs/@local/hash-isomorphic-utils/src/flows/action-definitions.ts b/libs/@local/hash-isomorphic-utils/src/flows/action-definitions.ts index c3180ff052a..ef066b0d5bf 100644 --- a/libs/@local/hash-isomorphic-utils/src/flows/action-definitions.ts +++ b/libs/@local/hash-isomorphic-utils/src/flows/action-definitions.ts @@ -6,7 +6,10 @@ import type { StepInput, } from "./types.js"; -export type ActionDefinitionId = +/** + * Activities that are registered to the 'ai' temporal task queue. + */ +export type AiFlowActionDefinitionId = | "answerQuestion" | "generateWebQueries" | "getFileFromUrl" @@ -21,7 +24,14 @@ export type ActionDefinitionId = | "webSearch" | "writeGoogleSheet"; -const actionDefinitionsAsConst = { +/** + * Activities that are registered to the 'integration' temporal task queue. + */ +export type IntegrationFlowActionDefinitionId = + | "getScheduledFlights" + | "persistIntegrationEntities"; + +const aiFlowActionDefinitionsAsConst = { generateWebQueries: { actionDefinitionId: "generateWebQueries", name: "Generate Web Query", @@ -528,55 +538,154 @@ const actionDefinitionsAsConst = { }, ], }, -} as const satisfies Record>; +} as const satisfies Record< + AiFlowActionDefinitionId, + DeepReadOnly> +>; -export const actionDefinitions = actionDefinitionsAsConst as unknown as Record< - ActionDefinitionId, - ActionDefinition +const integrationFlowActionDefinitionsAsConst = { + getScheduledFlights: { + actionDefinitionId: "getScheduledFlights", + name: "Get Scheduled Flights", + description: + "Fetch scheduled flight arrivals from AeroAPI for a given airport and date.", + kind: "action", + inputs: [ + { + oneOfPayloadKinds: ["Text"], + name: "airportIcao", + description: + "The ICAO code of the airport (e.g. 'EGLL' for London Heathrow)", + required: true, + array: false, + }, + { + oneOfPayloadKinds: ["Date"], + name: "date", + description: + "The date to fetch flights for in ISO format (e.g. '2024-01-15')", + required: true, + array: false, + }, + ], + outputs: [ + { + payloadKind: "ProposedEntity", + name: "proposedEntities", + description: "The proposed flight entities and related data", + array: true, + required: true, + }, + ], + }, + persistIntegrationEntities: { + actionDefinitionId: "persistIntegrationEntities", + name: "Persist Integration Entities", + description: + "Persist proposed entities from an integration to the graph database.", + kind: "action", + inputs: [ + { + oneOfPayloadKinds: ["ProposedEntity"], + name: "proposedEntities", + description: "The proposed entities to persist", + required: true, + array: true, + }, + ], + outputs: [ + { + payloadKind: "PersistedEntities", + name: "persistedEntities", + description: + "The result of persisting the entities, including any failures", + array: false, + required: true, + }, + ], + }, +} as const satisfies Record< + IntegrationFlowActionDefinitionId, + DeepReadOnly> >; -export type InputNameForAction< - T extends keyof typeof actionDefinitionsAsConst, -> = (typeof actionDefinitionsAsConst)[T]["inputs"][number]["name"]; +export const aiActionDefinitions = + aiFlowActionDefinitionsAsConst as unknown as Record< + AiFlowActionDefinitionId, + ActionDefinition + >; + +export const integrationActionDefinitions = + integrationFlowActionDefinitionsAsConst as unknown as Record< + IntegrationFlowActionDefinitionId, + ActionDefinition + >; + +export const actionDefinitions = { + ...aiActionDefinitions, + ...integrationActionDefinitions, +}; -export type OutputNameForAction< - T extends keyof typeof actionDefinitionsAsConst, -> = (typeof actionDefinitionsAsConst)[T]["outputs"][number]["name"]; +export type InputNameForAiFlowAction< + T extends keyof typeof aiFlowActionDefinitionsAsConst, +> = (typeof aiFlowActionDefinitionsAsConst)[T]["inputs"][number]["name"]; -export type InputPayloadKindForAction< - T extends ActionDefinitionId, - N extends InputNameForAction, +export type OutputNameForAiFlowAction< + T extends keyof typeof aiFlowActionDefinitionsAsConst, +> = (typeof aiFlowActionDefinitionsAsConst)[T]["outputs"][number]["name"]; + +export type InputPayloadKindForAiFlowAction< + T extends AiFlowActionDefinitionId, + N extends InputNameForAiFlowAction, > = Extract< - (typeof actionDefinitionsAsConst)[T]["inputs"][number], + (typeof aiFlowActionDefinitionsAsConst)[T]["inputs"][number], { name: N } >["oneOfPayloadKinds"][number]; -type InputPayloadType< - T extends ActionDefinitionId, - N extends InputNameForAction, +export type InputNameForIntegrationFlowAction< + T extends keyof typeof integrationFlowActionDefinitionsAsConst, +> = + (typeof integrationFlowActionDefinitionsAsConst)[T]["inputs"][number]["name"]; + +export type OutputNameForIntegrationFlowAction< + T extends keyof typeof integrationFlowActionDefinitionsAsConst, +> = + (typeof integrationFlowActionDefinitionsAsConst)[T]["outputs"][number]["name"]; + +export type InputPayloadKindForIntegrationFlowAction< + T extends IntegrationFlowActionDefinitionId, + N extends InputNameForIntegrationFlowAction, > = Extract< - (typeof actionDefinitionsAsConst)[T]["inputs"][number], + (typeof integrationFlowActionDefinitionsAsConst)[T]["inputs"][number], + { name: N } +>["oneOfPayloadKinds"][number]; + +type AiFlowInputPayloadType< + T extends AiFlowActionDefinitionId, + N extends InputNameForAiFlowAction, +> = Extract< + (typeof aiFlowActionDefinitionsAsConst)[T]["inputs"][number], { name: N } > extends { required: true; array: true } - ? PayloadKindValues[InputPayloadKindForAction][] + ? PayloadKindValues[InputPayloadKindForAiFlowAction][] : Extract< - (typeof actionDefinitionsAsConst)[T]["inputs"][number], + (typeof aiFlowActionDefinitionsAsConst)[T]["inputs"][number], { name: N } > extends { required: false; array: true } - ? PayloadKindValues[InputPayloadKindForAction][] | undefined + ? PayloadKindValues[InputPayloadKindForAiFlowAction][] | undefined : Extract< - (typeof actionDefinitionsAsConst)[T]["inputs"][number], + (typeof aiFlowActionDefinitionsAsConst)[T]["inputs"][number], { name: N } > extends { required: true; array: false } - ? PayloadKindValues[InputPayloadKindForAction] - : PayloadKindValues[InputPayloadKindForAction] | undefined; + ? PayloadKindValues[InputPayloadKindForAiFlowAction] + : PayloadKindValues[InputPayloadKindForAiFlowAction] | undefined; -type SimplifiedActionInputsObject = { - [N in InputNameForAction]: InputPayloadType; +type SimplifiedActionInputsObject = { + [N in InputNameForAiFlowAction]: AiFlowInputPayloadType; }; -export const getSimplifiedActionInputs = < - T extends ActionDefinitionId, +export const getSimplifiedAiFlowActionInputs = < + T extends AiFlowActionDefinitionId, >(params: { inputs: StepInput[]; actionType: T; @@ -585,9 +694,9 @@ export const getSimplifiedActionInputs = < return inputs.reduce( (acc, input) => { - const inputName = input.inputName as InputNameForAction; + const inputName = input.inputName as InputNameForAiFlowAction; - acc[inputName] = input.payload.value as InputPayloadType< + acc[inputName] = input.payload.value as AiFlowInputPayloadType< T, typeof inputName >; @@ -597,3 +706,59 @@ export const getSimplifiedActionInputs = < {} as SimplifiedActionInputsObject, ); }; + +type IntegrationFlowInputPayloadType< + T extends IntegrationFlowActionDefinitionId, + N extends InputNameForIntegrationFlowAction, +> = Extract< + (typeof integrationFlowActionDefinitionsAsConst)[T]["inputs"][number], + { name: N } +> extends { required: true; array: true } + ? PayloadKindValues[InputPayloadKindForIntegrationFlowAction][] + : Extract< + (typeof integrationFlowActionDefinitionsAsConst)[T]["inputs"][number], + { name: N } + > extends { required: false; array: true } + ? + | PayloadKindValues[InputPayloadKindForIntegrationFlowAction][] + | undefined + : Extract< + (typeof integrationFlowActionDefinitionsAsConst)[T]["inputs"][number], + { name: N } + > extends { required: true; array: false } + ? PayloadKindValues[InputPayloadKindForIntegrationFlowAction] + : + | PayloadKindValues[InputPayloadKindForIntegrationFlowAction] + | undefined; + +type SimplifiedIntegrationActionInputsObject< + T extends IntegrationFlowActionDefinitionId, +> = { + [N in InputNameForIntegrationFlowAction]: IntegrationFlowInputPayloadType< + T, + N + >; +}; + +export const getSimplifiedIntegrationFlowActionInputs = < + T extends IntegrationFlowActionDefinitionId, +>(params: { + inputs: StepInput[]; + actionType: T; +}): SimplifiedIntegrationActionInputsObject => { + const { inputs } = params; + + return inputs.reduce( + (acc, input) => { + const inputName = input.inputName as InputNameForIntegrationFlowAction; + + acc[inputName] = input.payload.value as IntegrationFlowInputPayloadType< + T, + typeof inputName + >; + + return acc; + }, + {} as SimplifiedIntegrationActionInputsObject, + ); +}; diff --git a/libs/@local/hash-isomorphic-utils/src/flows/browser-plugin-flow-definitions.ts b/libs/@local/hash-isomorphic-utils/src/flows/browser-plugin-flow-definitions.ts index 82a3f530a7d..891bdd5412b 100644 --- a/libs/@local/hash-isomorphic-utils/src/flows/browser-plugin-flow-definitions.ts +++ b/libs/@local/hash-isomorphic-utils/src/flows/browser-plugin-flow-definitions.ts @@ -1,8 +1,9 @@ import type { EntityUuid } from "@blockprotocol/type-system"; import type { - InputNameForAction, - OutputNameForAction, + AiFlowActionDefinitionId, + InputNameForAiFlowAction, + OutputNameForAiFlowAction, } from "./action-definitions.js"; import type { AutomaticInferenceTriggerInputName, @@ -13,216 +14,223 @@ import type { import { browserInferenceFlowOutput } from "./browser-plugin-flow-types.js"; import type { FlowDefinition } from "./types.js"; -export const manualBrowserInferenceFlowDefinition: FlowDefinition = { - name: "Analyze webpage", - flowDefinitionId: "manual-browser-inference" as EntityUuid, - description: "Find entities of the requested types in a web page", - trigger: { - kind: "trigger", - description: "Triggered manually by user for a specific web page", - triggerDefinitionId: "userTrigger", - outputs: [ - { - payloadKind: - "WebPage" satisfies ManualInferenceTriggerInputs["visitedWebPage"]["kind"], - description: "The web page visited", - name: "visitedWebPage" satisfies ManualInferenceTriggerInputName, - array: false, - required: true, - }, - { - payloadKind: - "VersionedUrl" satisfies ManualInferenceTriggerInputs["entityTypeIds"]["kind"], - description: "The ids of the entity types to create entities of", - name: "entityTypeIds" satisfies ManualInferenceTriggerInputName, - array: true, - required: true, - }, - { - payloadKind: - "Text" satisfies ManualInferenceTriggerInputs["model"]["kind"], - description: "The model to use for inference", - name: "model" satisfies ManualInferenceTriggerInputName, - array: true, - required: true, - }, - { - payloadKind: - "Boolean" satisfies ManualInferenceTriggerInputs["draft"]["kind"], - description: "Whether the entities should be created as drafts or not", - name: "draft" satisfies ManualInferenceTriggerInputName, - array: false, - required: true, - }, - ], - }, - steps: [ - { - stepId: "0", - kind: "action", - actionDefinitionId: "inferEntitiesFromContent", - description: "Find entities in web page content", - inputSources: [ - { - inputName: - "content" satisfies InputNameForAction<"inferEntitiesFromContent">, - kind: "step-output", - sourceStepId: "trigger", - sourceStepOutputName: - "visitedWebPage" satisfies ManualInferenceTriggerInputName, - }, +export const manualBrowserInferenceFlowDefinition: FlowDefinition = + { + name: "Analyze webpage", + type: "ai", + flowDefinitionId: "manual-browser-inference" as EntityUuid, + description: "Find entities of the requested types in a web page", + trigger: { + kind: "trigger", + description: "Triggered manually by user for a specific web page", + triggerDefinitionId: "userTrigger", + outputs: [ { - inputName: - "entityTypeIds" satisfies InputNameForAction<"inferEntitiesFromContent">, - kind: "step-output", - sourceStepId: "trigger", - sourceStepOutputName: - "entityTypeIds" satisfies ManualInferenceTriggerInputName, + payloadKind: + "WebPage" satisfies ManualInferenceTriggerInputs["visitedWebPage"]["kind"], + description: "The web page visited", + name: "visitedWebPage" satisfies ManualInferenceTriggerInputName, + array: false, + required: true, }, { - inputName: - "model" satisfies InputNameForAction<"inferEntitiesFromContent">, - kind: "step-output", - sourceStepId: "trigger", - sourceStepOutputName: - "model" satisfies ManualInferenceTriggerInputName, + payloadKind: + "VersionedUrl" satisfies ManualInferenceTriggerInputs["entityTypeIds"]["kind"], + description: "The ids of the entity types to create entities of", + name: "entityTypeIds" satisfies ManualInferenceTriggerInputName, + array: true, + required: true, }, - ], - }, - { - stepId: "1", - kind: "action", - actionDefinitionId: "persistEntities", - description: "Save proposed entities to database", - inputSources: [ { - inputName: - "proposedEntities" satisfies InputNameForAction<"persistEntities">, - kind: "step-output", - sourceStepId: "0", - sourceStepOutputName: - "proposedEntities" satisfies OutputNameForAction<"inferEntitiesFromContent">, + payloadKind: + "Text" satisfies ManualInferenceTriggerInputs["model"]["kind"], + description: "The model to use for inference", + name: "model" satisfies ManualInferenceTriggerInputName, + array: true, + required: true, }, { - inputName: "draft" satisfies InputNameForAction<"persistEntities">, - kind: "step-output", - sourceStepId: "trigger", - sourceStepOutputName: - "draft" satisfies ManualInferenceTriggerInputName, + payloadKind: + "Boolean" satisfies ManualInferenceTriggerInputs["draft"]["kind"], + description: + "Whether the entities should be created as drafts or not", + name: "draft" satisfies ManualInferenceTriggerInputName, + array: false, + required: true, }, ], }, - ], - outputs: [ - { - stepId: "1", - stepOutputName: - "persistedEntities" as const satisfies OutputNameForAction<"persistEntities">, - ...browserInferenceFlowOutput, - }, - ], -}; - -export const automaticBrowserInferenceFlowDefinition: FlowDefinition = { - name: "Auto-analyze webpage", - flowDefinitionId: "automatic-browser-inference" as EntityUuid, - description: - "Find entities in a web page according to the user's passive analysis settings", - trigger: { - kind: "trigger", - description: "Triggered automatically when the user visited a web page", - triggerDefinitionId: "userVisitedWebPageTrigger", + steps: [ + { + stepId: "0", + kind: "action", + actionDefinitionId: "inferEntitiesFromContent", + description: "Find entities in web page content", + inputSources: [ + { + inputName: + "content" satisfies InputNameForAiFlowAction<"inferEntitiesFromContent">, + kind: "step-output", + sourceStepId: "trigger", + sourceStepOutputName: + "visitedWebPage" satisfies ManualInferenceTriggerInputName, + }, + { + inputName: + "entityTypeIds" satisfies InputNameForAiFlowAction<"inferEntitiesFromContent">, + kind: "step-output", + sourceStepId: "trigger", + sourceStepOutputName: + "entityTypeIds" satisfies ManualInferenceTriggerInputName, + }, + { + inputName: + "model" satisfies InputNameForAiFlowAction<"inferEntitiesFromContent">, + kind: "step-output", + sourceStepId: "trigger", + sourceStepOutputName: + "model" satisfies ManualInferenceTriggerInputName, + }, + ], + }, + { + stepId: "1", + kind: "action", + actionDefinitionId: "persistEntities", + description: "Save proposed entities to database", + inputSources: [ + { + inputName: + "proposedEntities" satisfies InputNameForAiFlowAction<"persistEntities">, + kind: "step-output", + sourceStepId: "0", + sourceStepOutputName: + "proposedEntities" satisfies OutputNameForAiFlowAction<"inferEntitiesFromContent">, + }, + { + inputName: + "draft" satisfies InputNameForAiFlowAction<"persistEntities">, + kind: "step-output", + sourceStepId: "trigger", + sourceStepOutputName: + "draft" satisfies ManualInferenceTriggerInputName, + }, + ], + }, + ], outputs: [ { - payloadKind: - "WebPage" satisfies AutomaticInferenceTriggerInputs["visitedWebPage"]["kind"], - description: "The web page visited", - name: "visitedWebPage" satisfies AutomaticInferenceTriggerInputName, - array: false, - required: true, + stepId: "1", + stepOutputName: + "persistedEntities" as const satisfies OutputNameForAiFlowAction<"persistEntities">, + ...browserInferenceFlowOutput, }, ], - }, - steps: [ - { - stepId: "0", - kind: "action", - actionDefinitionId: "processAutomaticBrowsingSettings", - description: - "Decide which types of entity to find given the web page visited", - inputSources: [ - { - inputName: - "webPage" satisfies InputNameForAction<"processAutomaticBrowsingSettings">, - kind: "step-output", - sourceStepId: "trigger", - sourceStepOutputName: - "visitedWebPage" satisfies AutomaticInferenceTriggerInputName, - }, - ], - }, - { - stepId: "1", - kind: "action", - actionDefinitionId: "inferEntitiesFromContent", - description: "Infer entities from web page content", - inputSources: [ - { - inputName: - "content" satisfies InputNameForAction<"inferEntitiesFromContent">, - kind: "step-output", - sourceStepId: "trigger", - sourceStepOutputName: - "visitedWebPage" satisfies AutomaticInferenceTriggerInputName, - }, - { - inputName: - "model" satisfies InputNameForAction<"inferEntitiesFromContent">, - kind: "step-output", - sourceStepId: "0", - sourceStepOutputName: - "model" satisfies OutputNameForAction<"processAutomaticBrowsingSettings">, - }, - { - inputName: - "entityTypeIds" satisfies InputNameForAction<"inferEntitiesFromContent">, - kind: "step-output", - sourceStepId: "0", - sourceStepOutputName: - "entityTypeIds" satisfies OutputNameForAction<"processAutomaticBrowsingSettings">, - }, - ], - }, - { - stepId: "2", - kind: "action", - actionDefinitionId: "persistEntities", - description: "Save proposed entities to database", - inputSources: [ - { - inputName: - "proposedEntities" satisfies InputNameForAction<"persistEntities">, - kind: "step-output", - sourceStepId: "1", - sourceStepOutputName: - "proposedEntities" satisfies OutputNameForAction<"inferEntitiesFromContent">, - }, + }; + +export const automaticBrowserInferenceFlowDefinition: FlowDefinition = + { + name: "Auto-analyze webpage", + type: "ai", + flowDefinitionId: "automatic-browser-inference" as EntityUuid, + description: + "Find entities in a web page according to the user's passive analysis settings", + trigger: { + kind: "trigger", + description: "Triggered automatically when the user visited a web page", + triggerDefinitionId: "userVisitedWebPageTrigger", + outputs: [ { - inputName: "draft" satisfies InputNameForAction<"persistEntities">, - kind: "step-output", - sourceStepId: "0", - sourceStepOutputName: - "draft" satisfies OutputNameForAction<"processAutomaticBrowsingSettings">, + payloadKind: + "WebPage" satisfies AutomaticInferenceTriggerInputs["visitedWebPage"]["kind"], + description: "The web page visited", + name: "visitedWebPage" satisfies AutomaticInferenceTriggerInputName, + array: false, + required: true, }, ], }, - ], - outputs: [ - { - stepId: "2", - stepOutputName: - "persistedEntities" as const satisfies OutputNameForAction<"persistEntities">, - ...browserInferenceFlowOutput, - }, - ], -}; + steps: [ + { + stepId: "0", + kind: "action", + actionDefinitionId: "processAutomaticBrowsingSettings", + description: + "Decide which types of entity to find given the web page visited", + inputSources: [ + { + inputName: + "webPage" satisfies InputNameForAiFlowAction<"processAutomaticBrowsingSettings">, + kind: "step-output", + sourceStepId: "trigger", + sourceStepOutputName: + "visitedWebPage" satisfies AutomaticInferenceTriggerInputName, + }, + ], + }, + { + stepId: "1", + kind: "action", + actionDefinitionId: "inferEntitiesFromContent", + description: "Infer entities from web page content", + inputSources: [ + { + inputName: + "content" satisfies InputNameForAiFlowAction<"inferEntitiesFromContent">, + kind: "step-output", + sourceStepId: "trigger", + sourceStepOutputName: + "visitedWebPage" satisfies AutomaticInferenceTriggerInputName, + }, + { + inputName: + "model" satisfies InputNameForAiFlowAction<"inferEntitiesFromContent">, + kind: "step-output", + sourceStepId: "0", + sourceStepOutputName: + "model" satisfies OutputNameForAiFlowAction<"processAutomaticBrowsingSettings">, + }, + { + inputName: + "entityTypeIds" satisfies InputNameForAiFlowAction<"inferEntitiesFromContent">, + kind: "step-output", + sourceStepId: "0", + sourceStepOutputName: + "entityTypeIds" satisfies OutputNameForAiFlowAction<"processAutomaticBrowsingSettings">, + }, + ], + }, + { + stepId: "2", + kind: "action", + actionDefinitionId: "persistEntities", + description: "Save proposed entities to database", + inputSources: [ + { + inputName: + "proposedEntities" satisfies InputNameForAiFlowAction<"persistEntities">, + kind: "step-output", + sourceStepId: "1", + sourceStepOutputName: + "proposedEntities" satisfies OutputNameForAiFlowAction<"inferEntitiesFromContent">, + }, + { + inputName: + "draft" satisfies InputNameForAiFlowAction<"persistEntities">, + kind: "step-output", + sourceStepId: "0", + sourceStepOutputName: + "draft" satisfies OutputNameForAiFlowAction<"processAutomaticBrowsingSettings">, + }, + ], + }, + ], + outputs: [ + { + stepId: "2", + stepOutputName: + "persistedEntities" as const satisfies OutputNameForAiFlowAction<"persistEntities">, + ...browserInferenceFlowOutput, + }, + ], + }; diff --git a/libs/@local/hash-isomorphic-utils/src/flows/example-flow-definitions.ts b/libs/@local/hash-isomorphic-utils/src/flows/example-flow-definitions.ts index f4e45537187..6f08b6cada0 100644 --- a/libs/@local/hash-isomorphic-utils/src/flows/example-flow-definitions.ts +++ b/libs/@local/hash-isomorphic-utils/src/flows/example-flow-definitions.ts @@ -1,678 +1,702 @@ import type { EntityUuid } from "@blockprotocol/type-system"; import type { - InputNameForAction, - OutputNameForAction, + AiFlowActionDefinitionId, + InputNameForAiFlowAction, + OutputNameForAiFlowAction, } from "./action-definitions.js"; import type { FlowDefinition } from "./types.js"; -export const researchTaskFlowDefinition: FlowDefinition = { - name: "Research task", - flowDefinitionId: "research-task" as EntityUuid, - description: - "Conduct research on a given topic, and provide expert analysis on the discovered data", - trigger: { - triggerDefinitionId: "userTrigger", +export const researchTaskFlowDefinition: FlowDefinition = + { + name: "Research task", + type: "ai", + flowDefinitionId: "research-task" as EntityUuid, description: - "User provides research specification and entity types to discover", - kind: "trigger", - outputs: [ - { - payloadKind: "Text", - name: "Research guidance", - array: false, - required: true, - }, - { - payloadKind: "VersionedUrl", - name: "Entity Types", - array: true, - required: true, - }, - { - payloadKind: "Text", - name: "Research question", - array: false, - required: true, - }, - { - payloadKind: "GoogleAccountId", - name: "Google Account", - array: false, - required: true, - }, - { - payloadKind: "GoogleSheet", - name: "Google Sheet", - array: false, - required: true, - }, - { - payloadKind: "Boolean", - name: "Create as draft", - array: false, - required: true, - }, - ], - }, - groups: [ - { - groupId: 1, - description: "Research and persist entities", - }, - { - groupId: 2, - description: "Perform analysis and write to Google Sheet", - }, - ], - steps: [ - { - stepId: "1", - kind: "action", - groupId: 1, - actionDefinitionId: "researchEntities", + "Conduct research on a given topic, and provide expert analysis on the discovered data", + trigger: { + triggerDefinitionId: "userTrigger", description: - "Discover entities according to research specification, using public web sources", - inputSources: [ + "User provides research specification and entity types to discover", + kind: "trigger", + outputs: [ { - inputName: "prompt" satisfies InputNameForAction<"researchEntities">, - kind: "step-output", - sourceStepId: "trigger", - sourceStepOutputName: "Research guidance", + payloadKind: "Text", + name: "Research guidance", + array: false, + required: true, }, { - inputName: - "entityTypeIds" satisfies InputNameForAction<"researchEntities">, - kind: "step-output", - sourceStepId: "trigger", - sourceStepOutputName: "Entity Types", + payloadKind: "VersionedUrl", + name: "Entity Types", + array: true, + required: true, }, - ], - }, - { - stepId: "2", - kind: "action", - groupId: 1, - description: "Save discovered entities and relationships to HASH graph", - actionDefinitionId: "persistEntities", - inputSources: [ { - inputName: - "proposedEntities" satisfies InputNameForAction<"persistEntities">, - kind: "step-output", - sourceStepId: "1", - sourceStepOutputName: - "proposedEntities" satisfies OutputNameForAction<"researchEntities">, + payloadKind: "Text", + name: "Research question", + array: false, + required: true, }, { - inputName: "draft" satisfies InputNameForAction<"persistEntities">, - kind: "step-output", - sourceStepId: "trigger", - sourceStepOutputName: "Create as draft", + payloadKind: "GoogleAccountId", + name: "Google Account", + array: false, + required: true, }, - ], - }, - { - stepId: "3", - groupId: 2, - kind: "action", - actionDefinitionId: "answerQuestion", - description: "Answer user's question using discovered entities", - inputSources: [ { - inputName: "question" satisfies InputNameForAction<"answerQuestion">, - kind: "step-output", - sourceStepId: "trigger", - sourceStepOutputName: "Research question", + payloadKind: "GoogleSheet", + name: "Google Sheet", + array: false, + required: true, }, { - inputName: "entities" satisfies InputNameForAction<"answerQuestion">, - kind: "step-output", - sourceStepId: "2", - sourceStepOutputName: - "persistedEntities" satisfies OutputNameForAction<"persistEntities">, + payloadKind: "Boolean", + name: "Create as draft", + array: false, + required: true, }, ], }, - { - stepId: "4", - groupId: 2, - kind: "action", - actionDefinitionId: "writeGoogleSheet", - description: "Save CSV to Google Sheet", - inputSources: [ - { - inputName: - "audience" satisfies InputNameForAction<"writeGoogleSheet">, - kind: "hardcoded", - payload: { - kind: "ActorType", - value: "user", + groups: [ + { + groupId: 1, + description: "Research and persist entities", + }, + { + groupId: 2, + description: "Perform analysis and write to Google Sheet", + }, + ], + steps: [ + { + stepId: "1", + kind: "action", + groupId: 1, + actionDefinitionId: "researchEntities", + description: + "Discover entities according to research specification, using public web sources", + inputSources: [ + { + inputName: + "prompt" satisfies InputNameForAiFlowAction<"researchEntities">, + kind: "step-output", + sourceStepId: "trigger", + sourceStepOutputName: "Research guidance", }, - }, - { - inputName: - "googleAccountId" satisfies InputNameForAction<"writeGoogleSheet">, - kind: "step-output", - sourceStepId: "trigger", - sourceStepOutputName: "Google Account", - }, - { - inputName: - "googleSheet" satisfies InputNameForAction<"writeGoogleSheet">, - kind: "step-output", - sourceStepId: "trigger", - sourceStepOutputName: "Google Sheet", - }, - { - inputName: - "dataToWrite" satisfies InputNameForAction<"writeGoogleSheet">, - kind: "step-output", - sourceStepId: "3", - sourceStepOutputName: - "answer" satisfies OutputNameForAction<"answerQuestion">, - }, - ], - }, - ], - outputs: [ - { - stepId: "3", - stepOutputName: "answer" satisfies OutputNameForAction<"answerQuestion">, - payloadKind: "Text", - name: "answer" as const, - array: false, - required: true, - }, - { - stepId: "4", - stepOutputName: - "googleSheetEntity" satisfies OutputNameForAction<"writeGoogleSheet">, - payloadKind: "Entity", - name: "googleSheetEntity" as const, - array: false, - required: true, - }, - ], -}; - -export const researchEntitiesFlowDefinition: FlowDefinition = { - name: "Research entities", - flowDefinitionId: "research-entities" as EntityUuid, - description: - "Discover entities according to a research brief, save them to HASH and Google Sheets", - trigger: { - triggerDefinitionId: "userTrigger", - description: - "User provides research specification and entity types to discover", - kind: "trigger", - outputs: [ + { + inputName: + "entityTypeIds" satisfies InputNameForAiFlowAction<"researchEntities">, + kind: "step-output", + sourceStepId: "trigger", + sourceStepOutputName: "Entity Types", + }, + ], + }, { - payloadKind: "Text", - name: "Research guidance", - array: false, - required: true, + stepId: "2", + kind: "action", + groupId: 1, + description: "Save discovered entities and relationships to HASH graph", + actionDefinitionId: "persistEntities", + inputSources: [ + { + inputName: + "proposedEntities" satisfies InputNameForAiFlowAction<"persistEntities">, + kind: "step-output", + sourceStepId: "1", + sourceStepOutputName: + "proposedEntities" satisfies OutputNameForAiFlowAction<"researchEntities">, + }, + { + inputName: + "draft" satisfies InputNameForAiFlowAction<"persistEntities">, + kind: "step-output", + sourceStepId: "trigger", + sourceStepOutputName: "Create as draft", + }, + ], }, { - payloadKind: "VersionedUrl", - name: "Entity Types", - array: true, - required: true, + stepId: "3", + groupId: 2, + kind: "action", + actionDefinitionId: "answerQuestion", + description: "Answer user's question using discovered entities", + inputSources: [ + { + inputName: + "question" satisfies InputNameForAiFlowAction<"answerQuestion">, + kind: "step-output", + sourceStepId: "trigger", + sourceStepOutputName: "Research question", + }, + { + inputName: + "entities" satisfies InputNameForAiFlowAction<"answerQuestion">, + kind: "step-output", + sourceStepId: "2", + sourceStepOutputName: + "persistedEntities" satisfies OutputNameForAiFlowAction<"persistEntities">, + }, + ], }, { - payloadKind: "GoogleAccountId", - name: "Google Account", - array: false, - required: true, + stepId: "4", + groupId: 2, + kind: "action", + actionDefinitionId: "writeGoogleSheet", + description: "Save CSV to Google Sheet", + inputSources: [ + { + inputName: + "audience" satisfies InputNameForAiFlowAction<"writeGoogleSheet">, + kind: "hardcoded", + payload: { + kind: "ActorType", + value: "user", + }, + }, + { + inputName: + "googleAccountId" satisfies InputNameForAiFlowAction<"writeGoogleSheet">, + kind: "step-output", + sourceStepId: "trigger", + sourceStepOutputName: "Google Account", + }, + { + inputName: + "googleSheet" satisfies InputNameForAiFlowAction<"writeGoogleSheet">, + kind: "step-output", + sourceStepId: "trigger", + sourceStepOutputName: "Google Sheet", + }, + { + inputName: + "dataToWrite" satisfies InputNameForAiFlowAction<"writeGoogleSheet">, + kind: "step-output", + sourceStepId: "3", + sourceStepOutputName: + "answer" satisfies OutputNameForAiFlowAction<"answerQuestion">, + }, + ], }, + ], + outputs: [ { - payloadKind: "GoogleSheet", - name: "Google Sheet", + stepId: "3", + stepOutputName: + "answer" satisfies OutputNameForAiFlowAction<"answerQuestion">, + payloadKind: "Text", + name: "answer" as const, array: false, required: true, }, { - payloadKind: "Boolean", - name: "Create as draft", + stepId: "4", + stepOutputName: + "googleSheetEntity" satisfies OutputNameForAiFlowAction<"writeGoogleSheet">, + payloadKind: "Entity", + name: "googleSheetEntity" as const, array: false, required: true, }, ], - }, - groups: [ - { - groupId: 1, - description: "Research and persist entities", - }, - { - groupId: 2, - description: "Save data to Google Sheet", - }, - ], - steps: [ - { - stepId: "1", - kind: "action", - groupId: 1, - actionDefinitionId: "researchEntities", + }; + +export const researchEntitiesFlowDefinition: FlowDefinition = + { + name: "Research entities", + type: "ai", + flowDefinitionId: "research-entities" as EntityUuid, + description: + "Discover entities according to a research brief, save them to HASH and Google Sheets", + trigger: { + triggerDefinitionId: "userTrigger", description: - "Discover entities according to research specification, using public web sources", - inputSources: [ + "User provides research specification and entity types to discover", + kind: "trigger", + outputs: [ { - inputName: "prompt" satisfies InputNameForAction<"researchEntities">, - kind: "step-output", - sourceStepId: "trigger", - sourceStepOutputName: "Research guidance", + payloadKind: "Text", + name: "Research guidance", + array: false, + required: true, }, { - inputName: - "entityTypeIds" satisfies InputNameForAction<"researchEntities">, - kind: "step-output", - sourceStepId: "trigger", - sourceStepOutputName: "Entity Types", - }, - ], - }, - { - stepId: "2", - kind: "action", - groupId: 1, - description: "Save discovered entities and relationships to HASH graph", - actionDefinitionId: "persistEntities", - inputSources: [ - { - inputName: - "proposedEntities" satisfies InputNameForAction<"persistEntities">, - kind: "step-output", - sourceStepId: "1", - sourceStepOutputName: - "proposedEntities" satisfies OutputNameForAction<"researchEntities">, - }, - { - inputName: "draft" satisfies InputNameForAction<"persistEntities">, - kind: "step-output", - sourceStepId: "trigger", - sourceStepOutputName: "Create as draft", - }, - ], - }, - { - stepId: "3", - groupId: 2, - kind: "action", - actionDefinitionId: "writeGoogleSheet", - description: "Save discovered entities to Google Sheet", - inputSources: [ - { - inputName: - "audience" satisfies InputNameForAction<"writeGoogleSheet">, - kind: "hardcoded", - payload: { - kind: "ActorType", - value: "user", - }, + payloadKind: "VersionedUrl", + name: "Entity Types", + array: true, + required: true, }, { - inputName: - "googleAccountId" satisfies InputNameForAction<"writeGoogleSheet">, - kind: "step-output", - sourceStepId: "trigger", - sourceStepOutputName: "Google Account", + payloadKind: "GoogleAccountId", + name: "Google Account", + array: false, + required: true, }, { - inputName: - "googleSheet" satisfies InputNameForAction<"writeGoogleSheet">, - kind: "step-output", - sourceStepId: "trigger", - sourceStepOutputName: "Google Sheet", + payloadKind: "GoogleSheet", + name: "Google Sheet", + array: false, + required: true, }, { - inputName: - "dataToWrite" satisfies InputNameForAction<"writeGoogleSheet">, - kind: "step-output", - sourceStepId: "2", - sourceStepOutputName: - "persistedEntities" satisfies OutputNameForAction<"persistEntities">, + payloadKind: "Boolean", + name: "Create as draft", + array: false, + required: true, }, ], }, - ], - outputs: [ - { - stepId: "3", - stepOutputName: - "googleSheetEntity" satisfies OutputNameForAction<"writeGoogleSheet">, - payloadKind: "PersistedEntity", - name: "googleSheetEntity" as const, - array: false, - required: true, - }, - ], -}; - -export const ftseInvestorsFlowDefinition: FlowDefinition = { - name: "FTSE350 investors", - flowDefinitionId: "ftse-350-investors" as EntityUuid, - description: - "Research the FTSE350 index, its constituents, and the top investors in the index", - trigger: { - triggerDefinitionId: "userTrigger", - description: - "User chooses the web to output data to, and whether entities should be created as draft", - kind: "trigger", + groups: [ + { + groupId: 1, + description: "Research and persist entities", + }, + { + groupId: 2, + description: "Save data to Google Sheet", + }, + ], + steps: [ + { + stepId: "1", + kind: "action", + groupId: 1, + actionDefinitionId: "researchEntities", + description: + "Discover entities according to research specification, using public web sources", + inputSources: [ + { + inputName: + "prompt" satisfies InputNameForAiFlowAction<"researchEntities">, + kind: "step-output", + sourceStepId: "trigger", + sourceStepOutputName: "Research guidance", + }, + { + inputName: + "entityTypeIds" satisfies InputNameForAiFlowAction<"researchEntities">, + kind: "step-output", + sourceStepId: "trigger", + sourceStepOutputName: "Entity Types", + }, + ], + }, + { + stepId: "2", + kind: "action", + groupId: 1, + description: "Save discovered entities and relationships to HASH graph", + actionDefinitionId: "persistEntities", + inputSources: [ + { + inputName: + "proposedEntities" satisfies InputNameForAiFlowAction<"persistEntities">, + kind: "step-output", + sourceStepId: "1", + sourceStepOutputName: + "proposedEntities" satisfies OutputNameForAiFlowAction<"researchEntities">, + }, + { + inputName: + "draft" satisfies InputNameForAiFlowAction<"persistEntities">, + kind: "step-output", + sourceStepId: "trigger", + sourceStepOutputName: "Create as draft", + }, + ], + }, + { + stepId: "3", + groupId: 2, + kind: "action", + actionDefinitionId: "writeGoogleSheet", + description: "Save discovered entities to Google Sheet", + inputSources: [ + { + inputName: + "audience" satisfies InputNameForAiFlowAction<"writeGoogleSheet">, + kind: "hardcoded", + payload: { + kind: "ActorType", + value: "user", + }, + }, + { + inputName: + "googleAccountId" satisfies InputNameForAiFlowAction<"writeGoogleSheet">, + kind: "step-output", + sourceStepId: "trigger", + sourceStepOutputName: "Google Account", + }, + { + inputName: + "googleSheet" satisfies InputNameForAiFlowAction<"writeGoogleSheet">, + kind: "step-output", + sourceStepId: "trigger", + sourceStepOutputName: "Google Sheet", + }, + { + inputName: + "dataToWrite" satisfies InputNameForAiFlowAction<"writeGoogleSheet">, + kind: "step-output", + sourceStepId: "2", + sourceStepOutputName: + "persistedEntities" satisfies OutputNameForAiFlowAction<"persistEntities">, + }, + ], + }, + ], outputs: [ { - payloadKind: "Boolean", - name: "draft", + stepId: "3", + stepOutputName: + "googleSheetEntity" satisfies OutputNameForAiFlowAction<"writeGoogleSheet">, + payloadKind: "PersistedEntity", + name: "googleSheetEntity" as const, array: false, - required: false, + required: true, }, ], - }, - groups: [ - { - groupId: 1, - description: "Research FTSE350 constituents", - }, - { - groupId: 2, - description: "Research investments in the FTSE350", - }, - { - groupId: 3, - description: "Calculate top investors", - }, - ], - steps: [ - { - stepId: "1", - groupId: 1, - kind: "action", - actionDefinitionId: "researchEntities", - description: "Research the constituents of the FTSE350 index", - inputSources: [ - { - inputName: "prompt" satisfies InputNameForAction<"researchEntities">, - kind: "hardcoded", - payload: { - kind: "Text", - value: "Find the constituents in the FTSE350 index", - }, - }, + }; + +export const ftseInvestorsFlowDefinition: FlowDefinition = + { + name: "FTSE350 investors", + type: "ai", + flowDefinitionId: "ftse-350-investors" as EntityUuid, + description: + "Research the FTSE350 index, its constituents, and the top investors in the index", + trigger: { + triggerDefinitionId: "userTrigger", + description: + "User chooses the web to output data to, and whether entities should be created as draft", + kind: "trigger", + outputs: [ { - inputName: - "entityTypeIds" satisfies InputNameForAction<"researchEntities">, - kind: "hardcoded", - payload: { - kind: "VersionedUrl", - value: [ - "https://hash.ai/@h/types/entity-type/stock-market-constituent/v/1", - "https://hash.ai/@h/types/entity-type/stock-market-index/v/1", - ], - }, + payloadKind: "Boolean", + name: "draft", + array: false, + required: false, }, ], }, - { - stepId: "2", - groupId: 1, - kind: "action", - description: "Save discovered members of the FTSE350 to HASH graph", - actionDefinitionId: "persistEntities", - inputSources: [ - { - inputName: - "proposedEntities" satisfies InputNameForAction<"persistEntities">, + groups: [ + { + groupId: 1, + description: "Research FTSE350 constituents", + }, + { + groupId: 2, + description: "Research investments in the FTSE350", + }, + { + groupId: 3, + description: "Calculate top investors", + }, + ], + steps: [ + { + stepId: "1", + groupId: 1, + kind: "action", + actionDefinitionId: "researchEntities", + description: "Research the constituents of the FTSE350 index", + inputSources: [ + { + inputName: + "prompt" satisfies InputNameForAiFlowAction<"researchEntities">, + kind: "hardcoded", + payload: { + kind: "Text", + value: "Find the constituents in the FTSE350 index", + }, + }, + { + inputName: + "entityTypeIds" satisfies InputNameForAiFlowAction<"researchEntities">, + kind: "hardcoded", + payload: { + kind: "VersionedUrl", + value: [ + "https://hash.ai/@h/types/entity-type/stock-market-constituent/v/1", + "https://hash.ai/@h/types/entity-type/stock-market-index/v/1", + ], + }, + }, + ], + }, + { + stepId: "2", + groupId: 1, + kind: "action", + description: "Save discovered members of the FTSE350 to HASH graph", + actionDefinitionId: "persistEntities", + inputSources: [ + { + inputName: + "proposedEntities" satisfies InputNameForAiFlowAction<"persistEntities">, + kind: "step-output", + sourceStepId: "1", + sourceStepOutputName: + "proposedEntities" satisfies OutputNameForAiFlowAction<"researchEntities">, + }, + { + inputName: + "draft" satisfies InputNameForAiFlowAction<"persistEntities">, + kind: "step-output", + sourceStepId: "trigger", + sourceStepOutputName: "draft", + }, + ], + }, + { + stepId: "3", + groupId: 2, + kind: "parallel-group", + description: "Research investors and investments in FTSE350 companies", + inputSourceToParallelizeOn: { + inputName: "existingEntities", kind: "step-output", - sourceStepId: "1", + sourceStepId: "2", sourceStepOutputName: - "proposedEntities" satisfies OutputNameForAction<"researchEntities">, - }, - { - inputName: "draft" satisfies InputNameForAction<"persistEntities">, - kind: "step-output", - sourceStepId: "trigger", - sourceStepOutputName: "draft", - }, - ], - }, - { - stepId: "3", - groupId: 2, - kind: "parallel-group", - description: "Research investors and investments in FTSE350 companies", - inputSourceToParallelizeOn: { - inputName: "existingEntities", - kind: "step-output", - sourceStepId: "2", - sourceStepOutputName: - "persistedEntities" satisfies OutputNameForAction<"persistEntities">, - }, - steps: [ - { - stepId: "3.1", - groupId: 2, - kind: "action", - actionDefinitionId: "researchEntities", - description: - "Research investors and investments in a FTSE350 company", - inputSources: [ - { - inputName: - "prompt" satisfies InputNameForAction<"researchEntities">, - kind: "hardcoded", - payload: { - kind: "Text", - value: - "Find the investors in the provided FTSE350 constituent, and their investments in that company", + "persistedEntities" satisfies OutputNameForAiFlowAction<"persistEntities">, + }, + steps: [ + { + stepId: "3.1", + groupId: 2, + kind: "action", + actionDefinitionId: "researchEntities", + description: + "Research investors and investments in a FTSE350 company", + inputSources: [ + { + inputName: + "prompt" satisfies InputNameForAiFlowAction<"researchEntities">, + kind: "hardcoded", + payload: { + kind: "Text", + value: + "Find the investors in the provided FTSE350 constituent, and their investments in that company", + }, }, - }, - { - inputName: - "entityTypeIds" satisfies InputNameForAction<"researchEntities">, - kind: "hardcoded", - payload: { - kind: "VersionedUrl", - value: [ - "https://hash.ai/@h/types/entity-type/invested-in/v/1", - "https://hash.ai/@h/types/entity-type/investment-fund/v/1", - "https://hash.ai/@h/types/entity-type/company/v/1", - ], + { + inputName: + "entityTypeIds" satisfies InputNameForAiFlowAction<"researchEntities">, + kind: "hardcoded", + payload: { + kind: "VersionedUrl", + value: [ + "https://hash.ai/@h/types/entity-type/invested-in/v/1", + "https://hash.ai/@h/types/entity-type/investment-fund/v/1", + "https://hash.ai/@h/types/entity-type/company/v/1", + ], + }, }, - }, - { - inputName: - "existingEntities" satisfies InputNameForAction<"researchEntities">, - kind: "parallel-group-input", - }, - ], - }, - { + { + inputName: + "existingEntities" satisfies InputNameForAiFlowAction<"researchEntities">, + kind: "parallel-group-input", + }, + ], + }, + { + stepId: "3.2", + groupId: 2, + kind: "action", + description: + "Save discovered FTSE350 investors and their investments to HASH graph", + actionDefinitionId: "persistEntities", + inputSources: [ + { + inputName: + "proposedEntities" satisfies InputNameForAiFlowAction<"persistEntities">, + kind: "step-output", + sourceStepId: "3.1", + sourceStepOutputName: + "proposedEntities" satisfies OutputNameForAiFlowAction<"researchEntities">, + }, + { + inputName: + "draft" satisfies InputNameForAiFlowAction<"persistEntities">, + kind: "step-output", + sourceStepId: "trigger", + sourceStepOutputName: "draft", + }, + ], + }, + ], + aggregateOutput: { stepId: "3.2", - groupId: 2, - kind: "action", - description: - "Save discovered FTSE350 investors and their investments to HASH graph", - actionDefinitionId: "persistEntities", - inputSources: [ - { - inputName: - "proposedEntities" satisfies InputNameForAction<"persistEntities">, - kind: "step-output", - sourceStepId: "3.1", - sourceStepOutputName: - "proposedEntities" satisfies OutputNameForAction<"researchEntities">, - }, - { - inputName: - "draft" satisfies InputNameForAction<"persistEntities">, - kind: "step-output", - sourceStepId: "trigger", - sourceStepOutputName: "draft", - }, - ], + stepOutputName: + "persistedEntities" satisfies OutputNameForAiFlowAction<"persistEntities">, + required: true, + name: "persistedEntities" as const, + payloadKind: "PersistedEntity", + array: true, }, - ], - aggregateOutput: { - stepId: "3.2", - stepOutputName: - "persistedEntities" satisfies OutputNameForAction<"persistEntities">, - required: true, - name: "persistedEntities" as const, - payloadKind: "PersistedEntity", - array: true, }, - }, - { - stepId: "4", - groupId: 3, - kind: "action", - actionDefinitionId: "answerQuestion", - description: - "Calculate the top 10 investors in the FTSE350 by market cap", - inputSources: [ - { - inputName: "question" satisfies InputNameForAction<"answerQuestion">, - kind: "step-output", - sourceStepId: "trigger", - sourceStepOutputName: "question", - }, - { - inputName: "entities" satisfies InputNameForAction<"answerQuestion">, - kind: "step-output", - sourceStepId: "3", - sourceStepOutputName: - "persistedEntities" satisfies OutputNameForAction<"persistEntities">, - }, - ], - }, - ], - outputs: [ - { - stepId: "3", - stepOutputName: - "persistedEntities" satisfies OutputNameForAction<"persistEntities">, - name: "persistedEntities" as const, - payloadKind: "PersistedEntities", - array: false, - required: true, - }, - { - stepId: "4", - stepOutputName: "answer" satisfies OutputNameForAction<"answerQuestion">, - payloadKind: "Text", - name: "answer" as const, - array: false, - required: true, - }, - ], -}; - -export const inferUserEntitiesFromWebPageFlowDefinition: FlowDefinition = { - name: "Analyze webpage entities", - flowDefinitionId: "infer-user-entities-from-web-page" as EntityUuid, - description: - "Infer entities from a web page, based on the user's provided entity types", - trigger: { - kind: "trigger", - description: "Triggered when user visits a web page", - triggerDefinitionId: "userTrigger", - outputs: [ { - payloadKind: "Text", - name: "visitedWebPageUrl", - array: false, - required: true, + stepId: "4", + groupId: 3, + kind: "action", + actionDefinitionId: "answerQuestion", + description: + "Calculate the top 10 investors in the FTSE350 by market cap", + inputSources: [ + { + inputName: + "question" satisfies InputNameForAiFlowAction<"answerQuestion">, + kind: "step-output", + sourceStepId: "trigger", + sourceStepOutputName: "question", + }, + { + inputName: + "entities" satisfies InputNameForAiFlowAction<"answerQuestion">, + kind: "step-output", + sourceStepId: "3", + sourceStepOutputName: + "persistedEntities" satisfies OutputNameForAiFlowAction<"persistEntities">, + }, + ], }, + ], + outputs: [ { - payloadKind: "VersionedUrl", - name: "entityTypeIds", - array: true, + stepId: "3", + stepOutputName: + "persistedEntities" satisfies OutputNameForAiFlowAction<"persistEntities">, + name: "persistedEntities" as const, + payloadKind: "PersistedEntities", + array: false, required: true, }, { - payloadKind: "Boolean", - name: "draft", + stepId: "4", + stepOutputName: + "answer" satisfies OutputNameForAiFlowAction<"answerQuestion">, + payloadKind: "Text", + name: "answer" as const, array: false, required: true, }, ], - }, - steps: [ - { - stepId: "0", - kind: "action", - actionDefinitionId: "getWebPageByUrl", - description: "Retrieve web page content from URL", - inputSources: [ - { - inputName: "url" satisfies InputNameForAction<"getWebPageByUrl">, - kind: "step-output", - sourceStepId: "trigger", - sourceStepOutputName: "visitedWebPageUrl", - }, - ], - }, - { - stepId: "1", - kind: "action", - actionDefinitionId: "inferEntitiesFromContent", - description: "Infer entities from web page content", - inputSources: [ + }; + +export const inferUserEntitiesFromWebPageFlowDefinition: FlowDefinition = + { + name: "Analyze webpage entities", + type: "ai", + flowDefinitionId: "infer-user-entities-from-web-page" as EntityUuid, + description: + "Infer entities from a web page, based on the user's provided entity types", + trigger: { + kind: "trigger", + description: "Triggered when user visits a web page", + triggerDefinitionId: "userTrigger", + outputs: [ { - inputName: - "content" satisfies InputNameForAction<"inferEntitiesFromContent">, - kind: "step-output", - sourceStepId: "0", - sourceStepOutputName: - "webPage" satisfies OutputNameForAction<"getWebPageByUrl">, + payloadKind: "Text", + name: "visitedWebPageUrl", + array: false, + required: true, }, { - inputName: - "entityTypeIds" satisfies InputNameForAction<"inferEntitiesFromContent">, - kind: "step-output", - sourceStepId: "trigger", - sourceStepOutputName: "entityTypeIds", - }, - ], - }, - { - stepId: "2", - kind: "action", - actionDefinitionId: "persistEntities", - description: "Save proposed entities to database", - inputSources: [ - { - inputName: - "proposedEntities" satisfies InputNameForAction<"persistEntities">, - kind: "step-output", - sourceStepId: "1", - sourceStepOutputName: - "proposedEntities" satisfies OutputNameForAction<"inferEntitiesFromContent">, + payloadKind: "VersionedUrl", + name: "entityTypeIds", + array: true, + required: true, }, { - inputName: "draft" satisfies InputNameForAction<"persistEntities">, - kind: "step-output", - sourceStepId: "trigger", - sourceStepOutputName: "draft", + payloadKind: "Boolean", + name: "draft", + array: false, + required: true, }, ], }, - ], - outputs: [ - { - stepId: "2", - stepOutputName: "persistedEntities", - name: "persistedEntities" as const, - payloadKind: "PersistedEntity", - array: true, - required: true, - }, - ], -}; + steps: [ + { + stepId: "0", + kind: "action", + actionDefinitionId: "getWebPageByUrl", + description: "Retrieve web page content from URL", + inputSources: [ + { + inputName: + "url" satisfies InputNameForAiFlowAction<"getWebPageByUrl">, + kind: "step-output", + sourceStepId: "trigger", + sourceStepOutputName: "visitedWebPageUrl", + }, + ], + }, + { + stepId: "1", + kind: "action", + actionDefinitionId: "inferEntitiesFromContent", + description: "Infer entities from web page content", + inputSources: [ + { + inputName: + "content" satisfies InputNameForAiFlowAction<"inferEntitiesFromContent">, + kind: "step-output", + sourceStepId: "0", + sourceStepOutputName: + "webPage" satisfies OutputNameForAiFlowAction<"getWebPageByUrl">, + }, + { + inputName: + "entityTypeIds" satisfies InputNameForAiFlowAction<"inferEntitiesFromContent">, + kind: "step-output", + sourceStepId: "trigger", + sourceStepOutputName: "entityTypeIds", + }, + ], + }, + { + stepId: "2", + kind: "action", + actionDefinitionId: "persistEntities", + description: "Save proposed entities to database", + inputSources: [ + { + inputName: + "proposedEntities" satisfies InputNameForAiFlowAction<"persistEntities">, + kind: "step-output", + sourceStepId: "1", + sourceStepOutputName: + "proposedEntities" satisfies OutputNameForAiFlowAction<"inferEntitiesFromContent">, + }, + { + inputName: + "draft" satisfies InputNameForAiFlowAction<"persistEntities">, + kind: "step-output", + sourceStepId: "trigger", + sourceStepOutputName: "draft", + }, + ], + }, + ], + outputs: [ + { + stepId: "2", + stepOutputName: "persistedEntities", + name: "persistedEntities" as const, + payloadKind: "PersistedEntity", + array: true, + required: true, + }, + ], + }; -export const answerQuestionFlow: FlowDefinition = { +export const answerQuestionFlow: FlowDefinition = { name: "Answer question", + type: "ai", flowDefinitionId: "answer-question-flow" as EntityUuid, description: "Answer a question based on the provided context", trigger: { @@ -720,13 +744,15 @@ export const answerQuestionFlow: FlowDefinition = { description: "Answer question on the provided context", inputSources: [ { - inputName: "question" satisfies InputNameForAction<"answerQuestion">, + inputName: + "question" satisfies InputNameForAiFlowAction<"answerQuestion">, kind: "step-output", sourceStepId: "trigger", sourceStepOutputName: "question", }, { - inputName: "context" satisfies InputNameForAction<"answerQuestion">, + inputName: + "context" satisfies InputNameForAiFlowAction<"answerQuestion">, kind: "step-output", sourceStepId: "trigger", sourceStepOutputName: "context", @@ -741,7 +767,7 @@ export const answerQuestionFlow: FlowDefinition = { inputSources: [ { inputName: - "audience" satisfies InputNameForAction<"writeGoogleSheet">, + "audience" satisfies InputNameForAiFlowAction<"writeGoogleSheet">, kind: "hardcoded", payload: { kind: "ActorType", @@ -750,25 +776,25 @@ export const answerQuestionFlow: FlowDefinition = { }, { inputName: - "googleAccountId" satisfies InputNameForAction<"writeGoogleSheet">, + "googleAccountId" satisfies InputNameForAiFlowAction<"writeGoogleSheet">, kind: "step-output", sourceStepId: "trigger", sourceStepOutputName: "Google Account", }, { inputName: - "googleSheet" satisfies InputNameForAction<"writeGoogleSheet">, + "googleSheet" satisfies InputNameForAiFlowAction<"writeGoogleSheet">, kind: "step-output", sourceStepId: "trigger", sourceStepOutputName: "Google Sheet", }, { inputName: - "dataToWrite" satisfies InputNameForAction<"writeGoogleSheet">, + "dataToWrite" satisfies InputNameForAiFlowAction<"writeGoogleSheet">, kind: "step-output", sourceStepId: "1", sourceStepOutputName: - "answer" satisfies OutputNameForAction<"answerQuestion">, + "answer" satisfies OutputNameForAiFlowAction<"answerQuestion">, }, ], }, @@ -809,7 +835,7 @@ export const answerQuestionFlow: FlowDefinition = { { stepId: "2", stepOutputName: - "googleSheetEntity" satisfies OutputNameForAction<"writeGoogleSheet">, + "googleSheetEntity" satisfies OutputNameForAiFlowAction<"writeGoogleSheet">, payloadKind: "PersistedEntity", name: "googleSheetEntity" as const, array: false, @@ -818,8 +844,9 @@ export const answerQuestionFlow: FlowDefinition = { ], }; -export const saveFileFromUrl: FlowDefinition = { +export const saveFileFromUrl: FlowDefinition = { name: "Save file from URL", + type: "ai", flowDefinitionId: "save-file-from-url" as EntityUuid, description: "Save file from URL to HASH", trigger: { @@ -856,21 +883,21 @@ export const saveFileFromUrl: FlowDefinition = { "Retrieve file from URL, mirror into HASH and create associated entity", inputSources: [ { - inputName: "url" satisfies InputNameForAction<"getFileFromUrl">, + inputName: "url" satisfies InputNameForAiFlowAction<"getFileFromUrl">, kind: "step-output", sourceStepId: "trigger", sourceStepOutputName: "url", }, { inputName: - "description" satisfies InputNameForAction<"getFileFromUrl">, + "description" satisfies InputNameForAiFlowAction<"getFileFromUrl">, kind: "step-output", sourceStepId: "trigger", sourceStepOutputName: "description", }, { inputName: - "displayName" satisfies InputNameForAction<"getFileFromUrl">, + "displayName" satisfies InputNameForAiFlowAction<"getFileFromUrl">, kind: "step-output", sourceStepId: "trigger", sourceStepOutputName: "displayName", diff --git a/libs/@local/hash-isomorphic-utils/src/flows/file-flow-definitions.ts b/libs/@local/hash-isomorphic-utils/src/flows/file-flow-definitions.ts index a8128399b02..6a0ef53f83a 100644 --- a/libs/@local/hash-isomorphic-utils/src/flows/file-flow-definitions.ts +++ b/libs/@local/hash-isomorphic-utils/src/flows/file-flow-definitions.ts @@ -1,8 +1,9 @@ import type { EntityUuid } from "@blockprotocol/type-system"; import type { - InputNameForAction, - OutputNameForAction, + AiFlowActionDefinitionId, + InputNameForAiFlowAction, + OutputNameForAiFlowAction, } from "./action-definitions.js"; import type { FlowDefinition } from "./types.js"; @@ -91,66 +92,68 @@ import type { FlowDefinition } from "./types.js"; * ) * } */ -export const inferMetadataFromDocumentFlowDefinition: FlowDefinition = { - name: "Infer metadata from document", - flowDefinitionId: "infer-metadata-from-document" as EntityUuid, - description: - "Infer metadata from a document, assign appropriate type to document, and create associated entities.", - trigger: { - kind: "trigger", - description: "Triggered when user visits a web page", - triggerDefinitionId: "onFileUpload", +export const inferMetadataFromDocumentFlowDefinition: FlowDefinition = + { + name: "Infer metadata from document", + type: "ai", + flowDefinitionId: "infer-metadata-from-document" as EntityUuid, + description: + "Infer metadata from a document, assign appropriate type to document, and create associated entities.", + trigger: { + kind: "trigger", + description: "Triggered when user visits a web page", + triggerDefinitionId: "onFileUpload", + outputs: [ + { + payloadKind: "EntityId", + name: "fileEntityId", + array: false, + required: true, + }, + ], + }, + steps: [ + { + stepId: "1", + kind: "action", + actionDefinitionId: "inferMetadataFromDocument", + description: + "Infer metadata from document, assign appropriate type, propose associated entities", + inputSources: [ + { + inputName: + "documentEntityId" satisfies InputNameForAiFlowAction<"inferMetadataFromDocument">, + kind: "step-output", + sourceStepId: "trigger", + sourceStepOutputName: "fileEntityId", + }, + ], + }, + { + stepId: "2", + kind: "action", + actionDefinitionId: "persistEntities", + description: "Save proposed entities to database", + inputSources: [ + { + inputName: + "proposedEntities" satisfies InputNameForAiFlowAction<"persistEntities">, + kind: "step-output", + sourceStepId: "1", + sourceStepOutputName: + "proposedEntities" satisfies OutputNameForAiFlowAction<"inferMetadataFromDocument">, + }, + ], + }, + ], outputs: [ { - payloadKind: "EntityId", - name: "fileEntityId", - array: false, + stepId: "2", + stepOutputName: "persistedEntities", + name: "persistedEntities" as const, + payloadKind: "PersistedEntity", + array: true, required: true, }, ], - }, - steps: [ - { - stepId: "1", - kind: "action", - actionDefinitionId: "inferMetadataFromDocument", - description: - "Infer metadata from document, assign appropriate type, propose associated entities", - inputSources: [ - { - inputName: - "documentEntityId" satisfies InputNameForAction<"inferMetadataFromDocument">, - kind: "step-output", - sourceStepId: "trigger", - sourceStepOutputName: "fileEntityId", - }, - ], - }, - { - stepId: "2", - kind: "action", - actionDefinitionId: "persistEntities", - description: "Save proposed entities to database", - inputSources: [ - { - inputName: - "proposedEntities" satisfies InputNameForAction<"persistEntities">, - kind: "step-output", - sourceStepId: "1", - sourceStepOutputName: - "proposedEntities" satisfies OutputNameForAction<"inferMetadataFromDocument">, - }, - ], - }, - ], - outputs: [ - { - stepId: "2", - stepOutputName: "persistedEntities", - name: "persistedEntities" as const, - payloadKind: "PersistedEntity", - array: true, - required: true, - }, - ], -}; + }; diff --git a/libs/@local/hash-isomorphic-utils/src/flows/goal-flow-definitions.ts b/libs/@local/hash-isomorphic-utils/src/flows/goal-flow-definitions.ts index 92c6aa758ca..5aee4f5023d 100644 --- a/libs/@local/hash-isomorphic-utils/src/flows/goal-flow-definitions.ts +++ b/libs/@local/hash-isomorphic-utils/src/flows/goal-flow-definitions.ts @@ -1,8 +1,9 @@ import type { EntityUuid } from "@blockprotocol/type-system"; import type { - InputNameForAction, - OutputNameForAction, + AiFlowActionDefinitionId, + InputNameForAiFlowAction, + OutputNameForAiFlowAction, } from "./action-definitions.js"; import { googleSheetDeliverable, @@ -24,6 +25,7 @@ export type GoalFlowTriggerInput = export const goalFlowDefinition = { name: "Research and save to HASH", + type: "ai", flowDefinitionId: "research-goal" as EntityUuid, description: "Discover entities according to a research brief, save them to HASH", @@ -69,7 +71,8 @@ export const goalFlowDefinition = { "Discover entities according to research specification, using public web sources", inputSources: [ { - inputName: "prompt" satisfies InputNameForAction<"researchEntities">, + inputName: + "prompt" satisfies InputNameForAiFlowAction<"researchEntities">, kind: "step-output", sourceStepId: "trigger", sourceStepOutputName: @@ -77,7 +80,7 @@ export const goalFlowDefinition = { }, { inputName: - "entityTypeIds" satisfies InputNameForAction<"researchEntities">, + "entityTypeIds" satisfies InputNameForAiFlowAction<"researchEntities">, kind: "step-output", sourceStepId: "trigger", sourceStepOutputName: "Entity Types" satisfies GoalFlowTriggerInput, @@ -93,14 +96,15 @@ export const goalFlowDefinition = { inputSources: [ { inputName: - "proposedEntities" satisfies InputNameForAction<"persistEntities">, + "proposedEntities" satisfies InputNameForAiFlowAction<"persistEntities">, kind: "step-output", sourceStepId: "1", sourceStepOutputName: - "proposedEntities" satisfies OutputNameForAction<"researchEntities">, + "proposedEntities" satisfies OutputNameForAiFlowAction<"researchEntities">, }, { - inputName: "draft" satisfies InputNameForAction<"persistEntities">, + inputName: + "draft" satisfies InputNameForAiFlowAction<"persistEntities">, kind: "step-output", sourceStepId: "trigger", sourceStepOutputName: @@ -110,95 +114,100 @@ export const goalFlowDefinition = { }, ], outputs: [], -} satisfies FlowDefinition; +} satisfies FlowDefinition; -export const goalFlowDefinitionWithSpreadsheetDeliverable: FlowDefinition = { - ...goalFlowDefinition, - name: "Research and save entities to Google Sheets", - flowDefinitionId: "goal-with-spreadsheet" as EntityUuid, - description: - "Discover entities according to a research brief, save them to HASH and to a Google Sheet", - groups: [ - { - groupId: 1, - description: "Research and persist entities", - }, - { - groupId: 2, - description: "Deliver Google Sheet", +export const goalFlowDefinitionWithSpreadsheetDeliverable: FlowDefinition = + { + ...goalFlowDefinition, + type: "ai", + name: "Research and save entities to Google Sheets", + flowDefinitionId: "goal-with-spreadsheet" as EntityUuid, + description: + "Discover entities according to a research brief, save them to HASH and to a Google Sheet", + groups: [ + { + groupId: 1, + description: "Research and persist entities", + }, + { + groupId: 2, + description: "Deliver Google Sheet", + }, + ], + trigger: { + ...goalFlowDefinition.trigger, + outputs: [ + ...goalFlowDefinition.trigger.outputs, + ...googleSheetTriggerInputs, + ], }, - ], - trigger: { - ...goalFlowDefinition.trigger, + steps: [ + ...goalFlowDefinition.steps, + { + ...googleSheetStep, + groupId: 2, + stepId: "3", + }, + ], outputs: [ - ...goalFlowDefinition.trigger.outputs, - ...googleSheetTriggerInputs, + { + ...googleSheetDeliverable, + stepId: "3", + }, ], - }, - steps: [ - ...goalFlowDefinition.steps, - { - ...googleSheetStep, - groupId: 2, - stepId: "3", - }, - ], - outputs: [ - { - ...googleSheetDeliverable, - stepId: "3", - }, - ], -}; + }; -export const goalFlowDefinitionWithReportDeliverable: FlowDefinition = { - ...goalFlowDefinition, - name: "Research and write a report", - flowDefinitionId: "goal-with-report" as EntityUuid, - description: "Write a report based on a research specification", - groups: [ - { - groupId: 1, - description: "Research and persist entities", - }, - { - groupId: 2, - description: "Write report", - }, - ], - trigger: { - ...goalFlowDefinition.trigger, - outputs: [ - ...goalFlowDefinition.trigger.outputs, - ...markdownReportTriggerInputs, +export const goalFlowDefinitionWithReportDeliverable: FlowDefinition = + { + ...goalFlowDefinition, + type: "ai", + name: "Research and write a report", + flowDefinitionId: "goal-with-report" as EntityUuid, + description: "Write a report based on a research specification", + groups: [ + { + groupId: 1, + description: "Research and persist entities", + }, + { + groupId: 2, + description: "Write report", + }, ], - }, - steps: [ - { - ...goalFlowDefinition.steps[0]!, - inputSources: [ - ...goalFlowDefinition.steps[0]!.inputSources, - markdownReportResearchEntitiesStepInput, + trigger: { + ...goalFlowDefinition.trigger, + outputs: [ + ...goalFlowDefinition.trigger.outputs, + ...markdownReportTriggerInputs, ], }, - goalFlowDefinition.steps[1]!, - { - ...markdownReportStep, - groupId: 2, - stepId: "3", - }, - ], - outputs: [ - { - ...markdownReportDeliverable, - stepId: "3", - }, - ], -}; + steps: [ + { + ...goalFlowDefinition.steps[0]!, + inputSources: [ + ...goalFlowDefinition.steps[0]!.inputSources, + markdownReportResearchEntitiesStepInput, + ], + }, + goalFlowDefinition.steps[1]!, + { + ...markdownReportStep, + groupId: 2, + stepId: "3", + }, + ], + outputs: [ + { + ...markdownReportDeliverable, + stepId: "3", + }, + ], + }; -export const goalFlowDefinitionWithReportAndSpreadsheetDeliverable: FlowDefinition = +export const goalFlowDefinitionWithReportAndSpreadsheetDeliverable: FlowDefinition = { ...goalFlowDefinition, + type: "ai", name: "Research and write a report, save entities to Google Sheets", flowDefinitionId: "goal-with-report-and-sheet" as EntityUuid, description: @@ -253,9 +262,9 @@ export const goalFlowDefinitionWithReportAndSpreadsheetDeliverable: FlowDefiniti ], }; -export const goalFlowDefinitionIds = [ +export const goalFlowDefinitionIds: string[] = [ goalFlowDefinition.flowDefinitionId, goalFlowDefinitionWithSpreadsheetDeliverable.flowDefinitionId, goalFlowDefinitionWithReportDeliverable.flowDefinitionId, - goalFlowDefinitionWithReportAndSpreadsheetDeliverable, + goalFlowDefinitionWithReportAndSpreadsheetDeliverable.flowDefinitionId, ]; diff --git a/libs/@local/hash-isomorphic-utils/src/flows/goal-flow-definitions/google-sheets.ts b/libs/@local/hash-isomorphic-utils/src/flows/goal-flow-definitions/google-sheets.ts index 7b9153bcf52..bb64800975a 100644 --- a/libs/@local/hash-isomorphic-utils/src/flows/goal-flow-definitions/google-sheets.ts +++ b/libs/@local/hash-isomorphic-utils/src/flows/goal-flow-definitions/google-sheets.ts @@ -1,8 +1,9 @@ import type { DistributiveOmit } from "@local/advanced-types/distribute"; import type { - InputNameForAction, - OutputNameForAction, + AiFlowActionDefinitionId, + InputNameForAiFlowAction, + OutputNameForAiFlowAction, } from "../action-definitions.js"; import type { FlowDefinition } from "../types.js"; @@ -21,7 +22,7 @@ export const googleSheetTriggerInputs = [ array: false, required: true, }, -] satisfies FlowDefinition["trigger"]["outputs"]; +] satisfies FlowDefinition["trigger"]["outputs"]; export const googleSheetStep = { kind: "action", @@ -29,7 +30,8 @@ export const googleSheetStep = { description: "Save discovered entities to Google Sheet", inputSources: [ { - inputName: "audience" satisfies InputNameForAction<"writeGoogleSheet">, + inputName: + "audience" satisfies InputNameForAiFlowAction<"writeGoogleSheet">, kind: "hardcoded", payload: { kind: "ActorType", @@ -38,35 +40,40 @@ export const googleSheetStep = { }, { inputName: - "googleAccountId" satisfies InputNameForAction<"writeGoogleSheet">, + "googleAccountId" satisfies InputNameForAiFlowAction<"writeGoogleSheet">, kind: "step-output", sourceStepId: "trigger", sourceStepOutputName: "Google Account" satisfies GoogleSheetTriggerInput, }, { - inputName: "googleSheet" satisfies InputNameForAction<"writeGoogleSheet">, + inputName: + "googleSheet" satisfies InputNameForAiFlowAction<"writeGoogleSheet">, kind: "step-output", sourceStepId: "trigger", sourceStepOutputName: "Google Sheet" satisfies GoogleSheetTriggerInput, }, { - inputName: "dataToWrite" satisfies InputNameForAction<"writeGoogleSheet">, + inputName: + "dataToWrite" satisfies InputNameForAiFlowAction<"writeGoogleSheet">, kind: "step-output", sourceStepId: "2", sourceStepOutputName: - "persistedEntities" satisfies OutputNameForAction<"persistEntities">, + "persistedEntities" satisfies OutputNameForAiFlowAction<"persistEntities">, }, ], } satisfies DistributiveOmit< - FlowDefinition["steps"][number], + FlowDefinition["steps"][number], "stepId" | "groupId" >; export const googleSheetDeliverable = { stepOutputName: - "googleSheetEntity" satisfies OutputNameForAction<"writeGoogleSheet">, + "googleSheetEntity" satisfies OutputNameForAiFlowAction<"writeGoogleSheet">, payloadKind: "PersistedEntity", name: "googleSheetEntity" as const, array: false, required: true, -} satisfies Omit; +} satisfies Omit< + FlowDefinition["outputs"][number], + "stepId" +>; diff --git a/libs/@local/hash-isomorphic-utils/src/flows/goal-flow-definitions/markdown-report.ts b/libs/@local/hash-isomorphic-utils/src/flows/goal-flow-definitions/markdown-report.ts index 6f649c58728..b74a6113669 100644 --- a/libs/@local/hash-isomorphic-utils/src/flows/goal-flow-definitions/markdown-report.ts +++ b/libs/@local/hash-isomorphic-utils/src/flows/goal-flow-definitions/markdown-report.ts @@ -1,8 +1,9 @@ import type { DistributiveOmit } from "@local/advanced-types/distribute"; import type { - InputNameForAction, - OutputNameForAction, + AiFlowActionDefinitionId, + InputNameForAiFlowAction, + OutputNameForAiFlowAction, } from "../action-definitions.js"; import type { ActionStepDefinition, FlowDefinition } from "../types.js"; @@ -15,11 +16,11 @@ export const markdownReportTriggerInputs = [ array: false, required: true, }, -] satisfies FlowDefinition["trigger"]["outputs"]; +] satisfies FlowDefinition["trigger"]["outputs"]; export const markdownReportResearchEntitiesStepInput = { inputName: - "reportSpecification" satisfies InputNameForAction<"researchEntities">, + "reportSpecification" satisfies InputNameForAiFlowAction<"researchEntities">, kind: "step-output", sourceStepId: "trigger", sourceStepOutputName: "Report specification" satisfies ReportTriggerInput, @@ -31,28 +32,34 @@ export const markdownReportStep = { description: "Write report based on the research specification", inputSources: [ { - inputName: "question" satisfies InputNameForAction<"answerQuestion">, + inputName: + "question" satisfies InputNameForAiFlowAction<"answerQuestion">, kind: "step-output", sourceStepId: "trigger", sourceStepOutputName: "Report specification", }, { - inputName: "entities" satisfies InputNameForAction<"answerQuestion">, + inputName: + "entities" satisfies InputNameForAiFlowAction<"answerQuestion">, kind: "step-output", sourceStepId: "2", sourceStepOutputName: - "persistedEntities" satisfies OutputNameForAction<"persistEntities">, + "persistedEntities" satisfies OutputNameForAiFlowAction<"persistEntities">, }, ], } satisfies DistributiveOmit< - FlowDefinition["steps"][number], + FlowDefinition["steps"][number], "stepId" | "groupId" >; export const markdownReportDeliverable = { - stepOutputName: "answer" satisfies OutputNameForAction<"answerQuestion">, + stepOutputName: + "answer" satisfies OutputNameForAiFlowAction<"answerQuestion">, payloadKind: "Text", name: "report" as const, array: false, required: true, -} satisfies Omit; +} satisfies Omit< + FlowDefinition["outputs"][number], + "stepId" +>; diff --git a/libs/@local/hash-isomorphic-utils/src/flows/integration-flow-definitions.ts b/libs/@local/hash-isomorphic-utils/src/flows/integration-flow-definitions.ts new file mode 100644 index 00000000000..709d11786e6 --- /dev/null +++ b/libs/@local/hash-isomorphic-utils/src/flows/integration-flow-definitions.ts @@ -0,0 +1,92 @@ +import type { EntityUuid } from "@blockprotocol/type-system"; + +import type { + InputNameForIntegrationFlowAction, + IntegrationFlowActionDefinitionId, + OutputNameForIntegrationFlowAction, +} from "./action-definitions.js"; +import type { FlowDefinition } from "./types.js"; + +/** + * Flow definition for fetching scheduled flights for an airport on a given date and persisting them to the graph. + */ +export const scheduledFlightsFlowDefinition: FlowDefinition = + { + name: "Get Scheduled Flights", + type: "integration", + flowDefinitionId: "scheduled-flights" as EntityUuid, + description: + "Fetch and save scheduled flight arrivals for an airport on a given date.", + trigger: { + triggerDefinitionId: "userTrigger", + description: + "User provides an airport ICAO code and date to fetch scheduled flights for", + kind: "trigger", + outputs: [ + { + payloadKind: "Text", + name: "Airport ICAO", + array: false, + required: true, + }, + { + payloadKind: "Date", + name: "Date", + array: false, + required: true, + }, + ], + }, + steps: [ + { + stepId: "1", + kind: "action", + actionDefinitionId: "getScheduledFlights", + description: + "Fetch scheduled flight arrivals from for the specified airport and date", + inputSources: [ + { + inputName: + "airportIcao" satisfies InputNameForIntegrationFlowAction<"getScheduledFlights">, + kind: "step-output", + sourceStepId: "trigger", + sourceStepOutputName: "Airport ICAO", + }, + { + inputName: + "date" satisfies InputNameForIntegrationFlowAction<"getScheduledFlights">, + kind: "step-output", + sourceStepId: "trigger", + sourceStepOutputName: "Date", + }, + ], + }, + { + stepId: "2", + kind: "action", + description: "Save discovered entities and relationships to HASH graph", + actionDefinitionId: "persistIntegrationEntities", + inputSources: [ + { + inputName: + "proposedEntities" satisfies InputNameForIntegrationFlowAction<"persistIntegrationEntities">, + kind: "step-output", + sourceStepId: "1", + sourceStepOutputName: + "proposedEntities" satisfies OutputNameForIntegrationFlowAction<"getScheduledFlights">, + }, + ], + }, + ], + outputs: [ + { + stepId: "2", + stepOutputName: + "persistedEntities" satisfies OutputNameForIntegrationFlowAction<"persistIntegrationEntities">, + payloadKind: "PersistedEntities", + name: "persistedEntities" as const, + array: false, + required: true, + }, + ], + }; diff --git a/libs/@local/hash-isomorphic-utils/src/flows/mappings.ts b/libs/@local/hash-isomorphic-utils/src/flows/mappings.ts index 0a4083d23a2..fa767f81e0f 100644 --- a/libs/@local/hash-isomorphic-utils/src/flows/mappings.ts +++ b/libs/@local/hash-isomorphic-utils/src/flows/mappings.ts @@ -9,13 +9,14 @@ import type { FlowDefinition as FlowDefinitionEntity } from "../system-types/flo import type { FlowRun } from "../system-types/flowrun.js"; import type { TriggerDefinitionId } from "./trigger-definitions.js"; import type { + FlowActionDefinitionId, FlowDefinition, LocalFlowRun, OutputDefinition, } from "./types.js"; export const mapFlowDefinitionToEntityProperties = ( - flowDefinition: FlowDefinition, + flowDefinition: FlowDefinition, ): FlowDefinitionEntity["properties"] => ({ "https://blockprotocol.org/@blockprotocol/types/property-type/name/": flowDefinition.name, @@ -35,7 +36,7 @@ export const mapFlowDefinitionToEntityProperties = ( export const mapFlowDefinitionEntityToFlowDefinition = ( entity: HashEntity, -): FlowDefinition => { +): FlowDefinition => { const { name, description, @@ -46,12 +47,14 @@ export const mapFlowDefinitionEntityToFlowDefinition = ( return { name, + type: "ai", description, flowDefinitionId: extractEntityUuidFromEntityId( entity.metadata.recordId.entityId, ), - outputs: outputDefinitions as FlowDefinition["outputs"], - steps: stepDefinitions as FlowDefinition["steps"], + outputs: + outputDefinitions as FlowDefinition["outputs"], + steps: stepDefinitions as FlowDefinition["steps"], trigger: { kind: "trigger", triggerDefinitionId: @@ -62,7 +65,7 @@ export const mapFlowDefinitionEntityToFlowDefinition = ( "https://hash.ai/@h/types/property-type/output-definitions/" ] as OutputDefinition[], /** @todo: fix this */ - } as unknown as FlowDefinition["trigger"], + } as unknown as FlowDefinition["trigger"], }; }; diff --git a/libs/@local/hash-isomorphic-utils/src/flows/temporal-types.ts b/libs/@local/hash-isomorphic-utils/src/flows/temporal-types.ts index 8f086fc8dfd..421b0453114 100644 --- a/libs/@local/hash-isomorphic-utils/src/flows/temporal-types.ts +++ b/libs/@local/hash-isomorphic-utils/src/flows/temporal-types.ts @@ -1,21 +1,34 @@ import type { UserId, WebId } from "@blockprotocol/type-system"; import type { Status } from "@local/status"; +import type { AiFlowActionDefinitionId } from "./action-definitions.js"; import type { + FlowActionDefinitionId, FlowDataSources, FlowDefinition, FlowTrigger, LocalFlowRun, } from "./types.js"; -export type RunFlowWorkflowParams = { - dataSources: FlowDataSources; +export type BaseRunFlowWorkflowParams< + ValidActionDefinitionId extends + FlowActionDefinitionId = FlowActionDefinitionId, +> = { + flowDefinition: FlowDefinition; flowTrigger: FlowTrigger; - flowDefinition: FlowDefinition; userAuthentication: { actorId: UserId }; webId: WebId; }; +export type RunAiFlowWorkflowParams = + BaseRunFlowWorkflowParams & { + dataSources: FlowDataSources; + }; + +export type RunFlowWorkflowParams = + | BaseRunFlowWorkflowParams + | RunAiFlowWorkflowParams; + export type RunFlowWorkflowResponse = Status<{ flow?: LocalFlowRun; outputs?: LocalFlowRun["outputs"]; diff --git a/libs/@local/hash-isomorphic-utils/src/flows/types.ts b/libs/@local/hash-isomorphic-utils/src/flows/types.ts index 38aed2278b9..b60cc9fe581 100644 --- a/libs/@local/hash-isomorphic-utils/src/flows/types.ts +++ b/libs/@local/hash-isomorphic-utils/src/flows/types.ts @@ -13,11 +13,18 @@ import type { DistributiveOmit } from "@local/advanced-types/distribute"; import type { SerializedEntity } from "@local/hash-graph-sdk/entity"; import type { Status } from "@local/status"; -import type { FlowRun } from "../graphql/api-types.gen.js"; +import type { FlowRun, FlowType } from "../graphql/api-types.gen.js"; import type { ActorTypeDataType } from "../system-types/google/googlesheetsfile.js"; -import type { ActionDefinitionId } from "./action-definitions.js"; +import type { + AiFlowActionDefinitionId, + IntegrationFlowActionDefinitionId, +} from "./action-definitions.js"; import type { TriggerDefinitionId } from "./trigger-definitions.js"; +export type FlowActionDefinitionId = + | AiFlowActionDefinitionId + | IntegrationFlowActionDefinitionId; + export type DeepReadOnly = { readonly [key in keyof T]: DeepReadOnly; }; @@ -80,14 +87,18 @@ export type PersistedEntities = { failedEntityProposals: FailedEntityProposal[]; }; -export type FlowInputs = [ - { - dataSources: FlowDataSources; - flowDefinition: FlowDefinition; - flowTrigger: FlowTrigger; - webId: WebId; - }, -]; +type BaseFlowInputs = { + flowDefinition: FlowDefinition; + flowType: FlowType; + flowTrigger: FlowTrigger; + webId: WebId; +}; + +type AiFlowInputs = BaseFlowInputs & { + dataSources: FlowDataSources; +}; + +export type FlowInputs = [BaseFlowInputs | AiFlowInputs]; export const textFormats = ["CSV", "HTML", "Markdown", "Plain"] as const; @@ -105,6 +116,7 @@ export type WebSearchResult = Pick; export type PayloadKindValues = { ActorType: ActorTypeDataType; Boolean: boolean; + Date: string; // e.g. "2025-01-01" Entity: SerializedEntity; EntityId: EntityId; FormattedText: FormattedText; @@ -170,7 +182,9 @@ export type TriggerDefinition = { outputs?: OutputDefinition[]; }; -export type ActionDefinition = { +export type ActionDefinition< + ActionDefinitionId extends FlowActionDefinitionId, +> = { kind: "action"; actionDefinitionId: ActionDefinitionId; name: string; @@ -179,10 +193,6 @@ export type ActionDefinition = { outputs: OutputDefinition[]; }; -/** - * Flow Definition - */ - export type StepInputSource

= { inputName: string; } & ( @@ -207,6 +217,7 @@ export type StepInputSource

= { ); export type ActionStepDefinition< + ActionDefinitionId extends FlowActionDefinitionId = FlowActionDefinitionId, AdditionalInputSources extends { inputName: string } | null = null, > = { kind: "action"; @@ -220,26 +231,35 @@ export type ActionStepDefinition< retryCount?: number; }; -export type ActionStepWithParallelInput = ActionStepDefinition<{ - /** - * This additional input source refers to the dispersed input - * for a parallel group. - */ - inputName: string; - kind: "parallel-group-input"; -}>; +export type ActionStepWithParallelInput< + ActionDefinitionId extends FlowActionDefinitionId = FlowActionDefinitionId, +> = ActionStepDefinition< + ActionDefinitionId, + { + /** + * This additional input source refers to the dispersed input + * for a parallel group. + */ + inputName: string; + kind: "parallel-group-input"; + } +>; -export type StepDefinition = - | ActionStepDefinition - | ActionStepWithParallelInput - | ParallelGroupStepDefinition; +export type StepDefinition< + ActionDefinitionId extends FlowActionDefinitionId = FlowActionDefinitionId, +> = + | ActionStepDefinition + | ActionStepWithParallelInput + | ParallelGroupStepDefinition; /** * A step which spawns multiple parallel branches of steps based on an array input. * * e.g. for each input entity, do X with that entity in a separate branch. */ -export type ParallelGroupStepDefinition = { +export type ParallelGroupStepDefinition< + ActionDefinitionId extends FlowActionDefinitionId = FlowActionDefinitionId, +> = { kind: "parallel-group"; stepId: string; groupId?: number; @@ -254,7 +274,7 @@ export type ParallelGroupStepDefinition = { * The steps that will be executed in parallel branches for each payload * item in the provided `ArrayPayload`. */ - steps: StepDefinition[]; + steps: StepDefinition[]; /** * The aggregate output of the parallel group must be defined * as an `array` output. @@ -294,25 +314,29 @@ export type StepGroup = { description: string; }; -export type FlowDefinition = { - name: string; - description: string; - flowDefinitionId: EntityUuid; - trigger: FlowDefinitionTrigger; - groups?: StepGroup[]; - steps: StepDefinition[]; - outputs: (OutputDefinition & { - /** - * The step ID for the step in the flow that will produce the - * output. - */ - stepId: string; - /** - * The name of the output in the step - */ - stepOutputName: string; - })[]; -}; +export type FlowDefinition = + { + type: ActionDefinitionId extends AiFlowActionDefinitionId + ? "ai" + : "integration"; + name: string; + description: string; + flowDefinitionId: EntityUuid; + trigger: FlowDefinitionTrigger; + groups?: StepGroup[]; + steps: StepDefinition[]; + outputs: (OutputDefinition & { + /** + * The step ID for the step in the flow that will produce the + * output. + */ + stepId: string; + /** + * The name of the output in the step + */ + stepOutputName: string; + })[]; + }; export type StepInput

= { inputName: string; @@ -324,9 +348,13 @@ export type StepOutput

= { payload: P; }; -export type StepRunOutput = Status>>; +export type StepRunOutput = Status< + Required, "outputs">> +>; -export type ActionStep = { +export type ActionStep< + ActionDefinitionId extends FlowActionDefinitionId = FlowActionDefinitionId, +> = { stepId: string; kind: "action"; actionDefinitionId: ActionDefinitionId; @@ -335,15 +363,19 @@ export type ActionStep = { outputs?: StepOutput[]; }; -export type ParallelGroupStep = { +export type ParallelGroupStep< + ActionDefinitionId extends FlowActionDefinitionId = FlowActionDefinitionId, +> = { stepId: string; kind: "parallel-group"; inputToParallelizeOn?: StepInput; - steps?: FlowStep[]; + steps?: FlowStep[]; aggregateOutput?: StepOutput; }; -export type FlowStep = ActionStep | ParallelGroupStep; +export type FlowStep< + ActionDefinitionId extends FlowActionDefinitionId = FlowActionDefinitionId, +> = ActionStep | ParallelGroupStep; export type FlowTrigger = { triggerDefinitionId: TriggerDefinitionId; @@ -372,12 +404,14 @@ export type FlowDataSources = { /** * A simplified type for a FlowRun used internally in the worker logic. */ -export type LocalFlowRun = { +export type LocalFlowRun< + ActionDefinitionId extends FlowActionDefinitionId = FlowActionDefinitionId, +> = { name: string; flowRunId: EntityUuid; trigger: FlowTrigger; flowDefinitionId: EntityUuid; - steps: FlowStep[]; + steps: FlowStep[]; outputs?: StepOutput[]; }; diff --git a/libs/@local/hash-isomorphic-utils/src/flows/util.ts b/libs/@local/hash-isomorphic-utils/src/flows/util.ts index c27a7e1223a..0a4179539ee 100644 --- a/libs/@local/hash-isomorphic-utils/src/flows/util.ts +++ b/libs/@local/hash-isomorphic-utils/src/flows/util.ts @@ -1,26 +1,45 @@ -import { actionDefinitions } from "./action-definitions.js"; +import { + aiActionDefinitions, + integrationActionDefinitions, +} from "./action-definitions.js"; import { triggerDefinitions } from "./trigger-definitions.js"; import type { - ActionStepDefinition, + ActionDefinition, + ActionStepWithParallelInput, + FlowActionDefinitionId, FlowDefinition, ParallelGroupStepDefinition, StepDefinition, } from "./types.js"; -type AnyStepDefinition = - | StepDefinition - | ActionStepDefinition<{ - inputName: string; - kind: "parallel-group-input"; - }>; +export type FlowType = "ai" | "integration"; -const recursivelyValidateSteps = (params: { - allStepDefinitions: AnyStepDefinition[]; +type ActionDefinitionsMap = Record< + T, + ActionDefinition +>; + +const getActionDefinitions = ( + flowType: FlowType, +): ActionDefinitionsMap => { + if (flowType === "ai") { + return aiActionDefinitions as unknown as ActionDefinitionsMap; + } + return integrationActionDefinitions as unknown as ActionDefinitionsMap; +}; + +type FlowStepDefinition = + | StepDefinition + | ActionStepWithParallelInput; + +const recursivelyValidateSteps = (params: { + actionDefinitions: ActionDefinitionsMap; + allStepDefinitions: FlowStepDefinition[]; stepIds: Set; - flow: FlowDefinition; - step: AnyStepDefinition; + flow: FlowDefinition; + step: FlowStepDefinition; }) => { - const { flow, step, stepIds, allStepDefinitions } = params; + const { actionDefinitions, flow, step, stepIds, allStepDefinitions } = params; if (stepIds.has(step.stepId)) { throw new Error(`Duplicate step id: ${step.stepId}`); @@ -82,7 +101,7 @@ const recursivelyValidateSteps = (params: { const childActionSteps = step.steps.filter( ( childStep, - ): childStep is Extract => + ): childStep is Extract, { kind: "action" }> => childStep.kind === "action", ); @@ -167,6 +186,7 @@ const recursivelyValidateSteps = (params: { for (const childStep of step.steps) { recursivelyValidateSteps({ + actionDefinitions, stepIds, flow, step: childStep, @@ -252,7 +272,7 @@ const recursivelyValidateSteps = (params: { ) ) { throw new Error( - `${errorPrefix}references an output "${inputSource.sourceStepOutputName}" of step "${inputSource.sourceStepId}" that does not match the expected payload kinds of the input`, + `${errorPrefix}references an output "${inputSource.sourceStepOutputName}" of step "${inputSource.sourceStepId}" that does not match the expected payload kinds of the input (expected: ${matchingDefinitionInput.oneOfPayloadKinds.join(", ")}, actual: ${matchingSourceStepOutput.payloadKind})`, ); } } else if (inputSource.kind === "hardcoded") { @@ -286,9 +306,11 @@ const recursivelyValidateSteps = (params: { } }; -const getAllStepDefinitionsInParallelGroupDefinition = ( - stepDefinition: ParallelGroupStepDefinition, -): AnyStepDefinition[] => [ +const getAllStepDefinitionsInParallelGroupDefinition = < + T extends FlowActionDefinitionId, +>( + stepDefinition: ParallelGroupStepDefinition, +): FlowStepDefinition[] => [ ...stepDefinition.steps, ...stepDefinition.steps.flatMap((childStep) => childStep.kind === "parallel-group" @@ -297,7 +319,11 @@ const getAllStepDefinitionsInParallelGroupDefinition = ( ), ]; -export const getAllStepDefinitionsInFlowDefinition = (flow: FlowDefinition) => [ +export const getAllStepDefinitionsInFlowDefinition = < + T extends FlowActionDefinitionId, +>( + flow: FlowDefinition, +) => [ ...flow.steps, ...flow.steps.flatMap((step) => step.kind === "parallel-group" @@ -313,15 +339,26 @@ export const getAllStepDefinitionsInFlowDefinition = (flow: FlowDefinition) => [ * - Input sources (other step outputs, flow triggers, hardcoded values) exist and match expected types. * * @param flow The flow definition to validate. + * @param flowType The type of flow ('ai' or 'integration') to determine which action definitions to use. * @returns true if the flow definition passes all validation checks. */ -export const validateFlowDefinition = (flow: FlowDefinition) => { +export const validateFlowDefinition = ( + flow: FlowDefinition, + flowType: FlowType, +) => { const stepIds = new Set(); + const actionDefinitions = getActionDefinitions(flowType); const allStepDefinitions = getAllStepDefinitionsInFlowDefinition(flow); for (const step of flow.steps) { - recursivelyValidateSteps({ stepIds, flow, step, allStepDefinitions }); + recursivelyValidateSteps({ + actionDefinitions, + stepIds, + flow, + step, + allStepDefinitions, + }); } return true; diff --git a/libs/@local/hash-isomorphic-utils/src/graphql/scalar-mapping.ts b/libs/@local/hash-isomorphic-utils/src/graphql/scalar-mapping.ts index b82c8fb1b38..e3616a98596 100644 --- a/libs/@local/hash-isomorphic-utils/src/graphql/scalar-mapping.ts +++ b/libs/@local/hash-isomorphic-utils/src/graphql/scalar-mapping.ts @@ -129,8 +129,11 @@ export const scalars = { OntologyTemporalMetadata: "@local/hash-graph-client#OntologyTemporalMetadata", FlowTrigger: "@local/hash-isomorphic-utils/flows/types#FlowTrigger", + FlowActionDefinitionId: + "@local/hash-isomorphic-utils/flows/types#FlowActionDefinitionId", FlowDataSources: "@local/hash-isomorphic-utils/flows/types#FlowDataSources", - FlowDefinition: "@local/hash-isomorphic-utils/flows/types#FlowDefinition", + FlowDefinition: + "@local/hash-isomorphic-utils/flows/types#FlowDefinition", FlowInputs: "@local/hash-isomorphic-utils/flows/types#FlowInputs", ExternalInputRequest: "@local/hash-isomorphic-utils/flows/types#ExternalInputRequest", diff --git a/libs/@local/hash-isomorphic-utils/src/graphql/type-defs/knowledge/flow.typedef.ts b/libs/@local/hash-isomorphic-utils/src/graphql/type-defs/knowledge/flow.typedef.ts index 313d6212f61..8c17b66758a 100644 --- a/libs/@local/hash-isomorphic-utils/src/graphql/type-defs/knowledge/flow.typedef.ts +++ b/libs/@local/hash-isomorphic-utils/src/graphql/type-defs/knowledge/flow.typedef.ts @@ -195,19 +195,28 @@ export const flowTypedef = gql` getFlowRunById(flowRunId: String!): FlowRun! } + # FlowActionDefinitionId is just here so that the type is generated along with the other scalars, + # as we need to pass it to FlowDefinition. + scalar FlowActionDefinitionId scalar FlowDefinition scalar FlowDataSources scalar FlowTrigger scalar ExternalInputResponseWithoutUser + enum FlowType { + ai + integration + } + extend type Mutation { """ Start a new flow run, and return its flowRunId to allow for identifying it later. """ startFlow( - dataSources: FlowDataSources! + dataSources: FlowDataSources flowDefinition: FlowDefinition! flowTrigger: FlowTrigger! + flowType: FlowType! webId: WebId! ): EntityUuid! diff --git a/libs/@local/hash-isomorphic-utils/src/ontology-type-ids.ts b/libs/@local/hash-isomorphic-utils/src/ontology-type-ids.ts index f1037625aea..d97b317b42b 100644 --- a/libs/@local/hash-isomorphic-utils/src/ontology-type-ids.ts +++ b/libs/@local/hash-isomorphic-utils/src/ontology-type-ids.ts @@ -10,6 +10,21 @@ export const systemEntityTypes = { entityTypeId: "https://hash.ai/@h/types/entity-type/actor/v/2", entityTypeBaseUrl: "https://hash.ai/@h/types/entity-type/actor/" as BaseUrl, }, + aircraft: { + entityTypeId: "https://hash.ai/@h/types/entity-type/aircraft/v/1", + entityTypeBaseUrl: + "https://hash.ai/@h/types/entity-type/aircraft/" as BaseUrl, + }, + airline: { + entityTypeId: "https://hash.ai/@h/types/entity-type/airline/v/1", + entityTypeBaseUrl: + "https://hash.ai/@h/types/entity-type/airline/" as BaseUrl, + }, + airport: { + entityTypeId: "https://hash.ai/@h/types/entity-type/airport/v/1", + entityTypeBaseUrl: + "https://hash.ai/@h/types/entity-type/airport/" as BaseUrl, + }, block: { entityTypeId: "https://hash.ai/@h/types/entity-type/block/v/1", entityTypeBaseUrl: "https://hash.ai/@h/types/entity-type/block/" as BaseUrl, @@ -77,6 +92,11 @@ export const systemEntityTypes = { entityTypeId: "https://hash.ai/@h/types/entity-type/file/v/2", entityTypeBaseUrl: "https://hash.ai/@h/types/entity-type/file/" as BaseUrl, }, + flight: { + entityTypeId: "https://hash.ai/@h/types/entity-type/flight/v/1", + entityTypeBaseUrl: + "https://hash.ai/@h/types/entity-type/flight/" as BaseUrl, + }, flowDefinition: { entityTypeId: "https://hash.ai/@h/types/entity-type/flow-definition/v/1", entityTypeBaseUrl: @@ -274,6 +294,11 @@ export const systemLinkEntityTypes = { linkEntityTypeBaseUrl: "https://hash.ai/@h/types/entity-type/affiliated-with/" as BaseUrl, }, + arrivesAt: { + linkEntityTypeId: "https://hash.ai/@h/types/entity-type/arrives-at/v/1", + linkEntityTypeBaseUrl: + "https://hash.ai/@h/types/entity-type/arrives-at/" as BaseUrl, + }, associatedWithAccount: { linkEntityTypeId: "https://hash.ai/@h/types/entity-type/associated-with-account/v/1", @@ -290,6 +315,11 @@ export const systemLinkEntityTypes = { linkEntityTypeBaseUrl: "https://hash.ai/@h/types/entity-type/created/" as BaseUrl, }, + departsFrom: { + linkEntityTypeId: "https://hash.ai/@h/types/entity-type/departs-from/v/1", + linkEntityTypeBaseUrl: + "https://hash.ai/@h/types/entity-type/departs-from/" as BaseUrl, + }, has: { linkEntityTypeId: "https://hash.ai/@h/types/entity-type/has/v/1", linkEntityTypeBaseUrl: @@ -405,6 +435,11 @@ export const systemLinkEntityTypes = { linkEntityTypeBaseUrl: "https://hash.ai/@h/types/entity-type/occurred-in-text/" as BaseUrl, }, + operatedBy: { + linkEntityTypeId: "https://hash.ai/@h/types/entity-type/operated-by/v/1", + linkEntityTypeBaseUrl: + "https://hash.ai/@h/types/entity-type/operated-by/" as BaseUrl, + }, recordsUsageOf: { linkEntityTypeId: "https://hash.ai/@h/types/entity-type/records-usage-of/v/1", @@ -450,6 +485,11 @@ export const systemLinkEntityTypes = { linkEntityTypeBaseUrl: "https://hash.ai/@h/types/entity-type/updated/" as BaseUrl, }, + usesAircraft: { + linkEntityTypeId: "https://hash.ai/@h/types/entity-type/uses-aircraft/v/1", + linkEntityTypeBaseUrl: + "https://hash.ai/@h/types/entity-type/uses-aircraft/" as BaseUrl, + }, usesUserSecret: { linkEntityTypeId: "https://hash.ai/@h/types/entity-type/uses-user-secret/v/1", @@ -468,6 +508,18 @@ export const systemPropertyTypes = { propertyTypeBaseUrl: "https://hash.ai/@h/types/property-type/actual-enrollment/" as BaseUrl, }, + actualGateTime: { + propertyTypeId: + "https://hash.ai/@h/types/property-type/actual-gate-time/v/1", + propertyTypeBaseUrl: + "https://hash.ai/@h/types/property-type/actual-gate-time/" as BaseUrl, + }, + actualRunwayTime: { + propertyTypeId: + "https://hash.ai/@h/types/property-type/actual-runway-time/v/1", + propertyTypeBaseUrl: + "https://hash.ai/@h/types/property-type/actual-runway-time/" as BaseUrl, + }, actualStudyCompletionDate: { propertyTypeId: "https://hash.ai/@h/types/property-type/actual-study-completion-date/v/1", @@ -486,6 +538,11 @@ export const systemPropertyTypes = { propertyTypeBaseUrl: "https://hash.ai/@h/types/property-type/actual-study-start-date/" as BaseUrl, }, + altitude: { + propertyTypeId: "https://hash.ai/@h/types/property-type/altitude/v/1", + propertyTypeBaseUrl: + "https://hash.ai/@h/types/property-type/altitude/" as BaseUrl, + }, applicationPreferences: { propertyTypeId: "https://hash.ai/@h/types/property-type/application-preferences/v/1", @@ -513,12 +570,27 @@ export const systemPropertyTypes = { propertyTypeBaseUrl: "https://hash.ai/@h/types/property-type/automatic-inference-configuration/" as BaseUrl, }, + baggageClaim: { + propertyTypeId: "https://hash.ai/@h/types/property-type/baggage-claim/v/1", + propertyTypeBaseUrl: + "https://hash.ai/@h/types/property-type/baggage-claim/" as BaseUrl, + }, browserPluginTab: { propertyTypeId: "https://hash.ai/@h/types/property-type/browser-plugin-tab/v/1", propertyTypeBaseUrl: "https://hash.ai/@h/types/property-type/browser-plugin-tab/" as BaseUrl, }, + city: { + propertyTypeId: "https://hash.ai/@h/types/property-type/city/v/1", + propertyTypeBaseUrl: + "https://hash.ai/@h/types/property-type/city/" as BaseUrl, + }, + codeshare: { + propertyTypeId: "https://hash.ai/@h/types/property-type/codeshare/v/1", + propertyTypeBaseUrl: + "https://hash.ai/@h/types/property-type/codeshare/" as BaseUrl, + }, componentId: { propertyTypeId: "https://hash.ai/@h/types/property-type/component-id/v/1", propertyTypeBaseUrl: @@ -553,11 +625,22 @@ export const systemPropertyTypes = { propertyTypeBaseUrl: "https://hash.ai/@h/types/property-type/definition-object/" as BaseUrl, }, + delayInSeconds: { + propertyTypeId: + "https://hash.ai/@h/types/property-type/delay-in-seconds/v/1", + propertyTypeBaseUrl: + "https://hash.ai/@h/types/property-type/delay-in-seconds/" as BaseUrl, + }, deletedAt: { propertyTypeId: "https://hash.ai/@h/types/property-type/deleted-at/v/1", propertyTypeBaseUrl: "https://hash.ai/@h/types/property-type/deleted-at/" as BaseUrl, }, + direction: { + propertyTypeId: "https://hash.ai/@h/types/property-type/direction/v/1", + propertyTypeBaseUrl: + "https://hash.ai/@h/types/property-type/direction/" as BaseUrl, + }, doi: { propertyTypeId: "https://hash.ai/@h/types/property-type/doi/v/1", propertyTypeBaseUrl: @@ -596,12 +679,24 @@ export const systemPropertyTypes = { propertyTypeBaseUrl: "https://hash.ai/@h/types/property-type/estimated-enrollment/" as BaseUrl, }, + estimatedGateTime: { + propertyTypeId: + "https://hash.ai/@h/types/property-type/estimated-gate-time/v/1", + propertyTypeBaseUrl: + "https://hash.ai/@h/types/property-type/estimated-gate-time/" as BaseUrl, + }, estimatedPrimaryCompletionDate: { propertyTypeId: "https://hash.ai/@h/types/property-type/estimated-primary-completion-date/v/1", propertyTypeBaseUrl: "https://hash.ai/@h/types/property-type/estimated-primary-completion-date/" as BaseUrl, }, + estimatedRunwayTime: { + propertyTypeId: + "https://hash.ai/@h/types/property-type/estimated-runway-time/v/1", + propertyTypeBaseUrl: + "https://hash.ai/@h/types/property-type/estimated-runway-time/" as BaseUrl, + }, estimatedStudyCompletionDate: { propertyTypeId: "https://hash.ai/@h/types/property-type/estimated-study-completion-date/v/1", @@ -682,6 +777,26 @@ export const systemPropertyTypes = { propertyTypeBaseUrl: "https://hash.ai/@h/types/property-type/finding/" as BaseUrl, }, + flightDate: { + propertyTypeId: "https://hash.ai/@h/types/property-type/flight-date/v/1", + propertyTypeBaseUrl: + "https://hash.ai/@h/types/property-type/flight-date/" as BaseUrl, + }, + flightNumber: { + propertyTypeId: "https://hash.ai/@h/types/property-type/flight-number/v/1", + propertyTypeBaseUrl: + "https://hash.ai/@h/types/property-type/flight-number/" as BaseUrl, + }, + flightStatus: { + propertyTypeId: "https://hash.ai/@h/types/property-type/flight-status/v/1", + propertyTypeBaseUrl: + "https://hash.ai/@h/types/property-type/flight-status/" as BaseUrl, + }, + flightType: { + propertyTypeId: "https://hash.ai/@h/types/property-type/flight-type/v/1", + propertyTypeBaseUrl: + "https://hash.ai/@h/types/property-type/flight-type/" as BaseUrl, + }, flowDefinitionId: { propertyTypeId: "https://hash.ai/@h/types/property-type/flow-definition-id/v/1", @@ -694,18 +809,38 @@ export const systemPropertyTypes = { propertyTypeBaseUrl: "https://hash.ai/@h/types/property-type/fractional-index/" as BaseUrl, }, + gate: { + propertyTypeId: "https://hash.ai/@h/types/property-type/gate/v/1", + propertyTypeBaseUrl: + "https://hash.ai/@h/types/property-type/gate/" as BaseUrl, + }, graphChangeType: { propertyTypeId: "https://hash.ai/@h/types/property-type/graph-change-type/v/1", propertyTypeBaseUrl: "https://hash.ai/@h/types/property-type/graph-change-type/" as BaseUrl, }, + groundSpeed: { + propertyTypeId: "https://hash.ai/@h/types/property-type/ground-speed/v/1", + propertyTypeBaseUrl: + "https://hash.ai/@h/types/property-type/ground-speed/" as BaseUrl, + }, heightInPixels: { propertyTypeId: "https://hash.ai/@h/types/property-type/height-in-pixels/v/1", propertyTypeBaseUrl: "https://hash.ai/@h/types/property-type/height-in-pixels/" as BaseUrl, }, + iataCode: { + propertyTypeId: "https://hash.ai/@h/types/property-type/iata-code/v/1", + propertyTypeBaseUrl: + "https://hash.ai/@h/types/property-type/iata-code/" as BaseUrl, + }, + icaoCode: { + propertyTypeId: "https://hash.ai/@h/types/property-type/icao-code/v/1", + propertyTypeBaseUrl: + "https://hash.ai/@h/types/property-type/icao-code/" as BaseUrl, + }, icon: { propertyTypeId: "https://hash.ai/@h/types/property-type/icon/v/1", propertyTypeBaseUrl: @@ -744,6 +879,11 @@ export const systemPropertyTypes = { propertyTypeBaseUrl: "https://hash.ai/@h/types/property-type/intervention/" as BaseUrl, }, + isOnGround: { + propertyTypeId: "https://hash.ai/@h/types/property-type/is-on-ground/v/1", + propertyTypeBaseUrl: + "https://hash.ai/@h/types/property-type/is-on-ground/" as BaseUrl, + }, isbn: { propertyTypeId: "https://hash.ai/@h/types/property-type/isbn/v/1", propertyTypeBaseUrl: @@ -760,6 +900,11 @@ export const systemPropertyTypes = { propertyTypeBaseUrl: "https://hash.ai/@h/types/property-type/kratos-identity-id/" as BaseUrl, }, + latitude: { + propertyTypeId: "https://hash.ai/@h/types/property-type/latitude/v/1", + propertyTypeBaseUrl: + "https://hash.ai/@h/types/property-type/latitude/" as BaseUrl, + }, linearOrgId: { propertyTypeId: "https://hash.ai/@h/types/property-type/linear-org-id/v/1", propertyTypeBaseUrl: @@ -775,6 +920,11 @@ export const systemPropertyTypes = { propertyTypeBaseUrl: "https://hash.ai/@h/types/property-type/location/" as BaseUrl, }, + longitude: { + propertyTypeId: "https://hash.ai/@h/types/property-type/longitude/v/1", + propertyTypeBaseUrl: + "https://hash.ai/@h/types/property-type/longitude/" as BaseUrl, + }, machineIdentifier: { propertyTypeId: "https://hash.ai/@h/types/property-type/machine-identifier/v/1", @@ -905,6 +1055,12 @@ export const systemPropertyTypes = { propertyTypeBaseUrl: "https://hash.ai/@h/types/property-type/read-at/" as BaseUrl, }, + registrationNumber: { + propertyTypeId: + "https://hash.ai/@h/types/property-type/registration-number/v/1", + propertyTypeBaseUrl: + "https://hash.ai/@h/types/property-type/registration-number/" as BaseUrl, + }, resolvedAt: { propertyTypeId: "https://hash.ai/@h/types/property-type/resolved-at/v/1", propertyTypeBaseUrl: @@ -921,6 +1077,23 @@ export const systemPropertyTypes = { propertyTypeBaseUrl: "https://hash.ai/@h/types/property-type/rotation-in-rads/" as BaseUrl, }, + runway: { + propertyTypeId: "https://hash.ai/@h/types/property-type/runway/v/1", + propertyTypeBaseUrl: + "https://hash.ai/@h/types/property-type/runway/" as BaseUrl, + }, + scheduledGateTime: { + propertyTypeId: + "https://hash.ai/@h/types/property-type/scheduled-gate-time/v/1", + propertyTypeBaseUrl: + "https://hash.ai/@h/types/property-type/scheduled-gate-time/" as BaseUrl, + }, + scheduledRunwayTime: { + propertyTypeId: + "https://hash.ai/@h/types/property-type/scheduled-runway-time/v/1", + propertyTypeBaseUrl: + "https://hash.ai/@h/types/property-type/scheduled-runway-time/" as BaseUrl, + }, serviceName: { propertyTypeId: "https://hash.ai/@h/types/property-type/service-name/v/1", propertyTypeBaseUrl: @@ -973,11 +1146,21 @@ export const systemPropertyTypes = { propertyTypeBaseUrl: "https://hash.ai/@h/types/property-type/summary/" as BaseUrl, }, + terminal: { + propertyTypeId: "https://hash.ai/@h/types/property-type/terminal/v/1", + propertyTypeBaseUrl: + "https://hash.ai/@h/types/property-type/terminal/" as BaseUrl, + }, timeFrame: { propertyTypeId: "https://hash.ai/@h/types/property-type/time-frame/v/1", propertyTypeBaseUrl: "https://hash.ai/@h/types/property-type/time-frame/" as BaseUrl, }, + timezone: { + propertyTypeId: "https://hash.ai/@h/types/property-type/timezone/v/1", + propertyTypeBaseUrl: + "https://hash.ai/@h/types/property-type/timezone/" as BaseUrl, + }, title: { propertyTypeId: "https://hash.ai/@h/types/property-type/title/v/1", propertyTypeBaseUrl: @@ -1033,6 +1216,11 @@ export const systemPropertyTypes = { propertyTypeBaseUrl: "https://hash.ai/@h/types/property-type/vault-path/" as BaseUrl, }, + verticalSpeed: { + propertyTypeId: "https://hash.ai/@h/types/property-type/vertical-speed/v/1", + propertyTypeBaseUrl: + "https://hash.ai/@h/types/property-type/vertical-speed/" as BaseUrl, + }, websiteUrl: { propertyTypeId: "https://hash.ai/@h/types/property-type/website-url/v/1", propertyTypeBaseUrl: @@ -1072,6 +1260,13 @@ export const systemDataTypes = { title: "Actor Type", description: "The type of thing that can, should or will act on something.", }, + angle: { + dataTypeId: "https://hash.ai/@h/types/data-type/angle/v/1", + dataTypeBaseUrl: "https://hash.ai/@h/types/data-type/angle/" as BaseUrl, + title: "Angle", + description: + "A measure of rotation or the space between two intersecting lines.", + }, bits: { dataTypeId: "https://hash.ai/@h/types/data-type/bits/v/1", dataTypeBaseUrl: "https://hash.ai/@h/types/data-type/bits/" as BaseUrl, @@ -1120,6 +1315,12 @@ export const systemDataTypes = { description: "A measure of the length of time, defined as the time period of a full rotation of the Earth with respect to the Sun. On average, this is 24 hours.", }, + degree: { + dataTypeId: "https://hash.ai/@h/types/data-type/degree/v/1", + dataTypeBaseUrl: "https://hash.ai/@h/types/data-type/degree/" as BaseUrl, + title: "Degree", + description: "A unit of angular measure equal to 1/360 of a full rotation.", + }, doi: { dataTypeId: "https://hash.ai/@h/types/data-type/doi/v/1", dataTypeBaseUrl: "https://hash.ai/@h/types/data-type/doi/" as BaseUrl, @@ -1153,6 +1354,22 @@ export const systemDataTypes = { description: "An imperial unit of length. 3 feet equals 1 yard. Equivalent to 0.3048 meters in the International System of Units (SI).", }, + feetPerMinute: { + dataTypeId: "https://hash.ai/@h/types/data-type/feet-per-minute/v/1", + dataTypeBaseUrl: + "https://hash.ai/@h/types/data-type/feet-per-minute/" as BaseUrl, + title: "Feet per Minute", + description: + "A unit of vertical speed commonly used in aviation to measure rate of climb or descent.", + }, + flightStatus: { + dataTypeId: "https://hash.ai/@h/types/data-type/flight-status/v/1", + dataTypeBaseUrl: + "https://hash.ai/@h/types/data-type/flight-status/" as BaseUrl, + title: "Flight Status", + description: + "The current operational status of a flight, indicating whether it is scheduled, in progress, completed, or has encountered issues.", + }, frequency: { dataTypeId: "https://hash.ai/@h/types/data-type/frequency/v/1", dataTypeBaseUrl: "https://hash.ai/@h/types/data-type/frequency/" as BaseUrl, @@ -1272,6 +1489,14 @@ export const systemDataTypes = { description: "A unit of length in the International System of Units (SI), equal to one thousand meters.", }, + kilometersPerHour: { + dataTypeId: "https://hash.ai/@h/types/data-type/kilometers-per-hour/v/1", + dataTypeBaseUrl: + "https://hash.ai/@h/types/data-type/kilometers-per-hour/" as BaseUrl, + title: "Kilometers per Hour", + description: + "A unit of speed expressing the number of kilometers traveled in one hour.", + }, kilowatts: { dataTypeId: "https://hash.ai/@h/types/data-type/kilowatts/v/1", dataTypeBaseUrl: "https://hash.ai/@h/types/data-type/kilowatts/" as BaseUrl, @@ -1279,12 +1504,33 @@ export const systemDataTypes = { description: "A unit of power in the International System of Units (SI), equal to one thousand watts.", }, + knots: { + dataTypeId: "https://hash.ai/@h/types/data-type/knots/v/1", + dataTypeBaseUrl: "https://hash.ai/@h/types/data-type/knots/" as BaseUrl, + title: "Knots", + description: + "A unit of speed equal to one nautical mile per hour, commonly used in aviation and maritime contexts.", + }, + latitude: { + dataTypeId: "https://hash.ai/@h/types/data-type/latitude/v/1", + dataTypeBaseUrl: "https://hash.ai/@h/types/data-type/latitude/" as BaseUrl, + title: "Latitude", + description: + "The angular distance of a position north or south of the equator, ranging from -90° (South Pole) to +90° (North Pole).", + }, length: { dataTypeId: "https://hash.ai/@h/types/data-type/length/v/1", dataTypeBaseUrl: "https://hash.ai/@h/types/data-type/length/" as BaseUrl, title: "Length", description: "A measure of distance.", }, + longitude: { + dataTypeId: "https://hash.ai/@h/types/data-type/longitude/v/1", + dataTypeBaseUrl: "https://hash.ai/@h/types/data-type/longitude/" as BaseUrl, + title: "Longitude", + description: + "The angular distance of a position east or west of the prime meridian, ranging from -180° to +180°.", + }, megabytes: { dataTypeId: "https://hash.ai/@h/types/data-type/megabytes/v/1", dataTypeBaseUrl: "https://hash.ai/@h/types/data-type/megabytes/" as BaseUrl, @@ -1312,6 +1558,14 @@ export const systemDataTypes = { description: "The base unit of length in the International System of Units (SI).", }, + metersPerSecond: { + dataTypeId: "https://hash.ai/@h/types/data-type/meters-per-second/v/1", + dataTypeBaseUrl: + "https://hash.ai/@h/types/data-type/meters-per-second/" as BaseUrl, + title: "Meters per Second", + description: + "The SI unit of speed, expressing the number of meters traveled in one second.", + }, "metricLength(si)": { dataTypeId: "https://hash.ai/@h/types/data-type/metric-length-si/v/1", dataTypeBaseUrl: @@ -1392,6 +1646,13 @@ export const systemDataTypes = { description: "The base unit of duration in the International System of Units (SI), defined as about 9 billion oscillations of the caesium atom.", }, + speed: { + dataTypeId: "https://hash.ai/@h/types/data-type/speed/v/1", + dataTypeBaseUrl: "https://hash.ai/@h/types/data-type/speed/" as BaseUrl, + title: "Speed", + description: + "A measure of the rate of movement or change in position over time.", + }, terabytes: { dataTypeId: "https://hash.ai/@h/types/data-type/terabytes/v/1", dataTypeBaseUrl: "https://hash.ai/@h/types/data-type/terabytes/" as BaseUrl, diff --git a/libs/@local/hash-isomorphic-utils/src/system-types/aircraft.ts b/libs/@local/hash-isomorphic-utils/src/system-types/aircraft.ts new file mode 100644 index 00000000000..0d1056bddbe --- /dev/null +++ b/libs/@local/hash-isomorphic-utils/src/system-types/aircraft.ts @@ -0,0 +1,31 @@ +/** + * This file was automatically generated – do not edit it. + */ + +import type { + Aircraft, + AircraftOutgoingLinkAndTarget, + AircraftOutgoingLinksByLinkEntityTypeId, + AircraftProperties, + AircraftPropertiesWithMetadata, + ICAOCodePropertyValue, + ICAOCodePropertyValueWithMetadata, + RegistrationNumberPropertyValue, + RegistrationNumberPropertyValueWithMetadata, + TextDataType, + TextDataTypeWithMetadata, +} from "./shared.js"; + +export type { + Aircraft, + AircraftOutgoingLinkAndTarget, + AircraftOutgoingLinksByLinkEntityTypeId, + AircraftProperties, + AircraftPropertiesWithMetadata, + ICAOCodePropertyValue, + ICAOCodePropertyValueWithMetadata, + RegistrationNumberPropertyValue, + RegistrationNumberPropertyValueWithMetadata, + TextDataType, + TextDataTypeWithMetadata, +}; diff --git a/libs/@local/hash-isomorphic-utils/src/system-types/airline.ts b/libs/@local/hash-isomorphic-utils/src/system-types/airline.ts new file mode 100644 index 00000000000..d3e7746b2fd --- /dev/null +++ b/libs/@local/hash-isomorphic-utils/src/system-types/airline.ts @@ -0,0 +1,35 @@ +/** + * This file was automatically generated – do not edit it. + */ + +import type { + Airline, + AirlineOutgoingLinkAndTarget, + AirlineOutgoingLinksByLinkEntityTypeId, + AirlineProperties, + AirlinePropertiesWithMetadata, + IATACodePropertyValue, + IATACodePropertyValueWithMetadata, + ICAOCodePropertyValue, + ICAOCodePropertyValueWithMetadata, + NamePropertyValue, + NamePropertyValueWithMetadata, + TextDataType, + TextDataTypeWithMetadata, +} from "./shared.js"; + +export type { + Airline, + AirlineOutgoingLinkAndTarget, + AirlineOutgoingLinksByLinkEntityTypeId, + AirlineProperties, + AirlinePropertiesWithMetadata, + IATACodePropertyValue, + IATACodePropertyValueWithMetadata, + ICAOCodePropertyValue, + ICAOCodePropertyValueWithMetadata, + NamePropertyValue, + NamePropertyValueWithMetadata, + TextDataType, + TextDataTypeWithMetadata, +}; diff --git a/libs/@local/hash-isomorphic-utils/src/system-types/airport.ts b/libs/@local/hash-isomorphic-utils/src/system-types/airport.ts new file mode 100644 index 00000000000..cc91f98b51a --- /dev/null +++ b/libs/@local/hash-isomorphic-utils/src/system-types/airport.ts @@ -0,0 +1,43 @@ +/** + * This file was automatically generated – do not edit it. + */ + +import type { + Airport, + AirportOutgoingLinkAndTarget, + AirportOutgoingLinksByLinkEntityTypeId, + AirportProperties, + AirportPropertiesWithMetadata, + CityPropertyValue, + CityPropertyValueWithMetadata, + IATACodePropertyValue, + IATACodePropertyValueWithMetadata, + ICAOCodePropertyValue, + ICAOCodePropertyValueWithMetadata, + NamePropertyValue, + NamePropertyValueWithMetadata, + TextDataType, + TextDataTypeWithMetadata, + TimezonePropertyValue, + TimezonePropertyValueWithMetadata, +} from "./shared.js"; + +export type { + Airport, + AirportOutgoingLinkAndTarget, + AirportOutgoingLinksByLinkEntityTypeId, + AirportProperties, + AirportPropertiesWithMetadata, + CityPropertyValue, + CityPropertyValueWithMetadata, + IATACodePropertyValue, + IATACodePropertyValueWithMetadata, + ICAOCodePropertyValue, + ICAOCodePropertyValueWithMetadata, + NamePropertyValue, + NamePropertyValueWithMetadata, + TextDataType, + TextDataTypeWithMetadata, + TimezonePropertyValue, + TimezonePropertyValueWithMetadata, +}; diff --git a/libs/@local/hash-isomorphic-utils/src/system-types/flight.ts b/libs/@local/hash-isomorphic-utils/src/system-types/flight.ts new file mode 100644 index 00000000000..951233d1177 --- /dev/null +++ b/libs/@local/hash-isomorphic-utils/src/system-types/flight.ts @@ -0,0 +1,677 @@ +/** + * This file was automatically generated – do not edit it. + */ + +import type { + ArrayMetadata, + Confidence, + ObjectMetadata, + PropertyProvenance, +} from "@blockprotocol/type-system"; + +import type { + Aircraft, + AircraftOutgoingLinkAndTarget, + AircraftOutgoingLinksByLinkEntityTypeId, + AircraftProperties, + AircraftPropertiesWithMetadata, + Airline, + AirlineOutgoingLinkAndTarget, + AirlineOutgoingLinksByLinkEntityTypeId, + AirlineProperties, + AirlinePropertiesWithMetadata, + Airport, + AirportOutgoingLinkAndTarget, + AirportOutgoingLinksByLinkEntityTypeId, + AirportProperties, + AirportPropertiesWithMetadata, + BooleanDataType, + BooleanDataTypeWithMetadata, + CityPropertyValue, + CityPropertyValueWithMetadata, + DateDataType, + DateDataTypeWithMetadata, + DateTimeDataType, + DateTimeDataTypeWithMetadata, + IATACodePropertyValue, + IATACodePropertyValueWithMetadata, + ICAOCodePropertyValue, + ICAOCodePropertyValueWithMetadata, + IntegerDataType, + IntegerDataTypeWithMetadata, + Link, + LinkOutgoingLinkAndTarget, + LinkOutgoingLinksByLinkEntityTypeId, + LinkProperties, + LinkPropertiesWithMetadata, + NamePropertyValue, + NamePropertyValueWithMetadata, + NumberDataType, + NumberDataTypeWithMetadata, + RegistrationNumberPropertyValue, + RegistrationNumberPropertyValueWithMetadata, + TextDataType, + TextDataTypeWithMetadata, + TimezonePropertyValue, + TimezonePropertyValueWithMetadata, +} from "./shared.js"; + +export type { + Aircraft, + AircraftOutgoingLinkAndTarget, + AircraftOutgoingLinksByLinkEntityTypeId, + AircraftProperties, + AircraftPropertiesWithMetadata, + Airline, + AirlineOutgoingLinkAndTarget, + AirlineOutgoingLinksByLinkEntityTypeId, + AirlineProperties, + AirlinePropertiesWithMetadata, + Airport, + AirportOutgoingLinkAndTarget, + AirportOutgoingLinksByLinkEntityTypeId, + AirportProperties, + AirportPropertiesWithMetadata, + BooleanDataType, + BooleanDataTypeWithMetadata, + CityPropertyValue, + CityPropertyValueWithMetadata, + DateDataType, + DateDataTypeWithMetadata, + DateTimeDataType, + DateTimeDataTypeWithMetadata, + IATACodePropertyValue, + IATACodePropertyValueWithMetadata, + ICAOCodePropertyValue, + ICAOCodePropertyValueWithMetadata, + IntegerDataType, + IntegerDataTypeWithMetadata, + Link, + LinkOutgoingLinkAndTarget, + LinkOutgoingLinksByLinkEntityTypeId, + LinkProperties, + LinkPropertiesWithMetadata, + NamePropertyValue, + NamePropertyValueWithMetadata, + NumberDataType, + NumberDataTypeWithMetadata, + RegistrationNumberPropertyValue, + RegistrationNumberPropertyValueWithMetadata, + TextDataType, + TextDataTypeWithMetadata, + TimezonePropertyValue, + TimezonePropertyValueWithMetadata, +}; + +/** + * The actual date and time of gate departure (pushback) or arrival. + */ +export type ActualGateTimePropertyValue = DateTimeDataType; + +export type ActualGateTimePropertyValueWithMetadata = + DateTimeDataTypeWithMetadata; + +/** + * The actual date and time of runway departure (takeoff) or arrival (touchdown). + */ +export type ActualRunwayTimePropertyValue = DateTimeDataType; + +export type ActualRunwayTimePropertyValueWithMetadata = + DateTimeDataTypeWithMetadata; + +/** + * The height of an object above a reference point, such as sea level or the ground. + */ +export type AltitudePropertyValue = MetersDataType; + +export type AltitudePropertyValueWithMetadata = MetersDataTypeWithMetadata; + +/** + * A measure of rotation or the space between two intersecting lines. + */ +export type AngleDataType = NumberDataType; + +export type AngleDataTypeWithMetadata = { + value: AngleDataType; + metadata: AngleDataTypeMetadata; +}; +export type AngleDataTypeMetadata = { + provenance?: PropertyProvenance; + confidence?: Confidence; + dataTypeId: "https://hash.ai/@h/types/data-type/angle/v/1"; +}; + +/** + * Indicates the airport at which a flight arrives, including arrival-specific details. + */ +export type ArrivesAt = { + entityTypeIds: ["https://hash.ai/@h/types/entity-type/arrives-at/v/1"]; + properties: ArrivesAtProperties; + propertiesWithMetadata: ArrivesAtPropertiesWithMetadata; +}; + +export type ArrivesAtOutgoingLinkAndTarget = never; + +export type ArrivesAtOutgoingLinksByLinkEntityTypeId = {}; + +/** + * Indicates the airport at which a flight arrives, including arrival-specific details. + */ +export type ArrivesAtProperties = LinkProperties & { + "https://hash.ai/@h/types/property-type/actual-gate-time/"?: ActualGateTimePropertyValue; + "https://hash.ai/@h/types/property-type/actual-runway-time/"?: ActualRunwayTimePropertyValue; + "https://hash.ai/@h/types/property-type/baggage-claim/"?: BaggageClaimPropertyValue; + "https://hash.ai/@h/types/property-type/delay-in-seconds/"?: DelayInSecondsPropertyValue; + "https://hash.ai/@h/types/property-type/estimated-gate-time/"?: EstimatedGateTimePropertyValue; + "https://hash.ai/@h/types/property-type/estimated-runway-time/"?: EstimatedRunwayTimePropertyValue; + "https://hash.ai/@h/types/property-type/gate/"?: GatePropertyValue; + "https://hash.ai/@h/types/property-type/runway/"?: RunwayPropertyValue; + "https://hash.ai/@h/types/property-type/scheduled-gate-time/"?: ScheduledGateTimePropertyValue; + "https://hash.ai/@h/types/property-type/scheduled-runway-time/"?: ScheduledRunwayTimePropertyValue; + "https://hash.ai/@h/types/property-type/terminal/"?: TerminalPropertyValue; +}; + +export type ArrivesAtPropertiesWithMetadata = LinkPropertiesWithMetadata & { + metadata?: ObjectMetadata; + value: { + "https://hash.ai/@h/types/property-type/actual-gate-time/"?: ActualGateTimePropertyValueWithMetadata; + "https://hash.ai/@h/types/property-type/actual-runway-time/"?: ActualRunwayTimePropertyValueWithMetadata; + "https://hash.ai/@h/types/property-type/baggage-claim/"?: BaggageClaimPropertyValueWithMetadata; + "https://hash.ai/@h/types/property-type/delay-in-seconds/"?: DelayInSecondsPropertyValueWithMetadata; + "https://hash.ai/@h/types/property-type/estimated-gate-time/"?: EstimatedGateTimePropertyValueWithMetadata; + "https://hash.ai/@h/types/property-type/estimated-runway-time/"?: EstimatedRunwayTimePropertyValueWithMetadata; + "https://hash.ai/@h/types/property-type/gate/"?: GatePropertyValueWithMetadata; + "https://hash.ai/@h/types/property-type/runway/"?: RunwayPropertyValueWithMetadata; + "https://hash.ai/@h/types/property-type/scheduled-gate-time/"?: ScheduledGateTimePropertyValueWithMetadata; + "https://hash.ai/@h/types/property-type/scheduled-runway-time/"?: ScheduledRunwayTimePropertyValueWithMetadata; + "https://hash.ai/@h/types/property-type/terminal/"?: TerminalPropertyValueWithMetadata; + }; +}; + +/** + * The area or carousel number where passengers collect their checked luggage after a flight. + */ +export type BaggageClaimPropertyValue = TextDataType; + +export type BaggageClaimPropertyValueWithMetadata = TextDataTypeWithMetadata; + +/** + * A codeshare flight number, where multiple airlines sell seats on the same flight under their own flight numbers. + */ +export type CodesharePropertyValue = { + "https://hash.ai/@h/types/property-type/iata-code/"?: IATACodePropertyValue; + "https://hash.ai/@h/types/property-type/icao-code/"?: ICAOCodePropertyValue; +}; + +export type CodesharePropertyValueWithMetadata = { + metadata?: ObjectMetadata; + value: { + "https://hash.ai/@h/types/property-type/iata-code/"?: IATACodePropertyValueWithMetadata; + "https://hash.ai/@h/types/property-type/icao-code/"?: ICAOCodePropertyValueWithMetadata; + }; +}; + +/** + * A unit of angular measure equal to 1/360 of a full rotation. + */ +export type DegreeDataType = AngleDataType; + +export type DegreeDataTypeWithMetadata = { + value: DegreeDataType; + metadata: DegreeDataTypeMetadata; +}; +export type DegreeDataTypeMetadata = { + provenance?: PropertyProvenance; + confidence?: Confidence; + dataTypeId: "https://hash.ai/@h/types/data-type/degree/v/1"; +}; + +/** + * The amount of delay in seconds for a scheduled event such as a flight departure or arrival. + */ +export type DelayInSecondsPropertyValue = IntegerDataType; + +export type DelayInSecondsPropertyValueWithMetadata = + IntegerDataTypeWithMetadata; + +/** + * Indicates the airport from which a flight departs, including departure-specific details. + */ +export type DepartsFrom = { + entityTypeIds: ["https://hash.ai/@h/types/entity-type/departs-from/v/1"]; + properties: DepartsFromProperties; + propertiesWithMetadata: DepartsFromPropertiesWithMetadata; +}; + +export type DepartsFromOutgoingLinkAndTarget = never; + +export type DepartsFromOutgoingLinksByLinkEntityTypeId = {}; + +/** + * Indicates the airport from which a flight departs, including departure-specific details. + */ +export type DepartsFromProperties = LinkProperties & { + "https://hash.ai/@h/types/property-type/actual-gate-time/"?: ActualGateTimePropertyValue; + "https://hash.ai/@h/types/property-type/actual-runway-time/"?: ActualRunwayTimePropertyValue; + "https://hash.ai/@h/types/property-type/delay-in-seconds/"?: DelayInSecondsPropertyValue; + "https://hash.ai/@h/types/property-type/estimated-gate-time/"?: EstimatedGateTimePropertyValue; + "https://hash.ai/@h/types/property-type/estimated-runway-time/"?: EstimatedRunwayTimePropertyValue; + "https://hash.ai/@h/types/property-type/gate/"?: GatePropertyValue; + "https://hash.ai/@h/types/property-type/runway/"?: RunwayPropertyValue; + "https://hash.ai/@h/types/property-type/scheduled-gate-time/"?: ScheduledGateTimePropertyValue; + "https://hash.ai/@h/types/property-type/scheduled-runway-time/"?: ScheduledRunwayTimePropertyValue; + "https://hash.ai/@h/types/property-type/terminal/"?: TerminalPropertyValue; +}; + +export type DepartsFromPropertiesWithMetadata = LinkPropertiesWithMetadata & { + metadata?: ObjectMetadata; + value: { + "https://hash.ai/@h/types/property-type/actual-gate-time/"?: ActualGateTimePropertyValueWithMetadata; + "https://hash.ai/@h/types/property-type/actual-runway-time/"?: ActualRunwayTimePropertyValueWithMetadata; + "https://hash.ai/@h/types/property-type/delay-in-seconds/"?: DelayInSecondsPropertyValueWithMetadata; + "https://hash.ai/@h/types/property-type/estimated-gate-time/"?: EstimatedGateTimePropertyValueWithMetadata; + "https://hash.ai/@h/types/property-type/estimated-runway-time/"?: EstimatedRunwayTimePropertyValueWithMetadata; + "https://hash.ai/@h/types/property-type/gate/"?: GatePropertyValueWithMetadata; + "https://hash.ai/@h/types/property-type/runway/"?: RunwayPropertyValueWithMetadata; + "https://hash.ai/@h/types/property-type/scheduled-gate-time/"?: ScheduledGateTimePropertyValueWithMetadata; + "https://hash.ai/@h/types/property-type/scheduled-runway-time/"?: ScheduledRunwayTimePropertyValueWithMetadata; + "https://hash.ai/@h/types/property-type/terminal/"?: TerminalPropertyValueWithMetadata; + }; +}; + +/** + * The heading or bearing of something, measured in degrees from true north. + */ +export type DirectionPropertyValue = DegreeDataType; + +export type DirectionPropertyValueWithMetadata = DegreeDataTypeWithMetadata; + +/** + * The predicted date and time for gate departure (pushback) or arrival. + */ +export type EstimatedGateTimePropertyValue = DateTimeDataType; + +export type EstimatedGateTimePropertyValueWithMetadata = + DateTimeDataTypeWithMetadata; + +/** + * The predicted date and time for runway departure (takeoff) or arrival (touchdown). + */ +export type EstimatedRunwayTimePropertyValue = DateTimeDataType; + +export type EstimatedRunwayTimePropertyValueWithMetadata = + DateTimeDataTypeWithMetadata; + +/** + * A unit of vertical speed commonly used in aviation to measure rate of climb or descent. + */ +export type FeetPerMinuteDataType = SpeedDataType; + +export type FeetPerMinuteDataTypeWithMetadata = { + value: FeetPerMinuteDataType; + metadata: FeetPerMinuteDataTypeMetadata; +}; +export type FeetPerMinuteDataTypeMetadata = { + provenance?: PropertyProvenance; + confidence?: Confidence; + dataTypeId: "https://hash.ai/@h/types/data-type/feet-per-minute/v/1"; +}; + +/** + * A scheduled air transport service between two airports. + */ +export type Flight = { + entityTypeIds: ["https://hash.ai/@h/types/entity-type/flight/v/1"]; + properties: FlightProperties; + propertiesWithMetadata: FlightPropertiesWithMetadata; +}; + +export type FlightArrivesAtLink = { + linkEntity: ArrivesAt; + rightEntity: Airport; +}; + +/** + * The calendar date on which a flight is scheduled to operate. + */ +export type FlightDatePropertyValue = DateDataType; + +export type FlightDatePropertyValueWithMetadata = DateDataTypeWithMetadata; + +export type FlightDepartsFromLink = { + linkEntity: DepartsFrom; + rightEntity: Airport; +}; + +/** + * A numeric or alphanumeric code identifying a specific scheduled airline service. + */ +export type FlightNumberPropertyValue = TextDataType; + +export type FlightNumberPropertyValueWithMetadata = TextDataTypeWithMetadata; + +export type FlightOperatedByLink = { + linkEntity: OperatedBy; + rightEntity: Airline; +}; + +export type FlightOutgoingLinkAndTarget = + | FlightArrivesAtLink + | FlightDepartsFromLink + | FlightOperatedByLink + | FlightUsesAircraftLink; + +export type FlightOutgoingLinksByLinkEntityTypeId = { + "https://hash.ai/@h/types/entity-type/arrives-at/v/1": FlightArrivesAtLink; + "https://hash.ai/@h/types/entity-type/departs-from/v/1": FlightDepartsFromLink; + "https://hash.ai/@h/types/entity-type/operated-by/v/1": FlightOperatedByLink; + "https://hash.ai/@h/types/entity-type/uses-aircraft/v/1": FlightUsesAircraftLink; +}; + +/** + * A scheduled air transport service between two airports. + */ +export type FlightProperties = { + "https://hash.ai/@h/types/property-type/altitude/"?: AltitudePropertyValue; + "https://hash.ai/@h/types/property-type/codeshare/"?: CodesharePropertyValue[]; + "https://hash.ai/@h/types/property-type/direction/"?: DirectionPropertyValue; + "https://hash.ai/@h/types/property-type/flight-date/"?: FlightDatePropertyValue; + "https://hash.ai/@h/types/property-type/flight-number/": FlightNumberPropertyValue; + "https://hash.ai/@h/types/property-type/flight-status/"?: FlightStatusPropertyValue; + "https://hash.ai/@h/types/property-type/flight-type/"?: FlightTypePropertyValue; + "https://hash.ai/@h/types/property-type/ground-speed/"?: GroundSpeedPropertyValue; + "https://hash.ai/@h/types/property-type/iata-code/"?: IATACodePropertyValue; + "https://hash.ai/@h/types/property-type/icao-code/"?: ICAOCodePropertyValue; + "https://hash.ai/@h/types/property-type/is-on-ground/"?: IsOnGroundPropertyValue; + "https://hash.ai/@h/types/property-type/latitude/"?: LatitudePropertyValue; + "https://hash.ai/@h/types/property-type/longitude/"?: LongitudePropertyValue; + "https://hash.ai/@h/types/property-type/vertical-speed/"?: VerticalSpeedPropertyValue; +}; + +export type FlightPropertiesWithMetadata = { + metadata?: ObjectMetadata; + value: { + "https://hash.ai/@h/types/property-type/altitude/"?: AltitudePropertyValueWithMetadata; + "https://hash.ai/@h/types/property-type/codeshare/"?: { + value: CodesharePropertyValueWithMetadata[]; + metadata?: ArrayMetadata; + }; + "https://hash.ai/@h/types/property-type/direction/"?: DirectionPropertyValueWithMetadata; + "https://hash.ai/@h/types/property-type/flight-date/"?: FlightDatePropertyValueWithMetadata; + "https://hash.ai/@h/types/property-type/flight-number/": FlightNumberPropertyValueWithMetadata; + "https://hash.ai/@h/types/property-type/flight-status/"?: FlightStatusPropertyValueWithMetadata; + "https://hash.ai/@h/types/property-type/flight-type/"?: FlightTypePropertyValueWithMetadata; + "https://hash.ai/@h/types/property-type/ground-speed/"?: GroundSpeedPropertyValueWithMetadata; + "https://hash.ai/@h/types/property-type/iata-code/"?: IATACodePropertyValueWithMetadata; + "https://hash.ai/@h/types/property-type/icao-code/"?: ICAOCodePropertyValueWithMetadata; + "https://hash.ai/@h/types/property-type/is-on-ground/"?: IsOnGroundPropertyValueWithMetadata; + "https://hash.ai/@h/types/property-type/latitude/"?: LatitudePropertyValueWithMetadata; + "https://hash.ai/@h/types/property-type/longitude/"?: LongitudePropertyValueWithMetadata; + "https://hash.ai/@h/types/property-type/vertical-speed/"?: VerticalSpeedPropertyValueWithMetadata; + }; +}; + +/** + * The current operational status of a flight, indicating whether it is scheduled, in progress, completed, or has encountered issues. + */ +export type FlightStatusDataType = + | "Scheduled" + | "Active" + | "Landed" + | "Cancelled" + | "Incident" + | "Diverted"; + +export type FlightStatusDataTypeWithMetadata = { + value: FlightStatusDataType; + metadata: FlightStatusDataTypeMetadata; +}; +export type FlightStatusDataTypeMetadata = { + provenance?: PropertyProvenance; + confidence?: Confidence; + dataTypeId: "https://hash.ai/@h/types/data-type/flight-status/v/1"; +}; + +/** + * The current operational status of a flight. + */ +export type FlightStatusPropertyValue = FlightStatusDataType; + +export type FlightStatusPropertyValueWithMetadata = + FlightStatusDataTypeWithMetadata; + +/** + * The category of flight operation. + */ +export type FlightTypePropertyValue = TextDataType; + +export type FlightTypePropertyValueWithMetadata = TextDataTypeWithMetadata; + +export type FlightUsesAircraftLink = { + linkEntity: UsesAircraft; + rightEntity: Aircraft; +}; + +/** + * The gate number or identifier at an airport terminal where passengers board or disembark. + */ +export type GatePropertyValue = TextDataType; + +export type GatePropertyValueWithMetadata = TextDataTypeWithMetadata; + +/** + * The horizontal speed of an aircraft relative to the ground. + */ +export type GroundSpeedPropertyValue = KnotsDataType; + +export type GroundSpeedPropertyValueWithMetadata = KnotsDataTypeWithMetadata; + +/** + * Whether something is currently on the ground. + */ +export type IsOnGroundPropertyValue = BooleanDataType; + +export type IsOnGroundPropertyValueWithMetadata = BooleanDataTypeWithMetadata; + +/** + * A unit of speed equal to one nautical mile per hour, commonly used in aviation and maritime contexts. + */ +export type KnotsDataType = SpeedDataType; + +export type KnotsDataTypeWithMetadata = { + value: KnotsDataType; + metadata: KnotsDataTypeMetadata; +}; +export type KnotsDataTypeMetadata = { + provenance?: PropertyProvenance; + confidence?: Confidence; + dataTypeId: "https://hash.ai/@h/types/data-type/knots/v/1"; +}; + +/** + * The angular distance of a position north or south of the equator, ranging from -90° (South Pole) to +90° (North Pole). + */ +export type LatitudeDataType = DegreeDataType; + +export type LatitudeDataTypeWithMetadata = { + value: LatitudeDataType; + metadata: LatitudeDataTypeMetadata; +}; +export type LatitudeDataTypeMetadata = { + provenance?: PropertyProvenance; + confidence?: Confidence; + dataTypeId: "https://hash.ai/@h/types/data-type/latitude/v/1"; +}; + +/** + * The angular distance of a position north or south of the equator. + */ +export type LatitudePropertyValue = LatitudeDataType; + +export type LatitudePropertyValueWithMetadata = LatitudeDataTypeWithMetadata; + +/** + * A measure of distance. + */ +export type LengthDataType = NumberDataType; + +export type LengthDataTypeWithMetadata = { + value: LengthDataType; + metadata: LengthDataTypeMetadata; +}; +export type LengthDataTypeMetadata = { + provenance?: PropertyProvenance; + confidence?: Confidence; + dataTypeId: "https://hash.ai/@h/types/data-type/length/v/1"; +}; + +/** + * The angular distance of a position east or west of the prime meridian, ranging from -180° to +180°. + */ +export type LongitudeDataType = DegreeDataType; + +export type LongitudeDataTypeWithMetadata = { + value: LongitudeDataType; + metadata: LongitudeDataTypeMetadata; +}; +export type LongitudeDataTypeMetadata = { + provenance?: PropertyProvenance; + confidence?: Confidence; + dataTypeId: "https://hash.ai/@h/types/data-type/longitude/v/1"; +}; + +/** + * The angular distance of a position east or west of the prime meridian. + */ +export type LongitudePropertyValue = LongitudeDataType; + +export type LongitudePropertyValueWithMetadata = LongitudeDataTypeWithMetadata; + +/** + * The base unit of length in the International System of Units (SI). + */ +export type MetersDataType = MetricLengthSIDataType; + +export type MetersDataTypeWithMetadata = { + value: MetersDataType; + metadata: MetersDataTypeMetadata; +}; +export type MetersDataTypeMetadata = { + provenance?: PropertyProvenance; + confidence?: Confidence; + dataTypeId: "https://hash.ai/@h/types/data-type/meters/v/1"; +}; + +/** + * A measure of distance in the International System of Units (SI), the international standard for decimal-based measurements. + */ +export type MetricLengthSIDataType = LengthDataType; + +export type MetricLengthSIDataTypeWithMetadata = { + value: MetricLengthSIDataType; + metadata: MetricLengthSIDataTypeMetadata; +}; +export type MetricLengthSIDataTypeMetadata = { + provenance?: PropertyProvenance; + confidence?: Confidence; + dataTypeId: "https://hash.ai/@h/types/data-type/metric-length-si/v/1"; +}; + +/** + * Indicates the airline that operates a flight. + */ +export type OperatedBy = { + entityTypeIds: ["https://hash.ai/@h/types/entity-type/operated-by/v/1"]; + properties: OperatedByProperties; + propertiesWithMetadata: OperatedByPropertiesWithMetadata; +}; + +export type OperatedByOutgoingLinkAndTarget = never; + +export type OperatedByOutgoingLinksByLinkEntityTypeId = {}; + +/** + * Indicates the airline that operates a flight. + */ +export type OperatedByProperties = LinkProperties & {}; + +export type OperatedByPropertiesWithMetadata = LinkPropertiesWithMetadata & { + metadata?: ObjectMetadata; + value: {}; +}; + +/** + * The runway identifier used for takeoff or landing. + */ +export type RunwayPropertyValue = TextDataType; + +export type RunwayPropertyValueWithMetadata = TextDataTypeWithMetadata; + +/** + * The originally planned date and time for gate departure (pushback) or arrival. + */ +export type ScheduledGateTimePropertyValue = DateTimeDataType; + +export type ScheduledGateTimePropertyValueWithMetadata = + DateTimeDataTypeWithMetadata; + +/** + * The originally planned date and time for runway departure (takeoff) or arrival (touchdown). + */ +export type ScheduledRunwayTimePropertyValue = DateTimeDataType; + +export type ScheduledRunwayTimePropertyValueWithMetadata = + DateTimeDataTypeWithMetadata; + +/** + * A measure of the rate of movement or change in position over time. + */ +export type SpeedDataType = NumberDataType; + +export type SpeedDataTypeWithMetadata = { + value: SpeedDataType; + metadata: SpeedDataTypeMetadata; +}; +export type SpeedDataTypeMetadata = { + provenance?: PropertyProvenance; + confidence?: Confidence; + dataTypeId: "https://hash.ai/@h/types/data-type/speed/v/1"; +}; + +/** + * The terminal building or area at an airport where passengers check in, wait, and board flights. + */ +export type TerminalPropertyValue = TextDataType; + +export type TerminalPropertyValueWithMetadata = TextDataTypeWithMetadata; + +/** + * Indicates the aircraft used to operate a flight. + */ +export type UsesAircraft = { + entityTypeIds: ["https://hash.ai/@h/types/entity-type/uses-aircraft/v/1"]; + properties: UsesAircraftProperties; + propertiesWithMetadata: UsesAircraftPropertiesWithMetadata; +}; + +export type UsesAircraftOutgoingLinkAndTarget = never; + +export type UsesAircraftOutgoingLinksByLinkEntityTypeId = {}; + +/** + * Indicates the aircraft used to operate a flight. + */ +export type UsesAircraftProperties = LinkProperties & {}; + +export type UsesAircraftPropertiesWithMetadata = LinkPropertiesWithMetadata & { + metadata?: ObjectMetadata; + value: {}; +}; + +/** + * The rate of vertical movement (climb or descent). + */ +export type VerticalSpeedPropertyValue = FeetPerMinuteDataType; + +export type VerticalSpeedPropertyValueWithMetadata = + FeetPerMinuteDataTypeWithMetadata; diff --git a/libs/@local/hash-isomorphic-utils/src/system-types/shared.ts b/libs/@local/hash-isomorphic-utils/src/system-types/shared.ts index 6653b9a0255..5b63b6023e8 100644 --- a/libs/@local/hash-isomorphic-utils/src/system-types/shared.ts +++ b/libs/@local/hash-isomorphic-utils/src/system-types/shared.ts @@ -61,6 +61,101 @@ export type AffiliatedWithPropertiesWithMetadata = value: {}; }; +/** + * A vehicle designed for air travel, such as an airplane or helicopter. + */ +export type Aircraft = { + entityTypeIds: ["https://hash.ai/@h/types/entity-type/aircraft/v/1"]; + properties: AircraftProperties; + propertiesWithMetadata: AircraftPropertiesWithMetadata; +}; + +export type AircraftOutgoingLinkAndTarget = never; + +export type AircraftOutgoingLinksByLinkEntityTypeId = {}; + +/** + * A vehicle designed for air travel, such as an airplane or helicopter. + */ +export type AircraftProperties = { + "https://hash.ai/@h/types/property-type/icao-code/"?: ICAOCodePropertyValue; + "https://hash.ai/@h/types/property-type/registration-number/": RegistrationNumberPropertyValue; +}; + +export type AircraftPropertiesWithMetadata = { + metadata?: ObjectMetadata; + value: { + "https://hash.ai/@h/types/property-type/icao-code/"?: ICAOCodePropertyValueWithMetadata; + "https://hash.ai/@h/types/property-type/registration-number/": RegistrationNumberPropertyValueWithMetadata; + }; +}; + +/** + * A company that provides air transport services for passengers and/or cargo. + */ +export type Airline = { + entityTypeIds: ["https://hash.ai/@h/types/entity-type/airline/v/1"]; + properties: AirlineProperties; + propertiesWithMetadata: AirlinePropertiesWithMetadata; +}; + +export type AirlineOutgoingLinkAndTarget = never; + +export type AirlineOutgoingLinksByLinkEntityTypeId = {}; + +/** + * A company that provides air transport services for passengers and/or cargo. + */ +export type AirlineProperties = { + "https://blockprotocol.org/@blockprotocol/types/property-type/name/": NamePropertyValue; + "https://hash.ai/@h/types/property-type/iata-code/"?: IATACodePropertyValue; + "https://hash.ai/@h/types/property-type/icao-code/"?: ICAOCodePropertyValue; +}; + +export type AirlinePropertiesWithMetadata = { + metadata?: ObjectMetadata; + value: { + "https://blockprotocol.org/@blockprotocol/types/property-type/name/": NamePropertyValueWithMetadata; + "https://hash.ai/@h/types/property-type/iata-code/"?: IATACodePropertyValueWithMetadata; + "https://hash.ai/@h/types/property-type/icao-code/"?: ICAOCodePropertyValueWithMetadata; + }; +}; + +/** + * A facility where aircraft take off and land, with infrastructure for passenger and cargo services. + */ +export type Airport = { + entityTypeIds: ["https://hash.ai/@h/types/entity-type/airport/v/1"]; + properties: AirportProperties; + propertiesWithMetadata: AirportPropertiesWithMetadata; +}; + +export type AirportOutgoingLinkAndTarget = never; + +export type AirportOutgoingLinksByLinkEntityTypeId = {}; + +/** + * A facility where aircraft take off and land, with infrastructure for passenger and cargo services. + */ +export type AirportProperties = { + "https://blockprotocol.org/@blockprotocol/types/property-type/name/": NamePropertyValue; + "https://hash.ai/@h/types/property-type/city/"?: CityPropertyValue; + "https://hash.ai/@h/types/property-type/iata-code/"?: IATACodePropertyValue; + "https://hash.ai/@h/types/property-type/icao-code/"?: ICAOCodePropertyValue; + "https://hash.ai/@h/types/property-type/timezone/"?: TimezonePropertyValue; +}; + +export type AirportPropertiesWithMetadata = { + metadata?: ObjectMetadata; + value: { + "https://blockprotocol.org/@blockprotocol/types/property-type/name/": NamePropertyValueWithMetadata; + "https://hash.ai/@h/types/property-type/city/"?: CityPropertyValueWithMetadata; + "https://hash.ai/@h/types/property-type/iata-code/"?: IATACodePropertyValueWithMetadata; + "https://hash.ai/@h/types/property-type/icao-code/"?: ICAOCodePropertyValueWithMetadata; + "https://hash.ai/@h/types/property-type/timezone/"?: TimezonePropertyValueWithMetadata; + }; +}; + /** * A user or other entity's preferences for how an application should behave or appear */ @@ -249,6 +344,13 @@ export type BytesDataTypeMetadata = { dataTypeId: "https://hash.ai/@h/types/data-type/bytes/v/1"; }; +/** + * The city where something is located, occurred, etc. + */ +export type CityPropertyValue = TextDataType; + +export type CityPropertyValueWithMetadata = TextDataTypeWithMetadata; + /** * Comment associated with the issue. */ @@ -341,6 +443,21 @@ export type DOIPropertyValue = DOIDataType; export type DOIPropertyValueWithMetadata = DOIDataTypeWithMetadata; +/** + * A reference to a particular day represented within a calendar system, formatted according to RFC 3339. + */ +export type DateDataType = TextDataType; + +export type DateDataTypeWithMetadata = { + value: DateDataType; + metadata: DateDataTypeMetadata; +}; +export type DateDataTypeMetadata = { + provenance?: PropertyProvenance; + confidence?: Confidence; + dataTypeId: "https://hash.ai/@h/types/data-type/date/v/1"; +}; + /** * A reference to a particular date and time, formatted according to RFC 3339. */ @@ -928,6 +1045,20 @@ export type HasTextPropertiesWithMetadata = LinkPropertiesWithMetadata & { value: {}; }; +/** + * A code assigned by the International Air Transport Association (IATA) to identify airports, airlines, or aircraft types. + */ +export type IATACodePropertyValue = TextDataType; + +export type IATACodePropertyValueWithMetadata = TextDataTypeWithMetadata; + +/** + * A code assigned by the International Civil Aviation Organization (ICAO) to identify airports, airlines, or aircraft types. + */ +export type ICAOCodePropertyValue = TextDataType; + +export type ICAOCodePropertyValueWithMetadata = TextDataTypeWithMetadata; + /** * An emoji icon. */ @@ -1666,6 +1797,14 @@ export type ReadAtPropertyValue = DateTimeDataType; export type ReadAtPropertyValueWithMetadata = DateTimeDataTypeWithMetadata; +/** + * A unique alphanumeric code assigned to an aircraft, also known as a tail number (e.g. 'N123AB'). + */ +export type RegistrationNumberPropertyValue = TextDataType; + +export type RegistrationNumberPropertyValueWithMetadata = + TextDataTypeWithMetadata; + /** * Stringified timestamp of when something was resolved. */ @@ -1839,6 +1978,13 @@ export type TextualContentPropertyValueWithMetadata = metadata?: ArrayMetadata; }; +/** + * A time zone identifier (e.g. 'America/Los_Angeles', 'Europe/London'). + */ +export type TimezonePropertyValue = TextDataType; + +export type TimezonePropertyValueWithMetadata = TextDataTypeWithMetadata; + /** * The title of something. */ diff --git a/libs/@local/hash-isomorphic-utils/src/system-types/studyrecord.ts b/libs/@local/hash-isomorphic-utils/src/system-types/studyrecord.ts index f73f5b7bcf5..efe4698710a 100644 --- a/libs/@local/hash-isomorphic-utils/src/system-types/studyrecord.ts +++ b/libs/@local/hash-isomorphic-utils/src/system-types/studyrecord.ts @@ -20,6 +20,8 @@ import type { AuthoredByOutgoingLinksByLinkEntityTypeId, AuthoredByProperties, AuthoredByPropertiesWithMetadata, + DateDataType, + DateDataTypeWithMetadata, DescriptionPropertyValue, DescriptionPropertyValueWithMetadata, Doc, @@ -91,6 +93,8 @@ export type { AuthoredByOutgoingLinksByLinkEntityTypeId, AuthoredByProperties, AuthoredByPropertiesWithMetadata, + DateDataType, + DateDataTypeWithMetadata, DescriptionPropertyValue, DescriptionPropertyValueWithMetadata, Doc, @@ -183,21 +187,6 @@ export type ActualStudyStartDatePropertyValue = DateDataType; export type ActualStudyStartDatePropertyValueWithMetadata = DateDataTypeWithMetadata; -/** - * A reference to a particular day represented within a calendar system, formatted according to RFC 3339. - */ -export type DateDataType = TextDataType; - -export type DateDataTypeWithMetadata = { - value: DateDataType; - metadata: DateDataTypeMetadata; -}; -export type DateDataTypeMetadata = { - provenance?: PropertyProvenance; - confidence?: Confidence; - dataTypeId: "https://hash.ai/@h/types/data-type/date/v/1"; -}; - /** * The estimated number of participants that will be enrolled in something. */ diff --git a/yarn.lock b/yarn.lock index a8c15ccecef..a6b59358c2d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -987,6 +987,7 @@ __metadata: "@local/hash-graph-client": "npm:0.0.0-private" "@local/hash-graph-sdk": "npm:0.0.0-private" "@local/hash-isomorphic-utils": "npm:0.0.0-private" + "@local/status": "npm:0.0.0-private" "@local/tsconfig": "npm:0.0.0-private" "@sentry/node": "npm:10.27.0" "@temporalio/activity": "npm:1.12.1" @@ -995,6 +996,7 @@ __metadata: "@types/dotenv-flow": "npm:3.3.3" agentkeepalive: "npm:4.6.0" axios: "npm:1.12.2" + cache-manager: "npm:5.7.6" dotenv-flow: "npm:3.3.0" eslint: "npm:9.38.0" rimraf: "npm:6.1.2"