From 30c67a308b630b4975dde2def91011ddd0816f36 Mon Sep 17 00:00:00 2001 From: Riccardo Senica Date: Mon, 23 Jun 2025 19:04:01 +0200 Subject: [PATCH] Data storage (#3) * feat: add prisma * fix: generate prisma --- .env.example | 1 + .env.test | 1 + package.json | 10 +++++-- prisma/schema.prisma | 23 ++++++++++++++++ utils/anthropicClient.ts | 22 ++++++++++++++-- utils/commands/anthropic.ts | 48 +++++++++++++++++++++++++++++----- utils/db.ts | 44 +++++++++++++++++++++++++++++++ yarn.lock | 52 ++++++++++++++++++++++++++++++++++++- 8 files changed, 189 insertions(+), 12 deletions(-) create mode 100644 prisma/schema.prisma create mode 100644 utils/db.ts diff --git a/.env.example b/.env.example index 73088ba..72ad97f 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,3 @@ +DATABASE_URL="" USER_KEY="" ANTHROPIC_API_KEY="" \ No newline at end of file diff --git a/.env.test b/.env.test index 2367cd6..0ab33b1 100644 --- a/.env.test +++ b/.env.test @@ -1,2 +1,3 @@ +DATABASE_URL="" USER_KEY="test-key-123" ANTHROPIC_API_KEY="" \ No newline at end of file diff --git a/package.json b/package.json index d0cde7a..8b73404 100644 --- a/package.json +++ b/package.json @@ -5,9 +5,10 @@ "author": "riccardo@frompixels.com", "scripts": { "dev": "next dev", - "build": "next build", + "build": "prisma generate && next build", "start": "next start", "lint": "next lint", + "lint:fix": "next lint --fix && prettier --config .prettierrc '**/*.{js,jsx,ts,tsx,json,css,scss,md}' --write", "format": "prettier --config .prettierrc '**/*.{js,jsx,ts,tsx,json,css,scss,md}' --write", "typecheck": "tsc --noEmit", "test": "jest", @@ -17,10 +18,14 @@ "analyze": "BUNDLE_ANALYZE=true next build", "audit": "audit-ci", "vercel:link": "vercel link", - "vercel:env": "vercel env pull .env" + "vercel:env": "vercel env pull .env", + "db:generate": "prisma generate", + "db:push": "prisma db push", + "db:migrate": "prisma migrate dev" }, "dependencies": { "@anthropic-ai/sdk": "^0.33.1", + "@prisma/client": "^5.22.0", "axios": "^1.8.2", "next": "^15.2.4", "react": "^19.0.0", @@ -45,6 +50,7 @@ "jest": "^29.7.0", "lint-staged": "^15.3.0", "prettier": "^3.4.2", + "prisma": "^5.22.0", "ts-jest": "^29.2.5", "ts-node": "^10.9.2", "typescript": "^5.7.2" diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..00c5c48 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,23 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model AnthropicQuery { + id String @id @default(cuid()) + question String @db.Text + response String @db.Text + success Boolean + errorMessage String? @db.Text + tokensUsed Int? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@map("anthropic_queries") + @@index([createdAt]) + @@index([success]) +} \ No newline at end of file diff --git a/utils/anthropicClient.ts b/utils/anthropicClient.ts index ca7eed6..e52c4fc 100644 --- a/utils/anthropicClient.ts +++ b/utils/anthropicClient.ts @@ -1,6 +1,11 @@ import Anthropic from '@anthropic-ai/sdk'; -export async function getMessage(text: string) { +interface AnthropicResponse { + text: string; + tokensUsed: number; +} + +export async function getMessage(text: string): Promise { const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY }); @@ -18,7 +23,20 @@ export async function getMessage(text: string) { try { const data = response.content as [{ type: string; text: string }]; - return data[0].text; + const tokensUsed = + (response.usage?.input_tokens || 0) + + (response.usage?.output_tokens || 0); + + console.info('Token usage:', { + input_tokens: response.usage?.input_tokens, + output_tokens: response.usage?.output_tokens, + total: tokensUsed + }); + + return { + text: data[0].text, + tokensUsed + }; } catch (error) { throw new Error(`Anthropic client error: ${JSON.stringify(error)}.`); } diff --git a/utils/commands/anthropic.ts b/utils/commands/anthropic.ts index 6bd1f20..466a5d4 100644 --- a/utils/commands/anthropic.ts +++ b/utils/commands/anthropic.ts @@ -1,32 +1,66 @@ import { getMessage } from '@utils/anthropicClient'; import { ShortcutsResponse } from '../types'; +import { dbOperations } from '@utils/db'; export async function anthropicCommand( parameters: Record | undefined ): Promise { + let question = ''; + let response = ''; + let success = false; + let errorMessage: string | undefined; + let tokensUsed: number | undefined; + try { if (!parameters || !parameters['question']) { + errorMessage = 'Need to provide a question.'; return { success: false, message: 'Sorry. Need to provide a question.' }; } - const response = await getMessage( + question = parameters['question']; + const prompt = 'I want to know ' + - parameters['question'] + - '. Structure the response in a manner suitable for spoken communication.' - ); + question + + '. Structure the response in a manner suitable for spoken communication.'; + + const anthropicResponse = await getMessage(prompt); + response = anthropicResponse.text; + tokensUsed = anthropicResponse.tokensUsed; + success = true; return { success: true, - message: response + message: response, + data: { + tokensUsed + } }; } catch (error) { - console.error(error); + console.error('Anthropic command error:', error); + success = false; + errorMessage = error instanceof Error ? error.message : 'Unknown error'; + response = 'Sorry. There was a problem with Anthropic.'; + return { success: false, - message: 'Sorry. There was a problem with Anthropic.' + message: response }; + } finally { + if (question) { + try { + await dbOperations.saveAnthropicQuery({ + question, + response, + success, + errorMessage, + tokensUsed + }); + } catch (error) { + console.error('Failed to log query to database:', error); + } + } } } diff --git a/utils/db.ts b/utils/db.ts new file mode 100644 index 0000000..721afda --- /dev/null +++ b/utils/db.ts @@ -0,0 +1,44 @@ +import { PrismaClient } from '@prisma/client'; + +declare global { + var prisma: PrismaClient | undefined; +} + +const db = global.prisma || new PrismaClient(); + +if (process.env.NODE_ENV === 'development') { + global.prisma = db; +} + +export const dbOperations = { + async saveAnthropicQuery({ + question, + response, + success, + errorMessage, + tokensUsed + }: { + question: string; + response: string; + success: boolean; + errorMessage?: string; + tokensUsed?: number; + }) { + try { + return await db.anthropicQuery.create({ + data: { + question, + response, + success, + errorMessage, + tokensUsed + } + }); + } catch (error) { + console.error('Failed to save query:', error); + return null; + } + } +}; + +export { db }; diff --git a/yarn.lock b/yarn.lock index 4a111ee..1efa059 100644 --- a/yarn.lock +++ b/yarn.lock @@ -945,6 +945,47 @@ resolved "https://registry.yarnpkg.com/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz#3dc35ba0f1e66b403c00b39344f870298ebb1c8e" integrity sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA== +"@prisma/client@^5.22.0": + version "5.22.0" + resolved "https://registry.yarnpkg.com/@prisma/client/-/client-5.22.0.tgz#da1ca9c133fbefe89e0da781c75e1c59da5f8802" + integrity sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA== + +"@prisma/debug@5.22.0": + version "5.22.0" + resolved "https://registry.yarnpkg.com/@prisma/debug/-/debug-5.22.0.tgz#58af56ed7f6f313df9fb1042b6224d3174bbf412" + integrity sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ== + +"@prisma/engines-version@5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2": + version "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2" + resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz#d534dd7235c1ba5a23bacd5b92cc0ca3894c28f4" + integrity sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ== + +"@prisma/engines@5.22.0": + version "5.22.0" + resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-5.22.0.tgz#28f3f52a2812c990a8b66eb93a0987816a5b6d84" + integrity sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA== + dependencies: + "@prisma/debug" "5.22.0" + "@prisma/engines-version" "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2" + "@prisma/fetch-engine" "5.22.0" + "@prisma/get-platform" "5.22.0" + +"@prisma/fetch-engine@5.22.0": + version "5.22.0" + resolved "https://registry.yarnpkg.com/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz#4fb691b483a450c5548aac2f837b267dd50ef52e" + integrity sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA== + dependencies: + "@prisma/debug" "5.22.0" + "@prisma/engines-version" "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2" + "@prisma/get-platform" "5.22.0" + +"@prisma/get-platform@5.22.0": + version "5.22.0" + resolved "https://registry.yarnpkg.com/@prisma/get-platform/-/get-platform-5.22.0.tgz#fc675bc9d12614ca2dade0506c9c4a77e7dddacd" + integrity sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q== + dependencies: + "@prisma/debug" "5.22.0" + "@rtsao/scc@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@rtsao/scc/-/scc-1.1.0.tgz#927dd2fae9bc3361403ac2c7a00c32ddce9ad7e8" @@ -2648,7 +2689,7 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== -fsevents@^2.3.2: +fsevents@2.3.3, fsevents@^2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== @@ -4322,6 +4363,15 @@ pretty-format@^29.0.0, pretty-format@^29.7.0: ansi-styles "^5.0.0" react-is "^18.0.0" +prisma@^5.22.0: + version "5.22.0" + resolved "https://registry.yarnpkg.com/prisma/-/prisma-5.22.0.tgz#1f6717ff487cdef5f5799cc1010459920e2e6197" + integrity sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A== + dependencies: + "@prisma/engines" "5.22.0" + optionalDependencies: + fsevents "2.3.3" + prompts@^2.0.1: version "2.4.2" resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069"