Skip to content

Commit 1e16343

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 e069c8d commit 1e16343

File tree

2 files changed

+199
-0
lines changed

2 files changed

+199
-0
lines changed
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+
```
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
/**
2+
* Example: OpenRouter FileParserPlugin with @effect/ai
3+
*
4+
* This example demonstrates how to use OpenRouter's FileParserPlugin with
5+
* Effect AI, combining idiomatic Effect patterns with PDF file processing:
6+
* - Effect.gen for generator-style effect composition
7+
* - Layer-based dependency injection
8+
* - Type-safe error handling with Effect
9+
* - File processing with the FileParserPlugin
10+
* - Uses shared fixtures module with absolute paths
11+
*
12+
* To run: bun run typescript/effect-ai/src/plugin-file-parser/file-parser-all-sizes.ts
13+
*/
14+
15+
import * as OpenRouterClient from '@effect/ai-openrouter/OpenRouterClient';
16+
import * as OpenRouterLanguageModel from '@effect/ai-openrouter/OpenRouterLanguageModel';
17+
import * as LanguageModel from '@effect/ai/LanguageModel';
18+
import * as Prompt from '@effect/ai/Prompt';
19+
import { FetchHttpClient } from '@effect/platform';
20+
import * as BunContext from '@effect/platform-bun/BunContext';
21+
import {
22+
type PdfSize,
23+
PDF_SIZES,
24+
extractCode,
25+
formatSize,
26+
getPdfPath,
27+
getPdfSize,
28+
readExpectedCode,
29+
readPdfAsDataUrl,
30+
} from '@openrouter-examples/shared/fixtures';
31+
import { Console, Effect, Layer, Redacted } from 'effect';
32+
33+
/**
34+
* OpenRouter FileParserPlugin configuration
35+
* This plugin enables server-side PDF parsing using OpenRouter's file parser
36+
* instead of including PDF content in the message text.
37+
*/
38+
const fileParserConfig: OpenRouterLanguageModel.Config.Service = {
39+
plugins: [
40+
{
41+
id: 'file-parser',
42+
pdf: {
43+
engine: 'mistral-ocr',
44+
},
45+
},
46+
],
47+
};
48+
49+
/**
50+
* Process a single PDF file with logging and error handling
51+
*/
52+
const processPdf = (size: PdfSize, expectedCode: string) =>
53+
Effect.gen(function* () {
54+
yield* Console.log(`\n=== ${size.toUpperCase()} PDF ===`);
55+
56+
const sizeBytes = getPdfSize(size);
57+
yield* Console.log(`Size: ${formatSize(sizeBytes)}`);
58+
yield* Console.log(`Expected: ${expectedCode}`);
59+
60+
const dataUrl = yield* Effect.promise(() => readPdfAsDataUrl(size));
61+
62+
/**
63+
* Construct prompt with file attachment for file parser plugin
64+
*
65+
* IMPORTANT: The PDF is sent as a file attachment via Prompt.makePart("file", ...)
66+
* and will be processed by OpenRouter's file parser plugin server-side.
67+
* The PDF content is NOT included in the text content - only the user instruction
68+
* is sent as text. The file parser plugin extracts the PDF content automatically.
69+
*/
70+
const prompt = Prompt.make([
71+
Prompt.makeMessage('user', {
72+
content: [
73+
// PDF file attachment - processed by file parser plugin
74+
Prompt.makePart('file', {
75+
mediaType: 'application/pdf',
76+
fileName: `${size}.pdf`,
77+
data: dataUrl,
78+
}),
79+
// Text instruction only - NO PDF content included here
80+
Prompt.makePart('text', {
81+
text: 'Extract the verification code. Reply with ONLY the code.',
82+
}),
83+
],
84+
}),
85+
]);
86+
87+
// Generate text with file parser plugin enabled
88+
// The plugin processes the PDF file attachment server-side
89+
const response = yield* LanguageModel.generateText({
90+
prompt,
91+
}).pipe(OpenRouterLanguageModel.withConfigOverride(fileParserConfig));
92+
93+
const extracted = extractCode(response.text);
94+
const success = extracted === expectedCode;
95+
96+
yield* Console.log(`Extracted: ${extracted || '(none)'}`);
97+
yield* Console.log(`Status: ${success ? '✅ PASS' : '❌ FAIL'}`);
98+
99+
return { success, extracted, expected: expectedCode };
100+
});
101+
102+
/**
103+
* Main program orchestrating all PDF runs
104+
*/
105+
const program = Effect.gen(function* () {
106+
yield* Console.log('╔════════════════════════════════════════════════════════════════════════════╗');
107+
yield* Console.log('║ OpenRouter FileParserPlugin - Effect AI ║');
108+
yield* Console.log('╚════════════════════════════════════════════════════════════════════════════╝');
109+
yield* Console.log();
110+
yield* Console.log('Testing PDF processing with verification code extraction');
111+
yield* Console.log();
112+
113+
const logFailure =
114+
(label: string) =>
115+
(error: unknown) =>
116+
Effect.gen(function* () {
117+
yield* Console.error(`Error processing ${label}:`, error);
118+
return {
119+
success: false,
120+
extracted: null,
121+
expected: '',
122+
};
123+
});
124+
125+
const results = yield* Effect.all(
126+
PDF_SIZES.map((size) =>
127+
Effect.gen(function* () {
128+
const expectedCode = yield* Effect.promise(() => readExpectedCode(size));
129+
return yield* processPdf(size, expectedCode).pipe(
130+
Effect.catchAll(logFailure(size)),
131+
);
132+
}),
133+
),
134+
{ concurrency: 'unbounded' },
135+
);
136+
137+
yield* Console.log('\n' + '='.repeat(80));
138+
139+
const passed = results.filter((r) => r.success).length;
140+
const total = results.length;
141+
142+
yield* Console.log(`Results: ${passed}/${total} passed`);
143+
yield* Console.log('='.repeat(80));
144+
145+
if (passed === total) {
146+
yield* Console.log('\n✅ All PDF sizes processed successfully!');
147+
return yield* Effect.succeed(0);
148+
}
149+
yield* Console.log('\n❌ Some PDF tests failed');
150+
return yield* Effect.succeed(1);
151+
});
152+
153+
/**
154+
* Layer composition for dependency injection
155+
*/
156+
const OpenRouterClientLayer = OpenRouterClient.layer({
157+
apiKey: Redacted.make(process.env.OPENROUTER_API_KEY!),
158+
}).pipe(Layer.provide(FetchHttpClient.layer));
159+
160+
const OpenRouterModelLayer = OpenRouterLanguageModel.layer({
161+
model: 'openai/gpt-4o-mini',
162+
config: {
163+
temperature: 0.7,
164+
max_tokens: 500,
165+
},
166+
}).pipe(Layer.provide(OpenRouterClientLayer));
167+
168+
/**
169+
* Run the program with dependency injection
170+
*/
171+
const exitCode = await program.pipe(
172+
Effect.provide(OpenRouterModelLayer),
173+
Effect.provide(BunContext.layer),
174+
Effect.runPromise,
175+
);
176+
177+
process.exit(exitCode);

0 commit comments

Comments
 (0)