diff --git a/examples/claude-promptlayer-nextjs/.env.example b/examples/claude-promptlayer-nextjs/.env.example new file mode 100644 index 0000000..d6754b7 --- /dev/null +++ b/examples/claude-promptlayer-nextjs/.env.example @@ -0,0 +1,8 @@ +# Required for Claude API calls. +ANTHROPIC_API_KEY= + +# Required for PromptLayer tracing. +PROMPTLAYER_API_KEY= + +# Optional. Defaults to 3333. +PORT=3333 diff --git a/examples/claude-promptlayer-nextjs/.gitignore b/examples/claude-promptlayer-nextjs/.gitignore new file mode 100644 index 0000000..10adbb1 --- /dev/null +++ b/examples/claude-promptlayer-nextjs/.gitignore @@ -0,0 +1,30 @@ +# Dependencies +node_modules/ + +# Environment +.env +.env*.local + +# Next.js +.next/ +out/ +next-env.d.ts + +# Production/build output +dist/ +build/ + +# TypeScript +tsconfig.tsbuildinfo + +# Logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# OS/editor +.DS_Store +.vscode/ +.idea/ diff --git a/examples/claude-promptlayer-nextjs/README.md b/examples/claude-promptlayer-nextjs/README.md new file mode 100644 index 0000000..02f3fda --- /dev/null +++ b/examples/claude-promptlayer-nextjs/README.md @@ -0,0 +1,20 @@ +# Claude PromptLayer Next App + +Tiny Next.js app for manually testing PromptLayer's Claude Agent SDK integration. + +## Setup + +```bash +cp .env.example .env.local +npm install +npm run dev +``` + +Open the local URL printed by Next.js. + +## Environment + +- `ANTHROPIC_API_KEY` runs Claude. +- `PROMPTLAYER_API_KEY` enables PromptLayer tracing through `getClaudeConfig()`. + +The development server uses port `3333` by default. diff --git a/examples/claude-promptlayer-nextjs/app/api/run/route.ts b/examples/claude-promptlayer-nextjs/app/api/run/route.ts new file mode 100644 index 0000000..30432bb --- /dev/null +++ b/examples/claude-promptlayer-nextjs/app/api/run/route.ts @@ -0,0 +1,90 @@ +import { NextResponse } from "next/server"; +import { createSdkMcpServer, query, tool, type Options } from "@anthropic-ai/claude-agent-sdk"; +import { getClaudeConfig } from "promptlayer/claude-agents"; + +export const runtime = "nodejs"; + +export async function POST(request: Request) { + try { + const body = await request.json(); + const prompt = String(body.prompt || "").trim(); + const model = String(body.model || "sonnet").trim(); + + if (!prompt) { + return NextResponse.json({ error: "Prompt is required." }, { status: 400 }); + } + + const anthropicKey = requiredEnv("ANTHROPIC_API_KEY"); + const promptLayerKey = requiredEnv("PROMPTLAYER_API_KEY"); + const promptLayer = getClaudeConfig({ apiKey: promptLayerKey }); + const labTools = createSdkMcpServer({ + name: "lab-tools", + version: "0.1.0", + alwaysLoad: true, + tools: [ + tool("random_number", "Generate a random integer from 1 to 100.", {}, async () => { + const value = Math.floor(Math.random() * 100) + 1; + return { + content: [{ type: "text", text: String(value) }], + structuredContent: { value } + }; + }) + ] + }); + + const options: Options = { + cwd: process.cwd(), + model, + maxTurns: 3, + plugins: [promptLayer.plugin], + mcpServers: { + "lab-tools": labTools + }, + allowedTools: ["mcp__lab-tools__random_number"], + env: { + ...process.env, + ANTHROPIC_API_KEY: anthropicKey, + ...promptLayer.env + } + }; + + const messages: unknown[] = []; + for await (const message of query({ prompt, options })) { + messages.push(message); + } + + return NextResponse.json({ + text: extractText(messages), + messages + }); + } catch (error) { + return NextResponse.json( + { error: error instanceof Error ? error.message : String(error) }, + { status: 500 } + ); + } +} + +function requiredEnv(name: "ANTHROPIC_API_KEY" | "PROMPTLAYER_API_KEY") { + const value = process.env[name]?.trim(); + if (!value) throw new Error(`Missing ${name}. Add it to .env and restart npm run dev.`); + return value; +} + +function extractText(value: unknown) { + const parts: string[] = []; + visit(value); + return parts.join("\n"); + + function visit(item: unknown) { + if (Array.isArray(item)) { + item.forEach(visit); + return; + } + if (!item || typeof item !== "object") return; + + const record = item as Record; + if (typeof record.text === "string") parts.push(record.text); + Object.values(record).forEach(visit); + } +} diff --git a/examples/claude-promptlayer-nextjs/app/globals.css b/examples/claude-promptlayer-nextjs/app/globals.css new file mode 100644 index 0000000..ba88091 --- /dev/null +++ b/examples/claude-promptlayer-nextjs/app/globals.css @@ -0,0 +1,132 @@ +* { + box-sizing: border-box; +} + +body { + margin: 0; + background: #f6f7f8; + color: #172026; + font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; +} + +button, +input, +textarea { + font: inherit; +} + +button { + min-height: 38px; + border: 1px solid #d6dde2; + border-radius: 6px; + background: #fff; + padding: 0 14px; + cursor: pointer; +} + +button:disabled { + cursor: wait; + opacity: 0.7; +} + +.shell { + width: min(900px, calc(100vw - 32px)); + margin: 0 auto; + padding: 28px 0 40px; +} + +.panelHead { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; +} + +h1, +h2, +p { + margin: 0; +} + +h1 { + font-size: 28px; +} + +h2 { + font-size: 18px; +} + +p, +label { + color: #5d6b76; +} + +p { + margin-top: 6px; +} + +.panel, +.output { + border: 1px solid #d6dde2; + border-radius: 8px; + background: #fff; +} + +.panel, +.output { + margin-top: 18px; + padding: 18px; +} + +label { + display: grid; + gap: 6px; + margin-top: 14px; + font-size: 13px; +} + +label:first-child { + margin-top: 0; +} + +input, +textarea { + width: 100%; + border: 1px solid #d6dde2; + border-radius: 6px; + padding: 9px 10px; +} + +textarea { + min-height: 140px; + resize: vertical; +} + +.primary { + width: fit-content; + margin-top: 16px; + border-color: #0f766e; + background: #0f766e; + color: #fff; +} + +pre { + min-height: 260px; + max-height: 560px; + overflow: auto; + margin: 14px 0 0; + padding: 14px; + border-radius: 6px; + background: #101820; + color: #d7f7ef; + font-size: 13px; + line-height: 1.5; + white-space: pre-wrap; +} + +@media (max-width: 720px) { + .panelHead { + display: grid; + grid-template-columns: 1fr; + } +} diff --git a/examples/claude-promptlayer-nextjs/app/layout.tsx b/examples/claude-promptlayer-nextjs/app/layout.tsx new file mode 100644 index 0000000..dec73ab --- /dev/null +++ b/examples/claude-promptlayer-nextjs/app/layout.tsx @@ -0,0 +1,15 @@ +import type { Metadata } from "next"; +import "./globals.css"; + +export const metadata: Metadata = { + title: "Claude PromptLayer Next App", + description: "Local manual tester for Claude Agent SDK plus PromptLayer." +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} diff --git a/examples/claude-promptlayer-nextjs/app/page.tsx b/examples/claude-promptlayer-nextjs/app/page.tsx new file mode 100644 index 0000000..349de43 --- /dev/null +++ b/examples/claude-promptlayer-nextjs/app/page.tsx @@ -0,0 +1,65 @@ +"use client"; + +import { useState } from "react"; + +const defaultPrompt = "Use the random_number tool, then tell me the number it returned."; + +export default function Home() { + const [model, setModel] = useState("sonnet"); + const [prompt, setPrompt] = useState(defaultPrompt); + const [output, setOutput] = useState("Ready."); + const [running, setRunning] = useState(false); + + async function runPrompt() { + setRunning(true); + setOutput("Running..."); + + try { + const response = await fetch("/api/run", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ model, prompt }) + }); + const data = await response.json(); + + setOutput(JSON.stringify(data, null, 2)); + } catch (error) { + setOutput(JSON.stringify({ error: error instanceof Error ? error.message : String(error) }, null, 2)); + } finally { + setRunning(false); + } + } + + return ( +
+
+

Claude PromptLayer Lab

+

One route, one SDK call, PromptLayer tracing enabled through code.

+
+ +
+ + +