From 7637b94ea77f79d7907c35df74d3a70360f8b701 Mon Sep 17 00:00:00 2001 From: Kyle Corbitt Date: Sat, 5 Aug 2023 13:49:03 -0700 Subject: [PATCH 01/49] schema changes --- app/prisma/schema.prisma | 60 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/app/prisma/schema.prisma b/app/prisma/schema.prisma index 6b8812f..32ce994 100644 --- a/app/prisma/schema.prisma +++ b/app/prisma/schema.prisma @@ -210,6 +210,8 @@ model Organization { organizationUsers OrganizationUser[] experiments Experiment[] datasets Dataset[] + loggedCalls LoggedCall[] + apiKeys ApiKey[] } enum OrganizationUserRole { @@ -249,6 +251,64 @@ model WorldChampEntrant { @@unique([userId]) } +model LoggedCall { + id String @id @default(uuid()) @db.Uuid + + startTime DateTime + endTime DateTime + + reqPayload Json + respPayload Json? + respStatus Int? + error String? + + durationMs Int? + inputTokens Int? + outputTokens Int? + finishReason String? + completionId String? + totalCost Decimal? @db.Decimal(18, 12) + + cacheParentId String? @db.Uuid + cacheParent LoggedCall? @relation(name: "CacheParentChild", fields: [cacheParentId], references: [id], onDelete: Cascade) + + organizationId String @db.Uuid + organization Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade) + + tags LoggedCallTag[] + cacheChildren LoggedCall[] @relation(name: "CacheParentChild") + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([startTime]) +} + +model LoggedCallTag { + id String @id @default(cuid()) + name String + value String? + + loggedCallId String + loggedCall LoggedCall @relation(fields: [loggedCallId], references: [id], onDelete: Cascade) + + @@index([name]) + @@index([name, value]) +} + +model ApiKey { + id String @id @default(uuid()) @db.Uuid + + name String + apiKey String @unique + + organizationId String @db.Uuid + organization Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + model Account { id String @id @default(uuid()) @db.Uuid userId String @db.Uuid From 9e859c199e9e2d1ffb1869e4c08b2dd26e84b355 Mon Sep 17 00:00:00 2001 From: Kyle Corbitt Date: Sat, 5 Aug 2023 17:56:51 -0700 Subject: [PATCH 02/49] two LoggedCall tables --- app/prisma/schema.prisma | 65 ++++++++++++++++++++------ app/src/server/scripts/test-queries.ts | 63 +++++++++++++++++++++++++ 2 files changed, 113 insertions(+), 15 deletions(-) create mode 100644 app/src/server/scripts/test-queries.ts diff --git a/app/prisma/schema.prisma b/app/prisma/schema.prisma index 32ce994..9c5c92e 100644 --- a/app/prisma/schema.prisma +++ b/app/prisma/schema.prisma @@ -254,14 +254,53 @@ model WorldChampEntrant { model LoggedCall { id String @id @default(uuid()) @db.Uuid + startTime DateTime + + // True if this call was served from the cache, false otherwise + cacheHit Boolean + + // A LoggedCall is always associated with a LoggedCallModelResponse. If this + // is a cache miss, it's a new LoggedCallModelResponse we created for this. + // If it's a cache hit, it's the existing LoggedCallModelResponse we served. + modelResponseId String @db.Uuid + modelResponse LoggedCallModelResponse @relation(fields: [modelResponseId], references: [id], onDelete: Cascade) + + // The response created by this LoggedCall. Will be null if this LoggedCall is a cache hit. + createdResponse LoggedCallModelResponse[] @relation(name: "ModelResponseCreatedBy") + + organizationId String @db.Uuid + organization Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade) + + tags LoggedCallTag[] + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([startTime]) +} + +model LoggedCallModelResponse { + id String @id @default(uuid()) @db.Uuid + + reqPayload Json + + // The HTTP status returned by the model provider + respStatus Int? + respPayload Json? + + // Should be null if the request was successful, and some string if the request failed. + error String? + startTime DateTime endTime DateTime - reqPayload Json - respPayload Json? - respStatus Int? - error String? + // Note: the function to calculate the cacheKey should include the project + // ID so we don't share cached responses between projects, which could be an + // attack vector. Also, we should only set the cacheKey on the model if the + // request was successful. + cacheKey String? + // Derived fields durationMs Int? inputTokens Int? outputTokens Int? @@ -269,19 +308,15 @@ model LoggedCall { completionId String? totalCost Decimal? @db.Decimal(18, 12) - cacheParentId String? @db.Uuid - cacheParent LoggedCall? @relation(name: "CacheParentChild", fields: [cacheParentId], references: [id], onDelete: Cascade) + // The LoggedCall that created this LoggedCallModelResponse + createdById String @unique @db.Uuid + createdBy LoggedCall @relation(name: "ModelResponseCreatedBy", fields: [createdById], references: [id], onDelete: Cascade) - organizationId String @db.Uuid - organization Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + loggedCalls LoggedCall[] - tags LoggedCallTag[] - cacheChildren LoggedCall[] @relation(name: "CacheParentChild") - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@index([startTime]) + @@index([cacheKey]) } model LoggedCallTag { diff --git a/app/src/server/scripts/test-queries.ts b/app/src/server/scripts/test-queries.ts new file mode 100644 index 0000000..e69d230 --- /dev/null +++ b/app/src/server/scripts/test-queries.ts @@ -0,0 +1,63 @@ +import dayjs from "dayjs"; +import { prisma } from "../db"; + +const projectId = "1234"; + +// Find all calls in the last 24 hours +const responses = await prisma.loggedCall.findMany({ + where: { + organizationId: projectId, + startTime: { + gt: dayjs() + .subtract(24 * 3600) + .toDate(), + }, + }, + include: { + modelResponse: true, + }, + orderBy: { + startTime: "desc", + }, +}); + +// Find all calls in the last 24 hours with promptId 'hello-world' +const helloWorld = await prisma.loggedCall.findMany({ + where: { + organizationId: projectId, + startTime: { + gt: dayjs() + .subtract(24 * 3600) + .toDate(), + }, + tags: { + some: { + name: "promptId", + value: "hello-world", + }, + }, + }, + include: { + modelResponse: true, + }, + orderBy: { + startTime: "desc", + }, +}); + +// Total spent on OpenAI in the last month +const totalSpent = await prisma.loggedCallModelResponse.aggregate({ + _sum: { + totalCost: true, + }, + where: { + createdBy: { + organizationId: projectId, + }, + startTime: { + gt: dayjs() + .subtract(30 * 24 * 3600) + .toDate(), + }, + }, +}); From 7f8b574c9f1cdc23fafeb7f7766b61188a4f558d Mon Sep 17 00:00:00 2001 From: David Corbitt Date: Sat, 5 Aug 2023 20:37:16 -0700 Subject: [PATCH 03/49] Add checkCache and report routes --- app/@types/nextjs-routes.d.ts | 2 + app/package.json | 2 + app/pnpm-lock.yaml | 117 ++++++++++- .../migration.sql | 90 ++++++++ app/prisma/schema.prisma | 14 +- app/src/pages/api/[...trpc].ts | 22 ++ app/src/pages/api/openapi.json.ts | 16 ++ .../server/api/routers/externalApi.router.ts | 197 ++++++++++++++++++ app/src/server/api/trpc.ts | 35 ++-- app/src/server/tasks/queryModel.task.ts | 6 +- app/src/server/utils/generateNewCell.ts | 4 +- .../utils/{hashPrompt.ts => hashObject.ts} | 13 +- 12 files changed, 489 insertions(+), 29 deletions(-) create mode 100644 app/prisma/migrations/20230806024615_add_logged_calls_and_api_keys/migration.sql create mode 100644 app/src/pages/api/[...trpc].ts create mode 100644 app/src/pages/api/openapi.json.ts create mode 100644 app/src/server/api/routers/externalApi.router.ts rename app/src/server/utils/{hashPrompt.ts => hashObject.ts} (75%) diff --git a/app/@types/nextjs-routes.d.ts b/app/@types/nextjs-routes.d.ts index 63efc55..32c34bd 100644 --- a/app/@types/nextjs-routes.d.ts +++ b/app/@types/nextjs-routes.d.ts @@ -12,8 +12,10 @@ declare module "nextjs-routes" { export type Route = | StaticRoute<"/account/signin"> + | DynamicRoute<"/api/[...trpc]", { "trpc": string[] }> | DynamicRoute<"/api/auth/[...nextauth]", { "nextauth": string[] }> | StaticRoute<"/api/experiments/og-image"> + | StaticRoute<"/api/openapi"> | StaticRoute<"/api/sentry-example-api"> | DynamicRoute<"/api/trpc/[trpc]", { "trpc": string }> | DynamicRoute<"/data/[id]", { "id": string }> diff --git a/app/package.json b/app/package.json index 1855f60..3a0dcaa 100644 --- a/app/package.json +++ b/app/package.json @@ -66,6 +66,7 @@ "next": "^13.4.2", "next-auth": "^4.22.1", "next-query-params": "^4.2.3", + "nextjs-cors": "^2.1.2", "nextjs-routes": "^2.0.1", "openai": "4.0.0-beta.7", "pluralize": "^8.0.0", @@ -87,6 +88,7 @@ "socket.io": "^4.7.1", "socket.io-client": "^4.7.1", "superjson": "1.12.2", + "trpc-openapi": "^1.2.0", "tsx": "^3.12.7", "type-fest": "^4.0.0", "use-query-params": "^2.2.1", diff --git a/app/pnpm-lock.yaml b/app/pnpm-lock.yaml index 91855f5..0575dfe 100644 --- a/app/pnpm-lock.yaml +++ b/app/pnpm-lock.yaml @@ -1,4 +1,4 @@ -lockfileVersion: '6.1' +lockfileVersion: '6.0' settings: autoInstallPeers: true @@ -137,6 +137,9 @@ dependencies: next-query-params: specifier: ^4.2.3 version: 4.2.3(next@13.4.2)(react@18.2.0)(use-query-params@2.2.1) + nextjs-cors: + specifier: ^2.1.2 + version: 2.1.2(next@13.4.2) nextjs-routes: specifier: ^2.0.1 version: 2.0.1(next@13.4.2) @@ -200,6 +203,9 @@ dependencies: superjson: specifier: 1.12.2 version: 1.12.2 + trpc-openapi: + specifier: ^1.2.0 + version: 1.2.0(@trpc/server@10.26.0)(zod@3.21.4) tsx: specifier: ^3.12.7 version: 3.12.7 @@ -4144,6 +4150,15 @@ packages: wrap-ansi: 7.0.0 dev: false + /co-body@6.1.0: + resolution: {integrity: sha512-m7pOT6CdLN7FuXUcpuz/8lfQ/L77x8SchHCF4G0RBTJO20Wzmhn5Sp4/5WsKy8OSpifBSUrmg83qEqaDHdyFuQ==} + dependencies: + inflation: 2.0.0 + qs: 6.11.0 + raw-body: 2.5.1 + type-is: 1.6.18 + dev: false + /color-convert@1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} dependencies: @@ -4235,6 +4250,10 @@ packages: /convert-source-map@1.9.0: resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} + /cookie-es@1.0.0: + resolution: {integrity: sha512-mWYvfOLrfEc996hlKcdABeIiPHUPC6DM2QYZdGGOvhOTbA3tjm2eBwqlJpoFdjC89NI4Qt6h0Pu06Mp+1Pj5OQ==} + dev: false + /cookie-signature@1.0.6: resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} dev: false @@ -4462,11 +4481,20 @@ packages: has-property-descriptors: 1.0.0 object-keys: 1.1.1 + /defu@6.1.2: + resolution: {integrity: sha512-+uO4+qr7msjNNWKYPHqN/3+Dx3NFkmIzayk2L1MyZQlvgZb/J1A0fo410dpKrN2SnqFjt8n4JL8fDJE0wIgjFQ==} + dev: false + /delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} dev: false + /depd@1.1.2: + resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==} + engines: {node: '>= 0.6'} + dev: false + /depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -4477,6 +4505,10 @@ packages: engines: {node: '>=6'} dev: true + /destr@2.0.1: + resolution: {integrity: sha512-M1Ob1zPSIvlARiJUkKqvAZ3VAqQY6Jcuth/pBKQ2b1dX/Qx0OnJ8Vux6J2H5PTMQeRzWrrbTu70VxBfv/OPDJA==} + dev: false + /destroy@1.2.0: resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} @@ -5636,6 +5668,18 @@ packages: - pg-native dev: false + /h3@1.7.1: + resolution: {integrity: sha512-A9V2NEDNHet7v1gCg7CMwerSigLi0SRbhTy7C3lGb0N4YKIpPmLDjedTUopqp4dnn7COHfqUjjaz3zbtz4QduA==} + dependencies: + cookie-es: 1.0.0 + defu: 6.1.2 + destr: 2.0.1 + iron-webcrypto: 0.7.1 + radix3: 1.0.1 + ufo: 1.1.2 + uncrypto: 0.1.3 + dev: false + /has-bigints@1.0.2: resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} dev: true @@ -5782,6 +5826,11 @@ packages: engines: {node: '>=0.8.19'} dev: true + /inflation@2.0.0: + resolution: {integrity: sha512-m3xv4hJYR2oXw4o4Y5l6P5P16WYmazYof+el6Al3f+YlggGj6qT9kImBAnzDelRALnP5d3h4jGBPKzYCizjZZw==} + engines: {node: '>= 0.8.0'} + dev: false + /inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} dependencies: @@ -5811,6 +5860,10 @@ packages: engines: {node: '>= 0.10'} dev: false + /iron-webcrypto@0.7.1: + resolution: {integrity: sha512-K/UmlEhPCPXEHV5hAtH5C0tI5JnFuOrv4yO/j7ODPl3HaiiHBLbOLTde+ieUaAyfCATe4LoAnclyF+hmSCOVmQ==} + dev: false + /is-alphabetical@1.0.4: resolution: {integrity: sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==} dev: false @@ -6568,6 +6621,15 @@ packages: - babel-plugin-macros dev: false + /nextjs-cors@2.1.2(next@13.4.2): + resolution: {integrity: sha512-2yOVivaaf2ILe4f/qY32hnj3oC77VCOsUQJQfhVMGsXE/YMEWUY2zy78sH9FKUCM7eG42/l3pDofIzMD781XGA==} + peerDependencies: + next: ^8.1.1-canary.54 || ^9.0.0 || ^10.0.0-0 || ^11.0.0 || ^12.0.0 || ^13.0.0 + dependencies: + cors: 2.8.5 + next: 13.4.2(@babel/core@7.22.9)(react-dom@18.2.0)(react@18.2.0) + dev: false + /nextjs-routes@2.0.1(next@13.4.2): resolution: {integrity: sha512-pBGRm6uR44zwUjWWYn6+gwz08BhBbqUYlIzsbNHAh1TWohHYKWFaa2YVsj8BxEo726MZYg87OJPnHpaaY1ia0w==} hasBin: true @@ -6595,6 +6657,22 @@ packages: whatwg-url: 5.0.0 dev: false + /node-mocks-http@1.12.2: + resolution: {integrity: sha512-xhWwC0dh35R9rf0j3bRZXuISXdHxxtMx0ywZQBwjrg3yl7KpRETzogfeCamUIjltpn0Fxvs/ZhGJul1vPLrdJQ==} + engines: {node: '>=0.6'} + dependencies: + accepts: 1.3.8 + content-disposition: 0.5.4 + depd: 1.1.2 + fresh: 0.5.2 + merge-descriptors: 1.0.1 + methods: 1.1.2 + mime: 1.6.0 + parseurl: 1.3.3 + range-parser: 1.2.1 + type-is: 1.6.18 + dev: false + /node-releases@2.0.13: resolution: {integrity: sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==} @@ -6754,6 +6832,10 @@ packages: - supports-color dev: false + /openapi-types@12.1.3: + resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==} + dev: false + /openapi-typescript@5.4.1: resolution: {integrity: sha512-AGB2QiZPz4rE7zIwV3dRHtoUC/CWHhUjuzGXvtmMQN2AFV8xCTLKcZUHLcdPQmt/83i22nRE7+TxXOXkK+gf4Q==} engines: {node: '>= 14.0.0'} @@ -7203,6 +7285,10 @@ packages: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} dev: true + /radix3@1.0.1: + resolution: {integrity: sha512-y+AcwZ3HcUIGc9zGsNVf5+BY/LxL+z+4h4J3/pp8jxSmy1STaCocPS3qrj4tA5ehUSzqtqK+0Aygvz/r/8vy4g==} + dev: false + /randombytes@2.1.0: resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} dependencies: @@ -8180,6 +8266,22 @@ packages: hasBin: true dev: false + /trpc-openapi@1.2.0(@trpc/server@10.26.0)(zod@3.21.4): + resolution: {integrity: sha512-pfYoCd/3KYXWXvUPZBKJw455OOwngKN/6SIcj7Yit19OMLJ+8yVZkEvGEeg5wUSwfsiTdRsKuvqkRPXVSwV7ew==} + peerDependencies: + '@trpc/server': ^10.0.0 + zod: ^3.14.4 + dependencies: + '@trpc/server': 10.26.0 + co-body: 6.1.0 + h3: 1.7.1 + lodash.clonedeep: 4.5.0 + node-mocks-http: 1.12.2 + openapi-types: 12.1.3 + zod: 3.21.4 + zod-to-json-schema: 3.21.4(zod@3.21.4) + dev: false + /tsconfck@2.1.2(typescript@5.0.4): resolution: {integrity: sha512-ghqN1b0puy3MhhviwO2kGF8SeMDNhEbnKxjK7h6+fvY9JAxqvXi8y5NAHSQv687OVboS2uZIByzGd45/YxrRHg==} engines: {node: ^14.13.1 || ^16 || >=18} @@ -8322,7 +8424,6 @@ packages: /ufo@1.1.2: resolution: {integrity: sha512-TrY6DsjTQQgyS3E3dBaOXf0TpPD8u9FVrVYmKVegJuFw51n/YB9XPt+U6ydzFG5ZIN7+DIjPbNmXoBj9esYhgQ==} - dev: true /unbox-primitive@1.0.2: resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} @@ -8333,6 +8434,10 @@ packages: which-boxed-primitive: 1.0.2 dev: true + /uncrypto@0.1.3: + resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} + dev: false + /undici@5.22.1: resolution: {integrity: sha512-Ji2IJhFXZY0x/0tVBXeQwgPlLWw13GVzpsWPQ3rV50IFMMof2I55PZZxtm4P6iNq+L5znYN9nSTAq0ZyE6lSJw==} engines: {node: '>=14.0'} @@ -8861,6 +8966,14 @@ packages: resolution: {integrity: sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA==} dev: false + /zod-to-json-schema@3.21.4(zod@3.21.4): + resolution: {integrity: sha512-fjUZh4nQ1s6HMccgIeE0VP4QG/YRGPmyjO9sAh890aQKPEk3nqbfUXhMFaC+Dr5KvYBm8BCyvfpZf2jY9aGSsw==} + peerDependencies: + zod: ^3.21.4 + dependencies: + zod: 3.21.4 + dev: false + /zod@3.21.4: resolution: {integrity: sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==} dev: false diff --git a/app/prisma/migrations/20230806024615_add_logged_calls_and_api_keys/migration.sql b/app/prisma/migrations/20230806024615_add_logged_calls_and_api_keys/migration.sql new file mode 100644 index 0000000..4611bab --- /dev/null +++ b/app/prisma/migrations/20230806024615_add_logged_calls_and_api_keys/migration.sql @@ -0,0 +1,90 @@ +-- CreateTable +CREATE TABLE "LoggedCall" ( + "id" UUID NOT NULL, + "startTime" TIMESTAMP(3) NOT NULL, + "cacheHit" BOOLEAN NOT NULL, + "modelResponseId" UUID NOT NULL, + "organizationId" UUID NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "LoggedCall_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "LoggedCallModelResponse" ( + "id" UUID NOT NULL, + "reqPayload" JSONB NOT NULL, + "respStatus" INTEGER, + "respPayload" JSONB, + "error" TEXT, + "startTime" TIMESTAMP(3) NOT NULL, + "endTime" TIMESTAMP(3) NOT NULL, + "cacheKey" TEXT, + "durationMs" INTEGER, + "inputTokens" INTEGER, + "outputTokens" INTEGER, + "finishReason" TEXT, + "completionId" TEXT, + "totalCost" DECIMAL(18,12), + "originalLoggedCallId" UUID NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "LoggedCallModelResponse_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "LoggedCallTag" ( + "id" UUID NOT NULL, + "name" TEXT NOT NULL, + "value" TEXT, + "loggedCallId" UUID NOT NULL, + + CONSTRAINT "LoggedCallTag_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ApiKey" ( + "id" UUID NOT NULL, + "name" TEXT NOT NULL, + "apiKey" TEXT NOT NULL, + "organizationId" UUID NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "ApiKey_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "LoggedCall_startTime_idx" ON "LoggedCall"("startTime"); + +-- CreateIndex +CREATE UNIQUE INDEX "LoggedCallModelResponse_originalLoggedCallId_key" ON "LoggedCallModelResponse"("originalLoggedCallId"); + +-- CreateIndex +CREATE INDEX "LoggedCallModelResponse_cacheKey_idx" ON "LoggedCallModelResponse"("cacheKey"); + +-- CreateIndex +CREATE INDEX "LoggedCallTag_name_idx" ON "LoggedCallTag"("name"); + +-- CreateIndex +CREATE INDEX "LoggedCallTag_name_value_idx" ON "LoggedCallTag"("name", "value"); + +-- CreateIndex +CREATE UNIQUE INDEX "ApiKey_apiKey_key" ON "ApiKey"("apiKey"); + +-- AddForeignKey +ALTER TABLE "LoggedCall" ADD CONSTRAINT "LoggedCall_modelResponseId_fkey" FOREIGN KEY ("modelResponseId") REFERENCES "LoggedCallModelResponse"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "LoggedCall" ADD CONSTRAINT "LoggedCall_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "LoggedCallModelResponse" ADD CONSTRAINT "LoggedCallModelResponse_originalLoggedCallId_fkey" FOREIGN KEY ("originalLoggedCallId") REFERENCES "LoggedCall"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "LoggedCallTag" ADD CONSTRAINT "LoggedCallTag_loggedCallId_fkey" FOREIGN KEY ("loggedCallId") REFERENCES "LoggedCall"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ApiKey" ADD CONSTRAINT "ApiKey_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/app/prisma/schema.prisma b/app/prisma/schema.prisma index 9c5c92e..7ed4bfe 100644 --- a/app/prisma/schema.prisma +++ b/app/prisma/schema.prisma @@ -260,13 +260,13 @@ model LoggedCall { cacheHit Boolean // A LoggedCall is always associated with a LoggedCallModelResponse. If this - // is a cache miss, it's a new LoggedCallModelResponse we created for this. - // If it's a cache hit, it's the existing LoggedCallModelResponse we served. + // is a cache miss, we create a new LoggedCallModelResponse. + // If it's a cache hit, it's a pre-existing LoggedCallModelResponse. modelResponseId String @db.Uuid modelResponse LoggedCallModelResponse @relation(fields: [modelResponseId], references: [id], onDelete: Cascade) // The response created by this LoggedCall. Will be null if this LoggedCall is a cache hit. - createdResponse LoggedCallModelResponse[] @relation(name: "ModelResponseCreatedBy") + createdResponse LoggedCallModelResponse[] @relation(name: "ModelResponseOriginalCall") organizationId String @db.Uuid organization Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade) @@ -309,8 +309,8 @@ model LoggedCallModelResponse { totalCost Decimal? @db.Decimal(18, 12) // The LoggedCall that created this LoggedCallModelResponse - createdById String @unique @db.Uuid - createdBy LoggedCall @relation(name: "ModelResponseCreatedBy", fields: [createdById], references: [id], onDelete: Cascade) + originalLoggedCallId String @unique @db.Uuid + originalLoggedCall LoggedCall @relation(name: "ModelResponseOriginalCall", fields: [originalLoggedCallId], references: [id], onDelete: Cascade) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -320,11 +320,11 @@ model LoggedCallModelResponse { } model LoggedCallTag { - id String @id @default(cuid()) + id String @id @default(uuid()) @db.Uuid name String value String? - loggedCallId String + loggedCallId String @db.Uuid loggedCall LoggedCall @relation(fields: [loggedCallId], references: [id], onDelete: Cascade) @@index([name]) diff --git a/app/src/pages/api/[...trpc].ts b/app/src/pages/api/[...trpc].ts new file mode 100644 index 0000000..4751e6c --- /dev/null +++ b/app/src/pages/api/[...trpc].ts @@ -0,0 +1,22 @@ +import { type NextApiRequest, type NextApiResponse } from "next"; +import cors from "nextjs-cors"; +import { createOpenApiNextHandler } from "trpc-openapi"; +import { createProcedureCache } from "trpc-openapi/dist/adapters/node-http/procedures"; +import { appRouter } from "~/server/api/root.router"; +import { createTRPCContext } from "~/server/api/trpc"; + +const openApiHandler = createOpenApiNextHandler({ + router: appRouter, + createContext: createTRPCContext, +}); + +const cache = createProcedureCache(appRouter); + +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + // Setup CORS + await cors(req, res); + + return openApiHandler(req, res); +}; + +export default handler; diff --git a/app/src/pages/api/openapi.json.ts b/app/src/pages/api/openapi.json.ts new file mode 100644 index 0000000..c02ca8e --- /dev/null +++ b/app/src/pages/api/openapi.json.ts @@ -0,0 +1,16 @@ +import { type NextApiRequest, type NextApiResponse } from "next"; +import { generateOpenApiDocument } from "trpc-openapi"; +import { appRouter } from "~/server/api/root.router"; + +export const openApiDocument = generateOpenApiDocument(appRouter, { + title: "OpenPipe API", + description: "The public API for reporting API calls to OpenPipe", + version: "0.1.0", + baseUrl: "https://app.openpipe.ai/api", +}); +// Respond with our OpenAPI schema +const hander = (req: NextApiRequest, res: NextApiResponse) => { + res.status(200).send(openApiDocument); +}; + +export default hander; diff --git a/app/src/server/api/routers/externalApi.router.ts b/app/src/server/api/routers/externalApi.router.ts new file mode 100644 index 0000000..d2bd363 --- /dev/null +++ b/app/src/server/api/routers/externalApi.router.ts @@ -0,0 +1,197 @@ +import { type Prisma } from "@prisma/client"; +import { type JsonValue } from "type-fest"; +import { z } from "zod"; +import { v4 as uuidv4 } from "uuid"; + +import { createTRPCRouter, publicProcedure } from "~/server/api/trpc"; +import { prisma } from "~/server/db"; +import { hashRequest } from "~/server/utils/hashObject"; + +const reqValidator = z.object({ + model: z.string(), + messages: z.array(z.any()), +}); + +const respValidator = z.object({ + id: z.string(), + model: z.string(), + usage: z.object({ + total_tokens: z.number(), + prompt_tokens: z.number(), + completion_tokens: z.number(), + }), + choices: z.array( + z.object({ + finish_reason: z.string(), + }), + ), +}); + +export const externalApiRouter = createTRPCRouter({ + checkCache: publicProcedure + .meta({ + openapi: { + method: "POST", + path: "/v1/check-cache", + description: "Check if a prompt is cached", + }, + }) + .input( + z.object({ + startTime: z.number().describe("Unix timestamp in milliseconds"), + reqPayload: z.unknown().describe("JSON-encoded request payload"), + tags: z + .record(z.string()) + .optional() + .describe( + 'Extra tags to attach to the call for filtering. Eg { "userId": "123", "promptId": "populate-title" }', + ), + }), + ) + .output( + z.object({ + respPayload: z.unknown().optional().describe("JSON-encoded response payload"), + }), + ) + .mutation(async ({ input, ctx }) => { + const apiKey = ctx.apiKey; + if (!apiKey) { + throw new Error("Missing API key"); + } + const key = await prisma.apiKey.findUnique({ + where: { apiKey }, + }); + if (!key) { + throw new Error("Invalid API key"); + } + const reqPayload = await reqValidator.spa(input.reqPayload); + const cacheKey = hashRequest(key.organizationId, reqPayload as JsonValue); + + const existingResponse = await prisma.loggedCallModelResponse.findFirst({ + where: { + cacheKey, + }, + include: { + originalLoggedCall: true, + }, + orderBy: { + startTime: "desc", + } + }); + + if (!existingResponse) return { respPayload: null }; + + await prisma.loggedCall.create({ + data: { + organizationId: key.organizationId, + startTime: new Date(input.startTime), + cacheHit: false, + modelResponseId: existingResponse.id, + } + }) + + return { + respPayload: existingResponse.respPayload, + }; + }), + + report: publicProcedure + .meta({ + openapi: { + method: "POST", + path: "/v1/report", + description: "Report an API call", + }, + }) + .input( + z.object({ + startTime: z.number().describe("Unix timestamp in milliseconds"), + endTime: z.number().describe("Unix timestamp in milliseconds"), + reqPayload: z.unknown().describe("JSON-encoded request payload"), + respPayload: z.unknown().optional().describe("JSON-encoded response payload"), + respStatus: z.number().optional().describe("HTTP status code of response"), + error: z.string().optional().describe("User-friendly error message"), + tags: z + .record(z.string()) + .optional() + .describe( + 'Extra tags to attach to the call for filtering. Eg { "userId": "123", "promptId": "populate-title" }', + ), + }), + ) + .output(z.void()) + .mutation(async ({ input, ctx }) => { + const apiKey = ctx.apiKey; + if (!apiKey) { + throw new Error("Missing API key"); + } + const key = await prisma.apiKey.findUnique({ + where: { apiKey }, + }); + if (!key) { + throw new Error("Invalid API key"); + } + const reqPayload = await reqValidator.spa(input.reqPayload); + const respPayload = await respValidator.spa(input.respPayload); + + const requestHash = hashRequest(key.organizationId, reqPayload as JsonValue); + + const newLoggedCallId = uuidv4(); + const newModelResponseId = uuidv4(); + + const usage = respPayload.success ? respPayload.data.usage : undefined; + + await prisma.$transaction([ + prisma.loggedCall.create({ + data: { + id: newLoggedCallId, + organizationId: key.organizationId, + startTime: new Date(input.startTime), + cacheHit: false, + modelResponseId: newModelResponseId, + }, + }), + prisma.loggedCallModelResponse.create({ + data: { + id: newModelResponseId, + originalLoggedCallId: newLoggedCallId, + startTime: new Date(input.startTime), + endTime: new Date(input.endTime), + reqPayload: input.reqPayload as Prisma.InputJsonValue, + respPayload: input.respPayload as Prisma.InputJsonValue, + respStatus: input.respStatus, + error: input.error, + durationMs: input.endTime - input.startTime, + ...(respPayload.success + ? { + cacheKey: requestHash, + inputTokens: usage ? usage.prompt_tokens : undefined, + outputTokens: usage ? usage.completion_tokens : undefined, + model: respPayload.data.model, + } + : null), + }, + }), + ]); + + if (input.tags) { + const tagsToCreate = Object.entries(input.tags).map(([name, value]) => ({ + loggedCallId: newLoggedCallId, + // sanitize tags + name: name.replaceAll(/[^a-zA-Z0-9_]/g, "_"), + value, + })); + + if (reqPayload.success) { + tagsToCreate.push({ + loggedCallId: newLoggedCallId, + name: "$model", + value: reqPayload.data.model, + }); + } + await prisma.loggedCallTag.createMany({ + data: tagsToCreate, + }); + } + }), +}); diff --git a/app/src/server/api/trpc.ts b/app/src/server/api/trpc.ts index dc67814..2f6c8b9 100644 --- a/app/src/server/api/trpc.ts +++ b/app/src/server/api/trpc.ts @@ -11,6 +11,7 @@ import { initTRPC, TRPCError } from "@trpc/server"; import { type CreateNextContextOptions } from "@trpc/server/adapters/next"; import { type Session } from "next-auth"; import superjson from "superjson"; +import { type OpenApiMeta } from "trpc-openapi"; import { ZodError } from "zod"; import { getServerAuthSession } from "~/server/auth"; import { prisma } from "~/server/db"; @@ -26,6 +27,7 @@ import { capturePath } from "~/utils/analytics/serverAnalytics"; type CreateContextOptions = { session: Session | null; + apiKey: string | null; }; // eslint-disable-next-line @typescript-eslint/no-empty-function @@ -44,6 +46,7 @@ const noOp = () => {}; export const createInnerTRPCContext = (opts: CreateContextOptions) => { return { session: opts.session, + apiKey: opts.apiKey, prisma, markAccessControlRun: noOp, }; @@ -61,8 +64,13 @@ export const createTRPCContext = async (opts: CreateNextContextOptions) => { // Get the session from the server using the getServerSession wrapper function const session = await getServerAuthSession({ req, res }); + const apiKey = req.headers["x-openpipe-api-key"] as string | null; + + console.log('api key is', apiKey) + return createInnerTRPCContext({ session, + apiKey, }); }; @@ -76,18 +84,21 @@ export const createTRPCContext = async (opts: CreateNextContextOptions) => { export type TRPCContext = Awaited>; -const t = initTRPC.context().create({ - transformer: superjson, - errorFormatter({ shape, error }) { - return { - ...shape, - data: { - ...shape.data, - zodError: error.cause instanceof ZodError ? error.cause.flatten() : null, - }, - }; - }, -}); +const t = initTRPC + .context() + .meta() + .create({ + transformer: superjson, + errorFormatter({ shape, error }) { + return { + ...shape, + data: { + ...shape.data, + zodError: error.cause instanceof ZodError ? error.cause.flatten() : null, + }, + }; + }, + }); /** * 3. ROUTER & PROCEDURE (THE IMPORTANT BIT) diff --git a/app/src/server/tasks/queryModel.task.ts b/app/src/server/tasks/queryModel.task.ts index 6f9d08b..9b32d9d 100644 --- a/app/src/server/tasks/queryModel.task.ts +++ b/app/src/server/tasks/queryModel.task.ts @@ -1,10 +1,10 @@ import { type Prisma } from "@prisma/client"; -import { type JsonObject } from "type-fest"; +import { JsonValue, type JsonObject } from "type-fest"; import modelProviders from "~/modelProviders/modelProviders"; import { prisma } from "~/server/db"; import { wsConnection } from "~/utils/wsConnection"; import { runEvalsForOutput } from "../utils/evaluations"; -import hashPrompt from "../utils/hashPrompt"; +import hashObject from "../utils/hashObject"; import defineTask from "./defineTask"; import parsePromptConstructor from "~/promptConstructor/parse"; @@ -99,7 +99,7 @@ export const queryModel = defineTask("queryModel", async (task) = } : null; - const inputHash = hashPrompt(prompt); + const inputHash = hashObject(prompt as JsonValue); let modelResponse = await prisma.modelResponse.create({ data: { diff --git a/app/src/server/utils/generateNewCell.ts b/app/src/server/utils/generateNewCell.ts index 2f79693..858781e 100644 --- a/app/src/server/utils/generateNewCell.ts +++ b/app/src/server/utils/generateNewCell.ts @@ -1,7 +1,7 @@ import { Prisma } from "@prisma/client"; import { prisma } from "../db"; import { type JsonObject } from "type-fest"; -import hashPrompt from "./hashPrompt"; +import hashObject from "./hashObject"; import { omit } from "lodash-es"; import { queueQueryModel } from "../tasks/queryModel.task"; import parsePromptConstructor from "~/promptConstructor/parse"; @@ -57,7 +57,7 @@ export const generateNewCell = async ( return; } - const inputHash = hashPrompt(parsedConstructFn); + const inputHash = hashObject(parsedConstructFn); cell = await prisma.scenarioVariantCell.create({ data: { diff --git a/app/src/server/utils/hashPrompt.ts b/app/src/server/utils/hashObject.ts similarity index 75% rename from app/src/server/utils/hashPrompt.ts rename to app/src/server/utils/hashObject.ts index 7b1f347..562e7c9 100644 --- a/app/src/server/utils/hashPrompt.ts +++ b/app/src/server/utils/hashObject.ts @@ -1,6 +1,5 @@ import crypto from "crypto"; import { type JsonValue } from "type-fest"; -import { ParsedPromptConstructor } from "~/promptConstructor/parse"; function sortKeys(obj: JsonValue): JsonValue { if (typeof obj !== "object" || obj === null) { @@ -25,9 +24,17 @@ function sortKeys(obj: JsonValue): JsonValue { return sortedObj; } -export default function hashPrompt(prompt: ParsedPromptConstructor): string { +export function hashRequest(organizationId: string, reqPayload: JsonValue): string { + const obj = { + organizationId, + reqPayload, + }; + return hashObject(obj); +} + +export default function hashObject(obj: JsonValue): string { // Sort object keys recursively - const sortedObj = sortKeys(prompt as unknown as JsonValue); + const sortedObj = sortKeys(obj); // Convert to JSON and hash it const str = JSON.stringify(sortedObj); From 109a9ddb1e2a2377ae6b7a2b98d72e84fe14aa44 Mon Sep 17 00:00:00 2001 From: David Corbitt Date: Sun, 6 Aug 2023 13:50:43 -0700 Subject: [PATCH 04/49] Add js client lib --- app/openapitools.json | 7 + app/package.json | 2 +- .../server/api/routers/externalApi.router.ts | 2 +- app/src/server/auth.ts | 7 +- app/src/server/scripts/client-codegen.ts | 36 +++++ client-libs/js/codegen/.gitignore | 4 + client-libs/js/codegen/.npmignore | 1 + .../js/codegen/.openapi-generator-ignore | 23 +++ .../js/codegen/.openapi-generator/FILES | 9 ++ .../js/codegen/.openapi-generator/VERSION | 1 + client-libs/js/codegen/api.ts | 26 +++ client-libs/js/codegen/base.ts | 72 +++++++++ client-libs/js/codegen/common.ts | 150 ++++++++++++++++++ client-libs/js/codegen/configuration.ts | 101 ++++++++++++ client-libs/js/codegen/git_push.sh | 57 +++++++ client-libs/js/codegen/index.ts | 18 +++ client-libs/js/package.json | 21 +++ client-libs/schema.json | 62 ++++++++ 18 files changed, 596 insertions(+), 3 deletions(-) create mode 100644 app/openapitools.json create mode 100644 app/src/server/scripts/client-codegen.ts create mode 100644 client-libs/js/codegen/.gitignore create mode 100644 client-libs/js/codegen/.npmignore create mode 100644 client-libs/js/codegen/.openapi-generator-ignore create mode 100644 client-libs/js/codegen/.openapi-generator/FILES create mode 100644 client-libs/js/codegen/.openapi-generator/VERSION create mode 100644 client-libs/js/codegen/api.ts create mode 100644 client-libs/js/codegen/base.ts create mode 100644 client-libs/js/codegen/common.ts create mode 100644 client-libs/js/codegen/configuration.ts create mode 100644 client-libs/js/codegen/git_push.sh create mode 100644 client-libs/js/codegen/index.ts create mode 100644 client-libs/js/package.json create mode 100644 client-libs/schema.json diff --git a/app/openapitools.json b/app/openapitools.json new file mode 100644 index 0000000..cd53ff4 --- /dev/null +++ b/app/openapitools.json @@ -0,0 +1,7 @@ +{ + "$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json", + "spaces": 2, + "generator-cli": { + "version": "6.6.0" + } +} diff --git a/app/package.json b/app/package.json index 3a0dcaa..6bfba73 100644 --- a/app/package.json +++ b/app/package.json @@ -16,7 +16,7 @@ "postinstall": "prisma generate", "lint": "next lint", "start": "next start", - "codegen": "tsx src/codegen/export-openai-types.ts", + "codegen": "tsx src/server/scripts/client-codegen.ts", "seed": "tsx prisma/seed.ts", "check": "concurrently 'pnpm lint' 'pnpm tsc' 'pnpm prettier . --check'", "test": "pnpm vitest --no-threads" diff --git a/app/src/server/api/routers/externalApi.router.ts b/app/src/server/api/routers/externalApi.router.ts index d2bd363..c654c53 100644 --- a/app/src/server/api/routers/externalApi.router.ts +++ b/app/src/server/api/routers/externalApi.router.ts @@ -85,7 +85,7 @@ export const externalApiRouter = createTRPCRouter({ data: { organizationId: key.organizationId, startTime: new Date(input.startTime), - cacheHit: false, + cacheHit: true, modelResponseId: existingResponse.id, } }) diff --git a/app/src/server/auth.ts b/app/src/server/auth.ts index f2b779c..98e468f 100644 --- a/app/src/server/auth.ts +++ b/app/src/server/auth.ts @@ -2,9 +2,14 @@ import { PrismaAdapter } from "@next-auth/prisma-adapter"; import { type GetServerSidePropsContext } from "next"; import { getServerSession, type NextAuthOptions, type DefaultSession } from "next-auth"; import { prisma } from "~/server/db"; -import GitHubProvider from "next-auth/providers/github"; +import GitHubModule from "next-auth/providers/github"; import { env } from "~/env.mjs"; +// The client codegen script doesn't properly read the default export +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const untypedGitHubModule = GitHubModule as unknown as any +const GitHubProvider: typeof GitHubModule = untypedGitHubModule.default ? untypedGitHubModule.default : untypedGitHubModule + /** * Module augmentation for `next-auth` types. Allows us to add custom properties to the `session` * object and keep type safety. diff --git a/app/src/server/scripts/client-codegen.ts b/app/src/server/scripts/client-codegen.ts new file mode 100644 index 0000000..d3bbc0f --- /dev/null +++ b/app/src/server/scripts/client-codegen.ts @@ -0,0 +1,36 @@ +import "dotenv/config"; +import { openApiDocument } from "~/pages/api/openapi.json"; +import fs from "fs"; +import path from "path"; +import { execSync } from "child_process"; + +console.log("Exporting public OpenAPI schema to client-libs/schema.json"); + +const scriptPath = import.meta.url.replace("file://", ""); +const clientLibsPath = path.join(path.dirname(scriptPath), "../../../../client-libs"); + +const schemaPath = path.join( + clientLibsPath, + "schema.json" +); + +console.log("Exporting schema"); +fs.writeFileSync(schemaPath, JSON.stringify(openApiDocument, null, 2), "utf-8"); + +console.log("Generating Typescript client"); + +const tsClientPath = path.join( + clientLibsPath, + "js/codegen" +); + +fs.rmSync(tsClientPath, { recursive: true, force: true }); + +execSync( + `pnpm dlx @openapitools/openapi-generator-cli generate -i "${schemaPath}" -g typescript-axios -o "${tsClientPath}"`, + { + stdio: "inherit", + } +); + +console.log("Done!"); diff --git a/client-libs/js/codegen/.gitignore b/client-libs/js/codegen/.gitignore new file mode 100644 index 0000000..149b576 --- /dev/null +++ b/client-libs/js/codegen/.gitignore @@ -0,0 +1,4 @@ +wwwroot/*.js +node_modules +typings +dist diff --git a/client-libs/js/codegen/.npmignore b/client-libs/js/codegen/.npmignore new file mode 100644 index 0000000..999d88d --- /dev/null +++ b/client-libs/js/codegen/.npmignore @@ -0,0 +1 @@ +# empty npmignore to ensure all required files (e.g., in the dist folder) are published by npm \ No newline at end of file diff --git a/client-libs/js/codegen/.openapi-generator-ignore b/client-libs/js/codegen/.openapi-generator-ignore new file mode 100644 index 0000000..7484ee5 --- /dev/null +++ b/client-libs/js/codegen/.openapi-generator-ignore @@ -0,0 +1,23 @@ +# OpenAPI Generator Ignore +# Generated by openapi-generator https://github.com/openapitools/openapi-generator + +# Use this file to prevent files from being overwritten by the generator. +# The patterns follow closely to .gitignore or .dockerignore. + +# As an example, the C# client generator defines ApiClient.cs. +# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: +#ApiClient.cs + +# You can match any string of characters against a directory, file or extension with a single asterisk (*): +#foo/*/qux +# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux + +# You can recursively match patterns against a directory, file or extension with a double asterisk (**): +#foo/**/qux +# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux + +# You can also negate patterns with an exclamation (!). +# For example, you can ignore all files in a docs folder with the file extension .md: +#docs/*.md +# Then explicitly reverse the ignore rule for a single file: +#!docs/README.md diff --git a/client-libs/js/codegen/.openapi-generator/FILES b/client-libs/js/codegen/.openapi-generator/FILES new file mode 100644 index 0000000..16b445e --- /dev/null +++ b/client-libs/js/codegen/.openapi-generator/FILES @@ -0,0 +1,9 @@ +.gitignore +.npmignore +.openapi-generator-ignore +api.ts +base.ts +common.ts +configuration.ts +git_push.sh +index.ts diff --git a/client-libs/js/codegen/.openapi-generator/VERSION b/client-libs/js/codegen/.openapi-generator/VERSION new file mode 100644 index 0000000..cd802a1 --- /dev/null +++ b/client-libs/js/codegen/.openapi-generator/VERSION @@ -0,0 +1 @@ +6.6.0 \ No newline at end of file diff --git a/client-libs/js/codegen/api.ts b/client-libs/js/codegen/api.ts new file mode 100644 index 0000000..f1f89f8 --- /dev/null +++ b/client-libs/js/codegen/api.ts @@ -0,0 +1,26 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * OpenPipe API + * The public API for reporting API calls to OpenPipe + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +import type { Configuration } from './configuration'; +import type { AxiosPromise, AxiosInstance, AxiosRequestConfig } from 'axios'; +import globalAxios from 'axios'; +// Some imports not used depending on template conditions +// @ts-ignore +import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from './common'; +import type { RequestArgs } from './base'; +// @ts-ignore +import { BASE_PATH, COLLECTION_FORMATS, BaseAPI, RequiredError } from './base'; + + diff --git a/client-libs/js/codegen/base.ts b/client-libs/js/codegen/base.ts new file mode 100644 index 0000000..33af09a --- /dev/null +++ b/client-libs/js/codegen/base.ts @@ -0,0 +1,72 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * OpenPipe API + * The public API for reporting API calls to OpenPipe + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +import type { Configuration } from './configuration'; +// Some imports not used depending on template conditions +// @ts-ignore +import type { AxiosPromise, AxiosInstance, AxiosRequestConfig } from 'axios'; +import globalAxios from 'axios'; + +export const BASE_PATH = "https://app.openpipe.ai/api".replace(/\/+$/, ""); + +/** + * + * @export + */ +export const COLLECTION_FORMATS = { + csv: ",", + ssv: " ", + tsv: "\t", + pipes: "|", +}; + +/** + * + * @export + * @interface RequestArgs + */ +export interface RequestArgs { + url: string; + options: AxiosRequestConfig; +} + +/** + * + * @export + * @class BaseAPI + */ +export class BaseAPI { + protected configuration: Configuration | undefined; + + constructor(configuration?: Configuration, protected basePath: string = BASE_PATH, protected axios: AxiosInstance = globalAxios) { + if (configuration) { + this.configuration = configuration; + this.basePath = configuration.basePath || this.basePath; + } + } +}; + +/** + * + * @export + * @class RequiredError + * @extends {Error} + */ +export class RequiredError extends Error { + constructor(public field: string, msg?: string) { + super(msg); + this.name = "RequiredError" + } +} diff --git a/client-libs/js/codegen/common.ts b/client-libs/js/codegen/common.ts new file mode 100644 index 0000000..6a57377 --- /dev/null +++ b/client-libs/js/codegen/common.ts @@ -0,0 +1,150 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * OpenPipe API + * The public API for reporting API calls to OpenPipe + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +import type { Configuration } from "./configuration"; +import type { RequestArgs } from "./base"; +import type { AxiosInstance, AxiosResponse } from 'axios'; +import { RequiredError } from "./base"; + +/** + * + * @export + */ +export const DUMMY_BASE_URL = 'https://example.com' + +/** + * + * @throws {RequiredError} + * @export + */ +export const assertParamExists = function (functionName: string, paramName: string, paramValue: unknown) { + if (paramValue === null || paramValue === undefined) { + throw new RequiredError(paramName, `Required parameter ${paramName} was null or undefined when calling ${functionName}.`); + } +} + +/** + * + * @export + */ +export const setApiKeyToObject = async function (object: any, keyParamName: string, configuration?: Configuration) { + if (configuration && configuration.apiKey) { + const localVarApiKeyValue = typeof configuration.apiKey === 'function' + ? await configuration.apiKey(keyParamName) + : await configuration.apiKey; + object[keyParamName] = localVarApiKeyValue; + } +} + +/** + * + * @export + */ +export const setBasicAuthToObject = function (object: any, configuration?: Configuration) { + if (configuration && (configuration.username || configuration.password)) { + object["auth"] = { username: configuration.username, password: configuration.password }; + } +} + +/** + * + * @export + */ +export const setBearerAuthToObject = async function (object: any, configuration?: Configuration) { + if (configuration && configuration.accessToken) { + const accessToken = typeof configuration.accessToken === 'function' + ? await configuration.accessToken() + : await configuration.accessToken; + object["Authorization"] = "Bearer " + accessToken; + } +} + +/** + * + * @export + */ +export const setOAuthToObject = async function (object: any, name: string, scopes: string[], configuration?: Configuration) { + if (configuration && configuration.accessToken) { + const localVarAccessTokenValue = typeof configuration.accessToken === 'function' + ? await configuration.accessToken(name, scopes) + : await configuration.accessToken; + object["Authorization"] = "Bearer " + localVarAccessTokenValue; + } +} + +function setFlattenedQueryParams(urlSearchParams: URLSearchParams, parameter: any, key: string = ""): void { + if (parameter == null) return; + if (typeof parameter === "object") { + if (Array.isArray(parameter)) { + (parameter as any[]).forEach(item => setFlattenedQueryParams(urlSearchParams, item, key)); + } + else { + Object.keys(parameter).forEach(currentKey => + setFlattenedQueryParams(urlSearchParams, parameter[currentKey], `${key}${key !== '' ? '.' : ''}${currentKey}`) + ); + } + } + else { + if (urlSearchParams.has(key)) { + urlSearchParams.append(key, parameter); + } + else { + urlSearchParams.set(key, parameter); + } + } +} + +/** + * + * @export + */ +export const setSearchParams = function (url: URL, ...objects: any[]) { + const searchParams = new URLSearchParams(url.search); + setFlattenedQueryParams(searchParams, objects); + url.search = searchParams.toString(); +} + +/** + * + * @export + */ +export const serializeDataIfNeeded = function (value: any, requestOptions: any, configuration?: Configuration) { + const nonString = typeof value !== 'string'; + const needsSerialization = nonString && configuration && configuration.isJsonMime + ? configuration.isJsonMime(requestOptions.headers['Content-Type']) + : nonString; + return needsSerialization + ? JSON.stringify(value !== undefined ? value : {}) + : (value || ""); +} + +/** + * + * @export + */ +export const toPathString = function (url: URL) { + return url.pathname + url.search + url.hash +} + +/** + * + * @export + */ +export const createRequestFunction = function (axiosArgs: RequestArgs, globalAxios: AxiosInstance, BASE_PATH: string, configuration?: Configuration) { + return >(axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => { + const axiosRequestArgs = {...axiosArgs.options, url: (configuration?.basePath || basePath) + axiosArgs.url}; + return axios.request(axiosRequestArgs); + }; +} diff --git a/client-libs/js/codegen/configuration.ts b/client-libs/js/codegen/configuration.ts new file mode 100644 index 0000000..4bd4a54 --- /dev/null +++ b/client-libs/js/codegen/configuration.ts @@ -0,0 +1,101 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * OpenPipe API + * The public API for reporting API calls to OpenPipe + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +export interface ConfigurationParameters { + apiKey?: string | Promise | ((name: string) => string) | ((name: string) => Promise); + username?: string; + password?: string; + accessToken?: string | Promise | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise); + basePath?: string; + baseOptions?: any; + formDataCtor?: new () => any; +} + +export class Configuration { + /** + * parameter for apiKey security + * @param name security name + * @memberof Configuration + */ + apiKey?: string | Promise | ((name: string) => string) | ((name: string) => Promise); + /** + * parameter for basic security + * + * @type {string} + * @memberof Configuration + */ + username?: string; + /** + * parameter for basic security + * + * @type {string} + * @memberof Configuration + */ + password?: string; + /** + * parameter for oauth2 security + * @param name security name + * @param scopes oauth2 scope + * @memberof Configuration + */ + accessToken?: string | Promise | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise); + /** + * override base path + * + * @type {string} + * @memberof Configuration + */ + basePath?: string; + /** + * base options for axios calls + * + * @type {any} + * @memberof Configuration + */ + baseOptions?: any; + /** + * The FormData constructor that will be used to create multipart form data + * requests. You can inject this here so that execution environments that + * do not support the FormData class can still run the generated client. + * + * @type {new () => FormData} + */ + formDataCtor?: new () => any; + + constructor(param: ConfigurationParameters = {}) { + this.apiKey = param.apiKey; + this.username = param.username; + this.password = param.password; + this.accessToken = param.accessToken; + this.basePath = param.basePath; + this.baseOptions = param.baseOptions; + this.formDataCtor = param.formDataCtor; + } + + /** + * Check if the given MIME is a JSON MIME. + * JSON MIME examples: + * application/json + * application/json; charset=UTF8 + * APPLICATION/JSON + * application/vnd.company+json + * @param mime - MIME (Multipurpose Internet Mail Extensions) + * @return True if the given MIME is JSON, false otherwise. + */ + public isJsonMime(mime: string): boolean { + const jsonMime: RegExp = new RegExp('^(application\/json|[^;/ \t]+\/[^;/ \t]+[+]json)[ \t]*(;.*)?$', 'i'); + return mime !== null && (jsonMime.test(mime) || mime.toLowerCase() === 'application/json-patch+json'); + } +} diff --git a/client-libs/js/codegen/git_push.sh b/client-libs/js/codegen/git_push.sh new file mode 100644 index 0000000..f53a75d --- /dev/null +++ b/client-libs/js/codegen/git_push.sh @@ -0,0 +1,57 @@ +#!/bin/sh +# ref: https://help.github.com/articles/adding-an-existing-project-to-github-using-the-command-line/ +# +# Usage example: /bin/sh ./git_push.sh wing328 openapi-petstore-perl "minor update" "gitlab.com" + +git_user_id=$1 +git_repo_id=$2 +release_note=$3 +git_host=$4 + +if [ "$git_host" = "" ]; then + git_host="github.com" + echo "[INFO] No command line input provided. Set \$git_host to $git_host" +fi + +if [ "$git_user_id" = "" ]; then + git_user_id="GIT_USER_ID" + echo "[INFO] No command line input provided. Set \$git_user_id to $git_user_id" +fi + +if [ "$git_repo_id" = "" ]; then + git_repo_id="GIT_REPO_ID" + echo "[INFO] No command line input provided. Set \$git_repo_id to $git_repo_id" +fi + +if [ "$release_note" = "" ]; then + release_note="Minor update" + echo "[INFO] No command line input provided. Set \$release_note to $release_note" +fi + +# Initialize the local directory as a Git repository +git init + +# Adds the files in the local repository and stages them for commit. +git add . + +# Commits the tracked changes and prepares them to be pushed to a remote repository. +git commit -m "$release_note" + +# Sets the new remote +git_remote=$(git remote) +if [ "$git_remote" = "" ]; then # git remote not defined + + if [ "$GIT_TOKEN" = "" ]; then + echo "[INFO] \$GIT_TOKEN (environment variable) is not set. Using the git credential in your environment." + git remote add origin https://${git_host}/${git_user_id}/${git_repo_id}.git + else + git remote add origin https://${git_user_id}:"${GIT_TOKEN}"@${git_host}/${git_user_id}/${git_repo_id}.git + fi + +fi + +git pull origin master + +# Pushes (Forces) the changes in the local repository up to the remote repository +echo "Git pushing to https://${git_host}/${git_user_id}/${git_repo_id}.git" +git push origin master 2>&1 | grep -v 'To https' diff --git a/client-libs/js/codegen/index.ts b/client-libs/js/codegen/index.ts new file mode 100644 index 0000000..3bc3def --- /dev/null +++ b/client-libs/js/codegen/index.ts @@ -0,0 +1,18 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * OpenPipe API + * The public API for reporting API calls to OpenPipe + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +export * from "./api"; +export * from "./configuration"; + diff --git a/client-libs/js/package.json b/client-libs/js/package.json new file mode 100644 index 0000000..9f7a8a4 --- /dev/null +++ b/client-libs/js/package.json @@ -0,0 +1,21 @@ +{ + "name": "openpipe", + "version": "0.1.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "openai": "^3.3.0" + }, + "devDependencies": { + "dotenv": "^16.3.1", + "tsx": "^3.12.7", + "typescript": "^5.1.6" + } + } + \ No newline at end of file diff --git a/client-libs/schema.json b/client-libs/schema.json new file mode 100644 index 0000000..a3be03f --- /dev/null +++ b/client-libs/schema.json @@ -0,0 +1,62 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "OpenPipe API", + "description": "The public API for reporting API calls to OpenPipe", + "version": "0.1.0" + }, + "servers": [ + { + "url": "https://app.openpipe.ai/api" + } + ], + "paths": {}, + "components": { + "securitySchemes": { + "Authorization": { + "type": "http", + "scheme": "bearer" + } + }, + "responses": { + "error": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "code": { + "type": "string" + }, + "issues": { + "type": "array", + "items": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "additionalProperties": false + } + } + }, + "required": [ + "message", + "code" + ], + "additionalProperties": false + } + } + } + } + } + } +} \ No newline at end of file From a53d70d8b2445fa071694b58b874e855253cb971 Mon Sep 17 00:00:00 2001 From: David Corbitt Date: Sun, 6 Aug 2023 17:29:06 -0700 Subject: [PATCH 05/49] Add basic typescript lib --- app/pnpm-lock.yaml | 563 +++++++++--------- app/src/server/api/root.router.ts | 2 + app/src/server/api/trpc.ts | 2 +- app/src/server/scripts/client-codegen.ts | 2 + app/src/server/utils/openai.ts | 8 +- client-libs/js/codegen/api.ts | 26 - client-libs/js/package.json | 21 - client-libs/schema.json | 127 +++- client-libs/typescript/.gitignore | 2 + .../{js => typescript}/codegen/.gitignore | 0 .../{js => typescript}/codegen/.npmignore | 0 .../codegen/.openapi-generator-ignore | 0 .../codegen/.openapi-generator/FILES | 0 .../codegen/.openapi-generator/VERSION | 0 client-libs/typescript/codegen/api.js | 272 +++++++++ client-libs/typescript/codegen/api.ts | 319 ++++++++++ client-libs/typescript/codegen/base.js | 80 +++ .../{js => typescript}/codegen/base.ts | 0 client-libs/typescript/codegen/common.js | 252 ++++++++ .../{js => typescript}/codegen/common.ts | 0 .../typescript/codegen/configuration.js | 44 ++ .../codegen/configuration.ts | 0 .../{js => typescript}/codegen/git_push.sh | 0 client-libs/typescript/codegen/index.js | 31 + .../{js => typescript}/codegen/index.ts | 0 client-libs/typescript/index.ts | 3 + client-libs/typescript/openai-legacy/index.ts | 81 +++ client-libs/typescript/openai/index.ts | 31 + client-libs/typescript/package.json | 24 + client-libs/typescript/pnpm-lock.yaml | 548 +++++++++++++++++ client-libs/typescript/tsconfig.json | 13 + 31 files changed, 2126 insertions(+), 325 deletions(-) delete mode 100644 client-libs/js/codegen/api.ts delete mode 100644 client-libs/js/package.json create mode 100644 client-libs/typescript/.gitignore rename client-libs/{js => typescript}/codegen/.gitignore (100%) rename client-libs/{js => typescript}/codegen/.npmignore (100%) rename client-libs/{js => typescript}/codegen/.openapi-generator-ignore (100%) rename client-libs/{js => typescript}/codegen/.openapi-generator/FILES (100%) rename client-libs/{js => typescript}/codegen/.openapi-generator/VERSION (100%) create mode 100644 client-libs/typescript/codegen/api.js create mode 100644 client-libs/typescript/codegen/api.ts create mode 100644 client-libs/typescript/codegen/base.js rename client-libs/{js => typescript}/codegen/base.ts (100%) create mode 100644 client-libs/typescript/codegen/common.js rename client-libs/{js => typescript}/codegen/common.ts (100%) create mode 100644 client-libs/typescript/codegen/configuration.js rename client-libs/{js => typescript}/codegen/configuration.ts (100%) rename client-libs/{js => typescript}/codegen/git_push.sh (100%) create mode 100644 client-libs/typescript/codegen/index.js rename client-libs/{js => typescript}/codegen/index.ts (100%) create mode 100644 client-libs/typescript/index.ts create mode 100644 client-libs/typescript/openai-legacy/index.ts create mode 100644 client-libs/typescript/openai/index.ts create mode 100644 client-libs/typescript/package.json create mode 100644 client-libs/typescript/pnpm-lock.yaml create mode 100644 client-libs/typescript/tsconfig.json diff --git a/app/pnpm-lock.yaml b/app/pnpm-lock.yaml index 0575dfe..fa06d85 100644 --- a/app/pnpm-lock.yaml +++ b/app/pnpm-lock.yaml @@ -337,14 +337,13 @@ packages: '@types/node': 18.16.0 '@types/node-fetch': 2.6.4 abort-controller: 3.0.0 - agentkeepalive: 4.3.0 + agentkeepalive: 4.5.0 digest-fetch: 1.3.0 form-data-encoder: 1.7.2 formdata-node: 4.4.1 node-fetch: 2.6.12 transitivePeerDependencies: - encoding - - supports-color dev: false /@apidevtools/json-schema-ref-parser@10.1.0: @@ -415,7 +414,7 @@ packages: '@babel/compat-data': 7.22.9 '@babel/core': 7.22.9 '@babel/helper-validator-option': 7.22.5 - browserslist: 4.21.9 + browserslist: 4.21.10 lru-cache: 5.1.1 semver: 6.3.1 @@ -1994,8 +1993,8 @@ packages: dev: false optional: true - /@esbuild/android-arm64@0.18.14: - resolution: {integrity: sha512-rZ2v+Luba5/3D6l8kofWgTnqE+qsC/L5MleKIKFyllHTKHrNBMqeRCnZI1BtRx8B24xMYxeU32iIddRQqMsOsg==} + /@esbuild/android-arm64@0.18.18: + resolution: {integrity: sha512-dkAPYzRHq3dNXIzOyAknYOzsx8o3KWaNiuu56B2rP9IFPmFWMS58WQcTlUQi6iloku8ZyHHMluCe5sTWhKq/Yw==} engines: {node: '>=12'} cpu: [arm64] os: [android] @@ -2012,8 +2011,8 @@ packages: dev: false optional: true - /@esbuild/android-arm@0.18.14: - resolution: {integrity: sha512-blODaaL+lngG5bdK/t4qZcQvq2BBqrABmYwqPPcS5VRxrCSGHb9R/rA3fqxh7R18I7WU4KKv+NYkt22FDfalcg==} + /@esbuild/android-arm@0.18.18: + resolution: {integrity: sha512-oBymf7ZwplAawSxmiSlBCf+FMcY0f4bs5QP2jn43JKUf0M9DnrUTjqa5RvFPl1elw+sMfcpfBRPK+rb+E1q7zg==} engines: {node: '>=12'} cpu: [arm] os: [android] @@ -2030,8 +2029,8 @@ packages: dev: false optional: true - /@esbuild/android-x64@0.18.14: - resolution: {integrity: sha512-qSwh8y38QKl+1Iqg+YhvCVYlSk3dVLk9N88VO71U4FUjtiSFylMWK3Ugr8GC6eTkkP4Tc83dVppt2n8vIdlSGg==} + /@esbuild/android-x64@0.18.18: + resolution: {integrity: sha512-r7/pVcrUQMYkjvtE/1/n6BxhWM+/9tvLxDG1ev1ce4z3YsqoxMK9bbOM6bFcj0BowMeGQvOZWcBV182lFFKmrw==} engines: {node: '>=12'} cpu: [x64] os: [android] @@ -2048,8 +2047,8 @@ packages: dev: false optional: true - /@esbuild/darwin-arm64@0.18.14: - resolution: {integrity: sha512-9Hl2D2PBeDYZiNbnRKRWuxwHa9v5ssWBBjisXFkVcSP5cZqzZRFBUWEQuqBHO4+PKx4q4wgHoWtfQ1S7rUqJ2Q==} + /@esbuild/darwin-arm64@0.18.18: + resolution: {integrity: sha512-MSe2iV9MAH3wfP0g+vzN9bp36rtPPuCSk+bT5E2vv/d8krvW5uB/Pi/Q5+txUZuxsG3GcO8dhygjnFq0wJU9hQ==} engines: {node: '>=12'} cpu: [arm64] os: [darwin] @@ -2066,8 +2065,8 @@ packages: dev: false optional: true - /@esbuild/darwin-x64@0.18.14: - resolution: {integrity: sha512-ZnI3Dg4ElQ6tlv82qLc/UNHtFsgZSKZ7KjsUNAo1BF1SoYDjkGKHJyCrYyWjFecmXpvvG/KJ9A/oe0H12odPLQ==} + /@esbuild/darwin-x64@0.18.18: + resolution: {integrity: sha512-ARFYISOWkaifjcr48YtO70gcDNeOf1H2RnmOj6ip3xHIj66f3dAbhcd5Nph5np6oHI7DhHIcr9MWO18RvUL1bw==} engines: {node: '>=12'} cpu: [x64] os: [darwin] @@ -2084,8 +2083,8 @@ packages: dev: false optional: true - /@esbuild/freebsd-arm64@0.18.14: - resolution: {integrity: sha512-h3OqR80Da4oQCIa37zl8tU5MwHQ7qgPV0oVScPfKJK21fSRZEhLE4IIVpmcOxfAVmqjU6NDxcxhYaM8aDIGRLw==} + /@esbuild/freebsd-arm64@0.18.18: + resolution: {integrity: sha512-BHnXmexzEWRU2ZySJosU0Ts0NRnJnNrMB6t4EiIaOSel73I8iLsNiTPLH0rJulAh19cYZutsB5XHK6N8fi5eMg==} engines: {node: '>=12'} cpu: [arm64] os: [freebsd] @@ -2102,8 +2101,8 @@ packages: dev: false optional: true - /@esbuild/freebsd-x64@0.18.14: - resolution: {integrity: sha512-ha4BX+S6CZG4BoH9tOZTrFIYC1DH13UTCRHzFc3GWX74nz3h/N6MPF3tuR3XlsNjMFUazGgm35MPW5tHkn2lzQ==} + /@esbuild/freebsd-x64@0.18.18: + resolution: {integrity: sha512-n823w35wm0ZOobbuE//0sJjuz1Qj619+AwjgOcAJMN2pomZhH9BONCtn+KlfrmM/NWZ+27yB/eGVFzUIWLeh3w==} engines: {node: '>=12'} cpu: [x64] os: [freebsd] @@ -2120,8 +2119,8 @@ packages: dev: false optional: true - /@esbuild/linux-arm64@0.18.14: - resolution: {integrity: sha512-IXORRe22In7U65NZCzjwAUc03nn8SDIzWCnfzJ6t/8AvGx5zBkcLfknI+0P+hhuftufJBmIXxdSTbzWc8X/V4w==} + /@esbuild/linux-arm64@0.18.18: + resolution: {integrity: sha512-zANxnwF0sCinDcAqoMohGoWBK9QaFJ65Vgh0ZE+RXtURaMwx+RfmfLElqtnn7X8OYNckMoIXSg7u+tZ3tqTlrA==} engines: {node: '>=12'} cpu: [arm64] os: [linux] @@ -2138,8 +2137,8 @@ packages: dev: false optional: true - /@esbuild/linux-arm@0.18.14: - resolution: {integrity: sha512-5+7vehI1iqru5WRtJyU2XvTOvTGURw3OZxe3YTdE9muNNIdmKAVmSHpB3Vw2LazJk2ifEdIMt/wTWnVe5V98Kg==} + /@esbuild/linux-arm@0.18.18: + resolution: {integrity: sha512-Kck3jxPLQU4VeAGwe8Q4NU+IWIx+suULYOFUI9T0C2J1+UQlOHJ08ITN+MaJJ+2youzJOmKmcphH/t3SJxQ1Tw==} engines: {node: '>=12'} cpu: [arm] os: [linux] @@ -2156,8 +2155,8 @@ packages: dev: false optional: true - /@esbuild/linux-ia32@0.18.14: - resolution: {integrity: sha512-BfHlMa0nibwpjG+VXbOoqJDmFde4UK2gnW351SQ2Zd4t1N3zNdmUEqRkw/srC1Sa1DRBE88Dbwg4JgWCbNz/FQ==} + /@esbuild/linux-ia32@0.18.18: + resolution: {integrity: sha512-+VHz2sIRlY5u8IlaLJpdf5TL2kM76yx186pW7bpTB+vLWpzcFQVP04L842ZB2Ty13A1VXUvy3DbU1jV65P2skg==} engines: {node: '>=12'} cpu: [ia32] os: [linux] @@ -2174,8 +2173,8 @@ packages: dev: false optional: true - /@esbuild/linux-loong64@0.18.14: - resolution: {integrity: sha512-j2/Ex++DRUWIAaUDprXd3JevzGtZ4/d7VKz+AYDoHZ3HjJzCyYBub9CU1wwIXN+viOP0b4VR3RhGClsvyt/xSw==} + /@esbuild/linux-loong64@0.18.18: + resolution: {integrity: sha512-fXPEPdeGBvguo/1+Na8OIWz3667BN1cwbGtTEZWTd0qdyTsk5gGf9jVX8MblElbDb/Cpw6y5JiaQuL96YmvBwQ==} engines: {node: '>=12'} cpu: [loong64] os: [linux] @@ -2192,8 +2191,8 @@ packages: dev: false optional: true - /@esbuild/linux-mips64el@0.18.14: - resolution: {integrity: sha512-qn2+nc+ZCrJmiicoAnJXJJkZWt8Nwswgu1crY7N+PBR8ChBHh89XRxj38UU6Dkthl2yCVO9jWuafZ24muzDC/A==} + /@esbuild/linux-mips64el@0.18.18: + resolution: {integrity: sha512-dLvRB87pIBIRnEIC32LIcgwK1JzlIuADIRjLKdUIpxauKwMuS/xMpN+cFl+0nN4RHNYOZ57DmXFFmQAcdlFOmw==} engines: {node: '>=12'} cpu: [mips64el] os: [linux] @@ -2210,8 +2209,8 @@ packages: dev: false optional: true - /@esbuild/linux-ppc64@0.18.14: - resolution: {integrity: sha512-aGzXzd+djqeEC5IRkDKt3kWzvXoXC6K6GyYKxd+wsFJ2VQYnOWE954qV2tvy5/aaNrmgPTb52cSCHFE+Z7Z0yg==} + /@esbuild/linux-ppc64@0.18.18: + resolution: {integrity: sha512-fRChqIJZ7hLkXSKfBLYgsX9Ssb5OGCjk3dzCETF5QSS1qjTgayLv0ALUdJDB9QOh/nbWwp+qfLZU6md4XcjL7w==} engines: {node: '>=12'} cpu: [ppc64] os: [linux] @@ -2228,8 +2227,8 @@ packages: dev: false optional: true - /@esbuild/linux-riscv64@0.18.14: - resolution: {integrity: sha512-8C6vWbfr0ygbAiMFLS6OPz0BHvApkT2gCboOGV76YrYw+sD/MQJzyITNsjZWDXJwPu9tjrFQOVG7zijRzBCnLw==} + /@esbuild/linux-riscv64@0.18.18: + resolution: {integrity: sha512-ALK/BT3u7Hoa/vHjow6W6+MKF0ohYcVcVA1EpskI4bkBPVuDLrUDqt2YFifg5UcZc8qup0CwQqWmFUd6VMNgaA==} engines: {node: '>=12'} cpu: [riscv64] os: [linux] @@ -2246,8 +2245,8 @@ packages: dev: false optional: true - /@esbuild/linux-s390x@0.18.14: - resolution: {integrity: sha512-G/Lf9iu8sRMM60OVGOh94ZW2nIStksEcITkXdkD09/T6QFD/o+g0+9WVyR/jajIb3A0LvBJ670tBnGe1GgXMgw==} + /@esbuild/linux-s390x@0.18.18: + resolution: {integrity: sha512-crT7jtOXd9iirY65B+mJQ6W0HWdNy8dtkZqKGWNcBnunpLcTCfne5y5bKic9bhyYzKpQEsO+C/VBPD8iF0RhRw==} engines: {node: '>=12'} cpu: [s390x] os: [linux] @@ -2264,8 +2263,8 @@ packages: dev: false optional: true - /@esbuild/linux-x64@0.18.14: - resolution: {integrity: sha512-TBgStYBQaa3EGhgqIDM+ECnkreb0wkcKqL7H6m+XPcGUoU4dO7dqewfbm0mWEQYH3kzFHrzjOFNpSAVzDZRSJw==} + /@esbuild/linux-x64@0.18.18: + resolution: {integrity: sha512-/NSgghjBOW9ELqjXDYxOCCIsvQUZpvua1/6NdnA9Vnrp9UzEydyDdFXljUjMMS9p5KxMzbMO9frjHYGVHBfCHg==} engines: {node: '>=12'} cpu: [x64] os: [linux] @@ -2282,8 +2281,8 @@ packages: dev: false optional: true - /@esbuild/netbsd-x64@0.18.14: - resolution: {integrity: sha512-stvCcjyCQR2lMTroqNhAbvROqRjxPEq0oQ380YdXxA81TaRJEucH/PzJ/qsEtsHgXlWFW6Ryr/X15vxQiyRXVg==} + /@esbuild/netbsd-x64@0.18.18: + resolution: {integrity: sha512-8Otf05Vx5sZjLLDulgr5QS5lsWXMplKZEyHMArH9/S4olLlhzmdhQBPhzhJTNwaL2FJNdWcUPNGAcoD5zDTfUA==} engines: {node: '>=12'} cpu: [x64] os: [netbsd] @@ -2300,8 +2299,8 @@ packages: dev: false optional: true - /@esbuild/openbsd-x64@0.18.14: - resolution: {integrity: sha512-apAOJF14CIsN5ht1PA57PboEMsNV70j3FUdxLmA2liZ20gEQnfTG5QU0FhENo5nwbTqCB2O3WDsXAihfODjHYw==} + /@esbuild/openbsd-x64@0.18.18: + resolution: {integrity: sha512-tFiFF4kT5L5qhVrWJUNxEXWvvX8nK/UX9ZrB7apuTwY3f6+Xy4aFMBPwAVrBYtBd5MOUuyOVHK6HBZCAHkwUlw==} engines: {node: '>=12'} cpu: [x64] os: [openbsd] @@ -2318,8 +2317,8 @@ packages: dev: false optional: true - /@esbuild/sunos-x64@0.18.14: - resolution: {integrity: sha512-fYRaaS8mDgZcGybPn2MQbn1ZNZx+UXFSUoS5Hd2oEnlsyUcr/l3c6RnXf1bLDRKKdLRSabTmyCy7VLQ7VhGdOQ==} + /@esbuild/sunos-x64@0.18.18: + resolution: {integrity: sha512-MPogVV8Bzh8os4OM+YDGGsSzCzmNRiyKGtHoJyZLtI4BMmd6EcxmGlcEGK1uM46h1BiOyi7Z7teUtzzQhvkC+w==} engines: {node: '>=12'} cpu: [x64] os: [sunos] @@ -2336,8 +2335,8 @@ packages: dev: false optional: true - /@esbuild/win32-arm64@0.18.14: - resolution: {integrity: sha512-1c44RcxKEJPrVj62XdmYhxXaU/V7auELCmnD+Ri+UCt+AGxTvzxl9uauQhrFso8gj6ZV1DaORV0sT9XSHOAk8Q==} + /@esbuild/win32-arm64@0.18.18: + resolution: {integrity: sha512-YKD6LF/XXY9REu+ZL5RAsusiG48n602qxsMVh/E8FFD9hp4OyTQaL9fpE1ovxwQXqFio+tT0ITUGjDSSSPN13w==} engines: {node: '>=12'} cpu: [arm64] os: [win32] @@ -2354,8 +2353,8 @@ packages: dev: false optional: true - /@esbuild/win32-ia32@0.18.14: - resolution: {integrity: sha512-EXAFttrdAxZkFQmpvcAQ2bywlWUsONp/9c2lcfvPUhu8vXBBenCXpoq9YkUvVP639ld3YGiYx0YUQ6/VQz3Maw==} + /@esbuild/win32-ia32@0.18.18: + resolution: {integrity: sha512-NjSBmBsyZBTsZB6ga6rA6PfG/RHnwruUz/9YEVXcm4STGauFWvhYhOMhEyw1yU5NVgYYm8CH5AltCm77TS21/Q==} engines: {node: '>=12'} cpu: [ia32] os: [win32] @@ -2372,8 +2371,8 @@ packages: dev: false optional: true - /@esbuild/win32-x64@0.18.14: - resolution: {integrity: sha512-K0QjGbcskx+gY+qp3v4/940qg8JitpXbdxFhRDA1aYoNaPff88+aEwoq45aqJ+ogpxQxmU0ZTjgnrQD/w8iiUg==} + /@esbuild/win32-x64@0.18.18: + resolution: {integrity: sha512-eTSg/gC3p3tdjj4roDhe5xu94l1s2jMazP8u2FsYO8SEKvSpPOO71EucprDn/IuErDPvTFUhV9lTw5z5WJCRKQ==} engines: {node: '>=12'} cpu: [x64] os: [win32] @@ -2388,16 +2387,16 @@ packages: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 dependencies: eslint: 8.40.0 - eslint-visitor-keys: 3.4.1 + eslint-visitor-keys: 3.4.2 dev: true - /@eslint-community/regexpp@4.5.1: - resolution: {integrity: sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==} + /@eslint-community/regexpp@4.6.2: + resolution: {integrity: sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} dev: true - /@eslint/eslintrc@2.1.0: - resolution: {integrity: sha512-Lj7DECXqIVCqnqjjHMPna4vn6GJcMgul/wuS0je9OZ9gsL0zzDpKPVtcG1HaDVc+9y+qgXneTeUMbCqXJNpH1A==} + /@eslint/eslintrc@2.1.1: + resolution: {integrity: sha512-9t7ZA7NGGK8ckelF0PQCfcxIUzs1Md5rrO6U/c+FIQNanea5UZC0wqKXH4vHBccmu4ZJgZ2idtPeW7+Q2npOEA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: ajv: 6.12.6 @@ -2418,14 +2417,21 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true - /@floating-ui/core@1.3.1: - resolution: {integrity: sha512-Bu+AMaXNjrpjh41znzHqaz3r2Nr8hHuHZT6V2LBKMhyMl0FgKA62PNYbqnfgmzOhoWZj70Zecisbo4H1rotP5g==} + /@floating-ui/core@1.4.1: + resolution: {integrity: sha512-jk3WqquEJRlcyu7997NtR5PibI+y5bi+LS3hPmguVClypenMsCY3CBa3LAQnozRCtCrYWSEtAdiskpamuJRFOQ==} + dependencies: + '@floating-ui/utils': 0.1.1 dev: false - /@floating-ui/dom@1.4.5: - resolution: {integrity: sha512-96KnRWkRnuBSSFbj0sFGwwOUd8EkiecINVl0O9wiZlZ64EkpyAOG3Xc2vKKNJmru0Z7RqWNymA+6b8OZqjgyyw==} + /@floating-ui/dom@1.5.1: + resolution: {integrity: sha512-KwvVcPSXg6mQygvA1TjbN/gh///36kKtllIF8SUm0qpFj8+rvYrpvlYdL1JoA71SHpDqgSSdGOSoQ0Mp3uY5aw==} dependencies: - '@floating-ui/core': 1.3.1 + '@floating-ui/core': 1.4.1 + '@floating-ui/utils': 0.1.1 + dev: false + + /@floating-ui/utils@0.1.1: + resolution: {integrity: sha512-m0G6wlnhm/AX0H12IOWtK8gASEMffnX08RtKkCgTdHb9JpHKGloI7icFfLg9ZmQeavcvR0PKmzxClyuFPSjKWw==} dev: false /@fontsource/inconsolata@5.0.5: @@ -2636,7 +2642,7 @@ packages: resolution: {integrity: sha512-E6s9hfQx125CfGXW5896s0ZtUEecTS69KvqkNDPxKomeZ/Y2rNsG90yO8K47uchXqKw5RhD/rCNcOJ2VODfQiw==} dependencies: '@types/json-schema': 7.0.12 - '@types/node': 20.4.2 + '@types/node': 20.4.8 fast-deep-equal: 3.1.3 openapi-typescript: 5.4.1 dev: true @@ -2650,11 +2656,11 @@ packages: engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} dependencies: cross-spawn: 7.0.3 - fast-glob: 3.3.0 + fast-glob: 3.3.1 is-glob: 4.0.3 open: 9.1.0 picocolors: 1.0.0 - tslib: 2.6.0 + tslib: 2.6.1 dev: true /@popperjs/core@2.11.8: @@ -2732,7 +2738,7 @@ packages: '@sentry/core': 7.61.0 '@sentry/types': 7.61.0 '@sentry/utils': 7.61.0 - tslib: 2.6.0 + tslib: 2.6.1 dev: false /@sentry/browser@7.61.0: @@ -2744,7 +2750,7 @@ packages: '@sentry/replay': 7.61.0 '@sentry/types': 7.61.0 '@sentry/utils': 7.61.0 - tslib: 2.6.0 + tslib: 2.6.1 dev: false /@sentry/cli@1.75.2: @@ -2770,7 +2776,7 @@ packages: dependencies: '@sentry/types': 7.61.0 '@sentry/utils': 7.61.0 - tslib: 2.6.0 + tslib: 2.6.1 dev: false /@sentry/integrations@7.61.0: @@ -2780,7 +2786,7 @@ packages: '@sentry/types': 7.61.0 '@sentry/utils': 7.61.0 localforage: 1.10.0 - tslib: 2.6.0 + tslib: 2.6.1 dev: false /@sentry/nextjs@7.61.0(next@13.4.2)(react@18.2.0)(webpack@5.88.2): @@ -2807,7 +2813,7 @@ packages: react: 18.2.0 rollup: 2.78.0 stacktrace-parser: 0.1.10 - tslib: 2.6.0 + tslib: 2.6.1 webpack: 5.88.2 transitivePeerDependencies: - encoding @@ -2825,7 +2831,7 @@ packages: cookie: 0.4.2 https-proxy-agent: 5.0.1 lru_map: 0.3.3 - tslib: 2.6.0 + tslib: 2.6.1 transitivePeerDependencies: - supports-color dev: false @@ -2841,7 +2847,7 @@ packages: '@sentry/utils': 7.61.0 hoist-non-react-statics: 3.3.2 react: 18.2.0 - tslib: 2.6.0 + tslib: 2.6.1 dev: false /@sentry/replay@7.61.0: @@ -2863,7 +2869,7 @@ packages: engines: {node: '>=8'} dependencies: '@sentry/types': 7.61.0 - tslib: 2.6.0 + tslib: 2.6.1 dev: false /@sentry/webpack-plugin@1.20.0: @@ -2897,7 +2903,7 @@ packages: /@swc/helpers@0.5.1: resolution: {integrity: sha512-sJ902EfIzn1Fa+qYmjdQqh8tPsoxyBz+8yBKC2HKUxyezKJFwPGOn7pv4WY6QuQW//ySQi5lJjA/ZT9sNWWNTg==} dependencies: - tslib: 2.6.0 + tslib: 2.6.1 dev: false /@t3-oss/env-core@0.3.1(typescript@5.0.4)(zod@3.21.4): @@ -3092,7 +3098,7 @@ packages: /@types/eslint-scope@3.7.4: resolution: {integrity: sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==} dependencies: - '@types/eslint': 8.44.1 + '@types/eslint': 8.44.2 '@types/estree': 1.0.1 /@types/eslint@8.37.0: @@ -3102,8 +3108,8 @@ packages: '@types/json-schema': 7.0.12 dev: true - /@types/eslint@8.44.1: - resolution: {integrity: sha512-XpNDc4Z5Tb4x+SW1MriMVeIsMoONHCkWFMkR/aPJbzEsxqHy+4Glu/BqTdPrApfDeMaXbtNh6bseNgl5KaWrSg==} + /@types/eslint@8.44.2: + resolution: {integrity: sha512-sdPRb9K6iL5XZOmBubg8yiFp5yS/JdUDQsq5e6h95km91MCYMuvp7mh1fjPEYUhvHepKpZOjnEaMBR4PxjWDzg==} dependencies: '@types/estree': 1.0.1 '@types/json-schema': 7.0.12 @@ -3156,23 +3162,23 @@ packages: /@types/lodash-es@4.17.8: resolution: {integrity: sha512-euY3XQcZmIzSy7YH5+Unb3b2X12Wtk54YWINBvvGQ5SmMvwb11JQskGsfkH/5HXK77Kr8GF0wkVDIxzAisWtog==} dependencies: - '@types/lodash': 4.14.195 + '@types/lodash': 4.14.196 dev: true /@types/lodash.clonedeep@4.5.7: resolution: {integrity: sha512-ccNqkPptFIXrpVqUECi60/DFxjNKsfoQxSQsgcBJCX/fuX1wgyQieojkcWH/KpE3xzLoWN/2k+ZeGqIN3paSvw==} dependencies: - '@types/lodash': 4.14.195 + '@types/lodash': 4.14.196 dev: false /@types/lodash.mergewith@4.6.7: resolution: {integrity: sha512-3m+lkO5CLRRYU0fhGRp7zbsGi6+BZj0uTVSwvcKU+nSlhjA9/QRNfuSGnD2mX6hQA7ZbmcCkzk5h4ZYGOtk14A==} dependencies: - '@types/lodash': 4.14.195 + '@types/lodash': 4.14.196 dev: false - /@types/lodash@4.14.195: - resolution: {integrity: sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg==} + /@types/lodash@4.14.196: + resolution: {integrity: sha512-22y3o88f4a94mKljsZcanlNWPzO0uBsBdzLAngf2tp533LzZcQzb6+eZPJ+vCTt+bqF2XnvT9gejTLsAcJAJyQ==} /@types/mime@1.3.2: resolution: {integrity: sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==} @@ -3200,11 +3206,11 @@ packages: /@types/node@18.16.0: resolution: {integrity: sha512-BsAaKhB+7X+H4GnSjGhJG9Qi8Tw+inU9nJDwmD5CgOmBLEI6ArdhikpLX7DjbjDRDTbqZzU2LSQNZg8WGPiSZQ==} - /@types/node@18.17.1: - resolution: {integrity: sha512-xlR1jahfizdplZYRU59JlUx9uzF1ARa8jbhM11ccpCJya8kvos5jwdm2ZAgxSCwOl0fq21svP18EVwPBXMQudw==} + /@types/node@18.17.3: + resolution: {integrity: sha512-2x8HWtFk0S99zqVQABU9wTpr8wPoaDHZUcAkoTKH+nL7kPv3WUI9cRi/Kk5Mz4xdqXSqTkKP7IWNoQQYCnDsTA==} - /@types/node@20.4.2: - resolution: {integrity: sha512-Dd0BYtWgnWJKwO1jkmTrzofjK2QXXcai0dmtzvIBhcA+RsG5h8R3xlyta0kGOZRNfL9GuRtb1knmPEhQrePCEw==} + /@types/node@20.4.8: + resolution: {integrity: sha512-0mHckf6D2DiIAzh8fM8f3HQCvMKDpK94YQ0DSVkfWTG9BZleYIWudw9cJxX8oCk9bM+vAkDyujDV6dmKHbvQpg==} dev: true /@types/parse-json@4.0.0: @@ -3308,7 +3314,7 @@ packages: typescript: optional: true dependencies: - '@eslint-community/regexpp': 4.5.1 + '@eslint-community/regexpp': 4.6.2 '@typescript-eslint/parser': 5.59.6(eslint@8.40.0)(typescript@5.0.4) '@typescript-eslint/scope-manager': 5.59.6 '@typescript-eslint/type-utils': 5.59.6(eslint@8.40.0)(typescript@5.0.4) @@ -3424,7 +3430,7 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: '@typescript-eslint/types': 5.59.6 - eslint-visitor-keys: 3.4.1 + eslint-visitor-keys: 3.4.2 dev: true /@vercel/og@0.5.9: @@ -3455,9 +3461,9 @@ packages: /@vitest/snapshot@0.33.0: resolution: {integrity: sha512-tJjrl//qAHbyHajpFvr8Wsk8DIOODEebTu7pgBrP07iOepR5jYkLFiqLq2Ltxv+r0uptUb4izv1J8XBOwKkVYA==} dependencies: - magic-string: 0.30.1 + magic-string: 0.30.2 pathe: 1.1.1 - pretty-format: 29.6.1 + pretty-format: 29.6.2 dev: true /@vitest/spy@0.33.0: @@ -3471,7 +3477,7 @@ packages: dependencies: diff-sequences: 29.4.3 loupe: 2.3.6 - pretty-format: 29.6.1 + pretty-format: 29.6.2 dev: true /@webassemblyjs/ast@1.11.6: @@ -3628,15 +3634,11 @@ packages: - supports-color dev: false - /agentkeepalive@4.3.0: - resolution: {integrity: sha512-7Epl1Blf4Sy37j4v9f9FjICCh4+KAQOyXgHEwlyBiAQLbhKdq/i2QQU3amQalS/wPhdPzDXPL5DMR5bkn+YeWg==} + /agentkeepalive@4.5.0: + resolution: {integrity: sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==} engines: {node: '>= 8.0.0'} dependencies: - debug: 4.3.4 - depd: 2.0.0 humanize-ms: 1.2.1 - transitivePeerDependencies: - - supports-color dev: false /ajv-keywords@3.5.2(ajv@6.12.6): @@ -3699,7 +3701,7 @@ packages: resolution: {integrity: sha512-xcLxITLe2HYa1cnYnwCjkOO1PqUHQpozB8x9AR0OgWN2woOBi5kSDVxKfd0b7sb1hw5qFeJhXm9H1nu3xSfLeQ==} engines: {node: '>=10'} dependencies: - tslib: 2.6.0 + tslib: 2.6.1 dev: false /aria-query@5.3.0: @@ -3735,6 +3737,17 @@ packages: engines: {node: '>=8'} dev: true + /array.prototype.findlastindex@1.2.2: + resolution: {integrity: sha512-tb5thFFlUcp7NdNF6/MpDk/1r/4awWG1FIz3YqDf+/zJSTezBb+/5WViH41obXULHVpDzoiCLpJ/ZO9YbJMsdw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.22.1 + es-shim-unscopables: 1.0.0 + get-intrinsic: 1.2.1 + dev: true + /array.prototype.flat@1.3.1: resolution: {integrity: sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==} engines: {node: '>= 0.4'} @@ -3798,14 +3811,14 @@ packages: resolution: {integrity: sha512-O0yuUDnZeQDL+ncNGlJ78BiO4jnYI3bvMsD5prT0/nsgijG/LpNBIr63gTjVTNsiGkgQhiyCShTgxt8oXOrklA==} engines: {node: '>=4'} dependencies: - tslib: 2.6.0 + tslib: 2.6.1 dev: false /ast-types@0.16.1: resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} engines: {node: '>=4'} dependencies: - tslib: 2.6.0 + tslib: 2.6.1 dev: false /asynckit@0.4.0: @@ -3856,7 +3869,7 @@ packages: dependencies: '@babel/runtime': 7.22.6 cosmiconfig: 6.0.0 - resolve: 1.22.2 + resolve: 1.22.4 dev: false /babel-plugin-macros@3.1.0: @@ -3865,7 +3878,7 @@ packages: dependencies: '@babel/runtime': 7.22.6 cosmiconfig: 7.1.0 - resolve: 1.22.2 + resolve: 1.22.4 dev: false /babel-plugin-syntax-jsx@6.18.0: @@ -3962,20 +3975,10 @@ packages: hasBin: true dependencies: caniuse-lite: 1.0.30001519 - electron-to-chromium: 1.4.482 + electron-to-chromium: 1.4.485 node-releases: 2.0.13 update-browserslist-db: 1.0.11(browserslist@4.21.10) - /browserslist@4.21.9: - resolution: {integrity: sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - dependencies: - caniuse-lite: 1.0.30001517 - electron-to-chromium: 1.4.465 - node-releases: 2.0.13 - update-browserslist-db: 1.0.11(browserslist@4.21.9) - /buffer-from@0.1.2: resolution: {integrity: sha512-RiWIenusJsmI2KcvqQABB83tLxCByE3upSP8QU3rJDMVFGPWLvPQJt/O1Su9moRWeH7d+Q2HYb68f6+v+tw2vg==} dev: false @@ -4029,9 +4032,6 @@ packages: resolution: {integrity: sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==} dev: false - /caniuse-lite@1.0.30001517: - resolution: {integrity: sha512-Vdhm5S11DaFVLlyiKu4hiUTkpZu+y1KA/rZZqVQfOD5YdDT/eQKlkt7NaE0WGOFgX32diqt9MiP9CAiFeRklaA==} - /caniuse-lite@1.0.30001519: resolution: {integrity: sha512-0QHgqR+Jv4bxHMp8kZ1Kn8CH55OikjKJ6JmKkZYP1F3D7w+lnFXF70nG5eNfsZS89jadi5Ywy5UCSKLAglIRkg==} @@ -4154,8 +4154,8 @@ packages: resolution: {integrity: sha512-m7pOT6CdLN7FuXUcpuz/8lfQ/L77x8SchHCF4G0RBTJO20Wzmhn5Sp4/5WsKy8OSpifBSUrmg83qEqaDHdyFuQ==} dependencies: inflation: 2.0.0 - qs: 6.11.0 - raw-body: 2.5.1 + qs: 6.11.2 + raw-body: 2.5.2 type-is: 1.6.18 dev: false @@ -4465,7 +4465,7 @@ packages: dependencies: bundle-name: 3.0.0 default-browser-id: 3.0.0 - execa: 7.1.1 + execa: 7.2.0 titleize: 3.0.0 dev: true @@ -4578,11 +4578,8 @@ packages: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} dev: false - /electron-to-chromium@1.4.465: - resolution: {integrity: sha512-XQcuHvEJRMU97UJ75e170mgcITZoz0lIyiaVjk6R+NMTJ8KBIvUHYd1779swgOppUlzxR+JsLpq59PumaXS1jQ==} - - /electron-to-chromium@1.4.482: - resolution: {integrity: sha512-h+UqpfmEr1Qkk0zp7ej/jid7CXoq4m4QzW6wNTb0ELJ/BZCpA4wgUylBIMGCe621tnr4l5VmoHjdoSx2lbnNJA==} + /electron-to-chromium@1.4.485: + resolution: {integrity: sha512-1ndQ5IBNEnFirPwvyud69GHL+31FkE09gH/CJ6m3KCbkx3i0EVOrjwz4UNxRmN9H8OVHbC6vMRZGN1yCvjSs9w==} /emoji-regex@10.2.1: resolution: {integrity: sha512-97g6QgOk8zlDRdgq1WxwgTMgEWGVAQvB5Fdpgc1MkNy56la5SKP9GsMXKDOdqwn90/41a8yPwIGk1Y6WVbeMQA==} @@ -4613,12 +4610,12 @@ packages: engines: {node: '>= 0.8'} dev: false - /engine.io-client@6.5.1: - resolution: {integrity: sha512-hE5wKXH8Ru4L19MbM1GgYV/2Qo54JSMh1rlJbfpa40bEWkCKNo3ol2eOtGmowcr+ysgbI7+SGL+by42Q3pt/Ng==} + /engine.io-client@6.5.2: + resolution: {integrity: sha512-CQZqbrpEYnrpGqC07a9dJDz4gePZUgTPMU3NKJPSeQOyw27Tst4Pl3FemKoFGAlHzgZmKjoRmiJvbWfhCXUlIg==} dependencies: '@socket.io/component-emitter': 3.1.0 debug: 4.3.4 - engine.io-parser: 5.1.0 + engine.io-parser: 5.2.1 ws: 8.11.0 xmlhttprequest-ssl: 2.0.0 transitivePeerDependencies: @@ -4627,14 +4624,14 @@ packages: - utf-8-validate dev: false - /engine.io-parser@5.1.0: - resolution: {integrity: sha512-enySgNiK5tyZFynt3z7iqBR+Bto9EVVVvDFuTT0ioHCGbzirZVGDGiQjZzEp8hWl6hd5FSVytJGuScX1C1C35w==} + /engine.io-parser@5.2.1: + resolution: {integrity: sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==} engines: {node: '>=10.0.0'} dev: false - /engine.io@6.5.1: - resolution: {integrity: sha512-mGqhI+D7YxS9KJMppR6Iuo37Ed3abhU8NdfgSvJSDUafQutrN+sPTncJYTyM9+tkhSmWodKtVYGPPHyXJEwEQA==} - engines: {node: '>=10.0.0'} + /engine.io@6.5.2: + resolution: {integrity: sha512-IXsMcGpw/xRfjra46sVZVHiSWo/nJ/3g1337q9KNXtS6YRzbW5yIzTCb9DjhrBe7r3GZQR0I4+nq+4ODk5g/cA==} + engines: {node: '>=10.2.0'} dependencies: '@types/cookie': 0.4.1 '@types/cors': 2.8.13 @@ -4644,7 +4641,7 @@ packages: cookie: 0.4.2 cors: 2.8.5 debug: 4.3.4 - engine.io-parser: 5.1.0 + engine.io-parser: 5.2.1 ws: 8.11.0 transitivePeerDependencies: - bufferutil @@ -4805,34 +4802,34 @@ packages: '@esbuild/win32-x64': 0.17.19 dev: false - /esbuild@0.18.14: - resolution: {integrity: sha512-uNPj5oHPYmj+ZhSQeYQVFZ+hAlJZbAGOmmILWIqrGvPVlNLbyOvU5Bu6Woi8G8nskcx0vwY0iFoMPrzT86Ko+w==} + /esbuild@0.18.18: + resolution: {integrity: sha512-UckDPWvdVJLNT0npk5AMTpVwGRQhS76rWFLmHwEtgNvWlR9sgVV1eyc/oeBtM86q9s8ABBLMmm0CwNxhVemOiw==} engines: {node: '>=12'} hasBin: true requiresBuild: true optionalDependencies: - '@esbuild/android-arm': 0.18.14 - '@esbuild/android-arm64': 0.18.14 - '@esbuild/android-x64': 0.18.14 - '@esbuild/darwin-arm64': 0.18.14 - '@esbuild/darwin-x64': 0.18.14 - '@esbuild/freebsd-arm64': 0.18.14 - '@esbuild/freebsd-x64': 0.18.14 - '@esbuild/linux-arm': 0.18.14 - '@esbuild/linux-arm64': 0.18.14 - '@esbuild/linux-ia32': 0.18.14 - '@esbuild/linux-loong64': 0.18.14 - '@esbuild/linux-mips64el': 0.18.14 - '@esbuild/linux-ppc64': 0.18.14 - '@esbuild/linux-riscv64': 0.18.14 - '@esbuild/linux-s390x': 0.18.14 - '@esbuild/linux-x64': 0.18.14 - '@esbuild/netbsd-x64': 0.18.14 - '@esbuild/openbsd-x64': 0.18.14 - '@esbuild/sunos-x64': 0.18.14 - '@esbuild/win32-arm64': 0.18.14 - '@esbuild/win32-ia32': 0.18.14 - '@esbuild/win32-x64': 0.18.14 + '@esbuild/android-arm': 0.18.18 + '@esbuild/android-arm64': 0.18.18 + '@esbuild/android-x64': 0.18.18 + '@esbuild/darwin-arm64': 0.18.18 + '@esbuild/darwin-x64': 0.18.18 + '@esbuild/freebsd-arm64': 0.18.18 + '@esbuild/freebsd-x64': 0.18.18 + '@esbuild/linux-arm': 0.18.18 + '@esbuild/linux-arm64': 0.18.18 + '@esbuild/linux-ia32': 0.18.18 + '@esbuild/linux-loong64': 0.18.18 + '@esbuild/linux-mips64el': 0.18.18 + '@esbuild/linux-ppc64': 0.18.18 + '@esbuild/linux-riscv64': 0.18.18 + '@esbuild/linux-s390x': 0.18.18 + '@esbuild/linux-x64': 0.18.18 + '@esbuild/netbsd-x64': 0.18.18 + '@esbuild/openbsd-x64': 0.18.18 + '@esbuild/sunos-x64': 0.18.18 + '@esbuild/win32-arm64': 0.18.18 + '@esbuild/win32-ia32': 0.18.18 + '@esbuild/win32-x64': 0.18.18 dev: true /escalade@3.1.1: @@ -4864,11 +4861,11 @@ packages: '@rushstack/eslint-patch': 1.3.2 '@typescript-eslint/parser': 5.59.6(eslint@8.40.0)(typescript@5.0.4) eslint: 8.40.0 - eslint-import-resolver-node: 0.3.7 - eslint-import-resolver-typescript: 3.5.5(@typescript-eslint/parser@5.59.6)(eslint-import-resolver-node@0.3.7)(eslint-plugin-import@2.27.5)(eslint@8.40.0) - eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.59.6)(eslint-import-resolver-typescript@3.5.5)(eslint@8.40.0) + eslint-import-resolver-node: 0.3.8 + eslint-import-resolver-typescript: 3.5.5(@typescript-eslint/parser@5.59.6)(eslint-import-resolver-node@0.3.8)(eslint-plugin-import@2.28.0)(eslint@8.40.0) + eslint-plugin-import: 2.28.0(@typescript-eslint/parser@5.59.6)(eslint-import-resolver-typescript@3.5.5)(eslint@8.40.0) eslint-plugin-jsx-a11y: 6.7.1(eslint@8.40.0) - eslint-plugin-react: 7.32.2(eslint@8.40.0) + eslint-plugin-react: 7.33.1(eslint@8.40.0) eslint-plugin-react-hooks: 4.6.0(eslint@8.40.0) typescript: 5.0.4 transitivePeerDependencies: @@ -4876,17 +4873,17 @@ packages: - supports-color dev: true - /eslint-import-resolver-node@0.3.7: - resolution: {integrity: sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==} + /eslint-import-resolver-node@0.3.8: + resolution: {integrity: sha512-tEe+Pok22qIGaK3KoMP+N96GVDS66B/zreoVVmiavLvRUEmGRtvb4B8wO9jwnb8d2lvHtrkhZ7UD73dWBVnf/Q==} dependencies: debug: 3.2.7 - is-core-module: 2.12.1 - resolve: 1.22.2 + is-core-module: 2.13.0 + resolve: 1.22.4 transitivePeerDependencies: - supports-color dev: true - /eslint-import-resolver-typescript@3.5.5(@typescript-eslint/parser@5.59.6)(eslint-import-resolver-node@0.3.7)(eslint-plugin-import@2.27.5)(eslint@8.40.0): + /eslint-import-resolver-typescript@3.5.5(@typescript-eslint/parser@5.59.6)(eslint-import-resolver-node@0.3.8)(eslint-plugin-import@2.28.0)(eslint@8.40.0): resolution: {integrity: sha512-TdJqPHs2lW5J9Zpe17DZNQuDnox4xo2o+0tE7Pggain9Rbc19ik8kFtXdxZ250FVx2kF4vlt2RSf4qlUpG7bhw==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: @@ -4896,11 +4893,11 @@ packages: debug: 4.3.4 enhanced-resolve: 5.15.0 eslint: 8.40.0 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.59.6)(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.5.5)(eslint@8.40.0) - eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.59.6)(eslint-import-resolver-typescript@3.5.5)(eslint@8.40.0) + eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.59.6)(eslint-import-resolver-node@0.3.8)(eslint-import-resolver-typescript@3.5.5)(eslint@8.40.0) + eslint-plugin-import: 2.28.0(@typescript-eslint/parser@5.59.6)(eslint-import-resolver-typescript@3.5.5)(eslint@8.40.0) get-tsconfig: 4.6.2 globby: 13.2.2 - is-core-module: 2.12.1 + is-core-module: 2.13.0 is-glob: 4.0.3 synckit: 0.8.5 transitivePeerDependencies: @@ -4910,7 +4907,7 @@ packages: - supports-color dev: true - /eslint-module-utils@2.8.0(@typescript-eslint/parser@5.59.6)(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.5.5)(eslint@8.40.0): + /eslint-module-utils@2.8.0(@typescript-eslint/parser@5.59.6)(eslint-import-resolver-node@0.3.8)(eslint-import-resolver-typescript@3.5.5)(eslint@8.40.0): resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} engines: {node: '>=4'} peerDependencies: @@ -4934,14 +4931,14 @@ packages: '@typescript-eslint/parser': 5.59.6(eslint@8.40.0)(typescript@5.0.4) debug: 3.2.7 eslint: 8.40.0 - eslint-import-resolver-node: 0.3.7 - eslint-import-resolver-typescript: 3.5.5(@typescript-eslint/parser@5.59.6)(eslint-import-resolver-node@0.3.7)(eslint-plugin-import@2.27.5)(eslint@8.40.0) + eslint-import-resolver-node: 0.3.8 + eslint-import-resolver-typescript: 3.5.5(@typescript-eslint/parser@5.59.6)(eslint-import-resolver-node@0.3.8)(eslint-plugin-import@2.28.0)(eslint@8.40.0) transitivePeerDependencies: - supports-color dev: true - /eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.59.6)(eslint-import-resolver-typescript@3.5.5)(eslint@8.40.0): - resolution: {integrity: sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==} + /eslint-plugin-import@2.28.0(@typescript-eslint/parser@5.59.6)(eslint-import-resolver-typescript@3.5.5)(eslint@8.40.0): + resolution: {integrity: sha512-B8s/n+ZluN7sxj9eUf7/pRFERX0r5bnFA2dCaLHy2ZeaQEAz0k+ZZkFWRFHJAqxfxQDx6KLv9LeIki7cFdwW+Q==} engines: {node: '>=4'} peerDependencies: '@typescript-eslint/parser': '*' @@ -4952,19 +4949,22 @@ packages: dependencies: '@typescript-eslint/parser': 5.59.6(eslint@8.40.0)(typescript@5.0.4) array-includes: 3.1.6 + array.prototype.findlastindex: 1.2.2 array.prototype.flat: 1.3.1 array.prototype.flatmap: 1.3.1 debug: 3.2.7 doctrine: 2.1.0 eslint: 8.40.0 - eslint-import-resolver-node: 0.3.7 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.59.6)(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.5.5)(eslint@8.40.0) + eslint-import-resolver-node: 0.3.8 + eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.59.6)(eslint-import-resolver-node@0.3.8)(eslint-import-resolver-typescript@3.5.5)(eslint@8.40.0) has: 1.0.3 - is-core-module: 2.12.1 + is-core-module: 2.13.0 is-glob: 4.0.3 minimatch: 3.1.2 + object.fromentries: 2.0.6 + object.groupby: 1.0.0 object.values: 1.1.6 - resolve: 1.22.2 + resolve: 1.22.4 semver: 6.3.1 tsconfig-paths: 3.14.2 transitivePeerDependencies: @@ -4990,7 +4990,7 @@ packages: emoji-regex: 9.2.2 eslint: 8.40.0 has: 1.0.3 - jsx-ast-utils: 3.3.4 + jsx-ast-utils: 3.3.5 language-tags: 1.0.5 minimatch: 3.1.2 object.entries: 1.1.6 @@ -5007,8 +5007,8 @@ packages: eslint: 8.40.0 dev: true - /eslint-plugin-react@7.32.2(eslint@8.40.0): - resolution: {integrity: sha512-t2fBMa+XzonrrNkyVirzKlvn5RXzzPwRHtMvLAtVZrt8oxgnTQaYbU6SXTOO1mwQgp1y5+toMSKInnzGr0Knqg==} + /eslint-plugin-react@7.33.1(eslint@8.40.0): + resolution: {integrity: sha512-L093k0WAMvr6VhNwReB8VgOq5s2LesZmrpPdKz/kZElQDzqS7G7+DnKoqT+w4JwuiGeAhAvHO0fvy0Eyk4ejDA==} engines: {node: '>=4'} peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 @@ -5019,7 +5019,7 @@ packages: doctrine: 2.1.0 eslint: 8.40.0 estraverse: 5.3.0 - jsx-ast-utils: 3.3.4 + jsx-ast-utils: 3.3.5 minimatch: 3.1.2 object.entries: 1.1.6 object.fromentries: 2.0.6 @@ -5058,16 +5058,16 @@ packages: esrecurse: 4.3.0 estraverse: 4.3.0 - /eslint-scope@7.2.1: - resolution: {integrity: sha512-CvefSOsDdaYYvxChovdrPo/ZGt8d5lrJWleAc1diXRKhHGiTYEI26cvo8Kle/wGnsizoCJjK73FMg1/IkIwiNA==} + /eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: esrecurse: 4.3.0 estraverse: 5.3.0 dev: true - /eslint-visitor-keys@3.4.1: - resolution: {integrity: sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==} + /eslint-visitor-keys@3.4.2: + resolution: {integrity: sha512-8drBzUEyZ2llkpCA67iYrgEssKDUu68V8ChqqOfFupIaG/LCVPUT+CoGJpT77zJprs4T/W7p07LP7zAIMuweVw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true @@ -5077,8 +5077,8 @@ packages: hasBin: true dependencies: '@eslint-community/eslint-utils': 4.4.0(eslint@8.40.0) - '@eslint-community/regexpp': 4.5.1 - '@eslint/eslintrc': 2.1.0 + '@eslint-community/regexpp': 4.6.2 + '@eslint/eslintrc': 2.1.1 '@eslint/js': 8.40.0 '@humanwhocodes/config-array': 0.11.10 '@humanwhocodes/module-importer': 1.0.1 @@ -5089,8 +5089,8 @@ packages: debug: 4.3.4 doctrine: 3.0.0 escape-string-regexp: 4.0.0 - eslint-scope: 7.2.1 - eslint-visitor-keys: 3.4.1 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.2 espree: 9.6.1 esquery: 1.5.0 esutils: 2.0.3 @@ -5105,7 +5105,7 @@ packages: imurmurhash: 0.1.4 is-glob: 4.0.3 is-path-inside: 3.0.3 - js-sdsl: 4.4.1 + js-sdsl: 4.4.2 js-yaml: 4.1.0 json-stable-stringify-without-jsonify: 1.0.1 levn: 0.4.1 @@ -5126,7 +5126,7 @@ packages: dependencies: acorn: 8.10.0 acorn-jsx: 5.3.2(acorn@8.10.0) - eslint-visitor-keys: 3.4.1 + eslint-visitor-keys: 3.4.2 dev: true /esprima@4.0.1: @@ -5201,8 +5201,8 @@ packages: strip-final-newline: 2.0.0 dev: true - /execa@7.1.1: - resolution: {integrity: sha512-wH0eMf/UXckdUYnO21+HDztteVv05rq2GXksxT4fCGeHkBhw1DROXh40wcjMcRqDOWE7iPJ4n3M7e2+YFP+76Q==} + /execa@7.2.0: + resolution: {integrity: sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==} engines: {node: ^14.18.0 || ^16.14.0 || >=18.0.0} dependencies: cross-spawn: 7.0.3 @@ -5264,8 +5264,8 @@ packages: /fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - /fast-glob@3.3.0: - resolution: {integrity: sha512-ChDuvbOypPuNjO8yIDf36x7BlZX1smcUMTTcyoIjycexOxd6DFsKsg21qVBzEmr3G7fUKIRy2/psii+CIUt7FA==} + /fast-glob@3.3.1: + resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} engines: {node: '>=8.6.0'} dependencies: '@nodelib/fs.stat': 2.0.5 @@ -5358,7 +5358,7 @@ packages: resolution: {integrity: sha512-KSuV3ur4gf2KqMNoZx3nXNVhqCkn42GuTYCX4tXPEwf0MjpFQmNMiN6m7dXaUXgIoivL6/65agoUMg4RLS0Vbg==} engines: {node: '>=10'} dependencies: - tslib: 2.6.0 + tslib: 2.6.1 dev: false /follow-redirects@1.15.2: @@ -5429,7 +5429,7 @@ packages: dependencies: react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - tslib: 2.6.0 + tslib: 2.6.1 optionalDependencies: '@emotion/is-prop-valid': 0.8.8 dev: false @@ -5611,7 +5611,7 @@ packages: dependencies: array-union: 2.1.0 dir-glob: 3.0.1 - fast-glob: 3.3.0 + fast-glob: 3.3.1 ignore: 5.2.4 merge2: 1.4.1 slash: 3.0.0 @@ -5622,7 +5622,7 @@ packages: engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} dependencies: dir-glob: 3.0.1 - fast-glob: 3.3.0 + fast-glob: 3.3.1 ignore: 5.2.4 merge2: 1.4.1 slash: 4.0.0 @@ -5661,8 +5661,8 @@ packages: chokidar: 3.5.3 cosmiconfig: 7.1.0 json5: 2.2.3 - pg: 8.11.1 - tslib: 2.6.0 + pg: 8.11.2 + tslib: 2.6.1 yargs: 16.2.0 transitivePeerDependencies: - pg-native @@ -5676,7 +5676,7 @@ packages: destr: 2.0.1 iron-webcrypto: 0.7.1 radix3: 1.0.1 - ufo: 1.1.2 + ufo: 1.2.0 uncrypto: 0.1.3 dev: false @@ -5928,8 +5928,8 @@ packages: resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} engines: {node: '>= 0.4'} - /is-core-module@2.12.1: - resolution: {integrity: sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==} + /is-core-module@2.13.0: + resolution: {integrity: sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==} dependencies: has: 1.0.3 @@ -6116,7 +6116,7 @@ packages: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} engines: {node: '>= 10.13.0'} dependencies: - '@types/node': 18.17.1 + '@types/node': 18.17.3 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -6124,8 +6124,8 @@ packages: resolution: {integrity: sha512-j8GhLiKmUAh+dsFXlX1aJCbt5KMibuKb+d7j1JaOJG6s2UjX1PQlW+OKB/sD4a/5ZYF4RcmYmLSndOoU3Lt/3g==} dev: false - /js-sdsl@4.4.1: - resolution: {integrity: sha512-6Gsx8R0RucyePbWqPssR8DyfuXmLBooYN5cZFZKjHGnQuaf7pEzhtpceagJxVu4LqhYY5EYA7nko3FmeHZ1KbA==} + /js-sdsl@4.4.2: + resolution: {integrity: sha512-dwXFwByc/ajSV6m5bcKAPwe4yDDF6D614pxmIi5odytzxRlwqF6nwoiCek80Ixc7Cvma5awClxrzFtxCQvcM8w==} dev: true /js-tiktoken@1.0.7: @@ -6158,7 +6158,7 @@ packages: dependencies: '@bcherny/json-schema-ref-parser': 10.0.5-fork '@types/json-schema': 7.0.12 - '@types/lodash': 4.14.195 + '@types/lodash': 4.14.196 '@types/prettier': 2.7.3 cli-color: 2.0.3 get-stdin: 8.0.0 @@ -6203,8 +6203,8 @@ packages: resolution: {integrity: sha512-S6cATIPVv1z0IlxdN+zUk5EPjkGCdnhN4wVSBlvoUO1tOLJootbo9CquNJmbIh4yikWHiUedhRYrNPn1arpEmQ==} dev: false - /jsx-ast-utils@3.3.4: - resolution: {integrity: sha512-fX2TVdCViod6HwKEtSWGHs57oFhVfCMwieb9PuRDgjDPh5XeqJiHFFFJCHxU5cnTc3Bu/GRL+kPiFmw8XWOfKw==} + /jsx-ast-utils@3.3.5: + resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} dependencies: array-includes: 3.1.6 @@ -6350,8 +6350,8 @@ packages: '@jridgewell/sourcemap-codec': 1.4.15 dev: false - /magic-string@0.30.1: - resolution: {integrity: sha512-mbVKXPmS0z0G4XqFDCTllmDQ6coZzn94aMlb0o/A4HEHJCKcanlDZwYJgwnkmgD3jyWhUgj9VsPrfd972yPffA==} + /magic-string@0.30.2: + resolution: {integrity: sha512-lNZdu7pewtq/ZvWUp9Wpf/x7WzMTsR26TWV03BRZrXFsv+BI6dy8RAiKgm1uM/kyR0rCfUcqvOlXKG66KhIGug==} engines: {node: '>=12'} dependencies: '@jridgewell/sourcemap-codec': 1.4.15 @@ -6482,7 +6482,7 @@ packages: acorn: 8.10.0 pathe: 1.1.1 pkg-types: 1.0.3 - ufo: 1.1.2 + ufo: 1.2.0 dev: true /monaco-editor@0.40.0: @@ -6568,7 +6568,7 @@ packages: dependencies: next: 13.4.2(@babel/core@7.22.9)(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 - tslib: 2.6.0 + tslib: 2.6.1 use-query-params: 2.2.1(react-dom@18.2.0)(react@18.2.0) dev: false @@ -6600,7 +6600,7 @@ packages: '@next/env': 13.4.2 '@swc/helpers': 0.5.1 busboy: 1.6.0 - caniuse-lite: 1.0.30001517 + caniuse-lite: 1.0.30001519 postcss: 8.4.14 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) @@ -6755,6 +6755,15 @@ packages: es-abstract: 1.22.1 dev: true + /object.groupby@1.0.0: + resolution: {integrity: sha512-70MWG6NfRH9GnbZOikuhPPYzpUpof9iW2J9E4dW7FXTqPNb6rllE6u39SKwwiNh8lCwX3DDb5OgcKGiEBrTTyw==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.22.1 + get-intrinsic: 1.2.1 + dev: true + /object.hasown@1.1.2: resolution: {integrity: sha512-B5UIT3J1W+WuWIU55h0mjlwaqxiE5vYENJXIXZ4VFe05pNYrkKuK0U/6aFcb0pKywYJh7IhfoqUfKVmrJJHZHw==} dependencies: @@ -6822,14 +6831,13 @@ packages: '@types/node': 18.16.0 '@types/node-fetch': 2.6.4 abort-controller: 3.0.0 - agentkeepalive: 4.3.0 + agentkeepalive: 4.5.0 digest-fetch: 1.3.0 form-data-encoder: 1.7.2 formdata-node: 4.4.1 node-fetch: 2.6.12 transitivePeerDependencies: - encoding - - supports-color dev: false /openapi-types@12.1.3: @@ -6845,7 +6853,7 @@ packages: mime: 3.0.0 prettier: 2.8.8 tiny-glob: 0.2.9 - undici: 5.22.1 + undici: 5.23.0 yargs-parser: 21.1.1 dev: true @@ -6854,10 +6862,10 @@ packages: hasBin: true dependencies: ansi-colors: 4.1.3 - fast-glob: 3.3.0 + fast-glob: 3.3.1 js-yaml: 4.1.0 supports-color: 9.4.0 - undici: 5.22.1 + undici: 5.23.0 yargs-parser: 21.1.1 dev: true @@ -6994,8 +7002,8 @@ packages: dev: false optional: true - /pg-connection-string@2.6.1: - resolution: {integrity: sha512-w6ZzNu6oMmIzEAYVw+RLK0+nqHPt8K3ZnknKi+g48Ak2pr3dtljJW3o+D/n2zzCG07Zoe9VOX3aiKpj+BN0pjg==} + /pg-connection-string@2.6.2: + resolution: {integrity: sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==} dev: false /pg-int8@1.0.1: @@ -7008,12 +7016,12 @@ packages: engines: {node: '>=4'} dev: false - /pg-pool@3.6.1(pg@8.11.1): + /pg-pool@3.6.1(pg@8.11.2): resolution: {integrity: sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==} peerDependencies: pg: '>=8.0' dependencies: - pg: 8.11.1 + pg: 8.11.2 dev: false /pg-protocol@1.6.0: @@ -7044,8 +7052,8 @@ packages: postgres-range: 1.1.3 dev: false - /pg@8.11.1: - resolution: {integrity: sha512-utdq2obft07MxaDg0zBJI+l/M3mBRfIpEN3iSemsz0G5F2/VXx+XzqF4oxrbIZXQxt2AZzIUzyVg/YM6xOP/WQ==} + /pg@8.11.2: + resolution: {integrity: sha512-l4rmVeV8qTIrrPrIR3kZQqBgSN93331s9i6wiUiLOSk0Q7PmUxZD/m1rQI622l3NfqBby9Ar5PABfS/SulfieQ==} engines: {node: '>= 8.0.0'} peerDependencies: pg-native: '>=3.0.1' @@ -7055,8 +7063,8 @@ packages: dependencies: buffer-writer: 2.0.0 packet-reader: 1.0.0 - pg-connection-string: 2.6.1 - pg-pool: 3.6.1(pg@8.11.1) + pg-connection-string: 2.6.2 + pg-pool: 3.6.1(pg@8.11.2) pg-protocol: 1.6.0 pg-types: 2.2.0 pgpass: 1.0.5 @@ -7103,8 +7111,8 @@ packages: source-map-js: 1.0.2 dev: false - /postcss@8.4.26: - resolution: {integrity: sha512-jrXHFF8iTloAenySjM/ob3gSj7pCu0Ji49hnjqzsgSRa50hkWCKD0HQ+gMNJkW38jBI68MpAAg7ZWwHwX8NMMw==} + /postcss@8.4.27: + resolution: {integrity: sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==} engines: {node: ^10 || ^12 || >=14} dependencies: nanoid: 3.3.6 @@ -7205,8 +7213,8 @@ packages: hasBin: true dev: false - /pretty-format@29.6.1: - resolution: {integrity: sha512-7jRj+yXO0W7e4/tSJKoR7HRIHLPPjtNaUGG2xxKQnGvPNRkgWcQ0AZX6P4KBRJN4FcTBWb3sa7DVUJmocYuoog==} + /pretty-format@29.6.2: + resolution: {integrity: sha512-1q0oC8eRveTg5nnBEWMXAU2qpv65Gnuf2eCQzSjxpWFkPaPARwqZZDGuNE0zPAZfTCHzIk3A8dIjwlQKKLphyg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/schemas': 29.6.0 @@ -7281,6 +7289,13 @@ packages: side-channel: 1.0.4 dev: false + /qs@6.11.2: + resolution: {integrity: sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==} + engines: {node: '>=0.6'} + dependencies: + side-channel: 1.0.4 + dev: false + /queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} dev: true @@ -7309,6 +7324,16 @@ packages: unpipe: 1.0.0 dev: false + /raw-body@2.5.2: + resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} + engines: {node: '>= 0.8'} + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + dev: false + /raw-loader@4.0.2(webpack@5.88.2): resolution: {integrity: sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA==} engines: {node: '>= 10.13.0'} @@ -7325,7 +7350,7 @@ packages: dependencies: '@babel/runtime': 7.22.6 '@types/base16': 1.0.2 - '@types/lodash': 4.14.195 + '@types/lodash': 4.14.196 base16: 1.0.0 color: 3.2.1 csstype: 3.1.2 @@ -7422,7 +7447,7 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: '@babel/runtime': 7.22.6 - '@types/lodash': 4.14.195 + '@types/lodash': 4.14.196 '@types/react': 18.2.6 react: 18.2.0 react-base16-styling: 0.9.1 @@ -7441,7 +7466,7 @@ packages: '@types/react': 18.2.6 react: 18.2.0 react-style-singleton: 2.2.1(@types/react@18.2.6)(react@18.2.0) - tslib: 2.6.0 + tslib: 2.6.1 dev: false /react-remove-scroll@2.5.6(@types/react@18.2.6)(react@18.2.0): @@ -7458,7 +7483,7 @@ packages: react: 18.2.0 react-remove-scroll-bar: 2.3.4(@types/react@18.2.6)(react@18.2.0) react-style-singleton: 2.2.1(@types/react@18.2.6)(react@18.2.0) - tslib: 2.6.0 + tslib: 2.6.1 use-callback-ref: 1.3.0(@types/react@18.2.6)(react@18.2.0) use-sidecar: 1.1.2(@types/react@18.2.6)(react@18.2.0) dev: false @@ -7472,7 +7497,7 @@ packages: '@babel/runtime': 7.22.6 '@emotion/cache': 11.11.0 '@emotion/react': 11.11.1(@types/react@18.2.6)(react@18.2.0) - '@floating-ui/dom': 1.4.5 + '@floating-ui/dom': 1.5.1 '@types/react-transition-group': 4.4.6 memoize-one: 6.0.0 prop-types: 15.8.1 @@ -7506,7 +7531,7 @@ packages: get-nonce: 1.0.1 invariant: 2.2.4 react: 18.2.0 - tslib: 2.6.0 + tslib: 2.6.1 dev: false /react-syntax-highlighter@15.5.0(react@18.2.0): @@ -7593,7 +7618,7 @@ packages: ast-types: 0.16.1 esprima: 4.0.1 source-map: 0.6.1 - tslib: 2.6.0 + tslib: 2.6.1 dev: false /refractor@3.6.0: @@ -7633,11 +7658,11 @@ packages: /resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} - /resolve@1.22.2: - resolution: {integrity: sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==} + /resolve@1.22.4: + resolution: {integrity: sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==} hasBin: true dependencies: - is-core-module: 2.12.1 + is-core-module: 2.13.0 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 @@ -7645,7 +7670,7 @@ packages: resolution: {integrity: sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ==} hasBin: true dependencies: - is-core-module: 2.12.1 + is-core-module: 2.13.0 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 dev: true @@ -7670,8 +7695,8 @@ packages: fsevents: 2.3.2 dev: false - /rollup@3.26.3: - resolution: {integrity: sha512-7Tin0C8l86TkpcMtXvQu6saWH93nhG3dGQ1/+l5V2TDMceTxO7kDiK6GzbfLWNNxqJXm591PcEZUozZm51ogwQ==} + /rollup@3.27.2: + resolution: {integrity: sha512-YGwmHf7h2oUHkVBT248x0yt6vZkYQ3/rvE5iQuVBh3WO8GcJ6BNeOkpoX1yMHIiBm18EMLjBPIoUDkhgnyxGOQ==} engines: {node: '>=14.18.0', npm: '>=8.0.0'} hasBin: true optionalDependencies: @@ -7698,7 +7723,7 @@ packages: /rxjs@7.8.1: resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} dependencies: - tslib: 2.6.0 + tslib: 2.6.1 dev: false /safe-array-concat@1.0.0: @@ -7880,7 +7905,7 @@ packages: dependencies: '@socket.io/component-emitter': 3.1.0 debug: 4.3.4 - engine.io-client: 6.5.1 + engine.io-client: 6.5.2 socket.io-parser: 4.2.4 transitivePeerDependencies: - bufferutil @@ -7906,7 +7931,7 @@ packages: base64id: 2.0.0 cors: 2.8.5 debug: 4.3.4 - engine.io: 6.5.1 + engine.io: 6.5.2 socket.io-adapter: 2.5.2 socket.io-parser: 4.2.4 transitivePeerDependencies: @@ -8062,8 +8087,8 @@ packages: engines: {node: '>=8'} dev: true - /strip-literal@1.0.1: - resolution: {integrity: sha512-QZTsipNpa2Ppr6v1AmJHESqJ3Uz247MUS0OjrnnZjFAvEoWqxuyFuXn2xLgMtRnijJShAa1HL0gtJyUs7u7n3Q==} + /strip-literal@1.3.0: + resolution: {integrity: sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==} dependencies: acorn: 8.10.0 dev: true @@ -8129,7 +8154,7 @@ packages: engines: {node: ^14.18.0 || >=16.0.0} dependencies: '@pkgr/utils': 2.4.2 - tslib: 2.6.0 + tslib: 2.6.1 dev: true /tapable@2.2.1: @@ -8312,8 +8337,8 @@ packages: resolution: {integrity: sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==} dev: false - /tslib@2.6.0: - resolution: {integrity: sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==} + /tslib@2.6.1: + resolution: {integrity: sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==} /tsutils@3.21.0(typescript@5.0.4): resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} @@ -8422,8 +8447,8 @@ packages: engines: {node: '>=12.20'} hasBin: true - /ufo@1.1.2: - resolution: {integrity: sha512-TrY6DsjTQQgyS3E3dBaOXf0TpPD8u9FVrVYmKVegJuFw51n/YB9XPt+U6ydzFG5ZIN7+DIjPbNmXoBj9esYhgQ==} + /ufo@1.2.0: + resolution: {integrity: sha512-RsPyTbqORDNDxqAdQPQBpgqhWle1VcTSou/FraClYlHf6TZnQcGslpLcAphNR+sQW4q5lLWLbOsRlh9j24baQg==} /unbox-primitive@1.0.2: resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} @@ -8438,8 +8463,8 @@ packages: resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} dev: false - /undici@5.22.1: - resolution: {integrity: sha512-Ji2IJhFXZY0x/0tVBXeQwgPlLWw13GVzpsWPQ3rV50IFMMof2I55PZZxtm4P6iNq+L5znYN9nSTAq0ZyE6lSJw==} + /undici@5.23.0: + resolution: {integrity: sha512-1D7w+fvRsqlQ9GscLBwcAJinqcZGHUKjbOmXdlE/v8BvEGXjeWAax+341q44EuTcHXXnfyKNbKRq4Lg7OzhMmg==} engines: {node: '>=14.0'} dependencies: busboy: 1.6.0 @@ -8472,16 +8497,6 @@ packages: escalade: 3.1.1 picocolors: 1.0.0 - /update-browserslist-db@1.0.11(browserslist@4.21.9): - resolution: {integrity: sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==} - hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' - dependencies: - browserslist: 4.21.9 - escalade: 3.1.1 - picocolors: 1.0.0 - /uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} dependencies: @@ -8499,7 +8514,7 @@ packages: dependencies: '@types/react': 18.2.6 react: 18.2.0 - tslib: 2.6.0 + tslib: 2.6.1 dev: false /use-composed-ref@1.3.0(react@18.2.0): @@ -8568,7 +8583,7 @@ packages: '@types/react': 18.2.6 detect-node-es: 1.1.0 react: 18.2.0 - tslib: 2.6.0 + tslib: 2.6.1 dev: false /use-sync-external-store@1.2.0(react@18.2.0): @@ -8623,7 +8638,7 @@ packages: mlly: 1.4.0 pathe: 1.1.1 picocolors: 1.0.0 - vite: 4.4.4(@types/node@18.16.0) + vite: 4.4.8(@types/node@18.16.0) transitivePeerDependencies: - '@types/node' - less @@ -8651,8 +8666,8 @@ packages: - typescript dev: false - /vite@4.4.4(@types/node@18.16.0): - resolution: {integrity: sha512-4mvsTxjkveWrKDJI70QmelfVqTm+ihFAb6+xf4sjEU2TmUCTlVX87tmg/QooPEMQb/lM9qGHT99ebqPziEd3wg==} + /vite@4.4.8(@types/node@18.16.0): + resolution: {integrity: sha512-LONawOUUjxQridNWGQlNizfKH89qPigK36XhMI7COMGztz8KNY0JHim7/xDd71CZwGT4HtSRgI7Hy+RlhG0Gvg==} engines: {node: ^14.18.0 || >=16.0.0} hasBin: true peerDependencies: @@ -8680,9 +8695,9 @@ packages: optional: true dependencies: '@types/node': 18.16.0 - esbuild: 0.18.14 - postcss: 8.4.26 - rollup: 3.26.3 + esbuild: 0.18.18 + postcss: 8.4.27 + rollup: 3.27.2 optionalDependencies: fsevents: 2.3.2 dev: true @@ -8732,14 +8747,14 @@ packages: chai: 4.3.7 debug: 4.3.4 local-pkg: 0.4.3 - magic-string: 0.30.1 + magic-string: 0.30.2 pathe: 1.1.1 picocolors: 1.0.0 std-env: 3.3.3 - strip-literal: 1.0.1 + strip-literal: 1.3.0 tinybench: 2.5.0 tinypool: 0.6.0 - vite: 4.4.4(@types/node@18.16.0) + vite: 4.4.8(@types/node@18.16.0) vite-node: 0.33.0(@types/node@18.16.0) why-is-node-running: 2.2.2 transitivePeerDependencies: diff --git a/app/src/server/api/root.router.ts b/app/src/server/api/root.router.ts index 6ec1d8e..9ff807f 100644 --- a/app/src/server/api/root.router.ts +++ b/app/src/server/api/root.router.ts @@ -8,6 +8,7 @@ import { evaluationsRouter } from "./routers/evaluations.router"; import { worldChampsRouter } from "./routers/worldChamps.router"; import { datasetsRouter } from "./routers/datasets.router"; import { datasetEntries } from "./routers/datasetEntries.router"; +import { externalApiRouter } from "./routers/externalApi.router"; /** * This is the primary router for your server. @@ -24,6 +25,7 @@ export const appRouter = createTRPCRouter({ worldChamps: worldChampsRouter, datasets: datasetsRouter, datasetEntries: datasetEntries, + externalApi: externalApiRouter, }); // export type definition of API diff --git a/app/src/server/api/trpc.ts b/app/src/server/api/trpc.ts index 2f6c8b9..eab697e 100644 --- a/app/src/server/api/trpc.ts +++ b/app/src/server/api/trpc.ts @@ -64,7 +64,7 @@ export const createTRPCContext = async (opts: CreateNextContextOptions) => { // Get the session from the server using the getServerSession wrapper function const session = await getServerAuthSession({ req, res }); - const apiKey = req.headers["x-openpipe-api-key"] as string | null; + const apiKey = req.headers["X-Openpipe-Api-Key"] as string | null; console.log('api key is', apiKey) diff --git a/app/src/server/scripts/client-codegen.ts b/app/src/server/scripts/client-codegen.ts index d3bbc0f..e5a2945 100644 --- a/app/src/server/scripts/client-codegen.ts +++ b/app/src/server/scripts/client-codegen.ts @@ -34,3 +34,5 @@ execSync( ); console.log("Done!"); + +process.exit(0); diff --git a/app/src/server/utils/openai.ts b/app/src/server/utils/openai.ts index e345456..9112474 100644 --- a/app/src/server/utils/openai.ts +++ b/app/src/server/utils/openai.ts @@ -1,6 +1,10 @@ import { env } from "~/env.mjs"; -import OpenAI from "openai"; +// import OpenAI from "openai"; + +// import { OpenPipe } from "../../../../client-libs/js/openai/index"; + +import { OpenAI } from "openpipe"; // Set a dummy key so it doesn't fail at build time -export const openai = new OpenAI({ apiKey: env.OPENAI_API_KEY ?? "dummy-key" }); +export const openai = new OpenAI.OpenPipe({ apiKey: env.OPENAI_API_KEY ?? "dummy-key" }); diff --git a/client-libs/js/codegen/api.ts b/client-libs/js/codegen/api.ts deleted file mode 100644 index f1f89f8..0000000 --- a/client-libs/js/codegen/api.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -/** - * OpenPipe API - * The public API for reporting API calls to OpenPipe - * - * The version of the OpenAPI document: 0.1.0 - * - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - - -import type { Configuration } from './configuration'; -import type { AxiosPromise, AxiosInstance, AxiosRequestConfig } from 'axios'; -import globalAxios from 'axios'; -// Some imports not used depending on template conditions -// @ts-ignore -import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from './common'; -import type { RequestArgs } from './base'; -// @ts-ignore -import { BASE_PATH, COLLECTION_FORMATS, BaseAPI, RequiredError } from './base'; - - diff --git a/client-libs/js/package.json b/client-libs/js/package.json deleted file mode 100644 index 9f7a8a4..0000000 --- a/client-libs/js/package.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "openpipe", - "version": "0.1.0", - "description": "", - "main": "index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "keywords": [], - "author": "", - "license": "ISC", - "dependencies": { - "openai": "^3.3.0" - }, - "devDependencies": { - "dotenv": "^16.3.1", - "tsx": "^3.12.7", - "typescript": "^5.1.6" - } - } - \ No newline at end of file diff --git a/client-libs/schema.json b/client-libs/schema.json index a3be03f..3cf2280 100644 --- a/client-libs/schema.json +++ b/client-libs/schema.json @@ -10,7 +10,132 @@ "url": "https://app.openpipe.ai/api" } ], - "paths": {}, + "paths": { + "/v1/check-cache": { + "post": { + "operationId": "externalApi-checkCache", + "description": "Check if a prompt is cached", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "startTime": { + "type": "number", + "description": "Unix timestamp in milliseconds" + }, + "reqPayload": { + "description": "JSON-encoded request payload" + }, + "tags": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Extra tags to attach to the call for filtering. Eg { \"userId\": \"123\", \"promptId\": \"populate-title\" }" + } + }, + "required": [ + "startTime" + ], + "additionalProperties": false + } + } + } + }, + "parameters": [], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "respPayload": { + "description": "JSON-encoded response payload" + } + }, + "additionalProperties": false + } + } + } + }, + "default": { + "$ref": "#/components/responses/error" + } + } + } + }, + "/v1/report": { + "post": { + "operationId": "externalApi-report", + "description": "Report an API call", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "startTime": { + "type": "number", + "description": "Unix timestamp in milliseconds" + }, + "endTime": { + "type": "number", + "description": "Unix timestamp in milliseconds" + }, + "reqPayload": { + "description": "JSON-encoded request payload" + }, + "respPayload": { + "description": "JSON-encoded response payload" + }, + "respStatus": { + "type": "number", + "description": "HTTP status code of response" + }, + "error": { + "type": "string", + "description": "User-friendly error message" + }, + "tags": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Extra tags to attach to the call for filtering. Eg { \"userId\": \"123\", \"promptId\": \"populate-title\" }" + } + }, + "required": [ + "startTime", + "endTime" + ], + "additionalProperties": false + } + } + } + }, + "parameters": [], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "default": { + "$ref": "#/components/responses/error" + } + } + } + } + }, "components": { "securitySchemes": { "Authorization": { diff --git a/client-libs/typescript/.gitignore b/client-libs/typescript/.gitignore new file mode 100644 index 0000000..f06235c --- /dev/null +++ b/client-libs/typescript/.gitignore @@ -0,0 +1,2 @@ +node_modules +dist diff --git a/client-libs/js/codegen/.gitignore b/client-libs/typescript/codegen/.gitignore similarity index 100% rename from client-libs/js/codegen/.gitignore rename to client-libs/typescript/codegen/.gitignore diff --git a/client-libs/js/codegen/.npmignore b/client-libs/typescript/codegen/.npmignore similarity index 100% rename from client-libs/js/codegen/.npmignore rename to client-libs/typescript/codegen/.npmignore diff --git a/client-libs/js/codegen/.openapi-generator-ignore b/client-libs/typescript/codegen/.openapi-generator-ignore similarity index 100% rename from client-libs/js/codegen/.openapi-generator-ignore rename to client-libs/typescript/codegen/.openapi-generator-ignore diff --git a/client-libs/js/codegen/.openapi-generator/FILES b/client-libs/typescript/codegen/.openapi-generator/FILES similarity index 100% rename from client-libs/js/codegen/.openapi-generator/FILES rename to client-libs/typescript/codegen/.openapi-generator/FILES diff --git a/client-libs/js/codegen/.openapi-generator/VERSION b/client-libs/typescript/codegen/.openapi-generator/VERSION similarity index 100% rename from client-libs/js/codegen/.openapi-generator/VERSION rename to client-libs/typescript/codegen/.openapi-generator/VERSION diff --git a/client-libs/typescript/codegen/api.js b/client-libs/typescript/codegen/api.js new file mode 100644 index 0000000..9680129 --- /dev/null +++ b/client-libs/typescript/codegen/api.js @@ -0,0 +1,272 @@ +"use strict"; +/* tslint:disable */ +/* eslint-disable */ +/** + * OpenPipe API + * The public API for reporting API calls to OpenPipe + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + if (typeof b !== "function" && b !== null) + throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; + return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.DefaultApi = exports.DefaultApiFactory = exports.DefaultApiFp = exports.DefaultApiAxiosParamCreator = void 0; +var axios_1 = require("axios"); +// Some imports not used depending on template conditions +// @ts-ignore +var common_1 = require("./common"); +// @ts-ignore +var base_1 = require("./base"); +/** + * DefaultApi - axios parameter creator + * @export + */ +var DefaultApiAxiosParamCreator = function (configuration) { + var _this = this; + return { + /** + * Check if a prompt is cached + * @param {ExternalApiCheckCacheRequest} externalApiCheckCacheRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + externalApiCheckCache: function (externalApiCheckCacheRequest, options) { + if (options === void 0) { options = {}; } + return __awaiter(_this, void 0, void 0, function () { + var localVarPath, localVarUrlObj, baseOptions, localVarRequestOptions, localVarHeaderParameter, localVarQueryParameter, headersFromBaseOptions; + return __generator(this, function (_a) { + // verify required parameter 'externalApiCheckCacheRequest' is not null or undefined + (0, common_1.assertParamExists)('externalApiCheckCache', 'externalApiCheckCacheRequest', externalApiCheckCacheRequest); + localVarPath = "/v1/check-cache"; + localVarUrlObj = new URL(localVarPath, common_1.DUMMY_BASE_URL); + if (configuration) { + baseOptions = configuration.baseOptions; + } + localVarRequestOptions = __assign(__assign({ method: 'POST' }, baseOptions), options); + localVarHeaderParameter = {}; + localVarQueryParameter = {}; + localVarHeaderParameter['Content-Type'] = 'application/json'; + (0, common_1.setSearchParams)(localVarUrlObj, localVarQueryParameter); + headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = __assign(__assign(__assign({}, localVarHeaderParameter), headersFromBaseOptions), options.headers); + localVarRequestOptions.data = (0, common_1.serializeDataIfNeeded)(externalApiCheckCacheRequest, localVarRequestOptions, configuration); + return [2 /*return*/, { + url: (0, common_1.toPathString)(localVarUrlObj), + options: localVarRequestOptions, + }]; + }); + }); + }, + /** + * Report an API call + * @param {ExternalApiReportRequest} externalApiReportRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + externalApiReport: function (externalApiReportRequest, options) { + if (options === void 0) { options = {}; } + return __awaiter(_this, void 0, void 0, function () { + var localVarPath, localVarUrlObj, baseOptions, localVarRequestOptions, localVarHeaderParameter, localVarQueryParameter, headersFromBaseOptions; + return __generator(this, function (_a) { + // verify required parameter 'externalApiReportRequest' is not null or undefined + (0, common_1.assertParamExists)('externalApiReport', 'externalApiReportRequest', externalApiReportRequest); + localVarPath = "/v1/report"; + localVarUrlObj = new URL(localVarPath, common_1.DUMMY_BASE_URL); + if (configuration) { + baseOptions = configuration.baseOptions; + } + localVarRequestOptions = __assign(__assign({ method: 'POST' }, baseOptions), options); + localVarHeaderParameter = {}; + localVarQueryParameter = {}; + localVarHeaderParameter['Content-Type'] = 'application/json'; + (0, common_1.setSearchParams)(localVarUrlObj, localVarQueryParameter); + headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = __assign(__assign(__assign({}, localVarHeaderParameter), headersFromBaseOptions), options.headers); + localVarRequestOptions.data = (0, common_1.serializeDataIfNeeded)(externalApiReportRequest, localVarRequestOptions, configuration); + return [2 /*return*/, { + url: (0, common_1.toPathString)(localVarUrlObj), + options: localVarRequestOptions, + }]; + }); + }); + }, + }; +}; +exports.DefaultApiAxiosParamCreator = DefaultApiAxiosParamCreator; +/** + * DefaultApi - functional programming interface + * @export + */ +var DefaultApiFp = function (configuration) { + var localVarAxiosParamCreator = (0, exports.DefaultApiAxiosParamCreator)(configuration); + return { + /** + * Check if a prompt is cached + * @param {ExternalApiCheckCacheRequest} externalApiCheckCacheRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + externalApiCheckCache: function (externalApiCheckCacheRequest, options) { + return __awaiter(this, void 0, void 0, function () { + var localVarAxiosArgs; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, localVarAxiosParamCreator.externalApiCheckCache(externalApiCheckCacheRequest, options)]; + case 1: + localVarAxiosArgs = _a.sent(); + return [2 /*return*/, (0, common_1.createRequestFunction)(localVarAxiosArgs, axios_1.default, base_1.BASE_PATH, configuration)]; + } + }); + }); + }, + /** + * Report an API call + * @param {ExternalApiReportRequest} externalApiReportRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + externalApiReport: function (externalApiReportRequest, options) { + return __awaiter(this, void 0, void 0, function () { + var localVarAxiosArgs; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, localVarAxiosParamCreator.externalApiReport(externalApiReportRequest, options)]; + case 1: + localVarAxiosArgs = _a.sent(); + return [2 /*return*/, (0, common_1.createRequestFunction)(localVarAxiosArgs, axios_1.default, base_1.BASE_PATH, configuration)]; + } + }); + }); + }, + }; +}; +exports.DefaultApiFp = DefaultApiFp; +/** + * DefaultApi - factory interface + * @export + */ +var DefaultApiFactory = function (configuration, basePath, axios) { + var localVarFp = (0, exports.DefaultApiFp)(configuration); + return { + /** + * Check if a prompt is cached + * @param {ExternalApiCheckCacheRequest} externalApiCheckCacheRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + externalApiCheckCache: function (externalApiCheckCacheRequest, options) { + return localVarFp.externalApiCheckCache(externalApiCheckCacheRequest, options).then(function (request) { return request(axios, basePath); }); + }, + /** + * Report an API call + * @param {ExternalApiReportRequest} externalApiReportRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + externalApiReport: function (externalApiReportRequest, options) { + return localVarFp.externalApiReport(externalApiReportRequest, options).then(function (request) { return request(axios, basePath); }); + }, + }; +}; +exports.DefaultApiFactory = DefaultApiFactory; +/** + * DefaultApi - object-oriented interface + * @export + * @class DefaultApi + * @extends {BaseAPI} + */ +var DefaultApi = /** @class */ (function (_super) { + __extends(DefaultApi, _super); + function DefaultApi() { + return _super !== null && _super.apply(this, arguments) || this; + } + /** + * Check if a prompt is cached + * @param {ExternalApiCheckCacheRequest} externalApiCheckCacheRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof DefaultApi + */ + DefaultApi.prototype.externalApiCheckCache = function (externalApiCheckCacheRequest, options) { + var _this = this; + return (0, exports.DefaultApiFp)(this.configuration).externalApiCheckCache(externalApiCheckCacheRequest, options).then(function (request) { return request(_this.axios, _this.basePath); }); + }; + /** + * Report an API call + * @param {ExternalApiReportRequest} externalApiReportRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof DefaultApi + */ + DefaultApi.prototype.externalApiReport = function (externalApiReportRequest, options) { + var _this = this; + return (0, exports.DefaultApiFp)(this.configuration).externalApiReport(externalApiReportRequest, options).then(function (request) { return request(_this.axios, _this.basePath); }); + }; + return DefaultApi; +}(base_1.BaseAPI)); +exports.DefaultApi = DefaultApi; diff --git a/client-libs/typescript/codegen/api.ts b/client-libs/typescript/codegen/api.ts new file mode 100644 index 0000000..64b5068 --- /dev/null +++ b/client-libs/typescript/codegen/api.ts @@ -0,0 +1,319 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * OpenPipe API + * The public API for reporting API calls to OpenPipe + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +import type { Configuration } from './configuration'; +import type { AxiosPromise, AxiosInstance, AxiosRequestConfig } from 'axios'; +import globalAxios from 'axios'; +// Some imports not used depending on template conditions +// @ts-ignore +import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from './common'; +import type { RequestArgs } from './base'; +// @ts-ignore +import { BASE_PATH, COLLECTION_FORMATS, BaseAPI, RequiredError } from './base'; + +/** + * + * @export + * @interface ExternalApiCheckCache200Response + */ +export interface ExternalApiCheckCache200Response { + /** + * JSON-encoded response payload + * @type {any} + * @memberof ExternalApiCheckCache200Response + */ + 'respPayload'?: any; +} +/** + * + * @export + * @interface ExternalApiCheckCacheDefaultResponse + */ +export interface ExternalApiCheckCacheDefaultResponse { + /** + * + * @type {string} + * @memberof ExternalApiCheckCacheDefaultResponse + */ + 'message': string; + /** + * + * @type {string} + * @memberof ExternalApiCheckCacheDefaultResponse + */ + 'code': string; + /** + * + * @type {Array} + * @memberof ExternalApiCheckCacheDefaultResponse + */ + 'issues'?: Array; +} +/** + * + * @export + * @interface ExternalApiCheckCacheDefaultResponseIssuesInner + */ +export interface ExternalApiCheckCacheDefaultResponseIssuesInner { + /** + * + * @type {string} + * @memberof ExternalApiCheckCacheDefaultResponseIssuesInner + */ + 'message': string; +} +/** + * + * @export + * @interface ExternalApiCheckCacheRequest + */ +export interface ExternalApiCheckCacheRequest { + /** + * Unix timestamp in milliseconds + * @type {number} + * @memberof ExternalApiCheckCacheRequest + */ + 'startTime': number; + /** + * JSON-encoded request payload + * @type {any} + * @memberof ExternalApiCheckCacheRequest + */ + 'reqPayload'?: any; + /** + * Extra tags to attach to the call for filtering. Eg { \"userId\": \"123\", \"promptId\": \"populate-title\" } + * @type {{ [key: string]: string; }} + * @memberof ExternalApiCheckCacheRequest + */ + 'tags'?: { [key: string]: string; }; +} +/** + * + * @export + * @interface ExternalApiReportRequest + */ +export interface ExternalApiReportRequest { + /** + * Unix timestamp in milliseconds + * @type {number} + * @memberof ExternalApiReportRequest + */ + 'startTime': number; + /** + * Unix timestamp in milliseconds + * @type {number} + * @memberof ExternalApiReportRequest + */ + 'endTime': number; + /** + * JSON-encoded request payload + * @type {any} + * @memberof ExternalApiReportRequest + */ + 'reqPayload'?: any; + /** + * JSON-encoded response payload + * @type {any} + * @memberof ExternalApiReportRequest + */ + 'respPayload'?: any; + /** + * HTTP status code of response + * @type {number} + * @memberof ExternalApiReportRequest + */ + 'respStatus'?: number; + /** + * User-friendly error message + * @type {string} + * @memberof ExternalApiReportRequest + */ + 'error'?: string; + /** + * Extra tags to attach to the call for filtering. Eg { \"userId\": \"123\", \"promptId\": \"populate-title\" } + * @type {{ [key: string]: string; }} + * @memberof ExternalApiReportRequest + */ + 'tags'?: { [key: string]: string; }; +} + +/** + * DefaultApi - axios parameter creator + * @export + */ +export const DefaultApiAxiosParamCreator = function (configuration?: Configuration) { + return { + /** + * Check if a prompt is cached + * @param {ExternalApiCheckCacheRequest} externalApiCheckCacheRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + externalApiCheckCache: async (externalApiCheckCacheRequest: ExternalApiCheckCacheRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'externalApiCheckCacheRequest' is not null or undefined + assertParamExists('externalApiCheckCache', 'externalApiCheckCacheRequest', externalApiCheckCacheRequest) + const localVarPath = `/v1/check-cache`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(externalApiCheckCacheRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * Report an API call + * @param {ExternalApiReportRequest} externalApiReportRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + externalApiReport: async (externalApiReportRequest: ExternalApiReportRequest, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'externalApiReportRequest' is not null or undefined + assertParamExists('externalApiReport', 'externalApiReportRequest', externalApiReportRequest) + const localVarPath = `/v1/report`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(externalApiReportRequest, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * DefaultApi - functional programming interface + * @export + */ +export const DefaultApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = DefaultApiAxiosParamCreator(configuration) + return { + /** + * Check if a prompt is cached + * @param {ExternalApiCheckCacheRequest} externalApiCheckCacheRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async externalApiCheckCache(externalApiCheckCacheRequest: ExternalApiCheckCacheRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.externalApiCheckCache(externalApiCheckCacheRequest, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, + /** + * Report an API call + * @param {ExternalApiReportRequest} externalApiReportRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async externalApiReport(externalApiReportRequest: ExternalApiReportRequest, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.externalApiReport(externalApiReportRequest, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, + } +}; + +/** + * DefaultApi - factory interface + * @export + */ +export const DefaultApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = DefaultApiFp(configuration) + return { + /** + * Check if a prompt is cached + * @param {ExternalApiCheckCacheRequest} externalApiCheckCacheRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + externalApiCheckCache(externalApiCheckCacheRequest: ExternalApiCheckCacheRequest, options?: any): AxiosPromise { + return localVarFp.externalApiCheckCache(externalApiCheckCacheRequest, options).then((request) => request(axios, basePath)); + }, + /** + * Report an API call + * @param {ExternalApiReportRequest} externalApiReportRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + externalApiReport(externalApiReportRequest: ExternalApiReportRequest, options?: any): AxiosPromise { + return localVarFp.externalApiReport(externalApiReportRequest, options).then((request) => request(axios, basePath)); + }, + }; +}; + +/** + * DefaultApi - object-oriented interface + * @export + * @class DefaultApi + * @extends {BaseAPI} + */ +export class DefaultApi extends BaseAPI { + /** + * Check if a prompt is cached + * @param {ExternalApiCheckCacheRequest} externalApiCheckCacheRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof DefaultApi + */ + public externalApiCheckCache(externalApiCheckCacheRequest: ExternalApiCheckCacheRequest, options?: AxiosRequestConfig) { + return DefaultApiFp(this.configuration).externalApiCheckCache(externalApiCheckCacheRequest, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * Report an API call + * @param {ExternalApiReportRequest} externalApiReportRequest + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof DefaultApi + */ + public externalApiReport(externalApiReportRequest: ExternalApiReportRequest, options?: AxiosRequestConfig) { + return DefaultApiFp(this.configuration).externalApiReport(externalApiReportRequest, options).then((request) => request(this.axios, this.basePath)); + } +} + + diff --git a/client-libs/typescript/codegen/base.js b/client-libs/typescript/codegen/base.js new file mode 100644 index 0000000..5d52771 --- /dev/null +++ b/client-libs/typescript/codegen/base.js @@ -0,0 +1,80 @@ +"use strict"; +/* tslint:disable */ +/* eslint-disable */ +/** + * OpenPipe API + * The public API for reporting API calls to OpenPipe + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + if (typeof b !== "function" && b !== null) + throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +exports.RequiredError = exports.BaseAPI = exports.COLLECTION_FORMATS = exports.BASE_PATH = void 0; +var axios_1 = require("axios"); +exports.BASE_PATH = "https://app.openpipe.ai/api".replace(/\/+$/, ""); +/** + * + * @export + */ +exports.COLLECTION_FORMATS = { + csv: ",", + ssv: " ", + tsv: "\t", + pipes: "|", +}; +/** + * + * @export + * @class BaseAPI + */ +var BaseAPI = /** @class */ (function () { + function BaseAPI(configuration, basePath, axios) { + if (basePath === void 0) { basePath = exports.BASE_PATH; } + if (axios === void 0) { axios = axios_1.default; } + this.basePath = basePath; + this.axios = axios; + if (configuration) { + this.configuration = configuration; + this.basePath = configuration.basePath || this.basePath; + } + } + return BaseAPI; +}()); +exports.BaseAPI = BaseAPI; +; +/** + * + * @export + * @class RequiredError + * @extends {Error} + */ +var RequiredError = /** @class */ (function (_super) { + __extends(RequiredError, _super); + function RequiredError(field, msg) { + var _this = _super.call(this, msg) || this; + _this.field = field; + _this.name = "RequiredError"; + return _this; + } + return RequiredError; +}(Error)); +exports.RequiredError = RequiredError; diff --git a/client-libs/js/codegen/base.ts b/client-libs/typescript/codegen/base.ts similarity index 100% rename from client-libs/js/codegen/base.ts rename to client-libs/typescript/codegen/base.ts diff --git a/client-libs/typescript/codegen/common.js b/client-libs/typescript/codegen/common.js new file mode 100644 index 0000000..fa65488 --- /dev/null +++ b/client-libs/typescript/codegen/common.js @@ -0,0 +1,252 @@ +"use strict"; +/* tslint:disable */ +/* eslint-disable */ +/** + * OpenPipe API + * The public API for reporting API calls to OpenPipe + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; + return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.createRequestFunction = exports.toPathString = exports.serializeDataIfNeeded = exports.setSearchParams = exports.setOAuthToObject = exports.setBearerAuthToObject = exports.setBasicAuthToObject = exports.setApiKeyToObject = exports.assertParamExists = exports.DUMMY_BASE_URL = void 0; +var base_1 = require("./base"); +/** + * + * @export + */ +exports.DUMMY_BASE_URL = 'https://example.com'; +/** + * + * @throws {RequiredError} + * @export + */ +var assertParamExists = function (functionName, paramName, paramValue) { + if (paramValue === null || paramValue === undefined) { + throw new base_1.RequiredError(paramName, "Required parameter ".concat(paramName, " was null or undefined when calling ").concat(functionName, ".")); + } +}; +exports.assertParamExists = assertParamExists; +/** + * + * @export + */ +var setApiKeyToObject = function (object, keyParamName, configuration) { + return __awaiter(this, void 0, void 0, function () { + var localVarApiKeyValue, _a; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + if (!(configuration && configuration.apiKey)) return [3 /*break*/, 5]; + if (!(typeof configuration.apiKey === 'function')) return [3 /*break*/, 2]; + return [4 /*yield*/, configuration.apiKey(keyParamName)]; + case 1: + _a = _b.sent(); + return [3 /*break*/, 4]; + case 2: return [4 /*yield*/, configuration.apiKey]; + case 3: + _a = _b.sent(); + _b.label = 4; + case 4: + localVarApiKeyValue = _a; + object[keyParamName] = localVarApiKeyValue; + _b.label = 5; + case 5: return [2 /*return*/]; + } + }); + }); +}; +exports.setApiKeyToObject = setApiKeyToObject; +/** + * + * @export + */ +var setBasicAuthToObject = function (object, configuration) { + if (configuration && (configuration.username || configuration.password)) { + object["auth"] = { username: configuration.username, password: configuration.password }; + } +}; +exports.setBasicAuthToObject = setBasicAuthToObject; +/** + * + * @export + */ +var setBearerAuthToObject = function (object, configuration) { + return __awaiter(this, void 0, void 0, function () { + var accessToken, _a; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + if (!(configuration && configuration.accessToken)) return [3 /*break*/, 5]; + if (!(typeof configuration.accessToken === 'function')) return [3 /*break*/, 2]; + return [4 /*yield*/, configuration.accessToken()]; + case 1: + _a = _b.sent(); + return [3 /*break*/, 4]; + case 2: return [4 /*yield*/, configuration.accessToken]; + case 3: + _a = _b.sent(); + _b.label = 4; + case 4: + accessToken = _a; + object["Authorization"] = "Bearer " + accessToken; + _b.label = 5; + case 5: return [2 /*return*/]; + } + }); + }); +}; +exports.setBearerAuthToObject = setBearerAuthToObject; +/** + * + * @export + */ +var setOAuthToObject = function (object, name, scopes, configuration) { + return __awaiter(this, void 0, void 0, function () { + var localVarAccessTokenValue, _a; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + if (!(configuration && configuration.accessToken)) return [3 /*break*/, 5]; + if (!(typeof configuration.accessToken === 'function')) return [3 /*break*/, 2]; + return [4 /*yield*/, configuration.accessToken(name, scopes)]; + case 1: + _a = _b.sent(); + return [3 /*break*/, 4]; + case 2: return [4 /*yield*/, configuration.accessToken]; + case 3: + _a = _b.sent(); + _b.label = 4; + case 4: + localVarAccessTokenValue = _a; + object["Authorization"] = "Bearer " + localVarAccessTokenValue; + _b.label = 5; + case 5: return [2 /*return*/]; + } + }); + }); +}; +exports.setOAuthToObject = setOAuthToObject; +function setFlattenedQueryParams(urlSearchParams, parameter, key) { + if (key === void 0) { key = ""; } + if (parameter == null) + return; + if (typeof parameter === "object") { + if (Array.isArray(parameter)) { + parameter.forEach(function (item) { return setFlattenedQueryParams(urlSearchParams, item, key); }); + } + else { + Object.keys(parameter).forEach(function (currentKey) { + return setFlattenedQueryParams(urlSearchParams, parameter[currentKey], "".concat(key).concat(key !== '' ? '.' : '').concat(currentKey)); + }); + } + } + else { + if (urlSearchParams.has(key)) { + urlSearchParams.append(key, parameter); + } + else { + urlSearchParams.set(key, parameter); + } + } +} +/** + * + * @export + */ +var setSearchParams = function (url) { + var objects = []; + for (var _i = 1; _i < arguments.length; _i++) { + objects[_i - 1] = arguments[_i]; + } + var searchParams = new URLSearchParams(url.search); + setFlattenedQueryParams(searchParams, objects); + url.search = searchParams.toString(); +}; +exports.setSearchParams = setSearchParams; +/** + * + * @export + */ +var serializeDataIfNeeded = function (value, requestOptions, configuration) { + var nonString = typeof value !== 'string'; + var needsSerialization = nonString && configuration && configuration.isJsonMime + ? configuration.isJsonMime(requestOptions.headers['Content-Type']) + : nonString; + return needsSerialization + ? JSON.stringify(value !== undefined ? value : {}) + : (value || ""); +}; +exports.serializeDataIfNeeded = serializeDataIfNeeded; +/** + * + * @export + */ +var toPathString = function (url) { + return url.pathname + url.search + url.hash; +}; +exports.toPathString = toPathString; +/** + * + * @export + */ +var createRequestFunction = function (axiosArgs, globalAxios, BASE_PATH, configuration) { + return function (axios, basePath) { + if (axios === void 0) { axios = globalAxios; } + if (basePath === void 0) { basePath = BASE_PATH; } + var axiosRequestArgs = __assign(__assign({}, axiosArgs.options), { url: ((configuration === null || configuration === void 0 ? void 0 : configuration.basePath) || basePath) + axiosArgs.url }); + return axios.request(axiosRequestArgs); + }; +}; +exports.createRequestFunction = createRequestFunction; diff --git a/client-libs/js/codegen/common.ts b/client-libs/typescript/codegen/common.ts similarity index 100% rename from client-libs/js/codegen/common.ts rename to client-libs/typescript/codegen/common.ts diff --git a/client-libs/typescript/codegen/configuration.js b/client-libs/typescript/codegen/configuration.js new file mode 100644 index 0000000..bbc5bfb --- /dev/null +++ b/client-libs/typescript/codegen/configuration.js @@ -0,0 +1,44 @@ +"use strict"; +/* tslint:disable */ +/* eslint-disable */ +/** + * OpenPipe API + * The public API for reporting API calls to OpenPipe + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.Configuration = void 0; +var Configuration = /** @class */ (function () { + function Configuration(param) { + if (param === void 0) { param = {}; } + this.apiKey = param.apiKey; + this.username = param.username; + this.password = param.password; + this.accessToken = param.accessToken; + this.basePath = param.basePath; + this.baseOptions = param.baseOptions; + this.formDataCtor = param.formDataCtor; + } + /** + * Check if the given MIME is a JSON MIME. + * JSON MIME examples: + * application/json + * application/json; charset=UTF8 + * APPLICATION/JSON + * application/vnd.company+json + * @param mime - MIME (Multipurpose Internet Mail Extensions) + * @return True if the given MIME is JSON, false otherwise. + */ + Configuration.prototype.isJsonMime = function (mime) { + var jsonMime = new RegExp('^(application\/json|[^;/ \t]+\/[^;/ \t]+[+]json)[ \t]*(;.*)?$', 'i'); + return mime !== null && (jsonMime.test(mime) || mime.toLowerCase() === 'application/json-patch+json'); + }; + return Configuration; +}()); +exports.Configuration = Configuration; diff --git a/client-libs/js/codegen/configuration.ts b/client-libs/typescript/codegen/configuration.ts similarity index 100% rename from client-libs/js/codegen/configuration.ts rename to client-libs/typescript/codegen/configuration.ts diff --git a/client-libs/js/codegen/git_push.sh b/client-libs/typescript/codegen/git_push.sh similarity index 100% rename from client-libs/js/codegen/git_push.sh rename to client-libs/typescript/codegen/git_push.sh diff --git a/client-libs/typescript/codegen/index.js b/client-libs/typescript/codegen/index.js new file mode 100644 index 0000000..519d4f3 --- /dev/null +++ b/client-libs/typescript/codegen/index.js @@ -0,0 +1,31 @@ +"use strict"; +/* tslint:disable */ +/* eslint-disable */ +/** + * OpenPipe API + * The public API for reporting API calls to OpenPipe + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __exportStar = (this && this.__exportStar) || function(m, exports) { + for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +__exportStar(require("./api"), exports); +__exportStar(require("./configuration"), exports); diff --git a/client-libs/js/codegen/index.ts b/client-libs/typescript/codegen/index.ts similarity index 100% rename from client-libs/js/codegen/index.ts rename to client-libs/typescript/codegen/index.ts diff --git a/client-libs/typescript/index.ts b/client-libs/typescript/index.ts new file mode 100644 index 0000000..04c7df2 --- /dev/null +++ b/client-libs/typescript/index.ts @@ -0,0 +1,3 @@ +// main.ts or index.ts at the root level +export * as OpenAI from './openai'; +export * as OpenAILegacy from './openai-legacy'; diff --git a/client-libs/typescript/openai-legacy/index.ts b/client-libs/typescript/openai-legacy/index.ts new file mode 100644 index 0000000..99b8d80 --- /dev/null +++ b/client-libs/typescript/openai-legacy/index.ts @@ -0,0 +1,81 @@ +import * as openPipeClient from "../codegen"; +import * as openai from "openai-legacy"; +import { version } from "../package.json"; + +// Anything we don't override we want to pass through to openai directly +export * as openAILegacy from "openai-legacy"; + +type OPConfigurationParameters = { + apiKey?: string; + basePath?: string; +}; + +export class Configuration extends openai.Configuration { + public qkConfig?: openPipeClient.Configuration; + + constructor(config: openai.ConfigurationParameters & { opParameters?: OPConfigurationParameters }) { + super(config); + if (config.opParameters) { + this.qkConfig = new openPipeClient.Configuration(config.opParameters); + } + } +} + +type CreateChatCompletion = InstanceType["createChatCompletion"]; + +export class OpenAIApi extends openai.OpenAIApi { + public openPipeApi?: openPipeClient.DefaultApi; + + constructor(config: Configuration) { + super(config); + if (config.qkConfig) { + this.openPipeApi = new openPipeClient.DefaultApi(config.qkConfig); + } + } + + public async createChatCompletion( + createChatCompletionRequest: Parameters[0], + options?: Parameters[1] + ): ReturnType { + const startTime = Date.now(); + let resp: Awaited> | null = null; + let respPayload: openai.CreateChatCompletionResponse | null = null; + let respStatus: number | undefined = undefined; + let error: string | undefined; + try { + resp = await super.createChatCompletion(createChatCompletionRequest, options); + respPayload = resp.data; + respStatus = resp.status; + } catch (err) { + console.error("Error in createChatCompletion"); + if ("isAxiosError" in err && err.isAxiosError) { + error = err.response?.data?.error?.message; + respPayload = err.response?.data; + respStatus = err.response?.status; + } else if ("message" in err) { + error = err.message.toString(); + } + throw err; + } finally { + this.openPipeApi + ?.externalApiReport({ + startTime, + endTime: Date.now(), + reqPayload: createChatCompletionRequest, + respPayload: respPayload, + respStatus: respStatus, + error, + tags: { + client: "openai-js", + clientVersion: version, + }, + }) + .catch((err) => { + console.error("Error reporting to QK", err); + }); + } + + console.log("done"); + return resp; + } +} diff --git a/client-libs/typescript/openai/index.ts b/client-libs/typescript/openai/index.ts new file mode 100644 index 0000000..8c43fed --- /dev/null +++ b/client-libs/typescript/openai/index.ts @@ -0,0 +1,31 @@ +import * as OpenAI from "openai-beta"; +import { readEnv } from "openai-beta/core"; + +// Anything we don't override we want to pass through to openai directly +export * as openai from "openai-beta"; + +interface ClientOptions extends OpenAI.ClientOptions { + openPipeApiKey?: string; + openPipeBaseUrl?: string; +} + +export class OpenPipe extends OpenAI.OpenAI { + openPipeApiKey: string; + openPipeBaseUrl: string; + + constructor({ + openPipeApiKey = readEnv("OPENPIPE_API_KEY"), + openPipeBaseUrl = readEnv("OPENPIPE_BASE_URL") ?? + `https://app.openpipe.ai/v1`, + ...opts + }: ClientOptions = {}) { + if (openPipeApiKey === undefined) { + console.error( + "The OPENPIPE_API_KEY environment variable is missing or empty; either provide it, or instantiate the OpenPipe client with an openPipeApiKey option, like new OpenPipe({ openPipeApiKey: undefined })." + ); + } + super({ + ...opts, + }); + } +} diff --git a/client-libs/typescript/package.json b/client-libs/typescript/package.json new file mode 100644 index 0000000..1173fc2 --- /dev/null +++ b/client-libs/typescript/package.json @@ -0,0 +1,24 @@ +{ + "name": "openpipe", + "version": "0.1.0", + "description": "Metrics and auto-evaluation for LLM calls", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "axios": "^0.26.0", + "openai-beta": "npm:openai@4.0.0-beta.7", + "openai-legacy": "npm:openai@3.3.0" + }, + "devDependencies": { + "@types/node": "^20.4.8", + "dotenv": "^16.3.1", + "tsx": "^3.12.7", + "typescript": "^5.0.4" + } +} diff --git a/client-libs/typescript/pnpm-lock.yaml b/client-libs/typescript/pnpm-lock.yaml new file mode 100644 index 0000000..5d145bb --- /dev/null +++ b/client-libs/typescript/pnpm-lock.yaml @@ -0,0 +1,548 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +dependencies: + axios: + specifier: ^0.26.0 + version: 0.26.0 + openai-beta: + specifier: npm:openai@4.0.0-beta.7 + version: /openai@4.0.0-beta.7 + openai-legacy: + specifier: npm:openai@3.3.0 + version: /openai@3.3.0 + +devDependencies: + '@types/node': + specifier: ^20.4.8 + version: 20.4.8 + dotenv: + specifier: ^16.3.1 + version: 16.3.1 + tsx: + specifier: ^3.12.7 + version: 3.12.7 + typescript: + specifier: ^5.0.4 + version: 5.0.4 + +packages: + + /@esbuild-kit/cjs-loader@2.4.2: + resolution: {integrity: sha512-BDXFbYOJzT/NBEtp71cvsrGPwGAMGRB/349rwKuoxNSiKjPraNNnlK6MIIabViCjqZugu6j+xeMDlEkWdHHJSg==} + dependencies: + '@esbuild-kit/core-utils': 3.1.0 + get-tsconfig: 4.6.2 + dev: true + + /@esbuild-kit/core-utils@3.1.0: + resolution: {integrity: sha512-Uuk8RpCg/7fdHSceR1M6XbSZFSuMrxcePFuGgyvsBn+u339dk5OeL4jv2EojwTN2st/unJGsVm4qHWjWNmJ/tw==} + dependencies: + esbuild: 0.17.19 + source-map-support: 0.5.21 + dev: true + + /@esbuild-kit/esm-loader@2.5.5: + resolution: {integrity: sha512-Qwfvj/qoPbClxCRNuac1Du01r9gvNOT+pMYtJDapfB1eoGN1YlJ1BixLyL9WVENRx5RXgNLdfYdx/CuswlGhMw==} + dependencies: + '@esbuild-kit/core-utils': 3.1.0 + get-tsconfig: 4.6.2 + dev: true + + /@esbuild/android-arm64@0.17.19: + resolution: {integrity: sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-arm@0.17.19: + resolution: {integrity: sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-x64@0.17.19: + resolution: {integrity: sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-arm64@0.17.19: + resolution: {integrity: sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-x64@0.17.19: + resolution: {integrity: sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-arm64@0.17.19: + resolution: {integrity: sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-x64@0.17.19: + resolution: {integrity: sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm64@0.17.19: + resolution: {integrity: sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm@0.17.19: + resolution: {integrity: sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ia32@0.17.19: + resolution: {integrity: sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-loong64@0.17.19: + resolution: {integrity: sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-mips64el@0.17.19: + resolution: {integrity: sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ppc64@0.17.19: + resolution: {integrity: sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-riscv64@0.17.19: + resolution: {integrity: sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-s390x@0.17.19: + resolution: {integrity: sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-x64@0.17.19: + resolution: {integrity: sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/netbsd-x64@0.17.19: + resolution: {integrity: sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/openbsd-x64@0.17.19: + resolution: {integrity: sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/sunos-x64@0.17.19: + resolution: {integrity: sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-arm64@0.17.19: + resolution: {integrity: sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-ia32@0.17.19: + resolution: {integrity: sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-x64@0.17.19: + resolution: {integrity: sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@types/node-fetch@2.6.4: + resolution: {integrity: sha512-1ZX9fcN4Rvkvgv4E6PAY5WXUFWFcRWxZa3EW83UjycOB9ljJCedb2CupIP4RZMEwF/M3eTcCihbBRgwtGbg5Rg==} + dependencies: + '@types/node': 20.4.8 + form-data: 3.0.1 + dev: false + + /@types/node@18.17.3: + resolution: {integrity: sha512-2x8HWtFk0S99zqVQABU9wTpr8wPoaDHZUcAkoTKH+nL7kPv3WUI9cRi/Kk5Mz4xdqXSqTkKP7IWNoQQYCnDsTA==} + dev: false + + /@types/node@20.4.8: + resolution: {integrity: sha512-0mHckf6D2DiIAzh8fM8f3HQCvMKDpK94YQ0DSVkfWTG9BZleYIWudw9cJxX8oCk9bM+vAkDyujDV6dmKHbvQpg==} + + /abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + dependencies: + event-target-shim: 5.0.1 + dev: false + + /agentkeepalive@4.5.0: + resolution: {integrity: sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==} + engines: {node: '>= 8.0.0'} + dependencies: + humanize-ms: 1.2.1 + dev: false + + /asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + dev: false + + /axios@0.26.0: + resolution: {integrity: sha512-lKoGLMYtHvFrPVt3r+RBMp9nh34N0M8zEfCWqdWZx6phynIEhQqAdydpyBAAG211zlhX9Rgu08cOamy6XjE5Og==} + dependencies: + follow-redirects: 1.15.2 + transitivePeerDependencies: + - debug + dev: false + + /base-64@0.1.0: + resolution: {integrity: sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==} + dev: false + + /buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + dev: true + + /charenc@0.0.2: + resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==} + dev: false + + /combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + dependencies: + delayed-stream: 1.0.0 + dev: false + + /crypt@0.0.2: + resolution: {integrity: sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==} + dev: false + + /delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + dev: false + + /digest-fetch@1.3.0: + resolution: {integrity: sha512-CGJuv6iKNM7QyZlM2T3sPAdZWd/p9zQiRNS9G+9COUCwzWFTs0Xp8NF5iePx7wtvhDykReiRRrSeNb4oMmB8lA==} + dependencies: + base-64: 0.1.0 + md5: 2.3.0 + dev: false + + /dotenv@16.3.1: + resolution: {integrity: sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==} + engines: {node: '>=12'} + dev: true + + /esbuild@0.17.19: + resolution: {integrity: sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/android-arm': 0.17.19 + '@esbuild/android-arm64': 0.17.19 + '@esbuild/android-x64': 0.17.19 + '@esbuild/darwin-arm64': 0.17.19 + '@esbuild/darwin-x64': 0.17.19 + '@esbuild/freebsd-arm64': 0.17.19 + '@esbuild/freebsd-x64': 0.17.19 + '@esbuild/linux-arm': 0.17.19 + '@esbuild/linux-arm64': 0.17.19 + '@esbuild/linux-ia32': 0.17.19 + '@esbuild/linux-loong64': 0.17.19 + '@esbuild/linux-mips64el': 0.17.19 + '@esbuild/linux-ppc64': 0.17.19 + '@esbuild/linux-riscv64': 0.17.19 + '@esbuild/linux-s390x': 0.17.19 + '@esbuild/linux-x64': 0.17.19 + '@esbuild/netbsd-x64': 0.17.19 + '@esbuild/openbsd-x64': 0.17.19 + '@esbuild/sunos-x64': 0.17.19 + '@esbuild/win32-arm64': 0.17.19 + '@esbuild/win32-ia32': 0.17.19 + '@esbuild/win32-x64': 0.17.19 + dev: true + + /event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + dev: false + + /follow-redirects@1.15.2: + resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + dev: false + + /form-data-encoder@1.7.2: + resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==} + dev: false + + /form-data@3.0.1: + resolution: {integrity: sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==} + engines: {node: '>= 6'} + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + dev: false + + /form-data@4.0.0: + resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} + engines: {node: '>= 6'} + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + dev: false + + /formdata-node@4.4.1: + resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==} + engines: {node: '>= 12.20'} + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 4.0.0-beta.3 + dev: false + + /fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /get-tsconfig@4.6.2: + resolution: {integrity: sha512-E5XrT4CbbXcXWy+1jChlZmrmCwd5KGx502kDCXJJ7y898TtWW9FwoG5HfOLVRKmlmDGkWN2HM9Ho+/Y8F0sJDg==} + dependencies: + resolve-pkg-maps: 1.0.0 + dev: true + + /humanize-ms@1.2.1: + resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} + dependencies: + ms: 2.1.3 + dev: false + + /is-buffer@1.1.6: + resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} + dev: false + + /md5@2.3.0: + resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==} + dependencies: + charenc: 0.0.2 + crypt: 0.0.2 + is-buffer: 1.1.6 + dev: false + + /mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + dev: false + + /mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + dependencies: + mime-db: 1.52.0 + dev: false + + /ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + dev: false + + /node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + dev: false + + /node-fetch@2.6.12: + resolution: {integrity: sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + dependencies: + whatwg-url: 5.0.0 + dev: false + + /openai@3.3.0: + resolution: {integrity: sha512-uqxI/Au+aPRnsaQRe8CojU0eCR7I0mBiKjD3sNMzY6DaC1ZVrc85u98mtJW6voDug8fgGN+DIZmTDxTthxb7dQ==} + dependencies: + axios: 0.26.0 + form-data: 4.0.0 + transitivePeerDependencies: + - debug + dev: false + + /openai@4.0.0-beta.7: + resolution: {integrity: sha512-jHjwvpMuGkNxiQ3erwLZsOvPEhcVrMtwtfNeYmGCjhbdB+oStVw/7pIhIPkualu8rlhLwgMR7awknIaN3IQcOA==} + dependencies: + '@types/node': 18.17.3 + '@types/node-fetch': 2.6.4 + abort-controller: 3.0.0 + agentkeepalive: 4.5.0 + digest-fetch: 1.3.0 + form-data-encoder: 1.7.2 + formdata-node: 4.4.1 + node-fetch: 2.6.12 + transitivePeerDependencies: + - encoding + dev: false + + /resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + dev: true + + /source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + dev: true + + /source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + dev: true + + /tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + dev: false + + /tsx@3.12.7: + resolution: {integrity: sha512-C2Ip+jPmqKd1GWVQDvz/Eyc6QJbGfE7NrR3fx5BpEHMZsEHoIxHL1j+lKdGobr8ovEyqeNkPLSKp6SCSOt7gmw==} + hasBin: true + dependencies: + '@esbuild-kit/cjs-loader': 2.4.2 + '@esbuild-kit/core-utils': 3.1.0 + '@esbuild-kit/esm-loader': 2.5.5 + optionalDependencies: + fsevents: 2.3.2 + dev: true + + /typescript@5.0.4: + resolution: {integrity: sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==} + engines: {node: '>=12.20'} + hasBin: true + dev: true + + /web-streams-polyfill@4.0.0-beta.3: + resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==} + engines: {node: '>= 14'} + dev: false + + /webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + dev: false + + /whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + dev: false diff --git a/client-libs/typescript/tsconfig.json b/client-libs/typescript/tsconfig.json new file mode 100644 index 0000000..60b88bf --- /dev/null +++ b/client-libs/typescript/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "declaration": true, + "target": "es6", + "module": "commonjs", + "noImplicitAny": true, + "resolveJsonModule": true, + "outDir": "dist", + "rootDir": ".", + "skipLibCheck": true + }, + "exclude": ["dist", "node_modules"], +} From 6b304f8456888113a4707475cefccab1c5dbf869 Mon Sep 17 00:00:00 2001 From: David Corbitt Date: Sun, 6 Aug 2023 23:23:20 -0700 Subject: [PATCH 06/49] Show selected org --- app/@types/nextjs-routes.d.ts | 1 + .../migration.sql | 2 + app/prisma/schema.prisma | 3 + .../ExperimentSettingsDrawer/DeleteButton.tsx | 5 ++ app/src/components/nav/AppShell.tsx | 90 +++++++------------ app/src/components/nav/IconLink.tsx | 23 +++++ app/src/components/nav/NavSidebarOption.tsx | 26 ++++++ app/src/components/nav/ProjectMenu.tsx | 74 +++++++++++++++ app/src/components/nav/UserMenu.tsx | 56 +++++++----- app/src/pages/data/[id].tsx | 5 +- app/src/pages/data/index.tsx | 4 +- app/src/pages/experiments/[id].tsx | 3 +- app/src/pages/experiments/index.tsx | 4 +- app/src/pages/home/index.tsx | 56 ++++++++++++ app/src/pages/index.tsx | 2 +- app/src/server/api/root.router.ts | 2 + .../api/routers/organizations.router.ts | 71 +++++++++++++++ app/src/server/tasks/worker.ts | 2 +- app/src/server/utils/openai.ts | 5 +- app/src/state/store.ts | 7 ++ app/src/utils/accessControl.ts | 21 +++++ app/src/utils/hooks.ts | 9 ++ client-libs/typescript/openai/index.ts | 6 +- 23 files changed, 380 insertions(+), 97 deletions(-) create mode 100644 app/prisma/migrations/20230807044936_add_name_to_organization/migration.sql create mode 100644 app/src/components/nav/IconLink.tsx create mode 100644 app/src/components/nav/NavSidebarOption.tsx create mode 100644 app/src/components/nav/ProjectMenu.tsx create mode 100644 app/src/pages/home/index.tsx create mode 100644 app/src/server/api/routers/organizations.router.ts diff --git a/app/@types/nextjs-routes.d.ts b/app/@types/nextjs-routes.d.ts index 32c34bd..fbccfff 100644 --- a/app/@types/nextjs-routes.d.ts +++ b/app/@types/nextjs-routes.d.ts @@ -22,6 +22,7 @@ declare module "nextjs-routes" { | StaticRoute<"/data"> | DynamicRoute<"/experiments/[id]", { "id": string }> | StaticRoute<"/experiments"> + | StaticRoute<"/home"> | StaticRoute<"/"> | StaticRoute<"/sentry-example-page"> | StaticRoute<"/world-champs"> diff --git a/app/prisma/migrations/20230807044936_add_name_to_organization/migration.sql b/app/prisma/migrations/20230807044936_add_name_to_organization/migration.sql new file mode 100644 index 0000000..8ec9300 --- /dev/null +++ b/app/prisma/migrations/20230807044936_add_name_to_organization/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Organization" ADD COLUMN "name" TEXT NOT NULL DEFAULT 'Project 1'; diff --git a/app/prisma/schema.prisma b/app/prisma/schema.prisma index 7ed4bfe..387084a 100644 --- a/app/prisma/schema.prisma +++ b/app/prisma/schema.prisma @@ -200,8 +200,11 @@ model DatasetEntry { updatedAt DateTime @updatedAt } +// TODO rename Organization to Project model Organization { id String @id @default(uuid()) @db.Uuid + name String @default("Project 1") + personalOrgUserId String? @unique @db.Uuid PersonalOrgUser User? @relation(fields: [personalOrgUserId], references: [id], onDelete: Cascade) diff --git a/app/src/components/ExperimentSettingsDrawer/DeleteButton.tsx b/app/src/components/ExperimentSettingsDrawer/DeleteButton.tsx index 6c5f6c7..c5e7a9f 100644 --- a/app/src/components/ExperimentSettingsDrawer/DeleteButton.tsx +++ b/app/src/components/ExperimentSettingsDrawer/DeleteButton.tsx @@ -14,6 +14,7 @@ import { import { useRouter } from "next/router"; import { useRef } from "react"; import { BsTrash } from "react-icons/bs"; +import { useAppStore } from "~/state/store"; import { api } from "~/utils/api"; import { useExperiment, useHandledAsyncCallback } from "~/utils/hooks"; @@ -23,6 +24,8 @@ export const DeleteButton = () => { const utils = api.useContext(); const router = useRouter(); + const closeDrawer = useAppStore((s) => s.closeDrawer); + const { isOpen, onOpen, onClose } = useDisclosure(); const cancelRef = useRef(null); @@ -31,6 +34,8 @@ export const DeleteButton = () => { await mutation.mutateAsync({ id: experiment.data.id }); await utils.experiments.list.invalidate(); await router.push({ pathname: "/experiments" }); + closeDrawer(); + onClose(); }, [mutation, experiment.data?.id, router]); diff --git a/app/src/components/nav/AppShell.tsx b/app/src/components/nav/AppShell.tsx index b6aec3e..2356dca 100644 --- a/app/src/components/nav/AppShell.tsx +++ b/app/src/components/nav/AppShell.tsx @@ -7,48 +7,21 @@ import { Image, Text, Box, - type BoxProps, Link as ChakraLink, Flex, } from "@chakra-ui/react"; import Head from "next/head"; -import Link, { type LinkProps } from "next/link"; +import Link from "next/link"; import { BsGithub, BsPersonCircle } from "react-icons/bs"; -import { useRouter } from "next/router"; -import { type IconType } from "react-icons"; import { RiDatabase2Line, RiFlaskLine } from "react-icons/ri"; import { signIn, useSession } from "next-auth/react"; import UserMenu from "./UserMenu"; import { env } from "~/env.mjs"; +import ProjectMenu from "./ProjectMenu"; +import NavSidebarOption from "./NavSidebarOption"; +import IconLink from "./IconLink"; -type IconLinkProps = BoxProps & LinkProps & { label?: string; icon: IconType; href: string }; - -const IconLink = ({ icon, label, href, color, ...props }: IconLinkProps) => { - const router = useRouter(); - const isActive = href && router.pathname.startsWith(href); - return ( - - - - - {label} - - - - ); -}; - -const Divider = () => ; +const Divider = () => ; const NavSidebar = () => { const user = useSession().data; @@ -56,22 +29,28 @@ const NavSidebar = () => { return ( - + OpenPipe - + + {user != null && ( <> + + {env.NEXT_PUBLIC_SHOW_DATA && ( @@ -79,29 +58,26 @@ const NavSidebar = () => { )} {user === null && ( - { - signIn("github").catch(console.error); - }} - > - - - Sign In - - + + { + signIn("github").catch(console.error); + }} + > + + + Sign In + + + )} - {user ? ( - - ) : ( - - )} + {user && } + { + return ( + + + + + + {label} + + + + + ); +}; + +export default IconLink; diff --git a/app/src/components/nav/NavSidebarOption.tsx b/app/src/components/nav/NavSidebarOption.tsx new file mode 100644 index 0000000..9c3afea --- /dev/null +++ b/app/src/components/nav/NavSidebarOption.tsx @@ -0,0 +1,26 @@ +import { Box, type BoxProps } from "@chakra-ui/react"; +import { useRouter } from "next/router"; + +const NavSidebarOption = ({ + activeHrefPattern, + ...props +}: { activeHrefPattern?: string } & BoxProps) => { + const router = useRouter(); + const isActive = activeHrefPattern && router.pathname.startsWith(activeHrefPattern); + return ( + + {props.children} + + ); +}; + +export default NavSidebarOption; diff --git a/app/src/components/nav/ProjectMenu.tsx b/app/src/components/nav/ProjectMenu.tsx new file mode 100644 index 0000000..faa2e4c --- /dev/null +++ b/app/src/components/nav/ProjectMenu.tsx @@ -0,0 +1,74 @@ +import { + HStack, + VStack, + Text, + Popover, + PopoverTrigger, + PopoverContent, + Flex, +} from "@chakra-ui/react"; +import { useEffect } from "react"; +import Link from "next/link"; + +import { useAppStore } from "~/state/store"; +import { api } from "~/utils/api"; +import NavSidebarOption from "./NavSidebarOption"; +import { useSelectedOrg } from "~/utils/hooks"; + +export default function ProjectMenu() { + const selectedOrgId = useAppStore((s) => s.selectedOrgId); + const setSelectedOrgId = useAppStore((s) => s.setSelectedOrgId); + + const { data } = api.organizations.list.useQuery(); + + useEffect(() => { + if (data && data[0] && (!selectedOrgId || !data.find((org) => org.id === selectedOrgId))) { + setSelectedOrgId(data[0].id); + } + }, [selectedOrgId, setSelectedOrgId, data]); + + const { data: selectedOrg } = useSelectedOrg(); + + return ( + <> + + + + + PROJECT + + + + + + {selectedOrg?.name[0]?.toUpperCase()} + + + {selectedOrg?.name} + + + + + + + + + + ); +} diff --git a/app/src/components/nav/UserMenu.tsx b/app/src/components/nav/UserMenu.tsx index 0f70e31..7abf17b 100644 --- a/app/src/components/nav/UserMenu.tsx +++ b/app/src/components/nav/UserMenu.tsx @@ -14,6 +14,7 @@ import { import { type Session } from "next-auth"; import { signOut } from "next-auth/react"; import { BsBoxArrowRight, BsChevronRight, BsPersonCircle } from "react-icons/bs"; +import NavSidebarOption from "./NavSidebarOption"; export default function UserMenu({ user, ...rest }: { user: Session } & StackProps) { const { colorMode } = useColorMode(); @@ -27,30 +28,39 @@ export default function UserMenu({ user, ...rest }: { user: Session } & StackPro return ( <> - - + - {profileImage} - - - {user.user.name} - - - {user.user.email} - - - - - + ACCOUNT + + + + + {profileImage} + + + {user.user.name} + + + {/* {user.user.email} */} + + + + + + + {/* sign out */} diff --git a/app/src/pages/data/[id].tsx b/app/src/pages/data/[id].tsx index 8d06b53..5b68a02 100644 --- a/app/src/pages/data/[id].tsx +++ b/app/src/pages/data/[id].tsx @@ -56,8 +56,7 @@ export default function Dataset() { - + {datasetId && } diff --git a/app/src/pages/data/index.tsx b/app/src/pages/data/index.tsx index b1e581e..5c9a411 100644 --- a/app/src/pages/data/index.tsx +++ b/app/src/pages/data/index.tsx @@ -50,7 +50,7 @@ export default function DatasetsPage() { return ( - + @@ -60,7 +60,7 @@ export default function DatasetsPage() { - + {datasets.data && !datasets.isLoading ? ( datasets?.data?.map((dataset) => ( diff --git a/app/src/pages/experiments/[id].tsx b/app/src/pages/experiments/[id].tsx index 998b19c..83dfcbb 100644 --- a/app/src/pages/experiments/[id].tsx +++ b/app/src/pages/experiments/[id].tsx @@ -105,7 +105,8 @@ export default function Experiment() { - + @@ -60,7 +60,7 @@ export default function ExperimentsPage() { - + {experiments.data && !experiments.isLoading ? ( experiments?.data?.map((exp) => ) diff --git a/app/src/pages/home/index.tsx b/app/src/pages/home/index.tsx new file mode 100644 index 0000000..b79caec --- /dev/null +++ b/app/src/pages/home/index.tsx @@ -0,0 +1,56 @@ +import { Breadcrumb, BreadcrumbItem, HStack, Input } from "@chakra-ui/react"; +import { useEffect, useState } from "react"; +import AppShell from "~/components/nav/AppShell"; +import { api } from "~/utils/api"; +import { useHandledAsyncCallback, useSelectedOrg } from "~/utils/hooks"; + +export default function HomePage() { + const utils = api.useContext(); + const { data: selectedOrg } = useSelectedOrg(); + + const updateMutation = api.organizations.update.useMutation(); + const [onSaveName] = useHandledAsyncCallback(async () => { + if (name && name !== selectedOrg?.name && selectedOrg?.id) { + await updateMutation.mutateAsync({ + id: selectedOrg.id, + updates: { name }, + }); + await Promise.all([utils.organizations.get.invalidate({ id: selectedOrg.id })]); + } + }, [updateMutation, selectedOrg]); + + const [name, setName] = useState(selectedOrg?.name); + useEffect(() => { + setName(selectedOrg?.name); + }, [selectedOrg?.name]); + return ( + + + + + setName(e.target.value)} + onBlur={onSaveName} + borderWidth={1} + borderColor="transparent" + fontSize={16} + px={0} + minW={{ base: 100, lg: 300 }} + flex={1} + _hover={{ borderColor: "gray.300" }} + _focus={{ borderColor: "blue.500", outline: "none" }} + /> + + + + + ); +} diff --git a/app/src/pages/index.tsx b/app/src/pages/index.tsx index ac4897c..7ddaa6b 100644 --- a/app/src/pages/index.tsx +++ b/app/src/pages/index.tsx @@ -4,7 +4,7 @@ import { type GetServerSideProps } from "next"; export const getServerSideProps: GetServerSideProps = async () => { return { redirect: { - destination: "/experiments", + destination: "/home", permanent: false, }, }; diff --git a/app/src/server/api/root.router.ts b/app/src/server/api/root.router.ts index 9ff807f..d50d907 100644 --- a/app/src/server/api/root.router.ts +++ b/app/src/server/api/root.router.ts @@ -9,6 +9,7 @@ import { worldChampsRouter } from "./routers/worldChamps.router"; import { datasetsRouter } from "./routers/datasets.router"; import { datasetEntries } from "./routers/datasetEntries.router"; import { externalApiRouter } from "./routers/externalApi.router"; +import { organizationsRouter } from "./routers/organizations.router"; /** * This is the primary router for your server. @@ -25,6 +26,7 @@ export const appRouter = createTRPCRouter({ worldChamps: worldChampsRouter, datasets: datasetsRouter, datasetEntries: datasetEntries, + organizations: organizationsRouter, externalApi: externalApiRouter, }); diff --git a/app/src/server/api/routers/organizations.router.ts b/app/src/server/api/routers/organizations.router.ts new file mode 100644 index 0000000..195532c --- /dev/null +++ b/app/src/server/api/routers/organizations.router.ts @@ -0,0 +1,71 @@ +import { v4 as uuidv4 } from "uuid"; +import { z } from "zod"; + +import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc"; +import { prisma } from "~/server/db"; +import { requireCanModifyOrganization, requireNothing } from "~/utils/accessControl"; + +export const organizationsRouter = createTRPCRouter({ + list: protectedProcedure.query(async ({ ctx }) => { + const userId = ctx.session.user.id; + requireNothing(ctx); + + if (!userId) { + return null; + } + + const organizations = await prisma.organization.findMany({ + where: { + organizationUsers: { + some: { userId: ctx.session.user.id }, + }, + }, + orderBy: { + createdAt: "desc", + }, + }); + + if (!organizations.length) { + const newOrgId = uuidv4(); + const [newOrg] = await prisma.$transaction([ + prisma.organization.create({ + data: { + id: newOrgId, + personalOrgUserId: userId, + }, + }), + prisma.organizationUser.create({ + data: { + userId, + organizationId: newOrgId, + role: "ADMIN", + }, + }), + ]); + organizations.push(newOrg); + } + + return organizations; + }), + get: protectedProcedure.input(z.object({ id: z.string() })).query(async ({ input, ctx }) => { + requireNothing(ctx); + return await prisma.organization.findUnique({ + where: { + id: input.id, + }, + }); + }), + update: protectedProcedure + .input(z.object({ id: z.string(), updates: z.object({ name: z.string() }) })) + .mutation(async ({ input, ctx }) => { + await requireCanModifyOrganization(input.id, ctx); + return await prisma.organization.update({ + where: { + id: input.id, + }, + data: { + name: input.updates.name, + }, + }); + }), +}); diff --git a/app/src/server/tasks/worker.ts b/app/src/server/tasks/worker.ts index 74be92f..c49b960 100644 --- a/app/src/server/tasks/worker.ts +++ b/app/src/server/tasks/worker.ts @@ -17,7 +17,7 @@ const taskList = registeredTasks.reduce((acc, task) => { // Run a worker to execute jobs: const runner = await run({ connectionString: env.DATABASE_URL, - concurrency: 50, + concurrency: 10, // Install signal handlers for graceful shutdown on SIGINT, SIGTERM, etc noHandleSignals: false, pollInterval: 1000, diff --git a/app/src/server/utils/openai.ts b/app/src/server/utils/openai.ts index 9112474..5397e2c 100644 --- a/app/src/server/utils/openai.ts +++ b/app/src/server/utils/openai.ts @@ -1,10 +1,7 @@ import { env } from "~/env.mjs"; // import OpenAI from "openai"; - -// import { OpenPipe } from "../../../../client-libs/js/openai/index"; - import { OpenAI } from "openpipe"; // Set a dummy key so it doesn't fail at build time -export const openai = new OpenAI.OpenPipe({ apiKey: env.OPENAI_API_KEY ?? "dummy-key" }); +export const openai = new OpenAI.OpenAI({ apiKey: env.OPENAI_API_KEY ?? "dummy-key" }); diff --git a/app/src/state/store.ts b/app/src/state/store.ts index ba8a506..5584b21 100644 --- a/app/src/state/store.ts +++ b/app/src/state/store.ts @@ -14,6 +14,8 @@ export type State = { api: APIClient | null; setApi: (api: APIClient) => void; sharedVariantEditor: SharedVariantEditorSlice; + selectedOrgId: string | null; + setSelectedOrgId: (orgId: string) => void; }; export type SliceCreator = StateCreator; @@ -39,6 +41,11 @@ const useBaseStore = create( state.drawerOpen = false; }), sharedVariantEditor: createVariantEditorSlice(set, get, ...rest), + selectedOrgId: null, + setSelectedOrgId: (orgId: string) => + set((state) => { + state.selectedOrgId = orgId; + }), })), ); diff --git a/app/src/utils/accessControl.ts b/app/src/utils/accessControl.ts index 5fc1fd4..98aca75 100644 --- a/app/src/utils/accessControl.ts +++ b/app/src/utils/accessControl.ts @@ -16,6 +16,27 @@ export const requireNothing = (ctx: TRPCContext) => { ctx.markAccessControlRun(); }; +export const requireCanModifyOrganization = async (organizationId: string, ctx: TRPCContext) => { + const userId = ctx.session?.user.id; + if (!userId) { + throw new TRPCError({ code: "UNAUTHORIZED" }); + } + + const canModify = await prisma.organizationUser.findFirst({ + where: { + userId, + organizationId, + role: { in: [OrganizationUserRole.ADMIN, OrganizationUserRole.MEMBER] }, + }, + }); + + if (!canModify) { + throw new TRPCError({ code: "UNAUTHORIZED" }); + } + + ctx.markAccessControlRun(); +} + export const requireCanViewDataset = async (datasetId: string, ctx: TRPCContext) => { const dataset = await prisma.dataset.findFirst({ where: { diff --git a/app/src/utils/hooks.ts b/app/src/utils/hooks.ts index 451c7dc..3c1518f 100644 --- a/app/src/utils/hooks.ts +++ b/app/src/utils/hooks.ts @@ -2,6 +2,7 @@ import { useRouter } from "next/router"; import { type RefObject, useCallback, useEffect, useRef, useState } from "react"; import { api } from "~/utils/api"; import { NumberParam, useQueryParam, withDefault } from "use-query-params"; +import { useAppStore } from "~/state/store"; export const useExperiment = () => { const router = useRouter(); @@ -132,3 +133,11 @@ export const useScenario = (scenarioId: string) => { }; export const useVisibleScenarioIds = () => useScenarios().data?.scenarios.map((s) => s.id) ?? []; + +export const useSelectedOrg = () => { + const selectedOrgId = useAppStore((state) => state.selectedOrgId); + return api.organizations.get.useQuery( + { id: selectedOrgId ?? "" }, + { enabled: !!selectedOrgId }, + ); +}; diff --git a/client-libs/typescript/openai/index.ts b/client-libs/typescript/openai/index.ts index 8c43fed..81df9f3 100644 --- a/client-libs/typescript/openai/index.ts +++ b/client-libs/typescript/openai/index.ts @@ -1,15 +1,15 @@ -import * as OpenAI from "openai-beta"; +import * as openai from "openai-beta"; import { readEnv } from "openai-beta/core"; // Anything we don't override we want to pass through to openai directly export * as openai from "openai-beta"; -interface ClientOptions extends OpenAI.ClientOptions { +interface ClientOptions extends openai.ClientOptions { openPipeApiKey?: string; openPipeBaseUrl?: string; } -export class OpenPipe extends OpenAI.OpenAI { +export class OpenAI extends openai.OpenAI { openPipeApiKey: string; openPipeBaseUrl: string; From 1a838824ae701ab2387fa8030aa59470c45a5f9f Mon Sep 17 00:00:00 2001 From: David Corbitt Date: Mon, 7 Aug 2023 11:16:54 -0700 Subject: [PATCH 07/49] Use PageHeaderContainer for all breadcrumbs --- app/@types/nextjs-routes.d.ts | 1 + app/src/components/nav/AppShell.tsx | 17 +++++- .../components/nav/PageHeaderContainer.tsx | 15 +++++ app/src/components/nav/UserMenu.tsx | 57 +++++++------------ app/src/pages/data/[id].tsx | 13 ++--- app/src/pages/data/index.tsx | 57 +++++++++---------- app/src/pages/experiments/[id].tsx | 14 ++--- app/src/pages/experiments/index.tsx | 47 +++++++-------- app/src/pages/home/index.tsx | 13 ++--- app/src/pages/settings/index.tsx | 52 +++++++++++++++++ 10 files changed, 167 insertions(+), 119 deletions(-) create mode 100644 app/src/components/nav/PageHeaderContainer.tsx create mode 100644 app/src/pages/settings/index.tsx diff --git a/app/@types/nextjs-routes.d.ts b/app/@types/nextjs-routes.d.ts index fbccfff..79d87f2 100644 --- a/app/@types/nextjs-routes.d.ts +++ b/app/@types/nextjs-routes.d.ts @@ -25,6 +25,7 @@ declare module "nextjs-routes" { | StaticRoute<"/home"> | StaticRoute<"/"> | StaticRoute<"/sentry-example-page"> + | StaticRoute<"/settings"> | StaticRoute<"/world-champs"> | StaticRoute<"/world-champs/signup">; diff --git a/app/src/components/nav/AppShell.tsx b/app/src/components/nav/AppShell.tsx index 2356dca..4dcbdfc 100644 --- a/app/src/components/nav/AppShell.tsx +++ b/app/src/components/nav/AppShell.tsx @@ -12,7 +12,7 @@ import { } from "@chakra-ui/react"; import Head from "next/head"; import Link from "next/link"; -import { BsGithub, BsPersonCircle } from "react-icons/bs"; +import { BsGearFill, BsGithub, BsPersonCircle } from "react-icons/bs"; import { RiDatabase2Line, RiFlaskLine } from "react-icons/ri"; import { signIn, useSession } from "next-auth/react"; import UserMenu from "./UserMenu"; @@ -76,6 +76,21 @@ const NavSidebar = () => { )} + + + CONFIGURATION + + + + + {user && } diff --git a/app/src/components/nav/PageHeaderContainer.tsx b/app/src/components/nav/PageHeaderContainer.tsx new file mode 100644 index 0000000..944f33d --- /dev/null +++ b/app/src/components/nav/PageHeaderContainer.tsx @@ -0,0 +1,15 @@ +import { Flex, type FlexProps } from "@chakra-ui/react"; + +const PageHeaderContainer = (props: FlexProps) => { + return ; +}; + +export default PageHeaderContainer; diff --git a/app/src/components/nav/UserMenu.tsx b/app/src/components/nav/UserMenu.tsx index 7abf17b..8960e5b 100644 --- a/app/src/components/nav/UserMenu.tsx +++ b/app/src/components/nav/UserMenu.tsx @@ -8,7 +8,6 @@ import { PopoverTrigger, PopoverContent, Link, - useColorMode, type StackProps, } from "@chakra-ui/react"; import { type Session } from "next-auth"; @@ -17,7 +16,6 @@ import { BsBoxArrowRight, BsChevronRight, BsPersonCircle } from "react-icons/bs" import NavSidebarOption from "./NavSidebarOption"; export default function UserMenu({ user, ...rest }: { user: Session } & StackProps) { - const { colorMode } = useColorMode(); const profileImage = user.user.image ? ( profile picture @@ -28,39 +26,28 @@ export default function UserMenu({ user, ...rest }: { user: Session } & StackPro return ( <> - - - ACCOUNT - - - - - {profileImage} - - - {user.user.name} - - - {/* {user.user.email} */} - - - - - - - + + + + {profileImage} + + + {user.user.name} + + + {/* {user.user.email} */} + + + + + + {/* sign out */} diff --git a/app/src/pages/data/[id].tsx b/app/src/pages/data/[id].tsx index 5b68a02..2cb6228 100644 --- a/app/src/pages/data/[id].tsx +++ b/app/src/pages/data/[id].tsx @@ -18,6 +18,7 @@ import { api } from "~/utils/api"; import { useDataset, useHandledAsyncCallback } from "~/utils/hooks"; import DatasetEntriesTable from "~/components/datasets/DatasetEntriesTable"; import { DatasetHeaderButtons } from "~/components/datasets/DatasetHeaderButtons/DatasetHeaderButtons"; +import PageHeaderContainer from "~/components/nav/PageHeaderContainer"; export default function Dataset() { const router = useRouter(); @@ -55,14 +56,8 @@ export default function Dataset() { return ( - - + + @@ -88,7 +83,7 @@ export default function Dataset() { - + {datasetId && } diff --git a/app/src/pages/data/index.tsx b/app/src/pages/data/index.tsx index 5c9a411..94b5f50 100644 --- a/app/src/pages/data/index.tsx +++ b/app/src/pages/data/index.tsx @@ -1,14 +1,12 @@ import { SimpleGrid, Icon, - VStack, Breadcrumb, BreadcrumbItem, Flex, Center, Text, Link, - HStack, } from "@chakra-ui/react"; import AppShell from "~/components/nav/AppShell"; import { api } from "~/utils/api"; @@ -19,6 +17,7 @@ import { DatasetCardSkeleton, NewDatasetCard, } from "~/components/datasets/DatasetCard"; +import PageHeaderContainer from "~/components/nav/PageHeaderContainer"; export default function DatasetsPage() { const datasets = api.datasets.list.useQuery(); @@ -50,34 +49,32 @@ export default function DatasetsPage() { return ( - - - - - - Datasets - - - - - - - {datasets.data && !datasets.isLoading ? ( - datasets?.data?.map((dataset) => ( - - )) - ) : ( - <> - - - - - )} - - + + + + + Datasets + + + + + + + {datasets.data && !datasets.isLoading ? ( + datasets?.data?.map((dataset) => ( + + )) + ) : ( + <> + + + + + )} + ); } diff --git a/app/src/pages/experiments/[id].tsx b/app/src/pages/experiments/[id].tsx index 83dfcbb..515986b 100644 --- a/app/src/pages/experiments/[id].tsx +++ b/app/src/pages/experiments/[id].tsx @@ -23,6 +23,7 @@ import { useAppStore } from "~/state/store"; import { useSyncVariantEditor } from "~/state/sync"; import { ExperimentHeaderButtons } from "~/components/experiments/ExperimentHeaderButtons/ExperimentHeaderButtons"; import Head from "next/head"; +import PageHeaderContainer from "~/components/nav/PageHeaderContainer"; // TODO: import less to fix deployment with server side props // export const getServerSideProps = async (context: GetServerSidePropsContext<{ id: string }>) => { @@ -104,15 +105,8 @@ export default function Experiment() { )} - - + + @@ -144,7 +138,7 @@ export default function Experiment() { - + diff --git a/app/src/pages/experiments/index.tsx b/app/src/pages/experiments/index.tsx index b9915e7..eb66b72 100644 --- a/app/src/pages/experiments/index.tsx +++ b/app/src/pages/experiments/index.tsx @@ -1,14 +1,12 @@ import { SimpleGrid, Icon, - VStack, Breadcrumb, BreadcrumbItem, Flex, Center, Text, Link, - HStack, } from "@chakra-ui/react"; import { RiFlaskLine } from "react-icons/ri"; import AppShell from "~/components/nav/AppShell"; @@ -19,6 +17,7 @@ import { NewExperimentCard, } from "~/components/experiments/ExperimentCard"; import { signIn, useSession } from "next-auth/react"; +import PageHeaderContainer from "~/components/nav/PageHeaderContainer"; export default function ExperimentsPage() { const experiments = api.experiments.list.useQuery(); @@ -50,29 +49,27 @@ export default function ExperimentsPage() { return ( - - - - - - Experiments - - - - - - - {experiments.data && !experiments.isLoading ? ( - experiments?.data?.map((exp) => ) - ) : ( - <> - - - - - )} - - + + + + + Experiments + + + + + + + {experiments.data && !experiments.isLoading ? ( + experiments?.data?.map((exp) => ) + ) : ( + <> + + + + + )} + ); } diff --git a/app/src/pages/home/index.tsx b/app/src/pages/home/index.tsx index b79caec..516a6e8 100644 --- a/app/src/pages/home/index.tsx +++ b/app/src/pages/home/index.tsx @@ -1,6 +1,7 @@ -import { Breadcrumb, BreadcrumbItem, HStack, Input } from "@chakra-ui/react"; +import { Breadcrumb, BreadcrumbItem, Input } from "@chakra-ui/react"; import { useEffect, useState } from "react"; import AppShell from "~/components/nav/AppShell"; +import PageHeaderContainer from "~/components/nav/PageHeaderContainer"; import { api } from "~/utils/api"; import { useHandledAsyncCallback, useSelectedOrg } from "~/utils/hooks"; @@ -25,13 +26,7 @@ export default function HomePage() { }, [selectedOrg?.name]); return ( - + - + ); } diff --git a/app/src/pages/settings/index.tsx b/app/src/pages/settings/index.tsx new file mode 100644 index 0000000..ef77742 --- /dev/null +++ b/app/src/pages/settings/index.tsx @@ -0,0 +1,52 @@ +import { Breadcrumb, BreadcrumbItem, Input } from "@chakra-ui/react"; +import { useEffect, useState } from "react"; +import AppShell from "~/components/nav/AppShell"; +import PageHeaderContainer from "~/components/nav/PageHeaderContainer"; +import { api } from "~/utils/api"; +import { useHandledAsyncCallback, useSelectedOrg } from "~/utils/hooks"; + +export default function Settings() { + const utils = api.useContext(); + const { data: selectedOrg } = useSelectedOrg(); + + const updateMutation = api.organizations.update.useMutation(); + const [onSaveName] = useHandledAsyncCallback(async () => { + if (name && name !== selectedOrg?.name && selectedOrg?.id) { + await updateMutation.mutateAsync({ + id: selectedOrg.id, + updates: { name }, + }); + await Promise.all([utils.organizations.get.invalidate({ id: selectedOrg.id })]); + } + }, [updateMutation, selectedOrg]); + + const [name, setName] = useState(selectedOrg?.name); + useEffect(() => { + setName(selectedOrg?.name); + }, [selectedOrg?.name]); + + return ( + + + + + setName(e.target.value)} + onBlur={onSaveName} + borderWidth={1} + borderColor="transparent" + fontSize={16} + px={0} + minW={{ base: 100, lg: 300 }} + flex={1} + _hover={{ borderColor: "gray.300" }} + _focus={{ borderColor: "blue.500", outline: "none" }} + /> + + + + + ); +} From 57166e96b40a15cf336c39512a98d5feeb95db5a Mon Sep 17 00:00:00 2001 From: David Corbitt Date: Mon, 7 Aug 2023 11:21:59 -0700 Subject: [PATCH 08/49] Fix project settings IconLink --- app/src/components/nav/AppShell.tsx | 4 +--- app/src/components/nav/IconLink.tsx | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/app/src/components/nav/AppShell.tsx b/app/src/components/nav/AppShell.tsx index 4dcbdfc..43f2217 100644 --- a/app/src/components/nav/AppShell.tsx +++ b/app/src/components/nav/AppShell.tsx @@ -87,9 +87,7 @@ const NavSidebar = () => { > CONFIGURATION - - - + {user && } diff --git a/app/src/components/nav/IconLink.tsx b/app/src/components/nav/IconLink.tsx index 4171b63..e961a34 100644 --- a/app/src/components/nav/IconLink.tsx +++ b/app/src/components/nav/IconLink.tsx @@ -11,7 +11,7 @@ const IconLink = ({ icon, label, href, color, ...props }: IconLinkProps) => { - + {label} From c9f59bfb7919f85e167c066c2daf587844028d23 Mon Sep 17 00:00:00 2001 From: David Corbitt Date: Mon, 7 Aug 2023 11:50:59 -0700 Subject: [PATCH 09/49] Add project to breadcrumb --- .../components/nav/PageHeaderContainer.tsx | 2 ++ .../nav/ProjectBreadcrumbContents.tsx | 34 +++++++++++++++++++ app/src/pages/data/[id].tsx | 4 +++ app/src/pages/data/index.tsx | 4 +++ app/src/pages/experiments/[id].tsx | 5 +++ app/src/pages/experiments/index.tsx | 4 +++ app/src/pages/home/index.tsx | 23 ++++--------- app/src/pages/settings/index.tsx | 22 ++++-------- 8 files changed, 67 insertions(+), 31 deletions(-) create mode 100644 app/src/components/nav/ProjectBreadcrumbContents.tsx diff --git a/app/src/components/nav/PageHeaderContainer.tsx b/app/src/components/nav/PageHeaderContainer.tsx index 944f33d..f17b312 100644 --- a/app/src/components/nav/PageHeaderContainer.tsx +++ b/app/src/components/nav/PageHeaderContainer.tsx @@ -4,10 +4,12 @@ const PageHeaderContainer = (props: FlexProps) => { return ; }; diff --git a/app/src/components/nav/ProjectBreadcrumbContents.tsx b/app/src/components/nav/ProjectBreadcrumbContents.tsx new file mode 100644 index 0000000..68de209 --- /dev/null +++ b/app/src/components/nav/ProjectBreadcrumbContents.tsx @@ -0,0 +1,34 @@ +import { HStack, Flex, Text } from "@chakra-ui/react"; +import Link from "next/link"; + +import { useSelectedOrg } from "~/utils/hooks"; + +// Have to export only contents here instead of full BreadcrumbItem because Chakra doesn't +// recognize a BreadcrumbItem exported with this component as a valid child of Breadcrumb. + +export default function ProjectBreadcrumbContents() { + const { data: selectedOrg } = useSelectedOrg(); + + return ( + + + + {selectedOrg?.name[0]?.toUpperCase()} + + + {selectedOrg?.name} + + + + ); +} diff --git a/app/src/pages/data/[id].tsx b/app/src/pages/data/[id].tsx index 2cb6228..63921a6 100644 --- a/app/src/pages/data/[id].tsx +++ b/app/src/pages/data/[id].tsx @@ -19,6 +19,7 @@ import { useDataset, useHandledAsyncCallback } from "~/utils/hooks"; import DatasetEntriesTable from "~/components/datasets/DatasetEntriesTable"; import { DatasetHeaderButtons } from "~/components/datasets/DatasetHeaderButtons/DatasetHeaderButtons"; import PageHeaderContainer from "~/components/nav/PageHeaderContainer"; +import ProjectBreadcrumbContents from "~/components/nav/ProjectBreadcrumbContents"; export default function Dataset() { const router = useRouter(); @@ -58,6 +59,9 @@ export default function Dataset() { + + + diff --git a/app/src/pages/data/index.tsx b/app/src/pages/data/index.tsx index 94b5f50..843a63e 100644 --- a/app/src/pages/data/index.tsx +++ b/app/src/pages/data/index.tsx @@ -18,6 +18,7 @@ import { NewDatasetCard, } from "~/components/datasets/DatasetCard"; import PageHeaderContainer from "~/components/nav/PageHeaderContainer"; +import ProjectBreadcrumbContents from "~/components/nav/ProjectBreadcrumbContents"; export default function DatasetsPage() { const datasets = api.datasets.list.useQuery(); @@ -51,6 +52,9 @@ export default function DatasetsPage() { + + + Datasets diff --git a/app/src/pages/experiments/[id].tsx b/app/src/pages/experiments/[id].tsx index 515986b..04ce91d 100644 --- a/app/src/pages/experiments/[id].tsx +++ b/app/src/pages/experiments/[id].tsx @@ -24,6 +24,8 @@ import { useSyncVariantEditor } from "~/state/sync"; import { ExperimentHeaderButtons } from "~/components/experiments/ExperimentHeaderButtons/ExperimentHeaderButtons"; import Head from "next/head"; import PageHeaderContainer from "~/components/nav/PageHeaderContainer"; +import ProjectBreadcrumbContents from "~/components/nav/ProjectBreadcrumbContents"; + // TODO: import less to fix deployment with server side props // export const getServerSideProps = async (context: GetServerSidePropsContext<{ id: string }>) => { @@ -107,6 +109,9 @@ export default function Experiment() { + + + diff --git a/app/src/pages/experiments/index.tsx b/app/src/pages/experiments/index.tsx index eb66b72..59e8f25 100644 --- a/app/src/pages/experiments/index.tsx +++ b/app/src/pages/experiments/index.tsx @@ -18,6 +18,7 @@ import { } from "~/components/experiments/ExperimentCard"; import { signIn, useSession } from "next-auth/react"; import PageHeaderContainer from "~/components/nav/PageHeaderContainer"; +import ProjectBreadcrumbContents from "~/components/nav/ProjectBreadcrumbContents"; export default function ExperimentsPage() { const experiments = api.experiments.list.useQuery(); @@ -51,6 +52,9 @@ export default function ExperimentsPage() { + + + Experiments diff --git a/app/src/pages/home/index.tsx b/app/src/pages/home/index.tsx index 516a6e8..6927cd8 100644 --- a/app/src/pages/home/index.tsx +++ b/app/src/pages/home/index.tsx @@ -1,7 +1,8 @@ -import { Breadcrumb, BreadcrumbItem, Input } from "@chakra-ui/react"; +import { Breadcrumb, BreadcrumbItem, Text } from "@chakra-ui/react"; import { useEffect, useState } from "react"; import AppShell from "~/components/nav/AppShell"; import PageHeaderContainer from "~/components/nav/PageHeaderContainer"; +import ProjectBreadcrumbContents from "~/components/nav/ProjectBreadcrumbContents"; import { api } from "~/utils/api"; import { useHandledAsyncCallback, useSelectedOrg } from "~/utils/hooks"; @@ -27,22 +28,12 @@ export default function HomePage() { return ( - + + + + - setName(e.target.value)} - onBlur={onSaveName} - borderWidth={1} - borderColor="transparent" - fontSize={16} - px={0} - minW={{ base: 100, lg: 300 }} - flex={1} - _hover={{ borderColor: "gray.300" }} - _focus={{ borderColor: "blue.500", outline: "none" }} - /> + Home diff --git a/app/src/pages/settings/index.tsx b/app/src/pages/settings/index.tsx index ef77742..8566bc4 100644 --- a/app/src/pages/settings/index.tsx +++ b/app/src/pages/settings/index.tsx @@ -1,9 +1,11 @@ -import { Breadcrumb, BreadcrumbItem, Input } from "@chakra-ui/react"; +import { Breadcrumb, BreadcrumbItem, Text } from "@chakra-ui/react"; import { useEffect, useState } from "react"; + import AppShell from "~/components/nav/AppShell"; import PageHeaderContainer from "~/components/nav/PageHeaderContainer"; import { api } from "~/utils/api"; import { useHandledAsyncCallback, useSelectedOrg } from "~/utils/hooks"; +import ProjectBreadcrumbContents from "~/components/nav/ProjectBreadcrumbContents"; export default function Settings() { const utils = api.useContext(); @@ -29,21 +31,11 @@ export default function Settings() { + + + - setName(e.target.value)} - onBlur={onSaveName} - borderWidth={1} - borderColor="transparent" - fontSize={16} - px={0} - minW={{ base: 100, lg: 300 }} - flex={1} - _hover={{ borderColor: "gray.300" }} - _focus={{ borderColor: "blue.500", outline: "none" }} - /> + Project Settings From 8f49bace53a7fd9648092d934f5511ffd1c43b96 Mon Sep 17 00:00:00 2001 From: David Corbitt Date: Mon, 7 Aug 2023 13:08:33 -0700 Subject: [PATCH 10/49] Backfill api keys --- app/src/components/CopiableCode.tsx | 43 ++++++++++++ app/src/pages/settings/index.tsx | 70 ++++++++++++++++++- .../api/routers/organizations.router.ts | 11 +++ app/src/server/scripts/backfillApiKeys.ts | 33 +++++++++ app/src/server/utils/generateApiKey.ts | 11 +++ 5 files changed, 167 insertions(+), 1 deletion(-) create mode 100644 app/src/components/CopiableCode.tsx create mode 100644 app/src/server/scripts/backfillApiKeys.ts create mode 100644 app/src/server/utils/generateApiKey.ts diff --git a/app/src/components/CopiableCode.tsx b/app/src/components/CopiableCode.tsx new file mode 100644 index 0000000..8215b78 --- /dev/null +++ b/app/src/components/CopiableCode.tsx @@ -0,0 +1,43 @@ +import { HStack, Icon, IconButton, Tooltip, Text } from "@chakra-ui/react"; +import { useCallback, useState } from "react"; +import { MdContentCopy } from "react-icons/md"; + +const CopiableCode = ({ code }: { code: string }) => { + const [copied, setCopied] = useState(false); + + const copyToClipboard = useCallback(() => { + const onCopy = async () => { + console.log("copied!"); + await navigator.clipboard.writeText(code); + setCopied(true); + }; + void onCopy(); + }, [code]); + return ( + + + {code} + + + } + size="xs" + colorScheme="white" + variant="ghost" + onClick={copyToClipboard} + onMouseLeave={() => setCopied(false)} + /> + + + ); +}; + +export default CopiableCode; diff --git a/app/src/pages/settings/index.tsx b/app/src/pages/settings/index.tsx index 8566bc4..8ca407a 100644 --- a/app/src/pages/settings/index.tsx +++ b/app/src/pages/settings/index.tsx @@ -1,4 +1,13 @@ -import { Breadcrumb, BreadcrumbItem, Text } from "@chakra-ui/react"; +import { + Breadcrumb, + BreadcrumbItem, + Text, + type TextProps, + VStack, + Input, + Button, + Divider, +} from "@chakra-ui/react"; import { useEffect, useState } from "react"; import AppShell from "~/components/nav/AppShell"; @@ -6,11 +15,15 @@ import PageHeaderContainer from "~/components/nav/PageHeaderContainer"; import { api } from "~/utils/api"; import { useHandledAsyncCallback, useSelectedOrg } from "~/utils/hooks"; import ProjectBreadcrumbContents from "~/components/nav/ProjectBreadcrumbContents"; +import CopiableCode from "~/components/CopiableCode"; export default function Settings() { const utils = api.useContext(); const { data: selectedOrg } = useSelectedOrg(); + const apiKey = + selectedOrg?.apiKeys?.length && selectedOrg?.apiKeys[0] ? selectedOrg?.apiKeys[0].apiKey : ""; + const updateMutation = api.organizations.update.useMutation(); const [onSaveName] = useHandledAsyncCallback(async () => { if (name && name !== selectedOrg?.name && selectedOrg?.id) { @@ -39,6 +52,61 @@ export default function Settings() { + + + + Project Settings + + + Configure your project settings. These settings only apply to {selectedOrg?.name}. + + + + + + Display Name + + setName(e.target.value)} + borderColor="gray.300" + /> + + + + + Project API Key + + Use your project API key to authenticate your requests when sending data to OpenPipe. You can set this key in your environment variables, + or use it directly in your code. + + + + + ); } + +const Subtitle = (props: TextProps) => ; diff --git a/app/src/server/api/routers/organizations.router.ts b/app/src/server/api/routers/organizations.router.ts index 195532c..1216f54 100644 --- a/app/src/server/api/routers/organizations.router.ts +++ b/app/src/server/api/routers/organizations.router.ts @@ -3,6 +3,7 @@ import { z } from "zod"; import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc"; import { prisma } from "~/server/db"; +import { generateApiKey } from "~/server/utils/generateApiKey"; import { requireCanModifyOrganization, requireNothing } from "~/utils/accessControl"; export const organizationsRouter = createTRPCRouter({ @@ -41,6 +42,13 @@ export const organizationsRouter = createTRPCRouter({ role: "ADMIN", }, }), + prisma.apiKey.create({ + data: { + name: "Default API Key", + organizationId: newOrgId, + apiKey: generateApiKey(), + }, + }), ]); organizations.push(newOrg); } @@ -53,6 +61,9 @@ export const organizationsRouter = createTRPCRouter({ where: { id: input.id, }, + include: { + apiKeys: true, + } }); }), update: protectedProcedure diff --git a/app/src/server/scripts/backfillApiKeys.ts b/app/src/server/scripts/backfillApiKeys.ts new file mode 100644 index 0000000..869a21c --- /dev/null +++ b/app/src/server/scripts/backfillApiKeys.ts @@ -0,0 +1,33 @@ +import { type Prisma } from "@prisma/client"; +import { prisma } from "~/server/db"; +import { generateApiKey } from "~/server/utils/generateApiKey"; + +console.log("backfilling api keys"); + +const organizations = await prisma.organization.findMany({ + include: { + apiKeys: true, + }, +}); + +console.log(`found ${organizations.length} organizations`); + +const apiKeysToCreate: Prisma.ApiKeyCreateManyInput[] = []; + +for (const org of organizations) { + if (!org.apiKeys.length) { + apiKeysToCreate.push({ + name: "Default API Key", + organizationId: org.id, + apiKey: generateApiKey(), + }); + } +} + +console.log(`creating ${apiKeysToCreate.length} api keys`); + +await prisma.apiKey.createMany({ + data: apiKeysToCreate, +}); + +console.log("done"); \ No newline at end of file diff --git a/app/src/server/utils/generateApiKey.ts b/app/src/server/utils/generateApiKey.ts new file mode 100644 index 0000000..5ff451b --- /dev/null +++ b/app/src/server/utils/generateApiKey.ts @@ -0,0 +1,11 @@ +const KEY_LENGTH = 42; + +export const generateApiKey = () => { + const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + let randomChars = ""; + for (let i = 0; i < KEY_LENGTH; i++) { + randomChars += chars.charAt(Math.floor(Math.random() * chars.length)); + } + + return `opc_${randomChars}`; +}; From f8f855adf4f920ee4969638b2feecd22395eadc1 Mon Sep 17 00:00:00 2001 From: David Corbitt Date: Mon, 7 Aug 2023 14:10:01 -0700 Subject: [PATCH 11/49] Theme default divider --- app/src/theme/ChakraThemeProvider.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/app/src/theme/ChakraThemeProvider.tsx b/app/src/theme/ChakraThemeProvider.tsx index 0c3f247..a8b7ff7 100644 --- a/app/src/theme/ChakraThemeProvider.tsx +++ b/app/src/theme/ChakraThemeProvider.tsx @@ -1,6 +1,5 @@ -import { extendTheme } from "@chakra-ui/react"; +import { extendTheme, defineStyleConfig, ChakraProvider } from "@chakra-ui/react"; import "@fontsource/inconsolata"; -import { ChakraProvider } from "@chakra-ui/react"; import { modalAnatomy } from "@chakra-ui/anatomy"; import { createMultiStyleConfigHelpers } from "@chakra-ui/styled-system"; @@ -18,6 +17,13 @@ const modalTheme = defineMultiStyleConfig({ }), }); +const Divider = defineStyleConfig({ + baseStyle: { + borderColor: "gray.300", + backgroundColor: "gray.300", + }, +}); + const theme = extendTheme({ styles: { global: (props: { colorMode: "dark" | "light" }) => ({ @@ -53,6 +59,7 @@ const theme = extendTheme({ }, }, Modal: modalTheme, + Divider, }, }); From dc497dbd99c3ab1db56fee402b04cccee152a8b5 Mon Sep 17 00:00:00 2001 From: David Corbitt Date: Mon, 7 Aug 2023 14:10:32 -0700 Subject: [PATCH 12/49] Query experiments and datasets by org --- app/src/components/StatsCard.tsx | 28 +++++++ app/src/components/nav/UserMenu.tsx | 44 +++++------ app/src/pages/data/index.tsx | 4 +- app/src/pages/experiments/index.tsx | 4 +- app/src/pages/home/index.tsx | 46 +++++++----- app/src/server/api/routers/datasets.router.ts | 38 +++++----- .../server/api/routers/experiments.router.ts | 74 +++++++++---------- app/src/utils/accessControl.ts | 20 +++++ app/src/utils/hooks.ts | 21 +++++- 9 files changed, 172 insertions(+), 107 deletions(-) create mode 100644 app/src/components/StatsCard.tsx diff --git a/app/src/components/StatsCard.tsx b/app/src/components/StatsCard.tsx new file mode 100644 index 0000000..56eacfb --- /dev/null +++ b/app/src/components/StatsCard.tsx @@ -0,0 +1,28 @@ +import { VStack, HStack, type StackProps, Text, Divider } from "@chakra-ui/react"; +import Link, { type LinkProps } from "next/link"; + +const StatsCard = ({ + title, + href, + children, + ...rest +}: { title: string; href: string } & StackProps & LinkProps) => { + return ( + + + + {title} + + + + View all + + + + + {children} + + ); +}; + +export default StatsCard; diff --git a/app/src/components/nav/UserMenu.tsx b/app/src/components/nav/UserMenu.tsx index 8960e5b..9d538bc 100644 --- a/app/src/components/nav/UserMenu.tsx +++ b/app/src/components/nav/UserMenu.tsx @@ -9,6 +9,7 @@ import { PopoverContent, Link, type StackProps, + Box, } from "@chakra-ui/react"; import { type Session } from "next-auth"; import { signOut } from "next-auth/react"; @@ -16,7 +17,6 @@ import { BsBoxArrowRight, BsChevronRight, BsPersonCircle } from "react-icons/bs" import NavSidebarOption from "./NavSidebarOption"; export default function UserMenu({ user, ...rest }: { user: Session } & StackProps) { - const profileImage = user.user.image ? ( profile picture ) : ( @@ -27,26 +27,28 @@ export default function UserMenu({ user, ...rest }: { user: Session } & StackPro <> - - - {profileImage} - - - {user.user.name} - - - {/* {user.user.email} */} - - - - - + + + + {profileImage} + + + {user.user.name} + + + {/* {user.user.email} */} + + + + + + diff --git a/app/src/pages/data/index.tsx b/app/src/pages/data/index.tsx index 843a63e..bbc99ea 100644 --- a/app/src/pages/data/index.tsx +++ b/app/src/pages/data/index.tsx @@ -9,7 +9,6 @@ import { Link, } from "@chakra-ui/react"; import AppShell from "~/components/nav/AppShell"; -import { api } from "~/utils/api"; import { signIn, useSession } from "next-auth/react"; import { RiDatabase2Line } from "react-icons/ri"; import { @@ -19,9 +18,10 @@ import { } from "~/components/datasets/DatasetCard"; import PageHeaderContainer from "~/components/nav/PageHeaderContainer"; import ProjectBreadcrumbContents from "~/components/nav/ProjectBreadcrumbContents"; +import { useDatasets } from "~/utils/hooks"; export default function DatasetsPage() { - const datasets = api.datasets.list.useQuery(); + const datasets = useDatasets(); const user = useSession().data; const authLoading = useSession().status === "loading"; diff --git a/app/src/pages/experiments/index.tsx b/app/src/pages/experiments/index.tsx index 59e8f25..666d8e5 100644 --- a/app/src/pages/experiments/index.tsx +++ b/app/src/pages/experiments/index.tsx @@ -10,7 +10,6 @@ import { } from "@chakra-ui/react"; import { RiFlaskLine } from "react-icons/ri"; import AppShell from "~/components/nav/AppShell"; -import { api } from "~/utils/api"; import { ExperimentCard, ExperimentCardSkeleton, @@ -19,9 +18,10 @@ import { import { signIn, useSession } from "next-auth/react"; import PageHeaderContainer from "~/components/nav/PageHeaderContainer"; import ProjectBreadcrumbContents from "~/components/nav/ProjectBreadcrumbContents"; +import { useExperiments } from "~/utils/hooks"; export default function ExperimentsPage() { - const experiments = api.experiments.list.useQuery(); + const experiments = useExperiments(); const user = useSession().data; const authLoading = useSession().status === "loading"; diff --git a/app/src/pages/home/index.tsx b/app/src/pages/home/index.tsx index 6927cd8..7f9b3af 100644 --- a/app/src/pages/home/index.tsx +++ b/app/src/pages/home/index.tsx @@ -1,30 +1,15 @@ -import { Breadcrumb, BreadcrumbItem, Text } from "@chakra-ui/react"; -import { useEffect, useState } from "react"; +import { Breadcrumb, BreadcrumbItem, Divider, Text, VStack } from "@chakra-ui/react"; + import AppShell from "~/components/nav/AppShell"; import PageHeaderContainer from "~/components/nav/PageHeaderContainer"; import ProjectBreadcrumbContents from "~/components/nav/ProjectBreadcrumbContents"; -import { api } from "~/utils/api"; -import { useHandledAsyncCallback, useSelectedOrg } from "~/utils/hooks"; +import { useExperiments, useSelectedOrg } from "~/utils/hooks"; export default function HomePage() { - const utils = api.useContext(); const { data: selectedOrg } = useSelectedOrg(); - const updateMutation = api.organizations.update.useMutation(); - const [onSaveName] = useHandledAsyncCallback(async () => { - if (name && name !== selectedOrg?.name && selectedOrg?.id) { - await updateMutation.mutateAsync({ - id: selectedOrg.id, - updates: { name }, - }); - await Promise.all([utils.organizations.get.invalidate({ id: selectedOrg.id })]); - } - }, [updateMutation, selectedOrg]); + const experiments = useExperiments(); - const [name, setName] = useState(selectedOrg?.name); - useEffect(() => { - setName(selectedOrg?.name); - }, [selectedOrg?.name]); return ( @@ -33,10 +18,31 @@ export default function HomePage() { - Home + Homepage + + + {selectedOrg?.name} + + + {/* TODO: Add more dashboard cards (one looks weird) */} + {/* + + + {experiments.data?.slice(0, 5).map((exp) => ( + + + {exp.label} + Last updated {formatTimePast(exp.updatedAt)} + + + ))} + + + */} + ); } diff --git a/app/src/server/api/routers/datasets.router.ts b/app/src/server/api/routers/datasets.router.ts index b25fde4..7ba7787 100644 --- a/app/src/server/api/routers/datasets.router.ts +++ b/app/src/server/api/routers/datasets.router.ts @@ -4,35 +4,33 @@ import { prisma } from "~/server/db"; import { requireCanModifyDataset, requireCanViewDataset, + requireCanViewOrganization, requireNothing, } from "~/utils/accessControl"; import userOrg from "~/server/utils/userOrg"; export const datasetsRouter = createTRPCRouter({ - list: protectedProcedure.query(async ({ ctx }) => { - // Anyone can list experiments - requireNothing(ctx); + list: protectedProcedure + .input(z.object({ organizationId: z.string() })) + .query(async ({ input, ctx }) => { + await requireCanViewOrganization(input.organizationId, ctx); - const datasets = await prisma.dataset.findMany({ - where: { - organization: { - organizationUsers: { - some: { userId: ctx.session.user.id }, + const datasets = await prisma.dataset.findMany({ + where: { + organizationId: input.organizationId, + }, + orderBy: { + createdAt: "desc", + }, + include: { + _count: { + select: { datasetEntries: true }, }, }, - }, - orderBy: { - createdAt: "desc", - }, - include: { - _count: { - select: { datasetEntries: true }, - }, - }, - }); + }); - return datasets; - }), + return datasets; + }), get: publicProcedure.input(z.object({ id: z.string() })).query(async ({ input, ctx }) => { await requireCanViewDataset(input.id, ctx); diff --git a/app/src/server/api/routers/experiments.router.ts b/app/src/server/api/routers/experiments.router.ts index 2008baa..699daa6 100644 --- a/app/src/server/api/routers/experiments.router.ts +++ b/app/src/server/api/routers/experiments.router.ts @@ -9,6 +9,7 @@ import { canModifyExperiment, requireCanModifyExperiment, requireCanViewExperiment, + requireCanViewOrganization, requireNothing, } from "~/utils/accessControl"; import userOrg from "~/server/utils/userOrg"; @@ -43,50 +44,47 @@ export const experimentsRouter = createTRPCRouter({ testScenarioCount, }; }), - list: protectedProcedure.query(async ({ ctx }) => { - // Anyone can list experiments - requireNothing(ctx); + list: protectedProcedure + .input(z.object({ organizationId: z.string() })) + .query(async ({ input, ctx }) => { + await requireCanViewOrganization(input.organizationId, ctx) - const experiments = await prisma.experiment.findMany({ - where: { - organization: { - organizationUsers: { - some: { userId: ctx.session.user.id }, - }, + const experiments = await prisma.experiment.findMany({ + where: { + organizationId: input.organizationId, }, - }, - orderBy: { - sortIndex: "desc", - }, - }); + orderBy: { + sortIndex: "desc", + }, + }); - // TODO: look for cleaner way to do this. Maybe aggregate? - const experimentsWithCounts = await Promise.all( - experiments.map(async (experiment) => { - const visibleTestScenarioCount = await prisma.testScenario.count({ - where: { - experimentId: experiment.id, - visible: true, - }, - }); + // TODO: look for cleaner way to do this. Maybe aggregate? + const experimentsWithCounts = await Promise.all( + experiments.map(async (experiment) => { + const visibleTestScenarioCount = await prisma.testScenario.count({ + where: { + experimentId: experiment.id, + visible: true, + }, + }); - const visiblePromptVariantCount = await prisma.promptVariant.count({ - where: { - experimentId: experiment.id, - visible: true, - }, - }); + const visiblePromptVariantCount = await prisma.promptVariant.count({ + where: { + experimentId: experiment.id, + visible: true, + }, + }); - return { - ...experiment, - testScenarioCount: visibleTestScenarioCount, - promptVariantCount: visiblePromptVariantCount, - }; - }), - ); + return { + ...experiment, + testScenarioCount: visibleTestScenarioCount, + promptVariantCount: visiblePromptVariantCount, + }; + }), + ); - return experimentsWithCounts; - }), + return experimentsWithCounts; + }), get: publicProcedure.input(z.object({ id: z.string() })).query(async ({ input, ctx }) => { await requireCanViewExperiment(input.id, ctx); diff --git a/app/src/utils/accessControl.ts b/app/src/utils/accessControl.ts index 98aca75..ad55904 100644 --- a/app/src/utils/accessControl.ts +++ b/app/src/utils/accessControl.ts @@ -16,6 +16,26 @@ export const requireNothing = (ctx: TRPCContext) => { ctx.markAccessControlRun(); }; +export const requireCanViewOrganization = async (organizationId: string, ctx: TRPCContext) => { + const userId = ctx.session?.user.id; + if (!userId) { + throw new TRPCError({ code: "UNAUTHORIZED" }); + } + + const canView = await prisma.organizationUser.findFirst({ + where: { + userId, + organizationId, + }, + }); + + if (!canView) { + throw new TRPCError({ code: "UNAUTHORIZED" }); + } + + ctx.markAccessControlRun(); +}; + export const requireCanModifyOrganization = async (organizationId: string, ctx: TRPCContext) => { const userId = ctx.session?.user.id; if (!userId) { diff --git a/app/src/utils/hooks.ts b/app/src/utils/hooks.ts index 3c1518f..11ae5e3 100644 --- a/app/src/utils/hooks.ts +++ b/app/src/utils/hooks.ts @@ -4,6 +4,14 @@ import { api } from "~/utils/api"; import { NumberParam, useQueryParam, withDefault } from "use-query-params"; import { useAppStore } from "~/state/store"; +export const useExperiments = () => { + const selectedOrgId = useAppStore((state) => state.selectedOrgId); + return api.experiments.list.useQuery( + { organizationId: selectedOrgId ?? "" }, + { enabled: !!selectedOrgId }, + ); +}; + export const useExperiment = () => { const router = useRouter(); const experiment = api.experiments.get.useQuery( @@ -18,6 +26,14 @@ export const useExperimentAccess = () => { return useExperiment().data?.access ?? { canView: false, canModify: false }; }; +export const useDatasets = () => { + const selectedOrgId = useAppStore((state) => state.selectedOrgId); + return api.datasets.list.useQuery( + { organizationId: selectedOrgId ?? "" }, + { enabled: !!selectedOrgId }, + ); +}; + export const useDataset = () => { const router = useRouter(); const dataset = api.datasets.get.useQuery( @@ -136,8 +152,5 @@ export const useVisibleScenarioIds = () => useScenarios().data?.scenarios.map((s export const useSelectedOrg = () => { const selectedOrgId = useAppStore((state) => state.selectedOrgId); - return api.organizations.get.useQuery( - { id: selectedOrgId ?? "" }, - { enabled: !!selectedOrgId }, - ); + return api.organizations.get.useQuery({ id: selectedOrgId ?? "" }, { enabled: !!selectedOrgId }); }; From c0f10cd52241df732fa599249b8b70657aef0a4b Mon Sep 17 00:00:00 2001 From: David Corbitt Date: Mon, 7 Aug 2023 14:12:22 -0700 Subject: [PATCH 13/49] Remove comment --- app/src/pages/home/index.tsx | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/app/src/pages/home/index.tsx b/app/src/pages/home/index.tsx index 7f9b3af..3347590 100644 --- a/app/src/pages/home/index.tsx +++ b/app/src/pages/home/index.tsx @@ -3,13 +3,11 @@ import { Breadcrumb, BreadcrumbItem, Divider, Text, VStack } from "@chakra-ui/re import AppShell from "~/components/nav/AppShell"; import PageHeaderContainer from "~/components/nav/PageHeaderContainer"; import ProjectBreadcrumbContents from "~/components/nav/ProjectBreadcrumbContents"; -import { useExperiments, useSelectedOrg } from "~/utils/hooks"; +import { useSelectedOrg } from "~/utils/hooks"; export default function HomePage() { const { data: selectedOrg } = useSelectedOrg(); - const experiments = useExperiments(); - return ( @@ -27,21 +25,6 @@ export default function HomePage() { {selectedOrg?.name} - {/* TODO: Add more dashboard cards (one looks weird) */} - {/* - - - {experiments.data?.slice(0, 5).map((exp) => ( - - - {exp.label} - Last updated {formatTimePast(exp.updatedAt)} - - - ))} - - - */} ); From d220cd30e8fa6d53bb93e46bf673d36933e1ab2b Mon Sep 17 00:00:00 2001 From: David Corbitt Date: Mon, 7 Aug 2023 15:18:23 -0700 Subject: [PATCH 14/49] Allow user to change projects --- app/src/components/nav/NavSidebarOption.tsx | 5 +- app/src/components/nav/ProjectMenu.tsx | 164 +++++++++++++++--- .../api/routers/organizations.router.ts | 33 +++- 3 files changed, 172 insertions(+), 30 deletions(-) diff --git a/app/src/components/nav/NavSidebarOption.tsx b/app/src/components/nav/NavSidebarOption.tsx index 9c3afea..052e4b5 100644 --- a/app/src/components/nav/NavSidebarOption.tsx +++ b/app/src/components/nav/NavSidebarOption.tsx @@ -3,8 +3,9 @@ import { useRouter } from "next/router"; const NavSidebarOption = ({ activeHrefPattern, + disableHoverEffect, ...props -}: { activeHrefPattern?: string } & BoxProps) => { +}: { activeHrefPattern?: string; disableHoverEffect?: boolean } & BoxProps) => { const router = useRouter(); const isActive = activeHrefPattern && router.pathname.startsWith(activeHrefPattern); return ( @@ -12,7 +13,7 @@ const NavSidebarOption = ({ w="full" fontWeight={isActive ? "bold" : "500"} bgColor={isActive ? "gray.200" : "transparent"} - _hover={{ bgColor: "gray.200", textDecoration: "none" }} + _hover={disableHoverEffect ? undefined : { bgColor: "gray.200", textDecoration: "none" }} justifyContent="start" cursor="pointer" borderRadius={4} diff --git a/app/src/components/nav/ProjectMenu.tsx b/app/src/components/nav/ProjectMenu.tsx index faa2e4c..195fa0c 100644 --- a/app/src/components/nav/ProjectMenu.tsx +++ b/app/src/components/nav/ProjectMenu.tsx @@ -6,47 +6,78 @@ import { PopoverTrigger, PopoverContent, Flex, + IconButton, + Icon, + Divider, + Button, + useDisclosure, + Spinner, } from "@chakra-ui/react"; -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import Link from "next/link"; +import { AiFillCaretDown } from "react-icons/ai"; +import { BsGear, BsPlus } from "react-icons/bs"; +import { type Organization } from "@prisma/client"; import { useAppStore } from "~/state/store"; import { api } from "~/utils/api"; import NavSidebarOption from "./NavSidebarOption"; -import { useSelectedOrg } from "~/utils/hooks"; +import { useHandledAsyncCallback, useSelectedOrg } from "~/utils/hooks"; +import { useRouter } from "next/router"; export default function ProjectMenu() { + const router = useRouter(); + const isActive = router.pathname.startsWith("/home"); + const utils = api.useContext(); + const selectedOrgId = useAppStore((s) => s.selectedOrgId); const setSelectedOrgId = useAppStore((s) => s.setSelectedOrgId); - const { data } = api.organizations.list.useQuery(); + const { data: orgs } = api.organizations.list.useQuery(); useEffect(() => { - if (data && data[0] && (!selectedOrgId || !data.find((org) => org.id === selectedOrgId))) { - setSelectedOrgId(data[0].id); + if (orgs && orgs[0] && (!selectedOrgId || !orgs.find((org) => org.id === selectedOrgId))) { + setSelectedOrgId(orgs[0].id); } - }, [selectedOrgId, setSelectedOrgId, data]); + }, [selectedOrgId, setSelectedOrgId, orgs]); const { data: selectedOrg } = useSelectedOrg(); + const [expandButtonHovered, setExpandButtonHovered] = useState(false); + + const popover = useDisclosure(); + + const createMutation = api.organizations.create.useMutation(); + const [createProject, isLoading] = useHandledAsyncCallback(async () => { + const newOrg = await createMutation.mutateAsync({ name: "New Project" }); + await utils.organizations.list.invalidate(); + setSelectedOrgId(newOrg.id); + await router.push({ pathname: "/settings" }); + }, [createMutation, router]); + return ( <> - - - - - PROJECT - - - - + + + + PROJECT + + + + + - - + + } + size="xs" + colorScheme="gray" + color="gray.500" + variant="ghost" + mr={2} + borderRadius={4} + onMouseEnter={() => setExpandButtonHovered(true)} + onMouseLeave={() => setExpandButtonHovered(false)} + _hover={{ bgColor: isActive ? "gray.300" : "gray.200", transitionDelay: 0 }} + onClick={(event) => { + event.preventDefault(); + popover.onToggle(); + }} + /> + + + + + + + + + PROJECTS + + + + {orgs?.map((org) => ( + + ))} + + + + New project + - - + ); } + +const ProjectOption = ({ org, isActive }: { org: Organization; isActive: boolean }) => { + const setSelectedOrgId = useAppStore((s) => s.setSelectedOrgId); + const [gearHovered, setGearHovered] = useState(false); + return ( + setSelectedOrgId(org.id)} + w="full" + justifyContent="space-between" + bgColor={isActive ? "gray.100" : "transparent"} + _hover={gearHovered ? undefined : { bgColor: "gray.200", textDecoration: "none" }} + p={2} + borderRadius={4} + > + {org.name} + } + variant="ghost" + size="xs" + p={0} + onMouseEnter={() => setGearHovered(true)} + onMouseLeave={() => setGearHovered(false)} + _hover={{ bgColor: isActive ? "gray.300" : "gray.100", transitionDelay: 0 }} + borderRadius={4} + /> + + ); +}; diff --git a/app/src/server/api/routers/organizations.router.ts b/app/src/server/api/routers/organizations.router.ts index 1216f54..293e0f6 100644 --- a/app/src/server/api/routers/organizations.router.ts +++ b/app/src/server/api/routers/organizations.router.ts @@ -22,7 +22,7 @@ export const organizationsRouter = createTRPCRouter({ }, }, orderBy: { - createdAt: "desc", + createdAt: "asc", }, }); @@ -63,7 +63,7 @@ export const organizationsRouter = createTRPCRouter({ }, include: { apiKeys: true, - } + }, }); }), update: protectedProcedure @@ -79,4 +79,33 @@ export const organizationsRouter = createTRPCRouter({ }, }); }), + create: protectedProcedure + .input(z.object({ name: z.string() })) + .mutation(async ({ input, ctx }) => { + requireNothing(ctx); + const newOrgId = uuidv4(); + const [newOrg] = await prisma.$transaction([ + prisma.organization.create({ + data: { + id: newOrgId, + name: input.name, + }, + }), + prisma.organizationUser.create({ + data: { + userId: ctx.session.user.id, + organizationId: newOrgId, + role: "ADMIN", + }, + }), + prisma.apiKey.create({ + data: { + name: "Default API Key", + organizationId: newOrgId, + apiKey: generateApiKey(), + }, + }), + ]); + return newOrg; + }), }); From 74029e54789228ddee1d924024a689772d11dad4 Mon Sep 17 00:00:00 2001 From: David Corbitt Date: Mon, 7 Aug 2023 15:21:03 -0700 Subject: [PATCH 15/49] Close project menu after navigating --- app/src/components/nav/ProjectMenu.tsx | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/app/src/components/nav/ProjectMenu.tsx b/app/src/components/nav/ProjectMenu.tsx index 195fa0c..a56a898 100644 --- a/app/src/components/nav/ProjectMenu.tsx +++ b/app/src/components/nav/ProjectMenu.tsx @@ -130,7 +130,12 @@ export default function ProjectMenu() { {orgs?.map((org) => ( - + ))} { +const ProjectOption = ({ + org, + isActive, + onClose, +}: { + org: Organization; + isActive: boolean; + onClose: () => void; +}) => { const setSelectedOrgId = useAppStore((s) => s.setSelectedOrgId); const [gearHovered, setGearHovered] = useState(false); return ( setSelectedOrgId(org.id)} + onClick={() => { + setSelectedOrgId(org.id); + onClose(); + }} w="full" justifyContent="space-between" bgColor={isActive ? "gray.100" : "transparent"} From 9f17d9873692075d713affd4990cd2bad81ef0e3 Mon Sep 17 00:00:00 2001 From: David Corbitt Date: Mon, 7 Aug 2023 17:12:09 -0700 Subject: [PATCH 16/49] Attempt to log (without api key) --- client-libs/typescript/openai/index.ts | 89 ++++++++++++++++++++++++-- 1 file changed, 82 insertions(+), 7 deletions(-) diff --git a/client-libs/typescript/openai/index.ts b/client-libs/typescript/openai/index.ts index 81df9f3..8490362 100644 --- a/client-libs/typescript/openai/index.ts +++ b/client-libs/typescript/openai/index.ts @@ -1,8 +1,14 @@ import * as openai from "openai-beta"; -import { readEnv } from "openai-beta/core"; +import { + readEnv, + type RequestOptions, +} from "openai-beta/core"; +import { + CompletionCreateParams, +} from "openai-beta/resources/chat/completions"; -// Anything we don't override we want to pass through to openai directly export * as openai from "openai-beta"; +import * as openPipeClient from "../codegen"; interface ClientOptions extends openai.ClientOptions { openPipeApiKey?: string; @@ -10,8 +16,7 @@ interface ClientOptions extends openai.ClientOptions { } export class OpenAI extends openai.OpenAI { - openPipeApiKey: string; - openPipeBaseUrl: string; + public openPipeApi?: openPipeClient.DefaultApi; constructor({ openPipeApiKey = readEnv("OPENPIPE_API_KEY"), @@ -19,13 +24,83 @@ export class OpenAI extends openai.OpenAI { `https://app.openpipe.ai/v1`, ...opts }: ClientOptions = {}) { + super({ ...opts }); + + if (openPipeApiKey) { + this.openPipeApi = new openPipeClient.DefaultApi(new openPipeClient.Configuration({ + apiKey: openPipeApiKey, + basePath: openPipeBaseUrl, + })); + } + + // Override the chat property + this.chat = new ExtendedChat(this); + if (openPipeApiKey === undefined) { console.error( "The OPENPIPE_API_KEY environment variable is missing or empty; either provide it, or instantiate the OpenPipe client with an openPipeApiKey option, like new OpenPipe({ openPipeApiKey: undefined })." ); } - super({ - ...opts, - }); + } +} + +class ExtendedChat extends openai.OpenAI.Chat { + completions: ExtendedCompletions; + + constructor(openaiInstance: OpenAI) { + super(openaiInstance); + // Initialize the new completions instance + this.completions = new ExtendedCompletions(openaiInstance); + } +} + +class ExtendedCompletions extends openai.OpenAI.Chat.Completions { + private openaiInstance: OpenAI; + + constructor(openaiInstance: OpenAI) { + super(openaiInstance); + this.openaiInstance = openaiInstance; + } + + async create( + params: + | CompletionCreateParams.CreateChatCompletionRequestNonStreaming + | CompletionCreateParams.CreateChatCompletionRequestStreaming, + options?: RequestOptions, + tags?: Record + ): Promise { + // Your pre API call logic here + console.log("Doing pre API call..."); + + // Determine the type of request + if (params.hasOwnProperty("stream") && params.stream === true) { + const result = await super.create( + params as CompletionCreateParams.CreateChatCompletionRequestStreaming, + options + ); + // Your post API call logic here + console.log("Doing post API call for NonStreaming..."); + return result; + } else { + const startTime = Date.now(); + const result = await super.create( + params as CompletionCreateParams.CreateChatCompletionRequestNonStreaming, + options + ); + console.log('result is this', result) + this.openaiInstance.openPipeApi?.externalApiReport({ + startTime, + endTime: Date.now(), + reqPayload: params, + respPayload: result, + respStatus: 200, + error: undefined, + tags, + }); + + // Your post API call logic here + console.log("Doing post API call for Streaming..."); + return result; + } } } From 0f9a83cf45e2e3ae6169966ba6815a515f964b3d Mon Sep 17 00:00:00 2001 From: David Corbitt Date: Mon, 7 Aug 2023 19:10:27 -0700 Subject: [PATCH 17/49] Assign experiments and datasets to correct org --- app/src/components/datasets/DatasetCard.tsx | 6 +- .../components/experiments/ExperimentCard.tsx | 13 +- .../nav/ProjectBreadcrumbContents.tsx | 10 +- app/src/pages/data/[id].tsx | 2 +- app/src/pages/experiments/[id].tsx | 2 +- app/src/server/api/routers/datasets.router.ts | 39 +- .../server/api/routers/experiments.router.ts | 476 +++++++++--------- 7 files changed, 280 insertions(+), 268 deletions(-) diff --git a/app/src/components/datasets/DatasetCard.tsx b/app/src/components/datasets/DatasetCard.tsx index d5f9344..fc59911 100644 --- a/app/src/components/datasets/DatasetCard.tsx +++ b/app/src/components/datasets/DatasetCard.tsx @@ -15,6 +15,7 @@ import { useRouter } from "next/router"; import { BsPlusSquare } from "react-icons/bs"; import { api } from "~/utils/api"; import { useHandledAsyncCallback } from "~/utils/hooks"; +import { useAppStore } from "~/state/store"; type DatasetData = { name: string; @@ -71,11 +72,12 @@ const CountLabel = ({ label, count }: { label: string; count: number }) => { export const NewDatasetCard = () => { const router = useRouter(); + const selectedOrgId = useAppStore((s) => s.selectedOrgId); const createMutation = api.datasets.create.useMutation(); const [createDataset, isLoading] = useHandledAsyncCallback(async () => { - const newDataset = await createMutation.mutateAsync({ label: "New Dataset" }); + const newDataset = await createMutation.mutateAsync({ organizationId: selectedOrgId ?? "" }); await router.push({ pathname: "/data/[id]", query: { id: newDataset.id } }); - }, [createMutation, router]); + }, [createMutation, router, selectedOrgId]); return ( diff --git a/app/src/components/experiments/ExperimentCard.tsx b/app/src/components/experiments/ExperimentCard.tsx index 9a8f4c3..01853af 100644 --- a/app/src/components/experiments/ExperimentCard.tsx +++ b/app/src/components/experiments/ExperimentCard.tsx @@ -15,6 +15,7 @@ import { useRouter } from "next/router"; import { BsPlusSquare } from "react-icons/bs"; import { api } from "~/utils/api"; import { useHandledAsyncCallback } from "~/utils/hooks"; +import { useAppStore } from "~/state/store"; type ExperimentData = { testScenarioCount: number; @@ -75,11 +76,17 @@ const CountLabel = ({ label, count }: { label: string; count: number }) => { export const NewExperimentCard = () => { const router = useRouter(); + const selectedOrgId = useAppStore((s) => s.selectedOrgId); const createMutation = api.experiments.create.useMutation(); const [createExperiment, isLoading] = useHandledAsyncCallback(async () => { - const newExperiment = await createMutation.mutateAsync({ label: "New Experiment" }); - await router.push({ pathname: "/experiments/[id]", query: { id: newExperiment.id } }); - }, [createMutation, router]); + const newExperiment = await createMutation.mutateAsync({ + organizationId: selectedOrgId ?? "", + }); + await router.push({ + pathname: "/experiments/[id]", + query: { id: newExperiment.id }, + }); + }, [createMutation, router, selectedOrgId]); return ( diff --git a/app/src/components/nav/ProjectBreadcrumbContents.tsx b/app/src/components/nav/ProjectBreadcrumbContents.tsx index 68de209..c2ecdbc 100644 --- a/app/src/components/nav/ProjectBreadcrumbContents.tsx +++ b/app/src/components/nav/ProjectBreadcrumbContents.tsx @@ -1,14 +1,14 @@ import { HStack, Flex, Text } from "@chakra-ui/react"; import Link from "next/link"; - import { useSelectedOrg } from "~/utils/hooks"; // Have to export only contents here instead of full BreadcrumbItem because Chakra doesn't // recognize a BreadcrumbItem exported with this component as a valid child of Breadcrumb. - -export default function ProjectBreadcrumbContents() { +export default function ProjectBreadcrumbContents({ orgName = "" }: { orgName?: string }) { const { data: selectedOrg } = useSelectedOrg(); + orgName = orgName || selectedOrg?.name || ""; + return ( @@ -23,10 +23,10 @@ export default function ProjectBreadcrumbContents() { alignItems="center" justifyContent="center" > - {selectedOrg?.name[0]?.toUpperCase()} + {orgName[0]?.toUpperCase()} - {selectedOrg?.name} + {orgName} diff --git a/app/src/pages/data/[id].tsx b/app/src/pages/data/[id].tsx index 63921a6..b4a79fb 100644 --- a/app/src/pages/data/[id].tsx +++ b/app/src/pages/data/[id].tsx @@ -60,7 +60,7 @@ export default function Dataset() { - + diff --git a/app/src/pages/experiments/[id].tsx b/app/src/pages/experiments/[id].tsx index 04ce91d..d1df7cd 100644 --- a/app/src/pages/experiments/[id].tsx +++ b/app/src/pages/experiments/[id].tsx @@ -110,7 +110,7 @@ export default function Experiment() { - + diff --git a/app/src/server/api/routers/datasets.router.ts b/app/src/server/api/routers/datasets.router.ts index 7ba7787..80bfd26 100644 --- a/app/src/server/api/routers/datasets.router.ts +++ b/app/src/server/api/routers/datasets.router.ts @@ -3,11 +3,10 @@ import { createTRPCRouter, protectedProcedure, publicProcedure } from "~/server/ import { prisma } from "~/server/db"; import { requireCanModifyDataset, + requireCanModifyOrganization, requireCanViewDataset, requireCanViewOrganization, - requireNothing, } from "~/utils/accessControl"; -import userOrg from "~/server/utils/userOrg"; export const datasetsRouter = createTRPCRouter({ list: protectedProcedure @@ -36,30 +35,30 @@ export const datasetsRouter = createTRPCRouter({ await requireCanViewDataset(input.id, ctx); return await prisma.dataset.findFirstOrThrow({ where: { id: input.id }, + include: { + organization: true, + }, }); }), - create: protectedProcedure.input(z.object({})).mutation(async ({ ctx }) => { - // Anyone can create an experiment - requireNothing(ctx); + create: protectedProcedure + .input(z.object({ organizationId: z.string() })) + .mutation(async ({ input, ctx }) => { + await requireCanModifyOrganization(input.organizationId, ctx); - const numDatasets = await prisma.dataset.count({ - where: { - organization: { - organizationUsers: { - some: { userId: ctx.session.user.id }, - }, + const numDatasets = await prisma.dataset.count({ + where: { + organizationId: input.organizationId, }, - }, - }); + }); - return await prisma.dataset.create({ - data: { - name: `Dataset ${numDatasets + 1}`, - organizationId: (await userOrg(ctx.session.user.id)).id, - }, - }); - }), + return await prisma.dataset.create({ + data: { + name: `Dataset ${numDatasets + 1}`, + organizationId: input.organizationId, + }, + }); + }), update: protectedProcedure .input(z.object({ id: z.string(), updates: z.object({ name: z.string() }) })) diff --git a/app/src/server/api/routers/experiments.router.ts b/app/src/server/api/routers/experiments.router.ts index 699daa6..b69e3d2 100644 --- a/app/src/server/api/routers/experiments.router.ts +++ b/app/src/server/api/routers/experiments.router.ts @@ -8,11 +8,10 @@ import { generateNewCell } from "~/server/utils/generateNewCell"; import { canModifyExperiment, requireCanModifyExperiment, + requireCanModifyOrganization, requireCanViewExperiment, requireCanViewOrganization, - requireNothing, } from "~/utils/accessControl"; -import userOrg from "~/server/utils/userOrg"; import generateTypes from "~/modelProviders/generateTypes"; import { promptConstructorVersion } from "~/promptConstructor/version"; @@ -47,7 +46,7 @@ export const experimentsRouter = createTRPCRouter({ list: protectedProcedure .input(z.object({ organizationId: z.string() })) .query(async ({ input, ctx }) => { - await requireCanViewOrganization(input.organizationId, ctx) + await requireCanViewOrganization(input.organizationId, ctx); const experiments = await prisma.experiment.findMany({ where: { @@ -90,6 +89,9 @@ export const experimentsRouter = createTRPCRouter({ await requireCanViewExperiment(input.id, ctx); const experiment = await prisma.experiment.findFirstOrThrow({ where: { id: input.id }, + include: { + organization: true, + }, }); const canModify = ctx.session?.user.id @@ -105,222 +107,224 @@ export const experimentsRouter = createTRPCRouter({ }; }), - fork: protectedProcedure.input(z.object({ id: z.string() })).mutation(async ({ input, ctx }) => { - await requireCanViewExperiment(input.id, ctx); + fork: protectedProcedure + .input(z.object({ id: z.string(), organizationId: z.string() })) + .mutation(async ({ input, ctx }) => { + await requireCanViewExperiment(input.id, ctx); + await requireCanModifyOrganization(input.organizationId, ctx); - const [ - existingExp, - existingVariants, - existingScenarios, - existingCells, - evaluations, - templateVariables, - ] = await prisma.$transaction([ - prisma.experiment.findUniqueOrThrow({ - where: { - id: input.id, - }, - }), - prisma.promptVariant.findMany({ - where: { - experimentId: input.id, - visible: true, - }, - }), - prisma.testScenario.findMany({ - where: { - experimentId: input.id, - visible: true, - }, - }), - prisma.scenarioVariantCell.findMany({ - where: { - testScenario: { - visible: true, + const [ + existingExp, + existingVariants, + existingScenarios, + existingCells, + evaluations, + templateVariables, + ] = await prisma.$transaction([ + prisma.experiment.findUniqueOrThrow({ + where: { + id: input.id, }, - promptVariant: { + }), + prisma.promptVariant.findMany({ + where: { experimentId: input.id, visible: true, }, - }, - include: { - modelResponses: { - include: { - outputEvaluations: true, + }), + prisma.testScenario.findMany({ + where: { + experimentId: input.id, + visible: true, + }, + }), + prisma.scenarioVariantCell.findMany({ + where: { + testScenario: { + visible: true, + }, + promptVariant: { + experimentId: input.id, + visible: true, }, }, - }, - }), - prisma.evaluation.findMany({ - where: { - experimentId: input.id, - }, - }), - prisma.templateVariable.findMany({ - where: { - experimentId: input.id, - }, - }), - ]); + include: { + modelResponses: { + include: { + outputEvaluations: true, + }, + }, + }, + }), + prisma.evaluation.findMany({ + where: { + experimentId: input.id, + }, + }), + prisma.templateVariable.findMany({ + where: { + experimentId: input.id, + }, + }), + ]); - const newExperimentId = uuidv4(); + const newExperimentId = uuidv4(); - const existingToNewVariantIds = new Map(); - const variantsToCreate: Prisma.PromptVariantCreateManyInput[] = []; - for (const variant of existingVariants) { - const newVariantId = uuidv4(); - existingToNewVariantIds.set(variant.id, newVariantId); - variantsToCreate.push({ - ...variant, - id: newVariantId, - experimentId: newExperimentId, - }); - } - - const existingToNewScenarioIds = new Map(); - const scenariosToCreate: Prisma.TestScenarioCreateManyInput[] = []; - for (const scenario of existingScenarios) { - const newScenarioId = uuidv4(); - existingToNewScenarioIds.set(scenario.id, newScenarioId); - scenariosToCreate.push({ - ...scenario, - id: newScenarioId, - experimentId: newExperimentId, - variableValues: scenario.variableValues as Prisma.InputJsonValue, - }); - } - - const existingToNewEvaluationIds = new Map(); - const evaluationsToCreate: Prisma.EvaluationCreateManyInput[] = []; - for (const evaluation of evaluations) { - const newEvaluationId = uuidv4(); - existingToNewEvaluationIds.set(evaluation.id, newEvaluationId); - evaluationsToCreate.push({ - ...evaluation, - id: newEvaluationId, - experimentId: newExperimentId, - }); - } - - const cellsToCreate: Prisma.ScenarioVariantCellCreateManyInput[] = []; - const modelResponsesToCreate: Prisma.ModelResponseCreateManyInput[] = []; - const outputEvaluationsToCreate: Prisma.OutputEvaluationCreateManyInput[] = []; - for (const cell of existingCells) { - const newCellId = uuidv4(); - const { modelResponses, ...cellData } = cell; - cellsToCreate.push({ - ...cellData, - id: newCellId, - promptVariantId: existingToNewVariantIds.get(cell.promptVariantId) ?? "", - testScenarioId: existingToNewScenarioIds.get(cell.testScenarioId) ?? "", - prompt: (cell.prompt as Prisma.InputJsonValue) ?? undefined, - }); - for (const modelResponse of modelResponses) { - const newModelResponseId = uuidv4(); - const { outputEvaluations, ...modelResponseData } = modelResponse; - modelResponsesToCreate.push({ - ...modelResponseData, - id: newModelResponseId, - scenarioVariantCellId: newCellId, - output: (modelResponse.output as Prisma.InputJsonValue) ?? undefined, + const existingToNewVariantIds = new Map(); + const variantsToCreate: Prisma.PromptVariantCreateManyInput[] = []; + for (const variant of existingVariants) { + const newVariantId = uuidv4(); + existingToNewVariantIds.set(variant.id, newVariantId); + variantsToCreate.push({ + ...variant, + id: newVariantId, + experimentId: newExperimentId, }); - for (const evaluation of outputEvaluations) { - outputEvaluationsToCreate.push({ - ...evaluation, - id: uuidv4(), - modelResponseId: newModelResponseId, - evaluationId: existingToNewEvaluationIds.get(evaluation.evaluationId) ?? "", + } + + const existingToNewScenarioIds = new Map(); + const scenariosToCreate: Prisma.TestScenarioCreateManyInput[] = []; + for (const scenario of existingScenarios) { + const newScenarioId = uuidv4(); + existingToNewScenarioIds.set(scenario.id, newScenarioId); + scenariosToCreate.push({ + ...scenario, + id: newScenarioId, + experimentId: newExperimentId, + variableValues: scenario.variableValues as Prisma.InputJsonValue, + }); + } + + const existingToNewEvaluationIds = new Map(); + const evaluationsToCreate: Prisma.EvaluationCreateManyInput[] = []; + for (const evaluation of evaluations) { + const newEvaluationId = uuidv4(); + existingToNewEvaluationIds.set(evaluation.id, newEvaluationId); + evaluationsToCreate.push({ + ...evaluation, + id: newEvaluationId, + experimentId: newExperimentId, + }); + } + + const cellsToCreate: Prisma.ScenarioVariantCellCreateManyInput[] = []; + const modelResponsesToCreate: Prisma.ModelResponseCreateManyInput[] = []; + const outputEvaluationsToCreate: Prisma.OutputEvaluationCreateManyInput[] = []; + for (const cell of existingCells) { + const newCellId = uuidv4(); + const { modelResponses, ...cellData } = cell; + cellsToCreate.push({ + ...cellData, + id: newCellId, + promptVariantId: existingToNewVariantIds.get(cell.promptVariantId) ?? "", + testScenarioId: existingToNewScenarioIds.get(cell.testScenarioId) ?? "", + prompt: (cell.prompt as Prisma.InputJsonValue) ?? undefined, + }); + for (const modelResponse of modelResponses) { + const newModelResponseId = uuidv4(); + const { outputEvaluations, ...modelResponseData } = modelResponse; + modelResponsesToCreate.push({ + ...modelResponseData, + id: newModelResponseId, + scenarioVariantCellId: newCellId, + output: (modelResponse.output as Prisma.InputJsonValue) ?? undefined, }); + for (const evaluation of outputEvaluations) { + outputEvaluationsToCreate.push({ + ...evaluation, + id: uuidv4(), + modelResponseId: newModelResponseId, + evaluationId: existingToNewEvaluationIds.get(evaluation.evaluationId) ?? "", + }); + } } } - } - const templateVariablesToCreate: Prisma.TemplateVariableCreateManyInput[] = []; - for (const templateVariable of templateVariables) { - templateVariablesToCreate.push({ - ...templateVariable, - id: uuidv4(), - experimentId: newExperimentId, - }); - } + const templateVariablesToCreate: Prisma.TemplateVariableCreateManyInput[] = []; + for (const templateVariable of templateVariables) { + templateVariablesToCreate.push({ + ...templateVariable, + id: uuidv4(), + experimentId: newExperimentId, + }); + } - const maxSortIndex = - ( - await prisma.experiment.aggregate({ - _max: { - sortIndex: true, + const maxSortIndex = + ( + await prisma.experiment.aggregate({ + _max: { + sortIndex: true, + }, + }) + )._max?.sortIndex ?? 0; + + await prisma.$transaction([ + prisma.experiment.create({ + data: { + id: newExperimentId, + sortIndex: maxSortIndex + 1, + label: `${existingExp.label} (forked)`, + organizationId: input.organizationId, }, - }) - )._max?.sortIndex ?? 0; + }), + prisma.promptVariant.createMany({ + data: variantsToCreate, + }), + prisma.testScenario.createMany({ + data: scenariosToCreate, + }), + prisma.scenarioVariantCell.createMany({ + data: cellsToCreate, + }), + prisma.modelResponse.createMany({ + data: modelResponsesToCreate, + }), + prisma.evaluation.createMany({ + data: evaluationsToCreate, + }), + prisma.outputEvaluation.createMany({ + data: outputEvaluationsToCreate, + }), + prisma.templateVariable.createMany({ + data: templateVariablesToCreate, + }), + ]); - await prisma.$transaction([ - prisma.experiment.create({ + return newExperimentId; + }), + + create: protectedProcedure + .input(z.object({ organizationId: z.string() })) + .mutation(async ({ input, ctx }) => { + await requireCanModifyOrganization(input.organizationId, ctx); + + const maxSortIndex = + ( + await prisma.experiment.aggregate({ + _max: { + sortIndex: true, + }, + where: { organizationId: input.organizationId }, + }) + )._max?.sortIndex ?? 0; + + const exp = await prisma.experiment.create({ data: { - id: newExperimentId, sortIndex: maxSortIndex + 1, - label: `${existingExp.label} (forked)`, - organizationId: (await userOrg(ctx.session.user.id)).id, + label: `Experiment ${maxSortIndex + 1}`, + organizationId: input.organizationId, }, - }), - prisma.promptVariant.createMany({ - data: variantsToCreate, - }), - prisma.testScenario.createMany({ - data: scenariosToCreate, - }), - prisma.scenarioVariantCell.createMany({ - data: cellsToCreate, - }), - prisma.modelResponse.createMany({ - data: modelResponsesToCreate, - }), - prisma.evaluation.createMany({ - data: evaluationsToCreate, - }), - prisma.outputEvaluation.createMany({ - data: outputEvaluationsToCreate, - }), - prisma.templateVariable.createMany({ - data: templateVariablesToCreate, - }), - ]); + }); - return newExperimentId; - }), - - create: protectedProcedure.input(z.object({})).mutation(async ({ ctx }) => { - // Anyone can create an experiment - requireNothing(ctx); - - const organizationId = (await userOrg(ctx.session.user.id)).id; - - const maxSortIndex = - ( - await prisma.experiment.aggregate({ - _max: { - sortIndex: true, - }, - where: { organizationId }, - }) - )._max?.sortIndex ?? 0; - - const exp = await prisma.experiment.create({ - data: { - sortIndex: maxSortIndex + 1, - label: `Experiment ${maxSortIndex + 1}`, - organizationId, - }, - }); - - const [variant, _, scenario1, scenario2, scenario3] = await prisma.$transaction([ - prisma.promptVariant.create({ - data: { - experimentId: exp.id, - label: "Prompt Variant 1", - sortIndex: 0, - // The interpolated $ is necessary until dedent incorporates - // https://github.com/dmnd/dedent/pull/46 - promptConstructor: dedent` + const [variant, _, scenario1, scenario2, scenario3] = await prisma.$transaction([ + prisma.promptVariant.create({ + data: { + experimentId: exp.id, + label: "Prompt Variant 1", + sortIndex: 0, + // The interpolated $ is necessary until dedent incorporates + // https://github.com/dmnd/dedent/pull/46 + promptConstructor: dedent` /** * Use Javascript to define an OpenAI chat completion * (https://platform.openai.com/docs/api-reference/chat/create). @@ -339,49 +343,49 @@ export const experimentsRouter = createTRPCRouter({ }, ], });`, - model: "gpt-3.5-turbo-0613", - modelProvider: "openai/ChatCompletion", - promptConstructorVersion, - }, - }), - prisma.templateVariable.create({ - data: { - experimentId: exp.id, - label: "language", - }, - }), - prisma.testScenario.create({ - data: { - experimentId: exp.id, - variableValues: { - language: "English", + model: "gpt-3.5-turbo-0613", + modelProvider: "openai/ChatCompletion", + promptConstructorVersion, }, - }, - }), - prisma.testScenario.create({ - data: { - experimentId: exp.id, - variableValues: { - language: "Spanish", + }), + prisma.templateVariable.create({ + data: { + experimentId: exp.id, + label: "language", }, - }, - }), - prisma.testScenario.create({ - data: { - experimentId: exp.id, - variableValues: { - language: "German", + }), + prisma.testScenario.create({ + data: { + experimentId: exp.id, + variableValues: { + language: "English", + }, }, - }, - }), - ]); + }), + prisma.testScenario.create({ + data: { + experimentId: exp.id, + variableValues: { + language: "Spanish", + }, + }, + }), + prisma.testScenario.create({ + data: { + experimentId: exp.id, + variableValues: { + language: "German", + }, + }, + }), + ]); - await generateNewCell(variant.id, scenario1.id); - await generateNewCell(variant.id, scenario2.id); - await generateNewCell(variant.id, scenario3.id); + await generateNewCell(variant.id, scenario1.id); + await generateNewCell(variant.id, scenario2.id); + await generateNewCell(variant.id, scenario3.id); - return exp; - }), + return exp; + }), update: protectedProcedure .input(z.object({ id: z.string(), updates: z.object({ label: z.string() }) })) From 8fed9730da45b725d9b28ee556dc196bc89076a1 Mon Sep 17 00:00:00 2001 From: David Corbitt Date: Mon, 7 Aug 2023 21:04:38 -0700 Subject: [PATCH 18/49] Send api token properly --- .../migration.sql | 2 ++ app/prisma/schema.prisma | 8 ++--- .../server/api/routers/externalApi.router.ts | 34 ++++++++++++------- app/src/server/api/trpc.ts | 4 +-- client-libs/typescript/openai/index.ts | 31 ++++++++++------- 5 files changed, 46 insertions(+), 33 deletions(-) create mode 100644 app/prisma/migrations/20230808034313_make_model_response_optional/migration.sql diff --git a/app/prisma/migrations/20230808034313_make_model_response_optional/migration.sql b/app/prisma/migrations/20230808034313_make_model_response_optional/migration.sql new file mode 100644 index 0000000..f9c991c --- /dev/null +++ b/app/prisma/migrations/20230808034313_make_model_response_optional/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "LoggedCall" ALTER COLUMN "modelResponseId" DROP NOT NULL; diff --git a/app/prisma/schema.prisma b/app/prisma/schema.prisma index 387084a..06797f6 100644 --- a/app/prisma/schema.prisma +++ b/app/prisma/schema.prisma @@ -265,11 +265,11 @@ model LoggedCall { // A LoggedCall is always associated with a LoggedCallModelResponse. If this // is a cache miss, we create a new LoggedCallModelResponse. // If it's a cache hit, it's a pre-existing LoggedCallModelResponse. - modelResponseId String @db.Uuid - modelResponse LoggedCallModelResponse @relation(fields: [modelResponseId], references: [id], onDelete: Cascade) + modelResponseId String? @db.Uuid + modelResponse LoggedCallModelResponse? @relation(fields: [modelResponseId], references: [id], onDelete: Cascade) - // The response created by this LoggedCall. Will be null if this LoggedCall is a cache hit. - createdResponse LoggedCallModelResponse[] @relation(name: "ModelResponseOriginalCall") + // The responses created by this LoggedCall. Will be empty if this LoggedCall was a cache hit. + createdResponses LoggedCallModelResponse[] @relation(name: "ModelResponseOriginalCall") organizationId String @db.Uuid organization Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade) diff --git a/app/src/server/api/routers/externalApi.router.ts b/app/src/server/api/routers/externalApi.router.ts index c654c53..08619bf 100644 --- a/app/src/server/api/routers/externalApi.router.ts +++ b/app/src/server/api/routers/externalApi.router.ts @@ -2,6 +2,7 @@ import { type Prisma } from "@prisma/client"; import { type JsonValue } from "type-fest"; import { z } from "zod"; import { v4 as uuidv4 } from "uuid"; +import { TRPCError } from "@trpc/server"; import { createTRPCRouter, publicProcedure } from "~/server/api/trpc"; import { prisma } from "~/server/db"; @@ -56,13 +57,13 @@ export const externalApiRouter = createTRPCRouter({ .mutation(async ({ input, ctx }) => { const apiKey = ctx.apiKey; if (!apiKey) { - throw new Error("Missing API key"); + throw new TRPCError({ code: "UNAUTHORIZED" }); } const key = await prisma.apiKey.findUnique({ where: { apiKey }, }); if (!key) { - throw new Error("Invalid API key"); + throw new TRPCError({ code: "UNAUTHORIZED" }); } const reqPayload = await reqValidator.spa(input.reqPayload); const cacheKey = hashRequest(key.organizationId, reqPayload as JsonValue); @@ -76,19 +77,19 @@ export const externalApiRouter = createTRPCRouter({ }, orderBy: { startTime: "desc", - } + }, }); if (!existingResponse) return { respPayload: null }; await prisma.loggedCall.create({ data: { - organizationId: key.organizationId, - startTime: new Date(input.startTime), - cacheHit: true, - modelResponseId: existingResponse.id, - } - }) + organizationId: key.organizationId, + startTime: new Date(input.startTime), + cacheHit: true, + modelResponseId: existingResponse.id, + }, + }); return { respPayload: existingResponse.respPayload, @@ -123,13 +124,13 @@ export const externalApiRouter = createTRPCRouter({ .mutation(async ({ input, ctx }) => { const apiKey = ctx.apiKey; if (!apiKey) { - throw new Error("Missing API key"); + throw new TRPCError({ code: "UNAUTHORIZED" }); } const key = await prisma.apiKey.findUnique({ where: { apiKey }, }); if (!key) { - throw new Error("Invalid API key"); + throw new TRPCError({ code: "UNAUTHORIZED" }); } const reqPayload = await reqValidator.spa(input.reqPayload); const respPayload = await respValidator.spa(input.respPayload); @@ -148,7 +149,6 @@ export const externalApiRouter = createTRPCRouter({ organizationId: key.organizationId, startTime: new Date(input.startTime), cacheHit: false, - modelResponseId: newModelResponseId, }, }), prisma.loggedCallModelResponse.create({ @@ -167,11 +167,19 @@ export const externalApiRouter = createTRPCRouter({ cacheKey: requestHash, inputTokens: usage ? usage.prompt_tokens : undefined, outputTokens: usage ? usage.completion_tokens : undefined, - model: respPayload.data.model, } : null), }, }), + // Avoid foreign key constraint error by updating the logged call after the model response is created + prisma.loggedCall.update({ + where: { + id: newLoggedCallId, + }, + data: { + modelResponseId: newModelResponseId, + }, + }), ]); if (input.tags) { diff --git a/app/src/server/api/trpc.ts b/app/src/server/api/trpc.ts index eab697e..fc1f1e3 100644 --- a/app/src/server/api/trpc.ts +++ b/app/src/server/api/trpc.ts @@ -64,9 +64,7 @@ export const createTRPCContext = async (opts: CreateNextContextOptions) => { // Get the session from the server using the getServerSession wrapper function const session = await getServerAuthSession({ req, res }); - const apiKey = req.headers["X-Openpipe-Api-Key"] as string | null; - - console.log('api key is', apiKey) + const apiKey = req.headers["x-openpipe-api-key"] as string | null; return createInnerTRPCContext({ session, diff --git a/client-libs/typescript/openai/index.ts b/client-libs/typescript/openai/index.ts index 8490362..5676f4b 100644 --- a/client-libs/typescript/openai/index.ts +++ b/client-libs/typescript/openai/index.ts @@ -1,11 +1,7 @@ import * as openai from "openai-beta"; -import { - readEnv, - type RequestOptions, -} from "openai-beta/core"; -import { - CompletionCreateParams, -} from "openai-beta/resources/chat/completions"; +import { readEnv, type RequestOptions } from "openai-beta/core"; +import { CompletionCreateParams } from "openai-beta/resources/chat/completions"; +import axios from "axios"; export * as openai from "openai-beta"; import * as openPipeClient from "../codegen"; @@ -27,10 +23,20 @@ export class OpenAI extends openai.OpenAI { super({ ...opts }); if (openPipeApiKey) { - this.openPipeApi = new openPipeClient.DefaultApi(new openPipeClient.Configuration({ - apiKey: openPipeApiKey, - basePath: openPipeBaseUrl, - })); + const axiosInstance = axios.create({ + baseURL: openPipeBaseUrl, + headers: { + 'x-openpipe-api-key': openPipeApiKey, + }, + }); + this.openPipeApi = new openPipeClient.DefaultApi( + new openPipeClient.Configuration({ + apiKey: openPipeApiKey, + basePath: openPipeBaseUrl, + }), + undefined, + axiosInstance + ); } // Override the chat property @@ -87,8 +93,7 @@ class ExtendedCompletions extends openai.OpenAI.Chat.Completions { params as CompletionCreateParams.CreateChatCompletionRequestNonStreaming, options ); - console.log('result is this', result) - this.openaiInstance.openPipeApi?.externalApiReport({ + await this.openaiInstance.openPipeApi?.externalApiReport({ startTime, endTime: Date.now(), reqPayload: params, From 6d32f1c06edb76040965dd7258717ba93b4298e8 Mon Sep 17 00:00:00 2001 From: David Corbitt Date: Mon, 7 Aug 2023 21:45:21 -0700 Subject: [PATCH 19/49] Allow admins to delete projects --- .../projectSettings/DeleteProjectDialog.tsx | 89 +++++++++++ app/src/pages/settings/index.tsx | 150 +++++++++++------- .../api/routers/organizations.router.ts | 53 +++++-- app/src/utils/accessControl.ts | 21 +++ 4 files changed, 244 insertions(+), 69 deletions(-) create mode 100644 app/src/components/projectSettings/DeleteProjectDialog.tsx diff --git a/app/src/components/projectSettings/DeleteProjectDialog.tsx b/app/src/components/projectSettings/DeleteProjectDialog.tsx new file mode 100644 index 0000000..1f586f2 --- /dev/null +++ b/app/src/components/projectSettings/DeleteProjectDialog.tsx @@ -0,0 +1,89 @@ +import { + Button, + AlertDialog, + AlertDialogBody, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogContent, + AlertDialogOverlay, + Input, + Text, + VStack, + Box, + Spinner, +} from "@chakra-ui/react"; + +import { useRouter } from "next/router"; +import { useRef, useState } from "react"; +import { api } from "~/utils/api"; +import { useHandledAsyncCallback, useSelectedOrg } from "~/utils/hooks"; + +export const DeleteProjectDialog = ({ + isOpen, + onClose, +}: { + isOpen: boolean; + onClose: () => void; +}) => { + const selectedOrg = useSelectedOrg(); + const deleteMutation = api.organizations.delete.useMutation(); + const utils = api.useContext(); + const router = useRouter(); + + const cancelRef = useRef(null); + + const [onDeleteConfirm, isDeleting] = useHandledAsyncCallback(async () => { + if (!selectedOrg.data?.id) return; + await deleteMutation.mutateAsync({ id: selectedOrg.data.id }); + await utils.organizations.list.invalidate(); + await router.push({ pathname: "/home" }); + onClose(); + }, [deleteMutation, selectedOrg, router]); + + const [nameToDelete, setNameToDelete] = useState(""); + + return ( + + + + + Delete Project + + + + + + If you delete this project all the associated data and experiments will be deleted + as well. If you are sure that you want to delete this project, please type the name + of the project below. + + + {selectedOrg.data?.name} + + setNameToDelete(e.target.value)} + /> + + + + + + + + + + + ); +}; diff --git a/app/src/pages/settings/index.tsx b/app/src/pages/settings/index.tsx index 8ca407a..cb1139e 100644 --- a/app/src/pages/settings/index.tsx +++ b/app/src/pages/settings/index.tsx @@ -4,11 +4,15 @@ import { Text, type TextProps, VStack, + HStack, Input, Button, Divider, + Icon, + useDisclosure, } from "@chakra-ui/react"; import { useEffect, useState } from "react"; +import { BsTrash } from "react-icons/bs"; import AppShell from "~/components/nav/AppShell"; import PageHeaderContainer from "~/components/nav/PageHeaderContainer"; @@ -16,6 +20,7 @@ import { api } from "~/utils/api"; import { useHandledAsyncCallback, useSelectedOrg } from "~/utils/hooks"; import ProjectBreadcrumbContents from "~/components/nav/ProjectBreadcrumbContents"; import CopiableCode from "~/components/CopiableCode"; +import { DeleteProjectDialog } from "~/components/projectSettings/DeleteProjectDialog"; export default function Settings() { const utils = api.useContext(); @@ -40,72 +45,97 @@ export default function Settings() { setName(selectedOrg?.name); }, [selectedOrg?.name]); + const deleteProjectOpen = useDisclosure(); + return ( - - - - - - - - Project Settings - - - - - - - Project Settings - - - Configure your project settings. These settings only apply to {selectedOrg?.name}. - - - - - - Display Name + <> + + + + + + + + Project Settings + + + + + + + Project Settings - setName(e.target.value)} - borderColor="gray.300" - /> - - - - - Project API Key - Use your project API key to authenticate your requests when sending data to OpenPipe. You can set this key in your environment variables, - or use it directly in your code. + Configure your project settings. These settings only apply to {selectedOrg?.name}. - + + + + Display Name + + setName(e.target.value)} + borderColor="gray.300" + /> + + + + + Project API Key + + Use your project API key to authenticate your requests when sending data to + OpenPipe. You can set this key in your environment variables, or use it directly in + your code. + + + + + + Danger Zone + + Permanently delete your project and all of its data. This action cannot be undone. + + + + Delete {selectedOrg?.name} + + + - - + + + ); } diff --git a/app/src/server/api/routers/organizations.router.ts b/app/src/server/api/routers/organizations.router.ts index 293e0f6..9e427c5 100644 --- a/app/src/server/api/routers/organizations.router.ts +++ b/app/src/server/api/routers/organizations.router.ts @@ -1,10 +1,15 @@ +import { TRPCError } from "@trpc/server"; import { v4 as uuidv4 } from "uuid"; import { z } from "zod"; import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc"; import { prisma } from "~/server/db"; import { generateApiKey } from "~/server/utils/generateApiKey"; -import { requireCanModifyOrganization, requireNothing } from "~/utils/accessControl"; +import { + requireCanModifyOrganization, + requireIsOrgAdmin, + requireNothing, +} from "~/utils/accessControl"; export const organizationsRouter = createTRPCRouter({ list: protectedProcedure.query(async ({ ctx }) => { @@ -57,14 +62,34 @@ export const organizationsRouter = createTRPCRouter({ }), get: protectedProcedure.input(z.object({ id: z.string() })).query(async ({ input, ctx }) => { requireNothing(ctx); - return await prisma.organization.findUnique({ - where: { - id: input.id, - }, - include: { - apiKeys: true, - }, - }); + const [org, userRole] = await prisma.$transaction([ + prisma.organization.findUnique({ + where: { + id: input.id, + }, + include: { + apiKeys: true, + }, + }), + prisma.organizationUser.findFirst({ + where: { + userId: ctx.session.user.id, + organizationId: input.id, + role: { + in: ["ADMIN", "MEMBER"], + }, + }, + }), + ]); + + if (!org) { + throw new TRPCError({ code: "NOT_FOUND" }); + } + + return { + ...org, + role: userRole?.role ?? null, + }; }), update: protectedProcedure .input(z.object({ id: z.string(), updates: z.object({ name: z.string() }) })) @@ -108,4 +133,14 @@ export const organizationsRouter = createTRPCRouter({ ]); return newOrg; }), + delete: protectedProcedure + .input(z.object({ id: z.string() })) + .mutation(async ({ input, ctx }) => { + await requireIsOrgAdmin(input.id, ctx); + return await prisma.organization.delete({ + where: { + id: input.id, + }, + }); + }), }); diff --git a/app/src/utils/accessControl.ts b/app/src/utils/accessControl.ts index ad55904..f4d7aac 100644 --- a/app/src/utils/accessControl.ts +++ b/app/src/utils/accessControl.ts @@ -16,6 +16,27 @@ export const requireNothing = (ctx: TRPCContext) => { ctx.markAccessControlRun(); }; +export const requireIsOrgAdmin = async (organizationId: string, ctx: TRPCContext) => { + const userId = ctx.session?.user.id; + if (!userId) { + throw new TRPCError({ code: "UNAUTHORIZED" }); + } + + const isAdmin = await prisma.organizationUser.findFirst({ + where: { + userId, + organizationId, + role: "ADMIN", + }, + }); + + if (!isAdmin) { + throw new TRPCError({ code: "UNAUTHORIZED" }); + } + + ctx.markAccessControlRun(); +}; + export const requireCanViewOrganization = async (organizationId: string, ctx: TRPCContext) => { const userId = ctx.session?.user.id; if (!userId) { From f47010a6e7734f4c0f16bcee51f67597dec24a42 Mon Sep 17 00:00:00 2001 From: David Corbitt Date: Mon, 7 Aug 2023 21:45:36 -0700 Subject: [PATCH 20/49] Fix prettier --- app/src/components/StatsCard.tsx | 4 +--- app/src/components/nav/IconLink.tsx | 2 +- .../components/nav/PageHeaderContainer.tsx | 24 ++++++++++--------- app/src/pages/experiments/[id].tsx | 1 - app/src/server/auth.ts | 6 +++-- app/src/server/scripts/backfillApiKeys.ts | 2 +- app/src/server/scripts/client-codegen.ts | 12 +++------- app/src/utils/accessControl.ts | 2 +- 8 files changed, 24 insertions(+), 29 deletions(-) diff --git a/app/src/components/StatsCard.tsx b/app/src/components/StatsCard.tsx index 56eacfb..a44352e 100644 --- a/app/src/components/StatsCard.tsx +++ b/app/src/components/StatsCard.tsx @@ -14,9 +14,7 @@ const StatsCard = ({ {title} - - View all - + View all diff --git a/app/src/components/nav/IconLink.tsx b/app/src/components/nav/IconLink.tsx index e961a34..8ae56f8 100644 --- a/app/src/components/nav/IconLink.tsx +++ b/app/src/components/nav/IconLink.tsx @@ -11,7 +11,7 @@ const IconLink = ({ icon, label, href, color, ...props }: IconLinkProps) => { - + {label} diff --git a/app/src/components/nav/PageHeaderContainer.tsx b/app/src/components/nav/PageHeaderContainer.tsx index f17b312..a682481 100644 --- a/app/src/components/nav/PageHeaderContainer.tsx +++ b/app/src/components/nav/PageHeaderContainer.tsx @@ -1,17 +1,19 @@ import { Flex, type FlexProps } from "@chakra-ui/react"; const PageHeaderContainer = (props: FlexProps) => { - return ; + return ( + + ); }; export default PageHeaderContainer; diff --git a/app/src/pages/experiments/[id].tsx b/app/src/pages/experiments/[id].tsx index d1df7cd..1d89685 100644 --- a/app/src/pages/experiments/[id].tsx +++ b/app/src/pages/experiments/[id].tsx @@ -26,7 +26,6 @@ import Head from "next/head"; import PageHeaderContainer from "~/components/nav/PageHeaderContainer"; import ProjectBreadcrumbContents from "~/components/nav/ProjectBreadcrumbContents"; - // TODO: import less to fix deployment with server side props // export const getServerSideProps = async (context: GetServerSidePropsContext<{ id: string }>) => { // const experimentId = context.params?.id as string; diff --git a/app/src/server/auth.ts b/app/src/server/auth.ts index 98e468f..f1644a4 100644 --- a/app/src/server/auth.ts +++ b/app/src/server/auth.ts @@ -7,8 +7,10 @@ import { env } from "~/env.mjs"; // The client codegen script doesn't properly read the default export // eslint-disable-next-line @typescript-eslint/no-explicit-any -const untypedGitHubModule = GitHubModule as unknown as any -const GitHubProvider: typeof GitHubModule = untypedGitHubModule.default ? untypedGitHubModule.default : untypedGitHubModule +const untypedGitHubModule = GitHubModule as unknown as any; +const GitHubProvider: typeof GitHubModule = untypedGitHubModule.default + ? untypedGitHubModule.default + : untypedGitHubModule; /** * Module augmentation for `next-auth` types. Allows us to add custom properties to the `session` diff --git a/app/src/server/scripts/backfillApiKeys.ts b/app/src/server/scripts/backfillApiKeys.ts index 869a21c..b7768f6 100644 --- a/app/src/server/scripts/backfillApiKeys.ts +++ b/app/src/server/scripts/backfillApiKeys.ts @@ -30,4 +30,4 @@ await prisma.apiKey.createMany({ data: apiKeysToCreate, }); -console.log("done"); \ No newline at end of file +console.log("done"); diff --git a/app/src/server/scripts/client-codegen.ts b/app/src/server/scripts/client-codegen.ts index e5a2945..997df8c 100644 --- a/app/src/server/scripts/client-codegen.ts +++ b/app/src/server/scripts/client-codegen.ts @@ -9,20 +9,14 @@ console.log("Exporting public OpenAPI schema to client-libs/schema.json"); const scriptPath = import.meta.url.replace("file://", ""); const clientLibsPath = path.join(path.dirname(scriptPath), "../../../../client-libs"); -const schemaPath = path.join( - clientLibsPath, - "schema.json" -); +const schemaPath = path.join(clientLibsPath, "schema.json"); console.log("Exporting schema"); fs.writeFileSync(schemaPath, JSON.stringify(openApiDocument, null, 2), "utf-8"); console.log("Generating Typescript client"); -const tsClientPath = path.join( - clientLibsPath, - "js/codegen" -); +const tsClientPath = path.join(clientLibsPath, "js/codegen"); fs.rmSync(tsClientPath, { recursive: true, force: true }); @@ -30,7 +24,7 @@ execSync( `pnpm dlx @openapitools/openapi-generator-cli generate -i "${schemaPath}" -g typescript-axios -o "${tsClientPath}"`, { stdio: "inherit", - } + }, ); console.log("Done!"); diff --git a/app/src/utils/accessControl.ts b/app/src/utils/accessControl.ts index f4d7aac..3985e1f 100644 --- a/app/src/utils/accessControl.ts +++ b/app/src/utils/accessControl.ts @@ -76,7 +76,7 @@ export const requireCanModifyOrganization = async (organizationId: string, ctx: } ctx.markAccessControlRun(); -} +}; export const requireCanViewDataset = async (datasetId: string, ctx: TRPCContext) => { const dataset = await prisma.dataset.findFirst({ From 8c5345a2914cf9b90a660d3e31aab10d8027abea Mon Sep 17 00:00:00 2001 From: David Corbitt Date: Tue, 8 Aug 2023 10:20:10 -0700 Subject: [PATCH 21/49] Allow user to open project menu on mobile --- app/src/components/nav/ProjectMenu.tsx | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/app/src/components/nav/ProjectMenu.tsx b/app/src/components/nav/ProjectMenu.tsx index a56a898..827f7c7 100644 --- a/app/src/components/nav/ProjectMenu.tsx +++ b/app/src/components/nav/ProjectMenu.tsx @@ -12,8 +12,9 @@ import { Button, useDisclosure, Spinner, + useBreakpointValue, } from "@chakra-ui/react"; -import { useEffect, useState } from "react"; +import React, { useCallback, useEffect, useState } from "react"; import Link from "next/link"; import { AiFillCaretDown } from "react-icons/ai"; import { BsGear, BsPlus } from "react-icons/bs"; @@ -55,6 +56,16 @@ export default function ProjectMenu() { await router.push({ pathname: "/settings" }); }, [createMutation, router]); + const openMenu = useCallback( + (event: React.MouseEvent) => { + event.preventDefault(); + popover.onToggle(); + }, + [popover], + ); + + const sidebarExpanded = useBreakpointValue({ base: false, md: true }); + return ( <> {selectedOrg?.name[0]?.toUpperCase()} @@ -107,10 +119,7 @@ export default function ProjectMenu() { onMouseEnter={() => setExpandButtonHovered(true)} onMouseLeave={() => setExpandButtonHovered(false)} _hover={{ bgColor: isActive ? "gray.300" : "gray.200", transitionDelay: 0 }} - onClick={(event) => { - event.preventDefault(); - popover.onToggle(); - }} + onClick={openMenu} /> From 6f8db40f74fbf3f77a487fb12139c9a6791bf8a7 Mon Sep 17 00:00:00 2001 From: David Corbitt Date: Tue, 8 Aug 2023 11:12:04 -0700 Subject: [PATCH 22/49] Fix logging --- client-libs/typescript/openai/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client-libs/typescript/openai/index.ts b/client-libs/typescript/openai/index.ts index 5676f4b..8c20bdb 100644 --- a/client-libs/typescript/openai/index.ts +++ b/client-libs/typescript/openai/index.ts @@ -85,7 +85,7 @@ class ExtendedCompletions extends openai.OpenAI.Chat.Completions { options ); // Your post API call logic here - console.log("Doing post API call for NonStreaming..."); + console.log("Doing post API call for Streaming..."); return result; } else { const startTime = Date.now(); @@ -104,7 +104,7 @@ class ExtendedCompletions extends openai.OpenAI.Chat.Completions { }); // Your post API call logic here - console.log("Doing post API call for Streaming..."); + console.log("Doing post API call for NonStreaming..."); return result; } } From a1249f17c93f167873dd8ab9911c71daa5aca181 Mon Sep 17 00:00:00 2001 From: David Corbitt Date: Tue, 8 Aug 2023 11:18:35 -0700 Subject: [PATCH 23/49] Add basic dashboard to homepage --- app/package.json | 5 + app/pnpm-lock.yaml | 293 +++++++++++++++++- .../components/dashboard/LoggedCallTable.tsx | 201 ++++++++++++ app/src/pages/home/index.tsx | 152 ++++++++- app/src/server/api/root.router.ts | 2 + .../server/api/routers/dashboard.router.ts | 95 ++++++ app/src/server/db.ts | 60 +++- 7 files changed, 789 insertions(+), 19 deletions(-) create mode 100644 app/src/components/dashboard/LoggedCallTable.tsx create mode 100644 app/src/server/api/routers/dashboard.router.ts diff --git a/app/package.json b/app/package.json index 6bfba73..802dae8 100644 --- a/app/package.json +++ b/app/package.json @@ -62,13 +62,16 @@ "json-schema-to-typescript": "^13.0.2", "json-stringify-pretty-compact": "^4.0.0", "jsonschema": "^1.4.1", + "kysely": "^0.26.1", "lodash-es": "^4.17.21", + "lucide-react": "^0.265.0", "next": "^13.4.2", "next-auth": "^4.22.1", "next-query-params": "^4.2.3", "nextjs-cors": "^2.1.2", "nextjs-routes": "^2.0.1", "openai": "4.0.0-beta.7", + "pg": "^8.11.2", "pluralize": "^8.0.0", "posthog-js": "^1.75.3", "posthog-node": "^3.1.1", @@ -84,6 +87,7 @@ "react-syntax-highlighter": "^15.5.0", "react-textarea-autosize": "^8.5.0", "recast": "^0.23.3", + "recharts": "^2.7.2", "replicate": "^0.12.3", "socket.io": "^4.7.1", "socket.io-client": "^4.7.1", @@ -108,6 +112,7 @@ "@types/json-schema": "^7.0.12", "@types/lodash-es": "^4.17.8", "@types/node": "^18.16.0", + "@types/pg": "^8.10.2", "@types/pluralize": "^0.0.30", "@types/prismjs": "^1.26.0", "@types/react": "^18.2.6", diff --git a/app/pnpm-lock.yaml b/app/pnpm-lock.yaml index fa06d85..ba5868d 100644 --- a/app/pnpm-lock.yaml +++ b/app/pnpm-lock.yaml @@ -125,9 +125,15 @@ dependencies: jsonschema: specifier: ^1.4.1 version: 1.4.1 + kysely: + specifier: ^0.26.1 + version: 0.26.1 lodash-es: specifier: ^4.17.21 version: 4.17.21 + lucide-react: + specifier: ^0.265.0 + version: 0.265.0(react@18.2.0) next: specifier: ^13.4.2 version: 13.4.2(@babel/core@7.22.9)(react-dom@18.2.0)(react@18.2.0) @@ -146,6 +152,9 @@ dependencies: openai: specifier: 4.0.0-beta.7 version: 4.0.0-beta.7 + pg: + specifier: ^8.11.2 + version: 8.11.2 pluralize: specifier: ^8.0.0 version: 8.0.0 @@ -191,6 +200,9 @@ dependencies: recast: specifier: ^0.23.3 version: 0.23.3 + recharts: + specifier: ^2.7.2 + version: 2.7.2(prop-types@15.8.1)(react-dom@18.2.0)(react@18.2.0) replicate: specifier: ^0.12.3 version: 0.12.3 @@ -259,6 +271,9 @@ devDependencies: '@types/node': specifier: ^18.16.0 version: 18.16.0 + '@types/pg': + specifier: ^8.10.2 + version: 8.10.2 '@types/pluralize': specifier: ^0.0.30 version: 0.0.30 @@ -3077,7 +3092,7 @@ packages: /@types/connect@3.4.35: resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==} dependencies: - '@types/node': 18.16.0 + '@types/node': 18.17.3 dev: true /@types/cookie@0.4.1: @@ -3089,6 +3104,48 @@ packages: dependencies: '@types/node': 18.16.0 + /@types/d3-array@3.0.5: + resolution: {integrity: sha512-Qk7fpJ6qFp+26VeQ47WY0mkwXaiq8+76RJcncDEfMc2ocRzXLO67bLFRNI4OX1aGBoPzsM5Y2T+/m1pldOgD+A==} + dev: false + + /@types/d3-color@3.1.0: + resolution: {integrity: sha512-HKuicPHJuvPgCD+np6Se9MQvS6OCbJmOjGvylzMJRlDwUXjKTTXs6Pwgk79O09Vj/ho3u1ofXnhFOaEWWPrlwA==} + dev: false + + /@types/d3-ease@3.0.0: + resolution: {integrity: sha512-aMo4eaAOijJjA6uU+GIeW018dvy9+oH5Y2VPPzjjfxevvGQ/oRDs+tfYC9b50Q4BygRR8yE2QCLsrT0WtAVseA==} + dev: false + + /@types/d3-interpolate@3.0.1: + resolution: {integrity: sha512-jx5leotSeac3jr0RePOH1KdR9rISG91QIE4Q2PYTu4OymLTZfA3SrnURSLzKH48HmXVUru50b8nje4E79oQSQw==} + dependencies: + '@types/d3-color': 3.1.0 + dev: false + + /@types/d3-path@3.0.0: + resolution: {integrity: sha512-0g/A+mZXgFkQxN3HniRDbXMN79K3CdTpLsevj+PXiTcb2hVyvkZUBg37StmgCQkaD84cUJ4uaDAWq7UJOQy2Tg==} + dev: false + + /@types/d3-scale@4.0.3: + resolution: {integrity: sha512-PATBiMCpvHJSMtZAMEhc2WyL+hnzarKzI6wAHYjhsonjWJYGq5BXTzQjv4l8m2jO183/4wZ90rKvSeT7o72xNQ==} + dependencies: + '@types/d3-time': 3.0.0 + dev: false + + /@types/d3-shape@3.1.1: + resolution: {integrity: sha512-6Uh86YFF7LGg4PQkuO2oG6EMBRLuW9cbavUW46zkIO5kuS2PfTqo2o9SkgtQzguBHbLgNnU90UNsITpsX1My+A==} + dependencies: + '@types/d3-path': 3.0.0 + dev: false + + /@types/d3-time@3.0.0: + resolution: {integrity: sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg==} + dev: false + + /@types/d3-timer@3.0.0: + resolution: {integrity: sha512-HNB/9GHqu7Fo8AQiugyJbv6ZxYz58wef0esl4Mv828w1ZKpAshw/uFWVDUcIB9KKFeFKoxS3cHY07FFgtTRZ1g==} + dev: false + /@types/debug@4.1.8: resolution: {integrity: sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==} dependencies: @@ -3139,7 +3196,7 @@ packages: resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} dependencies: '@types/minimatch': 5.1.2 - '@types/node': 18.16.0 + '@types/node': 18.17.3 dev: false /@types/hast@2.3.5: @@ -3199,7 +3256,7 @@ packages: /@types/node-fetch@2.6.4: resolution: {integrity: sha512-1ZX9fcN4Rvkvgv4E6PAY5WXUFWFcRWxZa3EW83UjycOB9ljJCedb2CupIP4RZMEwF/M3eTcCihbBRgwtGbg5Rg==} dependencies: - '@types/node': 18.16.0 + '@types/node': 18.17.3 form-data: 3.0.1 dev: false @@ -3220,10 +3277,9 @@ packages: /@types/pg@8.10.2: resolution: {integrity: sha512-MKFs9P6nJ+LAeHLU3V0cODEOgyThJ3OAnmOlsZsxux6sfQs3HRXR5bBn7xG5DjckEFhTAxsXi7k7cd0pCMxpJw==} dependencies: - '@types/node': 18.16.0 + '@types/node': 18.17.3 pg-protocol: 1.6.0 pg-types: 4.0.1 - dev: false /@types/pluralize@0.0.30: resolution: {integrity: sha512-kVww6xZrW/db5BR9OqiT71J9huRdQ+z/r+LbDuT7/EK50mCmj5FoaIARnVv0rvjUS/YpDox0cDU9lpQT011VBA==} @@ -3284,7 +3340,7 @@ packages: resolution: {integrity: sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q==} dependencies: '@types/mime': 1.3.2 - '@types/node': 18.16.0 + '@types/node': 18.17.3 dev: true /@types/serve-static@1.15.2: @@ -4364,6 +4420,10 @@ packages: postcss-value-parser: 4.2.0 dev: false + /css-unit-converter@1.1.2: + resolution: {integrity: sha512-IiJwMC8rdZE0+xiEZHeru6YoONC4rfPMqGm2W85jMIbkFvv5nFTwJVFHam2eFrN6txmoUYFAFXiv8ICVeTO0MA==} + dev: false + /csstype@2.6.21: resolution: {integrity: sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==} dev: false @@ -4375,6 +4435,77 @@ packages: resolution: {integrity: sha512-JiQosUWiOFgp4hQn0an+SBoV9IKdqzhROM0iiN4LB7UpfJBlsSJlWl9nq4zGgxgMAzHJ6V4t29VAVD+3+2NJAg==} dev: true + /d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + dependencies: + internmap: 2.0.3 + dev: false + + /d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + dev: false + + /d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + dev: false + + /d3-format@3.1.0: + resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==} + engines: {node: '>=12'} + dev: false + + /d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + dependencies: + d3-color: 3.1.0 + dev: false + + /d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + dev: false + + /d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.0 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + dev: false + + /d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + dependencies: + d3-path: 3.1.0 + dev: false + + /d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + dependencies: + d3-time: 3.1.0 + dev: false + + /d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + dependencies: + d3-array: 3.2.4 + dev: false + + /d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + dev: false + /d@1.0.1: resolution: {integrity: sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==} dependencies: @@ -4430,6 +4561,10 @@ packages: dependencies: ms: 2.1.2 + /decimal.js-light@2.5.1: + resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + dev: false + /decimal.js@10.4.3: resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} dev: false @@ -4556,6 +4691,12 @@ packages: esutils: 2.0.3 dev: true + /dom-helpers@3.4.0: + resolution: {integrity: sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==} + dependencies: + '@babel/runtime': 7.22.6 + dev: false + /dom-helpers@5.2.1: resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} dependencies: @@ -4635,7 +4776,7 @@ packages: dependencies: '@types/cookie': 0.4.1 '@types/cors': 2.8.13 - '@types/node': 18.16.0 + '@types/node': 18.17.3 accepts: 1.3.8 base64id: 2.0.0 cookie: 0.4.2 @@ -5182,6 +5323,10 @@ packages: engines: {node: '>=6'} dev: false + /eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + dev: false + /events@3.3.0: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} @@ -5264,6 +5409,11 @@ packages: /fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + /fast-equals@5.0.1: + resolution: {integrity: sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==} + engines: {node: '>=6.0.0'} + dev: false + /fast-glob@3.3.1: resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} engines: {node: '>=8.6.0'} @@ -5849,6 +5999,11 @@ packages: side-channel: 1.0.4 dev: true + /internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + dev: false + /invariant@2.2.4: resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} dependencies: @@ -6213,6 +6368,11 @@ packages: object.values: 1.1.6 dev: true + /kysely@0.26.1: + resolution: {integrity: sha512-FVRomkdZofBu3O8SiwAOXrwbhPZZr8mBN5ZeUWyprH29jzvy6Inzqbd0IMmGxpd4rcOCL9HyyBNWBa8FBqDAdg==} + engines: {node: '>=14.0.0'} + dev: false + /language-subtag-registry@0.3.22: resolution: {integrity: sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==} dev: true @@ -6343,6 +6503,14 @@ packages: resolution: {integrity: sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ==} dev: false + /lucide-react@0.265.0(react@18.2.0): + resolution: {integrity: sha512-znyvziBEUQ7CKR31GiU4viomQbJrpDLG5ac+FajwiZIavC3YbPFLkzQx3dCXT4JWJx/pB34EwmtiZ0ElGZX0PA==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 + dependencies: + react: 18.2.0 + dev: false + /magic-string@0.27.0: resolution: {integrity: sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==} engines: {node: '>=12'} @@ -6782,7 +6950,6 @@ packages: /obuf@1.1.2: resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} - dev: false /oidc-token-hash@5.0.3: resolution: {integrity: sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==} @@ -7009,12 +7176,10 @@ packages: /pg-int8@1.0.1: resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} engines: {node: '>=4.0.0'} - dev: false /pg-numeric@1.0.2: resolution: {integrity: sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==} engines: {node: '>=4'} - dev: false /pg-pool@3.6.1(pg@8.11.2): resolution: {integrity: sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==} @@ -7026,7 +7191,6 @@ packages: /pg-protocol@1.6.0: resolution: {integrity: sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q==} - dev: false /pg-types@2.2.0: resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} @@ -7050,7 +7214,6 @@ packages: postgres-date: 2.0.1 postgres-interval: 3.0.0 postgres-range: 1.1.3 - dev: false /pg@8.11.2: resolution: {integrity: sha512-l4rmVeV8qTIrrPrIR3kZQqBgSN93331s9i6wiUiLOSk0Q7PmUxZD/m1rQI622l3NfqBby9Ar5PABfS/SulfieQ==} @@ -7098,6 +7261,10 @@ packages: engines: {node: '>=4'} dev: false + /postcss-value-parser@3.3.1: + resolution: {integrity: sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==} + dev: false + /postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} dev: false @@ -7128,7 +7295,6 @@ packages: /postgres-array@3.0.2: resolution: {integrity: sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog==} engines: {node: '>=12'} - dev: false /postgres-bytea@1.0.0: resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==} @@ -7140,7 +7306,6 @@ packages: engines: {node: '>= 6'} dependencies: obuf: 1.1.2 - dev: false /postgres-date@1.0.7: resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} @@ -7150,7 +7315,6 @@ packages: /postgres-date@2.0.1: resolution: {integrity: sha512-YtMKdsDt5Ojv1wQRvUhnyDJNSr2dGIC96mQVKz7xufp07nfuFONzdaowrMHjlAzY6GDLd4f+LUHHAAM1h4MdUw==} engines: {node: '>=12'} - dev: false /postgres-interval@1.2.0: resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} @@ -7162,11 +7326,9 @@ packages: /postgres-interval@3.0.0: resolution: {integrity: sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==} engines: {node: '>=12'} - dev: false /postgres-range@1.1.3: resolution: {integrity: sha512-VdlZoocy5lCP0c/t66xAfclglEapXPCIVhqqJRncYpvbCgImF0w67aPKfbqUMr72tO2k5q0TdTZwCLjPTI6C9g==} - dev: false /posthog-js@1.75.3: resolution: {integrity: sha512-q5xP4R/Tx8E6H0goZQjY+URMLATFiYXc2raHA+31aNvpBs118fPTmExa4RK6MgRZDFhBiMUBZNT6aj7dM3SyUQ==} @@ -7453,6 +7615,10 @@ packages: react-base16-styling: 0.9.1 dev: false + /react-lifecycles-compat@3.0.4: + resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==} + dev: false + /react-remove-scroll-bar@2.3.4(@types/react@18.2.6)(react@18.2.0): resolution: {integrity: sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A==} engines: {node: '>=10'} @@ -7488,6 +7654,17 @@ packages: use-sidecar: 1.1.2(@types/react@18.2.6)(react@18.2.0) dev: false + /react-resize-detector@8.1.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-S7szxlaIuiy5UqLhLL1KY3aoyGHbZzsTpYal9eYMwCyKqoqoVLCmIgAgNyIM1FhnP2KyBygASJxdhejrzjMb+w==} + peerDependencies: + react: ^16.0.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 + dependencies: + lodash: 4.17.21 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /react-select@5.7.4(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-NhuE56X+p9QDFh4BgeygHFIvJJszO1i1KSkg/JPcIJrbovyRtI+GuOEa4XzFCEpZRAEoEI8u/cAHK+jG/PgUzQ==} peerDependencies: @@ -7509,6 +7686,20 @@ packages: - '@types/react' dev: false + /react-smooth@2.0.3(prop-types@15.8.1)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-yl4y3XiMorss7ayF5QnBiSprig0+qFHui8uh7Hgg46QX5O+aRMRKlfGGNGLHno35JkQSvSYY8eCWkBfHfrSHfg==} + peerDependencies: + prop-types: ^15.6.0 + react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + dependencies: + fast-equals: 5.0.1 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-transition-group: 2.9.0(react-dom@18.2.0)(react@18.2.0) + dev: false + /react-ssr-prepass@1.5.0(react@18.2.0): resolution: {integrity: sha512-yFNHrlVEReVYKsLI5lF05tZoHveA5pGzjFbFJY/3pOqqjGOmMmqx83N4hIjN2n6E1AOa+eQEUxs3CgRnPmT0RQ==} peerDependencies: @@ -7561,6 +7752,20 @@ packages: - '@types/react' dev: false + /react-transition-group@2.9.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==} + peerDependencies: + react: '>=15.0.0' + react-dom: '>=15.0.0' + dependencies: + dom-helpers: 3.4.0 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-lifecycles-compat: 3.0.4 + dev: false + /react-transition-group@4.4.5(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} peerDependencies: @@ -7621,6 +7826,41 @@ packages: tslib: 2.6.1 dev: false + /recharts-scale@0.4.5: + resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==} + dependencies: + decimal.js-light: 2.5.1 + dev: false + + /recharts@2.7.2(prop-types@15.8.1)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-HMKRBkGoOXHW+7JcRa6+MukPSifNtJlqbc+JreGVNA407VLE/vOP+8n3YYjprDVVIF9E2ZgwWnL3D7K/LUFzBg==} + engines: {node: '>=12'} + peerDependencies: + prop-types: ^15.6.0 + react: ^16.0.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 + dependencies: + classnames: 2.3.2 + eventemitter3: 4.0.7 + lodash: 4.17.21 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-is: 16.13.1 + react-resize-detector: 8.1.0(react-dom@18.2.0)(react@18.2.0) + react-smooth: 2.0.3(prop-types@15.8.1)(react-dom@18.2.0)(react@18.2.0) + recharts-scale: 0.4.5 + reduce-css-calc: 2.1.8 + victory-vendor: 36.6.11 + dev: false + + /reduce-css-calc@2.1.8: + resolution: {integrity: sha512-8liAVezDmUcH+tdzoEGrhfbGcP7nOV4NkGE3a74+qqvE7nt9i4sKLGBuZNOnpI4WiGksiNPklZxva80061QiPg==} + dependencies: + css-unit-converter: 1.1.2 + postcss-value-parser: 3.3.1 + dev: false + /refractor@3.6.0: resolution: {integrity: sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==} dependencies: @@ -8628,6 +8868,25 @@ packages: engines: {node: '>= 0.8'} dev: false + /victory-vendor@36.6.11: + resolution: {integrity: sha512-nT8kCiJp8dQh8g991J/R5w5eE2KnO8EAIP0xocWlh9l2okngMWglOPoMZzJvek8Q1KUc4XE/mJxTZnvOB1sTYg==} + dependencies: + '@types/d3-array': 3.0.5 + '@types/d3-ease': 3.0.0 + '@types/d3-interpolate': 3.0.1 + '@types/d3-scale': 4.0.3 + '@types/d3-shape': 3.1.1 + '@types/d3-time': 3.0.0 + '@types/d3-timer': 3.0.0 + d3-array: 3.2.4 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-scale: 4.0.2 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-timer: 3.0.1 + dev: false + /vite-node@0.33.0(@types/node@18.16.0): resolution: {integrity: sha512-19FpHYbwWWxDr73ruNahC+vtEdza52kA90Qb3La98yZ0xULqV8A5JLNPUff0f5zID4984tW7l3DH2przTJUZSw==} engines: {node: '>=v14.18.0'} diff --git a/app/src/components/dashboard/LoggedCallTable.tsx b/app/src/components/dashboard/LoggedCallTable.tsx new file mode 100644 index 0000000..70aa7a2 --- /dev/null +++ b/app/src/components/dashboard/LoggedCallTable.tsx @@ -0,0 +1,201 @@ +import { + Box, + Card, + CardHeader, + Heading, + Table, + Tbody, + Td, + Th, + Thead, + Tr, + Tooltip, + Collapse, + HStack, + VStack, + IconButton, + useToast, + Icon, + Button, + ButtonGroup, +} from "@chakra-ui/react"; +import dayjs from "dayjs"; +import relativeTime from "dayjs/plugin/relativeTime"; +import { ChevronUpIcon, ChevronDownIcon, CopyIcon } from "lucide-react"; +import { useMemo, useState } from "react"; +import { type RouterOutputs, api } from "~/utils/api"; +import SyntaxHighlighter from "react-syntax-highlighter"; +import { atelierCaveLight } from "react-syntax-highlighter/dist/cjs/styles/hljs"; +import stringify from "json-stringify-pretty-compact"; +import Link from "next/link"; + +dayjs.extend(relativeTime); + +type LoggedCall = RouterOutputs["dashboard"]["loggedCalls"][0]; + +const FormattedJson = ({ json }: { json: any }) => { + const jsonString = stringify(json, { maxLength: 40 }); + const toast = useToast(); + + const copyToClipboard = async (text: string) => { + try { + await navigator.clipboard.writeText(text); + toast({ + title: "Copied to clipboard", + status: "success", + duration: 2000, + }); + } catch (err) { + toast({ + title: "Failed to copy to clipboard", + status: "error", + duration: 2000, + }); + } + }; + + return ( + + + {jsonString} + + } + position="absolute" + top={1} + right={1} + size="xs" + variant="ghost" + onClick={() => void copyToClipboard(jsonString)} + /> + + ); +}; + +function TableRow({ + loggedCall, + isExpanded, + onToggle, +}: { + loggedCall: LoggedCall; + isExpanded: boolean; + onToggle: () => void; +}) { + const isError = loggedCall.modelResponse?.respStatus !== 200; + const timeAgo = dayjs(loggedCall.startTime).fromNow(); + const fullTime = dayjs(loggedCall.startTime).toString(); + + const model = useMemo( + () => loggedCall.tags.find((tag) => tag.name.startsWith("$model"))?.value, + [loggedCall.tags], + ); + + return ( + <> + td": { borderBottom: "none" }, + }} + > + + + + + + + {timeAgo} + + + + {model} + {((loggedCall.modelResponse?.durationMs ?? 0) / 1000).toFixed(2)}s + {loggedCall.modelResponse?.inputTokens} + {loggedCall.modelResponse?.outputTokens} + + {loggedCall.modelResponse?.respStatus ?? "No response"} + + + + + + + + + Input + + + + Output + + + + + + + + + + + + ); +} + +export default function LoggedCallTable() { + const [expandedRow, setExpandedRow] = useState(null); + const loggedCalls = api.dashboard.loggedCalls.useQuery({}); + + return ( + + + + Logged Calls + + + + + + + + + + + + + + + {loggedCalls.data?.map((loggedCall) => { + return ( + { + if (loggedCall.id === expandedRow) { + setExpandedRow(null); + } else { + setExpandedRow(loggedCall.id); + } + }} + /> + ); + })} + +
+ TimeModelDurationInput tokensOutput tokensStatus
+
+ ); +} diff --git a/app/src/pages/home/index.tsx b/app/src/pages/home/index.tsx index 3347590..35e2606 100644 --- a/app/src/pages/home/index.tsx +++ b/app/src/pages/home/index.tsx @@ -1,13 +1,62 @@ -import { Breadcrumb, BreadcrumbItem, Divider, Text, VStack } from "@chakra-ui/react"; +import { + Heading, + Text, + Stat, + StatLabel, + StatNumber, + VStack, + HStack, + Card, + CardBody, + CardHeader, + Icon, + Table, + Tbody, + Tr, + Td, + Divider, + Breadcrumb, + BreadcrumbItem, +} from "@chakra-ui/react"; +import { + LineChart, + Line, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + Legend, + ResponsiveContainer, +} from "recharts"; +import { Ban, DollarSign, Hash } from "lucide-react"; +import { useMemo } from "react"; import AppShell from "~/components/nav/AppShell"; import PageHeaderContainer from "~/components/nav/PageHeaderContainer"; import ProjectBreadcrumbContents from "~/components/nav/ProjectBreadcrumbContents"; import { useSelectedOrg } from "~/utils/hooks"; +import dayjs from "~/utils/dayjs"; +import { api } from "~/utils/api"; +import LoggedCallTable from "~/components/dashboard/LoggedCallTable"; export default function HomePage() { const { data: selectedOrg } = useSelectedOrg(); + const stats = api.dashboard.stats.useQuery( + { organizationId: selectedOrg?.id ?? "" }, + { enabled: !!selectedOrg }, + ); + + const data = useMemo(() => { + return ( + stats.data?.periods.map(({ period, numQueries, totalCost }) => ({ + period, + Requests: numQueries, + "Total Spent (USD)": parseFloat(totalCost.toString()), + })) || [] + ); + }, [stats.data]); + return ( @@ -25,6 +74,107 @@ export default function HomePage() { {selectedOrg?.name} + + + + + + Usage Statistics + + + + + + dayjs(str).format("MMM D")} + /> + + + + + + + + + + + + + + + + + Total Spent + + + + ${parseFloat(stats.data?.totals?.totalCost?.toString() ?? "0").toFixed(2)} + + + + + + + + + Total Requests + + + + {stats.data?.totals?.numQueries + ? parseInt(stats.data?.totals?.numQueries.toString())?.toLocaleString() + : undefined} + + + + + + + + + Errors + + + + + + {stats.data?.errors?.map((error) => ( + + + + + ))} + +
+ {error.name} ({error.code}) + + {parseInt(error.count.toString()).toLocaleString()} +
+
+
+
+
+ +
); diff --git a/app/src/server/api/root.router.ts b/app/src/server/api/root.router.ts index d50d907..702f1ea 100644 --- a/app/src/server/api/root.router.ts +++ b/app/src/server/api/root.router.ts @@ -10,6 +10,7 @@ import { datasetsRouter } from "./routers/datasets.router"; import { datasetEntries } from "./routers/datasetEntries.router"; import { externalApiRouter } from "./routers/externalApi.router"; import { organizationsRouter } from "./routers/organizations.router"; +import { dashboardRouter } from "./routers/dashboard.router"; /** * This is the primary router for your server. @@ -27,6 +28,7 @@ export const appRouter = createTRPCRouter({ datasets: datasetsRouter, datasetEntries: datasetEntries, organizations: organizationsRouter, + dashboard: dashboardRouter, externalApi: externalApiRouter, }); diff --git a/app/src/server/api/routers/dashboard.router.ts b/app/src/server/api/routers/dashboard.router.ts new file mode 100644 index 0000000..f4de2a9 --- /dev/null +++ b/app/src/server/api/routers/dashboard.router.ts @@ -0,0 +1,95 @@ +import { sql } from "kysely"; +import { z } from "zod"; +import { createTRPCRouter, publicProcedure } from "~/server/api/trpc"; +import { kysely, prisma } from "~/server/db"; + +export const dashboardRouter = createTRPCRouter({ + stats: publicProcedure + .input( + z.object({ + startDate: z.string().optional(), + organizationId: z.string(), + }), + ) + .query(async ({ input }) => { + console.log("made it 1"); + // Return the stats group by hour + const periods = await kysely + .selectFrom("LoggedCall") + .leftJoin( + "LoggedCallModelResponse", + "LoggedCall.id", + "LoggedCallModelResponse.originalLoggedCallId", + ) + .where("organizationId", "=", input.organizationId) + .select(({ fn }) => [ + sql`date_trunc('day', "LoggedCallModelResponse"."startTime")`.as("period"), + sql`count("LoggedCall"."id")::int`.as("numQueries"), + fn.sum(fn.coalesce('LoggedCallModelResponse.totalCost', sql`0`)).as("totalCost"), + ]) + .groupBy("period") + .orderBy("period") + .execute(); + + console.log("made it 2"); + + const totals = await kysely + .selectFrom("LoggedCall") + .where("organizationId", "=", input.organizationId) + .leftJoin( + "LoggedCallModelResponse", + "LoggedCall.id", + "LoggedCallModelResponse.originalLoggedCallId", + ) + .select(({ fn }) => [ + fn.sum(fn.coalesce('LoggedCallModelResponse.totalCost', sql`0`)).as("totalCost"), + fn.count("id").as("numQueries"), + ]) + .executeTakeFirst(); + + console.log("made it 3"); + + const errors = await kysely + .selectFrom("LoggedCall") + .where("organizationId", "=", input.organizationId) + .leftJoin( + "LoggedCallModelResponse", + "LoggedCall.id", + "LoggedCallModelResponse.originalLoggedCallId", + ) + .select(({ fn }) => [fn.count("id").as("count"), "respStatus as code"]) + .where("respStatus", ">", 200) + .groupBy("code") + .orderBy("count", "desc") + .execute(); + + console.log("made it 4"); + + const namedErrors = errors.map((e) => { + if (e.code === 429) { + return { ...e, name: "Rate limited" }; + } else if (e.code === 500) { + return { ...e, name: "Internal server error" }; + } else { + return { ...e, name: "Other" }; + } + }); + + console.log("data is", { periods, totals, errors: namedErrors }); + + return { periods, totals, errors: namedErrors }; + // const resp = await kysely.selectFrom("LoggedCall").selectAll().execute(); + }), + + // TODO useInfiniteQuery + // https://discord.com/channels/966627436387266600/1122258443886153758/1122258443886153758 + loggedCalls: publicProcedure.input(z.object({})).query(async ({ input }) => { + const loggedCalls = await prisma.loggedCall.findMany({ + orderBy: { startTime: "desc" }, + include: { tags: true, modelResponse: true }, + take: 20, + }); + + return loggedCalls; + }), +}); diff --git a/app/src/server/db.ts b/app/src/server/db.ts index 1cbc4cf..8ed322b 100644 --- a/app/src/server/db.ts +++ b/app/src/server/db.ts @@ -1,6 +1,56 @@ -import { PrismaClient } from "@prisma/client"; +import { + type Experiment, + type PromptVariant, + type TestScenario, + type TemplateVariable, + type ScenarioVariantCell, + type ModelResponse, + type Evaluation, + type OutputEvaluation, + type Dataset, + type DatasetEntry, + type Organization, + type OrganizationUser, + type WorldChampEntrant, + type LoggedCall, + type LoggedCallModelResponse, + type LoggedCallTag, + type ApiKey, + type Account, + type Session, + type User, + type VerificationToken, + PrismaClient, +} from "@prisma/client"; +import { Kysely, PostgresDialect } from "kysely"; +import { Pool } from "pg"; + import { env } from "~/env.mjs"; +interface DB { + Experiment: Experiment; + PromptVariant: PromptVariant; + TestScenario: TestScenario; + TemplateVariable: TemplateVariable; + ScenarioVariantCell: ScenarioVariantCell; + ModelResponse: ModelResponse; + Evaluation: Evaluation; + OutputEvaluation: OutputEvaluation; + Dataset: Dataset; + DatasetEntry: DatasetEntry; + Organization: Organization; + OrganizationUser: OrganizationUser; + WorldChampEntrant: WorldChampEntrant; + LoggedCall: LoggedCall; + LoggedCallModelResponse: LoggedCallModelResponse; + LoggedCallTag: LoggedCallTag; + ApiKey: ApiKey; + Account: Account; + Session: Session; + User: User; + VerificationToken: VerificationToken; +} + const globalForPrisma = globalThis as unknown as { prisma: PrismaClient | undefined; }; @@ -14,4 +64,12 @@ export const prisma = : ["error"], }); +export const kysely = new Kysely({ + dialect: new PostgresDialect({ + pool: new Pool({ + connectionString: env.DATABASE_URL, + }), + }), +}); + if (env.NODE_ENV !== "production") globalForPrisma.prisma = prisma; From bd7c8b43b0cb86bcd6f6e4956437fbc091650d34 Mon Sep 17 00:00:00 2001 From: David Corbitt Date: Tue, 8 Aug 2023 11:27:59 -0700 Subject: [PATCH 24/49] utlilize useHandledAsyncCallback in CopiableCode --- app/src/components/CopiableCode.tsx | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/app/src/components/CopiableCode.tsx b/app/src/components/CopiableCode.tsx index 8215b78..1c4d136 100644 --- a/app/src/components/CopiableCode.tsx +++ b/app/src/components/CopiableCode.tsx @@ -1,17 +1,14 @@ import { HStack, Icon, IconButton, Tooltip, Text } from "@chakra-ui/react"; -import { useCallback, useState } from "react"; +import { useState } from "react"; import { MdContentCopy } from "react-icons/md"; +import { useHandledAsyncCallback } from "~/utils/hooks"; const CopiableCode = ({ code }: { code: string }) => { const [copied, setCopied] = useState(false); - const copyToClipboard = useCallback(() => { - const onCopy = async () => { - console.log("copied!"); - await navigator.clipboard.writeText(code); - setCopied(true); - }; - void onCopy(); + const [copyToClipboard] = useHandledAsyncCallback(async () => { + await navigator.clipboard.writeText(code); + setCopied(true); }, [code]); return ( Date: Tue, 8 Aug 2023 11:32:13 -0700 Subject: [PATCH 25/49] Use boxSize on ProjectBreadcrumbContents --- app/src/components/nav/ProjectBreadcrumbContents.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/src/components/nav/ProjectBreadcrumbContents.tsx b/app/src/components/nav/ProjectBreadcrumbContents.tsx index c2ecdbc..be3f999 100644 --- a/app/src/components/nav/ProjectBreadcrumbContents.tsx +++ b/app/src/components/nav/ProjectBreadcrumbContents.tsx @@ -16,10 +16,7 @@ export default function ProjectBreadcrumbContents({ orgName = "" }: { orgName?: p={1} borderRadius={4} backgroundColor="orange.100" - minW={6} - maxW={6} - minH={6} - maxH={6} + boxSize={6} alignItems="center" justifyContent="center" > From b9396e63ccb2ef3ec9f28d88c9caed0efd4ce64e Mon Sep 17 00:00:00 2001 From: David Corbitt Date: Tue, 8 Aug 2023 11:34:15 -0700 Subject: [PATCH 26/49] Change /settings to /project/settings --- app/@types/nextjs-routes.d.ts | 2 +- app/src/components/nav/AppShell.tsx | 2 +- app/src/components/nav/ProjectMenu.tsx | 4 ++-- app/src/pages/{ => project}/settings/index.tsx | 0 4 files changed, 4 insertions(+), 4 deletions(-) rename app/src/pages/{ => project}/settings/index.tsx (100%) diff --git a/app/@types/nextjs-routes.d.ts b/app/@types/nextjs-routes.d.ts index 79d87f2..5b61bbc 100644 --- a/app/@types/nextjs-routes.d.ts +++ b/app/@types/nextjs-routes.d.ts @@ -24,8 +24,8 @@ declare module "nextjs-routes" { | StaticRoute<"/experiments"> | StaticRoute<"/home"> | StaticRoute<"/"> + | StaticRoute<"/project/settings"> | StaticRoute<"/sentry-example-page"> - | StaticRoute<"/settings"> | StaticRoute<"/world-champs"> | StaticRoute<"/world-champs/signup">; diff --git a/app/src/components/nav/AppShell.tsx b/app/src/components/nav/AppShell.tsx index 43f2217..c577d55 100644 --- a/app/src/components/nav/AppShell.tsx +++ b/app/src/components/nav/AppShell.tsx @@ -87,7 +87,7 @@ const NavSidebar = () => { > CONFIGURATION - + {user && } diff --git a/app/src/components/nav/ProjectMenu.tsx b/app/src/components/nav/ProjectMenu.tsx index 827f7c7..3a38afb 100644 --- a/app/src/components/nav/ProjectMenu.tsx +++ b/app/src/components/nav/ProjectMenu.tsx @@ -53,7 +53,7 @@ export default function ProjectMenu() { const newOrg = await createMutation.mutateAsync({ name: "New Project" }); await utils.organizations.list.invalidate(); setSelectedOrgId(newOrg.id); - await router.push({ pathname: "/settings" }); + await router.push({ pathname: "/project/settings" }); }, [createMutation, router]); const openMenu = useCallback( @@ -195,7 +195,7 @@ const ProjectOption = ({ {org.name} } variant="ghost" diff --git a/app/src/pages/settings/index.tsx b/app/src/pages/project/settings/index.tsx similarity index 100% rename from app/src/pages/settings/index.tsx rename to app/src/pages/project/settings/index.tsx From ae7acbfdd4ac72ff61721c062f8beaad25b2ca97 Mon Sep 17 00:00:00 2001 From: David Corbitt Date: Tue, 8 Aug 2023 11:40:58 -0700 Subject: [PATCH 27/49] Require user to be able to view organization to get it --- app/src/server/api/routers/organizations.router.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/server/api/routers/organizations.router.ts b/app/src/server/api/routers/organizations.router.ts index 9e427c5..263c4aa 100644 --- a/app/src/server/api/routers/organizations.router.ts +++ b/app/src/server/api/routers/organizations.router.ts @@ -7,6 +7,7 @@ import { prisma } from "~/server/db"; import { generateApiKey } from "~/server/utils/generateApiKey"; import { requireCanModifyOrganization, + requireCanViewOrganization, requireIsOrgAdmin, requireNothing, } from "~/utils/accessControl"; @@ -61,7 +62,7 @@ export const organizationsRouter = createTRPCRouter({ return organizations; }), get: protectedProcedure.input(z.object({ id: z.string() })).query(async ({ input, ctx }) => { - requireNothing(ctx); + await requireCanViewOrganization(input.id, ctx); const [org, userRole] = await prisma.$transaction([ prisma.organization.findUnique({ where: { From ea91d692d3cd3436032932dd12918b530674174e Mon Sep 17 00:00:00 2001 From: David Corbitt Date: Tue, 8 Aug 2023 11:45:03 -0700 Subject: [PATCH 28/49] Use crypto-random-string --- app/package.json | 1 + app/pnpm-lock.yaml | 15 +++++++++++++++ app/src/server/utils/generateApiKey.ts | 12 +++--------- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/app/package.json b/app/package.json index 802dae8..e7c5818 100644 --- a/app/package.json +++ b/app/package.json @@ -50,6 +50,7 @@ "chroma-js": "^2.4.2", "concurrently": "^8.2.0", "cors": "^2.8.5", + "crypto-random-string": "^5.0.0", "dayjs": "^1.11.8", "dedent": "^1.0.1", "dotenv": "^16.3.1", diff --git a/app/pnpm-lock.yaml b/app/pnpm-lock.yaml index ba5868d..76ff991 100644 --- a/app/pnpm-lock.yaml +++ b/app/pnpm-lock.yaml @@ -89,6 +89,9 @@ dependencies: cors: specifier: ^2.8.5 version: 2.8.5 + crypto-random-string: + specifier: ^5.0.0 + version: 5.0.0 dayjs: specifier: ^1.11.8 version: 1.11.8 @@ -4393,6 +4396,13 @@ packages: resolution: {integrity: sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==} dev: false + /crypto-random-string@5.0.0: + resolution: {integrity: sha512-KWjTXWwxFd6a94m5CdRGW/t82Tr8DoBc9dNnPCAbFI1EBweN6v1tv8y4Y1m7ndkp/nkIBRxUxAzpaBnR2k3bcQ==} + engines: {node: '>=14.16'} + dependencies: + type-fest: 2.19.0 + dev: false + /css-background-parser@0.1.0: resolution: {integrity: sha512-2EZLisiZQ+7m4wwur/qiYJRniHX4K5Tc9w93MT3AS0WS1u5kaZ4FKXlOTBhOjc+CgEgPiGY+fX1yWD8UwpEqUA==} dev: false @@ -8623,6 +8633,11 @@ packages: engines: {node: '>=8'} dev: false + /type-fest@2.19.0: + resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} + engines: {node: '>=12.20'} + dev: false + /type-fest@4.0.0: resolution: {integrity: sha512-d/oYtUnPM9zar2fqqGLYPzgcY0qUlYK0evgNVti93xpzfjGkMgZHu9Lvgrkn0rqGXTgsFRxFamzjGoD9Uo+dgw==} engines: {node: '>=16'} diff --git a/app/src/server/utils/generateApiKey.ts b/app/src/server/utils/generateApiKey.ts index 5ff451b..25ce442 100644 --- a/app/src/server/utils/generateApiKey.ts +++ b/app/src/server/utils/generateApiKey.ts @@ -1,11 +1,5 @@ +import cryptoRandomString from "crypto-random-string"; + const KEY_LENGTH = 42; -export const generateApiKey = () => { - const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - let randomChars = ""; - for (let i = 0; i < KEY_LENGTH; i++) { - randomChars += chars.charAt(Math.floor(Math.random() * chars.length)); - } - - return `opc_${randomChars}`; -}; +export const generateApiKey = () => `opc_${cryptoRandomString({ length: KEY_LENGTH })}`; From 6188f5556901c37b90dc7d1fbb13309b26d72544 Mon Sep 17 00:00:00 2001 From: David Corbitt Date: Tue, 8 Aug 2023 11:49:08 -0700 Subject: [PATCH 29/49] Fix dashboard stats --- .../server/api/routers/dashboard.router.ts | 20 +++++-------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/app/src/server/api/routers/dashboard.router.ts b/app/src/server/api/routers/dashboard.router.ts index f4de2a9..ba5d5b6 100644 --- a/app/src/server/api/routers/dashboard.router.ts +++ b/app/src/server/api/routers/dashboard.router.ts @@ -12,7 +12,6 @@ export const dashboardRouter = createTRPCRouter({ }), ) .query(async ({ input }) => { - console.log("made it 1"); // Return the stats group by hour const periods = await kysely .selectFrom("LoggedCall") @@ -25,30 +24,26 @@ export const dashboardRouter = createTRPCRouter({ .select(({ fn }) => [ sql`date_trunc('day', "LoggedCallModelResponse"."startTime")`.as("period"), sql`count("LoggedCall"."id")::int`.as("numQueries"), - fn.sum(fn.coalesce('LoggedCallModelResponse.totalCost', sql`0`)).as("totalCost"), + fn.sum(fn.coalesce("LoggedCallModelResponse.totalCost", sql`0`)).as("totalCost"), ]) .groupBy("period") .orderBy("period") .execute(); - console.log("made it 2"); - const totals = await kysely .selectFrom("LoggedCall") - .where("organizationId", "=", input.organizationId) .leftJoin( "LoggedCallModelResponse", "LoggedCall.id", "LoggedCallModelResponse.originalLoggedCallId", ) + .where("organizationId", "=", input.organizationId) .select(({ fn }) => [ - fn.sum(fn.coalesce('LoggedCallModelResponse.totalCost', sql`0`)).as("totalCost"), - fn.count("id").as("numQueries"), + fn.sum(fn.coalesce("LoggedCallModelResponse.totalCost", sql`0`)).as("totalCost"), + fn.count("LoggedCall.id").as("numQueries"), ]) .executeTakeFirst(); - console.log("made it 3"); - const errors = await kysely .selectFrom("LoggedCall") .where("organizationId", "=", input.organizationId) @@ -57,14 +52,12 @@ export const dashboardRouter = createTRPCRouter({ "LoggedCall.id", "LoggedCallModelResponse.originalLoggedCallId", ) - .select(({ fn }) => [fn.count("id").as("count"), "respStatus as code"]) + .select(({ fn }) => [fn.count("LoggedCall.id").as("count"), "respStatus as code"]) .where("respStatus", ">", 200) .groupBy("code") .orderBy("count", "desc") .execute(); - console.log("made it 4"); - const namedErrors = errors.map((e) => { if (e.code === 429) { return { ...e, name: "Rate limited" }; @@ -75,10 +68,7 @@ export const dashboardRouter = createTRPCRouter({ } }); - console.log("data is", { periods, totals, errors: namedErrors }); - return { periods, totals, errors: namedErrors }; - // const resp = await kysely.selectFrom("LoggedCall").selectAll().execute(); }), // TODO useInfiniteQuery From 49c68fdbf2ef8b2288abdf1ef57ae4344c60fc11 Mon Sep 17 00:00:00 2001 From: David Corbitt Date: Tue, 8 Aug 2023 12:07:18 -0700 Subject: [PATCH 30/49] Upsert personalOrg when listing orgs --- app/prisma/schema.prisma | 2 +- app/src/pages/project/settings/index.tsx | 46 +++++++++++-------- .../api/routers/organizations.router.ts | 28 ++--------- app/src/server/utils/userOrg.ts | 9 ++++ 4 files changed, 42 insertions(+), 43 deletions(-) diff --git a/app/prisma/schema.prisma b/app/prisma/schema.prisma index 06797f6..60b7d7d 100644 --- a/app/prisma/schema.prisma +++ b/app/prisma/schema.prisma @@ -206,7 +206,7 @@ model Organization { name String @default("Project 1") personalOrgUserId String? @unique @db.Uuid - PersonalOrgUser User? @relation(fields: [personalOrgUserId], references: [id], onDelete: Cascade) + personalOrgUser User? @relation(fields: [personalOrgUserId], references: [id], onDelete: Cascade) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt diff --git a/app/src/pages/project/settings/index.tsx b/app/src/pages/project/settings/index.tsx index cb1139e..18dfe17 100644 --- a/app/src/pages/project/settings/index.tsx +++ b/app/src/pages/project/settings/index.tsx @@ -113,24 +113,34 @@ export default function Settings() { - - Danger Zone - - Permanently delete your project and all of its data. This action cannot be undone. - - - - Delete {selectedOrg?.name} - - + {selectedOrg?.personalOrgUserId ? ( + + Personal Project + + This project is {selectedOrg?.personalOrgUser?.name}'s personal project. It cannot be + deleted. + + + ) : ( + + Danger Zone + + Permanently delete your project and all of its data. This action cannot be undone. + + + + Delete {selectedOrg?.name} + + + )} diff --git a/app/src/server/api/routers/organizations.router.ts b/app/src/server/api/routers/organizations.router.ts index 263c4aa..93cc864 100644 --- a/app/src/server/api/routers/organizations.router.ts +++ b/app/src/server/api/routers/organizations.router.ts @@ -5,6 +5,7 @@ import { z } from "zod"; import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc"; import { prisma } from "~/server/db"; import { generateApiKey } from "~/server/utils/generateApiKey"; +import userOrg from "~/server/utils/userOrg"; import { requireCanModifyOrganization, requireCanViewOrganization, @@ -33,30 +34,8 @@ export const organizationsRouter = createTRPCRouter({ }); if (!organizations.length) { - const newOrgId = uuidv4(); - const [newOrg] = await prisma.$transaction([ - prisma.organization.create({ - data: { - id: newOrgId, - personalOrgUserId: userId, - }, - }), - prisma.organizationUser.create({ - data: { - userId, - organizationId: newOrgId, - role: "ADMIN", - }, - }), - prisma.apiKey.create({ - data: { - name: "Default API Key", - organizationId: newOrgId, - apiKey: generateApiKey(), - }, - }), - ]); - organizations.push(newOrg); + // TODO: We should move this to a separate endpoint that is called on sign up + await userOrg(userId); } return organizations; @@ -70,6 +49,7 @@ export const organizationsRouter = createTRPCRouter({ }, include: { apiKeys: true, + personalOrgUser: true, }, }), prisma.organizationUser.findFirst({ diff --git a/app/src/server/utils/userOrg.ts b/app/src/server/utils/userOrg.ts index a88d8f7..cbbecb1 100644 --- a/app/src/server/utils/userOrg.ts +++ b/app/src/server/utils/userOrg.ts @@ -1,4 +1,5 @@ import { prisma } from "~/server/db"; +import { generateApiKey } from "./generateApiKey"; export default async function userOrg(userId: string) { return await prisma.organization.upsert({ @@ -14,6 +15,14 @@ export default async function userOrg(userId: string) { role: "ADMIN", }, }, + apiKeys: { + create: [ + { + name: "Default API Key", + apiKey: generateApiKey(), + }, + ], + }, }, }); } From 41d06596cbbcd4361136cadac3d36903b00e2a44 Mon Sep 17 00:00:00 2001 From: David Corbitt Date: Tue, 8 Aug 2023 12:09:09 -0700 Subject: [PATCH 31/49] Fix prettier --- app/src/pages/project/settings/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/pages/project/settings/index.tsx b/app/src/pages/project/settings/index.tsx index 18dfe17..1e8a184 100644 --- a/app/src/pages/project/settings/index.tsx +++ b/app/src/pages/project/settings/index.tsx @@ -117,8 +117,8 @@ export default function Settings() { Personal Project - This project is {selectedOrg?.personalOrgUser?.name}'s personal project. It cannot be - deleted. + This project is {selectedOrg?.personalOrgUser?.name}'s personal project. It cannot + be deleted. ) : ( From a2ace63f25c1702d434695bc2a6691b1702cf34f Mon Sep 17 00:00:00 2001 From: David Corbitt Date: Tue, 8 Aug 2023 12:24:14 -0700 Subject: [PATCH 32/49] Hackily fix seeding --- app/src/server/db.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/server/db.ts b/app/src/server/db.ts index 8ed322b..57c433e 100644 --- a/app/src/server/db.ts +++ b/app/src/server/db.ts @@ -23,7 +23,12 @@ import { PrismaClient, } from "@prisma/client"; import { Kysely, PostgresDialect } from "kysely"; -import { Pool } from "pg"; +// TODO: Revert to normal import when our tsconfig.json is fixed +// import { Pool } from "pg"; +import PGModule from "pg"; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const UntypedPool = PGModule.Pool as any; +const Pool = (UntypedPool.default ? UntypedPool.default : UntypedPool) as typeof PGModule.Pool; import { env } from "~/env.mjs"; From a2c322ff43e701861169e3c7a62f48262eba0f29 Mon Sep 17 00:00:00 2001 From: David Corbitt Date: Tue, 8 Aug 2023 13:20:02 -0700 Subject: [PATCH 33/49] Add requireAuth to AppShell --- app/src/components/nav/AppShell.tsx | 23 ++++++++++++++--- app/src/pages/data/index.tsx | 31 +---------------------- app/src/pages/experiments/index.tsx | 39 ++--------------------------- app/src/pages/home/index.tsx | 2 +- 4 files changed, 24 insertions(+), 71 deletions(-) diff --git a/app/src/components/nav/AppShell.tsx b/app/src/components/nav/AppShell.tsx index c577d55..5bb8d22 100644 --- a/app/src/components/nav/AppShell.tsx +++ b/app/src/components/nav/AppShell.tsx @@ -106,7 +106,15 @@ const NavSidebar = () => { ); }; -export default function AppShell(props: { children: React.ReactNode; title?: string }) { +export default function AppShell({ + children, + title, + requireAuth, +}: { + children: React.ReactNode; + title?: string; + requireAuth?: boolean; +}) { const [vh, setVh] = useState("100vh"); // Default height to prevent flicker on initial render useEffect(() => { @@ -126,14 +134,23 @@ export default function AppShell(props: { children: React.ReactNode; title?: str }; }, []); + const user = useSession().data; + const authLoading = useSession().status === "loading"; + + useEffect(() => { + if (requireAuth && user === null && !authLoading) { + signIn("github").catch(console.error); + } + }, [requireAuth, user, authLoading]); + return ( - {props.title ? `${props.title} | OpenPipe` : "OpenPipe"} + {title ? `${title} | OpenPipe` : "OpenPipe"} - {props.children} + {children} ); diff --git a/app/src/pages/data/index.tsx b/app/src/pages/data/index.tsx index bbc99ea..c37de8f 100644 --- a/app/src/pages/data/index.tsx +++ b/app/src/pages/data/index.tsx @@ -4,12 +4,8 @@ import { Breadcrumb, BreadcrumbItem, Flex, - Center, - Text, - Link, } from "@chakra-ui/react"; import AppShell from "~/components/nav/AppShell"; -import { signIn, useSession } from "next-auth/react"; import { RiDatabase2Line } from "react-icons/ri"; import { DatasetCard, @@ -23,33 +19,8 @@ import { useDatasets } from "~/utils/hooks"; export default function DatasetsPage() { const datasets = useDatasets(); - const user = useSession().data; - const authLoading = useSession().status === "loading"; - - if (user === null || authLoading) { - return ( - -
- {!authLoading && ( - - { - signIn("github").catch(console.error); - }} - textDecor="underline" - > - Sign in - {" "} - to view or create new datasets! - - )} -
-
- ); - } - return ( - + diff --git a/app/src/pages/experiments/index.tsx b/app/src/pages/experiments/index.tsx index 666d8e5..6704f17 100644 --- a/app/src/pages/experiments/index.tsx +++ b/app/src/pages/experiments/index.tsx @@ -1,13 +1,4 @@ -import { - SimpleGrid, - Icon, - Breadcrumb, - BreadcrumbItem, - Flex, - Center, - Text, - Link, -} from "@chakra-ui/react"; +import { SimpleGrid, Icon, Breadcrumb, BreadcrumbItem, Flex } from "@chakra-ui/react"; import { RiFlaskLine } from "react-icons/ri"; import AppShell from "~/components/nav/AppShell"; import { @@ -15,7 +6,6 @@ import { ExperimentCardSkeleton, NewExperimentCard, } from "~/components/experiments/ExperimentCard"; -import { signIn, useSession } from "next-auth/react"; import PageHeaderContainer from "~/components/nav/PageHeaderContainer"; import ProjectBreadcrumbContents from "~/components/nav/ProjectBreadcrumbContents"; import { useExperiments } from "~/utils/hooks"; @@ -23,33 +13,8 @@ import { useExperiments } from "~/utils/hooks"; export default function ExperimentsPage() { const experiments = useExperiments(); - const user = useSession().data; - const authLoading = useSession().status === "loading"; - - if (user === null || authLoading) { - return ( - -
- {!authLoading && ( - - { - signIn("github").catch(console.error); - }} - textDecor="underline" - > - Sign in - {" "} - to view or create new experiments! - - )} -
-
- ); - } - return ( - + diff --git a/app/src/pages/home/index.tsx b/app/src/pages/home/index.tsx index 35e2606..60a1a8a 100644 --- a/app/src/pages/home/index.tsx +++ b/app/src/pages/home/index.tsx @@ -58,7 +58,7 @@ export default function HomePage() { }, [stats.data]); return ( - + From cb791e3c738ae56060d9852f4cbc92a8c36c4f16 Mon Sep 17 00:00:00 2001 From: David Corbitt Date: Tue, 8 Aug 2023 13:37:55 -0700 Subject: [PATCH 34/49] Replace home page with logged calls page --- app/@types/nextjs-routes.d.ts | 2 +- app/src/components/nav/AppShell.tsx | 2 ++ .../nav/ProjectBreadcrumbContents.tsx | 33 +++++++++---------- app/src/components/nav/ProjectMenu.tsx | 4 +-- .../projectSettings/DeleteProjectDialog.tsx | 2 +- app/src/pages/index.tsx | 2 +- .../pages/{home => logged-calls}/index.tsx | 4 +-- 7 files changed, 24 insertions(+), 25 deletions(-) rename app/src/pages/{home => logged-calls}/index.tsx (98%) diff --git a/app/@types/nextjs-routes.d.ts b/app/@types/nextjs-routes.d.ts index 5b61bbc..6253ce2 100644 --- a/app/@types/nextjs-routes.d.ts +++ b/app/@types/nextjs-routes.d.ts @@ -22,8 +22,8 @@ declare module "nextjs-routes" { | StaticRoute<"/data"> | DynamicRoute<"/experiments/[id]", { "id": string }> | StaticRoute<"/experiments"> - | StaticRoute<"/home"> | StaticRoute<"/"> + | StaticRoute<"/logged-calls"> | StaticRoute<"/project/settings"> | StaticRoute<"/sentry-example-page"> | StaticRoute<"/world-champs"> diff --git a/app/src/components/nav/AppShell.tsx b/app/src/components/nav/AppShell.tsx index 5bb8d22..2527b9e 100644 --- a/app/src/components/nav/AppShell.tsx +++ b/app/src/components/nav/AppShell.tsx @@ -13,6 +13,7 @@ import { import Head from "next/head"; import Link from "next/link"; import { BsGearFill, BsGithub, BsPersonCircle } from "react-icons/bs"; +import { IoStatsChartOutline } from "react-icons/io5"; import { RiDatabase2Line, RiFlaskLine } from "react-icons/ri"; import { signIn, useSession } from "next-auth/react"; import UserMenu from "./UserMenu"; @@ -51,6 +52,7 @@ const NavSidebar = () => { <> + {env.NEXT_PUBLIC_SHOW_DATA && ( diff --git a/app/src/components/nav/ProjectBreadcrumbContents.tsx b/app/src/components/nav/ProjectBreadcrumbContents.tsx index be3f999..3d642c7 100644 --- a/app/src/components/nav/ProjectBreadcrumbContents.tsx +++ b/app/src/components/nav/ProjectBreadcrumbContents.tsx @@ -1,5 +1,4 @@ import { HStack, Flex, Text } from "@chakra-ui/react"; -import Link from "next/link"; import { useSelectedOrg } from "~/utils/hooks"; // Have to export only contents here instead of full BreadcrumbItem because Chakra doesn't @@ -10,22 +9,20 @@ export default function ProjectBreadcrumbContents({ orgName = "" }: { orgName?: orgName = orgName || selectedOrg?.name || ""; return ( - - - - {orgName[0]?.toUpperCase()} - - - {orgName} - - - + + + {orgName[0]?.toUpperCase()} + + + {orgName} + + ); } diff --git a/app/src/components/nav/ProjectMenu.tsx b/app/src/components/nav/ProjectMenu.tsx index 3a38afb..ccf3d23 100644 --- a/app/src/components/nav/ProjectMenu.tsx +++ b/app/src/components/nav/ProjectMenu.tsx @@ -86,7 +86,7 @@ export default function ProjectMenu() { PROJECT - + { setSelectedOrgId(org.id); onClose(); diff --git a/app/src/components/projectSettings/DeleteProjectDialog.tsx b/app/src/components/projectSettings/DeleteProjectDialog.tsx index 1f586f2..ade3841 100644 --- a/app/src/components/projectSettings/DeleteProjectDialog.tsx +++ b/app/src/components/projectSettings/DeleteProjectDialog.tsx @@ -36,7 +36,7 @@ export const DeleteProjectDialog = ({ if (!selectedOrg.data?.id) return; await deleteMutation.mutateAsync({ id: selectedOrg.data.id }); await utils.organizations.list.invalidate(); - await router.push({ pathname: "/home" }); + await router.push({ pathname: "/logged-calls" }); onClose(); }, [deleteMutation, selectedOrg, router]); diff --git a/app/src/pages/index.tsx b/app/src/pages/index.tsx index 7ddaa6b..e3a84ea 100644 --- a/app/src/pages/index.tsx +++ b/app/src/pages/index.tsx @@ -4,7 +4,7 @@ import { type GetServerSideProps } from "next"; export const getServerSideProps: GetServerSideProps = async () => { return { redirect: { - destination: "/home", + destination: "/logged-calls", permanent: false, }, }; diff --git a/app/src/pages/home/index.tsx b/app/src/pages/logged-calls/index.tsx similarity index 98% rename from app/src/pages/home/index.tsx rename to app/src/pages/logged-calls/index.tsx index 60a1a8a..9969d0f 100644 --- a/app/src/pages/home/index.tsx +++ b/app/src/pages/logged-calls/index.tsx @@ -39,7 +39,7 @@ import dayjs from "~/utils/dayjs"; import { api } from "~/utils/api"; import LoggedCallTable from "~/components/dashboard/LoggedCallTable"; -export default function HomePage() { +export default function LoggedCalls() { const { data: selectedOrg } = useSelectedOrg(); const stats = api.dashboard.stats.useQuery( @@ -65,7 +65,7 @@ export default function HomePage() { - Homepage + Logged Calls From 7a4aa5f0aa0e001a625cffa973abf7e02c9267b2 Mon Sep 17 00:00:00 2001 From: David Corbitt Date: Tue, 8 Aug 2023 13:45:46 -0700 Subject: [PATCH 35/49] Use openpipe optionally in app --- app/src/env.mjs | 2 ++ app/src/server/utils/openai.ts | 8 ++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/app/src/env.mjs b/app/src/env.mjs index bec0e5e..f0f5f3f 100644 --- a/app/src/env.mjs +++ b/app/src/env.mjs @@ -20,6 +20,7 @@ export const env = createEnv({ REPLICATE_API_TOKEN: z.string().default("placeholder"), ANTHROPIC_API_KEY: z.string().default("placeholder"), SENTRY_AUTH_TOKEN: z.string().optional(), + USE_OPENPIPE: z.string().optional(), }, /** @@ -54,6 +55,7 @@ export const env = createEnv({ ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY, NEXT_PUBLIC_SENTRY_DSN: process.env.NEXT_PUBLIC_SENTRY_DSN, SENTRY_AUTH_TOKEN: process.env.SENTRY_AUTH_TOKEN, + USE_OPENPIPE: process.env.USE_OPENPIPE, }, /** * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. diff --git a/app/src/server/utils/openai.ts b/app/src/server/utils/openai.ts index 5397e2c..a18df5e 100644 --- a/app/src/server/utils/openai.ts +++ b/app/src/server/utils/openai.ts @@ -1,7 +1,11 @@ import { env } from "~/env.mjs"; -// import OpenAI from "openai"; +import { default as OriginalOpenAI } from "openai"; import { OpenAI } from "openpipe"; +const openAIConfig = { apiKey: env.OPENAI_API_KEY ?? "dummy-key" }; + // Set a dummy key so it doesn't fail at build time -export const openai = new OpenAI.OpenAI({ apiKey: env.OPENAI_API_KEY ?? "dummy-key" }); +export const openai = env.USE_OPENPIPE + ? new OpenAI.OpenAI(openAIConfig) + : new OriginalOpenAI(openAIConfig); From fd286f68740fc81a49ea87c83aae19b6242d4afd Mon Sep 17 00:00:00 2001 From: David Corbitt Date: Tue, 8 Aug 2023 13:47:54 -0700 Subject: [PATCH 36/49] Fix prettier --- app/src/pages/data/index.tsx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/app/src/pages/data/index.tsx b/app/src/pages/data/index.tsx index c37de8f..fb55389 100644 --- a/app/src/pages/data/index.tsx +++ b/app/src/pages/data/index.tsx @@ -1,10 +1,4 @@ -import { - SimpleGrid, - Icon, - Breadcrumb, - BreadcrumbItem, - Flex, -} from "@chakra-ui/react"; +import { SimpleGrid, Icon, Breadcrumb, BreadcrumbItem, Flex } from "@chakra-ui/react"; import AppShell from "~/components/nav/AppShell"; import { RiDatabase2Line } from "react-icons/ri"; import { From 5dd7e67396693f14eac1bf87255c8b3c1ea8f39c Mon Sep 17 00:00:00 2001 From: David Corbitt Date: Tue, 8 Aug 2023 13:50:57 -0700 Subject: [PATCH 37/49] Make project options full width --- app/src/components/nav/ProjectMenu.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/components/nav/ProjectMenu.tsx b/app/src/components/nav/ProjectMenu.tsx index ccf3d23..aa67a34 100644 --- a/app/src/components/nav/ProjectMenu.tsx +++ b/app/src/components/nav/ProjectMenu.tsx @@ -137,7 +137,7 @@ export default function ProjectMenu() { PROJECTS - + {orgs?.map((org) => ( Date: Tue, 8 Aug 2023 13:57:10 -0700 Subject: [PATCH 38/49] Replace USE_OPENPIPE with OPENPIPE_API_KEY --- app/.env.example | 3 +++ app/src/env.mjs | 4 ++-- app/src/server/utils/openai.ts | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/.env.example b/app/.env.example index 4eaf96d..acdeb20 100644 --- a/app/.env.example +++ b/app/.env.example @@ -31,3 +31,6 @@ NEXT_PUBLIC_HOST="http://localhost:3000" # Next Auth Github Provider GITHUB_CLIENT_ID="your_client_id" GITHUB_CLIENT_SECRET="your_secret" + +OPENPIPE_BASE_URL="http://localhost:3000/api" +OPENPIPE_API_KEY="your_key" diff --git a/app/src/env.mjs b/app/src/env.mjs index f0f5f3f..11b22d5 100644 --- a/app/src/env.mjs +++ b/app/src/env.mjs @@ -20,7 +20,7 @@ export const env = createEnv({ REPLICATE_API_TOKEN: z.string().default("placeholder"), ANTHROPIC_API_KEY: z.string().default("placeholder"), SENTRY_AUTH_TOKEN: z.string().optional(), - USE_OPENPIPE: z.string().optional(), + OPENPIPE_API_KEY: z.string().optional(), }, /** @@ -55,7 +55,7 @@ export const env = createEnv({ ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY, NEXT_PUBLIC_SENTRY_DSN: process.env.NEXT_PUBLIC_SENTRY_DSN, SENTRY_AUTH_TOKEN: process.env.SENTRY_AUTH_TOKEN, - USE_OPENPIPE: process.env.USE_OPENPIPE, + OPENPIPE_API_KEY: process.env.OPENPIPE_API_KEY, }, /** * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. diff --git a/app/src/server/utils/openai.ts b/app/src/server/utils/openai.ts index a18df5e..b29700b 100644 --- a/app/src/server/utils/openai.ts +++ b/app/src/server/utils/openai.ts @@ -6,6 +6,6 @@ import { OpenAI } from "openpipe"; const openAIConfig = { apiKey: env.OPENAI_API_KEY ?? "dummy-key" }; // Set a dummy key so it doesn't fail at build time -export const openai = env.USE_OPENPIPE +export const openai = env.OPENPIPE_API_KEY ? new OpenAI.OpenAI(openAIConfig) : new OriginalOpenAI(openAIConfig); From ca33bb0b08ffe778462736c9801e22ab1c1f6624 Mon Sep 17 00:00:00 2001 From: David Corbitt Date: Tue, 8 Aug 2023 14:27:14 -0700 Subject: [PATCH 39/49] Add beta to logged calls --- app/src/components/nav/AppShell.tsx | 2 +- app/src/components/nav/IconLink.tsx | 22 +++++++++++++++------- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/app/src/components/nav/AppShell.tsx b/app/src/components/nav/AppShell.tsx index 2527b9e..cd56de2 100644 --- a/app/src/components/nav/AppShell.tsx +++ b/app/src/components/nav/AppShell.tsx @@ -52,7 +52,7 @@ const NavSidebar = () => { <> - + {env.NEXT_PUBLIC_SHOW_DATA && ( diff --git a/app/src/components/nav/IconLink.tsx b/app/src/components/nav/IconLink.tsx index 8ae56f8..6613392 100644 --- a/app/src/components/nav/IconLink.tsx +++ b/app/src/components/nav/IconLink.tsx @@ -3,17 +3,25 @@ import Link, { type LinkProps } from "next/link"; import { type IconType } from "react-icons"; import NavSidebarOption from "./NavSidebarOption"; -type IconLinkProps = BoxProps & LinkProps & { label?: string; icon: IconType; href: string }; +type IconLinkProps = BoxProps & + LinkProps & { label?: string; icon: IconType; href: string; beta?: boolean }; -const IconLink = ({ icon, label, href, color, ...props }: IconLinkProps) => { +const IconLink = ({ icon, label, href, color, beta, ...props }: IconLinkProps) => { return ( - - - - {label} - + + + + + {label} + + + {beta && ( + + BETA + + )} From 2861c64428261f5ed784a205d380891e1275a1b4 Mon Sep 17 00:00:00 2001 From: David Corbitt Date: Tue, 8 Aug 2023 14:31:08 -0700 Subject: [PATCH 40/49] Fix prettier --- app/src/components/nav/AppShell.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/components/nav/AppShell.tsx b/app/src/components/nav/AppShell.tsx index cd56de2..db29c73 100644 --- a/app/src/components/nav/AppShell.tsx +++ b/app/src/components/nav/AppShell.tsx @@ -52,7 +52,7 @@ const NavSidebar = () => { <> - + {env.NEXT_PUBLIC_SHOW_DATA && ( From 65a0f9065f9fa4888e20fae3075ff5d53d0baf7c Mon Sep 17 00:00:00 2001 From: David Corbitt Date: Tue, 8 Aug 2023 15:26:14 -0700 Subject: [PATCH 41/49] Backfill usage statistics --- app/src/pages/logged-calls/index.tsx | 4 ++-- .../server/api/routers/dashboard.router.ts | 23 ++++++++++++++++++- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/app/src/pages/logged-calls/index.tsx b/app/src/pages/logged-calls/index.tsx index 9969d0f..ca671db 100644 --- a/app/src/pages/logged-calls/index.tsx +++ b/app/src/pages/logged-calls/index.tsx @@ -105,14 +105,14 @@ export default function LoggedCalls() { stroke="#8884d8" yAxisId="left" dot={false} - strokeWidth={3} + strokeWidth={2} /> diff --git a/app/src/server/api/routers/dashboard.router.ts b/app/src/server/api/routers/dashboard.router.ts index ba5d5b6..b31e365 100644 --- a/app/src/server/api/routers/dashboard.router.ts +++ b/app/src/server/api/routers/dashboard.router.ts @@ -2,6 +2,7 @@ import { sql } from "kysely"; import { z } from "zod"; import { createTRPCRouter, publicProcedure } from "~/server/api/trpc"; import { kysely, prisma } from "~/server/db"; +import dayjs from "~/utils/dayjs"; export const dashboardRouter = createTRPCRouter({ stats: publicProcedure @@ -30,6 +31,26 @@ export const dashboardRouter = createTRPCRouter({ .orderBy("period") .execute(); + let originalDataIndex = periods.length - 1; + let dayToMatch = dayjs(input.startDate).startOf("day"); + const backfilledPeriods: typeof periods = []; + + // Backfill from now to 14 days ago or the date of the first logged call, whichever is earlier + while (backfilledPeriods.length < 14 || originalDataIndex >= 0) { + const nextOriginalPeriod = periods[originalDataIndex]; + if (nextOriginalPeriod && dayjs(nextOriginalPeriod?.period).isSame(dayToMatch, "day")) { + backfilledPeriods.unshift(nextOriginalPeriod); + originalDataIndex--; + } else { + backfilledPeriods.unshift({ + period: dayjs(dayToMatch).toDate(), + numQueries: 0, + totalCost: 0, + }); + } + dayToMatch = dayToMatch.subtract(1, "day"); + } + const totals = await kysely .selectFrom("LoggedCall") .leftJoin( @@ -68,7 +89,7 @@ export const dashboardRouter = createTRPCRouter({ } }); - return { periods, totals, errors: namedErrors }; + return { periods: backfilledPeriods, totals, errors: namedErrors }; }), // TODO useInfiniteQuery From ded86cba0829122829df2ea4eae45fd9ab257fed Mon Sep 17 00:00:00 2001 From: David Corbitt Date: Tue, 8 Aug 2023 16:39:42 -0700 Subject: [PATCH 42/49] Add dashboard seed command --- app/prisma/seedDashboard.ts | 394 ++++++++++++++++++++++++++++++++++++ 1 file changed, 394 insertions(+) create mode 100644 app/prisma/seedDashboard.ts diff --git a/app/prisma/seedDashboard.ts b/app/prisma/seedDashboard.ts new file mode 100644 index 0000000..bbbd48f --- /dev/null +++ b/app/prisma/seedDashboard.ts @@ -0,0 +1,394 @@ +import { v4 as uuidv4 } from "uuid"; +import { type Prisma } from "@prisma/client"; + +import { prisma } from "~/server/db"; +import { hashRequest } from "~/server/utils/hashObject"; +import { type JsonValue } from "type-fest"; + +const MODEL_RESPONSE_TEMPLATES: { + reqPayload: any; + respPayload: any; + respStatus: number; + error: string | null; + inputTokens: number; + outputTokens: number; + finishReason: string; +}[] = [ + { + reqPayload: { + model: "gpt-3.5-turbo-0613", + messages: [ + { + role: "system", + content: + "The user is testing multiple scenarios against the same prompt. Attempt to generate a new scenario that is different from the others.", + }, + { + role: "user", + content: + 'Prompt constructor function:\n---\n/**\n * Use Javascript to define an OpenAI chat completion\n * (https://platform.openai.com/docs/api-reference/chat/create).\n *\n * You have access to the current scenario in the `scenario`\n * variable.\n */\n\ndefinePrompt("openai/ChatCompletion", {\n model: "gpt-3.5-turbo-0613",\n stream: true,\n messages: [\n {\n role: "system",\n content: `Write \'Start experimenting!\' in ${scenario.language}`,\n },\n ],\n});', + }, + { + role: "assistant", + content: null, + function_call: { + name: "add_scenario", + arguments: '{"language":"French"}', + }, + }, + { + role: "assistant", + content: null, + function_call: { + name: "add_scenario", + arguments: '{"language":"English"}', + }, + }, + { + role: "assistant", + content: null, + function_call: { + name: "add_scenario", + arguments: '{"language":"Spanish"}', + }, + }, + { + role: "assistant", + content: null, + function_call: { + name: "add_scenario", + arguments: '{"language":"German"}', + }, + }, + ], + functions: [ + { + name: "add_scenario", + parameters: { + type: "object", + properties: { + language: { type: "string" }, + }, + }, + }, + ], + temperature: 0.5, + function_call: { + name: "add_scenario", + }, + }, + respStatus: 200, + respPayload: { + id: "chatcmpl-7lNspqePJWVyXwXebupxb1eMozo6Q", + model: "gpt-3.5-turbo-0613", + usage: { + total_tokens: 241, + prompt_tokens: 236, + completion_tokens: 5, + }, + object: "chat.completion", + choices: [ + { + index: 0, + message: { + role: "assistant", + content: null, + function_call: { + name: "add_scenario", + arguments: '{"language":"Italian"}', + }, + }, + finish_reason: "stop", + }, + ], + created: 1691527579, + }, + error: null, + inputTokens: 236, + outputTokens: 5, + finishReason: "stop", + }, + { + reqPayload: { + model: "gpt-3.5-turbo-0613", + messages: [ + { + role: "system", + content: + "The user is testing multiple scenarios against the same prompt. Attempt to generate a new scenario that is different from the others.", + }, + { + role: "user", + content: + 'Prompt constructor function:\n---\n/**\n * Use Javascript to define an OpenAI chat completion\n * (https://platform.openai.com/docs/api-reference/chat/create).\n *\n * You have access to the current scenario in the `scenario`\n * variable.\n */\n\ndefinePrompt("openai/ChatCompletion", {\n model: "gpt-3.5-turbo-0613",\n stream: true,\n messages: [\n {\n role: "system",\n content: `Write \'Start experimenting!\' in ${scenario.language}`,\n },\n ],\n});', + }, + { + role: "assistant", + content: null, + function_call: { + name: "add_scenario", + arguments: '{"language":"English"}', + }, + }, + { + role: "assistant", + content: null, + function_call: { + name: "add_scenario", + arguments: '{"language":"Spanish"}', + }, + }, + { + role: "assistant", + content: null, + function_call: { + name: "add_scenario", + arguments: '{"language":"German"}', + }, + }, + ], + functions: [ + { + name: "add_scenario", + parameters: { + type: "object", + properties: { + language: { type: "string" }, + }, + }, + }, + ], + temperature: 0.5, + function_call: { + name: "add_scenario", + }, + }, + respStatus: 200, + respPayload: { + id: "chatcmpl-7lNifmc5AncyAvleZRDBhAcLFYBIT", + model: "gpt-3.5-turbo-0613", + usage: { + total_tokens: 227, + prompt_tokens: 222, + completion_tokens: 5, + }, + object: "chat.completion", + choices: [ + { + index: 0, + message: { + role: "assistant", + content: null, + function_call: { + name: "add_scenario", + arguments: '{"language":"French"}', + }, + }, + finish_reason: "stop", + }, + ], + created: 1691526949, + }, + error: null, + inputTokens: 222, + outputTokens: 5, + finishReason: "stop", + }, + { + reqPayload: { + model: "gpt-3.5-turbo-0613", + stream: false, + messages: [ + { + role: "system", + content: "Write 'Start experimenting!' in German", + }, + ], + }, + respStatus: 200, + respPayload: { + id: "chatcmpl-7lNh1TtrsJVgz3Nj70bKkZZk7xPi7", + model: "gpt-3.5-turbo-0613", + usage: { + total_tokens: 21, + prompt_tokens: 14, + completion_tokens: 7, + }, + object: "chat.completion", + choices: [ + { + index: 0, + message: { + role: "assistant", + content: "Fang an zu experimentieren!", + }, + finish_reason: "stop", + }, + ], + created: 1691526847, + }, + error: null, + inputTokens: 14, + outputTokens: 7, + finishReason: "stop", + }, + { + reqPayload: { + model: "gpt-4", + messages: [ + { + role: "system", + content: + 'Your job is to update prompt constructor functions. Here is the api shape for the current model:\n---\n{\n "type": "object",\n "properties": {\n "model": {\n "description": "ID of the model to use. See the [model endpoint compatibility](/docs/models/model-endpoint-compatibility) table for details on which models work with the Chat API.",\n "example": "gpt-3.5-turbo",\n "type": "string",\n "enum": [\n "gpt-4",\n "gpt-4-0613",\n "gpt-4-32k",\n "gpt-4-32k-0613",\n "gpt-3.5-turbo",\n "gpt-3.5-turbo-16k",\n "gpt-3.5-turbo-0613",\n "gpt-3.5-turbo-16k-0613"\n ]\n },\n "messages": {\n "description": "A list of messages comprising the conversation so far. [Example Python code](https://github.com/openai/openai-cookbook/blob/main/examples/How_to_format_inputs_to_ChatGPT_models.ipynb).",\n "type": "array",\n "minItems": 1,\n "items": {\n "type": "object",\n "properties": {\n "role": {\n "type": "string",\n "enum": [\n "system",\n "user",\n "assistant",\n "function"\n ],\n "description": "The role of the messages author. One of `system`, `user`, `assistant`, or `function`."\n },\n "content": {\n "type": "string",\n "description": "The contents of the message. `content` is required for all messages except assistant messages with function calls."\n },\n "name": {\n "type": "string",\n "description": "The name of the author of this message. `name` is required if role is `function`, and it should be the name of the function whose response is in the `content`. May contain a-z, A-Z, 0-9, and underscores, with a maximum length of 64 characters."\n },\n "function_call": {\n "type": "object",\n "description": "The name and arguments of a function that should be called, as generated by the model.",\n "properties": {\n "name": {\n "type": "string",\n "description": "The name of the function to call."\n },\n "arguments": {\n "type": "string",\n "description": "The arguments to call the function with, as generated by the model in JSON format. Note that the model does not always generate valid JSON, and may hallucinate parameters not defined by your function schema. Validate the arguments in your code before calling your function."\n }\n }\n }\n },\n "required": [\n "role"\n ]\n }\n },\n "functions": {\n "description": "A list of functions the model may generate JSON inputs for.",\n "type": "array",\n "minItems": 1,\n "items": {\n "type": "object",\n "properties": {\n "name": {\n "type": "string",\n "description": "The name of the function to be called. Must be a-z, A-Z, 0-9, or contain underscores and dashes, with a maximum length of 64."\n },\n "description": {\n "type": "string",\n "description": "The description of what the function does."\n },\n "parameters": {\n "type": "object",\n "description": "The parameters the functions accepts, described as a JSON Schema object. See the [guide](/docs/guides/gpt/function-calling) for examples, and the [JSON Schema reference](https://json-schema.org/understanding-json-schema/) for documentation about the format.",\n "additionalProperties": true\n }\n },\n "required": [\n "name"\n ]\n }\n },\n "function_call": {\n "description": "Controls how the model responds to function calls. \\"none\\" means the model does not call a function, and responds to the end-user. \\"auto\\" means the model can pick between an end-user or calling a function. Specifying a particular function via `{\\"name\\":\\\\ \\"my_function\\"}` forces the model to call that function. \\"none\\" is the default when no functions are present. \\"auto\\" is the default if functions are present.",\n "oneOf": [\n {\n "type": "string",\n "enum": [\n "none",\n "auto"\n ]\n },\n {\n "type": "object",\n "properties": {\n "name": {\n "type": "string",\n "description": "The name of the function to call."\n }\n },\n "required": [\n "name"\n ]\n }\n ]\n },\n "temperature": {\n "type": "number",\n "minimum": 0,\n "maximum": 2,\n "default": 1,\n "example": 1,\n "nullable": true,\n "description": "What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic.\\n\\nWe generally recommend altering this or `top_p` but not both.\\n"\n },\n "top_p": {\n "type": "number",\n "minimum": 0,\n "maximum": 1,\n "default": 1,\n "example": 1,\n "nullable": true,\n "description": "An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered.\\n\\nWe generally recommend altering this or `temperature` but not both.\\n"\n },\n "n": {\n "type": "integer",\n "minimum": 1,\n "maximum": 128,\n "default": 1,\n "example": 1,\n "nullable": true,\n "description": "How many chat completion choices to generate for each input message."\n },\n "stream": {\n "description": "If set, partial message deltas will be sent, like in ChatGPT. Tokens will be sent as data-only [server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Event_stream_format) as they become available, with the stream terminated by a `data: [DONE]` message. [Example Python code](https://github.com/openai/openai-cookbook/blob/main/examples/How_to_stream_completions.ipynb).\\n",\n "type": "boolean",\n "nullable": true,\n "default": false\n },\n "stop": {\n "description": "Up to 4 sequences where the API will stop generating further tokens.\\n",\n "default": null,\n "oneOf": [\n {\n "type": "string",\n "nullable": true\n },\n {\n "type": "array",\n "minItems": 1,\n "maxItems": 4,\n "items": {\n "type": "string"\n }\n }\n ]\n },\n "max_tokens": {\n "description": "The maximum number of [tokens](/tokenizer) to generate in the chat completion.\\n\\nThe total length of input tokens and generated tokens is limited by the model\'s context length. [Example Python code](https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb) for counting tokens.\\n",\n "type": "integer"\n },\n "presence_penalty": {\n "type": "number",\n "default": 0,\n "minimum": -2,\n "maximum": 2,\n "nullable": true,\n "description": "Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, increasing the model\'s likelihood to talk about new topics.\\n\\n[See more information about frequency and presence penalties.](/docs/api-reference/parameter-details)\\n"\n },\n "frequency_penalty": {\n "type": "number",\n "default": 0,\n "minimum": -2,\n "maximum": 2,\n "nullable": true,\n "description": "Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model\'s likelihood to repeat the same line verbatim.\\n\\n[See more information about frequency and presence penalties.](/docs/api-reference/parameter-details)\\n"\n },\n "logit_bias": {\n "type": "object",\n "x-oaiTypeLabel": "map",\n "default": null,\n "nullable": true,\n "description": "Modify the likelihood of specified tokens appearing in the completion.\\n\\nAccepts a json object that maps tokens (specified by their token ID in the tokenizer) to an associated bias value from -100 to 100. Mathematically, the bias is added to the logits generated by the model prior to sampling. The exact effect will vary per model, but values between -1 and 1 should decrease or increase likelihood of selection; values like -100 or 100 should result in a ban or exclusive selection of the relevant token.\\n"\n },\n "user": {\n "type": "string",\n "example": "user-1234",\n "description": "A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. [Learn more](/docs/guides/safety-best-practices/end-user-ids).\\n"\n }\n },\n "required": [\n "model",\n "messages"\n ]\n}\n\nDo not add any assistant messages.', + }, + { + role: "user", + content: + 'This is the current prompt constructor function:\n---\n/**\n * Use Javascript to define an OpenAI chat completion\n * (https://platform.openai.com/docs/api-reference/chat/create).\n *\n * You have access to the current scenario in the `scenario`\n * variable.\n */\n\ndefinePrompt("openai/ChatCompletion", {\n model: "gpt-3.5-turbo-0613",\n stream: true,\n messages: [\n {\n role: "system",\n content: `Write \'Start experimenting!\' in ${scenario.language}`,\n },\n ],\n});', + }, + { + role: "user", + content: + "Return the prompt constructor function for LLama 2 7B Chat given the existing prompt constructor function for GPT-3.5 Turbo", + }, + { + role: "user", + content: + 'As seen in the first argument to definePrompt, the old provider endpoint was "openai/ChatCompletion". The new provider endpoint is "replicate/llama2". Here is the schema for the new model:\n---\n{\n "type": "object",\n "properties": {\n "model": {\n "type": "string",\n "enum": [\n "7b-chat",\n "13b-chat",\n "70b-chat"\n ]\n },\n "system_prompt": {\n "type": "string",\n "description": "System prompt to send to Llama v2. This is prepended to the prompt and helps guide system behavior."\n },\n "prompt": {\n "type": "string",\n "description": "Prompt to send to Llama v2."\n },\n "max_new_tokens": {\n "type": "number",\n "description": "Maximum number of tokens to generate. A word is generally 2-3 tokens (minimum: 1)"\n },\n "temperature": {\n "type": "number",\n "description": "Adjusts randomness of outputs, 0.1 is a good starting value. (minimum: 0.01; maximum: 5)"\n },\n "top_p": {\n "type": "number",\n "description": "When decoding text, samples from the top p percentage of most likely tokens; lower to ignore less likely tokens (minimum: 0.01; maximum: 1)"\n },\n "repetition_penalty": {\n "type": "number",\n "description": "Penalty for repeated words in generated text; 1 is no penalty, values greater than 1 discourage repetition, less than 1 encourage it. (minimum: 0.01; maximum: 5)"\n },\n "debug": {\n "type": "boolean",\n "description": "provide debugging output in logs"\n }\n },\n "required": [\n "model",\n "prompt"\n ]\n}', + }, + ], + functions: [ + { + name: "update_prompt_constructor_function", + parameters: { + type: "object", + properties: { + new_prompt_function: { + type: "string", + description: "The new prompt function, runnable in typescript", + }, + }, + }, + }, + ], + function_call: { + name: "update_prompt_constructor_function", + }, + }, + respStatus: 200, + respPayload: { + id: "chatcmpl-7lQS3MktOT8BTgNEytl9dkyssCQqL", + model: "gpt-4-0613", + usage: { + total_tokens: 2910, + prompt_tokens: 2802, + completion_tokens: 108, + }, + object: "chat.completion", + choices: [ + { + index: 0, + message: { + role: "assistant", + content: null, + function_call: { + name: "update_prompt_constructor_function", + arguments: + '{\n "new_prompt_function": "definePrompt(\\"replicate/llama2\\", {\\n model: \\"7b-chat\\",\\n system_prompt: `Write \'Start experimenting!\' in ${scenario.language}`,\\n prompt: `Experiment with the Llama v2 model`,\\n max_new_tokens: 100,\\n temperature: 0.1,\\n top_p: 0.95,\\n repetition_penalty: 1.2,\\n debug: true,\\n});"\n}', + }, + }, + finish_reason: "stop", + }, + ], + created: 1691537451, + }, + error: null, + inputTokens: 2802, + outputTokens: 108, + finishReason: "stop", + }, +]; + +await prisma.loggedCallModelResponse.deleteMany(); + +const org = await prisma.organization.findFirst({ + where: { + personalOrgUserId: { + not: null, + }, + }, + orderBy: { + createdAt: "asc", + }, +}); + +if (!org) { + console.error("No org found. Sign up to create your first org."); + process.exit(1); +} + +const loggedCallsToCreate: Prisma.LoggedCallCreateManyInput[] = []; +const loggedCallModelResponsesToCreate: Prisma.LoggedCallModelResponseCreateManyInput[] = []; +const loggedCallsToUpdate: Prisma.LoggedCallUpdateArgs[] = []; +const loggedCallTagsToCreate: Prisma.LoggedCallTagCreateManyInput[] = []; +for (let i = 0; i < 100; i++) { + const loggedCallId = uuidv4(); + const loggedCallModelResponseId = uuidv4(); + // choose random time in the last two weeks, with a bias towards the last few days + const startTime = new Date(Date.now() - Math.random() * 1000 * 60 * 60 * 24 * 14); + // choose random time anywhere from 1 to 25 seconds later + const endTime = new Date(startTime.getTime() + Math.random() * 1000 * 24); + loggedCallsToCreate.push({ + id: loggedCallId, + cacheHit: false, + startTime, + organizationId: org.id, + createdAt: startTime, + }); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const template = + MODEL_RESPONSE_TEMPLATES[Math.floor(Math.random() * MODEL_RESPONSE_TEMPLATES.length)]!; + loggedCallModelResponsesToCreate.push({ + id: loggedCallModelResponseId, + startTime, + endTime, + originalLoggedCallId: loggedCallId, + reqPayload: template.reqPayload, + respPayload: template.respPayload, + respStatus: template.respStatus, + error: template.error, + createdAt: startTime, + cacheKey: hashRequest(org.id, template.reqPayload as JsonValue), + durationMs: endTime.getTime() - startTime.getTime(), + inputTokens: template.inputTokens, + outputTokens: template.outputTokens, + finishReason: template.finishReason, + totalCost: template.inputTokens * 0.06 + template.outputTokens * 0.06, + }); + loggedCallsToUpdate.push({ + where: { + id: loggedCallId, + }, + data: { + modelResponseId: loggedCallModelResponseId, + }, + }); + loggedCallTagsToCreate.push({ + loggedCallId, + name: "$model", + value: template.reqPayload.model, + }); +} + +await prisma.$transaction([ + prisma.loggedCall.createMany({ + data: loggedCallsToCreate, + }), + prisma.loggedCallModelResponse.createMany({ + data: loggedCallModelResponsesToCreate, + }), + ...loggedCallsToUpdate.map((update) => prisma.loggedCall.update(update)), + prisma.loggedCallTag.createMany({ + data: loggedCallTagsToCreate, + }), +]); From 3424aa36ba1e99d7b35cec9ec077636a7276d90d Mon Sep 17 00:00:00 2001 From: David Corbitt Date: Tue, 8 Aug 2023 17:20:20 -0700 Subject: [PATCH 43/49] Automatically push personalOrg into list --- app/src/server/api/routers/organizations.router.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/server/api/routers/organizations.router.ts b/app/src/server/api/routers/organizations.router.ts index 93cc864..141aab2 100644 --- a/app/src/server/api/routers/organizations.router.ts +++ b/app/src/server/api/routers/organizations.router.ts @@ -35,7 +35,8 @@ export const organizationsRouter = createTRPCRouter({ if (!organizations.length) { // TODO: We should move this to a separate endpoint that is called on sign up - await userOrg(userId); + const personalOrg = await userOrg(userId); + organizations.push(personalOrg); } return organizations; From 760bfbbe32d76ba83c56c847dd58446d8ed0d2e9 Mon Sep 17 00:00:00 2001 From: David Corbitt Date: Tue, 8 Aug 2023 18:15:42 -0700 Subject: [PATCH 44/49] Fix issue with timezones --- app/prisma/seedDashboard.ts | 32 ++++++++++++++----- .../server/api/routers/dashboard.router.ts | 8 +++-- app/src/server/utils/openai.ts | 10 +++--- app/src/utils/dayjs.ts | 2 ++ 4 files changed, 38 insertions(+), 14 deletions(-) diff --git a/app/prisma/seedDashboard.ts b/app/prisma/seedDashboard.ts index bbbd48f..88d2ad3 100644 --- a/app/prisma/seedDashboard.ts +++ b/app/prisma/seedDashboard.ts @@ -331,13 +331,19 @@ const loggedCallsToCreate: Prisma.LoggedCallCreateManyInput[] = []; const loggedCallModelResponsesToCreate: Prisma.LoggedCallModelResponseCreateManyInput[] = []; const loggedCallsToUpdate: Prisma.LoggedCallUpdateArgs[] = []; const loggedCallTagsToCreate: Prisma.LoggedCallTagCreateManyInput[] = []; -for (let i = 0; i < 100; i++) { +for (let i = 0; i < 437; i++) { const loggedCallId = uuidv4(); const loggedCallModelResponseId = uuidv4(); + const template = + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + MODEL_RESPONSE_TEMPLATES[Math.floor(Math.random() * MODEL_RESPONSE_TEMPLATES.length)]!; + const model = template.reqPayload.model; // choose random time in the last two weeks, with a bias towards the last few days - const startTime = new Date(Date.now() - Math.random() * 1000 * 60 * 60 * 24 * 14); - // choose random time anywhere from 1 to 25 seconds later - const endTime = new Date(startTime.getTime() + Math.random() * 1000 * 24); + const startTime = new Date(Date.now() - Math.pow(Math.random(), 2) * 1000 * 60 * 60 * 24 * 14); + // choose random delay anywhere from 2 to 10 seconds later for gpt-4, or 1 to 5 seconds for gpt-3.5 + const delay = + model === "gpt-4" ? 1000 * 2 + Math.random() * 1000 * 8 : 1000 + Math.random() * 1000 * 4; + const endTime = new Date(startTime.getTime() + delay); loggedCallsToCreate.push({ id: loggedCallId, cacheHit: false, @@ -345,9 +351,18 @@ for (let i = 0; i < 100; i++) { organizationId: org.id, createdAt: startTime, }); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const template = - MODEL_RESPONSE_TEMPLATES[Math.floor(Math.random() * MODEL_RESPONSE_TEMPLATES.length)]!; + + const { promptTokenPrice, completionTokenPrice } = + model === "gpt-4" + ? { + promptTokenPrice: 0.00003, + completionTokenPrice: 0.00006, + } + : { + promptTokenPrice: 0.0000015, + completionTokenPrice: 0.000002, + }; + loggedCallModelResponsesToCreate.push({ id: loggedCallModelResponseId, startTime, @@ -363,7 +378,8 @@ for (let i = 0; i < 100; i++) { inputTokens: template.inputTokens, outputTokens: template.outputTokens, finishReason: template.finishReason, - totalCost: template.inputTokens * 0.06 + template.outputTokens * 0.06, + totalCost: + template.inputTokens * promptTokenPrice + template.outputTokens * completionTokenPrice, }); loggedCallsToUpdate.push({ where: { diff --git a/app/src/server/api/routers/dashboard.router.ts b/app/src/server/api/routers/dashboard.router.ts index b31e365..b18e1c0 100644 --- a/app/src/server/api/routers/dashboard.router.ts +++ b/app/src/server/api/routers/dashboard.router.ts @@ -32,11 +32,15 @@ export const dashboardRouter = createTRPCRouter({ .execute(); let originalDataIndex = periods.length - 1; - let dayToMatch = dayjs(input.startDate).startOf("day"); + // *SLAMS DOWN GLASS OF WHISKEY* timezones, amirite? + let dayToMatch = dayjs(input.startDate || new Date()).add(1, "day"); const backfilledPeriods: typeof periods = []; // Backfill from now to 14 days ago or the date of the first logged call, whichever is earlier - while (backfilledPeriods.length < 14 || originalDataIndex >= 0) { + while ( + backfilledPeriods.length < 14 || + (periods[0]?.period && !dayToMatch.isBefore(periods[0]?.period, "day")) + ) { const nextOriginalPeriod = periods[originalDataIndex]; if (nextOriginalPeriod && dayjs(nextOriginalPeriod?.period).isSame(dayToMatch, "day")) { backfilledPeriods.unshift(nextOriginalPeriod); diff --git a/app/src/server/utils/openai.ts b/app/src/server/utils/openai.ts index b29700b..64ac3d8 100644 --- a/app/src/server/utils/openai.ts +++ b/app/src/server/utils/openai.ts @@ -1,11 +1,13 @@ import { env } from "~/env.mjs"; import { default as OriginalOpenAI } from "openai"; -import { OpenAI } from "openpipe"; +// import { OpenAI } from "openpipe"; const openAIConfig = { apiKey: env.OPENAI_API_KEY ?? "dummy-key" }; // Set a dummy key so it doesn't fail at build time -export const openai = env.OPENPIPE_API_KEY - ? new OpenAI.OpenAI(openAIConfig) - : new OriginalOpenAI(openAIConfig); +// export const openai = env.OPENPIPE_API_KEY +// ? new OpenAI.OpenAI(openAIConfig) +// : new OriginalOpenAI(openAIConfig); + +export const openai = new OriginalOpenAI(openAIConfig); diff --git a/app/src/utils/dayjs.ts b/app/src/utils/dayjs.ts index 1d84bff..9656a25 100644 --- a/app/src/utils/dayjs.ts +++ b/app/src/utils/dayjs.ts @@ -1,9 +1,11 @@ import dayjs from "dayjs"; import duration from "dayjs/plugin/duration"; import relativeTime from "dayjs/plugin/relativeTime"; +import timezone from "dayjs/plugin/timezone"; dayjs.extend(duration); dayjs.extend(relativeTime); +dayjs.extend(timezone); export const formatTimePast = (date: Date) => dayjs.duration(dayjs(date).diff(dayjs())).humanize(true); From 1b2b6c145655d607c873e4aa2cd3a8f6d1c8d9b7 Mon Sep 17 00:00:00 2001 From: David Corbitt Date: Tue, 8 Aug 2023 18:19:59 -0700 Subject: [PATCH 45/49] Send organizationId in fork mutation --- .../ExperimentHeaderButtons/useOnForkButtonPressed.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/src/components/experiments/ExperimentHeaderButtons/useOnForkButtonPressed.tsx b/app/src/components/experiments/ExperimentHeaderButtons/useOnForkButtonPressed.tsx index 9fbca82..62f99de 100644 --- a/app/src/components/experiments/ExperimentHeaderButtons/useOnForkButtonPressed.tsx +++ b/app/src/components/experiments/ExperimentHeaderButtons/useOnForkButtonPressed.tsx @@ -3,18 +3,23 @@ import { api } from "~/utils/api"; import { useExperiment, useHandledAsyncCallback } from "~/utils/hooks"; import { signIn, useSession } from "next-auth/react"; import { useRouter } from "next/router"; +import { useAppStore } from "~/state/store"; export const useOnForkButtonPressed = () => { const router = useRouter(); const user = useSession().data; const experiment = useExperiment(); + const selectedOrgId = useAppStore((state) => state.selectedOrgId); const forkMutation = api.experiments.fork.useMutation(); const [onFork, isForking] = useHandledAsyncCallback(async () => { - if (!experiment.data?.id) return; - const forkedExperimentId = await forkMutation.mutateAsync({ id: experiment.data.id }); + if (!experiment.data?.id || !selectedOrgId) return; + const forkedExperimentId = await forkMutation.mutateAsync({ + id: experiment.data.id, + organizationId: selectedOrgId, + }); await router.push({ pathname: "/experiments/[id]", query: { id: forkedExperimentId } }); }, [forkMutation, experiment.data?.id, router]); From 7bb414026e9922107c851ca6d5ce6dbe9752c7ae Mon Sep 17 00:00:00 2001 From: David Corbitt Date: Tue, 8 Aug 2023 18:30:02 -0700 Subject: [PATCH 46/49] Fix typescript error --- app/src/server/scripts/test-queries.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/server/scripts/test-queries.ts b/app/src/server/scripts/test-queries.ts index e69d230..aa80bd7 100644 --- a/app/src/server/scripts/test-queries.ts +++ b/app/src/server/scripts/test-queries.ts @@ -51,7 +51,7 @@ const totalSpent = await prisma.loggedCallModelResponse.aggregate({ totalCost: true, }, where: { - createdBy: { + originalLoggedCall: { organizationId: projectId, }, startTime: { From c62ced867aec7d29175290dd0c25da3ea122e357 Mon Sep 17 00:00:00 2001 From: David Corbitt Date: Wed, 9 Aug 2023 01:21:42 -0700 Subject: [PATCH 47/49] Increase dashboard seeding number --- app/prisma/seedDashboard.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/prisma/seedDashboard.ts b/app/prisma/seedDashboard.ts index 88d2ad3..ff7de60 100644 --- a/app/prisma/seedDashboard.ts +++ b/app/prisma/seedDashboard.ts @@ -331,7 +331,7 @@ const loggedCallsToCreate: Prisma.LoggedCallCreateManyInput[] = []; const loggedCallModelResponsesToCreate: Prisma.LoggedCallModelResponseCreateManyInput[] = []; const loggedCallsToUpdate: Prisma.LoggedCallUpdateArgs[] = []; const loggedCallTagsToCreate: Prisma.LoggedCallTagCreateManyInput[] = []; -for (let i = 0; i < 437; i++) { +for (let i = 0; i < 1437; i++) { const loggedCallId = uuidv4(); const loggedCallModelResponseId = uuidv4(); const template = From 4feb3e5829ea38e67123a48a3b6894b8bc4a230f Mon Sep 17 00:00:00 2001 From: David Corbitt Date: Wed, 9 Aug 2023 01:22:04 -0700 Subject: [PATCH 48/49] Avoid adding extra day to usage statistics --- app/src/server/api/routers/dashboard.router.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/server/api/routers/dashboard.router.ts b/app/src/server/api/routers/dashboard.router.ts index b18e1c0..ac11dbb 100644 --- a/app/src/server/api/routers/dashboard.router.ts +++ b/app/src/server/api/routers/dashboard.router.ts @@ -8,6 +8,7 @@ export const dashboardRouter = createTRPCRouter({ stats: publicProcedure .input( z.object({ + // TODO: actually take startDate into account startDate: z.string().optional(), organizationId: z.string(), }), @@ -33,7 +34,11 @@ export const dashboardRouter = createTRPCRouter({ let originalDataIndex = periods.length - 1; // *SLAMS DOWN GLASS OF WHISKEY* timezones, amirite? - let dayToMatch = dayjs(input.startDate || new Date()).add(1, "day"); + let dayToMatch = dayjs(input.startDate || new Date()) + // Ensure that the initial date we're matching against is never before the first period + if (periods[originalDataIndex] && dayToMatch.isBefore(periods[originalDataIndex]?.period, "day")) { + dayToMatch = dayjs(periods[originalDataIndex]?.period); + } const backfilledPeriods: typeof periods = []; // Backfill from now to 14 days ago or the date of the first logged call, whichever is earlier From f6f04e537e1f10f548def73d5d740437b8c26e8f Mon Sep 17 00:00:00 2001 From: Kyle Corbitt Date: Wed, 9 Aug 2023 14:21:05 -0700 Subject: [PATCH 49/49] project menu updates --- app/Dockerfile | 1 + app/src/components/nav/AppShell.tsx | 4 +- app/src/components/nav/ProjectMenu.tsx | 185 +++++++----------- .../projectSettings/DeleteProjectDialog.tsx | 2 +- app/src/env.mjs | 2 + app/src/pages/index.tsx | 2 +- app/src/pages/project/settings/index.tsx | 2 +- .../server/api/routers/dashboard.router.ts | 7 +- app/src/server/scripts/client-codegen.ts | 2 +- app/src/server/tasks/queryModel.task.ts | 2 +- app/src/utils/useSocket.ts | 1 - 11 files changed, 92 insertions(+), 118 deletions(-) diff --git a/app/Dockerfile b/app/Dockerfile index 044fe16..3ef59e3 100644 --- a/app/Dockerfile +++ b/app/Dockerfile @@ -23,6 +23,7 @@ ARG NEXT_PUBLIC_SOCKET_URL ARG NEXT_PUBLIC_HOST ARG NEXT_PUBLIC_SENTRY_DSN ARG SENTRY_AUTH_TOKEN +ARG NEXT_PUBLIC_FF_SHOW_LOGGED_CALLS WORKDIR /app COPY --from=deps /app/node_modules ./node_modules diff --git a/app/src/components/nav/AppShell.tsx b/app/src/components/nav/AppShell.tsx index db29c73..7070d1d 100644 --- a/app/src/components/nav/AppShell.tsx +++ b/app/src/components/nav/AppShell.tsx @@ -52,7 +52,9 @@ const NavSidebar = () => { <> - + {env.NEXT_PUBLIC_FF_SHOW_LOGGED_CALLS && ( + + )} {env.NEXT_PUBLIC_SHOW_DATA && ( diff --git a/app/src/components/nav/ProjectMenu.tsx b/app/src/components/nav/ProjectMenu.tsx index aa67a34..6e5852e 100644 --- a/app/src/components/nav/ProjectMenu.tsx +++ b/app/src/components/nav/ProjectMenu.tsx @@ -12,9 +12,8 @@ import { Button, useDisclosure, Spinner, - useBreakpointValue, } from "@chakra-ui/react"; -import React, { useCallback, useEffect, useState } from "react"; +import React, { useEffect, useState } from "react"; import Link from "next/link"; import { AiFillCaretDown } from "react-icons/ai"; import { BsGear, BsPlus } from "react-icons/bs"; @@ -44,8 +43,6 @@ export default function ProjectMenu() { const { data: selectedOrg } = useSelectedOrg(); - const [expandButtonHovered, setExpandButtonHovered] = useState(false); - const popover = useDisclosure(); const createMutation = api.organizations.create.useMutation(); @@ -56,113 +53,84 @@ export default function ProjectMenu() { await router.push({ pathname: "/project/settings" }); }, [createMutation, router]); - const openMenu = useCallback( - (event: React.MouseEvent) => { - event.preventDefault(); - popover.onToggle(); - }, - [popover], - ); - - const sidebarExpanded = useBreakpointValue({ base: false, md: true }); - return ( - <> - + - - - PROJECT - - - - - - - {selectedOrg?.name[0]?.toUpperCase()} - - - {selectedOrg?.name} - - - - } - size="xs" - colorScheme="gray" - color="gray.500" - variant="ghost" - mr={2} - borderRadius={4} - onMouseEnter={() => setExpandButtonHovered(true)} - onMouseLeave={() => setExpandButtonHovered(false)} - _hover={{ bgColor: isActive ? "gray.300" : "gray.200", transitionDelay: 0 }} - onClick={openMenu} - /> - - - - - - + + - - - PROJECTS - - - - {orgs?.map((org) => ( - - ))} - - - - New project + + + + {selectedOrg?.name[0]?.toUpperCase()} + + + {selectedOrg?.name} + + - - - - + + + + + PROJECTS + + + + {orgs?.map((org) => ( + + ))} + + + + New project + + + + + + ); } @@ -180,7 +148,7 @@ const ProjectOption = ({ return ( { setSelectedOrgId(org.id); onClose(); @@ -190,7 +158,6 @@ const ProjectOption = ({ bgColor={isActive ? "gray.100" : "transparent"} _hover={gearHovered ? undefined : { bgColor: "gray.200", textDecoration: "none" }} p={2} - borderRadius={4} > {org.name} { return { redirect: { - destination: "/logged-calls", + destination: "/experiments", permanent: false, }, }; diff --git a/app/src/pages/project/settings/index.tsx b/app/src/pages/project/settings/index.tsx index 1e8a184..6083e69 100644 --- a/app/src/pages/project/settings/index.tsx +++ b/app/src/pages/project/settings/index.tsx @@ -111,7 +111,7 @@ export default function Settings() { your code. - + {selectedOrg?.personalOrgUserId ? ( diff --git a/app/src/server/api/routers/dashboard.router.ts b/app/src/server/api/routers/dashboard.router.ts index ac11dbb..a97ce6f 100644 --- a/app/src/server/api/routers/dashboard.router.ts +++ b/app/src/server/api/routers/dashboard.router.ts @@ -34,9 +34,12 @@ export const dashboardRouter = createTRPCRouter({ let originalDataIndex = periods.length - 1; // *SLAMS DOWN GLASS OF WHISKEY* timezones, amirite? - let dayToMatch = dayjs(input.startDate || new Date()) + let dayToMatch = dayjs(input.startDate || new Date()); // Ensure that the initial date we're matching against is never before the first period - if (periods[originalDataIndex] && dayToMatch.isBefore(periods[originalDataIndex]?.period, "day")) { + if ( + periods[originalDataIndex] && + dayToMatch.isBefore(periods[originalDataIndex]?.period, "day") + ) { dayToMatch = dayjs(periods[originalDataIndex]?.period); } const backfilledPeriods: typeof periods = []; diff --git a/app/src/server/scripts/client-codegen.ts b/app/src/server/scripts/client-codegen.ts index 997df8c..78d1262 100644 --- a/app/src/server/scripts/client-codegen.ts +++ b/app/src/server/scripts/client-codegen.ts @@ -16,7 +16,7 @@ fs.writeFileSync(schemaPath, JSON.stringify(openApiDocument, null, 2), "utf-8"); console.log("Generating Typescript client"); -const tsClientPath = path.join(clientLibsPath, "js/codegen"); +const tsClientPath = path.join(clientLibsPath, "typescript/codegen"); fs.rmSync(tsClientPath, { recursive: true, force: true }); diff --git a/app/src/server/tasks/queryModel.task.ts b/app/src/server/tasks/queryModel.task.ts index 9b32d9d..d7a5dc8 100644 --- a/app/src/server/tasks/queryModel.task.ts +++ b/app/src/server/tasks/queryModel.task.ts @@ -1,5 +1,5 @@ import { type Prisma } from "@prisma/client"; -import { JsonValue, type JsonObject } from "type-fest"; +import { type JsonValue, type JsonObject } from "type-fest"; import modelProviders from "~/modelProviders/modelProviders"; import { prisma } from "~/server/db"; import { wsConnection } from "~/utils/wsConnection"; diff --git a/app/src/utils/useSocket.ts b/app/src/utils/useSocket.ts index ba69387..bbefa4c 100644 --- a/app/src/utils/useSocket.ts +++ b/app/src/utils/useSocket.ts @@ -11,7 +11,6 @@ export default function useSocket(channel?: string | null) { useEffect(() => { if (!channel) return; - console.log("connecting to channel", channel); // Create websocket connection socketRef.current = io(url);