Skip to content

Commit 2fbc45d

Browse files
Add FileParserPlugin example for Effect AI
Add Effect AI example that demonstrates PDF processing using: - Effect.gen for effect composition - Layer-based dependency injection - Type-safe error handling - Shared fixtures module for PDF test files No PDF fixtures included - these should be inherited from pdf-example-fetch in the stack.
1 parent ec6d0fd commit 2fbc45d

File tree

3 files changed

+509
-46
lines changed

3 files changed

+509
-46
lines changed
Lines changed: 311 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,322 @@
11
#!/usr/bin/env bun
2+
3+
/**
4+
* Run all Effect-AI examples in parallel using Effect
5+
*
6+
* This script demonstrates Effect patterns for:
7+
* - Parallel execution with concurrency control
8+
* - Structured error handling
9+
* - Resource management (file system, processes)
10+
* - Type-safe results tracking
11+
*/
12+
13+
// TODO: use @effect/platform instead of node.js APIs
14+
15+
import { Effect, Console, Exit } from "effect";
16+
import * as fs from "node:fs";
17+
import * as path from "node:path";
18+
import { spawn } from "node:child_process";
19+
20+
// ============================================================================
21+
// Types
22+
// ============================================================================
23+
24+
interface ExampleResult {
25+
readonly example: string;
26+
readonly exitCode: number;
27+
readonly duration: number;
28+
readonly success: boolean;
29+
readonly startTime: Date;
30+
readonly endTime: Date;
31+
}
32+
33+
// ============================================================================
34+
// Error Types
35+
// ============================================================================
36+
37+
class ExampleNotFoundError {
38+
readonly _tag = "ExampleNotFoundError";
39+
constructor(readonly example: string) {}
40+
}
41+
42+
class ExampleExecutionError {
43+
readonly _tag = "ExampleExecutionError";
44+
constructor(
45+
readonly example: string,
46+
readonly exitCode: number,
47+
readonly message: string
48+
) {}
49+
}
50+
51+
// ============================================================================
52+
// Utilities
53+
// ============================================================================
54+
55+
/**
56+
* Recursively find all .ts files in a directory
57+
*/
58+
const findExamples = (dir: string): Effect.Effect<readonly string[]> =>
59+
Effect.gen(function* () {
60+
const entries = yield* Effect.sync(() => fs.readdirSync(dir));
61+
const files: string[] = [];
62+
63+
for (const entry of entries) {
64+
const fullPath = path.join(dir, entry);
65+
const stat = yield* Effect.sync(() => fs.statSync(fullPath));
66+
67+
if (stat.isDirectory()) {
68+
const subFiles = yield* findExamples(fullPath);
69+
files.push(...subFiles);
70+
} else if (entry.endsWith('.ts')) {
71+
files.push(fullPath);
72+
}
73+
}
74+
75+
return files.sort();
76+
});
77+
278
/**
3-
* Run all example files in the src/ directory
4-
* Each example is run in a separate process to handle process.exit() calls
79+
* Check if example file exists
580
*/
81+
const checkExampleExists = (example: string) =>
82+
Effect.gen(function* () {
83+
const exists = yield* Effect.sync(() => fs.existsSync(example));
684

7-
import { readdirSync, statSync } from 'fs';
8-
import { join } from 'path';
9-
import { $ } from 'bun';
10-
11-
const srcDir = join(import.meta.dir, 'src');
12-
13-
// Recursively find all .ts files in src/
14-
function findExamples(dir: string): string[] {
15-
const entries = readdirSync(dir);
16-
const files: string[] = [];
17-
18-
for (const entry of entries) {
19-
const fullPath = join(dir, entry);
20-
const stat = statSync(fullPath);
21-
22-
if (stat.isDirectory()) {
23-
files.push(...findExamples(fullPath));
24-
} else if (entry.endsWith('.ts')) {
25-
files.push(fullPath);
85+
if (!exists) {
86+
return yield* Effect.fail(new ExampleNotFoundError(example));
2687
}
88+
89+
return example;
90+
});
91+
92+
/**
93+
* Run a single example and capture output
94+
*/
95+
const runExample = (example: string, baseDir: string) =>
96+
Effect.gen(function* () {
97+
const startTime = new Date();
98+
const relativePath = example.replace(baseDir + '/', '');
99+
100+
// Run the example using bun
101+
const exitCode = yield* Effect.async<number, ExampleExecutionError>(
102+
(resume) => {
103+
const proc = spawn("bun", ["run", example], {
104+
stdio: ["ignore", "inherit", "inherit"],
105+
env: process.env,
106+
});
107+
108+
proc.on("close", (code) => {
109+
resume(Effect.succeed(code ?? 0));
110+
});
111+
112+
proc.on("error", (err) => {
113+
resume(
114+
Effect.fail(
115+
new ExampleExecutionError(
116+
example,
117+
-1,
118+
`Failed to spawn process: ${err.message}`
119+
)
120+
)
121+
);
122+
});
123+
}
124+
);
125+
126+
const endTime = new Date();
127+
const duration = endTime.getTime() - startTime.getTime();
128+
129+
const result: ExampleResult = {
130+
example: relativePath,
131+
exitCode,
132+
duration,
133+
success: exitCode === 0,
134+
startTime,
135+
endTime,
136+
};
137+
138+
return result;
139+
});
140+
141+
/**
142+
* Format duration in human-readable format
143+
*/
144+
const formatDuration = (ms: number): string => {
145+
const seconds = Math.floor(ms / 1000);
146+
const milliseconds = ms % 1000;
147+
148+
if (seconds > 0) {
149+
return `${seconds}.${Math.floor(milliseconds / 100)}s`;
27150
}
28-
29-
return files.sort();
30-
}
151+
return `${milliseconds}ms`;
152+
};
153+
154+
// ============================================================================
155+
// Main Program
156+
// ============================================================================
157+
158+
const program = Effect.gen(function* () {
159+
const baseDir = import.meta.dir;
160+
const srcDir = path.join(baseDir, 'src');
161+
162+
// Print header
163+
yield* Console.log("=".repeat(80));
164+
yield* Console.log("Effect-AI Examples Runner");
165+
yield* Console.log("=".repeat(80));
166+
yield* Console.log("");
31167

32-
const examples = findExamples(srcDir);
33-
console.log(`Found ${examples.length} example(s)\n`);
34-
35-
let failed = 0;
36-
for (const example of examples) {
37-
const relativePath = example.replace(import.meta.dir + '/', '');
38-
console.log(`\n${'='.repeat(80)}`);
39-
console.log(`Running: ${relativePath}`);
40-
console.log('='.repeat(80));
41-
42-
try {
43-
await $`bun run ${example}`.quiet();
44-
console.log(`✅ ${relativePath} completed successfully`);
45-
} catch (error) {
46-
console.error(`❌ ${relativePath} failed`);
47-
failed++;
168+
// Find all examples
169+
yield* Console.log("🔍 Searching for examples...");
170+
const examples = yield* findExamples(srcDir);
171+
yield* Console.log(`✓ Found ${examples.length} example(s)`);
172+
yield* Console.log("");
173+
174+
// Check all examples exist
175+
yield* Effect.all(
176+
examples.map((example) => checkExampleExists(example)),
177+
{ concurrency: "unbounded" }
178+
);
179+
180+
// Launch all examples
181+
yield* Console.log(`🚀 Launching ${examples.length} examples in parallel...`);
182+
yield* Console.log("");
183+
184+
// Print all examples being launched
185+
for (const example of examples) {
186+
const relativePath = example.replace(baseDir + '/', '');
187+
yield* Console.log(`⏳ Launching: ${relativePath}`);
48188
}
49-
}
50189

51-
console.log(`\n${'='.repeat(80)}`);
52-
console.log(`Results: ${examples.length - failed}/${examples.length} passed`);
53-
console.log('='.repeat(80));
190+
yield* Console.log("");
191+
yield* Console.log(`📊 All ${examples.length} examples launched!`);
192+
yield* Console.log(" Waiting for completion...");
193+
yield* Console.log("");
194+
195+
// Create tasks to run
196+
const exampleTasks = examples.map((example) => runExample(example, baseDir));
197+
198+
// Run all examples in parallel and collect results
199+
const results = yield* Effect.all(
200+
exampleTasks.map((task) => Effect.exit(task)),
201+
{ concurrency: "unbounded" }
202+
);
203+
204+
// Process results
205+
const successfulResults: ExampleResult[] = [];
206+
const failedResults: Array<{ example: string; error: unknown }> = [];
207+
208+
for (let index = 0; index < results.length; index++) {
209+
const exit = results[index];
210+
const example = examples[index];
211+
212+
if (!exit || !example) continue;
213+
214+
if (Exit.isSuccess(exit)) {
215+
const result = exit.value;
216+
successfulResults.push(result);
217+
218+
yield* Console.log(
219+
`✅ Success: ${result.example} (${formatDuration(result.duration)})`
220+
);
221+
} else if (Exit.isFailure(exit)) {
222+
const cause = exit.cause;
223+
const relativePath = example.replace(baseDir + '/', '');
224+
failedResults.push({ example: relativePath, error: cause });
225+
226+
yield* Console.log(`❌ Failed: ${relativePath}`);
227+
}
228+
}
229+
230+
// Print summary
231+
yield* Console.log("");
232+
yield* Console.log("=".repeat(80));
233+
yield* Console.log("Summary");
234+
yield* Console.log("=".repeat(80));
235+
yield* Console.log(`Total examples: ${examples.length}`);
236+
yield* Console.log(`✅ Successful: ${successfulResults.length}`);
237+
yield* Console.log(`❌ Failed: ${failedResults.length}`);
238+
yield* Console.log("");
239+
240+
// Show successful examples with details
241+
if (successfulResults.length > 0) {
242+
yield* Console.log("✅ Successful examples:");
243+
for (const result of successfulResults) {
244+
yield* Console.log(
245+
` ${result.example} - ${formatDuration(result.duration)}`
246+
);
247+
}
248+
yield* Console.log("");
249+
}
250+
251+
// Show failed examples with details
252+
if (failedResults.length > 0) {
253+
yield* Console.log("❌ Failed examples:");
254+
for (const failure of failedResults) {
255+
yield* Console.log(` ${failure.example}`);
256+
257+
// Try to extract error message
258+
const errorMsg = failure.error instanceof Error
259+
? failure.error.message
260+
: String(failure.error);
54261

55-
if (failed > 0) {
262+
if (errorMsg) {
263+
yield* Console.log(` Error: ${errorMsg}`);
264+
}
265+
}
266+
yield* Console.log("");
267+
}
268+
269+
// Final status
270+
yield* Console.log("=".repeat(80));
271+
if (failedResults.length > 0) {
272+
yield* Console.log(`Results: ${successfulResults.length}/${examples.length} passed`);
273+
yield* Console.log("=".repeat(80));
274+
275+
return { success: false, results: successfulResults, failures: failedResults };
276+
} else {
277+
yield* Console.log("All examples completed successfully! ✓");
278+
yield* Console.log("=".repeat(80));
279+
280+
return { success: true, results: successfulResults, failures: [] };
281+
}
282+
});
283+
284+
// ============================================================================
285+
// Error Handling
286+
// ============================================================================
287+
288+
const handleError = (error: unknown) => {
289+
if (error instanceof ExampleNotFoundError) {
290+
console.error(`\n❌ Example not found: ${error.example}\n`);
291+
process.exit(1);
292+
}
293+
294+
console.error("\n❌ Unexpected error:", error);
56295
process.exit(1);
57-
}
296+
};
297+
298+
// ============================================================================
299+
// Main Execution
300+
// ============================================================================
301+
302+
const main = async () => {
303+
const exit = await Effect.runPromiseExit(program);
304+
305+
if (Exit.isSuccess(exit)) {
306+
const { success } = exit.value;
307+
process.exit(success ? 0 : 1);
308+
} else {
309+
const cause = exit.cause;
310+
311+
// Extract the first failure from the cause
312+
const failure = cause._tag === "Fail" ? cause.error : cause;
313+
314+
handleError(failure);
315+
}
316+
};
317+
318+
// Run the program
319+
main().catch((error) => {
320+
console.error("\n❌ Fatal error:", error);
321+
process.exit(1);
322+
});
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# OpenRouter FileParserPlugin Examples (Effect AI)
2+
3+
Examples demonstrating OpenRouter's FileParserPlugin with @effect/ai.
4+
5+
## Overview
6+
7+
The FileParserPlugin integrates with Effect AI's type-safe, composable architecture to provide:
8+
9+
- Server-side PDF parsing with OpenRouter's file parser
10+
- Effect-based error handling and composition
11+
- Layer-based dependency injection for configuration
12+
- Concurrent processing with Effect.all
13+
14+
## Examples
15+
16+
- `file-parser-all-sizes.ts` - Tests PDF processing across multiple file sizes with Effect patterns
17+
18+
## Running
19+
20+
```bash
21+
bun run typescript/effect-ai/src/plugin-file-parser/file-parser-all-sizes.ts
22+
```

0 commit comments

Comments
 (0)