Browse Source

feat(db): add AgentChatLog and AgentFeedback schema with migrations

Add 3 additive Prisma migrations for agent chat logging,
feedback collection, and model ID tracking.
Wrap demo portfolio seed data behind SEED_DEMO_DATA env guard.
pull/6458/head
Ryan Waits 1 month ago
parent
commit
5c34cdbed1
  1. 26
      prisma/migrations/20260224210123_added_agent_chat_log/migration.sql
  2. 24
      prisma/migrations/20260226171642_add_agent_feedback_and_verification/migration.sql
  3. 21
      prisma/migrations/20260228180000_fix_non_uuid_ids_and_add_model_id/migration.sql
  4. 35
      prisma/schema.prisma
  5. 244
      prisma/seed.mts

26
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");

24
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;

21
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';

35
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

244
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<string, string>();
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()

Loading…
Cancel
Save