Vercel ai gateway (#10)
* feat: use vercel ai gateway * fix: correct response handling * ci: add pipeline
This commit is contained in:
@@ -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
86
.github/workflows/ci.yml
vendored
Normal 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
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
14
package.json
14
package.json
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
66
utils/aiGatewayClient.ts
Normal 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.');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user