Full Examples

Complete runnable examples are available in the examples/ directory on GitHub.

Simple Text Translation

The most basic use case — send text, get text back.

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

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

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

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

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

tsx
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>
  );
}
Live Demo

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.

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

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

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

ts
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 };
  },
};