yaml-flow 3.0.0 → 3.1.1
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/README.md +44 -23
- package/dist/{constants-B_ftYTTE.d.ts → constants-B2zqu10b.d.ts} +7 -57
- package/dist/{constants-CiyHX8L-.d.cts → constants-DJZU1pwJ.d.cts} +7 -57
- package/dist/continuous-event-graph/index.cjs +1161 -182
- package/dist/continuous-event-graph/index.cjs.map +1 -1
- package/dist/continuous-event-graph/index.d.cts +567 -48
- package/dist/continuous-event-graph/index.d.ts +567 -48
- package/dist/continuous-event-graph/index.js +1151 -183
- package/dist/continuous-event-graph/index.js.map +1 -1
- package/dist/event-graph/index.cjs +35 -11
- package/dist/event-graph/index.cjs.map +1 -1
- package/dist/event-graph/index.d.cts +14 -5
- package/dist/event-graph/index.d.ts +14 -5
- package/dist/event-graph/index.js +34 -11
- package/dist/event-graph/index.js.map +1 -1
- package/dist/index.cjs +945 -414
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -4
- package/dist/index.d.ts +5 -4
- package/dist/index.js +936 -415
- package/dist/index.js.map +1 -1
- package/dist/inference/index.cjs +31 -7
- package/dist/inference/index.cjs.map +1 -1
- package/dist/inference/index.d.cts +2 -2
- package/dist/inference/index.d.ts +2 -2
- package/dist/inference/index.js +31 -7
- package/dist/inference/index.js.map +1 -1
- package/dist/{types-CxJg9Jrt.d.cts → types-BwvgvlOO.d.cts} +2 -2
- package/dist/{types-BuEo3wVG.d.ts → types-ClRA8hzC.d.ts} +2 -2
- package/dist/{types-BpWrH1sf.d.cts → types-DEj7OakX.d.cts} +14 -4
- package/dist/{types-BpWrH1sf.d.ts → types-DEj7OakX.d.ts} +14 -4
- package/dist/validate-DEZ2Ymdb.d.ts +53 -0
- package/dist/validate-DqKTZg_o.d.cts +53 -0
- package/examples/batch/batch-step-machine.ts +121 -0
- package/examples/browser/index.html +367 -0
- package/examples/continuous-event-graph/live-cards-board.ts +215 -0
- package/examples/continuous-event-graph/live-portfolio-dashboard.ts +555 -0
- package/examples/continuous-event-graph/portfolio-tracker.ts +287 -0
- package/examples/continuous-event-graph/reactive-monitoring.ts +265 -0
- package/examples/continuous-event-graph/reactive-pipeline.ts +168 -0
- package/examples/continuous-event-graph/soc-incident-board.ts +287 -0
- package/examples/continuous-event-graph/stock-dashboard.ts +229 -0
- package/examples/event-graph/ci-cd-pipeline.ts +243 -0
- package/examples/event-graph/executor-diamond.ts +165 -0
- package/examples/event-graph/executor-pipeline.ts +161 -0
- package/examples/event-graph/research-pipeline.ts +137 -0
- package/examples/flows/ai-conversation.yaml +116 -0
- package/examples/flows/order-processing.yaml +143 -0
- package/examples/flows/simple-greeting.yaml +54 -0
- package/examples/graph-of-graphs/multi-stage-etl.ts +307 -0
- package/examples/graph-of-graphs/url-processing-pipeline.ts +254 -0
- package/examples/inference/azure-deployment.ts +149 -0
- package/examples/inference/copilot-cli.ts +138 -0
- package/examples/inference/data-pipeline.ts +145 -0
- package/examples/inference/pluggable-adapters.ts +254 -0
- package/examples/ingest.js +733 -0
- package/examples/node/ai-conversation.ts +195 -0
- package/examples/node/simple-greeting.ts +101 -0
- package/package.json +3 -2
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Batch Example: Process tickets through a Step Machine flow
|
|
3
|
+
*
|
|
4
|
+
* Demonstrates:
|
|
5
|
+
* - batch() with step-machine processor
|
|
6
|
+
* - Concurrency control (3 slots)
|
|
7
|
+
* - Progress tracking
|
|
8
|
+
* - Mixed success/failure handling
|
|
9
|
+
*
|
|
10
|
+
* Run with: npx tsx examples/batch/batch-step-machine.ts
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { batch } from '../../src/batch/index.js';
|
|
14
|
+
import { createStepMachine } from '../../src/step-machine/index.js';
|
|
15
|
+
import type { StepFlowConfig, StepHandler } from '../../src/step-machine/types.js';
|
|
16
|
+
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// 1. Define a simple flow
|
|
19
|
+
// ============================================================================
|
|
20
|
+
|
|
21
|
+
const ticketFlow: StepFlowConfig = {
|
|
22
|
+
id: 'support-ticket',
|
|
23
|
+
settings: { start_step: 'classify', max_total_steps: 10 },
|
|
24
|
+
steps: {
|
|
25
|
+
classify: {
|
|
26
|
+
produces_data: ['category'],
|
|
27
|
+
transitions: { billing: 'handle', technical: 'handle', unknown: 'escalate' },
|
|
28
|
+
},
|
|
29
|
+
handle: {
|
|
30
|
+
expects_data: ['category'],
|
|
31
|
+
produces_data: ['resolution'],
|
|
32
|
+
transitions: { resolved: 'done', failed: 'escalate' },
|
|
33
|
+
},
|
|
34
|
+
escalate: {
|
|
35
|
+
expects_data: ['category'],
|
|
36
|
+
produces_data: ['escalation_id'],
|
|
37
|
+
transitions: { done: 'done' },
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
terminal_states: {
|
|
41
|
+
done: { return_intent: 'resolved', return_artifacts: ['resolution', 'escalation_id'] },
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const handlers: Record<string, StepHandler> = {
|
|
46
|
+
classify: async (input) => {
|
|
47
|
+
const msg = (input.message as string) || '';
|
|
48
|
+
if (msg.includes('bill') || msg.includes('charge')) return { result: 'billing', data: { category: 'billing' } };
|
|
49
|
+
if (msg.includes('crash') || msg.includes('error')) return { result: 'technical', data: { category: 'technical' } };
|
|
50
|
+
return { result: 'unknown', data: { category: 'unknown' } };
|
|
51
|
+
},
|
|
52
|
+
handle: async (input) => {
|
|
53
|
+
// Simulate occasional failure
|
|
54
|
+
if (Math.random() < 0.2) return { result: 'failed' };
|
|
55
|
+
return { result: 'resolved', data: { resolution: `Resolved ${input.category} issue` } };
|
|
56
|
+
},
|
|
57
|
+
escalate: async (input) => {
|
|
58
|
+
return { result: 'done', data: { escalation_id: `ESC-${Date.now()}` } };
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// ============================================================================
|
|
63
|
+
// 2. Batch of tickets
|
|
64
|
+
// ============================================================================
|
|
65
|
+
|
|
66
|
+
const tickets = [
|
|
67
|
+
{ id: 'T-001', message: 'I was double-charged on my bill' },
|
|
68
|
+
{ id: 'T-002', message: 'App crashes on startup' },
|
|
69
|
+
{ id: 'T-003', message: 'How do I change my password?' },
|
|
70
|
+
{ id: 'T-004', message: 'Billing error on invoice #1234' },
|
|
71
|
+
{ id: 'T-005', message: 'Error 500 on checkout page' },
|
|
72
|
+
{ id: 'T-006', message: 'Cannot access my account' },
|
|
73
|
+
{ id: 'T-007', message: 'Refund not processed on my bill' },
|
|
74
|
+
{ id: 'T-008', message: 'App throws error on login' },
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
// ============================================================================
|
|
78
|
+
// 3. Run batch
|
|
79
|
+
// ============================================================================
|
|
80
|
+
|
|
81
|
+
async function main() {
|
|
82
|
+
console.log(`Processing ${tickets.length} tickets with 3 concurrent slots\n`);
|
|
83
|
+
|
|
84
|
+
const result = await batch(tickets, {
|
|
85
|
+
concurrency: 3,
|
|
86
|
+
|
|
87
|
+
processor: async (ticket) => {
|
|
88
|
+
const machine = createStepMachine(ticketFlow, handlers);
|
|
89
|
+
return machine.run({ message: ticket.message });
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
onItemComplete: (ticket, flowResult) => {
|
|
93
|
+
console.log(` ✓ ${ticket.id}: ${flowResult.intent} — ${flowResult.stepHistory.join(' → ')}`);
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
onItemError: (ticket, error) => {
|
|
97
|
+
console.log(` ✗ ${ticket.id}: ${error.message}`);
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
onProgress: (p) => {
|
|
101
|
+
if (p.percent % 25 === 0) {
|
|
102
|
+
console.log(` [${p.percent}%] ${p.completed + p.failed}/${p.total} done, ${p.active} active`);
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
console.log(`\n${'='.repeat(50)}`);
|
|
108
|
+
console.log(`Results: ${result.completed} completed, ${result.failed} failed (${result.durationMs}ms)`);
|
|
109
|
+
|
|
110
|
+
// Show per-item breakdown
|
|
111
|
+
for (const item of result.items) {
|
|
112
|
+
const ticket = item.item;
|
|
113
|
+
if (item.status === 'completed') {
|
|
114
|
+
console.log(` ${ticket.id}: ${item.result?.intent} (${item.durationMs}ms)`);
|
|
115
|
+
} else {
|
|
116
|
+
console.log(` ${ticket.id}: FAILED — ${item.error?.message}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>yaml-flow Browser Example</title>
|
|
7
|
+
<style>
|
|
8
|
+
* { box-sizing: border-box; }
|
|
9
|
+
body {
|
|
10
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
11
|
+
max-width: 800px;
|
|
12
|
+
margin: 40px auto;
|
|
13
|
+
padding: 20px;
|
|
14
|
+
background: #f5f5f5;
|
|
15
|
+
}
|
|
16
|
+
h1 { color: #333; }
|
|
17
|
+
.card {
|
|
18
|
+
background: white;
|
|
19
|
+
border-radius: 8px;
|
|
20
|
+
padding: 20px;
|
|
21
|
+
margin: 20px 0;
|
|
22
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
23
|
+
}
|
|
24
|
+
.step-list {
|
|
25
|
+
display: flex;
|
|
26
|
+
gap: 10px;
|
|
27
|
+
flex-wrap: wrap;
|
|
28
|
+
margin: 10px 0;
|
|
29
|
+
}
|
|
30
|
+
.step {
|
|
31
|
+
padding: 8px 16px;
|
|
32
|
+
border-radius: 20px;
|
|
33
|
+
background: #e0e0e0;
|
|
34
|
+
font-size: 14px;
|
|
35
|
+
}
|
|
36
|
+
.step.active {
|
|
37
|
+
background: #2196F3;
|
|
38
|
+
color: white;
|
|
39
|
+
}
|
|
40
|
+
.step.completed {
|
|
41
|
+
background: #4CAF50;
|
|
42
|
+
color: white;
|
|
43
|
+
}
|
|
44
|
+
button {
|
|
45
|
+
background: #2196F3;
|
|
46
|
+
color: white;
|
|
47
|
+
border: none;
|
|
48
|
+
padding: 12px 24px;
|
|
49
|
+
border-radius: 6px;
|
|
50
|
+
cursor: pointer;
|
|
51
|
+
font-size: 16px;
|
|
52
|
+
margin: 5px;
|
|
53
|
+
}
|
|
54
|
+
button:hover { background: #1976D2; }
|
|
55
|
+
button:disabled { background: #ccc; cursor: not-allowed; }
|
|
56
|
+
.log {
|
|
57
|
+
background: #263238;
|
|
58
|
+
color: #aed581;
|
|
59
|
+
padding: 15px;
|
|
60
|
+
border-radius: 6px;
|
|
61
|
+
font-family: 'Consolas', monospace;
|
|
62
|
+
font-size: 13px;
|
|
63
|
+
max-height: 300px;
|
|
64
|
+
overflow-y: auto;
|
|
65
|
+
}
|
|
66
|
+
.log-entry { margin: 5px 0; }
|
|
67
|
+
.log-entry.error { color: #ef5350; }
|
|
68
|
+
.log-entry.transition { color: #64B5F6; }
|
|
69
|
+
.result {
|
|
70
|
+
padding: 15px;
|
|
71
|
+
border-radius: 6px;
|
|
72
|
+
margin-top: 15px;
|
|
73
|
+
}
|
|
74
|
+
.result.success { background: #C8E6C9; }
|
|
75
|
+
.result.error { background: #FFCDD2; }
|
|
76
|
+
input {
|
|
77
|
+
padding: 10px;
|
|
78
|
+
border: 1px solid #ddd;
|
|
79
|
+
border-radius: 4px;
|
|
80
|
+
font-size: 16px;
|
|
81
|
+
width: 200px;
|
|
82
|
+
}
|
|
83
|
+
</style>
|
|
84
|
+
</head>
|
|
85
|
+
<body>
|
|
86
|
+
<h1>🔄 yaml-flow Browser Demo</h1>
|
|
87
|
+
|
|
88
|
+
<div class="card">
|
|
89
|
+
<h2>Simple Greeting Flow</h2>
|
|
90
|
+
<p>Enter your name and run the flow to see it in action.</p>
|
|
91
|
+
|
|
92
|
+
<div style="margin: 15px 0;">
|
|
93
|
+
<input type="text" id="nameInput" placeholder="Enter your name" value="Developer">
|
|
94
|
+
<button onclick="runFlow()">Run Flow</button>
|
|
95
|
+
<button onclick="clearLog()">Clear Log</button>
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<h3>Steps</h3>
|
|
99
|
+
<div class="step-list" id="stepList">
|
|
100
|
+
<div class="step" data-step="greet">greet</div>
|
|
101
|
+
<div class="step" data-step="validate">validate</div>
|
|
102
|
+
<div class="step" data-step="personalize">personalize</div>
|
|
103
|
+
<div class="step" data-step="success_state">✓ success</div>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<div class="card">
|
|
108
|
+
<h3>Execution Log</h3>
|
|
109
|
+
<div class="log" id="log"></div>
|
|
110
|
+
|
|
111
|
+
<div id="result"></div>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<script type="module">
|
|
115
|
+
// Import from CDN (in production) or local build
|
|
116
|
+
// For this demo, we'll inline a simplified version
|
|
117
|
+
|
|
118
|
+
// ============= Inline yaml-flow (simplified for demo) =============
|
|
119
|
+
|
|
120
|
+
class MemoryStore {
|
|
121
|
+
constructor() {
|
|
122
|
+
this.runs = new Map();
|
|
123
|
+
this.data = new Map();
|
|
124
|
+
}
|
|
125
|
+
async saveRunState(runId, state) { this.runs.set(runId, {...state}); }
|
|
126
|
+
async loadRunState(runId) { return this.runs.get(runId) || null; }
|
|
127
|
+
async deleteRunState(runId) { this.runs.delete(runId); this.data.delete(runId); }
|
|
128
|
+
async setData(runId, key, value) {
|
|
129
|
+
if (!this.data.has(runId)) this.data.set(runId, {});
|
|
130
|
+
this.data.get(runId)[key] = value;
|
|
131
|
+
}
|
|
132
|
+
async getData(runId, key) { return this.data.get(runId)?.[key]; }
|
|
133
|
+
async getAllData(runId) { return {...(this.data.get(runId) || {})}; }
|
|
134
|
+
async clearData(runId) { this.data.delete(runId); }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
class FlowEngine {
|
|
138
|
+
constructor(flow, handlers, options = {}) {
|
|
139
|
+
this.flow = flow;
|
|
140
|
+
this.handlers = handlers;
|
|
141
|
+
this.store = options.store || new MemoryStore();
|
|
142
|
+
this.components = options.components || {};
|
|
143
|
+
this.options = options;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async run(initialData = {}) {
|
|
147
|
+
const runId = crypto.randomUUID();
|
|
148
|
+
const startedAt = Date.now();
|
|
149
|
+
|
|
150
|
+
const runState = {
|
|
151
|
+
runId,
|
|
152
|
+
currentStep: this.flow.settings.start_step,
|
|
153
|
+
status: 'running',
|
|
154
|
+
stepHistory: [],
|
|
155
|
+
iterationCounts: {},
|
|
156
|
+
retryCounts: {},
|
|
157
|
+
startedAt,
|
|
158
|
+
updatedAt: startedAt,
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
await this.store.saveRunState(runId, runState);
|
|
162
|
+
for (const [key, value] of Object.entries(initialData)) {
|
|
163
|
+
await this.store.setData(runId, key, value);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
let iterations = 0;
|
|
167
|
+
const maxSteps = this.flow.settings.max_total_steps || 100;
|
|
168
|
+
|
|
169
|
+
while (iterations < maxSteps) {
|
|
170
|
+
const currentStep = runState.currentStep;
|
|
171
|
+
|
|
172
|
+
// Check terminal state
|
|
173
|
+
const terminalState = this.flow.terminal_states[currentStep];
|
|
174
|
+
if (terminalState) {
|
|
175
|
+
const allData = await this.store.getAllData(runId);
|
|
176
|
+
return {
|
|
177
|
+
runId,
|
|
178
|
+
status: 'completed',
|
|
179
|
+
intent: terminalState.return_intent,
|
|
180
|
+
data: this.extractReturnData(terminalState.return_artifacts, allData),
|
|
181
|
+
finalStep: currentStep,
|
|
182
|
+
stepHistory: runState.stepHistory,
|
|
183
|
+
durationMs: Date.now() - startedAt,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const stepConfig = this.flow.steps[currentStep];
|
|
188
|
+
const handler = this.handlers[currentStep];
|
|
189
|
+
|
|
190
|
+
// Build input
|
|
191
|
+
const allData = await this.store.getAllData(runId);
|
|
192
|
+
const input = stepConfig.expects_data
|
|
193
|
+
? Object.fromEntries(stepConfig.expects_data.map(k => [k, allData[k]]))
|
|
194
|
+
: allData;
|
|
195
|
+
|
|
196
|
+
// Execute
|
|
197
|
+
this.options.onStep?.(currentStep, 'start');
|
|
198
|
+
const result = await handler(input, { runId, stepName: currentStep, components: this.components, store: this.store });
|
|
199
|
+
this.options.onStep?.(currentStep, result.result);
|
|
200
|
+
|
|
201
|
+
// Store output
|
|
202
|
+
if (result.data) {
|
|
203
|
+
for (const [key, value] of Object.entries(result.data)) {
|
|
204
|
+
await this.store.setData(runId, key, value);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Transition
|
|
209
|
+
const nextStep = stepConfig.transitions[result.result];
|
|
210
|
+
this.options.onTransition?.(currentStep, nextStep);
|
|
211
|
+
|
|
212
|
+
runState.stepHistory.push(currentStep);
|
|
213
|
+
runState.currentStep = nextStep;
|
|
214
|
+
iterations++;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return { runId, status: 'max_iterations', data: {}, stepHistory: runState.stepHistory };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
extractReturnData(artifacts, allData) {
|
|
221
|
+
if (!artifacts || artifacts === false) return {};
|
|
222
|
+
if (typeof artifacts === 'string') return { [artifacts]: allData[artifacts] };
|
|
223
|
+
if (Array.isArray(artifacts)) return Object.fromEntries(artifacts.map(k => [k, allData[k]]));
|
|
224
|
+
return allData;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ============= Flow Definition =============
|
|
229
|
+
|
|
230
|
+
const flow = {
|
|
231
|
+
settings: {
|
|
232
|
+
start_step: 'greet',
|
|
233
|
+
max_total_steps: 10
|
|
234
|
+
},
|
|
235
|
+
steps: {
|
|
236
|
+
greet: {
|
|
237
|
+
produces_data: ['greeting', 'user_name'],
|
|
238
|
+
transitions: { success: 'validate', failure: 'error_state' }
|
|
239
|
+
},
|
|
240
|
+
validate: {
|
|
241
|
+
expects_data: ['greeting', 'user_name'],
|
|
242
|
+
produces_data: ['is_valid'],
|
|
243
|
+
transitions: { success: 'personalize', invalid: 'error_state', failure: 'error_state' }
|
|
244
|
+
},
|
|
245
|
+
personalize: {
|
|
246
|
+
expects_data: ['greeting', 'user_name'],
|
|
247
|
+
produces_data: ['final_message'],
|
|
248
|
+
transitions: { success: 'success_state', failure: 'error_state' }
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
terminal_states: {
|
|
252
|
+
success_state: { return_intent: 'success', return_artifacts: ['final_message', 'user_name'] },
|
|
253
|
+
error_state: { return_intent: 'error', return_artifacts: false }
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
// ============= Step Handlers =============
|
|
258
|
+
|
|
259
|
+
const handlers = {
|
|
260
|
+
async greet(input) {
|
|
261
|
+
const userName = input.initial_name || 'World';
|
|
262
|
+
await sleep(300); // Simulate async work
|
|
263
|
+
return {
|
|
264
|
+
result: 'success',
|
|
265
|
+
data: { greeting: 'Hello', user_name: userName }
|
|
266
|
+
};
|
|
267
|
+
},
|
|
268
|
+
|
|
269
|
+
async validate(input) {
|
|
270
|
+
const { greeting, user_name } = input;
|
|
271
|
+
await sleep(200);
|
|
272
|
+
const isValid = greeting && user_name;
|
|
273
|
+
return {
|
|
274
|
+
result: isValid ? 'success' : 'invalid',
|
|
275
|
+
data: { is_valid: isValid }
|
|
276
|
+
};
|
|
277
|
+
},
|
|
278
|
+
|
|
279
|
+
async personalize(input) {
|
|
280
|
+
const { greeting, user_name } = input;
|
|
281
|
+
await sleep(200);
|
|
282
|
+
return {
|
|
283
|
+
result: 'success',
|
|
284
|
+
data: { final_message: `${greeting}, ${user_name}! Welcome to yaml-flow in the browser! 🎉` }
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
function sleep(ms) {
|
|
290
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ============= UI Functions =============
|
|
294
|
+
|
|
295
|
+
const logEl = document.getElementById('log');
|
|
296
|
+
const resultEl = document.getElementById('result');
|
|
297
|
+
const stepEls = document.querySelectorAll('.step');
|
|
298
|
+
|
|
299
|
+
function log(message, type = '') {
|
|
300
|
+
const entry = document.createElement('div');
|
|
301
|
+
entry.className = `log-entry ${type}`;
|
|
302
|
+
entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
|
|
303
|
+
logEl.appendChild(entry);
|
|
304
|
+
logEl.scrollTop = logEl.scrollHeight;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function updateStep(stepName, status) {
|
|
308
|
+
stepEls.forEach(el => {
|
|
309
|
+
if (el.dataset.step === stepName) {
|
|
310
|
+
el.classList.remove('active', 'completed');
|
|
311
|
+
el.classList.add(status);
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function resetSteps() {
|
|
317
|
+
stepEls.forEach(el => el.classList.remove('active', 'completed'));
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
window.clearLog = function() {
|
|
321
|
+
logEl.innerHTML = '';
|
|
322
|
+
resultEl.innerHTML = '';
|
|
323
|
+
resetSteps();
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
window.runFlow = async function() {
|
|
327
|
+
resetSteps();
|
|
328
|
+
resultEl.innerHTML = '';
|
|
329
|
+
|
|
330
|
+
const name = document.getElementById('nameInput').value || 'World';
|
|
331
|
+
log(`Starting flow with name: "${name}"`);
|
|
332
|
+
|
|
333
|
+
const engine = new FlowEngine(flow, handlers, {
|
|
334
|
+
store: new MemoryStore(),
|
|
335
|
+
onStep: (step, status) => {
|
|
336
|
+
log(`Step [${step}]: ${status}`);
|
|
337
|
+
updateStep(step, status === 'start' ? 'active' : 'completed');
|
|
338
|
+
},
|
|
339
|
+
onTransition: (from, to) => {
|
|
340
|
+
log(`Transition: ${from} → ${to}`, 'transition');
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
try {
|
|
345
|
+
const result = await engine.run({ initial_name: name });
|
|
346
|
+
|
|
347
|
+
log(`Flow completed: ${result.intent}`);
|
|
348
|
+
updateStep(result.finalStep, 'completed');
|
|
349
|
+
|
|
350
|
+
resultEl.innerHTML = `
|
|
351
|
+
<div class="result ${result.intent === 'success' ? 'success' : 'error'}">
|
|
352
|
+
<strong>Result:</strong> ${result.intent}<br>
|
|
353
|
+
<strong>Message:</strong> ${result.data.final_message || 'N/A'}<br>
|
|
354
|
+
<strong>Duration:</strong> ${result.durationMs}ms<br>
|
|
355
|
+
<strong>Steps:</strong> ${result.stepHistory.join(' → ')} → ${result.finalStep}
|
|
356
|
+
</div>
|
|
357
|
+
`;
|
|
358
|
+
} catch (error) {
|
|
359
|
+
log(`Error: ${error.message}`, 'error');
|
|
360
|
+
resultEl.innerHTML = `<div class="result error">Error: ${error.message}</div>`;
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
log('yaml-flow browser demo loaded. Click "Run Flow" to start.');
|
|
365
|
+
</script>
|
|
366
|
+
</body>
|
|
367
|
+
</html>
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live Cards → Reactive Graph Example: Board Dashboard
|
|
3
|
+
*
|
|
4
|
+
* Demonstrates `liveCardsToReactiveGraph` — the bridge that converts
|
|
5
|
+
* live card / source JSON definitions into a fully wired ReactiveGraph.
|
|
6
|
+
*
|
|
7
|
+
* This example shows both overloads:
|
|
8
|
+
* 1. Flat array of cards → reactive graph
|
|
9
|
+
* 2. LiveBoard object → reactive graph (with board-level id/settings)
|
|
10
|
+
*
|
|
11
|
+
* Features exercised:
|
|
12
|
+
* - Source nodes with pre-populated state (static data feeds)
|
|
13
|
+
* - Custom sourceHandlers (simulated API fetch)
|
|
14
|
+
* - Card nodes with compute expressions (sum, avg, count, template)
|
|
15
|
+
* - Cross-card data flow via data.requires / data.provides
|
|
16
|
+
* - LiveBoard overload — board.id, board.settings forwarded to GraphConfig
|
|
17
|
+
* - validateReactiveGraph on the resulting graph
|
|
18
|
+
*
|
|
19
|
+
* Run with: npx tsx examples/continuous-event-graph/live-cards-board.ts
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
liveCardsToReactiveGraph,
|
|
24
|
+
validateReactiveGraph,
|
|
25
|
+
} from '../../src/continuous-event-graph/index.js';
|
|
26
|
+
import type { LiveCard, LiveBoard } from '../../src/continuous-event-graph/index.js';
|
|
27
|
+
|
|
28
|
+
// ============================================================================
|
|
29
|
+
// 1. Flat cards array → Reactive Graph
|
|
30
|
+
// ============================================================================
|
|
31
|
+
|
|
32
|
+
console.log('=== Part 1: Flat cards array ===\n');
|
|
33
|
+
|
|
34
|
+
const cards: LiveCard[] = [
|
|
35
|
+
{
|
|
36
|
+
id: 'market-feed',
|
|
37
|
+
type: 'source',
|
|
38
|
+
meta: { title: 'Live Market Prices' },
|
|
39
|
+
state: { prices: [142.5, 305.8, 89.2, 211.0, 178.3] },
|
|
40
|
+
source: { kind: 'static', bindTo: 'state.prices' },
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: 'stats',
|
|
44
|
+
type: 'card',
|
|
45
|
+
meta: { title: 'Price Statistics' },
|
|
46
|
+
state: {},
|
|
47
|
+
data: { requires: ['market-feed'] },
|
|
48
|
+
compute: {
|
|
49
|
+
total: { fn: 'sum', input: 'state.market-feed.prices' },
|
|
50
|
+
avg: { fn: 'avg', input: 'state.market-feed.prices' },
|
|
51
|
+
count: { fn: 'count', input: 'state.market-feed.prices' },
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: 'summary',
|
|
56
|
+
type: 'card',
|
|
57
|
+
meta: { title: 'Summary Label' },
|
|
58
|
+
state: {},
|
|
59
|
+
data: { requires: ['stats'] },
|
|
60
|
+
compute: {
|
|
61
|
+
label: {
|
|
62
|
+
fn: 'template',
|
|
63
|
+
input: 'state.stats',
|
|
64
|
+
format: '{{count}} stocks — total ${{total}}, avg ${{avg}}',
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
const flatResult = liveCardsToReactiveGraph(cards, {
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
console.log('Graph ID:', flatResult.config.id);
|
|
74
|
+
console.log('Tasks:', Object.keys(flatResult.config.tasks).join(', '));
|
|
75
|
+
console.log('Pushing trigger event...\n');
|
|
76
|
+
|
|
77
|
+
flatResult.graph.push({
|
|
78
|
+
type: 'inject-tokens',
|
|
79
|
+
tokens: [],
|
|
80
|
+
timestamp: new Date().toISOString(),
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
await sleep(1000);
|
|
84
|
+
|
|
85
|
+
const flatState = flatResult.graph.getState();
|
|
86
|
+
for (const [name, task] of Object.entries(flatState.state.tasks)) {
|
|
87
|
+
const hash = task.lastDataHash ? ` (hash: ${task.lastDataHash.slice(0, 8)}…)` : '';
|
|
88
|
+
console.log(` ${name}: ${task.status}${hash}`);
|
|
89
|
+
}
|
|
90
|
+
console.log(` Outputs: [${flatState.state.availableOutputs.join(', ')}]`);
|
|
91
|
+
|
|
92
|
+
flatResult.graph.dispose();
|
|
93
|
+
|
|
94
|
+
// ============================================================================
|
|
95
|
+
// 2. LiveBoard → Reactive Graph
|
|
96
|
+
// ============================================================================
|
|
97
|
+
|
|
98
|
+
console.log('\n=== Part 2: LiveBoard overload ===\n');
|
|
99
|
+
|
|
100
|
+
const board: LiveBoard = {
|
|
101
|
+
id: 'portfolio-board',
|
|
102
|
+
title: 'Portfolio Analytics Dashboard',
|
|
103
|
+
mode: 'board',
|
|
104
|
+
positions: {
|
|
105
|
+
'equity-feed': { x: 0, y: 0, w: 300, h: 200 },
|
|
106
|
+
'bond-feed': { x: 320, y: 0, w: 300, h: 200 },
|
|
107
|
+
'portfolio-mix': { x: 160, y: 240, w: 300, h: 200 },
|
|
108
|
+
'risk-summary': { x: 160, y: 480, w: 300, h: 200 },
|
|
109
|
+
},
|
|
110
|
+
settings: {
|
|
111
|
+
completion: 'manual',
|
|
112
|
+
},
|
|
113
|
+
nodes: [
|
|
114
|
+
{
|
|
115
|
+
id: 'equity-feed',
|
|
116
|
+
type: 'source',
|
|
117
|
+
meta: { title: 'Equity Prices' },
|
|
118
|
+
state: {},
|
|
119
|
+
source: { kind: 'api', bindTo: 'state.raw', url_template: 'https://api.example.com/equity' },
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
id: 'bond-feed',
|
|
123
|
+
type: 'source',
|
|
124
|
+
meta: { title: 'Bond Yields' },
|
|
125
|
+
state: { yields: [3.2, 4.1, 2.8, 5.0] },
|
|
126
|
+
source: { kind: 'static', bindTo: 'state.yields' },
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
id: 'portfolio-mix',
|
|
130
|
+
type: 'card',
|
|
131
|
+
meta: { title: 'Portfolio Mix Calculator' },
|
|
132
|
+
state: {},
|
|
133
|
+
data: { requires: ['equity-feed', 'bond-feed'] },
|
|
134
|
+
compute: {
|
|
135
|
+
equity_total: { fn: 'sum', input: 'state.equity-feed.prices' },
|
|
136
|
+
bond_total: { fn: 'sum', input: 'state.bond-feed.yields' },
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
id: 'risk-summary',
|
|
141
|
+
type: 'card',
|
|
142
|
+
meta: { title: 'Risk Summary' },
|
|
143
|
+
state: {},
|
|
144
|
+
data: { requires: ['portfolio-mix'] },
|
|
145
|
+
compute: {
|
|
146
|
+
label: {
|
|
147
|
+
fn: 'template',
|
|
148
|
+
input: 'state.portfolio-mix',
|
|
149
|
+
format: 'Equities: ${{equity_total}} | Bonds: {{bond_total}}%',
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const boardResult = liveCardsToReactiveGraph(board, {
|
|
157
|
+
// Custom handler for the API-based source — simulates a fetch
|
|
158
|
+
sourceHandlers: {
|
|
159
|
+
'equity-feed': async ({ callbackToken }) => {
|
|
160
|
+
console.log(' [equity-feed] Simulating API fetch...');
|
|
161
|
+
await sleep(100);
|
|
162
|
+
// Use the graph to resolve — bridge wires this up automatically
|
|
163
|
+
boardResult.graph.resolveCallback(callbackToken, { prices: [155.2, 310.4, 92.1, 220.5] });
|
|
164
|
+
return 'task-initiated' as const;
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
console.log('Board ID:', boardResult.config.id);
|
|
170
|
+
console.log('Completion:', boardResult.config.settings.completion);
|
|
171
|
+
console.log('Tasks:', Object.keys(boardResult.config.tasks).join(', '));
|
|
172
|
+
console.log('Cards in map:', boardResult.cards.size);
|
|
173
|
+
console.log();
|
|
174
|
+
|
|
175
|
+
// Push and run
|
|
176
|
+
boardResult.graph.push({
|
|
177
|
+
type: 'inject-tokens',
|
|
178
|
+
tokens: [],
|
|
179
|
+
timestamp: new Date().toISOString(),
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
await sleep(1500);
|
|
183
|
+
|
|
184
|
+
const boardState = boardResult.graph.getState();
|
|
185
|
+
console.log('Final task states:');
|
|
186
|
+
for (const [name, task] of Object.entries(boardState.state.tasks)) {
|
|
187
|
+
const hash = task.lastDataHash ? ` (hash: ${task.lastDataHash.slice(0, 8)}…)` : '';
|
|
188
|
+
console.log(` ${name}: ${task.status} (executed ${task.executionCount}x)${hash}`);
|
|
189
|
+
}
|
|
190
|
+
console.log(` Outputs: [${boardState.state.availableOutputs.join(', ')}]`);
|
|
191
|
+
|
|
192
|
+
// ============================================================================
|
|
193
|
+
// 3. Validate the reactive graph
|
|
194
|
+
// ============================================================================
|
|
195
|
+
|
|
196
|
+
console.log('\n=== Validation ===');
|
|
197
|
+
const validation = validateReactiveGraph({
|
|
198
|
+
graph: boardResult.graph,
|
|
199
|
+
handlers: boardResult.handlers,
|
|
200
|
+
});
|
|
201
|
+
console.log(` Valid: ${validation.valid} (${validation.issues.length} issues)`);
|
|
202
|
+
for (const issue of validation.issues) {
|
|
203
|
+
console.log(` [${issue.severity}] ${issue.code}: ${issue.message}`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
boardResult.graph.dispose();
|
|
207
|
+
console.log('\nDone.');
|
|
208
|
+
|
|
209
|
+
// ============================================================================
|
|
210
|
+
// Util
|
|
211
|
+
// ============================================================================
|
|
212
|
+
|
|
213
|
+
function sleep(ms: number): Promise<void> {
|
|
214
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
215
|
+
}
|