diff --git a/js/testapps/anthropic/README.md b/js/testapps/anthropic/README.md new file mode 100644 index 0000000000..3343b90ea7 --- /dev/null +++ b/js/testapps/anthropic/README.md @@ -0,0 +1,67 @@ +# Anthropic Plugin Sample + +This test app demonstrates usage of the Genkit Anthropic plugin against both the stable and beta runners, organized by feature. + +## Directory Structure + +``` +src/ + stable/ + basic.ts - Basic stable API examples (hello, streaming) + text-plain.ts - Text/plain error handling demonstration + webp.ts - WEBP image handling demonstration + pdf.ts - PDF document processing examples + attention-first-page.pdf - Sample PDF file for testing + beta/ + basic.ts - Basic beta API examples +``` + +## Setup + +1. From the repo root run `pnpm install` followed by `pnpm run setup` to link workspace dependencies. +2. In this directory, optionally run `pnpm install` if you want a local `node_modules/`. +3. Export an Anthropic API key (or add it to a `.env` file) before running any samples: + + ```bash + export ANTHROPIC_API_KEY=your-key + ``` + +## Available scripts + +### Basic Examples +- `pnpm run build` – Compile the TypeScript sources into `lib/`. +- `pnpm run start:stable` – Run the compiled stable basic sample. +- `pnpm run start:beta` – Run the compiled beta basic sample. +- `pnpm run dev:stable` – Start the Genkit Dev UI over `src/stable/basic.ts` with live reload. +- `pnpm run dev:beta` – Start the Genkit Dev UI over `src/beta/basic.ts` with live reload. + +### Feature-Specific Examples +- `pnpm run dev:stable:text-plain` – Start Dev UI for text/plain error handling demo. +- `pnpm run dev:stable:webp` – Start Dev UI for WEBP image handling demo. +- `pnpm run dev:stable:pdf` – Start Dev UI for PDF document processing demo. + +## Flows + +Each source file defines flows that can be invoked from the Dev UI or the Genkit CLI: + +### Basic Examples +- `anthropic-stable-hello` – Simple greeting using stable API +- `anthropic-stable-stream` – Streaming response example +- `anthropic-beta-hello` – Simple greeting using beta API +- `anthropic-beta-stream` – Streaming response with beta API +- `anthropic-beta-opus41` – Test Opus 4.1 model with beta API + +### Text/Plain Handling +- `stable-text-plain-error` – Demonstrates the helpful error when using text/plain as media +- `stable-text-plain-correct` – Shows the correct way to send text content + +### WEBP Image Handling +- `stable-webp-matching` – WEBP image with matching contentType +- `stable-webp-mismatched` – WEBP image with mismatched contentType (demonstrates the fix) + +### PDF Document Processing +- `stable-pdf-base64` – Process a PDF from a local file using base64 encoding +- `stable-pdf-url` – Process a PDF from a publicly accessible URL +- `stable-pdf-analysis` – Analyze a PDF document for key topics, concepts, and visual elements + +Example: `genkit flow:run anthropic-stable-hello` diff --git a/js/testapps/anthropic/package.json b/js/testapps/anthropic/package.json new file mode 100644 index 0000000000..08e1a0d2fd --- /dev/null +++ b/js/testapps/anthropic/package.json @@ -0,0 +1,36 @@ +{ + "name": "anthropic-testapp", + "version": "0.0.1", + "description": "Sample Genkit app showcasing Anthropic plugin stable and beta usage.", + "main": "lib/stable/basic.js", + "scripts": { + "build": "tsc", + "build:watch": "tsc --watch", + "start:stable": "node lib/stable/basic.js", + "start:beta": "node lib/beta/basic.js", + "dev:stable": "genkit start -- npx tsx --watch src/stable/basic.ts", + "dev:beta": "genkit start -- npx tsx --watch src/beta/basic.ts", + "dev:stable:text-plain": "genkit start -- npx tsx --watch src/stable/text-plain.ts", + "dev:stable:webp": "genkit start -- npx tsx --watch src/stable/webp.ts", + "dev:stable:pdf": "genkit start -- npx tsx --watch src/stable/pdf.ts", + "genkit:dev": "cross-env GENKIT_ENV=dev npm run dev:stable", + "genkit:start": "cross-env GENKIT_ENV=dev genkit start -- tsx --watch src/stable/basic.ts", + "dev": "export GENKIT_RUNTIME_ID=$(openssl rand -hex 8) && node lib/stable/basic.js 2>&1" + }, + "keywords": [ + "genkit", + "anthropic", + "sample" + ], + "author": "", + "license": "Apache-2.0", + "dependencies": { + "genkit": "workspace:*", + "@genkit-ai/anthropic": "workspace:*" + }, + "devDependencies": { + "cross-env": "^10.1.0", + "tsx": "^4.19.2", + "typescript": "^5.6.2" + } +} diff --git a/js/testapps/anthropic/src/beta/basic.ts b/js/testapps/anthropic/src/beta/basic.ts new file mode 100644 index 0000000000..d1309b3400 --- /dev/null +++ b/js/testapps/anthropic/src/beta/basic.ts @@ -0,0 +1,76 @@ +/** + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { anthropic } from '@genkit-ai/anthropic'; +import { genkit } from 'genkit'; + +const ai = genkit({ + plugins: [ + // Default all flows in this sample to the beta surface + anthropic({ apiVersion: 'beta', cacheSystemPrompt: true }), + ], +}); + +const betaHaiku = anthropic.model('claude-3-5-haiku', { apiVersion: 'beta' }); +const betaSonnet = anthropic.model('claude-sonnet-4-5', { apiVersion: 'beta' }); +const betaOpus41 = anthropic.model('claude-opus-4-1', { apiVersion: 'beta' }); + +ai.defineFlow('anthropic-beta-hello', async () => { + const { text } = await ai.generate({ + model: betaHaiku, + prompt: + 'You are Claude on the beta API. Provide a concise greeting that mentions that you are using the beta API.', + config: { temperature: 0.6 }, + }); + + return text; +}); + +ai.defineFlow('anthropic-beta-stream', async (_, { sendChunk }) => { + const { stream } = ai.generateStream({ + model: betaSonnet, + prompt: [ + { + text: 'Outline two experimental capabilities unlocked by the Anthropic beta API.', + }, + ], + config: { + apiVersion: 'beta', + temperature: 0.4, + }, + }); + + const collected: string[] = []; + for await (const chunk of stream) { + if (chunk.text) { + collected.push(chunk.text); + sendChunk(chunk.text); + } + } + + return collected.join(''); +}); + +ai.defineFlow('anthropic-beta-opus41', async () => { + const { text } = await ai.generate({ + model: betaOpus41, + prompt: + 'You are Claude Opus 4.1 on the beta API. Provide a brief greeting that confirms you are using the beta API.', + config: { temperature: 0.6 }, + }); + + return text; +}); diff --git a/js/testapps/anthropic/src/stable/attention-first-page.pdf b/js/testapps/anthropic/src/stable/attention-first-page.pdf new file mode 100644 index 0000000000..95c6625029 Binary files /dev/null and b/js/testapps/anthropic/src/stable/attention-first-page.pdf differ diff --git a/js/testapps/anthropic/src/stable/basic.ts b/js/testapps/anthropic/src/stable/basic.ts new file mode 100644 index 0000000000..246a42539a --- /dev/null +++ b/js/testapps/anthropic/src/stable/basic.ts @@ -0,0 +1,51 @@ +/** + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { anthropic } from '@genkit-ai/anthropic'; +import { genkit } from 'genkit'; + +const ai = genkit({ + plugins: [ + // Configure the plugin with environment-driven API key + anthropic(), + ], +}); + +ai.defineFlow('anthropic-stable-hello', async () => { + const { text } = await ai.generate({ + model: anthropic.model('claude-sonnet-4-5'), + prompt: 'You are a friendly Claude assistant. Greet the user briefly.', + }); + + return text; +}); + +ai.defineFlow('anthropic-stable-stream', async (_, { sendChunk }) => { + const { stream } = ai.generateStream({ + model: anthropic.model('claude-sonnet-4-5'), + prompt: 'Compose a short limerick about using Genkit with Anthropic.', + }); + + let response = ''; + for await (const chunk of stream) { + response += chunk.text ?? ''; + if (chunk.text) { + sendChunk(chunk.text); + } + } + + return response; +}); diff --git a/js/testapps/anthropic/src/stable/pdf.ts b/js/testapps/anthropic/src/stable/pdf.ts new file mode 100644 index 0000000000..8953dff696 --- /dev/null +++ b/js/testapps/anthropic/src/stable/pdf.ts @@ -0,0 +1,122 @@ +/** + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { anthropic } from '@genkit-ai/anthropic'; +import * as fs from 'fs'; +import { genkit } from 'genkit'; +import * as path from 'path'; + +const ai = genkit({ + plugins: [anthropic()], +}); + +/** + * This flow demonstrates PDF document processing with Claude using base64 encoding. + * The PDF is read from the source directory and sent as a base64 data URL. + */ +ai.defineFlow('stable-pdf-base64', async () => { + // Read PDF file from the same directory as this source file + const pdfPath = path.join(__dirname, 'attention-first-page.pdf'); + const pdfBuffer = fs.readFileSync(pdfPath); + const pdfBase64 = pdfBuffer.toString('base64'); + + const { text } = await ai.generate({ + model: anthropic.model('claude-sonnet-4-5'), + messages: [ + { + role: 'user', + content: [ + { + text: 'What are the key findings or main points in this document?', + }, + { + media: { + url: `data:application/pdf;base64,${pdfBase64}`, + contentType: 'application/pdf', + }, + }, + ], + }, + ], + }); + + return text; +}); + +/** + * This flow demonstrates PDF document processing with a URL reference. + * Note: This requires the PDF to be hosted at a publicly accessible URL. + */ +ai.defineFlow('stable-pdf-url', async () => { + // Example: Using a publicly hosted PDF URL + // In a real application, you would use your own hosted PDF + const pdfUrl = + 'https://assets.anthropic.com/m/1cd9d098ac3e6467/original/Claude-3-Model-Card-October-Addendum.pdf'; + + const { text } = await ai.generate({ + model: anthropic.model('claude-sonnet-4-5'), + messages: [ + { + role: 'user', + content: [ + { + text: 'Summarize the key points from this document.', + }, + { + media: { + url: pdfUrl, + contentType: 'application/pdf', + }, + }, + ], + }, + ], + }); + + return text; +}); + +/** + * This flow demonstrates analyzing specific aspects of a PDF document. + * Claude can understand both text and visual elements (charts, tables, images) in PDFs. + */ +ai.defineFlow('stable-pdf-analysis', async () => { + const pdfPath = path.join(__dirname, 'attention-first-page.pdf'); + const pdfBuffer = fs.readFileSync(pdfPath); + const pdfBase64 = pdfBuffer.toString('base64'); + + const { text } = await ai.generate({ + model: anthropic.model('claude-sonnet-4-5'), + messages: [ + { + role: 'user', + content: [ + { + text: 'Analyze this document and provide:\n1. The main topic or subject\n2. Any key technical concepts mentioned\n3. Any visual elements (charts, tables, diagrams) if present', + }, + { + media: { + url: `data:application/pdf;base64,${pdfBase64}`, + contentType: 'application/pdf', + }, + }, + ], + }, + ], + }); + + return text; +}); diff --git a/js/testapps/anthropic/src/stable/text-plain.ts b/js/testapps/anthropic/src/stable/text-plain.ts new file mode 100644 index 0000000000..0b290d53e6 --- /dev/null +++ b/js/testapps/anthropic/src/stable/text-plain.ts @@ -0,0 +1,83 @@ +/** + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { anthropic } from '@genkit-ai/anthropic'; +import { genkit } from 'genkit'; + +const ai = genkit({ + plugins: [anthropic()], +}); + +/** + * This flow demonstrates the error that occurs when trying to use text/plain + * files as media. The plugin will throw a helpful error message guiding users + * to use text content instead. + * + * Error message: "Unsupported media type: text/plain. Text files should be sent + * as text content in the message, not as media. For example, use { text: '...' } + * instead of { media: { url: '...', contentType: 'text/plain' } }" + */ +ai.defineFlow('stable-text-plain-error', async () => { + try { + await ai.generate({ + model: anthropic.model('claude-sonnet-4-5'), + messages: [ + { + role: 'user', + content: [ + { + media: { + url: 'data:text/plain;base64,SGVsbG8gV29ybGQ=', + contentType: 'text/plain', + }, + }, + ], + }, + ], + }); + return 'Unexpected: Should have thrown an error'; + } catch (error: any) { + return { + error: error.message, + note: 'This demonstrates the helpful error message for text/plain files', + }; + } +}); + +/** + * This flow demonstrates the correct way to send text content. + * Instead of using media with text/plain, use the text field directly. + */ +ai.defineFlow('stable-text-plain-correct', async () => { + // Read the text content (in a real app, you'd read from a file) + const textContent = 'Hello World\n\nThis is a text file content.'; + + const { text } = await ai.generate({ + model: anthropic.model('claude-sonnet-4-5'), + messages: [ + { + role: 'user', + content: [ + { + text: `Please summarize this text file content:\n\n${textContent}`, + }, + ], + }, + ], + }); + + return text; +}); diff --git a/js/testapps/anthropic/src/stable/webp.ts b/js/testapps/anthropic/src/stable/webp.ts new file mode 100644 index 0000000000..f8a861024b --- /dev/null +++ b/js/testapps/anthropic/src/stable/webp.ts @@ -0,0 +1,95 @@ +/** + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { anthropic } from '@genkit-ai/anthropic'; +import { genkit } from 'genkit'; + +const ai = genkit({ + plugins: [anthropic()], +}); + +/** + * This flow demonstrates WEBP image handling with matching contentType. + * Both the data URL and the contentType field specify image/webp. + */ +ai.defineFlow('stable-webp-matching', async () => { + // Minimal valid WEBP image (1x1 pixel, transparent) + // In a real app, you'd load an actual WEBP image file + const webpImageData = + 'data:image/webp;base64,UklGRiQAAABXRUJQVlA4IBgAAAAwAQCdASoBAAEAAwA0JaQAA3AA/vuUAAA='; + + const { text } = await ai.generate({ + model: anthropic.model('claude-sonnet-4-5'), + messages: [ + { + role: 'user', + content: [ + { text: 'Describe this image:' }, + { + media: { + url: webpImageData, + contentType: 'image/webp', + }, + }, + ], + }, + ], + }); + + return text; +}); + +/** + * This flow demonstrates the fix for WEBP images with mismatched contentType. + * Even if contentType says 'image/png', the plugin will use 'image/webp' from + * the data URL, preventing API validation errors. + * + * This fix ensures that the media_type sent to Anthropic matches the actual + * image data, which is critical for WEBP images that were previously causing + * "Image does not match the provided media type" errors. + */ +ai.defineFlow('stable-webp-mismatched', async () => { + // Minimal valid WEBP image (1x1 pixel, transparent) + const webpImageData = + 'data:image/webp;base64,UklGRiQAAABXRUJQVlA4IBgAAAAwAQCdASoBAAEAAwA0JaQAA3AA/vuUAAA='; + + const { text } = await ai.generate({ + model: anthropic.model('claude-sonnet-4-5'), + messages: [ + { + role: 'user', + content: [ + { + text: 'Describe this image (note: contentType is wrong but data URL is correct):', + }, + { + media: { + // Data URL says WEBP, but contentType says PNG + // The plugin will use WEBP from the data URL (the fix) + url: webpImageData, + contentType: 'image/png', // This mismatch is handled correctly + }, + }, + ], + }, + ], + }); + + return { + result: text, + note: 'The plugin correctly used image/webp from the data URL, not image/png from contentType', + }; +}); diff --git a/js/testapps/anthropic/tsconfig.json b/js/testapps/anthropic/tsconfig.json new file mode 100644 index 0000000000..efbb566bf7 --- /dev/null +++ b/js/testapps/anthropic/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compileOnSave": true, + "include": ["src"], + "compilerOptions": { + "module": "commonjs", + "noImplicitReturns": true, + "outDir": "lib", + "sourceMap": true, + "strict": true, + "target": "es2017", + "skipLibCheck": true, + "esModuleInterop": true + } +}