diff --git a/prisma/migrations/20260224210123_added_agent_chat_log/migration.sql b/prisma/migrations/20260224210123_added_agent_chat_log/migration.sql new file mode 100644 index 000000000..c2ec9af7f --- /dev/null +++ b/prisma/migrations/20260224210123_added_agent_chat_log/migration.sql @@ -0,0 +1,26 @@ +-- CreateTable +CREATE TABLE "AgentChatLog" ( + "id" TEXT NOT NULL, + "requestId" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "latencyMs" INTEGER NOT NULL, + "totalSteps" INTEGER NOT NULL, + "toolsUsed" TEXT[] DEFAULT ARRAY[]::TEXT[], + "promptTokens" INTEGER NOT NULL DEFAULT 0, + "completionTokens" INTEGER NOT NULL DEFAULT 0, + "totalTokens" INTEGER NOT NULL DEFAULT 0, + "errorOccurred" BOOLEAN NOT NULL DEFAULT false, + "errorMessage" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "AgentChatLog_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "AgentChatLog_requestId_key" ON "AgentChatLog"("requestId"); + +-- CreateIndex +CREATE INDEX "AgentChatLog_userId_idx" ON "AgentChatLog"("userId"); + +-- CreateIndex +CREATE INDEX "AgentChatLog_createdAt_idx" ON "AgentChatLog"("createdAt"); diff --git a/prisma/migrations/20260226171642_add_agent_feedback_and_verification/migration.sql b/prisma/migrations/20260226171642_add_agent_feedback_and_verification/migration.sql new file mode 100644 index 000000000..b234b5354 --- /dev/null +++ b/prisma/migrations/20260226171642_add_agent_feedback_and_verification/migration.sql @@ -0,0 +1,24 @@ +-- AlterTable +ALTER TABLE "AgentChatLog" ADD COLUMN "verificationResult" JSONB, +ADD COLUMN "verificationScore" DOUBLE PRECISION; + +-- CreateTable +CREATE TABLE "AgentFeedback" ( + "id" TEXT NOT NULL, + "requestId" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "rating" INTEGER NOT NULL, + "comment" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "AgentFeedback_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "AgentFeedback_createdAt_idx" ON "AgentFeedback"("createdAt"); + +-- CreateIndex +CREATE UNIQUE INDEX "AgentFeedback_requestId_userId_key" ON "AgentFeedback"("requestId", "userId"); + +-- AddForeignKey +ALTER TABLE "AgentFeedback" ADD CONSTRAINT "AgentFeedback_requestId_fkey" FOREIGN KEY ("requestId") REFERENCES "AgentChatLog"("requestId") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/20260228180000_fix_non_uuid_ids_and_add_model_id/migration.sql b/prisma/migrations/20260228180000_fix_non_uuid_ids_and_add_model_id/migration.sql new file mode 100644 index 000000000..8e5282c0b --- /dev/null +++ b/prisma/migrations/20260228180000_fix_non_uuid_ids_and_add_model_id/migration.sql @@ -0,0 +1,21 @@ +-- AddColumn: modelId to AgentChatLog +ALTER TABLE "AgentChatLog" ADD COLUMN "modelId" TEXT; + +-- ============================================================ +-- Fix non-UUID SymbolProfile IDs (sp-* → proper UUIDs) +-- All FK constraints use ON UPDATE CASCADE, so Order.symbolProfileId +-- and other references update automatically. +-- ============================================================ +UPDATE "SymbolProfile" SET "id" = 'd8f0b8c3-212c-48ef-a837-fff75ef98176' WHERE "id" = 'sp-aapl'; +UPDATE "SymbolProfile" SET "id" = '5bb696ab-aaf3-4924-a0e4-79c69bfcd81b' WHERE "id" = 'sp-msft'; +UPDATE "SymbolProfile" SET "id" = '7df6544c-c592-459c-af69-aafe65db60c9' WHERE "id" = 'sp-voo'; +UPDATE "SymbolProfile" SET "id" = 'ba75d50e-34f6-4c9e-bbb7-71b43b7cbfc0' WHERE "id" = 'sp-googl'; +UPDATE "SymbolProfile" SET "id" = '8b846370-2e16-4594-9785-a94da15d60a1' WHERE "id" = 'sp-btc'; + +-- ============================================================ +-- Delete test user with non-RFC4122 UUID (aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee) +-- All FK constraints use ON DELETE CASCADE, so their orders (ord-*), +-- account (11111111-2222-3333-4444-555555555555), etc. are cleaned up automatically. +-- The seed will recreate the demo user with proper UUIDs on next run. +-- ============================================================ +DELETE FROM "User" WHERE "id" = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 232dde9ca..8453c48d2 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -338,6 +338,41 @@ enum Provider { OIDC } +model AgentChatLog { + id String @id @default(uuid()) + requestId String @unique + userId String + latencyMs Int + totalSteps Int + toolsUsed String[] @default([]) + promptTokens Int @default(0) + completionTokens Int @default(0) + totalTokens Int @default(0) + modelId String? + errorOccurred Boolean @default(false) + errorMessage String? + verificationScore Float? + verificationResult Json? + createdAt DateTime @default(now()) + feedback AgentFeedback[] + + @@index([userId]) + @@index([createdAt]) +} + +model AgentFeedback { + id String @id @default(uuid()) + requestId String + userId String + rating Int + comment String? + createdAt DateTime @default(now()) + chatLog AgentChatLog @relation(fields: [requestId], references: [requestId]) + + @@unique([requestId, userId]) + @@index([createdAt]) +} + enum Role { ADMIN DEMO diff --git a/prisma/seed.mts b/prisma/seed.mts index 18389aab1..862df2269 100644 --- a/prisma/seed.mts +++ b/prisma/seed.mts @@ -1,14 +1,20 @@ import { PrismaClient } from '@prisma/client'; +import { createHmac } from 'node:crypto'; const prisma = new PrismaClient(); +const ACCESS_TOKEN_SALT = + process.env.ACCESS_TOKEN_SALT ?? 'agentforge-dev-salt-2026'; + +function hashToken(plain: string): string { + return createHmac('sha512', ACCESS_TOKEN_SALT).update(plain).digest('hex'); +} + async function main() { + // Tags — always seeded (upstream default) await prisma.tag.createMany({ data: [ - { - id: '4452656d-9fa4-4bd0-ba38-70492e31d180', - name: 'EMERGENCY_FUND' - }, + { id: '4452656d-9fa4-4bd0-ba38-70492e31d180', name: 'EMERGENCY_FUND' }, { id: 'f2e868af-8333-459f-b161-cbc6544c24bd', name: 'EXCLUDE_FROM_ANALYSIS' @@ -16,6 +22,236 @@ async function main() { ], skipDuplicates: true }); + + // Demo portfolio data — opt-in via SEED_DEMO_DATA=true + if (process.env.SEED_DEMO_DATA === 'true') { + const DEMO_USER_ID = '403deeb8-edd5-4e64-99a9-3752782ad0a2'; + const DEMO_ACCOUNT_ID = 'c2a0c0f6-366b-4992-97ac-1adfa81d6cbc'; + const DEMO_ACCESS_TOKEN_PLAIN = 'demo-token-2026'; + + // Demo user + await prisma.user.upsert({ + where: { id: DEMO_USER_ID }, + update: {}, + create: { + id: DEMO_USER_ID, + accessToken: hashToken(DEMO_ACCESS_TOKEN_PLAIN), + provider: 'ANONYMOUS', + role: 'ADMIN' + } + }); + + // Demo account + await prisma.account.upsert({ + where: { + id_userId: { id: DEMO_ACCOUNT_ID, userId: DEMO_USER_ID } + }, + update: {}, + create: { + id: DEMO_ACCOUNT_ID, + userId: DEMO_USER_ID, + name: 'Main Brokerage', + balance: 5000, + currency: 'USD', + isExcluded: false + } + }); + + // Symbol profiles + const symbols = [ + { + id: 'd8f0b8c3-212c-48ef-a837-fff75ef98176', + symbol: 'AAPL', + name: 'Apple Inc.', + currency: 'USD', + dataSource: 'YAHOO' as const, + assetClass: 'EQUITY' as const, + assetSubClass: 'STOCK' as const + }, + { + id: '5bb696ab-aaf3-4924-a0e4-79c69bfcd81b', + symbol: 'MSFT', + name: 'Microsoft Corporation', + currency: 'USD', + dataSource: 'YAHOO' as const, + assetClass: 'EQUITY' as const, + assetSubClass: 'STOCK' as const + }, + { + id: '7df6544c-c592-459c-af69-aafe65db60c9', + symbol: 'VOO', + name: 'Vanguard S&P 500 ETF', + currency: 'USD', + dataSource: 'YAHOO' as const, + assetClass: 'EQUITY' as const, + assetSubClass: 'ETF' as const + }, + { + id: 'ba75d50e-34f6-4c9e-bbb7-71b43b7cbfc0', + symbol: 'GOOGL', + name: 'Alphabet Inc.', + currency: 'USD', + dataSource: 'YAHOO' as const, + assetClass: 'EQUITY' as const, + assetSubClass: 'STOCK' as const + }, + { + id: '8b846370-2e16-4594-9785-a94da15d60a1', + symbol: 'bitcoin', + name: 'Bitcoin', + currency: 'USD', + dataSource: 'COINGECKO' as const, + assetClass: 'ALTERNATIVE_INVESTMENT' as const, + assetSubClass: 'CRYPTOCURRENCY' as const + } + ]; + + for (const sp of symbols) { + await prisma.symbolProfile.upsert({ + where: { + dataSource_symbol: { dataSource: sp.dataSource, symbol: sp.symbol } + }, + update: {}, + create: { + id: sp.id, + symbol: sp.symbol, + name: sp.name, + currency: sp.currency, + dataSource: sp.dataSource, + assetClass: sp.assetClass, + assetSubClass: sp.assetSubClass + } + }); + } + + // Resolve actual symbolProfile IDs (may differ if profiles pre-existed) + const profileLookup = new Map(); + for (const sp of symbols) { + const found = await prisma.symbolProfile.findUnique({ + where: { + dataSource_symbol: { dataSource: sp.dataSource, symbol: sp.symbol } + }, + select: { id: true } + }); + if (found) { + profileLookup.set(sp.id, found.id); + } + } + + // Orders + const orders = [ + { + id: '49f6cba4-7dd2-47e1-918e-4d8538e3818f', + seedProfileId: 'd8f0b8c3-212c-48ef-a837-fff75ef98176', + type: 'BUY' as const, + quantity: 15, + unitPrice: 178.5, + fee: 0, + date: new Date('2024-03-15'), + currency: 'USD' + }, + { + id: '7090411c-a046-4363-a89e-d61d28417820', + seedProfileId: '5bb696ab-aaf3-4924-a0e4-79c69bfcd81b', + type: 'BUY' as const, + quantity: 10, + unitPrice: 420.0, + fee: 0, + date: new Date('2024-04-01'), + currency: 'USD' + }, + { + id: '06cd0784-c5f4-42a0-b799-eb48b61b7afb', + seedProfileId: '7df6544c-c592-459c-af69-aafe65db60c9', + type: 'BUY' as const, + quantity: 20, + unitPrice: 480.0, + fee: 0, + date: new Date('2024-01-10'), + currency: 'USD' + }, + { + id: '151b2a27-f82f-4393-85b2-a57660fc2d25', + seedProfileId: 'ba75d50e-34f6-4c9e-bbb7-71b43b7cbfc0', + type: 'BUY' as const, + quantity: 8, + unitPrice: 155.0, + fee: 0, + date: new Date('2024-06-20'), + currency: 'USD' + }, + { + id: 'abc07d0a-3be2-4185-9ae4-d2b0fa4a5d48', + seedProfileId: '8b846370-2e16-4594-9785-a94da15d60a1', + type: 'BUY' as const, + quantity: 0.5, + unitPrice: 43000.0, + fee: 0, + date: new Date('2024-02-01'), + currency: 'USD' + }, + { + id: 'e3a1f7c2-8d94-4b61-a5e3-9c72d1f08e34', + seedProfileId: 'd8f0b8c3-212c-48ef-a837-fff75ef98176', + type: 'BUY' as const, + quantity: 5, + unitPrice: 195.0, + fee: 0, + date: new Date('2024-09-15'), + currency: 'USD' + }, + { + id: 'f7b2c8d1-6e45-4a93-b812-d3e9f0a17c56', + seedProfileId: '7df6544c-c592-459c-af69-aafe65db60c9', + type: 'DIVIDEND' as const, + quantity: 0, + unitPrice: 1.78, + fee: 0, + date: new Date('2024-12-20'), + currency: 'USD' + }, + { + id: 'a1c3e5f7-2b4d-4869-9a0c-e6f8d2b4a7c1', + seedProfileId: '5bb696ab-aaf3-4924-a0e4-79c69bfcd81b', + type: 'SELL' as const, + quantity: 3, + unitPrice: 450.0, + fee: 0, + date: new Date('2025-01-10'), + currency: 'USD' + } + ]; + + for (const ord of orders) { + const symbolProfileId = profileLookup.get(ord.seedProfileId); + if (!symbolProfileId) { + console.warn( + `Skipping order ${ord.id}: no symbolProfile found for seed ID ${ord.seedProfileId}` + ); + continue; + } + await prisma.order.upsert({ + where: { id: ord.id }, + update: { symbolProfileId }, + create: { + id: ord.id, + userId: DEMO_USER_ID, + accountId: DEMO_ACCOUNT_ID, + accountUserId: DEMO_USER_ID, + symbolProfileId, + type: ord.type, + quantity: ord.quantity, + unitPrice: ord.unitPrice, + fee: ord.fee, + date: ord.date, + currency: ord.currency + } + }); + } + + console.log('Seeded demo user, account, symbols, and orders.'); + console.log(` Login token: ${DEMO_ACCESS_TOKEN_PLAIN}`); + } } main()