SMB analysts want to use AI to generate and execute data analysis code, but raw LLM outputs are often malformed, execution can runaway, and costs spiral without per‑job budgeting.
A complete, working implementation of this recipe — downloadable as a zip or browsable file by file. Generated by our build pipeline; tested with full coverage before publishing.
In this tutorial, you’ll build a Vertex AI-powered data analysis API that generates and runs code inside an E2B sandbox, with per-session budget enforcement. The system picks the cheapest capable Vertex AI model using @reaatech/llm-router-engine, maintains conversation state with @reaatech/session-continuity, and repairs malformed LLM output with @reaatech/structured-repair-core. By the end, you’ll have a working Next.js API that can accept a data-analysis prompt, route it through a budget-aware LLM pipeline, execute the generated code in an isolated sandbox, and return structured results with spend tracking.
Prerequisites
Node.js >= 22 — the project uses the built-in crypto.randomUUID() and modern ES module support
pnpm 10.x — the package manager wired in package.json
An E2B API key — sign up at e2b.dev/dashboard and create a key; the sandbox executor reads it from E2B_API_KEY
A Google Cloud project with Vertex AI enabled — enable the Vertex AI API in your project, note the project ID and location (e.g., us-central1)
Familiarity with TypeScript and Next.js App Router — you’ll be writing route handlers and service modules
Step 1: Scaffold the project and install dependencies
Start from an empty directory. Create a Next.js project with the App Router, then add the packages this recipe needs.
terminal
npx
create-next-app@latest
vertex-ai-analysis
--typescript
--app
--src-dir
--eslint
--no-tailwind
--import-alias
"@/*"
cd vertex-ai-analysis
Now install the dependencies you’ll use. The @reaatech/* packages provide the budget engine, LLM router, session manager, and structured repair. @google-cloud/vertexai calls the Vertex AI Gemini API. @e2b/code-interpreter runs code in an isolated sandbox. p-limit limits concurrent sandboxes, and zod defines the output schema.
Your package.json should now have all dependencies pinned to exact versions (no ^ or ~). The scripts section should include dev, build, start, lint, typecheck, and test:
Also add the same placeholders to .env.example so other developers know what’s required:
env
# Env vars used by vertex-ai-sandboxed-data-analysis-with-budget-guardrails-for-smbs.# The builder adds entries here as it wires up each integration.# Keep placeholders only — never commit real values.NODE_ENV=development# E2B Code Interpreter — get your API key at https://e2b.dev/dashboardE2B_API_KEY=<your-e2b-api-key># Google Cloud Vertex AI — project ID and regionGOOGLE_CLOUD_PROJECT=<your-gcp-project-id>GOOGLE_CLOUD_LOCATION=us-central1# Per-analysis budget limit in USDANALYSIS_BUDGET_LIMIT=1.00# Default Vertex AI model ID for fallback/auto-downgradeVERTEX_MODEL_ID=gemini-1.5-flash# Sandbox execution timeout in millisecondsE2B_SANDBOX_TIMEOUT_MS=30000
Step 3: Define the shared types
Create src/lib/types.ts. These interfaces flow through every layer of the pipeline — from the HTTP request body to the sandbox result to the final API response:
The @reaatech/session-continuity package needs an IStorageAdapter to persist sessions and messages, and a TokenCounter to estimate token usage. Create src/lib/memory-storage.ts with an in-memory implementation of both. This keeps everything local — no database required.
ts
import { type IStorageAdapter, type TokenCounter, type HealthStatus, type Session, type Message,} from "@reaatech/session-continuity";export class InMemoryStorageAdapter implements IStorageAdapter { private sessions = new Map<string, Session>(); private messages = new Map<string, Message[]>(); private messageIdCounter = 0; createSession( session: Omit<Session, "id"
Step 5: Create the budget enforcement layer
Create src/lib/budget-check.ts. This wraps @reaatech/agent-budget-engine’s BudgetController into helper functions that your analytics agent calls at each stage of the pipeline — checking budget before generating, recording spend after execution, and reading the current state:
Create src/lib/vertex-client.ts. This wraps @google-cloud/vertexai into a simple class with a generateContent method that your agent calls to produce analysis code:
ts
import { VertexAI } from "@google-cloud/vertexai";export class VertexAIClient { private client: VertexAI; constructor() { const project = process.env.GOOGLE_CLOUD_PROJECT ?? ""; const location = process.env.GOOGLE_CLOUD_LOCATION ?? "us-central1"; this.client = new VertexAI({ project, location }); } getGenerativeModel(modelId: string) { return this.client.getGenerativeModel({ model: modelId, generationConfig: { maxOutputTokens: 4096 }, }); } async generateContent( modelId: string, prompt: string, systemInstruction?: string, ): Promise<string> { try { const model = this.getGenerativeModel(modelId); const result = await model.generateContent({ contents: [{ role: "user", parts: [{ text: prompt }] }], systemInstruction: systemInstruction ? { role: "system", parts: [{ text: systemInstruction }] } : undefined, }); const text = result.response.candidates?.[0]?.content?.parts?.[0]?.text; return text ?? ""; } catch (cause) { throw new Error( `Vertex AI generateContent failed for model ${modelId}`, { cause: cause instanceof Error ? cause : new Error(String(cause)) }, ); } }}export function createVertexClient(): VertexAIClient { return new VertexAIClient();}
Step 7: Create the session store
Create src/lib/session-store.ts. This wraps @reaatech/session-continuity’s SessionManager with factory functions and helpers for creating sessions, adding messages, and retrieving conversation context:
ts
import { SessionManager, type Session, type Message,} from "@reaatech/session-continuity";import { InMemoryStorageAdapter, SimpleTokenCounter } from "./memory-storage.js";let singletonManager: SessionManager | null = null;export function createSessionManager(): SessionManager { const storage = new InMemoryStorageAdapter(); const tokenCounter = new SimpleTokenCounter(); const manager = new SessionManager({ storage, tokenCounter, tokenBudget: { maxTokens: 8000, reserveTokens: 1000, overflowStrategy: "compress", }, compression: { strategy: "sliding_window", targetTokens: 7000, }, }); singletonManager = manager; return manager;}export function getSingletonManager(): SessionManager { if (singletonManager === null) { return createSessionManager(); } return singletonManager;}export async function createSession( manager: SessionManager, userId?: string,): Promise<Session> { return manager.createSession({ userId });}export async function getSession( manager: SessionManager, sessionId: string,): Promise<Session> { return manager.getSession(sessionId);}export async function addMessage( manager: SessionManager, sessionId: string, role: "user" | "assistant", content: string,): Promise<Message> { return manager.addMessage(sessionId, { role, content });}export async function getConversationContext( manager: SessionManager, sessionId: string,): Promise<Message[]> { return manager.getConversationContext(sessionId);}export async function endSession( manager: SessionManager, sessionId: string,): Promise<void> { return manager.endSession(sessionId);}
Step 8: Wire up the LLM router
Create src/lib/router-config.ts. This defines two Vertex AI models — gemini-1.5-flash ($0.075/M input tokens) and gemini-1.5-pro ($1.25/M input tokens) — and configures a cost-optimized routing strategy:
Create src/lib/sandbox-executor.ts. The executeCode function creates an E2B sandbox, runs the generated code with a configurable timeout, and returns the text output. A pLimit(2) concurrency limiter ensures at most two sandboxes run at once:
Create src/lib/output-repair.ts. The generated code’s output may be malformed — wrapped in fences, missing fields, or not valid JSON. This module uses @reaatech/structured-repair-core to fix it against a Zod schema:
ts
import { z } from "zod";import { repair, repairOutput, isValid, analyzeInput, UnrepairableError,} from "@reaatech/structured-repair-core";export const AnalysisResultSchema = z.object({ summary: z.string(), insights: z.array(z.string()), code: z.string().optional(),});export type AnalysisResult = z.infer<typeof AnalysisResultSchema>;export async function repairAnalysisOutput(raw: string): Promise<AnalysisResult> { try { return await repair(AnalysisResultSchema, raw); } catch { throw new UnrepairableError("Could not repair analysis output", raw, []); }}export function repairWithDiagnostics(raw: string) { return repairOutput({ schema: AnalysisResultSchema, input: raw, debug: true, });}export function validateOutput(raw: string): boolean { return isValid(AnalysisResultSchema, raw);}export function analyzeRawInput(raw: string) { return analyzeInput(raw);}
Step 11: Build the analytics agent
Create src/lib/analytics-agent.ts. This is the orchestrator that wires all components into a single runAnalysis method:
ts
import { BudgetController } from "@reaatech/agent-budget-engine";import { SessionManager } from "@reaatech/session-continuity";import { LLMRouter } from "@reaatech/llm-router-engine";import { checkBudget, recordSpend, getState, defineBudget,} from "./budget-check.js";import { getSession, addMessage } from "./session-store.js";import { type VertexAIClient } from "./vertex-client.js";import { executeCode } from "./sandbox-executor.js";import { repairAnalysisOutput } from "./output-repair.js";import { type AnalysisResponse } from
Step 12: Create the API route handlers
Create app/api/analysis/route.ts — the main endpoint:
ts
import { type NextRequest, NextResponse } from "next/server";import { AnalyticsAgent } from "../../../src/lib/analytics-agent.js";import { createBudgetController } from "../../../src/lib/budget-check.js";import { createSessionManager } from "../../../src/lib/session-store.js";import { createVertexClient } from "../../../src/lib/vertex-client.js";import { createLLMRouter } from "../../../src/lib/router-config.js";const budgetController = createBudgetController();const sessionManager = createSessionManager();const vertexClient = createVertexClient();const llmRouter = createLLMRouter(vertexClient);const agent = new AnalyticsAgent({ budgetController, sessionManager, vertexClient, llmRouter,});export async function POST(req: NextRequest) { try { const body: { sessionId?: string; prompt?: string } = await req.json() as { sessionId?: string; prompt?: string }; if (!body.sessionId || !body.prompt) { return NextResponse.json( { error: "Missing sessionId or prompt" }, { status: 400 }, ); } const result = await agent.runAnalysis(body.sessionId, body.prompt); return NextResponse.json(result, { status: result.success ? 200 : 500, }); } catch (error) { console.error("[API/analysis]", error); return NextResponse.json( { error: "Internal server error" }, { status: 500 }, ); }}export function GET(_req: NextRequest) { void _req; return NextResponse.json({ status: "ok", service: "vertex-ai-analysis" });}
Create app/api/analysis/session/route.ts — the session lifecycle endpoint:
Verify everything works by running the type checker, linter, and test suite:
terminal
pnpm typecheckpnpm lintpnpm test
Expected output:typecheck exits with no errors. lint exits clean. pnpm test runs 110 tests and prints a summary ending with all tests passing. The coverage report shows full line coverage with high branch and function coverage across the pipeline modules:
curl -X POST http://localhost:3000/api/analysis \ -H 'Content-Type: application/json' \ -d '{"sessionId":"<paste-session-id>","prompt":"analyze sales data trends"}'
Expected output: a JSON response showing the analysis result with budget tracking:
json
{"success":true,"data":{"summary":"Sales data shows steady growth...","insights":["Q1 revenue increased 12%","Top 3 products account for 60% of sales"]},"sessionId":"...","budgetSpent":0.05,"budgetRemaining":0.95}
Add accurate pricing — integrate @reaatech/agent-budget-pricing for per-token cost estimation instead of the default flat estimate
Swap storage for persistence — replace InMemoryStorageAdapter with PostgreSQL or Redis via a custom IStorageAdapter so sessions survive server restarts
Add a dashboard UI — build a Next.js page that lists sessions, shows budget utilization charts, and lets users submit analysis prompts from a form