Vercel ai gateway (#10)

* feat: use vercel ai gateway

* fix: correct response handling

* ci: add pipeline
This commit is contained in:
2025-10-11 15:07:54 +02:00
committed by GitHub
parent 99a04d2a3e
commit 017e538396
11 changed files with 1116 additions and 1003 deletions

View File

@@ -1,4 +1,3 @@
ANTHROPIC_API_KEY=
NEXT_PUBLIC_CONTACT_EMAIL= NEXT_PUBLIC_CONTACT_EMAIL=
POSTGRES_DATABASE= POSTGRES_DATABASE=
POSTGRES_HOST= POSTGRES_HOST=

86
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,86 @@
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
lint-and-typecheck:
name: Lint & Type Check
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'yarn'
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Run ESLint
run: yarn lint
- name: Run TypeScript type check
run: yarn typecheck
- name: Run Prettier check
run: yarn format
build:
name: Build
runs-on: ubuntu-latest
needs: lint-and-typecheck
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'yarn'
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Build application
run: yarn build
- name: Report deployment readiness
if: github.ref == 'refs/heads/main'
run: echo "✅ All checks passed - ready for deployment"
commitlint:
name: Commit Lint
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'yarn'
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Validate commit messages
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
yarn commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose
else
yarn commitlint --from HEAD~1 --to HEAD --verbose
fi

View File

@@ -1,6 +1,6 @@
# Synthetic Consumers Data Generator # Synthetic Consumers Data Generator
A NextJS application that makes use of the Anthropic Claude API to generate synthetic consumers and their weekly purchase history. For creating synthetic datasets for retail/e-commerce applications with believable user behaviors and spending patterns. A NextJS application that makes use of Anthropic models via Vercel AI Gateway to generate synthetic consumers and their weekly purchase history. For creating synthetic datasets for retail/e-commerce applications with believable user behaviors and spending patterns.
## 🌟 Features ## 🌟 Features

View File

@@ -16,8 +16,8 @@
"vercel:env": "vercel env pull .env" "vercel:env": "vercel env pull .env"
}, },
"dependencies": { "dependencies": {
"@anthropic-ai/sdk": "^0.32.1",
"@vercel/analytics": "^1.5.0", "@vercel/analytics": "^1.5.0",
"ai": "^5.0.68",
"axios": "^1.12.0", "axios": "^1.12.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
@@ -29,20 +29,21 @@
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"tailwind-merge": "^2.5.5", "tailwind-merge": "^2.5.5",
"zod": "^3.23.8" "zod": "^4.1.8"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^18.4.3", "@commitlint/cli": "^18.4.3",
"@commitlint/config-conventional": "^18.4.3", "@commitlint/config-conventional": "^18.4.3",
"@types/json-schema": "^7.0.15",
"@types/node": "^22.10.1", "@types/node": "^22.10.1",
"@types/react": "^18.3.12", "@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1", "@types/react-dom": "^18.3.1",
"@typescript-eslint/eslint-plugin": "^6.12.0", "@typescript-eslint/eslint-plugin": "^8.46.0",
"@typescript-eslint/parser": "^6.12.0", "@typescript-eslint/parser": "^8.46.0",
"audit-ci": "^6.6.1", "audit-ci": "^6.6.1",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"eslint": "^8", "eslint": "^8",
"eslint-config-next": "^15.0.3", "eslint-config-next": "^15.5.4",
"eslint-config-prettier": "^9.0.0", "eslint-config-prettier": "^9.0.0",
"husky": "^8.0.3", "husky": "^8.0.3",
"lint-staged": "^15.1.0", "lint-staged": "^15.1.0",
@@ -59,5 +60,8 @@
"*.{json,ts,tsx}": [ "*.{json,ts,tsx}": [
"prettier --write --ignore-unknown" "prettier --write --ignore-unknown"
] ]
},
"resolutions": {
"brace-expansion": "^2.0.2"
} }
} }

View File

@@ -19,7 +19,7 @@
} }
], ],
"paths": { "paths": {
"@anthropic": ["./utils/anthropicClient.ts"], "@aiGateway": ["./utils/aiGatewayClient.ts"],
"@components/*": ["./components/*"], "@components/*": ["./components/*"],
"@utils/*": ["./utils/*"], "@utils/*": ["./utils/*"],
"@consumer/*": ["./utils/consumer/*"], "@consumer/*": ["./utils/consumer/*"],

66
utils/aiGatewayClient.ts Normal file
View File

@@ -0,0 +1,66 @@
import 'dotenv/config';
import { generateText, tool, jsonSchema } from 'ai';
import type { JSONSchema7 } from 'json-schema';
export interface BaseTool {
readonly name: string;
readonly input_schema: {
readonly type: 'object';
readonly properties: Record<string, unknown>;
readonly required?: readonly string[];
readonly description?: string;
};
}
export interface ToolUseBlock {
type: 'tool_use';
id: string;
name: string;
input: Record<string, unknown>;
}
export async function makeRequest<T extends BaseTool>(
prompt: string,
toolDef: T
): Promise<Record<string, unknown>> {
try {
const { steps } = await generateText({
model: 'anthropic/claude-sonnet-4.5',
temperature: 1,
tools: {
[toolDef.name]: tool({
description: toolDef.input_schema.description || '',
inputSchema: jsonSchema(toolDef.input_schema as JSONSchema7),
execute: async args => args
})
},
toolChoice: {
type: 'tool',
toolName: toolDef.name
},
prompt
});
const toolCalls = steps.flatMap(step => step.toolCalls);
if (!toolCalls || toolCalls.length === 0) {
throw new Error('No tool calls found in response');
}
const typedCall = toolCalls[0] as unknown as {
toolName: string;
input: Record<string, unknown>;
};
if (typedCall.toolName !== toolDef.name) {
throw new Error(
`Expected tool ${toolDef.name} but got ${typedCall.toolName}`
);
}
return typedCall.input;
} catch (error) {
console.error('Error making request:', error);
throw Error('Vercel AI Gateway client error.');
}
}

View File

@@ -1,58 +0,0 @@
import 'dotenv/config';
import Anthropic from '@anthropic-ai/sdk';
export interface BaseTool {
readonly name: string;
readonly input_schema: {
readonly type: 'object';
readonly properties: Record<string, unknown>;
readonly required?: readonly string[];
readonly description?: string;
};
}
export type ToolUseBlock = {
type: 'tool_use';
id: string;
name: string;
input: Record<string, any>;
};
export async function makeRequest<T extends BaseTool>(prompt: string, tool: T) {
if (!process.env.ANTHROPIC_API_KEY) {
throw Error('No Anthropic API key found.');
}
const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
try {
const response = await client.messages.create({
model: 'claude-sonnet-4-0',
max_tokens: 8192,
temperature: 1,
tools: [tool],
messages: [{ role: 'user', content: prompt }]
});
if (response.stop_reason && response.stop_reason !== 'tool_use') {
throw Error(JSON.stringify(response));
}
if (response.content.length != 2) {
throw Error(JSON.stringify(response));
}
const content = response.content;
const toolUse = content.find((block): block is ToolUseBlock => block.type === 'tool_use');
if (!toolUse) {
throw new Error('No tool_use block found in response');
}
return toolUse.input;
} catch (error) {
console.error('Error making request:', error);
throw Error('Anthropic client error.');
}
}

View File

@@ -1,7 +1,7 @@
import 'dotenv/config'; import 'dotenv/config';
import { Consumer, consumerSchema } from './types'; import { Consumer, consumerSchema } from './types';
import { Tool } from './tool'; import { Tool } from './tool';
import { BaseTool, makeRequest } from '@anthropic'; import { BaseTool, makeRequest } from '@utils/aiGatewayClient';
import { generatePrompt } from './prompt'; import { generatePrompt } from './prompt';
import { generateConsumerSeed } from '@utils/generateConsumerSeed'; import { generateConsumerSeed } from '@utils/generateConsumerSeed';

View File

@@ -91,7 +91,7 @@ const coreSchema = z.object({
}); });
const routinesSchema = z.object({ const routinesSchema = z.object({
weekday: z.record(weekdayActivitySchema), weekday: z.record(z.string(), weekdayActivitySchema),
weekend: z.array(z.string()), weekend: z.array(z.string()),
commute: z.object({ commute: z.object({
method: z.string(), method: z.string(),
@@ -111,7 +111,7 @@ const financesSchema = z.object({
subscriptions: z.array(subscriptionSchema), subscriptions: z.array(subscriptionSchema),
spending_patterns: z.object({ spending_patterns: z.object({
impulsive_score: z.number().min(1).max(10), impulsive_score: z.number().min(1).max(10),
categories: z.record(spendingCategorySchema) categories: z.record(z.string(), spendingCategorySchema)
}) })
}); });

View File

@@ -1,6 +1,6 @@
import { PurchaseList, purchaseListSchema } from './types'; import { PurchaseList, purchaseListSchema } from './types';
import { Tool } from './tool'; import { Tool } from './tool';
import { BaseTool, makeRequest } from '@anthropic'; import { BaseTool, makeRequest } from '@utils/aiGatewayClient';
import { generatePrompt } from './prompt'; import { generatePrompt } from './prompt';
import { Consumer } from '@consumer/types'; import { Consumer } from '@consumer/types';

1882
yarn.lock

File diff suppressed because it is too large Load Diff