@doclo/flows 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +80 -2
- package/dist/index.js +370 -9
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -468,7 +468,7 @@ type SerializableFlow = {
|
|
|
468
468
|
/**
|
|
469
469
|
* Serializable step definition
|
|
470
470
|
*/
|
|
471
|
-
type SerializableStep = SerializableStandardStep | SerializableConditionalStep | SerializableForEachStep;
|
|
471
|
+
type SerializableStep = SerializableStandardStep | SerializableConditionalStep | SerializableForEachStep | SerializableRouteStep;
|
|
472
472
|
/**
|
|
473
473
|
* Standard sequential step
|
|
474
474
|
*/
|
|
@@ -514,6 +514,56 @@ type SerializableForEachStep = {
|
|
|
514
514
|
config: SplitConfig;
|
|
515
515
|
itemFlow: SerializableFlow | FlowReference;
|
|
516
516
|
};
|
|
517
|
+
/**
|
|
518
|
+
* Route branch configuration
|
|
519
|
+
* Maps multiple MIME types to a single branch
|
|
520
|
+
*/
|
|
521
|
+
type RouteBranchConfig = {
|
|
522
|
+
/**
|
|
523
|
+
* MIME types that route to this branch.
|
|
524
|
+
* Supports exact matches (e.g., 'application/pdf')
|
|
525
|
+
* and glob patterns (e.g., 'image/*')
|
|
526
|
+
*/
|
|
527
|
+
mimeTypes: string[];
|
|
528
|
+
/**
|
|
529
|
+
* Optional description for documentation/debugging
|
|
530
|
+
*/
|
|
531
|
+
description?: string;
|
|
532
|
+
};
|
|
533
|
+
/**
|
|
534
|
+
* Route step configuration
|
|
535
|
+
* Routes documents based on detected MIME type (no provider required)
|
|
536
|
+
*/
|
|
537
|
+
type RouteConfig = {
|
|
538
|
+
type: 'route';
|
|
539
|
+
/**
|
|
540
|
+
* Branch definitions mapping branch names to MIME type configurations.
|
|
541
|
+
* Order matters for matching - first matching branch wins.
|
|
542
|
+
*/
|
|
543
|
+
branches: Record<string, RouteBranchConfig>;
|
|
544
|
+
};
|
|
545
|
+
/**
|
|
546
|
+
* Route step (MIME-based routing + branches)
|
|
547
|
+
*
|
|
548
|
+
* Unlike conditional (which uses VLM categorization), route uses
|
|
549
|
+
* deterministic MIME detection from magic bytes - no provider needed.
|
|
550
|
+
*
|
|
551
|
+
* Branches can be either inline flows or references to separate flows.
|
|
552
|
+
* Use references to avoid hitting database JSON nesting limits.
|
|
553
|
+
*/
|
|
554
|
+
type SerializableRouteStep = {
|
|
555
|
+
type: 'route';
|
|
556
|
+
id: string;
|
|
557
|
+
name?: string;
|
|
558
|
+
nodeType: 'route';
|
|
559
|
+
config: RouteConfig;
|
|
560
|
+
branches: Record<string, SerializableFlow | FlowReference>;
|
|
561
|
+
/**
|
|
562
|
+
* Fallback branch for unmatched MIME types.
|
|
563
|
+
* If not provided and no branch matches, throws error with details.
|
|
564
|
+
*/
|
|
565
|
+
others?: SerializableFlow | FlowReference;
|
|
566
|
+
};
|
|
517
567
|
/**
|
|
518
568
|
* Input mapping configuration for trigger nodes
|
|
519
569
|
* Declarative alternatives to mapInput functions (for serialization)
|
|
@@ -555,6 +605,9 @@ type ParseConfig = {
|
|
|
555
605
|
onTie?: 'random' | 'fail' | 'retry';
|
|
556
606
|
};
|
|
557
607
|
maxTokens?: number;
|
|
608
|
+
promptRef?: string;
|
|
609
|
+
promptVariables?: Record<string, any>;
|
|
610
|
+
additionalInstructions?: string;
|
|
558
611
|
};
|
|
559
612
|
type ExtractConfig = {
|
|
560
613
|
type: 'extract';
|
|
@@ -571,6 +624,9 @@ type ExtractConfig = {
|
|
|
571
624
|
max_tokens?: number;
|
|
572
625
|
};
|
|
573
626
|
maxTokens?: number;
|
|
627
|
+
promptRef?: string;
|
|
628
|
+
promptVariables?: Record<string, any>;
|
|
629
|
+
additionalInstructions?: string;
|
|
574
630
|
};
|
|
575
631
|
type SplitConfig = {
|
|
576
632
|
type: 'split';
|
|
@@ -606,6 +662,8 @@ type CategorizeConfig = {
|
|
|
606
662
|
onTie?: 'random' | 'fail' | 'retry';
|
|
607
663
|
};
|
|
608
664
|
promptRef?: string;
|
|
665
|
+
promptVariables?: Record<string, any>;
|
|
666
|
+
additionalInstructions?: string;
|
|
609
667
|
maxTokens?: number;
|
|
610
668
|
};
|
|
611
669
|
type TriggerConfig = {
|
|
@@ -781,6 +839,26 @@ interface ForEachCompositeConfig {
|
|
|
781
839
|
* Includes full observability, metrics merging, and error context.
|
|
782
840
|
*/
|
|
783
841
|
declare function createForEachCompositeNode(config: ForEachCompositeConfig): NodeDef<FlowInput, unknown[]>;
|
|
842
|
+
/**
|
|
843
|
+
* Configuration for route composite node
|
|
844
|
+
*/
|
|
845
|
+
interface RouteCompositeConfig {
|
|
846
|
+
stepId: string;
|
|
847
|
+
routeConfig: RouteConfig;
|
|
848
|
+
branches: Record<string, SerializableFlow | FlowReference>;
|
|
849
|
+
others?: SerializableFlow | FlowReference;
|
|
850
|
+
providers: ProviderRegistry;
|
|
851
|
+
flows: FlowRegistry;
|
|
852
|
+
}
|
|
853
|
+
/**
|
|
854
|
+
* Creates a composite node that:
|
|
855
|
+
* 1. Detects MIME type from input (base64 or URL)
|
|
856
|
+
* 2. Routes to appropriate branch based on MIME type
|
|
857
|
+
* 3. Returns the branch flow's output
|
|
858
|
+
*
|
|
859
|
+
* No provider required - uses deterministic MIME detection from magic bytes.
|
|
860
|
+
*/
|
|
861
|
+
declare function createRouteCompositeNode(config: RouteCompositeConfig): NodeDef<FlowInput, unknown>;
|
|
784
862
|
|
|
785
863
|
/**
|
|
786
864
|
* Flow Validation
|
|
@@ -961,4 +1039,4 @@ declare function buildTwoProviderFlow(opts: {
|
|
|
961
1039
|
}>;
|
|
962
1040
|
};
|
|
963
1041
|
|
|
964
|
-
export { type BatchFlowResult, type BuiltFlow, type CategorizeConfig, type ConditionalCompositeConfig, type ExtractConfig, FLOW_REGISTRY, type FieldMapping, type FlowBuilder, type FlowOptions, type FlowProgressCallbacks, type FlowReference, type FlowRegistry$1 as FlowRegistry, type FlowRunOptions, FlowSerializationError, type FlowValidationResult, type ForEachCompositeConfig, type InputMappingConfig, type NodeConfig, type OutputConfig, type ParseConfig, type ProviderRegistry, type SerializableConditionalStep, type SerializableFlow, type SerializableForEachStep, type SerializableInputValidation, type SerializableStandardStep, type SerializableStep, type SplitConfig, type TriggerConfig, type ValidationError, type ValidationOptions, type ValidationResult, type ValidationWarning, buildFlowFromConfig, buildMultiProviderFlow, buildTwoProviderFlow, buildVLMDirectFlow, clearRegistry, createConditionalCompositeNode, createFlow, createForEachCompositeNode, defineFlowConfig, extractNodeMetadata, getFlow, getFlowCount, hasFlow, isBatchFlowResult, isFlowReference, isSingleFlowResult, listFlows, registerFlow, resolveFlowReference, unregisterFlow, validateFlow, validateFlowOrThrow };
|
|
1042
|
+
export { type BatchFlowResult, type BuiltFlow, type CategorizeConfig, type ConditionalCompositeConfig, type ExtractConfig, FLOW_REGISTRY, type FieldMapping, type FlowBuilder, type FlowOptions, type FlowProgressCallbacks, type FlowReference, type FlowRegistry$1 as FlowRegistry, type FlowRunOptions, FlowSerializationError, type FlowValidationResult, type ForEachCompositeConfig, type InputMappingConfig, type NodeConfig, type OutputConfig, type ParseConfig, type ProviderRegistry, type RouteBranchConfig, type RouteCompositeConfig, type RouteConfig, type SerializableConditionalStep, type SerializableFlow, type SerializableForEachStep, type SerializableInputValidation, type SerializableRouteStep, type SerializableStandardStep, type SerializableStep, type SplitConfig, type TriggerConfig, type ValidationError, type ValidationOptions, type ValidationResult, type ValidationWarning, buildFlowFromConfig, buildMultiProviderFlow, buildTwoProviderFlow, buildVLMDirectFlow, clearRegistry, createConditionalCompositeNode, createFlow, createForEachCompositeNode, createRouteCompositeNode, defineFlowConfig, extractNodeMetadata, getFlow, getFlowCount, hasFlow, isBatchFlowResult, isFlowReference, isSingleFlowResult, listFlows, registerFlow, resolveFlowReference, unregisterFlow, validateFlow, validateFlowOrThrow };
|
package/dist/index.js
CHANGED
|
@@ -53,6 +53,15 @@ function normalizeFlowInput(input) {
|
|
|
53
53
|
}
|
|
54
54
|
return input;
|
|
55
55
|
}
|
|
56
|
+
function filterInternalArtifacts(artifacts) {
|
|
57
|
+
const filtered = {};
|
|
58
|
+
for (const [key, value] of Object.entries(artifacts)) {
|
|
59
|
+
if (!key.startsWith("__")) {
|
|
60
|
+
filtered[key] = value;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return filtered;
|
|
64
|
+
}
|
|
56
65
|
var Flow = class {
|
|
57
66
|
steps = [];
|
|
58
67
|
observability;
|
|
@@ -1051,7 +1060,7 @@ Correct: .conditional('step', () => parse({ provider }))`
|
|
|
1051
1060
|
results: flowResults,
|
|
1052
1061
|
metrics,
|
|
1053
1062
|
aggregated: aggregateMetrics(metrics),
|
|
1054
|
-
artifacts
|
|
1063
|
+
artifacts: filterInternalArtifacts(artifacts)
|
|
1055
1064
|
};
|
|
1056
1065
|
}
|
|
1057
1066
|
} catch (error) {
|
|
@@ -1115,7 +1124,7 @@ Correct: .conditional('step', () => parse({ provider }))`
|
|
|
1115
1124
|
outputs,
|
|
1116
1125
|
metrics,
|
|
1117
1126
|
aggregated: aggregateMetrics(metrics),
|
|
1118
|
-
artifacts
|
|
1127
|
+
artifacts: filterInternalArtifacts(artifacts)
|
|
1119
1128
|
};
|
|
1120
1129
|
} else {
|
|
1121
1130
|
result = {
|
|
@@ -1124,7 +1133,7 @@ Correct: .conditional('step', () => parse({ provider }))`
|
|
|
1124
1133
|
outputs,
|
|
1125
1134
|
metrics,
|
|
1126
1135
|
aggregated: aggregateMetrics(metrics),
|
|
1127
|
-
artifacts
|
|
1136
|
+
artifacts: filterInternalArtifacts(artifacts)
|
|
1128
1137
|
};
|
|
1129
1138
|
}
|
|
1130
1139
|
} else {
|
|
@@ -1132,7 +1141,7 @@ Correct: .conditional('step', () => parse({ provider }))`
|
|
|
1132
1141
|
output: currentData,
|
|
1133
1142
|
metrics,
|
|
1134
1143
|
aggregated: aggregateMetrics(metrics),
|
|
1135
|
-
artifacts
|
|
1144
|
+
artifacts: filterInternalArtifacts(artifacts)
|
|
1136
1145
|
};
|
|
1137
1146
|
}
|
|
1138
1147
|
if (this.observability && sampled && traceContext && executionId) {
|
|
@@ -1243,6 +1252,7 @@ import { parse, extract, split as split2, categorize as categorize2, trigger, ou
|
|
|
1243
1252
|
|
|
1244
1253
|
// src/composite-nodes.ts
|
|
1245
1254
|
import { FlowExecutionError as FlowExecutionError2 } from "@doclo/core";
|
|
1255
|
+
import { detectMimeTypeFromBase64Async, detectMimeTypeFromBase64 } from "@doclo/core";
|
|
1246
1256
|
import { categorize, split } from "@doclo/nodes";
|
|
1247
1257
|
function parseProviderName(name) {
|
|
1248
1258
|
const colonIndex = name.indexOf(":");
|
|
@@ -1622,6 +1632,214 @@ function resolveBranchFlow(flowOrRef, flows) {
|
|
|
1622
1632
|
}
|
|
1623
1633
|
return flowOrRef;
|
|
1624
1634
|
}
|
|
1635
|
+
function matchesMimePattern(mimeType, pattern) {
|
|
1636
|
+
if (pattern === mimeType) return true;
|
|
1637
|
+
if (pattern.endsWith("/*")) {
|
|
1638
|
+
const prefix = pattern.slice(0, -2);
|
|
1639
|
+
return mimeType.startsWith(prefix + "/");
|
|
1640
|
+
}
|
|
1641
|
+
return false;
|
|
1642
|
+
}
|
|
1643
|
+
function findMatchingBranch(mimeType, branches) {
|
|
1644
|
+
for (const [branchName, config] of Object.entries(branches)) {
|
|
1645
|
+
for (const pattern of config.mimeTypes) {
|
|
1646
|
+
if (matchesMimePattern(mimeType, pattern)) {
|
|
1647
|
+
return branchName;
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
return void 0;
|
|
1652
|
+
}
|
|
1653
|
+
function extractBase64Data(input) {
|
|
1654
|
+
if (input.startsWith("data:")) {
|
|
1655
|
+
const commaIndex = input.indexOf(",");
|
|
1656
|
+
if (commaIndex !== -1) {
|
|
1657
|
+
return input.substring(commaIndex + 1);
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
return input;
|
|
1661
|
+
}
|
|
1662
|
+
function createRouteCompositeNode(config) {
|
|
1663
|
+
const { stepId, routeConfig, branches, others, providers, flows } = config;
|
|
1664
|
+
return {
|
|
1665
|
+
key: "route-composite",
|
|
1666
|
+
run: async (input, ctx) => {
|
|
1667
|
+
const t0 = Date.now();
|
|
1668
|
+
let detectedMimeType;
|
|
1669
|
+
let selectedBranch;
|
|
1670
|
+
let phase = "detect";
|
|
1671
|
+
try {
|
|
1672
|
+
let base64Data;
|
|
1673
|
+
if (input.base64) {
|
|
1674
|
+
base64Data = extractBase64Data(input.base64);
|
|
1675
|
+
} else if (input.url) {
|
|
1676
|
+
const response = await fetch(input.url);
|
|
1677
|
+
if (!response.ok) {
|
|
1678
|
+
throw new Error(`Failed to fetch URL: ${response.status} ${response.statusText}`);
|
|
1679
|
+
}
|
|
1680
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
1681
|
+
const bytes = new Uint8Array(arrayBuffer);
|
|
1682
|
+
let binary = "";
|
|
1683
|
+
for (let i = 0; i < bytes.byteLength; i++) {
|
|
1684
|
+
binary += String.fromCharCode(bytes[i]);
|
|
1685
|
+
}
|
|
1686
|
+
base64Data = btoa(binary);
|
|
1687
|
+
} else {
|
|
1688
|
+
throw new Error(
|
|
1689
|
+
"Route node requires either url or base64 input for MIME detection"
|
|
1690
|
+
);
|
|
1691
|
+
}
|
|
1692
|
+
try {
|
|
1693
|
+
detectedMimeType = await detectMimeTypeFromBase64Async(base64Data);
|
|
1694
|
+
} catch {
|
|
1695
|
+
try {
|
|
1696
|
+
detectedMimeType = detectMimeTypeFromBase64(base64Data);
|
|
1697
|
+
} catch (syncErr) {
|
|
1698
|
+
throw new Error(
|
|
1699
|
+
`Unable to detect MIME type from input. Ensure the document is a supported format. Error: ${syncErr instanceof Error ? syncErr.message : String(syncErr)}`
|
|
1700
|
+
);
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
if (ctx?.emit) {
|
|
1704
|
+
ctx.emit(`${stepId}:detectedMimeType`, detectedMimeType);
|
|
1705
|
+
}
|
|
1706
|
+
phase = "route";
|
|
1707
|
+
selectedBranch = findMatchingBranch(detectedMimeType, routeConfig.branches);
|
|
1708
|
+
if (!selectedBranch && others) {
|
|
1709
|
+
selectedBranch = "others";
|
|
1710
|
+
}
|
|
1711
|
+
if (!selectedBranch) {
|
|
1712
|
+
const availableBranches = Object.entries(routeConfig.branches).map(([name, cfg]) => `${name}: [${cfg.mimeTypes.join(", ")}]`).join("; ");
|
|
1713
|
+
throw new Error(
|
|
1714
|
+
`No branch matches MIME type "${detectedMimeType}". Available branches: ${availableBranches}. Add an 'others' fallback branch to handle unmatched types.`
|
|
1715
|
+
);
|
|
1716
|
+
}
|
|
1717
|
+
if (ctx?.emit) {
|
|
1718
|
+
ctx.emit(`${stepId}:selectedBranch`, selectedBranch);
|
|
1719
|
+
}
|
|
1720
|
+
phase = "branch";
|
|
1721
|
+
const branchFlowOrRef = selectedBranch === "others" ? others : branches[selectedBranch];
|
|
1722
|
+
if (!branchFlowOrRef) {
|
|
1723
|
+
throw new Error(
|
|
1724
|
+
`Branch "${selectedBranch}" not found in route configuration`
|
|
1725
|
+
);
|
|
1726
|
+
}
|
|
1727
|
+
const branchFlowDef = resolveBranchFlow(branchFlowOrRef, flows);
|
|
1728
|
+
const branchFlow = buildFlowFromConfig(
|
|
1729
|
+
branchFlowDef,
|
|
1730
|
+
providers,
|
|
1731
|
+
flows,
|
|
1732
|
+
ctx?.observability?.config ? {
|
|
1733
|
+
observability: ctx.observability.config,
|
|
1734
|
+
metadata: {
|
|
1735
|
+
...ctx.observability?.metadata,
|
|
1736
|
+
parentNode: stepId,
|
|
1737
|
+
phase: "branch",
|
|
1738
|
+
detectedMimeType,
|
|
1739
|
+
selectedBranch
|
|
1740
|
+
}
|
|
1741
|
+
} : void 0
|
|
1742
|
+
);
|
|
1743
|
+
const branchT0 = Date.now();
|
|
1744
|
+
const branchResultRaw = await branchFlow.run(input);
|
|
1745
|
+
if (!isSingleFlowResult(branchResultRaw)) {
|
|
1746
|
+
throw new Error("Branch flow returned batch result instead of single result");
|
|
1747
|
+
}
|
|
1748
|
+
const branchResult = branchResultRaw;
|
|
1749
|
+
if (ctx?.metrics && branchResult.metrics) {
|
|
1750
|
+
const branchMetrics = flattenMetrics(
|
|
1751
|
+
branchResult.metrics,
|
|
1752
|
+
`${stepId}.branch.${selectedBranch}`
|
|
1753
|
+
);
|
|
1754
|
+
branchMetrics.forEach((m) => ctx.metrics.push(m));
|
|
1755
|
+
}
|
|
1756
|
+
if (ctx?.emit) {
|
|
1757
|
+
ctx.emit(`${stepId}:branchOutput`, branchResult.output);
|
|
1758
|
+
ctx.emit(`${stepId}:branchArtifacts`, branchResult.artifacts);
|
|
1759
|
+
}
|
|
1760
|
+
const branchCost = branchResult.metrics ? branchResult.metrics.reduce((sum, m) => sum + (m.costUSD ?? 0), 0) : 0;
|
|
1761
|
+
const totalMs = Date.now() - t0;
|
|
1762
|
+
const branchMs = branchResult.metrics ? branchResult.metrics.reduce((sum, m) => sum + (m.ms ?? 0), 0) : 0;
|
|
1763
|
+
const overheadMs = totalMs - branchMs;
|
|
1764
|
+
if (ctx?.metrics) {
|
|
1765
|
+
const wrapperMetric = {
|
|
1766
|
+
step: stepId,
|
|
1767
|
+
configStepId: ctx.stepId,
|
|
1768
|
+
startMs: t0,
|
|
1769
|
+
provider: "internal",
|
|
1770
|
+
// No external provider - uses built-in MIME detection
|
|
1771
|
+
model: "mime-detection",
|
|
1772
|
+
ms: totalMs,
|
|
1773
|
+
costUSD: branchCost,
|
|
1774
|
+
// Total cost from branch (MIME detection is free)
|
|
1775
|
+
attemptNumber: 1,
|
|
1776
|
+
// Composite wrappers don't retry, always 1
|
|
1777
|
+
metadata: {
|
|
1778
|
+
kind: "wrapper",
|
|
1779
|
+
// Distinguish wrapper from leaf metrics
|
|
1780
|
+
type: "route",
|
|
1781
|
+
rollup: true,
|
|
1782
|
+
// Duration includes child work
|
|
1783
|
+
overheadMs,
|
|
1784
|
+
// Pure wrapper overhead (MIME detection + routing)
|
|
1785
|
+
detectedMimeType,
|
|
1786
|
+
selectedBranch,
|
|
1787
|
+
branchStepCount: branchResult.metrics?.length || 0,
|
|
1788
|
+
branchFlowId: selectedBranch === "others" ? others && typeof others === "object" && "flowRef" in others ? others.flowRef : "inline" : typeof branches[selectedBranch] === "object" && "flowRef" in branches[selectedBranch] ? branches[selectedBranch].flowRef : "inline"
|
|
1789
|
+
}
|
|
1790
|
+
};
|
|
1791
|
+
ctx.metrics.push(wrapperMetric);
|
|
1792
|
+
}
|
|
1793
|
+
return branchResult.output;
|
|
1794
|
+
} catch (error) {
|
|
1795
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
1796
|
+
const isNestedFlowError = err instanceof FlowExecutionError2;
|
|
1797
|
+
if (ctx?.metrics) {
|
|
1798
|
+
ctx.metrics.push({
|
|
1799
|
+
step: stepId,
|
|
1800
|
+
configStepId: ctx.stepId,
|
|
1801
|
+
startMs: t0,
|
|
1802
|
+
ms: Date.now() - t0,
|
|
1803
|
+
costUSD: 0,
|
|
1804
|
+
attemptNumber: 1,
|
|
1805
|
+
// @ts-ignore - Add error field
|
|
1806
|
+
error: err.message,
|
|
1807
|
+
metadata: {
|
|
1808
|
+
kind: "wrapper",
|
|
1809
|
+
type: "route",
|
|
1810
|
+
failed: true,
|
|
1811
|
+
detectedMimeType,
|
|
1812
|
+
selectedBranch,
|
|
1813
|
+
failedPhase: phase
|
|
1814
|
+
}
|
|
1815
|
+
});
|
|
1816
|
+
}
|
|
1817
|
+
const flowPath = [{
|
|
1818
|
+
stepId,
|
|
1819
|
+
stepIndex: 0,
|
|
1820
|
+
stepType: "route",
|
|
1821
|
+
branch: selectedBranch || void 0
|
|
1822
|
+
}];
|
|
1823
|
+
if (isNestedFlowError && err.flowPath) {
|
|
1824
|
+
flowPath.push(...err.flowPath);
|
|
1825
|
+
}
|
|
1826
|
+
const rootCauseMessage = isNestedFlowError ? err.getRootCause().message : err.message;
|
|
1827
|
+
throw new FlowExecutionError2(
|
|
1828
|
+
`Route step "${stepId}" failed${detectedMimeType ? ` (mimeType: ${detectedMimeType})` : ""}${selectedBranch ? ` (branch: ${selectedBranch})` : ""} in phase: ${phase}
|
|
1829
|
+
Error: ${rootCauseMessage}`,
|
|
1830
|
+
stepId,
|
|
1831
|
+
0,
|
|
1832
|
+
"route",
|
|
1833
|
+
[],
|
|
1834
|
+
isNestedFlowError ? err.originalError : err,
|
|
1835
|
+
void 0,
|
|
1836
|
+
flowPath,
|
|
1837
|
+
isNestedFlowError ? err.allCompletedSteps : void 0
|
|
1838
|
+
);
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
};
|
|
1842
|
+
}
|
|
1625
1843
|
|
|
1626
1844
|
// src/serialization.ts
|
|
1627
1845
|
function extractNodeMetadata(node2) {
|
|
@@ -1684,6 +1902,16 @@ function buildFlowFromConfig(flowDef, providers, flows, options) {
|
|
|
1684
1902
|
flows: flows || {}
|
|
1685
1903
|
});
|
|
1686
1904
|
flow = flow.step(step.id, node2, step.name);
|
|
1905
|
+
} else if (step.type === "route") {
|
|
1906
|
+
const node2 = createRouteCompositeNode({
|
|
1907
|
+
stepId: step.id,
|
|
1908
|
+
routeConfig: step.config,
|
|
1909
|
+
branches: step.branches,
|
|
1910
|
+
others: step.others,
|
|
1911
|
+
providers,
|
|
1912
|
+
flows: flows || {}
|
|
1913
|
+
});
|
|
1914
|
+
flow = flow.step(step.id, node2, step.name);
|
|
1687
1915
|
} else {
|
|
1688
1916
|
const exhaustiveCheck = step;
|
|
1689
1917
|
throw new FlowSerializationError(`Unknown step type: ${exhaustiveCheck.type}`);
|
|
@@ -1808,7 +2036,10 @@ function createNodeFromConfig(nodeType, config, providers, flows) {
|
|
|
1808
2036
|
return parse({
|
|
1809
2037
|
provider,
|
|
1810
2038
|
consensus: cfg.consensus,
|
|
1811
|
-
maxTokens: cfg.maxTokens
|
|
2039
|
+
maxTokens: cfg.maxTokens,
|
|
2040
|
+
promptRef: cfg.promptRef,
|
|
2041
|
+
promptVariables: cfg.promptVariables,
|
|
2042
|
+
additionalInstructions: cfg.additionalInstructions
|
|
1812
2043
|
});
|
|
1813
2044
|
}
|
|
1814
2045
|
case "extract": {
|
|
@@ -1818,7 +2049,10 @@ function createNodeFromConfig(nodeType, config, providers, flows) {
|
|
|
1818
2049
|
schema: cfg.schema,
|
|
1819
2050
|
consensus: cfg.consensus,
|
|
1820
2051
|
reasoning: cfg.reasoning,
|
|
1821
|
-
maxTokens: cfg.maxTokens
|
|
2052
|
+
maxTokens: cfg.maxTokens,
|
|
2053
|
+
promptRef: cfg.promptRef,
|
|
2054
|
+
promptVariables: cfg.promptVariables,
|
|
2055
|
+
additionalInstructions: cfg.additionalInstructions
|
|
1822
2056
|
});
|
|
1823
2057
|
}
|
|
1824
2058
|
case "split": {
|
|
@@ -1840,7 +2074,10 @@ function createNodeFromConfig(nodeType, config, providers, flows) {
|
|
|
1840
2074
|
provider,
|
|
1841
2075
|
categories: cfg.categories,
|
|
1842
2076
|
consensus: cfg.consensus,
|
|
1843
|
-
maxTokens: cfg.maxTokens
|
|
2077
|
+
maxTokens: cfg.maxTokens,
|
|
2078
|
+
promptRef: cfg.promptRef,
|
|
2079
|
+
promptVariables: cfg.promptVariables,
|
|
2080
|
+
additionalInstructions: cfg.additionalInstructions
|
|
1844
2081
|
});
|
|
1845
2082
|
}
|
|
1846
2083
|
default:
|
|
@@ -1916,6 +2153,26 @@ function calculateFlowNestingDepth(flow, currentDepth = 1) {
|
|
|
1916
2153
|
}
|
|
1917
2154
|
}
|
|
1918
2155
|
}
|
|
2156
|
+
} else if (step.type === "route") {
|
|
2157
|
+
const routeStep = step;
|
|
2158
|
+
if (routeStep.branches) {
|
|
2159
|
+
for (const branchFlowOrRef of Object.values(routeStep.branches)) {
|
|
2160
|
+
if ("flowRef" in branchFlowOrRef) {
|
|
2161
|
+
maxDepth = Math.max(maxDepth, stepDepth + 3);
|
|
2162
|
+
} else {
|
|
2163
|
+
const branchDepth = calculateFlowNestingDepth(branchFlowOrRef, stepDepth + 2);
|
|
2164
|
+
maxDepth = Math.max(maxDepth, branchDepth);
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
if (routeStep.others) {
|
|
2169
|
+
if ("flowRef" in routeStep.others) {
|
|
2170
|
+
maxDepth = Math.max(maxDepth, stepDepth + 3);
|
|
2171
|
+
} else {
|
|
2172
|
+
const othersDepth = calculateFlowNestingDepth(routeStep.others, stepDepth + 2);
|
|
2173
|
+
maxDepth = Math.max(maxDepth, othersDepth);
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
1919
2176
|
} else if (step.type === "forEach") {
|
|
1920
2177
|
const forEachStep = step;
|
|
1921
2178
|
if (forEachStep.itemFlow) {
|
|
@@ -2076,8 +2333,111 @@ function validateFlow(flowDef, options = {}) {
|
|
|
2076
2333
|
}
|
|
2077
2334
|
}
|
|
2078
2335
|
}
|
|
2336
|
+
} else if (step.type === "route") {
|
|
2337
|
+
const routeStep = step;
|
|
2338
|
+
if (!routeStep.branches || typeof routeStep.branches !== "object") {
|
|
2339
|
+
errors.push({
|
|
2340
|
+
type: "invalid_config",
|
|
2341
|
+
stepId,
|
|
2342
|
+
message: "Route step missing or invalid branches field"
|
|
2343
|
+
});
|
|
2344
|
+
} else if (Object.keys(routeStep.branches).length === 0) {
|
|
2345
|
+
errors.push({
|
|
2346
|
+
type: "invalid_config",
|
|
2347
|
+
stepId,
|
|
2348
|
+
message: "Route step must have at least one branch"
|
|
2349
|
+
});
|
|
2350
|
+
}
|
|
2351
|
+
if (routeStep.config?.branches && routeStep.branches) {
|
|
2352
|
+
for (const branchName of Object.keys(routeStep.config.branches)) {
|
|
2353
|
+
if (!routeStep.branches[branchName]) {
|
|
2354
|
+
errors.push({
|
|
2355
|
+
type: "invalid_config",
|
|
2356
|
+
stepId,
|
|
2357
|
+
message: `Branch "${branchName}" defined in config but missing flow definition`
|
|
2358
|
+
});
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2361
|
+
for (const [branchName, branchConfig] of Object.entries(routeStep.config.branches)) {
|
|
2362
|
+
if (!branchConfig.mimeTypes || branchConfig.mimeTypes.length === 0) {
|
|
2363
|
+
errors.push({
|
|
2364
|
+
type: "invalid_config",
|
|
2365
|
+
stepId: `${stepId}.${branchName}`,
|
|
2366
|
+
message: `Branch "${branchName}" has no MIME types defined`
|
|
2367
|
+
});
|
|
2368
|
+
} else {
|
|
2369
|
+
for (const mimeType of branchConfig.mimeTypes) {
|
|
2370
|
+
if (!mimeType.includes("/")) {
|
|
2371
|
+
errors.push({
|
|
2372
|
+
type: "invalid_config",
|
|
2373
|
+
stepId: `${stepId}.${branchName}`,
|
|
2374
|
+
message: `Invalid MIME type pattern: "${mimeType}". Expected format: "type/subtype" or "type/*"`
|
|
2375
|
+
});
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
if (routeStep.branches) {
|
|
2382
|
+
for (const [branchName, branchFlowOrRef] of Object.entries(routeStep.branches)) {
|
|
2383
|
+
if ("flowRef" in branchFlowOrRef) {
|
|
2384
|
+
const flowRef = branchFlowOrRef.flowRef;
|
|
2385
|
+
if (typeof flowRef !== "string" || flowRef.trim() === "") {
|
|
2386
|
+
errors.push({
|
|
2387
|
+
type: "invalid_config",
|
|
2388
|
+
stepId: `${stepId}.${branchName}`,
|
|
2389
|
+
message: `Branch "${branchName}": flowRef must be a non-empty string`
|
|
2390
|
+
});
|
|
2391
|
+
}
|
|
2392
|
+
} else {
|
|
2393
|
+
const branchResult = validateFlow(branchFlowOrRef, options);
|
|
2394
|
+
for (const error of branchResult.errors) {
|
|
2395
|
+
errors.push({
|
|
2396
|
+
...error,
|
|
2397
|
+
stepId: `${stepId}.${branchName}`,
|
|
2398
|
+
message: `Branch "${branchName}": ${error.message}`
|
|
2399
|
+
});
|
|
2400
|
+
}
|
|
2401
|
+
for (const warning of branchResult.warnings) {
|
|
2402
|
+
warnings.push({
|
|
2403
|
+
...warning,
|
|
2404
|
+
stepId: `${stepId}.${branchName}`,
|
|
2405
|
+
message: `Branch "${branchName}": ${warning.message}`
|
|
2406
|
+
});
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
if (routeStep.others) {
|
|
2412
|
+
if ("flowRef" in routeStep.others) {
|
|
2413
|
+
const flowRef = routeStep.others.flowRef;
|
|
2414
|
+
if (typeof flowRef !== "string" || flowRef.trim() === "") {
|
|
2415
|
+
errors.push({
|
|
2416
|
+
type: "invalid_config",
|
|
2417
|
+
stepId: `${stepId}.others`,
|
|
2418
|
+
message: "Others branch flowRef must be a non-empty string"
|
|
2419
|
+
});
|
|
2420
|
+
}
|
|
2421
|
+
} else {
|
|
2422
|
+
const othersResult = validateFlow(routeStep.others, options);
|
|
2423
|
+
for (const error of othersResult.errors) {
|
|
2424
|
+
errors.push({
|
|
2425
|
+
...error,
|
|
2426
|
+
stepId: `${stepId}.others`,
|
|
2427
|
+
message: `Others branch: ${error.message}`
|
|
2428
|
+
});
|
|
2429
|
+
}
|
|
2430
|
+
for (const warning of othersResult.warnings) {
|
|
2431
|
+
warnings.push({
|
|
2432
|
+
...warning,
|
|
2433
|
+
stepId: `${stepId}.others`,
|
|
2434
|
+
message: `Others branch: ${warning.message}`
|
|
2435
|
+
});
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2079
2439
|
}
|
|
2080
|
-
const validNodeTypes = ["parse", "extract", "split", "categorize", "trigger", "output"];
|
|
2440
|
+
const validNodeTypes = ["parse", "extract", "split", "categorize", "trigger", "output", "route"];
|
|
2081
2441
|
if (!step.nodeType || !validNodeTypes.includes(step.nodeType)) {
|
|
2082
2442
|
errors.push({
|
|
2083
2443
|
type: "invalid_config",
|
|
@@ -2099,7 +2459,7 @@ function validateFlow(flowDef, options = {}) {
|
|
|
2099
2459
|
const hasProviderRef2 = (cfg) => {
|
|
2100
2460
|
return "providerRef" in cfg && typeof cfg.providerRef === "string";
|
|
2101
2461
|
};
|
|
2102
|
-
if (step.nodeType !== "trigger" && step.nodeType !== "output") {
|
|
2462
|
+
if (step.nodeType !== "trigger" && step.nodeType !== "output" && step.nodeType !== "route") {
|
|
2103
2463
|
if (!hasProviderRef2(config)) {
|
|
2104
2464
|
errors.push({
|
|
2105
2465
|
type: "missing_provider",
|
|
@@ -2609,6 +2969,7 @@ export {
|
|
|
2609
2969
|
createConditionalCompositeNode,
|
|
2610
2970
|
createFlow,
|
|
2611
2971
|
createForEachCompositeNode,
|
|
2972
|
+
createRouteCompositeNode,
|
|
2612
2973
|
defineFlowConfig,
|
|
2613
2974
|
extract2 as extract,
|
|
2614
2975
|
extractNodeMetadata,
|