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=
|
||||
POSTGRES_DATABASE=
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
|
||||
14
package.json
14
package.json
@@ -16,8 +16,8 @@
|
||||
"vercel:env": "vercel env pull .env"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.32.1",
|
||||
"@vercel/analytics": "^1.5.0",
|
||||
"ai": "^5.0.68",
|
||||
"axios": "^1.12.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
@@ -29,20 +29,21 @@
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"tailwind-merge": "^2.5.5",
|
||||
"zod": "^3.23.8"
|
||||
"zod": "^4.1.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^18.4.3",
|
||||
"@commitlint/config-conventional": "^18.4.3",
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"@types/node": "^22.10.1",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@typescript-eslint/eslint-plugin": "^6.12.0",
|
||||
"@typescript-eslint/parser": "^6.12.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.46.0",
|
||||
"@typescript-eslint/parser": "^8.46.0",
|
||||
"audit-ci": "^6.6.1",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "^15.0.3",
|
||||
"eslint-config-next": "^15.5.4",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"husky": "^8.0.3",
|
||||
"lint-staged": "^15.1.0",
|
||||
@@ -59,5 +60,8 @@
|
||||
"*.{json,ts,tsx}": [
|
||||
"prettier --write --ignore-unknown"
|
||||
]
|
||||
},
|
||||
"resolutions": {
|
||||
"brace-expansion": "^2.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@anthropic": ["./utils/anthropicClient.ts"],
|
||||
"@aiGateway": ["./utils/aiGatewayClient.ts"],
|
||||
"@components/*": ["./components/*"],
|
||||
"@utils/*": ["./utils/*"],
|
||||
"@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 { Consumer, consumerSchema } from './types';
|
||||
import { Tool } from './tool';
|
||||
import { BaseTool, makeRequest } from '@anthropic';
|
||||
import { BaseTool, makeRequest } from '@utils/aiGatewayClient';
|
||||
import { generatePrompt } from './prompt';
|
||||
import { generateConsumerSeed } from '@utils/generateConsumerSeed';
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ const coreSchema = z.object({
|
||||
});
|
||||
|
||||
const routinesSchema = z.object({
|
||||
weekday: z.record(weekdayActivitySchema),
|
||||
weekday: z.record(z.string(), weekdayActivitySchema),
|
||||
weekend: z.array(z.string()),
|
||||
commute: z.object({
|
||||
method: z.string(),
|
||||
@@ -111,7 +111,7 @@ const financesSchema = z.object({
|
||||
subscriptions: z.array(subscriptionSchema),
|
||||
spending_patterns: z.object({
|
||||
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 { Tool } from './tool';
|
||||
import { BaseTool, makeRequest } from '@anthropic';
|
||||
import { BaseTool, makeRequest } from '@utils/aiGatewayClient';
|
||||
import { generatePrompt } from './prompt';
|
||||
import { Consumer } from '@consumer/types';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user