Examples
Real-world code examples demonstrating Alchemy patterns.
Complete runnable examples are available in the examples/ directory on GitHub.
Simple Text Translation
The most basic use case — send text, get text back.
import { Alchemist } from "@edv4h/alchemy-node";
import { OpenAITransmuter } from "@edv4h/alchemy-plugin-transmuter-openai";
const alchemist = new Alchemist({
transmuter: new OpenAITransmuter({
apiKey: process.env.OPENAI_API_KEY,
}),
});
const recipe = {
spell: () => "Translate this text to Japanese",
temperature: 0.3,
roleDefinition: "You are a professional translator",
};
const materials = [
{ type: "text" as const, text: "The quick brown fox jumps over the lazy dog." },
];
const result = await alchemist.transmute(recipe, materials);
console.log(result);
// → "素早い茶色の狐が怠けた犬を飛び越える。"
Structured JSON Output
Use Zod schemas to get validated, type-safe JSON responses from the LLM.
import { JsonRefiner } from "@edv4h/alchemy-core";
import { Alchemist } from "@edv4h/alchemy-node";
import { OpenAITransmuter } from "@edv4h/alchemy-plugin-transmuter-openai";
import { z } from "zod";
const ProductSchema = z.object({
name: z.string(),
tagline: z.string().max(100),
features: z.array(z.object({
title: z.string(),
description: z.string(),
})).min(3).max(5),
targetAudience: z.string(),
tone: z.enum(["professional", "casual", "playful"]),
});
const alchemist = new Alchemist({
transmuter: new OpenAITransmuter(),
});
const recipe = {
spell: () => "Generate a product description based on these notes",
refiner: new JsonRefiner(ProductSchema),
temperature: 0.7,
};
const materials = [
{ type: "text" as const, text: "Note-taking app for developers, markdown support, git integration" },
];
const result = await alchemist.transmute(recipe, materials);
// result is fully typed!
console.log(result.name); // "DevNotes"
console.log(result.features[0]); // { title: "...", description: "..." }
Multimodal Input
Combine text, images, and data in a single request.
import { JsonRefiner, dataToText } from "@edv4h/alchemy-core";
import { Alchemist } from "@edv4h/alchemy-node";
import { OpenAITransmuter } from "@edv4h/alchemy-plugin-transmuter-openai";
import { imageUrlToBase64 } from "@edv4h/alchemy-plugin-transforms-node";
import { z } from "zod";
const alchemist = new Alchemist({
transmuter: new OpenAITransmuter({ defaultModel: "gpt-4o" }),
transforms: [
imageUrlToBase64(), // Convert image URLs to base64 for API
dataToText(), // Convert CSV/JSON data to readable text
],
});
const AnalysisSchema = z.object({
chartDescription: z.string(),
trend: z.enum(["up", "down", "stable"]),
insights: z.array(z.string()),
dataHighlights: z.array(z.string()),
});
const recipe = {
spell: () => "Analyze this chart and its underlying data",
refiner: new JsonRefiner(AnalysisSchema),
};
const materials = [
{ type: "text" as const, text: "Q4 2024 Sales Report" },
{ type: "image" as const, source: { kind: "url" as const, url: "https://example.com/sales-chart.png" } },
{
type: "data" as const,
format: "csv" as const,
content: "month,revenue\nOct,45000\nNov,52000\nDec,61000",
label: "Monthly Revenue",
},
];
const result = await alchemist.transmute(recipe, materials);
console.log(result.trend); // "up"
console.log(result.insights); // ["Revenue grew 35% over Q4", ...]
Streaming Responses
Get progressive output for long-form content generation.
import { Alchemist } from "@edv4h/alchemy-node";
import { OpenAITransmuter } from "@edv4h/alchemy-plugin-transmuter-openai";
const alchemist = new Alchemist({
transmuter: new OpenAITransmuter(),
});
const recipe = {
spell: () => "Write a detailed tutorial about building REST APIs with Hono",
temperature: 0.6,
roleDefinition: "You are a senior developer writing a technical blog post",
};
const materials = [
{ type: "text" as const, text: "Target audience: intermediate TypeScript developers" },
];
// Stream a long response
const stream = alchemist.stream(recipe, materials);
// Process chunks as they arrive
let fullText = "";
for await (const chunk of stream) {
process.stdout.write(chunk);
fullText += chunk;
}
console.log(`\n\nTotal length: ${fullText.length} characters`);
Generate Variations
Generate multiple variations of the same recipe in parallel.
import { JsonRefiner } from "@edv4h/alchemy-core";
import { Alchemist } from "@edv4h/alchemy-node";
import { OpenAITransmuter } from "@edv4h/alchemy-plugin-transmuter-openai";
import { z } from "zod";
const CopySchema = z.object({
headline: z.string(),
body: z.string(),
cta: z.string(),
});
const alchemist = new Alchemist({
transmuter: new OpenAITransmuter(),
});
const recipe = {
spell: () => "Write marketing copy for this product",
refiner: new JsonRefiner(CopySchema),
temperature: 0.9,
roleDefinition: "You are a creative copywriter",
};
const materials = [
{ type: "text" as const, text: "AI-powered code review tool for teams" },
];
// Generate 3 variations in parallel
const results = await alchemist.generate(recipe, materials, 3);
for (const [key, value] of Object.entries(results)) {
if ("error" in value) continue;
console.log(`${key}:`, value.headline);
// "variation-1": { headline: "...", body: "...", cta: "..." }
}
React UI with useAlchemy
Build an interactive LLM-powered UI with the useAlchemy hook.
import { useAlchemy } from "@edv4h/alchemy-react";
import type { MaterialInput } from "@edv4h/alchemy-react";
function TranslatorApp() {
const {
transmute,
result,
isLoading,
error,
} = useAlchemy<{ translation: string; language: string }>({
initialRecipeId: "translate",
baseUrl: "/api",
});
const handleTranslate = async (text: string) => {
const materials: MaterialInput[] = [
{ type: "text", text },
];
await transmute(materials);
};
return (
<div>
<textarea
placeholder="Enter text to translate..."
onKeyDown={(e) => {
if (e.key === "Enter" && e.metaKey) {
handleTranslate(e.currentTarget.value);
}
}}
/>
{isLoading && <p>Translating...</p>}
{error && <p className="error">{error}</p>}
{result && (
<div className="result">
<p>{result.translation}</p>
<small>Language: {result.language}</small>
</div>
)}
</div>
);
}
Check out the hono-app example for a full-stack demo with React frontend and Hono backend, including a playground, travel assistant, and team landing page generator.
Material Validation
Validate materials before transmutation to catch errors early and avoid wasted API calls.
import { JsonRefiner, runMaterialValidation } from "@edv4h/alchemy-core";
import type { Recipe, MaterialPart } from "@edv4h/alchemy-core";
import { z } from "zod";
// Define a recipe with material requirements
const imageAnalysisRecipe: Recipe<MaterialPart[], { description: string }> = {
id: "image-analysis",
spell: (material) => ({ output: "Describe the provided images" }),
refiner: new JsonRefiner(z.object({ description: z.string() })),
// Declarative: require 1-3 images
requiredMaterials: [
{ type: "image", min: 1, max: 3, label: "Photos to analyze" },
],
};
// Validate before calling the API (async)
const materials: MaterialPart[] = [
{ type: "text", text: "What's in this photo?" },
];
const validation = await runMaterialValidation(imageAnalysisRecipe, materials);
if (!validation.valid) {
console.error("Validation failed:");
for (const issue of validation.issues ?? []) {
console.error(
` ${issue.label ?? issue.type}: expected ${issue.requirement.min}–${issue.requirement.max ?? "∞"}, got ${issue.actual} (${issue.kind})`
);
}
// → "Photos to analyze: expected 1–3, got 0 (too_few)"
}
Quality Scoring (evaluate & judgeMaterials)
Score each material's quality on a 0–1 scale and let judgeMaterials decide whether to proceed.
import { JsonRefiner, runMaterialValidation } from "@edv4h/alchemy-core";
import type { Recipe, MaterialPart, DataMaterialPart, TextMaterialPart } from "@edv4h/alchemy-core";
import { z } from "zod";
const AnalysisSchema = z.object({ themes: z.array(z.string()) });
type AnalysisResult = z.infer<typeof AnalysisSchema>;
const recipe: Recipe<MaterialPart[], AnalysisResult> = {
id: "find-theme",
requiredMaterials: [
{
type: "data", min: 1, label: "Engagement score",
evaluate: (parts) => {
const data = JSON.parse((parts[0] as DataMaterialPart).content);
return {
score: data.responseRate,
message: data.responseRate < 0.5 ? "Low response rate" : undefined,
};
},
},
{
type: "text", min: 1, label: "Issue description",
evaluate: (parts) => {
const len = (parts[0] as TextMaterialPart).text.length;
return { score: Math.min(len / 100, 1) };
},
},
],
judgeMaterials: (evaluations) => {
const failed = evaluations.filter((e) => e.evaluation.score < 0.3);
if (failed.length > 0) {
return { canTransmute: false, message: `${failed[0].label} quality is insufficient` };
}
const avg = evaluations.reduce((s, e) => s + e.evaluation.score, 0) / evaluations.length;
if (avg < 0.5) {
return { canTransmute: true, warning: "Material quality is low, accuracy may decrease" };
}
return { canTransmute: true };
},
spell: (material) => "Analyze the data and find themes",
refiner: new JsonRefiner(AnalysisSchema),
};
// Sample materials with a data part and text description
const materials: MaterialPart[] = [
{ type: "data", format: "json", content: '{"responseRate": 0.45}' },
{ type: "text", text: "Team engagement has been declining since Q3" },
];
// The result includes evaluations and judgement
const result = await runMaterialValidation(recipe, materials);
if (result.judgement?.warning) {
console.warn(result.judgement.warning);
}
Auto-Validation with Alchemist
Enable validateMaterials: true on AlchemistConfig to automatically validate before each transmute() or stream() call. Throws MaterialValidationError on failure.
import { Alchemist, MaterialValidationError } from "@edv4h/alchemy-node";
import { OpenAITransmuter } from "@edv4h/alchemy-plugin-transmuter-openai";
const alchemist = new Alchemist({
transmuter: new OpenAITransmuter(),
validateMaterials: true,
});
try {
const result = await alchemist.transmute(recipe, materials);
} catch (e) {
if (e instanceof MaterialValidationError) {
console.error(e.message); // "Engagement score quality is insufficient"
console.log(e.result.evaluations); // Per-material scores
console.log(e.result.judgement); // { canTransmute: false, message: "..." }
}
}
Custom Validation
For more complex rules, use validateMaterials alongside or instead of requiredMaterials. Supports both sync and async functions.
const recipe: Recipe<MaterialPart[], string> = {
id: "travel-plan",
spell: (material) => ({ output: "Create a travel itinerary" }),
refiner: new TextRefiner(),
requiredMaterials: [
{ type: "text", min: 1, label: "Travel preferences" },
],
validateMaterials: (parts) => {
const textParts = parts.filter((p) => p.type === "text");
const totalLength = textParts.reduce(
(sum, p) => sum + (p.type === "text" ? p.text.length : 0), 0,
);
if (totalLength < 20) {
return { valid: false, message: "Please provide more detail (at least 20 characters)" };
}
return { valid: true };
},
};