|
| 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